Saltar a contenido

CRUD básico

Ejemplo completo: recurso Product con CRUD + soft delete + scoping por company.

Modelo

# app/models/product.py
from uuid import UUID

from sqlalchemy import Boolean, Float, ForeignKey, String
from sqlalchemy.orm import Mapped, mapped_column

from app.models.base import BaseModel
from app.models.types import GUID


class Product(BaseModel):
    __tablename__ = "products"

    company_id: Mapped[UUID] = mapped_column(
        GUID(), ForeignKey("companies.id", ondelete="CASCADE"), nullable=False, index=True
    )
    name: Mapped[str] = mapped_column(String(200), nullable=False)
    sku: Mapped[str] = mapped_column(String(50), unique=True, nullable=False, index=True)
    price: Mapped[float] = mapped_column(Float, nullable=False)
    is_active: Mapped[bool] = mapped_column(Boolean, server_default="true", nullable=False)

Schemas

# app/schemas/product.py
import uuid
from datetime import datetime
from typing import Optional

from pydantic import BaseModel, ConfigDict, Field

from app.schemas.base import BaseSchema


class ProductCreateSchema(BaseModel):
    name: str = Field(..., min_length=1, max_length=200)
    sku: str = Field(..., min_length=1, max_length=50)
    price: float = Field(..., gt=0)
    is_active: bool = True
    model_config = ConfigDict(extra="ignore")


class ProductUpdateSchema(BaseModel):
    name: Optional[str] = Field(None, max_length=200)
    price: Optional[float] = Field(None, gt=0)
    is_active: Optional[bool] = None
    model_config = ConfigDict(extra="ignore")


class ProductResponseSchema(BaseSchema):
    id: uuid.UUID
    company_id: uuid.UUID
    name: str
    sku: str
    price: float
    is_active: bool
    created_at: datetime
    updated_at: datetime

Repo + service + factory + controller

# app/repositories/product/repository.py
from typing import Any
from fastapi_basekit.aio.sqlalchemy.repository.base import BaseRepository
from sqlalchemy import select
from app.models.product import Product


class ProductRepository(BaseRepository):
    model = Product

    def build_list_queryset(self, **kwargs: Any):
        return select(self.model).where(self.model.deleted_at.is_(None))
# app/services/product_service.py
from typing import Optional
from fastapi import Request
from fastapi_basekit.aio.sqlalchemy.service.base import BaseService
from sqlalchemy.ext.asyncio import AsyncSession
from app.repositories.product.repository import ProductRepository


class ProductService(BaseService):
    repository: ProductRepository
    search_fields = ["name", "sku"]
    duplicate_check_fields = ["sku"]

    def __init__(self, repository, request=None, session=None):
        super().__init__(repository, request=request)
        self.repository = repository
        self.session = session

    def get_filters(self, filters: Optional[dict] = None) -> dict:
        filters = filters or {}
        user = getattr(self.request.state, "user", None)
        if user and user.company_id and not user.is_platform_admin:
            filters["company_id"] = user.company_id
        return filters

    async def create(self, payload, check_fields=None):
        data = payload.model_dump() if hasattr(payload, "model_dump") else dict(payload)
        user = getattr(self.request.state, "user", None)
        if user and user.company_id:
            data["company_id"] = user.company_id
        return await super().create(data, check_fields)
# app/services/dependency.py — añadir
def get_product_service(request, session=Depends(get_db)):
    from app.repositories.product.repository import ProductRepository
    from app.services.product_service import ProductService
    return ProductService(ProductRepository(session), request=request, session=session)
# app/api/v1/endpoints/product/product.py
import uuid
from typing import Optional

from fastapi import APIRouter, Depends, Query, status
from fastapi_basekit.aio.sqlalchemy.controller.base import SQLAlchemyBaseController
from fastapi_basekit.schema.base import BasePaginationResponse, BaseResponse
from fastapi_restful.cbv import cbv

from app.models.auth import Users
from app.schemas.product import (
    ProductCreateSchema,
    ProductResponseSchema,
    ProductUpdateSchema,
)
from app.services.dependency import get_dependency_service, get_product_service
from app.services.product_service import ProductService

router = APIRouter(prefix="/products")


@cbv(router)
class ProductController(SQLAlchemyBaseController):
    service: ProductService = Depends(get_product_service)
    schema_class = ProductResponseSchema
    user: Users = Depends(get_dependency_service)

    @router.get("/", response_model=BasePaginationResponse[ProductResponseSchema])
    async def list_products(
        self,
        page: int = Query(1, ge=1),
        count: int = Query(10, ge=1, le=100),
        search: Optional[str] = Query(None),
    ):
        return await self.list()

    @router.post("/", response_model=BaseResponse[ProductResponseSchema], status_code=201)
    async def create_product(self, data: ProductCreateSchema):
        created = await self.service.create(data)
        return self.format_response(ProductResponseSchema.model_validate(created), message="Created")

    @router.get("/{product_id}", response_model=BaseResponse[ProductResponseSchema])
    async def get_product(self, product_id: uuid.UUID):
        return await self.retrieve(product_id)

    @router.put("/{product_id}", response_model=BaseResponse[ProductResponseSchema])
    async def update_product(self, product_id: uuid.UUID, data: ProductUpdateSchema):
        updated = await self.service.update(str(product_id), data.model_dump(exclude_unset=True))
        return self.format_response(ProductResponseSchema.model_validate(updated), message="Updated")

    @router.delete("/{product_id}", status_code=status.HTTP_204_NO_CONTENT)
    async def delete_product(self, product_id: uuid.UUID):
        return await self.delete(product_id)

Test

make migrate-create   # add_products_table
make migrate-up

curl -X POST http://localhost:8000/api/v1/products/ \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"Widget","sku":"WGT-001","price":29.99}'

curl http://localhost:8000/api/v1/products/?page=1&count=20&search=widget \
  -H "Authorization: Bearer $TOKEN"