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>
258 lines
8.9 KiB
Python
258 lines
8.9 KiB
Python
"""Publishing, domain-edge refs, SEO & PWA shells — Phase 11.7.
|
|
|
|
PublishRelease = publish workflow audit (site/page) beyond simple status flips.
|
|
DomainEdgeBinding = DNS/SSL/edge map refs (ADR-009); edge ownership stays Core/Nginx.
|
|
SeoMetadata / SitemapShell / RobotsShell = SEO document shells.
|
|
PwaManifest = PWA readiness shell (manifest + service-worker ref).
|
|
No bundles/feature toggles in this phase.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy import DateTime, Index, 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 (
|
|
GUID,
|
|
DomainEdgeStatus,
|
|
PublishReleaseStatus,
|
|
PublishTarget,
|
|
PwaStatus,
|
|
RobotsStatus,
|
|
SeoStatus,
|
|
SeoTarget,
|
|
SitemapStatus,
|
|
)
|
|
|
|
|
|
class PublishRelease(
|
|
Base,
|
|
UUIDPrimaryKeyMixin,
|
|
TenantMixin,
|
|
TimestampMixin,
|
|
SoftDeleteMixin,
|
|
ActorAuditMixin,
|
|
OptimisticLockMixin,
|
|
):
|
|
"""Publish workflow shell for a site or page."""
|
|
|
|
__tablename__ = "experience_publish_releases"
|
|
|
|
workspace_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False)
|
|
target_type: Mapped[PublishTarget] = mapped_column(nullable=False)
|
|
target_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False)
|
|
status: Mapped[PublishReleaseStatus] = mapped_column(
|
|
default=PublishReleaseStatus.PENDING, nullable=False
|
|
)
|
|
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
completed_at: Mapped[datetime | None] = mapped_column(
|
|
DateTime(timezone=True), nullable=True
|
|
)
|
|
metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
|
|
|
__table_args__ = (
|
|
Index(
|
|
"ix_experience_publish_releases_target",
|
|
"tenant_id",
|
|
"target_type",
|
|
"target_id",
|
|
),
|
|
Index("ix_experience_publish_releases_status", "tenant_id", "status"),
|
|
Index(
|
|
"ix_experience_publish_releases_tenant_deleted", "tenant_id", "is_deleted"
|
|
),
|
|
)
|
|
|
|
|
|
class DomainEdgeBinding(
|
|
Base,
|
|
UUIDPrimaryKeyMixin,
|
|
TenantMixin,
|
|
TimestampMixin,
|
|
SoftDeleteMixin,
|
|
ActorAuditMixin,
|
|
OptimisticLockMixin,
|
|
):
|
|
"""Edge DNS/SSL binding refs for a site domain (ADR-009) — no Nginx ownership."""
|
|
|
|
__tablename__ = "experience_domain_edge_bindings"
|
|
|
|
workspace_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False)
|
|
site_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False)
|
|
site_domain_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False)
|
|
host: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
status: Mapped[DomainEdgeStatus] = mapped_column(
|
|
default=DomainEdgeStatus.PENDING, nullable=False
|
|
)
|
|
dns_status_ref: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
ssl_cert_ref: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
edge_map_ref: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint(
|
|
"tenant_id",
|
|
"site_domain_id",
|
|
name="uq_experience_domain_edge_bindings_domain",
|
|
),
|
|
Index("ix_experience_domain_edge_bindings_site", "tenant_id", "site_id"),
|
|
Index("ix_experience_domain_edge_bindings_host", "tenant_id", "host"),
|
|
Index("ix_experience_domain_edge_bindings_status", "tenant_id", "status"),
|
|
Index(
|
|
"ix_experience_domain_edge_bindings_tenant_deleted",
|
|
"tenant_id",
|
|
"is_deleted",
|
|
),
|
|
)
|
|
|
|
|
|
class SeoMetadata(
|
|
Base,
|
|
UUIDPrimaryKeyMixin,
|
|
TenantMixin,
|
|
TimestampMixin,
|
|
SoftDeleteMixin,
|
|
ActorAuditMixin,
|
|
OptimisticLockMixin,
|
|
):
|
|
"""SEO metadata shell for a site or page."""
|
|
|
|
__tablename__ = "experience_seo_metadata"
|
|
|
|
workspace_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False)
|
|
target_type: Mapped[SeoTarget] = mapped_column(nullable=False)
|
|
target_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False)
|
|
status: Mapped[SeoStatus] = mapped_column(default=SeoStatus.DRAFT, nullable=False)
|
|
title: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
canonical_url: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
|
robots_directive: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
|
og_json: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
|
metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint(
|
|
"tenant_id",
|
|
"target_type",
|
|
"target_id",
|
|
name="uq_experience_seo_metadata_target",
|
|
),
|
|
Index("ix_experience_seo_metadata_status", "tenant_id", "status"),
|
|
Index(
|
|
"ix_experience_seo_metadata_tenant_deleted", "tenant_id", "is_deleted"
|
|
),
|
|
)
|
|
|
|
|
|
class SitemapShell(
|
|
Base,
|
|
UUIDPrimaryKeyMixin,
|
|
TenantMixin,
|
|
TimestampMixin,
|
|
SoftDeleteMixin,
|
|
ActorAuditMixin,
|
|
OptimisticLockMixin,
|
|
):
|
|
"""Sitemap document shell for a site."""
|
|
|
|
__tablename__ = "experience_sitemap_shells"
|
|
|
|
workspace_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False)
|
|
site_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False)
|
|
status: Mapped[SitemapStatus] = mapped_column(
|
|
default=SitemapStatus.DRAFT, nullable=False
|
|
)
|
|
entries_json: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
|
storage_file_ref: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint(
|
|
"tenant_id", "site_id", name="uq_experience_sitemap_shells_site"
|
|
),
|
|
Index("ix_experience_sitemap_shells_status", "tenant_id", "status"),
|
|
Index(
|
|
"ix_experience_sitemap_shells_tenant_deleted", "tenant_id", "is_deleted"
|
|
),
|
|
)
|
|
|
|
|
|
class RobotsShell(
|
|
Base,
|
|
UUIDPrimaryKeyMixin,
|
|
TenantMixin,
|
|
TimestampMixin,
|
|
SoftDeleteMixin,
|
|
ActorAuditMixin,
|
|
OptimisticLockMixin,
|
|
):
|
|
"""Robots.txt shell for a site."""
|
|
|
|
__tablename__ = "experience_robots_shells"
|
|
|
|
workspace_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False)
|
|
site_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False)
|
|
status: Mapped[RobotsStatus] = mapped_column(
|
|
default=RobotsStatus.DRAFT, nullable=False
|
|
)
|
|
content_text: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
rules_json: 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", name="uq_experience_robots_shells_site"
|
|
),
|
|
Index("ix_experience_robots_shells_status", "tenant_id", "status"),
|
|
Index(
|
|
"ix_experience_robots_shells_tenant_deleted", "tenant_id", "is_deleted"
|
|
),
|
|
)
|
|
|
|
|
|
class PwaManifest(
|
|
Base,
|
|
UUIDPrimaryKeyMixin,
|
|
TenantMixin,
|
|
TimestampMixin,
|
|
SoftDeleteMixin,
|
|
ActorAuditMixin,
|
|
OptimisticLockMixin,
|
|
):
|
|
"""PWA readiness shell — manifest JSON + service-worker Storage ref."""
|
|
|
|
__tablename__ = "experience_pwa_manifests"
|
|
|
|
workspace_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False)
|
|
site_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False)
|
|
status: Mapped[PwaStatus] = mapped_column(default=PwaStatus.DRAFT, nullable=False)
|
|
name: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
short_name: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
|
start_url: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
|
manifest_json: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
|
service_worker_ref: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint(
|
|
"tenant_id", "site_id", name="uq_experience_pwa_manifests_site"
|
|
),
|
|
Index("ix_experience_pwa_manifests_status", "tenant_id", "status"),
|
|
Index(
|
|
"ix_experience_pwa_manifests_tenant_deleted", "tenant_id", "is_deleted"
|
|
),
|
|
)
|