@orpc/openapi 0.0.0-next.460b50b → 0.0.0-next.47dbd93

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/README.md +138 -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 +10 -3
  18. package/dist/index.d.ts +10 -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.CbIlrReM.d.mts → openapi.BGy4N6eR.d.mts} +30 -8
  25. package/dist/shared/{openapi.CbIlrReM.d.ts → openapi.BGy4N6eR.d.ts} +30 -8
  26. package/dist/shared/{openapi.C_3bk7bB.mjs → openapi.BwdtJjDu.mjs} +222 -69
  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
@@ -1,11 +1,11 @@
1
1
  import { AnySchema, OpenAPI, AnyContractProcedure, AnyContractRouter } from '@orpc/contract';
2
2
  import { StandardOpenAPIJsonSerializerOptions } from '@orpc/openapi-client/standard';
3
- import { AnyProcedure, AnyRouter } from '@orpc/server';
4
- import { Promisable } from '@orpc/shared';
3
+ import { AnyProcedure, TraverseContractProcedureCallbackOptions, AnyRouter } from '@orpc/server';
4
+ import { Promisable, Value } from '@orpc/shared';
5
5
  import { JSONSchema } from 'json-schema-typed/draft-2020-12';
6
6
 
7
7
  interface SchemaConverterComponent {
8
- allowedStrategies: SchemaConvertOptions['strategy'][];
8
+ allowedStrategies: readonly SchemaConvertOptions['strategy'][];
9
9
  schema: AnySchema;
10
10
  required: boolean;
11
11
  ref: string;
@@ -15,7 +15,7 @@ interface SchemaConvertOptions {
15
15
  /**
16
16
  * Common components should use `$ref` to represent themselves if matched.
17
17
  */
18
- components?: SchemaConverterComponent[];
18
+ components?: readonly SchemaConverterComponent[];
19
19
  /**
20
20
  * Minimum schema structure depth required before using `$ref` for components.
21
21
  *
@@ -33,7 +33,7 @@ interface ConditionalSchemaConverter extends SchemaConverter {
33
33
  }
34
34
  declare class CompositeSchemaConverter implements SchemaConverter {
35
35
  private readonly converters;
36
- constructor(converters: ConditionalSchemaConverter[]);
36
+ constructor(converters: readonly ConditionalSchemaConverter[]);
37
37
  convert(schema: AnySchema | undefined, options: SchemaConvertOptions): Promise<[required: boolean, jsonSchema: JSONSchema]>;
38
38
  }
39
39
 
@@ -44,9 +44,16 @@ interface OpenAPIGeneratorGenerateOptions extends Partial<Omit<OpenAPI.Document,
44
44
  /**
45
45
  * Exclude procedures from the OpenAPI specification.
46
46
  *
47
+ * @deprecated Use `filter` option instead.
47
48
  * @default () => false
48
49
  */
49
50
  exclude?: (procedure: AnyProcedure | AnyContractProcedure, path: readonly string[]) => boolean;
51
+ /**
52
+ * Filter procedures. Return `false` to exclude a procedure from the OpenAPI specification.
53
+ *
54
+ * @default true
55
+ */
56
+ filter?: Value<boolean, [options: TraverseContractProcedureCallbackOptions]>;
50
57
  /**
51
58
  * Common schemas to be used for $ref resolution.
52
59
  */
@@ -74,12 +81,27 @@ interface OpenAPIGeneratorGenerateOptions extends Partial<Omit<OpenAPI.Document,
74
81
  */
75
82
  strategy?: SchemaConvertOptions['strategy'];
76
83
  schema: AnySchema;
84
+ } | {
85
+ error: 'UndefinedError';
86
+ schema?: never;
77
87
  }>;
88
+ /**
89
+ * Define a custom JSON schema for the error response body when using
90
+ * type-safe errors. Helps align ORPC error formatting with existing API
91
+ * response standards or conventions.
92
+ *
93
+ * @remarks
94
+ * - Return `null | undefined` to use the default error response body shaper.
95
+ */
96
+ customErrorResponseBodySchema?: Value<JSONSchema | undefined | null, [
97
+ definedErrors: [code: string, defaultMessage: string, dataRequired: boolean, dataSchema: JSONSchema][],
98
+ status: number
99
+ ]>;
78
100
  }
