prisma-zod-generator
Version:
Prisma 2+ generator to emit Zod schemas from your Prisma schema
1,011 lines • 121 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;
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 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 */
}
if (field.type === 'BigInt' &&
(typeof defaultValue === 'string' || typeof defaultValue === 'number') &&
/