@nanobpm/urban 0.17.0 → 0.18.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.
Files changed (37) hide show
  1. package/dist/cli.js +69 -5
  2. package/dist/toolkit/derivers/domain.d.ts +69 -0
  3. package/dist/toolkit/derivers/domain.js +243 -0
  4. package/dist/toolkit/derivers/messages.d.ts +36 -0
  5. package/dist/toolkit/derivers/messages.js +134 -0
  6. package/dist/toolkit/derivers/meta.d.ts +34 -0
  7. package/dist/toolkit/derivers/meta.js +106 -0
  8. package/dist/toolkit/derivers/migrations.d.ts +34 -1
  9. package/dist/toolkit/derivers/migrations.js +37 -8
  10. package/dist/toolkit/derivers/worker-io.d.ts +14 -0
  11. package/dist/toolkit/derivers/worker-io.js +13 -2
  12. package/dist/toolkit/fsio.js +31 -2
  13. package/dist/toolkit/gen.d.ts +28 -0
  14. package/dist/toolkit/gen.js +60 -11
  15. package/dist/toolkit/index.d.ts +18 -4
  16. package/dist/toolkit/index.js +12 -3
  17. package/dist/toolkit/models.d.ts +63 -0
  18. package/dist/toolkit/models.js +142 -0
  19. package/package.json +1 -1
  20. package/src/cli.ts +70 -5
  21. package/src/toolkit/derivers/domain.test.ts +140 -0
  22. package/src/toolkit/derivers/domain.ts +317 -0
  23. package/src/toolkit/derivers/messages.test.ts +174 -0
  24. package/src/toolkit/derivers/messages.ts +166 -0
  25. package/src/toolkit/derivers/meta.test.ts +140 -0
  26. package/src/toolkit/derivers/meta.ts +128 -0
  27. package/src/toolkit/derivers/migrations.test.ts +42 -4
  28. package/src/toolkit/derivers/migrations.ts +61 -9
  29. package/src/toolkit/derivers/worker-io.test.ts +10 -0
  30. package/src/toolkit/derivers/worker-io.ts +14 -2
  31. package/src/toolkit/fsio.test.ts +23 -0
  32. package/src/toolkit/fsio.ts +22 -2
  33. package/src/toolkit/gen.test.ts +84 -2
  34. package/src/toolkit/gen.ts +86 -12
  35. package/src/toolkit/index.ts +66 -3
  36. package/src/toolkit/models.test.ts +159 -0
  37. package/src/toolkit/models.ts +193 -0
package/dist/cli.js CHANGED
@@ -2,7 +2,8 @@
2
2
  //
3
3
  // urban new <name> scaffold a new app (delegates to create-urban-app)
4
4
  // urban check load + validate the manifest, report issues
5
- // urban gen derive generated artifacts (migrations, worker I/O)
5
+ // urban gen derive generated artifacts (migrations, worker I/O, code-first models)
6
+ // urban derive derive code-first models only (workflows/*.ts → processes/*.bpmn)
6
7
  // urban run materialize + serve the app (deploy, data, workers, surfaces, triggers)
7
8
  // urban dev like run, plus watch sources and hot-reload on change
8
9
  // urban deploy deploy models only, then exit
@@ -11,7 +12,7 @@
11
12
  // --port <n>, -h/--help, -v/--version.
12
13
  import { collectManifestIssues, installSignalHandlers, loadManifest, runDev, runFromEnv, selectHost, } from "./runtime/index.js";
13
14
  import { scaffold, slugify } from "create-urban-app";
14
- import { createNodeGenIO, runGen, scaffoldWorkers } from "./toolkit/index.js";
15
+ import { createNodeGenIO, previewModels, runGen, scaffoldWorkers } from "./toolkit/index.js";
15
16
  import { pathToFileURL } from "node:url";
16
17
  import { readFileSync } from "node:fs";
17
18
  // Read the package version from package.json (one hop up from both src/cli.ts and the
@@ -27,7 +28,7 @@ function readVersion() {
27
28
  }
28
29
  const VERSION = readVersion();
