@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.
- package/dist/runtime/core/manifest.d.ts +4 -134
- package/dist/runtime/core/manifest.js +11 -6
- package/dist/runtime/core/modules/triggers.d.ts +14 -2
- package/dist/runtime/core/modules/triggers.js +0 -6
- package/dist/runtime/core/validate.js +11 -10
- package/dist/runtime/index.d.ts +1 -1
- 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 +3 -2
- package/src/runtime/core/manifest.test.ts +2 -3
- package/src/runtime/core/manifest.ts +35 -134
- package/src/runtime/core/modules/triggers.test.ts +5 -3
- package/src/runtime/core/modules/triggers.ts +18 -4
- package/src/runtime/core/modules/workers.ts +2 -2
- package/src/runtime/core/validate.ts +11 -10
- package/src/runtime/index.ts +7 -4
- 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
- package/dist/runtime/schema/nano-app.schema.json +0 -503
- 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
|
-
/**
|
|
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.
|
|
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
|
|
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
|
|
27
|
+
test("workerJobType returns the taskType", () => {
|
|
28
28
|
assert.equal(workerJobType({ taskType: "a", handler: "h" }), "a");
|
|
29
|
-
assert.equal(workerJobType({
|
|
30
|
-
assert.equal(workerJobType({ handler: "h" }), undefined);
|
|
29
|
+
assert.equal(workerJobType({ taskType: "llm-job", llm: "gpt" }), "llm-job");
|
|
31
30
|
});
|