@nanobpm/urban 0.6.0 → 0.8.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.
@@ -5,6 +5,8 @@
5
5
 
6
6
  import type { AppApi, RuntimeContext } from "../context.ts";
7
7
  import { html, json, normalizeRoutePath, type Route } from "../router.ts";
8
+ import { mountActions } from "./actions.ts";
9
+ import { mountPages } from "./pages.ts";
8
10
 
9
11
  /** Escape HTML-significant characters before embedding a value in markup. */
10
12
  function escapeHtml(s: string): string {
@@ -98,6 +100,22 @@ export function mountSurfaces(ctx: RuntimeContext, app: AppApi): SurfacesHandle
98
100
  });
99
101
  }
100
102
 
103
+ // App-authored action overrides mount BEFORE the generic pages routes so an exact
104
+ // override (e.g. /app/actions/cancel) shadows the generic action; the router is
105
+ // first-match-wins.
106
+ const actions = mountActions(ctx, app);
107
+ if (actions.routes.length > 0) {
108
+ routes.push(...actions.routes);
109
+ enabled.push(`actions(${actions.routes.length})`);
110
+ }
111
+
112
+ // The pages surface (the schema-driven page runtime) mounts its own routes.
113
+ const pages = mountPages(ctx, app);
114
+ if (pages.routes.length > 0) {
115
+ routes.push(...pages.routes);
116
+ enabled.push("pages@/");
117
+ }
118
+
101
119
  ctx.host.log("info", "surfaces mounted", { enabled });
102
120
  return { name: "surfaces", routes, describe: () => ({ enabled }) };
103
121
  }
