@orpc/openapi 0.0.0-next.1b95eee → 0.0.0-next.1bc0bef
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 +123 -14
- package/dist/adapters/aws-lambda/index.d.mts +8 -5
- package/dist/adapters/aws-lambda/index.d.ts +8 -5
- package/dist/adapters/aws-lambda/index.mjs +3 -3
- package/dist/adapters/fastify/index.d.mts +23 -0
- package/dist/adapters/fastify/index.d.ts +23 -0
- package/dist/adapters/fastify/index.mjs +18 -0
- package/dist/adapters/fetch/index.d.mts +11 -5
- package/dist/adapters/fetch/index.d.ts +11 -5
- package/dist/adapters/fetch/index.mjs +1 -1
- package/dist/adapters/node/index.d.mts +11 -5
- package/dist/adapters/node/index.d.ts +11 -5
- package/dist/adapters/node/index.mjs +1 -1
- package/dist/adapters/standard/index.d.mts +8 -23
- package/dist/adapters/standard/index.d.ts +8 -23
- package/dist/adapters/standard/index.mjs +1 -1
- package/dist/index.d.mts +9 -3
- package/dist/index.d.ts +9 -3
- package/dist/index.mjs +2 -2
- package/dist/plugins/index.d.mts +20 -3
- package/dist/plugins/index.d.ts +20 -3
- package/dist/plugins/index.mjs +69 -20
- package/dist/shared/{openapi.DYi1fARS.d.mts → openapi.BGy4N6eR.d.mts} +28 -6
- package/dist/shared/{openapi.DYi1fARS.d.ts → openapi.BGy4N6eR.d.ts} +28 -6
- package/dist/shared/{openapi.C_3bk7bB.mjs → openapi.CoREqFh3.mjs} +192 -64
- package/dist/shared/{openapi.C_UtQ8Us.mjs → openapi.DIt-Z9W1.mjs} +19 -8
- package/dist/shared/openapi.DwaweYRb.d.mts +54 -0
- package/dist/shared/openapi.DwaweYRb.d.ts +54 -0
- package/package.json +19 -13
- package/dist/shared/openapi.D3j94c9n.d.mts +0 -12
- package/dist/shared/openapi.D3j94c9n.d.ts +0 -12
|
@@ -3,7 +3,7 @@ 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';
|
|
6
|
+
import { isObject, stringifyJSON, findDeepMatches, toArray, clone, value } from '@orpc/shared';
|
|
7
7
|
import { TypeName } from 'json-schema-typed/draft-2020-12';
|
|
8
8
|
|
|
9
9
|
const OPERATION_EXTENDER_SYMBOL = Symbol("ORPC_OPERATION_EXTENDER");
|
|
@@ -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));
|
|
@@ -354,6 +359,107 @@ function resolveOpenAPIJsonSchemaRef(doc, schema) {
|
|
|
354
359
|
const resolved = doc.components?.schemas?.[name];
|
|
355
360
|
return resolved ?? schema;
|
|
356
361
|
}
|
|
362
|
+
function simplifyComposedObjectJsonSchemasAndRefs(schema, doc) {
|
|
363
|
+
if (doc) {
|
|
364
|
+
schema = resolveOpenAPIJsonSchemaRef(doc, schema);
|
|
365
|
+
}
|
|
366
|
+
if (typeof schema !== "object" || !schema.anyOf && !schema.oneOf && !schema.allOf) {
|
|
367
|
+
return schema;
|
|
368
|
+
}
|
|
369
|
+
const unionSchemas = [
|
|
370
|
+
...toArray(schema.anyOf?.map((s) => simplifyComposedObjectJsonSchemasAndRefs(s, doc))),
|
|
371
|
+
...toArray(schema.oneOf?.map((s) => simplifyComposedObjectJsonSchemasAndRefs(s, doc)))
|
|
372
|
+
];
|
|
373
|
+
const objectUnionSchemas = [];
|
|
374
|
+
for (const u of unionSchemas) {
|
|
375
|
+
if (!isObjectSchema(u)) {
|
|
376
|
+
return schema;
|
|
377
|
+
}
|
|
378
|
+
objectUnionSchemas.push(u);
|
|
379
|
+
}
|
|
380
|
+
const mergedUnionPropertyMap = /* @__PURE__ */ new Map();
|
|
381
|
+
for (const u of objectUnionSchemas) {
|
|
382
|
+
if (u.properties) {
|
|
383
|
+
for (const [key, value] of Object.entries(u.properties)) {
|
|
384
|
+
let entry = mergedUnionPropertyMap.get(key);
|
|
385
|
+
if (!entry) {
|
|
386
|
+
const required = objectUnionSchemas.every((s) => s.required?.includes(key));
|
|
387
|
+
entry = { required, schemas: [] };
|
|
388
|
+
mergedUnionPropertyMap.set(key, entry);
|
|
389
|
+
}
|
|
390
|
+
entry.schemas.push(value);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
const intersectionSchemas = toArray(schema.allOf?.map((s) => simplifyComposedObjectJsonSchemasAndRefs(s, doc)));
|
|
395
|
+
const objectIntersectionSchemas = [];
|
|
396
|
+
for (const u of intersectionSchemas) {
|
|
397
|
+
if (!isObjectSchema(u)) {
|
|
398
|
+
return schema;
|
|
399
|
+
}
|
|
400
|
+
objectIntersectionSchemas.push(u);
|
|
401
|
+
}
|
|
402
|
+
if (isObjectSchema(schema)) {
|
|
403
|
+
objectIntersectionSchemas.push(schema);
|
|
404
|
+
}
|
|
405
|
+
const mergedInteractionPropertyMap = /* @__PURE__ */ new Map();
|
|
406
|
+
for (const u of objectIntersectionSchemas) {
|
|
407
|
+
if (u.properties) {
|
|
408
|
+
for (const [key, value] of Object.entries(u.properties)) {
|
|
409
|
+
let entry = mergedInteractionPropertyMap.get(key);
|
|
410
|
+
if (!entry) {
|
|
411
|
+
const required = objectIntersectionSchemas.some((s) => s.required?.includes(key));
|
|
412
|
+
entry = { required, schemas: [] };
|
|
413
|
+
mergedInteractionPropertyMap.set(key, entry);
|
|
414
|
+
}
|
|
415
|
+
entry.schemas.push(value);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const resultObjectSchema = { type: "object", properties: {}, required: [] };
|
|
420
|
+
const keys = /* @__PURE__ */ new Set([
|
|
421
|
+
...mergedUnionPropertyMap.keys(),
|
|
422
|
+
...mergedInteractionPropertyMap.keys()
|
|
423
|
+
]);
|
|
424
|
+
if (keys.size === 0) {
|
|
425
|
+
return schema;
|
|
426
|
+
}
|
|
427
|
+
const deduplicateSchemas = (schemas) => {
|
|
428
|
+
const seen = /* @__PURE__ */ new Set();
|
|
429
|
+
const result = [];
|
|
430
|
+
for (const schema2 of schemas) {
|
|
431
|
+
const key = stringifyJSON(schema2);
|
|
432
|
+
if (!seen.has(key)) {
|
|
433
|
+
seen.add(key);
|
|
434
|
+
result.push(schema2);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return result;
|
|
438
|
+
};
|
|
439
|
+
for (const key of keys) {
|
|
440
|
+
const unionEntry = mergedUnionPropertyMap.get(key);
|
|
441
|
+
const intersectionEntry = mergedInteractionPropertyMap.get(key);
|
|
442
|
+
resultObjectSchema.properties[key] = (() => {
|
|
443
|
+
const dedupedUnionSchemas = unionEntry ? deduplicateSchemas(unionEntry.schemas) : [];
|
|
444
|
+
const dedupedIntersectionSchemas = intersectionEntry ? deduplicateSchemas(intersectionEntry.schemas) : [];
|
|
445
|
+
if (!dedupedUnionSchemas.length) {
|
|
446
|
+
return dedupedIntersectionSchemas.length === 1 ? dedupedIntersectionSchemas[0] : { allOf: dedupedIntersectionSchemas };
|
|
447
|
+
}
|
|
448
|
+
if (!dedupedIntersectionSchemas.length) {
|
|
449
|
+
return dedupedUnionSchemas.length === 1 ? dedupedUnionSchemas[0] : { anyOf: dedupedUnionSchemas };
|
|
450
|
+
}
|
|
451
|
+
const allOf = deduplicateSchemas([
|
|
452
|
+
...dedupedIntersectionSchemas,
|
|
453
|
+
dedupedUnionSchemas.length === 1 ? dedupedUnionSchemas[0] : { anyOf: dedupedUnionSchemas }
|
|
454
|
+
]);
|
|
455
|
+
return allOf.length === 1 ? allOf[0] : { allOf };
|
|
456
|
+
})();
|
|
457
|
+
if (unionEntry?.required || intersectionEntry?.required) {
|
|
458
|
+
resultObjectSchema.required.push(key);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return resultObjectSchema;
|
|
462
|
+
}
|
|
357
463
|
|
|
358
464
|
class CompositeSchemaConverter {
|
|
359
465
|
converters;
|
|
@@ -382,37 +488,38 @@ class OpenAPIGenerator {
|
|
|
382
488
|
/**
|
|
383
489
|
* Generates OpenAPI specifications from oRPC routers/contracts.
|
|
384
490
|
*
|
|
385
|
-
* @see {@link https://orpc.
|
|
491
|
+
* @see {@link https://orpc.dev/docs/openapi/openapi-specification OpenAPI Specification Docs}
|
|
386
492
|
*/
|
|
387
|
-
async generate(router,
|
|
388
|
-
const
|
|
493
|
+
async generate(router, { customErrorResponseBodySchema, commonSchemas, filter: baseFilter, exclude, ...baseDoc } = {}) {
|
|
494
|
+
const filter = baseFilter ?? (({ contract, path }) => {
|
|
495
|
+
return !(exclude?.(contract, path) ?? false);
|
|
496
|
+
});
|
|
389
497
|
const doc = {
|
|
390
|
-
...clone(
|
|
391
|
-
info:
|
|
392
|
-
openapi: "3.1.1"
|
|
393
|
-
exclude: void 0,
|
|
394
|
-
commonSchemas: void 0
|
|
498
|
+
...clone(baseDoc),
|
|
499
|
+
info: baseDoc.info ?? { title: "API Reference", version: "0.0.0" },
|
|
500
|
+
openapi: "3.1.1"
|
|
395
501
|
};
|
|
396
|
-
const baseSchemaConvertOptions = await this.#resolveCommonSchemas(doc,
|
|
502
|
+
const { baseSchemaConvertOptions, undefinedErrorJsonSchema } = await this.#resolveCommonSchemas(doc, commonSchemas);
|
|
397
503
|
const contracts = [];
|
|
398
|
-
await resolveContractProcedures({ path: [], router }, (
|
|
399
|
-
if (!
|
|
400
|
-
|
|
504
|
+
await resolveContractProcedures({ path: [], router }, (traverseOptions) => {
|
|
505
|
+
if (!value(filter, traverseOptions)) {
|
|
506
|
+
return;
|
|
401
507
|
}
|
|
508
|
+
contracts.push(traverseOptions);
|
|
402
509
|
});
|
|
403
510
|
const errors = [];
|
|
404
511
|
for (const { contract, path } of contracts) {
|
|
405
|
-
const
|
|
512
|
+
const stringPath = path.join(".");
|
|
406
513
|
try {
|
|
407
514
|
const def = contract["~orpc"];
|
|
408
515
|
const method = toOpenAPIMethod(fallbackContractConfig("defaultMethod", def.route.method));
|
|
409
516
|
const httpPath = toOpenAPIPath(def.route.path ?? toHttpPath(path));
|
|
410
517
|
let operationObjectRef;
|
|
411
|
-
if (def.route.spec !== void 0) {
|
|
518
|
+
if (def.route.spec !== void 0 && typeof def.route.spec !== "function") {
|
|
412
519
|
operationObjectRef = def.route.spec;
|
|
413
520
|
} else {
|
|
414
521
|
operationObjectRef = {
|
|
415
|
-
operationId,
|
|
522
|
+
operationId: def.route.operationId ?? stringPath,
|
|
416
523
|
summary: def.route.summary,
|
|
417
524
|
description: def.route.description,
|
|
418
525
|
deprecated: def.route.deprecated,
|
|
@@ -420,7 +527,10 @@ class OpenAPIGenerator {
|
|
|
420
527
|
};
|
|
421
528
|
await this.#request(doc, operationObjectRef, def, baseSchemaConvertOptions);
|
|
422
529
|
await this.#successResponse(doc, operationObjectRef, def, baseSchemaConvertOptions);
|
|
423
|
-
await this.#errorResponse(operationObjectRef, def, baseSchemaConvertOptions);
|
|
530
|
+
await this.#errorResponse(operationObjectRef, def, baseSchemaConvertOptions, undefinedErrorJsonSchema, customErrorResponseBodySchema);
|
|
531
|
+
}
|
|
532
|
+
if (typeof def.route.spec === "function") {
|
|
533
|
+
operationObjectRef = def.route.spec(operationObjectRef);
|
|
424
534
|
}
|
|
425
535
|
doc.paths ??= {};
|
|
426
536
|
doc.paths[httpPath] ??= {};
|
|
@@ -430,7 +540,7 @@ class OpenAPIGenerator {
|
|
|
430
540
|
throw e;
|
|
431
541
|
}
|
|
432
542
|
errors.push(
|
|
433
|
-
`[OpenAPIGenerator] Error occurred while generating OpenAPI for procedure at path: ${
|
|
543
|
+
`[OpenAPIGenerator] Error occurred while generating OpenAPI for procedure at path: ${stringPath}
|
|
434
544
|
${e.message}`
|
|
435
545
|
);
|
|
436
546
|
}
|
|
@@ -445,11 +555,26 @@ ${errors.join("\n\n")}`
|
|
|
445
555
|
return this.serializer.serialize(doc)[0];
|
|
446
556
|
}
|
|
447
557
|
async #resolveCommonSchemas(doc, commonSchemas) {
|
|
448
|
-
|
|
558
|
+
let undefinedErrorJsonSchema = {
|
|
559
|
+
type: "object",
|
|
560
|
+
properties: {
|
|
561
|
+
defined: { const: false },
|
|
562
|
+
code: { type: "string" },
|
|
563
|
+
status: { type: "number" },
|
|
564
|
+
message: { type: "string" },
|
|
565
|
+
data: {}
|
|
566
|
+
},
|
|
567
|
+
required: ["defined", "code", "status", "message"]
|
|
568
|
+
};
|
|
569
|
+
const baseSchemaConvertOptions = {};
|
|
449
570
|
if (commonSchemas) {
|
|
450
|
-
|
|
571
|
+
baseSchemaConvertOptions.components = [];
|
|
451
572
|
for (const key in commonSchemas) {
|
|
452
|
-
const
|
|
573
|
+
const options = commonSchemas[key];
|
|
574
|
+
if (options.schema === void 0) {
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
const { schema, strategy = "input" } = options;
|
|
453
578
|
const [required, json] = await this.converter.convert(schema, { strategy });
|
|
454
579
|
const allowedStrategies = [strategy];
|
|
455
580
|
if (strategy === "input") {
|
|
@@ -463,7 +588,7 @@ ${errors.join("\n\n")}`
|
|
|
463
588
|
allowedStrategies.push("input");
|
|
464
589
|
}
|
|
465
590
|
}
|
|
466
|
-
|
|
591
|
+
baseSchemaConvertOptions.components.push({
|
|
467
592
|
schema,
|
|
468
593
|
required,
|
|
469
594
|
ref: `#/components/schemas/${key}`,
|
|
@@ -473,11 +598,19 @@ ${errors.join("\n\n")}`
|
|
|
473
598
|
doc.components ??= {};
|
|
474
599
|
doc.components.schemas ??= {};
|
|
475
600
|
for (const key in commonSchemas) {
|
|
476
|
-
const
|
|
601
|
+
const options = commonSchemas[key];
|
|
602
|
+
if (options.schema === void 0) {
|
|
603
|
+
if (options.error === "UndefinedError") {
|
|
604
|
+
doc.components.schemas[key] = toOpenAPISchema(undefinedErrorJsonSchema);
|
|
605
|
+
undefinedErrorJsonSchema = { $ref: `#/components/schemas/${key}` };
|
|
606
|
+
}
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
const { schema, strategy = "input" } = options;
|
|
477
610
|
const [, json] = await this.converter.convert(
|
|
478
611
|
schema,
|
|
479
612
|
{
|
|
480
|
-
...
|
|
613
|
+
...baseSchemaConvertOptions,
|
|
481
614
|
strategy,
|
|
482
615
|
minStructureDepthForRef: 1
|
|
483
616
|
// not allow use $ref for root schemas
|
|
@@ -486,7 +619,7 @@ ${errors.join("\n\n")}`
|
|
|
486
619
|
doc.components.schemas[key] = toOpenAPISchema(json);
|
|
487
620
|
}
|
|
488
621
|
}
|
|
489
|
-
return
|
|
622
|
+
return { baseSchemaConvertOptions, undefinedErrorJsonSchema };
|
|
490
623
|
}
|
|
491
624
|
async #request(doc, ref, def, baseSchemaConvertOptions) {
|
|
492
625
|
const method = fallbackContractConfig("defaultMethod", def.route.method);
|
|
@@ -507,13 +640,15 @@ ${errors.join("\n\n")}`
|
|
|
507
640
|
def.inputSchema,
|
|
508
641
|
{
|
|
509
642
|
...baseSchemaConvertOptions,
|
|
510
|
-
strategy: "input"
|
|
511
|
-
minStructureDepthForRef: dynamicParams?.length || inputStructure === "detailed" ? 1 : 0
|
|
643
|
+
strategy: "input"
|
|
512
644
|
}
|
|
513
645
|
);
|
|
514
646
|
if (isAnySchema(schema) && !dynamicParams?.length) {
|
|
515
647
|
return;
|
|
516
648
|
}
|
|
649
|
+
if (inputStructure === "detailed" || inputStructure === "compact" && (dynamicParams?.length || method === "GET")) {
|
|
650
|
+
schema = simplifyComposedObjectJsonSchemasAndRefs(schema, doc);
|
|
651
|
+
}
|
|
517
652
|
if (inputStructure === "compact") {
|
|
518
653
|
if (dynamicParams?.length) {
|
|
519
654
|
const error2 = new OpenAPIGeneratorError(
|
|
@@ -553,7 +688,7 @@ ${errors.join("\n\n")}`
|
|
|
553
688
|
if (!isObjectSchema(schema)) {
|
|
554
689
|
throw error;
|
|
555
690
|
}
|
|
556
|
-
const resolvedParamSchema = schema.properties?.params !== void 0 ?
|
|
691
|
+
const resolvedParamSchema = schema.properties?.params !== void 0 ? simplifyComposedObjectJsonSchemasAndRefs(schema.properties.params, doc) : void 0;
|
|
557
692
|
if (dynamicParams?.length && (resolvedParamSchema === void 0 || !isObjectSchema(resolvedParamSchema) || !checkParamsSchema(resolvedParamSchema, dynamicParams))) {
|
|
558
693
|
throw new OpenAPIGeneratorError(
|
|
559
694
|
'When input structure is "detailed" and path has dynamic params, the "params" schema must be an object with all dynamic params as required.'
|
|
@@ -562,7 +697,7 @@ ${errors.join("\n\n")}`
|
|
|
562
697
|
for (const from of ["params", "query", "headers"]) {
|
|
563
698
|
const fromSchema = schema.properties?.[from];
|
|
564
699
|
if (fromSchema !== void 0) {
|
|
565
|
-
const resolvedSchema =
|
|
700
|
+
const resolvedSchema = simplifyComposedObjectJsonSchemasAndRefs(fromSchema, doc);
|
|
566
701
|
if (!isObjectSchema(resolvedSchema)) {
|
|
567
702
|
throw error;
|
|
568
703
|
}
|
|
@@ -623,13 +758,14 @@ ${errors.join("\n\n")}`
|
|
|
623
758
|
|
|
624
759
|
But got: ${stringifyJSON(item)}
|
|
625
760
|
`);
|
|
626
|
-
|
|
761
|
+
const simplifiedItem = simplifyComposedObjectJsonSchemasAndRefs(item, doc);
|
|
762
|
+
if (!isObjectSchema(simplifiedItem)) {
|
|
627
763
|
throw error;
|
|
628
764
|
}
|
|
629
765
|
let schemaStatus;
|
|
630
766
|
let schemaDescription;
|
|
631
|
-
if (
|
|
632
|
-
const statusSchema = resolveOpenAPIJsonSchemaRef(doc,
|
|
767
|
+
if (simplifiedItem.properties?.status !== void 0) {
|
|
768
|
+
const statusSchema = resolveOpenAPIJsonSchemaRef(doc, simplifiedItem.properties.status);
|
|
633
769
|
if (typeof statusSchema !== "object" || statusSchema.const === void 0 || typeof statusSchema.const !== "number" || !Number.isInteger(statusSchema.const) || isORPCErrorStatus(statusSchema.const)) {
|
|
634
770
|
throw error;
|
|
635
771
|
}
|
|
@@ -649,8 +785,8 @@ ${errors.join("\n\n")}`
|
|
|
649
785
|
ref.responses[itemStatus] = {
|
|
650
786
|
description: itemDescription
|
|
651
787
|
};
|
|
652
|
-
if (
|
|
653
|
-
const headersSchema =
|
|
788
|
+
if (simplifiedItem.properties?.headers !== void 0) {
|
|
789
|
+
const headersSchema = simplifyComposedObjectJsonSchemasAndRefs(simplifiedItem.properties.headers, doc);
|
|
654
790
|
if (!isObjectSchema(headersSchema)) {
|
|
655
791
|
throw error;
|
|
656
792
|
}
|
|
@@ -660,61 +796,53 @@ ${errors.join("\n\n")}`
|
|
|
660
796
|
ref.responses[itemStatus].headers ??= {};
|
|
661
797
|
ref.responses[itemStatus].headers[key] = {
|
|
662
798
|
schema: toOpenAPISchema(headerSchema),
|
|
663
|
-
required:
|
|
799
|
+
required: simplifiedItem.required?.includes("headers") && headersSchema.required?.includes(key)
|
|
664
800
|
};
|
|
665
801
|
}
|
|
666
802
|
}
|
|
667
803
|
}
|
|
668
|
-
if (
|
|
804
|
+
if (simplifiedItem.properties?.body !== void 0) {
|
|
669
805
|
ref.responses[itemStatus].content = toOpenAPIContent(
|
|
670
|
-
applySchemaOptionality(
|
|
806
|
+
applySchemaOptionality(simplifiedItem.required?.includes("body") ?? false, simplifiedItem.properties.body)
|
|
671
807
|
);
|
|
672
808
|
}
|
|
673
809
|
}
|
|
674
810
|
}
|
|
675
|
-
async #errorResponse(ref, def, baseSchemaConvertOptions) {
|
|
811
|
+
async #errorResponse(ref, def, baseSchemaConvertOptions, undefinedErrorSchema, customErrorResponseBodySchema) {
|
|
676
812
|
const errorMap = def.errorMap;
|
|
677
|
-
const
|
|
813
|
+
const errorResponsesByStatus = {};
|
|
678
814
|
for (const code in errorMap) {
|
|
679
815
|
const config = errorMap[code];
|
|
680
816
|
if (!config) {
|
|
681
817
|
continue;
|
|
682
818
|
}
|
|
683
819
|
const status = fallbackORPCErrorStatus(code, config.status);
|
|
684
|
-
const
|
|
820
|
+
const defaultMessage = fallbackORPCErrorMessage(code, config.message);
|
|
821
|
+
errorResponsesByStatus[status] ??= { status, definedErrorDefinitions: [], errorSchemaVariants: [] };
|
|
685
822
|
const [dataRequired, dataSchema] = await this.converter.convert(config.data, { ...baseSchemaConvertOptions, strategy: "output" });
|
|
686
|
-
|
|
687
|
-
|
|
823
|
+
errorResponsesByStatus[status].definedErrorDefinitions.push([code, defaultMessage, dataRequired, dataSchema]);
|
|
824
|
+
errorResponsesByStatus[status].errorSchemaVariants.push({
|
|
688
825
|
type: "object",
|
|
689
826
|
properties: {
|
|
690
827
|
defined: { const: true },
|
|
691
828
|
code: { const: code },
|
|
692
829
|
status: { const: status },
|
|
693
|
-
message: { type: "string", default:
|
|
830
|
+
message: { type: "string", default: defaultMessage },
|
|
694
831
|
data: dataSchema
|
|
695
832
|
},
|
|
696
833
|
required: dataRequired ? ["defined", "code", "status", "message", "data"] : ["defined", "code", "status", "message"]
|
|
697
834
|
});
|
|
698
835
|
}
|
|
699
836
|
ref.responses ??= {};
|
|
700
|
-
for (const
|
|
701
|
-
const
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
837
|
+
for (const statusString in errorResponsesByStatus) {
|
|
838
|
+
const errorResponse = errorResponsesByStatus[statusString];
|
|
839
|
+
const customBodySchema = value(customErrorResponseBodySchema, errorResponse.definedErrorDefinitions, errorResponse.status);
|
|
840
|
+
ref.responses[statusString] = {
|
|
841
|
+
description: statusString,
|
|
842
|
+
content: toOpenAPIContent(customBodySchema ?? {
|
|
705
843
|
oneOf: [
|
|
706
|
-
...
|
|
707
|
-
|
|
708
|
-
type: "object",
|
|
709
|
-
properties: {
|
|
710
|
-
defined: { const: false },
|
|
711
|
-
code: { type: "string" },
|
|
712
|
-
status: { type: "number" },
|
|
713
|
-
message: { type: "string" },
|
|
714
|
-
data: {}
|
|
715
|
-
},
|
|
716
|
-
required: ["defined", "code", "status", "message"]
|
|
717
|
-
}
|
|
844
|
+
...errorResponse.errorSchemaVariants,
|
|
845
|
+
undefinedErrorSchema
|
|
718
846
|
]
|
|
719
847
|
})
|
|
720
848
|
};
|
|
@@ -722,4 +850,4 @@ ${errors.join("\n\n")}`
|
|
|
722
850
|
}
|
|
723
851
|
}
|
|
724
852
|
|
|
725
|
-
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,
|
|
853
|
+
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, separateObjectSchema as m, filterSchemaBranches as n, applySchemaOptionality as o, expandUnionSchema as p, expandArrayableSchema as q, resolveOpenAPIJsonSchemaRef as r, simplifyComposedObjectJsonSchemasAndRefs as s, toOpenAPIPath as t, isPrimitiveSchema as u };
|
|
@@ -2,15 +2,17 @@ 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';
|
|
9
9
|
|
|
10
10
|
class StandardOpenAPICodec {
|
|
11
|
-
constructor(serializer) {
|
|
11
|
+
constructor(serializer, options = {}) {
|
|
12
12
|
this.serializer = serializer;
|
|
13
|
+
this.customErrorResponseBodyEncoder = options.customErrorResponseBodyEncoder;
|
|
13
14
|
}
|
|
15
|
+
customErrorResponseBodyEncoder;
|
|
14
16
|
async decode(request, params, procedure) {
|
|
15
17
|
const inputStructure = fallbackContractConfig("defaultInputStructure", procedure["~orpc"].route.inputStructure);
|
|
16
18
|
if (inputStructure === "compact") {
|
|
@@ -73,10 +75,11 @@ class StandardOpenAPICodec {
|
|
|
73
75
|
};
|
|
74
76
|
}
|
|
75
77
|
encodeError(error) {
|
|
78
|
+
const body = this.customErrorResponseBodyEncoder?.(error) ?? error.toJSON();
|
|
76
79
|
return {
|
|
77
80
|
status: error.status,
|
|
78
81
|
headers: {},
|
|
79
|
-
body: this.serializer.serialize(
|
|
82
|
+
body: this.serializer.serialize(body, { outputFormat: "plain" })
|
|
80
83
|
};
|
|
81
84
|
}
|
|
82
85
|
#isDetailedOutput(output) {
|
|
@@ -97,14 +100,22 @@ function toRou3Pattern(path) {
|
|
|
97
100
|
return standardizeHTTPPath(path).replace(/\/\{\+([^}]+)\}/g, "/**:$1").replace(/\/\{([^}]+)\}/g, "/:$1");
|
|
98
101
|
}
|
|
99
102
|
function decodeParams(params) {
|
|
100
|
-
return Object.fromEntries(Object.entries(params).map(([key, value]) => [key,
|
|
103
|
+
return Object.fromEntries(Object.entries(params).map(([key, value]) => [key, tryDecodeURIComponent(value)]));
|
|
101
104
|
}
|
|
102
105
|
|
|
103
106
|
class StandardOpenAPIMatcher {
|
|
107
|
+
filter;
|
|
104
108
|
tree = createRouter();
|
|
105
109
|
pendingRouters = [];
|
|
110
|
+
constructor(options = {}) {
|
|
111
|
+
this.filter = options.filter ?? true;
|
|
112
|
+
}
|
|
106
113
|
init(router, path = []) {
|
|
107
|
-
const laziedOptions = traverseContractProcedures({ router, path }, (
|
|
114
|
+
const laziedOptions = traverseContractProcedures({ router, path }, (traverseOptions) => {
|
|
115
|
+
if (!value(this.filter, traverseOptions)) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const { path: path2, contract } = traverseOptions;
|
|
108
119
|
const method = fallbackContractConfig("defaultMethod", contract["~orpc"].route.method);
|
|
109
120
|
const httpPath = toRou3Pattern(contract["~orpc"].route.path ?? toHttpPath(path2));
|
|
110
121
|
if (isProcedure(contract)) {
|
|
@@ -168,10 +179,10 @@ class StandardOpenAPIMatcher {
|
|
|
168
179
|
class StandardOpenAPIHandler extends StandardHandler {
|
|
169
180
|
constructor(router, options) {
|
|
170
181
|
const jsonSerializer = new StandardOpenAPIJsonSerializer(options);
|
|
171
|
-
const bracketNotationSerializer = new StandardBracketNotationSerializer();
|
|
182
|
+
const bracketNotationSerializer = new StandardBracketNotationSerializer(options);
|
|
172
183
|
const serializer = new StandardOpenAPISerializer(jsonSerializer, bracketNotationSerializer);
|
|
173
|
-
const matcher = new StandardOpenAPIMatcher();
|
|
174
|
-
const codec = new StandardOpenAPICodec(serializer);
|
|
184
|
+
const matcher = new StandardOpenAPIMatcher(options);
|
|
185
|
+
const codec = new StandardOpenAPICodec(serializer, options);
|
|
175
186
|
super(router, matcher, codec, options);
|
|
176
187
|
}
|
|
177
188
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { StandardOpenAPISerializer, StandardOpenAPIJsonSerializerOptions, StandardBracketNotationSerializerOptions } from '@orpc/openapi-client/standard';
|
|
2
|
+
import { AnyProcedure, TraverseContractProcedureCallbackOptions, AnyRouter, Context, Router } from '@orpc/server';
|
|
3
|
+
import { StandardCodec, StandardParams, StandardMatcher, StandardMatchResult, StandardHandlerOptions, StandardHandler } from '@orpc/server/standard';
|
|
4
|
+
import { ORPCError, HTTPPath } from '@orpc/client';
|
|
5
|
+
import { StandardLazyRequest, StandardResponse } from '@orpc/standard-server';
|
|
6
|
+
import { Value } from '@orpc/shared';
|
|
7
|
+
|
|
8
|
+
interface StandardOpenAPICodecOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Customize how an ORPC error is encoded into a response body.
|
|
11
|
+
* Use this if your API needs a different error output structure.
|
|
12
|
+
*
|
|
13
|
+
* @remarks
|
|
14
|
+
* - Return `null | undefined` to fallback to default behavior
|
|
15
|
+
*
|
|
16
|
+
* @default ((e) => e.toJSON())
|
|
17
|
+
*/
|
|
18
|
+
customErrorResponseBodyEncoder?: (error: ORPCError<any, any>) => unknown;
|
|
19
|
+
}
|
|
20
|
+
declare class StandardOpenAPICodec implements StandardCodec {
|
|
21
|
+
#private;
|
|
22
|
+
private readonly serializer;
|
|
23
|
+
private readonly customErrorResponseBodyEncoder;
|
|
24
|
+
constructor(serializer: StandardOpenAPISerializer, options?: StandardOpenAPICodecOptions);
|
|
25
|
+
decode(request: StandardLazyRequest, params: StandardParams | undefined, procedure: AnyProcedure): Promise<unknown>;
|
|
26
|
+
encode(output: unknown, procedure: AnyProcedure): StandardResponse;
|
|
27
|
+
encodeError(error: ORPCError<any, any>): StandardResponse;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface StandardOpenAPIMatcherOptions {
|
|
31
|
+
/**
|
|
32
|
+
* Filter procedures. Return `false` to exclude a procedure from matching.
|
|
33
|
+
*
|
|
34
|
+
* @default true
|
|
35
|
+
*/
|
|
36
|
+
filter?: Value<boolean, [options: TraverseContractProcedureCallbackOptions]>;
|
|
37
|
+
}
|
|
38
|
+
declare class StandardOpenAPIMatcher implements StandardMatcher {
|
|
39
|
+
private readonly filter;
|
|
40
|
+
private readonly tree;
|
|
41
|
+
private pendingRouters;
|
|
42
|
+
constructor(options?: StandardOpenAPIMatcherOptions);
|
|
43
|
+
init(router: AnyRouter, path?: readonly string[]): void;
|
|
44
|
+
match(method: string, pathname: HTTPPath): Promise<StandardMatchResult>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface StandardOpenAPIHandlerOptions<T extends Context> extends StandardHandlerOptions<T>, StandardOpenAPIJsonSerializerOptions, StandardBracketNotationSerializerOptions, StandardOpenAPIMatcherOptions, StandardOpenAPICodecOptions {
|
|
48
|
+
}
|
|
49
|
+
declare class StandardOpenAPIHandler<T extends Context> extends StandardHandler<T> {
|
|
50
|
+
constructor(router: Router<any, T>, options: NoInfer<StandardOpenAPIHandlerOptions<T>>);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export { StandardOpenAPICodec as a, StandardOpenAPIHandler as c, StandardOpenAPIMatcher as e };
|
|
54
|
+
export type { StandardOpenAPICodecOptions as S, StandardOpenAPIHandlerOptions as b, StandardOpenAPIMatcherOptions as d };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { StandardOpenAPISerializer, StandardOpenAPIJsonSerializerOptions, StandardBracketNotationSerializerOptions } from '@orpc/openapi-client/standard';
|
|
2
|
+
import { AnyProcedure, TraverseContractProcedureCallbackOptions, AnyRouter, Context, Router } from '@orpc/server';
|
|
3
|
+
import { StandardCodec, StandardParams, StandardMatcher, StandardMatchResult, StandardHandlerOptions, StandardHandler } from '@orpc/server/standard';
|
|
4
|
+
import { ORPCError, HTTPPath } from '@orpc/client';
|
|
5
|
+
import { StandardLazyRequest, StandardResponse } from '@orpc/standard-server';
|
|
6
|
+
import { Value } from '@orpc/shared';
|
|
7
|
+
|
|
8
|
+
interface StandardOpenAPICodecOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Customize how an ORPC error is encoded into a response body.
|
|
11
|
+
* Use this if your API needs a different error output structure.
|
|
12
|
+
*
|
|
13
|
+
* @remarks
|
|
14
|
+
* - Return `null | undefined` to fallback to default behavior
|
|
15
|
+
*
|
|
16
|
+
* @default ((e) => e.toJSON())
|
|
17
|
+
*/
|
|
18
|
+
customErrorResponseBodyEncoder?: (error: ORPCError<any, any>) => unknown;
|
|
19
|
+
}
|
|
20
|
+
declare class StandardOpenAPICodec implements StandardCodec {
|
|
21
|
+
#private;
|
|
22
|
+
private readonly serializer;
|
|
23
|
+
private readonly customErrorResponseBodyEncoder;
|
|
24
|
+
constructor(serializer: StandardOpenAPISerializer, options?: StandardOpenAPICodecOptions);
|
|
25
|
+
decode(request: StandardLazyRequest, params: StandardParams | undefined, procedure: AnyProcedure): Promise<unknown>;
|
|
26
|
+
encode(output: unknown, procedure: AnyProcedure): StandardResponse;
|
|
27
|
+
encodeError(error: ORPCError<any, any>): StandardResponse;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface StandardOpenAPIMatcherOptions {
|
|
31
|
+
/**
|
|
32
|
+
* Filter procedures. Return `false` to exclude a procedure from matching.
|
|
33
|
+
*
|
|
34
|
+
* @default true
|
|
35
|
+
*/
|
|
36
|
+
filter?: Value<boolean, [options: TraverseContractProcedureCallbackOptions]>;
|
|
37
|
+
}
|
|
38
|
+
declare class StandardOpenAPIMatcher implements StandardMatcher {
|
|
39
|
+
private readonly filter;
|
|
40
|
+
private readonly tree;
|
|
41
|
+
private pendingRouters;
|
|
42
|
+
constructor(options?: StandardOpenAPIMatcherOptions);
|
|
43
|
+
init(router: AnyRouter, path?: readonly string[]): void;
|
|
44
|
+
match(method: string, pathname: HTTPPath): Promise<StandardMatchResult>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface StandardOpenAPIHandlerOptions<T extends Context> extends StandardHandlerOptions<T>, StandardOpenAPIJsonSerializerOptions, StandardBracketNotationSerializerOptions, StandardOpenAPIMatcherOptions, StandardOpenAPICodecOptions {
|
|
48
|
+
}
|
|
49
|
+
declare class StandardOpenAPIHandler<T extends Context> extends StandardHandler<T> {
|
|
50
|
+
constructor(router: Router<any, T>, options: NoInfer<StandardOpenAPIHandlerOptions<T>>);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export { StandardOpenAPICodec as a, StandardOpenAPIHandler as c, StandardOpenAPIMatcher as e };
|
|
54
|
+
export type { StandardOpenAPICodecOptions as S, StandardOpenAPIHandlerOptions as b, StandardOpenAPIMatcherOptions as d };
|