TorbatYar/backend/services/experience/app/specifications/components.py
Mortezakoohjani 203671a7bf feat(experience): ship Experience Platform phases 11.0-11.10
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>
2026-07-27 11:43:10 +03:30

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)