@nanobpm/urban 0.25.0 → 0.26.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 +17 -2
- 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 +1 -1
- 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 +10 -0
- package/src/runtime/core/modules/pages.ts +17 -2
- 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
package/dist/toolkit/index.d.ts
CHANGED
|
@@ -2,14 +2,16 @@ export type { DerivedArtifact, Deriver } from "./artifact.ts";
|
|
|
2
2
|
export { GENERATED_DIR, sortArtifacts } from "./artifact.ts";
|
|
3
3
|
export { deriveMigrations, migrationsDeriver, sqlType, createTableSql, tableSchemaForType, groupTypesBySource, } from "./derivers/migrations.ts";
|
|
4
4
|
export type { ToolkitManifest, ToolkitType, ToolkitField, DerivedColumn, DerivedTable, } from "./derivers/migrations.ts";
|
|
5
|
-
export { deriveDomain, emitDomain, emitDomainModel, emitDomainDts, emitDomainDtsForSources, emitDomainTypeRegistry, domainDeriver, sourcesFromManifest, registryFromManifest, sqliteAffinityToTs, interfaceName, } from "./derivers/domain.ts";
|
|
6
|
-
export type { SourceSchema, DomainFieldDef, DomainTypeDef, DomainTypeRegistry, } from "./derivers/domain.ts";
|
|
7
|
-
export {
|
|
5
|
+
export { deriveDomain, emitDomain, emitDomainModel, emitDomainBindings, emitDomainDts, emitDomainDtsForSources, emitDomainTypeRegistry, domainDeriver, isPrimitiveKeyword, sourcesFromManifest, registryFromManifest, sqliteAffinityToTs, interfaceName, DOMAIN_BINDINGS, } from "./derivers/domain.ts";
|
|
6
|
+
export type { SourceSchema, DomainFieldDef, DomainTypeDef, DomainTypeRegistry, ColumnMeta, TableMeta, } from "./derivers/domain.ts";
|
|
7
|
+
export { resolveShapes, columnToField, emitDomainModelJson, DOMAIN_MODEL_JSON, } from "./derivers/shapes.ts";
|
|
8
|
+
export type { ShapeOp, ShapeDecl, ShapeDiagnostic, ShapeResolution, FusedMetaDecl, } from "./derivers/shapes.ts";
|
|
9
|
+
export { deriveWorkerBindings, emitWorkerBindings, emitWorkerBindingsRuntime, overlayDerivedWorkerIo, workerIoDeriver, scanModelWorkers, WORKER_BINDINGS_DTS, WORKER_BINDINGS_TS, DOMAIN_DTS, } from "./derivers/worker-io.ts";
|
|
8
10
|
export type { ModelSource, WorkerIo, WorkerBindingDecl } from "./derivers/worker-io.ts";
|
|
9
11
|
export { byModelPath, typeRefFor } from "./derivers/worker-io.ts";
|
|
10
12
|
export { deriveMeta, emitMeta, foldMeta, metaDeriver, scanModelMeta, META_TS, } from "./derivers/meta.ts";
|
|
11
13
|
export type { MetaDecl } from "./derivers/meta.ts";
|
|
12
|
-
export { deriveMessageBindings, emitMessageBindings, messagesDeriver, scanModelMessages, MESSAGE_BINDINGS_DTS, } from "./derivers/messages.ts";
|
|
14
|
+
export { deriveMessageBindings, emitMessageBindings, emitMessageBindingsRuntime, messagesDeriver, scanModelMessages, MESSAGE_BINDINGS_DTS, MESSAGE_BINDINGS_TS, } from "./derivers/messages.ts";
|
|
13
15
|
export type { MessageIo, MessageBindingDecl } from "./derivers/messages.ts";
|
|
14
16
|
export declare const DERIVERS: readonly [import("./artifact.ts").Deriver<import("./derivers/migrations.ts").ToolkitManifest>, import("./artifact.ts").Deriver<import("./derivers/migrations.ts").ToolkitManifest>, import("./artifact.ts").Deriver<{
|
|
15
17
|
models: import("./derivers/worker-io.ts").ModelSource[];
|
package/dist/toolkit/index.js
CHANGED
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
export { GENERATED_DIR, sortArtifacts } from "./artifact.js";
|
|
6
6
|
// Derivers
|
|
7
7
|
export { deriveMigrations, migrationsDeriver, sqlType, createTableSql, tableSchemaForType, groupTypesBySource, } from "./derivers/migrations.js";
|
|
8
|
-
export { deriveDomain, emitDomain, emitDomainModel, emitDomainDts, emitDomainDtsForSources, emitDomainTypeRegistry, domainDeriver, sourcesFromManifest, registryFromManifest, sqliteAffinityToTs, interfaceName, } from "./derivers/domain.js";
|
|
9
|
-
export {
|
|
8
|
+
export { deriveDomain, emitDomain, emitDomainModel, emitDomainBindings, emitDomainDts, emitDomainDtsForSources, emitDomainTypeRegistry, domainDeriver, isPrimitiveKeyword, sourcesFromManifest, registryFromManifest, sqliteAffinityToTs, interfaceName, DOMAIN_BINDINGS, } from "./derivers/domain.js";
|
|
9
|
+
export { resolveShapes, columnToField, emitDomainModelJson, DOMAIN_MODEL_JSON, } from "./derivers/shapes.js";
|
|
10
|
+
export { deriveWorkerBindings, emitWorkerBindings, emitWorkerBindingsRuntime, overlayDerivedWorkerIo, workerIoDeriver, scanModelWorkers, WORKER_BINDINGS_DTS, WORKER_BINDINGS_TS, DOMAIN_DTS, } from "./derivers/worker-io.js";
|
|
10
11
|
export { byModelPath, typeRefFor } from "./derivers/worker-io.js";
|
|
11
12
|
export { deriveMeta, emitMeta, foldMeta, metaDeriver, scanModelMeta, META_TS, } from "./derivers/meta.js";
|
|
12
|
-
export { deriveMessageBindings, emitMessageBindings, messagesDeriver, scanModelMessages, MESSAGE_BINDINGS_DTS, } from "./derivers/messages.js";
|
|
13
|
+
export { deriveMessageBindings, emitMessageBindings, emitMessageBindingsRuntime, messagesDeriver, scanModelMessages, MESSAGE_BINDINGS_DTS, MESSAGE_BINDINGS_TS, } from "./derivers/messages.js";
|
|
13
14
|
// The registry of all derivers (for discovery / IDE migration).
|
|
14
15
|
import { migrationsDeriver } from "./derivers/migrations.js";
|
|
15
16
|
import { domainDeriver } from "./derivers/domain.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/urban",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"description": "Urban: build and run code-first apps on Nano — runtime, derivation toolkit, and CLI in one. Author durable processes with @nanobpm/workflow (defineFlow), a typed datasource, forms, triggers and surfaces from a nano.app.json manifest, on Node or Deno.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -46,6 +46,11 @@ function expectRecords(value: unknown): Record<string, unknown>[] {
|
|
|
46
46
|
return value;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
function expectString(value: unknown): string {
|
|
50
|
+
if (typeof value !== "string") throw new TypeError("expected a string");
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
|
|
49
54
|
test("sources lists declared datasources and the default", async () => {
|
|
50
55
|
const { dir, cleanup } = await fixture();
|
|
51
56
|
try {
|
|
@@ -225,15 +230,6 @@ test("migrations treats a missing migrations dir as an empty list", async () =>
|
|
|
225
230
|
}
|
|
226
231
|
});
|
|
227
232
|
|
|
228
|
-
test("domaintypes is not yet implemented and errors clearly", async () => {
|
|
229
|
-
const { dir, cleanup } = await fixture();
|
|
230
|
-
try {
|
|
231
|
-
await assert.rejects(run(dir, { op: "domaintypes" }), /not yet implemented/);
|
|
232
|
-
} finally {
|
|
233
|
-
await cleanup();
|
|
234
|
-
}
|
|
235
|
-
});
|
|
236
|
-
|
|
237
233
|
test("an unknown op and an unknown source error", async () => {
|
|
238
234
|
const { dir, cleanup } = await fixture();
|
|
239
235
|
try {
|
|
@@ -304,3 +300,130 @@ test("an absolute --manifest path is honoured as-is (not re-prefixed with root)"
|
|
|
304
300
|
await cleanup();
|
|
305
301
|
}
|
|
306
302
|
});
|
|
303
|
+
|
|
304
|
+
// --- domaintypes: live-schema reification + shape fuse (ADR 0029 §4/§6, ADR 0040 §9/§10) ---
|
|
305
|
+
//
|
|
306
|
+
// The op ported off the console's vendored data_cli (host dry-out nano-bpm#576). It migrates each
|
|
307
|
+
// sqlite source (write mode only), introspects the live schema, folds the manifest `types` + the
|
|
308
|
+
// model's composed shapes through the fuse, and RETURNS every artifact's content (the read-only
|
|
309
|
+
// host never writes files — the caller persists). These lock that contract.
|
|
310
|
+
|
|
311
|
+
test("domaintypes migrates, introspects the live schema, and returns every artifact", async () => {
|
|
312
|
+
const { dir, cleanup } = await fixture();
|
|
313
|
+
try {
|
|
314
|
+
const r = await run(dir, { op: "domaintypes" });
|
|
315
|
+
// Both migrations ran on the shared `_urban_migrations` ledger before introspection.
|
|
316
|
+
assert.deepEqual(r.migrated, { app: ["001_orders.sql", "002_customers.sql"] });
|
|
317
|
+
// Two tables reified from the live schema.
|
|
318
|
+
assert.equal(r.tables, 2);
|
|
319
|
+
// The domain `.d.ts` carries an interface per live table.
|
|
320
|
+
assert.equal(typeof r.text, "string");
|
|
321
|
+
assert.match(expectString(r.text), /export interface Orders\b/);
|
|
322
|
+
assert.match(expectString(r.text), /export interface Customers\b/);
|
|
323
|
+
// Persisting: every `*Path` is the app-relative nano-generated target.
|
|
324
|
+
assert.equal(r.path, "nano-generated/domain-rows.d.ts");
|
|
325
|
+
assert.equal(r.bindingsPath, "nano-generated/domain.ts");
|
|
326
|
+
assert.equal(r.workerBindingsPath, "nano-generated/worker-io.d.ts");
|
|
327
|
+
assert.equal(r.workerRuntimePath, "nano-generated/workers.ts");
|
|
328
|
+
assert.equal(r.messageBindingsPath, "nano-generated/message-io.d.ts");
|
|
329
|
+
assert.equal(r.messageRuntimePath, "nano-generated/messages.ts");
|
|
330
|
+
assert.equal(r.metaPath, "nano-generated/meta.ts");
|
|
331
|
+
assert.equal(r.domainModelPath, "nano-generated/domain.json");
|
|
332
|
+
// The Fused Domain Model is emitted and lists both live tables.
|
|
333
|
+
assert.equal(typeof r.domainModel, "string");
|
|
334
|
+
const model = JSON.parse(expectString(r.domainModel));
|
|
335
|
+
assert.deepEqual(
|
|
336
|
+
model.entities.map((e: { id: string }) => e.id).sort(),
|
|
337
|
+
["app.customers", "app.orders"],
|
|
338
|
+
);
|
|
339
|
+
// The static runtime wrappers are returned verbatim for the caller to write.
|
|
340
|
+
assert.equal(typeof r.workerRuntime, "string");
|
|
341
|
+
assert.equal(typeof r.messageRuntime, "string");
|
|
342
|
+
assert.deepEqual(r.shapeDiagnostics, []);
|
|
343
|
+
} finally {
|
|
344
|
+
await cleanup();
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test("domaintypes write:false is a side-effect-free preview (no migrate, null paths)", async () => {
|
|
349
|
+
const { dir, cleanup } = await fixture();
|
|
350
|
+
try {
|
|
351
|
+
const r = await run(dir, { op: "domaintypes", write: false });
|
|
352
|
+
// The preview never migrates — nothing applied, DB stays fresh.
|
|
353
|
+
assert.deepEqual(r.migrated, {});
|
|
354
|
+
// …and because this fixture's DB is fresh (unmigrated), introspecting the *current* live schema
|
|
355
|
+
// finds no tables. (An existing DB could legitimately report tables here; this count is scoped
|
|
356
|
+
// to the fresh fixture, not a general preview guarantee.)
|
|
357
|
+
assert.equal(r.tables, 0);
|
|
358
|
+
// Content is still returned (the composer preview needs `text`)…
|
|
359
|
+
assert.equal(typeof r.text, "string");
|
|
360
|
+
// …but every persistence target is null and the largest artifact is skipped.
|
|
361
|
+
assert.equal(r.path, null);
|
|
362
|
+
assert.equal(r.bindingsPath, null);
|
|
363
|
+
assert.equal(r.workerBindingsPath, null);
|
|
364
|
+
assert.equal(r.workerRuntimePath, null);
|
|
365
|
+
assert.equal(r.messageBindingsPath, null);
|
|
366
|
+
assert.equal(r.messageRuntimePath, null);
|
|
367
|
+
assert.equal(r.metaPath, null);
|
|
368
|
+
assert.equal(r.domainModelPath, null);
|
|
369
|
+
assert.equal(r.domainModel, null);
|
|
370
|
+
// A second preview did not leave a migrated DB behind for the next write.
|
|
371
|
+
const applied = await run(dir, { op: "migrations" });
|
|
372
|
+
assert.ok(expectRecords(applied.entries).every((e) => e.applied === false));
|
|
373
|
+
} finally {
|
|
374
|
+
await cleanup();
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
test("domaintypes folds a composed shape into DomainTypes and reports its diagnostics", async () => {
|
|
379
|
+
const { dir, cleanup } = await fixture();
|
|
380
|
+
try {
|
|
381
|
+
const r = await run(dir, {
|
|
382
|
+
op: "domaintypes",
|
|
383
|
+
derivedShapes: [
|
|
384
|
+
{
|
|
385
|
+
id: "ReviewRound",
|
|
386
|
+
ops: [
|
|
387
|
+
{ op: "extend", name: "prUrl", type: "string" },
|
|
388
|
+
{ op: "extend", name: "approved", type: "boolean" },
|
|
389
|
+
],
|
|
390
|
+
},
|
|
391
|
+
// A broken shape: unresolved extend type → error diagnostic, omitted from the emit.
|
|
392
|
+
{ id: "Broken", ops: [{ op: "extend", name: "ref", type: "NoSuchType" }] },
|
|
393
|
+
],
|
|
394
|
+
});
|
|
395
|
+
// The good shape reifies into the `DomainTypes` registry surfaced by the domain `.d.ts`.
|
|
396
|
+
assert.match(expectString(r.text), /ReviewRound/);
|
|
397
|
+
assert.doesNotMatch(expectString(r.text), /Broken/);
|
|
398
|
+
// The broken shape's diagnostic is returned so the panel can surface it.
|
|
399
|
+
const diags = expectRecords(r.shapeDiagnostics);
|
|
400
|
+
assert.equal(diags.length, 1);
|
|
401
|
+
assert.equal(diags[0].shape, "Broken");
|
|
402
|
+
assert.equal(diags[0].severity, "error");
|
|
403
|
+
} finally {
|
|
404
|
+
await cleanup();
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
test("domaintypes emits worker/message bindings from the injected model IO maps", async () => {
|
|
409
|
+
const { dir, cleanup } = await fixture();
|
|
410
|
+
try {
|
|
411
|
+
const r = await run(dir, {
|
|
412
|
+
op: "domaintypes",
|
|
413
|
+
derivedShapes: [
|
|
414
|
+
{ id: "Order", ops: [{ op: "extend", name: "total", type: "integer" }] },
|
|
415
|
+
],
|
|
416
|
+
derivedWorkers: [
|
|
417
|
+
{ taskType: "review", inputType: "Order", outputType: "Order", headerKeys: ["priority"] },
|
|
418
|
+
],
|
|
419
|
+
derivedMessages: [{ messageName: "approved", inputType: "Order" }],
|
|
420
|
+
});
|
|
421
|
+
// The worker binding names the task type and threads the declared header key.
|
|
422
|
+
assert.match(expectString(r.workerBindings), /review/);
|
|
423
|
+
assert.match(expectString(r.workerBindings), /priority/);
|
|
424
|
+
// The message binding names the message.
|
|
425
|
+
assert.match(expectString(r.messageBindings), /approved/);
|
|
426
|
+
} finally {
|
|
427
|
+
await cleanup();
|
|
428
|
+
}
|
|
429
|
+
});
|
|
@@ -14,9 +14,10 @@
|
|
|
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
|
|
|
21
22
|
import type { HostContext, SqliteDb } from "../host.ts";
|
|
22
23
|
import { isRecord } from "../guards.ts";
|
|
@@ -25,11 +26,48 @@ import { loadManifest } from "../manifest.ts";
|
|
|
25
26
|
import { validateManifest } from "../validate.ts";
|
|
26
27
|
import { makeGateway, type DataSource as GatewayDataSource } from "./gateway.ts";
|
|
27
28
|
import { applyMigrations, MIGRATIONS_TABLE, openSqliteSource, resolveAppPath } from "./datasource.ts";
|
|
29
|
+
// The `domaintypes` op composes the domain reifier from the pure toolkit derivers (single source of
|
|
30
|
+
// truth for the IDE's codegen, host dry-out nano-bpm#576). Imported directly from the deriver files
|
|
31
|
+
// (not the `toolkit/` barrel, which pulls the Node fs IO) so core stays free of `node:*`/Deno; the
|
|
32
|
+
// derivers' only core dependency is the gateway's `TableMeta`/`ColumnMeta` *types*, which erase.
|
|
33
|
+
import { GENERATED_DIR } from "../../../toolkit/artifact.ts";
|
|
34
|
+
import {
|
|
35
|
+
DOMAIN_BINDINGS,
|
|
36
|
+
type DomainTypeDef,
|
|
37
|
+
type DomainTypeRegistry,
|
|
38
|
+
emitDomainBindings,
|
|
39
|
+
emitDomainModel,
|
|
40
|
+
registryFromManifest,
|
|
41
|
+
type SourceSchema,
|
|
42
|
+
} from "../../../toolkit/derivers/domain.ts";
|
|
43
|
+
import {
|
|
44
|
+
DOMAIN_MODEL_JSON,
|
|
45
|
+
emitDomainModelJson,
|
|
46
|
+
type FusedMetaDecl,
|
|
47
|
+
resolveShapes,
|
|
48
|
+
type ShapeDecl,
|
|
49
|
+
} from "../../../toolkit/derivers/shapes.ts";
|
|
50
|
+
import {
|
|
51
|
+
DOMAIN_DTS,
|
|
52
|
+
emitWorkerBindings,
|
|
53
|
+
emitWorkerBindingsRuntime,
|
|
54
|
+
overlayDerivedWorkerIo,
|
|
55
|
+
WORKER_BINDINGS_DTS,
|
|
56
|
+
WORKER_BINDINGS_TS,
|
|
57
|
+
type WorkerBindingDecl,
|
|
58
|
+
} from "../../../toolkit/derivers/worker-io.ts";
|
|
59
|
+
import {
|
|
60
|
+
emitMessageBindings,
|
|
61
|
+
emitMessageBindingsRuntime,
|
|
62
|
+
MESSAGE_BINDINGS_DTS,
|
|
63
|
+
MESSAGE_BINDINGS_TS,
|
|
64
|
+
type MessageBindingDecl,
|
|
65
|
+
} from "../../../toolkit/derivers/messages.ts";
|
|
66
|
+
import { emitMeta, META_TS, type MetaDecl } from "../../../toolkit/derivers/meta.ts";
|
|
28
67
|
|
|
29
|
-
/** The DB-manager op protocol's known op set
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* type-safety and catches typos at compile time. */
|
|
68
|
+
/** The DB-manager op protocol's known op set — every op (including `domaintypes`) is dispatched by
|
|
69
|
+
* `runDataOp`. Typing `op` as this union (instead of `string`) gives SDK/console callers type-safety
|
|
70
|
+
* and catches typos at compile time. */
|
|
33
71
|
export type DataOp =
|
|
34
72
|
| "sources"
|
|
35
73
|
| "schema"
|
|
@@ -46,6 +84,27 @@ export interface DataRequest {
|
|
|
46
84
|
sql?: string;
|
|
47
85
|
params?: unknown[];
|
|
48
86
|
statements?: string[];
|
|
87
|
+
/**
|
|
88
|
+
* `domaintypes`: persist the derived artifacts (default true). A `write:false` request is the
|
|
89
|
+
* latency-sensitive composer preview — it introspects WITHOUT migrating the DB first and returns
|
|
90
|
+
* only the artifact *content* (never a DB side-effect per keystroke). `urban data` never writes
|
|
91
|
+
* files itself (the host is read-only); the caller persists the returned content when `write` is
|
|
92
|
+
* not false. This flag only gates the pre-introspection migration side-effect.
|
|
93
|
+
*/
|
|
94
|
+
write?: boolean;
|
|
95
|
+
/** `domaintypes`: the model-derived worker-IO map (`taskType → {in,out,headerKeys}`), scanned
|
|
96
|
+
* from the process models by the caller. Overlaid on the manifest `workers[]` (authoritative for
|
|
97
|
+
* the envelope). Absent → the manifest projection alone. */
|
|
98
|
+
derivedWorkers?: WorkerBindingDecl[];
|
|
99
|
+
/** `domaintypes`: the model-derived message-payload map (`messageName → {in,out}`), scanned from
|
|
100
|
+
* the models' `bpmn:message` envelopes. Authoritative (no manifest projection). */
|
|
101
|
+
derivedMessages?: MessageBindingDecl[];
|
|
102
|
+
/** `domaintypes`: the model-derived composed shapes (`nano:shape`). Each is resolved through the
|
|
103
|
+
* fuse into a flat domain type folded into the `types` registry. Absent → no shapes. */
|
|
104
|
+
derivedShapes?: ShapeDecl[];
|
|
105
|
+
/** `domaintypes`: the model-derived model-level metadata (`nano:meta`). Folded into `meta.ts` and
|
|
106
|
+
* the structured `domain.json`. Absent → no metadata. */
|
|
107
|
+
derivedMeta?: MetaDecl[];
|
|
49
108
|
}
|
|
50
109
|
|
|
51
110
|
/** One datasource the manifest declares, resolved against the environment (env templates in
|
|
@@ -281,10 +340,120 @@ export async function runDataOp(
|
|
|
281
340
|
}
|
|
282
341
|
}
|
|
283
342
|
case "domaintypes": {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
343
|
+
// ADR 0029 §4.1/§4.2/§6 + ADR 0040 §9/§10: reify the domain model from the *live* schema.
|
|
344
|
+
// Union every declared datasource (the table spine), fold in the manifest `types` registry and
|
|
345
|
+
// the model's composed `nano:shape`s (resolved through the fuse), and derive the worker/message
|
|
346
|
+
// typed accessors. Unlike the vendored data_cli, this op NEVER writes files (the host is
|
|
347
|
+
// read-only): it returns every artifact's content and the caller persists it. `write:false` is
|
|
348
|
+
// the latency-sensitive composer preview — it skips the pre-introspection migration so a
|
|
349
|
+
// debounced keystroke has no DB side-effect and pays only for `text` + `shapeDiagnostics`.
|
|
350
|
+
const { default: def, sources } = listSources(manifest);
|
|
351
|
+
const persist = req.write !== false;
|
|
352
|
+
// Introspect each sqlite source's live schema. When persisting, migrate first (the domain
|
|
353
|
+
// model is derived from `gw.schema()`, so a regen that raced ahead of `migrate` on a fresh DB
|
|
354
|
+
// would emit an empty `Domain`); the migration runs on the shared `_urban_migrations` ledger.
|
|
355
|
+
const migrated: Record<string, string[]> = {};
|
|
356
|
+
const schemas: SourceSchema[] = [];
|
|
357
|
+
for (const s of sources) {
|
|
358
|
+
if (String(s.driver) !== "sqlite") continue; // only sqlite is introspectable (see openGateway)
|
|
359
|
+
const { gw, db } = await openGateway(host, root, manifest, s.name);
|
|
360
|
+
try {
|
|
361
|
+
if (persist) {
|
|
362
|
+
const applied = await applyMigrations(
|
|
363
|
+
host,
|
|
364
|
+
db,
|
|
365
|
+
root,
|
|
366
|
+
s.migrations ?? DEFAULT_MIGRATIONS_DIR,
|
|
367
|
+
);
|
|
368
|
+
if (applied.length) migrated[s.name] = applied;
|
|
369
|
+
}
|
|
370
|
+
schemas.push({ source: s.name, tables: await gw.schema() });
|
|
371
|
+
} finally {
|
|
372
|
+
db.close();
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const types = registryFromManifest(manifest);
|
|
377
|
+
// Snapshot the manifest `types` *before* folding the resolved shapes so `domain.json` can tag
|
|
378
|
+
// each entity with its true provenance (manifest vs model) — once folded they are
|
|
379
|
+
// indistinguishable in `types`.
|
|
380
|
+
const manifestTypesSnapshot: DomainTypeRegistry = { ...types };
|
|
381
|
+
// Composed motion shapes (ADR 0040 §9/§10): resolve each `nano:shape` against the leaf fuse
|
|
382
|
+
// into a flat domain type and fold it into the registry *before* it feeds the domain model and
|
|
383
|
+
// the worker/message bindings, so a composed shape becomes a first-class `DomainTypes` entry.
|
|
384
|
+
// Broken shapes are omitted; their diagnostics are returned so the panel can surface them.
|
|
385
|
+
const shapeResolution = resolveShapes(req.derivedShapes ?? [], types, schemas);
|
|
386
|
+
const shapeDeclById = new Map((req.derivedShapes ?? []).map((s) => [s.id, s]));
|
|
387
|
+
const shapeEntities: { decl: ShapeDecl; def: DomainTypeDef }[] = [];
|
|
388
|
+
for (const [id, shapeDef] of Object.entries(shapeResolution.types)) {
|
|
389
|
+
types[id] = shapeDef;
|
|
390
|
+
shapeEntities.push({ decl: shapeDeclById.get(id) ?? { id, ops: [] }, def: shapeDef });
|
|
391
|
+
}
|
|
392
|
+
const declaredIds = Object.keys(types);
|
|
393
|
+
|
|
394
|
+
const text = emitDomainModel(schemas, def, types);
|
|
395
|
+
const bindings = emitDomainBindings(schemas, def);
|
|
396
|
+
const derivedMeta: MetaDecl[] = req.derivedMeta ?? [];
|
|
397
|
+
const metaAccessor = emitMeta(derivedMeta);
|
|
398
|
+
// Worker-IO map (ADR 0033 §3): the model is authoritative for the envelope, so an injected
|
|
399
|
+
// `derivedWorkers` is overlaid on the manifest `workers[]` (which still carries non-IO
|
|
400
|
+
// bindings such as `llm`); absent, the manifest projection stands alone.
|
|
401
|
+
const manifestWorkers: WorkerBindingDecl[] = (manifest.workers ?? []).map((w) => ({
|
|
402
|
+
taskType: w.taskType,
|
|
403
|
+
inputType: w.inputType,
|
|
404
|
+
outputType: w.outputType,
|
|
405
|
+
}));
|
|
406
|
+
const workers = req.derivedWorkers
|
|
407
|
+
? overlayDerivedWorkerIo(manifestWorkers, req.derivedWorkers)
|
|
408
|
+
: manifestWorkers;
|
|
409
|
+
const workerBindings = emitWorkerBindings(workers, declaredIds);
|
|
410
|
+
const workerRuntime = emitWorkerBindingsRuntime();
|
|
411
|
+
// Message-payload registry (ADR 0040 slice 2): no manifest projection, so the model-derived
|
|
412
|
+
// map is authoritative and emitted directly.
|
|
413
|
+
const messages = req.derivedMessages ?? [];
|
|
414
|
+
const messageBindings = emitMessageBindings(messages, declaredIds);
|
|
415
|
+
const messageRuntime = emitMessageBindingsRuntime();
|
|
416
|
+
// The structured Fused Domain Model (`domain.json`, ADR 0040 §1): the largest artifact, so it
|
|
417
|
+
// is built only when persisting (the preview consumes only `text` + `shapeDiagnostics`).
|
|
418
|
+
const meta: FusedMetaDecl[] = derivedMeta;
|
|
419
|
+
const domainModel = persist
|
|
420
|
+
? emitDomainModelJson({
|
|
421
|
+
sources: schemas,
|
|
422
|
+
default: def,
|
|
423
|
+
manifestTypes: manifestTypesSnapshot,
|
|
424
|
+
shapes: shapeEntities,
|
|
425
|
+
meta,
|
|
426
|
+
diagnostics: shapeResolution.diagnostics,
|
|
427
|
+
})
|
|
428
|
+
: null;
|
|
429
|
+
|
|
430
|
+
const rel = (name: string): string => `${GENERATED_DIR}/${name}`;
|
|
431
|
+
const tables = schemas.reduce((n, s) => n + s.tables.length, 0);
|
|
432
|
+
// The returned `*Path` fields are the app-relative targets the caller writes to when
|
|
433
|
+
// persisting (null on a preview). `urban data` returns content only — persisting is the
|
|
434
|
+
// caller's job (host dry-out nano-bpm#576). The static runtime wrappers (`workers.ts`,
|
|
435
|
+
// `messages.ts`) are returned too so the caller can write them verbatim.
|
|
436
|
+
return {
|
|
437
|
+
path: persist ? rel(DOMAIN_DTS) : null,
|
|
438
|
+
text,
|
|
439
|
+
tables,
|
|
440
|
+
bindingsPath: persist ? rel(DOMAIN_BINDINGS) : null,
|
|
441
|
+
bindings,
|
|
442
|
+
workerBindingsPath: persist ? rel(WORKER_BINDINGS_DTS) : null,
|
|
443
|
+
workerBindings,
|
|
444
|
+
workerRuntimePath: persist ? rel(WORKER_BINDINGS_TS) : null,
|
|
445
|
+
workerRuntime,
|
|
446
|
+
messageBindingsPath: persist ? rel(MESSAGE_BINDINGS_DTS) : null,
|
|
447
|
+
messageBindings,
|
|
448
|
+
messageRuntimePath: persist ? rel(MESSAGE_BINDINGS_TS) : null,
|
|
449
|
+
messageRuntime,
|
|
450
|
+
metaPath: persist ? rel(META_TS) : null,
|
|
451
|
+
meta: metaAccessor,
|
|
452
|
+
domainModelPath: persist ? rel(DOMAIN_MODEL_JSON) : null,
|
|
453
|
+
domainModel,
|
|
454
|
+
migrated,
|
|
455
|
+
shapeDiagnostics: shapeResolution.diagnostics,
|
|
456
|
+
};
|
|
288
457
|
}
|
|
289
458
|
default:
|
|
290
459
|
throw new Error(`unknown op "${req.op}"`);
|
|
@@ -86,12 +86,22 @@ test("GET / serves the renderer shell with the home marker", async () => {
|
|
|
86
86
|
assert.equal(res.status, 200);
|
|
87
87
|
assert.match(res.headers?.["content-type"] ?? "", /text\/html/);
|
|
88
88
|
assert.match(res.body ?? "", /data-home="home"/);
|
|
89
|
+
// The runtime module is loaded by a *relative* src so it resolves against the
|
|
90
|
+
// document's mount path — works both at the origin root and under the Nano
|
|
91
|
+
// console's /console/app-view/<name>/ reverse proxy. A root-absolute
|
|
92
|
+
// "/app/runtime.js" would 404 against the console origin and never hydrate.
|
|
93
|
+
assert.match(res.body ?? "", /src="\.\/app\/runtime\.js"/);
|
|
94
|
+
assert.doesNotMatch(res.body ?? "", /src="\/app\/runtime\.js"/);
|
|
89
95
|
});
|
|
90
96
|
|
|
91
97
|
test("GET /app/runtime.js serves the renderer module", async () => {
|
|
92
98
|
const res = await dispatch("GET", "/app/runtime.js");
|
|
93
99
|
assert.match(res.headers?.["content-type"] ?? "", /javascript/);
|
|
94
100
|
assert.match(res.body ?? "", /pc:refresh/);
|
|
101
|
+
// Endpoints are rebased against the served module URL so the app works under a
|
|
102
|
+
// path-prefixed reverse proxy (Nano console embed), not just at the origin root.
|
|
103
|
+
assert.match(res.body ?? "", /new URL\("\.\.\/", import\.meta\.url\)/);
|
|
104
|
+
assert.match(res.body ?? "", /function apiUrl\(u\)/);
|
|
95
105
|
});
|
|
96
106
|
|
|
97
107
|
test("renderer preserves grid row-detail expansion across refreshes", async () => {
|
|
@@ -348,7 +348,7 @@ function rendererShell(homePage: string): string {
|
|
|
348
348
|
</head>
|
|
349
349
|
<body>
|
|
350
350
|
<main id="page" data-home="${escapeAttr(homePage)}"><p class="pc-empty">Loading…</p></main>
|
|
351
|
-
<script type="module" src="
|
|
351
|
+
<script type="module" src="./app/runtime.js"></script>
|
|
352
352
|
</body>
|
|
353
353
|
</html>`;
|
|
354
354
|
}
|
|
@@ -404,8 +404,23 @@ const RENDERER_JS = String.raw`
|
|
|
404
404
|
const root = document.getElementById("page");
|
|
405
405
|
const HOME = root.dataset.home || "home";
|
|
406
406
|
|
|
407
|
+
// Resolve every app endpoint against where THIS module was actually served from,
|
|
408
|
+
// so absolute-looking "/app/…" paths work both at the origin root (direct run,
|
|
409
|
+
// e.g. the CLI on :3000) and under a path-prefixed reverse proxy (the Nano
|
|
410
|
+
// console embeds the app at /console/app-view/<name>/). "../" drops the trailing
|
|
411
|
+
// "app/runtime.js" to land on the app's mount root (always ends in "/").
|
|
412
|
+
const APP_BASE = new URL("../", import.meta.url);
|
|
413
|
+
function apiUrl(u) {
|
|
414
|
+
// Rebase root-absolute app paths ("/app/…") onto the mount root. Leave a
|
|
415
|
+
// protocol-relative ("//host/…") or otherwise non-root-relative string alone —
|
|
416
|
+
// only a single leading slash denotes an app path we own.
|
|
417
|
+
return typeof u === "string" && u.startsWith("/") && !u.startsWith("//")
|
|
418
|
+
? new URL(u.slice(1), APP_BASE).toString()
|
|
419
|
+
: u;
|
|
420
|
+
}
|
|
421
|
+
|
|
407
422
|
async function getJSON(url, opts) {
|
|
408
|
-
const r = await fetch(url, opts);
|
|
423
|
+
const r = await fetch(apiUrl(url), opts);
|
|
409
424
|
const body = await r.json().catch(() => ({}));
|
|
410
425
|
if (!r.ok) throw new Error(body.error || ("HTTP " + r.status));
|
|
411
426
|
return body;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { emitDomainBindings } from "./domain.ts";
|
|
4
|
+
import { emitWorkerBindings, emitWorkerBindingsRuntime } from "./worker-io.ts";
|
|
5
|
+
import { emitMessageBindings, emitMessageBindingsRuntime } from "./messages.ts";
|
|
6
|
+
|
|
7
|
+
// Byte-exact goldens for the emitters ported into the toolkit for the domaintypes op (host dry-out
|
|
8
|
+
// nano-bpm#576). Captured verbatim from the console's own emitters via Deno; these lock the toolkit
|
|
9
|
+
// output to the console byte-for-byte so the two can never drift while both exist.
|
|
10
|
+
const GOLDENS: Record<string, string> = JSON.parse("{\"bindings\":\"// AUTO-GENERATED by nanobpmn from the datasource schema (ADR 0029 §4.1/§6).\\n// The typed data-object layer: `openDomain()` → one `Table<T>` per table, so\\n// workers manipulate typed rows instead of hand-writing SQL. Do not edit —\\n// regenerated from the live tables. Erased to plain JS at `deno compile`.\\n// eslint-disable\\n\\nimport { openDataSource, type DataSource, type Table } from \\\"./data-sdk.ts\\\";\\nimport type { Customers, Orders } from \\\"./domain-rows.d.ts\\\";\\n\\nexport interface Domain {\\n /** The underlying datasource handle — the raw-SQL escape hatch. */\\n readonly raw: DataSource;\\n readonly orders: Table<Orders>;\\n readonly customers: Table<Customers>;\\n /** Close the underlying connection. */\\n close(): void;\\n}\\n\\n/**\\n * Open the App's datasource as a typed domain: each table is\\n * a `Table<T>` gateway (`db.orders.insert(...)`, `db.orders.get(id)`), with the\\n * raw `DataSource` at `db.raw` for anything the gateway doesn't cover.\\n */\\nexport async function openDomain(source?: string): Promise<Domain> {\\n const raw = await openDataSource(source);\\n return {\\n raw,\\n orders: raw.table<Orders>(\\\"orders\\\", \\\"id\\\"),\\n customers: raw.table<Customers>(\\\"customers\\\", \\\"id\\\"),\\n close: () => raw.close(),\\n };\\n}\\n\\nexport type { Table };\\n\",\"workerRuntime\":\"// AUTO-GENERATED by nanobpmn (ADR 0033 §3): the typed `defineWorker`.\\n// Re-exports the worker SDK and overrides `defineWorker` with a taskType-keyed\\n// typed signature (job.variables, job.customHeaders + result typed from the\\n// worker's declared input/output/header contract). Erased to a pass-through at\\n// runtime. Do not edit.\\n// eslint-disable\\n\\nimport { defineWorker as defineWorkerRaw } from \\\"./worker-sdk.ts\\\";\\nimport type { WorkerOptions } from \\\"./worker-sdk.ts\\\";\\nimport type { WorkerInputs, WorkerOutputs, WorkerHeaders, WorkerTaskType, WorkerVars, WorkerHdrs } from \\\"./worker-io.d.ts\\\";\\n\\nexport * from \\\"./worker-sdk.ts\\\";\\n\\ntype InFor<K extends WorkerTaskType> = K extends keyof WorkerInputs ? WorkerInputs[K] : WorkerVars;\\ntype OutFor<K extends WorkerTaskType> = K extends keyof WorkerOutputs ? WorkerOutputs[K] : WorkerVars;\\ntype HdrFor<K extends WorkerTaskType> = K extends keyof WorkerHeaders ? WorkerHeaders[K] : WorkerHdrs;\\n\\n/**\\n * Typed `defineWorker`: `type` is constrained to the model's declared job types\\n * (`WorkerTaskType`, ADR 0033 §3) so it autocompletes and rejects unknown jobs,\\n * and the handler's `job.variables`, `job.customHeaders` + result are typed from\\n * the worker's declared `inputType`/`outputType`/header keys. A declared job type\\n * with no declared type falls back to WorkerVars / WorkerHdrs.\\n */\\nexport function defineWorker<K extends WorkerTaskType>(\\n opts: { type: K } & WorkerOptions<InFor<K> & object, OutFor<K> & object, HdrFor<K> & object>,\\n): void {\\n defineWorkerRaw(opts as unknown as WorkerOptions);\\n}\\n\",\"messageRuntime\":\"// AUTO-GENERATED by nanobpmn (ADR 0040 slice 2): the typed `publishMessage`.\\n// Re-exports the worker SDK's `publishMessage` and overrides it with a\\n// messageName-keyed typed signature (variables typed from the message's declared\\n// envelope). Erased to a pass-through at runtime. Do not edit.\\n// eslint-disable\\n\\nimport { publishMessage as publishMessageRaw } from \\\"./worker-sdk.ts\\\";\\nimport type { PublishMessageOptions, PublishMessageResult } from \\\"./worker-sdk.ts\\\";\\nimport type { MessageName, MessagePayloads, MessageVars } from \\\"./message-io.d.ts\\\";\\n\\nexport type { PublishMessageOptions, PublishMessageResult } from \\\"./worker-sdk.ts\\\";\\n\\ntype PayloadFor<K extends MessageName> = K extends keyof MessagePayloads ? MessagePayloads[K] : MessageVars;\\n\\n/**\\n * Typed `publishMessage`: `name` is constrained to the model's declared message\\n * names (`MessageName`, ADR 0040 slice 2) so it autocompletes and rejects unknown\\n * messages, and `variables` are typed from the message's declared envelope. A\\n * declared message with no domain type falls back to MessageVars.\\n */\\nexport function publishMessage<K extends MessageName>(\\n name: K,\\n opts?: PublishMessageOptions<PayloadFor<K> & object>,\\n): Promise<PublishMessageResult> {\\n return publishMessageRaw(name, opts as unknown as PublishMessageOptions);\\n}\\n\",\"workerBindingsWithHeaders\":\"// AUTO-GENERATED by nanobpmn from the App manifest (ADR 0033 §3).\\n// The bridge from the process model to the worker type system: each declared\\n// worker's `taskType` maps to the TS type of its input payload (`job.variables`)\\n// and result, so the typed `defineWorker` types a handler by its job type. Do\\n// not edit — regenerated from the manifest. Erased to plain JS at compile.\\n// eslint-disable\\n\\nimport type { DomainTypes } from \\\"./domain-rows.d.ts\\\";\\n\\n/** Untyped fallback for a job whose worker declares no input/output type. */\\nexport type WorkerVars = Record<string, unknown>;\\n\\n/** Untyped fallback for a job whose worker declares no custom headers. */\\nexport type WorkerHdrs = Record<string, unknown>;\\n\\n/** Every declared worker `taskType` (ADR 0033 §3): the model-derived set the\\n * typed `defineWorker` accepts, so `type` autocompletes and rejects unknown jobs. */\\nexport type WorkerTaskType = \\\"review\\\";\\n\\n/** Input payload (`job.variables`) per declared worker, keyed by `taskType`. */\\nexport interface WorkerInputs {\\n \\\"review\\\": DomainTypes[\\\"Order\\\"];\\n}\\n\\n/** Output payload (worker result) per declared worker, keyed by `taskType`. */\\nexport interface WorkerOutputs {\\n \\\"review\\\": DomainTypes[\\\"Order\\\"];\\n}\\n\\n/** Custom headers (`job.customHeaders`) per declared worker, keyed by `taskType`.\\n * Header values are strings on the wire, so each declared key maps to `string`;\\n * the extra index signature keeps undeclared headers accessible (non-breaking). */\\nexport interface WorkerHeaders {\\n \\\"review\\\": { \\\"priority\\\": string; \\\"x-flag\\\": string; [key: string]: string };\\n}\\n\",\"messageBindings\":\"// AUTO-GENERATED by nanobpmn from the process model (ADR 0040 slice 2).\\n// The bridge from a `bpmn:message`'s data envelope to the message type system:\\n// each named message maps to the TS type of its payload, so the typed\\n// `publishMessage` types a call by its message name. Do not edit — regenerated\\n// from the model. Erased to plain JS at compile.\\n// eslint-disable\\n\\nimport type { DomainTypes } from \\\"./domain-rows.d.ts\\\";\\n\\n/** Untyped fallback payload for a message that declares no envelope type. */\\nexport type MessageVars = Record<string, unknown>;\\n\\n/** Every declared message `name` (ADR 0040 slice 2): the model-derived set the\\n * typed `publishMessage` accepts, so `name` autocompletes and rejects unknown messages. */\\nexport type MessageName = \\\"approved\\\";\\n\\n/** Payload (`variables`) per declared message, keyed by `name`. */\\nexport interface MessagePayloads {\\n \\\"approved\\\": DomainTypes[\\\"Order\\\"];\\n}\\n\"}\n");
|
|
11
|
+
|
|
12
|
+
const sources = [
|
|
13
|
+
{
|
|
14
|
+
source: "app",
|
|
15
|
+
tables: [
|
|
16
|
+
{
|
|
17
|
+
name: "orders",
|
|
18
|
+
columns: [
|
|
19
|
+
{ name: "id", type: "INTEGER", notNull: true, primaryKey: true },
|
|
20
|
+
{ name: "total", type: "INTEGER", notNull: true, primaryKey: false },
|
|
21
|
+
],
|
|
22
|
+
indexes: [],
|
|
23
|
+
foreignKeys: [],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: "customers",
|
|
27
|
+
columns: [
|
|
28
|
+
{ name: "id", type: "INTEGER", notNull: true, primaryKey: true },
|
|
29
|
+
{ name: "name", type: "TEXT", notNull: true, primaryKey: false },
|
|
30
|
+
{ name: "order_id", type: "INTEGER", notNull: false, primaryKey: false },
|
|
31
|
+
],
|
|
32
|
+
indexes: [],
|
|
33
|
+
foreignKeys: [{ column: "order_id", refTable: "orders", refColumn: "id", onDelete: "" }],
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
test("emitDomainBindings matches the console byte-for-byte", () => {
|
|
40
|
+
assert.equal(emitDomainBindings(sources, "app"), GOLDENS.bindings);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("emitWorkerBindingsRuntime matches the console byte-for-byte", () => {
|
|
44
|
+
assert.equal(emitWorkerBindingsRuntime(), GOLDENS.workerRuntime);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("emitMessageBindingsRuntime matches the console byte-for-byte", () => {
|
|
48
|
+
assert.equal(emitMessageBindingsRuntime(), GOLDENS.messageRuntime);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("emitWorkerBindings carries headerKeys through WorkerHeaders (console parity)", () => {
|
|
52
|
+
assert.equal(
|
|
53
|
+
emitWorkerBindings(
|
|
54
|
+
[{ taskType: "review", inputType: "Order", outputType: "Order", headerKeys: ["priority", "x-flag"] }],
|
|
55
|
+
["Order"],
|
|
56
|
+
),
|
|
57
|
+
GOLDENS.workerBindingsWithHeaders,
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("emitMessageBindings matches the console byte-for-byte", () => {
|
|
62
|
+
assert.equal(
|
|
63
|
+
emitMessageBindings([{ messageName: "approved", inputType: "Order" }], ["Order"]),
|
|
64
|
+
GOLDENS.messageBindings,
|
|
65
|
+
);
|
|
66
|
+
});
|