@nanobpm/urban 0.35.0 → 0.36.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/cli.d.ts +0 -1
- package/dist/cli.js +32 -47
- package/dist/openapi/spec.d.ts +9 -0
- package/dist/openapi/spec.js +22 -0
- package/dist/runtime/core/modules/api.d.ts +19 -4
- package/dist/runtime/core/modules/api.js +81 -3
- package/dist/runtime/core/runtime.d.ts +10 -0
- package/dist/runtime/core/runtime.js +2 -2
- package/dist/runtime/index.d.ts +3 -1
- package/dist/runtime/index.js +2 -1
- package/dist/toolkit/api-dir.d.ts +11 -0
- package/dist/toolkit/api-dir.js +36 -0
- package/dist/toolkit/derivers/api.d.ts +25 -2
- package/dist/toolkit/derivers/api.js +60 -5
- package/dist/toolkit/gen.js +4 -1
- package/dist/toolkit/generate.d.ts +27 -0
- package/dist/toolkit/generate.js +47 -0
- package/dist/toolkit/index.d.ts +8 -1
- package/dist/toolkit/index.js +5 -1
- package/dist/toolkit/scaffold/operations.d.ts +21 -0
- package/dist/toolkit/scaffold/operations.js +55 -0
- package/dist/toolkit/scaffold/workers.js +1 -1
- package/dist/toolkit/scaffold.d.ts +16 -0
- package/dist/toolkit/scaffold.js +40 -0
- package/package.json +74 -74
- package/src/cli.test.ts +0 -1
- package/src/cli.ts +30 -45
- package/src/openapi/spec.ts +24 -0
- package/src/runtime/core/modules/api.test.ts +104 -2
- package/src/runtime/core/modules/api.ts +95 -8
- package/src/runtime/core/runtime.test.ts +53 -1
- package/src/runtime/core/runtime.ts +12 -2
- package/src/runtime/index.ts +3 -1
- package/src/toolkit/api-dir.test.ts +32 -0
- package/src/toolkit/api-dir.ts +41 -0
- package/src/toolkit/derivers/api.test.ts +41 -3
- package/src/toolkit/derivers/api.ts +75 -5
- package/src/toolkit/gen.ts +5 -2
- package/src/toolkit/generate.test.ts +135 -0
- package/src/toolkit/generate.ts +71 -0
- package/src/toolkit/index.ts +10 -0
- package/src/toolkit/scaffold/operations.test.ts +136 -0
- package/src/toolkit/scaffold/operations.ts +75 -0
- package/src/toolkit/scaffold/workers.ts +1 -1
- package/src/toolkit/scaffold.ts +56 -0
package/dist/toolkit/gen.js
CHANGED
|
@@ -98,7 +98,10 @@ async function collectAll(opts) {
|
|
|
98
98
|
const specRef = typeof manifest.api?.spec === "string" ? manifest.api.spec.trim() : "";
|
|
99
99
|
if (!opts.modelsOnly && specRef.length > 0) {
|
|
100
100
|
const specText = await io.readText(join(root, specRef));
|
|
101
|
-
|
|
101
|
+
const opsDir = typeof manifest.api?.dir === "string" && manifest.api.dir.trim().length > 0
|
|
102
|
+
? manifest.api.dir.trim()
|
|
103
|
+
: undefined;
|
|
104
|
+
artifacts.push(...deriveApi(parseSpec(specText), opsDir));
|
|
102
105
|
}
|
|
103
106
|
return { artifacts: sortArtifacts(artifacts), derived };
|
|
104
107
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { GenOptions, GenResult } from "./gen.ts";
|
|
2
|
+
import type { OperationStubOutcome, StubOutcome } from "./scaffold.ts";
|
|
3
|
+
import type { StubManifestEntry } from "./scaffold/workers.ts";
|
|
4
|
+
export interface GenerateResult extends GenResult {
|
|
5
|
+
/** Worker-stub scaffolding outcomes (created / kept / would-create). */
|
|
6
|
+
workerStubs: StubOutcome[];
|
|
7
|
+
/** Operation-delegate scaffolding outcomes (created / kept / would-create). */
|
|
8
|
+
operationStubs: OperationStubOutcome[];
|
|
9
|
+
/** Worker manifest entries wired into `workers[]` (write mode only). */
|
|
10
|
+
wiredWorkers: StubManifestEntry[];
|
|
11
|
+
/** True when `workers[]` was patched (write mode, unwired workers present). */
|
|
12
|
+
manifestPatched: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* App-relative paths of stubs that would be created — the scaffolding equivalent of `drift`.
|
|
15
|
+
* Populated in `check` mode (nothing is written); a caller should fail CI when non-empty.
|
|
16
|
+
*/
|
|
17
|
+
missingStubs: string[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Derive artifacts and scaffold the write-once handler stubs they reference, in one pass.
|
|
21
|
+
* In `check` mode nothing is written and any would-be-created stub is reported in `missingStubs`.
|
|
22
|
+
* Scaffolding is skipped when derivation is `incomplete` (never scaffold from a half-derived model);
|
|
23
|
+
* the caller reports the derivation failure first.
|
|
24
|
+
*/
|
|
25
|
+
export declare function generate(opts: GenOptions & {
|
|
26
|
+
check?: boolean;
|
|
27
|
+
}): Promise<GenerateResult>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// `urban gen`: the whole generation concern in one pass — derive artifacts (migrations, worker-I/O,
|
|
2
|
+
// code-first models, OpenAPI contracts + controller) AND scaffold the write-once handler stubs those
|
|
3
|
+
// artifacts reference (worker stubs from the model, operation-delegate stubs from the OpenAPI spec).
|
|
4
|
+
//
|
|
5
|
+
// This composes the pure derivation (`runGen`, gen.ts) with the write-once scaffolders (scaffold.ts)
|
|
6
|
+
// WITHOUT a cycle: scaffold.ts already depends on gen.ts, so this leaf module depends on both and
|
|
7
|
+
// neither depends back. Scaffolding is write-once (an existing, human-owned stub is never clobbered),
|
|
8
|
+
// so folding it into `gen` is idempotent. `--check` writes nothing: a missing stub surfaces in
|
|
9
|
+
// `missingStubs` exactly like a drifted artifact surfaces in `drift`, so CI fails when a stub the
|
|
10
|
+
// generated controller imports isn't committed.
|
|
11
|
+
import { runGen } from "./gen.js";
|
|
12
|
+
import { scaffoldOperations, scaffoldWorkers } from "./scaffold.js";
|
|
13
|
+
/**
|
|
14
|
+
* Derive artifacts and scaffold the write-once handler stubs they reference, in one pass.
|
|
15
|
+
* In `check` mode nothing is written and any would-be-created stub is reported in `missingStubs`.
|
|
16
|
+
* Scaffolding is skipped when derivation is `incomplete` (never scaffold from a half-derived model);
|
|
17
|
+
* the caller reports the derivation failure first.
|
|
18
|
+
*/
|
|
19
|
+
export async function generate(opts) {
|
|
20
|
+
const res = await runGen(opts);
|
|
21
|
+
const empty = {
|
|
22
|
+
...res,
|
|
23
|
+
workerStubs: [],
|
|
24
|
+
operationStubs: [],
|
|
25
|
+
wiredWorkers: [],
|
|
26
|
+
manifestPatched: false,
|
|
27
|
+
missingStubs: [],
|
|
28
|
+
};
|
|
29
|
+
if (res.incomplete)
|
|
30
|
+
return empty;
|
|
31
|
+
const write = !opts.check;
|
|
32
|
+
const scaffoldOpts = { root: opts.root, io: opts.io, manifestFile: opts.manifestFile, write };
|
|
33
|
+
const workers = await scaffoldWorkers(scaffoldOpts);
|
|
34
|
+
const operations = await scaffoldOperations(scaffoldOpts);
|
|
35
|
+
const missingStubs = [
|
|
36
|
+
...workers.outcomes.filter((o) => o.status === "would-create").map((o) => o.handlerPath),
|
|
37
|
+
...operations.outcomes.filter((o) => o.status === "would-create").map((o) => o.handlerPath),
|
|
38
|
+
];
|
|
39
|
+
return {
|
|
40
|
+
...res,
|
|
41
|
+
workerStubs: workers.outcomes,
|
|
42
|
+
operationStubs: operations.outcomes,
|
|
43
|
+
wiredWorkers: workers.wired,
|
|
44
|
+
manifestPatched: workers.manifestPatched,
|
|
45
|
+
missingStubs,
|
|
46
|
+
};
|
|
47
|
+
}
|
package/dist/toolkit/index.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export { deriveMeta, emitMeta, foldMeta, metaDeriver, scanModelMeta, META_TS, }
|
|
|
13
13
|
export type { MetaDecl } from "./derivers/meta.ts";
|
|
14
14
|
export { deriveMessageBindings, emitMessageBindings, emitMessageBindingsRuntime, messagesDeriver, scanModelMessages, MESSAGE_BINDINGS_DTS, MESSAGE_BINDINGS_TS, } from "./derivers/messages.ts";
|
|
15
15
|
export type { MessageIo, MessageBindingDecl } from "./derivers/messages.ts";
|
|
16
|
-
export { apiDeriver, deriveApi, emitApiBindings, emitApiBindingsRuntime, schemaToTs, API_BINDINGS_DTS, API_BINDINGS_TS, } from "./derivers/api.ts";
|
|
16
|
+
export { apiDeriver, deriveApi, emitApiBindings, emitApiBindingsRuntime, emitApiController, schemaToTs, API_BINDINGS_DTS, API_BINDINGS_TS, API_CONTROLLER_TS, } from "./derivers/api.ts";
|
|
17
17
|
export { collectOperations, HTTP_METHODS, isSafeOperationId, operationsWithoutId, operationsWithUnsafeId, parseSpec, refName, resolveSchema, toRouteMatcher, undeclaredPathParams, validateValue, } from "../openapi/spec.ts";
|
|
18
18
|
export type { HttpMethodLower, OpenApiDoc, OpenApiParameter, OpenApiSchema, OperationInfo, } from "../openapi/spec.ts";
|
|
19
19
|
export declare const DERIVERS: readonly [import("./artifact.ts").Deriver<import("./derivers/migrations.ts").ToolkitManifest>, import("./artifact.ts").Deriver<import("./derivers/migrations.ts").ToolkitManifest>, import("./artifact.ts").Deriver<{
|
|
@@ -26,9 +26,12 @@ export declare const DERIVERS: readonly [import("./artifact.ts").Deriver<import(
|
|
|
26
26
|
declaredTypeIds: Iterable<string>;
|
|
27
27
|
}>, import("./artifact.ts").Deriver<{
|
|
28
28
|
doc: import("../openapi/spec.ts").OpenApiDoc;
|
|
29
|
+
operationsDir?: string;
|
|
29
30
|
}>];
|
|
30
31
|
export { collectArtifacts, runGen, joinPath, readModels, expandPattern, previewModels } from "./gen.ts";
|
|
31
32
|
export type { GenIO, GenOptions, GenResult } from "./gen.ts";
|
|
33
|
+
export { generate } from "./generate.ts";
|
|
34
|
+
export type { GenerateResult } from "./generate.ts";
|
|
32
35
|
export { createNodeGenIO } from "./fsio.ts";
|
|
33
36
|
export { deriveModels, isWorkflow, bpmnFilename, processesOutDir, DEFAULT_WORKFLOW_PATTERNS, PROCESSES_DIR, MODEL_PROVENANCE, } from "./models.ts";
|
|
34
37
|
export type { DerivedModels, DerivedModelInfo, ModelError, ModelsManifest } from "./models.ts";
|
|
@@ -36,5 +39,9 @@ export { planWorkerScaffold, renderWorkerStub, slugifyTaskType, } from "./scaffo
|
|
|
36
39
|
export type { ScaffoldWorker, SkippedWorker, SkipReason, StubManifestEntry, WorkerScaffoldPlan, WorkerStubPlan, } from "./scaffold/workers.ts";
|
|
37
40
|
export { scaffoldWorkers } from "./scaffold.ts";
|
|
38
41
|
export type { ScaffoldOptions, ScaffoldRun, StubOutcome, StubStatus } from "./scaffold.ts";
|
|
42
|
+
export { planOperationScaffold, renderOperationStub } from "./scaffold/operations.ts";
|
|
43
|
+
export type { OperationStubPlan } from "./scaffold/operations.ts";
|
|
44
|
+
export { scaffoldOperations } from "./scaffold.ts";
|
|
45
|
+
export type { OperationScaffoldRun, OperationStubOutcome } from "./scaffold.ts";
|
|
39
46
|
export { addConnector } from "./addConnector.ts";
|
|
40
47
|
export type { AddConnectorOptions, AddConnectorResult } from "./addConnector.ts";
|
package/dist/toolkit/index.js
CHANGED
|
@@ -11,7 +11,7 @@ export { deriveWorkerBindings, emitWorkerBindings, emitWorkerBindingsRuntime, ov
|
|
|
11
11
|
export { byModelPath, typeRefFor } from "./derivers/worker-io.js";
|
|
12
12
|
export { deriveMeta, emitMeta, foldMeta, metaDeriver, scanModelMeta, META_TS, } from "./derivers/meta.js";
|
|
13
13
|
export { deriveMessageBindings, emitMessageBindings, emitMessageBindingsRuntime, messagesDeriver, scanModelMessages, MESSAGE_BINDINGS_DTS, MESSAGE_BINDINGS_TS, } from "./derivers/messages.js";
|
|
14
|
-
export { apiDeriver, deriveApi, emitApiBindings, emitApiBindingsRuntime, schemaToTs, API_BINDINGS_DTS, API_BINDINGS_TS, } from "./derivers/api.js";
|
|
14
|
+
export { apiDeriver, deriveApi, emitApiBindings, emitApiBindingsRuntime, emitApiController, schemaToTs, API_BINDINGS_DTS, API_BINDINGS_TS, API_CONTROLLER_TS, } from "./derivers/api.js";
|
|
15
15
|
// The OpenAPI machinery the deriver + runtime share (ADR 0058).
|
|
16
16
|
export { collectOperations, HTTP_METHODS, isSafeOperationId, operationsWithoutId, operationsWithUnsafeId, parseSpec, refName, resolveSchema, toRouteMatcher, undeclaredPathParams, validateValue, } from "../openapi/spec.js";
|
|
17
17
|
// The registry of all derivers (for discovery / IDE migration).
|
|
@@ -31,10 +31,14 @@ export const DERIVERS = [
|
|
|
31
31
|
];
|
|
32
32
|
// Gen orchestrator + IO
|
|
33
33
|
export { collectArtifacts, runGen, joinPath, readModels, expandPattern, previewModels } from "./gen.js";
|
|
34
|
+
export { generate } from "./generate.js";
|
|
34
35
|
export { createNodeGenIO } from "./fsio.js";
|
|
35
36
|
// Code-first model derivation (workflows/*.ts → BPMN)
|
|
36
37
|
export { deriveModels, isWorkflow, bpmnFilename, processesOutDir, DEFAULT_WORKFLOW_PATTERNS, PROCESSES_DIR, MODEL_PROVENANCE, } from "./models.js";
|
|
37
38
|
// Worker-stub scaffolder (ADR 0056): write-once handler stubs from the model.
|
|
38
39
|
export { planWorkerScaffold, renderWorkerStub, slugifyTaskType, } from "./scaffold/workers.js";
|
|
39
40
|
export { scaffoldWorkers } from "./scaffold.js";
|
|
41
|
+
// Operation-delegate scaffolder (ADR 0059): write-once typed delegate stubs from the OpenAPI spec.
|
|
42
|
+
export { planOperationScaffold, renderOperationStub } from "./scaffold/operations.js";
|
|
43
|
+
export { scaffoldOperations } from "./scaffold.js";
|
|
40
44
|
export { addConnector } from "./addConnector.js";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { DEFAULT_OPERATIONS_DIR } from "../api-dir.ts";
|
|
2
|
+
import { type OpenApiDoc } from "../../openapi/spec.ts";
|
|
3
|
+
export { DEFAULT_OPERATIONS_DIR };
|
|
4
|
+
/** One planned operation-delegate stub. */
|
|
5
|
+
export interface OperationStubPlan {
|
|
6
|
+
operationId: string;
|
|
7
|
+
/** App-relative delegate path: `<dir>/<operationId>.ts`. */
|
|
8
|
+
handlerPath: string;
|
|
9
|
+
/** The stub file contents (a typed `defineOperation` that throws `NotImplemented` → HTTP 501). */
|
|
10
|
+
stub: string;
|
|
11
|
+
}
|
|
12
|
+
/** Render a stub matching a real hand-authored operation delegate (default-exported, typed via the
|
|
13
|
+
* generated `defineOperation`). It throws `NotImplemented` so the endpoint returns 501 until the
|
|
14
|
+
* body is implemented — never a silent 500, never a drift from the contract. */
|
|
15
|
+
export declare function renderOperationStub(operationId: string, dir?: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* Plan write-once delegate stubs from the spec: one per declared `operationId` (collectOperations
|
|
18
|
+
* already skips ops without a safe id). The planner emits a plan for every operation; the impure
|
|
19
|
+
* edge (`scaffoldOperations`) keeps an existing file verbatim and creates only the absent ones.
|
|
20
|
+
*/
|
|
21
|
+
export declare function planOperationScaffold(doc: OpenApiDoc, dir?: string): OperationStubPlan[];
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Pure planner for the write-once operation-delegate scaffolder (ADR 0059). Given a parsed OpenAPI
|
|
2
|
+
// document, it plans one typed delegate stub per declared `operationId` — the ONE part of the HTTP
|
|
3
|
+
// surface that cannot be derived from the spec (the handler *body* is human logic). Everything here
|
|
4
|
+
// is pure and testable like a deriver; the impure write-if-absent edge lives in `../scaffold.ts`.
|
|
5
|
+
//
|
|
6
|
+
// scaffold ≠ derive: stubs are human-owned files under `<dir>/<operationId>.ts`, so they are planned
|
|
7
|
+
// once and never overwritten — unlike the derived `nano-generated/` tree (which includes the
|
|
8
|
+
// controller registry that statically imports these delegates and type-checks the whole set).
|
|
9
|
+
import { DEFAULT_OPERATIONS_DIR, normalizeApiDir } from "../api-dir.js";
|
|
10
|
+
import { assertUniqueOperationIds, collectOperations } from "../../openapi/spec.js";
|
|
11
|
+
export { DEFAULT_OPERATIONS_DIR };
|
|
12
|
+
/** Normalize an `api.dir` to a clean relative segment (see `normalizeApiDir`); rejects absolute
|
|
13
|
+
* paths and `..` segments so stubs are scaffolded where the runtime resolves delegates from. */
|
|
14
|
+
function normalizeDir(dir) {
|
|
15
|
+
return normalizeApiDir(dir);
|
|
16
|
+
}
|
|
17
|
+
/** Number of `../` hops from `<dir>/<file>.ts` back up to the app root (dir may be nested). */
|
|
18
|
+
function upToRoot(dir) {
|
|
19
|
+
const depth = dir.split("/").filter((s) => s.length > 0).length;
|
|
20
|
+
return "../".repeat(depth);
|
|
21
|
+
}
|
|
22
|
+
/** Render a stub matching a real hand-authored operation delegate (default-exported, typed via the
|
|
23
|
+
* generated `defineOperation`). It throws `NotImplemented` so the endpoint returns 501 until the
|
|
24
|
+
* body is implemented — never a silent 500, never a drift from the contract. */
|
|
25
|
+
export function renderOperationStub(operationId, dir = DEFAULT_OPERATIONS_DIR) {
|
|
26
|
+
const up = upToRoot(normalizeDir(dir));
|
|
27
|
+
const key = JSON.stringify(operationId);
|
|
28
|
+
return (`// Operation delegate stub for the \`${operationId}\` endpoint, scaffolded from the OpenAPI spec\n` +
|
|
29
|
+
`// (ADR 0059). This file is yours to edit — \`urban gen\` will never overwrite it. The\n` +
|
|
30
|
+
`// generated controller (nano-generated/controller.ts) imports this default export and\n` +
|
|
31
|
+
`// type-checks it against the spec, so the signature cannot drift. Implement the body below\n` +
|
|
32
|
+
`// (the validated \`{ req, params, query, body }\` input + the injected \`app\`) and delete the throw.\n` +
|
|
33
|
+
`import { NotImplemented } from "@nanobpm/urban";\n` +
|
|
34
|
+
`import { defineOperation } from ${JSON.stringify(`${up}nano-generated/operations.ts`)};\n` +
|
|
35
|
+
`\n` +
|
|
36
|
+
`export default defineOperation(${key}, async (input, app) => {\n` +
|
|
37
|
+
` // \`input\` is the validated request (params/query/body/req); reach app state + services\n` +
|
|
38
|
+
` // through the injected \`app\` API. Return \`{ status?, headers?, body? }\` (or nothing → 204).\n` +
|
|
39
|
+
` throw new NotImplemented(${key});\n` +
|
|
40
|
+
`});\n`);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Plan write-once delegate stubs from the spec: one per declared `operationId` (collectOperations
|
|
44
|
+
* already skips ops without a safe id). The planner emits a plan for every operation; the impure
|
|
45
|
+
* edge (`scaffoldOperations`) keeps an existing file verbatim and creates only the absent ones.
|
|
46
|
+
*/
|
|
47
|
+
export function planOperationScaffold(doc, dir = DEFAULT_OPERATIONS_DIR) {
|
|
48
|
+
assertUniqueOperationIds(doc);
|
|
49
|
+
const cleanDir = normalizeDir(dir);
|
|
50
|
+
return collectOperations(doc).map((op) => ({
|
|
51
|
+
operationId: op.operationId,
|
|
52
|
+
handlerPath: `${cleanDir}/${op.operationId}.ts`,
|
|
53
|
+
stub: renderOperationStub(op.operationId, cleanDir),
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
@@ -41,7 +41,7 @@ export function renderWorkerStub(taskType, typedIn, typedOut) {
|
|
|
41
41
|
? ` // \`job.variables\` is typed as WorkerInputs[${key}].`
|
|
42
42
|
: ` // \`job.variables\` is an open Record<string, unknown> (no declared input type).`;
|
|
43
43
|
return (`// Handler stub for the \`${taskType}\` service task, scaffolded from the model (ADR 0056).\n` +
|
|
44
|
-
`// This file is yours to edit — \`urban
|
|
44
|
+
`// This file is yours to edit — \`urban gen\` will never overwrite it. Implement the body\n` +
|
|
45
45
|
`// below (use \`app.data.table(...)\` for state) and delete the throw.\n` +
|
|
46
46
|
`import type { AppJobHandler } from "@nanobpm/urban/worker";\n` +
|
|
47
47
|
generatedImport +
|
|
@@ -29,3 +29,19 @@ export interface ScaffoldRun {
|
|
|
29
29
|
}
|
|
30
30
|
/** Scaffold write-once worker stubs from the model, wiring them into `manifest.workers[]`. */
|
|
31
31
|
export declare function scaffoldWorkers(opts: ScaffoldOptions): Promise<ScaffoldRun>;
|
|
32
|
+
/** One planned operation-delegate outcome (write-once). */
|
|
33
|
+
export interface OperationStubOutcome {
|
|
34
|
+
operationId: string;
|
|
35
|
+
handlerPath: string;
|
|
36
|
+
status: StubStatus;
|
|
37
|
+
}
|
|
38
|
+
export interface OperationScaffoldRun {
|
|
39
|
+
outcomes: OperationStubOutcome[];
|
|
40
|
+
write: boolean;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Scaffold write-once operation-delegate stubs from the app's OpenAPI spec (ADR 0059). One typed
|
|
44
|
+
* `defineOperation` stub per declared `operationId`, created only if absent (human-owned files are
|
|
45
|
+
* kept verbatim). Dry-run by default. A no-op when the app declares no `api.spec`.
|
|
46
|
+
*/
|
|
47
|
+
export declare function scaffoldOperations(opts: ScaffoldOptions): Promise<OperationScaffoldRun>;
|
package/dist/toolkit/scaffold.js
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
// human-owned, so an existing file is KEPT verbatim, never clobbered (the opposite of `runGen`).
|
|
5
5
|
import { joinPath, readModels } from "./gen.js";
|
|
6
6
|
import { planWorkerScaffold, } from "./scaffold/workers.js";
|
|
7
|
+
import { planOperationScaffold } from "./scaffold/operations.js";
|
|
8
|
+
import { parseSpec } from "../openapi/spec.js";
|
|
7
9
|
/** Scaffold write-once worker stubs from the model, wiring them into `manifest.workers[]`. */
|
|
8
10
|
export async function scaffoldWorkers(opts) {
|
|
9
11
|
const { root, io } = opts;
|
|
@@ -52,3 +54,41 @@ export async function scaffoldWorkers(opts) {
|
|
|
52
54
|
}
|
|
53
55
|
return { outcomes, skipped, wired, manifestPatched, write };
|
|
54
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Scaffold write-once operation-delegate stubs from the app's OpenAPI spec (ADR 0059). One typed
|
|
59
|
+
* `defineOperation` stub per declared `operationId`, created only if absent (human-owned files are
|
|
60
|
+
* kept verbatim). Dry-run by default. A no-op when the app declares no `api.spec`.
|
|
61
|
+
*/
|
|
62
|
+
export async function scaffoldOperations(opts) {
|
|
63
|
+
const { root, io } = opts;
|
|
64
|
+
const write = opts.write ?? false;
|
|
65
|
+
const manifestFile = opts.manifestFile ?? "nano.app.json";
|
|
66
|
+
const manifestPath = joinPath(root, manifestFile);
|
|
67
|
+
const manifest = JSON.parse(await io.readText(manifestPath));
|
|
68
|
+
const specRef = typeof manifest.api?.spec === "string" ? manifest.api.spec.trim() : "";
|
|
69
|
+
if (specRef.length === 0)
|
|
70
|
+
return { outcomes: [], write };
|
|
71
|
+
const specText = await io.readText(joinPath(root, specRef));
|
|
72
|
+
const dir = typeof manifest.api?.dir === "string" && manifest.api.dir.trim().length > 0
|
|
73
|
+
? manifest.api.dir.trim()
|
|
74
|
+
: undefined;
|
|
75
|
+
const plans = planOperationScaffold(parseSpec(specText), dir);
|
|
76
|
+
const outcomes = [];
|
|
77
|
+
for (const plan of plans) {
|
|
78
|
+
const abs = joinPath(root, plan.handlerPath);
|
|
79
|
+
const exists = await io.exists(abs);
|
|
80
|
+
let status;
|
|
81
|
+
if (exists) {
|
|
82
|
+
status = "kept"; // human-owned — never clobber
|
|
83
|
+
}
|
|
84
|
+
else if (write) {
|
|
85
|
+
await io.writeText(abs, plan.stub);
|
|
86
|
+
status = "created";
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
status = "would-create";
|
|
90
|
+
}
|
|
91
|
+
outcomes.push({ operationId: plan.operationId, handlerPath: plan.handlerPath, status });
|
|
92
|
+
}
|
|
93
|
+
return { outcomes, write };
|
|
94
|
+
}
|
package/package.json
CHANGED
|
@@ -1,76 +1,76 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
2
|
+
"name": "@nanobpm/urban",
|
|
3
|
+
"version": "0.36.0",
|
|
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
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"nanobpm",
|
|
9
|
+
"urban",
|
|
10
|
+
"cli",
|
|
11
|
+
"runtime",
|
|
12
|
+
"codegen",
|
|
13
|
+
"derivation",
|
|
14
|
+
"nano-app",
|
|
15
|
+
"rad"
|
|
16
|
+
],
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/nanobpm/nano-ide.git",
|
|
23
|
+
"directory": "packages/urban"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=22.6"
|
|
27
|
+
},
|
|
28
|
+
"bin": {
|
|
29
|
+
"urban": "src/bin.mjs"
|
|
30
|
+
},
|
|
31
|
+
"main": "./dist/index.js",
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"exports": {
|
|
34
|
+
".": {
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"default": "./dist/index.js"
|
|
37
|
+
},
|
|
38
|
+
"./runtime": {
|
|
39
|
+
"types": "./dist/runtime/index.d.ts",
|
|
40
|
+
"default": "./dist/runtime/index.js"
|
|
41
|
+
},
|
|
42
|
+
"./toolkit": {
|
|
43
|
+
"types": "./dist/toolkit/index.d.ts",
|
|
44
|
+
"default": "./dist/toolkit/index.js"
|
|
45
|
+
},
|
|
46
|
+
"./worker": {
|
|
47
|
+
"types": "./dist/worker.d.ts",
|
|
48
|
+
"default": "./dist/worker.js"
|
|
49
|
+
},
|
|
50
|
+
"./source": "./src/index.ts"
|
|
51
|
+
},
|
|
52
|
+
"files": [
|
|
53
|
+
"dist",
|
|
54
|
+
"src",
|
|
55
|
+
"README.md"
|
|
56
|
+
],
|
|
57
|
+
"scripts": {
|
|
58
|
+
"build": "tsc -p tsconfig.build.json",
|
|
59
|
+
"prepack": "npm run build",
|
|
60
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
61
|
+
"test": "node --test --experimental-strip-types \"src/**/*.test.ts\"",
|
|
62
|
+
"test:deno": "deno test -A --no-check src/"
|
|
63
|
+
},
|
|
64
|
+
"dependencies": {
|
|
65
|
+
"@nanobpm/nano-app-schema": "^0.4.0",
|
|
66
|
+
"@nanobpm/nano-sdk": "^1.2.3",
|
|
67
|
+
"@nanobpm/workflow": "^0.8.0",
|
|
68
|
+
"bpmn-auto-layout": "^2.0.0-alpha.2",
|
|
69
|
+
"create-urban-app": "^0.9.0",
|
|
70
|
+
"yaml": "^2.9.0"
|
|
71
|
+
},
|
|
72
|
+
"devDependencies": {
|
|
73
|
+
"@types/node": "^22.20.1",
|
|
74
|
+
"typescript": "^5.9.3"
|
|
75
|
+
}
|
|
76
76
|
}
|
package/src/cli.test.ts
CHANGED
package/src/cli.ts
CHANGED
|
@@ -25,7 +25,7 @@ import { denoGlobal, processGlobal } from "./runtime/adapters/globals.ts";
|
|
|
25
25
|
import { errorMessage, isRecord } from "./runtime/core/guards.ts";
|
|
26
26
|
import { scaffold, slugify } from "create-urban-app";
|
|
27
27
|
import type { DataRequest } from "./runtime/index.ts";
|
|
28
|
-
import { addConnector, createNodeGenIO, previewModels,
|
|
28
|
+
import { addConnector, createNodeGenIO, previewModels, generate, runGen } from "./toolkit/index.ts";
|
|
29
29
|
import { pathToFileURL } from "node:url";
|
|
30
30
|
import { readFileSync } from "node:fs";
|
|
31
31
|
import { spawnSync } from "node:child_process";
|
|
@@ -50,7 +50,6 @@ interface Flags {
|
|
|
50
50
|
manifest: string;
|
|
51
51
|
port?: number;
|
|
52
52
|
check: boolean;
|
|
53
|
-
write: boolean;
|
|
54
53
|
deno: boolean;
|
|
55
54
|
models: boolean;
|
|
56
55
|
stdout: boolean;
|
|
@@ -62,7 +61,7 @@ interface Flags {
|
|
|
62
61
|
}
|
|
63
62
|
|
|
64
63
|
function parse(argv: string[]): Flags {
|
|
65
|
-
const f: Flags = { root: ".", manifest: "nano.app.json", check: false,
|
|
64
|
+
const f: Flags = { root: ".", manifest: "nano.app.json", check: false, deno: false, models: true, stdout: false, install: true, help: false, version: false, _: [] };
|
|
66
65
|
const need = (i: number, flag: string): string => {
|
|
67
66
|
const v = argv[i];
|
|
68
67
|
if (v === undefined || v.startsWith("-")) throw new Error(`flag ${flag} requires a value`);
|
|
@@ -73,7 +72,6 @@ function parse(argv: string[]): Flags {
|
|
|
73
72
|
if (a === "-h" || a === "--help") f.help = true;
|
|
74
73
|
else if (a === "-v" || a === "--version") f.version = true;
|
|
75
74
|
else if (a === "--check") f.check = true;
|
|
76
|
-
else if (a === "--write") f.write = true;
|
|
77
75
|
else if (a === "--deno") f.deno = true;
|
|
78
76
|
else if (a === "--no-models") f.models = false;
|
|
79
77
|
else if (a === "--models") f.models = true;
|
|
@@ -108,11 +106,12 @@ Usage:
|
|
|
108
106
|
urban new <name> [--root <path>] [--deno] [--style model|code] scaffold a new Urban app
|
|
109
107
|
(--code-first is shorthand for --style code)
|
|
110
108
|
urban check validate the manifest
|
|
111
|
-
urban gen [--check] [--no-models] derive artifacts
|
|
109
|
+
urban gen [--check] [--no-models] derive artifacts + scaffold write-once handler stubs
|
|
110
|
+
(--check: verify up-to-date without writing; a missing stub or
|
|
111
|
+
drifted artifact fails — run \`urban gen\`)
|
|
112
112
|
(--no-models: skip writing derived .bpmn; type-contracts only)
|
|
113
113
|
urban derive [--check|--stdout] derive code-first models only (workflows/*.ts → processes/*.bpmn)
|
|
114
114
|
(--stdout: print {models,incomplete} JSON without writing)
|
|
115
|
-
urban stubs [--write] scaffold write-once handler stubs from the model
|
|
116
115
|
urban add <pkg> [--no-install] install a connector pack + wire it into nano.app.json
|
|
117
116
|
urban run materialize + serve the app
|
|
118
117
|
urban dev run + watch sources, hot-reload on change
|
|
@@ -148,23 +147,43 @@ async function cmdCheck(f: Flags): Promise<number> {
|
|
|
148
147
|
|
|
149
148
|
async function cmdGen(f: Flags): Promise<number> {
|
|
150
149
|
const io = createNodeGenIO();
|
|
151
|
-
const res = await
|
|
150
|
+
const res = await generate({ root: f.root, io, manifestFile: f.manifest, check: f.check, emitModels: f.models });
|
|
152
151
|
if (res.incomplete) {
|
|
153
152
|
console.error(`✖ ${res.modelErrors.length} workflow(s) failed to derive:`);
|
|
154
153
|
for (const e of res.modelErrors) console.error(` • ${e.path}: ${e.message}`);
|
|
155
154
|
return 1;
|
|
156
155
|
}
|
|
157
156
|
if (f.check) {
|
|
158
|
-
if (res.drift.length === 0) {
|
|
159
|
-
console.log(`✔ generated artifacts are up to date (${res.artifacts.length} checked)`);
|
|
157
|
+
if (res.drift.length === 0 && res.missingStubs.length === 0) {
|
|
158
|
+
console.log(`✔ generated artifacts + handler stubs are up to date (${res.artifacts.length} checked)`);
|
|
160
159
|
return 0;
|
|
161
160
|
}
|
|
162
|
-
|
|
163
|
-
|
|
161
|
+
if (res.drift.length > 0) {
|
|
162
|
+
console.error(`✖ ${res.drift.length} generated artifact(s) are out of date — run \`urban gen\`:`);
|
|
163
|
+
for (const p of res.drift) console.error(` • ${p}`);
|
|
164
|
+
}
|
|
165
|
+
if (res.missingStubs.length > 0) {
|
|
166
|
+
console.error(`✖ ${res.missingStubs.length} handler stub(s) missing — run \`urban gen\`:`);
|
|
167
|
+
for (const p of res.missingStubs) console.error(` • ${p}`);
|
|
168
|
+
}
|
|
164
169
|
return 1;
|
|
165
170
|
}
|
|
166
171
|
console.log(`✔ generated ${res.artifacts.length} artifact(s):`);
|
|
167
172
|
for (const a of res.artifacts) console.log(` • ${a.path}`);
|
|
173
|
+
|
|
174
|
+
// Write-once handler stubs scaffolded as part of generation (workers from the model, operation
|
|
175
|
+
// delegates from the OpenAPI spec). Existing, human-owned stubs are kept verbatim.
|
|
176
|
+
const created = [
|
|
177
|
+
...res.workerStubs.filter((o) => o.status === "created").map((o) => o.handlerPath),
|
|
178
|
+
...res.operationStubs.filter((o) => o.status === "created").map((o) => o.handlerPath),
|
|
179
|
+
];
|
|
180
|
+
if (created.length > 0) {
|
|
181
|
+
console.log(`✔ scaffolded ${created.length} handler stub(s):`);
|
|
182
|
+
for (const p of created) console.log(` + ${p}`);
|
|
183
|
+
}
|
|
184
|
+
if (res.manifestPatched) {
|
|
185
|
+
console.log(`✔ wired ${res.wiredWorkers.length} worker(s) into ${f.manifest}`);
|
|
186
|
+
}
|
|
168
187
|
return 0;
|
|
169
188
|
}
|
|
170
189
|
|
|
@@ -217,38 +236,6 @@ async function cmdDerive(f: Flags): Promise<number> {
|
|
|
217
236
|
return 0;
|
|
218
237
|
}
|
|
219
238
|
|
|
220
|
-
async function cmdStubs(f: Flags): Promise<number> {
|
|
221
|
-
const io = createNodeGenIO();
|
|
222
|
-
const run = await scaffoldWorkers({ root: f.root, io, manifestFile: f.manifest, write: f.write });
|
|
223
|
-
|
|
224
|
-
const created = run.outcomes.filter((o) => o.status === "created");
|
|
225
|
-
const would = run.outcomes.filter((o) => o.status === "would-create");
|
|
226
|
-
const kept = run.outcomes.filter((o) => o.status === "kept");
|
|
227
|
-
const typing = (o: { typedIn: boolean; typedOut: boolean }) =>
|
|
228
|
-
o.typedIn || o.typedOut ? ` (typed${o.typedIn ? " in" : ""}${o.typedOut ? " out" : ""})` : "";
|
|
229
|
-
|
|
230
|
-
if (run.write) {
|
|
231
|
-
console.log(`✔ scaffolded ${created.length} worker stub(s):`);
|
|
232
|
-
for (const o of created) console.log(` + ${o.handlerPath}${typing(o)}`);
|
|
233
|
-
if (run.manifestPatched) {
|
|
234
|
-
console.log(`✔ wired ${run.wired.length} worker(s) into ${f.manifest}`);
|
|
235
|
-
}
|
|
236
|
-
} else {
|
|
237
|
-
if (would.length === 0) {
|
|
238
|
-
console.log(`✔ no new worker stubs to scaffold`);
|
|
239
|
-
} else {
|
|
240
|
-
console.log(`Would scaffold ${would.length} worker stub(s) — run with --write to apply:`);
|
|
241
|
-
for (const o of would) console.log(` + ${o.handlerPath}${typing(o)}`);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
for (const o of kept) console.log(` = ${o.handlerPath} (kept — already exists)`);
|
|
245
|
-
if (run.skipped.length > 0) {
|
|
246
|
-
console.log(` skipped ${run.skipped.length}: ` +
|
|
247
|
-
run.skipped.map((s) => `${s.taskType} (${s.reason})`).join(", "));
|
|
248
|
-
}
|
|
249
|
-
return 0;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
239
|
async function cmdRun(f: Flags, mount?: Record<string, boolean>): Promise<number> {
|
|
253
240
|
const app = await runFromEnv({
|
|
254
241
|
root: f.root,
|
|
@@ -430,8 +417,6 @@ export async function main(argv: string[]): Promise<number> {
|
|
|
430
417
|
return cmdGen(f);
|
|
431
418
|
case "derive":
|
|
432
419
|
return cmdDerive(f);
|
|
433
|
-
case "stubs":
|
|
434
|
-
return cmdStubs(f);
|
|
435
420
|
case "add":
|
|
436
421
|
return cmdAdd(f);
|
|
437
422
|
case "data":
|
package/src/openapi/spec.ts
CHANGED
|
@@ -265,6 +265,30 @@ export function collectOperations(doc: OpenApiDoc): OperationInfo[] {
|
|
|
265
265
|
return out;
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
+
/**
|
|
269
|
+
* Throw if the document declares the same `operationId` on more than one operation. An operationId
|
|
270
|
+
* must be unique across the whole OpenAPI document (it names the delegate module `<dir>/<operationId>`
|
|
271
|
+
* AND the generated controller registry key), so a duplicate would silently overwrite an earlier
|
|
272
|
+
* entry in the emitted object literal — dispatching the wrong delegate while still type-checking — and
|
|
273
|
+
* make the scaffolder plan the same stub twice. Called by the gen/scaffold path so `urban gen`
|
|
274
|
+
* fails loudly on an incoherent spec instead of shipping a mis-dispatching registry.
|
|
275
|
+
*/
|
|
276
|
+
export function assertUniqueOperationIds(doc: OpenApiDoc): void {
|
|
277
|
+
const seen = new Set<string>();
|
|
278
|
+
const duplicates = new Set<string>();
|
|
279
|
+
for (const op of collectOperations(doc)) {
|
|
280
|
+
if (seen.has(op.operationId)) duplicates.add(op.operationId);
|
|
281
|
+
seen.add(op.operationId);
|
|
282
|
+
}
|
|
283
|
+
if (duplicates.size > 0) {
|
|
284
|
+
const list = [...duplicates].sort().join(", ");
|
|
285
|
+
throw new Error(
|
|
286
|
+
`OpenAPI spec declares duplicate operationId(s): ${list}. Each operationId must be unique — ` +
|
|
287
|
+
"it names the delegate module and the generated controller registry key.",
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
268
292
|
/** "method path" pointers for every operation that declares no `operationId`. Such operations are
|
|
269
293
|
* unroutable (no delegate to bind), so the runtime mount logs them at `warn` and skips them. */
|
|
270
294
|
export function operationsWithoutId(doc: OpenApiDoc): string[] {
|