@uipath/maestro-builder-sdk 5.2.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/LICENSE +21 -0
- package/README.md +322 -0
- package/dist/bindings-v2.d.ts +37 -0
- package/dist/bindings-v2.js +155 -0
- package/dist/bindings.d.ts +6 -0
- package/dist/bindings.js +6 -0
- package/dist/bpmn/bpmn-expr-check.d.ts +41 -0
- package/dist/bpmn/bpmn-expr-check.js +218 -0
- package/dist/bpmn/bpmn-sdk.d.ts +2257 -0
- package/dist/bpmn/bpmn-sdk.js +1476 -0
- package/dist/bpmn/check-cli.d.ts +3 -0
- package/dist/bpmn/check-cli.js +32 -0
- package/dist/bpmn/check.d.ts +25 -0
- package/dist/bpmn/check.js +430 -0
- package/dist/bpmn/compile-cli.d.ts +3 -0
- package/dist/bpmn/compile-cli.js +60 -0
- package/dist/bpmn/connector.d.ts +56 -0
- package/dist/bpmn/connector.js +199 -0
- package/dist/bpmn/decompile-cli.d.ts +8 -0
- package/dist/bpmn/decompile-cli.js +54 -0
- package/dist/bpmn/decompile.d.ts +6 -0
- package/dist/bpmn/decompile.js +1108 -0
- package/dist/bpmn/format-profile.d.ts +27 -0
- package/dist/bpmn/format-profile.js +11 -0
- package/dist/bpmn/intsvc.d.ts +77 -0
- package/dist/bpmn/intsvc.js +155 -0
- package/dist/bpmn/load.d.ts +4 -0
- package/dist/bpmn/load.js +22 -0
- package/dist/bpmn/merge-cli.d.ts +3 -0
- package/dist/bpmn/merge-cli.js +45 -0
- package/dist/bpmn/merge.d.ts +19 -0
- package/dist/bpmn/merge.js +293 -0
- package/dist/bpmn/registry-types.json +1869 -0
- package/dist/bpmn/serialize.d.ts +28 -0
- package/dist/bpmn/serialize.js +757 -0
- package/dist/bpmn/tidy-cli.d.ts +2 -0
- package/dist/bpmn/tidy-cli.js +39 -0
- package/dist/bpmn/typed-node.d.ts +382 -0
- package/dist/bpmn/typed-node.js +551 -0
- package/dist/bpmn/uipath-moddle.v1.json +823 -0
- package/dist/bpmn/xml-ranges.d.ts +45 -0
- package/dist/bpmn/xml-ranges.js +151 -0
- package/dist/case/case-expr-check.d.ts +35 -0
- package/dist/case/case-expr-check.js +85 -0
- package/dist/case/case-sdk.d.ts +1282 -0
- package/dist/case/case-sdk.js +923 -0
- package/dist/case/check-cli.d.ts +3 -0
- package/dist/case/check-cli.js +31 -0
- package/dist/case/check.d.ts +18 -0
- package/dist/case/check.js +456 -0
- package/dist/case/compile-cli.d.ts +3 -0
- package/dist/case/compile-cli.js +291 -0
- package/dist/case/decompile-cli.d.ts +9 -0
- package/dist/case/decompile-cli.js +117 -0
- package/dist/case/decompile.d.ts +187 -0
- package/dist/case/decompile.js +1170 -0
- package/dist/case/entry-points.d.ts +79 -0
- package/dist/case/entry-points.js +118 -0
- package/dist/case/format-profile.d.ts +60 -0
- package/dist/case/format-profile.js +79 -0
- package/dist/case/ids.d.ts +24 -0
- package/dist/case/ids.js +76 -0
- package/dist/case/load.d.ts +12 -0
- package/dist/case/load.js +32 -0
- package/dist/case/preserve.d.ts +133 -0
- package/dist/case/preserve.js +105 -0
- package/dist/case/serialize.d.ts +10 -0
- package/dist/case/serialize.js +1414 -0
- package/dist/check-cli.d.ts +3 -0
- package/dist/check-cli.js +118 -0
- package/dist/check.d.ts +146 -0
- package/dist/check.js +4898 -0
- package/dist/cli/commands/bpmn.d.ts +2 -0
- package/dist/cli/commands/bpmn.js +73 -0
- package/dist/cli/commands/case.d.ts +2 -0
- package/dist/cli/commands/case.js +60 -0
- package/dist/cli/commands/flow.d.ts +2 -0
- package/dist/cli/commands/flow.js +99 -0
- package/dist/cli/commands/registry.d.ts +10 -0
- package/dist/cli/commands/registry.js +170 -0
- package/dist/cli/common.d.ts +30 -0
- package/dist/cli/common.js +92 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +18 -0
- package/dist/cli-run.d.ts +2 -0
- package/dist/cli-run.js +42 -0
- package/dist/compile-cli.d.ts +3 -0
- package/dist/compile-cli.js +160 -0
- package/dist/config.d.ts +47 -0
- package/dist/config.js +201 -0
- package/dist/core/actions.d.ts +3244 -0
- package/dist/core/actions.js +1062 -0
- package/dist/core/binding-messages.d.ts +1 -0
- package/dist/core/binding-messages.js +16 -0
- package/dist/core/bindings.d.ts +12 -0
- package/dist/core/bindings.js +67 -0
- package/dist/core/ceql.d.ts +73 -0
- package/dist/core/ceql.js +272 -0
- package/dist/core/cli-spelling.d.ts +39 -0
- package/dist/core/cli-spelling.js +62 -0
- package/dist/core/connector-raw-node.d.ts +21 -0
- package/dist/core/connector-raw-node.js +47 -0
- package/dist/core/connectors.d.ts +184 -0
- package/dist/core/connectors.js +128 -0
- package/dist/core/expr-check.d.ts +93 -0
- package/dist/core/expr-check.js +186 -0
- package/dist/core/expr.d.ts +323 -0
- package/dist/core/expr.js +400 -0
- package/dist/core/index.d.ts +10 -0
- package/dist/core/index.js +10 -0
- package/dist/core/library.d.ts +440 -0
- package/dist/core/library.js +1183 -0
- package/dist/core/list-envelope.d.ts +45 -0
- package/dist/core/list-envelope.js +97 -0
- package/dist/core/lookup-store.d.ts +22 -0
- package/dist/core/lookup-store.js +85 -0
- package/dist/core/lookups.d.ts +227 -0
- package/dist/core/lookups.js +231 -0
- package/dist/core/node-classes.d.ts +234 -0
- package/dist/core/node-classes.js +158 -0
- package/dist/core/script-return.d.ts +68 -0
- package/dist/core/script-return.js +197 -0
- package/dist/core/stable-id.d.ts +28 -0
- package/dist/core/stable-id.js +37 -0
- package/dist/core-definitions.json +16249 -0
- package/dist/decompile-cli.d.ts +3 -0
- package/dist/decompile-cli.js +131 -0
- package/dist/decompile.d.ts +138 -0
- package/dist/decompile.js +2482 -0
- package/dist/emit.d.ts +10 -0
- package/dist/emit.js +60 -0
- package/dist/event-filters.d.ts +130 -0
- package/dist/event-filters.js +137 -0
- package/dist/flow-expr-check.d.ts +17 -0
- package/dist/flow-expr-check.js +193 -0
- package/dist/flow-sdk.d.ts +1341 -0
- package/dist/flow-sdk.js +1164 -0
- package/dist/flow-variables.d.ts +13 -0
- package/dist/flow-variables.js +42 -0
- package/dist/format-profile.d.ts +72 -0
- package/dist/format-profile.js +97 -0
- package/dist/generators/_connections.py +287 -0
- package/dist/generators/_events.py +355 -0
- package/dist/generators/_lookups.py +114 -0
- package/dist/generators/_naming.py +185 -0
- package/dist/generators/_objects.py +299 -0
- package/dist/generators/_resolve.py +273 -0
- package/dist/generators/convert_library_to_md.py +1441 -0
- package/dist/generators/extract_library.py +125 -0
- package/dist/generators/generate_connectors_ts.py +936 -0
- package/dist/generators/generate_library_json.py +2527 -0
- package/dist/generators/generate_triggers_ts.py +110 -0
- package/dist/generators/materialize_registry.py +155 -0
- package/dist/generators/prepare_connector.py +1688 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +35 -0
- package/dist/ixp-definition.json +1982 -0
- package/dist/library.d.ts +6 -0
- package/dist/library.js +6 -0
- package/dist/load.d.ts +16 -0
- package/dist/load.js +95 -0
- package/dist/merge-cli.d.ts +3 -0
- package/dist/merge-cli.js +47 -0
- package/dist/merge.d.ts +125 -0
- package/dist/merge.js +0 -0
- package/dist/migrate.d.ts +15 -0
- package/dist/migrate.js +245 -0
- package/dist/node-runtime.d.ts +3 -0
- package/dist/node-runtime.js +22 -0
- package/dist/package-name.d.ts +17 -0
- package/dist/package-name.js +17 -0
- package/dist/ref-resolve.d.ts +77 -0
- package/dist/ref-resolve.js +170 -0
- package/dist/registry/cache.d.ts +55 -0
- package/dist/registry/cache.js +134 -0
- package/dist/registry/curated-overlay.d.ts +16 -0
- package/dist/registry/curated-overlay.js +62 -0
- package/dist/registry/extractor.d.ts +22 -0
- package/dist/registry/extractor.js +18 -0
- package/dist/registry/materialize-bin.d.ts +2 -0
- package/dist/registry/materialize-bin.js +7 -0
- package/dist/registry/materialize-cli.d.ts +1 -0
- package/dist/registry/materialize-cli.js +46 -0
- package/dist/registry/materializer.d.ts +23 -0
- package/dist/registry/materializer.js +34 -0
- package/dist/registry/path-cli.d.ts +1 -0
- package/dist/registry/path-cli.js +56 -0
- package/dist/registry/prepare-cli.d.ts +62 -0
- package/dist/registry/prepare-cli.js +185 -0
- package/dist/registry/pull-cli.d.ts +1 -0
- package/dist/registry/pull-cli.js +274 -0
- package/dist/registry/python.d.ts +26 -0
- package/dist/registry/python.js +46 -0
- package/dist/registry/search-cli.d.ts +52 -0
- package/dist/registry/search-cli.js +159 -0
- package/dist/schema-version.d.ts +165 -0
- package/dist/schema-version.js +148 -0
- package/dist/serialize.d.ts +64 -0
- package/dist/serialize.js +6398 -0
- package/lib/README.md +112 -0
- package/lib/library-json.samples/index.json +158 -0
- package/lib/library-json.samples/invariant-collections.json +83 -0
- package/lib/library-json.samples/list-envelopes.json +123 -0
- package/lib/library-json.samples/uipath-atlassian-jira/create-issue@1.0.0.json +131 -0
- package/lib/library-json.samples/uipath-atlassian-jira/create-issue@1.0.0.v1def.json +306 -0
- package/lib/library-json.samples/uipath-atlassian-jira/generic/create_issue/create@1.0.0.json +86 -0
- package/lib/library-json.samples/uipath-atlassian-jira/generic/create_issue/create@1.0.0.v1def.json +208 -0
- package/lib/library-json.samples/uipath-atlassian-jira/get-issue@1.0.0.json +2040 -0
- package/lib/library-json.samples/uipath-atlassian-jira/get-issue@1.0.0.v1def.json +195 -0
- package/lib/library-json.samples/uipath-google-vertex/execute-google-vertex-agent@1.0.0.json +169 -0
- package/lib/library-json.samples/uipath-google-vertex/execute-google-vertex-agent@1.0.0.v1def.json +209 -0
- package/lib/library-json.samples/uipath-http-webhook/event-http-webhook@1.0.0.json +63 -0
- package/lib/library-json.samples/uipath-http-webhook/event-http-webhook@1.0.0.v1def.json +262 -0
- package/lib/library-json.samples/uipath-http-webhook/trigger-http-webhook@1.0.0.json +63 -0
- package/lib/library-json.samples/uipath-http-webhook/trigger-http-webhook@1.0.0.v1def.json +213 -0
- package/lib/library-json.samples/uipath-microsoft-azureactivedirectory/list-groups@1.0.0.json +227 -0
- package/lib/library-json.samples/uipath-microsoft-azureactivedirectory/list-groups@1.0.0.v1def.json +373 -0
- package/lib/library-json.samples/uipath-microsoft-onedrive/event-file-created@1.0.0.json +116 -0
- package/lib/library-json.samples/uipath-microsoft-onedrive/event-file-created@1.0.0.v1def.json +369 -0
- package/lib/library-json.samples/uipath-microsoft-onedrive/event-list-item-added@1.0.0.json +56 -0
- package/lib/library-json.samples/uipath-microsoft-onedrive/event-list-item-added@1.0.0.v1def.json +256 -0
- package/lib/library-json.samples/uipath-microsoft-onedrive/trigger-file-created@1.0.0.json +116 -0
- package/lib/library-json.samples/uipath-microsoft-onedrive/trigger-file-created@1.0.0.v1def.json +320 -0
- package/lib/library-json.samples/uipath-microsoft-onedrive/trigger-list-item-added@1.0.0.json +56 -0
- package/lib/library-json.samples/uipath-microsoft-onedrive/trigger-list-item-added@1.0.0.v1def.json +207 -0
- package/lib/library-json.samples/uipath-microsoft-outlook365/event-email-received@1.0.0.json +107 -0
- package/lib/library-json.samples/uipath-microsoft-outlook365/event-email-received@1.0.0.v1def.json +264 -0
- package/lib/library-json.samples/uipath-microsoft-outlook365/send-email@1.0.0.json +142 -0
- package/lib/library-json.samples/uipath-microsoft-outlook365/send-email@1.0.0.v1def.json +306 -0
- package/lib/library-json.samples/uipath-microsoft-outlook365/trigger-email-received@1.0.0.json +107 -0
- package/lib/library-json.samples/uipath-microsoft-outlook365/trigger-email-received@1.0.0.v1def.json +217 -0
- package/lib/library-json.samples/uipath-microsoft-powerautomate/invoke-a-microsoft-power-automate-flow@1.0.0.json +85 -0
- package/lib/library-json.samples/uipath-microsoft-powerautomate/invoke-a-microsoft-power-automate-flow@1.0.0.v1def.json +209 -0
- package/lib/library-json.samples/uipath-salesforce-slack/send-message-to-user@1.0.0.json +755 -0
- package/lib/library-json.samples/uipath-salesforce-slack/send-message-to-user@1.0.0.v1def.json +513 -0
- package/lib/library-json.samples/uipath-uipath-dataservice/query-entity-records@1.0.0.json +122 -0
- package/lib/library-json.samples/uipath-uipath-dataservice/query-entity-records@1.0.0.v1def.json +227 -0
- package/package.json +85 -0
package/dist/flow-sdk.js
ADDED
|
@@ -0,0 +1,1164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* flow-sdk — a small fluent builder for UiPath Flow workflows.
|
|
3
|
+
*
|
|
4
|
+
* You author a workflow as TypeScript code that *constructs* a graph of nodes
|
|
5
|
+
* and edges. Calling `.build()` returns a plain description of that graph; the
|
|
6
|
+
* `compile` tool serializes it into a real `.flow` JSON file.
|
|
7
|
+
*
|
|
8
|
+
* This is deliberately a builder, not a program: control flow (branch/loop) is
|
|
9
|
+
* expressed by calling `.branch()` / `.loop()`, not with native `if`/`for`.
|
|
10
|
+
* Data references are expressed with the expression helpers (`input`, `out`,
|
|
11
|
+
* `v`, `js`, `tmpl`) rather than by reading real values — nothing actually runs
|
|
12
|
+
* at build time except the graph construction.
|
|
13
|
+
*
|
|
14
|
+
* The expression helpers and action specs are shared across all builder SDKs
|
|
15
|
+
* and live in `core/`; this module re-exports them so the historical
|
|
16
|
+
* `flow-sdk.js` authoring surface is unchanged, and adds the Flow-specific
|
|
17
|
+
* builder (steps, branches, loops, return).
|
|
18
|
+
*/
|
|
19
|
+
// Re-export the shared core authoring surface (Expr, lit/v/input/out/js/tmpl,
|
|
20
|
+
// types, and http/script/connector action specs) so `./flow-sdk.js` remains the
|
|
21
|
+
// single import an author needs.
|
|
22
|
+
export * from './core/expr.js';
|
|
23
|
+
export * from './core/actions.js';
|
|
24
|
+
// `$ref`-split ingestion (design §5): decompile resolves local-file refs into
|
|
25
|
+
// one object model. Exported so a host doing its own file reading can too.
|
|
26
|
+
export { resolveFlowRefs, resolveRefPath, hasFileRefs } from './ref-resolve.js';
|
|
27
|
+
export * from './core/connectors.js';
|
|
28
|
+
export * from './core/lookups.js';
|
|
29
|
+
export * from './core/node-classes.js';
|
|
30
|
+
import { err as errRef, toExpr, types } from './core/expr.js';
|
|
31
|
+
import { builtinActionSpec, builtinTriggerSpec, subscriptionFrom } from './core/actions.js';
|
|
32
|
+
import { checkFlowExpressions, FlowBuildError } from './flow-expr-check.js';
|
|
33
|
+
import { FLOW_SCHEMA_VERSIONS } from './format-profile.js';
|
|
34
|
+
import { schemaVersionRefusal } from './schema-version.js';
|
|
35
|
+
export { FlowBuildError, checkFlowExpressions } from './flow-expr-check.js';
|
|
36
|
+
const TYPE_NAMES = Object.values(types);
|
|
37
|
+
/**
|
|
38
|
+
* Reject a `.input()` / `.output()` argument that is not a `{ name: type }` map.
|
|
39
|
+
*
|
|
40
|
+
* WHY THIS THROWS INSTEAD OF DIAGNOSING. TypeScript's types are erased at run
|
|
41
|
+
* time, so `.output('title', 'string')` — the object-shaped API called as if it
|
|
42
|
+
* took two positional arguments — reaches the builder as the plain string
|
|
43
|
+
* `'title'`. `Object.entries('title')` is `[['0','t'],['1','i'],…]`, so the flow
|
|
44
|
+
* quietly declared five globals named `"0".."4"` with types `t,i,t,l,e`.
|
|
45
|
+
* The source checker said "no issues" (nothing it looks at was wrong — the names and types
|
|
46
|
+
* were merely nonsense), and the mistake only surfaced further down the ladder, about
|
|
47
|
+
* a file the author never wrote.
|
|
48
|
+
*
|
|
49
|
+
* This is the same class as `.branch('big','amount')` (card T1) and the same fix
|
|
50
|
+
* shape: normalize or reject at the builder, where the mistake is. `.input()` and
|
|
51
|
+
* `.output()` run while the module is being evaluated, so the throw arrives before
|
|
52
|
+
* anything else — with the call that caused it named.
|
|
53
|
+
*/
|
|
54
|
+
function assertShape(method, shape) {
|
|
55
|
+
if (typeof shape !== 'object' || shape === null || Array.isArray(shape)) {
|
|
56
|
+
throw new TypeError(`.${method}() takes ONE object mapping names to types — got ${Array.isArray(shape) ? 'an array' : typeof shape}`
|
|
57
|
+
+ `${typeof shape === 'string' ? ` (${JSON.stringify(shape)})` : ''}. `
|
|
58
|
+
+ `Write .${method}({ ${typeof shape === 'string' ? shape : 'name'}: types.string }), not `
|
|
59
|
+
+ `.${method}('name', 'string'): a string would be iterated character by character and `
|
|
60
|
+
+ `declare one garbage variable per letter.`);
|
|
61
|
+
}
|
|
62
|
+
return shape;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Normalize a `TypeDesc | VarSpec` declaration value into VarDecl fields.
|
|
66
|
+
* Outputs reject `default` — the format only persists defaults on `in`/`inout`.
|
|
67
|
+
*/
|
|
68
|
+
function varDeclOf(method, name, value) {
|
|
69
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
70
|
+
const spec = value;
|
|
71
|
+
assertType(method, name, spec.type);
|
|
72
|
+
if (method === 'output' && spec.default !== undefined) {
|
|
73
|
+
throw new TypeError(`.output() declared ${JSON.stringify(name)} with a default, but the .flow format only persists `
|
|
74
|
+
+ `defaults on inputs and vars. Bind the value with .return() instead.`);
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
type: spec.type,
|
|
78
|
+
...(spec.default !== undefined ? { default: spec.default } : {}),
|
|
79
|
+
...(spec.description !== undefined ? { description: spec.description } : {}),
|
|
80
|
+
...(spec.subType !== undefined ? { subType: spec.subType } : {}),
|
|
81
|
+
...(spec.schema !== undefined ? { schema: spec.schema } : {}),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
assertType(method, name, value);
|
|
85
|
+
return { type: value };
|
|
86
|
+
}
|
|
87
|
+
/** A declared variable's type must be one of the seven `types.*` names. */
|
|
88
|
+
function assertType(method, name, type) {
|
|
89
|
+
if (typeof type === 'string' && TYPE_NAMES.includes(type))
|
|
90
|
+
return;
|
|
91
|
+
throw new TypeError(`.${method}() declared ${JSON.stringify(name)} with type ${JSON.stringify(type)}, which is not `
|
|
92
|
+
+ `a type. Use one of ${TYPE_NAMES.map((t) => `types.${t}`).join(', ')}.`);
|
|
93
|
+
}
|
|
94
|
+
// ─── Triggers (what starts the flow) ────────────────────────────────────────
|
|
95
|
+
//
|
|
96
|
+
// Every flow starts at one trigger node. Omit `.trigger(...)` and you get the
|
|
97
|
+
// manual trigger — started by a caller, on demand — which is what all the
|
|
98
|
+
// examples do and stays the default.
|
|
99
|
+
//
|
|
100
|
+
// `scheduled(...)` is the other start trigger: the platform fires the flow on a
|
|
101
|
+
// recurring interval, with no caller. Note what that means for a LOCAL run —
|
|
102
|
+
// the interval is packaged into the flow's BPMN as a timer expression, and the
|
|
103
|
+
// thing that reads it and fires is the platform's scheduler. Nothing in this
|
|
104
|
+
// repo's ladder runs a timer, so locally a scheduled flow starts exactly like a
|
|
105
|
+
// manual one; what we can verify here is that the emitted node is the right
|
|
106
|
+
// node carrying the right interval.
|
|
107
|
+
/**
|
|
108
|
+
* The eight intervals the designer offers in its Frequency dropdown. Any other
|
|
109
|
+
* valid
|
|
110
|
+
* {@link https://docs.digi.com/resources/documentation/digidocs/90001488-13/reference/r_iso_8601_duration_format.htm | ISO-8601 repeating interval}
|
|
111
|
+
* works too (see `ScheduleEvery`) — these are
|
|
112
|
+
* just the ones that round-trip as a named preset rather than as "custom".
|
|
113
|
+
*/
|
|
114
|
+
export const SCHEDULE_PRESETS = [
|
|
115
|
+
'R/PT5M', 'R/PT15M', 'R/PT30M', 'R/PT1H', 'R/PT6H', 'R/PT12H', 'R/P1D', 'R/P1W',
|
|
116
|
+
];
|
|
117
|
+
/**
|
|
118
|
+
* The manual trigger, explicitly. Omitting `.trigger(...)` means exactly this —
|
|
119
|
+
* the factory exists so decompiled source can carry the trigger's exact
|
|
120
|
+
* definition version (`.trigger(manual(), { version: '1.0.0' })`) instead of
|
|
121
|
+
* leaving the default implicit, and so hand-written flows can opt into the
|
|
122
|
+
* same explicitness.
|
|
123
|
+
*
|
|
124
|
+
* @returns A trigger spec to pass to `.trigger(...)`.
|
|
125
|
+
*/
|
|
126
|
+
export function manual() {
|
|
127
|
+
return builtinTriggerSpec({ kind: 'manual' });
|
|
128
|
+
}
|
|
129
|
+
export function onEvent(a, opts) {
|
|
130
|
+
return builtinTriggerSpec({ kind: 'event', subscription: subscriptionFrom(a, opts) });
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Start the flow on a recurring schedule instead of on demand.
|
|
134
|
+
*
|
|
135
|
+
* @remarks
|
|
136
|
+
* Field shapes, the interval format, and what a local run does and does not
|
|
137
|
+
* prove: `references/scheduled-trigger.md`.
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* **Run a flow daily**
|
|
141
|
+
* ```ts
|
|
142
|
+
* export default flow('nightly-rollup')
|
|
143
|
+
* .trigger(scheduled({ every: 'R/P1D' }))
|
|
144
|
+
* .step('rollup', script({ code: 'return { ok: true };' }))
|
|
145
|
+
* .build();
|
|
146
|
+
* ```
|
|
147
|
+
*
|
|
148
|
+
* @param inputs - The recurring interval, e.g. `{ every: 'R/PT1H' }`.
|
|
149
|
+
* @returns A trigger spec to pass to `.trigger(...)`.
|
|
150
|
+
*/
|
|
151
|
+
export function scheduled(inputs) {
|
|
152
|
+
return builtinTriggerSpec({ kind: 'scheduled', inputs });
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Start the flow from a FORM a person fills in (`core.trigger.form`) — the
|
|
156
|
+
* platform renders one field per flow input and the submitted values ARE the
|
|
157
|
+
* flow's inputs.
|
|
158
|
+
*
|
|
159
|
+
* ```ts
|
|
160
|
+
* export default flow('expense-request')
|
|
161
|
+
* .input({ amount: types.number, reason: types.string })
|
|
162
|
+
* .trigger(formTrigger())
|
|
163
|
+
* .step('review', hitl({ … }))
|
|
164
|
+
* .build();
|
|
165
|
+
* ```
|
|
166
|
+
*
|
|
167
|
+
* The form's fields are DERIVED from `.input()` — one per input, the same rule
|
|
168
|
+
* the designer's own form-trigger editor applies to a flow's arguments: the
|
|
169
|
+
* field id is the input's name (so submitted values line up by name), the label
|
|
170
|
+
* is the name sentence-cased, the field type follows the input's type, and a
|
|
171
|
+
* field is required exactly when its input declares no default.
|
|
172
|
+
*
|
|
173
|
+
* The definition is BUNDLED from the workbench manifest — the tenant registry
|
|
174
|
+
* does not serve the family (same availability note as do-while).
|
|
175
|
+
*
|
|
176
|
+
* No local rung renders a form: `--input` supplies the values locally, and a
|
|
177
|
+
* green ladder proves the trigger type, the derived schema, and that the graph
|
|
178
|
+
* runs — not that a person saw a form.
|
|
179
|
+
*
|
|
180
|
+
* @returns A trigger spec to pass to `.trigger(...)`.
|
|
181
|
+
*/
|
|
182
|
+
export function formTrigger() {
|
|
183
|
+
return builtinTriggerSpec({ kind: 'form' });
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Start the flow when a new CONVERSATION is created
|
|
187
|
+
* (`core.trigger.conversation`) — the entry point for a chat-driven process.
|
|
188
|
+
*
|
|
189
|
+
* ```ts
|
|
190
|
+
* export default flow('support-chat')
|
|
191
|
+
* .trigger(conversationTrigger())
|
|
192
|
+
* .step('listen', waitForMessage({ conversationId: out('start', 'conversationId') }))
|
|
193
|
+
* .step('answer', sendMessage({
|
|
194
|
+
* conversationId: out('start', 'conversationId'),
|
|
195
|
+
* exchangeId: out('listen', 'conversationContext.latestExchangeId'),
|
|
196
|
+
* content: 'Looking into it.',
|
|
197
|
+
* }))
|
|
198
|
+
* .build();
|
|
199
|
+
* ```
|
|
200
|
+
*
|
|
201
|
+
* The trigger publishes the conversation it started —
|
|
202
|
+
* `out('start', 'conversationId')` — which is what every conversational step
|
|
203
|
+
* is keyed by. Nothing else is authored on it.
|
|
204
|
+
*
|
|
205
|
+
* The definition is BUNDLED from the workbench manifest: the family is
|
|
206
|
+
* `AvailableOnTenant: false` today, so a flow compiles and validates offline
|
|
207
|
+
* while the platform catches up (the do-while precedent).
|
|
208
|
+
*
|
|
209
|
+
* @returns A trigger spec to pass to `.trigger(...)`.
|
|
210
|
+
*/
|
|
211
|
+
export function conversationTrigger() {
|
|
212
|
+
return builtinTriggerSpec({ kind: 'conversation' });
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Start the flow when a phone call comes IN (`core.trigger.voice`).
|
|
216
|
+
*
|
|
217
|
+
* ```ts
|
|
218
|
+
* export default flow('support-line')
|
|
219
|
+
* .trigger(voiceTrigger())
|
|
220
|
+
* .step('greet', voiceAgent({
|
|
221
|
+
* systemPrompt: 'Greet {{input.customerName}} and find out why they called.',
|
|
222
|
+
* inputs: { customerName: input('customerName') },
|
|
223
|
+
* callContext: out('start', 'callContext'),
|
|
224
|
+
* }))
|
|
225
|
+
* .step('hangUp', endCall({ callContext: out('start', 'callContext') }))
|
|
226
|
+
* .build();
|
|
227
|
+
* ```
|
|
228
|
+
*
|
|
229
|
+
* The trigger publishes the live call as `out('start', 'callContext')` — the
|
|
230
|
+
* object (`{ type, id, conversationId, … }`) every voice step is keyed by.
|
|
231
|
+
* Nothing else is authored on it.
|
|
232
|
+
*
|
|
233
|
+
* The definition is BUNDLED from the workbench manifest: the family is
|
|
234
|
+
* `AvailableOnTenant: false` today (the do-while precedent).
|
|
235
|
+
*
|
|
236
|
+
* @returns A trigger spec to pass to `.trigger(...)`.
|
|
237
|
+
*/
|
|
238
|
+
export function voiceTrigger() {
|
|
239
|
+
return builtinTriggerSpec({ kind: 'voice' });
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* The designer's sticky-note palette. Any other string is passed through — the
|
|
243
|
+
* `.flow` format stores `inputs.color` unvalidated — but these five are what the
|
|
244
|
+
* canvas offers and what round-trips as a named swatch.
|
|
245
|
+
*/
|
|
246
|
+
export const STICKY_NOTE_COLORS = ['yellow', 'pink', 'blue', 'green', 'white'];
|
|
247
|
+
// ─── Builder ────────────────────────────────────────────────────────────────
|
|
248
|
+
/** Collects a sequence of steps. Used for the flow body and each branch/loop arm. */
|
|
249
|
+
class StepList {
|
|
250
|
+
steps = [];
|
|
251
|
+
/**
|
|
252
|
+
* The step whose failure led here, set only on the sub-builder `.onError(...)`
|
|
253
|
+
* hands its callback. `err()` on this builder needs no step name because of it.
|
|
254
|
+
*/
|
|
255
|
+
errorSource;
|
|
256
|
+
/**
|
|
257
|
+
* Read the failure that led into this handler → `err('<the failed step>', field)`.
|
|
258
|
+
*
|
|
259
|
+
* @remarks
|
|
260
|
+
* Only available on the sub-builder `.onError(...)` / `.stepToList('error', …)`
|
|
261
|
+
* passes to its callback, because only there is "the step that failed" a fact
|
|
262
|
+
* rather than a guess.
|
|
263
|
+
*
|
|
264
|
+
* Prefer it over the free `err('<step>', field)` inside a handler. It cannot
|
|
265
|
+
* name the wrong step, it cannot drift when the step is renamed, and — the
|
|
266
|
+
* reason it exists — it keeps the envelope's LOCATION out of author-visible
|
|
267
|
+
* code entirely. Which variable carries the envelope is per node family
|
|
268
|
+
* (measured: `<step>.error` on connectors, script, deep-rag, subflow and
|
|
269
|
+
* queue; `<step>.output` on managed http), and the compiler resolves it.
|
|
270
|
+
*
|
|
271
|
+
* ```ts
|
|
272
|
+
* .step('load', script({ code: 'return JSON.parse($vars.start.output.raw);' }))
|
|
273
|
+
* .onError((h) => h.return({ note: tmpl`bad payload: ${h.err('message')}` }))
|
|
274
|
+
* ```
|
|
275
|
+
*
|
|
276
|
+
* @param field - One of the envelope's fields. Omit it to test WHETHER the
|
|
277
|
+
* step failed (the boolean the platform sets when the boundary fires).
|
|
278
|
+
* @returns An {@link Expr} reading the failure of the step this handler guards.
|
|
279
|
+
* @see err
|
|
280
|
+
*/
|
|
281
|
+
err(field) {
|
|
282
|
+
if (!this.errorSource) {
|
|
283
|
+
throw new TypeError('err(field) with no step name is only available inside an error handler — the sub-builder '
|
|
284
|
+
+ '.onError((h) => …) / .stepToList(\'error\', (h) => …) passes. Elsewhere, name the step: '
|
|
285
|
+
+ "err('<step>', 'message').");
|
|
286
|
+
}
|
|
287
|
+
return errRef(this.errorSource, field);
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Add an action node (see `http` / `script` / `subflow`).
|
|
291
|
+
*
|
|
292
|
+
* @param name - The step's id. Read its result downstream with `out('<name>')`,
|
|
293
|
+
* so it must be unique within the flow.
|
|
294
|
+
* @param spec - What the node does, from an action factory.
|
|
295
|
+
* @param options - Node options; `version` selects the exact definition
|
|
296
|
+
* version (see {@link NodeOptions}).
|
|
297
|
+
* @returns This builder, so calls chain.
|
|
298
|
+
*/
|
|
299
|
+
step(name, spec, options) {
|
|
300
|
+
// A custom FlowAction subclass carries no `kind` and is lowered through its
|
|
301
|
+
// own contribution; the built-in kind-switched paths never see it, so the
|
|
302
|
+
// stored Step keeps the structural spec type.
|
|
303
|
+
this.steps.push({ kind: 'action', name, spec: spec, ...(options ? { options } : {}) });
|
|
304
|
+
return this;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Handle the PREVIOUS step's failure: if it fails, the flow runs `bodyFn`'s
|
|
308
|
+
* steps instead of continuing.
|
|
309
|
+
*
|
|
310
|
+
* @remarks
|
|
311
|
+
* ```ts
|
|
312
|
+
* .step('fetch', http({ url: apiUrl, managed: true, returns: { items: 'array' } }))
|
|
313
|
+
* .onError((h) => h.return({ result: lit('Article not found') }))
|
|
314
|
+
* .step('total', transform({ … })) // runs only when `fetch` SUCCEEDED
|
|
315
|
+
* .return({ result: out('total') })
|
|
316
|
+
* ```
|
|
317
|
+
*
|
|
318
|
+
* The handler is its own path — anything chained after `.onError(...)` belongs
|
|
319
|
+
* to the SUCCESS path only. Inside it, read the failure with
|
|
320
|
+
* {@link StepList.err} — `h.err('message')` — or with the free
|
|
321
|
+
* `err('<step>', 'message')` if you prefer to name the step. The envelope
|
|
322
|
+
* carries `code`, `message`, `detail`, `category` and `status` (`code` is a
|
|
323
|
+
* string even for an HTTP status, `status` a number), plus an undeclared
|
|
324
|
+
* `response` and `element` the runtime adds.
|
|
325
|
+
*
|
|
326
|
+
* Do NOT reach for `out('<step>', …)` in here. That reads the step's SUCCESS
|
|
327
|
+
* output, which was never written — the handler runs precisely because the
|
|
328
|
+
* step failed — and `check` refuses it (ERROR_ENVELOPE_VIA_OUTPUT). This doc
|
|
329
|
+
* line used to recommend `out('<step>', 'error', 'message')`, which is not
|
|
330
|
+
* even a legal call: `out(step, path?)` takes two arguments.
|
|
331
|
+
*
|
|
332
|
+
* **Three ways the handler can end**: `.return(...)` answers the flow,
|
|
333
|
+
* `.terminate(...)` stops the run, and `h.rejoin('<step>')` compensates and
|
|
334
|
+
* hands control back to a step the success path also reaches — the shape most
|
|
335
|
+
* deployed flows use. A handler that just runs out of steps still gets its own
|
|
336
|
+
* End: rejoining is something the author SAYS, never something wiring infers.
|
|
337
|
+
* See {@link StepList.stepToRef}.
|
|
338
|
+
*
|
|
339
|
+
* Two things this does for you, by construction: it sets
|
|
340
|
+
* `errorHandlingEnabled` on the node in the same write as the edge (without
|
|
341
|
+
* it the platform routes but Studio Web HIDES the handle, so no human can see
|
|
342
|
+
* your handler), and it refuses at build time on a step whose node type has no
|
|
343
|
+
* error port.
|
|
344
|
+
*
|
|
345
|
+
* **A `.loop()` container takes one too.** `core.logic.loop` declares the
|
|
346
|
+
* handle, and a body step's failure routes to it: the container's envelope
|
|
347
|
+
* carries the body's message in `detail` and the failing body step's id in
|
|
348
|
+
* `element`, and the instance completes rather than faulting. `.doWhile()` does
|
|
349
|
+
* NOT — it declares an error variable and no error handle — so that one is
|
|
350
|
+
* refused by name, and its failures belong to the body step that can fail.
|
|
351
|
+
*
|
|
352
|
+
* **A plain `http()` step is the trap worth knowing**: a 404 there is a
|
|
353
|
+
* SUCCESS with `statusCode: 404`, so the handler never runs — use
|
|
354
|
+
* `http({ …, managed: true })` when failures must route. `check` says so
|
|
355
|
+
* (HTTP_ONERROR_V1).
|
|
356
|
+
*
|
|
357
|
+
* Which node types can fail, what the envelope carries, and what a local run
|
|
358
|
+
* does and does not prove: `references/error-handling.md`.
|
|
359
|
+
*
|
|
360
|
+
* @param bodyFn - Receives a sub-builder for the handler's own path. Read the
|
|
361
|
+
* failure inside it with `h.err('message')` ({@link StepList.err}).
|
|
362
|
+
* @returns This builder, positioned on the SUCCESS path — anything chained after
|
|
363
|
+
* this call runs only when the step succeeded.
|
|
364
|
+
* @enforcedBy HTTP_ONERROR_V1 On an http step this needs `managed: true`; on the
|
|
365
|
+
* standalone node a 4xx arrives on the SUCCESS path and no handler runs.
|
|
366
|
+
*/
|
|
367
|
+
onError(bodyFn) {
|
|
368
|
+
return this.addPortList('error', bodyFn, '.onError()', 'handler');
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* The step a port-scoped edge leaves, and the guard that it is allowed to.
|
|
372
|
+
*
|
|
373
|
+
* Scans BACK past edges already attached to the same step: after
|
|
374
|
+
* `.onError(...)` the last entry in the list is that handler, not the action it
|
|
375
|
+
* guards, so `steps.at(-1)` would refuse a second port-edge on the same step —
|
|
376
|
+
* and would attach `.stepToRef('error', …)` to the wrong thing.
|
|
377
|
+
*
|
|
378
|
+
* A port takes exactly one outgoing edge (the definitions say so:
|
|
379
|
+
* `maxConnections: 1`), so a second edge from the same port is refused here
|
|
380
|
+
* rather than emitted and silently dropped downstream.
|
|
381
|
+
*/
|
|
382
|
+
portSource(port, called) {
|
|
383
|
+
let i = this.steps.length - 1;
|
|
384
|
+
const attached = [];
|
|
385
|
+
while (i >= 0 && (this.steps[i].kind === 'stepToList' || this.steps[i].kind === 'stepToRef')) {
|
|
386
|
+
attached.push(this.steps[i]);
|
|
387
|
+
i--;
|
|
388
|
+
}
|
|
389
|
+
const source = this.steps[i];
|
|
390
|
+
// A `.loop()` CONTAINER is a legal source for the error port. Its definition
|
|
391
|
+
// declares the handle at both bundled versions (`core.logic.loop@1.0.0` and
|
|
392
|
+
// `@2.4`, gated on `errorHandlingEnabled` like every other error handle), and
|
|
393
|
+
// the platform routes it: a body step throwing inside a measured run left the
|
|
394
|
+
// instance Completed with the container's envelope published — `detail` the
|
|
395
|
+
// body's own message, `element` the id of the body step that failed.
|
|
396
|
+
//
|
|
397
|
+
// `.doWhile()` is NOT: `core.logic.dowhile@1.0` declares an error VARIABLE
|
|
398
|
+
// and no error HANDLE, so an edge would leave a handle the node does not
|
|
399
|
+
// have — the same shape as the agent `memory` handle that fails validate.
|
|
400
|
+
const containerPort = port === 'error' && source?.kind === 'loop';
|
|
401
|
+
if (!source || (source.kind !== 'action' && !containerPort)) {
|
|
402
|
+
throw new TypeError(`${called} leaves a port of the step BEFORE it, and ${source ? `the previous step is a ${source.kind}` : 'there is no previous step'}. `
|
|
403
|
+
+ `Write .step('fetch', http({…}))${called}`
|
|
404
|
+
+ (source?.kind === 'doWhile' && port === 'error'
|
|
405
|
+
? ' — a .doWhile() declares an error variable but no error HANDLE (core.logic.dowhile@1.0), so the edge '
|
|
406
|
+
+ 'would leave a handle the node does not have. Handle the failure inside the body instead.'
|
|
407
|
+
: ' — a branch, parallel, terminate or return has no port to leave from; a .loop() has one only for '
|
|
408
|
+
+ "'error'."));
|
|
409
|
+
}
|
|
410
|
+
const taken = attached.some((s) => (s.kind === 'stepToList' || s.kind === 'stepToRef') && s.port === port);
|
|
411
|
+
if (taken) {
|
|
412
|
+
throw new TypeError(`${called} is already set on step "${source.name}"'s "${port}" port. A port takes exactly ONE outgoing edge `
|
|
413
|
+
+ `(the definition says so: maxConnections: 1), so put everything that path needs in one place.`);
|
|
414
|
+
}
|
|
415
|
+
return source;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Run a step list from a named PORT of the step this follows — the general form
|
|
419
|
+
* of an error handler, and the reason `.onError()` needs no machinery of its own.
|
|
420
|
+
*
|
|
421
|
+
* @remarks
|
|
422
|
+
* The body is its own path. It ends by answering (`.return`), stopping the run
|
|
423
|
+
* (`.terminate`), or handing control back with `.stepToRef(...)`; a body that
|
|
424
|
+
* simply runs out of steps gets its own End, exactly as an error handler does.
|
|
425
|
+
*
|
|
426
|
+
* The main path is untouched — an edge out of a NON-default port is a side exit,
|
|
427
|
+
* so whatever you chain after this call still belongs to the step's normal
|
|
428
|
+
* continuation.
|
|
429
|
+
*
|
|
430
|
+
* ```ts
|
|
431
|
+
* .step('fetch', http({ … , managed: true }))
|
|
432
|
+
* .stepToList('error', (h) => h.step('log', script({ … })).stepToRef('settle'))
|
|
433
|
+
* .step('parse', script({ … })) // ← still the SUCCESS path
|
|
434
|
+
* ```
|
|
435
|
+
*
|
|
436
|
+
* @param port - The port of the preceding step to leave from, e.g. `'error'`.
|
|
437
|
+
* @param bodyFn - Receives a sub-builder for the path that port leads to.
|
|
438
|
+
* @returns This builder, so calls chain.
|
|
439
|
+
* @see stepToRef
|
|
440
|
+
*/
|
|
441
|
+
stepToList(port, bodyFn) {
|
|
442
|
+
return this.addPortList(port, bodyFn, '.stepToList()', 'body');
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* The shared body of `.stepToList()` and `.onError()`.
|
|
446
|
+
*
|
|
447
|
+
* Private because `called` and `noun` exist only so a refusal names the method
|
|
448
|
+
* the author actually called — they are not part of the surface, and a public
|
|
449
|
+
* parameter nobody should pass is a worse trade than one extra hop.
|
|
450
|
+
*/
|
|
451
|
+
addPortList(port, bodyFn, called, noun) {
|
|
452
|
+
const prev = this.portSource(port, called);
|
|
453
|
+
const h = new StepList();
|
|
454
|
+
// The handler knows which step failed, so `h.err(field)` does not have to be
|
|
455
|
+
// told. Set for the error port only: on any other port list the preceding
|
|
456
|
+
// step SUCCEEDED, and there is no envelope to read.
|
|
457
|
+
if (port === 'error')
|
|
458
|
+
h.errorSource = prev.name;
|
|
459
|
+
bodyFn?.(h);
|
|
460
|
+
if (h.steps.length === 0) {
|
|
461
|
+
throw new TypeError(`${called} on step "${prev.name}" has an empty ${noun}. An edge to nothing is worse than none — `
|
|
462
|
+
+ `the path still leaves the main flow. Return a value, terminate the run, or do the compensating work.`);
|
|
463
|
+
}
|
|
464
|
+
this.steps.push({ kind: 'stepToList', port, body: h.steps });
|
|
465
|
+
return this;
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Branch on a condition. `thenFn`/`elseFn` receive a sub-builder for each arm.
|
|
469
|
+
*
|
|
470
|
+
* @remarks
|
|
471
|
+
* Each arm can name itself with `.label(...)` — the then-arm's label becomes the
|
|
472
|
+
* node's `trueLabel`, the else-arm's its `falseLabel` (the text the designer
|
|
473
|
+
* shows on the two outgoing edges). Omit it and the arms keep the default
|
|
474
|
+
* `"True"` / `"False"`.
|
|
475
|
+
*
|
|
476
|
+
* @param name - The decision node's id.
|
|
477
|
+
* @param cond - The condition, as an expression — typically `js\`…\``.
|
|
478
|
+
* @param thenFn - Receives a sub-builder for the TRUE arm.
|
|
479
|
+
* @param elseFn - Receives a sub-builder for the FALSE arm. Omit it for a
|
|
480
|
+
* decision whose false path simply continues.
|
|
481
|
+
* @param options - Node options; `version` selects the exact definition
|
|
482
|
+
* version (see {@link NodeOptions}).
|
|
483
|
+
* @returns This builder, after both arms have rejoined.
|
|
484
|
+
*/
|
|
485
|
+
branch(name, cond, thenFn, elseFn, options) {
|
|
486
|
+
if (options?.updates) {
|
|
487
|
+
throw new TypeError('.branch(): { updates } is not supported here — the platform strips variable updates from '
|
|
488
|
+
+ 'decision/switch/merge nodes on save. Attach them to a step before or after instead.');
|
|
489
|
+
}
|
|
490
|
+
const t = new ArmBuilder();
|
|
491
|
+
thenFn(t);
|
|
492
|
+
const e = new ArmBuilder();
|
|
493
|
+
if (elseFn)
|
|
494
|
+
elseFn(e);
|
|
495
|
+
// Normalize the condition the way `.switch` normalizes its discriminant: an
|
|
496
|
+
// authored `.flow.ts` runs with TYPES ERASED, so a bare value reaches here
|
|
497
|
+
// as itself rather than an Expr, and every later reader (check's reference
|
|
498
|
+
// scan, the serializer) then walks `undefined`. Wrapping it makes the
|
|
499
|
+
// mistake a named rung-1 diagnostic (BRANCH_CONST_COND) instead of
|
|
500
|
+
// `Cannot read properties of undefined (reading 'matchAll')`.
|
|
501
|
+
this.steps.push({
|
|
502
|
+
kind: 'branch',
|
|
503
|
+
name,
|
|
504
|
+
...(t.armLabel !== undefined ? { trueLabel: t.armLabel } : {}),
|
|
505
|
+
...(e.armLabel !== undefined ? { falseLabel: e.armLabel } : {}),
|
|
506
|
+
cond: toExpr(cond),
|
|
507
|
+
then: t.steps,
|
|
508
|
+
otherwise: e.steps,
|
|
509
|
+
...(options ? { options } : {}),
|
|
510
|
+
});
|
|
511
|
+
return this;
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* N-way branch on the value of `on` — one arm per case, plus an optional
|
|
515
|
+
* default arm. Each arm gets its own sub-builder, exactly like `.branch`.
|
|
516
|
+
*
|
|
517
|
+
* @remarks
|
|
518
|
+
* A case matches when `on === case.value`, compared the way JavaScript's own
|
|
519
|
+
* `switch` compares: **strictly, so the type counts**. `value: 2` matches a
|
|
520
|
+
* `types.number` input carrying `2`; `value: '2'` does not. Cases are
|
|
521
|
+
* evaluated top to bottom and the first match wins.
|
|
522
|
+
*
|
|
523
|
+
* @example
|
|
524
|
+
* **Route on a value, with a default arm**
|
|
525
|
+
* ```ts
|
|
526
|
+
* .switch('routeQuarter', input('quarter'), [
|
|
527
|
+
* { value: 1, body: (b) => b.step('spring', script({ code: 'return "Spring";' })) },
|
|
528
|
+
* { value: 2, body: (b) => b.step('summer', script({ code: 'return "Summer";' })) },
|
|
529
|
+
* ], (other) => other.step('unknown', script({ code: 'return "Unknown";' })))
|
|
530
|
+
* ```
|
|
531
|
+
*
|
|
532
|
+
* @param name - The switch node's id.
|
|
533
|
+
* @param on - The value to compare each case against.
|
|
534
|
+
* @param cases - One entry per arm, evaluated top to bottom; the first strict
|
|
535
|
+
* match wins. `label` names the arm's outgoing edge.
|
|
536
|
+
* @param defaultFn - Receives a sub-builder for the arm taken when nothing
|
|
537
|
+
* matches. Omit it and an unmatched value simply continues.
|
|
538
|
+
* @param options - Node options; `version` selects the exact definition
|
|
539
|
+
* version (see {@link NodeOptions}).
|
|
540
|
+
* @returns This builder, after every arm has rejoined.
|
|
541
|
+
*/
|
|
542
|
+
switch(name, on, cases, defaultFn, options) {
|
|
543
|
+
if (options?.updates) {
|
|
544
|
+
throw new TypeError('.switch(): { updates } is not supported here — the platform strips variable updates from '
|
|
545
|
+
+ 'decision/switch/merge nodes on save. Attach them to a step before or after instead.');
|
|
546
|
+
}
|
|
547
|
+
const arms = (cases ?? []).map((c) => {
|
|
548
|
+
const arm = new ArmBuilder();
|
|
549
|
+
c.body?.(arm);
|
|
550
|
+
// An arm's own `.label(...)` wins over the case config's `label`; either
|
|
551
|
+
// overrides the serializer's derived `Case "<value>"`.
|
|
552
|
+
const label = arm.armLabel ?? c.label;
|
|
553
|
+
return { value: c.value, ...(label !== undefined ? { label } : {}), body: arm.steps };
|
|
554
|
+
});
|
|
555
|
+
let dflt;
|
|
556
|
+
if (defaultFn) {
|
|
557
|
+
const d = new StepList();
|
|
558
|
+
defaultFn(d);
|
|
559
|
+
dflt = d.steps;
|
|
560
|
+
}
|
|
561
|
+
// `on` is coerced rather than trusted: the authored `.flow.ts` runs with
|
|
562
|
+
// types stripped, so a plain string here (`.switch('q', 'quarter', …)`)
|
|
563
|
+
// would otherwise reach the serializer as `undefined === 2`. As a literal
|
|
564
|
+
// Expr it becomes a check-time diagnostic instead (SWITCH_CONST_ON).
|
|
565
|
+
this.steps.push({
|
|
566
|
+
kind: 'switch',
|
|
567
|
+
name,
|
|
568
|
+
on: toExpr(on),
|
|
569
|
+
cases: arms,
|
|
570
|
+
...(dflt !== undefined ? { default: dflt } : {}),
|
|
571
|
+
...(options ? { options } : {}),
|
|
572
|
+
});
|
|
573
|
+
return this;
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Run two or more arms **in parallel** from this point and join them back
|
|
577
|
+
* together on a Merge node — the one place the builder stops being a straight
|
|
578
|
+
* chain.
|
|
579
|
+
*
|
|
580
|
+
* @remarks
|
|
581
|
+
* `name` names the **Merge** node (the join). Each arm is a sub-builder, just
|
|
582
|
+
* like a `.branch` arm, and every arm starts from the same place the chain had
|
|
583
|
+
* reached, so the graph forks there and reconverges on the Merge.
|
|
584
|
+
*
|
|
585
|
+
* Anything you chain afterwards continues from the Merge, so it can read every
|
|
586
|
+
* arm's output. The Merge itself computes nothing and publishes nothing — read
|
|
587
|
+
* the steps inside the arms, never `$vars.<merge>`.
|
|
588
|
+
*
|
|
589
|
+
* Arm semantics, what a local run does and does not prove, and the rules
|
|
590
|
+
* `check` enforces: `references/parallel-merge.md`.
|
|
591
|
+
*
|
|
592
|
+
* @example
|
|
593
|
+
* **Fetch two things at once, then combine them after the join**
|
|
594
|
+
* ```ts
|
|
595
|
+
* .parallel('sync', [
|
|
596
|
+
* (a) => a.step('fetchWeather', script({ code: 'return "sunny";' })),
|
|
597
|
+
* (b) => b.step('fetchNews', script({ code: 'return "quiet";' })),
|
|
598
|
+
* ])
|
|
599
|
+
* .step('combine', script({ code: 'return $vars.fetchWeather.output + $vars.fetchNews.output;' }))
|
|
600
|
+
* ```
|
|
601
|
+
*
|
|
602
|
+
* @param name - The split node's id. The Merge node is derived from it.
|
|
603
|
+
* @param arms - One callback per parallel arm, each receiving its own
|
|
604
|
+
* sub-builder. Two or more.
|
|
605
|
+
* @param options - Node options; `version` selects the exact definition
|
|
606
|
+
* version (see {@link NodeOptions}).
|
|
607
|
+
* @returns This builder, positioned after the Merge — so what you chain next
|
|
608
|
+
* runs once, when every arm has finished.
|
|
609
|
+
*/
|
|
610
|
+
parallel(name, arms, options) {
|
|
611
|
+
if (options?.updates) {
|
|
612
|
+
throw new TypeError('.parallel(): { updates } is not supported here — the platform strips variable updates from '
|
|
613
|
+
+ 'decision/switch/merge nodes on save. Attach them to a step before or after instead.');
|
|
614
|
+
}
|
|
615
|
+
const built = (arms ?? []).map((armFn) => {
|
|
616
|
+
const a = new StepList();
|
|
617
|
+
armFn?.(a);
|
|
618
|
+
return a.steps;
|
|
619
|
+
});
|
|
620
|
+
this.steps.push({ kind: 'parallel', name, arms: built, ...(options ? { options } : {}) });
|
|
621
|
+
return this;
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Iterate `collection`; `bodyFn` receives a sub-builder for the loop body.
|
|
625
|
+
*
|
|
626
|
+
* @param name - The loop node's id.
|
|
627
|
+
* @param collection - The array to iterate, as a reference — `v('items')` or
|
|
628
|
+
* `out('fetch', 'body.items')`.
|
|
629
|
+
* @param bodyFn - Receives a sub-builder for the body, run once per element.
|
|
630
|
+
* @param options - Node options; `version` selects the exact definition
|
|
631
|
+
* version (see {@link NodeOptions}).
|
|
632
|
+
* @returns This builder, positioned after the loop.
|
|
633
|
+
*/
|
|
634
|
+
loop(name, collection, bodyFn, options) {
|
|
635
|
+
const b = new StepList();
|
|
636
|
+
bodyFn(b);
|
|
637
|
+
this.steps.push({ kind: 'loop', name, collection: toExpr(collection), body: b.steps, ...(options ? { options } : {}) });
|
|
638
|
+
return this;
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Run the body, then repeat **while `condition` is true** — the condition is
|
|
642
|
+
* checked AFTER each iteration, so the body always runs at least once
|
|
643
|
+
* (`core.logic.dowhile`). The container publishes no data output; write
|
|
644
|
+
* results to a `.var()` from inside the body (`{ updates }`), and read the
|
|
645
|
+
* loop's progress nowhere — unlike `.loop()` there is no `currentItem`.
|
|
646
|
+
*
|
|
647
|
+
* @example
|
|
648
|
+
* **Paginate until the API says stop**
|
|
649
|
+
* ```ts
|
|
650
|
+
* .var('page', types.number, 1)
|
|
651
|
+
* .doWhile('paginate', js`$vars.fetch.output.hasNextPage === true`, (b) => b
|
|
652
|
+
* .step('fetch', http({ url: tmpl`https://api.example.test/items?page=${v('page')}`, method: 'GET', managed: false, returns: { hasNextPage: 'boolean' } }), {
|
|
653
|
+
* updates: { page: js`$vars.page + 1` },
|
|
654
|
+
* }), { limit: 50 })
|
|
655
|
+
* ```
|
|
656
|
+
*
|
|
657
|
+
* @param name - The container node's id.
|
|
658
|
+
* @param condition - Repeat while this is true, checked after each pass.
|
|
659
|
+
* @param bodyFn - Receives a sub-builder for the body, run once per pass.
|
|
660
|
+
* @param options - `limit` (1–10,000; blank = platform default 10,000),
|
|
661
|
+
* `breakEnabled`, and the common node options (see {@link DoWhileOptions}).
|
|
662
|
+
* @returns This builder, positioned after the loop.
|
|
663
|
+
*/
|
|
664
|
+
doWhile(name, condition, bodyFn, options) {
|
|
665
|
+
const b = new StepList();
|
|
666
|
+
bodyFn(b);
|
|
667
|
+
this.steps.push({ kind: 'doWhile', name, condition: toExpr(condition), body: b.steps, ...(options ? { options } : {}) });
|
|
668
|
+
return this;
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Exit the enclosing `.loop()` / `.doWhile()` through its **break handle**,
|
|
672
|
+
* ending the whole loop now — not just this iteration. Terminal on its path:
|
|
673
|
+
* nothing may follow it, exactly like `.terminate()`. Using it enables the
|
|
674
|
+
* container's break handle automatically. Compilation fails when there is no
|
|
675
|
+
* enclosing container.
|
|
676
|
+
*
|
|
677
|
+
* @returns This builder. Nothing after this call on the same path can run.
|
|
678
|
+
*/
|
|
679
|
+
break() {
|
|
680
|
+
this.steps.push({ kind: 'break' });
|
|
681
|
+
return this;
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Stop the **whole run**, here and now — not just this path.
|
|
685
|
+
*
|
|
686
|
+
* @remarks
|
|
687
|
+
* That is the only difference from `.return()`, and it is a big one: inside a
|
|
688
|
+
* `.parallel` arm a terminate **aborts the sibling arms**, whatever they were
|
|
689
|
+
* in the middle of. A `.return()` in the same place would only end its own
|
|
690
|
+
* path and leave the others running.
|
|
691
|
+
*
|
|
692
|
+
* Nothing may follow a terminate: it publishes no value, and no step after it
|
|
693
|
+
* — in its arm or chained behind it — can ever run.
|
|
694
|
+
*
|
|
695
|
+
* What aborts, what the run reports, and what each local rung can prove:
|
|
696
|
+
* `references/terminate.md`.
|
|
697
|
+
*
|
|
698
|
+
* @example
|
|
699
|
+
* **Abort the sibling arm from inside a parallel**
|
|
700
|
+
* ```ts
|
|
701
|
+
* .parallel('branches', [
|
|
702
|
+
* (a) => a.terminate('stopEverything'),
|
|
703
|
+
* (b) => b.step('wait', delay({ duration: 'PT10S' })).return({ message: 'done' }),
|
|
704
|
+
* ])
|
|
705
|
+
* ```
|
|
706
|
+
*
|
|
707
|
+
* @param name - The terminate node's id.
|
|
708
|
+
* @param label - Text the designer shows on the node. Omit it for the default.
|
|
709
|
+
* @param options - Node options; `version` selects the exact definition
|
|
710
|
+
* version (see {@link NodeOptions}).
|
|
711
|
+
* @returns This builder. Nothing after this call on the same path can run.
|
|
712
|
+
*/
|
|
713
|
+
terminate(name, label, options) {
|
|
714
|
+
this.steps.push({ kind: 'terminate', name, ...(label !== undefined ? { label } : {}), ...(options ? { options } : {}) });
|
|
715
|
+
return this;
|
|
716
|
+
}
|
|
717
|
+
stepToRef(portOrTarget, maybeTarget) {
|
|
718
|
+
const port = maybeTarget === undefined ? 'output' : portOrTarget;
|
|
719
|
+
const target = maybeTarget === undefined ? portOrTarget : maybeTarget;
|
|
720
|
+
if (typeof target !== 'string' || target.length === 0) {
|
|
721
|
+
throw new TypeError(`.stepToRef() takes the NAME of the step to continue at — .stepToRef('priceIt') — and got ${JSON.stringify(target)}. `
|
|
722
|
+
+ `It is the same name you passed to .step(), so the edge can be wired after the whole flow is built.`);
|
|
723
|
+
}
|
|
724
|
+
// A non-default port leaves the PRECEDING step, so it is guarded the same way
|
|
725
|
+
// `.stepToList()` is; the default port continues the list and needs no source.
|
|
726
|
+
if (port !== 'output')
|
|
727
|
+
this.portSource(port, `.stepToRef('${port}', …)`);
|
|
728
|
+
this.steps.push({ kind: 'stepToRef', port, target });
|
|
729
|
+
return this;
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Terminate this path, binding flow outputs to expressions.
|
|
733
|
+
*
|
|
734
|
+
* @param values - One entry per declared flow output. A raw value is wrapped as
|
|
735
|
+
* a literal; an {@link Expr} is emitted as a reference.
|
|
736
|
+
* @param options - Node options; `version` selects the exact definition
|
|
737
|
+
* version (see {@link NodeOptions}).
|
|
738
|
+
* @returns This builder. This path has answered, so nothing after it runs.
|
|
739
|
+
*/
|
|
740
|
+
return(values = {}, options) {
|
|
741
|
+
const norm = {};
|
|
742
|
+
for (const [k, val] of Object.entries(values))
|
|
743
|
+
norm[k] = toExpr(val);
|
|
744
|
+
this.steps.push({ kind: 'return', name: 'return', values: norm, ...(options ? { options } : {}) });
|
|
745
|
+
return this;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* The sub-builder an ARM callback receives — a `StepList` that can also name
|
|
750
|
+
* itself with `.label(...)`.
|
|
751
|
+
*
|
|
752
|
+
* @remarks
|
|
753
|
+
* It exists so `.label()` is available exactly where it means something (a
|
|
754
|
+
* `.branch` or `.switch` arm) and NOT on the top-level flow, which names itself
|
|
755
|
+
* with `.name(...)`. The parent's `branch`/`switch` reads `armLabel` back after
|
|
756
|
+
* running the callback and lowers it to the node's arm label (`trueLabel` /
|
|
757
|
+
* `falseLabel` for a decision, the case `label` for a switch).
|
|
758
|
+
*/
|
|
759
|
+
class ArmBuilder extends StepList {
|
|
760
|
+
armLabel;
|
|
761
|
+
/**
|
|
762
|
+
* Name this arm. The label is what the designer shows on the arm's outgoing
|
|
763
|
+
* edge; it also survives a round-trip through Flow JSON, where a bare
|
|
764
|
+
* `"True"` / `"False"` / `Case "<value>"` would not carry the author's intent.
|
|
765
|
+
* Order-independent — call it anywhere in the arm's chain.
|
|
766
|
+
*
|
|
767
|
+
* @param text - The label the designer shows on this arm's outgoing edge.
|
|
768
|
+
* @returns This builder, so calls chain.
|
|
769
|
+
*/
|
|
770
|
+
label(text) {
|
|
771
|
+
this.armLabel = text;
|
|
772
|
+
return this;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
class FlowBuilder extends StepList {
|
|
776
|
+
_id;
|
|
777
|
+
_name;
|
|
778
|
+
_description;
|
|
779
|
+
_layout;
|
|
780
|
+
_stickyNotes;
|
|
781
|
+
_version = '1.0.0';
|
|
782
|
+
_schemaVersion;
|
|
783
|
+
_inputs = [];
|
|
784
|
+
_outputs = [];
|
|
785
|
+
_vars = [];
|
|
786
|
+
_trigger;
|
|
787
|
+
_triggerOptions;
|
|
788
|
+
_entryPoints = [];
|
|
789
|
+
_triggerId;
|
|
790
|
+
constructor(_id) {
|
|
791
|
+
super();
|
|
792
|
+
this._id = _id;
|
|
793
|
+
this._name = _id;
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Set the flow's display name.
|
|
797
|
+
*
|
|
798
|
+
* @param n - The name the designer shows. Defaults to the flow's id.
|
|
799
|
+
* @returns This builder, so calls chain.
|
|
800
|
+
* @defaultValue the flow's id
|
|
801
|
+
*/
|
|
802
|
+
name(n) {
|
|
803
|
+
this._name = n;
|
|
804
|
+
return this;
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Set the flow's description — persisted as the top-level `description`
|
|
808
|
+
* field of the emitted `.flow`, shown in the designer's flow header.
|
|
809
|
+
*
|
|
810
|
+
* @param text - What this flow does, for humans.
|
|
811
|
+
* @returns This builder, so calls chain.
|
|
812
|
+
*/
|
|
813
|
+
description(text) {
|
|
814
|
+
this._description = text;
|
|
815
|
+
return this;
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* Set designer layout — node positions/sizes and edge routes — keyed by the
|
|
819
|
+
* flow's own logical ids (step names, the trigger id, `end`). Optional pure
|
|
820
|
+
* metadata: nothing here changes what the flow does, and omitting it leaves
|
|
821
|
+
* the serializer's default arrangement. Unknown keys fail compilation rather
|
|
822
|
+
* than being dropped.
|
|
823
|
+
*
|
|
824
|
+
* @param layout - Node layout by step name, edge routes by `"from->to"`.
|
|
825
|
+
* @returns This builder, so calls chain.
|
|
826
|
+
*/
|
|
827
|
+
layout(layout) {
|
|
828
|
+
this._layout = layout;
|
|
829
|
+
return this;
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Pin free-text notes to the canvas, for whoever reads the diagram.
|
|
833
|
+
*
|
|
834
|
+
* @remarks
|
|
835
|
+
* Sticky notes are the only annotation `.flow` has. Each becomes a real
|
|
836
|
+
* `stickyNote` node, which the platform skips during validation and
|
|
837
|
+
* conversion and whose edges it drops — so a note can never change what the
|
|
838
|
+
* flow does, and `check` treats it as inert.
|
|
839
|
+
*
|
|
840
|
+
* Calling this twice REPLACES the set rather than appending, matching
|
|
841
|
+
* `.layout()`.
|
|
842
|
+
*
|
|
843
|
+
* @example
|
|
844
|
+
* ```ts
|
|
845
|
+
* .stickyNotes([
|
|
846
|
+
* { content: 'Rates refresh at 02:00 UTC', x: 240, y: 40 },
|
|
847
|
+
* { id: 'why-retry', content: 'The vendor 502s under load.', color: 'pink',
|
|
848
|
+
* x: 240, y: 320, width: 220, height: 120 },
|
|
849
|
+
* ])
|
|
850
|
+
* ```
|
|
851
|
+
*
|
|
852
|
+
* @param notes - The notes to pin. Position is required; `id`, `color` and
|
|
853
|
+
* size have defaults.
|
|
854
|
+
* @returns This builder, so calls chain.
|
|
855
|
+
*/
|
|
856
|
+
stickyNotes(notes) {
|
|
857
|
+
this._stickyNotes = notes;
|
|
858
|
+
return this;
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Set what starts the flow. Omit this call for the **manual** trigger (a
|
|
862
|
+
* caller starts the flow on demand) — that is the default and what most
|
|
863
|
+
* flows want.
|
|
864
|
+
*
|
|
865
|
+
* @example
|
|
866
|
+
* **Run the flow hourly instead of on demand**
|
|
867
|
+
* ```ts
|
|
868
|
+
* .trigger(scheduled({ every: 'R/PT1H' }))
|
|
869
|
+
* ```
|
|
870
|
+
*
|
|
871
|
+
* @param spec - What starts the flow, from `scheduled(...)` or `onEvent(...)`.
|
|
872
|
+
* @param options - Node options; `version` selects the exact definition
|
|
873
|
+
* version (see {@link NodeOptions}).
|
|
874
|
+
* @returns This builder, so calls chain.
|
|
875
|
+
* @defaultValue the manual trigger
|
|
876
|
+
*/
|
|
877
|
+
trigger(spec, options) {
|
|
878
|
+
if (options?.updates) {
|
|
879
|
+
throw new TypeError('.trigger(): { updates } is not supported — a trigger has not run anything to assign from.');
|
|
880
|
+
}
|
|
881
|
+
this._trigger = spec;
|
|
882
|
+
if (options)
|
|
883
|
+
this._triggerOptions = options;
|
|
884
|
+
return this;
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* Add an ADDITIONAL flow root: its own trigger node, its own
|
|
888
|
+
* scoped inputs (read them with `entryInput('<id>', '<name>')`), and an
|
|
889
|
+
* optional prefix that runs before the root joins the shared body. Without a
|
|
890
|
+
* prefix the root connects straight to the first shared step; a prefix that
|
|
891
|
+
* ends terminally (or hands off with `.stepToRef()`) joins nothing.
|
|
892
|
+
*
|
|
893
|
+
* @remarks
|
|
894
|
+
* The DEFAULT root stays `.trigger()` / `.input()` — one flow, one default
|
|
895
|
+
* entry point, marked `isDefaultEntryPoint` in the emitted file. Every root
|
|
896
|
+
* (default included) gets its platform entry-point identity when a flow has
|
|
897
|
+
* more than one.
|
|
898
|
+
*
|
|
899
|
+
* @example
|
|
900
|
+
* **A manual flow with a second, nightly-scheduled root**
|
|
901
|
+
* ```ts
|
|
902
|
+
* flow('order-intake')
|
|
903
|
+
* .input({ order: types.object }) // the default (manual) root
|
|
904
|
+
* .entryPoint('nightly', scheduled({ every: 'R/P1D' }), {
|
|
905
|
+
* inputs: { batchDate: types.string },
|
|
906
|
+
* }, (b) => b.step('loadBatch', script({ code: 'return { order: { id: $vars.nightly.output.batchDate } };', returns: 'object' })))
|
|
907
|
+
* .step('normalize', script({ code: 'return 1;' })) // shared body
|
|
908
|
+
* ```
|
|
909
|
+
*
|
|
910
|
+
* @param id - The root's trigger-node id — unique among entry points, steps,
|
|
911
|
+
* and the default trigger id. Expressions read it as `$vars.<id>.output.*`.
|
|
912
|
+
* @param trigger - What fires this root: `manual()`, `scheduled(...)`, or
|
|
913
|
+
* `onEvent(...)`.
|
|
914
|
+
* @param options - `version` (exact trigger definition version) and `inputs`
|
|
915
|
+
* — this root's scoped inputs, declared like `.input()`.
|
|
916
|
+
* @param prefixFn - Optional sub-builder for the root's prefix steps.
|
|
917
|
+
* @returns This builder, so calls chain.
|
|
918
|
+
*/
|
|
919
|
+
entryPoint(id, trigger, options, prefixFn) {
|
|
920
|
+
const inputs = [];
|
|
921
|
+
for (const [name, value] of Object.entries(options?.inputs ?? {})) {
|
|
922
|
+
inputs.push({ name, ...varDeclOf('input', name, value) });
|
|
923
|
+
}
|
|
924
|
+
const b = new StepList();
|
|
925
|
+
prefixFn?.(b);
|
|
926
|
+
this._entryPoints.push({
|
|
927
|
+
id,
|
|
928
|
+
trigger: trigger,
|
|
929
|
+
...(options?.version !== undefined ? { options: { version: options.version } } : {}),
|
|
930
|
+
inputs,
|
|
931
|
+
steps: b.steps,
|
|
932
|
+
});
|
|
933
|
+
return this;
|
|
934
|
+
}
|
|
935
|
+
/**
|
|
936
|
+
* Rename the trigger node. The designer can rename it, so the SDK can too.
|
|
937
|
+
*
|
|
938
|
+
* @remarks
|
|
939
|
+
* The id is not cosmetic: a flow's declared inputs are published as the trigger
|
|
940
|
+
* node's output, so `input('amount')` reads `$vars.<triggerId>.output.amount`.
|
|
941
|
+
* Renaming the node rewrites every such reference with it.
|
|
942
|
+
*
|
|
943
|
+
* @example
|
|
944
|
+
* **Rename the trigger; input reads follow**
|
|
945
|
+
* ```ts
|
|
946
|
+
* .triggerId('intake') // input('amount') → $vars.intake.output.amount
|
|
947
|
+
* ```
|
|
948
|
+
*
|
|
949
|
+
* @param id - The trigger node's new id. Every `input(...)` reference is
|
|
950
|
+
* rewritten to read through it.
|
|
951
|
+
* @returns This builder, so calls chain.
|
|
952
|
+
* @defaultValue `'start'`
|
|
953
|
+
*/
|
|
954
|
+
triggerId(id) {
|
|
955
|
+
this._triggerId = id;
|
|
956
|
+
return this;
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* Set the flow's version.
|
|
960
|
+
*
|
|
961
|
+
* @param vsn - The version string. It does NOT reach the emitted `.flow`.
|
|
962
|
+
* @returns This builder, so calls chain.
|
|
963
|
+
* @deprecated Top-level `version` in a `.flow` file is the FILE-FORMAT
|
|
964
|
+
* version, owned by the serializer's format profile (design §5.4) — it was
|
|
965
|
+
* never an author-controlled artifact version, and the serializer has always
|
|
966
|
+
* overwritten this value. The call is kept for source compatibility and
|
|
967
|
+
* ignored. To target a specific file-format version, use
|
|
968
|
+
* {@link FlowBuilder.schemaVersion}; there is no author-controlled artifact
|
|
969
|
+
* version in the format for this to have meant.
|
|
970
|
+
*/
|
|
971
|
+
version(vsn) {
|
|
972
|
+
this._version = vsn;
|
|
973
|
+
return this;
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* Declare the `.flow` FILE-FORMAT (schema) version this flow targets.
|
|
977
|
+
*
|
|
978
|
+
* @remarks
|
|
979
|
+
* This is the document's grammar version — the top-level `version` a `.flow`
|
|
980
|
+
* carries — and it is the one version concept that IS the author's to set,
|
|
981
|
+
* which is exactly what {@link FlowBuilder.version} is not. Omit it and the
|
|
982
|
+
* flow declares the SDK's default (the fleet write floor, `1.9`), which is
|
|
983
|
+
* what every flow authored before this method existed already did.
|
|
984
|
+
*
|
|
985
|
+
* Its real job is BROWNFIELD PRESERVATION. `flow-decompile` emits this call
|
|
986
|
+
* with the version it read, so a decompile → edit → compile round-trip
|
|
987
|
+
* re-declares the file's own version instead of silently rewriting a `1.10`
|
|
988
|
+
* file as `1.9`. Migrating a document BETWEEN versions stays
|
|
989
|
+
* `uip maestro flow migrate`'s job: a hop can carry data repairs (the
|
|
990
|
+
* 1.9 → 1.10 hop repairs connector binding names) that this SDK has no way to
|
|
991
|
+
* reproduce from the source alone.
|
|
992
|
+
*
|
|
993
|
+
* Only versions this build can actually write are accepted — `1.6` through
|
|
994
|
+
* `1.10`, the band whose schemas are the same shape as what the serializer
|
|
995
|
+
* emits. A version is a CLAIM that the whole document matches that schema,
|
|
996
|
+
* not a switch that reshapes it, so declaring one the serializer cannot emit
|
|
997
|
+
* would write a false claim; the call throws instead. Both directions off the
|
|
998
|
+
* floor cost something — above it, a reader pinned lower (an older CLI, an
|
|
999
|
+
* Automation Suite ring) refuses the file; below it, every reader migrates
|
|
1000
|
+
* forward on open — so `check()` reports `SCHEMA_VERSION_ABOVE_FLOOR` /
|
|
1001
|
+
* `SCHEMA_VERSION_BELOW_FLOOR` rather than deciding for you.
|
|
1002
|
+
*
|
|
1003
|
+
* @example
|
|
1004
|
+
* **Round-trip a 1.10 flow without downgrading it**
|
|
1005
|
+
* ```ts
|
|
1006
|
+
* flow('claims').schemaVersion('1.10')
|
|
1007
|
+
* ```
|
|
1008
|
+
*
|
|
1009
|
+
* @param version - A writable file-format version, e.g. `'1.9'` or `'1.10'`.
|
|
1010
|
+
* @returns This builder, so calls chain.
|
|
1011
|
+
* @throws Error when this SDK build cannot write `version`.
|
|
1012
|
+
* @defaultValue `'1.9'` — `FLOW_FORMAT_PROFILE.version`, the fleet write floor.
|
|
1013
|
+
*/
|
|
1014
|
+
schemaVersion(version) {
|
|
1015
|
+
const refusal = schemaVersionRefusal(FLOW_SCHEMA_VERSIONS, version);
|
|
1016
|
+
if (refusal)
|
|
1017
|
+
throw new Error(refusal);
|
|
1018
|
+
this._schemaVersion = version;
|
|
1019
|
+
return this;
|
|
1020
|
+
}
|
|
1021
|
+
/**
|
|
1022
|
+
* Declare the flow's inputs. Read them with `input('<name>')`.
|
|
1023
|
+
*
|
|
1024
|
+
* @param shape - Input names to type descriptors, e.g. `{ amount: types.number }`.
|
|
1025
|
+
* @returns This builder, so calls chain.
|
|
1026
|
+
* @see input
|
|
1027
|
+
*/
|
|
1028
|
+
input(shape) {
|
|
1029
|
+
for (const [name, value] of Object.entries(assertShape('input', shape))) {
|
|
1030
|
+
this._inputs.push({ name, ...varDeclOf('input', name, value) });
|
|
1031
|
+
}
|
|
1032
|
+
return this;
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Declare the flow's outputs — what `.return(...)` binds values to.
|
|
1036
|
+
*
|
|
1037
|
+
* @param shape - Output names to type descriptors, e.g. `{ total: types.number }`.
|
|
1038
|
+
* @returns This builder, so calls chain.
|
|
1039
|
+
*/
|
|
1040
|
+
output(shape) {
|
|
1041
|
+
for (const [name, value] of Object.entries(assertShape('output', shape))) {
|
|
1042
|
+
this._outputs.push({ name, ...varDeclOf('output', name, value) });
|
|
1043
|
+
}
|
|
1044
|
+
return this;
|
|
1045
|
+
}
|
|
1046
|
+
/**
|
|
1047
|
+
* Declare a flow-level variable. Read it with `v('<name>')`.
|
|
1048
|
+
*
|
|
1049
|
+
* @param name - The variable's name.
|
|
1050
|
+
* @param type - One of the `types.*` descriptors.
|
|
1051
|
+
* @param defaultValue - Its initial value. Omit it to start undeclared.
|
|
1052
|
+
* @returns This builder, so calls chain.
|
|
1053
|
+
* @see v
|
|
1054
|
+
*/
|
|
1055
|
+
var(name, type, defaultValue) {
|
|
1056
|
+
const decl = varDeclOf('var', name, type);
|
|
1057
|
+
if (defaultValue !== undefined)
|
|
1058
|
+
decl.default = defaultValue;
|
|
1059
|
+
this._vars.push({ name, ...decl });
|
|
1060
|
+
return this;
|
|
1061
|
+
}
|
|
1062
|
+
/**
|
|
1063
|
+
* Finish the flow and return the graph the compiler serializes.
|
|
1064
|
+
*
|
|
1065
|
+
* @returns The built description — inputs, outputs, variables, trigger and steps.
|
|
1066
|
+
* @throws FlowBuildError when an expression references something undeclared.
|
|
1067
|
+
*/
|
|
1068
|
+
build() {
|
|
1069
|
+
const built = {
|
|
1070
|
+
id: this._id,
|
|
1071
|
+
name: this._name,
|
|
1072
|
+
...(this._description !== undefined ? { description: this._description } : {}),
|
|
1073
|
+
...(this._layout !== undefined ? { layout: this._layout } : {}),
|
|
1074
|
+
...(this._stickyNotes !== undefined ? { stickyNotes: this._stickyNotes } : {}),
|
|
1075
|
+
version: this._version,
|
|
1076
|
+
...(this._schemaVersion !== undefined ? { schemaVersion: this._schemaVersion } : {}),
|
|
1077
|
+
inputs: this._inputs,
|
|
1078
|
+
outputs: this._outputs,
|
|
1079
|
+
vars: this._vars,
|
|
1080
|
+
steps: this.steps,
|
|
1081
|
+
...(this._trigger !== undefined ? { trigger: this._trigger } : {}),
|
|
1082
|
+
...(this._triggerOptions !== undefined ? { triggerOptions: this._triggerOptions } : {}),
|
|
1083
|
+
...(this._entryPoints.length ? { entryPoints: this._entryPoints } : {}),
|
|
1084
|
+
...(this._triggerId !== undefined ? { triggerId: this._triggerId } : {}),
|
|
1085
|
+
};
|
|
1086
|
+
// First-level validation, intrinsic to running the SDK: catch a definitely-
|
|
1087
|
+
// broken expression HERE, before the model is serialized, rather than three
|
|
1088
|
+
// steps later at `uip maestro flow validate`. Throws so nothing invalid is
|
|
1089
|
+
// written; the deeper checks stay with the validate CLI. See flow-expr-check.
|
|
1090
|
+
const errors = checkFlowExpressions(built).filter((d) => d.level === 'error');
|
|
1091
|
+
if (errors.length)
|
|
1092
|
+
throw new FlowBuildError(built, errors);
|
|
1093
|
+
return built;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1097
|
+
* Start building a flow with the given id.
|
|
1098
|
+
*
|
|
1099
|
+
* @param id - The flow's stable identifier, and its display name until
|
|
1100
|
+
* `.name(...)` says otherwise.
|
|
1101
|
+
* @returns A {@link FlowBuilder} to declare inputs and chain steps onto.
|
|
1102
|
+
* @example
|
|
1103
|
+
* ```ts
|
|
1104
|
+
* export default flow('hello').name('Hello')
|
|
1105
|
+
* .output({ greeting: types.string })
|
|
1106
|
+
* .step('greet', script({ code: 'return "hi";', returns: 'string' }))
|
|
1107
|
+
* .return({ greeting: out('greet') })
|
|
1108
|
+
* .build();
|
|
1109
|
+
* ```
|
|
1110
|
+
*/
|
|
1111
|
+
export function flow(id) {
|
|
1112
|
+
return new FlowBuilder(id);
|
|
1113
|
+
}
|
|
1114
|
+
/**
|
|
1115
|
+
* Run another flow as one step, and read back what it returns.
|
|
1116
|
+
*
|
|
1117
|
+
* @remarks
|
|
1118
|
+
* The child is an ordinary `flow()` in the same file — its own inputs, steps
|
|
1119
|
+
* and `.return()`. Don't call `.build()` on it; the parent's `.build()` collects
|
|
1120
|
+
* it. Two scopes, joined only by this call: the child sees NOTHING of the
|
|
1121
|
+
* parent's variables except the values passed in here.
|
|
1122
|
+
*
|
|
1123
|
+
* `inputs` keys are the CHILD's declared input names; the values are
|
|
1124
|
+
* parent-scope expressions. A subflow's output is an OBJECT of the child's
|
|
1125
|
+
* declared outputs, so read it with the output's name —
|
|
1126
|
+
* `out('callReverse', 'reversed')`, never a bare `out('callReverse')`.
|
|
1127
|
+
*
|
|
1128
|
+
* Scopes, reuse, nesting, and what a local run does and does not prove:
|
|
1129
|
+
* `references/subflow.md`.
|
|
1130
|
+
*
|
|
1131
|
+
* @example
|
|
1132
|
+
* **Call a child flow and read its named output**
|
|
1133
|
+
* ```ts
|
|
1134
|
+
* const reverse = flow('reverse-text')
|
|
1135
|
+
* .input({ text: types.string })
|
|
1136
|
+
* .output({ reversed: types.string })
|
|
1137
|
+
* .step('rev', script({ code: 'return $vars.text.split("").reverse().join("");' }))
|
|
1138
|
+
* .return({ reversed: out('rev') });
|
|
1139
|
+
*
|
|
1140
|
+
* export default flow('reverse-outer')
|
|
1141
|
+
* .input({ text: types.string })
|
|
1142
|
+
* .output({ result: types.string })
|
|
1143
|
+
* .step('callReverse', subflow(reverse, { text: input('text') }))
|
|
1144
|
+
* .return({ result: out('callReverse', 'reversed') }) // NAME the child output
|
|
1145
|
+
* .build();
|
|
1146
|
+
* ```
|
|
1147
|
+
*
|
|
1148
|
+
* @param child - The child flow — a `flow()` builder or an already-built flow.
|
|
1149
|
+
* @param inputs - One value per the CHILD's declared input names, as parent-scope
|
|
1150
|
+
* expressions.
|
|
1151
|
+
* @returns An action spec for `.step(...)`.
|
|
1152
|
+
*/
|
|
1153
|
+
export function subflow(child, inputs = {}) {
|
|
1154
|
+
// Accept the builder OR an already-built flow. Authors reach for the builder
|
|
1155
|
+
// (the parent's `.build()` is the file's one terminal call), and a stray
|
|
1156
|
+
// `.build()` on the child should not be an error worth a diagnostic.
|
|
1157
|
+
const built = typeof child?.build === 'function'
|
|
1158
|
+
? child.build()
|
|
1159
|
+
: child;
|
|
1160
|
+
const norm = {};
|
|
1161
|
+
for (const [k, val] of Object.entries(inputs ?? {}))
|
|
1162
|
+
norm[k] = toExpr(val);
|
|
1163
|
+
return builtinActionSpec({ kind: 'subflow', child: built, inputs: norm });
|
|
1164
|
+
}
|