@nanobpm/urban 0.20.0 → 0.22.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/README.md +35 -0
- package/dist/cli.d.ts +26 -0
- package/dist/cli.js +80 -16
- package/dist/runtime/adapters/deno.js +19 -3
- package/dist/runtime/adapters/detect.js +2 -2
- package/dist/runtime/adapters/globals.d.ts +19 -0
- package/dist/runtime/adapters/globals.js +9 -0
- package/dist/runtime/adapters/node.js +25 -5
- package/dist/runtime/core/context.d.ts +4 -0
- package/dist/runtime/core/guards.d.ts +2 -0
- package/dist/runtime/core/guards.js +10 -0
- package/dist/runtime/core/host.js +5 -4
- package/dist/runtime/core/manifest.d.ts +8 -0
- package/dist/runtime/core/manifest.js +0 -1
- package/dist/runtime/core/modules/actions.js +9 -5
- package/dist/runtime/core/modules/connectors.js +6 -4
- package/dist/runtime/core/modules/dataops.d.ts +36 -0
- package/dist/runtime/core/modules/dataops.js +231 -0
- package/dist/runtime/core/modules/datasource.d.ts +51 -1
- package/dist/runtime/core/modules/datasource.js +98 -21
- package/dist/runtime/core/modules/deploy.d.ts +3 -1
- package/dist/runtime/core/modules/deploy.js +29 -7
- package/dist/runtime/core/modules/gateway.d.ts +1 -1
- package/dist/runtime/core/modules/gateway.js +16 -17
- package/dist/runtime/core/modules/llm.js +9 -7
- package/dist/runtime/core/modules/pages.js +86 -20
- package/dist/runtime/core/modules/security.js +5 -5
- package/dist/runtime/core/modules/templates.d.ts +18 -0
- package/dist/runtime/core/modules/templates.js +133 -0
- package/dist/runtime/core/modules/triggers.js +14 -5
- package/dist/runtime/core/modules/workers.js +15 -9
- package/dist/runtime/core/runtime.d.ts +5 -0
- package/dist/runtime/core/runtime.js +5 -5
- package/dist/runtime/core/validate.d.ts +2 -1
- package/dist/runtime/core/validate.js +38 -28
- package/dist/runtime/devserver.js +2 -1
- package/dist/runtime/engine/nanosdk.d.ts +1 -1
- package/dist/runtime/engine/nanosdk.js +15 -9
- package/dist/runtime/index.d.ts +5 -0
- package/dist/runtime/index.js +4 -0
- package/dist/runtime/run.js +10 -7
- package/dist/toolkit/addConnector.js +4 -3
- package/dist/toolkit/fsio.js +2 -1
- package/dist/toolkit/models.js +3 -5
- package/package.json +1 -1
- package/src/cli.test.ts +44 -0
- package/src/cli.ts +82 -16
- package/src/runtime/adapters/deno.ts +23 -3
- package/src/runtime/adapters/detect.ts +2 -2
- package/src/runtime/adapters/globals.ts +27 -0
- package/src/runtime/adapters/node.ts +27 -4
- package/src/runtime/connector-worker-sdk.test.ts +2 -1
- package/src/runtime/core/context.ts +4 -0
- package/src/runtime/core/guards.ts +10 -0
- package/src/runtime/core/host.ts +4 -4
- package/src/runtime/core/manifest.ts +19 -5
- package/src/runtime/core/modules/actions.test.ts +82 -31
- package/src/runtime/core/modules/actions.ts +11 -7
- package/src/runtime/core/modules/connectors.test.ts +29 -8
- package/src/runtime/core/modules/connectors.ts +6 -4
- package/src/runtime/core/modules/dataops.test.ts +306 -0
- package/src/runtime/core/modules/dataops.ts +292 -0
- package/src/runtime/core/modules/datasource.migrations.test.ts +12 -3
- package/src/runtime/core/modules/datasource.test.ts +129 -0
- package/src/runtime/core/modules/datasource.ts +105 -23
- package/src/runtime/core/modules/deploy.test.ts +258 -0
- package/src/runtime/core/modules/deploy.ts +29 -7
- package/src/runtime/core/modules/gateway.test.ts +5 -4
- package/src/runtime/core/modules/gateway.ts +29 -30
- package/src/runtime/core/modules/llm.test.ts +10 -12
- package/src/runtime/core/modules/llm.ts +11 -9
- package/src/runtime/core/modules/pages.test.ts +38 -2
- package/src/runtime/core/modules/pages.ts +89 -27
- package/src/runtime/core/modules/security.ts +5 -13
- package/src/runtime/core/modules/surfaces.test.ts +53 -10
- package/src/runtime/core/modules/templates.ts +153 -0
- package/src/runtime/core/modules/triggers.test.ts +51 -19
- package/src/runtime/core/modules/triggers.ts +14 -6
- package/src/runtime/core/modules/workers.test.ts +43 -20
- package/src/runtime/core/modules/workers.ts +18 -10
- package/src/runtime/core/runtime.test.ts +23 -10
- package/src/runtime/core/runtime.ts +15 -2
- package/src/runtime/core/validate.ts +43 -32
- package/src/runtime/devserver.test.ts +18 -5
- package/src/runtime/devserver.ts +2 -1
- package/src/runtime/engine/nanosdk.test.ts +16 -11
- package/src/runtime/engine/nanosdk.ts +22 -15
- package/src/runtime/index.ts +7 -0
- package/src/runtime/run.ts +10 -10
- package/src/toolkit/addConnector.ts +6 -5
- package/src/toolkit/derivers/messages.test.ts +4 -2
- package/src/toolkit/derivers/worker-io.test.ts +4 -2
- package/src/toolkit/fsio.ts +2 -1
- package/src/toolkit/gen.ts +4 -4
- package/src/toolkit/models.test.ts +1 -1
- package/src/toolkit/models.ts +3 -4
- package/src/toolkit/scaffold.ts +1 -1
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// schema JSON means the envelope check tracks the published schema and can't drift from it
|
|
9
9
|
// silently — the whole point of consuming the package rather than vendoring a copy.
|
|
10
10
|
import schema from "@nanobpm/nano-app-schema/schema" with { type: "json" };
|
|
11
|
-
import {
|
|
11
|
+
import { isRecord } from "./guards.js";
|
|
12
12
|
export class ManifestValidationError extends Error {
|
|
13
13
|
issues;
|
|
14
14
|
constructor(issues) {
|
|
@@ -22,7 +22,7 @@ const S = schema;
|
|
|
22
22
|
/** Validate a parsed manifest. Returns the list of issues (empty === valid). */
|
|
23
23
|
export function collectManifestIssues(m) {
|
|
24
24
|
const issues = [];
|
|
25
|
-
if (
|
|
25
|
+
if (!isRecord(m)) {
|
|
26
26
|
return [{ path: "$", message: "manifest must be a JSON object" }];
|
|
27
27
|
}
|
|
28
28
|
const obj = m;
|
|
@@ -50,10 +50,11 @@ export function collectManifestIssues(m) {
|
|
|
50
50
|
issues.push({ path: "name", message: "must be a non-empty string" });
|
|
51
51
|
}
|
|
52
52
|
// — Runtime binding rules —
|
|
53
|
-
const
|
|
54
|
-
if (
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
const workers = Array.isArray(obj.workers) ? obj.workers : undefined;
|
|
54
|
+
if (workers) {
|
|
55
|
+
workers.forEach((w, i) => {
|
|
56
|
+
const worker = isRecord(w) ? w : undefined;
|
|
57
|
+
if (typeof worker?.taskType !== "string" || worker.taskType.length === 0) {
|
|
57
58
|
issues.push({ path: `workers[${i}]`, message: "missing taskType" });
|
|
58
59
|
}
|
|
59
60
|
// A worker is backed by exactly one of: a `handler` file, an `llm` binding,
|
|
@@ -62,11 +63,11 @@ export function collectManifestIssues(m) {
|
|
|
62
63
|
// `connector` actually resolves to an installed pack is a runtime seam check
|
|
63
64
|
// (mountConnectors), not something this static manifest check can see.
|
|
64
65
|
const backings = [];
|
|
65
|
-
if (typeof
|
|
66
|
+
if (typeof worker?.handler === "string" && worker.handler.length > 0)
|
|
66
67
|
backings.push("handler");
|
|
67
|
-
if (typeof
|
|
68
|
+
if (typeof worker?.llm === "string" && worker.llm.length > 0)
|
|
68
69
|
backings.push("llm");
|
|
69
|
-
if (typeof
|
|
70
|
+
if (typeof worker?.connector === "string" && worker.connector.length > 0)
|
|
70
71
|
backings.push("connector");
|
|
71
72
|
if (backings.length === 0) {
|
|
72
73
|
issues.push({
|
|
@@ -81,34 +82,38 @@ export function collectManifestIssues(m) {
|
|
|
81
82
|
});
|
|
82
83
|
}
|
|
83
84
|
// A `connection` must reference a declared top-level `connections` entry.
|
|
84
|
-
if (typeof
|
|
85
|
-
const conns =
|
|
86
|
-
?
|
|
85
|
+
if (typeof worker?.connection === "string" && worker.connection.length > 0) {
|
|
86
|
+
const conns = isRecord(obj.connections)
|
|
87
|
+
? obj.connections
|
|
87
88
|
: undefined;
|
|
88
|
-
if (!conns || !(
|
|
89
|
+
if (!conns || !(worker.connection in conns)) {
|
|
89
90
|
issues.push({
|
|
90
91
|
path: `workers[${i}].connection`,
|
|
91
|
-
message: `no such connection "${
|
|
92
|
+
message: `no such connection "${worker.connection}" (add it to connections)`,
|
|
92
93
|
});
|
|
93
94
|
}
|
|
94
95
|
}
|
|
95
96
|
});
|
|
96
97
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
const data = isRecord(obj.data) ? obj.data : undefined;
|
|
99
|
+
const sources = isRecord(data?.sources) ? data.sources : undefined;
|
|
100
|
+
if (sources) {
|
|
101
|
+
for (const [name, src] of Object.entries(sources)) {
|
|
102
|
+
const source = isRecord(src) ? src : undefined;
|
|
103
|
+
if (!source || typeof source.driver !== "string") {
|
|
100
104
|
issues.push({ path: `data.sources.${name}.driver`, message: "missing driver" });
|
|
101
105
|
}
|
|
102
|
-
if (!
|
|
106
|
+
if (!source || typeof source.url !== "string") {
|
|
103
107
|
issues.push({ path: `data.sources.${name}.url`, message: "missing url" });
|
|
104
108
|
}
|
|
105
109
|
}
|
|
106
|
-
if (
|
|
107
|
-
issues.push({ path: "data.default", message: `no such source "${
|
|
110
|
+
if (typeof data?.default === "string" && !sources[data.default]) {
|
|
111
|
+
issues.push({ path: "data.default", message: `no such source "${data.default}"` });
|
|
108
112
|
}
|
|
109
113
|
}
|
|
110
|
-
|
|
111
|
-
|
|
114
|
+
const types = isRecord(obj.types) ? obj.types : undefined;
|
|
115
|
+
if (types) {
|
|
116
|
+
for (const [name, t] of Object.entries(types)) {
|
|
112
117
|
// `table` is optional in the schema: a type may declare `fields` without a
|
|
113
118
|
// `table` (a transient / non-persisted domain type). Don't require it.
|
|
114
119
|
if (t && typeof t !== "object") {
|
|
@@ -116,20 +121,25 @@ export function collectManifestIssues(m) {
|
|
|
116
121
|
}
|
|
117
122
|
}
|
|
118
123
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
124
|
+
const triggers = Array.isArray(obj.triggers) ? obj.triggers : undefined;
|
|
125
|
+
if (triggers) {
|
|
126
|
+
triggers.forEach((t, i) => {
|
|
127
|
+
const trigger = isRecord(t) ? t : undefined;
|
|
128
|
+
if (!trigger?.id)
|
|
122
129
|
issues.push({ path: `triggers[${i}].id`, message: "missing id" });
|
|
123
|
-
if (!
|
|
130
|
+
if (!trigger?.type)
|
|
124
131
|
issues.push({ path: `triggers[${i}].type`, message: "missing type" });
|
|
125
132
|
});
|
|
126
133
|
}
|
|
127
134
|
return issues;
|
|
128
135
|
}
|
|
129
|
-
|
|
130
|
-
export function validateManifest(m) {
|
|
136
|
+
export function assertValidManifest(m) {
|
|
131
137
|
const issues = collectManifestIssues(m);
|
|
132
138
|
if (issues.length > 0)
|
|
133
139
|
throw new ManifestValidationError(issues);
|
|
140
|
+
}
|
|
141
|
+
/** Throw ManifestValidationError if the manifest is invalid; otherwise return it typed. */
|
|
142
|
+
export function validateManifest(m) {
|
|
143
|
+
assertValidManifest(m);
|
|
134
144
|
return m;
|
|
135
145
|
}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// live-reload can layer on later behind this same seam.
|
|
13
13
|
import { runFromEnv } from "./run.js";
|
|
14
14
|
import { selectHost } from "./adapters/detect.js";
|
|
15
|
+
import { errorMessage } from "./core/guards.js";
|
|
15
16
|
import { createNodeGenIO, runGen } from "../toolkit/index.js";
|
|
16
17
|
const SOURCE_EXT = [".bpmn", ".dmn", ".form", ".ts", ".js", ".mjs", ".sql"];
|
|
17
18
|
// Directories whose churn must never trigger a reload: generated output (gen writes here —
|
|
@@ -109,7 +110,7 @@ export async function runDev(opts = {}, deps) {
|
|
|
109
110
|
}
|
|
110
111
|
catch (err) {
|
|
111
112
|
// Keep the dev server alive on a bad edit; the next save will retry.
|
|
112
|
-
log(`✖ reload failed: ${
|
|
113
|
+
log(`✖ reload failed: ${errorMessage(err)}`);
|
|
113
114
|
}
|
|
114
115
|
finally {
|
|
115
116
|
reloading = false;
|
|
@@ -6,7 +6,7 @@ export declare function requireProcessInstanceKey(key: string | number | null |
|
|
|
6
6
|
/** A job as delivered to a nano-sdk job handler: the frame fields plus the
|
|
7
7
|
* acknowledgement actions the handler must call. */
|
|
8
8
|
export interface NanoSdkActivatedJob {
|
|
9
|
-
jobKey: string;
|
|
9
|
+
jobKey: string | number;
|
|
10
10
|
type?: string;
|
|
11
11
|
processInstanceKey?: string | number;
|
|
12
12
|
elementId?: string;
|
|
@@ -19,6 +19,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
19
19
|
return path;
|
|
20
20
|
};
|
|
21
21
|
import { isBpmnError } from "../core/host.js";
|
|
22
|
+
import { isRecord } from "../core/guards.js";
|
|
22
23
|
/** Coerce an engine response's process-instance key to a non-empty string, or
|
|
23
24
|
* throw — a missing key means a malformed/partial response, not a real instance. */
|
|
24
25
|
export function requireProcessInstanceKey(key) {
|
|
@@ -47,6 +48,7 @@ export class SdkEngineClient {
|
|
|
47
48
|
* the same connection. Surfaced to handlers as `AppApi.sdk`.
|
|
48
49
|
*/
|
|
49
50
|
get sdk() {
|
|
51
|
+
// biome-ignore lint/plugin: `this.client` is the same `@nanobpm/nano-sdk` client at runtime; exposing its full EngineSdkClient surface is the adapter boundary (see EngineSdkClient in ./sdk.ts).
|
|
50
52
|
return this.client;
|
|
51
53
|
}
|
|
52
54
|
async deployResources(resources) {
|
|
@@ -60,11 +62,12 @@ export class SdkEngineClient {
|
|
|
60
62
|
variables: input.variables ?? {},
|
|
61
63
|
awaitCompletion: input.awaitCompletion ?? false,
|
|
62
64
|
});
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
+
const rawKey = body.processInstanceKey ?? body.key;
|
|
66
|
+
const key = typeof rawKey === "string" || typeof rawKey === "number" ? rawKey : undefined;
|
|
67
|
+
const variables = isRecord(body.variables) ? body.variables : undefined;
|
|
65
68
|
return {
|
|
66
69
|
processInstanceKey: requireProcessInstanceKey(key),
|
|
67
|
-
variables
|
|
70
|
+
variables,
|
|
68
71
|
};
|
|
69
72
|
}
|
|
70
73
|
async cancelInstance(input) {
|
|
@@ -80,8 +83,8 @@ export class SdkEngineClient {
|
|
|
80
83
|
async searchUserTasks(filter) {
|
|
81
84
|
// User tasks are an eventually consistent read; ask for zero-wait consistency so
|
|
82
85
|
// the search reflects what is currently visible without blocking.
|
|
83
|
-
const body = await this.client.searchUserTasks({ filter: (filter ?? {}) }, { consistency: { waitUpToMs: 0 } });
|
|
84
|
-
const items = body.items
|
|
86
|
+
const body = await this.client.searchUserTasks({ filter: { ...(filter ?? {}) } }, { consistency: { waitUpToMs: 0 } });
|
|
87
|
+
const items = Array.isArray(body.items) ? body.items.filter(isRecord) : [];
|
|
85
88
|
return items.flatMap((it) => {
|
|
86
89
|
const userTaskKey = it.userTaskKey ?? it.key;
|
|
87
90
|
if (userTaskKey == null || userTaskKey === "") {
|
|
@@ -90,8 +93,8 @@ export class SdkEngineClient {
|
|
|
90
93
|
}
|
|
91
94
|
return [{
|
|
92
95
|
userTaskKey: String(userTaskKey),
|
|
93
|
-
elementId: it.elementId,
|
|
94
|
-
variables: it.variables,
|
|
96
|
+
elementId: typeof it.elementId === "string" ? it.elementId : undefined,
|
|
97
|
+
variables: isRecord(it.variables) ? it.variables : undefined,
|
|
95
98
|
}];
|
|
96
99
|
});
|
|
97
100
|
}
|
|
@@ -210,12 +213,15 @@ async function importNanoSdk() {
|
|
|
210
213
|
// Indirect the specifier so the (lazily loaded) module is not resolved at
|
|
211
214
|
// typecheck time and stays out of the import graph for injected-client paths.
|
|
212
215
|
const spec = "@nanobpm/nano-sdk";
|
|
213
|
-
const mod =
|
|
214
|
-
if (
|
|
216
|
+
const mod = await import(__rewriteRelativeImportExtension(spec));
|
|
217
|
+
if (!isNanoSdkModule(mod)) {
|
|
215
218
|
throw new Error("@nanobpm/nano-sdk does not export createCamundaClient");
|
|
216
219
|
}
|
|
217
220
|
return mod.createCamundaClient;
|
|
218
221
|
}
|
|
222
|
+
function isNanoSdkModule(value) {
|
|
223
|
+
return isRecord(value) && typeof value.createCamundaClient === "function";
|
|
224
|
+
}
|
|
219
225
|
/**
|
|
220
226
|
* Build the Urban runtime's `EngineClient`, backed by a single `@nanobpm/nano-sdk`
|
|
221
227
|
* client. Provide a `client`/`createClient` seam to inject a fake (tests, embedded
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export { adaptConnectorHandler, mountConnectors, resolveInstalledConnectors, } f
|
|
|
11
11
|
export type { ConnectorsHandle } from "./core/modules/connectors.ts";
|
|
12
12
|
export { DataLayer, TypeRepo } from "./core/modules/datasource.ts";
|
|
13
13
|
export type { ProvisionedSource } from "./core/modules/datasource.ts";
|
|
14
|
+
export { runDataOp } from "./core/modules/dataops.ts";
|
|
15
|
+
export type { DataOp, DataRequest, ResolvedSource } from "./core/modules/dataops.ts";
|
|
14
16
|
export { makeGateway, Table } from "./core/modules/gateway.ts";
|
|
15
17
|
export type { ColumnMeta, DataSource as GatewayDataSource, ExecResult, ForeignKeyMeta, Row, TableMeta, } from "./core/modules/gateway.ts";
|
|
16
18
|
export { createPagesRoutes, mountPages } from "./core/modules/pages.ts";
|
|
@@ -22,6 +24,9 @@ export { resolveHandler, sdkDecisionEvaluator } from "./core/modules/workers.ts"
|
|
|
22
24
|
export { buildMessages, callLlm, resolveProvider, runLlmJob, } from "./core/modules/llm.ts";
|
|
23
25
|
export type { ChatMessage, DecisionEvaluator, EnvLookup, LlmRuntime, LlmVars, ProviderConfig, } from "./core/modules/llm.ts";
|
|
24
26
|
export { evalCorrelation } from "./core/modules/triggers.ts";
|
|
27
|
+
export { deployModels } from "./core/modules/deploy.ts";
|
|
28
|
+
export { applyTemplates, resolveTemplates } from "./core/modules/templates.ts";
|
|
29
|
+
export type { TemplateApplication, TemplateSource } from "./core/modules/templates.ts";
|
|
25
30
|
export { createNodeHost } from "./adapters/node.ts";
|
|
26
31
|
export { createDenoHost } from "./adapters/deno.ts";
|
|
27
32
|
export { isDeno, selectHost } from "./adapters/detect.ts";
|
package/dist/runtime/index.js
CHANGED
|
@@ -12,12 +12,16 @@ export { BpmnError, isBpmnError } from "./core/host.js";
|
|
|
12
12
|
export { adaptConnectorHandler, mountConnectors, resolveInstalledConnectors, } from "./core/modules/connectors.js";
|
|
13
13
|
// Data layer
|
|
14
14
|
export { DataLayer, TypeRepo } from "./core/modules/datasource.js";
|
|
15
|
+
export { runDataOp } from "./core/modules/dataops.js";
|
|
15
16
|
export { makeGateway, Table } from "./core/modules/gateway.js";
|
|
16
17
|
export { createPagesRoutes, mountPages } from "./core/modules/pages.js";
|
|
17
18
|
export { mountActions, resolveActionHandler } from "./core/modules/actions.js";
|
|
18
19
|
export { resolveHandler, sdkDecisionEvaluator } from "./core/modules/workers.js";
|
|
19
20
|
export { buildMessages, callLlm, resolveProvider, runLlmJob, } from "./core/modules/llm.js";
|
|
20
21
|
export { evalCorrelation } from "./core/modules/triggers.js";
|
|
22
|
+
// Model deploy + `{{name}}` template substitution
|
|
23
|
+
export { deployModels } from "./core/modules/deploy.js";
|
|
24
|
+
export { applyTemplates, resolveTemplates } from "./core/modules/templates.js";
|
|
21
25
|
// Adapters + engine + run entrypoint
|
|
22
26
|
export { createNodeHost } from "./adapters/node.js";
|
|
23
27
|
export { createDenoHost } from "./adapters/deno.js";
|
package/dist/runtime/run.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// create the Urban app, start it, and install a graceful-shutdown handler.
|
|
4
4
|
import { createUrbanApp } from "./core/runtime.js";
|
|
5
5
|
import { selectHost } from "./adapters/detect.js";
|
|
6
|
+
import { denoGlobal, processGlobal } from "./adapters/globals.js";
|
|
6
7
|
import { createNanoSdkEngineClient } from "./engine/nanosdk.js";
|
|
7
8
|
export async function runFromEnv(opts = {}) {
|
|
8
9
|
const host = opts.host ?? selectHost({ cwd: opts.root });
|
|
@@ -26,6 +27,7 @@ export async function runFromEnv(opts = {}) {
|
|
|
26
27
|
manifestPath: opts.manifestPath,
|
|
27
28
|
port: opts.port,
|
|
28
29
|
mount: opts.mount,
|
|
30
|
+
templates: opts.templates,
|
|
29
31
|
});
|
|
30
32
|
await app.start();
|
|
31
33
|
if (opts.handleSignals !== false)
|
|
@@ -52,18 +54,19 @@ export function installSignalHandlers(stop) {
|
|
|
52
54
|
}
|
|
53
55
|
});
|
|
54
56
|
};
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
+
const deno = denoGlobal();
|
|
58
|
+
const proc = processGlobal();
|
|
59
|
+
if (deno?.addSignalListener) {
|
|
57
60
|
try {
|
|
58
|
-
|
|
59
|
-
|
|
61
|
+
deno.addSignalListener("SIGINT", onSignal);
|
|
62
|
+
deno.addSignalListener("SIGTERM", onSignal);
|
|
60
63
|
}
|
|
61
64
|
catch {
|
|
62
65
|
/* signal listeners may be unavailable (e.g. Windows) */
|
|
63
66
|
}
|
|
64
67
|
}
|
|
65
|
-
else if (
|
|
66
|
-
|
|
67
|
-
|
|
68
|
+
else if (proc?.on) {
|
|
69
|
+
proc.on("SIGINT", onSignal);
|
|
70
|
+
proc.on("SIGTERM", onSignal);
|
|
68
71
|
}
|
|
69
72
|
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// This module only reads/writes files through the GenIO port — installing the
|
|
12
12
|
// package (a subprocess) is the CLI's job, so this stays runtime-agnostic and
|
|
13
13
|
// unit-testable.
|
|
14
|
+
import { errorMessage, isRecord } from "../runtime/core/guards.js";
|
|
14
15
|
const MANIFEST_FILE = "nano-ide.ext.json";
|
|
15
16
|
function joinPath(a, b) {
|
|
16
17
|
// Trim both slash styles (as toolkit/gen.ts joinPath does) so a Windows-style
|
|
@@ -40,7 +41,7 @@ export async function addConnector(opts) {
|
|
|
40
41
|
pack = JSON.parse(await io.readText(packManifestPath));
|
|
41
42
|
}
|
|
42
43
|
catch (err) {
|
|
43
|
-
throw new Error(`failed to parse ${packManifestPath}: ${err
|
|
44
|
+
throw new Error(`failed to parse ${packManifestPath}: ${errorMessage(err)}`);
|
|
44
45
|
}
|
|
45
46
|
if (typeof pack.id !== "string" || !pack.id) {
|
|
46
47
|
throw new Error(`${packManifestPath} has no string "id" (a pack must declare its id).`);
|
|
@@ -65,7 +66,7 @@ export async function addConnector(opts) {
|
|
|
65
66
|
app = JSON.parse(await io.readText(appManifestPath));
|
|
66
67
|
}
|
|
67
68
|
catch (err) {
|
|
68
|
-
throw new Error(`failed to parse ${appManifestPath}: ${err
|
|
69
|
+
throw new Error(`failed to parse ${appManifestPath}: ${errorMessage(err)}`);
|
|
69
70
|
}
|
|
70
71
|
if (app === null || typeof app !== "object" || Array.isArray(app)) {
|
|
71
72
|
throw new Error(`${appManifestPath} is not a JSON object.`);
|
|
@@ -109,7 +110,7 @@ export async function addConnector(opts) {
|
|
|
109
110
|
// A pre-existing connection of this name must be a compatible object of the
|
|
110
111
|
// same pack `type`; otherwise wiring workers to it would produce an invalid
|
|
111
112
|
// or misleading manifest. Fail loudly instead of silently reusing it.
|
|
112
|
-
if (
|
|
113
|
+
if (!isRecord(existing)) {
|
|
113
114
|
throw new Error(`connection "${connection}" already exists and is not an object.`);
|
|
114
115
|
}
|
|
115
116
|
const conn = existing;
|
package/dist/toolkit/fsio.js
CHANGED
|
@@ -58,7 +58,8 @@ export function createNodeGenIO() {
|
|
|
58
58
|
// stat failed (e.g. a virtual path); fall back to no cache key and let import() surface it.
|
|
59
59
|
}
|
|
60
60
|
const href = `${pathToFileURL(abs).href}${version}`;
|
|
61
|
-
|
|
61
|
+
const mod = import(__rewriteRelativeImportExtension(href));
|
|
62
|
+
return mod;
|
|
62
63
|
},
|
|
63
64
|
};
|
|
64
65
|
}
|
package/dist/toolkit/models.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// no-op and the rest of `urban gen` proceeds on the authored/derived models unchanged.
|
|
10
10
|
import { toDeployableBpmn } from "@nanobpm/workflow";
|
|
11
11
|
import { expandPattern, joinPath } from "./gen.js";
|
|
12
|
+
import { isRecord } from "../runtime/core/guards.js";
|
|
12
13
|
/** Default source glob for code-first flows (matches the code-first scaffold layout). */
|
|
13
14
|
export const DEFAULT_WORKFLOW_PATTERNS = ["workflows/*.ts"];
|
|
14
15
|
/** Default output dir for derived models (matches the model-first scaffold, so a derived
|
|
@@ -19,10 +20,7 @@ export const PROCESSES_DIR = "processes";
|
|
|
19
20
|
export const MODEL_PROVENANCE = "Generated by @nanobpm/urban from workflows/*.ts — do not edit; edit the flow and re-run `urban gen`.";
|
|
20
21
|
/** A `defineFlow` export: has a string `id` and an `imperative`/`declarative` `kind`. */
|
|
21
22
|
export function isWorkflow(v) {
|
|
22
|
-
|
|
23
|
-
return false;
|
|
24
|
-
const o = v;
|
|
25
|
-
return typeof o.id === "string" && (o.kind === "imperative" || o.kind === "declarative");
|
|
23
|
+
return isRecord(v) && typeof v.id === "string" && (v.kind === "imperative" || v.kind === "declarative");
|
|
26
24
|
}
|
|
27
25
|
/** Sanitize a flow id into a safe `.bpmn` filename (mirrors the host generator). */
|
|
28
26
|
export function bpmnFilename(id) {
|
|
@@ -65,7 +63,7 @@ function messageOf(e) {
|
|
|
65
63
|
/** Append an actionable hint when a `.ts` import fails because the runtime can't strip types. */
|
|
66
64
|
function importHint(rel, e) {
|
|
67
65
|
const base = messageOf(e);
|
|
68
|
-
const code = e
|
|
66
|
+
const code = isRecord(e) && typeof e.code === "string" ? e.code : "";
|
|
69
67
|
const looksLikeNoTsLoader = rel.endsWith(".ts") &&
|
|
70
68
|
(code === "ERR_UNKNOWN_FILE_EXTENSION" || /Unknown file extension "\.ts"/.test(base));
|
|
71
69
|
if (!looksLikeNoTsLoader)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/urban",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.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",
|
package/src/cli.test.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { cmdData } from "./cli.ts";
|
|
4
|
+
import { isRecord } from "./runtime/core/guards.ts";
|
|
5
|
+
|
|
6
|
+
// The `urban data` gateway's contract (see cmdData's docstring): *every* reply — including an
|
|
7
|
+
// unreadable request — is a parseable `{ ok: false, error }` envelope on stdout AND the process
|
|
8
|
+
// exits 0, so the Nano console can always parse the reply off stdout and never mistakes a
|
|
9
|
+
// malformed request for a subprocess crash. This guards the unreadable-request path, which used
|
|
10
|
+
// to exit non-zero and could make a caller that keys off the exit code discard the (parseable)
|
|
11
|
+
// error payload.
|
|
12
|
+
test("urban data exits 0 with a parseable envelope on an unreadable request", async () => {
|
|
13
|
+
const flags: Parameters<typeof cmdData>[0] = {
|
|
14
|
+
root: ".",
|
|
15
|
+
manifest: "nano.app.json",
|
|
16
|
+
check: false,
|
|
17
|
+
write: false,
|
|
18
|
+
deno: false,
|
|
19
|
+
models: true,
|
|
20
|
+
stdout: false,
|
|
21
|
+
install: true,
|
|
22
|
+
help: false,
|
|
23
|
+
version: false,
|
|
24
|
+
_: ["data"],
|
|
25
|
+
};
|
|
26
|
+
const lines: string[] = [];
|
|
27
|
+
const orig = console.log;
|
|
28
|
+
console.log = (...args: unknown[]) => {
|
|
29
|
+
lines.push(args.map(String).join(" "));
|
|
30
|
+
};
|
|
31
|
+
let code: number;
|
|
32
|
+
try {
|
|
33
|
+
code = await cmdData(flags, async () => "this is not json {");
|
|
34
|
+
} finally {
|
|
35
|
+
console.log = orig;
|
|
36
|
+
}
|
|
37
|
+
assert.equal(code, 0);
|
|
38
|
+
assert.equal(lines.length, 1);
|
|
39
|
+
const reply: unknown = JSON.parse(lines[0]);
|
|
40
|
+
assert.ok(isRecord(reply));
|
|
41
|
+
assert.equal(reply.ok, false);
|
|
42
|
+
if (typeof reply.error !== "string") throw new TypeError("expected string error");
|
|
43
|
+
assert.match(reply.error, /^bad request: /);
|
|
44
|
+
});
|
package/src/cli.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// urban run materialize + serve the app (deploy, data, workers, surfaces, triggers)
|
|
8
8
|
// urban dev like run, plus watch sources and hot-reload on change
|
|
9
9
|
// urban deploy deploy models only, then exit
|
|
10
|
+
// urban data DB-manager op gateway (stdin JSON → stdout JSON)
|
|
10
11
|
//
|
|
11
12
|
// Global flags: --root <dir> (default "."), --manifest <file> (default nano.app.json),
|
|
12
13
|
// --port <n>, -h/--help, -v/--version.
|
|
@@ -15,11 +16,15 @@ import {
|
|
|
15
16
|
collectManifestIssues,
|
|
16
17
|
installSignalHandlers,
|
|
17
18
|
loadManifest,
|
|
19
|
+
runDataOp,
|
|
18
20
|
runDev,
|
|
19
21
|
runFromEnv,
|
|
20
22
|
selectHost,
|
|
21
23
|
} from "./runtime/index.ts";
|
|
24
|
+
import { denoGlobal, processGlobal } from "./runtime/adapters/globals.ts";
|
|
25
|
+
import { errorMessage, isRecord } from "./runtime/core/guards.ts";
|
|
22
26
|
import { scaffold, slugify } from "create-urban-app";
|
|
27
|
+
import type { DataRequest } from "./runtime/index.ts";
|
|
23
28
|
import { addConnector, createNodeGenIO, previewModels, runGen, scaffoldWorkers } from "./toolkit/index.ts";
|
|
24
29
|
import { pathToFileURL } from "node:url";
|
|
25
30
|
import { readFileSync } from "node:fs";
|
|
@@ -30,7 +35,8 @@ import { spawnSync } from "node:child_process";
|
|
|
30
35
|
function readVersion(): string {
|
|
31
36
|
try {
|
|
32
37
|
const url = new URL("../package.json", import.meta.url);
|
|
33
|
-
|
|
38
|
+
const pkg: unknown = JSON.parse(readFileSync(url, "utf8"));
|
|
39
|
+
return isRecord(pkg) && typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
34
40
|
} catch {
|
|
35
41
|
return "0.0.0";
|
|
36
42
|
}
|
|
@@ -111,6 +117,9 @@ Usage:
|
|
|
111
117
|
urban run materialize + serve the app
|
|
112
118
|
urban dev run + watch sources, hot-reload on change
|
|
113
119
|
urban deploy deploy models only, then exit
|
|
120
|
+
urban data DB-manager op gateway: read a JSON request on stdin
|
|
121
|
+
({op:sources|schema|query|exec|script|migrations|migrate,…}),
|
|
122
|
+
write a JSON result on stdout
|
|
114
123
|
|
|
115
124
|
Global flags:
|
|
116
125
|
--root <dir> app root (default ".")
|
|
@@ -270,6 +279,63 @@ async function cmdDeploy(f: Flags): Promise<number> {
|
|
|
270
279
|
return 0;
|
|
271
280
|
}
|
|
272
281
|
|
|
282
|
+
/** Read all of stdin as UTF-8 text (Node or Deno). Returns "" if stdin is empty/closed. */
|
|
283
|
+
async function readStdin(): Promise<string> {
|
|
284
|
+
const deno = denoGlobal();
|
|
285
|
+
const proc = processGlobal();
|
|
286
|
+
const chunks: string[] = [];
|
|
287
|
+
const dec = new TextDecoder();
|
|
288
|
+
if (deno?.stdin?.readable) {
|
|
289
|
+
for await (const chunk of deno.stdin.readable) chunks.push(dec.decode(chunk, { stream: true }));
|
|
290
|
+
chunks.push(dec.decode());
|
|
291
|
+
return chunks.join("");
|
|
292
|
+
}
|
|
293
|
+
if (proc?.stdin) {
|
|
294
|
+
for await (const chunk of proc.stdin) {
|
|
295
|
+
chunks.push(typeof chunk === "string" ? chunk : dec.decode(chunk, { stream: true }));
|
|
296
|
+
}
|
|
297
|
+
chunks.push(dec.decode());
|
|
298
|
+
return chunks.join("");
|
|
299
|
+
}
|
|
300
|
+
return "";
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* `urban data` — the DB-manager op gateway. Reads a single JSON request from stdin
|
|
305
|
+
* (`{ op, source?, sql?, params?, statements? }`) and writes a single JSON response to stdout
|
|
306
|
+
* (`{ ok: true, ...result }` or `{ ok: false, error }`). Every response — a handled op error *or*
|
|
307
|
+
* an unreadable request — is a parseable `{ ok: false, error }` envelope and still exits 0, so the
|
|
308
|
+
* caller can always parse the reply off stdout and never has to treat a non-zero exit as a lost
|
|
309
|
+
* error; the process only exits non-zero on an unexpected crash. This is the seam the Nano console
|
|
310
|
+
* re-points its Data panel at (host dry-out nano-bpm#576), replacing the vendored data-cli.ts.
|
|
311
|
+
*/
|
|
312
|
+
export async function cmdData(f: Flags, readInput: () => Promise<string> = readStdin): Promise<number> {
|
|
313
|
+
let req: DataRequest;
|
|
314
|
+
try {
|
|
315
|
+
req = JSON.parse(await readInput());
|
|
316
|
+
} catch (err) {
|
|
317
|
+
console.log(
|
|
318
|
+
JSON.stringify({
|
|
319
|
+
ok: false,
|
|
320
|
+
error: `bad request: ${err instanceof Error ? err.message : String(err)}`,
|
|
321
|
+
}),
|
|
322
|
+
);
|
|
323
|
+
return 0;
|
|
324
|
+
}
|
|
325
|
+
const host = selectHost({ cwd: f.root });
|
|
326
|
+
try {
|
|
327
|
+
// Mirror runFromEnv: the host is anchored at f.root, so pass root "." to avoid
|
|
328
|
+
// double-prefixing every manifest/db path (host.abs already resolves against f.root).
|
|
329
|
+
const result = await runDataOp(host, ".", f.manifest, req);
|
|
330
|
+
console.log(JSON.stringify({ ok: true, ...result }));
|
|
331
|
+
} catch (err) {
|
|
332
|
+
console.log(
|
|
333
|
+
JSON.stringify({ ok: false, error: err instanceof Error ? err.message : String(err) }),
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
return 0;
|
|
337
|
+
}
|
|
338
|
+
|
|
273
339
|
async function cmdNew(f: Flags): Promise<number> {
|
|
274
340
|
const name = f._[1];
|
|
275
341
|
if (!name) {
|
|
@@ -332,7 +398,7 @@ async function cmdAdd(f: Flags): Promise<number> {
|
|
|
332
398
|
}
|
|
333
399
|
return 0;
|
|
334
400
|
} catch (err) {
|
|
335
|
-
console.error(`✖ ${(err
|
|
401
|
+
console.error(`✖ ${errorMessage(err)}`);
|
|
336
402
|
return 1;
|
|
337
403
|
}
|
|
338
404
|
}
|
|
@@ -342,7 +408,7 @@ export async function main(argv: string[]): Promise<number> {
|
|
|
342
408
|
try {
|
|
343
409
|
f = parse(argv);
|
|
344
410
|
} catch (err) {
|
|
345
|
-
console.error(
|
|
411
|
+
console.error(errorMessage(err));
|
|
346
412
|
return 1;
|
|
347
413
|
}
|
|
348
414
|
if (f.version) {
|
|
@@ -368,6 +434,8 @@ export async function main(argv: string[]): Promise<number> {
|
|
|
368
434
|
return cmdStubs(f);
|
|
369
435
|
case "add":
|
|
370
436
|
return cmdAdd(f);
|
|
437
|
+
case "data":
|
|
438
|
+
return cmdData(f);
|
|
371
439
|
case "run":
|
|
372
440
|
return cmdRun(f);
|
|
373
441
|
case "dev":
|
|
@@ -381,29 +449,27 @@ export async function main(argv: string[]): Promise<number> {
|
|
|
381
449
|
}
|
|
382
450
|
}
|
|
383
451
|
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
const nodeMain = argv1 ? meta.url === pathToFileURL(argv1).href : false;
|
|
391
|
-
if (meta.main === true || nodeMain) {
|
|
452
|
+
const proc = processGlobal();
|
|
453
|
+
const deno = denoGlobal();
|
|
454
|
+
const importMetaMain: unknown = Reflect.get(import.meta, "main");
|
|
455
|
+
const argv1 = proc?.argv?.[1];
|
|
456
|
+
const nodeMain = argv1 ? import.meta.url === pathToFileURL(argv1).href : false;
|
|
457
|
+
if (importMetaMain === true || nodeMain) {
|
|
392
458
|
// Node passes args via process.argv (slice off exec+script); Deno (run directly)
|
|
393
459
|
// exposes them on Deno.args. Prefer whichever actually carries args.
|
|
394
|
-
const fromNode =
|
|
395
|
-
const fromDeno =
|
|
460
|
+
const fromNode = proc?.argv?.slice(2);
|
|
461
|
+
const fromDeno = deno?.args;
|
|
396
462
|
const argv = (fromNode && fromNode.length ? fromNode : fromDeno) ?? fromNode ?? [];
|
|
397
463
|
const exit = (code: number) => {
|
|
398
|
-
if (
|
|
399
|
-
else if (
|
|
464
|
+
if (proc?.exit) proc.exit(code);
|
|
465
|
+
else if (deno?.exit) deno.exit(code);
|
|
400
466
|
};
|
|
401
467
|
main(argv).then(
|
|
402
468
|
(code) => {
|
|
403
469
|
if (code >= 0) exit(code);
|
|
404
470
|
},
|
|
405
471
|
(err) => {
|
|
406
|
-
console.error(
|
|
472
|
+
console.error(errorMessage(err));
|
|
407
473
|
exit(1);
|
|
408
474
|
},
|
|
409
475
|
);
|