@orpc/openapi 0.0.0-next.f9e0a4c → 0.0.0-next.f9e5dec

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.
Files changed (31) hide show
  1. package/README.md +5 -1
  2. package/dist/adapters/aws-lambda/index.d.mts +2 -1
  3. package/dist/adapters/aws-lambda/index.d.ts +2 -1
  4. package/dist/adapters/aws-lambda/index.mjs +1 -1
  5. package/dist/adapters/fastify/index.d.mts +23 -0
  6. package/dist/adapters/fastify/index.d.ts +23 -0
  7. package/dist/adapters/fastify/index.mjs +18 -0
  8. package/dist/adapters/fetch/index.d.mts +7 -3
  9. package/dist/adapters/fetch/index.d.ts +7 -3
  10. package/dist/adapters/fetch/index.mjs +1 -1
  11. package/dist/adapters/node/index.d.mts +7 -3
  12. package/dist/adapters/node/index.d.ts +7 -3
  13. package/dist/adapters/node/index.mjs +1 -1
  14. package/dist/adapters/standard/index.d.mts +7 -16
  15. package/dist/adapters/standard/index.d.ts +7 -16
  16. package/dist/adapters/standard/index.mjs +1 -1
  17. package/dist/index.d.mts +10 -4
  18. package/dist/index.d.ts +10 -4
  19. package/dist/index.mjs +3 -3
  20. package/dist/plugins/index.d.mts +19 -4
  21. package/dist/plugins/index.d.ts +19 -4
  22. package/dist/plugins/index.mjs +59 -19
  23. package/dist/shared/{openapi.BVXcB0u4.mjs → openapi.DIt-Z9W1.mjs} +6 -3
  24. package/dist/shared/{openapi.DRqtR2lw.mjs → openapi.DNv9yFfn.mjs} +140 -37
  25. package/dist/shared/openapi.DwaweYRb.d.mts +54 -0
  26. package/dist/shared/openapi.DwaweYRb.d.ts +54 -0
  27. package/dist/shared/{openapi.TOD62C7O.d.mts → openapi.dbQeFCUJ.d.mts} +14 -2
  28. package/dist/shared/{openapi.TOD62C7O.d.ts → openapi.dbQeFCUJ.d.ts} +14 -2
  29. package/package.json +17 -10
  30. package/dist/shared/openapi.CQmjvnb0.d.mts +0 -31
  31. package/dist/shared/openapi.CQmjvnb0.d.ts +0 -31
@@ -1,11 +1,11 @@
1
1
  import { stringifyJSON, once, value } from '@orpc/shared';
2
- import { O as OpenAPIGenerator } from '../shared/openapi.DRqtR2lw.mjs';
2
+ import { O as OpenAPIGenerator } from '../shared/openapi.DNv9yFfn.mjs';
3
3
  import '@orpc/client';
4
4
  import '@orpc/client/standard';
5
5
  import '@orpc/contract';
6
6
  import '@orpc/openapi-client/standard';
7
7
  import '@orpc/server';
8
- import '@orpc/json-schema-typed/draft-2020-12';
8
+ import 'json-schema-typed/draft-2020-12';
9
9
 
