prisma-zod-generator
Version:
Prisma 2+ generator to emit Zod schemas from your Prisma schema
336 lines • 14.3 kB
JavaScript
;
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