@nanobpm/urban 0.17.0 → 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.
- package/dist/toolkit/derivers/domain.d.ts +69 -0
- package/dist/toolkit/derivers/domain.js +243 -0
- package/dist/toolkit/derivers/messages.d.ts +36 -0
- package/dist/toolkit/derivers/messages.js +134 -0
- package/dist/toolkit/derivers/meta.d.ts +34 -0
- package/dist/toolkit/derivers/meta.js +106 -0
- package/dist/toolkit/derivers/migrations.d.ts +34 -1
- package/dist/toolkit/derivers/migrations.js +37 -8
- package/dist/toolkit/derivers/worker-io.d.ts +14 -0
- package/dist/toolkit/derivers/worker-io.js +13 -2
- package/dist/toolkit/gen.js +8 -2
- package/dist/toolkit/index.d.ts +15 -3
- package/dist/toolkit/index.js +9 -2
- package/package.json +1 -1
- package/src/toolkit/derivers/domain.test.ts +140 -0
- package/src/toolkit/derivers/domain.ts +317 -0
- package/src/toolkit/derivers/messages.test.ts +174 -0
- package/src/toolkit/derivers/messages.ts +166 -0
- package/src/toolkit/derivers/meta.test.ts +140 -0
- package/src/toolkit/derivers/meta.ts +128 -0
- package/src/toolkit/derivers/migrations.test.ts +42 -4
- package/src/toolkit/derivers/migrations.ts +61 -9
- package/src/toolkit/derivers/worker-io.test.ts +10 -0
- package/src/toolkit/derivers/worker-io.ts +14 -2
- package/src/toolkit/gen.test.ts +8 -1
- package/src/toolkit/gen.ts +8 -2
- package/src/toolkit/index.ts +53 -2
|
@@ -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
|
-
/**
|
|
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
|
-
/**
|
|
35
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
60
|
+
return { table, columns };
|
|
48
61
|
}
|
|
49
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
}
|
package/dist/toolkit/gen.js
CHANGED
|
@@ -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
|
}
|
package/dist/toolkit/index.d.ts
CHANGED
|
@@ -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
|
|
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
|
}>];
|
package/dist/toolkit/index.js
CHANGED
|
@@ -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
|
-
|
|
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.17.
|
|
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",
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import {
|
|
4
|
+
emitDomainModel,
|
|
5
|
+
emitDomain,
|
|
6
|
+
deriveDomain,
|
|
7
|
+
sourcesFromManifest,
|
|
8
|
+
registryFromManifest,
|
|
9
|
+
sqliteAffinityToTs,
|
|
10
|
+
interfaceName,
|
|
11
|
+
type SourceSchema,
|
|
12
|
+
type TableMeta,
|
|
13
|
+
type DomainTypeRegistry,
|
|
14
|
+
} from "./domain.ts";
|
|
15
|
+
import type { ToolkitManifest } from "./migrations.ts";
|
|
16
|
+
|
|
17
|
+
// Goldens captured verbatim from the console's `emitDomainModel` (server domain_types.ts) via
|
|
18
|
+
// Deno — this IS the per-emitter drift gate (ADR 0053): the toolkit must be byte-identical.
|
|
19
|
+
const GOLDENS: Record<string, string> = JSON.parse("{\n \"empty\": \"// AUTO-GENERATED by nanobpmn from the datasource schema (ADR 0029 §4.1).\\n// Do not edit — regenerated from the live tables. Erased at `deno compile`.\\n// eslint-disable\\n\\nexport interface DomainTables {}\\n\",\n \"tablesOnly\": \"// AUTO-GENERATED by nanobpmn from the datasource schema (ADR 0029 §4.1).\\n// Do not edit — regenerated from the live tables. Erased at `deno compile`.\\n// eslint-disable\\n\\nexport interface Customers {\\n /** INTEGER (primary key) */\\n id: number;\\n /** TEXT */\\n email: string;\\n /** TEXT */\\n tier: string | null;\\n}\\n\\nexport interface OrderItems {\\n /** INTEGER (primary key) */\\n id: number;\\n /** INTEGER */\\n qty: number;\\n /** REAL */\\n \\\"unit price\\\": number | null;\\n}\\n\\n/** Every table keyed by its wire name — index it for a row type. */\\nexport interface DomainTables {\\n \\\"customers\\\": Customers;\\n \\\"order_items\\\": OrderItems;\\n}\\n\",\n \"typesOnly\": \"// AUTO-GENERATED by nanobpmn from the datasource schema (ADR 0029 §4.1).\\n// Do not edit — regenerated from the live tables. Erased at `deno compile`.\\n// eslint-disable\\n\\nexport interface DomainTables {}\\n\\n/** Declared (transient) domain types from the manifest `types` registry (ADR 0029 §4.2), keyed by id. */\\nexport interface DomainTypes {\\n \\\"taxReturn\\\": {\\n year: number;\\n notes?: string;\\n lines: DomainTypes[\\\"lineItem\\\"][];\\n };\\n \\\"lineItem\\\": {\\n \\\"line no\\\": number;\\n amount: number;\\n };\\n}\\n\",\n \"both\": \"// AUTO-GENERATED by nanobpmn from the datasource schema (ADR 0029 §4.1).\\n// Do not edit — regenerated from the live tables. Erased at `deno compile`.\\n// eslint-disable\\n\\nexport interface Greetings {\\n /** INTEGER (primary key) */\\n id: number;\\n /** TEXT */\\n who: string;\\n /** TEXT */\\n message: string;\\n /** TEXT */\\n createdAt: string | null;\\n}\\n\\n/** Every table keyed by its wire name — index it for a row type. */\\nexport interface DomainTables {\\n \\\"greetings\\\": Greetings;\\n}\\n\\n/** Declared (transient) domain types from the manifest `types` registry (ADR 0029 §4.2), keyed by id. */\\nexport interface DomainTypes {\\n \\\"greeting\\\": {\\n who: string;\\n message: string;\\n createdAt?: string;\\n };\\n}\\n\",\n \"multi\": \"// AUTO-GENERATED by nanobpmn from the datasource schemas (ADR 0029 §4.1/§6).\\n// Do not edit — regenerated from the live tables. Erased at `deno compile`.\\n// eslint-disable\\n\\nexport interface AppCustomers {\\n /** INTEGER (primary key) */\\n id: number;\\n /** TEXT */\\n email: string;\\n}\\n\\nexport interface AnalyticsEvents {\\n /** INTEGER (primary key) */\\n id: number;\\n /** TEXT */\\n kind: string;\\n}\\n\\n/** Every datasource keyed by alias, then table by wire name. */\\nexport interface DomainSources {\\n \\\"app\\\": {\\n \\\"customers\\\": AppCustomers;\\n };\\n \\\"analytics\\\": {\\n \\\"events\\\": AnalyticsEvents;\\n };\\n}\\n\\n/** The default datasource's tables — index by wire name. */\\nexport type DomainTables = DomainSources[\\\"analytics\\\"];\\n\"\n}");
|
|
20
|
+
|
|
21
|
+
const col = (name: string, type: string, notNull: boolean, primaryKey: boolean) =>
|
|
22
|
+
({ name, type, notNull, primaryKey });
|
|
23
|
+
const table = (name: string, cols: TableMeta["columns"]): TableMeta => ({ name, columns: cols, indexes: [], foreignKeys: [] });
|
|
24
|
+
|
|
25
|
+
test("emitDomainModel: empty app → bare DomainTables (byte-parity)", () => {
|
|
26
|
+
assert.equal(emitDomainModel([], undefined, {}), GOLDENS.empty);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("emitDomainModel: table spine only (byte-parity)", () => {
|
|
30
|
+
const sources: SourceSchema[] = [{ source: "app", tables: [
|
|
31
|
+
table("customers", [col("id","INTEGER",false,true), col("email","TEXT",true,false), col("tier","TEXT",false,false)]),
|
|
32
|
+
table("order_items", [col("id","INTEGER",false,true), col("qty","INTEGER",true,false), col("unit price","REAL",false,false)]),
|
|
33
|
+
]}];
|
|
34
|
+
assert.equal(emitDomainModel(sources, "app", {}), GOLDENS.tablesOnly);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("emitDomainModel: registry only — nominal ref, list, optional, non-ident key (byte-parity)", () => {
|
|
38
|
+
const types: DomainTypeRegistry = {
|
|
39
|
+
taxReturn: { fields: { year: { type: "integer" }, notes: { type: "string", optional: true }, lines: { type: "lineItem", list: true } } },
|
|
40
|
+
lineItem: { fields: { "line no": { type: "integer" }, amount: { type: "number" } } },
|
|
41
|
+
};
|
|
42
|
+
assert.equal(emitDomainModel([], undefined, types), GOLDENS.typesOnly);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("emitDomainModel: table + registry coexist for same type (byte-parity)", () => {
|
|
46
|
+
const sources: SourceSchema[] = [{ source: "app", tables: [
|
|
47
|
+
table("greetings", [col("id","INTEGER",false,true), col("who","TEXT",true,false), col("message","TEXT",true,false), col("createdAt","TEXT",false,false)]),
|
|
48
|
+
]}];
|
|
49
|
+
const types: DomainTypeRegistry = { greeting: { table: "greetings", fields: { who: { type: "string" }, message: { type: "string" }, createdAt: { type: "datetime", optional: true } } } };
|
|
50
|
+
assert.equal(emitDomainModel(sources, "app", types), GOLDENS.both);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("emitDomainModel: multi-source → DomainSources + DomainTables alias to default (byte-parity)", () => {
|
|
54
|
+
const sources: SourceSchema[] = [
|
|
55
|
+
{ source: "app", tables: [ table("customers", [col("id","INTEGER",false,true), col("email","TEXT",true,false)]) ] },
|
|
56
|
+
{ source: "analytics", tables: [ table("events", [col("id","INTEGER",false,true), col("kind","TEXT",true,false)]) ] },
|
|
57
|
+
];
|
|
58
|
+
assert.equal(emitDomainModel(sources, "analytics", {}), GOLDENS.multi);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// --- manifest bridge: code-first derivation reproduces the console output --------------------
|
|
62
|
+
|
|
63
|
+
test("emitDomain: manifest types drive BOTH the table spine and the registry (byte-parity)", () => {
|
|
64
|
+
const manifest: ToolkitManifest = {
|
|
65
|
+
data: { default: "app" },
|
|
66
|
+
types: { greeting: { table: "greetings", fields: {
|
|
67
|
+
who: { type: "string" }, message: { type: "string" }, createdAt: { type: "datetime", optional: true },
|
|
68
|
+
} } },
|
|
69
|
+
};
|
|
70
|
+
// The manifest 'greeting' type yields DomainTables["greetings"] (id + fields) and
|
|
71
|
+
// DomainTypes["greeting"] (declared fields only) — identical to the console's `both` case.
|
|
72
|
+
assert.equal(emitDomain(manifest), GOLDENS.both);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("sourcesFromManifest: implicit id PK + field affinity + nullability", () => {
|
|
76
|
+
const manifest: ToolkitManifest = { data: { default: "app" }, types: {
|
|
77
|
+
greeting: { table: "greetings", fields: { who: { type: "string" }, createdAt: { type: "datetime", optional: true } } },
|
|
78
|
+
} };
|
|
79
|
+
const [src] = sourcesFromManifest(manifest);
|
|
80
|
+
assert.equal(src.source, "app");
|
|
81
|
+
assert.deepEqual(src.tables[0], table("greetings", [
|
|
82
|
+
col("id", "INTEGER", false, true),
|
|
83
|
+
col("who", "TEXT", true, false),
|
|
84
|
+
col("createdAt", "TEXT", false, false),
|
|
85
|
+
]));
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("sourcesFromManifest: table name defaults to the type id; types group by source", () => {
|
|
89
|
+
const manifest: ToolkitManifest = { data: { default: "app" }, types: {
|
|
90
|
+
order: { fields: { total: { type: "number" } } },
|
|
91
|
+
event: { source: "analytics", fields: { kind: { type: "string" } } },
|
|
92
|
+
} };
|
|
93
|
+
const sources = sourcesFromManifest(manifest);
|
|
94
|
+
assert.deepEqual(sources.map((s) => s.source), ["app", "analytics"]);
|
|
95
|
+
assert.equal(sources[0].tables[0].name, "order");
|
|
96
|
+
assert.equal(sources[1].tables[0].name, "event");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("registryFromManifest: declared fields only (no persisted id); type defaults to string", () => {
|
|
100
|
+
const manifest: ToolkitManifest = { types: {
|
|
101
|
+
greeting: { table: "greetings", fields: { who: {}, note: { type: "string", optional: true } } },
|
|
102
|
+
} };
|
|
103
|
+
assert.deepEqual(registryFromManifest(manifest), {
|
|
104
|
+
greeting: { table: "greetings", fields: { who: { type: "string", optional: undefined, list: undefined }, note: { type: "string", optional: true, list: undefined } } },
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("registryFromManifest + emitDomain: the `list` modifier is preserved as `T[]`", () => {
|
|
109
|
+
const manifest: ToolkitManifest = { types: {
|
|
110
|
+
order: { fields: { tags: { type: "string", list: true } } },
|
|
111
|
+
} };
|
|
112
|
+
// The bridge must forward `list` (not drop it) so the emitter can project the repeated field.
|
|
113
|
+
assert.equal(registryFromManifest(manifest).order.fields.tags.list, true);
|
|
114
|
+
assert.match(emitDomain(manifest), /tags: string\[\];/);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("deriveDomain: emits domain-rows.d.ts only when the manifest declares types", () => {
|
|
118
|
+
assert.deepEqual(deriveDomain({}), []);
|
|
119
|
+
assert.deepEqual(deriveDomain({ types: {} }), []);
|
|
120
|
+
const arts = deriveDomain({ data: { default: "app" }, types: { greeting: { table: "greetings", fields: { who: { type: "string" } } } } });
|
|
121
|
+
assert.equal(arts.length, 1);
|
|
122
|
+
assert.equal(arts[0].path, "nano-generated/domain-rows.d.ts");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("sqliteAffinityToTs: SQLite affinity rules with boolean/datetime overrides", () => {
|
|
126
|
+
assert.equal(sqliteAffinityToTs("INTEGER"), "number");
|
|
127
|
+
assert.equal(sqliteAffinityToTs("TEXT"), "string");
|
|
128
|
+
assert.equal(sqliteAffinityToTs("REAL"), "number");
|
|
129
|
+
assert.equal(sqliteAffinityToTs("BLOB"), "Uint8Array");
|
|
130
|
+
assert.equal(sqliteAffinityToTs("BOOLEAN"), "boolean");
|
|
131
|
+
assert.equal(sqliteAffinityToTs("DATETIME"), "string");
|
|
132
|
+
assert.equal(sqliteAffinityToTs("NUMERIC"), "number");
|
|
133
|
+
assert.equal(sqliteAffinityToTs(""), "unknown");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("interfaceName: PascalCases + sanitises table names to identifiers", () => {
|
|
137
|
+
assert.equal(interfaceName("order_items"), "OrderItems");
|
|
138
|
+
assert.equal(interfaceName("customers"), "Customers");
|
|
139
|
+
assert.equal(interfaceName("2024_events"), "T_2024Events");
|
|
140
|
+
});
|