next-openapi-gen 0.2.2 → 0.3.2
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 +6 -1
- package/dist/lib/route-processor.js +43 -10
- package/dist/lib/schema-processor.js +280 -47
- package/dist/lib/utils.js +44 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
## Interfaces
|
|
11
11
|
|
|
12
|
-
- Scalar
|
|
12
|
+
- Scalar 🆕
|
|
13
13
|
- Swagger
|
|
14
14
|
- Redoc
|
|
15
15
|
- Stoplight Elements
|
|
@@ -89,6 +89,7 @@ Annotate your API routes using JSDoc comments. Here's an example:
|
|
|
89
89
|
</td>
|
|
90
90
|
<td>
|
|
91
91
|
<img width="340" alt="api-login-scalar" src="https://raw.githubusercontent.com/tazo90/next-openapi-gen/refs/heads/main/assets/api-login-scalar.png" alt-text="api-login"/>
|
|
92
|
+
<br/><br/><br/>
|
|
92
93
|
<img width="340" alt="api-login-swagger" src="https://raw.githubusercontent.com/tazo90/next-openapi-gen/refs/heads/main/assets/api-login-swagger.png" alt-text="api-login-swagger"/>
|
|
93
94
|
</td>
|
|
94
95
|
</tr>
|
|
@@ -148,6 +149,7 @@ Annotate your API routes using JSDoc comments. Here's an example:
|
|
|
148
149
|
</td>
|
|
149
150
|
<td>
|
|
150
151
|
<img width="340" alt="api-users-scalar" src="https://raw.githubusercontent.com/tazo90/next-openapi-gen/refs/heads/main/assets/api-users-scalar.png" alt-text="api-users-scalar"/>
|
|
152
|
+
<br/><br/><br/>
|
|
151
153
|
<img width="340" alt="api-users-swagger" src="https://raw.githubusercontent.com/tazo90/next-openapi-gen/refs/heads/main/assets/api-users-swagger.png" alt-text="api-users-swagger"/>
|
|
152
154
|
</td>
|
|
153
155
|
</tr>
|
|
@@ -201,6 +203,9 @@ The `next.openapi.json` file allows you to configure the behavior of the OpenAPI
|
|
|
201
203
|
</thead>
|
|
202
204
|
<tbody>
|
|
203
205
|
<tr>
|
|
206
|
+
<td>
|
|
207
|
+
<img width="320" alt="scalar" src="https://raw.githubusercontent.com/tazo90/next-openapi-gen/refs/heads/main/assets/scalar.png" alt-text="scalar">
|
|
208
|
+
</td>
|
|
204
209
|
<td>
|
|
205
210
|
<img width="320" alt="swagger" src="https://raw.githubusercontent.com/tazo90/next-openapi-gen/refs/heads/main/assets/swagger.png" alt-text="swagger">
|
|
206
211
|
</td>
|
|
@@ -4,7 +4,7 @@ import path from "path";
|
|
|
4
4
|
import traverse from "@babel/traverse";
|
|
5
5
|
import { parse } from "@babel/parser";
|
|
6
6
|
import { SchemaProcessor } from "./schema-processor.js";
|
|
7
|
-
import { capitalize, extractJSDocComments, getOperationId } from "./utils.js";
|
|
7
|
+
import { capitalize, extractJSDocComments, extractPathParameters, getOperationId, } from "./utils.js";
|
|
8
8
|
const HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
9
9
|
const MUTATION_HTTP_METHODS = ["PATCH", "POST", "PUT"];
|
|
10
10
|
export class RouteProcessor {
|
|
@@ -38,7 +38,8 @@ export class RouteProcessor {
|
|
|
38
38
|
const dataTypes = extractJSDocComments(path);
|
|
39
39
|
if (this.isRoute(declaration.id.name)) {
|
|
40
40
|
// Don't bother adding routes for processing if only including OpenAPI routes and the route is not OpenAPI
|
|
41
|
-
if (!this.config.includeOpenApiRoutes ||
|
|
41
|
+
if (!this.config.includeOpenApiRoutes ||
|
|
42
|
+
(this.config.includeOpenApiRoutes && dataTypes.isOpenApi))
|
|
42
43
|
this.addRouteToPaths(declaration.id.name, filePath, dataTypes);
|
|
43
44
|
}
|
|
44
45
|
}
|
|
@@ -48,7 +49,8 @@ export class RouteProcessor {
|
|
|
48
49
|
if (this.isRoute(decl.id.name)) {
|
|
49
50
|
const dataTypes = extractJSDocComments(path);
|
|
50
51
|
// Don't bother adding routes for processing if only including OpenAPI routes and the route is not OpenAPI
|
|
51
|
-
if (!this.config.includeOpenApiRoutes ||
|
|
52
|
+
if (!this.config.includeOpenApiRoutes ||
|
|
53
|
+
(this.config.includeOpenApiRoutes && dataTypes.isOpenApi))
|
|
52
54
|
this.addRouteToPaths(decl.id.name, filePath, dataTypes);
|
|
53
55
|
}
|
|
54
56
|
}
|
|
@@ -75,8 +77,10 @@ export class RouteProcessor {
|
|
|
75
77
|
this.scanApiRoutes(filePath);
|
|
76
78
|
// @ts-ignore
|
|
77
79
|
}
|
|
78
|
-
else if (file.endsWith(".ts")) {
|
|
79
|
-
|
|
80
|
+
else if (file.endsWith(".ts") || file.endsWith(".tsx")) {
|
|
81
|
+
if (file === "route.ts" || file === "route.tsx") {
|
|
82
|
+
this.processFile(filePath);
|
|
83
|
+
}
|
|
80
84
|
}
|
|
81
85
|
});
|
|
82
86
|
}
|
|
@@ -113,7 +117,21 @@ export class RouteProcessor {
|
|
|
113
117
|
definition.parameters =
|
|
114
118
|
this.schemaProcessor.createRequestParamsSchema(params);
|
|
115
119
|
}
|
|
116
|
-
|
|
120
|
+
// Add path parameters
|
|
121
|
+
const pathParamNames = extractPathParameters(routePath);
|
|
122
|
+
if (pathParamNames.length > 0) {
|
|
123
|
+
// If we have path parameters but no schema, create a default schema
|
|
124
|
+
if (!pathParams) {
|
|
125
|
+
const defaultPathParams = this.schemaProcessor.createDefaultPathParamsSchema(pathParamNames);
|
|
126
|
+
definition.parameters.push(...defaultPathParams);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
const moreParams = this.schemaProcessor.createRequestParamsSchema(pathParams, true);
|
|
130
|
+
definition.parameters.push(...moreParams);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
else if (pathParams) {
|
|
134
|
+
// If no path parameters in route but we have a schema, use it
|
|
117
135
|
const moreParams = this.schemaProcessor.createRequestParamsSchema(pathParams, true);
|
|
118
136
|
definition.parameters.push(...moreParams);
|
|
119
137
|
}
|
|
@@ -129,14 +147,29 @@ export class RouteProcessor {
|
|
|
129
147
|
this.swaggerPaths[routePath][method] = definition;
|
|
130
148
|
}
|
|
131
149
|
getRoutePath(filePath) {
|
|
150
|
+
// First, check if it's an app router path
|
|
151
|
+
if (filePath.includes("/app/api/")) {
|
|
152
|
+
// Get the relative path from the api directory
|
|
153
|
+
const apiDirPos = filePath.indexOf("/app/api/");
|
|
154
|
+
let relativePath = filePath.substring(apiDirPos + "/app/api".length);
|
|
155
|
+
// Remove the /route.ts or /route.tsx suffix
|
|
156
|
+
relativePath = relativePath.replace(/\/route\.tsx?$/, "");
|
|
157
|
+
// Convert directory separators to URL path format
|
|
158
|
+
relativePath = relativePath.replaceAll("\\", "/");
|
|
159
|
+
// Convert Next.js dynamic route syntax to OpenAPI parameter syntax
|
|
160
|
+
relativePath = relativePath.replace(/\/\[([^\]]+)\]/g, "/{$1}");
|
|
161
|
+
// Handle catch-all routes ([...param])
|
|
162
|
+
relativePath = relativePath.replace(/\/\[\.\.\.(.*)\]/g, "/{$1}");
|
|
163
|
+
return relativePath;
|
|
164
|
+
}
|
|
165
|
+
// For pages router or other formats
|
|
132
166
|
const suffixPath = filePath.split("api")[1];
|
|
133
167
|
return suffixPath
|
|
134
|
-
.replace(
|
|
168
|
+
.replace(/route\.tsx?$/, "")
|
|
135
169
|
.replaceAll("\\", "/")
|
|
136
170
|
.replace(/\/$/, "")
|
|
137
|
-
|
|
138
|
-
.
|
|
139
|
-
.replaceAll("]", "}");
|
|
171
|
+
.replace(/\/\[([^\]]+)\]/g, "/{$1}") // Replace [param] with {param}
|
|
172
|
+
.replace(/\/\[\.\.\.(.*)\]/g, "/{$1}"); // Replace [...param] with {param}
|
|
140
173
|
}
|
|
141
174
|
getSortedPaths(paths) {
|
|
142
175
|
function comparePaths(a, b) {
|
|
@@ -11,6 +11,7 @@ export class SchemaProcessor {
|
|
|
11
11
|
directoryCache = {};
|
|
12
12
|
statCache = {};
|
|
13
13
|
processSchemaTracker = {};
|
|
14
|
+
processingTypes = new Set();
|
|
14
15
|
constructor(schemaDir) {
|
|
15
16
|
this.schemaDir = path.resolve(schemaDir);
|
|
16
17
|
}
|
|
@@ -71,37 +72,49 @@ export class SchemaProcessor {
|
|
|
71
72
|
});
|
|
72
73
|
}
|
|
73
74
|
resolveType(typeName) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
return {};
|
|
77
|
-
if (t.isTSEnumDeclaration(typeNode)) {
|
|
78
|
-
const enumValues = this.processEnum(typeNode);
|
|
79
|
-
return enumValues;
|
|
75
|
+
if (this.processingTypes.has(typeName)) {
|
|
76
|
+
// Return reference to type to avoid infinite recursion
|
|
77
|
+
return { $ref: `#/components/schemas/${typeName}` };
|
|
80
78
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
...options,
|
|
91
|
-
};
|
|
92
|
-
properties[propName] = property;
|
|
93
|
-
}
|
|
94
|
-
});
|
|
79
|
+
// Add type to precessing types
|
|
80
|
+
this.processingTypes.add(typeName);
|
|
81
|
+
try {
|
|
82
|
+
const typeNode = this.typeDefinitions[typeName.toString()];
|
|
83
|
+
if (!typeNode)
|
|
84
|
+
return {};
|
|
85
|
+
if (t.isTSEnumDeclaration(typeNode)) {
|
|
86
|
+
const enumValues = this.processEnum(typeNode);
|
|
87
|
+
return enumValues;
|
|
95
88
|
}
|
|
96
|
-
|
|
89
|
+
if (t.isTSTypeLiteral(typeNode) || t.isTSInterfaceBody(typeNode)) {
|
|
90
|
+
const properties = {};
|
|
91
|
+
if ("members" in typeNode) {
|
|
92
|
+
(typeNode.members || []).forEach((member) => {
|
|
93
|
+
if (t.isTSPropertySignature(member) && t.isIdentifier(member.key)) {
|
|
94
|
+
const propName = member.key.name;
|
|
95
|
+
const options = this.getPropertyOptions(member);
|
|
96
|
+
const property = {
|
|
97
|
+
...this.resolveTSNodeType(member.typeAnnotation?.typeAnnotation),
|
|
98
|
+
...options,
|
|
99
|
+
};
|
|
100
|
+
properties[propName] = property;
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return { type: "object", properties };
|
|
105
|
+
}
|
|
106
|
+
if (t.isTSArrayType(typeNode)) {
|
|
107
|
+
return {
|
|
108
|
+
type: "array",
|
|
109
|
+
items: this.resolveTSNodeType(typeNode.elementType),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
return {};
|
|
97
113
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
items: this.resolveTSNodeType(typeNode.elementType),
|
|
102
|
-
};
|
|
114
|
+
finally {
|
|
115
|
+
// Remove type from processed set after we finish
|
|
116
|
+
this.processingTypes.delete(typeName);
|
|
103
117
|
}
|
|
104
|
-
return {};
|
|
105
118
|
}
|
|
106
119
|
isDateString(node) {
|
|
107
120
|
if (t.isStringLiteral(node)) {
|
|
@@ -111,22 +124,91 @@ export class SchemaProcessor {
|
|
|
111
124
|
return false;
|
|
112
125
|
}
|
|
113
126
|
isDateObject(node) {
|
|
114
|
-
return t.isNewExpression(node) && t.isIdentifier(node.callee, { name: "Date" });
|
|
127
|
+
return (t.isNewExpression(node) && t.isIdentifier(node.callee, { name: "Date" }));
|
|
115
128
|
}
|
|
116
129
|
isDateNode(node) {
|
|
117
130
|
return this.isDateString(node) || this.isDateObject(node);
|
|
118
131
|
}
|
|
119
132
|
resolveTSNodeType(node) {
|
|
133
|
+
if (!node)
|
|
134
|
+
return { type: "object" }; // Default type for undefined/null
|
|
120
135
|
if (t.isTSStringKeyword(node))
|
|
121
136
|
return { type: "string" };
|
|
122
137
|
if (t.isTSNumberKeyword(node))
|
|
123
138
|
return { type: "number" };
|
|
124
139
|
if (t.isTSBooleanKeyword(node))
|
|
125
140
|
return { type: "boolean" };
|
|
141
|
+
if (t.isTSAnyKeyword(node) || t.isTSUnknownKeyword(node))
|
|
142
|
+
return { type: "object" };
|
|
143
|
+
if (t.isTSVoidKeyword(node) ||
|
|
144
|
+
t.isTSNullKeyword(node) ||
|
|
145
|
+
t.isTSUndefinedKeyword(node))
|
|
146
|
+
return { type: "null" };
|
|
126
147
|
if (this.isDateNode(node))
|
|
127
|
-
return { type: "
|
|
148
|
+
return { type: "string", format: "date-time" };
|
|
149
|
+
// Handle literal types like "admin" | "member" | "guest"
|
|
150
|
+
if (t.isTSLiteralType(node)) {
|
|
151
|
+
if (t.isStringLiteral(node.literal)) {
|
|
152
|
+
return {
|
|
153
|
+
type: "string",
|
|
154
|
+
enum: [node.literal.value],
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
else if (t.isNumericLiteral(node.literal)) {
|
|
158
|
+
return {
|
|
159
|
+
type: "number",
|
|
160
|
+
enum: [node.literal.value],
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
else if (t.isBooleanLiteral(node.literal)) {
|
|
164
|
+
return {
|
|
165
|
+
type: "boolean",
|
|
166
|
+
enum: [node.literal.value],
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
128
170
|
if (t.isTSTypeReference(node) && t.isIdentifier(node.typeName)) {
|
|
129
171
|
const typeName = node.typeName.name;
|
|
172
|
+
// Special handling for built-in types
|
|
173
|
+
if (typeName === "Date") {
|
|
174
|
+
return { type: "string", format: "date-time" };
|
|
175
|
+
}
|
|
176
|
+
if (typeName === "Array" || typeName === "ReadonlyArray") {
|
|
177
|
+
if (node.typeParameters && node.typeParameters.params.length > 0) {
|
|
178
|
+
return {
|
|
179
|
+
type: "array",
|
|
180
|
+
items: this.resolveTSNodeType(node.typeParameters.params[0]),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
return { type: "array", items: { type: "object" } };
|
|
184
|
+
}
|
|
185
|
+
if (typeName === "Record") {
|
|
186
|
+
if (node.typeParameters && node.typeParameters.params.length > 1) {
|
|
187
|
+
const keyType = this.resolveTSNodeType(node.typeParameters.params[0]);
|
|
188
|
+
const valueType = this.resolveTSNodeType(node.typeParameters.params[1]);
|
|
189
|
+
return {
|
|
190
|
+
type: "object",
|
|
191
|
+
additionalProperties: valueType,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
return { type: "object", additionalProperties: true };
|
|
195
|
+
}
|
|
196
|
+
if (typeName === "Partial" ||
|
|
197
|
+
typeName === "Required" ||
|
|
198
|
+
typeName === "Readonly") {
|
|
199
|
+
if (node.typeParameters && node.typeParameters.params.length > 0) {
|
|
200
|
+
return this.resolveTSNodeType(node.typeParameters.params[0]);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (typeName === "Pick" || typeName === "Omit") {
|
|
204
|
+
if (node.typeParameters && node.typeParameters.params.length > 0) {
|
|
205
|
+
return this.resolveTSNodeType(node.typeParameters.params[0]);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// Check if it is a type that we are already processing
|
|
209
|
+
if (this.processingTypes.has(typeName)) {
|
|
210
|
+
return { $ref: `#/components/schemas/${typeName}` };
|
|
211
|
+
}
|
|
130
212
|
// Find type definition
|
|
131
213
|
this.findSchemaDefinition(typeName, this.contentType);
|
|
132
214
|
return this.resolveType(node.typeName.name);
|
|
@@ -148,32 +230,115 @@ export class SchemaProcessor {
|
|
|
148
230
|
return { type: "object", properties };
|
|
149
231
|
}
|
|
150
232
|
if (t.isTSUnionType(node)) {
|
|
233
|
+
// Handle union types with literal types, like "admin" | "member" | "guest"
|
|
234
|
+
const literals = node.types.filter((type) => t.isTSLiteralType(type));
|
|
235
|
+
// Check if all union elements are literals
|
|
236
|
+
if (literals.length === node.types.length) {
|
|
237
|
+
// All union members are literals, convert to enum
|
|
238
|
+
const enumValues = literals
|
|
239
|
+
.map((type) => {
|
|
240
|
+
if (t.isTSLiteralType(type) && t.isStringLiteral(type.literal)) {
|
|
241
|
+
return type.literal.value;
|
|
242
|
+
}
|
|
243
|
+
else if (t.isTSLiteralType(type) &&
|
|
244
|
+
t.isNumericLiteral(type.literal)) {
|
|
245
|
+
return type.literal.value;
|
|
246
|
+
}
|
|
247
|
+
else if (t.isTSLiteralType(type) &&
|
|
248
|
+
t.isBooleanLiteral(type.literal)) {
|
|
249
|
+
return type.literal.value;
|
|
250
|
+
}
|
|
251
|
+
return null;
|
|
252
|
+
})
|
|
253
|
+
.filter((value) => value !== null);
|
|
254
|
+
if (enumValues.length > 0) {
|
|
255
|
+
// Check if all enum values are of the same type
|
|
256
|
+
const firstType = typeof enumValues[0];
|
|
257
|
+
const sameType = enumValues.every((val) => typeof val === firstType);
|
|
258
|
+
if (sameType) {
|
|
259
|
+
return {
|
|
260
|
+
type: firstType,
|
|
261
|
+
enum: enumValues,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
// Handling null | undefined in type union
|
|
267
|
+
const nullableTypes = node.types.filter((type) => t.isTSNullKeyword(type) ||
|
|
268
|
+
t.isTSUndefinedKeyword(type) ||
|
|
269
|
+
t.isTSVoidKeyword(type));
|
|
270
|
+
const nonNullableTypes = node.types.filter((type) => !t.isTSNullKeyword(type) &&
|
|
271
|
+
!t.isTSUndefinedKeyword(type) &&
|
|
272
|
+
!t.isTSVoidKeyword(type));
|
|
273
|
+
// If a type can be null/undefined, we mark it as nullable
|
|
274
|
+
if (nullableTypes.length > 0 && nonNullableTypes.length === 1) {
|
|
275
|
+
const mainType = this.resolveTSNodeType(nonNullableTypes[0]);
|
|
276
|
+
return {
|
|
277
|
+
...mainType,
|
|
278
|
+
nullable: true,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
// Standard union type support via oneOf
|
|
282
|
+
return {
|
|
283
|
+
oneOf: node.types
|
|
284
|
+
.filter((type) => !t.isTSNullKeyword(type) &&
|
|
285
|
+
!t.isTSUndefinedKeyword(type) &&
|
|
286
|
+
!t.isTSVoidKeyword(type))
|
|
287
|
+
.map((subNode) => this.resolveTSNodeType(subNode)),
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
if (t.isTSIntersectionType(node)) {
|
|
291
|
+
// For intersection types, we combine properties
|
|
292
|
+
const allProperties = {};
|
|
293
|
+
const requiredProperties = [];
|
|
294
|
+
node.types.forEach((typeNode) => {
|
|
295
|
+
const resolvedType = this.resolveTSNodeType(typeNode);
|
|
296
|
+
if (resolvedType.type === "object" && resolvedType.properties) {
|
|
297
|
+
Object.entries(resolvedType.properties).forEach(([key, value]) => {
|
|
298
|
+
allProperties[key] = value;
|
|
299
|
+
// @ts-ignore
|
|
300
|
+
if (value.required) {
|
|
301
|
+
requiredProperties.push(key);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
});
|
|
151
306
|
return {
|
|
152
|
-
|
|
307
|
+
type: "object",
|
|
308
|
+
properties: allProperties,
|
|
309
|
+
required: requiredProperties.length > 0 ? requiredProperties : undefined,
|
|
153
310
|
};
|
|
154
311
|
}
|
|
155
|
-
//
|
|
312
|
+
// Case where a type is a reference to another defined type
|
|
156
313
|
if (t.isTSTypeReference(node) && t.isIdentifier(node.typeName)) {
|
|
157
314
|
return { $ref: `#/components/schemas/${node.typeName.name}` };
|
|
158
315
|
}
|
|
159
316
|
console.warn("Unrecognized TypeScript type node:", node);
|
|
160
|
-
return {};
|
|
317
|
+
return { type: "object" }; // By default we return an object
|
|
161
318
|
}
|
|
162
319
|
processSchemaFile(filePath, schemaName) {
|
|
163
320
|
// Check if the file has already been processed
|
|
164
321
|
if (this.processSchemaTracker[`${filePath}-${schemaName}`])
|
|
165
322
|
return;
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
323
|
+
try {
|
|
324
|
+
// Recognizes different elements of TS like variable, type, interface, enum
|
|
325
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
326
|
+
const ast = parse(content, {
|
|
327
|
+
sourceType: "module",
|
|
328
|
+
plugins: ["typescript", "decorators-legacy"],
|
|
329
|
+
});
|
|
330
|
+
this.collectTypeDefinitions(ast, schemaName);
|
|
331
|
+
// Reset the set of processed types before each schema processing
|
|
332
|
+
this.processingTypes.clear();
|
|
333
|
+
const definition = this.resolveType(schemaName);
|
|
334
|
+
this.openapiDefinitions[schemaName] = definition;
|
|
335
|
+
this.processSchemaTracker[`${filePath}-${schemaName}`] = true;
|
|
336
|
+
return definition;
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
console.error(`Error processing schema file ${filePath} for schema ${schemaName}:`, error);
|
|
340
|
+
return { type: "object" }; // By default we return an empty object on error
|
|
341
|
+
}
|
|
177
342
|
}
|
|
178
343
|
processEnum(enumNode) {
|
|
179
344
|
// Initialization OpenAPI enum object
|
|
@@ -199,9 +364,7 @@ export class SchemaProcessor {
|
|
|
199
364
|
return enumSchema;
|
|
200
365
|
}
|
|
201
366
|
getPropertyOptions(node) {
|
|
202
|
-
const key = node.key.name;
|
|
203
367
|
const isOptional = !!node.optional; // check if property is optional
|
|
204
|
-
const typeName = node.typeAnnotation?.typeAnnotation?.typeName?.name;
|
|
205
368
|
let description = null;
|
|
206
369
|
// get comments for field
|
|
207
370
|
if (node.trailingComments && node.trailingComments.length) {
|
|
@@ -219,6 +382,71 @@ export class SchemaProcessor {
|
|
|
219
382
|
}
|
|
220
383
|
return options;
|
|
221
384
|
}
|
|
385
|
+
/**
|
|
386
|
+
* Generate example values based on parameter type and name
|
|
387
|
+
*/
|
|
388
|
+
getExampleForParam(paramName, type = "string") {
|
|
389
|
+
// Common ID-like parameters
|
|
390
|
+
if (paramName === "id" ||
|
|
391
|
+
paramName.endsWith("Id") ||
|
|
392
|
+
paramName.endsWith("_id")) {
|
|
393
|
+
return type === "string" ? "123abc" : 123;
|
|
394
|
+
}
|
|
395
|
+
// For specific common parameter names
|
|
396
|
+
switch (paramName.toLowerCase()) {
|
|
397
|
+
case "slug":
|
|
398
|
+
return "example-slug";
|
|
399
|
+
case "uuid":
|
|
400
|
+
return "123e4567-e89b-12d3-a456-426614174000";
|
|
401
|
+
case "username":
|
|
402
|
+
return "johndoe";
|
|
403
|
+
case "email":
|
|
404
|
+
return "user@example.com";
|
|
405
|
+
case "name":
|
|
406
|
+
return "example-name";
|
|
407
|
+
case "date":
|
|
408
|
+
return "2023-01-01";
|
|
409
|
+
case "page":
|
|
410
|
+
return 1;
|
|
411
|
+
default:
|
|
412
|
+
// Default examples by type
|
|
413
|
+
if (type === "string")
|
|
414
|
+
return "example";
|
|
415
|
+
if (type === "number")
|
|
416
|
+
return 1;
|
|
417
|
+
if (type === "boolean")
|
|
418
|
+
return true;
|
|
419
|
+
return "example";
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Create a default schema for path parameters when no schema is defined
|
|
424
|
+
*/
|
|
425
|
+
createDefaultPathParamsSchema(paramNames) {
|
|
426
|
+
return paramNames.map((paramName) => {
|
|
427
|
+
// Guess the parameter type based on the name
|
|
428
|
+
let type = "string";
|
|
429
|
+
if (paramName === "id" ||
|
|
430
|
+
paramName.endsWith("Id") ||
|
|
431
|
+
paramName === "page" ||
|
|
432
|
+
paramName === "limit" ||
|
|
433
|
+
paramName === "size" ||
|
|
434
|
+
paramName === "count") {
|
|
435
|
+
type = "number";
|
|
436
|
+
}
|
|
437
|
+
const example = this.getExampleForParam(paramName, type);
|
|
438
|
+
return {
|
|
439
|
+
name: paramName,
|
|
440
|
+
in: "path",
|
|
441
|
+
required: true,
|
|
442
|
+
schema: {
|
|
443
|
+
type: type,
|
|
444
|
+
},
|
|
445
|
+
example: example,
|
|
446
|
+
description: `Path parameter: ${paramName}`,
|
|
447
|
+
};
|
|
448
|
+
});
|
|
449
|
+
}
|
|
222
450
|
createRequestParamsSchema(params, isPathParam = false) {
|
|
223
451
|
const queryParams = [];
|
|
224
452
|
if (params.properties) {
|
|
@@ -229,7 +457,7 @@ export class SchemaProcessor {
|
|
|
229
457
|
schema: {
|
|
230
458
|
type: value.type,
|
|
231
459
|
},
|
|
232
|
-
required: value.required,
|
|
460
|
+
required: isPathParam ? true : value.required, // Path parameters are always required
|
|
233
461
|
};
|
|
234
462
|
if (value.enum) {
|
|
235
463
|
param.schema.enum = value.enum;
|
|
@@ -238,6 +466,11 @@ export class SchemaProcessor {
|
|
|
238
466
|
param.description = value.description;
|
|
239
467
|
param.schema.description = value.description;
|
|
240
468
|
}
|
|
469
|
+
// Add examples for path parameters
|
|
470
|
+
if (isPathParam) {
|
|
471
|
+
const example = this.getExampleForParam(name, value.type);
|
|
472
|
+
param.example = example;
|
|
473
|
+
}
|
|
241
474
|
queryParams.push(param);
|
|
242
475
|
}
|
|
243
476
|
}
|
|
@@ -264,7 +497,7 @@ export class SchemaProcessor {
|
|
|
264
497
|
},
|
|
265
498
|
};
|
|
266
499
|
}
|
|
267
|
-
getSchemaContent({ paramsType, pathParamsType, bodyType, responseType }) {
|
|
500
|
+
getSchemaContent({ paramsType, pathParamsType, bodyType, responseType, }) {
|
|
268
501
|
let params = this.openapiDefinitions[paramsType];
|
|
269
502
|
let pathParams = this.openapiDefinitions[pathParamsType];
|
|
270
503
|
let body = this.openapiDefinitions[bodyType];
|
package/dist/lib/utils.js
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
export function capitalize(string) {
|
|
2
2
|
return string.charAt(0).toUpperCase() + string.slice(1);
|
|
3
3
|
}
|
|
4
|
+
/**
|
|
5
|
+
* Extract path parameters from a route path
|
|
6
|
+
* e.g. /users/{id}/posts/{postId} -> ['id', 'postId']
|
|
7
|
+
*/
|
|
8
|
+
export function extractPathParameters(routePath) {
|
|
9
|
+
const paramRegex = /{([^}]+)}/g;
|
|
10
|
+
const params = [];
|
|
11
|
+
let match;
|
|
12
|
+
while ((match = paramRegex.exec(routePath)) !== null) {
|
|
13
|
+
params.push(match[1]);
|
|
14
|
+
}
|
|
15
|
+
return params;
|
|
16
|
+
}
|
|
4
17
|
export function extractJSDocComments(path) {
|
|
5
18
|
const comments = path.node.leadingComments;
|
|
6
19
|
let summary = "";
|
|
@@ -16,8 +29,7 @@ export function extractJSDocComments(path) {
|
|
|
16
29
|
const commentValue = cleanComment(comment.value);
|
|
17
30
|
isOpenApi = commentValue.includes("@openapi");
|
|
18
31
|
if (!summary) {
|
|
19
|
-
|
|
20
|
-
summary = commentValue.split("\n")[summaryIndex];
|
|
32
|
+
summary = commentValue.split("\n")[0];
|
|
21
33
|
}
|
|
22
34
|
if (commentValue.includes("@auth")) {
|
|
23
35
|
const regex = /@auth:\s*(.*)/;
|
|
@@ -64,7 +76,7 @@ export function extractJSDocComments(path) {
|
|
|
64
76
|
};
|
|
65
77
|
}
|
|
66
78
|
export function extractTypeFromComment(commentValue, tag) {
|
|
67
|
-
return commentValue.match(new RegExp(`${tag}\\s
|
|
79
|
+
return commentValue.match(new RegExp(`${tag}\\s*\\s*(\\w+)`))?.[1] || "";
|
|
68
80
|
}
|
|
69
81
|
export function cleanComment(commentValue) {
|
|
70
82
|
return commentValue.replace(/\*\s*/g, "").trim();
|
|
@@ -80,6 +92,35 @@ export function cleanSpec(spec) {
|
|
|
80
92
|
];
|
|
81
93
|
const newSpec = { ...spec };
|
|
82
94
|
propsToRemove.forEach((key) => delete newSpec[key]);
|
|
95
|
+
// Process paths to ensure good examples for path parameters
|
|
96
|
+
if (newSpec.paths) {
|
|
97
|
+
Object.keys(newSpec.paths).forEach((path) => {
|
|
98
|
+
// Check if path contains parameters
|
|
99
|
+
if (path.includes("{") && path.includes("}")) {
|
|
100
|
+
// For each HTTP method in this path
|
|
101
|
+
Object.keys(newSpec.paths[path]).forEach((method) => {
|
|
102
|
+
const operation = newSpec.paths[path][method];
|
|
103
|
+
// Set example properties for each path parameter
|
|
104
|
+
if (operation.parameters) {
|
|
105
|
+
operation.parameters.forEach((param) => {
|
|
106
|
+
if (param.in === "path" && !param.example) {
|
|
107
|
+
// Generate an example based on parameter name
|
|
108
|
+
if (param.name === "id" || param.name.endsWith("Id")) {
|
|
109
|
+
param.example = 123;
|
|
110
|
+
}
|
|
111
|
+
else if (param.name === "slug") {
|
|
112
|
+
param.example = "example-slug";
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
param.example = "example";
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
83
124
|
return newSpec;
|
|
84
125
|
}
|
|
85
126
|
export function getOperationId(routePath, method) {
|
package/package.json
CHANGED