UNPKG

prisma-zod-generator

Version:

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

1,041 lines 117 kB
"use strict"; /** * Pure Model Schema Generator * * Generates Zod schemas representing the raw Prisma model structure, * similar to zod-prisma functionality but with enhanced inline validation support. */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.PrismaTypeMapper = exports.DEFAULT_TYPE_MAPPING_CONFIG = void 0; const path_1 = __importDefault(require("path")); const zod_comments_1 = require("../parsers/zod-comments"); const logger_1 = require("../utils/logger"); const naming_resolver_1 = require("../utils/naming-resolver"); /** * Default type mapping configuration */ exports.DEFAULT_TYPE_MAPPING_CONFIG = { decimalMode: 'decimal', jsonMode: 'unknown', strictDateValidation: true, validateBigInt: true, includeDatabaseValidations: true, provider: 'postgresql', zodImportTarget: 'auto', complexTypes: { decimal: { validatePrecision: true, maxPrecision: 18, maxScale: 8, allowNegative: true, }, dateTime: { allowFuture: true, allowPast: true, timezoneMode: 'preserve', }, json: { maxDepth: 10, allowNull: true, validateStructure: false, }, bytes: { maxSize: 16 * 1024 * 1024, // 16MB minSize: 0, useBase64: true, }, }, }; /** * Prisma field type mapper */ class PrismaTypeMapper { constructor(config = {}) { var _a, _b, _c, _d; // Deep merge for complex nested configuration this.config = { ...exports.DEFAULT_TYPE_MAPPING_CONFIG, ...config, complexTypes: { ...exports.DEFAULT_TYPE_MAPPING_CONFIG.complexTypes, ...config.complexTypes, decimal: { ...exports.DEFAULT_TYPE_MAPPING_CONFIG.complexTypes.decimal, ...(_a = config.complexTypes) === null || _a === void 0 ? void 0 : _a.decimal, }, dateTime: { ...exports.DEFAULT_TYPE_MAPPING_CONFIG.complexTypes.dateTime, ...(_b = config.complexTypes) === null || _b === void 0 ? void 0 : _b.dateTime, }, json: { ...exports.DEFAULT_TYPE_MAPPING_CONFIG.complexTypes.json, ...(_c = config.complexTypes) === null || _c === void 0 ? void 0 : _c.json, }, bytes: { ...exports.DEFAULT_TYPE_MAPPING_CONFIG.complexTypes.bytes, ...(_d = config.complexTypes) === null || _d === void 0 ? void 0 : _d.bytes, }, }, }; } /** * Map a Prisma field to Zod schema * * @param field - Prisma DMMF field * @param model - Parent model for context * @returns Field type mapping result */ mapFieldToZodSchema(field, model) { const result = { zodSchema: '', imports: new Set(['z']), additionalValidations: [], requiresSpecialHandling: false, }; try { // Check for custom schema replacements first (before any type-specific processing) if (field.documentation) { // Fast-path: support custom full schema replacement via @zod.custom.use(<expr>) const customUseMatch = field.documentation.match(/@zod\.custom\.use\(((?:[^()]|\([^)]*\))*)\)(.*)$/m); if (customUseMatch) { const baseExpression = customUseMatch[1].trim(); const chainedMethods = customUseMatch[2].trim(); if (baseExpression) { let fullExpression = baseExpression; if (chainedMethods) { fullExpression += chainedMethods; } result.zodSchema = fullExpression; result.additionalValidations.push('// Replaced base schema via @zod.custom.use'); result.requiresSpecialHandling = true; return result; // Skip all other processing } } // Fast-path: support custom object schema via @zod.custom({ ... }) const customMatch = field.documentation.match(/@zod\.custom\(((?:\{[^}]*\}|\[[^\]]*\]|(?:[^()]|\([^)]*\))*?))\)(.*)$/m); if (customMatch) { const objectExpression = customMatch[1].trim(); const chainedMethods = customMatch[2].trim(); if (objectExpression) { let zodSchema; if (objectExpression.startsWith('{')) { // Convert JSON object to z.object() try { const parsedObject = JSON.parse(objectExpression); const zodObject = this.convertObjectToZodSchema(parsedObject); zodSchema = `z.object(${zodObject})`; } catch { // If JSON parsing fails, preserve the raw expression zodSchema = `z.object(${objectExpression})`; } } else if (objectExpression.startsWith('[')) { // Convert JSON array to z.array() try { const parsedArray = JSON.parse(objectExpression); const zodArray = this.convertArrayToZodSchema(parsedArray); zodSchema = `z.array(${zodArray})`; } catch { // If JSON parsing fails, preserve the raw expression zodSchema = `z.array(${objectExpression})`; } } else { // For other expressions, use them directly zodSchema = objectExpression; } // Add any chained methods if (chainedMethods) { zodSchema += chainedMethods; } result.zodSchema = zodSchema; result.additionalValidations.push('// Replaced base schema via @zod.custom'); result.requiresSpecialHandling = true; return result; // Skip all other processing } } } // Handle scalar types if (field.kind === 'scalar') { this.mapScalarType(field, result, model); } // Handle enum types else if (field.kind === 'enum') { this.mapEnumType(field, result); } // Handle object types (relations) else if (field.kind === 'object') { this.mapObjectType(field, model, result); } // Handle unsupported types else { this.mapUnsupportedType(field, result); } // Apply list wrapper if needed BEFORE inline validations // This ensures @zod.nullable() applies to the array itself, not the elements if (field.isList) { this.applyListWrapper(result); } // Apply inline validation from @zod comments AFTER list wrapper this.applyInlineValidations(field, result, model.name); // Apply enhanced optionality handling const optionalityResult = this.determineFieldOptionality(field, model); if (optionalityResult.isOptional || optionalityResult.hasDefaultValue) { this.applyEnhancedOptionalityWrapper(result, optionalityResult); } // Generate comprehensive JSDoc documentation this.generateJSDocumentation(field, result, model.name, optionalityResult); // Add database-specific validations if (this.config.includeDatabaseValidations) { this.addDatabaseValidations(field, result); } } catch (error) { // Fallback to string type on mapping error console.warn(`Failed to map field ${field.name} of type ${field.type}:`, error); const isJsonSchemaCompatible = this.config.jsonSchemaCompatible; result.zodSchema = isJsonSchemaCompatible ? 'z.any()' : 'z.unknown()'; result.additionalValidations.push(`// Warning: Failed to map type ${field.type}, using ${isJsonSchemaCompatible ? 'any' : 'unknown'}`); } return result; } /** * Map scalar types to Zod schemas */ mapScalarType(field, result, model) { var _a, _b, _c; const scalarType = field.type; switch (scalarType) { case 'String': result.zodSchema = 'z.string()'; break; case 'Int': result.zodSchema = 'z.number().int()'; result.additionalValidations.push('// Integer validation applied'); break; case 'BigInt': // Check for JSON Schema compatibility mode let cfg = null; try { // eslint-disable-next-line @typescript-eslint/no-require-imports -- lazy require to avoid circular import const transformer = require('../transformer').default; cfg = (_a = transformer.getGeneratorConfig) === null || _a === void 0 ? void 0 : _a.call(transformer); } catch { /* ignore */ } if (cfg === null || cfg === void 0 ? void 0 : cfg.jsonSchemaCompatible) { const format = ((_b = cfg.jsonSchemaOptions) === null || _b === void 0 ? void 0 : _b.bigIntFormat) || 'string'; if (format === 'string') { result.zodSchema = 'z.string().regex(/^\\d+$/, "Invalid bigint string")'; result.additionalValidations.push('// BigInt as string for JSON Schema compatibility'); } else { result.zodSchema = 'z.number().int()'; result.additionalValidations.push('// BigInt as number for JSON Schema compatibility (may lose precision)'); } } else { result.zodSchema = 'z.bigint()'; if (this.config.validateBigInt) { result.additionalValidations.push('// BigInt validation enabled'); } } break; case 'Float': result.zodSchema = 'z.number()'; break; case 'Decimal': this.mapDecimalType(field, result, model.name); break; case 'Boolean': result.zodSchema = 'z.boolean()'; break; case 'DateTime': this.mapDateTimeType(field, result); break; case 'Json': this.mapJsonType(field, result); break; case 'Bytes': this.mapBytesType(field, result); break; default: // Check for custom type mappings if ((_c = this.config.customTypeMappings) === null || _c === void 0 ? void 0 : _c[scalarType]) { result.zodSchema = this.config.customTypeMappings[scalarType]; result.requiresSpecialHandling = true; } else { // Unknown scalar type - fallback to string result.zodSchema = 'z.string()'; result.additionalValidations.push(`// Unknown scalar type: ${scalarType}, mapped to string`); } break; } } /** * Map Decimal type with enhanced validation based on configuration */ mapDecimalType(field, result, modelName) { const decimalConfig = this.config.complexTypes.decimal; // Default to 'decimal' mode if not specified const mode = this.config.decimalMode || 'decimal'; switch (mode) { case 'string': { result.zodSchema = 'z.string()'; // Build precision-aware regex pattern let regexPattern = '^'; if (decimalConfig.allowNegative) { regexPattern += '-?'; } if (decimalConfig.validatePrecision && decimalConfig.maxPrecision) { const maxIntegerDigits = decimalConfig.maxPrecision - (decimalConfig.maxScale || 0); const maxScaleDigits = decimalConfig.maxScale || 0; if (maxScaleDigits > 0) { regexPattern += `\\d{1,${maxIntegerDigits}}(?:\\.\\d{1,${maxScaleDigits}})?`; } else { regexPattern += `\\d{1,${maxIntegerDigits}}`; } } else { regexPattern += '\\d*\\.?\\d+'; } regexPattern += '$'; result.additionalValidations.push(`.regex(/${regexPattern}/, "Invalid decimal format")`); // Add precision validation documentation if (decimalConfig.validatePrecision) { result.additionalValidations.push(`// Precision: max ${decimalConfig.maxPrecision} digits, scale ${decimalConfig.maxScale}`); } if (!decimalConfig.allowNegative) { result.additionalValidations.push('// Positive values only'); } break; } case 'number': result.zodSchema = 'z.number()'; // Add number-specific validations if (!decimalConfig.allowNegative) { result.additionalValidations.push('.min(0, "Negative values not allowed")'); } // Add precision warnings for number mode result.additionalValidations.push('// Warning: Decimal as number - precision may be lost for large values'); if (decimalConfig.validatePrecision && decimalConfig.maxPrecision && decimalConfig.maxPrecision > 15) { result.additionalValidations.push('// Warning: JavaScript numbers lose precision beyond 15-16 digits'); } break; case 'decimal': { // Full Decimal.js support matching zod-prisma-types // For pure models, use instanceof(Prisma.Decimal) const modelContext = modelName ? `, { message: "Field '${field.name}' must be a Decimal. Location: ['Models', '${modelName}']", }` : ''; result.zodSchema = `z.instanceof(Prisma.Decimal${modelContext})`; result.additionalValidations.push('// Decimal field using Prisma.Decimal type'); result.requiresSpecialHandling = true; // Mark that we need Prisma import (non-type import) // Note: The import system expects just the identifier, not the full import statement result.imports.add('Prisma'); break; } default: result.zodSchema = 'z.string()'; result.additionalValidations.push(`.regex(/^${decimalConfig.allowNegative ? '-?' : ''}\\d*\\.?\\d+$/, "Invalid decimal format")`); break; } if (mode !== 'decimal') { result.requiresSpecialHandling = true; result.additionalValidations.push(`// Decimal field mapped as ${mode} with enhanced validation`); } } /** * Map DateTime type with enhanced validation and timezone handling */ mapDateTimeType(field, result) { var _a, _b; const dateTimeConfig = this.config.complexTypes.dateTime; // Respect global generator dateTimeStrategy if available let strategy = 'date'; let cfg = null; try { // Lazy load transformer to avoid circular import at module load // eslint-disable-next-line @typescript-eslint/no-require-imports -- lazy require to avoid circular import at module top level const transformer = require('../transformer').default; cfg = (_a = transformer.getGeneratorConfig) === null || _a === void 0 ? void 0 : _a.call(transformer); if (cfg === null || cfg === void 0 ? void 0 : cfg.dateTimeStrategy) strategy = cfg.dateTimeStrategy; } catch { /* ignore */ } // JSON Schema compatibility mode overrides all other strategies if (cfg === null || cfg === void 0 ? void 0 : cfg.jsonSchemaCompatible) { const format = ((_b = cfg.jsonSchemaOptions) === null || _b === void 0 ? void 0 : _b.dateTimeFormat) || 'isoString'; if (format === 'isoDate') { result.zodSchema = 'z.string().regex(/^\\d{4}-\\d{2}-\\d{2}$/, "Invalid ISO date")'; result.additionalValidations.push('// DateTime as ISO date string for JSON Schema compatibility'); } else { // isoString - no transform for JSON Schema compatibility result.zodSchema = 'z.string().regex(/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$/, "Invalid ISO datetime")'; result.additionalValidations.push('// DateTime as ISO string for JSON Schema compatibility'); } return; } if (strategy === 'isoString') { result.zodSchema = 'z.string().regex(/\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z/, "Invalid ISO datetime").transform(v => new Date(v))'; result.additionalValidations.push('// DateTime mapped from ISO string'); } else if (strategy === 'coerce') { result.zodSchema = 'z.coerce.date()'; result.additionalValidations.push('// DateTime coerced from input'); } else { if (this.config.strictDateValidation) { result.zodSchema = 'z.date()'; result.additionalValidations.push('// Strict date validation enabled'); } else { result.zodSchema = 'z.union([z.date(), z.string().datetime()])'; result.additionalValidations.push('// Flexible date/string input with ISO 8601 validation'); } } // Add date range validations const validations = []; if (dateTimeConfig.minDate) { try { const minDate = new Date(dateTimeConfig.minDate); validations.push(`.min(new Date("${dateTimeConfig.minDate}"), "Date must be after ${minDate.toLocaleDateString()}")`); result.additionalValidations.push(`// Minimum date: ${dateTimeConfig.minDate}`); } catch { result.additionalValidations.push(`// Warning: Invalid minDate format: ${dateTimeConfig.minDate}`); } } if (dateTimeConfig.maxDate) { try { const maxDate = new Date(dateTimeConfig.maxDate); validations.push(`.max(new Date("${dateTimeConfig.maxDate}"), "Date must be before ${maxDate.toLocaleDateString()}")`); result.additionalValidations.push(`// Maximum date: ${dateTimeConfig.maxDate}`); } catch { result.additionalValidations.push(`// Warning: Invalid maxDate format: ${dateTimeConfig.maxDate}`); } } if (!dateTimeConfig.allowFuture) { validations.push('.max(new Date(), "Future dates not allowed")'); result.additionalValidations.push('// Future dates not allowed'); } if (!dateTimeConfig.allowPast) { validations.push('.min(new Date(), "Past dates not allowed")'); result.additionalValidations.push('// Past dates not allowed'); } // Apply date validations to the schema if (validations.length > 0) { if (this.config.strictDateValidation) { // For strict validation, apply directly to date result.additionalValidations.push(...validations.map((v) => v.replace('.', '.refine((date) => date'))); } else { // For flexible validation, need to handle both date and string result.additionalValidations.push('// Date range validations applied to Date objects only'); } } // Add timezone handling documentation switch (dateTimeConfig.timezoneMode) { case 'utc': result.additionalValidations.push('// Timezone: All dates normalized to UTC'); break; case 'local': result.additionalValidations.push('// Timezone: All dates converted to local timezone'); break; case 'preserve': result.additionalValidations.push('// Timezone: Original timezone information preserved'); break; } result.requiresSpecialHandling = true; } /** * Map JSON type with enhanced validation and structure checking */ mapJsonType(field, result) { const jsonConfig = this.config.complexTypes.json; const isJsonSchemaCompatible = this.config.jsonSchemaCompatible; switch (this.config.jsonMode) { case 'unknown': result.zodSchema = isJsonSchemaCompatible ? 'z.any()' : 'z.unknown()'; break; case 'record': if (jsonConfig.allowNull) { result.zodSchema = isJsonSchemaCompatible ? 'z.record(z.any()).nullable()' : 'z.record(z.unknown()).nullable()'; } else { result.zodSchema = isJsonSchemaCompatible ? 'z.record(z.any())' : 'z.record(z.unknown())'; } break; case 'any': result.zodSchema = 'z.any()'; break; default: result.zodSchema = isJsonSchemaCompatible ? 'z.any()' : 'z.unknown()'; break; } // Add JSON-specific validations const validations = []; if (jsonConfig.validateStructure) { // Add custom JSON validation validations.push('.refine((val) => { try { JSON.stringify(val); return true; } catch { return false; } }, "Must be valid JSON serializable data")'); result.additionalValidations.push('// JSON structure validation enabled'); } if (jsonConfig.maxDepth !== undefined && jsonConfig.maxDepth > 0) { // Add depth validation function const depthValidation = `.refine((val) => { const getDepth = (obj: unknown, depth: number = 0): number => { if (depth > ${jsonConfig.maxDepth}) return depth; if (obj === null || typeof obj !== 'object') return depth; const values = Object.values(obj as Record<string, unknown>); if (values.length === 0) return depth; return Math.max(...values.map(v => getDepth(v, depth + 1))); }; return getDepth(val) <= ${jsonConfig.maxDepth}; }, "JSON nesting depth exceeds maximum of ${jsonConfig.maxDepth}")`; validations.push(depthValidation); result.additionalValidations.push(`// Maximum nesting depth: ${jsonConfig.maxDepth}`); } if (jsonConfig.maxLength !== undefined && jsonConfig.maxLength > 0) { // Add length validation for JSON string representation validations.push(`.refine((val) => JSON.stringify(val).length <= ${jsonConfig.maxLength}, "JSON string representation too long")`); result.additionalValidations.push(`// Maximum JSON string length: ${jsonConfig.maxLength} characters`); } // Apply validations if any if (validations.length > 0) { result.zodSchema = `${result.zodSchema}${validations.join('')}`; } // Add null handling information if (!jsonConfig.allowNull && this.config.jsonMode === 'record') { result.additionalValidations.push('// Null values not allowed in JSON structure'); } else if (jsonConfig.allowNull) { result.additionalValidations.push('// Null values allowed in JSON structure'); } result.requiresSpecialHandling = true; result.additionalValidations.push(`// JSON field mapped as ${this.config.jsonMode} with enhanced validation`); } /** * Map Bytes type with enhanced validation for binary data and file handling */ mapBytesType(field, result) { var _a, _b; const bytesConfig = this.config.complexTypes.bytes; // Check for JSON Schema compatibility mode first let cfg = null; try { // eslint-disable-next-line @typescript-eslint/no-require-imports -- lazy require to avoid circular import const transformer = require('../transformer').default; cfg = (_a = transformer.getGeneratorConfig) === null || _a === void 0 ? void 0 : _a.call(transformer); } catch { /* ignore */ } if (cfg === null || cfg === void 0 ? void 0 : cfg.jsonSchemaCompatible) { const format = ((_b = cfg.jsonSchemaOptions) === null || _b === void 0 ? void 0 : _b.bytesFormat) || 'base64String'; if (format === 'base64String') { result.zodSchema = 'z.string().regex(/^[A-Za-z0-9+/]*={0,2}$/, "Invalid base64 string")'; result.additionalValidations.push('// Bytes as base64 string for JSON Schema compatibility'); } else { result.zodSchema = 'z.string().regex(/^[0-9a-fA-F]*$/, "Invalid hex string")'; result.additionalValidations.push('// Bytes as hex string for JSON Schema compatibility'); } return; } // For better compatibility with consumers and tests, prefer base64 string mapping by default if (bytesConfig.useBase64 !== false) { // Use base64 string representation result.zodSchema = 'z.string()'; // Add base64 validation result.additionalValidations.push('.regex(/^[A-Za-z0-9+/]*={0,2}$/, "Must be valid base64 string")'); // Add size validations for base64 if (bytesConfig.minSize !== undefined && bytesConfig.minSize > 0) { // Base64 encoding: 4 chars for every 3 bytes, so minSize * 4/3 const minBase64Length = Math.ceil((bytesConfig.minSize * 4) / 3); result.additionalValidations.push(`.min(${minBase64Length}, "Base64 string too short")`); result.additionalValidations.push(`// Minimum size: ${bytesConfig.minSize} bytes`); } if (bytesConfig.maxSize !== undefined && bytesConfig.maxSize > 0) { // Base64 encoding: 4 chars for every 3 bytes, so maxSize * 4/3 const maxBase64Length = Math.ceil((bytesConfig.maxSize * 4) / 3); result.additionalValidations.push(`.max(${maxBase64Length}, "Base64 string too long")`); result.additionalValidations.push(`// Maximum size: ${bytesConfig.maxSize} bytes (${this.formatFileSize(bytesConfig.maxSize)})`); } result.additionalValidations.push('// Bytes field mapped to base64 string'); } else { // Use Uint8Array (compatible with Prisma Bytes type) if (this.config.provider === 'mongodb') { result.zodSchema = 'z.instanceof(Uint8Array)'; } else { result.zodSchema = 'z.instanceof(Uint8Array)'; } // Add size validations for binary data (Uint8Array) const validations = []; if (bytesConfig.minSize !== undefined && bytesConfig.minSize > 0) { validations.push(`.refine((buffer) => buffer.length >= ${bytesConfig.minSize}, "File too small")`); result.additionalValidations.push(`// Minimum size: ${bytesConfig.minSize} bytes`); } if (bytesConfig.maxSize !== undefined && bytesConfig.maxSize > 0) { validations.push(`.refine((buffer) => buffer.length <= ${bytesConfig.maxSize}, "File too large")`); result.additionalValidations.push(`// Maximum size: ${bytesConfig.maxSize} bytes (${this.formatFileSize(bytesConfig.maxSize)})`); } // Apply size validations if (validations.length > 0) { result.additionalValidations.push(...validations); } result.additionalValidations.push('// Bytes field mapped to Uint8Array'); } // Add MIME type validation if specified if (bytesConfig.allowedMimeTypes && bytesConfig.allowedMimeTypes.length > 0) { result.additionalValidations.push(`// Allowed MIME types: ${bytesConfig.allowedMimeTypes.join(', ')}`); if (!bytesConfig.useBase64) { // For binary types, we can add file type validation (this would require file-type detection) result.additionalValidations.push('// Note: MIME type validation requires additional file-type detection library'); } } result.requiresSpecialHandling = true; result.additionalValidations.push(`// Bytes field with enhanced validation (${bytesConfig.useBase64 ? 'base64' : 'Uint8Array'})`); } /** * Format file size in human-readable format */ formatFileSize(bytes) { const units = ['B', 'KB', 'MB', 'GB']; let size = bytes; let unitIndex = 0; while (size >= 1024 && unitIndex < units.length - 1) { size /= 1024; unitIndex++; } return `${size.toFixed(unitIndex > 0 ? 1 : 0)}${units[unitIndex]}`; } /** * Map enum types */ mapEnumType(field, result) { var _a, _b; const enumName = field.type; // Use proper enum naming resolution instead of hardcoded "Schema" suffix try { // eslint-disable-next-line @typescript-eslint/no-require-imports const { resolveEnumNaming, generateExportName } = require('../utils/naming-resolver'); // Access the global transformer config like done elsewhere in this file // eslint-disable-next-line @typescript-eslint/no-require-imports const cfg = (_b = (_a = require('../transformer').default).getGeneratorConfig) === null || _b === void 0 ? void 0 : _b.call(_a); const enumNaming = resolveEnumNaming(cfg); const actualExportName = generateExportName(enumNaming.exportNamePattern, enumName, undefined, undefined, enumName); result.zodSchema = actualExportName; result.imports.add(actualExportName); } catch { // Fallback to the old pattern if naming resolution fails result.zodSchema = `${enumName}Schema`; result.imports.add(`${enumName}Schema`); } result.additionalValidations.push(`// Enum type: ${enumName}`); } /** * Map object types (relations) */ mapObjectType(field, model, result) { var _a, _b, _c; const relatedModelName = field.type; // For pure model schemas, we typically don't include full relation objects // Instead, we might include just the foreign key fields or omit relations entirely if (field.relationName) { // Determine the correct export symbol for the related model based on naming config let relatedExportName = `${relatedModelName}Schema`; try { // eslint-disable-next-line @typescript-eslint/no-require-imports const { resolvePureModelNaming, applyPattern } = require('../utils/naming-resolver'); // eslint-disable-next-line @typescript-eslint/no-require-imports const transformer = require('../transformer'); const cfg = transformer.Transformer ? transformer.Transformer.getGeneratorConfig() : (_a = transformer.default) === null || _a === void 0 ? void 0 : _a.getGeneratorConfig(); const namingResolved = resolvePureModelNaming(cfg); relatedExportName = applyPattern(namingResolved.exportNamePattern, relatedModelName, namingResolved.schemaSuffix, namingResolved.typeSuffix); } catch { relatedExportName = `${relatedModelName}Schema`; } // Determine zod target to choose recursion strategy let target = 'auto'; try { // eslint-disable-next-line @typescript-eslint/no-require-imports const transformer = require('../transformer').default; target = ((_c = (_b = transformer.getGeneratorConfig) === null || _b === void 0 ? void 0 : _b.call(transformer).zodImportTarget) !== null && _c !== void 0 ? _c : 'auto'); } catch { /* ignore */ } const useGetterRecursion = target === 'v4'; // Relation field -> always reference the resolved export name if (field.relationFromFields && field.relationFromFields.length > 0) { result.zodSchema = useGetterRecursion ? `${relatedExportName}` : `z.lazy(() => ${relatedExportName})`; result.imports.add(relatedExportName); result.requiresSpecialHandling = true; result.additionalValidations.push(`// Relation to ${relatedModelName}`); } else { result.zodSchema = useGetterRecursion ? `${relatedExportName}` : `z.lazy(() => ${relatedExportName})`; result.imports.add(relatedExportName); result.requiresSpecialHandling = true; result.additionalValidations.push(`// Back-relation to ${relatedModelName}`); } } else { // Non-relation object type (shouldn't happen in normal Prisma schemas) const isJsonSchemaCompatible = this.config.jsonSchemaCompatible; result.zodSchema = isJsonSchemaCompatible ? 'z.any()' : 'z.unknown()'; result.additionalValidations.push(`// Unknown object type: ${relatedModelName}`); } } /** * Handle unsupported field types */ mapUnsupportedType(field, result) { const isJsonSchemaCompatible = this.config.jsonSchemaCompatible; result.zodSchema = isJsonSchemaCompatible ? 'z.any()' : 'z.unknown()'; result.additionalValidations.push(`// Unsupported field kind: ${field.kind}`); console.warn(`Unsupported field kind: ${field.kind} for field ${field.name}`); } /** * Apply list wrapper for array fields */ applyListWrapper(result) { result.zodSchema = `z.array(${result.zodSchema})`; result.additionalValidations.push('// Array field'); } /** * Apply optional wrapper for optional fields */ applyOptionalWrapper(result) { result.zodSchema = `${result.zodSchema}.optional()`; } /** * Apply enhanced optionality wrapper with default values and special handling */ applyEnhancedOptionalityWrapper(result, optionalityResult) { // Apply the optionality modifier if (optionalityResult.zodModifier) { // Avoid duplicating default() if schema already contains a default const hasExistingDefault = /\.default\(/.test(result.zodSchema); if (hasExistingDefault) { // Strip .default(...) from the modifier if present const cleanedModifier = optionalityResult.zodModifier.replace(/\.default\([^)]*\)/g, ''); result.zodSchema = `${result.zodSchema}${cleanedModifier}`; } else { result.zodSchema = `${result.zodSchema}${optionalityResult.zodModifier}`; } } // Add optionality information to validations result.additionalValidations.push(`// Field optionality: ${optionalityResult.optionalityReason}`); // Add any additional notes optionalityResult.additionalNotes.forEach((note) => { result.additionalValidations.push(`// ${note}`); }); // Handle special cases if (optionalityResult.isAutoGenerated) { result.requiresSpecialHandling = true; result.additionalValidations.push('// Auto-generated field - handle with care in mutations'); } } /** * Determine field optionality with sophisticated logic * * @param field - Prisma DMMF field * @param model - Parent model for context * @returns Optionality information */ determineFieldOptionality(field, model) { const result = { isOptional: false, isNullable: false, hasDefaultValue: false, isAutoGenerated: false, optionalityReason: 'required', zodModifier: '', additionalNotes: [], }; // Check if field is explicitly optional in schema if (!field.isRequired) { result.isOptional = true; result.optionalityReason = 'schema_optional'; result.zodModifier = '.optional()'; result.additionalNotes.push('Field marked as optional in Prisma schema'); } // Check for default values if (field.hasDefaultValue) { result.hasDefaultValue = true; // Fields with default values can be optional during creation if (this.shouldMakeDefaultFieldOptional(field)) { result.isOptional = true; result.optionalityReason = 'has_default'; result.zodModifier = '.optional()'; result.additionalNotes.push('Field has default value, making it optional for input'); } // Add default value information this.addDefaultValueInfo(field, result); } // Check for auto-generated fields if (this.isAutoGeneratedField(field)) { result.isAutoGenerated = true; result.isOptional = true; result.optionalityReason = 'auto_generated'; result.zodModifier = '.optional()'; result.additionalNotes.push('Auto-generated field, optional for input'); } // Handle special field types this.handleSpecialFieldOptionalityRules(field, model, result); // Database-specific optionality rules this.applyDatabaseSpecificOptionalityRules(field, result); return result; } /** * Check if a field with default value should be optional */ shouldMakeDefaultFieldOptional(field) { // Auto-generated fields should always be optional if (this.isAutoGeneratedField(field)) { return true; } // UUID fields with default values are typically optional if (field.type === 'String' && field.isId && field.hasDefaultValue) { return true; } // DateTime fields with now() default should be optional if (field.type === 'DateTime' && field.hasDefaultValue) { return true; } // Integer fields with autoincrement should be optional if ((field.type === 'Int' || field.type === 'BigInt') && field.isId && field.hasDefaultValue) { return true; } // For other fields, check if explicitly marked as optional return !field.isRequired; } /** * Add default value information to optionality result */ addDefaultValueInfo(field, result) { if (field.default) { const defaultValue = field.default; if (typeof defaultValue === 'object' && defaultValue !== null) { // Handle function defaults like now(), uuid(), etc. if ('name' in defaultValue) { const functionName = defaultValue.name; result.additionalNotes.push(`Default function: ${functionName}()`); // Add appropriate Zod default if possible if (functionName === 'now' && field.type === 'DateTime') { result.zodModifier += '.default(() => new Date())'; } else if (functionName === 'uuid' && field.type === 'String') { // Avoid emitting inline UUID generator that requires extra imports/types. // Let the database generate UUIDs by default and keep schema validation simple. result.additionalNotes.push('UUID default detected; no inline generator emitted'); } else if (functionName === 'cuid' && field.type === 'String') { // Avoid emitting an undefined generateCuid() helper. // Let the database generate CUIDs by default and keep schema validation simple. result.additionalNotes.push('CUID default detected; no inline generator emitted'); } } } else { // Handle literal defaults let literalValue = JSON.stringify(defaultValue); // Preserve trailing .0 for Float defaults like 30.0 (JSON.stringify(30.0) => "30") if (typeof defaultValue === 'number' && field.type === 'Float') { const asString = String(defaultValue); if (/^\d+$/.test(asString)) { // If the Prisma schema likely had a .0, format with one decimal place literalValue = `${asString}.0`; } else { literalValue = asString; } } // Defer duplicate default check to wrapper stage result.zodModifier += `.default(${literalValue})`; result.additionalNotes.push(`Default value: ${literalValue}`); } } } /** * Check if field is auto-generated */ isAutoGeneratedField(field) { // ID fields with default values are typically auto-generated if (field.isId && field.hasDefaultValue) { return true; } // updatedAt fields are auto-generated if (field.isUpdatedAt) { return true; } // createdAt fields with default now() are auto-generated if (field.type === 'DateTime' && field.hasDefaultValue) { const defaultValue = field.default; if (typeof defaultValue === 'object' && defaultValue !== null && 'name' in defaultValue) { return defaultValue.name === 'now'; } } return false; } /** * Handle special optionality rules for specific field types */ handleSpecialFieldOptionalityRules(field, model, result) { // Handle relation fields specially if (field.kind === 'object') { if (field.relationName) { // Back-relations are typically optional if (!field.relationFromFields || field.relationFromFields.length === 0) { result.isOptional = true; result.optionalityReason = 'back_relation'; result.zodModifier = '.optional()'; result.additionalNotes.push('Back-relation field, typically optional'); } // Forward relations depend on foreign key nullability else { const foreignKeyFields = field.relationFromFields; const allForeignKeysOptional = foreignKeyFields.every((fkField) => { const referencedField = model.fields.find((f) => f.name === fkField); return referencedField && !referencedField.isRequired; }); if (allForeignKeysOptional) { result.isOptional = true; result.optionalityReason = 'nullable_foreign_keys'; result.zodModifier = '.optional()'; result.additionalNotes.push('Foreign key fields are nullable, making relation optional'); } } } } // Handle JSON fields - often optional due to complexity if (field.type === 'Json' && field.isRequired) { result.additionalNotes.push('JSON field is required - consider validation complexity'); } // Handle Bytes fields - often optional for file uploads if (field.type === 'Bytes') { result.additionalNotes.push('Bytes field - consider file upload requirements'); } } /** * Apply database-specific optionality rules */ applyDatabaseSpecificOptionalityRules(field, result) { if (!this.config.provider) return; switch (this.config.provider) { case 'postgresql': this.applyPostgreSQLOptionalityRules(field, result); break; case 'mysql': this.applyMySQLOptionalityRules(field, result); break; case 'sqlite': this.applySQLiteOptionalityRules(field, result); break; case 'mongodb': this.applyMongoDBOptionalityRules(field, result); break; } } /** * PostgreSQL-specific optionality rules */ applyPostgreSQLOptionalityRules(field, result) { // PostgreSQL UUID fields with gen_random_uuid() default if (field.type === 'String' && field.isId && field.hasDefaultValue) { result.additionalNotes.push('PostgreSQL UUID primary key with default generation'); } // PostgreSQL serial fields if ((field.type === 'Int' || field.type === 'BigInt') && field.isId && field.hasDefaultValue) { result.additionalNotes.push('PostgreSQL serial/bigserial primary key'); } } /** * MySQL-specific optionality rules */ applyMySQLOptionalityRules(field, result) { // MySQL AUTO_INCREMENT fields if ((field.type === 'Int' || field.type === 'BigInt') && field.isId && field.hasDefaultValue) { result.additionalNotes.push('MySQL AUTO_INCREMENT primary key'); } // MySQL TIMESTAMP fields if (field.type === 'DateTime' && field.hasDefaultValue) { result.additionalNotes.push('MySQL TIMESTAMP with default value'); } } /** * SQLite-specific optionality rules */ applySQLiteOptionalityRules(field, result) { // SQLite INTEGER PRIMARY KEY is always auto-generated if (field.type === 'Int' && field.isId) { result.additionalNotes.push('SQLite INTEGER PRIMARY KEY (ROWID alias)'); } } /** * MongoDB-specific optionality rules */ applyMongoDBOptionalityRules(field, result) { // MongoDB _id fields if (field.isId && field.name === 'id') { result.additionalNotes.push('MongoDB _id field (ObjectId)'); } // MongoDB supports undefined values differently than SQL databases if (!field.isRequired) { result.additionalNotes.push('MongoDB field allows undefined values'); } } /** * Apply inline validation from @zod comments */ applyInlineValidations(field, result, modelName = 'Unknown') { if (!field.documentation) { return; } try { // Create field comment context const context = { modelName: modelName, fieldName: field.name, fieldType: field.type, comment: field.documentation, isOptional: !field.isRequired, isList: field.isList, }; // Extract field comments const extractedComment = (0, zod_comments_1.extractFieldComment)(context); if (!extractedComment.hasZodAnnotations) { // If there are extraction errors, report them if (extractedComment.extractionErrors.length > 0) { result.additionalValidations.push(`// Comment extraction warnings: ${extractedComment.extractionErrors.join(', ')}`); } return; } // Parse @zod annotations const parseResult = (0, zod_comments_1.parseZodAnnotations)(extractedComment.normalizedComment, context); if (!parseResult.isValid || parseResult.annotations.leng