291 lines
8.6 KiB
JavaScript
Executable File
291 lines
8.6 KiB
JavaScript
Executable File
#!/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 <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();
|