@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.
- 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.d.ts +7 -5
- package/dist/toolkit/scaffold/workers.js +12 -5
- package/dist/toolkit/scaffold.d.ts +16 -0
- package/dist/toolkit/scaffold.js +41 -1
- 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.test.ts +35 -0
- package/src/toolkit/scaffold/workers.ts +13 -5
- package/src/toolkit/scaffold.ts +63 -1
package/dist/cli.d.ts
CHANGED
package/dist/cli.js
CHANGED
|
@@ -15,7 +15,7 @@ import { collectManifestIssues, installSignalHandlers, loadManifest, runDataOp,
|
|
|
15
15
|
import { denoGlobal, processGlobal } from "./runtime/adapters/globals.js";
|
|
16
16
|
import { errorMessage, isRecord } from "./runtime/core/guards.js";
|
|
17
17
|
import { scaffold, slugify } from "create-urban-app";
|
|
18
|
-
import { addConnector, createNodeGenIO, previewModels,
|
|
18
|
+
import { addConnector, createNodeGenIO, previewModels, generate, runGen } from "./toolkit/index.js";
|
|
19
19
|
import { pathToFileURL } from "node:url";
|
|
20
20
|
import { readFileSync } from "node:fs";
|
|
21
21
|
import { spawnSync } from "node:child_process";
|
|
@@ -33,7 +33,7 @@ function readVersion() {
|
|
|
33
33
|
}
|
|
34
34
|
const VERSION = readVersion();
|
|
35
35
|
function parse(argv) {
|
|
36
|
-
const f = { root: ".", manifest: "nano.app.json", check: false,
|
|
36
|
+
const f = { root: ".", manifest: "nano.app.json", check: false, deno: false, models: true, stdout: false, install: true, help: false, version: false, _: [] };
|
|
37
37
|
const need = (i, flag) => {
|
|
38
38
|
const v = argv[i];
|
|
39
39
|
if (v === undefined || v.startsWith("-"))
|
|
@@ -48,8 +48,6 @@ function parse(argv) {
|
|
|
48
48
|
f.version = true;
|
|
49
49
|
else if (a === "--check")
|
|
50
50
|
f.check = true;
|
|
51
|
-
else if (a === "--write")
|
|
52
|
-
f.write = true;
|
|
53
51
|
else if (a === "--deno")
|
|
54
52
|
f.deno = true;
|
|
55
53
|
else if (a === "--no-models")
|
|
@@ -97,11 +95,12 @@ Usage:
|
|
|
97
95
|
urban new <name> [--root <path>] [--deno] [--style model|code] scaffold a new Urban app
|
|
98
96
|
(--code-first is shorthand for --style code)
|
|
99
97
|
urban check validate the manifest
|
|
100
|
-
urban gen [--check] [--no-models] derive artifacts
|
|
98
|
+
urban gen [--check] [--no-models] derive artifacts + scaffold write-once handler stubs
|
|
99
|
+
(--check: verify up-to-date without writing; a missing stub or
|
|
100
|
+
drifted artifact fails — run \`urban gen\`)
|
|
101
101
|
(--no-models: skip writing derived .bpmn; type-contracts only)
|
|
102
102
|
urban derive [--check|--stdout] derive code-first models only (workflows/*.ts → processes/*.bpmn)
|
|
103
103
|
(--stdout: print {models,incomplete} JSON without writing)
|
|
104
|
-
urban stubs [--write] scaffold write-once handler stubs from the model
|
|
105
104
|
urban add <pkg> [--no-install] install a connector pack + wire it into nano.app.json
|
|
106
105
|
urban run materialize + serve the app
|
|
107
106
|
urban dev run + watch sources, hot-reload on change
|
|
@@ -136,7 +135,7 @@ async function cmdCheck(f) {
|
|
|
136
135
|
}
|
|
137
136
|
async function cmdGen(f) {
|
|
138
137
|
const io = createNodeGenIO();
|
|
139
|
-
const res = await
|
|
138
|
+
const res = await generate({ root: f.root, io, manifestFile: f.manifest, check: f.check, emitModels: f.models });
|
|
140
139
|
if (res.incomplete) {
|
|
141
140
|
console.error(`✖ ${res.modelErrors.length} workflow(s) failed to derive:`);
|
|
142
141
|
for (const e of res.modelErrors)
|
|
@@ -144,18 +143,39 @@ async function cmdGen(f) {
|
|
|
144
143
|
return 1;
|
|
145
144
|
}
|
|
146
145
|
if (f.check) {
|
|
147
|
-
if (res.drift.length === 0) {
|
|
148
|
-
console.log(`✔ generated artifacts are up to date (${res.artifacts.length} checked)`);
|
|
146
|
+
if (res.drift.length === 0 && res.missingStubs.length === 0) {
|
|
147
|
+
console.log(`✔ generated artifacts + handler stubs are up to date (${res.artifacts.length} checked)`);
|
|
149
148
|
return 0;
|
|
150
149
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
150
|
+
if (res.drift.length > 0) {
|
|
151
|
+
console.error(`✖ ${res.drift.length} generated artifact(s) are out of date — run \`urban gen\`:`);
|
|
152
|
+
for (const p of res.drift)
|
|
153
|
+
console.error(` • ${p}`);
|
|
154
|
+
}
|
|
155
|
+
if (res.missingStubs.length > 0) {
|
|
156
|
+
console.error(`✖ ${res.missingStubs.length} handler stub(s) missing — run \`urban gen\`:`);
|
|
157
|
+
for (const p of res.missingStubs)
|
|
158
|
+
console.error(` • ${p}`);
|
|
159
|
+
}
|
|
154
160
|
return 1;
|
|
155
161
|
}
|
|
156
162
|
console.log(`✔ generated ${res.artifacts.length} artifact(s):`);
|
|
157
163
|
for (const a of res.artifacts)
|
|
158
164
|
console.log(` • ${a.path}`);
|
|
165
|
+
// Write-once handler stubs scaffolded as part of generation (workers from the model, operation
|
|
166
|
+
// delegates from the OpenAPI spec). Existing, human-owned stubs are kept verbatim.
|
|
167
|
+
const created = [
|
|
168
|
+
...res.workerStubs.filter((o) => o.status === "created").map((o) => o.handlerPath),
|
|
169
|
+
...res.operationStubs.filter((o) => o.status === "created").map((o) => o.handlerPath),
|
|
170
|
+
];
|
|
171
|
+
if (created.length > 0) {
|
|
172
|
+
console.log(`✔ scaffolded ${created.length} handler stub(s):`);
|
|
173
|
+
for (const p of created)
|
|
174
|
+
console.log(` + ${p}`);
|
|
175
|
+
}
|
|
176
|
+
if (res.manifestPatched) {
|
|
177
|
+
console.log(`✔ wired ${res.wiredWorkers.length} worker(s) into ${f.manifest}`);
|
|
178
|
+
}
|
|
159
179
|
return 0;
|
|
160
180
|
}
|
|
161
181
|
async function cmdDerive(f) {
|
|
@@ -204,39 +224,6 @@ async function cmdDerive(f) {
|
|
|
204
224
|
console.log(` • ${a.path}`);
|
|
205
225
|
return 0;
|
|
206
226
|
}
|
|
207
|
-
async function cmdStubs(f) {
|
|
208
|
-
const io = createNodeGenIO();
|
|
209
|
-
const run = await scaffoldWorkers({ root: f.root, io, manifestFile: f.manifest, write: f.write });
|
|
210
|
-
const created = run.outcomes.filter((o) => o.status === "created");
|
|
211
|
-
const would = run.outcomes.filter((o) => o.status === "would-create");
|
|
212
|
-
const kept = run.outcomes.filter((o) => o.status === "kept");
|
|
213
|
-
const typing = (o) => o.typedIn || o.typedOut ? ` (typed${o.typedIn ? " in" : ""}${o.typedOut ? " out" : ""})` : "";
|
|
214
|
-
if (run.write) {
|
|
215
|
-
console.log(`✔ scaffolded ${created.length} worker stub(s):`);
|
|
216
|
-
for (const o of created)
|
|
217
|
-
console.log(` + ${o.handlerPath}${typing(o)}`);
|
|
218
|
-
if (run.manifestPatched) {
|
|
219
|
-
console.log(`✔ wired ${run.wired.length} worker(s) into ${f.manifest}`);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
else {
|
|
223
|
-
if (would.length === 0) {
|
|
224
|
-
console.log(`✔ no new worker stubs to scaffold`);
|
|
225
|
-
}
|
|
226
|
-
else {
|
|
227
|
-
console.log(`Would scaffold ${would.length} worker stub(s) — run with --write to apply:`);
|
|
228
|
-
for (const o of would)
|
|
229
|
-
console.log(` + ${o.handlerPath}${typing(o)}`);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
for (const o of kept)
|
|
233
|
-
console.log(` = ${o.handlerPath} (kept — already exists)`);
|
|
234
|
-
if (run.skipped.length > 0) {
|
|
235
|
-
console.log(` skipped ${run.skipped.length}: ` +
|
|
236
|
-
run.skipped.map((s) => `${s.taskType} (${s.reason})`).join(", "));
|
|
237
|
-
}
|
|
238
|
-
return 0;
|
|
239
|
-
}
|
|
240
227
|
async function cmdRun(f, mount) {
|
|
241
228
|
const app = await runFromEnv({
|
|
242
229
|
root: f.root,
|
|
@@ -417,8 +404,6 @@ export async function main(argv) {
|
|
|
417
404
|
return cmdGen(f);
|
|
418
405
|
case "derive":
|
|
419
406
|
return cmdDerive(f);
|
|
420
|
-
case "stubs":
|
|
421
|
-
return cmdStubs(f);
|
|
422
407
|
case "add":
|
|
423
408
|
return cmdAdd(f);
|
|
424
409
|
case "data":
|
package/dist/openapi/spec.d.ts
CHANGED
|
@@ -118,6 +118,15 @@ export declare function resolveSchema(doc: OpenApiDoc, schema: OpenApiSchema | u
|
|
|
118
118
|
* (path, method) order. Operations without an operationId are skipped here; the runtime mount logs
|
|
119
119
|
* them at `warn` and skips them too — this keeps the pure collector total. */
|
|
120
120
|
export declare function collectOperations(doc: OpenApiDoc): OperationInfo[];
|
|
121
|
+
/**
|
|
122
|
+
* Throw if the document declares the same `operationId` on more than one operation. An operationId
|
|
123
|
+
* must be unique across the whole OpenAPI document (it names the delegate module `<dir>/<operationId>`
|
|
124
|
+
* AND the generated controller registry key), so a duplicate would silently overwrite an earlier
|
|
125
|
+
* entry in the emitted object literal — dispatching the wrong delegate while still type-checking — and
|
|
126
|
+
* make the scaffolder plan the same stub twice. Called by the gen/scaffold path so `urban gen`
|
|
127
|
+
* fails loudly on an incoherent spec instead of shipping a mis-dispatching registry.
|
|
128
|
+
*/
|
|
129
|
+
export declare function assertUniqueOperationIds(doc: OpenApiDoc): void;
|
|
121
130
|
/** "method path" pointers for every operation that declares no `operationId`. Such operations are
|
|
122
131
|
* unroutable (no delegate to bind), so the runtime mount logs them at `warn` and skips them. */
|
|
123
132
|
export declare function operationsWithoutId(doc: OpenApiDoc): string[];
|
package/dist/openapi/spec.js
CHANGED
|
@@ -148,6 +148,28 @@ export function collectOperations(doc) {
|
|
|
148
148
|
}
|
|
149
149
|
return out;
|
|
150
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* Throw if the document declares the same `operationId` on more than one operation. An operationId
|
|
153
|
+
* must be unique across the whole OpenAPI document (it names the delegate module `<dir>/<operationId>`
|
|
154
|
+
* AND the generated controller registry key), so a duplicate would silently overwrite an earlier
|
|
155
|
+
* entry in the emitted object literal — dispatching the wrong delegate while still type-checking — and
|
|
156
|
+
* make the scaffolder plan the same stub twice. Called by the gen/scaffold path so `urban gen`
|
|
157
|
+
* fails loudly on an incoherent spec instead of shipping a mis-dispatching registry.
|
|
158
|
+
*/
|
|
159
|
+
export function assertUniqueOperationIds(doc) {
|
|
160
|
+
const seen = new Set();
|
|
161
|
+
const duplicates = new Set();
|
|
162
|
+
for (const op of collectOperations(doc)) {
|
|
163
|
+
if (seen.has(op.operationId))
|
|
164
|
+
duplicates.add(op.operationId);
|
|
165
|
+
seen.add(op.operationId);
|
|
166
|
+
}
|
|
167
|
+
if (duplicates.size > 0) {
|
|
168
|
+
const list = [...duplicates].sort().join(", ");
|
|
169
|
+
throw new Error(`OpenAPI spec declares duplicate operationId(s): ${list}. Each operationId must be unique — ` +
|
|
170
|
+
"it names the delegate module and the generated controller registry key.");
|
|
171
|
+
}
|
|
172
|
+
}
|
|
151
173
|
/** "method path" pointers for every operation that declares no `operationId`. Such operations are
|
|
152
174
|
* unroutable (no delegate to bind), so the runtime mount logs them at `warn` and skips them. */
|
|
153
175
|
export function operationsWithoutId(doc) {
|
|
@@ -13,17 +13,23 @@ export interface ApiBinding {
|
|
|
13
13
|
* for free — the UI reads the SAME OpenAPI document the runtime routes + validates from. */
|
|
14
14
|
docs?: boolean | string;
|
|
15
15
|
}
|
|
16
|
-
/** The `{ params, query, body }` contract a typed operation is generic over.
|
|
16
|
+
/** The `{ params, query, body }` contract a typed operation is generic over. `body` is optional so
|
|
17
|
+
* a generated request type for an operation without a required requestBody (its `body?` is
|
|
18
|
+
* optional/`undefined`) still satisfies the constraint — see the generated `operations.ts` /
|
|
19
|
+
* `controller.ts`, which parameterise `OperationHandler` over the per-op request type. */
|
|
17
20
|
export interface OperationContract {
|
|
18
21
|
params: unknown;
|
|
19
22
|
query: unknown;
|
|
20
|
-
body
|
|
23
|
+
body?: unknown;
|
|
21
24
|
}
|
|
22
|
-
/** The loose contract an untyped delegate sees (strings on the wire; unknown JSON body).
|
|
25
|
+
/** The loose contract an untyped delegate sees (strings on the wire; unknown JSON body). `body` is
|
|
26
|
+
* optional so a raw (untyped) delegate typed `OperationHandler<DefaultContract>` still satisfies the
|
|
27
|
+
* generated registry's per-operation `OperationHandler<ReqFor<K>>` slot — an op whose request body
|
|
28
|
+
* is optional/absent must be a subtype of the default contract (handler args are contravariant). */
|
|
23
29
|
export interface DefaultContract {
|
|
24
30
|
params: Record<string, string>;
|
|
25
31
|
query: Record<string, string | string[] | undefined>;
|
|
26
|
-
body
|
|
32
|
+
body?: unknown;
|
|
27
33
|
}
|
|
28
34
|
/** The input handed to an operation delegate: the raw request plus the parsed (and, unless ejected,
|
|
29
35
|
* validated) path params, query, and JSON body. */
|
|
@@ -41,6 +47,15 @@ export interface OperationResult<Res = unknown> {
|
|
|
41
47
|
}
|
|
42
48
|
/** An app-authored operation delegate: validated/typed input + the injected AppApi. */
|
|
43
49
|
export type OperationHandler<Req extends OperationContract = DefaultContract, Res = unknown> = (input: OperationInput<Req>, app: AppApi) => Promise<OperationResult<Res> | void> | OperationResult<Res> | void;
|
|
50
|
+
/**
|
|
51
|
+
* Thrown by a write-once operation stub to signal the endpoint exists but has no implementation
|
|
52
|
+
* yet; the dispatcher maps it to HTTP `501 Not Implemented` (ADR 0059). Delete the throw once you
|
|
53
|
+
* implement the handler body. A brand property makes the check robust across module realms.
|
|
54
|
+
*/
|
|
55
|
+
export declare class NotImplemented extends Error {
|
|
56
|
+
readonly urbanNotImplemented: true;
|
|
57
|
+
constructor(operationId: string);
|
|
58
|
+
}
|
|
44
59
|
/**
|
|
45
60
|
* Typed-identity helper the generated `operations.ts` wrapper re-types per operationId. Authoring:
|
|
46
61
|
* `export default defineOperation("createInvoice", async ({ params, query, body }, app) => { ... })`
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// `x-urban-eject: true` OpenAPI vendor extension. The delegate is always additively imperative: it
|
|
13
13
|
// gets the raw `req` and full `AppApi` on top of the validated input.
|
|
14
14
|
import { errorMessage } from "../guards.js";
|
|
15
|
+
import { MODULE_EXTENSION_CANDIDATES } from "../module-path.js";
|
|
15
16
|
import { html, json, normalizeRoutePath } from "../router.js";
|
|
16
17
|
import { resolveAppPath } from "./datasource.js";
|
|
17
18
|
import { collectOperations, isSafeOperationId, operationsWithoutId, operationsWithUnsafeId, parseSpec, resolveSchema, toRouteMatcher, undeclaredPathParams, undeclaredSecuritySchemes, evaluateSecurity, validateValue, } from "../../../openapi/spec.js";
|
|
@@ -51,6 +52,23 @@ window.ui = SwaggerUIBundle({ url: ${JSON.stringify(specUrl)}, dom_id: '#swagger
|
|
|
51
52
|
</body>
|
|
52
53
|
</html>`;
|
|
53
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Thrown by a write-once operation stub to signal the endpoint exists but has no implementation
|
|
57
|
+
* yet; the dispatcher maps it to HTTP `501 Not Implemented` (ADR 0059). Delete the throw once you
|
|
58
|
+
* implement the handler body. A brand property makes the check robust across module realms.
|
|
59
|
+
*/
|
|
60
|
+
export class NotImplemented extends Error {
|
|
61
|
+
urbanNotImplemented = true;
|
|
62
|
+
constructor(operationId) {
|
|
63
|
+
super(`operation not implemented: ${operationId}`);
|
|
64
|
+
this.name = "NotImplemented";
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** True for a `NotImplemented` throw (instanceof, or the brand — resilient to realm/dup-copy). */
|
|
68
|
+
function isNotImplemented(e) {
|
|
69
|
+
return (e instanceof NotImplemented ||
|
|
70
|
+
(typeof e === "object" && e !== null && Reflect.get(e, "urbanNotImplemented") === true));
|
|
71
|
+
}
|
|
54
72
|
/**
|
|
55
73
|
* Typed-identity helper the generated `operations.ts` wrapper re-types per operationId. Authoring:
|
|
56
74
|
* `export default defineOperation("createInvoice", async ({ params, query, body }, app) => { ... })`
|
|
@@ -64,6 +82,9 @@ export function defineOperation(_id, handler) {
|
|
|
64
82
|
function isFunction(v) {
|
|
65
83
|
return typeof v === "function";
|
|
66
84
|
}
|
|
85
|
+
function isRecord(v) {
|
|
86
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
87
|
+
}
|
|
67
88
|
/** Resolve an operation delegate from a loaded module: `default` (when a function) else `handler`. */
|
|
68
89
|
export function resolveOperationHandler(mod) {
|
|
69
90
|
if (isFunction(mod.default))
|
|
@@ -190,7 +211,8 @@ export function mountApi(ctx, app) {
|
|
|
190
211
|
}
|
|
191
212
|
return docPromise;
|
|
192
213
|
};
|
|
193
|
-
// Lazily import + cache each delegate module, mirroring mountActions.
|
|
214
|
+
// Lazily import + cache each delegate module, mirroring mountActions. This is the back-compat
|
|
215
|
+
// fallback for apps without a generated controller registry (see loadController below).
|
|
194
216
|
const moduleCache = new Map();
|
|
195
217
|
const loadModule = (operationId) => {
|
|
196
218
|
// Defense-in-depth at the import sink: collectOperations already skips unsafe ids, but never
|
|
@@ -209,6 +231,43 @@ export function mountApi(ctx, app) {
|
|
|
209
231
|
}
|
|
210
232
|
return pending;
|
|
211
233
|
};
|
|
234
|
+
// Prefer the generated delegate registry (ADR 0059): one statically-typed barrel the toolkit
|
|
235
|
+
// emits from the spec, so a missing/mismatched/orphan delegate is a `tsc` error, and dispatch is
|
|
236
|
+
// a deterministic map lookup rather than a per-request path import. Imported ONCE and cached.
|
|
237
|
+
// Absent (an app that hasn't re-genned) → `null`, and dispatch falls back to `loadModule`. A
|
|
238
|
+
// present-but-broken controller (a delegate that throws at import) is NOT swallowed — it retries
|
|
239
|
+
// and surfaces as a 500 with the reason, exactly like a bad delegate does today.
|
|
240
|
+
let controllerPromise;
|
|
241
|
+
const loadController = () => {
|
|
242
|
+
if (!controllerPromise) {
|
|
243
|
+
const rel = "nano-generated/controller";
|
|
244
|
+
controllerPromise = (async () => {
|
|
245
|
+
// Probe every supported module extension (compiled .js first, mirroring resolveModulePath),
|
|
246
|
+
// NOT just `.ts`: a published / `urban run` app ships `controller.js` with the TS sources
|
|
247
|
+
// absent, and probing only `.ts` there would wrongly report the registry missing and fall
|
|
248
|
+
// back to per-op dynamic imports — defeating deterministic dispatch in production.
|
|
249
|
+
let present = false;
|
|
250
|
+
for (const ext of MODULE_EXTENSION_CANDIDATES) {
|
|
251
|
+
if (await ctx.host.exists(resolveAppPath(ctx.root, rel + ext))) {
|
|
252
|
+
present = true;
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (!present)
|
|
257
|
+
return null;
|
|
258
|
+
const mod = await ctx.host.importModule(resolveAppPath(ctx.root, rel));
|
|
259
|
+
const ops = mod.operations;
|
|
260
|
+
if (!isRecord(ops)) {
|
|
261
|
+
throw new Error("nano-generated/controller must export an `operations` registry");
|
|
262
|
+
}
|
|
263
|
+
return ops;
|
|
264
|
+
})().catch((e) => {
|
|
265
|
+
controllerPromise = undefined; // don't cache the failure — let a later request retry
|
|
266
|
+
throw e;
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
return controllerPromise;
|
|
270
|
+
};
|
|
212
271
|
// Build the operation table lazily from the loaded doc (cached).
|
|
213
272
|
let opsPromise;
|
|
214
273
|
const loadOps = () => {
|
|
@@ -375,7 +434,22 @@ export function mountApi(ctx, app) {
|
|
|
375
434
|
}
|
|
376
435
|
let handler;
|
|
377
436
|
try {
|
|
378
|
-
|
|
437
|
+
const controller = await loadController();
|
|
438
|
+
if (controller) {
|
|
439
|
+
// Deterministic dispatch: the generated registry guarantees (at `tsc` time) one delegate
|
|
440
|
+
// per operationId. A missing/non-function key here means the controller is stale vs the spec.
|
|
441
|
+
const candidate = controller[op.operationId];
|
|
442
|
+
if (!isFunction(candidate)) {
|
|
443
|
+
ctx.host.log("error", "operation not registered in the generated controller (run `urban gen`)", {
|
|
444
|
+
operationId: op.operationId,
|
|
445
|
+
});
|
|
446
|
+
return json({ error: `operation ${op.operationId} is not registered — run \`urban gen\`` }, 500);
|
|
447
|
+
}
|
|
448
|
+
handler = candidate;
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
handler = resolveOperationHandler(await loadModule(op.operationId));
|
|
452
|
+
}
|
|
379
453
|
}
|
|
380
454
|
catch (e) {
|
|
381
455
|
ctx.host.log("error", "operation delegate failed to load", {
|
|
@@ -418,10 +492,14 @@ export function mountApi(ctx, app) {
|
|
|
418
492
|
};
|
|
419
493
|
}
|
|
420
494
|
catch (e) {
|
|
495
|
+
// A write-once stub (or any delegate) can signal "endpoint exists, not implemented yet" with
|
|
496
|
+
// a NotImplemented throw → 501, distinct from an unexpected fault (500).
|
|
497
|
+
if (isNotImplemented(e)) {
|
|
498
|
+
return json({ error: errorMessage(e), operationId: op.operationId }, 501);
|
|
499
|
+
}
|
|
421
500
|
return json({ error: errorMessage(e) }, 500);
|
|
422
501
|
}
|
|
423
502
|
};
|
|
424
|
-
// Categorical safety net: any unexpected throw during dispatch — e.g. a malformed spec
|
|
425
503
|
// `pattern` surfacing from validation — becomes a controlled 500 rather than escaping into
|
|
426
504
|
// the host request handler, which has no top-level catch of its own.
|
|
427
505
|
const dispatch = async (req) => {
|
|
@@ -2,6 +2,7 @@ import type { EngineClient, HostContext } from "./host.ts";
|
|
|
2
2
|
import { type AppManifest } from "./manifest.ts";
|
|
3
3
|
import type { TemplateSource } from "./modules/templates.ts";
|
|
4
4
|
import { DataLayer } from "./modules/datasource.ts";
|
|
5
|
+
import type { SchedulerDeps } from "./modules/scheduler.ts";
|
|
5
6
|
import { type SecurityPolicy } from "./modules/security.ts";
|
|
6
7
|
/** Resolve the HTTP port: explicit option, else $PORT, else 8090. Throws a clear
|
|
7
8
|
* error when $PORT is set but not a valid integer in 0..65535. */
|
|
@@ -34,6 +35,15 @@ export interface CreateUrbanAppOptions {
|
|
|
34
35
|
* `name → content` map). Merged over the manifest's `models.templates` (this wins on
|
|
35
36
|
* collision). See `deployModels`. */
|
|
36
37
|
templates?: TemplateSource;
|
|
38
|
+
/**
|
|
39
|
+
* Injectable timer + clock seam driving every background loop (the cron trigger loops
|
|
40
|
+
* and the instance-tracking reconciler). Defaults to the live scheduler (real timers,
|
|
41
|
+
* wall clock). The e2e test kit injects a manual scheduler so those loops advance
|
|
42
|
+
* deterministically over a virtual clock — the single seam that makes a whole-app
|
|
43
|
+
* `settle()` possible. Threaded verbatim into `mountTriggers` and `mountInstanceTracking`
|
|
44
|
+
* so one injected scheduler drives them both (no per-module wiring, no drift).
|
|
45
|
+
*/
|
|
46
|
+
scheduler?: SchedulerDeps;
|
|
37
47
|
}
|
|
38
48
|
export interface UrbanApp {
|
|
39
49
|
readonly manifest: AppManifest;
|
|
@@ -146,13 +146,13 @@ export async function createUrbanApp(opts) {
|
|
|
146
146
|
describe.surfaces = s.describe();
|
|
147
147
|
}
|
|
148
148
|
if (flags.triggers) {
|
|
149
|
-
const t = mountTriggers(ctx, api);
|
|
149
|
+
const t = mountTriggers(ctx, api, opts.scheduler);
|
|
150
150
|
routes.push(...t.routes);
|
|
151
151
|
mounted.push(t);
|
|
152
152
|
describe.triggers = t.describe?.();
|
|
153
153
|
}
|
|
154
154
|
if (flags.instanceTracking && (manifest.instanceTracking?.length ?? 0) > 0) {
|
|
155
|
-
const it = mountInstanceTracking(ctx, api);
|
|
155
|
+
const it = mountInstanceTracking(ctx, api, opts.scheduler);
|
|
156
156
|
mounted.push(it);
|
|
157
157
|
describe.instanceTracking = it.describe?.();
|
|
158
158
|
}
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export { cancelInstanceReconciling } from "./core/modules/cancel.ts";
|
|
|
21
21
|
export type { CancelInstanceResult, CancelInstanceState } from "./core/modules/cancel.ts";
|
|
22
22
|
export { mountActions, resolveActionHandler } from "./core/modules/actions.ts";
|
|
23
23
|
export type { ActionHandler, ActionRequest, ActionResult, ActionsHandle } from "./core/modules/actions.ts";
|
|
24
|
-
export { defineOperation, mountApi, resolveOperationHandler } from "./core/modules/api.ts";
|
|
24
|
+
export { defineOperation, mountApi, resolveOperationHandler, NotImplemented } from "./core/modules/api.ts";
|
|
25
25
|
export type { ApiBinding, ApiHandle, DefaultContract, OperationContract, OperationHandler, OperationInput, OperationResult, } from "./core/modules/api.ts";
|
|
26
26
|
export type { AppJobHandler } from "./core/modules/workers.ts";
|
|
27
27
|
export { resolveHandler, sdkDecisionEvaluator } from "./core/modules/workers.ts";
|
|
@@ -31,6 +31,8 @@ export { evalCorrelation } from "./core/modules/triggers.ts";
|
|
|
31
31
|
export { deployModels } from "./core/modules/deploy.ts";
|
|
32
32
|
export { applyTemplates, resolveTemplates } from "./core/modules/templates.ts";
|
|
33
33
|
export type { TemplateApplication, TemplateSource } from "./core/modules/templates.ts";
|
|
34
|
+
export { defaultScheduler, MAX_TIMER_DELAY_MS } from "./core/modules/scheduler.ts";
|
|
35
|
+
export type { SchedulerDeps } from "./core/modules/scheduler.ts";
|
|
34
36
|
export { createNodeHost } from "./adapters/node.ts";
|
|
35
37
|
export { createDenoHost } from "./adapters/deno.ts";
|
|
36
38
|
export { isDeno, selectHost } from "./adapters/detect.ts";
|
package/dist/runtime/index.js
CHANGED
|
@@ -17,13 +17,14 @@ export { makeGateway, Table } from "./core/modules/gateway.js";
|
|
|
17
17
|
export { createPagesRoutes, mountPages } from "./core/modules/pages.js";
|
|
18
18
|
export { cancelInstanceReconciling } from "./core/modules/cancel.js";
|
|
19
19
|
export { mountActions, resolveActionHandler } from "./core/modules/actions.js";
|
|
20
|
-
export { defineOperation, mountApi, resolveOperationHandler } from "./core/modules/api.js";
|
|
20
|
+
export { defineOperation, mountApi, resolveOperationHandler, NotImplemented } from "./core/modules/api.js";
|
|
21
21
|
export { resolveHandler, sdkDecisionEvaluator } from "./core/modules/workers.js";
|
|
22
22
|
export { buildMessages, callLlm, resolveProvider, runLlmJob, } from "./core/modules/llm.js";
|
|
23
23
|
export { evalCorrelation } from "./core/modules/triggers.js";
|
|
24
24
|
// Model deploy + `{{name}}` template substitution
|
|
25
25
|
export { deployModels } from "./core/modules/deploy.js";
|
|
26
26
|
export { applyTemplates, resolveTemplates } from "./core/modules/templates.js";
|
|
27
|
+
export { defaultScheduler, MAX_TIMER_DELAY_MS } from "./core/modules/scheduler.js";
|
|
27
28
|
// Adapters + engine + run entrypoint
|
|
28
29
|
export { createNodeHost } from "./adapters/node.js";
|
|
29
30
|
export { createDenoHost } from "./adapters/deno.js";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** The default directory operation delegates live in (mirrors the runtime's `api.dir` default). */
|
|
2
|
+
export declare const DEFAULT_OPERATIONS_DIR = "operations";
|
|
3
|
+
/**
|
|
4
|
+
* Normalize an `api.dir` to a clean, app-relative directory segment (no leading/trailing slashes,
|
|
5
|
+
* backslashes folded to `/`). An empty value falls back to {@link DEFAULT_OPERATIONS_DIR}.
|
|
6
|
+
*
|
|
7
|
+
* Throws if `dir` is absolute (POSIX `/…` or Windows `C:\…`/`\…`) or contains a `..` traversal
|
|
8
|
+
* segment — both would let the generated controller's import paths or the scaffolder's write target
|
|
9
|
+
* diverge from (or escape) the app root.
|
|
10
|
+
*/
|
|
11
|
+
export declare function normalizeApiDir(dir: string): string;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Shared normalization + validation for the OpenAPI surface's `api.dir` (the directory operation
|
|
2
|
+
// delegates live in). Used by BOTH the deriver (which bakes `<dir>/<operationId>` into the generated
|
|
3
|
+
// controller's static import paths) and the scaffolder (which writes stubs to `<dir>/<operationId>.ts`)
|
|
4
|
+
// so the two can never disagree on where a delegate lives — a single rule, one place.
|
|
5
|
+
//
|
|
6
|
+
// The rule is intentionally strict: `api.dir` must be a safe app-relative directory. An absolute path
|
|
7
|
+
// or a `..` segment is REJECTED with a clear error rather than silently rewritten, because a silent
|
|
8
|
+
// rewrite would let `urban gen` scaffold into one directory while the runtime resolves delegates
|
|
9
|
+
// from another (drift), and a `..` segment could escape the app root when later joined.
|
|
10
|
+
/** The default directory operation delegates live in (mirrors the runtime's `api.dir` default). */
|
|
11
|
+
export const DEFAULT_OPERATIONS_DIR = "operations";
|
|
12
|
+
/**
|
|
13
|
+
* Normalize an `api.dir` to a clean, app-relative directory segment (no leading/trailing slashes,
|
|
14
|
+
* backslashes folded to `/`). An empty value falls back to {@link DEFAULT_OPERATIONS_DIR}.
|
|
15
|
+
*
|
|
16
|
+
* Throws if `dir` is absolute (POSIX `/…` or Windows `C:\…`/`\…`) or contains a `..` traversal
|
|
17
|
+
* segment — both would let the generated controller's import paths or the scaffolder's write target
|
|
18
|
+
* diverge from (or escape) the app root.
|
|
19
|
+
*/
|
|
20
|
+
export function normalizeApiDir(dir) {
|
|
21
|
+
const raw = dir.trim();
|
|
22
|
+
if (raw.length === 0)
|
|
23
|
+
return DEFAULT_OPERATIONS_DIR;
|
|
24
|
+
if (isAbsolutePath(raw)) {
|
|
25
|
+
throw new Error(`api.dir must be an app-relative directory, not an absolute path: ${dir}`);
|
|
26
|
+
}
|
|
27
|
+
const cleaned = raw.replace(/\\/g, "/").replace(/^\.?\/+/, "").replace(/\/+$/, "");
|
|
28
|
+
if (cleaned.split("/").some((seg) => seg === "..")) {
|
|
29
|
+
throw new Error(`api.dir must not contain ".." path segments: ${dir}`);
|
|
30
|
+
}
|
|
31
|
+
return cleaned.length > 0 ? cleaned : DEFAULT_OPERATIONS_DIR;
|
|
32
|
+
}
|
|
33
|
+
/** True for a POSIX absolute path (`/…`) or a Windows absolute/drive path (`C:\…`, `C:/…`, `\…`). */
|
|
34
|
+
function isAbsolutePath(path) {
|
|
35
|
+
return /^([/\\]|[A-Za-z]:[/\\])/.test(path);
|
|
36
|
+
}
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import type { DerivedArtifact, Deriver } from "../artifact.ts";
|
|
2
|
+
import { DEFAULT_OPERATIONS_DIR } from "../api-dir.ts";
|
|
2
3
|
import { type OpenApiDoc, type OpenApiSchema } from "../../openapi/spec.ts";
|
|
3
4
|
/** Filenames of the emitted artifacts (siblings of worker-io.d.ts in nano-generated/). */
|
|
4
5
|
export declare const API_BINDINGS_DTS = "api-io.d.ts";
|
|
5
6
|
export declare const API_BINDINGS_TS = "operations.ts";
|
|
7
|
+
/** The generated delegate registry: statically imports every operation delegate and asserts the
|
|
8
|
+
* complete, correctly-typed set via `satisfies` — so a missing/mismatched/orphan delegate fails
|
|
9
|
+
* `tsc` (ADR 0059). The runtime dispatches through this map instead of per-op path imports. */
|
|
10
|
+
export declare const API_CONTROLLER_TS = "controller.ts";
|
|
11
|
+
export { DEFAULT_OPERATIONS_DIR };
|
|
6
12
|
/** Render a JSON-Schema (OpenAPI subset) as a TS type expression. Local component `$ref`s render as
|
|
7
13
|
* their component name (emitted as named interfaces); unknown shapes fall back to `unknown`. */
|
|
8
14
|
export declare function schemaToTs(schema: OpenApiSchema | undefined, depth?: number): string;
|
|
@@ -12,8 +18,25 @@ export declare function emitApiBindings(doc: OpenApiDoc): string;
|
|
|
12
18
|
* generated typed `defineWorker`). Written verbatim (it never changes with the spec) — it only
|
|
13
19
|
* re-types the runtime's `defineOperation`. */
|
|
14
20
|
export declare function emitApiBindingsRuntime(): string;
|
|
15
|
-
/**
|
|
16
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Emit `controller.ts`: the generated delegate registry (ADR 0059). It statically imports every
|
|
23
|
+
* operation delegate (`<dir>/<operationId>.ts`) and asserts the whole set against the spec-derived
|
|
24
|
+
* contract with a single `satisfies { [K in ApiOperationId]: OperationHandler<ReqFor<K>, ResFor<K>> }`.
|
|
25
|
+
* That one assertion gives four compile-time drift guarantees, all surfaced by `tsc`:
|
|
26
|
+
* • a missing delegate file → the static `import` fails ("Cannot find module");
|
|
27
|
+
* • an op in the spec, missing here → the mapped type requires key `K` ("Property is missing");
|
|
28
|
+
* • a delegate with the wrong signature → not assignable to `OperationHandler<Req, Res>`;
|
|
29
|
+
* • an orphan delegate (no spec op) → an excess property vs the mapped type.
|
|
30
|
+
* The runtime imports this ONE barrel and dispatches via `operations[operationId]`, so existing
|
|
31
|
+
* endpoints cannot drift from the contract. Delegates are keyed by their real operationId; the
|
|
32
|
+
* import binding is index-based (`op0`, `op1`, …) because an operationId is a safe path segment but
|
|
33
|
+
* not necessarily a valid TS identifier. Requires each delegate to be a default export.
|
|
34
|
+
*/
|
|
35
|
+
export declare function emitApiController(doc: OpenApiDoc, operationsDir?: string): string;
|
|
36
|
+
/** Derive the api artifacts from a parsed OpenAPI document. `operationsDir` is the manifest's
|
|
37
|
+
* `api.dir` (default `operations/`) — the controller's static delegate imports resolve into it. */
|
|
38
|
+
export declare function deriveApi(doc: OpenApiDoc, operationsDir?: string): DerivedArtifact[];
|
|
17
39
|
export declare const apiDeriver: Deriver<{
|
|
18
40
|
doc: OpenApiDoc;
|
|
41
|
+
operationsDir?: string;
|
|
19
42
|
}>;
|
|
@@ -6,10 +6,16 @@
|
|
|
6
6
|
// (`runtime/core/modules/api.ts`) is the matching half: it validates + routes from the SAME spec via
|
|
7
7
|
// the shared `openapi/spec.ts`, so types and runtime never drift.
|
|
8
8
|
import { GENERATED_DIR } from "../artifact.js";
|
|
9
|
-
import {
|
|
9
|
+
import { DEFAULT_OPERATIONS_DIR, normalizeApiDir } from "../api-dir.js";
|
|
10
|
+
import { collectOperations, assertUniqueOperationIds, isObjectSchema, refName, } from "../../openapi/spec.js";
|
|
10
11
|
/** Filenames of the emitted artifacts (siblings of worker-io.d.ts in nano-generated/). */
|
|
11
12
|
export const API_BINDINGS_DTS = "api-io.d.ts";
|
|
12
13
|
export const API_BINDINGS_TS = "operations.ts";
|
|
14
|
+
/** The generated delegate registry: statically imports every operation delegate and asserts the
|
|
15
|
+
* complete, correctly-typed set via `satisfies` — so a missing/mismatched/orphan delegate fails
|
|
16
|
+
* `tsc` (ADR 0059). The runtime dispatches through this map instead of per-op path imports. */
|
|
17
|
+
export const API_CONTROLLER_TS = "controller.ts";
|
|
18
|
+
export { DEFAULT_OPERATIONS_DIR };
|
|
13
19
|
/** A TS identifier form of an operationId (used for the per-op type names). */
|
|
14
20
|
function typeStem(operationId) {
|
|
15
21
|
const cleaned = operationId.replace(/[^A-Za-z0-9]+/g, " ").trim();
|
|
@@ -210,15 +216,64 @@ export function emitApiBindingsRuntime() {
|
|
|
210
216
|
` return defineOperationRaw(id, handler);\n` +
|
|
211
217
|
`}\n`);
|
|
212
218
|
}
|
|
213
|
-
/**
|
|
214
|
-
|
|
219
|
+
/** Normalize an `api.dir` to a clean relative segment (see `normalizeApiDir`); rejects absolute
|
|
220
|
+
* paths and `..` segments so the generated import paths stay app-relative and can't drift. */
|
|
221
|
+
function normalizeOperationsDir(dir) {
|
|
222
|
+
return normalizeApiDir(dir);
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Emit `controller.ts`: the generated delegate registry (ADR 0059). It statically imports every
|
|
226
|
+
* operation delegate (`<dir>/<operationId>.ts`) and asserts the whole set against the spec-derived
|
|
227
|
+
* contract with a single `satisfies { [K in ApiOperationId]: OperationHandler<ReqFor<K>, ResFor<K>> }`.
|
|
228
|
+
* That one assertion gives four compile-time drift guarantees, all surfaced by `tsc`:
|
|
229
|
+
* • a missing delegate file → the static `import` fails ("Cannot find module");
|
|
230
|
+
* • an op in the spec, missing here → the mapped type requires key `K` ("Property is missing");
|
|
231
|
+
* • a delegate with the wrong signature → not assignable to `OperationHandler<Req, Res>`;
|
|
232
|
+
* • an orphan delegate (no spec op) → an excess property vs the mapped type.
|
|
233
|
+
* The runtime imports this ONE barrel and dispatches via `operations[operationId]`, so existing
|
|
234
|
+
* endpoints cannot drift from the contract. Delegates are keyed by their real operationId; the
|
|
235
|
+
* import binding is index-based (`op0`, `op1`, …) because an operationId is a safe path segment but
|
|
236
|
+
* not necessarily a valid TS identifier. Requires each delegate to be a default export.
|
|
237
|
+
*/
|
|
238
|
+
export function emitApiController(doc, operationsDir = DEFAULT_OPERATIONS_DIR) {
|
|
239
|
+
assertUniqueOperationIds(doc);
|
|
240
|
+
const ops = collectOperations(doc);
|
|
241
|
+
const dir = normalizeOperationsDir(operationsDir);
|
|
242
|
+
const header = "// AUTO-GENERATED by @nanobpm/urban from the app's OpenAPI spec (ADR 0059): the delegate\n" +
|
|
243
|
+
"// registry. Statically imports every operation delegate and asserts the complete, correctly-\n" +
|
|
244
|
+
"// typed set via `satisfies` — a missing, mismatched, or orphan delegate fails `tsc`. The\n" +
|
|
245
|
+
"// runtime dispatches through this map, so endpoints cannot drift from the spec. Do not edit —\n" +
|
|
246
|
+
"// regenerated from the spec.\n" +
|
|
247
|
+
"// eslint-disable\n\n";
|
|
248
|
+
const typeImports = `import type { OperationHandler } from "@nanobpm/urban";\n` +
|
|
249
|
+
`import type { ApiOperationId, ApiOperations } from "./${API_BINDINGS_DTS}";\n`;
|
|
250
|
+
const delegateImports = ops
|
|
251
|
+
.map((op, i) => `import op${i} from ${JSON.stringify(`../${dir}/${op.operationId}.ts`)};`)
|
|
252
|
+
.join("\n");
|
|
253
|
+
const mapEntries = ops.map((op, i) => ` ${JSON.stringify(op.operationId)}: op${i},`).join("\n");
|
|
254
|
+
return (header +
|
|
255
|
+
typeImports +
|
|
256
|
+
(delegateImports ? `${delegateImports}\n` : "") +
|
|
257
|
+
`\n` +
|
|
258
|
+
`type ReqFor<K extends ApiOperationId> = ApiOperations[K]["request"];\n` +
|
|
259
|
+
`type ResFor<K extends ApiOperationId> = ApiOperations[K]["response"];\n\n` +
|
|
260
|
+
`/** Every operationId → its default-exported delegate. The \`satisfies\` clause makes \`tsc\`\n` +
|
|
261
|
+
` * reject a missing, wrongly-typed, or orphan delegate (ADR 0059). */\n` +
|
|
262
|
+
`export const operations = {\n${mapEntries}${mapEntries ? "\n" : ""}} satisfies {\n` +
|
|
263
|
+
` [K in ApiOperationId]: OperationHandler<ReqFor<K>, ResFor<K>>;\n` +
|
|
264
|
+
`};\n`);
|
|
265
|
+
}
|
|
266
|
+
/** Derive the api artifacts from a parsed OpenAPI document. `operationsDir` is the manifest's
|
|
267
|
+
* `api.dir` (default `operations/`) — the controller's static delegate imports resolve into it. */
|
|
268
|
+
export function deriveApi(doc, operationsDir = DEFAULT_OPERATIONS_DIR) {
|
|
215
269
|
return [
|
|
216
270
|
{ path: `${GENERATED_DIR}/${API_BINDINGS_DTS}`, content: emitApiBindings(doc) },
|
|
217
271
|
{ path: `${GENERATED_DIR}/${API_BINDINGS_TS}`, content: emitApiBindingsRuntime() },
|
|
272
|
+
{ path: `${GENERATED_DIR}/${API_CONTROLLER_TS}`, content: emitApiController(doc, operationsDir) },
|
|
218
273
|
];
|
|
219
274
|
}
|
|
220
275
|
export const apiDeriver = {
|
|
221
276
|
id: "openapi->api-io",
|
|
222
|
-
describe: "Derive api-io.d.ts (typed request/response per operationId) + the typed defineOperation.",
|
|
223
|
-
derive: ({ doc }) => deriveApi(doc),
|
|
277
|
+
describe: "Derive api-io.d.ts (typed request/response per operationId) + the typed defineOperation + the delegate registry.",
|
|
278
|
+
derive: ({ doc, operationsDir }) => deriveApi(doc, operationsDir),
|
|
224
279
|
};
|