79
101
  /**
80
102
  * The generator that converts oRPC routers/contracts to OpenAPI specifications.
81
103
  *
82
- * @see {@link https://orpc.unnoq.com/docs/openapi/openapi-specification OpenAPI Specification Docs}
104
+ * @see {@link https://orpc.dev/docs/openapi/openapi-specification OpenAPI Specification Docs}
83
105
  */
84
106
  declare class OpenAPIGenerator {
85
107
  #private;
@@ -89,9 +111,9 @@ declare class OpenAPIGenerator {
89
111
  /**
90
112
  * Generates OpenAPI specifications from oRPC routers/contracts.
91
113
  *
92
- * @see {@link https://orpc.unnoq.com/docs/openapi/openapi-specification OpenAPI Specification Docs}
114
+ * @see {@link https://orpc.dev/docs/openapi/openapi-specification OpenAPI Specification Docs}
93
115
  */
94
- generate(router: AnyContractRouter | AnyRouter, options?: OpenAPIGeneratorGenerateOptions): Promise<OpenAPI.Document>;
116
+ generate(router: AnyContractRouter | AnyRouter, { customErrorResponseBodySchema, commonSchemas, filter: baseFilter, exclude, ...baseDoc }?: OpenAPIGeneratorGenerateOptions): Promise<OpenAPI.Document>;
95
117
  }
96
118
 
97
119
  export { OpenAPIGenerator as b, CompositeSchemaConverter as e };
@@ -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
  };
@@ -354,6 +382,107 @@ function resolveOpenAPIJsonSchemaRef(doc, schema) {
354
382
  const resolved = doc.components?.schemas?.[name];
355
383
  return resolved ?? schema;
356
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
+ }
357
486
 
