@orpc/openapi 0.0.0-next.7336c81 → 0.0.0-next.739ee37

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 -7
  2. package/dist/adapters/aws-lambda/index.d.mts +20 -0
  3. package/dist/adapters/aws-lambda/index.d.ts +20 -0
  4. package/dist/adapters/aws-lambda/index.mjs +18 -0
  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 +10 -4
  9. package/dist/adapters/fetch/index.d.ts +10 -4
  10. package/dist/adapters/fetch/index.mjs +2 -1
  11. package/dist/adapters/node/index.d.mts +10 -4
  12. package/dist/adapters/node/index.d.ts +10 -4
  13. package/dist/adapters/node/index.mjs +2 -1
  14. package/dist/adapters/standard/index.d.mts +8 -22
  15. package/dist/adapters/standard/index.d.ts +8 -22
  16. package/dist/adapters/standard/index.mjs +2 -1
  17. package/dist/index.d.mts +29 -15
  18. package/dist/index.d.ts +29 -15
  19. package/dist/index.mjs +34 -8
  20. package/dist/plugins/index.d.mts +26 -12
  21. package/dist/plugins/index.d.ts +26 -12
  22. package/dist/plugins/index.mjs +74 -23
  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.p5tsmBXx.mjs → openapi.DIt-Z9W1.mjs} +45 -13
  26. package/dist/shared/{openapi.fMEQd3Yd.mjs → openapi.DrTcell5.mjs} +293 -87
  27. package/dist/shared/openapi.DwaweYRb.d.mts +54 -0
  28. package/dist/shared/openapi.DwaweYRb.d.ts +54 -0
  29. package/package.json +21 -11
  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.DP97kr00.d.mts +0 -47
  33. package/dist/shared/openapi.DP97kr00.d.ts +0 -47
@@ -1,15 +1,18 @@
1
1
  import { standardizeHTTPPath, StandardOpenAPIJsonSerializer, StandardBracketNotationSerializer, StandardOpenAPISerializer } from '@orpc/openapi-client/standard';
2
2
  import { StandardHandler } from '@orpc/server/standard';
3
+ import { isORPCErrorStatus } from '@orpc/client';
3
4
  import { fallbackContractConfig } from '@orpc/contract';
4
- import { isObject } from '@orpc/shared';
5
+ import { isObject, stringifyJSON, tryDecodeURIComponent, value } from '@orpc/shared';
5
6
  import { toHttpPath } from '@orpc/client/standard';
6
7
  import { traverseContractProcedures, isProcedure, getLazyMeta, unlazy, getRouter, createContractedProcedure } from '@orpc/server';
7
8
  import { createRouter, addRoute, findRoute } from 'rou3';
8
9
 
