@orpc/openapi 0.0.0-next.a09e9be → 0.0.0-next.a153125
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 -7
- package/dist/adapters/aws-lambda/index.d.mts +17 -0
- package/dist/adapters/aws-lambda/index.d.ts +17 -0
- package/dist/adapters/aws-lambda/index.mjs +18 -0
- package/dist/adapters/fetch/index.mjs +2 -1
- package/dist/adapters/node/index.mjs +2 -1
- package/dist/adapters/standard/index.d.mts +1 -0
- package/dist/adapters/standard/index.d.ts +1 -0
- package/dist/adapters/standard/index.mjs +2 -1
- package/dist/index.d.mts +23 -12
- package/dist/index.d.ts +23 -12
- package/dist/index.mjs +3 -3
- package/dist/plugins/index.d.mts +9 -10
- package/dist/plugins/index.d.ts +9 -10
- package/dist/plugins/index.mjs +1 -1
- package/dist/shared/openapi.B3hexduL.d.mts +101 -0
- package/dist/shared/openapi.B3hexduL.d.ts +101 -0
- package/dist/shared/{openapi.p5tsmBXx.mjs → openapi.C_UtQ8Us.mjs} +27 -6
- package/dist/shared/{openapi.fMEQd3Yd.mjs → openapi.DrrBsJ0w.mjs} +260 -66
- package/package.json +14 -10
- package/dist/shared/openapi.DP97kr00.d.mts +0 -47
- package/dist/shared/openapi.DP97kr00.d.ts +0 -47
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { AnySchema, OpenAPI, AnyContractProcedure, AnyContractRouter } from '@orpc/contract';
|
|
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';
|
|
6
|
+
|
|
7
|
+
interface SchemaConverterComponent {
|
|
8
|
+
allowedStrategies: readonly SchemaConvertOptions['strategy'][];
|
|
9
|
+
schema: AnySchema;
|
|
10
|
+
required: boolean;
|
|
11
|
+
ref: string;
|
|
12
|
+
}
|
|
13
|
+
interface SchemaConvertOptions {
|
|
14
|
+
strategy: 'input' | 'output';
|
|
15
|
+
/**
|
|
16
|
+
* Common components should use `$ref` to represent themselves if matched.
|
|
17
|
+
*/
|
|
18
|
+
components?: readonly SchemaConverterComponent[];
|
|
19
|
+
/**
|
|
20
|
+
* Minimum schema structure depth required before using `$ref` for components.
|
|
21
|
+
*
|
|
22
|
+
* For example, if set to 2, `$ref` will only be used for schemas nested at depth 2 or greater.
|
|
23
|
+
*
|
|
24
|
+
* @default 0 - No depth limit;
|
|
25
|
+
*/
|
|
26
|
+
minStructureDepthForRef?: number;
|
|
27
|
+
}
|
|
28
|
+
interface SchemaConverter {
|
|
29
|
+
convert(schema: AnySchema | undefined, options: SchemaConvertOptions): Promisable<[required: boolean, jsonSchema: JSONSchema]>;
|
|
30
|
+
}
|
|
31
|
+
interface ConditionalSchemaConverter extends SchemaConverter {
|
|
32
|
+
condition(schema: AnySchema | undefined, options: SchemaConvertOptions): Promisable<boolean>;
|
|
33
|
+
}
|
|
34
|
+
declare class CompositeSchemaConverter implements SchemaConverter {
|
|
35
|
+
private readonly converters;
|
|
36
|
+
constructor(converters: ConditionalSchemaConverter[]);
|
|
37
|
+
convert(schema: AnySchema | undefined, options: SchemaConvertOptions): Promise<[required: boolean, jsonSchema: JSONSchema]>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface OpenAPIGeneratorOptions extends StandardOpenAPIJsonSerializerOptions {
|
|
41
|
+
schemaConverters?: ConditionalSchemaConverter[];
|
|
42
|
+
}
|
|
43
|
+
interface OpenAPIGeneratorGenerateOptions extends Partial<Omit<OpenAPI.Document, 'openapi'>> {
|
|
44
|
+
/**
|
|
45
|
+
* Exclude procedures from the OpenAPI specification.
|
|
46
|
+
*
|
|
47
|
+
* @default () => false
|
|
48
|
+
*/
|
|
49
|
+
exclude?: (procedure: AnyProcedure | AnyContractProcedure, path: readonly string[]) => boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Common schemas to be used for $ref resolution.
|
|
52
|
+
*/
|
|
53
|
+
commonSchemas?: Record<string, {
|
|
54
|
+
/**
|
|
55
|
+
* Determines which schema definition to use when input and output schemas differ.
|
|
56
|
+
* This is needed because some schemas transform data differently between input and output,
|
|
57
|
+
* making it impossible to use a single $ref for both cases.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* // This schema transforms a string input into a number output
|
|
62
|
+
* const Schema = z.string()
|
|
63
|
+
* .transform(v => Number(v))
|
|
64
|
+
* .pipe(z.number())
|
|
65
|
+
*
|
|
66
|
+
* // Input schema: { type: 'string' }
|
|
67
|
+
* // Output schema: { type: 'number' }
|
|
68
|
+
* ```
|
|
69
|
+
*
|
|
70
|
+
* When schemas differ between input and output, you must explicitly choose
|
|
71
|
+
* which version to use for the OpenAPI specification.
|
|
72
|
+
*
|
|
73
|
+
* @default 'input' - Uses the input schema definition by default
|
|
74
|
+
*/
|
|
75
|
+
strategy?: SchemaConvertOptions['strategy'];
|
|
76
|
+
schema: AnySchema;
|
|
77
|
+
} | {
|
|
78
|
+
error: 'UndefinedError';
|
|
79
|
+
schema?: never;
|
|
80
|
+
}>;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The generator that converts oRPC routers/contracts to OpenAPI specifications.
|
|
84
|
+
*
|
|
85
|
+
* @see {@link https://orpc.unnoq.com/docs/openapi/openapi-specification OpenAPI Specification Docs}
|
|
86
|
+
*/
|
|
87
|
+
declare class OpenAPIGenerator {
|
|
88
|
+
#private;
|
|
89
|
+
private readonly serializer;
|
|
90
|
+
private readonly converter;
|
|
91
|
+
constructor(options?: OpenAPIGeneratorOptions);
|
|
92
|
+
/**
|
|
93
|
+
* Generates OpenAPI specifications from oRPC routers/contracts.
|
|
94
|
+
*
|
|
95
|
+
* @see {@link https://orpc.unnoq.com/docs/openapi/openapi-specification OpenAPI Specification Docs}
|
|
96
|
+
*/
|
|
97
|
+
generate(router: AnyContractRouter | AnyRouter, options?: OpenAPIGeneratorGenerateOptions): Promise<OpenAPI.Document>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export { OpenAPIGenerator as b, CompositeSchemaConverter as e };
|
|
101
|
+
export type { ConditionalSchemaConverter as C, OpenAPIGeneratorOptions as O, SchemaConverterComponent as S, OpenAPIGeneratorGenerateOptions as a, SchemaConvertOptions as c, SchemaConverter as d };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { standardizeHTTPPath, StandardOpenAPIJsonSerializer, StandardBracketNotationSerializer, StandardOpenAPISerializer } from '@orpc/openapi-client/standard';
|
|
2
2
|
import { StandardHandler } from '@orpc/server/standard';
|
|
3
|
+
import { isORPCErrorStatus } from '@orpc/client';
|
|
3
4
|
import { fallbackContractConfig } from '@orpc/contract';
|
|
4
|
-
import { isObject } from '@orpc/shared';
|
|
5
|
+
import { isObject, stringifyJSON } from '@orpc/shared';
|
|
5
6
|
import { toHttpPath } from '@orpc/client/standard';
|
|
6
7
|
import { traverseContractProcedures, isProcedure, getLazyMeta, unlazy, getRouter, createContractedProcedure } from '@orpc/server';
|
|
7
8
|
import { createRouter, addRoute, findRoute } from 'rou3';
|
|
@@ -52,13 +53,21 @@ class StandardOpenAPICodec {
|
|
|
52
53
|
body: this.serializer.serialize(output)
|
|
53
54
|
};
|
|
54
55
|
}
|
|
55
|
-
if (!
|
|
56
|
-
throw new Error(
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
if (!this.#isDetailedOutput(output)) {
|
|
57
|
+
throw new Error(`
|
|
58
|
+
Invalid "detailed" output structure:
|
|
59
|
+
\u2022 Expected an object with optional properties:
|
|
60
|
+
- status (number 200-399)
|
|
61
|
+
- headers (Record<string, string | string[]>)
|
|
62
|
+
- body (any)
|
|
63
|
+
\u2022 No extra keys allowed.
|
|
64
|
+
|
|
65
|
+
Actual value:
|
|
66
|
+
${stringifyJSON(output)}
|
|
67
|
+
`);
|
|
59
68
|
}
|
|
60
69
|
return {
|
|
61
|
-
status: successStatus,
|
|
70
|
+
status: output.status ?? successStatus,
|
|
62
71
|
headers: output.headers ?? {},
|
|
63
72
|
body: this.serializer.serialize(output.body)
|
|
64
73
|
};
|
|
@@ -70,6 +79,18 @@ class StandardOpenAPICodec {
|
|
|
70
79
|
body: this.serializer.serialize(error.toJSON(), { outputFormat: "plain" })
|
|
71
80
|
};
|
|
72
81
|
}
|
|
82
|
+
#isDetailedOutput(output) {
|
|
83
|
+
if (!isObject(output)) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
if (output.headers && !isObject(output.headers)) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
if (output.status !== void 0 && (typeof output.status !== "number" || !Number.isInteger(output.status) || isORPCErrorStatus(output.status))) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
73
94
|
}
|
|
74
95
|
|
|
75
96
|
function toRou3Pattern(path) {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { fallbackORPCErrorStatus, fallbackORPCErrorMessage } from '@orpc/client';
|
|
1
|
+
import { isORPCErrorStatus, fallbackORPCErrorStatus, fallbackORPCErrorMessage } from '@orpc/client';
|
|
2
2
|
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, findDeepMatches, toArray, clone } from '@orpc/shared';
|
|
7
|
-
import 'json-schema-typed/draft-2020-12';
|
|
6
|
+
import { isObject, stringifyJSON, findDeepMatches, toArray, clone } from '@orpc/shared';
|
|
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) {
|
|
@@ -184,6 +184,57 @@ function applySchemaOptionality(required, schema) {
|
|
|
184
184
|
]
|
|
185
185
|
};
|
|
186
186
|
}
|
|
187
|
+
function expandUnionSchema(schema) {
|
|
188
|
+
if (typeof schema === "object") {
|
|
189
|
+
for (const keyword of ["anyOf", "oneOf"]) {
|
|
190
|
+
if (schema[keyword] && Object.keys(schema).every(
|
|
191
|
+
(k) => k === keyword || !LOGIC_KEYWORDS.includes(k)
|
|
192
|
+
)) {
|
|
193
|
+
return schema[keyword].flatMap((s) => expandUnionSchema(s));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return [schema];
|
|
198
|
+
}
|
|
199
|
+
function expandArrayableSchema(schema) {
|
|
200
|
+
const schemas = expandUnionSchema(schema);
|
|
201
|
+
if (schemas.length !== 2) {
|
|
202
|
+
return void 0;
|
|
203
|
+
}
|
|
204
|
+
const arraySchema = schemas.find(
|
|
205
|
+
(s) => typeof s === "object" && s.type === "array" && Object.keys(s).filter((k) => LOGIC_KEYWORDS.includes(k)).every((k) => k === "type" || k === "items")
|
|
206
|
+
);
|
|
207
|
+
if (arraySchema === void 0) {
|
|
208
|
+
return void 0;
|
|
209
|
+
}
|
|
210
|
+
const items1 = arraySchema.items;
|
|
211
|
+
const items2 = schemas.find((s) => s !== arraySchema);
|
|
212
|
+
if (stringifyJSON(items1) !== stringifyJSON(items2)) {
|
|
213
|
+
return void 0;
|
|
214
|
+
}
|
|
215
|
+
return [items2, arraySchema];
|
|
216
|
+
}
|
|
217
|
+
const PRIMITIVE_SCHEMA_TYPES = /* @__PURE__ */ new Set([
|
|
218
|
+
TypeName.String,
|
|
219
|
+
TypeName.Number,
|
|
220
|
+
TypeName.Integer,
|
|
221
|
+
TypeName.Boolean,
|
|
222
|
+
TypeName.Null
|
|
223
|
+
]);
|
|
224
|
+
function isPrimitiveSchema(schema) {
|
|
225
|
+
return expandUnionSchema(schema).every((s) => {
|
|
226
|
+
if (typeof s === "boolean") {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
if (typeof s.type === "string" && PRIMITIVE_SCHEMA_TYPES.has(s.type)) {
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
if (s.const !== void 0) {
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
return false;
|
|
236
|
+
});
|
|
237
|
+
}
|
|
187
238
|
|
|
188
239
|
function toOpenAPIPath(path) {
|
|
189
240
|
return standardizeHTTPPath(path).replace(/\/\{\+([^}]+)\}/g, "/{$1}");
|
|
@@ -256,13 +307,26 @@ function toOpenAPIParameters(schema, parameterIn) {
|
|
|
256
307
|
const parameters = [];
|
|
257
308
|
for (const key in schema.properties) {
|
|
258
309
|
const keySchema = schema.properties[key];
|
|
310
|
+
let isDeepObjectStyle = true;
|
|
311
|
+
if (parameterIn !== "query") {
|
|
312
|
+
isDeepObjectStyle = false;
|
|
313
|
+
} else if (isPrimitiveSchema(keySchema)) {
|
|
314
|
+
isDeepObjectStyle = false;
|
|
315
|
+
} else {
|
|
316
|
+
const [item] = expandArrayableSchema(keySchema) ?? [];
|
|
317
|
+
if (item !== void 0 && isPrimitiveSchema(item)) {
|
|
318
|
+
isDeepObjectStyle = false;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
259
321
|
parameters.push({
|
|
260
322
|
name: key,
|
|
261
323
|
in: parameterIn,
|
|
262
324
|
required: schema.required?.includes(key),
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
325
|
+
schema: toOpenAPISchema(keySchema),
|
|
326
|
+
style: isDeepObjectStyle ? "deepObject" : void 0,
|
|
327
|
+
explode: isDeepObjectStyle ? true : void 0,
|
|
328
|
+
allowEmptyValue: parameterIn === "query" ? true : void 0,
|
|
329
|
+
allowReserved: parameterIn === "query" ? true : void 0
|
|
266
330
|
});
|
|
267
331
|
}
|
|
268
332
|
return parameters;
|
|
@@ -281,6 +345,15 @@ function checkParamsSchema(schema, params) {
|
|
|
281
345
|
function toOpenAPISchema(schema) {
|
|
282
346
|
return schema === true ? {} : schema === false ? { not: {} } : schema;
|
|
283
347
|
}
|
|
348
|
+
const OPENAPI_JSON_SCHEMA_REF_PREFIX = "#/components/schemas/";
|
|
349
|
+
function resolveOpenAPIJsonSchemaRef(doc, schema) {
|
|
350
|
+
if (typeof schema !== "object" || !schema.$ref?.startsWith(OPENAPI_JSON_SCHEMA_REF_PREFIX)) {
|
|
351
|
+
return schema;
|
|
352
|
+
}
|
|
353
|
+
const name = schema.$ref.slice(OPENAPI_JSON_SCHEMA_REF_PREFIX.length);
|
|
354
|
+
const resolved = doc.components?.schemas?.[name];
|
|
355
|
+
return resolved ?? schema;
|
|
356
|
+
}
|
|
284
357
|
|
|
285
358
|
class CompositeSchemaConverter {
|
|
286
359
|
converters;
|
|
@@ -312,14 +385,20 @@ class OpenAPIGenerator {
|
|
|
312
385
|
* @see {@link https://orpc.unnoq.com/docs/openapi/openapi-specification OpenAPI Specification Docs}
|
|
313
386
|
*/
|
|
314
387
|
async generate(router, options = {}) {
|
|
388
|
+
const exclude = options.exclude ?? (() => false);
|
|
315
389
|
const doc = {
|
|
316
390
|
...clone(options),
|
|
317
391
|
info: options.info ?? { title: "API Reference", version: "0.0.0" },
|
|
318
|
-
openapi: "3.1.1"
|
|
392
|
+
openapi: "3.1.1",
|
|
393
|
+
exclude: void 0,
|
|
394
|
+
commonSchemas: void 0
|
|
319
395
|
};
|
|
396
|
+
const { baseSchemaConvertOptions, undefinedErrorJsonSchema } = await this.#resolveCommonSchemas(doc, options.commonSchemas);
|
|
320
397
|
const contracts = [];
|
|
321
398
|
await resolveContractProcedures({ path: [], router }, ({ contract, path }) => {
|
|
322
|
-
|
|
399
|
+
if (!exclude(contract, path)) {
|
|
400
|
+
contracts.push({ contract, path });
|
|
401
|
+
}
|
|
323
402
|
});
|
|
324
403
|
const errors = [];
|
|
325
404
|
for (const { contract, path } of contracts) {
|
|
@@ -328,16 +407,21 @@ class OpenAPIGenerator {
|
|
|
328
407
|
const def = contract["~orpc"];
|
|
329
408
|
const method = toOpenAPIMethod(fallbackContractConfig("defaultMethod", def.route.method));
|
|
330
409
|
const httpPath = toOpenAPIPath(def.route.path ?? toHttpPath(path));
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
410
|
+
let operationObjectRef;
|
|
411
|
+
if (def.route.spec !== void 0) {
|
|
412
|
+
operationObjectRef = def.route.spec;
|
|
413
|
+
} else {
|
|
414
|
+
operationObjectRef = {
|
|
415
|
+
operationId,
|
|
416
|
+
summary: def.route.summary,
|
|
417
|
+
description: def.route.description,
|
|
418
|
+
deprecated: def.route.deprecated,
|
|
419
|
+
tags: def.route.tags?.map((tag) => tag)
|
|
420
|
+
};
|
|
421
|
+
await this.#request(doc, operationObjectRef, def, baseSchemaConvertOptions);
|
|
422
|
+
await this.#successResponse(doc, operationObjectRef, def, baseSchemaConvertOptions);
|
|
423
|
+
await this.#errorResponse(operationObjectRef, def, baseSchemaConvertOptions, undefinedErrorJsonSchema);
|
|
424
|
+
}
|
|
341
425
|
doc.paths ??= {};
|
|
342
426
|
doc.paths[httpPath] ??= {};
|
|
343
427
|
doc.paths[httpPath][method] = applyCustomOpenAPIOperation(operationObjectRef, contract);
|
|
@@ -360,22 +444,96 @@ ${errors.join("\n\n")}`
|
|
|
360
444
|
}
|
|
361
445
|
return this.serializer.serialize(doc)[0];
|
|
362
446
|
}
|
|
363
|
-
async #
|
|
447
|
+
async #resolveCommonSchemas(doc, commonSchemas) {
|
|
448
|
+
let undefinedErrorJsonSchema = {
|
|
449
|
+
type: "object",
|
|
450
|
+
properties: {
|
|
451
|
+
defined: { const: false },
|
|
452
|
+
code: { type: "string" },
|
|
453
|
+
status: { type: "number" },
|
|
454
|
+
message: { type: "string" },
|
|
455
|
+
data: {}
|
|
456
|
+
},
|
|
457
|
+
required: ["defined", "code", "status", "message"]
|
|
458
|
+
};
|
|
459
|
+
const baseSchemaConvertOptions = {};
|
|
460
|
+
if (commonSchemas) {
|
|
461
|
+
baseSchemaConvertOptions.components = [];
|
|
462
|
+
for (const key in commonSchemas) {
|
|
463
|
+
const options = commonSchemas[key];
|
|
464
|
+
if (options.schema === void 0) {
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
const { schema, strategy = "input" } = options;
|
|
468
|
+
const [required, json] = await this.converter.convert(schema, { strategy });
|
|
469
|
+
const allowedStrategies = [strategy];
|
|
470
|
+
if (strategy === "input") {
|
|
471
|
+
const [outputRequired, outputJson] = await this.converter.convert(schema, { strategy: "output" });
|
|
472
|
+
if (outputRequired === required && stringifyJSON(outputJson) === stringifyJSON(json)) {
|
|
473
|
+
allowedStrategies.push("output");
|
|
474
|
+
}
|
|
475
|
+
} else if (strategy === "output") {
|
|
476
|
+
const [inputRequired, inputJson] = await this.converter.convert(schema, { strategy: "input" });
|
|
477
|
+
if (inputRequired === required && stringifyJSON(inputJson) === stringifyJSON(json)) {
|
|
478
|
+
allowedStrategies.push("input");
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
baseSchemaConvertOptions.components.push({
|
|
482
|
+
schema,
|
|
483
|
+
required,
|
|
484
|
+
ref: `#/components/schemas/${key}`,
|
|
485
|
+
allowedStrategies
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
doc.components ??= {};
|
|
489
|
+
doc.components.schemas ??= {};
|
|
490
|
+
for (const key in commonSchemas) {
|
|
491
|
+
const options = commonSchemas[key];
|
|
492
|
+
if (options.schema === void 0) {
|
|
493
|
+
if (options.error === "UndefinedError") {
|
|
494
|
+
doc.components.schemas[key] = toOpenAPISchema(undefinedErrorJsonSchema);
|
|
495
|
+
undefinedErrorJsonSchema = { $ref: `#/components/schemas/${key}` };
|
|
496
|
+
}
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
const { schema, strategy = "input" } = options;
|
|
500
|
+
const [, json] = await this.converter.convert(
|
|
501
|
+
schema,
|
|
502
|
+
{
|
|
503
|
+
...baseSchemaConvertOptions,
|
|
504
|
+
strategy,
|
|
505
|
+
minStructureDepthForRef: 1
|
|
506
|
+
// not allow use $ref for root schemas
|
|
507
|
+
}
|
|
508
|
+
);
|
|
509
|
+
doc.components.schemas[key] = toOpenAPISchema(json);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return { baseSchemaConvertOptions, undefinedErrorJsonSchema };
|
|
513
|
+
}
|
|
514
|
+
async #request(doc, ref, def, baseSchemaConvertOptions) {
|
|
364
515
|
const method = fallbackContractConfig("defaultMethod", def.route.method);
|
|
365
516
|
const details = getEventIteratorSchemaDetails(def.inputSchema);
|
|
366
517
|
if (details) {
|
|
367
518
|
ref.requestBody = {
|
|
368
519
|
required: true,
|
|
369
520
|
content: toOpenAPIEventIteratorContent(
|
|
370
|
-
await this.converter.convert(details.yields, { strategy: "input" }),
|
|
371
|
-
await this.converter.convert(details.returns, { strategy: "input" })
|
|
521
|
+
await this.converter.convert(details.yields, { ...baseSchemaConvertOptions, strategy: "input" }),
|
|
522
|
+
await this.converter.convert(details.returns, { ...baseSchemaConvertOptions, strategy: "input" })
|
|
372
523
|
)
|
|
373
524
|
};
|
|
374
525
|
return;
|
|
375
526
|
}
|
|
376
527
|
const dynamicParams = getDynamicParams(def.route.path)?.map((v) => v.name);
|
|
377
528
|
const inputStructure = fallbackContractConfig("defaultInputStructure", def.route.inputStructure);
|
|
378
|
-
let [required, schema] = await this.converter.convert(
|
|
529
|
+
let [required, schema] = await this.converter.convert(
|
|
530
|
+
def.inputSchema,
|
|
531
|
+
{
|
|
532
|
+
...baseSchemaConvertOptions,
|
|
533
|
+
strategy: "input",
|
|
534
|
+
minStructureDepthForRef: dynamicParams?.length || inputStructure === "detailed" ? 1 : 0
|
|
535
|
+
}
|
|
536
|
+
);
|
|
379
537
|
if (isAnySchema(schema) && !dynamicParams?.length) {
|
|
380
538
|
return;
|
|
381
539
|
}
|
|
@@ -418,7 +576,8 @@ ${errors.join("\n\n")}`
|
|
|
418
576
|
if (!isObjectSchema(schema)) {
|
|
419
577
|
throw error;
|
|
420
578
|
}
|
|
421
|
-
|
|
579
|
+
const resolvedParamSchema = schema.properties?.params !== void 0 ? resolveOpenAPIJsonSchemaRef(doc, schema.properties.params) : void 0;
|
|
580
|
+
if (dynamicParams?.length && (resolvedParamSchema === void 0 || !isObjectSchema(resolvedParamSchema) || !checkParamsSchema(resolvedParamSchema, dynamicParams))) {
|
|
422
581
|
throw new OpenAPIGeneratorError(
|
|
423
582
|
'When input structure is "detailed" and path has dynamic params, the "params" schema must be an object with all dynamic params as required.'
|
|
424
583
|
);
|
|
@@ -426,12 +585,13 @@ ${errors.join("\n\n")}`
|
|
|
426
585
|
for (const from of ["params", "query", "headers"]) {
|
|
427
586
|
const fromSchema = schema.properties?.[from];
|
|
428
587
|
if (fromSchema !== void 0) {
|
|
429
|
-
|
|
588
|
+
const resolvedSchema = resolveOpenAPIJsonSchemaRef(doc, fromSchema);
|
|
589
|
+
if (!isObjectSchema(resolvedSchema)) {
|
|
430
590
|
throw error;
|
|
431
591
|
}
|
|
432
592
|
const parameterIn = from === "params" ? "path" : from === "headers" ? "header" : "query";
|
|
433
593
|
ref.parameters ??= [];
|
|
434
|
-
ref.parameters.push(...toOpenAPIParameters(
|
|
594
|
+
ref.parameters.push(...toOpenAPIParameters(resolvedSchema, parameterIn));
|
|
435
595
|
}
|
|
436
596
|
}
|
|
437
597
|
if (schema.properties?.body !== void 0) {
|
|
@@ -441,7 +601,7 @@ ${errors.join("\n\n")}`
|
|
|
441
601
|
};
|
|
442
602
|
}
|
|
443
603
|
}
|
|
444
|
-
async #successResponse(ref, def) {
|
|
604
|
+
async #successResponse(doc, ref, def, baseSchemaConvertOptions) {
|
|
445
605
|
const outputSchema = def.outputSchema;
|
|
446
606
|
const status = fallbackContractConfig("defaultSuccessStatus", def.route.successStatus);
|
|
447
607
|
const description = fallbackContractConfig("defaultSuccessDescription", def.route?.successDescription);
|
|
@@ -452,46 +612,90 @@ ${errors.join("\n\n")}`
|
|
|
452
612
|
ref.responses[status] = {
|
|
453
613
|
description,
|
|
454
614
|
content: toOpenAPIEventIteratorContent(
|
|
455
|
-
await this.converter.convert(eventIteratorSchemaDetails.yields, { strategy: "output" }),
|
|
456
|
-
await this.converter.convert(eventIteratorSchemaDetails.returns, { strategy: "output" })
|
|
615
|
+
await this.converter.convert(eventIteratorSchemaDetails.yields, { ...baseSchemaConvertOptions, strategy: "output" }),
|
|
616
|
+
await this.converter.convert(eventIteratorSchemaDetails.returns, { ...baseSchemaConvertOptions, strategy: "output" })
|
|
457
617
|
)
|
|
458
618
|
};
|
|
459
619
|
return;
|
|
460
620
|
}
|
|
461
|
-
const [required, json] = await this.converter.convert(
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
621
|
+
const [required, json] = await this.converter.convert(
|
|
622
|
+
outputSchema,
|
|
623
|
+
{
|
|
624
|
+
...baseSchemaConvertOptions,
|
|
625
|
+
strategy: "output",
|
|
626
|
+
minStructureDepthForRef: outputStructure === "detailed" ? 1 : 0
|
|
627
|
+
}
|
|
628
|
+
);
|
|
466
629
|
if (outputStructure === "compact") {
|
|
630
|
+
ref.responses ??= {};
|
|
631
|
+
ref.responses[status] = {
|
|
632
|
+
description
|
|
633
|
+
};
|
|
467
634
|
ref.responses[status].content = toOpenAPIContent(applySchemaOptionality(required, json));
|
|
468
635
|
return;
|
|
469
636
|
}
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
637
|
+
const handledStatuses = /* @__PURE__ */ new Set();
|
|
638
|
+
for (const item of expandUnionSchema(json)) {
|
|
639
|
+
const error = new OpenAPIGeneratorError(`
|
|
640
|
+
When output structure is "detailed", output schema must satisfy:
|
|
641
|
+
{
|
|
642
|
+
status?: number, // must be a literal number and in the range of 200-399
|
|
643
|
+
headers?: Record<string, unknown>,
|
|
644
|
+
body?: unknown
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
But got: ${stringifyJSON(item)}
|
|
648
|
+
`);
|
|
649
|
+
if (!isObjectSchema(item)) {
|
|
478
650
|
throw error;
|
|
479
651
|
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
652
|
+
let schemaStatus;
|
|
653
|
+
let schemaDescription;
|
|
654
|
+
if (item.properties?.status !== void 0) {
|
|
655
|
+
const statusSchema = resolveOpenAPIJsonSchemaRef(doc, item.properties.status);
|
|
656
|
+
if (typeof statusSchema !== "object" || statusSchema.const === void 0 || typeof statusSchema.const !== "number" || !Number.isInteger(statusSchema.const) || isORPCErrorStatus(statusSchema.const)) {
|
|
657
|
+
throw error;
|
|
658
|
+
}
|
|
659
|
+
schemaStatus = statusSchema.const;
|
|
660
|
+
schemaDescription = statusSchema.description;
|
|
661
|
+
}
|
|
662
|
+
const itemStatus = schemaStatus ?? status;
|
|
663
|
+
const itemDescription = schemaDescription ?? description;
|
|
664
|
+
if (handledStatuses.has(itemStatus)) {
|
|
665
|
+
throw new OpenAPIGeneratorError(`
|
|
666
|
+
When output structure is "detailed", each success status must be unique.
|
|
667
|
+
But got status: ${itemStatus} used more than once.
|
|
668
|
+
`);
|
|
669
|
+
}
|
|
670
|
+
handledStatuses.add(itemStatus);
|
|
671
|
+
ref.responses ??= {};
|
|
672
|
+
ref.responses[itemStatus] = {
|
|
673
|
+
description: itemDescription
|
|
674
|
+
};
|
|
675
|
+
if (item.properties?.headers !== void 0) {
|
|
676
|
+
const headersSchema = resolveOpenAPIJsonSchemaRef(doc, item.properties.headers);
|
|
677
|
+
if (!isObjectSchema(headersSchema)) {
|
|
678
|
+
throw error;
|
|
679
|
+
}
|
|
680
|
+
for (const key in headersSchema.properties) {
|
|
681
|
+
const headerSchema = headersSchema.properties[key];
|
|
682
|
+
if (headerSchema !== void 0) {
|
|
683
|
+
ref.responses[itemStatus].headers ??= {};
|
|
684
|
+
ref.responses[itemStatus].headers[key] = {
|
|
685
|
+
schema: toOpenAPISchema(headerSchema),
|
|
686
|
+
required: item.required?.includes("headers") && headersSchema.required?.includes(key)
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
if (item.properties?.body !== void 0) {
|
|
692
|
+
ref.responses[itemStatus].content = toOpenAPIContent(
|
|
693
|
+
applySchemaOptionality(item.required?.includes("body") ?? false, item.properties.body)
|
|
694
|
+
);
|
|
486
695
|
}
|
|
487
|
-
}
|
|
488
|
-
if (json.properties?.body !== void 0) {
|
|
489
|
-
ref.responses[status].content = toOpenAPIContent(
|
|
490
|
-
applySchemaOptionality(json.required?.includes("body") ?? false, json.properties.body)
|
|
491
|
-
);
|
|
492
696
|
}
|
|
493
697
|
}
|
|
494
|
-
async #errorResponse(ref, def) {
|
|
698
|
+
async #errorResponse(ref, def, baseSchemaConvertOptions, undefinedErrorSchema) {
|
|
495
699
|
const errorMap = def.errorMap;
|
|
496
700
|
const errors = {};
|
|
497
701
|
for (const code in errorMap) {
|
|
@@ -501,7 +705,7 @@ ${errors.join("\n\n")}`
|
|
|
501
705
|
}
|
|
502
706
|
const status = fallbackORPCErrorStatus(code, config.status);
|
|
503
707
|
const message = fallbackORPCErrorMessage(code, config.message);
|
|
504
|
-
const [dataRequired, dataSchema] = await this.converter.convert(config.data, { strategy: "output" });
|
|
708
|
+
const [dataRequired, dataSchema] = await this.converter.convert(config.data, { ...baseSchemaConvertOptions, strategy: "output" });
|
|
505
709
|
errors[status] ??= [];
|
|
506
710
|
errors[status].push({
|
|
507
711
|
type: "object",
|
|
@@ -523,17 +727,7 @@ ${errors.join("\n\n")}`
|
|
|
523
727
|
content: toOpenAPIContent({
|
|
524
728
|
oneOf: [
|
|
525
729
|
...schemas,
|
|
526
|
-
|
|
527
|
-
type: "object",
|
|
528
|
-
properties: {
|
|
529
|
-
defined: { const: false },
|
|
530
|
-
code: { type: "string" },
|
|
531
|
-
status: { type: "number" },
|
|
532
|
-
message: { type: "string" },
|
|
533
|
-
data: {}
|
|
534
|
-
},
|
|
535
|
-
required: ["defined", "code", "status", "message"]
|
|
536
|
-
}
|
|
730
|
+
undefinedErrorSchema
|
|
537
731
|
]
|
|
538
732
|
})
|
|
539
733
|
};
|
|
@@ -541,4 +735,4 @@ ${errors.join("\n\n")}`
|
|
|
541
735
|
}
|
|
542
736
|
}
|
|
543
737
|
|
|
544
|
-
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, filterSchemaBranches as m, applySchemaOptionality as n, separateObjectSchema as s, toOpenAPIPath as t };
|
|
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, filterSchemaBranches as m, applySchemaOptionality as n, expandUnionSchema as o, expandArrayableSchema as p, isPrimitiveSchema as q, resolveOpenAPIJsonSchemaRef as r, separateObjectSchema as s, toOpenAPIPath as t };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orpc/openapi",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.0-next.
|
|
4
|
+
"version": "0.0.0-next.a153125",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://orpc.unnoq.com",
|
|
7
7
|
"repository": {
|
|
@@ -38,6 +38,11 @@
|
|
|
38
38
|
"types": "./dist/adapters/node/index.d.mts",
|
|
39
39
|
"import": "./dist/adapters/node/index.mjs",
|
|
40
40
|
"default": "./dist/adapters/node/index.mjs"
|
|
41
|
+
},
|
|
42
|
+
"./aws-lambda": {
|
|
43
|
+
"types": "./dist/adapters/aws-lambda/index.d.mts",
|
|
44
|
+
"import": "./dist/adapters/aws-lambda/index.mjs",
|
|
45
|
+
"default": "./dist/adapters/aws-lambda/index.mjs"
|
|
41
46
|
}
|
|
42
47
|
},
|
|
43
48
|
"files": [
|
|
@@ -45,17 +50,16 @@
|
|
|
45
50
|
],
|
|
46
51
|
"dependencies": {
|
|
47
52
|
"json-schema-typed": "^8.0.1",
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"@orpc/client": "0.0.0-next.
|
|
51
|
-
"@orpc/
|
|
52
|
-
"@orpc/
|
|
53
|
-
"@orpc/standard-server": "0.0.0-next.
|
|
54
|
-
"@orpc/
|
|
55
|
-
"@orpc/server": "0.0.0-next.a09e9be"
|
|
53
|
+
"rou3": "^0.7.2",
|
|
54
|
+
"@orpc/client": "0.0.0-next.a153125",
|
|
55
|
+
"@orpc/openapi-client": "0.0.0-next.a153125",
|
|
56
|
+
"@orpc/server": "0.0.0-next.a153125",
|
|
57
|
+
"@orpc/shared": "0.0.0-next.a153125",
|
|
58
|
+
"@orpc/standard-server": "0.0.0-next.a153125",
|
|
59
|
+
"@orpc/contract": "0.0.0-next.a153125"
|
|
56
60
|
},
|
|
57
61
|
"devDependencies": {
|
|
58
|
-
"zod": "^3.
|
|
62
|
+
"zod": "^3.25.67"
|
|
59
63
|
},
|
|
60
64
|
"scripts": {
|
|
61
65
|
"build": "unbuild",
|