@orpc/openapi 0.0.0-next.0ce0926 → 0.0.0-next.0ceddfc

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