#!/usr/bin/env node import fs from 'fs/promises'; import path from 'path'; import { fileURLToPath } from 'url'; import mongoose from 'mongoose'; import dotenv from 'dotenv'; // Load environment variables dotenv.config({ path: '.env.local' }); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // MongoDB connection URI const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/witlab-funnel'; // Mongoose schemas (inline for the script) const FunnelDataSchema = new mongoose.Schema({ meta: { id: { type: String, required: true }, version: String, title: String, description: String, firstScreenId: String }, defaultTexts: { nextButton: { type: String, default: 'Next' }, continueButton: { type: String, default: 'Continue' } }, screens: [mongoose.Schema.Types.Mixed] }, { _id: false }); const FunnelSchema = new mongoose.Schema({ // Основные данные воронки funnelData: { type: FunnelDataSchema, required: true }, // Метаданные для админки name: { type: String, required: true, trim: true, maxlength: 200 }, description: { type: String, trim: true, maxlength: 1000 }, status: { type: String, enum: ['draft', 'published', 'archived'], default: 'published', // Импортированные воронки считаем опубликованными required: true }, // Система версий version: { type: Number, default: 1, min: 1 }, // Пользовательские данные createdBy: { type: String, default: 'import-script' }, lastModifiedBy: { type: String, default: 'import-script' }, // Статистика usage: { totalViews: { type: Number, default: 0, min: 0 }, totalCompletions: { type: Number, default: 0, min: 0 }, lastUsed: Date }, // Timestamps publishedAt: { type: Date, default: Date.now } }, { timestamps: true, collection: 'funnels' }); // Индексы FunnelSchema.index({ 'funnelData.meta.id': 1 }, { unique: true }); FunnelSchema.index({ status: 1, updatedAt: -1 }); const FunnelModel = mongoose.models.Funnel || mongoose.model('Funnel', FunnelSchema); // Utility functions function generateFunnelName(funnelData) { // Пытаемся получить имя из разных источников if (funnelData.meta?.title) { return funnelData.meta.title; } if (funnelData.meta?.id) { // Преобразуем ID в читаемое название return funnelData.meta.id .split('-') .map(word => word.charAt(0).toUpperCase() + word.slice(1)) .join(' '); } return 'Imported Funnel'; } function generateFunnelDescription(funnelData) { if (funnelData.meta?.description) { return funnelData.meta.description; } // Генерируем описание на основе количества экранов const screenCount = funnelData.screens?.length || 0; const templates = funnelData.screens?.map(s => s.template).filter(Boolean) || []; const uniqueTemplates = [...new Set(templates)]; return `Воронка с ${screenCount} экран${screenCount === 1 ? 'ом' : screenCount < 5 ? 'ами' : 'ами'}.${ uniqueTemplates.length > 0 ? ` Типы: ${uniqueTemplates.join(', ')}.` : '' } Импортирована из JSON файла.`; } async function connectToDatabase() { try { await mongoose.connect(MONGODB_URI, { bufferCommands: false, maxPoolSize: 10, serverSelectionTimeoutMS: 5000, socketTimeoutMS: 45000, }); console.log('✅ Connected to MongoDB'); } catch (error) { console.error('❌ Failed to connect to MongoDB:', error.message); process.exit(1); } } async function findFunnelFiles() { const funnelsDir = path.join(__dirname, '..', 'public', 'funnels'); try { const files = await fs.readdir(funnelsDir); const jsonFiles = files.filter(file => file.endsWith('.json')); console.log(`📁 Found ${jsonFiles.length} funnel files in public/funnels/`); return jsonFiles.map(file => ({ filename: file, filepath: path.join(funnelsDir, file) })); } catch (error) { console.error('❌ Failed to read funnels directory:', error.message); process.exit(1); } } async function validateFunnelData(funnelData, filename) { const errors = []; if (!funnelData) { errors.push('Empty funnel data'); } if (!funnelData.meta) { errors.push('Missing meta object'); } else { if (!funnelData.meta.id) { errors.push('Missing meta.id'); } } if (!funnelData.screens) { errors.push('Missing screens array'); } else if (!Array.isArray(funnelData.screens)) { errors.push('screens is not an array'); } else if (funnelData.screens.length === 0) { errors.push('Empty screens array'); } if (errors.length > 0) { console.warn(`⚠️ Validation warnings for ${filename}:`, errors); return false; } return true; } async function importFunnel(funnelFile) { const { filename, filepath } = funnelFile; try { // Читаем и парсим JSON файл const fileContent = await fs.readFile(filepath, 'utf-8'); const funnelData = JSON.parse(fileContent); // Валидируем структуру данных if (!validateFunnelData(funnelData, filename)) { return { filename, status: 'skipped', reason: 'Invalid data structure' }; } // Проверяем, существует ли уже воронка с таким ID const existingFunnel = await FunnelModel.findOne({ 'funnelData.meta.id': funnelData.meta.id }); if (existingFunnel) { return { filename, status: 'exists', funnelId: funnelData.meta.id, reason: 'Funnel with this ID already exists in database' }; } // Создаем новую запись в базе данных const funnel = new FunnelModel({ funnelData: funnelData, name: generateFunnelName(funnelData), description: generateFunnelDescription(funnelData), status: 'published', version: 1, createdBy: 'import-script', lastModifiedBy: 'import-script', usage: { totalViews: 0, totalCompletions: 0 }, publishedAt: new Date() }); const savedFunnel = await funnel.save(); return { filename, status: 'imported', funnelId: funnelData.meta.id, databaseId: savedFunnel._id.toString(), name: savedFunnel.name }; } catch (error) { return { filename, status: 'error', reason: error.message }; } } async function main() { console.log('🚀 Starting funnel import process...\n'); // Подключаемся к базе данных await connectToDatabase(); // Находим все JSON файлы воронок const funnelFiles = await findFunnelFiles(); if (funnelFiles.length === 0) { console.log('📭 No funnel files found to import.'); process.exit(0); } console.log(`\n📥 Starting import of ${funnelFiles.length} funnels...\n`); // Импортируем каждую воронку const results = []; for (let i = 0; i < funnelFiles.length; i++) { const funnelFile = funnelFiles[i]; const progress = `[${i + 1}/${funnelFiles.length}]`; console.log(`${progress} Processing ${funnelFile.filename}...`); const result = await importFunnel(funnelFile); results.push(result); // Показываем результат switch (result.status) { case 'imported': console.log(` ✅ Imported as "${result.name}" (ID: ${result.funnelId})`); break; case 'exists': console.log(` ⏭️ Skipped - already exists (ID: ${result.funnelId})`); break; case 'skipped': console.log(` ⚠️ Skipped - ${result.reason}`); break; case 'error': console.log(` ❌ Error - ${result.reason}`); break; } } // Показываем сводку console.log('\n📊 Import Summary:'); console.log('=================='); const imported = results.filter(r => r.status === 'imported'); const existing = results.filter(r => r.status === 'exists'); const skipped = results.filter(r => r.status === 'skipped'); const errors = results.filter(r => r.status === 'error'); console.log(`✅ Successfully imported: ${imported.length}`); console.log(`⏭️ Already existed: ${existing.length}`); console.log(`⚠️ Skipped (invalid): ${skipped.length}`); console.log(`❌ Errors: ${errors.length}`); console.log(`📁 Total processed: ${results.length}`); // Показываем детальную информацию об импортированных воронках if (imported.length > 0) { console.log('\n📋 Imported Funnels:'); imported.forEach(result => { console.log(` • ${result.name} (${result.funnelId}) - ${result.filename}`); }); } // Показываем ошибки if (errors.length > 0) { console.log('\n❌ Errors:'); errors.forEach(result => { console.log(` • ${result.filename}: ${result.reason}`); }); } // Показываем пропущенные if (skipped.length > 0) { console.log('\n⚠️ Skipped:'); skipped.forEach(result => { console.log(` • ${result.filename}: ${result.reason}`); }); } console.log('\n🎉 Import process completed!'); // Показываем следующие шаги if (imported.length > 0) { console.log('\n📌 Next Steps:'); console.log('• Visit /admin to manage your funnels'); console.log('• Imported funnels are marked as "published"'); console.log('• You can edit them in /admin/builder/[id]'); console.log('• Original JSON files remain unchanged'); } await mongoose.connection.close(); console.log('\n👋 Database connection closed.'); } // Запускаем скрипт main().catch(error => { console.error('\n💥 Fatal error:', error); process.exit(1); });