prisma-zod-generator
Version:
Prisma 2+ generator to emit Zod schemas from your Prisma schema
1,390 lines • 99.3 kB
JavaScript
"use strict";
/**
* Zod Comment Parser
*
* Parses @zod validation annotations from Prisma schema field comments
* to generate enhanced Zod validation schemas.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.extractFieldComments = extractFieldComments;
exports.extractFieldComment = extractFieldComment;
exports.normalizeComment = normalizeComment;
exports.detectZodAnnotations = detectZodAnnotations;
exports.validateCommentStructure = validateCommentStructure;
exports.getExtractionStatistics = getExtractionStatistics;
exports.filterZodComments = filterZodComments;
exports.getAllExtractionErrors = getAllExtractionErrors;
exports.parseZodAnnotations = parseZodAnnotations;
exports.validateZodAnnotations = validateZodAnnotations;
exports.mapAnnotationsToZodSchema = mapAnnotationsToZodSchema;
exports.generateCompleteZodSchema = generateCompleteZodSchema;
exports.getBaseZodType = getBaseZodType;
exports.getBaseZodTypeForField = getBaseZodTypeForField;
exports.getRequiredImports = getRequiredImports;
exports.detectCustomImports = detectCustomImports;
exports.parseCustomImports = parseCustomImports;
exports.findBalancedParentheses = findBalancedParentheses;
exports.extractModelCustomImports = extractModelCustomImports;
exports.extractFieldCustomImports = extractFieldCustomImports;
const logger_1 = require("../utils/logger");
/**
* Extract comments from Prisma DMMF model fields
*
* This function processes Prisma model field definitions and extracts
* comment data with proper normalization and context for downstream parsing.
*
* @param models - Array of Prisma DMMF models
* @returns Array of extracted field comments with context
*/
function extractFieldComments(models) {
const extractedComments = [];
for (const model of models) {
for (const field of model.fields) {
const comment = field.documentation || '';
// Skip fields without comments
if (!comment.trim()) {
continue;
}
try {
const context = {
modelName: model.name,
fieldName: field.name,
fieldType: field.type,
comment: comment,
isOptional: !field.isRequired,
isList: field.isList,
};
const extractedComment = extractFieldComment(context);
extractedComments.push(extractedComment);
}
catch (error) {
// Add error handling for individual field processing failures
extractedComments.push({
context: {
modelName: model.name,
fieldName: field.name,
fieldType: field.type,
comment: comment,
isOptional: !field.isRequired,
isList: field.isList,
},
normalizedComment: '',
hasZodAnnotations: false,
extractionErrors: [
`Failed to extract comment: ${error instanceof Error ? error.message : String(error)}`,
],
});
}
}
}
return extractedComments;
}
/**
* Extract and normalize a single field comment
*
* @param context - Field context with comment and metadata
* @returns Extracted comment with processing information
*/
function extractFieldComment(context) {
const errors = [];
try {
// Normalize whitespace and handle multi-line comments
const normalizedComment = normalizeComment(context.comment);
// Check if comment contains @zod annotations
const hasZodAnnotations = detectZodAnnotations(normalizedComment);
// Validate comment structure
const structureErrors = validateCommentStructure(normalizedComment, context);
errors.push(...structureErrors);
return {
context,
normalizedComment,
hasZodAnnotations,
extractionErrors: errors,
};
}
catch (error) {
errors.push(`Comment extraction failed: ${error instanceof Error ? error.message : String(error)}`);
return {
context,
normalizedComment: '',
hasZodAnnotations: false,
extractionErrors: errors,
};
}
}
/**
* Normalize comment content for consistent processing
*
* Handles multi-line comments, whitespace normalization, and ensures
* consistent format for downstream parsing.
*
* @param comment - Raw comment from Prisma field
* @returns Normalized comment string
*/
function normalizeComment(comment) {
if (!comment || typeof comment !== 'string') {
return '';
}
// Handle multi-line comments by preserving @zod annotations on separate lines
const lines = comment.split(/\r?\n/).map((line) => line.trim());
// Remove empty lines and normalize whitespace
const normalizedLines = lines
.filter((line) => line.length > 0)
.map((line) => line.replace(/\s+/g, ' ').trim());
return normalizedLines.join(' ');
}
/**
* Detect if comment contains @zod validation annotations
*
* Performs a quick check to determine if the comment contains
* any @zod annotations before more expensive parsing operations.
*
* @param comment - Normalized comment string
* @returns True if @zod annotations are detected
*/
function detectZodAnnotations(comment) {
if (!comment) {
return false;
}
// Look for @zod patterns (case-insensitive) - allow optional whitespace between @zod and .
const zodPattern = /@zod\s*\./i;
return zodPattern.test(comment);
}
/**
* Validate comment structure and detect potential issues
*
* Performs structural validation to catch common comment formatting
* issues that could cause parsing problems later.
*
* @param comment - Normalized comment string
* @param context - Field context for error reporting
* @returns Array of validation errors
*/
function validateCommentStructure(comment, context) {
const errors = [];
if (!comment) {
return errors; // Empty comments are valid
}
// Check for unmatched parentheses in @zod annotations
if (detectZodAnnotations(comment)) {
const parenthesesErrors = validateParentheses(comment, context);
errors.push(...parenthesesErrors);
// Check for malformed @zod syntax
const syntaxErrors = validateZodSyntax(comment, context);
errors.push(...syntaxErrors);
}
return errors;
}
/**
* Validate parentheses matching in comments
*
* @param comment - Comment to validate
* @param context - Field context for error reporting
* @returns Array of parentheses validation errors
*/
function validateParentheses(comment, context) {
const errors = [];
let parenCount = 0;
for (let i = 0; i < comment.length; i++) {
const char = comment[i];
if (char === '(') {
parenCount++;
}
else if (char === ')') {
parenCount--;
if (parenCount < 0) {
errors.push(`${context.modelName}.${context.fieldName}: Unmatched closing parenthesis at position ${i}`);
break;
}
}
}
if (parenCount > 0) {
errors.push(`${context.modelName}.${context.fieldName}: ${parenCount} unmatched opening parenthesis(es)`);
}
return errors;
}
/**
* Extract @zod annotations from comment with proper nested parentheses handling
*
* @param comment - Comment string
* @returns Array of annotation strings
*/
function extractZodAnnotations(comment) {
const annotations = [];
const zodRegex = /@zod\s*\./gi;
let match;
while ((match = zodRegex.exec(comment)) !== null) {
const startIndex = match.index;
// Find the method name
let currentIndex = startIndex + match[0].length;
// Skip whitespace
while (currentIndex < comment.length && /\s/.test(comment[currentIndex])) {
currentIndex++;
}
// Extract method name
const methodStart = currentIndex;
while (currentIndex < comment.length && /[a-zA-Z0-9_]/.test(comment[currentIndex])) {
currentIndex++;
}
if (currentIndex === methodStart)
continue; // No method name found
// Skip whitespace
while (currentIndex < comment.length && /\s/.test(comment[currentIndex])) {
currentIndex++;
}
// Optional generic args after method name (e.g., <"UserId">)
if (currentIndex < comment.length && comment[currentIndex] === '<') {
const genericRes = parseGenericArgs(comment, currentIndex);
if (genericRes) {
currentIndex = genericRes.nextIndex;
// Skip whitespace after generics
while (currentIndex < comment.length && /\s/.test(comment[currentIndex])) {
currentIndex++;
}
}
}
// Check if there are parameters
if (currentIndex < comment.length && comment[currentIndex] === '(') {
// Find the matching closing parenthesis with proper nesting
let depth = 0;
let inString = false;
let stringChar = '';
while (currentIndex < comment.length) {
const char = comment[currentIndex];
if (!inString && (char === '"' || char === "'")) {
inString = true;
stringChar = char;
}
else if (inString && char === stringChar && comment[currentIndex - 1] !== '\\') {
inString = false;
stringChar = '';
}
else if (!inString) {
if (char === '(') {
depth++;
}
else if (char === ')') {
depth--;
if (depth === 0) {
currentIndex++; // Include the closing parenthesis
break;
}
}
}
currentIndex++;
}
if (depth === 0) {
annotations.push(comment.substring(startIndex, currentIndex));
}
}
else {
// No parameters, just method name (and possible generics)
annotations.push(comment.substring(startIndex, currentIndex));
}
}
return annotations;
}
/**
* Parse @zod annotations with proper nested parentheses support
*
* @param comment - Comment string
* @param context - Field context
* @returns Parse result with annotations and errors
*/
function parseZodAnnotationsWithNestedParentheses(comment, context) {
const result = {
annotations: [],
parseErrors: [],
isValid: true,
};
const annotationStrings = extractZodAnnotations(comment);
for (const annotationStr of annotationStrings) {
try {
const methodMatch = annotationStr.match(/@zod\s*\.([a-zA-Z_][a-zA-Z0-9_]*)/i);
if (!methodMatch)
continue;
const methodName = methodMatch[1];
// Scan after method token for optional generics before params
const methodToken = methodMatch[0];
const methodTokenIndex = annotationStr.indexOf(methodToken);
let scanIndex = methodTokenIndex + methodToken.length;
while (scanIndex < annotationStr.length && /\s/.test(annotationStr[scanIndex]))
scanIndex++;
let genericArgs = '';
if (annotationStr[scanIndex] === '<') {
const genericRes = parseGenericArgs(annotationStr, scanIndex);
if (genericRes) {
genericArgs = genericRes.segment;
scanIndex = genericRes.nextIndex;
while (scanIndex < annotationStr.length && /\s/.test(annotationStr[scanIndex]))
scanIndex++;
}
}
const paramStart = annotationStr.indexOf('(', scanIndex);
if (paramStart !== -1) {
const paramEnd = annotationStr.lastIndexOf(')');
if (paramEnd !== -1) {
const parameterString = annotationStr.substring(paramStart, paramEnd + 1);
const fakeMatch = [
annotationStr,
methodName,
parameterString,
];
fakeMatch.index = 0;
const annotation = parseZodAnnotation(fakeMatch, context, genericArgs);
result.annotations.push(annotation);
}
}
else {
const fakeMatch = [annotationStr, methodName, ''];
fakeMatch.index = 0;
const annotation = parseZodAnnotation(fakeMatch, context, genericArgs);
result.annotations.push(annotation);
}
}
catch (error) {
const errorMessage = `Failed to parse @zod annotation "${annotationStr}": ${error instanceof Error ? error.message : String(error)}`;
result.parseErrors.push(errorMessage);
result.isValid = false;
}
}
return result;
}
/**
* Validate basic @zod annotation syntax
*
* @param comment - Comment to validate
* @param context - Field context for error reporting
* @returns Array of syntax validation errors
*/
function validateZodSyntax(comment, context) {
const errors = [];
// Look for @zod annotations and validate basic structure - allow optional whitespace
const zodMatches = extractZodAnnotations(comment);
if (zodMatches) {
zodMatches.forEach((match, _index) => {
// Check for common syntax issues
if (match.includes('..')) {
errors.push(`${context.modelName}.${context.fieldName}: Invalid double dots in @zod annotation: ${match}`);
}
if (match.includes('@zod.()')) {
errors.push(`${context.modelName}.${context.fieldName}: Empty method name in @zod annotation: ${match}`);
}
});
}
return errors;
}
/**
* Get comment extraction statistics for debugging
*
* @param extractedComments - Array of extracted comments
* @returns Statistics about comment extraction
*/
function getExtractionStatistics(extractedComments) {
const totalFields = extractedComments.length;
const fieldsWithComments = extractedComments.filter((ec) => ec.normalizedComment.length > 0).length;
const fieldsWithZodAnnotations = extractedComments.filter((ec) => ec.hasZodAnnotations).length;
const extractionErrors = extractedComments.filter((ec) => ec.extractionErrors.length > 0).length;
return {
totalFields,
fieldsWithComments,
fieldsWithZodAnnotations,
extractionErrors,
};
}
/**
* Filter extracted comments to only those with @zod annotations
*
* @param extractedComments - Array of all extracted comments
* @returns Array of comments containing @zod annotations
*/
function filterZodComments(extractedComments) {
return extractedComments.filter((ec) => ec.hasZodAnnotations && ec.extractionErrors.length === 0);
}
/**
* Get all extraction errors across multiple field comments
*
* @param extractedComments - Array of extracted comments
* @returns Array of all errors with field context
*/
function getAllExtractionErrors(extractedComments) {
return extractedComments
.filter((ec) => ec.extractionErrors.length > 0)
.map((ec) => ({
modelName: ec.context.modelName,
fieldName: ec.context.fieldName,
errors: ec.extractionErrors,
}));
}
/**
* Parse @zod annotations from normalized comment text
*
* Extracts and parses all @zod annotations including method names,
* parameters, and handles complex chaining scenarios.
*
* @param comment - Normalized comment string
* @param context - Field context for error reporting
* @returns Parse result with annotations and errors
*/
function parseZodAnnotations(comment, context) {
const result = {
annotations: [],
parseErrors: [],
isValid: true,
};
if (!comment || !detectZodAnnotations(comment)) {
return result;
}
try {
// Check if we have multiple method calls (chained) by counting dots after @zod
const methodCount = (comment.match(/\.([a-zA-Z_][a-zA-Z0-9_]*)/g) || []).length;
const hasChainedAnnotations = methodCount > 1;
if (hasChainedAnnotations) {
// Handle chained @zod annotations (e.g., @zod.min(1).max(100))
const chainedAnnotations = parseChainedZodAnnotations(comment, context);
result.annotations.push(...chainedAnnotations.annotations);
result.parseErrors.push(...chainedAnnotations.parseErrors);
if (chainedAnnotations.parseErrors.length > 0) {
result.isValid = false;
}
}
else {
// Handle simple @zod annotations with proper nested parentheses support
const annotations = parseZodAnnotationsWithNestedParentheses(comment, context);
result.annotations.push(...annotations.annotations);
result.parseErrors.push(...annotations.parseErrors);
if (annotations.parseErrors.length > 0) {
result.isValid = false;
}
}
// Drop bare base-type tokens (e.g. the leading `.string()` in
// `@zod.string().min(1)`): they are no-ops in the emitted chain, and
// passing them through validation rejects the whole comment (issue #374).
// 'date' is intentionally absent: @zod.date() is a registered format method.
const BASE_TYPE_TOKENS = new Set(['string', 'number', 'boolean', 'bigint']);
result.annotations = result.annotations.filter((a) => !(BASE_TYPE_TOKENS.has(a.method) && (!a.parameters || a.parameters.length === 0)));
// Validate and filter the parsed annotations
if (result.annotations.length > 0) {
const validAnnotations = [];
const validationErrors = [];
for (const annotation of result.annotations) {
try {
validateZodMethod(annotation, context);
validAnnotations.push(annotation);
}
catch (error) {
validationErrors.push(`${context.modelName}.${context.fieldName}: ${error instanceof Error ? error.message : String(error)}`);
// Continue processing other annotations instead of failing completely
}
}
// Only fail if no annotations are valid
if (validAnnotations.length === 0 && validationErrors.length > 0) {
result.parseErrors.push(...validationErrors);
result.isValid = false;
}
else {
result.annotations = validAnnotations;
// Log validation errors but don't fail if we have some valid annotations
if (validationErrors.length > 0) {
logger_1.logger.warn('Some @zod annotations were invalid and filtered out:', validationErrors);
}
}
}
}
catch (error) {
result.parseErrors.push(`General parsing error: ${error instanceof Error ? error.message : String(error)}`);
result.isValid = false;
}
return result;
}
/**
* Parse a single @zod annotation match
*
* @param match - Regex match result
* @param context - Field context for error reporting
* @returns Parsed annotation object
*/
function parseZodAnnotation(match, context, genericArgs = '') {
const [fullMatch, methodName, parameterString] = match;
const position = match.index || 0;
// Parse parameters if present
let parameters = [];
if (parameterString && parameterString.trim() !== '()') {
try {
parameters = parseZodParameters(parameterString, context, methodName);
}
catch (error) {
throw new Error(`Invalid parameters in ${methodName}: ${error instanceof Error ? error.message : String(error)}`);
}
}
return {
method: methodName,
parameters,
rawMatch: fullMatch,
position,
genericArgs: genericArgs || undefined,
};
}
/**
* Parse chained @zod annotations like @zod.min(1).max(100)
*
* @param comment - Comment string
* @param context - Field context
* @returns Parse result for chained annotations
*/
function parseChainedZodAnnotations(comment, context) {
const result = {
annotations: [],
parseErrors: [],
isValid: true,
};
try {
// Find the @zod annotation start
const zodIndex = comment.indexOf('@zod');
if (zodIndex === -1) {
return result;
}
// Extract the entire @zod chain starting from @zod
const zodChain = comment.slice(zodIndex);
// Use the improved parseMethodChain function to handle the entire chain
const chainedMethods = parseMethodChain(zodChain, context);
result.annotations.push(...chainedMethods);
}
catch (error) {
result.parseErrors.push(`Failed to parse chained annotation: ${error instanceof Error ? error.message : String(error)}`);
result.isValid = false;
}
return result;
}
/**
* Parse a method chain like .min(1).max(100)
*
* @param chainString - The chained method string
* @param context - Field context
* @returns Array of parsed annotations
*/
function parseMethodChain(chainString, context) {
const annotations = [];
// Parse method calls manually to handle nested parentheses properly
let i = 0;
while (i < chainString.length) {
// Find next method call starting with a dot
const dotIndex = chainString.indexOf('.', i);
if (dotIndex === -1)
break;
// Extract method name
const methodStart = dotIndex + 1;
let methodEnd = methodStart;
while (methodEnd < chainString.length && /[a-zA-Z0-9_]/.test(chainString[methodEnd])) {
methodEnd++;
}
if (methodEnd === methodStart) {
i = dotIndex + 1;
continue;
}
const methodName = chainString.slice(methodStart, methodEnd);
// Skip whitespace
while (methodEnd < chainString.length && /\s/.test(chainString[methodEnd])) {
methodEnd++;
}
// Optional generic arguments after method name
let iAfterName = methodEnd;
while (iAfterName < chainString.length && /\s/.test(chainString[iAfterName])) {
iAfterName++;
}
let genericArgs = '';
if (iAfterName < chainString.length && chainString[iAfterName] === '<') {
const genRes = parseGenericArgs(chainString, iAfterName);
if (genRes) {
genericArgs = genRes.segment;
iAfterName = genRes.nextIndex;
while (iAfterName < chainString.length && /\s/.test(chainString[iAfterName])) {
iAfterName++;
}
}
}
// Check for parameter list
let parameterString = '';
if (iAfterName < chainString.length && chainString[iAfterName] === '(') {
// Find matching closing parenthesis, handling nested parentheses
let parenCount = 1;
const paramStart = iAfterName + 1;
let paramEnd = paramStart;
while (paramEnd < chainString.length && parenCount > 0) {
const char = chainString[paramEnd];
if (char === '(') {
parenCount++;
}
else if (char === ')') {
parenCount--;
}
paramEnd++;
}
if (parenCount === 0) {
parameterString = chainString.slice(iAfterName, paramEnd);
i = paramEnd;
}
else {
throw new Error(`Unmatched parentheses in method ${methodName}`);
}
}
else {
// No parameters
parameterString = '()';
i = iAfterName;
}
// Parse parameters
let parameters = [];
if (parameterString && parameterString.trim() !== '()') {
try {
parameters = parseZodParameters(parameterString, context, methodName);
}
catch (error) {
throw new Error(`Invalid parameters in chained method ${methodName}: ${error instanceof Error ? error.message : String(error)}`);
}
}
annotations.push({
method: methodName,
parameters,
rawMatch: `.${methodName}${genericArgs}${parameterString}`,
position: dotIndex,
genericArgs: genericArgs || undefined,
});
}
return annotations;
}
/**
* Parse parameter string from @zod annotation
*
* Handles various parameter types: numbers, strings, booleans, arrays, objects
*
* @param parameterString - Parameter string including parentheses
* @param context - Field context for error reporting
* @param methodName - Method name for error context
* @returns Array of parsed parameters
*/
function parseZodParameters(parameterString, context, methodName) {
// Remove outer parentheses
const innerParams = parameterString.slice(1, -1).trim();
if (!innerParams) {
return [];
}
try {
// Handle simple cases first
if (isSimpleParameter(innerParams)) {
return [parseSimpleParameter(innerParams)];
}
// Handle multiple parameters - split by commas not inside quotes or nested structures
const paramParts = splitParameters(innerParams);
return paramParts.map((part) => parseSimpleParameter(part.trim()));
}
catch (error) {
throw new Error(`Failed to parse parameters "${innerParams}" for ${methodName}: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Check if parameter string is a simple single parameter
*
* @param paramString - Parameter string
* @returns True if simple parameter
*/
function isSimpleParameter(paramString) {
const trimmed = paramString.trim();
// Check if it contains unquoted commas (indicating multiple parameters)
let inQuotes = false;
let quoteChar = '';
let bracketDepth = 0;
for (let i = 0; i < trimmed.length; i++) {
const char = trimmed[i];
if (!inQuotes && (char === '"' || char === "'")) {
inQuotes = true;
quoteChar = char;
}
else if (inQuotes && char === quoteChar && trimmed[i - 1] !== '\\') {
inQuotes = false;
quoteChar = '';
}
else if (!inQuotes) {
if (char === '[' || char === '{' || char === '(') {
bracketDepth++;
}
else if (char === ']' || char === '}' || char === ')') {
bracketDepth--;
}
else if (char === ',' && bracketDepth === 0) {
return false; // Multiple parameters
}
}
}
return true;
}
/**
* Parse generic arguments starting at the given index (which must point to '<').
* Handles nested angle brackets and quoted strings.
* Returns the captured segment and the index immediately after it.
*/
function parseGenericArgs(source, startIndex) {
if (source[startIndex] !== '<')
return null;
let i = startIndex;
let depth = 0;
let inString = false;
let stringChar = '';
while (i < source.length) {
const char = source[i];
if (!inString && (char === '"' || char === "'")) {
inString = true;
stringChar = char;
}
else if (inString && char === stringChar && source[i - 1] !== '\\') {
inString = false;
stringChar = '';
}
else if (!inString) {
if (char === '<') {
depth++;
}
else if (char === '>') {
depth--;
if (depth === 0) {
const segment = source.slice(startIndex, i + 1);
return { segment, nextIndex: i + 1 };
}
}
}
i++;
}
return null;
}
/**
* Split parameter string by commas, respecting quotes and nested structures
*
* @param paramString - Parameter string
* @returns Array of parameter parts
*/
function splitParameters(paramString) {
const parts = [];
let current = '';
let inQuotes = false;
let quoteChar = '';
let bracketDepth = 0;
for (let i = 0; i < paramString.length; i++) {
const char = paramString[i];
if (!inQuotes && (char === '"' || char === "'")) {
inQuotes = true;
quoteChar = char;
current += char;
}
else if (inQuotes && char === quoteChar && paramString[i - 1] !== '\\') {
inQuotes = false;
quoteChar = '';
current += char;
}
else if (!inQuotes) {
if (char === '[' || char === '{' || char === '(') {
bracketDepth++;
current += char;
}
else if (char === ']' || char === '}' || char === ')') {
bracketDepth--;
current += char;
}
else if (char === ',' && bracketDepth === 0) {
parts.push(current);
current = '';
}
else {
current += char;
}
}
else {
current += char;
}
}
if (current) {
parts.push(current);
}
return parts;
}
/**
* Parse a single parameter value
*
* @param paramValue - Parameter string value
* @returns Parsed parameter value
*/
function parseSimpleParameter(paramValue) {
const trimmed = paramValue.trim();
if (!trimmed) {
throw new Error('Empty parameter value');
}
// Boolean values
if (trimmed === 'true')
return true;
if (trimmed === 'false')
return false;
// Null and undefined
if (trimmed === 'null')
return null;
if (trimmed === 'undefined')
return undefined;
// Numbers (integers and floats)
if (/^-?\d+$/.test(trimmed)) {
return parseInt(trimmed, 10);
}
if (/^-?\d*\.\d+$/.test(trimmed)) {
return parseFloat(trimmed);
}
// Quoted strings
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
(trimmed.startsWith("'") && trimmed.endsWith("'"))) {
return trimmed.slice(1, -1);
}
// Regular expressions
if (trimmed.startsWith('/') && trimmed.match(/\/[gimuy]*$/)) {
try {
const lastSlash = trimmed.lastIndexOf('/');
const pattern = trimmed.slice(1, lastSlash);
const flags = trimmed.slice(lastSlash + 1);
return new RegExp(pattern, flags);
}
catch {
throw new Error(`Invalid regex: ${trimmed}`);
}
}
// Arrays (basic support)
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
try {
return JSON.parse(trimmed);
}
catch {
throw new Error(`Invalid array: ${trimmed}`);
}
}
// Objects (JavaScript object literal support)
if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
try {
// First try standard JSON parsing for simple cases
return JSON.parse(trimmed);
}
catch {
// For complex JavaScript object literals, return as-is as a special marker
// This will be handled specially in formatParameters to preserve JavaScript expressions
return { __js_object_literal__: trimmed };
}
}
// Complex expressions (like new Date(), function calls, etc.)
if (trimmed.includes('(') && trimmed.includes(')')) {
// Pass through complex expressions as-is for evaluation at runtime
return trimmed;
}
// Unquoted strings (identifiers, etc.)
if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(trimmed)) {
return trimmed;
}
// If we can't parse it, pass it through as a raw string
// This allows complex expressions to be used in the generated code
return trimmed;
}
/**
* Validate parsed @zod annotations for semantic correctness
*
* @param annotations - Array of parsed annotations
* @param context - Field context
* @returns Array of validation errors
*/
function validateZodAnnotations(annotations, context) {
const errors = [];
for (const annotation of annotations) {
try {
validateZodMethod(annotation, context);
}
catch (error) {
errors.push(`${context.modelName}.${context.fieldName}: ${error instanceof Error ? error.message : String(error)}`);
}
}
return errors;
}
/**
* Validate a single @zod method annotation
*
* @param annotation - Parsed annotation
* @param context - Field context
*/
function validateZodMethod(annotation, context) {
const { method, parameters } = annotation;
// Common string validation methods
const stringMethods = [
'min',
'max',
'length',
'email',
'url',
'uuid',
'regex',
'includes',
'startsWith',
'endsWith',
'trim',
'toLowerCase',
'toUpperCase',
'datetime',
'ip',
'cidr',
'date',
'time',
'duration',
'normalize',
'uppercase',
'lowercase',
// New Zod v4 string format methods - Issue #233
'httpUrl',
'hostname',
'guid',
'nanoid',
'cuid',
'cuid2',
'ulid',
'base64',
'base64url',
'hex',
'jwt',
'hash',
'ipv4',
'ipv6',
'cidrv4',
'cidrv6',
'emoji',
'isoDate',
'isoTime',
'isoDatetime',
'isoDuration',
];
// Common number validation methods
const numberMethods = [
'min',
'max',
'int',
'positive',
'negative',
'nonnegative',
'nonpositive',
'finite',
'multipleOf',
'step',
'gt',
'gte',
'lt',
'lte',
];
// Common array validation methods
const arrayMethods = ['min', 'max', 'length', 'nonempty'];
// Methods that require parameters
const requiresParams = [
'min',
'max',
'length',
'regex',
'includes',
'startsWith',
'endsWith',
'enum',
'default',
'refine',
'transform',
'multipleOf',
'step',
'gt',
'gte',
'lt',
'lte',
'catch',
'pipe',
// New methods that require parameters - Issue #233
'hash', // hash requires algorithm parameter like "sha256"
'custom', // custom requires object/array schema parameter
];
// Methods that don't allow parameters
const noParams = [
'nonempty',
'trim',
'toLowerCase',
'toUpperCase',
'uppercase',
'lowercase',
'brand',
'readonly',
];
// Methods that accept optional parameters (format validation methods)
const optionalParams = [
// Existing string format methods
'email',
'url',
'uuid',
'datetime',
'ip',
'cidr',
'date',
'time',
'duration',
'normalize',
// New Zod v4 string format methods - Issue #233
'httpUrl',
'hostname',
'guid',
'nanoid',
'cuid',
'cuid2',
'ulid',
'base64',
'base64url',
'hex',
'jwt',
'ipv4',
'ipv6',
'cidrv4',
'cidrv6',
'emoji',
'isoDate',
'isoTime',
'isoDatetime',
'isoDuration',
];
// Methods that accept optional error message parameter
const optionalErrorMessage = [
'int',
'positive',
'negative',
'nonnegative',
'nonpositive',
'finite',
];
// Additional known methods not covered above
const additionalMethods = [
'default',
'optional',
'nullable',
'nullish',
'refine',
'transform',
'enum',
'object',
'array',
'record',
'json',
'catch',
'pipe',
'brand',
'readonly',
// Schema metadata (issue #225/#371): describe works on v3 and v4;
// meta is v4-only and is version-mapped during emission.
'describe',
'meta',
];
// Check if method is known
const allKnownMethods = [
...new Set([
...stringMethods,
...numberMethods,
...arrayMethods,
...additionalMethods,
...optionalErrorMessage,
...optionalParams,
...requiresParams, // Add methods that require parameters
]),
];
if (!allKnownMethods.includes(method)) {
throw new Error(`Unknown @zod method: ${method} - this method is not supported`);
}
// Validate parameter requirements
if (requiresParams.includes(method) && parameters.length === 0) {
throw new Error(`Method ${method} requires parameters`);
}
if (noParams.includes(method) && parameters.length > 0) {
throw new Error(`Method ${method} does not accept parameters`);
}
// Validate optional parameter methods (string format methods)
if (optionalParams.includes(method) && parameters.length > 1) {
throw new Error(`Method ${method} accepts at most one parameter`);
}
// Validate optional error message methods
if (optionalErrorMessage.includes(method) && parameters.length > 1) {
throw new Error(`Method ${method} accepts at most one parameter (error message)`);
}
if (optionalErrorMessage.includes(method) &&
parameters.length === 1 &&
!isErrorMessageParam(parameters[0])) {
throw new Error(`Method ${method} error message parameter must be a string or params object`);
}
// Type-specific validations based on field type
if (context.fieldType === 'String') {
validateStringMethodParameters(method, parameters);
}
else if (context.fieldType === 'Int' || context.fieldType === 'Float') {
validateNumberMethodParameters(method, parameters);
}
}
/**
* Accept both error-message shapes Zod supports: the string shorthand and the
* params object form (`{ message: "..." }` / `{ error: "..." }`). Object-form
* arguments arrive either JSON-parsed or as `{ __js_object_literal__ }` markers.
*/
function isErrorMessageParam(param) {
return (typeof param === 'string' ||
(typeof param === 'object' &&
param !== null &&
!Array.isArray(param) &&
!(param instanceof RegExp)));
}
/**
* Validate parameters for string validation methods
*
* @param method - Validation method name
* @param parameters - Method parameters
*/
function validateStringMethodParameters(method, parameters) {
switch (method) {
case 'min':
case 'max':
case 'length':
// Allow 1 parameter (number) or 2 parameters (number + error message)
if (parameters.length < 1 ||
parameters.length > 2 ||
typeof parameters[0] !== 'number' ||
parameters[0] < 0 ||
(parameters.length === 2 && !isErrorMessageParam(parameters[1]))) {
throw new Error(`${method} requires a non-negative number parameter and optional error message (string or params object)`);
}
break;
case 'regex':
if (parameters.length < 1 || parameters.length > 2) {
throw new Error(`regex requires 1-2 parameters (RegExp/string, optional error message)`);
}
if (!(parameters[0] instanceof RegExp || typeof parameters[0] === 'string')) {
throw new Error(`regex first parameter must be a RegExp or string`);
}
if (parameters.length === 2 && typeof parameters[1] !== 'string') {
throw new Error(`regex second parameter (error message) must be a string`);
}
break;
case 'includes':
case 'startsWith':
case 'endsWith':
if (parameters.length !== 1 || typeof parameters[0] !== 'string') {
throw new Error(`${method} requires a string parameter`);
}
break;
case 'hash':
if (parameters.length !== 1 || typeof parameters[0] !== 'string') {
throw new Error(`hash requires a single string algorithm parameter (e.g., 'sha256')`);
}
break;
}
}
/**
* Validate parameters for number validation methods
*
* @param method - Validation method name
* @param parameters - Method parameters
*/
function validateNumberMethodParameters(method, parameters) {
switch (method) {
case 'min':
case 'max':
// Allow 1 parameter (number) or 2 parameters (number + error message)
if (parameters.length < 1 ||
parameters.length > 2 ||
typeof parameters[0] !== 'number' ||
(parameters.length === 2 && !isErrorMessageParam(parameters[1]))) {
throw new Error(`${method} requires a number parameter and optional error message`);
}
break;
case 'positive':
case 'negative':
case 'int':
case 'finite':
// Allow 0 parameters or 1 parameter (error message)
if (parameters.length > 1 ||
(parameters.length === 1 && !isErrorMessageParam(parameters[0]))) {
throw new Error(`${method} accepts optional error message parameter`);
}
break;
case 'multipleOf':
case 'step':
// Allow 1 parameter (number) or 2 parameters (number + error message)
if (parameters.length < 1 ||
parameters.length > 2 ||
typeof parameters[0] !== 'number' ||
(parameters.length === 2 && !isErrorMessageParam(parameters[1]))) {
throw new Error(`${method} requires a number parameter and optional error message`);
}
break;
}
}
/**
* Map parsed @zod annotations to Zod schema method calls
*
* Converts parsed annotations into actual Zod validation method chains
* that can be used in generated schema code.
*
* @param annotations - Array of parsed annotations
* @param context - Field context for validation
* @param zodVersion - Zod version target ('v3', 'v4', or 'auto')
* @returns Schema generation result with method chain and imports
*/
function mapAnnotationsToZodSchema(annotations, context, zodVersion = 'auto') {
const result = {
schemaChain: '',
imports: new Set(),
errors: [],
isValid: true,
};
if (annotations.length === 0) {
return result;
}
try {
const methodCalls = [];
const methodResults = [];
for (const annotation of annotations) {
try {
const methodResult = mapAnnotationToZodMethod(annotation, context, zodVersion);
// Skip empty fallbacks (e.g., v3 for v4-only base methods)
if (methodResult.methodCall) {
methodCalls.push(methodResult.methodCall);
}
methodResults.push(methodResult);
// Add any required imports
if (methodResult.requiredImport) {
result.imports.add(methodResult.requiredImport);
}
}
catch (error) {
result.errors.push(`Failed to map ${annotation.method}: ${error instanceof Error ? error.message : String(error)}`);
result.isValid = false;
}
}
// Join all method calls into a chain
if (methodCalls.length > 0) {
// Check if we have a base replacement method (like z.email() in v4)
const baseReplacements = methodResults.filter((result) => result.isBaseReplacement);
const regularReplacements = methodCalls.filter((call) => {
var _a;
return call &&
!call.startsWith('.') &&
!((_a = methodResults.find((r) => r.methodCall === call)) === null || _a === void 0 ? void 0 : _a.isBaseReplacement);
});
const chainMethods = methodCalls.filter((call) => call.startsWith('.'));
if (baseReplacements.length > 0) {
// Handle base replacement (like z.email() in v4) with chaining
if (baseReplacements.length > 1) {
result.errors.push('Multiple base replacement methods detected - only one allowed per field');
result.isValid = false;
}
else {
result.schemaChain = baseReplacements[0].methodCall + chainMethods.join('');
}
}
else if (regularReplacements.length > 0) {
// Handle regular replacement methods (json, enum)
const chainMethods = methodCalls.filter((call) => call.startsWith('.'));
if (regularReplacements.length > 1) {
result.errors.push('Multiple replacement methods detected - only one allowed per field');
result.isValid = false;
}
else {
// Use the replacement method as base, then chain any additional methods
result.schemaChain = regularReplacements[0] + chainMethods.join('');
}
}
else {
// All are chaining methods, join normally
result.schemaChain = methodCalls.join('');
}
}
}
catch (error) {
result.errors.push(`Schema generation failed: ${error instanceof Error ? error.message : String(error)}`);
result.isValid = false;
}
return result;
}
/**
* Resolve the target Zod version for syntax generation
*
* @param zodVersion - Version specification ('auto', 'v3', or 'v4')
* @returns Resolved version ('v3' or 'v4')
*/
function resolveZodVersion(zodVersion) {
var _a;
if (zodVersion === 'v3' || zodVersion === 'v4') {
return zodVersion;
}
// Auto-detect Zod version
try {
// Try to detect Zod version by loading zod package.json
// eslint-disable-next-line @typescript-eslint/no-require-imports
const zodPackage = require('zod/package.json');
const version = zodPackage.version;
if (version) {
const majorVersion = parseInt(version.split('.')[0], 10);
return majorVersion >= 4 ? 'v4' : 'v3';
}
}
catch {
// If we can't load zod package.json, try alternative detection
try {
// Try to detect by checking if z.email() method exists as standalone
// eslint-disable-next-line @typescript-eslint/no-require-imports
const zod = require('zod');
if (typeof ((_a = zod.z) === null || _a === void 0 ? void 0 : _a.email) === 'function') {
return 'v4';
}
}
catch {
// Ignore inner detection errors
}
}
// Fallback to v3 if detection fails
logger_1.logger.warn('Failed to detect Zod version, defaulting to v3 syntax');
return 'v3';
}
/**
* Map a single annotation to a Zod method call
*
* @param annotation - Parsed annotation
* @param context - Field context
* @param zodVersion - Zod version target for version-specific handling
* @returns Method mapping result
*/
/**
* Map .meta({...}) parameters to a .describe(...) call for Zod v3, which has
* no .meta(). Returns null when no description key can be extracted.
*/
function metaParamsToDescribeCall(parameters) {
const p = parameters[0];
if (!p || typeof p !== 'object')
return null;
if ('__js_object_literal__' in p) {
const literal = p.__js_object_literal__;
const m = literal.match(/\bdescription\s*:\s*("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/);
return m ? `.describe(${m[1]})` : null;
}
const description = p.description;
return typeof description === 'string' ? `.describe(${JSON.stringify(description)})` : null;
}
function mapAnnotationToZodMethod(annotation, context, zodVersion = 'auto') {
var _a;
const { method, parameters } = annotation;
// Schema metadata (issues #225/#371). describe exists on v3 and v4; meta is
// v4-only and downgrades to .describe(description) on v3.
if (method === 'describe') {
return { methodCall: `.describe(${formatParameters(parameters)})` };
}
if (method === 'meta') {
if (resolveZodVersion(zodVersion) === 'v4') {
return { methodCall: `.meta(${formatParameters(parameters)})` };
}
const describeCall = metaParamsToDescribeCall(parameters);
if (describeCall) {
return { methodCall: describeCall };
}
logger_1.logger.warn(`@zod.meta() requires Zod