@nanobpm/urban 0.35.0 → 0.37.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.
Files changed (47) hide show
  1. package/dist/cli.d.ts +0 -1
  2. package/dist/cli.js +32 -47
  3. package/dist/openapi/spec.d.ts +9 -0
  4. package/dist/openapi/spec.js +22 -0
  5. package/dist/runtime/core/modules/api.d.ts +19 -4
  6. package/dist/runtime/core/modules/api.js +81 -3
  7. package/dist/runtime/core/runtime.d.ts +10 -0
  8. package/dist/runtime/core/runtime.js +2 -2
  9. package/dist/runtime/index.d.ts +3 -1
  10. package/dist/runtime/index.js +2 -1
  11. package/dist/toolkit/api-dir.d.ts +11 -0
  12. package/dist/toolkit/api-dir.js +36 -0
  13. package/dist/toolkit/derivers/api.d.ts +25 -2
  14. package/dist/toolkit/derivers/api.js +60 -5
  15. package/dist/toolkit/gen.js +4 -1
  16. package/dist/toolkit/generate.d.ts +27 -0
  17. package/dist/toolkit/generate.js +47 -0
  18. package/dist/toolkit/index.d.ts +8 -1
  19. package/dist/toolkit/index.js +5 -1
  20. package/dist/toolkit/scaffold/operations.d.ts +21 -0
  21. package/dist/toolkit/scaffold/operations.js +55 -0
  22. package/dist/toolkit/scaffold/workers.d.ts +7 -5
  23. package/dist/toolkit/scaffold/workers.js +12 -5
  24. package/dist/toolkit/scaffold.d.ts +16 -0
  25. package/dist/toolkit/scaffold.js +41 -1
  26. package/package.json +74 -74
  27. package/src/cli.test.ts +0 -1
  28. package/src/cli.ts +30 -45
  29. package/src/openapi/spec.ts +24 -0
  30. package/src/runtime/core/modules/api.test.ts +104 -2
  31. package/src/runtime/core/modules/api.ts +95 -8
  32. package/src/runtime/core/runtime.test.ts +53 -1
  33. package/src/runtime/core/runtime.ts +12 -2
  34. package/src/runtime/index.ts +3 -1
  35. package/src/toolkit/api-dir.test.ts +32 -0
  36. package/src/toolkit/api-dir.ts +41 -0
  37. package/src/toolkit/derivers/api.test.ts +41 -3
  38. package/src/toolkit/derivers/api.ts +75 -5
  39. package/src/toolkit/gen.ts +5 -2
  40. package/src/toolkit/generate.test.ts +135 -0
  41. package/src/toolkit/generate.ts +71 -0
  42. package/src/toolkit/index.ts +10 -0
  43. package/src/toolkit/scaffold/operations.test.ts +136 -0
  44. package/src/toolkit/scaffold/operations.ts +75 -0
  45. package/src/toolkit/scaffold/workers.test.ts +35 -0
  46. package/src/toolkit/scaffold/workers.ts +13 -5
  47. package/src/toolkit/scaffold.ts +63 -1
@@ -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
- artifacts.push(...deriveApi(parseSpec(specText)));
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
+ }
@@ -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";
@@ -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
+ }
@@ -26,7 +26,7 @@ export interface WorkerStubPlan {
26
26
  stub: string;
27
27
  manifestEntry: StubManifestEntry;
28
28
  }
