@orpc/openapi 0.0.0-next.a2fc015 → 0.0.0-next.a320605
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 +7 -7
- package/dist/adapters/aws-lambda/index.d.mts +19 -0
- package/dist/adapters/aws-lambda/index.d.ts +19 -0
- package/dist/adapters/aws-lambda/index.mjs +18 -0
- package/dist/adapters/fetch/index.d.mts +4 -2
- package/dist/adapters/fetch/index.d.ts +4 -2
- package/dist/adapters/fetch/index.mjs +2 -1
- package/dist/adapters/node/index.d.mts +4 -2
- package/dist/adapters/node/index.d.ts +4 -2
- package/dist/adapters/node/index.mjs +2 -1
- package/dist/adapters/standard/index.d.mts +6 -11
- package/dist/adapters/standard/index.d.ts +6 -11
- package/dist/adapters/standard/index.mjs +2 -1
- package/dist/index.d.mts +24 -13
- package/dist/index.d.ts +24 -13
- package/dist/index.mjs +3 -3
- package/dist/plugins/index.d.mts +10 -11
- package/dist/plugins/index.d.ts +10 -11
- package/dist/plugins/index.mjs +36 -25
- package/dist/shared/{openapi.p5tsmBXx.mjs → openapi.BVXcB0u4.mjs} +39 -10
- package/dist/shared/openapi.CQmjvnb0.d.mts +31 -0
- package/dist/shared/openapi.CQmjvnb0.d.ts +31 -0
- package/dist/shared/{openapi.fMEQd3Yd.mjs → openapi.DRqtR2lw.mjs} +278 -72
- package/dist/shared/openapi.TOD62C7O.d.mts +108 -0
- package/dist/shared/openapi.TOD62C7O.d.ts +108 -0
- package/package.json +15 -11
- package/dist/shared/openapi.D3j94c9n.d.mts +0 -12
- package/dist/shared/openapi.D3j94c9n.d.ts +0 -12
- package/dist/shared/openapi.DP97kr00.d.mts +0 -47
- package/dist/shared/openapi.DP97kr00.d.ts +0 -47
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { fallbackORPCErrorStatus, fallbackORPCErrorMessage } from '@orpc/client';
|
|
1
|
+
import { isORPCErrorStatus, fallbackORPCErrorStatus, fallbackORPCErrorMessage } from '@orpc/client';
|
|
2
2
|
import { toHttpPath } from '@orpc/client/standard';
|
|
3
3
|
import { fallbackContractConfig, getEventIteratorSchemaDetails } from '@orpc/contract';
|
|
4
4
|
import { standardizeHTTPPath, StandardOpenAPIJsonSerializer, getDynamicParams } from '@orpc/openapi-client/standard';
|
|
5
5
|
import { isProcedure, resolveContractProcedures } from '@orpc/server';
|
|
6
|
-
import { isObject, findDeepMatches, toArray, clone } from '@orpc/shared';
|
|
7
|
-
import 'json-schema-typed/draft-2020-12';
|
|
6
|
+
import { isObject, stringifyJSON, findDeepMatches, toArray, clone, value } from '@orpc/shared';
|
|
7
|
+
import { TypeName } from '@orpc/json-schema-typed/draft-2020-12';
|
|
8
8
|
|
|
9
9
|
const OPERATION_EXTENDER_SYMBOL = Symbol("ORPC_OPERATION_EXTENDER");
|
|
10
10
|
function customOpenAPIOperation(o, extend) {
|
|
@@ -114,13 +114,18 @@ function isAnySchema(schema) {
|
|
|
114
114
|
return false;
|
|
115
115
|
}
|
|
116
116
|
function separateObjectSchema(schema, separatedProperties) {
|
|
117
|
-
if (Object.keys(schema).some(
|
|
117
|
+
if (Object.keys(schema).some(
|
|
118
|
+
(k) => !["type", "properties", "required", "additionalProperties"].includes(k) && LOGIC_KEYWORDS.includes(k) && schema[k] !== void 0
|
|
119
|
+
)) {
|
|
118
120
|
return [{ type: "object" }, schema];
|
|
119
121
|
}
|
|
120
122
|
const matched = { ...schema };
|
|
121
123
|
const rest = { ...schema };
|
|
122
|
-
matched.properties =
|
|
123
|
-
|
|
124
|
+
matched.properties = separatedProperties.reduce((acc, key) => {
|
|
125
|
+
const keySchema = schema.properties?.[key] ?? schema.additionalProperties;
|
|
126
|
+
if (keySchema !== void 0) {
|
|
127
|
+
acc[key] = keySchema;
|
|
128
|
+
}
|
|
124
129
|
return acc;
|
|
125
130
|
}, {});
|
|
126
131
|
matched.required = schema.required?.filter((key) => separatedProperties.includes(key));
|
|
@@ -184,6 +189,57 @@ function applySchemaOptionality(required, schema) {
|
|
|
184
189
|
]
|
|
185
190
|
};
|
|
186
191
|
}
|
|
192
|
+
function expandUnionSchema(schema) {
|
|
193
|
+
if (typeof schema === "object") {
|
|
194
|
+
for (const keyword of ["anyOf", "oneOf"]) {
|
|
195
|
+
if (schema[keyword] && Object.keys(schema).every(
|
|
196
|
+
(k) => k === keyword || !LOGIC_KEYWORDS.includes(k)
|
|
197
|
+
)) {
|
|
198
|
+
return schema[keyword].flatMap((s) => expandUnionSchema(s));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return [schema];
|
|
203
|
+
}
|
|
204
|
+
function expandArrayableSchema(schema) {
|
|
205
|
+
const schemas = expandUnionSchema(schema);
|
|
206
|
+
if (schemas.length !== 2) {
|
|
207
|
+
return void 0;
|
|
208
|
+
}
|
|
209
|
+
const arraySchema = schemas.find(
|
|
210
|
+
(s) => typeof s === "object" && s.type === "array" && Object.keys(s).filter((k) => LOGIC_KEYWORDS.includes(k)).every((k) => k === "type" || k === "items")
|
|
211
|
+
);
|
|
212
|
+
if (arraySchema === void 0) {
|
|
213
|
+
return void 0;
|
|
214
|
+
}
|
|
215
|
+
const items1 = arraySchema.items;
|
|
216
|
+
const items2 = schemas.find((s) => s !== arraySchema);
|
|
217
|
+
if (stringifyJSON(items1) !== stringifyJSON(items2)) {
|
|
218
|
+
return void 0;
|
|
219
|
+
}
|
|
220
|
+
return [items2, arraySchema];
|
|
221
|
+
}
|
|
222
|
+
const PRIMITIVE_SCHEMA_TYPES = /* @__PURE__ */ new Set([
|
|
223
|
+
TypeName.String,
|
|
224
|
+
TypeName.Number,
|
|
225
|
+
TypeName.Integer,
|
|
226
|
+
TypeName.Boolean,
|
|
227
|
+
TypeName.Null
|
|
228
|
+
]);
|
|
229
|
+
function isPrimitiveSchema(schema) {
|
|
230
|
+
return expandUnionSchema(schema).every((s) => {
|
|
231
|
+
if (typeof s === "boolean") {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
if (typeof s.type === "string" && PRIMITIVE_SCHEMA_TYPES.has(s.type)) {
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
if (s.const !== void 0) {
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
return false;
|
|
241
|
+
});
|
|
242
|
+
}
|
|
187
243
|
|
|
188
244
|
function toOpenAPIPath(path) {
|
|
189
245
|
return standardizeHTTPPath(path).replace(/\/\{\+([^}]+)\}/g, "/{$1}");
|
|
@@ -256,13 +312,26 @@ function toOpenAPIParameters(schema, parameterIn) {
|
|
|
256
312
|
const parameters = [];
|
|
257
313
|
for (const key in schema.properties) {
|
|
258
314
|
const keySchema = schema.properties[key];
|
|
315
|
+
let isDeepObjectStyle = true;
|
|
316
|
+
if (parameterIn !== "query") {
|
|
317
|
+
isDeepObjectStyle = false;
|
|
318
|
+
} else if (isPrimitiveSchema(keySchema)) {
|
|
319
|
+
isDeepObjectStyle = false;
|
|
320
|
+
} else {
|
|
321
|
+
const [item] = expandArrayableSchema(keySchema) ?? [];
|
|
322
|
+
if (item !== void 0 && isPrimitiveSchema(item)) {
|
|
323
|
+
isDeepObjectStyle = false;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
259
326
|
parameters.push({
|
|
260
327
|
name: key,
|
|
261
328
|
in: parameterIn,
|
|
262
329
|
required: schema.required?.includes(key),
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
330
|
+
schema: toOpenAPISchema(keySchema),
|
|
331
|
+
style: isDeepObjectStyle ? "deepObject" : void 0,
|
|
332
|
+
explode: isDeepObjectStyle ? true : void 0,
|
|
333
|
+
allowEmptyValue: parameterIn === "query" ? true : void 0,
|
|
334
|
+
allowReserved: parameterIn === "query" ? true : void 0
|
|
266
335
|
});
|
|
267
336
|
}
|
|
268
337
|
return parameters;
|
|
@@ -281,6 +350,15 @@ function checkParamsSchema(schema, params) {
|
|
|
281
350
|
function toOpenAPISchema(schema) {
|
|
282
351
|
return schema === true ? {} : schema === false ? { not: {} } : schema;
|
|
283
352
|
}
|
|
353
|
+
const OPENAPI_JSON_SCHEMA_REF_PREFIX = "#/components/schemas/";
|
|
354
|
+
function resolveOpenAPIJsonSchemaRef(doc, schema) {
|
|
355
|
+
if (typeof schema !== "object" || !schema.$ref?.startsWith(OPENAPI_JSON_SCHEMA_REF_PREFIX)) {
|
|
356
|
+
return schema;
|
|
357
|
+
}
|
|
358
|
+
const name = schema.$ref.slice(OPENAPI_JSON_SCHEMA_REF_PREFIX.length);
|
|
359
|
+
const resolved = doc.components?.schemas?.[name];
|
|
360
|
+
return resolved ?? schema;
|
|
361
|
+
}
|
|
284
362
|
|
|
285
363
|
class CompositeSchemaConverter {
|
|
286
364
|
converters;
|
|
@@ -312,32 +390,50 @@ class OpenAPIGenerator {
|
|
|
312
390
|
* @see {@link https://orpc.unnoq.com/docs/openapi/openapi-specification OpenAPI Specification Docs}
|
|
313
391
|
*/
|
|
314
392
|
async generate(router, options = {}) {
|
|
393
|
+
const filter = options.filter ?? (({ contract, path }) => {
|
|
394
|
+
return !(options.exclude?.(contract, path) ?? false);
|
|
395
|
+
});
|
|
315
396
|
const doc = {
|
|
316
397
|
...clone(options),
|
|
317
398
|
info: options.info ?? { title: "API Reference", version: "0.0.0" },
|
|
318
|
-
openapi: "3.1.1"
|
|
399
|
+
openapi: "3.1.1",
|
|
400
|
+
exclude: void 0,
|
|
401
|
+
filter: void 0,
|
|
402
|
+
commonSchemas: void 0
|
|
319
403
|
};
|
|
404
|
+
const { baseSchemaConvertOptions, undefinedErrorJsonSchema } = await this.#resolveCommonSchemas(doc, options.commonSchemas);
|
|
320
405
|
const contracts = [];
|
|
321
|
-
await resolveContractProcedures({ path: [], router }, (
|
|
322
|
-
|
|
406
|
+
await resolveContractProcedures({ path: [], router }, (traverseOptions) => {
|
|
407
|
+
if (!value(filter, traverseOptions)) {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
contracts.push(traverseOptions);
|
|
323
411
|
});
|
|
324
412
|
const errors = [];
|
|
325
413
|
for (const { contract, path } of contracts) {
|
|
326
|
-
const
|
|
414
|
+
const stringPath = path.join(".");
|
|
327
415
|
try {
|
|
328
416
|
const def = contract["~orpc"];
|
|
329
417
|
const method = toOpenAPIMethod(fallbackContractConfig("defaultMethod", def.route.method));
|
|
330
418
|
const httpPath = toOpenAPIPath(def.route.path ?? toHttpPath(path));
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
419
|
+
let operationObjectRef;
|
|
420
|
+
if (def.route.spec !== void 0 && typeof def.route.spec !== "function") {
|
|
421
|
+
operationObjectRef = def.route.spec;
|
|
422
|
+
} else {
|
|
423
|
+
operationObjectRef = {
|
|
424
|
+
operationId: def.route.operationId ?? stringPath,
|
|
425
|
+
summary: def.route.summary,
|
|
426
|
+
description: def.route.description,
|
|
427
|
+
deprecated: def.route.deprecated,
|
|
428
|
+
tags: def.route.tags?.map((tag) => tag)
|
|
429
|
+
};
|
|
430
|
+
await this.#request(doc, operationObjectRef, def, baseSchemaConvertOptions);
|
|
431
|
+
await this.#successResponse(doc, operationObjectRef, def, baseSchemaConvertOptions);
|
|
432
|
+
await this.#errorResponse(operationObjectRef, def, baseSchemaConvertOptions, undefinedErrorJsonSchema);
|
|
433
|
+
}
|
|
434
|
+
if (typeof def.route.spec === "function") {
|
|
435
|
+
operationObjectRef = def.route.spec(operationObjectRef);
|
|
436
|
+
}
|
|
341
437
|
doc.paths ??= {};
|
|
342
438
|
doc.paths[httpPath] ??= {};
|
|
343
439
|
doc.paths[httpPath][method] = applyCustomOpenAPIOperation(operationObjectRef, contract);
|
|
@@ -346,7 +442,7 @@ class OpenAPIGenerator {
|
|
|
346
442
|
throw e;
|
|
347
443
|
}
|
|
348
444
|
errors.push(
|
|
349
|
-
`[OpenAPIGenerator] Error occurred while generating OpenAPI for procedure at path: ${
|
|
445
|
+
`[OpenAPIGenerator] Error occurred while generating OpenAPI for procedure at path: ${stringPath}
|
|
350
446
|
${e.message}`
|
|
351
447
|
);
|
|
352
448
|
}
|
|
@@ -360,22 +456,96 @@ ${errors.join("\n\n")}`
|
|
|
360
456
|
}
|
|
361
457
|
return this.serializer.serialize(doc)[0];
|
|
362
458
|
}
|
|
363
|
-
async #
|
|
459
|
+
async #resolveCommonSchemas(doc, commonSchemas) {
|
|
460
|
+
let undefinedErrorJsonSchema = {
|
|
461
|
+
type: "object",
|
|
462
|
+
properties: {
|
|
463
|
+
defined: { const: false },
|
|
464
|
+
code: { type: "string" },
|
|
465
|
+
status: { type: "number" },
|
|
466
|
+
message: { type: "string" },
|
|
467
|
+
data: {}
|
|
468
|
+
},
|
|
469
|
+
required: ["defined", "code", "status", "message"]
|
|
470
|
+
};
|
|
471
|
+
const baseSchemaConvertOptions = {};
|
|
472
|
+
if (commonSchemas) {
|
|
473
|
+
baseSchemaConvertOptions.components = [];
|
|
474
|
+
for (const key in commonSchemas) {
|
|
475
|
+
const options = commonSchemas[key];
|
|
476
|
+
if (options.schema === void 0) {
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
const { schema, strategy = "input" } = options;
|
|
480
|
+
const [required, json] = await this.converter.convert(schema, { strategy });
|
|
481
|
+
const allowedStrategies = [strategy];
|
|
482
|
+
if (strategy === "input") {
|
|
483
|
+
const [outputRequired, outputJson] = await this.converter.convert(schema, { strategy: "output" });
|
|
484
|
+
if (outputRequired === required && stringifyJSON(outputJson) === stringifyJSON(json)) {
|
|
485
|
+
allowedStrategies.push("output");
|
|
486
|
+
}
|
|
487
|
+
} else if (strategy === "output") {
|
|
488
|
+
const [inputRequired, inputJson] = await this.converter.convert(schema, { strategy: "input" });
|
|
489
|
+
if (inputRequired === required && stringifyJSON(inputJson) === stringifyJSON(json)) {
|
|
490
|
+
allowedStrategies.push("input");
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
baseSchemaConvertOptions.components.push({
|
|
494
|
+
schema,
|
|
495
|
+
required,
|
|
496
|
+
ref: `#/components/schemas/${key}`,
|
|
497
|
+
allowedStrategies
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
doc.components ??= {};
|
|
501
|
+
doc.components.schemas ??= {};
|
|
502
|
+
for (const key in commonSchemas) {
|
|
503
|
+
const options = commonSchemas[key];
|
|
504
|
+
if (options.schema === void 0) {
|
|
505
|
+
if (options.error === "UndefinedError") {
|
|
506
|
+
doc.components.schemas[key] = toOpenAPISchema(undefinedErrorJsonSchema);
|
|
507
|
+
undefinedErrorJsonSchema = { $ref: `#/components/schemas/${key}` };
|
|
508
|
+
}
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
const { schema, strategy = "input" } = options;
|
|
512
|
+
const [, json] = await this.converter.convert(
|
|
513
|
+
schema,
|
|
514
|
+
{
|
|
515
|
+
...baseSchemaConvertOptions,
|
|
516
|
+
strategy,
|
|
517
|
+
minStructureDepthForRef: 1
|
|
518
|
+
// not allow use $ref for root schemas
|
|
519
|
+
}
|
|
520
|
+
);
|
|
521
|
+
doc.components.schemas[key] = toOpenAPISchema(json);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return { baseSchemaConvertOptions, undefinedErrorJsonSchema };
|
|
525
|
+
}
|
|
526
|
+
async #request(doc, ref, def, baseSchemaConvertOptions) {
|
|
364
527
|
const method = fallbackContractConfig("defaultMethod", def.route.method);
|
|
365
528
|
const details = getEventIteratorSchemaDetails(def.inputSchema);
|
|
366
529
|
if (details) {
|
|
367
530
|
ref.requestBody = {
|
|
368
531
|
required: true,
|
|
369
532
|
content: toOpenAPIEventIteratorContent(
|
|
370
|
-
await this.converter.convert(details.yields, { strategy: "input" }),
|
|
371
|
-
await this.converter.convert(details.returns, { strategy: "input" })
|
|
533
|
+
await this.converter.convert(details.yields, { ...baseSchemaConvertOptions, strategy: "input" }),
|
|
534
|
+
await this.converter.convert(details.returns, { ...baseSchemaConvertOptions, strategy: "input" })
|
|
372
535
|
)
|
|
373
536
|
};
|
|
374
537
|
return;
|
|
375
538
|
}
|
|
376
539
|
const dynamicParams = getDynamicParams(def.route.path)?.map((v) => v.name);
|
|
377
540
|
const inputStructure = fallbackContractConfig("defaultInputStructure", def.route.inputStructure);
|
|
378
|
-
let [required, schema] = await this.converter.convert(
|
|
541
|
+
let [required, schema] = await this.converter.convert(
|
|
542
|
+
def.inputSchema,
|
|
543
|
+
{
|
|
544
|
+
...baseSchemaConvertOptions,
|
|
545
|
+
strategy: "input",
|
|
546
|
+
minStructureDepthForRef: dynamicParams?.length || inputStructure === "detailed" ? 1 : 0
|
|
547
|
+
}
|
|
548
|
+
);
|
|
379
549
|
if (isAnySchema(schema) && !dynamicParams?.length) {
|
|
380
550
|
return;
|
|
381
551
|
}
|
|
@@ -418,7 +588,8 @@ ${errors.join("\n\n")}`
|
|
|
418
588
|
if (!isObjectSchema(schema)) {
|
|
419
589
|
throw error;
|
|
420
590
|
}
|
|
421
|
-
|
|
591
|
+
const resolvedParamSchema = schema.properties?.params !== void 0 ? resolveOpenAPIJsonSchemaRef(doc, schema.properties.params) : void 0;
|
|
592
|
+
if (dynamicParams?.length && (resolvedParamSchema === void 0 || !isObjectSchema(resolvedParamSchema) || !checkParamsSchema(resolvedParamSchema, dynamicParams))) {
|
|
422
593
|
throw new OpenAPIGeneratorError(
|
|
423
594
|
'When input structure is "detailed" and path has dynamic params, the "params" schema must be an object with all dynamic params as required.'
|
|
424
595
|
);
|
|
@@ -426,12 +597,13 @@ ${errors.join("\n\n")}`
|
|
|
426
597
|
for (const from of ["params", "query", "headers"]) {
|
|
427
598
|
const fromSchema = schema.properties?.[from];
|
|
428
599
|
if (fromSchema !== void 0) {
|
|
429
|
-
|
|
600
|
+
const resolvedSchema = resolveOpenAPIJsonSchemaRef(doc, fromSchema);
|
|
601
|
+
if (!isObjectSchema(resolvedSchema)) {
|
|
430
602
|
throw error;
|
|
431
603
|
}
|
|
432
604
|
const parameterIn = from === "params" ? "path" : from === "headers" ? "header" : "query";
|
|
433
605
|
ref.parameters ??= [];
|
|
434
|
-
ref.parameters.push(...toOpenAPIParameters(
|
|
606
|
+
ref.parameters.push(...toOpenAPIParameters(resolvedSchema, parameterIn));
|
|
435
607
|
}
|
|
436
608
|
}
|
|
437
609
|
if (schema.properties?.body !== void 0) {
|
|
@@ -441,7 +613,7 @@ ${errors.join("\n\n")}`
|
|
|
441
613
|
};
|
|
442
614
|
}
|
|
443
615
|
}
|
|
444
|
-
async #successResponse(ref, def) {
|
|
616
|
+
async #successResponse(doc, ref, def, baseSchemaConvertOptions) {
|
|
445
617
|
const outputSchema = def.outputSchema;
|
|
446
618
|
const status = fallbackContractConfig("defaultSuccessStatus", def.route.successStatus);
|
|
447
619
|
const description = fallbackContractConfig("defaultSuccessDescription", def.route?.successDescription);
|
|
@@ -452,46 +624,90 @@ ${errors.join("\n\n")}`
|
|
|
452
624
|
ref.responses[status] = {
|
|
453
625
|
description,
|
|
454
626
|
content: toOpenAPIEventIteratorContent(
|
|
455
|
-
await this.converter.convert(eventIteratorSchemaDetails.yields, { strategy: "output" }),
|
|
456
|
-
await this.converter.convert(eventIteratorSchemaDetails.returns, { strategy: "output" })
|
|
627
|
+
await this.converter.convert(eventIteratorSchemaDetails.yields, { ...baseSchemaConvertOptions, strategy: "output" }),
|
|
628
|
+
await this.converter.convert(eventIteratorSchemaDetails.returns, { ...baseSchemaConvertOptions, strategy: "output" })
|
|
457
629
|
)
|
|
458
630
|
};
|
|
459
631
|
return;
|
|
460
632
|
}
|
|
461
|
-
const [required, json] = await this.converter.convert(
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
633
|
+
const [required, json] = await this.converter.convert(
|
|
634
|
+
outputSchema,
|
|
635
|
+
{
|
|
636
|
+
...baseSchemaConvertOptions,
|
|
637
|
+
strategy: "output",
|
|
638
|
+
minStructureDepthForRef: outputStructure === "detailed" ? 1 : 0
|
|
639
|
+
}
|
|
640
|
+
);
|
|
466
641
|
if (outputStructure === "compact") {
|
|
642
|
+
ref.responses ??= {};
|
|
643
|
+
ref.responses[status] = {
|
|
644
|
+
description
|
|
645
|
+
};
|
|
467
646
|
ref.responses[status].content = toOpenAPIContent(applySchemaOptionality(required, json));
|
|
468
647
|
return;
|
|
469
648
|
}
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
649
|
+
const handledStatuses = /* @__PURE__ */ new Set();
|
|
650
|
+
for (const item of expandUnionSchema(json)) {
|
|
651
|
+
const error = new OpenAPIGeneratorError(`
|
|
652
|
+
When output structure is "detailed", output schema must satisfy:
|
|
653
|
+
{
|
|
654
|
+
status?: number, // must be a literal number and in the range of 200-399
|
|
655
|
+
headers?: Record<string, unknown>,
|
|
656
|
+
body?: unknown
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
But got: ${stringifyJSON(item)}
|
|
660
|
+
`);
|
|
661
|
+
if (!isObjectSchema(item)) {
|
|
478
662
|
throw error;
|
|
479
663
|
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
664
|
+
let schemaStatus;
|
|
665
|
+
let schemaDescription;
|
|
666
|
+
if (item.properties?.status !== void 0) {
|
|
667
|
+
const statusSchema = resolveOpenAPIJsonSchemaRef(doc, item.properties.status);
|
|
668
|
+
if (typeof statusSchema !== "object" || statusSchema.const === void 0 || typeof statusSchema.const !== "number" || !Number.isInteger(statusSchema.const) || isORPCErrorStatus(statusSchema.const)) {
|
|
669
|
+
throw error;
|
|
670
|
+
}
|
|
671
|
+
schemaStatus = statusSchema.const;
|
|
672
|
+
schemaDescription = statusSchema.description;
|
|
673
|
+
}
|
|
674
|
+
const itemStatus = schemaStatus ?? status;
|
|
675
|
+
const itemDescription = schemaDescription ?? description;
|
|
676
|
+
if (handledStatuses.has(itemStatus)) {
|
|
677
|
+
throw new OpenAPIGeneratorError(`
|
|
678
|
+
When output structure is "detailed", each success status must be unique.
|
|
679
|
+
But got status: ${itemStatus} used more than once.
|
|
680
|
+
`);
|
|
681
|
+
}
|
|
682
|
+
handledStatuses.add(itemStatus);
|
|
683
|
+
ref.responses ??= {};
|
|
684
|
+
ref.responses[itemStatus] = {
|
|
685
|
+
description: itemDescription
|
|
686
|
+
};
|
|
687
|
+
if (item.properties?.headers !== void 0) {
|
|
688
|
+
const headersSchema = resolveOpenAPIJsonSchemaRef(doc, item.properties.headers);
|
|
689
|
+
if (!isObjectSchema(headersSchema)) {
|
|
690
|
+
throw error;
|
|
691
|
+
}
|
|
692
|
+
for (const key in headersSchema.properties) {
|
|
693
|
+
const headerSchema = headersSchema.properties[key];
|
|
694
|
+
if (headerSchema !== void 0) {
|
|
695
|
+
ref.responses[itemStatus].headers ??= {};
|
|
696
|
+
ref.responses[itemStatus].headers[key] = {
|
|
697
|
+
schema: toOpenAPISchema(headerSchema),
|
|
698
|
+
required: item.required?.includes("headers") && headersSchema.required?.includes(key)
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
if (item.properties?.body !== void 0) {
|
|
704
|
+
ref.responses[itemStatus].content = toOpenAPIContent(
|
|
705
|
+
applySchemaOptionality(item.required?.includes("body") ?? false, item.properties.body)
|
|
706
|
+
);
|
|
486
707
|
}
|
|
487
|
-
}
|
|
488
|
-
if (json.properties?.body !== void 0) {
|
|
489
|
-
ref.responses[status].content = toOpenAPIContent(
|
|
490
|
-
applySchemaOptionality(json.required?.includes("body") ?? false, json.properties.body)
|
|
491
|
-
);
|
|
492
708
|
}
|
|
493
709
|
}
|
|
494
|
-
async #errorResponse(ref, def) {
|
|
710
|
+
async #errorResponse(ref, def, baseSchemaConvertOptions, undefinedErrorSchema) {
|
|
495
711
|
const errorMap = def.errorMap;
|
|
496
712
|
const errors = {};
|
|
497
713
|
for (const code in errorMap) {
|
|
@@ -501,7 +717,7 @@ ${errors.join("\n\n")}`
|
|
|
501
717
|
}
|
|
502
718
|
const status = fallbackORPCErrorStatus(code, config.status);
|
|
503
719
|
const message = fallbackORPCErrorMessage(code, config.message);
|
|
504
|
-
const [dataRequired, dataSchema] = await this.converter.convert(config.data, { strategy: "output" });
|
|
720
|
+
const [dataRequired, dataSchema] = await this.converter.convert(config.data, { ...baseSchemaConvertOptions, strategy: "output" });
|
|
505
721
|
errors[status] ??= [];
|
|
506
722
|
errors[status].push({
|
|
507
723
|
type: "object",
|
|
@@ -523,17 +739,7 @@ ${errors.join("\n\n")}`
|
|
|
523
739
|
content: toOpenAPIContent({
|
|
524
740
|
oneOf: [
|
|
525
741
|
...schemas,
|
|
526
|
-
|
|
527
|
-
type: "object",
|
|
528
|
-
properties: {
|
|
529
|
-
defined: { const: false },
|
|
530
|
-
code: { type: "string" },
|
|
531
|
-
status: { type: "number" },
|
|
532
|
-
message: { type: "string" },
|
|
533
|
-
data: {}
|
|
534
|
-
},
|
|
535
|
-
required: ["defined", "code", "status", "message"]
|
|
536
|
-
}
|
|
742
|
+
undefinedErrorSchema
|
|
537
743
|
]
|
|
538
744
|
})
|
|
539
745
|
};
|
|
@@ -541,4 +747,4 @@ ${errors.join("\n\n")}`
|
|
|
541
747
|
}
|
|
542
748
|
}
|
|
543
749
|
|
|
544
|
-
export { CompositeSchemaConverter as C, LOGIC_KEYWORDS as L, OpenAPIGenerator as O, applyCustomOpenAPIOperation as a, toOpenAPIMethod as b, customOpenAPIOperation as c, toOpenAPIContent as d, toOpenAPIEventIteratorContent as e, toOpenAPIParameters as f, getCustomOpenAPIOperation as g, checkParamsSchema as h, toOpenAPISchema as i, isFileSchema as j, isObjectSchema as k, isAnySchema as l, filterSchemaBranches as m, applySchemaOptionality as n, separateObjectSchema as s, toOpenAPIPath as t };
|
|
750
|
+
export { CompositeSchemaConverter as C, LOGIC_KEYWORDS as L, OpenAPIGenerator as O, applyCustomOpenAPIOperation as a, toOpenAPIMethod as b, customOpenAPIOperation as c, toOpenAPIContent as d, toOpenAPIEventIteratorContent as e, toOpenAPIParameters as f, getCustomOpenAPIOperation as g, checkParamsSchema as h, toOpenAPISchema as i, isFileSchema as j, isObjectSchema as k, isAnySchema as l, filterSchemaBranches as m, applySchemaOptionality as n, expandUnionSchema as o, expandArrayableSchema as p, isPrimitiveSchema as q, resolveOpenAPIJsonSchemaRef as r, separateObjectSchema as s, toOpenAPIPath as t };
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { AnySchema, OpenAPI, AnyContractProcedure, AnyContractRouter } from '@orpc/contract';
|
|
2
|
+
import { StandardOpenAPIJsonSerializerOptions } from '@orpc/openapi-client/standard';
|
|
3
|
+
import { AnyProcedure, TraverseContractProcedureCallbackOptions, AnyRouter } from '@orpc/server';
|
|
4
|
+
import { Promisable, Value } from '@orpc/shared';
|
|
5
|
+
import { JSONSchema } from '@orpc/json-schema-typed/draft-2020-12';
|
|
6
|
+
|
|
7
|
+
interface SchemaConverterComponent {
|
|
8
|
+
allowedStrategies: readonly SchemaConvertOptions['strategy'][];
|
|
9
|
+
schema: AnySchema;
|
|
10
|
+
required: boolean;
|
|
11
|
+
ref: string;
|
|
12
|
+
}
|
|
13
|
+
interface SchemaConvertOptions {
|
|
14
|
+
strategy: 'input' | 'output';
|
|
15
|
+
/**
|
|
16
|
+
* Common components should use `$ref` to represent themselves if matched.
|
|
17
|
+
*/
|
|
18
|
+
components?: readonly SchemaConverterComponent[];
|
|
19
|
+
/**
|
|
20
|
+
* Minimum schema structure depth required before using `$ref` for components.
|
|
21
|
+
*
|
|
22
|
+
* For example, if set to 2, `$ref` will only be used for schemas nested at depth 2 or greater.
|
|
23
|
+
*
|
|
24
|
+
* @default 0 - No depth limit;
|
|
25
|
+
*/
|
|
26
|
+
minStructureDepthForRef?: number;
|
|
27
|
+
}
|
|
28
|
+
interface SchemaConverter {
|
|
29
|
+
convert(schema: AnySchema | undefined, options: SchemaConvertOptions): Promisable<[required: boolean, jsonSchema: JSONSchema]>;
|
|
30
|
+
}
|
|
31
|
+
interface ConditionalSchemaConverter extends SchemaConverter {
|
|
32
|
+
condition(schema: AnySchema | undefined, options: SchemaConvertOptions): Promisable<boolean>;
|
|
33
|
+
}
|
|
34
|
+
declare class CompositeSchemaConverter implements SchemaConverter {
|
|
35
|
+
private readonly converters;
|
|
36
|
+
constructor(converters: readonly ConditionalSchemaConverter[]);
|
|
37
|
+
convert(schema: AnySchema | undefined, options: SchemaConvertOptions): Promise<[required: boolean, jsonSchema: JSONSchema]>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface OpenAPIGeneratorOptions extends StandardOpenAPIJsonSerializerOptions {
|
|
41
|
+
schemaConverters?: ConditionalSchemaConverter[];
|
|
42
|
+
}
|
|
43
|
+
interface OpenAPIGeneratorGenerateOptions extends Partial<Omit<OpenAPI.Document, 'openapi'>> {
|
|
44
|
+
/**
|
|
45
|
+
* Exclude procedures from the OpenAPI specification.
|
|
46
|
+
*
|
|
47
|
+
* @deprecated Use `filter` option instead.
|
|
48
|
+
* @default () => false
|
|
49
|
+
*/
|
|
50
|
+
exclude?: (procedure: AnyProcedure | AnyContractProcedure, path: readonly string[]) => boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Filter procedures. Return `false` to exclude a procedure from the OpenAPI specification.
|
|
53
|
+
*
|
|
54
|
+
* @default true
|
|
55
|
+
*/
|
|
56
|
+
filter?: Value<boolean, [options: TraverseContractProcedureCallbackOptions]>;
|
|
57
|
+
/**
|
|
58
|
+
* Common schemas to be used for $ref resolution.
|
|
59
|
+
*/
|
|
60
|
+
commonSchemas?: Record<string, {
|
|
61
|
+
/**
|
|
62
|
+
* Determines which schema definition to use when input and output schemas differ.
|
|
63
|
+
* This is needed because some schemas transform data differently between input and output,
|
|
64
|
+
* making it impossible to use a single $ref for both cases.
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```ts
|
|
68
|
+
* // This schema transforms a string input into a number output
|
|
69
|
+
* const Schema = z.string()
|
|
70
|
+
* .transform(v => Number(v))
|
|
71
|
+
* .pipe(z.number())
|
|
72
|
+
*
|
|
73
|
+
* // Input schema: { type: 'string' }
|
|
74
|
+
* // Output schema: { type: 'number' }
|
|
75
|
+
* ```
|
|
76
|
+
*
|
|
77
|
+
* When schemas differ between input and output, you must explicitly choose
|
|
78
|
+
* which version to use for the OpenAPI specification.
|
|
79
|
+
*
|
|
80
|
+
* @default 'input' - Uses the input schema definition by default
|
|
81
|
+
*/
|
|
82
|
+
strategy?: SchemaConvertOptions['strategy'];
|
|
83
|
+
schema: AnySchema;
|
|
84
|
+
} | {
|
|
85
|
+
error: 'UndefinedError';
|
|
86
|
+
schema?: never;
|
|
87
|
+
}>;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The generator that converts oRPC routers/contracts to OpenAPI specifications.
|
|
91
|
+
*
|
|
92
|
+
* @see {@link https://orpc.unnoq.com/docs/openapi/openapi-specification OpenAPI Specification Docs}
|
|
93
|
+
*/
|
|
94
|
+
declare class OpenAPIGenerator {
|
|
95
|
+
#private;
|
|
96
|
+
private readonly serializer;
|
|
97
|
+
private readonly converter;
|
|
98
|
+
constructor(options?: OpenAPIGeneratorOptions);
|
|
99
|
+
/**
|
|
100
|
+
* Generates OpenAPI specifications from oRPC routers/contracts.
|
|
101
|
+
*
|
|
102
|
+
* @see {@link https://orpc.unnoq.com/docs/openapi/openapi-specification OpenAPI Specification Docs}
|
|
103
|
+
*/
|
|
104
|
+
generate(router: AnyContractRouter | AnyRouter, options?: OpenAPIGeneratorGenerateOptions): Promise<OpenAPI.Document>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export { OpenAPIGenerator as b, CompositeSchemaConverter as e };
|
|
108
|
+
export type { ConditionalSchemaConverter as C, OpenAPIGeneratorOptions as O, SchemaConverterComponent as S, OpenAPIGeneratorGenerateOptions as a, SchemaConvertOptions as c, SchemaConverter as d };
|