@orpc/openapi 2.0.0-beta.4 → 2.0.0-beta.40
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/README.md +97 -94
- package/dist/adapters/aws-lambda/index.d.mts +26 -0
- package/dist/adapters/aws-lambda/index.d.ts +26 -0
- package/dist/adapters/aws-lambda/index.mjs +21 -0
- package/dist/adapters/fastify/index.d.mts +23 -0
- package/dist/adapters/fastify/index.d.ts +23 -0
- package/dist/adapters/fastify/index.mjs +21 -0
- package/dist/adapters/fetch/index.d.mts +15 -5
- package/dist/adapters/fetch/index.d.ts +15 -5
- package/dist/adapters/fetch/index.mjs +6 -6
- package/dist/adapters/node/index.d.mts +9 -4
- package/dist/adapters/node/index.d.ts +9 -4
- package/dist/adapters/node/index.mjs +4 -4
- package/dist/adapters/standard/index.d.mts +10 -46
- package/dist/adapters/standard/index.d.ts +10 -46
- package/dist/adapters/standard/index.mjs +6 -6
- package/dist/extensions/route.d.mts +5 -7
- package/dist/extensions/route.d.ts +5 -7
- package/dist/helpers/index.d.mts +9 -1
- package/dist/helpers/index.d.ts +9 -1
- package/dist/helpers/index.mjs +1 -1
- package/dist/index.d.mts +77 -45
- package/dist/index.d.ts +77 -45
- package/dist/index.mjs +694 -750
- package/dist/plugins/index.d.mts +21 -4
- package/dist/plugins/index.d.ts +21 -4
- package/dist/plugins/index.mjs +7 -2
- package/dist/shared/{openapi.DmAa7YPO.mjs → openapi.B1LWf28E.mjs} +134 -64
- package/dist/shared/openapi.B2G-HeFn.mjs +123 -0
- package/dist/shared/{openapi.B2SK0ZAr.mjs → openapi.BdClbrwk.mjs} +27 -35
- package/dist/shared/openapi.BwNINoeN.d.ts +45 -0
- package/dist/shared/{openapi.BQzzr4-4.d.ts → openapi.Cem81Zhm.d.mts} +41 -12
- package/dist/shared/{openapi.BcEtAxQj.d.mts → openapi.Cem81Zhm.d.ts} +41 -12
- package/dist/shared/openapi.DE5kXinS.d.mts +45 -0
- package/dist/shared/openapi.DI21x5Yo.d.mts +31 -0
- package/dist/shared/openapi.DI21x5Yo.d.ts +31 -0
- package/dist/shared/{openapi.C7m7NAmH.d.mts → openapi.Dl3gew1x.d.mts} +16 -5
- package/dist/shared/{openapi.C7m7NAmH.d.ts → openapi.Dl3gew1x.d.ts} +16 -5
- package/dist/shared/{openapi.7vgmPxca.d.ts → openapi.EM8eC1Xn.d.ts} +12 -9
- package/dist/shared/{openapi.CX6Ri5dP.d.mts → openapi.fnBK1ELE.d.mts} +12 -9
- package/dist/shared/{openapi.CTlpLuKN.mjs → openapi.wQKxWEI2.mjs} +97 -48
- package/package.json +57 -17
- package/dist/shared/openapi.Bt87OzTt.mjs +0 -131
- package/dist/shared/openapi.CYgMBSUF.d.mts +0 -18
- package/dist/shared/openapi.CYgMBSUF.d.ts +0 -18
package/dist/index.mjs
CHANGED
|
@@ -1,786 +1,200 @@
|
|
|
1
|
-
export { B as BracketNotationSerializer } from './shared/openapi.
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
export { d as OpenAPIJsonSerializer } from './shared/openapi.
|
|
1
|
+
export { B as BracketNotationSerializer } from './shared/openapi.B2G-HeFn.mjs';
|
|
2
|
+
import { createContractClientFactory, getAsyncIteratorObjectSchemaDetails, ProcedureContract, resolveMetaPlugins } from '@orpc/contract';
|
|
3
|
+
import { D as DEFAULT_OPENAPI_METHOD, a as DEFAULT_OPENAPI_INPUT_STRUCTURE, c as DEFAULT_OPENAPI_SUCCESS_DESCRIPTION, b as DEFAULT_OPENAPI_OUTPUT_STRUCTURE, i as isBodylessMethod, O as OpenAPISerializer, g as getDynamicPathParams } from './shared/openapi.B1LWf28E.mjs';
|
|
4
|
+
export { d as OpenAPIJsonSerializer } from './shared/openapi.B1LWf28E.mjs';
|
|
5
5
|
import { g as getOpenAPIMeta, o as openapi } from './shared/openapi.B9PQzqBn.mjs';
|
|
6
|
+
import { downgradeSpecV32ToV31, downgradeSpecV31ToV30 } from '@openapi-spec/downgrader';
|
|
6
7
|
import { COMMON_ERROR_STATUS_MAP } from '@orpc/client';
|
|
7
8
|
export { COMMON_ERROR_STATUS_MAP } from '@orpc/client';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import '@
|
|
9
|
+
import { encodeJsonPointerSegment, ensureJsonSchemaObject, mapJsonSchemaRefs, decodeJsonPointerSegment, isUnconstrainedSchema, combineJsonSchemasWithComposition, extractJsonObjectSchemaEntries, combineJsonObjectSchemaEntries, flattenJsonUnionSchema, isJsonPrimitiveSchema, matchArrayableJsonSchema, isJsonFileSchema, DelegatingJsonSchemaConverter, StandardJsonSchemaConverter } from '@orpc/json-schema';
|
|
10
|
+
import { DEFAULT_SUCCESS_STATUS, DEFAULT_ERROR_STATUS, walkProcedureContractsAsync } from '@orpc/server';
|
|
11
|
+
import { isDeepEqual, value, findDeepMatches, isPlainObject, stringifyJSON, toArray, clone, pathToHttpPath, mergeHttpPath, isTypescriptObject } from '@orpc/shared';
|
|
12
|
+
import '@standard-server/core';
|
|
12
13
|
|
|
13
|
-
function
|
|
14
|
-
return
|
|
14
|
+
function createContractJsonifiedClientFactory(link, options = {}) {
|
|
15
|
+
return createContractClientFactory(link, options);
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
class
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
openapi: options.base?.openapi ?? "3.1.2",
|
|
33
|
-
info: options.base?.info ?? { title: "API Reference", version: "0.0.0" }
|
|
34
|
-
};
|
|
35
|
-
const errors = [];
|
|
36
|
-
await walkProcedureContractsAsync(router, async (contract, path) => {
|
|
37
|
-
if (value(options.filter, contract, path) === false) {
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
try {
|
|
41
|
-
const def = contract["~orpc"];
|
|
42
|
-
const meta = getOpenAPIMeta(contract);
|
|
43
|
-
const method = (meta?.method ?? DEFAULT_OPENAPI_METHOD).toLowerCase();
|
|
44
|
-
const postPath = meta?.path ?? pathToHttpPath(path);
|
|
45
|
-
const httpPath = meta?.prefix ? mergeHttpPath(meta.prefix, postPath) : postPath;
|
|
46
|
-
const dynamicPathParams = getDynamicPathParams(httpPath);
|
|
47
|
-
const openApiPath = toOpenAPIPath(httpPath, dynamicPathParams);
|
|
48
|
-
let operationRef;
|
|
49
|
-
if (meta?.spec !== void 0 && typeof meta.spec !== "function") {
|
|
50
|
-
operationRef = meta.spec;
|
|
51
|
-
} else {
|
|
52
|
-
operationRef = {
|
|
53
|
-
operationId: meta?.operationId ?? path.join("."),
|
|
54
|
-
summary: meta?.summary,
|
|
55
|
-
description: meta?.description,
|
|
56
|
-
deprecated: meta?.deprecated,
|
|
57
|
-
tags: meta?.tags?.map((tag) => tag)
|
|
58
|
-
};
|
|
59
|
-
await this.request(doc, operationRef, def, meta, dynamicPathParams, options, path);
|
|
60
|
-
await this.successResponse(doc, operationRef, def, meta, options, path);
|
|
61
|
-
await this.errorResponse(doc, operationRef, def, meta, options);
|
|
62
|
-
}
|
|
63
|
-
if (typeof meta?.spec === "function") {
|
|
64
|
-
operationRef = meta.spec(operationRef);
|
|
65
|
-
}
|
|
66
|
-
doc.paths ??= {};
|
|
67
|
-
doc.paths[openApiPath] ??= {};
|
|
68
|
-
doc.paths[openApiPath][method] = operationRef;
|
|
69
|
-
} catch (e) {
|
|
70
|
-
if (!(e instanceof OpenAPIGeneratorError)) {
|
|
71
|
-
throw e;
|
|
72
|
-
}
|
|
73
|
-
errors.push(
|
|
74
|
-
`[OpenAPIGenerator] Error occurred while generating OpenAPI for procedure at path: ${path.join(".")}
|
|
75
|
-
${e.message}`
|
|
76
|
-
);
|
|
18
|
+
class OpenAPIComponentRegistry {
|
|
19
|
+
constructor(doc, customComponentName) {
|
|
20
|
+
this.doc = doc;
|
|
21
|
+
this.customComponentName = customComponentName;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Registers `schema` as a component under `preferredName` (or an equivalent/postfixed name)
|
|
25
|
+
* and returns a `$ref` to it.
|
|
26
|
+
*/
|
|
27
|
+
register(preferredName, schema) {
|
|
28
|
+
const { $defs, ...body } = schema;
|
|
29
|
+
let defName = preferredName;
|
|
30
|
+
if ($defs) {
|
|
31
|
+
for (let i = 2; defName in $defs; i++) {
|
|
32
|
+
defName = `${preferredName}${i}`;
|
|
77
33
|
}
|
|
78
|
-
});
|
|
79
|
-
if (errors.length) {
|
|
80
|
-
throw new OpenAPIGeneratorError(
|
|
81
|
-
`Some error occurred during OpenAPI generation:
|
|
82
|
-
|
|
83
|
-
${errors.join("\n\n")}`
|
|
84
|
-
);
|
|
85
34
|
}
|
|
86
|
-
return this.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return [strip$schemaField(jsonSchema), optional];
|
|
35
|
+
return this.hoistDefs({
|
|
36
|
+
$defs: { ...$defs, [defName]: body },
|
|
37
|
+
$ref: `#/$defs/${encodeJsonPointerSegment(defName)}`
|
|
38
|
+
});
|
|
91
39
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
async request(doc, ref, def, meta, dynamicPathParams, options, path) {
|
|
108
|
-
const method = meta?.method ?? DEFAULT_OPENAPI_METHOD;
|
|
109
|
-
const inputStructure = meta?.inputStructure ?? DEFAULT_OPENAPI_INPUT_STRUCTURE;
|
|
110
|
-
const inputSchemas = def.inputSchemas;
|
|
111
|
-
if (inputStructure === "compact") {
|
|
112
|
-
const eventIteratorDetails = getEventIteratorDetails(inputSchemas);
|
|
113
|
-
if (eventIteratorDetails) {
|
|
114
|
-
const [yieldSchemas, returnSchemas] = eventIteratorDetails;
|
|
115
|
-
const yieldResult = await this.convertSchemas(yieldSchemas, "input");
|
|
116
|
-
const returnResult = await this.convertSchemas(returnSchemas, "input");
|
|
117
|
-
ref.requestBody = {
|
|
118
|
-
required: true,
|
|
119
|
-
content: toEventIteratorContent(yieldResult, returnResult, doc, options)
|
|
120
|
-
};
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
const dynamicParams = dynamicPathParams?.map((v) => v.parameterName);
|
|
125
|
-
const [schema, optional] = await this.convertSchemas(inputSchemas, "input");
|
|
126
|
-
if (isUnconstrainedSchema(schema) && !dynamicParams?.length) {
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
const objectSchemaEntries = extractJsonObjectSchemaEntries(schema);
|
|
130
|
-
if (!objectSchemaEntries) {
|
|
131
|
-
if (inputStructure === "detailed") {
|
|
132
|
-
throw new OpenAPIGeneratorError(
|
|
133
|
-
`Procedure at path "${path.join(".")}" has inputStructure "detailed" but its input schema is not an object.
|
|
134
|
-
Expected shape: { params?: Record<string, unknown>, query?: Record<string, unknown>, headers?: Record<string, unknown>, body?: unknown }`
|
|
135
|
-
);
|
|
136
|
-
}
|
|
137
|
-
if (method === "GET") {
|
|
138
|
-
throw new OpenAPIGeneratorError(
|
|
139
|
-
`Procedure at path "${path.join(".")}" uses method "GET" but its input schema is not an object.
|
|
140
|
-
GET procedures map all input fields to query parameters, so the schema must be an object.
|
|
141
|
-
Expected: Record<string, unknown>`
|
|
142
|
-
);
|
|
40
|
+
/**
|
|
41
|
+
* Moves a schema's root-level `$defs` into `doc.components.schemas` and rewrites
|
|
42
|
+
* its refs accordingly.
|
|
43
|
+
*/
|
|
44
|
+
hoistDefs(schema, direction) {
|
|
45
|
+
if (typeof schema !== "object" || !schema.$defs) {
|
|
46
|
+
return schema;
|
|
47
|
+
}
|
|
48
|
+
const { $defs, ...rest } = schema;
|
|
49
|
+
const defs = {};
|
|
50
|
+
const preferredNames = {};
|
|
51
|
+
for (const defName of Object.keys($defs)) {
|
|
52
|
+
const defSchema = $defs[defName];
|
|
53
|
+
if (defSchema === void 0) {
|
|
54
|
+
continue;
|
|
143
55
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
if (
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
56
|
+
const normalized = ensureJsonSchemaObject(defSchema);
|
|
57
|
+
defs[defName] = normalized;
|
|
58
|
+
preferredNames[defName] = this.customComponentName?.(defName, normalized) ?? defName;
|
|
59
|
+
}
|
|
60
|
+
const defNames = Object.keys(defs);
|
|
61
|
+
if (defNames.length === 0) {
|
|
62
|
+
return schema;
|
|
63
|
+
}
|
|
64
|
+
this.doc.components ??= {};
|
|
65
|
+
this.doc.components.schemas ??= {};
|
|
66
|
+
const componentsSchemas = this.doc.components.schemas;
|
|
67
|
+
const identityRenameMap = Object.fromEntries(
|
|
68
|
+
defNames.map((defName) => [defName, preferredNames[defName]])
|
|
69
|
+
);
|
|
70
|
+
const renameMap = {};
|
|
71
|
+
const pendingSchemas = [];
|
|
72
|
+
for (const defName of defNames) {
|
|
73
|
+
const cleanSchema = defs[defName];
|
|
74
|
+
const candidateSchemas = Object.fromEntries(
|
|
75
|
+
defNames.map((currentDefName) => [
|
|
76
|
+
preferredNames[currentDefName],
|
|
77
|
+
rewriteComponentSchemaRefs(defs[currentDefName], {
|
|
78
|
+
...identityRenameMap,
|
|
79
|
+
...renameMap
|
|
80
|
+
})
|
|
81
|
+
])
|
|
82
|
+
);
|
|
83
|
+
const preferredName = preferredNames[defName];
|
|
84
|
+
const prelimSchema = candidateSchemas[preferredName];
|
|
85
|
+
const [componentName, reuseExisting] = resolveComponentName(
|
|
86
|
+
componentsSchemas,
|
|
87
|
+
new Set(Object.values(renameMap)),
|
|
88
|
+
preferredName,
|
|
89
|
+
prelimSchema,
|
|
90
|
+
candidateSchemas,
|
|
91
|
+
direction
|
|
92
|
+
);
|
|
93
|
+
renameMap[defName] = componentName;
|
|
94
|
+
if (!reuseExisting) {
|
|
95
|
+
pendingSchemas.push({ cleanSchema, componentName });
|
|
155
96
|
}
|
|
156
|
-
dynamicParams.forEach((name) => {
|
|
157
|
-
const entry = paramsObjectSchemaEntries.find(([n]) => n === name);
|
|
158
|
-
if (!entry) {
|
|
159
|
-
throw new OpenAPIGeneratorError(
|
|
160
|
-
`Procedure at path "${path.join(".")}" is missing dynamic param "${name}" in its input schema.
|
|
161
|
-
Route params: ${dynamicParams.map((p) => `{${p}}`).join(", ")}
|
|
162
|
-
Schema keys: ${paramsObjectSchemaEntries.map(([n]) => n).join(", ") || "(none)"}`
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
|
-
if (entry[2]) {
|
|
166
|
-
throw new OpenAPIGeneratorError(
|
|
167
|
-
`Procedure at path "${path.join(".")}" has dynamic param "${name}" marked as optional in its input schema, but path params must always be required in OpenAPI.`
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
ref.parameters ??= [];
|
|
171
|
-
ref.parameters.push({
|
|
172
|
-
in: "path",
|
|
173
|
-
required: true,
|
|
174
|
-
name,
|
|
175
|
-
schema: toOpenAPISchema(entry[1], doc, options)
|
|
176
|
-
});
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
if (queryObjectSchemaEntries) {
|
|
180
|
-
queryObjectSchemaEntries.forEach(([name, schema2, optional2]) => {
|
|
181
|
-
const style = meta?.queryStyles?.[name];
|
|
182
|
-
const parameter = {
|
|
183
|
-
in: "query",
|
|
184
|
-
name,
|
|
185
|
-
schema: toOpenAPISchema(schema2, doc, options),
|
|
186
|
-
allowEmptyValue: true,
|
|
187
|
-
allowReserved: true
|
|
188
|
-
};
|
|
189
|
-
if (!optional2) {
|
|
190
|
-
parameter.required = true;
|
|
191
|
-
}
|
|
192
|
-
if (style === "comma-delimited-array" || style === "comma-delimited-object") {
|
|
193
|
-
parameter.explode = false;
|
|
194
|
-
} else if (style === "pipe-delimited-array" || style === "pipe-delimited-object") {
|
|
195
|
-
parameter.style = "pipeDelimited";
|
|
196
|
-
} else if (style === "space-delimited-array" || style === "space-delimited-object") {
|
|
197
|
-
parameter.style = "spaceDelimited";
|
|
198
|
-
} else if (style === "json") {
|
|
199
|
-
parameter.content = {
|
|
200
|
-
"application/json": { schema: parameter.schema }
|
|
201
|
-
};
|
|
202
|
-
delete parameter.schema;
|
|
203
|
-
} else if (style === void 0) {
|
|
204
|
-
if (flattenJsonUnionSchema(schema2).some((s) => !isJsonPrimitiveSchema(s))) {
|
|
205
|
-
const arrayable = matchArrayableJsonSchema(schema2);
|
|
206
|
-
if (!arrayable || flattenJsonUnionSchema(arrayable[0]).some((s) => !isJsonPrimitiveSchema(s))) {
|
|
207
|
-
parameter.style = "deepObject";
|
|
208
|
-
parameter.explode = true;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
} else ;
|
|
212
|
-
ref.parameters ??= [];
|
|
213
|
-
ref.parameters.push(parameter);
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
if (headersObjectSchemaEntries) {
|
|
217
|
-
headersObjectSchemaEntries.forEach(([name, schema2, optional2]) => {
|
|
218
|
-
ref.parameters ??= [];
|
|
219
|
-
ref.parameters.push({
|
|
220
|
-
in: "header",
|
|
221
|
-
name,
|
|
222
|
-
required: optional2 ? void 0 : true,
|
|
223
|
-
schema: toOpenAPISchema(schema2, doc, options)
|
|
224
|
-
});
|
|
225
|
-
});
|
|
226
97
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
};
|
|
98
|
+
for (const { cleanSchema, componentName } of pendingSchemas) {
|
|
99
|
+
componentsSchemas[componentName] = rewriteComponentSchemaRefs(
|
|
100
|
+
cleanSchema,
|
|
101
|
+
renameMap
|
|
102
|
+
);
|
|
233
103
|
}
|
|
104
|
+
return rewriteComponentSchemaRefs(rest, renameMap);
|
|
234
105
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const status = meta?.successStatus ?? DEFAULT_SUCCESS_STATUS;
|
|
238
|
-
const description = meta?.successDescription ?? DEFAULT_OPENAPI_SUCCESS_DESCRIPTION;
|
|
239
|
-
const outputStructure = meta?.outputStructure ?? DEFAULT_OPENAPI_OUTPUT_STRUCTURE;
|
|
240
|
-
if (outputStructure === "compact") {
|
|
241
|
-
const eventDetails = getEventIteratorDetails(outputSchemas);
|
|
242
|
-
if (eventDetails) {
|
|
243
|
-
const [yieldSchemas, returnSchemas] = eventDetails;
|
|
244
|
-
const yieldResult = await this.convertSchemas(yieldSchemas, "output");
|
|
245
|
-
const returnResult = await this.convertSchemas(returnSchemas, "output");
|
|
246
|
-
ref.responses ??= {};
|
|
247
|
-
ref.responses[status] = {
|
|
248
|
-
description,
|
|
249
|
-
content: toEventIteratorContent(yieldResult, returnResult, doc, options)
|
|
250
|
-
};
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
const [schema] = await this.convertSchemas(outputSchemas, "output");
|
|
255
|
-
if (isUnconstrainedSchema(schema) || outputStructure === "compact") {
|
|
256
|
-
ref.responses ??= {};
|
|
257
|
-
ref.responses[status] = {
|
|
258
|
-
description,
|
|
259
|
-
content: toBodyContent(schema, doc, options)
|
|
260
|
-
};
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
const schemasByStatus = /* @__PURE__ */ new Map();
|
|
264
|
-
for (const item of flattenJsonUnionSchema(schema)) {
|
|
265
|
-
const objectSchemaEntries = extractJsonObjectSchemaEntries(item);
|
|
266
|
-
if (!objectSchemaEntries) {
|
|
267
|
-
throw new OpenAPIGeneratorError(
|
|
268
|
-
`Procedure at path "${path.join(".")}" has outputStructure "detailed" but its output schema is not an object.
|
|
269
|
-
Expected shape: { status: number (200-299), headers?: Record<string, unknown>, body?: unknown }`
|
|
270
|
-
);
|
|
271
|
-
}
|
|
272
|
-
const statusSchema = objectSchemaEntries?.find(([name]) => name === "status")?.[1];
|
|
273
|
-
if (statusSchema !== void 0 && (typeof statusSchema !== "object" || !Number.isInteger(statusSchema.const) || statusSchema.const < 200 || statusSchema.const >= 300)) {
|
|
274
|
-
throw new OpenAPIGeneratorError(
|
|
275
|
-
`Procedure at path "${path.join(".")}" has an invalid "status" field in its outputStructure "detailed" schema.
|
|
276
|
-
Expected: a const integer in the 200-299 range
|
|
277
|
-
Received: ${stringifyJSON(statusSchema)}`
|
|
278
|
-
);
|
|
279
|
-
}
|
|
280
|
-
const status2 = (statusSchema?.const || void 0) ?? meta?.successStatus ?? DEFAULT_SUCCESS_STATUS;
|
|
281
|
-
const description2 = statusSchema?.description;
|
|
282
|
-
const schemas = schemasByStatus.get(status2);
|
|
283
|
-
const headersSchema = objectSchemaEntries?.find(([name]) => name === "headers")?.[1];
|
|
284
|
-
const bodySchema = objectSchemaEntries?.find(([name]) => name === "body")?.[1];
|
|
285
|
-
if (schemas) {
|
|
286
|
-
schemas.push({ description: description2, headers: headersSchema, body: bodySchema });
|
|
287
|
-
} else {
|
|
288
|
-
schemasByStatus.set(status2, [{ description: description2, headers: headersSchema, body: bodySchema }]);
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
for (const [status2, schemas] of schemasByStatus.entries()) {
|
|
292
|
-
const descriptions = schemas.map(({ description: description2 }) => description2).filter((d) => d !== void 0);
|
|
293
|
-
const responseObject = {
|
|
294
|
-
description: descriptions.length ? descriptions.join(", ") : description
|
|
295
|
-
};
|
|
296
|
-
const bodySchemas = schemas.map(({ body }) => body).filter((b) => b !== void 0);
|
|
297
|
-
if (bodySchemas.length) {
|
|
298
|
-
responseObject.content = toBodyContent(combineJsonSchemasWithComposition("anyOf", bodySchemas), doc, options);
|
|
299
|
-
}
|
|
300
|
-
const headerSchemas = schemas.map(({ headers }) => headers).filter((b) => b !== void 0);
|
|
301
|
-
if (headerSchemas.length) {
|
|
302
|
-
const entries = extractJsonObjectSchemaEntries(combineJsonSchemasWithComposition("anyOf", headerSchemas));
|
|
303
|
-
entries?.forEach(([name, schema2, optional]) => {
|
|
304
|
-
responseObject.headers ??= {};
|
|
305
|
-
responseObject.headers[name] = {
|
|
306
|
-
required: optional ? void 0 : true,
|
|
307
|
-
schema: toOpenAPISchema(schema2, doc, options)
|
|
308
|
-
};
|
|
309
|
-
});
|
|
310
|
-
}
|
|
311
|
-
ref.responses ??= {};
|
|
312
|
-
ref.responses[status2] = responseObject;
|
|
313
|
-
}
|
|
106
|
+
toOpenAPISchema(schema, direction) {
|
|
107
|
+
return ensureJsonSchemaObject(this.hoistDefs(schema, direction));
|
|
314
108
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
const definitions = errorDefinitionsByStatus.get(status);
|
|
328
|
-
if (definitions) {
|
|
329
|
-
definitions.push({ code, dataJsonSchema, dataOptional, defaultMessage });
|
|
330
|
-
} else {
|
|
331
|
-
errorDefinitionsByStatus.set(status, [{ code, dataJsonSchema, dataOptional, defaultMessage }]);
|
|
109
|
+
}
|
|
110
|
+
function resolveComponentName(componentsSchemas, claimedNames, preferredName, schema, candidateSchemas, direction) {
|
|
111
|
+
let mintName;
|
|
112
|
+
for (let i = 1; ; i++) {
|
|
113
|
+
const [componentName, mintable, tail] = componentNameCandidate(preferredName, direction, i);
|
|
114
|
+
const existingSchema = componentsSchemas[componentName];
|
|
115
|
+
if (existingSchema === void 0) {
|
|
116
|
+
if (mintable && !claimedNames.has(componentName)) {
|
|
117
|
+
mintName ??= componentName;
|
|
118
|
+
if (tail) {
|
|
119
|
+
return [mintName, false];
|
|
120
|
+
}
|
|
332
121
|
}
|
|
122
|
+
continue;
|
|
333
123
|
}
|
|
334
|
-
if (
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
data: {}
|
|
346
|
-
},
|
|
347
|
-
required: ["defined", "inferable", "code", "status", "message"]
|
|
348
|
-
}
|
|
349
|
-
},
|
|
350
|
-
$ref: "#/$defs/UndefinedError"
|
|
351
|
-
}, doc, options);
|
|
352
|
-
for (const [status, definitions] of errorDefinitionsByStatus.entries()) {
|
|
353
|
-
const descriptions = definitions.map(({ defaultMessage }) => defaultMessage).filter((m) => m !== void 0);
|
|
354
|
-
const customBodySchema = value(
|
|
355
|
-
options.customErrorResponseBodySchema,
|
|
356
|
-
definitions.map((def2) => ({ ...def2, dataJsonSchema: hoistDefs(def2.dataJsonSchema, doc, options) })),
|
|
357
|
-
status
|
|
358
|
-
);
|
|
359
|
-
const responseSchema = customBodySchema ?? combineJsonSchemasWithComposition("oneOf", [
|
|
360
|
-
...definitions.map(({ code, dataJsonSchema, dataOptional, defaultMessage }) => {
|
|
361
|
-
return combineJsonObjectSchemaEntries([
|
|
362
|
-
["defined", { const: true }, false],
|
|
363
|
-
["inferable", { type: "boolean" }, false],
|
|
364
|
-
["code", { const: code }, false],
|
|
365
|
-
["status", { const: status }, false],
|
|
366
|
-
["message", { type: "string", default: defaultMessage }, false],
|
|
367
|
-
["data", dataJsonSchema, dataOptional]
|
|
368
|
-
]);
|
|
369
|
-
}),
|
|
370
|
-
undefinedErrorSchema
|
|
371
|
-
]);
|
|
372
|
-
ref.responses ??= {};
|
|
373
|
-
ref.responses[status] = {
|
|
374
|
-
description: descriptions.length ? descriptions.join(", ") : status.toString(),
|
|
375
|
-
content: {
|
|
376
|
-
"application/json": {
|
|
377
|
-
schema: toOpenAPISchema(responseSchema, doc, options)
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
};
|
|
381
|
-
}
|
|
124
|
+
if (areSchemasEquivalentForReuse(
|
|
125
|
+
schema,
|
|
126
|
+
existingSchema,
|
|
127
|
+
schema,
|
|
128
|
+
existingSchema,
|
|
129
|
+
candidateSchemas,
|
|
130
|
+
componentsSchemas,
|
|
131
|
+
/* @__PURE__ */ new Map([[preferredName, componentName]]),
|
|
132
|
+
/* @__PURE__ */ new Map([[componentName, preferredName]])
|
|
133
|
+
)) {
|
|
134
|
+
return [componentName, true];
|
|
382
135
|
}
|
|
383
136
|
}
|
|
384
137
|
}
|
|
385
|
-
function
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
doc,
|
|
389
|
-
options
|
|
390
|
-
));
|
|
391
|
-
}
|
|
392
|
-
function toOpenAPIPath(path, dynamicPathParams) {
|
|
393
|
-
if (!dynamicPathParams?.length) {
|
|
394
|
-
return path;
|
|
395
|
-
}
|
|
396
|
-
let normalized = "";
|
|
397
|
-
let currentIndex = 0;
|
|
398
|
-
for (const param of dynamicPathParams) {
|
|
399
|
-
normalized += path.slice(currentIndex, param.startIndex);
|
|
400
|
-
normalized += `{${param.parameterName}}`;
|
|
401
|
-
currentIndex = param.startIndex + param.segment.length;
|
|
402
|
-
}
|
|
403
|
-
normalized += path.slice(currentIndex);
|
|
404
|
-
return normalized;
|
|
405
|
-
}
|
|
406
|
-
function strip$schemaField(schema) {
|
|
407
|
-
if (typeof schema !== "object") {
|
|
408
|
-
return schema;
|
|
409
|
-
}
|
|
410
|
-
const { $schema, ...rest } = schema;
|
|
411
|
-
return rest;
|
|
412
|
-
}
|
|
413
|
-
function getEventIteratorDetails(schemas) {
|
|
414
|
-
if (!schemas || schemas.length === 0) {
|
|
415
|
-
return void 0;
|
|
138
|
+
function componentNameCandidate(preferredName, direction, attempt) {
|
|
139
|
+
if (attempt === 1) {
|
|
140
|
+
return [preferredName, true, false];
|
|
416
141
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
const details = getEventIteratorSchemaDetails(s);
|
|
421
|
-
if (!details) {
|
|
422
|
-
return void 0;
|
|
142
|
+
if (direction !== void 0) {
|
|
143
|
+
if (attempt === 2) {
|
|
144
|
+
return [`${preferredName}${direction === "input" ? "Input" : "Output"}`, true, false];
|
|
423
145
|
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
returnSchemas.push(details.returnSchema);
|
|
146
|
+
if (attempt === 3) {
|
|
147
|
+
return [`${preferredName}${direction === "input" ? "Output" : "Input"}`, false, false];
|
|
427
148
|
}
|
|
149
|
+
return [`${preferredName}${attempt - 2}`, true, true];
|
|
428
150
|
}
|
|
429
|
-
return
|
|
151
|
+
return [`${preferredName}${attempt}`, true, true];
|
|
430
152
|
}
|
|
431
|
-
function
|
|
432
|
-
|
|
433
|
-
combineJsonObjectSchemaEntries([
|
|
434
|
-
["event", { const: "message" }, false],
|
|
435
|
-
["data", yieldSchema, yieldOptional],
|
|
436
|
-
["id", { type: "string" }, true],
|
|
437
|
-
["retry", { type: "number" }, true]
|
|
438
|
-
]),
|
|
439
|
-
combineJsonObjectSchemaEntries([
|
|
440
|
-
["event", { const: "close" }, false],
|
|
441
|
-
["data", returnSchema, returnOptional],
|
|
442
|
-
["id", { type: "string" }, true],
|
|
443
|
-
["retry", { type: "number" }, true]
|
|
444
|
-
]),
|
|
445
|
-
{
|
|
446
|
-
type: "object",
|
|
447
|
-
properties: {
|
|
448
|
-
event: { const: "error" },
|
|
449
|
-
data: {},
|
|
450
|
-
id: { type: "string" },
|
|
451
|
-
retry: { type: "number" }
|
|
452
|
-
},
|
|
453
|
-
required: ["event"]
|
|
454
|
-
}
|
|
455
|
-
]);
|
|
456
|
-
return {
|
|
457
|
-
"text/event-stream": {
|
|
458
|
-
schema: toOpenAPISchema(schema, doc, options)
|
|
459
|
-
}
|
|
460
|
-
};
|
|
153
|
+
function definedKeysOf(object) {
|
|
154
|
+
return Object.keys(object).filter((key) => object[key] !== void 0).sort();
|
|
461
155
|
}
|
|
462
|
-
function
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
}
|
|
468
|
-
const contentMediaType = s.contentMediaType ?? "*/*";
|
|
469
|
-
const schemas = fileSchemasByMediaType.get(contentMediaType);
|
|
470
|
-
if (schemas) {
|
|
471
|
-
schemas.push(s);
|
|
472
|
-
} else {
|
|
473
|
-
fileSchemasByMediaType.set(contentMediaType, [s]);
|
|
474
|
-
}
|
|
156
|
+
function areSchemasEquivalentForReuse(candidate, existing, candidateRootSchema, existingRootSchema, candidateSchemas, existingSchemas, candidateToExistingComponentNames, existingToCandidateComponentNames, visited = /* @__PURE__ */ new WeakMap()) {
|
|
157
|
+
if (candidate === existing) {
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
if (typeof candidate !== typeof existing) {
|
|
475
161
|
return false;
|
|
476
|
-
});
|
|
477
|
-
const content = {};
|
|
478
|
-
if (rest.length > 0) {
|
|
479
|
-
const restSchema = fileSchemasByMediaType.size ? combineJsonSchemasWithComposition("anyOf", rest) : schema;
|
|
480
|
-
const hasNestedFiles = findDeepMatches(
|
|
481
|
-
(v) => isPlainObject(v) && isJsonFileSchema(v),
|
|
482
|
-
restSchema
|
|
483
|
-
).values.length > 0;
|
|
484
|
-
const contentType = hasNestedFiles ? "multipart/form-data" : "application/json";
|
|
485
|
-
const fileSchemas = fileSchemasByMediaType.get(contentType);
|
|
486
|
-
fileSchemasByMediaType.delete(contentType);
|
|
487
|
-
content[contentType] = {
|
|
488
|
-
schema: toOpenAPISchema(combineJsonSchemasWithComposition("anyOf", [restSchema, ...toArray(fileSchemas)]), doc, options)
|
|
489
|
-
};
|
|
490
162
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
schema: toOpenAPISchema(combineJsonSchemasWithComposition("anyOf", schemas), doc, options)
|
|
494
|
-
};
|
|
163
|
+
if (candidate === null || existing === null) {
|
|
164
|
+
return candidate === existing;
|
|
495
165
|
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
function hoistDefs(schema, doc, options) {
|
|
499
|
-
if (typeof schema !== "object") {
|
|
500
|
-
return schema;
|
|
166
|
+
if (typeof candidate !== "object" || typeof existing !== "object") {
|
|
167
|
+
return isDeepEqual(candidate, existing);
|
|
501
168
|
}
|
|
502
|
-
|
|
503
|
-
|
|
169
|
+
const seenExisting = visited.get(candidate);
|
|
170
|
+
if (seenExisting?.has(existing)) {
|
|
171
|
+
return true;
|
|
504
172
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
const normalized = normalizeHoistedDefSchema(defSchema);
|
|
514
|
-
if (value(options.shouldHoistDef, defName, normalized) !== false) {
|
|
515
|
-
hoistedDefs[defName] = normalized;
|
|
516
|
-
} else {
|
|
517
|
-
localDefs[defName] = normalized;
|
|
173
|
+
if (seenExisting) {
|
|
174
|
+
seenExisting.add(existing);
|
|
175
|
+
} else {
|
|
176
|
+
visited.set(candidate, new WeakSet([existing]));
|
|
177
|
+
}
|
|
178
|
+
if (Array.isArray(candidate) || Array.isArray(existing)) {
|
|
179
|
+
if (!Array.isArray(candidate) || !Array.isArray(existing) || candidate.length !== existing.length) {
|
|
180
|
+
return false;
|
|
518
181
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
const renameMap = {};
|
|
531
|
-
const pendingSchemas = [];
|
|
532
|
-
for (const defName of Object.keys(hoistedDefs)) {
|
|
533
|
-
const cleanSchema = hoistedDefs[defName];
|
|
534
|
-
const existingSchema = componentsSchemas[defName];
|
|
535
|
-
const candidateSchemas = Object.fromEntries(
|
|
536
|
-
Object.keys(hoistedDefs).map((currentDefName) => [
|
|
537
|
-
currentDefName,
|
|
538
|
-
rewriteComponentSchemaRefs(
|
|
539
|
-
withReferencedLocalDefs(hoistedDefs[currentDefName], localDefs),
|
|
540
|
-
{
|
|
541
|
-
...identityRenameMap,
|
|
542
|
-
...renameMap
|
|
543
|
-
}
|
|
544
|
-
)
|
|
545
|
-
])
|
|
546
|
-
);
|
|
547
|
-
const prelimSchema = candidateSchemas[defName];
|
|
548
|
-
if (existingSchema !== void 0) {
|
|
549
|
-
const reusableComponentName = findReusableComponentName(componentsSchemas, defName, prelimSchema, candidateSchemas);
|
|
550
|
-
if (reusableComponentName !== void 0) {
|
|
551
|
-
renameMap[defName] = reusableComponentName;
|
|
552
|
-
continue;
|
|
553
|
-
}
|
|
554
|
-
const componentName = findUniqueComponentName(componentsSchemas, defName);
|
|
555
|
-
renameMap[defName] = componentName;
|
|
556
|
-
pendingSchemas.push({ cleanSchema, componentName });
|
|
557
|
-
} else {
|
|
558
|
-
const reusableComponentName = findReusableComponentName(componentsSchemas, defName, prelimSchema, candidateSchemas);
|
|
559
|
-
if (reusableComponentName !== void 0) {
|
|
560
|
-
renameMap[defName] = reusableComponentName;
|
|
561
|
-
continue;
|
|
562
|
-
}
|
|
563
|
-
renameMap[defName] = defName;
|
|
564
|
-
pendingSchemas.push({ cleanSchema, componentName: defName });
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
for (const { cleanSchema, componentName } of pendingSchemas) {
|
|
568
|
-
componentsSchemas[componentName] = rewriteComponentSchemaRefs(
|
|
569
|
-
withReferencedLocalDefs(cleanSchema, localDefs),
|
|
570
|
-
renameMap
|
|
571
|
-
);
|
|
572
|
-
}
|
|
573
|
-
return rewriteComponentSchemaRefs(withReferencedLocalDefs(rest, localDefs), renameMap);
|
|
574
|
-
}
|
|
575
|
-
function normalizeHoistedDefSchema(schema) {
|
|
576
|
-
let cleanSchema = typeof schema === "boolean" ? schema ? {} : { not: {} } : { ...schema };
|
|
577
|
-
if (cleanSchema.additionalProperties === false) {
|
|
578
|
-
const { additionalProperties: _ignored, ...withoutAdditionalProperties } = cleanSchema;
|
|
579
|
-
cleanSchema = withoutAdditionalProperties;
|
|
580
|
-
}
|
|
581
|
-
return cleanSchema;
|
|
582
|
-
}
|
|
583
|
-
function hoistReferencedLocalDefs(hoistedDefs, localDefs) {
|
|
584
|
-
const queue = Object.values(hoistedDefs);
|
|
585
|
-
while (queue.length > 0) {
|
|
586
|
-
const current = queue.shift();
|
|
587
|
-
if (current === void 0) {
|
|
588
|
-
continue;
|
|
589
|
-
}
|
|
590
|
-
visitSchemaRefs(current, (refName) => {
|
|
591
|
-
const referenced = localDefs[refName];
|
|
592
|
-
if (referenced === void 0) {
|
|
593
|
-
return;
|
|
594
|
-
}
|
|
595
|
-
hoistedDefs[refName] = referenced;
|
|
596
|
-
delete localDefs[refName];
|
|
597
|
-
queue.push(referenced);
|
|
598
|
-
});
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
function withReferencedLocalDefs(schema, localDefs) {
|
|
602
|
-
const referencedLocalDefs = collectReferencedLocalDefNames(schema, localDefs);
|
|
603
|
-
if (referencedLocalDefs.length === 0) {
|
|
604
|
-
return schema;
|
|
605
|
-
}
|
|
606
|
-
const mergedDefs = {
|
|
607
|
-
...schema.$defs
|
|
608
|
-
};
|
|
609
|
-
for (const defName of referencedLocalDefs) {
|
|
610
|
-
mergedDefs[defName] = localDefs[defName];
|
|
611
|
-
}
|
|
612
|
-
return {
|
|
613
|
-
...schema,
|
|
614
|
-
$defs: mergedDefs
|
|
615
|
-
};
|
|
616
|
-
}
|
|
617
|
-
function collectReferencedLocalDefNames(schema, localDefs) {
|
|
618
|
-
if (Object.keys(localDefs).length === 0) {
|
|
619
|
-
return [];
|
|
620
|
-
}
|
|
621
|
-
const referenced = /* @__PURE__ */ new Set();
|
|
622
|
-
const queued = /* @__PURE__ */ new Set();
|
|
623
|
-
const queue = [schema];
|
|
624
|
-
while (queue.length > 0) {
|
|
625
|
-
const current = queue.shift();
|
|
626
|
-
if (current === void 0) {
|
|
627
|
-
continue;
|
|
628
|
-
}
|
|
629
|
-
visitSchemaRefs(current, (refName) => {
|
|
630
|
-
if (localDefs[refName] === void 0 || referenced.has(refName)) {
|
|
631
|
-
return;
|
|
632
|
-
}
|
|
633
|
-
referenced.add(refName);
|
|
634
|
-
if (!queued.has(refName)) {
|
|
635
|
-
queued.add(refName);
|
|
636
|
-
queue.push(localDefs[refName]);
|
|
637
|
-
}
|
|
638
|
-
});
|
|
639
|
-
}
|
|
640
|
-
return [...referenced];
|
|
641
|
-
}
|
|
642
|
-
function visitSchemaRefs(schema, onRef, seen = /* @__PURE__ */ new Set()) {
|
|
643
|
-
if (typeof schema !== "object" || schema === null) {
|
|
644
|
-
return;
|
|
645
|
-
}
|
|
646
|
-
if (seen.has(schema)) {
|
|
647
|
-
return;
|
|
648
|
-
}
|
|
649
|
-
seen.add(schema);
|
|
650
|
-
if (typeof schema.$ref === "string") {
|
|
651
|
-
const refName = parseLocalDefRefName(schema.$ref);
|
|
652
|
-
if (refName !== void 0) {
|
|
653
|
-
onRef(refName);
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
for (const keyword of ["allOf", "anyOf", "oneOf"]) {
|
|
657
|
-
if (Array.isArray(schema[keyword])) {
|
|
658
|
-
for (const item of schema[keyword]) {
|
|
659
|
-
visitSchemaRefs(item, onRef, seen);
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
if (schema.properties) {
|
|
664
|
-
for (const property of Object.values(schema.properties)) {
|
|
665
|
-
visitSchemaRefs(property, onRef, seen);
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
if (schema.items !== void 0) {
|
|
669
|
-
visitSchemaRefs(schema.items, onRef, seen);
|
|
670
|
-
}
|
|
671
|
-
if (typeof schema.additionalProperties === "object" && schema.additionalProperties !== null) {
|
|
672
|
-
visitSchemaRefs(schema.additionalProperties, onRef, seen);
|
|
673
|
-
}
|
|
674
|
-
if (schema.not !== void 0) {
|
|
675
|
-
visitSchemaRefs(schema.not, onRef, seen);
|
|
676
|
-
}
|
|
677
|
-
if (schema.if !== void 0) {
|
|
678
|
-
visitSchemaRefs(schema.if, onRef, seen);
|
|
679
|
-
}
|
|
680
|
-
if (schema.then !== void 0) {
|
|
681
|
-
visitSchemaRefs(schema.then, onRef, seen);
|
|
682
|
-
}
|
|
683
|
-
if (schema.else !== void 0) {
|
|
684
|
-
visitSchemaRefs(schema.else, onRef, seen);
|
|
685
|
-
}
|
|
686
|
-
if (Array.isArray(schema.prefixItems)) {
|
|
687
|
-
for (const item of schema.prefixItems) {
|
|
688
|
-
visitSchemaRefs(item, onRef, seen);
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
if (schema.$defs) {
|
|
692
|
-
for (const def of Object.values(schema.$defs)) {
|
|
693
|
-
if (def !== void 0) {
|
|
694
|
-
visitSchemaRefs(def, onRef, seen);
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
function findUniqueComponentName(componentsSchemas, baseName) {
|
|
700
|
-
const candidate = `${baseName}`;
|
|
701
|
-
if (componentsSchemas[candidate] === void 0)
|
|
702
|
-
return candidate;
|
|
703
|
-
let i = 2;
|
|
704
|
-
while (componentsSchemas[`${baseName}${i}`] !== void 0) {
|
|
705
|
-
i++;
|
|
706
|
-
}
|
|
707
|
-
return `${baseName}${i}`;
|
|
708
|
-
}
|
|
709
|
-
function findReusableComponentName(componentsSchemas, defName, schema, candidateSchemas) {
|
|
710
|
-
const exactMatch = componentsSchemas[defName];
|
|
711
|
-
if (exactMatch !== void 0 && areSchemasEquivalentForReuse(
|
|
712
|
-
schema,
|
|
713
|
-
exactMatch,
|
|
714
|
-
schema,
|
|
715
|
-
exactMatch,
|
|
716
|
-
candidateSchemas,
|
|
717
|
-
componentsSchemas,
|
|
718
|
-
/* @__PURE__ */ new Map([[defName, defName]]),
|
|
719
|
-
/* @__PURE__ */ new Map([[defName, defName]])
|
|
720
|
-
)) {
|
|
721
|
-
return defName;
|
|
722
|
-
}
|
|
723
|
-
for (const [componentName, componentSchema] of Object.entries(componentsSchemas)) {
|
|
724
|
-
if (componentName === defName) {
|
|
725
|
-
continue;
|
|
726
|
-
}
|
|
727
|
-
if (areSchemasEquivalentForReuse(
|
|
728
|
-
schema,
|
|
729
|
-
componentSchema,
|
|
730
|
-
schema,
|
|
731
|
-
componentSchema,
|
|
732
|
-
candidateSchemas,
|
|
733
|
-
componentsSchemas,
|
|
734
|
-
/* @__PURE__ */ new Map([[defName, componentName]]),
|
|
735
|
-
/* @__PURE__ */ new Map([[componentName, defName]])
|
|
736
|
-
)) {
|
|
737
|
-
return componentName;
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
return void 0;
|
|
741
|
-
}
|
|
742
|
-
function areSchemasEquivalentForReuse(candidate, existing, candidateRootSchema, existingRootSchema, candidateSchemas, existingSchemas, candidateToExistingComponentNames, existingToCandidateComponentNames, visited = /* @__PURE__ */ new WeakMap()) {
|
|
743
|
-
if (candidate === existing) {
|
|
744
|
-
return true;
|
|
745
|
-
}
|
|
746
|
-
if (typeof candidate !== typeof existing) {
|
|
747
|
-
return false;
|
|
748
|
-
}
|
|
749
|
-
if (candidate === null || existing === null) {
|
|
750
|
-
return candidate === existing;
|
|
751
|
-
}
|
|
752
|
-
if (typeof candidate !== "object" || typeof existing !== "object") {
|
|
753
|
-
return isDeepEqual(candidate, existing);
|
|
754
|
-
}
|
|
755
|
-
const seenExisting = visited.get(candidate);
|
|
756
|
-
if (seenExisting?.has(existing)) {
|
|
757
|
-
return true;
|
|
758
|
-
}
|
|
759
|
-
if (seenExisting) {
|
|
760
|
-
seenExisting.add(existing);
|
|
761
|
-
} else {
|
|
762
|
-
visited.set(candidate, new WeakSet([existing]));
|
|
763
|
-
}
|
|
764
|
-
if (Array.isArray(candidate) || Array.isArray(existing)) {
|
|
765
|
-
if (!Array.isArray(candidate) || !Array.isArray(existing) || candidate.length !== existing.length) {
|
|
766
|
-
return false;
|
|
767
|
-
}
|
|
768
|
-
return candidate.every((item, index) => areSchemasEquivalentForReuse(
|
|
769
|
-
item,
|
|
770
|
-
existing[index],
|
|
771
|
-
candidateRootSchema,
|
|
772
|
-
existingRootSchema,
|
|
773
|
-
candidateSchemas,
|
|
774
|
-
existingSchemas,
|
|
775
|
-
candidateToExistingComponentNames,
|
|
776
|
-
existingToCandidateComponentNames,
|
|
777
|
-
visited
|
|
778
|
-
));
|
|
182
|
+
return candidate.every((item, index) => areSchemasEquivalentForReuse(
|
|
183
|
+
item,
|
|
184
|
+
existing[index],
|
|
185
|
+
candidateRootSchema,
|
|
186
|
+
existingRootSchema,
|
|
187
|
+
candidateSchemas,
|
|
188
|
+
existingSchemas,
|
|
189
|
+
candidateToExistingComponentNames,
|
|
190
|
+
existingToCandidateComponentNames,
|
|
191
|
+
visited
|
|
192
|
+
));
|
|
779
193
|
}
|
|
780
194
|
const candidateObject = candidate;
|
|
781
195
|
const existingObject = existing;
|
|
782
|
-
const candidateKeys =
|
|
783
|
-
const existingKeys =
|
|
196
|
+
const candidateKeys = definedKeysOf(candidateObject);
|
|
197
|
+
const existingKeys = definedKeysOf(existingObject);
|
|
784
198
|
if (!isDeepEqual(candidateKeys, existingKeys)) {
|
|
785
199
|
return false;
|
|
786
200
|
}
|
|
@@ -897,6 +311,536 @@ function rewriteComponentSchemaRefs(schema, renameMap) {
|
|
|
897
311
|
});
|
|
898
312
|
}
|
|
899
313
|
|
|
314
|
+
class OpenAPIGeneratorError extends TypeError {
|
|
315
|
+
}
|
|
316
|
+
function toOpenAPIPath(path, dynamicPathParams) {
|
|
317
|
+
if (!dynamicPathParams?.length) {
|
|
318
|
+
return path;
|
|
319
|
+
}
|
|
320
|
+
let normalized = "";
|
|
321
|
+
let currentIndex = 0;
|
|
322
|
+
for (const param of dynamicPathParams) {
|
|
323
|
+
normalized += path.slice(currentIndex, param.startIndex);
|
|
324
|
+
normalized += `{${param.parameterName}}`;
|
|
325
|
+
currentIndex = param.startIndex + param.segment.length;
|
|
326
|
+
}
|
|
327
|
+
normalized += path.slice(currentIndex);
|
|
328
|
+
return normalized;
|
|
329
|
+
}
|
|
330
|
+
function buildRequest(ctx, operation, def, meta, dynamicPathParams) {
|
|
331
|
+
const method = meta?.method ?? DEFAULT_OPENAPI_METHOD;
|
|
332
|
+
const inputStructure = meta?.inputStructure ?? DEFAULT_OPENAPI_INPUT_STRUCTURE;
|
|
333
|
+
if (inputStructure === "compact") {
|
|
334
|
+
const iteratorDetails = getAsyncIteratorObjectDetails(def.inputSchemas);
|
|
335
|
+
if (iteratorDetails) {
|
|
336
|
+
operation.requestBody = {
|
|
337
|
+
required: true,
|
|
338
|
+
content: toAsyncIteratorObjectContent(
|
|
339
|
+
ctx,
|
|
340
|
+
"input",
|
|
341
|
+
ctx.convertSchemas(iteratorDetails[0], "input"),
|
|
342
|
+
ctx.convertSchemas(iteratorDetails[1], "input")
|
|
343
|
+
)
|
|
344
|
+
};
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
const dynamicParams = dynamicPathParams?.map((v) => v.parameterName);
|
|
349
|
+
const [schema, optional] = ctx.convertSchemas(def.inputSchemas, "input");
|
|
350
|
+
if (isUnconstrainedSchema(schema) && !dynamicParams?.length) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const parts = inputStructure === "compact" ? extractCompactRequestParts(schema, optional, method, dynamicParams) : extractDetailedRequestParts(schema);
|
|
354
|
+
renderPathParameters(ctx, operation, dynamicParams, parts.paramsEntries, meta?.paramsStyles);
|
|
355
|
+
renderQueryParameters(ctx, operation, parts.queryEntries, meta?.queryStyles);
|
|
356
|
+
renderHeaderParameters(ctx, operation, parts.headersEntries);
|
|
357
|
+
if (parts.bodySchema !== void 0) {
|
|
358
|
+
operation.requestBody = {
|
|
359
|
+
required: parts.bodyOptional ? void 0 : true,
|
|
360
|
+
content: toBodyContent(ctx, "input", parts.bodySchema)
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
function extractCompactRequestParts(schema, optional, method, dynamicParams) {
|
|
365
|
+
const entries = extractJsonObjectSchemaEntries(schema);
|
|
366
|
+
if (!entries && isBodylessMethod(method)) {
|
|
367
|
+
throw new OpenAPIGeneratorError(
|
|
368
|
+
`method is ${method} but the input schema is not an object.
|
|
369
|
+
${method} sends every input field as a query parameter, so the input must be an object schema.
|
|
370
|
+
Fix: make the input an object, or use a method with a request body (POST, PUT, PATCH, DELETE).`
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
const paramsEntries = entries?.filter(([name]) => dynamicParams?.includes(name));
|
|
374
|
+
const restEntries = entries?.filter(([name]) => !dynamicParams?.includes(name));
|
|
375
|
+
const hasBody = !isBodylessMethod(method);
|
|
376
|
+
const bodySchema = !hasBody ? void 0 : !dynamicParams?.length ? schema : restEntries ? combineJsonObjectSchemaEntries(restEntries) : void 0;
|
|
377
|
+
return {
|
|
378
|
+
paramsEntries,
|
|
379
|
+
queryEntries: isBodylessMethod(method) ? restEntries : void 0,
|
|
380
|
+
headersEntries: void 0,
|
|
381
|
+
bodySchema,
|
|
382
|
+
bodyOptional: !dynamicParams?.length ? optional : restEntries?.every(([, , optional2]) => optional2)
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
function extractDetailedRequestParts(schema) {
|
|
386
|
+
const entries = extractJsonObjectSchemaEntries(schema);
|
|
387
|
+
if (!entries) {
|
|
388
|
+
throw new OpenAPIGeneratorError(
|
|
389
|
+
`inputStructure is "detailed" but the input schema is not an object.
|
|
390
|
+
Expected an object shaped like: { params?: object, query?: object, headers?: object, body?: unknown }`
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
const section = (name) => entries.find(([entryName]) => entryName === name);
|
|
394
|
+
return {
|
|
395
|
+
paramsEntries: extractJsonObjectSchemaEntries(section("params")?.[1] ?? false),
|
|
396
|
+
queryEntries: extractJsonObjectSchemaEntries(section("query")?.[1] ?? false),
|
|
397
|
+
headersEntries: extractJsonObjectSchemaEntries(section("headers")?.[1] ?? false),
|
|
398
|
+
bodySchema: section("body")?.[1],
|
|
399
|
+
bodyOptional: section("body")?.[2]
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
function renderPathParameters(ctx, operation, dynamicParams, paramsEntries, paramsStyles) {
|
|
403
|
+
if (!dynamicParams?.length) {
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (!paramsEntries) {
|
|
407
|
+
throw new OpenAPIGeneratorError(
|
|
408
|
+
`the route declares path params (${dynamicParams.map((p) => `{${p}}`).join(", ")}) but there is no object schema to source them from.
|
|
409
|
+
compact mode: the input schema must be an object containing each param.
|
|
410
|
+
detailed mode: the input's "params" section must be an object containing each param.`
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
for (const name of dynamicParams) {
|
|
414
|
+
const entry = paramsEntries.find(([entryName]) => entryName === name);
|
|
415
|
+
if (!entry) {
|
|
416
|
+
throw new OpenAPIGeneratorError(
|
|
417
|
+
`dynamic param "{${name}}" is missing from the input schema.
|
|
418
|
+
Route params: ${dynamicParams.map((p) => `{${p}}`).join(", ")}
|
|
419
|
+
Schema keys: ${paramsEntries.map(([n]) => n).join(", ") || "(none)"}`
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
if (entry[2]) {
|
|
423
|
+
throw new OpenAPIGeneratorError(
|
|
424
|
+
`dynamic param "${name}" is optional in the input schema.
|
|
425
|
+
OpenAPI requires path params to be required. Make "${name}" required.`
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
const style = paramsStyles?.[name];
|
|
429
|
+
const parameter = {
|
|
430
|
+
in: "path",
|
|
431
|
+
required: true,
|
|
432
|
+
name,
|
|
433
|
+
schema: ctx.registry.toOpenAPISchema(entry[1], "input")
|
|
434
|
+
};
|
|
435
|
+
if (style === "comma-delimited-array" || style === "comma-delimited-object") {
|
|
436
|
+
parameter.style = "simple";
|
|
437
|
+
parameter.explode = false;
|
|
438
|
+
}
|
|
439
|
+
operation.parameters ??= [];
|
|
440
|
+
operation.parameters.push(parameter);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
function renderQueryParameters(ctx, operation, queryEntries, queryStyles) {
|
|
444
|
+
for (const [name, schema, optional] of queryEntries ?? []) {
|
|
445
|
+
const style = queryStyles?.[name];
|
|
446
|
+
const parameter = {
|
|
447
|
+
in: "query",
|
|
448
|
+
name,
|
|
449
|
+
schema: ctx.registry.toOpenAPISchema(schema, "input"),
|
|
450
|
+
allowEmptyValue: true,
|
|
451
|
+
allowReserved: true
|
|
452
|
+
};
|
|
453
|
+
if (!optional) {
|
|
454
|
+
parameter.required = true;
|
|
455
|
+
}
|
|
456
|
+
if (style === "comma-delimited-array" || style === "comma-delimited-object") {
|
|
457
|
+
parameter.explode = false;
|
|
458
|
+
} else if (style === "pipe-delimited-array" || style === "pipe-delimited-object") {
|
|
459
|
+
parameter.style = "pipeDelimited";
|
|
460
|
+
} else if (style === "space-delimited-array" || style === "space-delimited-object") {
|
|
461
|
+
parameter.style = "spaceDelimited";
|
|
462
|
+
} else if (style === "json") {
|
|
463
|
+
parameter.content = {
|
|
464
|
+
"application/json": { schema: parameter.schema }
|
|
465
|
+
};
|
|
466
|
+
delete parameter.schema;
|
|
467
|
+
} else if (style === void 0) {
|
|
468
|
+
if (flattenJsonUnionSchema(schema).some((s) => !isJsonPrimitiveSchema(s))) {
|
|
469
|
+
const arrayable = matchArrayableJsonSchema(schema);
|
|
470
|
+
if (!arrayable || flattenJsonUnionSchema(arrayable[0]).some((s) => !isJsonPrimitiveSchema(s))) {
|
|
471
|
+
parameter.style = "deepObject";
|
|
472
|
+
parameter.explode = true;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
} else ;
|
|
476
|
+
operation.parameters ??= [];
|
|
477
|
+
operation.parameters.push(parameter);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
function renderHeaderParameters(ctx, operation, headersEntries) {
|
|
481
|
+
for (const [name, schema, optional] of headersEntries ?? []) {
|
|
482
|
+
operation.parameters ??= [];
|
|
483
|
+
operation.parameters.push({
|
|
484
|
+
in: "header",
|
|
485
|
+
name,
|
|
486
|
+
required: optional ? void 0 : true,
|
|
487
|
+
schema: ctx.registry.toOpenAPISchema(schema, "input")
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
function buildSuccessResponse(ctx, operation, def, meta) {
|
|
492
|
+
const status = meta?.successStatus ?? DEFAULT_SUCCESS_STATUS;
|
|
493
|
+
const description = meta?.successDescription ?? DEFAULT_OPENAPI_SUCCESS_DESCRIPTION;
|
|
494
|
+
const outputStructure = meta?.outputStructure ?? DEFAULT_OPENAPI_OUTPUT_STRUCTURE;
|
|
495
|
+
if (outputStructure === "compact") {
|
|
496
|
+
const iteratorDetails = getAsyncIteratorObjectDetails(def.outputSchemas);
|
|
497
|
+
if (iteratorDetails) {
|
|
498
|
+
operation.responses ??= {};
|
|
499
|
+
operation.responses[toResponseStatusKey(status)] = {
|
|
500
|
+
description,
|
|
501
|
+
content: toAsyncIteratorObjectContent(
|
|
502
|
+
ctx,
|
|
503
|
+
"output",
|
|
504
|
+
ctx.convertSchemas(iteratorDetails[0], "output"),
|
|
505
|
+
ctx.convertSchemas(iteratorDetails[1], "output")
|
|
506
|
+
)
|
|
507
|
+
};
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
const [schema] = ctx.convertSchemas(def.outputSchemas, "output");
|
|
512
|
+
if (isUnconstrainedSchema(schema) || outputStructure === "compact") {
|
|
513
|
+
operation.responses ??= {};
|
|
514
|
+
operation.responses[toResponseStatusKey(status)] = {
|
|
515
|
+
description,
|
|
516
|
+
content: toBodyContent(ctx, "output", schema)
|
|
517
|
+
};
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
for (const [responseStatus, parts] of extractDetailedResponseParts(schema, status)) {
|
|
521
|
+
const responseObject = {
|
|
522
|
+
description: parts.descriptions.length ? parts.descriptions.join(", ") : description
|
|
523
|
+
};
|
|
524
|
+
if (parts.bodies.length) {
|
|
525
|
+
responseObject.content = toBodyContent(ctx, "output", combineJsonSchemasWithComposition("anyOf", parts.bodies));
|
|
526
|
+
}
|
|
527
|
+
if (parts.headers.length) {
|
|
528
|
+
const entries = extractJsonObjectSchemaEntries(combineJsonSchemasWithComposition("anyOf", parts.headers));
|
|
529
|
+
entries?.forEach(([name, schema2, optional]) => {
|
|
530
|
+
responseObject.headers ??= {};
|
|
531
|
+
responseObject.headers[name] = {
|
|
532
|
+
required: optional ? void 0 : true,
|
|
533
|
+
schema: ctx.registry.toOpenAPISchema(schema2, "output")
|
|
534
|
+
};
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
operation.responses ??= {};
|
|
538
|
+
operation.responses[toResponseStatusKey(responseStatus)] = responseObject;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
function toResponseStatusKey(status) {
|
|
542
|
+
return String(status);
|
|
543
|
+
}
|
|
544
|
+
function extractDetailedResponseParts(schema, defaultStatus) {
|
|
545
|
+
const partsByStatus = /* @__PURE__ */ new Map();
|
|
546
|
+
for (const item of flattenJsonUnionSchema(schema)) {
|
|
547
|
+
const entries = extractJsonObjectSchemaEntries(item);
|
|
548
|
+
if (!entries) {
|
|
549
|
+
throw new OpenAPIGeneratorError(
|
|
550
|
+
`outputStructure is "detailed" but the output schema (or one of its union members) is not an object.
|
|
551
|
+
Expected each member shaped like: { status?: number (< 400), headers?: object, body?: unknown }`
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
const statusSchema = entries.find(([name]) => name === "status")?.[1];
|
|
555
|
+
if (statusSchema !== void 0 && (typeof statusSchema !== "object" || !Number.isInteger(statusSchema.const) || statusSchema.const >= 400)) {
|
|
556
|
+
throw new OpenAPIGeneratorError(
|
|
557
|
+
`invalid "status" field in the detailed output schema.
|
|
558
|
+
Expected: a literal (const) integer below 400
|
|
559
|
+
Received: ${stringifyJSON(statusSchema)}`
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
const status = (statusSchema?.const || void 0) ?? defaultStatus;
|
|
563
|
+
let parts = partsByStatus.get(status);
|
|
564
|
+
if (!parts) {
|
|
565
|
+
parts = { descriptions: [], bodies: [], headers: [] };
|
|
566
|
+
partsByStatus.set(status, parts);
|
|
567
|
+
}
|
|
568
|
+
if (statusSchema?.description !== void 0) {
|
|
569
|
+
parts.descriptions.push(statusSchema.description);
|
|
570
|
+
}
|
|
571
|
+
const headersSchema = entries.find(([name]) => name === "headers")?.[1];
|
|
572
|
+
if (headersSchema !== void 0) {
|
|
573
|
+
parts.headers.push(headersSchema);
|
|
574
|
+
}
|
|
575
|
+
const bodySchema = entries.find(([name]) => name === "body")?.[1];
|
|
576
|
+
if (bodySchema !== void 0) {
|
|
577
|
+
parts.bodies.push(bodySchema);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return partsByStatus;
|
|
581
|
+
}
|
|
582
|
+
function buildErrorResponse(ctx, operation, def) {
|
|
583
|
+
const definitionsByStatus = /* @__PURE__ */ new Map();
|
|
584
|
+
for (const code of Object.keys(def.errorMap)) {
|
|
585
|
+
const config = def.errorMap[code];
|
|
586
|
+
if (!config) {
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
const status = ctx.errorStatusMap[code] ?? DEFAULT_ERROR_STATUS;
|
|
590
|
+
const [dataJsonSchema, dataOptional] = ctx.convertSchemas(config.data !== void 0 ? [config.data] : void 0, "output");
|
|
591
|
+
let definitions = definitionsByStatus.get(status);
|
|
592
|
+
if (!definitions) {
|
|
593
|
+
definitions = [];
|
|
594
|
+
definitionsByStatus.set(status, definitions);
|
|
595
|
+
}
|
|
596
|
+
definitions.push({ code, dataJsonSchema, dataOptional, defaultMessage: config.message });
|
|
597
|
+
}
|
|
598
|
+
if (!definitionsByStatus.size) {
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
const undefinedErrorSchema = ctx.registry.register("UndefinedError", {
|
|
602
|
+
title: "UndefinedError",
|
|
603
|
+
type: "object",
|
|
604
|
+
properties: {
|
|
605
|
+
defined: { const: false },
|
|
606
|
+
code: { type: "string" },
|
|
607
|
+
message: { type: "string" },
|
|
608
|
+
data: {}
|
|
609
|
+
},
|
|
610
|
+
required: ["defined", "code", "message"]
|
|
611
|
+
});
|
|
612
|
+
for (const [status, definitions] of definitionsByStatus.entries()) {
|
|
613
|
+
const descriptions = definitions.map(({ defaultMessage }) => defaultMessage).filter((m) => m !== void 0);
|
|
614
|
+
const customBodySchema = value(
|
|
615
|
+
ctx.customErrorResponseBodySchema,
|
|
616
|
+
definitions.map((def2) => ({ ...def2, dataJsonSchema: ctx.registry.hoistDefs(def2.dataJsonSchema, "output") })),
|
|
617
|
+
status
|
|
618
|
+
);
|
|
619
|
+
const responseSchema = customBodySchema ?? combineJsonSchemasWithComposition("oneOf", [
|
|
620
|
+
...definitions.map(({ code, dataJsonSchema, dataOptional }) => {
|
|
621
|
+
return ctx.registry.register(toErrorComponentName(code), {
|
|
622
|
+
title: code,
|
|
623
|
+
...combineJsonObjectSchemaEntries([
|
|
624
|
+
["defined", { const: true }, false],
|
|
625
|
+
["code", { const: code }, false],
|
|
626
|
+
// avoid using the defaultMessage here to improve component reusability
|
|
627
|
+
["message", { type: "string" }, false],
|
|
628
|
+
["data", ctx.registry.hoistDefs(dataJsonSchema, "output"), dataOptional]
|
|
629
|
+
])
|
|
630
|
+
});
|
|
631
|
+
}),
|
|
632
|
+
undefinedErrorSchema
|
|
633
|
+
]);
|
|
634
|
+
operation.responses ??= {};
|
|
635
|
+
operation.responses[toResponseStatusKey(status)] = {
|
|
636
|
+
description: descriptions.length ? descriptions.join(", ") : status.toString(),
|
|
637
|
+
content: {
|
|
638
|
+
"application/json": {
|
|
639
|
+
schema: ctx.registry.toOpenAPISchema(responseSchema, "output")
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
function toErrorComponentName(code) {
|
|
646
|
+
const name = code.split(/[^a-z0-9]+/i).filter(Boolean).map((part) => {
|
|
647
|
+
const rest = part.slice(1);
|
|
648
|
+
return part.charAt(0).toUpperCase() + (rest === rest.toUpperCase() ? rest.toLowerCase() : rest);
|
|
649
|
+
}).join("");
|
|
650
|
+
return name || "Error";
|
|
651
|
+
}
|
|
652
|
+
function getAsyncIteratorObjectDetails(schemas) {
|
|
653
|
+
if (!schemas || schemas.length === 0) {
|
|
654
|
+
return void 0;
|
|
655
|
+
}
|
|
656
|
+
const yieldSchemas = [];
|
|
657
|
+
const returnSchemas = [];
|
|
658
|
+
for (const s of schemas) {
|
|
659
|
+
const details = getAsyncIteratorObjectSchemaDetails(s);
|
|
660
|
+
if (!details) {
|
|
661
|
+
return void 0;
|
|
662
|
+
}
|
|
663
|
+
yieldSchemas.push(details.yieldSchema);
|
|
664
|
+
if (details.returnSchema) {
|
|
665
|
+
returnSchemas.push(details.returnSchema);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return [yieldSchemas, returnSchemas];
|
|
669
|
+
}
|
|
670
|
+
function toAsyncIteratorObjectContent(ctx, direction, [yieldSchema, yieldOptional], [returnSchema, returnOptional]) {
|
|
671
|
+
const schema = combineJsonSchemasWithComposition("oneOf", [
|
|
672
|
+
combineJsonObjectSchemaEntries([
|
|
673
|
+
["event", { const: "message" }, false],
|
|
674
|
+
["data", yieldSchema, yieldOptional],
|
|
675
|
+
["id", { type: "string" }, true],
|
|
676
|
+
["retry", { type: "number" }, true]
|
|
677
|
+
]),
|
|
678
|
+
combineJsonObjectSchemaEntries([
|
|
679
|
+
["event", { const: "close" }, false],
|
|
680
|
+
["data", returnSchema, returnOptional],
|
|
681
|
+
["id", { type: "string" }, true],
|
|
682
|
+
["retry", { type: "number" }, true]
|
|
683
|
+
]),
|
|
684
|
+
{
|
|
685
|
+
type: "object",
|
|
686
|
+
properties: {
|
|
687
|
+
event: { const: "error" },
|
|
688
|
+
data: {},
|
|
689
|
+
id: { type: "string" },
|
|
690
|
+
retry: { type: "number" }
|
|
691
|
+
},
|
|
692
|
+
required: ["event"]
|
|
693
|
+
}
|
|
694
|
+
]);
|
|
695
|
+
return {
|
|
696
|
+
"text/event-stream": {
|
|
697
|
+
schema: ctx.registry.toOpenAPISchema(schema, direction)
|
|
698
|
+
}
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
function toBodyContent(ctx, direction, schema) {
|
|
702
|
+
const fileSchemasByMediaType = /* @__PURE__ */ new Map();
|
|
703
|
+
const rest = flattenJsonUnionSchema(schema).filter((s) => {
|
|
704
|
+
if (!isJsonFileSchema(s)) {
|
|
705
|
+
return !isUnconstrainedSchema(s);
|
|
706
|
+
}
|
|
707
|
+
const contentMediaType = s.contentMediaType ?? "*/*";
|
|
708
|
+
const schemas = fileSchemasByMediaType.get(contentMediaType);
|
|
709
|
+
if (schemas) {
|
|
710
|
+
schemas.push(s);
|
|
711
|
+
} else {
|
|
712
|
+
fileSchemasByMediaType.set(contentMediaType, [s]);
|
|
713
|
+
}
|
|
714
|
+
return false;
|
|
715
|
+
});
|
|
716
|
+
const content = {};
|
|
717
|
+
if (rest.length > 0) {
|
|
718
|
+
const restSchema = fileSchemasByMediaType.size ? combineJsonSchemasWithComposition("anyOf", rest) : schema;
|
|
719
|
+
const hasNestedFiles = findDeepMatches(
|
|
720
|
+
(v) => isPlainObject(v) && isJsonFileSchema(v),
|
|
721
|
+
restSchema
|
|
722
|
+
).values.length > 0;
|
|
723
|
+
const contentType = hasNestedFiles ? "multipart/form-data" : "application/json";
|
|
724
|
+
const fileSchemas = fileSchemasByMediaType.get(contentType);
|
|
725
|
+
fileSchemasByMediaType.delete(contentType);
|
|
726
|
+
content[contentType] = {
|
|
727
|
+
schema: ctx.registry.toOpenAPISchema(combineJsonSchemasWithComposition("anyOf", [restSchema, ...fileSchemas ?? []]), direction)
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
for (const [contentType, schemas] of fileSchemasByMediaType.entries()) {
|
|
731
|
+
content[contentType] = {
|
|
732
|
+
schema: ctx.registry.toOpenAPISchema(combineJsonSchemasWithComposition("anyOf", schemas), direction)
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
return content;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
class OpenAPIGenerator {
|
|
739
|
+
serializer;
|
|
740
|
+
converter;
|
|
741
|
+
constructor(options = {}) {
|
|
742
|
+
this.serializer = options.serializer ?? new OpenAPISerializer();
|
|
743
|
+
this.converter = new DelegatingJsonSchemaConverter([
|
|
744
|
+
...toArray(options.converters),
|
|
745
|
+
new StandardJsonSchemaConverter()
|
|
746
|
+
]);
|
|
747
|
+
}
|
|
748
|
+
async generate(router, options = {}) {
|
|
749
|
+
const version = options.version ?? "3.2.0";
|
|
750
|
+
const doc = {
|
|
751
|
+
...clone(options.base),
|
|
752
|
+
openapi: "3.2.0",
|
|
753
|
+
info: options.base?.info ?? { title: "API Reference", version: "0.0.0" }
|
|
754
|
+
};
|
|
755
|
+
const ctx = {
|
|
756
|
+
registry: new OpenAPIComponentRegistry(doc, options.customComponentName),
|
|
757
|
+
convertSchemas: (schemas, direction) => this.convertSchemas(schemas, direction),
|
|
758
|
+
errorStatusMap: options.errorStatusMap ?? COMMON_ERROR_STATUS_MAP,
|
|
759
|
+
customErrorResponseBodySchema: options.customErrorResponseBodySchema
|
|
760
|
+
};
|
|
761
|
+
const errors = [];
|
|
762
|
+
await walkProcedureContractsAsync(router, (contract, path) => {
|
|
763
|
+
if (value(options.filter, contract, path) === false) {
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
try {
|
|
767
|
+
const def = contract["~orpc"];
|
|
768
|
+
const meta = getOpenAPIMeta(contract);
|
|
769
|
+
const method = (meta?.method ?? DEFAULT_OPENAPI_METHOD).toLowerCase();
|
|
770
|
+
if (method === "query" && !version.startsWith("3.2.")) {
|
|
771
|
+
throw new OpenAPIGeneratorError(
|
|
772
|
+
`QUERY operations require OpenAPI 3.2, but version "${version}" was requested.`
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
const postPath = meta?.path ?? pathToHttpPath(path);
|
|
776
|
+
const httpPath = meta?.prefix ? mergeHttpPath(meta.prefix, postPath) : postPath;
|
|
777
|
+
const dynamicPathParams = getDynamicPathParams(httpPath);
|
|
778
|
+
const openApiPath = toOpenAPIPath(httpPath, dynamicPathParams);
|
|
779
|
+
let operation;
|
|
780
|
+
if (meta?.spec !== void 0 && typeof meta.spec !== "function") {
|
|
781
|
+
operation = meta.spec;
|
|
782
|
+
} else {
|
|
783
|
+
operation = {
|
|
784
|
+
operationId: meta?.operationId ?? path.join("."),
|
|
785
|
+
summary: meta?.summary,
|
|
786
|
+
description: meta?.description,
|
|
787
|
+
deprecated: meta?.deprecated,
|
|
788
|
+
tags: meta?.tags?.map((tag) => tag)
|
|
789
|
+
};
|
|
790
|
+
buildRequest(ctx, operation, def, meta, dynamicPathParams);
|
|
791
|
+
buildSuccessResponse(ctx, operation, def, meta);
|
|
792
|
+
buildErrorResponse(ctx, operation, def);
|
|
793
|
+
}
|
|
794
|
+
if (typeof meta?.spec === "function") {
|
|
795
|
+
operation = meta.spec(operation);
|
|
796
|
+
}
|
|
797
|
+
doc.paths ??= {};
|
|
798
|
+
doc.paths[openApiPath] ??= {};
|
|
799
|
+
doc.paths[openApiPath][method] = operation;
|
|
800
|
+
} catch (e) {
|
|
801
|
+
if (!(e instanceof OpenAPIGeneratorError)) {
|
|
802
|
+
throw e;
|
|
803
|
+
}
|
|
804
|
+
errors.push(`Procedure at ${path.join(".") || "(root)"}: ${e.message}`);
|
|
805
|
+
}
|
|
806
|
+
});
|
|
807
|
+
if (errors.length) {
|
|
808
|
+
throw new OpenAPIGeneratorError(
|
|
809
|
+
`[OpenAPIGenerator] Failed to generate the OpenAPI document (${errors.length} error${errors.length === 1 ? "" : "s"}):
|
|
810
|
+
|
|
811
|
+
${errors.join("\n\n")}`
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
const versioned = toVersionedOpenAPIDocument(doc, version);
|
|
815
|
+
return this.serializer.serialize(versioned, { asFormData: false, useFormDataForBlobFields: false });
|
|
816
|
+
}
|
|
817
|
+
convertSchema(schema, direction) {
|
|
818
|
+
const [jsonSchema, optional] = this.converter.convert(schema, direction);
|
|
819
|
+
return [strip$schemaField(jsonSchema), optional];
|
|
820
|
+
}
|
|
821
|
+
convertSchemas(schemas, direction) {
|
|
822
|
+
if (!schemas || schemas.length <= 1) {
|
|
823
|
+
return this.convertSchema(schemas?.[0], direction);
|
|
824
|
+
}
|
|
825
|
+
const results = schemas.map((s) => this.convertSchema(s, direction));
|
|
826
|
+
return [
|
|
827
|
+
combineJsonSchemasWithComposition("allOf", results.map(([jsonSchema]) => jsonSchema)),
|
|
828
|
+
results.every(([, optional]) => optional)
|
|
829
|
+
];
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
function toVersionedOpenAPIDocument(doc, version) {
|
|
833
|
+
const downgraded = version.startsWith("3.2.") ? doc : version.startsWith("3.1.") ? downgradeSpecV32ToV31(doc) : downgradeSpecV31ToV30(downgradeSpecV32ToV31(doc));
|
|
834
|
+
return { ...downgraded, openapi: version };
|
|
835
|
+
}
|
|
836
|
+
function strip$schemaField(schema) {
|
|
837
|
+
if (typeof schema !== "object") {
|
|
838
|
+
return schema;
|
|
839
|
+
}
|
|
840
|
+
const { $schema, ...rest } = schema;
|
|
841
|
+
return rest;
|
|
842
|
+
}
|
|
843
|
+
|
|
900
844
|
function populateRouterContractOpenAPIPaths(router, options = {}) {
|
|
901
845
|
const path = toArray(options.path);
|
|
902
846
|
if (router instanceof ProcedureContract) {
|
|
@@ -918,7 +862,7 @@ function populateRouterContractOpenAPIPaths(router, options = {}) {
|
|
|
918
862
|
return router;
|
|
919
863
|
}
|
|
920
864
|
const populated = {};
|
|
921
|
-
for (const key
|
|
865
|
+
for (const key of Object.keys(router)) {
|
|
922
866
|
populated[key] = populateRouterContractOpenAPIPaths(
|
|
923
867
|
router[key],
|
|
924
868
|
{ ...options, path: [...path, key] }
|
|
@@ -927,4 +871,4 @@ function populateRouterContractOpenAPIPaths(router, options = {}) {
|
|
|
927
871
|
return populated;
|
|
928
872
|
}
|
|
929
873
|
|
|
930
|
-
export { DEFAULT_OPENAPI_INPUT_STRUCTURE, DEFAULT_OPENAPI_METHOD, DEFAULT_OPENAPI_OUTPUT_STRUCTURE, DEFAULT_OPENAPI_SUCCESS_DESCRIPTION, OpenAPIGenerator, OpenAPIGeneratorError, OpenAPISerializer,
|
|
874
|
+
export { DEFAULT_OPENAPI_INPUT_STRUCTURE, DEFAULT_OPENAPI_METHOD, DEFAULT_OPENAPI_OUTPUT_STRUCTURE, DEFAULT_OPENAPI_SUCCESS_DESCRIPTION, OpenAPIGenerator, OpenAPIGeneratorError, OpenAPISerializer, createContractJsonifiedClientFactory, getDynamicPathParams, getOpenAPIMeta, isBodylessMethod, openapi, populateRouterContractOpenAPIPaths };
|