201 lines
6.1 KiB
Python
201 lines
6.1 KiB
Python
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"]
|