@uipath/maestro-builder-sdk 6.68.7 → 6.69.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/case/check.d.ts +25 -57
- package/dist/case/check.js +93 -249
- package/dist/case/cli-library.d.ts +4 -7
- package/dist/case/cli-library.js +5 -46
- package/dist/case/serialize.d.ts +48 -2
- package/dist/case/serialize.js +269 -67
- package/dist/case/subscription.d.ts +6 -4
- package/dist/case/subscription.js +15 -5
- package/dist/core/refusals.js +3 -2
- package/package.json +2 -2
package/dist/case/check.d.ts
CHANGED
|
@@ -1,80 +1,48 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* case/check — fast
|
|
2
|
+
* case/check — fast validation of a BuiltCase.
|
|
3
3
|
*
|
|
4
4
|
* Surfaces the common `uip maestro case validate` failures early (task without
|
|
5
5
|
* an entry rule, case/stage without a completion rule, an unsatisfiable
|
|
6
|
-
* `required-tasks-completed
|
|
7
|
-
*
|
|
8
|
-
* `
|
|
6
|
+
* `required-tasks-completed`) so authoring gets instant feedback, and runs the
|
|
7
|
+
* serializer in collect mode (`serializerRefusals`), so it also reports every
|
|
8
|
+
* refusal `compile` would raise (unresolved stage/task references, duplicate
|
|
9
|
+
* stage labels, a connector input the operation does not declare), each from
|
|
10
|
+
* its one implementation and placed on its task, stage or trigger
|
|
11
|
+
* (docs/CHECK_COMPILE_CONTRACT_DESIGN.md §3.5, phase 8). The authoritative gate
|
|
12
|
+
* is still `uip maestro case validate` / `@uipath/case-schema` on the emitted file.
|
|
9
13
|
*/
|
|
10
14
|
import type { BuiltCase } from './case-sdk.js';
|
|
11
15
|
import { type ConnectorCheckOpts } from '../core/connector-checks.js';
|
|
12
|
-
import type {
|
|
13
|
-
import { type EventCheckOpts } from '../core/event-checks.js';
|
|
16
|
+
import type { Library } from '../core/library.js';
|
|
14
17
|
export interface Diagnostic {
|
|
15
18
|
level: 'error' | 'warning';
|
|
16
19
|
code: string;
|
|
17
20
|
message: string;
|
|
21
|
+
/** What the finding is about: a task's name, a stage's label, a trigger's label. */
|
|
18
22
|
where?: string;
|
|
19
23
|
suggestion?: string;
|
|
24
|
+
/** The id of the node `where` names, for a refusal the serializer placed on one (`serializerRefusals`). */
|
|
25
|
+
nodeId?: string;
|
|
20
26
|
}
|
|
21
27
|
/**
|
|
22
28
|
* The connector-library facts Case `check` reads, from the same `Library`
|
|
23
29
|
* instance `compile` serializes against (`caseCheckOpts` in `./cli-library.ts`).
|
|
24
30
|
*
|
|
25
|
-
*
|
|
26
|
-
* `
|
|
27
|
-
* the
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
31
|
+
* `check` runs the serializer in collect mode (`serializerRefusals`), with
|
|
32
|
+
* `library` and `bindings`, so every refusal `compile` makes (an operation or
|
|
33
|
+
* event the library cannot resolve, a connector input the operation does not
|
|
34
|
+
* declare, an unrecorded lookup, a binding label bindings.json cannot answer,
|
|
35
|
+
* an unverified BYOA connection, an external task's descriptor contract) is
|
|
36
|
+
* reported from its one implementation. The other hooks feed the warnings only
|
|
37
|
+
* `check` gives: the schema-readiness warnings and the lookup rules of
|
|
38
|
+
* `checkConnectorLookups`.
|
|
32
39
|
*
|
|
33
|
-
* Every hook fails open: without a library, `check`
|
|
34
|
-
*
|
|
40
|
+
* Every hook fails open: without a library, `check` judges a connector task up
|
|
41
|
+
* to the library, as `compile` would.
|
|
35
42
|
*/
|
|
36
|
-
export type CaseCheckOpts = Pick<ConnectorCheckOpts, '
|
|
37
|
-
/**
|
|
38
|
-
|
|
39
|
-
* when it resolves: `EVENT_UNKNOWN` (the event is not in the library, or
|
|
40
|
-
* refuses the `object`) or `EVENT_GENERIC_NO_OBJECT` (a GENERIC event with no
|
|
41
|
-
* `object`). Built on `resolveCaseSubscription`, the call compile makes.
|
|
42
|
-
*/
|
|
43
|
-
/**
|
|
44
|
-
* What Case `compile` would refuse an External Agent / External Workflow
|
|
45
|
-
* task's resolved descriptor with (`externalDescriptorRefusal`), or
|
|
46
|
-
* `undefined`: also `undefined` when the descriptor does not resolve, which
|
|
47
|
-
* `connectorResolveError` reports.
|
|
48
|
-
*/
|
|
49
|
-
externalDescriptorError?: (kind: 'external-agent' | 'external-workflow', descriptor: {
|
|
50
|
-
key: string;
|
|
51
|
-
action: string;
|
|
52
|
-
version?: string;
|
|
53
|
-
object?: string;
|
|
54
|
-
}) => string | undefined;
|
|
55
|
-
/**
|
|
56
|
-
* What Case `compile` would refuse an External Agent / External Workflow
|
|
57
|
-
* task's mode and event with, judged against what the library records for
|
|
58
|
-
* the object (`externalExecution`): EXTERNAL_TASK_MODE (no mode from the
|
|
59
|
-
* author or the library), EXTERNAL_TASK_MODE_MISMATCH, EXTERNAL_TASK_EVENT.
|
|
60
|
-
* `undefined` when they are fine, or the descriptor does not resolve.
|
|
61
|
-
*/
|
|
62
|
-
externalExecutionError?: (owner: string, descriptor: {
|
|
63
|
-
key: string;
|
|
64
|
-
action: string;
|
|
65
|
-
version?: string;
|
|
66
|
-
object?: string;
|
|
67
|
-
}, authored: {
|
|
68
|
-
mode?: unknown;
|
|
69
|
-
event?: unknown;
|
|
70
|
-
}) => {
|
|
71
|
-
code: string;
|
|
72
|
-
message: string;
|
|
73
|
-
} | undefined;
|
|
74
|
-
subscriptionError?: (kind: 'trigger' | 'event', subscription: EventSubscription) => {
|
|
75
|
-
code: string;
|
|
76
|
-
message: string;
|
|
77
|
-
} | undefined;
|
|
43
|
+
export type CaseCheckOpts = Pick<ConnectorCheckOpts, 'lookups' | 'resolutions' | 'schemaReadiness' | 'bindings'> & {
|
|
44
|
+
/** The library `compile` serializes against, for the serializer `check` runs. */
|
|
45
|
+
library?: Library;
|
|
78
46
|
};
|
|
79
47
|
/**
|
|
80
48
|
* Validate a built case.
|
package/dist/case/check.js
CHANGED
|
@@ -1,16 +1,9 @@
|
|
|
1
|
-
import { businessEventProblems } from './business-event-shape.js';
|
|
2
|
-
import { isUnboundInArg, unboundInArgMessage } from './in-arg-binding.js';
|
|
3
|
-
import { ruleShapeProblems } from './rule-options.js';
|
|
4
1
|
import { types } from '../core/expr.js';
|
|
5
|
-
import { CASE_FORMAT_PROFILE, CASE_SCHEMA_VERSIONS, authoredCaseVersion
|
|
2
|
+
import { CASE_FORMAT_PROFILE, CASE_SCHEMA_VERSIONS, authoredCaseVersion } from './format-profile.js';
|
|
6
3
|
import { schemaVersionRefusal } from '../schema-version.js';
|
|
7
|
-
import {
|
|
8
|
-
import { EQUALS_EXPR_DIALECT, checkBindingLabels, checkConnectorContract, checkConnectorLookups, checkConnectorSchema, } from '../core/connector-checks.js';
|
|
9
|
-
import { checkEventResolution } from '../core/event-checks.js';
|
|
10
|
-
import { EXTERNAL_TASK_DESCRIPTOR_CONTRACT, externalExecution } from './external.js';
|
|
11
|
-
import { isResourceLabelRefusal, resolveResourceRef } from './resource-identity.js';
|
|
12
|
-
import { taskShapeProblems } from './task-shape.js';
|
|
4
|
+
import { EQUALS_EXPR_DIALECT, checkConnectorLookups, checkConnectorSchema, } from '../core/connector-checks.js';
|
|
13
5
|
import { isConnectorPlaceholder } from './connector-placeholder.js';
|
|
6
|
+
import { serialize } from './serialize.js';
|
|
14
7
|
const TYPE_NAMES = new Set(Object.values(types));
|
|
15
8
|
// audit-case-coverage: rule-slot table start
|
|
16
9
|
// This runtime table must equal the committed matrix's rejected cells. The audit
|
|
@@ -26,17 +19,6 @@ const ILLEGAL = [];
|
|
|
26
19
|
*/
|
|
27
20
|
export function check(built, opts = {}) {
|
|
28
21
|
const diags = [];
|
|
29
|
-
// The same registry `serialize` resolves against, so the two agree on which
|
|
30
|
-
// labels a decompiled source's preserved binding rows answer.
|
|
31
|
-
const bindings = preservedBindings(opts.bindings, caseJsonPreservation(built));
|
|
32
|
-
if (bindings !== opts.bindings)
|
|
33
|
-
opts = { ...opts, bindings };
|
|
34
|
-
for (const message of caseJsonPreservationErrors(caseJsonPreservation(built)))
|
|
35
|
-
diags.push({
|
|
36
|
-
level: 'error',
|
|
37
|
-
code: 'PRESERVATION_COLLISION',
|
|
38
|
-
message: `Invalid Case JSON preservation payload: ${message}.`,
|
|
39
|
-
});
|
|
40
22
|
if (!built.id)
|
|
41
23
|
diags.push({ level: 'error', code: 'NO_ID', message: 'casePlan() requires an id.' });
|
|
42
24
|
const requestedVersion = authoredCaseVersion(built);
|
|
@@ -46,10 +28,7 @@ export function check(built, opts = {}) {
|
|
|
46
28
|
code: 'VERSION_DEPRECATED',
|
|
47
29
|
message: `.version(${JSON.stringify(requestedVersion)}) is deprecated: the Case JSON schema version is declared with .schemaVersion(), which accepts only versions this build can write. This SDK writes ${CASE_FORMAT_PROFILE.writeVersion}.`,
|
|
48
30
|
});
|
|
49
|
-
//
|
|
50
|
-
const mismatch = caseVersionMismatch(built);
|
|
51
|
-
if (mismatch)
|
|
52
|
-
diags.push({ level: 'error', code: 'VERSION_MISMATCH', message: mismatch });
|
|
31
|
+
// A version this build cannot write is the serializer's VERSION_MISMATCH.
|
|
53
32
|
}
|
|
54
33
|
// The DECLARED grammar version (`.schemaVersion()`), as opposed to the
|
|
55
34
|
// deprecated `.version()` above. It is already refused at the call, so this
|
|
@@ -97,55 +76,10 @@ export function check(built, opts = {}) {
|
|
|
97
76
|
where,
|
|
98
77
|
});
|
|
99
78
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
for (const stage of built.stages) {
|
|
79
|
+
// A repeated stage label or task name, and the select-next-stage exit rules,
|
|
80
|
+
// are the serializer's (DUP_STAGE, DUP_TASK, SELECT_NEXT_STAGE_*).
|
|
81
|
+
for (const stage of built.stages)
|
|
104
82
|
checkName('Stage label', stage.label, stage.label);
|
|
105
|
-
if (stageLabels.has(stage.label))
|
|
106
|
-
diags.push({ level: 'error', code: 'DUP_STAGE', message: `Duplicate stage label "${stage.label}".`, where: stage.label });
|
|
107
|
-
stageLabels.add(stage.label);
|
|
108
|
-
for (const exit of stage.exit) {
|
|
109
|
-
if (!exit.selectNextStage)
|
|
110
|
-
continue;
|
|
111
|
-
selectNextStageCount += 1;
|
|
112
|
-
if (exit.type !== 'wait-for-user')
|
|
113
|
-
diags.push({
|
|
114
|
-
level: 'error',
|
|
115
|
-
code: 'SELECT_NEXT_STAGE_EXIT_TYPE',
|
|
116
|
-
message: `Stage "${stage.label}" configures selectNextStage on an exit that is not type "wait-for-user".`,
|
|
117
|
-
where: stage.label,
|
|
118
|
-
});
|
|
119
|
-
if (!exit.selectNextStage.objectName.trim())
|
|
120
|
-
diags.push({
|
|
121
|
-
level: 'error',
|
|
122
|
-
code: 'SELECT_NEXT_STAGE_OBJECT_NAME',
|
|
123
|
-
message: `Stage "${stage.label}" selectNextStage.objectName must name a Data Fabric entity.`,
|
|
124
|
-
where: stage.label,
|
|
125
|
-
});
|
|
126
|
-
// Its connector's labels resolve through the same `bindingRef` as a
|
|
127
|
-
// connector task's, so they get the same binding-label rules.
|
|
128
|
-
const connector = exit.selectNextStage.connector;
|
|
129
|
-
if (connector)
|
|
130
|
-
checkBindingLabels('select-next-stage connector', connector, opts, (code, message) => diags.push({ level: 'error', code, message, where: stage.label }), { key: connector.connectorKey });
|
|
131
|
-
}
|
|
132
|
-
for (const lane of stage.lanes)
|
|
133
|
-
for (const t of lane) {
|
|
134
|
-
// Task displayNames must be unique across the case: the platform rejects
|
|
135
|
-
// duplicates (CRITICAL), and a bare selected-tasks-completed reference
|
|
136
|
-
// would silently mis-resolve to the first task of that name.
|
|
137
|
-
if (taskNames.has(t.name))
|
|
138
|
-
diags.push({
|
|
139
|
-
level: 'error',
|
|
140
|
-
code: 'DUP_TASK',
|
|
141
|
-
message: `Duplicate task displayName "${t.name}" — task names must be unique across the case.`,
|
|
142
|
-
where: `${stage.label}/${t.name}`,
|
|
143
|
-
});
|
|
144
|
-
taskNames.add(t.name);
|
|
145
|
-
// Also register the qualified `Stage/Task` key so a qualified reference resolves.
|
|
146
|
-
taskNames.add(`${stage.label}/${t.name}`);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
83
|
// Condition `displayName`s must be unique across the whole case — `uip maestro
|
|
150
84
|
// case validate` rejects a repeat as CASE_MGMT_RULE_NAME_DUPLICATE, at error
|
|
151
85
|
// severity. The namespace spans stage entry, stage exit and task entry
|
|
@@ -209,12 +143,6 @@ export function check(built, opts = {}) {
|
|
|
209
143
|
for (const c of task.entry)
|
|
210
144
|
checkConditionName(c.displayName, `task "${task.name}" entry`);
|
|
211
145
|
}
|
|
212
|
-
if (selectNextStageCount > 1)
|
|
213
|
-
diags.push({
|
|
214
|
-
level: 'error',
|
|
215
|
-
code: 'SELECT_NEXT_STAGE_MULTIPLE',
|
|
216
|
-
message: 'A case can configure selectNextStage on at most one exit because the wire contract has one case-level connector.',
|
|
217
|
-
});
|
|
218
146
|
if (built.stages.length === 0)
|
|
219
147
|
diags.push({ level: 'error', code: 'NO_STAGES', message: 'Case has no stages.' });
|
|
220
148
|
const hasCompletion = built.caseExit.some((c) => c.marksCaseComplete !== false);
|
|
@@ -224,35 +152,11 @@ export function check(built, opts = {}) {
|
|
|
224
152
|
code: 'NO_CASE_COMPLETION',
|
|
225
153
|
message: 'Case has no completion rule — add .completeWhen(rule(\'required-stages-completed\')).',
|
|
226
154
|
});
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
const businessEvents = new Set((built.businessEvents ?? []).map((event) => event.name));
|
|
232
|
-
for (const problem of businessEventProblems(built.businessEvents ?? []))
|
|
233
|
-
diags.push({ level: 'error', code: problem.code, message: problem.message });
|
|
234
|
-
const refEvent = (r, where) => {
|
|
235
|
-
if (r.rule !== 'api-event')
|
|
236
|
-
return;
|
|
237
|
-
if (!r.businessEvent)
|
|
238
|
-
diags.push({
|
|
239
|
-
level: 'error',
|
|
240
|
-
code: 'API_EVENT_NO_BUSINESS_EVENT',
|
|
241
|
-
message: 'Rule "api-event" names no business event, so it listens for nothing — pass { businessEvent: \'<name>\' } with a name declared by .businessEvent(\'<name>\').',
|
|
242
|
-
where,
|
|
243
|
-
});
|
|
244
|
-
else if (!businessEvents.has(r.businessEvent))
|
|
245
|
-
diags.push({
|
|
246
|
-
level: 'error',
|
|
247
|
-
code: 'UNKNOWN_BUSINESS_EVENT',
|
|
248
|
-
message: `Rule "api-event" listens for business event "${r.businessEvent}", which the case does not declare — add .businessEvent(${JSON.stringify(r.businessEvent)}) to the case.`,
|
|
249
|
-
where,
|
|
250
|
-
});
|
|
251
|
-
};
|
|
155
|
+
// The business events' shapes, a rule's shape, an `api-event` rule's event,
|
|
156
|
+
// and a rule's stage and task references are the serializer's
|
|
157
|
+
// (`businessEventProblems`, `ruleShapeProblems`, API_EVENT_NO_BUSINESS_EVENT,
|
|
158
|
+
// UNKNOWN_BUSINESS_EVENT, UNKNOWN_STAGE_REF, UNKNOWN_TASK_REF).
|
|
252
159
|
const refPlacement = (r, slot, where) => {
|
|
253
|
-
for (const problem of ruleShapeProblems(r))
|
|
254
|
-
diags.push({ level: 'error', code: problem.code, message: problem.message, where });
|
|
255
|
-
refEvent(r, where);
|
|
256
160
|
const bad = ILLEGAL.find((x) => x.rule === r.rule && x.slot === slot);
|
|
257
161
|
if (bad)
|
|
258
162
|
diags.push({
|
|
@@ -263,28 +167,16 @@ export function check(built, opts = {}) {
|
|
|
263
167
|
});
|
|
264
168
|
};
|
|
265
169
|
const refStage = (r, where) => {
|
|
266
|
-
if (
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
level: 'warning',
|
|
274
|
-
code: 'RULE_TASKS_REQUIRED',
|
|
275
|
-
message: 'Rule "selected-tasks-completed" has neither selected tasks nor an expression gate — pass { tasks: [...] } or use when(expression).',
|
|
276
|
-
where,
|
|
277
|
-
});
|
|
278
|
-
for (const n of r.tasks ?? [])
|
|
279
|
-
if (!taskNames.has(n))
|
|
280
|
-
diags.push({ level: 'error', code: 'UNKNOWN_TASK_REF', message: `Rule "selected-tasks-completed" references unknown task "${n}".`, where });
|
|
281
|
-
}
|
|
170
|
+
if (r.rule === 'selected-tasks-completed' && !r.tasks?.length && !r.expression)
|
|
171
|
+
diags.push({
|
|
172
|
+
level: 'warning',
|
|
173
|
+
code: 'RULE_TASKS_REQUIRED',
|
|
174
|
+
message: 'Rule "selected-tasks-completed" has neither selected tasks nor an expression gate — pass { tasks: [...] } or use when(expression).',
|
|
175
|
+
where,
|
|
176
|
+
});
|
|
282
177
|
};
|
|
283
|
-
// An In-arg with no `{ from }`
|
|
284
|
-
//
|
|
285
|
-
for (const vd of built.vars)
|
|
286
|
-
if (isUnboundInArg(vd, built.triggers))
|
|
287
|
-
diags.push({ level: 'error', code: 'CASE_IN_ARG_UNBOUND', message: unboundInArgMessage(vd.name, built.triggers), where: vd.name });
|
|
178
|
+
// An In-arg with no `{ from }` and no primary trigger to bind to is the
|
|
179
|
+
// serializer's CASE_IN_ARG_UNBOUND (in-arg-binding.ts), reported per arg.
|
|
288
180
|
// ── `=js:vars.<name>` resolvability — REMOVED, superseded by #257 ───────────
|
|
289
181
|
// This file briefly carried a VAR_NOT_REFERENCEABLE error asserting that only a
|
|
290
182
|
// trigger-bound In-arg (`.input(shape, { from })`) could be read as `=js:vars.x`,
|
|
@@ -303,7 +195,7 @@ export function check(built, opts = {}) {
|
|
|
303
195
|
// positive that blocks emit (this gate runs before serialization), so it must be
|
|
304
196
|
// re-derived from the serializer rather than from remembered probe results.
|
|
305
197
|
for (const stage of built.stages) {
|
|
306
|
-
checkStage(stage, diags, refStage, refPlacement
|
|
198
|
+
checkStage(stage, diags, refStage, refPlacement);
|
|
307
199
|
stage.slaRules.forEach((s, i) => checkSla(s, `stage "${stage.label}" SLA #${i + 1}`, diags));
|
|
308
200
|
}
|
|
309
201
|
built.slaRules.forEach((s, i) => checkSla(s, `case SLA #${i + 1}`, diags));
|
|
@@ -314,98 +206,88 @@ export function check(built, opts = {}) {
|
|
|
314
206
|
refPlacement(r, 'case-exit', 'case-exit');
|
|
315
207
|
}
|
|
316
208
|
checkLibraryConnectors(built, opts, diags);
|
|
209
|
+
diags.push(...serializerRefusals(built, opts, diags));
|
|
317
210
|
return diags;
|
|
318
211
|
}
|
|
319
212
|
/**
|
|
320
|
-
*
|
|
321
|
-
*
|
|
213
|
+
* Every refusal `compile` would raise for `built`: the serializer, run in
|
|
214
|
+
* collect mode (docs/CHECK_COMPILE_CONTRACT_DESIGN.md §3.5, phase 8), so each
|
|
215
|
+
* rule it enforces is reported from its one implementation, placed on the
|
|
216
|
+
* task, stage or trigger it is about (`where`, `nodeId`).
|
|
322
217
|
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
218
|
+
* LIBRARY_REQUIRED is not reported: without a library `check` fails open, as
|
|
219
|
+
* its library rules always have. A throw that is no refusal comes back as
|
|
220
|
+
* COMPILE_FAILED, on its node, which is what `compile` says; on a node `check`
|
|
221
|
+
* already refuses it is that refusal's consequence and is not repeated. An
|
|
222
|
+
* `internal SDK error:` is passed on, unless `check` or the serializer already
|
|
223
|
+
* found an error, which the author has to fix first.
|
|
224
|
+
*/
|
|
225
|
+
function serializerRefusals(built, opts, found) {
|
|
226
|
+
const collected = [];
|
|
227
|
+
try {
|
|
228
|
+
serialize(built, {
|
|
229
|
+
...(opts.library ? { library: opts.library } : {}),
|
|
230
|
+
...(opts.bindings ? { bindings: opts.bindings } : {}),
|
|
231
|
+
collect: (r) => collected.push(r),
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
if (!found.some((d) => d.level === 'error') && !collected.some((r) => r.code !== 'LIBRARY_REQUIRED'))
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
238
|
+
const out = [];
|
|
239
|
+
const seen = new Set();
|
|
240
|
+
for (const r of collected) {
|
|
241
|
+
if (r.code === 'LIBRARY_REQUIRED')
|
|
242
|
+
continue;
|
|
243
|
+
// Only a crash on a node is the consequence of a finding there; a scope-level
|
|
244
|
+
// one (no `where`) is reported even beside a case-level error such as NO_ID.
|
|
245
|
+
if (r.code === 'COMPILE_FAILED' && r.where !== undefined && found.some((d) => d.level === 'error' && d.where === r.where))
|
|
246
|
+
continue;
|
|
247
|
+
// One finding per node: the same refusal twice there is one rule broken once.
|
|
248
|
+
const key = `${r.code}\n${r.where ?? ''}\n${r.message}`;
|
|
249
|
+
if (seen.has(key))
|
|
250
|
+
continue;
|
|
251
|
+
seen.add(key);
|
|
252
|
+
out.push({
|
|
253
|
+
level: 'error', code: r.code, message: r.message,
|
|
254
|
+
...(r.where !== undefined ? { where: r.where } : {}),
|
|
255
|
+
...(r.suggestion !== undefined ? { suggestion: r.suggestion } : {}),
|
|
256
|
+
...(r.nodeId !== undefined ? { nodeId: r.nodeId } : {}),
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
return out;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* The connector-library warnings only `check` gives (#1014): a connector task's
|
|
263
|
+
* schema-readiness warnings, because the `connectors-local` overlay `registry
|
|
264
|
+
* prepare` writes is what Case compile resolves against too, and the lookup
|
|
265
|
+
* rules of `checkConnectorLookups` (a pasted id, a match key the field does not
|
|
266
|
+
* declare). Everything compile refuses (the operation, its inputs, an
|
|
267
|
+
* unrecorded lookup, the binding labels, the event subscriptions, an external
|
|
268
|
+
* task's descriptor) is the serializer's (`serializerRefusals`).
|
|
338
269
|
*/
|
|
339
270
|
function checkLibraryConnectors(built, opts, diags) {
|
|
340
|
-
const reporter = (
|
|
271
|
+
const reporter = (at) => (d) => {
|
|
341
272
|
diags.push({ level: d.level, code: d.code, message: d.message, where: at, ...(d.suggestion ? { suggestion: d.suggestion } : {}) });
|
|
342
273
|
};
|
|
343
|
-
const subscription = (kind, sub, where, at) => {
|
|
344
|
-
// The call `resolvedSubscriptionBag` makes (`resolveCaseSubscription`), so
|
|
345
|
-
// check refuses exactly what compile refuses, with the same text.
|
|
346
|
-
const refused = opts.subscriptionError?.(kind, sub);
|
|
347
|
-
if (refused !== undefined)
|
|
348
|
-
diags.push({ level: 'error', code: refused.code, message: `${where}: ${refused.message}`, where: at });
|
|
349
|
-
checkBindingLabels(where, sub, opts, (code, message) => diags.push({ level: 'error', code, message, where: at }), { key: sub.connector, action: sub.event });
|
|
350
|
-
// From the shared event rules, the two Case compile enforces: a `where` lookup
|
|
351
|
-
// nobody recorded (LOOKUP_UNRESOLVED, a warning) and an unverified BYOA
|
|
352
|
-
// connection (EVENT_BYOA_UNVERIFIED). Only their hooks are passed, so nothing
|
|
353
|
-
// else in them fires.
|
|
354
|
-
checkEventResolution(sub, where, kind, {
|
|
355
|
-
resolutions: opts.resolutions ?? {},
|
|
356
|
-
...(opts.eventFacts ? { eventFacts: opts.eventFacts } : {}),
|
|
357
|
-
...(opts.byoa ? { byoa: opts.byoa } : {}),
|
|
358
|
-
...(opts.bindings ? { bindings: opts.bindings } : {}),
|
|
359
|
-
}, (code, message) => diags.push({ level: 'error', code, message, where: at }), (code, message) => diags.push({ level: 'warning', code, message, where: at }));
|
|
360
|
-
};
|
|
361
|
-
const rules = (conditions, where) => {
|
|
362
|
-
for (const c of conditions)
|
|
363
|
-
for (const g of c.rules)
|
|
364
|
-
for (const r of g)
|
|
365
|
-
if (r.rule === 'wait-for-connector' && isSubscription(r.connector))
|
|
366
|
-
subscription('event', r.connector, `wait-for-connector rule in ${where}`, where);
|
|
367
|
-
};
|
|
368
|
-
built.triggers.forEach((t, i) => {
|
|
369
|
-
if (t.kind !== 'event' || !t.eventSubscription)
|
|
370
|
-
return;
|
|
371
|
-
const name = t.name ?? `Trigger ${i + 1}`;
|
|
372
|
-
subscription('trigger', t.eventSubscription, `Event trigger "${name}"`, name);
|
|
373
|
-
});
|
|
374
274
|
for (const stage of built.stages) {
|
|
375
|
-
rules(stage.entry, `stage "${stage.label}" entry`);
|
|
376
|
-
rules(stage.exit, `stage "${stage.label}" exit`);
|
|
377
275
|
for (const t of stage.lanes.flat()) {
|
|
378
|
-
rules(t.entry, `task "${t.name}" entry`);
|
|
379
276
|
// A placeholder (`.unresolved('connector')`) names no operation to resolve.
|
|
380
277
|
if (t.kind === 'connector' && t.connector && !isConnectorPlaceholder(t)) {
|
|
381
278
|
const where = `Connector task "${t.name}"`;
|
|
382
279
|
const spec = t.connector;
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
280
|
+
// An unrecorded lookup is the serializer's LOOKUP_UNRESOLVED.
|
|
281
|
+
checkConnectorLookups(where, spec, opts, EQUALS_EXPR_DIALECT, reporter(t.name), false);
|
|
282
|
+
checkConnectorSchema(where, spec, opts, EQUALS_EXPR_DIALECT, reporter(t.name));
|
|
386
283
|
}
|
|
387
284
|
if ((t.kind === 'external-agent' || t.kind === 'external-workflow')
|
|
388
285
|
&& t.external?.connector?.key?.trim() && t.external.connector.action?.trim()) {
|
|
389
286
|
const where = `${t.kind === 'external-agent' ? 'External Agent' : 'External Workflow'} task "${t.name}"`;
|
|
390
|
-
|
|
391
|
-
checkConnectorLookups(where, spec, opts, EQUALS_EXPR_DIALECT, reporter(where, t.name));
|
|
392
|
-
checkConnectorContract(where, spec, opts, reporter(where, t.name));
|
|
393
|
-
const refused = opts.externalDescriptorError?.(t.kind, spec);
|
|
394
|
-
if (refused !== undefined)
|
|
395
|
-
diags.push({ level: 'error', code: EXTERNAL_TASK_DESCRIPTOR_CONTRACT, message: `${where}: ${refused}`, where: t.name });
|
|
396
|
-
const execution = opts.externalExecutionError?.(where, spec, t.external);
|
|
397
|
-
if (execution !== undefined)
|
|
398
|
-
diags.push({ level: 'error', code: execution.code, message: execution.message, where: t.name });
|
|
287
|
+
checkConnectorLookups(where, t.external.connector, opts, EQUALS_EXPR_DIALECT, reporter(t.name), false);
|
|
399
288
|
}
|
|
400
|
-
if (t.kind === 'wait-for-connector' && isSubscription(t.waitConnector))
|
|
401
|
-
subscription('event', t.waitConnector, `Wait-for-connector task "${t.name}"`, t.name);
|
|
402
289
|
}
|
|
403
290
|
}
|
|
404
|
-
rules(built.caseExit, 'case exit');
|
|
405
|
-
}
|
|
406
|
-
/** A resolved subscription, not a placeholder; the test `serialize`'s `isResolvedSubscription` makes. */
|
|
407
|
-
function isSubscription(spec) {
|
|
408
|
-
return !!spec && typeof spec === 'object' && 'connector' in spec && 'event' in spec;
|
|
409
291
|
}
|
|
410
292
|
/**
|
|
411
293
|
* SLA guardrails surfaced early: zero at-risk percentages that the engine
|
|
@@ -446,9 +328,7 @@ function isNilOrEmpty(value) {
|
|
|
446
328
|
return Object.keys(value).length === 0;
|
|
447
329
|
return false;
|
|
448
330
|
}
|
|
449
|
-
function checkStage(stage, diags, refStage, refPlacement
|
|
450
|
-
/** A library judges external tasks' mode and event (`externalExecutionError`). */
|
|
451
|
-
externalJudge) {
|
|
331
|
+
function checkStage(stage, diags, refStage, refPlacement) {
|
|
452
332
|
const where = `stage "${stage.label}"`;
|
|
453
333
|
if (stage.exception && stage.required !== false)
|
|
454
334
|
diags.push({
|
|
@@ -518,9 +398,9 @@ externalJudge) {
|
|
|
518
398
|
});
|
|
519
399
|
for (const t of tasks) {
|
|
520
400
|
// A timer with no or several values, an action field key compile does not
|
|
521
|
-
// write
|
|
522
|
-
|
|
523
|
-
|
|
401
|
+
// write, a `=bindings.<label>` resource name bindings.json cannot answer, and
|
|
402
|
+
// an external task's mode, connection and folder are the serializer's
|
|
403
|
+
// (`taskShapeProblems`, `resolveResourceRef`, EXTERNAL_TASK_*).
|
|
524
404
|
const invalidBinding = [...(t.inputs ?? []), ...(t.outputs ?? [])]
|
|
525
405
|
.find((binding) => !TYPE_NAMES.has(binding.type));
|
|
526
406
|
if (invalidBinding)
|
|
@@ -530,16 +410,6 @@ externalJudge) {
|
|
|
530
410
|
message: `Task "${t.name}" binding "${'var' in invalidBinding ? invalidBinding.var : invalidBinding.name}" has unsupported type ${JSON.stringify(invalidBinding.type)}. Use a type name such as "string" or "jsonSchema"; jsonSchema(...) is only for declaring a structured case variable.`,
|
|
531
411
|
where: t.name,
|
|
532
412
|
});
|
|
533
|
-
// The builder binds a resource task itself: it mints the name/folderPath
|
|
534
|
-
// binding pair from the literal name and folder. A `=bindings.<label>` name
|
|
535
|
-
// or folder is read through bindings.json first, the way a connector's
|
|
536
|
-
// `connection:` label is; one bindings.json cannot answer is refused with the
|
|
537
|
-
// same text serialize throws (`resolveResourceRef`).
|
|
538
|
-
if (t.ref) {
|
|
539
|
-
const resolved = resolveResourceRef(t.ref, bindings, `Task "${t.name}"`);
|
|
540
|
-
if (isResourceLabelRefusal(resolved))
|
|
541
|
-
diags.push({ level: 'error', code: resolved.code, message: resolved.message, where: t.name });
|
|
542
|
-
}
|
|
543
413
|
// A custom row (`{ assign }`) is evaluated in array order, after the rows
|
|
544
414
|
// before it on the same task: it cannot read itself or a later row's
|
|
545
415
|
// variable (it would see the old value). `uip maestro case validate
|
|
@@ -628,39 +498,13 @@ externalJudge) {
|
|
|
628
498
|
message: `${family} task "${t.name}" has no configuration; empty data degrades to a runtime no-op placeholder.`,
|
|
629
499
|
where: t.name,
|
|
630
500
|
});
|
|
631
|
-
else
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
const judged = externalExecution(`${family} task "${t.name}"`, t.external, undefined);
|
|
639
|
-
if (judged?.code !== undefined)
|
|
640
|
-
diags.push({ level: 'error', code: judged.code, message: judged.message, where: t.name });
|
|
641
|
-
}
|
|
642
|
-
if (!t.external.connector?.key?.trim() || !t.external.connector?.action?.trim())
|
|
643
|
-
diags.push({
|
|
644
|
-
level: 'error',
|
|
645
|
-
code: 'EXTERNAL_TASK_DESCRIPTOR',
|
|
646
|
-
message: `${family} task "${t.name}" requires a generated Integration Service descriptor.`,
|
|
647
|
-
where: t.name,
|
|
648
|
-
});
|
|
649
|
-
if (!t.external.connector?.connection?.trim())
|
|
650
|
-
diags.push({
|
|
651
|
-
level: 'error',
|
|
652
|
-
code: 'EXTERNAL_TASK_CONNECTION',
|
|
653
|
-
message: `${family} task "${t.name}" requires a non-empty connection binding name.`,
|
|
654
|
-
where: t.name,
|
|
655
|
-
});
|
|
656
|
-
if (!t.external.connector?.folder?.trim())
|
|
657
|
-
diags.push({
|
|
658
|
-
level: 'error',
|
|
659
|
-
code: 'EXTERNAL_TASK_FOLDER',
|
|
660
|
-
message: `${family} task "${t.name}" requires a non-empty folder binding name.`,
|
|
661
|
-
where: t.name,
|
|
662
|
-
});
|
|
663
|
-
}
|
|
501
|
+
else if (!t.external.connector?.key?.trim() || !t.external.connector?.action?.trim())
|
|
502
|
+
diags.push({
|
|
503
|
+
level: 'error',
|
|
504
|
+
code: 'EXTERNAL_TASK_DESCRIPTOR',
|
|
505
|
+
message: `${family} task "${t.name}" requires a generated Integration Service descriptor.`,
|
|
506
|
+
where: t.name,
|
|
507
|
+
});
|
|
664
508
|
}
|
|
665
509
|
if (stage.exception && t.runOnce !== undefined)
|
|
666
510
|
diags.push({
|
|
@@ -4,13 +4,10 @@ import type { CaseCheckOpts } from './check.js';
|
|
|
4
4
|
/**
|
|
5
5
|
* Case's check options from a library the CLI already opened, or `{}` without one.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* With `bindings`, the binding-label rules (BINDING_SELF_NAME, BINDING_UNDECLARED),
|
|
12
|
-
* which Case `serialize` enforces through the same `bindingLabelRefusal`, and the
|
|
13
|
-
* BYOA facts and ledger, for the refusal `resolveCaseSubscription` makes.
|
|
7
|
+
* The library and bindings themselves, for the serializer `check` runs in
|
|
8
|
+
* collect mode (it reports every refusal compile makes, from compile's own
|
|
9
|
+
* code), plus the hooks for the warnings only `check` gives: a connector task's
|
|
10
|
+
* lookup rules and the schema-readiness warnings.
|
|
14
11
|
*
|
|
15
12
|
* @param library - The library `openCliLibrary` returned, or `undefined`.
|
|
16
13
|
* @param bindings - The project's bindings.json, as compile reads it.
|
package/dist/case/cli-library.js
CHANGED
|
@@ -8,18 +8,13 @@
|
|
|
8
8
|
* both `case check` and `case compile`'s check pass call.
|
|
9
9
|
*/
|
|
10
10
|
import { connectorCheckOpts } from '../core/cli-library.js';
|
|
11
|
-
import { externalDescriptorRefusal, externalExecution } from './external.js';
|
|
12
|
-
import { EVENT_GENERIC_NO_OBJECT, GenericEventNoObjectError, resolveCaseSubscription } from './subscription.js';
|
|
13
11
|
/**
|
|
14
12
|
* Case's check options from a library the CLI already opened, or `{}` without one.
|
|
15
13
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* With `bindings`, the binding-label rules (BINDING_SELF_NAME, BINDING_UNDECLARED),
|
|
21
|
-
* which Case `serialize` enforces through the same `bindingLabelRefusal`, and the
|
|
22
|
-
* BYOA facts and ledger, for the refusal `resolveCaseSubscription` makes.
|
|
14
|
+
* The library and bindings themselves, for the serializer `check` runs in
|
|
15
|
+
* collect mode (it reports every refusal compile makes, from compile's own
|
|
16
|
+
* code), plus the hooks for the warnings only `check` gives: a connector task's
|
|
17
|
+
* lookup rules and the schema-readiness warnings.
|
|
23
18
|
*
|
|
24
19
|
* @param library - The library `openCliLibrary` returned, or `undefined`.
|
|
25
20
|
* @param bindings - The project's bindings.json, as compile reads it.
|
|
@@ -30,46 +25,10 @@ export function caseCheckOpts(library, bindings) {
|
|
|
30
25
|
return bindings ? { bindings } : {};
|
|
31
26
|
const shared = connectorCheckOpts(library, bindings);
|
|
32
27
|
return {
|
|
28
|
+
library,
|
|
33
29
|
...(bindings ? { bindings } : {}),
|
|
34
|
-
connectorResolveError: shared.connectorResolveError,
|
|
35
|
-
connectorInputs: shared.connectorInputs,
|
|
36
30
|
lookups: shared.lookups,
|
|
37
31
|
resolutions: shared.resolutions,
|
|
38
|
-
eventFacts: shared.eventFacts,
|
|
39
|
-
byoa: shared.byoa,
|
|
40
32
|
schemaReadiness: shared.schemaReadiness,
|
|
41
|
-
externalDescriptorError: (kind, d) => {
|
|
42
|
-
let rc;
|
|
43
|
-
try {
|
|
44
|
-
rc = library.resolve(d.key, d.action, d.version, d.object);
|
|
45
|
-
}
|
|
46
|
-
catch {
|
|
47
|
-
return undefined; // connectorResolveError's to report
|
|
48
|
-
}
|
|
49
|
-
return externalDescriptorRefusal(kind, rc);
|
|
50
|
-
},
|
|
51
|
-
externalExecutionError: (owner, d, authored) => {
|
|
52
|
-
let rc;
|
|
53
|
-
try {
|
|
54
|
-
rc = library.resolve(d.key, d.action, d.version, d.object);
|
|
55
|
-
}
|
|
56
|
-
catch {
|
|
57
|
-
return undefined; // connectorResolveError's to report
|
|
58
|
-
}
|
|
59
|
-
const judged = externalExecution(owner, authored, rc.entry?.operation ?? {});
|
|
60
|
-
return judged?.code !== undefined ? { code: judged.code, message: judged.message } : undefined;
|
|
61
|
-
},
|
|
62
|
-
subscriptionError: (kind, sub) => {
|
|
63
|
-
try {
|
|
64
|
-
resolveCaseSubscription(library, kind, sub);
|
|
65
|
-
return undefined;
|
|
66
|
-
}
|
|
67
|
-
catch (error) {
|
|
68
|
-
return {
|
|
69
|
-
code: error instanceof GenericEventNoObjectError ? EVENT_GENERIC_NO_OBJECT : 'EVENT_UNKNOWN',
|
|
70
|
-
message: error instanceof Error ? error.message : String(error),
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
},
|
|
74
33
|
};
|
|
75
34
|
}
|
package/dist/case/serialize.d.ts
CHANGED
|
@@ -1,13 +1,59 @@
|
|
|
1
1
|
import { Library } from '../core/library.js';
|
|
2
|
-
import {
|
|
2
|
+
import { type BindingsView } from '../core/binding-messages.js';
|
|
3
3
|
import { type CaseJsonPreservation } from './preserve.js';
|
|
4
4
|
import type { BuiltCase } from './case-sdk.js';
|
|
5
|
+
/** A refusal a collect-mode serialize reported instead of throwing (see {@link CaseSerializeOpts.collect}). */
|
|
6
|
+
export interface CaseCollectedRefusal {
|
|
7
|
+
code: string;
|
|
8
|
+
/** The sentence an ordinary compile refuses with. */
|
|
9
|
+
message: string;
|
|
10
|
+
/** What to write instead, when the refusal knows ({@link CompileRefusal.suggestion}). */
|
|
11
|
+
suggestion?: string;
|
|
12
|
+
/**
|
|
13
|
+
* What the refusal is about, as `check` names it: a task's name, a stage's
|
|
14
|
+
* label, a trigger's label. Absent for a scope-level refusal.
|
|
15
|
+
*/
|
|
16
|
+
where?: string;
|
|
17
|
+
/** The id of the node `where` names (the task, stage or trigger node). */
|
|
18
|
+
nodeId?: string;
|
|
19
|
+
}
|
|
5
20
|
/** Optional wiring for connector tasks — the library that resolves a connector
|
|
6
21
|
* op, and the bindings that map its symbolic connection/folder to resources. */
|
|
7
22
|
export interface CaseSerializeOpts {
|
|
8
23
|
library?: Library;
|
|
9
|
-
bindings
|
|
24
|
+
/** The project's bindings.json (`Bindings`), or any view that answers the label rules. */
|
|
25
|
+
bindings?: BindingsView;
|
|
26
|
+
/**
|
|
27
|
+
* Collect mode, for `check` (docs/CHECK_COMPILE_CONTRACT_DESIGN.md §3.5, phase 8).
|
|
28
|
+
*
|
|
29
|
+
* When given, a {@link CompileRefusal} that escapes one node's emission is
|
|
30
|
+
* reported here instead of thrown, and emission goes on, so every later node
|
|
31
|
+
* is judged too. The nodes with their own catch are a trigger, a task, and
|
|
32
|
+
* each stage entry condition, exit condition and SLA (reported on its stage).
|
|
33
|
+
* A refused trigger or task is replaced by an opaque stand-in: its id, type
|
|
34
|
+
* and name, and `data: { standIn: true, spec: <the task as authored> }`, with
|
|
35
|
+
* nothing it had added to the root bindings. A throw that is no refusal is
|
|
36
|
+
* reported the same way, as COMPILE_FAILED, which is what `compile` reports.
|
|
37
|
+
* A refusal that names no one node (two stages with one label, a case exit
|
|
38
|
+
* rule, a variable default, a layout or preservation entry) is scope-level:
|
|
39
|
+
* it is reported with no `where`, and collection stops there, so `serialize`
|
|
40
|
+
* returns `undefined`. An `internal SDK error:` throw is never caught. The
|
|
41
|
+
* output is for nothing to read: no caller writes it.
|
|
42
|
+
*
|
|
43
|
+
* For instance, a case whose connector task "Notify" names an input the
|
|
44
|
+
* operation does not declare, and whose stage "Review" exits to a stage that
|
|
45
|
+
* does not exist, reports both, where an ordinary compile stops at the first.
|
|
46
|
+
*/
|
|
47
|
+
collect?: (refusal: CaseCollectedRefusal) => void;
|
|
10
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Serialize `built` to a caseplan. With {@link CaseSerializeOpts.collect}, the
|
|
51
|
+
* refusals are reported instead of thrown, and the result is `undefined` when
|
|
52
|
+
* a scope-level refusal stopped collection.
|
|
53
|
+
*/
|
|
54
|
+
export declare function serialize(built: BuiltCase, opts: CaseSerializeOpts & {
|
|
55
|
+
collect: NonNullable<CaseSerializeOpts['collect']>;
|
|
56
|
+
}): Record<string, unknown> | undefined;
|
|
11
57
|
export declare function serialize(built: BuiltCase, opts?: CaseSerializeOpts): Record<string, unknown>;
|
|
12
58
|
export declare const CASE_NODE_LAYOUT_FIELDS: readonly ["position", "style", "measured", "width", "height", "zIndex"];
|
|
13
59
|
/** How `applyPreservation` treats a field the target already has. */
|
package/dist/case/serialize.js
CHANGED
|
@@ -32,6 +32,7 @@ import { responseJsonSchema } from '../core/connector-response-schema.js';
|
|
|
32
32
|
import { ruleShapeProblems } from './rule-options.js';
|
|
33
33
|
import { CompileRefusal } from '../core/compile-refusal.js';
|
|
34
34
|
import { implicitInArgBuiltTrigger, unboundInArgMessage } from './in-arg-binding.js';
|
|
35
|
+
import { jsonClone } from '../core/json-clone.js';
|
|
35
36
|
import { taskShapeProblems } from './task-shape.js';
|
|
36
37
|
import { connectorPlaceholderData, isConnectorPlaceholder } from './connector-placeholder.js';
|
|
37
38
|
/** The ids a serialize pass hashed from element ids, by the document it returned. */
|
|
@@ -92,6 +93,100 @@ function conditionNamer(built) {
|
|
|
92
93
|
};
|
|
93
94
|
}
|
|
94
95
|
export function serialize(built, opts = {}) {
|
|
96
|
+
const collect = opts.collect;
|
|
97
|
+
if (!collect)
|
|
98
|
+
return serializeCase(built, opts);
|
|
99
|
+
try {
|
|
100
|
+
return serializeCase(built, opts);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
// A node's own refusal was taken by its catch (`collected`), so one that
|
|
104
|
+
// gets here was thrown outside any one node: scope-level, and collection
|
|
105
|
+
// stops. A throw that is no refusal is compile's COMPILE_FAILED; an internal
|
|
106
|
+
// SDK error is passed on.
|
|
107
|
+
const refusal = refusalOf(error);
|
|
108
|
+
collect({ code: refusal.code, message: refusal.message, ...(refusal.suggestion !== undefined ? { suggestion: refusal.suggestion } : {}) });
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* What a throw is, in collect mode: a refusal as it is, and any other throw (a
|
|
114
|
+
* `TypeError` on a spec the serializer has no refusal for) as COMPILE_FAILED,
|
|
115
|
+
* which is what `compile` reports for it. An `internal SDK error:` is passed on.
|
|
116
|
+
*/
|
|
117
|
+
function refusalOf(error) {
|
|
118
|
+
if (error instanceof CompileRefusal)
|
|
119
|
+
return error;
|
|
120
|
+
if (!(error instanceof Error) || error.message.startsWith('internal SDK error:'))
|
|
121
|
+
throw error;
|
|
122
|
+
return new CompileRefusal('COMPILE_FAILED', error.message);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Emit one node under collect mode's per-node catch. Outside collect mode this
|
|
126
|
+
* is just `emit()`. In it, a refusal escaping `emit` is reported on `where`
|
|
127
|
+
* (node `nodeId`), the root bindings and output ids the node had claimed are
|
|
128
|
+
* undone, and `standIn()` is returned in its place.
|
|
129
|
+
*
|
|
130
|
+
* The variable companions a node registered are kept, and its stand-in
|
|
131
|
+
* registers the rest (`standInCompanions`): they come from the outputs as
|
|
132
|
+
* authored, not from the part that was refused, so a preserved variable row or
|
|
133
|
+
* a read of one does not fail just because its writer was refused.
|
|
134
|
+
*
|
|
135
|
+
* The mark is the collections' sizes, so a node that emits cleanly costs
|
|
136
|
+
* nothing extra: every collection here is only added to, and Maps and Sets
|
|
137
|
+
* iterate in insertion order, so undoing is deleting what came after the mark.
|
|
138
|
+
*/
|
|
139
|
+
function collected(resolver, where, nodeId, emit, standIn) {
|
|
140
|
+
const collect = resolver.collect;
|
|
141
|
+
if (!collect)
|
|
142
|
+
return emit();
|
|
143
|
+
const mark = {
|
|
144
|
+
bindingSink: resolver.bindingSink.size,
|
|
145
|
+
offlineBindingIds: resolver.offlineBindingIds.size,
|
|
146
|
+
outputIds: resolver.outputIds.size,
|
|
147
|
+
elementSeededIds: resolver.elementSeededIds.length,
|
|
148
|
+
};
|
|
149
|
+
try {
|
|
150
|
+
return emit();
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
const refusal = refusalOf(error);
|
|
154
|
+
collect({
|
|
155
|
+
code: refusal.code, message: refusal.message,
|
|
156
|
+
...(refusal.suggestion !== undefined ? { suggestion: refusal.suggestion } : {}),
|
|
157
|
+
where, nodeId,
|
|
158
|
+
});
|
|
159
|
+
const after = (keys, size) => [...keys].slice(size);
|
|
160
|
+
for (const k of after(resolver.bindingSink.keys(), mark.bindingSink))
|
|
161
|
+
resolver.bindingSink.delete(k);
|
|
162
|
+
for (const k of after(resolver.offlineBindingIds.keys(), mark.offlineBindingIds))
|
|
163
|
+
resolver.offlineBindingIds.delete(k);
|
|
164
|
+
for (const k of after(resolver.outputIds, mark.outputIds))
|
|
165
|
+
resolver.outputIds.delete(k);
|
|
166
|
+
resolver.elementSeededIds.length = mark.elementSeededIds;
|
|
167
|
+
return standIn();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/** The root companions a stand-in's authored outputs need, the way an emitted node registers them. */
|
|
171
|
+
function standInCompanions(resolver, outputs) {
|
|
172
|
+
for (const o of outputs ?? [])
|
|
173
|
+
if (!resolver.varCompanions.has(o.var))
|
|
174
|
+
resolver.varCompanions.set(o.var, { id: o.var, name: o.var, type: o.type, elementId: 'root', custom: true });
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* The value an author wrote, as plain JSON, for a stand-in. Deterministic, so
|
|
178
|
+
* two compiles of one unchanged task give equal stand-ins
|
|
179
|
+
* (docs/CHECK_COMPILE_CONTRACT_DESIGN.md D5).
|
|
180
|
+
*/
|
|
181
|
+
function authoredJson(value) {
|
|
182
|
+
try {
|
|
183
|
+
return value === undefined ? {} : jsonClone(value);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return {};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function serializeCase(built, opts) {
|
|
95
190
|
const versionMismatch = caseVersionMismatch(built);
|
|
96
191
|
if (versionMismatch)
|
|
97
192
|
throw new CompileRefusal('VERSION_MISMATCH', versionMismatch);
|
|
@@ -119,6 +214,7 @@ export function serialize(built, opts = {}) {
|
|
|
119
214
|
businessEvents: new Set((built.businessEvents ?? []).map((event) => event.name)),
|
|
120
215
|
conditionName: conditionNamer(built),
|
|
121
216
|
elementSeededIds: [],
|
|
217
|
+
...(opts.collect ? { collect: opts.collect } : {}),
|
|
122
218
|
};
|
|
123
219
|
// Preallocate SLA + escalation ids (before any rule is emitted) so a
|
|
124
220
|
// `sla-status-change` rule can resolve the `slaId`/`escalationId` it points at.
|
|
@@ -259,7 +355,7 @@ export function serialize(built, opts = {}) {
|
|
|
259
355
|
// outputs bound (collected during stage emission above). A declared var wins
|
|
260
356
|
// on a name clash. An own-name output (`id == var`) writes no root entry: like
|
|
261
357
|
// the Designer, the output row is itself the variable `=vars.<id>` reads.
|
|
262
|
-
const variables = emitVariables(built.vars, ids, triggerNodeByRef, triggerNodes, built.triggers);
|
|
358
|
+
const variables = emitVariables(built.vars, ids, triggerNodeByRef, triggerNodes, built.triggers, resolver.collect);
|
|
263
359
|
const inputOutputs = variables.inputOutputs;
|
|
264
360
|
const declared = new Set(inputOutputs.map((e) => e.id));
|
|
265
361
|
for (const [name, companion] of resolver.varCompanions)
|
|
@@ -961,7 +1057,7 @@ function reseed(value, ids) {
|
|
|
961
1057
|
value[key] = next;
|
|
962
1058
|
}
|
|
963
1059
|
}
|
|
964
|
-
function emitVariables(vars, ids, triggerNodeByRef, triggerNodes, triggers) {
|
|
1060
|
+
function emitVariables(vars, ids, triggerNodeByRef, triggerNodes, triggers, collect) {
|
|
965
1061
|
const implicit = implicitInArgBuiltTrigger(triggers);
|
|
966
1062
|
const inputs = [];
|
|
967
1063
|
const outputs = [];
|
|
@@ -1019,8 +1115,9 @@ function emitVariables(vars, ids, triggerNodeByRef, triggerNodes, triggers) {
|
|
|
1019
1115
|
// The Designer writes no other In-arg shape, and `uip maestro case validate
|
|
1020
1116
|
// --strict` refuses an In-arg without its companion
|
|
1021
1117
|
// (STRICT_INPUTOUTPUTS_COMPANION). Without `{ from }` the arg binds to the
|
|
1022
|
-
// primary trigger (in-arg-binding.ts); when there is none it is refused
|
|
1023
|
-
//
|
|
1118
|
+
// primary trigger (in-arg-binding.ts); when there is none it is refused.
|
|
1119
|
+
// In collect mode each such arg is reported on its own name and left out,
|
|
1120
|
+
// so `check` reports every one, once, from this rule.
|
|
1024
1121
|
let node;
|
|
1025
1122
|
if (vd.sourceTrigger) {
|
|
1026
1123
|
node = triggerNodeByRef.get(vd.sourceTrigger);
|
|
@@ -1029,8 +1126,13 @@ function emitVariables(vars, ids, triggerNodeByRef, triggerNodes, triggers) {
|
|
|
1029
1126
|
}
|
|
1030
1127
|
else if (implicit !== undefined)
|
|
1031
1128
|
node = triggerNodes[implicit];
|
|
1032
|
-
else
|
|
1033
|
-
|
|
1129
|
+
else {
|
|
1130
|
+
const refusal = new CompileRefusal('CASE_IN_ARG_UNBOUND', unboundInArgMessage(vd.name, triggers));
|
|
1131
|
+
if (!collect)
|
|
1132
|
+
throw refusal;
|
|
1133
|
+
collect({ code: refusal.code, message: refusal.message, where: vd.name });
|
|
1134
|
+
continue;
|
|
1135
|
+
}
|
|
1034
1136
|
entry.elementId = node.id;
|
|
1035
1137
|
inputs.push(entry);
|
|
1036
1138
|
inputOutputs.push({ id: vd.name, name: vd.name, type: vd.type, elementId: node.id, ...jsonSchemaFields(vd) });
|
|
@@ -1076,55 +1178,73 @@ function emitTriggers(triggers, ids, resolver) {
|
|
|
1076
1178
|
const list = triggers.length ? triggers : [{ kind: 'manual' }];
|
|
1077
1179
|
return list.map((t, i) => {
|
|
1078
1180
|
const triggerId = i === 0 ? 'trigger_1' : ids.make('trigger_', 6, `trigger:${i}:${t.name ?? t.kind}`);
|
|
1079
|
-
|
|
1080
|
-
if (i > 0)
|
|
1081
|
-
data.parentElement = { id: 'root', type: 'case-management:root' };
|
|
1082
|
-
if (CASE_FORMAT_PROFILE.triggerTypeVersion)
|
|
1083
|
-
data.typeVersion = CASE_FORMAT_PROFILE.triggerTypeVersion;
|
|
1181
|
+
// Refused in collect mode, it is stood in, and every later node is still judged.
|
|
1084
1182
|
const label = t.name ?? `Trigger ${i + 1}`;
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1183
|
+
return collected(resolver, label, triggerId, () => emitTrigger(t, i, triggerId, resolver), () => {
|
|
1184
|
+
standInCompanions(resolver, t.eventOutputs);
|
|
1185
|
+
// Labelled as an emitted trigger is, so a `.layout({ triggers })` entry still finds it.
|
|
1186
|
+
return {
|
|
1187
|
+
id: triggerId,
|
|
1188
|
+
[CASE_FORMAT_PROFILE.nodeTypeField]: CASE_FORMAT_PROFILE.triggerNodeType,
|
|
1189
|
+
data: {
|
|
1190
|
+
...(CASE_FORMAT_PROFILE.triggerLabelField === 'display' ? { display: { label } } : { label }),
|
|
1191
|
+
standIn: true,
|
|
1192
|
+
spec: authoredJson(t),
|
|
1193
|
+
},
|
|
1096
1194
|
};
|
|
1097
|
-
}
|
|
1098
|
-
else if (t.kind === 'event') {
|
|
1099
|
-
// With no subscription this remains the byte-identical, serviceType-only
|
|
1100
|
-
// placeholder. A subscription resolves through the same library/binding
|
|
1101
|
-
// machinery as connector tasks and wait-for-connector bags.
|
|
1102
|
-
const bag = t.eventSubscription
|
|
1103
|
-
? resolvedSubscriptionBag('trigger', t.eventSubscription, resolver, triggerId, `Event trigger "${label}"`)
|
|
1104
|
-
: {
|
|
1105
|
-
serviceType: CASE_FORMAT_PROFILE.eventServiceType,
|
|
1106
|
-
// A placeholder that names its source object carries it the way
|
|
1107
|
-
// `case sdd convert` does: one `context[objectName]` row.
|
|
1108
|
-
...(t.eventObject ? { context: [io('objectName', 'string', { value: t.eventObject })] } : {}),
|
|
1109
|
-
};
|
|
1110
|
-
if (t.eventOutputs?.length) {
|
|
1111
|
-
const outputs = t.eventOutputs.map((o) => {
|
|
1112
|
-
const field = o.source.replace(/^=/, '').split('.').pop() || o.source;
|
|
1113
|
-
if (!resolver.varCompanions.has(o.var))
|
|
1114
|
-
resolver.varCompanions.set(o.var, { id: o.var, name: o.var, type: o.type, elementId: 'root', custom: true });
|
|
1115
|
-
return { name: field, var: o.var, source: o.source, type: o.type, value: o.var };
|
|
1116
|
-
});
|
|
1117
|
-
bag.outputs = [...(bag.outputs ?? []), ...outputs];
|
|
1118
|
-
}
|
|
1119
|
-
data[CASE_FORMAT_PROFILE.triggerBagField] = bag;
|
|
1120
|
-
}
|
|
1121
|
-
return {
|
|
1122
|
-
id: triggerId,
|
|
1123
|
-
[CASE_FORMAT_PROFILE.nodeTypeField]: CASE_FORMAT_PROFILE.triggerNodeType,
|
|
1124
|
-
data,
|
|
1125
|
-
};
|
|
1195
|
+
});
|
|
1126
1196
|
});
|
|
1127
1197
|
}
|
|
1198
|
+
/** One trigger node (see {@link emitTriggers}). */
|
|
1199
|
+
function emitTrigger(t, i, triggerId, resolver) {
|
|
1200
|
+
const data = {};
|
|
1201
|
+
if (i > 0)
|
|
1202
|
+
data.parentElement = { id: 'root', type: 'case-management:root' };
|
|
1203
|
+
if (CASE_FORMAT_PROFILE.triggerTypeVersion)
|
|
1204
|
+
data.typeVersion = CASE_FORMAT_PROFILE.triggerTypeVersion;
|
|
1205
|
+
const label = t.name ?? `Trigger ${i + 1}`;
|
|
1206
|
+
if (CASE_FORMAT_PROFILE.triggerLabelField === 'display')
|
|
1207
|
+
data.display = { label };
|
|
1208
|
+
else
|
|
1209
|
+
data.label = label;
|
|
1210
|
+
if (t.description)
|
|
1211
|
+
data.description = t.description;
|
|
1212
|
+
if (t.kind === 'timer') {
|
|
1213
|
+
data[CASE_FORMAT_PROFILE.triggerBagField] = {
|
|
1214
|
+
timerType: 'timeCycle',
|
|
1215
|
+
timeCycle: t.timeCycle,
|
|
1216
|
+
serviceType: CASE_FORMAT_PROFILE.timerServiceType,
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
else if (t.kind === 'event') {
|
|
1220
|
+
// With no subscription this remains the byte-identical, serviceType-only
|
|
1221
|
+
// placeholder. A subscription resolves through the same library/binding
|
|
1222
|
+
// machinery as connector tasks and wait-for-connector bags.
|
|
1223
|
+
const bag = t.eventSubscription
|
|
1224
|
+
? resolvedSubscriptionBag('trigger', t.eventSubscription, resolver, triggerId, `Event trigger "${label}"`)
|
|
1225
|
+
: {
|
|
1226
|
+
serviceType: CASE_FORMAT_PROFILE.eventServiceType,
|
|
1227
|
+
// A placeholder that names its source object carries it the way
|
|
1228
|
+
// `case sdd convert` does: one `context[objectName]` row.
|
|
1229
|
+
...(t.eventObject ? { context: [io('objectName', 'string', { value: t.eventObject })] } : {}),
|
|
1230
|
+
};
|
|
1231
|
+
if (t.eventOutputs?.length) {
|
|
1232
|
+
const outputs = t.eventOutputs.map((o) => {
|
|
1233
|
+
const field = o.source.replace(/^=/, '').split('.').pop() || o.source;
|
|
1234
|
+
if (!resolver.varCompanions.has(o.var))
|
|
1235
|
+
resolver.varCompanions.set(o.var, { id: o.var, name: o.var, type: o.type, elementId: 'root', custom: true });
|
|
1236
|
+
return { name: field, var: o.var, source: o.source, type: o.type, value: o.var };
|
|
1237
|
+
});
|
|
1238
|
+
bag.outputs = [...(bag.outputs ?? []), ...outputs];
|
|
1239
|
+
}
|
|
1240
|
+
data[CASE_FORMAT_PROFILE.triggerBagField] = bag;
|
|
1241
|
+
}
|
|
1242
|
+
return {
|
|
1243
|
+
id: triggerId,
|
|
1244
|
+
[CASE_FORMAT_PROFILE.nodeTypeField]: CASE_FORMAT_PROFILE.triggerNodeType,
|
|
1245
|
+
data,
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1128
1248
|
/** A stage's task sets as the serializer writes them — one set per parallel task (`v31TaskSets`). */
|
|
1129
1249
|
function laneSets(lanes) {
|
|
1130
1250
|
return v31TaskSets(lanes, (task) => task.entry.some((condition) => condition.rules.some((group) => group.some((r) => r.rule === 'runs-sequentially'))));
|
|
@@ -1149,14 +1269,18 @@ function emitStage(stage, lanes, si, stageId, laneTaskIds, ids, resolver) {
|
|
|
1149
1269
|
data.description = stage.description;
|
|
1150
1270
|
if (stage.required !== undefined)
|
|
1151
1271
|
data.isRequired = stage.required;
|
|
1272
|
+
// Each condition and SLA is judged on its own in collect mode, reported on the
|
|
1273
|
+
// stage; a refused one is left out, and the stage's other parts are still judged.
|
|
1274
|
+
const onStage = (emit) => collected(resolver, stage.label, stageId, emit, () => undefined);
|
|
1275
|
+
const present = (items) => items.filter((item) => item !== undefined);
|
|
1152
1276
|
if (stage.entry.length || stage.exception) {
|
|
1153
|
-
data.entryConditions = stage.entry.map((c, i) => emitEntry(c, `${stage.label}:entry:${i}`, stageId, stage.label, ids, resolver));
|
|
1277
|
+
data.entryConditions = present(stage.entry.map((c, i) => onStage(() => emitEntry(c, `${stage.label}:entry:${i}`, stageId, stage.label, ids, resolver))));
|
|
1154
1278
|
}
|
|
1155
1279
|
if (stage.exit.length || stage.exception) {
|
|
1156
|
-
data.exitConditions = stage.exit.map((c, i) => emitExit(c, `${stage.label}:exit:${i}`, stageId, stage.label, ids, resolver));
|
|
1280
|
+
data.exitConditions = present(stage.exit.map((c, i) => onStage(() => emitExit(c, `${stage.label}:exit:${i}`, stageId, stage.label, ids, resolver))));
|
|
1157
1281
|
}
|
|
1158
1282
|
if (stage.slaRules.length) {
|
|
1159
|
-
data.slaRules = stage.slaRules.map((s, i, all) => emitSla(s, i, all.length, ids, resolver));
|
|
1283
|
+
data.slaRules = present(stage.slaRules.map((s, i, all) => onStage(() => emitSla(s, i, all.length, ids, resolver))));
|
|
1160
1284
|
}
|
|
1161
1285
|
const nodeType = stage.exception && CASE_FORMAT_PROFILE.exceptionStageField === CASE_FORMAT_PROFILE.nodeTypeField
|
|
1162
1286
|
? CASE_FORMAT_PROFILE.exceptionStageValue
|
|
@@ -1172,8 +1296,23 @@ function emitStage(stage, lanes, si, stageId, laneTaskIds, ids, resolver) {
|
|
|
1172
1296
|
}
|
|
1173
1297
|
function emitTask(task, stageLabel, taskId, ids, resolver) {
|
|
1174
1298
|
const type = task.kind === 'connector' ? CASE_FORMAT_PROFILE.connectorActivityNodeType : task.kind;
|
|
1175
|
-
const node = { id: taskId, displayName: task.name, [CASE_FORMAT_PROFILE.nodeTypeField]: type };
|
|
1176
1299
|
const stageId = resolver.stageIdByLabel.get(stageLabel);
|
|
1300
|
+
// Collect mode's per-task catch: a refused task is stood in, and every later
|
|
1301
|
+
// task is still judged.
|
|
1302
|
+
return collected(resolver, task.name, taskId, () => emitTaskNode(task, stageLabel, stageId, taskId, type, ids, resolver), () => {
|
|
1303
|
+
standInCompanions(resolver, task.outputs);
|
|
1304
|
+
return {
|
|
1305
|
+
id: taskId,
|
|
1306
|
+
displayName: task.name,
|
|
1307
|
+
[CASE_FORMAT_PROFILE.nodeTypeField]: type,
|
|
1308
|
+
elementId: `${stageId}-${taskId}`,
|
|
1309
|
+
data: { standIn: true, spec: authoredJson(task) },
|
|
1310
|
+
};
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
/** One task node (see {@link emitTask}). */
|
|
1314
|
+
function emitTaskNode(task, stageLabel, stageId, taskId, type, ids, resolver) {
|
|
1315
|
+
const node = { id: taskId, displayName: task.name, [CASE_FORMAT_PROFILE.nodeTypeField]: type };
|
|
1177
1316
|
node.data = taskData(task, resolver, taskId, stageId, ids);
|
|
1178
1317
|
// Every task carries `elementId: "<stageId>-<taskId>"`, the identity the
|
|
1179
1318
|
// converter matches a task's io rows against:
|
|
@@ -1380,10 +1519,14 @@ function isResolvedSubscription(spec) {
|
|
|
1380
1519
|
/** Resolve a Case event/wait subscription into the designer/runtime bag pinned
|
|
1381
1520
|
* by the archived v1 artifacts. Placeholder bags never enter this path. */
|
|
1382
1521
|
function resolvedSubscriptionBag(kind, spec, resolver, elementId, where) {
|
|
1522
|
+
// The binding labels need no library, so they are judged first: a `check`
|
|
1523
|
+
// without one still reports them.
|
|
1524
|
+
refuseBindingLabels(resolver, spec, { where, target: { key: spec.connector, action: spec.event } });
|
|
1383
1525
|
if (!resolver.library)
|
|
1384
1526
|
throw new CompileRefusal('LIBRARY_REQUIRED', `${kind === 'trigger' ? 'event trigger' : 'wait-for-connector'} needs the connector library — `
|
|
1385
1527
|
+ `pass { library } to serialize or use uip maestro case compile --library.`);
|
|
1386
|
-
const
|
|
1528
|
+
const library = resolver.library;
|
|
1529
|
+
const rc = onTask(where, () => resolveCaseSubscription(library, kind, spec, { where, bindings: resolver.bindings }));
|
|
1387
1530
|
const op = rc.entry.operation ?? {};
|
|
1388
1531
|
const context = [io('connectorKey', 'string', { value: rc.connectorKey })];
|
|
1389
1532
|
const target = { key: spec.connector, action: spec.event };
|
|
@@ -1399,7 +1542,7 @@ function resolvedSubscriptionBag(kind, spec, resolver, elementId, where) {
|
|
|
1399
1542
|
return {
|
|
1400
1543
|
serviceType: kind === 'trigger' ? CASE_FORMAT_PROFILE.eventServiceType : CASE_FORMAT_PROFILE.waitConnectorServiceType,
|
|
1401
1544
|
context,
|
|
1402
|
-
inputs: [eventInput(resolveEventWhere(spec.where,
|
|
1545
|
+
inputs: [eventInput(resolveEventWhere(spec.where, library.lookupResolutions, where), elementId)],
|
|
1403
1546
|
outputs: connectorOutputs(rc, elementId, resolver),
|
|
1404
1547
|
bindings: [],
|
|
1405
1548
|
};
|
|
@@ -1754,9 +1897,11 @@ function businessRuleTaskData(task, resolver, elementId, ids) {
|
|
|
1754
1897
|
* referenced as `=bindings.<id>`, with the binding entry collected at case root.
|
|
1755
1898
|
*/
|
|
1756
1899
|
function connectorTaskData(spec, resolver, elementId, where, executionType, event) {
|
|
1900
|
+
refuseBindingLabels(resolver, spec, { where, target: { key: spec.key, action: spec.action } });
|
|
1757
1901
|
if (!resolver.library)
|
|
1758
1902
|
throw new CompileRefusal('LIBRARY_REQUIRED', `connector task needs the connector library — pass { library } to serialize or use uip maestro case compile --library.`);
|
|
1759
|
-
const
|
|
1903
|
+
const library = resolver.library;
|
|
1904
|
+
const rc = onTask(where, () => resolveOperation(library, spec));
|
|
1760
1905
|
const op = rc.entry.operation ?? {};
|
|
1761
1906
|
const context = [
|
|
1762
1907
|
io('connectorKey', 'string', { value: rc.connectorKey }),
|
|
@@ -1784,11 +1929,11 @@ function connectorTaskData(spec, resolver, elementId, where, executionType, even
|
|
|
1784
1929
|
// operation); the SPELLING of the path/query keys is Case's own: the Case
|
|
1785
1930
|
// designer camelCases them (`send_as` → `sendAs`, see
|
|
1786
1931
|
// `fixtures/execute-connector-activity.sample.json`), and the body is nested.
|
|
1787
|
-
const slots = buildConnectorInputs(rc, spec.inputs ?? {}, {
|
|
1932
|
+
const slots = onTask(where, () => buildConnectorInputs(rc, spec.inputs ?? {}, {
|
|
1788
1933
|
dialect: EQUALS_EXPR_DIALECT,
|
|
1789
|
-
resolutions:
|
|
1790
|
-
lookups:
|
|
1791
|
-
});
|
|
1934
|
+
resolutions: library.lookupResolutions,
|
|
1935
|
+
lookups: library.lookupSpecs(spec.key, spec.action, spec.version, spec.object),
|
|
1936
|
+
}));
|
|
1792
1937
|
const camelKeys = (slot) => Object.fromEntries(Object.entries(slot ?? {}).map(([name, value]) => [toCamel(name), value]));
|
|
1793
1938
|
const pathParameters = camelKeys(slots.pathParameters);
|
|
1794
1939
|
const queryParameters = camelKeys(slots.queryParameters);
|
|
@@ -1822,21 +1967,31 @@ function connectorTaskData(spec, resolver, elementId, where, executionType, even
|
|
|
1822
1967
|
* "v2"`, and the sync or async service type that execution type gives.
|
|
1823
1968
|
*/
|
|
1824
1969
|
function externalTaskData(kind, spec, resolver, elementId, where) {
|
|
1970
|
+
// What needs no library first, so a `check` without one still reports it: the
|
|
1971
|
+
// authored mode and event judged alone (`externalExecution` with no operation
|
|
1972
|
+
// only refuses what is wrong whatever the library says), and the binding names.
|
|
1973
|
+
const authored = externalExecution(where, spec, undefined);
|
|
1974
|
+
if (authored?.code !== undefined)
|
|
1975
|
+
throw new CompileRefusal(authored.code, authored.message);
|
|
1976
|
+
if (!spec.connector.connection?.trim())
|
|
1977
|
+
throw new CompileRefusal('EXTERNAL_TASK_CONNECTION', `${where} requires a non-empty connection binding name.`);
|
|
1978
|
+
if (!spec.connector.folder?.trim())
|
|
1979
|
+
throw new CompileRefusal('EXTERNAL_TASK_FOLDER', `${where} requires a non-empty folder binding name.`);
|
|
1980
|
+
refuseBindingLabels(resolver, spec.connector, { where, target: { key: spec.connector.key, action: spec.connector.action } });
|
|
1825
1981
|
if (!resolver.library)
|
|
1826
1982
|
throw new CompileRefusal('LIBRARY_REQUIRED', `${kind} task needs the connector library — pass { library } to serialize or use uip maestro case compile --library.`);
|
|
1827
|
-
const
|
|
1983
|
+
const library = resolver.library;
|
|
1984
|
+
const rc = onTask(where, () => resolveOperation(library, spec.connector));
|
|
1828
1985
|
const refused = externalDescriptorRefusal(kind, rc);
|
|
1829
1986
|
if (refused)
|
|
1830
|
-
throw new CompileRefusal(EXTERNAL_TASK_DESCRIPTOR_CONTRACT, refused);
|
|
1831
|
-
// The authored mode and event, else the library's (#1369)
|
|
1987
|
+
throw new CompileRefusal(EXTERNAL_TASK_DESCRIPTOR_CONTRACT, `${where}: ${refused}`);
|
|
1988
|
+
// The authored mode and event, else the library's (#1369).
|
|
1832
1989
|
const execution = externalExecution(where, spec, rc.entry?.operation ?? {});
|
|
1833
1990
|
if (!execution)
|
|
1834
1991
|
throw new Error(`internal SDK error: ${where} was judged without its library operation.`);
|
|
1835
1992
|
if (execution.code !== undefined)
|
|
1836
1993
|
throw new CompileRefusal(execution.code, execution.message);
|
|
1837
1994
|
const { mode, event } = execution;
|
|
1838
|
-
if (!spec.connector.connection || !spec.connector.folder)
|
|
1839
|
-
throw new CompileRefusal(spec.connector.connection ? 'EXTERNAL_TASK_FOLDER' : 'EXTERNAL_TASK_CONNECTION', `${kind} task requires non-empty connection and folder binding names.`);
|
|
1840
1995
|
const data = connectorTaskData(spec.connector, resolver, elementId, where, mode, event);
|
|
1841
1996
|
const context = data.context;
|
|
1842
1997
|
const metadataAt = context.findIndex((row) => row.name === 'metadata');
|
|
@@ -1886,6 +2041,53 @@ function offlineBindingId(resolver, seed) {
|
|
|
1886
2041
|
}
|
|
1887
2042
|
return id;
|
|
1888
2043
|
}
|
|
2044
|
+
/**
|
|
2045
|
+
* A connector operation from the library. A throw `library.resolve` makes on no
|
|
2046
|
+
* purpose is still a refusal, CONNECTOR_RESOLVE_FAILED, the code `check` has
|
|
2047
|
+
* always given it (#1015).
|
|
2048
|
+
*/
|
|
2049
|
+
function resolveOperation(library, spec) {
|
|
2050
|
+
try {
|
|
2051
|
+
return library.resolve(spec.key, spec.action, spec.version, spec.object);
|
|
2052
|
+
}
|
|
2053
|
+
catch (error) {
|
|
2054
|
+
if (error instanceof CompileRefusal || (error instanceof Error && error.message.startsWith('internal SDK error:')))
|
|
2055
|
+
throw error;
|
|
2056
|
+
throw new CompileRefusal('CONNECTOR_RESOLVE_FAILED', error instanceof Error ? error.message : String(error));
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
/**
|
|
2060
|
+
* Run `fn`, naming the task or subscription site (`where`) in a refusal it throws. The library's
|
|
2061
|
+
* refusals (the operation, its fields) name the connector, not the task: they
|
|
2062
|
+
* say which task as `check` always has (`Connector task "Post": connector not
|
|
2063
|
+
* in library: …`).
|
|
2064
|
+
*/
|
|
2065
|
+
function onTask(where, fn) {
|
|
2066
|
+
try {
|
|
2067
|
+
return fn();
|
|
2068
|
+
}
|
|
2069
|
+
catch (error) {
|
|
2070
|
+
// A refusal that already names the site (a BYOA refusal) is passed on as it is.
|
|
2071
|
+
if (!(error instanceof CompileRefusal) || error.message.startsWith(`${where}: `))
|
|
2072
|
+
throw error;
|
|
2073
|
+
throw new CompileRefusal(error.code, `${where}: ${error.message}`, error.suggestion);
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
/**
|
|
2077
|
+
* Refuse a connection or folder label bindings.json cannot answer, before
|
|
2078
|
+
* anything that needs the library: the refusal `bindingRef` and
|
|
2079
|
+
* `folderBindingRef` make, so `check` reports it with no library too.
|
|
2080
|
+
*/
|
|
2081
|
+
function refuseBindingLabels(resolver, labels, site) {
|
|
2082
|
+
for (const attribute of ['connection', 'folder']) {
|
|
2083
|
+
const name = labels[attribute];
|
|
2084
|
+
if (!name)
|
|
2085
|
+
continue;
|
|
2086
|
+
const refused = bindingLabelRefusal(resolver.bindings, name, attribute, site.where, site.target);
|
|
2087
|
+
if (refused)
|
|
2088
|
+
throw refused;
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
1889
2091
|
/** Resolve a symbolic connection/folder to a root binding entry (collected once),
|
|
1890
2092
|
* returning its id + resourceKey for the `=bindings.<id>` reference. */
|
|
1891
2093
|
function bindingRef(resolver, name, resource, propertyAttribute, displayName, site) {
|
|
@@ -22,16 +22,18 @@ export declare class GenericEventNoObjectError extends CompileRefusal {
|
|
|
22
22
|
/**
|
|
23
23
|
* Resolve a Case event subscription: `library.resolveEvent` with the
|
|
24
24
|
* subscription's own `version` and `object`, then refuse a GENERIC event the
|
|
25
|
-
* author named no object for.
|
|
25
|
+
* author named no object for. Whatever `resolveEvent` throws (a connector or
|
|
26
|
+
* event the library does not carry, a version or object it does not resolve)
|
|
27
|
+
* is one condition, an event compile cannot place: EVENT_UNKNOWN, as Flow's
|
|
28
|
+
* compile says.
|
|
26
29
|
*
|
|
27
30
|
* @param library - The connector library.
|
|
28
31
|
* @param kind - `trigger` for an event trigger, `event` for a wait-for-connector task or rule.
|
|
29
32
|
* @param sub - The authored subscription.
|
|
30
33
|
* @param byoa - Where the subscription is and the bindings, to refuse an
|
|
31
|
-
* unverified BYOA connection
|
|
32
|
-
* that rule from the shared event rules).
|
|
34
|
+
* unverified BYOA connection.
|
|
33
35
|
* @returns The resolved event.
|
|
34
|
-
* @throws
|
|
36
|
+
* @throws EVENT_UNKNOWN, {@link GenericEventNoObjectError}, or a `ByoaRefusal`.
|
|
35
37
|
*/
|
|
36
38
|
export declare function resolveCaseSubscription(library: Library, kind: 'trigger' | 'event', sub: EventSubscription, byoa?: {
|
|
37
39
|
where: string;
|
|
@@ -16,19 +16,29 @@ function caseObjectOption(object) {
|
|
|
16
16
|
/**
|
|
17
17
|
* Resolve a Case event subscription: `library.resolveEvent` with the
|
|
18
18
|
* subscription's own `version` and `object`, then refuse a GENERIC event the
|
|
19
|
-
* author named no object for.
|
|
19
|
+
* author named no object for. Whatever `resolveEvent` throws (a connector or
|
|
20
|
+
* event the library does not carry, a version or object it does not resolve)
|
|
21
|
+
* is one condition, an event compile cannot place: EVENT_UNKNOWN, as Flow's
|
|
22
|
+
* compile says.
|
|
20
23
|
*
|
|
21
24
|
* @param library - The connector library.
|
|
22
25
|
* @param kind - `trigger` for an event trigger, `event` for a wait-for-connector task or rule.
|
|
23
26
|
* @param sub - The authored subscription.
|
|
24
27
|
* @param byoa - Where the subscription is and the bindings, to refuse an
|
|
25
|
-
* unverified BYOA connection
|
|
26
|
-
* that rule from the shared event rules).
|
|
28
|
+
* unverified BYOA connection.
|
|
27
29
|
* @returns The resolved event.
|
|
28
|
-
* @throws
|
|
30
|
+
* @throws EVENT_UNKNOWN, {@link GenericEventNoObjectError}, or a `ByoaRefusal`.
|
|
29
31
|
*/
|
|
30
32
|
export function resolveCaseSubscription(library, kind, sub, byoa) {
|
|
31
|
-
|
|
33
|
+
let rc;
|
|
34
|
+
try {
|
|
35
|
+
rc = library.resolveEvent(kind, sub.connector, sub.event, sub.version, sub.object);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (error instanceof Error && error.message.startsWith('internal SDK error:'))
|
|
39
|
+
throw error;
|
|
40
|
+
throw new CompileRefusal('EVENT_UNKNOWN', error instanceof Error ? error.message : String(error));
|
|
41
|
+
}
|
|
32
42
|
const noObject = genericEventObjectRefusal(rc, sub, caseObjectOption);
|
|
33
43
|
if (noObject)
|
|
34
44
|
throw new GenericEventNoObjectError(noObject);
|
package/dist/core/refusals.js
CHANGED
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
* stops `compile` from writing. A serializer warning (`CONNECTION_STUB`) is
|
|
7
7
|
* `lint` too: it never blocks.
|
|
8
8
|
* - `lowering`: `compile` cannot write a correct `.flow` (or caseplan, or `.bpmn`). It always
|
|
9
|
-
* stops `compile`; for a Flow, `check` reports it too, from the serializer it
|
|
10
|
-
* collect mode (`SerializeOpts.collect`), so the rule
|
|
9
|
+
* stops `compile`; for a Flow or a Case, `check` reports it too, from the serializer it
|
|
10
|
+
* runs in collect mode (`SerializeOpts.collect`, `CaseSerializeOpts.collect`), so the rule
|
|
11
|
+
* lives in one place.
|
|
11
12
|
* - `draft`: an unfinished canvas node (no connection yet, a required input
|
|
12
13
|
* left empty, a generic operation with no object). These are the codes a
|
|
13
14
|
* draft-tolerant compile collects instead of throwing (`DraftCode` in
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uipath/maestro-builder-sdk",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.69.0",
|
|
4
4
|
"description": "Build UiPath Flow, Case, and BPMN artifacts by writing TypeScript.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://docs.uipath.com/maestro",
|
|
@@ -100,5 +100,5 @@
|
|
|
100
100
|
"@types/node": "^22.7.0",
|
|
101
101
|
"esbuild": "^0.28.1"
|
|
102
102
|
},
|
|
103
|
-
"gitref": "
|
|
103
|
+
"gitref": "f1c4fc99c993d7cc8defcda32496be344f018a23"
|
|
104
104
|
}
|