@@ -18,6 +18,7 @@ class FakeEngine implements EngineClient {
18
18
  deployed = 0;
19
19
  workers = new Map<string, JobHandler>();
20
20
  messages: { name: string; correlationKey?: string; variables?: Record<string, unknown> }[] = [];
21
+ canceledInstances: string[] = [];
21
22
  completedTasks: { key: string; variables?: Record<string, unknown> }[] = [];
22
23
  userTasks = [{ userTaskKey: "ut-1", elementId: "approve", variables: {} }];
23
24
 
@@ -28,6 +29,9 @@ class FakeEngine implements EngineClient {
28
29
  async createInstance() {
29
30
  return { processInstanceKey: "pi-1" };
30
31
  }
32
+ async cancelInstance(input: { processInstanceKey: string }) {
33
+ this.canceledInstances.push(input.processInstanceKey);
34
+ }
31
35
  async publishMessage(input: { name: string; correlationKey?: string; variables?: Record<string, unknown> }) {
32
36
  this.messages.push(input);
33
37
  }
@@ -29,6 +29,14 @@ test("unknown top-level keys are rejected (additionalProperties:false)", () => {
29
29
  assert.ok(issues.some((i) => i.path === "bogus"));
30
30
  });
31
31
 
32
+ test("an `actions` array is an accepted top-level key (ADR 0055 §3)", () => {
33
+ const issues = collectManifestIssues({
34
+ ...valid,
35
+ actions: [{ path: "/app/actions/cancel", module: "actions/cancel.ts" }],
36
+ });
37
+ assert.deepEqual(issues, []);
38
+ });
39
+
32
40
  test("bad schemaVersion and bad slug id are reported", () => {
33
41
  const issues = collectManifestIssues({ ...valid, schemaVersion: 2, id: "Not A Slug" });
34
42
  assert.ok(issues.some((i) => i.path === "schemaVersion"));
@@ -53,6 +53,10 @@ function fakeSdkClient(overrides: Partial<NanoSdkClient> = {}): NanoSdkClient &
53
53
  calls.push("createProcessInstance");
54
54
  return { processInstanceKey: 99, variables: input.variables };
55
55
  },
56
+ async cancelProcessInstance(input) {
57
+ calls.push("cancelProcessInstance");
58
+ return { ...input };
59
+ },
56
60
  async publishMessage(input) {
57
61
  calls.push("publishMessage");
58
62
  return { key: 1, ...input };
@@ -131,6 +135,20 @@ test("createInstance throws when the SDK response omits the instance key", async
131
135
  );
132
136
  });
133
137
 
138
+ test("cancelInstance routes through the SDK", async () => {
139
+ let seen: { processInstanceKey: string | number } | undefined;
140
+ const client = fakeSdkClient({
141
+ cancelProcessInstance: async (input) => {
142
+ seen = input;
143
+ return {};
144
+ },
145
+ });
146
+ const engine = new SdkEngineClient(client);
147
+ await engine.cancelInstance({ processInstanceKey: "pi-7" });
148
+ assert.deepEqual(seen, { processInstanceKey: "pi-7" });
149
+ assert.ok(client.calls.includes("cancelProcessInstance") || seen !== undefined);
150
+ });
151
+
134
152
  test("publishMessage defaults correlationKey/variables", async () => {
135
153
  let seen: Record<string, unknown> | undefined;
136
154
  const client = fakeSdkClient({
@@ -79,6 +79,10 @@ export interface NanoSdkClient {
79
79
  },
80
80
  options?: unknown,
81
81
  ): Promise<Record<string, unknown>>;
82
+ cancelProcessInstance(
83
+ input: { processInstanceKey: string | number },
84
+ options?: unknown,
85
+ ): Promise<unknown>;
82
86
  publishMessage(
83
87
  input: { name: string; correlationKey?: string; variables?: Record<string, unknown> },
84
88
  options?: unknown,
@@ -150,6 +154,10 @@ export class SdkEngineClient implements EngineClient {
150
154
  };
151
155
  }
152
156
 
157
+ async cancelInstance(input: { processInstanceKey: string }): Promise<void> {
158
+ await this.client.cancelProcessInstance({ processInstanceKey: input.processInstanceKey });
159
+ }
160
+
153
161
  async publishMessage(input: {
154
162
  name: string;
155
163
  correlationKey?: string;
@@ -17,6 +17,7 @@ export {
17
17
  workerJobType,
18
18
  } from "./core/manifest.ts";
19
19
  export type {
20
+ ActionDecl,
20
21
  AppManifest,
21
22
  DataSource,
22
23
  DomainType,
@@ -61,6 +62,10 @@ export type {
61
62
  Row,
62
63
  TableMeta,
63
64
  } from "./core/modules/gateway.ts";
65
+ export { createPagesRoutes, mountPages } from "./core/modules/pages.ts";
66
+ export type { PagesDataSource, PagesDeps, PagesHandle, PagesOptions } from "./core/modules/pages.ts";
67
+ export { mountActions, resolveActionHandler } from "./core/modules/actions.ts";
68
+ export type { ActionHandler, ActionRequest, ActionResult, ActionsHandle } from "./core/modules/actions.ts";
64
69
  export type { AppJobHandler } from "./core/modules/workers.ts";
65
70
  export { resolveHandler } from "./core/modules/workers.ts";
66
71
  export { evalCorrelation } from "./core/modules/triggers.ts";
@@ -52,6 +52,11 @@
52
52
  "additionalProperties": { "$ref": "#/$defs/connection" }
53
53
  },
54
54
  "surfaces": { "$ref": "#/$defs/surfaces" },
55
+ "actions": {
56
+ "type": "array",
57
+ "items": { "$ref": "#/$defs/action" },
58
+ "description": "App-authored action handler overrides (ADR 0055 §3): each binds an HTTP route to a handler module that wraps the generic pages start/cancel/message actions with business logic, injected with the app's runtime API."
59
+ },
55
60
  "workers": {
56
61
  "type": "array",
57
62
  "items": { "$ref": "#/$defs/worker" },
@@ -367,6 +372,33 @@
367
372
  }
368
373
  }
369
374
  },
375
+ "action": {
376
+ "type": "object",
377
+ "additionalProperties": false,
378
+ "required": ["path", "module"],
379
+ "description": "An app-authored action handler override (ADR 0055 §3): binds a route to a handler module (default-exports an ActionHandler). Mounted before the generic pages action routes, so an exact override shadows the generic one.",
380
+ "properties": {
381
+ "path": {
382
+ "type": "string",
383
+ "minLength": 1,
384
+ "description": "Route path to serve, e.g. '/app/actions/cancel' or '/app/actions/start/convergence-loop'."
385
+ },
386
+ "module": {
387
+ "type": "string",
388
+ "minLength": 1,
389
+ "description": "Handler module path relative to the app root; default-exports (or names `handler`) an ActionHandler."
390
+ },
391
+ "method": {
392
+ "type": "string",
393
+ "minLength": 1,
394
+ "description": "HTTP method to match. Default 'POST'."
395
+ },
396
+ "prefix": {
397
+ "type": "boolean",
398
+ "description": "Match `path` as a prefix rather than exactly. Default false."
399
+ }
400
+ }
401
+ },
370
402
  "llmBinding": {
371
403
  "type": "object",
372
404
  "additionalProperties": false,