#!/usr/bin/env tsx import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'fs'; import { dirname, join, resolve } from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); interface ZodSchema { name: string; tableName: string; content: string; } function zodToPydanticType(zodType: string, isOpt: boolean, isNull: boolean): string { const typeMap: Record = { 'z.string()': 'str', 'z.string().uuid()': 'str', 'z.string().datetime()': 'datetime', 'z.number().int()': 'int', 'z.number()': 'float', 'z.boolean()': 'bool', 'z.record(z.any())': 'Dict[str, Any]', 'z.unknown()': 'Any', }; const baseType = zodType .replace(/\.optional\(\)/g, '') .replace(/\.nullable\(\)/g, '') .replace(/\.email\(\)/g, ''); let pythonType = typeMap[baseType] || 'Any'; if (isOpt || isNull) { pythonType = `Optional[${pythonType}]`; } return pythonType; } function parseZodSchema(content: string): ZodSchema[] { const schemas: ZodSchema[] = []; const schemaPattern = /export const (\w+Schema) = z\.object\s*\(\s*\{/g; let match; while ((match = schemaPattern.exec(content)) !== null) { const schemaName = match[1]; const tableName = schemaName.replace('Schema', '').toLowerCase(); const startPos = match.index + match[0].length; let braceCount = 1; let pos = startPos; let endPos = -1; while (pos < content.length && braceCount > 0) { if (content[pos] === '{') braceCount++; else if (content[pos] === '}') braceCount--; if (braceCount === 0) { endPos = pos; break; } pos++; } if (endPos === -1) continue; const fieldsText = content.substring(startPos, endPos); schemas.push({ name: schemaName, tableName, content: fieldsText, }); } return schemas; } function generatePydanticModel(schema: ZodSchema): string { const className = schema.name.replace('Schema', ''); const lines = schema.content .split('\n') .map(l => l.trim()) .filter(l => l); const fields: string[] = []; const imports = new Set(['BaseModel']); for (const line of lines) { const fieldMatch = line.match(/(\w+):\s*([^,]+)/); if (fieldMatch) { const [, fieldName, zodType] = fieldMatch; const trimmed = zodType.trim(); const isOptional = /\.optional\(\)/.test(trimmed); const isNullable = /\.nullable\(\)/.test(trimmed); const pythonType = zodToPydanticType(trimmed, isOptional, isNullable); if (pythonType.includes('Optional')) imports.add('Optional'); if (pythonType.includes('datetime')) imports.add('datetime'); if (pythonType.includes('Dict')) imports.add('Dict'); if (pythonType.includes('Any')) imports.add('Any'); let fieldDef = `${fieldName}: ${pythonType}`; if (zodType.includes('.optional()')) { fieldDef += ' = None'; } fields.push(` ${fieldDef}`); } } const importList = Array.from(imports); const pydanticImports = `from pydantic import BaseModel`; const typingItems = importList.filter(i => ['Optional', 'Dict', 'Any'].includes(i)); const typingImports = typingItems.length ? `from typing import ${typingItems.join(', ')}` : ''; const datetimeImports = importList.includes('datetime') ? 'from datetime import datetime' : ''; const allImports = [pydanticImports, typingImports, datetimeImports].filter(Boolean).join('\n'); return `${allImports} class ${className}(BaseModel): ${fields.length > 0 ? fields.join('\n') : ' pass'} class Config: from_attributes = True json_encoders = { datetime: lambda v: v.isoformat() } `; } function main() { console.error('🔄 Translating Zod schemas to Pydantic models...'); const zodDir = resolve(__dirname, '../output/zod'); const pydanticDir = resolve(__dirname, '../output/pydantic'); if (!existsSync(zodDir)) { console.error('⚠️ No Zod schemas found. Run merge:zod first.'); return; } mkdirSync(pydanticDir, { recursive: true }); const zodFiles = readdirSync(zodDir).filter((f: string) => f.endsWith('.ts') && f !== 'index.ts'); if (zodFiles.length === 0) { console.error('⚠️ No Zod schema files found'); return; } const allModels: string[] = []; for (const file of zodFiles) { const content = readFileSync(join(zodDir, file), 'utf-8'); const schemas = parseZodSchema(content); if (schemas.length > 0) { const tableName = file.replace('.ts', ''); const models = schemas.map(generatePydanticModel).join('\n\n'); writeFileSync(join(pydanticDir, `${tableName}.py`), models); schemas.forEach(s => { allModels.push(s.name.replace('Schema', '')); }); } } const initContent = allModels .map(model => { // Map model names to their corresponding table names for imports const modelToTable: Record = { Post: 'posts', Posts: 'posts', Recipe: 'recipes', Recipes: 'recipes', Topic: 'topic', Profile: 'profiles', Profiles: 'profiles', UnescoPetitionSignature: 'unescopetitionsignatures', }; const tableName = modelToTable[model] || model.toLowerCase(); return `from .${tableName} import ${model}`; }) .join('\n') + `\n\n__all__ = [${allModels.map(m => `"${m}"`).join(', ')}]\n`; writeFileSync(join(pydanticDir, '__init__.py'), initContent); console.error(`✅ Generated ${allModels.length} Pydantic models in output/pydantic/`); allModels.forEach(model => console.error(` - ${model}`)); } if (import.meta.url === `file://${process.argv[1]}`) { main(); }