"""Theme & layout query specifications — Phase 11.3 (versioned).""" 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.themes import ( ExperienceLayout, ExperienceLayoutVersion, ExperienceTheme, ExperienceThemeVersion, PageLayoutBinding, SiteThemeBinding, ) from app.models.types import ( DirectionMode, LayoutKind, LayoutStatus, LayoutVersionStatus, ThemeStatus, ThemeVersionStatus, ) THEME_SORT_FIELDS = frozenset( {"created_at", "updated_at", "code", "name", "status", "direction_mode"} ) THEME_VERSION_SORT_FIELDS = frozenset( {"created_at", "updated_at", "version_number", "status"} ) LAYOUT_SORT_FIELDS = frozenset( {"created_at", "updated_at", "code", "name", "status", "direction_mode", "kind"} ) LAYOUT_VERSION_SORT_FIELDS = frozenset( {"created_at", "updated_at", "version_number", "status"} ) SITE_THEME_BINDING_SORT_FIELDS = frozenset({"created_at", "updated_at"}) PAGE_LAYOUT_BINDING_SORT_FIELDS = frozenset({"created_at", "updated_at"}) @dataclass(frozen=True, slots=True) class ThemeListSpec: workspace_id: UUID | None = None status: ThemeStatus | str | None = None direction_mode: DirectionMode | 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(ExperienceTheme.workspace_id == self.workspace_id) if self.status is not None: status = ( self.status if isinstance(self.status, ThemeStatus) else ThemeStatus(str(self.status)) ) clauses.append(ExperienceTheme.status == status) if self.direction_mode is not None: mode = ( self.direction_mode if isinstance(self.direction_mode, DirectionMode) else DirectionMode(str(self.direction_mode)) ) clauses.append(ExperienceTheme.direction_mode == mode) if self.q: term = f"%{self.q.strip()}%" clauses.append( or_( ExperienceTheme.code.ilike(term), ExperienceTheme.name.ilike(term), ExperienceTheme.description.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 THEME_SORT_FIELDS else "created_at" column = getattr(ExperienceTheme, 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 ThemeVersionListSpec: theme_id: UUID | None = None status: ThemeVersionStatus | str | None = None sort_by: str = "version_number" sort_dir: str = "desc" def filter_clauses(self) -> list[ColumnElement]: clauses: list[ColumnElement] = [] if self.theme_id is not None: clauses.append(ExperienceThemeVersion.theme_id == self.theme_id) if self.status is not None: status = ( self.status if isinstance(self.status, ThemeVersionStatus) else ThemeVersionStatus(str(self.status)) ) clauses.append(ExperienceThemeVersion.status == status) 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 THEME_VERSION_SORT_FIELDS else "version_number" ) column = getattr(ExperienceThemeVersion, 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 LayoutListSpec: workspace_id: UUID | None = None status: LayoutStatus | str | None = None direction_mode: DirectionMode | str | None = None kind: LayoutKind | 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(ExperienceLayout.workspace_id == self.workspace_id) if self.status is not None: status = ( self.status if isinstance(self.status, LayoutStatus) else LayoutStatus(str(self.status)) ) clauses.append(ExperienceLayout.status == status) if self.direction_mode is not None: mode = ( self.direction_mode if isinstance(self.direction_mode, DirectionMode) else DirectionMode(str(self.direction_mode)) ) clauses.append(ExperienceLayout.direction_mode == mode) if self.kind is not None: kind = ( self.kind if isinstance(self.kind, LayoutKind) else LayoutKind(str(self.kind)) ) clauses.append(ExperienceLayout.kind == kind) if self.q: term = f"%{self.q.strip()}%" clauses.append( or_( ExperienceLayout.code.ilike(term), ExperienceLayout.name.ilike(term), ExperienceLayout.description.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 LAYOUT_SORT_FIELDS else "created_at" ) column = getattr(ExperienceLayout, 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 LayoutVersionListSpec: layout_id: UUID | None = None status: LayoutVersionStatus | str | None = None sort_by: str = "version_number" sort_dir: str = "desc" def filter_clauses(self) -> list[ColumnElement]: clauses: list[ColumnElement] = [] if self.layout_id is not None: clauses.append(ExperienceLayoutVersion.layout_id == self.layout_id) if self.status is not None: status = ( self.status if isinstance(self.status, LayoutVersionStatus) else LayoutVersionStatus(str(self.status)) ) clauses.append(ExperienceLayoutVersion.status == status) 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 LAYOUT_VERSION_SORT_FIELDS else "version_number" ) column = getattr(ExperienceLayoutVersion, 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 SiteThemeBindingListSpec: site_id: UUID | None = None theme_id: UUID | None = None theme_version_id: UUID | None = None workspace_id: UUID | None = None is_active: bool | None = None sort_by: str = "created_at" sort_dir: str = "desc" def filter_clauses(self) -> list[ColumnElement]: clauses: list[ColumnElement] = [] if self.site_id is not None: clauses.append(SiteThemeBinding.site_id == self.site_id) if self.theme_id is not None: clauses.append(SiteThemeBinding.theme_id == self.theme_id) if self.theme_version_id is not None: clauses.append(SiteThemeBinding.theme_version_id == self.theme_version_id) if self.workspace_id is not None: clauses.append(SiteThemeBinding.workspace_id == self.workspace_id) if self.is_active is not None: clauses.append(SiteThemeBinding.is_active.is_(self.is_active)) 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 SITE_THEME_BINDING_SORT_FIELDS else "created_at" ) column = getattr(SiteThemeBinding, 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 PageLayoutBindingListSpec: page_id: UUID | None = None layout_id: UUID | None = None layout_version_id: UUID | None = None site_id: UUID | None = None workspace_id: UUID | None = None is_active: bool | None = None sort_by: str = "created_at" sort_dir: str = "desc" def filter_clauses(self) -> list[ColumnElement]: clauses: list[ColumnElement] = [] if self.page_id is not None: clauses.append(PageLayoutBinding.page_id == self.page_id) if self.layout_id is not None: clauses.append(PageLayoutBinding.layout_id == self.layout_id) if self.layout_version_id is not None: clauses.append(PageLayoutBinding.layout_version_id == self.layout_version_id) if self.site_id is not None: clauses.append(PageLayoutBinding.site_id == self.site_id) if self.workspace_id is not None: clauses.append(PageLayoutBinding.workspace_id == self.workspace_id) if self.is_active is not None: clauses.append(PageLayoutBinding.is_active.is_(self.is_active)) 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 PAGE_LAYOUT_BINDING_SORT_FIELDS else "created_at" ) column = getattr(PageLayoutBinding, sort_key) order = desc(column) if self.sort_dir.lower() != "asc" else asc(column) return stmt.order_by(order)