29
30
  function parse(argv) {
30
- const f = { root: ".", manifest: "nano.app.json", check: false, write: false, deno: false, help: false, version: false, _: [] };
31
+ const f = { root: ".", manifest: "nano.app.json", check: false, write: false, deno: false, models: true, stdout: false, help: false, version: false, _: [] };
31
32
  const need = (i, flag) => {
32
33
  const v = argv[i];
33
34
  if (v === undefined || v.startsWith("-"))
@@ -46,6 +47,12 @@ function parse(argv) {
46
47
  f.write = true;
47
48
  else if (a === "--deno")
48
49
  f.deno = true;
50
+ else if (a === "--no-models")
51
+ f.models = false;
52
+ else if (a === "--models")
53
+ f.models = true;
54
+ else if (a === "--stdout")
55
+ f.stdout = true;
49
56
  else if (a === "--code-first")
50
57
  f.style = "code";
51
58
  else if (a === "--style") {
@@ -81,7 +88,10 @@ Usage:
81
88
  urban new <name> [--root <path>] [--deno] [--style model|code] scaffold a new Urban app
82
89
  (--code-first is shorthand for --style code)
83
90
  urban check validate the manifest
84
- urban gen [--check] derive artifacts (migrations, worker-io)
91
+ urban gen [--check] [--no-models] derive artifacts (migrations, worker-io, + code-first models)
92
+ (--no-models: skip writing derived .bpmn; type-contracts only)
93
+ urban derive [--check|--stdout] derive code-first models only (workflows/*.ts → processes/*.bpmn)
94
+ (--stdout: print {models,incomplete} JSON without writing)
85
95
  urban stubs [--write] scaffold write-once handler stubs from the model
86
96
  urban run materialize + serve the app
87
97
  urban dev run + watch sources, hot-reload on change
@@ -113,7 +123,13 @@ async function cmdCheck(f) {
113
123
  }
114
124
  async function cmdGen(f) {
115
125
  const io = createNodeGenIO();
116
- const res = await runGen({ root: f.root, io, manifestFile: f.manifest, check: f.check });
126
+ const res = await runGen({ root: f.root, io, manifestFile: f.manifest, check: f.check, emitModels: f.models });
127
+ if (res.incomplete) {
128
+ console.error(`✖ ${res.modelErrors.length} workflow(s) failed to derive:`);
129
+ for (const e of res.modelErrors)
130
+ console.error(` • ${e.path}: ${e.message}`);
131
+ return 1;
132
+ }
117
133
  if (f.check) {
118
134
  if (res.drift.length === 0) {
119
135
  console.log(`✔ generated artifacts are up to date (${res.artifacts.length} checked)`);
@@ -129,6 +145,52 @@ async function cmdGen(f) {
129
145
  console.log(` • ${a.path}`);
130
146
  return 0;
131
147
  }
148
+ async function cmdDerive(f) {
149
+ // `urban derive` IS model derivation, so `--no-models` is contradictory. Reject it explicitly
150
+ // rather than silently ignoring it (flags are global, so it's easy to pass by accident).
151
+ if (!f.models) {
152
+ console.error("✖ `urban derive` derives models — `--no-models` is contradictory. Use `urban gen --no-models` for type-contracts only.");
153
+ return 2;
154
+ }
155
+ const io = createNodeGenIO();
156
+ // Non-writing preview: emit `{ models: [{ id, kind, xml }], incomplete }` to stdout for the
157
+ // console's read-only model viewer (never touches `processes/`).
158
+ if (f.stdout) {
159
+ const d = await previewModels({ root: f.root, io, manifestFile: f.manifest });
160
+ process.stdout.write(JSON.stringify({
161
+ models: d.list.map((m) => ({ id: m.id, kind: m.kind, xml: m.xml })),
162
+ incomplete: d.incomplete,
163
+ }) + "\n");
164
+ if (d.incomplete) {
165
+ for (const e of d.errors)
166
+ console.error(` • ${e.path}: ${e.message}`);
167
+ return 1;
168
+ }
169
+ return 0;
170
+ }
171
+ // Derive + write models only (skip the type-contract derivers); `generate_models` delegates here.
172
+ const res = await runGen({ root: f.root, io, manifestFile: f.manifest, check: f.check, modelsOnly: true });
173
+ if (res.incomplete) {
174
+ console.error(`✖ ${res.modelErrors.length} workflow(s) failed to derive:`);
175
+ for (const e of res.modelErrors)
176
+ console.error(` • ${e.path}: ${e.message}`);
177
+ return 1;
178
+ }
179
+ if (f.check) {
180
+ if (res.drift.length === 0) {
181
+ console.log(`✔ derived models are up to date (${res.artifacts.length} checked)`);
182
+ return 0;
183
+ }
184
+ console.error(`✖ ${res.drift.length} derived model(s) are out of date — run \`urban derive\`:`);
185
+ for (const p of res.drift)
186
+ console.error(` • ${p}`);
187
+ return 1;
188
+ }
189
+ console.log(`✔ derived ${res.artifacts.length} model(s):`);
190
+ for (const a of res.artifacts)
191
+ console.log(` • ${a.path}`);
192
+ return 0;
193
+ }
132
194
  async function cmdStubs(f) {
133
195
  const io = createNodeGenIO();
134
196
  const run = await scaffoldWorkers({ root: f.root, io, manifestFile: f.manifest, write: f.write });
@@ -232,6 +294,8 @@ export async function main(argv) {
232
294
  return cmdCheck(f);
233
295
  case "gen":
234
296
  return cmdGen(f);
297
+ case "derive":
298
+ return cmdDerive(f);
235
299
  case "stubs":
236
300
  return cmdStubs(f);
237
301
  case "run":
@@ -0,0 +1,69 @@
1
+ import type { DerivedArtifact, Deriver } from "../artifact.ts";
2
+ import type { ColumnMeta, TableMeta } from "../../runtime/core/modules/gateway.ts";
3
+ import { type ToolkitManifest } from "./migrations.ts";
4
+ export type { ColumnMeta, TableMeta };
5
+ /** One datasource's tables, paired with its manifest alias. */
6
+ export interface SourceSchema {
7
+ source: string;
8
+ tables: TableMeta[];
9
+ }
10
+ /**
11
+ * Map a SQLite declared column type to a TypeScript field type. SQLite is dynamically typed but
12
+ * assigns each column a *type affinity* from its declared name, so we apply the affinity rules
13
+ * (https://sqlite.org/datatype3.html §3.1) with two maker-friendly overrides: `BOOLEAN` →
14
+ * `boolean` and date/time types → `string` (ISO text), matching how Urban surfaces them.
15
+ */
16
+ export declare function sqliteAffinityToTs(declaredType: string): string;
17
+ /** PascalCase a table name into an interface name, sanitised to an identifier. */
18
+ export declare function interfaceName(table: string): string;
19
+ /**
20
+ * Emit the full `domain-rows.d.ts` from a datasource's tables: one `interface` per table plus a
21
+ * `DomainTables` lookup keyed by the *raw* table name, so callers write `DomainTables["customers"]`.
22
+ */
23
+ export declare function emitDomainDts(tables: TableMeta[]): string;
24
+ /**
25
+ * Emit `domain-rows.d.ts` for an App that declares *multiple* datasources: one `interface` per
26
+ * (source, table), a `DomainSources` map keyed by alias then wire table name, and a `DomainTables`
27
+ * alias to the default source. When there is one source (or none) this is byte-identical to
28
+ * `emitDomainDts` so the common case stays stable.
29
+ */
30
+ export declare function emitDomainDtsForSources(sources: SourceSchema[], defaultSource?: string): string;
31
+ /** One field of a declared domain type in the manifest `types` registry. */
32
+ export interface DomainFieldDef {
33
+ /** A primitive keyword or the id of another registry type (nominal ref). */
34
+ type: string;
35
+ optional?: boolean;
36
+ list?: boolean;
37
+ }
38
+ /** A declared (transient) domain record type from the manifest `types` block. */
39
+ export interface DomainTypeDef {
40
+ name?: string;
41
+ match?: string;
42
+ table?: string;
43
+ fields: Record<string, DomainFieldDef>;
44
+ }
45
+ /** The manifest `types` registry: type id → declared record type. */
46
+ export type DomainTypeRegistry = Record<string, DomainTypeDef>;
47
+ /**
48
+ * Emit the `DomainTypes` block for the manifest `types` registry: an inline map keyed by type id.
49
+ * Returns `""` when the registry is empty, so the table spine stays byte-identical when no types
50
+ * are declared. `optional` widens the key with `?`.
51
+ */
52
+ export declare function emitDomainTypeRegistry(types: DomainTypeRegistry): string;
53
+ /**
54
+ * Compose the full `domain-rows.d.ts`: the datasource table spine (every source, ADR 0029 §6)
55
+ * followed by the manifest `types` registry (§4.2).
56
+ */
57
+ export declare function emitDomainModel(sources: SourceSchema[], defaultSource: string | undefined, types: DomainTypeRegistry): string;
58
+ /** The datasource table spine derived from the manifest types, grouped by source (same grouping
59
+ * and order as the SQL migration). */
60
+ export declare function sourcesFromManifest(manifest: ToolkitManifest): SourceSchema[];
61
+ /** The `DomainTypes` registry derived from the manifest types (declared fields only — the
62
+ * persisted `id` belongs to the table row, not the logical type). */
63
+ export declare function registryFromManifest(manifest: ToolkitManifest): DomainTypeRegistry;
64
+ /** Emit the `domain-rows.d.ts` content for an app manifest. */
65
+ export declare function emitDomain(manifest: ToolkitManifest): string;
66
+ /** Derive `nano-generated/domain-rows.d.ts` from the manifest types. Empty when the app declares
67
+ * no types (no tables, no registry, and nothing references `DomainTypes`). */
68
+ export declare function deriveDomain(manifest: ToolkitManifest): DerivedArtifact[];
69
+ export declare const domainDeriver: Deriver<ToolkitManifest>;
@@ -0,0 +1,243 @@
1
+ // Deriver: manifest `types` → `domain-rows.d.ts`, the domain row-type contract (ADR 0029 §4.1/§6,
2
+ // ADR 0053). Two blocks compose into one generated `.d.ts`:
3
+ // • the **table spine** — one `interface` per persisted table plus a `DomainTables` lookup
4
+ // keyed by wire name, so a worker types `DomainTables["greetings"]`; and
5
+ // • the **`DomainTypes` registry** — the declared (transient) manifest types keyed by id, so
6
+ // envelope/worker/message refs resolve to `DomainTypes["<id>"]` (see `typeRefFor`).
7
+ //
8
+ // Byte-faithful port of the console's `emitDomainModel`/`emitDomainDts…`/`emitDomainTypeRegistry`
9
+ // (server `domain_types.ts`), so the toolkit is a drop-in for the IDE's codegen. The emitters are
10
+ // pure; the table spine is derived from the manifest (code-first, ADR 0054) via the *same*
11
+ // canonical `tableSchemaForType` the SQL migration uses — one schema truth, no drift surface.
12
+ import { GENERATED_DIR } from "../artifact.js";
13
+ import { DOMAIN_DTS } from "./worker-io.js";
14
+ import { groupTypesBySource, tableSchemaForType, } from "./migrations.js";
15
+ // --- table spine emitters (port of domain_types.ts) ------------------------------------------
16
+ /**
17
+ * Map a SQLite declared column type to a TypeScript field type. SQLite is dynamically typed but
18
+ * assigns each column a *type affinity* from its declared name, so we apply the affinity rules
19
+ * (https://sqlite.org/datatype3.html §3.1) with two maker-friendly overrides: `BOOLEAN` →
20
+ * `boolean` and date/time types → `string` (ISO text), matching how Urban surfaces them.
21
+ */
22
+ export function sqliteAffinityToTs(declaredType) {
23
+ const t = declaredType.toUpperCase();
24
+ if (t.length === 0)
25
+ return "unknown"; // NONE affinity — opaque
26
+ if (t.includes("BOOL"))
27
+ return "boolean";
28
+ if (t.includes("DATE") || t.includes("TIME"))
29
+ return "string";
30
+ if (t.includes("INT"))
31
+ return "number";
32
+ if (t.includes("CHAR") || t.includes("CLOB") || t.includes("TEXT"))
33
+ return "string";
34
+ if (t.includes("BLOB"))
35
+ return "Uint8Array";
36
+ if (t.includes("REAL") || t.includes("FLOA") || t.includes("DOUB"))
37
+ return "number";
38
+ return "number"; // NUMERIC affinity
39
+ }
40
+ /** A TS identifier is safe to use bare as a property name / type name. */
41
+ function isIdent(name) {
42
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name);
43
+ }
44
+ /** PascalCase a table name into an interface name, sanitised to an identifier. */
45
+ export function interfaceName(table) {
46
+ const pascal = table
47
+ .replace(/[^A-Za-z0-9]+/g, " ")
48
+ .trim()
49
+ .split(/\s+/)
50
+ .map((w) => (w ? w[0].toUpperCase() + w.slice(1) : ""))
51
+ .join("");
52
+ const safe = pascal.replace(/[^A-Za-z0-9_$]/g, "");
53
+ return /^[A-Za-z_$]/.test(safe) ? safe : `T_${safe}`;
54
+ }
55
+ /** The TS field type for a column: nullable columns widen with `| null`. */
56
+ function fieldType(col) {
57
+ const base = sqliteAffinityToTs(col.type);
58
+ // A NOT NULL column (or the INTEGER PRIMARY KEY rowid alias) is never null.
59
+ return col.notNull || col.primaryKey ? base : `${base} | null`;
60
+ }
61
+ /** One `export interface` block for a table under an explicit interface name. */
62
+ function tableInterfaceNamed(name, t) {
63
+ const fields = t.columns
64
+ .map((c) => {
65
+ const key = isIdent(c.name) ? c.name : JSON.stringify(c.name);
66
+ const pk = c.primaryKey ? " (primary key)" : "";
67
+ return ` /** ${c.type || "?"}${pk} */\n ${key}: ${fieldType(c)};`;
68
+ })
69
+ .join("\n");
70
+ return `export interface ${name} {\n${fields}\n}`;
71
+ }
72
+ /** One `export interface` block for a table (name derived from the table). */
73
+ function tableInterface(t) {
74
+ return tableInterfaceNamed(interfaceName(t.name), t);
75
+ }
76
+ /**
77
+ * Emit the full `domain-rows.d.ts` from a datasource's tables: one `interface` per table plus a
78
+ * `DomainTables` lookup keyed by the *raw* table name, so callers write `DomainTables["customers"]`.
79
+ */
80
+ export function emitDomainDts(tables) {
81
+ const header = "// AUTO-GENERATED by nanobpmn from the datasource schema (ADR 0029 §4.1).\n" +
82
+ "// Do not edit — regenerated from the live tables. Erased at `deno compile`.\n" +
83
+ "// eslint-disable\n";
84
+ if (tables.length === 0) {
85
+ return `${header}\nexport interface DomainTables {}\n`;
86
+ }
87
+ const interfaces = tables.map(tableInterface).join("\n\n");
88
+ const map = tables
89
+ .map((t) => ` ${JSON.stringify(t.name)}: ${interfaceName(t.name)};`)
90
+ .join("\n");
91
+ return `${header}\n${interfaces}\n\n/** Every table keyed by its wire name — index it for a row type. */\nexport interface DomainTables {\n${map}\n}\n`;
92
+ }
93
+ /**
94
+ * The interface name for a table in a multi-source union: the source alias is PascalCased and
95
+ * prefixed to the table's interface name so tables that share a name across datasources get
96
+ * distinct, collision-free interfaces (`AppCustomers`, `AnalyticsCustomers`).
97
+ */
98
+ function prefixedInterfaceName(source, table) {
99
+ return `${interfaceName(source)}${interfaceName(table)}`;
100
+ }
101
+ /**
102
+ * Emit `domain-rows.d.ts` for an App that declares *multiple* datasources: one `interface` per
103
+ * (source, table), a `DomainSources` map keyed by alias then wire table name, and a `DomainTables`
104
+ * alias to the default source. When there is one source (or none) this is byte-identical to
105
+ * `emitDomainDts` so the common case stays stable.
106
+ */
107
+ export function emitDomainDtsForSources(sources, defaultSource) {
108
+ if (sources.length <= 1) {
109
+ return emitDomainDts(sources[0]?.tables ?? []);
110
+ }
111
+ const header = "// AUTO-GENERATED by nanobpmn from the datasource schemas (ADR 0029 §4.1/§6).\n" +
112
+ "// Do not edit — regenerated from the live tables. Erased at `deno compile`.\n" +
113
+ "// eslint-disable\n";
114
+ const interfaces = sources
115
+ .flatMap((s) => s.tables.map((t) => tableInterfaceNamed(prefixedInterfaceName(s.source, t.name), t)))
116
+ .join("\n\n");
117
+ const sourceMap = sources
118
+ .map((s) => {
119
+ if (s.tables.length === 0)
120
+ return ` ${JSON.stringify(s.source)}: {};`;
121
+ const tableMap = s.tables
122
+ .map((t) => ` ${JSON.stringify(t.name)}: ${prefixedInterfaceName(s.source, t.name)};`)
123
+ .join("\n");
124
+ return ` ${JSON.stringify(s.source)}: {\n${tableMap}\n };`;
125
+ })
126
+ .join("\n");
127
+ const def = defaultSource && sources.some((s) => s.source === defaultSource)
128
+ ? defaultSource
129
+ : sources[0].source;
130
+ return `${header}\n${interfaces}\n\n` +
131
+ `/** Every datasource keyed by alias, then table by wire name. */\n` +
132
+ `export interface DomainSources {\n${sourceMap}\n}\n\n` +
133
+ `/** The default datasource's tables — index by wire name. */\n` +
134
+ `export type DomainTables = DomainSources[${JSON.stringify(def)}];\n`;
135
+ }
136
+ /** Manifest primitive field keywords → TS types (ADR 0029 §4.2 / schema). */
137
+ const PRIMITIVE_TS = {
138
+ string: "string",
139
+ number: "number",
140
+ integer: "number",
141
+ boolean: "boolean",
142
+ date: "string",
143
+ datetime: "string",
144
+ json: "unknown",
145
+ };
146
+ /**
147
+ * The TS type for a registry field. A primitive keyword maps directly (and takes precedence over
148
+ * an identically named type, per the schema); anything else is a nominal reference to another
149
+ * registry type, emitted as an indexed access into `DomainTypes`. An unresolved reference degrades
150
+ * to `unknown`. `list` wraps the base type in `[]`.
151
+ */
152
+ function fieldTsType(field, ids) {
153
+ const base = PRIMITIVE_TS[field.type] ??
154
+ (ids.has(field.type) ? `DomainTypes[${JSON.stringify(field.type)}]` : "unknown");
155
+ return field.list ? `${base}[]` : base;
156
+ }
157
+ /**
158
+ * Emit the `DomainTypes` block for the manifest `types` registry: an inline map keyed by type id.
159
+ * Returns `""` when the registry is empty, so the table spine stays byte-identical when no types
160
+ * are declared. `optional` widens the key with `?`.
161
+ */
162
+ export function emitDomainTypeRegistry(types) {
163
+ const ids = new Set(Object.keys(types));
164
+ if (ids.size === 0)
165
+ return "";
166
+ const entries = Object.entries(types)
167
+ .map(([id, def]) => {
168
+ const fields = Object.entries(def.fields ?? {})
169
+ .map(([fname, field]) => {
170
+ const key = isIdent(fname) ? fname : JSON.stringify(fname);
171
+ const opt = field.optional ? "?" : "";
172
+ return ` ${key}${opt}: ${fieldTsType(field, ids)};`;
173
+ })
174
+ .join("\n");
175
+ const body = fields.length > 0 ? `{\n${fields}\n }` : "{}";
176
+ return ` ${JSON.stringify(id)}: ${body};`;
177
+ })
178
+ .join("\n");
179
+ return `/** Declared (transient) domain types from the manifest \`types\` registry (ADR 0029 §4.2), keyed by id. */\nexport interface DomainTypes {\n${entries}\n}\n`;
180
+ }
181
+ /**
182
+ * Compose the full `domain-rows.d.ts`: the datasource table spine (every source, ADR 0029 §6)
183
+ * followed by the manifest `types` registry (§4.2).
184
+ */
185
+ export function emitDomainModel(sources, defaultSource, types) {
186
+ const spine = emitDomainDtsForSources(sources, defaultSource);
187
+ const registry = emitDomainTypeRegistry(types);
188
+ return registry ? `${spine}\n${registry}` : spine;
189
+ }
190
+ // --- manifest bridge (code-first, ADR 0054) --------------------------------------------------
191
+ /** Project one manifest type's canonical schema into a `TableMeta` for the row-type spine. Reuses
192
+ * the *same* `tableSchemaForType` the SQL migration uses, so the row types can't drift from the DB. */
193
+ function tableMetaFromType(typeName, def) {
194
+ const { table, columns } = tableSchemaForType(typeName, def);
195
+ return {
196
+ name: table,
197
+ columns: columns.map((c) => ({
198
+ name: c.name,
199
+ type: c.sqlType,
200
+ notNull: c.notNull,
201
+ primaryKey: c.primaryKey,
202
+ })),
203
+ indexes: [],
204
+ foreignKeys: [],
205
+ };
206
+ }
207
+ /** The datasource table spine derived from the manifest types, grouped by source (same grouping
208
+ * and order as the SQL migration). */
209
+ export function sourcesFromManifest(manifest) {
210
+ return [...groupTypesBySource(manifest)].map(([source, list]) => ({
211
+ source,
212
+ tables: list.map(([name, def]) => tableMetaFromType(name, def)),
213
+ }));
214
+ }
215
+ /** The `DomainTypes` registry derived from the manifest types (declared fields only — the
216
+ * persisted `id` belongs to the table row, not the logical type). */
217
+ export function registryFromManifest(manifest) {
218
+ const out = {};
219
+ for (const [id, def] of Object.entries(manifest.types ?? {})) {
220
+ const fields = {};
221
+ for (const [fname, f] of Object.entries(def.fields ?? {})) {
222
+ fields[fname] = { type: f.type ?? "string", optional: f.optional, list: f.list };
223
+ }
224
+ out[id] = { table: def.table, fields };
225
+ }
226
+ return out;
227
+ }
228
+ /** Emit the `domain-rows.d.ts` content for an app manifest. */
229
+ export function emitDomain(manifest) {
230
+ return emitDomainModel(sourcesFromManifest(manifest), manifest.data?.default ?? "app", registryFromManifest(manifest));
231
+ }
232
+ /** Derive `nano-generated/domain-rows.d.ts` from the manifest types. Empty when the app declares
233
+ * no types (no tables, no registry, and nothing references `DomainTypes`). */
234
+ export function deriveDomain(manifest) {
235
+ if (!manifest.types || Object.keys(manifest.types).length === 0)
236
+ return [];
237
+ return [{ path: `${GENERATED_DIR}/${DOMAIN_DTS}`, content: emitDomain(manifest) }];
238
+ }
239
+ export const domainDeriver = {
240
+ id: "types->domain",
241
+ describe: "Derive domain row types (DomainTables spine + DomainTypes registry) from manifest types.",
242
+ derive: deriveDomain,
243
+ };
@@ -0,0 +1,36 @@
1
+ import type { DerivedArtifact, Deriver } from "../artifact.ts";
2
+ import { type ModelSource } from "./worker-io.ts";
3
+ /** Filename the console uses for the derived message-payload map; kept identical (drop-in). */
4
+ export declare const MESSAGE_BINDINGS_DTS = "message-io.d.ts";
5
+ /** One message-IO entry scanned from a model: a `bpmn:message` name plus the envelope
6
+ * type refs it carries. `inputType` is the received payload (what a `publishMessage`
7
+ * caller sends); `outputType` is reserved. Mirrors the Rust `MessageIo`. */
8
+ export interface MessageIo {
9
+ messageName: string;
10
+ inputType?: string;
11
+ outputType?: string;
12
+ }
13
+ /** The binding shape the emitter reads. Identical to a scanned `MessageIo`, so it is an
14
+ * alias — one canonical shape, no second declaration to drift (the console keeps the two
15
+ * apart only because its scan output is Rust and its emitter input is TS). */
16
+ export type MessageBindingDecl = MessageIo;
17
+ /** Scan one BPMN document for `bpmn:message` envelopes. Handles both a self-closing
18
+ * `<bpmn:message name=.. />` (name only — completes the `MessageName` union) and an open
19
+ * message whose body carries the `io.nanobpm.dataEnvelope.in/out` `zeebe:property`. */
20
+ export declare function scanModelMessages(xml: string): MessageIo[];
21
+ /** Emit `message-io.d.ts` from message bindings + the set of declared domain type ids.
22
+ * Each named message joins the `MessageName` union; a message whose envelope names a
23
+ * declared type also maps to it in `MessagePayloads` (else falls back to `MessageVars`). */
24
+ export declare function emitMessageBindings(messages: MessageBindingDecl[], declaredTypeIds: Iterable<string>): string;
25
+ /**
26
+ * Derive `nano-generated/message-io.d.ts` from BPMN models. Messages are aggregated the way
27
+ * the console's Rust scan does (a `BTreeMap` keyed by name): deduped by name, sorted by name,
28
+ * with the envelope type refs merged first-wins across models — so the derived output matches
29
+ * the host's `derivedMessages` for the same models. `declaredTypeIds` is the manifest `types`
30
+ * key set; a message only gets a typed payload when its envelope names a declared type.
31
+ */
32
+ export declare function deriveMessageBindings(models: ModelSource[], declaredTypeIds?: Iterable<string>): DerivedArtifact[];
33
+ export declare const messagesDeriver: Deriver<{
34
+ models: ModelSource[];
35
+ declaredTypeIds: Iterable<string>;
36
+ }>;
@@ -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
+ }>;