"""SQLAlchemy implementation of PostRepository. This module provides the concrete implementation of PostRepository using SQLAlchemy ORM for data persistence. """ from uuid import UUID from sqlalchemy import or_, select from sqlalchemy.ext.asyncio import AsyncSession from app.domain.entities import Post from app.domain.entities.like import PostLike from app.domain.repositories import PostRepository from app.domain.value_objects import Content, Slug, Title from app.infrastructure.database.models import PostLikeORM, PostORM class SQLAlchemyPostRepository(PostRepository): """SQLAlchemy implementation of Post repository. Provides data access methods for Post entities using SQLAlchemy ORM. Handles conversion between domain entities and ORM models. Attributes: _session: SQLAlchemy async session for database operations. Example: >>> repo = SQLAlchemyPostRepository(session) >>> post = await repo.get_by_id(post_id) """ def __init__(self, session: AsyncSession) -> None: """Initialize repository with session. Args: session: SQLAlchemy async session instance. """ self._session = session def _to_domain(self, orm: PostORM) -> Post: """Convert ORM model to domain entity. Args: orm: SQLAlchemy ORM model instance. Returns: Domain Post entity with validated value objects. """ return Post( id=UUID(orm.id), title=Title(orm.title), content=Content(orm.content), slug=Slug(orm.slug), author_id=orm.author_id, published=orm.published, like_count=orm.like_count, tags=orm.tags or [], created_at=orm.created_at, updated_at=orm.updated_at, ) def _to_orm(self, post: Post) -> PostORM: """Convert domain entity to ORM model. Args: post: Domain Post entity. Returns: SQLAlchemy ORM model instance. """ return PostORM( id=str(post.id), title=post.title.value, content=post.content.value, slug=post.slug.value, author_id=post.author_id, published=post.published, like_count=post.like_count, tags=post.tags, created_at=post.created_at, updated_at=post.updated_at, ) async def get_by_id(self, entity_id: UUID) -> Post | None: """Get post by ID. Args: entity_id: Unique identifier of the post. Returns: Post entity if found, None otherwise. """ result = await self._session.execute(select(PostORM).where(PostORM.id == str(entity_id))) orm = result.scalar_one_or_none() return self._to_domain(orm) if orm else None async def get_all(self) -> list[Post]: """Get all posts. Returns: List of all Post entities. """ result = await self._session.execute(select(PostORM)) orms = result.scalars().all() return [self._to_domain(orm) for orm in orms] async def add(self, entity: Post) -> None: """Add new post. Args: entity: Post entity to add. """ orm = self._to_orm(entity) self._session.add(orm) async def update(self, entity: Post) -> None: """Update existing post. Args: entity: Post entity with updated data. """ result = await self._session.execute(select(PostORM).where(PostORM.id == str(entity.id))) orm = result.scalar_one() orm.title = entity.title.value orm.content = entity.content.value orm.slug = entity.slug.value orm.published = entity.published orm.like_count = entity.like_count orm.tags = entity.tags orm.updated_at = entity.updated_at async def delete(self, entity_id: UUID) -> None: """Delete post by ID. Args: entity_id: Unique identifier of the post to delete. """ result = await self._session.execute(select(PostORM).where(PostORM.id == str(entity_id))) orm = result.scalar_one_or_none() if orm: await self._session.delete(orm) async def exists(self, entity_id: UUID) -> bool: """Check if post exists. Args: entity_id: Unique identifier of the post. Returns: True if post exists, False otherwise. """ result = await self._session.execute(select(PostORM).where(PostORM.id == str(entity_id))) return result.scalar_one_or_none() is not None async def get_by_slug(self, slug: str) -> Post | None: """Get post by slug. Args: slug: URL-friendly slug identifier. Returns: Post entity if found, None otherwise. """ result = await self._session.execute(select(PostORM).where(PostORM.slug == slug)) orm = result.scalar_one_or_none() return self._to_domain(orm) if orm else None async def get_by_author( self, author_id: str, limit: int | None = None, offset: int | None = None, ) -> list[Post]: """Get posts by author. Args: author_id: Identifier of the author. limit: Maximum number of posts to return. offset: Number of posts to skip. Returns: List of Post entities by the author. """ query = ( select(PostORM) .where(PostORM.author_id == author_id) .order_by(PostORM.created_at.desc()) ) if limit is not None: query = query.limit(limit) if offset is not None: query = query.offset(offset) result = await self._session.execute(query) orms = result.scalars().all() return [self._to_domain(orm) for orm in orms] async def get_published( self, limit: int | None = None, offset: int | None = None, ) -> list[Post]: """Get published posts. Args: limit: Maximum number of posts to return. offset: Number of posts to skip. Returns: List of published Post entities. """ query = ( select(PostORM).where(PostORM.published.is_(True)).order_by(PostORM.created_at.desc()) ) if limit is not None: query = query.limit(limit) if offset is not None: query = query.offset(offset) result = await self._session.execute(query) orms = result.scalars().all() return [self._to_domain(orm) for orm in orms] async def get_by_tag( self, tag: str, limit: int | None = None, offset: int | None = None, ) -> list[Post]: """Get posts by tag. Args: tag: Tag to filter by. limit: Maximum number of posts to return. offset: Number of posts to skip. Returns: List of Post entities with the tag. """ query = select(PostORM).where(PostORM.tags.contains([tag])) if limit is not None: query = query.limit(limit) if offset is not None: query = query.offset(offset) result = await self._session.execute(query) orms = result.scalars().all() return [self._to_domain(orm) for orm in orms] async def slug_exists(self, slug: str) -> bool: """Check if slug exists. Args: slug: Slug to check for existence. Returns: True if slug exists, False otherwise. """ result = await self._session.execute(select(PostORM).where(PostORM.slug == slug)) return result.scalar_one_or_none() is not None async def search( self, query: str, limit: int | None = None, offset: int | None = None, ) -> list[Post]: """Search posts. Args: query: Search query string. limit: Maximum number of posts to return. offset: Number of posts to skip. Returns: List of Post entities matching the query. """ search_pattern = f"%{query}%" stmt = select(PostORM).where( or_( PostORM.title.ilike(search_pattern), PostORM.content.ilike(search_pattern), ) ) if limit is not None: stmt = stmt.limit(limit) if offset is not None: stmt = stmt.offset(offset) result = await self._session.execute(stmt) orms = result.scalars().all() return [self._to_domain(orm) for orm in orms] async def get_like(self, post_id: UUID, liked_by: str) -> PostLike | None: """Get a like by post and user/device. Args: post_id: UUID of the post. liked_by: User ID or device ID. Returns: PostLike if found, None otherwise. """ result = await self._session.execute( select(PostLikeORM).where( PostLikeORM.post_id == str(post_id), PostLikeORM.liked_by == liked_by, ) ) orm = result.scalar_one_or_none() if not orm: return None return PostLike( id=UUID(orm.id), post_id=UUID(orm.post_id), liked_by=orm.liked_by, created_at=orm.created_at, ) async def add_like(self, like: PostLike) -> None: """Add a new like. Args: like: PostLike entity to add. """ orm = PostLikeORM( id=str(like.id), post_id=str(like.post_id), liked_by=like.liked_by, created_at=like.created_at, ) self._session.add(orm) async def remove_like(self, post_id: UUID, liked_by: str) -> None: """Remove a like by post and user/device. Args: post_id: UUID of the post. liked_by: User ID or device ID. """ result = await self._session.execute( select(PostLikeORM).where( PostLikeORM.post_id == str(post_id), PostLikeORM.liked_by == liked_by, ) ) orm = result.scalar_one_or_none() if orm: await self._session.delete(orm)