"""Sites & page resources — Phase 11.1. Independent aggregates use UUID refs within experience_db only. No SQLAlchemy relationships. No page-builder / theme / template engines. """ from __future__ import annotations import uuid from datetime import datetime from sqlalchemy import Boolean, DateTime, Index, Integer, String, Text, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.types import JSON from app.core.database import Base from app.models.base import ( ActorAuditMixin, OptimisticLockMixin, SoftDeleteMixin, TenantMixin, TimestampMixin, UUIDPrimaryKeyMixin, ) from app.models.types import DomainKind, GUID, PageType, PublishStatus class ExperienceSite( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, OptimisticLockMixin, ): """Tenant site under a workspace — multiple sites per workspace.""" __tablename__ = "experience_sites" workspace_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) code: Mapped[str] = mapped_column(String(50), nullable=False) name: Mapped[str] = mapped_column(String(255), nullable=False) description: Mapped[str | None] = mapped_column(Text, nullable=True) publish_status: Mapped[PublishStatus] = mapped_column( default=PublishStatus.DRAFT, nullable=False ) default_locale: Mapped[str] = mapped_column(String(16), default="fa", nullable=False) default_direction: Mapped[str] = mapped_column(String(3), default="rtl", nullable=False) primary_domain: Mapped[str | None] = mapped_column(String(255), nullable=True) home_page_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True) published_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True ) archived_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True ) settings: Mapped[dict | None] = mapped_column(JSON, nullable=True) metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) __table_args__ = ( UniqueConstraint( "tenant_id", "workspace_id", "code", name="uq_experience_sites_tenant_code" ), Index("ix_experience_sites_workspace", "tenant_id", "workspace_id"), Index("ix_experience_sites_publish", "tenant_id", "publish_status"), Index("ix_experience_sites_tenant_deleted", "tenant_id", "is_deleted"), ) class ExperiencePage( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, OptimisticLockMixin, ): """Page-as-resource — typed, publishable; layout/components come in later phases.""" __tablename__ = "experience_pages" workspace_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) site_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) code: Mapped[str] = mapped_column(String(50), nullable=False) slug: Mapped[str] = mapped_column(String(200), nullable=False) title: Mapped[str] = mapped_column(String(255), nullable=False) summary: Mapped[str | None] = mapped_column(Text, nullable=True) page_type: Mapped[PageType] = mapped_column(default=PageType.LANDING, nullable=False) publish_status: Mapped[PublishStatus] = mapped_column( default=PublishStatus.DRAFT, nullable=False ) locale: Mapped[str] = mapped_column(String(16), default="fa", nullable=False) text_direction: Mapped[str] = mapped_column(String(3), default="rtl", nullable=False) sort_order: Mapped[int] = mapped_column(Integer, default=0, nullable=False) is_home: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) published_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True ) archived_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True ) # Content shell only — no builder tree in 11.1 content_shell: Mapped[dict | None] = mapped_column(JSON, nullable=True) seo_shell: Mapped[dict | None] = mapped_column(JSON, nullable=True) external_refs: Mapped[dict | None] = mapped_column(JSON, nullable=True) metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) __table_args__ = ( UniqueConstraint( "tenant_id", "site_id", "code", name="uq_experience_pages_tenant_code" ), UniqueConstraint( "tenant_id", "site_id", "slug", name="uq_experience_pages_tenant_slug" ), Index("ix_experience_pages_site", "tenant_id", "site_id"), Index("ix_experience_pages_type", "tenant_id", "page_type"), Index("ix_experience_pages_publish", "tenant_id", "publish_status"), Index("ix_experience_pages_tenant_deleted", "tenant_id", "is_deleted"), ) class ExperienceSiteDomain( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, ): """Domain / subdomain binding refs for a site — DNS/SSL remain edge/Core.""" __tablename__ = "experience_site_domains" workspace_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) site_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) host: Mapped[str] = mapped_column(String(255), nullable=False) domain_kind: Mapped[DomainKind] = mapped_column( default=DomainKind.SUBDOMAIN, nullable=False ) is_primary: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) is_verified: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) verification_token: Mapped[str | None] = mapped_column(String(100), nullable=True) path_prefix: Mapped[str | None] = mapped_column(String(100), nullable=True) metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) __table_args__ = ( UniqueConstraint( "tenant_id", "host", name="uq_experience_site_domains_tenant_host" ), Index("ix_experience_site_domains_site", "tenant_id", "site_id"), Index("ix_experience_site_domains_tenant_deleted", "tenant_id", "is_deleted"), )