@nanobpm/urban 0.25.0 → 0.27.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/modules/dataops.d.ts +28 -4
- package/dist/runtime/core/modules/dataops.js +123 -5
- package/dist/runtime/core/modules/pages.js +108 -25
- package/dist/toolkit/derivers/domain.d.ts +18 -0
- package/dist/toolkit/derivers/domain.js +134 -0
- package/dist/toolkit/derivers/messages.d.ts +14 -0
- package/dist/toolkit/derivers/messages.js +37 -0
- package/dist/toolkit/derivers/shapes.d.ts +96 -0
- package/dist/toolkit/derivers/shapes.js +498 -0
- package/dist/toolkit/derivers/worker-io.d.ts +26 -0
- package/dist/toolkit/derivers/worker-io.js +101 -1
- package/dist/toolkit/index.d.ts +6 -4
- package/dist/toolkit/index.js +4 -3
- package/package.json +2 -2
- package/src/runtime/core/modules/dataops.test.ts +132 -9
- package/src/runtime/core/modules/dataops.ts +180 -11
- package/src/runtime/core/modules/pages.test.ts +42 -0
- package/src/runtime/core/modules/pages.ts +108 -25
- package/src/toolkit/derivers/bindings-parity.test.ts +66 -0
- package/src/toolkit/derivers/domain.ts +171 -0
- package/src/toolkit/derivers/messages.ts +38 -0
- package/src/toolkit/derivers/shapes.test.ts +171 -0
- package/src/toolkit/derivers/shapes.ts +602 -0
- package/src/toolkit/derivers/worker-io.ts +103 -1
- package/src/toolkit/index.ts +24 -0
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { HostContext } from "../host.ts";
|
|
2
2
|
import type { AppManifest } from "../manifest.ts";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
import { type ShapeDecl } from "../../../toolkit/derivers/shapes.ts";
|
|
4
|
+
import { type WorkerBindingDecl } from "../../../toolkit/derivers/worker-io.ts";
|
|
5
|
+
import { type MessageBindingDecl } from "../../../toolkit/derivers/messages.ts";
|
|
6
|
+
import { type MetaDecl } from "../../../toolkit/derivers/meta.ts";
|
|
7
|
+
/** The DB-manager op protocol's known op set — every op (including `domaintypes`) is dispatched by
|
|
8
|
+
* `runDataOp`. Typing `op` as this union (instead of `string`) gives SDK/console callers type-safety
|
|
9
|
+
* and catches typos at compile time. */
|
|
7
10
|
export type DataOp = "sources" | "schema" | "query" | "exec" | "script" | "migrations" | "migrate" | "domaintypes";
|
|
8
11
|
export interface DataRequest {
|
|
9
12
|
op: DataOp;
|
|
@@ -11,6 +14,27 @@ export interface DataRequest {
|
|
|
11
14
|
sql?: string;
|
|
12
15
|
params?: unknown[];
|
|
13
16
|
statements?: string[];
|
|
17
|
+
/**
|
|
18
|
+
* `domaintypes`: persist the derived artifacts (default true). A `write:false` request is the
|
|
19
|
+
* latency-sensitive composer preview — it introspects WITHOUT migrating the DB first and returns
|
|
20
|
+
* only the artifact *content* (never a DB side-effect per keystroke). `urban data` never writes
|
|
21
|
+
* files itself (the host is read-only); the caller persists the returned content when `write` is
|
|
22
|
+
* not false. This flag only gates the pre-introspection migration side-effect.
|
|
23
|
+
*/
|
|
24
|
+
write?: boolean;
|
|
25
|
+
/** `domaintypes`: the model-derived worker-IO map (`taskType → {in,out,headerKeys}`), scanned
|
|
26
|
+
* from the process models by the caller. Overlaid on the manifest `workers[]` (authoritative for
|
|
27
|
+
* the envelope). Absent → the manifest projection alone. */
|
|
28
|
+
derivedWorkers?: WorkerBindingDecl[];
|
|
29
|
+
/** `domaintypes`: the model-derived message-payload map (`messageName → {in,out}`), scanned from
|
|
30
|
+
* the models' `bpmn:message` envelopes. Authoritative (no manifest projection). */
|
|
31
|
+
derivedMessages?: MessageBindingDecl[];
|
|
32
|
+
/** `domaintypes`: the model-derived composed shapes (`nano:shape`). Each is resolved through the
|
|
33
|
+
* fuse into a flat domain type folded into the `types` registry. Absent → no shapes. */
|
|
34
|
+
derivedShapes?: ShapeDecl[];
|
|
35
|
+
/** `domaintypes`: the model-derived model-level metadata (`nano:meta`). Folded into `meta.ts` and
|
|
36
|
+
* the structured `domain.json`. Absent → no metadata. */
|
|
37
|
+
derivedMeta?: MetaDecl[];
|
|
14
38
|
}
|
|
15
39
|
/** One datasource the manifest declares, resolved against the environment (env templates in
|
|
16
40
|
* `driver`/`url` are already expanded by `loadManifest`). Mirrors the console's `ResolvedSource`
|
|
@@ -14,14 +14,25 @@
|
|
|
14
14
|
// op = "sources" | "schema" | "query" | "exec" | "script" | "migrations" | "migrate"
|
|
15
15
|
// | "domaintypes"
|
|
16
16
|
//
|
|
17
|
-
// `domaintypes` (live-schema reification + shape resolution)
|
|
18
|
-
//
|
|
19
|
-
//
|
|
17
|
+
// `domaintypes` (live-schema reification + shape resolution) composes the domain reifier from the
|
|
18
|
+
// pure toolkit derivers (ported from the console's `domain_types.ts`, host dry-out #576); it never
|
|
19
|
+
// writes files (the host is read-only) — it returns every artifact's content and the caller
|
|
20
|
+
// persists it.
|
|
20
21
|
import { isRecord } from "../guards.js";
|
|
21
22
|
import { loadManifest } from "../manifest.js";
|
|
22
23
|
import { validateManifest } from "../validate.js";
|
|
23
24
|
import { makeGateway } from "./gateway.js";
|
|
24
25
|
import { applyMigrations, MIGRATIONS_TABLE, openSqliteSource, resolveAppPath } from "./datasource.js";
|
|
26
|
+
// The `domaintypes` op composes the domain reifier from the pure toolkit derivers (single source of
|
|
27
|
+
// truth for the IDE's codegen, host dry-out nano-bpm#576). Imported directly from the deriver files
|
|
28
|
+
// (not the `toolkit/` barrel, which pulls the Node fs IO) so core stays free of `node:*`/Deno; the
|
|
29
|
+
// derivers' only core dependency is the gateway's `TableMeta`/`ColumnMeta` *types*, which erase.
|
|
30
|
+
import { GENERATED_DIR } from "../../../toolkit/artifact.js";
|
|
31
|
+
import { DOMAIN_BINDINGS, emitDomainBindings, emitDomainModel, registryFromManifest, } from "../../../toolkit/derivers/domain.js";
|
|
32
|
+
import { DOMAIN_MODEL_JSON, emitDomainModelJson, resolveShapes, } from "../../../toolkit/derivers/shapes.js";
|
|
33
|
+
import { DOMAIN_DTS, emitWorkerBindings, emitWorkerBindingsRuntime, overlayDerivedWorkerIo, WORKER_BINDINGS_DTS, WORKER_BINDINGS_TS, } from "../../../toolkit/derivers/worker-io.js";
|
|
34
|
+
import { emitMessageBindings, emitMessageBindingsRuntime, MESSAGE_BINDINGS_DTS, MESSAGE_BINDINGS_TS, } from "../../../toolkit/derivers/messages.js";
|
|
35
|
+
import { emitMeta, META_TS } from "../../../toolkit/derivers/meta.js";
|
|
25
36
|
const DEFAULT_MIGRATIONS_DIR = "db/migrations";
|
|
26
37
|
/** JSON-safe a value: BigInt → number (when lossless) or string, recursing objects/arrays. SQLite
|
|
27
38
|
* reports `lastInsertRowid` (and INTEGER columns) as BigInt, which `JSON.stringify` cannot encode. */
|
|
@@ -222,8 +233,115 @@ export async function runDataOp(host, root, manifestPath, req) {
|
|
|
222
233
|
}
|
|
223
234
|
}
|
|
224
235
|
case "domaintypes": {
|
|
225
|
-
|
|
226
|
-
|
|
236
|
+
// ADR 0029 §4.1/§4.2/§6 + ADR 0040 §9/§10: reify the domain model from the *live* schema.
|
|
237
|
+
// Union every declared datasource (the table spine), fold in the manifest `types` registry and
|
|
238
|
+
// the model's composed `nano:shape`s (resolved through the fuse), and derive the worker/message
|
|
239
|
+
// typed accessors. Unlike the vendored data_cli, this op NEVER writes files (the host is
|
|
240
|
+
// read-only): it returns every artifact's content and the caller persists it. `write:false` is
|
|
241
|
+
// the latency-sensitive composer preview — it skips the pre-introspection migration so a
|
|
242
|
+
// debounced keystroke has no DB side-effect and pays only for `text` + `shapeDiagnostics`.
|
|
243
|
+
const { default: def, sources } = listSources(manifest);
|
|
244
|
+
const persist = req.write !== false;
|
|
245
|
+
// Introspect each sqlite source's live schema. When persisting, migrate first (the domain
|
|
246
|
+
// model is derived from `gw.schema()`, so a regen that raced ahead of `migrate` on a fresh DB
|
|
247
|
+
// would emit an empty `Domain`); the migration runs on the shared `_urban_migrations` ledger.
|
|
248
|
+
const migrated = {};
|
|
249
|
+
const schemas = [];
|
|
250
|
+
for (const s of sources) {
|
|
251
|
+
if (String(s.driver) !== "sqlite")
|
|
252
|
+
continue; // only sqlite is introspectable (see openGateway)
|
|
253
|
+
const { gw, db } = await openGateway(host, root, manifest, s.name);
|
|
254
|
+
try {
|
|
255
|
+
if (persist) {
|
|
256
|
+
const applied = await applyMigrations(host, db, root, s.migrations ?? DEFAULT_MIGRATIONS_DIR);
|
|
257
|
+
if (applied.length)
|
|
258
|
+
migrated[s.name] = applied;
|
|
259
|
+
}
|
|
260
|
+
schemas.push({ source: s.name, tables: await gw.schema() });
|
|
261
|
+
}
|
|
262
|
+
finally {
|
|
263
|
+
db.close();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const types = registryFromManifest(manifest);
|
|
267
|
+
// Snapshot the manifest `types` *before* folding the resolved shapes so `domain.json` can tag
|
|
268
|
+
// each entity with its true provenance (manifest vs model) — once folded they are
|
|
269
|
+
// indistinguishable in `types`.
|
|
270
|
+
const manifestTypesSnapshot = { ...types };
|
|
271
|
+
// Composed motion shapes (ADR 0040 §9/§10): resolve each `nano:shape` against the leaf fuse
|
|
272
|
+
// into a flat domain type and fold it into the registry *before* it feeds the domain model and
|
|
273
|
+
// the worker/message bindings, so a composed shape becomes a first-class `DomainTypes` entry.
|
|
274
|
+
// Broken shapes are omitted; their diagnostics are returned so the panel can surface them.
|
|
275
|
+
const shapeResolution = resolveShapes(req.derivedShapes ?? [], types, schemas);
|
|
276
|
+
const shapeDeclById = new Map((req.derivedShapes ?? []).map((s) => [s.id, s]));
|
|
277
|
+
const shapeEntities = [];
|
|
278
|
+
for (const [id, shapeDef] of Object.entries(shapeResolution.types)) {
|
|
279
|
+
types[id] = shapeDef;
|
|
280
|
+
shapeEntities.push({ decl: shapeDeclById.get(id) ?? { id, ops: [] }, def: shapeDef });
|
|
281
|
+
}
|
|
282
|
+
const declaredIds = Object.keys(types);
|
|
283
|
+
const text = emitDomainModel(schemas, def, types);
|
|
284
|
+
const bindings = emitDomainBindings(schemas, def);
|
|
285
|
+
const derivedMeta = req.derivedMeta ?? [];
|
|
286
|
+
const metaAccessor = emitMeta(derivedMeta);
|
|
287
|
+
// Worker-IO map (ADR 0033 §3): the model is authoritative for the envelope, so an injected
|
|
288
|
+
// `derivedWorkers` is overlaid on the manifest `workers[]` (which still carries non-IO
|
|
289
|
+
// bindings such as `llm`); absent, the manifest projection stands alone.
|
|
290
|
+
const manifestWorkers = (manifest.workers ?? []).map((w) => ({
|
|
291
|
+
taskType: w.taskType,
|
|
292
|
+
inputType: w.inputType,
|
|
293
|
+
outputType: w.outputType,
|
|
294
|
+
}));
|
|
295
|
+
const workers = req.derivedWorkers
|
|
296
|
+
? overlayDerivedWorkerIo(manifestWorkers, req.derivedWorkers)
|
|
297
|
+
: manifestWorkers;
|
|
298
|
+
const workerBindings = emitWorkerBindings(workers, declaredIds);
|
|
299
|
+
const workerRuntime = emitWorkerBindingsRuntime();
|
|
300
|
+
// Message-payload registry (ADR 0040 slice 2): no manifest projection, so the model-derived
|
|
301
|
+
// map is authoritative and emitted directly.
|
|
302
|
+
const messages = req.derivedMessages ?? [];
|
|
303
|
+
const messageBindings = emitMessageBindings(messages, declaredIds);
|
|
304
|
+
const messageRuntime = emitMessageBindingsRuntime();
|
|
305
|
+
// The structured Fused Domain Model (`domain.json`, ADR 0040 §1): the largest artifact, so it
|
|
306
|
+
// is built only when persisting (the preview consumes only `text` + `shapeDiagnostics`).
|
|
307
|
+
const meta = derivedMeta;
|
|
308
|
+
const domainModel = persist
|
|
309
|
+
? emitDomainModelJson({
|
|
310
|
+
sources: schemas,
|
|
311
|
+
default: def,
|
|
312
|
+
manifestTypes: manifestTypesSnapshot,
|
|
313
|
+
shapes: shapeEntities,
|
|
314
|
+
meta,
|
|
315
|
+
diagnostics: shapeResolution.diagnostics,
|
|
316
|
+
})
|
|
317
|
+
: null;
|
|
318
|
+
const rel = (name) => `${GENERATED_DIR}/${name}`;
|
|
319
|
+
const tables = schemas.reduce((n, s) => n + s.tables.length, 0);
|
|
320
|
+
// The returned `*Path` fields are the app-relative targets the caller writes to when
|
|
321
|
+
// persisting (null on a preview). `urban data` returns content only — persisting is the
|
|
322
|
+
// caller's job (host dry-out nano-bpm#576). The static runtime wrappers (`workers.ts`,
|
|
323
|
+
// `messages.ts`) are returned too so the caller can write them verbatim.
|
|
324
|
+
return {
|
|
325
|
+
path: persist ? rel(DOMAIN_DTS) : null,
|
|
326
|
+
text,
|
|
327
|
+
tables,
|
|
328
|
+
bindingsPath: persist ? rel(DOMAIN_BINDINGS) : null,
|
|
329
|
+
bindings,
|
|
330
|
+
workerBindingsPath: persist ? rel(WORKER_BINDINGS_DTS) : null,
|
|
331
|
+
workerBindings,
|
|
332
|
+
workerRuntimePath: persist ? rel(WORKER_BINDINGS_TS) : null,
|
|
333
|
+
workerRuntime,
|
|
334
|
+
messageBindingsPath: persist ? rel(MESSAGE_BINDINGS_DTS) : null,
|
|
335
|
+
messageBindings,
|
|
336
|
+
messageRuntimePath: persist ? rel(MESSAGE_BINDINGS_TS) : null,
|
|
337
|
+
messageRuntime,
|
|
338
|
+
metaPath: persist ? rel(META_TS) : null,
|
|
339
|
+
meta: metaAccessor,
|
|
340
|
+
domainModelPath: persist ? rel(DOMAIN_MODEL_JSON) : null,
|
|
341
|
+
domainModel,
|
|
342
|
+
migrated,
|
|
343
|
+
shapeDiagnostics: shapeResolution.diagnostics,
|
|
344
|
+
};
|
|
227
345
|
}
|
|
228
346
|
default:
|
|
229
347
|
throw new Error(`unknown op "${req.op}"`);
|
|
@@ -319,50 +319,86 @@ function rendererShell(homePage) {
|
|
|
319
319
|
</head>
|
|
320
320
|
<body>
|
|
321
321
|
<main id="page" data-home="${escapeAttr(homePage)}"><p class="pc-empty">Loading…</p></main>
|
|
322
|
-
<script type="module" src="
|
|
322
|
+
<script type="module" src="./app/runtime.js"></script>
|
|
323
323
|
</body>
|
|
324
324
|
</html>`;
|
|
325
325
|
}
|
|
326
|
+
// The app's stylesheet resolves every colour through the Nano console's
|
|
327
|
+
// semantic `--nano-*` token contract (see the Magikcraft/nano-bpm repo's
|
|
328
|
+
// `console/src/theme/tokens.css` — MIRROR: keep the palettes below in sync). When the app is embedded in the
|
|
329
|
+
// console it inherits nothing across the iframe boundary, so the console
|
|
330
|
+
// postMessages its resolved tokens + appearance and the runtime lays them onto
|
|
331
|
+
// this document's :root (see RENDERER_JS). Standalone (CLI on :3000) there is no
|
|
332
|
+
// console, so the defaults below stand on their own: dark by default, light when
|
|
333
|
+
// the OS asks for it and nothing has themed us. `--pc-*` are kept as thin
|
|
334
|
+
// aliases so existing class rules re-resolve through the shared tokens.
|
|
326
335
|
const RENDERER_CSS = `
|
|
327
|
-
:root {
|
|
336
|
+
:root {
|
|
337
|
+
color-scheme: dark;
|
|
338
|
+
--nano-app:#0b0b10; --nano-panel:#10101a; --nano-raised:#16161f; --nano-inset:#08080c; --nano-hover:#1e1e2a;
|
|
339
|
+
--nano-edge:#24242f; --nano-edge-strong:#383848;
|
|
340
|
+
--nano-text:#f2f2f7; --nano-text-muted:#a3a3b2; --nano-text-faint:#6e6e80;
|
|
341
|
+
--nano-accent:#8b5cf6; --nano-accent-strong:#a78bfa; --nano-accent-2:#22d3ee; --nano-on-accent:#ffffff;
|
|
342
|
+
--nano-ok:#34d399; --nano-warn:#fbbf24; --nano-danger:#fb7185; --nano-info:#38bdf8;
|
|
343
|
+
}
|
|
344
|
+
:root[data-appearance="light"] {
|
|
345
|
+
color-scheme: light;
|
|
346
|
+
--nano-app:#f5f5f9; --nano-panel:#fdfdfe; --nano-raised:#ffffff; --nano-inset:#ededf3; --nano-hover:#e8e8f0;
|
|
347
|
+
--nano-edge:#e2e2ea; --nano-edge-strong:#c5c5d4;
|
|
348
|
+
--nano-text:#1a1a22; --nano-text-muted:#565664; --nano-text-faint:#8c8c9c;
|
|
349
|
+
--nano-accent:#7c3aed; --nano-accent-strong:#6d28d9; --nano-accent-2:#0891b2; --nano-on-accent:#ffffff;
|
|
350
|
+
--nano-ok:#059669; --nano-warn:#b45309; --nano-danger:#e11d48; --nano-info:#0369a1;
|
|
351
|
+
}
|
|
352
|
+
@media (prefers-color-scheme: light) {
|
|
353
|
+
/* Standalone only: follow the OS until the console (which sets data-appearance) themes us. */
|
|
354
|
+
:root:not([data-appearance]) {
|
|
355
|
+
color-scheme: light;
|
|
356
|
+
--nano-app:#f5f5f9; --nano-panel:#fdfdfe; --nano-raised:#ffffff; --nano-inset:#ededf3; --nano-hover:#e8e8f0;
|
|
357
|
+
--nano-edge:#e2e2ea; --nano-edge-strong:#c5c5d4;
|
|
358
|
+
--nano-text:#1a1a22; --nano-text-muted:#565664; --nano-text-faint:#8c8c9c;
|
|
359
|
+
--nano-accent:#7c3aed; --nano-accent-strong:#6d28d9; --nano-accent-2:#0891b2; --nano-on-accent:#ffffff;
|
|
360
|
+
--nano-ok:#059669; --nano-warn:#b45309; --nano-danger:#e11d48; --nano-info:#0369a1;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
:root { --pc-edge:var(--nano-edge); --pc-accent:var(--nano-accent); }
|
|
328
364
|
* { box-sizing: border-box; }
|
|
329
|
-
body { margin:0; font:15px/1.5 system-ui,sans-serif; padding:2rem; max-width:64rem; margin-inline:auto; }
|
|
330
|
-
.pc-empty {
|
|
365
|
+
body { margin:0; font:15px/1.5 system-ui,sans-serif; padding:2rem; max-width:64rem; margin-inline:auto; background:var(--nano-app); color:var(--nano-text); }
|
|
366
|
+
.pc-empty { color:var(--nano-text-faint); }
|
|
331
367
|
.pc-heading { font-size:1.6rem; font-weight:650; margin:0 0 .25rem; }
|
|
332
|
-
.pc-sub {
|
|
368
|
+
.pc-sub { color:var(--nano-text-muted); margin:.25rem 0 1rem; }
|
|
333
369
|
.pc-body { margin:.5rem 0; }
|
|
334
|
-
.pc-card { border:1px solid var(--
|
|
370
|
+
.pc-card { border:1px solid var(--nano-edge); border-radius:.6rem; padding:1rem 1.15rem; margin:1rem 0; background:var(--nano-panel); }
|
|
335
371
|
.pc-card h2 { font-size:1rem; margin:0 0 .75rem; }
|
|
336
372
|
.pc-field { display:flex; flex-direction:column; gap:.25rem; margin-bottom:.6rem; }
|
|
337
|
-
.pc-field label { font-size:.8rem;
|
|
338
|
-
.pc-field input { padding:.5rem .6rem; border:1px solid var(--
|
|
339
|
-
.pc-btn { padding:.5rem .9rem; border:0; border-radius:.4rem; background:var(--
|
|
373
|
+
.pc-field label { font-size:.8rem; color:var(--nano-text-muted); }
|
|
374
|
+
.pc-field input { padding:.5rem .6rem; border:1px solid var(--nano-edge); border-radius:.4rem; font:inherit; background:var(--nano-inset); color:var(--nano-text); }
|
|
375
|
+
.pc-btn { padding:.5rem .9rem; border:0; border-radius:.4rem; background:var(--nano-accent); color:var(--nano-on-accent); font:inherit; cursor:pointer; }
|
|
340
376
|
.pc-btn:disabled { opacity:.5; cursor:default; }
|
|
341
377
|
.pc-msg { font-size:.85rem; margin-top:.5rem; min-height:1.2em; }
|
|
342
|
-
.pc-msg.err { color
|
|
343
|
-
.pc-msg.ok { color
|
|
378
|
+
.pc-msg.err { color:var(--nano-danger); }
|
|
379
|
+
.pc-msg.ok { color:var(--nano-ok); }
|
|
344
380
|
table.pc-grid { width:100%; border-collapse:collapse; font-size:.9rem; }
|
|
345
|
-
table.pc-grid th, table.pc-grid td { text-align:left; padding:.4rem .6rem; border-bottom:1px solid var(--
|
|
346
|
-
table.pc-grid th { font-weight:600;
|
|
381
|
+
table.pc-grid th, table.pc-grid td { text-align:left; padding:.4rem .6rem; border-bottom:1px solid var(--nano-edge); }
|
|
382
|
+
table.pc-grid th { font-weight:600; color:var(--nano-text-muted); }
|
|
347
383
|
.pc-tabs { display:flex; gap:.5rem; margin-bottom:.75rem; }
|
|
348
|
-
.pc-tab { padding:.35rem .8rem; border:1px solid var(--
|
|
349
|
-
.pc-tab.active { background:var(--
|
|
384
|
+
.pc-tab { padding:.35rem .8rem; border:1px solid var(--nano-edge); border-radius:.4rem; background:transparent; color:inherit; font:inherit; cursor:pointer; }
|
|
385
|
+
.pc-tab.active { background:var(--nano-accent); color:var(--nano-on-accent); border-color:var(--nano-accent); }
|
|
350
386
|
.pc-btn-sm { padding:.25rem .55rem; font-size:.8rem; margin-right:.3rem; }
|
|
351
|
-
.pc-chevron { background:transparent; color:inherit; border:1px solid var(--
|
|
387
|
+
.pc-chevron { background:transparent; color:inherit; border:1px solid var(--nano-edge); }
|
|
352
388
|
.pc-row-actions { white-space:nowrap; text-align:right; }
|
|
353
389
|
.pc-detail { padding:.75rem .25rem; }
|
|
354
390
|
.pc-detail-field { display:flex; gap:.5rem; font-size:.85rem; margin:.15rem 0; }
|
|
355
|
-
.pc-detail-label {
|
|
356
|
-
.pc-link { color:var(--
|
|
391
|
+
.pc-detail-label { color:var(--nano-text-muted); min-width:8rem; }
|
|
392
|
+
.pc-link { color:var(--nano-accent); }
|
|
357
393
|
.pc-child { margin:.6rem 0; }
|
|
358
|
-
.pc-child-title { font-size:.8rem; font-weight:600;
|
|
359
|
-
.pc-transcript { white-space:pre-wrap; max-height:22rem; overflow:auto; background:
|
|
360
|
-
.pc-subform { margin-top:.75rem; padding:.6rem; border:1px dashed var(--
|
|
394
|
+
.pc-child-title { font-size:.8rem; font-weight:600; color:var(--nano-text-muted); margin-bottom:.25rem; }
|
|
395
|
+
.pc-transcript { white-space:pre-wrap; max-height:22rem; overflow:auto; background:var(--nano-inset); padding:.5rem; border-radius:.4rem; font-size:.8rem; margin-top:.4rem; }
|
|
396
|
+
.pc-subform { margin-top:.75rem; padding:.6rem; border:1px dashed var(--nano-edge); border-radius:.5rem; }
|
|
361
397
|
.pc-subform-title { font-weight:600; font-size:.85rem; margin-bottom:.4rem; }
|
|
362
|
-
.pc-prompt { font-size:.85rem;
|
|
363
|
-
.pc-textarea { width:100%; min-height:4rem; padding:.5rem; border:1px solid var(--
|
|
398
|
+
.pc-prompt { font-size:.85rem; color:var(--nano-text-muted); margin-bottom:.4rem; white-space:pre-wrap; }
|
|
399
|
+
.pc-textarea { width:100%; min-height:4rem; padding:.5rem; border:1px solid var(--nano-edge); border-radius:.4rem; font:inherit; background:var(--nano-inset); color:var(--nano-text); }
|
|
364
400
|
.pc-collapse-header { display:flex; align-items:center; gap:.5rem; width:100%; margin:0 0 .75rem; padding:0; background:transparent; border:0; color:inherit; font:inherit; font-size:1rem; font-weight:600; cursor:pointer; text-align:left; }
|
|
365
|
-
.pc-chevron-inline {
|
|
401
|
+
.pc-chevron-inline { color:var(--nano-text-faint); font-size:.75rem; width:1em; }
|
|
366
402
|
.pc-card-body[hidden] { display:none; }
|
|
367
403
|
`;
|
|
368
404
|
// The schema-driven browser renderer (ADR 0042 §3). Plain ES module string served at
|
|
@@ -373,8 +409,55 @@ const RENDERER_JS = String.raw `
|
|
|
373
409
|
const root = document.getElementById("page");
|
|
374
410
|
const HOME = root.dataset.home || "home";
|
|
375
411
|
|
|
412
|
+
// Theme bridge for the Nano console embed. The app runs in a sandboxed,
|
|
413
|
+
// same-origin iframe, so the console's --nano-* custom properties and its
|
|
414
|
+
// data-appearance never cascade in. Instead the console postMessages its
|
|
415
|
+
// resolved tokens; we lay them onto this document's :root and mirror the
|
|
416
|
+
// appearance. Standalone (no console parent) nothing is posted, so the CSS
|
|
417
|
+
// defaults stand. We announce readiness so the console can push the theme even
|
|
418
|
+
// if its first message raced our listener install.
|
|
419
|
+
function applyTheme(msg) {
|
|
420
|
+
if (!msg || msg.type !== "nano-theme") return;
|
|
421
|
+
const el = document.documentElement;
|
|
422
|
+
if (msg.appearance === "light" || msg.appearance === "dark") el.dataset.appearance = msg.appearance;
|
|
423
|
+
const vars = msg.vars;
|
|
424
|
+
if (vars && typeof vars === "object") {
|
|
425
|
+
for (const [k, v] of Object.entries(vars)) {
|
|
426
|
+
// Only accept the shared token namespace; ignore anything else.
|
|
427
|
+
if (typeof k === "string" && k.startsWith("--nano-") && typeof v === "string") {
|
|
428
|
+
el.style.setProperty(k, v);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (window.parent && window.parent !== window) {
|
|
434
|
+
window.addEventListener("message", (ev) => {
|
|
435
|
+
// Same-origin proxy (posture A): only trust a message from the framing
|
|
436
|
+
// parent AND from our own origin. ev.source alone is insufficient — a
|
|
437
|
+
// cross-origin page that frames a standalone run becomes window.parent, so
|
|
438
|
+
// an origin check is what actually pins the trust boundary.
|
|
439
|
+
if (ev.origin === window.location.origin && ev.source === window.parent) applyTheme(ev.data);
|
|
440
|
+
});
|
|
441
|
+
window.parent.postMessage({ type: "nano-app-ready" }, window.location.origin);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Resolve every app endpoint against where THIS module was actually served from,
|
|
445
|
+
// so absolute-looking "/app/…" paths work both at the origin root (direct run,
|
|
446
|
+
// e.g. the CLI on :3000) and under a path-prefixed reverse proxy (the Nano
|
|
447
|
+
// console embeds the app at /console/app-view/<name>/). "../" drops the trailing
|
|
448
|
+
// "app/runtime.js" to land on the app's mount root (always ends in "/").
|
|
449
|
+
const APP_BASE = new URL("../", import.meta.url);
|
|
450
|
+
function apiUrl(u) {
|
|
451
|
+
// Rebase root-absolute app paths ("/app/…") onto the mount root. Leave a
|
|
452
|
+
// protocol-relative ("//host/…") or otherwise non-root-relative string alone —
|
|
453
|
+
// only a single leading slash denotes an app path we own.
|
|
454
|
+
return typeof u === "string" && u.startsWith("/") && !u.startsWith("//")
|
|
455
|
+
? new URL(u.slice(1), APP_BASE).toString()
|
|
456
|
+
: u;
|
|
457
|
+
}
|
|
458
|
+
|
|
376
459
|
async function getJSON(url, opts) {
|
|
377
|
-
const r = await fetch(url, opts);
|
|
460
|
+
const r = await fetch(apiUrl(url), opts);
|
|
378
461
|
const body = await r.json().catch(() => ({}));
|
|
379
462
|
if (!r.ok) throw new Error(body.error || ("HTTP " + r.status));
|
|
380
463
|
return body;
|
|
@@ -44,6 +44,9 @@ export interface DomainTypeDef {
|
|
|
44
44
|
}
|
|
45
45
|
/** The manifest `types` registry: type id → declared record type. */
|
|
46
46
|
export type DomainTypeRegistry = Record<string, DomainTypeDef>;
|
|
47
|
+
/** Whether a manifest field keyword resolves to a primitive (vs a nominal ref). Shared with the
|
|
48
|
+
* shape fuse (`shapes.ts`) so the primitive-keyword set has one definition (no drift). */
|
|
49
|
+
export declare function isPrimitiveKeyword(type: string): boolean;
|
|
47
50
|
/**
|
|
48
51
|
* Emit the `DomainTypes` block for the manifest `types` registry: an inline map keyed by type id.
|
|
49
52
|
* Returns `""` when the registry is empty, so the table spine stays byte-identical when no types
|
|
@@ -55,6 +58,21 @@ export declare function emitDomainTypeRegistry(types: DomainTypeRegistry): strin
|
|
|
55
58
|
* followed by the manifest `types` registry (§4.2).
|
|
56
59
|
*/
|
|
57
60
|
export declare function emitDomainModel(sources: SourceSchema[], defaultSource: string | undefined, types: DomainTypeRegistry): string;
|
|
61
|
+
/** The generated bindings file's basename — the typed `openDomain()` accessor materialised next to
|
|
62
|
+
* `domain-rows.d.ts` under a project's `nano-generated/`. */
|
|
63
|
+
export declare const DOMAIN_BINDINGS = "domain.ts";
|
|
64
|
+
/**
|
|
65
|
+
* Emit `domain.ts`: a typed data-object accessor over the App's default datasource, so a worker
|
|
66
|
+
* writes `db.orders.insert({...})` / `db.orders.get(id)` instead of hand-writing SQL (ADR 0029 §6).
|
|
67
|
+
* The generic `Table<T>` runtime lives in `data-sdk.ts`; this generated module only binds each
|
|
68
|
+
* table name to its row type (from `domain-rows.d.ts`) and primary key, so it imports nothing but
|
|
69
|
+
* the sibling SDK (relative) and a type-only `domain-rows.d.ts` — staying dual-runtime and erased at
|
|
70
|
+
* `deno compile`. When the App declares multiple datasources this emits a *keyed*
|
|
71
|
+
* `openDomain<K>(source?)` whose returned accessor is typed against that source's tables; `db.raw`
|
|
72
|
+
* is the per-source escape hatch. Single-source apps keep the zero-arg `openDomain()` concrete form
|
|
73
|
+
* (byte-stable).
|
|
74
|
+
*/
|
|
75
|
+
export declare function emitDomainBindings(sources: SourceSchema[], defaultSource?: string): string;
|
|
58
76
|
/** The datasource table spine derived from the manifest types, grouped by source (same grouping
|
|
59
77
|
* and order as the SQL migration). */
|
|
60
78
|
export declare function sourcesFromManifest(manifest: ToolkitManifest): SourceSchema[];
|
|
@@ -143,6 +143,11 @@ const PRIMITIVE_TS = {
|
|
|
143
143
|
datetime: "string",
|
|
144
144
|
json: "unknown",
|
|
145
145
|
};
|
|
146
|
+
/** Whether a manifest field keyword resolves to a primitive (vs a nominal ref). Shared with the
|
|
147
|
+
* shape fuse (`shapes.ts`) so the primitive-keyword set has one definition (no drift). */
|
|
148
|
+
export function isPrimitiveKeyword(type) {
|
|
149
|
+
return Object.prototype.hasOwnProperty.call(PRIMITIVE_TS, type);
|
|
150
|
+
}
|
|
146
151
|
/**
|
|
147
152
|
* The TS type for a registry field. A primitive keyword maps directly (and takes precedence over
|
|
148
153
|
* an identically named type, per the schema); anything else is a nominal reference to another
|
|
@@ -187,6 +192,135 @@ export function emitDomainModel(sources, defaultSource, types) {
|
|
|
187
192
|
const registry = emitDomainTypeRegistry(types);
|
|
188
193
|
return registry ? `${spine}\n${registry}` : spine;
|
|
189
194
|
}
|
|
195
|
+
// --- domain bindings: the typed data-object accessor (ADR 0029 §6) ---------------------------
|
|
196
|
+
/** The generated bindings file's basename — the typed `openDomain()` accessor materialised next to
|
|
197
|
+
* `domain-rows.d.ts` under a project's `nano-generated/`. */
|
|
198
|
+
export const DOMAIN_BINDINGS = "domain.ts";
|
|
199
|
+
/** Members `openDomain()` reserves on the returned handle — the `DomainBase` escape hatch
|
|
200
|
+
* (`raw`/`close`). A table whose wire name is one of these would clobber the escape hatch (or the
|
|
201
|
+
* type intersection), so it is dropped from the typed table surface; it stays reachable via
|
|
202
|
+
* `db.raw.table("<name>", pk)`. */
|
|
203
|
+
const RESERVED_DOMAIN_MEMBERS = new Set(["raw", "close"]);
|
|
204
|
+
/** The primary-key column for a table's `Table` gateway: its first declared PK column, or `id`
|
|
205
|
+
* when the table declares none. */
|
|
206
|
+
function primaryKeyOf(t) {
|
|
207
|
+
return t.columns.find((c) => c.primaryKey)?.name ?? "id";
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Emit `domain.ts`: a typed data-object accessor over the App's default datasource, so a worker
|
|
211
|
+
* writes `db.orders.insert({...})` / `db.orders.get(id)` instead of hand-writing SQL (ADR 0029 §6).
|
|
212
|
+
* The generic `Table<T>` runtime lives in `data-sdk.ts`; this generated module only binds each
|
|
213
|
+
* table name to its row type (from `domain-rows.d.ts`) and primary key, so it imports nothing but
|
|
214
|
+
* the sibling SDK (relative) and a type-only `domain-rows.d.ts` — staying dual-runtime and erased at
|
|
215
|
+
* `deno compile`. When the App declares multiple datasources this emits a *keyed*
|
|
216
|
+
* `openDomain<K>(source?)` whose returned accessor is typed against that source's tables; `db.raw`
|
|
217
|
+
* is the per-source escape hatch. Single-source apps keep the zero-arg `openDomain()` concrete form
|
|
218
|
+
* (byte-stable).
|
|
219
|
+
*/
|
|
220
|
+
export function emitDomainBindings(sources, defaultSource) {
|
|
221
|
+
const header = "// AUTO-GENERATED by nanobpmn from the datasource schema (ADR 0029 §4.1/§6).\n" +
|
|
222
|
+
"// The typed data-object layer: `openDomain()` → one `Table<T>` per table, so\n" +
|
|
223
|
+
"// workers manipulate typed rows instead of hand-writing SQL. Do not edit —\n" +
|
|
224
|
+
"// regenerated from the live tables. Erased to plain JS at `deno compile`.\n" +
|
|
225
|
+
"// eslint-disable\n";
|
|
226
|
+
const multi = sources.length > 1;
|
|
227
|
+
const def = defaultSource && sources.some((s) => s.source === defaultSource)
|
|
228
|
+
? defaultSource
|
|
229
|
+
: sources[0]?.source;
|
|
230
|
+
const primary = sources.find((s) => s.source === def);
|
|
231
|
+
const tables = (primary?.tables ?? []).filter((t) => !RESERVED_DOMAIN_MEMBERS.has(t.name));
|
|
232
|
+
// Multi-source apps: emit a *keyed* accessor so `openDomain("analytics")` is typed against that
|
|
233
|
+
// source's tables, not the default's. The row-type spine (`DomainSources`, keyed by alias then
|
|
234
|
+
// wire table name) is already emitted in `domain-rows.d.ts`; here we bind a runtime
|
|
235
|
+
// table-descriptor map per source and a generic `openDomain<K>` that indexes it. Single-source
|
|
236
|
+
// apps keep the zero-arg concrete form below (byte-stable).
|
|
237
|
+
if (multi) {
|
|
238
|
+
return emitKeyedDomainBindings(header, sources, def);
|
|
239
|
+
}
|
|
240
|
+
// Interface names must match `domain-rows.d.ts` exactly (prefixed when multi-source).
|
|
241
|
+
const typeName = (table) => multi ? prefixedInterfaceName(primary.source, table) : interfaceName(table);
|
|
242
|
+
const propKey = (table) => (isIdent(table) ? table : JSON.stringify(table));
|
|
243
|
+
const typeImports = [...new Set(tables.map((t) => typeName(t.name)))].sort();
|
|
244
|
+
const importTypes = typeImports.length > 0
|
|
245
|
+
? `import type { ${typeImports.join(", ")} } from "./${DOMAIN_DTS}";\n`
|
|
246
|
+
: "";
|
|
247
|
+
const fields = tables
|
|
248
|
+
.map((t) => ` readonly ${propKey(t.name)}: Table<${typeName(t.name)}>;`)
|
|
249
|
+
.join("\n");
|
|
250
|
+
const binds = tables
|
|
251
|
+
.map((t) => ` ${propKey(t.name)}: raw.table<${typeName(t.name)}>(` +
|
|
252
|
+
`${JSON.stringify(t.name)}, ${JSON.stringify(primaryKeyOf(t))}),`)
|
|
253
|
+
.join("\n");
|
|
254
|
+
const domainIface = `export interface Domain {\n` +
|
|
255
|
+
` /** The underlying datasource handle — the raw-SQL escape hatch. */\n` +
|
|
256
|
+
` readonly raw: DataSource;\n` +
|
|
257
|
+
(fields ? `${fields}\n` : "") +
|
|
258
|
+
` /** Close the underlying connection. */\n close(): void;\n}`;
|
|
259
|
+
const openFn = `/**\n` +
|
|
260
|
+
` * Open the App's ${multi ? `default (\`${def}\`) ` : ""}datasource as a typed domain: each table is\n` +
|
|
261
|
+
` * a \`Table<T>\` gateway (\`db.orders.insert(...)\`, \`db.orders.get(id)\`), with the\n` +
|
|
262
|
+
` * raw \`DataSource\` at \`db.raw\` for anything the gateway doesn't cover.\n` +
|
|
263
|
+
` */\n` +
|
|
264
|
+
`export async function openDomain(source?: string): Promise<Domain> {\n` +
|
|
265
|
+
` const raw = await openDataSource(source);\n` +
|
|
266
|
+
` return {\n raw,\n` +
|
|
267
|
+
(binds ? `${binds}\n` : "") +
|
|
268
|
+
` close: () => raw.close(),\n };\n}`;
|
|
269
|
+
return `${header}\n` +
|
|
270
|
+
`import { openDataSource, type DataSource, type Table } from "./data-sdk.ts";\n` +
|
|
271
|
+
importTypes +
|
|
272
|
+
`\n${domainIface}\n\n${openFn}\n\nexport type { Table };\n`;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Emit the *keyed* `domain.ts` for a multi-datasource App. `openDomain(source)` is
|
|
276
|
+
* runtime-polymorphic (it opens whichever source you name); here
|
|
277
|
+
* `openDomain<K extends DomainSource>(source?: K)` selects the row-type map for the requested
|
|
278
|
+
* source from `DomainSources` (emitted in `domain-rows.d.ts`), and a runtime per-source
|
|
279
|
+
* table-descriptor map binds the matching `Table<T>` gateways. Zero-arg / default-source calls keep
|
|
280
|
+
* resolving to the default source.
|
|
281
|
+
*/
|
|
282
|
+
function emitKeyedDomainBindings(header, sources, def) {
|
|
283
|
+
const tableDescriptors = sources
|
|
284
|
+
.map((s) => {
|
|
285
|
+
const rows = s.tables
|
|
286
|
+
.filter((t) => !RESERVED_DOMAIN_MEMBERS.has(t.name))
|
|
287
|
+
.map((t) => `{ name: ${JSON.stringify(t.name)}, pk: ${JSON.stringify(primaryKeyOf(t))} }`)
|
|
288
|
+
.join(", ");
|
|
289
|
+
return ` ${JSON.stringify(s.source)}: [${rows}],`;
|
|
290
|
+
})
|
|
291
|
+
.join("\n");
|
|
292
|
+
const runtimeMap = `const DOMAIN_TABLES: Record<string, { name: string; pk: string }[]> = {\n` +
|
|
293
|
+
`${tableDescriptors}\n};\n` +
|
|
294
|
+
`const DEFAULT_SOURCE = ${JSON.stringify(def)};`;
|
|
295
|
+
const types = `/** The App's datasource aliases (ADR 0024). */\n` +
|
|
296
|
+
`export type DomainSource = keyof DomainSources;\n\n` +
|
|
297
|
+
`interface DomainBase {\n` +
|
|
298
|
+
` /** The underlying datasource handle — the raw-SQL escape hatch. */\n` +
|
|
299
|
+
` readonly raw: DataSource;\n` +
|
|
300
|
+
` /** Close the underlying connection. */\n close(): void;\n}\n\n` +
|
|
301
|
+
`type DomainTablesOf<M extends Record<string, object>> = { readonly [K in keyof M as K extends "raw" | "close" ? never : K]: Table<M[K]> };\n\n` +
|
|
302
|
+
`/** A datasource opened as a typed domain: one \`Table<T>\` per table, plus\n` +
|
|
303
|
+
` * \`raw\`/\`close\`. Defaults to the default source (\`${def}\`) when unparameterised. */\n` +
|
|
304
|
+
`export type Domain<M extends Record<string, object> = DomainSources[${JSON.stringify(def)}]> =\n` +
|
|
305
|
+
` DomainBase & DomainTablesOf<M>;`;
|
|
306
|
+
const openFn = `/**\n` +
|
|
307
|
+
` * Open one of the App's datasources as a typed domain. The \`source\` key\n` +
|
|
308
|
+
` * selects that source's tables (\`openDomain("analytics").customers\`); omit it\n` +
|
|
309
|
+
` * for the default source (\`${def}\`). \`db.raw\` is the raw-SQL escape hatch.\n` +
|
|
310
|
+
` */\n` +
|
|
311
|
+
`export async function openDomain<K extends DomainSource = ${JSON.stringify(def)}>(\n` +
|
|
312
|
+
` source?: K,\n` +
|
|
313
|
+
`): Promise<Domain<DomainSources[K]>> {\n` +
|
|
314
|
+
` const key = (source ?? DEFAULT_SOURCE) as string;\n` +
|
|
315
|
+
` const raw = await openDataSource(key);\n` +
|
|
316
|
+
` const db: Record<string, unknown> = { raw, close: () => raw.close() };\n` +
|
|
317
|
+
` for (const t of DOMAIN_TABLES[key] ?? []) db[t.name] = raw.table(t.name, t.pk);\n` +
|
|
318
|
+
` return db as Domain<DomainSources[K]>;\n}`;
|
|
319
|
+
return `${header}\n` +
|
|
320
|
+
`import { openDataSource, type DataSource, type Table } from "./data-sdk.ts";\n` +
|
|
321
|
+
`import type { DomainSources } from "./${DOMAIN_DTS}";\n\n` +
|
|
322
|
+
`${runtimeMap}\n\n${types}\n\n${openFn}\n\nexport type { Table };\n`;
|
|
323
|
+
}
|
|
190
324
|
// --- manifest bridge (code-first, ADR 0054) --------------------------------------------------
|
|
191
325
|
/** Project one manifest type's canonical schema into a `TableMeta` for the row-type spine. Reuses
|
|
192
326
|
* the *same* `tableSchemaForType` the SQL migration uses, so the row types can't drift from the DB. */
|
|
@@ -2,6 +2,9 @@ import type { DerivedArtifact, Deriver } from "../artifact.ts";
|
|
|
2
2
|
import { type ModelSource } from "./worker-io.ts";
|
|
3
3
|
/** Filename the console uses for the derived message-payload map; kept identical (drop-in). */
|
|
4
4
|
export declare const MESSAGE_BINDINGS_DTS = "message-io.d.ts";
|
|
5
|
+
/** The generated typed-`publishMessage` wrapper's basename — re-exports the worker SDK's
|
|
6
|
+
* `publishMessage` with a messageName-keyed typed signature. */
|
|
7
|
+
export declare const MESSAGE_BINDINGS_TS = "messages.ts";
|
|
5
8
|
/** One message-IO entry scanned from a model: a `bpmn:message` name plus the envelope
|
|
6
9
|
* type refs it carries. `inputType` is the received payload (what a `publishMessage`
|
|
7
10
|
* caller sends); `outputType` is reserved. Mirrors the Rust `MessageIo`. */
|
|
@@ -34,3 +37,14 @@ export declare const messagesDeriver: Deriver<{
|
|
|
34
37
|
models: ModelSource[];
|
|
35
38
|
declaredTypeIds: Iterable<string>;
|
|
36
39
|
}>;
|
|
40
|
+
/**
|
|
41
|
+
* The static typed-`publishMessage` wrapper (`messages.ts`). It re-exports the worker SDK's
|
|
42
|
+
* `publishMessage` (and its option/result types) and overrides `publishMessage` with a signature
|
|
43
|
+
* that keys off the `name` string literal: when a message is present in the generated
|
|
44
|
+
* `MessagePayloads` (ADR 0040 slice 2), the call's `variables` are typed from the declared domain
|
|
45
|
+
* type; otherwise they fall back to `MessageVars`. The body is a pass-through — the wire stays
|
|
46
|
+
* untyped JSON. This file never changes with the model, so it is written verbatim (unlike the
|
|
47
|
+
* regenerated `message-io.d.ts`). A byte-faithful port of the console's
|
|
48
|
+
* `emitMessageBindingsRuntime`.
|
|
49
|
+
*/
|
|
50
|
+
export declare function emitMessageBindingsRuntime(): string;
|
|
@@ -9,6 +9,9 @@ import { GENERATED_DIR } from "../artifact.js";
|
|
|
9
9
|
import { byModelPath, DOMAIN_DTS, typeRefFor } from "./worker-io.js";
|
|
10
10
|
/** Filename the console uses for the derived message-payload map; kept identical (drop-in). */
|
|
11
11
|
export const MESSAGE_BINDINGS_DTS = "message-io.d.ts";
|
|
12
|
+
/** The generated typed-`publishMessage` wrapper's basename — re-exports the worker SDK's
|
|
13
|
+
* `publishMessage` with a messageName-keyed typed signature. */
|
|
14
|
+
export const MESSAGE_BINDINGS_TS = "messages.ts";
|
|
12
15
|
const ENVELOPE_IN = "io.nanobpm.dataEnvelope.in";
|
|
13
16
|
const ENVELOPE_OUT = "io.nanobpm.dataEnvelope.out";
|
|
14
17
|
function attr(tag, name) {
|
|
@@ -132,3 +135,37 @@ export const messagesDeriver = {
|
|
|
132
135
|
describe: "Derive message-io.d.ts (bpmn:message name + envelope payload) from BPMN models.",
|
|
133
136
|
derive: ({ models, declaredTypeIds }) => deriveMessageBindings(models, declaredTypeIds),
|
|
134
137
|
};
|
|
138
|
+
/**
|
|
139
|
+
* The static typed-`publishMessage` wrapper (`messages.ts`). It re-exports the worker SDK's
|
|
140
|
+
* `publishMessage` (and its option/result types) and overrides `publishMessage` with a signature
|
|
141
|
+
* that keys off the `name` string literal: when a message is present in the generated
|
|
142
|
+
* `MessagePayloads` (ADR 0040 slice 2), the call's `variables` are typed from the declared domain
|
|
143
|
+
* type; otherwise they fall back to `MessageVars`. The body is a pass-through — the wire stays
|
|
144
|
+
* untyped JSON. This file never changes with the model, so it is written verbatim (unlike the
|
|
145
|
+
* regenerated `message-io.d.ts`). A byte-faithful port of the console's
|
|
146
|
+
* `emitMessageBindingsRuntime`.
|
|
147
|
+
*/
|
|
148
|
+
export function emitMessageBindingsRuntime() {
|
|
149
|
+
return "// AUTO-GENERATED by nanobpmn (ADR 0040 slice 2): the typed `publishMessage`.\n" +
|
|
150
|
+
"// Re-exports the worker SDK's `publishMessage` and overrides it with a\n" +
|
|
151
|
+
"// messageName-keyed typed signature (variables typed from the message's declared\n" +
|
|
152
|
+
"// envelope). Erased to a pass-through at runtime. Do not edit.\n" +
|
|
153
|
+
"// eslint-disable\n\n" +
|
|
154
|
+
`import { publishMessage as publishMessageRaw } from "./worker-sdk.ts";\n` +
|
|
155
|
+
`import type { PublishMessageOptions, PublishMessageResult } from "./worker-sdk.ts";\n` +
|
|
156
|
+
`import type { MessageName, MessagePayloads, MessageVars } from "./${MESSAGE_BINDINGS_DTS}";\n\n` +
|
|
157
|
+
`export type { PublishMessageOptions, PublishMessageResult } from "./worker-sdk.ts";\n\n` +
|
|
158
|
+
`type PayloadFor<K extends MessageName> = K extends keyof MessagePayloads ? MessagePayloads[K] : MessageVars;\n\n` +
|
|
159
|
+
`/**\n` +
|
|
160
|
+
` * Typed \`publishMessage\`: \`name\` is constrained to the model's declared message\n` +
|
|
161
|
+
` * names (\`MessageName\`, ADR 0040 slice 2) so it autocompletes and rejects unknown\n` +
|
|
162
|
+
` * messages, and \`variables\` are typed from the message's declared envelope. A\n` +
|
|
163
|
+
` * declared message with no domain type falls back to MessageVars.\n` +
|
|
164
|
+
` */\n` +
|
|
165
|
+
`export function publishMessage<K extends MessageName>(\n` +
|
|
166
|
+
` name: K,\n` +
|
|
167
|
+
` opts?: PublishMessageOptions<PayloadFor<K> & object>,\n` +
|
|
168
|
+
`): Promise<PublishMessageResult> {\n` +
|
|
169
|
+
` return publishMessageRaw(name, opts as unknown as PublishMessageOptions);\n` +
|
|
170
|
+
`}\n`;
|
|
171
|
+
}
|