@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
|
@@ -100,12 +100,19 @@ export class Table<T extends object = Row> {
|
|
|
100
100
|
this.pk = pk;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
/** Insert one row
|
|
104
|
-
*
|
|
103
|
+
/** Insert one row; returns the new primary-key value (the inserted rowid for an INTEGER
|
|
104
|
+
* PRIMARY KEY). Keys whose value is `undefined` are omitted, so the table's own column
|
|
105
|
+
* `DEFAULT`/`NULL` governs — `undefined` means "not provided, let the schema decide", never
|
|
106
|
+
* a bound value. An explicit `null` is preserved (it stores `NULL`). */
|
|
105
107
|
async insert(row: Partial<T>): Promise<number | bigint> {
|
|
106
|
-
const
|
|
108
|
+
const provided = Object.keys(row);
|
|
109
|
+
const keys = provided.filter((k) => (row as Row)[k] !== undefined);
|
|
107
110
|
if (keys.length === 0) {
|
|
108
|
-
throw new Error(
|
|
111
|
+
throw new Error(
|
|
112
|
+
provided.length === 0
|
|
113
|
+
? `Table(${this.name}).insert: no columns to insert (empty row)`
|
|
114
|
+
: `Table(${this.name}).insert: no columns to insert (all values were undefined)`,
|
|
115
|
+
);
|
|
109
116
|
}
|
|
110
117
|
const cols = keys.map(quoteIdent).join(", ");
|
|
111
118
|
const ph = keys.map(() => "?").join(", ");
|
|
@@ -158,9 +165,11 @@ export class Table<T extends object = Row> {
|
|
|
158
165
|
return rows[0] as T | undefined;
|
|
159
166
|
}
|
|
160
167
|
|
|
161
|
-
/** Patch the row with the given primary key; returns rows changed.
|
|
168
|
+
/** Patch the row with the given primary key; returns rows changed. Keys whose value is
|
|
169
|
+
* `undefined` are skipped (that column is left unchanged); an explicit `null` clears the
|
|
170
|
+
* column to `NULL`. A patch with no defined keys is a no-op. */
|
|
162
171
|
async update(id: unknown, patch: Partial<T>): Promise<number> {
|
|
163
|
-
const keys = Object.keys(patch);
|
|
172
|
+
const keys = Object.keys(patch).filter((k) => (patch as Row)[k] !== undefined);
|
|
164
173
|
if (keys.length === 0) return 0;
|
|
165
174
|
const set = keys.map((k) => `${quoteIdent(k)} = ?`).join(", ");
|
|
166
175
|
const r = await this.#src.exec(
|
|
@@ -2,7 +2,7 @@ import { test } from "node:test";
|
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
3
|
import type { AppApi, RuntimeContext } from "../context.ts";
|
|
4
4
|
import type { HttpRequest } from "../host.ts";
|
|
5
|
-
import type {
|
|
5
|
+
import type { Trigger } from "../manifest.ts";
|
|
6
6
|
import { makeRouter } from "../router.ts";
|
|
7
7
|
import { evalCorrelation, mountTriggers, runTriggerAction, type SchedulerDeps } from "./triggers.ts";
|
|
8
8
|
|
|
@@ -33,7 +33,7 @@ function fakeApp(): { app: AppApi; calls: EngineCall[]; logs: Array<{ level: str
|
|
|
33
33
|
return { app, calls, logs };
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
function fakeCtx(triggers:
|
|
36
|
+
function fakeCtx(triggers: Trigger[]): { ctx: RuntimeContext; hostLogs: Array<{ level: string; msg: string }> } {
|
|
37
37
|
const hostLogs: Array<{ level: string; msg: string }> = [];
|
|
38
38
|
const ctx = {
|
|
39
39
|
root: "/app",
|
|
@@ -113,6 +113,8 @@ test("evalCorrelation returns undefined for a non-scalar result", () => {
|
|
|
113
113
|
|
|
114
114
|
test("runTriggerAction starts a process with action.variables", async () => {
|
|
115
115
|
const { app, calls } = fakeApp();
|
|
116
|
+
// The manifest surface types `variables` as a FEEL string; the runtime helper
|
|
117
|
+
// (runTriggerAction/resolveActionVariables) also accepts an inline object literal.
|
|
116
118
|
const res = await runTriggerAction(app, { start: "proc-1", variables: { a: 1 } }, {
|
|
117
119
|
body: { ignored: true },
|
|
118
120
|
headers: {},
|
|
@@ -207,7 +209,7 @@ test("cron trigger with an invalid spec logs an error and does not schedule", ()
|
|
|
207
209
|
|
|
208
210
|
test("cron trigger with no actionable action is not scheduled", () => {
|
|
209
211
|
const { app, logs } = fakeApp();
|
|
210
|
-
const { ctx } = fakeCtx([{ id: "noop", type: "cron", spec: "0 6 * * *" }]);
|
|
212
|
+
const { ctx } = fakeCtx([{ id: "noop", type: "cron", spec: "0 6 * * *", action: {} }]);
|
|
211
213
|
const sched = fakeScheduler(0);
|
|
212
214
|
const handle = mountTriggers(ctx, app, sched);
|
|
213
215
|
assert.equal(sched.pending(), 0);
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import type { AppApi, Mounted, RuntimeContext } from "../context.ts";
|
|
7
7
|
import type { HttpRequest } from "../host.ts";
|
|
8
8
|
import { json, normalizeRoutePath, type Route } from "../router.ts";
|
|
9
|
-
import type {
|
|
9
|
+
import type { Trigger } from "../manifest.ts";
|
|
10
10
|
import { nextCronFire, parseCron } from "./cron.ts";
|
|
11
11
|
|
|
12
12
|
export interface TriggersHandle extends Mounted {
|
|
@@ -112,9 +112,23 @@ function headerRecord(req: HttpRequest): Record<string, string> {
|
|
|
112
112
|
* of `correlationKey`/`variables`; cron passes a minimal `{ firedAt }` body. Returns what
|
|
113
113
|
* was done.
|
|
114
114
|
*/
|
|
115
|
+
/**
|
|
116
|
+
* The trigger action as the *runtime* helper reads it: the fields `runTriggerAction`
|
|
117
|
+
* consumes, with `variables` permitting an inline object literal as well as a FEEL string.
|
|
118
|
+
* The manifest surface keeps `variables` string-only (a FEEL expression);
|
|
119
|
+
* `resolveActionVariables` tolerates both, so this shape matches runtime behaviour without
|
|
120
|
+
* loosening the schema. A locked `Trigger["action"]` is assignable to it.
|
|
121
|
+
*/
|
|
122
|
+
export interface RuntimeTriggerAction {
|
|
123
|
+
start?: string;
|
|
124
|
+
message?: string;
|
|
125
|
+
correlationKey?: string;
|
|
126
|
+
variables?: string | Record<string, unknown>;
|
|
127
|
+
}
|
|
128
|
+
|
|
115
129
|
export async function runTriggerAction(
|
|
116
130
|
app: AppApi,
|
|
117
|
-
action:
|
|
131
|
+
action: RuntimeTriggerAction | undefined,
|
|
118
132
|
scope: Scope,
|
|
119
133
|
): Promise<{ kind: "start" | "message" | "none"; target?: string; correlationKey?: string }> {
|
|
120
134
|
if (!action) return { kind: "none" };
|
|
@@ -161,7 +175,7 @@ export function mountTriggers(
|
|
|
161
175
|
let stopped = false;
|
|
162
176
|
|
|
163
177
|
// Arm a cron trigger: compute the next UTC fire, sleep until it, fire the action, repeat.
|
|
164
|
-
const armCron = (trig:
|
|
178
|
+
const armCron = (trig: Trigger): void => {
|
|
165
179
|
const spec = trig.spec;
|
|
166
180
|
if (!spec) {
|
|
167
181
|
ctx.host.log("warn", `trigger "${trig.id}": cron trigger has no spec, skipped`);
|
|
@@ -241,7 +255,7 @@ export function mountTriggers(
|
|
|
241
255
|
arm();
|
|
242
256
|
};
|
|
243
257
|
|
|
244
|
-
for (const trig of (ctx.manifest.triggers ?? []) as
|
|
258
|
+
for (const trig of (ctx.manifest.triggers ?? []) as Trigger[]) {
|
|
245
259
|
if (trig.type === "cron") {
|
|
246
260
|
armCron(trig);
|
|
247
261
|
continue;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import type { AppApi, Mounted, RuntimeContext } from "../context.ts";
|
|
6
6
|
import type { EngineJob, JobHandler, WorkerSubscription } from "../host.ts";
|
|
7
|
-
import { workerJobType, type
|
|
7
|
+
import { workerJobType, type Worker } from "../manifest.ts";
|
|
8
8
|
import { type DecisionEvaluator, type LlmRuntime, runLlmJob } from "./llm.ts";
|
|
9
9
|
|
|
10
10
|
/** The subset of the engine SDK the LLM decision-rails need: evaluate a DMN decision,
|
|
@@ -106,7 +106,7 @@ export async function mountWorkers(ctx: RuntimeContext, app: AppApi): Promise<Wo
|
|
|
106
106
|
return mod;
|
|
107
107
|
};
|
|
108
108
|
|
|
109
|
-
for (const decl of decls as
|
|
109
|
+
for (const decl of decls as Worker[]) {
|
|
110
110
|
const jobType = workerJobType(decl);
|
|
111
111
|
if (!jobType) continue;
|
|
112
112
|
if (!decl.handler) {
|
|
@@ -152,6 +152,12 @@ test("runtime materializes the manifest end-to-end against a fake engine", async
|
|
|
152
152
|
|
|
153
153
|
// field-drift guard
|
|
154
154
|
assert.throws(() => app.data!.repo("task").insert({ bogus: 1 }));
|
|
155
|
+
// all-declared-but-undefined payload throws (parity with Table.insert): the caller
|
|
156
|
+
// meant to write those columns, so we don't silently insert a DEFAULT VALUES row.
|
|
157
|
+
assert.throws(
|
|
158
|
+
() => app.data!.repo("task").insert({ title: undefined, status: undefined }),
|
|
159
|
+
/all values were undefined/,
|
|
160
|
+
);
|
|
155
161
|
} finally {
|
|
156
162
|
await app.stop();
|
|
157
163
|
await rm(dir, { recursive: true, force: true });
|
|
@@ -1,13 +1,14 @@
|
|
|
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
|
|
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.
|
|
9
10
|
|
|
10
|
-
import schema from "
|
|
11
|
+
import schema from "@nanobpm/nano-app-schema/schema" with { type: "json" };
|
|
11
12
|
import { workerJobType, type AppManifest } from "./manifest.ts";
|
|
12
13
|
|
|
13
14
|
export interface ValidationIssue {
|
|
@@ -73,7 +74,7 @@ export function collectManifestIssues(m: unknown): ValidationIssue[] {
|
|
|
73
74
|
if (Array.isArray(man.workers)) {
|
|
74
75
|
man.workers.forEach((w, i) => {
|
|
75
76
|
if (!workerJobType(w)) {
|
|
76
|
-
issues.push({ path: `workers[${i}]`, message: "missing taskType
|
|
77
|
+
issues.push({ path: `workers[${i}]`, message: "missing taskType" });
|
|
77
78
|
}
|
|
78
79
|
// The schema allows a worker backed by either a `handler` file or an `llm`
|
|
79
80
|
// binding (oneOf). Require exactly one — don't force `handler`.
|
package/src/runtime/index.ts
CHANGED
|
@@ -19,13 +19,16 @@ export {
|
|
|
19
19
|
export type {
|
|
20
20
|
ActionDecl,
|
|
21
21
|
AppManifest,
|
|
22
|
+
ChatSurface,
|
|
22
23
|
DataSource,
|
|
24
|
+
DomainField,
|
|
23
25
|
DomainType,
|
|
24
26
|
LlmBinding,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
PagesSurface,
|
|
28
|
+
Surfaces,
|
|
29
|
+
TaskInboxSurface,
|
|
30
|
+
Trigger,
|
|
31
|
+
Worker,
|
|
29
32
|
} from "./core/manifest.ts";
|
|
30
33
|
|
|
31
34
|
// Validation
|