|
|
@@ -0,0 +1,635 @@
|
|
|
+"""PostgreSQL persistence for unified governance responsibilities."""
|
|
|
+
|
|
|
+from __future__ import annotations
|
|
|
+
|
|
|
+import copy
|
|
|
+import json
|
|
|
+from datetime import datetime
|
|
|
+from typing import Any
|
|
|
+
|
|
|
+from sqlalchemy import text
|
|
|
+
|
|
|
+from app.core.common.identifiers import new_governance_uid
|
|
|
+from app.core.governance.responsibilities import SqlAlchemyResponsibilityRepository
|
|
|
+
|
|
|
+
|
|
|
+def _plain(row) -> dict[str, Any]:
|
|
|
+ result = dict(row)
|
|
|
+ for key, value in tuple(result.items()):
|
|
|
+ if value is None:
|
|
|
+ continue
|
|
|
+ if isinstance(value, datetime):
|
|
|
+ result[key] = value.isoformat()
|
|
|
+ elif key.endswith("uid") or key in {"uid", "created_by", "updated_by"}:
|
|
|
+ result[key] = str(value)
|
|
|
+ return result
|
|
|
+
|
|
|
+
|
|
|
+class SqlAlchemyUnifiedResponsibilityRepository:
|
|
|
+ def __init__(self, session):
|
|
|
+ self.session = session
|
|
|
+ self.matrix_repository = SqlAlchemyResponsibilityRepository(session)
|
|
|
+
|
|
|
+ def get(self, resource_type: str, resource_uid: str) -> dict[str, Any]:
|
|
|
+ return self.matrix_repository.get(resource_type, resource_uid)
|
|
|
+
|
|
|
+ def parent(self, resource_type: str, resource_uid: str):
|
|
|
+ row = (
|
|
|
+ self.session.execute(
|
|
|
+ text(
|
|
|
+ """
|
|
|
+ SELECT uid::text AS uid, resource_type, resource_uid,
|
|
|
+ parent_type, parent_uid, revision,
|
|
|
+ updated_by::text AS updated_by, updated_at
|
|
|
+ FROM public.governance_responsibility_hierarchy
|
|
|
+ WHERE resource_type = :resource_type
|
|
|
+ AND resource_uid = :resource_uid
|
|
|
+ """
|
|
|
+ ),
|
|
|
+ {"resource_type": resource_type, "resource_uid": resource_uid},
|
|
|
+ )
|
|
|
+ .mappings()
|
|
|
+ .one_or_none()
|
|
|
+ )
|
|
|
+ return _plain(row) if row else None
|
|
|
+
|
|
|
+ def _audit(
|
|
|
+ self,
|
|
|
+ *,
|
|
|
+ resource_type: str,
|
|
|
+ resource_uid: str,
|
|
|
+ actor_uid: str,
|
|
|
+ action: str,
|
|
|
+ before: Any,
|
|
|
+ after: Any,
|
|
|
+ ) -> None:
|
|
|
+ self.session.execute(
|
|
|
+ text(
|
|
|
+ """
|
|
|
+ INSERT INTO public.governance_responsibility_audit_events (
|
|
|
+ resource_type, resource_uid, actor_uid, action,
|
|
|
+ before_state, after_state
|
|
|
+ ) VALUES (
|
|
|
+ :resource_type, :resource_uid, CAST(:actor_uid AS uuid),
|
|
|
+ :action, CAST(:before AS jsonb), CAST(:after AS jsonb)
|
|
|
+ )
|
|
|
+ """
|
|
|
+ ),
|
|
|
+ {
|
|
|
+ "resource_type": resource_type,
|
|
|
+ "resource_uid": resource_uid,
|
|
|
+ "actor_uid": actor_uid,
|
|
|
+ "action": action,
|
|
|
+ "before": json.dumps(before, ensure_ascii=False),
|
|
|
+ "after": json.dumps(after, ensure_ascii=False),
|
|
|
+ },
|
|
|
+ )
|
|
|
+
|
|
|
+ def set_parent(self, record: dict[str, Any], expected_revision: int):
|
|
|
+ self.session.execute(
|
|
|
+ text("SELECT pg_advisory_xact_lock(hashtext('responsibility-hierarchy'))")
|
|
|
+ )
|
|
|
+ current = self.parent(record["resource_type"], record["resource_uid"])
|
|
|
+ current_revision = int(current["revision"]) if current else 0
|
|
|
+ if current_revision != expected_revision:
|
|
|
+ raise RuntimeError("hierarchy revision conflict")
|
|
|
+ cycle = self.session.execute(
|
|
|
+ text(
|
|
|
+ """
|
|
|
+ WITH RECURSIVE ancestors(resource_type, resource_uid, depth) AS (
|
|
|
+ SELECT CAST(:parent_type AS VARCHAR(40)),
|
|
|
+ CAST(:parent_uid AS VARCHAR(120)), 0
|
|
|
+ UNION ALL
|
|
|
+ SELECT h.parent_type, h.parent_uid, a.depth + 1
|
|
|
+ FROM ancestors a
|
|
|
+ JOIN public.governance_responsibility_hierarchy h
|
|
|
+ ON h.resource_type = a.resource_type
|
|
|
+ AND h.resource_uid = a.resource_uid
|
|
|
+ WHERE a.depth < 20
|
|
|
+ )
|
|
|
+ SELECT EXISTS (
|
|
|
+ SELECT 1 FROM ancestors
|
|
|
+ WHERE resource_type = :resource_type
|
|
|
+ AND resource_uid = :resource_uid
|
|
|
+ )
|
|
|
+ """
|
|
|
+ ),
|
|
|
+ record,
|
|
|
+ ).scalar_one()
|
|
|
+ if cycle:
|
|
|
+ raise ValueError("responsibility hierarchy contains a cycle")
|
|
|
+ revision = current_revision + 1
|
|
|
+ uid = current["uid"] if current else new_governance_uid()
|
|
|
+ self.session.execute(
|
|
|
+ text(
|
|
|
+ """
|
|
|
+ INSERT INTO public.governance_responsibility_hierarchy (
|
|
|
+ uid, resource_type, resource_uid, parent_type, parent_uid,
|
|
|
+ revision, updated_by
|
|
|
+ ) VALUES (
|
|
|
+ CAST(:uid AS uuid), :resource_type, :resource_uid,
|
|
|
+ :parent_type, :parent_uid, :revision,
|
|
|
+ CAST(:updated_by AS uuid)
|
|
|
+ )
|
|
|
+ ON CONFLICT (resource_type, resource_uid) DO UPDATE SET
|
|
|
+ parent_type = EXCLUDED.parent_type,
|
|
|
+ parent_uid = EXCLUDED.parent_uid,
|
|
|
+ revision = EXCLUDED.revision,
|
|
|
+ updated_by = EXCLUDED.updated_by,
|
|
|
+ updated_at = CURRENT_TIMESTAMP
|
|
|
+ """
|
|
|
+ ),
|
|
|
+ {**record, "uid": uid, "revision": revision},
|
|
|
+ )
|
|
|
+ saved = self.parent(record["resource_type"], record["resource_uid"])
|
|
|
+ self._audit(
|
|
|
+ resource_type=record["resource_type"],
|
|
|
+ resource_uid=record["resource_uid"],
|
|
|
+ actor_uid=record["updated_by"],
|
|
|
+ action="hierarchy_replaced",
|
|
|
+ before=current or {},
|
|
|
+ after=saved,
|
|
|
+ )
|
|
|
+ return saved
|
|
|
+
|
|
|
+ def users_available(self, user_uids) -> set[str]:
|
|
|
+ values = sorted(set(user_uids))
|
|
|
+ if not values:
|
|
|
+ return set()
|
|
|
+ rows = self.session.execute(
|
|
|
+ text(
|
|
|
+ """
|
|
|
+ SELECT id::text FROM public.users
|
|
|
+ WHERE status = 'active' AND id::text = ANY(:user_uids)
|
|
|
+ """
|
|
|
+ ),
|
|
|
+ {"user_uids": values},
|
|
|
+ )
|
|
|
+ return {str(row[0]) for row in rows}
|
|
|
+
|
|
|
+ def active_delegation(
|
|
|
+ self,
|
|
|
+ source_user_uid: str,
|
|
|
+ responsibility_role: str,
|
|
|
+ chain: list[dict[str, Any]],
|
|
|
+ at: datetime,
|
|
|
+ ):
|
|
|
+ rows = (
|
|
|
+ self.session.execute(
|
|
|
+ text(
|
|
|
+ """
|
|
|
+ SELECT uid::text AS uid,
|
|
|
+ source_user_uid::text AS source_user_uid,
|
|
|
+ delegate_user_uid::text AS delegate_user_uid,
|
|
|
+ scope_type, scope_uid, responsibility_role,
|
|
|
+ delegation_type, starts_at, ends_at, reason,
|
|
|
+ status, current_version,
|
|
|
+ created_by::text AS created_by,
|
|
|
+ updated_by::text AS updated_by,
|
|
|
+ created_at, updated_at
|
|
|
+ FROM public.governance_responsibility_delegations
|
|
|
+ WHERE source_user_uid = CAST(:source_user_uid AS uuid)
|
|
|
+ AND status = 'active'
|
|
|
+ AND starts_at <= :evaluated_at
|
|
|
+ AND (ends_at IS NULL OR ends_at > :evaluated_at)
|
|
|
+ AND (
|
|
|
+ responsibility_role IS NULL
|
|
|
+ OR responsibility_role = :responsibility_role
|
|
|
+ )
|
|
|
+ """
|
|
|
+ ),
|
|
|
+ {
|
|
|
+ "source_user_uid": source_user_uid,
|
|
|
+ "responsibility_role": responsibility_role,
|
|
|
+ "evaluated_at": at,
|
|
|
+ },
|
|
|
+ )
|
|
|
+ .mappings()
|
|
|
+ .all()
|
|
|
+ )
|
|
|
+ depths = {
|
|
|
+ (node["resource_type"], node["resource_uid"]): node["depth"]
|
|
|
+ for node in chain
|
|
|
+ }
|
|
|
+ candidates = []
|
|
|
+ for row in rows:
|
|
|
+ item = _plain(row)
|
|
|
+ scope = (item.get("scope_type"), item.get("scope_uid"))
|
|
|
+ if scope == (None, None):
|
|
|
+ depth = 999
|
|
|
+ elif scope in depths:
|
|
|
+ depth = depths[scope]
|
|
|
+ else:
|
|
|
+ continue
|
|
|
+ candidates.append(
|
|
|
+ (depth, 0 if item.get("responsibility_role") else 1, item)
|
|
|
+ )
|
|
|
+ return sorted(candidates, key=lambda value: value[:2])[0][2] if candidates else None
|
|
|
+
|
|
|
+ def create_delegation(self, record: dict[str, Any]):
|
|
|
+ self.session.execute(
|
|
|
+ text(
|
|
|
+ """
|
|
|
+ INSERT INTO public.governance_responsibility_delegations (
|
|
|
+ uid, source_user_uid, delegate_user_uid, scope_type,
|
|
|
+ scope_uid, responsibility_role, delegation_type,
|
|
|
+ starts_at, ends_at, reason, status, current_version,
|
|
|
+ created_by, updated_by, created_at, updated_at
|
|
|
+ ) VALUES (
|
|
|
+ CAST(:uid AS uuid), CAST(:source_user_uid AS uuid),
|
|
|
+ CAST(:delegate_user_uid AS uuid), :scope_type, :scope_uid,
|
|
|
+ :responsibility_role, :delegation_type, :starts_at,
|
|
|
+ :ends_at, :reason, :status, :current_version,
|
|
|
+ CAST(:created_by AS uuid), CAST(:updated_by AS uuid),
|
|
|
+ :created_at, :updated_at
|
|
|
+ )
|
|
|
+ """
|
|
|
+ ),
|
|
|
+ record,
|
|
|
+ )
|
|
|
+ self._audit(
|
|
|
+ resource_type=record.get("scope_type") or "organization",
|
|
|
+ resource_uid=record.get("scope_uid") or "*",
|
|
|
+ actor_uid=record["created_by"],
|
|
|
+ action="delegation_created",
|
|
|
+ before={},
|
|
|
+ after=record,
|
|
|
+ )
|
|
|
+ return copy.deepcopy(record)
|
|
|
+
|
|
|
+ def get_delegation(self, uid: str):
|
|
|
+ row = (
|
|
|
+ self.session.execute(
|
|
|
+ text(
|
|
|
+ """
|
|
|
+ SELECT uid::text AS uid,
|
|
|
+ source_user_uid::text AS source_user_uid,
|
|
|
+ delegate_user_uid::text AS delegate_user_uid,
|
|
|
+ scope_type, scope_uid, responsibility_role,
|
|
|
+ delegation_type, starts_at, ends_at, reason,
|
|
|
+ status, current_version,
|
|
|
+ created_by::text AS created_by,
|
|
|
+ updated_by::text AS updated_by,
|
|
|
+ created_at, updated_at
|
|
|
+ FROM public.governance_responsibility_delegations
|
|
|
+ WHERE uid = CAST(:uid AS uuid)
|
|
|
+ """
|
|
|
+ ),
|
|
|
+ {"uid": uid},
|
|
|
+ )
|
|
|
+ .mappings()
|
|
|
+ .one_or_none()
|
|
|
+ )
|
|
|
+ return _plain(row) if row else None
|
|
|
+
|
|
|
+ def list_delegations(self, *, status: str | None = None):
|
|
|
+ where = "WHERE status = :status" if status else ""
|
|
|
+ rows = (
|
|
|
+ self.session.execute(
|
|
|
+ text(
|
|
|
+ f"""
|
|
|
+ SELECT uid::text AS uid,
|
|
|
+ source_user_uid::text AS source_user_uid,
|
|
|
+ delegate_user_uid::text AS delegate_user_uid,
|
|
|
+ scope_type, scope_uid, responsibility_role,
|
|
|
+ delegation_type, starts_at, ends_at, reason,
|
|
|
+ status, current_version,
|
|
|
+ created_by::text AS created_by,
|
|
|
+ updated_by::text AS updated_by,
|
|
|
+ created_at, updated_at
|
|
|
+ FROM public.governance_responsibility_delegations
|
|
|
+ {where}
|
|
|
+ ORDER BY created_at DESC, uid DESC
|
|
|
+ """
|
|
|
+ ),
|
|
|
+ {"status": status} if status else {},
|
|
|
+ )
|
|
|
+ .mappings()
|
|
|
+ .all()
|
|
|
+ )
|
|
|
+ return [_plain(row) for row in rows]
|
|
|
+
|
|
|
+ def update_delegation(self, record: dict[str, Any], expected_version: int):
|
|
|
+ result = self.session.execute(
|
|
|
+ text(
|
|
|
+ """
|
|
|
+ UPDATE public.governance_responsibility_delegations
|
|
|
+ SET status = :status,
|
|
|
+ current_version = current_version + 1,
|
|
|
+ updated_by = CAST(:updated_by AS uuid),
|
|
|
+ updated_at = :updated_at
|
|
|
+ WHERE uid = CAST(:uid AS uuid)
|
|
|
+ AND current_version = :expected_version
|
|
|
+ """
|
|
|
+ ),
|
|
|
+ {**record, "expected_version": expected_version},
|
|
|
+ )
|
|
|
+ if result.rowcount != 1:
|
|
|
+ raise RuntimeError("delegation version conflict")
|
|
|
+ saved = self.get_delegation(record["uid"])
|
|
|
+ self._audit(
|
|
|
+ resource_type=record.get("scope_type") or "organization",
|
|
|
+ resource_uid=record.get("scope_uid") or "*",
|
|
|
+ actor_uid=record["updated_by"],
|
|
|
+ action=f"delegation_{record['status']}",
|
|
|
+ before=record,
|
|
|
+ after=saved,
|
|
|
+ )
|
|
|
+ return saved
|
|
|
+
|
|
|
+ def expire_delegations(self, at: datetime, actor_uid: str):
|
|
|
+ candidates = [
|
|
|
+ item
|
|
|
+ for item in self.list_delegations(status="active")
|
|
|
+ if item.get("ends_at")
|
|
|
+ and datetime.fromisoformat(item["ends_at"]) <= at
|
|
|
+ ]
|
|
|
+ return [
|
|
|
+ self.update_delegation(
|
|
|
+ {**item, "status": "expired", "updated_by": actor_uid, "updated_at": at.isoformat()},
|
|
|
+ int(item["current_version"]),
|
|
|
+ )
|
|
|
+ for item in candidates
|
|
|
+ ]
|
|
|
+
|
|
|
+ def create_policy(self, policy: dict[str, Any], version: dict[str, Any]):
|
|
|
+ self._insert_policy(policy)
|
|
|
+ self._insert_policy_version(version)
|
|
|
+ self._audit_policy("policy_created", policy, {}, {**policy, "version": version})
|
|
|
+ return copy.deepcopy(policy)
|
|
|
+
|
|
|
+ def _insert_policy(self, policy: dict[str, Any]):
|
|
|
+ self.session.execute(
|
|
|
+ text(
|
|
|
+ """
|
|
|
+ INSERT INTO public.governance_responsibility_policies (
|
|
|
+ uid, code, name, policy_type, scope_type, scope_uid,
|
|
|
+ status, current_version, active_version_uid,
|
|
|
+ created_by, created_at, updated_at
|
|
|
+ ) VALUES (
|
|
|
+ CAST(:uid AS uuid), :code, :name, :policy_type,
|
|
|
+ :scope_type, :scope_uid, :status, :current_version,
|
|
|
+ CAST(:active_version_uid AS uuid), CAST(:created_by AS uuid),
|
|
|
+ :created_at, :updated_at
|
|
|
+ )
|
|
|
+ """
|
|
|
+ ),
|
|
|
+ policy,
|
|
|
+ )
|
|
|
+
|
|
|
+ def _insert_policy_version(self, version: dict[str, Any]):
|
|
|
+ self.session.execute(
|
|
|
+ text(
|
|
|
+ """
|
|
|
+ INSERT INTO public.governance_responsibility_policy_versions (
|
|
|
+ uid, policy_uid, version, status, definition,
|
|
|
+ created_by, created_at, published_by, published_at
|
|
|
+ ) VALUES (
|
|
|
+ CAST(:uid AS uuid), CAST(:policy_uid AS uuid), :version,
|
|
|
+ :status, CAST(:definition AS jsonb),
|
|
|
+ CAST(:created_by AS uuid), :created_at,
|
|
|
+ CAST(:published_by AS uuid), :published_at
|
|
|
+ )
|
|
|
+ """
|
|
|
+ ),
|
|
|
+ {**version, "definition": json.dumps(version["definition"], ensure_ascii=False)},
|
|
|
+ )
|
|
|
+
|
|
|
+ def _audit_policy(self, action, policy, before, after):
|
|
|
+ version = after.get("version") if isinstance(after, dict) else None
|
|
|
+ version = version if isinstance(version, dict) else {}
|
|
|
+ self._audit(
|
|
|
+ resource_type="responsibility_policy",
|
|
|
+ resource_uid=policy["uid"],
|
|
|
+ actor_uid=(after.get("published_by") if isinstance(after, dict) else None)
|
|
|
+ or version.get("published_by")
|
|
|
+ or version.get("created_by")
|
|
|
+ or policy["created_by"],
|
|
|
+ action=action,
|
|
|
+ before=before,
|
|
|
+ after=after,
|
|
|
+ )
|
|
|
+
|
|
|
+ def get_policy(self, uid: str):
|
|
|
+ row = (
|
|
|
+ self.session.execute(
|
|
|
+ text(
|
|
|
+ """
|
|
|
+ SELECT uid::text AS uid, code, name, policy_type,
|
|
|
+ scope_type, scope_uid, status, current_version,
|
|
|
+ active_version_uid::text AS active_version_uid,
|
|
|
+ created_by::text AS created_by, created_at, updated_at
|
|
|
+ FROM public.governance_responsibility_policies
|
|
|
+ WHERE uid = CAST(:uid AS uuid)
|
|
|
+ """
|
|
|
+ ),
|
|
|
+ {"uid": uid},
|
|
|
+ )
|
|
|
+ .mappings()
|
|
|
+ .one_or_none()
|
|
|
+ )
|
|
|
+ return _plain(row) if row else None
|
|
|
+
|
|
|
+ def list_policies(self):
|
|
|
+ rows = (
|
|
|
+ self.session.execute(
|
|
|
+ text(
|
|
|
+ """
|
|
|
+ SELECT p.uid::text AS uid, p.code, p.name, p.policy_type,
|
|
|
+ p.scope_type, p.scope_uid, p.status,
|
|
|
+ p.current_version,
|
|
|
+ p.active_version_uid::text AS active_version_uid,
|
|
|
+ p.created_by::text AS created_by,
|
|
|
+ p.created_at, p.updated_at,
|
|
|
+ v.definition AS active_definition
|
|
|
+ FROM public.governance_responsibility_policies p
|
|
|
+ LEFT JOIN public.governance_responsibility_policy_versions v
|
|
|
+ ON v.uid = p.active_version_uid
|
|
|
+ ORDER BY p.updated_at DESC, p.uid DESC
|
|
|
+ """
|
|
|
+ )
|
|
|
+ )
|
|
|
+ .mappings()
|
|
|
+ .all()
|
|
|
+ )
|
|
|
+ return [_plain(row) for row in rows]
|
|
|
+
|
|
|
+ def policy_version(self, policy_uid: str, version: int):
|
|
|
+ row = (
|
|
|
+ self.session.execute(
|
|
|
+ text(
|
|
|
+ """
|
|
|
+ SELECT uid::text AS uid, policy_uid::text AS policy_uid,
|
|
|
+ version, status, definition,
|
|
|
+ created_by::text AS created_by, created_at,
|
|
|
+ published_by::text AS published_by, published_at
|
|
|
+ FROM public.governance_responsibility_policy_versions
|
|
|
+ WHERE policy_uid = CAST(:policy_uid AS uuid)
|
|
|
+ AND version = :version
|
|
|
+ """
|
|
|
+ ),
|
|
|
+ {"policy_uid": policy_uid, "version": version},
|
|
|
+ )
|
|
|
+ .mappings()
|
|
|
+ .one_or_none()
|
|
|
+ )
|
|
|
+ return _plain(row) if row else None
|
|
|
+
|
|
|
+ def revise_policy(self, policy, version, expected_version):
|
|
|
+ current = self.get_policy(policy["uid"])
|
|
|
+ result = self.session.execute(
|
|
|
+ text(
|
|
|
+ """
|
|
|
+ UPDATE public.governance_responsibility_policies
|
|
|
+ SET status = :status, current_version = :current_version,
|
|
|
+ active_version_uid = CAST(:active_version_uid AS uuid),
|
|
|
+ updated_at = :updated_at
|
|
|
+ WHERE uid = CAST(:uid AS uuid)
|
|
|
+ AND current_version = :expected_version
|
|
|
+ """
|
|
|
+ ),
|
|
|
+ {**policy, "expected_version": expected_version},
|
|
|
+ )
|
|
|
+ if result.rowcount != 1:
|
|
|
+ raise RuntimeError("policy version conflict")
|
|
|
+ self._insert_policy_version(version)
|
|
|
+ saved = self.get_policy(policy["uid"])
|
|
|
+ self._audit_policy("policy_revised", policy, current, {**saved, "version": version})
|
|
|
+ return saved
|
|
|
+
|
|
|
+ def publish_policy(self, policy, version, expected_version):
|
|
|
+ current = self.get_policy(policy["uid"])
|
|
|
+ self.session.execute(
|
|
|
+ text(
|
|
|
+ """
|
|
|
+ UPDATE public.governance_responsibility_policy_versions
|
|
|
+ SET status = 'superseded'
|
|
|
+ WHERE policy_uid = CAST(:policy_uid AS uuid)
|
|
|
+ AND status = 'published'
|
|
|
+ """
|
|
|
+ ),
|
|
|
+ {"policy_uid": policy["uid"]},
|
|
|
+ )
|
|
|
+ version_result = self.session.execute(
|
|
|
+ text(
|
|
|
+ """
|
|
|
+ UPDATE public.governance_responsibility_policy_versions
|
|
|
+ SET status = 'published',
|
|
|
+ published_by = CAST(:published_by AS uuid),
|
|
|
+ published_at = :published_at
|
|
|
+ WHERE uid = CAST(:uid AS uuid) AND status = 'draft'
|
|
|
+ """
|
|
|
+ ),
|
|
|
+ version,
|
|
|
+ )
|
|
|
+ result = self.session.execute(
|
|
|
+ text(
|
|
|
+ """
|
|
|
+ UPDATE public.governance_responsibility_policies
|
|
|
+ SET status = 'published',
|
|
|
+ active_version_uid = CAST(:active_version_uid AS uuid),
|
|
|
+ updated_at = :updated_at
|
|
|
+ WHERE uid = CAST(:uid AS uuid)
|
|
|
+ AND current_version = :expected_version
|
|
|
+ """
|
|
|
+ ),
|
|
|
+ {**policy, "expected_version": expected_version},
|
|
|
+ )
|
|
|
+ if version_result.rowcount != 1 or result.rowcount != 1:
|
|
|
+ raise RuntimeError("policy version conflict")
|
|
|
+ saved = self.get_policy(policy["uid"])
|
|
|
+ self._audit_policy("policy_published", policy, current, {**saved, "version": version})
|
|
|
+ return saved
|
|
|
+
|
|
|
+ def policies_for_chain(self, chain: list[dict[str, Any]]):
|
|
|
+ result = []
|
|
|
+ for node in chain:
|
|
|
+ rows = (
|
|
|
+ self.session.execute(
|
|
|
+ text(
|
|
|
+ """
|
|
|
+ SELECT p.uid::text AS uid, p.code, p.name,
|
|
|
+ p.policy_type, p.scope_type, p.scope_uid,
|
|
|
+ p.status, p.current_version,
|
|
|
+ p.active_version_uid::text AS active_version_uid,
|
|
|
+ p.created_by::text AS created_by,
|
|
|
+ v.definition
|
|
|
+ FROM public.governance_responsibility_policies p
|
|
|
+ JOIN public.governance_responsibility_policy_versions v
|
|
|
+ ON v.uid = p.active_version_uid
|
|
|
+ WHERE p.status = 'published'
|
|
|
+ AND p.scope_type = :scope_type
|
|
|
+ AND p.scope_uid = :scope_uid
|
|
|
+ ORDER BY p.policy_type, p.code
|
|
|
+ """
|
|
|
+ ),
|
|
|
+ {"scope_type": node["resource_type"], "scope_uid": node["resource_uid"]},
|
|
|
+ )
|
|
|
+ .mappings()
|
|
|
+ .all()
|
|
|
+ )
|
|
|
+ result.extend({**_plain(row), "inheritance_depth": node["depth"]} for row in rows)
|
|
|
+ return result
|
|
|
+
|
|
|
+ def responsibility_operations(self, owner_uid: str):
|
|
|
+ tasks = []
|
|
|
+ quality_rows = self.session.execute(
|
|
|
+ text(
|
|
|
+ """
|
|
|
+ SELECT uid::text AS uid, issue_code AS code, message AS title,
|
|
|
+ status, priority, due_at,
|
|
|
+ (due_at IS NOT NULL AND due_at < CURRENT_TIMESTAMP
|
|
|
+ AND status <> 'closed') AS overdue,
|
|
|
+ occurrence_number AS recurrence_count
|
|
|
+ FROM public.device_quality_issues
|
|
|
+ WHERE assignee_uid = CAST(:owner_uid AS uuid)
|
|
|
+ AND status <> 'closed'
|
|
|
+ ORDER BY due_at NULLS LAST, created_at DESC
|
|
|
+ """
|
|
|
+ ),
|
|
|
+ {"owner_uid": owner_uid},
|
|
|
+ ).mappings()
|
|
|
+ tasks.extend({"kind": "quality_issue", **_plain(row)} for row in quality_rows)
|
|
|
+ incident_rows = self.session.execute(
|
|
|
+ text(
|
|
|
+ """
|
|
|
+ SELECT uid::text AS uid, code, title, status, severity,
|
|
|
+ FALSE AS overdue, 1 AS recurrence_count
|
|
|
+ FROM public.data_incidents
|
|
|
+ WHERE owner_uid = CAST(:owner_uid AS uuid)
|
|
|
+ AND status <> 'closed'
|
|
|
+ ORDER BY updated_at DESC
|
|
|
+ """
|
|
|
+ ),
|
|
|
+ {"owner_uid": owner_uid},
|
|
|
+ ).mappings()
|
|
|
+ tasks.extend({"kind": "data_incident", **_plain(row)} for row in incident_rows)
|
|
|
+ correction_rows = self.session.execute(
|
|
|
+ text(
|
|
|
+ """
|
|
|
+ SELECT uid::text AS uid, '元数据纠正' AS title, status,
|
|
|
+ FALSE AS overdue, 1 AS recurrence_count
|
|
|
+ FROM public.active_metadata_corrections
|
|
|
+ WHERE assignee_uid = CAST(:owner_uid AS uuid)
|
|
|
+ AND status = 'pending'
|
|
|
+ ORDER BY updated_at DESC
|
|
|
+ """
|
|
|
+ ),
|
|
|
+ {"owner_uid": owner_uid},
|
|
|
+ ).mappings()
|
|
|
+ tasks.extend({"kind": "metadata_correction", **_plain(row)} for row in correction_rows)
|
|
|
+ metric_rows = self.session.execute(
|
|
|
+ text(
|
|
|
+ """
|
|
|
+ SELECT uid::text AS uid, code, name, sli_type, scope_type,
|
|
|
+ scope_uid, operator, target, window_seconds, status
|
|
|
+ FROM public.data_slo_policies
|
|
|
+ WHERE owner_uid = CAST(:owner_uid AS uuid)
|
|
|
+ AND status = 'active'
|
|
|
+ ORDER BY code
|
|
|
+ """
|
|
|
+ ),
|
|
|
+ {"owner_uid": owner_uid},
|
|
|
+ ).mappings()
|
|
|
+ metrics = [{"kind": "slo", **_plain(row)} for row in metric_rows]
|
|
|
+ return {"tasks": tasks, "metrics": metrics}
|