@orpc/openapi 0.0.0-next.fa8d145 → 0.0.0-next.fb5a52a
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 +11 -8
- package/dist/adapters/aws-lambda/index.d.mts +20 -0
- package/dist/adapters/aws-lambda/index.d.ts +20 -0
- package/dist/adapters/aws-lambda/index.mjs +18 -0
- package/dist/adapters/fastify/index.d.mts +23 -0
- package/dist/adapters/fastify/index.d.ts +23 -0
- package/dist/adapters/fastify/index.mjs +18 -0
- package/dist/adapters/fetch/index.d.mts +9 -3
- package/dist/adapters/fetch/index.d.ts +9 -3
- package/dist/adapters/fetch/index.mjs +1 -1
- package/dist/adapters/node/index.d.mts +9 -3
- package/dist/adapters/node/index.d.ts +9 -3
- package/dist/adapters/node/index.mjs +1 -1
- package/dist/adapters/standard/index.d.mts +8 -23
- package/dist/adapters/standard/index.d.ts +8 -23
- package/dist/adapters/standard/index.mjs +1 -1
- package/dist/index.d.mts +24 -13
- package/dist/index.d.ts +24 -13
- package/dist/index.mjs +3 -3
- package/dist/plugins/index.d.mts +26 -12
- package/dist/plugins/index.d.ts +26 -12
- package/dist/plugins/index.mjs +59 -19
- package/dist/shared/openapi.BfNjg7j9.d.mts +120 -0
- package/dist/shared/openapi.BfNjg7j9.d.ts +120 -0
- package/dist/shared/{openapi.PDTdnRIU.mjs → openapi.CzHcOMxv.mjs} +335 -77
- package/dist/shared/{openapi.C_UtQ8Us.mjs → openapi.DIt-Z9W1.mjs} +19 -8
- package/dist/shared/openapi.DwaweYRb.d.mts +54 -0
- package/dist/shared/openapi.DwaweYRb.d.ts +54 -0
- package/package.json +21 -11
- package/dist/shared/openapi.CwdCLgSU.d.mts +0 -53
- package/dist/shared/openapi.CwdCLgSU.d.ts +0 -53
- package/dist/shared/openapi.D3j94c9n.d.mts +0 -12
- package/dist/shared/openapi.D3j94c9n.d.ts +0 -12
|
@@ -3,8 +3,8 @@ import { toHttpPath } from '@orpc/client/standard';
|
|
|
3
3
|
import { fallbackContractConfig, getEventIteratorSchemaDetails } from '@orpc/contract';
|
|
4
4
|
import { standardizeHTTPPath, StandardOpenAPIJsonSerializer, getDynamicParams } from '@orpc/openapi-client/standard';
|
|
5
5
|
import { isProcedure, resolveContractProcedures } from '@orpc/server';
|
|
6
|
-
import { isObject, findDeepMatches, toArray, clone,
|
|
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));
|
|
@@ -196,6 +201,45 @@ function expandUnionSchema(schema) {
|
|
|
196
201
|
}
|
|
197
202
|
return [schema];
|
|
198
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
|
+
}
|
|
199
243
|
|
|
200
244
|
function toOpenAPIPath(path) {
|
|
201
245
|
return standardizeHTTPPath(path).replace(/\/\{\+([^}]+)\}/g, "/{$1}");
|
|
@@ -268,13 +312,26 @@ function toOpenAPIParameters(schema, parameterIn) {
|
|
|
268
312
|
const parameters = [];
|
|
269
313
|
for (const key in schema.properties) {
|
|
270
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
|
+
}
|
|
271
326
|
parameters.push({
|
|
272
327
|
name: key,
|
|
273
328
|
in: parameterIn,
|
|
274
329
|
required: schema.required?.includes(key),
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
|
278
335
|
});
|
|
279
336
|
}
|
|
280
337
|
return parameters;
|
|
@@ -293,6 +350,116 @@ function checkParamsSchema(schema, params) {
|
|
|
293
350
|
function toOpenAPISchema(schema) {
|
|
294
351
|
return schema === true ? {} : schema === false ? { not: {} } : schema;
|
|
295
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
|
+
}
|
|
362
|
+
function simplifyComposedObjectJsonSchemasAndRefs(schema, doc) {
|
|
363
|
+
if (doc) {
|
|
364
|
+
schema = resolveOpenAPIJsonSchemaRef(doc, schema);
|
|
365
|
+
}
|
|
366
|
+
if (typeof schema !== "object" || !schema.anyOf && !schema.oneOf && !schema.allOf) {
|
|
367
|
+
return schema;
|
|
368
|
+
}
|
|
369
|
+
const unionSchemas = [
|
|
370
|
+
...toArray(schema.anyOf?.map((s) => simplifyComposedObjectJsonSchemasAndRefs(s, doc))),
|
|
371
|
+
...toArray(schema.oneOf?.map((s) => simplifyComposedObjectJsonSchemasAndRefs(s, doc)))
|
|
372
|
+
];
|
|
373
|
+
const objectUnionSchemas = [];
|
|
374
|
+
for (const u of unionSchemas) {
|
|
375
|
+
if (!isObjectSchema(u)) {
|
|
376
|
+
return schema;
|
|
377
|
+
}
|
|
378
|
+
objectUnionSchemas.push(u);
|
|
379
|
+
}
|
|
380
|
+
const mergedUnionPropertyMap = /* @__PURE__ */ new Map();
|
|
381
|
+
for (const u of objectUnionSchemas) {
|
|
382
|
+
if (u.properties) {
|
|
383
|
+
for (const [key, value] of Object.entries(u.properties)) {
|
|
384
|
+
let entry = mergedUnionPropertyMap.get(key);
|
|
385
|
+
if (!entry) {
|
|
386
|
+
const required = objectUnionSchemas.every((s) => s.required?.includes(key));
|
|
387
|
+
entry = { required, schemas: [] };
|
|
388
|
+
mergedUnionPropertyMap.set(key, entry);
|
|
389
|
+
}
|
|
390
|
+
entry.schemas.push(value);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
const intersectionSchemas = toArray(schema.allOf?.map((s) => simplifyComposedObjectJsonSchemasAndRefs(s, doc)));
|
|
395
|
+
const objectIntersectionSchemas = [];
|
|
396
|
+
for (const u of intersectionSchemas) {
|
|
397
|
+
if (!isObjectSchema(u)) {
|
|
398
|
+
return schema;
|
|
399
|
+
}
|
|
400
|
+
objectIntersectionSchemas.push(u);
|
|
401
|
+
}
|
|
402
|
+
if (isObjectSchema(schema)) {
|
|
403
|
+
objectIntersectionSchemas.push(schema);
|
|
404
|
+
}
|
|
405
|
+
const mergedInteractionPropertyMap = /* @__PURE__ */ new Map();
|
|
406
|
+
for (const u of objectIntersectionSchemas) {
|
|
407
|
+
if (u.properties) {
|
|
408
|
+
for (const [key, value] of Object.entries(u.properties)) {
|
|
409
|
+
let entry = mergedInteractionPropertyMap.get(key);
|
|
410
|
+
if (!entry) {
|
|
411
|
+
const required = objectIntersectionSchemas.some((s) => s.required?.includes(key));
|
|
412
|
+
entry = { required, schemas: [] };
|
|
413
|
+
mergedInteractionPropertyMap.set(key, entry);
|
|
414
|
+
}
|
|
415
|
+
entry.schemas.push(value);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const resultObjectSchema = { type: "object", properties: {}, required: [] };
|
|
420
|
+
const keys = /* @__PURE__ */ new Set([
|
|
421
|
+
...mergedUnionPropertyMap.keys(),
|
|
422
|
+
...mergedInteractionPropertyMap.keys()
|
|
423
|
+
]);
|
|
424
|
+
if (keys.size === 0) {
|
|
425
|
+
return schema;
|
|
426
|
+
}
|
|
427
|
+
const deduplicateSchemas = (schemas) => {
|
|
428
|
+
const seen = /* @__PURE__ */ new Set();
|
|
429
|
+
const result = [];
|
|
430
|
+
for (const schema2 of schemas) {
|
|
431
|
+
const key = stringifyJSON(schema2);
|
|
432
|
+
if (!seen.has(key)) {
|
|
433
|
+
seen.add(key);
|
|
434
|
+
result.push(schema2);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return result;
|
|
438
|
+
};
|
|
439
|
+
for (const key of keys) {
|
|
440
|
+
const unionEntry = mergedUnionPropertyMap.get(key);
|
|
441
|
+
const intersectionEntry = mergedInteractionPropertyMap.get(key);
|
|
442
|
+
resultObjectSchema.properties[key] = (() => {
|
|
443
|
+
const dedupedUnionSchemas = unionEntry ? deduplicateSchemas(unionEntry.schemas) : [];
|
|
444
|
+
const dedupedIntersectionSchemas = intersectionEntry ? deduplicateSchemas(intersectionEntry.schemas) : [];
|
|
445
|
+
if (!dedupedUnionSchemas.length) {
|
|
446
|
+
return dedupedIntersectionSchemas.length === 1 ? dedupedIntersectionSchemas[0] : { allOf: dedupedIntersectionSchemas };
|
|
447
|
+
}
|
|
448
|
+
if (!dedupedIntersectionSchemas.length) {
|
|
449
|
+
return dedupedUnionSchemas.length === 1 ? dedupedUnionSchemas[0] : { anyOf: dedupedUnionSchemas };
|
|
450
|
+
}
|
|
451
|
+
const allOf = deduplicateSchemas([
|
|
452
|
+
...dedupedIntersectionSchemas,
|
|
453
|
+
dedupedUnionSchemas.length === 1 ? dedupedUnionSchemas[0] : { anyOf: dedupedUnionSchemas }
|
|
454
|
+
]);
|
|
455
|
+
return allOf.length === 1 ? allOf[0] : { allOf };
|
|
456
|
+
})();
|
|
457
|
+
if (unionEntry?.required || intersectionEntry?.required) {
|
|
458
|
+
resultObjectSchema.required.push(key);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return resultObjectSchema;
|
|
462
|
+
}
|
|
296
463
|
|
|
297
464
|
class CompositeSchemaConverter {
|
|
298
465
|
converters;
|
|
@@ -323,37 +490,48 @@ class OpenAPIGenerator {
|
|
|
323
490
|
*
|
|
324
491
|
* @see {@link https://orpc.unnoq.com/docs/openapi/openapi-specification OpenAPI Specification Docs}
|
|
325
492
|
*/
|
|
326
|
-
async generate(router,
|
|
327
|
-
const
|
|
493
|
+
async generate(router, { customErrorResponseBodySchema, commonSchemas, filter: baseFilter, exclude, ...baseDoc } = {}) {
|
|
494
|
+
const filter = baseFilter ?? (({ contract, path }) => {
|
|
495
|
+
return !(exclude?.(contract, path) ?? false);
|
|
496
|
+
});
|
|
328
497
|
const doc = {
|
|
329
|
-
...clone(
|
|
330
|
-
info:
|
|
331
|
-
openapi: "3.1.1"
|
|
332
|
-
exclude: void 0
|
|
498
|
+
...clone(baseDoc),
|
|
499
|
+
info: baseDoc.info ?? { title: "API Reference", version: "0.0.0" },
|
|
500
|
+
openapi: "3.1.1"
|
|
333
501
|
};
|
|
502
|
+
const { baseSchemaConvertOptions, undefinedErrorJsonSchema } = await this.#resolveCommonSchemas(doc, commonSchemas);
|
|
334
503
|
const contracts = [];
|
|
335
|
-
await resolveContractProcedures({ path: [], router }, (
|
|
336
|
-
if (!
|
|
337
|
-
|
|
504
|
+
await resolveContractProcedures({ path: [], router }, (traverseOptions) => {
|
|
505
|
+
if (!value(filter, traverseOptions)) {
|
|
506
|
+
return;
|
|
338
507
|
}
|
|
508
|
+
contracts.push(traverseOptions);
|
|
339
509
|
});
|
|
340
510
|
const errors = [];
|
|
341
511
|
for (const { contract, path } of contracts) {
|
|
342
|
-
const
|
|
512
|
+
const stringPath = path.join(".");
|
|
343
513
|
try {
|
|
344
514
|
const def = contract["~orpc"];
|
|
345
515
|
const method = toOpenAPIMethod(fallbackContractConfig("defaultMethod", def.route.method));
|
|
346
516
|
const httpPath = toOpenAPIPath(def.route.path ?? toHttpPath(path));
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
517
|
+
let operationObjectRef;
|
|
518
|
+
if (def.route.spec !== void 0 && typeof def.route.spec !== "function") {
|
|
519
|
+
operationObjectRef = def.route.spec;
|
|
520
|
+
} else {
|
|
521
|
+
operationObjectRef = {
|
|
522
|
+
operationId: def.route.operationId ?? stringPath,
|
|
523
|
+
summary: def.route.summary,
|
|
524
|
+
description: def.route.description,
|
|
525
|
+
deprecated: def.route.deprecated,
|
|
526
|
+
tags: def.route.tags?.map((tag) => tag)
|
|
527
|
+
};
|
|
528
|
+
await this.#request(doc, operationObjectRef, def, baseSchemaConvertOptions);
|
|
529
|
+
await this.#successResponse(doc, operationObjectRef, def, baseSchemaConvertOptions);
|
|
530
|
+
await this.#errorResponse(operationObjectRef, def, baseSchemaConvertOptions, undefinedErrorJsonSchema, customErrorResponseBodySchema);
|
|
531
|
+
}
|
|
532
|
+
if (typeof def.route.spec === "function") {
|
|
533
|
+
operationObjectRef = def.route.spec(operationObjectRef);
|
|
534
|
+
}
|
|
357
535
|
doc.paths ??= {};
|
|
358
536
|
doc.paths[httpPath] ??= {};
|
|
359
537
|
doc.paths[httpPath][method] = applyCustomOpenAPIOperation(operationObjectRef, contract);
|
|
@@ -362,7 +540,7 @@ class OpenAPIGenerator {
|
|
|
362
540
|
throw e;
|
|
363
541
|
}
|
|
364
542
|
errors.push(
|
|
365
|
-
`[OpenAPIGenerator] Error occurred while generating OpenAPI for procedure at path: ${
|
|
543
|
+
`[OpenAPIGenerator] Error occurred while generating OpenAPI for procedure at path: ${stringPath}
|
|
366
544
|
${e.message}`
|
|
367
545
|
);
|
|
368
546
|
}
|
|
@@ -376,25 +554,101 @@ ${errors.join("\n\n")}`
|
|
|
376
554
|
}
|
|
377
555
|
return this.serializer.serialize(doc)[0];
|
|
378
556
|
}
|
|
379
|
-
async #
|
|
557
|
+
async #resolveCommonSchemas(doc, commonSchemas) {
|
|
558
|
+
let undefinedErrorJsonSchema = {
|
|
559
|
+
type: "object",
|
|
560
|
+
properties: {
|
|
561
|
+
defined: { const: false },
|
|
562
|
+
code: { type: "string" },
|
|
563
|
+
status: { type: "number" },
|
|
564
|
+
message: { type: "string" },
|
|
565
|
+
data: {}
|
|
566
|
+
},
|
|
567
|
+
required: ["defined", "code", "status", "message"]
|
|
568
|
+
};
|
|
569
|
+
const baseSchemaConvertOptions = {};
|
|
570
|
+
if (commonSchemas) {
|
|
571
|
+
baseSchemaConvertOptions.components = [];
|
|
572
|
+
for (const key in commonSchemas) {
|
|
573
|
+
const options = commonSchemas[key];
|
|
574
|
+
if (options.schema === void 0) {
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
const { schema, strategy = "input" } = options;
|
|
578
|
+
const [required, json] = await this.converter.convert(schema, { strategy });
|
|
579
|
+
const allowedStrategies = [strategy];
|
|
580
|
+
if (strategy === "input") {
|
|
581
|
+
const [outputRequired, outputJson] = await this.converter.convert(schema, { strategy: "output" });
|
|
582
|
+
if (outputRequired === required && stringifyJSON(outputJson) === stringifyJSON(json)) {
|
|
583
|
+
allowedStrategies.push("output");
|
|
584
|
+
}
|
|
585
|
+
} else if (strategy === "output") {
|
|
586
|
+
const [inputRequired, inputJson] = await this.converter.convert(schema, { strategy: "input" });
|
|
587
|
+
if (inputRequired === required && stringifyJSON(inputJson) === stringifyJSON(json)) {
|
|
588
|
+
allowedStrategies.push("input");
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
baseSchemaConvertOptions.components.push({
|
|
592
|
+
schema,
|
|
593
|
+
required,
|
|
594
|
+
ref: `#/components/schemas/${key}`,
|
|
595
|
+
allowedStrategies
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
doc.components ??= {};
|
|
599
|
+
doc.components.schemas ??= {};
|
|
600
|
+
for (const key in commonSchemas) {
|
|
601
|
+
const options = commonSchemas[key];
|
|
602
|
+
if (options.schema === void 0) {
|
|
603
|
+
if (options.error === "UndefinedError") {
|
|
604
|
+
doc.components.schemas[key] = toOpenAPISchema(undefinedErrorJsonSchema);
|
|
605
|
+
undefinedErrorJsonSchema = { $ref: `#/components/schemas/${key}` };
|
|
606
|
+
}
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
const { schema, strategy = "input" } = options;
|
|
610
|
+
const [, json] = await this.converter.convert(
|
|
611
|
+
schema,
|
|
612
|
+
{
|
|
613
|
+
...baseSchemaConvertOptions,
|
|
614
|
+
strategy,
|
|
615
|
+
minStructureDepthForRef: 1
|
|
616
|
+
// not allow use $ref for root schemas
|
|
617
|
+
}
|
|
618
|
+
);
|
|
619
|
+
doc.components.schemas[key] = toOpenAPISchema(json);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return { baseSchemaConvertOptions, undefinedErrorJsonSchema };
|
|
623
|
+
}
|
|
624
|
+
async #request(doc, ref, def, baseSchemaConvertOptions) {
|
|
380
625
|
const method = fallbackContractConfig("defaultMethod", def.route.method);
|
|
381
626
|
const details = getEventIteratorSchemaDetails(def.inputSchema);
|
|
382
627
|
if (details) {
|
|
383
628
|
ref.requestBody = {
|
|
384
629
|
required: true,
|
|
385
630
|
content: toOpenAPIEventIteratorContent(
|
|
386
|
-
await this.converter.convert(details.yields, { strategy: "input" }),
|
|
387
|
-
await this.converter.convert(details.returns, { strategy: "input" })
|
|
631
|
+
await this.converter.convert(details.yields, { ...baseSchemaConvertOptions, strategy: "input" }),
|
|
632
|
+
await this.converter.convert(details.returns, { ...baseSchemaConvertOptions, strategy: "input" })
|
|
388
633
|
)
|
|
389
634
|
};
|
|
390
635
|
return;
|
|
391
636
|
}
|
|
392
637
|
const dynamicParams = getDynamicParams(def.route.path)?.map((v) => v.name);
|
|
393
638
|
const inputStructure = fallbackContractConfig("defaultInputStructure", def.route.inputStructure);
|
|
394
|
-
let [required, schema] = await this.converter.convert(
|
|
639
|
+
let [required, schema] = await this.converter.convert(
|
|
640
|
+
def.inputSchema,
|
|
641
|
+
{
|
|
642
|
+
...baseSchemaConvertOptions,
|
|
643
|
+
strategy: "input"
|
|
644
|
+
}
|
|
645
|
+
);
|
|
395
646
|
if (isAnySchema(schema) && !dynamicParams?.length) {
|
|
396
647
|
return;
|
|
397
648
|
}
|
|
649
|
+
if (inputStructure === "detailed" || inputStructure === "compact" && (dynamicParams?.length || method === "GET")) {
|
|
650
|
+
schema = simplifyComposedObjectJsonSchemasAndRefs(schema, doc);
|
|
651
|
+
}
|
|
398
652
|
if (inputStructure === "compact") {
|
|
399
653
|
if (dynamicParams?.length) {
|
|
400
654
|
const error2 = new OpenAPIGeneratorError(
|
|
@@ -434,7 +688,8 @@ ${errors.join("\n\n")}`
|
|
|
434
688
|
if (!isObjectSchema(schema)) {
|
|
435
689
|
throw error;
|
|
436
690
|
}
|
|
437
|
-
|
|
691
|
+
const resolvedParamSchema = schema.properties?.params !== void 0 ? simplifyComposedObjectJsonSchemasAndRefs(schema.properties.params, doc) : void 0;
|
|
692
|
+
if (dynamicParams?.length && (resolvedParamSchema === void 0 || !isObjectSchema(resolvedParamSchema) || !checkParamsSchema(resolvedParamSchema, dynamicParams))) {
|
|
438
693
|
throw new OpenAPIGeneratorError(
|
|
439
694
|
'When input structure is "detailed" and path has dynamic params, the "params" schema must be an object with all dynamic params as required.'
|
|
440
695
|
);
|
|
@@ -442,12 +697,13 @@ ${errors.join("\n\n")}`
|
|
|
442
697
|
for (const from of ["params", "query", "headers"]) {
|
|
443
698
|
const fromSchema = schema.properties?.[from];
|
|
444
699
|
if (fromSchema !== void 0) {
|
|
445
|
-
|
|
700
|
+
const resolvedSchema = simplifyComposedObjectJsonSchemasAndRefs(fromSchema, doc);
|
|
701
|
+
if (!isObjectSchema(resolvedSchema)) {
|
|
446
702
|
throw error;
|
|
447
703
|
}
|
|
448
704
|
const parameterIn = from === "params" ? "path" : from === "headers" ? "header" : "query";
|
|
449
705
|
ref.parameters ??= [];
|
|
450
|
-
ref.parameters.push(...toOpenAPIParameters(
|
|
706
|
+
ref.parameters.push(...toOpenAPIParameters(resolvedSchema, parameterIn));
|
|
451
707
|
}
|
|
452
708
|
}
|
|
453
709
|
if (schema.properties?.body !== void 0) {
|
|
@@ -457,7 +713,7 @@ ${errors.join("\n\n")}`
|
|
|
457
713
|
};
|
|
458
714
|
}
|
|
459
715
|
}
|
|
460
|
-
async #successResponse(ref, def) {
|
|
716
|
+
async #successResponse(doc, ref, def, baseSchemaConvertOptions) {
|
|
461
717
|
const outputSchema = def.outputSchema;
|
|
462
718
|
const status = fallbackContractConfig("defaultSuccessStatus", def.route.successStatus);
|
|
463
719
|
const description = fallbackContractConfig("defaultSuccessDescription", def.route?.successDescription);
|
|
@@ -468,13 +724,20 @@ ${errors.join("\n\n")}`
|
|
|
468
724
|
ref.responses[status] = {
|
|
469
725
|
description,
|
|
470
726
|
content: toOpenAPIEventIteratorContent(
|
|
471
|
-
await this.converter.convert(eventIteratorSchemaDetails.yields, { strategy: "output" }),
|
|
472
|
-
await this.converter.convert(eventIteratorSchemaDetails.returns, { strategy: "output" })
|
|
727
|
+
await this.converter.convert(eventIteratorSchemaDetails.yields, { ...baseSchemaConvertOptions, strategy: "output" }),
|
|
728
|
+
await this.converter.convert(eventIteratorSchemaDetails.returns, { ...baseSchemaConvertOptions, strategy: "output" })
|
|
473
729
|
)
|
|
474
730
|
};
|
|
475
731
|
return;
|
|
476
732
|
}
|
|
477
|
-
const [required, json] = await this.converter.convert(
|
|
733
|
+
const [required, json] = await this.converter.convert(
|
|
734
|
+
outputSchema,
|
|
735
|
+
{
|
|
736
|
+
...baseSchemaConvertOptions,
|
|
737
|
+
strategy: "output",
|
|
738
|
+
minStructureDepthForRef: outputStructure === "detailed" ? 1 : 0
|
|
739
|
+
}
|
|
740
|
+
);
|
|
478
741
|
if (outputStructure === "compact") {
|
|
479
742
|
ref.responses ??= {};
|
|
480
743
|
ref.responses[status] = {
|
|
@@ -495,17 +758,19 @@ ${errors.join("\n\n")}`
|
|
|
495
758
|
|
|
496
759
|
But got: ${stringifyJSON(item)}
|
|
497
760
|
`);
|
|
498
|
-
|
|
761
|
+
const simplifiedItem = simplifyComposedObjectJsonSchemasAndRefs(item, doc);
|
|
762
|
+
if (!isObjectSchema(simplifiedItem)) {
|
|
499
763
|
throw error;
|
|
500
764
|
}
|
|
501
765
|
let schemaStatus;
|
|
502
766
|
let schemaDescription;
|
|
503
|
-
if (
|
|
504
|
-
|
|
767
|
+
if (simplifiedItem.properties?.status !== void 0) {
|
|
768
|
+
const statusSchema = resolveOpenAPIJsonSchemaRef(doc, simplifiedItem.properties.status);
|
|
769
|
+
if (typeof statusSchema !== "object" || statusSchema.const === void 0 || typeof statusSchema.const !== "number" || !Number.isInteger(statusSchema.const) || isORPCErrorStatus(statusSchema.const)) {
|
|
505
770
|
throw error;
|
|
506
771
|
}
|
|
507
|
-
schemaStatus =
|
|
508
|
-
schemaDescription =
|
|
772
|
+
schemaStatus = statusSchema.const;
|
|
773
|
+
schemaDescription = statusSchema.description;
|
|
509
774
|
}
|
|
510
775
|
const itemStatus = schemaStatus ?? status;
|
|
511
776
|
const itemDescription = schemaDescription ?? description;
|
|
@@ -520,71 +785,64 @@ ${errors.join("\n\n")}`
|
|
|
520
785
|
ref.responses[itemStatus] = {
|
|
521
786
|
description: itemDescription
|
|
522
787
|
};
|
|
523
|
-
if (
|
|
524
|
-
|
|
788
|
+
if (simplifiedItem.properties?.headers !== void 0) {
|
|
789
|
+
const headersSchema = simplifyComposedObjectJsonSchemasAndRefs(simplifiedItem.properties.headers, doc);
|
|
790
|
+
if (!isObjectSchema(headersSchema)) {
|
|
525
791
|
throw error;
|
|
526
792
|
}
|
|
527
|
-
for (const key in
|
|
528
|
-
const headerSchema =
|
|
793
|
+
for (const key in headersSchema.properties) {
|
|
794
|
+
const headerSchema = headersSchema.properties[key];
|
|
529
795
|
if (headerSchema !== void 0) {
|
|
530
796
|
ref.responses[itemStatus].headers ??= {};
|
|
531
797
|
ref.responses[itemStatus].headers[key] = {
|
|
532
798
|
schema: toOpenAPISchema(headerSchema),
|
|
533
|
-
required:
|
|
799
|
+
required: simplifiedItem.required?.includes("headers") && headersSchema.required?.includes(key)
|
|
534
800
|
};
|
|
535
801
|
}
|
|
536
802
|
}
|
|
537
803
|
}
|
|
538
|
-
if (
|
|
804
|
+
if (simplifiedItem.properties?.body !== void 0) {
|
|
539
805
|
ref.responses[itemStatus].content = toOpenAPIContent(
|
|
540
|
-
applySchemaOptionality(
|
|
806
|
+
applySchemaOptionality(simplifiedItem.required?.includes("body") ?? false, simplifiedItem.properties.body)
|
|
541
807
|
);
|
|
542
808
|
}
|
|
543
809
|
}
|
|
544
810
|
}
|
|
545
|
-
async #errorResponse(ref, def) {
|
|
811
|
+
async #errorResponse(ref, def, baseSchemaConvertOptions, undefinedErrorSchema, customErrorResponseBodySchema) {
|
|
546
812
|
const errorMap = def.errorMap;
|
|
547
|
-
const
|
|
813
|
+
const errorResponsesByStatus = {};
|
|
548
814
|
for (const code in errorMap) {
|
|
549
815
|
const config = errorMap[code];
|
|
550
816
|
if (!config) {
|
|
551
817
|
continue;
|
|
552
818
|
}
|
|
553
819
|
const status = fallbackORPCErrorStatus(code, config.status);
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
820
|
+
const defaultMessage = fallbackORPCErrorMessage(code, config.message);
|
|
821
|
+
errorResponsesByStatus[status] ??= { status, definedErrorDefinitions: [], errorSchemaVariants: [] };
|
|
822
|
+
const [dataRequired, dataSchema] = await this.converter.convert(config.data, { ...baseSchemaConvertOptions, strategy: "output" });
|
|
823
|
+
errorResponsesByStatus[status].definedErrorDefinitions.push([code, defaultMessage, dataRequired, dataSchema]);
|
|
824
|
+
errorResponsesByStatus[status].errorSchemaVariants.push({
|
|
558
825
|
type: "object",
|
|
559
826
|
properties: {
|
|
560
827
|
defined: { const: true },
|
|
561
828
|
code: { const: code },
|
|
562
829
|
status: { const: status },
|
|
563
|
-
message: { type: "string", default:
|
|
830
|
+
message: { type: "string", default: defaultMessage },
|
|
564
831
|
data: dataSchema
|
|
565
832
|
},
|
|
566
833
|
required: dataRequired ? ["defined", "code", "status", "message", "data"] : ["defined", "code", "status", "message"]
|
|
567
834
|
});
|
|
568
835
|
}
|
|
569
836
|
ref.responses ??= {};
|
|
570
|
-
for (const
|
|
571
|
-
const
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
837
|
+
for (const statusString in errorResponsesByStatus) {
|
|
838
|
+
const errorResponse = errorResponsesByStatus[statusString];
|
|
839
|
+
const customBodySchema = value(customErrorResponseBodySchema, errorResponse.definedErrorDefinitions, errorResponse.status);
|
|
840
|
+
ref.responses[statusString] = {
|
|
841
|
+
description: statusString,
|
|
842
|
+
content: toOpenAPIContent(customBodySchema ?? {
|
|
575
843
|
oneOf: [
|
|
576
|
-
...
|
|
577
|
-
|
|
578
|
-
type: "object",
|
|
579
|
-
properties: {
|
|
580
|
-
defined: { const: false },
|
|
581
|
-
code: { type: "string" },
|
|
582
|
-
status: { type: "number" },
|
|
583
|
-
message: { type: "string" },
|
|
584
|
-
data: {}
|
|
585
|
-
},
|
|
586
|
-
required: ["defined", "code", "status", "message"]
|
|
587
|
-
}
|
|
844
|
+
...errorResponse.errorSchemaVariants,
|
|
845
|
+
undefinedErrorSchema
|
|
588
846
|
]
|
|
589
847
|
})
|
|
590
848
|
};
|
|
@@ -592,4 +850,4 @@ ${errors.join("\n\n")}`
|
|
|
592
850
|
}
|
|
593
851
|
}
|
|
594
852
|
|
|
595
|
-
export { CompositeSchemaConverter as C, LOGIC_KEYWORDS as L, OpenAPIGenerator as O, applyCustomOpenAPIOperation as a, toOpenAPIMethod as b, customOpenAPIOperation as c, toOpenAPIContent as d, toOpenAPIEventIteratorContent as e, toOpenAPIParameters as f, getCustomOpenAPIOperation as g, checkParamsSchema as h, toOpenAPISchema as i, isFileSchema as j, isObjectSchema as k, isAnySchema as l,
|
|
853
|
+
export { CompositeSchemaConverter as C, LOGIC_KEYWORDS as L, OpenAPIGenerator as O, applyCustomOpenAPIOperation as a, toOpenAPIMethod as b, customOpenAPIOperation as c, toOpenAPIContent as d, toOpenAPIEventIteratorContent as e, toOpenAPIParameters as f, getCustomOpenAPIOperation as g, checkParamsSchema as h, toOpenAPISchema as i, isFileSchema as j, isObjectSchema as k, isAnySchema as l, separateObjectSchema as m, filterSchemaBranches as n, applySchemaOptionality as o, expandUnionSchema as p, expandArrayableSchema as q, resolveOpenAPIJsonSchemaRef as r, simplifyComposedObjectJsonSchemasAndRefs as s, toOpenAPIPath as t, isPrimitiveSchema as u };
|
|
@@ -2,15 +2,17 @@ import { standardizeHTTPPath, StandardOpenAPIJsonSerializer, StandardBracketNota
|
|
|
2
2
|
import { StandardHandler } from '@orpc/server/standard';
|
|
3
3
|
import { isORPCErrorStatus } from '@orpc/client';
|
|
4
4
|
import { fallbackContractConfig } from '@orpc/contract';
|
|
5
|
-
import { isObject, stringifyJSON } from '@orpc/shared';
|
|
5
|
+
import { isObject, stringifyJSON, tryDecodeURIComponent, value } from '@orpc/shared';
|
|
6
6
|
import { toHttpPath } from '@orpc/client/standard';
|
|
7
7
|
import { traverseContractProcedures, isProcedure, getLazyMeta, unlazy, getRouter, createContractedProcedure } from '@orpc/server';
|
|
8
8
|
import { createRouter, addRoute, findRoute } from 'rou3';
|
|
9
9
|
|
|
10
10
|
class StandardOpenAPICodec {
|
|
11
|
-
constructor(serializer) {
|
|
11
|
+
constructor(serializer, options = {}) {
|
|
12
12
|
this.serializer = serializer;
|
|
13
|
+
this.customErrorResponseBodyEncoder = options.customErrorResponseBodyEncoder;
|
|
13
14
|
}
|
|
15
|
+
customErrorResponseBodyEncoder;
|
|
14
16
|
async decode(request, params, procedure) {
|
|
15
17
|
const inputStructure = fallbackContractConfig("defaultInputStructure", procedure["~orpc"].route.inputStructure);
|
|
16
18
|
if (inputStructure === "compact") {
|
|
@@ -73,10 +75,11 @@ class StandardOpenAPICodec {
|
|
|
73
75
|
};
|
|
74
76
|
}
|
|
75
77
|
encodeError(error) {
|
|
78
|
+
const body = this.customErrorResponseBodyEncoder?.(error) ?? error.toJSON();
|
|
76
79
|
return {
|
|
77
80
|
status: error.status,
|
|
78
81
|
headers: {},
|
|
79
|
-
body: this.serializer.serialize(
|
|
82
|
+
body: this.serializer.serialize(body, { outputFormat: "plain" })
|
|
80
83
|
};
|
|
81
84
|
}
|
|
82
85
|
#isDetailedOutput(output) {
|
|
@@ -97,14 +100,22 @@ function toRou3Pattern(path) {
|
|
|
97
100
|
return standardizeHTTPPath(path).replace(/\/\{\+([^}]+)\}/g, "/**:$1").replace(/\/\{([^}]+)\}/g, "/:$1");
|
|
98
101
|
}
|
|
99
102
|
function decodeParams(params) {
|
|
100
|
-
return Object.fromEntries(Object.entries(params).map(([key, value]) => [key,
|
|
103
|
+
return Object.fromEntries(Object.entries(params).map(([key, value]) => [key, tryDecodeURIComponent(value)]));
|
|
101
104
|
}
|
|
102
105
|
|
|
103
106
|
class StandardOpenAPIMatcher {
|
|
107
|
+
filter;
|
|
104
108
|
tree = createRouter();
|
|
105
109
|
pendingRouters = [];
|
|
110
|
+
constructor(options = {}) {
|
|
111
|
+
this.filter = options.filter ?? true;
|
|
112
|
+
}
|
|
106
113
|
init(router, path = []) {
|
|
107
|
-
const laziedOptions = traverseContractProcedures({ router, path }, (
|
|
114
|
+
const laziedOptions = traverseContractProcedures({ router, path }, (traverseOptions) => {
|
|
115
|
+
if (!value(this.filter, traverseOptions)) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const { path: path2, contract } = traverseOptions;
|
|
108
119
|
const method = fallbackContractConfig("defaultMethod", contract["~orpc"].route.method);
|
|
109
120
|
const httpPath = toRou3Pattern(contract["~orpc"].route.path ?? toHttpPath(path2));
|
|
110
121
|
if (isProcedure(contract)) {
|
|
@@ -168,10 +179,10 @@ class StandardOpenAPIMatcher {
|
|
|
168
179
|
class StandardOpenAPIHandler extends StandardHandler {
|
|
169
180
|
constructor(router, options) {
|
|
170
181
|
const jsonSerializer = new StandardOpenAPIJsonSerializer(options);
|
|
171
|
-
const bracketNotationSerializer = new StandardBracketNotationSerializer();
|
|
182
|
+
const bracketNotationSerializer = new StandardBracketNotationSerializer(options);
|
|
172
183
|
const serializer = new StandardOpenAPISerializer(jsonSerializer, bracketNotationSerializer);
|
|
173
|
-
const matcher = new StandardOpenAPIMatcher();
|
|
174
|
-
const codec = new StandardOpenAPICodec(serializer);
|
|
184
|
+
const matcher = new StandardOpenAPIMatcher(options);
|
|
185
|
+
const codec = new StandardOpenAPICodec(serializer, options);
|
|
175
186
|
super(router, matcher, codec, options);
|
|
176
187
|
}
|
|
177
188
|
}
|