prisma-zod-generator
Version:
Prisma 2+ generator to emit Zod schemas from your Prisma schema
1,041 lines • 117 kB
JavaScript
"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