355 lines
10 KiB
JavaScript
355 lines
10 KiB
JavaScript
#!/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);
|
||
});
|