w-funnel/scripts/sync-funnels-from-db.mjs
2025-09-28 22:48:50 +02:00

291 lines
8.6 KiB
JavaScript
Executable File
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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();