TorbatYar/backend/services/experience/app/repositories/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

178 lines
6.5 KiB
Python

"""Component library repositories — Phase 11.2."""
from __future__ import annotations
from typing import Sequence
from uuid import UUID
from sqlalchemy import func, select
from app.models.components import (
ExperienceComponent,
ExperienceComponentVersion,
PageComponentPlacement,
)
from app.repositories.base import TenantBaseRepository
from app.specifications.components import (
ComponentListSpec,
ComponentVersionListSpec,
PlacementListSpec,
)
class ExperienceComponentRepository(TenantBaseRepository[ExperienceComponent]):
model = ExperienceComponent
async def get_by_code(
self, tenant_id: UUID, workspace_id: UUID, code: str
) -> ExperienceComponent | None:
stmt = select(self.model).where(
self.model.tenant_id == tenant_id,
self.model.workspace_id == workspace_id,
self.model.code == code,
self.model.is_deleted.is_(False),
)
result = await self.session.execute(stmt)
return result.scalar_one_or_none()
async def list_filtered(
self,
tenant_id: UUID,
*,
offset: int,
limit: int,
spec: ComponentListSpec | None = None,
) -> tuple[Sequence[ExperienceComponent], int]:
base = select(self.model).where(
self.model.tenant_id == tenant_id,
self.model.is_deleted.is_(False),
)
count_base = select(func.count()).select_from(self.model).where(
self.model.tenant_id == tenant_id,
self.model.is_deleted.is_(False),
)
if spec is not None:
base = spec.apply(base)
for clause in spec.filter_clauses():
count_base = count_base.where(clause)
else:
base = base.order_by(self.model.created_at.desc())
total = int((await self.session.execute(count_base)).scalar_one())
result = await self.session.execute(base.offset(offset).limit(limit))
return result.scalars().all(), total
class ExperienceComponentVersionRepository(
TenantBaseRepository[ExperienceComponentVersion]
):
model = ExperienceComponentVersion
async def get_by_number(
self, tenant_id: UUID, component_id: UUID, version_number: int
) -> ExperienceComponentVersion | None:
stmt = select(self.model).where(
self.model.tenant_id == tenant_id,
self.model.component_id == component_id,
self.model.version_number == version_number,
self.model.is_deleted.is_(False),
)
result = await self.session.execute(stmt)
return result.scalar_one_or_none()
async def max_version_number(self, tenant_id: UUID, component_id: UUID) -> int:
stmt = select(func.max(self.model.version_number)).where(
self.model.tenant_id == tenant_id,
self.model.component_id == component_id,
self.model.is_deleted.is_(False),
)
result = await self.session.execute(stmt)
value = result.scalar_one()
return int(value or 0)
async def list_filtered(
self,
tenant_id: UUID,
*,
offset: int,
limit: int,
spec: ComponentVersionListSpec | None = None,
) -> tuple[Sequence[ExperienceComponentVersion], int]:
base = select(self.model).where(
self.model.tenant_id == tenant_id,
self.model.is_deleted.is_(False),
)
count_base = select(func.count()).select_from(self.model).where(
self.model.tenant_id == tenant_id,
self.model.is_deleted.is_(False),
)
if spec is not None:
base = spec.apply(base)
for clause in spec.filter_clauses():
count_base = count_base.where(clause)
else:
base = base.order_by(self.model.version_number.desc())
total = int((await self.session.execute(count_base)).scalar_one())
result = await self.session.execute(base.offset(offset).limit(limit))
return result.scalars().all(), total
class PageComponentPlacementRepository(TenantBaseRepository[PageComponentPlacement]):
model = PageComponentPlacement
async def get_by_slot(
self, tenant_id: UUID, page_id: UUID, slot_key: str
) -> PageComponentPlacement | None:
stmt = select(self.model).where(
self.model.tenant_id == tenant_id,
self.model.page_id == page_id,
self.model.slot_key == slot_key,
self.model.is_deleted.is_(False),
)
result = await self.session.execute(stmt)
return result.scalar_one_or_none()
async def count_by_component(self, tenant_id: UUID, component_id: UUID) -> int:
stmt = select(func.count()).select_from(self.model).where(
self.model.tenant_id == tenant_id,
self.model.component_id == component_id,
self.model.is_deleted.is_(False),
)
result = await self.session.execute(stmt)
return int(result.scalar_one())
async def count_by_version(
self, tenant_id: UUID, component_version_id: UUID
) -> int:
stmt = select(func.count()).select_from(self.model).where(
self.model.tenant_id == tenant_id,
self.model.component_version_id == component_version_id,
self.model.is_deleted.is_(False),
)
result = await self.session.execute(stmt)
return int(result.scalar_one())
async def list_filtered(
self,
tenant_id: UUID,
*,
offset: int,
limit: int,
spec: PlacementListSpec | None = None,
) -> tuple[Sequence[PageComponentPlacement], int]:
base = select(self.model).where(
self.model.tenant_id == tenant_id,
self.model.is_deleted.is_(False),
)
count_base = select(func.count()).select_from(self.model).where(
self.model.tenant_id == tenant_id,
self.model.is_deleted.is_(False),
)
if spec is not None:
base = spec.apply(base)
for clause in spec.filter_clauses():
count_base = count_base.where(clause)
else:
base = base.order_by(self.model.sort_order.asc())
total = int((await self.session.execute(count_base)).scalar_one())
result = await self.session.execute(base.offset(offset).limit(limit))
return result.scalars().all(), total