@orpc/openapi 0.0.0-next.f356b86 → 0.0.0-next.f397ca2
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 +7 -2
- package/dist/adapters/aws-lambda/index.d.mts +6 -3
- package/dist/adapters/aws-lambda/index.d.ts +6 -3
- package/dist/adapters/aws-lambda/index.mjs +3 -3
- 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 +9 -3
- package/dist/adapters/fetch/index.d.ts +9 -3
- package/dist/adapters/fetch/index.mjs +1 -1
- package/dist/adapters/node/index.d.mts +9 -3
- package/dist/adapters/node/index.d.ts +9 -3
- package/dist/adapters/node/index.mjs +1 -1
- package/dist/adapters/standard/index.d.mts +8 -23
- package/dist/adapters/standard/index.d.ts +8 -23
- package/dist/adapters/standard/index.mjs +1 -1
- package/dist/index.d.mts +10 -4
- package/dist/index.d.ts +10 -4
- package/dist/index.mjs +3 -3
- package/dist/plugins/index.d.mts +19 -4
- package/dist/plugins/index.d.ts +19 -4
- package/dist/plugins/index.mjs +59 -19
- package/dist/shared/{openapi.8DHd5bRK.d.mts → openapi.BfNjg7j9.d.mts} +23 -4
- package/dist/shared/{openapi.8DHd5bRK.d.ts → openapi.BfNjg7j9.d.ts} +23 -4
- package/dist/shared/{openapi.DrrBsJ0w.mjs → openapi.CzHcOMxv.mjs} +161 -46
- package/dist/shared/{openapi.-sXpEIAO.mjs → openapi.DIt-Z9W1.mjs} +18 -7
- package/dist/shared/openapi.DwaweYRb.d.mts +54 -0
- package/dist/shared/openapi.DwaweYRb.d.ts +54 -0
- package/package.json +16 -10
- package/dist/shared/openapi.BWrlhfev.d.mts +0 -12
- package/dist/shared/openapi.BWrlhfev.d.ts +0 -12
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.CzHcOMxv.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.
|
|
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, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
30
|
-
this.renderDocsHtml = options.renderDocsHtml ?? ((specUrl, title, head, scriptUrl, config, spec) => {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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'][];
|
|
@@ -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,
|
|
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'][];
|
|
@@ -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,
|
|
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 };
|
|
@@ -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(
|
|
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 =
|
|
123
|
-
|
|
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));
|
|
@@ -354,6 +359,107 @@ function resolveOpenAPIJsonSchemaRef(doc, schema) {
|
|
|
354
359
|
const resolved = doc.components?.schemas?.[name];
|
|
355
360
|
return resolved ?? schema;
|
|
356
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
|
+
}
|
|
357
463
|
|
|
358
464
|
class CompositeSchemaConverter {
|
|
359
465
|
converters;
|
|
@@ -384,35 +490,36 @@ class OpenAPIGenerator {
|
|
|
384
490
|
*
|
|
385
491
|
* @see {@link https://orpc.unnoq.com/docs/openapi/openapi-specification OpenAPI Specification Docs}
|
|
386
492
|
*/
|
|
387
|
-
async generate(router,
|
|
388
|
-
const
|
|
493
|
+
async generate(router, { customErrorResponseBodySchema, commonSchemas, filter: baseFilter, exclude, ...baseDoc } = {}) {
|
|
494
|
+
const filter = baseFilter ?? (({ contract, path }) => {
|
|
495
|
+
return !(exclude?.(contract, path) ?? false);
|
|
496
|
+
});
|
|
389
497
|
const doc = {
|
|
390
|
-
...clone(
|
|
391
|
-
info:
|
|
392
|
-
openapi: "3.1.1"
|
|
393
|
-
exclude: void 0,
|
|
394
|
-
commonSchemas: void 0
|
|
498
|
+
...clone(baseDoc),
|
|
499
|
+
info: baseDoc.info ?? { title: "API Reference", version: "0.0.0" },
|
|
500
|
+
openapi: "3.1.1"
|
|
395
501
|
};
|
|
396
|
-
const { baseSchemaConvertOptions, undefinedErrorJsonSchema } = await this.#resolveCommonSchemas(doc,
|
|
502
|
+
const { baseSchemaConvertOptions, undefinedErrorJsonSchema } = await this.#resolveCommonSchemas(doc, commonSchemas);
|
|
397
503
|
const contracts = [];
|
|
398
|
-
await resolveContractProcedures({ path: [], router }, (
|
|
399
|
-
if (!
|
|
400
|
-
|
|
504
|
+
await resolveContractProcedures({ path: [], router }, (traverseOptions) => {
|
|
505
|
+
if (!value(filter, traverseOptions)) {
|
|
506
|
+
return;
|
|
401
507
|
}
|
|
508
|
+
contracts.push(traverseOptions);
|
|
402
509
|
});
|
|
403
510
|
const errors = [];
|
|
404
511
|
for (const { contract, path } of contracts) {
|
|
405
|
-
const
|
|
512
|
+
const stringPath = path.join(".");
|
|
406
513
|
try {
|
|
407
514
|
const def = contract["~orpc"];
|
|
408
515
|
const method = toOpenAPIMethod(fallbackContractConfig("defaultMethod", def.route.method));
|
|
409
516
|
const httpPath = toOpenAPIPath(def.route.path ?? toHttpPath(path));
|
|
410
517
|
let operationObjectRef;
|
|
411
|
-
if (def.route.spec !== void 0) {
|
|
518
|
+
if (def.route.spec !== void 0 && typeof def.route.spec !== "function") {
|
|
412
519
|
operationObjectRef = def.route.spec;
|
|
413
520
|
} else {
|
|
414
521
|
operationObjectRef = {
|
|
415
|
-
operationId,
|
|
522
|
+
operationId: def.route.operationId ?? stringPath,
|
|
416
523
|
summary: def.route.summary,
|
|
417
524
|
description: def.route.description,
|
|
418
525
|
deprecated: def.route.deprecated,
|
|
@@ -420,7 +527,10 @@ class OpenAPIGenerator {
|
|
|
420
527
|
};
|
|
421
528
|
await this.#request(doc, operationObjectRef, def, baseSchemaConvertOptions);
|
|
422
529
|
await this.#successResponse(doc, operationObjectRef, def, baseSchemaConvertOptions);
|
|
423
|
-
await this.#errorResponse(operationObjectRef, def, baseSchemaConvertOptions, undefinedErrorJsonSchema);
|
|
530
|
+
await this.#errorResponse(operationObjectRef, def, baseSchemaConvertOptions, undefinedErrorJsonSchema, customErrorResponseBodySchema);
|
|
531
|
+
}
|
|
532
|
+
if (typeof def.route.spec === "function") {
|
|
533
|
+
operationObjectRef = def.route.spec(operationObjectRef);
|
|
424
534
|
}
|
|
425
535
|
doc.paths ??= {};
|
|
426
536
|
doc.paths[httpPath] ??= {};
|
|
@@ -430,7 +540,7 @@ class OpenAPIGenerator {
|
|
|
430
540
|
throw e;
|
|
431
541
|
}
|
|
432
542
|
errors.push(
|
|
433
|
-
`[OpenAPIGenerator] Error occurred while generating OpenAPI for procedure at path: ${
|
|
543
|
+
`[OpenAPIGenerator] Error occurred while generating OpenAPI for procedure at path: ${stringPath}
|
|
434
544
|
${e.message}`
|
|
435
545
|
);
|
|
436
546
|
}
|
|
@@ -530,13 +640,15 @@ ${errors.join("\n\n")}`
|
|
|
530
640
|
def.inputSchema,
|
|
531
641
|
{
|
|
532
642
|
...baseSchemaConvertOptions,
|
|
533
|
-
strategy: "input"
|
|
534
|
-
minStructureDepthForRef: dynamicParams?.length || inputStructure === "detailed" ? 1 : 0
|
|
643
|
+
strategy: "input"
|
|
535
644
|
}
|
|
536
645
|
);
|
|
537
646
|
if (isAnySchema(schema) && !dynamicParams?.length) {
|
|
538
647
|
return;
|
|
539
648
|
}
|
|
649
|
+
if (inputStructure === "detailed" || inputStructure === "compact" && (dynamicParams?.length || method === "GET")) {
|
|
650
|
+
schema = simplifyComposedObjectJsonSchemasAndRefs(schema, doc);
|
|
651
|
+
}
|
|
540
652
|
if (inputStructure === "compact") {
|
|
541
653
|
if (dynamicParams?.length) {
|
|
542
654
|
const error2 = new OpenAPIGeneratorError(
|
|
@@ -576,7 +688,7 @@ ${errors.join("\n\n")}`
|
|
|
576
688
|
if (!isObjectSchema(schema)) {
|
|
577
689
|
throw error;
|
|
578
690
|
}
|
|
579
|
-
const resolvedParamSchema = schema.properties?.params !== void 0 ?
|
|
691
|
+
const resolvedParamSchema = schema.properties?.params !== void 0 ? simplifyComposedObjectJsonSchemasAndRefs(schema.properties.params, doc) : void 0;
|
|
580
692
|
if (dynamicParams?.length && (resolvedParamSchema === void 0 || !isObjectSchema(resolvedParamSchema) || !checkParamsSchema(resolvedParamSchema, dynamicParams))) {
|
|
581
693
|
throw new OpenAPIGeneratorError(
|
|
582
694
|
'When input structure is "detailed" and path has dynamic params, the "params" schema must be an object with all dynamic params as required.'
|
|
@@ -585,7 +697,7 @@ ${errors.join("\n\n")}`
|
|
|
585
697
|
for (const from of ["params", "query", "headers"]) {
|
|
586
698
|
const fromSchema = schema.properties?.[from];
|
|
587
699
|
if (fromSchema !== void 0) {
|
|
588
|
-
const resolvedSchema =
|
|
700
|
+
const resolvedSchema = simplifyComposedObjectJsonSchemasAndRefs(fromSchema, doc);
|
|
589
701
|
if (!isObjectSchema(resolvedSchema)) {
|
|
590
702
|
throw error;
|
|
591
703
|
}
|
|
@@ -646,13 +758,14 @@ ${errors.join("\n\n")}`
|
|
|
646
758
|
|
|
647
759
|
But got: ${stringifyJSON(item)}
|
|
648
760
|
`);
|
|
649
|
-
|
|
761
|
+
const simplifiedItem = simplifyComposedObjectJsonSchemasAndRefs(item, doc);
|
|
762
|
+
if (!isObjectSchema(simplifiedItem)) {
|
|
650
763
|
throw error;
|
|
651
764
|
}
|
|
652
765
|
let schemaStatus;
|
|
653
766
|
let schemaDescription;
|
|
654
|
-
if (
|
|
655
|
-
const statusSchema = resolveOpenAPIJsonSchemaRef(doc,
|
|
767
|
+
if (simplifiedItem.properties?.status !== void 0) {
|
|
768
|
+
const statusSchema = resolveOpenAPIJsonSchemaRef(doc, simplifiedItem.properties.status);
|
|
656
769
|
if (typeof statusSchema !== "object" || statusSchema.const === void 0 || typeof statusSchema.const !== "number" || !Number.isInteger(statusSchema.const) || isORPCErrorStatus(statusSchema.const)) {
|
|
657
770
|
throw error;
|
|
658
771
|
}
|
|
@@ -672,8 +785,8 @@ ${errors.join("\n\n")}`
|
|
|
672
785
|
ref.responses[itemStatus] = {
|
|
673
786
|
description: itemDescription
|
|
674
787
|
};
|
|
675
|
-
if (
|
|
676
|
-
const headersSchema =
|
|
788
|
+
if (simplifiedItem.properties?.headers !== void 0) {
|
|
789
|
+
const headersSchema = simplifyComposedObjectJsonSchemasAndRefs(simplifiedItem.properties.headers, doc);
|
|
677
790
|
if (!isObjectSchema(headersSchema)) {
|
|
678
791
|
throw error;
|
|
679
792
|
}
|
|
@@ -683,50 +796,52 @@ ${errors.join("\n\n")}`
|
|
|
683
796
|
ref.responses[itemStatus].headers ??= {};
|
|
684
797
|
ref.responses[itemStatus].headers[key] = {
|
|
685
798
|
schema: toOpenAPISchema(headerSchema),
|
|
686
|
-
required:
|
|
799
|
+
required: simplifiedItem.required?.includes("headers") && headersSchema.required?.includes(key)
|
|
687
800
|
};
|
|
688
801
|
}
|
|
689
802
|
}
|
|
690
803
|
}
|
|
691
|
-
if (
|
|
804
|
+
if (simplifiedItem.properties?.body !== void 0) {
|
|
692
805
|
ref.responses[itemStatus].content = toOpenAPIContent(
|
|
693
|
-
applySchemaOptionality(
|
|
806
|
+
applySchemaOptionality(simplifiedItem.required?.includes("body") ?? false, simplifiedItem.properties.body)
|
|
694
807
|
);
|
|
695
808
|
}
|
|
696
809
|
}
|
|
697
810
|
}
|
|
698
|
-
async #errorResponse(ref, def, baseSchemaConvertOptions, undefinedErrorSchema) {
|
|
811
|
+
async #errorResponse(ref, def, baseSchemaConvertOptions, undefinedErrorSchema, customErrorResponseBodySchema) {
|
|
699
812
|
const errorMap = def.errorMap;
|
|
700
|
-
const
|
|
813
|
+
const errorResponsesByStatus = {};
|
|
701
814
|
for (const code in errorMap) {
|
|
702
815
|
const config = errorMap[code];
|
|
703
816
|
if (!config) {
|
|
704
817
|
continue;
|
|
705
818
|
}
|
|
706
819
|
const status = fallbackORPCErrorStatus(code, config.status);
|
|
707
|
-
const
|
|
820
|
+
const defaultMessage = fallbackORPCErrorMessage(code, config.message);
|
|
821
|
+
errorResponsesByStatus[status] ??= { status, definedErrorDefinitions: [], errorSchemaVariants: [] };
|
|
708
822
|
const [dataRequired, dataSchema] = await this.converter.convert(config.data, { ...baseSchemaConvertOptions, strategy: "output" });
|
|
709
|
-
|
|
710
|
-
|
|
823
|
+
errorResponsesByStatus[status].definedErrorDefinitions.push([code, defaultMessage, dataRequired, dataSchema]);
|
|
824
|
+
errorResponsesByStatus[status].errorSchemaVariants.push({
|
|
711
825
|
type: "object",
|
|
712
826
|
properties: {
|
|
713
827
|
defined: { const: true },
|
|
714
828
|
code: { const: code },
|
|
715
829
|
status: { const: status },
|
|
716
|
-
message: { type: "string", default:
|
|
830
|
+
message: { type: "string", default: defaultMessage },
|
|
717
831
|
data: dataSchema
|
|
718
832
|
},
|
|
719
833
|
required: dataRequired ? ["defined", "code", "status", "message", "data"] : ["defined", "code", "status", "message"]
|
|
720
834
|
});
|
|
721
835
|
}
|
|
722
836
|
ref.responses ??= {};
|
|
723
|
-
for (const
|
|
724
|
-
const
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
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 ?? {
|
|
728
843
|
oneOf: [
|
|
729
|
-
...
|
|
844
|
+
...errorResponse.errorSchemaVariants,
|
|
730
845
|
undefinedErrorSchema
|
|
731
846
|
]
|
|
732
847
|
})
|
|
@@ -735,4 +850,4 @@ ${errors.join("\n\n")}`
|
|
|
735
850
|
}
|
|
736
851
|
}
|
|
737
852
|
|
|
738
|
-
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 };
|
|
@@ -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(
|
|
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,
|
|
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 }, (
|
|
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
|
}
|