@orpc/openapi 0.0.0-next.3d25567 → 0.0.0-next.3d4bea8

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 (31) hide show
  1. package/README.md +7 -2
  2. package/dist/adapters/aws-lambda/index.d.mts +6 -3
  3. package/dist/adapters/aws-lambda/index.d.ts +6 -3
  4. package/dist/adapters/aws-lambda/index.mjs +3 -3
  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 +3 -3
  18. package/dist/index.d.ts +3 -3
  19. package/dist/index.mjs +3 -3
  20. package/dist/plugins/index.d.mts +19 -4
  21. package/dist/plugins/index.d.ts +19 -4
  22. package/dist/plugins/index.mjs +59 -19
  23. package/dist/shared/{openapi.B3hexduL.d.mts → openapi.BfNjg7j9.d.mts} +24 -5
  24. package/dist/shared/{openapi.B3hexduL.d.ts → openapi.BfNjg7j9.d.ts} +24 -5
  25. package/dist/shared/{openapi.-sXpEIAO.mjs → openapi.DIt-Z9W1.mjs} +18 -7
  26. package/dist/shared/{openapi.DrrBsJ0w.mjs → openapi.DrTcell5.mjs} +47 -35
  27. package/dist/shared/openapi.DwaweYRb.d.mts +54 -0
  28. package/dist/shared/openapi.DwaweYRb.d.ts +54 -0
  29. package/package.json +16 -10
  30. package/dist/shared/openapi.BWrlhfev.d.mts +0 -12
  31. package/dist/shared/openapi.BWrlhfev.d.ts +0 -12
@@ -1,11 +1,11 @@
1
1
  import { stringifyJSON, once, value } from '@orpc/shared';
2
- import { O as OpenAPIGenerator } from '../shared/openapi.DrrBsJ0w.mjs';
2
+ import { O as OpenAPIGenerator } from '../shared/openapi.DrTcell5.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,
@@ -1,8 +1,8 @@
1
1
  import { AnySchema, OpenAPI, AnyContractProcedure, AnyContractRouter } from '@orpc/contract';
2
2
  import { StandardOpenAPIJsonSerializerOptions } from '@orpc/openapi-client/standard';
3
- import { AnyProcedure, AnyRouter } from '@orpc/server';
4
- import { Promisable } from '@orpc/shared';
5
- import { JSONSchema } from 'json-schema-typed/draft-2020-12';
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
6
 
7
7
  interface SchemaConverterComponent {
8
8
  allowedStrategies: readonly SchemaConvertOptions['strategy'][];
@@ -33,7 +33,7 @@ interface ConditionalSchemaConverter extends SchemaConverter {
33
33
  }
34
34
  declare class CompositeSchemaConverter implements SchemaConverter {
35
35
  private readonly converters;
36
- constructor(converters: ConditionalSchemaConverter[]);
36
+ constructor(converters: readonly ConditionalSchemaConverter[]);
37
37
  convert(schema: AnySchema | undefined, options: SchemaConvertOptions): Promise<[required: boolean, jsonSchema: JSONSchema]>;
38
38
  }
39
39
 
@@ -44,9 +44,16 @@ interface OpenAPIGeneratorGenerateOptions extends Partial<Omit<OpenAPI.Document,
44
44
  /**
45
45
  * Exclude procedures from the OpenAPI specification.
46
46
  *
47
+ * @deprecated Use `filter` option instead.
47
48
  * @default () => false
48
49
  */
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]>;
50
57
  /**
51
58
  * Common schemas to be used for $ref resolution.
52
59
  */
@@ -78,6 +85,18 @@ interface OpenAPIGeneratorGenerateOptions extends Partial<Omit<OpenAPI.Document,
78
85
  error: 'UndefinedError';
79
86
  schema?: never;
80
87
  }>;
88
+ /**
89
+ * Define a custom JSON schema for the error response body when using
90
+ * type-safe errors. Helps align ORPC error formatting with existing API
91
+ * response standards or conventions.
92
+ *
93
+ * @remarks
94
+ * - Return `null | undefined` to use the default error response body shaper.
95
+ */
96
+ customErrorResponseBodySchema?: Value<JSONSchema | undefined | null, [
97
+ definedErrors: [code: string, defaultMessage: string, dataRequired: boolean, dataSchema: JSONSchema][],
98
+ status: number
99
+ ]>;
81
100
  }
