@nanobpm/urban 0.16.1 → 0.17.1

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.
Files changed (42) hide show
  1. package/dist/runtime/core/manifest.d.ts +4 -134
  2. package/dist/runtime/core/manifest.js +11 -6
  3. package/dist/runtime/core/modules/triggers.d.ts +14 -2
  4. package/dist/runtime/core/modules/triggers.js +0 -6
  5. package/dist/runtime/core/validate.js +11 -10
  6. package/dist/runtime/index.d.ts +1 -1
  7. package/dist/toolkit/derivers/domain.d.ts +69 -0
  8. package/dist/toolkit/derivers/domain.js +243 -0
  9. package/dist/toolkit/derivers/messages.d.ts +36 -0
  10. package/dist/toolkit/derivers/messages.js +134 -0
  11. package/dist/toolkit/derivers/meta.d.ts +34 -0
  12. package/dist/toolkit/derivers/meta.js +106 -0
  13. package/dist/toolkit/derivers/migrations.d.ts +34 -1
  14. package/dist/toolkit/derivers/migrations.js +37 -8
  15. package/dist/toolkit/derivers/worker-io.d.ts +14 -0
  16. package/dist/toolkit/derivers/worker-io.js +13 -2
  17. package/dist/toolkit/gen.js +8 -2
  18. package/dist/toolkit/index.d.ts +15 -3
  19. package/dist/toolkit/index.js +9 -2
  20. package/package.json +3 -2
  21. package/src/runtime/core/manifest.test.ts +2 -3
  22. package/src/runtime/core/manifest.ts +35 -134
  23. package/src/runtime/core/modules/triggers.test.ts +5 -3
  24. package/src/runtime/core/modules/triggers.ts +18 -4
  25. package/src/runtime/core/modules/workers.ts +2 -2
  26. package/src/runtime/core/validate.ts +11 -10
  27. package/src/runtime/index.ts +7 -4
  28. package/src/toolkit/derivers/domain.test.ts +140 -0
  29. package/src/toolkit/derivers/domain.ts +317 -0
  30. package/src/toolkit/derivers/messages.test.ts +174 -0
  31. package/src/toolkit/derivers/messages.ts +166 -0
  32. package/src/toolkit/derivers/meta.test.ts +140 -0
  33. package/src/toolkit/derivers/meta.ts +128 -0
  34. package/src/toolkit/derivers/migrations.test.ts +42 -4
  35. package/src/toolkit/derivers/migrations.ts +61 -9
  36. package/src/toolkit/derivers/worker-io.test.ts +10 -0
  37. package/src/toolkit/derivers/worker-io.ts +14 -2
  38. package/src/toolkit/gen.test.ts +8 -1
  39. package/src/toolkit/gen.ts +8 -2
  40. package/src/toolkit/index.ts +53 -2
  41. package/dist/runtime/schema/nano-app.schema.json +0 -503
  42. package/src/runtime/schema/nano-app.schema.json +0 -503
