@orpc/openapi 0.0.0-next.2147e3f → 0.0.0-next.21703c0

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 (33) hide show
  1. package/README.md +137 -14
  2. package/dist/adapters/aws-lambda/index.d.mts +8 -5
  3. package/dist/adapters/aws-lambda/index.d.ts +8 -5
  4. package/dist/adapters/aws-lambda/index.mjs +5 -5
  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 +11 -5
  9. package/dist/adapters/fetch/index.d.ts +11 -5
  10. package/dist/adapters/fetch/index.mjs +3 -3
  11. package/dist/adapters/node/index.d.mts +11 -5
  12. package/dist/adapters/node/index.d.ts +11 -5
  13. package/dist/adapters/node/index.mjs +3 -3
  14. package/dist/adapters/standard/index.d.mts +8 -23
  15. package/dist/adapters/standard/index.d.ts +8 -23
  16. package/dist/adapters/standard/index.mjs +1 -1
  17. package/dist/index.d.mts +11 -3
  18. package/dist/index.d.ts +11 -3
  19. package/dist/index.mjs +2 -2
  20. package/dist/plugins/index.d.mts +20 -3
  21. package/dist/plugins/index.d.ts +20 -3
  22. package/dist/plugins/index.mjs +69 -20
  23. package/dist/shared/{openapi.C_UtQ8Us.mjs → openapi.BB-W-NKv.mjs} +33 -8
  24. package/dist/shared/openapi.BGy4N6eR.d.mts +120 -0
  25. package/dist/shared/openapi.BGy4N6eR.d.ts +120 -0
  26. package/dist/shared/{openapi.DaYgbD_w.mjs → openapi.BwdtJjDu.mjs} +300 -74
  27. package/dist/shared/openapi.DwaweYRb.d.mts +54 -0
  28. package/dist/shared/openapi.DwaweYRb.d.ts +54 -0
  29. package/package.json +20 -13
  30. package/dist/shared/openapi.D3j94c9n.d.mts +0 -12
  31. package/dist/shared/openapi.D3j94c9n.d.ts +0 -12
  32. package/dist/shared/openapi.qZLdpE0a.d.mts +0 -52
  33. package/dist/shared/openapi.qZLdpE0a.d.ts +0 -52
