@orpc/openapi 0.0.0-next.e385fb7 → 0.0.0-next.e49bee7

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 +11 -8
  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 +19 -13
  18. package/dist/index.d.ts +19 -13
  19. package/dist/index.mjs +3 -3
  20. package/dist/plugins/index.d.mts +20 -6
  21. package/dist/plugins/index.d.ts +20 -6
  22. package/dist/plugins/index.mjs +59 -19
  23. package/dist/shared/openapi.BfNjg7j9.d.mts +120 -0
  24. package/dist/shared/openapi.BfNjg7j9.d.ts +120 -0
  25. package/dist/shared/{openapi.ExRBHuvW.mjs → openapi.CzHcOMxv.mjs} +280 -74
  26. package/dist/shared/{openapi.C_UtQ8Us.mjs → openapi.DIt-Z9W1.mjs} +19 -8
  27. package/dist/shared/openapi.DwaweYRb.d.mts +54 -0
  28. package/dist/shared/openapi.DwaweYRb.d.ts +54 -0
  29. package/package.json +16 -11
  30. package/dist/shared/openapi.CwdCLgSU.d.mts +0 -53
  31. package/dist/shared/openapi.CwdCLgSU.d.ts +0 -53
  32. package/dist/shared/openapi.D3j94c9n.d.mts +0 -12
  33. package/dist/shared/openapi.D3j94c9n.d.ts +0 -12
@@ -3,8 +3,8 @@ import { toHttpPath } from '@orpc/client/standard';
3
3
  import { fallbackContractConfig, getEventIteratorSchemaDetails } from '@orpc/contract';
4
4
  import { standardizeHTTPPath, StandardOpenAPIJsonSerializer, getDynamicParams } from '@orpc/openapi-client/standard';
5
5
  import { isProcedure, resolveContractProcedures } from '@orpc/server';
6
- import { isObject, stringifyJSON, findDeepMatches, toArray, clone } from '@orpc/shared';
7
- import { TypeName } from 'json-schema-typed/draft-2020-12';
6
+ import { isObject, stringifyJSON, findDeepMatches, toArray, clone, value } from '@orpc/shared';
7
+ import { TypeName } from '@orpc/interop/json-schema-typed/draft-2020-12';
8
8
 
9
9
  const OPERATION_EXTENDER_SYMBOL = Symbol("ORPC_OPERATION_EXTENDER");
