@nanobpm/urban 0.49.1 → 0.51.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.
@@ -22,7 +22,13 @@ import type {
22
22
  UserTaskSummary,
23
23
  WorkerSubscription,
24
24
  } from "../core/host.ts";
25
- import { isBpmnError, presentFormIdentifier } from "../core/host.ts";
25
+ import { isBpmnError } from "../core/host.ts";
26
+ import {
27
+ buildFormSchema,
28
+ parseFormSchema,
29
+ pickFormLinkage,
30
+ resolveFormIdentifier,
31
+ } from "../core/form-contract.ts";
26
32
  import type { EngineSdkClient } from "./sdk.ts";
27
33
  import { isRecord } from "../core/guards.ts";
28
34
 
@@ -227,12 +233,7 @@ export class SdkEngineClient implements EngineClient {
227
233
  // User tasks are an eventually consistent read; ask for zero-wait consistency so
228
234
  // the search reflects what is currently visible without blocking.
229
235
  const body = await this.client.searchUserTasks(
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" } },
236
+ { filter: { ...(filter ?? {}) } },
236
237
  { consistency: { waitUpToMs: 0 } },
237
238
  );
238
239
  const items = Array.isArray(body.items) ? body.items.filter(isRecord) : [];
@@ -243,19 +244,13 @@ export class SdkEngineClient implements EngineClient {
243
244
  return [];
244
245
  }
245
246
  // The engine resolves a `formId="X"` linkage to the latest deployed form's key at
246
- // task creation, so `formKey` (not a form id) is the linkage it reports on the task.
247
- const formKey =
248
- it.formKey != null && it.formKey !== "" ? String(it.formKey) : undefined;
249
- const externalFormReference =
250
- typeof it.externalFormReference === "string" && it.externalFormReference !== ""
251
- ? it.externalFormReference
252
- : undefined;
247
+ // task creation, so `formKey` (not a form id) is the linkage it reports on the task;
248
+ // no `resolveFormKeyByFormId` resolver is passed because the gateway already resolved it.
253
249
  return [{
254
250
  userTaskKey: String(userTaskKey),
255
251
  elementId: typeof it.elementId === "string" ? it.elementId : undefined,
256
252
  variables: isRecord(it.variables) ? it.variables : undefined,
257
- ...(formKey ? { formKey } : {}),
258
- ...(externalFormReference ? { externalFormReference } : {}),
253
+ ...pickFormLinkage(it),
259
254
  }];
260
255
  });
261
256
  }
@@ -268,12 +263,14 @@ export class SdkEngineClient implements EngineClient {
268
263
  // form or 404s (→ null) when there is no such form.
269
264
  //
270
265
  // This is the single gate that resolves which identifier addresses the form: an
271
- // empty/whitespace identifier is treated as *absent* (via `presentFormIdentifier`, the
272
- // shared presence rule) so a blank `formKey` (e.g. a `?formKey=` query param) falls
266
+ // empty/whitespace identifier is treated as *absent* (via the shared presence rule in
267
+ // `resolveFormIdentifier`) so a blank `formKey` (e.g. a `?formKey=` query param) falls
273
268
  // through to a valid `formId` instead of being taken as a present-but-unresolvable key
274
- // that short-circuits to null.
275
- const key = presentFormIdentifier(input.formKey) ?? presentFormIdentifier(input.formId);
276
- if (key == null) return null;
269
+ // that short-circuits to null. The REST gateway addresses a form by `value` regardless
270
+ // of whether it is a deploy key or an authored id, so `kind` is not consulted here.
271
+ const resolved = resolveFormIdentifier(input);
272
+ if (resolved == null) return null;
273
+ const key = resolved.value;
277
274
  let body: Record<string, unknown>;
278
275
  try {
279
276
  body = await this.client.getFormByKey(
@@ -290,27 +287,21 @@ export class SdkEngineClient implements EngineClient {
290
287
  return null;
291
288
  }
292
289
  if (!isRecord(body)) return null;
293
- const rawSchema = body.schema;
294
290
  // The engine serializes the form-js schema as a JSON string; parse it to the object
295
291
  // the surface renders. Tolerate an already-parsed object (an in-memory/embedded engine).
296
- let schema: Record<string, unknown> | undefined;
297
- if (typeof rawSchema === "string") {
298
- try {
299
- const parsed: unknown = JSON.parse(rawSchema);
300
- if (isRecord(parsed)) schema = parsed;
301
- } catch {
292
+ const schema = parseFormSchema(body.schema);
293
+ if (!schema) {
294
+ if (typeof body.schema === "string") {
302
295
  this.log("warn", "getForm: form schema is not valid JSON", { key, formKey: input.formKey, formId: input.formId });
303
296
  }
304
- } else if (isRecord(rawSchema)) {
305
- schema = rawSchema;
297
+ return null;
306
298
  }
307
- if (!schema) return null;
308
- return {
299
+ return buildFormSchema({
309
300
  schema,
310
- ...(body.formKey != null && body.formKey !== "" ? { formKey: String(body.formKey) } : {}),
311
- ...(typeof body.formId === "string" && body.formId !== "" ? { formId: body.formId } : {}),
312
- ...(typeof body.version === "number" ? { version: body.version } : {}),
313
- };
301
+ formKey: body.formKey,
302
+ formId: body.formId,
303
+ version: body.version,
304
+ });
314
305
  }
315
306
 
316
307
  async completeUserTask(userTaskKey: string, variables?: Record<string, unknown>): Promise<void> {
@@ -52,16 +52,31 @@ export type { ValidationIssue } from "./core/validate.ts";
52
52
  export type {
53
53
  EngineClient,
54
54
  EngineJob,
55
+ FormSchema,
55
56
  HostContext,
56
57
  HttpHandler,
57
58
  HttpRequest,
58
59
  HttpResponse,
59
60
  HttpServer,
60
61
  JobHandler,
62
+ ProcessInstanceSnapshot,
63
+ ProcessInstanceState,
61
64
  SqliteDb,
65
+ UserTaskState,
66
+ UserTaskSummary,
62
67
  WatchHandle,
63
68
  WorkerSubscription,
64
69
  } from "./core/host.ts";
70
+ // Shared form/user-task contract normalization (issue #252): the single source of truth
71
+ // both engine adapters (SDK/REST + WASM test double) call, so their form-identifier,
72
+ // form-schema, and user-task form-linkage handling cannot drift.
73
+ export {
74
+ buildFormSchema,
75
+ parseFormSchema,
76
+ pickFormLinkage,
77
+ presentFormIdentifier,
78
+ resolveFormIdentifier,
79
+ } from "./core/form-contract.ts";
65
80
  export type { AppApi, Mounted, RuntimeContext } from "./core/context.ts";
66
81
  // Structured logging surface (see core/logger.ts): the shape of `AppApi.log`, plus `createLogger`
67
82
  // so consumers can build a Logger for a custom sink or a no-op test double (`createLogger(() => {})`).