Install and reconcile module workflow standards

This commit is contained in:
2026-07-31 19:40:04 +02:00
parent 9bccbd68da
commit 43c1f3fb72
8 changed files with 1289 additions and 5 deletions
@@ -0,0 +1,200 @@
from __future__ import annotations
from sqlalchemy import case, or_, select
from sqlalchemy.orm import Session
from govoplan_core.auth import ApiPrincipal
from govoplan_core.core.workflows import (
WorkflowCurrentStepResolution,
WorkflowInstanceRef,
WorkflowStandardStartRequest,
)
from govoplan_workflow_engine.backend.db.models import (
WorkflowDefinition,
WorkflowDefinitionRevision,
WorkflowInstance,
WorkflowInstanceStep,
)
from govoplan_workflow_engine.backend.instance_service import (
get_instance as get_workflow_instance,
resolve_step,
start_instance,
)
from govoplan_workflow_engine.backend.schemas import (
WorkflowInstanceStartRequest,
WorkflowStepActionRequest,
)
class SqlWorkflowOrchestrationProvider:
def __init__(self, *, registry: object | None = None) -> None:
self._registry = registry
def start_standard(
self,
session: object,
principal: object,
*,
request: WorkflowStandardStartRequest,
) -> WorkflowInstanceRef:
sql_session = _session(session)
api_principal = _principal(principal)
definition = _active_standard(
sql_session,
tenant_id=request.tenant_id,
origin_module_id=request.origin_module_id,
definition_key=request.definition_key,
)
instance, replayed = start_instance(
sql_session,
tenant_id=request.tenant_id,
definition_id=definition.id,
actor_id=request.actor_id,
principal=api_principal,
registry=self._registry,
payload=WorkflowInstanceStartRequest(
idempotency_key=request.idempotency_key,
input=dict(request.input),
correlation_id=request.correlation_id,
),
start_origin=request.start_origin,
)
return _instance_ref(sql_session, instance, replayed=replayed)
def resolve_current_step(
self,
session: object,
principal: object,
*,
tenant_id: str,
instance_id: str,
resolution: WorkflowCurrentStepResolution,
) -> WorkflowInstanceRef:
sql_session = _session(session)
api_principal = _principal(principal)
instance = get_workflow_instance(
sql_session,
tenant_id=tenant_id,
instance_id=instance_id,
)
step_id = instance.current_step_id
if step_id is None:
raise ValueError("Workflow instance has no current waiting step.")
if (
resolution.expected_step_id is not None
and resolution.expected_step_id != step_id
):
raise ValueError("Workflow current step changed; reload and retry.")
resolved = resolve_step(
sql_session,
tenant_id=tenant_id,
instance_id=instance_id,
step_id=step_id,
actor_id=resolution.actor_id,
principal=api_principal,
registry=self._registry,
payload=WorkflowStepActionRequest(
action=resolution.action, # type: ignore[arg-type]
output=dict(resolution.output),
evidence=list(resolution.evidence),
comment=resolution.comment,
),
)
return _instance_ref(sql_session, resolved)
def get_instance(
self,
session: object,
*,
tenant_id: str,
instance_id: str,
) -> WorkflowInstanceRef:
sql_session = _session(session)
instance = get_workflow_instance(
sql_session,
tenant_id=tenant_id,
instance_id=instance_id,
)
return _instance_ref(sql_session, instance)
def _active_standard(
session: Session,
*,
tenant_id: str,
origin_module_id: str,
definition_key: str,
) -> WorkflowDefinition:
definition = session.scalar(
select(WorkflowDefinition)
.where(
or_(
WorkflowDefinition.tenant_id == tenant_id,
WorkflowDefinition.tenant_id.is_(None),
),
WorkflowDefinition.standard_origin_module_id == origin_module_id,
WorkflowDefinition.standard_definition_key == definition_key,
WorkflowDefinition.status == "active",
WorkflowDefinition.active_revision.is_not(None),
WorkflowDefinition.deleted_at.is_(None),
)
.order_by(
case(
(WorkflowDefinition.tenant_id == tenant_id, 0),
else_=1,
),
WorkflowDefinition.updated_at.desc(),
)
)
if definition is None:
raise ValueError(
f"Active Workflow standard is unavailable: "
f"{origin_module_id}/{definition_key}."
)
return definition
def _instance_ref(
session: Session,
instance: WorkflowInstance,
*,
replayed: bool = False,
) -> WorkflowInstanceRef:
revision = session.get(
WorkflowDefinitionRevision,
instance.definition_revision_id,
)
if revision is None:
raise ValueError("Workflow instance revision is unavailable.")
step = (
session.get(WorkflowInstanceStep, instance.current_step_id)
if instance.current_step_id
else None
)
return WorkflowInstanceRef(
id=instance.id,
tenant_id=instance.tenant_id,
definition_id=instance.definition_id,
definition_revision_id=instance.definition_revision_id,
definition_revision=revision.revision,
definition_hash=revision.content_hash,
status=instance.status,
current_step_id=instance.current_step_id,
current_node_id=step.node_id if step is not None else None,
replayed=replayed,
)
def _session(value: object) -> Session:
if not isinstance(value, Session):
raise TypeError("Workflow orchestration requires a SQLAlchemy Session.")
return value
def _principal(value: object) -> ApiPrincipal:
if not isinstance(value, ApiPrincipal):
raise TypeError("Workflow orchestration requires an API principal.")
return value
__all__ = ["SqlWorkflowOrchestrationProvider"]