@orpc/openapi 0.0.0-next.a09e9be → 0.0.0-next.a153125

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.
@@ -0,0 +1,101 @@
1
+ import { AnySchema, OpenAPI, AnyContractProcedure, AnyContractRouter } from '@orpc/contract';
2
+ import { StandardOpenAPIJsonSerializerOptions } from '@orpc/openapi-client/standard';
3
+ import { AnyProcedure, AnyRouter } from '@orpc/server';
4
+ import { Promisable } from '@orpc/shared';
5
+ import { JSONSchema } from 'json-schema-typed/draft-2020-12';
6
+
7
+ interface SchemaConverterComponent {
8
+ allowedStrategies: readonly SchemaConvertOptions['strategy'][];
9
+ schema: AnySchema;
10
+ required: boolean;
11
+ ref: string;
12
+ }
13
+ interface SchemaConvertOptions {
14
+ strategy: 'input' | 'output';
15
+ /**
16
+ * Common components should use `$ref` to represent themselves if matched.
17
+ */
18
+ components?: readonly SchemaConverterComponent[];
19
+ /**
20
+ * Minimum schema structure depth required before using `$ref` for components.
21
+ *
22
+ * For example, if set to 2, `$ref` will only be used for schemas nested at depth 2 or greater.
23
+ *
24
+ * @default 0 - No depth limit;
25
+ */
26
+ minStructureDepthForRef?: number;
27
+ }
28
+ interface SchemaConverter {
29
+ convert(schema: AnySchema | undefined, options: SchemaConvertOptions): Promisable<[required: boolean, jsonSchema: JSONSchema]>;
30
+ }
31
+ interface ConditionalSchemaConverter extends SchemaConverter {
32
+ condition(schema: AnySchema | undefined, options: SchemaConvertOptions): Promisable<boolean>;
33
+ }
34
+ declare class CompositeSchemaConverter implements SchemaConverter {
35
+ private readonly converters;
36
+ constructor(converters: ConditionalSchemaConverter[]);
37
+ convert(schema: AnySchema | undefined, options: SchemaConvertOptions): Promise<[required: boolean, jsonSchema: JSONSchema]>;
38
+ }
39
+
40
+ interface OpenAPIGeneratorOptions extends StandardOpenAPIJsonSerializerOptions {
41
+ schemaConverters?: ConditionalSchemaConverter[];
42
+ }
43
+ interface OpenAPIGeneratorGenerateOptions extends Partial<Omit<OpenAPI.Document, 'openapi'>> {
44
+ /**
45
+ * Exclude procedures from the OpenAPI specification.
46
+ *
47
+ * @default () => false
48
+ */
49
+ exclude?: (procedure: AnyProcedure | AnyContractProcedure, path: readonly string[]) => boolean;
50
+ /**
51
+ * Common schemas to be used for $ref resolution.
52
+ */
53
+ commonSchemas?: Record<string, {
54
+ /**
55
+ * Determines which schema definition to use when input and output schemas differ.
56
+ * This is needed because some schemas transform data differently between input and output,
57
+ * making it impossible to use a single $ref for both cases.
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * // This schema transforms a string input into a number output
62
+ * const Schema = z.string()
63
+ * .transform(v => Number(v))
64
+ * .pipe(z.number())
65
+ *
66
+ * // Input schema: { type: 'string' }
67
+ * // Output schema: { type: 'number' }
68
+ * ```
69
+ *
70
+ * When schemas differ between input and output, you must explicitly choose
71
+ * which version to use for the OpenAPI specification.
72
+ *
73
+ * @default 'input' - Uses the input schema definition by default
74
+ */
75
+ strategy?: SchemaConvertOptions['strategy'];
76
+ schema: AnySchema;
77
+ } | {
78
+ error: 'UndefinedError';
79
+ schema?: never;
80
+ }>;
81
+ }
82
+ /**
83
+ * The generator that converts oRPC routers/contracts to OpenAPI specifications.
84
+ *
85
+ * @see {@link https://orpc.unnoq.com/docs/openapi/openapi-specification OpenAPI Specification Docs}
86
+ */
87
+ declare class OpenAPIGenerator {
88
+ #private;
89
+ private readonly serializer;
90
+ private readonly converter;
91
+ constructor(options?: OpenAPIGeneratorOptions);
92
+ /**
93
+ * Generates OpenAPI specifications from oRPC routers/contracts.
94
+ *
95
+ * @see {@link https://orpc.unnoq.com/docs/openapi/openapi-specification OpenAPI Specification Docs}
96
+ */
97
+ generate(router: AnyContractRouter | AnyRouter, options?: OpenAPIGeneratorGenerateOptions): Promise<OpenAPI.Document>;
98
+ }
99
+
100
+ export { OpenAPIGenerator as b, CompositeSchemaConverter as e };
101
+ export type { ConditionalSchemaConverter as C, OpenAPIGeneratorOptions as O, SchemaConverterComponent as S, OpenAPIGeneratorGenerateOptions as a, SchemaConvertOptions as c, SchemaConverter as d };
@@ -1,7 +1,8 @@
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 } 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';
@@ -52,13 +53,21 @@ class StandardOpenAPICodec {
52
53
  body: this.serializer.serialize(output)
53
54
  };
