@orpc/openapi 0.0.0-next.fa8d145 → 0.0.0-next.fb5a52a

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +11 -8
  2. package/dist/adapters/aws-lambda/index.d.mts +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 +9 -3
  9. package/dist/adapters/fetch/index.d.ts +9 -3
  10. package/dist/adapters/fetch/index.mjs +1 -1
  11. package/dist/adapters/node/index.d.mts +9 -3
  12. package/dist/adapters/node/index.d.ts +9 -3
  13. package/dist/adapters/node/index.mjs +1 -1
  14. package/dist/adapters/standard/index.d.mts +8 -23
  15. package/dist/adapters/standard/index.d.ts +8 -23
  16. package/dist/adapters/standard/index.mjs +1 -1
  17. package/dist/index.d.mts +24 -13
  18. package/dist/index.d.ts +24 -13
  19. package/dist/index.mjs +3 -3
  20. package/dist/plugins/index.d.mts +26 -12
  21. package/dist/plugins/index.d.ts +26 -12
  22. package/dist/plugins/index.mjs +59 -19
  23. package/dist/shared/openapi.BfNjg7j9.d.mts +120 -0
  24. package/dist/shared/openapi.BfNjg7j9.d.ts +120 -0
  25. package/dist/shared/{openapi.PDTdnRIU.mjs → openapi.CzHcOMxv.mjs} +335 -77
  26. package/dist/shared/{openapi.C_UtQ8Us.mjs → openapi.DIt-Z9W1.mjs} +19 -8
  27. package/dist/shared/openapi.DwaweYRb.d.mts +54 -0
  28. package/dist/shared/openapi.DwaweYRb.d.ts +54 -0
  29. package/package.json +21 -11
  30. package/dist/shared/openapi.CwdCLgSU.d.mts +0 -53
  31. package/dist/shared/openapi.CwdCLgSU.d.ts +0 -53
  32. package/dist/shared/openapi.D3j94c9n.d.mts +0 -12
  33. package/dist/shared/openapi.D3j94c9n.d.ts +0 -12
@@ -3,8 +3,8 @@ import { toHttpPath } from '@orpc/client/standard';
3
3
  import { fallbackContractConfig, getEventIteratorSchemaDetails } from '@orpc/contract';
4
4
  import { standardizeHTTPPath, StandardOpenAPIJsonSerializer, getDynamicParams } from '@orpc/openapi-client/standard';
5
5
  import { isProcedure, resolveContractProcedures } from '@orpc/server';
6
- import { isObject, findDeepMatches, toArray, clone, stringifyJSON } 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));
@@ -196,6 +201,45 @@ function expandUnionSchema(schema) {
196
201
  }
197
202
  return [schema];
198
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
+ }
199
243
 
200
244
  function toOpenAPIPath(path) {
201
245
  return standardizeHTTPPath(path).replace(/\/\{\+([^}]+)\}/g, "/{$1}");
@@ -268,13 +312,26 @@ function toOpenAPIParameters(schema, parameterIn) {
268
312
  const parameters = [];
269
313
  for (const key in schema.properties) {
270
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
+ }
271
326
  parameters.push({
272
327
  name: key,
273
328
  in: parameterIn,
274
329
  required: schema.required?.includes(key),
275
- style: parameterIn === "query" ? "deepObject" : void 0,
276
- explode: parameterIn === "query" ? true : void 0,
277
- 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
278
335
  });
279
336
  }
280
337
  return parameters;
