"""Component library APIs — Phase 11.2.""" from __future__ import annotations from uuid import UUID from fastapi import APIRouter, Depends, Query, status from sqlalchemy.ext.asyncio import AsyncSession from app.api.deps import get_db, get_pagination, require_tenant from app.api.permissions import require_permissions from app.commands.components import ComponentCommands from app.permissions.definitions import ( COMPONENTS_CREATE, COMPONENTS_DELETE, COMPONENTS_MANAGE, COMPONENTS_UPDATE, COMPONENTS_VIEW, ) from app.queries.components import ComponentQueries from app.schemas.components import ( ExperienceComponentCreate, ExperienceComponentListResponse, ExperienceComponentRead, ExperienceComponentUpdate, ) from app.specifications.components import COMPONENT_SORT_FIELDS, ComponentListSpec from shared.pagination import PaginationParams from shared.security import CurrentUser router = APIRouter() _COMPONENT_KINDS = { "layout_block", "content", "media", "navigation", "form_shell", "widget_shell", "cta", "hero", "menu_ref", "catalog_ref", "custom", } _COMPONENT_STATUSES = {"draft", "active", "deprecated", "archived"} def _list_spec( workspace_id: UUID | None = Query(default=None), kind: str | None = Query(default=None), status_filter: str | None = Query(default=None, alias="status"), category: str | None = Query(default=None, max_length=100), q: str | None = Query(default=None, max_length=100), sort_by: str = Query(default="created_at"), sort_dir: str = Query(default="desc", pattern="^(?i)(asc|desc)$"), ) -> ComponentListSpec: if sort_by not in COMPONENT_SORT_FIELDS: sort_by = "created_at" kind_value = kind if kind in _COMPONENT_KINDS else None status_value = status_filter if status_filter in _COMPONENT_STATUSES else None return ComponentListSpec( workspace_id=workspace_id, kind=kind_value, # type: ignore[arg-type] status=status_value, # type: ignore[arg-type] category=category, q=q, sort_by=sort_by, sort_dir=sort_dir.lower(), ) @router.post( "", response_model=ExperienceComponentRead, status_code=status.HTTP_201_CREATED ) async def create_component( body: ExperienceComponentCreate, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), user: CurrentUser = Depends(require_permissions(COMPONENTS_CREATE)), ): return await ComponentCommands(db).create_component(tenant_id, body, actor=user) @router.get("", response_model=ExperienceComponentListResponse) async def list_components( tenant_id: UUID = Depends(require_tenant), pagination: PaginationParams = Depends(get_pagination), spec: ComponentListSpec = Depends(_list_spec), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(require_permissions(COMPONENTS_VIEW)), ): items, total = await ComponentQueries(db).list_components( tenant_id, offset=pagination.offset, limit=pagination.page_size, spec=spec, ) return ExperienceComponentListResponse( items=items, total=total, page=pagination.page, page_size=pagination.page_size, ) @router.get("/{component_id}", response_model=ExperienceComponentRead) async def get_component( component_id: UUID, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(require_permissions(COMPONENTS_VIEW)), ): return await ComponentQueries(db).get_component(tenant_id, component_id) @router.patch("/{component_id}", response_model=ExperienceComponentRead) async def update_component( component_id: UUID, body: ExperienceComponentUpdate, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), user: CurrentUser = Depends(require_permissions(COMPONENTS_UPDATE)), ): return await ComponentCommands(db).update_component( tenant_id, component_id, body, actor=user ) @router.post("/{component_id}/activate", response_model=ExperienceComponentRead) async def activate_component( component_id: UUID, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), user: CurrentUser = Depends(require_permissions(COMPONENTS_MANAGE)), ): return await ComponentCommands(db).activate_component( tenant_id, component_id, actor=user ) @router.post("/{component_id}/deprecate", response_model=ExperienceComponentRead) async def deprecate_component( component_id: UUID, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), user: CurrentUser = Depends(require_permissions(COMPONENTS_MANAGE)), ): return await ComponentCommands(db).deprecate_component( tenant_id, component_id, actor=user ) @router.post("/{component_id}/archive", response_model=ExperienceComponentRead) async def archive_component( component_id: UUID, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), user: CurrentUser = Depends(require_permissions(COMPONENTS_MANAGE)), ): return await ComponentCommands(db).archive_component( tenant_id, component_id, actor=user ) @router.post("/{component_id}/delete", response_model=ExperienceComponentRead) async def soft_delete_component( component_id: UUID, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), user: CurrentUser = Depends(require_permissions(COMPONENTS_DELETE)), ): return await ComponentCommands(db).soft_delete_component( tenant_id, component_id, actor=user )