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"]