@uipath/maestro-builder-sdk 6.6.1 → 6.8.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.
@@ -1,4 +1,5 @@
1
1
  import { createRequire } from 'node:module';
2
+ import { resolveFromRegistry } from './registry-fallback.js';
2
3
  const require = createRequire(import.meta.url);
3
4
  const REGISTRY = require('./registry-types.json');
4
5
  /** Every extension type in the snapshot, sorted. */
@@ -8,19 +9,35 @@ export function registryTypeNames() {
8
9
  /**
9
10
  * The spec for one extension type.
10
11
  *
12
+ * Snapshot first — that is the offline, reviewable, common path, and a hit costs
13
+ * a property read. On a MISS the registry is asked directly through `uip`
14
+ * ({@link resolveFromRegistry}), because a snapshot is always behind the platform
15
+ * by however long it has been since the last refresh, and a type outside it used
16
+ * to be unauthorable rather than merely unsupported: the typed methods,
17
+ * `dist/api-members.md` and {@link registryTypeNames} are all generated from this
18
+ * same file, so an author had no way even to learn the name. Set
19
+ * `FLOW_SDK_BPMN_REGISTRY_FALLBACK=0` to keep resolution snapshot-only.
20
+ *
11
21
  * @param type - The extension type, e.g. `'Intsvc.UnifiedHttpRequest'`.
12
22
  * @returns Its emission spec.
13
- * @throws If the type is not in the committed snapshot — which means either a
14
- * typo or a registry refresh the snapshot has not caught up with.
23
+ * @throws If the type is in neither the snapshot nor the registry — which means a
24
+ * typo, or no `uip` to ask.
15
25
  */
16
26
  export function registryType(type) {
17
- const spec = REGISTRY.types[type];
27
+ const spec = REGISTRY.types[type] ?? resolveFromRegistry(type);
18
28
  if (!spec) {
19
- throw new Error(`unknown extension type "${type}". The committed registry snapshot has ${REGISTRY.provenance.typeCount} types; ` +
20
- 'refresh it with `node scripts/materialize-bpmn-registry.mjs` if the platform added one.');
29
+ throw new Error(`unknown extension type "${type}". The committed registry snapshot has ${REGISTRY.provenance.typeCount} types `
30
+ + `(uip ${REGISTRY.provenance.uipVersion}) and the registry could not be asked for this one either — `
31
+ + 'no `uip` on PATH, not logged in, or the type does not exist. '
32
+ + 'Check the spelling with `uip maestro bpmn registry search <term>`, then refresh the snapshot with '
33
+ + '`node scripts/materialize-bpmn-registry.mjs`.');
21
34
  }
22
35
  return spec;
23
36
  }
37
+ /** True when the snapshot itself carries the type — no tenant needed to emit it. */
38
+ export function isSnapshotType(type) {
39
+ return Object.hasOwn(REGISTRY.types, type);
40
+ }
24
41
  /**
25
42
  * Facts the platform enforces but the registry does not state.
26
43
  *
package/dist/check.js CHANGED
@@ -116,7 +116,17 @@ function checkFlow(built, ancestors, opts = {}) {
116
116
  // decision waits until the expressions are collected (see
117
117
  // HITL_OUTCOMES_UNREACHABLE below).
118
118
  const hitlChoiceNoPorts = new Map();
119
- const dataFabricUpdates = [];
119
+ // Data Fabric writes that target a record `{ fromRead: '<step>' }` — update and
120
+ // delete share the selector, so they share the check.
121
+ const dataFabricRefs = [];
122
+ // Delete step names. `core.datafabric.delete` is the one verb of the family
123
+ // whose definition declares NO outputDefinition, so `$vars.<delete>.output` is
124
+ // undefined at run time (see DATAFABRIC_DELETE_HAS_NO_OUTPUT below).
125
+ const dataFabricDeletes = new Set();
126
+ // Reads that return a LIST. A multi-record read is refused as a write target
127
+ // (see DATAFABRIC_FROMREAD_MULTIPLE below) — the serializer emits a plain
128
+ // no-op task for the write, and nothing at any rung reports it.
129
+ const dataFabricMultiReads = new Set();
120
130
  // Placeholder (`mock()`) step names. The platform's `core.logic.mock` declares
121
131
  // one output whose `source` is the literal `"null"` — it publishes nothing —
122
132
  // so `$vars.<mock>.output` is null at run time, always (see
@@ -435,24 +445,52 @@ function checkFlow(built, ancestors, opts = {}) {
435
445
  });
436
446
  }
437
447
  }
438
- if (s.spec.kind === 'dataFabricRead' || s.spec.kind === 'dataFabricUpdate') {
448
+ if (s.spec.kind === 'dataFabricRead' || s.spec.kind === 'dataFabricCreate'
449
+ || s.spec.kind === 'dataFabricUpdate' || s.spec.kind === 'dataFabricDelete') {
439
450
  if (!String(s.spec.inputs.entity ?? '').trim()) {
440
451
  diags.push({
441
452
  level: 'error', code: 'DATAFABRIC_NO_ENTITY', step: s.name,
442
453
  message: `"${s.name}" names no Data Fabric entity. Set { entity: '<entity name>' }.`,
443
454
  });
444
455
  }
456
+ if (s.spec.kind === 'dataFabricRead') {
457
+ dataFabricReads.add(s.name);
458
+ if (s.spec.inputs.resultMode === 'multiple')
459
+ dataFabricMultiReads.add(s.name);
460
+ checkDataFabricQuery(s.name, s.spec.inputs, diags);
461
+ }
462
+ if (s.spec.kind === 'dataFabricCreate') {
463
+ // Not the same failure as an empty update: a BLANK value is omitted
464
+ // from the insert so the column default applies, so an all-blank
465
+ // `fieldValues` serializes to an empty body and runs green having
466
+ // inserted nothing. The node's schema rejects that — and the fix is
467
+ // a real value, never a placeholder to satisfy the rule.
468
+ const written = Object.values(s.spec.inputs.values ?? {})
469
+ .filter((v) => (v instanceof Expr ? true : String(v ?? '').trim() !== ''));
470
+ if (written.length === 0) {
471
+ diags.push({
472
+ level: 'error', code: 'DATAFABRIC_EMPTY_VALUES', step: s.name,
473
+ message: `"${s.name}" sets no column to a non-blank value, so it would insert nothing and run green. `
474
+ + 'The node\'s schema rejects that — give a real column a real value rather than a placeholder.',
475
+ });
476
+ }
477
+ checkDataFabricColumns(s.name, Object.keys(s.spec.inputs.values ?? {}), 'values', diags);
478
+ }
445
479
  if (s.spec.kind === 'dataFabricUpdate') {
446
- dataFabricUpdates.push({ step: s.name, inputs: s.spec.inputs });
447
480
  if (Object.keys(s.spec.inputs.set ?? {}).length === 0) {
448
481
  diags.push({
449
482
  level: 'error', code: 'DATAFABRIC_EMPTY_SET', step: s.name,
450
483
  message: `"${s.name}" writes no columns — { set } is empty, so the update would no-op green.`,
451
484
  });
452
485
  }
486
+ checkDataFabricColumns(s.name, Object.keys(s.spec.inputs.set ?? {}), 'set', diags);
453
487
  }
454
- else {
455
- dataFabricReads.add(s.name);
488
+ checkDataFabricScope(s.name, s.spec.inputs, diags);
489
+ if (s.spec.kind === 'dataFabricUpdate' || s.spec.kind === 'dataFabricDelete') {
490
+ const factory = s.spec.kind === 'dataFabricUpdate' ? 'dataFabricUpdate' : 'dataFabricDelete';
491
+ dataFabricRefs.push({ step: s.name, factory, record: s.spec.inputs.record });
492
+ if (s.spec.kind === 'dataFabricDelete')
493
+ dataFabricDeletes.add(s.name);
456
494
  }
457
495
  }
458
496
  if (s.spec.kind === 'ixpExtract')
@@ -729,12 +767,27 @@ function checkFlow(built, ancestors, opts = {}) {
729
767
  }
730
768
  }
731
769
  reportUnreachableHitlOutcomes(hitlChoiceNoPorts, exprs, diags);
732
- for (const { step, inputs } of dataFabricUpdates) {
733
- const fromRead = inputs.record?.fromRead;
734
- if (typeof fromRead === 'string' && !dataFabricReads.has(fromRead)) {
770
+ for (const { step, factory, record } of dataFabricRefs) {
771
+ const fromRead = record?.fromRead;
772
+ if (typeof fromRead !== 'string')
773
+ continue;
774
+ if (!dataFabricReads.has(fromRead)) {
735
775
  diags.push({
736
776
  level: 'error', code: 'DATAFABRIC_FROMREAD_UNKNOWN', step,
737
777
  message: `"${step}" targets record: { fromRead: ${JSON.stringify(fromRead)} }, but no dataFabricRead step has that name.`,
778
+ suggestion: `${factory}({ …, record: { byId: '<record id>' } })`,
779
+ });
780
+ continue;
781
+ }
782
+ // A LIST is not a write target: the serializer refuses it and emits a plain
783
+ // no-op task, so the flow validates, runs green, and writes nothing.
784
+ if (dataFabricMultiReads.has(fromRead)) {
785
+ diags.push({
786
+ level: 'error', code: 'DATAFABRIC_FROMREAD_MULTIPLE', step,
787
+ message: `"${step}" targets record: { fromRead: ${JSON.stringify(fromRead)} }, but "${fromRead}" is a `
788
+ + "resultMode: 'multiple' query. A multi-record read is refused as a write target — the step compiles to "
789
+ + 'a no-op and the run goes green having written nothing.',
790
+ suggestion: `loop the results and target each record with ${factory}({ …, record: { byId: … } })`,
738
791
  });
739
792
  }
740
793
  }
@@ -1002,6 +1055,20 @@ function checkFlow(built, ancestors, opts = {}) {
1002
1055
  });
1003
1056
  continue;
1004
1057
  }
1058
+ // A Data Fabric delete is the one verb of that family whose definition
1059
+ // declares no outputDefinition at all — the record is gone, so there is
1060
+ // nothing to publish. Read the record on the step that FOUND it instead,
1061
+ // which on the `{ fromRead }` form is already in the flow.
1062
+ if (dataFabricDeletes.has(name)) {
1063
+ diags.push({
1064
+ level: 'error',
1065
+ code: 'DATAFABRIC_DELETE_HAS_NO_OUTPUT',
1066
+ message: `Delete "${name}" produces no output, but "$vars.${name}${field ? '.' + field : ''}" reads one in "${step}". `
1067
+ + 'A Data Fabric delete declares no outputs — take the value off the dataFabricRead step that found the record, before the delete.',
1068
+ step,
1069
+ });
1070
+ continue;
1071
+ }
1005
1072
  if (delayNames.has(name)) {
1006
1073
  diags.push({
1007
1074
  level: 'error',
@@ -4635,6 +4702,102 @@ function checkCaseValueTypes(s, typeOf, diags, triggerId) {
4635
4702
  * `output` is the aggregate (after the loop) and `error` the failure detail.
4636
4703
  */
4637
4704
  const LOOP_FIELDS = new Set(['currentItem', 'currentIndex', 'collection', 'output', 'error']);
4705
+ /**
4706
+ * Columns Data Fabric assigns and never accepts a write for.
4707
+ *
4708
+ * The canvas hides them from both write editors, but the serializer has no
4709
+ * guard: a hand-authored `Id` row satisfies the node's own "at least one field"
4710
+ * rule, ships in the body, and is rejected — and the rejection is only logged,
4711
+ * so the row comes back unchanged and the run goes green.
4712
+ */
4713
+ const DATAFABRIC_SYSTEM_COLUMNS = new Set(['Id', 'CreateTime', 'CreatedBy', 'UpdateTime', 'UpdatedBy']);
4714
+ function checkDataFabricColumns(step, columns, option, diags) {
4715
+ const system = columns.filter((c) => DATAFABRIC_SYSTEM_COLUMNS.has(c));
4716
+ if (system.length) {
4717
+ diags.push({
4718
+ level: 'error', code: 'DATAFABRIC_SYSTEM_COLUMN', step,
4719
+ message: `"${step}" writes ${system.map((c) => JSON.stringify(c)).join(', ')} in { ${option} }. `
4720
+ + 'Data Fabric assigns its system columns (Id, CreateTime, CreatedBy, UpdateTime, UpdatedBy) and rejects a '
4721
+ + 'write to them — and it only LOGS the rejection, so the row comes back unchanged and the run goes green.',
4722
+ });
4723
+ }
4724
+ }
4725
+ /**
4726
+ * A folder-scoped entity is portable only with its `Entity` bindings.
4727
+ *
4728
+ * `_folderKey` on its own still serializes — it falls back to a source-org
4729
+ * literal and logs a warning — so the flow looks fine until it is deployed to
4730
+ * another org. Refusing the half-authored pair here is the whole point: the
4731
+ * alternative is a defect that only appears at deploy time, in a different org,
4732
+ * to somebody else.
4733
+ */
4734
+ function checkDataFabricScope(step, inputs, diags) {
4735
+ if (inputs.folderKey !== undefined && inputs.resourceKey === undefined) {
4736
+ diags.push({
4737
+ level: 'error', code: 'DATAFABRIC_FOLDER_NO_RESOURCE_KEY', step,
4738
+ message: `"${step}" sets { folderKey } without { resourceKey }. A folder-scoped entity needs both — with only `
4739
+ + 'the folder key the entity serializes as a source-org literal, which validates, runs in this org, and '
4740
+ + 'breaks on deploy to another one.',
4741
+ suggestion: 'resolve the key with `uip solution resources list --kind Entity`, or drop { folderKey } and keep '
4742
+ + 'the entity tenant-scoped',
4743
+ });
4744
+ }
4745
+ if (inputs.resourceKey !== undefined && inputs.folderKey === undefined) {
4746
+ diags.push({
4747
+ level: 'error', code: 'DATAFABRIC_RESOURCE_KEY_NO_FOLDER', step,
4748
+ message: `"${step}" sets { resourceKey } without { folderKey }. The pair exists to scope a FOLDER entity; a `
4749
+ + 'tenant-scoped entity needs neither and serializes as a dotted literal.',
4750
+ });
4751
+ }
4752
+ }
4753
+ /**
4754
+ * The query options, none of which the node's `inputDefinition` declares — they
4755
+ * are `_`-prefixed extras, so `uip maestro flow validate` checks none of them
4756
+ * and every mistake here is silent at run time.
4757
+ */
4758
+ function checkDataFabricQuery(step, inputs, diags) {
4759
+ const listOnly = ['limit', 'skip', 'sort', 'fields'].filter((k) => inputs[k] !== undefined);
4760
+ if (listOnly.length && inputs.resultMode !== 'multiple') {
4761
+ diags.push({
4762
+ level: 'error', code: 'DATAFABRIC_QUERY_OPTS_WITHOUT_MULTIPLE', step,
4763
+ message: `"${step}" sets { ${listOnly.join(', ')} }, which only a list query uses, but its resultMode is `
4764
+ + `${inputs.resultMode ? JSON.stringify(inputs.resultMode) : 'unset (a single-record read)'}.`,
4765
+ suggestion: "resultMode: 'multiple'",
4766
+ });
4767
+ }
4768
+ // The emitted query ALWAYS carries a limit — omitted it resolves to 100 — and
4769
+ // 1000 is a hard ceiling that TRUNCATES rather than erroring above it.
4770
+ if (inputs.limit !== undefined && (!Number.isInteger(inputs.limit) || inputs.limit < 1 || inputs.limit > 1000)) {
4771
+ diags.push({
4772
+ level: 'error', code: 'DATAFABRIC_LIMIT_RANGE', step,
4773
+ message: `"${step}": limit must be a whole number between 1 and 1000 (got ${inputs.limit}). 1000 is a hard `
4774
+ + 'ceiling — a larger value truncates silently, so page with { skip } instead.',
4775
+ });
4776
+ }
4777
+ // `in` is honoured only on a list query; on a single read the row compiles
4778
+ // away, and a read whose filters compiled away returns EVERY record.
4779
+ if (inputs.resultMode !== 'multiple' && (inputs.filters ?? []).some((f) => f.operator === 'in')) {
4780
+ diags.push({
4781
+ level: 'error', code: 'DATAFABRIC_IN_REQUIRES_MULTIPLE', step,
4782
+ message: `"${step}" filters with the 'in' operator, which is honoured only when resultMode is 'multiple'. `
4783
+ + 'On a single-record read that row compiles away — and a read whose filters compiled away matches every record.',
4784
+ });
4785
+ }
4786
+ // A filter expression switched on and left blank is refused by the serializer,
4787
+ // and it refuses the WHOLE query rather than dropping the row, which strands
4788
+ // every downstream reference to this step.
4789
+ for (const f of inputs.filters ?? []) {
4790
+ const blank = f.value instanceof Expr ? f.value.js.trim() === '' : String(f.value ?? '').trim() === '';
4791
+ if (blank) {
4792
+ diags.push({
4793
+ level: 'error', code: 'DATAFABRIC_FILTER_BLANK_VALUE', step,
4794
+ message: `"${step}" has a filter on ${JSON.stringify(f.field)} with a blank value. The serializer refuses the `
4795
+ + 'ENTIRE query rather than dropping the row, so the step emits nothing and every downstream $vars read of '
4796
+ + 'it breaks.',
4797
+ });
4798
+ }
4799
+ }
4800
+ }
4638
4801
  function collectActionExprs(s, out, errorSource) {
4639
4802
  const push = (val) => {
4640
4803
  if (val instanceof Expr && !val.literal)
@@ -4691,6 +4854,30 @@ function collectActionExprs(s, out, errorSource) {
4691
4854
  push(val);
4692
4855
  return;
4693
4856
  }
4857
+ if (s.spec.kind === 'dataFabricRead' || s.spec.kind === 'dataFabricCreate'
4858
+ || s.spec.kind === 'dataFabricUpdate' || s.spec.kind === 'dataFabricDelete') {
4859
+ // Every value a Data Fabric step carries is authored against flow scope: a
4860
+ // filter's comparison value, the record id, the columns written. The family
4861
+ // was missing here entirely, so a `$vars.` typo in any of them travelled to
4862
+ // the platform unchecked — and the delete verb's no-output rule below has
4863
+ // nothing to fire on until these are collected.
4864
+ if (s.spec.kind === 'dataFabricRead') {
4865
+ for (const f of s.spec.inputs.filters ?? [])
4866
+ push(f.value);
4867
+ return;
4868
+ }
4869
+ if (s.spec.kind === 'dataFabricCreate') {
4870
+ for (const val of Object.values(s.spec.inputs.values ?? {}))
4871
+ push(val);
4872
+ return;
4873
+ }
4874
+ push(s.spec.inputs.record?.byId);
4875
+ if (s.spec.kind === 'dataFabricUpdate') {
4876
+ for (const val of Object.values(s.spec.inputs.set ?? {}))
4877
+ push(val);
4878
+ }
4879
+ return;
4880
+ }
4694
4881
  if (s.spec.kind === 'connector') {
4695
4882
  // Same gap on the connector surface: `messageToSend: tmpl`…${v('typo')}``
4696
4883
  // reached the runtime unchecked.