@orpc/openapi 0.0.0-next.f779f69 → 0.0.0-next.f7af1c4

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.
@@ -2,7 +2,7 @@ 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';
@@ -97,14 +97,22 @@ function toRou3Pattern(path) {
97
97
  return standardizeHTTPPath(path).replace(/\/\{\+([^}]+)\}/g, "/**:$1").replace(/\/\{([^}]+)\}/g, "/:$1");
98
98
  }
99
99
  function decodeParams(params) {
100
- return Object.fromEntries(Object.entries(params).map(([key, value]) => [key, decodeURIComponent(value)]));
100
+ return Object.fromEntries(Object.entries(params).map(([key, value]) => [key, tryDecodeURIComponent(value)]));
101
101
  }
102
102
 
103
103
  class StandardOpenAPIMatcher {
104
+ filter;
104
105
  tree = createRouter();
105
106
  pendingRouters = [];
107
+ constructor(options = {}) {
108
+ this.filter = options.filter ?? true;
109
+ }
106
110
  init(router, path = []) {
107
- const laziedOptions = traverseContractProcedures({ router, path }, ({ path: path2, contract }) => {
111
+ const laziedOptions = traverseContractProcedures({ router, path }, (traverseOptions) => {
112
+ if (!value(this.filter, traverseOptions)) {
113
+ return;
114
+ }
115
+ const { path: path2, contract } = traverseOptions;
108
116
  const method = fallbackContractConfig("defaultMethod", contract["~orpc"].route.method);
109
117
  const httpPath = toRou3Pattern(contract["~orpc"].route.path ?? toHttpPath(path2));
110
118
  if (isProcedure(contract)) {
@@ -168,9 +176,9 @@ class StandardOpenAPIMatcher {
168
176
  class StandardOpenAPIHandler extends StandardHandler {
169
177
  constructor(router, options) {
170
178
  const jsonSerializer = new StandardOpenAPIJsonSerializer(options);
171
- const bracketNotationSerializer = new StandardBracketNotationSerializer();
179
+ const bracketNotationSerializer = new StandardBracketNotationSerializer(options);
172
180
  const serializer = new StandardOpenAPISerializer(jsonSerializer, bracketNotationSerializer);
173
- const matcher = new StandardOpenAPIMatcher();
181
+ const matcher = new StandardOpenAPIMatcher(options);
174
182
  const codec = new StandardOpenAPICodec(serializer);
175
183
  super(router, matcher, codec, options);
176
184
  }
@@ -3,8 +3,8 @@ import { toHttpPath } from '@orpc/client/standard';
3
3
  import { fallbackContractConfig, getEventIteratorSchemaDetails } from '@orpc/contract';
4
4
  import { standardizeHTTPPath, StandardOpenAPIJsonSerializer, getDynamicParams } from '@orpc/openapi-client/standard';
5
5
  import { isProcedure, resolveContractProcedures } from '@orpc/server';
6
- import { isObject, stringifyJSON, findDeepMatches, toArray, clone } from '@orpc/shared';
7
- import { TypeName } from 'json-schema-typed/draft-2020-12';
6
+ import { isObject, stringifyJSON, findDeepMatches, toArray, clone, value } from '@orpc/shared';
7
+ import { TypeName } from '@orpc/interop/json-schema-typed/draft-2020-12';
8
8
 
9
9
  const OPERATION_EXTENDER_SYMBOL = Symbol("ORPC_OPERATION_EXTENDER");
10
10
  function customOpenAPIOperation(o, extend) {
@@ -114,13 +114,18 @@ function isAnySchema(schema) {
114
114
  return false;
115
115
  }
116
116
  function separateObjectSchema(schema, separatedProperties) {
117
- if (Object.keys(schema).some((k) => k !== "type" && k !== "properties" && k !== "required" && LOGIC_KEYWORDS.includes(k))) {
117
+ if (Object.keys(schema).some(
118
+ (k) => !["type", "properties", "required", "additionalProperties"].includes(k) && LOGIC_KEYWORDS.includes(k) && schema[k] !== void 0
119
+ )) {
118
120
  return [{ type: "object" }, schema];
119
121
  }
120
122
  const matched = { ...schema };
121
123
  const rest = { ...schema };
122
- matched.properties = schema.properties && Object.entries(schema.properties).filter(([key]) => separatedProperties.includes(key)).reduce((acc, [key, value]) => {
123
- acc[key] = value;
124
+ matched.properties = separatedProperties.reduce((acc, key) => {
125
+ const keySchema = schema.properties?.[key] ?? schema.additionalProperties;
126
+ if (keySchema !== void 0) {
127
+ acc[key] = keySchema;
128
+ }
124
129
  return acc;
125
130
  }, {});
126
131
  matched.required = schema.required?.filter((key) => separatedProperties.includes(key));
@@ -345,6 +350,15 @@ function checkParamsSchema(schema, params) {
345
350
  function toOpenAPISchema(schema) {
346
351
  return schema === true ? {} : schema === false ? { not: {} } : schema;
347
352
  }
353
+ const OPENAPI_JSON_SCHEMA_REF_PREFIX = "#/components/schemas/";
354
+ function resolveOpenAPIJsonSchemaRef(doc, schema) {
355
+ if (typeof schema !== "object" || !schema.$ref?.startsWith(OPENAPI_JSON_SCHEMA_REF_PREFIX)) {
356
+ return schema;
357
+ }
358
+ const name = schema.$ref.slice(OPENAPI_JSON_SCHEMA_REF_PREFIX.length);
359
+ const resolved = doc.components?.schemas?.[name];
360
+ return resolved ?? schema;
361
+ }
348
362
 
349
363
  class CompositeSchemaConverter {
350
364
  converters;
@@ -376,40 +390,49 @@ class OpenAPIGenerator {
376
390
  * @see {@link https://orpc.unnoq.com/docs/openapi/openapi-specification OpenAPI Specification Docs}
377
391
  */
378
392
  async generate(router, options = {}) {
379
- const exclude = options.exclude ?? (() => false);
393
+ const filter = options.filter ?? (({ contract, path }) => {
394
+ return !(options.exclude?.(contract, path) ?? false);
395
+ });
380
396
  const doc = {
381
397
  ...clone(options),
382
398
  info: options.info ?? { title: "API Reference", version: "0.0.0" },
383
399
  openapi: "3.1.1",
384
- exclude: void 0
400
+ exclude: void 0,
401
+ filter: void 0,
402
+ commonSchemas: void 0
385
403
  };
404
+ const { baseSchemaConvertOptions, undefinedErrorJsonSchema } = await this.#resolveCommonSchemas(doc, options.commonSchemas);
386
405
  const contracts = [];
387
- await resolveContractProcedures({ path: [], router }, ({ contract, path }) => {
388
- if (!exclude(contract, path)) {
389
- contracts.push({ contract, path });
406
+ await resolveContractProcedures({ path: [], router }, (traverseOptions) => {
407
+ if (!value(filter, traverseOptions)) {
408
+ return;
390
409
  }
410
+ contracts.push(traverseOptions);
391
411
  });
392
412
  const errors = [];
393
413
  for (const { contract, path } of contracts) {
394
- const operationId = path.join(".");
414
+ const stringPath = path.join(".");
395
415
  try {
396
416
  const def = contract["~orpc"];
397
417
  const method = toOpenAPIMethod(fallbackContractConfig("defaultMethod", def.route.method));
398
418
  const httpPath = toOpenAPIPath(def.route.path ?? toHttpPath(path));
399
419
  let operationObjectRef;
400
- if (def.route.spec !== void 0) {
420
+ if (def.route.spec !== void 0 && typeof def.route.spec !== "function") {
401
421
  operationObjectRef = def.route.spec;
402
422
  } else {
403
423
  operationObjectRef = {
404
- operationId,
424
+ operationId: def.route.operationId ?? stringPath,
405
425
  summary: def.route.summary,
406
426
  description: def.route.description,
407
427
  deprecated: def.route.deprecated,
408
428
  tags: def.route.tags?.map((tag) => tag)
409
429
  };
410
- await this.#request(operationObjectRef, def);
411
- await this.#successResponse(operationObjectRef, def);
412
- await this.#errorResponse(operationObjectRef, def);
430
+ await this.#request(doc, operationObjectRef, def, baseSchemaConvertOptions);
431
+ await this.#successResponse(doc, operationObjectRef, def, baseSchemaConvertOptions);
432
+ await this.#errorResponse(operationObjectRef, def, baseSchemaConvertOptions, undefinedErrorJsonSchema);
433
+ }
434
+ if (typeof def.route.spec === "function") {
435
+ operationObjectRef = def.route.spec(operationObjectRef);
413
436
  }
414
437
  doc.paths ??= {};
415
438
  doc.paths[httpPath] ??= {};
@@ -419,7 +442,7 @@ class OpenAPIGenerator {
419
442
  throw e;
420
443
  }
421
444
  errors.push(
422
- `[OpenAPIGenerator] Error occurred while generating OpenAPI for procedure at path: ${operationId}
445
+ `[OpenAPIGenerator] Error occurred while generating OpenAPI for procedure at path: ${stringPath}
423
446
  ${e.message}`
424
447
  );
425
448
  }
@@ -433,22 +456,96 @@ ${errors.join("\n\n")}`
433
456
  }
434
457
  return this.serializer.serialize(doc)[0];
435
458
  }
436
- async #request(ref, def) {
459
+ async #resolveCommonSchemas(doc, commonSchemas) {
460
+ let undefinedErrorJsonSchema = {
461
+ type: "object",
462
+ properties: {
463
+ defined: { const: false },
464
+ code: { type: "string" },
465
+ status: { type: "number" },
466
+ message: { type: "string" },
467
+ data: {}
468
+ },
469
+ required: ["defined", "code", "status", "message"]
470
+ };
471
+ const baseSchemaConvertOptions = {};
472
+ if (commonSchemas) {
473
+ baseSchemaConvertOptions.components = [];
474
+ for (const key in commonSchemas) {
475
+ const options = commonSchemas[key];
476
+ if (options.schema === void 0) {
477
+ continue;
478
+ }
479
+ const { schema, strategy = "input" } = options;
480
+ const [required, json] = await this.converter.convert(schema, { strategy });
481
+ const allowedStrategies = [strategy];
482
+ if (strategy === "input") {
483
+ const [outputRequired, outputJson] = await this.converter.convert(schema, { strategy: "output" });
484
+ if (outputRequired === required && stringifyJSON(outputJson) === stringifyJSON(json)) {
485
+ allowedStrategies.push("output");
486
+ }
487
+ } else if (strategy === "output") {
488
+ const [inputRequired, inputJson] = await this.converter.convert(schema, { strategy: "input" });
489
+ if (inputRequired === required && stringifyJSON(inputJson) === stringifyJSON(json)) {
490
+ allowedStrategies.push("input");
491
+ }
492
+ }
493
+ baseSchemaConvertOptions.components.push({
494
+ schema,
495
+ required,
496
+ ref: `#/components/schemas/${key}`,
497
+ allowedStrategies
498
+ });
499
+ }
500
+ doc.components ??= {};
501
+ doc.components.schemas ??= {};
502
+ for (const key in commonSchemas) {
503
+ const options = commonSchemas[key];
504
+ if (options.schema === void 0) {
505
+ if (options.error === "UndefinedError") {
506
+ doc.components.schemas[key] = toOpenAPISchema(undefinedErrorJsonSchema);
507
+ undefinedErrorJsonSchema = { $ref: `#/components/schemas/${key}` };
508
+ }
509
+ continue;
510
+ }
511
+ const { schema, strategy = "input" } = options;
512
+ const [, json] = await this.converter.convert(
513
+ schema,
514
+ {
515
+ ...baseSchemaConvertOptions,
516
+ strategy,
517
+ minStructureDepthForRef: 1
518
+ // not allow use $ref for root schemas
519
+ }
520
+ );
521
+ doc.components.schemas[key] = toOpenAPISchema(json);
522
+ }
523
+ }
524
+ return { baseSchemaConvertOptions, undefinedErrorJsonSchema };
525
+ }
526
+ async #request(doc, ref, def, baseSchemaConvertOptions) {
437
527
  const method = fallbackContractConfig("defaultMethod", def.route.method);
438
528
  const details = getEventIteratorSchemaDetails(def.inputSchema);
439
529
  if (details) {
440
530
  ref.requestBody = {
441
531
  required: true,
442
532
  content: toOpenAPIEventIteratorContent(
443
- await this.converter.convert(details.yields, { strategy: "input" }),
444
- await this.converter.convert(details.returns, { strategy: "input" })
533
+ await this.converter.convert(details.yields, { ...baseSchemaConvertOptions, strategy: "input" }),
534
+ await this.converter.convert(details.returns, { ...baseSchemaConvertOptions, strategy: "input" })
445
535
  )
446
536
  };
447
537
  return;
448
538
  }
449
539
  const dynamicParams = getDynamicParams(def.route.path)?.map((v) => v.name);
450
540
  const inputStructure = fallbackContractConfig("defaultInputStructure", def.route.inputStructure);
451
- let [required, schema] = await this.converter.convert(def.inputSchema, { strategy: "input" });
541
+ let [required, schema] = await this.converter.convert(
542
+ def.inputSchema,
543
+ {
544
+ ...baseSchemaConvertOptions,
545
+ strategy: "input",
546
+ minStructureDepthForRef: dynamicParams?.length || inputStructure === "detailed" ? 1 : 0
547
+ }
548
+ );
452
549
  if (isAnySchema(schema) && !dynamicParams?.length) {
453
550
  return;
454
551
  }
@@ -470,13 +567,14 @@ ${errors.join("\n\n")}`
470
567
  ref.parameters.push(...toOpenAPIParameters(paramsSchema, "path"));
471
568
  }
472
569
  if (method === "GET") {
473
- if (!isObjectSchema(schema)) {
570
+ const resolvedSchema = resolveOpenAPIJsonSchemaRef(doc, schema);
571
+ if (!isObjectSchema(resolvedSchema)) {
474
572
  throw new OpenAPIGeneratorError(
475
573
  'When method is "GET", input schema must satisfy: object | any | unknown'
476
574
  );
477
575
  }
478
576
  ref.parameters ??= [];
479
- ref.parameters.push(...toOpenAPIParameters(schema, "query"));
577
+ ref.parameters.push(...toOpenAPIParameters(resolvedSchema, "query"));
480
578
  } else {
481
579
  ref.requestBody = {
482
580
  required,
@@ -491,7 +589,8 @@ ${errors.join("\n\n")}`
491
589
  if (!isObjectSchema(schema)) {
492
590
  throw error;
493
591
  }
494
- if (dynamicParams?.length && (schema.properties?.params === void 0 || !isObjectSchema(schema.properties.params) || !checkParamsSchema(schema.properties.params, dynamicParams))) {
592
+ const resolvedParamSchema = schema.properties?.params !== void 0 ? resolveOpenAPIJsonSchemaRef(doc, schema.properties.params) : void 0;
593
+ if (dynamicParams?.length && (resolvedParamSchema === void 0 || !isObjectSchema(resolvedParamSchema) || !checkParamsSchema(resolvedParamSchema, dynamicParams))) {
495
594
  throw new OpenAPIGeneratorError(
496
595
  'When input structure is "detailed" and path has dynamic params, the "params" schema must be an object with all dynamic params as required.'
497
596
  );
@@ -499,12 +598,13 @@ ${errors.join("\n\n")}`
499
598
  for (const from of ["params", "query", "headers"]) {
500
599
  const fromSchema = schema.properties?.[from];
501
600
  if (fromSchema !== void 0) {
502
- if (!isObjectSchema(fromSchema)) {
601
+ const resolvedSchema = resolveOpenAPIJsonSchemaRef(doc, fromSchema);
602
+ if (!isObjectSchema(resolvedSchema)) {
503
603
  throw error;
504
604
  }
505
605
  const parameterIn = from === "params" ? "path" : from === "headers" ? "header" : "query";
506
606
  ref.parameters ??= [];
507
- ref.parameters.push(...toOpenAPIParameters(fromSchema, parameterIn));
607
+ ref.parameters.push(...toOpenAPIParameters(resolvedSchema, parameterIn));
508
608
  }
509
609
  }
510
610
  if (schema.properties?.body !== void 0) {
@@ -514,7 +614,7 @@ ${errors.join("\n\n")}`
514
614
  };
515
615
  }
516
616
  }
517
- async #successResponse(ref, def) {
617
+ async #successResponse(doc, ref, def, baseSchemaConvertOptions) {
518
618
  const outputSchema = def.outputSchema;
519
619
  const status = fallbackContractConfig("defaultSuccessStatus", def.route.successStatus);
520
620
  const description = fallbackContractConfig("defaultSuccessDescription", def.route?.successDescription);
@@ -525,13 +625,20 @@ ${errors.join("\n\n")}`
525
625
  ref.responses[status] = {
526
626
  description,
527
627
  content: toOpenAPIEventIteratorContent(
528
- await this.converter.convert(eventIteratorSchemaDetails.yields, { strategy: "output" }),
529
- await this.converter.convert(eventIteratorSchemaDetails.returns, { strategy: "output" })
628
+ await this.converter.convert(eventIteratorSchemaDetails.yields, { ...baseSchemaConvertOptions, strategy: "output" }),
629
+ await this.converter.convert(eventIteratorSchemaDetails.returns, { ...baseSchemaConvertOptions, strategy: "output" })
530
630
  )
531
631
  };
532
632
  return;
533
633
  }
534
- const [required, json] = await this.converter.convert(outputSchema, { strategy: "output" });
634
+ const [required, json] = await this.converter.convert(
635
+ outputSchema,
636
+ {
637
+ ...baseSchemaConvertOptions,
638
+ strategy: "output",
639
+ minStructureDepthForRef: outputStructure === "detailed" ? 1 : 0
640
+ }
641
+ );
535
642
  if (outputStructure === "compact") {
536
643
  ref.responses ??= {};
537
644
  ref.responses[status] = {
@@ -558,11 +665,12 @@ ${errors.join("\n\n")}`
558
665
  let schemaStatus;
559
666
  let schemaDescription;
560
667
  if (item.properties?.status !== void 0) {
561
- 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)) {
668
+ const statusSchema = resolveOpenAPIJsonSchemaRef(doc, item.properties.status);
669
+ if (typeof statusSchema !== "object" || statusSchema.const === void 0 || typeof statusSchema.const !== "number" || !Number.isInteger(statusSchema.const) || isORPCErrorStatus(statusSchema.const)) {
562
670
  throw error;
563
671
  }
564
- schemaStatus = item.properties.status.const;
565
- schemaDescription = item.properties.status.description;
672
+ schemaStatus = statusSchema.const;
673
+ schemaDescription = statusSchema.description;
566
674
  }
567
675
  const itemStatus = schemaStatus ?? status;
568
676
  const itemDescription = schemaDescription ?? description;
@@ -578,16 +686,17 @@ ${errors.join("\n\n")}`
578
686
  description: itemDescription
579
687
  };
580
688
  if (item.properties?.headers !== void 0) {
581
- if (!isObjectSchema(item.properties.headers)) {
689
+ const headersSchema = resolveOpenAPIJsonSchemaRef(doc, item.properties.headers);
690
+ if (!isObjectSchema(headersSchema)) {
582
691
  throw error;
583
692
  }
584
- for (const key in item.properties.headers.properties) {
585
- const headerSchema = item.properties.headers.properties[key];
693
+ for (const key in headersSchema.properties) {
694
+ const headerSchema = headersSchema.properties[key];
586
695
  if (headerSchema !== void 0) {
587
696
  ref.responses[itemStatus].headers ??= {};
588
697
  ref.responses[itemStatus].headers[key] = {
589
698
  schema: toOpenAPISchema(headerSchema),
590
- required: item.properties.headers.required?.includes(key)
699
+ required: item.required?.includes("headers") && headersSchema.required?.includes(key)
591
700
  };
592
701
  }
593
702
  }
@@ -599,7 +708,7 @@ ${errors.join("\n\n")}`
599
708
  }
600
709
  }
601
710
  }
602
- async #errorResponse(ref, def) {
711
+ async #errorResponse(ref, def, baseSchemaConvertOptions, undefinedErrorSchema) {
603
712
  const errorMap = def.errorMap;
604
713
  const errors = {};
605
714
  for (const code in errorMap) {
@@ -609,7 +718,7 @@ ${errors.join("\n\n")}`
609
718
  }
610
719
  const status = fallbackORPCErrorStatus(code, config.status);
611
720
  const message = fallbackORPCErrorMessage(code, config.message);
612
- const [dataRequired, dataSchema] = await this.converter.convert(config.data, { strategy: "output" });
721
+ const [dataRequired, dataSchema] = await this.converter.convert(config.data, { ...baseSchemaConvertOptions, strategy: "output" });
613
722
  errors[status] ??= [];
614
723
  errors[status].push({
615
724
  type: "object",
@@ -631,17 +740,7 @@ ${errors.join("\n\n")}`
631
740
  content: toOpenAPIContent({
632
741
  oneOf: [
633
742
  ...schemas,
634
- {
635
- type: "object",
636
- properties: {
637
- defined: { const: false },
638
- code: { type: "string" },
639
- status: { type: "number" },
640
- message: { type: "string" },
641
- data: {}
642
- },
643
- required: ["defined", "code", "status", "message"]
644
- }
743
+ undefinedErrorSchema
645
744
  ]
646
745
  })
647
746
  };
@@ -649,4 +748,4 @@ ${errors.join("\n\n")}`
649
748
  }
650
749
  }
651
750
 
652
- export { CompositeSchemaConverter as C, LOGIC_KEYWORDS as L, OpenAPIGenerator as O, applyCustomOpenAPIOperation as a, toOpenAPIMethod as b, customOpenAPIOperation as c, toOpenAPIContent as d, toOpenAPIEventIteratorContent as e, toOpenAPIParameters as f, getCustomOpenAPIOperation as g, checkParamsSchema as h, toOpenAPISchema as i, isFileSchema as j, isObjectSchema as k, isAnySchema as l, filterSchemaBranches as m, applySchemaOptionality as n, expandUnionSchema as o, expandArrayableSchema as p, isPrimitiveSchema as q, separateObjectSchema as s, toOpenAPIPath as t };
751
+ 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 };
@@ -0,0 +1,31 @@
1
+ import { StandardOpenAPIJsonSerializerOptions, StandardBracketNotationSerializerOptions } from '@orpc/openapi-client/standard';
2
+ import { TraverseContractProcedureCallbackOptions, AnyRouter, Context, Router } from '@orpc/server';
3
+ import { StandardMatcher, StandardMatchResult, StandardHandlerOptions, StandardHandler } from '@orpc/server/standard';
4
+ import { HTTPPath } from '@orpc/client';
5
+ import { Value } from '@orpc/shared';
6
+
7
+ interface StandardOpenAPIMatcherOptions {
8
+ /**
9
+ * Filter procedures. Return `false` to exclude a procedure from matching.
10
+ *
11
+ * @default true
12
+ */
13
+ filter?: Value<boolean, [options: TraverseContractProcedureCallbackOptions]>;
14
+ }
15
+ declare class StandardOpenAPIMatcher implements StandardMatcher {
16
+ private readonly filter;
17
+ private readonly tree;
18
+ private pendingRouters;
19
+ constructor(options?: StandardOpenAPIMatcherOptions);
20
+ init(router: AnyRouter, path?: readonly string[]): void;
21
+ match(method: string, pathname: HTTPPath): Promise<StandardMatchResult>;
22
+ }
23
+
24
+ interface StandardOpenAPIHandlerOptions<T extends Context> extends StandardHandlerOptions<T>, StandardOpenAPIJsonSerializerOptions, StandardBracketNotationSerializerOptions, StandardOpenAPIMatcherOptions {
25
+ }
26
+ declare class StandardOpenAPIHandler<T extends Context> extends StandardHandler<T> {
27
+ constructor(router: Router<any, T>, options: NoInfer<StandardOpenAPIHandlerOptions<T>>);
28
+ }
29
+
30
+ export { StandardOpenAPIHandler as a, StandardOpenAPIMatcher as c };
31
+ export type { StandardOpenAPIHandlerOptions as S, StandardOpenAPIMatcherOptions as b };
@@ -0,0 +1,31 @@
1
+ import { StandardOpenAPIJsonSerializerOptions, StandardBracketNotationSerializerOptions } from '@orpc/openapi-client/standard';
2
+ import { TraverseContractProcedureCallbackOptions, AnyRouter, Context, Router } from '@orpc/server';
3
+ import { StandardMatcher, StandardMatchResult, StandardHandlerOptions, StandardHandler } from '@orpc/server/standard';
4
+ import { HTTPPath } from '@orpc/client';
5
+ import { Value } from '@orpc/shared';
6
+
7
+ interface StandardOpenAPIMatcherOptions {
8
+ /**
9
+ * Filter procedures. Return `false` to exclude a procedure from matching.
10
+ *
11
+ * @default true
12
+ */
13
+ filter?: Value<boolean, [options: TraverseContractProcedureCallbackOptions]>;
14
+ }
15
+ declare class StandardOpenAPIMatcher implements StandardMatcher {
16
+ private readonly filter;
17
+ private readonly tree;
18
+ private pendingRouters;
19
+ constructor(options?: StandardOpenAPIMatcherOptions);
20
+ init(router: AnyRouter, path?: readonly string[]): void;
21
+ match(method: string, pathname: HTTPPath): Promise<StandardMatchResult>;
22
+ }
23
+
24
+ interface StandardOpenAPIHandlerOptions<T extends Context> extends StandardHandlerOptions<T>, StandardOpenAPIJsonSerializerOptions, StandardBracketNotationSerializerOptions, StandardOpenAPIMatcherOptions {
25
+ }
26
+ declare class StandardOpenAPIHandler<T extends Context> extends StandardHandler<T> {
27
+ constructor(router: Router<any, T>, options: NoInfer<StandardOpenAPIHandlerOptions<T>>);
28
+ }
29
+
30
+ export { StandardOpenAPIHandler as a, StandardOpenAPIMatcher as c };
31
+ export type { StandardOpenAPIHandlerOptions as S, StandardOpenAPIMatcherOptions as b };
@@ -0,0 +1,108 @@
1
+ import { AnySchema, OpenAPI, AnyContractProcedure, AnyContractRouter } from '@orpc/contract';
2
+ import { StandardOpenAPIJsonSerializerOptions } from '@orpc/openapi-client/standard';
3
+ import { AnyProcedure, TraverseContractProcedureCallbackOptions, AnyRouter } from '@orpc/server';
4
+ import { Promisable, Value } from '@orpc/shared';
5
+ import { JSONSchema } from '@orpc/interop/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: readonly 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
+ * @deprecated Use `filter` option instead.
48
+ * @default () => false
49
+ */
50
+ exclude?: (procedure: AnyProcedure | AnyContractProcedure, path: readonly string[]) => boolean;
51
+ /**
52
+ * Filter procedures. Return `false` to exclude a procedure from the OpenAPI specification.
53
+ *
54
+ * @default true
55
+ */
56
+ filter?: Value<boolean, [options: TraverseContractProcedureCallbackOptions]>;
57
+ /**
58
+ * Common schemas to be used for $ref resolution.
59
+ */
60
+ commonSchemas?: Record<string, {
61
+ /**
62
+ * Determines which schema definition to use when input and output schemas differ.
63
+ * This is needed because some schemas transform data differently between input and output,
64
+ * making it impossible to use a single $ref for both cases.
65
+ *
66
+ * @example
67
+ * ```ts
68
+ * // This schema transforms a string input into a number output
69
+ * const Schema = z.string()
70
+ * .transform(v => Number(v))
71
+ * .pipe(z.number())
72
+ *
73
+ * // Input schema: { type: 'string' }
74
+ * // Output schema: { type: 'number' }
75
+ * ```
76
+ *
77
+ * When schemas differ between input and output, you must explicitly choose
78
+ * which version to use for the OpenAPI specification.
79
+ *
80
+ * @default 'input' - Uses the input schema definition by default
81
+ */
82
+ strategy?: SchemaConvertOptions['strategy'];
83
+ schema: AnySchema;
84
+ } | {
85
+ error: 'UndefinedError';
86
+ schema?: never;
87
+ }>;
88
+ }
89
+ /**
90
+ * The generator that converts oRPC routers/contracts to OpenAPI specifications.
91
+ *
92
+ * @see {@link https://orpc.unnoq.com/docs/openapi/openapi-specification OpenAPI Specification Docs}
93
+ */
94
+ declare class OpenAPIGenerator {
95
+ #private;
96
+ private readonly serializer;
97
+ private readonly converter;
98
+ constructor(options?: OpenAPIGeneratorOptions);
99
+ /**
100
+ * Generates OpenAPI specifications from oRPC routers/contracts.
101
+ *
102
+ * @see {@link https://orpc.unnoq.com/docs/openapi/openapi-specification OpenAPI Specification Docs}
103
+ */
104
+ generate(router: AnyContractRouter | AnyRouter, options?: OpenAPIGeneratorGenerateOptions): Promise<OpenAPI.Document>;
105
+ }
106
+
107
+ export { OpenAPIGenerator as b, CompositeSchemaConverter as e };
108
+ export type { ConditionalSchemaConverter as C, OpenAPIGeneratorOptions as O, SchemaConverterComponent as S, OpenAPIGeneratorGenerateOptions as a, SchemaConvertOptions as c, SchemaConverter as d };