UNPKG

prisma-zod-generator

Version:

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

336 lines 14.3 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.MANIFEST_FILENAME = void 0; exports.validateOutputPathSafety = validateOutputPathSafety; exports.loadManifest = loadManifest; exports.saveManifest = saveManifest; exports.createNewManifest = createNewManifest; exports.addFileToManifest = addFileToManifest; exports.addDirectoryToManifest = addDirectoryToManifest; exports.safeCleanupOutput = safeCleanupOutput; const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const fs_2 = require("fs"); const logger_1 = require("./logger"); exports.MANIFEST_FILENAME = '.prisma-zod-generator-manifest.json'; // Using ConfigurableSafetyResult from types/safety.ts instead // export interface SafetyValidationResult - REMOVED const DANGEROUS_PATHS = [ 'src', 'lib', 'components', 'pages', 'app', 'utils', 'hooks', 'services', 'api', ]; const PROJECT_FILES = [ 'package.json', 'tsconfig.json', 'next.config.js', 'vite.config.js', 'webpack.config.js', 'rollup.config.js', '.gitignore', 'README.md', ]; const USER_CODE_EXTENSIONS = [ '.ts', '.js', '.tsx', '.jsx', '.vue', '.svelte', '.py', '.java', '.cs', ]; async function validateOutputPathSafety(outputPath, config) { // If safety is disabled, always allow if (!config.enabled) { return { isSafe: true, warnings: [], errors: [], blocked: false, bypassReason: 'Safety system is disabled', }; } const result = { isSafe: true, warnings: [], errors: [], blocked: false, }; try { const resolvedPath = path_1.default.resolve(outputPath); const dirName = path_1.default.basename(resolvedPath); // Combine built-in dangerous paths with custom ones const allDangerousPaths = [...DANGEROUS_PATHS, ...config.customDangerousPaths]; // Check for dangerous path names if (allDangerousPaths.includes(dirName.toLowerCase())) { const message = `Output directory "${dirName}" is a common source code directory name. ` + `Consider using a dedicated subdirectory like "${dirName}/generated" instead.`; if (config.allowDangerousPaths) { result.warnings.push(`${message} (Allowed by configuration)`); } else { result.warnings.push(message); } } if (!fs_1.default.existsSync(resolvedPath)) { return result; } // Combine built-in project files with custom ones const allProjectFiles = [...PROJECT_FILES, ...config.customProjectFiles]; for (const projectFile of allProjectFiles) { const projectFilePath = path_1.default.join(resolvedPath, projectFile); if (fs_1.default.existsSync(projectFilePath)) { const message = `Output directory contains project file "${projectFile}". ` + `This suggests it's a project root directory that should not be cleaned automatically.`; if (config.allowProjectRoots) { result.warnings.push(`${message} (Allowed by configuration)`); } else if (config.warningsOnly) { result.warnings.push(`${message} (Would block but warnings-only mode enabled)`); } else { result.errors.push(message); result.isSafe = false; result.blocked = true; } } } const files = await fs_2.promises.readdir(resolvedPath, { withFileTypes: true }); const suspiciousFiles = []; const manifestExists = files.some((f) => f.name === exports.MANIFEST_FILENAME); for (const file of files) { if (file.name === exports.MANIFEST_FILENAME) continue; if (file.isFile()) { const ext = path_1.default.extname(file.name).toLowerCase(); if (file.name.includes('.zod.') || file.name.includes('.schema.') || file.name === 'index.ts' || file.name.startsWith('prisma-zod-')) { continue; } if (USER_CODE_EXTENSIONS.includes(ext) || !file.name.includes('.') || file.name.startsWith('.env')) { suspiciousFiles.push(file.name); } } } if (suspiciousFiles.length > 0 && !manifestExists) { const baseMessage = `Output directory contains ${suspiciousFiles.length} files that may be user code: ` + `${suspiciousFiles.slice(0, 3).join(', ')}${suspiciousFiles.length > 3 ? ', ...' : ''}. ` + `No manifest file found from previous generator runs.`; if (config.allowUserFiles) { result.warnings.push(`${baseMessage} (Allowed by configuration)`); } else if (suspiciousFiles.length > config.maxUserFiles) { const errorMessage = `Too many potentially user-generated files (${suspiciousFiles.length}) found. ` + `Maximum allowed: ${config.maxUserFiles}. For safety, automatic cleanup is disabled. ` + `Please use a dedicated directory for generated schemas.`; if (config.warningsOnly) { result.warnings.push(`${errorMessage} (Would block but warnings-only mode enabled)`); } else { result.errors.push(errorMessage); result.isSafe = false; result.blocked = true; } } else { result.warnings.push(baseMessage); } } } catch (error) { result.errors.push(`Failed to validate output path safety: ${error}`); result.isSafe = false; } return result; } async function loadManifest(outputPath) { const manifestPath = path_1.default.join(outputPath, exports.MANIFEST_FILENAME); try { const content = await fs_2.promises.readFile(manifestPath, 'utf8'); const manifest = JSON.parse(content); if (!manifest.version || !manifest.generatedAt || !Array.isArray(manifest.files) || !Array.isArray(manifest.directories)) { logger_1.logger.debug(`[safeOutputManagement] Invalid manifest structure in ${manifestPath}`); return null; } return manifest; } catch (error) { logger_1.logger.debug(`[safeOutputManagement] Could not load manifest: ${error}`); return null; } } async function saveManifest(outputPath, manifest) { const manifestPath = path_1.default.join(outputPath, exports.MANIFEST_FILENAME); try { const content = JSON.stringify(manifest, null, 2); await fs_2.promises.writeFile(manifestPath, content, 'utf8'); logger_1.logger.debug(`[safeOutputManagement] Manifest saved to ${manifestPath}`); } catch (error) { logger_1.logger.debug(`[safeOutputManagement] Failed to save manifest: ${error}`); } } function createNewManifest(outputPath, singleFileMode = false, singleFileName) { return { version: '1.0', generatorVersion: process.env.npm_package_version || 'unknown', generatedAt: new Date().toISOString(), outputPath: path_1.default.resolve(outputPath), files: [], directories: [], singleFileMode, singleFileName, }; } function addFileToManifest(manifest, filePath, outputPath) { const relativePath = path_1.default.relative(outputPath, filePath); if (!manifest.files.includes(relativePath)) { manifest.files.push(relativePath); } const dir = path_1.default.dirname(relativePath); if (dir !== '.' && !manifest.directories.includes(dir)) { manifest.directories.push(dir); } } function addDirectoryToManifest(manifest, dirPath, outputPath) { const relativePath = path_1.default.relative(outputPath, dirPath); if (relativePath !== '.' && !manifest.directories.includes(relativePath)) { manifest.directories.push(relativePath); } } async function isLikelyGeneratedFile(filePath) { try { const content = await fs_2.promises.readFile(filePath, 'utf8'); const generatorSignatures = [ '// Generated by prisma-zod-generator', '/* Generated by prisma-zod-generator', 'import { Prisma', 'from "./objects/', 'from "./enums/', 'export const', 'z.object({', 'z.enum([', 'PrismaClient', 'Prisma.', ]; const matchCount = generatorSignatures.reduce((count, sig) => (content.includes(sig) ? count + 1 : count), 0); return matchCount >= 2; } catch (error) { logger_1.logger.debug(`[safeOutputManagement] Could not analyze file ${filePath}: ${error}`); return false; } } async function performSmartCleanup(outputPath) { logger_1.logger.debug('[safeOutputManagement] Performing smart cleanup using pattern analysis'); try { const files = await fs_2.promises.readdir(outputPath, { withFileTypes: true }); const cleanupPromises = []; for (const file of files) { const fullPath = path_1.default.join(outputPath, file.name); if (file.isFile()) { if (file.name === exports.MANIFEST_FILENAME) continue; cleanupPromises.push(isLikelyGeneratedFile(fullPath).then(async (isGenerated) => { if (isGenerated) { await fs_2.promises.unlink(fullPath); logger_1.logger.debug(`[safeOutputManagement] Removed likely generated file: ${file.name}`); } else { logger_1.logger.debug(`[safeOutputManagement] Preserved potentially user file: ${file.name}`); } })); } else if (file.isDirectory()) { const knownGeneratedDirs = ['enums', 'objects', 'schemas', 'results']; if (knownGeneratedDirs.includes(file.name.toLowerCase())) { cleanupPromises.push(fs_2.promises.rm(fullPath, { recursive: true, force: true }).then(() => { logger_1.logger.debug(`[safeOutputManagement] Removed generated directory: ${file.name}`); })); } } } await Promise.all(cleanupPromises); } catch (error) { logger_1.logger.debug(`[safeOutputManagement] Smart cleanup failed: ${error}`); } } async function performManifestBasedCleanup(outputPath, manifest) { logger_1.logger.debug('[safeOutputManagement] Performing manifest-based cleanup'); const cleanupPromises = []; for (const relativePath of manifest.files) { const fullPath = path_1.default.join(outputPath, relativePath); cleanupPromises.push(fs_2.promises .unlink(fullPath) .then(() => logger_1.logger.debug(`[safeOutputManagement] Removed tracked file: ${relativePath}`)) .catch(() => { })); } const sortedDirs = [...manifest.directories].sort((a, b) => b.length - a.length); for (const relativePath of sortedDirs) { const fullPath = path_1.default.join(outputPath, relativePath); cleanupPromises.push(fs_2.promises .rmdir(fullPath) .then(() => logger_1.logger.debug(`[safeOutputManagement] Removed empty directory: ${relativePath}`)) .catch(() => { })); } await Promise.all(cleanupPromises); } async function safeCleanupOutput(outputPath, config, singleFileMode = false, singleFileName) { logger_1.logger.debug(`[safeOutputManagement] Starting safe cleanup for: ${outputPath}`); logger_1.logger.debug(`[safeOutputManagement] Safety config: ${JSON.stringify(config)}`); const safetyResult = await validateOutputPathSafety(outputPath, config); for (const warning of safetyResult.warnings) { logger_1.logger.debug(`[safeOutputManagement] WARNING: ${warning}`); } if (!safetyResult.isSafe && !safetyResult.bypassReason) { const errorMsg = safetyResult.errors.join(' '); logger_1.logger.debug(`[safeOutputManagement] ERROR: ${errorMsg}`); throw new Error(`Unsafe output path detected: ${errorMsg}\n\n` + `To resolve this issue:\n` + `1. Use a dedicated directory for generated schemas (e.g., "./generated" or "./src/generated")\n` + `2. Or use a subdirectory within your source folder (e.g., "./src/zod-schemas")\n` + `3. Configure safety options to allow this path\n` + `4. Or disable safety checks entirely (not recommended)\n\n` + `This safety check prevents accidental deletion of your work.`); } // If safety is bypassed, log the reason if (safetyResult.bypassReason) { logger_1.logger.debug(`[safeOutputManagement] Safety bypassed: ${safetyResult.bypassReason}`); } // Skip cleanup and manifest operations if configured if (!config.skipManifest) { const existingManifest = await loadManifest(outputPath); if (existingManifest) { await performManifestBasedCleanup(outputPath, existingManifest); } else { await performSmartCleanup(outputPath); } } else { logger_1.logger.debug('[safeOutputManagement] Skipping cleanup and manifest operations (skipManifest enabled)'); } const newManifest = createNewManifest(outputPath, singleFileMode, singleFileName); logger_1.logger.debug(`[safeOutputManagement] Safe cleanup completed for: ${outputPath}`); return newManifest; } //# sourceMappingURL=safeOutputManagement.js.map