124 lines
3.7 KiB
Python
124 lines
3.7 KiB
Python
"""Migration manifest for durable user-engine store adapters."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from pathlib import PurePosixPath
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class MigrationStep:
|
|
"""One ordered durable-store migration known to user-engine."""
|
|
|
|
version: str
|
|
name: str
|
|
description: str
|
|
record_types: tuple[str, ...]
|
|
sql_path: str | None = None
|
|
|
|
|
|
USER_ENGINE_STORE_RECORD_TYPES = (
|
|
"users",
|
|
"accounts",
|
|
"external_identities",
|
|
"tenant_accounts",
|
|
"memberships",
|
|
"applications",
|
|
"application_bindings",
|
|
"catalogs",
|
|
"family_invitations",
|
|
"registration_sessions",
|
|
"identity_factors",
|
|
"prepared_accounts",
|
|
"access_profiles",
|
|
"active_access_contexts",
|
|
"welcome_protocols",
|
|
"onboarding_journeys",
|
|
"profile_values",
|
|
"audit_records",
|
|
"outbox_events",
|
|
)
|
|
|
|
USER_ENGINE_RECORD_COUNT_KEYS = (
|
|
"users",
|
|
"accounts",
|
|
"tenant_accounts",
|
|
"memberships",
|
|
"applications",
|
|
"bindings",
|
|
"catalogs",
|
|
"family_invitations",
|
|
"registration_sessions",
|
|
"identity_factors",
|
|
"prepared_accounts",
|
|
"access_profiles",
|
|
"active_access_contexts",
|
|
"welcome_protocols",
|
|
"onboarding_journeys",
|
|
"profile_values",
|
|
"audit_records",
|
|
"pending_outbox_events",
|
|
)
|
|
|
|
USER_ENGINE_MIGRATIONS = (
|
|
MigrationStep(
|
|
version="0001_initial",
|
|
name="initial durable store",
|
|
description=(
|
|
"Create the schema-version ledger, logical record table, audit "
|
|
"log table, and pending outbox table used by user-engine store "
|
|
"adapters."
|
|
),
|
|
record_types=USER_ENGINE_STORE_RECORD_TYPES,
|
|
sql_path="migrations/postgres/0001_user_engine_store.sql",
|
|
),
|
|
)
|
|
|
|
LATEST_SCHEMA_VERSION = USER_ENGINE_MIGRATIONS[-1].version
|
|
|
|
|
|
def migration_manifest() -> tuple[MigrationStep, ...]:
|
|
"""Return the ordered durable-store migration manifest."""
|
|
return USER_ENGINE_MIGRATIONS
|
|
|
|
|
|
def validate_migration_manifest(
|
|
migrations: tuple[MigrationStep, ...] = USER_ENGINE_MIGRATIONS,
|
|
) -> tuple[str, ...]:
|
|
"""Return manifest validation errors without touching provider resources."""
|
|
errors: list[str] = []
|
|
if not migrations:
|
|
return ("migration manifest must not be empty",)
|
|
|
|
seen_versions: set[str] = set()
|
|
previous_version = ""
|
|
for migration in migrations:
|
|
if not migration.version:
|
|
errors.append("migration version must not be empty")
|
|
if migration.version in seen_versions:
|
|
errors.append(f"duplicate migration version {migration.version}")
|
|
if previous_version and migration.version <= previous_version:
|
|
errors.append(
|
|
f"migration {migration.version} must sort after {previous_version}"
|
|
)
|
|
if not migration.name:
|
|
errors.append(f"migration {migration.version} must have a name")
|
|
if not migration.record_types:
|
|
errors.append(
|
|
f"migration {migration.version} must declare logical record types"
|
|
)
|
|
if len(set(migration.record_types)) != len(migration.record_types):
|
|
errors.append(f"migration {migration.version} has duplicate record types")
|
|
if migration.sql_path is not None:
|
|
path = PurePosixPath(migration.sql_path)
|
|
if path.is_absolute() or ".." in path.parts:
|
|
errors.append(
|
|
f"migration {migration.version} sql_path must stay repo-relative"
|
|
)
|
|
if path.suffix != ".sql":
|
|
errors.append(
|
|
f"migration {migration.version} sql_path must reference SQL"
|
|
)
|
|
seen_versions.add(migration.version)
|
|
previous_version = migration.version
|
|
return tuple(errors)
|