82
101
  /**
83
102
  * The generator that converts oRPC routers/contracts to OpenAPI specifications.
@@ -94,7 +113,7 @@ declare class OpenAPIGenerator {
94
113
  *
95
114
  * @see {@link https://orpc.unnoq.com/docs/openapi/openapi-specification OpenAPI Specification Docs}
96
115
  */
97
- generate(router: AnyContractRouter | AnyRouter, options?: OpenAPIGeneratorGenerateOptions): Promise<OpenAPI.Document>;
116
+ generate(router: AnyContractRouter | AnyRouter, { customErrorResponseBodySchema, commonSchemas, filter: baseFilter, exclude, ...baseDoc }?: OpenAPIGeneratorGenerateOptions): Promise<OpenAPI.Document>;
98
117
  }
99
118
 
100
119
  export { OpenAPIGenerator as b, CompositeSchemaConverter as e };
@@ -1,8 +1,8 @@
1
1
  import { AnySchema, OpenAPI, AnyContractProcedure, AnyContractRouter } from '@orpc/contract';
2
2
  import { StandardOpenAPIJsonSerializerOptions } from '@orpc/openapi-client/standard';
3
- import { AnyProcedure, AnyRouter } from '@orpc/server';
4
- import { Promisable } from '@orpc/shared';
5
- import { JSONSchema } from 'json-schema-typed/draft-2020-12';
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
6
 
7
7
  interface SchemaConverterComponent {
8
8
  allowedStrategies: readonly SchemaConvertOptions['strategy'][];
@@ -33,7 +33,7 @@ interface ConditionalSchemaConverter extends SchemaConverter {
33
33
  }
34
34
  declare class CompositeSchemaConverter implements SchemaConverter {
35
35
  private readonly converters;
36
- constructor(converters: ConditionalSchemaConverter[]);
36
+ constructor(converters: readonly ConditionalSchemaConverter[]);
37
37
  convert(schema: AnySchema | undefined, options: SchemaConvertOptions): Promise<[required: boolean, jsonSchema: JSONSchema]>;
38
38
  }
39
39
 
@@ -44,9 +44,16 @@ interface OpenAPIGeneratorGenerateOptions extends Partial<Omit<OpenAPI.Document,
44
44
  /**
45
45
  * Exclude procedures from the OpenAPI specification.
46
46
  *
47
+ * @deprecated Use `filter` option instead.
47
48
  * @default () => false
48
49
  */
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]>;
50
57
  /**
51
58
  * Common schemas to be used for $ref resolution.
52
59
  */
@@ -78,6 +85,18 @@ interface OpenAPIGeneratorGenerateOptions extends Partial<Omit<OpenAPI.Document,
78
85
  error: 'UndefinedError';
79
86
  schema?: never;
80
87
  }>;
88
+ /**
89
+ * Define a custom JSON schema for the error response body when using
90
+ * type-safe errors. Helps align ORPC error formatting with existing API
91
+ * response standards or conventions.
92
+ *
93
+ * @remarks
94
+ * - Return `null | undefined` to use the default error response body shaper.
95
+ */
96
+ customErrorResponseBodySchema?: Value<JSONSchema | undefined | null, [
97
+ definedErrors: [code: string, defaultMessage: string, dataRequired: boolean, dataSchema: JSONSchema][],
98
+ status: number
99
+ ]>;
81
100
  }
82
101
  /**
83
102
  * The generator that converts oRPC routers/contracts to OpenAPI specifications.
@@ -94,7 +113,7 @@ declare class OpenAPIGenerator {
94
113
  *
95
114
  * @see {@link https://orpc.unnoq.com/docs/openapi/openapi-specification OpenAPI Specification Docs}
96
115
  */
97
- generate(router: AnyContractRouter | AnyRouter, options?: OpenAPIGeneratorGenerateOptions): Promise<OpenAPI.Document>;
116
+ generate(router: AnyContractRouter | AnyRouter, { customErrorResponseBodySchema, commonSchemas, filter: baseFilter, exclude, ...baseDoc }?: OpenAPIGeneratorGenerateOptions): Promise<OpenAPI.Document>;
98
117
  }
99
118
 
100
119
  export { OpenAPIGenerator as b, CompositeSchemaConverter as e };
