@zudojs/openapi 0.1.0 → 1.1.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 +327 -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 +127 -25
- package/dist/openApiHttp/openApiHttpAdapter.core.js +212 -16
- package/dist/openApiRegistry/openApiRegistry.core.d.ts +38 -5
- package/dist/openApiRegistry/openApiRegistry.core.js +167 -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 +51 -3
- package/dist/openApiSchema/schemaConverter.core.js +475 -118
- 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 +135 -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 +143 -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 +274 -66
- package/package.json +25 -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,27 +1,62 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Specific OpenAPI error subclasses.
|
|
3
|
+
*
|
|
4
|
+
* Each subclass supplies a default `code`, `statusCode` and `expose`, and each
|
|
5
|
+
* lets the caller override them — the options are spread *after* the defaults,
|
|
6
|
+
* so a value passed in is the value used.
|
|
3
7
|
*/
|
|
4
8
|
import { OpenAPIError } from "./openApiError.base.js";
|
|
5
|
-
/**
|
|
9
|
+
/** Renders an issue path the way a reader would write it. */
|
|
10
|
+
export function formatIssuePath(path) {
|
|
11
|
+
return path
|
|
12
|
+
.map((segment) => (typeof segment === "number" ? `[${segment}]` : segment))
|
|
13
|
+
.join(".")
|
|
14
|
+
.replace(/\.\[/g, "[");
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Error thrown when OpenAPI validation fails.
|
|
18
|
+
*
|
|
19
|
+
* The issues are kept on the error. Discarding them left callers with the
|
|
20
|
+
* message "OpenAPI validation failed." and no way to learn what was wrong
|
|
21
|
+
* short of re-running the validator.
|
|
22
|
+
*/
|
|
6
23
|
export class OpenAPIValidationError extends OpenAPIError {
|
|
7
|
-
|
|
24
|
+
issues;
|
|
25
|
+
constructor(message, issues = [], options = {}) {
|
|
8
26
|
super(message, {
|
|
27
|
+
code: "OPENAPI_VALIDATION",
|
|
28
|
+
statusCode: 500,
|
|
29
|
+
expose: false,
|
|
9
30
|
...options,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
31
|
+
metadata: {
|
|
32
|
+
issueCount: issues.length,
|
|
33
|
+
issues: issues.map((issue) => ({
|
|
34
|
+
path: formatIssuePath(issue.path),
|
|
35
|
+
message: issue.message,
|
|
36
|
+
severity: issue.severity,
|
|
37
|
+
})),
|
|
38
|
+
...(options.metadata ?? {}),
|
|
39
|
+
},
|
|
13
40
|
});
|
|
14
41
|
this.name = "OpenAPIValidationError";
|
|
42
|
+
this.issues = Object.freeze([...issues]);
|
|
43
|
+
}
|
|
44
|
+
/** A multi-line summary suitable for a log line or a CLI. */
|
|
45
|
+
format() {
|
|
46
|
+
if (this.issues.length === 0)
|
|
47
|
+
return this.message;
|
|
48
|
+
const lines = this.issues.map((issue) => ` ${issue.severity}: ${formatIssuePath(issue.path)} — ${issue.message}`);
|
|
49
|
+
return [this.message, ...lines].join("\n");
|
|
15
50
|
}
|
|
16
51
|
}
|
|
17
52
|
/** Error thrown when the OpenAPI document is invalid. */
|
|
18
53
|
export class OpenAPIDocumentError extends OpenAPIError {
|
|
19
54
|
constructor(message, options = {}) {
|
|
20
55
|
super(message, {
|
|
21
|
-
|
|
22
|
-
code: options.code ?? "OPENAPI_DOCUMENT",
|
|
56
|
+
code: "OPENAPI_DOCUMENT",
|
|
23
57
|
statusCode: 500,
|
|
24
58
|
expose: false,
|
|
59
|
+
...options,
|
|
25
60
|
});
|
|
26
61
|
this.name = "OpenAPIDocumentError";
|
|
27
62
|
}
|
|
@@ -30,46 +65,55 @@ export class OpenAPIDocumentError extends OpenAPIError {
|
|
|
30
65
|
export class OpenAPIComponentError extends OpenAPIError {
|
|
31
66
|
constructor(message, options = {}) {
|
|
32
67
|
super(message, {
|
|
33
|
-
|
|
34
|
-
code: options.code ?? "OPENAPI_COMPONENT",
|
|
68
|
+
code: "OPENAPI_COMPONENT",
|
|
35
69
|
statusCode: 500,
|
|
36
70
|
expose: false,
|
|
71
|
+
...options,
|
|
37
72
|
});
|
|
38
73
|
this.name = "OpenAPIComponentError";
|
|
39
74
|
}
|
|
40
75
|
}
|
|
41
|
-
/** Error thrown when
|
|
76
|
+
/** Error thrown when a component name is already registered. */
|
|
42
77
|
export class OpenAPIComponentConflictError extends OpenAPIError {
|
|
43
|
-
|
|
44
|
-
|
|
78
|
+
section;
|
|
79
|
+
componentName;
|
|
80
|
+
constructor(section, componentName, options = {}) {
|
|
81
|
+
super(`A component named "${componentName}" is already registered under "${section}". ` +
|
|
82
|
+
`Component names must be unique within a section.`, {
|
|
83
|
+
code: "OPENAPI_COMPONENT_CONFLICT",
|
|
84
|
+
statusCode: 500,
|
|
85
|
+
expose: false,
|
|
45
86
|
...options,
|
|
46
|
-
|
|
47
|
-
statusCode: 409,
|
|
48
|
-
expose: true,
|
|
87
|
+
metadata: { section, componentName, ...(options.metadata ?? {}) },
|
|
49
88
|
});
|
|
50
89
|
this.name = "OpenAPIComponentConflictError";
|
|
90
|
+
this.section = section;
|
|
91
|
+
this.componentName = componentName;
|
|
51
92
|
}
|
|
52
93
|
}
|
|
53
|
-
/** Error thrown when an OpenAPI reference is invalid. */
|
|
94
|
+
/** Error thrown when an OpenAPI reference is invalid or cannot be resolved. */
|
|
54
95
|
export class OpenAPIReferenceError extends OpenAPIError {
|
|
55
|
-
|
|
56
|
-
|
|
96
|
+
reference;
|
|
97
|
+
constructor(reference, options = {}) {
|
|
98
|
+
super(`Unresolvable OpenAPI reference: "${reference}".`, {
|
|
99
|
+
code: "OPENAPI_REFERENCE",
|
|
100
|
+
statusCode: 500,
|
|
101
|
+
expose: false,
|
|
57
102
|
...options,
|
|
58
|
-
|
|
59
|
-
statusCode: 400,
|
|
60
|
-
expose: true,
|
|
103
|
+
metadata: { reference, ...(options.metadata ?? {}) },
|
|
61
104
|
});
|
|
62
105
|
this.name = "OpenAPIReferenceError";
|
|
106
|
+
this.reference = reference;
|
|
63
107
|
}
|
|
64
108
|
}
|
|
65
|
-
/** Error thrown when an OpenAPI route
|
|
109
|
+
/** Error thrown when an OpenAPI route cannot be converted. */
|
|
66
110
|
export class OpenAPIRouteError extends OpenAPIError {
|
|
67
111
|
constructor(message, options = {}) {
|
|
68
112
|
super(message, {
|
|
69
|
-
|
|
70
|
-
code: options.code ?? "OPENAPI_ROUTE",
|
|
113
|
+
code: "OPENAPI_ROUTE",
|
|
71
114
|
statusCode: 500,
|
|
72
115
|
expose: false,
|
|
116
|
+
...options,
|
|
73
117
|
});
|
|
74
118
|
this.name = "OpenAPIRouteError";
|
|
75
119
|
}
|
|
@@ -78,10 +122,10 @@ export class OpenAPIRouteError extends OpenAPIError {
|
|
|
78
122
|
export class OpenAPISchemaError extends OpenAPIError {
|
|
79
123
|
constructor(message, options = {}) {
|
|
80
124
|
super(message, {
|
|
81
|
-
|
|
82
|
-
code: options.code ?? "OPENAPI_SCHEMA",
|
|
125
|
+
code: "OPENAPI_SCHEMA",
|
|
83
126
|
statusCode: 500,
|
|
84
127
|
expose: false,
|
|
128
|
+
...options,
|
|
85
129
|
});
|
|
86
130
|
this.name = "OpenAPISchemaError";
|
|
87
131
|
}
|
|
@@ -90,34 +134,38 @@ export class OpenAPISchemaError extends OpenAPIError {
|
|
|
90
134
|
export class OpenAPISerializationError extends OpenAPIError {
|
|
91
135
|
constructor(message, options = {}) {
|
|
92
136
|
super(message, {
|
|
93
|
-
|
|
94
|
-
code: options.code ?? "OPENAPI_SERIALIZATION",
|
|
137
|
+
code: "OPENAPI_SERIALIZATION",
|
|
95
138
|
statusCode: 500,
|
|
96
139
|
expose: false,
|
|
140
|
+
...options,
|
|
97
141
|
});
|
|
98
142
|
this.name = "OpenAPISerializationError";
|
|
99
143
|
}
|
|
100
144
|
}
|
|
101
145
|
/** Error thrown when the OpenAPI version is unsupported. */
|
|
102
146
|
export class OpenAPIVersionError extends OpenAPIError {
|
|
103
|
-
|
|
104
|
-
|
|
147
|
+
version;
|
|
148
|
+
constructor(version, supported = [], options = {}) {
|
|
149
|
+
super(`Unsupported OpenAPI version "${version}".` +
|
|
150
|
+
(supported.length > 0 ? ` Supported: ${supported.join(", ")}.` : ""), {
|
|
151
|
+
code: "OPENAPI_VERSION",
|
|
152
|
+
statusCode: 500,
|
|
153
|
+
expose: false,
|
|
105
154
|
...options,
|
|
106
|
-
|
|
107
|
-
statusCode: 400,
|
|
108
|
-
expose: true,
|
|
155
|
+
metadata: { version, supported, ...(options.metadata ?? {}) },
|
|
109
156
|
});
|
|
110
157
|
this.name = "OpenAPIVersionError";
|
|
158
|
+
this.version = version;
|
|
111
159
|
}
|
|
112
160
|
}
|
|
113
161
|
/** Error thrown when an OpenAPI operation is invalid. */
|
|
114
162
|
export class OpenAPIOperationError extends OpenAPIError {
|
|
115
163
|
constructor(message, options = {}) {
|
|
116
164
|
super(message, {
|
|
117
|
-
|
|
118
|
-
code: options.code ?? "OPENAPI_OPERATION",
|
|
165
|
+
code: "OPENAPI_OPERATION",
|
|
119
166
|
statusCode: 500,
|
|
120
167
|
expose: false,
|
|
168
|
+
...options,
|
|
121
169
|
});
|
|
122
170
|
this.name = "OpenAPIOperationError";
|
|
123
171
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @zudojs/openapi/openApiHttp
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Document coordination and HTTP serving.
|
|
5
5
|
*/
|
|
6
|
-
export { OpenAPIManager } from "./openApiHttpAdapter.core.js";
|
|
6
|
+
export { OpenAPIManager, createOpenAPIManager, type OpenAPIManagerOptions, type OpenAPIDocumentResponse, type OpenAPIUIResponse, } from "./openApiHttpAdapter.core.js";
|
|
7
7
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @zudojs/openapi/openApiHttp
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Document coordination and HTTP serving.
|
|
5
5
|
*/
|
|
6
|
-
export { OpenAPIManager } from "./openApiHttpAdapter.core.js";
|
|
6
|
+
export { OpenAPIManager, createOpenAPIManager, } from "./openApiHttpAdapter.core.js";
|
|
7
7
|
//# sourceMappingURL=index.js.map
|
|
@@ -1,38 +1,140 @@
|
|
|
1
|
-
import type { OpenAPIDocument } from "../openApiTypes/openApiTypes.core.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import type { OpenAPIDocument, OpenAPIInfo, OpenAPILogo, OpenAPISchema, OpenAPISecurityRequirement, OpenAPISecurityScheme, OpenAPIServer, OpenAPITag } from "../openApiTypes/openApiTypes.core.js";
|
|
2
|
+
import type { RouteInfo, OpenAPIHttpMethod } from "../openApiRouting/routeMetadata.type.js";
|
|
3
|
+
import { type OpenAPIValidationResult } from "../openApiValidation/openApiValidator.core.js";
|
|
4
|
+
import { type OpenAPIUIOptions } from "../openApiUi/openApiUi.core.js";
|
|
5
|
+
/** Options for {@link OpenAPIManager}. */
|
|
6
|
+
export interface OpenAPIManagerOptions {
|
|
7
|
+
/** Specification version to emit. Default: 3.1.0. */
|
|
8
|
+
readonly version?: string;
|
|
9
|
+
/** Document metadata. Without it the document is titled "API" at 0.0.0. */
|
|
10
|
+
readonly info?: OpenAPIInfo;
|
|
11
|
+
readonly servers?: readonly OpenAPIServer[];
|
|
12
|
+
readonly tags?: readonly OpenAPITag[];
|
|
13
|
+
readonly security?: readonly OpenAPISecurityRequirement[];
|
|
14
|
+
/**
|
|
15
|
+
* How long a generated document is reused before it is rebuilt, in ms.
|
|
16
|
+
* Default: five minutes. `0` disables caching. Mutating the manager
|
|
17
|
+
* invalidates the cache regardless.
|
|
18
|
+
*/
|
|
19
|
+
readonly cacheTtlMs?: number;
|
|
20
|
+
/** Reports what a schema conversion could not express. */
|
|
21
|
+
readonly onSchemaWarning?: (name: string, warnings: readonly string[]) => void;
|
|
22
|
+
/** Supplies the clock, for tests. */
|
|
23
|
+
readonly now?: () => number;
|
|
24
|
+
/**
|
|
25
|
+
* Logo written to `info["x-logo"]` so viewers such as ReDoc and Scalar
|
|
26
|
+
* show it, and used by {@link OpenAPIManager.toUIResponse}.
|
|
27
|
+
* Default: the Zudo mark. Pass `false` to emit no logo, or an
|
|
28
|
+
* {@link OpenAPILogo} to use your own.
|
|
29
|
+
*/
|
|
30
|
+
readonly branding?: boolean | OpenAPILogo;
|
|
31
|
+
}
|
|
32
|
+
/** An HTTP response carrying a rendered documentation page. */
|
|
33
|
+
export interface OpenAPIUIResponse {
|
|
34
|
+
readonly status: 200;
|
|
35
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
36
|
+
readonly body: string;
|
|
37
|
+
}
|
|
38
|
+
/** An HTTP response carrying the specification document. */
|
|
39
|
+
export interface OpenAPIDocumentResponse {
|
|
40
|
+
readonly status: 200;
|
|
41
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
42
|
+
readonly body: string;
|
|
43
|
+
}
|
|
4
44
|
/**
|
|
5
|
-
* High-level OpenAPI manager that coordinates generation, validation, and
|
|
45
|
+
* High-level OpenAPI manager that coordinates generation, validation, and
|
|
46
|
+
* serving.
|
|
47
|
+
*
|
|
48
|
+
* Every mutation invalidates the cached document, so a route added after a
|
|
49
|
+
* first `generate()` appears in the next one, and regeneration is idempotent
|
|
50
|
+
* rather than throwing on the routes it registered last time.
|
|
6
51
|
*/
|
|
7
52
|
export declare class OpenAPIManager {
|
|
8
53
|
private readonly registry;
|
|
9
54
|
private readonly scanner;
|
|
10
55
|
private readonly validator;
|
|
56
|
+
private readonly schemas;
|
|
57
|
+
private readonly cacheTtlMs;
|
|
58
|
+
private readonly now;
|
|
59
|
+
private readonly logo;
|
|
11
60
|
private cachedDocument?;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
61
|
+
private cachedAt;
|
|
62
|
+
/** Whether the cached document was produced by a validating generate. */
|
|
63
|
+
private cachedValidated;
|
|
64
|
+
constructor(versionOrOptions?: string | OpenAPIManagerOptions);
|
|
65
|
+
/** The document version this manager emits. */
|
|
66
|
+
get version(): string;
|
|
67
|
+
/** Sets the document's `info` object. */
|
|
68
|
+
setInfo(info: OpenAPIInfo): this;
|
|
69
|
+
addServer(server: OpenAPIServer): this;
|
|
70
|
+
addTag(tag: OpenAPITag): this;
|
|
71
|
+
addSecurityRequirement(requirement: OpenAPISecurityRequirement): this;
|
|
72
|
+
addSecurityScheme(name: string, scheme: OpenAPISecurityScheme): this;
|
|
73
|
+
/** Registers a route. Duplicate method+path combinations are rejected. */
|
|
74
|
+
addRoute(route: RouteInfo): this;
|
|
75
|
+
/** Registers a route, replacing any existing one for the same method+path. */
|
|
76
|
+
setRoute(route: RouteInfo): this;
|
|
77
|
+
/** Removes a route. Returns whether one was removed. */
|
|
78
|
+
removeRoute(method: OpenAPIHttpMethod, path: string): boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Registers a component schema.
|
|
81
|
+
*
|
|
82
|
+
* `schema` is converted from a `@zudojs/schema` schema; pass an already
|
|
83
|
+
* converted {@link OpenAPISchema} to {@link OpenAPIManager.addRawSchema}.
|
|
84
|
+
*/
|
|
85
|
+
addSchema(name: string, schema: unknown): this;
|
|
86
|
+
/** Registers an already-converted OpenAPI schema. */
|
|
87
|
+
addRawSchema(name: string, schema: OpenAPISchema): this;
|
|
88
|
+
/** Conversion warnings, keyed by component name. */
|
|
89
|
+
schemaWarnings(): ReadonlyMap<string, readonly string[]>;
|
|
90
|
+
/**
|
|
91
|
+
* Builds the document from the registered routes and components.
|
|
92
|
+
*
|
|
93
|
+
* Safe to call repeatedly: routes are replaced rather than re-added.
|
|
94
|
+
*/
|
|
32
95
|
generate(validate?: boolean): OpenAPIDocument;
|
|
33
|
-
|
|
96
|
+
/**
|
|
97
|
+
* Returns the document, rebuilding it when the cache is stale, absent, or
|
|
98
|
+
* was produced without the validation this call asks for.
|
|
99
|
+
*/
|
|
100
|
+
getDocument(validate?: boolean): OpenAPIDocument;
|
|
101
|
+
private isCacheFresh;
|
|
102
|
+
/** Validates the current document without throwing. */
|
|
103
|
+
validate(): OpenAPIValidationResult;
|
|
104
|
+
/** Drops the cached document. Called automatically by every mutation. */
|
|
105
|
+
invalidateCache(): this;
|
|
106
|
+
/** Drops every registered route, component and the cached document. */
|
|
107
|
+
reset(): this;
|
|
108
|
+
/**
|
|
109
|
+
* @deprecated Use {@link OpenAPIManager.reset} — the old name cleared all
|
|
110
|
+
* registered routes, which is not what "invalidate" suggests.
|
|
111
|
+
*/
|
|
34
112
|
invalidate(): void;
|
|
35
113
|
toJSON(validate?: boolean): string;
|
|
36
114
|
toYAML(validate?: boolean): string;
|
|
115
|
+
/**
|
|
116
|
+
* Builds an HTTP response carrying the document.
|
|
117
|
+
*
|
|
118
|
+
* Framework-agnostic on purpose: `{ status, headers, body }` is what every
|
|
119
|
+
* adapter in this monorepo can turn into its own response type.
|
|
120
|
+
*/
|
|
121
|
+
toResponse(options?: {
|
|
122
|
+
readonly format?: "json" | "yaml";
|
|
123
|
+
readonly validate?: boolean;
|
|
124
|
+
readonly cacheControl?: string;
|
|
125
|
+
}): OpenAPIDocumentResponse;
|
|
126
|
+
/**
|
|
127
|
+
* Builds an HTTP response carrying a branded documentation page (Swagger UI
|
|
128
|
+
* by default, ReDoc on request) that loads the specification from
|
|
129
|
+
* `options.specUrl`. Pair it with {@link OpenAPIManager.toResponse}:
|
|
130
|
+
*
|
|
131
|
+
* ```ts
|
|
132
|
+
* app.get("/openapi.json", () => manager.toResponse());
|
|
133
|
+
* app.get("/docs", () => manager.toUIResponse({ specUrl: "/openapi.json" }));
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
toUIResponse(options: OpenAPIUIOptions): OpenAPIUIResponse;
|
|
37
137
|
}
|
|
138
|
+
/** Creates an OpenAPI manager. */
|
|
139
|
+
export declare function createOpenAPIManager(options?: OpenAPIManagerOptions): OpenAPIManager;
|
|
38
140
|
//# sourceMappingURL=openApiHttpAdapter.core.d.ts.map
|
|
@@ -1,61 +1,257 @@
|
|
|
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;
|
|
13
24
|
cachedDocument;
|
|
14
|
-
|
|
15
|
-
|
|
25
|
+
cachedAt = 0;
|
|
26
|
+
/** Whether the cached document was produced by a validating generate. */
|
|
27
|
+
cachedValidated = false;
|
|
28
|
+
constructor(versionOrOptions = {}) {
|
|
29
|
+
const options = typeof versionOrOptions === "string"
|
|
30
|
+
? { version: versionOrOptions }
|
|
31
|
+
: versionOrOptions;
|
|
32
|
+
this.registry = new OpenAPIRegistryImpl(options.version ?? DEFAULT_OPENAPI_VERSION);
|
|
16
33
|
this.scanner = new OpenAPIRouteScannerImpl();
|
|
17
34
|
this.validator = new OpenAPIValidatorImpl();
|
|
35
|
+
this.schemas = new SchemaRegistryImpl({
|
|
36
|
+
version: options.version ?? DEFAULT_OPENAPI_VERSION,
|
|
37
|
+
onWarning: options.onSchemaWarning,
|
|
38
|
+
});
|
|
39
|
+
this.cacheTtlMs = options.cacheTtlMs ?? DOCUMENT_CACHE_TTL_MS;
|
|
40
|
+
this.now = options.now ?? (() => Date.now());
|
|
41
|
+
this.logo =
|
|
42
|
+
options.branding === false
|
|
43
|
+
? undefined
|
|
44
|
+
: options.branding === true || options.branding === undefined
|
|
45
|
+
? zudoLogo()
|
|
46
|
+
: options.branding;
|
|
47
|
+
if (options.info)
|
|
48
|
+
this.registry.setInfo(options.info);
|
|
49
|
+
for (const server of options.servers ?? [])
|
|
50
|
+
this.registry.addServer(server);
|
|
51
|
+
for (const tag of options.tags ?? [])
|
|
52
|
+
this.registry.setTag(tag);
|
|
53
|
+
for (const requirement of options.security ?? []) {
|
|
54
|
+
this.registry.addSecurityRequirement(requirement);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** The document version this manager emits. */
|
|
58
|
+
get version() {
|
|
59
|
+
return this.registry.version;
|
|
60
|
+
}
|
|
61
|
+
/* ── Mutation ────────────────────────────────────────────────────────── */
|
|
62
|
+
/** Sets the document's `info` object. */
|
|
63
|
+
setInfo(info) {
|
|
64
|
+
this.registry.setInfo(info);
|
|
65
|
+
return this.invalidateCache();
|
|
66
|
+
}
|
|
67
|
+
addServer(server) {
|
|
68
|
+
this.registry.addServer(server);
|
|
69
|
+
return this.invalidateCache();
|
|
70
|
+
}
|
|
71
|
+
addTag(tag) {
|
|
72
|
+
this.registry.setTag(tag);
|
|
73
|
+
return this.invalidateCache();
|
|
18
74
|
}
|
|
19
|
-
|
|
20
|
-
|
|
75
|
+
addSecurityRequirement(requirement) {
|
|
76
|
+
this.registry.addSecurityRequirement(requirement);
|
|
77
|
+
return this.invalidateCache();
|
|
21
78
|
}
|
|
22
|
-
|
|
23
|
-
|
|
79
|
+
addSecurityScheme(name, scheme) {
|
|
80
|
+
this.registry.registerSecurityScheme(name, scheme);
|
|
81
|
+
return this.invalidateCache();
|
|
24
82
|
}
|
|
83
|
+
/** Registers a route. Duplicate method+path combinations are rejected. */
|
|
25
84
|
addRoute(route) {
|
|
26
85
|
this.scanner.addRoute(route);
|
|
86
|
+
return this.invalidateCache();
|
|
27
87
|
}
|
|
88
|
+
/** Registers a route, replacing any existing one for the same method+path. */
|
|
89
|
+
setRoute(route) {
|
|
90
|
+
this.scanner.setRoute(route);
|
|
91
|
+
return this.invalidateCache();
|
|
92
|
+
}
|
|
93
|
+
/** Removes a route. Returns whether one was removed. */
|
|
94
|
+
removeRoute(method, path) {
|
|
95
|
+
const removed = this.scanner.removeRoute(method, path);
|
|
96
|
+
if (removed)
|
|
97
|
+
this.invalidateCache();
|
|
98
|
+
return removed;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Registers a component schema.
|
|
102
|
+
*
|
|
103
|
+
* `schema` is converted from a `@zudojs/schema` schema; pass an already
|
|
104
|
+
* converted {@link OpenAPISchema} to {@link OpenAPIManager.addRawSchema}.
|
|
105
|
+
*/
|
|
28
106
|
addSchema(name, schema) {
|
|
107
|
+
const converted = this.schemas.register(name, schema);
|
|
108
|
+
this.registry.registerSchema(name, converted);
|
|
109
|
+
return this.invalidateCache();
|
|
110
|
+
}
|
|
111
|
+
/** Registers an already-converted OpenAPI schema. */
|
|
112
|
+
addRawSchema(name, schema) {
|
|
29
113
|
this.registry.registerSchema(name, schema);
|
|
114
|
+
return this.invalidateCache();
|
|
30
115
|
}
|
|
116
|
+
/** Conversion warnings, keyed by component name. */
|
|
117
|
+
schemaWarnings() {
|
|
118
|
+
return this.schemas.warnings();
|
|
119
|
+
}
|
|
120
|
+
/* ── Generation ──────────────────────────────────────────────────────── */
|
|
121
|
+
/**
|
|
122
|
+
* Builds the document from the registered routes and components.
|
|
123
|
+
*
|
|
124
|
+
* Safe to call repeatedly: routes are replaced rather than re-added.
|
|
125
|
+
*/
|
|
31
126
|
generate(validate = false) {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
127
|
+
for (const route of this.scanner.scan()) {
|
|
128
|
+
this.registry.setRoute(route);
|
|
129
|
+
}
|
|
130
|
+
// Brand the document unless the caller supplied a logo or opted out.
|
|
131
|
+
const info = this.registry.getInfo();
|
|
132
|
+
if (this.logo && !info["x-logo"]) {
|
|
133
|
+
this.registry.setInfo({ ...info, "x-logo": this.logo });
|
|
35
134
|
}
|
|
36
135
|
const document = this.registry.generate();
|
|
37
|
-
if (validate)
|
|
136
|
+
if (validate)
|
|
38
137
|
this.validator.assertValid(document);
|
|
39
|
-
}
|
|
40
138
|
this.cachedDocument = document;
|
|
139
|
+
this.cachedAt = this.now();
|
|
140
|
+
this.cachedValidated = validate;
|
|
41
141
|
return document;
|
|
42
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Returns the document, rebuilding it when the cache is stale, absent, or
|
|
145
|
+
* was produced without the validation this call asks for.
|
|
146
|
+
*/
|
|
43
147
|
getDocument(validate = false) {
|
|
44
|
-
if (this.cachedDocument) {
|
|
148
|
+
if (this.cachedDocument && this.isCacheFresh()) {
|
|
149
|
+
if (!validate || this.cachedValidated)
|
|
150
|
+
return this.cachedDocument;
|
|
151
|
+
// The cached document was never validated; validating it now is
|
|
152
|
+
// cheaper than rebuilding, and skipping the check silently is what a
|
|
153
|
+
// caller passing `true` is explicitly asking us not to do.
|
|
154
|
+
this.validator.assertValid(this.cachedDocument);
|
|
155
|
+
this.cachedValidated = true;
|
|
45
156
|
return this.cachedDocument;
|
|
46
157
|
}
|
|
47
158
|
return this.generate(validate);
|
|
48
159
|
}
|
|
49
|
-
|
|
160
|
+
isCacheFresh() {
|
|
161
|
+
if (this.cacheTtlMs <= 0)
|
|
162
|
+
return false;
|
|
163
|
+
return this.now() - this.cachedAt < this.cacheTtlMs;
|
|
164
|
+
}
|
|
165
|
+
/** Validates the current document without throwing. */
|
|
166
|
+
validate() {
|
|
167
|
+
return this.validator.validate(this.getDocument());
|
|
168
|
+
}
|
|
169
|
+
/** Drops the cached document. Called automatically by every mutation. */
|
|
170
|
+
invalidateCache() {
|
|
50
171
|
this.cachedDocument = undefined;
|
|
172
|
+
this.cachedAt = 0;
|
|
173
|
+
this.cachedValidated = false;
|
|
174
|
+
return this;
|
|
175
|
+
}
|
|
176
|
+
/** Drops every registered route, component and the cached document. */
|
|
177
|
+
reset() {
|
|
51
178
|
this.scanner.clear();
|
|
52
179
|
this.registry.clear();
|
|
180
|
+
this.schemas.clear();
|
|
181
|
+
return this.invalidateCache();
|
|
53
182
|
}
|
|
183
|
+
/**
|
|
184
|
+
* @deprecated Use {@link OpenAPIManager.reset} — the old name cleared all
|
|
185
|
+
* registered routes, which is not what "invalidate" suggests.
|
|
186
|
+
*/
|
|
187
|
+
invalidate() {
|
|
188
|
+
this.reset();
|
|
189
|
+
}
|
|
190
|
+
/* ── Serialization ───────────────────────────────────────────────────── */
|
|
54
191
|
toJSON(validate = false) {
|
|
55
192
|
return toOpenAPIJSON(this.getDocument(validate));
|
|
56
193
|
}
|
|
57
194
|
toYAML(validate = false) {
|
|
58
195
|
return toOpenAPIYAML(this.getDocument(validate));
|
|
59
196
|
}
|
|
197
|
+
/* ── Serving ─────────────────────────────────────────────────────────── */
|
|
198
|
+
/**
|
|
199
|
+
* Builds an HTTP response carrying the document.
|
|
200
|
+
*
|
|
201
|
+
* Framework-agnostic on purpose: `{ status, headers, body }` is what every
|
|
202
|
+
* adapter in this monorepo can turn into its own response type.
|
|
203
|
+
*/
|
|
204
|
+
toResponse(options) {
|
|
205
|
+
const format = options?.format ?? "json";
|
|
206
|
+
const body = format === "yaml"
|
|
207
|
+
? this.toYAML(options?.validate ?? false)
|
|
208
|
+
: this.toJSON(options?.validate ?? false);
|
|
209
|
+
return Object.freeze({
|
|
210
|
+
status: 200,
|
|
211
|
+
headers: Object.freeze({
|
|
212
|
+
"content-type": format === "yaml"
|
|
213
|
+
? "application/yaml; charset=utf-8"
|
|
214
|
+
: `${DEFAULT_MEDIA_TYPE}; charset=utf-8`,
|
|
215
|
+
"cache-control": options?.cacheControl ?? "public, max-age=300",
|
|
216
|
+
}),
|
|
217
|
+
body,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Builds an HTTP response carrying a branded documentation page (Swagger UI
|
|
222
|
+
* by default, ReDoc on request) that loads the specification from
|
|
223
|
+
* `options.specUrl`. Pair it with {@link OpenAPIManager.toResponse}:
|
|
224
|
+
*
|
|
225
|
+
* ```ts
|
|
226
|
+
* app.get("/openapi.json", () => manager.toResponse());
|
|
227
|
+
* app.get("/docs", () => manager.toUIResponse({ specUrl: "/openapi.json" }));
|
|
228
|
+
* ```
|
|
229
|
+
*/
|
|
230
|
+
toUIResponse(options) {
|
|
231
|
+
const info = this.registry.getInfo();
|
|
232
|
+
const body = renderOpenAPIUI({
|
|
233
|
+
title: info.title && info.title !== "API"
|
|
234
|
+
? `${info.title} · API reference`
|
|
235
|
+
: undefined,
|
|
236
|
+
...options,
|
|
237
|
+
logo: options.logo !== undefined
|
|
238
|
+
? options.logo
|
|
239
|
+
: this.logo === undefined
|
|
240
|
+
? false
|
|
241
|
+
: undefined,
|
|
242
|
+
});
|
|
243
|
+
return Object.freeze({
|
|
244
|
+
status: 200,
|
|
245
|
+
headers: Object.freeze({
|
|
246
|
+
"content-type": "text/html; charset=utf-8",
|
|
247
|
+
"cache-control": "public, max-age=300",
|
|
248
|
+
}),
|
|
249
|
+
body,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/** Creates an OpenAPI manager. */
|
|
254
|
+
export function createOpenAPIManager(options) {
|
|
255
|
+
return new OpenAPIManager(options ?? {});
|
|
60
256
|
}
|
|
61
257
|
//# sourceMappingURL=openApiHttpAdapter.core.js.map
|