@@ -0,0 +1,134 @@
1
+ // Deriver: BPMN models → `message-io.d.ts`, the message-payload type map (ADR 0040 slice 2).
2
+ // Scans each process for `bpmn:message` envelopes and maps each message name to its declared
3
+ // payload type. It emits the SAME `message-io.d.ts` the console generates (a faithful port of
4
+ // `emitMessageBindings` in the server's domain_types.ts), so the toolkit is a drop-in for the
5
+ // IDE's codegen (ADR 0053). The static typed-`publishMessage` wrapper (`messages.ts`) never
6
+ // changes with the model, so — like the worker SDK — it is a package/runtime concern, not a
7
+ // derived artifact; only the model-derived `message-io.d.ts` is emitted here.
8
+ import { GENERATED_DIR } from "../artifact.js";
9
+ import { byModelPath, DOMAIN_DTS, typeRefFor } from "./worker-io.js";
10
+ /** Filename the console uses for the derived message-payload map; kept identical (drop-in). */
11
+ export const MESSAGE_BINDINGS_DTS = "message-io.d.ts";
12
+ const ENVELOPE_IN = "io.nanobpm.dataEnvelope.in";
13
+ const ENVELOPE_OUT = "io.nanobpm.dataEnvelope.out";
14
+ function attr(tag, name) {
15
+ const m = tag.match(new RegExp(`\\b${name}\\s*=\\s*"([^"]*)"`));
16
+ return m ? m[1] : undefined;
17
+ }
18
+ /** An envelope ref is trimmed; an empty/whitespace value contributes nothing (mirrors the
19
+ * Rust `envelope_ref`). A message name, by contrast, is a correlation identity matched
20
+ * verbatim by the engine, so it is NOT trimmed — only an absent/empty name is dropped. */
21
+ function envelopeRef(value) {
22
+ const v = value?.trim();
23
+ return v ? v : undefined;
24
+ }
25
+ /** Scan one BPMN document for `bpmn:message` envelopes. Handles both a self-closing
26
+ * `<bpmn:message name=.. />` (name only — completes the `MessageName` union) and an open
27
+ * message whose body carries the `io.nanobpm.dataEnvelope.in/out` `zeebe:property`. */
28
+ export function scanModelMessages(xml) {
29
+ const out = [];
30
+ const blockRe = /<bpmn:message\b([^>]*?)(?:\/>|>([\s\S]*?)<\/bpmn:message>)/g;
31
+ let m;
32
+ while ((m = blockRe.exec(xml)) !== null) {
33
+ const name = attr(`<x ${m[1]}>`, "name");
34
+ if (name == null || name.length === 0)
35
+ continue;
36
+ const io = { messageName: name };
37
+ const body = m[2] ?? "";
38
+ const propRe = /<zeebe:property\b[^>]*>/g;
39
+ let p;
40
+ while ((p = propRe.exec(body)) !== null) {
41
+ const pname = attr(p[0], "name");
42
+ if (pname === ENVELOPE_IN)
43
+ io.inputType = envelopeRef(attr(p[0], "value"));
44
+ else if (pname === ENVELOPE_OUT)
45
+ io.outputType = envelopeRef(attr(p[0], "value"));
46
+ }
47
+ out.push(io);
48
+ }
49
+ return out;
50
+ }
51
+ // --- Faithful port of the console's emitMessageBindings (domain_types.ts) so the emitted
52
+ // message-io.d.ts is byte-compatible with the IDE's own codegen. ---
53
+ /** Emit `message-io.d.ts` from message bindings + the set of declared domain type ids.
54
+ * Each named message joins the `MessageName` union; a message whose envelope names a
55
+ * declared type also maps to it in `MessagePayloads` (else falls back to `MessageVars`). */
56
+ export function emitMessageBindings(messages, declaredTypeIds) {
57
+ const declared = new Set(declaredTypeIds);
58
+ const propKey = (m) => JSON.stringify(m);
59
+ const names = [
60
+ ...new Set(messages
61
+ .map((m) => m?.messageName)
62
+ .filter((n) => typeof n === "string" && n.length > 0)),
63
+ ];
64
+ const nameUnion = names.length > 0
65
+ ? names.map((n) => JSON.stringify(n)).join(" | ")
66
+ : "string";
67
+ const payloads = [];
68
+ for (const m of messages) {
69
+ if (typeof m?.messageName !== "string" || m.messageName.length === 0)
70
+ continue;
71
+ const ref = typeRefFor(m.inputType, declared);
72
+ if (ref)
73
+ payloads.push(` ${propKey(m.messageName)}: ${ref};`);
74
+ }
75
+ const header = "// AUTO-GENERATED by nanobpmn from the process model (ADR 0040 slice 2).\n" +
76
+ "// The bridge from a `bpmn:message`'s data envelope to the message type system:\n" +
77
+ "// each named message maps to the TS type of its payload, so the typed\n" +
78
+ "// `publishMessage` types a call by its message name. Do not edit — regenerated\n" +
79
+ "// from the model. Erased to plain JS at compile.\n" +
80
+ "// eslint-disable\n";
81
+ const needsRegistry = payloads.length > 0;
82
+ const importTypes = needsRegistry
83
+ ? `import type { DomainTypes } from "./${DOMAIN_DTS}";\n`
84
+ : "";
85
+ const payloadsIface = payloads.length > 0
86
+ ? `export interface MessagePayloads {\n${payloads.join("\n")}\n}\n`
87
+ : `export interface MessagePayloads {}\n`;
88
+ return `${header}\n` +
89
+ importTypes +
90
+ `\n/** Untyped fallback payload for a message that declares no envelope type. */\n` +
91
+ `export type MessageVars = Record<string, unknown>;\n\n` +
92
+ `/** Every declared message \`name\` (ADR 0040 slice 2): the model-derived set the\n` +
93
+ ` * typed \`publishMessage\` accepts, so \`name\` autocompletes and rejects unknown messages. */\n` +
94
+ `export type MessageName = ${nameUnion};\n\n` +
95
+ `/** Payload (\`variables\`) per declared message, keyed by \`name\`. */\n` +
96
+ payloadsIface;
97
+ }
98
+ /**
99
+ * Derive `nano-generated/message-io.d.ts` from BPMN models. Messages are aggregated the way
100
+ * the console's Rust scan does (a `BTreeMap` keyed by name): deduped by name, sorted by name,
101
+ * with the envelope type refs merged first-wins across models — so the derived output matches
102
+ * the host's `derivedMessages` for the same models. `declaredTypeIds` is the manifest `types`
103
+ * key set; a message only gets a typed payload when its envelope names a declared type.
104
+ */
105
+ export function deriveMessageBindings(models, declaredTypeIds = []) {
106
+ const byName = new Map();
107
+ for (const model of [...models].sort(byModelPath)) {
108
+ for (const msg of scanModelMessages(model.xml)) {
109
+ const existing = byName.get(msg.messageName);
110
+ if (!existing) {
111
+ byName.set(msg.messageName, {
112
+ messageName: msg.messageName,
113
+ inputType: msg.inputType,
114
+ outputType: msg.outputType,
115
+ });
116
+ }
117
+ else {
118
+ // First-wins merge of the envelope refs (mirrors the Rust `fill`).
119
+ if (existing.inputType == null)
120
+ existing.inputType = msg.inputType;
121
+ if (existing.outputType == null)
122
+ existing.outputType = msg.outputType;
123
+ }
124
+ }
125
+ }
126
+ const messages = [...byName.values()].sort((a, b) => a.messageName < b.messageName ? -1 : a.messageName > b.messageName ? 1 : 0);
127
+ const content = emitMessageBindings(messages, declaredTypeIds);
128
+ return [{ path: `${GENERATED_DIR}/${MESSAGE_BINDINGS_DTS}`, content }];
129
+ }
130
+ export const messagesDeriver = {
131
+ id: "model->message-io",
132
+ describe: "Derive message-io.d.ts (bpmn:message name + envelope payload) from BPMN models.",
133
+ derive: ({ models, declaredTypeIds }) => deriveMessageBindings(models, declaredTypeIds),
134
+ };
@@ -0,0 +1,34 @@
1
+ import type { DerivedArtifact, Deriver } from "../artifact.ts";
2
+ import { type ModelSource } from "./worker-io.ts";
3
+ /** Basename the console uses for the generated accessor; kept identical (drop-in). */
4
+ export declare const META_TS = "meta.ts";
5
+ /** One model-level metadata entry (`nano:meta`), tagged with its defining process for
6
+ * the `model:<processId>` provenance (ADR 0040 §5). `process` is carried for the fuse;
7
+ * the emitted `meta.ts` folds on key/value only. */
8
+ export interface MetaDecl {
9
+ process?: string;
10
+ key: string;
11
+ value: string;
12
+ }
13
+ /** Scan one BPMN document for model-level `nano:meta` key/value entries, in document
14
+ * order, tagged with the enclosing process. A meta with no `key` attribute is ignored;
15
+ * a missing `value` folds to the empty string (matches envelope_scan.rs). */
16
+ export declare function scanModelMeta(xml: string): MetaDecl[];
17
+ /** Fold the scanned model-level metadata into a single key→value record. When two models
18
+ * declare the same key the last (scan order) wins. Empty keys are ignored. The accumulator
19
+ * is a **null-prototype** dict so user-authored keys (including `__proto__`) round-trip
20
+ * deterministically as own data properties. */
21
+ export declare function foldMeta(metas: MetaDecl[]): Record<string, string>;
22
+ /** Emit `meta.ts`: the typed model-metadata accessor (`@nanobpm/meta`, ADR 0040 §5).
23
+ * `AppMeta` keys the declared metadata as string-literal properties; `appMeta` is the
24
+ * concrete null-prototype record; the overloaded `meta()` returns the typed value for a
25
+ * known key and `string | undefined` for a dynamic one. Strip-safe (ADR 0036). */
26
+ export declare function emitMeta(metas: MetaDecl[]): string;
27
+ /**
28
+ * Derive `nano-generated/meta.ts` from BPMN models. Deterministic: models are scanned in
29
+ * path order, then their `nano:meta` entries folded (last write wins) into the accessor.
30
+ */
31
+ export declare function deriveMeta(models: ModelSource[]): DerivedArtifact[];
32
+ export declare const metaDeriver: Deriver<{
33
+ models: ModelSource[];
34
+ }>;
@@ -0,0 +1,106 @@
1
+ // Deriver: BPMN models → `meta.ts`, the typed model-metadata accessor (`@nanobpm/meta`,
2
+ // ADR 0040 §5). Scans each process for `nano:meta` key/value siblings of the `nano:shapes`
3
+ // container and folds them into a single app-wide record. It emits the SAME `meta.ts` the
4
+ // console generates (a faithful port of `foldMeta`/`emitMeta` in the server's domain_types.ts),
5
+ // so the toolkit is a drop-in for the IDE's codegen (ADR 0053).
6
+ import { GENERATED_DIR } from "../artifact.js";
7
+ import { byModelPath } from "./worker-io.js";
8
+ /** Basename the console uses for the generated accessor; kept identical (drop-in). */
9
+ export const META_TS = "meta.ts";
10
+ function attr(tag, name) {
11
+ const m = tag.match(new RegExp(`\\b${name}\\s*=\\s*"([^"]*)"`));
12
+ return m ? m[1] : undefined;
13
+ }
14
+ function processId(xml) {
15
+ const m = xml.match(/<[\w.-]*:?process\b[^>]*>/);
16
+ return m ? attr(m[0], "id") : undefined;
17
+ }
18
+ // A `nano:shape` carrier (`<...:shape>…</...:shape>`, and self-closing) — meta is only
19
+ // recognised *outside* a shape carrier (its op children, e.g. `extend`, are a different
20
+ // vocabulary), so strip carriers before matching `nano:meta` (mirrors envelope_scan.rs).
21
+ const SHAPE_BLOCK_RE = /<([\w.-]*:)?shape\b[^>]*(?:\/>|>[\s\S]*?<\/\1?shape\s*>)/g;
22
+ // A `nano:meta` element (any namespace prefix; matches the Rust local-name scan).
23
+ const META_RE = /<([\w.-]*:)?meta\b([^>]*?)\/?>/g;
24
+ /** Scan one BPMN document for model-level `nano:meta` key/value entries, in document
25
+ * order, tagged with the enclosing process. A meta with no `key` attribute is ignored;
26
+ * a missing `value` folds to the empty string (matches envelope_scan.rs). */
27
+ export function scanModelMeta(xml) {
28
+ const proc = processId(xml);
29
+ const withoutShapes = xml.replace(SHAPE_BLOCK_RE, "");
30
+ const out = [];
31
+ let m;
32
+ while ((m = META_RE.exec(withoutShapes)) !== null) {
33
+ const key = attr(`<x ${m[2]}>`, "key");
34
+ if (key == null)
35
+ continue;
36
+ out.push({ process: proc, key, value: attr(`<x ${m[2]}>`, "value") ?? "" });
37
+ }
38
+ return out;
39
+ }
40
+ // --- Faithful port of the console's foldMeta/emitMeta (domain_types.ts) so the emitted
41
+ // meta.ts is byte-compatible with the IDE's own codegen. ---
42
+ function isIdent(name) {
43
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name);
44
+ }
45
+ /** Fold the scanned model-level metadata into a single key→value record. When two models
46
+ * declare the same key the last (scan order) wins. Empty keys are ignored. The accumulator
47
+ * is a **null-prototype** dict so user-authored keys (including `__proto__`) round-trip
48
+ * deterministically as own data properties. */
49
+ export function foldMeta(metas) {
50
+ const out = Object.create(null);
51
+ for (const m of metas) {
52
+ const key = (m?.key ?? "").trim();
53
+ if (!key)
54
+ continue;
55
+ out[key] = m.value ?? "";
56
+ }
57
+ return out;
58
+ }
59
+ /** Emit `meta.ts`: the typed model-metadata accessor (`@nanobpm/meta`, ADR 0040 §5).
60
+ * `AppMeta` keys the declared metadata as string-literal properties; `appMeta` is the
61
+ * concrete null-prototype record; the overloaded `meta()` returns the typed value for a
62
+ * known key and `string | undefined` for a dynamic one. Strip-safe (ADR 0036). */
63
+ export function emitMeta(metas) {
64
+ const folded = foldMeta(metas);
65
+ const keys = Object.keys(folded);
66
+ const propKey = (k) => (isIdent(k) ? k : JSON.stringify(k));
67
+ const header = "// AUTO-GENERATED by nanobpmn from the process model (ADR 0040 §5): the typed\n" +
68
+ "// model-metadata accessor. Each `nano:meta` key/value the models declare becomes\n" +
69
+ "// a checked `AppMeta` property, so `meta(key)` is typed and app-wide queryable\n" +
70
+ "// without a second store. Do not edit — regenerated from the model.\n" +
71
+ "// eslint-disable\n\n";
72
+ const iface = keys.length > 0
73
+ ? `export interface AppMeta {\n${keys.map((k) => ` ${propKey(k)}: string;`).join("\n")}\n}\n`
74
+ : `export interface AppMeta {}\n`;
75
+ const constDecl = keys.length > 0
76
+ ? `export const appMeta: AppMeta = (() => {\n` +
77
+ ` const m: Record<string, string> = Object.create(null);\n` +
78
+ keys.map((k) => ` m[${JSON.stringify(k)}] = ${JSON.stringify(folded[k])};`).join("\n") +
79
+ `\n return m as AppMeta;\n})();\n`
80
+ : `export const appMeta: AppMeta = Object.create(null) as AppMeta;\n`;
81
+ const accessor = `\n/** Read a model-level metadata value. A declared key is typed; a dynamic key\n` +
82
+ ` * returns \`string | undefined\`. */\n` +
83
+ `export function meta<K extends keyof AppMeta>(key: K): AppMeta[K];\n` +
84
+ `export function meta(key: string): string | undefined;\n` +
85
+ `export function meta(key: string): string | undefined {\n` +
86
+ ` return (appMeta as Record<string, string>)[key];\n` +
87
+ `}\n`;
88
+ return `${header}/** Every model-level metadata key the App's models declare (ADR 0040 §5). */\n` +
89
+ iface + "\n" + constDecl + accessor;
90
+ }
91
+ /**
92
+ * Derive `nano-generated/meta.ts` from BPMN models. Deterministic: models are scanned in
93
+ * path order, then their `nano:meta` entries folded (last write wins) into the accessor.
94
+ */
95
+ export function deriveMeta(models) {
96
+ const metas = [];
97
+ for (const model of [...models].sort(byModelPath)) {
98
+ metas.push(...scanModelMeta(model.xml));
99
+ }
100
+ return [{ path: `${GENERATED_DIR}/${META_TS}`, content: emitMeta(metas) }];
101
+ }
102
+ export const metaDeriver = {
103
+ id: "model->meta",
104
+ describe: "Derive meta.ts (the typed @nanobpm/meta accessor) from BPMN nano:meta entries.",
105
+ derive: ({ models }) => deriveMeta(models),
106
+ };
@@ -2,6 +2,8 @@ import type { DerivedArtifact, Deriver } from "../artifact.ts";
2
2
  export interface ToolkitField {
3
3
  type?: string;
4
4
  optional?: boolean;
5
+ /** Marks a repeated field: the domain row-type projects it as `T[]` in the `DomainTypes` registry. */
6
+ list?: boolean;
5
7
  }
