@orpc/openapi 0.0.0-next.f9e0a4c → 0.0.0-next.f9e5dec
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 +5 -1
- package/dist/adapters/aws-lambda/index.d.mts +2 -1
- package/dist/adapters/aws-lambda/index.d.ts +2 -1
- 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 +7 -3
- package/dist/adapters/fetch/index.d.ts +7 -3
- package/dist/adapters/fetch/index.mjs +1 -1
- package/dist/adapters/node/index.d.mts +7 -3
- package/dist/adapters/node/index.d.ts +7 -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 +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.BVXcB0u4.mjs → openapi.DIt-Z9W1.mjs} +6 -3
- package/dist/shared/{openapi.DRqtR2lw.mjs → openapi.DNv9yFfn.mjs} +140 -37
- package/dist/shared/openapi.DwaweYRb.d.mts +54 -0
- package/dist/shared/openapi.DwaweYRb.d.ts +54 -0
- package/dist/shared/{openapi.TOD62C7O.d.mts → openapi.dbQeFCUJ.d.mts} +14 -2
- package/dist/shared/{openapi.TOD62C7O.d.ts → openapi.dbQeFCUJ.d.ts} +14 -2
- package/package.json +17 -10
- package/dist/shared/openapi.CQmjvnb0.d.mts +0 -31
- package/dist/shared/openapi.CQmjvnb0.d.ts +0 -31
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.DNv9yFfn.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;
|
|
@@ -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,
|
|
@@ -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
|
}
|
|
@@ -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;
|
|
@@ -389,19 +490,16 @@ class OpenAPIGenerator {
|
|
|
389
490
|
*
|
|
390
491
|
* @see {@link https://orpc.unnoq.com/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(
|
|
@@ -588,7 +688,7 @@ ${errors.join("\n\n")}`
|
|
|
588
688
|
if (!isObjectSchema(schema)) {
|
|
589
689
|
throw error;
|
|
590
690
|
}
|
|
591
|
-
const resolvedParamSchema = schema.properties?.params !== void 0 ?
|
|
691
|
+
const resolvedParamSchema = schema.properties?.params !== void 0 ? simplifyComposedObjectJsonSchemasAndRefs(schema.properties.params, doc) : void 0;
|
|
592
692
|
if (dynamicParams?.length && (resolvedParamSchema === void 0 || !isObjectSchema(resolvedParamSchema) || !checkParamsSchema(resolvedParamSchema, dynamicParams))) {
|
|
593
693
|
throw new OpenAPIGeneratorError(
|
|
594
694
|
'When input structure is "detailed" and path has dynamic params, the "params" schema must be an object with all dynamic params as required.'
|
|
@@ -597,7 +697,7 @@ ${errors.join("\n\n")}`
|
|
|
597
697
|
for (const from of ["params", "query", "headers"]) {
|
|
598
698
|
const fromSchema = schema.properties?.[from];
|
|
599
699
|
if (fromSchema !== void 0) {
|
|
600
|
-
const resolvedSchema =
|
|
700
|
+
const resolvedSchema = simplifyComposedObjectJsonSchemasAndRefs(fromSchema, doc);
|
|
601
701
|
if (!isObjectSchema(resolvedSchema)) {
|
|
602
702
|
throw error;
|
|
603
703
|
}
|
|
@@ -658,13 +758,14 @@ ${errors.join("\n\n")}`
|
|
|
658
758
|
|
|
659
759
|
But got: ${stringifyJSON(item)}
|
|
660
760
|
`);
|
|
661
|
-
|
|
761
|
+
const simplifiedItem = simplifyComposedObjectJsonSchemasAndRefs(item, doc);
|
|
762
|
+
if (!isObjectSchema(simplifiedItem)) {
|
|
662
763
|
throw error;
|
|
663
764
|
}
|
|
664
765
|
let schemaStatus;
|
|
665
766
|
let schemaDescription;
|
|
666
|
-
if (
|
|
667
|
-
const statusSchema = resolveOpenAPIJsonSchemaRef(doc,
|
|
767
|
+
if (simplifiedItem.properties?.status !== void 0) {
|
|
768
|
+
const statusSchema = resolveOpenAPIJsonSchemaRef(doc, simplifiedItem.properties.status);
|
|
668
769
|
if (typeof statusSchema !== "object" || statusSchema.const === void 0 || typeof statusSchema.const !== "number" || !Number.isInteger(statusSchema.const) || isORPCErrorStatus(statusSchema.const)) {
|
|
669
770
|
throw error;
|
|
670
771
|
}
|
|
@@ -684,8 +785,8 @@ ${errors.join("\n\n")}`
|
|
|
684
785
|
ref.responses[itemStatus] = {
|
|
685
786
|
description: itemDescription
|
|
686
787
|
};
|
|
687
|
-
if (
|
|
688
|
-
const headersSchema =
|
|
788
|
+
if (simplifiedItem.properties?.headers !== void 0) {
|
|
789
|
+
const headersSchema = simplifyComposedObjectJsonSchemasAndRefs(simplifiedItem.properties.headers, doc);
|
|
689
790
|
if (!isObjectSchema(headersSchema)) {
|
|
690
791
|
throw error;
|
|
691
792
|
}
|
|
@@ -695,50 +796,52 @@ ${errors.join("\n\n")}`
|
|
|
695
796
|
ref.responses[itemStatus].headers ??= {};
|
|
696
797
|
ref.responses[itemStatus].headers[key] = {
|
|
697
798
|
schema: toOpenAPISchema(headerSchema),
|
|
698
|
-
required:
|
|
799
|
+
required: simplifiedItem.required?.includes("headers") && headersSchema.required?.includes(key)
|
|
699
800
|
};
|
|
700
801
|
}
|
|
701
802
|
}
|
|
702
803
|
}
|
|
703
|
-
if (
|
|
804
|
+
if (simplifiedItem.properties?.body !== void 0) {
|
|
704
805
|
ref.responses[itemStatus].content = toOpenAPIContent(
|
|
705
|
-
applySchemaOptionality(
|
|
806
|
+
applySchemaOptionality(simplifiedItem.required?.includes("body") ?? false, simplifiedItem.properties.body)
|
|
706
807
|
);
|
|
707
808
|
}
|
|
708
809
|
}
|
|
709
810
|
}
|
|
710
|
-
async #errorResponse(ref, def, baseSchemaConvertOptions, undefinedErrorSchema) {
|
|
811
|
+
async #errorResponse(ref, def, baseSchemaConvertOptions, undefinedErrorSchema, customErrorResponseBodySchema) {
|
|
711
812
|
const errorMap = def.errorMap;
|
|
712
|
-
const
|
|
813
|
+
const errorResponsesByStatus = {};
|
|
713
814
|
for (const code in errorMap) {
|
|
714
815
|
const config = errorMap[code];
|
|
715
816
|
if (!config) {
|
|
716
817
|
continue;
|
|
717
818
|
}
|
|
718
819
|
const status = fallbackORPCErrorStatus(code, config.status);
|
|
719
|
-
const
|
|
820
|
+
const defaultMessage = fallbackORPCErrorMessage(code, config.message);
|
|
821
|
+
errorResponsesByStatus[status] ??= { status, definedErrorDefinitions: [], errorSchemaVariants: [] };
|
|
720
822
|
const [dataRequired, dataSchema] = await this.converter.convert(config.data, { ...baseSchemaConvertOptions, strategy: "output" });
|
|
721
|
-
|
|
722
|
-
|
|
823
|
+
errorResponsesByStatus[status].definedErrorDefinitions.push([code, defaultMessage, dataRequired, dataSchema]);
|
|
824
|
+
errorResponsesByStatus[status].errorSchemaVariants.push({
|
|
723
825
|
type: "object",
|
|
724
826
|
properties: {
|
|
725
827
|
defined: { const: true },
|
|
726
828
|
code: { const: code },
|
|
727
829
|
status: { const: status },
|
|
728
|
-
message: { type: "string", default:
|
|
830
|
+
message: { type: "string", default: defaultMessage },
|
|
729
831
|
data: dataSchema
|
|
730
832
|
},
|
|
731
833
|
required: dataRequired ? ["defined", "code", "status", "message", "data"] : ["defined", "code", "status", "message"]
|
|
732
834
|
});
|
|
733
835
|
}
|
|
734
836
|
ref.responses ??= {};
|
|
735
|
-
for (const
|
|
736
|
-
const
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
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 ?? {
|
|
740
843
|
oneOf: [
|
|
741
|
-
...
|
|
844
|
+
...errorResponse.errorSchemaVariants,
|
|
742
845
|
undefinedErrorSchema
|
|
743
846
|
]
|
|
744
847
|
})
|
|
@@ -747,4 +850,4 @@ ${errors.join("\n\n")}`
|
|
|
747
850
|
}
|
|
748
851
|
}
|
|
749
852
|
|
|
750
|
-
export { CompositeSchemaConverter as C, LOGIC_KEYWORDS as L, OpenAPIGenerator as O, applyCustomOpenAPIOperation as a, toOpenAPIMethod as b, customOpenAPIOperation as c, toOpenAPIContent as d, toOpenAPIEventIteratorContent as e, toOpenAPIParameters as f, getCustomOpenAPIOperation as g, checkParamsSchema as h, toOpenAPISchema as i, isFileSchema as j, isObjectSchema as k, isAnySchema as l,
|
|
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 };
|
|
@@ -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 };
|
|
@@ -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,6 +85,18 @@ 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.
|
|
@@ -101,7 +113,7 @@ declare class OpenAPIGenerator {
|
|
|
101
113
|
*
|
|
102
114
|
* @see {@link https://orpc.unnoq.com/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,6 +85,18 @@ 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.
|
|
@@ -101,7 +113,7 @@ declare class OpenAPIGenerator {
|
|
|
101
113
|
*
|
|
102
114
|
* @see {@link https://orpc.unnoq.com/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 };
|