UNPKG

prisma-zod-generator

Version:

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

1,014 lines (1,013 loc) 128 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; exports.stripTopLevelOptional = stripTopLevelOptional; 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, }, }, }; /** * Remove `.optional()` calls that sit at the top level of a Zod expression, * leaving any nested inside parentheses untouched. * * The generator appends its own optionality to the chain (sometimes mid-chain, * before a `.default(...)`), and pure-model emission re-applies optionality * according to `optionalFieldBehavior`. A plain global replace also stripped * `.optional()` from inside user-supplied expressions such as * `@zod.custom.use(z.array(z.object({ b: z.number().optional() })))`, silently * changing the user's schema. */ function stripTopLevelOptional(expression) { const token = '.optional()'; let result = ''; let depth = 0; let inString = false; let quote = ''; for (let i = 0; i < expression.length;) { const char = expression[i]; if (inString) { if (char === '\\') { result += expression.slice(i, i + 2); i += 2; continue; } if (char === quote) { inString = false; quote = ''; } result += char; i++; continue; } if (char === '"' || char === "'" || char === '`') { inString = true; quote = char; result += char; i++; continue; } if (char === '(') depth++; else if (char === ')') depth--; else if (depth === 0 && expression.startsWith(token, i)) { i += token.length; continue; } result += char; i++; } return result; } /** * 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) { var _a, _b; 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>). // Anchor on `.custom.use(` (not `@zod.custom.use(`) so the import-prefixed // form `@zod.import([...]).custom.use(...)` is honored too — this is how a // Json field references an imported schema, e.g. z.array(WorkflowNodeSchema) // (issue #386). The @zod.import(...) portion is emitted separately via // extractFieldCustomImports. // // Use balanced-paren extraction (not a regex): the argument can nest // parentheses arbitrarily deep, e.g. an inline // `z.object({ items: z.array(z.object({ id: z.string() })) })`, which a // fixed-depth regex truncates into invalid TypeScript. const customUseMarker = /@zod\b/.test(field.documentation) ? field.documentation.search(/\.custom\.use\s*\(/) : -1; if (customUseMarker !== -1) { const openParen = field.documentation.indexOf('(', customUseMarker); const closeParen = openParen !== -1 ? (0, zod_comments_1.findBalancedParentheses)(field.documentation, openParen + 1) : -1; if (closeParen !== -1) { const baseExpression = field.documentation.slice(openParen + 1, closeParen).trim(); // Chained methods = remainder of the same line after the closing paren // (e.g. `.describe("…")`), matching the previous `(.*)$` behavior. const chainedMethods = ((_b = (_a = field.documentation.slice(closeParen + 1).match(/^[^\r\n]*/)) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : '').trim(); if (baseExpression) { const fullExpression = chainedMethods ? `${baseExpression}${chainedMethods}` : baseExpression; 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; } } /** * Extract decimal precision/scale from Prisma native type metadata when available. * Falls back to parsing field documentation in environments where nativeType is absent. */ extractDecimalPrecisionScale(field) { var _a; const fieldWithNativeType = field; const nativeType = fieldWithNativeType.nativeType; if (Array.isArray(nativeType) && nativeType.length >= 2) { const [typeName, params] = nativeType; const supportedTypeNames = new Set(['Decimal', 'Numeric', 'DECIMAL', 'NUMERIC']); if (typeof typeName === 'string' && supportedTypeNames.has(typeName)) { if (Array.isArray(params) && params.length >= 2) { const precision = Number(params[0]); const scale = Number(params[1]); if (Number.isInteger(precision) && Number.isInteger(scale) && precision > 0 && scale >= 0 && precision >= scale) { return { precision, scale }; } } } } const documentation = (_a = field.documentation) !== null && _a !== void 0 ? _a : ''; const docMatch = documentation.match(/@db\.(?:Decimal|Numeric)\((\d+),\s*(\d+)\)/i); if (docMatch) { const precision = parseInt(docMatch[1], 10); const scale = parseInt(docMatch[2], 10); if (Number.isInteger(precision) && Number.isInteger(scale) && precision > 0 && scale >= 0 && precision >= scale) { return { precision, scale }; } } return null; } /** * Map Decimal type with enhanced validation based on configuration */ mapDecimalType(field, result, modelName) { var _a, _b; const decimalConfig = this.config.complexTypes.decimal; // Default to 'decimal' mode if not specified const mode = this.config.decimalMode || 'decimal'; const decimalNativeConstraints = this.extractDecimalPrecisionScale(field); const maxPrecision = (_a = decimalNativeConstraints === null || decimalNativeConstraints === void 0 ? void 0 : decimalNativeConstraints.precision) !== null && _a !== void 0 ? _a : decimalConfig.maxPrecision; const maxScale = (_b = decimalNativeConstraints === null || decimalNativeConstraints === void 0 ? void 0 : decimalNativeConstraints.scale) !== null && _b !== void 0 ? _b : decimalConfig.maxScale; switch (mode) { case 'string': { result.zodSchema = 'z.string()'; // Build precision-aware regex pattern let regexPattern = '^'; if (decimalConfig.allowNegative) { regexPattern += '-?'; } if (decimalConfig.validatePrecision && maxPrecision) { const maxIntegerDigits = maxPrecision - (maxScale || 0); const maxScaleDigits = maxScale || 0; if (maxIntegerDigits > 0) { if (maxScaleDigits > 0) { regexPattern += `\\d{1,${maxIntegerDigits}}(?:\\.\\d{1,${maxScaleDigits}})?`; } else { regexPattern += `\\d{1,${maxIntegerDigits}}`; } } else { regexPattern += '\\d*\\.?\\d+'; } } else { regexPattern += '\\d*\\.?\\d+'; } regexPattern += '$'; result.additionalValidations.push(`.regex(/${regexPattern}/, "Invalid decimal format")`); // Add precision validation documentation if (decimalConfig.validatePrecision && maxPrecision) { result.additionalValidations.push(`// Precision: max ${maxPrecision} digits, scale ${maxScale || 0}`); } 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 a structural Decimal.isDecimal check instead of instanceof: // the browser and server Prisma runtimes bundle separate Decimal class copies, so // instanceof fails across copies while Decimal.isDecimal is cross-copy safe. const modelContext = modelName ? `, { message: "Field '${field.name}' must be a Decimal. Location: ['Models', '${modelName}']", }` : ''; result.zodSchema = `z.custom<InstanceType<typeof Prisma.Decimal>>((v) => Prisma.Decimal.isDecimal(v)${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 // One-level-nested parens: strips .default(BigInt("0")) / .default(new Date("...")) whole 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) { var _a, _b, _c, _d, _e; 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; } } // BigInt/DateTime literal defaults arrive from DMMF as strings; they // must be emitted as constructors matching the base schema type, not // as quoted strings (issue #373). jsonSchemaCompatible modes keep // string/number bases, so the raw literal stays correct there. let jsonCompatCfg = null; try { // eslint-disable-next-line @typescript-eslint/no-require-imports -- lazy require to avoid circular import jsonCompatCfg = (_c = (_b = (_a = require('../transformer').default).getGeneratorConfig) === null || _b === void 0 ? void 0 : _b.call(_a)) !== null && _c !== void 0 ? _c : null; } catch { /* ignore */