@trpc/openapi 11.19.0-alpha → 11.21.1-canary.20.10e2ef98f
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/dist/cli.mjs +120 -26
- package/dist/index.cjs +122 -27
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +122 -27
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -9
- package/src/generate.ts +226 -40
package/src/generate.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
OperationObject,
|
|
13
13
|
PathItemObject,
|
|
14
14
|
PathsObject,
|
|
15
|
+
ResponseObject,
|
|
15
16
|
SchemaObject,
|
|
16
17
|
ServerObject,
|
|
17
18
|
} from './types';
|
|
@@ -21,6 +22,7 @@ interface ProcedureInfo {
|
|
|
21
22
|
type: 'query' | 'mutation' | 'subscription';
|
|
22
23
|
inputSchema: SchemaObject | null;
|
|
23
24
|
outputSchema: SchemaObject | null;
|
|
25
|
+
errorSchema: SchemaObject | null;
|
|
24
26
|
description?: string;
|
|
25
27
|
}
|
|
26
28
|
|
|
@@ -450,32 +452,87 @@ function convertUnionType(
|
|
|
450
452
|
schemas.push({ type: 'null' });
|
|
451
453
|
}
|
|
452
454
|
|
|
453
|
-
|
|
455
|
+
const distinctSchemas = dedupeSchemas(schemas, ctx.schemas);
|
|
456
|
+
if (distinctSchemas.length === 0) {
|
|
454
457
|
return {};
|
|
455
458
|
}
|
|
456
459
|
|
|
457
|
-
const [firstSchema] =
|
|
458
|
-
if (
|
|
460
|
+
const [firstSchema] = distinctSchemas;
|
|
461
|
+
if (distinctSchemas.length === 1 && firstSchema !== undefined) {
|
|
459
462
|
return firstSchema;
|
|
460
463
|
}
|
|
461
464
|
|
|
462
465
|
// When all schemas are simple type-only schemas (no other properties),
|
|
463
466
|
// collapse into a single `type` array. e.g. string | null → type: ["string", "null"]
|
|
464
|
-
if (
|
|
465
|
-
return { type:
|
|
467
|
+
if (distinctSchemas.every(isSimpleTypeSchema)) {
|
|
468
|
+
return { type: distinctSchemas.map((s) => s.type as string) };
|
|
466
469
|
}
|
|
467
470
|
|
|
468
471
|
// Detect discriminated unions: all oneOf members are objects sharing a common
|
|
469
472
|
// required property whose value is a `const`. If found, add a `discriminator`.
|
|
470
|
-
const discriminatorProp = detectDiscriminatorProperty(
|
|
473
|
+
const discriminatorProp = detectDiscriminatorProperty(distinctSchemas);
|
|
471
474
|
if (discriminatorProp) {
|
|
472
475
|
return {
|
|
473
|
-
oneOf:
|
|
476
|
+
oneOf: distinctSchemas,
|
|
474
477
|
discriminator: { propertyName: discriminatorProp },
|
|
475
478
|
};
|
|
476
479
|
}
|
|
477
480
|
|
|
478
|
-
return { oneOf:
|
|
481
|
+
return { oneOf: distinctSchemas };
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function dedupeSchemas(
|
|
485
|
+
schemas: SchemaObject[],
|
|
486
|
+
components: Record<string, SchemaObject>,
|
|
487
|
+
): SchemaObject[] {
|
|
488
|
+
const seen = new Set<string>();
|
|
489
|
+
|
|
490
|
+
return schemas.filter((schema) => {
|
|
491
|
+
const key = canonicalize(resolveBareRef(schema, components));
|
|
492
|
+
if (seen.has(key)) {
|
|
493
|
+
return false;
|
|
494
|
+
}
|
|
495
|
+
seen.add(key);
|
|
496
|
+
return true;
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* A `$ref` on its own describes exactly the component it points at, so compare
|
|
502
|
+
* through the component - otherwise the same shape reached both by reference
|
|
503
|
+
* and inline looks like two distinct schemas.
|
|
504
|
+
*/
|
|
505
|
+
function resolveBareRef(
|
|
506
|
+
schema: SchemaObject,
|
|
507
|
+
components: Record<string, SchemaObject>,
|
|
508
|
+
): SchemaObject {
|
|
509
|
+
if (!isBareRef(schema)) {
|
|
510
|
+
return schema;
|
|
511
|
+
}
|
|
512
|
+
const target = getReferencedSchema(schema, components);
|
|
513
|
+
// A component mid-conversion is still an empty placeholder (cyclic types);
|
|
514
|
+
// compare those by their `$ref` so distinct ones stay distinct.
|
|
515
|
+
return target && isNonEmptySchema(target) ? target : schema;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/** A `$ref` with no sibling keys, i.e. one that stands for its component. */
|
|
519
|
+
function isBareRef(schema: SchemaObject): boolean {
|
|
520
|
+
return schema.$ref !== undefined && Object.keys(schema).length === 1;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function canonicalize(value: unknown): string {
|
|
524
|
+
if (Array.isArray(value)) {
|
|
525
|
+
return `[${value.map(canonicalize).join(',')}]`;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (value && typeof value === 'object') {
|
|
529
|
+
const entries = Object.entries(value)
|
|
530
|
+
.filter(([, v]) => v !== undefined)
|
|
531
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
532
|
+
.map(([k, v]) => `${JSON.stringify(k)}:${canonicalize(v)}`);
|
|
533
|
+
return `{${entries.join(',')}}`;
|
|
534
|
+
}
|
|
535
|
+
return JSON.stringify(value) ?? 'undefined';
|
|
479
536
|
}
|
|
480
537
|
|
|
481
538
|
/**
|
|
@@ -872,6 +929,8 @@ interface WalkCtx {
|
|
|
872
929
|
schemaCtx: SchemaCtx;
|
|
873
930
|
/** Runtime descriptions keyed by procedure path (when a router instance is available). */
|
|
874
931
|
runtimeDescriptions: Map<string, RuntimeDescriptions>;
|
|
932
|
+
/** The router-wide error shape, used to spot procedures that differ from it. */
|
|
933
|
+
routerErrorType: ts.Type | null;
|
|
875
934
|
}
|
|
876
935
|
|
|
877
936
|
/**
|
|
@@ -948,7 +1007,12 @@ function getProcedureInputTypeName(type: ts.Type, path: string): string {
|
|
|
948
1007
|
}
|
|
949
1008
|
}
|
|
950
1009
|
|
|
951
|
-
|
|
1010
|
+
return `${pascalCasePath(path) || 'Procedure'}Input`;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
/** `user.getById` → `UserGetById`, for naming components after a procedure. */
|
|
1014
|
+
function pascalCasePath(path: string): string {
|
|
1015
|
+
return path
|
|
952
1016
|
.split('.')
|
|
953
1017
|
.filter(Boolean)
|
|
954
1018
|
.map((segment) =>
|
|
@@ -959,8 +1023,6 @@ function getProcedureInputTypeName(type: ts.Type, path: string): string {
|
|
|
959
1023
|
.join(''),
|
|
960
1024
|
)
|
|
961
1025
|
.join('');
|
|
962
|
-
|
|
963
|
-
return `${fallbackName || 'Procedure'}Input`;
|
|
964
1026
|
}
|
|
965
1027
|
|
|
966
1028
|
function isUnknownLikeType(type: ts.Type): boolean {
|
|
@@ -1142,10 +1204,64 @@ function extractProcedure(def: ProcedureDef, ctx: WalkCtx): void {
|
|
|
1142
1204
|
type: def.typeName as 'query' | 'mutation' | 'subscription',
|
|
1143
1205
|
inputSchema,
|
|
1144
1206
|
outputSchema,
|
|
1207
|
+
errorSchema: extractProcedureErrorSchema($typesType, ctx),
|
|
1145
1208
|
description: def.description,
|
|
1146
1209
|
});
|
|
1147
1210
|
}
|
|
1148
1211
|
|
|
1212
|
+
function extractProcedureErrorSchema(
|
|
1213
|
+
$typesType: ts.Type,
|
|
1214
|
+
ctx: WalkCtx,
|
|
1215
|
+
): SchemaObject | null {
|
|
1216
|
+
const { schemaCtx } = ctx;
|
|
1217
|
+
const errorSym = $typesType.getProperty('errorShape');
|
|
1218
|
+
if (!errorSym) {
|
|
1219
|
+
return null;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
const { checker } = schemaCtx;
|
|
1223
|
+
const errorType = checker.getTypeOfSymbol(errorSym);
|
|
1224
|
+
if (
|
|
1225
|
+
hasFlag(errorType, ts.TypeFlags.Any) ||
|
|
1226
|
+
isUnknownLikeType(errorType) ||
|
|
1227
|
+
addsNothingToRouterErrorType(errorType, ctx.routerErrorType, checker)
|
|
1228
|
+
) {
|
|
1229
|
+
return null;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
const schema = typeToJsonSchema(errorType, schemaCtx);
|
|
1233
|
+
return isNonEmptySchema(schema) ? schema : null;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
function addsNothingToRouterErrorType(
|
|
1237
|
+
errorType: ts.Type,
|
|
1238
|
+
routerErrorType: ts.Type | null,
|
|
1239
|
+
checker: ts.TypeChecker,
|
|
1240
|
+
): boolean {
|
|
1241
|
+
if (!routerErrorType) {
|
|
1242
|
+
return false;
|
|
1243
|
+
}
|
|
1244
|
+
if (errorType === routerErrorType) {
|
|
1245
|
+
return true;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
const describe = (type: ts.Type) =>
|
|
1249
|
+
checker.typeToString(
|
|
1250
|
+
type,
|
|
1251
|
+
undefined,
|
|
1252
|
+
ts.TypeFormatFlags.NoTruncation |
|
|
1253
|
+
ts.TypeFormatFlags.UseFullyQualifiedType,
|
|
1254
|
+
);
|
|
1255
|
+
const unionMembers = (type: ts.Type) =>
|
|
1256
|
+
type.isUnion() ? type.types : [type];
|
|
1257
|
+
|
|
1258
|
+
const routerMembers = new Set(unionMembers(routerErrorType).map(describe));
|
|
1259
|
+
|
|
1260
|
+
return unionMembers(errorType).every((member) =>
|
|
1261
|
+
routerMembers.has(describe(member)),
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1149
1265
|
/** Extract the JSDoc comment text from a symbol, if any. */
|
|
1150
1266
|
function getJsDocComment(
|
|
1151
1267
|
sym: ts.Symbol,
|
|
@@ -1337,11 +1453,10 @@ function loadCompilerOptions(startDir: string): ts.CompilerOptions {
|
|
|
1337
1453
|
* it to a JSON Schema. Returns `null` when the path cannot be resolved
|
|
1338
1454
|
* (e.g. older tRPC versions or missing type info).
|
|
1339
1455
|
*/
|
|
1340
|
-
function
|
|
1456
|
+
function getRouterErrorType(
|
|
1341
1457
|
routerType: ts.Type,
|
|
1342
1458
|
checker: ts.TypeChecker,
|
|
1343
|
-
|
|
1344
|
-
): SchemaObject | null {
|
|
1459
|
+
): ts.Type | null {
|
|
1345
1460
|
const walk = (type: ts.Type, keys: string[]): ts.Type | null => {
|
|
1346
1461
|
const [head, ...rest] = keys;
|
|
1347
1462
|
if (!head) {
|
|
@@ -1360,11 +1475,20 @@ function extractErrorSchema(
|
|
|
1360
1475
|
'$types',
|
|
1361
1476
|
'errorShape',
|
|
1362
1477
|
]);
|
|
1363
|
-
if (!errorShapeType) {
|
|
1478
|
+
if (!errorShapeType || hasFlag(errorShapeType, ts.TypeFlags.Any)) {
|
|
1364
1479
|
return null;
|
|
1365
1480
|
}
|
|
1366
1481
|
|
|
1367
|
-
|
|
1482
|
+
return errorShapeType;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
function extractErrorSchema(
|
|
1486
|
+
routerType: ts.Type,
|
|
1487
|
+
checker: ts.TypeChecker,
|
|
1488
|
+
schemaCtx: SchemaCtx,
|
|
1489
|
+
): SchemaObject | null {
|
|
1490
|
+
const errorShapeType = getRouterErrorType(routerType, checker);
|
|
1491
|
+
if (!errorShapeType) {
|
|
1368
1492
|
return null;
|
|
1369
1493
|
}
|
|
1370
1494
|
|
|
@@ -1375,6 +1499,14 @@ function extractErrorSchema(
|
|
|
1375
1499
|
// OpenAPI document builder
|
|
1376
1500
|
// ---------------------------------------------------------------------------
|
|
1377
1501
|
|
|
1502
|
+
function wrapInErrorEnvelope(errorSchema: SchemaObject): SchemaObject {
|
|
1503
|
+
return {
|
|
1504
|
+
type: 'object',
|
|
1505
|
+
properties: { error: errorSchema },
|
|
1506
|
+
required: ['error'],
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1378
1510
|
/** Fallback error schema when the router type doesn't expose an error shape. */
|
|
1379
1511
|
const DEFAULT_ERROR_SCHEMA: SchemaObject = {
|
|
1380
1512
|
type: 'object',
|
|
@@ -1418,6 +1550,7 @@ function wrapInSuccessEnvelope(
|
|
|
1418
1550
|
function buildProcedureOperation(
|
|
1419
1551
|
proc: ProcedureInfo,
|
|
1420
1552
|
method: 'get' | 'post',
|
|
1553
|
+
errorResponseRef: string,
|
|
1421
1554
|
): OperationObject {
|
|
1422
1555
|
const [tag = proc.path] = proc.path.split('.');
|
|
1423
1556
|
const operation: OperationObject = {
|
|
@@ -1433,7 +1566,7 @@ function buildProcedureOperation(
|
|
|
1433
1566
|
},
|
|
1434
1567
|
},
|
|
1435
1568
|
},
|
|
1436
|
-
default: { $ref:
|
|
1569
|
+
default: { $ref: errorResponseRef },
|
|
1437
1570
|
},
|
|
1438
1571
|
};
|
|
1439
1572
|
|
|
@@ -1463,18 +1596,85 @@ function buildProcedureOperation(
|
|
|
1463
1596
|
return operation;
|
|
1464
1597
|
}
|
|
1465
1598
|
|
|
1599
|
+
function buildErrorResponse(schema: SchemaObject): ResponseObject {
|
|
1600
|
+
return {
|
|
1601
|
+
description: 'Error response',
|
|
1602
|
+
content: {
|
|
1603
|
+
'application/json': { schema: wrapInErrorEnvelope(schema) },
|
|
1604
|
+
},
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
function buildErrorResponses(
|
|
1609
|
+
procedures: ProcedureInfo[],
|
|
1610
|
+
routerErrorSchema: SchemaObject,
|
|
1611
|
+
schemas: Record<string, SchemaObject>,
|
|
1612
|
+
): {
|
|
1613
|
+
responses: Record<string, ResponseObject>;
|
|
1614
|
+
refByProcedurePath: Map<string, string>;
|
|
1615
|
+
} {
|
|
1616
|
+
const responses: Record<string, ResponseObject> = {
|
|
1617
|
+
Error: buildErrorResponse(routerErrorSchema),
|
|
1618
|
+
};
|
|
1619
|
+
const shapeKey = (schema: SchemaObject) =>
|
|
1620
|
+
canonicalize(resolveBareRef(schema, schemas));
|
|
1621
|
+
const refByShape = new Map<string, string>([
|
|
1622
|
+
[shapeKey(routerErrorSchema), '#/components/responses/Error'],
|
|
1623
|
+
]);
|
|
1624
|
+
const refByProcedurePath = new Map<string, string>();
|
|
1625
|
+
|
|
1626
|
+
for (const proc of procedures) {
|
|
1627
|
+
if (!proc.errorSchema) {
|
|
1628
|
+
continue;
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
const key = shapeKey(proc.errorSchema);
|
|
1632
|
+
let ref = refByShape.get(key);
|
|
1633
|
+
if (!ref) {
|
|
1634
|
+
// A shape that already is a component needs no second name for it.
|
|
1635
|
+
const name = `${pascalCasePath(proc.path) || 'Procedure'}Error`;
|
|
1636
|
+
const shape = isBareRef(proc.errorSchema)
|
|
1637
|
+
? proc.errorSchema
|
|
1638
|
+
: registerSchema(`${name}Shape`, proc.errorSchema, schemas);
|
|
1639
|
+
const responseName = ensureUniqueName(name, responses);
|
|
1640
|
+
responses[responseName] = buildErrorResponse(shape);
|
|
1641
|
+
ref = `#/components/responses/${responseName}`;
|
|
1642
|
+
refByShape.set(key, ref);
|
|
1643
|
+
}
|
|
1644
|
+
refByProcedurePath.set(proc.path, ref);
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
return { responses, refByProcedurePath };
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
function registerSchema(
|
|
1651
|
+
name: string,
|
|
1652
|
+
schema: SchemaObject,
|
|
1653
|
+
schemas: Record<string, SchemaObject>,
|
|
1654
|
+
): SchemaObject {
|
|
1655
|
+
const refName = ensureUniqueName(name, schemas);
|
|
1656
|
+
schemas[refName] = schema;
|
|
1657
|
+
return { $ref: `#/components/schemas/${refName}` };
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1466
1660
|
function buildOpenAPIDocument(
|
|
1467
1661
|
procedures: ProcedureInfo[],
|
|
1468
1662
|
options: GenerateOptions,
|
|
1469
1663
|
meta: RouterMeta = { errorSchema: null },
|
|
1470
1664
|
): Document {
|
|
1471
1665
|
const paths: PathsObject = {};
|
|
1666
|
+
const included = procedures.filter((proc) =>
|
|
1667
|
+
shouldIncludeProcedureInOpenAPI(proc.type),
|
|
1668
|
+
);
|
|
1472
1669
|
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1670
|
+
const schemas = meta.schemas ?? {};
|
|
1671
|
+
const { responses: errorResponses, refByProcedurePath } = buildErrorResponses(
|
|
1672
|
+
included,
|
|
1673
|
+
meta.errorSchema ?? DEFAULT_ERROR_SCHEMA,
|
|
1674
|
+
schemas,
|
|
1675
|
+
);
|
|
1477
1676
|
|
|
1677
|
+
for (const proc of included) {
|
|
1478
1678
|
const opPath = `/${proc.path}`;
|
|
1479
1679
|
const method = proc.type === 'query' ? 'get' : 'post';
|
|
1480
1680
|
|
|
@@ -1483,11 +1683,11 @@ function buildOpenAPIDocument(
|
|
|
1483
1683
|
pathItem[method] = buildProcedureOperation(
|
|
1484
1684
|
proc,
|
|
1485
1685
|
method,
|
|
1686
|
+
refByProcedurePath.get(proc.path) ?? '#/components/responses/Error',
|
|
1486
1687
|
) as PathItemObject[typeof method];
|
|
1487
1688
|
}
|
|
1488
1689
|
|
|
1489
|
-
const hasNamedSchemas =
|
|
1490
|
-
meta.schemas !== undefined && Object.keys(meta.schemas).length > 0;
|
|
1690
|
+
const hasNamedSchemas = Object.keys(schemas).length > 0;
|
|
1491
1691
|
|
|
1492
1692
|
return {
|
|
1493
1693
|
openapi: '3.1.1',
|
|
@@ -1499,23 +1699,8 @@ function buildOpenAPIDocument(
|
|
|
1499
1699
|
...(options.servers?.length ? { servers: options.servers } : {}),
|
|
1500
1700
|
paths,
|
|
1501
1701
|
components: {
|
|
1502
|
-
...(hasNamedSchemas
|
|
1503
|
-
responses:
|
|
1504
|
-
Error: {
|
|
1505
|
-
description: 'Error response',
|
|
1506
|
-
content: {
|
|
1507
|
-
'application/json': {
|
|
1508
|
-
schema: {
|
|
1509
|
-
type: 'object',
|
|
1510
|
-
properties: {
|
|
1511
|
-
error: meta.errorSchema ?? DEFAULT_ERROR_SCHEMA,
|
|
1512
|
-
},
|
|
1513
|
-
required: ['error'],
|
|
1514
|
-
},
|
|
1515
|
-
},
|
|
1516
|
-
},
|
|
1517
|
-
},
|
|
1518
|
-
},
|
|
1702
|
+
...(hasNamedSchemas ? { schemas } : {}),
|
|
1703
|
+
responses: errorResponses,
|
|
1519
1704
|
},
|
|
1520
1705
|
};
|
|
1521
1706
|
}
|
|
@@ -1595,6 +1780,7 @@ export async function generateOpenAPIDocument(
|
|
|
1595
1780
|
seen: new Set(),
|
|
1596
1781
|
schemaCtx,
|
|
1597
1782
|
runtimeDescriptions,
|
|
1783
|
+
routerErrorType: getRouterErrorType(routerType, checker),
|
|
1598
1784
|
};
|
|
1599
1785
|
walkType({ type: routerType, ctx: walkCtx, currentPath: '' });
|
|
1600
1786
|
|