@uipath/maestro-builder-sdk 6.46.6 → 6.47.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.
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The problems with a case's declared business events that `check` reports and
3
+ * `serialize` refuses: an empty name, a name declared twice (an `api-event`
4
+ * rule matches by name, so it could not tell the two apart), an option
5
+ * `.businessEvent()` does not read (it would be dropped from the caseplan), or a
6
+ * payload of a type the Designer does not write.
7
+ *
8
+ * A payload is an object or array schema (`type` absent, `'object'` or
9
+ * `'array'`): the two shapes the Case Designer and `uip maestro case sdd
10
+ * convert` write. Any other type, such as `{ type: 'string' }`, is refused.
11
+ * Nobody has confirmed that Studio Web's Business events panel loads one; once
12
+ * someone does, this can be loosened.
13
+ */
14
+ import type { BuiltBusinessEvent } from './case-sdk.js';
15
+ /** One problem with a declared business event. */
16
+ export interface BusinessEventProblem {
17
+ code: 'BUSINESS_EVENT_NAME' | 'DUP_BUSINESS_EVENT' | 'BUSINESS_EVENT_UNKNOWN_OPTION' | 'BUSINESS_EVENT_PAYLOAD_TYPE';
18
+ message: string;
19
+ }
20
+ /**
21
+ * Report every problem with the case's business events, in declaration order.
22
+ * Options set to `undefined` count as absent.
23
+ *
24
+ * @param events - The events as `.businessEvent()` recorded them.
25
+ * @returns The problems, empty when every event is well formed.
26
+ */
27
+ export declare function businessEventProblems(events: readonly BuiltBusinessEvent[]): BusinessEventProblem[];
@@ -0,0 +1,45 @@
1
+ /** The keys a declared business event carries; any other is refused. */
2
+ const BUSINESS_EVENT_KEYS = ['name', 'description', 'payload'];
3
+ /**
4
+ * Report every problem with the case's business events, in declaration order.
5
+ * Options set to `undefined` count as absent.
6
+ *
7
+ * @param events - The events as `.businessEvent()` recorded them.
8
+ * @returns The problems, empty when every event is well formed.
9
+ */
10
+ export function businessEventProblems(events) {
11
+ const problems = [];
12
+ const seen = new Set();
13
+ for (const event of events) {
14
+ if (typeof event.name !== 'string' || !event.name.trim())
15
+ problems.push({ code: 'BUSINESS_EVENT_NAME', message: 'A business event has an empty name — rules match events by name.' });
16
+ else if (seen.has(event.name))
17
+ problems.push({
18
+ code: 'DUP_BUSINESS_EVENT',
19
+ message: `Business event "${event.name}" is declared twice — rules match events by name, so each name must be unique.`,
20
+ });
21
+ else
22
+ seen.add(event.name);
23
+ for (const key of Object.keys(event))
24
+ if (!BUSINESS_EVENT_KEYS.includes(key) && event[key] !== undefined)
25
+ problems.push({
26
+ code: 'BUSINESS_EVENT_UNKNOWN_OPTION',
27
+ message: `Business event "${String(event.name)}" does not take option "${key}", so it would be dropped from the caseplan — ` +
28
+ `.businessEvent() takes "description" and "payload" (the payload's JSON Schema object; the caseplan's "schema" string is written from it).`,
29
+ });
30
+ const payload = event.payload;
31
+ if (payload !== undefined) {
32
+ const isRecord = typeof payload === 'object' && payload !== null && !Array.isArray(payload);
33
+ const type = isRecord ? payload.type : undefined;
34
+ if (!isRecord || (type !== undefined && type !== 'object' && type !== 'array'))
35
+ problems.push({
36
+ code: 'BUSINESS_EVENT_PAYLOAD_TYPE',
37
+ message: `Business event "${String(event.name)}" payload must be an object or array schema (no "type", type "object" or type "array"), ` +
38
+ `e.g. { type: 'object', properties: { amount: { type: 'number' } } }` +
39
+ (isRecord ? `; got type ${JSON.stringify(type)}.` : '; got a value that is not a JSON Schema object.') +
40
+ ` Other types are refused until Studio Web's Business events panel is confirmed to load them.`,
41
+ });
42
+ }
43
+ }
44
+ return problems;
45
+ }
@@ -24,7 +24,7 @@ export { preserveCaseJson } from './preserve.js';
24
24
  export type { CaseJsonPreservation, PreservedCaseNode, PreservedTaskFields, PreservedVariableLane } from './preserve.js';
