@orpc/openapi 0.0.0-next.e6490f2 → 0.0.0-next.e77ba7d

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 +7 -2
  2. package/dist/adapters/aws-lambda/index.d.mts +6 -3
  3. package/dist/adapters/aws-lambda/index.d.ts +6 -3
  4. package/dist/adapters/aws-lambda/index.mjs +3 -3
  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 +9 -3
  9. package/dist/adapters/fetch/index.d.ts +9 -3
  10. package/dist/adapters/fetch/index.mjs +1 -1
  11. package/dist/adapters/node/index.d.mts +9 -3
  12. package/dist/adapters/node/index.d.ts +9 -3
  13. package/dist/adapters/node/index.mjs +1 -1
  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 +9 -2
  18. package/dist/index.d.ts +9 -2
  19. package/dist/index.mjs +2 -2
  20. package/dist/plugins/index.d.mts +18 -3
  21. package/dist/plugins/index.d.ts +18 -3
  22. package/dist/plugins/index.mjs +58 -18
  23. package/dist/shared/{openapi.C_UtQ8Us.mjs → openapi.DIt-Z9W1.mjs} +19 -8
  24. package/dist/shared/{openapi.DaYgbD_w.mjs → openapi.DNv9yFfn.mjs} +269 -68
  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.dbQeFCUJ.d.mts +120 -0
  28. package/dist/shared/openapi.dbQeFCUJ.d.ts +120 -0
  29. package/package.json +17 -10
  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");
