#!/usr/bin/env node import fs from 'fs/promises'; import path from 'path'; import { fileURLToPath } from 'url'; import { execSync } from 'child_process'; 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); const projectRoot = path.dirname(__dirname); const funnelsDir = path.join(projectRoot, 'public', 'funnels'); // MongoDB connection URI const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/witlab-funnel'; // Mongoose schemas (same as in import 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: 'draft', required: true }, version: { type: Number, default: 1, min: 1 }, createdBy: { type: String, default: 'system' }, lastModifiedBy: { type: String, default: 'system' }, usage: { totalViews: { type: Number, default: 0, min: 0 }, totalCompletions: { type: Number, default: 0, min: 0 }, lastUsed: Date }, publishedAt: { type: Date, default: Date.now } }, { timestamps: true, collection: 'funnels' }); const Funnel = mongoose.model('Funnel', FunnelSchema); async function connectDB() { try { await mongoose.connect(MONGODB_URI); console.log('✅ Connected to MongoDB'); } catch (error) { console.error('❌ MongoDB connection failed:', error.message); process.exit(1); } } async function ensureFunnelsDir() { try { await fs.access(funnelsDir); } catch { await fs.mkdir(funnelsDir, { recursive: true }); console.log('📁 Created funnels directory'); } } async function clearFunnelsDir() { try { const files = await fs.readdir(funnelsDir); for (const file of files) { if (file.endsWith('.json')) { await fs.unlink(path.join(funnelsDir, file)); } } console.log('🧹 Cleared existing JSON files'); } catch (error) { console.error('⚠️ Error clearing funnels directory:', error.message); } } async function getLatestPublishedFunnels() { try { // Группируем по funnelData.meta.id и берем последнюю версию каждой опубликованной воронки const latestFunnels = await Funnel.aggregate([ // Фильтруем только опубликованные воронки { $match: { status: 'published' } }, // Сортируем по версии в убывающем порядке { $sort: { 'funnelData.meta.id': 1, version: -1 } }, // Группируем по ID воронки и берем первый документ (с наибольшей версией) { $group: { _id: '$funnelData.meta.id', latestFunnel: { $first: '$$ROOT' } } }, // Заменяем корневой документ на latestFunnel { $replaceRoot: { newRoot: '$latestFunnel' } } ]); console.log(`📊 Found ${latestFunnels.length} latest published funnels`); return latestFunnels; } catch (error) { console.error('❌ Error fetching funnels:', error.message); throw error; } } async function saveFunnelToFile(funnel) { const funnelId = funnel.funnelData.meta.id; const fileName = `${funnelId}.json`; const filePath = path.join(funnelsDir, fileName); try { // Сохраняем только funnelData (структуру воронки) const funnelContent = JSON.stringify(funnel.funnelData, null, 2); await fs.writeFile(filePath, funnelContent, 'utf8'); console.log(`💾 Saved ${fileName} (v${funnel.version})`); } catch (error) { console.error(`❌ Error saving ${fileName}:`, error.message); throw error; } } async function bakeFunnels() { try { console.log('🔥 Baking funnels...'); execSync('npm run bake:funnels', { cwd: projectRoot, stdio: 'inherit' }); console.log('✅ Funnels baked successfully'); } catch (error) { console.error('❌ Error baking funnels:', error.message); throw error; } } // Парсим аргументы командной строки const args = process.argv.slice(2); const options = { funnelIds: [], dryRun: false, keepFiles: false, }; // Парсим опции for (let i = 0; i < args.length; i++) { const arg = args[i]; if (arg === '--dry-run') { options.dryRun = true; } else if (arg === '--keep-files') { options.keepFiles = true; } else if (arg === '--funnel-ids') { // Следующий аргумент должен содержать ID воронок через запятую const idsArg = args[++i]; if (idsArg) { options.funnelIds = idsArg.split(',').map(id => id.trim()); } } else if (arg === '--help' || arg === '-h') { console.log(` 🔄 Sync Funnels from Database Usage: npm run sync:funnels [options] Options: --dry-run Show what would be synced without actually doing it --keep-files Keep JSON files after baking (useful for debugging) --funnel-ids Sync only specific funnel IDs (comma-separated) --help, -h Show this help message Examples: npm run sync:funnels npm run sync:funnels -- --dry-run npm run sync:funnels -- --funnel-ids funnel-test,ru-career-accelerator npm run sync:funnels -- --keep-files --dry-run `); process.exit(0); } } // Обновляем функцию syncFunnels для поддержки опций async function syncFunnelsWithOptions() { if (options.dryRun) { console.log('🔍 DRY RUN MODE - No actual changes will be made\n'); } console.log('🚀 Starting funnel sync from database...\n'); try { // 1. Подключаемся к базе данных await connectDB(); // 2. Создаем/очищаем папку для воронок (только если не dry-run) await ensureFunnelsDir(); if (!options.dryRun) { await clearFunnelsDir(); } // 3. Получаем последние версии всех опубликованных воронок const allFunnels = await getLatestPublishedFunnels(); // Фильтруем по указанным ID если они заданы let funnels = allFunnels; if (options.funnelIds.length > 0) { funnels = allFunnels.filter(funnel => options.funnelIds.includes(funnel.funnelData.meta.id) ); console.log(`🎯 Filtering to ${funnels.length} specific funnels: ${options.funnelIds.join(', ')}`); } if (funnels.length === 0) { console.log('ℹ️ No published funnels found matching criteria'); return; } // 4. Сохраняем каждую воронку в JSON файл for (const funnel of funnels) { if (options.dryRun) { console.log(`🔍 Would save ${funnel.funnelData.meta.id}.json (v${funnel.version})`); } else { await saveFunnelToFile(funnel); } } // 5. Запекаем воронки в TypeScript if (!options.dryRun) { await bakeFunnels(); } else { console.log('🔍 Would bake funnels to TypeScript'); } // 6. Удаляем JSON файлы после запекания (если не указано сохранить) if (!options.dryRun && !options.keepFiles) { await clearFunnelsDir(); } else if (options.keepFiles) { console.log('📁 Keeping JSON files as requested'); } else if (options.dryRun) { console.log('🔍 Would clean up JSON files'); } console.log('\n🎉 Funnel sync completed successfully!'); console.log(`📈 ${options.dryRun ? 'Would sync' : 'Synced'} ${funnels.length} funnels from database`); } catch (error) { console.error('\n💥 Sync failed:', error.message); process.exit(1); } finally { await mongoose.disconnect(); console.log('🔌 Disconnected from MongoDB'); } } // Запускаем скрипт с опциями syncFunnelsWithOptions();