54
55
  }
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
- );
56
+ if (!this.#isDetailedOutput(output)) {
57
+ throw new Error(`
58
+ Invalid "detailed" output structure:
59
+ \u2022 Expected an object with optional properties:
60
+ - status (number 200-399)
61
+ - headers (Record<string, string | string[]>)
62
+ - body (any)
63
+ \u2022 No extra keys allowed.
64
+
65
+ Actual value:
66
+ ${stringifyJSON(output)}
67
+ `);
59
68
  }
60
69
  return {
61
- status: successStatus,
70
+ status: output.status ?? successStatus,
62
71
  headers: output.headers ?? {},
63
72
  body: this.serializer.serialize(output.body)
64
73
  };
@@ -70,6 +79,18 @@ class StandardOpenAPICodec {
70
79
  body: this.serializer.serialize(error.toJSON(), { outputFormat: "plain" })
71
80
  };
72
81
  }
82
+ #isDetailedOutput(output) {
83
+ if (!isObject(output)) {
84
+ return false;
85
+ }
86
+ if (output.headers && !isObject(output.headers)) {
87
+ return false;
88
+ }
89
+ if (output.status !== void 0 && (typeof output.status !== "number" || !Number.isInteger(output.status) || isORPCErrorStatus(output.status))) {
90
+ return false;
91
+ }
92
+ return true;
93
+ }
73
94
  }
74
95
 
75
96
  function toRou3Pattern(path) {
@@ -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 } from '@orpc/shared';
7
+ import { TypeName } from 'json-schema-typed/draft-2020-12';
8
8
 
9
9
  const OPERATION_EXTENDER_SYMBOL = Symbol("ORPC_OPERATION_EXTENDER");
10
10
  function customOpenAPIOperation(o, extend) {
@@ -184,6 +184,57 @@ function applySchemaOptionality(required, schema) {
184
184
  ]
185
185
  };
186
186
  }
187
+ function expandUnionSchema(schema) {
188
+ if (typeof schema === "object") {
189
+ for (const keyword of ["anyOf", "oneOf"]) {
190
+ if (schema[keyword] && Object.keys(schema).every(
191
+ (k) => k === keyword || !LOGIC_KEYWORDS.includes(k)
192
+ )) {
193
+ return schema[keyword].flatMap((s) => expandUnionSchema(s));
194
+ }
195
+ }
196
+ }
197
+ return [schema];
198
+ }
199
+ function expandArrayableSchema(schema) {
200
+ const schemas = expandUnionSchema(schema);
201
+ if (schemas.length !== 2) {
202
+ return void 0;
203
+ }
204
+ const arraySchema = schemas.find(
205
+ (s) => typeof s === "object" && s.type === "array" && Object.keys(s).filter((k) => LOGIC_KEYWORDS.includes(k)).every((k) => k === "type" || k === "items")
206
+ );
207
+ if (arraySchema === void 0) {
208
+ return void 0;
209
+ }
210
+ const items1 = arraySchema.items;
211
+ const items2 = schemas.find((s) => s !== arraySchema);
212
+ if (stringifyJSON(items1) !== stringifyJSON(items2)) {
213
+ return void 0;
214
+ }
215
+ return [items2, arraySchema];
216
+ }
217
+ const PRIMITIVE_SCHEMA_TYPES = /* @__PURE__ */ new Set([
218
+ TypeName.String,
219
+ TypeName.Number,
220
+ TypeName.Integer,
221
+ TypeName.Boolean,
222
+ TypeName.Null
223
+ ]);
224
+ function isPrimitiveSchema(schema) {
225
+ return expandUnionSchema(schema).every((s) => {
226
+ if (typeof s === "boolean") {
227
+ return false;
228
+ }
229
+ if (typeof s.type === "string" && PRIMITIVE_SCHEMA_TYPES.has(s.type)) {
230
+ return true;
231
+ }
232
+ if (s.const !== void 0) {
233
+ return true;
234
+ }
235
+ return false;
236
+ });
237
+ }
187
238
 
