@orpc/openapi 0.0.0-next.f7af1c4 → 0.0.0-next.f8670e3
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.
- package/README.md +125 -12
- package/dist/adapters/aws-lambda/index.d.mts +4 -3
- package/dist/adapters/aws-lambda/index.d.ts +4 -3
- package/dist/adapters/aws-lambda/index.mjs +1 -1
- package/dist/adapters/fastify/index.d.mts +23 -0
- package/dist/adapters/fastify/index.d.ts +23 -0
- package/dist/adapters/fastify/index.mjs +18 -0
- package/dist/adapters/fetch/index.d.mts +4 -3
- package/dist/adapters/fetch/index.d.ts +4 -3
- package/dist/adapters/fetch/index.mjs +1 -1
- package/dist/adapters/node/index.d.mts +4 -3
- package/dist/adapters/node/index.d.ts +4 -3
- package/dist/adapters/node/index.mjs +1 -1
- package/dist/adapters/standard/index.d.mts +7 -16
- package/dist/adapters/standard/index.d.ts +7 -16
- package/dist/adapters/standard/index.mjs +1 -1
- package/dist/index.d.mts +11 -5
- package/dist/index.d.ts +11 -5
- package/dist/index.mjs +3 -3
- package/dist/plugins/index.d.mts +4 -2
- package/dist/plugins/index.d.ts +4 -2
- package/dist/plugins/index.mjs +21 -12
- package/dist/shared/{openapi.CfjfVeBJ.d.mts → openapi.BGy4N6eR.d.mts} +16 -4
- package/dist/shared/{openapi.CfjfVeBJ.d.ts → openapi.BGy4N6eR.d.ts} +16 -4
- package/dist/shared/{openapi.BlSv9FKY.mjs → openapi.CoREqFh3.mjs} +143 -41
- package/dist/shared/{openapi.BVXcB0u4.mjs → openapi.DIt-Z9W1.mjs} +6 -3
- package/dist/shared/openapi.DwaweYRb.d.mts +54 -0
- package/dist/shared/openapi.DwaweYRb.d.ts +54 -0
- package/package.json +19 -13
- package/dist/shared/openapi.CQmjvnb0.d.mts +0 -31
- package/dist/shared/openapi.CQmjvnb0.d.ts +0 -31
package/dist/index.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { c as customOpenAPIOperation } from './shared/openapi.
|
|
2
|
-
export { C as CompositeSchemaConverter, L as LOGIC_KEYWORDS, O as OpenAPIGenerator, a as applyCustomOpenAPIOperation,
|
|
1
|
+
import { c as customOpenAPIOperation } from './shared/openapi.CoREqFh3.mjs';
|
|
2
|
+
export { C as CompositeSchemaConverter, L as LOGIC_KEYWORDS, O as OpenAPIGenerator, a as applyCustomOpenAPIOperation, o as applySchemaOptionality, h as checkParamsSchema, q as expandArrayableSchema, p as expandUnionSchema, n as filterSchemaBranches, g as getCustomOpenAPIOperation, l as isAnySchema, j as isFileSchema, k as isObjectSchema, u as isPrimitiveSchema, r as resolveOpenAPIJsonSchemaRef, m as separateObjectSchema, s as simplifyComposedObjectJsonSchemasAndRefs, d as toOpenAPIContent, e as toOpenAPIEventIteratorContent, b as toOpenAPIMethod, f as toOpenAPIParameters, t as toOpenAPIPath, i as toOpenAPISchema } from './shared/openapi.CoREqFh3.mjs';
|
|
3
3
|
import { createORPCErrorFromJson } from '@orpc/client';
|
|
4
4
|
import { StandardOpenAPISerializer, StandardOpenAPIJsonSerializer, StandardBracketNotationSerializer } from '@orpc/openapi-client/standard';
|
|
5
5
|
import { ORPCError, createRouterClient } from '@orpc/server';
|
|
6
6
|
import { resolveMaybeOptionalOptions } from '@orpc/shared';
|
|
7
|
-
export { ContentEncoding as JSONSchemaContentEncoding, Format as JSONSchemaFormat, TypeName as JSONSchemaTypeName } from '
|
|
7
|
+
export { ContentEncoding as JSONSchemaContentEncoding, Format as JSONSchemaFormat, TypeName as JSONSchemaTypeName } from 'json-schema-typed/draft-2020-12';
|
|
8
8
|
import '@orpc/client/standard';
|
|
9
9
|
import '@orpc/contract';
|
|
10
10
|
|
package/dist/plugins/index.d.mts
CHANGED
|
@@ -2,9 +2,9 @@ import { OpenAPI } from '@orpc/contract';
|
|
|
2
2
|
import { Context, HTTPPath, Router } from '@orpc/server';
|
|
3
3
|
import { StandardHandlerInterceptorOptions, StandardHandlerPlugin, StandardHandlerOptions } from '@orpc/server/standard';
|
|
4
4
|
import { Value, Promisable } from '@orpc/shared';
|
|
5
|
-
import { O as OpenAPIGeneratorOptions, a as OpenAPIGeneratorGenerateOptions } from '../shared/openapi.
|
|
5
|
+
import { O as OpenAPIGeneratorOptions, a as OpenAPIGeneratorGenerateOptions } from '../shared/openapi.BGy4N6eR.mjs';
|
|
6
6
|
import '@orpc/openapi-client/standard';
|
|
7
|
-
import '
|
|
7
|
+
import 'json-schema-typed/draft-2020-12';
|
|
8
8
|
|
|
9
9
|
interface OpenAPIReferencePluginOptions<T extends Context> extends OpenAPIGeneratorOptions {
|
|
10
10
|
/**
|
|
@@ -43,6 +43,8 @@ interface OpenAPIReferencePluginOptions<T extends Context> extends OpenAPIGenera
|
|
|
43
43
|
/**
|
|
44
44
|
* HTML to inject into the <head> of the docs page.
|
|
45
45
|
*
|
|
46
|
+
* @warning This is not escaped special characters, so must be used with caution to avoid XSS vulnerabilities.
|
|
47
|
+
*
|
|
46
48
|
* @default ''
|
|
47
49
|
*/
|
|
48
50
|
docsHead?: Value<Promisable<string>, [StandardHandlerInterceptorOptions<T>]>;
|
package/dist/plugins/index.d.ts
CHANGED
|
@@ -2,9 +2,9 @@ import { OpenAPI } from '@orpc/contract';
|
|
|
2
2
|
import { Context, HTTPPath, Router } from '@orpc/server';
|
|
3
3
|
import { StandardHandlerInterceptorOptions, StandardHandlerPlugin, StandardHandlerOptions } from '@orpc/server/standard';
|
|
4
4
|
import { Value, Promisable } from '@orpc/shared';
|
|
5
|
-
import { O as OpenAPIGeneratorOptions, a as OpenAPIGeneratorGenerateOptions } from '../shared/openapi.
|
|
5
|
+
import { O as OpenAPIGeneratorOptions, a as OpenAPIGeneratorGenerateOptions } from '../shared/openapi.BGy4N6eR.js';
|
|
6
6
|
import '@orpc/openapi-client/standard';
|
|
7
|
-
import '
|
|
7
|
+
import 'json-schema-typed/draft-2020-12';
|
|
8
8
|
|
|
9
9
|
interface OpenAPIReferencePluginOptions<T extends Context> extends OpenAPIGeneratorOptions {
|
|
10
10
|
/**
|
|
@@ -43,6 +43,8 @@ interface OpenAPIReferencePluginOptions<T extends Context> extends OpenAPIGenera
|
|
|
43
43
|
/**
|
|
44
44
|
* HTML to inject into the <head> of the docs page.
|
|
45
45
|
*
|
|
46
|
+
* @warning This is not escaped special characters, so must be used with caution to avoid XSS vulnerabilities.
|
|
47
|
+
*
|
|
46
48
|
* @default ''
|
|
47
49
|
*/
|
|
48
50
|
docsHead?: Value<Promisable<string>, [StandardHandlerInterceptorOptions<T>]>;
|
package/dist/plugins/index.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { stringifyJSON, once, value } from '@orpc/shared';
|
|
2
|
-
import { O as OpenAPIGenerator } from '../shared/openapi.
|
|
2
|
+
import { O as OpenAPIGenerator } from '../shared/openapi.CoREqFh3.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 '
|
|
8
|
+
import 'json-schema-typed/draft-2020-12';
|
|
9
9
|
|
|
10
10
|
class OpenAPIReferencePlugin {
|
|
11
11
|
generator;
|
|
@@ -30,7 +30,8 @@ class OpenAPIReferencePlugin {
|
|
|
30
30
|
this.docsHead = options.docsHead ?? "";
|
|
31
31
|
this.specPath = options.specPath ?? "/spec.json";
|
|
32
32
|
this.generator = new OpenAPIGenerator(options);
|
|
33
|
-
const
|
|
33
|
+
const escapeHtmlEntities = (s) => s.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
34
|
+
const escapeJsonForHtml = (obj) => stringifyJSON(obj).replace(/&/g, "\\u0026").replace(/'/g, "\\u0027").replace(/</g, "\\u003C").replace(/>/g, "\\u003E").replace(/\//g, "\\u002F");
|
|
34
35
|
this.renderDocsHtml = options.renderDocsHtml ?? ((specUrl, title, head, scriptUrl, config, spec, docsProvider, cssUrl) => {
|
|
35
36
|
let body;
|
|
36
37
|
if (docsProvider === "swagger") {
|
|
@@ -51,11 +52,15 @@ class OpenAPIReferencePlugin {
|
|
|
51
52
|
<body>
|
|
52
53
|
<div id="app"></div>
|
|
53
54
|
|
|
54
|
-
<script src="${
|
|
55
|
+
<script src="${escapeHtmlEntities(scriptUrl)}"><\/script>
|
|
55
56
|
|
|
57
|
+
<!-- IMPORTANT: assign to a variable first to prevent ), ( in values breaking the call expression. -->
|
|
58
|
+
<!-- IMPORTANT: escapeJsonForHtml ensures <, > cannot terminate the <\/script> tag prematurely. -->
|
|
56
59
|
<script>
|
|
60
|
+
const swaggerConfig = ${escapeJsonForHtml(swaggerConfig).replace(/"(SwaggerUIBundle\.[^"]+)"/g, "$1")}
|
|
61
|
+
|
|
57
62
|
window.onload = () => {
|
|
58
|
-
window.ui = SwaggerUIBundle(
|
|
63
|
+
window.ui = SwaggerUIBundle(swaggerConfig)
|
|
59
64
|
}
|
|
60
65
|
<\/script>
|
|
61
66
|
</body>
|
|
@@ -67,12 +72,16 @@ class OpenAPIReferencePlugin {
|
|
|
67
72
|
};
|
|
68
73
|
body = `
|
|
69
74
|
<body>
|
|
70
|
-
<div id="app"
|
|
71
|
-
|
|
72
|
-
<script src="${
|
|
73
|
-
|
|
75
|
+
<div id="app"></div>
|
|
76
|
+
|
|
77
|
+
<script src="${escapeHtmlEntities(scriptUrl)}"><\/script>
|
|
78
|
+
|
|
79
|
+
<!-- IMPORTANT: assign to a variable first to prevent ), ( in values breaking the call expression. -->
|
|
80
|
+
<!-- IMPORTANT: escapeJsonForHtml ensures <, > cannot terminate the <\/script> tag prematurely. -->
|
|
74
81
|
<script>
|
|
75
|
-
|
|
82
|
+
const scalarConfig = ${escapeJsonForHtml(scalarConfig)}
|
|
83
|
+
|
|
84
|
+
Scalar.createApiReference('#app', scalarConfig)
|
|
76
85
|
<\/script>
|
|
77
86
|
</body>
|
|
78
87
|
`;
|
|
@@ -83,8 +92,8 @@ class OpenAPIReferencePlugin {
|
|
|
83
92
|
<head>
|
|
84
93
|
<meta charset="utf-8" />
|
|
85
94
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
86
|
-
<title>${
|
|
87
|
-
${cssUrl ? `<link rel="stylesheet" type="text/css" href="${
|
|
95
|
+
<title>${escapeHtmlEntities(title)}</title>
|
|
96
|
+
${cssUrl ? `<link rel="stylesheet" type="text/css" href="${escapeHtmlEntities(cssUrl)}" />` : ""}
|
|
88
97
|
${head}
|
|
89
98
|
</head>
|
|
90
99
|
${body}
|
|
@@ -2,7 +2,7 @@ import { AnySchema, OpenAPI, AnyContractProcedure, AnyContractRouter } from '@or
|
|
|
2
2
|
import { StandardOpenAPIJsonSerializerOptions } from '@orpc/openapi-client/standard';
|
|
3
3
|
import { AnyProcedure, TraverseContractProcedureCallbackOptions, AnyRouter } from '@orpc/server';
|
|
4
4
|
import { Promisable, Value } from '@orpc/shared';
|
|
5
|
-
import { JSONSchema } from '
|
|
5
|
+
import { JSONSchema } from 'json-schema-typed/draft-2020-12';
|
|
6
6
|
|
|
7
7
|
interface SchemaConverterComponent {
|
|
8
8
|
allowedStrategies: readonly SchemaConvertOptions['strategy'][];
|
|
@@ -85,11 +85,23 @@ interface OpenAPIGeneratorGenerateOptions extends Partial<Omit<OpenAPI.Document,
|
|
|
85
85
|
error: 'UndefinedError';
|
|
86
86
|
schema?: never;
|
|
87
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
|
+
]>;
|
|
88
100
|
}
|
|
89
101
|
/**
|
|
90
102
|
* The generator that converts oRPC routers/contracts to OpenAPI specifications.
|
|
91
103
|
*
|
|
92
|
-
* @see {@link https://orpc.
|
|
104
|
+
* @see {@link https://orpc.dev/docs/openapi/openapi-specification OpenAPI Specification Docs}
|
|
93
105
|
*/
|
|
94
106
|
declare class OpenAPIGenerator {
|
|
95
107
|
#private;
|
|
@@ -99,9 +111,9 @@ declare class OpenAPIGenerator {
|
|
|
99
111
|
/**
|
|
100
112
|
* Generates OpenAPI specifications from oRPC routers/contracts.
|
|
101
113
|
*
|
|
102
|
-
* @see {@link https://orpc.
|
|
114
|
+
* @see {@link https://orpc.dev/docs/openapi/openapi-specification OpenAPI Specification Docs}
|
|
103
115
|
*/
|
|
104
|
-
generate(router: AnyContractRouter | AnyRouter,
|
|
116
|
+
generate(router: AnyContractRouter | AnyRouter, { customErrorResponseBodySchema, commonSchemas, filter: baseFilter, exclude, ...baseDoc }?: OpenAPIGeneratorGenerateOptions): Promise<OpenAPI.Document>;
|
|
105
117
|
}
|
|
106
118
|
|
|
107
119
|
export { OpenAPIGenerator as b, CompositeSchemaConverter as e };
|
|
@@ -2,7 +2,7 @@ import { AnySchema, OpenAPI, AnyContractProcedure, AnyContractRouter } from '@or
|
|
|
2
2
|
import { StandardOpenAPIJsonSerializerOptions } from '@orpc/openapi-client/standard';
|
|
3
3
|
import { AnyProcedure, TraverseContractProcedureCallbackOptions, AnyRouter } from '@orpc/server';
|
|
4
4
|
import { Promisable, Value } from '@orpc/shared';
|
|
5
|
-
import { JSONSchema } from '
|
|
5
|
+
import { JSONSchema } from 'json-schema-typed/draft-2020-12';
|
|
6
6
|
|
|
7
7
|
interface SchemaConverterComponent {
|
|
8
8
|
allowedStrategies: readonly SchemaConvertOptions['strategy'][];
|
|
@@ -85,11 +85,23 @@ interface OpenAPIGeneratorGenerateOptions extends Partial<Omit<OpenAPI.Document,
|
|
|
85
85
|
error: 'UndefinedError';
|
|
86
86
|
schema?: never;
|
|
87
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
|
+
]>;
|
|
88
100
|
}
|
|
89
101
|
/**
|
|
90
102
|
* The generator that converts oRPC routers/contracts to OpenAPI specifications.
|
|
91
103
|
*
|
|
92
|
-
* @see {@link https://orpc.
|
|
104
|
+
* @see {@link https://orpc.dev/docs/openapi/openapi-specification OpenAPI Specification Docs}
|
|
93
105
|
*/
|
|
94
106
|
declare class OpenAPIGenerator {
|
|
95
107
|
#private;
|
|
@@ -99,9 +111,9 @@ declare class OpenAPIGenerator {
|
|
|
99
111
|
/**
|
|
100
112
|
* Generates OpenAPI specifications from oRPC routers/contracts.
|
|
101
113
|
*
|
|
102
|
-
* @see {@link https://orpc.
|
|
114
|
+
* @see {@link https://orpc.dev/docs/openapi/openapi-specification OpenAPI Specification Docs}
|
|
103
115
|
*/
|
|
104
|
-
generate(router: AnyContractRouter | AnyRouter,
|
|
116
|
+
generate(router: AnyContractRouter | AnyRouter, { customErrorResponseBodySchema, commonSchemas, filter: baseFilter, exclude, ...baseDoc }?: OpenAPIGeneratorGenerateOptions): Promise<OpenAPI.Document>;
|
|
105
117
|
}
|
|
106
118
|
|
|
107
119
|
export { OpenAPIGenerator as b, CompositeSchemaConverter as e };
|
|
@@ -4,7 +4,7 @@ import { fallbackContractConfig, getEventIteratorSchemaDetails } from '@orpc/con
|
|
|
4
4
|
import { standardizeHTTPPath, StandardOpenAPIJsonSerializer, getDynamicParams } from '@orpc/openapi-client/standard';
|
|
5
5
|
import { isProcedure, resolveContractProcedures } from '@orpc/server';
|
|
6
6
|
import { isObject, stringifyJSON, findDeepMatches, toArray, clone, value } from '@orpc/shared';
|
|
7
|
-
import { TypeName } from '
|
|
7
|
+
import { TypeName } from 'json-schema-typed/draft-2020-12';
|
|
8
8
|
|
|
9
9
|
const OPERATION_EXTENDER_SYMBOL = Symbol("ORPC_OPERATION_EXTENDER");
|
|
10
10
|
function customOpenAPIOperation(o, extend) {
|
|
@@ -359,6 +359,107 @@ function resolveOpenAPIJsonSchemaRef(doc, schema) {
|
|
|
359
359
|
const resolved = doc.components?.schemas?.[name];
|
|
360
360
|
return resolved ?? schema;
|
|
361
361
|
}
|
|
362
|
+
function simplifyComposedObjectJsonSchemasAndRefs(schema, doc) {
|
|
363
|
+
if (doc) {
|
|
364
|
+
schema = resolveOpenAPIJsonSchemaRef(doc, schema);
|
|
365
|
+
}
|
|
366
|
+
if (typeof schema !== "object" || !schema.anyOf && !schema.oneOf && !schema.allOf) {
|
|
367
|
+
return schema;
|
|
368
|
+
}
|
|
369
|
+
const unionSchemas = [
|
|
370
|
+
...toArray(schema.anyOf?.map((s) => simplifyComposedObjectJsonSchemasAndRefs(s, doc))),
|
|
371
|
+
...toArray(schema.oneOf?.map((s) => simplifyComposedObjectJsonSchemasAndRefs(s, doc)))
|
|
372
|
+
];
|
|
373
|
+
const objectUnionSchemas = [];
|
|
374
|
+
for (const u of unionSchemas) {
|
|
375
|
+
if (!isObjectSchema(u)) {
|
|
376
|
+
return schema;
|
|
377
|
+
}
|
|
378
|
+
objectUnionSchemas.push(u);
|
|
379
|
+
}
|
|
380
|
+
const mergedUnionPropertyMap = /* @__PURE__ */ new Map();
|
|
381
|
+
for (const u of objectUnionSchemas) {
|
|
382
|
+
if (u.properties) {
|
|
383
|
+
for (const [key, value] of Object.entries(u.properties)) {
|
|
384
|
+
let entry = mergedUnionPropertyMap.get(key);
|
|
385
|
+
if (!entry) {
|
|
386
|
+
const required = objectUnionSchemas.every((s) => s.required?.includes(key));
|
|
387
|
+
entry = { required, schemas: [] };
|
|
388
|
+
mergedUnionPropertyMap.set(key, entry);
|
|
389
|
+
}
|
|
390
|
+
entry.schemas.push(value);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
const intersectionSchemas = toArray(schema.allOf?.map((s) => simplifyComposedObjectJsonSchemasAndRefs(s, doc)));
|
|
395
|
+
const objectIntersectionSchemas = [];
|
|
396
|
+
for (const u of intersectionSchemas) {
|
|
397
|
+
if (!isObjectSchema(u)) {
|
|
398
|
+
return schema;
|
|
399
|
+
}
|
|
400
|
+
objectIntersectionSchemas.push(u);
|
|
401
|
+
}
|
|
402
|
+
if (isObjectSchema(schema)) {
|
|
403
|
+
objectIntersectionSchemas.push(schema);
|
|
404
|
+
}
|
|
405
|
+
const mergedInteractionPropertyMap = /* @__PURE__ */ new Map();
|
|
406
|
+
for (const u of objectIntersectionSchemas) {
|
|
407
|
+
if (u.properties) {
|
|
408
|
+
for (const [key, value] of Object.entries(u.properties)) {
|
|
409
|
+
let entry = mergedInteractionPropertyMap.get(key);
|
|
410
|
+
if (!entry) {
|
|
411
|
+
const required = objectIntersectionSchemas.some((s) => s.required?.includes(key));
|
|
412
|
+
entry = { required, schemas: [] };
|
|
413
|
+
mergedInteractionPropertyMap.set(key, entry);
|
|
414
|
+
}
|
|
415
|
+
entry.schemas.push(value);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const resultObjectSchema = { type: "object", properties: {}, required: [] };
|
|
420
|
+
const keys = /* @__PURE__ */ new Set([
|
|
421
|
+
...mergedUnionPropertyMap.keys(),
|
|
422
|
+
...mergedInteractionPropertyMap.keys()
|
|
423
|
+
]);
|
|
424
|
+
if (keys.size === 0) {
|
|
425
|
+
return schema;
|
|
426
|
+
}
|
|
427
|
+
const deduplicateSchemas = (schemas) => {
|
|
428
|
+
const seen = /* @__PURE__ */ new Set();
|
|
429
|
+
const result = [];
|
|
430
|
+
for (const schema2 of schemas) {
|
|
431
|
+
const key = stringifyJSON(schema2);
|
|
432
|
+
if (!seen.has(key)) {
|
|
433
|
+
seen.add(key);
|
|
434
|
+
result.push(schema2);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return result;
|
|
438
|
+
};
|
|
439
|
+
for (const key of keys) {
|
|
440
|
+
const unionEntry = mergedUnionPropertyMap.get(key);
|
|
441
|
+
const intersectionEntry = mergedInteractionPropertyMap.get(key);
|
|
442
|
+
resultObjectSchema.properties[key] = (() => {
|
|
443
|
+
const dedupedUnionSchemas = unionEntry ? deduplicateSchemas(unionEntry.schemas) : [];
|
|
444
|
+
const dedupedIntersectionSchemas = intersectionEntry ? deduplicateSchemas(intersectionEntry.schemas) : [];
|
|
445
|
+
if (!dedupedUnionSchemas.length) {
|
|
446
|
+
return dedupedIntersectionSchemas.length === 1 ? dedupedIntersectionSchemas[0] : { allOf: dedupedIntersectionSchemas };
|
|
447
|
+
}
|
|
448
|
+
if (!dedupedIntersectionSchemas.length) {
|
|
449
|
+
return dedupedUnionSchemas.length === 1 ? dedupedUnionSchemas[0] : { anyOf: dedupedUnionSchemas };
|
|
450
|
+
}
|
|
451
|
+
const allOf = deduplicateSchemas([
|
|
452
|
+
...dedupedIntersectionSchemas,
|
|
453
|
+
dedupedUnionSchemas.length === 1 ? dedupedUnionSchemas[0] : { anyOf: dedupedUnionSchemas }
|
|
454
|
+
]);
|
|
455
|
+
return allOf.length === 1 ? allOf[0] : { allOf };
|
|
456
|
+
})();
|
|
457
|
+
if (unionEntry?.required || intersectionEntry?.required) {
|
|
458
|
+
resultObjectSchema.required.push(key);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return resultObjectSchema;
|
|
462
|
+
}
|
|
362
463
|
|
|
363
464
|
class CompositeSchemaConverter {
|
|
364
465
|
converters;
|
|
@@ -387,21 +488,18 @@ class OpenAPIGenerator {
|
|
|
387
488
|
/**
|
|
388
489
|
* Generates OpenAPI specifications from oRPC routers/contracts.
|
|
389
490
|
*
|
|
390
|
-
* @see {@link https://orpc.
|
|
491
|
+
* @see {@link https://orpc.dev/docs/openapi/openapi-specification OpenAPI Specification Docs}
|
|
391
492
|
*/
|
|
392
|
-
async generate(router,
|
|
393
|
-
const filter =
|
|
394
|
-
return !(
|
|
493
|
+
async generate(router, { customErrorResponseBodySchema, commonSchemas, filter: baseFilter, exclude, ...baseDoc } = {}) {
|
|
494
|
+
const filter = baseFilter ?? (({ contract, path }) => {
|
|
495
|
+
return !(exclude?.(contract, path) ?? false);
|
|
395
496
|
});
|
|
396
497
|
const doc = {
|
|
397
|
-
...clone(
|
|
398
|
-
info:
|
|
399
|
-
openapi: "3.1.1"
|
|
400
|
-
exclude: void 0,
|
|
401
|
-
filter: void 0,
|
|
402
|
-
commonSchemas: void 0
|
|
498
|
+
...clone(baseDoc),
|
|
499
|
+
info: baseDoc.info ?? { title: "API Reference", version: "0.0.0" },
|
|
500
|
+
openapi: "3.1.1"
|
|
403
501
|
};
|
|
404
|
-
const { baseSchemaConvertOptions, undefinedErrorJsonSchema } = await this.#resolveCommonSchemas(doc,
|
|
502
|
+
const { baseSchemaConvertOptions, undefinedErrorJsonSchema } = await this.#resolveCommonSchemas(doc, commonSchemas);
|
|
405
503
|
const contracts = [];
|
|
406
504
|
await resolveContractProcedures({ path: [], router }, (traverseOptions) => {
|
|
407
505
|
if (!value(filter, traverseOptions)) {
|
|
@@ -429,7 +527,7 @@ class OpenAPIGenerator {
|
|
|
429
527
|
};
|
|
430
528
|
await this.#request(doc, operationObjectRef, def, baseSchemaConvertOptions);
|
|
431
529
|
await this.#successResponse(doc, operationObjectRef, def, baseSchemaConvertOptions);
|
|
432
|
-
await this.#errorResponse(operationObjectRef, def, baseSchemaConvertOptions, undefinedErrorJsonSchema);
|
|
530
|
+
await this.#errorResponse(operationObjectRef, def, baseSchemaConvertOptions, undefinedErrorJsonSchema, customErrorResponseBodySchema);
|
|
433
531
|
}
|
|
434
532
|
if (typeof def.route.spec === "function") {
|
|
435
533
|
operationObjectRef = def.route.spec(operationObjectRef);
|
|
@@ -542,13 +640,15 @@ ${errors.join("\n\n")}`
|
|
|
542
640
|
def.inputSchema,
|
|
543
641
|
{
|
|
544
642
|
...baseSchemaConvertOptions,
|
|
545
|
-
strategy: "input"
|
|
546
|
-
minStructureDepthForRef: dynamicParams?.length || inputStructure === "detailed" ? 1 : 0
|
|
643
|
+
strategy: "input"
|
|
547
644
|
}
|
|
548
645
|
);
|
|
549
646
|
if (isAnySchema(schema) && !dynamicParams?.length) {
|
|
550
647
|
return;
|
|
551
648
|
}
|
|
649
|
+
if (inputStructure === "detailed" || inputStructure === "compact" && (dynamicParams?.length || method === "GET")) {
|
|
650
|
+
schema = simplifyComposedObjectJsonSchemasAndRefs(schema, doc);
|
|
651
|
+
}
|
|
552
652
|
if (inputStructure === "compact") {
|
|
553
653
|
if (dynamicParams?.length) {
|
|
554
654
|
const error2 = new OpenAPIGeneratorError(
|
|
@@ -567,14 +667,13 @@ ${errors.join("\n\n")}`
|
|
|
567
667
|
ref.parameters.push(...toOpenAPIParameters(paramsSchema, "path"));
|
|
568
668
|
}
|
|
569
669
|
if (method === "GET") {
|
|
570
|
-
|
|
571
|
-
if (!isObjectSchema(resolvedSchema)) {
|
|
670
|
+
if (!isObjectSchema(schema)) {
|
|
572
671
|
throw new OpenAPIGeneratorError(
|
|
573
672
|
'When method is "GET", input schema must satisfy: object | any | unknown'
|
|
574
673
|
);
|
|
575
674
|
}
|
|
576
675
|
ref.parameters ??= [];
|
|
577
|
-
ref.parameters.push(...toOpenAPIParameters(
|
|
676
|
+
ref.parameters.push(...toOpenAPIParameters(schema, "query"));
|
|
578
677
|
} else {
|
|
579
678
|
ref.requestBody = {
|
|
580
679
|
required,
|
|
@@ -589,7 +688,7 @@ ${errors.join("\n\n")}`
|
|
|
589
688
|
if (!isObjectSchema(schema)) {
|
|
590
689
|
throw error;
|
|
591
690
|
}
|
|
592
|
-
const resolvedParamSchema = schema.properties?.params !== void 0 ?
|
|
691
|
+
const resolvedParamSchema = schema.properties?.params !== void 0 ? simplifyComposedObjectJsonSchemasAndRefs(schema.properties.params, doc) : void 0;
|
|
593
692
|
if (dynamicParams?.length && (resolvedParamSchema === void 0 || !isObjectSchema(resolvedParamSchema) || !checkParamsSchema(resolvedParamSchema, dynamicParams))) {
|
|
594
693
|
throw new OpenAPIGeneratorError(
|
|
595
694
|
'When input structure is "detailed" and path has dynamic params, the "params" schema must be an object with all dynamic params as required.'
|
|
@@ -598,7 +697,7 @@ ${errors.join("\n\n")}`
|
|
|
598
697
|
for (const from of ["params", "query", "headers"]) {
|
|
599
698
|
const fromSchema = schema.properties?.[from];
|
|
600
699
|
if (fromSchema !== void 0) {
|
|
601
|
-
const resolvedSchema =
|
|
700
|
+
const resolvedSchema = simplifyComposedObjectJsonSchemasAndRefs(fromSchema, doc);
|
|
602
701
|
if (!isObjectSchema(resolvedSchema)) {
|
|
603
702
|
throw error;
|
|
604
703
|
}
|
|
@@ -659,13 +758,14 @@ ${errors.join("\n\n")}`
|
|
|
659
758
|
|
|
660
759
|
But got: ${stringifyJSON(item)}
|
|
661
760
|
`);
|
|
662
|
-
|
|
761
|
+
const simplifiedItem = simplifyComposedObjectJsonSchemasAndRefs(item, doc);
|
|
762
|
+
if (!isObjectSchema(simplifiedItem)) {
|
|
663
763
|
throw error;
|
|
664
764
|
}
|
|
665
765
|
let schemaStatus;
|
|
666
766
|
let schemaDescription;
|
|
667
|
-
if (
|
|
668
|
-
const statusSchema = resolveOpenAPIJsonSchemaRef(doc,
|
|
767
|
+
if (simplifiedItem.properties?.status !== void 0) {
|
|
768
|
+
const statusSchema = resolveOpenAPIJsonSchemaRef(doc, simplifiedItem.properties.status);
|
|
669
769
|
if (typeof statusSchema !== "object" || statusSchema.const === void 0 || typeof statusSchema.const !== "number" || !Number.isInteger(statusSchema.const) || isORPCErrorStatus(statusSchema.const)) {
|
|
670
770
|
throw error;
|
|
671
771
|
}
|
|
@@ -685,8 +785,8 @@ ${errors.join("\n\n")}`
|
|
|
685
785
|
ref.responses[itemStatus] = {
|
|
686
786
|
description: itemDescription
|
|
687
787
|
};
|
|
688
|
-
if (
|
|
689
|
-
const headersSchema =
|
|
788
|
+
if (simplifiedItem.properties?.headers !== void 0) {
|
|
789
|
+
const headersSchema = simplifyComposedObjectJsonSchemasAndRefs(simplifiedItem.properties.headers, doc);
|
|
690
790
|
if (!isObjectSchema(headersSchema)) {
|
|
691
791
|
throw error;
|
|
692
792
|
}
|
|
@@ -696,50 +796,52 @@ ${errors.join("\n\n")}`
|
|
|
696
796
|
ref.responses[itemStatus].headers ??= {};
|
|
697
797
|
ref.responses[itemStatus].headers[key] = {
|
|
698
798
|
schema: toOpenAPISchema(headerSchema),
|
|
699
|
-
required:
|
|
799
|
+
required: simplifiedItem.required?.includes("headers") && headersSchema.required?.includes(key)
|
|
700
800
|
};
|
|
701
801
|
}
|
|
702
802
|
}
|
|
703
803
|
}
|
|
704
|
-
if (
|
|
804
|
+
if (simplifiedItem.properties?.body !== void 0) {
|
|
705
805
|
ref.responses[itemStatus].content = toOpenAPIContent(
|
|
706
|
-
applySchemaOptionality(
|
|
806
|
+
applySchemaOptionality(simplifiedItem.required?.includes("body") ?? false, simplifiedItem.properties.body)
|
|
707
807
|
);
|
|
708
808
|
}
|
|
709
809
|
}
|
|
710
810
|
}
|
|
711
|
-
async #errorResponse(ref, def, baseSchemaConvertOptions, undefinedErrorSchema) {
|
|
811
|
+
async #errorResponse(ref, def, baseSchemaConvertOptions, undefinedErrorSchema, customErrorResponseBodySchema) {
|
|
712
812
|
const errorMap = def.errorMap;
|
|
713
|
-
const
|
|
813
|
+
const errorResponsesByStatus = {};
|
|
714
814
|
for (const code in errorMap) {
|
|
715
815
|
const config = errorMap[code];
|
|
716
816
|
if (!config) {
|
|
717
817
|
continue;
|
|
718
818
|
}
|
|
719
819
|
const status = fallbackORPCErrorStatus(code, config.status);
|
|
720
|
-
const
|
|
820
|
+
const defaultMessage = fallbackORPCErrorMessage(code, config.message);
|
|
821
|
+
errorResponsesByStatus[status] ??= { status, definedErrorDefinitions: [], errorSchemaVariants: [] };
|
|
721
822
|
const [dataRequired, dataSchema] = await this.converter.convert(config.data, { ...baseSchemaConvertOptions, strategy: "output" });
|
|
722
|
-
|
|
723
|
-
|
|
823
|
+
errorResponsesByStatus[status].definedErrorDefinitions.push([code, defaultMessage, dataRequired, dataSchema]);
|
|
824
|
+
errorResponsesByStatus[status].errorSchemaVariants.push({
|
|
724
825
|
type: "object",
|
|
725
826
|
properties: {
|
|
726
827
|
defined: { const: true },
|
|
727
828
|
code: { const: code },
|
|
728
829
|
status: { const: status },
|
|
729
|
-
message: { type: "string", default:
|
|
830
|
+
message: { type: "string", default: defaultMessage },
|
|
730
831
|
data: dataSchema
|
|
731
832
|
},
|
|
732
833
|
required: dataRequired ? ["defined", "code", "status", "message", "data"] : ["defined", "code", "status", "message"]
|
|
733
834
|
});
|
|
734
835
|
}
|
|
735
836
|
ref.responses ??= {};
|
|
736
|
-
for (const
|
|
737
|
-
const
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
837
|
+
for (const statusString in errorResponsesByStatus) {
|
|
838
|
+
const errorResponse = errorResponsesByStatus[statusString];
|
|
839
|
+
const customBodySchema = value(customErrorResponseBodySchema, errorResponse.definedErrorDefinitions, errorResponse.status);
|
|
840
|
+
ref.responses[statusString] = {
|
|
841
|
+
description: statusString,
|
|
842
|
+
content: toOpenAPIContent(customBodySchema ?? {
|
|
741
843
|
oneOf: [
|
|
742
|
-
...
|
|
844
|
+
...errorResponse.errorSchemaVariants,
|
|
743
845
|
undefinedErrorSchema
|
|
744
846
|
]
|
|
745
847
|
})
|
|
@@ -748,4 +850,4 @@ ${errors.join("\n\n")}`
|
|
|
748
850
|
}
|
|
749
851
|
}
|
|
750
852
|
|
|
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,
|
|
853
|
+
export { CompositeSchemaConverter as C, LOGIC_KEYWORDS as L, OpenAPIGenerator as O, applyCustomOpenAPIOperation as a, toOpenAPIMethod as b, customOpenAPIOperation as c, toOpenAPIContent as d, toOpenAPIEventIteratorContent as e, toOpenAPIParameters as f, getCustomOpenAPIOperation as g, checkParamsSchema as h, toOpenAPISchema as i, isFileSchema as j, isObjectSchema as k, isAnySchema as l, separateObjectSchema as m, filterSchemaBranches as n, applySchemaOptionality as o, expandUnionSchema as p, expandArrayableSchema as q, resolveOpenAPIJsonSchemaRef as r, simplifyComposedObjectJsonSchemasAndRefs as s, toOpenAPIPath as t, isPrimitiveSchema as u };
|
|
@@ -8,9 +8,11 @@ import { traverseContractProcedures, isProcedure, getLazyMeta, unlazy, getRouter
|
|
|
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(
|
|
82
|
+
body: this.serializer.serialize(body, { outputFormat: "plain" })
|
|
80
83
|
};
|
|
81
84
|
}
|
|
82
85
|
#isDetailedOutput(output) {
|
|
@@ -179,7 +182,7 @@ class StandardOpenAPIHandler extends StandardHandler {
|
|
|
179
182
|
const bracketNotationSerializer = new StandardBracketNotationSerializer(options);
|
|
180
183
|
const serializer = new StandardOpenAPISerializer(jsonSerializer, bracketNotationSerializer);
|
|
181
184
|
const matcher = new StandardOpenAPIMatcher(options);
|
|
182
|
-
const codec = new StandardOpenAPICodec(serializer);
|
|
185
|
+
const codec = new StandardOpenAPICodec(serializer, options);
|
|
183
186
|
super(router, matcher, codec, options);
|
|
184
187
|
}
|
|
185
188
|
}
|
|
@@ -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 };
|