Add the experience service with sites through analytics/AI hooks, migrations through 0011, and phase docs/manifests marking the track complete. Co-authored-by: Cursor <cursoragent@cursor.com>
156 lines
5.5 KiB
Python
156 lines
5.5 KiB
Python
"""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)
|