@nanobpm/urban 0.21.0 → 0.22.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/README.md +35 -0
- package/dist/cli.d.ts +26 -0
- package/dist/cli.js +80 -16
- package/dist/runtime/adapters/deno.js +19 -3
- package/dist/runtime/adapters/detect.js +2 -2
- package/dist/runtime/adapters/globals.d.ts +19 -0
- package/dist/runtime/adapters/globals.js +9 -0
- package/dist/runtime/adapters/node.js +25 -5
- package/dist/runtime/core/context.d.ts +4 -0
- package/dist/runtime/core/guards.d.ts +2 -0
- package/dist/runtime/core/guards.js +10 -0
- package/dist/runtime/core/host.js +5 -4
- package/dist/runtime/core/manifest.d.ts +8 -0
- package/dist/runtime/core/manifest.js +0 -1
- package/dist/runtime/core/modules/actions.js +9 -5
- package/dist/runtime/core/modules/connectors.js +6 -4
- package/dist/runtime/core/modules/dataops.d.ts +36 -0
- package/dist/runtime/core/modules/dataops.js +231 -0
- package/dist/runtime/core/modules/datasource.d.ts +51 -1
- package/dist/runtime/core/modules/datasource.js +98 -21
- package/dist/runtime/core/modules/deploy.d.ts +3 -1
- package/dist/runtime/core/modules/deploy.js +29 -7
- package/dist/runtime/core/modules/gateway.d.ts +1 -1
- package/dist/runtime/core/modules/gateway.js +16 -17
- package/dist/runtime/core/modules/llm.js +9 -7
- package/dist/runtime/core/modules/pages.js +42 -15
- package/dist/runtime/core/modules/security.js +5 -5
- package/dist/runtime/core/modules/templates.d.ts +18 -0
- package/dist/runtime/core/modules/templates.js +133 -0
- package/dist/runtime/core/modules/triggers.js +14 -5
- package/dist/runtime/core/modules/workers.js +15 -9
- package/dist/runtime/core/runtime.d.ts +5 -0
- package/dist/runtime/core/runtime.js +5 -5
- package/dist/runtime/core/validate.d.ts +2 -1
- package/dist/runtime/core/validate.js +38 -28
- package/dist/runtime/devserver.js +2 -1
- package/dist/runtime/engine/nanosdk.d.ts +1 -1
- package/dist/runtime/engine/nanosdk.js +15 -9
- package/dist/runtime/index.d.ts +5 -0
- package/dist/runtime/index.js +4 -0
- package/dist/runtime/run.js +10 -7
- package/dist/toolkit/addConnector.js +4 -3
- package/dist/toolkit/fsio.js +2 -1
- package/dist/toolkit/models.js +3 -5
- package/package.json +1 -1
- package/src/cli.test.ts +44 -0
- package/src/cli.ts +82 -16
- package/src/runtime/adapters/deno.ts +23 -3
- package/src/runtime/adapters/detect.ts +2 -2
- package/src/runtime/adapters/globals.ts +27 -0
- package/src/runtime/adapters/node.ts +27 -4
- package/src/runtime/connector-worker-sdk.test.ts +2 -1
- package/src/runtime/core/context.ts +4 -0
- package/src/runtime/core/guards.ts +10 -0
- package/src/runtime/core/host.ts +4 -4
- package/src/runtime/core/manifest.ts +19 -5
- package/src/runtime/core/modules/actions.test.ts +82 -31
- package/src/runtime/core/modules/actions.ts +11 -7
- package/src/runtime/core/modules/connectors.test.ts +29 -8
- package/src/runtime/core/modules/connectors.ts +6 -4
- package/src/runtime/core/modules/dataops.test.ts +306 -0
- package/src/runtime/core/modules/dataops.ts +292 -0
- package/src/runtime/core/modules/datasource.migrations.test.ts +12 -3
- package/src/runtime/core/modules/datasource.test.ts +129 -0
- package/src/runtime/core/modules/datasource.ts +105 -23
- package/src/runtime/core/modules/deploy.test.ts +258 -0
- package/src/runtime/core/modules/deploy.ts +29 -7
- package/src/runtime/core/modules/gateway.test.ts +5 -4
- package/src/runtime/core/modules/gateway.ts +29 -30
- package/src/runtime/core/modules/llm.test.ts +10 -12
- package/src/runtime/core/modules/llm.ts +11 -9
- package/src/runtime/core/modules/pages.test.ts +22 -2
- package/src/runtime/core/modules/pages.ts +45 -22
- package/src/runtime/core/modules/security.ts +5 -13
- package/src/runtime/core/modules/surfaces.test.ts +53 -10
- package/src/runtime/core/modules/templates.ts +153 -0
- package/src/runtime/core/modules/triggers.test.ts +51 -19
- package/src/runtime/core/modules/triggers.ts +14 -6
- package/src/runtime/core/modules/workers.test.ts +43 -20
- package/src/runtime/core/modules/workers.ts +18 -10
- package/src/runtime/core/runtime.test.ts +23 -10
- package/src/runtime/core/runtime.ts +15 -2
- package/src/runtime/core/validate.ts +43 -32
- package/src/runtime/devserver.test.ts +18 -5
- package/src/runtime/devserver.ts +2 -1
- package/src/runtime/engine/nanosdk.test.ts +16 -11
- package/src/runtime/engine/nanosdk.ts +22 -15
- package/src/runtime/index.ts +7 -0
- package/src/runtime/run.ts +10 -10
- package/src/toolkit/addConnector.ts +6 -5
- package/src/toolkit/derivers/messages.test.ts +4 -2
- package/src/toolkit/derivers/worker-io.test.ts +4 -2
- package/src/toolkit/fsio.ts +2 -1
- package/src/toolkit/gen.ts +4 -4
- package/src/toolkit/models.test.ts +1 -1
- package/src/toolkit/models.ts +3 -4
- package/src/toolkit/scaffold.ts +1 -1
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
// silently — the whole point of consuming the package rather than vendoring a copy.
|
|
10
10
|
|
|
11
11
|
import schema from "@nanobpm/nano-app-schema/schema" with { type: "json" };
|
|
12
|
-
import {
|
|
12
|
+
import { isRecord } from "./guards.ts";
|
|
13
|
+
import type { AppManifest } from "./manifest.ts";
|
|
13
14
|
|
|
14
15
|
export interface ValidationIssue {
|
|
15
16
|
path: string;
|
|
@@ -35,15 +36,15 @@ interface JsonSchema {
|
|
|
35
36
|
$defs?: { slug?: { pattern?: string } };
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
const S = schema
|
|
39
|
+
const S: JsonSchema = schema;
|
|
39
40
|
|
|
40
41
|
/** Validate a parsed manifest. Returns the list of issues (empty === valid). */
|
|
41
42
|
export function collectManifestIssues(m: unknown): ValidationIssue[] {
|
|
42
43
|
const issues: ValidationIssue[] = [];
|
|
43
|
-
if (
|
|
44
|
+
if (!isRecord(m)) {
|
|
44
45
|
return [{ path: "$", message: "manifest must be a JSON object" }];
|
|
45
46
|
}
|
|
46
|
-
const obj = m
|
|
47
|
+
const obj = m;
|
|
47
48
|
|
|
48
49
|
// — Envelope, driven by the schema —
|
|
49
50
|
for (const req of S.required ?? []) {
|
|
@@ -69,11 +70,11 @@ export function collectManifestIssues(m: unknown): ValidationIssue[] {
|
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
// — Runtime binding rules —
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (
|
|
73
|
+
const workers = Array.isArray(obj.workers) ? obj.workers : undefined;
|
|
74
|
+
if (workers) {
|
|
75
|
+
workers.forEach((w, i) => {
|
|
76
|
+
const worker = isRecord(w) ? w : undefined;
|
|
77
|
+
if (typeof worker?.taskType !== "string" || worker.taskType.length === 0) {
|
|
77
78
|
issues.push({ path: `workers[${i}]`, message: "missing taskType" });
|
|
78
79
|
}
|
|
79
80
|
// A worker is backed by exactly one of: a `handler` file, an `llm` binding,
|
|
@@ -82,9 +83,9 @@ export function collectManifestIssues(m: unknown): ValidationIssue[] {
|
|
|
82
83
|
// `connector` actually resolves to an installed pack is a runtime seam check
|
|
83
84
|
// (mountConnectors), not something this static manifest check can see.
|
|
84
85
|
const backings: string[] = [];
|
|
85
|
-
if (typeof
|
|
86
|
-
if (typeof
|
|
87
|
-
if (typeof
|
|
86
|
+
if (typeof worker?.handler === "string" && worker.handler.length > 0) backings.push("handler");
|
|
87
|
+
if (typeof worker?.llm === "string" && worker.llm.length > 0) backings.push("llm");
|
|
88
|
+
if (typeof worker?.connector === "string" && worker.connector.length > 0) backings.push("connector");
|
|
88
89
|
if (backings.length === 0) {
|
|
89
90
|
issues.push({
|
|
90
91
|
path: `workers[${i}]`,
|
|
@@ -97,37 +98,41 @@ export function collectManifestIssues(m: unknown): ValidationIssue[] {
|
|
|
97
98
|
});
|
|
98
99
|
}
|
|
99
100
|
// A `connection` must reference a declared top-level `connections` entry.
|
|
100
|
-
if (typeof
|
|
101
|
+
if (typeof worker?.connection === "string" && worker.connection.length > 0) {
|
|
101
102
|
const conns =
|
|
102
|
-
|
|
103
|
-
?
|
|
103
|
+
isRecord(obj.connections)
|
|
104
|
+
? obj.connections
|
|
104
105
|
: undefined;
|
|
105
|
-
if (!conns || !(
|
|
106
|
+
if (!conns || !(worker.connection in conns)) {
|
|
106
107
|
issues.push({
|
|
107
108
|
path: `workers[${i}].connection`,
|
|
108
|
-
message: `no such connection "${
|
|
109
|
+
message: `no such connection "${worker.connection}" (add it to connections)`,
|
|
109
110
|
});
|
|
110
111
|
}
|
|
111
112
|
}
|
|
112
113
|
});
|
|
113
114
|
}
|
|
114
115
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
const data = isRecord(obj.data) ? obj.data : undefined;
|
|
117
|
+
const sources = isRecord(data?.sources) ? data.sources : undefined;
|
|
118
|
+
if (sources) {
|
|
119
|
+
for (const [name, src] of Object.entries(sources)) {
|
|
120
|
+
const source = isRecord(src) ? src : undefined;
|
|
121
|
+
if (!source || typeof source.driver !== "string") {
|
|
118
122
|
issues.push({ path: `data.sources.${name}.driver`, message: "missing driver" });
|
|
119
123
|
}
|
|
120
|
-
if (!
|
|
124
|
+
if (!source || typeof source.url !== "string") {
|
|
121
125
|
issues.push({ path: `data.sources.${name}.url`, message: "missing url" });
|
|
122
126
|
}
|
|
123
127
|
}
|
|
124
|
-
if (
|
|
125
|
-
issues.push({ path: "data.default", message: `no such source "${
|
|
128
|
+
if (typeof data?.default === "string" && !sources[data.default]) {
|
|
129
|
+
issues.push({ path: "data.default", message: `no such source "${data.default}"` });
|
|
126
130
|
}
|
|
127
131
|
}
|
|
128
132
|
|
|
129
|
-
|
|
130
|
-
|
|
133
|
+
const types = isRecord(obj.types) ? obj.types : undefined;
|
|
134
|
+
if (types) {
|
|
135
|
+
for (const [name, t] of Object.entries(types)) {
|
|
131
136
|
// `table` is optional in the schema: a type may declare `fields` without a
|
|
132
137
|
// `table` (a transient / non-persisted domain type). Don't require it.
|
|
133
138
|
if (t && typeof t !== "object") {
|
|
@@ -136,19 +141,25 @@ export function collectManifestIssues(m: unknown): ValidationIssue[] {
|
|
|
136
141
|
}
|
|
137
142
|
}
|
|
138
143
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
144
|
+
const triggers = Array.isArray(obj.triggers) ? obj.triggers : undefined;
|
|
145
|
+
if (triggers) {
|
|
146
|
+
triggers.forEach((t, i) => {
|
|
147
|
+
const trigger = isRecord(t) ? t : undefined;
|
|
148
|
+
if (!trigger?.id) issues.push({ path: `triggers[${i}].id`, message: "missing id" });
|
|
149
|
+
if (!trigger?.type) issues.push({ path: `triggers[${i}].type`, message: "missing type" });
|
|
143
150
|
});
|
|
144
151
|
}
|
|
145
152
|
|
|
146
153
|
return issues;
|
|
147
154
|
}
|
|
148
155
|
|
|
149
|
-
|
|
150
|
-
export function validateManifest(m: unknown): AppManifest {
|
|
156
|
+
export function assertValidManifest(m: unknown): asserts m is AppManifest {
|
|
151
157
|
const issues = collectManifestIssues(m);
|
|
152
158
|
if (issues.length > 0) throw new ManifestValidationError(issues);
|
|
153
|
-
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Throw ManifestValidationError if the manifest is invalid; otherwise return it typed. */
|
|
162
|
+
export function validateManifest(m: unknown): AppManifest {
|
|
163
|
+
assertValidManifest(m);
|
|
164
|
+
return m;
|
|
154
165
|
}
|
|
@@ -92,13 +92,14 @@ test("stop() during an in-flight reload does not start a replacement app", async
|
|
|
92
92
|
const id = `a${starts.length}`;
|
|
93
93
|
starts.push(id);
|
|
94
94
|
if (id === "a0") {
|
|
95
|
-
|
|
95
|
+
const app: UrbanApp = {
|
|
96
96
|
...fakeApp(stops, id),
|
|
97
97
|
async stop() {
|
|
98
98
|
await stopGate; // block the first reload inside app.stop()
|
|
99
99
|
stops.push(id);
|
|
100
100
|
},
|
|
101
|
-
}
|
|
101
|
+
};
|
|
102
|
+
return app;
|
|
102
103
|
}
|
|
103
104
|
return fakeApp(stops, id);
|
|
104
105
|
},
|
|
@@ -195,18 +196,30 @@ test("a change during an in-flight reload doesn't detach stop() from it", async
|
|
|
195
196
|
function fakeHost(): { host: HostContext; fire: (p: string) => void; closed: () => boolean } {
|
|
196
197
|
let cb: ((p: string) => void) | undefined;
|
|
197
198
|
let closed = false;
|
|
198
|
-
const host = {
|
|
199
|
+
const host: HostContext = {
|
|
200
|
+
runtime: "node",
|
|
201
|
+
env: () => undefined,
|
|
202
|
+
readTextFile: async () => "",
|
|
203
|
+
listDir: async () => [],
|
|
204
|
+
exists: async () => false,
|
|
205
|
+
openSqlite: () => {
|
|
206
|
+
throw new Error("sqlite not used in this test");
|
|
207
|
+
},
|
|
208
|
+
importModule: async () => ({}),
|
|
209
|
+
serveHttp: async () => ({ port: 0, stop: async () => {} }),
|
|
199
210
|
watch(onChange: (p: string) => void) {
|
|
200
211
|
cb = onChange;
|
|
201
212
|
return { close: () => (closed = true) };
|
|
202
213
|
},
|
|
203
|
-
|
|
214
|
+
now: () => 0,
|
|
215
|
+
log: () => {},
|
|
216
|
+
};
|
|
204
217
|
return { host, fire: (p) => cb?.(p), closed: () => closed };
|
|
205
218
|
}
|
|
206
219
|
|
|
207
220
|
function fakeApp(stops: string[], id: string): UrbanApp {
|
|
208
221
|
return {
|
|
209
|
-
manifest: { id
|
|
222
|
+
manifest: { schemaVersion: 1, id, name: id },
|
|
210
223
|
root: ".",
|
|
211
224
|
async start() {},
|
|
212
225
|
async stop() {
|
package/src/runtime/devserver.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { runFromEnv } from "./run.ts";
|
|
15
15
|
import { selectHost } from "./adapters/detect.ts";
|
|
16
|
+
import { errorMessage } from "./core/guards.ts";
|
|
16
17
|
import type { HostContext, WatchHandle } from "./core/host.ts";
|
|
17
18
|
import type { UrbanApp } from "./core/runtime.ts";
|
|
18
19
|
import { createNodeGenIO, runGen } from "../toolkit/index.ts";
|
|
@@ -136,7 +137,7 @@ export async function runDev(opts: DevOptions = {}, deps?: Partial<DevDeps>): Pr
|
|
|
136
137
|
log("✔ reloaded");
|
|
137
138
|
} catch (err) {
|
|
138
139
|
// Keep the dev server alive on a bad edit; the next save will retry.
|
|
139
|
-
log(`✖ reload failed: ${
|
|
140
|
+
log(`✖ reload failed: ${errorMessage(err)}`);
|
|
140
141
|
} finally {
|
|
141
142
|
reloading = false;
|
|
142
143
|
if (queued && !stopped) {
|
|
@@ -224,7 +224,7 @@ test("registerWorker creates a worker the SDK auto-starts, and dispatches + comp
|
|
|
224
224
|
assert.equal(rec.started, 1);
|
|
225
225
|
|
|
226
226
|
let completedWith: Record<string, unknown> | undefined;
|
|
227
|
-
|
|
227
|
+
const job: NanoSdkActivatedJob = {
|
|
228
228
|
jobKey: 12,
|
|
229
229
|
processInstanceKey: 34,
|
|
230
230
|
elementId: "e1",
|
|
@@ -236,7 +236,8 @@ test("registerWorker creates a worker the SDK auto-starts, and dispatches + comp
|
|
|
236
236
|
async fail() {
|
|
237
237
|
throw new Error("should not fail");
|
|
238
238
|
},
|
|
239
|
-
}
|
|
239
|
+
};
|
|
240
|
+
await rec.dispatch(job);
|
|
240
241
|
assert.deepEqual(handled, [{ n: 5 }]);
|
|
241
242
|
assert.deepEqual(completedWith, { out: 5 });
|
|
242
243
|
|
|
@@ -252,7 +253,7 @@ test("registerWorker fails the job when the handler throws", async () => {
|
|
|
252
253
|
});
|
|
253
254
|
const rec = client.workers[0];
|
|
254
255
|
let failBody: { errorMessage: string; retries?: number } | undefined;
|
|
255
|
-
|
|
256
|
+
const job: NanoSdkActivatedJob = {
|
|
256
257
|
jobKey: "j1",
|
|
257
258
|
variables: {},
|
|
258
259
|
async complete() {
|
|
@@ -262,7 +263,8 @@ test("registerWorker fails the job when the handler throws", async () => {
|
|
|
262
263
|
failBody = body;
|
|
263
264
|
return "failed";
|
|
264
265
|
},
|
|
265
|
-
}
|
|
266
|
+
};
|
|
267
|
+
await rec.dispatch(job);
|
|
266
268
|
assert.equal(failBody?.errorMessage, "boom");
|
|
267
269
|
assert.equal(failBody?.retries, 0);
|
|
268
270
|
});
|
|
@@ -275,7 +277,7 @@ test("registerWorker routes a thrown BpmnError to the engine's error()", async (
|
|
|
275
277
|
});
|
|
276
278
|
const rec = client.workers[0];
|
|
277
279
|
let errorBody: { errorCode: string; errorMessage?: string } | undefined;
|
|
278
|
-
|
|
280
|
+
const job: NanoSdkActivatedJob = {
|
|
279
281
|
jobKey: "j1",
|
|
280
282
|
variables: {},
|
|
281
283
|
async complete() {
|
|
@@ -288,7 +290,8 @@ test("registerWorker routes a thrown BpmnError to the engine's error()", async (
|
|
|
288
290
|
errorBody = body;
|
|
289
291
|
return "raised";
|
|
290
292
|
},
|
|
291
|
-
}
|
|
293
|
+
};
|
|
294
|
+
await rec.dispatch(job);
|
|
292
295
|
assert.equal(errorBody?.errorCode, "NOT_FOUND");
|
|
293
296
|
assert.equal(errorBody?.errorMessage, "no such record");
|
|
294
297
|
});
|
|
@@ -301,7 +304,7 @@ test("registerWorker falls back to fail() for a BpmnError when the SDK has no er
|
|
|
301
304
|
});
|
|
302
305
|
const rec = client.workers[0];
|
|
303
306
|
let failBody: { errorMessage: string } | undefined;
|
|
304
|
-
|
|
307
|
+
const job: NanoSdkActivatedJob = {
|
|
305
308
|
jobKey: "j1",
|
|
306
309
|
variables: {},
|
|
307
310
|
async complete() {
|
|
@@ -312,7 +315,8 @@ test("registerWorker falls back to fail() for a BpmnError when the SDK has no er
|
|
|
312
315
|
return "failed";
|
|
313
316
|
},
|
|
314
317
|
// no error() — older transport
|
|
315
|
-
}
|
|
318
|
+
};
|
|
319
|
+
await rec.dispatch(job);
|
|
316
320
|
assert.equal(failBody?.errorMessage, "no such record");
|
|
317
321
|
});
|
|
318
322
|
|
|
@@ -324,7 +328,7 @@ test("registerWorker falls back to fail() when the BPMN error report itself thro
|
|
|
324
328
|
});
|
|
325
329
|
const rec = client.workers[0];
|
|
326
330
|
let failBody: { errorMessage: string } | undefined;
|
|
327
|
-
|
|
331
|
+
const job: NanoSdkActivatedJob = {
|
|
328
332
|
jobKey: "j1",
|
|
329
333
|
variables: {},
|
|
330
334
|
async complete() {
|
|
@@ -337,7 +341,8 @@ test("registerWorker falls back to fail() when the BPMN error report itself thro
|
|
|
337
341
|
failBody = body;
|
|
338
342
|
return "failed";
|
|
339
343
|
},
|
|
340
|
-
}
|
|
344
|
+
};
|
|
345
|
+
await rec.dispatch(job);
|
|
341
346
|
// The job must still be acknowledged via fail() rather than silently dropped.
|
|
342
347
|
assert.equal(failBody?.errorMessage, "no such record");
|
|
343
348
|
});
|
|
@@ -371,7 +376,7 @@ test("close drains the REST-fallback worker via the SDK's stopAllWorkers", async
|
|
|
371
376
|
|
|
372
377
|
test("close tolerates a client without stopAllWorkers", async () => {
|
|
373
378
|
const client = fakeSdkClient();
|
|
374
|
-
delete
|
|
379
|
+
delete client.stopAllWorkers;
|
|
375
380
|
const engine = new SdkEngineClient(client);
|
|
376
381
|
await engine.registerWorker("a", async () => ({}));
|
|
377
382
|
await engine.close();
|
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
} from "../core/host.ts";
|
|
20
20
|
import { isBpmnError } from "../core/host.ts";
|
|
21
21
|
import type { EngineSdkClient } from "./sdk.ts";
|
|
22
|
+
import { isRecord } from "../core/guards.ts";
|
|
22
23
|
|
|
23
24
|
/** Coerce an engine response's process-instance key to a non-empty string, or
|
|
24
25
|
* throw — a missing key means a malformed/partial response, not a real instance. */
|
|
@@ -32,7 +33,7 @@ export function requireProcessInstanceKey(key: string | number | null | undefine
|
|
|
32
33
|
/** A job as delivered to a nano-sdk job handler: the frame fields plus the
|
|
33
34
|
* acknowledgement actions the handler must call. */
|
|
34
35
|
export interface NanoSdkActivatedJob {
|
|
35
|
-
jobKey: string;
|
|
36
|
+
jobKey: string | number;
|
|
36
37
|
type?: string;
|
|
37
38
|
processInstanceKey?: string | number;
|
|
38
39
|
elementId?: string;
|
|
@@ -134,7 +135,8 @@ export class SdkEngineClient implements EngineClient {
|
|
|
134
135
|
* the same connection. Surfaced to handlers as `AppApi.sdk`.
|
|
135
136
|
*/
|
|
136
137
|
get sdk(): EngineSdkClient {
|
|
137
|
-
|
|
138
|
+
// biome-ignore lint/plugin: `this.client` is the same `@nanobpm/nano-sdk` client at runtime; exposing its full EngineSdkClient surface is the adapter boundary (see EngineSdkClient in ./sdk.ts).
|
|
139
|
+
return this.client as NanoSdkClient & EngineSdkClient;
|
|
138
140
|
}
|
|
139
141
|
|
|
140
142
|
async deployResources(
|
|
@@ -155,12 +157,13 @@ export class SdkEngineClient implements EngineClient {
|
|
|
155
157
|
variables: input.variables ?? {},
|
|
156
158
|
awaitCompletion: input.awaitCompletion ?? false,
|
|
157
159
|
});
|
|
160
|
+
const rawKey = body.processInstanceKey ?? body.key;
|
|
158
161
|
const key =
|
|
159
|
-
|
|
160
|
-
|
|
162
|
+
typeof rawKey === "string" || typeof rawKey === "number" ? rawKey : undefined;
|
|
163
|
+
const variables = isRecord(body.variables) ? body.variables : undefined;
|
|
161
164
|
return {
|
|
162
165
|
processInstanceKey: requireProcessInstanceKey(key),
|
|
163
|
-
variables
|
|
166
|
+
variables,
|
|
164
167
|
};
|
|
165
168
|
}
|
|
166
169
|
|
|
@@ -188,10 +191,10 @@ export class SdkEngineClient implements EngineClient {
|
|
|
188
191
|
// User tasks are an eventually consistent read; ask for zero-wait consistency so
|
|
189
192
|
// the search reflects what is currently visible without blocking.
|
|
190
193
|
const body = await this.client.searchUserTasks(
|
|
191
|
-
{ filter: (filter ?? {})
|
|
194
|
+
{ filter: { ...(filter ?? {}) } },
|
|
192
195
|
{ consistency: { waitUpToMs: 0 } },
|
|
193
196
|
);
|
|
194
|
-
const items = (body.items
|
|
197
|
+
const items = Array.isArray(body.items) ? body.items.filter(isRecord) : [];
|
|
195
198
|
return items.flatMap((it) => {
|
|
196
199
|
const userTaskKey = it.userTaskKey ?? it.key;
|
|
197
200
|
if (userTaskKey == null || userTaskKey === "") {
|
|
@@ -200,8 +203,8 @@ export class SdkEngineClient implements EngineClient {
|
|
|
200
203
|
}
|
|
201
204
|
return [{
|
|
202
205
|
userTaskKey: String(userTaskKey),
|
|
203
|
-
elementId: it.elementId
|
|
204
|
-
variables: it.variables
|
|
206
|
+
elementId: typeof it.elementId === "string" ? it.elementId : undefined,
|
|
207
|
+
variables: isRecord(it.variables) ? it.variables : undefined,
|
|
205
208
|
}];
|
|
206
209
|
});
|
|
207
210
|
}
|
|
@@ -236,7 +239,7 @@ export class SdkEngineClient implements EngineClient {
|
|
|
236
239
|
};
|
|
237
240
|
try {
|
|
238
241
|
const out = await handler(engineJob);
|
|
239
|
-
return await job.complete(
|
|
242
|
+
return await job.complete(out ?? {});
|
|
240
243
|
} catch (err) {
|
|
241
244
|
// A handler-raised BPMN error is a modelled, non-retryable outcome:
|
|
242
245
|
// report it as a BPMN error (routed to an error boundary/event) rather
|
|
@@ -246,7 +249,7 @@ export class SdkEngineClient implements EngineClient {
|
|
|
246
249
|
if (isBpmnError(err) && typeof job.error === "function") {
|
|
247
250
|
// `message` is typed loosely (the guard survives module duplication),
|
|
248
251
|
// so coerce to a string before slicing to avoid a secondary crash.
|
|
249
|
-
const raw =
|
|
252
|
+
const raw = err.message;
|
|
250
253
|
const message = typeof raw === "string" && raw.length > 0 ? raw : err.errorCode;
|
|
251
254
|
this.log("info", `handler ${jobType} raised BPMN error`, {
|
|
252
255
|
errorCode: err.errorCode,
|
|
@@ -339,15 +342,19 @@ async function importNanoSdk(): Promise<
|
|
|
339
342
|
// Indirect the specifier so the (lazily loaded) module is not resolved at
|
|
340
343
|
// typecheck time and stays out of the import graph for injected-client paths.
|
|
341
344
|
const spec = "@nanobpm/nano-sdk";
|
|
342
|
-
const mod =
|
|
343
|
-
|
|
344
|
-
};
|
|
345
|
-
if (typeof mod.createCamundaClient !== "function") {
|
|
345
|
+
const mod: unknown = await import(spec);
|
|
346
|
+
if (!isNanoSdkModule(mod)) {
|
|
346
347
|
throw new Error("@nanobpm/nano-sdk does not export createCamundaClient");
|
|
347
348
|
}
|
|
348
349
|
return mod.createCamundaClient;
|
|
349
350
|
}
|
|
350
351
|
|
|
352
|
+
function isNanoSdkModule(value: unknown): value is {
|
|
353
|
+
createCamundaClient: (opts: Record<string, unknown>) => NanoSdkClient;
|
|
354
|
+
} {
|
|
355
|
+
return isRecord(value) && typeof value.createCamundaClient === "function";
|
|
356
|
+
}
|
|
357
|
+
|
|
351
358
|
/**
|
|
352
359
|
* Build the Urban runtime's `EngineClient`, backed by a single `@nanobpm/nano-sdk`
|
|
353
360
|
* client. Provide a `client`/`createClient` seam to inject a fake (tests, embedded
|
package/src/runtime/index.ts
CHANGED
|
@@ -70,6 +70,8 @@ export type { ConnectorsHandle } from "./core/modules/connectors.ts";
|
|
|
70
70
|
// Data layer
|
|
71
71
|
export { DataLayer, TypeRepo } from "./core/modules/datasource.ts";
|
|
72
72
|
export type { ProvisionedSource } from "./core/modules/datasource.ts";
|
|
73
|
+
export { runDataOp } from "./core/modules/dataops.ts";
|
|
74
|
+
export type { DataOp, DataRequest, ResolvedSource } from "./core/modules/dataops.ts";
|
|
73
75
|
export { makeGateway, Table } from "./core/modules/gateway.ts";
|
|
74
76
|
export type {
|
|
75
77
|
ColumnMeta,
|
|
@@ -101,6 +103,11 @@ export type {
|
|
|
101
103
|
} from "./core/modules/llm.ts";
|
|
102
104
|
export { evalCorrelation } from "./core/modules/triggers.ts";
|
|
103
105
|
|
|
106
|
+
// Model deploy + `{{name}}` template substitution
|
|
107
|
+
export { deployModels } from "./core/modules/deploy.ts";
|
|
108
|
+
export { applyTemplates, resolveTemplates } from "./core/modules/templates.ts";
|
|
109
|
+
export type { TemplateApplication, TemplateSource } from "./core/modules/templates.ts";
|
|
110
|
+
|
|
104
111
|
// Adapters + engine + run entrypoint
|
|
105
112
|
export { createNodeHost } from "./adapters/node.ts";
|
|
106
113
|
export { createDenoHost } from "./adapters/deno.ts";
|
package/src/runtime/run.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { createUrbanApp, type CreateUrbanAppOptions, type UrbanApp } from "./core/runtime.ts";
|
|
6
6
|
import { selectHost } from "./adapters/detect.ts";
|
|
7
|
+
import { denoGlobal, processGlobal } from "./adapters/globals.ts";
|
|
7
8
|
import { createNanoSdkEngineClient } from "./engine/nanosdk.ts";
|
|
8
9
|
import type { EngineClient } from "./core/host.ts";
|
|
9
10
|
|
|
@@ -49,6 +50,7 @@ export async function runFromEnv(opts: RunOptions = {}): Promise<UrbanApp> {
|
|
|
49
50
|
manifestPath: opts.manifestPath,
|
|
50
51
|
port: opts.port,
|
|
51
52
|
mount: opts.mount,
|
|
53
|
+
templates: opts.templates,
|
|
52
54
|
});
|
|
53
55
|
await app.start();
|
|
54
56
|
|
|
@@ -74,19 +76,17 @@ export function installSignalHandlers(stop: () => Promise<void>): void {
|
|
|
74
76
|
}
|
|
75
77
|
});
|
|
76
78
|
};
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
};
|
|
81
|
-
if (g.Deno) {
|
|
79
|
+
const deno = denoGlobal();
|
|
80
|
+
const proc = processGlobal();
|
|
81
|
+
if (deno?.addSignalListener) {
|
|
82
82
|
try {
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
deno.addSignalListener("SIGINT", onSignal);
|
|
84
|
+
deno.addSignalListener("SIGTERM", onSignal);
|
|
85
85
|
} catch {
|
|
86
86
|
/* signal listeners may be unavailable (e.g. Windows) */
|
|
87
87
|
}
|
|
88
|
-
} else if (
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
} else if (proc?.on) {
|
|
89
|
+
proc.on("SIGINT", onSignal);
|
|
90
|
+
proc.on("SIGTERM", onSignal);
|
|
91
91
|
}
|
|
92
92
|
}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
// unit-testable.
|
|
14
14
|
|
|
15
15
|
import type { GenIO } from "./gen.ts";
|
|
16
|
+
import { errorMessage, isRecord } from "../runtime/core/guards.ts";
|
|
16
17
|
|
|
17
18
|
/** A config field a pack declares (subset of ext-types `ConfigField`). */
|
|
18
19
|
interface PackConfigField {
|
|
@@ -90,9 +91,9 @@ export async function addConnector(opts: AddConnectorOptions): Promise<AddConnec
|
|
|
90
91
|
}
|
|
91
92
|
let pack: PackManifest;
|
|
92
93
|
try {
|
|
93
|
-
pack = JSON.parse(await io.readText(packManifestPath))
|
|
94
|
+
pack = JSON.parse(await io.readText(packManifestPath));
|
|
94
95
|
} catch (err) {
|
|
95
|
-
throw new Error(`failed to parse ${packManifestPath}: ${(err
|
|
96
|
+
throw new Error(`failed to parse ${packManifestPath}: ${errorMessage(err)}`);
|
|
96
97
|
}
|
|
97
98
|
if (typeof pack.id !== "string" || !pack.id) {
|
|
98
99
|
throw new Error(`${packManifestPath} has no string "id" (a pack must declare its id).`);
|
|
@@ -123,7 +124,7 @@ export async function addConnector(opts: AddConnectorOptions): Promise<AddConnec
|
|
|
123
124
|
try {
|
|
124
125
|
app = JSON.parse(await io.readText(appManifestPath));
|
|
125
126
|
} catch (err) {
|
|
126
|
-
throw new Error(`failed to parse ${appManifestPath}: ${(err
|
|
127
|
+
throw new Error(`failed to parse ${appManifestPath}: ${errorMessage(err)}`);
|
|
127
128
|
}
|
|
128
129
|
if (app === null || typeof app !== "object" || Array.isArray(app)) {
|
|
129
130
|
throw new Error(`${appManifestPath} is not a JSON object.`);
|
|
@@ -168,10 +169,10 @@ export async function addConnector(opts: AddConnectorOptions): Promise<AddConnec
|
|
|
168
169
|
// A pre-existing connection of this name must be a compatible object of the
|
|
169
170
|
// same pack `type`; otherwise wiring workers to it would produce an invalid
|
|
170
171
|
// or misleading manifest. Fail loudly instead of silently reusing it.
|
|
171
|
-
if (
|
|
172
|
+
if (!isRecord(existing)) {
|
|
172
173
|
throw new Error(`connection "${connection}" already exists and is not an object.`);
|
|
173
174
|
}
|
|
174
|
-
const conn = existing
|
|
175
|
+
const conn = existing;
|
|
175
176
|
if (conn.type !== undefined && conn.type !== pack.id) {
|
|
176
177
|
throw new Error(
|
|
177
178
|
`connection "${connection}" already exists with type "${String(conn.type)}", ` +
|
|
@@ -134,10 +134,12 @@ const XML = `<?xml version="1.0"?>
|
|
|
134
134
|
|
|
135
135
|
test("scanModelMessages extracts message name + envelope; a self-closing message is name-only", () => {
|
|
136
136
|
const m = scanModelMessages(XML);
|
|
137
|
-
const placed = m.find((x) => x.messageName === "OrderPlaced")
|
|
137
|
+
const placed = m.find((x) => x.messageName === "OrderPlaced");
|
|
138
|
+
assert.ok(placed);
|
|
138
139
|
assert.ok(placed);
|
|
139
140
|
assert.equal(placed.inputType, "Order");
|
|
140
|
-
const ping = m.find((x) => x.messageName === "Ping")
|
|
141
|
+
const ping = m.find((x) => x.messageName === "Ping");
|
|
142
|
+
assert.ok(ping);
|
|
141
143
|
assert.ok(ping);
|
|
142
144
|
assert.equal(ping.inputType, undefined);
|
|
143
145
|
});
|
|
@@ -40,13 +40,15 @@ const XML = `<?xml version="1.0"?>
|
|
|
40
40
|
|
|
41
41
|
test("scanModelWorkers extracts task type, element id, process, and envelope io", () => {
|
|
42
42
|
const w = scanModelWorkers(XML);
|
|
43
|
-
const charge = w.find((x) => x.taskType === "orders.charge")
|
|
43
|
+
const charge = w.find((x) => x.taskType === "orders.charge");
|
|
44
|
+
assert.ok(charge);
|
|
44
45
|
assert.ok(charge);
|
|
45
46
|
assert.equal(charge.elementId, "Charge");
|
|
46
47
|
assert.equal(charge.process, "orders");
|
|
47
48
|
assert.equal(charge.in, "Payment");
|
|
48
49
|
assert.equal(charge.out, "Receipt");
|
|
49
|
-
const notify = w.find((x) => x.taskType === "orders.notify")
|
|
50
|
+
const notify = w.find((x) => x.taskType === "orders.notify");
|
|
51
|
+
assert.ok(notify);
|
|
50
52
|
assert.equal(notify.in, undefined);
|
|
51
53
|
assert.equal(notify.out, undefined);
|
|
52
54
|
});
|
package/src/toolkit/fsio.ts
CHANGED
|
@@ -50,7 +50,8 @@ export function createNodeGenIO(): GenIO {
|
|
|
50
50
|
// stat failed (e.g. a virtual path); fall back to no cache key and let import() surface it.
|
|
51
51
|
}
|
|
52
52
|
const href = `${pathToFileURL(abs).href}${version}`;
|
|
53
|
-
|
|
53
|
+
const mod: Promise<Record<string, unknown>> = import(href);
|
|
54
|
+
return mod;
|
|
54
55
|
},
|
|
55
56
|
};
|
|
56
57
|
}
|
package/src/toolkit/gen.ts
CHANGED
|
@@ -110,9 +110,9 @@ async function collectAll(opts: GenOptions): Promise<{ artifacts: DerivedArtifac
|
|
|
110
110
|
const { root, io } = opts;
|
|
111
111
|
const emitModels = opts.emitModels ?? true;
|
|
112
112
|
const manifestPath = join(root, opts.manifestFile ?? "nano.app.json");
|
|
113
|
-
const manifest
|
|
113
|
+
const manifest: ToolkitManifest & {
|
|
114
114
|
models?: { processes?: string[]; workflows?: string[] };
|
|
115
|
-
};
|
|
115
|
+
} = JSON.parse(await io.readText(manifestPath));
|
|
116
116
|
|
|
117
117
|
const artifacts: DerivedArtifact[] = [];
|
|
118
118
|
|
|
@@ -186,8 +186,8 @@ export { dirOf, join as joinPath };
|
|
|
186
186
|
export async function previewModels(opts: GenOptions): Promise<DerivedModels> {
|
|
187
187
|
const { root, io } = opts;
|
|
188
188
|
const manifestPath = join(root, opts.manifestFile ?? "nano.app.json");
|
|
189
|
-
const manifest
|
|
189
|
+
const manifest: {
|
|
190
190
|
models?: { processes?: string[]; workflows?: string[] };
|
|
191
|
-
};
|
|
191
|
+
} = JSON.parse(await io.readText(manifestPath));
|
|
192
192
|
return deriveModels(root, io, manifest);
|
|
193
193
|
}
|
|
@@ -93,7 +93,7 @@ test("deriveModels turns workflows/*.ts into a provenance-stamped processes/<id>
|
|
|
93
93
|
|
|
94
94
|
test("deriveModels is a no-op without a module loader (pure/IDE callers)", async () => {
|
|
95
95
|
const io = memIO({ "/app/workflows/greet.ts": "// source" });
|
|
96
|
-
delete
|
|
96
|
+
delete io.importModule;
|
|
97
97
|
const d = await deriveModels("/app", io, CODE_FIRST_MANIFEST);
|
|
98
98
|
assert.equal(d.attempted, false);
|
|
99
99
|
assert.equal(d.artifacts.length, 0);
|
package/src/toolkit/models.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type { DerivedArtifact } from "./artifact.ts";
|
|
|
14
14
|
import type { GenIO } from "./gen.ts";
|
|
15
15
|
import { expandPattern, joinPath } from "./gen.ts";
|
|
16
16
|
import type { ModelSource } from "./derivers/worker-io.ts";
|
|
17
|
+
import { isRecord } from "../runtime/core/guards.ts";
|
|
17
18
|
|
|
18
19
|
/** Default source glob for code-first flows (matches the code-first scaffold layout). */
|
|
19
20
|
export const DEFAULT_WORKFLOW_PATTERNS = ["workflows/*.ts"];
|
|
@@ -34,9 +35,7 @@ export interface ModelsManifest {
|
|
|
34
35
|
|
|
35
36
|
/** A `defineFlow` export: has a string `id` and an `imperative`/`declarative` `kind`. */
|
|
36
37
|
export function isWorkflow(v: unknown): v is Workflow {
|
|
37
|
-
|
|
38
|
-
const o = v as { id?: unknown; kind?: unknown };
|
|
39
|
-
return typeof o.id === "string" && (o.kind === "imperative" || o.kind === "declarative");
|
|
38
|
+
return isRecord(v) && typeof v.id === "string" && (v.kind === "imperative" || v.kind === "declarative");
|
|
40
39
|
}
|
|
41
40
|
|
|
42
41
|
/** Sanitize a flow id into a safe `.bpmn` filename (mirrors the host generator). */
|
|
@@ -113,7 +112,7 @@ function messageOf(e: unknown): string {
|
|
|
113
112
|
/** Append an actionable hint when a `.ts` import fails because the runtime can't strip types. */
|
|
114
113
|
function importHint(rel: string, e: unknown): string {
|
|
115
114
|
const base = messageOf(e);
|
|
116
|
-
const code = (e
|
|
115
|
+
const code = isRecord(e) && typeof e.code === "string" ? e.code : "";
|
|
117
116
|
const looksLikeNoTsLoader =
|
|
118
117
|
rel.endsWith(".ts") &&
|
|
119
118
|
(code === "ERR_UNKNOWN_FILE_EXTENSION" || /Unknown file extension "\.ts"/.test(base));
|
package/src/toolkit/scaffold.ts
CHANGED
|
@@ -57,7 +57,7 @@ export async function scaffoldWorkers(opts: ScaffoldOptions): Promise<ScaffoldRu
|
|
|
57
57
|
const manifestFile = opts.manifestFile ?? "nano.app.json";
|
|
58
58
|
const manifestPath = joinPath(root, manifestFile);
|
|
59
59
|
|
|
60
|
-
const manifest = JSON.parse(await io.readText(manifestPath))
|
|
60
|
+
const manifest: ScaffoldManifest = JSON.parse(await io.readText(manifestPath));
|
|
61
61
|
const models = await readModels(root, io, manifest);
|
|
62
62
|
const declaredTypeIds = Object.keys(manifest.types ?? {});
|
|
63
63
|
|