25
25
  export { CaseBuildError } from './case-expr-check.js';
26
26
  export type { CaseLocatedDiagnostic } from './case-expr-check.js';
27
- export type CaseRuleType = 'case-entered' | 'required-tasks-completed' | 'required-stages-completed' | 'selected-stage-completed' | 'selected-stage-exited' | 'selected-tasks-completed' | 'current-stage-entered' | 'adhoc' | 'runs-sequentially' | 'user-selected-stage' | 'wait-for-connector' | 'sla-status-change';
27
+ export type CaseRuleType = 'case-entered' | 'required-tasks-completed' | 'required-stages-completed' | 'selected-stage-completed' | 'selected-stage-exited' | 'selected-tasks-completed' | 'current-stage-entered' | 'adhoc' | 'runs-sequentially' | 'user-selected-stage' | 'wait-for-connector' | 'sla-status-change' | 'api-event';
28
28
  export interface RuleOpts {
29
29
  /**
30
30
  * Symbolic stage label (for `selected-stage-completed` / `selected-stage-exited`).
@@ -72,6 +72,14 @@ export interface RuleOpts {
72
72
  * a **breach** rule (`slaId` alone). Resolved to `escalationId`.
73
73
  */
74
74
  escalation?: string;
75
+ /**
76
+ * For an `api-event` rule — the **name of the business event** it listens for,
77
+ * e.g. `rule('api-event', { businessEvent: 'Invoice Approved' })`. Declare the
78
+ * event on the case with {@link CaseBuilder.businessEvent}. Emitted as the
79
+ * rule's `businessEventType`; the match is on the exact name, so an
80
+ * undeclared or misspelled name never fires.
81
+ */
82
+ businessEvent?: string;
75
83
  /** A `=js:` gate on case state (for `adhoc`, or as an extra guard on any rule). */
76
84
  expression?: string;
77
85
  }
@@ -680,6 +688,29 @@ export interface CaseLayout {
680
688
  /** Trigger node layout keyed by its display name (`Trigger 1` for the default). */
681
689
  triggers?: Record<string, CaseNodeLayout>;
682
690
  }