9
10
  class StandardOpenAPICodec {
10
- constructor(serializer) {
11
+ constructor(serializer, options = {}) {
11
12
  this.serializer = serializer;
13
+ this.customErrorResponseBodyEncoder = options.customErrorResponseBodyEncoder;
12
14
  }
15
+ customErrorResponseBodyEncoder;
13
16
  async decode(request, params, procedure) {
14
17
  const inputStructure = fallbackContractConfig("defaultInputStructure", procedure["~orpc"].route.inputStructure);
15
18
  if (inputStructure === "compact") {
@@ -52,38 +55,67 @@ class StandardOpenAPICodec {
52
55
  body: this.serializer.serialize(output)
53
56
  };
54
57
  }
55
- if (!isObject(output)) {
56
- throw new Error(
57
- 'Invalid output structure for "detailed" output. Expected format: { body: any, headers?: Record<string, string | string[] | undefined> }'
58
- );
58
+ if (!this.#isDetailedOutput(output)) {
59
+ throw new Error(`
60
+ Invalid "detailed" output structure:
61
+ \u2022 Expected an object with optional properties:
62
+ - status (number 200-399)
63
+ - headers (Record<string, string | string[]>)
64
+ - body (any)
65
+ \u2022 No extra keys allowed.
66
+
67
+ Actual value:
68
+ ${stringifyJSON(output)}
69
+ `);
59
70
  }
60
71
  return {
61
- status: successStatus,
72
+ status: output.status ?? successStatus,
62
73
  headers: output.headers ?? {},
63
74
  body: this.serializer.serialize(output.body)
64
75
  };
65
76
  }
66
77
  encodeError(error) {
78
+ const body = this.customErrorResponseBodyEncoder?.(error) ?? error.toJSON();
67
79
  return {
68
80
  status: error.status,
69
81
  headers: {},
70
- body: this.serializer.serialize(error.toJSON(), { outputFormat: "plain" })
82
+ body: this.serializer.serialize(body, { outputFormat: "plain" })
71
83
  };
72
84
  }
85
+ #isDetailedOutput(output) {
86
+ if (!isObject(output)) {
87
+ return false;
88
+ }
89
+ if (output.headers && !isObject(output.headers)) {
90
+ return false;
91
+ }
92
+ if (output.status !== void 0 && (typeof output.status !== "number" || !Number.isInteger(output.status) || isORPCErrorStatus(output.status))) {
93
+ return false;
94
+ }
95
+ return true;
96
+ }
73
97
  }
74
98
 
75
99
  function toRou3Pattern(path) {
76
100
  return standardizeHTTPPath(path).replace(/\/\{\+([^}]+)\}/g, "/**:$1").replace(/\/\{([^}]+)\}/g, "/:$1");
77
101
  }
78
102
  function decodeParams(params) {
79
- 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)]));
80
104
  }
81
105
 
