@uipath/maestro-builder-sdk 6.66.7 → 6.66.9
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/bindings-v2.d.ts +15 -1
- package/dist/bindings-v2.js +29 -6
- package/dist/bpmn/check.js +16 -113
- package/dist/bpmn/serialize.d.ts +41 -0
- package/dist/bpmn/serialize.js +103 -1
- package/dist/bpmn/typed-node.d.ts +12 -0
- package/dist/bpmn/typed-node.js +24 -0
- package/dist/case/check.js +5 -40
- package/dist/case/serialize.js +6 -0
- package/dist/case/task-shape.d.ts +31 -0
- package/dist/case/task-shape.js +48 -0
- package/dist/check.js +5 -33
- package/dist/core/compile-refusal.d.ts +25 -0
- package/dist/core/compile-refusal.js +19 -0
- package/dist/core/process-resource.d.ts +9 -0
- package/dist/core/process-resource.js +4 -0
- package/dist/core/refusals.d.ts +24 -22
- package/dist/core/refusals.js +24 -22
- package/dist/nodes/agent/flow/inline-agent.js +14 -1
- package/dist/nodes/connector/flow/node-type.js +5 -3
- package/dist/nodes/process/flow/check.js +15 -80
- package/dist/nodes/process/flow/node-type.js +4 -0
- package/dist/nodes/process/flow/refusals.d.ts +8 -0
- package/dist/nodes/process/flow/refusals.js +92 -0
- package/dist/nodes/queue/flow/check.d.ts +25 -0
- package/dist/nodes/queue/flow/check.js +48 -18
- package/dist/nodes/queue/flow/node-type.js +13 -1
- package/dist/nodes/transform/flow/check.js +31 -43
- package/dist/nodes/transform/flow/node-type.js +35 -9
- package/dist/serialize.js +142 -19
- package/package.json +2 -2
|
@@ -2,7 +2,7 @@ import { AGENTIC_PROCESS_FORMS } from '../../../core/agentic-process-forms.js';
|
|
|
2
2
|
import { GUID_RE } from '../../../core/binding-messages.js';
|
|
3
3
|
import { nearestName } from '../../../core/edit-distance.js';
|
|
4
4
|
import { ownEntry } from '../../../core/own-entry.js';
|
|
5
|
-
import {
|
|
5
|
+
import { blank } from '../../../core/process-resource.js';
|
|
6
6
|
export const PROCESS_DIAGNOSTICS = {
|
|
7
7
|
AGENT_EMPTY_INPUT_FLOOR: { severity: 'warning', summary: 'an agent step that binds no inputs' },
|
|
8
8
|
AGENT_EMPTY_RETURNS: { severity: 'warning', summary: 'an agent step with returns: {}' },
|
|
@@ -90,16 +90,8 @@ export const PROCESS_DIAGNOSTICS = {
|
|
|
90
90
|
export function checkRpaWorkflow(inputs, ctx) {
|
|
91
91
|
const step = ctx.step;
|
|
92
92
|
const at = (code, message, suggestion) => ctx.report(code, message, suggestion);
|
|
93
|
-
|
|
94
|
-
if (blank(inputs?.key) && !
|
|
95
|
-
at('RPA_NO_KEY', renamedFieldMessage('RPA', step, 'process'), `key: ${JSON.stringify(inputs.process)}`);
|
|
96
|
-
}
|
|
97
|
-
else if (blank(inputs?.key)) {
|
|
98
|
-
at('RPA_NO_KEY', `RPA step "${step}" has no \`key\`. That is the published process's release key (a GUID), and `
|
|
99
|
-
+ `it becomes part of the node's type — without it there is no process to start. Find it with `
|
|
100
|
-
+ `\`uip or processes list --all-folders --process-type Process\` (the row's \`Key\`).`, `key: '486edc26-0658-4ac1-92c9-1ef953927151'`);
|
|
101
|
-
}
|
|
102
|
-
else if (!GUID_RE.test(String(inputs.key).trim())) {
|
|
93
|
+
// A blank key is the serializer's RPA_NO_KEY (refusals.ts).
|
|
94
|
+
if (!blank(inputs?.key) && !GUID_RE.test(String(inputs.key).trim())) {
|
|
103
95
|
at('RPA_KEY_NOT_KEY', `RPA step "${step}"'s \`key\` is ${JSON.stringify(inputs.key)}, which is not a release key. `
|
|
104
96
|
+ `\`key\` is the GUID the tenant gives the published process (it becomes the node type's suffix); `
|
|
105
97
|
+ `the process's NAME goes in \`name\` and its folder in \`folderPath\`.`, `key: '<guid>', name: ${JSON.stringify(inputs.key)}`);
|
|
@@ -121,12 +113,7 @@ export function checkRpaWorkflow(inputs, ctx) {
|
|
|
121
113
|
at('RPA_LOCAL_PROJECT_ID_NOT_GUID', `In-solution RPA step "${step}" has projectId ${JSON.stringify(inputs.projectId)}, which is not `
|
|
122
114
|
+ `the sibling project's GUID. Read \`resource.projectKey\` from the generated resource file.`, `projectId: '<resource.projectKey>'`);
|
|
123
115
|
}
|
|
124
|
-
|
|
125
|
-
at('RPA_LOCAL_HAS_FOLDER', `In-solution RPA step "${step}" also sets \`folderPath\` ${JSON.stringify(inputs.folderPath)}. An `
|
|
126
|
-
+ `in-solution sibling has no folder until the solution is deployed — the platform's own manifest `
|
|
127
|
-
+ `writes \`""\` — so the value would be dropped. Remove it, or drop \`location: 'in-solution'\` to `
|
|
128
|
-
+ `call the published copy.`);
|
|
129
|
-
}
|
|
116
|
+
// A folderPath beside in-solution is the serializer's RPA_LOCAL_HAS_FOLDER (refusals.ts).
|
|
130
117
|
// The two are swappable by shape, and swapping them is invisible until
|
|
131
118
|
// Orchestrator answers "Folder does not exist or the user does not have access".
|
|
132
119
|
const name = typeof inputs?.name === 'string' ? inputs.name.trim() : '';
|
|
@@ -160,16 +147,8 @@ export function checkRpaWorkflow(inputs, ctx) {
|
|
|
160
147
|
export function checkApiWorkflow(inputs, ctx) {
|
|
161
148
|
const step = ctx.step;
|
|
162
149
|
const at = (code, message, suggestion) => ctx.report(code, message, suggestion);
|
|
163
|
-
|
|
164
|
-
if (blank(inputs?.key) && !
|
|
165
|
-
at('APIWF_NO_KEY', renamedFieldMessage('API workflow', step, 'workflow'), `key: ${JSON.stringify(inputs.workflow)}`);
|
|
166
|
-
}
|
|
167
|
-
else if (blank(inputs?.key)) {
|
|
168
|
-
at('APIWF_NO_KEY', `API workflow step "${step}" has no \`key\`. That is the published workflow's key (a GUID), and `
|
|
169
|
-
+ `it becomes part of the node's type — without it there is no workflow to invoke. Find it with `
|
|
170
|
-
+ `\`uip or processes list --all-folders\` (an API workflow's ProcessKey reads "<name>.api.<name>").`, `key: 'ce857908-ee1d-4392-b552-38bcea0be29c'`);
|
|
171
|
-
}
|
|
172
|
-
else if (!GUID_RE.test(String(inputs.key).trim())) {
|
|
150
|
+
// A blank key is the serializer's APIWF_NO_KEY (refusals.ts).
|
|
151
|
+
if (!blank(inputs?.key) && !GUID_RE.test(String(inputs.key).trim())) {
|
|
173
152
|
at('APIWF_KEY_NOT_KEY', `API workflow step "${step}"'s \`key\` is ${JSON.stringify(inputs.key)}, which is not a key. `
|
|
174
153
|
+ `\`key\` is the GUID the tenant gives the published workflow (it becomes the node type's suffix); `
|
|
175
154
|
+ `the workflow's NAME goes in \`name\` and its folder in \`folderPath\`.`, `key: '<guid>', name: ${JSON.stringify(inputs.key)}`);
|
|
@@ -208,13 +187,8 @@ export function checkApiWorkflow(inputs, ctx) {
|
|
|
208
187
|
export function checkPublishedFunction(inputs, ctx) {
|
|
209
188
|
const step = ctx.step;
|
|
210
189
|
const at = (code, message, suggestion) => ctx.report(code, message, suggestion);
|
|
211
|
-
|
|
212
|
-
if (blank(inputs?.key)) {
|
|
213
|
-
at('FUNCTION_NO_KEY', `Function step "${step}" has no \`key\`. That is the published function's key (a GUID), and it becomes `
|
|
214
|
-
+ `part of the node's type — without it there is no function to invoke. Find it with `
|
|
215
|
-
+ `\`uip maestro flow registry search "uipath.core.function"\`.`, `key: '7059bdb5-fdd7-4e13-9d7b-1748aaeb129d'`);
|
|
216
|
-
}
|
|
217
|
-
else if (!GUID_RE.test(String(inputs.key).trim())) {
|
|
190
|
+
// A blank key is the serializer's FUNCTION_NO_KEY (refusals.ts).
|
|
191
|
+
if (!blank(inputs?.key) && !GUID_RE.test(String(inputs.key).trim())) {
|
|
218
192
|
at('FUNCTION_KEY_NOT_KEY', `Function step "${step}"'s \`key\` is ${JSON.stringify(inputs.key)}, which is not a key. \`key\` is the `
|
|
219
193
|
+ `GUID the tenant gives the published function (it becomes the node type's suffix); the function's NAME `
|
|
220
194
|
+ `goes in \`name\` and its folder in \`folderPath\`.`, `key: '<guid>', name: ${JSON.stringify(inputs.key)}`);
|
|
@@ -238,12 +212,7 @@ export function checkPublishedFunction(inputs, ctx) {
|
|
|
238
212
|
at('FUNCTION_LOCAL_PROJECT_ID_NOT_GUID', `In-solution function step "${step}" has projectId ${JSON.stringify(inputs.projectId)}, which is not `
|
|
239
213
|
+ `the sibling project's GUID. Read \`resource.projectKey\` from the generated resource file.`, `projectId: '<resource.projectKey>'`);
|
|
240
214
|
}
|
|
241
|
-
|
|
242
|
-
at('FUNCTION_LOCAL_HAS_FOLDER', `In-solution function step "${step}" also sets \`folderPath\` ${JSON.stringify(inputs.folderPath)}. An `
|
|
243
|
-
+ `in-solution sibling has no folder until the solution is deployed — the platform's own manifest `
|
|
244
|
-
+ `writes \`""\` — so the value would be dropped. Remove it, or drop \`location: 'in-solution'\` to `
|
|
245
|
-
+ `call the published copy.`);
|
|
246
|
-
}
|
|
215
|
+
// A folderPath beside in-solution is the serializer's FUNCTION_LOCAL_HAS_FOLDER (refusals.ts).
|
|
247
216
|
const name = typeof inputs?.name === 'string' ? inputs.name.trim() : '';
|
|
248
217
|
if (name.includes('/')) {
|
|
249
218
|
at('FUNCTION_NAME_IS_PATH', `Function step "${step}"'s \`name\` is ${JSON.stringify(inputs.name)}, which is a folder path. \`name\` is `
|
|
@@ -282,17 +251,8 @@ export function checkPublishedFunction(inputs, ctx) {
|
|
|
282
251
|
export function checkAgenticProcess(inputs, ctx) {
|
|
283
252
|
const step = ctx.step;
|
|
284
253
|
const at = (code, message, suggestion) => ctx.report(code, message, suggestion);
|
|
285
|
-
|
|
286
|
-
if (blank(inputs?.key) && !
|
|
287
|
-
at('AGENTIC_NO_KEY', renamedFieldMessage('Agentic process', step, 'process'), `key: ${JSON.stringify(inputs.process)}`);
|
|
288
|
-
}
|
|
289
|
-
else if (blank(inputs?.key)) {
|
|
290
|
-
at('AGENTIC_NO_KEY', `Agentic process step "${step}" has no \`key\`. That is the published process's key (a GUID), `
|
|
291
|
-
+ `and it becomes part of the node's type — without it there is no process to start. Find it with `
|
|
292
|
-
+ `\`uip or processes list --all-folders --limit 100\` (an agentic process's ProcessKey reads `
|
|
293
|
-
+ `"<name>.agentic.<name>"; the listing paginates at 50, so pass a limit or scope it to a folder).`, `key: '4fc450ab-89be-4462-8fc8-21ac4c1d6fb9'`);
|
|
294
|
-
}
|
|
295
|
-
else if (!GUID_RE.test(String(inputs.key).trim())) {
|
|
254
|
+
// A blank key is the serializer's AGENTIC_NO_KEY (refusals.ts).
|
|
255
|
+
if (!blank(inputs?.key) && !GUID_RE.test(String(inputs.key).trim())) {
|
|
296
256
|
at('AGENTIC_KEY_NOT_KEY', `Agentic process step "${step}"'s \`key\` is ${JSON.stringify(inputs.key)}, which is not a key. `
|
|
297
257
|
+ `\`key\` is the GUID the tenant gives the published process (it becomes the node type's suffix); `
|
|
298
258
|
+ `the process's NAME goes in \`name\` and its folder in \`folderPath\`.`, `key: '<guid>', name: ${JSON.stringify(inputs.key)}`);
|
|
@@ -337,29 +297,14 @@ export function checkAgenticProcess(inputs, ctx) {
|
|
|
337
297
|
at('AGENTIC_LOCAL_PROJECT_ID_NOT_GUID', `In-solution agentic process step "${step}" has projectId ${JSON.stringify(inputs.projectId)}, which is not `
|
|
338
298
|
+ `the sibling project's GUID. Read \`resource.projectKey\` from the generated resource file.`, `projectId: '<resource.projectKey>'`);
|
|
339
299
|
}
|
|
340
|
-
|
|
341
|
-
at('AGENTIC_LOCAL_HAS_FOLDER', `In-solution agentic process step "${step}" also sets \`folderPath\` ${JSON.stringify(inputs.folderPath)}. An `
|
|
342
|
-
+ `in-solution sibling has no folder until the solution is deployed — the platform's own manifest `
|
|
343
|
-
+ `writes \`""\` — so the value would be dropped. Remove it, or drop \`location: 'in-solution'\` to `
|
|
344
|
-
+ `call the published copy.`);
|
|
345
|
-
}
|
|
300
|
+
// A folderPath beside in-solution is the serializer's AGENTIC_LOCAL_HAS_FOLDER (refusals.ts).
|
|
346
301
|
const name = typeof inputs?.name === 'string' ? inputs.name.trim() : '';
|
|
347
302
|
if (name.includes('/')) {
|
|
348
303
|
at('AGENTIC_NAME_IS_PATH', `Agentic process step "${step}"'s \`name\` is ${JSON.stringify(inputs.name)}, which is a folder path. `
|
|
349
304
|
+ `\`name\` is the process's own name in Orchestrator; the path it lives in goes in \`folderPath\`. `
|
|
350
305
|
+ `Together they form the resourceKey "<folderPath>.<name>", so swapping them points the job at nothing.`);
|
|
351
306
|
}
|
|
352
|
-
//
|
|
353
|
-
// node is emitted — so a process argument by this name is dropped on the way to
|
|
354
|
-
// the tenant and nothing local notices.
|
|
355
|
-
if (inputs?.inputs && Object.prototype.hasOwnProperty.call(inputs.inputs, 'fireAndForget')) {
|
|
356
|
-
at('AGENTIC_RESERVED_INPUT', `Agentic process step "${step}" passes an input called \`fireAndForget\`. That name is RESERVED: it is `
|
|
357
|
-
+ `the platform's design-time switch for starting the process without waiting, and its serializer `
|
|
358
|
-
+ `removes the key from the node's inputs before emitting — so a process argument of that name never `
|
|
359
|
-
+ `arrives. Locally it would appear to work, because this engine passes the whole envelope through. `
|
|
360
|
-
+ `Rename the argument, and note that this surface cannot author fire-and-forget at all (the engine `
|
|
361
|
-
+ `always waits for the job).`);
|
|
362
|
-
}
|
|
307
|
+
// An argument called `fireAndForget` is the serializer's AGENTIC_RESERVED_INPUT (refusals.ts).
|
|
363
308
|
// An empty `returns: {}` is a statement that the process returns nothing, which is
|
|
364
309
|
// legal — but it is almost always a half-finished edit, and the difference between
|
|
365
310
|
// it and a missing `returns` is invisible in the emitted file.
|
|
@@ -500,19 +445,9 @@ export function checkAgenticProcessRead({ node: name, field, rest, reader: step
|
|
|
500
445
|
export function checkAgent(inputs, ctx) {
|
|
501
446
|
const step = ctx.step;
|
|
502
447
|
const at = (code, message, suggestion) => ctx.report(code, message, suggestion);
|
|
503
|
-
const blank = (v) => v === undefined || v === null || (typeof v === 'string' && v.trim() === '');
|
|
504
448
|
const inSolution = inputs?.location === 'in-solution';
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
}
|
|
508
|
-
else if (blank(inputs?.key)) {
|
|
509
|
-
at('AGENT_NO_KEY', `Agent step "${step}" has no \`key\`. It is a GUID and becomes part of the node's `
|
|
510
|
-
+ `type — without it there is no agent to start. For a published agent, find the Orchestrator `
|
|
511
|
-
+ `release with \`uip or processes list --all-folders --limit 200\` (take the row's \`Key\`) or `
|
|
512
|
-
+ `\`uip maestro flow registry search uipath.core.agent\`. For an in-solution agent, read `
|
|
513
|
-
+ `\`resource.key\` from \`resources/solution_folder/process/agent/<project>.json\`.`, `key: '6a47dc5b-5fd4-461f-b97d-6c1817abcaaa'`);
|
|
514
|
-
}
|
|
515
|
-
else if (!GUID_RE.test(String(inputs.key).trim())) {
|
|
449
|
+
// A blank key is the serializer's AGENT_NO_KEY (refusals.ts).
|
|
450
|
+
if (!blank(inputs?.key) && !GUID_RE.test(String(inputs.key).trim())) {
|
|
516
451
|
at('AGENT_KEY_NOT_GUID', `Agent step "${step}"'s \`key\` is ${JSON.stringify(inputs.key)}, which is not a key. \`key\` is `
|
|
517
452
|
+ `the GUID the tenant gives the published agent (it becomes the node type's suffix); the agent's NAME `
|
|
518
453
|
+ `goes in \`name\` and its folder in \`folderPath\`. Passing a ProcessKey like `
|
|
@@ -17,6 +17,7 @@ import { ownEntry } from '../../../core/own-entry.js';
|
|
|
17
17
|
import { resourceNodeSortOrder } from '../../../core/process-resource-sort-order.js';
|
|
18
18
|
import { processResourceBindings, processResourceLocation } from '../../../core/process-resource.js';
|
|
19
19
|
import { checkAgent, checkAgentRead, checkAgenticProcess, checkAgenticProcessRead, checkApiWorkflow, checkApiWorkflowRead, checkPublishedFunction, checkPublishedFunctionRead, checkRpaWorkflow, rpaWorkflowRead, } from './check.js';
|
|
20
|
+
import { refuseLossyProcess } from './refusals.js';
|
|
20
21
|
/** The RPA-workflow node type family. The process's release key is the suffix. */
|
|
21
22
|
const RPA_WORKFLOW_PREFIX = 'uipath.core.rpa-workflow.';
|
|
22
23
|
/**
|
|
@@ -635,6 +636,9 @@ export function processDefinition(spec) {
|
|
|
635
636
|
* are identical, and the DATA paths that differ are all downstream in the engine.
|
|
636
637
|
*/
|
|
637
638
|
function serializeProcess(spec, ctx) {
|
|
639
|
+
// A missing key, a folder beside in-solution, a reserved `fireAndForget`
|
|
640
|
+
// argument: each would be written wrong, so it is refused here (#1518).
|
|
641
|
+
refuseLossyProcess(spec.kind, spec.inputs, ctx.step);
|
|
638
642
|
const s = spec.inputs;
|
|
639
643
|
const fireAndForget = spec.kind === 'agenticProcess' && s.completion === 'fire-and-forget';
|
|
640
644
|
const def = processDefinition(spec);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type ProcessKind } from '../../../core/process-resource.js';
|
|
2
|
+
/**
|
|
3
|
+
* Throw the first condition of `inputs` the serializer would write wrong, as a
|
|
4
|
+
* `CompileRefusal` on the step. For instance `rpaWorkflow({ name, folderPath })`
|
|
5
|
+
* with no `key` throws `RPA_NO_KEY` instead of writing node type
|
|
6
|
+
* `uipath.core.rpa-workflow.undefined`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function refuseLossyProcess(kind: ProcessKind, inputs: unknown, step: string): void;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the process family's serializer refuses, because it cannot write the step
|
|
3
|
+
* the author wrote (#1518, docs/CHECK_COMPILE_CONTRACT_DESIGN.md §3.1). Since
|
|
4
|
+
* #1497 `compile` does not run `check`, so a rule whose condition the emitter
|
|
5
|
+
* silently loses has to stop the emitter itself. `check` still reports each one:
|
|
6
|
+
* it runs the serializer in collect mode, which places the refusal on the step.
|
|
7
|
+
*
|
|
8
|
+
* - `<PREFIX>_NO_KEY`: the node type is `<prefix><key>`, so an omitted key
|
|
9
|
+
* wrote `uipath.core.rpa-workflow.undefined` and a blank one the bare prefix.
|
|
10
|
+
* - `<PREFIX>_LOCAL_HAS_FOLDER`: an in-solution resource is written with
|
|
11
|
+
* folder `""` (`processResourceLocation`), so the author's folderPath is dropped.
|
|
12
|
+
* - `AGENTIC_RESERVED_INPUT`: `fireAndForget` is the design-time switch; the
|
|
13
|
+
* definition retypes it boolean and decompile deletes it as an argument.
|
|
14
|
+
*/
|
|
15
|
+
import { CompileRefusal } from '../../../core/compile-refusal.js';
|
|
16
|
+
import { ownEntry } from '../../../core/own-entry.js';
|
|
17
|
+
import { blank, renamedFieldMessage } from '../../../core/process-resource.js';
|
|
18
|
+
/** Per kind: the codes, the label messages use, and the retired name of `key` (if any). */
|
|
19
|
+
const KINDS = {
|
|
20
|
+
rpaWorkflow: {
|
|
21
|
+
noKeyCode: 'RPA_NO_KEY', localCode: 'RPA_LOCAL_HAS_FOLDER', label: 'RPA', oldField: 'process', localLabel: 'RPA step',
|
|
22
|
+
noKey: (step) => `RPA step "${step}" has no \`key\`. That is the published process's release key (a GUID), and `
|
|
23
|
+
+ `it becomes part of the node's type — without it there is no process to start. Find it with `
|
|
24
|
+
+ `\`uip or processes list --all-folders --process-type Process\` (the row's \`Key\`).`,
|
|
25
|
+
noKeySuggestion: `key: '486edc26-0658-4ac1-92c9-1ef953927151'`,
|
|
26
|
+
},
|
|
27
|
+
apiWorkflow: {
|
|
28
|
+
noKeyCode: 'APIWF_NO_KEY', label: 'API workflow', oldField: 'workflow',
|
|
29
|
+
noKey: (step) => `API workflow step "${step}" has no \`key\`. That is the published workflow's key (a GUID), and `
|
|
30
|
+
+ `it becomes part of the node's type — without it there is no workflow to invoke. Find it with `
|
|
31
|
+
+ `\`uip or processes list --all-folders\` (an API workflow's ProcessKey reads "<name>.api.<name>").`,
|
|
32
|
+
noKeySuggestion: `key: 'ce857908-ee1d-4392-b552-38bcea0be29c'`,
|
|
33
|
+
},
|
|
34
|
+
publishedFunction: {
|
|
35
|
+
noKeyCode: 'FUNCTION_NO_KEY', localCode: 'FUNCTION_LOCAL_HAS_FOLDER', label: 'Function', localLabel: 'function step',
|
|
36
|
+
noKey: (step) => `Function step "${step}" has no \`key\`. That is the published function's key (a GUID), and it becomes `
|
|
37
|
+
+ `part of the node's type — without it there is no function to invoke. Find it with `
|
|
38
|
+
+ `\`uip maestro flow registry search "uipath.core.function"\`.`,
|
|
39
|
+
noKeySuggestion: `key: '7059bdb5-fdd7-4e13-9d7b-1748aaeb129d'`,
|
|
40
|
+
},
|
|
41
|
+
agenticProcess: {
|
|
42
|
+
noKeyCode: 'AGENTIC_NO_KEY', localCode: 'AGENTIC_LOCAL_HAS_FOLDER', label: 'Agentic process', oldField: 'process', localLabel: 'agentic process step',
|
|
43
|
+
noKey: (step) => `Agentic process step "${step}" has no \`key\`. That is the published process's key (a GUID), `
|
|
44
|
+
+ `and it becomes part of the node's type — without it there is no process to start. Find it with `
|
|
45
|
+
+ `\`uip or processes list --all-folders --limit 100\` (an agentic process's ProcessKey reads `
|
|
46
|
+
+ `"<name>.agentic.<name>"; the listing paginates at 50, so pass a limit or scope it to a folder).`,
|
|
47
|
+
noKeySuggestion: `key: '4fc450ab-89be-4462-8fc8-21ac4c1d6fb9'`,
|
|
48
|
+
},
|
|
49
|
+
agent: {
|
|
50
|
+
noKeyCode: 'AGENT_NO_KEY', label: 'Agent', oldField: 'agent',
|
|
51
|
+
noKey: (step) => `Agent step "${step}" has no \`key\`. It is a GUID and becomes part of the node's `
|
|
52
|
+
+ `type — without it there is no agent to start. For a published agent, find the Orchestrator `
|
|
53
|
+
+ `release with \`uip or processes list --all-folders --limit 200\` (take the row's \`Key\`) or `
|
|
54
|
+
+ `\`uip maestro flow registry search uipath.core.agent\`. For an in-solution agent, read `
|
|
55
|
+
+ `\`resource.key\` from \`resources/solution_folder/process/agent/<project>.json\`.`,
|
|
56
|
+
noKeySuggestion: `key: '6a47dc5b-5fd4-461f-b97d-6c1817abcaaa'`,
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Throw the first condition of `inputs` the serializer would write wrong, as a
|
|
61
|
+
* `CompileRefusal` on the step. For instance `rpaWorkflow({ name, folderPath })`
|
|
62
|
+
* with no `key` throws `RPA_NO_KEY` instead of writing node type
|
|
63
|
+
* `uipath.core.rpa-workflow.undefined`.
|
|
64
|
+
*/
|
|
65
|
+
export function refuseLossyProcess(kind, inputs, step) {
|
|
66
|
+
const k = ownEntry(KINDS, kind);
|
|
67
|
+
const s = (inputs ?? {});
|
|
68
|
+
// Every blank form is refused: omitted writes `<prefix>undefined`, and `''`
|
|
69
|
+
// or whitespace writes the bare prefix (or one ending in spaces).
|
|
70
|
+
if (blank(s.key)) {
|
|
71
|
+
const old = k.oldField !== undefined ? s[k.oldField] : undefined;
|
|
72
|
+
if (k.oldField !== undefined && !blank(old)) {
|
|
73
|
+
throw new CompileRefusal(k.noKeyCode, renamedFieldMessage(k.label, step, k.oldField), `key: ${JSON.stringify(old)}`);
|
|
74
|
+
}
|
|
75
|
+
throw new CompileRefusal(k.noKeyCode, k.noKey(step), k.noKeySuggestion);
|
|
76
|
+
}
|
|
77
|
+
if (k.localCode !== undefined && k.localLabel !== undefined && s.location === 'in-solution' && !blank(s.folderPath)) {
|
|
78
|
+
throw new CompileRefusal(k.localCode, `In-solution ${k.localLabel} "${step}" also sets \`folderPath\` ${JSON.stringify(s.folderPath)}. An `
|
|
79
|
+
+ `in-solution sibling has no folder until the solution is deployed — the platform's own manifest `
|
|
80
|
+
+ `writes \`""\` — so the value would be dropped. Remove it, or drop \`location: 'in-solution'\` to `
|
|
81
|
+
+ `call the published copy.`);
|
|
82
|
+
}
|
|
83
|
+
const args = s.inputs;
|
|
84
|
+
if (kind === 'agenticProcess' && args !== null && typeof args === 'object'
|
|
85
|
+
&& Object.prototype.hasOwnProperty.call(args, 'fireAndForget')) {
|
|
86
|
+
throw new CompileRefusal('AGENTIC_RESERVED_INPUT', `Agentic process step "${step}" passes an input called \`fireAndForget\`. That name is RESERVED: it is `
|
|
87
|
+
+ `the platform's design-time switch for starting the process without waiting, and its serializer `
|
|
88
|
+
+ `removes the key from the node's inputs before emitting — so a process argument of that name never `
|
|
89
|
+
+ `arrives. Locally it would appear to work, because this engine passes the whole envelope through. `
|
|
90
|
+
+ `Rename the argument, or set \`completion: 'fire-and-forget'\` to start the process without waiting.`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -20,6 +20,31 @@ export declare function itemDataIsString(def: unknown): boolean;
|
|
|
20
20
|
* serializer's refusal, so the two say the same thing.
|
|
21
21
|
*/
|
|
22
22
|
export declare function queueItemExprMessage(step: string, typeName: string, version: string, field: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* Whether `item` is something other than an object of fields: an array, a
|
|
25
|
+
* primitive, or a whole-item expression (an `Expr` is an object, but its own
|
|
26
|
+
* fields are not the item's). An omitted or null item is the empty item.
|
|
27
|
+
*/
|
|
28
|
+
export declare function itemIsNotObject(item: unknown): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Why `serialize` refuses an `item` that is not an object (QUEUE_ITEM_NOT_OBJECT).
|
|
31
|
+
* Writing it anyway would rewrite it through `Object.entries`: `['INV-1']` would
|
|
32
|
+
* become `{ "0": "INV-1" }`, a string one field per character, a number `{}`,
|
|
33
|
+
* and a whole-item expression its own `js`/`literal` fields.
|
|
34
|
+
*/
|
|
35
|
+
export declare function queueItemNotObjectMessage(step: string, item: unknown): {
|
|
36
|
+
message: string;
|
|
37
|
+
suggestion: string;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Why `serialize` refuses `returns` on a fire-and-forget enqueue
|
|
41
|
+
* (QUEUE_RETURNS_WITHOUT_WAIT). That node publishes the item's own record, so
|
|
42
|
+
* the `.flow` has no place for `returns` and writing it would drop them.
|
|
43
|
+
*/
|
|
44
|
+
export declare function queueReturnsWithoutWaitMessage(step: string): {
|
|
45
|
+
message: string;
|
|
46
|
+
suggestion: string;
|
|
47
|
+
};
|
|
23
48
|
/**
|
|
24
49
|
* Check a `queueItem(...)` call.
|
|
25
50
|
*
|
|
@@ -14,7 +14,7 @@ export const QUEUE_DIAGNOSTICS = {
|
|
|
14
14
|
QUEUE_ITEM_EXPR_UNSUPPORTED: { severity: 'error', summary: 'an expression in an item stored as one JSON string' },
|
|
15
15
|
QUEUE_BAD_PRIORITY: { severity: 'error', summary: 'a priority the node type does not offer' },
|
|
16
16
|
QUEUE_BAD_DATE: { severity: 'error', summary: 'a date Orchestrator cannot read' },
|
|
17
|
-
QUEUE_RETURNS_WITHOUT_WAIT: { severity: '
|
|
17
|
+
QUEUE_RETURNS_WITHOUT_WAIT: { severity: 'error', summary: 'returns without wait: true' },
|
|
18
18
|
QUEUE_READ_NOT_OUTPUT: { severity: 'error', summary: 'a read outside the step\'s output' },
|
|
19
19
|
QUEUE_READ_WITHOUT_RETURNS: { severity: 'error', summary: 'a field read off a wait result with no returns' },
|
|
20
20
|
QUEUE_READ_UNKNOWN_FIELD: { severity: 'error', summary: 'a read of a field the step does not publish' },
|
|
@@ -40,6 +40,50 @@ export function queueItemExprMessage(step, typeName, version, field) {
|
|
|
40
40
|
+ `cannot be an expression there. Omit the { version } to emit the SDK's default version, which `
|
|
41
41
|
+ `stores the item as an object of values and expressions.`;
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Whether `item` is something other than an object of fields: an array, a
|
|
45
|
+
* primitive, or a whole-item expression (an `Expr` is an object, but its own
|
|
46
|
+
* fields are not the item's). An omitted or null item is the empty item.
|
|
47
|
+
*/
|
|
48
|
+
export function itemIsNotObject(item) {
|
|
49
|
+
if (item === undefined || item === null)
|
|
50
|
+
return false;
|
|
51
|
+
return item instanceof Expr || typeof item !== 'object' || Array.isArray(item);
|
|
52
|
+
}
|
|
53
|
+
function itemKind(item) {
|
|
54
|
+
if (item instanceof Expr)
|
|
55
|
+
return 'an expression';
|
|
56
|
+
return Array.isArray(item) ? 'an array' : typeof item;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Why `serialize` refuses an `item` that is not an object (QUEUE_ITEM_NOT_OBJECT).
|
|
60
|
+
* Writing it anyway would rewrite it through `Object.entries`: `['INV-1']` would
|
|
61
|
+
* become `{ "0": "INV-1" }`, a string one field per character, a number `{}`,
|
|
62
|
+
* and a whole-item expression its own `js`/`literal` fields.
|
|
63
|
+
*/
|
|
64
|
+
export function queueItemNotObjectMessage(step, item) {
|
|
65
|
+
return {
|
|
66
|
+
message: `Queue step "${step}"'s \`item\` is ${itemKind(item)}. It `
|
|
67
|
+
+ `must be an object of field names to primitive values — that object becomes the queue `
|
|
68
|
+
+ `item's \`SpecificContent\`.`,
|
|
69
|
+
suggestion: `item: { InvoiceId: 'INV-1', Amount: 120 }`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Why `serialize` refuses `returns` on a fire-and-forget enqueue
|
|
74
|
+
* (QUEUE_RETURNS_WITHOUT_WAIT). That node publishes the item's own record, so
|
|
75
|
+
* the `.flow` has no place for `returns` and writing it would drop them.
|
|
76
|
+
*/
|
|
77
|
+
export function queueReturnsWithoutWaitMessage(step) {
|
|
78
|
+
return {
|
|
79
|
+
message: `Queue step "${step}" declares \`returns\` but does not \`wait\`, so nothing produces those `
|
|
80
|
+
+ `fields and the \`.flow\` would drop them: a fire-and-forget enqueue publishes the queue ITEM's own `
|
|
81
|
+
+ `Orchestrator record (\`Key\`, \`Status\`, \`Reference\`, \`SpecificContent\`, …), whose fields are `
|
|
82
|
+
+ `already declared for you. \`returns\` describes what a CONSUMER wrote into a processed item's `
|
|
83
|
+
+ `result, which needs \`wait: true\`.`,
|
|
84
|
+
suggestion: `wait: true`,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
43
87
|
/**
|
|
44
88
|
* Check a `queueItem(...)` call.
|
|
45
89
|
*
|
|
@@ -102,12 +146,8 @@ export function checkQueueItem(inputs, ctx) {
|
|
|
102
146
|
+ `— a bare item with a reference is a real pattern — but if the work needs a payload, this `
|
|
103
147
|
+ `is where it goes.`, `item: { InvoiceId: input('invoiceRef') }`);
|
|
104
148
|
}
|
|
105
|
-
else if (
|
|
106
|
-
|
|
107
|
-
+ `must be an object of field names to primitive values — that object becomes the queue `
|
|
108
|
-
+ `item's \`SpecificContent\`.`, `item: { InvoiceId: 'INV-1', Amount: 120 }`);
|
|
109
|
-
}
|
|
110
|
-
else {
|
|
149
|
+
else if (!itemIsNotObject(item)) {
|
|
150
|
+
// (A non-object item is the serializer's QUEUE_ITEM_NOT_OBJECT.)
|
|
111
151
|
const itemIsString = itemDataIsString(ctx.definition());
|
|
112
152
|
for (const [k, v] of Object.entries(item)) {
|
|
113
153
|
// An expression where the pinned version stores one JSON string is the
|
|
@@ -140,17 +180,7 @@ export function checkQueueItem(inputs, ctx) {
|
|
|
140
180
|
+ `Orchestrator can read. It takes ISO-8601 — a full timestamp with a zone.`, `${field}: '2026-08-05T17:00:00Z'`);
|
|
141
181
|
}
|
|
142
182
|
}
|
|
143
|
-
// `returns`
|
|
144
|
-
// waited for one. On a fire-and-forget enqueue it is a misunderstanding worth
|
|
145
|
-
// naming: the output is the queue item's own record, and the serializer already
|
|
146
|
-
// declares its fields.
|
|
147
|
-
if (!inputs?.wait && inputs?.returns && Object.keys(inputs.returns).length) {
|
|
148
|
-
warn('QUEUE_RETURNS_WITHOUT_WAIT', `Queue step "${step}" declares \`returns\` but does not \`wait\`, so nothing produces those `
|
|
149
|
-
+ `fields: a fire-and-forget enqueue publishes the queue ITEM's own Orchestrator record `
|
|
150
|
-
+ `(\`Key\`, \`Status\`, \`Reference\`, \`SpecificContent\`, …), whose fields are already `
|
|
151
|
-
+ `declared for you. \`returns\` describes what a CONSUMER wrote into a processed item's `
|
|
152
|
-
+ `result, which needs \`wait: true\`.`, `wait: true`);
|
|
153
|
-
}
|
|
183
|
+
// `returns` without `wait` is the serializer's QUEUE_RETURNS_WITHOUT_WAIT.
|
|
154
184
|
}
|
|
155
185
|
/** The three priorities `core.action.queue.create[-and-wait]`'s form offers. */
|
|
156
186
|
const QUEUE_PRIORITIES = ['Low', 'Normal', 'High'];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Expr } from '../../../core/expr.js';
|
|
2
2
|
import { jsonClone } from '../../../core/json-clone.js';
|
|
3
3
|
import { isRecord } from '../../../core/node-envelope.js';
|
|
4
|
-
import { checkQueueItem, itemDataIsString, queueItemExprMessage } from './check.js';
|
|
4
|
+
import { checkQueueItem, itemDataIsString, itemIsNotObject, queueItemExprMessage, queueItemNotObjectMessage, queueReturnsWithoutWaitMessage, } from './check.js';
|
|
5
5
|
import { QUEUE_ITEM_RECORD_SCHEMA } from './record.js';
|
|
6
6
|
import { CompileRefusal } from '../../../core/compile-refusal.js';
|
|
7
7
|
const CREATE = 'core.action.queue.create';
|
|
@@ -247,6 +247,18 @@ function queueAt(typeName, wait) {
|
|
|
247
247
|
kind: 'queueItem', typeName, version: '1.2', factory: 'queueItem', definition: 'per-tenant', errorPort: true,
|
|
248
248
|
rawNode: { factory: wait ? 'queueItem({ wait: true })' : 'queueItem()' },
|
|
249
249
|
serialize: ({ inputs: s }, ctx) => {
|
|
250
|
+
// Both would be written lossily, so they are refused here, on the step:
|
|
251
|
+
// a non-object item (a whole-item Expr included) would be rewritten
|
|
252
|
+
// through `Object.entries`, and a
|
|
253
|
+
// fire-and-forget node carries the item record schema, not `returns`.
|
|
254
|
+
if (itemIsNotObject(s.item)) {
|
|
255
|
+
const { message, suggestion } = queueItemNotObjectMessage(ctx.step, s.item);
|
|
256
|
+
throw new CompileRefusal('QUEUE_ITEM_NOT_OBJECT', message, suggestion);
|
|
257
|
+
}
|
|
258
|
+
if (!s.wait && s.returns && Object.keys(s.returns).length) {
|
|
259
|
+
const { message, suggestion } = queueReturnsWithoutWaitMessage(ctx.step);
|
|
260
|
+
throw new CompileRefusal('QUEUE_RETURNS_WITHOUT_WAIT', message, suggestion);
|
|
261
|
+
}
|
|
250
262
|
// The bindings first: two steps on the same queue share one pair, and the
|
|
251
263
|
// node's `model.context` has to name the ids that actually exist.
|
|
252
264
|
const { ids, entries } = queueItemBindings(s, ctx.bindings);
|
|
@@ -23,7 +23,7 @@ export const TRANSFORM_DIAGNOSTICS = {
|
|
|
23
23
|
TRANSFORM_NO_COLLECTION: { severity: 'error', summary: 'no collection' },
|
|
24
24
|
TRANSFORM_NO_GROUP_FIELD: { severity: 'error', summary: 'a groupBy with no field' },
|
|
25
25
|
TRANSFORM_NO_OPERATIONS: { severity: 'error', summary: 'no operations' },
|
|
26
|
-
TRANSFORM_UNKNOWN_OP: { severity: 'error', summary: 'an operation type
|
|
26
|
+
TRANSFORM_UNKNOWN_OP: { severity: 'error', summary: 'an operation type compile cannot write' },
|
|
27
27
|
TRANSFORM_VARIANT_ONE_OP: { severity: 'error', summary: 'a variant with more or fewer than one operation' },
|
|
28
28
|
TRANSFORM_VARIANT_OP_MISMATCH: { severity: 'error', summary: 'a variant whose operation does not match it' },
|
|
29
29
|
TRANSFORM_VARIANT_UNKNOWN: { severity: 'error', summary: 'a variant no runtime resolves' },
|
|
@@ -32,44 +32,35 @@ export function checkTransform(inputs, ctx) {
|
|
|
32
32
|
const step = ctx.step;
|
|
33
33
|
const at = (code, message, suggestion) => ctx.report(code, message, suggestion || undefined);
|
|
34
34
|
// ── collection must be a bare variable path ──
|
|
35
|
+
// A missing, raw-value or literal collection has no path form at all: the
|
|
36
|
+
// serializer refuses it (TRANSFORM_NO_COLLECTION, TRANSFORM_COLLECTION_NOT_REF,
|
|
37
|
+
// TRANSFORM_COLLECTION_LITERAL in node-type.ts `collectionPath`).
|
|
35
38
|
const collection = inputs?.collection;
|
|
36
|
-
if (collection
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
// A collection that is not an array is PASSED THROUGH untouched — no error,
|
|
62
|
-
// no operations applied (faithful to v1). The result is a transform that
|
|
63
|
-
// looks like it ran and changed nothing.
|
|
64
|
-
// Only a DECLARED scalar can be judged here: a step output's type is not
|
|
65
|
-
// known offline, so this stays quiet about those rather than guessing.
|
|
66
|
-
const declared = ctx.declaredRef(collection.js);
|
|
67
|
-
const refName = declared?.name;
|
|
68
|
-
const declaredType = declared?.type;
|
|
69
|
-
if (declaredType && ['string', 'number', 'boolean'].includes(declaredType)) {
|
|
70
|
-
at('TRANSFORM_COLLECTION_SCALAR', `Transform "${step}"'s collection is "${refName}", declared as ${declaredType}. A transform runs its `
|
|
71
|
-
+ `operations over an ARRAY; anything else is passed through untouched — no error, nothing applied — so `
|
|
72
|
-
+ `the step would look like it ran and change nothing.`, `Declare it as types.array, or point the collection at the array field: collection: out('<step>', '<field>')`);
|
|
39
|
+
if (collection instanceof Expr && !collection.literal) {
|
|
40
|
+
if (!/^\$vars\.[A-Za-z0-9_]+(\.[A-Za-z0-9_]+)*$/.test(collection.js.trim())) {
|
|
41
|
+
// A computed expression (js`…`, tmpl`…`, `.concat(…)`) is the trap that
|
|
42
|
+
// survives every local rung: a local run resolves it to a value, so
|
|
43
|
+
// it runs correctly there and resolves to NOTHING once deployed, where the
|
|
44
|
+
// string is used as a lookup path.
|
|
45
|
+
at('TRANSFORM_COLLECTION_NOT_PATH', `Transform "${step}"'s collection must be a plain reference, but it is the expression `
|
|
46
|
+
+ `"${collection.js.trim().slice(0, 60)}". The deployed runtime uses that string as a lookup PATH, so a `
|
|
47
|
+
+ `computed expression resolves to nothing there even though it runs locally. Compute it in a script step `
|
|
48
|
+
+ `first, then point the transform at that step's output.`, `collection: out('<scriptStep>')`);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
// A collection that is not an array is PASSED THROUGH untouched — no error,
|
|
52
|
+
// no operations applied (faithful to v1). The result is a transform that
|
|
53
|
+
// looks like it ran and changed nothing.
|
|
54
|
+
// Only a DECLARED scalar can be judged here: a step output's type is not
|
|
55
|
+
// known offline, so this stays quiet about those rather than guessing.
|
|
56
|
+
const declared = ctx.declaredRef(collection.js);
|
|
57
|
+
const refName = declared?.name;
|
|
58
|
+
const declaredType = declared?.type;
|
|
59
|
+
if (declaredType && ['string', 'number', 'boolean'].includes(declaredType)) {
|
|
60
|
+
at('TRANSFORM_COLLECTION_SCALAR', `Transform "${step}"'s collection is "${refName}", declared as ${declaredType}. A transform runs its `
|
|
61
|
+
+ `operations over an ARRAY; anything else is passed through untouched — no error, nothing applied — so `
|
|
62
|
+
+ `the step would look like it ran and change nothing.`, `Declare it as types.array, or point the collection at the array field: collection: out('<step>', '<field>')`);
|
|
63
|
+
}
|
|
73
64
|
}
|
|
74
65
|
}
|
|
75
66
|
// ── the operation chain ──
|
|
@@ -82,9 +73,6 @@ export function checkTransform(inputs, ctx) {
|
|
|
82
73
|
checkVariant(step, inputs, ops, at);
|
|
83
74
|
ops.forEach((op, i) => {
|
|
84
75
|
const where = `operations[${i}]`;
|
|
85
|
-
// Read the discriminant before the narrowing below exhausts the union — at
|
|
86
|
-
// run time it can be anything (an author typo survives type erasure).
|
|
87
|
-
const kind = op?.type;
|
|
88
76
|
if (op?.type === 'filter') {
|
|
89
77
|
if (!Array.isArray(op.filters) || op.filters.length === 0) {
|
|
90
78
|
at('TRANSFORM_EMPTY_FILTER', `Transform "${step}" ${where} is a filter with no rules — the runtime passes every item through it.`);
|
|
@@ -145,8 +133,8 @@ export function checkTransform(inputs, ctx) {
|
|
|
145
133
|
}
|
|
146
134
|
return;
|
|
147
135
|
}
|
|
148
|
-
|
|
149
|
-
|
|
136
|
+
// Any other type is the serializer's TRANSFORM_UNKNOWN_OP (node-type.ts
|
|
137
|
+
// `transformInputs`): it would otherwise be written as a groupBy.
|
|
150
138
|
});
|
|
151
139
|
}
|
|
152
140
|
/**
|
|
@@ -59,6 +59,12 @@ function transformInputs(inputs, ctx) {
|
|
|
59
59
|
},
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
|
+
// Anything that is not a groupBy would be written as one below, which the
|
|
63
|
+
// runtime would then run as a real grouping (#1518). Refused instead.
|
|
64
|
+
if (op?.type !== 'groupBy') {
|
|
65
|
+
throw new CompileRefusal('TRANSFORM_UNKNOWN_OP', `Transform "${ctx.step}" operations[${i}] has type ${JSON.stringify(op?.type)}; `
|
|
66
|
+
+ `compile writes only filter, map or groupBy. Use one of them.`);
|
|
67
|
+
}
|
|
62
68
|
return {
|
|
63
69
|
id,
|
|
64
70
|
type: 'groupBy',
|
|
@@ -74,20 +80,40 @@ function transformInputs(inputs, ctx) {
|
|
|
74
80
|
},
|
|
75
81
|
};
|
|
76
82
|
});
|
|
77
|
-
return { collection: ctx.renameRefs(collectionPath(inputs.collection)), operations };
|
|
83
|
+
return { collection: ctx.renameRefs(collectionPath(inputs.collection, ctx.step)), operations };
|
|
78
84
|
}
|
|
79
85
|
/**
|
|
80
86
|
* The `collection` string. A bare reference (`v('x')` / `out('s','p')`) is
|
|
81
87
|
* already the path the runtime wants, so it is emitted verbatim. Anything else
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
88
|
+
* has no faithful path form, so it is refused here rather than written (#1518):
|
|
89
|
+
* a missing one would be the string "undefined", a raw array "[object Object]",
|
|
90
|
+
* a raw string `items` reads back as the expression js`items`, and `lit([1,2])`
|
|
91
|
+
* would be "[1,2]", the same text as js`[1,2]`. `compile` does not run `check`
|
|
92
|
+
* (#1497), so this is the only place that stops them. A computed expression
|
|
93
|
+
* (TRANSFORM_COLLECTION_NOT_PATH) is written as its own text, so it stays
|
|
94
|
+
* `check`'s.
|
|
86
95
|
*/
|
|
87
|
-
function collectionPath(collection) {
|
|
88
|
-
if (collection
|
|
89
|
-
|
|
90
|
-
|
|
96
|
+
function collectionPath(collection, step) {
|
|
97
|
+
if (collection === undefined || collection === null) {
|
|
98
|
+
throw new CompileRefusal('TRANSFORM_NO_COLLECTION', `Transform "${step}" has no collection. Point it at the variable or step output holding the array.`, `collection: v('catalog')`);
|
|
99
|
+
}
|
|
100
|
+
if (!(collection instanceof Expr)) {
|
|
101
|
+
// Types are stripped at run time, so `collection: [{…}]` or
|
|
102
|
+
// `collection: 'catalog'` reaches here as a raw value.
|
|
103
|
+
throw new CompileRefusal('TRANSFORM_COLLECTION_NOT_REF', `Transform "${step}" was given a ${Array.isArray(collection) ? 'raw array' : `raw ${typeof collection}`} as its `
|
|
104
|
+
+ `collection. That field is a variable PATH — the runtime turns the string into a lookup, so a value there `
|
|
105
|
+
+ `resolves to nothing and the transform silently sees an empty collection.`, typeof collection === 'string' ? `collection: v('${collection}')` : `collection: v('<varName>')`);
|
|
106
|
+
}
|
|
107
|
+
// A decompiled canvas Transform with no collection prints as `lit([])`
|
|
108
|
+
// (`printTransform`), so recompiling it stops here too: this message names
|
|
109
|
+
// the fix for that file as well as for authored inline data.
|
|
110
|
+
if (collection.literal) {
|
|
111
|
+
throw new CompileRefusal('TRANSFORM_COLLECTION_LITERAL', `Transform "${step}" has a literal collection, and that field is a variable PATH, so inline data has no `
|
|
112
|
+
+ `form there. A Transform decompiled with no collection prints as \`lit([])\` and lands here too. Bind `
|
|
113
|
+
+ `\`collection\` to a variable or step output, e.g. \`v('<var>')\`; for inline data, keep the array in a `
|
|
114
|
+
+ `variable default (\`.var('items', types.array, [...])\`) and reference it.`, `collection: v('<var>')`);
|
|
115
|
+
}
|
|
116
|
+
return collection.js;
|
|
91
117
|
}
|
|
92
118
|
/**
|
|
93
119
|
* A transform node publishes a COLLECTION — always.
|