@@ -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((k) => k !== "type" && k !== "properties" && k !== "required" && LOGIC_KEYWORDS.includes(k))) {
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 = schema.properties && Object.entries(schema.properties).filter(([key]) => separatedProperties.includes(key)).reduce((acc, [key, value]) => {
123
- acc[key] = value;
124
+ matched.properties = separatedProperties.reduce((acc, key) => {
125
+ const keySchema = schema.properties?.[key] ?? schema.additionalProperties;
126
+ if (keySchema !== void 0) {
127
+ acc[key] = keySchema;
128
+ }
124
129
  return acc;
125
130
  }, {});
126
131
  matched.required = schema.required?.filter((key) => separatedProperties.includes(key));
@@ -345,6 +350,116 @@ function checkParamsSchema(schema, params) {
345
350
  function toOpenAPISchema(schema) {
346
351
  return schema === true ? {} : schema === false ? { not: {} } : schema;
347
352
  }
353
+ const OPENAPI_JSON_SCHEMA_REF_PREFIX = "#/components/schemas/";
354
+ function resolveOpenAPIJsonSchemaRef(doc, schema) {
355
+ if (typeof schema !== "object" || !schema.$ref?.startsWith(OPENAPI_JSON_SCHEMA_REF_PREFIX)) {
356
+ return schema;
357
+ }
358
+ const name = schema.$ref.slice(OPENAPI_JSON_SCHEMA_REF_PREFIX.length);
359
+ const resolved = doc.components?.schemas?.[name];
360
+ return resolved ?? schema;
361
+ }
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
+ }
348
463
 
349
464
  class CompositeSchemaConverter {
350
465
  converters;
@@ -375,41 +490,47 @@ class OpenAPIGenerator {
375
490
  *
376
491
  * @see {@link https://orpc.unnoq.com/docs/openapi/openapi-specification OpenAPI Specification Docs}
377
492
  */
378
- async generate(router, options = {}) {
379
- const exclude = options.exclude ?? (() => false);
493
+ async generate(router, { customErrorResponseBodySchema, commonSchemas, filter: baseFilter, exclude, ...baseDoc } = {}) {
494
+ const filter = baseFilter ?? (({ contract, path }) => {
495
+ return !(exclude?.(contract, path) ?? false);
496
+ });
380
497
  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
498
+ ...clone(baseDoc),
499
+ info: baseDoc.info ?? { title: "API Reference", version: "0.0.0" },
500
+ openapi: "3.1.1"
385
501
  };
502
+ const { baseSchemaConvertOptions, undefinedErrorJsonSchema } = await this.#resolveCommonSchemas(doc, commonSchemas);
386
503
  const contracts = [];
387
- await resolveContractProcedures({ path: [], router }, ({ contract, path }) => {
388
- if (!exclude(contract, path)) {
389
- contracts.push({ contract, path });
504
+ await resolveContractProcedures({ path: [], router }, (traverseOptions) => {
505
+ if (!value(filter, traverseOptions)) {
506
+ return;
390
507
  }
508
+ contracts.push(traverseOptions);
391
509
  });
392
510
  const errors = [];
393
511
  for (const { contract, path } of contracts) {
394
- const operationId = path.join(".");
512
+ const stringPath = path.join(".");
395
513
  try {
396
514
  const def = contract["~orpc"];
397
515
  const method = toOpenAPIMethod(fallbackContractConfig("defaultMethod", def.route.method));
398
516
  const httpPath = toOpenAPIPath(def.route.path ?? toHttpPath(path));
399
517
  let operationObjectRef;
400
- if (def.route.spec !== void 0) {
518
+ if (def.route.spec !== void 0 && typeof def.route.spec !== "function") {
401
519
  operationObjectRef = def.route.spec;
402
520
  } else {
403
521
  operationObjectRef = {
404
- operationId,
522
+ operationId: def.route.operationId ?? stringPath,
405
523
  summary: def.route.summary,
406
524
  description: def.route.description,
407
525
  deprecated: def.route.deprecated,
408
526
  tags: def.route.tags?.map((tag) => tag)
409
527
  };
410
- await this.#request(operationObjectRef, def);
411
- await this.#successResponse(operationObjectRef, def);
412
- await this.#errorResponse(operationObjectRef, def);
528
+ await this.#request(doc, operationObjectRef, def, baseSchemaConvertOptions);
529
+ await this.#successResponse(doc, 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);
413
534
  }
414
535
  doc.paths ??= {};
415
536
  doc.paths[httpPath] ??= {};
@@ -419,7 +540,7 @@ class OpenAPIGenerator {
419
540
  throw e;
420
541
  }
421
542
  errors.push(
422
- `[OpenAPIGenerator] Error occurred while generating OpenAPI for procedure at path: ${operationId}
543
+ `[OpenAPIGenerator] Error occurred while generating OpenAPI for procedure at path: ${stringPath}
423
544
  ${e.message}`
424
545
  );
425
546
  }
@@ -433,25 +554,101 @@ ${errors.join("\n\n")}`
433
554
  }
434
555
  return this.serializer.serialize(doc)[0];
435
556
  }
436
- async #request(ref, def) {
557
+ async #resolveCommonSchemas(doc, commonSchemas) {
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 = {};
570
+ if (commonSchemas) {
571
+ baseSchemaConvertOptions.components = [];
572
+ for (const key in commonSchemas) {
573
+ const options = commonSchemas[key];
574
+ if (options.schema === void 0) {
575
+ continue;
576
+ }
577
+ const { schema, strategy = "input" } = options;
578
+ const [required, json] = await this.converter.convert(schema, { strategy });
579
+ const allowedStrategies = [strategy];
580
+ if (strategy === "input") {
581
+ const [outputRequired, outputJson] = await this.converter.convert(schema, { strategy: "output" });
582
+ if (outputRequired === required && stringifyJSON(outputJson) === stringifyJSON(json)) {
583
+ allowedStrategies.push("output");
584
+ }
585
+ } else if (strategy === "output") {
586
+ const [inputRequired, inputJson] = await this.converter.convert(schema, { strategy: "input" });
587
+ if (inputRequired === required && stringifyJSON(inputJson) === stringifyJSON(json)) {
588
+ allowedStrategies.push("input");
589
+ }
590
+ }
591
+ baseSchemaConvertOptions.components.push({
592
+ schema,
593
+ required,
594
+ ref: `#/components/schemas/${key}`,
595
+ allowedStrategies
596
+ });
597
+ }
598
+ doc.components ??= {};
599
+ doc.components.schemas ??= {};
600
+ for (const key in commonSchemas) {
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;
610
+ const [, json] = await this.converter.convert(
611
+ schema,
612
+ {
613
+ ...baseSchemaConvertOptions,
614
+ strategy,
615
+ minStructureDepthForRef: 1
616
+ // not allow use $ref for root schemas
617
+ }
618
+ );
619
+ doc.components.schemas[key] = toOpenAPISchema(json);
620
+ }
621
+ }
622
+ return { baseSchemaConvertOptions, undefinedErrorJsonSchema };
623
+ }
624
+ async #request(doc, ref, def, baseSchemaConvertOptions) {
437
625
  const method = fallbackContractConfig("defaultMethod", def.route.method);
438
626
  const details = getEventIteratorSchemaDetails(def.inputSchema);
439
627
  if (details) {
440
628
  ref.requestBody = {
441
629
  required: true,
442
630
  content: toOpenAPIEventIteratorContent(
443
- await this.converter.convert(details.yields, { strategy: "input" }),
444
- await this.converter.convert(details.returns, { strategy: "input" })
631
+ await this.converter.convert(details.yields, { ...baseSchemaConvertOptions, strategy: "input" }),
632
+ await this.converter.convert(details.returns, { ...baseSchemaConvertOptions, strategy: "input" })
445
633
  )
446
634
  };
447
635
  return;
448
636
  }
449
637
  const dynamicParams = getDynamicParams(def.route.path)?.map((v) => v.name);
450
638
  const inputStructure = fallbackContractConfig("defaultInputStructure", def.route.inputStructure);
451
- let [required, schema] = await this.converter.convert(def.inputSchema, { strategy: "input" });
639
+ let [required, schema] = await this.converter.convert(
640
+ def.inputSchema,
641
+ {
642
+ ...baseSchemaConvertOptions,
643
+ strategy: "input"
644
+ }
645
+ );
452
646
  if (isAnySchema(schema) && !dynamicParams?.length) {
453
647
  return;
454
648
  }
649
+ if (inputStructure === "detailed" || inputStructure === "compact" && (dynamicParams?.length || method === "GET")) {
650
+ schema = simplifyComposedObjectJsonSchemasAndRefs(schema, doc);
651
+ }
455
652
  if (inputStructure === "compact") {
456
653
  if (dynamicParams?.length) {
457
654
  const error2 = new OpenAPIGeneratorError(
@@ -491,7 +688,8 @@ ${errors.join("\n\n")}`
491
688
  if (!isObjectSchema(schema)) {
492
689
  throw error;
493
690
  }
494
- if (dynamicParams?.length && (schema.properties?.params === void 0 || !isObjectSchema(schema.properties.params) || !checkParamsSchema(schema.properties.params, dynamicParams))) {
691
+ const resolvedParamSchema = schema.properties?.params !== void 0 ? simplifyComposedObjectJsonSchemasAndRefs(schema.properties.params, doc) : void 0;
692
+ if (dynamicParams?.length && (resolvedParamSchema === void 0 || !isObjectSchema(resolvedParamSchema) || !checkParamsSchema(resolvedParamSchema, dynamicParams))) {
495
693
  throw new OpenAPIGeneratorError(
496
694
  'When input structure is "detailed" and path has dynamic params, the "params" schema must be an object with all dynamic params as required.'
497
695
  );
@@ -499,12 +697,13 @@ ${errors.join("\n\n")}`
499
697
  for (const from of ["params", "query", "headers"]) {
500
698
  const fromSchema = schema.properties?.[from];
501
699
  if (fromSchema !== void 0) {
502
- if (!isObjectSchema(fromSchema)) {
700
+ const resolvedSchema = simplifyComposedObjectJsonSchemasAndRefs(fromSchema, doc);
701
+ if (!isObjectSchema(resolvedSchema)) {
503
702
  throw error;
504
703
  }
505
704
  const parameterIn = from === "params" ? "path" : from === "headers" ? "header" : "query";
506
705
  ref.parameters ??= [];
507
- ref.parameters.push(...toOpenAPIParameters(fromSchema, parameterIn));
706
+ ref.parameters.push(...toOpenAPIParameters(resolvedSchema, parameterIn));
508
707
  }
509
708
  }
510
709
  if (schema.properties?.body !== void 0) {
@@ -514,7 +713,7 @@ ${errors.join("\n\n")}`
514
713
  };
515
714
  }
516
715
  }
517
- async #successResponse(ref, def) {
716
+ async #successResponse(doc, ref, def, baseSchemaConvertOptions) {
518
717
  const outputSchema = def.outputSchema;
519
718
  const status = fallbackContractConfig("defaultSuccessStatus", def.route.successStatus);
520
719
  const description = fallbackContractConfig("defaultSuccessDescription", def.route?.successDescription);
@@ -525,13 +724,20 @@ ${errors.join("\n\n")}`
525
724
  ref.responses[status] = {
526
725
  description,
527
726
  content: toOpenAPIEventIteratorContent(
528
- await this.converter.convert(eventIteratorSchemaDetails.yields, { strategy: "output" }),
529
- await this.converter.convert(eventIteratorSchemaDetails.returns, { strategy: "output" })
727
+ await this.converter.convert(eventIteratorSchemaDetails.yields, { ...baseSchemaConvertOptions, strategy: "output" }),
728
+ await this.converter.convert(eventIteratorSchemaDetails.returns, { ...baseSchemaConvertOptions, strategy: "output" })
530
729
  )
531
730
  };
532
731
  return;
533
732
  }
534
- const [required, json] = await this.converter.convert(outputSchema, { strategy: "output" });
733
+ const [required, json] = await this.converter.convert(
734
+ outputSchema,
735
+ {
736
+ ...baseSchemaConvertOptions,
737
+ strategy: "output",
738
+ minStructureDepthForRef: outputStructure === "detailed" ? 1 : 0
739
+ }
740
+ );
535
741
  if (outputStructure === "compact") {
536
742
  ref.responses ??= {};
537
743
  ref.responses[status] = {
@@ -552,17 +758,19 @@ ${errors.join("\n\n")}`
552
758
 
553
759
  But got: ${stringifyJSON(item)}
554
760
  `);
555
- if (!isObjectSchema(item)) {
761
+ const simplifiedItem = simplifyComposedObjectJsonSchemasAndRefs(item, doc);
762
+ if (!isObjectSchema(simplifiedItem)) {
556
763
  throw error;
557
764
  }
558
765
  let schemaStatus;
559
766
  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)) {
767
+ if (simplifiedItem.properties?.status !== void 0) {
768
+ const statusSchema = resolveOpenAPIJsonSchemaRef(doc, simplifiedItem.properties.status);
769
+ if (typeof statusSchema !== "object" || statusSchema.const === void 0 || typeof statusSchema.const !== "number" || !Number.isInteger(statusSchema.const) || isORPCErrorStatus(statusSchema.const)) {
562
770
  throw error;
563
771
  }
564
- schemaStatus = item.properties.status.const;
565
- schemaDescription = item.properties.status.description;
772
+ schemaStatus = statusSchema.const;
773
+ schemaDescription = statusSchema.description;
566
774
  }
567
775
  const itemStatus = schemaStatus ?? status;
568
776
  const itemDescription = schemaDescription ?? description;
@@ -577,71 +785,64 @@ ${errors.join("\n\n")}`
577
785
  ref.responses[itemStatus] = {
578
786
  description: itemDescription
579
787
  };
580
- if (item.properties?.headers !== void 0) {
581
- if (!isObjectSchema(item.properties.headers)) {
788
+ if (simplifiedItem.properties?.headers !== void 0) {
789
+ const headersSchema = simplifyComposedObjectJsonSchemasAndRefs(simplifiedItem.properties.headers, doc);
790
+ if (!isObjectSchema(headersSchema)) {
582
791
  throw error;
583
792
  }
584
- for (const key in item.properties.headers.properties) {
585
- const headerSchema = item.properties.headers.properties[key];
793
+ for (const key in headersSchema.properties) {
794
+ const headerSchema = headersSchema.properties[key];
586
795
  if (headerSchema !== void 0) {
587
796
  ref.responses[itemStatus].headers ??= {};
588
797
  ref.responses[itemStatus].headers[key] = {
589
798
  schema: toOpenAPISchema(headerSchema),
590
- required: item.properties.headers.required?.includes(key)
799
+ required: simplifiedItem.required?.includes("headers") && headersSchema.required?.includes(key)
591
800
  };
592
801
  }
593
802
  }
594
803
  }
595
- if (item.properties?.body !== void 0) {
804
+ if (simplifiedItem.properties?.body !== void 0) {
596
805
  ref.responses[itemStatus].content = toOpenAPIContent(
597
- applySchemaOptionality(item.required?.includes("body") ?? false, item.properties.body)
806
+ applySchemaOptionality(simplifiedItem.required?.includes("body") ?? false, simplifiedItem.properties.body)
598
807
  );
599
808
  }
600
809
  }
601
810
  }
602
- async #errorResponse(ref, def) {
811
+ async #errorResponse(ref, def, baseSchemaConvertOptions, undefinedErrorSchema, customErrorResponseBodySchema) {
603
812
  const errorMap = def.errorMap;
604
- const errors = {};
813
+ const errorResponsesByStatus = {};
605
814
  for (const code in errorMap) {
606
815
  const config = errorMap[code];
607
816
  if (!config) {
608
817
  continue;
609
818
  }
610
819
  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({
820
+ const defaultMessage = fallbackORPCErrorMessage(code, config.message);
821
+ errorResponsesByStatus[status] ??= { status, definedErrorDefinitions: [], errorSchemaVariants: [] };
822
+ const [dataRequired, dataSchema] = await this.converter.convert(config.data, { ...baseSchemaConvertOptions, strategy: "output" });
823
+ errorResponsesByStatus[status].definedErrorDefinitions.push([code, defaultMessage, dataRequired, dataSchema]);
824
+ errorResponsesByStatus[status].errorSchemaVariants.push({
615
825
  type: "object",
616
826
  properties: {
617
827
  defined: { const: true },
618
828
  code: { const: code },
619
829
  status: { const: status },
620
- message: { type: "string", default: message },
830
+ message: { type: "string", default: defaultMessage },
621
831
  data: dataSchema
622
832
  },
623
833
  required: dataRequired ? ["defined", "code", "status", "message", "data"] : ["defined", "code", "status", "message"]
624
834
  });
625
835
  }
626
836
  ref.responses ??= {};
627
- for (const status in errors) {
628
- const schemas = errors[status];
629
- ref.responses[status] = {
630
- description: status,
631
- 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 ?? {
632
843
  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
- }
844
+ ...errorResponse.errorSchemaVariants,
845
+ undefinedErrorSchema
645
846
  ]
646
847
  })
647
848
  };
@@ -649,4 +850,4 @@ ${errors.join("\n\n")}`
649
850
  }
650
851
  }
651
852
 
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 };
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 };