29
- export type SkipReason = "already-wired" | "orchestrator" | "duplicate";
29
+ export type SkipReason = "already-wired" | "orchestrator" | "duplicate" | "external";
30
30
  export interface SkippedWorker {
31
31
  taskType: string;
32
32
  reason: SkipReason;
@@ -41,8 +41,10 @@ export declare function slugifyTaskType(taskType: string): string;
41
41
  export declare function renderWorkerStub(taskType: string, typedIn: boolean, typedOut: boolean): string;
42
42
  /**
43
43
  * Plan write-once handler stubs from the models. A stub is planned for each service-task
44
- * `taskType` that is NOT already wired in the manifest, NOT the imperative orchestrator, and
45
- * not a duplicate of one already planned. `declaredTypeIds` are the manifest `types` ids — an
46
- * envelope `in`/`out` is typed only when it names one (matching the worker-io deriver).
44
+ * `taskType` that is NOT already wired in the manifest, NOT the imperative orchestrator, NOT
45
+ * declared external (serviced by an out-of-process worker), and not a duplicate of one already
46
+ * planned. `declaredTypeIds` are the manifest `types` ids — an envelope `in`/`out` is typed only
47
+ * when it names one (matching the worker-io deriver). `externalTaskTypes` are the manifest's
48
+ * declared external task types, which the app owns the model for but deliberately does not host.
47
49
  */
48
- export declare function planWorkerScaffold(models: ModelSource[], workers?: ScaffoldWorker[], declaredTypeIds?: Iterable<string>): WorkerScaffoldPlan;
50
+ export declare function planWorkerScaffold(models: ModelSource[], workers?: ScaffoldWorker[], declaredTypeIds?: Iterable<string>, externalTaskTypes?: Iterable<string>): WorkerScaffoldPlan;
@@ -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 stubs\` will never overwrite it. Implement the body\n` +
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 +
@@ -57,15 +57,18 @@ export function renderWorkerStub(taskType, typedIn, typedOut) {
57
57
  }
58
58
  /**
59
59
  * Plan write-once handler stubs from the models. A stub is planned for each service-task
60
- * `taskType` that is NOT already wired in the manifest, NOT the imperative orchestrator, and
61
- * not a duplicate of one already planned. `declaredTypeIds` are the manifest `types` ids — an
62
- * envelope `in`/`out` is typed only when it names one (matching the worker-io deriver).
60
+ * `taskType` that is NOT already wired in the manifest, NOT the imperative orchestrator, NOT
61
+ * declared external (serviced by an out-of-process worker), and not a duplicate of one already
62
+ * planned. `declaredTypeIds` are the manifest `types` ids — an envelope `in`/`out` is typed only
63
+ * when it names one (matching the worker-io deriver). `externalTaskTypes` are the manifest's
64
+ * declared external task types, which the app owns the model for but deliberately does not host.
63
65
  */
64
- export function planWorkerScaffold(models, workers = [], declaredTypeIds = []) {
66
+ export function planWorkerScaffold(models, workers = [], declaredTypeIds = [], externalTaskTypes = []) {
65
67
  const wired = new Set(workers
66
68
  .map((w) => w?.taskType)
67
69
  .filter((t) => typeof t === "string" && t.length > 0));
68
70
  const declared = new Set(declaredTypeIds);
71
+ const external = new Set(externalTaskTypes);
69
72
  const plans = [];
70
73
  const skipped = [];
71
74
  const seenTaskTypes = new Set();
@@ -77,6 +80,10 @@ export function planWorkerScaffold(models, workers = [], declaredTypeIds = []) {
77
80
  skipped.push({ taskType, reason: "orchestrator" });
78
81
  continue;
79
82
  }
83
+ if (external.has(taskType)) {
84
+ skipped.push({ taskType, reason: "external" });
85
+ continue;
86
+ }
80
87
  if (wired.has(taskType)) {
81
88
  skipped.push({ taskType, reason: "already-wired" });
82
89
  continue;
@@ -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>;
@@ -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;
@@ -13,7 +15,7 @@ export async function scaffoldWorkers(opts) {
13
15
  const manifest = JSON.parse(await io.readText(manifestPath));
14
16
  const models = await readModels(root, io, manifest);
15
17
  const declaredTypeIds = Object.keys(manifest.types ?? {});
16
- const { plans, skipped } = planWorkerScaffold(models, manifest.workers ?? [], declaredTypeIds);
18
+ const { plans, skipped } = planWorkerScaffold(models, manifest.workers ?? [], declaredTypeIds, manifest.externalTaskTypes ?? []);
17
19
  const outcomes = [];
18
20
  const wired = [];
19
21
  for (const plan of plans) {
@@ -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
- "name": "@nanobpm/urban",
3
- "version": "0.35.0",
4
- "description": "Urban: build and run code-first apps on Nano \u2014 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
- }
2
+ "name": "@nanobpm/urban",
3
+ "version": "0.37.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.5.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
@@ -14,7 +14,6 @@ test("urban data exits 0 with a parseable envelope on an unreadable request", as
14
14
  root: ".",
15
15
  manifest: "nano.app.json",
16
16
  check: false,
17
- write: false,
18
17
  deno: false,
19
18
  models: true,
20
19
  stdout: false,