refactor: migrate to DDD architecture with Dishka DI
Complete architectural refactoring from simple MVC to Clean Architecture/DDD pattern: Domain Layer: - Add entities (Post, BaseEntity) with business logic - Add value objects (Title, Content, Slug) with validation - Add repository interfaces (PostRepository) - Add domain exceptions Application Layer: - Add use cases (CreatePost, GetPost, UpdatePost, DeletePost, ListPosts, PublishPost) - Add DTOs for data transfer - Add TransactionManager interface Infrastructure Layer: - Add SQLAlchemy models and async database connection - Add SQLAlchemyPostRepository implementation - Add Dishka DI container with providers - Add error handlers and middleware Presentation Layer: - Add FastAPI routes with Dishka integration - Add Pydantic schemas - Add dependency injection using FromDishka[T] Other Changes: - Remove old flat structure (api/, common/, core/, modules/) - Add hatchling build system for package scripts - Add blog CLI command - Update AGENTS.md with new architecture docs - All 48 tests passing, mypy clean, ruff clean
This commit is contained in:
211
app/presentation/api/v1/posts.py
Normal file
211
app/presentation/api/v1/posts.py
Normal file
@@ -0,0 +1,211 @@
|
||||
"""Posts API routes."""
|
||||
|
||||
from uuid import UUID
|
||||
|
||||
from dishka.integrations.fastapi import DishkaRoute
|
||||
from fastapi import APIRouter, status
|
||||
|
||||
from app.application.dtos import CreatePostDTO, UpdatePostDTO
|
||||
from app.presentation.api.deps import (
|
||||
CreatePostDep,
|
||||
CurrentUserDep,
|
||||
DeletePostDep,
|
||||
GetPostDep,
|
||||
ListPostsDep,
|
||||
PublishPostDep,
|
||||
UpdatePostDep,
|
||||
)
|
||||
from app.presentation.schemas import (
|
||||
PostCreateSchema,
|
||||
PostListResponseSchema,
|
||||
PostResponseSchema,
|
||||
PostUpdateSchema,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/posts", tags=["posts"], route_class=DishkaRoute)
|
||||
|
||||
|
||||
@router.post(
|
||||
"",
|
||||
response_model=PostResponseSchema,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
summary="Create a new post",
|
||||
)
|
||||
async def create_post(
|
||||
schema: PostCreateSchema,
|
||||
use_case: CreatePostDep,
|
||||
current_user_id: CurrentUserDep,
|
||||
) -> PostResponseSchema:
|
||||
"""Create a new blog post."""
|
||||
dto = CreatePostDTO(
|
||||
title=schema.title,
|
||||
content=schema.content,
|
||||
author_id=current_user_id,
|
||||
tags=schema.tags,
|
||||
)
|
||||
result = await use_case.execute(dto)
|
||||
return PostResponseSchema(**result.__dict__)
|
||||
|
||||
|
||||
@router.get(
|
||||
"",
|
||||
response_model=PostListResponseSchema,
|
||||
summary="List all posts",
|
||||
)
|
||||
async def list_posts(use_case: ListPostsDep) -> PostListResponseSchema:
|
||||
"""Get all blog posts."""
|
||||
results = await use_case.all_posts()
|
||||
items = [PostResponseSchema(**r.__dict__) for r in results]
|
||||
return PostListResponseSchema(items=items, total=len(items))
|
||||
|
||||
|
||||
@router.get(
|
||||
"/published",
|
||||
response_model=PostListResponseSchema,
|
||||
summary="List published posts",
|
||||
)
|
||||
async def list_published_posts(
|
||||
use_case: ListPostsDep,
|
||||
) -> PostListResponseSchema:
|
||||
"""Get all published blog posts."""
|
||||
results = await use_case.published_posts()
|
||||
items = [PostResponseSchema(**r.__dict__) for r in results]
|
||||
return PostListResponseSchema(items=items, total=len(items))
|
||||
|
||||
|
||||
@router.get(
|
||||
"/search",
|
||||
response_model=PostListResponseSchema,
|
||||
summary="Search posts",
|
||||
)
|
||||
async def search_posts(
|
||||
query: str,
|
||||
use_case: ListPostsDep,
|
||||
) -> PostListResponseSchema:
|
||||
"""Search posts by query."""
|
||||
results = await use_case.search(query)
|
||||
items = [PostResponseSchema(**r.__dict__) for r in results]
|
||||
return PostListResponseSchema(items=items, total=len(items))
|
||||
|
||||
|
||||
@router.get(
|
||||
"/by-tag/{tag}",
|
||||
response_model=PostListResponseSchema,
|
||||
summary="Get posts by tag",
|
||||
)
|
||||
async def get_posts_by_tag(
|
||||
tag: str,
|
||||
use_case: ListPostsDep,
|
||||
) -> PostListResponseSchema:
|
||||
"""Get posts by tag."""
|
||||
results = await use_case.by_tag(tag)
|
||||
items = [PostResponseSchema(**r.__dict__) for r in results]
|
||||
return PostListResponseSchema(items=items, total=len(items))
|
||||
|
||||
|
||||
@router.get(
|
||||
"/by-author/{author_id}",
|
||||
response_model=PostListResponseSchema,
|
||||
summary="Get posts by author",
|
||||
)
|
||||
async def get_posts_by_author(
|
||||
author_id: str,
|
||||
use_case: ListPostsDep,
|
||||
) -> PostListResponseSchema:
|
||||
"""Get posts by author."""
|
||||
results = await use_case.by_author(author_id)
|
||||
items = [PostResponseSchema(**r.__dict__) for r in results]
|
||||
return PostListResponseSchema(items=items, total=len(items))
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{post_id}",
|
||||
response_model=PostResponseSchema,
|
||||
summary="Get post by ID",
|
||||
)
|
||||
async def get_post(
|
||||
post_id: UUID,
|
||||
use_case: GetPostDep,
|
||||
) -> PostResponseSchema:
|
||||
"""Get a post by its ID."""
|
||||
result = await use_case.by_id(post_id)
|
||||
return PostResponseSchema(**result.__dict__)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/slug/{slug}",
|
||||
response_model=PostResponseSchema,
|
||||
summary="Get post by slug",
|
||||
)
|
||||
async def get_post_by_slug(
|
||||
slug: str,
|
||||
use_case: GetPostDep,
|
||||
) -> PostResponseSchema:
|
||||
"""Get a post by its slug."""
|
||||
result = await use_case.by_slug(slug)
|
||||
return PostResponseSchema(**result.__dict__)
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/{post_id}",
|
||||
response_model=PostResponseSchema,
|
||||
summary="Update post",
|
||||
)
|
||||
async def update_post(
|
||||
post_id: UUID,
|
||||
schema: PostUpdateSchema,
|
||||
use_case: UpdatePostDep,
|
||||
current_user_id: CurrentUserDep,
|
||||
) -> PostResponseSchema:
|
||||
"""Update a post."""
|
||||
dto = UpdatePostDTO(
|
||||
title=schema.title,
|
||||
content=schema.content,
|
||||
tags=schema.tags,
|
||||
)
|
||||
result = await use_case.execute(post_id, dto, current_user_id)
|
||||
return PostResponseSchema(**result.__dict__)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/{post_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
summary="Delete post",
|
||||
)
|
||||
async def delete_post(
|
||||
post_id: UUID,
|
||||
use_case: DeletePostDep,
|
||||
current_user_id: CurrentUserDep,
|
||||
) -> None:
|
||||
"""Delete a post."""
|
||||
await use_case.execute(post_id, current_user_id)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{post_id}/publish",
|
||||
response_model=PostResponseSchema,
|
||||
summary="Publish post",
|
||||
)
|
||||
async def publish_post(
|
||||
post_id: UUID,
|
||||
use_case: PublishPostDep,
|
||||
current_user_id: CurrentUserDep,
|
||||
) -> PostResponseSchema:
|
||||
"""Publish a post."""
|
||||
result = await use_case.publish(post_id, current_user_id)
|
||||
return PostResponseSchema(**result.__dict__)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{post_id}/unpublish",
|
||||
response_model=PostResponseSchema,
|
||||
summary="Unpublish post",
|
||||
)
|
||||
async def unpublish_post(
|
||||
post_id: UUID,
|
||||
use_case: PublishPostDep,
|
||||
current_user_id: CurrentUserDep,
|
||||
) -> PostResponseSchema:
|
||||
"""Unpublish a post."""
|
||||
result = await use_case.unpublish(post_id, current_user_id)
|
||||
return PostResponseSchema(**result.__dict__)
|
||||
Reference in New Issue
Block a user