@nanobpm/urban 0.88.1 → 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.
- package/dist/runtime/core/host.d.ts +149 -1
- package/dist/runtime/core/host.js +3 -0
- package/dist/runtime/core/modules/mcp.js +68 -0
- package/dist/runtime/engine/nanosdk.d.ts +32 -1
- package/dist/runtime/engine/nanosdk.js +210 -6
- package/dist/runtime/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/runtime/core/host.ts +155 -0
- package/src/runtime/core/modules/actions.test.ts +3 -0
- package/src/runtime/core/modules/agent.test.ts +3 -0
- package/src/runtime/core/modules/api.test.ts +3 -0
- package/src/runtime/core/modules/cancel.test.ts +3 -0
- package/src/runtime/core/modules/connectors.test.ts +3 -0
- package/src/runtime/core/modules/datasource.migrations.test.ts +3 -0
- package/src/runtime/core/modules/instance-tracking.test.ts +3 -0
- package/src/runtime/core/modules/mcp.test.ts +97 -0
- package/src/runtime/core/modules/mcp.ts +66 -0
- package/src/runtime/core/modules/pages.test.ts +3 -0
- package/src/runtime/core/modules/surfaces.test.ts +6 -0
- package/src/runtime/core/modules/triggers.test.ts +3 -0
- package/src/runtime/core/modules/workers.test.ts +3 -0
- package/src/runtime/core/runtime.extensions.test.ts +9 -0
- package/src/runtime/core/runtime.test.ts +9 -0
- package/src/runtime/engine/nanosdk.test.ts +136 -0
- package/src/runtime/engine/nanosdk.ts +239 -6
- package/src/runtime/index.ts +4 -0
|
@@ -97,6 +97,18 @@ function fakeSdkClient(overrides: Partial<NanoSdkClient> = {}): NanoSdkClient &
|
|
|
97
97
|
calls.push("searchIncidents");
|
|
98
98
|
return { items: [] };
|
|
99
99
|
},
|
|
100
|
+
async searchVariables() {
|
|
101
|
+
calls.push("searchVariables");
|
|
102
|
+
return { items: [] };
|
|
103
|
+
},
|
|
104
|
+
async searchJobs() {
|
|
105
|
+
calls.push("searchJobs");
|
|
106
|
+
return { items: [] };
|
|
107
|
+
},
|
|
108
|
+
async getProcessDefinitionXml() {
|
|
109
|
+
calls.push("getProcessDefinitionXml");
|
|
110
|
+
return "";
|
|
111
|
+
},
|
|
100
112
|
async resolveIncident(input) {
|
|
101
113
|
calls.push("resolveIncident");
|
|
102
114
|
return { ...input };
|
|
@@ -303,6 +315,72 @@ test("searchUserTasks surfaces the resolved form linkage", async () => {
|
|
|
303
315
|
]);
|
|
304
316
|
});
|
|
305
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
|
+
|
|
306
384
|
test("getForm resolves by formKey and parses the serialized schema", async () => {
|
|
307
385
|
let seen: unknown;
|
|
308
386
|
const schema = { type: "default", schemaVersion: 18, components: [{ type: "textfield", key: "name" }] };
|
|
@@ -474,6 +552,64 @@ test("searchProcessInstances skips keyless and unrecognized-state rows", async (
|
|
|
474
552
|
assert.deepEqual(out, [{ processInstanceKey: "10", state: "COMPLETED" }]);
|
|
475
553
|
});
|
|
476
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
|
+
|
|
477
613
|
test("normalizeProcessInstanceState maps the terminal set and rejects others", () => {
|
|
478
614
|
assert.equal(normalizeProcessInstanceState("ACTIVE"), "ACTIVE");
|
|
479
615
|
assert.equal(normalizeProcessInstanceState("completed"), "COMPLETED");
|
|
@@ -24,12 +24,16 @@ import type {
|
|
|
24
24
|
IncidentFilter,
|
|
25
25
|
IncidentState,
|
|
26
26
|
IncidentSummary,
|
|
27
|
+
JobFilter,
|
|
27
28
|
JobHandler,
|
|
29
|
+
JobSummary,
|
|
28
30
|
ProcessInstanceSnapshot,
|
|
29
31
|
ProcessInstanceState,
|
|
30
32
|
UserTaskState,
|
|
31
33
|
UserTaskSummary,
|
|
32
34
|
UserTaskFilter,
|
|
35
|
+
VariableFilter,
|
|
36
|
+
VariableSummary,
|
|
33
37
|
WorkerSubscription,
|
|
34
38
|
} from "../core/host.ts";
|
|
35
39
|
import { assertDeployedWaitStateType, isBpmnError } from "../core/host.ts";
|
|
@@ -360,6 +364,99 @@ export function mapIncidentRow(
|
|
|
360
364
|
};
|
|
361
365
|
}
|
|
362
366
|
|
|
367
|
+
/** Map one engine variable row onto a {@link VariableSummary}, or `undefined` when the row is
|
|
368
|
+
* malformed (missing the `variableKey`, owning `processInstanceKey`, or `name` — the identity a
|
|
369
|
+
* caller needs). `value` is the engine's serialized JSON value (a string); a non-string value is
|
|
370
|
+
* JSON-encoded so the field is always a string a caller can `JSON.parse`. `scopeKey` falls back to
|
|
371
|
+
* the process-instance key for a process-level variable that omits it. `isTruncated` is the
|
|
372
|
+
* engine's large-value-clipped flag, defaulting to `false`. The single source of truth
|
|
373
|
+
* `searchVariables` extracts through. */
|
|
374
|
+
export function mapVariableRow(
|
|
375
|
+
row: Record<string, unknown>,
|
|
376
|
+
log: Log,
|
|
377
|
+
): VariableSummary | undefined {
|
|
378
|
+
const variableKey = presentEngineKey(row.variableKey);
|
|
379
|
+
if (variableKey === undefined) {
|
|
380
|
+
log("warn", "skipping variable with no variableKey in engine response");
|
|
381
|
+
return undefined;
|
|
382
|
+
}
|
|
383
|
+
const processInstanceKey = presentEngineKey(row.processInstanceKey);
|
|
384
|
+
if (processInstanceKey === undefined) {
|
|
385
|
+
log("warn", "skipping variable with no processInstanceKey in engine response", {
|
|
386
|
+
variableKey,
|
|
387
|
+
});
|
|
388
|
+
return undefined;
|
|
389
|
+
}
|
|
390
|
+
const name = presentText(row.name);
|
|
391
|
+
if (name === undefined) {
|
|
392
|
+
log("warn", "skipping variable with no name in engine response", { variableKey });
|
|
393
|
+
return undefined;
|
|
394
|
+
}
|
|
395
|
+
// A process-level variable is scoped to the process instance itself; the engine may omit
|
|
396
|
+
// `scopeKey` for it, so fall back to the process-instance key rather than dropping the row.
|
|
397
|
+
const scopeKey = presentEngineKey(row.scopeKey) ?? processInstanceKey;
|
|
398
|
+
// The engine serializes `value` as a JSON string. Keep a string as-is; encode any other type
|
|
399
|
+
// (an object/array/number a permissive engine might inline) so the field is always a string the
|
|
400
|
+
// caller can `JSON.parse`.
|
|
401
|
+
const value = typeof row.value === "string" ? row.value : JSON.stringify(row.value ?? null);
|
|
402
|
+
return {
|
|
403
|
+
variableKey,
|
|
404
|
+
name,
|
|
405
|
+
value,
|
|
406
|
+
scopeKey,
|
|
407
|
+
processInstanceKey,
|
|
408
|
+
isTruncated: row.isTruncated === true,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** Map one engine job row onto a {@link JobSummary}, or `undefined` when the row is malformed
|
|
413
|
+
* (missing the `jobKey`, `type`, or owning `processInstanceKey`). `worker`/`retries`/`elementId`/
|
|
414
|
+
* `deadline` are best-effort diagnostics: present when the engine reports them, omitted otherwise
|
|
415
|
+
* — an unset `worker` is the "queued, not leased" signal, so it must be absent rather than `""`.
|
|
416
|
+
* `state` is passed through as a bare string (the engine's job-state enum is broad). The single
|
|
417
|
+
* source of truth `searchJobs` extracts through. */
|
|
418
|
+
export function mapJobRow(
|
|
419
|
+
row: Record<string, unknown>,
|
|
420
|
+
log: Log,
|
|
421
|
+
): JobSummary | undefined {
|
|
422
|
+
const jobKey = presentEngineKey(row.jobKey);
|
|
423
|
+
if (jobKey === undefined) {
|
|
424
|
+
log("warn", "skipping job with no jobKey in engine response");
|
|
425
|
+
return undefined;
|
|
426
|
+
}
|
|
427
|
+
const type = presentText(row.type);
|
|
428
|
+
if (type === undefined) {
|
|
429
|
+
log("warn", "skipping job with no type in engine response", { jobKey });
|
|
430
|
+
return undefined;
|
|
431
|
+
}
|
|
432
|
+
const processInstanceKey = presentEngineKey(row.processInstanceKey);
|
|
433
|
+
if (processInstanceKey === undefined) {
|
|
434
|
+
log("warn", "skipping job with no processInstanceKey in engine response", { jobKey });
|
|
435
|
+
return undefined;
|
|
436
|
+
}
|
|
437
|
+
const state = presentText(row.state);
|
|
438
|
+
if (state === undefined) {
|
|
439
|
+
log("warn", "skipping job with no state in engine response", { jobKey });
|
|
440
|
+
return undefined;
|
|
441
|
+
}
|
|
442
|
+
const worker = presentText(row.worker);
|
|
443
|
+
const retries = typeof row.retries === "number" && Number.isFinite(row.retries)
|
|
444
|
+
? row.retries
|
|
445
|
+
: undefined;
|
|
446
|
+
const elementId = presentText(row.elementId);
|
|
447
|
+
const deadline = presentText(row.deadline);
|
|
448
|
+
return {
|
|
449
|
+
jobKey,
|
|
450
|
+
type,
|
|
451
|
+
state,
|
|
452
|
+
processInstanceKey,
|
|
453
|
+
...(worker ? { worker } : {}),
|
|
454
|
+
...(retries !== undefined ? { retries } : {}),
|
|
455
|
+
...(elementId ? { elementId } : {}),
|
|
456
|
+
...(deadline ? { deadline } : {}),
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
363
460
|
/** A job as delivered to a nano-sdk job handler: the frame fields plus the
|
|
364
461
|
* acknowledgement actions the handler must call. */
|
|
365
462
|
export interface NanoSdkActivatedJob {
|
|
@@ -463,6 +560,21 @@ export interface NanoSdkClient {
|
|
|
463
560
|
consistency?: unknown,
|
|
464
561
|
options?: unknown,
|
|
465
562
|
): Promise<Record<string, unknown>>;
|
|
563
|
+
searchVariables(
|
|
564
|
+
input: { filter?: Record<string, unknown>; page?: Record<string, unknown> },
|
|
565
|
+
consistency?: unknown,
|
|
566
|
+
options?: unknown,
|
|
567
|
+
): Promise<Record<string, unknown>>;
|
|
568
|
+
searchJobs(
|
|
569
|
+
input: { filter?: Record<string, unknown>; page?: Record<string, unknown> },
|
|
570
|
+
consistency?: unknown,
|
|
571
|
+
options?: unknown,
|
|
572
|
+
): Promise<Record<string, unknown>>;
|
|
573
|
+
getProcessDefinitionXml(
|
|
574
|
+
input: { processDefinitionKey: string },
|
|
575
|
+
consistency?: unknown,
|
|
576
|
+
options?: unknown,
|
|
577
|
+
): Promise<unknown>;
|
|
466
578
|
resolveIncident(
|
|
467
579
|
input: { incidentKey: string },
|
|
468
580
|
options?: unknown,
|
|
@@ -565,8 +677,19 @@ export class SdkEngineClient implements EngineClient {
|
|
|
565
677
|
}): Promise<UserTaskSummary[]> {
|
|
566
678
|
// User tasks are an eventually consistent read; ask for zero-wait consistency so
|
|
567
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
|
+
}
|
|
568
691
|
const body = await this.client.searchUserTasks(
|
|
569
|
-
{ filter:
|
|
692
|
+
{ filter: f },
|
|
570
693
|
{ consistency: { waitUpToMs: 0 } },
|
|
571
694
|
);
|
|
572
695
|
const items = Array.isArray(body.items) ? body.items.filter(isRecord) : [];
|
|
@@ -579,10 +702,16 @@ export class SdkEngineClient implements EngineClient {
|
|
|
579
702
|
// The engine resolves a `formId="X"` linkage to the latest deployed form's key at
|
|
580
703
|
// task creation, so `formKey` (not a form id) is the linkage it reports on the task;
|
|
581
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);
|
|
582
708
|
return [{
|
|
583
709
|
userTaskKey: String(userTaskKey),
|
|
584
710
|
elementId: typeof it.elementId === "string" ? it.elementId : undefined,
|
|
585
711
|
variables: isRecord(it.variables) ? it.variables : undefined,
|
|
712
|
+
...(processInstanceKey ? { processInstanceKey } : {}),
|
|
713
|
+
...(parentProcessInstanceKey ? { parentProcessInstanceKey } : {}),
|
|
714
|
+
...(rootProcessInstanceKey ? { rootProcessInstanceKey } : {}),
|
|
586
715
|
...pickFormLinkage(it),
|
|
587
716
|
}];
|
|
588
717
|
});
|
|
@@ -651,11 +780,26 @@ export class SdkEngineClient implements EngineClient {
|
|
|
651
780
|
async searchProcessInstances(filter?: {
|
|
652
781
|
processInstanceKeys?: string[];
|
|
653
782
|
state?: ProcessInstanceState;
|
|
783
|
+
parentProcessInstanceKey?: string;
|
|
784
|
+
rootProcessInstanceKey?: string;
|
|
654
785
|
}): Promise<ProcessInstanceSnapshot[]> {
|
|
655
786
|
const f: Record<string, unknown> = {};
|
|
656
787
|
if (filter?.state) f.state = filter.state;
|
|
657
|
-
|
|
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);
|
|
658
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;
|
|
659
803
|
// A process-instance search is an eventually consistent read; ask for zero-wait
|
|
660
804
|
// consistency so it reflects what is currently visible without blocking. Cap the page
|
|
661
805
|
// to the number of keys asked for (each key matches at most one instance), so a bounded
|
|
@@ -667,20 +811,33 @@ export class SdkEngineClient implements EngineClient {
|
|
|
667
811
|
);
|
|
668
812
|
const items = Array.isArray(body.items) ? body.items.filter(isRecord) : [];
|
|
669
813
|
return items.flatMap((it) => {
|
|
670
|
-
|
|
671
|
-
|
|
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) {
|
|
672
820
|
this.log("warn", "skipping process instance with no key in engine response");
|
|
673
821
|
return [];
|
|
674
822
|
}
|
|
675
823
|
const state = normalizeProcessInstanceState(it.state);
|
|
676
824
|
if (!state) {
|
|
677
825
|
this.log("warn", "skipping process instance with unrecognized state", {
|
|
678
|
-
processInstanceKey
|
|
826
|
+
processInstanceKey,
|
|
679
827
|
state: String(it.state),
|
|
680
828
|
});
|
|
681
829
|
return [];
|
|
682
830
|
}
|
|
683
|
-
|
|
831
|
+
const processDefinitionKey = presentEngineKey(it.processDefinitionKey);
|
|
832
|
+
const parentProcessInstanceKey = presentEngineKey(it.parentProcessInstanceKey);
|
|
833
|
+
const rootProcessInstanceKey = presentEngineKey(it.rootProcessInstanceKey);
|
|
834
|
+
return [{
|
|
835
|
+
processInstanceKey,
|
|
836
|
+
state,
|
|
837
|
+
...(processDefinitionKey ? { processDefinitionKey } : {}),
|
|
838
|
+
...(parentProcessInstanceKey ? { parentProcessInstanceKey } : {}),
|
|
839
|
+
...(rootProcessInstanceKey ? { rootProcessInstanceKey } : {}),
|
|
840
|
+
}];
|
|
684
841
|
});
|
|
685
842
|
}
|
|
686
843
|
|
|
@@ -784,6 +941,82 @@ export class SdkEngineClient implements EngineClient {
|
|
|
784
941
|
});
|
|
785
942
|
}
|
|
786
943
|
|
|
944
|
+
async searchVariables(filter?: VariableFilter): Promise<VariableSummary[]> {
|
|
945
|
+
// Same shape as `searchIncidents`: build the engine filter from the transport-agnostic
|
|
946
|
+
// selectors, read at zero-wait consistency (a variable search is eventually consistent), and
|
|
947
|
+
// map each row through the single `mapVariableRow` gate, dropping malformed rows.
|
|
948
|
+
const f: Record<string, unknown> = {};
|
|
949
|
+
const processInstanceKey = presentEngineKey(filter?.processInstanceKey);
|
|
950
|
+
if (processInstanceKey) f.processInstanceKey = processInstanceKey;
|
|
951
|
+
const scopeKey = presentEngineKey(filter?.scopeKey);
|
|
952
|
+
if (scopeKey) f.scopeKey = scopeKey;
|
|
953
|
+
const name = filter?.name?.trim();
|
|
954
|
+
if (name) f.name = name;
|
|
955
|
+
const body = await this.client.searchVariables(
|
|
956
|
+
{ filter: f },
|
|
957
|
+
{ consistency: { waitUpToMs: 0 } },
|
|
958
|
+
);
|
|
959
|
+
const items = Array.isArray(body.items) ? body.items.filter(isRecord) : [];
|
|
960
|
+
return items.flatMap((it) => {
|
|
961
|
+
const mapped = mapVariableRow(it, this.log);
|
|
962
|
+
return mapped ? [mapped] : [];
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
async searchJobs(filter?: JobFilter): Promise<JobSummary[]> {
|
|
967
|
+
// Same shape as `searchIncidents`: build the engine filter from the transport-agnostic
|
|
968
|
+
// selectors, read at zero-wait consistency (a job search is eventually consistent), and map
|
|
969
|
+
// each row through the single `mapJobRow` gate, dropping malformed rows.
|
|
970
|
+
const f: Record<string, unknown> = {};
|
|
971
|
+
const processInstanceKey = presentEngineKey(filter?.processInstanceKey);
|
|
972
|
+
if (processInstanceKey) f.processInstanceKey = processInstanceKey;
|
|
973
|
+
const state = filter?.state?.trim();
|
|
974
|
+
if (state) f.state = state;
|
|
975
|
+
const type = filter?.type?.trim();
|
|
976
|
+
if (type) f.type = type;
|
|
977
|
+
const elementId = filter?.elementId?.trim();
|
|
978
|
+
if (elementId) f.elementId = elementId;
|
|
979
|
+
const worker = filter?.worker?.trim();
|
|
980
|
+
if (worker) f.worker = worker;
|
|
981
|
+
const body = await this.client.searchJobs(
|
|
982
|
+
{ filter: f },
|
|
983
|
+
{ consistency: { waitUpToMs: 0 } },
|
|
984
|
+
);
|
|
985
|
+
const items = Array.isArray(body.items) ? body.items.filter(isRecord) : [];
|
|
986
|
+
return items.flatMap((it) => {
|
|
987
|
+
const mapped = mapJobRow(it, this.log);
|
|
988
|
+
return mapped ? [mapped] : [];
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
async getProcessDefinitionXml(
|
|
993
|
+
processDefinitionKey: string,
|
|
994
|
+
): Promise<string | null> {
|
|
995
|
+
// A blank key can never address a definition — short-circuit to null rather than issue a
|
|
996
|
+
// `GET /v2/process-definitions//xml` with an empty segment. Normalize a padded-but-valid key so
|
|
997
|
+
// `" 5 "` addresses the same definition, mirroring `getElementInstance`.
|
|
998
|
+
const key = presentEngineKey(processDefinitionKey);
|
|
999
|
+
if (key === undefined) return null;
|
|
1000
|
+
let xml: unknown;
|
|
1001
|
+
try {
|
|
1002
|
+
xml = await this.client.getProcessDefinitionXml(
|
|
1003
|
+
{ processDefinitionKey: key },
|
|
1004
|
+
{ consistency: { waitUpToMs: 0 } },
|
|
1005
|
+
);
|
|
1006
|
+
} catch (err) {
|
|
1007
|
+
// A 404 (no such definition) is an expected "not found", not a fault — mirror `getForm`/
|
|
1008
|
+
// `getElementInstance`, which treat a failed fetch as absence (null) rather than propagating.
|
|
1009
|
+
this.log("warn", "getProcessDefinitionXml: engine fetch failed", {
|
|
1010
|
+
processDefinitionKey: key,
|
|
1011
|
+
err: err instanceof Error ? err.message : String(err),
|
|
1012
|
+
});
|
|
1013
|
+
return null;
|
|
1014
|
+
}
|
|
1015
|
+
// The endpoint returns the raw BPMN XML as a string (204 → empty). Treat a blank/absent body
|
|
1016
|
+
// as "no XML" (null) so a caller distinguishes "have it" from "don't".
|
|
1017
|
+
return typeof xml === "string" && xml.trim() !== "" ? xml : null;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
787
1020
|
async resolveIncident(input: { incidentKey: string }): Promise<void> {
|
|
788
1021
|
await this.client.resolveIncident({
|
|
789
1022
|
incidentKey: requireEngineKey(input.incidentKey, "incidentKey"),
|
package/src/runtime/index.ts
CHANGED
|
@@ -100,13 +100,17 @@ export type {
|
|
|
100
100
|
EngineIncidentState,
|
|
101
101
|
IncidentFilter,
|
|
102
102
|
IncidentSummary,
|
|
103
|
+
JobFilter,
|
|
103
104
|
JobHandler,
|
|
105
|
+
JobSummary,
|
|
104
106
|
ProcessInstanceSnapshot,
|
|
105
107
|
ProcessInstanceState,
|
|
106
108
|
SqliteDb,
|
|
107
109
|
UserTaskState,
|
|
108
110
|
UserTaskSummary,
|
|
109
111
|
UserTaskFilter,
|
|
112
|
+
VariableFilter,
|
|
113
|
+
VariableSummary,
|
|
110
114
|
WatchHandle,
|
|
111
115
|
WorkerSubscription,
|
|
112
116
|
} from "./core/host.ts";
|