feat: expose bounded organization hierarchy routing
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
import re
|
||||
from typing import Any, TypeVar
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import inspect as sqlalchemy_inspect
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -15,6 +17,16 @@ from govoplan_core.core.configuration_control import (
|
||||
record_configuration_change_applied,
|
||||
)
|
||||
from govoplan_core.core.principal_cache import invalidate_auth_principals
|
||||
from govoplan_core.core.organizations import (
|
||||
ORGANIZATION_LIFECYCLE_EVENT_SCHEMA_VERSION,
|
||||
organization_lifecycle_event_type,
|
||||
)
|
||||
from govoplan_core.core.events import (
|
||||
EventObjectRef,
|
||||
EventTenantRef,
|
||||
PlatformEvent,
|
||||
emit_platform_event,
|
||||
)
|
||||
from govoplan_core.db.session import get_session
|
||||
from govoplan_organizations.backend.db.models import (
|
||||
OrganizationFunction,
|
||||
@@ -131,14 +143,29 @@ def _ensure_unique_slug(session: Session, model: type, tenant_id: str, slug: str
|
||||
|
||||
|
||||
def _commit(session: Session, item: ModelT) -> ModelT:
|
||||
invalidate_auth_principals(
|
||||
session,
|
||||
tenant_id=getattr(item, "tenant_id", None),
|
||||
source_module="organizations",
|
||||
resource_type=item.__class__.__name__,
|
||||
resource_id=str(getattr(item, "id", getattr(item, "tenant_id", "system"))),
|
||||
)
|
||||
lifecycle = _organization_lifecycle_change(item)
|
||||
try:
|
||||
session.flush()
|
||||
invalidate_auth_principals(
|
||||
session,
|
||||
tenant_id=getattr(item, "tenant_id", None),
|
||||
source_module="organizations",
|
||||
resource_type=item.__class__.__name__,
|
||||
resource_id=str(
|
||||
getattr(
|
||||
item,
|
||||
"id",
|
||||
getattr(item, "tenant_id", "system"),
|
||||
)
|
||||
),
|
||||
)
|
||||
if lifecycle is not None:
|
||||
_emit_organization_lifecycle_event(
|
||||
session,
|
||||
item,
|
||||
action=lifecycle[0],
|
||||
changes=lifecycle[1],
|
||||
)
|
||||
session.commit()
|
||||
except IntegrityError as exc:
|
||||
session.rollback()
|
||||
@@ -147,6 +174,121 @@ def _commit(session: Session, item: ModelT) -> ModelT:
|
||||
return item
|
||||
|
||||
|
||||
_LIFECYCLE_RESOURCES = {
|
||||
OrganizationUnitType: "unit_type",
|
||||
OrganizationStructure: "structure",
|
||||
OrganizationRelationType: "relation_type",
|
||||
OrganizationUnit: "unit",
|
||||
OrganizationRelation: "relation",
|
||||
OrganizationFunctionType: "function_type",
|
||||
OrganizationFunction: "function",
|
||||
}
|
||||
|
||||
|
||||
def _organization_lifecycle_change(
|
||||
item: object,
|
||||
) -> tuple[str, dict[str, dict[str, object | None]]] | None:
|
||||
resource_type = _LIFECYCLE_RESOURCES.get(type(item))
|
||||
if resource_type is None:
|
||||
return None
|
||||
state = sqlalchemy_inspect(item)
|
||||
changes: dict[str, dict[str, object | None]] = {}
|
||||
for attribute in state.mapper.column_attrs:
|
||||
history = state.attrs[attribute.key].history
|
||||
if not history.has_changes():
|
||||
continue
|
||||
changes[attribute.key] = {
|
||||
"before": (
|
||||
_organization_event_value(history.deleted[0])
|
||||
if history.deleted
|
||||
else None
|
||||
),
|
||||
"after": (
|
||||
_organization_event_value(history.added[0])
|
||||
if history.added
|
||||
else None
|
||||
),
|
||||
}
|
||||
if state.pending:
|
||||
action = "created"
|
||||
elif (
|
||||
"is_active" in changes
|
||||
and changes["is_active"]["after"] is False
|
||||
):
|
||||
action = "deactivated"
|
||||
elif resource_type == "unit" and "parent_id" in changes:
|
||||
action = "moved"
|
||||
else:
|
||||
action = "updated"
|
||||
return action, changes
|
||||
|
||||
|
||||
def _organization_event_value(value: object) -> object:
|
||||
if isinstance(value, datetime):
|
||||
return value.isoformat()
|
||||
return value
|
||||
|
||||
|
||||
def _emit_organization_lifecycle_event(
|
||||
session: Session,
|
||||
item: object,
|
||||
*,
|
||||
action: str,
|
||||
changes: dict[str, dict[str, object | None]],
|
||||
) -> None:
|
||||
resource_type = _LIFECYCLE_RESOURCES[type(item)]
|
||||
tenant_id = str(getattr(item, "tenant_id"))
|
||||
item_id = str(getattr(item, "id"))
|
||||
payload: dict[str, object] = {
|
||||
"schema_version": ORGANIZATION_LIFECYCLE_EVENT_SCHEMA_VERSION,
|
||||
"tenant_id": tenant_id,
|
||||
"resource_type": resource_type,
|
||||
"resource_id": item_id,
|
||||
"status": (
|
||||
"active"
|
||||
if bool(getattr(item, "is_active", True))
|
||||
else "inactive"
|
||||
),
|
||||
"changed_fields": sorted(changes),
|
||||
"changes": changes,
|
||||
}
|
||||
for field in (
|
||||
"slug",
|
||||
"unit_type_id",
|
||||
"organization_unit_id",
|
||||
"function_type_id",
|
||||
"structure_id",
|
||||
"relation_type_id",
|
||||
"source_unit_id",
|
||||
"target_unit_id",
|
||||
):
|
||||
value = getattr(item, field, None)
|
||||
if value is not None:
|
||||
payload[field] = str(value)
|
||||
emit_platform_event(
|
||||
session,
|
||||
PlatformEvent(
|
||||
type=organization_lifecycle_event_type(
|
||||
resource_type, # type: ignore[arg-type]
|
||||
action, # type: ignore[arg-type]
|
||||
),
|
||||
module_id="organizations",
|
||||
tenant=EventTenantRef(id=tenant_id),
|
||||
subject=EventObjectRef(
|
||||
type=f"organization_{resource_type}",
|
||||
id=item_id,
|
||||
label=str(getattr(item, "name", None) or "") or None,
|
||||
),
|
||||
resource=EventObjectRef(
|
||||
type=f"organization_{resource_type}",
|
||||
id=item_id,
|
||||
),
|
||||
payload=payload,
|
||||
classification="internal",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _requires_organization_change_request(session: Session, tenant_id: str) -> bool:
|
||||
item = session.query(OrganizationTenantSettings).filter(OrganizationTenantSettings.tenant_id == tenant_id).one_or_none()
|
||||
return bool(item and item.require_model_change_requests)
|
||||
|
||||
Reference in New Issue
Block a user