""" Service layer for BlogPost CRUD operations. """ import math import nh3 from typing import Optional from sqlalchemy import select, func from sqlalchemy.ext.asyncio import AsyncSession from app.models.post import BlogPost from app.schemas.post import PostCreate, PostUpdate, PaginatedPostsResponse, PostResponse def _sanitize_body(body: str) -> str: """Strip dangerous HTML tags/attributes using nh3.""" return nh3.clean(body) async def get_published_posts( db: AsyncSession, page: int = 1, per_page: int = 10 ) -> PaginatedPostsResponse: offset = (page - 1) * per_page count_result = await db.execute( select(func.count()).select_from(BlogPost).where(BlogPost.published == True) ) total = count_result.scalar_one() result = await db.execute( select(BlogPost) .where(BlogPost.published == True) .order_by(BlogPost.created_at.desc()) .offset(offset) .limit(per_page) ) items = list(result.scalars().all()) total_pages = math.ceil(total / per_page) if per_page > 0 else 0 return PaginatedPostsResponse( items=[PostResponse.model_validate(p) for p in items], total=total, page=page, per_page=per_page, total_pages=total_pages, ) async def get_post_by_slug( db: AsyncSession, slug: str, published_only: bool = True ) -> Optional[BlogPost]: stmt = select(BlogPost).where(BlogPost.slug == slug) if published_only: stmt = stmt.where(BlogPost.published == True) result = await db.execute(stmt) return result.scalars().first() async def create_post(db: AsyncSession, data: PostCreate) -> BlogPost: post = BlogPost( title=data.title, slug=data.slug, excerpt=data.excerpt, body=_sanitize_body(data.body), author=data.author, featured_image_url=data.featured_image_url, tags=data.tags, published=data.published, ) db.add(post) await db.flush() await db.refresh(post) return post async def update_post(db: AsyncSession, slug: str, data: PostUpdate) -> Optional[BlogPost]: post = await get_post_by_slug(db, slug, published_only=False) if post is None: return None update_data = data.model_dump(exclude_unset=True) if "body" in update_data and update_data["body"] is not None: update_data["body"] = _sanitize_body(update_data["body"]) for field, value in update_data.items(): setattr(post, field, value) await db.flush() await db.refresh(post) return post async def delete_post(db: AsyncSession, slug: str) -> bool: post = await get_post_by_slug(db, slug, published_only=False) if post is None: return False await db.delete(post) await db.flush() return True