@orpc/openapi 0.0.0-next.f16d90e → 0.0.0-next.f21e305
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 +8 -8
- package/dist/adapters/aws-lambda/index.d.mts +5 -3
- package/dist/adapters/aws-lambda/index.d.ts +5 -3
- package/dist/adapters/aws-lambda/index.mjs +3 -3
- package/dist/adapters/fetch/index.d.mts +8 -3
- package/dist/adapters/fetch/index.d.ts +8 -3
- package/dist/adapters/fetch/index.mjs +1 -1
- package/dist/adapters/node/index.d.mts +8 -3
- package/dist/adapters/node/index.d.ts +8 -3
- package/dist/adapters/node/index.mjs +1 -1
- package/dist/adapters/standard/index.d.mts +5 -11
- package/dist/adapters/standard/index.d.ts +5 -11
- package/dist/adapters/standard/index.mjs +1 -1
- package/dist/index.d.mts +5 -4
- package/dist/index.d.ts +5 -4
- package/dist/index.mjs +3 -3
- package/dist/plugins/index.d.mts +19 -4
- package/dist/plugins/index.d.ts +19 -4
- package/dist/plugins/index.mjs +59 -19
- package/dist/shared/{openapi.C_UtQ8Us.mjs → openapi.BVXcB0u4.mjs} +13 -5
- package/dist/shared/{openapi.DaYgbD_w.mjs → openapi.BlSv9FKY.mjs} +150 -51
- package/dist/shared/openapi.CQmjvnb0.d.mts +31 -0
- package/dist/shared/openapi.CQmjvnb0.d.ts +31 -0
- package/dist/shared/openapi.CfjfVeBJ.d.mts +108 -0
- package/dist/shared/openapi.CfjfVeBJ.d.ts +108 -0
- package/package.json +10 -10
- package/dist/shared/openapi.D3j94c9n.d.mts +0 -12
- package/dist/shared/openapi.D3j94c9n.d.ts +0 -12
- package/dist/shared/openapi.qZLdpE0a.d.mts +0 -52
- package/dist/shared/openapi.qZLdpE0a.d.ts +0 -52
|
@@ -2,7 +2,7 @@ import { standardizeHTTPPath, StandardOpenAPIJsonSerializer, StandardBracketNota
|
|
|
2
2
|
import { StandardHandler } from '@orpc/server/standard';
|
|
3
3
|
import { isORPCErrorStatus } from '@orpc/client';
|
|
4
4
|
import { fallbackContractConfig } from '@orpc/contract';
|
|
5
|
-
import { isObject, stringifyJSON } from '@orpc/shared';
|
|
5
|
+
import { isObject, stringifyJSON, tryDecodeURIComponent, value } from '@orpc/shared';
|
|
6
6
|
import { toHttpPath } from '@orpc/client/standard';
|
|
7
7
|
import { traverseContractProcedures, isProcedure, getLazyMeta, unlazy, getRouter, createContractedProcedure } from '@orpc/server';
|
|
8
8
|
import { createRouter, addRoute, findRoute } from 'rou3';
|
|
@@ -97,14 +97,22 @@ function toRou3Pattern(path) {
|
|
|
97
97
|
return standardizeHTTPPath(path).replace(/\/\{\+([^}]+)\}/g, "/**:$1").replace(/\/\{([^}]+)\}/g, "/:$1");
|
|
98
98
|
}
|
|
99
99
|
function decodeParams(params) {
|
|
100
|
-
return Object.fromEntries(Object.entries(params).map(([key, value]) => [key,
|
|
100
|
+
return Object.fromEntries(Object.entries(params).map(([key, value]) => [key, tryDecodeURIComponent(value)]));
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
class StandardOpenAPIMatcher {
|
|
104
|
+
filter;
|
|
104
105
|
tree = createRouter();
|
|
105
106
|
pendingRouters = [];
|
|
107
|
+
constructor(options = {}) {
|
|
108
|
+
this.filter = options.filter ?? true;
|
|
109
|
+
}
|
|
106
110
|
init(router, path = []) {
|
|
107
|
-
const laziedOptions = traverseContractProcedures({ router, path }, (
|
|
111
|
+
const laziedOptions = traverseContractProcedures({ router, path }, (traverseOptions) => {
|
|
112
|
+
if (!value(this.filter, traverseOptions)) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const { path: path2, contract } = traverseOptions;
|
|
108
116
|
const method = fallbackContractConfig("defaultMethod", contract["~orpc"].route.method);
|
|
109
117
|
const httpPath = toRou3Pattern(contract["~orpc"].route.path ?? toHttpPath(path2));
|
|
110
118
|
if (isProcedure(contract)) {
|
|
@@ -168,9 +176,9 @@ class StandardOpenAPIMatcher {
|
|
|
168
176
|
class StandardOpenAPIHandler extends StandardHandler {
|
|
169
177
|
constructor(router, options) {
|
|
170
178
|
const jsonSerializer = new StandardOpenAPIJsonSerializer(options);
|
|
171
|
-
const bracketNotationSerializer = new StandardBracketNotationSerializer();
|
|
179
|
+
const bracketNotationSerializer = new StandardBracketNotationSerializer(options);
|
|
172
180
|
const serializer = new StandardOpenAPISerializer(jsonSerializer, bracketNotationSerializer);
|
|
173
|
-
const matcher = new StandardOpenAPIMatcher();
|
|
181
|
+
const matcher = new StandardOpenAPIMatcher(options);
|
|
174
182
|
const codec = new StandardOpenAPICodec(serializer);
|
|
175
183
|
super(router, matcher, codec, options);
|
|
176
184
|
}
|
|
@@ -3,8 +3,8 @@ 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, stringifyJSON, findDeepMatches, toArray, clone } from '@orpc/shared';
|
|
7
|
-
import { TypeName } from 'json-schema-typed/draft-2020-12';
|
|
6
|
+
import { isObject, stringifyJSON, findDeepMatches, toArray, clone, value } from '@orpc/shared';
|
|
7
|
+
import { TypeName } from '@orpc/interop/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));
|
|
@@ -345,6 +350,15 @@ function checkParamsSchema(schema, params) {
|
|
|
345
350
|
function toOpenAPISchema(schema) {
|
|
346
351
|
return schema === true ? {} : schema === false ? { not: {} } : schema;
|
|
347
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
|
+
}
|
|
348
362
|
|
|
349
363
|
class CompositeSchemaConverter {
|
|
350
364
|
converters;
|
|
@@ -376,40 +390,49 @@ class OpenAPIGenerator {
|
|
|
376
390
|
* @see {@link https://orpc.unnoq.com/docs/openapi/openapi-specification OpenAPI Specification Docs}
|
|
377
391
|
*/
|
|
378
392
|
async generate(router, options = {}) {
|
|
379
|
-
const
|
|
393
|
+
const filter = options.filter ?? (({ contract, path }) => {
|
|
394
|
+
return !(options.exclude?.(contract, path) ?? false);
|
|
395
|
+
});
|
|
380
396
|
const doc = {
|
|
381
397
|
...clone(options),
|
|
382
398
|
info: options.info ?? { title: "API Reference", version: "0.0.0" },
|
|
383
399
|
openapi: "3.1.1",
|
|
384
|
-
exclude: void 0
|
|
400
|
+
exclude: void 0,
|
|
401
|
+
filter: void 0,
|
|
402
|
+
commonSchemas: void 0
|
|
385
403
|
};
|
|
404
|
+
const { baseSchemaConvertOptions, undefinedErrorJsonSchema } = await this.#resolveCommonSchemas(doc, options.commonSchemas);
|
|
386
405
|
const contracts = [];
|
|
387
|
-
await resolveContractProcedures({ path: [], router }, (
|
|
388
|
-
if (!
|
|
389
|
-
|
|
406
|
+
await resolveContractProcedures({ path: [], router }, (traverseOptions) => {
|
|
407
|
+
if (!value(filter, traverseOptions)) {
|
|
408
|
+
return;
|
|
390
409
|
}
|
|
410
|
+
contracts.push(traverseOptions);
|
|
391
411
|
});
|
|
392
412
|
const errors = [];
|
|
393
413
|
for (const { contract, path } of contracts) {
|
|
394
|
-
const
|
|
414
|
+
const stringPath = path.join(".");
|
|
395
415
|
try {
|
|
396
416
|
const def = contract["~orpc"];
|
|
397
417
|
const method = toOpenAPIMethod(fallbackContractConfig("defaultMethod", def.route.method));
|
|
398
418
|
const httpPath = toOpenAPIPath(def.route.path ?? toHttpPath(path));
|
|
399
419
|
let operationObjectRef;
|
|
400
|
-
if (def.route.spec !== void 0) {
|
|
420
|
+
if (def.route.spec !== void 0 && typeof def.route.spec !== "function") {
|
|
401
421
|
operationObjectRef = def.route.spec;
|
|
402
422
|
} else {
|
|
403
423
|
operationObjectRef = {
|
|
404
|
-
operationId,
|
|
424
|
+
operationId: def.route.operationId ?? stringPath,
|
|
405
425
|
summary: def.route.summary,
|
|
406
426
|
description: def.route.description,
|
|
407
427
|
deprecated: def.route.deprecated,
|
|
408
428
|
tags: def.route.tags?.map((tag) => tag)
|
|
409
429
|
};
|
|
410
|
-
await this.#request(operationObjectRef, def);
|
|
411
|
-
await this.#successResponse(operationObjectRef, def);
|
|
412
|
-
await this.#errorResponse(operationObjectRef, def);
|
|
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);
|
|
413
436
|
}
|
|
414
437
|
doc.paths ??= {};
|
|
415
438
|
doc.paths[httpPath] ??= {};
|
|
@@ -419,7 +442,7 @@ class OpenAPIGenerator {
|
|
|
419
442
|
throw e;
|
|
420
443
|
}
|
|
421
444
|
errors.push(
|
|
422
|
-
`[OpenAPIGenerator] Error occurred while generating OpenAPI for procedure at path: ${
|
|
445
|
+
`[OpenAPIGenerator] Error occurred while generating OpenAPI for procedure at path: ${stringPath}
|
|
423
446
|
${e.message}`
|
|
424
447
|
);
|
|
425
448
|
}
|
|
@@ -433,22 +456,96 @@ ${errors.join("\n\n")}`
|
|
|
433
456
|
}
|
|
434
457
|
return this.serializer.serialize(doc)[0];
|
|
435
458
|
}
|
|
436
|
-
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) {
|
|
437
527
|
const method = fallbackContractConfig("defaultMethod", def.route.method);
|
|
438
528
|
const details = getEventIteratorSchemaDetails(def.inputSchema);
|
|
439
529
|
if (details) {
|
|
440
530
|
ref.requestBody = {
|
|
441
531
|
required: true,
|
|
442
532
|
content: toOpenAPIEventIteratorContent(
|
|
443
|
-
await this.converter.convert(details.yields, { strategy: "input" }),
|
|
444
|
-
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" })
|
|
445
535
|
)
|
|
446
536
|
};
|
|
447
537
|
return;
|
|
448
538
|
}
|
|
449
539
|
const dynamicParams = getDynamicParams(def.route.path)?.map((v) => v.name);
|
|
450
540
|
const inputStructure = fallbackContractConfig("defaultInputStructure", def.route.inputStructure);
|
|
451
|
-
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
|
+
);
|
|
452
549
|
if (isAnySchema(schema) && !dynamicParams?.length) {
|
|
453
550
|
return;
|
|
454
551
|
}
|
|
@@ -470,13 +567,14 @@ ${errors.join("\n\n")}`
|
|
|
470
567
|
ref.parameters.push(...toOpenAPIParameters(paramsSchema, "path"));
|
|
471
568
|
}
|
|
472
569
|
if (method === "GET") {
|
|
473
|
-
|
|
570
|
+
const resolvedSchema = resolveOpenAPIJsonSchemaRef(doc, schema);
|
|
571
|
+
if (!isObjectSchema(resolvedSchema)) {
|
|
474
572
|
throw new OpenAPIGeneratorError(
|
|
475
573
|
'When method is "GET", input schema must satisfy: object | any | unknown'
|
|
476
574
|
);
|
|
477
575
|
}
|
|
478
576
|
ref.parameters ??= [];
|
|
479
|
-
ref.parameters.push(...toOpenAPIParameters(
|
|
577
|
+
ref.parameters.push(...toOpenAPIParameters(resolvedSchema, "query"));
|
|
480
578
|
} else {
|
|
481
579
|
ref.requestBody = {
|
|
482
580
|
required,
|
|
@@ -491,7 +589,8 @@ ${errors.join("\n\n")}`
|
|
|
491
589
|
if (!isObjectSchema(schema)) {
|
|
492
590
|
throw error;
|
|
493
591
|
}
|
|
494
|
-
|
|
592
|
+
const resolvedParamSchema = schema.properties?.params !== void 0 ? resolveOpenAPIJsonSchemaRef(doc, schema.properties.params) : void 0;
|
|
593
|
+
if (dynamicParams?.length && (resolvedParamSchema === void 0 || !isObjectSchema(resolvedParamSchema) || !checkParamsSchema(resolvedParamSchema, dynamicParams))) {
|
|
495
594
|
throw new OpenAPIGeneratorError(
|
|
496
595
|
'When input structure is "detailed" and path has dynamic params, the "params" schema must be an object with all dynamic params as required.'
|
|
497
596
|
);
|
|
@@ -499,12 +598,13 @@ ${errors.join("\n\n")}`
|
|
|
499
598
|
for (const from of ["params", "query", "headers"]) {
|
|
500
599
|
const fromSchema = schema.properties?.[from];
|
|
501
600
|
if (fromSchema !== void 0) {
|
|
502
|
-
|
|
601
|
+
const resolvedSchema = resolveOpenAPIJsonSchemaRef(doc, fromSchema);
|
|
602
|
+
if (!isObjectSchema(resolvedSchema)) {
|
|
503
603
|
throw error;
|
|
504
604
|
}
|
|
505
605
|
const parameterIn = from === "params" ? "path" : from === "headers" ? "header" : "query";
|
|
506
606
|
ref.parameters ??= [];
|
|
507
|
-
ref.parameters.push(...toOpenAPIParameters(
|
|
607
|
+
ref.parameters.push(...toOpenAPIParameters(resolvedSchema, parameterIn));
|
|
508
608
|
}
|
|
509
609
|
}
|
|
510
610
|
if (schema.properties?.body !== void 0) {
|
|
@@ -514,7 +614,7 @@ ${errors.join("\n\n")}`
|
|
|
514
614
|
};
|
|
515
615
|
}
|
|
516
616
|
}
|
|
517
|
-
async #successResponse(ref, def) {
|
|
617
|
+
async #successResponse(doc, ref, def, baseSchemaConvertOptions) {
|
|
518
618
|
const outputSchema = def.outputSchema;
|
|
519
619
|
const status = fallbackContractConfig("defaultSuccessStatus", def.route.successStatus);
|
|
520
620
|
const description = fallbackContractConfig("defaultSuccessDescription", def.route?.successDescription);
|
|
@@ -525,13 +625,20 @@ ${errors.join("\n\n")}`
|
|
|
525
625
|
ref.responses[status] = {
|
|
526
626
|
description,
|
|
527
627
|
content: toOpenAPIEventIteratorContent(
|
|
528
|
-
await this.converter.convert(eventIteratorSchemaDetails.yields, { strategy: "output" }),
|
|
529
|
-
await this.converter.convert(eventIteratorSchemaDetails.returns, { strategy: "output" })
|
|
628
|
+
await this.converter.convert(eventIteratorSchemaDetails.yields, { ...baseSchemaConvertOptions, strategy: "output" }),
|
|
629
|
+
await this.converter.convert(eventIteratorSchemaDetails.returns, { ...baseSchemaConvertOptions, strategy: "output" })
|
|
530
630
|
)
|
|
531
631
|
};
|
|
532
632
|
return;
|
|
533
633
|
}
|
|
534
|
-
const [required, json] = await this.converter.convert(
|
|
634
|
+
const [required, json] = await this.converter.convert(
|
|
635
|
+
outputSchema,
|
|
636
|
+
{
|
|
637
|
+
...baseSchemaConvertOptions,
|
|
638
|
+
strategy: "output",
|
|
639
|
+
minStructureDepthForRef: outputStructure === "detailed" ? 1 : 0
|
|
640
|
+
}
|
|
641
|
+
);
|
|
535
642
|
if (outputStructure === "compact") {
|
|
536
643
|
ref.responses ??= {};
|
|
537
644
|
ref.responses[status] = {
|
|
@@ -558,11 +665,12 @@ ${errors.join("\n\n")}`
|
|
|
558
665
|
let schemaStatus;
|
|
559
666
|
let schemaDescription;
|
|
560
667
|
if (item.properties?.status !== void 0) {
|
|
561
|
-
|
|
668
|
+
const statusSchema = resolveOpenAPIJsonSchemaRef(doc, item.properties.status);
|
|
669
|
+
if (typeof statusSchema !== "object" || statusSchema.const === void 0 || typeof statusSchema.const !== "number" || !Number.isInteger(statusSchema.const) || isORPCErrorStatus(statusSchema.const)) {
|
|
562
670
|
throw error;
|
|
563
671
|
}
|
|
564
|
-
schemaStatus =
|
|
565
|
-
schemaDescription =
|
|
672
|
+
schemaStatus = statusSchema.const;
|
|
673
|
+
schemaDescription = statusSchema.description;
|
|
566
674
|
}
|
|
567
675
|
const itemStatus = schemaStatus ?? status;
|
|
568
676
|
const itemDescription = schemaDescription ?? description;
|
|
@@ -578,16 +686,17 @@ ${errors.join("\n\n")}`
|
|
|
578
686
|
description: itemDescription
|
|
579
687
|
};
|
|
580
688
|
if (item.properties?.headers !== void 0) {
|
|
581
|
-
|
|
689
|
+
const headersSchema = resolveOpenAPIJsonSchemaRef(doc, item.properties.headers);
|
|
690
|
+
if (!isObjectSchema(headersSchema)) {
|
|
582
691
|
throw error;
|
|
583
692
|
}
|
|
584
|
-
for (const key in
|
|
585
|
-
const headerSchema =
|
|
693
|
+
for (const key in headersSchema.properties) {
|
|
694
|
+
const headerSchema = headersSchema.properties[key];
|
|
586
695
|
if (headerSchema !== void 0) {
|
|
587
696
|
ref.responses[itemStatus].headers ??= {};
|
|
588
697
|
ref.responses[itemStatus].headers[key] = {
|
|
589
698
|
schema: toOpenAPISchema(headerSchema),
|
|
590
|
-
required: item.
|
|
699
|
+
required: item.required?.includes("headers") && headersSchema.required?.includes(key)
|
|
591
700
|
};
|
|
592
701
|
}
|
|
593
702
|
}
|
|
@@ -599,7 +708,7 @@ ${errors.join("\n\n")}`
|
|
|
599
708
|
}
|
|
600
709
|
}
|
|
601
710
|
}
|
|
602
|
-
async #errorResponse(ref, def) {
|
|
711
|
+
async #errorResponse(ref, def, baseSchemaConvertOptions, undefinedErrorSchema) {
|
|
603
712
|
const errorMap = def.errorMap;
|
|
604
713
|
const errors = {};
|
|
605
714
|
for (const code in errorMap) {
|
|
@@ -609,7 +718,7 @@ ${errors.join("\n\n")}`
|
|
|
609
718
|
}
|
|
610
719
|
const status = fallbackORPCErrorStatus(code, config.status);
|
|
611
720
|
const message = fallbackORPCErrorMessage(code, config.message);
|
|
612
|
-
const [dataRequired, dataSchema] = await this.converter.convert(config.data, { strategy: "output" });
|
|
721
|
+
const [dataRequired, dataSchema] = await this.converter.convert(config.data, { ...baseSchemaConvertOptions, strategy: "output" });
|
|
613
722
|
errors[status] ??= [];
|
|
614
723
|
errors[status].push({
|
|
615
724
|
type: "object",
|
|
@@ -631,17 +740,7 @@ ${errors.join("\n\n")}`
|
|
|
631
740
|
content: toOpenAPIContent({
|
|
632
741
|
oneOf: [
|
|
633
742
|
...schemas,
|
|
634
|
-
|
|
635
|
-
type: "object",
|
|
636
|
-
properties: {
|
|
637
|
-
defined: { const: false },
|
|
638
|
-
code: { type: "string" },
|
|
639
|
-
status: { type: "number" },
|
|
640
|
-
message: { type: "string" },
|
|
641
|
-
data: {}
|
|
642
|
-
},
|
|
643
|
-
required: ["defined", "code", "status", "message"]
|
|
644
|
-
}
|
|
743
|
+
undefinedErrorSchema
|
|
645
744
|
]
|
|
646
745
|
})
|
|
647
746
|
};
|
|
@@ -649,4 +748,4 @@ ${errors.join("\n\n")}`
|
|
|
649
748
|
}
|
|
650
749
|
}
|
|
651
750
|
|
|
652
|
-
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, separateObjectSchema as s, toOpenAPIPath as t };
|
|
751
|
+
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,31 @@
|
|
|
1
|
+
import { StandardOpenAPIJsonSerializerOptions, StandardBracketNotationSerializerOptions } from '@orpc/openapi-client/standard';
|
|
2
|
+
import { TraverseContractProcedureCallbackOptions, AnyRouter, Context, Router } from '@orpc/server';
|
|
3
|
+
import { StandardMatcher, StandardMatchResult, StandardHandlerOptions, StandardHandler } from '@orpc/server/standard';
|
|
4
|
+
import { HTTPPath } from '@orpc/client';
|
|
5
|
+
import { Value } from '@orpc/shared';
|
|
6
|
+
|
|
7
|
+
interface StandardOpenAPIMatcherOptions {
|
|
8
|
+
/**
|
|
9
|
+
* Filter procedures. Return `false` to exclude a procedure from matching.
|
|
10
|
+
*
|
|
11
|
+
* @default true
|
|
12
|
+
*/
|
|
13
|
+
filter?: Value<boolean, [options: TraverseContractProcedureCallbackOptions]>;
|
|
14
|
+
}
|
|
15
|
+
declare class StandardOpenAPIMatcher implements StandardMatcher {
|
|
16
|
+
private readonly filter;
|
|
17
|
+
private readonly tree;
|
|
18
|
+
private pendingRouters;
|
|
19
|
+
constructor(options?: StandardOpenAPIMatcherOptions);
|
|
20
|
+
init(router: AnyRouter, path?: readonly string[]): void;
|
|
21
|
+
match(method: string, pathname: HTTPPath): Promise<StandardMatchResult>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface StandardOpenAPIHandlerOptions<T extends Context> extends StandardHandlerOptions<T>, StandardOpenAPIJsonSerializerOptions, StandardBracketNotationSerializerOptions, StandardOpenAPIMatcherOptions {
|
|
25
|
+
}
|
|
26
|
+
declare class StandardOpenAPIHandler<T extends Context> extends StandardHandler<T> {
|
|
27
|
+
constructor(router: Router<any, T>, options: NoInfer<StandardOpenAPIHandlerOptions<T>>);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export { StandardOpenAPIHandler as a, StandardOpenAPIMatcher as c };
|
|
31
|
+
export type { StandardOpenAPIHandlerOptions as S, StandardOpenAPIMatcherOptions as b };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { StandardOpenAPIJsonSerializerOptions, StandardBracketNotationSerializerOptions } from '@orpc/openapi-client/standard';
|
|
2
|
+
import { TraverseContractProcedureCallbackOptions, AnyRouter, Context, Router } from '@orpc/server';
|
|
3
|
+
import { StandardMatcher, StandardMatchResult, StandardHandlerOptions, StandardHandler } from '@orpc/server/standard';
|
|
4
|
+
import { HTTPPath } from '@orpc/client';
|
|
5
|
+
import { Value } from '@orpc/shared';
|
|
6
|
+
|
|
7
|
+
interface StandardOpenAPIMatcherOptions {
|
|
8
|
+
/**
|
|
9
|
+
* Filter procedures. Return `false` to exclude a procedure from matching.
|
|
10
|
+
*
|
|
11
|
+
* @default true
|
|
12
|
+
*/
|
|
13
|
+
filter?: Value<boolean, [options: TraverseContractProcedureCallbackOptions]>;
|
|
14
|
+
}
|
|
15
|
+
declare class StandardOpenAPIMatcher implements StandardMatcher {
|
|
16
|
+
private readonly filter;
|
|
17
|
+
private readonly tree;
|
|
18
|
+
private pendingRouters;
|
|
19
|
+
constructor(options?: StandardOpenAPIMatcherOptions);
|
|
20
|
+
init(router: AnyRouter, path?: readonly string[]): void;
|
|
21
|
+
match(method: string, pathname: HTTPPath): Promise<StandardMatchResult>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface StandardOpenAPIHandlerOptions<T extends Context> extends StandardHandlerOptions<T>, StandardOpenAPIJsonSerializerOptions, StandardBracketNotationSerializerOptions, StandardOpenAPIMatcherOptions {
|
|
25
|
+
}
|
|
26
|
+
declare class StandardOpenAPIHandler<T extends Context> extends StandardHandler<T> {
|
|
27
|
+
constructor(router: Router<any, T>, options: NoInfer<StandardOpenAPIHandlerOptions<T>>);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export { StandardOpenAPIHandler as a, StandardOpenAPIMatcher as c };
|
|
31
|
+
export type { StandardOpenAPIHandlerOptions as S, StandardOpenAPIMatcherOptions as b };
|
|
@@ -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/interop/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 };
|