358
487
  class CompositeSchemaConverter {
359
488
  converters;
@@ -382,37 +511,38 @@ class OpenAPIGenerator {
382
511
  /**
383
512
  * Generates OpenAPI specifications from oRPC routers/contracts.
384
513
  *
385
- * @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}
386
515
  */
387
- async generate(router, options = {}) {
388
- 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
+ });
389
520
  const doc = {
390
- ...clone(options),
391
- info: options.info ?? { title: "API Reference", version: "0.0.0" },
392
- openapi: "3.1.1",
393
- exclude: void 0,
394
- commonSchemas: void 0
521
+ ...clone(baseDoc),
522
+ info: baseDoc.info ?? { title: "API Reference", version: "0.0.0" },
523
+ openapi: "3.1.1"
395
524
  };
396
- const baseSchemaConvertOptions = await this.#resolveCommonSchemas(doc, options.commonSchemas);
525
+ const { baseSchemaConvertOptions, undefinedErrorJsonSchema } = await this.#resolveCommonSchemas(doc, commonSchemas);
397
526
  const contracts = [];
398
- await resolveContractProcedures({ path: [], router }, ({ contract, path }) => {
399
- if (!exclude(contract, path)) {
400
- contracts.push({ contract, path });
527
+ await resolveContractProcedures({ path: [], router }, (traverseOptions) => {
528
+ if (!value(filter, traverseOptions)) {
529
+ return;
401
530
  }
531
+ contracts.push(traverseOptions);
402
532
  });
403
533
  const errors = [];
404
534
  for (const { contract, path } of contracts) {
405
- const operationId = path.join(".");
535
+ const stringPath = path.join(".");
406
536
  try {
407
537
  const def = contract["~orpc"];
408
538
  const method = toOpenAPIMethod(fallbackContractConfig("defaultMethod", def.route.method));
409
539
  const httpPath = toOpenAPIPath(def.route.path ?? toHttpPath(path));
410
540
  let operationObjectRef;
411
- if (def.route.spec !== void 0) {
541
+ if (def.route.spec !== void 0 && typeof def.route.spec !== "function") {
412
542
  operationObjectRef = def.route.spec;
413
543
  } else {
414
544
  operationObjectRef = {
415
- operationId,
545
+ operationId: def.route.operationId ?? stringPath,
416
546
  summary: def.route.summary,
417
547
  description: def.route.description,
418
548
  deprecated: def.route.deprecated,
@@ -420,7 +550,10 @@ class OpenAPIGenerator {
420
550
  };
421
551
  await this.#request(doc, operationObjectRef, def, baseSchemaConvertOptions);
422
552
  await this.#successResponse(doc, operationObjectRef, def, baseSchemaConvertOptions);
423
- await this.#errorResponse(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);
424
557
  }
425
558
  doc.paths ??= {};
426
559
  doc.paths[httpPath] ??= {};
@@ -430,7 +563,7 @@ class OpenAPIGenerator {
430
563
  throw e;
431
564
  }
432
565
  errors.push(
433
- `[OpenAPIGenerator] Error occurred while generating OpenAPI for procedure at path: ${operationId}
566
+ `[OpenAPIGenerator] Error occurred while generating OpenAPI for procedure at path: ${stringPath}
434
567
  ${e.message}`
435
568
  );
436
569
  }
@@ -445,11 +578,26 @@ ${errors.join("\n\n")}`
445
578
  return this.serializer.serialize(doc)[0];
446
579
  }
447
580
  async #resolveCommonSchemas(doc, commonSchemas) {
448
- const baseOptions = {};
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 = {};
449
593
  if (commonSchemas) {
450
- baseOptions.components = [];
594
+ baseSchemaConvertOptions.components = [];
451
595
  for (const key in commonSchemas) {
452
- const { schema, strategy = "input" } = commonSchemas[key];
596
+ const options = commonSchemas[key];
597
+ if (options.schema === void 0) {
598
+ continue;
599
+ }
600
+ const { schema, strategy = "input" } = options;
453
601
  const [required, json] = await this.converter.convert(schema, { strategy });
454
602
  const allowedStrategies = [strategy];
455
603
  if (strategy === "input") {
@@ -463,7 +611,7 @@ ${errors.join("\n\n")}`
463
611
  allowedStrategies.push("input");
464
612
  }
465
613
  }
466
- baseOptions.components.push({
614
+ baseSchemaConvertOptions.components.push({
467
615
  schema,
468
616
  required,
469
617
  ref: `#/components/schemas/${key}`,
@@ -473,11 +621,19 @@ ${errors.join("\n\n")}`
473
621
  doc.components ??= {};
474
622
  doc.components.schemas ??= {};
475
623
  for (const key in commonSchemas) {
476
- const { schema, strategy = "input" } = commonSchemas[key];
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;
477
633
  const [, json] = await this.converter.convert(
478
634
  schema,
479
635
  {
480
- ...baseOptions,
636
+ ...baseSchemaConvertOptions,
481
637
  strategy,
482
638
  minStructureDepthForRef: 1
483
639
  // not allow use $ref for root schemas
@@ -486,7 +642,7 @@ ${errors.join("\n\n")}`
486
642
  doc.components.schemas[key] = toOpenAPISchema(json);
487
643
  }
488
644
  }
489
- return baseOptions;
645
+ return { baseSchemaConvertOptions, undefinedErrorJsonSchema };
490
646
  }
491
647
  async #request(doc, ref, def, baseSchemaConvertOptions) {
492
648
  const method = fallbackContractConfig("defaultMethod", def.route.method);
@@ -507,13 +663,16 @@ ${errors.join("\n\n")}`
507
663
  def.inputSchema,
508
664
  {
509
665
  ...baseSchemaConvertOptions,
510
- strategy: "input",
511
- minStructureDepthForRef: dynamicParams?.length || inputStructure === "detailed" ? 1 : 0
666
+ strategy: "input"
512
667
  }
513
668
  );
669
+ let omitResponseBody = false;
514
670
  if (isAnySchema(schema) && !dynamicParams?.length) {
515
671
  return;
516
672
  }
673
+ if (inputStructure === "detailed" || inputStructure === "compact" && (dynamicParams?.length || method === "GET")) {
674
+ schema = simplifyComposedObjectJsonSchemasAndRefs(schema, doc);
675
+ }
517
676
  if (inputStructure === "compact") {
518
677
  if (dynamicParams?.length) {
519
678
  const error2 = new OpenAPIGeneratorError(
@@ -525,6 +684,7 @@ ${errors.join("\n\n")}`
525
684
  const [paramsSchema, rest] = separateObjectSchema(schema, dynamicParams);
526
685
  schema = rest;
527
686
  required = rest.required ? rest.required.length !== 0 : false;
687
+ omitResponseBody = !required && !rest.properties;
528
688
  if (!checkParamsSchema(paramsSchema, dynamicParams)) {
529
689
  throw error2;
530
690
  }
@@ -539,7 +699,7 @@ ${errors.join("\n\n")}`
539
699
  }
540
700
  ref.parameters ??= [];
541
701
  ref.parameters.push(...toOpenAPIParameters(schema, "query"));
542
- } else {
702
+ } else if (!omitResponseBody) {
543
703
  ref.requestBody = {
544
704
  required,
545
705
  content: toOpenAPIContent(schema)
@@ -553,7 +713,7 @@ ${errors.join("\n\n")}`
553
713
  if (!isObjectSchema(schema)) {
554
714
  throw error;
555
715
  }
556
- const resolvedParamSchema = schema.properties?.params !== void 0 ? resolveOpenAPIJsonSchemaRef(doc, schema.properties.params) : void 0;
716
+ const resolvedParamSchema = schema.properties?.params !== void 0 ? simplifyComposedObjectJsonSchemasAndRefs(schema.properties.params, doc) : void 0;
557
717
  if (dynamicParams?.length && (resolvedParamSchema === void 0 || !isObjectSchema(resolvedParamSchema) || !checkParamsSchema(resolvedParamSchema, dynamicParams))) {
558
718
  throw new OpenAPIGeneratorError(
559
719
  'When input structure is "detailed" and path has dynamic params, the "params" schema must be an object with all dynamic params as required.'
@@ -562,7 +722,7 @@ ${errors.join("\n\n")}`
562
722
  for (const from of ["params", "query", "headers"]) {
563
723
  const fromSchema = schema.properties?.[from];
564
724
  if (fromSchema !== void 0) {
565
- const resolvedSchema = resolveOpenAPIJsonSchemaRef(doc, fromSchema);
725
+ const resolvedSchema = simplifyComposedObjectJsonSchemasAndRefs(fromSchema, doc);
566
726
  if (!isObjectSchema(resolvedSchema)) {
567
727
  throw error;
568
728
  }
@@ -623,13 +783,14 @@ ${errors.join("\n\n")}`
623
783
 
624
784
  But got: ${stringifyJSON(item)}
625
785
  `);
626
- if (!isObjectSchema(item)) {
786
+ const simplifiedItem = simplifyComposedObjectJsonSchemasAndRefs(item, doc);
787
+ if (!isObjectSchema(simplifiedItem)) {
627
788
  throw error;
628
789
  }
629
790
  let schemaStatus;
630
791
  let schemaDescription;
631
- if (item.properties?.status !== void 0) {
632
- const statusSchema = resolveOpenAPIJsonSchemaRef(doc, item.properties.status);
792
+ if (simplifiedItem.properties?.status !== void 0) {
793
+ const statusSchema = resolveOpenAPIJsonSchemaRef(doc, simplifiedItem.properties.status);
633
794
  if (typeof statusSchema !== "object" || statusSchema.const === void 0 || typeof statusSchema.const !== "number" || !Number.isInteger(statusSchema.const) || isORPCErrorStatus(statusSchema.const)) {
634
795
  throw error;
635
796
  }
@@ -649,8 +810,8 @@ ${errors.join("\n\n")}`
649
810
  ref.responses[itemStatus] = {
650
811
  description: itemDescription
651
812
  };
652
- if (item.properties?.headers !== void 0) {
653
- const headersSchema = resolveOpenAPIJsonSchemaRef(doc, item.properties.headers);
813
+ if (simplifiedItem.properties?.headers !== void 0) {
814
+ const headersSchema = simplifyComposedObjectJsonSchemasAndRefs(simplifiedItem.properties.headers, doc);
654
815
  if (!isObjectSchema(headersSchema)) {
655
816
  throw error;
656
817
  }
@@ -660,61 +821,53 @@ ${errors.join("\n\n")}`
660
821
  ref.responses[itemStatus].headers ??= {};
661
822
  ref.responses[itemStatus].headers[key] = {
662
823
  schema: toOpenAPISchema(headerSchema),
663
- required: item.required?.includes("headers") && headersSchema.required?.includes(key)
824
+ required: simplifiedItem.required?.includes("headers") && headersSchema.required?.includes(key)
664
825
  };
665
826
  }
666
827
  }
667
828
  }
668
- if (item.properties?.body !== void 0) {
829
+ if (simplifiedItem.properties?.body !== void 0) {
669
830
  ref.responses[itemStatus].content = toOpenAPIContent(
670
- applySchemaOptionality(item.required?.includes("body") ?? false, item.properties.body)
831
+ applySchemaOptionality(simplifiedItem.required?.includes("body") ?? false, simplifiedItem.properties.body)
671
832
  );
672
833
  }
673
834
  }
674
835
  }
675
- async #errorResponse(ref, def, baseSchemaConvertOptions) {
836
+ async #errorResponse(ref, def, baseSchemaConvertOptions, undefinedErrorSchema, customErrorResponseBodySchema) {
676
837
  const errorMap = def.errorMap;
677
- const errors = {};
838
+ const errorResponsesByStatus = {};
678
839
  for (const code in errorMap) {
679
840
  const config = errorMap[code];
680
841
  if (!config) {
681
842
  continue;
682
843
  }
683
844
  const status = fallbackORPCErrorStatus(code, config.status);
684
- const message = fallbackORPCErrorMessage(code, config.message);
845
+ const defaultMessage = fallbackORPCErrorMessage(code, config.message);
846
+ errorResponsesByStatus[status] ??= { status, definedErrorDefinitions: [], errorSchemaVariants: [] };
685
847
  const [dataRequired, dataSchema] = await this.converter.convert(config.data, { ...baseSchemaConvertOptions, strategy: "output" });
686
- errors[status] ??= [];
687
- errors[status].push({
848
+ errorResponsesByStatus[status].definedErrorDefinitions.push([code, defaultMessage, dataRequired, dataSchema]);
849
+ errorResponsesByStatus[status].errorSchemaVariants.push({
688
850
  type: "object",
689
851
  properties: {
690
852
  defined: { const: true },
691
853
  code: { const: code },
692
854
  status: { const: status },
693
- message: { type: "string", default: message },
855
+ message: { type: "string", default: defaultMessage },
694
856
  data: dataSchema
695
857
  },
696
858
  required: dataRequired ? ["defined", "code", "status", "message", "data"] : ["defined", "code", "status", "message"]
697
859
  });
698
860
  }
699
861
  ref.responses ??= {};
700
- for (const status in errors) {
701
- const schemas = errors[status];
702
- ref.responses[status] = {
703
- description: status,
704
- 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 ?? {
705
868
  oneOf: [
706
- ...schemas,
707
- {
708
- type: "object",
709
- properties: {
710
- defined: { const: false },
711
- code: { type: "string" },
712
- status: { type: "number" },
713
- message: { type: "string" },
714
- data: {}
715
- },
716
- required: ["defined", "code", "status", "message"]
717
- }
869
+ ...errorResponse.errorSchemaVariants,
870
+ undefinedErrorSchema
718
871
  ]
719
872
  })
720
873
  };
@@ -722,4 +875,4 @@ ${errors.join("\n\n")}`
722
875
  }
723
876
  }
724
877
 
725
- export { CompositeSchemaConverter as C, LOGIC_KEYWORDS as L, OpenAPIGenerator as O, applyCustomOpenAPIOperation as a, toOpenAPIMethod as b, customOpenAPIOperation as c, toOpenAPIContent as d, toOpenAPIEventIteratorContent as e, toOpenAPIParameters as f, getCustomOpenAPIOperation as g, checkParamsSchema as h, toOpenAPISchema as i, isFileSchema as j, isObjectSchema as k, isAnySchema as l, filterSchemaBranches as m, applySchemaOptionality as n, expandUnionSchema as o, expandArrayableSchema as p, isPrimitiveSchema as q, resolveOpenAPIJsonSchemaRef as r, 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 };