UNPKG

prisma-zod-generator

Version:

Prisma 2+ generator to emit Zod schemas from your Prisma schema

673 lines 31.9 kB
"use strict"; /** * Variant File Generation Coordinator * Orchestrates creation of multiple schema files for each variant */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.VariantFileGenerationCoordinator = void 0; const fs_1 = require("fs"); const path_1 = __importDefault(require("path")); const model_1 = require("../generators/model"); const variants_1 = require("../types/variants"); const formatFile_1 = require("../utils/formatFile"); const naming_resolver_1 = require("../utils/naming-resolver"); const transformer_1 = __importDefault(require("../transformer")); const naming_1 = require("../utils/naming"); const writeFileSafely_1 = require("../utils/writeFileSafely"); const config_1 = require("./config"); const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); /** * Variant File Generation Coordinator * Main class that orchestrates generation of all schema variants */ class VariantFileGenerationCoordinator { constructor(configManager, namingSystem, typeMapper) { this.progressCallbacks = []; this.configManager = configManager || new config_1.VariantConfigurationManager(); this.namingSystem = namingSystem || new naming_1.VariantNamingSystem(); this.typeMapper = typeMapper || new model_1.PrismaTypeMapper(); } /** * Generate all variants for multiple models */ async generateAllVariants(models, options = {}, importExtension = '') { const startTime = Date.now(); const progress = { totalModels: models.length, processedModels: 0, totalVariants: this.calculateTotalVariants(models, options), processedVariants: 0, currentModel: '', currentVariant: variants_1.VariantType.PURE, startTime, errors: [], }; const collections = []; const allErrors = []; try { if (options.parallelGeneration) { // Generate models in parallel const promises = models.map((model) => this.generateModelVariants(model, options, progress, importExtension)); const results = await Promise.allSettled(promises); results.forEach((result, index) => { if (result.status === 'fulfilled') { collections.push(result.value); } else { allErrors.push(`Model ${models[index].name}: ${result.reason}`); } }); } else { // Generate models sequentially for (const model of models) { try { progress.currentModel = model.name; this.notifyProgress(progress); const collection = await this.generateModelVariants(model, options, progress, importExtension); collections.push(collection); progress.processedModels++; } catch (error) { const errorMessage = `Failed to generate variants for model ${model.name}: ${error instanceof Error ? error.message : 'Unknown error'}`; allErrors.push(errorMessage); progress.errors.push({ model: model.name, variant: progress.currentVariant, error: errorMessage, timestamp: Date.now(), }); } } } // Generate index files if requested if (options.generateIndexFiles) { await this.generateIndexFiles(collections, options, importExtension); } // Validate dependencies if requested if (options.validateDependencies) { this.validateDependencies(collections); } const statistics = this.calculateStatistics(collections, startTime); return { collections, statistics, errors: allErrors, }; } catch (error) { throw new Error(`Generation coordination failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Generate variants for a single model */ async generateModelVariants(model, options = {}, progress, importExtension = '') { const enabledVariants = options.enabledVariants || Object.values(variants_1.VariantType); const modelStartTime = Date.now(); const collection = { modelName: model.name, variants: { [variants_1.VariantType.PURE]: null, [variants_1.VariantType.INPUT]: null, [variants_1.VariantType.RESULT]: null, }, dependencies: new Set(), crossVariantReferences: { [variants_1.VariantType.PURE]: [], [variants_1.VariantType.INPUT]: [], [variants_1.VariantType.RESULT]: [], }, indexFile: { fileName: '', content: '', exports: new Set(), }, generationSummary: { totalVariants: enabledVariants.length, successfulVariants: 0, failedVariants: 0, totalErrors: 0, processingTime: 0, }, }; // Generate each enabled variant for (const variantType of enabledVariants) { try { if (progress) { progress.currentVariant = variantType; this.notifyProgress(progress); } const variantResult = await this.generateSingleVariant(model, variantType, options); collection.variants[variantType] = variantResult; // Track dependencies variantResult.dependencies.forEach((dep) => collection.dependencies.add(dep)); collection.generationSummary.successfulVariants++; if (progress) { progress.processedVariants++; } } catch (error) { const errorMessage = `Failed to generate ${variantType} variant: ${error instanceof Error ? error.message : 'Unknown error'}`; collection.generationSummary.failedVariants++; collection.generationSummary.totalErrors++; if (progress) { progress.errors.push({ model: model.name, variant: variantType, error: errorMessage, timestamp: Date.now(), }); } } } // Generate model index file collection.indexFile = this.generateModelIndexFile(collection, importExtension); // Calculate cross-variant references this.calculateCrossVariantReferences(collection); collection.generationSummary.processingTime = Date.now() - modelStartTime; return collection; } /** * Generate a single variant for a model */ async generateSingleVariant(model, variantType, options = {}) { var _a; const config = this.configManager.getVariantConfig(model.name, variantType); const naming = this.namingSystem.generateNaming(model.name, variantType, config.naming); const context = { model, variant: variantType, config, naming, outputDirectory: options.outputDirectory || './generated/schemas', typeMapper: this.typeMapper, }; // Generate schema content using the model generator const schemaComposition = this.typeMapper.generateModelSchema(model); const fileContent = this.typeMapper.generateSchemaFileContent(schemaComposition); // Debug: Log content before variant customizations if (process.env.DEBUG_PRISMA_ZOD === '1' && context.variant === variants_1.VariantType.PURE) { console.log('🔍 DEBUG: Pre-customization content for pure variant:'); console.log('Schema composition:', JSON.stringify({ modelLevelValidation: schemaComposition.modelLevelValidation, customImports: ((_a = schemaComposition.customImports) === null || _a === void 0 ? void 0 : _a.length) || 0, customImportsDetail: schemaComposition.customImports || [], }, null, 2)); console.log('File content preview:'); console.log(fileContent.content); console.log('File content dependencies:', fileContent.dependencies); } // Apply variant-specific filtering and customizations const filteredContent = this.applyVariantCustomizations(fileContent.content, context); const formattedContent = await this.formatContent(filteredContent); // Write file to disk const fullPath = `${context.outputDirectory}/${naming.filePath}`; await this.writeVariantFile(fullPath, formattedContent, options.preserveExisting); // Calculate file size (unused but kept for potential future use) // const fileSizeKB = Buffer.byteLength(formattedContent, 'utf8') / 1024; return { variantType, fileName: naming.fileName, filePath: fullPath, schemaName: naming.schemaName, typeName: naming.typeName, content: formattedContent, dependencies: fileContent.dependencies, exports: new Set([naming.schemaExportName, naming.typeExportName]), imports: new Set(['z']), // Basic zod import fieldCount: schemaComposition.fields.length, excludedFieldCount: this.calculateExcludedFields(model, context), validationCount: this.calculateValidationCount(schemaComposition), errors: [], }; } /** * Apply variant-specific customizations to content */ applyVariantCustomizations(content, context) { let customizedContent = content; const prismaImportSpecifier = this.resolveVariantPrismaImportPath(context); const escapedPrismaImport = escapeRegExp(prismaImportSpecifier); const prismaImportRemovalRe = new RegExp(`^\\s*import\\s*\\{[^}]*\\}\\s*from\\s*['\"]${escapedPrismaImport}['\"];?\\s*$`, 'gm'); // Preserve custom imports and model-level validation before applying other customizations const preservedCustomImports = this.extractCustomImports(customizedContent, prismaImportSpecifier); const preservedModelValidation = this.extractModelLevelValidation(customizedContent); // Debug logging if (process.env.DEBUG_PRISMA_ZOD === '1' && context.variant === variants_1.VariantType.PURE) { console.log('🔍 DEBUG: Extracted custom imports:', preservedCustomImports); console.log('🔍 DEBUG: Extracted model validation:', preservedModelValidation); } // Apply field exclusions const fieldExclusions = this.configManager.getEffectiveFieldExclusions(context.model.name, context.variant, context.model.fields.map((f) => f.name)); // Remove excluded fields from content fieldExclusions.excludedFields.forEach((fieldName) => { const fieldRegex = new RegExp(`^\\s*${fieldName}:.*$`, 'gm'); customizedContent = customizedContent.replace(fieldRegex, ''); }); // Apply validation customizations context.model.fields.forEach((field) => { if (!fieldExclusions.excludedFields.includes(field.name)) { const validationCustoms = this.configManager.getEffectiveValidationCustomizations(context.model.name, context.variant, field.name); if (validationCustoms.validations.length > 0) { const fieldPattern = new RegExp(`(${field.name}:\\s*z\\.[^,\\n}]+)`, 'g'); customizedContent = customizedContent.replace(fieldPattern, (match) => { const validationChain = validationCustoms.validations.join('.'); return `${match}.${validationChain}`; }); } } }); // Apply documentation customizations based on schema options if (!context.config.schemaOptions.includeDocumentation) { // Remove JSDoc comments customizedContent = customizedContent.replace(/\/\*\*[\s\S]*?\*\//g, ''); } // Enum handling strategy: // - For pure variant files the tests expect native enum value import from @prisma/client (e.g. import { Role } ... and z.enum(Role)). // - For other variants (input/result) we continue to rewrite to generated enum schemas (RoleSchema) to stay consistent with model generator. const enumUsageRe = /z\.(?:enum|nativeEnum)\(([_A-Za-z][_A-Za-z0-9]*)\)/g; const usedEnumNames = []; if (context.variant === variants_1.VariantType.PURE) { // Variant base content currently uses generated enum schemas (RoleSchema). Convert them back to native enum usage. // 1. Detect imports of generated enum schemas and extract enum names. const enumSchemaImportRe = /import\s*\{\s*([A-Za-z0-9_]+)Schema\s*\}\s*from\s*['"].*?\/enums\/[A-Za-z0-9_]+\.schema(?:\.[a-z]+)?['"];?\n?/g; customizedContent = customizedContent.replace(enumSchemaImportRe, (_full, enumBase) => { if (!usedEnumNames.includes(enumBase)) usedEnumNames.push(enumBase); return ''; // remove the schema import }); // 2. Replace occurrences of EnumNameSchema with z.enum(EnumName) usedEnumNames.forEach((enumName) => { const schemaRefRe = new RegExp(`${enumName}Schema`, 'g'); customizedContent = customizedContent.replace(schemaRefRe, `z.enum(${enumName})`); }); // 3. Collect any remaining direct z.enum/nativeEnum(Enum) patterns (in case mapper changed) to include in import list let match; while ((match = enumUsageRe.exec(customizedContent)) !== null) { const enumName = match[1]; if (!usedEnumNames.includes(enumName)) usedEnumNames.push(enumName); } if (usedEnumNames.length > 0) { // Remove existing Prisma Client enum imports to avoid duplication customizedContent = customizedContent.replace(prismaImportRemovalRe, ''); // Insert consolidated import after zod import customizedContent = customizedContent.replace(/(import\s*\{\s*z\s*\}\s*from\s*['"]zod['"]\s*;?)/, (m) => `${m}\nimport { ${usedEnumNames.join(', ')} } from '${prismaImportSpecifier}';`); } } else { // Non-pure variants: Need to handle both direct enum schema references and enum references in custom.use expressions // First find all enum references that need schema imports customizedContent = customizedContent.replace(enumUsageRe, (_m, enumName) => { if (!usedEnumNames.includes(enumName)) usedEnumNames.push(enumName); return `${enumName}Schema`; }); // Also scan for enum names that might be referenced directly without z.enum() wrapper // This handles cases where @zod.custom.use contains direct enum references const enumSchemaImportRe = /import\s*\{\s*([A-Za-z0-9_]+)Schema\s*\}\s*from\s*['"].*?\/enums\/[A-Za-z0-9_]+\.schema(?:\.[a-z]+)?['"];?\n?/g; let importMatch; while ((importMatch = enumSchemaImportRe.exec(customizedContent)) !== null) { const enumBase = importMatch[1]; if (!usedEnumNames.includes(enumBase)) usedEnumNames.push(enumBase); } if (usedEnumNames.length > 0) { // Remove existing Prisma Client enum imports to avoid duplication customizedContent = customizedContent.replace(prismaImportRemovalRe, ''); // Ensure enum schema imports are present (handle missing imports for variants) const missingEnumImports = []; usedEnumNames.forEach((enumName) => { const hasImport = customizedContent.includes(`import { ${enumName}Schema }`); if (!hasImport) { missingEnumImports.push(enumName); } }); if (missingEnumImports.length > 0) { customizedContent = customizedContent.replace(/(import\s*\{\s*z\s*\}\s*from\s*['"]zod['"]\s*;?)/, (match) => { var _a; try { const cfg = (_a = transformer_1.default.getGeneratorConfig) === null || _a === void 0 ? void 0 : _a.call(transformer_1.default); const enumNaming = (0, naming_resolver_1.resolveEnumNaming)(cfg); const ext = transformer_1.default.getImportFileExtension(); const importLines = missingEnumImports .map((name) => { const fileName = (0, naming_resolver_1.generateFileName)(enumNaming.filePattern, name, undefined, undefined, name); const base = fileName.replace(/\.ts$/, ''); const exportName = (0, naming_resolver_1.generateExportName)(enumNaming.exportNamePattern, name, undefined, undefined, name); // Only alias when export name differs from expected <Enum>Schema if (exportName === `${name}Schema`) { return `import { ${exportName} } from '../enums/${base}${ext}';`; } else { return `import { ${exportName} as ${name}Schema } from '../enums/${base}${ext}';`; } }) .join('\n'); return `${match}\n${importLines}`; } catch { // Fallback to legacy hardcoded schema names if naming resolution fails const ext = transformer_1.default.getImportFileExtension ? transformer_1.default.getImportFileExtension() : ''; const importLines = missingEnumImports .map((name) => `import { ${name}Schema } from '../enums/${name}.schema${ext}';`) .join('\n'); return `${match}\n${importLines}`; } }); } } } // Note: .partial() support is handled during template generation in prisma-generator.ts // This avoids fragile regex patterns that break with custom naming conventions // Restore preserved custom imports and model-level validation customizedContent = this.restoreCustomImports(customizedContent, preservedCustomImports); customizedContent = this.restoreModelLevelValidation(customizedContent, preservedModelValidation); return customizedContent; } /** * Extract custom imports from content (non-enum related) */ extractCustomImports(content, prismaImportSpecifier) { const customImports = []; // Match import statements that aren't zod, @prisma/client, or enum schema imports const importRegex = /^import\s+.*?from\s+['"][^'"]*['"];?\s*$/gm; let match; while ((match = importRegex.exec(content)) !== null) { const importLine = match[0]; // Skip standard imports if (importLine.includes("from 'zod") || importLine.includes('from "zod') || importLine.includes(`from '${prismaImportSpecifier}`) || importLine.includes(`from "${prismaImportSpecifier}`) || importLine.includes("from '@prisma/client") || importLine.includes('from "@prisma/client') || importLine.includes('/enums/') || importLine.includes('type { Prisma }') || importLine.includes('JsonNullValue') || importLine.includes('json-helpers')) { continue; } // This is likely a custom import customImports.push(importLine); } return customImports; } /** * Extract model-level validation from content */ extractModelLevelValidation(content) { // Look for .refine() calls at the end of object definitions, accounting for .strict() const refineRegex = /\}\)\.strict\(\)\.refine\([^;]+\);/; const match = content.match(refineRegex); return match ? match[0] : null; } /** * Restore custom imports to content */ restoreCustomImports(content, customImports) { if (customImports.length === 0) { return content; } // Insert custom imports after the zod import const zodImportRegex = /(import\s*\*\s*as\s*z\s*from\s*['"]zod[^'"]*['"];?\s*)/; const match = content.match(zodImportRegex); if (match) { const insertPosition = match.index + match[0].length; const customImportLines = customImports.join('\n') + '\n'; return content.slice(0, insertPosition) + customImportLines + content.slice(insertPosition); } return content; } /** * Restore model-level validation to content */ restoreModelLevelValidation(content, modelValidation) { if (!modelValidation) { return content; } // Replace the .strict() ending with the model validation const strictRegex = /\}\)\.strict\(\);/; if (strictRegex.test(content)) { // Extract the .refine() part from the full validation const refineMatch = modelValidation.match(/\.refine\([^;]+\);/); if (refineMatch) { return content.replace(strictRegex, `}).strict()${refineMatch[0]}`); } } return content; } resolveVariantPrismaImportPath(context) { var _a, _b, _c; const baseDir = path_1.default.isAbsolute(context.outputDirectory) ? context.outputDirectory : path_1.default.resolve((_b = (_a = transformer_1.default.getOutputPath) === null || _a === void 0 ? void 0 : _a.call(transformer_1.default)) !== null && _b !== void 0 ? _b : process.cwd(), context.outputDirectory); const variantDir = ((_c = context.naming) === null || _c === void 0 ? void 0 : _c.filePath) ? path_1.default.dirname(context.naming.filePath) : ''; const targetDir = variantDir ? path_1.default.resolve(baseDir, variantDir) : baseDir; return transformer_1.default.resolvePrismaImportPath(targetDir); } /** * Generate model index file */ generateModelIndexFile(collection, importExtension = '') { const exports = new Set(); const imports = []; // Collect exports from all variants Object.entries(collection.variants).forEach(([_variantType, result]) => { if (result) { imports.push(`export { ${result.schemaName}, ${result.typeName} } from './${result.fileName.replace('.ts', '')}${importExtension}';`); exports.add(result.schemaName); exports.add(result.typeName); } }); const content = [ '/**', ` * ${collection.modelName} Schema Variants Index`, ` * Auto-generated file - do not edit manually`, ` * Generated at: ${new Date().toISOString()}`, ' */', '', ...imports, '', ].join('\n'); return { fileName: `${collection.modelName.toLowerCase()}.ts`, content, exports, }; } /** * Generate global index files for all variants */ async generateIndexFiles(collections, options, importExtension = '') { const outputDir = options.outputDirectory || './generated/schemas'; // Generate variant-specific index files for (const variantType of Object.values(variants_1.VariantType)) { const variantExports = []; collections.forEach((collection) => { const variant = collection.variants[variantType]; if (variant) { variantExports.push(`export { ${variant.schemaName}, ${variant.typeName} } from './${variantType}/${variant.fileName.replace('.ts', '')}${importExtension}';`); } }); if (variantExports.length > 0) { const variantIndexContent = [ '/**', ` * ${variantType.toUpperCase()} Variant Schemas Index`, ` * Auto-generated file - do not edit manually`, ` * Generated at: ${new Date().toISOString()}`, ' */', '', ...variantExports, '', ].join('\n'); const variantIndexPath = `${outputDir}/${variantType}.ts`; await this.writeVariantFile(variantIndexPath, variantIndexContent); } } // Generate main index file const mainIndexContent = [ '/**', ' * All Schema Variants Index', ' * Auto-generated file - do not edit manually', ` * Generated at: ${new Date().toISOString()}`, ' */', '', ...Object.values(variants_1.VariantType).map((variant) => { return `export * from './${variant}${importExtension}';`; }), '', ].join('\n'); const mainIndexPath = `${outputDir}/index.ts`; await this.writeVariantFile(mainIndexPath, mainIndexContent); } /** * Calculate cross-variant references */ calculateCrossVariantReferences(collection) { // This would analyze which variants reference each other // For now, implement basic logic Object.keys(collection.variants).forEach((variantType) => { const variant = variantType; collection.crossVariantReferences[variant] = []; // Pure variants might be referenced by input/result variants if (variant === variants_1.VariantType.PURE) { if (collection.variants[variants_1.VariantType.INPUT]) { collection.crossVariantReferences[variant].push(variants_1.VariantType.INPUT); } if (collection.variants[variants_1.VariantType.RESULT]) { collection.crossVariantReferences[variant].push(variants_1.VariantType.RESULT); } } }); } /** * Helper methods */ calculateTotalVariants(models, options) { const enabledVariants = options.enabledVariants || Object.values(variants_1.VariantType); return models.length * enabledVariants.length; } calculateExcludedFields(model, context) { const exclusions = this.configManager.getEffectiveFieldExclusions(context.model.name, context.variant, model.fields.map((f) => f.name)); return exclusions.excludedFields.length; } calculateValidationCount(composition) { var _a; return ((_a = composition.statistics) === null || _a === void 0 ? void 0 : _a.enhancedFields) || 0; } async formatContent(content) { try { return await (0, formatFile_1.formatFile)(content); } catch { // Return unformatted content if formatting fails return content; } } async writeVariantFile(filePath, content, preserveExisting) { if (preserveExisting) { // Check if file exists and skip if it does try { await fs_1.promises.access(filePath); return; // File exists, skip writing } catch { // File doesn't exist, proceed with writing } } await (0, writeFileSafely_1.writeFileSafely)(filePath, content); } validateDependencies(collections) { // Implement dependency validation logic const allModels = new Set(collections.map((c) => c.modelName)); collections.forEach((collection) => { collection.dependencies.forEach((dep) => { if (!allModels.has(dep)) { throw new Error(`Model ${collection.modelName} depends on ${dep} which is not being generated`); } }); }); } calculateStatistics(collections, startTime) { const totalTime = Date.now() - startTime; const variantCounts = { [variants_1.VariantType.PURE]: 0, [variants_1.VariantType.INPUT]: 0, [variants_1.VariantType.RESULT]: 0, }; const errorCounts = { [variants_1.VariantType.PURE]: 0, [variants_1.VariantType.INPUT]: 0, [variants_1.VariantType.RESULT]: 0, }; const filesSizesKB = {}; const dependencyGraph = {}; collections.forEach((collection) => { Object.entries(collection.variants).forEach(([variant, result]) => { if (result) { variantCounts[variant]++; filesSizesKB[result.filePath] = Buffer.byteLength(result.content, 'utf8') / 1024; } }); dependencyGraph[collection.modelName] = Array.from(collection.dependencies); }); return { totalTime, averageModelTime: totalTime / collections.length, variantCounts, errorCounts, filesSizesKB, dependencyGraph, }; } notifyProgress(progress) { this.progressCallbacks.forEach((callback) => { try { callback(progress); } catch { // Ignore callback errors to prevent disrupting generation } }); } /** * Public API methods */ /** * Add progress callback */ onProgress(callback) { this.progressCallbacks.push(callback); } /** * Remove progress callback */ removeProgressCallback(callback) { const index = this.progressCallbacks.indexOf(callback); if (index > -1) { this.progressCallbacks.splice(index, 1); } } /** * Get current configuration */ getConfiguration() { return this.configManager; } /** * Update configuration */ updateConfiguration(configManager) { this.configManager = configManager; } } exports.VariantFileGenerationCoordinator = VariantFileGenerationCoordinator; exports.default = VariantFileGenerationCoordinator; //# sourceMappingURL=generator.js.map