feat: add reusable workflow definition graphs

This commit is contained in:
2026-07-28 12:43:26 +02:00
parent 0d099b05b7
commit 85eef00913
13 changed files with 972 additions and 3 deletions
+126
View File
@@ -0,0 +1,126 @@
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException, status
from govoplan_core.auth import ApiPrincipal, get_api_principal, has_scope
from govoplan_workflow.backend.manifest import (
ADMIN_SCOPE,
DEFINITION_READ_SCOPE,
DEFINITION_WRITE_SCOPE,
)
from govoplan_workflow.backend.node_library import WORKFLOW_GRAPH_LIBRARY
from govoplan_workflow.backend.schemas import (
WorkflowConfigFieldResponse,
WorkflowDiagnosticResponse,
WorkflowGraphValidationRequest,
WorkflowGraphValidationResponse,
WorkflowNodeLibraryResponse,
WorkflowNodeTypeResponse,
WorkflowPortResponse,
)
from govoplan_workflow.backend.validation import validate_workflow_graph
router = APIRouter(prefix="/workflow", tags=["workflow"])
def _require_any_scope(principal: ApiPrincipal, *scopes: str) -> None:
if any(has_scope(principal, scope) for scope in scopes):
return
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Missing one of the required scopes: {', '.join(scopes)}",
)
@router.get("/node-types", response_model=WorkflowNodeLibraryResponse)
def api_node_types(
principal: ApiPrincipal = Depends(get_api_principal),
) -> WorkflowNodeLibraryResponse:
_require_any_scope(
principal,
DEFINITION_READ_SCOPE,
DEFINITION_WRITE_SCOPE,
ADMIN_SCOPE,
)
return WorkflowNodeLibraryResponse(
id=WORKFLOW_GRAPH_LIBRARY.id,
version=WORKFLOW_GRAPH_LIBRARY.version,
allows_cycles=WORKFLOW_GRAPH_LIBRARY.constraints.allow_cycles,
nodes=[
WorkflowNodeTypeResponse(
type=definition.type,
category=definition.category,
category_label=WORKFLOW_GRAPH_LIBRARY.category_labels[definition.category],
label=definition.label,
description=definition.description,
icon=definition.icon,
input_ports=[
WorkflowPortResponse(
id=port.id,
label=port.label,
required=port.required,
multiple=port.multiple,
minimum_connections=port.minimum_connections,
)
for port in definition.input_ports
],
output_ports=[
WorkflowPortResponse(
id=port.id,
label=port.label,
required=port.required,
multiple=port.multiple,
minimum_connections=port.minimum_connections,
)
for port in definition.output_ports
],
config_fields=[
WorkflowConfigFieldResponse(
id=field.id,
label=field.label,
kind=field.kind,
required=field.required,
description=field.description,
options=list(field.options),
)
for field in definition.config_fields
],
default_config=dict(definition.default_config),
)
for definition in WORKFLOW_GRAPH_LIBRARY.node_types
],
)
@router.post(
"/definitions/validate",
response_model=WorkflowGraphValidationResponse,
)
def api_validate_definition(
payload: WorkflowGraphValidationRequest,
principal: ApiPrincipal = Depends(get_api_principal),
) -> WorkflowGraphValidationResponse:
_require_any_scope(
principal,
DEFINITION_READ_SCOPE,
DEFINITION_WRITE_SCOPE,
ADMIN_SCOPE,
)
diagnostics = validate_workflow_graph(payload.graph)
return WorkflowGraphValidationResponse(
valid=not any(item.severity == "error" for item in diagnostics),
diagnostics=[
WorkflowDiagnosticResponse(
severity=item.severity,
code=item.code,
message=item.message,
node_id=item.node_id,
field=item.field,
)
for item in diagnostics
],
)
__all__ = ["router"]