@open-mercato/core 0.10.1-develop.7816.1.6c959754cf → 0.10.1-develop.7821.1.e5115777cb
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/communication_channels/workers/poll-channel.js +44 -16
- package/dist/modules/communication_channels/workers/poll-channel.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +1 -1
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/workflows/api/definitions/[id]/route.js +15 -0
- package/dist/modules/workflows/api/definitions/[id]/route.js.map +2 -2
- package/dist/modules/workflows/api/definitions/generate/route.js +3 -1
- package/dist/modules/workflows/api/definitions/generate/route.js.map +2 -2
- package/dist/modules/workflows/api/definitions/route.js +15 -0
- package/dist/modules/workflows/api/definitions/route.js.map +2 -2
- package/dist/modules/workflows/lib/agent-result-mapping.js +3 -14
- package/dist/modules/workflows/lib/agent-result-mapping.js.map +2 -2
- package/dist/modules/workflows/lib/definition-evaluation.js +16 -2
- package/dist/modules/workflows/lib/definition-evaluation.js.map +2 -2
- package/dist/modules/workflows/lib/mapping-target-validation.js +85 -0
- package/dist/modules/workflows/lib/mapping-target-validation.js.map +7 -0
- package/dist/modules/workflows/lib/parallel-handler.js +10 -8
- package/dist/modules/workflows/lib/parallel-handler.js.map +2 -2
- package/dist/modules/workflows/lib/safe-mapping-path.js +38 -0
- package/dist/modules/workflows/lib/safe-mapping-path.js.map +7 -0
- package/dist/modules/workflows/lib/step-handler.js +5 -16
- package/dist/modules/workflows/lib/step-handler.js.map +2 -2
- package/package.json +8 -8
- package/src/modules/communication_channels/workers/poll-channel.ts +34 -5
- package/src/modules/sales/commands/documents.ts +3 -1
- package/src/modules/workflows/api/definitions/[id]/route.ts +17 -0
- package/src/modules/workflows/api/definitions/generate/route.ts +3 -1
- package/src/modules/workflows/api/definitions/route.ts +15 -0
- package/src/modules/workflows/i18n/de.json +1 -0
- package/src/modules/workflows/i18n/en.json +1 -0
- package/src/modules/workflows/i18n/es.json +1 -0
- package/src/modules/workflows/i18n/ko.json +1 -0
- package/src/modules/workflows/i18n/pl.json +1 -0
- package/src/modules/workflows/lib/agent-result-mapping.ts +3 -16
- package/src/modules/workflows/lib/definition-evaluation.ts +19 -2
- package/src/modules/workflows/lib/mapping-target-validation.ts +113 -0
- package/src/modules/workflows/lib/parallel-handler.ts +13 -9
- package/src/modules/workflows/lib/safe-mapping-path.ts +44 -0
- package/src/modules/workflows/lib/step-handler.ts +5 -32
|
@@ -1,17 +1,6 @@
|
|
|
1
1
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
2
|
+
import { safeGetNestedValue, safeSetNestedValue } from "./safe-mapping-path.js";
|
|
2
3
|
const logger = createLogger("workflows").child({ component: "agent-result-mapping" });
|
|
3
|
-
function getNestedValue(obj, path) {
|
|
4
|
-
return path.split(".").reduce((current, key) => current?.[key], obj);
|
|
5
|
-
}
|
|
6
|
-
function setNestedValue(obj, path, value) {
|
|
7
|
-
const keys = path.split(".");
|
|
8
|
-
const lastKey = keys.pop();
|
|
9
|
-
const target = keys.reduce((current, key) => {
|
|
10
|
-
if (!(key in current)) current[key] = {};
|
|
11
|
-
return current[key];
|
|
12
|
-
}, obj);
|
|
13
|
-
target[lastKey] = value;
|
|
14
|
-
}
|
|
15
4
|
function warnOnTemplateMapping(sourcePath) {
|
|
16
5
|
if (/\{\{.*\}\}/.test(sourcePath)) {
|
|
17
6
|
logger.warn(
|
|
@@ -41,9 +30,9 @@ function mapAgentResultToContext(envelope, outputMapping) {
|
|
|
41
30
|
const result = {};
|
|
42
31
|
for (const [targetKey, sourcePath] of Object.entries(outputMapping)) {
|
|
43
32
|
warnOnTemplateMapping(sourcePath);
|
|
44
|
-
const value =
|
|
33
|
+
const value = safeGetNestedValue(source, sourcePath);
|
|
45
34
|
if (value !== void 0) {
|
|
46
|
-
|
|
35
|
+
safeSetNestedValue(result, targetKey, value);
|
|
47
36
|
}
|
|
48
37
|
}
|
|
49
38
|
return result;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/workflows/lib/agent-result-mapping.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Maps an INVOKE_AGENT result into a workflow-context patch.\n *\n * An INVOKE_AGENT activity may declare an optional `outputMapping` (mirroring\n * SUB_WORKFLOW). When present, the agent's result is routed into the chosen\n * context keys instead of the legacy fixed keys. The mapping reads from a\n * normalized envelope so authors address a stable contract regardless of the\n * runtime (in-process vs OpenCode) or the resolution path (inline branch vs\n * parked-and-resumed worker).\n *\n * Returns the mapped patch when a non-empty mapping is provided, otherwise\n * `null` to signal the caller should fall back to its legacy fixed-key payload \u2014\n * keeping existing definitions byte-for-byte backward compatible.\n */\n\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('workflows').child({ component: 'agent-result-mapping' })\n\nexport type AgentResultEnvelope = {\n kind: 'auto_approved' | 'research' | 'user_task' | 'none_proposed' | 'artifact'\n agentId?: string\n proposalId?: string\n proposalPayload?: unknown\n data?: unknown\n /** Set for an `artifact` result: the files the run produced, by reference. */\n artifacts?: unknown[]\n}\n\nfunction
|
|
5
|
-
"mappings": "AAeA,SAAS,oBAAoB;
|
|
4
|
+
"sourcesContent": ["/**\n * Maps an INVOKE_AGENT result into a workflow-context patch.\n *\n * An INVOKE_AGENT activity may declare an optional `outputMapping` (mirroring\n * SUB_WORKFLOW). When present, the agent's result is routed into the chosen\n * context keys instead of the legacy fixed keys. The mapping reads from a\n * normalized envelope so authors address a stable contract regardless of the\n * runtime (in-process vs OpenCode) or the resolution path (inline branch vs\n * parked-and-resumed worker).\n *\n * Returns the mapped patch when a non-empty mapping is provided, otherwise\n * `null` to signal the caller should fall back to its legacy fixed-key payload \u2014\n * keeping existing definitions byte-for-byte backward compatible.\n */\n\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { safeGetNestedValue, safeSetNestedValue } from './safe-mapping-path'\n\nconst logger = createLogger('workflows').child({ component: 'agent-result-mapping' })\n\nexport type AgentResultEnvelope = {\n kind: 'auto_approved' | 'research' | 'user_task' | 'none_proposed' | 'artifact'\n agentId?: string\n proposalId?: string\n proposalPayload?: unknown\n data?: unknown\n /** Set for an `artifact` result: the files the run produced, by reference. */\n artifacts?: unknown[]\n}\n\nfunction warnOnTemplateMapping(sourcePath: string): void {\n if (/\\{\\{.*\\}\\}/.test(sourcePath)) {\n logger.warn(\n 'INVOKE_AGENT outputMapping value looks like a {{ }} template, but mapping values are plain ' +\n 'dot-paths into the agent result envelope (e.g. \"proposalPayload.riskScore\"). This entry ' +\n 'will not resolve and is being ignored.',\n { sourcePath }\n )\n }\n}\n\nexport function mapAgentResultToContext(\n envelope: AgentResultEnvelope,\n outputMapping: Record<string, string> | undefined | null\n): Record<string, any> | null {\n if (!outputMapping || Object.keys(outputMapping).length === 0) return null\n\n const source = {\n kind: envelope.kind,\n // `artifact` reports as `researcher` here for the same reason it routes onto\n // that handle: the disposition vocabulary describes DECISIONS, and producing\n // a file is not one. The `kind` above still says what actually came back.\n //\n // Note the two spellings are deliberate: `research` is the agent RESULT kind\n // (unification spec \u00A77), `researcher` the core-owned outcome handle it routes\n // to. Renaming the handle would rewrite persisted workflow graphs.\n disposition:\n envelope.kind === 'research' || envelope.kind === 'artifact' ? 'researcher' : envelope.kind,\n agentId: envelope.agentId,\n proposalId: envelope.proposalId,\n proposalPayload: envelope.proposalPayload,\n data: envelope.data,\n artifacts: envelope.artifacts,\n }\n\n const result: Record<string, any> = {}\n for (const [targetKey, sourcePath] of Object.entries(outputMapping)) {\n warnOnTemplateMapping(sourcePath)\n const value = safeGetNestedValue(source, sourcePath)\n if (value !== undefined) {\n safeSetNestedValue(result, targetKey, value)\n }\n }\n return result\n}\n"],
|
|
5
|
+
"mappings": "AAeA,SAAS,oBAAoB;AAC7B,SAAS,oBAAoB,0BAA0B;AAEvD,MAAM,SAAS,aAAa,WAAW,EAAE,MAAM,EAAE,WAAW,uBAAuB,CAAC;AAYpF,SAAS,sBAAsB,YAA0B;AACvD,MAAI,aAAa,KAAK,UAAU,GAAG;AACjC,WAAO;AAAA,MACL;AAAA,MAGA,EAAE,WAAW;AAAA,IACf;AAAA,EACF;AACF;AAEO,SAAS,wBACd,UACA,eAC4B;AAC5B,MAAI,CAAC,iBAAiB,OAAO,KAAK,aAAa,EAAE,WAAW,EAAG,QAAO;AAEtE,QAAM,SAAS;AAAA,IACb,MAAM,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAQf,aACE,SAAS,SAAS,cAAc,SAAS,SAAS,aAAa,eAAe,SAAS;AAAA,IACzF,SAAS,SAAS;AAAA,IAClB,YAAY,SAAS;AAAA,IACrB,iBAAiB,SAAS;AAAA,IAC1B,MAAM,SAAS;AAAA,IACf,WAAW,SAAS;AAAA,EACtB;AAEA,QAAM,SAA8B,CAAC;AACrC,aAAW,CAAC,WAAW,UAAU,KAAK,OAAO,QAAQ,aAAa,GAAG;AACnE,0BAAsB,UAAU;AAChC,UAAM,QAAQ,mBAAmB,QAAQ,UAAU;AACnD,QAAI,UAAU,QAAW;AACvB,yBAAmB,QAAQ,WAAW,KAAK;AAAA,IAC7C;AAAA,EACF;AACA,SAAO;AACT;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -9,6 +9,12 @@ import {
|
|
|
9
9
|
} from "./definition-error-body.js";
|
|
10
10
|
import { collectUnresolvedContextRefWarnings } from "./expression-refs.js";
|
|
11
11
|
import { definitionToGraph, validateWorkflowGraph } from "./graph-utils.js";
|
|
12
|
+
import {
|
|
13
|
+
collectUnsafeMappingTargetIssues,
|
|
14
|
+
mappingTargetIssuesForApi,
|
|
15
|
+
UNSAFE_MAPPING_TARGET_MESSAGE,
|
|
16
|
+
UNSAFE_MAPPING_TARGET_MESSAGE_KEY
|
|
17
|
+
} from "./mapping-target-validation.js";
|
|
12
18
|
const UNRESOLVED_CONTEXT_REF_MESSAGE = {
|
|
13
19
|
key: "workflows.visualEditor.problems.unresolvedContextRef",
|
|
14
20
|
fallback: 'Context reference "{path}" is not provided by any earlier step, trigger, or input'
|
|
@@ -47,7 +53,12 @@ function evaluateWorkflowDefinition(definition, options = {}) {
|
|
|
47
53
|
const { nodes, edges } = definitionToGraph(definition, { autoLayout: false });
|
|
48
54
|
const graphErrors = validateWorkflowGraph(nodes, edges);
|
|
49
55
|
const parsed = workflowDefinitionDataSchema.safeParse(definition);
|
|
50
|
-
const
|
|
56
|
+
const mappingMessage = options.translate ? options.translate(UNSAFE_MAPPING_TARGET_MESSAGE_KEY, UNSAFE_MAPPING_TARGET_MESSAGE, {}) : UNSAFE_MAPPING_TARGET_MESSAGE;
|
|
57
|
+
const mappingIssues = collectUnsafeMappingTargetIssues(definition, mappingMessage);
|
|
58
|
+
const zodIssues = [
|
|
59
|
+
...parsed.success ? [] : parsed.error.issues,
|
|
60
|
+
...mappingIssues
|
|
61
|
+
];
|
|
51
62
|
const ledger = options.ledger ?? null;
|
|
52
63
|
const configWarnings = [
|
|
53
64
|
...collectActivityConfigWarnings(definition),
|
|
@@ -67,7 +78,10 @@ function evaluateWorkflowDefinition(definition, options = {}) {
|
|
|
67
78
|
valid: errors === 0,
|
|
68
79
|
errorCount: errors,
|
|
69
80
|
warningCount: warnings,
|
|
70
|
-
issues:
|
|
81
|
+
issues: [
|
|
82
|
+
...parsed.success ? [] : normalizeDefinitionValidationIssues(parsed.error),
|
|
83
|
+
...mappingTargetIssuesForApi(definition, [], mappingMessage)
|
|
84
|
+
],
|
|
71
85
|
problems
|
|
72
86
|
};
|
|
73
87
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/workflows/lib/definition-evaluation.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * One evaluation path for a workflow definition.\n *\n * The Studio's Problems panel and the definition API routes historically ran\n * the same checks through two different normalizers: the panel builds\n * `WorkflowValidationIssue[]` via `collectValidationIssues`, the routes build\n * the structured `{path, code, message, expected?, got?}` body via\n * `normalizeDefinitionValidationIssues`. Neither reimplements the checks \u2014 they\n * share `workflowDefinitionDataSchema` \u2014 but nothing composed both.\n *\n * This module composes them, and NOTHING ELSE. Every check below is an existing\n * exported function; a second validator that could disagree with the panel is\n * precisely the failure mode to avoid, so callers that need \"what would the\n * Studio show me?\" MUST come through here rather than re-deriving the recipe.\n */\n\nimport { collectActivityConfigWarnings } from '../data/activity-config-warnings'\nimport { workflowDefinitionDataSchema } from '../data/validators'\nimport type { WorkflowDefinitionData } from '../data/entities'\nimport {\n collectValidationIssues,\n countIssuesBySeverity,\n type WorkflowIssueTranslator,\n type WorkflowValidationIssue,\n type ZodIssueLike,\n} from './collect-validation-issues'\nimport type { ContextLedger } from './context-ledger'\nimport {\n normalizeDefinitionValidationIssues,\n type DefinitionValidationIssue,\n} from './definition-error-body'\nimport { collectUnresolvedContextRefWarnings } from './expression-refs'\nimport { definitionToGraph, validateWorkflowGraph } from './graph-utils'\n\n/**\n * Problems-panel copy for an unresolved `{{context.*}}` reference. Shared so\n * the Studio and every server-side evaluator emit the identical warning instead\n * of drifting apart on the message text.\n */\nexport const UNRESOLVED_CONTEXT_REF_MESSAGE = {\n key: 'workflows.visualEditor.problems.unresolvedContextRef',\n fallback: 'Context reference \"{path}\" is not provided by any earlier step, trigger, or input',\n} as const\n\nfunction interpolateFallback(fallback: string, params: Record<string, string>): string {\n return fallback.replace(/\\{(\\w+)\\}/g, (match, name: string) =>\n Object.prototype.hasOwnProperty.call(params, name) ? params[name] : match,\n )\n}\n\n/**\n * Map unresolved `{{context.*}}` references onto Problems-panel warnings.\n * `translate` is optional \u2014 omit it (server, agent tools) to get the English\n * fallback the panel itself falls back to.\n */\nexport function toUnresolvedContextRefIssues(\n definition: WorkflowDefinitionData,\n ledger: ContextLedger,\n translate?: WorkflowIssueTranslator,\n): ZodIssueLike[] {\n return collectUnresolvedContextRefWarnings(definition, ledger).map((warning) => ({\n path: warning.path,\n message: translate\n ? translate(UNRESOLVED_CONTEXT_REF_MESSAGE.key, UNRESOLVED_CONTEXT_REF_MESSAGE.fallback, {\n path: warning.refPath,\n })\n : interpolateFallback(UNRESOLVED_CONTEXT_REF_MESSAGE.fallback, { path: warning.refPath }),\n }))\n}\n\nexport interface EvaluateWorkflowDefinitionOptions {\n /**\n * Server-computed context ledger. When supplied, unresolved-reference\n * warnings join the result exactly as they do in the Studio; when omitted\n * those warnings are skipped (they are advisory and never blocking).\n */\n ledger?: ContextLedger | null\n}\n\nexport interface WorkflowDefinitionEvaluation {\n /** No blocking errors \u2014 the same bar the Studio's Save handler enforces. */\n valid: boolean\n errorCount: number\n warningCount: number\n /**\n * The structured issue format: identical in shape and content to the\n * `details[]` array the definition create/update routes return on a 400.\n * Empty when the definition parses.\n */\n issues: DefinitionValidationIssue[]\n /** Exactly what the Studio's Problems panel renders, in its order. */\n problems: WorkflowValidationIssue[]\n}\n\n/**\n * Run the Studio Problems-panel evaluation plus the API routes' structured-body\n * normalizer over one definition.\n */\nexport function evaluateWorkflowDefinition(\n definition: WorkflowDefinitionData,\n options: EvaluateWorkflowDefinitionOptions = {},\n): WorkflowDefinitionEvaluation {\n // The Studio only ever evaluates well-formed canvas state, but an agent can\n // hand us anything. A definition whose `steps`/`transitions` are not arrays\n // cannot be turned into a graph at all, so it answers with the schema issues\n // alone rather than throwing out of a validation call.\n const shaped =\n Array.isArray((definition as { steps?: unknown })?.steps) &&\n Array.isArray((definition as { transitions?: unknown })?.transitions)\n if (!shaped) {\n const parsedShape = workflowDefinitionDataSchema.safeParse(definition)\n const structuralIssues = parsedShape.success\n ? []\n : normalizeDefinitionValidationIssues(parsedShape.error)\n return {\n valid: false,\n errorCount: structuralIssues.length,\n warningCount: 0,\n issues: structuralIssues,\n problems: structuralIssues.map((entry, index) => ({\n id: `schema-${index}`,\n severity: 'error' as const,\n message: entry.path.length ? `${entry.path.join('.')} - ${entry.message}` : entry.message,\n })),\n }\n }\n\n const { nodes, edges } = definitionToGraph(definition, { autoLayout: false })\n const graphErrors = validateWorkflowGraph(nodes, edges)\n const parsed = workflowDefinitionDataSchema.safeParse(definition)\n const zodIssues: ZodIssueLike[] = parsed.success ? [] : parsed.error.issues\n const ledger = options.ledger ?? null\n const configWarnings: ZodIssueLike[] = [\n ...collectActivityConfigWarnings(definition),\n ...(ledger ? toUnresolvedContextRefIssues(definition, ledger) : []),\n ]\n\n const problems = collectValidationIssues({\n graphErrors,\n zodIssues,\n configWarnings,\n nodes,\n edges,\n definition,\n ledger: ledger ?? undefined,\n })\n const { errors, warnings } = countIssuesBySeverity(problems)\n\n return {\n valid: errors === 0,\n errorCount: errors,\n warningCount: warnings,\n issues: parsed.success ? [] : normalizeDefinitionValidationIssues(parsed.error),\n problems,\n }\n}\n"],
|
|
5
|
-
"mappings": "AAgBA,SAAS,qCAAqC;AAC9C,SAAS,oCAAoC;AAE7C;AAAA,EACE;AAAA,EACA;AAAA,OAIK;AAEP;AAAA,EACE;AAAA,OAEK;AACP,SAAS,2CAA2C;AACpD,SAAS,mBAAmB,6BAA6B;
|
|
4
|
+
"sourcesContent": ["/**\n * One evaluation path for a workflow definition.\n *\n * The Studio's Problems panel and the definition API routes historically ran\n * the same checks through two different normalizers: the panel builds\n * `WorkflowValidationIssue[]` via `collectValidationIssues`, the routes build\n * the structured `{path, code, message, expected?, got?}` body via\n * `normalizeDefinitionValidationIssues`. Neither reimplements the checks \u2014 they\n * share `workflowDefinitionDataSchema` \u2014 but nothing composed both.\n *\n * This module composes them, and NOTHING ELSE. Every check below is an existing\n * exported function; a second validator that could disagree with the panel is\n * precisely the failure mode to avoid, so callers that need \"what would the\n * Studio show me?\" MUST come through here rather than re-deriving the recipe.\n */\n\nimport { collectActivityConfigWarnings } from '../data/activity-config-warnings'\nimport { workflowDefinitionDataSchema } from '../data/validators'\nimport type { WorkflowDefinitionData } from '../data/entities'\nimport {\n collectValidationIssues,\n countIssuesBySeverity,\n type WorkflowIssueTranslator,\n type WorkflowValidationIssue,\n type ZodIssueLike,\n} from './collect-validation-issues'\nimport type { ContextLedger } from './context-ledger'\nimport {\n normalizeDefinitionValidationIssues,\n type DefinitionValidationIssue,\n} from './definition-error-body'\nimport { collectUnresolvedContextRefWarnings } from './expression-refs'\nimport { definitionToGraph, validateWorkflowGraph } from './graph-utils'\nimport {\n collectUnsafeMappingTargetIssues,\n mappingTargetIssuesForApi,\n UNSAFE_MAPPING_TARGET_MESSAGE,\n UNSAFE_MAPPING_TARGET_MESSAGE_KEY,\n} from './mapping-target-validation'\n\n/**\n * Problems-panel copy for an unresolved `{{context.*}}` reference. Shared so\n * the Studio and every server-side evaluator emit the identical warning instead\n * of drifting apart on the message text.\n */\nexport const UNRESOLVED_CONTEXT_REF_MESSAGE = {\n key: 'workflows.visualEditor.problems.unresolvedContextRef',\n fallback: 'Context reference \"{path}\" is not provided by any earlier step, trigger, or input',\n} as const\n\nfunction interpolateFallback(fallback: string, params: Record<string, string>): string {\n return fallback.replace(/\\{(\\w+)\\}/g, (match, name: string) =>\n Object.prototype.hasOwnProperty.call(params, name) ? params[name] : match,\n )\n}\n\n/**\n * Map unresolved `{{context.*}}` references onto Problems-panel warnings.\n * `translate` is optional \u2014 omit it (server, agent tools) to get the English\n * fallback the panel itself falls back to.\n */\nexport function toUnresolvedContextRefIssues(\n definition: WorkflowDefinitionData,\n ledger: ContextLedger,\n translate?: WorkflowIssueTranslator,\n): ZodIssueLike[] {\n return collectUnresolvedContextRefWarnings(definition, ledger).map((warning) => ({\n path: warning.path,\n message: translate\n ? translate(UNRESOLVED_CONTEXT_REF_MESSAGE.key, UNRESOLVED_CONTEXT_REF_MESSAGE.fallback, {\n path: warning.refPath,\n })\n : interpolateFallback(UNRESOLVED_CONTEXT_REF_MESSAGE.fallback, { path: warning.refPath }),\n }))\n}\n\nexport interface EvaluateWorkflowDefinitionOptions {\n /**\n * Server-computed context ledger. When supplied, unresolved-reference\n * warnings join the result exactly as they do in the Studio; when omitted\n * those warnings are skipped (they are advisory and never blocking).\n */\n ledger?: ContextLedger | null\n translate?: WorkflowIssueTranslator\n}\n\nexport interface WorkflowDefinitionEvaluation {\n /** No blocking errors \u2014 the same bar the Studio's Save handler enforces. */\n valid: boolean\n errorCount: number\n warningCount: number\n /**\n * The structured issue format: identical in shape and content to the\n * `details[]` array the definition create/update routes return on a 400.\n * Empty when the definition parses.\n */\n issues: DefinitionValidationIssue[]\n /** Exactly what the Studio's Problems panel renders, in its order. */\n problems: WorkflowValidationIssue[]\n}\n\n/**\n * Run the Studio Problems-panel evaluation plus the API routes' structured-body\n * normalizer over one definition.\n */\nexport function evaluateWorkflowDefinition(\n definition: WorkflowDefinitionData,\n options: EvaluateWorkflowDefinitionOptions = {},\n): WorkflowDefinitionEvaluation {\n // The Studio only ever evaluates well-formed canvas state, but an agent can\n // hand us anything. A definition whose `steps`/`transitions` are not arrays\n // cannot be turned into a graph at all, so it answers with the schema issues\n // alone rather than throwing out of a validation call.\n const shaped =\n Array.isArray((definition as { steps?: unknown })?.steps) &&\n Array.isArray((definition as { transitions?: unknown })?.transitions)\n if (!shaped) {\n const parsedShape = workflowDefinitionDataSchema.safeParse(definition)\n const structuralIssues = parsedShape.success\n ? []\n : normalizeDefinitionValidationIssues(parsedShape.error)\n return {\n valid: false,\n errorCount: structuralIssues.length,\n warningCount: 0,\n issues: structuralIssues,\n problems: structuralIssues.map((entry, index) => ({\n id: `schema-${index}`,\n severity: 'error' as const,\n message: entry.path.length ? `${entry.path.join('.')} - ${entry.message}` : entry.message,\n })),\n }\n }\n\n const { nodes, edges } = definitionToGraph(definition, { autoLayout: false })\n const graphErrors = validateWorkflowGraph(nodes, edges)\n const parsed = workflowDefinitionDataSchema.safeParse(definition)\n const mappingMessage = options.translate\n ? options.translate(UNSAFE_MAPPING_TARGET_MESSAGE_KEY, UNSAFE_MAPPING_TARGET_MESSAGE, {})\n : UNSAFE_MAPPING_TARGET_MESSAGE\n const mappingIssues = collectUnsafeMappingTargetIssues(definition, mappingMessage)\n const zodIssues: ZodIssueLike[] = [\n ...(parsed.success ? [] : parsed.error.issues),\n ...mappingIssues,\n ]\n const ledger = options.ledger ?? null\n const configWarnings: ZodIssueLike[] = [\n ...collectActivityConfigWarnings(definition),\n ...(ledger ? toUnresolvedContextRefIssues(definition, ledger) : []),\n ]\n\n const problems = collectValidationIssues({\n graphErrors,\n zodIssues,\n configWarnings,\n nodes,\n edges,\n definition,\n ledger: ledger ?? undefined,\n })\n const { errors, warnings } = countIssuesBySeverity(problems)\n\n return {\n valid: errors === 0,\n errorCount: errors,\n warningCount: warnings,\n issues: [\n ...(parsed.success ? [] : normalizeDefinitionValidationIssues(parsed.error)),\n ...mappingTargetIssuesForApi(definition, [], mappingMessage),\n ],\n problems,\n }\n}\n"],
|
|
5
|
+
"mappings": "AAgBA,SAAS,qCAAqC;AAC9C,SAAS,oCAAoC;AAE7C;AAAA,EACE;AAAA,EACA;AAAA,OAIK;AAEP;AAAA,EACE;AAAA,OAEK;AACP,SAAS,2CAA2C;AACpD,SAAS,mBAAmB,6BAA6B;AACzD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAOA,MAAM,iCAAiC;AAAA,EAC5C,KAAK;AAAA,EACL,UAAU;AACZ;AAEA,SAAS,oBAAoB,UAAkB,QAAwC;AACrF,SAAO,SAAS;AAAA,IAAQ;AAAA,IAAc,CAAC,OAAO,SAC5C,OAAO,UAAU,eAAe,KAAK,QAAQ,IAAI,IAAI,OAAO,IAAI,IAAI;AAAA,EACtE;AACF;AAOO,SAAS,6BACd,YACA,QACA,WACgB;AAChB,SAAO,oCAAoC,YAAY,MAAM,EAAE,IAAI,CAAC,aAAa;AAAA,IAC/E,MAAM,QAAQ;AAAA,IACd,SAAS,YACL,UAAU,+BAA+B,KAAK,+BAA+B,UAAU;AAAA,MACrF,MAAM,QAAQ;AAAA,IAChB,CAAC,IACD,oBAAoB,+BAA+B,UAAU,EAAE,MAAM,QAAQ,QAAQ,CAAC;AAAA,EAC5F,EAAE;AACJ;AA+BO,SAAS,2BACd,YACA,UAA6C,CAAC,GAChB;AAK9B,QAAM,SACJ,MAAM,QAAS,YAAoC,KAAK,KACxD,MAAM,QAAS,YAA0C,WAAW;AACtE,MAAI,CAAC,QAAQ;AACX,UAAM,cAAc,6BAA6B,UAAU,UAAU;AACrE,UAAM,mBAAmB,YAAY,UACjC,CAAC,IACD,oCAAoC,YAAY,KAAK;AACzD,WAAO;AAAA,MACL,OAAO;AAAA,MACP,YAAY,iBAAiB;AAAA,MAC7B,cAAc;AAAA,MACd,QAAQ;AAAA,MACR,UAAU,iBAAiB,IAAI,CAAC,OAAO,WAAW;AAAA,QAChD,IAAI,UAAU,KAAK;AAAA,QACnB,UAAU;AAAA,QACV,SAAS,MAAM,KAAK,SAAS,GAAG,MAAM,KAAK,KAAK,GAAG,CAAC,MAAM,MAAM,OAAO,KAAK,MAAM;AAAA,MACpF,EAAE;AAAA,IACJ;AAAA,EACF;AAEA,QAAM,EAAE,OAAO,MAAM,IAAI,kBAAkB,YAAY,EAAE,YAAY,MAAM,CAAC;AAC5E,QAAM,cAAc,sBAAsB,OAAO,KAAK;AACtD,QAAM,SAAS,6BAA6B,UAAU,UAAU;AAChE,QAAM,iBAAiB,QAAQ,YAC3B,QAAQ,UAAU,mCAAmC,+BAA+B,CAAC,CAAC,IACtF;AACJ,QAAM,gBAAgB,iCAAiC,YAAY,cAAc;AACjF,QAAM,YAA4B;AAAA,IAChC,GAAI,OAAO,UAAU,CAAC,IAAI,OAAO,MAAM;AAAA,IACvC,GAAG;AAAA,EACL;AACA,QAAM,SAAS,QAAQ,UAAU;AACjC,QAAM,iBAAiC;AAAA,IACrC,GAAG,8BAA8B,UAAU;AAAA,IAC3C,GAAI,SAAS,6BAA6B,YAAY,MAAM,IAAI,CAAC;AAAA,EACnE;AAEA,QAAM,WAAW,wBAAwB;AAAA,IACvC;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,QAAQ,UAAU;AAAA,EACpB,CAAC;AACD,QAAM,EAAE,QAAQ,SAAS,IAAI,sBAAsB,QAAQ;AAE3D,SAAO;AAAA,IACL,OAAO,WAAW;AAAA,IAClB,YAAY;AAAA,IACZ,cAAc;AAAA,IACd,QAAQ;AAAA,MACN,GAAI,OAAO,UAAU,CAAC,IAAI,oCAAoC,OAAO,KAAK;AAAA,MAC1E,GAAG,0BAA0B,YAAY,CAAC,GAAG,cAAc;AAAA,IAC7D;AAAA,IACA;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { isSafeMappingTargetPath } from "./safe-mapping-path.js";
|
|
2
|
+
const UNSAFE_MAPPING_TARGET_MESSAGE_KEY = "workflows.validation.unsafeMappingTarget";
|
|
3
|
+
const UNSAFE_MAPPING_TARGET_MESSAGE = "Mapping target paths cannot contain __proto__, constructor, or prototype segments";
|
|
4
|
+
function isRecord(value) {
|
|
5
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
function collectMappingIssues(value, path, message) {
|
|
8
|
+
if (!isRecord(value)) return [];
|
|
9
|
+
return Object.keys(value).filter((targetPath) => !isSafeMappingTargetPath(targetPath)).map((targetPath) => ({
|
|
10
|
+
path: [...path, targetPath],
|
|
11
|
+
message
|
|
12
|
+
}));
|
|
13
|
+
}
|
|
14
|
+
function collectActivityIssues(activities, path, message) {
|
|
15
|
+
if (!Array.isArray(activities)) return [];
|
|
16
|
+
return activities.flatMap((activity, activityIndex) => {
|
|
17
|
+
if (!isRecord(activity) || activity.activityType !== "INVOKE_AGENT") return [];
|
|
18
|
+
const config = isRecord(activity.config) ? activity.config : {};
|
|
19
|
+
return collectMappingIssues(config.outputMapping, [
|
|
20
|
+
...path,
|
|
21
|
+
activityIndex,
|
|
22
|
+
"config",
|
|
23
|
+
"outputMapping"
|
|
24
|
+
], message);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function collectUnsafeMappingTargetIssues(definition, message = UNSAFE_MAPPING_TARGET_MESSAGE) {
|
|
28
|
+
const stepIssues = Array.isArray(definition.steps) ? definition.steps.flatMap((step, stepIndex) => {
|
|
29
|
+
if (!isRecord(step)) return [];
|
|
30
|
+
const config = isRecord(step.config) ? step.config : {};
|
|
31
|
+
const subWorkflowConfig = isRecord(step.subWorkflowConfig) ? step.subWorkflowConfig : {};
|
|
32
|
+
const issues = [];
|
|
33
|
+
if (step.stepType === "SUB_WORKFLOW") {
|
|
34
|
+
issues.push(
|
|
35
|
+
...collectMappingIssues(config.inputMapping, ["steps", stepIndex, "config", "inputMapping"], message),
|
|
36
|
+
...collectMappingIssues(config.outputMapping, ["steps", stepIndex, "config", "outputMapping"], message),
|
|
37
|
+
...collectMappingIssues(subWorkflowConfig.inputMapping, [
|
|
38
|
+
"steps",
|
|
39
|
+
stepIndex,
|
|
40
|
+
"subWorkflowConfig",
|
|
41
|
+
"inputMapping"
|
|
42
|
+
], message),
|
|
43
|
+
...collectMappingIssues(subWorkflowConfig.outputMapping, [
|
|
44
|
+
"steps",
|
|
45
|
+
stepIndex,
|
|
46
|
+
"subWorkflowConfig",
|
|
47
|
+
"outputMapping"
|
|
48
|
+
], message)
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
if (step.stepType === "PARALLEL_JOIN") {
|
|
52
|
+
issues.push(
|
|
53
|
+
...collectMappingIssues(config.outputMapping, ["steps", stepIndex, "config", "outputMapping"], message)
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
issues.push(...collectActivityIssues(step.activities, ["steps", stepIndex, "activities"], message));
|
|
57
|
+
return issues;
|
|
58
|
+
}) : [];
|
|
59
|
+
const transitionIssues = Array.isArray(definition.transitions) ? definition.transitions.flatMap((transition, transitionIndex) => {
|
|
60
|
+
if (!isRecord(transition)) return [];
|
|
61
|
+
return collectActivityIssues(transition.activities, [
|
|
62
|
+
"transitions",
|
|
63
|
+
transitionIndex,
|
|
64
|
+
"activities"
|
|
65
|
+
], message);
|
|
66
|
+
}) : [];
|
|
67
|
+
return [...stepIssues, ...transitionIssues];
|
|
68
|
+
}
|
|
69
|
+
function mappingTargetIssuesForApi(definition, prefix = [], message = UNSAFE_MAPPING_TARGET_MESSAGE) {
|
|
70
|
+
return collectUnsafeMappingTargetIssues(definition, message).map((issue) => ({
|
|
71
|
+
path: [
|
|
72
|
+
...prefix,
|
|
73
|
+
...issue.path.map((segment) => typeof segment === "number" ? segment : String(segment))
|
|
74
|
+
],
|
|
75
|
+
code: "custom",
|
|
76
|
+
message: issue.message
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
export {
|
|
80
|
+
UNSAFE_MAPPING_TARGET_MESSAGE,
|
|
81
|
+
UNSAFE_MAPPING_TARGET_MESSAGE_KEY,
|
|
82
|
+
collectUnsafeMappingTargetIssues,
|
|
83
|
+
mappingTargetIssuesForApi
|
|
84
|
+
};
|
|
85
|
+
//# sourceMappingURL=mapping-target-validation.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/workflows/lib/mapping-target-validation.ts"],
|
|
4
|
+
"sourcesContent": ["import type { WorkflowDefinitionData } from '../data/entities'\nimport type { DefinitionValidationIssue } from './definition-error-body'\nimport type { ZodIssueLike } from './collect-validation-issues'\nimport { isSafeMappingTargetPath } from './safe-mapping-path'\n\nexport const UNSAFE_MAPPING_TARGET_MESSAGE_KEY = 'workflows.validation.unsafeMappingTarget'\n\nexport const UNSAFE_MAPPING_TARGET_MESSAGE =\n 'Mapping target paths cannot contain __proto__, constructor, or prototype segments'\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nfunction collectMappingIssues(\n value: unknown,\n path: Array<string | number>,\n message: string,\n): ZodIssueLike[] {\n if (!isRecord(value)) return []\n return Object.keys(value)\n .filter((targetPath) => !isSafeMappingTargetPath(targetPath))\n .map((targetPath) => ({\n path: [...path, targetPath],\n message,\n }))\n}\n\nfunction collectActivityIssues(\n activities: unknown,\n path: Array<string | number>,\n message: string,\n): ZodIssueLike[] {\n if (!Array.isArray(activities)) return []\n return activities.flatMap((activity, activityIndex) => {\n if (!isRecord(activity) || activity.activityType !== 'INVOKE_AGENT') return []\n const config = isRecord(activity.config) ? activity.config : {}\n return collectMappingIssues(config.outputMapping, [\n ...path,\n activityIndex,\n 'config',\n 'outputMapping',\n ], message)\n })\n}\n\nexport function collectUnsafeMappingTargetIssues(\n definition: WorkflowDefinitionData,\n message = UNSAFE_MAPPING_TARGET_MESSAGE,\n): ZodIssueLike[] {\n const stepIssues = Array.isArray(definition.steps)\n ? definition.steps.flatMap((step, stepIndex) => {\n if (!isRecord(step)) return []\n const config = isRecord(step.config) ? step.config : {}\n const subWorkflowConfig = isRecord(step.subWorkflowConfig) ? step.subWorkflowConfig : {}\n const issues: ZodIssueLike[] = []\n\n if (step.stepType === 'SUB_WORKFLOW') {\n issues.push(\n ...collectMappingIssues(config.inputMapping, ['steps', stepIndex, 'config', 'inputMapping'], message),\n ...collectMappingIssues(config.outputMapping, ['steps', stepIndex, 'config', 'outputMapping'], message),\n ...collectMappingIssues(subWorkflowConfig.inputMapping, [\n 'steps',\n stepIndex,\n 'subWorkflowConfig',\n 'inputMapping',\n ], message),\n ...collectMappingIssues(subWorkflowConfig.outputMapping, [\n 'steps',\n stepIndex,\n 'subWorkflowConfig',\n 'outputMapping',\n ], message),\n )\n }\n if (step.stepType === 'PARALLEL_JOIN') {\n issues.push(\n ...collectMappingIssues(config.outputMapping, ['steps', stepIndex, 'config', 'outputMapping'], message),\n )\n }\n issues.push(...collectActivityIssues(step.activities, ['steps', stepIndex, 'activities'], message))\n return issues\n })\n : []\n\n const transitionIssues = Array.isArray(definition.transitions)\n ? definition.transitions.flatMap((transition, transitionIndex) => {\n if (!isRecord(transition)) return []\n return collectActivityIssues(transition.activities, [\n 'transitions',\n transitionIndex,\n 'activities',\n ], message)\n })\n : []\n\n return [...stepIssues, ...transitionIssues]\n}\n\nexport function mappingTargetIssuesForApi(\n definition: WorkflowDefinitionData,\n prefix: Array<string | number> = [],\n message = UNSAFE_MAPPING_TARGET_MESSAGE,\n): DefinitionValidationIssue[] {\n return collectUnsafeMappingTargetIssues(definition, message).map((issue) => ({\n path: [\n ...prefix,\n ...issue.path.map((segment) => (typeof segment === 'number' ? segment : String(segment))),\n ],\n code: 'custom',\n message: issue.message,\n }))\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import * as stepHandler from "./step-handler.js";
|
|
16
16
|
import { branchToken, mergeTokenContext } from "./execution-token.js";
|
|
17
17
|
import { WORKFLOW_ERROR_CONTEXT_KEY, buildErrorContextEntry } from "./error-routing.js";
|
|
18
|
+
import { isSafeMappingTargetPath, safeGetNestedValue } from "./safe-mapping-path.js";
|
|
18
19
|
async function resumeBranch(em, options) {
|
|
19
20
|
const branch = await em.findOne(WorkflowBranchInstance, {
|
|
20
21
|
id: options.branchInstanceId,
|
|
@@ -46,15 +47,19 @@ async function resumeBranch(em, options) {
|
|
|
46
47
|
await em.flush();
|
|
47
48
|
return true;
|
|
48
49
|
}
|
|
50
|
+
function applyParallelJoinOutputMapping(context, outputMapping) {
|
|
51
|
+
for (const [topKey, sourcePath] of Object.entries(outputMapping)) {
|
|
52
|
+
if (topKey === "branches" || !isSafeMappingTargetPath(topKey)) continue;
|
|
53
|
+
const value = safeGetNestedValue(context, sourcePath);
|
|
54
|
+
if (value !== void 0) context[topKey] = value;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
49
57
|
function normalizeWorkflowUserId(value) {
|
|
50
58
|
if (typeof value !== "string") return null;
|
|
51
59
|
const trimmed = value.trim();
|
|
52
60
|
if (!trimmed || trimmed.startsWith("trigger:")) return null;
|
|
53
61
|
return trimmed;
|
|
54
62
|
}
|
|
55
|
-
function getNestedValue(obj, path) {
|
|
56
|
-
return path.split(".").reduce((current, key) => current == null ? void 0 : current[key], obj);
|
|
57
|
-
}
|
|
58
63
|
async function openFork(em, instance, definition, forkStepDef) {
|
|
59
64
|
const forkStepId = forkStepDef.stepId;
|
|
60
65
|
const joinStepId = forkStepDef.config?.joinStepId;
|
|
@@ -401,11 +406,7 @@ async function fireJoin(em, instance, definition, branches, forkStepId) {
|
|
|
401
406
|
};
|
|
402
407
|
const outputMapping = joinStep?.config?.outputMapping;
|
|
403
408
|
if (outputMapping) {
|
|
404
|
-
|
|
405
|
-
if (topKey === "branches") continue;
|
|
406
|
-
const value = getNestedValue(nextContext, sourcePath);
|
|
407
|
-
if (value !== void 0) nextContext[topKey] = value;
|
|
408
|
-
}
|
|
409
|
+
applyParallelJoinOutputMapping(nextContext, outputMapping);
|
|
409
410
|
}
|
|
410
411
|
const afterJoinStepId = (definition.definition.transitions || []).find(
|
|
411
412
|
(transition) => transition.fromStepId === joinStepId
|
|
@@ -427,6 +428,7 @@ async function fireJoin(em, instance, definition, branches, forkStepId) {
|
|
|
427
428
|
}
|
|
428
429
|
export {
|
|
429
430
|
advanceBranches,
|
|
431
|
+
applyParallelJoinOutputMapping,
|
|
430
432
|
openFork,
|
|
431
433
|
resumeBranch,
|
|
432
434
|
resumeBranchAfterActivities
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/workflows/lib/parallel-handler.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Workflows Module - Parallel Fork / Join Handler\n *\n * Implements PARALLEL_FORK / PARALLEL_JOIN execution with a multi-token model:\n *\n * - `openFork` turns a PARALLEL_FORK step into N persistent branch tokens\n * (`WorkflowBranchInstance`), one per outgoing `auto` transition, and puts the\n * instance into the dormant `FORKED` state.\n * - `advanceBranches` runs the interleaved loop: while the instance is FORKED it\n * advances each ACTIVE branch one step at a time (BPMN semantics \u2014 single lock,\n * no thread-level concurrency). A branch pauses independently (USER_TASK /\n * signal / timer / async activity) without blocking siblings.\n * - When every branch has reached its JOIN (wait-all), `fireJoin` merges the\n * branch namespaces back into `instance.context`, applies optional\n * `outputMapping`, and resumes the root token at the step after the JOIN.\n * - A failed branch cancels its siblings and fails the whole instance.\n *\n * All work happens under the executor's pessimistic instance lock + transaction,\n * so wait-all counting and JOIN firing are race-free.\n */\n\nimport { EntityManager, LockMode } from '@mikro-orm/core'\nimport type { AwilixContainer } from 'awilix'\nimport {\n WorkflowInstance,\n WorkflowBranchInstance,\n WorkflowDefinition,\n StepInstance,\n UserTask,\n WorkflowEvent,\n} from '../data/entities'\nimport { logWorkflowEvent } from './event-logger'\nimport {\n ASYNC_ACTIVITY_OUTCOME_EVENT_TYPES,\n readPendingAsyncJobIds,\n resolveAsyncActivityWait,\n} from './async-activity-wait'\nimport * as stepHandler from './step-handler'\nimport { branchToken, mergeTokenContext, type ExecutionToken } from './execution-token'\nimport { WORKFLOW_ERROR_CONTEXT_KEY, buildErrorContextEntry } from './error-routing'\n\nexport interface AdvanceBranchesResult {\n outcome: 'joined' | 'waiting' | 'failed'\n error?: string\n}\n\nexport interface ResumeBranchOptions {\n instanceId: string\n branchInstanceId: string\n tenantId: string\n organizationId: string\n /** Values merged into the branch's private namespace before resuming (task form data, signal payload, \u2026). */\n contextMerge?: Record<string, any>\n /** Step instance to exit (the paused step's instance) before the branch advances past it. */\n exitStepInstanceId?: string | null\n exitOutput?: any\n}\n\n/**\n * Resume a single paused/waiting branch: optionally merge data into its\n * namespace, exit its paused step instance, and mark it ACTIVE. The caller then\n * re-enters `executeWorkflow` (FORKED mode) so the interleaved loop advances the\n * branch and, if it is the last to arrive, fires the JOIN.\n *\n * Idempotent: a re-delivered signal/timer/task on an already-ACTIVE or terminal\n * branch is a no-op. Returns false when there is nothing to resume.\n */\nexport async function resumeBranch(\n em: EntityManager,\n options: ResumeBranchOptions,\n): Promise<boolean> {\n const branch = await em.findOne(WorkflowBranchInstance, {\n id: options.branchInstanceId,\n workflowInstanceId: options.instanceId,\n tenantId: options.tenantId,\n organizationId: options.organizationId,\n })\n\n if (!branch) return false\n if (branch.status !== 'PAUSED' && branch.status !== 'WAITING_FOR_ACTIVITIES') {\n // Already active or terminal \u2014 re-delivery; nothing to do.\n return false\n }\n\n const now = new Date()\n if (options.contextMerge) {\n branch.contextNamespace = { ...(branch.contextNamespace || {}), ...options.contextMerge }\n }\n\n if (options.exitStepInstanceId) {\n const stepInstance = await em.findOne(StepInstance, {\n id: options.exitStepInstanceId,\n workflowInstanceId: options.instanceId,\n status: 'ACTIVE',\n })\n if (stepInstance) {\n await stepHandler.exitStep(em, stepInstance, options.exitOutput)\n }\n }\n\n branch.status = 'ACTIVE'\n branch.pendingTransition = null\n branch.updatedAt = now\n await em.flush()\n\n return true\n}\n\ninterface ParallelContext {\n userId?: string\n}\n\nfunction normalizeWorkflowUserId(value: unknown): string | null {\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n if (!trimmed || trimmed.startsWith('trigger:')) return null\n return trimmed\n}\n\nfunction getNestedValue(obj: any, path: string): any {\n return path.split('.').reduce((current, key) => (current == null ? undefined : current[key]), obj)\n}\n\n/**\n * Open a PARALLEL_FORK: create one ACTIVE branch per outgoing `auto` transition\n * and mark the instance FORKED. Branch tokens start positioned ON the fork step\n * so the interleaved loop runs each branch's fork transition (and its\n * activities) in that branch's own context.\n */\nexport async function openFork(\n em: EntityManager,\n instance: WorkflowInstance,\n definition: WorkflowDefinition,\n forkStepDef: any,\n): Promise<void> {\n const forkStepId: string = forkStepDef.stepId\n const joinStepId: string | undefined = forkStepDef.config?.joinStepId\n if (!joinStepId) {\n throw new Error(`[internal] PARALLEL_FORK \"${forkStepId}\" missing config.joinStepId`)\n }\n\n const outgoing = (definition.definition.transitions || []).filter(\n (transition: any) => transition.fromStepId === forkStepId && transition.trigger === 'auto',\n )\n\n const now = new Date()\n const branchKeys: string[] = []\n for (const transition of outgoing) {\n branchKeys.push(transition.transitionId)\n const branch = em.create(WorkflowBranchInstance, {\n workflowInstanceId: instance.id,\n forkStepId,\n joinStepId,\n branchKey: transition.transitionId,\n parentBranchId: null,\n // Start ON the fork; advanceBranches runs the branch_key transition first.\n currentStepId: forkStepId,\n status: 'ACTIVE',\n contextNamespace: {},\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n startedAt: now,\n createdAt: now,\n updatedAt: now,\n })\n em.persist(branch)\n }\n\n instance.status = 'FORKED'\n instance.activeForkStepId = forkStepId\n instance.updatedAt = now\n await em.flush()\n\n await logWorkflowEvent(em, {\n workflowInstanceId: instance.id,\n eventType: 'PARALLEL_FORK_OPENED',\n eventData: { forkStepId, joinStepId, branchKeys },\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n })\n}\n\n/**\n * Resume a branch that was WAITING_FOR_ACTIVITIES after its async activities\n * finished. Mirrors the instance-level resume but scoped to the branch:\n * merges completed-activity outputs into the branch namespace, advances the\n * branch's pending transition target, and marks it ACTIVE (or FAILED if any\n * async activity failed). The caller re-enters the interleaved loop.\n */\nexport async function resumeBranchAfterActivities(\n em: EntityManager,\n container: AwilixContainer,\n instanceId: string,\n branchInstanceId: string,\n): Promise<{ continueExecution: boolean }> {\n const branch = await em.findOne(\n WorkflowBranchInstance,\n { id: branchInstanceId, workflowInstanceId: instanceId },\n { lockMode: LockMode.PESSIMISTIC_WRITE },\n )\n if (!branch) {\n throw new Error('[internal] Branch not found during async resume')\n }\n if (branch.status !== 'WAITING_FOR_ACTIVITIES') {\n // Re-delivery or already advanced \u2014 nothing to do.\n return { continueExecution: false }\n }\n\n const namespace = branch.contextNamespace || {}\n const pendingJobIds = readPendingAsyncJobIds(namespace._pendingAsyncActivities)\n\n const outcomeEvents = pendingJobIds.length === 0\n ? []\n : await em.find(\n WorkflowEvent,\n {\n workflowInstanceId: instanceId,\n branchInstanceId,\n eventType: { $in: [...ASYNC_ACTIVITY_OUTCOME_EVENT_TYPES] },\n eventData: { async: true, jobId: { $in: pendingJobIds } },\n },\n { orderBy: { occurredAt: 'asc', id: 'asc' } },\n )\n const wait = resolveAsyncActivityWait(pendingJobIds, outcomeEvents)\n\n if (!wait.settled) {\n // Still waiting on other branch activities.\n return { continueExecution: false }\n }\n\n const completedEvents = wait.completed\n const failedCount = wait.failed.length\n\n const now = new Date()\n if (failedCount > 0) {\n branch.status = 'FAILED'\n branch.errorMessage = `${failedCount} async activities failed in branch \"${branch.branchKey}\"`\n branch.updatedAt = now\n await em.flush()\n // The interleaved loop will observe the FAILED branch and cancel siblings.\n return { continueExecution: true }\n }\n\n const mergedNamespace: Record<string, any> = { ...namespace }\n for (const event of completedEvents) {\n if (event.eventData?.output) {\n mergedNamespace[`${event.eventData.activityId}_result`] = event.eventData.output\n }\n }\n delete mergedNamespace._pendingAsyncActivities\n branch.contextNamespace = mergedNamespace\n\n const pending = branch.pendingTransition\n branch.pendingTransition = null\n branch.status = 'ACTIVE'\n branch.updatedAt = now\n await em.flush()\n\n // Execute the destination step in branch context (mirrors the instance-level\n // resume) so the branch cursor lands on an executed step. The step may pause\n // the branch again (e.g. a following USER_TASK); that is handled by the loop.\n if (pending) {\n const instance = await em.findOne(WorkflowInstance, { id: instanceId })\n if (instance) {\n branch.currentStepId = pending.toStepId\n await em.flush()\n await stepHandler.executeStep(\n em,\n instance,\n pending.toStepId,\n {\n workflowContext: { ...(instance.context || {}), ...(branch.contextNamespace || {}) },\n userId: normalizeWorkflowUserId(instance.metadata?.initiatedBy) ?? undefined,\n },\n container,\n branch,\n )\n }\n }\n\n return { continueExecution: true }\n}\n\n/**\n * Interleaved branch execution loop. Advances ACTIVE branches one step at a\n * time until either all branches reach the JOIN (\u2192 fireJoin \u2192 'joined'), a\n * branch fails (\u2192 cancel siblings + 'failed'), or no branch is ACTIVE because\n * they are all waiting on external resume (\u2192 'waiting').\n */\nexport async function advanceBranches(\n em: EntityManager,\n container: AwilixContainer,\n instance: WorkflowInstance,\n definition: WorkflowDefinition,\n context: ParallelContext,\n): Promise<AdvanceBranchesResult> {\n const forkStepId = instance.activeForkStepId\n if (!forkStepId) {\n throw new Error('[internal] advanceBranches called but instance has no active fork')\n }\n\n // Branch-aware iteration budget: each branch gets the same per-token budget as\n // the single-token loop, so fan-out alone never trips the guard.\n const allBranchesForBudget = await em.find(WorkflowBranchInstance, {\n workflowInstanceId: instance.id,\n forkStepId,\n })\n const maxIterations = Math.max(1, allBranchesForBudget.length) * 100\n let iterations = 0\n\n while (iterations < maxIterations) {\n iterations++\n\n const branches = await em.find(WorkflowBranchInstance, {\n workflowInstanceId: instance.id,\n forkStepId,\n })\n\n const active = branches.filter((b) => b.status === 'ACTIVE')\n\n if (active.length === 0) {\n const failed = branches.find((b) => b.status === 'FAILED')\n if (failed) {\n // A branch failed out-of-band (e.g. async-activity resume marked it\n // FAILED); cancel any non-terminal siblings before failing the instance.\n await cancelSiblings(em, instance, failed)\n return { outcome: 'failed', error: failed.errorMessage || 'Branch failed' }\n }\n if (branches.every((b) => b.status === 'COMPLETED')) {\n await fireJoin(em, instance, definition, branches, forkStepId)\n return { outcome: 'joined' }\n }\n // Some branches paused/waiting for external resume \u2014 instance idles.\n return { outcome: 'waiting' }\n }\n\n for (const branch of active) {\n const result = await advanceOneBranch(em, container, instance, definition, branch, context)\n if (result === 'failed') {\n await cancelSiblings(em, instance, branch)\n return { outcome: 'failed', error: branch.errorMessage || 'Branch failed' }\n }\n }\n }\n\n throw new Error('[internal] Maximum branch execution iterations reached - possible infinite loop')\n}\n\n/**\n * Advance a single ACTIVE branch by one step. Returns 'failed' if the branch\n * failed; otherwise mutates branch state (advanced / completed@join / paused).\n */\nasync function advanceOneBranch(\n em: EntityManager,\n container: AwilixContainer,\n instance: WorkflowInstance,\n definition: WorkflowDefinition,\n branch: WorkflowBranchInstance,\n context: ParallelContext,\n): Promise<'advanced' | 'failed'> {\n // Already at the join \u2192 synchronize (mark completed, do not execute the join).\n if (branch.currentStepId === branch.joinStepId) {\n await completeBranchAtJoin(em, instance, branch)\n return 'advanced'\n }\n\n const transitionHandler = await import('./transition-handler')\n\n const token = branchToken(instance, branch)\n // Token read context = instance snapshot overlaid with branch namespace.\n const readContext = { ...(instance.context || {}), ...(branch.contextNamespace || {}) }\n const evalContext = { workflowContext: readContext, userId: context.userId }\n\n // Select the transition to take. Branches positioned on the fork follow their\n // own branch_key transition; otherwise pick the highest-priority valid auto one.\n let selected: any | null = null\n if (branch.currentStepId === branch.forkStepId) {\n selected = (definition.definition.transitions || []).find(\n (transition: any) => transition.transitionId === branch.branchKey,\n ) || null\n } else {\n const valid = await transitionHandler.findValidTransitions(\n em,\n instance,\n branch.currentStepId,\n evalContext,\n )\n const validAuto = valid.filter((vt) => vt.isValid && vt.transition?.trigger === 'auto')\n selected = validAuto.length > 0 ? validAuto[0].transition : null\n }\n\n if (!selected) {\n // No outgoing transition and not at the join \u2192 the branch is stuck.\n await failBranch(em, instance, branch, `Branch \"${branch.branchKey}\" has no valid transition from \"${branch.currentStepId}\"`)\n return 'failed'\n }\n\n try {\n const transitionResult = await transitionHandler.executeTransitionForToken(\n em,\n container,\n token,\n selected.fromStepId,\n selected.toStepId,\n { ...evalContext, transitionId: selected.transitionId },\n )\n\n if (!transitionResult.success) {\n // Error routing (spec 5.9) inside a branch: a wired error route is taken\n // in the branch itself, so only unhandled failures propagate to the\n // instance. The failure-queue directive is deliberately instance-level and\n // is not honored per branch \u2014 such a branch keeps failing as it does today.\n const routed = await followBranchErrorRoute(\n em,\n container,\n instance,\n branch,\n token,\n transitionResult,\n evalContext,\n )\n if (routed) {\n if (branch.currentStepId === branch.joinStepId) {\n await completeBranchAtJoin(em, instance, branch)\n }\n return 'advanced'\n }\n await failBranch(em, instance, branch, transitionResult.error || 'Branch transition failed')\n return 'failed'\n }\n\n // executeTransitionForToken set branch.currentStepId = toStepId. If that is\n // the join, synchronize now (the join step itself is a no-op).\n if (branch.currentStepId === branch.joinStepId) {\n await completeBranchAtJoin(em, instance, branch)\n }\n return 'advanced'\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n await failBranch(em, instance, branch, message)\n return 'failed'\n }\n}\n\n/**\n * Follow a branch step's wired error route with the branch token, so the branch\n * keeps its own cursor and context namespace. Returns false when the transition\n * result carries no route or the route could not be taken.\n */\nasync function followBranchErrorRoute(\n em: EntityManager,\n container: AwilixContainer,\n instance: WorkflowInstance,\n branch: WorkflowBranchInstance,\n token: ExecutionToken,\n transitionResult: { failedStepId?: string; errorRoute?: { transitionId?: string; toStepId: string }; error?: string },\n evalContext: { workflowContext: Record<string, any>; userId?: string },\n): Promise<boolean> {\n const { failedStepId, errorRoute } = transitionResult\n if (!failedStepId || !errorRoute) return false\n\n const error = transitionResult.error || 'Branch transition failed'\n await logWorkflowEvent(em, {\n workflowInstanceId: instance.id,\n branchInstanceId: branch.id,\n eventType: 'ERROR_ROUTED',\n eventData: {\n failedStepId,\n toStepId: errorRoute.toStepId,\n transitionId: errorRoute.transitionId,\n branchKey: branch.branchKey,\n error,\n },\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n })\n\n const errorEntry = buildErrorContextEntry(failedStepId, error)\n mergeTokenContext(token, { [WORKFLOW_ERROR_CONTEXT_KEY]: errorEntry })\n await em.flush()\n\n const transitionHandler = await import('./transition-handler')\n const routedResult = await transitionHandler.executeTransitionForToken(\n em,\n container,\n token,\n failedStepId,\n errorRoute.toStepId,\n {\n ...evalContext,\n workflowContext: {\n ...evalContext.workflowContext,\n [WORKFLOW_ERROR_CONTEXT_KEY]: errorEntry,\n },\n transitionId: errorRoute.transitionId,\n },\n )\n\n return routedResult.success === true\n}\n\nasync function completeBranchAtJoin(\n em: EntityManager,\n instance: WorkflowInstance,\n branch: WorkflowBranchInstance,\n): Promise<void> {\n const now = new Date()\n branch.status = 'COMPLETED'\n branch.completedAt = now\n branch.updatedAt = now\n await em.flush()\n\n await logWorkflowEvent(em, {\n workflowInstanceId: instance.id,\n branchInstanceId: branch.id,\n eventType: 'PARALLEL_BRANCH_COMPLETED',\n eventData: { branchKey: branch.branchKey, joinStepId: branch.joinStepId },\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n })\n}\n\nasync function failBranch(\n em: EntityManager,\n instance: WorkflowInstance,\n branch: WorkflowBranchInstance,\n message: string,\n): Promise<void> {\n const now = new Date()\n branch.status = 'FAILED'\n branch.errorMessage = message\n branch.updatedAt = now\n await em.flush()\n\n await logWorkflowEvent(em, {\n workflowInstanceId: instance.id,\n branchInstanceId: branch.id,\n eventType: 'PARALLEL_BRANCH_FAILED',\n eventData: { branchKey: branch.branchKey, error: message },\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n })\n}\n\n/**\n * Cancel sibling branches of a failed branch (best-effort), cancel their open\n * user tasks, and log a cancellation event per branch.\n */\nasync function cancelSiblings(\n em: EntityManager,\n instance: WorkflowInstance,\n failedBranch: WorkflowBranchInstance,\n): Promise<void> {\n const siblings = await em.find(WorkflowBranchInstance, {\n workflowInstanceId: instance.id,\n forkStepId: failedBranch.forkStepId,\n })\n\n const now = new Date()\n for (const sibling of siblings) {\n if (sibling.id === failedBranch.id) continue\n if (sibling.status === 'ACTIVE' || sibling.status === 'PAUSED' || sibling.status === 'WAITING_FOR_ACTIVITIES') {\n sibling.status = 'CANCELLED'\n sibling.updatedAt = now\n\n // Best-effort: cancel the branch's open user tasks.\n const openTasks = await em.find(UserTask, {\n workflowInstanceId: instance.id,\n branchInstanceId: sibling.id,\n status: 'PENDING',\n })\n for (const task of openTasks) {\n task.status = 'CANCELLED'\n task.updatedAt = now\n }\n\n await em.flush()\n\n await logWorkflowEvent(em, {\n workflowInstanceId: instance.id,\n branchInstanceId: sibling.id,\n eventType: 'PARALLEL_BRANCH_CANCELLED',\n eventData: { branchKey: sibling.branchKey, reason: 'sibling-failed' },\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n })\n }\n }\n\n await logWorkflowEvent(em, {\n workflowInstanceId: instance.id,\n eventType: 'PARALLEL_FORK_FAILED',\n eventData: { forkStepId: failedBranch.forkStepId, failedBranchKey: failedBranch.branchKey },\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n })\n}\n\n/**\n * Fire the JOIN once all branches are COMPLETED: merge each branch namespace\n * under `instance.context.branches[branchKey]`, apply optional outputMapping to\n * lift selected values to the top level, then resume the root token at the step\n * after the JOIN.\n */\nasync function fireJoin(\n em: EntityManager,\n instance: WorkflowInstance,\n definition: WorkflowDefinition,\n branches: WorkflowBranchInstance[],\n forkStepId: string,\n): Promise<void> {\n const joinStepId = branches[0]?.joinStepId\n if (!joinStepId) {\n throw new Error('[internal] fireJoin called without a join step id')\n }\n\n const joinStep = definition.definition.steps.find((s: any) => s.stepId === joinStepId)\n\n // Deterministic merge \u2014 no silent collisions: each branch keeps its own slot.\n const branchesContext: Record<string, any> = { ...(instance.context?.branches || {}) }\n const mergedBranchKeys: string[] = []\n for (const branch of branches) {\n branchesContext[branch.branchKey] = branch.contextNamespace || {}\n mergedBranchKeys.push(branch.branchKey)\n }\n\n let nextContext: Record<string, any> = {\n ...(instance.context || {}),\n branches: branchesContext,\n }\n\n // Optional outputMapping: topLevelKey -> 'branches.<branchKey>.<path>'. The\n // reserved `branches` slot map is never overwritten \u2014 guard it explicitly so a\n // mapping cannot clobber the per-branch namespaces.\n const outputMapping: Record<string, string> | undefined = joinStep?.config?.outputMapping\n if (outputMapping) {\n for (const [topKey, sourcePath] of Object.entries(outputMapping)) {\n if (topKey === 'branches') continue\n const value = getNestedValue(nextContext, sourcePath)\n if (value !== undefined) nextContext[topKey] = value\n }\n }\n\n // Park the root token ON the JOIN step (not its successor) and let the\n // single-token executor loop run the JOIN's outgoing transition. Jumping\n // straight to the post-join step would bypass stepHandler.executeStep() for\n // that step \u2014 so a following USER_TASK / WAIT_FOR_TIMER / WAIT_FOR_SIGNAL would\n // never have its task created / timer enqueued / signal wait registered (the\n // instance would hang), and any activities on the JOIN's outgoing transition\n // would be skipped. The JOIN step itself is a no-op, so the loop simply runs\n // its outgoing transition through the normal step-entry path.\n const afterJoinStepId = (definition.definition.transitions || []).find(\n (transition: any) => transition.fromStepId === joinStepId,\n )?.toStepId ?? null\n\n const now = new Date()\n instance.context = nextContext\n instance.status = 'RUNNING'\n instance.activeForkStepId = null\n instance.currentStepId = joinStepId\n instance.updatedAt = now\n await em.flush()\n\n await logWorkflowEvent(em, {\n workflowInstanceId: instance.id,\n eventType: 'PARALLEL_JOIN_COMPLETED',\n eventData: { forkStepId, joinStepId, mergedBranchKeys, afterJoinStepId },\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n })\n}\n"],
|
|
5
|
-
"mappings": "AAqBA,SAAwB,gBAAgB;AAExC;AAAA,EACE;AAAA,EACA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,wBAAwB;AACjC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,YAAY,iBAAiB;AAC7B,SAAS,aAAa,yBAA8C;AACpE,SAAS,4BAA4B,8BAA8B;
|
|
4
|
+
"sourcesContent": ["/**\n * Workflows Module - Parallel Fork / Join Handler\n *\n * Implements PARALLEL_FORK / PARALLEL_JOIN execution with a multi-token model:\n *\n * - `openFork` turns a PARALLEL_FORK step into N persistent branch tokens\n * (`WorkflowBranchInstance`), one per outgoing `auto` transition, and puts the\n * instance into the dormant `FORKED` state.\n * - `advanceBranches` runs the interleaved loop: while the instance is FORKED it\n * advances each ACTIVE branch one step at a time (BPMN semantics \u2014 single lock,\n * no thread-level concurrency). A branch pauses independently (USER_TASK /\n * signal / timer / async activity) without blocking siblings.\n * - When every branch has reached its JOIN (wait-all), `fireJoin` merges the\n * branch namespaces back into `instance.context`, applies optional\n * `outputMapping`, and resumes the root token at the step after the JOIN.\n * - A failed branch cancels its siblings and fails the whole instance.\n *\n * All work happens under the executor's pessimistic instance lock + transaction,\n * so wait-all counting and JOIN firing are race-free.\n */\n\nimport { EntityManager, LockMode } from '@mikro-orm/core'\nimport type { AwilixContainer } from 'awilix'\nimport {\n WorkflowInstance,\n WorkflowBranchInstance,\n WorkflowDefinition,\n StepInstance,\n UserTask,\n WorkflowEvent,\n} from '../data/entities'\nimport { logWorkflowEvent } from './event-logger'\nimport {\n ASYNC_ACTIVITY_OUTCOME_EVENT_TYPES,\n readPendingAsyncJobIds,\n resolveAsyncActivityWait,\n} from './async-activity-wait'\nimport * as stepHandler from './step-handler'\nimport { branchToken, mergeTokenContext, type ExecutionToken } from './execution-token'\nimport { WORKFLOW_ERROR_CONTEXT_KEY, buildErrorContextEntry } from './error-routing'\nimport { isSafeMappingTargetPath, safeGetNestedValue } from './safe-mapping-path'\n\nexport interface AdvanceBranchesResult {\n outcome: 'joined' | 'waiting' | 'failed'\n error?: string\n}\n\nexport interface ResumeBranchOptions {\n instanceId: string\n branchInstanceId: string\n tenantId: string\n organizationId: string\n /** Values merged into the branch's private namespace before resuming (task form data, signal payload, \u2026). */\n contextMerge?: Record<string, any>\n /** Step instance to exit (the paused step's instance) before the branch advances past it. */\n exitStepInstanceId?: string | null\n exitOutput?: any\n}\n\n/**\n * Resume a single paused/waiting branch: optionally merge data into its\n * namespace, exit its paused step instance, and mark it ACTIVE. The caller then\n * re-enters `executeWorkflow` (FORKED mode) so the interleaved loop advances the\n * branch and, if it is the last to arrive, fires the JOIN.\n *\n * Idempotent: a re-delivered signal/timer/task on an already-ACTIVE or terminal\n * branch is a no-op. Returns false when there is nothing to resume.\n */\nexport async function resumeBranch(\n em: EntityManager,\n options: ResumeBranchOptions,\n): Promise<boolean> {\n const branch = await em.findOne(WorkflowBranchInstance, {\n id: options.branchInstanceId,\n workflowInstanceId: options.instanceId,\n tenantId: options.tenantId,\n organizationId: options.organizationId,\n })\n\n if (!branch) return false\n if (branch.status !== 'PAUSED' && branch.status !== 'WAITING_FOR_ACTIVITIES') {\n // Already active or terminal \u2014 re-delivery; nothing to do.\n return false\n }\n\n const now = new Date()\n if (options.contextMerge) {\n branch.contextNamespace = { ...(branch.contextNamespace || {}), ...options.contextMerge }\n }\n\n if (options.exitStepInstanceId) {\n const stepInstance = await em.findOne(StepInstance, {\n id: options.exitStepInstanceId,\n workflowInstanceId: options.instanceId,\n status: 'ACTIVE',\n })\n if (stepInstance) {\n await stepHandler.exitStep(em, stepInstance, options.exitOutput)\n }\n }\n\n branch.status = 'ACTIVE'\n branch.pendingTransition = null\n branch.updatedAt = now\n await em.flush()\n\n return true\n}\n\ninterface ParallelContext {\n userId?: string\n}\n\nexport function applyParallelJoinOutputMapping(\n context: Record<string, unknown>,\n outputMapping: Record<string, string>,\n): void {\n for (const [topKey, sourcePath] of Object.entries(outputMapping)) {\n if (topKey === 'branches' || !isSafeMappingTargetPath(topKey)) continue\n const value = safeGetNestedValue(context, sourcePath)\n if (value !== undefined) context[topKey] = value\n }\n}\n\nfunction normalizeWorkflowUserId(value: unknown): string | null {\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n if (!trimmed || trimmed.startsWith('trigger:')) return null\n return trimmed\n}\n\n/**\n * Open a PARALLEL_FORK: create one ACTIVE branch per outgoing `auto` transition\n * and mark the instance FORKED. Branch tokens start positioned ON the fork step\n * so the interleaved loop runs each branch's fork transition (and its\n * activities) in that branch's own context.\n */\nexport async function openFork(\n em: EntityManager,\n instance: WorkflowInstance,\n definition: WorkflowDefinition,\n forkStepDef: any,\n): Promise<void> {\n const forkStepId: string = forkStepDef.stepId\n const joinStepId: string | undefined = forkStepDef.config?.joinStepId\n if (!joinStepId) {\n throw new Error(`[internal] PARALLEL_FORK \"${forkStepId}\" missing config.joinStepId`)\n }\n\n const outgoing = (definition.definition.transitions || []).filter(\n (transition: any) => transition.fromStepId === forkStepId && transition.trigger === 'auto',\n )\n\n const now = new Date()\n const branchKeys: string[] = []\n for (const transition of outgoing) {\n branchKeys.push(transition.transitionId)\n const branch = em.create(WorkflowBranchInstance, {\n workflowInstanceId: instance.id,\n forkStepId,\n joinStepId,\n branchKey: transition.transitionId,\n parentBranchId: null,\n // Start ON the fork; advanceBranches runs the branch_key transition first.\n currentStepId: forkStepId,\n status: 'ACTIVE',\n contextNamespace: {},\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n startedAt: now,\n createdAt: now,\n updatedAt: now,\n })\n em.persist(branch)\n }\n\n instance.status = 'FORKED'\n instance.activeForkStepId = forkStepId\n instance.updatedAt = now\n await em.flush()\n\n await logWorkflowEvent(em, {\n workflowInstanceId: instance.id,\n eventType: 'PARALLEL_FORK_OPENED',\n eventData: { forkStepId, joinStepId, branchKeys },\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n })\n}\n\n/**\n * Resume a branch that was WAITING_FOR_ACTIVITIES after its async activities\n * finished. Mirrors the instance-level resume but scoped to the branch:\n * merges completed-activity outputs into the branch namespace, advances the\n * branch's pending transition target, and marks it ACTIVE (or FAILED if any\n * async activity failed). The caller re-enters the interleaved loop.\n */\nexport async function resumeBranchAfterActivities(\n em: EntityManager,\n container: AwilixContainer,\n instanceId: string,\n branchInstanceId: string,\n): Promise<{ continueExecution: boolean }> {\n const branch = await em.findOne(\n WorkflowBranchInstance,\n { id: branchInstanceId, workflowInstanceId: instanceId },\n { lockMode: LockMode.PESSIMISTIC_WRITE },\n )\n if (!branch) {\n throw new Error('[internal] Branch not found during async resume')\n }\n if (branch.status !== 'WAITING_FOR_ACTIVITIES') {\n // Re-delivery or already advanced \u2014 nothing to do.\n return { continueExecution: false }\n }\n\n const namespace = branch.contextNamespace || {}\n const pendingJobIds = readPendingAsyncJobIds(namespace._pendingAsyncActivities)\n\n const outcomeEvents = pendingJobIds.length === 0\n ? []\n : await em.find(\n WorkflowEvent,\n {\n workflowInstanceId: instanceId,\n branchInstanceId,\n eventType: { $in: [...ASYNC_ACTIVITY_OUTCOME_EVENT_TYPES] },\n eventData: { async: true, jobId: { $in: pendingJobIds } },\n },\n { orderBy: { occurredAt: 'asc', id: 'asc' } },\n )\n const wait = resolveAsyncActivityWait(pendingJobIds, outcomeEvents)\n\n if (!wait.settled) {\n // Still waiting on other branch activities.\n return { continueExecution: false }\n }\n\n const completedEvents = wait.completed\n const failedCount = wait.failed.length\n\n const now = new Date()\n if (failedCount > 0) {\n branch.status = 'FAILED'\n branch.errorMessage = `${failedCount} async activities failed in branch \"${branch.branchKey}\"`\n branch.updatedAt = now\n await em.flush()\n // The interleaved loop will observe the FAILED branch and cancel siblings.\n return { continueExecution: true }\n }\n\n const mergedNamespace: Record<string, any> = { ...namespace }\n for (const event of completedEvents) {\n if (event.eventData?.output) {\n mergedNamespace[`${event.eventData.activityId}_result`] = event.eventData.output\n }\n }\n delete mergedNamespace._pendingAsyncActivities\n branch.contextNamespace = mergedNamespace\n\n const pending = branch.pendingTransition\n branch.pendingTransition = null\n branch.status = 'ACTIVE'\n branch.updatedAt = now\n await em.flush()\n\n // Execute the destination step in branch context (mirrors the instance-level\n // resume) so the branch cursor lands on an executed step. The step may pause\n // the branch again (e.g. a following USER_TASK); that is handled by the loop.\n if (pending) {\n const instance = await em.findOne(WorkflowInstance, { id: instanceId })\n if (instance) {\n branch.currentStepId = pending.toStepId\n await em.flush()\n await stepHandler.executeStep(\n em,\n instance,\n pending.toStepId,\n {\n workflowContext: { ...(instance.context || {}), ...(branch.contextNamespace || {}) },\n userId: normalizeWorkflowUserId(instance.metadata?.initiatedBy) ?? undefined,\n },\n container,\n branch,\n )\n }\n }\n\n return { continueExecution: true }\n}\n\n/**\n * Interleaved branch execution loop. Advances ACTIVE branches one step at a\n * time until either all branches reach the JOIN (\u2192 fireJoin \u2192 'joined'), a\n * branch fails (\u2192 cancel siblings + 'failed'), or no branch is ACTIVE because\n * they are all waiting on external resume (\u2192 'waiting').\n */\nexport async function advanceBranches(\n em: EntityManager,\n container: AwilixContainer,\n instance: WorkflowInstance,\n definition: WorkflowDefinition,\n context: ParallelContext,\n): Promise<AdvanceBranchesResult> {\n const forkStepId = instance.activeForkStepId\n if (!forkStepId) {\n throw new Error('[internal] advanceBranches called but instance has no active fork')\n }\n\n // Branch-aware iteration budget: each branch gets the same per-token budget as\n // the single-token loop, so fan-out alone never trips the guard.\n const allBranchesForBudget = await em.find(WorkflowBranchInstance, {\n workflowInstanceId: instance.id,\n forkStepId,\n })\n const maxIterations = Math.max(1, allBranchesForBudget.length) * 100\n let iterations = 0\n\n while (iterations < maxIterations) {\n iterations++\n\n const branches = await em.find(WorkflowBranchInstance, {\n workflowInstanceId: instance.id,\n forkStepId,\n })\n\n const active = branches.filter((b) => b.status === 'ACTIVE')\n\n if (active.length === 0) {\n const failed = branches.find((b) => b.status === 'FAILED')\n if (failed) {\n // A branch failed out-of-band (e.g. async-activity resume marked it\n // FAILED); cancel any non-terminal siblings before failing the instance.\n await cancelSiblings(em, instance, failed)\n return { outcome: 'failed', error: failed.errorMessage || 'Branch failed' }\n }\n if (branches.every((b) => b.status === 'COMPLETED')) {\n await fireJoin(em, instance, definition, branches, forkStepId)\n return { outcome: 'joined' }\n }\n // Some branches paused/waiting for external resume \u2014 instance idles.\n return { outcome: 'waiting' }\n }\n\n for (const branch of active) {\n const result = await advanceOneBranch(em, container, instance, definition, branch, context)\n if (result === 'failed') {\n await cancelSiblings(em, instance, branch)\n return { outcome: 'failed', error: branch.errorMessage || 'Branch failed' }\n }\n }\n }\n\n throw new Error('[internal] Maximum branch execution iterations reached - possible infinite loop')\n}\n\n/**\n * Advance a single ACTIVE branch by one step. Returns 'failed' if the branch\n * failed; otherwise mutates branch state (advanced / completed@join / paused).\n */\nasync function advanceOneBranch(\n em: EntityManager,\n container: AwilixContainer,\n instance: WorkflowInstance,\n definition: WorkflowDefinition,\n branch: WorkflowBranchInstance,\n context: ParallelContext,\n): Promise<'advanced' | 'failed'> {\n // Already at the join \u2192 synchronize (mark completed, do not execute the join).\n if (branch.currentStepId === branch.joinStepId) {\n await completeBranchAtJoin(em, instance, branch)\n return 'advanced'\n }\n\n const transitionHandler = await import('./transition-handler')\n\n const token = branchToken(instance, branch)\n // Token read context = instance snapshot overlaid with branch namespace.\n const readContext = { ...(instance.context || {}), ...(branch.contextNamespace || {}) }\n const evalContext = { workflowContext: readContext, userId: context.userId }\n\n // Select the transition to take. Branches positioned on the fork follow their\n // own branch_key transition; otherwise pick the highest-priority valid auto one.\n let selected: any | null = null\n if (branch.currentStepId === branch.forkStepId) {\n selected = (definition.definition.transitions || []).find(\n (transition: any) => transition.transitionId === branch.branchKey,\n ) || null\n } else {\n const valid = await transitionHandler.findValidTransitions(\n em,\n instance,\n branch.currentStepId,\n evalContext,\n )\n const validAuto = valid.filter((vt) => vt.isValid && vt.transition?.trigger === 'auto')\n selected = validAuto.length > 0 ? validAuto[0].transition : null\n }\n\n if (!selected) {\n // No outgoing transition and not at the join \u2192 the branch is stuck.\n await failBranch(em, instance, branch, `Branch \"${branch.branchKey}\" has no valid transition from \"${branch.currentStepId}\"`)\n return 'failed'\n }\n\n try {\n const transitionResult = await transitionHandler.executeTransitionForToken(\n em,\n container,\n token,\n selected.fromStepId,\n selected.toStepId,\n { ...evalContext, transitionId: selected.transitionId },\n )\n\n if (!transitionResult.success) {\n // Error routing (spec 5.9) inside a branch: a wired error route is taken\n // in the branch itself, so only unhandled failures propagate to the\n // instance. The failure-queue directive is deliberately instance-level and\n // is not honored per branch \u2014 such a branch keeps failing as it does today.\n const routed = await followBranchErrorRoute(\n em,\n container,\n instance,\n branch,\n token,\n transitionResult,\n evalContext,\n )\n if (routed) {\n if (branch.currentStepId === branch.joinStepId) {\n await completeBranchAtJoin(em, instance, branch)\n }\n return 'advanced'\n }\n await failBranch(em, instance, branch, transitionResult.error || 'Branch transition failed')\n return 'failed'\n }\n\n // executeTransitionForToken set branch.currentStepId = toStepId. If that is\n // the join, synchronize now (the join step itself is a no-op).\n if (branch.currentStepId === branch.joinStepId) {\n await completeBranchAtJoin(em, instance, branch)\n }\n return 'advanced'\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n await failBranch(em, instance, branch, message)\n return 'failed'\n }\n}\n\n/**\n * Follow a branch step's wired error route with the branch token, so the branch\n * keeps its own cursor and context namespace. Returns false when the transition\n * result carries no route or the route could not be taken.\n */\nasync function followBranchErrorRoute(\n em: EntityManager,\n container: AwilixContainer,\n instance: WorkflowInstance,\n branch: WorkflowBranchInstance,\n token: ExecutionToken,\n transitionResult: { failedStepId?: string; errorRoute?: { transitionId?: string; toStepId: string }; error?: string },\n evalContext: { workflowContext: Record<string, any>; userId?: string },\n): Promise<boolean> {\n const { failedStepId, errorRoute } = transitionResult\n if (!failedStepId || !errorRoute) return false\n\n const error = transitionResult.error || 'Branch transition failed'\n await logWorkflowEvent(em, {\n workflowInstanceId: instance.id,\n branchInstanceId: branch.id,\n eventType: 'ERROR_ROUTED',\n eventData: {\n failedStepId,\n toStepId: errorRoute.toStepId,\n transitionId: errorRoute.transitionId,\n branchKey: branch.branchKey,\n error,\n },\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n })\n\n const errorEntry = buildErrorContextEntry(failedStepId, error)\n mergeTokenContext(token, { [WORKFLOW_ERROR_CONTEXT_KEY]: errorEntry })\n await em.flush()\n\n const transitionHandler = await import('./transition-handler')\n const routedResult = await transitionHandler.executeTransitionForToken(\n em,\n container,\n token,\n failedStepId,\n errorRoute.toStepId,\n {\n ...evalContext,\n workflowContext: {\n ...evalContext.workflowContext,\n [WORKFLOW_ERROR_CONTEXT_KEY]: errorEntry,\n },\n transitionId: errorRoute.transitionId,\n },\n )\n\n return routedResult.success === true\n}\n\nasync function completeBranchAtJoin(\n em: EntityManager,\n instance: WorkflowInstance,\n branch: WorkflowBranchInstance,\n): Promise<void> {\n const now = new Date()\n branch.status = 'COMPLETED'\n branch.completedAt = now\n branch.updatedAt = now\n await em.flush()\n\n await logWorkflowEvent(em, {\n workflowInstanceId: instance.id,\n branchInstanceId: branch.id,\n eventType: 'PARALLEL_BRANCH_COMPLETED',\n eventData: { branchKey: branch.branchKey, joinStepId: branch.joinStepId },\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n })\n}\n\nasync function failBranch(\n em: EntityManager,\n instance: WorkflowInstance,\n branch: WorkflowBranchInstance,\n message: string,\n): Promise<void> {\n const now = new Date()\n branch.status = 'FAILED'\n branch.errorMessage = message\n branch.updatedAt = now\n await em.flush()\n\n await logWorkflowEvent(em, {\n workflowInstanceId: instance.id,\n branchInstanceId: branch.id,\n eventType: 'PARALLEL_BRANCH_FAILED',\n eventData: { branchKey: branch.branchKey, error: message },\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n })\n}\n\n/**\n * Cancel sibling branches of a failed branch (best-effort), cancel their open\n * user tasks, and log a cancellation event per branch.\n */\nasync function cancelSiblings(\n em: EntityManager,\n instance: WorkflowInstance,\n failedBranch: WorkflowBranchInstance,\n): Promise<void> {\n const siblings = await em.find(WorkflowBranchInstance, {\n workflowInstanceId: instance.id,\n forkStepId: failedBranch.forkStepId,\n })\n\n const now = new Date()\n for (const sibling of siblings) {\n if (sibling.id === failedBranch.id) continue\n if (sibling.status === 'ACTIVE' || sibling.status === 'PAUSED' || sibling.status === 'WAITING_FOR_ACTIVITIES') {\n sibling.status = 'CANCELLED'\n sibling.updatedAt = now\n\n // Best-effort: cancel the branch's open user tasks.\n const openTasks = await em.find(UserTask, {\n workflowInstanceId: instance.id,\n branchInstanceId: sibling.id,\n status: 'PENDING',\n })\n for (const task of openTasks) {\n task.status = 'CANCELLED'\n task.updatedAt = now\n }\n\n await em.flush()\n\n await logWorkflowEvent(em, {\n workflowInstanceId: instance.id,\n branchInstanceId: sibling.id,\n eventType: 'PARALLEL_BRANCH_CANCELLED',\n eventData: { branchKey: sibling.branchKey, reason: 'sibling-failed' },\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n })\n }\n }\n\n await logWorkflowEvent(em, {\n workflowInstanceId: instance.id,\n eventType: 'PARALLEL_FORK_FAILED',\n eventData: { forkStepId: failedBranch.forkStepId, failedBranchKey: failedBranch.branchKey },\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n })\n}\n\n/**\n * Fire the JOIN once all branches are COMPLETED: merge each branch namespace\n * under `instance.context.branches[branchKey]`, apply optional outputMapping to\n * lift selected values to the top level, then resume the root token at the step\n * after the JOIN.\n */\nasync function fireJoin(\n em: EntityManager,\n instance: WorkflowInstance,\n definition: WorkflowDefinition,\n branches: WorkflowBranchInstance[],\n forkStepId: string,\n): Promise<void> {\n const joinStepId = branches[0]?.joinStepId\n if (!joinStepId) {\n throw new Error('[internal] fireJoin called without a join step id')\n }\n\n const joinStep = definition.definition.steps.find((s: any) => s.stepId === joinStepId)\n\n // Deterministic merge \u2014 no silent collisions: each branch keeps its own slot.\n const branchesContext: Record<string, any> = { ...(instance.context?.branches || {}) }\n const mergedBranchKeys: string[] = []\n for (const branch of branches) {\n branchesContext[branch.branchKey] = branch.contextNamespace || {}\n mergedBranchKeys.push(branch.branchKey)\n }\n\n let nextContext: Record<string, any> = {\n ...(instance.context || {}),\n branches: branchesContext,\n }\n\n // Optional outputMapping: topLevelKey -> 'branches.<branchKey>.<path>'. The\n // reserved `branches` slot map is never overwritten \u2014 guard it explicitly so a\n // mapping cannot clobber the per-branch namespaces.\n const outputMapping: Record<string, string> | undefined = joinStep?.config?.outputMapping\n if (outputMapping) {\n applyParallelJoinOutputMapping(nextContext, outputMapping)\n }\n\n // Park the root token ON the JOIN step (not its successor) and let the\n // single-token executor loop run the JOIN's outgoing transition. Jumping\n // straight to the post-join step would bypass stepHandler.executeStep() for\n // that step \u2014 so a following USER_TASK / WAIT_FOR_TIMER / WAIT_FOR_SIGNAL would\n // never have its task created / timer enqueued / signal wait registered (the\n // instance would hang), and any activities on the JOIN's outgoing transition\n // would be skipped. The JOIN step itself is a no-op, so the loop simply runs\n // its outgoing transition through the normal step-entry path.\n const afterJoinStepId = (definition.definition.transitions || []).find(\n (transition: any) => transition.fromStepId === joinStepId,\n )?.toStepId ?? null\n\n const now = new Date()\n instance.context = nextContext\n instance.status = 'RUNNING'\n instance.activeForkStepId = null\n instance.currentStepId = joinStepId\n instance.updatedAt = now\n await em.flush()\n\n await logWorkflowEvent(em, {\n workflowInstanceId: instance.id,\n eventType: 'PARALLEL_JOIN_COMPLETED',\n eventData: { forkStepId, joinStepId, mergedBranchKeys, afterJoinStepId },\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n })\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const FORBIDDEN_MAPPING_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
2
|
+
function isSafeMappingTargetPath(path) {
|
|
3
|
+
return !path.split(".").some((segment) => FORBIDDEN_MAPPING_PATH_SEGMENTS.has(segment));
|
|
4
|
+
}
|
|
5
|
+
function isObjectRecord(value) {
|
|
6
|
+
return typeof value === "object" && value !== null;
|
|
7
|
+
}
|
|
8
|
+
function safeGetNestedValue(source, path) {
|
|
9
|
+
return path.split(".").reduce((current, segment) => {
|
|
10
|
+
if (!isObjectRecord(current) || !Object.hasOwn(current, segment)) return void 0;
|
|
11
|
+
return current[segment];
|
|
12
|
+
}, source);
|
|
13
|
+
}
|
|
14
|
+
function cloneMappingContainer(value) {
|
|
15
|
+
if (Array.isArray(value)) return [...value];
|
|
16
|
+
if (isObjectRecord(value)) return { ...value };
|
|
17
|
+
return {};
|
|
18
|
+
}
|
|
19
|
+
function safeSetNestedValue(target, path, value) {
|
|
20
|
+
const segments = path.split(".");
|
|
21
|
+
if (!isSafeMappingTargetPath(path)) return false;
|
|
22
|
+
const lastSegment = segments.pop();
|
|
23
|
+
if (lastSegment === void 0) return false;
|
|
24
|
+
let current = target;
|
|
25
|
+
for (const segment of segments) {
|
|
26
|
+
const existing = Object.hasOwn(current, segment) ? current[segment] : void 0;
|
|
27
|
+
current[segment] = cloneMappingContainer(existing);
|
|
28
|
+
current = current[segment];
|
|
29
|
+
}
|
|
30
|
+
current[lastSegment] = value;
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
export {
|
|
34
|
+
isSafeMappingTargetPath,
|
|
35
|
+
safeGetNestedValue,
|
|
36
|
+
safeSetNestedValue
|
|
37
|
+
};
|
|
38
|
+
//# sourceMappingURL=safe-mapping-path.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/workflows/lib/safe-mapping-path.ts"],
|
|
4
|
+
"sourcesContent": ["const FORBIDDEN_MAPPING_PATH_SEGMENTS = new Set(['__proto__', 'constructor', 'prototype'])\n\nexport function isSafeMappingTargetPath(path: string): boolean {\n return !path.split('.').some((segment) => FORBIDDEN_MAPPING_PATH_SEGMENTS.has(segment))\n}\n\nfunction isObjectRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null\n}\n\nexport function safeGetNestedValue(source: unknown, path: string): unknown {\n return path.split('.').reduce<unknown>((current, segment) => {\n if (!isObjectRecord(current) || !Object.hasOwn(current, segment)) return undefined\n return current[segment]\n }, source)\n}\n\nfunction cloneMappingContainer(value: unknown): Record<string, unknown> {\n if (Array.isArray(value)) return [...value] as unknown as Record<string, unknown>\n if (isObjectRecord(value)) return { ...value }\n return {}\n}\n\nexport function safeSetNestedValue(\n target: Record<string, unknown>,\n path: string,\n value: unknown,\n): boolean {\n const segments = path.split('.')\n if (!isSafeMappingTargetPath(path)) return false\n\n const lastSegment = segments.pop()\n if (lastSegment === undefined) return false\n\n let current = target\n for (const segment of segments) {\n const existing = Object.hasOwn(current, segment) ? current[segment] : undefined\n current[segment] = cloneMappingContainer(existing)\n current = current[segment] as Record<string, unknown>\n }\n\n current[lastSegment] = value\n return true\n}\n"],
|
|
5
|
+
"mappings": "AAAA,MAAM,kCAAkC,oBAAI,IAAI,CAAC,aAAa,eAAe,WAAW,CAAC;AAElF,SAAS,wBAAwB,MAAuB;AAC7D,SAAO,CAAC,KAAK,MAAM,GAAG,EAAE,KAAK,CAAC,YAAY,gCAAgC,IAAI,OAAO,CAAC;AACxF;AAEA,SAAS,eAAe,OAAkD;AACxE,SAAO,OAAO,UAAU,YAAY,UAAU;AAChD;AAEO,SAAS,mBAAmB,QAAiB,MAAuB;AACzE,SAAO,KAAK,MAAM,GAAG,EAAE,OAAgB,CAAC,SAAS,YAAY;AAC3D,QAAI,CAAC,eAAe,OAAO,KAAK,CAAC,OAAO,OAAO,SAAS,OAAO,EAAG,QAAO;AACzE,WAAO,QAAQ,OAAO;AAAA,EACxB,GAAG,MAAM;AACX;AAEA,SAAS,sBAAsB,OAAyC;AACtE,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,CAAC,GAAG,KAAK;AAC1C,MAAI,eAAe,KAAK,EAAG,QAAO,EAAE,GAAG,MAAM;AAC7C,SAAO,CAAC;AACV;AAEO,SAAS,mBACd,QACA,MACA,OACS;AACT,QAAM,WAAW,KAAK,MAAM,GAAG;AAC/B,MAAI,CAAC,wBAAwB,IAAI,EAAG,QAAO;AAE3C,QAAM,cAAc,SAAS,IAAI;AACjC,MAAI,gBAAgB,OAAW,QAAO;AAEtC,MAAI,UAAU;AACd,aAAW,WAAW,UAAU;AAC9B,UAAM,WAAW,OAAO,OAAO,SAAS,OAAO,IAAI,QAAQ,OAAO,IAAI;AACtE,YAAQ,OAAO,IAAI,sBAAsB,QAAQ;AACjD,cAAU,QAAQ,OAAO;AAAA,EAC3B;AAEA,UAAQ,WAAW,IAAI;AACvB,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -36,6 +36,7 @@ import { scheduleUserTaskSla } from "./task-sla.js";
|
|
|
36
36
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
37
37
|
import { findWorkflowDefinition } from "./find-definition.js";
|
|
38
38
|
import { validateAgainstPorts } from "./port-contract.js";
|
|
39
|
+
import { safeGetNestedValue, safeSetNestedValue } from "./safe-mapping-path.js";
|
|
39
40
|
const logger = createLogger("workflows");
|
|
40
41
|
class StepExecutionError extends Error {
|
|
41
42
|
constructor(message, code, details) {
|
|
@@ -1109,24 +1110,12 @@ async function logStepEvent(em, event) {
|
|
|
1109
1110
|
await em.persist(workflowEvent).flush();
|
|
1110
1111
|
return workflowEvent;
|
|
1111
1112
|
}
|
|
1112
|
-
function getNestedValue(obj, path) {
|
|
1113
|
-
return path.split(".").reduce((current, key) => current?.[key], obj);
|
|
1114
|
-
}
|
|
1115
|
-
function setNestedValue(obj, path, value) {
|
|
1116
|
-
const keys = path.split(".");
|
|
1117
|
-
const lastKey = keys.pop();
|
|
1118
|
-
const target = keys.reduce((current, key) => {
|
|
1119
|
-
if (!(key in current)) current[key] = {};
|
|
1120
|
-
return current[key];
|
|
1121
|
-
}, obj);
|
|
1122
|
-
target[lastKey] = value;
|
|
1123
|
-
}
|
|
1124
1113
|
function mapInputData(sourceContext, mapping) {
|
|
1125
1114
|
const result = {};
|
|
1126
1115
|
for (const [targetKey, sourcePath] of Object.entries(mapping)) {
|
|
1127
|
-
const value =
|
|
1116
|
+
const value = safeGetNestedValue(sourceContext, sourcePath);
|
|
1128
1117
|
if (value !== void 0) {
|
|
1129
|
-
|
|
1118
|
+
safeSetNestedValue(result, targetKey, value);
|
|
1130
1119
|
}
|
|
1131
1120
|
}
|
|
1132
1121
|
return Object.keys(result).length > 0 ? result : sourceContext;
|
|
@@ -1134,9 +1123,9 @@ function mapInputData(sourceContext, mapping) {
|
|
|
1134
1123
|
function mapOutputData(childContext, mapping) {
|
|
1135
1124
|
const result = {};
|
|
1136
1125
|
for (const [targetKey, sourcePath] of Object.entries(mapping)) {
|
|
1137
|
-
const value =
|
|
1126
|
+
const value = safeGetNestedValue(childContext, sourcePath);
|
|
1138
1127
|
if (value !== void 0) {
|
|
1139
|
-
|
|
1128
|
+
safeSetNestedValue(result, targetKey, value);
|
|
1140
1129
|
}
|
|
1141
1130
|
}
|
|
1142
1131
|
return Object.keys(result).length > 0 ? result : childContext;
|