@orpc/openapi 0.0.0-next.e385fb7 → 0.0.0-next.e49bee7
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 +6 -3
- package/dist/adapters/aws-lambda/index.d.ts +6 -3
- package/dist/adapters/aws-lambda/index.mjs +3 -3
- 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 +19 -13
- package/dist/index.d.ts +19 -13
- package/dist/index.mjs +3 -3
- package/dist/plugins/index.d.mts +20 -6
- package/dist/plugins/index.d.ts +20 -6
- 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.ExRBHuvW.mjs → openapi.CzHcOMxv.mjs} +280 -74
- 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 +16 -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, stringifyJSON, findDeepMatches, toArray, clone } from '@orpc/shared';
|
|
7
|
-
import { TypeName } from '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));
|
|
@@ -345,6 +350,116 @@ function checkParamsSchema(schema, params) {
|
|
|
345
350
|
function toOpenAPISchema(schema) {
|
|
346
351
|
return schema === true ? {} : schema === false ? { not: {} } : schema;
|
|
347
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
|
+
}
|
|
348
463
|
|
|
349
464
|
class CompositeSchemaConverter {
|
|
350
465
|
converters;
|
|
@@ -375,37 +490,48 @@ class OpenAPIGenerator {
|
|
|
375
490
|
*
|
|
376
491
|
* @see {@link https://orpc.unnoq.com/docs/openapi/openapi-specification OpenAPI Specification Docs}
|
|
377
492
|
*/
|
|
378
|
-
async generate(router,
|
|
379
|
-
const
|
|
493
|
+
async generate(router, { customErrorResponseBodySchema, commonSchemas, filter: baseFilter, exclude, ...baseDoc } = {}) {
|
|
494
|
+
const filter = baseFilter ?? (({ contract, path }) => {
|
|
495
|
+
return !(exclude?.(contract, path) ?? false);
|
|
496
|
+
});
|
|
380
497
|
const doc = {
|
|
381
|
-
...clone(
|
|
382
|
-
info:
|
|
383
|
-
openapi: "3.1.1"
|
|
384
|
-
exclude: void 0
|
|
498
|
+
...clone(baseDoc),
|
|
499
|
+
info: baseDoc.info ?? { title: "API Reference", version: "0.0.0" },
|
|
500
|
+
openapi: "3.1.1"
|
|
385
501
|
};
|
|
502
|
+
const { baseSchemaConvertOptions, undefinedErrorJsonSchema } = await this.#resolveCommonSchemas(doc, commonSchemas);
|
|
386
503
|
const contracts = [];
|
|
387
|
-
await resolveContractProcedures({ path: [], router }, (
|
|
388
|
-
if (!
|
|
389
|
-
|
|
504
|
+
await resolveContractProcedures({ path: [], router }, (traverseOptions) => {
|
|
505
|
+
if (!value(filter, traverseOptions)) {
|
|
506
|
+
return;
|
|
390
507
|
}
|
|
508
|
+
contracts.push(traverseOptions);
|
|
391
509
|
});
|
|
392
510
|
const errors = [];
|
|
393
511
|
for (const { contract, path } of contracts) {
|
|
394
|
-
const
|
|
512
|
+
const stringPath = path.join(".");
|
|
395
513
|
try {
|
|
396
514
|
const def = contract["~orpc"];
|
|
397
515
|
const method = toOpenAPIMethod(fallbackContractConfig("defaultMethod", def.route.method));
|
|
398
516
|
const httpPath = toOpenAPIPath(def.route.path ?? toHttpPath(path));
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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
|
+
}
|
|
409
535
|
doc.paths ??= {};
|
|
410
536
|
doc.paths[httpPath] ??= {};
|
|
411
537
|
doc.paths[httpPath][method] = applyCustomOpenAPIOperation(operationObjectRef, contract);
|
|
@@ -414,7 +540,7 @@ class OpenAPIGenerator {
|
|
|
414
540
|
throw e;
|
|
415
541
|
}
|
|
416
542
|
errors.push(
|
|
417
|
-
`[OpenAPIGenerator] Error occurred while generating OpenAPI for procedure at path: ${
|
|
543
|
+
`[OpenAPIGenerator] Error occurred while generating OpenAPI for procedure at path: ${stringPath}
|
|
418
544
|
${e.message}`
|
|
419
545
|
);
|
|
420
546
|
}
|
|
@@ -428,25 +554,101 @@ ${errors.join("\n\n")}`
|
|
|
428
554
|
}
|
|
429
555
|
return this.serializer.serialize(doc)[0];
|
|
430
556
|
}
|
|
431
|
-
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) {
|
|
432
625
|
const method = fallbackContractConfig("defaultMethod", def.route.method);
|
|
433
626
|
const details = getEventIteratorSchemaDetails(def.inputSchema);
|
|
434
627
|
if (details) {
|
|
435
628
|
ref.requestBody = {
|
|
436
629
|
required: true,
|
|
437
630
|
content: toOpenAPIEventIteratorContent(
|
|
438
|
-
await this.converter.convert(details.yields, { strategy: "input" }),
|
|
439
|
-
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" })
|
|
440
633
|
)
|
|
441
634
|
};
|
|
442
635
|
return;
|
|
443
636
|
}
|
|
444
637
|
const dynamicParams = getDynamicParams(def.route.path)?.map((v) => v.name);
|
|
445
638
|
const inputStructure = fallbackContractConfig("defaultInputStructure", def.route.inputStructure);
|
|
446
|
-
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
|
+
);
|
|
447
646
|
if (isAnySchema(schema) && !dynamicParams?.length) {
|
|
448
647
|
return;
|
|
449
648
|
}
|
|
649
|
+
if (inputStructure === "detailed" || inputStructure === "compact" && (dynamicParams?.length || method === "GET")) {
|
|
650
|
+
schema = simplifyComposedObjectJsonSchemasAndRefs(schema, doc);
|
|
651
|
+
}
|
|
450
652
|
if (inputStructure === "compact") {
|
|
451
653
|
if (dynamicParams?.length) {
|
|
452
654
|
const error2 = new OpenAPIGeneratorError(
|
|
@@ -486,7 +688,8 @@ ${errors.join("\n\n")}`
|
|
|
486
688
|
if (!isObjectSchema(schema)) {
|
|
487
689
|
throw error;
|
|
488
690
|
}
|
|
489
|
-
|
|
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))) {
|
|
490
693
|
throw new OpenAPIGeneratorError(
|
|
491
694
|
'When input structure is "detailed" and path has dynamic params, the "params" schema must be an object with all dynamic params as required.'
|
|
492
695
|
);
|
|
@@ -494,12 +697,13 @@ ${errors.join("\n\n")}`
|
|
|
494
697
|
for (const from of ["params", "query", "headers"]) {
|
|
495
698
|
const fromSchema = schema.properties?.[from];
|
|
496
699
|
if (fromSchema !== void 0) {
|
|
497
|
-
|
|
700
|
+
const resolvedSchema = simplifyComposedObjectJsonSchemasAndRefs(fromSchema, doc);
|
|
701
|
+
if (!isObjectSchema(resolvedSchema)) {
|
|
498
702
|
throw error;
|
|
499
703
|
}
|
|
500
704
|
const parameterIn = from === "params" ? "path" : from === "headers" ? "header" : "query";
|
|
501
705
|
ref.parameters ??= [];
|
|
502
|
-
ref.parameters.push(...toOpenAPIParameters(
|
|
706
|
+
ref.parameters.push(...toOpenAPIParameters(resolvedSchema, parameterIn));
|
|
503
707
|
}
|
|
504
708
|
}
|
|
505
709
|
if (schema.properties?.body !== void 0) {
|
|
@@ -509,7 +713,7 @@ ${errors.join("\n\n")}`
|
|
|
509
713
|
};
|
|
510
714
|
}
|
|
511
715
|
}
|
|
512
|
-
async #successResponse(ref, def) {
|
|
716
|
+
async #successResponse(doc, ref, def, baseSchemaConvertOptions) {
|
|
513
717
|
const outputSchema = def.outputSchema;
|
|
514
718
|
const status = fallbackContractConfig("defaultSuccessStatus", def.route.successStatus);
|
|
515
719
|
const description = fallbackContractConfig("defaultSuccessDescription", def.route?.successDescription);
|
|
@@ -520,13 +724,20 @@ ${errors.join("\n\n")}`
|
|
|
520
724
|
ref.responses[status] = {
|
|
521
725
|
description,
|
|
522
726
|
content: toOpenAPIEventIteratorContent(
|
|
523
|
-
await this.converter.convert(eventIteratorSchemaDetails.yields, { strategy: "output" }),
|
|
524
|
-
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" })
|
|
525
729
|
)
|
|
526
730
|
};
|
|
527
731
|
return;
|
|
528
732
|
}
|
|
529
|
-
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
|
+
);
|
|
530
741
|
if (outputStructure === "compact") {
|
|
531
742
|
ref.responses ??= {};
|
|
532
743
|
ref.responses[status] = {
|
|
@@ -547,17 +758,19 @@ ${errors.join("\n\n")}`
|
|
|
547
758
|
|
|
548
759
|
But got: ${stringifyJSON(item)}
|
|
549
760
|
`);
|
|
550
|
-
|
|
761
|
+
const simplifiedItem = simplifyComposedObjectJsonSchemasAndRefs(item, doc);
|
|
762
|
+
if (!isObjectSchema(simplifiedItem)) {
|
|
551
763
|
throw error;
|
|
552
764
|
}
|
|
553
765
|
let schemaStatus;
|
|
554
766
|
let schemaDescription;
|
|
555
|
-
if (
|
|
556
|
-
|
|
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)) {
|
|
557
770
|
throw error;
|
|
558
771
|
}
|
|
559
|
-
schemaStatus =
|
|
560
|
-
schemaDescription =
|
|
772
|
+
schemaStatus = statusSchema.const;
|
|
773
|
+
schemaDescription = statusSchema.description;
|
|
561
774
|
}
|
|
562
775
|
const itemStatus = schemaStatus ?? status;
|
|
563
776
|
const itemDescription = schemaDescription ?? description;
|
|
@@ -572,71 +785,64 @@ ${errors.join("\n\n")}`
|
|
|
572
785
|
ref.responses[itemStatus] = {
|
|
573
786
|
description: itemDescription
|
|
574
787
|
};
|
|
575
|
-
if (
|
|
576
|
-
|
|
788
|
+
if (simplifiedItem.properties?.headers !== void 0) {
|
|
789
|
+
const headersSchema = simplifyComposedObjectJsonSchemasAndRefs(simplifiedItem.properties.headers, doc);
|
|
790
|
+
if (!isObjectSchema(headersSchema)) {
|
|
577
791
|
throw error;
|
|
578
792
|
}
|
|
579
|
-
for (const key in
|
|
580
|
-
const headerSchema =
|
|
793
|
+
for (const key in headersSchema.properties) {
|
|
794
|
+
const headerSchema = headersSchema.properties[key];
|
|
581
795
|
if (headerSchema !== void 0) {
|
|
582
796
|
ref.responses[itemStatus].headers ??= {};
|
|
583
797
|
ref.responses[itemStatus].headers[key] = {
|
|
584
798
|
schema: toOpenAPISchema(headerSchema),
|
|
585
|
-
required:
|
|
799
|
+
required: simplifiedItem.required?.includes("headers") && headersSchema.required?.includes(key)
|
|
586
800
|
};
|
|
587
801
|
}
|
|
588
802
|
}
|
|
589
803
|
}
|
|
590
|
-
if (
|
|
804
|
+
if (simplifiedItem.properties?.body !== void 0) {
|
|
591
805
|
ref.responses[itemStatus].content = toOpenAPIContent(
|
|
592
|
-
applySchemaOptionality(
|
|
806
|
+
applySchemaOptionality(simplifiedItem.required?.includes("body") ?? false, simplifiedItem.properties.body)
|
|
593
807
|
);
|
|
594
808
|
}
|
|
595
809
|
}
|
|
596
810
|
}
|
|
597
|
-
async #errorResponse(ref, def) {
|
|
811
|
+
async #errorResponse(ref, def, baseSchemaConvertOptions, undefinedErrorSchema, customErrorResponseBodySchema) {
|
|
598
812
|
const errorMap = def.errorMap;
|
|
599
|
-
const
|
|
813
|
+
const errorResponsesByStatus = {};
|
|
600
814
|
for (const code in errorMap) {
|
|
601
815
|
const config = errorMap[code];
|
|
602
816
|
if (!config) {
|
|
603
817
|
continue;
|
|
604
818
|
}
|
|
605
819
|
const status = fallbackORPCErrorStatus(code, config.status);
|
|
606
|
-
const
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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({
|
|
610
825
|
type: "object",
|
|
611
826
|
properties: {
|
|
612
827
|
defined: { const: true },
|
|
613
828
|
code: { const: code },
|
|
614
829
|
status: { const: status },
|
|
615
|
-
message: { type: "string", default:
|
|
830
|
+
message: { type: "string", default: defaultMessage },
|
|
616
831
|
data: dataSchema
|
|
617
832
|
},
|
|
618
833
|
required: dataRequired ? ["defined", "code", "status", "message", "data"] : ["defined", "code", "status", "message"]
|
|
619
834
|
});
|
|
620
835
|
}
|
|
621
836
|
ref.responses ??= {};
|
|
622
|
-
for (const
|
|
623
|
-
const
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
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 ?? {
|
|
627
843
|
oneOf: [
|
|
628
|
-
...
|
|
629
|
-
|
|
630
|
-
type: "object",
|
|
631
|
-
properties: {
|
|
632
|
-
defined: { const: false },
|
|
633
|
-
code: { type: "string" },
|
|
634
|
-
status: { type: "number" },
|
|
635
|
-
message: { type: "string" },
|
|
636
|
-
data: {}
|
|
637
|
-
},
|
|
638
|
-
required: ["defined", "code", "status", "message"]
|
|
639
|
-
}
|
|
844
|
+
...errorResponse.errorSchemaVariants,
|
|
845
|
+
undefinedErrorSchema
|
|
640
846
|
]
|
|
641
847
|
})
|
|
642
848
|
};
|
|
@@ -644,4 +850,4 @@ ${errors.join("\n\n")}`
|
|
|
644
850
|
}
|
|
645
851
|
}
|
|
646
852
|
|
|
647
|
-
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
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { StandardOpenAPISerializer, StandardOpenAPIJsonSerializerOptions, StandardBracketNotationSerializerOptions } from '@orpc/openapi-client/standard';
|
|
2
|
+
import { AnyProcedure, TraverseContractProcedureCallbackOptions, AnyRouter, Context, Router } from '@orpc/server';
|
|
3
|
+
import { StandardCodec, StandardParams, StandardMatcher, StandardMatchResult, StandardHandlerOptions, StandardHandler } from '@orpc/server/standard';
|
|
4
|
+
import { ORPCError, HTTPPath } from '@orpc/client';
|
|
5
|
+
import { StandardLazyRequest, StandardResponse } from '@orpc/standard-server';
|
|
6
|
+
import { Value } from '@orpc/shared';
|
|
7
|
+
|
|
8
|
+
interface StandardOpenAPICodecOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Customize how an ORPC error is encoded into a response body.
|
|
11
|
+
* Use this if your API needs a different error output structure.
|
|
12
|
+
*
|
|
13
|
+
* @remarks
|
|
14
|
+
* - Return `null | undefined` to fallback to default behavior
|
|
15
|
+
*
|
|
16
|
+
* @default ((e) => e.toJSON())
|
|
17
|
+
*/
|
|
18
|
+
customErrorResponseBodyEncoder?: (error: ORPCError<any, any>) => unknown;
|
|
19
|
+
}
|
|
20
|
+
declare class StandardOpenAPICodec implements StandardCodec {
|
|
21
|
+
#private;
|
|
22
|
+
private readonly serializer;
|
|
23
|
+
private readonly customErrorResponseBodyEncoder;
|
|
24
|
+
constructor(serializer: StandardOpenAPISerializer, options?: StandardOpenAPICodecOptions);
|
|
25
|
+
decode(request: StandardLazyRequest, params: StandardParams | undefined, procedure: AnyProcedure): Promise<unknown>;
|
|
26
|
+
encode(output: unknown, procedure: AnyProcedure): StandardResponse;
|
|
27
|
+
encodeError(error: ORPCError<any, any>): StandardResponse;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface StandardOpenAPIMatcherOptions {
|
|
31
|
+
/**
|
|
32
|
+
* Filter procedures. Return `false` to exclude a procedure from matching.
|
|
33
|
+
*
|
|
34
|
+
* @default true
|
|
35
|
+
*/
|
|
36
|
+
filter?: Value<boolean, [options: TraverseContractProcedureCallbackOptions]>;
|
|
37
|
+
}
|
|
38
|
+
declare class StandardOpenAPIMatcher implements StandardMatcher {
|
|
39
|
+
private readonly filter;
|
|
40
|
+
private readonly tree;
|
|
41
|
+
private pendingRouters;
|
|
42
|
+
constructor(options?: StandardOpenAPIMatcherOptions);
|
|
43
|
+
init(router: AnyRouter, path?: readonly string[]): void;
|
|
44
|
+
match(method: string, pathname: HTTPPath): Promise<StandardMatchResult>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface StandardOpenAPIHandlerOptions<T extends Context> extends StandardHandlerOptions<T>, StandardOpenAPIJsonSerializerOptions, StandardBracketNotationSerializerOptions, StandardOpenAPIMatcherOptions, StandardOpenAPICodecOptions {
|
|
48
|
+
}
|
|
49
|
+
declare class StandardOpenAPIHandler<T extends Context> extends StandardHandler<T> {
|
|
50
|
+
constructor(router: Router<any, T>, options: NoInfer<StandardOpenAPIHandlerOptions<T>>);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export { StandardOpenAPICodec as a, StandardOpenAPIHandler as c, StandardOpenAPIMatcher as e };
|
|
54
|
+
export type { StandardOpenAPICodecOptions as S, StandardOpenAPIHandlerOptions as b, StandardOpenAPIMatcherOptions as d };
|