"""Component query specifications — Phase 11.2.""" from __future__ import annotations from dataclasses import dataclass from uuid import UUID from sqlalchemy import Select, asc, desc, or_ from sqlalchemy.sql import ColumnElement from app.models.components import ( ExperienceComponent, ExperienceComponentVersion, PageComponentPlacement, ) from app.models.types import ( ComponentKind, ComponentStatus, ComponentVersionStatus, ) COMPONENT_SORT_FIELDS = frozenset( {"created_at", "updated_at", "code", "name", "kind", "status", "category"} ) VERSION_SORT_FIELDS = frozenset( {"created_at", "updated_at", "version_number", "status", "published_at"} ) PLACEMENT_SORT_FIELDS = frozenset( {"created_at", "updated_at", "sort_order", "slot_key"} ) @dataclass(frozen=True, slots=True) class ComponentListSpec: workspace_id: UUID | None = None kind: ComponentKind | str | None = None status: ComponentStatus | str | None = None category: str | None = None q: str | None = None sort_by: str = "created_at" sort_dir: str = "desc" def filter_clauses(self) -> list[ColumnElement]: clauses: list[ColumnElement] = [] if self.workspace_id is not None: clauses.append(ExperienceComponent.workspace_id == self.workspace_id) if self.kind is not None: kind = ( self.kind if isinstance(self.kind, ComponentKind) else ComponentKind(str(self.kind)) ) clauses.append(ExperienceComponent.kind == kind) if self.status is not None: status = ( self.status if isinstance(self.status, ComponentStatus) else ComponentStatus(str(self.status)) ) clauses.append(ExperienceComponent.status == status) if self.category: clauses.append(ExperienceComponent.category == self.category) if self.q: term = f"%{self.q.strip()}%" clauses.append( or_( ExperienceComponent.code.ilike(term), ExperienceComponent.name.ilike(term), ExperienceComponent.description.ilike(term), ExperienceComponent.category.ilike(term), ) ) return clauses def apply(self, stmt: Select) -> Select: clauses = self.filter_clauses() if clauses: stmt = stmt.where(*clauses) sort_key = ( self.sort_by if self.sort_by in COMPONENT_SORT_FIELDS else "created_at" ) column = getattr(ExperienceComponent, sort_key) order = desc(column) if self.sort_dir.lower() != "asc" else asc(column) return stmt.order_by(order) @dataclass(frozen=True, slots=True) class ComponentVersionListSpec: component_id: UUID | None = None status: ComponentVersionStatus | str | None = None q: str | None = None sort_by: str = "version_number" sort_dir: str = "desc" def filter_clauses(self) -> list[ColumnElement]: clauses: list[ColumnElement] = [] if self.component_id is not None: clauses.append( ExperienceComponentVersion.component_id == self.component_id ) if self.status is not None: status = ( self.status if isinstance(self.status, ComponentVersionStatus) else ComponentVersionStatus(str(self.status)) ) clauses.append(ExperienceComponentVersion.status == status) if self.q: term = f"%{self.q.strip()}%" clauses.append(ExperienceComponentVersion.changelog.ilike(term)) return clauses def apply(self, stmt: Select) -> Select: clauses = self.filter_clauses() if clauses: stmt = stmt.where(*clauses) sort_key = ( self.sort_by if self.sort_by in VERSION_SORT_FIELDS else "version_number" ) column = getattr(ExperienceComponentVersion, sort_key) order = desc(column) if self.sort_dir.lower() != "asc" else asc(column) return stmt.order_by(order) @dataclass(frozen=True, slots=True) class PlacementListSpec: page_id: UUID | None = None site_id: UUID | None = None component_id: UUID | None = None q: str | None = None sort_by: str = "sort_order" sort_dir: str = "asc" def filter_clauses(self) -> list[ColumnElement]: clauses: list[ColumnElement] = [] if self.page_id is not None: clauses.append(PageComponentPlacement.page_id == self.page_id) if self.site_id is not None: clauses.append(PageComponentPlacement.site_id == self.site_id) if self.component_id is not None: clauses.append(PageComponentPlacement.component_id == self.component_id) if self.q: term = f"%{self.q.strip()}%" clauses.append(PageComponentPlacement.slot_key.ilike(term)) return clauses def apply(self, stmt: Select) -> Select: clauses = self.filter_clauses() if clauses: stmt = stmt.where(*clauses) sort_key = ( self.sort_by if self.sort_by in PLACEMENT_SORT_FIELDS else "sort_order" ) column = getattr(PageComponentPlacement, sort_key) order = desc(column) if self.sort_dir.lower() != "asc" else asc(column) return stmt.order_by(order)