@nanobpm/urban 0.49.0 → 0.49.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/runtime/core/host.d.ts +13 -2
- package/dist/runtime/core/modules/surfaces.js +8 -1
- package/dist/runtime/engine/nanosdk.d.ts +2 -1
- package/dist/runtime/engine/nanosdk.js +7 -1
- package/package.json +1 -1
- package/src/runtime/core/host.ts +14 -2
- package/src/runtime/core/modules/surfaces.test.ts +17 -1
- package/src/runtime/core/modules/surfaces.ts +8 -1
- package/src/runtime/engine/nanosdk.test.ts +8 -1
- package/src/runtime/engine/nanosdk.ts +8 -1
|
@@ -187,6 +187,13 @@ export interface WorkerSubscription {
|
|
|
187
187
|
* element-instance state set (which adds SKIPPED/CANCELED/FAILED for tokens).
|
|
188
188
|
*/
|
|
189
189
|
export type ProcessInstanceState = "ACTIVE" | "COMPLETED" | "TERMINATED";
|
|
190
|
+
/**
|
|
191
|
+
* A user task's lifecycle state, as the engine reports it on a `/v2/user-tasks/search`
|
|
192
|
+
* result. `CREATED` is the only *open* (answerable) state — a task in any other state has
|
|
193
|
+
* already been answered (`COMPLETED`), withdrawn (`CANCELED`), or errored (`FAILED`) and must
|
|
194
|
+
* never surface in the `taskInbox` as if it were actionable.
|
|
195
|
+
*/
|
|
196
|
+
export type UserTaskState = "CREATED" | "COMPLETED" | "CANCELED" | "FAILED";
|
|
190
197
|
/** A single process instance's lifecycle snapshot returned by {@link EngineClient.searchProcessInstances}. */
|
|
191
198
|
export interface ProcessInstanceSnapshot {
|
|
192
199
|
readonly processInstanceKey: string;
|
|
@@ -253,12 +260,16 @@ export interface EngineClient {
|
|
|
253
260
|
correlationKey?: string;
|
|
254
261
|
variables?: Record<string, unknown>;
|
|
255
262
|
}): Promise<void>;
|
|
256
|
-
/** Search
|
|
257
|
-
*
|
|
263
|
+
/** Search user tasks (optionally by process instance, assignee, or candidate group).
|
|
264
|
+
* By default this is unfiltered by lifecycle state and may return tasks in any state
|
|
265
|
+
* (e.g. completed/canceled); pass `state: "CREATED"` to constrain the search to open
|
|
266
|
+
* (answerable) tasks. Each result carries the task's resolved form linkage
|
|
267
|
+
* (`formKey`/`externalFormReference`) when present. */
|
|
258
268
|
searchUserTasks(filter?: {
|
|
259
269
|
processInstanceKey?: string;
|
|
260
270
|
assignee?: string;
|
|
261
271
|
candidateGroup?: string;
|
|
272
|
+
state?: UserTaskState;
|
|
262
273
|
}): Promise<UserTaskSummary[]>;
|
|
263
274
|
/**
|
|
264
275
|
* Fetch a deployed form's form-js schema for the `taskInbox` surface. Resolve by
|
|
@@ -201,7 +201,14 @@ export function mountSurfaces(ctx, app) {
|
|
|
201
201
|
source: "surface:taskInbox",
|
|
202
202
|
handler: async (req) => {
|
|
203
203
|
const pik = req.query.get("processInstanceKey") ?? undefined;
|
|
204
|
-
|
|
204
|
+
// Constrain the inbox to open (answerable) tasks. Without a state filter the engine
|
|
205
|
+
// returns tasks in every state (CREATED/COMPLETED/CANCELED/...), so already-answered
|
|
206
|
+
// or withdrawn tasks would surface as if actionable — completing one then fails with
|
|
207
|
+
// "User task ... is not active". See nanobpm/nano-ide#248.
|
|
208
|
+
const tasks = await app.engine.searchUserTasks({
|
|
209
|
+
state: "CREATED",
|
|
210
|
+
...(pik ? { processInstanceKey: pik } : {}),
|
|
211
|
+
});
|
|
205
212
|
return json(tasks);
|
|
206
213
|
},
|
|
207
214
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EngineClient, FormSchema, JobHandler, ProcessInstanceSnapshot, ProcessInstanceState, UserTaskSummary, WorkerSubscription } from "../core/host.ts";
|
|
1
|
+
import type { EngineClient, FormSchema, JobHandler, ProcessInstanceSnapshot, ProcessInstanceState, UserTaskState, UserTaskSummary, WorkerSubscription } from "../core/host.ts";
|
|
2
2
|
import type { EngineSdkClient } from "./sdk.ts";
|
|
3
3
|
/** Coerce an engine response's process-instance key to a non-empty string, or
|
|
4
4
|
* throw — a missing key means a malformed/partial response, not a real instance. */
|
|
@@ -140,6 +140,7 @@ export declare class SdkEngineClient implements EngineClient {
|
|
|
140
140
|
processInstanceKey?: string;
|
|
141
141
|
assignee?: string;
|
|
142
142
|
candidateGroup?: string;
|
|
143
|
+
state?: UserTaskState;
|
|
143
144
|
}): Promise<UserTaskSummary[]>;
|
|
144
145
|
getForm(input: {
|
|
145
146
|
formKey?: string;
|
|
@@ -101,7 +101,13 @@ export class SdkEngineClient {
|
|
|
101
101
|
async searchUserTasks(filter) {
|
|
102
102
|
// User tasks are an eventually consistent read; ask for zero-wait consistency so
|
|
103
103
|
// the search reflects what is currently visible without blocking.
|
|
104
|
-
const body = await this.client.searchUserTasks(
|
|
104
|
+
const body = await this.client.searchUserTasks(
|
|
105
|
+
// This method is contracted to return *open* tasks; constrain the read to
|
|
106
|
+
// CREATED so answered/canceled tasks never surface as answerable (e.g. a
|
|
107
|
+
// re-escalated instance whose prior escalation task is already COMPLETED).
|
|
108
|
+
// Spread caller filters first and pin `state` last so a caller can never
|
|
109
|
+
// override the open-only (CREATED) constraint this method contracts to.
|
|
110
|
+
{ filter: { ...(filter ?? {}), state: "CREATED" } }, { consistency: { waitUpToMs: 0 } });
|
|
105
111
|
const items = Array.isArray(body.items) ? body.items.filter(isRecord) : [];
|
|
106
112
|
return items.flatMap((it) => {
|
|
107
113
|
const userTaskKey = it.userTaskKey ?? it.key;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/urban",
|
|
3
|
-
"version": "0.49.
|
|
3
|
+
"version": "0.49.1",
|
|
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
|
@@ -217,6 +217,14 @@ export interface WorkerSubscription {
|
|
|
217
217
|
*/
|
|
218
218
|
export type ProcessInstanceState = "ACTIVE" | "COMPLETED" | "TERMINATED";
|
|
219
219
|
|
|
220
|
+
/**
|
|
221
|
+
* A user task's lifecycle state, as the engine reports it on a `/v2/user-tasks/search`
|
|
222
|
+
* result. `CREATED` is the only *open* (answerable) state — a task in any other state has
|
|
223
|
+
* already been answered (`COMPLETED`), withdrawn (`CANCELED`), or errored (`FAILED`) and must
|
|
224
|
+
* never surface in the `taskInbox` as if it were actionable.
|
|
225
|
+
*/
|
|
226
|
+
export type UserTaskState = "CREATED" | "COMPLETED" | "CANCELED" | "FAILED";
|
|
227
|
+
|
|
220
228
|
/** A single process instance's lifecycle snapshot returned by {@link EngineClient.searchProcessInstances}. */
|
|
221
229
|
export interface ProcessInstanceSnapshot {
|
|
222
230
|
readonly processInstanceKey: string;
|
|
@@ -277,12 +285,16 @@ export interface EngineClient {
|
|
|
277
285
|
correlationKey?: string;
|
|
278
286
|
variables?: Record<string, unknown>;
|
|
279
287
|
}): Promise<void>;
|
|
280
|
-
/** Search
|
|
281
|
-
*
|
|
288
|
+
/** Search user tasks (optionally by process instance, assignee, or candidate group).
|
|
289
|
+
* By default this is unfiltered by lifecycle state and may return tasks in any state
|
|
290
|
+
* (e.g. completed/canceled); pass `state: "CREATED"` to constrain the search to open
|
|
291
|
+
* (answerable) tasks. Each result carries the task's resolved form linkage
|
|
292
|
+
* (`formKey`/`externalFormReference`) when present. */
|
|
282
293
|
searchUserTasks(filter?: {
|
|
283
294
|
processInstanceKey?: string;
|
|
284
295
|
assignee?: string;
|
|
285
296
|
candidateGroup?: string;
|
|
297
|
+
state?: UserTaskState;
|
|
286
298
|
}): Promise<UserTaskSummary[]>;
|
|
287
299
|
/**
|
|
288
300
|
* Fetch a deployed form's form-js schema for the `taskInbox` surface. Resolve by
|
|
@@ -171,12 +171,28 @@ test("/api/tasks surfaces the resolved form linkage", async () => {
|
|
|
171
171
|
};
|
|
172
172
|
const { tasks } = inboxRoutes(engine);
|
|
173
173
|
const res = await call(tasks, { query: "processInstanceKey=pi-9" });
|
|
174
|
-
assert.deepEqual(seenFilter, { processInstanceKey: "pi-9" });
|
|
174
|
+
assert.deepEqual(seenFilter, { state: "CREATED", processInstanceKey: "pi-9" });
|
|
175
175
|
const parsed = JSON.parse(String(res.body));
|
|
176
176
|
assert.equal(parsed[0].formKey, "form-123");
|
|
177
177
|
assert.equal(parsed[1].formKey, undefined);
|
|
178
178
|
});
|
|
179
179
|
|
|
180
|
+
test("/api/tasks constrains the search to open (CREATED) tasks", async () => {
|
|
181
|
+
let seenFilter: unknown;
|
|
182
|
+
const engine: EngineClient = {
|
|
183
|
+
...fakeEngine,
|
|
184
|
+
searchUserTasks: async (filter) => {
|
|
185
|
+
seenFilter = filter;
|
|
186
|
+
return [];
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
const { tasks } = inboxRoutes(engine);
|
|
190
|
+
await call(tasks);
|
|
191
|
+
// No processInstanceKey supplied, but the inbox must still pin state=CREATED so that
|
|
192
|
+
// already-answered/canceled tasks never surface as answerable (nanobpm/nano-ide#248).
|
|
193
|
+
assert.deepEqual(seenFilter, { state: "CREATED" });
|
|
194
|
+
});
|
|
195
|
+
|
|
180
196
|
test("/api/form resolves a linked form's schema", async () => {
|
|
181
197
|
let seen: unknown;
|
|
182
198
|
const schema = { type: "default", schemaVersion: 18, components: [{ type: "textfield", key: "name", label: "Name" }] };
|
|
@@ -215,7 +215,14 @@ export function mountSurfaces(ctx: RuntimeContext, app: AppApi): SurfacesHandle
|
|
|
215
215
|
source: "surface:taskInbox",
|
|
216
216
|
handler: async (req) => {
|
|
217
217
|
const pik = req.query.get("processInstanceKey") ?? undefined;
|
|
218
|
-
|
|
218
|
+
// Constrain the inbox to open (answerable) tasks. Without a state filter the engine
|
|
219
|
+
// returns tasks in every state (CREATED/COMPLETED/CANCELED/...), so already-answered
|
|
220
|
+
// or withdrawn tasks would surface as if actionable — completing one then fails with
|
|
221
|
+
// "User task ... is not active". See nanobpm/nano-ide#248.
|
|
222
|
+
const tasks = await app.engine.searchUserTasks({
|
|
223
|
+
state: "CREATED",
|
|
224
|
+
...(pik ? { processInstanceKey: pik } : {}),
|
|
225
|
+
});
|
|
219
226
|
return json(tasks);
|
|
220
227
|
},
|
|
221
228
|
});
|
|
@@ -177,8 +177,10 @@ test("publishMessage defaults correlationKey/variables", async () => {
|
|
|
177
177
|
|
|
178
178
|
test("searchUserTasks passes zero-wait consistency and maps items", async () => {
|
|
179
179
|
let consistency: unknown;
|
|
180
|
+
let input: { filter?: Record<string, unknown> } | undefined;
|
|
180
181
|
const client = fakeSdkClient({
|
|
181
|
-
searchUserTasks: async (
|
|
182
|
+
searchUserTasks: async (i, c) => {
|
|
183
|
+
input = i;
|
|
182
184
|
consistency = c;
|
|
183
185
|
return {
|
|
184
186
|
items: [
|
|
@@ -192,6 +194,11 @@ test("searchUserTasks passes zero-wait consistency and maps items", async () =>
|
|
|
192
194
|
const engine = new SdkEngineClient(client);
|
|
193
195
|
const tasks = await engine.searchUserTasks({ processInstanceKey: "pi" });
|
|
194
196
|
assert.deepEqual(consistency, { consistency: { waitUpToMs: 0 } });
|
|
197
|
+
// The search is constrained to open (CREATED) tasks so terminal tasks never
|
|
198
|
+
// surface as answerable, while caller filters (processInstanceKey) still apply.
|
|
199
|
+
// Assert individual fields so the check is independent of key insertion order.
|
|
200
|
+
assert.equal(input?.filter?.state, "CREATED");
|
|
201
|
+
assert.equal(input?.filter?.processInstanceKey, "pi");
|
|
195
202
|
assert.deepEqual(tasks, [
|
|
196
203
|
{ userTaskKey: "7", elementId: "task_a", variables: { x: 1 } },
|
|
197
204
|
{ userTaskKey: "8", elementId: undefined, variables: undefined },
|
|
@@ -18,6 +18,7 @@ import type {
|
|
|
18
18
|
JobHandler,
|
|
19
19
|
ProcessInstanceSnapshot,
|
|
20
20
|
ProcessInstanceState,
|
|
21
|
+
UserTaskState,
|
|
21
22
|
UserTaskSummary,
|
|
22
23
|
WorkerSubscription,
|
|
23
24
|
} from "../core/host.ts";
|
|
@@ -221,11 +222,17 @@ export class SdkEngineClient implements EngineClient {
|
|
|
221
222
|
processInstanceKey?: string;
|
|
222
223
|
assignee?: string;
|
|
223
224
|
candidateGroup?: string;
|
|
225
|
+
state?: UserTaskState;
|
|
224
226
|
}): Promise<UserTaskSummary[]> {
|
|
225
227
|
// User tasks are an eventually consistent read; ask for zero-wait consistency so
|
|
226
228
|
// the search reflects what is currently visible without blocking.
|
|
227
229
|
const body = await this.client.searchUserTasks(
|
|
228
|
-
|
|
230
|
+
// This method is contracted to return *open* tasks; constrain the read to
|
|
231
|
+
// CREATED so answered/canceled tasks never surface as answerable (e.g. a
|
|
232
|
+
// re-escalated instance whose prior escalation task is already COMPLETED).
|
|
233
|
+
// Spread caller filters first and pin `state` last so a caller can never
|
|
234
|
+
// override the open-only (CREATED) constraint this method contracts to.
|
|
235
|
+
{ filter: { ...(filter ?? {}), state: "CREATED" } },
|
|
229
236
|
{ consistency: { waitUpToMs: 0 } },
|
|
230
237
|
);
|
|
231
238
|
const items = Array.isArray(body.items) ? body.items.filter(isRecord) : [];
|