6
8
  export interface ToolkitType {
7
9
  table?: string;
@@ -20,7 +22,38 @@ export interface ToolkitManifest {
20
22
  /** Map a domain field type to a SQLite column type. Unknown types fall back to TEXT. */
21
23
  export declare function sqlType(t: string | undefined): string;
22
24
  export declare function assertSqlIdent(kind: string, name: string): string;
23
- /** Build the CREATE TABLE statement for one type. */
25
+ /** Double-quote an identifier for DDL. `assertSqlIdent` already rejects non-identifier
26
+ * chars (the injection guard), but quoting is what makes valid-but-reserved words
27
+ * (`order`, `group`, `select`, …) legal SQL — no reserved-word denylist to maintain.
28
+ * Embedded double quotes are doubled per the SQL standard (defensive; can't occur here). */
29
+ export declare function quoteSqlIdent(name: string): string;
30
+ /** A column in a manifest type's canonical table schema. `sqlType` is the SQLite
31
+ * declared type (the affinity source for both the CREATE TABLE and the row types). */
32
+ export interface DerivedColumn {
33
+ name: string;
34
+ sqlType: string;
35
+ notNull: boolean;
36
+ primaryKey: boolean;
37
+ }
38
+ /** The canonical table schema for one manifest type. */
39
+ export interface DerivedTable {
40
+ table: string;
41
+ columns: DerivedColumn[];
42
+ }
43
+ /**
44
+ * Derive the canonical table schema for one manifest type: an implicit
45
+ * autoincrement `id` primary key plus one column per declared field. This is the
46
+ * single source of truth both the SQL migration (`createTableSql`) and the domain
47
+ * row-types (`domain.ts`) derive from, so the schema can never drift between them.
48
+ */
49
+ export declare function tableSchemaForType(typeName: string, def: ToolkitType): DerivedTable;
50
+ /** Group manifest types by their datasource, preserving declaration order within each
51
+ * source. Shared by the migrations and domain derivers so both partition and order
52
+ * sources identically (a single source of truth for the source spine). */
53
+ export declare function groupTypesBySource(manifest: ToolkitManifest): Map<string, Array<[string, ToolkitType]>>;
54
+ /** Build the CREATE TABLE statement for one type, from its canonical schema. Table and
55
+ * column identifiers are quoted so valid-but-reserved names (`order`, `group`, …) emit
56
+ * legal SQL. */
24
57
  export declare function createTableSql(typeName: string, def: ToolkitType): string;
25
58
  export declare function deriveMigrations(manifest: ToolkitManifest): DerivedArtifact[];
26
59
  export declare const migrationsDeriver: Deriver<ToolkitManifest>;
@@ -31,25 +31,40 @@ export function assertSqlIdent(kind, name) {
31
31
  }
32
32
  return name;
33
33
  }
34
- /** Build the CREATE TABLE statement for one type. */
35
- export function createTableSql(typeName, def) {
34
+ /** Double-quote an identifier for DDL. `assertSqlIdent` already rejects non-identifier
35
+ * chars (the injection guard), but quoting is what makes valid-but-reserved words
36
+ * (`order`, `group`, `select`, …) legal SQL — no reserved-word denylist to maintain.
37
+ * Embedded double quotes are doubled per the SQL standard (defensive; can't occur here). */
38
+ export function quoteSqlIdent(name) {
39
+ return `"${name.replace(/"/g, '""')}"`;
40
+ }
41
+ /**
42
+ * Derive the canonical table schema for one manifest type: an implicit
43
+ * autoincrement `id` primary key plus one column per declared field. This is the
44
+ * single source of truth both the SQL migration (`createTableSql`) and the domain
45
+ * row-types (`domain.ts`) derive from, so the schema can never drift between them.
46
+ */
47
+ export function tableSchemaForType(typeName, def) {
36
48
  const table = assertSqlIdent("table name", def.table ?? typeName);
37
- const cols = [" id INTEGER PRIMARY KEY AUTOINCREMENT"];
49
+ const columns = [
50
+ { name: "id", sqlType: "INTEGER", notNull: false, primaryKey: true },
51
+ ];
38
52
  const fields = def.fields ?? {};
39
53
  for (const [name, f] of Object.entries(fields)) {
40
54
  assertSqlIdent("field name", name);
41
55
  if (name === "id") {
42
56
  throw new Error(`type "${typeName}": field "id" is reserved (the table's implicit primary key)`);
43
57
  }
44
- const notNull = f.optional ? "" : " NOT NULL";
45
- cols.push(` ${name} ${sqlType(f.type)}${notNull}`);
58
+ columns.push({ name, sqlType: sqlType(f.type), notNull: !f.optional, primaryKey: false });
46
59
  }
47
- return `CREATE TABLE IF NOT EXISTS ${table} (\n${cols.join(",\n")}\n);`;
60
+ return { table, columns };
48
61
  }
49
- export function deriveMigrations(manifest) {
62
+ /** Group manifest types by their datasource, preserving declaration order within each
63
+ * source. Shared by the migrations and domain derivers so both partition and order
64
+ * sources identically (a single source of truth for the source spine). */
65
+ export function groupTypesBySource(manifest) {
50
66
  const types = manifest.types ?? {};
51
67
  const defaultSource = manifest.data?.default ?? "app";
52
- // Group types by their datasource.
53
68
  const bySource = new Map();
54
69
  for (const [name, def] of Object.entries(types)) {
55
70
  const src = def.source ?? defaultSource;
@@ -57,6 +72,20 @@ export function deriveMigrations(manifest) {
57
72
  list.push([name, def]);
58
73
  bySource.set(src, list);
59
74
  }
75
+ return bySource;
76
+ }
77
+ /** Build the CREATE TABLE statement for one type, from its canonical schema. Table and
78
+ * column identifiers are quoted so valid-but-reserved names (`order`, `group`, …) emit
79
+ * legal SQL. */
80
+ export function createTableSql(typeName, def) {
81
+ const { table, columns } = tableSchemaForType(typeName, def);
82
+ const cols = columns.map((c) => c.primaryKey
83
+ ? ` ${quoteSqlIdent(c.name)} ${c.sqlType} PRIMARY KEY AUTOINCREMENT`
84
+ : ` ${quoteSqlIdent(c.name)} ${c.sqlType}${c.notNull ? " NOT NULL" : ""}`);
85
+ return `CREATE TABLE IF NOT EXISTS ${quoteSqlIdent(table)} (\n${cols.join(",\n")}\n);`;
86
+ }
87
+ export function deriveMigrations(manifest) {
88
+ const bySource = groupTypesBySource(manifest);
60
89
  const artifacts = [];
61
90
  for (const [src, list] of bySource) {
62
91
  const header = `-- Derived from manifest \`types\` by @nanobpm/urban (urban gen). Do not edit.\n` +
@@ -4,6 +4,16 @@ export interface ModelSource {
4
4
  path: string;
5
5
  xml: string;
6
6
  }
7
+ /** Deterministic comparator for ordering `ModelSource` lists by path. Returns 0 on equal
8
+ * paths so it satisfies the JS sort contract (a comparator that reports both `a>b` and
9
+ * `b>a` can reorder equal elements unpredictably) — keeping the derivers' cross-model
10
+ * fold order stable even if a manifest pattern yields duplicate paths. Shared by every
11
+ * model-scanning deriver so there is one canonical ordering. */
12
+ export declare function byModelPath(a: {
13
+ path: string;
14
+ }, b: {
15
+ path: string;
16
+ }): number;
7
17
  export interface WorkerIo {
8
18
  taskType: string;
9
19
  elementId?: string;
@@ -19,6 +29,10 @@ export declare const WORKER_BINDINGS_DTS = "worker-io.d.ts";
19
29
  export declare const DOMAIN_DTS = "domain-rows.d.ts";
20
30
  /** Scan one BPMN document for service-task worker I/O. */
21
31
  export declare function scanModelWorkers(xml: string): WorkerIo[];
32
+ /** A `DomainTypes[...]` ref for a declared type id (else undefined). The one canonical
33
+ * ref helper shared by the worker- and message-IO emitters (matches the console's single
34
+ * `typeRefFor` in domain_types.ts), so both stay identical to the host codegen. */
35
+ export declare function typeRefFor(id: string | undefined, declared: Set<string>): string | undefined;
22
36
  export interface WorkerBindingDecl {
23
37
  taskType?: string;
24
38
  inputType?: string;
@@ -4,6 +4,14 @@
4
4
  // `worker-io.d.ts` the console generates (a faithful port of `emitWorkerBindings` in the
5
5
  // server's domain_types.ts), so the toolkit is a drop-in for the IDE's codegen (ADR 0053).
6
6
  import { GENERATED_DIR } from "../artifact.js";
7
+ /** Deterministic comparator for ordering `ModelSource` lists by path. Returns 0 on equal
8
+ * paths so it satisfies the JS sort contract (a comparator that reports both `a>b` and
9
+ * `b>a` can reorder equal elements unpredictably) — keeping the derivers' cross-model
10
+ * fold order stable even if a manifest pattern yields duplicate paths. Shared by every
11
+ * model-scanning deriver so there is one canonical ordering. */
12
+ export function byModelPath(a, b) {
13
+ return a.path < b.path ? -1 : a.path > b.path ? 1 : 0;
14
+ }
7
15
  /** Filename the console uses; kept identical so the artifact is a drop-in. */
8
16
  export const WORKER_BINDINGS_DTS = "worker-io.d.ts";
9
17
  /** The domain type declarations `worker-io.d.ts` imports from (console: domain-rows.d.ts). */
@@ -51,7 +59,10 @@ export function scanModelWorkers(xml) {
51
59
  }
52
60
  // --- Faithful port of the console's emitWorkerBindings (domain_types.ts) so the emitted
53
61
  // worker-io.d.ts is byte-compatible with the IDE's own codegen. ---
54
- function typeRefFor(id, declared) {
62
+ /** A `DomainTypes[...]` ref for a declared type id (else undefined). The one canonical
63
+ * ref helper shared by the worker- and message-IO emitters (matches the console's single
64
+ * `typeRefFor` in domain_types.ts), so both stay identical to the host codegen. */
65
+ export function typeRefFor(id, declared) {
55
66
  return id != null && declared.has(id) ? `DomainTypes[${JSON.stringify(id)}]` : undefined;
56
67
  }
57
68
  /** Emit `worker-io.d.ts` from worker bindings + the set of declared domain type ids. */
@@ -120,7 +131,7 @@ export function emitWorkerBindings(workers, declaredTypeIds) {
120
131
  */
121
132
  export function deriveWorkerBindings(models, declaredTypeIds = []) {
122
133
  const workers = [];
123
- for (const model of [...models].sort((a, b) => (a.path < b.path ? -1 : 1))) {
134
+ for (const model of [...models].sort(byModelPath)) {
124
135
  for (const w of scanModelWorkers(model.xml)) {
125
136
  workers.push({ taskType: w.taskType, inputType: w.in, outputType: w.out });
126
137
  }
@@ -4,7 +4,10 @@
4
4
  // pure; all IO is confined here behind a tiny FS port so the same code runs on Node and Deno.
5
5
  import { sortArtifacts } from "./artifact.js";
6
6
  import { deriveMigrations } from "./derivers/migrations.js";
7
+ import { deriveDomain } from "./derivers/domain.js";
7
8
  import { deriveWorkerBindings } from "./derivers/worker-io.js";
9
+ import { deriveMeta } from "./derivers/meta.js";
10
+ import { deriveMessageBindings } from "./derivers/messages.js";
8
11
  function join(root, rel) {
9
12
  // Trim either separator so callers may pass Windows-style paths; GenIO
10
13
  // implementations accept forward slashes on all platforms.
@@ -47,15 +50,18 @@ export async function collectArtifacts(opts) {
47
50
  const manifestPath = join(root, opts.manifestFile ?? "nano.app.json");
48
51
  const manifest = JSON.parse(await io.readText(manifestPath));
49
52
  const artifacts = [];
50
- // 1. types → migrations
53
+ // 1. types → migrations + domain row types (DomainTables spine + DomainTypes registry)
51
54
  if (manifest.types && Object.keys(manifest.types).length > 0) {
52
55
  artifacts.push(...deriveMigrations(manifest));
56
+ artifacts.push(...deriveDomain(manifest));
53
57
  }
54
- // 2. models → worker I/O index
58
+ // 2. models → worker I/O index + model-metadata accessor + message payload map
55
59
  const models = await readModels(root, io, manifest);
56
60
  if (models.length > 0) {
57
61
  const declaredTypeIds = Object.keys(manifest.types ?? {});
58
62
  artifacts.push(...deriveWorkerBindings(models, declaredTypeIds));
63
+ artifacts.push(...deriveMeta(models));
64
+ artifacts.push(...deriveMessageBindings(models, declaredTypeIds));
59
65
  }
60
66
  return sortArtifacts(artifacts);
61
67
  }
@@ -1,10 +1,22 @@
1
1
  export type { DerivedArtifact, Deriver } from "./artifact.ts";
2
2
  export { GENERATED_DIR, sortArtifacts } from "./artifact.ts";
3
- export { deriveMigrations, migrationsDeriver, sqlType, createTableSql, } from "./derivers/migrations.ts";
4
- export type { ToolkitManifest, ToolkitType, ToolkitField } from "./derivers/migrations.ts";
3
+ export { deriveMigrations, migrationsDeriver, sqlType, createTableSql, tableSchemaForType, groupTypesBySource, } from "./derivers/migrations.ts";
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";
5
7
  export { deriveWorkerBindings, emitWorkerBindings, workerIoDeriver, scanModelWorkers, WORKER_BINDINGS_DTS, DOMAIN_DTS, } from "./derivers/worker-io.ts";
6
8
  export type { ModelSource, WorkerIo, WorkerBindingDecl } from "./derivers/worker-io.ts";
7
- export declare const DERIVERS: readonly [import("./artifact.ts").Deriver<import("./derivers/migrations.ts").ToolkitManifest>, import("./artifact.ts").Deriver<{
9
+ export { byModelPath, typeRefFor } from "./derivers/worker-io.ts";
10
+ export { deriveMeta, emitMeta, foldMeta, metaDeriver, scanModelMeta, META_TS, } from "./derivers/meta.ts";
11
+ export type { MetaDecl } from "./derivers/meta.ts";
12
+ export { deriveMessageBindings, emitMessageBindings, messagesDeriver, scanModelMessages, MESSAGE_BINDINGS_DTS, } from "./derivers/messages.ts";
13
+ export type { MessageIo, MessageBindingDecl } from "./derivers/messages.ts";
14
+ 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
+ models: import("./derivers/worker-io.ts").ModelSource[];
16
+ declaredTypeIds: Iterable<string>;
17
+ }>, import("./artifact.ts").Deriver<{
18
+ models: import("./derivers/worker-io.ts").ModelSource[];
19
+ }>, import("./artifact.ts").Deriver<{
8
20
  models: import("./derivers/worker-io.ts").ModelSource[];
9
21
  declaredTypeIds: Iterable<string>;
10
22
  }>];
@@ -4,12 +4,19 @@
4
4
  // (ADR 0054) — the toolkit derives migrations and worker I/O from the model.
5
5
  export { GENERATED_DIR, sortArtifacts } from "./artifact.js";
6
6
  // Derivers
7
- export { deriveMigrations, migrationsDeriver, sqlType, createTableSql, } from "./derivers/migrations.js";
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";
8
9
  export { deriveWorkerBindings, emitWorkerBindings, workerIoDeriver, scanModelWorkers, WORKER_BINDINGS_DTS, DOMAIN_DTS, } from "./derivers/worker-io.js";
10
+ export { byModelPath, typeRefFor } from "./derivers/worker-io.js";
11
+ 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";
9
13
  // The registry of all derivers (for discovery / IDE migration).
10
14
  import { migrationsDeriver } from "./derivers/migrations.js";
15
+ import { domainDeriver } from "./derivers/domain.js";
11
16
  import { workerIoDeriver } from "./derivers/worker-io.js";
12
- export const DERIVERS = [migrationsDeriver, workerIoDeriver];
17
+ import { metaDeriver } from "./derivers/meta.js";
18
+ import { messagesDeriver } from "./derivers/messages.js";
19
+ export const DERIVERS = [migrationsDeriver, domainDeriver, workerIoDeriver, metaDeriver, messagesDeriver];
13
20
  // Gen orchestrator + IO
14
21
  export { collectArtifacts, runGen, joinPath, readModels } from "./gen.js";
15
22
  export { createNodeGenIO } from "./fsio.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/urban",
3
- "version": "0.16.1",
3
+ "version": "0.17.1",
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",
@@ -55,13 +55,14 @@
55
55
  "README.md"
56
56
  ],
57
57
  "scripts": {
58
- "build": "tsc -p tsconfig.build.json && node -e \"const fs=require('fs');fs.mkdirSync('dist/runtime/schema',{recursive:true});fs.copyFileSync('src/runtime/schema/nano-app.schema.json','dist/runtime/schema/nano-app.schema.json')\"",
58
+ "build": "tsc -p tsconfig.build.json",
59
59
  "prepack": "npm run build",
60
60
  "typecheck": "tsc --noEmit -p tsconfig.json",
61
61
  "test": "node --test --experimental-strip-types \"src/**/*.test.ts\"",
62
62
  "test:deno": "deno test -A --no-check src/"
63
63
  },
64
64
  "dependencies": {
65
+ "@nanobpm/nano-app-schema": "^0.1.0",
65
66
  "@nanobpm/nano-sdk": "^1.2.3",
66
67
  "@nanobpm/workflow": "^0.7.0",
67
68
  "bpmn-auto-layout": "^2.0.0-alpha.2",
@@ -24,8 +24,7 @@ test("parseManifest expands env in place", () => {
24
24
  assert.equal(m.name, "App");
25
25
  });
26
26
 
27
- test("workerJobType prefers taskType then type", () => {
27
+ test("workerJobType returns the taskType", () => {
28
28
  assert.equal(workerJobType({ taskType: "a", handler: "h" }), "a");
29
- assert.equal(workerJobType({ type: "b", handler: "h" }), "b");
30
- assert.equal(workerJobType({ handler: "h" }), undefined);
29
+ assert.equal(workerJobType({ taskType: "llm-job", llm: "gpt" }), "llm-job");
31
30
  });