@nanobpm/urban 0.89.0 → 0.90.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.
|
@@ -202,6 +202,18 @@ export interface ProcessInstanceSnapshot {
|
|
|
202
202
|
* {@link EngineClient.getProcessDefinitionXml} to fetch the deployed BPMN (the instance →
|
|
203
203
|
* `processDefinitionKey` → deployed XML path, no repo checkout). Absent when the engine omits it. */
|
|
204
204
|
readonly processDefinitionKey?: string;
|
|
205
|
+
/** The key of this instance's *immediate* parent process instance — the caller that instantiated
|
|
206
|
+
* it as a native child (a `<bpmn:callActivity>`). `undefined` for a top-level (root) instance,
|
|
207
|
+
* and absent when the engine omits it (same optional/engine-provided convention as
|
|
208
|
+
* `processDefinitionKey`). Lets a reduced-capability read path map a native child back to the
|
|
209
|
+
* subject that spawned it without a raw-REST channel. */
|
|
210
|
+
readonly parentProcessInstanceKey?: string;
|
|
211
|
+
/** The key of this instance's *root* process instance — the top-level ancestor of the
|
|
212
|
+
* call-activity hierarchy this instance belongs to (equal to `processInstanceKey` for a
|
|
213
|
+
* top-level instance). `undefined`/absent when the engine omits it (only present for hierarchies
|
|
214
|
+
* created on an engine version that reports it). Lets a reduced-capability read path correlate a
|
|
215
|
+
* deep native descendant straight back to its root subject. */
|
|
216
|
+
readonly rootProcessInstanceKey?: string;
|
|
205
217
|
}
|
|
206
218
|
/**
|
|
207
219
|
* A single open user task as {@link EngineClient.searchUserTasks} reports it. Carries the
|
|
@@ -219,6 +231,24 @@ export interface UserTaskSummary {
|
|
|
219
231
|
readonly formKey?: string;
|
|
220
232
|
/** An external form reference (a form linked outside the deployment), if any. */
|
|
221
233
|
readonly externalFormReference?: string;
|
|
234
|
+
/** The key of the process instance this task belongs to. Absent when the engine omits it (same
|
|
235
|
+
* optional/engine-provided convention as `processDefinitionKey`). For a task owned by a native
|
|
236
|
+
* child instance, this is the child's own key — the handle a reduced-capability read path feeds
|
|
237
|
+
* to {@link EngineClient.searchProcessInstances} to resolve the child's parent/root linkage. */
|
|
238
|
+
readonly processInstanceKey?: string;
|
|
239
|
+
/** The key of the *immediate* parent process instance of this task's owning instance — the caller
|
|
240
|
+
* that spawned it as a native child. Absent when the engine's user-task read model omits it (as
|
|
241
|
+
* it does today: the engine surfaces the parent linkage on {@link ProcessInstanceSnapshot}, so a
|
|
242
|
+
* caller resolves it via `processInstanceKey` → `searchProcessInstances`). Declared here for
|
|
243
|
+
* parity with the process-instance snapshot and to flow through automatically if the read model
|
|
244
|
+
* starts carrying it. */
|
|
245
|
+
readonly parentProcessInstanceKey?: string;
|
|
246
|
+
/** The key of the *root* process instance of this task's owning instance — the top-level ancestor
|
|
247
|
+
* of the call-activity hierarchy the task lives in. Absent when the engine omits it. Lets a
|
|
248
|
+
* reduced-capability read path (`pollUserTasks` and other reconcile/affordance surfaces)
|
|
249
|
+
* correlate a native-child (e.g. human-escalation grandchild) task straight back to its root
|
|
250
|
+
* subject through the typed seam, with no raw-REST fallback. */
|
|
251
|
+
readonly rootProcessInstanceKey?: string;
|
|
222
252
|
}
|
|
223
253
|
/**
|
|
224
254
|
* The lifecycle-state-agnostic selectors for a user-task search: which process instance,
|
|
@@ -230,6 +260,14 @@ export interface UserTaskFilter {
|
|
|
230
260
|
processInstanceKey?: string;
|
|
231
261
|
assignee?: string;
|
|
232
262
|
candidateGroup?: string;
|
|
263
|
+
/** Select only tasks whose owning instance has this *immediate* parent process instance. Honored
|
|
264
|
+
* server-side by the live gateway where the read model supports it; the in-process testkit
|
|
265
|
+
* applies it client-side against the row's parent linkage (absent today on the user-task read
|
|
266
|
+
* model, so it matches nothing until the engine surfaces it there). */
|
|
267
|
+
parentProcessInstanceKey?: string;
|
|
268
|
+
/** Select only tasks whose owning instance belongs to this *root* process-instance hierarchy — the
|
|
269
|
+
* selector a reduced-capability path uses to fetch every open task under one root subject. */
|
|
270
|
+
rootProcessInstanceKey?: string;
|
|
233
271
|
}
|
|
234
272
|
/**
|
|
235
273
|
* An element instance's lifecycle state, as the engine reports it on a
|
|
@@ -582,6 +620,12 @@ export interface EngineClient {
|
|
|
582
620
|
searchProcessInstances(filter?: {
|
|
583
621
|
processInstanceKeys?: string[];
|
|
584
622
|
state?: ProcessInstanceState;
|
|
623
|
+
/** Select only instances whose *immediate* parent is this process instance — the reduced-path
|
|
624
|
+
* query for "the native children spawned by this subject". */
|
|
625
|
+
parentProcessInstanceKey?: string;
|
|
626
|
+
/** Select only instances belonging to this *root* process-instance hierarchy — "every instance
|
|
627
|
+
* under this root subject", the deep-descendant correlation query. */
|
|
628
|
+
rootProcessInstanceKey?: string;
|
|
585
629
|
}): Promise<ProcessInstanceSnapshot[]>;
|
|
586
630
|
/**
|
|
587
631
|
* Search element instances ("tokens") by process instance, element, and/or lifecycle state
|
|
@@ -236,6 +236,8 @@ export declare class SdkEngineClient implements EngineClient {
|
|
|
236
236
|
searchProcessInstances(filter?: {
|
|
237
237
|
processInstanceKeys?: string[];
|
|
238
238
|
state?: ProcessInstanceState;
|
|
239
|
+
parentProcessInstanceKey?: string;
|
|
240
|
+
rootProcessInstanceKey?: string;
|
|
239
241
|
}): Promise<ProcessInstanceSnapshot[]>;
|
|
240
242
|
searchElementInstances(filter?: ElementInstanceFilter): Promise<ElementInstanceSummary[]>;
|
|
241
243
|
searchElementInstanceWaitStates(filter?: ElementInstanceWaitStateFilter): Promise<ElementInstanceWaitState[]>;
|
|
@@ -465,7 +465,20 @@ export class SdkEngineClient {
|
|
|
465
465
|
async searchUserTasks(filter) {
|
|
466
466
|
// User tasks are an eventually consistent read; ask for zero-wait consistency so
|
|
467
467
|
// the search reflects what is currently visible without blocking.
|
|
468
|
-
|
|
468
|
+
// Normalize the process-instance key selectors through the shared presence rule so a
|
|
469
|
+
// blank/whitespace-only selector is *dropped* rather than forwarded as a present filter that
|
|
470
|
+
// silently matches nothing, and a padded key (`" 123 "`) matches the normalized keys we
|
|
471
|
+
// surface — mirroring `WasmEngineClient` (which normalizes via `presentKey`) and
|
|
472
|
+
// `searchProcessInstances` below (No Drift Surfaces).
|
|
473
|
+
const f = { ...(filter ?? {}) };
|
|
474
|
+
for (const sel of ["processInstanceKey", "parentProcessInstanceKey", "rootProcessInstanceKey"]) {
|
|
475
|
+
const normalized = presentEngineKey(filter?.[sel]);
|
|
476
|
+
if (normalized)
|
|
477
|
+
f[sel] = normalized;
|
|
478
|
+
else
|
|
479
|
+
delete f[sel];
|
|
480
|
+
}
|
|
481
|
+
const body = await this.client.searchUserTasks({ filter: f }, { consistency: { waitUpToMs: 0 } });
|
|
469
482
|
const items = Array.isArray(body.items) ? body.items.filter(isRecord) : [];
|
|
470
483
|
return items.flatMap((it) => {
|
|
471
484
|
const userTaskKey = it.userTaskKey ?? it.key;
|
|
@@ -476,10 +489,16 @@ export class SdkEngineClient {
|
|
|
476
489
|
// The engine resolves a `formId="X"` linkage to the latest deployed form's key at
|
|
477
490
|
// task creation, so `formKey` (not a form id) is the linkage it reports on the task;
|
|
478
491
|
// no `resolveFormKeyByFormId` resolver is passed because the gateway already resolved it.
|
|
492
|
+
const processInstanceKey = presentEngineKey(it.processInstanceKey);
|
|
493
|
+
const parentProcessInstanceKey = presentEngineKey(it.parentProcessInstanceKey);
|
|
494
|
+
const rootProcessInstanceKey = presentEngineKey(it.rootProcessInstanceKey);
|
|
479
495
|
return [{
|
|
480
496
|
userTaskKey: String(userTaskKey),
|
|
481
497
|
elementId: typeof it.elementId === "string" ? it.elementId : undefined,
|
|
482
498
|
variables: isRecord(it.variables) ? it.variables : undefined,
|
|
499
|
+
...(processInstanceKey ? { processInstanceKey } : {}),
|
|
500
|
+
...(parentProcessInstanceKey ? { parentProcessInstanceKey } : {}),
|
|
501
|
+
...(rootProcessInstanceKey ? { rootProcessInstanceKey } : {}),
|
|
483
502
|
...pickFormLinkage(it),
|
|
484
503
|
}];
|
|
485
504
|
});
|
|
@@ -545,9 +564,24 @@ export class SdkEngineClient {
|
|
|
545
564
|
const f = {};
|
|
546
565
|
if (filter?.state)
|
|
547
566
|
f.state = filter.state;
|
|
548
|
-
|
|
567
|
+
// Normalize each requested key through the shared presence rule (drop blank/whitespace-only
|
|
568
|
+
// entries, trim padded ones like `" 123 "`) so the `$in` filter carries only present keys that
|
|
569
|
+
// match the normalized keys we surface — mirroring `WasmEngineClient` and the selector
|
|
570
|
+
// handling above (No Drift Surfaces).
|
|
571
|
+
const keys = filter?.processInstanceKeys
|
|
572
|
+
?.map((k) => presentEngineKey(k))
|
|
573
|
+
.filter((k) => k !== undefined);
|
|
549
574
|
if (keys && keys.length > 0)
|
|
550
575
|
f.processInstanceKey = { $in: keys };
|
|
576
|
+
// Parent/root selectors let the reduced path query a subject's native descendants
|
|
577
|
+
// server-side. Normalize each through the shared presence rule so a blank selector is
|
|
578
|
+
// dropped rather than sent as an empty filter that would match nothing.
|
|
579
|
+
const parentProcessInstanceKey = presentEngineKey(filter?.parentProcessInstanceKey);
|
|
580
|
+
if (parentProcessInstanceKey)
|
|
581
|
+
f.parentProcessInstanceKey = parentProcessInstanceKey;
|
|
582
|
+
const rootProcessInstanceKey = presentEngineKey(filter?.rootProcessInstanceKey);
|
|
583
|
+
if (rootProcessInstanceKey)
|
|
584
|
+
f.rootProcessInstanceKey = rootProcessInstanceKey;
|
|
551
585
|
// A process-instance search is an eventually consistent read; ask for zero-wait
|
|
552
586
|
// consistency so it reflects what is currently visible without blocking. Cap the page
|
|
553
587
|
// to the number of keys asked for (each key matches at most one instance), so a bounded
|
|
@@ -556,24 +590,32 @@ export class SdkEngineClient {
|
|
|
556
590
|
const body = await this.client.searchProcessInstances({ filter: f, ...(page ? { page } : {}) }, { consistency: { waitUpToMs: 0 } });
|
|
557
591
|
const items = Array.isArray(body.items) ? body.items.filter(isRecord) : [];
|
|
558
592
|
return items.flatMap((it) => {
|
|
559
|
-
|
|
560
|
-
|
|
593
|
+
// Normalize the surfaced primary key through the shared presence rule (trim padded,
|
|
594
|
+
// drop blank/whitespace-only) so it matches the normalized parent/root linkage keys
|
|
595
|
+
// and the `searchUserTasks` mapping — the surfaced process-instance key must not drift
|
|
596
|
+
// between the two reduced-path searches (No Drift Surfaces).
|
|
597
|
+
const processInstanceKey = presentEngineKey(it.processInstanceKey);
|
|
598
|
+
if (!processInstanceKey) {
|
|
561
599
|
this.log("warn", "skipping process instance with no key in engine response");
|
|
562
600
|
return [];
|
|
563
601
|
}
|
|
564
602
|
const state = normalizeProcessInstanceState(it.state);
|
|
565
603
|
if (!state) {
|
|
566
604
|
this.log("warn", "skipping process instance with unrecognized state", {
|
|
567
|
-
processInstanceKey
|
|
605
|
+
processInstanceKey,
|
|
568
606
|
state: String(it.state),
|
|
569
607
|
});
|
|
570
608
|
return [];
|
|
571
609
|
}
|
|
572
610
|
const processDefinitionKey = presentEngineKey(it.processDefinitionKey);
|
|
611
|
+
const parentProcessInstanceKey = presentEngineKey(it.parentProcessInstanceKey);
|
|
612
|
+
const rootProcessInstanceKey = presentEngineKey(it.rootProcessInstanceKey);
|
|
573
613
|
return [{
|
|
574
|
-
processInstanceKey
|
|
614
|
+
processInstanceKey,
|
|
575
615
|
state,
|
|
576
616
|
...(processDefinitionKey ? { processDefinitionKey } : {}),
|
|
617
|
+
...(parentProcessInstanceKey ? { parentProcessInstanceKey } : {}),
|
|
618
|
+
...(rootProcessInstanceKey ? { rootProcessInstanceKey } : {}),
|
|
577
619
|
}];
|
|
578
620
|
});
|
|
579
621
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/urban",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.90.0",
|
|
4
4
|
"description": "Urban: build and run code-first apps on Nano — runtime, derivation toolkit, and CLI in one. Author durable processes with @nanobpm/workflow (defineFlow), a typed datasource, forms, triggers and surfaces from a nano.app.json manifest, on Node or Deno.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
package/src/runtime/core/host.ts
CHANGED
|
@@ -233,6 +233,18 @@ export interface ProcessInstanceSnapshot {
|
|
|
233
233
|
* {@link EngineClient.getProcessDefinitionXml} to fetch the deployed BPMN (the instance →
|
|
234
234
|
* `processDefinitionKey` → deployed XML path, no repo checkout). Absent when the engine omits it. */
|
|
235
235
|
readonly processDefinitionKey?: string;
|
|
236
|
+
/** The key of this instance's *immediate* parent process instance — the caller that instantiated
|
|
237
|
+
* it as a native child (a `<bpmn:callActivity>`). `undefined` for a top-level (root) instance,
|
|
238
|
+
* and absent when the engine omits it (same optional/engine-provided convention as
|
|
239
|
+
* `processDefinitionKey`). Lets a reduced-capability read path map a native child back to the
|
|
240
|
+
* subject that spawned it without a raw-REST channel. */
|
|
241
|
+
readonly parentProcessInstanceKey?: string;
|
|
242
|
+
/** The key of this instance's *root* process instance — the top-level ancestor of the
|
|
243
|
+
* call-activity hierarchy this instance belongs to (equal to `processInstanceKey` for a
|
|
244
|
+
* top-level instance). `undefined`/absent when the engine omits it (only present for hierarchies
|
|
245
|
+
* created on an engine version that reports it). Lets a reduced-capability read path correlate a
|
|
246
|
+
* deep native descendant straight back to its root subject. */
|
|
247
|
+
readonly rootProcessInstanceKey?: string;
|
|
236
248
|
}
|
|
237
249
|
|
|
238
250
|
/**
|
|
@@ -251,6 +263,24 @@ export interface UserTaskSummary {
|
|
|
251
263
|
readonly formKey?: string;
|
|
252
264
|
/** An external form reference (a form linked outside the deployment), if any. */
|
|
253
265
|
readonly externalFormReference?: string;
|
|
266
|
+
/** The key of the process instance this task belongs to. Absent when the engine omits it (same
|
|
267
|
+
* optional/engine-provided convention as `processDefinitionKey`). For a task owned by a native
|
|
268
|
+
* child instance, this is the child's own key — the handle a reduced-capability read path feeds
|
|
269
|
+
* to {@link EngineClient.searchProcessInstances} to resolve the child's parent/root linkage. */
|
|
270
|
+
readonly processInstanceKey?: string;
|
|
271
|
+
/** The key of the *immediate* parent process instance of this task's owning instance — the caller
|
|
272
|
+
* that spawned it as a native child. Absent when the engine's user-task read model omits it (as
|
|
273
|
+
* it does today: the engine surfaces the parent linkage on {@link ProcessInstanceSnapshot}, so a
|
|
274
|
+
* caller resolves it via `processInstanceKey` → `searchProcessInstances`). Declared here for
|
|
275
|
+
* parity with the process-instance snapshot and to flow through automatically if the read model
|
|
276
|
+
* starts carrying it. */
|
|
277
|
+
readonly parentProcessInstanceKey?: string;
|
|
278
|
+
/** The key of the *root* process instance of this task's owning instance — the top-level ancestor
|
|
279
|
+
* of the call-activity hierarchy the task lives in. Absent when the engine omits it. Lets a
|
|
280
|
+
* reduced-capability read path (`pollUserTasks` and other reconcile/affordance surfaces)
|
|
281
|
+
* correlate a native-child (e.g. human-escalation grandchild) task straight back to its root
|
|
282
|
+
* subject through the typed seam, with no raw-REST fallback. */
|
|
283
|
+
readonly rootProcessInstanceKey?: string;
|
|
254
284
|
}
|
|
255
285
|
|
|
256
286
|
/**
|
|
@@ -263,6 +293,14 @@ export interface UserTaskFilter {
|
|
|
263
293
|
processInstanceKey?: string;
|
|
264
294
|
assignee?: string;
|
|
265
295
|
candidateGroup?: string;
|
|
296
|
+
/** Select only tasks whose owning instance has this *immediate* parent process instance. Honored
|
|
297
|
+
* server-side by the live gateway where the read model supports it; the in-process testkit
|
|
298
|
+
* applies it client-side against the row's parent linkage (absent today on the user-task read
|
|
299
|
+
* model, so it matches nothing until the engine surfaces it there). */
|
|
300
|
+
parentProcessInstanceKey?: string;
|
|
301
|
+
/** Select only tasks whose owning instance belongs to this *root* process-instance hierarchy — the
|
|
302
|
+
* selector a reduced-capability path uses to fetch every open task under one root subject. */
|
|
303
|
+
rootProcessInstanceKey?: string;
|
|
266
304
|
}
|
|
267
305
|
|
|
268
306
|
/**
|
|
@@ -636,6 +674,12 @@ export interface EngineClient {
|
|
|
636
674
|
searchProcessInstances(filter?: {
|
|
637
675
|
processInstanceKeys?: string[];
|
|
638
676
|
state?: ProcessInstanceState;
|
|
677
|
+
/** Select only instances whose *immediate* parent is this process instance — the reduced-path
|
|
678
|
+
* query for "the native children spawned by this subject". */
|
|
679
|
+
parentProcessInstanceKey?: string;
|
|
680
|
+
/** Select only instances belonging to this *root* process-instance hierarchy — "every instance
|
|
681
|
+
* under this root subject", the deep-descendant correlation query. */
|
|
682
|
+
rootProcessInstanceKey?: string;
|
|
639
683
|
}): Promise<ProcessInstanceSnapshot[]>;
|
|
640
684
|
/**
|
|
641
685
|
* Search element instances ("tokens") by process instance, element, and/or lifecycle state
|
|
@@ -315,6 +315,72 @@ test("searchUserTasks surfaces the resolved form linkage", async () => {
|
|
|
315
315
|
]);
|
|
316
316
|
});
|
|
317
317
|
|
|
318
|
+
test("searchUserTasks normalizes the process-instance key selectors and drops blank ones", async () => {
|
|
319
|
+
// Guards the selector-normalization defect class: a padded selector (`" 42 "`) must reach the
|
|
320
|
+
// engine trimmed (so it matches the normalized keys the adapter surfaces), a blank/whitespace-only
|
|
321
|
+
// selector must be *dropped* (not forwarded as a present filter that silently matches nothing),
|
|
322
|
+
// and unrelated selectors must pass through untouched — mirroring `searchProcessInstances`.
|
|
323
|
+
let input: { filter?: Record<string, unknown> } | undefined;
|
|
324
|
+
const client = fakeSdkClient({
|
|
325
|
+
searchUserTasks: async (i) => {
|
|
326
|
+
input = i;
|
|
327
|
+
return { items: [] };
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
const engine = new SdkEngineClient(client);
|
|
331
|
+
await engine.searchUserTasks({
|
|
332
|
+
processInstanceKey: " 42 ", // padded → trimmed to "42"
|
|
333
|
+
parentProcessInstanceKey: " ", // whitespace-only → dropped
|
|
334
|
+
rootProcessInstanceKey: "", // blank → dropped
|
|
335
|
+
assignee: "alice", // unrelated selector passes through unchanged
|
|
336
|
+
});
|
|
337
|
+
const filter = input?.filter ?? {};
|
|
338
|
+
assert.equal(filter.processInstanceKey, "42", "padded key selector is trimmed");
|
|
339
|
+
assert.equal("parentProcessInstanceKey" in filter, false, "whitespace-only parent selector is dropped");
|
|
340
|
+
assert.equal("rootProcessInstanceKey" in filter, false, "blank root selector is dropped");
|
|
341
|
+
assert.equal(filter.assignee, "alice", "unrelated selectors pass through unchanged");
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test("searchUserTasks surfaces the process-instance linkage keys and drops blank ones", async () => {
|
|
345
|
+
// Guards the linkage-output defect class: the three linkage keys the engine reports on a task
|
|
346
|
+
// (processInstanceKey/parentProcessInstanceKey/rootProcessInstanceKey) must be surfaced on the
|
|
347
|
+
// returned `UserTaskSummary`, normalized through the same presence rule — a padded value trimmed,
|
|
348
|
+
// a blank/whitespace-only value omitted rather than surfaced as an empty string.
|
|
349
|
+
const client = fakeSdkClient({
|
|
350
|
+
searchUserTasks: async () => ({
|
|
351
|
+
items: [
|
|
352
|
+
{
|
|
353
|
+
userTaskKey: 1,
|
|
354
|
+
elementId: "a",
|
|
355
|
+
processInstanceKey: 10,
|
|
356
|
+
parentProcessInstanceKey: 20,
|
|
357
|
+
rootProcessInstanceKey: 30,
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
userTaskKey: 2,
|
|
361
|
+
elementId: "b",
|
|
362
|
+
processInstanceKey: " 11 ", // padded → trimmed
|
|
363
|
+
parentProcessInstanceKey: " ", // whitespace-only → omitted
|
|
364
|
+
rootProcessInstanceKey: "", // blank → omitted
|
|
365
|
+
},
|
|
366
|
+
],
|
|
367
|
+
}),
|
|
368
|
+
});
|
|
369
|
+
const engine = new SdkEngineClient(client);
|
|
370
|
+
const tasks = await engine.searchUserTasks();
|
|
371
|
+
assert.deepEqual(tasks, [
|
|
372
|
+
{
|
|
373
|
+
userTaskKey: "1",
|
|
374
|
+
elementId: "a",
|
|
375
|
+
variables: undefined,
|
|
376
|
+
processInstanceKey: "10",
|
|
377
|
+
parentProcessInstanceKey: "20",
|
|
378
|
+
rootProcessInstanceKey: "30",
|
|
379
|
+
},
|
|
380
|
+
{ userTaskKey: "2", elementId: "b", variables: undefined, processInstanceKey: "11" },
|
|
381
|
+
]);
|
|
382
|
+
});
|
|
383
|
+
|
|
318
384
|
test("getForm resolves by formKey and parses the serialized schema", async () => {
|
|
319
385
|
let seen: unknown;
|
|
320
386
|
const schema = { type: "default", schemaVersion: 18, components: [{ type: "textfield", key: "name" }] };
|
|
@@ -486,6 +552,64 @@ test("searchProcessInstances skips keyless and unrecognized-state rows", async (
|
|
|
486
552
|
assert.deepEqual(out, [{ processInstanceKey: "10", state: "COMPLETED" }]);
|
|
487
553
|
});
|
|
488
554
|
|
|
555
|
+
test("searchProcessInstances normalizes the parent/root key selectors and drops blank ones", async () => {
|
|
556
|
+
// Guards the request-mapping defect class for the reduced path's parent/root selectors: a padded
|
|
557
|
+
// selector must be trimmed, a blank/whitespace-only selector must be dropped rather than sent as
|
|
558
|
+
// an empty filter that matches nothing — mirroring `searchUserTasks` (No Drift Surfaces).
|
|
559
|
+
let seenInput: { filter?: Record<string, unknown> } | undefined;
|
|
560
|
+
const client = fakeSdkClient({
|
|
561
|
+
searchProcessInstances: async (input) => {
|
|
562
|
+
seenInput = input;
|
|
563
|
+
return { items: [] };
|
|
564
|
+
},
|
|
565
|
+
});
|
|
566
|
+
const engine = new SdkEngineClient(client);
|
|
567
|
+
await engine.searchProcessInstances({
|
|
568
|
+
parentProcessInstanceKey: " 42 ", // padded → trimmed to "42"
|
|
569
|
+
rootProcessInstanceKey: " ", // whitespace-only → dropped
|
|
570
|
+
});
|
|
571
|
+
const filter = seenInput?.filter ?? {};
|
|
572
|
+
assert.equal(filter.parentProcessInstanceKey, "42", "padded parent selector is trimmed");
|
|
573
|
+
assert.equal("rootProcessInstanceKey" in filter, false, "whitespace-only root selector is dropped");
|
|
574
|
+
// A selector-only search carries no key `$in` and no page cap.
|
|
575
|
+
assert.deepEqual(seenInput, { filter: { parentProcessInstanceKey: "42" } });
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
test("searchProcessInstances surfaces the parent/root linkage keys and drops blank ones", async () => {
|
|
579
|
+
// Guards the linkage-output defect class: the parent/root keys the engine reports on an instance
|
|
580
|
+
// must be surfaced on the returned `ProcessInstanceSnapshot`, normalized through the same presence
|
|
581
|
+
// rule — a padded value trimmed, a blank/whitespace-only value omitted rather than an empty string.
|
|
582
|
+
const client = fakeSdkClient({
|
|
583
|
+
searchProcessInstances: async () => ({
|
|
584
|
+
items: [
|
|
585
|
+
{
|
|
586
|
+
processInstanceKey: 10,
|
|
587
|
+
state: "ACTIVE",
|
|
588
|
+
parentProcessInstanceKey: 20,
|
|
589
|
+
rootProcessInstanceKey: 30,
|
|
590
|
+
},
|
|
591
|
+
{
|
|
592
|
+
processInstanceKey: " 11 ", // padded → trimmed
|
|
593
|
+
state: "COMPLETED",
|
|
594
|
+
parentProcessInstanceKey: " ", // whitespace-only → omitted
|
|
595
|
+
rootProcessInstanceKey: "", // blank → omitted
|
|
596
|
+
},
|
|
597
|
+
],
|
|
598
|
+
}),
|
|
599
|
+
});
|
|
600
|
+
const engine = new SdkEngineClient(client);
|
|
601
|
+
const out = await engine.searchProcessInstances();
|
|
602
|
+
assert.deepEqual(out, [
|
|
603
|
+
{
|
|
604
|
+
processInstanceKey: "10",
|
|
605
|
+
state: "ACTIVE",
|
|
606
|
+
parentProcessInstanceKey: "20",
|
|
607
|
+
rootProcessInstanceKey: "30",
|
|
608
|
+
},
|
|
609
|
+
{ processInstanceKey: "11", state: "COMPLETED" },
|
|
610
|
+
]);
|
|
611
|
+
});
|
|
612
|
+
|
|
489
613
|
test("normalizeProcessInstanceState maps the terminal set and rejects others", () => {
|
|
490
614
|
assert.equal(normalizeProcessInstanceState("ACTIVE"), "ACTIVE");
|
|
491
615
|
assert.equal(normalizeProcessInstanceState("completed"), "COMPLETED");
|
|
@@ -677,8 +677,19 @@ export class SdkEngineClient implements EngineClient {
|
|
|
677
677
|
}): Promise<UserTaskSummary[]> {
|
|
678
678
|
// User tasks are an eventually consistent read; ask for zero-wait consistency so
|
|
679
679
|
// the search reflects what is currently visible without blocking.
|
|
680
|
+
// Normalize the process-instance key selectors through the shared presence rule so a
|
|
681
|
+
// blank/whitespace-only selector is *dropped* rather than forwarded as a present filter that
|
|
682
|
+
// silently matches nothing, and a padded key (`" 123 "`) matches the normalized keys we
|
|
683
|
+
// surface — mirroring `WasmEngineClient` (which normalizes via `presentKey`) and
|
|
684
|
+
// `searchProcessInstances` below (No Drift Surfaces).
|
|
685
|
+
const f: Record<string, unknown> = { ...(filter ?? {}) };
|
|
686
|
+
for (const sel of ["processInstanceKey", "parentProcessInstanceKey", "rootProcessInstanceKey"] as const) {
|
|
687
|
+
const normalized = presentEngineKey(filter?.[sel]);
|
|
688
|
+
if (normalized) f[sel] = normalized;
|
|
689
|
+
else delete f[sel];
|
|
690
|
+
}
|
|
680
691
|
const body = await this.client.searchUserTasks(
|
|
681
|
-
{ filter:
|
|
692
|
+
{ filter: f },
|
|
682
693
|
{ consistency: { waitUpToMs: 0 } },
|
|
683
694
|
);
|
|
684
695
|
const items = Array.isArray(body.items) ? body.items.filter(isRecord) : [];
|
|
@@ -691,10 +702,16 @@ export class SdkEngineClient implements EngineClient {
|
|
|
691
702
|
// The engine resolves a `formId="X"` linkage to the latest deployed form's key at
|
|
692
703
|
// task creation, so `formKey` (not a form id) is the linkage it reports on the task;
|
|
693
704
|
// no `resolveFormKeyByFormId` resolver is passed because the gateway already resolved it.
|
|
705
|
+
const processInstanceKey = presentEngineKey(it.processInstanceKey);
|
|
706
|
+
const parentProcessInstanceKey = presentEngineKey(it.parentProcessInstanceKey);
|
|
707
|
+
const rootProcessInstanceKey = presentEngineKey(it.rootProcessInstanceKey);
|
|
694
708
|
return [{
|
|
695
709
|
userTaskKey: String(userTaskKey),
|
|
696
710
|
elementId: typeof it.elementId === "string" ? it.elementId : undefined,
|
|
697
711
|
variables: isRecord(it.variables) ? it.variables : undefined,
|
|
712
|
+
...(processInstanceKey ? { processInstanceKey } : {}),
|
|
713
|
+
...(parentProcessInstanceKey ? { parentProcessInstanceKey } : {}),
|
|
714
|
+
...(rootProcessInstanceKey ? { rootProcessInstanceKey } : {}),
|
|
698
715
|
...pickFormLinkage(it),
|
|
699
716
|
}];
|
|
700
717
|
});
|
|
@@ -763,11 +780,26 @@ export class SdkEngineClient implements EngineClient {
|
|
|
763
780
|
async searchProcessInstances(filter?: {
|
|
764
781
|
processInstanceKeys?: string[];
|
|
765
782
|
state?: ProcessInstanceState;
|
|
783
|
+
parentProcessInstanceKey?: string;
|
|
784
|
+
rootProcessInstanceKey?: string;
|
|
766
785
|
}): Promise<ProcessInstanceSnapshot[]> {
|
|
767
786
|
const f: Record<string, unknown> = {};
|
|
768
787
|
if (filter?.state) f.state = filter.state;
|
|
769
|
-
|
|
788
|
+
// Normalize each requested key through the shared presence rule (drop blank/whitespace-only
|
|
789
|
+
// entries, trim padded ones like `" 123 "`) so the `$in` filter carries only present keys that
|
|
790
|
+
// match the normalized keys we surface — mirroring `WasmEngineClient` and the selector
|
|
791
|
+
// handling above (No Drift Surfaces).
|
|
792
|
+
const keys = filter?.processInstanceKeys
|
|
793
|
+
?.map((k) => presentEngineKey(k))
|
|
794
|
+
.filter((k): k is string => k !== undefined);
|
|
770
795
|
if (keys && keys.length > 0) f.processInstanceKey = { $in: keys };
|
|
796
|
+
// Parent/root selectors let the reduced path query a subject's native descendants
|
|
797
|
+
// server-side. Normalize each through the shared presence rule so a blank selector is
|
|
798
|
+
// dropped rather than sent as an empty filter that would match nothing.
|
|
799
|
+
const parentProcessInstanceKey = presentEngineKey(filter?.parentProcessInstanceKey);
|
|
800
|
+
if (parentProcessInstanceKey) f.parentProcessInstanceKey = parentProcessInstanceKey;
|
|
801
|
+
const rootProcessInstanceKey = presentEngineKey(filter?.rootProcessInstanceKey);
|
|
802
|
+
if (rootProcessInstanceKey) f.rootProcessInstanceKey = rootProcessInstanceKey;
|
|
771
803
|
// A process-instance search is an eventually consistent read; ask for zero-wait
|
|
772
804
|
// consistency so it reflects what is currently visible without blocking. Cap the page
|
|
773
805
|
// to the number of keys asked for (each key matches at most one instance), so a bounded
|
|
@@ -779,24 +811,32 @@ export class SdkEngineClient implements EngineClient {
|
|
|
779
811
|
);
|
|
780
812
|
const items = Array.isArray(body.items) ? body.items.filter(isRecord) : [];
|
|
781
813
|
return items.flatMap((it) => {
|
|
782
|
-
|
|
783
|
-
|
|
814
|
+
// Normalize the surfaced primary key through the shared presence rule (trim padded,
|
|
815
|
+
// drop blank/whitespace-only) so it matches the normalized parent/root linkage keys
|
|
816
|
+
// and the `searchUserTasks` mapping — the surfaced process-instance key must not drift
|
|
817
|
+
// between the two reduced-path searches (No Drift Surfaces).
|
|
818
|
+
const processInstanceKey = presentEngineKey(it.processInstanceKey);
|
|
819
|
+
if (!processInstanceKey) {
|
|
784
820
|
this.log("warn", "skipping process instance with no key in engine response");
|
|
785
821
|
return [];
|
|
786
822
|
}
|
|
787
823
|
const state = normalizeProcessInstanceState(it.state);
|
|
788
824
|
if (!state) {
|
|
789
825
|
this.log("warn", "skipping process instance with unrecognized state", {
|
|
790
|
-
processInstanceKey
|
|
826
|
+
processInstanceKey,
|
|
791
827
|
state: String(it.state),
|
|
792
828
|
});
|
|
793
829
|
return [];
|
|
794
830
|
}
|
|
795
831
|
const processDefinitionKey = presentEngineKey(it.processDefinitionKey);
|
|
832
|
+
const parentProcessInstanceKey = presentEngineKey(it.parentProcessInstanceKey);
|
|
833
|
+
const rootProcessInstanceKey = presentEngineKey(it.rootProcessInstanceKey);
|
|
796
834
|
return [{
|
|
797
|
-
processInstanceKey
|
|
835
|
+
processInstanceKey,
|
|
798
836
|
state,
|
|
799
837
|
...(processDefinitionKey ? { processDefinitionKey } : {}),
|
|
838
|
+
...(parentProcessInstanceKey ? { parentProcessInstanceKey } : {}),
|
|
839
|
+
...(rootProcessInstanceKey ? { rootProcessInstanceKey } : {}),
|
|
800
840
|
}];
|
|
801
841
|
});
|
|
802
842
|
}
|