@orpc/openapi 0.0.0-next.0ce0926 → 0.0.0-next.0ceddfc
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -8
- package/dist/adapters/aws-lambda/index.d.mts +19 -0
- package/dist/adapters/aws-lambda/index.d.ts +19 -0
- package/dist/adapters/aws-lambda/index.mjs +18 -0
- package/dist/adapters/fetch/index.d.mts +3 -1
- package/dist/adapters/fetch/index.d.ts +3 -1
- package/dist/adapters/fetch/index.mjs +2 -1
- package/dist/adapters/node/index.d.mts +3 -1
- package/dist/adapters/node/index.d.ts +3 -1
- package/dist/adapters/node/index.mjs +2 -1
- package/dist/adapters/standard/index.d.mts +6 -11
- package/dist/adapters/standard/index.d.ts +6 -11
- package/dist/adapters/standard/index.mjs +2 -1
- package/dist/index.d.mts +24 -13
- package/dist/index.d.ts +24 -13
- package/dist/index.mjs +3 -3
- package/dist/plugins/index.d.mts +10 -11
- package/dist/plugins/index.d.ts +10 -11
- package/dist/plugins/index.mjs +2 -2
- package/dist/shared/{openapi.fMEQd3Yd.mjs → openapi.1iT1iSZi.mjs} +278 -72
- package/dist/shared/{openapi.p5tsmBXx.mjs → openapi.BVXcB0u4.mjs} +39 -10
- package/dist/shared/openapi.CQmjvnb0.d.mts +31 -0
- package/dist/shared/openapi.CQmjvnb0.d.ts +31 -0
- package/dist/shared/openapi.CfjfVeBJ.d.mts +108 -0
- package/dist/shared/openapi.CfjfVeBJ.d.ts +108 -0
- package/package.json +15 -11
- package/dist/shared/openapi.D3j94c9n.d.mts +0 -12
- package/dist/shared/openapi.D3j94c9n.d.ts +0 -12
- package/dist/shared/openapi.DP97kr00.d.mts +0 -47
- package/dist/shared/openapi.DP97kr00.d.ts +0 -47
|
@@ -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, value } from '@orpc/shared';
|
|
7
|
+
import { TypeName } from '@orpc/interop/json-schema-typed/draft-2020-12';
|
|
8
8
|
|
|
9
9
|
const OPERATION_EXTENDER_SYMBOL = Symbol("ORPC_OPERATION_EXTENDER");
|
|
10
10
|
function customOpenAPIOperation(o, extend) {
|
|
@@ -114,13 +114,18 @@ function isAnySchema(schema) {
|
|
|
114
114
|
return false;
|
|
115
115
|
}
|
|
116
116
|
function separateObjectSchema(schema, separatedProperties) {
|
|
117
|
-
if (Object.keys(schema).some(
|
|
117
|
+
if (Object.keys(schema).some(
|
|
118
|
+
(k) => !["type", "properties", "required", "additionalProperties"].includes(k) && LOGIC_KEYWORDS.includes(k) && schema[k] !== void 0
|
|
119
|
+
)) {
|
|
118
120
|
return [{ type: "object" }, schema];
|
|
119
121
|
}
|
|
120
122
|
const matched = { ...schema };
|
|
121
123
|
const rest = { ...schema };
|
|
122
|
-
matched.properties =
|
|
123
|
-
|
|
124
|
+
matched.properties = separatedProperties.reduce((acc, key) => {
|
|
125
|
+
const keySchema = schema.properties?.[key] ?? schema.additionalProperties;
|
|
126
|
+
if (keySchema !== void 0) {
|
|
127
|
+
acc[key] = keySchema;
|
|
128
|
+
}
|
|
124
129
|
return acc;
|
|
125
130
|
}, {});
|
|
126
131
|
matched.required = schema.required?.filter((key) => separatedProperties.includes(key));
|
|
@@ -184,6 +189,57 @@ function applySchemaOptionality(required, schema) {
|
|
|
184
189
|
]
|
|
185
190
|
};
|
|
186
191
|
}
|
|
192
|
+
function expandUnionSchema(schema) {
|
|
193
|
+
if (typeof schema === "object") {
|
|
194
|
+
for (const keyword of ["anyOf", "oneOf"]) {
|
|
195
|
+
if (schema[keyword] && Object.keys(schema).every(
|
|
196
|
+
(k) => k === keyword || !LOGIC_KEYWORDS.includes(k)
|
|
197
|
+
)) {
|
|
198
|
+
return schema[keyword].flatMap((s) => expandUnionSchema(s));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return [schema];
|
|
203
|
+
}
|
|
204
|
+
function expandArrayableSchema(schema) {
|
|
205
|
+
const schemas = expandUnionSchema(schema);
|
|
206
|
+
if (schemas.length !== 2) {
|
|
207
|
+
return void 0;
|
|
208
|
+
}
|
|
209
|
+
const arraySchema = schemas.find(
|
|
210
|
+
(s) => typeof s === "object" && s.type === "array" && Object.keys(s).filter((k) => LOGIC_KEYWORDS.includes(k)).every((k) => k === "type" || k === "items")
|
|
211
|
+
);
|
|
212
|
+
if (arraySchema === void 0) {
|
|
213
|
+
return void 0;
|
|
214
|
+
}
|
|
215
|
+
const items1 = arraySchema.items;
|
|
216
|
+
const items2 = schemas.find((s) => s !== arraySchema);
|
|
217
|
+
if (stringifyJSON(items1) !== stringifyJSON(items2)) {
|
|
218
|
+
return void 0;
|
|
219
|
+
}
|
|
220
|
+
return [items2, arraySchema];
|
|
221
|
+
}
|
|
222
|
+
const PRIMITIVE_SCHEMA_TYPES = /* @__PURE__ */ new Set([
|
|
223
|
+
TypeName.String,
|
|
224
|
+
TypeName.Number,
|
|
225
|
+
TypeName.Integer,
|
|
226
|
+
TypeName.Boolean,
|
|
227
|
+
TypeName.Null
|
|
228
|
+
]);
|
|
229
|
+
function isPrimitiveSchema(schema) {
|
|
230
|
+
return expandUnionSchema(schema).every((s) => {
|
|
231
|
+
if (typeof s === "boolean") {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
if (typeof s.type === "string" && PRIMITIVE_SCHEMA_TYPES.has(s.type)) {
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
if (s.const !== void 0) {
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
return false;
|
|
241
|
+
});
|
|
242
|
+
}
|
|
187
243
|
|
|
188
244
|
function toOpenAPIPath(path) {
|
|
189
245
|
return standardizeHTTPPath(path).replace(/\/\{\+([^}]+)\}/g, "/{$1}");
|
|
@@ -256,13 +312,26 @@ function toOpenAPIParameters(schema, parameterIn) {
|
|
|
256
312
|
const parameters = [];
|
|
257
313
|
for (const key in schema.properties) {
|
|
258
314
|
const keySchema = schema.properties[key];
|
|
315
|
+
let isDeepObjectStyle = true;
|
|
316
|
+
if (parameterIn !== "query") {
|
|
317
|
+
isDeepObjectStyle = false;
|
|
318
|
+
} else if (isPrimitiveSchema(keySchema)) {
|
|
319
|
+
isDeepObjectStyle = false;
|
|
320
|
+
} else {
|
|
321
|
+
const [item] = expandArrayableSchema(keySchema) ?? [];
|
|
322
|
+
if (item !== void 0 && isPrimitiveSchema(item)) {
|
|
323
|
+
isDeepObjectStyle = false;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
259
326
|
parameters.push({
|
|
260
327
|
name: key,
|
|
261
328
|
in: parameterIn,
|
|
262
329
|
required: schema.required?.includes(key),
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
330
|
+
schema: toOpenAPISchema(keySchema),
|
|
331
|
+
style: isDeepObjectStyle ? "deepObject" : void 0,
|
|
332
|
+
explode: isDeepObjectStyle ? true : void 0,
|
|
333
|
+
allowEmptyValue: parameterIn === "query" ? true : void 0,
|
|
334
|
+
allowReserved: parameterIn === "query" ? true : void 0
|
|
266
335
|
});
|
|
267
336
|
}
|
|
268
337
|
return parameters;
|
|
@@ -281,6 +350,15 @@ function checkParamsSchema(schema, params) {
|
|
|
281
350
|
function toOpenAPISchema(schema) {
|
|
282
351
|
return schema === true ? {} : schema === false ? { not: {} } : schema;
|
|
283
352
|
}
|
|
353
|
+
const OPENAPI_JSON_SCHEMA_REF_PREFIX = "#/components/schemas/";
|
|
354
|
+
function resolveOpenAPIJsonSchemaRef(doc, schema) {
|
|
355
|
+
if (typeof schema !== "object" || !schema.$ref?.startsWith(OPENAPI_JSON_SCHEMA_REF_PREFIX)) {
|
|
356
|
+
return schema;
|
|
357
|
+
}
|
|
358
|
+
const name = schema.$ref.slice(OPENAPI_JSON_SCHEMA_REF_PREFIX.length);
|
|
359
|
+
const resolved = doc.components?.schemas?.[name];
|
|
360
|
+
return resolved ?? schema;
|
|
361
|
+
}
|
|
284
362
|
|
|
285
363
|
class CompositeSchemaConverter {
|
|
286
364
|
converters;
|
|
@@ -312,32 +390,50 @@ class OpenAPIGenerator {
|
|
|
312
390
|
* @see {@link https://orpc.unnoq.com/docs/openapi/openapi-specification OpenAPI Specification Docs}
|
|
313
391
|
*/
|
|
314
392
|
async generate(router, options = {}) {
|
|
393
|
+
const filter = options.filter ?? (({ contract, path }) => {
|
|
394
|
+
return !(options.exclude?.(contract, path) ?? false);
|
|
395
|
+
});
|
|
315
396
|
const doc = {
|
|
316
397
|
...clone(options),
|
|
317
398
|
info: options.info ?? { title: "API Reference", version: "0.0.0" },
|
|
318
|
-
openapi: "3.1.1"
|
|
399
|
+
openapi: "3.1.1",
|
|
400
|
+
exclude: void 0,
|
|
401
|
+
filter: void 0,
|
|
402
|
+
commonSchemas: void 0
|
|
319
403
|
};
|
|
404
|
+
const { baseSchemaConvertOptions, undefinedErrorJsonSchema } = await this.#resolveCommonSchemas(doc, options.commonSchemas);
|
|
320
405
|
const contracts = [];
|
|
321
|
-
await resolveContractProcedures({ path: [], router }, (
|
|
322
|
-
|
|
406
|
+
await resolveContractProcedures({ path: [], router }, (traverseOptions) => {
|
|
407
|
+
if (!value(filter, traverseOptions)) {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
contracts.push(traverseOptions);
|
|
323
411
|
});
|
|
324
412
|
const errors = [];
|
|
325
413
|
for (const { contract, path } of contracts) {
|
|
326
|
-
const
|
|
414
|
+
const stringPath = path.join(".");
|
|
327
415
|
try {
|
|
328
416
|
const def = contract["~orpc"];
|
|
329
417
|
const method = toOpenAPIMethod(fallbackContractConfig("defaultMethod", def.route.method));
|
|
330
418
|
const httpPath = toOpenAPIPath(def.route.path ?? toHttpPath(path));
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
419
|
+
let operationObjectRef;
|
|
420
|
+
if (def.route.spec !== void 0 && typeof def.route.spec !== "function") {
|
|
421
|
+
operationObjectRef = def.route.spec;
|
|
422
|
+
} else {
|
|
423
|
+
operationObjectRef = {
|
|
424
|
+
operationId: def.route.operationId ?? stringPath,
|
|
425
|
+
summary: def.route.summary,
|
|
426
|
+
description: def.route.description,
|
|
427
|
+
deprecated: def.route.deprecated,
|
|
428
|
+
tags: def.route.tags?.map((tag) => tag)
|
|
429
|
+
};
|
|
430
|
+
await this.#request(doc, operationObjectRef, def, baseSchemaConvertOptions);
|
|
431
|
+
await this.#successResponse(doc, operationObjectRef, def, baseSchemaConvertOptions);
|
|
432
|
+
await this.#errorResponse(operationObjectRef, def, baseSchemaConvertOptions, undefinedErrorJsonSchema);
|
|
433
|
+
}
|
|
434
|
+
if (typeof def.route.spec === "function") {
|
|
435
|
+
operationObjectRef = def.route.spec(operationObjectRef);
|
|
436
|
+
}
|
|
341
437
|
doc.paths ??= {};
|
|
342
438
|
doc.paths[httpPath] ??= {};
|
|
343
439
|
doc.paths[httpPath][method] = applyCustomOpenAPIOperation(operationObjectRef, contract);
|
|
@@ -346,7 +442,7 @@ class OpenAPIGenerator {
|
|
|
346
442
|
throw e;
|
|
347
443
|
}
|
|
348
444
|
errors.push(
|
|
349
|
-
`[OpenAPIGenerator] Error occurred while generating OpenAPI for procedure at path: ${
|
|
445
|
+
`[OpenAPIGenerator] Error occurred while generating OpenAPI for procedure at path: ${stringPath}
|
|
350
446
|
${e.message}`
|
|
351
447
|
);
|
|
352
448
|
}
|
|
@@ -360,22 +456,96 @@ ${errors.join("\n\n")}`
|
|
|
360
456
|
}
|
|
361
457
|
return this.serializer.serialize(doc)[0];
|
|
362
458
|
}
|
|
363
|
-
async #
|
|
459
|
+
async #resolveCommonSchemas(doc, commonSchemas) {
|
|
460
|
+
let undefinedErrorJsonSchema = {
|
|
461
|
+
type: "object",
|
|
462
|
+
properties: {
|
|
463
|
+
defined: { const: false },
|
|
464
|
+
code: { type: "string" },
|
|
465
|
+
status: { type: "number" },
|
|
466
|
+
message: { type: "string" },
|
|
467
|
+
data: {}
|
|
468
|
+
},
|
|
469
|
+
required: ["defined", "code", "status", "message"]
|
|
470
|
+
};
|
|
471
|
+
const baseSchemaConvertOptions = {};
|
|
472
|
+
if (commonSchemas) {
|
|
473
|
+
baseSchemaConvertOptions.components = [];
|
|
474
|
+
for (const key in commonSchemas) {
|
|
475
|
+
const options = commonSchemas[key];
|
|
476
|
+
if (options.schema === void 0) {
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
const { schema, strategy = "input" } = options;
|
|
480
|
+
const [required, json] = await this.converter.convert(schema, { strategy });
|
|
481
|
+
const allowedStrategies = [strategy];
|
|
482
|
+
if (strategy === "input") {
|
|
483
|
+
const [outputRequired, outputJson] = await this.converter.convert(schema, { strategy: "output" });
|
|
484
|
+
if (outputRequired === required && stringifyJSON(outputJson) === stringifyJSON(json)) {
|
|
485
|
+
allowedStrategies.push("output");
|
|
486
|
+
}
|
|
487
|
+
} else if (strategy === "output") {
|
|
488
|
+
const [inputRequired, inputJson] = await this.converter.convert(schema, { strategy: "input" });
|
|
489
|
+
if (inputRequired === required && stringifyJSON(inputJson) === stringifyJSON(json)) {
|
|
490
|
+
allowedStrategies.push("input");
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
baseSchemaConvertOptions.components.push({
|
|
494
|
+
schema,
|
|
495
|
+
required,
|
|
496
|
+
ref: `#/components/schemas/${key}`,
|
|
497
|
+
allowedStrategies
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
doc.components ??= {};
|
|
501
|
+
doc.components.schemas ??= {};
|
|
502
|
+
for (const key in commonSchemas) {
|
|
503
|
+
const options = commonSchemas[key];
|
|
504
|
+
if (options.schema === void 0) {
|
|
505
|
+
if (options.error === "UndefinedError") {
|
|
506
|
+
doc.components.schemas[key] = toOpenAPISchema(undefinedErrorJsonSchema);
|
|
507
|
+
undefinedErrorJsonSchema = { $ref: `#/components/schemas/${key}` };
|
|
508
|
+
}
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
const { schema, strategy = "input" } = options;
|
|
512
|
+
const [, json] = await this.converter.convert(
|
|
513
|
+
schema,
|
|
514
|
+
{
|
|
515
|
+
...baseSchemaConvertOptions,
|
|
516
|
+
strategy,
|
|
517
|
+
minStructureDepthForRef: 1
|
|
518
|
+
// not allow use $ref for root schemas
|
|
519
|
+
}
|
|
520
|
+
);
|
|
521
|
+
doc.components.schemas[key] = toOpenAPISchema(json);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return { baseSchemaConvertOptions, undefinedErrorJsonSchema };
|
|
525
|
+
}
|
|
526
|
+
async #request(doc, ref, def, baseSchemaConvertOptions) {
|
|
364
527
|
const method = fallbackContractConfig("defaultMethod", def.route.method);
|
|
365
528
|
const details = getEventIteratorSchemaDetails(def.inputSchema);
|
|
366
529
|
if (details) {
|
|
367
530
|
ref.requestBody = {
|
|
368
531
|
required: true,
|
|
369
532
|
content: toOpenAPIEventIteratorContent(
|
|
370
|
-
await this.converter.convert(details.yields, { strategy: "input" }),
|
|
371
|
-
await this.converter.convert(details.returns, { strategy: "input" })
|
|
533
|
+
await this.converter.convert(details.yields, { ...baseSchemaConvertOptions, strategy: "input" }),
|
|
534
|
+
await this.converter.convert(details.returns, { ...baseSchemaConvertOptions, strategy: "input" })
|
|
372
535
|
)
|
|
373
536
|
};
|
|
374
537
|
return;
|
|
375
538
|
}
|
|
376
539
|
const dynamicParams = getDynamicParams(def.route.path)?.map((v) => v.name);
|
|
377
540
|
const inputStructure = fallbackContractConfig("defaultInputStructure", def.route.inputStructure);
|
|
378
|
-
let [required, schema] = await this.converter.convert(
|
|
541
|
+
let [required, schema] = await this.converter.convert(
|
|
542
|
+
def.inputSchema,
|
|
543
|
+
{
|
|
544
|
+
...baseSchemaConvertOptions,
|
|
545
|
+
strategy: "input",
|
|
546
|
+
minStructureDepthForRef: dynamicParams?.length || inputStructure === "detailed" ? 1 : 0
|
|
547
|
+
}
|
|
548
|
+
);
|
|
379
549
|
if (isAnySchema(schema) && !dynamicParams?.length) {
|
|
380
550
|
return;
|
|
381
551
|
}
|
|
@@ -418,7 +588,8 @@ ${errors.join("\n\n")}`
|
|
|
418
588
|
if (!isObjectSchema(schema)) {
|
|
419
589
|
throw error;
|
|
420
590
|
}
|
|
421
|
-
|
|
591
|
+
const resolvedParamSchema = schema.properties?.params !== void 0 ? resolveOpenAPIJsonSchemaRef(doc, schema.properties.params) : void 0;
|
|
592
|
+
if (dynamicParams?.length && (resolvedParamSchema === void 0 || !isObjectSchema(resolvedParamSchema) || !checkParamsSchema(resolvedParamSchema, dynamicParams))) {
|
|
422
593
|
throw new OpenAPIGeneratorError(
|
|
423
594
|
'When input structure is "detailed" and path has dynamic params, the "params" schema must be an object with all dynamic params as required.'
|
|
424
595
|
);
|
|
@@ -426,12 +597,13 @@ ${errors.join("\n\n")}`
|
|
|
426
597
|
for (const from of ["params", "query", "headers"]) {
|
|
427
598
|
const fromSchema = schema.properties?.[from];
|
|
428
599
|
if (fromSchema !== void 0) {
|
|
429
|
-
|
|
600
|
+
const resolvedSchema = resolveOpenAPIJsonSchemaRef(doc, fromSchema);
|
|
601
|
+
if (!isObjectSchema(resolvedSchema)) {
|
|
430
602
|
throw error;
|
|
431
603
|
}
|
|
432
604
|
const parameterIn = from === "params" ? "path" : from === "headers" ? "header" : "query";
|
|
433
605
|
ref.parameters ??= [];
|
|
434
|
-
ref.parameters.push(...toOpenAPIParameters(
|
|
606
|
+
ref.parameters.push(...toOpenAPIParameters(resolvedSchema, parameterIn));
|
|
435
607
|
}
|
|
436
608
|
}
|
|
437
609
|
if (schema.properties?.body !== void 0) {
|
|
@@ -441,7 +613,7 @@ ${errors.join("\n\n")}`
|
|
|
441
613
|
};
|
|
442
614
|
}
|
|
443
615
|
}
|
|
444
|
-
async #successResponse(ref, def) {
|
|
616
|
+
async #successResponse(doc, ref, def, baseSchemaConvertOptions) {
|
|
445
617
|
const outputSchema = def.outputSchema;
|
|
446
618
|
const status = fallbackContractConfig("defaultSuccessStatus", def.route.successStatus);
|
|
447
619
|
const description = fallbackContractConfig("defaultSuccessDescription", def.route?.successDescription);
|
|
@@ -452,46 +624,90 @@ ${errors.join("\n\n")}`
|
|
|
452
624
|
ref.responses[status] = {
|
|
453
625
|
description,
|
|
454
626
|
content: toOpenAPIEventIteratorContent(
|
|
455
|
-
await this.converter.convert(eventIteratorSchemaDetails.yields, { strategy: "output" }),
|
|
456
|
-
await this.converter.convert(eventIteratorSchemaDetails.returns, { strategy: "output" })
|
|
627
|
+
await this.converter.convert(eventIteratorSchemaDetails.yields, { ...baseSchemaConvertOptions, strategy: "output" }),
|
|
628
|
+
await this.converter.convert(eventIteratorSchemaDetails.returns, { ...baseSchemaConvertOptions, strategy: "output" })
|
|
457
629
|
)
|
|
458
630
|
};
|
|
459
631
|
return;
|
|
460
632
|
}
|
|
461
|
-
const [required, json] = await this.converter.convert(
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
633
|
+
const [required, json] = await this.converter.convert(
|
|
634
|
+
outputSchema,
|
|
635
|
+
{
|
|
636
|
+
...baseSchemaConvertOptions,
|
|
637
|
+
strategy: "output",
|
|
638
|
+
minStructureDepthForRef: outputStructure === "detailed" ? 1 : 0
|
|
639
|
+
}
|
|
640
|
+
);
|
|
466
641
|
if (outputStructure === "compact") {
|
|
642
|
+
ref.responses ??= {};
|
|
643
|
+
ref.responses[status] = {
|
|
644
|
+
description
|
|
645
|
+
};
|
|
467
646
|
ref.responses[status].content = toOpenAPIContent(applySchemaOptionality(required, json));
|
|
468
647
|
return;
|
|
469
648
|
}
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
649
|
+
const handledStatuses = /* @__PURE__ */ new Set();
|
|
650
|
+
for (const item of expandUnionSchema(json)) {
|
|
651
|
+
const error = new OpenAPIGeneratorError(`
|
|
652
|
+
When output structure is "detailed", output schema must satisfy:
|
|
653
|
+
{
|
|
654
|
+
status?: number, // must be a literal number and in the range of 200-399
|
|
655
|
+
headers?: Record<string, unknown>,
|
|
656
|
+
body?: unknown
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
But got: ${stringifyJSON(item)}
|
|
660
|
+
`);
|
|
661
|
+
if (!isObjectSchema(item)) {
|
|
478
662
|
throw error;
|
|
479
663
|
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
664
|
+
let schemaStatus;
|
|
665
|
+
let schemaDescription;
|
|
666
|
+
if (item.properties?.status !== void 0) {
|
|
667
|
+
const statusSchema = resolveOpenAPIJsonSchemaRef(doc, item.properties.status);
|
|
668
|
+
if (typeof statusSchema !== "object" || statusSchema.const === void 0 || typeof statusSchema.const !== "number" || !Number.isInteger(statusSchema.const) || isORPCErrorStatus(statusSchema.const)) {
|
|
669
|
+
throw error;
|
|
670
|
+
}
|
|
671
|
+
schemaStatus = statusSchema.const;
|
|
672
|
+
schemaDescription = statusSchema.description;
|
|
673
|
+
}
|
|
674
|
+
const itemStatus = schemaStatus ?? status;
|
|
675
|
+
const itemDescription = schemaDescription ?? description;
|
|
676
|
+
if (handledStatuses.has(itemStatus)) {
|
|
677
|
+
throw new OpenAPIGeneratorError(`
|
|
678
|
+
When output structure is "detailed", each success status must be unique.
|
|
679
|
+
But got status: ${itemStatus} used more than once.
|
|
680
|
+
`);
|
|
681
|
+
}
|
|
682
|
+
handledStatuses.add(itemStatus);
|
|
683
|
+
ref.responses ??= {};
|
|
684
|
+
ref.responses[itemStatus] = {
|
|
685
|
+
description: itemDescription
|
|
686
|
+
};
|
|
687
|
+
if (item.properties?.headers !== void 0) {
|
|
688
|
+
const headersSchema = resolveOpenAPIJsonSchemaRef(doc, item.properties.headers);
|
|
689
|
+
if (!isObjectSchema(headersSchema)) {
|
|
690
|
+
throw error;
|
|
691
|
+
}
|
|
692
|
+
for (const key in headersSchema.properties) {
|
|
693
|
+
const headerSchema = headersSchema.properties[key];
|
|
694
|
+
if (headerSchema !== void 0) {
|
|
695
|
+
ref.responses[itemStatus].headers ??= {};
|
|
696
|
+
ref.responses[itemStatus].headers[key] = {
|
|
697
|
+
schema: toOpenAPISchema(headerSchema),
|
|
698
|
+
required: item.required?.includes("headers") && headersSchema.required?.includes(key)
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
if (item.properties?.body !== void 0) {
|
|
704
|
+
ref.responses[itemStatus].content = toOpenAPIContent(
|
|
705
|
+
applySchemaOptionality(item.required?.includes("body") ?? false, item.properties.body)
|
|
706
|
+
);
|
|
486
707
|
}
|
|
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
708
|
}
|
|
493
709
|
}
|
|
494
|
-
async #errorResponse(ref, def) {
|
|
710
|
+
async #errorResponse(ref, def, baseSchemaConvertOptions, undefinedErrorSchema) {
|
|
495
711
|
const errorMap = def.errorMap;
|
|
496
712
|
const errors = {};
|
|
497
713
|
for (const code in errorMap) {
|
|
@@ -501,7 +717,7 @@ ${errors.join("\n\n")}`
|
|
|
501
717
|
}
|
|
502
718
|
const status = fallbackORPCErrorStatus(code, config.status);
|
|
503
719
|
const message = fallbackORPCErrorMessage(code, config.message);
|
|
504
|
-
const [dataRequired, dataSchema] = await this.converter.convert(config.data, { strategy: "output" });
|
|
720
|
+
const [dataRequired, dataSchema] = await this.converter.convert(config.data, { ...baseSchemaConvertOptions, strategy: "output" });
|
|
505
721
|
errors[status] ??= [];
|
|
506
722
|
errors[status].push({
|
|
507
723
|
type: "object",
|
|
@@ -523,17 +739,7 @@ ${errors.join("\n\n")}`
|
|
|
523
739
|
content: toOpenAPIContent({
|
|
524
740
|
oneOf: [
|
|
525
741
|
...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
|
-
}
|
|
742
|
+
undefinedErrorSchema
|
|
537
743
|
]
|
|
538
744
|
})
|
|
539
745
|
};
|
|
@@ -541,4 +747,4 @@ ${errors.join("\n\n")}`
|
|
|
541
747
|
}
|
|
542
748
|
}
|
|
543
749
|
|
|
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 };
|
|
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, filterSchemaBranches as m, applySchemaOptionality as n, expandUnionSchema as o, expandArrayableSchema as p, isPrimitiveSchema as q, resolveOpenAPIJsonSchemaRef as r, separateObjectSchema as s, toOpenAPIPath as t };
|
|
@@ -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, tryDecodeURIComponent, value } 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,20 +79,40 @@ 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) {
|
|
76
97
|
return standardizeHTTPPath(path).replace(/\/\{\+([^}]+)\}/g, "/**:$1").replace(/\/\{([^}]+)\}/g, "/:$1");
|
|
77
98
|
}
|
|
78
99
|
function decodeParams(params) {
|
|
79
|
-
return Object.fromEntries(Object.entries(params).map(([key, value]) => [key,
|
|
100
|
+
return Object.fromEntries(Object.entries(params).map(([key, value]) => [key, tryDecodeURIComponent(value)]));
|
|
80
101
|
}
|
|
81
102
|
|
|
82
103
|
class StandardOpenAPIMatcher {
|
|
104
|
+
filter;
|
|
83
105
|
tree = createRouter();
|
|
84
106
|
pendingRouters = [];
|
|
107
|
+
constructor(options = {}) {
|
|
108
|
+
this.filter = options.filter ?? true;
|
|
109
|
+
}
|
|
85
110
|
init(router, path = []) {
|
|
86
|
-
const laziedOptions = traverseContractProcedures({ router, path }, (
|
|
111
|
+
const laziedOptions = traverseContractProcedures({ router, path }, (traverseOptions) => {
|
|
112
|
+
if (!value(this.filter, traverseOptions)) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const { path: path2, contract } = traverseOptions;
|
|
87
116
|
const method = fallbackContractConfig("defaultMethod", contract["~orpc"].route.method);
|
|
88
117
|
const httpPath = toRou3Pattern(contract["~orpc"].route.path ?? toHttpPath(path2));
|
|
89
118
|
if (isProcedure(contract)) {
|
|
@@ -147,9 +176,9 @@ class StandardOpenAPIMatcher {
|
|
|
147
176
|
class StandardOpenAPIHandler extends StandardHandler {
|
|
148
177
|
constructor(router, options) {
|
|
149
178
|
const jsonSerializer = new StandardOpenAPIJsonSerializer(options);
|
|
150
|
-
const bracketNotationSerializer = new StandardBracketNotationSerializer();
|
|
179
|
+
const bracketNotationSerializer = new StandardBracketNotationSerializer(options);
|
|
151
180
|
const serializer = new StandardOpenAPISerializer(jsonSerializer, bracketNotationSerializer);
|
|
152
|
-
const matcher = new StandardOpenAPIMatcher();
|
|
181
|
+
const matcher = new StandardOpenAPIMatcher(options);
|
|
153
182
|
const codec = new StandardOpenAPICodec(serializer);
|
|
154
183
|
super(router, matcher, codec, options);
|
|
155
184
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { StandardOpenAPIJsonSerializerOptions, StandardBracketNotationSerializerOptions } from '@orpc/openapi-client/standard';
|
|
2
|
+
import { TraverseContractProcedureCallbackOptions, AnyRouter, Context, Router } from '@orpc/server';
|
|
3
|
+
import { StandardMatcher, StandardMatchResult, StandardHandlerOptions, StandardHandler } from '@orpc/server/standard';
|
|
4
|
+
import { HTTPPath } from '@orpc/client';
|
|
5
|
+
import { Value } from '@orpc/shared';
|
|
6
|
+
|
|
7
|
+
interface StandardOpenAPIMatcherOptions {
|
|
8
|
+
/**
|
|
9
|
+
* Filter procedures. Return `false` to exclude a procedure from matching.
|
|
10
|
+
*
|
|
11
|
+
* @default true
|
|
12
|
+
*/
|
|
13
|
+
filter?: Value<boolean, [options: TraverseContractProcedureCallbackOptions]>;
|
|
14
|
+
}
|
|
15
|
+
declare class StandardOpenAPIMatcher implements StandardMatcher {
|
|
16
|
+
private readonly filter;
|
|
17
|
+
private readonly tree;
|
|
18
|
+
private pendingRouters;
|
|
19
|
+
constructor(options?: StandardOpenAPIMatcherOptions);
|
|
20
|
+
init(router: AnyRouter, path?: readonly string[]): void;
|
|
21
|
+
match(method: string, pathname: HTTPPath): Promise<StandardMatchResult>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface StandardOpenAPIHandlerOptions<T extends Context> extends StandardHandlerOptions<T>, StandardOpenAPIJsonSerializerOptions, StandardBracketNotationSerializerOptions, StandardOpenAPIMatcherOptions {
|
|
25
|
+
}
|
|
26
|
+
declare class StandardOpenAPIHandler<T extends Context> extends StandardHandler<T> {
|
|
27
|
+
constructor(router: Router<any, T>, options: NoInfer<StandardOpenAPIHandlerOptions<T>>);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export { StandardOpenAPIHandler as a, StandardOpenAPIMatcher as c };
|
|
31
|
+
export type { StandardOpenAPIHandlerOptions as S, StandardOpenAPIMatcherOptions as b };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { StandardOpenAPIJsonSerializerOptions, StandardBracketNotationSerializerOptions } from '@orpc/openapi-client/standard';
|
|
2
|
+
import { TraverseContractProcedureCallbackOptions, AnyRouter, Context, Router } from '@orpc/server';
|
|
3
|
+
import { StandardMatcher, StandardMatchResult, StandardHandlerOptions, StandardHandler } from '@orpc/server/standard';
|
|
4
|
+
import { HTTPPath } from '@orpc/client';
|
|
5
|
+
import { Value } from '@orpc/shared';
|
|
6
|
+
|
|
7
|
+
interface StandardOpenAPIMatcherOptions {
|
|
8
|
+
/**
|
|
9
|
+
* Filter procedures. Return `false` to exclude a procedure from matching.
|
|
10
|
+
*
|
|
11
|
+
* @default true
|
|
12
|
+
*/
|
|
13
|
+
filter?: Value<boolean, [options: TraverseContractProcedureCallbackOptions]>;
|
|
14
|
+
}
|
|
15
|
+
declare class StandardOpenAPIMatcher implements StandardMatcher {
|
|
16
|
+
private readonly filter;
|
|
17
|
+
private readonly tree;
|
|
18
|
+
private pendingRouters;
|
|
19
|
+
constructor(options?: StandardOpenAPIMatcherOptions);
|
|
20
|
+
init(router: AnyRouter, path?: readonly string[]): void;
|
|
21
|
+
match(method: string, pathname: HTTPPath): Promise<StandardMatchResult>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface StandardOpenAPIHandlerOptions<T extends Context> extends StandardHandlerOptions<T>, StandardOpenAPIJsonSerializerOptions, StandardBracketNotationSerializerOptions, StandardOpenAPIMatcherOptions {
|
|
25
|
+
}
|
|
26
|
+
declare class StandardOpenAPIHandler<T extends Context> extends StandardHandler<T> {
|
|
27
|
+
constructor(router: Router<any, T>, options: NoInfer<StandardOpenAPIHandlerOptions<T>>);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export { StandardOpenAPIHandler as a, StandardOpenAPIMatcher as c };
|
|
31
|
+
export type { StandardOpenAPIHandlerOptions as S, StandardOpenAPIMatcherOptions as b };
|