@orpc/openapi 0.0.0-next.460b50b → 0.0.0-next.47dbd93
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 +138 -14
- package/dist/adapters/aws-lambda/index.d.mts +8 -5
- package/dist/adapters/aws-lambda/index.d.ts +8 -5
- package/dist/adapters/aws-lambda/index.mjs +5 -5
- 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 +11 -5
- package/dist/adapters/fetch/index.d.ts +11 -5
- package/dist/adapters/fetch/index.mjs +3 -3
- package/dist/adapters/node/index.d.mts +11 -5
- package/dist/adapters/node/index.d.ts +11 -5
- package/dist/adapters/node/index.mjs +3 -3
- 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 -3
- package/dist/index.d.ts +10 -3
- package/dist/index.mjs +2 -2
- package/dist/plugins/index.d.mts +20 -3
- package/dist/plugins/index.d.ts +20 -3
- package/dist/plugins/index.mjs +69 -20
- package/dist/shared/{openapi.C_UtQ8Us.mjs → openapi.BB-W-NKv.mjs} +33 -8
- package/dist/shared/{openapi.CbIlrReM.d.mts → openapi.BGy4N6eR.d.mts} +30 -8
- package/dist/shared/{openapi.CbIlrReM.d.ts → openapi.BGy4N6eR.d.ts} +30 -8
- package/dist/shared/{openapi.C_3bk7bB.mjs → openapi.BwdtJjDu.mjs} +222 -69
- package/dist/shared/openapi.DwaweYRb.d.mts +54 -0
- package/dist/shared/openapi.DwaweYRb.d.ts +54 -0
- package/package.json +20 -13
- package/dist/shared/openapi.D3j94c9n.d.mts +0 -12
- package/dist/shared/openapi.D3j94c9n.d.ts +0 -12
|
@@ -1,11 +1,11 @@
|
|
|
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';
|
|
3
|
+
import { AnyProcedure, TraverseContractProcedureCallbackOptions, AnyRouter } from '@orpc/server';
|
|
4
|
+
import { Promisable, Value } from '@orpc/shared';
|
|
5
5
|
import { JSONSchema } from 'json-schema-typed/draft-2020-12';
|
|
6
6
|
|
|
7
7
|
interface SchemaConverterComponent {
|
|
8
|
-
allowedStrategies: SchemaConvertOptions['strategy'][];
|
|
8
|
+
allowedStrategies: readonly SchemaConvertOptions['strategy'][];
|
|
9
9
|
schema: AnySchema;
|
|
10
10
|
required: boolean;
|
|
11
11
|
ref: string;
|
|
@@ -15,7 +15,7 @@ interface SchemaConvertOptions {
|
|
|
15
15
|
/**
|
|
16
16
|
* Common components should use `$ref` to represent themselves if matched.
|
|
17
17
|
*/
|
|
18
|
-
components?: SchemaConverterComponent[];
|
|
18
|
+
components?: readonly SchemaConverterComponent[];
|
|
19
19
|
/**
|
|
20
20
|
* Minimum schema structure depth required before using `$ref` for components.
|
|
21
21
|
*
|
|
@@ -33,7 +33,7 @@ interface ConditionalSchemaConverter extends SchemaConverter {
|
|
|
33
33
|
}
|
|
34
34
|
declare class CompositeSchemaConverter implements SchemaConverter {
|
|
35
35
|
private readonly converters;
|
|
36
|
-
constructor(converters: ConditionalSchemaConverter[]);
|
|
36
|
+
constructor(converters: readonly ConditionalSchemaConverter[]);
|
|
37
37
|
convert(schema: AnySchema | undefined, options: SchemaConvertOptions): Promise<[required: boolean, jsonSchema: JSONSchema]>;
|
|
38
38
|
}
|
|
39
39
|
|
|
@@ -44,9 +44,16 @@ interface OpenAPIGeneratorGenerateOptions extends Partial<Omit<OpenAPI.Document,
|
|
|
44
44
|
/**
|
|
45
45
|
* Exclude procedures from the OpenAPI specification.
|
|
46
46
|
*
|
|
47
|
+
* @deprecated Use `filter` option instead.
|
|
47
48
|
* @default () => false
|
|
48
49
|
*/
|
|
49
50
|
exclude?: (procedure: AnyProcedure | AnyContractProcedure, path: readonly string[]) => boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Filter procedures. Return `false` to exclude a procedure from the OpenAPI specification.
|
|
53
|
+
*
|
|
54
|
+
* @default true
|
|
55
|
+
*/
|
|
56
|
+
filter?: Value<boolean, [options: TraverseContractProcedureCallbackOptions]>;
|
|
50
57
|
/**
|
|
51
58
|
* Common schemas to be used for $ref resolution.
|
|
52
59
|
*/
|
|
@@ -74,12 +81,27 @@ interface OpenAPIGeneratorGenerateOptions extends Partial<Omit<OpenAPI.Document,
|
|
|
74
81
|
*/
|
|
75
82
|
strategy?: SchemaConvertOptions['strategy'];
|
|
76
83
|
schema: AnySchema;
|
|
84
|
+
} | {
|
|
85
|
+
error: 'UndefinedError';
|
|
86
|
+
schema?: never;
|
|
77
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
|
+
]>;
|
|
78
100
|
}
|
|
79
101
|
/**
|
|
80
102
|
* The generator that converts oRPC routers/contracts to OpenAPI specifications.
|
|
81
103
|
*
|
|
82
|
-
* @see {@link https://orpc.
|
|
104
|
+
* @see {@link https://orpc.dev/docs/openapi/openapi-specification OpenAPI Specification Docs}
|
|
83
105
|
*/
|
|
84
106
|
declare class OpenAPIGenerator {
|
|
85
107
|
#private;
|
|
@@ -89,9 +111,9 @@ declare class OpenAPIGenerator {
|
|
|
89
111
|
/**
|
|
90
112
|
* Generates OpenAPI specifications from oRPC routers/contracts.
|
|
91
113
|
*
|
|
92
|
-
* @see {@link https://orpc.
|
|
114
|
+
* @see {@link https://orpc.dev/docs/openapi/openapi-specification OpenAPI Specification Docs}
|
|
93
115
|
*/
|
|
94
|
-
generate(router: AnyContractRouter | AnyRouter,
|
|
116
|
+
generate(router: AnyContractRouter | AnyRouter, { customErrorResponseBodySchema, commonSchemas, filter: baseFilter, exclude, ...baseDoc }?: OpenAPIGeneratorGenerateOptions): Promise<OpenAPI.Document>;
|
|
95
117
|
}
|
|
96
118
|
|
|
97
119
|
export { OpenAPIGenerator as b, CompositeSchemaConverter as e };
|
|
@@ -3,7 +3,7 @@ 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';
|
|
6
|
+
import { isObject, stringifyJSON, findDeepMatches, toArray, clone, value } from '@orpc/shared';
|
|
7
7
|
import { TypeName } from 'json-schema-typed/draft-2020-12';
|
|
8
8
|
|
|
9
9
|
const OPERATION_EXTENDER_SYMBOL = Symbol("ORPC_OPERATION_EXTENDER");
|
|
@@ -108,22 +108,47 @@ function isAnySchema(schema) {
|
|
|
108
108
|
if (schema === true) {
|
|
109
109
|
return true;
|
|
110
110
|
}
|
|
111
|
-
if (Object.keys(schema).every((k) => !LOGIC_KEYWORDS.includes(k))) {
|
|
111
|
+
if (Object.keys(schema).filter((v) => schema[v] !== void 0).every((k) => !LOGIC_KEYWORDS.includes(k))) {
|
|
112
112
|
return true;
|
|
113
113
|
}
|
|
114
114
|
return false;
|
|
115
115
|
}
|
|
116
|
+
function isNeverSchema(schema) {
|
|
117
|
+
if (schema === false) {
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
if (typeof schema === "object" && schema.not !== void 0) {
|
|
121
|
+
if (schema.not === true) {
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
if (typeof schema.not === "object" && Object.keys(schema.not).length === 0) {
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
116
130
|
function separateObjectSchema(schema, separatedProperties) {
|
|
117
|
-
if (Object.keys(schema).some(
|
|
131
|
+
if (Object.keys(schema).some(
|
|
132
|
+
(k) => !["type", "properties", "required", "additionalProperties"].includes(k) && LOGIC_KEYWORDS.includes(k) && schema[k] !== void 0
|
|
133
|
+
)) {
|
|
118
134
|
return [{ type: "object" }, schema];
|
|
119
135
|
}
|
|
120
136
|
const matched = { ...schema };
|
|
121
137
|
const rest = { ...schema };
|
|
122
|
-
matched.properties =
|
|
123
|
-
|
|
138
|
+
matched.properties = separatedProperties.reduce((acc, key) => {
|
|
139
|
+
const keySchema = schema.properties?.[key] ?? schema.additionalProperties;
|
|
140
|
+
if (keySchema !== void 0) {
|
|
141
|
+
acc[key] = keySchema;
|
|
142
|
+
}
|
|
124
143
|
return acc;
|
|
125
144
|
}, {});
|
|
145
|
+
if (Object.keys(matched.properties).length === 0) {
|
|
146
|
+
matched.properties = void 0;
|
|
147
|
+
}
|
|
126
148
|
matched.required = schema.required?.filter((key) => separatedProperties.includes(key));
|
|
149
|
+
if (matched.required?.length === 0) {
|
|
150
|
+
matched.required = void 0;
|
|
151
|
+
}
|
|
127
152
|
matched.examples = schema.examples?.map((example) => {
|
|
128
153
|
if (!isObject(example)) {
|
|
129
154
|
return example;
|
|
@@ -135,11 +160,14 @@ function separateObjectSchema(schema, separatedProperties) {
|
|
|
135
160
|
return acc;
|
|
136
161
|
}, {});
|
|
137
162
|
});
|
|
138
|
-
rest.properties = schema.properties && Object.entries(schema.properties).filter(([key]) => !separatedProperties.includes(key)).reduce((acc, [key, value]) => {
|
|
163
|
+
rest.properties = schema.properties && Object.entries(schema.properties).filter(([key]) => !separatedProperties.includes(key)).reduce((acc = {}, [key, value]) => {
|
|
139
164
|
acc[key] = value;
|
|
140
165
|
return acc;
|
|
141
|
-
},
|
|
166
|
+
}, void 0);
|
|
142
167
|
rest.required = schema.required?.filter((key) => !separatedProperties.includes(key));
|
|
168
|
+
if (rest.required?.length === 0) {
|
|
169
|
+
rest.required = void 0;
|
|
170
|
+
}
|
|
143
171
|
rest.examples = schema.examples?.map((example) => {
|
|
144
172
|
if (!isObject(example)) {
|
|
145
173
|
return example;
|
|
@@ -250,7 +278,7 @@ function toOpenAPIContent(schema) {
|
|
|
250
278
|
schema: toOpenAPISchema(file)
|
|
251
279
|
};
|
|
252
280
|
}
|
|
253
|
-
if (restSchema !== void 0) {
|
|
281
|
+
if (restSchema !== void 0 && !isAnySchema(restSchema) && !isNeverSchema(restSchema)) {
|
|
254
282
|
content["application/json"] = {
|
|
255
283
|
schema: toOpenAPISchema(restSchema)
|
|
256
284
|
};
|
|
@@ -354,6 +382,107 @@ function resolveOpenAPIJsonSchemaRef(doc, schema) {
|
|
|
354
382
|
const resolved = doc.components?.schemas?.[name];
|
|
355
383
|
return resolved ?? schema;
|
|
356
384
|
}
|
|
385
|
+
function simplifyComposedObjectJsonSchemasAndRefs(schema, doc) {
|
|
386
|
+
if (doc) {
|
|
387
|
+
schema = resolveOpenAPIJsonSchemaRef(doc, schema);
|
|
388
|
+
}
|
|
389
|
+
if (typeof schema !== "object" || !schema.anyOf && !schema.oneOf && !schema.allOf) {
|
|
390
|
+
return schema;
|
|
391
|
+
}
|
|
392
|
+
const unionSchemas = [
|
|
393
|
+
...toArray(schema.anyOf?.map((s) => simplifyComposedObjectJsonSchemasAndRefs(s, doc))),
|
|
394
|
+
...toArray(schema.oneOf?.map((s) => simplifyComposedObjectJsonSchemasAndRefs(s, doc)))
|
|
395
|
+
];
|
|
396
|
+
const objectUnionSchemas = [];
|
|
397
|
+
for (const u of unionSchemas) {
|
|
398
|
+
if (!isObjectSchema(u)) {
|
|
399
|
+
return schema;
|
|
400
|
+
}
|
|
401
|
+
objectUnionSchemas.push(u);
|
|
402
|
+
}
|
|
403
|
+
const mergedUnionPropertyMap = /* @__PURE__ */ new Map();
|
|
404
|
+
for (const u of objectUnionSchemas) {
|
|
405
|
+
if (u.properties) {
|
|
406
|
+
for (const [key, value] of Object.entries(u.properties)) {
|
|
407
|
+
let entry = mergedUnionPropertyMap.get(key);
|
|
408
|
+
if (!entry) {
|
|
409
|
+
const required = objectUnionSchemas.every((s) => s.required?.includes(key));
|
|
410
|
+
entry = { required, schemas: [] };
|
|
411
|
+
mergedUnionPropertyMap.set(key, entry);
|
|
412
|
+
}
|
|
413
|
+
entry.schemas.push(value);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
const intersectionSchemas = toArray(schema.allOf?.map((s) => simplifyComposedObjectJsonSchemasAndRefs(s, doc)));
|
|
418
|
+
const objectIntersectionSchemas = [];
|
|
419
|
+
for (const u of intersectionSchemas) {
|
|
420
|
+
if (!isObjectSchema(u)) {
|
|
421
|
+
return schema;
|
|
422
|
+
}
|
|
423
|
+
objectIntersectionSchemas.push(u);
|
|
424
|
+
}
|
|
425
|
+
if (isObjectSchema(schema)) {
|
|
426
|
+
objectIntersectionSchemas.push(schema);
|
|
427
|
+
}
|
|
428
|
+
const mergedInteractionPropertyMap = /* @__PURE__ */ new Map();
|
|
429
|
+
for (const u of objectIntersectionSchemas) {
|
|
430
|
+
if (u.properties) {
|
|
431
|
+
for (const [key, value] of Object.entries(u.properties)) {
|
|
432
|
+
let entry = mergedInteractionPropertyMap.get(key);
|
|
433
|
+
if (!entry) {
|
|
434
|
+
const required = objectIntersectionSchemas.some((s) => s.required?.includes(key));
|
|
435
|
+
entry = { required, schemas: [] };
|
|
436
|
+
mergedInteractionPropertyMap.set(key, entry);
|
|
437
|
+
}
|
|
438
|
+
entry.schemas.push(value);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
const resultObjectSchema = { type: "object", properties: {}, required: [] };
|
|
443
|
+
const keys = /* @__PURE__ */ new Set([
|
|
444
|
+
...mergedUnionPropertyMap.keys(),
|
|
445
|
+
...mergedInteractionPropertyMap.keys()
|
|
446
|
+
]);
|
|
447
|
+
if (keys.size === 0) {
|
|
448
|
+
return schema;
|
|
449
|
+
}
|
|
450
|
+
const deduplicateSchemas = (schemas) => {
|
|
451
|
+
const seen = /* @__PURE__ */ new Set();
|
|
452
|
+
const result = [];
|
|
453
|
+
for (const schema2 of schemas) {
|
|
454
|
+
const key = stringifyJSON(schema2);
|
|
455
|
+
if (!seen.has(key)) {
|
|
456
|
+
seen.add(key);
|
|
457
|
+
result.push(schema2);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return result;
|
|
461
|
+
};
|
|
462
|
+
for (const key of keys) {
|
|
463
|
+
const unionEntry = mergedUnionPropertyMap.get(key);
|
|
464
|
+
const intersectionEntry = mergedInteractionPropertyMap.get(key);
|
|
465
|
+
resultObjectSchema.properties[key] = (() => {
|
|
466
|
+
const dedupedUnionSchemas = unionEntry ? deduplicateSchemas(unionEntry.schemas) : [];
|
|
467
|
+
const dedupedIntersectionSchemas = intersectionEntry ? deduplicateSchemas(intersectionEntry.schemas) : [];
|
|
468
|
+
if (!dedupedUnionSchemas.length) {
|
|
469
|
+
return dedupedIntersectionSchemas.length === 1 ? dedupedIntersectionSchemas[0] : { allOf: dedupedIntersectionSchemas };
|
|
470
|
+
}
|
|
471
|
+
if (!dedupedIntersectionSchemas.length) {
|
|
472
|
+
return dedupedUnionSchemas.length === 1 ? dedupedUnionSchemas[0] : { anyOf: dedupedUnionSchemas };
|
|
473
|
+
}
|
|
474
|
+
const allOf = deduplicateSchemas([
|
|
475
|
+
...dedupedIntersectionSchemas,
|
|
476
|
+
dedupedUnionSchemas.length === 1 ? dedupedUnionSchemas[0] : { anyOf: dedupedUnionSchemas }
|
|
477
|
+
]);
|
|
478
|
+
return allOf.length === 1 ? allOf[0] : { allOf };
|
|
479
|
+
})();
|
|
480
|
+
if (unionEntry?.required || intersectionEntry?.required) {
|
|
481
|
+
resultObjectSchema.required.push(key);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return resultObjectSchema;
|
|
485
|
+
}
|
|
357
486
|
|
|
358
487
|
class CompositeSchemaConverter {
|
|
359
488
|
converters;
|
|
@@ -382,37 +511,38 @@ class OpenAPIGenerator {
|
|
|
382
511
|
/**
|
|
383
512
|
* Generates OpenAPI specifications from oRPC routers/contracts.
|
|
384
513
|
*
|
|
385
|
-
* @see {@link https://orpc.
|
|
514
|
+
* @see {@link https://orpc.dev/docs/openapi/openapi-specification OpenAPI Specification Docs}
|
|
386
515
|
*/
|
|
387
|
-
async generate(router,
|
|
388
|
-
const
|
|
516
|
+
async generate(router, { customErrorResponseBodySchema, commonSchemas, filter: baseFilter, exclude, ...baseDoc } = {}) {
|
|
517
|
+
const filter = baseFilter ?? (({ contract, path }) => {
|
|
518
|
+
return !(exclude?.(contract, path) ?? false);
|
|
519
|
+
});
|
|
389
520
|
const doc = {
|
|
390
|
-
...clone(
|
|
391
|
-
info:
|
|
392
|
-
openapi: "3.1.1"
|
|
393
|
-
exclude: void 0,
|
|
394
|
-
commonSchemas: void 0
|
|
521
|
+
...clone(baseDoc),
|
|
522
|
+
info: baseDoc.info ?? { title: "API Reference", version: "0.0.0" },
|
|
523
|
+
openapi: "3.1.1"
|
|
395
524
|
};
|
|
396
|
-
const baseSchemaConvertOptions = await this.#resolveCommonSchemas(doc,
|
|
525
|
+
const { baseSchemaConvertOptions, undefinedErrorJsonSchema } = await this.#resolveCommonSchemas(doc, commonSchemas);
|
|
397
526
|
const contracts = [];
|
|
398
|
-
await resolveContractProcedures({ path: [], router }, (
|
|
399
|
-
if (!
|
|
400
|
-
|
|
527
|
+
await resolveContractProcedures({ path: [], router }, (traverseOptions) => {
|
|
528
|
+
if (!value(filter, traverseOptions)) {
|
|
529
|
+
return;
|
|
401
530
|
}
|
|
531
|
+
contracts.push(traverseOptions);
|
|
402
532
|
});
|
|
403
533
|
const errors = [];
|
|
404
534
|
for (const { contract, path } of contracts) {
|
|
405
|
-
const
|
|
535
|
+
const stringPath = path.join(".");
|
|
406
536
|
try {
|
|
407
537
|
const def = contract["~orpc"];
|
|
408
538
|
const method = toOpenAPIMethod(fallbackContractConfig("defaultMethod", def.route.method));
|
|
409
539
|
const httpPath = toOpenAPIPath(def.route.path ?? toHttpPath(path));
|
|
410
540
|
let operationObjectRef;
|
|
411
|
-
if (def.route.spec !== void 0) {
|
|
541
|
+
if (def.route.spec !== void 0 && typeof def.route.spec !== "function") {
|
|
412
542
|
operationObjectRef = def.route.spec;
|
|
413
543
|
} else {
|
|
414
544
|
operationObjectRef = {
|
|
415
|
-
operationId,
|
|
545
|
+
operationId: def.route.operationId ?? stringPath,
|
|
416
546
|
summary: def.route.summary,
|
|
417
547
|
description: def.route.description,
|
|
418
548
|
deprecated: def.route.deprecated,
|
|
@@ -420,7 +550,10 @@ class OpenAPIGenerator {
|
|
|
420
550
|
};
|
|
421
551
|
await this.#request(doc, operationObjectRef, def, baseSchemaConvertOptions);
|
|
422
552
|
await this.#successResponse(doc, operationObjectRef, def, baseSchemaConvertOptions);
|
|
423
|
-
await this.#errorResponse(operationObjectRef, def, baseSchemaConvertOptions);
|
|
553
|
+
await this.#errorResponse(operationObjectRef, def, baseSchemaConvertOptions, undefinedErrorJsonSchema, customErrorResponseBodySchema);
|
|
554
|
+
}
|
|
555
|
+
if (typeof def.route.spec === "function") {
|
|
556
|
+
operationObjectRef = def.route.spec(operationObjectRef);
|
|
424
557
|
}
|
|
425
558
|
doc.paths ??= {};
|
|
426
559
|
doc.paths[httpPath] ??= {};
|
|
@@ -430,7 +563,7 @@ class OpenAPIGenerator {
|
|
|
430
563
|
throw e;
|
|
431
564
|
}
|
|
432
565
|
errors.push(
|
|
433
|
-
`[OpenAPIGenerator] Error occurred while generating OpenAPI for procedure at path: ${
|
|
566
|
+
`[OpenAPIGenerator] Error occurred while generating OpenAPI for procedure at path: ${stringPath}
|
|
434
567
|
${e.message}`
|
|
435
568
|
);
|
|
436
569
|
}
|
|
@@ -445,11 +578,26 @@ ${errors.join("\n\n")}`
|
|
|
445
578
|
return this.serializer.serialize(doc)[0];
|
|
446
579
|
}
|
|
447
580
|
async #resolveCommonSchemas(doc, commonSchemas) {
|
|
448
|
-
|
|
581
|
+
let undefinedErrorJsonSchema = {
|
|
582
|
+
type: "object",
|
|
583
|
+
properties: {
|
|
584
|
+
defined: { const: false },
|
|
585
|
+
code: { type: "string" },
|
|
586
|
+
status: { type: "number" },
|
|
587
|
+
message: { type: "string" },
|
|
588
|
+
data: {}
|
|
589
|
+
},
|
|
590
|
+
required: ["defined", "code", "status", "message"]
|
|
591
|
+
};
|
|
592
|
+
const baseSchemaConvertOptions = {};
|
|
449
593
|
if (commonSchemas) {
|
|
450
|
-
|
|
594
|
+
baseSchemaConvertOptions.components = [];
|
|
451
595
|
for (const key in commonSchemas) {
|
|
452
|
-
const
|
|
596
|
+
const options = commonSchemas[key];
|
|
597
|
+
if (options.schema === void 0) {
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
const { schema, strategy = "input" } = options;
|
|
453
601
|
const [required, json] = await this.converter.convert(schema, { strategy });
|
|
454
602
|
const allowedStrategies = [strategy];
|
|
455
603
|
if (strategy === "input") {
|
|
@@ -463,7 +611,7 @@ ${errors.join("\n\n")}`
|
|
|
463
611
|
allowedStrategies.push("input");
|
|
464
612
|
}
|
|
465
613
|
}
|
|
466
|
-
|
|
614
|
+
baseSchemaConvertOptions.components.push({
|
|
467
615
|
schema,
|
|
468
616
|
required,
|
|
469
617
|
ref: `#/components/schemas/${key}`,
|
|
@@ -473,11 +621,19 @@ ${errors.join("\n\n")}`
|
|
|
473
621
|
doc.components ??= {};
|
|
474
622
|
doc.components.schemas ??= {};
|
|
475
623
|
for (const key in commonSchemas) {
|
|
476
|
-
const
|
|
624
|
+
const options = commonSchemas[key];
|
|
625
|
+
if (options.schema === void 0) {
|
|
626
|
+
if (options.error === "UndefinedError") {
|
|
627
|
+
doc.components.schemas[key] = toOpenAPISchema(undefinedErrorJsonSchema);
|
|
628
|
+
undefinedErrorJsonSchema = { $ref: `#/components/schemas/${key}` };
|
|
629
|
+
}
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
const { schema, strategy = "input" } = options;
|
|
477
633
|
const [, json] = await this.converter.convert(
|
|
478
634
|
schema,
|
|
479
635
|
{
|
|
480
|
-
...
|
|
636
|
+
...baseSchemaConvertOptions,
|
|
481
637
|
strategy,
|
|
482
638
|
minStructureDepthForRef: 1
|
|
483
639
|
// not allow use $ref for root schemas
|
|
@@ -486,7 +642,7 @@ ${errors.join("\n\n")}`
|
|
|
486
642
|
doc.components.schemas[key] = toOpenAPISchema(json);
|
|
487
643
|
}
|
|
488
644
|
}
|
|
489
|
-
return
|
|
645
|
+
return { baseSchemaConvertOptions, undefinedErrorJsonSchema };
|
|
490
646
|
}
|
|
491
647
|
async #request(doc, ref, def, baseSchemaConvertOptions) {
|
|
492
648
|
const method = fallbackContractConfig("defaultMethod", def.route.method);
|
|
@@ -507,13 +663,16 @@ ${errors.join("\n\n")}`
|
|
|
507
663
|
def.inputSchema,
|
|
508
664
|
{
|
|
509
665
|
...baseSchemaConvertOptions,
|
|
510
|
-
strategy: "input"
|
|
511
|
-
minStructureDepthForRef: dynamicParams?.length || inputStructure === "detailed" ? 1 : 0
|
|
666
|
+
strategy: "input"
|
|
512
667
|
}
|
|
513
668
|
);
|
|
669
|
+
let omitResponseBody = false;
|
|
514
670
|
if (isAnySchema(schema) && !dynamicParams?.length) {
|
|
515
671
|
return;
|
|
516
672
|
}
|
|
673
|
+
if (inputStructure === "detailed" || inputStructure === "compact" && (dynamicParams?.length || method === "GET")) {
|
|
674
|
+
schema = simplifyComposedObjectJsonSchemasAndRefs(schema, doc);
|
|
675
|
+
}
|
|
517
676
|
if (inputStructure === "compact") {
|
|
518
677
|
if (dynamicParams?.length) {
|
|
519
678
|
const error2 = new OpenAPIGeneratorError(
|
|
@@ -525,6 +684,7 @@ ${errors.join("\n\n")}`
|
|
|
525
684
|
const [paramsSchema, rest] = separateObjectSchema(schema, dynamicParams);
|
|
526
685
|
schema = rest;
|
|
527
686
|
required = rest.required ? rest.required.length !== 0 : false;
|
|
687
|
+
omitResponseBody = !required && !rest.properties;
|
|
528
688
|
if (!checkParamsSchema(paramsSchema, dynamicParams)) {
|
|
529
689
|
throw error2;
|
|
530
690
|
}
|
|
@@ -539,7 +699,7 @@ ${errors.join("\n\n")}`
|
|
|
539
699
|
}
|
|
540
700
|
ref.parameters ??= [];
|
|
541
701
|
ref.parameters.push(...toOpenAPIParameters(schema, "query"));
|
|
542
|
-
} else {
|
|
702
|
+
} else if (!omitResponseBody) {
|
|
543
703
|
ref.requestBody = {
|
|
544
704
|
required,
|
|
545
705
|
content: toOpenAPIContent(schema)
|
|
@@ -553,7 +713,7 @@ ${errors.join("\n\n")}`
|
|
|
553
713
|
if (!isObjectSchema(schema)) {
|
|
554
714
|
throw error;
|
|
555
715
|
}
|
|
556
|
-
const resolvedParamSchema = schema.properties?.params !== void 0 ?
|
|
716
|
+
const resolvedParamSchema = schema.properties?.params !== void 0 ? simplifyComposedObjectJsonSchemasAndRefs(schema.properties.params, doc) : void 0;
|
|
557
717
|
if (dynamicParams?.length && (resolvedParamSchema === void 0 || !isObjectSchema(resolvedParamSchema) || !checkParamsSchema(resolvedParamSchema, dynamicParams))) {
|
|
558
718
|
throw new OpenAPIGeneratorError(
|
|
559
719
|
'When input structure is "detailed" and path has dynamic params, the "params" schema must be an object with all dynamic params as required.'
|
|
@@ -562,7 +722,7 @@ ${errors.join("\n\n")}`
|
|
|
562
722
|
for (const from of ["params", "query", "headers"]) {
|
|
563
723
|
const fromSchema = schema.properties?.[from];
|
|
564
724
|
if (fromSchema !== void 0) {
|
|
565
|
-
const resolvedSchema =
|
|
725
|
+
const resolvedSchema = simplifyComposedObjectJsonSchemasAndRefs(fromSchema, doc);
|
|
566
726
|
if (!isObjectSchema(resolvedSchema)) {
|
|
567
727
|
throw error;
|
|
568
728
|
}
|
|
@@ -623,13 +783,14 @@ ${errors.join("\n\n")}`
|
|
|
623
783
|
|
|
624
784
|
But got: ${stringifyJSON(item)}
|
|
625
785
|
`);
|
|
626
|
-
|
|
786
|
+
const simplifiedItem = simplifyComposedObjectJsonSchemasAndRefs(item, doc);
|
|
787
|
+
if (!isObjectSchema(simplifiedItem)) {
|
|
627
788
|
throw error;
|
|
628
789
|
}
|
|
629
790
|
let schemaStatus;
|
|
630
791
|
let schemaDescription;
|
|
631
|
-
if (
|
|
632
|
-
const statusSchema = resolveOpenAPIJsonSchemaRef(doc,
|
|
792
|
+
if (simplifiedItem.properties?.status !== void 0) {
|
|
793
|
+
const statusSchema = resolveOpenAPIJsonSchemaRef(doc, simplifiedItem.properties.status);
|
|
633
794
|
if (typeof statusSchema !== "object" || statusSchema.const === void 0 || typeof statusSchema.const !== "number" || !Number.isInteger(statusSchema.const) || isORPCErrorStatus(statusSchema.const)) {
|
|
634
795
|
throw error;
|
|
635
796
|
}
|
|
@@ -649,8 +810,8 @@ ${errors.join("\n\n")}`
|
|
|
649
810
|
ref.responses[itemStatus] = {
|
|
650
811
|
description: itemDescription
|
|
651
812
|
};
|
|
652
|
-
if (
|
|
653
|
-
const headersSchema =
|
|
813
|
+
if (simplifiedItem.properties?.headers !== void 0) {
|
|
814
|
+
const headersSchema = simplifyComposedObjectJsonSchemasAndRefs(simplifiedItem.properties.headers, doc);
|
|
654
815
|
if (!isObjectSchema(headersSchema)) {
|
|
655
816
|
throw error;
|
|
656
817
|
}
|
|
@@ -660,61 +821,53 @@ ${errors.join("\n\n")}`
|
|
|
660
821
|
ref.responses[itemStatus].headers ??= {};
|
|
661
822
|
ref.responses[itemStatus].headers[key] = {
|
|
662
823
|
schema: toOpenAPISchema(headerSchema),
|
|
663
|
-
required:
|
|
824
|
+
required: simplifiedItem.required?.includes("headers") && headersSchema.required?.includes(key)
|
|
664
825
|
};
|
|
665
826
|
}
|
|
666
827
|
}
|
|
667
828
|
}
|
|
668
|
-
if (
|
|
829
|
+
if (simplifiedItem.properties?.body !== void 0) {
|
|
669
830
|
ref.responses[itemStatus].content = toOpenAPIContent(
|
|
670
|
-
applySchemaOptionality(
|
|
831
|
+
applySchemaOptionality(simplifiedItem.required?.includes("body") ?? false, simplifiedItem.properties.body)
|
|
671
832
|
);
|
|
672
833
|
}
|
|
673
834
|
}
|
|
674
835
|
}
|
|
675
|
-
async #errorResponse(ref, def, baseSchemaConvertOptions) {
|
|
836
|
+
async #errorResponse(ref, def, baseSchemaConvertOptions, undefinedErrorSchema, customErrorResponseBodySchema) {
|
|
676
837
|
const errorMap = def.errorMap;
|
|
677
|
-
const
|
|
838
|
+
const errorResponsesByStatus = {};
|
|
678
839
|
for (const code in errorMap) {
|
|
679
840
|
const config = errorMap[code];
|
|
680
841
|
if (!config) {
|
|
681
842
|
continue;
|
|
682
843
|
}
|
|
683
844
|
const status = fallbackORPCErrorStatus(code, config.status);
|
|
684
|
-
const
|
|
845
|
+
const defaultMessage = fallbackORPCErrorMessage(code, config.message);
|
|
846
|
+
errorResponsesByStatus[status] ??= { status, definedErrorDefinitions: [], errorSchemaVariants: [] };
|
|
685
847
|
const [dataRequired, dataSchema] = await this.converter.convert(config.data, { ...baseSchemaConvertOptions, strategy: "output" });
|
|
686
|
-
|
|
687
|
-
|
|
848
|
+
errorResponsesByStatus[status].definedErrorDefinitions.push([code, defaultMessage, dataRequired, dataSchema]);
|
|
849
|
+
errorResponsesByStatus[status].errorSchemaVariants.push({
|
|
688
850
|
type: "object",
|
|
689
851
|
properties: {
|
|
690
852
|
defined: { const: true },
|
|
691
853
|
code: { const: code },
|
|
692
854
|
status: { const: status },
|
|
693
|
-
message: { type: "string", default:
|
|
855
|
+
message: { type: "string", default: defaultMessage },
|
|
694
856
|
data: dataSchema
|
|
695
857
|
},
|
|
696
858
|
required: dataRequired ? ["defined", "code", "status", "message", "data"] : ["defined", "code", "status", "message"]
|
|
697
859
|
});
|
|
698
860
|
}
|
|
699
861
|
ref.responses ??= {};
|
|
700
|
-
for (const
|
|
701
|
-
const
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
862
|
+
for (const statusString in errorResponsesByStatus) {
|
|
863
|
+
const errorResponse = errorResponsesByStatus[statusString];
|
|
864
|
+
const customBodySchema = value(customErrorResponseBodySchema, errorResponse.definedErrorDefinitions, errorResponse.status);
|
|
865
|
+
ref.responses[statusString] = {
|
|
866
|
+
description: statusString,
|
|
867
|
+
content: toOpenAPIContent(customBodySchema ?? {
|
|
705
868
|
oneOf: [
|
|
706
|
-
...
|
|
707
|
-
|
|
708
|
-
type: "object",
|
|
709
|
-
properties: {
|
|
710
|
-
defined: { const: false },
|
|
711
|
-
code: { type: "string" },
|
|
712
|
-
status: { type: "number" },
|
|
713
|
-
message: { type: "string" },
|
|
714
|
-
data: {}
|
|
715
|
-
},
|
|
716
|
-
required: ["defined", "code", "status", "message"]
|
|
717
|
-
}
|
|
869
|
+
...errorResponse.errorSchemaVariants,
|
|
870
|
+
undefinedErrorSchema
|
|
718
871
|
]
|
|
719
872
|
})
|
|
720
873
|
};
|
|
@@ -722,4 +875,4 @@ ${errors.join("\n\n")}`
|
|
|
722
875
|
}
|
|
723
876
|
}
|
|
724
877
|
|
|
725
|
-
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,
|
|
878
|
+
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, isNeverSchema as m, separateObjectSchema as n, filterSchemaBranches as o, applySchemaOptionality as p, expandUnionSchema as q, resolveOpenAPIJsonSchemaRef as r, simplifyComposedObjectJsonSchemasAndRefs as s, toOpenAPIPath as t, expandArrayableSchema as u, isPrimitiveSchema as v };
|
|
@@ -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 };
|