691
+ /** Options for {@link CaseBuilder.businessEvent}. */
692
+ export interface BusinessEventOpts {
693
+ /** What the event means, shown in the Case Designer's Business events panel. */
694
+ description?: string;
695
+ /**
696
+ * The JSON Schema of the payload the sender attaches, e.g.
697
+ * `{ type: 'object', properties: { amount: { type: 'number' } } }`. Written to
698
+ * `schema` as a string, with `$schema` defaulted to draft 7, the way the Case
699
+ * Designer saves it. It must be an object or array schema (no `type`,
700
+ * `type: 'object'` or `type: 'array'`, the shapes the Designer writes): any
701
+ * other type is refused (BUSINESS_EVENT_PAYLOAD_TYPE) until someone confirms
702
+ * Studio Web's Business events panel loads it. Only the empty object schema (`{}`,
703
+ * `{ type: 'object' }` or `{ type: 'object', properties: {} }`) is left out;
704
+ * any other object payload, e.g. `{ type: 'object', required: ['a'] }`, is
705
+ * written.
706
+ */
707
+ payload?: Record<string, unknown>;
708
+ }
709
+ /** A declared business event, as the serializer reads it. */
710
+ export interface BuiltBusinessEvent extends BusinessEventOpts {
711
+ /** The event name an `api-event` rule matches on, exactly. */
712
+ name: string;
713
+ }
683
714
  export interface BuiltCase {
684
715
  id: string;
685
716
  name: string;
@@ -701,6 +732,8 @@ export interface BuiltCase {
701
732
  stages: BuiltStage[];
702
733
  caseExit: BuiltCaseExitCondition[];
703
734
  slaRules: BuiltSla[];
735
+ /** Business events an `api-event` rule can listen for (`metadata.businessEvents`). */
736
+ businessEvents?: BuiltBusinessEvent[];
704
737
  /** Empty = the default single manual trigger. */
705
738
  triggers: BuiltTrigger[];
706
739
  /** Optional designer layout, resolved to final node ids by the serializer. */
@@ -1146,6 +1179,7 @@ declare class CaseBuilder {
1146
1179
  private readonly _stages;
1147
1180
  private readonly _caseExit;
1148
1181
  private readonly _sla;
1182
+ private readonly _businessEvents;
1149
1183
  private readonly _triggers;
1150
1184
  constructor(_id: string);
1151
1185
  /**
@@ -1367,6 +1401,17 @@ declare class CaseBuilder {
1367
1401
  * @returns This builder, so calls chain.
1368
1402
  */
1369
1403
  sla(opts: SlaOpts): this;
1404
+ /**
1405
+ * Declare a business event: a named message an outside system sends to a
1406
+ * running case. An `api-event` rule listens for it by name, e.g.
1407
+ * `.exitWhen(rule('api-event', { businessEvent: 'Invoice Approved' }), …)`.
1408
+ * Emitted to `metadata.businessEvents`.
1409
+ *
1410
+ * @param name - The event name rules match on, exactly.
1411
+ * @param opts - Its description and payload schema.
1412
+ * @returns This builder, so calls chain.
1413
+ */
1414
+ businessEvent(name: string, opts?: BusinessEventOpts): this;
1370
1415
  /**
1371
1416
  * Finish the plan and return the description the serializer writes.
1372
1417
  *
@@ -652,6 +652,7 @@ class CaseBuilder {
652
652
  _stages = [];
653
653
  _caseExit = [];
654
654
  _sla = [];
655
+ _businessEvents = [];
655
656
  _triggers = [];
656
657
  constructor(_id) {
657
658
  this._id = _id;
@@ -936,6 +937,20 @@ class CaseBuilder {
936
937
  this._sla.push(buildSla(opts));
937
938
  return this;
938
939
  }
940
+ /**
941
+ * Declare a business event: a named message an outside system sends to a
942
+ * running case. An `api-event` rule listens for it by name, e.g.
943
+ * `.exitWhen(rule('api-event', { businessEvent: 'Invoice Approved' }), …)`.
944
+ * Emitted to `metadata.businessEvents`.
945
+ *
946
+ * @param name - The event name rules match on, exactly.
947
+ * @param opts - Its description and payload schema.
948
+ * @returns This builder, so calls chain.
949
+ */
950
+ businessEvent(name, opts = {}) {
951
+ this._businessEvents.push({ name, ...opts });
952
+ return this;
953
+ }
939
954
  /**
940
955
  * Finish the plan and return the description the serializer writes.
941
956
  *
@@ -958,6 +973,7 @@ class CaseBuilder {
958
973
  stages: this._stages,
959
974
  caseExit: this._caseExit,
960
975
  slaRules: this._sla,
976
+ ...(this._businessEvents.length ? { businessEvents: this._businessEvents } : {}),
961
977
  triggers: this._triggers,
962
978
  ...(this._layout ? { layout: this._layout } : {}),
963
979
  };
@@ -1,3 +1,5 @@
1
+ import { businessEventProblems } from './business-event-shape.js';
2
+ import { ruleShapeProblems } from './rule-options.js';
1
3
  import { types } from '../core/expr.js';
2
4
  import { authoredCaseVersion, CASE_FORMAT_PROFILE, CASE_SCHEMA_VERSIONS } from './format-profile.js';
3
5
  import { schemaVersionRefusal } from '../schema-version.js';
@@ -212,7 +214,35 @@ export function check(built, opts = {}) {
212
214
  code: 'NO_CASE_COMPLETION',
213
215
  message: 'Case has no completion rule — add .completeWhen(rule(\'required-stages-completed\')).',
214
216
  });
217
+ // Business events: `metadata.businessEvents` names, matched exactly by an
218
+ // `api-event` rule's `businessEventType`. `uip maestro case validate` rejects a
219
+ // rule naming none (BUSINESS_EVENT_NOT_SELECTED) or an undeclared one
220
+ // (BUSINESS_EVENT_NOT_FOUND); both are reported here, at the rule.
221
+ const businessEvents = new Set((built.businessEvents ?? []).map((event) => event.name));
222
+ for (const problem of businessEventProblems(built.businessEvents ?? []))
223
+ diags.push({ level: 'error', code: problem.code, message: problem.message });
224
+ const refEvent = (r, where) => {
225
+ if (r.rule !== 'api-event')
226
+ return;
227
+ if (!r.businessEvent)
228
+ diags.push({
229
+ level: 'error',
230
+ code: 'API_EVENT_NO_BUSINESS_EVENT',
231
+ message: 'Rule "api-event" names no business event, so it listens for nothing — pass { businessEvent: \'<name>\' } with a name declared by .businessEvent(\'<name>\').',
232
+ where,
233
+ });
234
+ else if (!businessEvents.has(r.businessEvent))
235
+ diags.push({
236
+ level: 'error',
237
+ code: 'UNKNOWN_BUSINESS_EVENT',
238
+ message: `Rule "api-event" listens for business event "${r.businessEvent}", which the case does not declare — add .businessEvent(${JSON.stringify(r.businessEvent)}) to the case.`,
239
+ where,
240
+ });
241
+ };
215
242
  const refPlacement = (r, slot, where) => {
243
+ for (const problem of ruleShapeProblems(r))
244
+ diags.push({ level: 'error', code: problem.code, message: problem.message, where });
245
+ refEvent(r, where);
216
246
  const bad = ILLEGAL.find((x) => x.rule === r.rule && x.slot === slot);
217
247
  if (bad)
218
248
  diags.push({
@@ -432,7 +462,7 @@ function checkStage(stage, diags, refStage, refPlacement, bindings) {
432
462
  // fails. So the signature below must include the expression and the refs, not
433
463
  // just the rule name — keying on the rule type alone would reject legal
434
464
  // value-tiered routing, which is the natural way to express "under $25k goes here".
435
- const sig = (c) => JSON.stringify(c.rules.map((g) => g.map((r) => [r.rule, r.stage ?? '', (r.tasks ?? []).join(','), r.expression ?? ''])));
465
+ const sig = (c) => JSON.stringify(c.rules.map((g) => g.map((r) => [r.rule, r.stage ?? '', (r.tasks ?? []).join(','), r.expression ?? '', r.businessEvent ?? ''])));
436
466
  const seenExit = new Map();
437
467
  stage.exit.forEach((c, i) => {
438
468
  const k = sig(c);
@@ -40,6 +40,7 @@ interface Rule {
40
40
  uipath?: SubscriptionBag;
41
41
  slaId?: string;
42
42
  escalationId?: string;
43
+ businessEventType?: string;
43
44
  }
44
45
  interface Condition {
45
46
  id?: string;
@@ -137,6 +138,12 @@ interface CasePlan {
137
138
  caseAppEnabled?: boolean;
138
139
  caseExitRules?: Condition[];
139
140
  slaRules?: SlaWire[];
141
+ businessEvents?: Array<{
142
+ name?: unknown;
143
+ description?: unknown;
144
+ schema?: unknown;
145
+ [key: string]: unknown;
146
+ }>;
140
147
  caseAppConfig?: {
141
148
  caseSummary?: unknown;
142
149
  sections?: Array<{
@@ -5,6 +5,7 @@ import { preserveSchemaVersion } from '../schema-version.js';
5
5
  import { ownEntry } from '../core/own-entry.js';
6
6
  import { isUnrestorableSlotId } from './preserve.js';
7
7
  import { isInSolutionKey, SOLUTION_FOLDER } from './resource-identity.js';
8
+ import { offlineBindingLabel } from './ids.js';
8
9
  const TASK_KIND_METHOD = {
9
10
  process: 'process',
10
11
  agent: 'agent',
@@ -260,11 +261,46 @@ function emitRule(rule, refs) {
260
261
  entries.push(['escalation', tsLiteral(escalation)]);
261
262
  }
262
263
  }
264
+ if (rule.rule === 'api-event' && rule.businessEventType !== undefined)
265
+ entries.push(['businessEvent', tsLiteral(rule.businessEventType)]);
263
266
  if (rule.conditionExpression !== undefined)
264
267
  entries.push(['expression', tsLiteral(rule.conditionExpression)]);
265
268
  const options = opts(entries);
266
269
  return options ? `rule(${tsLiteral(rule.rule)}, ${options})` : `rule(${tsLiteral(rule.rule)})`;
267
270
  }
271
+ /**
272
+ * `.businessEvent(name, { description, payload })` for one
273
+ * `metadata.businessEvents[]` entry. The Designer stores the payload schema as
274
+ * a JSON string; it comes back as the object the builder takes, `$schema`
275
+ * included, so the recompile writes the same string.
276
+ */
277
+ function businessEventSource(event) {
278
+ if (!event || typeof event !== 'object' || typeof event.name !== 'string')
279
+ throw new Error('a business event has no name, which is not authorable.');
280
+ const unknown = Object.keys(event).filter((key) => !['name', 'description', 'schema'].includes(key));
281
+ if (unknown.length)
282
+ throw new Error(`business event "${event.name}" has unsupported field "${unknown[0]}", which is not authorable.`);
283
+ if (event.description !== undefined && typeof event.description !== 'string')
284
+ throw new Error(`business event "${event.name}" has a non-string description.`);
285
+ let payload;
286
+ if (event.schema !== undefined) {
287
+ if (typeof event.schema !== 'string')
288
+ throw new Error(`business event "${event.name}" has a non-string schema.`);
289
+ try {
290
+ payload = JSON.parse(event.schema);
291
+ }
292
+ catch {
293
+ throw new Error(`business event "${event.name}" has a schema that is not JSON.`);
294
+ }
295
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload))
296
+ throw new Error(`business event "${event.name}" has a schema that is not a JSON Schema object.`);
297
+ }
298
+ const options = opts([
299
+ ['description', event.description !== undefined ? tsLiteral(event.description) : undefined],
300
+ ['payload', payload !== undefined ? tsLiteral(payload) : undefined],
301
+ ]);
302
+ return options ? `.businessEvent(${tsLiteral(event.name)}, ${options})` : `.businessEvent(${tsLiteral(event.name)})`;
303
+ }
268
304
  function emitRuleArg(condition, refs) {
269
305
  const groups = condition.rules ?? [];
270
306
  const rendered = groups.map((group) => group.map((rule) => emitRule(rule, refs)));
@@ -342,7 +378,18 @@ function bindingName(value, refs) {
342
378
  if (typeof value !== 'string' || !value.startsWith('=bindings.'))
343
379
  return undefined;
344
380
  const id = value.slice('=bindings.'.length);
345
- return refs.bindingById.get(id)?.id ?? id;
381
+ return bindingLabel(refs, id);
382
+ }
383
+ /**
384
+ * The name a binding reference decompiles to: the authored label when
385
+ * `serialize` minted the row offline (so a recompile with no bindings.json
386
+ * mints the same row, and a later bindings.json entry for that label matches),
387
+ * else the row id (a bindings.json or Designer row, which the recompile
388
+ * resolves by id).
389
+ */
390
+ function bindingLabel(refs, id) {
391
+ const row = refs.bindingById.get(id);
392
+ return offlineBindingLabel(row) ?? row?.id ?? id;
346
393
  }
347
394
  /**
348
395
  * The literal a resource task's `data.name` / `data.folderPath` names: the
@@ -567,7 +614,7 @@ function externalCall(task, refs, library) {
567
614
  const id = value.slice('=bindings.'.length);
568
615
  if (!refs.bindingById.has(id))
569
616
  throw new Error(`${owner} context ${name} references missing root binding "${id}".`);
570
- return id;
617
+ return bindingLabel(refs, id);
571
618
  };
572
619
  const identity = library.identifyAction({
573
620
  connectorKey: stringValue('connectorKey'),
@@ -1114,7 +1161,7 @@ export function collectPreservation(plan) {
1114
1161
  preservation.topLevel = topLevel;
1115
1162
  const metadataKnown = new Set([
1116
1163
  'caseIdentifier', 'caseIdentifierType', 'caseAppEnabled', 'caseAppConfig', 'publishVersion', 'caseExitRules', 'slaRules',
1117
- 'allowAdhocOptionalStageTasks', 'waitForHumanSelectNextStageDFConnector',
1164
+ 'allowAdhocOptionalStageTasks', 'waitForHumanSelectNextStageDFConnector', 'businessEvents',
1118
1165
  ]);
1119
1166
  const metadata = Object.fromEntries(Object.entries(plan.metadata ?? {}).filter(([name]) => !metadataKnown.has(name)));
1120
1167
  if (Object.keys(metadata).length)
@@ -1324,6 +1371,8 @@ export function decompileWithDiagnostics(plan, options = {}) {
1324
1371
  emitVariables(out, depth, plan, triggerNameById);
1325
1372
  triggers.forEach((_trigger, index) => out.line(depth, `.trigger(trigger${index + 1})`));
1326
1373
  (metadata.slaRules ?? []).forEach((sla, index) => out.line(depth, `.sla(${slaSource(sla, index)})`));
1374
+ for (const event of metadata.businessEvents ?? [])
1375
+ out.line(depth, businessEventSource(event));
1327
1376
  for (const node of plan.nodes ?? [])
1328
1377
  if (isStage(node))
1329
1378
  emitStage(out, depth, node, refs, options.library, selectNextStage);
@@ -22,3 +22,29 @@ export declare class IdGen {
22
22
  conditionId(seed: string): string;
23
23
  ruleId(seed: string): string;
24
24
  }
25
+ /**
26
+ * The seed `serialize` mints an offline binding id from (a connection or
27
+ * folder label no bindings.json declares). One definition for both sides, so
28
+ * {@link offlineBindingLabel} can recognise exactly the ids `serialize` mints.
29
+ *
30
+ * @param resource - The binding's `resource` (`Connection`).
31
+ * @param propertyAttribute - `ConnectionId` or `folderKey`.
32
+ * @param name - The authored label.
33
+ * @param connectionKey - For a folder bound to a connection, that connection's resourceKey.
34
+ * @returns The seed string.
35
+ */
36
+ export declare function offlineBindingSeed(resource: string, propertyAttribute: string, name: string, connectionKey?: string): string;
37
+ /**
38
+ * The authored label behind a root binding row that `serialize` minted offline,
39
+ * or `undefined` for any other row (a bindings.json entry, a Designer row).
40
+ *
41
+ * Offline the label is the row's only real content: a connection row carries
42
+ * it as `resourceKey`, a folder row on a connection as `default`, a plain
43
+ * folder row as `resourceKey`. The row is recognised only when its `id` is
44
+ * exactly the id that label mints, so decompile can name it by the label and
45
+ * a recompile with no bindings mints the same row again.
46
+ *
47
+ * @param row - A root binding entry.
48
+ * @returns The label, or `undefined`.
49
+ */
50
+ export declare function offlineBindingLabel(row: Record<string, unknown> | undefined): string | undefined;
package/dist/case/ids.js CHANGED
@@ -74,3 +74,52 @@ export class IdGen {
74
74
  return this.make('Rule_', 6, seed);
75
75
  }
76
76
  }
77
+ /**
78
+ * The seed `serialize` mints an offline binding id from (a connection or
79
+ * folder label no bindings.json declares). One definition for both sides, so
80
+ * {@link offlineBindingLabel} can recognise exactly the ids `serialize` mints.
81
+ *
82
+ * @param resource - The binding's `resource` (`Connection`).
83
+ * @param propertyAttribute - `ConnectionId` or `folderKey`.
84
+ * @param name - The authored label.
85
+ * @param connectionKey - For a folder bound to a connection, that connection's resourceKey.
86
+ * @returns The seed string.
87
+ */
88
+ export function offlineBindingSeed(resource, propertyAttribute, name, connectionKey) {
89
+ return connectionKey === undefined
90
+ ? `binding:${resource}:${propertyAttribute}:${name}`
91
+ : `binding:${resource}:${propertyAttribute}:${connectionKey}:${name}`;
92
+ }
93
+ /** The binding id {@link IdGen.make} gives `seed` at salt 0..3 (`b` + 8 base-62). */
94
+ function offlineBindingIds(seed) {
95
+ return [0, 1, 2, 3].map((salt) => 'b' + base62(salt === 0 ? seed : `${seed}~${salt}`, 8));
96
+ }
97
+ /**
98
+ * The authored label behind a root binding row that `serialize` minted offline,
99
+ * or `undefined` for any other row (a bindings.json entry, a Designer row).
100
+ *
101
+ * Offline the label is the row's only real content: a connection row carries
102
+ * it as `resourceKey`, a folder row on a connection as `default`, a plain
103
+ * folder row as `resourceKey`. The row is recognised only when its `id` is
104
+ * exactly the id that label mints, so decompile can name it by the label and
105
+ * a recompile with no bindings mints the same row again.
106
+ *
107
+ * @param row - A root binding entry.
108
+ * @returns The label, or `undefined`.
109
+ */
110
+ export function offlineBindingLabel(row) {
111
+ if (!row || typeof row.id !== 'string')
112
+ return undefined;
113
+ const { id, resource, propertyAttribute, resourceKey } = row;
114
+ if (typeof resource !== 'string' || typeof propertyAttribute !== 'string' || typeof resourceKey !== 'string')
115
+ return undefined;
116
+ const candidates = [
117
+ [resourceKey, offlineBindingSeed(resource, propertyAttribute, resourceKey)],
118
+ ];
119
+ if (propertyAttribute === 'folderKey' && typeof row.default === 'string')
120
+ candidates.push([row.default, offlineBindingSeed(resource, propertyAttribute, row.default, resourceKey)]);
121
+ for (const [label, seed] of candidates)
122
+ if (offlineBindingIds(seed).includes(id))
123
+ return label;
124
+ return undefined;
125
+ }
@@ -21,7 +21,7 @@ const TOP_LEVEL_KEYS = new Set([
21
21
  const METADATA_KEYS = new Set([
22
22
  'caseIdentifier', 'caseIdentifierType', 'caseAppEnabled', 'publishVersion', 'caseExitRules', 'slaRules',
23
23
  'caseAppConfig', 'adhocCaseEndData',
24
- 'allowAdhocOptionalStageTasks', 'waitForHumanSelectNextStageDFConnector',
24
+ 'allowAdhocOptionalStageTasks', 'waitForHumanSelectNextStageDFConnector', 'businessEvents',
25
25
  ]);
26
26
  const VARIABLE_KEYS = new Set([
27
27
  'id', 'name', 'type', 'default', 'body', '_jsonSchema', 'elementId', 'var',
@@ -0,0 +1,15 @@
1
+ /** Options each rule kind reads; check and serialize refuse any other key. */
2
+ import type { CaseRule } from './case-sdk.js';
3
+ /** One problem with a rule's shape: an unknown kind, or an option its kind does not read. */
4
+ export interface RuleShapeProblem {
5
+ code: 'UNKNOWN_RULE' | 'RULE_UNKNOWN_OPTION';
6
+ message: string;
7
+ }
8
+ /**
9
+ * Report an unknown rule kind, or each option the rule's kind does not read.
10
+ * Options set to `undefined` count as absent.
11
+ *
12
+ * @param r - The rule as built (`rule(type, opts)` or a literal).
13
+ * @returns The problems, empty when the rule is well formed.
14
+ */
15
+ export declare function ruleShapeProblems(r: CaseRule): RuleShapeProblem[];
@@ -0,0 +1,46 @@
1
+ import { ownEntry } from '../core/own-entry.js';
2
+ /** `expression` is a gate any rule can carry. */
3
+ const COMMON = ['expression'];
4
+ const RULE_OPTIONS = {
5
+ 'case-entered': [],
6
+ 'required-tasks-completed': [],
7
+ 'required-stages-completed': [],
8
+ 'selected-stage-completed': ['stage'],
9
+ 'selected-stage-exited': ['stage'],
10
+ 'selected-tasks-completed': ['tasks'],
11
+ 'current-stage-entered': [],
12
+ adhoc: [],
13
+ 'runs-sequentially': [],
14
+ 'user-selected-stage': [],
15
+ 'wait-for-connector': ['connector'],
16
+ 'sla-status-change': ['sla', 'escalation'],
17
+ 'api-event': ['businessEvent'],
18
+ };
19
+ /**
20
+ * Report an unknown rule kind, or each option the rule's kind does not read.
21
+ * Options set to `undefined` count as absent.
22
+ *
23
+ * @param r - The rule as built (`rule(type, opts)` or a literal).
24
+ * @returns The problems, empty when the rule is well formed.
25
+ */
26
+ export function ruleShapeProblems(r) {
27
+ const kind = r.rule;
28
+ const own = ownEntry(RULE_OPTIONS, kind);
29
+ if (!own)
30
+ return [
31
+ {
32
+ code: 'UNKNOWN_RULE',
33
+ message: `Unknown rule "${kind}" — the rule kinds are ${Object.keys(RULE_OPTIONS).map((k) => `"${k}"`).join(', ')}.`,
34
+ },
35
+ ];
36
+ const allowed = [...own, ...COMMON];
37
+ return Object.keys(r)
38
+ .filter((key) => key !== 'rule' && r[key] !== undefined)
39
+ .filter((key) => !allowed.includes(key))
40
+ .map((key) => ({
41
+ code: 'RULE_UNKNOWN_OPTION',
42
+ message: `Rule "${kind}" does not take option "${key}", so it would be dropped from the caseplan — ` +
43
+ `"${kind}" takes ${allowed.map((k) => `"${k}"`).join(', ')}.` +
44
+ (kind === 'api-event' ? ` Name the event with { businessEvent: '<name>' }.` : ''),
45
+ }));
46
+ }