@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
|
@@ -1,138 +1,8 @@
|
|
|
1
1
|
import type { HostContext } from "./host.ts";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
7
|
-
export interface TypeField {
|
|
8
|
-
type: string;
|
|
9
|
-
optional?: boolean;
|
|
10
|
-
}
|
|
11
|
-
export interface DomainType {
|
|
12
|
-
name?: string;
|
|
13
|
-
table?: string;
|
|
14
|
-
fields?: Record<string, TypeField>;
|
|
15
|
-
}
|
|
16
|
-
export interface WorkerDecl {
|
|
17
|
-
/** Job/task type the worker subscribes to. Schema allows `taskType` or `type`. */
|
|
18
|
-
taskType?: string;
|
|
19
|
-
type?: string;
|
|
20
|
-
/** Path to the handler module, relative to the app root. Mutually exclusive with `llm`. */
|
|
21
|
-
handler?: string;
|
|
22
|
-
/** Name of an `llm[]` binding used as the worker (LLM-as-worker). Mutually exclusive with `handler`. */
|
|
23
|
-
llm?: string;
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* One named LLM binding (`llm.<id>` in the manifest). A binding is usable as a worker
|
|
27
|
-
* (LLM-as-worker, ADR 0022 §E role 1) or as a chat surface agent. `provider` selects how
|
|
28
|
-
* the endpoint is resolved (`"env"` reads it from the environment); `model` is typically
|
|
29
|
-
* an env template. When `output.decision` is set, the model's JSON reply is fed through
|
|
30
|
-
* that DMN decision (the "rails") and the decision's output is returned instead.
|
|
31
|
-
*/
|
|
32
|
-
export interface LlmBinding {
|
|
33
|
-
/** Provider selector. `"env"` resolves an OpenAI-compatible endpoint from the environment. */
|
|
34
|
-
provider: string;
|
|
35
|
-
/** Model id, typically an env template (e.g. `${NANO_APP_LLM_MODEL}`). */
|
|
36
|
-
model: string;
|
|
37
|
-
/** Constrains the model's structured output — currently a DMN decision id (the rails). */
|
|
38
|
-
output?: {
|
|
39
|
-
decision?: string;
|
|
40
|
-
};
|
|
41
|
-
/** Action-API tools the agent may call (chat-agent role; unused by the worker role). */
|
|
42
|
-
tools?: string[];
|
|
43
|
-
}
|
|
44
|
-
export interface TriggerDecl {
|
|
45
|
-
id: string;
|
|
46
|
-
type: string;
|
|
47
|
-
path?: string;
|
|
48
|
-
auth?: string;
|
|
49
|
-
/** cron: the 5-field crontab spec (UTC), e.g. "0 6 * * *". */
|
|
50
|
-
spec?: string;
|
|
51
|
-
/** cron catch-up policy for fires missed while the app was down. Default "skip". */
|
|
52
|
-
onMissed?: "skip" | "once" | "all";
|
|
53
|
-
/** Source-kind-specific settings (e.g. file `{ pollMs }`). */
|
|
54
|
-
config?: Record<string, unknown>;
|
|
55
|
-
/** Name of a connections[] entry supplying this source's credentials. */
|
|
56
|
-
connection?: string;
|
|
57
|
-
/** Maps the event to one engine call: start a process OR publish a message (ADR 0025 §1). */
|
|
58
|
-
action?: {
|
|
59
|
-
/** Process id/name to start. */
|
|
60
|
-
start?: string;
|
|
61
|
-
/** messageName to publish as a CorrelateMessage. */
|
|
62
|
-
message?: string;
|
|
63
|
-
/** correlationKey (literal, or `= body.path`) for a message action. */
|
|
64
|
-
correlationKey?: string;
|
|
65
|
-
/**
|
|
66
|
-
* Variables for the started instance / published message. Per the manifest schema
|
|
67
|
-
* (`#/$defs/triggerAction.variables`) this is a FEEL string over the event body
|
|
68
|
-
* (e.g. `= body.payload`); an inline object literal is also accepted for convenience.
|
|
69
|
-
*/
|
|
70
|
-
variables?: string | Record<string, unknown>;
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
export interface SurfaceDecl {
|
|
74
|
-
enabled?: boolean;
|
|
75
|
-
path?: string;
|
|
76
|
-
agent?: string;
|
|
77
|
-
/** pages surface: directory of `*.page.json` (relative to app root). Default `pages`. */
|
|
78
|
-
pagesDir?: string;
|
|
79
|
-
/** pages surface: the page served at `/`. Default `home`. */
|
|
80
|
-
homePage?: string;
|
|
81
|
-
/** pages surface: max rows a `dataGrid` fetch returns. Default 200. */
|
|
82
|
-
rowLimit?: number;
|
|
83
|
-
/** pages surface: the injected default datasource name. Default `app`. */
|
|
84
|
-
sourceName?: string;
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* An app-authored action handler override (ADR 0055 phase 3). Each declaration binds a
|
|
88
|
-
* route to a handler module (default-exports an `ActionHandler`), letting an app wrap the
|
|
89
|
-
* generic pages start/cancel/message actions with business logic. Mounted before the
|
|
90
|
-
* generic pages action routes, so an exact override shadows the generic one.
|
|
91
|
-
*/
|
|
92
|
-
export interface ActionDecl {
|
|
93
|
-
/** Route path to serve, e.g. "/app/actions/cancel" or "/app/actions/start/convergence-loop". */
|
|
94
|
-
path: string;
|
|
95
|
-
/** Handler module path relative to the app root; default-exports an `ActionHandler`. */
|
|
96
|
-
module: string;
|
|
97
|
-
/** HTTP method to match. Default "POST". */
|
|
98
|
-
method?: string;
|
|
99
|
-
/** Match `path` as a prefix rather than exactly. Default false. */
|
|
100
|
-
prefix?: boolean;
|
|
101
|
-
}
|
|
102
|
-
export interface AppManifest {
|
|
103
|
-
$schema?: string;
|
|
104
|
-
schemaVersion: 1;
|
|
105
|
-
id: string;
|
|
106
|
-
name: string;
|
|
107
|
-
codename?: string;
|
|
108
|
-
runtime?: {
|
|
109
|
-
engine?: string;
|
|
110
|
-
node?: string;
|
|
111
|
-
};
|
|
112
|
-
models?: {
|
|
113
|
-
processes?: string[];
|
|
114
|
-
decisions?: string[];
|
|
115
|
-
forms?: string[];
|
|
116
|
-
};
|
|
117
|
-
data?: {
|
|
118
|
-
default?: string;
|
|
119
|
-
sources?: Record<string, DataSource>;
|
|
120
|
-
};
|
|
121
|
-
types?: Record<string, DomainType>;
|
|
122
|
-
triggers?: TriggerDecl[];
|
|
123
|
-
connections?: Record<string, {
|
|
124
|
-
type: string;
|
|
125
|
-
[k: string]: unknown;
|
|
126
|
-
}>;
|
|
127
|
-
surfaces?: Record<string, SurfaceDecl>;
|
|
128
|
-
actions?: ActionDecl[];
|
|
129
|
-
workers?: WorkerDecl[];
|
|
130
|
-
llm?: Record<string, LlmBinding>;
|
|
131
|
-
security?: Record<string, unknown>;
|
|
132
|
-
[k: string]: unknown;
|
|
133
|
-
}
|
|
134
|
-
/** The normalised job type for a worker declaration (`taskType` preferred, `type` fallback). */
|
|
135
|
-
export declare function workerJobType(w: WorkerDecl): string | undefined;
|
|
2
|
+
import type { AppManifest, Worker } from "@nanobpm/nano-app-schema";
|
|
3
|
+
export type { ActionDecl, AppManifest, ChatSurface, Connection, Data, DataSource, DomainField, DomainType, LlmBinding, Models, PagesSurface, Runtime, Security, Surfaces, TaskInboxSurface, Trigger, TriggerAction, Worker, } from "@nanobpm/nano-app-schema";
|
|
4
|
+
/** The job type a worker subscribes to (schema key: `taskType`). */
|
|
5
|
+
export declare function workerJobType(w: Worker): string | undefined;
|
|
136
6
|
/**
|
|
137
7
|
* Expand `${VAR}` and `${VAR:-default}` placeholders in a string using `lookup`.
|
|
138
8
|
* Unset vars with no default expand to "" (matching common shell semantics).
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
// Manifest types
|
|
2
|
-
//
|
|
3
|
-
// The
|
|
4
|
-
//
|
|
5
|
-
|
|
1
|
+
// Manifest types + env-placeholder expansion + a host-driven loader.
|
|
2
|
+
//
|
|
3
|
+
// The App manifest contract (nano.app.json) is owned by the canonical
|
|
4
|
+
// @nanobpm/nano-app-schema package (ADR 0027): the JSON Schema and its generated
|
|
5
|
+
// TypeScript types are the single source of truth. This module re-exports those
|
|
6
|
+
// types (type-only, so nothing from the package is pulled into the runtime
|
|
7
|
+
// bundle) and adds the runtime-side helpers the App loader needs. No shape is
|
|
8
|
+
// hand-mirrored here — that eliminated the schema/type drift the vendored copies
|
|
9
|
+
// used to cause.
|
|
10
|
+
/** The job type a worker subscribes to (schema key: `taskType`). */
|
|
6
11
|
export function workerJobType(w) {
|
|
7
|
-
return w.taskType
|
|
12
|
+
return w.taskType;
|
|
8
13
|
}
|
|
9
14
|
const ENV_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g;
|
|
10
15
|
/**
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { AppApi, Mounted, RuntimeContext } from "../context.ts";
|
|
2
2
|
import { type Route } from "../router.ts";
|
|
3
|
-
import type { TriggerDecl } from "../manifest.ts";
|
|
4
3
|
export interface TriggersHandle extends Mounted {
|
|
5
4
|
routes: Route[];
|
|
6
5
|
}
|
|
@@ -38,7 +37,20 @@ export declare function resolveActionVariables(expr: unknown, scope: Scope): Rec
|
|
|
38
37
|
* of `correlationKey`/`variables`; cron passes a minimal `{ firedAt }` body. Returns what
|
|
39
38
|
* was done.
|
|
40
39
|
*/
|
|
41
|
-
|
|
40
|
+
/**
|
|
41
|
+
* The trigger action as the *runtime* helper reads it: the fields `runTriggerAction`
|
|
42
|
+
* consumes, with `variables` permitting an inline object literal as well as a FEEL string.
|
|
43
|
+
* The manifest surface keeps `variables` string-only (a FEEL expression);
|
|
44
|
+
* `resolveActionVariables` tolerates both, so this shape matches runtime behaviour without
|
|
45
|
+
* loosening the schema. A locked `Trigger["action"]` is assignable to it.
|
|
46
|
+
*/
|
|
47
|
+
export interface RuntimeTriggerAction {
|
|
48
|
+
start?: string;
|
|
49
|
+
message?: string;
|
|
50
|
+
correlationKey?: string;
|
|
51
|
+
variables?: string | Record<string, unknown>;
|
|
52
|
+
}
|
|
53
|
+
export declare function runTriggerAction(app: AppApi, action: RuntimeTriggerAction | undefined, scope: Scope): Promise<{
|
|
42
54
|
kind: "start" | "message" | "none";
|
|
43
55
|
target?: string;
|
|
44
56
|
correlationKey?: string;
|
|
@@ -75,12 +75,6 @@ function headerRecord(req) {
|
|
|
75
75
|
req.headers.forEach((v, k) => (out[k.toLowerCase()] = v));
|
|
76
76
|
return out;
|
|
77
77
|
}
|
|
78
|
-
/**
|
|
79
|
-
* Fire a trigger's action against the engine: start a process or publish a message
|
|
80
|
-
* (ADR 0025 §1). `scope` supplies the event body/headers/query for FEEL-ish resolution
|
|
81
|
-
* of `correlationKey`/`variables`; cron passes a minimal `{ firedAt }` body. Returns what
|
|
82
|
-
* was done.
|
|
83
|
-
*/
|
|
84
78
|
export async function runTriggerAction(app, action, scope) {
|
|
85
79
|
if (!action)
|
|
86
80
|
return { kind: "none" };
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
// Manifest validation. We deliberately avoid a heavyweight JSON-Schema engine here to
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// — required keys, allowed keys
|
|
5
|
-
// the id slug pattern — and then
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// from it
|
|
9
|
-
|
|
1
|
+
// Manifest validation. We deliberately avoid a heavyweight JSON-Schema engine here to keep
|
|
2
|
+
// core lean and runtime-agnostic. Instead we drive validation *from* the canonical
|
|
3
|
+
// nano-app.schema.json (the ADR 0027 source of truth, imported from the
|
|
4
|
+
// @nanobpm/nano-app-schema package) for the top-level envelope — required keys, allowed keys
|
|
5
|
+
// (additionalProperties:false), the schemaVersion const, and the id slug pattern — and then
|
|
6
|
+
// layer the runtime's binding rules (a worker needs a handler, a data source needs
|
|
7
|
+
// driver+url, a typed table needs fields, a trigger needs id+type). Importing the shared
|
|
8
|
+
// schema JSON means the envelope check tracks the published schema and can't drift from it
|
|
9
|
+
// silently — the whole point of consuming the package rather than vendoring a copy.
|
|
10
|
+
import schema from "@nanobpm/nano-app-schema/schema" with { type: "json" };
|
|
10
11
|
import { workerJobType } from "./manifest.js";
|
|
11
12
|
export class ManifestValidationError extends Error {
|
|
12
13
|
issues;
|
|
@@ -53,7 +54,7 @@ export function collectManifestIssues(m) {
|
|
|
53
54
|
if (Array.isArray(man.workers)) {
|
|
54
55
|
man.workers.forEach((w, i) => {
|
|
55
56
|
if (!workerJobType(w)) {
|
|
56
|
-
issues.push({ path: `workers[${i}]`, message: "missing taskType
|
|
57
|
+
issues.push({ path: `workers[${i}]`, message: "missing taskType" });
|
|
57
58
|
}
|
|
58
59
|
// The schema allows a worker backed by either a `handler` file or an `llm`
|
|
59
60
|
// binding (oneOf). Require exactly one — don't force `handler`.
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { createUrbanApp } from "./core/runtime.ts";
|
|
2
2
|
export type { CreateUrbanAppOptions, MountFlags, UrbanApp, } from "./core/runtime.ts";
|
|
3
3
|
export { expandEnv, expandEnvString, loadManifest, parseManifest, workerJobType, } from "./core/manifest.ts";
|
|
4
|
-
export type { ActionDecl, AppManifest, DataSource, DomainType, LlmBinding,
|
|
4
|
+
export type { ActionDecl, AppManifest, ChatSurface, DataSource, DomainField, DomainType, LlmBinding, PagesSurface, Surfaces, TaskInboxSurface, Trigger, Worker, } from "./core/manifest.ts";
|
|
5
5
|
export { collectManifestIssues, ManifestValidationError, validateManifest, } from "./core/validate.ts";
|
|
6
6
|
export type { ValidationIssue } from "./core/validate.ts";
|
|
7
7
|
export type { EngineClient, EngineJob, HostContext, HttpHandler, HttpRequest, HttpResponse, HttpServer, JobHandler, SqliteDb, WatchHandle, WorkerSubscription, } from "./core/host.ts";
|
|
@@ -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
|
+
}>;
|