expediate 1.0.5 → 1.0.6
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/CHANGELOG.md +138 -0
- package/CONTRIBUTING.md +150 -0
- package/README.md +278 -779
- package/dist/apis.d.ts +372 -12
- package/dist/apis.d.ts.map +1 -1
- package/dist/apis.js +483 -65
- package/dist/apis.js.map +1 -1
- package/dist/cjs/index.js +2290 -807
- package/dist/git.d.ts +1 -1
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +5 -5
- package/dist/git.js.map +1 -1
- package/dist/http-objects.d.ts +26 -0
- package/dist/http-objects.d.ts.map +1 -0
- package/dist/http-objects.js +588 -0
- package/dist/http-objects.js.map +1 -0
- package/dist/index.d.ts +6 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/jwt-auth.d.ts +11 -0
- package/dist/jwt-auth.d.ts.map +1 -1
- package/dist/jwt-auth.js +9 -9
- package/dist/jwt-auth.js.map +1 -1
- package/dist/middleware.js +2 -2
- package/dist/middleware.js.map +1 -1
- package/dist/mimetypes.json +882 -1
- package/dist/misc.d.ts +161 -25
- package/dist/misc.d.ts.map +1 -1
- package/dist/misc.js +228 -80
- package/dist/misc.js.map +1 -1
- package/dist/openapi.d.ts +156 -13
- package/dist/openapi.d.ts.map +1 -1
- package/dist/openapi.js +214 -71
- package/dist/openapi.js.map +1 -1
- package/dist/router-types.d.ts +760 -0
- package/dist/router-types.d.ts.map +1 -0
- package/dist/router-types.js +23 -0
- package/dist/router-types.js.map +1 -0
- package/dist/router.d.ts +7 -530
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +128 -375
- package/dist/router.js.map +1 -1
- package/dist/static.d.ts +2 -2
- package/dist/static.d.ts.map +1 -1
- package/dist/static.js +77 -22
- package/dist/static.js.map +1 -1
- package/docs/THREAT_MODEL.md +52 -0
- package/docs/api-builder-v2-design.md +644 -0
- package/docs/api-builder-v3-design.md +397 -0
- package/docs/api-builder.md +454 -0
- package/docs/benchmark.md +27 -0
- package/docs/body-parsing.md +223 -0
- package/docs/errors.md +359 -0
- package/docs/expediate.png +0 -0
- package/docs/git.md +139 -0
- package/docs/jwt-auth.md +251 -0
- package/docs/logo.svg +12 -0
- package/docs/middleware.md +264 -0
- package/docs/openapi.md +180 -0
- package/docs/router.md +356 -0
- package/docs/static.md +128 -0
- package/docs/wiki.json +123 -0
- package/package.json +30 -8
- package/dist/cjs/apis.js +0 -327
- package/dist/cjs/git.js +0 -293
- package/dist/cjs/jwt-auth.js +0 -532
- package/dist/cjs/middleware.js +0 -511
- package/dist/cjs/mimetypes.json +0 -1
- package/dist/cjs/misc.js +0 -787
- package/dist/cjs/openapi.js +0 -485
- package/dist/cjs/router.js +0 -898
- package/dist/cjs/static.js +0 -669
package/dist/openapi.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ServiceMethod, ServiceInstance, ServiceDefinition } from './apis.js';
|
|
1
|
+
import type { ServiceMethod, ServiceInstance, ServiceDefinition, Guard, AuthBinding, ApiBuilderOptions } from './apis.js';
|
|
2
2
|
/**
|
|
3
3
|
* A JSON Schema object (subset of draft-07 / OpenAPI 3.1 Schema Object).
|
|
4
4
|
*
|
|
@@ -96,6 +96,21 @@ export interface OperationMeta {
|
|
|
96
96
|
responses?: Record<string, ResponseObject>;
|
|
97
97
|
/** Mark as deprecated in the generated spec. */
|
|
98
98
|
deprecated?: boolean;
|
|
99
|
+
/**
|
|
100
|
+
* Guards run before this handler, after the API-level and controller-level
|
|
101
|
+
* guards. Ignored by spec generation — this is the natural per-route
|
|
102
|
+
* metadata slot for the request pipeline.
|
|
103
|
+
*/
|
|
104
|
+
guards?: Guard[];
|
|
105
|
+
/**
|
|
106
|
+
* Permission(s) required to call this operation.
|
|
107
|
+
*
|
|
108
|
+
* Overrides the controller-level `permission`. When set, the pipeline
|
|
109
|
+
* runs `auth.check(ctx, required)` before the guards, and the generated
|
|
110
|
+
* spec emits `security: [{ bearerAuth: [] }]` plus an
|
|
111
|
+
* `x-required-permissions` vendor extension on the operation.
|
|
112
|
+
*/
|
|
113
|
+
permission?: string | string[];
|
|
99
114
|
/** Additional vendor extensions (keys should start with `x-`). */
|
|
100
115
|
[key: string]: unknown;
|
|
101
116
|
}
|
|
@@ -125,6 +140,101 @@ export interface OpenApiServiceMeta {
|
|
|
125
140
|
*/
|
|
126
141
|
responses?: Record<string, ResponseObject>;
|
|
127
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* A route map for spec-only documentation: keys are Express-style path
|
|
145
|
+
* patterns, values are {@link OperationMeta} objects directly. There is no
|
|
146
|
+
* handler to call here — only something to describe.
|
|
147
|
+
*/
|
|
148
|
+
export type RouteOpenApi = Record<string, OperationMeta>;
|
|
149
|
+
/**
|
|
150
|
+
* A spec-only counterpart to `ControllerDefinition`: groups documented
|
|
151
|
+
* routes under a shared path prefix, default tags, and a default permission
|
|
152
|
+
* requirement, without any of the request-handling fields (`guards`).
|
|
153
|
+
*/
|
|
154
|
+
export interface ControllerOpenApi {
|
|
155
|
+
/** Path prefix prepended to every route in this controller (may contain params). */
|
|
156
|
+
prefix?: string;
|
|
157
|
+
/** Default OpenAPI tags applied to routes that do not declare their own. */
|
|
158
|
+
tags?: string[];
|
|
159
|
+
/** Default permission requirement for every route of this controller. */
|
|
160
|
+
permission?: string | string[];
|
|
161
|
+
/** Documented `GET` operations (paths relative to `prefix`). */
|
|
162
|
+
GET?: RouteOpenApi;
|
|
163
|
+
/** Documented `POST` operations (paths relative to `prefix`). */
|
|
164
|
+
POST?: RouteOpenApi;
|
|
165
|
+
/** Documented `PUT` operations (paths relative to `prefix`). */
|
|
166
|
+
PUT?: RouteOpenApi;
|
|
167
|
+
/** Documented `DELETE` operations (paths relative to `prefix`). */
|
|
168
|
+
DELETE?: RouteOpenApi;
|
|
169
|
+
/** Documented `PATCH` operations (paths relative to `prefix`). */
|
|
170
|
+
PATCH?: RouteOpenApi;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* A spec-only counterpart to {@link ServiceDefinition}, for documenting
|
|
174
|
+
* routes that have no `ServiceDefinition` of their own — most notably the
|
|
175
|
+
* JWT plugin's `/auth/login`, `/auth/refresh`, and `/auth/logout` endpoints,
|
|
176
|
+
* which are mounted directly with `app.post(...)` rather than through
|
|
177
|
+
* `apiBuilder`.
|
|
178
|
+
*
|
|
179
|
+
* It carries the same OpenAPI-relevant shape as `ServiceDefinition`
|
|
180
|
+
* (controllers, root route maps, schemas, auth binding, service-level
|
|
181
|
+
* `openapi` metadata) but route map values are {@link OperationMeta} objects
|
|
182
|
+
* instead of handler functions, and there is no instance lifecycle
|
|
183
|
+
* (`scope` / `data` / `setup` / `methods`) to run.
|
|
184
|
+
*
|
|
185
|
+
* `guards` and `validate` are accepted only for structural parity with
|
|
186
|
+
* {@link ServiceDefinition}; spec generation ignores both — there is no
|
|
187
|
+
* request pipeline here to run them against.
|
|
188
|
+
*/
|
|
189
|
+
export interface ServiceOpenApi {
|
|
190
|
+
/** Sub-controllers merged into this source, same merge rules as `ServiceDefinition.controllers`. */
|
|
191
|
+
controllers?: ControllerOpenApi[];
|
|
192
|
+
/** Ignored by spec generation; accepted for structural parity with `ServiceDefinition.guards`. */
|
|
193
|
+
guards?: Guard[];
|
|
194
|
+
/** Authentication binding — `scheme` and `permissionsExtension` affect the generated spec. */
|
|
195
|
+
auth?: AuthBinding;
|
|
196
|
+
/** Ignored by spec generation; accepted for structural parity with `ServiceDefinition.validate`. */
|
|
197
|
+
validate?: boolean | ApiBuilderOptions;
|
|
198
|
+
/**
|
|
199
|
+
* Reusable JSON Schema components merged into `components.schemas`
|
|
200
|
+
* (see {@link ServiceDefinition.schemas}).
|
|
201
|
+
*/
|
|
202
|
+
schemas?: Record<string, JsonSchema>;
|
|
203
|
+
/** Service-level OpenAPI metadata (default tag, shared schemas/responses). */
|
|
204
|
+
openapi?: OpenApiServiceMeta;
|
|
205
|
+
/** Documented `GET` operations. */
|
|
206
|
+
GET?: RouteOpenApi;
|
|
207
|
+
/** Documented `POST` operations. */
|
|
208
|
+
POST?: RouteOpenApi;
|
|
209
|
+
/** Documented `PUT` operations. */
|
|
210
|
+
PUT?: RouteOpenApi;
|
|
211
|
+
/** Documented `DELETE` operations. */
|
|
212
|
+
DELETE?: RouteOpenApi;
|
|
213
|
+
/** Documented `PATCH` operations. */
|
|
214
|
+
PATCH?: RouteOpenApi;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Anything {@link openApiSpec} can document: a real {@link ServiceDefinition}
|
|
218
|
+
* (the kind built by `apiBuilder`) or a spec-only {@link ServiceOpenApi}
|
|
219
|
+
* describing routes that aren't backed by a service at all.
|
|
220
|
+
*
|
|
221
|
+
* `openApiSpec` accepts a single source or an array of sources, so a real API
|
|
222
|
+
* and hand-documented routes can be merged into one document:
|
|
223
|
+
*
|
|
224
|
+
* ```ts
|
|
225
|
+
* const authDocs: ServiceOpenApi = {
|
|
226
|
+
* openapi: { tag: 'auth' },
|
|
227
|
+
* POST: {
|
|
228
|
+
* '/auth/login': { summary: 'Log in', requestBody: loginBody },
|
|
229
|
+
* '/auth/refresh': { summary: 'Refresh a token', requestBody: refreshBody },
|
|
230
|
+
* '/auth/logout': { summary: 'Log out' },
|
|
231
|
+
* },
|
|
232
|
+
* };
|
|
233
|
+
*
|
|
234
|
+
* const spec = openApiSpec([authDocs, todoService], { title: 'Todo API', version: '1.0.0' });
|
|
235
|
+
* ```
|
|
236
|
+
*/
|
|
237
|
+
export type OpenApiSource = ServiceDefinition<any> | ServiceOpenApi;
|
|
128
238
|
/**
|
|
129
239
|
* Top-level options passed to {@link openApiSpec}.
|
|
130
240
|
*
|
|
@@ -144,10 +254,10 @@ export interface SpecOptions {
|
|
|
144
254
|
*/
|
|
145
255
|
basePath?: string;
|
|
146
256
|
/** Server list (defaults to `[{ url: '/' }]` when absent). */
|
|
147
|
-
servers?:
|
|
257
|
+
servers?: {
|
|
148
258
|
url: string;
|
|
149
259
|
description?: string;
|
|
150
|
-
}
|
|
260
|
+
}[];
|
|
151
261
|
/**
|
|
152
262
|
* Additional schemas merged into `components.schemas` (takes precedence
|
|
153
263
|
* over service-level `openapi.schemas` of the same name).
|
|
@@ -167,18 +277,24 @@ export interface OpenApiDocument {
|
|
|
167
277
|
version: string;
|
|
168
278
|
description?: string;
|
|
169
279
|
};
|
|
170
|
-
servers?:
|
|
280
|
+
servers?: {
|
|
171
281
|
url: string;
|
|
172
282
|
description?: string;
|
|
173
|
-
}
|
|
174
|
-
tags?:
|
|
283
|
+
}[];
|
|
284
|
+
tags?: {
|
|
175
285
|
name: string;
|
|
176
286
|
description?: string;
|
|
177
|
-
}
|
|
287
|
+
}[];
|
|
178
288
|
paths: Record<string, Record<string, unknown>>;
|
|
179
289
|
components: {
|
|
180
290
|
schemas: Record<string, JsonSchema>;
|
|
181
291
|
responses: Record<string, ResponseObject>;
|
|
292
|
+
/**
|
|
293
|
+
* Security schemes — emitted when at least one operation declares a
|
|
294
|
+
* `permission` (the scheme comes from `AuthBinding.scheme`, defaulting
|
|
295
|
+
* to HTTP bearer / JWT).
|
|
296
|
+
*/
|
|
297
|
+
securitySchemes?: Record<string, Record<string, unknown>>;
|
|
182
298
|
};
|
|
183
299
|
}
|
|
184
300
|
/**
|
|
@@ -240,22 +356,35 @@ export declare const DESCRIBE_META: unique symbol;
|
|
|
240
356
|
*/
|
|
241
357
|
export declare function describe<TInstance extends ServiceInstance = ServiceInstance>(handler: ServiceMethod<TInstance>, meta: OperationMeta): ServiceMethod<TInstance>;
|
|
242
358
|
/**
|
|
243
|
-
* Generate an OpenAPI 3.1.0 document from
|
|
359
|
+
* Generate an OpenAPI 3.1.0 document from one or more {@link OpenApiSource}
|
|
360
|
+
* values — real {@link ServiceDefinition}s, spec-only {@link ServiceOpenApi}
|
|
361
|
+
* descriptions, or a mix of both in a single array.
|
|
244
362
|
*
|
|
245
363
|
* Route handlers that have been annotated with {@link describe} contribute
|
|
246
364
|
* rich operation metadata (summary, description, parameters, requestBody,
|
|
247
365
|
* responses). Unannotated handlers receive sensible defaults automatically.
|
|
366
|
+
* `ServiceOpenApi` route maps provide that same {@link OperationMeta} shape
|
|
367
|
+
* directly, since there is no handler to annotate.
|
|
248
368
|
*
|
|
249
369
|
* The generated document always includes:
|
|
250
370
|
* - An `ApiError` schema (shape: `{ status?, message?, data? }`) in
|
|
251
371
|
* `components.schemas`.
|
|
252
372
|
* - An `ApiError` response (`500` reference) in `components.responses`.
|
|
253
373
|
*
|
|
254
|
-
* Caller-supplied `opts.schemas` and
|
|
255
|
-
*
|
|
256
|
-
*
|
|
374
|
+
* Caller-supplied `opts.schemas` and each source's `openapi.schemas` /
|
|
375
|
+
* `schemas` are merged on top of the built-in components, source by source
|
|
376
|
+
* in array order — for a single source this preserves the original
|
|
377
|
+
* precedence exactly: built-ins ← `openapi.schemas` ← `opts.schemas` ←
|
|
378
|
+
* `schemas`.
|
|
379
|
+
*
|
|
380
|
+
* Routes are merged and duplicate-checked **across all sources** (see
|
|
381
|
+
* {@link collectOpenApiRoutes}), so passing several sources still produces
|
|
382
|
+
* ONE document. Each route's default tag and permissions vendor-extension
|
|
383
|
+
* name are resolved from its own originating source; the
|
|
384
|
+
* `components.securitySchemes.bearerAuth` value comes from the first source
|
|
385
|
+
* in array order that declares a custom `auth.scheme`, else the default.
|
|
257
386
|
*
|
|
258
|
-
* @param service - The
|
|
387
|
+
* @param service - The source(s) to document.
|
|
259
388
|
* @param opts - Top-level spec options (title, version, basePath, …).
|
|
260
389
|
* @returns A fully-formed OpenAPI 3.1.0 document object.
|
|
261
390
|
*
|
|
@@ -271,8 +400,22 @@ export declare function describe<TInstance extends ServiceInstance = ServiceInst
|
|
|
271
400
|
* res.json(spec);
|
|
272
401
|
* });
|
|
273
402
|
* ```
|
|
403
|
+
*
|
|
404
|
+
* @example Merging a real service with hand-documented auth routes
|
|
405
|
+
* ```ts
|
|
406
|
+
* const authDocs: ServiceOpenApi = {
|
|
407
|
+
* openapi: { tag: 'auth' },
|
|
408
|
+
* POST: {
|
|
409
|
+
* '/auth/login': { summary: 'Log in' },
|
|
410
|
+
* '/auth/refresh': { summary: 'Refresh a token' },
|
|
411
|
+
* '/auth/logout': { summary: 'Log out' },
|
|
412
|
+
* },
|
|
413
|
+
* };
|
|
414
|
+
*
|
|
415
|
+
* const spec = openApiSpec([authDocs, todoDefinition], { title: 'Todo API', version: '1.0.0' });
|
|
416
|
+
* ```
|
|
274
417
|
*/
|
|
275
|
-
export declare function openApiSpec
|
|
418
|
+
export declare function openApiSpec(service: OpenApiSource | OpenApiSource[], opts: SpecOptions): OpenApiDocument;
|
|
276
419
|
declare module './apis.js' {
|
|
277
420
|
interface ServiceDefinition<TInstance extends ServiceInstance> {
|
|
278
421
|
/**
|
package/dist/openapi.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"openapi.d.ts","sourceRoot":"","sources":["../src/openapi.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"openapi.d.ts","sourceRoot":"","sources":["../src/openapi.ts"],"names":[],"mappings":"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"}
|
package/dist/openapi.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* DEALINGS IN THE SOFTWARE.
|
|
20
20
|
*/
|
|
21
21
|
'use strict';
|
|
22
|
+
import { joinPath, routeScore, normalizePermission } from './apis.js';
|
|
22
23
|
// ── YAML serialiser (zero-dependency, block-style) ────────────────────────────
|
|
23
24
|
/**
|
|
24
25
|
* YAML reserved keywords that must be quoted as scalars so that YAML parsers
|
|
@@ -27,15 +28,6 @@
|
|
|
27
28
|
const YAML_KW = new Set([
|
|
28
29
|
'true', 'false', 'yes', 'no', 'on', 'off', 'null', '~',
|
|
29
30
|
]);
|
|
30
|
-
/**
|
|
31
|
-
* Pattern for "safe" plain scalars: they can be represented without quoting.
|
|
32
|
-
* A scalar is safe when it:
|
|
33
|
-
* - contains only letters, digits, `_`, `-`, `.` or `/`
|
|
34
|
-
* - starts with a letter, `_`, `/`, or `$`
|
|
35
|
-
* - is not a YAML reserved keyword
|
|
36
|
-
* - does not look like a number
|
|
37
|
-
*/
|
|
38
|
-
const SAFE_SCALAR = /^[a-zA-Z$_/][a-zA-Z0-9$_\-./]*$/;
|
|
39
31
|
/** Pattern matching integer and floating-point number strings. */
|
|
40
32
|
const LOOKS_LIKE_NUMBER = /^[-+]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$|^0x[0-9a-fA-F]+$|^0o[0-7]+$/;
|
|
41
33
|
/**
|
|
@@ -68,7 +60,7 @@ function yamlString(s) {
|
|
|
68
60
|
s !== s.trim() ||
|
|
69
61
|
// Flow indicator characters anywhere — present in path patterns such as
|
|
70
62
|
// `/items/{id}` and must be quoted to avoid flow-collection ambiguity.
|
|
71
|
-
/[{}
|
|
63
|
+
/[{}[\]]/.test(s) ||
|
|
72
64
|
// Control characters.
|
|
73
65
|
/[\x00-\x1f\x7f]/.test(s);
|
|
74
66
|
if (!needsQuote)
|
|
@@ -159,6 +151,10 @@ function toYamlLines(value) {
|
|
|
159
151
|
}
|
|
160
152
|
return lines;
|
|
161
153
|
}
|
|
154
|
+
// Defensive fallback: every JSON value type (null, boolean, number, string,
|
|
155
|
+
// array, object) is handled above, so `value` here is only reachable for
|
|
156
|
+
// bigint/symbol/function — none of which occur in a JSON-derived spec.
|
|
157
|
+
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
|
162
158
|
return [String(value)];
|
|
163
159
|
}
|
|
164
160
|
/**
|
|
@@ -344,27 +340,130 @@ function buildAnnotatedResponses(responses) {
|
|
|
344
340
|
return result;
|
|
345
341
|
}
|
|
346
342
|
// ---------------------------------------------------------------------------
|
|
347
|
-
//
|
|
343
|
+
// Multi-source route collection (spec-generation counterpart to `collectRoutes`)
|
|
348
344
|
// ---------------------------------------------------------------------------
|
|
349
|
-
/**
|
|
345
|
+
/**
|
|
346
|
+
* The five HTTP verbs `openApiSpec` looks for route maps under.
|
|
347
|
+
*
|
|
348
|
+
* Kept as a private duplicate of `apis.ts`'s internal `VERBS` (only the
|
|
349
|
+
* derived {@link ApiVerb} type is exported from there) — not worth exporting
|
|
350
|
+
* a const for.
|
|
351
|
+
*/
|
|
350
352
|
const VERBS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'];
|
|
351
353
|
/**
|
|
352
|
-
*
|
|
354
|
+
* Build the merged, globally-sorted route table for one or more
|
|
355
|
+
* {@link OpenApiSource} values — the spec-generation counterpart to
|
|
356
|
+
* `apis.ts`'s `collectRoutes`.
|
|
357
|
+
*
|
|
358
|
+
* Deliberately kept separate from `collectRoutes` so the real request
|
|
359
|
+
* pipeline (`apiBuilder`) is never affected by spec-only concerns: this
|
|
360
|
+
* function only inspects route *shapes* to produce documentation and never
|
|
361
|
+
* invokes a handler. A route value is resolved as `describe()`-attached
|
|
362
|
+
* metadata when it is a function, or used directly as an {@link OperationMeta}
|
|
363
|
+
* object when it is not (the {@link ServiceOpenApi} case).
|
|
364
|
+
*
|
|
365
|
+
* Duplicate `(verb, path)` pairs are detected **across all sources**, not
|
|
366
|
+
* just within one — extending `collectRoutes`'s single-service duplicate
|
|
367
|
+
* check to the merged multi-source document.
|
|
368
|
+
*
|
|
369
|
+
* @throws Error on a duplicate `(verb, path)` pair across any of the sources.
|
|
370
|
+
*/
|
|
371
|
+
function collectOpenApiRoutes(sources) {
|
|
372
|
+
const routes = [];
|
|
373
|
+
/** Duplicate detection across ALL sources: `"VERB /joined/path"` → declarer label. */
|
|
374
|
+
const seen = new Map();
|
|
375
|
+
sources.forEach((source, sourceIndex) => {
|
|
376
|
+
const defaultTag = source.openapi?.tag;
|
|
377
|
+
const permissionsExtension = source.auth?.permissionsExtension ?? 'x-required-permissions';
|
|
378
|
+
// Root route maps form an implicit, anonymous controller — same trick as
|
|
379
|
+
// `collectRoutes`'s `rootController`.
|
|
380
|
+
const rootController = {
|
|
381
|
+
prefix: '',
|
|
382
|
+
GET: source.GET,
|
|
383
|
+
POST: source.POST,
|
|
384
|
+
PUT: source.PUT,
|
|
385
|
+
DELETE: source.DELETE,
|
|
386
|
+
PATCH: source.PATCH,
|
|
387
|
+
};
|
|
388
|
+
const controllers = [rootController, ...(source.controllers ?? [])];
|
|
389
|
+
controllers.forEach((controller, controllerIndex) => {
|
|
390
|
+
const label = controllerIndex === 0
|
|
391
|
+
? `source #${sourceIndex}`
|
|
392
|
+
: (controller.tags?.[0] ?? controller.prefix ?? `source #${sourceIndex} controller #${controllerIndex}`);
|
|
393
|
+
const prefix = controller.prefix ?? '';
|
|
394
|
+
for (const verb of VERBS) {
|
|
395
|
+
const routeMap = controller[verb];
|
|
396
|
+
if (!routeMap)
|
|
397
|
+
continue;
|
|
398
|
+
for (const [pattern, value] of Object.entries(routeMap)) {
|
|
399
|
+
const path = joinPath(prefix, pattern);
|
|
400
|
+
// Loud failure on duplicates, across the whole merged document.
|
|
401
|
+
const dupKey = `${verb} ${path}`;
|
|
402
|
+
const declarer = seen.get(dupKey);
|
|
403
|
+
if (declarer !== undefined) {
|
|
404
|
+
throw new Error(`openApiSpec: duplicate route ${verb} ${path}\n` +
|
|
405
|
+
` declared by '${declarer}' and '${label}'`);
|
|
406
|
+
}
|
|
407
|
+
seen.set(dupKey, label);
|
|
408
|
+
const meta = typeof value === 'function'
|
|
409
|
+
? value[DESCRIBE_META]
|
|
410
|
+
: value;
|
|
411
|
+
routes.push({
|
|
412
|
+
verb,
|
|
413
|
+
path,
|
|
414
|
+
meta,
|
|
415
|
+
tags: meta?.tags ?? controller.tags,
|
|
416
|
+
permission: normalizePermission(meta?.permission ?? controller.permission),
|
|
417
|
+
defaultTag,
|
|
418
|
+
permissionsExtension,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
// Global specificity sort across all sources and controllers.
|
|
425
|
+
routes.sort((a, b) => routeScore(b.path) - routeScore(a.path) || b.path.localeCompare(a.path));
|
|
426
|
+
return routes;
|
|
427
|
+
}
|
|
428
|
+
// ---------------------------------------------------------------------------
|
|
429
|
+
// Core spec generator
|
|
430
|
+
// ---------------------------------------------------------------------------
|
|
431
|
+
/** Default OpenAPI security scheme when `AuthBinding.scheme` is absent. */
|
|
432
|
+
const DEFAULT_SECURITY_SCHEME = {
|
|
433
|
+
type: 'http',
|
|
434
|
+
scheme: 'bearer',
|
|
435
|
+
bearerFormat: 'JWT',
|
|
436
|
+
};
|
|
437
|
+
/**
|
|
438
|
+
* Generate an OpenAPI 3.1.0 document from one or more {@link OpenApiSource}
|
|
439
|
+
* values — real {@link ServiceDefinition}s, spec-only {@link ServiceOpenApi}
|
|
440
|
+
* descriptions, or a mix of both in a single array.
|
|
353
441
|
*
|
|
354
442
|
* Route handlers that have been annotated with {@link describe} contribute
|
|
355
443
|
* rich operation metadata (summary, description, parameters, requestBody,
|
|
356
444
|
* responses). Unannotated handlers receive sensible defaults automatically.
|
|
445
|
+
* `ServiceOpenApi` route maps provide that same {@link OperationMeta} shape
|
|
446
|
+
* directly, since there is no handler to annotate.
|
|
357
447
|
*
|
|
358
448
|
* The generated document always includes:
|
|
359
449
|
* - An `ApiError` schema (shape: `{ status?, message?, data? }`) in
|
|
360
450
|
* `components.schemas`.
|
|
361
451
|
* - An `ApiError` response (`500` reference) in `components.responses`.
|
|
362
452
|
*
|
|
363
|
-
* Caller-supplied `opts.schemas` and
|
|
364
|
-
*
|
|
365
|
-
*
|
|
453
|
+
* Caller-supplied `opts.schemas` and each source's `openapi.schemas` /
|
|
454
|
+
* `schemas` are merged on top of the built-in components, source by source
|
|
455
|
+
* in array order — for a single source this preserves the original
|
|
456
|
+
* precedence exactly: built-ins ← `openapi.schemas` ← `opts.schemas` ←
|
|
457
|
+
* `schemas`.
|
|
458
|
+
*
|
|
459
|
+
* Routes are merged and duplicate-checked **across all sources** (see
|
|
460
|
+
* {@link collectOpenApiRoutes}), so passing several sources still produces
|
|
461
|
+
* ONE document. Each route's default tag and permissions vendor-extension
|
|
462
|
+
* name are resolved from its own originating source; the
|
|
463
|
+
* `components.securitySchemes.bearerAuth` value comes from the first source
|
|
464
|
+
* in array order that declares a custom `auth.scheme`, else the default.
|
|
366
465
|
*
|
|
367
|
-
* @param service - The
|
|
466
|
+
* @param service - The source(s) to document.
|
|
368
467
|
* @param opts - Top-level spec options (title, version, basePath, …).
|
|
369
468
|
* @returns A fully-formed OpenAPI 3.1.0 document object.
|
|
370
469
|
*
|
|
@@ -380,11 +479,24 @@ const VERBS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'];
|
|
|
380
479
|
* res.json(spec);
|
|
381
480
|
* });
|
|
382
481
|
* ```
|
|
482
|
+
*
|
|
483
|
+
* @example Merging a real service with hand-documented auth routes
|
|
484
|
+
* ```ts
|
|
485
|
+
* const authDocs: ServiceOpenApi = {
|
|
486
|
+
* openapi: { tag: 'auth' },
|
|
487
|
+
* POST: {
|
|
488
|
+
* '/auth/login': { summary: 'Log in' },
|
|
489
|
+
* '/auth/refresh': { summary: 'Refresh a token' },
|
|
490
|
+
* '/auth/logout': { summary: 'Log out' },
|
|
491
|
+
* },
|
|
492
|
+
* };
|
|
493
|
+
*
|
|
494
|
+
* const spec = openApiSpec([authDocs, todoDefinition], { title: 'Todo API', version: '1.0.0' });
|
|
495
|
+
* ```
|
|
383
496
|
*/
|
|
384
497
|
export function openApiSpec(service, opts) {
|
|
498
|
+
const sources = Array.isArray(service) ? service : [service];
|
|
385
499
|
const basePath = opts.basePath ?? '';
|
|
386
|
-
const svcMeta = service.openapi;
|
|
387
|
-
const defaultTag = svcMeta?.tag;
|
|
388
500
|
// ── Components ──────────────────────────────────────────────────────────────
|
|
389
501
|
const builtinSchemas = {
|
|
390
502
|
ApiError: {
|
|
@@ -404,64 +516,86 @@ export function openApiSpec(service, opts) {
|
|
|
404
516
|
},
|
|
405
517
|
},
|
|
406
518
|
};
|
|
407
|
-
// Merge schemas: built-ins ←
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
};
|
|
413
|
-
const
|
|
414
|
-
...
|
|
415
|
-
|
|
416
|
-
|
|
519
|
+
// Merge schemas: built-ins ← each source's openapi.schemas (array order)
|
|
520
|
+
// ← caller-level opts.schemas ← each source's own `schemas` (array order,
|
|
521
|
+
// last wins). For a single source this is exactly the original order:
|
|
522
|
+
// built-ins, service-level openapi meta, caller-level, service-definition
|
|
523
|
+
// `schemas` — which supersedes `opts.schemas`.
|
|
524
|
+
let schemas = { ...builtinSchemas };
|
|
525
|
+
for (const source of sources)
|
|
526
|
+
schemas = { ...schemas, ...source.openapi?.schemas };
|
|
527
|
+
schemas = { ...schemas, ...opts.schemas };
|
|
528
|
+
for (const source of sources)
|
|
529
|
+
schemas = { ...schemas, ...source.schemas };
|
|
530
|
+
let responses = { ...builtinResponses };
|
|
531
|
+
for (const source of sources)
|
|
532
|
+
responses = { ...responses, ...source.openapi?.responses };
|
|
417
533
|
// ── Tags ─────────────────────────────────────────────────────────────────────
|
|
418
534
|
const tags = [];
|
|
419
|
-
|
|
420
|
-
|
|
535
|
+
const seenTags = new Set();
|
|
536
|
+
for (const source of sources) {
|
|
537
|
+
const tag = source.openapi?.tag;
|
|
538
|
+
if (tag && !seenTags.has(tag)) {
|
|
539
|
+
seenTags.add(tag);
|
|
540
|
+
tags.push({ name: tag, description: source.openapi?.tagDescription });
|
|
541
|
+
}
|
|
421
542
|
}
|
|
543
|
+
// ── Security scheme ──────────────────────────────────────────────────────────
|
|
544
|
+
// First source in array order that declares a custom scheme wins; falls
|
|
545
|
+
// back to the default HTTP bearer/JWT scheme.
|
|
546
|
+
const securityScheme = sources.find(s => s.auth?.scheme)?.auth?.scheme ?? DEFAULT_SECURITY_SCHEME;
|
|
422
547
|
// ── Paths ────────────────────────────────────────────────────────────────────
|
|
548
|
+
// Operates on the merged route table (all sources' root route maps and
|
|
549
|
+
// controllers), so multiple sources still produce ONE document. Controller
|
|
550
|
+
// `tags` fill in `OperationMeta.tags` when a route declares none.
|
|
423
551
|
const paths = {};
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
if (k.startsWith('x-'))
|
|
460
|
-
operation[k] = v;
|
|
461
|
-
}
|
|
552
|
+
const routes = collectOpenApiRoutes(sources);
|
|
553
|
+
let securedRoutes = false;
|
|
554
|
+
for (const route of routes) {
|
|
555
|
+
const { verb, path: pattern, meta } = route;
|
|
556
|
+
const openApiPath = toOpenApiPath(pattern, basePath);
|
|
557
|
+
if (!paths[openApiPath])
|
|
558
|
+
paths[openApiPath] = {};
|
|
559
|
+
// ── Parameters ─────────────────────────────────────────────────────────
|
|
560
|
+
const annotatedParams = meta?.parameters ?? [];
|
|
561
|
+
const inferredParams = extractPathParams(pattern, annotatedParams);
|
|
562
|
+
const parameters = [...annotatedParams, ...inferredParams];
|
|
563
|
+
// ── Responses ──────────────────────────────────────────────────────────
|
|
564
|
+
const operationResponses = meta?.responses
|
|
565
|
+
? buildAnnotatedResponses(meta.responses)
|
|
566
|
+
: buildDefaultResponses(verb);
|
|
567
|
+
// ── Operation object ───────────────────────────────────────────────────
|
|
568
|
+
const operation = {
|
|
569
|
+
operationId: meta?.operationId ?? buildOperationId(verb, pattern),
|
|
570
|
+
...(meta?.summary && { summary: meta.summary }),
|
|
571
|
+
...(meta?.description && { description: meta.description }),
|
|
572
|
+
...(meta?.deprecated && { deprecated: true }),
|
|
573
|
+
...(parameters.length > 0 && { parameters }),
|
|
574
|
+
...(meta?.requestBody && { requestBody: meta.requestBody }),
|
|
575
|
+
responses: operationResponses,
|
|
576
|
+
};
|
|
577
|
+
// Apply route tags (meta-level, else controller-level), then this route's
|
|
578
|
+
// source default tag when neither is declared.
|
|
579
|
+
const opTags = route.tags ?? (route.defaultTag ? [route.defaultTag] : undefined);
|
|
580
|
+
if (opTags)
|
|
581
|
+
operation.tags = opTags;
|
|
582
|
+
// Carry through any vendor extensions (x-* keys).
|
|
583
|
+
if (meta) {
|
|
584
|
+
for (const [k, v] of Object.entries(meta)) {
|
|
585
|
+
if (k.startsWith('x-'))
|
|
586
|
+
operation[k] = v;
|
|
462
587
|
}
|
|
463
|
-
paths[openApiPath][verb.toLowerCase()] = operation;
|
|
464
588
|
}
|
|
589
|
+
// ── Security ───────────────────────────────────────────────────────────
|
|
590
|
+
// Routes carrying a permission requirement (route- or controller-level)
|
|
591
|
+
// are marked with the bearerAuth scheme and the vendor extension listing
|
|
592
|
+
// the required permissions.
|
|
593
|
+
if (route.permission) {
|
|
594
|
+
securedRoutes = true;
|
|
595
|
+
operation.security = [{ bearerAuth: [] }];
|
|
596
|
+
operation[route.permissionsExtension] = route.permission;
|
|
597
|
+
}
|
|
598
|
+
paths[openApiPath][verb.toLowerCase()] = operation;
|
|
465
599
|
}
|
|
466
600
|
// ── Assemble document ────────────────────────────────────────────────────────
|
|
467
601
|
const doc = {
|
|
@@ -474,7 +608,16 @@ export function openApiSpec(service, opts) {
|
|
|
474
608
|
...(opts.servers && { servers: opts.servers }),
|
|
475
609
|
...(tags.length > 0 && { tags }),
|
|
476
610
|
paths,
|
|
477
|
-
components: {
|
|
611
|
+
components: {
|
|
612
|
+
schemas,
|
|
613
|
+
responses,
|
|
614
|
+
// Emitted once when at least one operation declares a permission.
|
|
615
|
+
...(securedRoutes && {
|
|
616
|
+
securitySchemes: {
|
|
617
|
+
bearerAuth: securityScheme,
|
|
618
|
+
},
|
|
619
|
+
}),
|
|
620
|
+
},
|
|
478
621
|
};
|
|
479
622
|
return doc;
|
|
480
623
|
}
|