@orpc/openapi 0.0.0-next.fd6982a → 0.0.0-next.fdd2389

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.
@@ -1,11 +1,11 @@
1
1
  import { stringifyJSON, once, value } from '@orpc/shared';
2
- import { O as OpenAPIGenerator } from '../shared/openapi.PDTdnRIU.mjs';
2
+ import { O as OpenAPIGenerator } from '../shared/openapi.BlSv9FKY.mjs';
3
3
  import '@orpc/client';
4
4
  import '@orpc/client/standard';
5
5
  import '@orpc/contract';
6
6
  import '@orpc/openapi-client/standard';
7
7
  import '@orpc/server';
8
- import 'json-schema-typed/draft-2020-12';
8
+ import '@orpc/interop/json-schema-typed/draft-2020-12';
9
9
 
10
10
  class OpenAPIReferencePlugin {
11
11
  generator;
@@ -14,7 +14,9 @@ class OpenAPIReferencePlugin {
14
14
  docsPath;
15
15
  docsTitle;
16
16
  docsHead;
17
+ docsProvider;
17
18
  docsScriptUrl;
19
+ docsCssUrl;
18
20
  docsConfig;
19
21
  renderDocsHtml;
20
22
  constructor(options = {}) {
@@ -22,16 +24,59 @@ class OpenAPIReferencePlugin {
22
24
  this.docsPath = options.docsPath ?? "/";
23
25
  this.docsTitle = options.docsTitle ?? "API Reference";
24
26
  this.docsConfig = options.docsConfig ?? void 0;
25
- this.docsScriptUrl = options.docsScriptUrl ?? "https://cdn.jsdelivr.net/npm/@scalar/api-reference";
27
+ this.docsProvider = options.docsProvider ?? "scalar";
28
+ this.docsScriptUrl = options.docsScriptUrl ?? (this.docsProvider === "swagger" ? "https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js" : "https://cdn.jsdelivr.net/npm/@scalar/api-reference");
29
+ this.docsCssUrl = options.docsCssUrl ?? (this.docsProvider === "swagger" ? "https://unpkg.com/swagger-ui-dist/swagger-ui.css" : void 0);
26
30
  this.docsHead = options.docsHead ?? "";
27
31
  this.specPath = options.specPath ?? "/spec.json";
28
32
  this.generator = new OpenAPIGenerator(options);
29
33
  const esc = (s) => s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
30
- this.renderDocsHtml = options.renderDocsHtml ?? ((specUrl, title, head, scriptUrl, config, spec) => {
31
- const finalConfig = {
32
- content: stringifyJSON(spec),
33
- ...config
34
- };
34
+ this.renderDocsHtml = options.renderDocsHtml ?? ((specUrl, title, head, scriptUrl, config, spec, docsProvider, cssUrl) => {
35
+ let body;
36
+ if (docsProvider === "swagger") {
37
+ const swaggerConfig = {
38
+ dom_id: "#app",
39
+ spec,
40
+ deepLinking: true,
41
+ presets: [
42
+ "SwaggerUIBundle.presets.apis",
43
+ "SwaggerUIBundle.presets.standalone"
44
+ ],
45
+ plugins: [
46
+ "SwaggerUIBundle.plugins.DownloadUrl"
47
+ ],
48
+ ...config
49
+ };
50
+ body = `
51
+ <body>
52
+ <div id="app"></div>
53
+
54
+ <script src="${esc(scriptUrl)}"><\/script>
55
+
56
+ <script>
57
+ window.onload = () => {
58
+ window.ui = SwaggerUIBundle(${stringifyJSON(swaggerConfig).replace(/"(SwaggerUIBundle\.[^"]+)"/g, "$1")})
59
+ }
60
+ <\/script>
61
+ </body>
62
+ `;
63
+ } else {
64
+ const scalarConfig = {
65
+ content: stringifyJSON(spec),
66
+ ...config
67
+ };
68
+ body = `
69
+ <body>
70
+ <div id="app" data-config="${esc(stringifyJSON(scalarConfig))}"></div>
71
+
72
+ <script src="${esc(scriptUrl)}"><\/script>
73
+
74
+ <script>
75
+ Scalar.createApiReference('#app', JSON.parse(document.getElementById('app').dataset.config))
76
+ <\/script>
77
+ </body>
78
+ `;
79
+ }
35
80
  return `
36
81
  <!doctype html>
37
82
  <html>
@@ -39,19 +84,12 @@ class OpenAPIReferencePlugin {
39
84
  <meta charset="utf-8" />
40
85
  <meta name="viewport" content="width=device-width, initial-scale=1" />
41
86
  <title>${esc(title)}</title>
87
+ ${cssUrl ? `<link rel="stylesheet" type="text/css" href="${esc(cssUrl)}" />` : ""}
42
88
  ${head}
43
89
  </head>
44
- <body>
45
- <div id="app" data-config="${esc(stringifyJSON(finalConfig))}"></div>
46
-
47
- <script src="${esc(scriptUrl)}"><\/script>
48
-
49
- <script>
50
- Scalar.createApiReference('#app', JSON.parse(document.getElementById('app').dataset.config))
51
- <\/script>
52
- </body>
90
+ ${body}
53
91
  </html>
54
- `;
92
+ `;
55
93
  });
56
94
  }
57
95
  init(options, router) {
@@ -89,7 +127,9 @@ class OpenAPIReferencePlugin {
89
127
  await value(this.docsHead, options2),
90
128
  await value(this.docsScriptUrl, options2),
91
129
  await value(this.docsConfig, options2),
92
- await generateSpec()
130
+ await generateSpec(),
131
+ this.docsProvider,
132
+ await value(this.docsCssUrl, options2)
93
133
  );
94
134
  return {
95
135
  matched: true,
@@ -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, 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,15 @@ 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
+ }
296
362
 
297
363
  class CompositeSchemaConverter {
298
364
  converters;
@@ -324,36 +390,50 @@ class OpenAPIGenerator {
324
390
  * @see {@link https://orpc.unnoq.com/docs/openapi/openapi-specification OpenAPI Specification Docs}
325
391
  */
326
392
  async generate(router, options = {}) {
327
- const exclude = options.exclude ?? (() => false);
393
+ const filter = options.filter ?? (({ contract, path }) => {
394
+ return !(options.exclude?.(contract, path) ?? false);
395
+ });
328
396
  const doc = {
329
397
  ...clone(options),
330
398
  info: options.info ?? { title: "API Reference", version: "0.0.0" },
331
399
  openapi: "3.1.1",
332
- exclude: void 0
400
+ exclude: void 0,
401
+ filter: void 0,
402
+ commonSchemas: void 0
333
403
  };
404
+ const { baseSchemaConvertOptions, undefinedErrorJsonSchema } = await this.#resolveCommonSchemas(doc, options.commonSchemas);
334
405
  const contracts = [];
335
- await resolveContractProcedures({ path: [], router }, ({ contract, path }) => {
336
- if (!exclude(contract, path)) {
337
- contracts.push({ contract, path });
406
+ await resolveContractProcedures({ path: [], router }, (traverseOptions) => {
407
+ if (!value(filter, traverseOptions)) {
408
+ return;
338
409
  }
410
+ contracts.push(traverseOptions);
339
411
  });
340
412
  const errors = [];
341
413
  for (const { contract, path } of contracts) {
342
- const operationId = path.join(".");
414
+ const stringPath = path.join(".");
343
415
  try {
344
416
  const def = contract["~orpc"];
345
417
  const method = toOpenAPIMethod(fallbackContractConfig("defaultMethod", def.route.method));
346
418
  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);
419
+ let operationObjectRef;
420
+ if (def.route.spec !== void 0 && typeof def.route.spec !== "function") {
421
+ operationObjectRef = def.route.spec;
422
+ } else {
423
+ operationObjectRef = {
424
+ operationId: def.route.operationId ?? stringPath,
425
+ summary: def.route.summary,
426
+ description: def.route.description,
427
+ deprecated: def.route.deprecated,
428
+ tags: def.route.tags?.map((tag) => tag)
429
+ };
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);
436
+ }
357
437
  doc.paths ??= {};
358
438
  doc.paths[httpPath] ??= {};
359
439
  doc.paths[httpPath][method] = applyCustomOpenAPIOperation(operationObjectRef, contract);
@@ -362,7 +442,7 @@ class OpenAPIGenerator {
362
442
  throw e;
363
443
  }
364
444
  errors.push(
365
- `[OpenAPIGenerator] Error occurred while generating OpenAPI for procedure at path: ${operationId}
445
+ `[OpenAPIGenerator] Error occurred while generating OpenAPI for procedure at path: ${stringPath}
366
446
  ${e.message}`
367
447
  );
368
448
  }
@@ -376,22 +456,96 @@ ${errors.join("\n\n")}`
376
456
  }
377
457
  return this.serializer.serialize(doc)[0];
378
458
  }
379
- 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) {
380
527
  const method = fallbackContractConfig("defaultMethod", def.route.method);
381
528
  const details = getEventIteratorSchemaDetails(def.inputSchema);
382
529
  if (details) {
383
530
  ref.requestBody = {
384
531
  required: true,
385
532
  content: toOpenAPIEventIteratorContent(
386
- await this.converter.convert(details.yields, { strategy: "input" }),
387
- 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" })
388
535
  )
389
536
  };
390
537
  return;
391
538
  }
392
539
  const dynamicParams = getDynamicParams(def.route.path)?.map((v) => v.name);
393
540
  const inputStructure = fallbackContractConfig("defaultInputStructure", def.route.inputStructure);
394
- 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
+ );
395
549
  if (isAnySchema(schema) && !dynamicParams?.length) {
396
550
  return;
397
551
  }
@@ -413,13 +567,14 @@ ${errors.join("\n\n")}`
413
567
  ref.parameters.push(...toOpenAPIParameters(paramsSchema, "path"));
414
568
  }
415
569
  if (method === "GET") {
416
- if (!isObjectSchema(schema)) {
570
+ const resolvedSchema = resolveOpenAPIJsonSchemaRef(doc, schema);
571
+ if (!isObjectSchema(resolvedSchema)) {
417
572
  throw new OpenAPIGeneratorError(
418
573
  'When method is "GET", input schema must satisfy: object | any | unknown'
419
574
  );
420
575
  }
421
576
  ref.parameters ??= [];
422
- ref.parameters.push(...toOpenAPIParameters(schema, "query"));
577
+ ref.parameters.push(...toOpenAPIParameters(resolvedSchema, "query"));
423
578
  } else {
424
579
  ref.requestBody = {
425
580
  required,
@@ -434,7 +589,8 @@ ${errors.join("\n\n")}`
434
589
  if (!isObjectSchema(schema)) {
435
590
  throw error;
436
591
  }
437
- 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))) {
438
594
  throw new OpenAPIGeneratorError(
439
595
  'When input structure is "detailed" and path has dynamic params, the "params" schema must be an object with all dynamic params as required.'
440
596
  );
@@ -442,12 +598,13 @@ ${errors.join("\n\n")}`
442
598
  for (const from of ["params", "query", "headers"]) {
443
599
  const fromSchema = schema.properties?.[from];
444
600
  if (fromSchema !== void 0) {
445
- if (!isObjectSchema(fromSchema)) {
601
+ const resolvedSchema = resolveOpenAPIJsonSchemaRef(doc, fromSchema);
602
+ if (!isObjectSchema(resolvedSchema)) {
446
603
  throw error;
447
604
  }
448
605
  const parameterIn = from === "params" ? "path" : from === "headers" ? "header" : "query";
449
606
  ref.parameters ??= [];
450
- ref.parameters.push(...toOpenAPIParameters(fromSchema, parameterIn));
607
+ ref.parameters.push(...toOpenAPIParameters(resolvedSchema, parameterIn));
451
608
  }
452
609
  }
453
610
  if (schema.properties?.body !== void 0) {
@@ -457,7 +614,7 @@ ${errors.join("\n\n")}`
457
614
  };
458
615
  }
459
616
  }
460
- async #successResponse(ref, def) {
617
+ async #successResponse(doc, ref, def, baseSchemaConvertOptions) {
461
618
  const outputSchema = def.outputSchema;
462
619
  const status = fallbackContractConfig("defaultSuccessStatus", def.route.successStatus);
463
620
  const description = fallbackContractConfig("defaultSuccessDescription", def.route?.successDescription);
@@ -468,13 +625,20 @@ ${errors.join("\n\n")}`
468
625
  ref.responses[status] = {
469
626
  description,
470
627
  content: toOpenAPIEventIteratorContent(
471
- await this.converter.convert(eventIteratorSchemaDetails.yields, { strategy: "output" }),
472
- 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" })
473
630
  )
474
631
  };
475
632
  return;
476
633
  }
477
- 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
+ );
478
642
  if (outputStructure === "compact") {
479
643
  ref.responses ??= {};
480
644
  ref.responses[status] = {
@@ -501,11 +665,12 @@ ${errors.join("\n\n")}`
501
665
  let schemaStatus;
502
666
  let schemaDescription;
503
667
  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)) {
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)) {
505
670
  throw error;
506
671
  }
507
- schemaStatus = item.properties.status.const;
508
- schemaDescription = item.properties.status.description;
672
+ schemaStatus = statusSchema.const;
673
+ schemaDescription = statusSchema.description;
509
674
  }
510
675
  const itemStatus = schemaStatus ?? status;
511
676
  const itemDescription = schemaDescription ?? description;
@@ -521,16 +686,17 @@ ${errors.join("\n\n")}`
521
686
  description: itemDescription
522
687
  };
523
688
  if (item.properties?.headers !== void 0) {
524
- if (!isObjectSchema(item.properties.headers)) {
689
+ const headersSchema = resolveOpenAPIJsonSchemaRef(doc, item.properties.headers);
690
+ if (!isObjectSchema(headersSchema)) {
525
691
  throw error;
526
692
  }
527
- for (const key in item.properties.headers.properties) {
528
- const headerSchema = item.properties.headers.properties[key];
693
+ for (const key in headersSchema.properties) {
694
+ const headerSchema = headersSchema.properties[key];
529
695
  if (headerSchema !== void 0) {
530
696
  ref.responses[itemStatus].headers ??= {};
531
697
  ref.responses[itemStatus].headers[key] = {
532
698
  schema: toOpenAPISchema(headerSchema),
533
- required: item.properties.headers.required?.includes(key)
699
+ required: item.required?.includes("headers") && headersSchema.required?.includes(key)
534
700
  };
535
701
  }
536
702
  }
@@ -542,7 +708,7 @@ ${errors.join("\n\n")}`
542
708
  }
543
709
  }
544
710
  }
545
- async #errorResponse(ref, def) {
711
+ async #errorResponse(ref, def, baseSchemaConvertOptions, undefinedErrorSchema) {
546
712
  const errorMap = def.errorMap;
547
713
  const errors = {};
548
714
  for (const code in errorMap) {
@@ -552,7 +718,7 @@ ${errors.join("\n\n")}`
552
718
  }
553
719
  const status = fallbackORPCErrorStatus(code, config.status);
554
720
  const message = fallbackORPCErrorMessage(code, config.message);
555
- const [dataRequired, dataSchema] = await this.converter.convert(config.data, { strategy: "output" });
721
+ const [dataRequired, dataSchema] = await this.converter.convert(config.data, { ...baseSchemaConvertOptions, strategy: "output" });
556
722
  errors[status] ??= [];
557
723
  errors[status].push({
558
724
  type: "object",
@@ -574,17 +740,7 @@ ${errors.join("\n\n")}`
574
740
  content: toOpenAPIContent({
575
741
  oneOf: [
576
742
  ...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
- }
743
+ undefinedErrorSchema
588
744
  ]
589
745
  })
590
746
  };
@@ -592,4 +748,4 @@ ${errors.join("\n\n")}`
592
748
  }
593
749
  }
594
750
 
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 };
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 };