188
239
  function toOpenAPIPath(path) {
189
240
  return standardizeHTTPPath(path).replace(/\/\{\+([^}]+)\}/g, "/{$1}");
@@ -256,13 +307,26 @@ function toOpenAPIParameters(schema, parameterIn) {
256
307
  const parameters = [];
257
308
  for (const key in schema.properties) {
258
309
  const keySchema = schema.properties[key];
310
+ let isDeepObjectStyle = true;
311
+ if (parameterIn !== "query") {
312
+ isDeepObjectStyle = false;
313
+ } else if (isPrimitiveSchema(keySchema)) {
314
+ isDeepObjectStyle = false;
315
+ } else {
316
+ const [item] = expandArrayableSchema(keySchema) ?? [];
317
+ if (item !== void 0 && isPrimitiveSchema(item)) {
318
+ isDeepObjectStyle = false;
319
+ }
320
+ }
259
321
  parameters.push({
260
322
  name: key,
261
323
  in: parameterIn,
262
324
  required: schema.required?.includes(key),
263
- style: parameterIn === "query" ? "deepObject" : void 0,
264
- explode: parameterIn === "query" ? true : void 0,
265
- schema: toOpenAPISchema(keySchema)
325
+ schema: toOpenAPISchema(keySchema),
326
+ style: isDeepObjectStyle ? "deepObject" : void 0,
327
+ explode: isDeepObjectStyle ? true : void 0,
328
+ allowEmptyValue: parameterIn === "query" ? true : void 0,
329
+ allowReserved: parameterIn === "query" ? true : void 0
266
330
  });
267
331
  }
268
332
  return parameters;
@@ -281,6 +345,15 @@ function checkParamsSchema(schema, params) {
281
345
  function toOpenAPISchema(schema) {
282
346
  return schema === true ? {} : schema === false ? { not: {} } : schema;
283
347
  }
348
+ const OPENAPI_JSON_SCHEMA_REF_PREFIX = "#/components/schemas/";
349
+ function resolveOpenAPIJsonSchemaRef(doc, schema) {
350
+ if (typeof schema !== "object" || !schema.$ref?.startsWith(OPENAPI_JSON_SCHEMA_REF_PREFIX)) {
351
+ return schema;
352
+ }
353
+ const name = schema.$ref.slice(OPENAPI_JSON_SCHEMA_REF_PREFIX.length);
354
+ const resolved = doc.components?.schemas?.[name];
355
+ return resolved ?? schema;
356
+ }
284
357
 
285
358
  class CompositeSchemaConverter {
286
359
  converters;
@@ -312,14 +385,20 @@ class OpenAPIGenerator {
312
385
  * @see {@link https://orpc.unnoq.com/docs/openapi/openapi-specification OpenAPI Specification Docs}
313
386
  */
314
387
  async generate(router, options = {}) {
388
+ const exclude = options.exclude ?? (() => false);
315
389
  const doc = {
316
390
  ...clone(options),
317
391
  info: options.info ?? { title: "API Reference", version: "0.0.0" },
318
- openapi: "3.1.1"
392
+ openapi: "3.1.1",
393
+ exclude: void 0,
394
+ commonSchemas: void 0
319
395
  };
396
+ const { baseSchemaConvertOptions, undefinedErrorJsonSchema } = await this.#resolveCommonSchemas(doc, options.commonSchemas);
320
397
  const contracts = [];
321
398
  await resolveContractProcedures({ path: [], router }, ({ contract, path }) => {
322
- contracts.push({ contract, path });
399
+ if (!exclude(contract, path)) {
400
+ contracts.push({ contract, path });
401
+ }
323
402
  });
324
403
  const errors = [];
325
404
  for (const { contract, path } of contracts) {
@@ -328,16 +407,21 @@ class OpenAPIGenerator {
328
407
  const def = contract["~orpc"];
329
408
  const method = toOpenAPIMethod(fallbackContractConfig("defaultMethod", def.route.method));
330
409
  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);
410
+ let operationObjectRef;
411
+ if (def.route.spec !== void 0) {
412
+ operationObjectRef = def.route.spec;
413
+ } else {
414
+ operationObjectRef = {
415
+ operationId,
416
+ summary: def.route.summary,
417
+ description: def.route.description,
418
+ deprecated: def.route.deprecated,
419
+ tags: def.route.tags?.map((tag) => tag)
420
+ };
421
+ await this.#request(doc, operationObjectRef, def, baseSchemaConvertOptions);
422
+ await this.#successResponse(doc, operationObjectRef, def, baseSchemaConvertOptions);
423
+ await this.#errorResponse(operationObjectRef, def, baseSchemaConvertOptions, undefinedErrorJsonSchema);
424
+ }
341
425
  doc.paths ??= {};
342
426
  doc.paths[httpPath] ??= {};
343
427
  doc.paths[httpPath][method] = applyCustomOpenAPIOperation(operationObjectRef, contract);
@@ -360,22 +444,96 @@ ${errors.join("\n\n")}`
360
444
  }
361
445
  return this.serializer.serialize(doc)[0];
362
446
  }
363
- async #request(ref, def) {
447
+ async #resolveCommonSchemas(doc, commonSchemas) {
448
+ let undefinedErrorJsonSchema = {
449
+ type: "object",
450
+ properties: {
451
+ defined: { const: false },
452
+ code: { type: "string" },
453
+ status: { type: "number" },
454
+ message: { type: "string" },
455
+ data: {}
456
+ },
457
+ required: ["defined", "code", "status", "message"]
458
+ };
459
+ const baseSchemaConvertOptions = {};
460
+ if (commonSchemas) {
461
+ baseSchemaConvertOptions.components = [];
462
+ for (const key in commonSchemas) {
463
+ const options = commonSchemas[key];
464
+ if (options.schema === void 0) {
465
+ continue;
466
+ }
467
+ const { schema, strategy = "input" } = options;
468
+ const [required, json] = await this.converter.convert(schema, { strategy });
469
+ const allowedStrategies = [strategy];
470
+ if (strategy === "input") {
471
+ const [outputRequired, outputJson] = await this.converter.convert(schema, { strategy: "output" });
472
+ if (outputRequired === required && stringifyJSON(outputJson) === stringifyJSON(json)) {
473
+ allowedStrategies.push("output");
474
+ }
475
+ } else if (strategy === "output") {
476
+ const [inputRequired, inputJson] = await this.converter.convert(schema, { strategy: "input" });
477
+ if (inputRequired === required && stringifyJSON(inputJson) === stringifyJSON(json)) {
478
+ allowedStrategies.push("input");
479
+ }
480
+ }
481
+ baseSchemaConvertOptions.components.push({
482
+ schema,
483
+ required,
484
+ ref: `#/components/schemas/${key}`,
485
+ allowedStrategies
486
+ });
487
+ }
488
+ doc.components ??= {};
489
+ doc.components.schemas ??= {};
490
+ for (const key in commonSchemas) {
491
+ const options = commonSchemas[key];
492
+ if (options.schema === void 0) {
493
+ if (options.error === "UndefinedError") {
494
+ doc.components.schemas[key] = toOpenAPISchema(undefinedErrorJsonSchema);
495
+ undefinedErrorJsonSchema = { $ref: `#/components/schemas/${key}` };
496
+ }
497
+ continue;
498
+ }
499
+ const { schema, strategy = "input" } = options;
500
+ const [, json] = await this.converter.convert(
501
+ schema,
502
+ {
503
+ ...baseSchemaConvertOptions,
504
+ strategy,
505
+ minStructureDepthForRef: 1
506
+ // not allow use $ref for root schemas
507
+ }
508
+ );
509
+ doc.components.schemas[key] = toOpenAPISchema(json);
510
+ }
511
+ }
512
+ return { baseSchemaConvertOptions, undefinedErrorJsonSchema };
513
+ }
514
+ async #request(doc, ref, def, baseSchemaConvertOptions) {
364
515
  const method = fallbackContractConfig("defaultMethod", def.route.method);
365
516
  const details = getEventIteratorSchemaDetails(def.inputSchema);
366
517
  if (details) {
367
518
  ref.requestBody = {
368
519
  required: true,
369
520
  content: toOpenAPIEventIteratorContent(
370
- await this.converter.convert(details.yields, { strategy: "input" }),
371
- await this.converter.convert(details.returns, { strategy: "input" })
521
+ await this.converter.convert(details.yields, { ...baseSchemaConvertOptions, strategy: "input" }),
522
+ await this.converter.convert(details.returns, { ...baseSchemaConvertOptions, strategy: "input" })
372
523
  )
373
524
  };
374
525
  return;
375
526
  }
376
527
  const dynamicParams = getDynamicParams(def.route.path)?.map((v) => v.name);
377
528
  const inputStructure = fallbackContractConfig("defaultInputStructure", def.route.inputStructure);
378
- let [required, schema] = await this.converter.convert(def.inputSchema, { strategy: "input" });
529
+ let [required, schema] = await this.converter.convert(
530
+ def.inputSchema,
531
+ {
532
+ ...baseSchemaConvertOptions,
533
+ strategy: "input",
534
+ minStructureDepthForRef: dynamicParams?.length || inputStructure === "detailed" ? 1 : 0
535
+ }
536
+ );
379
537
  if (isAnySchema(schema) && !dynamicParams?.length) {
380
538
  return;
381
539
  }
@@ -418,7 +576,8 @@ ${errors.join("\n\n")}`
418
576
  if (!isObjectSchema(schema)) {
419
577
  throw error;
420
578
  }
421
- if (dynamicParams?.length && (schema.properties?.params === void 0 || !isObjectSchema(schema.properties.params) || !checkParamsSchema(schema.properties.params, dynamicParams))) {
579
+ const resolvedParamSchema = schema.properties?.params !== void 0 ? resolveOpenAPIJsonSchemaRef(doc, schema.properties.params) : void 0;
580
+ if (dynamicParams?.length && (resolvedParamSchema === void 0 || !isObjectSchema(resolvedParamSchema) || !checkParamsSchema(resolvedParamSchema, dynamicParams))) {
422
581
  throw new OpenAPIGeneratorError(
423
582
  'When input structure is "detailed" and path has dynamic params, the "params" schema must be an object with all dynamic params as required.'
424
583
  );
@@ -426,12 +585,13 @@ ${errors.join("\n\n")}`
426
585
  for (const from of ["params", "query", "headers"]) {
427
586
  const fromSchema = schema.properties?.[from];
428
587
  if (fromSchema !== void 0) {
429
- if (!isObjectSchema(fromSchema)) {
588
+ const resolvedSchema = resolveOpenAPIJsonSchemaRef(doc, fromSchema);
589
+ if (!isObjectSchema(resolvedSchema)) {
430
590
  throw error;
431
591
  }
432
592
  const parameterIn = from === "params" ? "path" : from === "headers" ? "header" : "query";
433
593
  ref.parameters ??= [];
434
- ref.parameters.push(...toOpenAPIParameters(fromSchema, parameterIn));
594
+ ref.parameters.push(...toOpenAPIParameters(resolvedSchema, parameterIn));
435
595
  }
436
596
  }
437
597
  if (schema.properties?.body !== void 0) {
@@ -441,7 +601,7 @@ ${errors.join("\n\n")}`
441
601
  };
442
602
  }
443
603
  }
444
- async #successResponse(ref, def) {
604
+ async #successResponse(doc, ref, def, baseSchemaConvertOptions) {
445
605
  const outputSchema = def.outputSchema;
446
606
  const status = fallbackContractConfig("defaultSuccessStatus", def.route.successStatus);
447
607
  const description = fallbackContractConfig("defaultSuccessDescription", def.route?.successDescription);
@@ -452,46 +612,90 @@ ${errors.join("\n\n")}`
452
612
  ref.responses[status] = {
453
613
  description,
454
614
  content: toOpenAPIEventIteratorContent(
455
- await this.converter.convert(eventIteratorSchemaDetails.yields, { strategy: "output" }),
456
- await this.converter.convert(eventIteratorSchemaDetails.returns, { strategy: "output" })
615
+ await this.converter.convert(eventIteratorSchemaDetails.yields, { ...baseSchemaConvertOptions, strategy: "output" }),
616
+ await this.converter.convert(eventIteratorSchemaDetails.returns, { ...baseSchemaConvertOptions, strategy: "output" })
457
617
  )
458
618
  };
459
619
  return;
460
620
  }
461
- const [required, json] = await this.converter.convert(outputSchema, { strategy: "output" });
462
- ref.responses ??= {};
463
- ref.responses[status] = {
464
- description
465
- };
621
+ const [required, json] = await this.converter.convert(
622
+ outputSchema,
623
+ {
624
+ ...baseSchemaConvertOptions,
625
+ strategy: "output",
626
+ minStructureDepthForRef: outputStructure === "detailed" ? 1 : 0
627
+ }
628
+ );
466
629
  if (outputStructure === "compact") {
630
+ ref.responses ??= {};
631
+ ref.responses[status] = {
632
+ description
633
+ };
467
634
  ref.responses[status].content = toOpenAPIContent(applySchemaOptionality(required, json));
468
635
  return;
469
636
  }
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)) {
637
+ const handledStatuses = /* @__PURE__ */ new Set();
638
+ for (const item of expandUnionSchema(json)) {
639
+ const error = new OpenAPIGeneratorError(`
640
+ When output structure is "detailed", output schema must satisfy:
641
+ {
642
+ status?: number, // must be a literal number and in the range of 200-399
643
+ headers?: Record<string, unknown>,
644
+ body?: unknown
645
+ }
646
+
647
+ But got: ${stringifyJSON(item)}
648
+ `);
649
+ if (!isObjectSchema(item)) {
478
650
  throw error;
479
651
  }
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
- };
652
+ let schemaStatus;
653
+ let schemaDescription;
654
+ if (item.properties?.status !== void 0) {
655
+ const statusSchema = resolveOpenAPIJsonSchemaRef(doc, item.properties.status);
656
+ if (typeof statusSchema !== "object" || statusSchema.const === void 0 || typeof statusSchema.const !== "number" || !Number.isInteger(statusSchema.const) || isORPCErrorStatus(statusSchema.const)) {
657
+ throw error;
658
+ }
659
+ schemaStatus = statusSchema.const;
660
+ schemaDescription = statusSchema.description;
661
+ }
662
+ const itemStatus = schemaStatus ?? status;
663
+ const itemDescription = schemaDescription ?? description;
664
+ if (handledStatuses.has(itemStatus)) {
665
+ throw new OpenAPIGeneratorError(`
666
+ When output structure is "detailed", each success status must be unique.
667
+ But got status: ${itemStatus} used more than once.
668
+ `);
669
+ }
670
+ handledStatuses.add(itemStatus);
671
+ ref.responses ??= {};
672
+ ref.responses[itemStatus] = {
673
+ description: itemDescription
674
+ };
675
+ if (item.properties?.headers !== void 0) {
676
+ const headersSchema = resolveOpenAPIJsonSchemaRef(doc, item.properties.headers);
677
+ if (!isObjectSchema(headersSchema)) {
678
+ throw error;
679
+ }
680
+ for (const key in headersSchema.properties) {
681
+ const headerSchema = headersSchema.properties[key];
682
+ if (headerSchema !== void 0) {
683
+ ref.responses[itemStatus].headers ??= {};
684
+ ref.responses[itemStatus].headers[key] = {
685
+ schema: toOpenAPISchema(headerSchema),
686
+ required: item.required?.includes("headers") && headersSchema.required?.includes(key)
687
+ };
688
+ }
689
+ }
690
+ }
691
+ if (item.properties?.body !== void 0) {
692
+ ref.responses[itemStatus].content = toOpenAPIContent(
693
+ applySchemaOptionality(item.required?.includes("body") ?? false, item.properties.body)
694
+ );
486
695
  }
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
696
  }
493
697
  }
