@uipath/maestro-builder-sdk 6.18.9 → 6.19.0
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/api-index.md +185 -184
- package/dist/api-members.md +504 -473
- package/dist/bpmn/uipath-moddle.v1.json +5 -0
- package/dist/case/decompile-cli.js +2 -8
- package/dist/check.js +177 -650
- package/dist/core/actions.d.ts +119 -760
- package/dist/core/actions.js +0 -306
- package/dist/core/error-port.d.ts +6 -0
- package/dist/core/error-port.js +9 -0
- package/dist/core/node-classes.d.ts +4 -63
- package/dist/core/node-classes.js +0 -112
- package/dist/core/nodes/family.d.ts +36 -0
- package/dist/core/nodes/family.js +1 -0
- package/dist/core/nodes/node-type.d.ts +136 -0
- package/dist/core/nodes/node-type.js +1 -0
- package/dist/core/nodes/registry.d.ts +57 -0
- package/dist/core/nodes/registry.js +122 -0
- package/dist/core/nodes/versioned.d.ts +50 -0
- package/dist/core/nodes/versioned.js +46 -0
- package/dist/core/typed-raw-node.d.ts +13 -62
- package/dist/core/typed-raw-node.js +40 -154
- package/dist/decompile-cli.js +2 -9
- package/dist/decompile.js +92 -271
- package/dist/flow-definitions.d.ts +10 -0
- package/dist/flow-definitions.js +35 -0
- package/dist/flow-node-registry.d.ts +13 -0
- package/dist/flow-node-registry.js +19 -0
- package/dist/flow-node-type.d.ts +192 -0
- package/dist/flow-node-type.js +1 -0
- package/dist/flow-sdk.d.ts +7 -0
- package/dist/flow-sdk.js +8 -0
- package/dist/nodes/data-fabric/flow/check.d.ts +13 -0
- package/dist/nodes/data-fabric/flow/check.js +164 -0
- package/dist/nodes/data-fabric/flow/factories.d.ts +113 -0
- package/dist/nodes/data-fabric/flow/factories.js +134 -0
- package/dist/nodes/data-fabric/flow/family.d.ts +7 -0
- package/dist/nodes/data-fabric/flow/family.js +9 -0
- package/dist/nodes/data-fabric/flow/index.d.ts +8 -0
- package/dist/nodes/data-fabric/flow/index.js +8 -0
- package/dist/nodes/data-fabric/flow/raw.d.ts +32 -0
- package/dist/nodes/data-fabric/flow/raw.js +100 -0
- package/dist/nodes/data-fabric/flow/shared.d.ts +77 -0
- package/dist/nodes/data-fabric/flow/shared.js +174 -0
- package/dist/nodes/data-fabric/flow/spec.d.ts +238 -0
- package/dist/nodes/data-fabric/flow/spec.js +1 -0
- package/dist/nodes/data-fabric/flow/verbs.d.ts +29 -0
- package/dist/nodes/data-fabric/flow/verbs.js +239 -0
- package/dist/nodes/delay/flow/check.d.ts +16 -0
- package/dist/nodes/delay/flow/check.js +86 -0
- package/dist/nodes/delay/flow/factories.d.ts +28 -0
- package/dist/nodes/delay/flow/factories.js +38 -0
- package/dist/nodes/delay/flow/family.d.ts +3 -0
- package/dist/nodes/delay/flow/family.js +8 -0
- package/dist/nodes/delay/flow/index.d.ts +3 -0
- package/dist/nodes/delay/flow/index.js +3 -0
- package/dist/nodes/delay/flow/node-type.d.ts +13 -0
- package/dist/nodes/delay/flow/node-type.js +71 -0
- package/dist/nodes/delay/flow/spec.d.ts +43 -0
- package/dist/nodes/delay/flow/spec.js +16 -0
- package/dist/nodes/document/flow/factories.d.ts +27 -0
- package/dist/nodes/document/flow/factories.js +30 -0
- package/dist/nodes/document/flow/family.d.ts +3 -0
- package/dist/nodes/document/flow/family.js +7 -0
- package/dist/nodes/document/flow/index.d.ts +3 -0
- package/dist/nodes/document/flow/index.js +3 -0
- package/dist/nodes/document/flow/node-type.d.ts +14 -0
- package/dist/nodes/document/flow/node-type.js +150 -0
- package/dist/nodes/document/flow/spec.d.ts +65 -0
- package/dist/nodes/document/flow/spec.js +1 -0
- package/dist/nodes/mock/flow/factories.d.ts +51 -0
- package/dist/nodes/mock/flow/factories.js +47 -0
- package/dist/nodes/mock/flow/family.d.ts +3 -0
- package/dist/nodes/mock/flow/family.js +7 -0
- package/dist/nodes/mock/flow/index.d.ts +2 -0
- package/dist/nodes/mock/flow/index.js +2 -0
- package/dist/nodes/mock/flow/node-type.d.ts +11 -0
- package/dist/nodes/mock/flow/node-type.js +51 -0
- package/dist/nodes/script/flow/check.d.ts +19 -0
- package/dist/nodes/script/flow/check.js +49 -0
- package/dist/nodes/script/flow/factories.d.ts +14 -0
- package/dist/nodes/script/flow/factories.js +15 -0
- package/dist/nodes/script/flow/family.d.ts +3 -0
- package/dist/nodes/script/flow/family.js +8 -0
- package/dist/nodes/script/flow/index.d.ts +3 -0
- package/dist/nodes/script/flow/index.js +3 -0
- package/dist/nodes/script/flow/node-type.d.ts +7 -0
- package/dist/nodes/script/flow/node-type.js +125 -0
- package/dist/nodes/script/flow/spec.d.ts +52 -0
- package/dist/nodes/script/flow/spec.js +1 -0
- package/dist/nodes/transform/flow/check.d.ts +6 -0
- package/dist/nodes/transform/flow/check.js +229 -0
- package/dist/nodes/transform/flow/factories.d.ts +52 -0
- package/dist/nodes/transform/flow/factories.js +53 -0
- package/dist/nodes/transform/flow/family.d.ts +3 -0
- package/dist/nodes/transform/flow/family.js +8 -0
- package/dist/nodes/transform/flow/index.d.ts +3 -0
- package/dist/nodes/transform/flow/index.js +3 -0
- package/dist/nodes/transform/flow/node-type.d.ts +7 -0
- package/dist/nodes/transform/flow/node-type.js +244 -0
- package/dist/nodes/transform/flow/spec.d.ts +116 -0
- package/dist/nodes/transform/flow/spec.js +10 -0
- package/dist/raw-node-advice.d.ts +16 -0
- package/dist/raw-node-advice.js +36 -0
- package/dist/raw-node.d.ts +71 -0
- package/dist/raw-node.js +116 -0
- package/dist/registry/uip/launch.d.ts +11 -1
- package/dist/registry/uip/launch.js +50 -43
- package/dist/registry/uip/pipeline-front-door.d.ts +2 -0
- package/dist/registry/uip/pipeline-front-door.js +88 -0
- package/dist/serialize.js +138 -492
- package/package.json +2 -2
package/dist/check.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* duplicate step names, references to undeclared variables/steps, empty branch
|
|
6
6
|
* arms, and missing terminals.
|
|
7
7
|
*/
|
|
8
|
-
import { Expr, SCHEDULE_PRESETS,
|
|
8
|
+
import { Expr, SCHEDULE_PRESETS, parseIxpProjectType, flatNameOf, flatNameSegmentsOf, FLAT_SEP } from './flow-sdk.js';
|
|
9
9
|
import { FlowNode } from './core/node-classes.js';
|
|
10
10
|
import { planContributionBindings } from './core/contribution-bindings.js';
|
|
11
11
|
import { definitionHasErrorPort, errorPortRefusal } from './core/error-port.js';
|
|
@@ -26,6 +26,9 @@ import { FLOW_SCHEMA_VERSIONS } from './format-profile.js';
|
|
|
26
26
|
import { compareSchemaVersions, schemaVersionRefusal } from './schema-version.js';
|
|
27
27
|
import { planFlowGlobals, planSharedInputs, sharedInputRoots } from './flow-variables.js';
|
|
28
28
|
import { ownEntry } from './core/own-entry.js';
|
|
29
|
+
import { FLOW_NODE_TYPES } from './flow-node-registry.js';
|
|
30
|
+
import { resolveVersion } from './core/nodes/versioned.js';
|
|
31
|
+
import { declaredErrorPortRefusal } from './core/error-port.js';
|
|
29
32
|
export function check(built, opts = {}) {
|
|
30
33
|
let diags = [];
|
|
31
34
|
const uses = collectConnectionUses(() => { diags = checkFlow(built, [], opts); });
|
|
@@ -115,6 +118,12 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
115
118
|
]);
|
|
116
119
|
const outputType = new Map(built.outputs.map((x) => [x.name, String(x.type)]));
|
|
117
120
|
const stepNames = new Set();
|
|
121
|
+
// Steps whose kind is a registered node type (flow-node-registry.ts), by name:
|
|
122
|
+
// their node-local rules, expressions and output contract come from the node
|
|
123
|
+
// type, and their family's rules run over all of them after the walk.
|
|
124
|
+
const registeredSteps = new Map();
|
|
125
|
+
/** Steps of a registered kind whose version did not resolve (NODE_VERSION_UNREGISTERED), so a family rule can stay silent about them. */
|
|
126
|
+
const unresolvedSteps = new Set();
|
|
118
127
|
/**
|
|
119
128
|
* Every expression the flow carries, with the step that failed when the
|
|
120
129
|
* expression sits inside that step's error handler. The failed step's own
|
|
@@ -133,8 +142,6 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
133
142
|
// `core.logic.merge` definition declares no outputs at all (see
|
|
134
143
|
// PARALLEL_HAS_NO_OUTPUT below).
|
|
135
144
|
const parallelNames = new Set();
|
|
136
|
-
const delayNames = new Set();
|
|
137
|
-
const dataFabricReads = new Set();
|
|
138
145
|
// Per-outcome routing bookkeeping: which hitl steps opted into outcome ports
|
|
139
146
|
// (and their declared outcome ids), and which ids actually got a route.
|
|
140
147
|
const hitlOutcomeSteps = new Map();
|
|
@@ -151,22 +158,6 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
151
158
|
// waits until the expressions are collected (see HITL_OUTCOMES_UNREACHABLE
|
|
152
159
|
// below).
|
|
153
160
|
const hitlChoiceNoPorts = new Map();
|
|
154
|
-
// Data Fabric writes that target a record `{ fromRead: '<step>' }` — update and
|
|
155
|
-
// delete share the selector, so they share the check.
|
|
156
|
-
const dataFabricRefs = [];
|
|
157
|
-
// Delete step names. `core.datafabric.delete` is the one verb of the family
|
|
158
|
-
// whose definition declares NO outputDefinition, so `$vars.<delete>.output` is
|
|
159
|
-
// undefined at run time (see DATAFABRIC_DELETE_HAS_NO_OUTPUT below).
|
|
160
|
-
const dataFabricDeletes = new Set();
|
|
161
|
-
// Reads that return a LIST. A multi-record read is refused as a write target
|
|
162
|
-
// (see DATAFABRIC_FROMREAD_MULTIPLE below) — the serializer emits a plain
|
|
163
|
-
// no-op task for the write, and nothing at any rung reports it.
|
|
164
|
-
const dataFabricMultiReads = new Set();
|
|
165
|
-
// Placeholder (`mock()`) step names. The platform's `core.logic.mock` declares
|
|
166
|
-
// one output whose `source` is the literal `"null"` — it publishes nothing —
|
|
167
|
-
// so `$vars.<mock>.output` is null at run time, always (see
|
|
168
|
-
// MOCK_HAS_NO_OUTPUT below).
|
|
169
|
-
const mockNames = new Set();
|
|
170
161
|
// Terminate step names. A terminate declares no action output in any registry
|
|
171
162
|
// version, so `$vars.<terminate>` is undefined at run time. The terminal
|
|
172
163
|
// flow-output mappings the serializer emits are validation metadata, not a
|
|
@@ -260,7 +251,7 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
260
251
|
declared.add(s.name); // a step's output is referenceable by name
|
|
261
252
|
}
|
|
262
253
|
if ((s.kind === 'stepToList' || s.kind === 'stepToRef') && s.port === 'error')
|
|
263
|
-
checkOnError(s, prevPortSource, diags);
|
|
254
|
+
checkOnError(s, prevPortSource, diags, prevPortSource && registeredSteps.get(prevPortSource.name));
|
|
264
255
|
if ((s.kind === 'stepToList' || s.kind === 'stepToRef') && s.port.startsWith('branch-')) {
|
|
265
256
|
const want = s.port.slice('branch-'.length);
|
|
266
257
|
const prevSpec = prevAction?.spec;
|
|
@@ -363,7 +354,17 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
363
354
|
if (s.kind === 'action') {
|
|
364
355
|
prevAction = s;
|
|
365
356
|
prevPortSource = s;
|
|
366
|
-
|
|
357
|
+
const registered = registeredKind(s.spec);
|
|
358
|
+
if (registered) {
|
|
359
|
+
const placed = checkRegisteredStep(registered, s, exprs, errorSource, opts, diags, typeOf, triggerId);
|
|
360
|
+
if (placed)
|
|
361
|
+
registeredSteps.set(s.name, placed);
|
|
362
|
+
else
|
|
363
|
+
unresolvedSteps.add(s.name);
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
collectActionExprs(s, exprs, errorSource);
|
|
367
|
+
}
|
|
367
368
|
if (s.spec.kind === 'connector') {
|
|
368
369
|
connectorSteps.set(s.name, s.spec);
|
|
369
370
|
checkConnectorLookups(s.name, s.spec, opts, diags);
|
|
@@ -380,8 +381,6 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
380
381
|
}
|
|
381
382
|
checkContribution(maybeCustom, s.options?.version, s.name, own.some((d) => d.severity === 'error'), diags);
|
|
382
383
|
}
|
|
383
|
-
if (s.spec.kind === 'transform')
|
|
384
|
-
checkTransform(s.name, s.spec.inputs, diags, typeOf, triggerId);
|
|
385
384
|
if (s.spec.kind === 'http') {
|
|
386
385
|
checkHttp(s.name, s.spec.inputs, diags);
|
|
387
386
|
// Only a complete pair: an empty half is HTTP_MANAGED_BINDING_INCOMPLETE, which
|
|
@@ -419,14 +418,6 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
419
418
|
}
|
|
420
419
|
}
|
|
421
420
|
}
|
|
422
|
-
if (s.spec.kind === 'script')
|
|
423
|
-
checkScript(s.name, s.spec.inputs, diags);
|
|
424
|
-
if (s.spec.kind === 'delay') {
|
|
425
|
-
checkDelay(s.name, s.spec.inputs, diags);
|
|
426
|
-
delayNames.add(s.name);
|
|
427
|
-
}
|
|
428
|
-
if (s.spec.kind === 'mock')
|
|
429
|
-
mockNames.add(s.name);
|
|
430
421
|
// `.stepSwitch` routes its exits as ARMS, so the `.stepToList` bookkeeping
|
|
431
422
|
// would report every one of them unrouted. STEP_SWITCH_EXIT_UNROUTED is
|
|
432
423
|
// that step's version of the same question.
|
|
@@ -497,71 +488,6 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
497
488
|
outputReaders.set(stepName, (field, rest, at, d) => rule(stepName, field, rest, inputs, at, d));
|
|
498
489
|
}
|
|
499
490
|
}
|
|
500
|
-
if (s.spec.kind === 'documentClassify' || s.spec.kind === 'dynamicExtract') {
|
|
501
|
-
const fr = s.spec.inputs.fileRef;
|
|
502
|
-
const empty = fr === undefined || fr === null || (typeof fr === 'string' && fr.trim() === '');
|
|
503
|
-
if (empty) {
|
|
504
|
-
diags.push({
|
|
505
|
-
level: 'error', code: 'DOC_NO_FILEREF', step: s.name,
|
|
506
|
-
message: `"${s.name}" has no fileRef — nothing to ${s.spec.kind === 'documentClassify' ? 'classify' : 'extract from'}. Reference a file input or an upstream attachment.`,
|
|
507
|
-
});
|
|
508
|
-
}
|
|
509
|
-
if (s.spec.kind === 'dynamicExtract' && s.spec.inputs.schema !== undefined
|
|
510
|
-
&& (typeof s.spec.inputs.schema !== 'object' || Array.isArray(s.spec.inputs.schema))) {
|
|
511
|
-
diags.push({
|
|
512
|
-
level: 'error', code: 'EXTRACT_SCHEMA_SHAPE', step: s.name,
|
|
513
|
-
message: `"${s.name}"'s schema must be an object (the inline extraction schema document).`,
|
|
514
|
-
});
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
if (s.spec.kind === 'dataFabricRead' || s.spec.kind === 'dataFabricCreate'
|
|
518
|
-
|| s.spec.kind === 'dataFabricUpdate' || s.spec.kind === 'dataFabricDelete') {
|
|
519
|
-
if (!String(s.spec.inputs.entity ?? '').trim()) {
|
|
520
|
-
diags.push({
|
|
521
|
-
level: 'error', code: 'DATAFABRIC_NO_ENTITY', step: s.name,
|
|
522
|
-
message: `"${s.name}" names no Data Fabric entity. Set { entity: '<entity name>' }.`,
|
|
523
|
-
});
|
|
524
|
-
}
|
|
525
|
-
if (s.spec.kind === 'dataFabricRead') {
|
|
526
|
-
dataFabricReads.add(s.name);
|
|
527
|
-
if (s.spec.inputs.resultMode === 'multiple')
|
|
528
|
-
dataFabricMultiReads.add(s.name);
|
|
529
|
-
checkDataFabricQuery(s.name, s.spec.inputs, diags);
|
|
530
|
-
}
|
|
531
|
-
if (s.spec.kind === 'dataFabricCreate') {
|
|
532
|
-
// Not the same failure as an empty update: a BLANK value is omitted
|
|
533
|
-
// from the insert so the column default applies, so an all-blank
|
|
534
|
-
// `fieldValues` serializes to an empty body and runs green having
|
|
535
|
-
// inserted nothing. The node's schema rejects that — and the fix is
|
|
536
|
-
// a real value, never a placeholder to satisfy the rule.
|
|
537
|
-
const written = Object.values(s.spec.inputs.values ?? {})
|
|
538
|
-
.filter((v) => (v instanceof Expr ? true : String(v ?? '').trim() !== ''));
|
|
539
|
-
if (written.length === 0) {
|
|
540
|
-
diags.push({
|
|
541
|
-
level: 'error', code: 'DATAFABRIC_EMPTY_VALUES', step: s.name,
|
|
542
|
-
message: `"${s.name}" sets no column to a non-blank value, so it would insert nothing and run green. `
|
|
543
|
-
+ 'The node\'s schema rejects that — give a real column a real value rather than a placeholder.',
|
|
544
|
-
});
|
|
545
|
-
}
|
|
546
|
-
checkDataFabricColumns(s.name, Object.keys(s.spec.inputs.values ?? {}), 'values', diags);
|
|
547
|
-
}
|
|
548
|
-
if (s.spec.kind === 'dataFabricUpdate') {
|
|
549
|
-
if (Object.keys(s.spec.inputs.set ?? {}).length === 0) {
|
|
550
|
-
diags.push({
|
|
551
|
-
level: 'error', code: 'DATAFABRIC_EMPTY_SET', step: s.name,
|
|
552
|
-
message: `"${s.name}" writes no columns — { set } is empty, so the update would no-op green.`,
|
|
553
|
-
});
|
|
554
|
-
}
|
|
555
|
-
checkDataFabricColumns(s.name, Object.keys(s.spec.inputs.set ?? {}), 'set', diags);
|
|
556
|
-
}
|
|
557
|
-
checkDataFabricScope(s.name, s.spec.inputs, diags);
|
|
558
|
-
if (s.spec.kind === 'dataFabricUpdate' || s.spec.kind === 'dataFabricDelete') {
|
|
559
|
-
const factory = s.spec.kind === 'dataFabricUpdate' ? 'dataFabricUpdate' : 'dataFabricDelete';
|
|
560
|
-
dataFabricRefs.push({ step: s.name, factory, record: s.spec.inputs.record });
|
|
561
|
-
if (s.spec.kind === 'dataFabricDelete')
|
|
562
|
-
dataFabricDeletes.add(s.name);
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
491
|
if (s.spec.kind === 'ixpExtract')
|
|
566
492
|
checkIxpExtract(s.name, s.spec.inputs, diags);
|
|
567
493
|
if (s.spec.kind === 'waitEvent') {
|
|
@@ -839,30 +765,7 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
839
765
|
}
|
|
840
766
|
}
|
|
841
767
|
reportUnreachableHitlOutcomes(hitlChoiceNoPorts, exprs, diags);
|
|
842
|
-
|
|
843
|
-
const fromRead = record?.fromRead;
|
|
844
|
-
if (typeof fromRead !== 'string')
|
|
845
|
-
continue;
|
|
846
|
-
if (!dataFabricReads.has(fromRead)) {
|
|
847
|
-
diags.push({
|
|
848
|
-
level: 'error', code: 'DATAFABRIC_FROMREAD_UNKNOWN', step,
|
|
849
|
-
message: `"${step}" targets record: { fromRead: ${JSON.stringify(fromRead)} }, but no dataFabricRead step has that name.`,
|
|
850
|
-
suggestion: `${factory}({ …, record: { byId: '<record id>' } })`,
|
|
851
|
-
});
|
|
852
|
-
continue;
|
|
853
|
-
}
|
|
854
|
-
// A LIST is not a write target: the serializer refuses it and emits a plain
|
|
855
|
-
// no-op task, so the flow validates, runs green, and writes nothing.
|
|
856
|
-
if (dataFabricMultiReads.has(fromRead)) {
|
|
857
|
-
diags.push({
|
|
858
|
-
level: 'error', code: 'DATAFABRIC_FROMREAD_MULTIPLE', step,
|
|
859
|
-
message: `"${step}" targets record: { fromRead: ${JSON.stringify(fromRead)} }, but "${fromRead}" is a `
|
|
860
|
-
+ "resultMode: 'multiple' query. A multi-record read is refused as a write target — the step compiles to "
|
|
861
|
-
+ 'a no-op and the run goes green having written nothing.',
|
|
862
|
-
suggestion: `loop the results and target each record with ${factory}({ …, record: { byId: … } })`,
|
|
863
|
-
});
|
|
864
|
-
}
|
|
865
|
-
}
|
|
768
|
+
checkRegisteredFamilies(registeredSteps, unresolvedSteps, stepNames, diags);
|
|
866
769
|
// Entry points: unique roots with valid triggers, colliding with nothing.
|
|
867
770
|
const epIds = new Set();
|
|
868
771
|
for (const ep of built.entryPoints ?? []) {
|
|
@@ -1144,6 +1047,16 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
1144
1047
|
});
|
|
1145
1048
|
continue;
|
|
1146
1049
|
}
|
|
1050
|
+
// A registered node type declares what its output is; `none` reports here,
|
|
1051
|
+
// at the same point as the per-family no-output rules below.
|
|
1052
|
+
const placed = registeredSteps.get(name);
|
|
1053
|
+
if (placed) {
|
|
1054
|
+
const contract = placed.impl.output(placed.spec);
|
|
1055
|
+
if (contract.kind === 'none') {
|
|
1056
|
+
diags.push(catalogued(placed.entry, step, contract.code, contract.message({ node: name, ref: `$vars.${name}${field ? '.' + field : ''}`, reader: step })));
|
|
1057
|
+
continue;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1147
1060
|
// A switch only routes; it has no output of its own, so any read of it is
|
|
1148
1061
|
// undefined at run time. Point the author at the arm's step instead.
|
|
1149
1062
|
if (switchNames.has(name)) {
|
|
@@ -1186,62 +1099,6 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
1186
1099
|
});
|
|
1187
1100
|
continue;
|
|
1188
1101
|
}
|
|
1189
|
-
// A Data Fabric delete is the one verb of that family whose definition
|
|
1190
|
-
// declares no outputDefinition at all — the record is gone, so there is
|
|
1191
|
-
// nothing to publish. Read the record on the step that FOUND it instead,
|
|
1192
|
-
// which on the `{ fromRead }` form is already in the flow.
|
|
1193
|
-
if (dataFabricDeletes.has(name)) {
|
|
1194
|
-
diags.push({
|
|
1195
|
-
level: 'error',
|
|
1196
|
-
code: 'DATAFABRIC_DELETE_HAS_NO_OUTPUT',
|
|
1197
|
-
message: `Delete "${name}" produces no output, but "$vars.${name}${field ? '.' + field : ''}" reads one in "${step}". `
|
|
1198
|
-
+ 'A Data Fabric delete declares no outputs — take the value off the dataFabricRead step that found the record, before the delete.',
|
|
1199
|
-
step,
|
|
1200
|
-
});
|
|
1201
|
-
continue;
|
|
1202
|
-
}
|
|
1203
|
-
if (delayNames.has(name)) {
|
|
1204
|
-
diags.push({
|
|
1205
|
-
level: 'error',
|
|
1206
|
-
code: 'DELAY_HAS_NO_OUTPUT',
|
|
1207
|
-
message: `Delay "${name}" produces no output, but "$vars.${name}${field ? '.' + field : ''}" reads one in "${step}". `
|
|
1208
|
-
+ 'A delay only waits — read the step that computed the value instead.',
|
|
1209
|
-
step,
|
|
1210
|
-
});
|
|
1211
|
-
continue;
|
|
1212
|
-
}
|
|
1213
|
-
// A `mock()` is a PLACEHOLDER: `core.logic.mock`'s definition declares one
|
|
1214
|
-
// output whose `source` is the literal `"null"`, so the value is null at run
|
|
1215
|
-
// time whatever the flow does. Unlike the four rules above, the node DOES
|
|
1216
|
-
// declare an output slot — which is exactly why the read is silent: it
|
|
1217
|
-
// resolves in the designer, it resolves in `variables.nodes`, and it hands
|
|
1218
|
-
// the downstream step a null.
|
|
1219
|
-
//
|
|
1220
|
-
// WHY THIS RULE LIVES HERE, in the authoring checker rather than in a
|
|
1221
|
-
// pass over the compiled artifact (the measurement,
|
|
1222
|
-
// convention #191). Reading a placeholder's output is the ARCHIVE's
|
|
1223
|
-
// dominant idiom, not a rarity: 55 of the 62 archived mock-carrying
|
|
1224
|
-
// artifacts do it — 110 read sites, 87 of them inside a script's body and
|
|
1225
|
-
// 19 in a decision's expression — and **39 of those 55 come from runs that
|
|
1226
|
-
// scored 1.0**. That is an author writing the downstream code for the real
|
|
1227
|
-
// node the placeholder stands in for, and a `.flow`-level rule refusing it
|
|
1228
|
-
// would refuse the deployed corpus (lesson 47). An SDK author is in the
|
|
1229
|
-
// other position: they wrote `mock()` here, in this file, and the SDK can
|
|
1230
|
-
// say at compile time that the read is dead.
|
|
1231
|
-
// Measured 2026-08-02 over 4,033 distinct archived artifacts —
|
|
1232
|
-
// runs/flow-sdk-cleanup-2026-08-02/step0/mock-output-read-sites.txt.
|
|
1233
|
-
if (mockNames.has(name)) {
|
|
1234
|
-
diags.push({
|
|
1235
|
-
level: 'error',
|
|
1236
|
-
code: 'MOCK_HAS_NO_OUTPUT',
|
|
1237
|
-
message: `Placeholder "${name}" publishes nothing, but "$vars.${name}${field ? '.' + field : ''}" reads it in "${step}". `
|
|
1238
|
-
+ 'A mock() is a marker for a node that does not exist yet — its output is null at run time, always. '
|
|
1239
|
-
+ 'Keep the placeholder while the real node does not exist, and have the reader use a value you supply instead (a flow input, as example/ClaimScaffold.flow.ts does). '
|
|
1240
|
-
+ 'Replace the placeholder only with the real node, once `registry search` finds it; a script returning fixed values is not the real node.',
|
|
1241
|
-
step,
|
|
1242
|
-
});
|
|
1243
|
-
continue;
|
|
1244
|
-
}
|
|
1245
1102
|
// A subflow hands back an OBJECT keyed by the child flow's declared
|
|
1246
1103
|
// outputs — even when the child declares exactly one. So the reference
|
|
1247
1104
|
// needs the output's NAME (`out('call', 'reversed')`), and the name has to
|
|
@@ -1985,26 +1842,6 @@ function checkTrigger(spec, diags, opts = {}, entryPointId) {
|
|
|
1985
1842
|
+ `comes round. Use a non-zero interval.`, `every: 'R/PT1H'`);
|
|
1986
1843
|
}
|
|
1987
1844
|
}
|
|
1988
|
-
/**
|
|
1989
|
-
* An EXACT ISO-8601 duration: weeks/days plus a time part, at least one
|
|
1990
|
-
* component. Copied in shape from the delay definition's own
|
|
1991
|
-
* `timerValue.pattern`, minus the years and months — see DELAY_CALENDAR below.
|
|
1992
|
-
*/
|
|
1993
|
-
const DURATION_RE = /^P(?!$)(\d+W)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?$/;
|
|
1994
|
-
/** Grammatical ISO-8601 duration, including the calendar-relative parts. */
|
|
1995
|
-
const ANY_DURATION_RE = /^P(?!$)(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?$/;
|
|
1996
|
-
/** Natural-language guesses an author might reach for, mapped to a duration. */
|
|
1997
|
-
const DURATION_WORDS = [
|
|
1998
|
-
['second', 'PT30S'], ['minute', 'PT15M'], ['hour', 'PT1H'],
|
|
1999
|
-
['day', 'P1D'], ['daily', 'P1D'], ['week', 'P1W'], ['weekly', 'P1W'],
|
|
2000
|
-
];
|
|
2001
|
-
/**
|
|
2002
|
-
* Diagnose a `delay` step's wait.
|
|
2003
|
-
*
|
|
2004
|
-
* Rung 1 is the only place an author hears about a bad duration in time to fix
|
|
2005
|
-
* it cheaply. Anything downstream speaks in terms of `timerPreset`/`timerValue`,
|
|
2006
|
-
* the two fields the serializer writes, rather than the one field the author typed.
|
|
2007
|
-
*/
|
|
2008
1845
|
/** A release key is a GUID — the shape 45/45 deployed rpa node types carry. */
|
|
2009
1846
|
/**
|
|
2010
1847
|
* The DECLARED name a single reference resolves to, or undefined.
|
|
@@ -3870,64 +3707,6 @@ function checkQueueItem(step, inputs, diags) {
|
|
|
3870
3707
|
}
|
|
3871
3708
|
/** The three priorities `core.action.queue.create[-and-wait]`'s form offers. */
|
|
3872
3709
|
const QUEUE_PRIORITIES = ['Low', 'Normal', 'High'];
|
|
3873
|
-
function checkDelay(step, inputs, diags) {
|
|
3874
|
-
const at = (code, message, suggestion) => {
|
|
3875
|
-
diags.push({ level: 'error', code, message, step, ...(suggestion ? { suggestion } : {}) });
|
|
3876
|
-
};
|
|
3877
|
-
const untilRaw = inputs?.until;
|
|
3878
|
-
if (typeof untilRaw === 'string' && untilRaw.trim() !== '') {
|
|
3879
|
-
// The absolute-date arm: an ISO-8601 date-time, e.g. 2026-09-01T09:00:00Z.
|
|
3880
|
-
const until = untilRaw.trim();
|
|
3881
|
-
if (!/^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:\d{2})?)?$/.test(until)) {
|
|
3882
|
-
at('DELAY_UNTIL_NOT_ISO', `Delay "${step}"'s until ${JSON.stringify(untilRaw)} is not an ISO-8601 date-time. `
|
|
3883
|
-
+ `Use e.g. '2026-09-01T09:00:00Z' (a timezone-less value is read in the tenant's zone).`, `until: '2026-09-01T09:00:00Z'`);
|
|
3884
|
-
}
|
|
3885
|
-
return;
|
|
3886
|
-
}
|
|
3887
|
-
const raw = inputs?.duration;
|
|
3888
|
-
if (raw === undefined || raw === null || (typeof raw === 'string' && raw.trim() === '')) {
|
|
3889
|
-
at('DELAY_NO_DURATION', `Delay "${step}" has no \`duration\`, so it carries no wait at all and the flow runs straight `
|
|
3890
|
-
+ `past it. Give it an ISO-8601 duration — ${DELAY_PRESETS.slice(0, 8).join(', ')} … are the `
|
|
3891
|
-
+ `designer's presets, and any other exact duration works too.`, `duration: 'PT15M'`);
|
|
3892
|
-
return;
|
|
3893
|
-
}
|
|
3894
|
-
const d = String(raw).trim();
|
|
3895
|
-
if (!ANY_DURATION_RE.test(d)) {
|
|
3896
|
-
at('DELAY_NOT_ISO', `Delay "${step}"'s duration ${JSON.stringify(raw)} is not an ISO-8601 duration. The format is `
|
|
3897
|
-
+ `"P" plus a date part and/or "T" plus a time part — PT30S, PT15M, PT2H, P1D, P1W. Note the `
|
|
3898
|
-
+ `"T": P1M is a MONTH, PT1M is a minute.`, nearestDuration(d));
|
|
3899
|
-
return;
|
|
3900
|
-
}
|
|
3901
|
-
if (!/[1-9]/.test(d)) {
|
|
3902
|
-
at('DELAY_ZERO', `Delay "${step}"'s duration ${JSON.stringify(d)} is zero-length, so the step waits for no time `
|
|
3903
|
-
+ `at all. Use a non-zero duration, or drop the step.`, `duration: 'PT15M'`);
|
|
3904
|
-
return;
|
|
3905
|
-
}
|
|
3906
|
-
// Grammatical, and the platform runs it — but a month is not a fixed number
|
|
3907
|
-
// of milliseconds, so NO local rung can run the flow at all.
|
|
3908
|
-
// Rejected here rather than approximated: guessing 30 days would
|
|
3909
|
-
// put a silent disagreement with the deployed engine back into the pipeline.
|
|
3910
|
-
if (!DURATION_RE.test(d)) {
|
|
3911
|
-
at('DELAY_CALENDAR', `Delay "${step}"'s duration ${JSON.stringify(d)} is calendar-relative: how long a month or a `
|
|
3912
|
-
+ `year is depends on when it starts. The platform resolves that at run time, but the local `
|
|
3913
|
-
+ `timer takes a fixed millisecond count, so this flow could not be replayed or run offline.`, `duration: 'P30D'`);
|
|
3914
|
-
}
|
|
3915
|
-
}
|
|
3916
|
-
/** Best guess at what the author meant, for a `DELAY_NOT_ISO` suggestion. */
|
|
3917
|
-
function nearestDuration(d) {
|
|
3918
|
-
// The likeliest slip: a time-part duration written without its "T".
|
|
3919
|
-
if (/^P\d+[HMS]$/.test(d) && ANY_DURATION_RE.test(`PT${d.slice(1)}`))
|
|
3920
|
-
return `duration: 'PT${d.slice(1)}'`;
|
|
3921
|
-
const lower = d.toLowerCase();
|
|
3922
|
-
const n = /(\d+)/.exec(lower)?.[1];
|
|
3923
|
-
const word = DURATION_WORDS.find(([w]) => lower.includes(w));
|
|
3924
|
-
if (!word)
|
|
3925
|
-
return undefined;
|
|
3926
|
-
if (!n)
|
|
3927
|
-
return `duration: '${word[1]}'`;
|
|
3928
|
-
const unit = word[1].replace(/\d+/, '');
|
|
3929
|
-
return `duration: '${unit.startsWith('PT') ? `PT${n}${unit.slice(2)}` : `P${n}${unit.slice(1)}`}'`;
|
|
3930
|
-
}
|
|
3931
3710
|
/** Best guess at what the author meant, for a `SCHEDULE_NOT_ISO` suggestion. */
|
|
3932
3711
|
function nearestInterval(every) {
|
|
3933
3712
|
// By far the likeliest slip: a correct duration missing the "R/" repeat marker.
|
|
@@ -4249,70 +4028,6 @@ const RETRY_COUNT_SOFT_MAX = 5;
|
|
|
4249
4028
|
* not a shorter timeout, it is a value the request node fails to configure with,
|
|
4250
4029
|
* and nothing downstream of `check` would say so in the author's own vocabulary.
|
|
4251
4030
|
*/
|
|
4252
|
-
/** The type vocabulary a script's `returns` may name — what the wire format accepts. */
|
|
4253
|
-
const SCRIPT_RETURN_TYPES = new Set(['string', 'number', 'boolean', 'object', 'array']);
|
|
4254
|
-
/**
|
|
4255
|
-
* A script step's `returns` — the author overriding what the body looks like.
|
|
4256
|
-
*
|
|
4257
|
-
* TypeScript already refuses a bad value at the call site, so this is the fence
|
|
4258
|
-
* for everything that does NOT go through the type checker: a `.flow.ts` loaded
|
|
4259
|
-
* as plain JS, a generated flow, a value read from config. Both mistakes below
|
|
4260
|
-
* reach the emitted file silently and are then the designer's problem instead of
|
|
4261
|
-
* the author's, which is precisely the failure this whole feature exists to undo.
|
|
4262
|
-
*/
|
|
4263
|
-
function checkScript(step, inputs, diags) {
|
|
4264
|
-
const returns = inputs?.returns;
|
|
4265
|
-
if (returns === undefined)
|
|
4266
|
-
return;
|
|
4267
|
-
if (typeof returns === 'string') {
|
|
4268
|
-
if (!SCRIPT_RETURN_TYPES.has(returns)) {
|
|
4269
|
-
diags.push({
|
|
4270
|
-
level: 'error',
|
|
4271
|
-
code: 'SCRIPT_RETURNS_UNKNOWN_TYPE',
|
|
4272
|
-
message: `Script step "${step}" declares \`returns: ${JSON.stringify(returns)}\`, which is not a type the `
|
|
4273
|
-
+ `wire format knows. Use one of ${[...SCRIPT_RETURN_TYPES].map((t) => `"${t}"`).join(', ')}, or a `
|
|
4274
|
-
+ `field map like \`{ total: "number" }\` to name an object's fields.`,
|
|
4275
|
-
step,
|
|
4276
|
-
});
|
|
4277
|
-
}
|
|
4278
|
-
return;
|
|
4279
|
-
}
|
|
4280
|
-
if (typeof returns !== 'object' || returns === null || Array.isArray(returns)) {
|
|
4281
|
-
diags.push({
|
|
4282
|
-
level: 'error',
|
|
4283
|
-
code: 'SCRIPT_RETURNS_UNKNOWN_TYPE',
|
|
4284
|
-
message: `Script step "${step}"'s \`returns\` is ${JSON.stringify(returns)}. It takes either a type name or a `
|
|
4285
|
-
+ `map of field names to types (\`{ total: "number" }\`).`,
|
|
4286
|
-
step,
|
|
4287
|
-
});
|
|
4288
|
-
return;
|
|
4289
|
-
}
|
|
4290
|
-
const fields = returns;
|
|
4291
|
-
// Same call as `APIWF_EMPTY_RETURNS` one surface over: an empty map declares
|
|
4292
|
-
// nothing, emits nothing, and reads exactly like omitting the key — so it is
|
|
4293
|
-
// almost always a half-finished edit rather than a statement.
|
|
4294
|
-
if (Object.keys(fields).length === 0) {
|
|
4295
|
-
diags.push({
|
|
4296
|
-
level: 'warning',
|
|
4297
|
-
code: 'SCRIPT_EMPTY_RETURNS',
|
|
4298
|
-
message: `Script step "${step}" declares \`returns: {}\`, which names no fields and emits nothing — the same `
|
|
4299
|
-
+ `as omitting it. Name the fields, or drop the key and let the body speak for itself.`,
|
|
4300
|
-
step,
|
|
4301
|
-
});
|
|
4302
|
-
return;
|
|
4303
|
-
}
|
|
4304
|
-
for (const [name, type] of Object.entries(fields)) {
|
|
4305
|
-
if (typeof type !== 'string' || !SCRIPT_RETURN_TYPES.has(type)) {
|
|
4306
|
-
diags.push({
|
|
4307
|
-
level: 'error',
|
|
4308
|
-
code: 'SCRIPT_RETURNS_UNKNOWN_TYPE',
|
|
4309
|
-
message: `Script step "${step}" declares field "${name}" as ${JSON.stringify(type)}, which is not a type the `
|
|
4310
|
-
+ `wire format knows. Use one of ${[...SCRIPT_RETURN_TYPES].map((t) => `"${t}"`).join(', ')}.`,
|
|
4311
|
-
step,
|
|
4312
|
-
});
|
|
4313
|
-
}
|
|
4314
|
-
}
|
|
4315
|
-
}
|
|
4316
4031
|
function checkHttp(step, inputs, diags) {
|
|
4317
4032
|
// `core.action.http` is retired on the platform side: after `registry pull
|
|
4318
4033
|
// --force` the tenant serves only `core.action.http.v2` (2.3), and a
|
|
@@ -4907,211 +4622,6 @@ function checkHitlRead(name, field, rest, form, step, diags) {
|
|
|
4907
4622
|
...(nearest !== undefined ? { suggestion: `out('${name}', '${nearest}')` } : {}),
|
|
4908
4623
|
});
|
|
4909
4624
|
}
|
|
4910
|
-
function checkTransform(step, inputs, diags, typeOf = new Map(), triggerId = 'start') {
|
|
4911
|
-
const at = (code, message, suggestion) => {
|
|
4912
|
-
diags.push({ level: 'error', code, message, step, ...(suggestion ? { suggestion } : {}) });
|
|
4913
|
-
};
|
|
4914
|
-
// ── collection must be a bare variable path ──
|
|
4915
|
-
const collection = inputs?.collection;
|
|
4916
|
-
if (collection === undefined || collection === null) {
|
|
4917
|
-
at('TRANSFORM_NO_COLLECTION', `Transform "${step}" has no collection. Point it at the variable or step output holding the array.`, `collection: v('catalog')`);
|
|
4918
|
-
}
|
|
4919
|
-
else if (!(collection instanceof Expr)) {
|
|
4920
|
-
// Types are stripped at run time, so `collection: [{…}]` or
|
|
4921
|
-
// `collection: 'catalog'` reaches here as a raw value.
|
|
4922
|
-
at('TRANSFORM_COLLECTION_NOT_REF', `Transform "${step}" was given a ${Array.isArray(collection) ? 'raw array' : `raw ${typeof collection}`} as its `
|
|
4923
|
-
+ `collection. That field is a variable PATH — the runtime turns the string into a lookup, so a value there `
|
|
4924
|
-
+ `resolves to nothing and the transform silently sees an empty collection.`, typeof collection === 'string' ? `collection: v('${collection}')` : `collection: v('<varName>')`);
|
|
4925
|
-
}
|
|
4926
|
-
else if (collection.literal) {
|
|
4927
|
-
at('TRANSFORM_COLLECTION_LITERAL', `Transform "${step}" has a literal collection. Inline data cannot be a path — keep the array in a variable `
|
|
4928
|
-
+ `default (\`.var('items', types.array, [...])\`) and reference it, or read an upstream step's output.`, `collection: v('items')`);
|
|
4929
|
-
}
|
|
4930
|
-
else if (!/^\$vars\.[A-Za-z0-9_]+(\.[A-Za-z0-9_]+)*$/.test(collection.js.trim())) {
|
|
4931
|
-
// A computed expression (js`…`, tmpl`…`, `.concat(…)`) is the trap that
|
|
4932
|
-
// survives every local rung: a local run resolves it to a value, so
|
|
4933
|
-
// it runs correctly there and resolves to NOTHING once deployed, where the
|
|
4934
|
-
// string is used as a lookup path.
|
|
4935
|
-
at('TRANSFORM_COLLECTION_NOT_PATH', `Transform "${step}"'s collection must be a plain reference, but it is the expression `
|
|
4936
|
-
+ `"${collection.js.trim().slice(0, 60)}". The deployed runtime uses that string as a lookup PATH, so a `
|
|
4937
|
-
+ `computed expression resolves to nothing there even though it runs locally. Compute it in a script step `
|
|
4938
|
-
+ `first, then point the transform at that step's output.`, `collection: out('<scriptStep>')`);
|
|
4939
|
-
}
|
|
4940
|
-
else {
|
|
4941
|
-
// A collection that is not an array is PASSED THROUGH untouched — no error,
|
|
4942
|
-
// no operations applied (faithful to v1). The result is a transform that
|
|
4943
|
-
// looks like it ran and changed nothing.
|
|
4944
|
-
// Only a DECLARED scalar can be judged here: a step output's type is not
|
|
4945
|
-
// known offline, so this stays quiet about those rather than guessing.
|
|
4946
|
-
const refName = declaredRefName(collection.js, triggerId);
|
|
4947
|
-
const declaredType = refName ? typeOf.get(refName) : undefined;
|
|
4948
|
-
if (declaredType && ['string', 'number', 'boolean'].includes(declaredType)) {
|
|
4949
|
-
at('TRANSFORM_COLLECTION_SCALAR', `Transform "${step}"'s collection is "${refName}", declared as ${declaredType}. A transform runs its `
|
|
4950
|
-
+ `operations over an ARRAY; anything else is passed through untouched — no error, nothing applied — so `
|
|
4951
|
-
+ `the step would look like it ran and change nothing.`, `Declare it as types.array, or point the collection at the array field: collection: out('<step>', '<field>')`);
|
|
4952
|
-
}
|
|
4953
|
-
}
|
|
4954
|
-
// ── the operation chain ──
|
|
4955
|
-
const ops = inputs?.operations;
|
|
4956
|
-
if (!Array.isArray(ops) || ops.length === 0) {
|
|
4957
|
-
at('TRANSFORM_NO_OPERATIONS', `Transform "${step}" has no operations, so it passes its collection straight through. Add at least one `
|
|
4958
|
-
+ `filter / map / groupBy operation, or drop the node.`, `operations: [{ type: 'filter', filters: [{ field: 'status', condition: 'equals', value: 'open' }] }]`);
|
|
4959
|
-
return;
|
|
4960
|
-
}
|
|
4961
|
-
checkVariant(step, inputs, ops, at);
|
|
4962
|
-
ops.forEach((op, i) => {
|
|
4963
|
-
const where = `operations[${i}]`;
|
|
4964
|
-
// Read the discriminant before the narrowing below exhausts the union — at
|
|
4965
|
-
// run time it can be anything (an author typo survives type erasure).
|
|
4966
|
-
const kind = op?.type;
|
|
4967
|
-
if (op?.type === 'filter') {
|
|
4968
|
-
if (!Array.isArray(op.filters) || op.filters.length === 0) {
|
|
4969
|
-
at('TRANSFORM_EMPTY_FILTER', `Transform "${step}" ${where} is a filter with no rules — the runtime passes every item through it.`);
|
|
4970
|
-
return;
|
|
4971
|
-
}
|
|
4972
|
-
for (const [j, f] of op.filters.entries()) {
|
|
4973
|
-
if (!f?.field && f?.field !== '')
|
|
4974
|
-
at('TRANSFORM_FILTER_NO_FIELD', `Transform "${step}" ${where}.filters[${j}] has no field, so the rule is dropped and the filter `
|
|
4975
|
-
+ `matches everything.`);
|
|
4976
|
-
checkVocab(step, `${where}.filters[${j}].condition`, f?.condition, FILTER_CONDITIONS, 'filter condition', 'passes every item through the rule', diags);
|
|
4977
|
-
if (typeof f?.value === 'string' && LOOKS_LIKE_EXPRESSION.test(f.value.trim()))
|
|
4978
|
-
at('TRANSFORM_FILTER_VALUE_EXPR', `Transform "${step}" ${where}.filters[${j}] compares against ${JSON.stringify(f.value)}. Filter values `
|
|
4979
|
-
+ `are LITERAL only — the runtime evaluates nothing here, so every item would be compared against that `
|
|
4980
|
-
+ `text and the filter would return nothing. Use a literal, or do the comparison in a script step.`, `value: 500`);
|
|
4981
|
-
// `contains` is TWO operators wearing one name — the runtime does
|
|
4982
|
-
// `Array.includes` when the FIELD holds an array and `String.includes`
|
|
4983
|
-
// otherwise, faithful to v1. Which
|
|
4984
|
-
// one runs is decided by the data, so nothing static can pick for you —
|
|
4985
|
-
// but an ARRAY on the compare side is wrong under both readings
|
|
4986
|
-
// (`['a','b']` is stringified to `a,b` for the substring test, and
|
|
4987
|
-
// `includes` looks for the array itself as one element), and it is the
|
|
4988
|
-
// shape an author reaches for when they mean "any of these".
|
|
4989
|
-
if ((f?.condition === 'contains' || f?.condition === 'not_contains') && Array.isArray(f?.value))
|
|
4990
|
-
at('TRANSFORM_CONTAINS_ARRAY_VALUE', `Transform "${step}" ${where}.filters[${j}] uses "${f.condition}" against an ARRAY. "contains" compares `
|
|
4991
|
-
+ `ONE value: if the item's field holds an array it tests membership of that value, otherwise it tests `
|
|
4992
|
-
+ `for that value as a substring — either way an array on the right matches nothing, silently. For `
|
|
4993
|
-
+ `"any of these", use one rule per value with the filter's "or" operation.`, `condition: 'contains', value: ${JSON.stringify(f.value[0] ?? 'x')}`);
|
|
4994
|
-
}
|
|
4995
|
-
return;
|
|
4996
|
-
}
|
|
4997
|
-
if (op?.type === 'map') {
|
|
4998
|
-
if (!Array.isArray(op.mappings) || op.mappings.length === 0) {
|
|
4999
|
-
at('TRANSFORM_EMPTY_MAP', `Transform "${step}" ${where} is a map with no mappings — with keepOriginalFields off that turns every `
|
|
5000
|
-
+ `item into {}.`);
|
|
5001
|
-
return;
|
|
5002
|
-
}
|
|
5003
|
-
for (const [j, m] of op.mappings.entries()) {
|
|
5004
|
-
if (!m?.field)
|
|
5005
|
-
at('TRANSFORM_MAP_NO_FIELD', `Transform "${step}" ${where}.mappings[${j}] has no field, so nothing is carried into the output.`);
|
|
5006
|
-
// Omitted is fine — it defaults to `copy`.
|
|
5007
|
-
if (m?.transformation !== undefined)
|
|
5008
|
-
checkVocab(step, `${where}.mappings[${j}].transformation`, m.transformation, TRANSFORMATIONS, 'transformation', 'copies the value unchanged', diags);
|
|
5009
|
-
}
|
|
5010
|
-
return;
|
|
5011
|
-
}
|
|
5012
|
-
if (op?.type === 'groupBy') {
|
|
5013
|
-
if (!op.groupByField)
|
|
5014
|
-
at('TRANSFORM_NO_GROUP_FIELD', `Transform "${step}" ${where} is a groupBy with no groupByField, so every item lands in one anonymous `
|
|
5015
|
-
+ `group.`);
|
|
5016
|
-
for (const [j, a] of (op.aggregations ?? []).entries()) {
|
|
5017
|
-
if (!a?.alias?.trim())
|
|
5018
|
-
at('TRANSFORM_AGG_NO_ALIAS', `Transform "${step}" ${where}.aggregations[${j}] has no alias. The runtime drops an unaliased `
|
|
5019
|
-
+ `aggregation, so its result would simply be absent from the output.`);
|
|
5020
|
-
if (a?.operation !== 'count' && !a?.field?.trim())
|
|
5021
|
-
at('TRANSFORM_AGG_NO_FIELD', `Transform "${step}" ${where}.aggregations[${j}] is "${a?.operation}" with no field. Only "count" `
|
|
5022
|
-
+ `works without one; the runtime drops the rest, so the aggregate would be missing from the output.`);
|
|
5023
|
-
checkVocab(step, `${where}.aggregations[${j}].operation`, a?.operation, AGGREGATION_OPERATIONS, 'aggregation operation', 'yields null for that aggregate', diags);
|
|
5024
|
-
}
|
|
5025
|
-
return;
|
|
5026
|
-
}
|
|
5027
|
-
at('TRANSFORM_UNKNOWN_OP', `Transform "${step}" ${where} has type ${JSON.stringify(kind)}. The runtime skips an operation it does `
|
|
5028
|
-
+ `not recognize, so the collection would pass through unchanged. Use 'filter', 'map', or 'groupBy'.`);
|
|
5029
|
-
});
|
|
5030
|
-
}
|
|
5031
|
-
/**
|
|
5032
|
-
* Diagnose the `variant` discriminator on a transform step.
|
|
5033
|
-
*
|
|
5034
|
-
* A variant emits a single-purpose node type (`core.action.transform.filter`,
|
|
5035
|
-
* `.map`, `.group-by`) instead of the generic one. The platform runs all four
|
|
5036
|
-
* node types through the SAME script generators and dispatches on the
|
|
5037
|
-
* OPERATION's type, never the node's — so a mismatch here does not fault and
|
|
5038
|
-
* does not change the answer. It produces a node that lies: a `.filter` node
|
|
5039
|
-
* that actually maps, whose operation the designer's filter editor cannot even
|
|
5040
|
-
* show. That is exactly the class this SDK catches at rung 1, because no rung
|
|
5041
|
-
* below it ever will.
|
|
5042
|
-
*/
|
|
5043
|
-
function checkVariant(step, inputs, ops, at) {
|
|
5044
|
-
const variant = inputs?.variant;
|
|
5045
|
-
if (variant === undefined || variant === null)
|
|
5046
|
-
return; // generic — chains freely
|
|
5047
|
-
const variants = Object.keys(VARIANT_OPERATION);
|
|
5048
|
-
if (typeof variant !== 'string' || !variants.includes(variant)) {
|
|
5049
|
-
at('TRANSFORM_VARIANT_UNKNOWN', `Transform "${step}" has variant ${JSON.stringify(variant)}, which is not a Transform node type. `
|
|
5050
|
-
+ `The emitted node would be "core.action.transform.${String(variant)}", which no runtime resolves. `
|
|
5051
|
-
+ `Valid: ${variants.join(', ')} — or omit \`variant\` for the generic chaining Transform.`, nearestIn(variant, variants));
|
|
5052
|
-
return;
|
|
5053
|
-
}
|
|
5054
|
-
const expected = VARIANT_OPERATION[variant];
|
|
5055
|
-
if (ops.length !== 1) {
|
|
5056
|
-
at('TRANSFORM_VARIANT_ONE_OP', `Transform "${step}" is the "${variant}" variant but carries ${ops.length} operations. A variant node runs `
|
|
5057
|
-
+ `exactly one — the designer's ${variant} editor shows a single ${expected} operation and nothing else. `
|
|
5058
|
-
+ `Drop \`variant\` to chain them in one generic Transform, or split them across one node per step.`);
|
|
5059
|
-
return;
|
|
5060
|
-
}
|
|
5061
|
-
const got = ops[0]?.type;
|
|
5062
|
-
if (got !== expected) {
|
|
5063
|
-
at('TRANSFORM_VARIANT_OP_MISMATCH', `Transform "${step}" is the "${variant}" variant but its operation is ${JSON.stringify(got)}, not `
|
|
5064
|
-
+ `"${expected}". The node type does not drive the runtime — the OPERATION does — so this would emit a `
|
|
5065
|
-
+ `node labelled "${variant}" that actually does ${JSON.stringify(got)}, and the designer's ${variant} `
|
|
5066
|
-
+ `editor could not show it. Match them, or drop \`variant\` for the generic Transform.`, typeof got === 'string' && Object.values(VARIANT_OPERATION).includes(got)
|
|
5067
|
-
? `variant: '${variants.find((v) => VARIANT_OPERATION[v] === got)}'`
|
|
5068
|
-
: `type: '${expected}'`);
|
|
5069
|
-
}
|
|
5070
|
-
}
|
|
5071
|
-
/** Nearest member of a closed vocabulary, for a "did you mean" suggestion. */
|
|
5072
|
-
function nearestIn(value, vocab) {
|
|
5073
|
-
const v = typeof value === 'string' ? value.toLowerCase() : '';
|
|
5074
|
-
return vocab.find((c) => c.replace(/[-_]/g, '') === v.replace(/[^a-z]/g, ''))
|
|
5075
|
-
?? vocab.find((c) => c.startsWith(v) || (v !== '' && v.startsWith(c)));
|
|
5076
|
-
}
|
|
5077
|
-
/** A string that is an expression the transform runtime will NOT evaluate. */
|
|
5078
|
-
const LOOKS_LIKE_EXPRESSION = /^(=js:|\$vars\.|\{\s*\$vars\.)/;
|
|
5079
|
-
// The three closed vocabularies a transform operation carries. TypeScript types
|
|
5080
|
-
// them, but an authored `.flow.ts` runs with types ERASED — so `condition:
|
|
5081
|
-
// 'greater'` reaches the runtime, whose generated script falls through on a name
|
|
5082
|
-
// it does not know and is silent about it:
|
|
5083
|
-
// condition → the rule passes every item
|
|
5084
|
-
// transformation → the value is copied unchanged
|
|
5085
|
-
// aggregation → the aggregate is null
|
|
5086
|
-
// Green at every rung, wrong answer. Checked here so the author hears it at
|
|
5087
|
-
// rung 1 — a flow that never went through this SDK gets no such warning.
|
|
5088
|
-
const FILTER_CONDITIONS = [
|
|
5089
|
-
'equals', 'not_equals',
|
|
5090
|
-
'greater_than', 'less_than', 'greater_equal', 'less_equal',
|
|
5091
|
-
'contains', 'not_contains',
|
|
5092
|
-
'starts_with', 'not_starts_with',
|
|
5093
|
-
'ends_with', 'not_ends_with',
|
|
5094
|
-
'matches_regex',
|
|
5095
|
-
'in', 'not_in',
|
|
5096
|
-
'is_null', 'is_not_null',
|
|
5097
|
-
'is_empty', 'is_not_empty',
|
|
5098
|
-
];
|
|
5099
|
-
const TRANSFORMATIONS = ['copy', 'uppercase', 'lowercase', 'trim'];
|
|
5100
|
-
const AGGREGATION_OPERATIONS = ['count', 'sum', 'average', 'min', 'max', 'first', 'last', 'collect'];
|
|
5101
|
-
/** Flag a value that is not a member of a closed vocabulary, suggesting the nearest. */
|
|
5102
|
-
function checkVocab(step, at, value, vocab, what, effect, diags) {
|
|
5103
|
-
if (typeof value === 'string' && vocab.includes(value))
|
|
5104
|
-
return;
|
|
5105
|
-
const near = nearestIn(value, vocab);
|
|
5106
|
-
diags.push({
|
|
5107
|
-
level: 'error',
|
|
5108
|
-
code: 'TRANSFORM_BAD_VOCAB',
|
|
5109
|
-
message: `Transform "${step}" ${at} is ${JSON.stringify(value)}, which is not a valid ${what}. The runtime does not `
|
|
5110
|
-
+ `recognize it and ${effect}, so the result would be silently wrong. Valid: ${vocab.join(', ')}.`,
|
|
5111
|
-
step,
|
|
5112
|
-
...(near ? { suggestion: near } : {}),
|
|
5113
|
-
});
|
|
5114
|
-
}
|
|
5115
4625
|
/**
|
|
5116
4626
|
* Flag a case value whose type cannot match the discriminant's declared type.
|
|
5117
4627
|
*
|
|
@@ -5174,100 +4684,116 @@ const LOOP_FIELDS = new Set(['currentItem', 'currentIteration', 'collection', 'o
|
|
|
5174
4684
|
* before the 2.4 default (#868); its own fields are not audited here.
|
|
5175
4685
|
*/
|
|
5176
4686
|
const LOOP_FIELDS_1_0_0 = new Set(['currentItem', 'currentIndex', 'collection', 'output', 'error']);
|
|
5177
|
-
/**
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
* guard: a hand-authored `Id` row satisfies the node's own "at least one field"
|
|
5182
|
-
* rule, ships in the body, and is rejected — and the rejection is only logged,
|
|
5183
|
-
* so the row comes back unchanged and the run goes green.
|
|
5184
|
-
*/
|
|
5185
|
-
const DATAFABRIC_SYSTEM_COLUMNS = new Set(['Id', 'CreateTime', 'CreatedBy', 'UpdateTime', 'UpdatedBy']);
|
|
5186
|
-
function checkDataFabricColumns(step, columns, option, diags) {
|
|
5187
|
-
const system = columns.filter((c) => DATAFABRIC_SYSTEM_COLUMNS.has(c));
|
|
5188
|
-
if (system.length) {
|
|
5189
|
-
diags.push({
|
|
5190
|
-
level: 'error', code: 'DATAFABRIC_SYSTEM_COLUMN', step,
|
|
5191
|
-
message: `"${step}" writes ${system.map((c) => JSON.stringify(c)).join(', ')} in { ${option} }. `
|
|
5192
|
-
+ 'Data Fabric assigns its system columns (Id, CreateTime, CreatedBy, UpdateTime, UpdatedBy) and rejects a '
|
|
5193
|
-
+ 'write to them — and it only LOGS the rejection, so the row comes back unchanged and the run goes green.',
|
|
5194
|
-
});
|
|
5195
|
-
}
|
|
4687
|
+
/** The registered node type for a spec's kind, or undefined (a custom FlowAction has no kind). */
|
|
4688
|
+
function registeredKind(spec) {
|
|
4689
|
+
const kind = spec?.kind;
|
|
4690
|
+
return typeof kind === 'string' ? FLOW_NODE_TYPES.byKind(kind, spec) : undefined;
|
|
5196
4691
|
}
|
|
5197
4692
|
/**
|
|
5198
|
-
*
|
|
4693
|
+
* Check one step whose kind is registered: its version, its node-local rules
|
|
4694
|
+
* (reported through the family's catalog), and the expressions it carries.
|
|
4695
|
+
* Returns its placement, or undefined when the version cannot be resolved — the
|
|
4696
|
+
* step is then reported and not checked further.
|
|
5199
4697
|
*
|
|
5200
|
-
*
|
|
5201
|
-
*
|
|
5202
|
-
*
|
|
5203
|
-
*
|
|
5204
|
-
* to
|
|
4698
|
+
* Its bindings are not checked here: unlike a contribution (`checkContribution`),
|
|
4699
|
+
* a registered type's bindings exist only once it serializes. Closing every
|
|
4700
|
+
* `<bindings.NAME>` its definition references is the node type's own contract,
|
|
4701
|
+
* which serialize enforces (#942) and the type's tests pin; an author cannot
|
|
4702
|
+
* cause the gap, so there is no author diagnostic to report.
|
|
5205
4703
|
*/
|
|
5206
|
-
function
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
4704
|
+
function checkRegisteredStep(entry, s, exprs, errorSource, opts, diags, typeOf, triggerId) {
|
|
4705
|
+
const spec = s.spec;
|
|
4706
|
+
/** The step's node-local rules and expressions, as `impl` defines them. */
|
|
4707
|
+
const runRules = (impl) => {
|
|
4708
|
+
const ctx = {
|
|
4709
|
+
step: s.name,
|
|
4710
|
+
version: impl.version,
|
|
4711
|
+
report: (code, message, suggestion) => diags.push(catalogued(entry, s.name, code, message, suggestion)),
|
|
4712
|
+
declaredRef: (js) => {
|
|
4713
|
+
const name = declaredRefName(js, triggerId);
|
|
4714
|
+
return name ? { name, type: typeOf.get(name) } : undefined;
|
|
4715
|
+
},
|
|
4716
|
+
options: opts,
|
|
4717
|
+
};
|
|
4718
|
+
impl.check?.(spec, ctx);
|
|
4719
|
+
for (const site of impl.expressions(spec)) {
|
|
4720
|
+
if ('code' in site) {
|
|
4721
|
+
// A script body: checked for its `$vars.` refs although it is a string. No
|
|
4722
|
+
// errorSource, as for `script()` (collectActionExprs).
|
|
4723
|
+
exprs.push({ js: site.code, step: s.name });
|
|
4724
|
+
}
|
|
4725
|
+
else {
|
|
4726
|
+
pushDeepExprs(site.value, s.name, errorSource, exprs);
|
|
4727
|
+
}
|
|
4728
|
+
}
|
|
4729
|
+
return ctx;
|
|
4730
|
+
};
|
|
4731
|
+
let impl;
|
|
4732
|
+
try {
|
|
4733
|
+
impl = resolveVersion(entry.type, spec, s.options?.version, `step "${s.name}"`);
|
|
5216
4734
|
}
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
4735
|
+
catch (e) {
|
|
4736
|
+
// A select() that picks an unregistered version is the node type's bug, not the author's pin.
|
|
4737
|
+
if (isInternalSdkError(e))
|
|
4738
|
+
throw e;
|
|
4739
|
+
diags.push({ level: 'error', code: 'NODE_VERSION_UNREGISTERED', step: s.name, message: e.message });
|
|
4740
|
+
// Still report the step's other mistakes in this same pass, with the version it
|
|
4741
|
+
// would get unpinned (select(), else the default) — as decompile lifts an
|
|
4742
|
+
// unregistered version — so fixing the pin is not followed by a second round.
|
|
4743
|
+
// It stays unplaced: the pin is wrong, so no family rule may rely on it.
|
|
4744
|
+
runRules(resolveVersion(entry.type, spec, undefined, `step "${s.name}"`));
|
|
4745
|
+
return undefined;
|
|
5223
4746
|
}
|
|
4747
|
+
const ctx = runRules(impl);
|
|
4748
|
+
return { entry, impl, spec, placement: { step: s.name, kind: impl.kind, spec, version: impl.version }, ctx };
|
|
5224
4749
|
}
|
|
5225
|
-
/**
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
function checkDataFabricQuery(step, inputs, diags) {
|
|
5231
|
-
const listOnly = ['limit', 'skip', 'sort', 'fields'].filter((k) => inputs[k] !== undefined);
|
|
5232
|
-
if (listOnly.length && inputs.resultMode !== 'multiple') {
|
|
5233
|
-
diags.push({
|
|
5234
|
-
level: 'error', code: 'DATAFABRIC_QUERY_OPTS_WITHOUT_MULTIPLE', step,
|
|
5235
|
-
message: `"${step}" sets { ${listOnly.join(', ')} }, which only a list query uses, but its resultMode is `
|
|
5236
|
-
+ `${inputs.resultMode ? JSON.stringify(inputs.resultMode) : 'unset (a single-record read)'}.`,
|
|
5237
|
-
suggestion: "resultMode: 'multiple'",
|
|
5238
|
-
});
|
|
4750
|
+
/** Every non-literal Expr reachable in `value`, as expressions to check. */
|
|
4751
|
+
function pushDeepExprs(value, step, errorSource, out) {
|
|
4752
|
+
if (value instanceof Expr) {
|
|
4753
|
+
if (!value.literal)
|
|
4754
|
+
out.push({ js: value.js, step, errorSource });
|
|
5239
4755
|
}
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
diags.push({
|
|
5244
|
-
level: 'error', code: 'DATAFABRIC_LIMIT_RANGE', step,
|
|
5245
|
-
message: `"${step}": limit must be a whole number between 1 and 1000 (got ${inputs.limit}). 1000 is a hard `
|
|
5246
|
-
+ 'ceiling — a larger value truncates silently, so page with { skip } instead.',
|
|
5247
|
-
});
|
|
4756
|
+
else if (Array.isArray(value)) {
|
|
4757
|
+
for (const v of value)
|
|
4758
|
+
pushDeepExprs(v, step, errorSource, out);
|
|
5248
4759
|
}
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
diags.push({
|
|
5253
|
-
level: 'error', code: 'DATAFABRIC_IN_REQUIRES_MULTIPLE', step,
|
|
5254
|
-
message: `"${step}" filters with the 'in' operator, which is honoured only when resultMode is 'multiple'. `
|
|
5255
|
-
+ 'On a single-record read that row compiles away — and a read whose filters compiled away matches every record.',
|
|
5256
|
-
});
|
|
4760
|
+
else if (value && typeof value === 'object') {
|
|
4761
|
+
for (const v of Object.values(value))
|
|
4762
|
+
pushDeepExprs(v, step, errorSource, out);
|
|
5257
4763
|
}
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
4764
|
+
}
|
|
4765
|
+
/** Whether `e` is one of the SDK's own `internal SDK error:` throws, which `check` never turns into a diagnostic. */
|
|
4766
|
+
function isInternalSdkError(e) {
|
|
4767
|
+
return e instanceof Error && e.message.startsWith('internal SDK error:');
|
|
4768
|
+
}
|
|
4769
|
+
/** A diagnostic a registered node type reports, at its family catalog's severity. */
|
|
4770
|
+
function catalogued(entry, step, code, message, suggestion) {
|
|
4771
|
+
const known = Object.hasOwn(entry.family.diagnostics, code) ? entry.family.diagnostics[code] : undefined;
|
|
4772
|
+
if (!known) {
|
|
4773
|
+
throw new Error(`internal SDK error: node family "${entry.family.id}" reports ${code}, which its diagnostics catalog does not list.`);
|
|
4774
|
+
}
|
|
4775
|
+
// Key order as the per-family rules always wrote it — `check --json` prints it.
|
|
4776
|
+
return { level: known.severity, code, step, message, ...(suggestion !== undefined ? { suggestion } : {}) };
|
|
4777
|
+
}
|
|
4778
|
+
/** Run each registered family's own rules over all of its placements in this flow. */
|
|
4779
|
+
function checkRegisteredFamilies(steps, unresolved, stepNames, diags) {
|
|
4780
|
+
const byFamily = new Map();
|
|
4781
|
+
for (const placed of steps.values()) {
|
|
4782
|
+
const list = byFamily.get(placed.entry.family.id) ?? [];
|
|
4783
|
+
list.push(placed);
|
|
4784
|
+
byFamily.set(placed.entry.family.id, list);
|
|
4785
|
+
}
|
|
4786
|
+
for (const placed of byFamily.values()) {
|
|
4787
|
+
const family = placed[0].entry.family;
|
|
4788
|
+
if (!family.check)
|
|
4789
|
+
continue;
|
|
4790
|
+
const ctx = {
|
|
4791
|
+
report: (step, code, message, suggestion) => diags.push(catalogued(placed[0].entry, step, code, message, suggestion)),
|
|
4792
|
+
hasStep: (step) => stepNames.has(step),
|
|
4793
|
+
placementOf: (step) => steps.get(step)?.placement,
|
|
4794
|
+
versionUnresolved: (step) => unresolved.has(step),
|
|
4795
|
+
};
|
|
4796
|
+
family.check(placed.map((p) => p.placement), ctx);
|
|
5271
4797
|
}
|
|
5272
4798
|
}
|
|
5273
4799
|
function collectActionExprs(s, out, errorSource) {
|
|
@@ -5294,20 +4820,6 @@ function collectActionExprs(s, out, errorSource) {
|
|
|
5294
4820
|
push(val);
|
|
5295
4821
|
return;
|
|
5296
4822
|
}
|
|
5297
|
-
if (s.spec.kind === 'script') {
|
|
5298
|
-
// A script body is a plain STRING, not an Expr — so it never went through
|
|
5299
|
-
// this check, and a bad `$vars.…` ref inside one used to surface only rungs
|
|
5300
|
-
// later, as a type error about generated code (flow-builder-sdk#20). The body
|
|
5301
|
-
// isn't parsed; the `$vars.<name>` refs it contains are checked like any other.
|
|
5302
|
-
out.push({ js: s.spec.inputs.code, step: s.name });
|
|
5303
|
-
return;
|
|
5304
|
-
}
|
|
5305
|
-
if (s.spec.kind === 'transform') {
|
|
5306
|
-
// The only expression a transform carries is its collection; everything
|
|
5307
|
-
// else is a literal or a field name (see checkTransform).
|
|
5308
|
-
push(s.spec.inputs.collection);
|
|
5309
|
-
return;
|
|
5310
|
-
}
|
|
5311
4823
|
if (s.spec.kind === 'hitl') {
|
|
5312
4824
|
// A human task's only expressions are the values its shown fields display.
|
|
5313
4825
|
// Without this a typo'd reference inside a form field would reach the
|
|
@@ -5326,30 +4838,6 @@ function collectActionExprs(s, out, errorSource) {
|
|
|
5326
4838
|
push(val);
|
|
5327
4839
|
return;
|
|
5328
4840
|
}
|
|
5329
|
-
if (s.spec.kind === 'dataFabricRead' || s.spec.kind === 'dataFabricCreate'
|
|
5330
|
-
|| s.spec.kind === 'dataFabricUpdate' || s.spec.kind === 'dataFabricDelete') {
|
|
5331
|
-
// Every value a Data Fabric step carries is authored against flow scope: a
|
|
5332
|
-
// filter's comparison value, the record id, the columns written. The family
|
|
5333
|
-
// was missing here entirely, so a `$vars.` typo in any of them travelled to
|
|
5334
|
-
// the platform unchecked — and the delete verb's no-output rule below has
|
|
5335
|
-
// nothing to fire on until these are collected.
|
|
5336
|
-
if (s.spec.kind === 'dataFabricRead') {
|
|
5337
|
-
for (const f of s.spec.inputs.filters ?? [])
|
|
5338
|
-
push(f.value);
|
|
5339
|
-
return;
|
|
5340
|
-
}
|
|
5341
|
-
if (s.spec.kind === 'dataFabricCreate') {
|
|
5342
|
-
for (const val of Object.values(s.spec.inputs.values ?? {}))
|
|
5343
|
-
push(val);
|
|
5344
|
-
return;
|
|
5345
|
-
}
|
|
5346
|
-
push(s.spec.inputs.record?.byId);
|
|
5347
|
-
if (s.spec.kind === 'dataFabricUpdate') {
|
|
5348
|
-
for (const val of Object.values(s.spec.inputs.set ?? {}))
|
|
5349
|
-
push(val);
|
|
5350
|
-
}
|
|
5351
|
-
return;
|
|
5352
|
-
}
|
|
5353
4841
|
if (s.spec.kind === 'connector') {
|
|
5354
4842
|
// Same gap on the connector surface: `messageToSend: tmpl`…${v('typo')}``
|
|
5355
4843
|
// reached the runtime unchecked.
|
|
@@ -5376,6 +4864,17 @@ function collectActionExprs(s, out, errorSource) {
|
|
|
5376
4864
|
const PER_TENANT_ERROR_PORT_KINDS = new Set([
|
|
5377
4865
|
'connector', 'rpaWorkflow', 'apiWorkflow', 'agenticProcess', 'agent', 'ixpExtract',
|
|
5378
4866
|
]);
|
|
4867
|
+
/**
|
|
4868
|
+
* The ONERROR_NO_ERROR_PORT sentence for a typed step — a built-in family or a
|
|
4869
|
+
* registered node type — whose node type has no error port. One spelling for
|
|
4870
|
+
* both, so porting a family onto the registry changes no message.
|
|
4871
|
+
*/
|
|
4872
|
+
function typedNoErrorPortMessage(stepName, kind, def) {
|
|
4873
|
+
const nodeType = def ? ` (${def.nodeType}@${def.version})` : '';
|
|
4874
|
+
return `Step "${stepName}" is a ${kind} step${nodeType}, and that node type has no error port — its definition does not `
|
|
4875
|
+
+ `set supportsErrorHandling, so the edge would leave a handle the node does not have. Handle the failure `
|
|
4876
|
+
+ `downstream instead (a delay cannot fail; a wait resumes or does not).`;
|
|
4877
|
+
}
|
|
5379
4878
|
/** The five fields the platform's error envelope declares — no more, no less. */
|
|
5380
4879
|
// The five the definitions declare, plus the two every probed runtime envelope
|
|
5381
4880
|
// carried anyway (`response`, `element`) — see ErrorEnvelopeField in core/expr.ts.
|
|
@@ -5385,7 +4884,9 @@ const ERROR_ENVELOPE_FIELDS = ['code', 'message', 'detail', 'category', 'status'
|
|
|
5385
4884
|
* mistakes at build time (no previous step, two handlers on one node, an empty
|
|
5386
4885
|
* handler); these are the ones only a checker can see.
|
|
5387
4886
|
*/
|
|
5388
|
-
function checkOnError(_handler, step, diags
|
|
4887
|
+
function checkOnError(_handler, step, diags,
|
|
4888
|
+
/** The step's registry record, when its kind is registered and its version resolved. */
|
|
4889
|
+
placed) {
|
|
5389
4890
|
// The builder refuses a handler with no eligible step before it, so a missing
|
|
5390
4891
|
// source here means a hand-built `BuiltFlow`; there is nothing to say about it.
|
|
5391
4892
|
if (!step)
|
|
@@ -5439,6 +4940,35 @@ function checkOnError(_handler, step, diags) {
|
|
|
5439
4940
|
return;
|
|
5440
4941
|
}
|
|
5441
4942
|
const spec = step.spec;
|
|
4943
|
+
// A registered node type: a bundled one asks its definition at the version it
|
|
4944
|
+
// will be emitted at; a per-tenant one answers for itself (its errorPort).
|
|
4945
|
+
if (registeredKind(spec)) {
|
|
4946
|
+
// No record: the version did not resolve, which NODE_VERSION_UNREGISTERED
|
|
4947
|
+
// already reports where the step is checked.
|
|
4948
|
+
if (!placed)
|
|
4949
|
+
return;
|
|
4950
|
+
const { impl } = placed;
|
|
4951
|
+
if (impl.definition === 'per-tenant') {
|
|
4952
|
+
const port = typeof impl.errorPort === 'function' ? impl.errorPort(spec) : impl.errorPort === true;
|
|
4953
|
+
if (!port) {
|
|
4954
|
+
diags.push({
|
|
4955
|
+
level: 'error', code: 'ONERROR_NO_ERROR_PORT', step: step.name,
|
|
4956
|
+
message: declaredErrorPortRefusal(step.name, impl.typeName, impl.version),
|
|
4957
|
+
});
|
|
4958
|
+
return;
|
|
4959
|
+
}
|
|
4960
|
+
}
|
|
4961
|
+
else {
|
|
4962
|
+
const def = bundledDefinition(impl.typeName, impl.version);
|
|
4963
|
+
if (def !== undefined && !definitionHasErrorPort(def)) {
|
|
4964
|
+
diags.push({ level: 'error', code: 'ONERROR_NO_ERROR_PORT', message: typedNoErrorPortMessage(step.name, impl.kind, def), step: step.name });
|
|
4965
|
+
return;
|
|
4966
|
+
}
|
|
4967
|
+
}
|
|
4968
|
+
// The port exists; the node type may still refuse the handler (plain HTTP's rule, once ported).
|
|
4969
|
+
impl.checkErrorHandler?.(placed.spec, placed.ctx);
|
|
4970
|
+
return;
|
|
4971
|
+
}
|
|
5442
4972
|
// A typed family with a bundled definition: ask the definition serialize will
|
|
5443
4973
|
// emit, at the version it will emit (#1011). For instance `voiceAgent()`
|
|
5444
4974
|
// defaults to uipath.agent.voice@1.0 (no error port) and `{ version: '1.1' }`
|
|
@@ -5451,13 +4981,10 @@ function checkOnError(_handler, step, diags) {
|
|
|
5451
4981
|
? bundledDef !== undefined && !definitionHasErrorPort(bundledDef)
|
|
5452
4982
|
: !PER_TENANT_ERROR_PORT_KINDS.has(spec.kind);
|
|
5453
4983
|
if (noPort) {
|
|
5454
|
-
const nodeType = bundledDef ? ` (${bundledDef.nodeType}@${bundledDef.version})` : '';
|
|
5455
4984
|
diags.push({
|
|
5456
4985
|
level: 'error',
|
|
5457
4986
|
code: 'ONERROR_NO_ERROR_PORT',
|
|
5458
|
-
message:
|
|
5459
|
-
+ `set supportsErrorHandling, so the edge would leave a handle the node does not have. Handle the failure `
|
|
5460
|
-
+ `downstream instead (a delay cannot fail; a wait resumes or does not).`,
|
|
4987
|
+
message: typedNoErrorPortMessage(step.name, spec.kind, bundledDef),
|
|
5461
4988
|
step: step.name,
|
|
5462
4989
|
});
|
|
5463
4990
|
return;
|