w-funnel/scripts/import-funnels-to-db.mjs
dev.daminik00 0fc1dc756e admin
2025-09-27 05:48:42 +02:00

355 lines
10 KiB
JavaScript
Raw Permalink 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 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);
});