@solidxai/core 0.1.13-beta.2 → 0.1.13-beta.4
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/dist/commands/seed.command.d.ts +2 -0
- package/dist/commands/seed.command.d.ts.map +1 -1
- package/dist/commands/seed.command.js +21 -0
- package/dist/commands/seed.command.js.map +1 -1
- package/dist/entities/saved-filters.entity.js +3 -3
- package/dist/entities/saved-filters.entity.js.map +1 -1
- package/dist/entities/workflow-trigger-execution.entity.js +1 -1
- package/dist/entities/workflow-trigger-execution.entity.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/interfaces.d.ts +1 -0
- package/dist/interfaces.d.ts.map +1 -1
- package/dist/interfaces.js.map +1 -1
- package/dist/seeders/module-metadata-seeder.service.d.ts.map +1 -1
- package/dist/seeders/module-metadata-seeder.service.js +44 -27
- package/dist/seeders/module-metadata-seeder.service.js.map +1 -1
- package/dist/seeders/seed-data/solid-core-metadata.json +80 -67
- package/dist/services/workflow/nodes/execution-fail.node.d.ts +6 -0
- package/dist/services/workflow/nodes/execution-fail.node.d.ts.map +1 -0
- package/dist/services/workflow/nodes/execution-fail.node.js +81 -0
- package/dist/services/workflow/nodes/execution-fail.node.js.map +1 -0
- package/dist/services/workflow/nodes/for-each.node.d.ts +2 -0
- package/dist/services/workflow/nodes/for-each.node.d.ts.map +1 -1
- package/dist/services/workflow/nodes/for-each.node.js +114 -5
- package/dist/services/workflow/nodes/for-each.node.js.map +1 -1
- package/dist/services/workflow/nodes/if.node.d.ts.map +1 -1
- package/dist/services/workflow/nodes/if.node.js +7 -0
- package/dist/services/workflow/nodes/if.node.js.map +1 -1
- package/dist/services/workflow/nodes/log-write.node.d.ts.map +1 -1
- package/dist/services/workflow/nodes/log-write.node.js +2 -0
- package/dist/services/workflow/nodes/log-write.node.js.map +1 -1
- package/dist/services/workflow/nodes/loop-until.node.d.ts +12 -0
- package/dist/services/workflow/nodes/loop-until.node.d.ts.map +1 -0
- package/dist/services/workflow/nodes/loop-until.node.js +281 -0
- package/dist/services/workflow/nodes/loop-until.node.js.map +1 -0
- package/dist/services/workflow/nodes/parallel.node.d.ts.map +1 -1
- package/dist/services/workflow/nodes/parallel.node.js +7 -0
- package/dist/services/workflow/nodes/parallel.node.js.map +1 -1
- package/dist/services/workflow/nodes/sequential.node.d.ts.map +1 -1
- package/dist/services/workflow/nodes/sequential.node.js +7 -0
- package/dist/services/workflow/nodes/sequential.node.js.map +1 -1
- package/dist/services/workflow/nodes/switch.node.d.ts.map +1 -1
- package/dist/services/workflow/nodes/switch.node.js +7 -0
- package/dist/services/workflow/nodes/switch.node.js.map +1 -1
- package/dist/services/workflow/workflow-definition-validator.service.d.ts.map +1 -1
- package/dist/services/workflow/workflow-definition-validator.service.js +5 -2
- package/dist/services/workflow/workflow-definition-validator.service.js.map +1 -1
- package/dist/services/workflow/workflow-expression.service.d.ts.map +1 -1
- package/dist/services/workflow/workflow-expression.service.js +5 -0
- package/dist/services/workflow/workflow-expression.service.js.map +1 -1
- package/dist/services/workflow/workflow-node-registry.service.d.ts +2 -1
- package/dist/services/workflow/workflow-node-registry.service.d.ts.map +1 -1
- package/dist/services/workflow/workflow-node-registry.service.js +7 -0
- package/dist/services/workflow/workflow-node-registry.service.js.map +1 -1
- package/dist/services/workflow/workflow-runtime.service.d.ts +8 -0
- package/dist/services/workflow/workflow-runtime.service.d.ts.map +1 -1
- package/dist/services/workflow/workflow-runtime.service.js +197 -32
- package/dist/services/workflow/workflow-runtime.service.js.map +1 -1
- package/dist/solid-core.module.d.ts.map +1 -1
- package/dist/solid-core.module.js +4 -0
- package/dist/solid-core.module.js.map +1 -1
- package/dist/types/workflow-dsl.types.d.ts +14 -0
- package/dist/types/workflow-dsl.types.d.ts.map +1 -1
- package/dist/types/workflow-dsl.types.js.map +1 -1
- package/dist-tests/api/authenticate.spec.js +119 -0
- package/dist-tests/api/authenticate.spec.js.map +1 -0
- package/dist-tests/api/crud-service.findOne.cityMaster.spec.js +97 -0
- package/dist-tests/api/crud-service.findOne.cityMaster.spec.js.map +1 -0
- package/dist-tests/api/ping.spec.js +21 -0
- package/dist-tests/api/ping.spec.js.map +1 -0
- package/dist-tests/helpers/auth.js +41 -0
- package/dist-tests/helpers/auth.js.map +1 -0
- package/dist-tests/helpers/env.js +11 -0
- package/dist-tests/helpers/env.js.map +1 -0
- package/docs/agent-builder/notebook-lm-prompt-for-agent-builder.md +136 -0
- package/docs/agent-builder/team-ready-note-agent-builder.md +305 -0
- package/docs/agent-hub-grooming.md +301 -0
- package/docs/dashboards/AGENTIC_DASHBOARD_IMPLEMENTATION_PLAN.md +438 -0
- package/docs/dashboards/dashboard-curl-smoke-tests.txt +146 -0
- package/docs/dashboards/delete-legacy-dashboard-metadata.sql +172 -0
- package/docs/datasource-introspection-ddl-analysis.md +326 -0
- package/docs/datasource-introspection-implementation-plan.md +306 -0
- package/docs/grouping-enhancements.md +89 -0
- package/docs/java-spring/README.md +3 -0
- package/docs/java-spring/solid-core-module-deep-dive-report.md +1317 -0
- package/docs/module-package-import-handoff.md +691 -0
- package/docs/seed-changes.md +65 -0
- package/docs/test-data-workflow.md +200 -0
- package/docs/type-declaration-import-issue.md +24 -0
- package/docs/workflow/business-automation-example-notes.md +309 -0
- package/docs/workflow/control-flow-node-addition-sop.md +324 -0
- package/docs/workflow/data-engineering-pipeline-example-notes.md +330 -0
- package/docs/workflow/foreach-example-notes.md +187 -0
- package/docs/workflow/hello-world-example-notes.md +271 -0
- package/docs/workflow/kestra-concepts-plugins-blueprints.md +315 -0
- package/docs/workflow/loop-until-example-notes.md +198 -0
- package/docs/workflow/microservices-and-apis-example-notes.md +264 -0
- package/docs/workflow/samples.md +25 -0
- package/docs/workflow/what-is-kestra.md +79 -0
- package/docs/workflow/workflow-core-module-handoff-summary.md +191 -0
- package/docs/workflow/workflow-module-brd.md +185 -0
- package/docs/workflow/workflow-module-domain-model.md +252 -0
- package/docs/workflow/workflow-module-metadata-dsl.md +302 -0
- package/docs/workflow/workflow-module-technical-spec.md +293 -0
- package/docs/workflow/workflow-node-type-contracts.md +635 -0
- package/docs/workflow/workflow-node-ui-metadata-contract.md +1262 -0
- package/docs/workflow/workflow-solidx-model-field-spec.md +284 -0
- package/package.json +1 -1
- package/src/commands/seed.command.ts +19 -0
- package/src/entities/saved-filters.entity.ts +3 -3
- package/src/entities/workflow-trigger-execution.entity.ts +1 -1
- package/src/index.ts +2 -0
- package/src/interfaces.ts +1 -0
- package/src/seeders/module-metadata-seeder.service.ts +50 -36
- package/src/seeders/seed-data/solid-core-metadata.json +80 -67
- package/src/services/workflow/nodes/execution-fail.node.ts +82 -0
- package/src/services/workflow/nodes/for-each.node.ts +140 -6
- package/src/services/workflow/nodes/if.node.ts +7 -0
- package/src/services/workflow/nodes/log-write.node.ts +2 -0
- package/src/services/workflow/nodes/loop-until.node.ts +343 -0
- package/src/services/workflow/nodes/parallel.node.ts +7 -0
- package/src/services/workflow/nodes/sequential.node.ts +7 -0
- package/src/services/workflow/nodes/switch.node.ts +7 -0
- package/src/services/workflow/workflow-definition-validator.service.ts +5 -2
- package/src/services/workflow/workflow-expression.service.ts +5 -0
- package/src/services/workflow/workflow-node-registry.service.ts +11 -0
- package/src/services/workflow/workflow-runtime.service.ts +274 -39
- package/src/solid-core.module.ts +4 -0
- package/src/types/workflow-dsl.types.ts +43 -0
- package/.claude/settings.local.json +0 -16
- package/CLAUDE.md +0 -26
- package/src/services/1.js +0 -6
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import { BadRequestException } from '@nestjs/common';
|
|
2
|
+
import { WorkflowNodeProvider } from '../../../decorators/workflow-node-provider.decorator';
|
|
3
|
+
import { WorkflowNodeHandler } from '../../../interfaces/workflow-node-handler.interface';
|
|
4
|
+
import {
|
|
5
|
+
WorkflowNodeExecutionContext,
|
|
6
|
+
WorkflowNodeHandlerResult,
|
|
7
|
+
} from '../../../types/workflow-dsl.types';
|
|
8
|
+
|
|
9
|
+
type LoopUntilCheckFrequency = {
|
|
10
|
+
interval?: string | number;
|
|
11
|
+
maxIterations?: string | number;
|
|
12
|
+
maxDuration?: string | number;
|
|
13
|
+
failOnMaxReached?: boolean | string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
@WorkflowNodeProvider({
|
|
17
|
+
type: 'loopUntil',
|
|
18
|
+
kind: 'control',
|
|
19
|
+
category: 'control-flow',
|
|
20
|
+
label: 'Loop Until',
|
|
21
|
+
description: 'Runs child nodes repeatedly until a condition evaluates true.',
|
|
22
|
+
tags: ['loop', 'iteration', 'condition', 'polling'],
|
|
23
|
+
authoring: {
|
|
24
|
+
defaultConfiguration: {
|
|
25
|
+
condition: '{{ outputs.checkStatus.ready == true }}',
|
|
26
|
+
checkFrequency: {
|
|
27
|
+
interval: 'PT1M',
|
|
28
|
+
failOnMaxReached: false,
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
configurationLayout: {
|
|
32
|
+
type: 'tabs',
|
|
33
|
+
tabs: [
|
|
34
|
+
{
|
|
35
|
+
key: 'condition',
|
|
36
|
+
label: 'Condition',
|
|
37
|
+
groups: ['Condition'],
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
key: 'guardrails',
|
|
41
|
+
label: 'Guardrails',
|
|
42
|
+
groups: ['Guardrails'],
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
configurationFields: [
|
|
47
|
+
{
|
|
48
|
+
key: 'condition',
|
|
49
|
+
label: 'Condition',
|
|
50
|
+
description:
|
|
51
|
+
'Boolean expression evaluated after each loop iteration. It can read outputs from the latest child-node run.',
|
|
52
|
+
valueType: 'expression',
|
|
53
|
+
required: true,
|
|
54
|
+
expressionAllowed: true,
|
|
55
|
+
widgetHint: 'textarea',
|
|
56
|
+
group: 'Condition',
|
|
57
|
+
uiSchema: {
|
|
58
|
+
layout: {
|
|
59
|
+
width: 'full',
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
key: 'checkFrequency',
|
|
65
|
+
label: 'Check Frequency',
|
|
66
|
+
description:
|
|
67
|
+
'Optional loop guardrails: interval, maxIterations, maxDuration, and failOnMaxReached.',
|
|
68
|
+
valueType: 'object',
|
|
69
|
+
expressionAllowed: true,
|
|
70
|
+
widgetHint: 'yaml-editor',
|
|
71
|
+
group: 'Guardrails',
|
|
72
|
+
uiSchema: {
|
|
73
|
+
layout: {
|
|
74
|
+
width: 'full',
|
|
75
|
+
},
|
|
76
|
+
editor: {
|
|
77
|
+
height: 'min(320px, calc(90vh - 360px))',
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
childSlots: [
|
|
83
|
+
{
|
|
84
|
+
key: 'tasks',
|
|
85
|
+
label: 'Loop Body',
|
|
86
|
+
description: 'Nodes that run before the condition is evaluated.',
|
|
87
|
+
kind: 'sequence',
|
|
88
|
+
layout: 'sequential',
|
|
89
|
+
required: true,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
key: 'errors',
|
|
93
|
+
label: 'Errors',
|
|
94
|
+
description: 'Nodes that run sequentially if this loop fails.',
|
|
95
|
+
kind: 'sequence',
|
|
96
|
+
layout: 'sequential',
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
outputs: [
|
|
100
|
+
{
|
|
101
|
+
key: 'iterationCount',
|
|
102
|
+
label: 'Iteration Count',
|
|
103
|
+
description: 'Number of loop body iterations that ran.',
|
|
104
|
+
valueType: 'number',
|
|
105
|
+
path: 'iterationCount',
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
key: 'conditionMatched',
|
|
109
|
+
label: 'Condition Matched',
|
|
110
|
+
description: 'Whether the loop stopped because the condition evaluated true.',
|
|
111
|
+
valueType: 'boolean',
|
|
112
|
+
path: 'conditionMatched',
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
supportsDescription: true,
|
|
116
|
+
supportsName: true,
|
|
117
|
+
},
|
|
118
|
+
documentation: {
|
|
119
|
+
summary:
|
|
120
|
+
'Runs the loop body, evaluates the condition, and repeats after the configured interval until the condition is true or a guardrail is reached.',
|
|
121
|
+
},
|
|
122
|
+
ui: {
|
|
123
|
+
icon: 'si-repeat',
|
|
124
|
+
iconColor: '#0f766e',
|
|
125
|
+
iconBackgroundColor: '#ccfbf1',
|
|
126
|
+
iconBorderColor: '#99f6e4',
|
|
127
|
+
modalSize: 'lg',
|
|
128
|
+
layoutHints: {
|
|
129
|
+
groupOrder: ['Condition', 'Guardrails'],
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
})
|
|
133
|
+
export class LoopUntilNode implements WorkflowNodeHandler {
|
|
134
|
+
async execute(
|
|
135
|
+
context: WorkflowNodeExecutionContext,
|
|
136
|
+
): Promise<WorkflowNodeHandlerResult> {
|
|
137
|
+
const configuration = context.node.configuration ?? {};
|
|
138
|
+
const condition = configuration.condition ?? configuration.expression;
|
|
139
|
+
if (!condition) {
|
|
140
|
+
throw new BadRequestException('loopUntil requires configuration.condition.');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const checkFrequency = this.resolveCheckFrequency(
|
|
144
|
+
context.expression.interpolate(configuration.checkFrequency ?? {}, context),
|
|
145
|
+
);
|
|
146
|
+
const childNodes = context.node.tasks ?? [];
|
|
147
|
+
const startedAt = Date.now();
|
|
148
|
+
let iterationCount = 0;
|
|
149
|
+
let conditionMatched = false;
|
|
150
|
+
let maxReached = false;
|
|
151
|
+
let latestChildOutputs: Record<string, any> = {};
|
|
152
|
+
|
|
153
|
+
while (true) {
|
|
154
|
+
iterationCount += 1;
|
|
155
|
+
context.outputs[context.node.id] = {
|
|
156
|
+
iterationCount,
|
|
157
|
+
conditionMatched: false,
|
|
158
|
+
maxReached: false,
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const iterationOutputs = {
|
|
162
|
+
...(context.outputs ?? {}),
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
latestChildOutputs = await context.runNodes(childNodes, {
|
|
166
|
+
outputs: iterationOutputs,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
Object.assign(context.outputs, iterationOutputs);
|
|
170
|
+
|
|
171
|
+
conditionMatched = context.expression.evaluateCondition(condition, {
|
|
172
|
+
...context,
|
|
173
|
+
outputs: context.outputs,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
context.outputs[context.node.id] = {
|
|
177
|
+
iterationCount,
|
|
178
|
+
conditionMatched,
|
|
179
|
+
maxReached: false,
|
|
180
|
+
latestOutputs: latestChildOutputs,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
await context.emitLog({
|
|
184
|
+
level: 'info',
|
|
185
|
+
eventType: 'node.loopUntil.evaluated',
|
|
186
|
+
source: 'loopUntil',
|
|
187
|
+
message: `LoopUntil node "${context.node.id}" iteration ${iterationCount} evaluated to ${conditionMatched}.`,
|
|
188
|
+
context: {
|
|
189
|
+
condition,
|
|
190
|
+
iterationCount,
|
|
191
|
+
conditionMatched,
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
if (conditionMatched) {
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
maxReached = this.hasReachedGuardrail(
|
|
200
|
+
iterationCount,
|
|
201
|
+
startedAt,
|
|
202
|
+
checkFrequency,
|
|
203
|
+
);
|
|
204
|
+
if (maxReached) {
|
|
205
|
+
if (checkFrequency.failOnMaxReached) {
|
|
206
|
+
throw new BadRequestException(
|
|
207
|
+
`loopUntil "${context.node.id}" reached its maximum check frequency guardrail before the condition matched.`,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (checkFrequency.intervalMs > 0) {
|
|
214
|
+
await this.delay(checkFrequency.intervalMs);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
output: {
|
|
220
|
+
iterationCount,
|
|
221
|
+
conditionMatched,
|
|
222
|
+
maxReached,
|
|
223
|
+
latestOutputs: latestChildOutputs,
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private resolveCheckFrequency(value: any) {
|
|
229
|
+
const checkFrequency: LoopUntilCheckFrequency =
|
|
230
|
+
value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
231
|
+
const maxIterations = this.optionalPositiveInteger(
|
|
232
|
+
checkFrequency.maxIterations,
|
|
233
|
+
'checkFrequency.maxIterations',
|
|
234
|
+
);
|
|
235
|
+
const maxDurationMs =
|
|
236
|
+
checkFrequency.maxDuration === undefined ||
|
|
237
|
+
checkFrequency.maxDuration === null ||
|
|
238
|
+
checkFrequency.maxDuration === ''
|
|
239
|
+
? undefined
|
|
240
|
+
: this.parseDurationMs(checkFrequency.maxDuration, 'checkFrequency.maxDuration');
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
intervalMs: this.parseDurationMs(
|
|
244
|
+
checkFrequency.interval ?? 'PT1M',
|
|
245
|
+
'checkFrequency.interval',
|
|
246
|
+
),
|
|
247
|
+
maxIterations,
|
|
248
|
+
maxDurationMs,
|
|
249
|
+
failOnMaxReached: this.resolveBoolean(checkFrequency.failOnMaxReached),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private hasReachedGuardrail(
|
|
254
|
+
iterationCount: number,
|
|
255
|
+
startedAt: number,
|
|
256
|
+
checkFrequency: {
|
|
257
|
+
maxIterations?: number;
|
|
258
|
+
maxDurationMs?: number;
|
|
259
|
+
},
|
|
260
|
+
): boolean {
|
|
261
|
+
return (
|
|
262
|
+
(checkFrequency.maxIterations !== undefined &&
|
|
263
|
+
iterationCount >= checkFrequency.maxIterations) ||
|
|
264
|
+
(checkFrequency.maxDurationMs !== undefined &&
|
|
265
|
+
Date.now() - startedAt >= checkFrequency.maxDurationMs)
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private optionalPositiveInteger(value: any, label: string): number | undefined {
|
|
270
|
+
if (value === undefined || value === null || value === '') {
|
|
271
|
+
return undefined;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const parsed = Number(value);
|
|
275
|
+
if (!Number.isFinite(parsed) || parsed < 1) {
|
|
276
|
+
throw new BadRequestException(`${label} must be a positive integer.`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return Math.floor(parsed);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private parseDurationMs(value: any, label: string): number {
|
|
283
|
+
if (typeof value === 'number') {
|
|
284
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
285
|
+
throw new BadRequestException(`${label} must be a non-negative duration.`);
|
|
286
|
+
}
|
|
287
|
+
return value;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const raw = String(value ?? '').trim();
|
|
291
|
+
if (!raw) {
|
|
292
|
+
return 0;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (/^\d+$/.test(raw)) {
|
|
296
|
+
return Number(raw);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const isoMatch = raw.match(
|
|
300
|
+
/^P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?)?$/i,
|
|
301
|
+
);
|
|
302
|
+
if (isoMatch) {
|
|
303
|
+
const [, days, hours, minutes, seconds] = isoMatch;
|
|
304
|
+
return (
|
|
305
|
+
Number(days ?? 0) * 24 * 60 * 60 * 1000 +
|
|
306
|
+
Number(hours ?? 0) * 60 * 60 * 1000 +
|
|
307
|
+
Number(minutes ?? 0) * 60 * 1000 +
|
|
308
|
+
Number(seconds ?? 0) * 1000
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const unitMatch = raw.match(/^(\d+(?:\.\d+)?)(ms|s|m|h|d)$/i);
|
|
313
|
+
if (unitMatch) {
|
|
314
|
+
const amount = Number(unitMatch[1]);
|
|
315
|
+
const unit = unitMatch[2].toLowerCase();
|
|
316
|
+
const multiplier =
|
|
317
|
+
unit === 'ms'
|
|
318
|
+
? 1
|
|
319
|
+
: unit === 's'
|
|
320
|
+
? 1000
|
|
321
|
+
: unit === 'm'
|
|
322
|
+
? 60 * 1000
|
|
323
|
+
: unit === 'h'
|
|
324
|
+
? 60 * 60 * 1000
|
|
325
|
+
: 24 * 60 * 60 * 1000;
|
|
326
|
+
return amount * multiplier;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
throw new BadRequestException(`${label} must be milliseconds, 1s/1m style, or ISO-8601 duration.`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
private resolveBoolean(value: any): boolean {
|
|
333
|
+
if (value === true || value === 'true') {
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private delay(ms: number): Promise<void> {
|
|
341
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
@@ -23,6 +23,13 @@ import {
|
|
|
23
23
|
layout: 'parallel',
|
|
24
24
|
required: true,
|
|
25
25
|
},
|
|
26
|
+
{
|
|
27
|
+
key: 'errors',
|
|
28
|
+
label: 'Errors',
|
|
29
|
+
description: 'Nodes that run sequentially if this parallel branch fails.',
|
|
30
|
+
kind: 'sequence',
|
|
31
|
+
layout: 'sequential',
|
|
32
|
+
},
|
|
26
33
|
],
|
|
27
34
|
supportsDescription: true,
|
|
28
35
|
supportsName: true,
|
|
@@ -23,6 +23,13 @@ import {
|
|
|
23
23
|
layout: 'sequential',
|
|
24
24
|
required: true,
|
|
25
25
|
},
|
|
26
|
+
{
|
|
27
|
+
key: 'errors',
|
|
28
|
+
label: 'Errors',
|
|
29
|
+
description: 'Nodes that run sequentially if this sequence fails.',
|
|
30
|
+
kind: 'sequence',
|
|
31
|
+
layout: 'sequential',
|
|
32
|
+
},
|
|
26
33
|
],
|
|
27
34
|
supportsDescription: true,
|
|
28
35
|
supportsName: true,
|
|
@@ -47,6 +47,13 @@ import {
|
|
|
47
47
|
'Fallback sequence that runs when no case key matches the resolved value.',
|
|
48
48
|
kind: 'sequence',
|
|
49
49
|
},
|
|
50
|
+
{
|
|
51
|
+
key: 'errors',
|
|
52
|
+
label: 'Errors',
|
|
53
|
+
description: 'Nodes that run sequentially if the selected case fails.',
|
|
54
|
+
kind: 'sequence',
|
|
55
|
+
layout: 'sequential',
|
|
56
|
+
},
|
|
50
57
|
],
|
|
51
58
|
supportsDescription: true,
|
|
52
59
|
supportsName: true,
|
|
@@ -30,6 +30,8 @@ export class WorkflowDefinitionValidatorService {
|
|
|
30
30
|
|
|
31
31
|
const nodeIds = new Set<string>();
|
|
32
32
|
this.validateNodes(definition.nodes, nodeIds);
|
|
33
|
+
this.validateNodes(definition.errors ?? [], nodeIds);
|
|
34
|
+
this.validateNodes(definition.finally ?? [], nodeIds);
|
|
33
35
|
this.validateTriggers(definition);
|
|
34
36
|
}
|
|
35
37
|
|
|
@@ -144,12 +146,12 @@ export class WorkflowDefinitionValidatorService {
|
|
|
144
146
|
for (const unsupportedKey of ['children', 'branches', 'nodes']) {
|
|
145
147
|
if (Object.prototype.hasOwnProperty.call(node, unsupportedKey)) {
|
|
146
148
|
throw new BadRequestException(
|
|
147
|
-
`Workflow node "${node.id ?? 'unknown'}" uses unsupported child key "${unsupportedKey}". Use "tasks", "then", "else", "defaults", or "cases" instead.`,
|
|
149
|
+
`Workflow node "${node.id ?? 'unknown'}" uses unsupported child key "${unsupportedKey}". Use "tasks", "then", "else", "defaults", "errors", or "cases" instead.`,
|
|
148
150
|
);
|
|
149
151
|
}
|
|
150
152
|
}
|
|
151
153
|
|
|
152
|
-
for (const childKey of ['tasks', 'then', 'else', 'defaults'] as const) {
|
|
154
|
+
for (const childKey of ['tasks', 'then', 'else', 'defaults', 'errors'] as const) {
|
|
153
155
|
const childValue = node[childKey];
|
|
154
156
|
if (
|
|
155
157
|
childValue !== undefined &&
|
|
@@ -216,6 +218,7 @@ export class WorkflowDefinitionValidatorService {
|
|
|
216
218
|
this.validateNodes(node.then ?? [], nodeIds);
|
|
217
219
|
this.validateNodes(node.else ?? [], nodeIds);
|
|
218
220
|
this.validateNodes(node.defaults ?? [], nodeIds);
|
|
221
|
+
this.validateNodes(node.errors ?? [], nodeIds);
|
|
219
222
|
Object.values(node.cases ?? {}).forEach((caseNodes) => {
|
|
220
223
|
this.validateNodes(caseNodes, nodeIds);
|
|
221
224
|
});
|
|
@@ -139,10 +139,15 @@ export class WorkflowExpressionService implements WorkflowExpressionResolver {
|
|
|
139
139
|
node: context.node,
|
|
140
140
|
item: context.item,
|
|
141
141
|
index: context.index,
|
|
142
|
+
parent: context.parent,
|
|
143
|
+
parents: context.parents,
|
|
144
|
+
loops: context.loops,
|
|
145
|
+
error: context.error,
|
|
142
146
|
};
|
|
143
147
|
|
|
144
148
|
const parts = path
|
|
145
149
|
.replace(/\[(?:'|")([^'"]+)(?:'|")\]/g, '.$1')
|
|
150
|
+
.replace(/\[(\d+)\]/g, '.$1')
|
|
146
151
|
.split('.')
|
|
147
152
|
.filter(Boolean);
|
|
148
153
|
|
|
@@ -53,6 +53,17 @@ export class WorkflowNodeRegistryService implements OnModuleInit {
|
|
|
53
53
|
return provider.handler;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
getMetadata(type: string): WorkflowNodeProviderMetadata {
|
|
57
|
+
const provider = this.providers.get(type);
|
|
58
|
+
if (!provider) {
|
|
59
|
+
throw new BadRequestException(
|
|
60
|
+
`No workflow node provider registered for type "${type}".`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return provider.metadata;
|
|
65
|
+
}
|
|
66
|
+
|
|
56
67
|
list(): WorkflowNodeMetadataResponse[] {
|
|
57
68
|
return Array.from(this.providers.values())
|
|
58
69
|
.map(({ metadata }) => this.toMetadataResponse(metadata))
|