@nanobpm/urban 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/runtime/core/manifest.d.ts +4 -134
- package/dist/runtime/core/manifest.js +11 -6
- package/dist/runtime/core/modules/datasource.js +10 -1
- package/dist/runtime/core/modules/gateway.d.ts +7 -3
- package/dist/runtime/core/modules/gateway.js +13 -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/package.json +4 -3
- package/src/runtime/core/manifest.test.ts +2 -3
- package/src/runtime/core/manifest.ts +35 -134
- package/src/runtime/core/modules/datasource.ts +12 -1
- package/src/runtime/core/modules/gateway.test.ts +57 -0
- package/src/runtime/core/modules/gateway.ts +15 -6
- 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/runtime.test.ts +6 -0
- package/src/runtime/core/validate.ts +11 -10
- package/src/runtime/index.ts +7 -4
- 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
|
/**
|
|
@@ -48,8 +48,17 @@ export class TypeRepo {
|
|
|
48
48
|
}
|
|
49
49
|
insert(row) {
|
|
50
50
|
this.assertFields(row);
|
|
51
|
-
|
|
51
|
+
// Omit keys whose value is `undefined` so the column's own `DEFAULT`/`NULL` governs;
|
|
52
|
+
// `undefined` means "not provided", never a bound value. An explicit `null` is preserved.
|
|
53
|
+
const provided = Object.keys(row);
|
|
54
|
+
const keys = provided.filter((k) => row[k] !== undefined);
|
|
52
55
|
if (keys.length === 0) {
|
|
56
|
+
// A present-but-all-undefined payload throws (parity with Table.insert): the caller
|
|
57
|
+
// meant to write those values, so silently inserting a full DEFAULT row would create
|
|
58
|
+
// an unintended record. A genuinely empty `insert({})` is the explicit "default row".
|
|
59
|
+
if (provided.length > 0) {
|
|
60
|
+
throw new Error(`insert into type "${this.typeName}": no columns to insert (all values were undefined)`);
|
|
61
|
+
}
|
|
53
62
|
return this.db.run(`INSERT INTO ${this.table()} DEFAULT VALUES`, []);
|
|
54
63
|
}
|
|
55
64
|
const cols = keys.join(", ");
|
|
@@ -59,8 +59,10 @@ export declare class Table<T extends object = Row> {
|
|
|
59
59
|
readonly name: string;
|
|
60
60
|
readonly pk: string;
|
|
61
61
|
constructor(src: DataSource, name: string, pk?: string);
|
|
62
|
-
/** Insert one row
|
|
63
|
-
*
|
|
62
|
+
/** Insert one row; returns the new primary-key value (the inserted rowid for an INTEGER
|
|
63
|
+
* PRIMARY KEY). Keys whose value is `undefined` are omitted, so the table's own column
|
|
64
|
+
* `DEFAULT`/`NULL` governs — `undefined` means "not provided, let the schema decide", never
|
|
65
|
+
* a bound value. An explicit `null` is preserved (it stores `NULL`). */
|
|
64
66
|
insert(row: Partial<T>): Promise<number | bigint>;
|
|
65
67
|
/** Fetch the row with the given primary key, or `undefined`. */
|
|
66
68
|
get(id: unknown): Promise<T | undefined>;
|
|
@@ -70,7 +72,9 @@ export declare class Table<T extends object = Row> {
|
|
|
70
72
|
find(where?: Partial<T>): Promise<T[]>;
|
|
71
73
|
/** The first row matching an equality filter, or `undefined`. */
|
|
72
74
|
findOne(where?: Partial<T>): Promise<T | undefined>;
|
|
73
|
-
/** Patch the row with the given primary key; returns rows changed.
|
|
75
|
+
/** Patch the row with the given primary key; returns rows changed. Keys whose value is
|
|
76
|
+
* `undefined` are skipped (that column is left unchanged); an explicit `null` clears the
|
|
77
|
+
* column to `NULL`. A patch with no defined keys is a no-op. */
|
|
74
78
|
update(id: unknown, patch: Partial<T>): Promise<number>;
|
|
75
79
|
/** Delete the row with the given primary key; returns rows changed. */
|
|
76
80
|
delete(id: unknown): Promise<number>;
|
|
@@ -40,12 +40,17 @@ export class Table {
|
|
|
40
40
|
this.name = name;
|
|
41
41
|
this.pk = pk;
|
|
42
42
|
}
|
|
43
|
-
/** Insert one row
|
|
44
|
-
*
|
|
43
|
+
/** Insert one row; returns the new primary-key value (the inserted rowid for an INTEGER
|
|
44
|
+
* PRIMARY KEY). Keys whose value is `undefined` are omitted, so the table's own column
|
|
45
|
+
* `DEFAULT`/`NULL` governs — `undefined` means "not provided, let the schema decide", never
|
|
46
|
+
* a bound value. An explicit `null` is preserved (it stores `NULL`). */
|
|
45
47
|
async insert(row) {
|
|
46
|
-
const
|
|
48
|
+
const provided = Object.keys(row);
|
|
49
|
+
const keys = provided.filter((k) => row[k] !== undefined);
|
|
47
50
|
if (keys.length === 0) {
|
|
48
|
-
throw new Error(
|
|
51
|
+
throw new Error(provided.length === 0
|
|
52
|
+
? `Table(${this.name}).insert: no columns to insert (empty row)`
|
|
53
|
+
: `Table(${this.name}).insert: no columns to insert (all values were undefined)`);
|
|
49
54
|
}
|
|
50
55
|
const cols = keys.map(quoteIdent).join(", ");
|
|
51
56
|
const ph = keys.map(() => "?").join(", ");
|
|
@@ -80,9 +85,11 @@ export class Table {
|
|
|
80
85
|
const rows = await this.#src.query(`SELECT * FROM ${quoteIdent(this.name)}${clause} LIMIT 1`, params);
|
|
81
86
|
return rows[0];
|
|
82
87
|
}
|
|
83
|
-
/** Patch the row with the given primary key; returns rows changed.
|
|
88
|
+
/** Patch the row with the given primary key; returns rows changed. Keys whose value is
|
|
89
|
+
* `undefined` are skipped (that column is left unchanged); an explicit `null` clears the
|
|
90
|
+
* column to `NULL`. A patch with no defined keys is a no-op. */
|
|
84
91
|
async update(id, patch) {
|
|
85
|
-
const keys = Object.keys(patch);
|
|
92
|
+
const keys = Object.keys(patch).filter((k) => patch[k] !== undefined);
|
|
86
93
|
if (keys.length === 0)
|
|
87
94
|
return 0;
|
|
88
95
|
const set = keys.map((k) => `${quoteIdent(k)} = ?`).join(", ");
|
|
@@ -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";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/urban",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
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,14 +55,15 @@
|
|
|
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-
|
|
65
|
+
"@nanobpm/nano-app-schema": "^0.1.0",
|
|
66
|
+
"@nanobpm/nano-sdk": "^1.2.3",
|
|
66
67
|
"@nanobpm/workflow": "^0.7.0",
|
|
67
68
|
"bpmn-auto-layout": "^2.0.0-alpha.2",
|
|
68
69
|
"create-urban-app": "^0.5.0"
|
|
@@ -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
|
});
|
|
@@ -1,139 +1,40 @@
|
|
|
1
|
-
// Manifest types
|
|
2
|
-
//
|
|
3
|
-
// The
|
|
4
|
-
//
|
|
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.
|
|
5
10
|
|
|
6
11
|
import type { HostContext } from "./host.ts";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* One named LLM binding (`llm.<id>` in the manifest). A binding is usable as a worker
|
|
37
|
-
* (LLM-as-worker, ADR 0022 §E role 1) or as a chat surface agent. `provider` selects how
|
|
38
|
-
* the endpoint is resolved (`"env"` reads it from the environment); `model` is typically
|
|
39
|
-
* an env template. When `output.decision` is set, the model's JSON reply is fed through
|
|
40
|
-
* that DMN decision (the "rails") and the decision's output is returned instead.
|
|
41
|
-
*/
|
|
42
|
-
export interface LlmBinding {
|
|
43
|
-
/** Provider selector. `"env"` resolves an OpenAI-compatible endpoint from the environment. */
|
|
44
|
-
provider: string;
|
|
45
|
-
/** Model id, typically an env template (e.g. `${NANO_APP_LLM_MODEL}`). */
|
|
46
|
-
model: string;
|
|
47
|
-
/** Constrains the model's structured output — currently a DMN decision id (the rails). */
|
|
48
|
-
output?: { decision?: string };
|
|
49
|
-
/** Action-API tools the agent may call (chat-agent role; unused by the worker role). */
|
|
50
|
-
tools?: string[];
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export interface TriggerDecl {
|
|
54
|
-
id: string;
|
|
55
|
-
type: string;
|
|
56
|
-
path?: string;
|
|
57
|
-
auth?: string;
|
|
58
|
-
/** cron: the 5-field crontab spec (UTC), e.g. "0 6 * * *". */
|
|
59
|
-
spec?: string;
|
|
60
|
-
/** cron catch-up policy for fires missed while the app was down. Default "skip". */
|
|
61
|
-
onMissed?: "skip" | "once" | "all";
|
|
62
|
-
/** Source-kind-specific settings (e.g. file `{ pollMs }`). */
|
|
63
|
-
config?: Record<string, unknown>;
|
|
64
|
-
/** Name of a connections[] entry supplying this source's credentials. */
|
|
65
|
-
connection?: string;
|
|
66
|
-
/** Maps the event to one engine call: start a process OR publish a message (ADR 0025 §1). */
|
|
67
|
-
action?: {
|
|
68
|
-
/** Process id/name to start. */
|
|
69
|
-
start?: string;
|
|
70
|
-
/** messageName to publish as a CorrelateMessage. */
|
|
71
|
-
message?: string;
|
|
72
|
-
/** correlationKey (literal, or `= body.path`) for a message action. */
|
|
73
|
-
correlationKey?: string;
|
|
74
|
-
/**
|
|
75
|
-
* Variables for the started instance / published message. Per the manifest schema
|
|
76
|
-
* (`#/$defs/triggerAction.variables`) this is a FEEL string over the event body
|
|
77
|
-
* (e.g. `= body.payload`); an inline object literal is also accepted for convenience.
|
|
78
|
-
*/
|
|
79
|
-
variables?: string | Record<string, unknown>;
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export interface SurfaceDecl {
|
|
84
|
-
enabled?: boolean;
|
|
85
|
-
path?: string;
|
|
86
|
-
agent?: string;
|
|
87
|
-
/** pages surface: directory of `*.page.json` (relative to app root). Default `pages`. */
|
|
88
|
-
pagesDir?: string;
|
|
89
|
-
/** pages surface: the page served at `/`. Default `home`. */
|
|
90
|
-
homePage?: string;
|
|
91
|
-
/** pages surface: max rows a `dataGrid` fetch returns. Default 200. */
|
|
92
|
-
rowLimit?: number;
|
|
93
|
-
/** pages surface: the injected default datasource name. Default `app`. */
|
|
94
|
-
sourceName?: string;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* An app-authored action handler override (ADR 0055 phase 3). Each declaration binds a
|
|
99
|
-
* route to a handler module (default-exports an `ActionHandler`), letting an app wrap the
|
|
100
|
-
* generic pages start/cancel/message actions with business logic. Mounted before the
|
|
101
|
-
* generic pages action routes, so an exact override shadows the generic one.
|
|
102
|
-
*/
|
|
103
|
-
export interface ActionDecl {
|
|
104
|
-
/** Route path to serve, e.g. "/app/actions/cancel" or "/app/actions/start/convergence-loop". */
|
|
105
|
-
path: string;
|
|
106
|
-
/** Handler module path relative to the app root; default-exports an `ActionHandler`. */
|
|
107
|
-
module: string;
|
|
108
|
-
/** HTTP method to match. Default "POST". */
|
|
109
|
-
method?: string;
|
|
110
|
-
/** Match `path` as a prefix rather than exactly. Default false. */
|
|
111
|
-
prefix?: boolean;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export interface AppManifest {
|
|
115
|
-
$schema?: string;
|
|
116
|
-
schemaVersion: 1;
|
|
117
|
-
id: string;
|
|
118
|
-
name: string;
|
|
119
|
-
codename?: string;
|
|
120
|
-
runtime?: { engine?: string; node?: string };
|
|
121
|
-
models?: { processes?: string[]; decisions?: string[]; forms?: string[] };
|
|
122
|
-
data?: { default?: string; sources?: Record<string, DataSource> };
|
|
123
|
-
types?: Record<string, DomainType>;
|
|
124
|
-
triggers?: TriggerDecl[];
|
|
125
|
-
connections?: Record<string, { type: string; [k: string]: unknown }>;
|
|
126
|
-
surfaces?: Record<string, SurfaceDecl>;
|
|
127
|
-
actions?: ActionDecl[];
|
|
128
|
-
workers?: WorkerDecl[];
|
|
129
|
-
llm?: Record<string, LlmBinding>;
|
|
130
|
-
security?: Record<string, unknown>;
|
|
131
|
-
[k: string]: unknown;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/** The normalised job type for a worker declaration (`taskType` preferred, `type` fallback). */
|
|
135
|
-
export function workerJobType(w: WorkerDecl): string | undefined {
|
|
136
|
-
return w.taskType ?? w.type;
|
|
12
|
+
import type { AppManifest, Worker } from "@nanobpm/nano-app-schema";
|
|
13
|
+
|
|
14
|
+
export type {
|
|
15
|
+
ActionDecl,
|
|
16
|
+
AppManifest,
|
|
17
|
+
ChatSurface,
|
|
18
|
+
Connection,
|
|
19
|
+
Data,
|
|
20
|
+
DataSource,
|
|
21
|
+
DomainField,
|
|
22
|
+
DomainType,
|
|
23
|
+
LlmBinding,
|
|
24
|
+
Models,
|
|
25
|
+
PagesSurface,
|
|
26
|
+
Runtime,
|
|
27
|
+
Security,
|
|
28
|
+
Surfaces,
|
|
29
|
+
TaskInboxSurface,
|
|
30
|
+
Trigger,
|
|
31
|
+
TriggerAction,
|
|
32
|
+
Worker,
|
|
33
|
+
} from "@nanobpm/nano-app-schema";
|
|
34
|
+
|
|
35
|
+
/** The job type a worker subscribes to (schema key: `taskType`). */
|
|
36
|
+
export function workerJobType(w: Worker): string | undefined {
|
|
37
|
+
return w.taskType;
|
|
137
38
|
}
|
|
138
39
|
|
|
139
40
|
const ENV_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g;
|
|
@@ -60,8 +60,19 @@ export class TypeRepo {
|
|
|
60
60
|
|
|
61
61
|
insert(row: Record<string, unknown>): { changes: number; lastInsertRowid: number | bigint } {
|
|
62
62
|
this.assertFields(row);
|
|
63
|
-
|
|
63
|
+
// Omit keys whose value is `undefined` so the column's own `DEFAULT`/`NULL` governs;
|
|
64
|
+
// `undefined` means "not provided", never a bound value. An explicit `null` is preserved.
|
|
65
|
+
const provided = Object.keys(row);
|
|
66
|
+
const keys = provided.filter((k) => row[k] !== undefined);
|
|
64
67
|
if (keys.length === 0) {
|
|
68
|
+
// A present-but-all-undefined payload throws (parity with Table.insert): the caller
|
|
69
|
+
// meant to write those values, so silently inserting a full DEFAULT row would create
|
|
70
|
+
// an unintended record. A genuinely empty `insert({})` is the explicit "default row".
|
|
71
|
+
if (provided.length > 0) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`insert into type "${this.typeName}": no columns to insert (all values were undefined)`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
65
76
|
return this.db.run(`INSERT INTO ${this.table()} DEFAULT VALUES`, []);
|
|
66
77
|
}
|
|
67
78
|
const cols = keys.join(", ");
|
|
@@ -162,3 +162,60 @@ test("query/exec run raw parameterised SQL", async () => {
|
|
|
162
162
|
assert.equal(Number((rows[0] as { total: number }).total), 42);
|
|
163
163
|
});
|
|
164
164
|
});
|
|
165
|
+
|
|
166
|
+
test("insert omits undefined-valued keys so the column DEFAULT applies", async () => {
|
|
167
|
+
await withGateway(async (src) => {
|
|
168
|
+
const orders = src.table<Order>("orders");
|
|
169
|
+
// `total` is `NOT NULL DEFAULT 0`; passing it as undefined must omit the column
|
|
170
|
+
// (not bind NULL, which would violate NOT NULL), letting the DEFAULT fill it.
|
|
171
|
+
const id = await orders.insert({ status: "new", total: undefined });
|
|
172
|
+
assert.equal((await orders.get(id))?.total, 0);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("insert preserves an explicit null; omits undefined", async () => {
|
|
177
|
+
await withGateway(async (src) => {
|
|
178
|
+
await src.exec("CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT, tag TEXT DEFAULT 'none')");
|
|
179
|
+
const notes = src.table("notes");
|
|
180
|
+
const id = await notes.insert({ body: null, tag: undefined });
|
|
181
|
+
const row = await notes.get(id);
|
|
182
|
+
assert.equal(row?.body, null); // explicit null is stored as NULL
|
|
183
|
+
assert.equal(row?.tag, "none"); // undefined omitted → column DEFAULT
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("update skips undefined keys and clears on explicit null", async () => {
|
|
188
|
+
await withGateway(async (src) => {
|
|
189
|
+
await src.exec("CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT, tag TEXT DEFAULT 'none')");
|
|
190
|
+
const notes = src.table("notes");
|
|
191
|
+
const id = await notes.insert({ body: "hello", tag: "x" });
|
|
192
|
+
// tag:undefined must be left unchanged; body updated.
|
|
193
|
+
await notes.update(id, { tag: undefined, body: "world" });
|
|
194
|
+
let row = await notes.get(id);
|
|
195
|
+
assert.equal(row?.body, "world");
|
|
196
|
+
assert.equal(row?.tag, "x");
|
|
197
|
+
// explicit null clears the column.
|
|
198
|
+
await notes.update(id, { body: null });
|
|
199
|
+
row = await notes.get(id);
|
|
200
|
+
assert.equal(row?.body, null);
|
|
201
|
+
// a patch of only-undefined keys is a no-op.
|
|
202
|
+
assert.equal(await notes.update(id, { tag: undefined }), 0);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("insert throws when every provided value is undefined", async () => {
|
|
207
|
+
await withGateway(async (src) => {
|
|
208
|
+
const orders = src.table<Order>("orders");
|
|
209
|
+
await assert.rejects(
|
|
210
|
+
orders.insert({ status: undefined, total: undefined }),
|
|
211
|
+
/all values were undefined/,
|
|
212
|
+
);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("insert throws a distinct message for a genuinely empty row", async () => {
|
|
217
|
+
await withGateway(async (src) => {
|
|
218
|
+
const orders = src.table<Order>("orders");
|
|
219
|
+
await assert.rejects(orders.insert({} as Partial<Order>), /empty row/);
|
|
220
|
+
});
|
|
221
|
+
});
|