@@ -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)) {
@@ -170,8 +181,8 @@ class StandardOpenAPIHandler extends StandardHandler {
170
181
  const jsonSerializer = new StandardOpenAPIJsonSerializer(options);
171
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
  }
@@ -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));
@@ -384,35 +389,36 @@ class OpenAPIGenerator {
384
389
  *
385
390
  * @see {@link https://orpc.unnoq.com/docs/openapi/openapi-specification OpenAPI Specification Docs}
386
391
  */
387
- async generate(router, options = {}) {
388
- const exclude = options.exclude ?? (() => false);
392
+ async generate(router, { customErrorResponseBodySchema, commonSchemas, filter: baseFilter, exclude, ...baseDoc } = {}) {
393
+ const filter = baseFilter ?? (({ contract, path }) => {
394
+ return !(exclude?.(contract, path) ?? false);
395
+ });
389
396
  const doc = {
390
- ...clone(options),
391
- info: options.info ?? { title: "API Reference", version: "0.0.0" },
392
- openapi: "3.1.1",
393
- exclude: void 0,
394
- commonSchemas: void 0
397
+ ...clone(baseDoc),
398
+ info: baseDoc.info ?? { title: "API Reference", version: "0.0.0" },
399
+ openapi: "3.1.1"
395
400
  };
396
- const { baseSchemaConvertOptions, undefinedErrorJsonSchema } = await this.#resolveCommonSchemas(doc, options.commonSchemas);
401
+ const { baseSchemaConvertOptions, undefinedErrorJsonSchema } = await this.#resolveCommonSchemas(doc, commonSchemas);
397
402
  const contracts = [];
398
- await resolveContractProcedures({ path: [], router }, ({ contract, path }) => {
399
- if (!exclude(contract, path)) {
400
- contracts.push({ contract, path });
403
+ await resolveContractProcedures({ path: [], router }, (traverseOptions) => {
404
+ if (!value(filter, traverseOptions)) {
405
+ return;
401
406
  }
407
+ contracts.push(traverseOptions);
402
408
  });
403
409
  const errors = [];
404
410
  for (const { contract, path } of contracts) {
405
- const operationId = path.join(".");
411
+ const stringPath = path.join(".");
406
412
  try {
407
413
  const def = contract["~orpc"];
408
414
  const method = toOpenAPIMethod(fallbackContractConfig("defaultMethod", def.route.method));
409
415
  const httpPath = toOpenAPIPath(def.route.path ?? toHttpPath(path));
410
416
  let operationObjectRef;
411
- if (def.route.spec !== void 0) {
417
+ if (def.route.spec !== void 0 && typeof def.route.spec !== "function") {
412
418
  operationObjectRef = def.route.spec;
413
419
  } else {
414
420
  operationObjectRef = {
415
- operationId,
421
+ operationId: def.route.operationId ?? stringPath,
416
422
  summary: def.route.summary,
417
423
  description: def.route.description,
418
424
  deprecated: def.route.deprecated,
@@ -420,7 +426,10 @@ class OpenAPIGenerator {
420
426
  };
421
427
  await this.#request(doc, operationObjectRef, def, baseSchemaConvertOptions);
422
428
  await this.#successResponse(doc, operationObjectRef, def, baseSchemaConvertOptions);
423
- await this.#errorResponse(operationObjectRef, def, baseSchemaConvertOptions, undefinedErrorJsonSchema);
429
+ await this.#errorResponse(operationObjectRef, def, baseSchemaConvertOptions, undefinedErrorJsonSchema, customErrorResponseBodySchema);
430
+ }
431
+ if (typeof def.route.spec === "function") {
432
+ operationObjectRef = def.route.spec(operationObjectRef);
424
433
  }
425
434
  doc.paths ??= {};
426
435
  doc.paths[httpPath] ??= {};
@@ -430,7 +439,7 @@ class OpenAPIGenerator {
430
439
  throw e;
431
440
  }
432
441
  errors.push(
433
- `[OpenAPIGenerator] Error occurred while generating OpenAPI for procedure at path: ${operationId}
442
+ `[OpenAPIGenerator] Error occurred while generating OpenAPI for procedure at path: ${stringPath}
434
443
  ${e.message}`
435
444
  );
436
445
  }
@@ -555,13 +564,14 @@ ${errors.join("\n\n")}`
555
564
  ref.parameters.push(...toOpenAPIParameters(paramsSchema, "path"));
556
565
  }
557
566
  if (method === "GET") {
558
- if (!isObjectSchema(schema)) {
567
+ const resolvedSchema = resolveOpenAPIJsonSchemaRef(doc, schema);
568
+ if (!isObjectSchema(resolvedSchema)) {
559
569
  throw new OpenAPIGeneratorError(
560
570
  'When method is "GET", input schema must satisfy: object | any | unknown'
561
571
  );
562
572
  }
563
573
  ref.parameters ??= [];
564
- ref.parameters.push(...toOpenAPIParameters(schema, "query"));
574
+ ref.parameters.push(...toOpenAPIParameters(resolvedSchema, "query"));
565
575
  } else {
566
576
  ref.requestBody = {
567
577
  required,
@@ -695,38 +705,40 @@ ${errors.join("\n\n")}`
695
705
  }
696
706
  }
697
707
  }
698
- async #errorResponse(ref, def, baseSchemaConvertOptions, undefinedErrorSchema) {
708
+ async #errorResponse(ref, def, baseSchemaConvertOptions, undefinedErrorSchema, customErrorResponseBodySchema) {
699
709
  const errorMap = def.errorMap;
700
- const errors = {};
710
+ const errorResponsesByStatus = {};
701
711
  for (const code in errorMap) {
702
712
  const config = errorMap[code];
703
713
  if (!config) {
704
714
  continue;
705
715
  }
706
716
  const status = fallbackORPCErrorStatus(code, config.status);
707
- const message = fallbackORPCErrorMessage(code, config.message);
717
+ const defaultMessage = fallbackORPCErrorMessage(code, config.message);
718
+ errorResponsesByStatus[status] ??= { status, definedErrorDefinitions: [], errorSchemaVariants: [] };
708
719
  const [dataRequired, dataSchema] = await this.converter.convert(config.data, { ...baseSchemaConvertOptions, strategy: "output" });
709
- errors[status] ??= [];
710
- errors[status].push({
720
+ errorResponsesByStatus[status].definedErrorDefinitions.push([code, defaultMessage, dataRequired, dataSchema]);
721
+ errorResponsesByStatus[status].errorSchemaVariants.push({
711
722
  type: "object",
712
723
  properties: {
713
724
  defined: { const: true },
714
725
  code: { const: code },
715
726
  status: { const: status },
716
- message: { type: "string", default: message },
727
+ message: { type: "string", default: defaultMessage },
717
728
  data: dataSchema
718
729
  },
719
730
  required: dataRequired ? ["defined", "code", "status", "message", "data"] : ["defined", "code", "status", "message"]
720
731
  });
721
732
  }
722
733
  ref.responses ??= {};
723
- for (const status in errors) {
724
- const schemas = errors[status];
725
- ref.responses[status] = {
726
- description: status,
727
- content: toOpenAPIContent({
734
+ for (const statusString in errorResponsesByStatus) {
735
+ const errorResponse = errorResponsesByStatus[statusString];
736
+ const customBodySchema = value(customErrorResponseBodySchema, errorResponse.definedErrorDefinitions, errorResponse.status);
737
+ ref.responses[statusString] = {
738
+ description: statusString,
739
+ content: toOpenAPIContent(customBodySchema ?? {
728
740
  oneOf: [
729
- ...schemas,
741
+ ...errorResponse.errorSchemaVariants,
730
742
  undefinedErrorSchema
731
743
  ]
732
744
  })
@@ -0,0 +1,54 @@
1
+ import { StandardOpenAPISerializer, StandardOpenAPIJsonSerializerOptions, StandardBracketNotationSerializerOptions } from '@orpc/openapi-client/standard';
2
+ import { AnyProcedure, TraverseContractProcedureCallbackOptions, AnyRouter, Context, Router } from '@orpc/server';
3
+ import { StandardCodec, StandardParams, StandardMatcher, StandardMatchResult, StandardHandlerOptions, StandardHandler } from '@orpc/server/standard';
4
+ import { ORPCError, HTTPPath } from '@orpc/client';
5
+ import { StandardLazyRequest, StandardResponse } from '@orpc/standard-server';
6
+ import { Value } from '@orpc/shared';
7
+
8
+ interface StandardOpenAPICodecOptions {
9
+ /**
10
+ * Customize how an ORPC error is encoded into a response body.
11
+ * Use this if your API needs a different error output structure.
12
+ *
13
+ * @remarks
14
+ * - Return `null | undefined` to fallback to default behavior
15
+ *
16
+ * @default ((e) => e.toJSON())
17
+ */
18
+ customErrorResponseBodyEncoder?: (error: ORPCError<any, any>) => unknown;
19
+ }
20
+ declare class StandardOpenAPICodec implements StandardCodec {
21
+ #private;
22
+ private readonly serializer;
23
+ private readonly customErrorResponseBodyEncoder;
24
+ constructor(serializer: StandardOpenAPISerializer, options?: StandardOpenAPICodecOptions);
25
+ decode(request: StandardLazyRequest, params: StandardParams | undefined, procedure: AnyProcedure): Promise<unknown>;
26
+ encode(output: unknown, procedure: AnyProcedure): StandardResponse;
27
+ encodeError(error: ORPCError<any, any>): StandardResponse;
28
+ }
29
+
30
+ interface StandardOpenAPIMatcherOptions {
31
+ /**
32
+ * Filter procedures. Return `false` to exclude a procedure from matching.
33
+ *
34
+ * @default true
35
+ */
36
+ filter?: Value<boolean, [options: TraverseContractProcedureCallbackOptions]>;
37
+ }
38
+ declare class StandardOpenAPIMatcher implements StandardMatcher {
39
+ private readonly filter;
40
+ private readonly tree;
41
+ private pendingRouters;
42
+ constructor(options?: StandardOpenAPIMatcherOptions);
43
+ init(router: AnyRouter, path?: readonly string[]): void;
44
+ match(method: string, pathname: HTTPPath): Promise<StandardMatchResult>;
45
+ }
46
+
47
+ interface StandardOpenAPIHandlerOptions<T extends Context> extends StandardHandlerOptions<T>, StandardOpenAPIJsonSerializerOptions, StandardBracketNotationSerializerOptions, StandardOpenAPIMatcherOptions, StandardOpenAPICodecOptions {
48
+ }
49
+ declare class StandardOpenAPIHandler<T extends Context> extends StandardHandler<T> {
50
+ constructor(router: Router<any, T>, options: NoInfer<StandardOpenAPIHandlerOptions<T>>);
51
+ }
52
+
53
+ export { StandardOpenAPICodec as a, StandardOpenAPIHandler as c, StandardOpenAPIMatcher as e };
54
+ export type { StandardOpenAPICodecOptions as S, StandardOpenAPIHandlerOptions as b, StandardOpenAPIMatcherOptions as d };
@@ -0,0 +1,54 @@
1
+ import { StandardOpenAPISerializer, StandardOpenAPIJsonSerializerOptions, StandardBracketNotationSerializerOptions } from '@orpc/openapi-client/standard';
2
+ import { AnyProcedure, TraverseContractProcedureCallbackOptions, AnyRouter, Context, Router } from '@orpc/server';
3
+ import { StandardCodec, StandardParams, StandardMatcher, StandardMatchResult, StandardHandlerOptions, StandardHandler } from '@orpc/server/standard';
4
+ import { ORPCError, HTTPPath } from '@orpc/client';
5
+ import { StandardLazyRequest, StandardResponse } from '@orpc/standard-server';
6
+ import { Value } from '@orpc/shared';
7
+
8
+ interface StandardOpenAPICodecOptions {
9
+ /**
10
+ * Customize how an ORPC error is encoded into a response body.
11
+ * Use this if your API needs a different error output structure.
12
+ *
13
+ * @remarks
14
+ * - Return `null | undefined` to fallback to default behavior
15
+ *
16
+ * @default ((e) => e.toJSON())
17
+ */
18
+ customErrorResponseBodyEncoder?: (error: ORPCError<any, any>) => unknown;
19
+ }
20
+ declare class StandardOpenAPICodec implements StandardCodec {
21
+ #private;
22
+ private readonly serializer;
23
+ private readonly customErrorResponseBodyEncoder;
24
+ constructor(serializer: StandardOpenAPISerializer, options?: StandardOpenAPICodecOptions);
25
+ decode(request: StandardLazyRequest, params: StandardParams | undefined, procedure: AnyProcedure): Promise<unknown>;
26
+ encode(output: unknown, procedure: AnyProcedure): StandardResponse;
27
+ encodeError(error: ORPCError<any, any>): StandardResponse;
28
+ }
29
+
30
+ interface StandardOpenAPIMatcherOptions {
31
+ /**
32
+ * Filter procedures. Return `false` to exclude a procedure from matching.
33
+ *
34
+ * @default true
35
+ */
36
+ filter?: Value<boolean, [options: TraverseContractProcedureCallbackOptions]>;
37
+ }
38
+ declare class StandardOpenAPIMatcher implements StandardMatcher {
39
+ private readonly filter;
40
+ private readonly tree;
41
+ private pendingRouters;
42
+ constructor(options?: StandardOpenAPIMatcherOptions);
43
+ init(router: AnyRouter, path?: readonly string[]): void;
44
+ match(method: string, pathname: HTTPPath): Promise<StandardMatchResult>;
45
+ }
46
+
47
+ interface StandardOpenAPIHandlerOptions<T extends Context> extends StandardHandlerOptions<T>, StandardOpenAPIJsonSerializerOptions, StandardBracketNotationSerializerOptions, StandardOpenAPIMatcherOptions, StandardOpenAPICodecOptions {
48
+ }
49
+ declare class StandardOpenAPIHandler<T extends Context> extends StandardHandler<T> {
50
+ constructor(router: Router<any, T>, options: NoInfer<StandardOpenAPIHandlerOptions<T>>);
51
+ }
52
+
53
+ export { StandardOpenAPICodec as a, StandardOpenAPIHandler as c, StandardOpenAPIMatcher as e };
54
+ export type { StandardOpenAPICodecOptions as S, StandardOpenAPIHandlerOptions as b, StandardOpenAPIMatcherOptions as d };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@orpc/openapi",
3
3
  "type": "module",
4
- "version": "0.0.0-next.3d25567",
4
+ "version": "0.0.0-next.3d4bea8",
5
5
  "license": "MIT",
6
6
  "homepage": "https://orpc.unnoq.com",
7
7
  "repository": {
@@ -39,6 +39,11 @@
39
39
  "import": "./dist/adapters/node/index.mjs",
40
40
  "default": "./dist/adapters/node/index.mjs"
41
41
  },
42
+ "./fastify": {
43
+ "types": "./dist/adapters/fastify/index.d.mts",
44
+ "import": "./dist/adapters/fastify/index.mjs",
45
+ "default": "./dist/adapters/fastify/index.mjs"
46
+ },
42
47
  "./aws-lambda": {
43
48
  "types": "./dist/adapters/aws-lambda/index.d.mts",
44
49
  "import": "./dist/adapters/aws-lambda/index.mjs",
@@ -49,17 +54,18 @@
49
54
  "dist"
50
55
  ],
51
56
  "dependencies": {
52
- "json-schema-typed": "^8.0.1",
53
- "rou3": "^0.7.3",
54
- "@orpc/client": "0.0.0-next.3d25567",
55
- "@orpc/contract": "0.0.0-next.3d25567",
56
- "@orpc/openapi-client": "0.0.0-next.3d25567",
57
- "@orpc/standard-server": "0.0.0-next.3d25567",
58
- "@orpc/server": "0.0.0-next.3d25567",
59
- "@orpc/shared": "0.0.0-next.3d25567"
57
+ "rou3": "^0.7.9",
58
+ "@orpc/client": "0.0.0-next.3d4bea8",
59
+ "@orpc/contract": "0.0.0-next.3d4bea8",
60
+ "@orpc/openapi-client": "0.0.0-next.3d4bea8",
61
+ "@orpc/server": "0.0.0-next.3d4bea8",
62
+ "@orpc/interop": "0.0.0-next.3d4bea8",
63
+ "@orpc/shared": "0.0.0-next.3d4bea8",
64
+ "@orpc/standard-server": "0.0.0-next.3d4bea8"
60
65
  },
61
66
  "devDependencies": {
62
- "zod": "^3.25.74"
67
+ "fastify": "^5.6.1",
68
+ "zod": "^4.1.12"
63
69
  },
64
70
  "scripts": {
65
71
  "build": "unbuild",