@@ -293,6 +350,116 @@ function checkParamsSchema(schema, params) {
293
350
  function toOpenAPISchema(schema) {
294
351
  return schema === true ? {} : schema === false ? { not: {} } : schema;
295
352
  }
353
+ const OPENAPI_JSON_SCHEMA_REF_PREFIX = "#/components/schemas/";
354
+ function resolveOpenAPIJsonSchemaRef(doc, schema) {
355
+ if (typeof schema !== "object" || !schema.$ref?.startsWith(OPENAPI_JSON_SCHEMA_REF_PREFIX)) {
356
+ return schema;
357
+ }
358
+ const name = schema.$ref.slice(OPENAPI_JSON_SCHEMA_REF_PREFIX.length);
359
+ const resolved = doc.components?.schemas?.[name];
360
+ return resolved ?? schema;
361
+ }
362
+ function simplifyComposedObjectJsonSchemasAndRefs(schema, doc) {
363
+ if (doc) {
364
+ schema = resolveOpenAPIJsonSchemaRef(doc, schema);
365
+ }
366
+ if (typeof schema !== "object" || !schema.anyOf && !schema.oneOf && !schema.allOf) {
367
+ return schema;
368
+ }
369
+ const unionSchemas = [
370
+ ...toArray(schema.anyOf?.map((s) => simplifyComposedObjectJsonSchemasAndRefs(s, doc))),
371
+ ...toArray(schema.oneOf?.map((s) => simplifyComposedObjectJsonSchemasAndRefs(s, doc)))
372
+ ];
373
+ const objectUnionSchemas = [];
374
+ for (const u of unionSchemas) {
375
+ if (!isObjectSchema(u)) {
376
+ return schema;
377
+ }
378
+ objectUnionSchemas.push(u);
379
+ }
380
+ const mergedUnionPropertyMap = /* @__PURE__ */ new Map();
381
+ for (const u of objectUnionSchemas) {
382
+ if (u.properties) {
383
+ for (const [key, value] of Object.entries(u.properties)) {
384
+ let entry = mergedUnionPropertyMap.get(key);
385
+ if (!entry) {
386
+ const required = objectUnionSchemas.every((s) => s.required?.includes(key));
387
+ entry = { required, schemas: [] };
388
+ mergedUnionPropertyMap.set(key, entry);
389
+ }
390
+ entry.schemas.push(value);
391
+ }
392
+ }
393
+ }
394
+ const intersectionSchemas = toArray(schema.allOf?.map((s) => simplifyComposedObjectJsonSchemasAndRefs(s, doc)));
395
+ const objectIntersectionSchemas = [];
396
+ for (const u of intersectionSchemas) {
397
+ if (!isObjectSchema(u)) {
398
+ return schema;
399
+ }
400
+ objectIntersectionSchemas.push(u);
401
+ }
402
+ if (isObjectSchema(schema)) {
403
+ objectIntersectionSchemas.push(schema);
404
+ }
405
+ const mergedInteractionPropertyMap = /* @__PURE__ */ new Map();
406
+ for (const u of objectIntersectionSchemas) {
407
+ if (u.properties) {
408
+ for (const [key, value] of Object.entries(u.properties)) {
409
+ let entry = mergedInteractionPropertyMap.get(key);
410
+ if (!entry) {
411
+ const required = objectIntersectionSchemas.some((s) => s.required?.includes(key));
412
+ entry = { required, schemas: [] };
413
+ mergedInteractionPropertyMap.set(key, entry);
414
+ }
415
+ entry.schemas.push(value);
416
+ }
417
+ }
418
+ }
419
+ const resultObjectSchema = { type: "object", properties: {}, required: [] };
420
+ const keys = /* @__PURE__ */ new Set([
421
+ ...mergedUnionPropertyMap.keys(),
422
+ ...mergedInteractionPropertyMap.keys()
423
+ ]);
424
+ if (keys.size === 0) {
425
+ return schema;
426
+ }
427
+ const deduplicateSchemas = (schemas) => {
428
+ const seen = /* @__PURE__ */ new Set();
429
+ const result = [];
430
+ for (const schema2 of schemas) {
431
+ const key = stringifyJSON(schema2);
432
+ if (!seen.has(key)) {
433
+ seen.add(key);
434
+ result.push(schema2);
435
+ }
436
+ }
437
+ return result;
438
+ };
439
+ for (const key of keys) {
440
+ const unionEntry = mergedUnionPropertyMap.get(key);
441
+ const intersectionEntry = mergedInteractionPropertyMap.get(key);
442
+ resultObjectSchema.properties[key] = (() => {
443
+ const dedupedUnionSchemas = unionEntry ? deduplicateSchemas(unionEntry.schemas) : [];
444
+ const dedupedIntersectionSchemas = intersectionEntry ? deduplicateSchemas(intersectionEntry.schemas) : [];
445
+ if (!dedupedUnionSchemas.length) {
446
+ return dedupedIntersectionSchemas.length === 1 ? dedupedIntersectionSchemas[0] : { allOf: dedupedIntersectionSchemas };
447
+ }
448
+ if (!dedupedIntersectionSchemas.length) {
449
+ return dedupedUnionSchemas.length === 1 ? dedupedUnionSchemas[0] : { anyOf: dedupedUnionSchemas };
450
+ }
451
+ const allOf = deduplicateSchemas([
452
+ ...dedupedIntersectionSchemas,
453
+ dedupedUnionSchemas.length === 1 ? dedupedUnionSchemas[0] : { anyOf: dedupedUnionSchemas }
454
+ ]);
455
+ return allOf.length === 1 ? allOf[0] : { allOf };
456
+ })();
457
+ if (unionEntry?.required || intersectionEntry?.required) {
458
+ resultObjectSchema.required.push(key);
459
+ }
460
+ }
461
+ return resultObjectSchema;
462
+ }
296
463
 
297
464
  class CompositeSchemaConverter {
298
465
  converters;
@@ -323,37 +490,48 @@ class OpenAPIGenerator {
323
490
  *
324
491
  * @see {@link https://orpc.unnoq.com/docs/openapi/openapi-specification OpenAPI Specification Docs}
325
492
  */
326
- async generate(router, options = {}) {
327
- const exclude = options.exclude ?? (() => false);
493
+ async generate(router, { customErrorResponseBodySchema, commonSchemas, filter: baseFilter, exclude, ...baseDoc } = {}) {
494
+ const filter = baseFilter ?? (({ contract, path }) => {
495
+ return !(exclude?.(contract, path) ?? false);
496
+ });
328
497
  const doc = {
329
- ...clone(options),
330
- info: options.info ?? { title: "API Reference", version: "0.0.0" },
331
- openapi: "3.1.1",
332
- exclude: void 0
498
+ ...clone(baseDoc),
499
+ info: baseDoc.info ?? { title: "API Reference", version: "0.0.0" },
500
+ openapi: "3.1.1"
333
501
  };
502
+ const { baseSchemaConvertOptions, undefinedErrorJsonSchema } = await this.#resolveCommonSchemas(doc, commonSchemas);
334
503
  const contracts = [];
335
- await resolveContractProcedures({ path: [], router }, ({ contract, path }) => {
336
- if (!exclude(contract, path)) {
337
- contracts.push({ contract, path });
504
+ await resolveContractProcedures({ path: [], router }, (traverseOptions) => {
505
+ if (!value(filter, traverseOptions)) {
506
+ return;
338
507
  }
508
+ contracts.push(traverseOptions);
339
509
  });
340
510
  const errors = [];
341
511
  for (const { contract, path } of contracts) {
342
- const operationId = path.join(".");
512
+ const stringPath = path.join(".");
343
513
  try {
344
514
  const def = contract["~orpc"];
345
515
  const method = toOpenAPIMethod(fallbackContractConfig("defaultMethod", def.route.method));
346
516
  const httpPath = toOpenAPIPath(def.route.path ?? toHttpPath(path));
347
- const operationObjectRef = {
348
- operationId,
349
- summary: def.route.summary,
350
- description: def.route.description,
351
- deprecated: def.route.deprecated,
352
- tags: def.route.tags?.map((tag) => tag)
353
- };
354
- await this.#request(operationObjectRef, def);
355
- await this.#successResponse(operationObjectRef, def);
356
- await this.#errorResponse(operationObjectRef, def);
517
+ let operationObjectRef;
518
+ if (def.route.spec !== void 0 && typeof def.route.spec !== "function") {
519
+ operationObjectRef = def.route.spec;
520
+ } else {
521
+ operationObjectRef = {
522
+ operationId: def.route.operationId ?? stringPath,
523
+ summary: def.route.summary,
524
+ description: def.route.description,
525
+ deprecated: def.route.deprecated,
526
+ tags: def.route.tags?.map((tag) => tag)
527
+ };
528
+ await this.#request(doc, operationObjectRef, def, baseSchemaConvertOptions);
529
+ await this.#successResponse(doc, operationObjectRef, def, baseSchemaConvertOptions);
530
+ await this.#errorResponse(operationObjectRef, def, baseSchemaConvertOptions, undefinedErrorJsonSchema, customErrorResponseBodySchema);
531
+ }
532
+ if (typeof def.route.spec === "function") {
533
+ operationObjectRef = def.route.spec(operationObjectRef);
534
+ }
357
535
  doc.paths ??= {};
358
536
  doc.paths[httpPath] ??= {};
359
537
  doc.paths[httpPath][method] = applyCustomOpenAPIOperation(operationObjectRef, contract);
@@ -362,7 +540,7 @@ class OpenAPIGenerator {
362
540
  throw e;
363
541
  }
364
542
  errors.push(
365
- `[OpenAPIGenerator] Error occurred while generating OpenAPI for procedure at path: ${operationId}
543
+ `[OpenAPIGenerator] Error occurred while generating OpenAPI for procedure at path: ${stringPath}
366
544
  ${e.message}`
367
545
  );
368
546
  }
@@ -376,25 +554,101 @@ ${errors.join("\n\n")}`
376
554
  }
377
555
  return this.serializer.serialize(doc)[0];
378
556
  }
379
- async #request(ref, def) {
557
+ async #resolveCommonSchemas(doc, commonSchemas) {
558
+ let undefinedErrorJsonSchema = {
559
+ type: "object",
560
+ properties: {
561
+ defined: { const: false },
562
+ code: { type: "string" },
563
+ status: { type: "number" },
564
+ message: { type: "string" },
565
+ data: {}
566
+ },
567
+ required: ["defined", "code", "status", "message"]
568
+ };
569
+ const baseSchemaConvertOptions = {};
570
+ if (commonSchemas) {
571
+ baseSchemaConvertOptions.components = [];
572
+ for (const key in commonSchemas) {
573
+ const options = commonSchemas[key];
574
+ if (options.schema === void 0) {
575
+ continue;
576
+ }
577
+ const { schema, strategy = "input" } = options;
578
+ const [required, json] = await this.converter.convert(schema, { strategy });
579
+ const allowedStrategies = [strategy];
580
+ if (strategy === "input") {
581
+ const [outputRequired, outputJson] = await this.converter.convert(schema, { strategy: "output" });
582
+ if (outputRequired === required && stringifyJSON(outputJson) === stringifyJSON(json)) {
583
+ allowedStrategies.push("output");
584
+ }
585
+ } else if (strategy === "output") {
586
+ const [inputRequired, inputJson] = await this.converter.convert(schema, { strategy: "input" });
587
+ if (inputRequired === required && stringifyJSON(inputJson) === stringifyJSON(json)) {
588
+ allowedStrategies.push("input");
589
+ }
590
+ }
591
+ baseSchemaConvertOptions.components.push({
592
+ schema,
593
+ required,
594
+ ref: `#/components/schemas/${key}`,
595
+ allowedStrategies
596
+ });
597
+ }
598
+ doc.components ??= {};
599
+ doc.components.schemas ??= {};
600
+ for (const key in commonSchemas) {
601
+ const options = commonSchemas[key];
602
+ if (options.schema === void 0) {
603
+ if (options.error === "UndefinedError") {
604
+ doc.components.schemas[key] = toOpenAPISchema(undefinedErrorJsonSchema);
605
+ undefinedErrorJsonSchema = { $ref: `#/components/schemas/${key}` };
606
+ }
607
+ continue;
608
+ }
609
+ const { schema, strategy = "input" } = options;
610
+ const [, json] = await this.converter.convert(
611
+ schema,
612
+ {
613
+ ...baseSchemaConvertOptions,
614
+ strategy,
615
+ minStructureDepthForRef: 1
616
+ // not allow use $ref for root schemas
617
+ }
618
+ );
619
+ doc.components.schemas[key] = toOpenAPISchema(json);
620
+ }
621
+ }
622
+ return { baseSchemaConvertOptions, undefinedErrorJsonSchema };
623
+ }
624
+ async #request(doc, ref, def, baseSchemaConvertOptions) {
380
625
  const method = fallbackContractConfig("defaultMethod", def.route.method);
381
626
  const details = getEventIteratorSchemaDetails(def.inputSchema);
382
627
  if (details) {
383
628
  ref.requestBody = {
384
629
  required: true,
385
630
  content: toOpenAPIEventIteratorContent(
386
- await this.converter.convert(details.yields, { strategy: "input" }),
387
- await this.converter.convert(details.returns, { strategy: "input" })
631
+ await this.converter.convert(details.yields, { ...baseSchemaConvertOptions, strategy: "input" }),
632
+ await this.converter.convert(details.returns, { ...baseSchemaConvertOptions, strategy: "input" })
388
633
  )
389
634
  };
390
635
  return;
391
636
  }
392
637
  const dynamicParams = getDynamicParams(def.route.path)?.map((v) => v.name);
393
638
  const inputStructure = fallbackContractConfig("defaultInputStructure", def.route.inputStructure);
394
- let [required, schema] = await this.converter.convert(def.inputSchema, { strategy: "input" });
639
+ let [required, schema] = await this.converter.convert(
640
+ def.inputSchema,
641
+ {
642
+ ...baseSchemaConvertOptions,
643
+ strategy: "input"
644
+ }
645
+ );
395
646
  if (isAnySchema(schema) && !dynamicParams?.length) {
396
647
  return;
397
648
  }
649
+ if (inputStructure === "detailed" || inputStructure === "compact" && (dynamicParams?.length || method === "GET")) {
650
+ schema = simplifyComposedObjectJsonSchemasAndRefs(schema, doc);
651
+ }
398
652
  if (inputStructure === "compact") {
399
653
  if (dynamicParams?.length) {
400
654
  const error2 = new OpenAPIGeneratorError(
@@ -434,7 +688,8 @@ ${errors.join("\n\n")}`
434
688
  if (!isObjectSchema(schema)) {
435
689
  throw error;
436
690
  }
437
- if (dynamicParams?.length && (schema.properties?.params === void 0 || !isObjectSchema(schema.properties.params) || !checkParamsSchema(schema.properties.params, dynamicParams))) {
691
+ const resolvedParamSchema = schema.properties?.params !== void 0 ? simplifyComposedObjectJsonSchemasAndRefs(schema.properties.params, doc) : void 0;
692
+ if (dynamicParams?.length && (resolvedParamSchema === void 0 || !isObjectSchema(resolvedParamSchema) || !checkParamsSchema(resolvedParamSchema, dynamicParams))) {
438
693
  throw new OpenAPIGeneratorError(
439
694
  'When input structure is "detailed" and path has dynamic params, the "params" schema must be an object with all dynamic params as required.'
440
695
  );
@@ -442,12 +697,13 @@ ${errors.join("\n\n")}`
442
697
  for (const from of ["params", "query", "headers"]) {
443
698
  const fromSchema = schema.properties?.[from];
444
699
  if (fromSchema !== void 0) {
445
- if (!isObjectSchema(fromSchema)) {
700
+ const resolvedSchema = simplifyComposedObjectJsonSchemasAndRefs(fromSchema, doc);
701
+ if (!isObjectSchema(resolvedSchema)) {
446
702
  throw error;
447
703
  }
448
704
  const parameterIn = from === "params" ? "path" : from === "headers" ? "header" : "query";
449
705
  ref.parameters ??= [];
450
- ref.parameters.push(...toOpenAPIParameters(fromSchema, parameterIn));
706
+ ref.parameters.push(...toOpenAPIParameters(resolvedSchema, parameterIn));
451
707
  }
452
708
  }
453
709
  if (schema.properties?.body !== void 0) {
@@ -457,7 +713,7 @@ ${errors.join("\n\n")}`
457
713
  };
458
714
  }
459
715
  }
460
- async #successResponse(ref, def) {
716
+ async #successResponse(doc, ref, def, baseSchemaConvertOptions) {
461
717
  const outputSchema = def.outputSchema;
462
718
  const status = fallbackContractConfig("defaultSuccessStatus", def.route.successStatus);
463
719
  const description = fallbackContractConfig("defaultSuccessDescription", def.route?.successDescription);
@@ -468,13 +724,20 @@ ${errors.join("\n\n")}`
468
724
  ref.responses[status] = {
469
725
  description,
470
726
  content: toOpenAPIEventIteratorContent(
471
- await this.converter.convert(eventIteratorSchemaDetails.yields, { strategy: "output" }),
472
- await this.converter.convert(eventIteratorSchemaDetails.returns, { strategy: "output" })
727
+ await this.converter.convert(eventIteratorSchemaDetails.yields, { ...baseSchemaConvertOptions, strategy: "output" }),
728
+ await this.converter.convert(eventIteratorSchemaDetails.returns, { ...baseSchemaConvertOptions, strategy: "output" })
473
729
  )
474
730
  };
475
731
  return;
476
732
  }
477
- const [required, json] = await this.converter.convert(outputSchema, { strategy: "output" });
733
+ const [required, json] = await this.converter.convert(
734
+ outputSchema,
735
+ {
736
+ ...baseSchemaConvertOptions,
737
+ strategy: "output",
738
+ minStructureDepthForRef: outputStructure === "detailed" ? 1 : 0
739
+ }
740
+ );
478
741
  if (outputStructure === "compact") {
479
742
  ref.responses ??= {};
480
743
  ref.responses[status] = {
@@ -495,17 +758,19 @@ ${errors.join("\n\n")}`
495
758
 
496
759
  But got: ${stringifyJSON(item)}
497
760
  `);
498
- if (!isObjectSchema(item)) {
761
+ const simplifiedItem = simplifyComposedObjectJsonSchemasAndRefs(item, doc);
762
+ if (!isObjectSchema(simplifiedItem)) {
499
763
  throw error;
500
764
  }
501
765
  let schemaStatus;
502
766
  let schemaDescription;
503
- if (item.properties?.status !== void 0) {
504
- if (typeof item.properties.status !== "object" || item.properties.status.const === void 0 || typeof item.properties.status.const !== "number" || !Number.isInteger(item.properties.status.const) || isORPCErrorStatus(item.properties.status.const)) {
767
+ if (simplifiedItem.properties?.status !== void 0) {
768
+ const statusSchema = resolveOpenAPIJsonSchemaRef(doc, simplifiedItem.properties.status);
769
+ if (typeof statusSchema !== "object" || statusSchema.const === void 0 || typeof statusSchema.const !== "number" || !Number.isInteger(statusSchema.const) || isORPCErrorStatus(statusSchema.const)) {
505
770
  throw error;
506
771
  }
507
- schemaStatus = item.properties.status.const;
508
- schemaDescription = item.properties.status.description;
772
+ schemaStatus = statusSchema.const;
773
+ schemaDescription = statusSchema.description;
509
774
  }
510
775
  const itemStatus = schemaStatus ?? status;
511
776
  const itemDescription = schemaDescription ?? description;
@@ -520,71 +785,64 @@ ${errors.join("\n\n")}`
520
785
  ref.responses[itemStatus] = {
521
786
  description: itemDescription
522
787
  };
523
- if (item.properties?.headers !== void 0) {
524
- if (!isObjectSchema(item.properties.headers)) {
788
+ if (simplifiedItem.properties?.headers !== void 0) {
789
+ const headersSchema = simplifyComposedObjectJsonSchemasAndRefs(simplifiedItem.properties.headers, doc);
790
+ if (!isObjectSchema(headersSchema)) {
525
791
  throw error;
526
792
  }
527
- for (const key in item.properties.headers.properties) {
528
- const headerSchema = item.properties.headers.properties[key];
793
+ for (const key in headersSchema.properties) {
794
+ const headerSchema = headersSchema.properties[key];
529
795
  if (headerSchema !== void 0) {
530
796
  ref.responses[itemStatus].headers ??= {};
531
797
  ref.responses[itemStatus].headers[key] = {
532
798
  schema: toOpenAPISchema(headerSchema),
533
- required: item.properties.headers.required?.includes(key)
799
+ required: simplifiedItem.required?.includes("headers") && headersSchema.required?.includes(key)
534
800
  };
535
801
  }
536
802
  }
537
803
  }
538
- if (item.properties?.body !== void 0) {
804
+ if (simplifiedItem.properties?.body !== void 0) {
539
805
  ref.responses[itemStatus].content = toOpenAPIContent(
540
- applySchemaOptionality(item.required?.includes("body") ?? false, item.properties.body)
806
+ applySchemaOptionality(simplifiedItem.required?.includes("body") ?? false, simplifiedItem.properties.body)
541
807
  );
542
808
  }
543
809
  }
544
810
  }
545
- async #errorResponse(ref, def) {
811
+ async #errorResponse(ref, def, baseSchemaConvertOptions, undefinedErrorSchema, customErrorResponseBodySchema) {
546
812
  const errorMap = def.errorMap;
547
- const errors = {};
813
+ const errorResponsesByStatus = {};
548
814
  for (const code in errorMap) {
549
815
  const config = errorMap[code];
550
816
  if (!config) {
551
817
  continue;
552
818
  }
553
819
  const status = fallbackORPCErrorStatus(code, config.status);
554
- const message = fallbackORPCErrorMessage(code, config.message);
555
- const [dataRequired, dataSchema] = await this.converter.convert(config.data, { strategy: "output" });
556
- errors[status] ??= [];
557
- errors[status].push({
820
+ const defaultMessage = fallbackORPCErrorMessage(code, config.message);
821
+ errorResponsesByStatus[status] ??= { status, definedErrorDefinitions: [], errorSchemaVariants: [] };
822
+ const [dataRequired, dataSchema] = await this.converter.convert(config.data, { ...baseSchemaConvertOptions, strategy: "output" });
823
+ errorResponsesByStatus[status].definedErrorDefinitions.push([code, defaultMessage, dataRequired, dataSchema]);
824
+ errorResponsesByStatus[status].errorSchemaVariants.push({
558
825
  type: "object",
559
826
  properties: {
560
827
  defined: { const: true },
561
828
  code: { const: code },
562
829
  status: { const: status },
563
- message: { type: "string", default: message },
830
+ message: { type: "string", default: defaultMessage },
564
831
  data: dataSchema
565
832
  },
566
833
  required: dataRequired ? ["defined", "code", "status", "message", "data"] : ["defined", "code", "status", "message"]
567
834
  });
568
835
  }
569
836
  ref.responses ??= {};
570
- for (const status in errors) {
571
- const schemas = errors[status];
572
- ref.responses[status] = {
573
- description: status,
574
- content: toOpenAPIContent({
837
+ for (const statusString in errorResponsesByStatus) {
838
+ const errorResponse = errorResponsesByStatus[statusString];
839
+ const customBodySchema = value(customErrorResponseBodySchema, errorResponse.definedErrorDefinitions, errorResponse.status);
840
+ ref.responses[statusString] = {
841
+ description: statusString,
842
+ content: toOpenAPIContent(customBodySchema ?? {
575
843
  oneOf: [
576
- ...schemas,
577
- {
578
- type: "object",
579
- properties: {
580
- defined: { const: false },
581
- code: { type: "string" },
582
- status: { type: "number" },
583
- message: { type: "string" },
584
- data: {}
585
- },
586
- required: ["defined", "code", "status", "message"]
587
- }
844
+ ...errorResponse.errorSchemaVariants,
845
+ undefinedErrorSchema
588
846
  ]
589
847
  })
590
848
  };
@@ -592,4 +850,4 @@ ${errors.join("\n\n")}`
592
850
  }
593
851
  }
594
852
 
595
- 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, separateObjectSchema as s, toOpenAPIPath as t };
853
+ export { CompositeSchemaConverter as C, LOGIC_KEYWORDS as L, OpenAPIGenerator as O, applyCustomOpenAPIOperation as a, toOpenAPIMethod as b, customOpenAPIOperation as c, toOpenAPIContent as d, toOpenAPIEventIteratorContent as e, toOpenAPIParameters as f, getCustomOpenAPIOperation as g, checkParamsSchema as h, toOpenAPISchema as i, isFileSchema as j, isObjectSchema as k, isAnySchema as l, separateObjectSchema as m, filterSchemaBranches as n, applySchemaOptionality as o, expandUnionSchema as p, expandArrayableSchema as q, resolveOpenAPIJsonSchemaRef as r, simplifyComposedObjectJsonSchemasAndRefs as s, toOpenAPIPath as t, isPrimitiveSchema as u };
@@ -2,15 +2,17 @@ import { standardizeHTTPPath, StandardOpenAPIJsonSerializer, StandardBracketNota
2
2
  import { StandardHandler } from '@orpc/server/standard';
3
3
  import { isORPCErrorStatus } from '@orpc/client';
4
4
  import { fallbackContractConfig } from '@orpc/contract';
5
- import { isObject, stringifyJSON } from '@orpc/shared';
5
+ import { isObject, stringifyJSON, tryDecodeURIComponent, value } from '@orpc/shared';
6
6
  import { toHttpPath } from '@orpc/client/standard';
7
7
  import { traverseContractProcedures, isProcedure, getLazyMeta, unlazy, getRouter, createContractedProcedure } from '@orpc/server';
8
8
  import { createRouter, addRoute, findRoute } from 'rou3';
9
9
 
10
10
  class StandardOpenAPICodec {
11
- constructor(serializer) {
11
+ constructor(serializer, options = {}) {
12
12
  this.serializer = serializer;
13
+ this.customErrorResponseBodyEncoder = options.customErrorResponseBodyEncoder;
13
14
  }
15
+ customErrorResponseBodyEncoder;
14
16
  async decode(request, params, procedure) {
15
17
  const inputStructure = fallbackContractConfig("defaultInputStructure", procedure["~orpc"].route.inputStructure);
16
18
  if (inputStructure === "compact") {
@@ -73,10 +75,11 @@ class StandardOpenAPICodec {
73
75
  };
74
76
  }
75
77
  encodeError(error) {
78
+ const body = this.customErrorResponseBodyEncoder?.(error) ?? error.toJSON();
76
79
  return {
77
80
  status: error.status,
78
81
  headers: {},
79
- body: this.serializer.serialize(error.toJSON(), { outputFormat: "plain" })
82
+ body: this.serializer.serialize(body, { outputFormat: "plain" })
80
83
  };
81
84
  }
82
85
  #isDetailedOutput(output) {
@@ -97,14 +100,22 @@ function toRou3Pattern(path) {
97
100
  return standardizeHTTPPath(path).replace(/\/\{\+([^}]+)\}/g, "/**:$1").replace(/\/\{([^}]+)\}/g, "/:$1");
98
101
  }
99
102
  function decodeParams(params) {
100
- return Object.fromEntries(Object.entries(params).map(([key, value]) => [key, decodeURIComponent(value)]));
103
+ return Object.fromEntries(Object.entries(params).map(([key, value]) => [key, tryDecodeURIComponent(value)]));
101
104
  }
102
105
 
103
106
  class StandardOpenAPIMatcher {
107
+ filter;
104
108
  tree = createRouter();
105
109
  pendingRouters = [];
110
+ constructor(options = {}) {
111
+ this.filter = options.filter ?? true;
112
+ }
106
113
  init(router, path = []) {
107
- const laziedOptions = traverseContractProcedures({ router, path }, ({ path: path2, contract }) => {
114
+ const laziedOptions = traverseContractProcedures({ router, path }, (traverseOptions) => {
115
+ if (!value(this.filter, traverseOptions)) {
116
+ return;
117
+ }
118
+ const { path: path2, contract } = traverseOptions;
108
119
  const method = fallbackContractConfig("defaultMethod", contract["~orpc"].route.method);
109
120
  const httpPath = toRou3Pattern(contract["~orpc"].route.path ?? toHttpPath(path2));
110
121
  if (isProcedure(contract)) {
@@ -168,10 +179,10 @@ class StandardOpenAPIMatcher {
168
179
  class StandardOpenAPIHandler extends StandardHandler {
169
180
  constructor(router, options) {
170
181
  const jsonSerializer = new StandardOpenAPIJsonSerializer(options);
171
- const bracketNotationSerializer = new StandardBracketNotationSerializer();
182
+ const bracketNotationSerializer = new StandardBracketNotationSerializer(options);
172
183
  const serializer = new StandardOpenAPISerializer(jsonSerializer, bracketNotationSerializer);
173
- const matcher = new StandardOpenAPIMatcher();
174
- const codec = new StandardOpenAPICodec(serializer);
184
+ const matcher = new StandardOpenAPIMatcher(options);
185
+ const codec = new StandardOpenAPICodec(serializer, options);
175
186
  super(router, matcher, codec, options);
176
187
  }
177
188
  }