next-openapi-gen 0.3.1 → 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/dist/lib/schema-processor.js +207 -42
- package/dist/lib/utils.js +2 -3
- package/package.json +1 -1
|
@@ -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)) {
|
|
@@ -117,16 +130,85 @@ export class SchemaProcessor {
|
|
|
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
|
package/dist/lib/utils.js
CHANGED
|
@@ -29,8 +29,7 @@ export function extractJSDocComments(path) {
|
|
|
29
29
|
const commentValue = cleanComment(comment.value);
|
|
30
30
|
isOpenApi = commentValue.includes("@openapi");
|
|
31
31
|
if (!summary) {
|
|
32
|
-
|
|
33
|
-
summary = commentValue.split("\n")[summaryIndex];
|
|
32
|
+
summary = commentValue.split("\n")[0];
|
|
34
33
|
}
|
|
35
34
|
if (commentValue.includes("@auth")) {
|
|
36
35
|
const regex = /@auth:\s*(.*)/;
|
|
@@ -77,7 +76,7 @@ export function extractJSDocComments(path) {
|
|
|
77
76
|
};
|
|
78
77
|
}
|
|
79
78
|
export function extractTypeFromComment(commentValue, tag) {
|
|
80
|
-
return commentValue.match(new RegExp(`${tag}\\s
|
|
79
|
+
return commentValue.match(new RegExp(`${tag}\\s*\\s*(\\w+)`))?.[1] || "";
|
|
81
80
|
}
|
|
82
81
|
export function cleanComment(commentValue) {
|
|
83
82
|
return commentValue.replace(/\*\s*/g, "").trim();
|
package/package.json
CHANGED