@uipath/maestro-builder-sdk 6.19.0 → 6.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-index.md +107 -107
- package/dist/api-members.md +358 -356
- package/dist/check.js +181 -45
- package/dist/cli/commands/flow.js +3 -0
- package/dist/compile-cli.js +51 -0
- package/dist/core/actions.d.ts +13 -5
- package/dist/core/actions.js +1 -1
- package/dist/core/binding-identity.d.ts +22 -0
- package/dist/core/binding-identity.js +28 -0
- package/dist/core/context-index.d.ts +34 -0
- package/dist/core/context-index.js +38 -0
- package/dist/core/definitionless-node-types.d.ts +16 -0
- package/dist/core/definitionless-node-types.js +16 -0
- package/dist/core/form-field-type.d.ts +33 -0
- package/dist/core/form-field-type.js +30 -0
- package/dist/core/hitl-routing.d.ts +8 -0
- package/dist/core/hitl-routing.js +17 -1
- package/dist/core/process-resource-sort-order.d.ts +40 -0
- package/dist/core/process-resource-sort-order.js +44 -0
- package/dist/core/wire-expr.d.ts +27 -0
- package/dist/core/wire-expr.js +34 -0
- package/dist/decompile-cli.js +11 -1
- package/dist/decompile.d.ts +8 -3
- package/dist/decompile.js +680 -104
- package/dist/flow-expr-check.js +1 -1
- package/dist/flow-sdk.d.ts +33 -1
- package/dist/flow-sdk.js +21 -2
- package/dist/flow-step-tree.js +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/merge-cli.js +25 -8
- package/dist/merge-roundtrip.d.ts +81 -0
- package/dist/merge-roundtrip.js +404 -0
- package/dist/merge.d.ts +35 -6
- package/dist/merge.js +124 -24
- package/dist/node-placement.d.ts +93 -0
- package/dist/node-placement.js +163 -0
- package/dist/nodes/transform/flow/node-type.js +2 -1
- package/dist/registry/uip/pipeline-front-door.js +5 -3
- package/dist/serialize.js +112 -61
- package/package.json +2 -2
package/dist/check.js
CHANGED
|
@@ -10,6 +10,7 @@ import { FlowNode } from './core/node-classes.js';
|
|
|
10
10
|
import { planContributionBindings } from './core/contribution-bindings.js';
|
|
11
11
|
import { definitionHasErrorPort, errorPortRefusal } from './core/error-port.js';
|
|
12
12
|
import { bundledActionNodeType, bundledDefinition, resolveContributionDefinition } from './serialize.js';
|
|
13
|
+
import { sdkFormFieldType } from './core/form-field-type.js';
|
|
13
14
|
import { QUEUE_ITEM_RECORD_SCHEMA } from './core/queue-record.js';
|
|
14
15
|
import { AGENTIC_PROCESS_FORMS } from './core/agentic-process-forms.js';
|
|
15
16
|
import { checkEventResolution, checkEventSubscription, eventParamsFor, } from './core/event-checks.js';
|
|
@@ -19,7 +20,7 @@ import { prepareCommand } from './core/cli-spelling.js';
|
|
|
19
20
|
import { commentText } from './core/comment-text.js';
|
|
20
21
|
import { editDistance } from './core/edit-distance.js';
|
|
21
22
|
import { isAnswerField } from './core/hitl-answers.js';
|
|
22
|
-
import { hitlDerivesOutcomeHandles, hitlExitModel, hitlRoutesPerOutcome } from './core/hitl-routing.js';
|
|
23
|
+
import { hitlDerivesOutcomeHandles, hitlExitModel, hitlRoutesPerOutcome, outcomeIdOf } from './core/hitl-routing.js';
|
|
23
24
|
import { declaredExits, hasExits } from './core/step-ports.js';
|
|
24
25
|
import { stopsAtEnd } from './flow-step-tree.js';
|
|
25
26
|
import { FLOW_SCHEMA_VERSIONS } from './format-profile.js';
|
|
@@ -179,6 +180,8 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
179
180
|
// OBJECT of the child's declared outputs, so a read of it is checked against
|
|
180
181
|
// that child rather than waved through (see SUBFLOW_* below).
|
|
181
182
|
const subflowChildren = new Map();
|
|
183
|
+
// `inLoop`: inside a loop/do-while body; `'arm'` when, counting from the
|
|
184
|
+
// innermost container, the path also sits in a `.parallel` arm.
|
|
182
185
|
const walk = (steps, inLoop = false, errorSource) => {
|
|
183
186
|
// Nothing may follow a terminate. `.return()` merely ends its own path, so a
|
|
184
187
|
// later step there is at worst dead weight — but a terminate ends the RUN,
|
|
@@ -197,6 +200,22 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
197
200
|
step: term.name,
|
|
198
201
|
});
|
|
199
202
|
}
|
|
203
|
+
// Nothing may follow a `.break()` or `.continue()` on the same path: the
|
|
204
|
+
// path leaves through the container's handle, so a later step would get no
|
|
205
|
+
// incoming edge.
|
|
206
|
+
const exitAt = steps.findIndex((st) => st.kind === 'break' || st.kind === 'continue');
|
|
207
|
+
if (exitAt !== -1 && exitAt < steps.length - 1) {
|
|
208
|
+
const after = steps[exitAt + 1];
|
|
209
|
+
const kind = steps[exitAt].kind;
|
|
210
|
+
diags.push({
|
|
211
|
+
level: 'error',
|
|
212
|
+
code: 'LOOP_EXIT_UNREACHABLE_AFTER',
|
|
213
|
+
message: `${steps.length - exitAt - 1} step(s) follow .${kind}(), starting with `
|
|
214
|
+
+ `"${'name' in after ? after.name : after.kind}". .${kind}() leaves this path through the container's `
|
|
215
|
+
+ `${kind} handle, so none of them can ever run. Move them before the .${kind}().`,
|
|
216
|
+
...('name' in after ? { step: String(after.name) } : {}),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
200
219
|
// Nothing may follow a human task whose EVERY outcome ends the run, except a
|
|
201
220
|
// `.return()`. Each outcome gets its own End node — the flow runtime never
|
|
202
221
|
// reads `action`, so only the graph can end the run there — which leaves
|
|
@@ -311,7 +330,7 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
311
330
|
}
|
|
312
331
|
else {
|
|
313
332
|
const model = hitlExitModel(prevSpec.inputs, prevAction?.options?.version);
|
|
314
|
-
const ids = (prevSpec.inputs.outcomes ?? []).map((o, i) =>
|
|
333
|
+
const ids = (prevSpec.inputs.outcomes ?? []).map((o, i) => outcomeIdOf(o, i));
|
|
315
334
|
if (model.shape === 'placeholder') {
|
|
316
335
|
diags.push({
|
|
317
336
|
level: 'error', code: 'HITL_OUTCOME_PORTS_OFF', step: prevAction?.name,
|
|
@@ -424,7 +443,7 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
424
443
|
if (s0.kind !== 'stepSwitch'
|
|
425
444
|
&& s.spec.kind === 'hitl' && hitlRoutesPerOutcome(s.spec.inputs, s.options?.version)) {
|
|
426
445
|
hitlOutcomeSteps.set(s.name, (s.spec.inputs.outcomes ?? []).map((o, i) => ({
|
|
427
|
-
id:
|
|
446
|
+
id: outcomeIdOf(o, i),
|
|
428
447
|
endsProcess: typeof o !== 'string' && o?.action === 'End',
|
|
429
448
|
})));
|
|
430
449
|
}
|
|
@@ -442,6 +461,7 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
442
461
|
&& !hitlRoutesPerOutcome(s.spec.inputs, s.options?.version)) {
|
|
443
462
|
hitlChoiceNoPorts.set(s.name, {
|
|
444
463
|
names: outcomes.map((o) => String((typeof o === 'string' ? o : o?.name) ?? '').trim()),
|
|
464
|
+
ids: outcomes.map((o, i) => outcomeIdOf(o, i)),
|
|
445
465
|
ends: outcomes.filter((o) => typeof o !== 'string' && o?.action === 'End')
|
|
446
466
|
.map((o) => String(o.name ?? '').trim()),
|
|
447
467
|
variant: s.spec.inputs?.variant,
|
|
@@ -563,6 +583,23 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
563
583
|
// Two arms with the same value: the second can never be reached, since
|
|
564
584
|
// the runtime takes the first matching case.
|
|
565
585
|
const seenValues = new Set();
|
|
586
|
+
// Two cases on one id share one `case-<id>` port (#1130: an explicit
|
|
587
|
+
// `id` can collide with a positional `case_<n>`).
|
|
588
|
+
const seenIds = new Set();
|
|
589
|
+
s.cases.forEach((c, i) => {
|
|
590
|
+
const caseId = c.id ?? `case_${i + 1}`;
|
|
591
|
+
if (typeof caseId !== 'string' || caseId === '' || seenIds.has(caseId)) {
|
|
592
|
+
diags.push({
|
|
593
|
+
level: 'error',
|
|
594
|
+
code: 'SWITCH_CASE_ID',
|
|
595
|
+
message: typeof caseId !== 'string' || caseId === ''
|
|
596
|
+
? `Switch "${s.name}" case ${JSON.stringify(c.value)} has an id that is not a non-empty string. Leave \`id\` out for the positional case_<n>.`
|
|
597
|
+
: `Switch "${s.name}" has two cases with the id "${caseId}", so both would leave from one 'case-${caseId}' port. Give one a different \`id\`.`,
|
|
598
|
+
step: s.name,
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
seenIds.add(String(caseId));
|
|
602
|
+
});
|
|
566
603
|
for (const c of s.cases) {
|
|
567
604
|
const key = `${typeof c.value}:${String(c.value)}`;
|
|
568
605
|
if (seenValues.has(key))
|
|
@@ -601,7 +638,7 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
601
638
|
parallelNames.add(s.name);
|
|
602
639
|
checkParallel(s, diags);
|
|
603
640
|
for (const arm of s.arms)
|
|
604
|
-
walk(arm, inLoop);
|
|
641
|
+
walk(arm, inLoop ? 'arm' : false);
|
|
605
642
|
}
|
|
606
643
|
else if (s.kind === 'loop') {
|
|
607
644
|
// A container is a legal ERROR-port source (its definition declares the
|
|
@@ -649,6 +686,17 @@ function checkFlow(built, ancestors, opts = {}) {
|
|
|
649
686
|
});
|
|
650
687
|
}
|
|
651
688
|
}
|
|
689
|
+
else if (s.kind === 'continue') {
|
|
690
|
+
if (!inLoop) {
|
|
691
|
+
diags.push({
|
|
692
|
+
level: 'error', code: 'CONTINUE_OUTSIDE_LOOP',
|
|
693
|
+
message: '.continue() is only valid inside a .loop() or .doWhile() body: it goes on to the container\'s next iteration.',
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
else if (inLoop === 'arm') {
|
|
697
|
+
diags.push(loopExitInArm());
|
|
698
|
+
}
|
|
699
|
+
}
|
|
652
700
|
else if (s.kind === 'return') {
|
|
653
701
|
for (const [name, e] of Object.entries(s.values)) {
|
|
654
702
|
addExpr(e.js, 'return');
|
|
@@ -1356,6 +1404,13 @@ function writtenTails(steps, incoming, sinks) {
|
|
|
1356
1404
|
breaks.push(...tails);
|
|
1357
1405
|
return [];
|
|
1358
1406
|
}
|
|
1407
|
+
// A `.continue()` path goes to the container's next pass, not to the steps
|
|
1408
|
+
// after it: for a do-while that is the condition test, one of the ways out,
|
|
1409
|
+
// exactly like reaching the end of the body — so it joins the exits too.
|
|
1410
|
+
if (st.kind === 'continue') {
|
|
1411
|
+
breaks.push(...tails);
|
|
1412
|
+
return [];
|
|
1413
|
+
}
|
|
1359
1414
|
if (st.kind === 'return' || st.kind === 'terminate')
|
|
1360
1415
|
return [];
|
|
1361
1416
|
const before = surelyWritten(tails);
|
|
@@ -2642,8 +2697,22 @@ function checkAgenticProcess(step, inputs, diags) {
|
|
|
2642
2697
|
}
|
|
2643
2698
|
const inSolution = inputs?.location === 'in-solution';
|
|
2644
2699
|
if (!inSolution && blank(inputs?.folderPath)) {
|
|
2645
|
-
|
|
2646
|
-
|
|
2700
|
+
// `registry list --local` shows an in-solution Flow sibling with folderPath: "".
|
|
2701
|
+
// An explicit `form` other than 'flow' keeps the published hint: switching it would change the node type.
|
|
2702
|
+
const meantInSolution = inputs?.form === 'flow'
|
|
2703
|
+
|| (typeof inputs?.folderPath === 'string' && inputs?.form === undefined);
|
|
2704
|
+
if (meantInSolution) {
|
|
2705
|
+
const formPart = inputs?.form === 'flow' ? '' : `, form: 'flow'`;
|
|
2706
|
+
at('AGENTIC_NO_FOLDER', `Agentic process step "${step}" has an empty \`folderPath\`. If the process is an unpublished sibling `
|
|
2707
|
+
+ `Flow in this solution, call it in-solution: set \`location: 'in-solution'\`${formPart} and \`projectId\` `
|
|
2708
|
+
+ `(the sibling's \`resource.projectKey\` in \`resources/solution_folder/process/flow/<project>.json\`), and `
|
|
2709
|
+
+ `drop \`folderPath\`. If it is published, set \`folderPath\` to the Orchestrator folder that holds it — `
|
|
2710
|
+
+ `the runtime starts the job by name + folder.`, `location: 'in-solution'${formPart}, projectId: '<resource.projectKey>'`);
|
|
2711
|
+
}
|
|
2712
|
+
else {
|
|
2713
|
+
at('AGENTIC_NO_FOLDER', `Agentic process step "${step}" has no \`folderPath\`. The runtime starts the job by name + folder, so `
|
|
2714
|
+
+ `the Orchestrator folder holding the process is required.`, `folderPath: 'Shared/uipath-agents/ProcurementProcess'`);
|
|
2715
|
+
}
|
|
2647
2716
|
}
|
|
2648
2717
|
const form = inputs?.form ?? 'bpmn';
|
|
2649
2718
|
if (inSolution && form !== 'flow') {
|
|
@@ -3115,13 +3184,31 @@ function checkInlineAgent(step, inputs, diags, opts = {}) {
|
|
|
3115
3184
|
+ `run-dependent search needs.`);
|
|
3116
3185
|
}
|
|
3117
3186
|
}
|
|
3187
|
+
// The index id is half of the resource node's TYPE
|
|
3188
|
+
// (`…context.index.<slug>.<id>`), and the platform resolves the index by it.
|
|
3189
|
+
// A blank, padded or non-GUID id builds a type no tenant serves, and nothing
|
|
3190
|
+
// downstream says so: the flow compiles, merges and validates, and the agent
|
|
3191
|
+
// runs ungrounded. So it is an error, not a warning. The id is tested as
|
|
3192
|
+
// written, not trimmed, because serialize emits it as written.
|
|
3193
|
+
for (const cx of contexts) {
|
|
3194
|
+
const id = typeof cx?.id === 'string' ? cx.id : '';
|
|
3195
|
+
if (!GUID_RE.test(id)) {
|
|
3196
|
+
at('AGENT_CONTEXT_NO_ID', `Inline agent step "${step}" grounds on context index "${cx?.name ?? ''}" with `
|
|
3197
|
+
+ (id.trim() === '' ? 'no `id`'
|
|
3198
|
+
: GUID_RE.test(id.trim()) ? `\`id\` ${JSON.stringify(cx?.id)}, which has whitespace around the GUID`
|
|
3199
|
+
: `\`id\` ${JSON.stringify(cx?.id)}, which is not a GUID`) + '. '
|
|
3200
|
+
+ `The id is the index's key: it is the tail of the resource node's type and how the runtime finds `
|
|
3201
|
+
+ `the index, so without a real one the agent runs ungrounded and \`validate\` does not notice. `
|
|
3202
|
+
+ `\`uip solution resources list --kind Index --source remote --search "<name>"\` returns it as \`Key\`.`);
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3118
3205
|
// An index outside the deployment folder needs its folder named. The resource
|
|
3119
3206
|
// file carries no `indexId`, so `indexName` + `folderPath` is the whole locator,
|
|
3120
3207
|
// and `@uipath/tool-agent`'s `createIndexBinding` leaves the folder off the
|
|
3121
3208
|
// binding when `folderPath` is empty. A WARNING, not an error: an index in the
|
|
3122
3209
|
// deployment folder resolves without it.
|
|
3123
3210
|
for (const cx of contexts) {
|
|
3124
|
-
if (cx
|
|
3211
|
+
if (cx != null && cx.folderPath === undefined) {
|
|
3125
3212
|
warn('AGENT_CONTEXT_NO_FOLDER_PATH', `Inline agent step "${step}" grounds on context index "${cx.name ?? ''}" without a \`folderPath\`. `
|
|
3126
3213
|
+ `The emitted resource carries no index id, so the folder is half of how the runtime finds the `
|
|
3127
3214
|
+ `index, and the binding is written without one. Give the index's folder — `
|
|
@@ -3448,8 +3535,8 @@ function checkInlineAgent(step, inputs, diags, opts = {}) {
|
|
|
3448
3535
|
for (const f of Array.isArray(e.fields) ? e.fields : []) {
|
|
3449
3536
|
if (!FORM_FIELD_TYPES.includes(String(f?.type))) {
|
|
3450
3537
|
at('INLINE_AGENT_ESCALATION_FIELD_TYPE_UNKNOWN', `Inline agent step "${step}"'s quick-form escalation gives the field "${String(f?.id ?? '?')}" the type `
|
|
3451
|
-
+ `${JSON.stringify(f?.type)}, which the form does not recognize — it would
|
|
3452
|
-
|
|
3538
|
+
+ `${JSON.stringify(f?.type)}, ${formFieldTypeWhy(f?.type, `which the form does not recognize — it would `
|
|
3539
|
+
+ `be published as a plain string`)}. Valid: ${FORM_FIELD_TYPES.join(', ')}.`, formFieldTypeHint(f?.type));
|
|
3453
3540
|
}
|
|
3454
3541
|
}
|
|
3455
3542
|
continue;
|
|
@@ -3831,7 +3918,7 @@ function armTails(steps, incoming) {
|
|
|
3831
3918
|
tails = [];
|
|
3832
3919
|
continue;
|
|
3833
3920
|
}
|
|
3834
|
-
if (st.kind === 'return' || st.kind === 'terminate' || st.kind === 'break')
|
|
3921
|
+
if (st.kind === 'return' || st.kind === 'terminate' || st.kind === 'break' || st.kind === 'continue')
|
|
3835
3922
|
return [];
|
|
3836
3923
|
if (st.kind === 'action') {
|
|
3837
3924
|
tails = actionTails(st.name, st.spec, st.options?.version);
|
|
@@ -4165,12 +4252,28 @@ function checkHttp(step, inputs, diags) {
|
|
|
4165
4252
|
}
|
|
4166
4253
|
}
|
|
4167
4254
|
}
|
|
4255
|
+
/**
|
|
4256
|
+
* Why a form-field type is refused: a product spelling of a type the SDK
|
|
4257
|
+
* models (`"string"` is the platform's own name for text), or a type
|
|
4258
|
+
* the SDK does not model at all. `unknownWhy` is the caller's reason for the
|
|
4259
|
+
* second case; the default is a human task's.
|
|
4260
|
+
*/
|
|
4261
|
+
function formFieldTypeWhy(type, unknownWhy = `which the platform does not recognize. Older runtimes read it as text, newer ones reject `
|
|
4262
|
+
+ `the form`) {
|
|
4263
|
+
const alias = sdkFormFieldType(type);
|
|
4264
|
+
return alias !== undefined
|
|
4265
|
+
? `which is the platform's spelling of the SDK's '${alias}' — the SDK writes '${alias}'`
|
|
4266
|
+
: unknownWhy;
|
|
4267
|
+
}
|
|
4168
4268
|
/** Every FormFieldType, once; `Record<FormFieldType, …>` makes tsc refuse a missing or extra one. */
|
|
4169
4269
|
const FORM_FIELD_TYPE_SET = { text: true, number: true, boolean: true, 'date-time': true };
|
|
4170
|
-
/**
|
|
4270
|
+
/** The form-field types the SDK models. */
|
|
4171
4271
|
const FORM_FIELD_TYPES = Object.keys(FORM_FIELD_TYPE_SET);
|
|
4172
4272
|
/** The `suggestion` for an unknown form-field type: the nearest valid one, else `type: 'text'`. */
|
|
4173
4273
|
function formFieldTypeHint(type) {
|
|
4274
|
+
const alias = sdkFormFieldType(type);
|
|
4275
|
+
if (alias !== undefined)
|
|
4276
|
+
return `type: '${alias}'`;
|
|
4174
4277
|
const nearest = nearestName(String(type), FORM_FIELD_TYPES);
|
|
4175
4278
|
return nearest !== undefined ? `type: '${nearest}'` : `type: 'text'`;
|
|
4176
4279
|
}
|
|
@@ -4182,18 +4285,6 @@ const RESERVED_ANSWER_IDS = new Set(['output', 'status', 'Action']);
|
|
|
4182
4285
|
* family — same availability note as do-while).
|
|
4183
4286
|
*/
|
|
4184
4287
|
const HITL_VARIANTS = ['quick-form', 'action-app', 'document-validation'];
|
|
4185
|
-
/**
|
|
4186
|
-
* An outcome's stable id — the serializer's own slug rule, mirrored so the
|
|
4187
|
-
* `outcome-<id>` port names this file validates are the ones that get emitted.
|
|
4188
|
-
*/
|
|
4189
|
-
function outcomeSlug(name, index) {
|
|
4190
|
-
const slug = String(name ?? '')
|
|
4191
|
-
.trim()
|
|
4192
|
-
.toLowerCase()
|
|
4193
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
4194
|
-
.replace(/^-+|-+$/g, '');
|
|
4195
|
-
return slug || `outcome-${index + 1}`;
|
|
4196
|
-
}
|
|
4197
4288
|
/**
|
|
4198
4289
|
* Why a base or quick-form task continues every outcome to the next step, as a
|
|
4199
4290
|
* clause for a diagnostic. `pinned` is the step's `{ version }`.
|
|
@@ -4263,10 +4354,9 @@ function reportUnreachableHitlOutcomes(choices, exprs, diags) {
|
|
|
4263
4354
|
for (const m of js.matchAll(/\$vars\.([A-Za-z0-9_]+)\s*\[/g))
|
|
4264
4355
|
readsOutcome.add(m[1]);
|
|
4265
4356
|
}
|
|
4266
|
-
for (const [step, { names, ends, variant, pinned, portsOff }] of choices) {
|
|
4357
|
+
for (const [step, { names, ids, ends, variant, pinned, portsOff }] of choices) {
|
|
4267
4358
|
if (readsOutcome.has(step))
|
|
4268
4359
|
continue;
|
|
4269
|
-
const ids = names.map((n, i) => outcomeSlug(n, i));
|
|
4270
4360
|
// The base node and quick-form draw a handle per outcome, so both can route
|
|
4271
4361
|
// it — `.stepSwitch`, or `outcomePorts: true` (a pin keeps its version). An
|
|
4272
4362
|
// app-backed variant cannot: the Action read is its only remedy.
|
|
@@ -4449,10 +4539,8 @@ function checkHitl(step, inputs, diags) {
|
|
|
4449
4539
|
return;
|
|
4450
4540
|
}
|
|
4451
4541
|
if (!FORM_FIELD_TYPES.includes(String(f?.type))) {
|
|
4452
|
-
at('HITL_FIELD_TYPE_UNKNOWN', `Human task "${step}" gives the form field "${id}" the type ${JSON.stringify(f?.type)},
|
|
4453
|
-
+
|
|
4454
|
-
+ `gets a text box and the value comes back as a string. Valid: `
|
|
4455
|
-
+ `${FORM_FIELD_TYPES.join(', ')}.`, formFieldTypeHint(f?.type));
|
|
4542
|
+
at('HITL_FIELD_TYPE_UNKNOWN', `Human task "${step}" gives the form field "${id}" the type ${JSON.stringify(f?.type)}, `
|
|
4543
|
+
+ `${formFieldTypeWhy(f?.type)}. Valid: ${FORM_FIELD_TYPES.join(', ')}.`, formFieldTypeHint(f?.type));
|
|
4456
4544
|
}
|
|
4457
4545
|
if (isAnswerField(f))
|
|
4458
4546
|
answers++; // an `output`, or an `inOut` whose reviewed value comes back
|
|
@@ -4491,8 +4579,26 @@ function checkHitl(step, inputs, diags) {
|
|
|
4491
4579
|
return;
|
|
4492
4580
|
}
|
|
4493
4581
|
const names = new Set();
|
|
4582
|
+
const ids = new Map();
|
|
4494
4583
|
outcomes.forEach((o, i) => {
|
|
4495
4584
|
const name = (typeof o === 'string' ? o : o?.name)?.trim();
|
|
4585
|
+
// Two outcomes on one id would share one `outcome-<id>` port, so the
|
|
4586
|
+
// second's edge could never be told apart. An explicit `id` can collide
|
|
4587
|
+
// with another outcome's derived slug.
|
|
4588
|
+
const id = outcomeIdOf(o, i);
|
|
4589
|
+
const explicitId = typeof o === 'object' && o !== null ? o.id : undefined;
|
|
4590
|
+
if (explicitId !== undefined && (typeof explicitId !== 'string' || explicitId === '')) {
|
|
4591
|
+
at('HITL_OUTCOME_BAD_ID', `Human task "${step}" outcome ${JSON.stringify(name ?? i)} has an id that is not a non-empty string. `
|
|
4592
|
+
+ `Leave \`id\` out to use the slug of the name.`, `{ name: ${JSON.stringify(name ?? 'Approve')} }`);
|
|
4593
|
+
}
|
|
4594
|
+
else if (ids.has(id) && ids.get(id) !== String(name ?? '')) {
|
|
4595
|
+
// Same name, same id: HITL_DUP_OUTCOME below reports it once, as a name clash.
|
|
4596
|
+
at('HITL_DUP_OUTCOME_ID', `Human task "${step}" gives outcomes "${ids.get(id)}" and "${name}" the same id "${id}", so both would `
|
|
4597
|
+
+ `leave from one 'outcome-${id}' port. Give one of them a different \`id\`.`);
|
|
4598
|
+
}
|
|
4599
|
+
else {
|
|
4600
|
+
ids.set(id, String(name ?? ''));
|
|
4601
|
+
}
|
|
4496
4602
|
if (!name) {
|
|
4497
4603
|
at('HITL_OUTCOME_NO_NAME', `Human task "${step}" has an outcome with no name (outcomes[${i}]). The result carries the `
|
|
4498
4604
|
+ `outcome's NAME — it is what "$vars.${step}.output.Action" holds and what the platform's `
|
|
@@ -5227,14 +5333,14 @@ function checkStepRefs(built, diags) {
|
|
|
5227
5333
|
index(built.steps, false, '', undefined);
|
|
5228
5334
|
/** Every ref, with where it sits. */
|
|
5229
5335
|
const found = [];
|
|
5230
|
-
const collect = (steps, from, scope, arm) => {
|
|
5336
|
+
const collect = (steps, from, scope, arm, enclosing = []) => {
|
|
5231
5337
|
let source = from;
|
|
5232
5338
|
for (const [i, s] of steps.entries()) {
|
|
5233
5339
|
if (s.kind === 'action')
|
|
5234
5340
|
source = s.name;
|
|
5235
5341
|
if (s.kind === 'stepToRef') {
|
|
5236
5342
|
found.push({
|
|
5237
|
-
target: s.target, port: s.port, from: source, scope, arm,
|
|
5343
|
+
target: s.target, port: s.port, from: source, scope, arm, enclosing,
|
|
5238
5344
|
// Only a default-port ref hands the path off, so only it can strand
|
|
5239
5345
|
// what follows; a side exit leaves the chain running.
|
|
5240
5346
|
after: s.port === 'output' ? steps.length - i - 1 : 0,
|
|
@@ -5242,23 +5348,23 @@ function checkStepRefs(built, diags) {
|
|
|
5242
5348
|
continue;
|
|
5243
5349
|
}
|
|
5244
5350
|
if (s.kind === 'stepToList')
|
|
5245
|
-
collect(s.body, source, scope, arm);
|
|
5351
|
+
collect(s.body, source, scope, arm, enclosing);
|
|
5246
5352
|
else if (s.kind === 'branch') {
|
|
5247
|
-
collect(s.then, source, scope, arm);
|
|
5248
|
-
collect(s.otherwise, source, scope, arm);
|
|
5353
|
+
collect(s.then, source, scope, arm, enclosing);
|
|
5354
|
+
collect(s.otherwise, source, scope, arm, enclosing);
|
|
5249
5355
|
}
|
|
5250
5356
|
else if (s.kind === 'switch') {
|
|
5251
|
-
s.cases.forEach((c) => collect(c.body, source, scope, arm));
|
|
5252
|
-
collect(s.default ?? [], source, scope, arm);
|
|
5357
|
+
s.cases.forEach((c) => collect(c.body, source, scope, arm, enclosing));
|
|
5358
|
+
collect(s.default ?? [], source, scope, arm, enclosing);
|
|
5253
5359
|
}
|
|
5254
5360
|
else if (s.kind === 'parallel')
|
|
5255
|
-
s.arms.forEach((a, i) => collect(a, source, scope, `${s.name}#${i}
|
|
5361
|
+
s.arms.forEach((a, i) => collect(a, source, scope, `${s.name}#${i}`, enclosing));
|
|
5256
5362
|
else if (s.kind === 'loop' || s.kind === 'doWhile')
|
|
5257
|
-
collect(s.body, source, s.name, arm);
|
|
5363
|
+
collect(s.body, source, s.name, arm, [...enclosing, s.name]);
|
|
5258
5364
|
}
|
|
5259
5365
|
};
|
|
5260
5366
|
collect(built.steps, undefined, '', undefined);
|
|
5261
|
-
for (const { target, from, after, scope, arm } of found) {
|
|
5367
|
+
for (const { target, from, after, scope, arm, enclosing } of found) {
|
|
5262
5368
|
const where = from ? `after "${from}"` : 'at the start of the flow';
|
|
5263
5369
|
const say = (level, code, message, suggestion) => {
|
|
5264
5370
|
diags.push({ level, code, message, step: from ?? 'stepToRef', ...(suggestion ? { suggestion } : {}) });
|
|
@@ -5269,6 +5375,16 @@ function checkStepRefs(built, diags) {
|
|
|
5269
5375
|
+ `${near ? ` (did you mean "${near}"?)` : ''}. It takes the same name you passed to .step().`, near ? `.stepToRef('${near}')` : undefined);
|
|
5270
5376
|
continue;
|
|
5271
5377
|
}
|
|
5378
|
+
// A ref to a container the path is INSIDE names it as a step, so the edge
|
|
5379
|
+
// enters it from its outer input: it starts the loop over from its first
|
|
5380
|
+
// item, while the iteration the path was in never finishes. What the author
|
|
5381
|
+
// means is almost always the next item, or out of the loop (#1129).
|
|
5382
|
+
if (enclosing.includes(target)) {
|
|
5383
|
+
say('error', 'REF_TO_OWN_LOOP', `.stepToRef('${target}') ${where} names the loop it is inside. That edge enters "${target}" from its outer `
|
|
5384
|
+
+ `input, which starts the loop over instead of going on to the next item. Use .continue() to go on to the `
|
|
5385
|
+
+ `next iteration, or .break() to leave the loop.`, '.continue()');
|
|
5386
|
+
continue;
|
|
5387
|
+
}
|
|
5272
5388
|
if (inPortList.has(target)) {
|
|
5273
5389
|
say('error', 'REF_INTO_PORT_LIST', `.stepToRef('${target}') ${where} targets a step inside another port's path. That path runs only when ITS `
|
|
5274
5390
|
+ `port fired — it is not somewhere another edge can jump into. Point at a step on the main path, or end `
|
|
@@ -5287,12 +5403,15 @@ function checkStepRefs(built, diags) {
|
|
|
5287
5403
|
+ `iteration, so those have no value. Point at a step after the loop, or do this work inside the body.`);
|
|
5288
5404
|
continue;
|
|
5289
5405
|
}
|
|
5290
|
-
// OUT of a loop body
|
|
5291
|
-
//
|
|
5292
|
-
//
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5406
|
+
// OUT of a loop body to a step after it. The container's own handles are
|
|
5407
|
+
// the measured ways out — `.break()` (its break handle) and `.continue()`
|
|
5408
|
+
// (its continue handle). Whether the runtime accepts an edge leaving a
|
|
5409
|
+
// bpmn:SubProcess some other way is not measured, so this warns and leaves
|
|
5410
|
+
// the verdict to `validate`.
|
|
5411
|
+
say('warning', 'REF_OUT_OF_LOOP', `.stepToRef('${target}') ${where} leaves the body of loop "${scope}" by an edge straight to a step outside it. `
|
|
5412
|
+
+ `To leave the loop, use .break() (the loop's break handle); to go on to the next item, use .continue(). `
|
|
5413
|
+
+ `Whether the runtime honours any other edge out of a loop body is not something this checker can prove: `
|
|
5414
|
+
+ `verify with uip maestro flow validate before relying on it.`);
|
|
5296
5415
|
}
|
|
5297
5416
|
// Best effort from here: the mechanism is understood, the runtime's behaviour
|
|
5298
5417
|
// is not measured, so these WARN and leave the verdict to `validate`.
|
|
@@ -5316,6 +5435,23 @@ function checkStepRefs(built, diags) {
|
|
|
5316
5435
|
}
|
|
5317
5436
|
}
|
|
5318
5437
|
}
|
|
5438
|
+
/**
|
|
5439
|
+
* LOOP_EXIT_IN_PARALLEL_ARM: a `.continue()` inside a `.parallel` arm sends
|
|
5440
|
+
* that arm's token to the container's continue handle, so it never reaches the
|
|
5441
|
+
* arm's Merge. The Merge is a bpmn:ParallelGateway that waits for every branch
|
|
5442
|
+
* it forked, and the iteration it belongs to is still running: the join never
|
|
5443
|
+
* fires and the iteration hangs. (`.break()` leaves the whole container, so
|
|
5444
|
+
* whether the runtime cancels the pending Merge is a different, unmeasured
|
|
5445
|
+
* question; it is not refused here.)
|
|
5446
|
+
*/
|
|
5447
|
+
function loopExitInArm() {
|
|
5448
|
+
return {
|
|
5449
|
+
level: 'error', code: 'LOOP_EXIT_IN_PARALLEL_ARM',
|
|
5450
|
+
message: `.continue() inside a .parallel() arm sends that arm's path to the loop's continue handle, so it never `
|
|
5451
|
+
+ `reaches the arm's Merge, which waits for every branch it forked: the join never fires and the iteration `
|
|
5452
|
+
+ `hangs. Put the .continue() after the .parallel(), on a .branch() that reads what the arms produced.`,
|
|
5453
|
+
};
|
|
5454
|
+
}
|
|
5319
5455
|
/**
|
|
5320
5456
|
* A field read off `$vars.<step>.error` — the envelope, and the one shape the
|
|
5321
5457
|
* authoring surface writes.
|
|
@@ -108,6 +108,7 @@ export function registerFlowCommands(program) {
|
|
|
108
108
|
.option('--original <file>', '(required) original .flow path, the artifact to merge into')
|
|
109
109
|
.option('-o, --output <file>', 'merged output path')
|
|
110
110
|
.option('--baseline <file>', 'compiled pristine decompile')
|
|
111
|
+
.option('--strict', 'refuse when the no-edit round trip (--baseline) differs from the original')
|
|
111
112
|
.allowExcessArguments()
|
|
112
113
|
.action(async (_edited, options, command) => {
|
|
113
114
|
const pair = namedOrLegacySecond(command, {
|
|
@@ -120,6 +121,8 @@ export function registerFlowCommands(program) {
|
|
|
120
121
|
const argv = [original, edited];
|
|
121
122
|
addValue(argv, '-o', options.output);
|
|
122
123
|
addValue(argv, '--baseline', options.baseline);
|
|
124
|
+
if (options.strict)
|
|
125
|
+
argv.push('--strict');
|
|
123
126
|
await invoke('flow merge', () => loadTool(import.meta.url, '../../merge-cli.js'), argv);
|
|
124
127
|
});
|
|
125
128
|
}
|
package/dist/compile-cli.js
CHANGED
|
@@ -167,7 +167,14 @@ export async function run(argv) {
|
|
|
167
167
|
// families the SDK cannot into the same directory (including `internal`
|
|
168
168
|
// built-ins such as `load-attachments`), so anything else found there is
|
|
169
169
|
// someone else's and is left alone.
|
|
170
|
+
//
|
|
171
|
+
// Owning the TYPE is not owning the FILE, though. Decompile keeps a canvas
|
|
172
|
+
// agent's `source` directory, so compiling decompiled source to another `.flow`
|
|
173
|
+
// in the same project writes into the ORIGINAL flow's agent directory, and the
|
|
174
|
+
// canvas resources there are still that flow's. A resource another
|
|
175
|
+
// `.flow` beside this one references by `inputs.source` is never pruned.
|
|
170
176
|
const removedResources = [];
|
|
177
|
+
const referencedElsewhere = resourceSourcesOfOtherFlows(outDir, resolve(out));
|
|
171
178
|
const owned = new Set(sidecars
|
|
172
179
|
.filter((s) => /\/resources\/[^/]+\/resource\.json$/.test(s.path))
|
|
173
180
|
.map((s) => resolve(outDir, s.path)));
|
|
@@ -182,6 +189,11 @@ export async function run(argv) {
|
|
|
182
189
|
const file = join(resourcesDir, entry, 'resource.json');
|
|
183
190
|
if (!existsSync(file) || owned.has(file))
|
|
184
191
|
continue;
|
|
192
|
+
const user = referencedElsewhere.get(entry);
|
|
193
|
+
if (user !== undefined) {
|
|
194
|
+
console.log(`compile: kept ${join('resources', entry, 'resource.json')}: ${user} still references it`);
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
185
197
|
let body;
|
|
186
198
|
try {
|
|
187
199
|
body = JSON.parse(readFileSync(file, 'utf8'));
|
|
@@ -248,3 +260,42 @@ function serializeWarningDiagnostic(w) {
|
|
|
248
260
|
return { level: 'warning', code: w.code, message: w.message };
|
|
249
261
|
}
|
|
250
262
|
runWhenInvokedDirectly(import.meta.url, 'compile', run);
|
|
263
|
+
/**
|
|
264
|
+
* The `inputs.source` uuids the OTHER `.flow` files in `dir` reference, each
|
|
265
|
+
* mapped to the first file that does. `dir` is the namespace sidecar paths are
|
|
266
|
+
* relative to, so these are the agent resources another flow still uses. The
|
|
267
|
+
* flow being written (`self`) is left out: what it no longer references is what
|
|
268
|
+
* pruning is for. Subflow bodies are included; an unreadable file contributes
|
|
269
|
+
* nothing.
|
|
270
|
+
*/
|
|
271
|
+
function resourceSourcesOfOtherFlows(dir, self) {
|
|
272
|
+
const users = new Map();
|
|
273
|
+
let entries;
|
|
274
|
+
try {
|
|
275
|
+
entries = readdirSync(dir).filter((f) => f.endsWith('.flow') && resolve(dir, f) !== self);
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
return users;
|
|
279
|
+
}
|
|
280
|
+
for (const f of entries) {
|
|
281
|
+
let flow;
|
|
282
|
+
try {
|
|
283
|
+
flow = JSON.parse(readFileSync(join(dir, f), 'utf8'));
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
const walk = (scope) => {
|
|
289
|
+
const s = scope;
|
|
290
|
+
for (const n of Array.isArray(s?.nodes) ? s.nodes : []) {
|
|
291
|
+
const src = n?.inputs?.source;
|
|
292
|
+
if (typeof src === 'string' && !users.has(src))
|
|
293
|
+
users.set(src, f);
|
|
294
|
+
}
|
|
295
|
+
if (s?.subflows && typeof s.subflows === 'object')
|
|
296
|
+
Object.values(s.subflows).forEach(walk);
|
|
297
|
+
};
|
|
298
|
+
walk(flow);
|
|
299
|
+
}
|
|
300
|
+
return users;
|
|
301
|
+
}
|
package/dist/core/actions.d.ts
CHANGED
|
@@ -190,9 +190,9 @@ export interface ConnectorOpts {
|
|
|
190
190
|
object?: string;
|
|
191
191
|
}
|
|
192
192
|
/**
|
|
193
|
-
* A form field's type.
|
|
194
|
-
*
|
|
195
|
-
*
|
|
193
|
+
* A form field's type. The platform does not recognize a type outside this set:
|
|
194
|
+
* older runtimes read it as text (so a number field becomes a text box), newer
|
|
195
|
+
* ones reject the form. That is why `check` rejects anything outside this set.
|
|
196
196
|
*/
|
|
197
197
|
export type FormFieldType = 'text' | 'number' | 'boolean' | 'date-time';
|
|
198
198
|
/** A value the reviewer READS: shown on the form, filled in from the flow. */
|
|
@@ -330,12 +330,20 @@ export interface DocValidationInputs {
|
|
|
330
330
|
* rather than continuing (the platform's own two-value vocabulary — anything
|
|
331
331
|
* that is not exactly `'End'` reads as continue).
|
|
332
332
|
*
|
|
333
|
+
* `id` is the outcome's wire id — `schema.outcomes[].id`, and the `outcome-<id>`
|
|
334
|
+
* port its edge leaves from. Leave it out and the id is a slug of `name`
|
|
335
|
+
* (`'Needs info'` → `needs-info`). Set it only to keep an id a flow already
|
|
336
|
+
* has, as decompile does for a task the designer drew
|
|
337
|
+
* (`{ name: 'Reject', id: 'outcome-1778015958049' }`): merge keeps the
|
|
338
|
+
* original node, so its edges must use that node's ids.
|
|
339
|
+
*
|
|
333
340
|
* @remarks
|
|
334
341
|
* A bare string is shorthand for `{ name }`.
|
|
335
342
|
*/
|
|
336
343
|
export type Outcome = string | {
|
|
337
344
|
name: string;
|
|
338
345
|
action?: 'Continue' | 'End';
|
|
346
|
+
id?: string;
|
|
339
347
|
};
|
|
340
348
|
/**
|
|
341
349
|
* WHICH human-task node type to emit. The platform ships three spellings of the
|
|
@@ -1132,7 +1140,7 @@ export interface AgentInputs {
|
|
|
1132
1140
|
*
|
|
1133
1141
|
* **Display-only, and it is the only thing that differs.** `registry get` on a
|
|
1134
1142
|
* coded and a low-code agent returns the same node type, the same
|
|
1135
|
-
* `Orchestrator.StartAgentJob` service type, the same `sortOrder
|
|
1143
|
+
* `Orchestrator.StartAgentJob` service type, the same `sortOrder` and the
|
|
1136
1144
|
* same bindings shape; the single difference is `display.icon` —
|
|
1137
1145
|
* `coded-agent` vs `autonomous-agent`. Which one a given resource is cannot be
|
|
1138
1146
|
* known offline, so it is stated here rather than guessed. Check with
|
|
@@ -2607,7 +2615,7 @@ export declare function agenticProcess(inputs: AgenticProcessInputs): ActionSpec
|
|
|
2607
2615
|
* @remarks
|
|
2608
2616
|
* **Coded and low-code agents are the same node.** This one factory authors both.
|
|
2609
2617
|
* `registry get` on a coded agent and on a low-code one returns the same node type,
|
|
2610
|
-
* the same service type, the same `sortOrder
|
|
2618
|
+
* the same service type, the same `sortOrder` and the same bindings shape; the
|
|
2611
2619
|
* only difference is the icon, which is what `flavour` sets. Which kind a resource
|
|
2612
2620
|
* is a property of what somebody published, not of the flow — so if a task asks for
|
|
2613
2621
|
* "a coded agent", what it is asking you to get right is the RESOURCE you point at.
|
package/dist/core/actions.js
CHANGED
|
@@ -609,7 +609,7 @@ export function agenticProcess(inputs) {
|
|
|
609
609
|
* @remarks
|
|
610
610
|
* **Coded and low-code agents are the same node.** This one factory authors both.
|
|
611
611
|
* `registry get` on a coded agent and on a low-code one returns the same node type,
|
|
612
|
-
* the same service type, the same `sortOrder
|
|
612
|
+
* the same service type, the same `sortOrder` and the same bindings shape; the
|
|
613
613
|
* only difference is the icon, which is what `flavour` sets. Which kind a resource
|
|
614
614
|
* is a property of what somebody published, not of the flow — so if a task asks for
|
|
615
615
|
* "a coded agent", what it is asking you to get right is the RESOURCE you point at.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a `.flow` binding row binds, as one comparable key.
|
|
3
|
+
*
|
|
4
|
+
* The key is `(resource, resourceKey, resourceSubType, propertyAttribute, name)`.
|
|
5
|
+
* It is NOT the row `id`: nodes never reference a row by id, and the two
|
|
6
|
+
* producers name rows differently (compile writes `b<Name><Prop>`, the canvas a
|
|
7
|
+
* random 9-character id), so two rows for the same property never share an id.
|
|
8
|
+
*
|
|
9
|
+
* `name` is part of the key. For a connection row it is the definition's own
|
|
10
|
+
* `<bindings.NAME>` placeholder, and two definitions sharing one connection
|
|
11
|
+
* spell it differently (`uipath-uipath-http connection`, `uipath-salesforce-slack
|
|
12
|
+
* connection`); each needs its own row, which is how compile keys them
|
|
13
|
+
* (`serialize.ts`, flow-builder-sdk#865). Process and queue rows carry
|
|
14
|
+
* `name === propertyAttribute`, so the extra field does not split them.
|
|
15
|
+
*
|
|
16
|
+
* Example: `{ resource: 'process', resourceKey: 'e05d...', resourceSubType: 'Flow',
|
|
17
|
+
* propertyAttribute: 'name', name: 'name' }` has the same identity whether its
|
|
18
|
+
* id is `bCalleeName` or `b7CE8ALjn`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function bindingIdentity(b: {
|
|
21
|
+
readonly [k: string]: unknown;
|
|
22
|
+
}): string;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a `.flow` binding row binds, as one comparable key.
|
|
3
|
+
*
|
|
4
|
+
* The key is `(resource, resourceKey, resourceSubType, propertyAttribute, name)`.
|
|
5
|
+
* It is NOT the row `id`: nodes never reference a row by id, and the two
|
|
6
|
+
* producers name rows differently (compile writes `b<Name><Prop>`, the canvas a
|
|
7
|
+
* random 9-character id), so two rows for the same property never share an id.
|
|
8
|
+
*
|
|
9
|
+
* `name` is part of the key. For a connection row it is the definition's own
|
|
10
|
+
* `<bindings.NAME>` placeholder, and two definitions sharing one connection
|
|
11
|
+
* spell it differently (`uipath-uipath-http connection`, `uipath-salesforce-slack
|
|
12
|
+
* connection`); each needs its own row, which is how compile keys them
|
|
13
|
+
* (`serialize.ts`, flow-builder-sdk#865). Process and queue rows carry
|
|
14
|
+
* `name === propertyAttribute`, so the extra field does not split them.
|
|
15
|
+
*
|
|
16
|
+
* Example: `{ resource: 'process', resourceKey: 'e05d...', resourceSubType: 'Flow',
|
|
17
|
+
* propertyAttribute: 'name', name: 'name' }` has the same identity whether its
|
|
18
|
+
* id is `bCalleeName` or `b7CE8ALjn`.
|
|
19
|
+
*/
|
|
20
|
+
export function bindingIdentity(b) {
|
|
21
|
+
return JSON.stringify([
|
|
22
|
+
b.resource ?? null,
|
|
23
|
+
b.resourceKey ?? null,
|
|
24
|
+
b.resourceSubType ?? null,
|
|
25
|
+
b.propertyAttribute ?? null,
|
|
26
|
+
b.name ?? null,
|
|
27
|
+
]);
|
|
28
|
+
}
|