prisma-zod-generator
Version:
Prisma 2+ generator to emit Zod schemas from your Prisma schema
673 lines • 31.9 kB
JavaScript
;
/**
* 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