@uipath/maestro-builder-sdk 6.21.2 → 6.22.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 +138 -138
- package/dist/api-members.md +407 -407
- package/dist/check.js +43 -331
- package/dist/core/actions.d.ts +0 -147
- package/dist/core/actions.js +0 -15
- package/dist/core/expr.d.ts +1 -1
- package/dist/core/expr.js +2 -2
- package/dist/core/nodes/node-type.d.ts +15 -0
- package/dist/core/step-ports.js +7 -23
- package/dist/core/typed-raw-node.js +0 -2
- package/dist/decompile.js +44 -88
- package/dist/flow-node-registry.js +2 -1
- package/dist/flow-node-type.d.ts +59 -0
- package/dist/flow-sdk.d.ts +1 -0
- package/dist/flow-sdk.js +1 -0
- package/dist/flow-step-exits.d.ts +12 -0
- package/dist/flow-step-exits.js +30 -0
- package/dist/nodes/http/flow/check.d.ts +58 -0
- package/dist/nodes/http/flow/check.js +248 -0
- package/dist/nodes/http/flow/factories.d.ts +14 -0
- package/dist/nodes/http/flow/factories.js +20 -0
- package/dist/nodes/http/flow/family.d.ts +8 -0
- package/dist/nodes/http/flow/family.js +13 -0
- package/dist/nodes/http/flow/index.d.ts +3 -0
- package/dist/nodes/http/flow/index.js +3 -0
- package/dist/nodes/http/flow/node-type.d.ts +7 -0
- package/dist/nodes/http/flow/node-type.js +411 -0
- package/dist/nodes/http/flow/spec.d.ts +151 -0
- package/dist/nodes/http/flow/spec.js +1 -0
- package/dist/serialize.d.ts +2 -2
- package/dist/serialize.js +20 -276
- package/package.json +2 -2
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The exits the author declared on a step — what `.stepSwitch()` routes over and
|
|
3
|
+
* a `.stepToList('<port>')` names — answered by the step's node type when its kind
|
|
4
|
+
* is registered (`FlowNodeType.exits`), else by `core/step-ports.ts` for a kind
|
|
5
|
+
* not yet ported. A module of its own so a test that swaps the registry
|
|
6
|
+
* (`vi.mock('./flow-node-registry.js')`) swaps what this asks too.
|
|
7
|
+
*/
|
|
8
|
+
import { resolveVersion } from './core/nodes/versioned.js';
|
|
9
|
+
import { declaredExits } from './core/step-ports.js';
|
|
10
|
+
import { FLOW_NODE_TYPES } from './flow-node-registry.js';
|
|
11
|
+
/**
|
|
12
|
+
* The author-declared exits of a step's spec: a registered type's own `exits`,
|
|
13
|
+
* at the version the step resolves to (its pin, else the type's choice; an
|
|
14
|
+
* unregistered pin falls back to that choice, as check's other rules do), and
|
|
15
|
+
* `declaredExits` for a kind not yet registered. `serialize` and `check` ask
|
|
16
|
+
* here, so they agree on what a port means.
|
|
17
|
+
*/
|
|
18
|
+
export function stepExits(spec, pinnedVersion) {
|
|
19
|
+
const entry = typeof spec?.kind === 'string' ? FLOW_NODE_TYPES.byKind(spec.kind, spec) : undefined;
|
|
20
|
+
if (!entry)
|
|
21
|
+
return declaredExits(spec, pinnedVersion);
|
|
22
|
+
let impl;
|
|
23
|
+
try {
|
|
24
|
+
impl = resolveVersion(entry.type, spec, pinnedVersion, 'exits');
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
impl = resolveVersion(entry.type, spec, undefined, 'exits');
|
|
28
|
+
}
|
|
29
|
+
return impl.exits?.(spec) ?? declaredExits(spec, pinnedVersion);
|
|
30
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The HTTP family's rules. Moved from check.ts in Phase 4 of
|
|
3
|
+
* docs/NODE_TYPE_ARCHITECTURE_DESIGN.md; codes and messages unchanged.
|
|
4
|
+
*/
|
|
5
|
+
import type { DiagnosticCatalog } from '../../../core/nodes/family.js';
|
|
6
|
+
import type { OutputPathRead } from '../../../core/nodes/node-type.js';
|
|
7
|
+
import type { FlowCheckContext } from '../../../flow-node-type.js';
|
|
8
|
+
import type { HttpInputs } from './spec.js';
|
|
9
|
+
export declare const HTTP_DIAGNOSTICS: DiagnosticCatalog;
|
|
10
|
+
/**
|
|
11
|
+
* The two http settings every deployed request node carries.
|
|
12
|
+
*
|
|
13
|
+
* Both definitions declare the defaults (`timeout: 'PT15M'`, `retryCount: 0`), so
|
|
14
|
+
* omitting them is safe and is what ~99% of deployed nodes do. What is NOT safe
|
|
15
|
+
* is writing them in a form the platform cannot read: an unparseable duration is
|
|
16
|
+
* not a shorter timeout, it is a value the request node fails to configure with,
|
|
17
|
+
* and nothing downstream of `check` would say so in the author's own vocabulary.
|
|
18
|
+
*/
|
|
19
|
+
export declare function checkHttp(inputs: HttpInputs, ctx: FlowCheckContext): void;
|
|
20
|
+
/**
|
|
21
|
+
* Every node-local rule of an `http()` step, in the order check.ts ran them: the
|
|
22
|
+
* request settings, a managed step's binding labels, then the response branches.
|
|
23
|
+
*/
|
|
24
|
+
export declare function checkHttpStep(inputs: HttpInputs, ctx: FlowCheckContext): void;
|
|
25
|
+
/**
|
|
26
|
+
* A read of an `http()` step's result.
|
|
27
|
+
*
|
|
28
|
+
* The response is an envelope — `{ body, statusCode, headers }` — and `returns`
|
|
29
|
+
* declares the parsed JSON body's top-level fields, so a declared field is read
|
|
30
|
+
* at `output.body.<field>` (`out('<step>', 'body.<field>')`). That nesting is the
|
|
31
|
+
* one thing this family does not share with the process families, and it is
|
|
32
|
+
* exactly where flow-builder-sdk#850's run went wrong: a branch kept reading
|
|
33
|
+
* `output.currentDayForecast…` (the Azure Maps connector's path) after the step
|
|
34
|
+
* became an Open-Meteo `http()`, passed `check` and `validate`, and faulted at
|
|
35
|
+
* `flow debug` as `[400302]`.
|
|
36
|
+
*/
|
|
37
|
+
export declare function checkHttpRead({ node: name, field, rest, reader: step }: OutputPathRead, spec: HttpInputs | undefined, report: (code: string, message: string, suggestion?: string) => void): void;
|
|
38
|
+
/**
|
|
39
|
+
* `.onError` after a plain `core.action.http` node. Two separate facts about a
|
|
40
|
+
* PLAIN http step, both measured, both fatal to a handler there:
|
|
41
|
+
*
|
|
42
|
+
* 1. a 4xx/5xx does not FAIL the node — it completes carrying `statusCode`,
|
|
43
|
+
* so the handler only ever runs on a transport failure (DNS, refused,
|
|
44
|
+
* timeout);
|
|
45
|
+
* 2. when it does run there is nothing to read. `core.action.http@1.0.0` and
|
|
46
|
+
* the `@1.3` that `uip maestro flow migrate` upgrades it to both publish
|
|
47
|
+
* `<step>.error = true` and leave `<step>.output` null — no envelope
|
|
48
|
+
* anywhere, unlike every other family. `validate` still says Valid.
|
|
49
|
+
*
|
|
50
|
+
* So this stays an error, and says the second part too: an author who reads
|
|
51
|
+
* the old message ("use the managed node if statuses must route") could
|
|
52
|
+
* reasonably conclude a transport-failure handler is fine as it is. check.ts
|
|
53
|
+
* reports the same refusal for a custom node built on the plain definition.
|
|
54
|
+
*/
|
|
55
|
+
export declare function plainHttpOnErrorMessage(stepName: string): {
|
|
56
|
+
message: string;
|
|
57
|
+
suggestion: string;
|
|
58
|
+
};
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { nearestName } from '../../../core/edit-distance.js';
|
|
2
|
+
export const HTTP_DIAGNOSTICS = {
|
|
3
|
+
HTTP_V1_RETIRED: { severity: 'warning', summary: 'the plain node is retired on the tenant registry' },
|
|
4
|
+
HTTP_BINDING_REQUIRES_MANAGED: { severity: 'error', summary: 'connection settings on the standalone node' },
|
|
5
|
+
HTTP_MANAGED_BINDING_INCOMPLETE: { severity: 'error', summary: 'a managed connection without both connection and folder' },
|
|
6
|
+
HTTP_TARGET_CONNECTOR_EMPTY: { severity: 'error', summary: 'an empty targetConnector' },
|
|
7
|
+
HTTP_TARGET_CONNECTOR_REQUIRES_BINDING: { severity: 'error', summary: 'targetConnector without a connection and folder' },
|
|
8
|
+
HTTP_TIMEOUT_NOT_DURATION: { severity: 'error', summary: 'timeout is not an ISO-8601 duration' },
|
|
9
|
+
HTTP_RETRY_COUNT_INVALID: { severity: 'error', summary: 'retryCount is not a non-negative integer' },
|
|
10
|
+
HTTP_RETRY_COUNT_HIGH: { severity: 'warning', summary: 'retryCount above what a local replay exercises' },
|
|
11
|
+
HTTP_BRANCH_NAME: { severity: 'error', summary: 'a branch name that is not port-safe' },
|
|
12
|
+
HTTP_BRANCH_DUP: { severity: 'error', summary: 'a branch declared twice' },
|
|
13
|
+
HTTP_BRANCH_CONST: { severity: 'error', summary: 'a branch condition that is a constant' },
|
|
14
|
+
HTTP_READ_NOT_OUTPUT: { severity: 'error', summary: 'a read of the step outside `output`' },
|
|
15
|
+
HTTP_READ_UNKNOWN_FIELD: { severity: 'error', summary: 'a read outside the response envelope or the declared body' },
|
|
16
|
+
HTTP_READ_WITHOUT_RETURNS: { severity: 'error', summary: 'a body field read with no `returns`' },
|
|
17
|
+
HTTP_ONERROR_V1: { severity: 'error', summary: '.onError() on the plain node' },
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* An ISO-8601 DURATION, which is what both http definitions declare `timeout` to
|
|
21
|
+
* be (`PT15M` by default) and what all 808 deployed http nodes carry.
|
|
22
|
+
*
|
|
23
|
+
* Deliberately the whole grammar rather than the `PT<n>[HMS]` subset the corpus
|
|
24
|
+
* happens to use: a too-narrow fence would refuse `P1D` or `PT1H30M`, which the
|
|
25
|
+
* platform parses perfectly well. What it does refuse is the family of things
|
|
26
|
+
* that are NOT durations at all and would reach the platform as an unparseable
|
|
27
|
+
* string — `'30s'`, `'30'`, `'PT'`, `'15 minutes'`.
|
|
28
|
+
*/
|
|
29
|
+
const ISO8601_DURATION = /^P(?!$)(\d+(?:\.\d+)?Y)?(\d+(?:\.\d+)?M)?(\d+(?:\.\d+)?W)?(\d+(?:\.\d+)?D)?(T(?!$)(\d+(?:\.\d+)?H)?(\d+(?:\.\d+)?M)?(\d+(?:\.\d+)?S)?)?$/;
|
|
30
|
+
/**
|
|
31
|
+
* Where `check` stops treating a retry count as plainly reasonable. The
|
|
32
|
+
* platform's own ceiling is NOT measured, and the deployed corpus's largest
|
|
33
|
+
* author-set value is 3 — so above this we WARN. A too-narrow fence is fatal
|
|
34
|
+
* (it refuses something that works); a too-wide one costs nothing.
|
|
35
|
+
*/
|
|
36
|
+
const RETRY_COUNT_SOFT_MAX = 5;
|
|
37
|
+
/**
|
|
38
|
+
* The two http settings every deployed request node carries.
|
|
39
|
+
*
|
|
40
|
+
* Both definitions declare the defaults (`timeout: 'PT15M'`, `retryCount: 0`), so
|
|
41
|
+
* omitting them is safe and is what ~99% of deployed nodes do. What is NOT safe
|
|
42
|
+
* is writing them in a form the platform cannot read: an unparseable duration is
|
|
43
|
+
* not a shorter timeout, it is a value the request node fails to configure with,
|
|
44
|
+
* and nothing downstream of `check` would say so in the author's own vocabulary.
|
|
45
|
+
*/
|
|
46
|
+
export function checkHttp(inputs, ctx) {
|
|
47
|
+
const step = ctx.step;
|
|
48
|
+
// `core.action.http` is retired on the platform side: after `registry pull
|
|
49
|
+
// --force` the tenant serves only `core.action.http.v2` (2.3), and a
|
|
50
|
+
// `registry get core.action.http` — bare or at the `@1.3` that `flow migrate`
|
|
51
|
+
// upgrades it to — answers "Node not found". It still compiles, validates and
|
|
52
|
+
// runs, which is why this is a warning and not an error: existing flows are
|
|
53
|
+
// not broken and `decompile` must keep round-tripping them.
|
|
54
|
+
//
|
|
55
|
+
// Moving to the managed node is not a rename. A plain step keeps a non-2xx on
|
|
56
|
+
// the SUCCESS path (`out(step,'statusCode')`); the managed one fails on it, so
|
|
57
|
+
// a status branch becomes an error handler reading the envelope's `status`.
|
|
58
|
+
//
|
|
59
|
+
// It is not a free upgrade either, and for a connection-less URL it is usually
|
|
60
|
+
// the wrong move (flow-builder-sdk#898). With no `connection` the managed node
|
|
61
|
+
// still goes through Integration Service: serialize emits the `uipath-uipath-http`
|
|
62
|
+
// connector on `ImplicitConnection`, so the request leaves from IS's SHARED
|
|
63
|
+
// egress. Measured on slack-weather-pipeline (3/3 runs): agents took the old
|
|
64
|
+
// `managed: true` hint, and Open-Meteo answered that egress 429 "Daily API
|
|
65
|
+
// request limit exceeded" while direct calls from the same box got 200. The
|
|
66
|
+
// plain node goes direct. So the hint only steers toward `managed: true` when
|
|
67
|
+
// the step was given connection settings; the two other real reasons to move
|
|
68
|
+
// (a binding, an `.onError()`) already fire as errors with that suggestion
|
|
69
|
+
// (HTTP_BINDING_REQUIRES_MANAGED, HTTP_ONERROR_V1).
|
|
70
|
+
if (inputs && !inputs.managed) {
|
|
71
|
+
const asked = inputs;
|
|
72
|
+
const wantsConnection = asked.connection !== undefined || asked.folder !== undefined
|
|
73
|
+
|| asked.targetConnector !== undefined;
|
|
74
|
+
if (wantsConnection) {
|
|
75
|
+
ctx.report('HTTP_V1_RETIRED', `Step "${step}" emits core.action.http, which the tenant registry no longer serves (only `
|
|
76
|
+
+ `core.action.http.v2). It sets Integration Service connection settings, which only the managed node `
|
|
77
|
+
+ `can use. Moving to managed: true changes behaviour rather than just the node type: a non-2xx stops `
|
|
78
|
+
+ `arriving on the success path with statusCode and fails the step instead, so a status branch becomes `
|
|
79
|
+
+ `an .onError() handler reading err('${step}', 'status').`, `http({ url: …, managed: true, connection, folder })`);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
ctx.report('HTTP_V1_RETIRED', `Step "${step}" emits core.action.http, which the tenant registry no longer serves (only `
|
|
83
|
+
+ `core.action.http.v2). It still compiles, validates and runs, and it calls the URL directly. For a URL `
|
|
84
|
+
+ `that needs no Integration Service connection, keep managed: false. managed: true is not a drop-in `
|
|
85
|
+
+ `replacement: even without a connection it sends the request through Integration Service's shared `
|
|
86
|
+
+ `egress (rate limits there are shared, and public APIs can answer 429 where a direct call succeeds), `
|
|
87
|
+
+ `and a non-2xx fails the step instead of arriving with statusCode. Switch only if the call needs an `
|
|
88
|
+
+ `Integration Service connection or an .onError() handler.`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const connection = inputs?.connection;
|
|
92
|
+
const folder = inputs?.folder;
|
|
93
|
+
const targetConnector = inputs?.targetConnector;
|
|
94
|
+
const bindingConfigured = connection !== undefined || folder !== undefined;
|
|
95
|
+
const targetConfigured = targetConnector !== undefined;
|
|
96
|
+
const nonEmpty = (value) => typeof value === 'string' && value.trim().length > 0;
|
|
97
|
+
if (!inputs?.managed && (bindingConfigured || targetConfigured)) {
|
|
98
|
+
ctx.report('HTTP_BINDING_REQUIRES_MANAGED', `Step "${step}" supplies Integration Service connection settings to the standalone HTTP node. `
|
|
99
|
+
+ `Only the managed node can use connection, folder, or targetConnector.`, `managed: true`);
|
|
100
|
+
}
|
|
101
|
+
else if (inputs?.managed && bindingConfigured && (!nonEmpty(connection) || !nonEmpty(folder))) {
|
|
102
|
+
ctx.report('HTTP_MANAGED_BINDING_INCOMPLETE', `Managed HTTP step "${step}" must name both a non-empty \`connection\` and \`folder\`. The platform `
|
|
103
|
+
+ `needs the pair to resolve the Integration Service connection at runtime.`, `connection: 'http', folder: 'shared'`);
|
|
104
|
+
}
|
|
105
|
+
if (inputs?.managed && targetConfigured && !nonEmpty(targetConnector)) {
|
|
106
|
+
ctx.report('HTTP_TARGET_CONNECTOR_EMPTY', `Managed HTTP step "${step}" sets an empty \`targetConnector\`; name its Integration Service connector key.`, `targetConnector: 'uipath-salesforce-slack'`);
|
|
107
|
+
}
|
|
108
|
+
else if (inputs?.managed && targetConfigured && !bindingConfigured) {
|
|
109
|
+
ctx.report('HTTP_TARGET_CONNECTOR_REQUIRES_BINDING', `Managed HTTP step "${step}" sets \`targetConnector\` without a connection and folder. `
|
|
110
|
+
+ `A connector-specific fallback needs the binding pair whose authentication it reuses.`, `connection: 'slack', folder: 'shared'`);
|
|
111
|
+
}
|
|
112
|
+
const timeout = inputs?.timeout;
|
|
113
|
+
if (timeout !== undefined) {
|
|
114
|
+
if (typeof timeout !== 'string' || !ISO8601_DURATION.test(timeout.trim())) {
|
|
115
|
+
ctx.report('HTTP_TIMEOUT_NOT_DURATION', `Step "${step}" sets timeout to ${JSON.stringify(timeout)}, which is not an ISO-8601 duration. The `
|
|
116
|
+
+ `platform reads this field as a duration string (both http definitions default it to "PT15M"), so a `
|
|
117
|
+
+ `value it cannot parse configures no timeout at all.`, `timeout: 'PT30S' // 30 seconds — also 'PT1M', 'PT15M', 'PT1H'`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const retryCount = inputs?.retryCount;
|
|
121
|
+
if (retryCount !== undefined) {
|
|
122
|
+
if (typeof retryCount !== 'number' || !Number.isInteger(retryCount) || retryCount < 0) {
|
|
123
|
+
ctx.report('HTTP_RETRY_COUNT_INVALID', `Step "${step}" sets retryCount to ${JSON.stringify(retryCount)}. It is a count of RETRIES after the `
|
|
124
|
+
+ `first attempt, so it must be a non-negative whole number — 0 (the definitions' default) means "try `
|
|
125
|
+
+ `once", 2 means up to three attempts.`, `retryCount: 2`);
|
|
126
|
+
}
|
|
127
|
+
else if (retryCount > RETRY_COUNT_SOFT_MAX) {
|
|
128
|
+
ctx.report('HTTP_RETRY_COUNT_HIGH', `Step "${step}" asks for ${retryCount} retries. The local runtime clamps at ${RETRY_COUNT_SOFT_MAX} and `
|
|
129
|
+
+ `the largest value in the deployed corpus is 3, so a local replay will not exercise what you wrote. `
|
|
130
|
+
+ `The platform's own ceiling is not measured — this is a warning, not a refusal.`, `retryCount: ${RETRY_COUNT_SOFT_MAX}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Every node-local rule of an `http()` step, in the order check.ts ran them: the
|
|
136
|
+
* request settings, a managed step's binding labels, then the response branches.
|
|
137
|
+
*/
|
|
138
|
+
export function checkHttpStep(inputs, ctx) {
|
|
139
|
+
const step = ctx.step;
|
|
140
|
+
checkHttp(inputs, ctx);
|
|
141
|
+
// Only a complete pair: an empty half is HTTP_MANAGED_BINDING_INCOMPLETE, which
|
|
142
|
+
// serialize refuses before it resolves either name.
|
|
143
|
+
const pair = inputs;
|
|
144
|
+
const filled = (v) => typeof v === 'string' && v.trim() !== '';
|
|
145
|
+
if (pair?.managed && filled(pair.connection) && filled(pair.folder)) {
|
|
146
|
+
const target = pair.targetConnector;
|
|
147
|
+
ctx.bindingLabels(`managed http "${step}"`, pair,
|
|
148
|
+
// The connector its CONNECTION belongs to: the target, not the wrapper.
|
|
149
|
+
{ id: step, connector: typeof target === 'string' && target !== '' ? target : 'uipath-uipath-http' });
|
|
150
|
+
}
|
|
151
|
+
const seen = new Set();
|
|
152
|
+
for (const b of inputs?.branches ?? []) {
|
|
153
|
+
if (!/^[A-Za-z0-9_-]+$/.test(b.name)) {
|
|
154
|
+
ctx.report('HTTP_BRANCH_NAME', `HTTP branch ${JSON.stringify(b.name)} on "${step}" is not port-safe — use letters, digits, _ or - (it becomes the branch-<name> port).`);
|
|
155
|
+
}
|
|
156
|
+
if (seen.has(b.name)) {
|
|
157
|
+
ctx.report('HTTP_BRANCH_DUP', `HTTP branch ${JSON.stringify(b.name)} on "${step}" is declared twice.`);
|
|
158
|
+
}
|
|
159
|
+
seen.add(b.name);
|
|
160
|
+
if (b.condition.literal) {
|
|
161
|
+
ctx.report('HTTP_BRANCH_CONST', `HTTP branch ${JSON.stringify(b.name)} on "${step}" tests a constant — it would fire always or never. Reference the response, e.g. $vars.${step}.output.statusCode.`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* The fields every HTTP response carries at the top of `output`, whatever the
|
|
167
|
+
* API answered. `core.action.http`'s definition declares all three; the author's
|
|
168
|
+
* `returns` describes what is INSIDE `body`, never beside it.
|
|
169
|
+
*/
|
|
170
|
+
const HTTP_ENVELOPE_FIELDS = ['body', 'statusCode', 'headers'];
|
|
171
|
+
/**
|
|
172
|
+
* A read of an `http()` step's result.
|
|
173
|
+
*
|
|
174
|
+
* The response is an envelope — `{ body, statusCode, headers }` — and `returns`
|
|
175
|
+
* declares the parsed JSON body's top-level fields, so a declared field is read
|
|
176
|
+
* at `output.body.<field>` (`out('<step>', 'body.<field>')`). That nesting is the
|
|
177
|
+
* one thing this family does not share with the process families, and it is
|
|
178
|
+
* exactly where flow-builder-sdk#850's run went wrong: a branch kept reading
|
|
179
|
+
* `output.currentDayForecast…` (the Azure Maps connector's path) after the step
|
|
180
|
+
* became an Open-Meteo `http()`, passed `check` and `validate`, and faulted at
|
|
181
|
+
* `flow debug` as `[400302]`.
|
|
182
|
+
*/
|
|
183
|
+
export function checkHttpRead({ node: name, field, rest, reader: step }, spec, report) {
|
|
184
|
+
if (field === undefined)
|
|
185
|
+
return; // `out('fetch')` — the whole response.
|
|
186
|
+
if (field === 'error')
|
|
187
|
+
return; // the failure envelope (managed http's error port).
|
|
188
|
+
if (field !== 'output') {
|
|
189
|
+
report('HTTP_READ_NOT_OUTPUT', `HTTP step "${name}" publishes its response under "output", but "$vars.${name}.${field}" `
|
|
190
|
+
+ `(read in "${step}") asks for "${field}", which is undefined at run time.`, `out('${name}', 'body.<field>')`);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const [top, inner] = rest.replace(/^\./, '').split('.');
|
|
194
|
+
if (!top)
|
|
195
|
+
return; // `$vars.<name>.output` — the whole envelope.
|
|
196
|
+
const declared = Object.keys(spec?.returns ?? {});
|
|
197
|
+
if (!HTTP_ENVELOPE_FIELDS.includes(top)) {
|
|
198
|
+
// The commonest form: a declared body field read one level too high.
|
|
199
|
+
const hoisted = declared.includes(top) ? top : undefined;
|
|
200
|
+
const nearest = hoisted ?? nearestName(top, declared);
|
|
201
|
+
report('HTTP_READ_UNKNOWN_FIELD', `"$vars.${name}.output.${top}" (read in "${step}") is not a field of HTTP step "${name}"'s `
|
|
202
|
+
+ `response. An HTTP response publishes ${HTTP_ENVELOPE_FIELDS.map((f) => `"${f}"`).join(', ')}; `
|
|
203
|
+
+ `the fields \`returns\` declares live under "body"`
|
|
204
|
+
+ (hoisted ? `, so "${top}" is read as "$vars.${name}.output.body.${top}".` : '.')
|
|
205
|
+
+ ` A read outside that shape resolves to undefined and faults a gateway at run time.`, nearest !== undefined ? `out('${name}', 'body.${nearest}')` : undefined);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (top !== 'body' || !inner)
|
|
209
|
+
return; // `statusCode`, `headers`, or the whole body.
|
|
210
|
+
if (declared.length === 0) {
|
|
211
|
+
report('HTTP_READ_WITHOUT_RETURNS', `"$vars.${name}.output.body.${inner}" (read in "${step}") reads a field of HTTP step "${name}"'s `
|
|
212
|
+
+ `response body, but the step declares no \`returns\` — so the node carries no body schema and nothing `
|
|
213
|
+
+ `can tell a real field from a typo. Only the API being called knows its body; declare the fields you read.`, `returns: { ${inner}: 'string' }`);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (declared.includes(inner))
|
|
217
|
+
return;
|
|
218
|
+
const nearest = nearestName(inner, declared);
|
|
219
|
+
report('HTTP_READ_UNKNOWN_FIELD', `HTTP step "${name}" declares no body field "${inner}" (read as "$vars.${name}.output.body.${inner}" `
|
|
220
|
+
+ `in "${step}"). Its \`returns\` names: ${declared.join(', ')}. A read outside that set resolves to undefined.`, nearest !== undefined ? `out('${name}', 'body.${nearest}')` : undefined);
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* `.onError` after a plain `core.action.http` node. Two separate facts about a
|
|
224
|
+
* PLAIN http step, both measured, both fatal to a handler there:
|
|
225
|
+
*
|
|
226
|
+
* 1. a 4xx/5xx does not FAIL the node — it completes carrying `statusCode`,
|
|
227
|
+
* so the handler only ever runs on a transport failure (DNS, refused,
|
|
228
|
+
* timeout);
|
|
229
|
+
* 2. when it does run there is nothing to read. `core.action.http@1.0.0` and
|
|
230
|
+
* the `@1.3` that `uip maestro flow migrate` upgrades it to both publish
|
|
231
|
+
* `<step>.error = true` and leave `<step>.output` null — no envelope
|
|
232
|
+
* anywhere, unlike every other family. `validate` still says Valid.
|
|
233
|
+
*
|
|
234
|
+
* So this stays an error, and says the second part too: an author who reads
|
|
235
|
+
* the old message ("use the managed node if statuses must route") could
|
|
236
|
+
* reasonably conclude a transport-failure handler is fine as it is. check.ts
|
|
237
|
+
* reports the same refusal for a custom node built on the plain definition.
|
|
238
|
+
*/
|
|
239
|
+
export function plainHttpOnErrorMessage(stepName) {
|
|
240
|
+
return {
|
|
241
|
+
message: `Step "${stepName}" handles errors on a plain http() request. A 4xx/5xx does NOT fail that node — it `
|
|
242
|
+
+ `completes with out('${stepName}', 'statusCode') set — so the handler runs only on a transport failure, `
|
|
243
|
+
+ `and when it does the node publishes no failure envelope at all (measured: "<step>.error" is the boolean `
|
|
244
|
+
+ `true and "<step>.output" is null, in 1.0.0 and in the 1.3 flow migrate produces), so err() has nothing `
|
|
245
|
+
+ `to read. The managed node is the one whose failures route AND carry the envelope.`,
|
|
246
|
+
suggestion: `http({ url: …, managed: true })`,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The HTTP factory, `http()`. Moved from core/actions.ts in Phase 4 of
|
|
3
|
+
* docs/NODE_TYPE_ARCHITECTURE_DESIGN.md; the package entry re-exports it.
|
|
4
|
+
*/
|
|
5
|
+
import { type ActionSpec } from '../../../core/actions.js';
|
|
6
|
+
import type { HttpInputs } from './spec.js';
|
|
7
|
+
/**
|
|
8
|
+
* Declare an HTTP request action, explicitly choosing standalone or managed.
|
|
9
|
+
*
|
|
10
|
+
* @param inputs - The request: `url`, `method`, `body`, the response fields you
|
|
11
|
+
* read back via `returns`, and the `managed` node choice.
|
|
12
|
+
* @returns An action spec to pass to `.step(name, …)`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function http(inputs: HttpInputs): ActionSpec;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The HTTP factory, `http()`. Moved from core/actions.ts in Phase 4 of
|
|
3
|
+
* docs/NODE_TYPE_ARCHITECTURE_DESIGN.md; the package entry re-exports it.
|
|
4
|
+
*/
|
|
5
|
+
import { builtinActionSpec } from '../../../core/actions.js';
|
|
6
|
+
/**
|
|
7
|
+
* Declare an HTTP request action, explicitly choosing standalone or managed.
|
|
8
|
+
*
|
|
9
|
+
* @param inputs - The request: `url`, `method`, `body`, the response fields you
|
|
10
|
+
* read back via `returns`, and the `managed` node choice.
|
|
11
|
+
* @returns An action spec to pass to `.step(name, …)`.
|
|
12
|
+
*/
|
|
13
|
+
export function http(inputs) {
|
|
14
|
+
if (typeof inputs.managed !== 'boolean') {
|
|
15
|
+
throw new Error('http() requires managed: true or managed: false because they emit different platform nodes. '
|
|
16
|
+
+ 'Use true for Managed HTTP Request/core.action.http.v2 or when non-2xx statuses must reach .onError(); '
|
|
17
|
+
+ 'use false for the standalone core.action.http node.');
|
|
18
|
+
}
|
|
19
|
+
return builtinActionSpec({ kind: 'http', inputs });
|
|
20
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** The HTTP node family, as flow-node-registry.ts lists it: `core.action.http` and `core.action.http.v2`. */
|
|
2
|
+
import type { FlowNodeFamily } from '../../../flow-node-type.js';
|
|
3
|
+
export declare const httpFlowFamily: FlowNodeFamily;
|
|
4
|
+
/**
|
|
5
|
+
* The plain node's `.onError` refusal, for check.ts: a custom node built on the
|
|
6
|
+
* `core.action.http` definition gets the same answer as an `http()` step.
|
|
7
|
+
*/
|
|
8
|
+
export { plainHttpOnErrorMessage } from './check.js';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { HTTP_DIAGNOSTICS } from './check.js';
|
|
2
|
+
import { httpNodeTypes } from './node-type.js';
|
|
3
|
+
export const httpFlowFamily = {
|
|
4
|
+
id: 'http',
|
|
5
|
+
title: 'HTTP',
|
|
6
|
+
types: httpNodeTypes,
|
|
7
|
+
diagnostics: HTTP_DIAGNOSTICS,
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* The plain node's `.onError` refusal, for check.ts: a custom node built on the
|
|
11
|
+
* `core.action.http` definition gets the same answer as an `http()` step.
|
|
12
|
+
*/
|
|
13
|
+
export { plainHttpOnErrorMessage } from './check.js';
|