UNPKG

prisma-zod-generator

Version:

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

1,015 lines 235 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const path_1 = __importDefault(require("path")); const results_1 = require("./generators/results"); const helpers_1 = require("./helpers"); const decimal_helpers_1 = require("./helpers/decimal-helpers"); const model_helpers_1 = require("./helpers/model-helpers"); const zod_integration_1 = require("./helpers/zod-integration"); const logger_1 = require("./utils/logger"); const naming_resolver_1 = require("./utils/naming-resolver"); const singleFileAggregator_1 = require("./utils/singleFileAggregator"); const strict_mode_resolver_1 = require("./utils/strict-mode-resolver"); const writeFileSafely_1 = require("./utils/writeFileSafely"); const writeIndexFile_1 = require("./utils/writeIndexFile"); class Transformer { // Lightweight helpers to safely access optional JSON Schema compatibility settings static isJsonSchemaModeEnabled() { const cfg = this.getGeneratorConfig(); return !!(cfg === null || cfg === void 0 ? void 0 : cfg.jsonSchemaCompatible); } static getJsonSchemaOptions() { var _a; const cfg = this.getGeneratorConfig(); return ((_a = cfg === null || cfg === void 0 ? void 0 : cfg.jsonSchemaOptions) !== null && _a !== void 0 ? _a : {}); } constructor(params) { var _a, _b, _c, _d, _e, _f; this.schemaImports = new Set(); this.hasJson = false; this.hasDecimal = false; // Track excluded field names for current object generation to inform typed Omit this.lastExcludedFieldNames = null; this.name = (_a = params.name) !== null && _a !== void 0 ? _a : ''; this.fields = (_b = params.fields) !== null && _b !== void 0 ? _b : []; this.models = (_c = params.models) !== null && _c !== void 0 ? _c : []; this.modelOperations = (_d = params.modelOperations) !== null && _d !== void 0 ? _d : []; this.enumTypes = (_e = params.enumTypes) !== null && _e !== void 0 ? _e : []; // Process models with Zod integration on initialization const generatorConfig = Transformer.getGeneratorConfig(); const zodVersion = ((_f = generatorConfig === null || generatorConfig === void 0 ? void 0 : generatorConfig.zodImportTarget) !== null && _f !== void 0 ? _f : 'auto'); this.enhancedModels = (0, zod_integration_1.processModelsWithZodIntegration)(this.models, { enableZodAnnotations: true, generateFallbackSchemas: true, validateTypeCompatibility: true, collectDetailedErrors: true, zodVersion: zodVersion, }); } static setOutputPath(outPath) { this.outputPath = outPath; } static setIsGenerateSelect(isGenerateSelect) { this.isGenerateSelect = isGenerateSelect; } static setIsGenerateInclude(isGenerateInclude) { this.isGenerateInclude = isGenerateInclude; } // Configuration setters static setGeneratorConfig(config) { this.generatorConfig = config; this.strictModeResolver = (0, strict_mode_resolver_1.createStrictModeResolver)(config); } static getGeneratorConfig() { return this.generatorConfig; } static getStrictModeResolver() { return this.strictModeResolver; } static escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } /** * Check if a model should be generated based on configuration */ static isModelEnabled(modelName) { var _a; const config = this.getGeneratorConfig(); // If no configuration is available, generate all models (default behavior) if (!config) { return true; } // Check if model has specific configuration const modelConfig = (_a = config.models) === null || _a === void 0 ? void 0 : _a[modelName]; if (modelConfig) { // If model config exists, check if it's explicitly enabled/disabled return modelConfig.enabled !== false; } // In minimal mode, do NOT blanket-disable models. // Default to enabling models unless explicitly disabled via config.models. // This aligns minimal mode with full mode regarding model selection; other constraints // (limited operations and pared-down object schemas) are handled elsewhere. // Default: enable all models when no explicit model disabling is configured return true; } /** * Get list of enabled models for generation */ static getEnabledModels(allModels) { return allModels.filter((model) => this.isModelEnabled(model.name)); } /** * Log information about model filtering */ static logModelFiltering(allModels) { const config = this.getGeneratorConfig(); if (!config) return; const enabledModels = this.getEnabledModels(allModels); const disabledModels = allModels.filter((model) => !this.isModelEnabled(model.name)); if (disabledModels.length > 0) { logger_1.logger.debug(`🚫 Models excluded from generation: ${disabledModels.map((m) => m.name).join(', ')}`); } if (config.mode === 'minimal' && enabledModels.length < allModels.length) { logger_1.logger.debug(`⚡ Minimal mode: generating ${enabledModels.length}/${allModels.length} models`); } } /** * Check if a specific operation should be generated for a model */ static isOperationEnabled(modelName, operationName) { var _a, _b; const config = this.getGeneratorConfig(); // If no configuration is available, generate all operations (default behavior) if (!config) { logger_1.logger.debug(`🔍 Operation check: ${modelName}.${operationName} = true (no config)`); return true; } // Map operation names for backward compatibility const operationMapping = { findMany: ['findMany'], findUnique: ['findUnique'], findUniqueOrThrow: ['findUniqueOrThrow'], findFirst: ['findFirst'], findFirstOrThrow: ['findFirstOrThrow'], createOne: ['create', 'createOne'], createMany: ['create', 'createMany'], createManyAndReturn: ['createManyAndReturn'], updateOne: ['update', 'updateOne'], updateMany: ['update', 'updateMany'], updateManyAndReturn: ['updateManyAndReturn'], deleteOne: ['delete', 'deleteOne'], deleteMany: ['delete', 'deleteMany'], upsertOne: ['upsert', 'upsertOne'], aggregate: ['aggregate'], groupBy: ['groupBy'], count: ['count'], }; // Check global operation exclusions first if ((_a = config.globalExclusions) === null || _a === void 0 ? void 0 : _a.operations) { const mappedOperationNames = operationMapping[operationName] || [operationName]; const isExcluded = mappedOperationNames.some((opName) => { var _a, _b, _c; return (_c = (_b = (_a = config.globalExclusions) === null || _a === void 0 ? void 0 : _a.operations) === null || _b === void 0 ? void 0 : _b.includes(opName)) !== null && _c !== void 0 ? _c : false; }); if (isExcluded) { logger_1.logger.debug(`🔍 Operation check: ${modelName}.${operationName} = false (globally excluded: [${config.globalExclusions.operations.join(', ')}])`); return false; } } // Check if model has specific configuration const modelConfig = (_b = config.models) === null || _b === void 0 ? void 0 : _b[modelName]; if (modelConfig && modelConfig.operations) { const allowedOperationNames = operationMapping[operationName] || [operationName]; const isEnabled = allowedOperationNames.some((opName) => { var _a, _b; return (_b = (_a = modelConfig.operations) === null || _a === void 0 ? void 0 : _a.includes(opName)) !== null && _b !== void 0 ? _b : false; }); logger_1.logger.debug(`🔍 Operation check: ${modelName}.${operationName} = ${isEnabled} (configured ops: [${modelConfig.operations.join(', ')}])`); return isEnabled; } // For minimal mode, only enable essential operations (no *Many, no upsert, no aggregate/groupBy) if (config.mode === 'minimal') { // Allow overrides via config.minimalOperations when present const configured = config.minimalOperations; const baseAllowed = configured && Array.isArray(configured) && configured.length > 0 ? configured : ['findMany', 'findUnique', 'findFirst', 'create', 'update', 'delete']; // Map legacy op names used in model mappings to these canonical names const opAliasMap = { createOne: 'create', updateOne: 'update', deleteOne: 'delete', upsertOne: 'upsert', }; const canonical = opAliasMap[operationName] || operationName; const isEnabled = baseAllowed.includes(canonical); logger_1.logger.debug(`🔍 Operation check: ${modelName}.${operationName} -> ${canonical} = ${isEnabled} (minimal mode)`); return isEnabled; } // Provider capability gating for AndReturn operations (not supported on all connectors) if (operationName === 'createManyAndReturn' || operationName === 'updateManyAndReturn') { const supportsAndReturn = ['postgresql', 'cockroachdb'].includes(this.provider); if (!supportsAndReturn) { logger_1.logger.debug(`🔍 Operation check: ${modelName}.${operationName} = false (provider=${this.provider} lacks AndReturn support)`); return false; } } // Default: enable all operations not explicitly disabled logger_1.logger.debug(`🔍 Operation check: ${modelName}.${operationName} = true (default)`); return true; } /** * Get list of enabled operations for a model */ static getEnabledOperations(modelName, allOperations) { return allOperations.filter((operation) => this.isOperationEnabled(modelName, operation)); } /** * Log information about operation filtering for a model */ static logOperationFiltering(modelName, allOperations) { const config = this.getGeneratorConfig(); if (!config) return; const enabledOperations = this.getEnabledOperations(modelName, allOperations); const disabledOperations = allOperations.filter((op) => !this.isOperationEnabled(modelName, op)); if (disabledOperations.length > 0) { logger_1.logger.debug(` 🔧 ${modelName}: excluded operations [${disabledOperations.join(', ')}]`); } if (config.mode === 'minimal' && enabledOperations.length < allOperations.length) { logger_1.logger.debug(` ⚡ ${modelName}: minimal mode - ${enabledOperations.length}/${allOperations.length} operations`); } } /** * Check if a field should be included in schema generation */ static isFieldEnabled(fieldName, modelName, variant) { var _a, _b, _c, _d, _e, _f, _g, _h; const config = this.getGeneratorConfig(); // If no configuration is available, include all fields (default behavior) if (!config) { return true; } // Start with a flag indicating whether the field should be excluded let shouldExclude = false; const debugReasons = []; // Check global exclusions (basic format - array of strings) if (config.globalExclusions && Array.isArray(config.globalExclusions)) { if (this.isFieldMatchingPatterns(fieldName, config.globalExclusions)) { shouldExclude = true; debugReasons.push(`global array exclusion`); } } // Check global exclusions for the specified variant if (variant && ((_a = config.globalExclusions) === null || _a === void 0 ? void 0 : _a[variant])) { if (this.isFieldMatchingPatterns(fieldName, config.globalExclusions[variant])) { shouldExclude = true; debugReasons.push(`global ${variant} exclusion`); } } // Check model-specific field exclusions and includes if (modelName) { const modelConfig = (_b = config.models) === null || _b === void 0 ? void 0 : _b[modelName]; // Check model-specific includes FIRST - these OVERRIDE exclusions (highest precedence) const legacyFields = modelConfig; if ((_c = legacyFields === null || legacyFields === void 0 ? void 0 : legacyFields.fields) === null || _c === void 0 ? void 0 : _c.include) { if (this.isFieldMatchingPatterns(fieldName, legacyFields.fields.include)) { logger_1.logger.debug(`🟢 Field enabled: ${modelName}.${fieldName} (${variant}) = true (model include override)`); return true; // Include overrides any previous or subsequent exclusion } } // Check variant-specific exclusions (new format) if ((_e = (_d = modelConfig === null || modelConfig === void 0 ? void 0 : modelConfig.variants) === null || _d === void 0 ? void 0 : _d[variant || 'pure']) === null || _e === void 0 ? void 0 : _e.excludeFields) { const ex = ((_g = (_f = modelConfig.variants) === null || _f === void 0 ? void 0 : _f[variant || 'pure']) === null || _g === void 0 ? void 0 : _g.excludeFields) || []; if (this.isFieldMatchingPatterns(fieldName, ex)) { shouldExclude = true; debugReasons.push(`model ${variant} variant exclusion`); } } // Check legacy format: fields.exclude (for backward compatibility) if ((_h = legacyFields === null || legacyFields === void 0 ? void 0 : legacyFields.fields) === null || _h === void 0 ? void 0 : _h.exclude) { logger_1.logger.debug(`🔍 Checking legacy fields.exclude for ${modelName}.${fieldName}: patterns =`, legacyFields.fields.exclude); if (this.isFieldMatchingPatterns(fieldName, legacyFields.fields.exclude)) { shouldExclude = true; debugReasons.push(`model fields.exclude`); logger_1.logger.debug(`🚫 Field excluded by legacy fields.exclude: ${modelName}.${fieldName}`); } } } const result = !shouldExclude; if (debugReasons.length > 0 || (modelName && ['password', 'views', 'internalId', 'metadata'].includes(fieldName))) { logger_1.logger.debug(`🔍 Field check: ${modelName || 'unknown'}.${fieldName} (${variant || 'unknown'}) = ${result} ${debugReasons.length > 0 ? `(${debugReasons.join(', ')})` : '(allowed)'}`); } // Return the opposite of shouldExclude (if should exclude, return false) return result; } /** * Check if a field name matches any pattern in the exclusion list * Supports wildcards: *field, field*, *field* */ static isFieldMatchingPatterns(fieldName, patterns) { // Debug: Reduced verbosity for performance (uncomment for detailed debugging) // console.log(`🔍 Pattern matching: fieldName='${fieldName}', patterns=`, patterns); const result = patterns.some((pattern) => { // Exact match (no wildcards) if (!pattern.includes('*')) { const match = fieldName === pattern; // console.log(` - Exact match '${pattern}': ${match}`); return match; } // Convert pattern to regex const regexPattern = pattern .replace(/\./g, '\\.') // Escape dots FIRST .replace(/\*/g, '.*'); // Then replace * with .* const regex = new RegExp(`^${regexPattern}$`); const match = regex.test(fieldName); // console.log(` - Regex match '${pattern}' -> /${regexPattern}/: ${match}`); return match; }); // console.log(`🔍 Pattern matching result: ${result}`); return result; } /** * Filter fields based on configuration and relationship preservation */ static filterFields(fields, modelName, variant, models, schemaName) { const config = (this.getGeneratorConfig() || {}); const strictCreateInputs = config.strictCreateInputs !== false; // default true const preserveRequiredScalarsOnCreate = config.preserveRequiredScalarsOnCreate !== false; // default true // Special case: WhereUniqueInput must retain unique identifier fields (e.g., id, unique columns) // Do NOT apply variant-based exclusions to this schema type const isWhereUniqueInput = !!schemaName && /WhereUniqueInput$/.test(schemaName); // Do not apply variant-based field exclusions to base Prisma Create input object schemas when strictCreateInputs is true // These must strictly match Prisma types to satisfy typed Zod bindings const isBasePrismaCreateInputName = !!schemaName && [ /^(\w+)CreateInput$/, /^(\w+)UncheckedCreateInput$/, /^(\w+)CreateManyInput$/, /^(\w+)CreateMany\w+Input$/, /^(\w+)CreateWithout\w+Input$/, /^(\w+)UncheckedCreateWithout\w+Input$/, /^(\w+)CreateOrConnectWithout\w+Input$/, /^(\w+)CreateNested(?:One|Many)Without\w+Input$/, ].some((re) => re.test(schemaName)); const isBasePrismaCreateInput = isBasePrismaCreateInputName && strictCreateInputs; const filtered = fields.filter((field) => { // Check basic field inclusion rules first if (!isWhereUniqueInput && !isBasePrismaCreateInput && !this.isFieldEnabled(field.name, modelName, variant)) { return false; } // In minimal mode, suppress relation and nested object fields in input variants // BUT preserve them for base Prisma create inputs to maintain type compatibility if (config.mode === 'minimal' && variant === 'input' && !isBasePrismaCreateInput) { if (this.isRelationFieldArg(field)) { return false; } } // For relation fields, also check if the related model is enabled // This is a safety check for schema args that might represent relation fields if (this.isRelationFieldArg(field)) { const relatedModelName = this.extractRelatedModelFromFieldArg(field); if (relatedModelName && !this.isModelEnabled(relatedModelName)) { return false; } } return true; }); // Handle foreign key preservation when relation fields are excluded const result = [...filtered]; if (modelName && variant === 'input' && !isBasePrismaCreateInput && models) { const model = models.find((m) => m.name === modelName); if (model) { const excludedRelationFields = this.getExcludedRelationFields(fields, filtered, model); const foreignKeyFields = this.getForeignKeyFieldsForExcludedRelations(excludedRelationFields, model); // Add foreign key fields that aren't already present AND aren't explicitly excluded for (const fkField of foreignKeyFields) { if (!result.some((field) => field.name === fkField.name)) { // Check if the foreign key field is explicitly excluded by configuration const isFieldAllowed = this.isFieldEnabled(fkField.name, modelName, variant); logger_1.logger.debug(`🔑 Foreign key preservation check: ${modelName}.${fkField.name} (${variant}) = ${isFieldAllowed}`); if (isFieldAllowed) { logger_1.logger.debug(`🔑 Adding foreign key field: ${modelName}.${fkField.name}`); result.push(fkField); } else { logger_1.logger.debug(`🚫 Skipping excluded foreign key field: ${modelName}.${fkField.name}`); } } } } } // If strictCreateInputs is false and this is a Create-like input, optionally re-add required non-auto scalars if (!strictCreateInputs && isBasePrismaCreateInputName && preserveRequiredScalarsOnCreate && modelName && models) { const model = models.find((m) => m.name === modelName); if (model) { const requiredScalars = model.fields.filter((f) => f.kind === 'scalar' && f.isRequired === true && f.hasDefaultValue !== true && f.isUpdatedAt !== true); for (const f of requiredScalars) { if (!result.some((arg) => arg.name === f.name)) { // Try to find the corresponding SchemaArg in original fields by name const original = fields.find((arg) => arg.name === f.name); if (original) result.push(original); } } } } return result; } /** * Get excluded relation fields by comparing original and filtered fields */ static getExcludedRelationFields(originalFields, filteredFields, model) { const excludedFieldNames = originalFields .filter((originalField) => !filteredFields.some((filteredField) => filteredField.name === originalField.name)) .map((field) => field.name); return model.fields.filter((field) => excludedFieldNames.includes(field.name) && field.kind === 'object' && field.relationFromFields && field.relationFromFields.length > 0); } /** * Get foreign key schema args for excluded relation fields */ static getForeignKeyFieldsForExcludedRelations(excludedRelationFields, model) { const foreignKeyFields = []; for (const relationField of excludedRelationFields) { if (!relationField.relationFromFields) continue; for (const fkFieldName of relationField.relationFromFields) { const fkField = model.fields.find((f) => f.name === fkFieldName); if (fkField && fkField.kind === 'scalar') { const inputTypes = [ { type: fkField.type, location: 'scalar', isList: fkField.isList, }, ]; const schemaArg = { name: fkField.name, isRequired: fkField.isRequired, isNullable: !fkField.isRequired, inputTypes, }; foreignKeyFields.push(schemaArg); } } } return foreignKeyFields; } /** * Check if a schema arg represents a relation field */ static isRelationFieldArg(field) { // Heuristic: relation-shaped nested inputs and relation filters // Detect a wide set of Prisma relation input object patterns const relationTypePatterns = [ // Connect targets for nested relations /\w+WhereUniqueInput$/, // Nested relation operations /\w+CreateNested(?:One|Many)Without\w+Input$/, /\w+UpdateNested(?:One|Many)Without\w+Input$/, /\w+UpsertNested(?:One|Many)Without\w+Input$/, // Without-variants used in nested create/update /\w+CreateOrConnectWithout\w+Input$/, /\w+CreateWithout\w+Input$/, /\w+UncheckedCreateWithout\w+Input$/, /\w+UpdateWithout\w+Input$/, /\w+UncheckedUpdateWithout\w+Input$/, /\w+UpdateToOneWithWhereWithout\w+Input$/, /\w+UpdateOneRequiredWithout\w+NestedInput$/, /\w+UpdateWithWhereUniqueWithout\w+Input$/, /\w+UpdateManyWithWhereWithout\w+Input$/, /\w+UpsertWithWhereUniqueWithout\w+Input$/, // Relation filter helpers /\w+ListRelationFilter$/, /\w+RelationFilter$/, /\w+ScalarRelationFilter$/, // Order by relation inputs /OrderByRelation/, ]; return field.inputTypes.some((inputType) => { if (inputType.location !== 'inputObjectTypes') return false; const typeName = String(inputType.type); return relationTypePatterns.some((re) => re.test(typeName)); }); } /** * Extract related model name from field arg */ static extractRelatedModelFromFieldArg(field) { for (const inputType of field.inputTypes) { if (inputType.location === 'inputObjectTypes') { const typeName = inputType.type; // Handle specific relation filter patterns first if (typeName.includes('ListRelationFilter')) { // Extract from "PostListRelationFilter" -> "Post" const match = typeName.match(/^(\w+)ListRelationFilter$/); if (match) return match[1]; } // Extract model name from other types like "PostWhereInput", "UserCreateInput", etc. const match = typeName.match(/^(\w+)(?:Where|Create|Update|OrderBy|RelationFilter|CreateNested|UpdateNested)/); if (match) { return match[1]; } } } return null; } /** * Extract model name from object schema context */ static extractModelNameFromContext(schemaName) { // Try to extract model name from common schema naming patterns // More specific patterns should come first to avoid false matches const patterns = [ // Basic input types - more specific patterns first /^(\w+)UncheckedCreateInput$/, /^(\w+)UncheckedUpdateInput$/, /^(\w+)UncheckedUpdateManyInput$/, /^(\w+)UpdateManyMutationInput$/, /^(\w+)WhereUniqueInput$/, /^(\w+)CreateManyInput$/, /^(\w+)UpdateManyInput$/, /^(\w+)WhereInput$/, /^(\w+)CreateInput$/, /^(\w+)UpdateInput$/, // Order by inputs /^(\w+)OrderByWithRelationInput$/, /^(\w+)OrderByWithAggregationInput$/, /^(\w+)OrderByRelationAggregateInput$/, // Filter inputs /^(\w+)ScalarWhereInput$/, /^(\w+)ScalarWhereWithAggregatesInput$/, /^(\w+)ListRelationFilter$/, /^(\w+)RelationFilter$/, /^(\w+)ScalarRelationFilter$/, // Aggregate inputs /^(\w+)CountAggregateInput$/, /^(\w+)CountOrderByAggregateInput$/, /^(\w+)AvgAggregateInput$/, /^(\w+)AvgOrderByAggregateInput$/, /^(\w+)MaxAggregateInput$/, /^(\w+)MaxOrderByAggregateInput$/, /^(\w+)MinAggregateInput$/, /^(\w+)MinOrderByAggregateInput$/, /^(\w+)SumAggregateInput$/, /^(\w+)SumOrderByAggregateInput$/, // Nested operation inputs /^(\w+)CreateNestedOneWithout\w+Input$/, /^(\w+)CreateNestedManyWithout\w+Input$/, /^(\w+)UpdateNestedOneWithout\w+Input$/, /^(\w+)UpdateNestedManyWithout\w+Input$/, /^(\w+)UpsertNestedOneWithout\w+Input$/, /^(\w+)UpsertNestedManyWithout\w+Input$/, /^(\w+)CreateOrConnectWithout\w+Input$/, /^(\w+)UpdateOneRequiredWithout\w+NestedInput$/, /^(\w+)UpdateToOneWithWhereWithout\w+Input$/, /^(\w+)UpsertWithout\w+Input$/, /^(\w+)CreateWithout\w+Input$/, /^(\w+)UpdateWithout\w+Input$/, /^(\w+)UncheckedCreateWithout\w+Input$/, /^(\w+)UncheckedUpdateWithout\w+Input$/, /^(\w+)UncheckedCreateNestedManyWithout\w+Input$/, /^(\w+)UncheckedUpdateManyWithout\w+Input$/, /^(\w+)UncheckedUpdateManyWithout\w+NestedInput$/, // Many-to-many relation inputs /^(\w+)CreateManyAuthorInput$/, /^(\w+)CreateManyAuthorInputEnvelope$/, /^(\w+)UpdateManyWithWhereWithout\w+Input$/, /^(\w+)UpdateWithWhereUniqueWithout\w+Input$/, /^(\w+)UpsertWithWhereUniqueWithout\w+Input$/, /^(\w+)UpdateManyWithout\w+NestedInput$/, // Additional specific patterns found in generated schemas /^(\w+)UncheckedUpdateManyInput$/, // Args and other schemas /^(\w+)Args$/, // Pure model schemas (variant schemas) /^(\w+)ModelSchema$/, // Generic CreateMany relationship patterns (placed at end to avoid interference) // Capture the owning model prefix (e.g. AccountCreateManyUserInputEnvelope -> Account) /^(\w+)CreateMany\w+InputEnvelope$/, /^(\w+)CreateMany\w+Input$/, ]; for (const pattern of patterns) { const match = schemaName.match(pattern); if (match) { return match[1]; } } return null; } /** * Determine schema variant from context */ static determineSchemaVariant(schemaName) { // Determine variant based on schema name patterns if (schemaName.includes('Create') || schemaName.includes('Update') || schemaName.includes('Where')) { return 'input'; } if (schemaName.includes('Result') || schemaName.includes('Output')) { return 'result'; } return 'pure'; // Default variant } /** * Log information about field filtering */ static logFieldFiltering(originalCount, filteredCount, modelName, variant) { if (originalCount !== filteredCount) { const context = modelName ? `${modelName}${variant ? ` (${variant})` : ''}` : 'schema'; logger_1.logger.debug(` 🎯 ${context}: filtered ${originalCount - filteredCount} fields (${filteredCount}/${originalCount} remaining)`); } } /** * Check if a model has relationships to other ENABLED models * This is a filtered version of checkModelHasModelRelation that only considers enabled models */ static checkModelHasEnabledModelRelation(model) { const { fields: modelFields } = model; for (const modelField of modelFields) { if (this.isEnabledRelationField(modelField)) { return true; } } return false; } /** * Check if a relation field points to an enabled model */ static isEnabledRelationField(modelField) { const { kind, relationName, type } = modelField; // Must be an object relation field if (kind !== 'object' || !relationName) { return false; } // Check if the related model is enabled const relatedModelName = type; return this.isModelEnabled(relatedModelName); } /** * Filter relation fields to only include those pointing to enabled models */ static filterRelationFields(fields) { return fields.filter((field) => { // Include non-relation fields if (field.kind !== 'object' || !field.relationName) { return true; } // Only include relation fields if the related model is enabled return this.isEnabledRelationField(field); }); } /** * Get list of enabled related models for a given model */ static getEnabledRelatedModels(model) { const relatedModels = new Set(); for (const field of model.fields) { if (this.isEnabledRelationField(field)) { relatedModels.add(field.type); } } return Array.from(relatedModels); } /** * Check if a model should have include schemas generated * Only if it has relationships to other enabled models */ static shouldGenerateIncludeSchema(model) { const config = this.getGeneratorConfig(); if ((config === null || config === void 0 ? void 0 : config.mode) === 'minimal') return false; return Transformer.isGenerateInclude && this.checkModelHasEnabledModelRelation(model); } /** * Check if a model should have select schemas generated */ static shouldGenerateSelectSchema(_model) { const config = this.getGeneratorConfig(); if ((config === null || config === void 0 ? void 0 : config.mode) === 'minimal') return false; return Transformer.isGenerateSelect; } /** * Log relationship preservation information */ static logRelationshipPreservation(model) { const config = this.getGeneratorConfig(); if (!config) return; const enabledRelatedModels = this.getEnabledRelatedModels(model); const totalRelationFields = model.fields.filter((f) => f.kind === 'object' && f.relationName).length; const enabledRelationFields = model.fields.filter((f) => this.isEnabledRelationField(f)).length; if (totalRelationFields > enabledRelationFields) { const disabledRelations = totalRelationFields - enabledRelationFields; logger_1.logger.debug(` 🔗 ${model.name}: ${disabledRelations} disabled relation(s) to filtered models`); } if (enabledRelatedModels.length > 0) { logger_1.logger.debug(` ✅ ${model.name}: active relations to [${enabledRelatedModels.join(', ')}]`); } } // Dual export setters static setExportTypedSchemas(exportTypedSchemas) { this.exportTypedSchemas = exportTypedSchemas; } static setExportZodSchemas(exportZodSchemas) { this.exportZodSchemas = exportZodSchemas; } static setTypedSchemaSuffix(typedSchemaSuffix) { this.typedSchemaSuffix = typedSchemaSuffix; } static setZodSchemaSuffix(zodSchemaSuffix) { this.zodSchemaSuffix = zodSchemaSuffix; } /** * Generate the correct object schema name based on export settings */ static getObjectSchemaName(baseName) { return this.exportTypedSchemas ? `${baseName}ObjectSchema` : `${baseName}Object${this.zodSchemaSuffix}`; } static getOutputPath() { return this.outputPath; } static setCurrentManifest(manifest) { this.currentManifest = manifest; } static getCurrentManifest() { return this.currentManifest; } static setPrismaClientOutputPath(prismaClientCustomPath) { this.prismaClientOutputPath = prismaClientCustomPath; this.isCustomPrismaClientOutputPath = prismaClientCustomPath !== '@prisma/client'; } /** * Resolve the import specifier for Prisma Client relative to a target directory. * Falls back to '@prisma/client' when user did not configure a custom output. */ static resolvePrismaImportPath(targetDir) { if (!this.isCustomPrismaClientOutputPath) return '@prisma/client'; try { let prismaClientImportTarget = this.prismaClientOutputPath; // For the new Prisma generator (provider = 'prisma-client'), the public entrypoints // live in generated `client.*` / `browser.*` files inside the configured output // directory. The Prisma output config represents the directory, so we explicitly // target a file. We target the browser-safe entrypoint: the server entry (client.*) // hard-imports node: builtins (node:process/node:path/node:url/node:buffer) and // breaks browser bundles, while browser.* exports everything the generated schemas // need (the Prisma namespace with Decimal/DbNull/JsonNull values plus all types). if (this.prismaClientProvider === 'prisma-client') { const hasFileExtension = path_1.default.extname(prismaClientImportTarget) !== ''; if (!hasFileExtension) { prismaClientImportTarget = path_1.default.join(prismaClientImportTarget, this.resolvePrismaClientEntryBasename()); } } // Compute relative path from the file's directory to the custom client output path let rel = path_1.default.relative(targetDir, prismaClientImportTarget).replace(/\\/g, '/'); if (!rel || rel === '') return '@prisma/client'; // Add file extension if needed (for ESM with importFileExtension) const extension = this.getImportFileExtension(); if (!extension || !/\.[a-z]+$/i.test(rel)) { rel = `${rel}${extension}`; } // Ensure it is a valid relative module specifier (prefix with ./ when needed) if (rel.startsWith('.') || rel.startsWith('/')) return rel; return `./${rel}`; } catch { return '@prisma/client'; } } /** * The `import type { Prisma }` line for CRUD operation schema files, or '' when * typed exports are disabled — the typed exports are the only consumers of the * Prisma namespace in those files, so the import would otherwise be unused. */ static prismaTypeImportLine(prismaImportPath) { return Transformer.exportTypedSchemas ? `import type { Prisma } from '${prismaImportPath}';\n` : ''; } /** * Determine which generated prisma-client entrypoint to import. * Prefers the browser-safe `browser.*` entry so generated schemas can be bundled * for client-side use. Falls back to the server `client.*` entry only when an * older prisma-client output layout without a browser entry is positively * detected on disk. When the client output has not been generated yet, assume * the modern layout (browser entry present). */ static resolvePrismaClientEntryBasename() { const outputDir = this.prismaClientOutputPath; const cached = this.prismaClientEntryBasenameCache.get(outputDir); if (cached) return cached; // Default to the server entry: `client.*` is always present in a // prisma-client output, whereas `browser.*` is not guaranteed. The // generators can also run before the client output exists (the directory is // then unreadable), and guessing `browser` there emits an import that does // not resolve — strictly worse than the server entry, which merely is not // browser-optimised. Prefer `browser` only when we can see it on disk. let entry = 'client'; try { // eslint-disable-next-line @typescript-eslint/no-require-imports -- lazy require keeps fs out of the module top level const fs = require('fs'); const entries = fs.readdirSync(outputDir); const entryFileRegex = /^(browser|client)\.(ts|mts|cts|js|mjs|cjs)$/; const hasBrowserEntry = entries.some((name) => { var _a; return ((_a = entryFileRegex.exec(name)) === null || _a === void 0 ? void 0 : _a[1]) === 'browser'; }); const hasClientEntry = entries.some((name) => { var _a; return ((_a = entryFileRegex.exec(name)) === null || _a === void 0 ? void 0 : _a[1]) === 'client'; }); if (hasBrowserEntry) entry = 'browser'; else if (hasClientEntry) entry = 'client'; } catch { // Output directory unreadable or not generated yet: keep the server entry. } this.prismaClientEntryBasenameCache.set(outputDir, entry); return entry; } static setPrismaClientProvider(provider) { this.prismaClientProvider = provider; } static setPrismaClientConfig(config) { this.prismaClientConfig = config; } static getPrismaClientProvider() { return this.prismaClientProvider; } static getPrismaClientConfig() { return this.prismaClientConfig; } /** * Determines the schemas directory path based on the output path. * If the output path already ends with 'schemas', use it directly. * Otherwise, append 'schemas' to the output path. */ static getSchemasPath() { const normalizedOutputPath = path_1.default.normalize(this.outputPath); const pathSegments = normalizedOutputPath.split(path_1.default.sep); const lastSegment = pathSegments[pathSegments.length - 1]; if (lastSegment === 'schemas') { return this.outputPath; } return path_1.default.join(this.outputPath, 'schemas'); } static async generateIndex() { const indexPath = path_1.default.join(Transformer.getSchemasPath(), 'index.ts'); const importExtension = Transformer.getImportFileExtension(); await (0, writeIndexFile_1.writeIndexFile)(indexPath, importExtension); } async generateEnumSchemas() { for (const enumType of this.enumTypes) { const { name, values } = enumType; // Filter out enum schemas for disabled models if (this.isEnumSchemaEnabled(name)) { // Use enum naming configuration const enumNamingConfig = (0, naming_resolver_1.resolveEnumNaming)(Transformer.getGeneratorConfig()); // Normalize enum name for consistent file naming and exports const normalizedEnumName = this.normalizeEnumName(name); const enumName = normalizedEnumName || name; const fileName = (0, naming_resolver_1.generateFileName)(enumNamingConfig.filePattern, enumName, // Use enum name as model name for naming pattern undefined, // No operation undefined, // No input type enumName); const schemaExportName = (0, naming_resolver_1.generateExportName)(enumNamingConfig.exportNamePattern, enumName, // Use enum name as model name for naming pattern undefined, // No operation undefined, // No input type enumName); await (0, writeFileSafely_1.writeFileSafely)(path_1.default.join(Transformer.getSchemasPath(), `enums/${fileName}`), `${this.generateImportZodStatement()}\n` + `export const ${schemaExportName} = z.enum([${values .map((v) => `'${v}'`) .join(', ')}])\n\n` + `export type ${enumName} = z.infer<typeof ${schemaExportName}>;`); } } } /** * Check if an enum schema should be generated based on enabled models */ isEnumSchemaEnabled(enumName) { // Always generate user-defined enums and Prisma built-in enums // Only filter model-specific generated enums based on model configuration // Check if this is a model-specific enum (like PostScalarFieldEnum) const modelName = this.extractModelNameFromEnum(enumName); if (modelName) { // This is a model-specific enum, check if the model is enabled return Transformer.isModelEnabled(modelName); } // For all other enums (user-defined enums like Status, Role, and Prisma built-ins), // always generate them regardless of model configuration return true; } /** * Extract model name from enum name */ extractModelNameFromEnum(enumName) { for (const pattern of Transformer.modelEnumPatterns) { const match = enumName.match(pattern); if (match) { return match[1]; } } return null; } /** * Normalize enum names to match import expectations * For model-related enums, use Pascal case model names consistently */ normalizeEnumName(enumName) { // Use shared patterns to detect and normalize model-specific enums for (const pattern of Transformer.modelEnumPatterns) { const match = enumName.match(pattern); if (match) { const modelName = match[1]; const pascalModelName = this.getPascalCaseModelName(modelName); // Rebuild enum name with normalized model name return enumName.replace(modelName, pascalModelName); } } // Return null for non-model-related enums to use original name return null; } /** * Convert model names to DMMF aggregate input names * For doc_parser_agent model: matches Prisma's DMMF format Doc_parser_agentCountAggregateInput */ getAggregateInputName(modelName, inputSuffix) { // Prisma's DMMF uses original model name with first letter capitalized for aggregate inputs // doc_parser_agent -> Doc_parser_agentCountAggregateInput (not DocParserAgentCountAggregateInput) const capitalizedModelName = modelName.charAt(0).toUpperCase() + modelName.slice(1); return `${capitalizedModelName}${inputSuffix}`; } /** * Get the correct Prisma type name for model types * Prisma uses the original model name as-is (e.g., doc_parser_agent -> Prisma.doc_parser_agentSelect) */ getPrismaTypeName(modelName, operationType) { /* Special case: Aggregate operations capitalize the first letter of model names. This applies to all naming conventions since kebab-case is not allowed in Prisma schemas.*/ if (operationType === 'Aggregate') { return modelName.charAt(0).toUpperCase() + modelName.slice(1); } // All other operations use the original model name without any case transformation return modelName; } /** * Check if a model has numeric fields that support avg/sum operations */ modelHasNumericFields(modelName) { const dmmfModel = this.models.find((m) => m.name === modelName); if (!dmmfModel) return false; return dmmfModel.fields.some((field) => ['Int', 'Float', 'Decimal', 'BigInt'].includes(field.type)); } generateImportZodStatement() { var _a; // Determine import target based on configuration const config = Transformer.getGeneratorConfig(); const target = ((_a = config === null || config === void 0 ? void 0 : config.zodImportTarget) !== null && _a !== void 0 ? _a : 'auto'); // v3 binds a named export (import { z }); auto/v4 bind the namespace. const useNamedBinding = target === 'v3'; const defaultPath = target === 'v4' ? 'zod/v4' : target === 'v3' ? 'zod/v3' : 'zod'; // A custom module path (issue #370) lets users point z at their own // re-export configured with an i18n error map, etc. The binding style still // follows zodImportTarget, so the custom module must export z accordingly. let path = defaultPath; const custom = typeof (config === null || config === void 0 ? void 0 : config.zodImportPath) === 'string' ? config.zodImportPath.trim() : ''; if (custom) { // Guard the generated string literal: a real module specifier never // contains quotes/whitespace. Reject anything else and warn. if (/^[a-zA-Z0-9_@./-]+$/.test(custom)) { path = custom; } else { logger_1.logger.warn(`[prisma-zod-generator] Ignoring invalid zodImportPath "${custom}"; falling back to '${defaultPath}'`); } } return useNamedBinding ? `import { z } from '${path}';\n` : `import * as z from '${path}';\n`; } /** * Generate custom import statements from @zod.import() annotations * * @param customImports - Array of custom import objects * @param outputPath - Current output path for relative import adjustment * @returns Generated import statements string */ generateCustomImportStatements(customImports, outputPath = '') { if (!customImports || customImports.length === 0) { return ''; } const importStatements = []; const seenImports = new Set(); for (const customImport of customImports) { if (!customImport || !customImport.importStatement) { continue; } let importStatement = customImport.importStatement; // Adjust relative paths for multi-file output if (outputPath && customImport.source.startsWith('.')) { importStatement = this.adjustRelativeImportPath(importStatement, outputPath); } // Avoid duplicate imports if (!seenImports.has(importStatement)) { seenImports.add(importStatement); importStatements.push(importStatement); } } if (importS