next-openapi-gen 0.10.4 → 1.0.0
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.d.ts +4 -0
- package/dist/cli.js +8599 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +8645 -26
- package/dist/next/index.d.ts +1 -0
- package/dist/next/index.js +7965 -0
- package/dist/react-router/index.d.ts +1 -0
- package/dist/react-router/index.js +7134 -0
- package/dist/vite/index.d.ts +1 -0
- package/dist/vite/index.js +7134 -0
- package/package.json +102 -79
- package/{dist/components/rapidoc.js → templates/init/ui/nextjs/rapidoc.tsx} +16 -20
- package/templates/init/ui/nextjs/redoc.tsx +11 -0
- package/{dist/components/scalar.js → templates/init/ui/nextjs/scalar.tsx} +15 -21
- package/{dist/components/stoplight.js → templates/init/ui/nextjs/stoplight.tsx} +11 -17
- package/templates/init/ui/nextjs/swagger.tsx +17 -0
- package/templates/init/ui/reactrouter/rapidoc.tsx +15 -0
- package/templates/init/ui/reactrouter/redoc.tsx +9 -0
- package/templates/init/ui/reactrouter/scalar.tsx +14 -0
- package/templates/init/ui/reactrouter/stoplight.tsx +10 -0
- package/templates/init/ui/reactrouter/swagger.tsx +11 -0
- package/templates/init/ui/tanstack/rapidoc.tsx +21 -0
- package/templates/init/ui/tanstack/redoc.tsx +14 -0
- package/templates/init/ui/tanstack/scalar.tsx +19 -0
- package/templates/init/ui/tanstack/stoplight.tsx +15 -0
- package/templates/init/ui/tanstack/swagger.tsx +16 -0
- package/templates/init/ui/template-types.d.ts +9 -0
- package/README.md +0 -1047
- package/dist/commands/generate.js +0 -24
- package/dist/commands/init.js +0 -194
- package/dist/components/redoc.js +0 -17
- package/dist/components/swagger.js +0 -21
- package/dist/lib/app-router-strategy.js +0 -66
- package/dist/lib/drizzle-zod-processor.js +0 -329
- package/dist/lib/logger.js +0 -39
- package/dist/lib/openapi-generator.js +0 -171
- package/dist/lib/pages-router-strategy.js +0 -198
- package/dist/lib/route-processor.js +0 -347
- package/dist/lib/router-strategy.js +0 -1
- package/dist/lib/schema-processor.js +0 -1612
- package/dist/lib/utils.js +0 -284
- package/dist/lib/zod-converter.js +0 -2133
- package/dist/openapi-template.js +0 -99
- package/dist/types.js +0 -1
|
@@ -1,2133 +0,0 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import traverseModule from "@babel/traverse";
|
|
4
|
-
import * as t from "@babel/types";
|
|
5
|
-
// Handle both ES modules and CommonJS
|
|
6
|
-
const traverse = traverseModule.default || traverseModule;
|
|
7
|
-
import { parseTypeScriptFile } from "./utils.js";
|
|
8
|
-
import { logger } from "./logger.js";
|
|
9
|
-
import { DrizzleZodProcessor } from "./drizzle-zod-processor.js";
|
|
10
|
-
/**
|
|
11
|
-
* Class for converting Zod schemas to OpenAPI specifications
|
|
12
|
-
*/
|
|
13
|
-
export class ZodSchemaConverter {
|
|
14
|
-
schemaDirs;
|
|
15
|
-
apiDir;
|
|
16
|
-
zodSchemas = {};
|
|
17
|
-
processingSchemas = new Set();
|
|
18
|
-
processedModules = new Set();
|
|
19
|
-
typeToSchemaMapping = {};
|
|
20
|
-
drizzleZodImports = new Set();
|
|
21
|
-
factoryCache = new Map(); // Cache for analyzed factory functions
|
|
22
|
-
factoryCheckCache = new Map(); // Cache for non-factory functions
|
|
23
|
-
fileASTCache = new Map(); // Cache for parsed files
|
|
24
|
-
fileImportsCache = new Map(); // Cache for file imports
|
|
25
|
-
// Current processing context (set during file processing)
|
|
26
|
-
currentFilePath;
|
|
27
|
-
currentAST;
|
|
28
|
-
currentImports;
|
|
29
|
-
constructor(schemaDir, apiDir) {
|
|
30
|
-
const dirs = Array.isArray(schemaDir) ? schemaDir : [schemaDir];
|
|
31
|
-
this.schemaDirs = dirs.map((d) => path.resolve(d));
|
|
32
|
-
this.apiDir = apiDir ? path.resolve(apiDir) : undefined;
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Find a Zod schema by name and convert it to OpenAPI spec
|
|
36
|
-
*/
|
|
37
|
-
convertZodSchemaToOpenApi(schemaName) {
|
|
38
|
-
// Run pre-scan only one time
|
|
39
|
-
if (Object.keys(this.typeToSchemaMapping).length === 0) {
|
|
40
|
-
this.preScanForTypeMappings();
|
|
41
|
-
}
|
|
42
|
-
logger.debug(`Looking for Zod schema: ${schemaName}`);
|
|
43
|
-
// Check mapped types
|
|
44
|
-
const mappedSchemaName = this.typeToSchemaMapping[schemaName];
|
|
45
|
-
if (mappedSchemaName) {
|
|
46
|
-
logger.debug(`Type '${schemaName}' is mapped to schema '${mappedSchemaName}'`);
|
|
47
|
-
schemaName = mappedSchemaName;
|
|
48
|
-
}
|
|
49
|
-
// Check for circular references
|
|
50
|
-
if (this.processingSchemas.has(schemaName)) {
|
|
51
|
-
return { $ref: `#/components/schemas/${schemaName}` };
|
|
52
|
-
}
|
|
53
|
-
// Add to processing set
|
|
54
|
-
this.processingSchemas.add(schemaName);
|
|
55
|
-
try {
|
|
56
|
-
// Return cached schema if it exists
|
|
57
|
-
if (this.zodSchemas[schemaName]) {
|
|
58
|
-
return this.zodSchemas[schemaName];
|
|
59
|
-
}
|
|
60
|
-
// Find all route files and process them first
|
|
61
|
-
const routeFiles = this.findRouteFiles();
|
|
62
|
-
for (const routeFile of routeFiles) {
|
|
63
|
-
this.processFileForZodSchema(routeFile, schemaName);
|
|
64
|
-
if (this.zodSchemas[schemaName]) {
|
|
65
|
-
logger.debug(`Found Zod schema '${schemaName}' in route file: ${routeFile}`);
|
|
66
|
-
return this.zodSchemas[schemaName];
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
// Scan schema directories
|
|
70
|
-
for (const dir of this.schemaDirs) {
|
|
71
|
-
this.scanDirectoryForZodSchema(dir, schemaName);
|
|
72
|
-
if (this.zodSchemas[schemaName])
|
|
73
|
-
break;
|
|
74
|
-
}
|
|
75
|
-
// Return the schema if found, or null if not
|
|
76
|
-
if (this.zodSchemas[schemaName]) {
|
|
77
|
-
logger.debug(`Found and processed Zod schema: ${schemaName}`);
|
|
78
|
-
return this.zodSchemas[schemaName];
|
|
79
|
-
}
|
|
80
|
-
logger.debug(`Could not find Zod schema: ${schemaName}`);
|
|
81
|
-
return null;
|
|
82
|
-
}
|
|
83
|
-
finally {
|
|
84
|
-
// Remove from processing set
|
|
85
|
-
this.processingSchemas.delete(schemaName);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* Find all route files in the project
|
|
90
|
-
*/
|
|
91
|
-
findRouteFiles() {
|
|
92
|
-
const routeFiles = [];
|
|
93
|
-
// When apiDir is configured, scan only that directory to prevent
|
|
94
|
-
// leaking schemas from routes outside the configured API boundary.
|
|
95
|
-
if (this.apiDir) {
|
|
96
|
-
if (fs.existsSync(this.apiDir)) {
|
|
97
|
-
this.findRouteFilesInDir(this.apiDir, routeFiles);
|
|
98
|
-
}
|
|
99
|
-
return routeFiles;
|
|
100
|
-
}
|
|
101
|
-
// Look for route files in common Next.js API directories
|
|
102
|
-
const possibleApiDirs = [
|
|
103
|
-
path.join(process.cwd(), "src", "app", "api"),
|
|
104
|
-
path.join(process.cwd(), "src", "pages", "api"),
|
|
105
|
-
path.join(process.cwd(), "app", "api"),
|
|
106
|
-
path.join(process.cwd(), "pages", "api"),
|
|
107
|
-
];
|
|
108
|
-
for (const dir of possibleApiDirs) {
|
|
109
|
-
if (fs.existsSync(dir)) {
|
|
110
|
-
this.findRouteFilesInDir(dir, routeFiles);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
return routeFiles;
|
|
114
|
-
}
|
|
115
|
-
/**
|
|
116
|
-
* Recursively find route files in a directory
|
|
117
|
-
*/
|
|
118
|
-
findRouteFilesInDir(dir, routeFiles) {
|
|
119
|
-
try {
|
|
120
|
-
const files = fs.readdirSync(dir);
|
|
121
|
-
for (const file of files) {
|
|
122
|
-
const filePath = path.join(dir, file);
|
|
123
|
-
const stats = fs.statSync(filePath);
|
|
124
|
-
if (stats.isDirectory()) {
|
|
125
|
-
this.findRouteFilesInDir(filePath, routeFiles);
|
|
126
|
-
}
|
|
127
|
-
else if (file === "route.ts" ||
|
|
128
|
-
file === "route.tsx" ||
|
|
129
|
-
(file.endsWith(".ts") && file.includes("api"))) {
|
|
130
|
-
routeFiles.push(filePath);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
catch (error) {
|
|
135
|
-
logger.error(`Error scanning directory ${dir} for route files: ${error}`);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* Recursively scan directory for Zod schemas
|
|
140
|
-
*/
|
|
141
|
-
scanDirectoryForZodSchema(dir, schemaName) {
|
|
142
|
-
try {
|
|
143
|
-
const files = fs.readdirSync(dir);
|
|
144
|
-
for (const file of files) {
|
|
145
|
-
const filePath = path.join(dir, file);
|
|
146
|
-
const stats = fs.statSync(filePath);
|
|
147
|
-
if (stats.isDirectory()) {
|
|
148
|
-
this.scanDirectoryForZodSchema(filePath, schemaName);
|
|
149
|
-
}
|
|
150
|
-
else if (file.endsWith(".ts") || file.endsWith(".tsx")) {
|
|
151
|
-
this.processFileForZodSchema(filePath, schemaName);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
catch (error) {
|
|
156
|
-
logger.error(`Error scanning directory ${dir}: ${error}`);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
/**
|
|
160
|
-
* Process a file to find Zod schema definitions
|
|
161
|
-
*/
|
|
162
|
-
processFileForZodSchema(filePath, schemaName) {
|
|
163
|
-
try {
|
|
164
|
-
const content = fs.readFileSync(filePath, "utf-8");
|
|
165
|
-
// Check if file contains schema we are looking for
|
|
166
|
-
if (!content.includes(schemaName)) {
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
// Pre-process all schemas in file
|
|
170
|
-
this.preprocessAllSchemasInFile(filePath);
|
|
171
|
-
// Return it, if the schema has already been processed during pre-processing
|
|
172
|
-
if (this.zodSchemas[schemaName]) {
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
// Parse the file
|
|
176
|
-
const ast = parseTypeScriptFile(content);
|
|
177
|
-
// Cache AST for later use
|
|
178
|
-
this.fileASTCache.set(filePath, ast);
|
|
179
|
-
// Create a map to store imported modules
|
|
180
|
-
let importedModules = {};
|
|
181
|
-
// Check if we have cached imports
|
|
182
|
-
if (this.fileImportsCache.has(filePath)) {
|
|
183
|
-
importedModules = this.fileImportsCache.get(filePath);
|
|
184
|
-
}
|
|
185
|
-
else {
|
|
186
|
-
// Build imports cache
|
|
187
|
-
traverse(ast, {
|
|
188
|
-
ImportDeclaration: (path) => {
|
|
189
|
-
const source = path.node.source.value;
|
|
190
|
-
// Track drizzle-zod imports
|
|
191
|
-
if (source === "drizzle-zod") {
|
|
192
|
-
path.node.specifiers.forEach((specifier) => {
|
|
193
|
-
if (t.isImportSpecifier(specifier) ||
|
|
194
|
-
t.isImportDefaultSpecifier(specifier)) {
|
|
195
|
-
this.drizzleZodImports.add(specifier.local.name);
|
|
196
|
-
}
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
// Process each import specifier
|
|
200
|
-
path.node.specifiers.forEach((specifier) => {
|
|
201
|
-
if (t.isImportSpecifier(specifier) ||
|
|
202
|
-
t.isImportDefaultSpecifier(specifier)) {
|
|
203
|
-
const importedName = specifier.local.name;
|
|
204
|
-
importedModules[importedName] = source;
|
|
205
|
-
}
|
|
206
|
-
});
|
|
207
|
-
},
|
|
208
|
-
});
|
|
209
|
-
// Cache imports for this file
|
|
210
|
-
this.fileImportsCache.set(filePath, importedModules);
|
|
211
|
-
}
|
|
212
|
-
// Set current processing context for use by processZodNode during factory expansion
|
|
213
|
-
this.currentFilePath = filePath;
|
|
214
|
-
this.currentAST = ast;
|
|
215
|
-
this.currentImports = importedModules;
|
|
216
|
-
// Look for all exported Zod schemas
|
|
217
|
-
traverse(ast, {
|
|
218
|
-
// For export const SchemaName = z.object({...})
|
|
219
|
-
ExportNamedDeclaration: (path) => {
|
|
220
|
-
if (t.isVariableDeclaration(path.node.declaration)) {
|
|
221
|
-
path.node.declaration.declarations.forEach((declaration) => {
|
|
222
|
-
if (t.isIdentifier(declaration.id) &&
|
|
223
|
-
declaration.id.name === schemaName &&
|
|
224
|
-
declaration.init) {
|
|
225
|
-
// Check if this is a drizzle-zod helper function
|
|
226
|
-
if (t.isCallExpression(declaration.init) &&
|
|
227
|
-
t.isIdentifier(declaration.init.callee) &&
|
|
228
|
-
this.drizzleZodImports.has(declaration.init.callee.name)) {
|
|
229
|
-
const schema = this.processZodNode(declaration.init);
|
|
230
|
-
if (schema) {
|
|
231
|
-
this.zodSchemas[schemaName] = schema;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
// Check if this is a call expression with .extend()
|
|
235
|
-
else if (t.isCallExpression(declaration.init) &&
|
|
236
|
-
t.isMemberExpression(declaration.init.callee) &&
|
|
237
|
-
t.isIdentifier(declaration.init.callee.property) &&
|
|
238
|
-
declaration.init.callee.property.name === "extend") {
|
|
239
|
-
const schema = this.processZodNode(declaration.init);
|
|
240
|
-
if (schema) {
|
|
241
|
-
this.zodSchemas[schemaName] = schema;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
// Existing code for z.object({...})
|
|
245
|
-
else if (t.isCallExpression(declaration.init) &&
|
|
246
|
-
t.isMemberExpression(declaration.init.callee) &&
|
|
247
|
-
t.isIdentifier(declaration.init.callee.object) &&
|
|
248
|
-
declaration.init.callee.object.name === "z") {
|
|
249
|
-
const schema = this.processZodNode(declaration.init);
|
|
250
|
-
if (schema) {
|
|
251
|
-
this.zodSchemas[schemaName] = schema;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
// Check if this is a factory function call
|
|
255
|
-
else if (t.isCallExpression(declaration.init) &&
|
|
256
|
-
t.isIdentifier(declaration.init.callee)) {
|
|
257
|
-
const factoryName = declaration.init.callee.name;
|
|
258
|
-
logger.debug(`[Schema] Detected potential factory function call: ${factoryName} for schema ${schemaName}`);
|
|
259
|
-
const factoryNode = this.findFactoryFunction(factoryName, filePath, ast, importedModules);
|
|
260
|
-
if (factoryNode) {
|
|
261
|
-
logger.debug(`[Schema] Found factory function, attempting to expand...`);
|
|
262
|
-
const schema = this.expandFactoryCall(factoryNode, declaration.init, filePath);
|
|
263
|
-
if (schema) {
|
|
264
|
-
this.zodSchemas[schemaName] = schema;
|
|
265
|
-
logger.debug(`[Schema] Successfully expanded factory function '${factoryName}' for schema '${schemaName}'`);
|
|
266
|
-
}
|
|
267
|
-
else {
|
|
268
|
-
logger.debug(`[Schema] Failed to expand factory function '${factoryName}'`);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
else {
|
|
272
|
-
logger.debug(`[Schema] Could not find factory function '${factoryName}'`);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
else if (t.isTSTypeAliasDeclaration(path.node.declaration)) {
|
|
279
|
-
// Handle export type aliases with z schema definitions
|
|
280
|
-
if (t.isIdentifier(path.node.declaration.id) &&
|
|
281
|
-
path.node.declaration.id.name === schemaName) {
|
|
282
|
-
const typeAnnotation = path.node.declaration.typeAnnotation;
|
|
283
|
-
// Check if this is a reference to a z schema (e.g., export type UserSchema = z.infer<typeof UserSchema>)
|
|
284
|
-
if (t.isTSTypeReference(typeAnnotation) &&
|
|
285
|
-
t.isIdentifier(typeAnnotation.typeName) &&
|
|
286
|
-
typeAnnotation.typeName.name === "z.infer") {
|
|
287
|
-
// Extract the schema name from z.infer<typeof SchemaName>
|
|
288
|
-
if (typeAnnotation.typeParameters &&
|
|
289
|
-
typeAnnotation.typeParameters.params.length > 0 &&
|
|
290
|
-
t.isTSTypeReference(typeAnnotation.typeParameters.params[0]) &&
|
|
291
|
-
t.isTSTypeQuery(typeAnnotation.typeParameters.params[0].typeName) &&
|
|
292
|
-
t.isIdentifier(
|
|
293
|
-
// @ts-ignore
|
|
294
|
-
typeAnnotation.typeParameters.params[0].typeName.exprName)) {
|
|
295
|
-
const referencedSchema =
|
|
296
|
-
// @ts-ignore
|
|
297
|
-
typeAnnotation.typeParameters.params[0].typeName.exprName
|
|
298
|
-
.name;
|
|
299
|
-
// Look for the referenced schema in the same file
|
|
300
|
-
if (!this.zodSchemas[referencedSchema]) {
|
|
301
|
-
this.processFileForZodSchema(filePath, referencedSchema);
|
|
302
|
-
}
|
|
303
|
-
// Use the referenced schema for this type alias
|
|
304
|
-
if (this.zodSchemas[referencedSchema]) {
|
|
305
|
-
this.zodSchemas[schemaName] =
|
|
306
|
-
this.zodSchemas[referencedSchema];
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
},
|
|
313
|
-
// For const SchemaName = z.object({...})
|
|
314
|
-
VariableDeclarator: (path) => {
|
|
315
|
-
if (t.isIdentifier(path.node.id) &&
|
|
316
|
-
path.node.id.name === schemaName &&
|
|
317
|
-
path.node.init) {
|
|
318
|
-
// Check if this is any Zod schema (including chained calls)
|
|
319
|
-
if (this.isZodSchema(path.node.init)) {
|
|
320
|
-
const schema = this.processZodNode(path.node.init);
|
|
321
|
-
if (schema) {
|
|
322
|
-
this.zodSchemas[schemaName] = schema;
|
|
323
|
-
}
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
// Helper function for processing the call chain
|
|
327
|
-
const processChainedCall = (node, baseSchema) => {
|
|
328
|
-
if (!t.isCallExpression(node) ||
|
|
329
|
-
!t.isMemberExpression(node.callee)) {
|
|
330
|
-
return baseSchema;
|
|
331
|
-
}
|
|
332
|
-
// @ts-ignore
|
|
333
|
-
const methodName = node.callee.property.name;
|
|
334
|
-
let schema = baseSchema;
|
|
335
|
-
// If there is an even deeper call, process it first
|
|
336
|
-
if (t.isCallExpression(node.callee.object)) {
|
|
337
|
-
schema = processChainedCall(node.callee.object, baseSchema);
|
|
338
|
-
}
|
|
339
|
-
// Now apply the current method
|
|
340
|
-
switch (methodName) {
|
|
341
|
-
case "omit":
|
|
342
|
-
if (node.arguments.length > 0 &&
|
|
343
|
-
t.isObjectExpression(node.arguments[0])) {
|
|
344
|
-
node.arguments[0].properties.forEach((prop) => {
|
|
345
|
-
if (t.isObjectProperty(prop) &&
|
|
346
|
-
t.isBooleanLiteral(prop.value) &&
|
|
347
|
-
prop.value.value === true) {
|
|
348
|
-
const key = t.isIdentifier(prop.key)
|
|
349
|
-
? prop.key.name
|
|
350
|
-
: t.isStringLiteral(prop.key)
|
|
351
|
-
? prop.key.value
|
|
352
|
-
: null;
|
|
353
|
-
if (key && schema.properties) {
|
|
354
|
-
logger.debug(`Removing property: ${key}`);
|
|
355
|
-
delete schema.properties[key];
|
|
356
|
-
if (schema.required) {
|
|
357
|
-
schema.required = schema.required.filter((r) => r !== key);
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
});
|
|
362
|
-
}
|
|
363
|
-
break;
|
|
364
|
-
case "partial":
|
|
365
|
-
// All fields become optional (T | undefined), not nullable
|
|
366
|
-
if (schema.properties) {
|
|
367
|
-
delete schema.required;
|
|
368
|
-
}
|
|
369
|
-
break;
|
|
370
|
-
case "pick":
|
|
371
|
-
if (node.arguments.length > 0 &&
|
|
372
|
-
t.isObjectExpression(node.arguments[0])) {
|
|
373
|
-
const keysToPick = [];
|
|
374
|
-
node.arguments[0].properties.forEach((prop) => {
|
|
375
|
-
if (t.isObjectProperty(prop) &&
|
|
376
|
-
t.isBooleanLiteral(prop.value) &&
|
|
377
|
-
prop.value.value === true) {
|
|
378
|
-
const key = t.isIdentifier(prop.key)
|
|
379
|
-
? prop.key.name
|
|
380
|
-
: t.isStringLiteral(prop.key)
|
|
381
|
-
? prop.key.value
|
|
382
|
-
: null;
|
|
383
|
-
if (key)
|
|
384
|
-
keysToPick.push(key);
|
|
385
|
-
}
|
|
386
|
-
});
|
|
387
|
-
// Keep only selected properties
|
|
388
|
-
if (schema.properties) {
|
|
389
|
-
const newProperties = {};
|
|
390
|
-
keysToPick.forEach((key) => {
|
|
391
|
-
if (schema.properties[key]) {
|
|
392
|
-
newProperties[key] = schema.properties[key];
|
|
393
|
-
}
|
|
394
|
-
});
|
|
395
|
-
schema.properties = newProperties;
|
|
396
|
-
// Update required
|
|
397
|
-
if (schema.required) {
|
|
398
|
-
schema.required = schema.required.filter((key) => keysToPick.includes(key));
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
break;
|
|
403
|
-
case "required":
|
|
404
|
-
// All fields become required — preserve genuine nullable flags
|
|
405
|
-
if (schema.properties) {
|
|
406
|
-
schema.required = Object.keys(schema.properties);
|
|
407
|
-
}
|
|
408
|
-
break;
|
|
409
|
-
case "extend":
|
|
410
|
-
// Extend the schema with new properties
|
|
411
|
-
if (node.arguments.length > 0 &&
|
|
412
|
-
t.isObjectExpression(node.arguments[0])) {
|
|
413
|
-
const extensionProperties = {};
|
|
414
|
-
const extensionRequired = [];
|
|
415
|
-
node.arguments[0].properties.forEach((prop) => {
|
|
416
|
-
if (t.isObjectProperty(prop)) {
|
|
417
|
-
const key = t.isIdentifier(prop.key)
|
|
418
|
-
? prop.key.name
|
|
419
|
-
: t.isStringLiteral(prop.key)
|
|
420
|
-
? prop.key.value
|
|
421
|
-
: null;
|
|
422
|
-
if (key) {
|
|
423
|
-
// Process the Zod type for this property
|
|
424
|
-
const propSchema = this.processZodNode(prop.value);
|
|
425
|
-
if (propSchema) {
|
|
426
|
-
extensionProperties[key] = propSchema;
|
|
427
|
-
const isOptional =
|
|
428
|
-
// @ts-ignore
|
|
429
|
-
this.isOptional(prop.value) || this.hasOptionalMethod(prop.value);
|
|
430
|
-
if (!isOptional) {
|
|
431
|
-
extensionRequired.push(key);
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
});
|
|
437
|
-
// Merge with existing schema
|
|
438
|
-
if (schema.properties) {
|
|
439
|
-
schema.properties = {
|
|
440
|
-
...schema.properties,
|
|
441
|
-
...extensionProperties,
|
|
442
|
-
};
|
|
443
|
-
}
|
|
444
|
-
else {
|
|
445
|
-
schema.properties = extensionProperties;
|
|
446
|
-
}
|
|
447
|
-
// Merge required arrays
|
|
448
|
-
if (extensionRequired.length > 0) {
|
|
449
|
-
schema.required = [
|
|
450
|
-
...(schema.required || []),
|
|
451
|
-
...extensionRequired,
|
|
452
|
-
];
|
|
453
|
-
// Deduplicate
|
|
454
|
-
schema.required = [...new Set(schema.required)];
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
break;
|
|
458
|
-
}
|
|
459
|
-
return schema;
|
|
460
|
-
};
|
|
461
|
-
// Find the underlying schema (the most nested object in the chain)
|
|
462
|
-
const findBaseSchema = (node) => {
|
|
463
|
-
if (t.isIdentifier(node)) {
|
|
464
|
-
return node.name;
|
|
465
|
-
}
|
|
466
|
-
else if (t.isMemberExpression(node)) {
|
|
467
|
-
return findBaseSchema(node.object);
|
|
468
|
-
}
|
|
469
|
-
else if (t.isCallExpression(node) &&
|
|
470
|
-
t.isMemberExpression(node.callee)) {
|
|
471
|
-
return findBaseSchema(node.callee.object);
|
|
472
|
-
}
|
|
473
|
-
return null;
|
|
474
|
-
};
|
|
475
|
-
// Check method calls on other schemas
|
|
476
|
-
if (t.isCallExpression(path.node.init)) {
|
|
477
|
-
const baseSchemaName = findBaseSchema(path.node.init);
|
|
478
|
-
if (baseSchemaName && baseSchemaName !== "z") {
|
|
479
|
-
logger.debug(`Found chained call starting from: ${baseSchemaName}`);
|
|
480
|
-
// First make sure the underlying schema is processed
|
|
481
|
-
if (!this.zodSchemas[baseSchemaName]) {
|
|
482
|
-
logger.debug(`Base schema ${baseSchemaName} not found, processing it first`);
|
|
483
|
-
this.processFileForZodSchema(filePath, baseSchemaName);
|
|
484
|
-
}
|
|
485
|
-
if (this.zodSchemas[baseSchemaName]) {
|
|
486
|
-
logger.debug("Base schema found, applying transformations");
|
|
487
|
-
// Copy base schema
|
|
488
|
-
const baseSchema = JSON.parse(JSON.stringify(this.zodSchemas[baseSchemaName]));
|
|
489
|
-
// Process the entire call chain
|
|
490
|
-
const finalSchema = processChainedCall(path.node.init, baseSchema);
|
|
491
|
-
this.zodSchemas[schemaName] = finalSchema;
|
|
492
|
-
logger.debug(`Created ${schemaName} with properties: ${Object.keys(finalSchema.properties || {})}`);
|
|
493
|
-
return;
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
// Check if it is .extend()
|
|
498
|
-
if (t.isCallExpression(path.node.init) &&
|
|
499
|
-
t.isMemberExpression(path.node.init.callee) &&
|
|
500
|
-
t.isIdentifier(path.node.init.callee.property) &&
|
|
501
|
-
path.node.init.callee.property.name === "extend") {
|
|
502
|
-
const schema = this.processZodNode(path.node.init);
|
|
503
|
-
if (schema) {
|
|
504
|
-
this.zodSchemas[schemaName] = schema;
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
// Existing code
|
|
508
|
-
else {
|
|
509
|
-
const schema = this.processZodNode(path.node.init);
|
|
510
|
-
if (schema) {
|
|
511
|
-
this.zodSchemas[schemaName] = schema;
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
},
|
|
516
|
-
// For type aliases that reference Zod schemas
|
|
517
|
-
TSTypeAliasDeclaration: (path) => {
|
|
518
|
-
if (t.isIdentifier(path.node.id)) {
|
|
519
|
-
const typeName = path.node.id.name;
|
|
520
|
-
if (t.isTSTypeReference(path.node.typeAnnotation) &&
|
|
521
|
-
t.isTSQualifiedName(path.node.typeAnnotation.typeName) &&
|
|
522
|
-
t.isIdentifier(path.node.typeAnnotation.typeName.left) &&
|
|
523
|
-
path.node.typeAnnotation.typeName.left.name === "z" &&
|
|
524
|
-
t.isIdentifier(path.node.typeAnnotation.typeName.right) &&
|
|
525
|
-
path.node.typeAnnotation.typeName.right.name === "infer") {
|
|
526
|
-
// Extract schema name from z.infer<typeof SchemaName>
|
|
527
|
-
if (path.node.typeAnnotation.typeParameters &&
|
|
528
|
-
path.node.typeAnnotation.typeParameters.params.length > 0) {
|
|
529
|
-
const param = path.node.typeAnnotation.typeParameters.params[0];
|
|
530
|
-
if (t.isTSTypeQuery(param) && t.isIdentifier(param.exprName)) {
|
|
531
|
-
const referencedSchemaName = param.exprName.name;
|
|
532
|
-
// Save mapping: TypeName -> SchemaName
|
|
533
|
-
this.typeToSchemaMapping[typeName] = referencedSchemaName;
|
|
534
|
-
logger.debug(`Mapped type '${typeName}' to schema '${referencedSchemaName}'`);
|
|
535
|
-
// Process the referenced schema if not already processed
|
|
536
|
-
if (!this.zodSchemas[referencedSchemaName]) {
|
|
537
|
-
this.processFileForZodSchema(filePath, referencedSchemaName);
|
|
538
|
-
}
|
|
539
|
-
// Use the referenced schema for this type
|
|
540
|
-
if (this.zodSchemas[referencedSchemaName]) {
|
|
541
|
-
this.zodSchemas[typeName] =
|
|
542
|
-
this.zodSchemas[referencedSchemaName];
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
if (path.node.id.name === schemaName) {
|
|
548
|
-
// Try to find if this is a z.infer<typeof SchemaName> pattern
|
|
549
|
-
if (t.isTSTypeReference(path.node.typeAnnotation) &&
|
|
550
|
-
t.isIdentifier(path.node.typeAnnotation.typeName) &&
|
|
551
|
-
path.node.typeAnnotation.typeName.name === "infer" &&
|
|
552
|
-
path.node.typeAnnotation.typeParameters &&
|
|
553
|
-
path.node.typeAnnotation.typeParameters.params.length > 0) {
|
|
554
|
-
const param = path.node.typeAnnotation.typeParameters.params[0];
|
|
555
|
-
if (t.isTSTypeQuery(param) && t.isIdentifier(param.exprName)) {
|
|
556
|
-
const referencedSchemaName = param.exprName.name;
|
|
557
|
-
// Find the referenced schema
|
|
558
|
-
this.processFileForZodSchema(filePath, referencedSchemaName);
|
|
559
|
-
if (this.zodSchemas[referencedSchemaName]) {
|
|
560
|
-
this.zodSchemas[schemaName] =
|
|
561
|
-
this.zodSchemas[referencedSchemaName];
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
},
|
|
568
|
-
});
|
|
569
|
-
}
|
|
570
|
-
catch (error) {
|
|
571
|
-
logger.error(`Error processing file ${filePath} for schema ${schemaName}: ${error}`);
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
/**
|
|
575
|
-
* Process all exported schemas in a file, not just the one we're looking for
|
|
576
|
-
*/
|
|
577
|
-
processAllSchemasInFile(filePath) {
|
|
578
|
-
try {
|
|
579
|
-
const content = fs.readFileSync(filePath, "utf-8");
|
|
580
|
-
const ast = parseTypeScriptFile(content);
|
|
581
|
-
traverse(ast, {
|
|
582
|
-
ExportNamedDeclaration: (path) => {
|
|
583
|
-
if (t.isVariableDeclaration(path.node.declaration)) {
|
|
584
|
-
path.node.declaration.declarations.forEach((declaration) => {
|
|
585
|
-
if (t.isIdentifier(declaration.id) &&
|
|
586
|
-
declaration.init &&
|
|
587
|
-
t.isCallExpression(declaration.init) &&
|
|
588
|
-
t.isMemberExpression(declaration.init.callee) &&
|
|
589
|
-
t.isIdentifier(declaration.init.callee.object) &&
|
|
590
|
-
declaration.init.callee.object.name === "z") {
|
|
591
|
-
const schemaName = declaration.id.name;
|
|
592
|
-
if (!this.zodSchemas[schemaName] &&
|
|
593
|
-
!this.processingSchemas.has(schemaName)) {
|
|
594
|
-
this.processingSchemas.add(schemaName);
|
|
595
|
-
const schema = this.processZodNode(declaration.init);
|
|
596
|
-
if (schema) {
|
|
597
|
-
this.zodSchemas[schemaName] = schema;
|
|
598
|
-
}
|
|
599
|
-
this.processingSchemas.delete(schemaName);
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
});
|
|
603
|
-
}
|
|
604
|
-
},
|
|
605
|
-
});
|
|
606
|
-
}
|
|
607
|
-
catch (error) {
|
|
608
|
-
logger.error(`Error processing all schemas in file ${filePath}: ${error}`);
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
/**
|
|
612
|
-
* Process a Zod node and convert it to OpenAPI schema
|
|
613
|
-
*/
|
|
614
|
-
processZodNode(node) {
|
|
615
|
-
// Handle drizzle-zod helper functions (e.g., createInsertSchema, createSelectSchema)
|
|
616
|
-
if (t.isCallExpression(node) &&
|
|
617
|
-
t.isIdentifier(node.callee) &&
|
|
618
|
-
this.drizzleZodImports.has(node.callee.name)) {
|
|
619
|
-
return DrizzleZodProcessor.processSchema(node);
|
|
620
|
-
}
|
|
621
|
-
// Handle reference to another schema (e.g. UserBaseSchema.extend)
|
|
622
|
-
if (t.isCallExpression(node) &&
|
|
623
|
-
t.isMemberExpression(node.callee) &&
|
|
624
|
-
t.isIdentifier(node.callee.object) &&
|
|
625
|
-
t.isIdentifier(node.callee.property) &&
|
|
626
|
-
node.callee.property.name === "extend") {
|
|
627
|
-
const baseSchemaName = node.callee.object.name;
|
|
628
|
-
// Check if the base schema already exists
|
|
629
|
-
if (!this.zodSchemas[baseSchemaName]) {
|
|
630
|
-
// Try to find the basic pattern
|
|
631
|
-
this.convertZodSchemaToOpenApi(baseSchemaName);
|
|
632
|
-
}
|
|
633
|
-
return this.processZodChain(node);
|
|
634
|
-
}
|
|
635
|
-
// Handle z.coerce.TYPE() patterns
|
|
636
|
-
if (t.isCallExpression(node) &&
|
|
637
|
-
t.isMemberExpression(node.callee) &&
|
|
638
|
-
t.isMemberExpression(node.callee.object) &&
|
|
639
|
-
t.isIdentifier(node.callee.object.object) &&
|
|
640
|
-
node.callee.object.object.name === "z" &&
|
|
641
|
-
t.isIdentifier(node.callee.object.property) &&
|
|
642
|
-
node.callee.object.property.name === "coerce" &&
|
|
643
|
-
t.isIdentifier(node.callee.property)) {
|
|
644
|
-
const coerceType = node.callee.property.name;
|
|
645
|
-
// Create a synthetic node for the underlying type using Babel types
|
|
646
|
-
const syntheticNode = t.callExpression(t.memberExpression(t.identifier("z"), t.identifier(coerceType)), []);
|
|
647
|
-
return this.processZodPrimitive(syntheticNode);
|
|
648
|
-
}
|
|
649
|
-
// Handle z.object({...})
|
|
650
|
-
if (t.isCallExpression(node) &&
|
|
651
|
-
t.isMemberExpression(node.callee) &&
|
|
652
|
-
t.isIdentifier(node.callee.object) &&
|
|
653
|
-
node.callee.object.name === "z" &&
|
|
654
|
-
t.isIdentifier(node.callee.property)) {
|
|
655
|
-
const methodName = node.callee.property.name;
|
|
656
|
-
if (methodName === "object" && node.arguments.length > 0) {
|
|
657
|
-
return this.processZodObject(node);
|
|
658
|
-
}
|
|
659
|
-
else if (methodName === "union" && node.arguments.length > 0) {
|
|
660
|
-
return this.processZodUnion(node);
|
|
661
|
-
}
|
|
662
|
-
else if (methodName === "intersection" && node.arguments.length > 0) {
|
|
663
|
-
return this.processZodIntersection(node);
|
|
664
|
-
}
|
|
665
|
-
else if (methodName === "tuple" && node.arguments.length > 0) {
|
|
666
|
-
return this.processZodTuple(node);
|
|
667
|
-
}
|
|
668
|
-
else if (methodName === "discriminatedUnion" &&
|
|
669
|
-
node.arguments.length > 1) {
|
|
670
|
-
return this.processZodDiscriminatedUnion(node);
|
|
671
|
-
}
|
|
672
|
-
else if (methodName === "literal" && node.arguments.length > 0) {
|
|
673
|
-
return this.processZodLiteral(node);
|
|
674
|
-
}
|
|
675
|
-
else {
|
|
676
|
-
return this.processZodPrimitive(node);
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
// Handle schema reference with method calls, e.g., Image.optional(), UserSchema.nullable()
|
|
680
|
-
if (t.isCallExpression(node) &&
|
|
681
|
-
t.isMemberExpression(node.callee) &&
|
|
682
|
-
t.isIdentifier(node.callee.object) &&
|
|
683
|
-
t.isIdentifier(node.callee.property) &&
|
|
684
|
-
node.callee.object.name !== "z" // Make sure it's not a z.* call
|
|
685
|
-
) {
|
|
686
|
-
const schemaName = node.callee.object.name;
|
|
687
|
-
const methodName = node.callee.property.name;
|
|
688
|
-
// Process base schema first if not already processed
|
|
689
|
-
if (!this.zodSchemas[schemaName]) {
|
|
690
|
-
this.convertZodSchemaToOpenApi(schemaName);
|
|
691
|
-
}
|
|
692
|
-
// If the schema exists, create a reference and apply the method
|
|
693
|
-
if (this.zodSchemas[schemaName]) {
|
|
694
|
-
let schema = {
|
|
695
|
-
allOf: [{ $ref: `#/components/schemas/${schemaName}` }],
|
|
696
|
-
};
|
|
697
|
-
// Apply method-specific transformations
|
|
698
|
-
switch (methodName) {
|
|
699
|
-
case "optional":
|
|
700
|
-
case "nullable":
|
|
701
|
-
case "nullish":
|
|
702
|
-
// Don't add nullable flag here as it would be at the wrong level
|
|
703
|
-
// The fact that it's optional is handled by not including it in required array
|
|
704
|
-
break;
|
|
705
|
-
case "describe":
|
|
706
|
-
if (node.arguments.length > 0 &&
|
|
707
|
-
t.isStringLiteral(node.arguments[0])) {
|
|
708
|
-
schema.description = node.arguments[0].value;
|
|
709
|
-
}
|
|
710
|
-
break;
|
|
711
|
-
default:
|
|
712
|
-
// For other methods, process as a chain
|
|
713
|
-
return this.processZodChain(node);
|
|
714
|
-
}
|
|
715
|
-
return schema;
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
// Handle chained methods, e.g., z.string().email().min(5)
|
|
719
|
-
if (t.isCallExpression(node) &&
|
|
720
|
-
t.isMemberExpression(node.callee) &&
|
|
721
|
-
t.isCallExpression(node.callee.object)) {
|
|
722
|
-
return this.processZodChain(node);
|
|
723
|
-
}
|
|
724
|
-
// Handle schema references like z.lazy(() => AnotherSchema)
|
|
725
|
-
if (t.isCallExpression(node) &&
|
|
726
|
-
t.isMemberExpression(node.callee) &&
|
|
727
|
-
t.isIdentifier(node.callee.object) &&
|
|
728
|
-
node.callee.object.name === "z" &&
|
|
729
|
-
t.isIdentifier(node.callee.property) &&
|
|
730
|
-
node.callee.property.name === "lazy" &&
|
|
731
|
-
node.arguments.length > 0) {
|
|
732
|
-
return this.processZodLazy(node);
|
|
733
|
-
}
|
|
734
|
-
// Handle potential factory function calls (e.g., createPaginatedSchema(UserSchema))
|
|
735
|
-
// This must be checked before falling back to "Unknown Zod schema node"
|
|
736
|
-
if (t.isCallExpression(node) &&
|
|
737
|
-
t.isIdentifier(node.callee)) {
|
|
738
|
-
logger.debug(`[processZodNode] Attempting to handle potential factory function: ${node.callee.name}`);
|
|
739
|
-
// We need the current file context - try to get it from the processing context
|
|
740
|
-
// Note: This is a limitation - we may not have file context during preprocessing
|
|
741
|
-
// In that case, we'll return a placeholder and let the main processing handle it
|
|
742
|
-
const currentFilePath = this.currentFilePath;
|
|
743
|
-
const currentAST = this.currentAST;
|
|
744
|
-
const importedModules = this.currentImports;
|
|
745
|
-
if (currentFilePath && currentAST && importedModules) {
|
|
746
|
-
const factoryNode = this.findFactoryFunction(node.callee.name, currentFilePath, currentAST, importedModules);
|
|
747
|
-
if (factoryNode) {
|
|
748
|
-
logger.debug(`[processZodNode] Found factory function, expanding...`);
|
|
749
|
-
const schema = this.expandFactoryCall(factoryNode, node, currentFilePath);
|
|
750
|
-
if (schema) {
|
|
751
|
-
logger.debug(`[processZodNode] Successfully expanded factory function '${node.callee.name}'`);
|
|
752
|
-
return schema;
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
logger.debug(`[processZodNode] Could not expand factory function '${node.callee.name}' - missing context or not a factory`);
|
|
757
|
-
}
|
|
758
|
-
// Handle standalone identifier references (e.g., userSchema used directly)
|
|
759
|
-
if (t.isIdentifier(node)) {
|
|
760
|
-
const schemaName = node.name;
|
|
761
|
-
// Try to find and process the referenced schema
|
|
762
|
-
if (!this.zodSchemas[schemaName]) {
|
|
763
|
-
this.convertZodSchemaToOpenApi(schemaName);
|
|
764
|
-
}
|
|
765
|
-
// Return a reference to the schema
|
|
766
|
-
return { $ref: `#/components/schemas/${schemaName}` };
|
|
767
|
-
}
|
|
768
|
-
logger.debug("Unknown Zod schema node:", node);
|
|
769
|
-
return { type: "object" };
|
|
770
|
-
}
|
|
771
|
-
/**
|
|
772
|
-
* Process a Zod lazy schema: z.lazy(() => Schema)
|
|
773
|
-
*/
|
|
774
|
-
processZodLazy(node) {
|
|
775
|
-
// Get the function in z.lazy(() => Schema)
|
|
776
|
-
if (node.arguments.length > 0 &&
|
|
777
|
-
t.isArrowFunctionExpression(node.arguments[0]) &&
|
|
778
|
-
node.arguments[0].body) {
|
|
779
|
-
const returnExpr = node.arguments[0].body;
|
|
780
|
-
// If the function returns an identifier, it's likely a reference to another schema
|
|
781
|
-
if (t.isIdentifier(returnExpr)) {
|
|
782
|
-
const schemaName = returnExpr.name;
|
|
783
|
-
// Create a reference to the schema
|
|
784
|
-
return { $ref: `#/components/schemas/${schemaName}` };
|
|
785
|
-
}
|
|
786
|
-
// If the function returns a complex expression, try to process it
|
|
787
|
-
return this.processZodNode(returnExpr);
|
|
788
|
-
}
|
|
789
|
-
return { type: "object" };
|
|
790
|
-
}
|
|
791
|
-
/**
|
|
792
|
-
* Process a Zod literal schema: z.literal("value")
|
|
793
|
-
*/
|
|
794
|
-
processZodLiteral(node) {
|
|
795
|
-
if (node.arguments.length === 0) {
|
|
796
|
-
return { type: "string" };
|
|
797
|
-
}
|
|
798
|
-
const arg = node.arguments[0];
|
|
799
|
-
if (t.isStringLiteral(arg)) {
|
|
800
|
-
return {
|
|
801
|
-
type: "string",
|
|
802
|
-
enum: [arg.value],
|
|
803
|
-
};
|
|
804
|
-
}
|
|
805
|
-
else if (t.isNumericLiteral(arg)) {
|
|
806
|
-
return {
|
|
807
|
-
type: "number",
|
|
808
|
-
enum: [arg.value],
|
|
809
|
-
};
|
|
810
|
-
}
|
|
811
|
-
else if (t.isBooleanLiteral(arg)) {
|
|
812
|
-
return {
|
|
813
|
-
type: "boolean",
|
|
814
|
-
enum: [arg.value],
|
|
815
|
-
};
|
|
816
|
-
}
|
|
817
|
-
return { type: "string" };
|
|
818
|
-
}
|
|
819
|
-
/**
|
|
820
|
-
* Process a Zod discriminated union: z.discriminatedUnion("type", [schema1, schema2])
|
|
821
|
-
*/
|
|
822
|
-
processZodDiscriminatedUnion(node) {
|
|
823
|
-
if (node.arguments.length < 2) {
|
|
824
|
-
return { type: "object" };
|
|
825
|
-
}
|
|
826
|
-
// Get the discriminator field name
|
|
827
|
-
let discriminator = "";
|
|
828
|
-
if (t.isStringLiteral(node.arguments[0])) {
|
|
829
|
-
discriminator = node.arguments[0].value;
|
|
830
|
-
}
|
|
831
|
-
// Get the schemas array
|
|
832
|
-
const schemasArray = node.arguments[1];
|
|
833
|
-
if (!t.isArrayExpression(schemasArray)) {
|
|
834
|
-
return { type: "object" };
|
|
835
|
-
}
|
|
836
|
-
const schemas = schemasArray.elements
|
|
837
|
-
.map((element) => this.processZodNode(element))
|
|
838
|
-
.filter((schema) => schema !== null);
|
|
839
|
-
if (schemas.length === 0) {
|
|
840
|
-
return { type: "object" };
|
|
841
|
-
}
|
|
842
|
-
// Create a discriminated mapping for oneOf
|
|
843
|
-
return {
|
|
844
|
-
type: "object",
|
|
845
|
-
discriminator: discriminator
|
|
846
|
-
? {
|
|
847
|
-
propertyName: discriminator,
|
|
848
|
-
}
|
|
849
|
-
: undefined,
|
|
850
|
-
oneOf: schemas,
|
|
851
|
-
};
|
|
852
|
-
}
|
|
853
|
-
/**
|
|
854
|
-
* Process a Zod tuple schema: z.tuple([z.string(), z.number()])
|
|
855
|
-
*/
|
|
856
|
-
processZodTuple(node) {
|
|
857
|
-
if (node.arguments.length === 0 ||
|
|
858
|
-
!t.isArrayExpression(node.arguments[0])) {
|
|
859
|
-
return { type: "array", items: { type: "string" } };
|
|
860
|
-
}
|
|
861
|
-
const tupleItems = node.arguments[0].elements.map((element) => this.processZodNode(element));
|
|
862
|
-
// In OpenAPI, we can represent this as an array with prefixItems (OpenAPI 3.1+)
|
|
863
|
-
// For OpenAPI 3.0.x, we'll use items with type: array
|
|
864
|
-
return {
|
|
865
|
-
type: "array",
|
|
866
|
-
items: tupleItems.length > 0 ? tupleItems[0] : { type: "string" },
|
|
867
|
-
// For OpenAPI 3.1+: prefixItems: tupleItems
|
|
868
|
-
};
|
|
869
|
-
}
|
|
870
|
-
/**
|
|
871
|
-
* Process a Zod intersection schema: z.intersection(schema1, schema2)
|
|
872
|
-
*/
|
|
873
|
-
processZodIntersection(node) {
|
|
874
|
-
if (node.arguments.length < 2) {
|
|
875
|
-
return { type: "object" };
|
|
876
|
-
}
|
|
877
|
-
const schema1 = this.processZodNode(node.arguments[0]);
|
|
878
|
-
const schema2 = this.processZodNode(node.arguments[1]);
|
|
879
|
-
// In OpenAPI, we can use allOf to represent intersection
|
|
880
|
-
return {
|
|
881
|
-
allOf: [schema1, schema2],
|
|
882
|
-
};
|
|
883
|
-
}
|
|
884
|
-
/**
|
|
885
|
-
* Process a Zod union schema: z.union([schema1, schema2])
|
|
886
|
-
*/
|
|
887
|
-
processZodUnion(node) {
|
|
888
|
-
if (node.arguments.length === 0 ||
|
|
889
|
-
!t.isArrayExpression(node.arguments[0])) {
|
|
890
|
-
return { type: "object" };
|
|
891
|
-
}
|
|
892
|
-
const unionItems = node.arguments[0].elements.map((element) => this.processZodNode(element));
|
|
893
|
-
// Check for common pattern: z.union([z.string(), z.null()]) which should be nullable string
|
|
894
|
-
if (unionItems.length === 2) {
|
|
895
|
-
const isNullable = unionItems.some((item) => item.type === "null" ||
|
|
896
|
-
(item.enum && item.enum.length === 1 && item.enum[0] === null));
|
|
897
|
-
if (isNullable) {
|
|
898
|
-
const nonNullItem = unionItems.find((item) => item.type !== "null" &&
|
|
899
|
-
!(item.enum && item.enum.length === 1 && item.enum[0] === null));
|
|
900
|
-
if (nonNullItem) {
|
|
901
|
-
return {
|
|
902
|
-
...nonNullItem,
|
|
903
|
-
nullable: true,
|
|
904
|
-
};
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
// Check if all union items are of the same type with different enum values
|
|
909
|
-
// This is common for string literals like: z.union([z.literal("a"), z.literal("b")])
|
|
910
|
-
const allSameType = unionItems.length > 0 &&
|
|
911
|
-
unionItems.every((item) => item.type === unionItems[0].type && item.enum);
|
|
912
|
-
if (allSameType) {
|
|
913
|
-
// Combine all enum values
|
|
914
|
-
const combinedEnums = unionItems.flatMap((item) => item.enum || []);
|
|
915
|
-
return {
|
|
916
|
-
type: unionItems[0].type,
|
|
917
|
-
enum: combinedEnums,
|
|
918
|
-
};
|
|
919
|
-
}
|
|
920
|
-
// Otherwise, use oneOf for general unions
|
|
921
|
-
return {
|
|
922
|
-
oneOf: unionItems,
|
|
923
|
-
};
|
|
924
|
-
}
|
|
925
|
-
/**
|
|
926
|
-
* Process a Zod object schema: z.object({...})
|
|
927
|
-
*/
|
|
928
|
-
processZodObject(node) {
|
|
929
|
-
if (node.arguments.length === 0 ||
|
|
930
|
-
!t.isObjectExpression(node.arguments[0])) {
|
|
931
|
-
return { type: "object" };
|
|
932
|
-
}
|
|
933
|
-
const objectExpression = node.arguments[0];
|
|
934
|
-
const properties = {};
|
|
935
|
-
const required = [];
|
|
936
|
-
objectExpression.properties.forEach((prop, index) => {
|
|
937
|
-
if (t.isObjectProperty(prop)) {
|
|
938
|
-
let propName;
|
|
939
|
-
// Handle both identifier and string literal keys
|
|
940
|
-
if (t.isIdentifier(prop.key)) {
|
|
941
|
-
propName = prop.key.name;
|
|
942
|
-
}
|
|
943
|
-
else if (t.isStringLiteral(prop.key)) {
|
|
944
|
-
propName = prop.key.value;
|
|
945
|
-
}
|
|
946
|
-
else {
|
|
947
|
-
logger.debug(`Skipping property ${index} - unsupported key type`);
|
|
948
|
-
return; // Skip if key is not identifier or string literal
|
|
949
|
-
}
|
|
950
|
-
if (t.isCallExpression(prop.value) &&
|
|
951
|
-
t.isMemberExpression(prop.value.callee) &&
|
|
952
|
-
t.isIdentifier(prop.value.callee.object)) {
|
|
953
|
-
const schemaName = prop.value.callee.object.name;
|
|
954
|
-
// @ts-ignore
|
|
955
|
-
const methodName = prop.value.callee.property.name;
|
|
956
|
-
// Process base schema first
|
|
957
|
-
if (!this.zodSchemas[schemaName]) {
|
|
958
|
-
this.convertZodSchemaToOpenApi(schemaName);
|
|
959
|
-
}
|
|
960
|
-
// For describe method, use reference with description
|
|
961
|
-
if (methodName === "describe" && this.zodSchemas[schemaName]) {
|
|
962
|
-
if (prop.value.arguments.length > 0 &&
|
|
963
|
-
t.isStringLiteral(prop.value.arguments[0])) {
|
|
964
|
-
properties[propName] = {
|
|
965
|
-
allOf: [{ $ref: `#/components/schemas/${schemaName}` }],
|
|
966
|
-
description: prop.value.arguments[0].value,
|
|
967
|
-
};
|
|
968
|
-
}
|
|
969
|
-
else {
|
|
970
|
-
properties[propName] = {
|
|
971
|
-
$ref: `#/components/schemas/${schemaName}`,
|
|
972
|
-
};
|
|
973
|
-
}
|
|
974
|
-
required.push(propName);
|
|
975
|
-
return;
|
|
976
|
-
}
|
|
977
|
-
// For other methods, process normally
|
|
978
|
-
const processedSchema = this.processZodNode(prop.value);
|
|
979
|
-
if (processedSchema) {
|
|
980
|
-
properties[propName] = processedSchema;
|
|
981
|
-
const isOptional = this.isOptional(prop.value) || this.hasOptionalMethod(prop.value);
|
|
982
|
-
if (!isOptional) {
|
|
983
|
-
required.push(propName);
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
return;
|
|
987
|
-
}
|
|
988
|
-
// Check if the property value is an identifier (reference to another schema)
|
|
989
|
-
if (t.isIdentifier(prop.value)) {
|
|
990
|
-
const referencedSchemaName = prop.value.name;
|
|
991
|
-
// Try to find and convert the referenced schema
|
|
992
|
-
if (!this.zodSchemas[referencedSchemaName]) {
|
|
993
|
-
this.convertZodSchemaToOpenApi(referencedSchemaName);
|
|
994
|
-
}
|
|
995
|
-
// Create a reference
|
|
996
|
-
properties[propName] = {
|
|
997
|
-
$ref: `#/components/schemas/${referencedSchemaName}`,
|
|
998
|
-
};
|
|
999
|
-
required.push(propName); // Assuming it's required unless marked optional
|
|
1000
|
-
return; // Skip further processing for this property
|
|
1001
|
-
}
|
|
1002
|
-
// For array of schemas (like z.array(PaymentMethodSchema))
|
|
1003
|
-
if (t.isCallExpression(prop.value) &&
|
|
1004
|
-
t.isMemberExpression(prop.value.callee) &&
|
|
1005
|
-
t.isIdentifier(prop.value.callee.object) &&
|
|
1006
|
-
prop.value.callee.object.name === "z" &&
|
|
1007
|
-
t.isIdentifier(prop.value.callee.property) &&
|
|
1008
|
-
prop.value.callee.property.name === "array" &&
|
|
1009
|
-
prop.value.arguments.length > 0 &&
|
|
1010
|
-
t.isIdentifier(prop.value.arguments[0])) {
|
|
1011
|
-
const itemSchemaName = prop.value.arguments[0].name;
|
|
1012
|
-
// Try to find and convert the referenced schema
|
|
1013
|
-
if (!this.zodSchemas[itemSchemaName]) {
|
|
1014
|
-
this.convertZodSchemaToOpenApi(itemSchemaName);
|
|
1015
|
-
}
|
|
1016
|
-
// Process as array with reference
|
|
1017
|
-
const arraySchema = this.processZodNode(prop.value);
|
|
1018
|
-
arraySchema.items = {
|
|
1019
|
-
$ref: `#/components/schemas/${itemSchemaName}`,
|
|
1020
|
-
};
|
|
1021
|
-
properties[propName] = arraySchema;
|
|
1022
|
-
const isOptional = this.isOptional(prop.value) || this.hasOptionalMethod(prop.value);
|
|
1023
|
-
if (!isOptional) {
|
|
1024
|
-
required.push(propName);
|
|
1025
|
-
}
|
|
1026
|
-
return; // Skip further processing for this property
|
|
1027
|
-
}
|
|
1028
|
-
// Process property value (a Zod schema)
|
|
1029
|
-
const propSchema = this.processZodNode(prop.value);
|
|
1030
|
-
if (propSchema) {
|
|
1031
|
-
properties[propName] = propSchema;
|
|
1032
|
-
// If the property is not marked as optional, add it to required list
|
|
1033
|
-
const isOptional =
|
|
1034
|
-
// @ts-ignore
|
|
1035
|
-
this.isOptional(prop.value) || this.hasOptionalMethod(prop.value);
|
|
1036
|
-
if (!isOptional) {
|
|
1037
|
-
required.push(propName);
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
}
|
|
1041
|
-
});
|
|
1042
|
-
const schema = {
|
|
1043
|
-
type: "object",
|
|
1044
|
-
properties,
|
|
1045
|
-
};
|
|
1046
|
-
if (required.length > 0) {
|
|
1047
|
-
// Deduplicate required array using Set
|
|
1048
|
-
// @ts-ignore
|
|
1049
|
-
schema.required = [...new Set(required)];
|
|
1050
|
-
}
|
|
1051
|
-
return schema;
|
|
1052
|
-
}
|
|
1053
|
-
/**
|
|
1054
|
-
* Process a Zod primitive schema: z.string(), z.number(), etc.
|
|
1055
|
-
*/
|
|
1056
|
-
processZodPrimitive(node) {
|
|
1057
|
-
if (!t.isMemberExpression(node.callee) ||
|
|
1058
|
-
!t.isIdentifier(node.callee.property)) {
|
|
1059
|
-
return { type: "string" };
|
|
1060
|
-
}
|
|
1061
|
-
const zodType = node.callee.property.name;
|
|
1062
|
-
let schema = {};
|
|
1063
|
-
// Basic type mapping
|
|
1064
|
-
switch (zodType) {
|
|
1065
|
-
case "string":
|
|
1066
|
-
schema = { type: "string" };
|
|
1067
|
-
break;
|
|
1068
|
-
case "number":
|
|
1069
|
-
schema = { type: "number" };
|
|
1070
|
-
break;
|
|
1071
|
-
case "boolean":
|
|
1072
|
-
schema = { type: "boolean" };
|
|
1073
|
-
break;
|
|
1074
|
-
case "date":
|
|
1075
|
-
schema = { type: "string", format: "date-time" };
|
|
1076
|
-
break;
|
|
1077
|
-
case "bigint":
|
|
1078
|
-
schema = { type: "integer", format: "int64" };
|
|
1079
|
-
break;
|
|
1080
|
-
case "any":
|
|
1081
|
-
case "unknown":
|
|
1082
|
-
schema = {}; // Empty schema matches anything
|
|
1083
|
-
break;
|
|
1084
|
-
case "null":
|
|
1085
|
-
case "undefined":
|
|
1086
|
-
schema = { type: "null" };
|
|
1087
|
-
break;
|
|
1088
|
-
case "array":
|
|
1089
|
-
let itemsType = { type: "string" };
|
|
1090
|
-
if (node.arguments.length > 0) {
|
|
1091
|
-
// Check if argument is an identifier (schema reference)
|
|
1092
|
-
if (t.isIdentifier(node.arguments[0])) {
|
|
1093
|
-
const schemaName = node.arguments[0].name;
|
|
1094
|
-
// Try to find and convert the referenced schema
|
|
1095
|
-
if (!this.zodSchemas[schemaName]) {
|
|
1096
|
-
this.convertZodSchemaToOpenApi(schemaName);
|
|
1097
|
-
}
|
|
1098
|
-
// @ts-ignore
|
|
1099
|
-
itemsType = { $ref: `#/components/schemas/${schemaName}` };
|
|
1100
|
-
}
|
|
1101
|
-
else {
|
|
1102
|
-
// @ts-ignore
|
|
1103
|
-
itemsType = this.processZodNode(node.arguments[0]);
|
|
1104
|
-
}
|
|
1105
|
-
}
|
|
1106
|
-
schema = { type: "array", items: itemsType };
|
|
1107
|
-
break;
|
|
1108
|
-
case "enum":
|
|
1109
|
-
if (node.arguments.length > 0 &&
|
|
1110
|
-
t.isArrayExpression(node.arguments[0])) {
|
|
1111
|
-
const enumValues = node.arguments[0].elements
|
|
1112
|
-
.filter((el) => t.isStringLiteral(el) || t.isNumericLiteral(el))
|
|
1113
|
-
// @ts-ignore
|
|
1114
|
-
.map((el) => el.value);
|
|
1115
|
-
const firstValue = enumValues[0];
|
|
1116
|
-
const valueType = typeof firstValue;
|
|
1117
|
-
schema = {
|
|
1118
|
-
type: valueType === "number" ? "number" : "string",
|
|
1119
|
-
enum: enumValues,
|
|
1120
|
-
};
|
|
1121
|
-
}
|
|
1122
|
-
else if (node.arguments.length > 0 &&
|
|
1123
|
-
t.isObjectExpression(node.arguments[0])) {
|
|
1124
|
-
// Handle z.enum({ KEY1: "value1", KEY2: "value2" })
|
|
1125
|
-
const enumValues = [];
|
|
1126
|
-
node.arguments[0].properties.forEach((prop) => {
|
|
1127
|
-
if (t.isObjectProperty(prop) && t.isStringLiteral(prop.value)) {
|
|
1128
|
-
enumValues.push(prop.value.value);
|
|
1129
|
-
}
|
|
1130
|
-
});
|
|
1131
|
-
if (enumValues.length > 0) {
|
|
1132
|
-
schema = {
|
|
1133
|
-
type: "string",
|
|
1134
|
-
enum: enumValues,
|
|
1135
|
-
};
|
|
1136
|
-
}
|
|
1137
|
-
else {
|
|
1138
|
-
schema = { type: "string" };
|
|
1139
|
-
}
|
|
1140
|
-
}
|
|
1141
|
-
else {
|
|
1142
|
-
schema = { type: "string" };
|
|
1143
|
-
}
|
|
1144
|
-
break;
|
|
1145
|
-
case "record":
|
|
1146
|
-
let valueType = { type: "string" };
|
|
1147
|
-
if (node.arguments.length > 0) {
|
|
1148
|
-
valueType = this.processZodNode(node.arguments[0]);
|
|
1149
|
-
}
|
|
1150
|
-
schema = {
|
|
1151
|
-
type: "object",
|
|
1152
|
-
additionalProperties: valueType,
|
|
1153
|
-
};
|
|
1154
|
-
break;
|
|
1155
|
-
case "map":
|
|
1156
|
-
schema = {
|
|
1157
|
-
type: "object",
|
|
1158
|
-
additionalProperties: true,
|
|
1159
|
-
};
|
|
1160
|
-
break;
|
|
1161
|
-
case "set":
|
|
1162
|
-
let setItemType = { type: "string" };
|
|
1163
|
-
if (node.arguments.length > 0) {
|
|
1164
|
-
setItemType = this.processZodNode(node.arguments[0]);
|
|
1165
|
-
}
|
|
1166
|
-
schema = {
|
|
1167
|
-
type: "array",
|
|
1168
|
-
items: setItemType,
|
|
1169
|
-
uniqueItems: true,
|
|
1170
|
-
};
|
|
1171
|
-
break;
|
|
1172
|
-
case "object":
|
|
1173
|
-
if (node.arguments.length > 0) {
|
|
1174
|
-
schema = this.processZodObject(node);
|
|
1175
|
-
}
|
|
1176
|
-
else {
|
|
1177
|
-
schema = { type: "object" };
|
|
1178
|
-
}
|
|
1179
|
-
break;
|
|
1180
|
-
case "custom":
|
|
1181
|
-
// Check if it has TypeScript generic type parameters (z.custom<File>())
|
|
1182
|
-
if (node.typeParameters && node.typeParameters.params.length > 0) {
|
|
1183
|
-
const typeParam = node.typeParameters.params[0];
|
|
1184
|
-
// Check if the generic type is File
|
|
1185
|
-
if (t.isTSTypeReference(typeParam) &&
|
|
1186
|
-
t.isIdentifier(typeParam.typeName) &&
|
|
1187
|
-
typeParam.typeName.name === "File") {
|
|
1188
|
-
schema = {
|
|
1189
|
-
type: "string",
|
|
1190
|
-
format: "binary",
|
|
1191
|
-
};
|
|
1192
|
-
}
|
|
1193
|
-
else {
|
|
1194
|
-
// Other generic types default to string
|
|
1195
|
-
schema = { type: "string" };
|
|
1196
|
-
}
|
|
1197
|
-
}
|
|
1198
|
-
else if (node.arguments.length > 0 &&
|
|
1199
|
-
t.isArrowFunctionExpression(node.arguments[0])) {
|
|
1200
|
-
// Legacy support: FormData validation
|
|
1201
|
-
schema = {
|
|
1202
|
-
type: "object",
|
|
1203
|
-
additionalProperties: true,
|
|
1204
|
-
};
|
|
1205
|
-
}
|
|
1206
|
-
else {
|
|
1207
|
-
// Default case for z.custom() without specific type detection
|
|
1208
|
-
schema = { type: "string" };
|
|
1209
|
-
}
|
|
1210
|
-
break;
|
|
1211
|
-
default:
|
|
1212
|
-
schema = { type: "string" };
|
|
1213
|
-
break;
|
|
1214
|
-
}
|
|
1215
|
-
// Extract description if it exists from direct method calls
|
|
1216
|
-
const description = this.extractDescriptionFromArguments(node);
|
|
1217
|
-
if (description) {
|
|
1218
|
-
schema.description = description;
|
|
1219
|
-
}
|
|
1220
|
-
return schema;
|
|
1221
|
-
}
|
|
1222
|
-
/**
|
|
1223
|
-
* Extract description from method arguments if it's a .describe() call
|
|
1224
|
-
*/
|
|
1225
|
-
extractDescriptionFromArguments(node) {
|
|
1226
|
-
if (t.isMemberExpression(node.callee) &&
|
|
1227
|
-
t.isIdentifier(node.callee.property) &&
|
|
1228
|
-
node.callee.property.name === "describe" &&
|
|
1229
|
-
node.arguments.length > 0 &&
|
|
1230
|
-
t.isStringLiteral(node.arguments[0])) {
|
|
1231
|
-
return node.arguments[0].value;
|
|
1232
|
-
}
|
|
1233
|
-
return null;
|
|
1234
|
-
}
|
|
1235
|
-
/**
|
|
1236
|
-
* Process a Zod chained method call: z.string().email().min(5)
|
|
1237
|
-
*/
|
|
1238
|
-
processZodChain(node) {
|
|
1239
|
-
if (!t.isMemberExpression(node.callee) ||
|
|
1240
|
-
!t.isIdentifier(node.callee.property)) {
|
|
1241
|
-
return { type: "object" };
|
|
1242
|
-
}
|
|
1243
|
-
const methodName = node.callee.property.name;
|
|
1244
|
-
// Process the parent chain first
|
|
1245
|
-
let schema = this.processZodNode(node.callee.object);
|
|
1246
|
-
// Apply the current method
|
|
1247
|
-
switch (methodName) {
|
|
1248
|
-
case "optional":
|
|
1249
|
-
// optional means T | undefined — not in required array, no nullable flag
|
|
1250
|
-
// Required array exclusion is handled by hasOptionalMethod() in processZodObject()
|
|
1251
|
-
break;
|
|
1252
|
-
case "nullable":
|
|
1253
|
-
// nullable means T | null — field stays required but can be null
|
|
1254
|
-
if (!schema.allOf) {
|
|
1255
|
-
schema.nullable = true;
|
|
1256
|
-
}
|
|
1257
|
-
break;
|
|
1258
|
-
case "nullish": // T | null | undefined
|
|
1259
|
-
// Not in required array (handled by hasOptionalMethod) AND can be null
|
|
1260
|
-
if (!schema.allOf) {
|
|
1261
|
-
schema.nullable = true;
|
|
1262
|
-
}
|
|
1263
|
-
break;
|
|
1264
|
-
case "describe":
|
|
1265
|
-
if (node.arguments.length > 0 && t.isStringLiteral(node.arguments[0])) {
|
|
1266
|
-
const description = node.arguments[0].value;
|
|
1267
|
-
// Check if description includes @deprecated
|
|
1268
|
-
if (description.startsWith("@deprecated")) {
|
|
1269
|
-
schema.deprecated = true;
|
|
1270
|
-
// Remove @deprecated from description
|
|
1271
|
-
schema.description = description.replace("@deprecated", "").trim();
|
|
1272
|
-
}
|
|
1273
|
-
else {
|
|
1274
|
-
schema.description = description;
|
|
1275
|
-
}
|
|
1276
|
-
}
|
|
1277
|
-
break;
|
|
1278
|
-
case "deprecated":
|
|
1279
|
-
schema.deprecated = true;
|
|
1280
|
-
break;
|
|
1281
|
-
case "min":
|
|
1282
|
-
if (node.arguments.length > 0 &&
|
|
1283
|
-
t.isNumericLiteral(node.arguments[0])) {
|
|
1284
|
-
if (schema.type === "string") {
|
|
1285
|
-
schema.minLength = node.arguments[0].value;
|
|
1286
|
-
}
|
|
1287
|
-
else if (schema.type === "number" || schema.type === "integer") {
|
|
1288
|
-
schema.minimum = node.arguments[0].value;
|
|
1289
|
-
}
|
|
1290
|
-
else if (schema.type === "array") {
|
|
1291
|
-
schema.minItems = node.arguments[0].value;
|
|
1292
|
-
}
|
|
1293
|
-
}
|
|
1294
|
-
break;
|
|
1295
|
-
case "max":
|
|
1296
|
-
if (node.arguments.length > 0 &&
|
|
1297
|
-
t.isNumericLiteral(node.arguments[0])) {
|
|
1298
|
-
if (schema.type === "string") {
|
|
1299
|
-
schema.maxLength = node.arguments[0].value;
|
|
1300
|
-
}
|
|
1301
|
-
else if (schema.type === "number" || schema.type === "integer") {
|
|
1302
|
-
schema.maximum = node.arguments[0].value;
|
|
1303
|
-
}
|
|
1304
|
-
else if (schema.type === "array") {
|
|
1305
|
-
schema.maxItems = node.arguments[0].value;
|
|
1306
|
-
}
|
|
1307
|
-
}
|
|
1308
|
-
break;
|
|
1309
|
-
case "length":
|
|
1310
|
-
if (node.arguments.length > 0 &&
|
|
1311
|
-
t.isNumericLiteral(node.arguments[0])) {
|
|
1312
|
-
if (schema.type === "string") {
|
|
1313
|
-
schema.minLength = node.arguments[0].value;
|
|
1314
|
-
schema.maxLength = node.arguments[0].value;
|
|
1315
|
-
}
|
|
1316
|
-
else if (schema.type === "array") {
|
|
1317
|
-
schema.minItems = node.arguments[0].value;
|
|
1318
|
-
schema.maxItems = node.arguments[0].value;
|
|
1319
|
-
}
|
|
1320
|
-
}
|
|
1321
|
-
break;
|
|
1322
|
-
case "email":
|
|
1323
|
-
schema.format = "email";
|
|
1324
|
-
break;
|
|
1325
|
-
case "url":
|
|
1326
|
-
schema.format = "uri";
|
|
1327
|
-
break;
|
|
1328
|
-
case "uri":
|
|
1329
|
-
schema.format = "uri";
|
|
1330
|
-
break;
|
|
1331
|
-
case "uuid":
|
|
1332
|
-
schema.format = "uuid";
|
|
1333
|
-
break;
|
|
1334
|
-
case "cuid":
|
|
1335
|
-
schema.format = "cuid";
|
|
1336
|
-
break;
|
|
1337
|
-
case "regex":
|
|
1338
|
-
if (node.arguments.length > 0 && t.isRegExpLiteral(node.arguments[0])) {
|
|
1339
|
-
schema.pattern = node.arguments[0].pattern;
|
|
1340
|
-
}
|
|
1341
|
-
break;
|
|
1342
|
-
case "startsWith":
|
|
1343
|
-
if (node.arguments.length > 0 && t.isStringLiteral(node.arguments[0])) {
|
|
1344
|
-
schema.pattern = `^${this.escapeRegExp(node.arguments[0].value)}`;
|
|
1345
|
-
}
|
|
1346
|
-
break;
|
|
1347
|
-
case "endsWith":
|
|
1348
|
-
if (node.arguments.length > 0 && t.isStringLiteral(node.arguments[0])) {
|
|
1349
|
-
schema.pattern = `${this.escapeRegExp(node.arguments[0].value)}`;
|
|
1350
|
-
}
|
|
1351
|
-
case "includes":
|
|
1352
|
-
if (node.arguments.length > 0 && t.isStringLiteral(node.arguments[0])) {
|
|
1353
|
-
schema.pattern = this.escapeRegExp(node.arguments[0].value);
|
|
1354
|
-
}
|
|
1355
|
-
break;
|
|
1356
|
-
case "int":
|
|
1357
|
-
schema.type = "integer";
|
|
1358
|
-
break;
|
|
1359
|
-
case "positive":
|
|
1360
|
-
schema.minimum = 0;
|
|
1361
|
-
schema.exclusiveMinimum = true;
|
|
1362
|
-
break;
|
|
1363
|
-
case "nonnegative":
|
|
1364
|
-
schema.minimum = 0;
|
|
1365
|
-
break;
|
|
1366
|
-
case "negative":
|
|
1367
|
-
schema.maximum = 0;
|
|
1368
|
-
schema.exclusiveMaximum = true;
|
|
1369
|
-
break;
|
|
1370
|
-
case "nonpositive":
|
|
1371
|
-
schema.maximum = 0;
|
|
1372
|
-
break;
|
|
1373
|
-
case "finite":
|
|
1374
|
-
// Can't express directly in OpenAPI
|
|
1375
|
-
break;
|
|
1376
|
-
case "safe":
|
|
1377
|
-
// Number is within the IEEE-754 "safe integer" range
|
|
1378
|
-
schema.minimum = -9007199254740991; // -(2^53 - 1)
|
|
1379
|
-
schema.maximum = 9007199254740991; // 2^53 - 1
|
|
1380
|
-
break;
|
|
1381
|
-
case "default":
|
|
1382
|
-
if (node.arguments.length > 0) {
|
|
1383
|
-
if (t.isStringLiteral(node.arguments[0])) {
|
|
1384
|
-
schema.default = node.arguments[0].value;
|
|
1385
|
-
}
|
|
1386
|
-
else if (t.isNumericLiteral(node.arguments[0])) {
|
|
1387
|
-
schema.default = node.arguments[0].value;
|
|
1388
|
-
}
|
|
1389
|
-
else if (t.isBooleanLiteral(node.arguments[0])) {
|
|
1390
|
-
schema.default = node.arguments[0].value;
|
|
1391
|
-
}
|
|
1392
|
-
else if (t.isNullLiteral(node.arguments[0])) {
|
|
1393
|
-
schema.default = null;
|
|
1394
|
-
}
|
|
1395
|
-
else if (t.isObjectExpression(node.arguments[0])) {
|
|
1396
|
-
// Try to create a default object, but this might not be complete
|
|
1397
|
-
const defaultObj = {};
|
|
1398
|
-
node.arguments[0].properties.forEach((prop) => {
|
|
1399
|
-
if (t.isObjectProperty(prop) &&
|
|
1400
|
-
(t.isIdentifier(prop.key) || t.isStringLiteral(prop.key)) &&
|
|
1401
|
-
(t.isStringLiteral(prop.value) ||
|
|
1402
|
-
t.isNumericLiteral(prop.value) ||
|
|
1403
|
-
t.isBooleanLiteral(prop.value))) {
|
|
1404
|
-
const key = t.isIdentifier(prop.key)
|
|
1405
|
-
? prop.key.name
|
|
1406
|
-
: prop.key.value;
|
|
1407
|
-
const value = t.isStringLiteral(prop.value)
|
|
1408
|
-
? prop.value.value
|
|
1409
|
-
: t.isNumericLiteral(prop.value)
|
|
1410
|
-
? prop.value.value
|
|
1411
|
-
: t.isBooleanLiteral(prop.value)
|
|
1412
|
-
? prop.value.value
|
|
1413
|
-
: null;
|
|
1414
|
-
if (key !== null && value !== null) {
|
|
1415
|
-
defaultObj[key] = value;
|
|
1416
|
-
}
|
|
1417
|
-
}
|
|
1418
|
-
});
|
|
1419
|
-
schema.default = defaultObj;
|
|
1420
|
-
}
|
|
1421
|
-
}
|
|
1422
|
-
break;
|
|
1423
|
-
case "extend":
|
|
1424
|
-
if (node.arguments.length > 0 &&
|
|
1425
|
-
t.isObjectExpression(node.arguments[0])) {
|
|
1426
|
-
// Get the base schema by processing the object that extend is called on
|
|
1427
|
-
const baseSchemaResult = this.processZodNode(node.callee.object);
|
|
1428
|
-
// If it's a reference, resolve it to the actual schema
|
|
1429
|
-
let baseSchema = baseSchemaResult;
|
|
1430
|
-
if (baseSchemaResult && baseSchemaResult.$ref) {
|
|
1431
|
-
const schemaName = baseSchemaResult.$ref.replace("#/components/schemas/", "");
|
|
1432
|
-
// Try to convert the base schema if not already processed
|
|
1433
|
-
if (!this.zodSchemas[schemaName]) {
|
|
1434
|
-
logger.debug(`[extend] Base schema ${schemaName} not found, attempting to convert it`);
|
|
1435
|
-
this.convertZodSchemaToOpenApi(schemaName);
|
|
1436
|
-
}
|
|
1437
|
-
// Now retrieve the converted schema
|
|
1438
|
-
if (this.zodSchemas[schemaName]) {
|
|
1439
|
-
baseSchema = this.zodSchemas[schemaName];
|
|
1440
|
-
}
|
|
1441
|
-
else {
|
|
1442
|
-
logger.debug(`Could not resolve reference for extend: ${schemaName}`);
|
|
1443
|
-
}
|
|
1444
|
-
}
|
|
1445
|
-
// Process the extension object
|
|
1446
|
-
const extendNode = {
|
|
1447
|
-
type: "CallExpression",
|
|
1448
|
-
callee: {
|
|
1449
|
-
type: "MemberExpression",
|
|
1450
|
-
object: { type: "Identifier", name: "z" },
|
|
1451
|
-
property: { type: "Identifier", name: "object" },
|
|
1452
|
-
computed: false,
|
|
1453
|
-
optional: false,
|
|
1454
|
-
},
|
|
1455
|
-
arguments: [node.arguments[0]],
|
|
1456
|
-
};
|
|
1457
|
-
const extendedProps = this.processZodObject(extendNode);
|
|
1458
|
-
// Merge base schema and extensions
|
|
1459
|
-
if (baseSchema && baseSchema.properties) {
|
|
1460
|
-
schema = {
|
|
1461
|
-
type: "object",
|
|
1462
|
-
properties: {
|
|
1463
|
-
...baseSchema.properties,
|
|
1464
|
-
...(extendedProps?.properties || {}),
|
|
1465
|
-
},
|
|
1466
|
-
required: [
|
|
1467
|
-
...(baseSchema.required || []),
|
|
1468
|
-
...(extendedProps?.required || []),
|
|
1469
|
-
].filter((item, index, arr) => arr.indexOf(item) === index), // Remove duplicates
|
|
1470
|
-
};
|
|
1471
|
-
// Copy other properties from base schema
|
|
1472
|
-
if (baseSchema.description)
|
|
1473
|
-
schema.description = baseSchema.description;
|
|
1474
|
-
}
|
|
1475
|
-
else {
|
|
1476
|
-
logger.debug("Could not resolve base schema for extend");
|
|
1477
|
-
schema = extendedProps || { type: "object" };
|
|
1478
|
-
}
|
|
1479
|
-
}
|
|
1480
|
-
break;
|
|
1481
|
-
case "refine":
|
|
1482
|
-
case "superRefine":
|
|
1483
|
-
// These are custom validators that cannot be easily represented in OpenAPI
|
|
1484
|
-
// We preserve the schema as is
|
|
1485
|
-
break;
|
|
1486
|
-
case "transform":
|
|
1487
|
-
// Transform doesn't change the schema validation, only the output format
|
|
1488
|
-
break;
|
|
1489
|
-
case "or":
|
|
1490
|
-
if (node.arguments.length > 0) {
|
|
1491
|
-
const alternativeSchema = this.processZodNode(node.arguments[0]);
|
|
1492
|
-
if (alternativeSchema) {
|
|
1493
|
-
schema = {
|
|
1494
|
-
oneOf: [schema, alternativeSchema],
|
|
1495
|
-
};
|
|
1496
|
-
}
|
|
1497
|
-
}
|
|
1498
|
-
break;
|
|
1499
|
-
case "and":
|
|
1500
|
-
if (node.arguments.length > 0) {
|
|
1501
|
-
const additionalSchema = this.processZodNode(node.arguments[0]);
|
|
1502
|
-
if (additionalSchema) {
|
|
1503
|
-
schema = {
|
|
1504
|
-
allOf: [schema, additionalSchema],
|
|
1505
|
-
};
|
|
1506
|
-
}
|
|
1507
|
-
}
|
|
1508
|
-
break;
|
|
1509
|
-
}
|
|
1510
|
-
return schema;
|
|
1511
|
-
}
|
|
1512
|
-
/**
|
|
1513
|
-
* Helper to escape special regex characters for pattern creation
|
|
1514
|
-
*/
|
|
1515
|
-
escapeRegExp(string) {
|
|
1516
|
-
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1517
|
-
}
|
|
1518
|
-
/**
|
|
1519
|
-
* Check if a Zod schema is optional
|
|
1520
|
-
*/
|
|
1521
|
-
isOptional(node) {
|
|
1522
|
-
// Direct .optional() call
|
|
1523
|
-
if (t.isCallExpression(node) &&
|
|
1524
|
-
t.isMemberExpression(node.callee) &&
|
|
1525
|
-
t.isIdentifier(node.callee.property) &&
|
|
1526
|
-
node.callee.property.name === "optional") {
|
|
1527
|
-
return true;
|
|
1528
|
-
}
|
|
1529
|
-
// Check for chained calls that end with .optional()
|
|
1530
|
-
if (t.isCallExpression(node) &&
|
|
1531
|
-
t.isMemberExpression(node.callee) &&
|
|
1532
|
-
t.isCallExpression(node.callee.object)) {
|
|
1533
|
-
return this.hasOptionalMethod(node);
|
|
1534
|
-
}
|
|
1535
|
-
return false;
|
|
1536
|
-
}
|
|
1537
|
-
/**
|
|
1538
|
-
* Check if a node has .optional() in its method chain
|
|
1539
|
-
*/
|
|
1540
|
-
hasOptionalMethod(node) {
|
|
1541
|
-
if (!t.isCallExpression(node)) {
|
|
1542
|
-
return false;
|
|
1543
|
-
}
|
|
1544
|
-
if (t.isMemberExpression(node.callee) &&
|
|
1545
|
-
t.isIdentifier(node.callee.property) &&
|
|
1546
|
-
(node.callee.property.name === "optional" ||
|
|
1547
|
-
node.callee.property.name === "nullish")) {
|
|
1548
|
-
return true;
|
|
1549
|
-
}
|
|
1550
|
-
if (t.isMemberExpression(node.callee) &&
|
|
1551
|
-
t.isCallExpression(node.callee.object)) {
|
|
1552
|
-
return this.hasOptionalMethod(node.callee.object);
|
|
1553
|
-
}
|
|
1554
|
-
return false;
|
|
1555
|
-
}
|
|
1556
|
-
/**
|
|
1557
|
-
* Get all processed Zod schemas
|
|
1558
|
-
*/
|
|
1559
|
-
getProcessedSchemas() {
|
|
1560
|
-
return this.zodSchemas;
|
|
1561
|
-
}
|
|
1562
|
-
/**
|
|
1563
|
-
* Pre-scan all files to build type mappings
|
|
1564
|
-
*/
|
|
1565
|
-
preScanForTypeMappings() {
|
|
1566
|
-
logger.debug("Pre-scanning for type mappings...");
|
|
1567
|
-
// Scan route files
|
|
1568
|
-
const routeFiles = this.findRouteFiles();
|
|
1569
|
-
for (const routeFile of routeFiles) {
|
|
1570
|
-
this.scanFileForTypeMappings(routeFile);
|
|
1571
|
-
}
|
|
1572
|
-
// Scan schema directories
|
|
1573
|
-
for (const dir of this.schemaDirs) {
|
|
1574
|
-
this.scanDirectoryForTypeMappings(dir);
|
|
1575
|
-
}
|
|
1576
|
-
}
|
|
1577
|
-
/**
|
|
1578
|
-
* Scan a single file for type mappings
|
|
1579
|
-
*/
|
|
1580
|
-
scanFileForTypeMappings(filePath) {
|
|
1581
|
-
try {
|
|
1582
|
-
const content = fs.readFileSync(filePath, "utf-8");
|
|
1583
|
-
const ast = parseTypeScriptFile(content);
|
|
1584
|
-
traverse(ast, {
|
|
1585
|
-
TSTypeAliasDeclaration: (path) => {
|
|
1586
|
-
if (t.isIdentifier(path.node.id)) {
|
|
1587
|
-
const typeName = path.node.id.name;
|
|
1588
|
-
// Check for z.infer<typeof SchemaName> pattern
|
|
1589
|
-
if (t.isTSTypeReference(path.node.typeAnnotation)) {
|
|
1590
|
-
const typeRef = path.node.typeAnnotation;
|
|
1591
|
-
// Handle both z.infer and just infer (when z is imported)
|
|
1592
|
-
let isInferType = false;
|
|
1593
|
-
if (t.isTSQualifiedName(typeRef.typeName) &&
|
|
1594
|
-
t.isIdentifier(typeRef.typeName.left) &&
|
|
1595
|
-
typeRef.typeName.left.name === "z" &&
|
|
1596
|
-
t.isIdentifier(typeRef.typeName.right) &&
|
|
1597
|
-
typeRef.typeName.right.name === "infer") {
|
|
1598
|
-
isInferType = true;
|
|
1599
|
-
}
|
|
1600
|
-
else if (t.isIdentifier(typeRef.typeName) &&
|
|
1601
|
-
typeRef.typeName.name === "infer") {
|
|
1602
|
-
isInferType = true;
|
|
1603
|
-
}
|
|
1604
|
-
if (isInferType &&
|
|
1605
|
-
typeRef.typeParameters &&
|
|
1606
|
-
typeRef.typeParameters.params.length > 0) {
|
|
1607
|
-
const param = typeRef.typeParameters.params[0];
|
|
1608
|
-
if (t.isTSTypeQuery(param) && t.isIdentifier(param.exprName)) {
|
|
1609
|
-
const referencedSchemaName = param.exprName.name;
|
|
1610
|
-
this.typeToSchemaMapping[typeName] = referencedSchemaName;
|
|
1611
|
-
logger.debug(`Pre-scan: Mapped type '${typeName}' to schema '${referencedSchemaName}'`);
|
|
1612
|
-
}
|
|
1613
|
-
}
|
|
1614
|
-
}
|
|
1615
|
-
}
|
|
1616
|
-
},
|
|
1617
|
-
});
|
|
1618
|
-
}
|
|
1619
|
-
catch (error) {
|
|
1620
|
-
logger.error(`Error scanning file ${filePath} for type mappings: ${error}`);
|
|
1621
|
-
}
|
|
1622
|
-
}
|
|
1623
|
-
/**
|
|
1624
|
-
* Recursively scan directory for type mappings
|
|
1625
|
-
*/
|
|
1626
|
-
scanDirectoryForTypeMappings(dir) {
|
|
1627
|
-
try {
|
|
1628
|
-
const files = fs.readdirSync(dir);
|
|
1629
|
-
for (const file of files) {
|
|
1630
|
-
const filePath = path.join(dir, file);
|
|
1631
|
-
const stats = fs.statSync(filePath);
|
|
1632
|
-
if (stats.isDirectory()) {
|
|
1633
|
-
this.scanDirectoryForTypeMappings(filePath);
|
|
1634
|
-
}
|
|
1635
|
-
else if (file.endsWith(".ts") || file.endsWith(".tsx")) {
|
|
1636
|
-
this.scanFileForTypeMappings(filePath);
|
|
1637
|
-
}
|
|
1638
|
-
}
|
|
1639
|
-
}
|
|
1640
|
-
catch (error) {
|
|
1641
|
-
logger.error(`Error scanning directory ${dir} for type mappings: ${error}`);
|
|
1642
|
-
}
|
|
1643
|
-
}
|
|
1644
|
-
/**
|
|
1645
|
-
* Pre-process all Zod schemas in a file
|
|
1646
|
-
*/
|
|
1647
|
-
preprocessAllSchemasInFile(filePath) {
|
|
1648
|
-
try {
|
|
1649
|
-
const content = fs.readFileSync(filePath, "utf-8");
|
|
1650
|
-
const ast = parseTypeScriptFile(content);
|
|
1651
|
-
// Cache AST for later use
|
|
1652
|
-
this.fileASTCache.set(filePath, ast);
|
|
1653
|
-
// Collect imports to enable factory function resolution during preprocessing
|
|
1654
|
-
let importedModules = {};
|
|
1655
|
-
// First, collect all drizzle-zod imports and regular imports
|
|
1656
|
-
traverse(ast, {
|
|
1657
|
-
ImportDeclaration: (path) => {
|
|
1658
|
-
const source = path.node.source.value;
|
|
1659
|
-
if (source === "drizzle-zod") {
|
|
1660
|
-
path.node.specifiers.forEach((specifier) => {
|
|
1661
|
-
if (t.isImportSpecifier(specifier) ||
|
|
1662
|
-
t.isImportDefaultSpecifier(specifier)) {
|
|
1663
|
-
this.drizzleZodImports.add(specifier.local.name);
|
|
1664
|
-
}
|
|
1665
|
-
});
|
|
1666
|
-
}
|
|
1667
|
-
// Track all imports for factory function resolution
|
|
1668
|
-
path.node.specifiers.forEach((specifier) => {
|
|
1669
|
-
if (t.isImportSpecifier(specifier) ||
|
|
1670
|
-
t.isImportDefaultSpecifier(specifier)) {
|
|
1671
|
-
const importedName = specifier.local.name;
|
|
1672
|
-
importedModules[importedName] = source;
|
|
1673
|
-
}
|
|
1674
|
-
});
|
|
1675
|
-
},
|
|
1676
|
-
});
|
|
1677
|
-
// Cache imports for this file
|
|
1678
|
-
this.fileImportsCache.set(filePath, importedModules);
|
|
1679
|
-
// Set current processing context for factory function expansion
|
|
1680
|
-
this.currentFilePath = filePath;
|
|
1681
|
-
this.currentAST = ast;
|
|
1682
|
-
this.currentImports = importedModules;
|
|
1683
|
-
// Collect all exported Zod schemas
|
|
1684
|
-
traverse(ast, {
|
|
1685
|
-
ExportNamedDeclaration: (path) => {
|
|
1686
|
-
if (t.isVariableDeclaration(path.node.declaration)) {
|
|
1687
|
-
path.node.declaration.declarations.forEach((declaration) => {
|
|
1688
|
-
if (t.isIdentifier(declaration.id) && declaration.init) {
|
|
1689
|
-
const schemaName = declaration.id.name;
|
|
1690
|
-
// Check if is Zos schema
|
|
1691
|
-
if (this.isZodSchema(declaration.init) &&
|
|
1692
|
-
!this.zodSchemas[schemaName]) {
|
|
1693
|
-
logger.debug(`Pre-processing Zod schema: ${schemaName}`);
|
|
1694
|
-
this.processingSchemas.add(schemaName);
|
|
1695
|
-
const schema = this.processZodNode(declaration.init);
|
|
1696
|
-
if (schema) {
|
|
1697
|
-
this.zodSchemas[schemaName] = schema;
|
|
1698
|
-
}
|
|
1699
|
-
this.processingSchemas.delete(schemaName);
|
|
1700
|
-
}
|
|
1701
|
-
}
|
|
1702
|
-
});
|
|
1703
|
-
}
|
|
1704
|
-
},
|
|
1705
|
-
// Also process non-exported const declarations
|
|
1706
|
-
VariableDeclaration: (path) => {
|
|
1707
|
-
path.node.declarations.forEach((declaration) => {
|
|
1708
|
-
if (t.isIdentifier(declaration.id) && declaration.init) {
|
|
1709
|
-
const schemaName = declaration.id.name;
|
|
1710
|
-
if (this.isZodSchema(declaration.init) &&
|
|
1711
|
-
!this.zodSchemas[schemaName] &&
|
|
1712
|
-
!this.processingSchemas.has(schemaName)) {
|
|
1713
|
-
logger.debug(`Pre-processing Zod schema: ${schemaName}`);
|
|
1714
|
-
this.processingSchemas.add(schemaName);
|
|
1715
|
-
const schema = this.processZodNode(declaration.init);
|
|
1716
|
-
if (schema) {
|
|
1717
|
-
this.zodSchemas[schemaName] = schema;
|
|
1718
|
-
}
|
|
1719
|
-
this.processingSchemas.delete(schemaName);
|
|
1720
|
-
}
|
|
1721
|
-
}
|
|
1722
|
-
});
|
|
1723
|
-
},
|
|
1724
|
-
});
|
|
1725
|
-
}
|
|
1726
|
-
catch (error) {
|
|
1727
|
-
logger.error(`Error pre-processing file ${filePath}: ${error}`);
|
|
1728
|
-
}
|
|
1729
|
-
}
|
|
1730
|
-
/**
|
|
1731
|
-
* Check if node is Zod schema
|
|
1732
|
-
*/
|
|
1733
|
-
isZodSchema(node) {
|
|
1734
|
-
if (t.isCallExpression(node)) {
|
|
1735
|
-
// Check for drizzle-zod helper functions (e.g., createInsertSchema, createSelectSchema)
|
|
1736
|
-
if (t.isIdentifier(node.callee) &&
|
|
1737
|
-
this.drizzleZodImports.has(node.callee.name)) {
|
|
1738
|
-
logger.debug(`[isZodSchema] Detected drizzle-zod function: ${node.callee.name}`);
|
|
1739
|
-
return true;
|
|
1740
|
-
}
|
|
1741
|
-
// Check direct z.method() calls
|
|
1742
|
-
if (t.isMemberExpression(node.callee) &&
|
|
1743
|
-
t.isIdentifier(node.callee.object) &&
|
|
1744
|
-
node.callee.object.name === "z") {
|
|
1745
|
-
return true;
|
|
1746
|
-
}
|
|
1747
|
-
// Check chained calls like z.string().regex()
|
|
1748
|
-
if (t.isMemberExpression(node.callee) &&
|
|
1749
|
-
t.isCallExpression(node.callee.object)) {
|
|
1750
|
-
return this.isZodSchema(node.callee.object);
|
|
1751
|
-
}
|
|
1752
|
-
// Do NOT treat unknown function calls as potential Zod schemas here
|
|
1753
|
-
// Factory functions will be detected and handled in processZodNode() instead
|
|
1754
|
-
// This prevents false positives during preprocessing
|
|
1755
|
-
}
|
|
1756
|
-
return false;
|
|
1757
|
-
}
|
|
1758
|
-
/**
|
|
1759
|
-
* Find a factory function by name (lazy detection with caching)
|
|
1760
|
-
* @param functionName - Name of the function to find
|
|
1761
|
-
* @param currentFilePath - Path of the current file being processed
|
|
1762
|
-
* @param currentAST - Already parsed AST of current file
|
|
1763
|
-
* @param importedModules - Map of imported module names to their sources
|
|
1764
|
-
* @returns Factory function node if found and returns Zod schema, null otherwise
|
|
1765
|
-
*/
|
|
1766
|
-
findFactoryFunction(functionName, currentFilePath, currentAST, importedModules) {
|
|
1767
|
-
// Check positive cache first
|
|
1768
|
-
if (this.factoryCache.has(functionName)) {
|
|
1769
|
-
logger.debug(`[Factory] Cache hit for function '${functionName}'`);
|
|
1770
|
-
return this.factoryCache.get(functionName);
|
|
1771
|
-
}
|
|
1772
|
-
// Check negative cache (already checked, not a factory)
|
|
1773
|
-
if (this.factoryCheckCache.has(functionName)) {
|
|
1774
|
-
logger.debug(`[Factory] Negative cache hit for function '${functionName}'`);
|
|
1775
|
-
return null;
|
|
1776
|
-
}
|
|
1777
|
-
logger.debug(`[Factory] Searching for function '${functionName}'`);
|
|
1778
|
-
// Look in current file first (AST already parsed)
|
|
1779
|
-
const localFactory = this.findFunctionInAST(currentAST, functionName);
|
|
1780
|
-
if (localFactory && this.returnsZodSchema(localFactory)) {
|
|
1781
|
-
logger.debug(`[Factory] Found Zod factory function '${functionName}' in current file`);
|
|
1782
|
-
this.factoryCache.set(functionName, localFactory);
|
|
1783
|
-
return localFactory;
|
|
1784
|
-
}
|
|
1785
|
-
// Check if function is imported
|
|
1786
|
-
const importSource = importedModules[functionName];
|
|
1787
|
-
if (importSource) {
|
|
1788
|
-
logger.debug(`[Factory] Function '${functionName}' is imported from '${importSource}'`);
|
|
1789
|
-
// Resolve import path
|
|
1790
|
-
const importedFilePath = this.resolveImportPath(currentFilePath, importSource);
|
|
1791
|
-
if (importedFilePath && fs.existsSync(importedFilePath)) {
|
|
1792
|
-
logger.debug(`[Factory] Resolved import to: ${importedFilePath}`);
|
|
1793
|
-
// Parse imported file (with caching) - this will also cache imports
|
|
1794
|
-
const importedAST = this.parseFileWithCache(importedFilePath);
|
|
1795
|
-
if (importedAST) {
|
|
1796
|
-
const importedFactory = this.findFunctionInAST(importedAST, functionName);
|
|
1797
|
-
if (importedFactory && this.returnsZodSchema(importedFactory)) {
|
|
1798
|
-
logger.debug(`[Factory] Found Zod factory function '${functionName}' in imported file`);
|
|
1799
|
-
this.factoryCache.set(functionName, importedFactory);
|
|
1800
|
-
return importedFactory;
|
|
1801
|
-
}
|
|
1802
|
-
else {
|
|
1803
|
-
logger.debug(`[Factory] Function '${functionName}' found in imported file but does not return Zod schema`);
|
|
1804
|
-
}
|
|
1805
|
-
}
|
|
1806
|
-
}
|
|
1807
|
-
else {
|
|
1808
|
-
logger.debug(`[Factory] Could not resolve import path for '${importSource}'`);
|
|
1809
|
-
}
|
|
1810
|
-
}
|
|
1811
|
-
else {
|
|
1812
|
-
logger.debug(`[Factory] Function '${functionName}' is not imported`);
|
|
1813
|
-
}
|
|
1814
|
-
// Not found or not a Zod factory - cache negative result
|
|
1815
|
-
logger.debug(`[Factory] Function '${functionName}' is not a Zod factory`);
|
|
1816
|
-
this.factoryCheckCache.set(functionName, false);
|
|
1817
|
-
return null;
|
|
1818
|
-
}
|
|
1819
|
-
/**
|
|
1820
|
-
* Find a function definition in an AST
|
|
1821
|
-
*/
|
|
1822
|
-
findFunctionInAST(ast, functionName) {
|
|
1823
|
-
let foundFunction = null;
|
|
1824
|
-
traverse(ast, {
|
|
1825
|
-
// Handle: export function createSchema() { ... }
|
|
1826
|
-
FunctionDeclaration: (path) => {
|
|
1827
|
-
if (t.isIdentifier(path.node.id) && path.node.id.name === functionName) {
|
|
1828
|
-
foundFunction = path.node;
|
|
1829
|
-
path.stop();
|
|
1830
|
-
}
|
|
1831
|
-
},
|
|
1832
|
-
// Handle: export const createSchema = (...) => { ... }
|
|
1833
|
-
VariableDeclarator: (path) => {
|
|
1834
|
-
if (t.isIdentifier(path.node.id) &&
|
|
1835
|
-
path.node.id.name === functionName &&
|
|
1836
|
-
(t.isArrowFunctionExpression(path.node.init) ||
|
|
1837
|
-
t.isFunctionExpression(path.node.init))) {
|
|
1838
|
-
foundFunction = path.node.init;
|
|
1839
|
-
path.stop();
|
|
1840
|
-
}
|
|
1841
|
-
},
|
|
1842
|
-
});
|
|
1843
|
-
return foundFunction;
|
|
1844
|
-
}
|
|
1845
|
-
/**
|
|
1846
|
-
* Check if a function returns a Zod schema by analyzing return statements
|
|
1847
|
-
*/
|
|
1848
|
-
returnsZodSchema(functionNode) {
|
|
1849
|
-
if (!t.isFunctionDeclaration(functionNode) &&
|
|
1850
|
-
!t.isArrowFunctionExpression(functionNode) &&
|
|
1851
|
-
!t.isFunctionExpression(functionNode)) {
|
|
1852
|
-
return false;
|
|
1853
|
-
}
|
|
1854
|
-
let returnsZod = false;
|
|
1855
|
-
// For arrow functions with direct return (no block)
|
|
1856
|
-
if (t.isArrowFunctionExpression(functionNode) &&
|
|
1857
|
-
!t.isBlockStatement(functionNode.body)) {
|
|
1858
|
-
returnsZod = this.isZodSchema(functionNode.body);
|
|
1859
|
-
logger.debug(`[Factory] Arrow function direct return, isZodSchema: ${returnsZod}`);
|
|
1860
|
-
return returnsZod;
|
|
1861
|
-
}
|
|
1862
|
-
// For functions with block statements, analyze return statements manually
|
|
1863
|
-
const body = functionNode.body;
|
|
1864
|
-
if (!t.isBlockStatement(body)) {
|
|
1865
|
-
return false;
|
|
1866
|
-
}
|
|
1867
|
-
// Manually walk through statements instead of using traverse
|
|
1868
|
-
const checkStatements = (statements) => {
|
|
1869
|
-
for (const stmt of statements) {
|
|
1870
|
-
if (t.isReturnStatement(stmt) && stmt.argument) {
|
|
1871
|
-
if (this.isZodSchema(stmt.argument)) {
|
|
1872
|
-
logger.debug(`[Factory] Found Zod schema in return statement`);
|
|
1873
|
-
return true;
|
|
1874
|
-
}
|
|
1875
|
-
}
|
|
1876
|
-
// Check nested blocks (if statements, etc.)
|
|
1877
|
-
else if (t.isIfStatement(stmt)) {
|
|
1878
|
-
if (t.isBlockStatement(stmt.consequent)) {
|
|
1879
|
-
if (checkStatements(stmt.consequent.body))
|
|
1880
|
-
return true;
|
|
1881
|
-
}
|
|
1882
|
-
else if (t.isReturnStatement(stmt.consequent) && stmt.consequent.argument) {
|
|
1883
|
-
if (this.isZodSchema(stmt.consequent.argument))
|
|
1884
|
-
return true;
|
|
1885
|
-
}
|
|
1886
|
-
if (stmt.alternate) {
|
|
1887
|
-
if (t.isBlockStatement(stmt.alternate)) {
|
|
1888
|
-
if (checkStatements(stmt.alternate.body))
|
|
1889
|
-
return true;
|
|
1890
|
-
}
|
|
1891
|
-
else if (t.isReturnStatement(stmt.alternate) && stmt.alternate.argument) {
|
|
1892
|
-
if (this.isZodSchema(stmt.alternate.argument))
|
|
1893
|
-
return true;
|
|
1894
|
-
}
|
|
1895
|
-
}
|
|
1896
|
-
}
|
|
1897
|
-
}
|
|
1898
|
-
return false;
|
|
1899
|
-
};
|
|
1900
|
-
returnsZod = checkStatements(body.body);
|
|
1901
|
-
return returnsZod;
|
|
1902
|
-
}
|
|
1903
|
-
/**
|
|
1904
|
-
* Parse a file with caching (also caches imports)
|
|
1905
|
-
*/
|
|
1906
|
-
parseFileWithCache(filePath) {
|
|
1907
|
-
if (this.fileASTCache.has(filePath)) {
|
|
1908
|
-
return this.fileASTCache.get(filePath);
|
|
1909
|
-
}
|
|
1910
|
-
try {
|
|
1911
|
-
const content = fs.readFileSync(filePath, "utf-8");
|
|
1912
|
-
const ast = parseTypeScriptFile(content);
|
|
1913
|
-
this.fileASTCache.set(filePath, ast);
|
|
1914
|
-
// Also build and cache imports for this file
|
|
1915
|
-
if (!this.fileImportsCache.has(filePath)) {
|
|
1916
|
-
const importedModules = {};
|
|
1917
|
-
traverse(ast, {
|
|
1918
|
-
ImportDeclaration: (path) => {
|
|
1919
|
-
const source = path.node.source.value;
|
|
1920
|
-
// Track drizzle-zod imports
|
|
1921
|
-
if (source === "drizzle-zod") {
|
|
1922
|
-
path.node.specifiers.forEach((specifier) => {
|
|
1923
|
-
if (t.isImportSpecifier(specifier) ||
|
|
1924
|
-
t.isImportDefaultSpecifier(specifier)) {
|
|
1925
|
-
this.drizzleZodImports.add(specifier.local.name);
|
|
1926
|
-
}
|
|
1927
|
-
});
|
|
1928
|
-
}
|
|
1929
|
-
// Process each import specifier
|
|
1930
|
-
path.node.specifiers.forEach((specifier) => {
|
|
1931
|
-
if (t.isImportSpecifier(specifier) ||
|
|
1932
|
-
t.isImportDefaultSpecifier(specifier)) {
|
|
1933
|
-
const importedName = specifier.local.name;
|
|
1934
|
-
importedModules[importedName] = source;
|
|
1935
|
-
}
|
|
1936
|
-
});
|
|
1937
|
-
},
|
|
1938
|
-
});
|
|
1939
|
-
this.fileImportsCache.set(filePath, importedModules);
|
|
1940
|
-
}
|
|
1941
|
-
return ast;
|
|
1942
|
-
}
|
|
1943
|
-
catch (error) {
|
|
1944
|
-
logger.error(`[Factory] Error parsing file '${filePath}': ${error}`);
|
|
1945
|
-
return null;
|
|
1946
|
-
}
|
|
1947
|
-
}
|
|
1948
|
-
/**
|
|
1949
|
-
* Resolve import path relative to current file
|
|
1950
|
-
*/
|
|
1951
|
-
resolveImportPath(currentFilePath, importSource) {
|
|
1952
|
-
// Handle relative imports
|
|
1953
|
-
if (importSource.startsWith(".")) {
|
|
1954
|
-
const currentDir = path.dirname(currentFilePath);
|
|
1955
|
-
let resolvedPath = path.resolve(currentDir, importSource);
|
|
1956
|
-
// Try adding extensions if not present
|
|
1957
|
-
const extensions = [".ts", ".tsx", ".js", ".jsx"];
|
|
1958
|
-
if (!path.extname(resolvedPath)) {
|
|
1959
|
-
for (const ext of extensions) {
|
|
1960
|
-
const withExt = resolvedPath + ext;
|
|
1961
|
-
if (fs.existsSync(withExt)) {
|
|
1962
|
-
return withExt;
|
|
1963
|
-
}
|
|
1964
|
-
}
|
|
1965
|
-
// Try index files
|
|
1966
|
-
for (const ext of extensions) {
|
|
1967
|
-
const indexPath = path.join(resolvedPath, `index${ext}`);
|
|
1968
|
-
if (fs.existsSync(indexPath)) {
|
|
1969
|
-
return indexPath;
|
|
1970
|
-
}
|
|
1971
|
-
}
|
|
1972
|
-
}
|
|
1973
|
-
else if (fs.existsSync(resolvedPath)) {
|
|
1974
|
-
return resolvedPath;
|
|
1975
|
-
}
|
|
1976
|
-
}
|
|
1977
|
-
// Handle absolute imports from schemaDir
|
|
1978
|
-
// This is a simplified approach - you might need to enhance this based on tsconfig paths
|
|
1979
|
-
return null;
|
|
1980
|
-
}
|
|
1981
|
-
/**
|
|
1982
|
-
* Expand a factory function call by substituting arguments
|
|
1983
|
-
*/
|
|
1984
|
-
expandFactoryCall(factoryNode, callNode, filePath) {
|
|
1985
|
-
if (!t.isFunctionDeclaration(factoryNode) &&
|
|
1986
|
-
!t.isArrowFunctionExpression(factoryNode) &&
|
|
1987
|
-
!t.isFunctionExpression(factoryNode)) {
|
|
1988
|
-
return null;
|
|
1989
|
-
}
|
|
1990
|
-
logger.debug(`[Factory] Expanding factory call with ${callNode.arguments.length} arguments`);
|
|
1991
|
-
// Build parameter -> argument mapping
|
|
1992
|
-
const paramMap = new Map();
|
|
1993
|
-
const params = factoryNode.params;
|
|
1994
|
-
for (let i = 0; i < params.length && i < callNode.arguments.length; i++) {
|
|
1995
|
-
const param = params[i];
|
|
1996
|
-
const arg = callNode.arguments[i];
|
|
1997
|
-
if (t.isIdentifier(param)) {
|
|
1998
|
-
paramMap.set(param.name, arg);
|
|
1999
|
-
logger.debug(`[Factory] Mapped parameter '${param.name}' to argument`);
|
|
2000
|
-
}
|
|
2001
|
-
else if (t.isObjectPattern(param)) {
|
|
2002
|
-
// Handle destructured parameters - simplified for now
|
|
2003
|
-
logger.debug(`[Factory] Skipping destructured parameter (not yet supported)`);
|
|
2004
|
-
}
|
|
2005
|
-
}
|
|
2006
|
-
// Extract return statement
|
|
2007
|
-
const returnNode = this.extractReturnNode(factoryNode);
|
|
2008
|
-
if (!returnNode) {
|
|
2009
|
-
logger.debug(`[Factory] No return statement found in factory`);
|
|
2010
|
-
return null;
|
|
2011
|
-
}
|
|
2012
|
-
logger.debug(`[Factory] Return node type: ${returnNode.type}`);
|
|
2013
|
-
// Clone and substitute parameters in return node
|
|
2014
|
-
const substitutedNode = this.substituteParameters(returnNode, paramMap, filePath);
|
|
2015
|
-
logger.debug(`[Factory] Substituted node type: ${substitutedNode.type}`);
|
|
2016
|
-
// Process the substituted node as a normal Zod schema
|
|
2017
|
-
const result = this.processZodNode(substitutedNode);
|
|
2018
|
-
if (result) {
|
|
2019
|
-
logger.debug(`[Factory] Successfully processed substituted node, result has ${Object.keys(result).length} keys`);
|
|
2020
|
-
}
|
|
2021
|
-
else {
|
|
2022
|
-
logger.debug(`[Factory] Failed to process substituted node`);
|
|
2023
|
-
}
|
|
2024
|
-
return result;
|
|
2025
|
-
}
|
|
2026
|
-
/**
|
|
2027
|
-
* Extract the return node from a function
|
|
2028
|
-
*/
|
|
2029
|
-
extractReturnNode(functionNode) {
|
|
2030
|
-
// For arrow functions with direct return (no block)
|
|
2031
|
-
if (t.isArrowFunctionExpression(functionNode) &&
|
|
2032
|
-
!t.isBlockStatement(functionNode.body)) {
|
|
2033
|
-
return functionNode.body;
|
|
2034
|
-
}
|
|
2035
|
-
// For functions with block statements
|
|
2036
|
-
const body = t.isFunctionDeclaration(functionNode) ||
|
|
2037
|
-
t.isArrowFunctionExpression(functionNode) ||
|
|
2038
|
-
t.isFunctionExpression(functionNode)
|
|
2039
|
-
? functionNode.body
|
|
2040
|
-
: null;
|
|
2041
|
-
if (!body || !t.isBlockStatement(body)) {
|
|
2042
|
-
return null;
|
|
2043
|
-
}
|
|
2044
|
-
// Find first return statement manually
|
|
2045
|
-
const findReturn = (statements) => {
|
|
2046
|
-
for (const stmt of statements) {
|
|
2047
|
-
if (t.isReturnStatement(stmt) && stmt.argument) {
|
|
2048
|
-
return stmt.argument;
|
|
2049
|
-
}
|
|
2050
|
-
// Check nested blocks
|
|
2051
|
-
if (t.isIfStatement(stmt)) {
|
|
2052
|
-
if (t.isBlockStatement(stmt.consequent)) {
|
|
2053
|
-
const found = findReturn(stmt.consequent.body);
|
|
2054
|
-
if (found)
|
|
2055
|
-
return found;
|
|
2056
|
-
}
|
|
2057
|
-
else if (t.isReturnStatement(stmt.consequent) && stmt.consequent.argument) {
|
|
2058
|
-
return stmt.consequent.argument;
|
|
2059
|
-
}
|
|
2060
|
-
if (stmt.alternate) {
|
|
2061
|
-
if (t.isBlockStatement(stmt.alternate)) {
|
|
2062
|
-
const found = findReturn(stmt.alternate.body);
|
|
2063
|
-
if (found)
|
|
2064
|
-
return found;
|
|
2065
|
-
}
|
|
2066
|
-
else if (t.isReturnStatement(stmt.alternate) && stmt.alternate.argument) {
|
|
2067
|
-
return stmt.alternate.argument;
|
|
2068
|
-
}
|
|
2069
|
-
}
|
|
2070
|
-
}
|
|
2071
|
-
}
|
|
2072
|
-
return null;
|
|
2073
|
-
};
|
|
2074
|
-
return findReturn(body.body);
|
|
2075
|
-
}
|
|
2076
|
-
/**
|
|
2077
|
-
* Substitute parameters with actual arguments in an AST node (deep clone and replace)
|
|
2078
|
-
*/
|
|
2079
|
-
substituteParameters(node, paramMap, filePath) {
|
|
2080
|
-
// Deep clone the node to avoid modifying the original
|
|
2081
|
-
const cloned = t.cloneNode(node, /* deep */ true, /* withoutLoc */ false);
|
|
2082
|
-
// Manual recursive substitution without traverse
|
|
2083
|
-
const substitute = (n) => {
|
|
2084
|
-
if (t.isIdentifier(n)) {
|
|
2085
|
-
// Replace if this is a parameter
|
|
2086
|
-
if (paramMap.has(n.name)) {
|
|
2087
|
-
const replacement = paramMap.get(n.name);
|
|
2088
|
-
return t.cloneNode(replacement, true, false);
|
|
2089
|
-
}
|
|
2090
|
-
return n;
|
|
2091
|
-
}
|
|
2092
|
-
// Handle CallExpression
|
|
2093
|
-
if (t.isCallExpression(n)) {
|
|
2094
|
-
return t.callExpression(substitute(n.callee), n.arguments.map((arg) => {
|
|
2095
|
-
if (t.isSpreadElement(arg)) {
|
|
2096
|
-
return t.spreadElement(substitute(arg.argument));
|
|
2097
|
-
}
|
|
2098
|
-
return substitute(arg);
|
|
2099
|
-
}));
|
|
2100
|
-
}
|
|
2101
|
-
// Handle MemberExpression
|
|
2102
|
-
if (t.isMemberExpression(n)) {
|
|
2103
|
-
return t.memberExpression(substitute(n.object), n.computed ? substitute(n.property) : n.property, n.computed);
|
|
2104
|
-
}
|
|
2105
|
-
// Handle ObjectExpression
|
|
2106
|
-
if (t.isObjectExpression(n)) {
|
|
2107
|
-
return t.objectExpression(n.properties.map((prop) => {
|
|
2108
|
-
if (t.isObjectProperty(prop)) {
|
|
2109
|
-
return t.objectProperty(prop.computed ? substitute(prop.key) : prop.key, substitute(prop.value), prop.computed, prop.shorthand);
|
|
2110
|
-
}
|
|
2111
|
-
if (t.isSpreadElement(prop)) {
|
|
2112
|
-
return t.spreadElement(substitute(prop.argument));
|
|
2113
|
-
}
|
|
2114
|
-
return prop;
|
|
2115
|
-
}));
|
|
2116
|
-
}
|
|
2117
|
-
// Handle ArrayExpression
|
|
2118
|
-
if (t.isArrayExpression(n)) {
|
|
2119
|
-
return t.arrayExpression(n.elements.map((elem) => {
|
|
2120
|
-
if (!elem)
|
|
2121
|
-
return null;
|
|
2122
|
-
if (t.isSpreadElement(elem)) {
|
|
2123
|
-
return t.spreadElement(substitute(elem.argument));
|
|
2124
|
-
}
|
|
2125
|
-
return substitute(elem);
|
|
2126
|
-
}));
|
|
2127
|
-
}
|
|
2128
|
-
// Return as-is for other node types
|
|
2129
|
-
return n;
|
|
2130
|
-
};
|
|
2131
|
-
return substitute(cloned);
|
|
2132
|
-
}
|
|
2133
|
-
}
|