10
10
  class OpenAPIReferencePlugin {
11
11
  generator;
@@ -14,7 +14,9 @@ class OpenAPIReferencePlugin {
14
14
  docsPath;
15
15
  docsTitle;
16
16
  docsHead;
17
+ docsProvider;
17
18
  docsScriptUrl;
19
+ docsCssUrl;
18
20
  docsConfig;
19
21
  renderDocsHtml;
20
22
  constructor(options = {}) {
@@ -22,16 +24,59 @@ class OpenAPIReferencePlugin {
22
24
  this.docsPath = options.docsPath ?? "/";
23
25
  this.docsTitle = options.docsTitle ?? "API Reference";
24
26
  this.docsConfig = options.docsConfig ?? void 0;
25
- this.docsScriptUrl = options.docsScriptUrl ?? "https://cdn.jsdelivr.net/npm/@scalar/api-reference";
27
+ this.docsProvider = options.docsProvider ?? "scalar";
28
+ this.docsScriptUrl = options.docsScriptUrl ?? (this.docsProvider === "swagger" ? "https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js" : "https://cdn.jsdelivr.net/npm/@scalar/api-reference");
29
+ this.docsCssUrl = options.docsCssUrl ?? (this.docsProvider === "swagger" ? "https://unpkg.com/swagger-ui-dist/swagger-ui.css" : void 0);
26
30
  this.docsHead = options.docsHead ?? "";
27
31
  this.specPath = options.specPath ?? "/spec.json";
28
32
  this.generator = new OpenAPIGenerator(options);
29
33
  const esc = (s) => s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
30
- this.renderDocsHtml = options.renderDocsHtml ?? ((specUrl, title, head, scriptUrl, config, spec) => {
31
- const finalConfig = {
32
- content: stringifyJSON(spec),
33
- ...config
34
- };
34
+ this.renderDocsHtml = options.renderDocsHtml ?? ((specUrl, title, head, scriptUrl, config, spec, docsProvider, cssUrl) => {
35
+ let body;
36
+ if (docsProvider === "swagger") {
37
+ const swaggerConfig = {
38
+ dom_id: "#app",
39
+ spec,
40
+ deepLinking: true,
41
+ presets: [
42
+ "SwaggerUIBundle.presets.apis",
43
+ "SwaggerUIBundle.presets.standalone"
44
+ ],
45
+ plugins: [
46
+ "SwaggerUIBundle.plugins.DownloadUrl"
47
+ ],
48
+ ...config
49
+ };
50
+ body = `
51
+ <body>
52
+ <div id="app"></div>
53
+
54
+ <script src="${esc(scriptUrl)}"><\/script>
55
+
56
+ <script>
57
+ window.onload = () => {
58
+ window.ui = SwaggerUIBundle(${stringifyJSON(swaggerConfig).replace(/"(SwaggerUIBundle\.[^"]+)"/g, "$1")})
59
+ }
60
+ <\/script>
61
+ </body>
62
+ `;
63
+ } else {
64
+ const scalarConfig = {
65
+ content: stringifyJSON(spec),
66
+ ...config
67
+ };
68
+ body = `
69
+ <body>
70
+ <div id="app" data-config="${esc(stringifyJSON(scalarConfig))}"></div>
71
+
72
+ <script src="${esc(scriptUrl)}"><\/script>
73
+
74
+ <script>
75
+ Scalar.createApiReference('#app', JSON.parse(document.getElementById('app').dataset.config))
76
+ <\/script>
77
+ </body>
78
+ `;
79
+ }
35
80
  return `
36
81
  <!doctype html>
37
82
  <html>
@@ -39,19 +84,12 @@ class OpenAPIReferencePlugin {
39
84
  <meta charset="utf-8" />
40
85
  <meta name="viewport" content="width=device-width, initial-scale=1" />
41
86
  <title>${esc(title)}</title>
87
+ ${cssUrl ? `<link rel="stylesheet" type="text/css" href="${esc(cssUrl)}" />` : ""}
42
88
  ${head}
43
89
  </head>
44
- <body>
45
- <div id="app" data-config="${esc(stringifyJSON(finalConfig))}"></div>
46
-
47
- <script src="${esc(scriptUrl)}"><\/script>
48
-
49
- <script>
50
- Scalar.createApiReference('#app', JSON.parse(document.getElementById('app').dataset.config))
51
- <\/script>
52
- </body>
90
+ ${body}
53
91
  </html>
54
- `;
92
+ `;
55
93
  });
56
94
  }
57
95
  init(options, router) {
@@ -89,7 +127,9 @@ class OpenAPIReferencePlugin {
89
127
  await value(this.docsHead, options2),
90
128
  await value(this.docsScriptUrl, options2),
91
129
  await value(this.docsConfig, options2),
92
- await generateSpec()
130
+ await generateSpec(),
131
+ this.docsProvider,
132
+ await value(this.docsCssUrl, options2)
93
133
  );
94
134
  return {
95
135
  matched: true,
@@ -8,9 +8,11 @@ import { traverseContractProcedures, isProcedure, getLazyMeta, unlazy, getRouter
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(error.toJSON(), { outputFormat: "plain" })
82
+ body: this.serializer.serialize(body, { outputFormat: "plain" })
80
83
  };
81
84
  }
82
85
  #isDetailedOutput(output) {
@@ -179,7 +182,7 @@ class StandardOpenAPIHandler extends StandardHandler {
179
182
  const bracketNotationSerializer = new StandardBracketNotationSerializer(options);
180
183
  const serializer = new StandardOpenAPISerializer(jsonSerializer, bracketNotationSerializer);
181
184
  const matcher = new StandardOpenAPIMatcher(options);
182
- const codec = new StandardOpenAPICodec(serializer);
185
+ const codec = new StandardOpenAPICodec(serializer, options);
183
186
  super(router, matcher, codec, options);
184
187
  }
185
188
  }
@@ -4,7 +4,7 @@ import { fallbackContractConfig, getEventIteratorSchemaDetails } from '@orpc/con
4
4
  import { standardizeHTTPPath, StandardOpenAPIJsonSerializer, getDynamicParams } from '@orpc/openapi-client/standard';
5
5
  import { isProcedure, resolveContractProcedures } from '@orpc/server';
6
6
  import { isObject, stringifyJSON, findDeepMatches, toArray, clone, value } from '@orpc/shared';
7
- import { TypeName } from '@orpc/json-schema-typed/draft-2020-12';
7
+ import { TypeName } from 'json-schema-typed/draft-2020-12';
8
8
 
9
9
  const OPERATION_EXTENDER_SYMBOL = Symbol("ORPC_OPERATION_EXTENDER");
10
10
  function customOpenAPIOperation(o, extend) {
@@ -359,6 +359,107 @@ function resolveOpenAPIJsonSchemaRef(doc, schema) {
359
359
  const resolved = doc.components?.schemas?.[name];
360
360
  return resolved ?? schema;
361
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
+ }
362
463
 
363
464
  class CompositeSchemaConverter {
364
465
  converters;
@@ -389,19 +490,16 @@ class OpenAPIGenerator {
389
490
  *
390
491
  * @see {@link https://orpc.unnoq.com/docs/openapi/openapi-specification OpenAPI Specification Docs}
391
492
  */
392
- async generate(router, options = {}) {
393
- const filter = options.filter ?? (({ contract, path }) => {
394
- return !(options.exclude?.(contract, path) ?? false);
493
+ async generate(router, { customErrorResponseBodySchema, commonSchemas, filter: baseFilter, exclude, ...baseDoc } = {}) {
494
+ const filter = baseFilter ?? (({ contract, path }) => {
495
+ return !(exclude?.(contract, path) ?? false);
395
496
  });
396
497
  const doc = {
397
- ...clone(options),
398
- info: options.info ?? { title: "API Reference", version: "0.0.0" },
399
- openapi: "3.1.1",
400
- exclude: void 0,
401
- filter: void 0,
402
- commonSchemas: void 0
498
+ ...clone(baseDoc),
499
+ info: baseDoc.info ?? { title: "API Reference", version: "0.0.0" },
500
+ openapi: "3.1.1"
403
501
  };
404
- const { baseSchemaConvertOptions, undefinedErrorJsonSchema } = await this.#resolveCommonSchemas(doc, options.commonSchemas);
502
+ const { baseSchemaConvertOptions, undefinedErrorJsonSchema } = await this.#resolveCommonSchemas(doc, commonSchemas);
405
503
  const contracts = [];
406
504
  await resolveContractProcedures({ path: [], router }, (traverseOptions) => {
407
505
  if (!value(filter, traverseOptions)) {
@@ -429,7 +527,7 @@ class OpenAPIGenerator {
429
527
  };
430
528
  await this.#request(doc, operationObjectRef, def, baseSchemaConvertOptions);
431
529
  await this.#successResponse(doc, operationObjectRef, def, baseSchemaConvertOptions);
432
- await this.#errorResponse(operationObjectRef, def, baseSchemaConvertOptions, undefinedErrorJsonSchema);
530
+ await this.#errorResponse(operationObjectRef, def, baseSchemaConvertOptions, undefinedErrorJsonSchema, customErrorResponseBodySchema);
433
531
  }
434
532
  if (typeof def.route.spec === "function") {
435
533
  operationObjectRef = def.route.spec(operationObjectRef);
@@ -542,13 +640,15 @@ ${errors.join("\n\n")}`
542
640
  def.inputSchema,
543
641
  {
544
642
  ...baseSchemaConvertOptions,
545
- strategy: "input",
546
- minStructureDepthForRef: dynamicParams?.length || inputStructure === "detailed" ? 1 : 0
643
+ strategy: "input"
547
644
  }
548
645
  );
549
646
  if (isAnySchema(schema) && !dynamicParams?.length) {
550
647
  return;
551
648
  }
649
+ if (inputStructure === "detailed" || inputStructure === "compact" && (dynamicParams?.length || method === "GET")) {
650
+ schema = simplifyComposedObjectJsonSchemasAndRefs(schema, doc);
651
+ }
552
652
  if (inputStructure === "compact") {
553
653
  if (dynamicParams?.length) {
554
654
  const error2 = new OpenAPIGeneratorError(
@@ -588,7 +688,7 @@ ${errors.join("\n\n")}`
588
688
  if (!isObjectSchema(schema)) {
589
689
  throw error;
590
690
  }
591
- const resolvedParamSchema = schema.properties?.params !== void 0 ? resolveOpenAPIJsonSchemaRef(doc, schema.properties.params) : void 0;
691
+ const resolvedParamSchema = schema.properties?.params !== void 0 ? simplifyComposedObjectJsonSchemasAndRefs(schema.properties.params, doc) : void 0;
592
692
  if (dynamicParams?.length && (resolvedParamSchema === void 0 || !isObjectSchema(resolvedParamSchema) || !checkParamsSchema(resolvedParamSchema, dynamicParams))) {
593
693
  throw new OpenAPIGeneratorError(
594
694
  'When input structure is "detailed" and path has dynamic params, the "params" schema must be an object with all dynamic params as required.'
@@ -597,7 +697,7 @@ ${errors.join("\n\n")}`
597
697
  for (const from of ["params", "query", "headers"]) {
598
698
  const fromSchema = schema.properties?.[from];
599
699
  if (fromSchema !== void 0) {
600
- const resolvedSchema = resolveOpenAPIJsonSchemaRef(doc, fromSchema);
700
+ const resolvedSchema = simplifyComposedObjectJsonSchemasAndRefs(fromSchema, doc);
601
701
  if (!isObjectSchema(resolvedSchema)) {
602
702
  throw error;
603
703
  }
@@ -658,13 +758,14 @@ ${errors.join("\n\n")}`
658
758
 
659
759
  But got: ${stringifyJSON(item)}
660
760
  `);
661
- if (!isObjectSchema(item)) {
761
+ const simplifiedItem = simplifyComposedObjectJsonSchemasAndRefs(item, doc);
762
+ if (!isObjectSchema(simplifiedItem)) {
662
763
  throw error;
663
764
  }
664
765
  let schemaStatus;
665
766
  let schemaDescription;
666
- if (item.properties?.status !== void 0) {
667
- const statusSchema = resolveOpenAPIJsonSchemaRef(doc, item.properties.status);
767
+ if (simplifiedItem.properties?.status !== void 0) {
768
+ const statusSchema = resolveOpenAPIJsonSchemaRef(doc, simplifiedItem.properties.status);
668
769
  if (typeof statusSchema !== "object" || statusSchema.const === void 0 || typeof statusSchema.const !== "number" || !Number.isInteger(statusSchema.const) || isORPCErrorStatus(statusSchema.const)) {
669
770
  throw error;
670
771
  }
@@ -684,8 +785,8 @@ ${errors.join("\n\n")}`
684
785
  ref.responses[itemStatus] = {
685
786
  description: itemDescription
686
787
  };
687
- if (item.properties?.headers !== void 0) {
688
- const headersSchema = resolveOpenAPIJsonSchemaRef(doc, item.properties.headers);
788
+ if (simplifiedItem.properties?.headers !== void 0) {
789
+ const headersSchema = simplifyComposedObjectJsonSchemasAndRefs(simplifiedItem.properties.headers, doc);
689
790
  if (!isObjectSchema(headersSchema)) {
690
791
  throw error;
691
792
  }
@@ -695,50 +796,52 @@ ${errors.join("\n\n")}`
695
796
  ref.responses[itemStatus].headers ??= {};
696
797
  ref.responses[itemStatus].headers[key] = {
697
798
  schema: toOpenAPISchema(headerSchema),
698
- required: item.required?.includes("headers") && headersSchema.required?.includes(key)
799
+ required: simplifiedItem.required?.includes("headers") && headersSchema.required?.includes(key)
699
800
  };
700
801
  }
701
802
  }
702
803
  }
703
- if (item.properties?.body !== void 0) {
804
+ if (simplifiedItem.properties?.body !== void 0) {
704
805
  ref.responses[itemStatus].content = toOpenAPIContent(
705
- applySchemaOptionality(item.required?.includes("body") ?? false, item.properties.body)
806
+ applySchemaOptionality(simplifiedItem.required?.includes("body") ?? false, simplifiedItem.properties.body)
706
807
  );
707
808
  }
708
809
  }
709
810
  }
710
- async #errorResponse(ref, def, baseSchemaConvertOptions, undefinedErrorSchema) {
811
+ async #errorResponse(ref, def, baseSchemaConvertOptions, undefinedErrorSchema, customErrorResponseBodySchema) {
711
812
  const errorMap = def.errorMap;
712
- const errors = {};
813
+ const errorResponsesByStatus = {};
713
814
  for (const code in errorMap) {
714
815
  const config = errorMap[code];
715
816
  if (!config) {
716
817
  continue;
717
818
  }
718
819
  const status = fallbackORPCErrorStatus(code, config.status);
719
- const message = fallbackORPCErrorMessage(code, config.message);
820
+ const defaultMessage = fallbackORPCErrorMessage(code, config.message);
821
+ errorResponsesByStatus[status] ??= { status, definedErrorDefinitions: [], errorSchemaVariants: [] };
720
822
  const [dataRequired, dataSchema] = await this.converter.convert(config.data, { ...baseSchemaConvertOptions, strategy: "output" });
721
- errors[status] ??= [];
722
- errors[status].push({
823
+ errorResponsesByStatus[status].definedErrorDefinitions.push([code, defaultMessage, dataRequired, dataSchema]);
824
+ errorResponsesByStatus[status].errorSchemaVariants.push({
723
825
  type: "object",
724
826
  properties: {
725
827
  defined: { const: true },
726
828
  code: { const: code },
727
829
  status: { const: status },
728
- message: { type: "string", default: message },
830
+ message: { type: "string", default: defaultMessage },
729
831
  data: dataSchema
730
832
  },
731
833
  required: dataRequired ? ["defined", "code", "status", "message", "data"] : ["defined", "code", "status", "message"]
732
834
  });
733
835
  }
734
836
  ref.responses ??= {};
735
- for (const status in errors) {
736
- const schemas = errors[status];
737
- ref.responses[status] = {
738
- description: status,
739
- content: toOpenAPIContent({
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 ?? {
740
843
  oneOf: [
741
- ...schemas,
844
+ ...errorResponse.errorSchemaVariants,
742
845
  undefinedErrorSchema
743
846
  ]
744
847
  })
@@ -747,4 +850,4 @@ ${errors.join("\n\n")}`
747
850
  }
748
851
  }
749
852
 
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 };
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 };
@@ -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 };
@@ -2,7 +2,7 @@ import { AnySchema, OpenAPI, AnyContractProcedure, AnyContractRouter } from '@or
2
2
  import { StandardOpenAPIJsonSerializerOptions } from '@orpc/openapi-client/standard';
3
3
  import { AnyProcedure, TraverseContractProcedureCallbackOptions, AnyRouter } from '@orpc/server';
4
4
  import { Promisable, Value } from '@orpc/shared';
5
- import { JSONSchema } from '@orpc/json-schema-typed/draft-2020-12';
5
+ import { JSONSchema } from 'json-schema-typed/draft-2020-12';
6
6
 
7
7
  interface SchemaConverterComponent {
8
8
  allowedStrategies: readonly SchemaConvertOptions['strategy'][];
@@ -85,6 +85,18 @@ interface OpenAPIGeneratorGenerateOptions extends Partial<Omit<OpenAPI.Document,
85
85
  error: 'UndefinedError';
86
86
  schema?: never;
87
87
  }>;
88
+ /**
89
+ * Define a custom JSON schema for the error response body when using
90
+ * type-safe errors. Helps align ORPC error formatting with existing API
91
+ * response standards or conventions.
92
+ *
93
+ * @remarks
94
+ * - Return `null | undefined` to use the default error response body shaper.
95
+ */
96
+ customErrorResponseBodySchema?: Value<JSONSchema | undefined | null, [
97
+ definedErrors: [code: string, defaultMessage: string, dataRequired: boolean, dataSchema: JSONSchema][],
98
+ status: number
99
+ ]>;
88
100
  }
89
101
  /**
90
102
  * The generator that converts oRPC routers/contracts to OpenAPI specifications.
@@ -101,7 +113,7 @@ declare class OpenAPIGenerator {
101
113
  *
102
114
  * @see {@link https://orpc.unnoq.com/docs/openapi/openapi-specification OpenAPI Specification Docs}
103
115
  */
104
- generate(router: AnyContractRouter | AnyRouter, options?: OpenAPIGeneratorGenerateOptions): Promise<OpenAPI.Document>;
116
+ generate(router: AnyContractRouter | AnyRouter, { customErrorResponseBodySchema, commonSchemas, filter: baseFilter, exclude, ...baseDoc }?: OpenAPIGeneratorGenerateOptions): Promise<OpenAPI.Document>;
105
117
  }
106
118
 
107
119
  export { OpenAPIGenerator as b, CompositeSchemaConverter as e };
@@ -2,7 +2,7 @@ import { AnySchema, OpenAPI, AnyContractProcedure, AnyContractRouter } from '@or
2
2
  import { StandardOpenAPIJsonSerializerOptions } from '@orpc/openapi-client/standard';
3
3
  import { AnyProcedure, TraverseContractProcedureCallbackOptions, AnyRouter } from '@orpc/server';
4
4
  import { Promisable, Value } from '@orpc/shared';
5
- import { JSONSchema } from '@orpc/json-schema-typed/draft-2020-12';
5
+ import { JSONSchema } from 'json-schema-typed/draft-2020-12';
6
6
 
7
7
  interface SchemaConverterComponent {
8
8
  allowedStrategies: readonly SchemaConvertOptions['strategy'][];
@@ -85,6 +85,18 @@ interface OpenAPIGeneratorGenerateOptions extends Partial<Omit<OpenAPI.Document,
85
85
  error: 'UndefinedError';
86
86
  schema?: never;
87
87
  }>;
88
+ /**
89
+ * Define a custom JSON schema for the error response body when using
90
+ * type-safe errors. Helps align ORPC error formatting with existing API
91
+ * response standards or conventions.
92
+ *
93
+ * @remarks
94
+ * - Return `null | undefined` to use the default error response body shaper.
95
+ */
96
+ customErrorResponseBodySchema?: Value<JSONSchema | undefined | null, [
97
+ definedErrors: [code: string, defaultMessage: string, dataRequired: boolean, dataSchema: JSONSchema][],
98
+ status: number
99
+ ]>;
88
100
  }
89
101
  /**
90
102
  * The generator that converts oRPC routers/contracts to OpenAPI specifications.
@@ -101,7 +113,7 @@ declare class OpenAPIGenerator {
101
113
  *
102
114
  * @see {@link https://orpc.unnoq.com/docs/openapi/openapi-specification OpenAPI Specification Docs}
103
115
  */
104
- generate(router: AnyContractRouter | AnyRouter, options?: OpenAPIGeneratorGenerateOptions): Promise<OpenAPI.Document>;
116
+ generate(router: AnyContractRouter | AnyRouter, { customErrorResponseBodySchema, commonSchemas, filter: baseFilter, exclude, ...baseDoc }?: OpenAPIGeneratorGenerateOptions): Promise<OpenAPI.Document>;
105
117
  }
106
118
 
107
119
  export { OpenAPIGenerator as b, CompositeSchemaConverter as e };