@@ -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");
@@ -108,22 +108,47 @@ function isAnySchema(schema) {
108
108
  if (schema === true) {
109
109
  return true;
110
110
  }
111
- if (Object.keys(schema).every((k) => !LOGIC_KEYWORDS.includes(k))) {
111
+ if (Object.keys(schema).filter((v) => schema[v] !== void 0).every((k) => !LOGIC_KEYWORDS.includes(k))) {
112
112
  return true;
113
113
  }
114
114
  return false;
115
115
  }
116
+ function isNeverSchema(schema) {
117
+ if (schema === false) {
118
+ return true;
119
+ }
120
+ if (typeof schema === "object" && schema.not !== void 0) {
121
+ if (schema.not === true) {
122
+ return true;
123
+ }
124
+ if (typeof schema.not === "object" && Object.keys(schema.not).length === 0) {
125
+ return true;
126
+ }
127
+ }
128
+ return false;
129
+ }
116
130
  function separateObjectSchema(schema, separatedProperties) {
117
- if (Object.keys(schema).some((k) => k !== "type" && k !== "properties" && k !== "required" && LOGIC_KEYWORDS.includes(k))) {
131
+ if (Object.keys(schema).some(
132
+ (k) => !["type", "properties", "required", "additionalProperties"].includes(k) && LOGIC_KEYWORDS.includes(k) && schema[k] !== void 0
133
+ )) {
118
134
  return [{ type: "object" }, schema];
119
135
  }
120
136
  const matched = { ...schema };
121
137
  const rest = { ...schema };
122
- matched.properties = schema.properties && Object.entries(schema.properties).filter(([key]) => separatedProperties.includes(key)).reduce((acc, [key, value]) => {
123
- acc[key] = value;
138
+ matched.properties = separatedProperties.reduce((acc, key) => {
139
+ const keySchema = schema.properties?.[key] ?? schema.additionalProperties;
140
+ if (keySchema !== void 0) {
141
+ acc[key] = keySchema;
142
+ }
124
143
  return acc;
125
144
  }, {});
145
+ if (Object.keys(matched.properties).length === 0) {
146
+ matched.properties = void 0;
147
+ }
126
148
  matched.required = schema.required?.filter((key) => separatedProperties.includes(key));
149
+ if (matched.required?.length === 0) {
150
+ matched.required = void 0;
151
+ }
127
152
  matched.examples = schema.examples?.map((example) => {
128
153
  if (!isObject(example)) {
129
154
  return example;
@@ -135,11 +160,14 @@ function separateObjectSchema(schema, separatedProperties) {
135
160
  return acc;
136
161
  }, {});
137
162
  });
138
- rest.properties = schema.properties && Object.entries(schema.properties).filter(([key]) => !separatedProperties.includes(key)).reduce((acc, [key, value]) => {
163
+ rest.properties = schema.properties && Object.entries(schema.properties).filter(([key]) => !separatedProperties.includes(key)).reduce((acc = {}, [key, value]) => {
139
164
  acc[key] = value;
140
165
  return acc;
141
- }, {});
166
+ }, void 0);
142
167
  rest.required = schema.required?.filter((key) => !separatedProperties.includes(key));
168
+ if (rest.required?.length === 0) {
169
+ rest.required = void 0;
170
+ }
143
171
  rest.examples = schema.examples?.map((example) => {
144
172
  if (!isObject(example)) {
145
173
  return example;
@@ -250,7 +278,7 @@ function toOpenAPIContent(schema) {
250
278
  schema: toOpenAPISchema(file)
251
279
  };
252
280
  }
253
- if (restSchema !== void 0) {
281
+ if (restSchema !== void 0 && !isAnySchema(restSchema) && !isNeverSchema(restSchema)) {
254
282
  content["application/json"] = {
255
283
  schema: toOpenAPISchema(restSchema)
256
284
  };
@@ -345,6 +373,116 @@ function checkParamsSchema(schema, params) {
345
373
  function toOpenAPISchema(schema) {
346
374
  return schema === true ? {} : schema === false ? { not: {} } : schema;
347
375
  }
376
+ const OPENAPI_JSON_SCHEMA_REF_PREFIX = "#/components/schemas/";
377
+ function resolveOpenAPIJsonSchemaRef(doc, schema) {
378
+ if (typeof schema !== "object" || !schema.$ref?.startsWith(OPENAPI_JSON_SCHEMA_REF_PREFIX)) {
379
+ return schema;
380
+ }
381
+ const name = schema.$ref.slice(OPENAPI_JSON_SCHEMA_REF_PREFIX.length);
382
+ const resolved = doc.components?.schemas?.[name];
383
+ return resolved ?? schema;
384
+ }
385
+ function simplifyComposedObjectJsonSchemasAndRefs(schema, doc) {
386
+ if (doc) {
387
+ schema = resolveOpenAPIJsonSchemaRef(doc, schema);
388
+ }
389
+ if (typeof schema !== "object" || !schema.anyOf && !schema.oneOf && !schema.allOf) {
390
+ return schema;
391
+ }
392
+ const unionSchemas = [
393
+ ...toArray(schema.anyOf?.map((s) => simplifyComposedObjectJsonSchemasAndRefs(s, doc))),
394
+ ...toArray(schema.oneOf?.map((s) => simplifyComposedObjectJsonSchemasAndRefs(s, doc)))
395
+ ];
396
+ const objectUnionSchemas = [];
397
+ for (const u of unionSchemas) {
398
+ if (!isObjectSchema(u)) {
399
+ return schema;
400
+ }
401
+ objectUnionSchemas.push(u);
402
+ }
403
+ const mergedUnionPropertyMap = /* @__PURE__ */ new Map();
404
+ for (const u of objectUnionSchemas) {
405
+ if (u.properties) {
406
+ for (const [key, value] of Object.entries(u.properties)) {
407
+ let entry = mergedUnionPropertyMap.get(key);
408
+ if (!entry) {
409
+ const required = objectUnionSchemas.every((s) => s.required?.includes(key));
410
+ entry = { required, schemas: [] };
411
+ mergedUnionPropertyMap.set(key, entry);
412
+ }
413
+ entry.schemas.push(value);
414
+ }
415
+ }
416
+ }
417
+ const intersectionSchemas = toArray(schema.allOf?.map((s) => simplifyComposedObjectJsonSchemasAndRefs(s, doc)));
418
+ const objectIntersectionSchemas = [];
419
+ for (const u of intersectionSchemas) {
420
+ if (!isObjectSchema(u)) {
421
+ return schema;
422
+ }
423
+ objectIntersectionSchemas.push(u);
424
+ }
425
+ if (isObjectSchema(schema)) {
426
+ objectIntersectionSchemas.push(schema);
427
+ }
428
+ const mergedInteractionPropertyMap = /* @__PURE__ */ new Map();
429
+ for (const u of objectIntersectionSchemas) {
430
+ if (u.properties) {
431
+ for (const [key, value] of Object.entries(u.properties)) {
432
+ let entry = mergedInteractionPropertyMap.get(key);
433
+ if (!entry) {
434
+ const required = objectIntersectionSchemas.some((s) => s.required?.includes(key));
435
+ entry = { required, schemas: [] };
436
+ mergedInteractionPropertyMap.set(key, entry);
437
+ }
438
+ entry.schemas.push(value);
439
+ }
440
+ }
441
+ }
442
+ const resultObjectSchema = { type: "object", properties: {}, required: [] };
443
+ const keys = /* @__PURE__ */ new Set([
444
+ ...mergedUnionPropertyMap.keys(),
445
+ ...mergedInteractionPropertyMap.keys()
446
+ ]);
447
+ if (keys.size === 0) {
448
+ return schema;
449
+ }
450
+ const deduplicateSchemas = (schemas) => {
451
+ const seen = /* @__PURE__ */ new Set();
452
+ const result = [];
453
+ for (const schema2 of schemas) {
454
+ const key = stringifyJSON(schema2);
455
+ if (!seen.has(key)) {
456
+ seen.add(key);
457
+ result.push(schema2);
458
+ }
459
+ }
460
+ return result;
461
+ };
462
+ for (const key of keys) {
463
+ const unionEntry = mergedUnionPropertyMap.get(key);
464
+ const intersectionEntry = mergedInteractionPropertyMap.get(key);
465
+ resultObjectSchema.properties[key] = (() => {
466
+ const dedupedUnionSchemas = unionEntry ? deduplicateSchemas(unionEntry.schemas) : [];
467
+ const dedupedIntersectionSchemas = intersectionEntry ? deduplicateSchemas(intersectionEntry.schemas) : [];
468
+ if (!dedupedUnionSchemas.length) {
469
+ return dedupedIntersectionSchemas.length === 1 ? dedupedIntersectionSchemas[0] : { allOf: dedupedIntersectionSchemas };
470
+ }
471
+ if (!dedupedIntersectionSchemas.length) {
472
+ return dedupedUnionSchemas.length === 1 ? dedupedUnionSchemas[0] : { anyOf: dedupedUnionSchemas };
473
+ }
474
+ const allOf = deduplicateSchemas([
475
+ ...dedupedIntersectionSchemas,
476
+ dedupedUnionSchemas.length === 1 ? dedupedUnionSchemas[0] : { anyOf: dedupedUnionSchemas }
477
+ ]);
478
+ return allOf.length === 1 ? allOf[0] : { allOf };
479
+ })();
480
+ if (unionEntry?.required || intersectionEntry?.required) {
481
+ resultObjectSchema.required.push(key);
482
+ }
483
+ }
484
+ return resultObjectSchema;
485
+ }
348
486
 
349
487
  class CompositeSchemaConverter {
350
488
  converters;
@@ -373,43 +511,49 @@ class OpenAPIGenerator {
373
511
  /**
374
512
  * Generates OpenAPI specifications from oRPC routers/contracts.
375
513
  *
376
- * @see {@link https://orpc.unnoq.com/docs/openapi/openapi-specification OpenAPI Specification Docs}
514
+ * @see {@link https://orpc.dev/docs/openapi/openapi-specification OpenAPI Specification Docs}
377
515
  */
378
- async generate(router, options = {}) {
379
- const exclude = options.exclude ?? (() => false);
516
+ async generate(router, { customErrorResponseBodySchema, commonSchemas, filter: baseFilter, exclude, ...baseDoc } = {}) {
517
+ const filter = baseFilter ?? (({ contract, path }) => {
518
+ return !(exclude?.(contract, path) ?? false);
519
+ });
380
520
  const doc = {
381
- ...clone(options),
382
- info: options.info ?? { title: "API Reference", version: "0.0.0" },
383
- openapi: "3.1.1",
384
- exclude: void 0
521
+ ...clone(baseDoc),
522
+ info: baseDoc.info ?? { title: "API Reference", version: "0.0.0" },
523
+ openapi: "3.1.1"
385
524
  };
525
+ const { baseSchemaConvertOptions, undefinedErrorJsonSchema } = await this.#resolveCommonSchemas(doc, commonSchemas);
386
526
  const contracts = [];
387
- await resolveContractProcedures({ path: [], router }, ({ contract, path }) => {
388
- if (!exclude(contract, path)) {
389
- contracts.push({ contract, path });
527
+ await resolveContractProcedures({ path: [], router }, (traverseOptions) => {
528
+ if (!value(filter, traverseOptions)) {
529
+ return;
390
530
  }
531
+ contracts.push(traverseOptions);
391
532
  });
392
533
  const errors = [];
393
534
  for (const { contract, path } of contracts) {
394
- const operationId = path.join(".");
535
+ const stringPath = path.join(".");
395
536
  try {
396
537
  const def = contract["~orpc"];
397
538
  const method = toOpenAPIMethod(fallbackContractConfig("defaultMethod", def.route.method));
398
539
  const httpPath = toOpenAPIPath(def.route.path ?? toHttpPath(path));
399
540
  let operationObjectRef;
400
- if (def.route.spec !== void 0) {
541
+ if (def.route.spec !== void 0 && typeof def.route.spec !== "function") {
401
542
  operationObjectRef = def.route.spec;
402
543
  } else {
403
544
  operationObjectRef = {
404
- operationId,
545
+ operationId: def.route.operationId ?? stringPath,
405
546
  summary: def.route.summary,
406
547
  description: def.route.description,
407
548
  deprecated: def.route.deprecated,
408
549
  tags: def.route.tags?.map((tag) => tag)
409
550
  };
410
- await this.#request(operationObjectRef, def);
411
- await this.#successResponse(operationObjectRef, def);
412
- await this.#errorResponse(operationObjectRef, def);
551
+ await this.#request(doc, operationObjectRef, def, baseSchemaConvertOptions);
552
+ await this.#successResponse(doc, operationObjectRef, def, baseSchemaConvertOptions);
553
+ await this.#errorResponse(operationObjectRef, def, baseSchemaConvertOptions, undefinedErrorJsonSchema, customErrorResponseBodySchema);
554
+ }
555
+ if (typeof def.route.spec === "function") {
556
+ operationObjectRef = def.route.spec(operationObjectRef);
413
557
  }
414
558
  doc.paths ??= {};
415
559
  doc.paths[httpPath] ??= {};
@@ -419,7 +563,7 @@ class OpenAPIGenerator {
419
563
  throw e;
420
564
  }
421
565
  errors.push(
422
- `[OpenAPIGenerator] Error occurred while generating OpenAPI for procedure at path: ${operationId}
566
+ `[OpenAPIGenerator] Error occurred while generating OpenAPI for procedure at path: ${stringPath}
423
567
  ${e.message}`
424
568
  );
425
569
  }
@@ -433,25 +577,102 @@ ${errors.join("\n\n")}`
433
577
  }
434
578
  return this.serializer.serialize(doc)[0];
435
579
  }
436
- async #request(ref, def) {
580
+ async #resolveCommonSchemas(doc, commonSchemas) {
581
+ let undefinedErrorJsonSchema = {
582
+ type: "object",
583
+ properties: {
584
+ defined: { const: false },
585
+ code: { type: "string" },
586
+ status: { type: "number" },
587
+ message: { type: "string" },
588
+ data: {}
589
+ },
590
+ required: ["defined", "code", "status", "message"]
591
+ };
592
+ const baseSchemaConvertOptions = {};
593
+ if (commonSchemas) {
594
+ baseSchemaConvertOptions.components = [];
595
+ for (const key in commonSchemas) {
596
+ const options = commonSchemas[key];
597
+ if (options.schema === void 0) {
598
+ continue;
599
+ }
600
+ const { schema, strategy = "input" } = options;
601
+ const [required, json] = await this.converter.convert(schema, { strategy });
602
+ const allowedStrategies = [strategy];
603
+ if (strategy === "input") {
604
+ const [outputRequired, outputJson] = await this.converter.convert(schema, { strategy: "output" });
605
+ if (outputRequired === required && stringifyJSON(outputJson) === stringifyJSON(json)) {
606
+ allowedStrategies.push("output");
607
+ }
608
+ } else if (strategy === "output") {
609
+ const [inputRequired, inputJson] = await this.converter.convert(schema, { strategy: "input" });
610
+ if (inputRequired === required && stringifyJSON(inputJson) === stringifyJSON(json)) {
611
+ allowedStrategies.push("input");
612
+ }
613
+ }
614
+ baseSchemaConvertOptions.components.push({
615
+ schema,
616
+ required,
617
+ ref: `#/components/schemas/${key}`,
618
+ allowedStrategies
619
+ });
620
+ }
621
+ doc.components ??= {};
622
+ doc.components.schemas ??= {};
623
+ for (const key in commonSchemas) {
624
+ const options = commonSchemas[key];
625
+ if (options.schema === void 0) {
626
+ if (options.error === "UndefinedError") {
627
+ doc.components.schemas[key] = toOpenAPISchema(undefinedErrorJsonSchema);
628
+ undefinedErrorJsonSchema = { $ref: `#/components/schemas/${key}` };
629
+ }
630
+ continue;
631
+ }
632
+ const { schema, strategy = "input" } = options;
633
+ const [, json] = await this.converter.convert(
634
+ schema,
635
+ {
636
+ ...baseSchemaConvertOptions,
637
+ strategy,
638
+ minStructureDepthForRef: 1
639
+ // not allow use $ref for root schemas
640
+ }
641
+ );
642
+ doc.components.schemas[key] = toOpenAPISchema(json);
643
+ }
644
+ }
645
+ return { baseSchemaConvertOptions, undefinedErrorJsonSchema };
646
+ }
647
+ async #request(doc, ref, def, baseSchemaConvertOptions) {
437
648
  const method = fallbackContractConfig("defaultMethod", def.route.method);
438
649
  const details = getEventIteratorSchemaDetails(def.inputSchema);
439
650
  if (details) {
440
651
  ref.requestBody = {
441
652
  required: true,
442
653
  content: toOpenAPIEventIteratorContent(
443
- await this.converter.convert(details.yields, { strategy: "input" }),
444
- await this.converter.convert(details.returns, { strategy: "input" })
654
+ await this.converter.convert(details.yields, { ...baseSchemaConvertOptions, strategy: "input" }),
655
+ await this.converter.convert(details.returns, { ...baseSchemaConvertOptions, strategy: "input" })
445
656
  )
446
657
  };
447
658
  return;
448
659
  }
449
660
  const dynamicParams = getDynamicParams(def.route.path)?.map((v) => v.name);
450
661
  const inputStructure = fallbackContractConfig("defaultInputStructure", def.route.inputStructure);
451
- let [required, schema] = await this.converter.convert(def.inputSchema, { strategy: "input" });
662
+ let [required, schema] = await this.converter.convert(
663
+ def.inputSchema,
664
+ {
665
+ ...baseSchemaConvertOptions,
666
+ strategy: "input"
667
+ }
668
+ );
669
+ let omitResponseBody = false;
452
670
  if (isAnySchema(schema) && !dynamicParams?.length) {
453
671
  return;
454
672
  }
673
+ if (inputStructure === "detailed" || inputStructure === "compact" && (dynamicParams?.length || method === "GET")) {
674
+ schema = simplifyComposedObjectJsonSchemasAndRefs(schema, doc);
675
+ }
455
676
  if (inputStructure === "compact") {
456
677
  if (dynamicParams?.length) {
457
678
  const error2 = new OpenAPIGeneratorError(
@@ -463,6 +684,7 @@ ${errors.join("\n\n")}`
463
684
  const [paramsSchema, rest] = separateObjectSchema(schema, dynamicParams);
464
685
  schema = rest;
465
686
  required = rest.required ? rest.required.length !== 0 : false;
687
+ omitResponseBody = !required && !rest.properties;
466
688
  if (!checkParamsSchema(paramsSchema, dynamicParams)) {
467
689
  throw error2;
468
690
  }
@@ -477,7 +699,7 @@ ${errors.join("\n\n")}`
477
699
  }
478
700
  ref.parameters ??= [];
479
701
  ref.parameters.push(...toOpenAPIParameters(schema, "query"));
480
- } else {
702
+ } else if (!omitResponseBody) {
481
703
  ref.requestBody = {
482
704
  required,
483
705
  content: toOpenAPIContent(schema)
@@ -491,7 +713,8 @@ ${errors.join("\n\n")}`
491
713
  if (!isObjectSchema(schema)) {
492
714
  throw error;
493
715
  }
494
- if (dynamicParams?.length && (schema.properties?.params === void 0 || !isObjectSchema(schema.properties.params) || !checkParamsSchema(schema.properties.params, dynamicParams))) {
716
+ const resolvedParamSchema = schema.properties?.params !== void 0 ? simplifyComposedObjectJsonSchemasAndRefs(schema.properties.params, doc) : void 0;
717
+ if (dynamicParams?.length && (resolvedParamSchema === void 0 || !isObjectSchema(resolvedParamSchema) || !checkParamsSchema(resolvedParamSchema, dynamicParams))) {
495
718
  throw new OpenAPIGeneratorError(
496
719
  'When input structure is "detailed" and path has dynamic params, the "params" schema must be an object with all dynamic params as required.'
497
720
  );
@@ -499,12 +722,13 @@ ${errors.join("\n\n")}`
499
722
  for (const from of ["params", "query", "headers"]) {
500
723
  const fromSchema = schema.properties?.[from];
501
724
  if (fromSchema !== void 0) {
502
- if (!isObjectSchema(fromSchema)) {
725
+ const resolvedSchema = simplifyComposedObjectJsonSchemasAndRefs(fromSchema, doc);
726
+ if (!isObjectSchema(resolvedSchema)) {
503
727
  throw error;
504
728
  }
505
729
  const parameterIn = from === "params" ? "path" : from === "headers" ? "header" : "query";
506
730
  ref.parameters ??= [];
507
- ref.parameters.push(...toOpenAPIParameters(fromSchema, parameterIn));
731
+ ref.parameters.push(...toOpenAPIParameters(resolvedSchema, parameterIn));
508
732
  }
509
733
  }
510
734
  if (schema.properties?.body !== void 0) {
@@ -514,7 +738,7 @@ ${errors.join("\n\n")}`
514
738
  };
515
739
  }
516
740
  }
517
- async #successResponse(ref, def) {
741
+ async #successResponse(doc, ref, def, baseSchemaConvertOptions) {
518
742
  const outputSchema = def.outputSchema;
519
743
  const status = fallbackContractConfig("defaultSuccessStatus", def.route.successStatus);
520
744
  const description = fallbackContractConfig("defaultSuccessDescription", def.route?.successDescription);
@@ -525,13 +749,20 @@ ${errors.join("\n\n")}`
525
749
  ref.responses[status] = {
526
750
  description,
527
751
  content: toOpenAPIEventIteratorContent(
528
- await this.converter.convert(eventIteratorSchemaDetails.yields, { strategy: "output" }),
529
- await this.converter.convert(eventIteratorSchemaDetails.returns, { strategy: "output" })
752
+ await this.converter.convert(eventIteratorSchemaDetails.yields, { ...baseSchemaConvertOptions, strategy: "output" }),
753
+ await this.converter.convert(eventIteratorSchemaDetails.returns, { ...baseSchemaConvertOptions, strategy: "output" })
530
754
  )
531
755
  };
532
756
  return;
533
757
  }
534
- const [required, json] = await this.converter.convert(outputSchema, { strategy: "output" });
758
+ const [required, json] = await this.converter.convert(
759
+ outputSchema,
760
+ {
761
+ ...baseSchemaConvertOptions,
762
+ strategy: "output",
763
+ minStructureDepthForRef: outputStructure === "detailed" ? 1 : 0
764
+ }
765
+ );
535
766
  if (outputStructure === "compact") {
536
767
  ref.responses ??= {};
537
768
  ref.responses[status] = {
@@ -552,17 +783,19 @@ ${errors.join("\n\n")}`
552
783
 
553
784
  But got: ${stringifyJSON(item)}
554
785
  `);
555
- if (!isObjectSchema(item)) {
786
+ const simplifiedItem = simplifyComposedObjectJsonSchemasAndRefs(item, doc);
787
+ if (!isObjectSchema(simplifiedItem)) {
556
788
  throw error;
557
789
  }
558
790
  let schemaStatus;
559
791
  let schemaDescription;
560
- if (item.properties?.status !== void 0) {
561
- if (typeof item.properties.status !== "object" || item.properties.status.const === void 0 || typeof item.properties.status.const !== "number" || !Number.isInteger(item.properties.status.const) || isORPCErrorStatus(item.properties.status.const)) {
792
+ if (simplifiedItem.properties?.status !== void 0) {
793
+ const statusSchema = resolveOpenAPIJsonSchemaRef(doc, simplifiedItem.properties.status);
794
+ if (typeof statusSchema !== "object" || statusSchema.const === void 0 || typeof statusSchema.const !== "number" || !Number.isInteger(statusSchema.const) || isORPCErrorStatus(statusSchema.const)) {
562
795
  throw error;
563
796
  }
564
- schemaStatus = item.properties.status.const;
565
- schemaDescription = item.properties.status.description;
797
+ schemaStatus = statusSchema.const;
798
+ schemaDescription = statusSchema.description;
566
799
  }
567
800
  const itemStatus = schemaStatus ?? status;
568
801
  const itemDescription = schemaDescription ?? description;
@@ -577,71 +810,64 @@ ${errors.join("\n\n")}`
577
810
  ref.responses[itemStatus] = {
578
811
  description: itemDescription
579
812
  };
580
- if (item.properties?.headers !== void 0) {
581
- if (!isObjectSchema(item.properties.headers)) {
813
+ if (simplifiedItem.properties?.headers !== void 0) {
814
+ const headersSchema = simplifyComposedObjectJsonSchemasAndRefs(simplifiedItem.properties.headers, doc);
815
+ if (!isObjectSchema(headersSchema)) {
582
816
  throw error;
583
817
  }
584
- for (const key in item.properties.headers.properties) {
585
- const headerSchema = item.properties.headers.properties[key];
818
+ for (const key in headersSchema.properties) {
819
+ const headerSchema = headersSchema.properties[key];
586
820
  if (headerSchema !== void 0) {
587
821
  ref.responses[itemStatus].headers ??= {};
588
822
  ref.responses[itemStatus].headers[key] = {
589
823
  schema: toOpenAPISchema(headerSchema),
590
- required: item.properties.headers.required?.includes(key)
824
+ required: simplifiedItem.required?.includes("headers") && headersSchema.required?.includes(key)
591
825
  };
592
826
  }
593
827
  }
594
828
  }
595
- if (item.properties?.body !== void 0) {
829
+ if (simplifiedItem.properties?.body !== void 0) {
596
830
  ref.responses[itemStatus].content = toOpenAPIContent(
597
- applySchemaOptionality(item.required?.includes("body") ?? false, item.properties.body)
831
+ applySchemaOptionality(simplifiedItem.required?.includes("body") ?? false, simplifiedItem.properties.body)
598
832
  );
599
833
  }
600
834
  }
601
835
  }
602
- async #errorResponse(ref, def) {
836
+ async #errorResponse(ref, def, baseSchemaConvertOptions, undefinedErrorSchema, customErrorResponseBodySchema) {
603
837
  const errorMap = def.errorMap;
604
- const errors = {};
838
+ const errorResponsesByStatus = {};
605
839
  for (const code in errorMap) {
606
840
  const config = errorMap[code];
607
841
  if (!config) {
608
842
  continue;
609
843
  }
610
844
  const status = fallbackORPCErrorStatus(code, config.status);
611
- const message = fallbackORPCErrorMessage(code, config.message);
612
- const [dataRequired, dataSchema] = await this.converter.convert(config.data, { strategy: "output" });
613
- errors[status] ??= [];
614
- errors[status].push({
845
+ const defaultMessage = fallbackORPCErrorMessage(code, config.message);
846
+ errorResponsesByStatus[status] ??= { status, definedErrorDefinitions: [], errorSchemaVariants: [] };
847
+ const [dataRequired, dataSchema] = await this.converter.convert(config.data, { ...baseSchemaConvertOptions, strategy: "output" });
848
+ errorResponsesByStatus[status].definedErrorDefinitions.push([code, defaultMessage, dataRequired, dataSchema]);
849
+ errorResponsesByStatus[status].errorSchemaVariants.push({
615
850
  type: "object",
616
851
  properties: {
617
852
  defined: { const: true },
618
853
  code: { const: code },
619
854
  status: { const: status },
620
- message: { type: "string", default: message },
855
+ message: { type: "string", default: defaultMessage },
621
856
  data: dataSchema
622
857
  },
623
858
  required: dataRequired ? ["defined", "code", "status", "message", "data"] : ["defined", "code", "status", "message"]
624
859
  });
625
860
  }
626
861
  ref.responses ??= {};
627
- for (const status in errors) {
628
- const schemas = errors[status];
629
- ref.responses[status] = {
630
- description: status,
631
- content: toOpenAPIContent({
862
+ for (const statusString in errorResponsesByStatus) {
863
+ const errorResponse = errorResponsesByStatus[statusString];
864
+ const customBodySchema = value(customErrorResponseBodySchema, errorResponse.definedErrorDefinitions, errorResponse.status);
865
+ ref.responses[statusString] = {
866
+ description: statusString,
867
+ content: toOpenAPIContent(customBodySchema ?? {
632
868
  oneOf: [
633
- ...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
- }
869
+ ...errorResponse.errorSchemaVariants,
870
+ undefinedErrorSchema
645
871
  ]
646
872
  })
647
873
  };
@@ -649,4 +875,4 @@ ${errors.join("\n\n")}`
649
875
  }
650
876
  }
651
877
 
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 };
878
+ 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, isNeverSchema as m, separateObjectSchema as n, filterSchemaBranches as o, applySchemaOptionality as p, expandUnionSchema as q, resolveOpenAPIJsonSchemaRef as r, simplifyComposedObjectJsonSchemasAndRefs as s, toOpenAPIPath as t, expandArrayableSchema as u, isPrimitiveSchema as v };
@@ -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 };