494
- async #errorResponse(ref, def) {
698
+ async #errorResponse(ref, def, baseSchemaConvertOptions, undefinedErrorSchema) {
495
699
  const errorMap = def.errorMap;
496
700
  const errors = {};
497
701
  for (const code in errorMap) {
@@ -501,7 +705,7 @@ ${errors.join("\n\n")}`
501
705
  }
502
706
  const status = fallbackORPCErrorStatus(code, config.status);
503
707
  const message = fallbackORPCErrorMessage(code, config.message);
504
- const [dataRequired, dataSchema] = await this.converter.convert(config.data, { strategy: "output" });
708
+ const [dataRequired, dataSchema] = await this.converter.convert(config.data, { ...baseSchemaConvertOptions, strategy: "output" });
505
709
  errors[status] ??= [];
506
710
  errors[status].push({
507
711
  type: "object",
@@ -523,17 +727,7 @@ ${errors.join("\n\n")}`
523
727
  content: toOpenAPIContent({
524
728
  oneOf: [
525
729
  ...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
- }
730
+ undefinedErrorSchema
537
731
  ]
538
732
  })
539
733
  };
@@ -541,4 +735,4 @@ ${errors.join("\n\n")}`
541
735
  }
542
736
  }
543
737
 
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 };
738
+ 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 };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@orpc/openapi",
3
3
  "type": "module",
4
- "version": "0.0.0-next.a09e9be",
4
+ "version": "0.0.0-next.a153125",
5
5
  "license": "MIT",
6
6
  "homepage": "https://orpc.unnoq.com",
7
7
  "repository": {
@@ -38,6 +38,11 @@
38
38
  "types": "./dist/adapters/node/index.d.mts",
39
39
  "import": "./dist/adapters/node/index.mjs",
40
40
  "default": "./dist/adapters/node/index.mjs"
41
+ },
42
+ "./aws-lambda": {
43
+ "types": "./dist/adapters/aws-lambda/index.d.mts",
44
+ "import": "./dist/adapters/aws-lambda/index.mjs",
45
+ "default": "./dist/adapters/aws-lambda/index.mjs"
41
46
  }
42
47
  },
43
48
  "files": [
@@ -45,17 +50,16 @@
45
50
  ],
46
51
  "dependencies": {
47
52
  "json-schema-typed": "^8.0.1",
48
- "openapi-types": "^12.1.3",
49
- "rou3": "^0.6.0",
50
- "@orpc/client": "0.0.0-next.a09e9be",
51
- "@orpc/contract": "0.0.0-next.a09e9be",
52
- "@orpc/openapi-client": "0.0.0-next.a09e9be",
53
- "@orpc/standard-server": "0.0.0-next.a09e9be",
54
- "@orpc/shared": "0.0.0-next.a09e9be",
55
- "@orpc/server": "0.0.0-next.a09e9be"
53
+ "rou3": "^0.7.2",
54
+ "@orpc/client": "0.0.0-next.a153125",
55
+ "@orpc/openapi-client": "0.0.0-next.a153125",
56
+ "@orpc/server": "0.0.0-next.a153125",
57
+ "@orpc/shared": "0.0.0-next.a153125",
58
+ "@orpc/standard-server": "0.0.0-next.a153125",
59
+ "@orpc/contract": "0.0.0-next.a153125"
56
60
  },
57
61
  "devDependencies": {
58
- "zod": "^3.24.2"
62
+ "zod": "^3.25.67"
59
63
  },
60
64
  "scripts": {
61
65
  "build": "unbuild",