@zudojs/openapi 0.1.0 → 1.2.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/LICENSE +21 -0
- package/README.md +340 -21
- package/dist/index.d.ts +26 -21
- package/dist/index.js +31 -13
- package/dist/openApiComponents/index.d.ts +2 -2
- package/dist/openApiComponents/index.js +1 -1
- package/dist/openApiConstants/index.d.ts +1 -1
- package/dist/openApiConstants/index.js +1 -1
- package/dist/openApiConstants/openApiConstants.core.d.ts +15 -3
- package/dist/openApiConstants/openApiConstants.core.js +22 -3
- package/dist/openApiDocument/index.d.ts +1 -1
- package/dist/openApiDocument/index.js +1 -1
- package/dist/openApiDocument/openApiDocument.builder.d.ts +39 -52
- package/dist/openApiDocument/openApiDocument.builder.js +73 -72
- package/dist/openApiErrors/index.d.ts +2 -2
- package/dist/openApiErrors/index.js +2 -1
- package/dist/openApiErrors/openApiError.base.d.ts +9 -0
- package/dist/openApiErrors/openApiError.base.js +7 -0
- package/dist/openApiErrors/openApiError.types.d.ts +34 -14
- package/dist/openApiErrors/openApiError.types.js +83 -35
- package/dist/openApiHttp/index.d.ts +2 -2
- package/dist/openApiHttp/index.js +2 -2
- package/dist/openApiHttp/openApiHttpAdapter.core.d.ts +129 -25
- package/dist/openApiHttp/openApiHttpAdapter.core.js +226 -16
- package/dist/openApiRegistry/openApiRegistry.core.d.ts +47 -5
- package/dist/openApiRegistry/openApiRegistry.core.js +180 -66
- package/dist/openApiRegistry/openApiRegistry.type.d.ts +18 -13
- package/dist/openApiRegistry/openApiRegistry.type.js +1 -2
- package/dist/openApiRouting/index.d.ts +2 -2
- package/dist/openApiRouting/index.js +1 -1
- package/dist/openApiRouting/routeConverter.core.d.ts +25 -7
- package/dist/openApiRouting/routeConverter.core.js +104 -58
- package/dist/openApiRouting/routeMetadata.type.d.ts +40 -17
- package/dist/openApiRouting/routeMetadata.type.js +9 -0
- package/dist/openApiRouting/routeScanner.core.d.ts +17 -47
- package/dist/openApiRouting/routeScanner.core.js +40 -55
- package/dist/openApiSchema/index.d.ts +4 -4
- package/dist/openApiSchema/index.js +2 -2
- package/dist/openApiSchema/references.core.d.ts +18 -1
- package/dist/openApiSchema/references.core.js +21 -1
- package/dist/openApiSchema/schemaConverter.core.d.ts +61 -3
- package/dist/openApiSchema/schemaConverter.core.js +523 -117
- package/dist/openApiSchema/schemaRegistry.core.d.ts +27 -10
- package/dist/openApiSchema/schemaRegistry.core.js +33 -14
- package/dist/openApiSerialization/openApiSerializer.core.d.ts +10 -4
- package/dist/openApiSerialization/openApiSerializer.core.js +158 -9
- package/dist/openApiTypes/index.d.ts +1 -1
- package/dist/openApiTypes/openApiDocument.type.d.ts +16 -0
- package/dist/openApiTypes/openApiSchema.type.d.ts +24 -1
- package/dist/openApiTypes/openApiSchema.type.js +3 -0
- package/dist/openApiUi/index.d.ts +8 -0
- package/dist/openApiUi/index.js +8 -0
- package/dist/openApiUi/openApiUi.brand.d.ts +26 -0
- package/dist/openApiUi/openApiUi.brand.js +44 -0
- package/dist/openApiUi/openApiUi.core.d.ts +47 -0
- package/dist/openApiUi/openApiUi.core.js +157 -0
- package/dist/openApiValidation/index.d.ts +2 -2
- package/dist/openApiValidation/index.js +1 -1
- package/dist/openApiValidation/openApiValidator.core.d.ts +33 -11
- package/dist/openApiValidation/openApiValidator.core.js +295 -66
- package/package.json +29 -18
- package/dist/.tsbuildinfo +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/openApiComponents/index.d.ts.map +0 -1
- package/dist/openApiComponents/index.js.map +0 -1
- package/dist/openApiConstants/index.d.ts.map +0 -1
- package/dist/openApiConstants/index.js.map +0 -1
- package/dist/openApiConstants/openApiConstants.core.d.ts.map +0 -1
- package/dist/openApiConstants/openApiConstants.core.js.map +0 -1
- package/dist/openApiDocument/index.d.ts.map +0 -1
- package/dist/openApiDocument/index.js.map +0 -1
- package/dist/openApiDocument/openApiDocument.builder.d.ts.map +0 -1
- package/dist/openApiDocument/openApiDocument.builder.js.map +0 -1
- package/dist/openApiErrors/index.d.ts.map +0 -1
- package/dist/openApiErrors/index.js.map +0 -1
- package/dist/openApiErrors/openApiError.base.d.ts.map +0 -1
- package/dist/openApiErrors/openApiError.base.js.map +0 -1
- package/dist/openApiErrors/openApiError.core.d.ts.map +0 -1
- package/dist/openApiErrors/openApiError.core.js.map +0 -1
- package/dist/openApiErrors/openApiError.types.d.ts.map +0 -1
- package/dist/openApiErrors/openApiError.types.js.map +0 -1
- package/dist/openApiHttp/index.d.ts.map +0 -1
- package/dist/openApiHttp/index.js.map +0 -1
- package/dist/openApiHttp/openApiHttpAdapter.core.d.ts.map +0 -1
- package/dist/openApiHttp/openApiHttpAdapter.core.js.map +0 -1
- package/dist/openApiRegistry/index.d.ts.map +0 -1
- package/dist/openApiRegistry/index.js.map +0 -1
- package/dist/openApiRegistry/openApiRegistry.core.d.ts.map +0 -1
- package/dist/openApiRegistry/openApiRegistry.core.js.map +0 -1
- package/dist/openApiRegistry/openApiRegistry.type.d.ts.map +0 -1
- package/dist/openApiRegistry/openApiRegistry.type.js.map +0 -1
- package/dist/openApiRouting/index.d.ts.map +0 -1
- package/dist/openApiRouting/index.js.map +0 -1
- package/dist/openApiRouting/routeConverter.core.d.ts.map +0 -1
- package/dist/openApiRouting/routeConverter.core.js.map +0 -1
- package/dist/openApiRouting/routeMetadata.type.d.ts.map +0 -1
- package/dist/openApiRouting/routeMetadata.type.js.map +0 -1
- package/dist/openApiRouting/routeScanner.core.d.ts.map +0 -1
- package/dist/openApiRouting/routeScanner.core.js.map +0 -1
- package/dist/openApiSchema/index.d.ts.map +0 -1
- package/dist/openApiSchema/index.js.map +0 -1
- package/dist/openApiSchema/references.core.d.ts.map +0 -1
- package/dist/openApiSchema/references.core.js.map +0 -1
- package/dist/openApiSchema/schemaConverter.core.d.ts.map +0 -1
- package/dist/openApiSchema/schemaConverter.core.js.map +0 -1
- package/dist/openApiSchema/schemaRegistry.core.d.ts.map +0 -1
- package/dist/openApiSchema/schemaRegistry.core.js.map +0 -1
- package/dist/openApiSerialization/index.d.ts.map +0 -1
- package/dist/openApiSerialization/index.js.map +0 -1
- package/dist/openApiSerialization/openApiSerializer.core.d.ts.map +0 -1
- package/dist/openApiSerialization/openApiSerializer.core.js.map +0 -1
- package/dist/openApiTypes/index.d.ts.map +0 -1
- package/dist/openApiTypes/index.js.map +0 -1
- package/dist/openApiTypes/openApiComponent.type.d.ts.map +0 -1
- package/dist/openApiTypes/openApiComponent.type.js.map +0 -1
- package/dist/openApiTypes/openApiDocument.type.d.ts.map +0 -1
- package/dist/openApiTypes/openApiDocument.type.js.map +0 -1
- package/dist/openApiTypes/openApiPath.type.d.ts.map +0 -1
- package/dist/openApiTypes/openApiPath.type.js.map +0 -1
- package/dist/openApiTypes/openApiSchema.type.d.ts.map +0 -1
- package/dist/openApiTypes/openApiSchema.type.js.map +0 -1
- package/dist/openApiTypes/openApiSecurity.type.d.ts.map +0 -1
- package/dist/openApiTypes/openApiSecurity.type.js.map +0 -1
- package/dist/openApiTypes/openApiTypes.core.d.ts.map +0 -1
- package/dist/openApiTypes/openApiTypes.core.js.map +0 -1
- package/dist/openApiValidation/index.d.ts.map +0 -1
- package/dist/openApiValidation/index.js.map +0 -1
- package/dist/openApiValidation/openApiValidator.core.d.ts.map +0 -1
- package/dist/openApiValidation/openApiValidator.core.js.map +0 -1
|
@@ -1,61 +1,271 @@
|
|
|
1
|
-
import { OpenAPIDocumentBuilder } from "../openApiDocument/openApiDocument.builder.js";
|
|
2
1
|
import { OpenAPIRegistryImpl } from "../openApiRegistry/openApiRegistry.core.js";
|
|
3
2
|
import { OpenAPIRouteScannerImpl } from "../openApiRouting/routeScanner.core.js";
|
|
4
|
-
import { OpenAPIValidatorImpl } from "../openApiValidation/openApiValidator.core.js";
|
|
3
|
+
import { OpenAPIValidatorImpl, } from "../openApiValidation/openApiValidator.core.js";
|
|
4
|
+
import { SchemaRegistryImpl } from "../openApiSchema/schemaRegistry.core.js";
|
|
5
5
|
import { toOpenAPIJSON, toOpenAPIYAML, } from "../openApiSerialization/openApiSerializer.core.js";
|
|
6
|
+
import { DEFAULT_MEDIA_TYPE, DEFAULT_OPENAPI_VERSION, DOCUMENT_CACHE_TTL_MS, } from "../openApiConstants/openApiConstants.core.js";
|
|
7
|
+
import { renderOpenAPIUI, zudoLogo, } from "../openApiUi/openApiUi.core.js";
|
|
6
8
|
/**
|
|
7
|
-
* High-level OpenAPI manager that coordinates generation, validation, and
|
|
9
|
+
* High-level OpenAPI manager that coordinates generation, validation, and
|
|
10
|
+
* serving.
|
|
11
|
+
*
|
|
12
|
+
* Every mutation invalidates the cached document, so a route added after a
|
|
13
|
+
* first `generate()` appears in the next one, and regeneration is idempotent
|
|
14
|
+
* rather than throwing on the routes it registered last time.
|
|
8
15
|
*/
|
|
9
16
|
export class OpenAPIManager {
|
|
10
17
|
registry;
|
|
11
18
|
scanner;
|
|
12
19
|
validator;
|
|
20
|
+
schemas;
|
|
21
|
+
cacheTtlMs;
|
|
22
|
+
now;
|
|
23
|
+
logo;
|
|
24
|
+
/** True when `branding` was a caller-supplied logo rather than the default. */
|
|
25
|
+
customLogo;
|
|
13
26
|
cachedDocument;
|
|
14
|
-
|
|
15
|
-
|
|
27
|
+
cachedAt = 0;
|
|
28
|
+
/** Whether the cached document was produced by a validating generate. */
|
|
29
|
+
cachedValidated = false;
|
|
30
|
+
constructor(versionOrOptions = {}) {
|
|
31
|
+
const options = typeof versionOrOptions === "string"
|
|
32
|
+
? { version: versionOrOptions }
|
|
33
|
+
: versionOrOptions;
|
|
34
|
+
this.registry = new OpenAPIRegistryImpl(options.version ?? DEFAULT_OPENAPI_VERSION);
|
|
16
35
|
this.scanner = new OpenAPIRouteScannerImpl();
|
|
17
36
|
this.validator = new OpenAPIValidatorImpl();
|
|
37
|
+
this.schemas = new SchemaRegistryImpl({
|
|
38
|
+
version: options.version ?? DEFAULT_OPENAPI_VERSION,
|
|
39
|
+
onWarning: options.onSchemaWarning,
|
|
40
|
+
});
|
|
41
|
+
this.cacheTtlMs = options.cacheTtlMs ?? DOCUMENT_CACHE_TTL_MS;
|
|
42
|
+
this.now = options.now ?? (() => Date.now());
|
|
43
|
+
this.logo =
|
|
44
|
+
options.branding === false
|
|
45
|
+
? undefined
|
|
46
|
+
: options.branding === true || options.branding === undefined
|
|
47
|
+
? zudoLogo()
|
|
48
|
+
: options.branding;
|
|
49
|
+
this.customLogo =
|
|
50
|
+
typeof options.branding === "object" && options.branding !== null;
|
|
51
|
+
if (options.info)
|
|
52
|
+
this.registry.setInfo(options.info);
|
|
53
|
+
for (const server of options.servers ?? [])
|
|
54
|
+
this.registry.addServer(server);
|
|
55
|
+
for (const tag of options.tags ?? [])
|
|
56
|
+
this.registry.setTag(tag);
|
|
57
|
+
for (const requirement of options.security ?? []) {
|
|
58
|
+
this.registry.addSecurityRequirement(requirement);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** The document version this manager emits. */
|
|
62
|
+
get version() {
|
|
63
|
+
return this.registry.version;
|
|
64
|
+
}
|
|
65
|
+
/* ── Mutation ────────────────────────────────────────────────────────── */
|
|
66
|
+
/** Sets the document's `info` object. */
|
|
67
|
+
setInfo(info) {
|
|
68
|
+
this.registry.setInfo(info);
|
|
69
|
+
return this.invalidateCache();
|
|
70
|
+
}
|
|
71
|
+
addServer(server) {
|
|
72
|
+
this.registry.addServer(server);
|
|
73
|
+
return this.invalidateCache();
|
|
74
|
+
}
|
|
75
|
+
addTag(tag) {
|
|
76
|
+
this.registry.setTag(tag);
|
|
77
|
+
return this.invalidateCache();
|
|
18
78
|
}
|
|
19
|
-
|
|
20
|
-
|
|
79
|
+
addSecurityRequirement(requirement) {
|
|
80
|
+
this.registry.addSecurityRequirement(requirement);
|
|
81
|
+
return this.invalidateCache();
|
|
21
82
|
}
|
|
22
|
-
|
|
23
|
-
|
|
83
|
+
addSecurityScheme(name, scheme) {
|
|
84
|
+
this.registry.registerSecurityScheme(name, scheme);
|
|
85
|
+
return this.invalidateCache();
|
|
24
86
|
}
|
|
87
|
+
/** Registers a route. Duplicate method+path combinations are rejected. */
|
|
25
88
|
addRoute(route) {
|
|
26
89
|
this.scanner.addRoute(route);
|
|
90
|
+
return this.invalidateCache();
|
|
27
91
|
}
|
|
92
|
+
/** Registers a route, replacing any existing one for the same method+path. */
|
|
93
|
+
setRoute(route) {
|
|
94
|
+
this.scanner.setRoute(route);
|
|
95
|
+
return this.invalidateCache();
|
|
96
|
+
}
|
|
97
|
+
/** Removes a route. Returns whether one was removed. */
|
|
98
|
+
removeRoute(method, path) {
|
|
99
|
+
const removed = this.scanner.removeRoute(method, path);
|
|
100
|
+
if (removed)
|
|
101
|
+
this.invalidateCache();
|
|
102
|
+
return removed;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Registers a component schema.
|
|
106
|
+
*
|
|
107
|
+
* `schema` is converted from a `@zudojs/schema` schema; pass an already
|
|
108
|
+
* converted {@link OpenAPISchema} to {@link OpenAPIManager.addRawSchema}.
|
|
109
|
+
*/
|
|
28
110
|
addSchema(name, schema) {
|
|
111
|
+
const converted = this.schemas.register(name, schema);
|
|
112
|
+
this.registry.registerSchema(name, converted);
|
|
113
|
+
return this.invalidateCache();
|
|
114
|
+
}
|
|
115
|
+
/** Registers an already-converted OpenAPI schema. */
|
|
116
|
+
addRawSchema(name, schema) {
|
|
29
117
|
this.registry.registerSchema(name, schema);
|
|
118
|
+
return this.invalidateCache();
|
|
30
119
|
}
|
|
120
|
+
/** Conversion warnings, keyed by component name. */
|
|
121
|
+
schemaWarnings() {
|
|
122
|
+
return this.schemas.warnings();
|
|
123
|
+
}
|
|
124
|
+
/* ── Generation ──────────────────────────────────────────────────────── */
|
|
125
|
+
/**
|
|
126
|
+
* Builds the document from the registered routes and components.
|
|
127
|
+
*
|
|
128
|
+
* Safe to call repeatedly: routes are replaced rather than re-added.
|
|
129
|
+
*/
|
|
31
130
|
generate(validate = false) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
131
|
+
// The scanner is the source of truth for routes. Re-setting on top of
|
|
132
|
+
// the previous route set kept routes that had since been removed or
|
|
133
|
+
// hidden, so `removeRoute()` had no effect once a document had been
|
|
134
|
+
// generated.
|
|
135
|
+
this.registry.clearRoutes();
|
|
136
|
+
for (const route of this.scanner.scan()) {
|
|
137
|
+
this.registry.setRoute(route);
|
|
138
|
+
}
|
|
139
|
+
// Brand the document unless the caller supplied a logo or opted out.
|
|
140
|
+
const info = this.registry.getInfo();
|
|
141
|
+
if (this.logo && !info["x-logo"]) {
|
|
142
|
+
this.registry.setInfo({ ...info, "x-logo": this.logo });
|
|
35
143
|
}
|
|
36
144
|
const document = this.registry.generate();
|
|
37
|
-
if (validate)
|
|
145
|
+
if (validate)
|
|
38
146
|
this.validator.assertValid(document);
|
|
39
|
-
}
|
|
40
147
|
this.cachedDocument = document;
|
|
148
|
+
this.cachedAt = this.now();
|
|
149
|
+
this.cachedValidated = validate;
|
|
41
150
|
return document;
|
|
42
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* Returns the document, rebuilding it when the cache is stale, absent, or
|
|
154
|
+
* was produced without the validation this call asks for.
|
|
155
|
+
*/
|
|
43
156
|
getDocument(validate = false) {
|
|
44
|
-
if (this.cachedDocument) {
|
|
157
|
+
if (this.cachedDocument && this.isCacheFresh()) {
|
|
158
|
+
if (!validate || this.cachedValidated)
|
|
159
|
+
return this.cachedDocument;
|
|
160
|
+
// The cached document was never validated; validating it now is
|
|
161
|
+
// cheaper than rebuilding, and skipping the check silently is what a
|
|
162
|
+
// caller passing `true` is explicitly asking us not to do.
|
|
163
|
+
this.validator.assertValid(this.cachedDocument);
|
|
164
|
+
this.cachedValidated = true;
|
|
45
165
|
return this.cachedDocument;
|
|
46
166
|
}
|
|
47
167
|
return this.generate(validate);
|
|
48
168
|
}
|
|
49
|
-
|
|
169
|
+
isCacheFresh() {
|
|
170
|
+
if (this.cacheTtlMs <= 0)
|
|
171
|
+
return false;
|
|
172
|
+
return this.now() - this.cachedAt < this.cacheTtlMs;
|
|
173
|
+
}
|
|
174
|
+
/** Validates the current document without throwing. */
|
|
175
|
+
validate() {
|
|
176
|
+
return this.validator.validate(this.getDocument());
|
|
177
|
+
}
|
|
178
|
+
/** Drops the cached document. Called automatically by every mutation. */
|
|
179
|
+
invalidateCache() {
|
|
50
180
|
this.cachedDocument = undefined;
|
|
181
|
+
this.cachedAt = 0;
|
|
182
|
+
this.cachedValidated = false;
|
|
183
|
+
return this;
|
|
184
|
+
}
|
|
185
|
+
/** Drops every registered route, component and the cached document. */
|
|
186
|
+
reset() {
|
|
51
187
|
this.scanner.clear();
|
|
52
188
|
this.registry.clear();
|
|
189
|
+
this.schemas.clear();
|
|
190
|
+
return this.invalidateCache();
|
|
53
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* @deprecated Use {@link OpenAPIManager.reset} — the old name cleared all
|
|
194
|
+
* registered routes, which is not what "invalidate" suggests.
|
|
195
|
+
*/
|
|
196
|
+
invalidate() {
|
|
197
|
+
this.reset();
|
|
198
|
+
}
|
|
199
|
+
/* ── Serialization ───────────────────────────────────────────────────── */
|
|
54
200
|
toJSON(validate = false) {
|
|
55
201
|
return toOpenAPIJSON(this.getDocument(validate));
|
|
56
202
|
}
|
|
57
203
|
toYAML(validate = false) {
|
|
58
204
|
return toOpenAPIYAML(this.getDocument(validate));
|
|
59
205
|
}
|
|
206
|
+
/* ── Serving ─────────────────────────────────────────────────────────── */
|
|
207
|
+
/**
|
|
208
|
+
* Builds an HTTP response carrying the document.
|
|
209
|
+
*
|
|
210
|
+
* Framework-agnostic on purpose: `{ status, headers, body }` is what every
|
|
211
|
+
* adapter in this monorepo can turn into its own response type.
|
|
212
|
+
*/
|
|
213
|
+
toResponse(options) {
|
|
214
|
+
const format = options?.format ?? "json";
|
|
215
|
+
const body = format === "yaml"
|
|
216
|
+
? this.toYAML(options?.validate ?? false)
|
|
217
|
+
: this.toJSON(options?.validate ?? false);
|
|
218
|
+
return Object.freeze({
|
|
219
|
+
status: 200,
|
|
220
|
+
headers: Object.freeze({
|
|
221
|
+
"content-type": format === "yaml"
|
|
222
|
+
? "application/yaml; charset=utf-8"
|
|
223
|
+
: `${DEFAULT_MEDIA_TYPE}; charset=utf-8`,
|
|
224
|
+
"cache-control": options?.cacheControl ?? "public, max-age=300",
|
|
225
|
+
}),
|
|
226
|
+
body,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Builds an HTTP response carrying a branded documentation page (Swagger UI
|
|
231
|
+
* by default, ReDoc on request) that loads the specification from
|
|
232
|
+
* `options.specUrl`. Pair it with {@link OpenAPIManager.toResponse}:
|
|
233
|
+
*
|
|
234
|
+
* ```ts
|
|
235
|
+
* app.get("/openapi.json", () => manager.toResponse());
|
|
236
|
+
* app.get("/docs", () => manager.toUIResponse({ specUrl: "/openapi.json" }));
|
|
237
|
+
* ```
|
|
238
|
+
*/
|
|
239
|
+
toUIResponse(options) {
|
|
240
|
+
const info = this.registry.getInfo();
|
|
241
|
+
const body = renderOpenAPIUI({
|
|
242
|
+
title: info.title && info.title !== "API"
|
|
243
|
+
? `${info.title} · API reference`
|
|
244
|
+
: undefined,
|
|
245
|
+
...options,
|
|
246
|
+
// Precedence: an explicit page logo, then the manager's `branding`
|
|
247
|
+
// (a custom logo is used as-is; `false` renders none), then the
|
|
248
|
+
// page's own default wordmark.
|
|
249
|
+
logo: options.logo !== undefined
|
|
250
|
+
? options.logo
|
|
251
|
+
: this.logo === undefined
|
|
252
|
+
? false
|
|
253
|
+
: this.customLogo
|
|
254
|
+
? this.logo
|
|
255
|
+
: undefined,
|
|
256
|
+
});
|
|
257
|
+
return Object.freeze({
|
|
258
|
+
status: 200,
|
|
259
|
+
headers: Object.freeze({
|
|
260
|
+
"content-type": "text/html; charset=utf-8",
|
|
261
|
+
"cache-control": "public, max-age=300",
|
|
262
|
+
}),
|
|
263
|
+
body,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/** Creates an OpenAPI manager. */
|
|
268
|
+
export function createOpenAPIManager(options) {
|
|
269
|
+
return new OpenAPIManager(options ?? {});
|
|
60
270
|
}
|
|
61
271
|
//# sourceMappingURL=openApiHttpAdapter.core.js.map
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import type { OpenAPIDocument, OpenAPIParameter, OpenAPIRequestBody, OpenAPISchema, OpenAPISecurityScheme, OpenAPITag, OpenAPIReference } from "../openApiTypes/openApiTypes.core.js";
|
|
1
|
+
import type { OpenAPIDocument, OpenAPIExample, OpenAPIHeader, OpenAPIInfo, OpenAPILink, OpenAPIParameter, OpenAPIPathItem, OpenAPIRequestBody, OpenAPIResponse, OpenAPISchema, OpenAPISecurityRequirement, OpenAPISecurityScheme, OpenAPIServer, OpenAPITag, OpenAPIReference } from "../openApiTypes/openApiTypes.core.js";
|
|
2
2
|
import type { OpenAPIRoute, OpenAPIRegistry } from "./openApiRegistry.type.js";
|
|
3
|
+
import type { ComponentSection } from "../openApiSchema/references.core.js";
|
|
3
4
|
/** Default OpenAPI registry implementation. */
|
|
4
5
|
export declare class OpenAPIRegistryImpl implements OpenAPIRegistry {
|
|
5
6
|
readonly version: string;
|
|
7
|
+
private info;
|
|
8
|
+
private readonly servers;
|
|
9
|
+
private readonly security;
|
|
6
10
|
private readonly routes;
|
|
7
11
|
private readonly schemas;
|
|
8
12
|
private readonly responses;
|
|
@@ -15,16 +19,54 @@ export declare class OpenAPIRegistryImpl implements OpenAPIRegistry {
|
|
|
15
19
|
private readonly links;
|
|
16
20
|
private readonly callbacks;
|
|
17
21
|
constructor(version?: string);
|
|
22
|
+
/** Sets the document's `info` object. */
|
|
23
|
+
setInfo(info: OpenAPIInfo): void;
|
|
24
|
+
/** The document's current `info` object. */
|
|
25
|
+
getInfo(): OpenAPIInfo;
|
|
26
|
+
addServer(server: OpenAPIServer): void;
|
|
27
|
+
addSecurityRequirement(requirement: OpenAPISecurityRequirement): void;
|
|
28
|
+
private static routeKey;
|
|
18
29
|
registerRoute(route: OpenAPIRoute): void;
|
|
30
|
+
/**
|
|
31
|
+
* Registers a route, replacing any existing one.
|
|
32
|
+
*
|
|
33
|
+
* This is what makes regeneration idempotent: re-registering the same routes
|
|
34
|
+
* into a registry that already holds them used to throw, so calling
|
|
35
|
+
* `generate()` twice failed.
|
|
36
|
+
*/
|
|
37
|
+
setRoute(route: OpenAPIRoute): void;
|
|
38
|
+
/** Removes a route. Returns whether one was registered. */
|
|
39
|
+
removeRoute(method: string, path: string): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Drops every registered route while keeping components, servers, tags
|
|
42
|
+
* and security. `OpenAPIManager.generate()` rebuilds the route set from
|
|
43
|
+
* its scanner on each call; without this a route removed from the
|
|
44
|
+
* scanner lived on in the registry and in every later document.
|
|
45
|
+
*/
|
|
46
|
+
clearRoutes(): void;
|
|
47
|
+
private static register;
|
|
19
48
|
registerSchema(name: string, schema: OpenAPISchema): void;
|
|
20
|
-
registerResponse(name: string, response:
|
|
49
|
+
registerResponse(name: string, response: OpenAPIResponse): void;
|
|
21
50
|
registerParameter(name: string, parameter: OpenAPIParameter): void;
|
|
22
51
|
registerRequestBody(name: string, body: OpenAPIRequestBody): void;
|
|
23
|
-
registerHeader(name: string, header:
|
|
24
|
-
registerExample(name: string, example:
|
|
52
|
+
registerHeader(name: string, header: OpenAPIHeader): void;
|
|
53
|
+
registerExample(name: string, example: OpenAPIExample): void;
|
|
25
54
|
registerSecurityScheme(name: string, scheme: OpenAPISecurityScheme): void;
|
|
55
|
+
registerLink(name: string, link: OpenAPILink): void;
|
|
56
|
+
registerCallback(name: string, callback: Readonly<Record<string, OpenAPIPathItem>>): void;
|
|
57
|
+
/**
|
|
58
|
+
* Registers a tag.
|
|
59
|
+
*
|
|
60
|
+
* Re-registering a tag name conflicts, like every other component: quietly
|
|
61
|
+
* keeping the first definition discarded corrected descriptions without a
|
|
62
|
+
* word.
|
|
63
|
+
*/
|
|
26
64
|
registerTag(tag: OpenAPITag): void;
|
|
27
|
-
|
|
65
|
+
/** Registers a tag, replacing any existing one with the same name. */
|
|
66
|
+
setTag(tag: OpenAPITag): void;
|
|
67
|
+
ref(section: ComponentSection, name: string): OpenAPIReference;
|
|
68
|
+
hasComponent(section: ComponentSection, name: string): boolean;
|
|
69
|
+
private componentMap;
|
|
28
70
|
generate(): OpenAPIDocument;
|
|
29
71
|
clear(): void;
|
|
30
72
|
}
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { createComponentReference } from "../openApiSchema/references.core.js";
|
|
2
|
+
import { DEFAULT_OPENAPI_VERSION, SUPPORTED_OPENAPI_VERSIONS, } from "../openApiConstants/openApiConstants.core.js";
|
|
3
|
+
import { OpenAPIComponentConflictError, OpenAPIOperationError, OpenAPIVersionError, } from "../openApiErrors/openApiError.types.js";
|
|
4
|
+
/** Placeholder used until {@link OpenAPIRegistryImpl.setInfo} is called. */
|
|
5
|
+
const DEFAULT_INFO = { title: "API", version: "0.0.0" };
|
|
3
6
|
/** Default OpenAPI registry implementation. */
|
|
4
7
|
export class OpenAPIRegistryImpl {
|
|
5
8
|
version;
|
|
9
|
+
info = DEFAULT_INFO;
|
|
10
|
+
servers = [];
|
|
11
|
+
security = [];
|
|
6
12
|
routes = new Map();
|
|
7
13
|
schemas = new Map();
|
|
8
14
|
responses = new Map();
|
|
@@ -15,14 +21,55 @@ export class OpenAPIRegistryImpl {
|
|
|
15
21
|
links = new Map();
|
|
16
22
|
callbacks = new Map();
|
|
17
23
|
constructor(version = DEFAULT_OPENAPI_VERSION) {
|
|
24
|
+
if (!SUPPORTED_OPENAPI_VERSIONS.includes(version)) {
|
|
25
|
+
// Fail where the version is chosen, not later inside generate().
|
|
26
|
+
throw new OpenAPIVersionError(version, SUPPORTED_OPENAPI_VERSIONS);
|
|
27
|
+
}
|
|
18
28
|
this.version = version;
|
|
19
29
|
}
|
|
30
|
+
/** Sets the document's `info` object. */
|
|
31
|
+
setInfo(info) {
|
|
32
|
+
this.info = { ...info };
|
|
33
|
+
}
|
|
34
|
+
/** The document's current `info` object. */
|
|
35
|
+
getInfo() {
|
|
36
|
+
return { ...this.info };
|
|
37
|
+
}
|
|
38
|
+
addServer(server) {
|
|
39
|
+
this.servers.push({ ...server });
|
|
40
|
+
}
|
|
41
|
+
addSecurityRequirement(requirement) {
|
|
42
|
+
this.security.push({ ...requirement });
|
|
43
|
+
}
|
|
44
|
+
static routeKey(route) {
|
|
45
|
+
return `${route.method.toLowerCase()}:${route.path}`;
|
|
46
|
+
}
|
|
20
47
|
registerRoute(route) {
|
|
21
|
-
const key =
|
|
48
|
+
const key = OpenAPIRegistryImpl.routeKey(route);
|
|
22
49
|
const existing = this.routes.get(key);
|
|
23
|
-
if (existing)
|
|
24
|
-
throw new OpenAPIOperationError(`Duplicate operation for ${route.method.toUpperCase()} ${route.path}
|
|
25
|
-
|
|
50
|
+
if (existing) {
|
|
51
|
+
throw new OpenAPIOperationError(`Duplicate operation for ${route.method.toUpperCase()} ${route.path}` +
|
|
52
|
+
(existing.operation.operationId
|
|
53
|
+
? ` (already registered as "${existing.operation.operationId}")`
|
|
54
|
+
: ""), {
|
|
55
|
+
metadata: {
|
|
56
|
+
method: route.method,
|
|
57
|
+
path: route.path,
|
|
58
|
+
operationId: existing.operation.operationId,
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
this.setRoute(route);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Registers a route, replacing any existing one.
|
|
66
|
+
*
|
|
67
|
+
* This is what makes regeneration idempotent: re-registering the same routes
|
|
68
|
+
* into a registry that already holds them used to throw, so calling
|
|
69
|
+
* `generate()` twice failed.
|
|
70
|
+
*/
|
|
71
|
+
setRoute(route) {
|
|
72
|
+
this.routes.set(OpenAPIRegistryImpl.routeKey(route), {
|
|
26
73
|
...route,
|
|
27
74
|
operation: Object.freeze({
|
|
28
75
|
...route.operation,
|
|
@@ -30,72 +77,134 @@ export class OpenAPIRegistryImpl {
|
|
|
30
77
|
}),
|
|
31
78
|
});
|
|
32
79
|
}
|
|
80
|
+
/** Removes a route. Returns whether one was registered. */
|
|
81
|
+
removeRoute(method, path) {
|
|
82
|
+
return this.routes.delete(OpenAPIRegistryImpl.routeKey({ method, path }));
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Drops every registered route while keeping components, servers, tags
|
|
86
|
+
* and security. `OpenAPIManager.generate()` rebuilds the route set from
|
|
87
|
+
* its scanner on each call; without this a route removed from the
|
|
88
|
+
* scanner lived on in the registry and in every later document.
|
|
89
|
+
*/
|
|
90
|
+
clearRoutes() {
|
|
91
|
+
this.routes.clear();
|
|
92
|
+
}
|
|
93
|
+
static register(map, section, name, value) {
|
|
94
|
+
if (map.has(name)) {
|
|
95
|
+
throw new OpenAPIComponentConflictError(section, name);
|
|
96
|
+
}
|
|
97
|
+
map.set(name, Object.freeze(value));
|
|
98
|
+
}
|
|
33
99
|
registerSchema(name, schema) {
|
|
34
|
-
|
|
35
|
-
throw new OpenAPIComponentConflictError(`schemas/${name}`);
|
|
36
|
-
this.schemas.set(name, Object.freeze({ ...schema }));
|
|
100
|
+
OpenAPIRegistryImpl.register(this.schemas, "schemas", name, { ...schema });
|
|
37
101
|
}
|
|
38
102
|
registerResponse(name, response) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
103
|
+
OpenAPIRegistryImpl.register(this.responses, "responses", name, {
|
|
104
|
+
...response,
|
|
105
|
+
});
|
|
42
106
|
}
|
|
43
107
|
registerParameter(name, parameter) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
108
|
+
OpenAPIRegistryImpl.register(this.parameters, "parameters", name, {
|
|
109
|
+
...parameter,
|
|
110
|
+
});
|
|
47
111
|
}
|
|
48
112
|
registerRequestBody(name, body) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
113
|
+
OpenAPIRegistryImpl.register(this.requestBodies, "requestBodies", name, {
|
|
114
|
+
...body,
|
|
115
|
+
});
|
|
52
116
|
}
|
|
53
117
|
registerHeader(name, header) {
|
|
54
|
-
|
|
55
|
-
throw new OpenAPIComponentConflictError(`headers/${name}`);
|
|
56
|
-
this.headers.set(name, Object.freeze({ ...header }));
|
|
118
|
+
OpenAPIRegistryImpl.register(this.headers, "headers", name, { ...header });
|
|
57
119
|
}
|
|
58
120
|
registerExample(name, example) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
121
|
+
OpenAPIRegistryImpl.register(this.examples, "examples", name, {
|
|
122
|
+
...example,
|
|
123
|
+
});
|
|
62
124
|
}
|
|
63
125
|
registerSecurityScheme(name, scheme) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
126
|
+
OpenAPIRegistryImpl.register(this.securitySchemes, "securitySchemes", name, {
|
|
127
|
+
...scheme,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
registerLink(name, link) {
|
|
131
|
+
OpenAPIRegistryImpl.register(this.links, "links", name, { ...link });
|
|
132
|
+
}
|
|
133
|
+
registerCallback(name, callback) {
|
|
134
|
+
OpenAPIRegistryImpl.register(this.callbacks, "callbacks", name, {
|
|
135
|
+
...callback,
|
|
136
|
+
});
|
|
67
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* Registers a tag.
|
|
140
|
+
*
|
|
141
|
+
* Re-registering a tag name conflicts, like every other component: quietly
|
|
142
|
+
* keeping the first definition discarded corrected descriptions without a
|
|
143
|
+
* word.
|
|
144
|
+
*/
|
|
68
145
|
registerTag(tag) {
|
|
69
|
-
if (
|
|
70
|
-
|
|
146
|
+
if (this.tags.has(tag.name)) {
|
|
147
|
+
throw new OpenAPIComponentConflictError("tags", tag.name);
|
|
148
|
+
}
|
|
149
|
+
this.tags.set(tag.name, Object.freeze({ ...tag }));
|
|
150
|
+
}
|
|
151
|
+
/** Registers a tag, replacing any existing one with the same name. */
|
|
152
|
+
setTag(tag) {
|
|
153
|
+
this.tags.set(tag.name, Object.freeze({ ...tag }));
|
|
71
154
|
}
|
|
72
155
|
ref(section, name) {
|
|
73
|
-
return
|
|
156
|
+
return createComponentReference(section, name);
|
|
157
|
+
}
|
|
158
|
+
hasComponent(section, name) {
|
|
159
|
+
return this.componentMap(section)?.has(name) ?? false;
|
|
160
|
+
}
|
|
161
|
+
componentMap(section) {
|
|
162
|
+
switch (section) {
|
|
163
|
+
case "schemas":
|
|
164
|
+
return this.schemas;
|
|
165
|
+
case "responses":
|
|
166
|
+
return this.responses;
|
|
167
|
+
case "parameters":
|
|
168
|
+
return this.parameters;
|
|
169
|
+
case "requestBodies":
|
|
170
|
+
return this.requestBodies;
|
|
171
|
+
case "headers":
|
|
172
|
+
return this.headers;
|
|
173
|
+
case "examples":
|
|
174
|
+
return this.examples;
|
|
175
|
+
case "securitySchemes":
|
|
176
|
+
return this.securitySchemes;
|
|
177
|
+
case "links":
|
|
178
|
+
return this.links;
|
|
179
|
+
case "callbacks":
|
|
180
|
+
return this.callbacks;
|
|
181
|
+
default:
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
74
184
|
}
|
|
75
185
|
generate() {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
186
|
+
// A `Map` rather than an object literal: a route registered at the path
|
|
187
|
+
// `__proto__` assigned to a literal sets the object's prototype instead
|
|
188
|
+
// of adding an entry, so the path disappears from the document with no
|
|
189
|
+
// error raised anywhere. `Object.fromEntries` defines own properties and
|
|
190
|
+
// has no such hole.
|
|
191
|
+
const paths = new Map();
|
|
192
|
+
for (const route of this.routes.values()) {
|
|
80
193
|
const operation = Object.freeze({
|
|
81
194
|
...route.operation,
|
|
82
195
|
responses: Object.freeze({ ...route.operation.responses }),
|
|
83
196
|
});
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
...(route.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
...(route.operation.parameters?.length
|
|
96
|
-
? { parameters: Object.freeze([...route.operation.parameters]) }
|
|
97
|
-
: {}),
|
|
98
|
-
});
|
|
197
|
+
// Operations keep their own parameters. Hoisting them to the path item
|
|
198
|
+
// makes them apply to every method on that path, so two methods with
|
|
199
|
+
// different parameters overwrote one another.
|
|
200
|
+
//
|
|
201
|
+
// The method is lower-cased here as well as in the cache key: a path
|
|
202
|
+
// item field is defined in lower case, and `GET` would emit a field no
|
|
203
|
+
// consumer recognises while still colliding on the key.
|
|
204
|
+
paths.set(route.path, Object.freeze({
|
|
205
|
+
...(paths.get(route.path) ?? {}),
|
|
206
|
+
[route.method.toLowerCase()]: operation,
|
|
207
|
+
}));
|
|
99
208
|
}
|
|
100
209
|
const components = {};
|
|
101
210
|
const componentMaps = [
|
|
@@ -113,24 +222,28 @@ export class OpenAPIRegistryImpl {
|
|
|
113
222
|
if (map.size > 0)
|
|
114
223
|
components[key] = Object.freeze(Object.fromEntries(map));
|
|
115
224
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
225
|
+
return Object.freeze({
|
|
226
|
+
openapi: this.version,
|
|
227
|
+
info: Object.freeze({ ...this.info }),
|
|
228
|
+
...(this.servers.length > 0
|
|
229
|
+
? { servers: Object.freeze([...this.servers]) }
|
|
230
|
+
: {}),
|
|
231
|
+
paths: Object.freeze(Object.fromEntries(paths)),
|
|
232
|
+
...(Object.keys(components).length > 0
|
|
233
|
+
? { components: Object.freeze(components) }
|
|
234
|
+
: {}),
|
|
235
|
+
...(this.security.length > 0
|
|
236
|
+
? { security: Object.freeze([...this.security]) }
|
|
237
|
+
: {}),
|
|
238
|
+
...(this.tags.size > 0
|
|
239
|
+
? { tags: Object.freeze([...this.tags.values()]) }
|
|
240
|
+
: {}),
|
|
241
|
+
});
|
|
132
242
|
}
|
|
133
243
|
clear() {
|
|
244
|
+
this.info = DEFAULT_INFO;
|
|
245
|
+
this.servers.length = 0;
|
|
246
|
+
this.security.length = 0;
|
|
134
247
|
for (const map of [
|
|
135
248
|
this.routes,
|
|
136
249
|
this.schemas,
|
|
@@ -143,8 +256,9 @@ export class OpenAPIRegistryImpl {
|
|
|
143
256
|
this.tags,
|
|
144
257
|
this.links,
|
|
145
258
|
this.callbacks,
|
|
146
|
-
])
|
|
259
|
+
]) {
|
|
147
260
|
map.clear();
|
|
261
|
+
}
|
|
148
262
|
}
|
|
149
263
|
}
|
|
150
264
|
//# sourceMappingURL=openApiRegistry.core.js.map
|