Add governed organization template upgrades
This commit is contained in:
@@ -22,6 +22,7 @@ from govoplan_core.core.organizations import (
|
||||
organization_lifecycle_event_type,
|
||||
)
|
||||
from govoplan_core.core.events import (
|
||||
EventActorRef,
|
||||
EventObjectRef,
|
||||
EventTenantRef,
|
||||
PlatformEvent,
|
||||
@@ -34,6 +35,7 @@ from govoplan_organizations.backend.db.models import (
|
||||
OrganizationModelInstantiation,
|
||||
OrganizationModelTemplate,
|
||||
OrganizationModelTemplateVersion,
|
||||
OrganizationModelUpgrade,
|
||||
OrganizationRelation,
|
||||
OrganizationRelationType,
|
||||
OrganizationTenantSettings,
|
||||
@@ -48,6 +50,12 @@ from .schemas import (
|
||||
FunctionTypeUpdateRequest,
|
||||
FunctionUpdateRequest,
|
||||
OrganizationModelInstantiationItem,
|
||||
OrganizationModelUpgradeApplyRequest,
|
||||
OrganizationModelUpgradeApplyResponse,
|
||||
OrganizationModelUpgradeCancelRequest,
|
||||
OrganizationModelUpgradeItem,
|
||||
OrganizationModelUpgradeListResponse,
|
||||
OrganizationModelUpgradePreviewRequest,
|
||||
OrganizationFunctionItem,
|
||||
OrganizationFunctionTypeItem,
|
||||
OrganizationModelResponse,
|
||||
@@ -80,6 +88,14 @@ from govoplan_organizations.backend.templates import (
|
||||
canonical_template_definition,
|
||||
instantiate_template_version,
|
||||
)
|
||||
from govoplan_organizations.backend.upgrades import (
|
||||
OrganizationUpgradeError,
|
||||
apply_model_upgrade,
|
||||
cancel_model_upgrade,
|
||||
create_model_upgrade_preview,
|
||||
current_model_instantiation,
|
||||
list_model_upgrades,
|
||||
)
|
||||
|
||||
|
||||
router = APIRouter(prefix="/organizations", tags=["organizations"])
|
||||
@@ -475,6 +491,74 @@ def _instantiation_item(
|
||||
)
|
||||
|
||||
|
||||
def _upgrade_item(item: OrganizationModelUpgrade) -> OrganizationModelUpgradeItem:
|
||||
return OrganizationModelUpgradeItem(
|
||||
id=item.id,
|
||||
tenant_id=item.tenant_id,
|
||||
template_id=item.template_id,
|
||||
source_instantiation_id=item.source_instantiation_id,
|
||||
source_template_version_id=item.source_template_version_id,
|
||||
target_template_version_id=item.target_template_version_id,
|
||||
status=item.status,
|
||||
revision=item.revision,
|
||||
base_definition_sha256=item.base_definition_sha256,
|
||||
local_definition_sha256=item.local_definition_sha256,
|
||||
target_definition_sha256=item.target_definition_sha256,
|
||||
preview=item.preview,
|
||||
decisions=item.decisions,
|
||||
requested_by_account_id=item.requested_by_account_id,
|
||||
applied_by_account_id=item.applied_by_account_id,
|
||||
cancelled_by_account_id=item.cancelled_by_account_id,
|
||||
applied_at=item.applied_at,
|
||||
cancelled_at=item.cancelled_at,
|
||||
provenance=dict(item.provenance or {}),
|
||||
created_at=item.created_at,
|
||||
updated_at=item.updated_at,
|
||||
)
|
||||
|
||||
|
||||
def _emit_model_upgrade_event(
|
||||
session: Session,
|
||||
principal: ApiPrincipal,
|
||||
item: OrganizationModelUpgrade,
|
||||
action: str,
|
||||
) -> None:
|
||||
emit_platform_event(
|
||||
session,
|
||||
PlatformEvent(
|
||||
type=f"organizations.model_upgrade.{action}.v1",
|
||||
module_id="organizations",
|
||||
tenant=EventTenantRef(id=item.tenant_id),
|
||||
actor=EventActorRef(type="account", id=principal.account_id),
|
||||
subject=EventObjectRef(type="organization_model_upgrade", id=item.id),
|
||||
resource=EventObjectRef(
|
||||
type="organization_model_template_version",
|
||||
id=item.target_template_version_id,
|
||||
),
|
||||
payload={
|
||||
"schema_version": "1",
|
||||
"tenant_id": item.tenant_id,
|
||||
"upgrade_id": item.id,
|
||||
"status": item.status,
|
||||
"revision": item.revision,
|
||||
"source_template_version_id": item.source_template_version_id,
|
||||
"target_template_version_id": item.target_template_version_id,
|
||||
"requires_decisions": int(
|
||||
dict(item.preview or {}).get("requires_decisions", 0)
|
||||
),
|
||||
"blocking_invalid_references": int(
|
||||
dict(item.preview or {}).get(
|
||||
"blocking_invalid_references", 0
|
||||
)
|
||||
),
|
||||
"decision_count": len(dict(item.decisions or {})),
|
||||
"silent_mutation": False,
|
||||
},
|
||||
classification="internal",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _template_version(
|
||||
session: Session,
|
||||
template_id: str,
|
||||
@@ -742,6 +826,172 @@ def instantiate_organization_model_template(
|
||||
return _instantiation_item(instantiation)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/model-upgrades",
|
||||
response_model=OrganizationModelUpgradeListResponse,
|
||||
)
|
||||
def list_organization_model_upgrades(
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(require_any_scope(*ORG_READ_SCOPES)),
|
||||
) -> OrganizationModelUpgradeListResponse:
|
||||
current = current_model_instantiation(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
)
|
||||
return OrganizationModelUpgradeListResponse(
|
||||
current_instantiation=(
|
||||
_instantiation_item(current) if current is not None else None
|
||||
),
|
||||
upgrades=[
|
||||
_upgrade_item(item)
|
||||
for item in list_model_upgrades(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/model-upgrades/preview",
|
||||
response_model=OrganizationModelUpgradeItem,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
def preview_organization_model_upgrade(
|
||||
payload: OrganizationModelUpgradePreviewRequest,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(
|
||||
require_any_scope(*ORG_MODEL_WRITE_SCOPES)
|
||||
),
|
||||
) -> OrganizationModelUpgradeItem:
|
||||
target = session.get(
|
||||
OrganizationModelTemplateVersion,
|
||||
payload.target_template_version_id,
|
||||
)
|
||||
if target is None:
|
||||
raise _not_found("Organization model template version")
|
||||
try:
|
||||
item = create_model_upgrade_preview(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
target_version=target,
|
||||
actor_account_id=principal.account_id,
|
||||
idempotency_key=payload.idempotency_key,
|
||||
)
|
||||
_emit_model_upgrade_event(session, principal, item, "previewed")
|
||||
session.commit()
|
||||
session.refresh(item)
|
||||
except OrganizationUpgradeError as exc:
|
||||
session.rollback()
|
||||
raise _conflict(str(exc)) from exc
|
||||
return _upgrade_item(item)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/model-upgrades/{upgrade_id}/cancel",
|
||||
response_model=OrganizationModelUpgradeItem,
|
||||
)
|
||||
def cancel_organization_model_upgrade(
|
||||
upgrade_id: str,
|
||||
payload: OrganizationModelUpgradeCancelRequest,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(
|
||||
require_any_scope(*ORG_MODEL_WRITE_SCOPES)
|
||||
),
|
||||
) -> OrganizationModelUpgradeItem:
|
||||
try:
|
||||
item = cancel_model_upgrade(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
upgrade_id=upgrade_id,
|
||||
expected_revision=payload.expected_revision,
|
||||
actor_account_id=principal.account_id,
|
||||
)
|
||||
_emit_model_upgrade_event(session, principal, item, "cancelled")
|
||||
session.commit()
|
||||
session.refresh(item)
|
||||
except OrganizationUpgradeError as exc:
|
||||
session.rollback()
|
||||
raise _conflict(str(exc)) from exc
|
||||
return _upgrade_item(item)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/model-upgrades/{upgrade_id}/apply",
|
||||
response_model=OrganizationModelUpgradeApplyResponse,
|
||||
)
|
||||
def apply_organization_model_upgrade(
|
||||
upgrade_id: str,
|
||||
payload: OrganizationModelUpgradeApplyRequest,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(
|
||||
require_any_scope(*ORG_MODEL_WRITE_SCOPES)
|
||||
),
|
||||
) -> OrganizationModelUpgradeApplyResponse:
|
||||
approval, control_target, _value = _ensure_organization_change_allowed(
|
||||
session,
|
||||
principal,
|
||||
tenant_id=principal.tenant_id,
|
||||
resource_type="model_upgrade",
|
||||
operation="applied",
|
||||
payload=payload,
|
||||
resource_id=upgrade_id,
|
||||
)
|
||||
before = _upgrade_item(
|
||||
_get_tenant_row(
|
||||
session,
|
||||
OrganizationModelUpgrade,
|
||||
upgrade_id,
|
||||
principal.tenant_id,
|
||||
"Organization model upgrade",
|
||||
)
|
||||
).model_dump(mode="json")
|
||||
try:
|
||||
item, instantiation = apply_model_upgrade(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
upgrade_id=upgrade_id,
|
||||
expected_revision=payload.expected_revision,
|
||||
decisions={
|
||||
key: value.model_dump(exclude_none=True)
|
||||
for key, value in payload.decisions.items()
|
||||
},
|
||||
actor_account_id=principal.account_id,
|
||||
)
|
||||
invalidate_auth_principals(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
source_module="organizations",
|
||||
resource_type="organization_model_upgrade",
|
||||
resource_id=item.id,
|
||||
)
|
||||
_emit_model_upgrade_event(session, principal, item, "applied")
|
||||
response = OrganizationModelUpgradeApplyResponse(
|
||||
upgrade=_upgrade_item(item),
|
||||
instantiation=_instantiation_item(instantiation),
|
||||
)
|
||||
if approval is None:
|
||||
session.commit()
|
||||
else:
|
||||
_record_organization_change_applied(
|
||||
session,
|
||||
principal,
|
||||
approval=approval,
|
||||
target=control_target,
|
||||
before=before,
|
||||
after=response.model_dump(mode="json"),
|
||||
)
|
||||
session.refresh(item)
|
||||
session.refresh(instantiation)
|
||||
except OrganizationUpgradeError as exc:
|
||||
session.rollback()
|
||||
raise _conflict(str(exc)) from exc
|
||||
return OrganizationModelUpgradeApplyResponse(
|
||||
upgrade=_upgrade_item(item),
|
||||
instantiation=_instantiation_item(instantiation),
|
||||
)
|
||||
|
||||
|
||||
@router.get("/model", response_model=OrganizationModelResponse)
|
||||
def get_organization_model(
|
||||
session: Session = Depends(get_session),
|
||||
|
||||
@@ -180,6 +180,102 @@ class OrganizationModelInstantiationItem(BaseModel):
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
UpgradeClassification = Literal[
|
||||
"unchanged",
|
||||
"compatible_addition",
|
||||
"compatible_change",
|
||||
"destructive_remapping",
|
||||
"local_divergence",
|
||||
"invalid_reference",
|
||||
]
|
||||
UpgradeDecisionAction = Literal["keep_local", "use_target", "map_to"]
|
||||
|
||||
|
||||
class OrganizationModelUpgradePreviewRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
target_template_version_id: str = Field(min_length=1, max_length=36)
|
||||
idempotency_key: str = Field(min_length=1, max_length=255)
|
||||
|
||||
|
||||
class OrganizationModelUpgradeDecision(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
action: UpgradeDecisionAction
|
||||
target_key: str | None = Field(default=None, max_length=500)
|
||||
|
||||
|
||||
class OrganizationModelUpgradeApplyRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
expected_revision: int = Field(ge=1)
|
||||
decisions: dict[str, OrganizationModelUpgradeDecision] = Field(
|
||||
default_factory=dict,
|
||||
max_length=5000,
|
||||
)
|
||||
change_request_id: str | None = Field(default=None, max_length=255)
|
||||
|
||||
|
||||
class OrganizationModelUpgradeCancelRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
expected_revision: int = Field(ge=1)
|
||||
|
||||
|
||||
class OrganizationModelUpgradeDiffEntry(BaseModel):
|
||||
id: str
|
||||
collection: str
|
||||
key: str
|
||||
classification: UpgradeClassification
|
||||
requires_decision: bool
|
||||
base: dict[str, Any] | None = None
|
||||
local: dict[str, Any] | None = None
|
||||
target: dict[str, Any] | None = None
|
||||
allowed_actions: list[UpgradeDecisionAction] = Field(default_factory=list)
|
||||
|
||||
|
||||
class OrganizationModelUpgradePreview(BaseModel):
|
||||
entries: list[OrganizationModelUpgradeDiffEntry] = Field(default_factory=list)
|
||||
counts: dict[str, int] = Field(default_factory=dict)
|
||||
requires_decisions: int = 0
|
||||
blocking_invalid_references: int = 0
|
||||
silent_mutation: bool = False
|
||||
|
||||
|
||||
class OrganizationModelUpgradeItem(BaseModel):
|
||||
id: str
|
||||
tenant_id: str
|
||||
template_id: str
|
||||
source_instantiation_id: str
|
||||
source_template_version_id: str
|
||||
target_template_version_id: str
|
||||
status: str
|
||||
revision: int
|
||||
base_definition_sha256: str
|
||||
local_definition_sha256: str
|
||||
target_definition_sha256: str
|
||||
preview: OrganizationModelUpgradePreview
|
||||
decisions: dict[str, OrganizationModelUpgradeDecision] = Field(default_factory=dict)
|
||||
requested_by_account_id: str | None = None
|
||||
applied_by_account_id: str | None = None
|
||||
cancelled_by_account_id: str | None = None
|
||||
applied_at: datetime | None = None
|
||||
cancelled_at: datetime | None = None
|
||||
provenance: dict[str, Any] = Field(default_factory=dict)
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class OrganizationModelUpgradeListResponse(BaseModel):
|
||||
current_instantiation: OrganizationModelInstantiationItem | None = None
|
||||
upgrades: list[OrganizationModelUpgradeItem] = Field(default_factory=list)
|
||||
|
||||
|
||||
class OrganizationModelUpgradeApplyResponse(BaseModel):
|
||||
upgrade: OrganizationModelUpgradeItem
|
||||
instantiation: OrganizationModelInstantiationItem
|
||||
|
||||
|
||||
class OrganizationSettingsItem(BaseModel):
|
||||
id: str | None = None
|
||||
tenant_id: str
|
||||
|
||||
@@ -156,6 +156,77 @@ class OrganizationModelInstantiation(Base, TimestampMixin):
|
||||
)
|
||||
|
||||
|
||||
class OrganizationModelUpgrade(Base, TimestampMixin):
|
||||
__tablename__ = "organizations_model_upgrades"
|
||||
__table_args__ = (
|
||||
UniqueConstraint(
|
||||
"tenant_id",
|
||||
"idempotency_key",
|
||||
name="uq_organizations_model_upgrade_idempotency",
|
||||
),
|
||||
Index(
|
||||
"ix_organizations_model_upgrades_tenant_status",
|
||||
"tenant_id",
|
||||
"status",
|
||||
"updated_at",
|
||||
),
|
||||
)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
||||
tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
|
||||
template_id: Mapped[str] = mapped_column(
|
||||
ForeignKey("organizations_model_templates.id", ondelete="RESTRICT"),
|
||||
nullable=False,
|
||||
index=True,
|
||||
)
|
||||
source_instantiation_id: Mapped[str] = mapped_column(
|
||||
ForeignKey("organizations_model_instantiations.id", ondelete="RESTRICT"),
|
||||
nullable=False,
|
||||
index=True,
|
||||
)
|
||||
source_template_version_id: Mapped[str] = mapped_column(
|
||||
ForeignKey(
|
||||
"organizations_model_template_versions.id",
|
||||
ondelete="RESTRICT",
|
||||
),
|
||||
nullable=False,
|
||||
index=True,
|
||||
)
|
||||
target_template_version_id: Mapped[str] = mapped_column(
|
||||
ForeignKey(
|
||||
"organizations_model_template_versions.id",
|
||||
ondelete="RESTRICT",
|
||||
),
|
||||
nullable=False,
|
||||
index=True,
|
||||
)
|
||||
status: Mapped[str] = mapped_column(
|
||||
String(30), default="previewed", nullable=False, index=True
|
||||
)
|
||||
revision: Mapped[int] = mapped_column(Integer, default=1, nullable=False)
|
||||
base_definition_sha256: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
local_definition_sha256: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
target_definition_sha256: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
preview: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
|
||||
decisions: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
|
||||
idempotency_key: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
request_digest: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
requested_by_account_id: Mapped[str | None] = mapped_column(
|
||||
String(36), nullable=True, index=True
|
||||
)
|
||||
applied_by_account_id: Mapped[str | None] = mapped_column(
|
||||
String(36), nullable=True, index=True
|
||||
)
|
||||
cancelled_by_account_id: Mapped[str | None] = mapped_column(
|
||||
String(36), nullable=True, index=True
|
||||
)
|
||||
applied_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
cancelled_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
provenance: Mapped[dict[str, Any]] = mapped_column(
|
||||
JSON, default=dict, nullable=False
|
||||
)
|
||||
|
||||
|
||||
class OrganizationStructure(Base, TimestampMixin):
|
||||
__tablename__ = "organizations_structures"
|
||||
__table_args__ = (UniqueConstraint("tenant_id", "slug", name="uq_organizations_structures_tenant_slug"),)
|
||||
@@ -257,6 +328,7 @@ __all__ = [
|
||||
"OrganizationModelInstantiation",
|
||||
"OrganizationModelTemplate",
|
||||
"OrganizationModelTemplateVersion",
|
||||
"OrganizationModelUpgrade",
|
||||
"OrganizationRelation",
|
||||
"OrganizationRelationType",
|
||||
"OrganizationStructure",
|
||||
|
||||
@@ -189,6 +189,14 @@ manifest = ModuleManifest(
|
||||
label="Organizations administration",
|
||||
order=85,
|
||||
),
|
||||
ViewSurface(
|
||||
id="organizations.admin.template-upgrades",
|
||||
module_id="organizations",
|
||||
kind="section",
|
||||
label="Organization template upgrades",
|
||||
parent_id="organizations.admin.tenant",
|
||||
order=86,
|
||||
),
|
||||
),
|
||||
),
|
||||
migration_spec=MigrationSpec(
|
||||
@@ -203,6 +211,7 @@ manifest = ModuleManifest(
|
||||
organization_models.OrganizationModelTemplate,
|
||||
organization_models.OrganizationModelTemplateVersion,
|
||||
organization_models.OrganizationModelInstantiation,
|
||||
organization_models.OrganizationModelUpgrade,
|
||||
organization_models.OrganizationStructure,
|
||||
organization_models.OrganizationRelationType,
|
||||
organization_models.OrganizationRelation,
|
||||
@@ -217,6 +226,53 @@ manifest = ModuleManifest(
|
||||
CAPABILITY_ORGANIZATION_HIERARCHY_DIRECTORY: (_organization_directory),
|
||||
},
|
||||
documentation=(
|
||||
DocumentationTopic(
|
||||
id="organizations.template-upgrades",
|
||||
title="Upgrade a tenant organization model",
|
||||
summary=(
|
||||
"Compare an immutable system template version with the current "
|
||||
"tenant-owned model before explicitly applying an upgrade."
|
||||
),
|
||||
body=(
|
||||
"Open Organizations administration and create a preview for a newer "
|
||||
"published version of the template used by the tenant. The persisted "
|
||||
"three-way comparison separates compatible additions, compatible "
|
||||
"changes, tenant-only divergence, destructive remapping, and invalid "
|
||||
"references. Tenant-only changes are preserved. Conflicting or "
|
||||
"destructive entries require an explicit keep, replace, or bounded "
|
||||
"mapping decision. Applying the reviewed preview creates a new "
|
||||
"tenant-owned instantiation and supersedes the previous provenance; "
|
||||
"it never creates live inheritance. A stale preview is rejected if the "
|
||||
"tenant model or either template version changed. Cancelling retains "
|
||||
"the review record without modifying organization data."
|
||||
),
|
||||
layer="configured",
|
||||
documentation_types=("admin",),
|
||||
audience=("tenant_admin", "operator"),
|
||||
related_modules=("policy", "audit", "idm", "access"),
|
||||
links=(
|
||||
DocumentationLink(
|
||||
label="Organizations administration",
|
||||
href="/admin?section=tenant-organization-settings",
|
||||
kind="runtime",
|
||||
),
|
||||
DocumentationLink(
|
||||
label="Organization upgrade API",
|
||||
href="/api/v1/organizations/model-upgrades",
|
||||
kind="api",
|
||||
),
|
||||
),
|
||||
metadata={
|
||||
"kind": "guide",
|
||||
"help_contexts": [
|
||||
"organizations.admin.template-upgrades",
|
||||
"organizations.template-upgrade.preview",
|
||||
"organizations.template-upgrade.decision",
|
||||
"organizations.template-upgrade.apply",
|
||||
],
|
||||
},
|
||||
order=27,
|
||||
),
|
||||
DocumentationTopic(
|
||||
id="organizations.model",
|
||||
title="Organization model",
|
||||
@@ -320,7 +376,9 @@ manifest = ModuleManifest(
|
||||
maturity="vertical_slice",
|
||||
documentation_ref="docs/ORGANIZATION_MODEL.md",
|
||||
test_ref="tests/test_model_templates.py",
|
||||
known_limits=("Cross-tenant template lifecycle and target upgrade evidence are not reference-ready.",),
|
||||
known_limits=(
|
||||
"Downstream modules must reconcile organization-reference changes from the emitted upgrade event; cross-module records are not rewritten directly.",
|
||||
),
|
||||
owned_concepts=("organization unit", "organization structure", "organization relation", "organization function"),
|
||||
non_owned_concepts=("function incumbency", "identity", "application role", "mandate"),
|
||||
recovery_docs=("docs/ORGANIZATION_MODEL.md",),
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
"""Development mirror for the organization template upgrade ledger."""
|
||||
|
||||
from govoplan_organizations.backend.migrations.versions.a61e4d9c72b8_organization_template_upgrades import ( # noqa: F401
|
||||
branch_labels,
|
||||
depends_on,
|
||||
downgrade,
|
||||
down_revision,
|
||||
revision,
|
||||
upgrade,
|
||||
)
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
"""organization template upgrade ledger
|
||||
|
||||
Revision ID: a61e4d9c72b8
|
||||
Revises: 7e8f9a0b1c2d
|
||||
Create Date: 2026-08-04 00:00:00.000000
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = "a61e4d9c72b8"
|
||||
down_revision = "7e8f9a0b1c2d"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"organizations_model_upgrades",
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("tenant_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("template_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("source_instantiation_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("source_template_version_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("target_template_version_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("status", sa.String(length=30), nullable=False),
|
||||
sa.Column("revision", sa.Integer(), nullable=False),
|
||||
sa.Column("base_definition_sha256", sa.String(length=64), nullable=False),
|
||||
sa.Column("local_definition_sha256", sa.String(length=64), nullable=False),
|
||||
sa.Column("target_definition_sha256", sa.String(length=64), nullable=False),
|
||||
sa.Column("preview", sa.JSON(), nullable=False),
|
||||
sa.Column("decisions", sa.JSON(), nullable=False),
|
||||
sa.Column("idempotency_key", sa.String(length=255), nullable=False),
|
||||
sa.Column("request_digest", sa.String(length=64), nullable=False),
|
||||
sa.Column("requested_by_account_id", sa.String(length=36), nullable=True),
|
||||
sa.Column("applied_by_account_id", sa.String(length=36), nullable=True),
|
||||
sa.Column("cancelled_by_account_id", sa.String(length=36), nullable=True),
|
||||
sa.Column("applied_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("cancelled_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("provenance", sa.JSON(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["template_id"],
|
||||
["organizations_model_templates.id"],
|
||||
name=op.f("fk_organizations_model_upgrades_template_id_organizations_model_templates"),
|
||||
ondelete="RESTRICT",
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["source_instantiation_id"],
|
||||
["organizations_model_instantiations.id"],
|
||||
name=op.f("fk_organizations_model_upgrades_source_instantiation_id_organizations_model_instantiations"),
|
||||
ondelete="RESTRICT",
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["source_template_version_id"],
|
||||
["organizations_model_template_versions.id"],
|
||||
name=op.f("fk_organizations_model_upgrades_source_template_version_id_organizations_model_template_versions"),
|
||||
ondelete="RESTRICT",
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["target_template_version_id"],
|
||||
["organizations_model_template_versions.id"],
|
||||
name=op.f("fk_organizations_model_upgrades_target_template_version_id_organizations_model_template_versions"),
|
||||
ondelete="RESTRICT",
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id", name=op.f("pk_organizations_model_upgrades")),
|
||||
sa.UniqueConstraint(
|
||||
"tenant_id",
|
||||
"idempotency_key",
|
||||
name="uq_organizations_model_upgrade_idempotency",
|
||||
),
|
||||
)
|
||||
for column in (
|
||||
"tenant_id",
|
||||
"template_id",
|
||||
"source_instantiation_id",
|
||||
"source_template_version_id",
|
||||
"target_template_version_id",
|
||||
"status",
|
||||
"requested_by_account_id",
|
||||
"applied_by_account_id",
|
||||
"cancelled_by_account_id",
|
||||
):
|
||||
op.create_index(
|
||||
op.f(f"ix_organizations_model_upgrades_{column}"),
|
||||
"organizations_model_upgrades",
|
||||
[column],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_organizations_model_upgrades_tenant_status",
|
||||
"organizations_model_upgrades",
|
||||
["tenant_id", "status", "updated_at"],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("organizations_model_upgrades")
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user