82
106
  class StandardOpenAPIMatcher {
107
+ filter;
83
108
  tree = createRouter();
84
109
  pendingRouters = [];
110
+ constructor(options = {}) {
111
+ this.filter = options.filter ?? true;
112
+ }
85
113
  init(router, path = []) {
86
- 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;
87
119
  const method = fallbackContractConfig("defaultMethod", contract["~orpc"].route.method);
88
120
  const httpPath = toRou3Pattern(contract["~orpc"].route.path ?? toHttpPath(path2));
89
121
  if (isProcedure(contract)) {
@@ -147,10 +179,10 @@ class StandardOpenAPIMatcher {
147
179
  class StandardOpenAPIHandler extends StandardHandler {
148
180
  constructor(router, options) {
149
181
  const jsonSerializer = new StandardOpenAPIJsonSerializer(options);
150
- const bracketNotationSerializer = new StandardBracketNotationSerializer();
182
+ const bracketNotationSerializer = new StandardBracketNotationSerializer(options);
151
183
  const serializer = new StandardOpenAPISerializer(jsonSerializer, bracketNotationSerializer);
152
- const matcher = new StandardOpenAPIMatcher();
153
- const codec = new StandardOpenAPICodec(serializer);
184
+ const matcher = new StandardOpenAPIMatcher(options);
185
+ const codec = new StandardOpenAPICodec(serializer, options);
154
186
  super(router, matcher, codec, options);
155
187
  }
156
188
  }
@@ -1,10 +1,10 @@
1
- import { fallbackORPCErrorStatus, fallbackORPCErrorMessage } from '@orpc/client';
1
+ import { isORPCErrorStatus, fallbackORPCErrorStatus, fallbackORPCErrorMessage } from '@orpc/client';
2
2
  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, findDeepMatches, toArray, clone } from '@orpc/shared';
7
- import '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));
@@ -184,6 +189,57 @@ function applySchemaOptionality(required, schema) {
184
189
  ]
185
190
  };
186
191
  }
192
+ function expandUnionSchema(schema) {
193
+ if (typeof schema === "object") {
194
+ for (const keyword of ["anyOf", "oneOf"]) {
195
+ if (schema[keyword] && Object.keys(schema).every(
196
+ (k) => k === keyword || !LOGIC_KEYWORDS.includes(k)
197
+ )) {
198
+ return schema[keyword].flatMap((s) => expandUnionSchema(s));
199
+ }
200
+ }
201
+ }
202
+ return [schema];
203
+ }
204
+ function expandArrayableSchema(schema) {
205
+ const schemas = expandUnionSchema(schema);
206
+ if (schemas.length !== 2) {
207
+ return void 0;
208
+ }
209
+ const arraySchema = schemas.find(
210
+ (s) => typeof s === "object" && s.type === "array" && Object.keys(s).filter((k) => LOGIC_KEYWORDS.includes(k)).every((k) => k === "type" || k === "items")
211
+ );
212
+ if (arraySchema === void 0) {
213
+ return void 0;
214
+ }
215
+ const items1 = arraySchema.items;
216
+ const items2 = schemas.find((s) => s !== arraySchema);
217
+ if (stringifyJSON(items1) !== stringifyJSON(items2)) {
218
+ return void 0;
219
+ }
220
+ return [items2, arraySchema];
221
+ }
222
+ const PRIMITIVE_SCHEMA_TYPES = /* @__PURE__ */ new Set([
223
+ TypeName.String,
224
+ TypeName.Number,
225
+ TypeName.Integer,
226
+ TypeName.Boolean,
227
+ TypeName.Null
228
+ ]);
229
+ function isPrimitiveSchema(schema) {
230
+ return expandUnionSchema(schema).every((s) => {
231
+ if (typeof s === "boolean") {
232
+ return false;
233
+ }
234
+ if (typeof s.type === "string" && PRIMITIVE_SCHEMA_TYPES.has(s.type)) {
235
+ return true;
236
+ }
237
+ if (s.const !== void 0) {
238
+ return true;
239
+ }
240
+ return false;
241
+ });
242
+ }
187
243
 
188
244
  function toOpenAPIPath(path) {
189
245
  return standardizeHTTPPath(path).replace(/\/\{\+([^}]+)\}/g, "/{$1}");
@@ -256,13 +312,26 @@ function toOpenAPIParameters(schema, parameterIn) {
256
312
  const parameters = [];
257
313
  for (const key in schema.properties) {
258
314
  const keySchema = schema.properties[key];
315
+ let isDeepObjectStyle = true;
316
+ if (parameterIn !== "query") {
317
+ isDeepObjectStyle = false;
318
+ } else if (isPrimitiveSchema(keySchema)) {
319
+ isDeepObjectStyle = false;
320
+ } else {
321
+ const [item] = expandArrayableSchema(keySchema) ?? [];
322
+ if (item !== void 0 && isPrimitiveSchema(item)) {
323
+ isDeepObjectStyle = false;
324
+ }
325
+ }
259
326
  parameters.push({
260
327
  name: key,
261
328
  in: parameterIn,
262
329
  required: schema.required?.includes(key),
263
- style: parameterIn === "query" ? "deepObject" : void 0,
264
- explode: parameterIn === "query" ? true : void 0,
265
- schema: toOpenAPISchema(keySchema)
330
+ schema: toOpenAPISchema(keySchema),
331
+ style: isDeepObjectStyle ? "deepObject" : void 0,
332
+ explode: isDeepObjectStyle ? true : void 0,
333
+ allowEmptyValue: parameterIn === "query" ? true : void 0,
334
+ allowReserved: parameterIn === "query" ? true : void 0
266
335
  });
267
336
  }
268
337
  return parameters;
@@ -281,6 +350,15 @@ function checkParamsSchema(schema, params) {
281
350
  function toOpenAPISchema(schema) {
282
351
  return schema === true ? {} : schema === false ? { not: {} } : schema;
283
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
+ }
284
362
 
285
363
  class CompositeSchemaConverter {
286
364
  converters;
@@ -311,33 +389,48 @@ class OpenAPIGenerator {
311
389
  *
312
390
  * @see {@link https://orpc.unnoq.com/docs/openapi/openapi-specification OpenAPI Specification Docs}
313
391
  */
314
- async generate(router, options = {}) {
392
+ async generate(router, { customErrorResponseBodySchema, commonSchemas, filter: baseFilter, exclude, ...baseDoc } = {}) {
393
+ const filter = baseFilter ?? (({ contract, path }) => {
394
+ return !(exclude?.(contract, path) ?? false);
395
+ });
315
396
  const doc = {
316
- ...clone(options),
317
- info: options.info ?? { title: "API Reference", version: "0.0.0" },
397
+ ...clone(baseDoc),
398
+ info: baseDoc.info ?? { title: "API Reference", version: "0.0.0" },
318
399
  openapi: "3.1.1"
319
400
  };
401
+ const { baseSchemaConvertOptions, undefinedErrorJsonSchema } = await this.#resolveCommonSchemas(doc, commonSchemas);
320
402
  const contracts = [];
321
- await resolveContractProcedures({ path: [], router }, ({ contract, path }) => {
322
- contracts.push({ contract, path });
403
+ await resolveContractProcedures({ path: [], router }, (traverseOptions) => {
404
+ if (!value(filter, traverseOptions)) {
405
+ return;
406
+ }
407
+ contracts.push(traverseOptions);
323
408
  });
324
409
  const errors = [];
325
410
  for (const { contract, path } of contracts) {
326
- const operationId = path.join(".");
411
+ const stringPath = path.join(".");
327
412
  try {
328
413
  const def = contract["~orpc"];
329
414
  const method = toOpenAPIMethod(fallbackContractConfig("defaultMethod", def.route.method));
330
415
  const httpPath = toOpenAPIPath(def.route.path ?? toHttpPath(path));
331
- const operationObjectRef = {
332
- operationId,
333
- summary: def.route.summary,
334
- description: def.route.description,
335
- deprecated: def.route.deprecated,
336
- tags: def.route.tags?.map((tag) => tag)
337
- };
338
- await this.#request(operationObjectRef, def);
339
- await this.#successResponse(operationObjectRef, def);
340
- await this.#errorResponse(operationObjectRef, def);
416
+ let operationObjectRef;
417
+ if (def.route.spec !== void 0 && typeof def.route.spec !== "function") {
418
+ operationObjectRef = def.route.spec;
419
+ } else {
420
+ operationObjectRef = {
421
+ operationId: def.route.operationId ?? stringPath,
422
+ summary: def.route.summary,
423
+ description: def.route.description,
424
+ deprecated: def.route.deprecated,
425
+ tags: def.route.tags?.map((tag) => tag)
426
+ };
427
+ await this.#request(doc, operationObjectRef, def, baseSchemaConvertOptions);
428
+ await this.#successResponse(doc, operationObjectRef, def, baseSchemaConvertOptions);
429
+ await this.#errorResponse(operationObjectRef, def, baseSchemaConvertOptions, undefinedErrorJsonSchema, customErrorResponseBodySchema);
430
+ }
431
+ if (typeof def.route.spec === "function") {
432
+ operationObjectRef = def.route.spec(operationObjectRef);
433
+ }
341
434
  doc.paths ??= {};
342
435
  doc.paths[httpPath] ??= {};
343
436
  doc.paths[httpPath][method] = applyCustomOpenAPIOperation(operationObjectRef, contract);
@@ -346,7 +439,7 @@ class OpenAPIGenerator {
346
439
  throw e;
347
440
  }
348
441
  errors.push(
349
- `[OpenAPIGenerator] Error occurred while generating OpenAPI for procedure at path: ${operationId}
442
+ `[OpenAPIGenerator] Error occurred while generating OpenAPI for procedure at path: ${stringPath}
350
443
  ${e.message}`
351
444
  );
352
445
  }
@@ -360,22 +453,96 @@ ${errors.join("\n\n")}`
360
453
  }
361
454
  return this.serializer.serialize(doc)[0];
362
455
  }
363
- async #request(ref, def) {
456
+ async #resolveCommonSchemas(doc, commonSchemas) {
457
+ let undefinedErrorJsonSchema = {
458
+ type: "object",
459
+ properties: {
460
+ defined: { const: false },
461
+ code: { type: "string" },
462
+ status: { type: "number" },
463
+ message: { type: "string" },
464
+ data: {}
465
+ },
466
+ required: ["defined", "code", "status", "message"]
467
+ };
468
+ const baseSchemaConvertOptions = {};
469
+ if (commonSchemas) {
470
+ baseSchemaConvertOptions.components = [];
471
+ for (const key in commonSchemas) {
472
+ const options = commonSchemas[key];
473
+ if (options.schema === void 0) {
474
+ continue;
475
+ }
476
+ const { schema, strategy = "input" } = options;
477
+ const [required, json] = await this.converter.convert(schema, { strategy });
478
+ const allowedStrategies = [strategy];
479
+ if (strategy === "input") {
480
+ const [outputRequired, outputJson] = await this.converter.convert(schema, { strategy: "output" });
481
+ if (outputRequired === required && stringifyJSON(outputJson) === stringifyJSON(json)) {
482
+ allowedStrategies.push("output");
483
+ }
484
+ } else if (strategy === "output") {
485
+ const [inputRequired, inputJson] = await this.converter.convert(schema, { strategy: "input" });
486
+ if (inputRequired === required && stringifyJSON(inputJson) === stringifyJSON(json)) {
487
+ allowedStrategies.push("input");
488
+ }
489
+ }
490
+ baseSchemaConvertOptions.components.push({
491
+ schema,
492
+ required,
493
+ ref: `#/components/schemas/${key}`,
494
+ allowedStrategies
495
+ });
496
+ }
497
+ doc.components ??= {};
498
+ doc.components.schemas ??= {};
499
+ for (const key in commonSchemas) {
500
+ const options = commonSchemas[key];
501
+ if (options.schema === void 0) {
502
+ if (options.error === "UndefinedError") {
503
+ doc.components.schemas[key] = toOpenAPISchema(undefinedErrorJsonSchema);
504
+ undefinedErrorJsonSchema = { $ref: `#/components/schemas/${key}` };
505
+ }
506
+ continue;
507
+ }
508
+ const { schema, strategy = "input" } = options;
509
+ const [, json] = await this.converter.convert(
510
+ schema,
511
+ {
512
+ ...baseSchemaConvertOptions,
513
+ strategy,
514
+ minStructureDepthForRef: 1
515
+ // not allow use $ref for root schemas
516
+ }
517
+ );
518
+ doc.components.schemas[key] = toOpenAPISchema(json);
519
+ }
520
+ }
521
+ return { baseSchemaConvertOptions, undefinedErrorJsonSchema };
522
+ }
523
+ async #request(doc, ref, def, baseSchemaConvertOptions) {
364
524
  const method = fallbackContractConfig("defaultMethod", def.route.method);
365
525
  const details = getEventIteratorSchemaDetails(def.inputSchema);
366
526
  if (details) {
367
527
  ref.requestBody = {
368
528
  required: true,
369
529
  content: toOpenAPIEventIteratorContent(
370
- await this.converter.convert(details.yields, { strategy: "input" }),
371
- await this.converter.convert(details.returns, { strategy: "input" })
530
+ await this.converter.convert(details.yields, { ...baseSchemaConvertOptions, strategy: "input" }),
531
+ await this.converter.convert(details.returns, { ...baseSchemaConvertOptions, strategy: "input" })
372
532
  )
373
533
  };
374
534
  return;
375
535
  }
376
536
  const dynamicParams = getDynamicParams(def.route.path)?.map((v) => v.name);
377
537
  const inputStructure = fallbackContractConfig("defaultInputStructure", def.route.inputStructure);
378
- let [required, schema] = await this.converter.convert(def.inputSchema, { strategy: "input" });
538
+ let [required, schema] = await this.converter.convert(
539
+ def.inputSchema,
540
+ {
541
+ ...baseSchemaConvertOptions,
542
+ strategy: "input",
543
+ minStructureDepthForRef: dynamicParams?.length || inputStructure === "detailed" ? 1 : 0
544
+ }
545
+ );
379
546
  if (isAnySchema(schema) && !dynamicParams?.length) {
380
547
  return;
381
548
  }
@@ -397,13 +564,14 @@ ${errors.join("\n\n")}`
397
564
  ref.parameters.push(...toOpenAPIParameters(paramsSchema, "path"));
398
565
  }
399
566
  if (method === "GET") {
400
- if (!isObjectSchema(schema)) {
567
+ const resolvedSchema = resolveOpenAPIJsonSchemaRef(doc, schema);
568
+ if (!isObjectSchema(resolvedSchema)) {
401
569
  throw new OpenAPIGeneratorError(
402
570
  'When method is "GET", input schema must satisfy: object | any | unknown'
403
571
  );
404
572
  }
405
573
  ref.parameters ??= [];
406
- ref.parameters.push(...toOpenAPIParameters(schema, "query"));
574
+ ref.parameters.push(...toOpenAPIParameters(resolvedSchema, "query"));
407
575
  } else {
408
576
  ref.requestBody = {
409
577
  required,
@@ -418,7 +586,8 @@ ${errors.join("\n\n")}`
418
586
  if (!isObjectSchema(schema)) {
419
587
  throw error;
420
588
  }
421
- if (dynamicParams?.length && (schema.properties?.params === void 0 || !isObjectSchema(schema.properties.params) || !checkParamsSchema(schema.properties.params, dynamicParams))) {
589
+ const resolvedParamSchema = schema.properties?.params !== void 0 ? resolveOpenAPIJsonSchemaRef(doc, schema.properties.params) : void 0;
590
+ if (dynamicParams?.length && (resolvedParamSchema === void 0 || !isObjectSchema(resolvedParamSchema) || !checkParamsSchema(resolvedParamSchema, dynamicParams))) {
422
591
  throw new OpenAPIGeneratorError(
423
592
  'When input structure is "detailed" and path has dynamic params, the "params" schema must be an object with all dynamic params as required.'
424
593
  );
@@ -426,12 +595,13 @@ ${errors.join("\n\n")}`
426
595
  for (const from of ["params", "query", "headers"]) {
427
596
  const fromSchema = schema.properties?.[from];
428
597
  if (fromSchema !== void 0) {
429
- if (!isObjectSchema(fromSchema)) {
598
+ const resolvedSchema = resolveOpenAPIJsonSchemaRef(doc, fromSchema);
599
+ if (!isObjectSchema(resolvedSchema)) {
430
600
  throw error;
431
601
  }
432
602
  const parameterIn = from === "params" ? "path" : from === "headers" ? "header" : "query";
433
603
  ref.parameters ??= [];
434
- ref.parameters.push(...toOpenAPIParameters(fromSchema, parameterIn));
604
+ ref.parameters.push(...toOpenAPIParameters(resolvedSchema, parameterIn));
435
605
  }
436
606
  }
437
607
  if (schema.properties?.body !== void 0) {
@@ -441,7 +611,7 @@ ${errors.join("\n\n")}`
441
611
  };
442
612
  }
443
613
  }
444
- async #successResponse(ref, def) {
614
+ async #successResponse(doc, ref, def, baseSchemaConvertOptions) {
445
615
  const outputSchema = def.outputSchema;
446
616
  const status = fallbackContractConfig("defaultSuccessStatus", def.route.successStatus);
447
617
  const description = fallbackContractConfig("defaultSuccessDescription", def.route?.successDescription);
@@ -452,88 +622,124 @@ ${errors.join("\n\n")}`
452
622
  ref.responses[status] = {
453
623
  description,
454
624
  content: toOpenAPIEventIteratorContent(
455
- await this.converter.convert(eventIteratorSchemaDetails.yields, { strategy: "output" }),
456
- await this.converter.convert(eventIteratorSchemaDetails.returns, { strategy: "output" })
625
+ await this.converter.convert(eventIteratorSchemaDetails.yields, { ...baseSchemaConvertOptions, strategy: "output" }),
626
+ await this.converter.convert(eventIteratorSchemaDetails.returns, { ...baseSchemaConvertOptions, strategy: "output" })
457
627
  )
458
628
  };
459
629
  return;
460
630
  }
461
- const [required, json] = await this.converter.convert(outputSchema, { strategy: "output" });
462
- ref.responses ??= {};
463
- ref.responses[status] = {
464
- description
465
- };
631
+ const [required, json] = await this.converter.convert(
632
+ outputSchema,
633
+ {
634
+ ...baseSchemaConvertOptions,
635
+ strategy: "output",
636
+ minStructureDepthForRef: outputStructure === "detailed" ? 1 : 0
637
+ }
638
+ );
466
639
  if (outputStructure === "compact") {
640
+ ref.responses ??= {};
641
+ ref.responses[status] = {
642
+ description
643
+ };
467
644
  ref.responses[status].content = toOpenAPIContent(applySchemaOptionality(required, json));
468
645
  return;
469
646
  }
470
- const error = new OpenAPIGeneratorError(
471
- 'When output structure is "detailed", output schema must satisfy: { headers?: Record<string, unknown>, body?: unknown }'
472
- );
473
- if (!isObjectSchema(json)) {
474
- throw error;
475
- }
476
- if (json.properties?.headers !== void 0) {
477
- if (!isObjectSchema(json.properties.headers)) {
647
+ const handledStatuses = /* @__PURE__ */ new Set();
648
+ for (const item of expandUnionSchema(json)) {
649
+ const error = new OpenAPIGeneratorError(`
650
+ When output structure is "detailed", output schema must satisfy:
651
+ {
652
+ status?: number, // must be a literal number and in the range of 200-399
653
+ headers?: Record<string, unknown>,
654
+ body?: unknown
655
+ }
656
+
657
+ But got: ${stringifyJSON(item)}
658
+ `);
659
+ if (!isObjectSchema(item)) {
478
660
  throw error;
479
661
  }
480
- for (const key in json.properties.headers.properties) {
481
- ref.responses[status].headers ??= {};
482
- ref.responses[status].headers[key] = {
483
- schema: toOpenAPISchema(json.properties.headers.properties[key]),
484
- required: json.properties.headers.required?.includes(key)
485
- };
662
+ let schemaStatus;
663
+ let schemaDescription;
664
+ if (item.properties?.status !== void 0) {
665
+ const statusSchema = resolveOpenAPIJsonSchemaRef(doc, item.properties.status);
666
+ if (typeof statusSchema !== "object" || statusSchema.const === void 0 || typeof statusSchema.const !== "number" || !Number.isInteger(statusSchema.const) || isORPCErrorStatus(statusSchema.const)) {
667
+ throw error;
668
+ }
669
+ schemaStatus = statusSchema.const;
670
+ schemaDescription = statusSchema.description;
671
+ }
672
+ const itemStatus = schemaStatus ?? status;
673
+ const itemDescription = schemaDescription ?? description;
674
+ if (handledStatuses.has(itemStatus)) {
675
+ throw new OpenAPIGeneratorError(`
676
+ When output structure is "detailed", each success status must be unique.
677
+ But got status: ${itemStatus} used more than once.
678
+ `);
679
+ }
680
+ handledStatuses.add(itemStatus);
681
+ ref.responses ??= {};
682
+ ref.responses[itemStatus] = {
683
+ description: itemDescription
684
+ };
685
+ if (item.properties?.headers !== void 0) {
686
+ const headersSchema = resolveOpenAPIJsonSchemaRef(doc, item.properties.headers);
687
+ if (!isObjectSchema(headersSchema)) {
688
+ throw error;
689
+ }
690
+ for (const key in headersSchema.properties) {
691
+ const headerSchema = headersSchema.properties[key];
692
+ if (headerSchema !== void 0) {
693
+ ref.responses[itemStatus].headers ??= {};
694
+ ref.responses[itemStatus].headers[key] = {
695
+ schema: toOpenAPISchema(headerSchema),
696
+ required: item.required?.includes("headers") && headersSchema.required?.includes(key)
697
+ };
698
+ }
699
+ }
700
+ }
701
+ if (item.properties?.body !== void 0) {
702
+ ref.responses[itemStatus].content = toOpenAPIContent(
703
+ applySchemaOptionality(item.required?.includes("body") ?? false, item.properties.body)
704
+ );
486
705
  }
487
- }
488
- if (json.properties?.body !== void 0) {
489
- ref.responses[status].content = toOpenAPIContent(
490
- applySchemaOptionality(json.required?.includes("body") ?? false, json.properties.body)
491
- );
492
706
  }
493
707
  }
494
- async #errorResponse(ref, def) {
708
+ async #errorResponse(ref, def, baseSchemaConvertOptions, undefinedErrorSchema, customErrorResponseBodySchema) {
495
709
  const errorMap = def.errorMap;
496
- const errors = {};
710
+ const errorResponsesByStatus = {};
497
711
  for (const code in errorMap) {
498
712
  const config = errorMap[code];
499
713
  if (!config) {
500
714
  continue;
501
715
  }
502
716
  const status = fallbackORPCErrorStatus(code, config.status);
503
- const message = fallbackORPCErrorMessage(code, config.message);
504
- const [dataRequired, dataSchema] = await this.converter.convert(config.data, { strategy: "output" });
505
- errors[status] ??= [];
506
- errors[status].push({
717
+ const defaultMessage = fallbackORPCErrorMessage(code, config.message);
718
+ errorResponsesByStatus[status] ??= { status, definedErrorDefinitions: [], errorSchemaVariants: [] };
719
+ const [dataRequired, dataSchema] = await this.converter.convert(config.data, { ...baseSchemaConvertOptions, strategy: "output" });
720
+ errorResponsesByStatus[status].definedErrorDefinitions.push([code, defaultMessage, dataRequired, dataSchema]);
721
+ errorResponsesByStatus[status].errorSchemaVariants.push({
507
722
  type: "object",
508
723
  properties: {
509
724
  defined: { const: true },
510
725
  code: { const: code },
511
726
  status: { const: status },
512
- message: { type: "string", default: message },
727
+ message: { type: "string", default: defaultMessage },
513
728
  data: dataSchema
514
729
  },
515
730
  required: dataRequired ? ["defined", "code", "status", "message", "data"] : ["defined", "code", "status", "message"]
516
731
  });
517
732
  }
518
733
  ref.responses ??= {};
519
- for (const status in errors) {
520
- const schemas = errors[status];
521
- ref.responses[status] = {
522
- description: status,
523
- content: toOpenAPIContent({
734
+ for (const statusString in errorResponsesByStatus) {
735
+ const errorResponse = errorResponsesByStatus[statusString];
736
+ const customBodySchema = value(customErrorResponseBodySchema, errorResponse.definedErrorDefinitions, errorResponse.status);
737
+ ref.responses[statusString] = {
738
+ description: statusString,
739
+ content: toOpenAPIContent(customBodySchema ?? {
524
740
  oneOf: [
525
- ...schemas,
526
- {
527
- type: "object",
528
- properties: {
529
- defined: { const: false },
530
- code: { type: "string" },
531
- status: { type: "number" },
532
- message: { type: "string" },
533
- data: {}
534
- },
535
- required: ["defined", "code", "status", "message"]
536
- }
741
+ ...errorResponse.errorSchemaVariants,
742
+ undefinedErrorSchema
537
743
  ]
538
744
  })
539
745
  };
@@ -541,4 +747,4 @@ ${errors.join("\n\n")}`
541
747
  }
542
748
  }
543
749
 
544
- 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, separateObjectSchema as s, toOpenAPIPath as t };
750
+ export { CompositeSchemaConverter as C, LOGIC_KEYWORDS as L, OpenAPIGenerator as O, applyCustomOpenAPIOperation as a, toOpenAPIMethod as b, customOpenAPIOperation as c, toOpenAPIContent as d, toOpenAPIEventIteratorContent as e, toOpenAPIParameters as f, getCustomOpenAPIOperation as g, checkParamsSchema as h, toOpenAPISchema as i, isFileSchema as j, isObjectSchema as k, isAnySchema as l, filterSchemaBranches as m, applySchemaOptionality as n, expandUnionSchema as o, expandArrayableSchema as p, isPrimitiveSchema as q, resolveOpenAPIJsonSchemaRef as r, separateObjectSchema as s, toOpenAPIPath as t };