10
10
  function customOpenAPIOperation(o, extend) {
@@ -114,13 +114,18 @@ function isAnySchema(schema) {
114
114
  return false;
115
115
  }
116
116
  function separateObjectSchema(schema, separatedProperties) {
117
- if (Object.keys(schema).some((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,37 +490,48 @@ 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
- const operationObjectRef = {
400
- operationId,
401
- summary: def.route.summary,
402
- description: def.route.description,
403
- deprecated: def.route.deprecated,
404
- tags: def.route.tags?.map((tag) => tag)
405
- };
406
- await this.#request(operationObjectRef, def);
407
- await this.#successResponse(operationObjectRef, def);
408
- await this.#errorResponse(operationObjectRef, def);
517
+ let operationObjectRef;
518
+ if (def.route.spec !== void 0 && typeof def.route.spec !== "function") {
519
+ operationObjectRef = def.route.spec;
520
+ } else {
521
+ operationObjectRef = {
522
+ operationId: def.route.operationId ?? stringPath,
523
+ summary: def.route.summary,
524
+ description: def.route.description,
525
+ deprecated: def.route.deprecated,
526
+ tags: def.route.tags?.map((tag) => tag)
527
+ };
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);
534
+ }
409
535
  doc.paths ??= {};
410
536
  doc.paths[httpPath] ??= {};
411
537
  doc.paths[httpPath][method] = applyCustomOpenAPIOperation(operationObjectRef, contract);
@@ -414,7 +540,7 @@ class OpenAPIGenerator {
414
540
  throw e;
415
541
  }
416
542
  errors.push(
417
- `[OpenAPIGenerator] Error occurred while generating OpenAPI for procedure at path: ${operationId}
543
+ `[OpenAPIGenerator] Error occurred while generating OpenAPI for procedure at path: ${stringPath}
418
544
  ${e.message}`
419
545
  );
420
546
  }
@@ -428,25 +554,101 @@ ${errors.join("\n\n")}`
428
554
  }
429
555
  return this.serializer.serialize(doc)[0];
430
556
  }
431
- 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) {
432
625
  const method = fallbackContractConfig("defaultMethod", def.route.method);
433
626
  const details = getEventIteratorSchemaDetails(def.inputSchema);
434
627
  if (details) {
435
628
  ref.requestBody = {
436
629
  required: true,
437
630
  content: toOpenAPIEventIteratorContent(
438
- await this.converter.convert(details.yields, { strategy: "input" }),
439
- 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" })
440
633
  )
441
634
  };
442
635
  return;
443
636
  }
444
637
  const dynamicParams = getDynamicParams(def.route.path)?.map((v) => v.name);
445
638
  const inputStructure = fallbackContractConfig("defaultInputStructure", def.route.inputStructure);
446
- 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
+ );
447
646
  if (isAnySchema(schema) && !dynamicParams?.length) {
448
647
  return;
449
648
  }
649
+ if (inputStructure === "detailed" || inputStructure === "compact" && (dynamicParams?.length || method === "GET")) {
650
+ schema = simplifyComposedObjectJsonSchemasAndRefs(schema, doc);
651
+ }
450
652
  if (inputStructure === "compact") {
451
653
  if (dynamicParams?.length) {
452
654
  const error2 = new OpenAPIGeneratorError(
@@ -486,7 +688,8 @@ ${errors.join("\n\n")}`
486
688
  if (!isObjectSchema(schema)) {
487
689
  throw error;
488
690
  }
489
- 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))) {
490
693
  throw new OpenAPIGeneratorError(
491
694
  'When input structure is "detailed" and path has dynamic params, the "params" schema must be an object with all dynamic params as required.'
492
695
  );
@@ -494,12 +697,13 @@ ${errors.join("\n\n")}`
494
697
  for (const from of ["params", "query", "headers"]) {
495
698
  const fromSchema = schema.properties?.[from];
496
699
  if (fromSchema !== void 0) {
497
- if (!isObjectSchema(fromSchema)) {
700
+ const resolvedSchema = simplifyComposedObjectJsonSchemasAndRefs(fromSchema, doc);
701
+ if (!isObjectSchema(resolvedSchema)) {
498
702
  throw error;
499
703
  }
500
704
  const parameterIn = from === "params" ? "path" : from === "headers" ? "header" : "query";
501
705
  ref.parameters ??= [];
502
- ref.parameters.push(...toOpenAPIParameters(fromSchema, parameterIn));
706
+ ref.parameters.push(...toOpenAPIParameters(resolvedSchema, parameterIn));
503
707
  }
504
708
  }
505
709
  if (schema.properties?.body !== void 0) {
@@ -509,7 +713,7 @@ ${errors.join("\n\n")}`
509
713
  };
510
714
  }
511
715
  }
512
- async #successResponse(ref, def) {
716
+ async #successResponse(doc, ref, def, baseSchemaConvertOptions) {
513
717
  const outputSchema = def.outputSchema;
514
718
  const status = fallbackContractConfig("defaultSuccessStatus", def.route.successStatus);
515
719
  const description = fallbackContractConfig("defaultSuccessDescription", def.route?.successDescription);
@@ -520,13 +724,20 @@ ${errors.join("\n\n")}`
520
724
  ref.responses[status] = {
521
725
  description,
522
726
  content: toOpenAPIEventIteratorContent(
523
- await this.converter.convert(eventIteratorSchemaDetails.yields, { strategy: "output" }),
524
- 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" })
525
729
  )
526
730
  };
527
731
  return;
528
732
  }
529
- 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
+ );
530
741
  if (outputStructure === "compact") {
531
742
  ref.responses ??= {};
532
743
  ref.responses[status] = {
@@ -547,17 +758,19 @@ ${errors.join("\n\n")}`
547
758
 
548
759
  But got: ${stringifyJSON(item)}
549
760
  `);
550
- if (!isObjectSchema(item)) {
761
+ const simplifiedItem = simplifyComposedObjectJsonSchemasAndRefs(item, doc);
762
+ if (!isObjectSchema(simplifiedItem)) {
551
763
  throw error;
552
764
  }
553
765
  let schemaStatus;
554
766
  let schemaDescription;
555
- if (item.properties?.status !== void 0) {
556
- 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)) {
557
770
  throw error;
558
771
  }
559
- schemaStatus = item.properties.status.const;
560
- schemaDescription = item.properties.status.description;
772
+ schemaStatus = statusSchema.const;
773
+ schemaDescription = statusSchema.description;
561
774
  }
562
775
  const itemStatus = schemaStatus ?? status;
563
776
  const itemDescription = schemaDescription ?? description;
@@ -572,71 +785,64 @@ ${errors.join("\n\n")}`
572
785
  ref.responses[itemStatus] = {
573
786
  description: itemDescription
574
787
  };
575
- if (item.properties?.headers !== void 0) {
576
- if (!isObjectSchema(item.properties.headers)) {
788
+ if (simplifiedItem.properties?.headers !== void 0) {
789
+ const headersSchema = simplifyComposedObjectJsonSchemasAndRefs(simplifiedItem.properties.headers, doc);
790
+ if (!isObjectSchema(headersSchema)) {
577
791
  throw error;
578
792
  }
579
- for (const key in item.properties.headers.properties) {
580
- const headerSchema = item.properties.headers.properties[key];
793
+ for (const key in headersSchema.properties) {
794
+ const headerSchema = headersSchema.properties[key];
581
795
  if (headerSchema !== void 0) {
582
796
  ref.responses[itemStatus].headers ??= {};
583
797
  ref.responses[itemStatus].headers[key] = {
584
798
  schema: toOpenAPISchema(headerSchema),
585
- required: item.properties.headers.required?.includes(key)
799
+ required: simplifiedItem.required?.includes("headers") && headersSchema.required?.includes(key)
586
800
  };
587
801
  }
588
802
  }
589
803
  }
590
- if (item.properties?.body !== void 0) {
804
+ if (simplifiedItem.properties?.body !== void 0) {
591
805
  ref.responses[itemStatus].content = toOpenAPIContent(
592
- applySchemaOptionality(item.required?.includes("body") ?? false, item.properties.body)
806
+ applySchemaOptionality(simplifiedItem.required?.includes("body") ?? false, simplifiedItem.properties.body)
593
807
  );
594
808
  }
595
809
  }
596
810
  }
597
- async #errorResponse(ref, def) {
811
+ async #errorResponse(ref, def, baseSchemaConvertOptions, undefinedErrorSchema, customErrorResponseBodySchema) {
598
812
  const errorMap = def.errorMap;
599
- const errors = {};
813
+ const errorResponsesByStatus = {};
600
814
  for (const code in errorMap) {
601
815
  const config = errorMap[code];
602
816
  if (!config) {
603
817
  continue;
604
818
  }
605
819
  const status = fallbackORPCErrorStatus(code, config.status);
606
- const message = fallbackORPCErrorMessage(code, config.message);
607
- const [dataRequired, dataSchema] = await this.converter.convert(config.data, { strategy: "output" });
608
- errors[status] ??= [];
609
- 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({
610
825
  type: "object",
611
826
  properties: {
612
827
  defined: { const: true },
613
828
  code: { const: code },
614
829
  status: { const: status },
615
- message: { type: "string", default: message },
830
+ message: { type: "string", default: defaultMessage },
616
831
  data: dataSchema
617
832
  },
618
833
  required: dataRequired ? ["defined", "code", "status", "message", "data"] : ["defined", "code", "status", "message"]
619
834
  });
620
835
  }
621
836
  ref.responses ??= {};
622
- for (const status in errors) {
623
- const schemas = errors[status];
624
- ref.responses[status] = {
625
- description: status,
626
- 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 ?? {
627
843
  oneOf: [
628
- ...schemas,
629
- {
630
- type: "object",
631
- properties: {
632
- defined: { const: false },
633
- code: { type: "string" },
634
- status: { type: "number" },
635
- message: { type: "string" },
636
- data: {}
637
- },
638
- required: ["defined", "code", "status", "message"]
639
- }
844
+ ...errorResponse.errorSchemaVariants,
845
+ undefinedErrorSchema
640
846
  ]
641
847
  })
642
848
  };
@@ -644,4 +850,4 @@ ${errors.join("\n\n")}`
644
850
  }
645
851
  }
646
852
 
647
- 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 };
@@ -2,15 +2,17 @@ import { standardizeHTTPPath, StandardOpenAPIJsonSerializer, StandardBracketNota
2
2
  import { StandardHandler } from '@orpc/server/standard';
3
3
  import { isORPCErrorStatus } from '@orpc/client';
4
4
  import { fallbackContractConfig } from '@orpc/contract';
5
- import { isObject, stringifyJSON } from '@orpc/shared';
5
+ import { isObject, stringifyJSON, tryDecodeURIComponent, value } from '@orpc/shared';
6
6
  import { toHttpPath } from '@orpc/client/standard';
7
7
  import { traverseContractProcedures, isProcedure, getLazyMeta, unlazy, getRouter, createContractedProcedure } from '@orpc/server';
8
8
  import { createRouter, addRoute, findRoute } from 'rou3';
9
9
 
10
10
  class StandardOpenAPICodec {
11
- constructor(serializer) {
11
+ constructor(serializer, options = {}) {
12
12
  this.serializer = serializer;
13
+ this.customErrorResponseBodyEncoder = options.customErrorResponseBodyEncoder;
13
14
  }
15
+ customErrorResponseBodyEncoder;
14
16
  async decode(request, params, procedure) {
15
17
  const inputStructure = fallbackContractConfig("defaultInputStructure", procedure["~orpc"].route.inputStructure);
16
18
  if (inputStructure === "compact") {
@@ -73,10 +75,11 @@ class StandardOpenAPICodec {
73
75
  };
74
76
  }
75
77
  encodeError(error) {
78
+ const body = this.customErrorResponseBodyEncoder?.(error) ?? error.toJSON();
76
79
  return {
77
80
  status: error.status,
78
81
  headers: {},
79
- body: this.serializer.serialize(error.toJSON(), { outputFormat: "plain" })
82
+ body: this.serializer.serialize(body, { outputFormat: "plain" })
80
83
  };
81
84
  }
82
85
  #isDetailedOutput(output) {
@@ -97,14 +100,22 @@ function toRou3Pattern(path) {
97
100
  return standardizeHTTPPath(path).replace(/\/\{\+([^}]+)\}/g, "/**:$1").replace(/\/\{([^}]+)\}/g, "/:$1");
98
101
  }
99
102
  function decodeParams(params) {
100
- return Object.fromEntries(Object.entries(params).map(([key, value]) => [key, decodeURIComponent(value)]));
103
+ return Object.fromEntries(Object.entries(params).map(([key, value]) => [key, tryDecodeURIComponent(value)]));
101
104
  }
102
105
 
103
106
  class StandardOpenAPIMatcher {
107
+ filter;
104
108
  tree = createRouter();
105
109
  pendingRouters = [];
110
+ constructor(options = {}) {
111
+ this.filter = options.filter ?? true;
112
+ }
106
113
  init(router, path = []) {
107
- const laziedOptions = traverseContractProcedures({ router, path }, ({ path: path2, contract }) => {
114
+ const laziedOptions = traverseContractProcedures({ router, path }, (traverseOptions) => {
115
+ if (!value(this.filter, traverseOptions)) {
116
+ return;
117
+ }
118
+ const { path: path2, contract } = traverseOptions;
108
119
  const method = fallbackContractConfig("defaultMethod", contract["~orpc"].route.method);
109
120
  const httpPath = toRou3Pattern(contract["~orpc"].route.path ?? toHttpPath(path2));
110
121
  if (isProcedure(contract)) {
@@ -168,10 +179,10 @@ class StandardOpenAPIMatcher {
168
179
  class StandardOpenAPIHandler extends StandardHandler {
169
180
  constructor(router, options) {
170
181
  const jsonSerializer = new StandardOpenAPIJsonSerializer(options);
171
- const bracketNotationSerializer = new StandardBracketNotationSerializer();
182
+ const bracketNotationSerializer = new StandardBracketNotationSerializer(options);
172
183
  const serializer = new StandardOpenAPISerializer(jsonSerializer, bracketNotationSerializer);
173
- const matcher = new StandardOpenAPIMatcher();
174
- const codec = new StandardOpenAPICodec(serializer);
184
+ const matcher = new StandardOpenAPIMatcher(options);
185
+ const codec = new StandardOpenAPICodec(serializer, options);
175
186
  super(router, matcher, codec, options);
176
187
  }
177
188
  }
@@ -0,0 +1,54 @@
1
+ import { StandardOpenAPISerializer, StandardOpenAPIJsonSerializerOptions, StandardBracketNotationSerializerOptions } from '@orpc/openapi-client/standard';
2
+ import { AnyProcedure, TraverseContractProcedureCallbackOptions, AnyRouter, Context, Router } from '@orpc/server';
3
+ import { StandardCodec, StandardParams, StandardMatcher, StandardMatchResult, StandardHandlerOptions, StandardHandler } from '@orpc/server/standard';
4
+ import { ORPCError, HTTPPath } from '@orpc/client';
5
+ import { StandardLazyRequest, StandardResponse } from '@orpc/standard-server';
6
+ import { Value } from '@orpc/shared';
7
+
8
+ interface StandardOpenAPICodecOptions {
9
+ /**
10
+ * Customize how an ORPC error is encoded into a response body.
11
+ * Use this if your API needs a different error output structure.
12
+ *
13
+ * @remarks
14
+ * - Return `null | undefined` to fallback to default behavior
15
+ *
16
+ * @default ((e) => e.toJSON())
17
+ */
18
+ customErrorResponseBodyEncoder?: (error: ORPCError<any, any>) => unknown;
19
+ }
20
+ declare class StandardOpenAPICodec implements StandardCodec {
21
+ #private;
22
+ private readonly serializer;
23
+ private readonly customErrorResponseBodyEncoder;
24
+ constructor(serializer: StandardOpenAPISerializer, options?: StandardOpenAPICodecOptions);
25
+ decode(request: StandardLazyRequest, params: StandardParams | undefined, procedure: AnyProcedure): Promise<unknown>;
26
+ encode(output: unknown, procedure: AnyProcedure): StandardResponse;
27
+ encodeError(error: ORPCError<any, any>): StandardResponse;
28
+ }
29
+
30
+ interface StandardOpenAPIMatcherOptions {
31
+ /**
32
+ * Filter procedures. Return `false` to exclude a procedure from matching.
33
+ *
34
+ * @default true
35
+ */
36
+ filter?: Value<boolean, [options: TraverseContractProcedureCallbackOptions]>;
37
+ }
38
+ declare class StandardOpenAPIMatcher implements StandardMatcher {
39
+ private readonly filter;
40
+ private readonly tree;
41
+ private pendingRouters;
42
+ constructor(options?: StandardOpenAPIMatcherOptions);
43
+ init(router: AnyRouter, path?: readonly string[]): void;
44
+ match(method: string, pathname: HTTPPath): Promise<StandardMatchResult>;
45
+ }
46
+
47
+ interface StandardOpenAPIHandlerOptions<T extends Context> extends StandardHandlerOptions<T>, StandardOpenAPIJsonSerializerOptions, StandardBracketNotationSerializerOptions, StandardOpenAPIMatcherOptions, StandardOpenAPICodecOptions {
48
+ }
49
+ declare class StandardOpenAPIHandler<T extends Context> extends StandardHandler<T> {
50
+ constructor(router: Router<any, T>, options: NoInfer<StandardOpenAPIHandlerOptions<T>>);
51
+ }
52
+
53
+ export { StandardOpenAPICodec as a, StandardOpenAPIHandler as c, StandardOpenAPIMatcher as e };
54
+ export type { StandardOpenAPICodecOptions as S, StandardOpenAPIHandlerOptions as b, StandardOpenAPIMatcherOptions as d };