forked from UKSOURCE/cms.hailearning.edu.vn
337 lines
10 KiB
JavaScript
337 lines
10 KiB
JavaScript
// scripts/migrateVisa.js
|
|
|
|
require("dotenv").config();
|
|
const fs = require("fs").promises;
|
|
const path = require("path");
|
|
const mongoose = require("mongoose");
|
|
const Visa = require("../models/visa");
|
|
|
|
// 1. Đọc file JSON
|
|
async function loadVisaData() {
|
|
const filePath = path.join(__dirname, "..", "data", "visa.json");
|
|
const raw = await fs.readFile(filePath, "utf8");
|
|
return JSON.parse(raw);
|
|
}
|
|
|
|
// 2. Hàm Transform: Đổ dữ liệu từ JSON vào đúng Schema
|
|
function transformVisa(sourceData) {
|
|
// JSON có structure hero.title và hero.summaryList
|
|
return {
|
|
hero: {
|
|
title: sourceData.hero?.title || "Visa",
|
|
summaryList: Array.isArray(sourceData.hero?.summaryList)
|
|
? sourceData.hero.summaryList.map((country) =>
|
|
transformCountry(country),
|
|
)
|
|
: [],
|
|
},
|
|
updatedAt: new Date(),
|
|
};
|
|
}
|
|
|
|
// Helper function: Transform individual country
|
|
function transformCountry(source) {
|
|
return {
|
|
id: source.id || 0,
|
|
name: source.name || "",
|
|
slug: source.slug || "",
|
|
icon: source.icon || "",
|
|
services: Array.isArray(source.services) ? source.services : [],
|
|
detailedView: source.detailedView
|
|
? transformDetailedView(source.detailedView)
|
|
: null,
|
|
};
|
|
}
|
|
|
|
// Helper function: Transform DetailedView
|
|
function transformDetailedView(source) {
|
|
return {
|
|
activeCountry: source.activeCountry
|
|
? transformActiveCountry(source.activeCountry)
|
|
: null,
|
|
relatedCountries: Array.isArray(source.relatedCountries)
|
|
? source.relatedCountries.map((country) => ({
|
|
id: country.id || 0,
|
|
name: country.name || "",
|
|
icon: country.icon || "",
|
|
}))
|
|
: [],
|
|
contactInfo: source.contactInfo
|
|
? transformContactInfo(source.contactInfo)
|
|
: null,
|
|
};
|
|
}
|
|
|
|
// Helper function: Transform ActiveCountry
|
|
function transformActiveCountry(source) {
|
|
return {
|
|
id: source.id || 0,
|
|
name: source.name || "",
|
|
title: source.title || "",
|
|
mainImage: source.mainImage || "",
|
|
description: source.description || "",
|
|
additionalInfo: source.additionalInfo || "",
|
|
tagline: source.tagline || "",
|
|
visaTypes: Array.isArray(source.visaTypes)
|
|
? source.visaTypes.map((type) => ({
|
|
category: type.category || "",
|
|
items: Array.isArray(type.items)
|
|
? type.items.map((item) => ({
|
|
title: item.title || "",
|
|
description: item.description || "",
|
|
}))
|
|
: [],
|
|
}))
|
|
: [],
|
|
visaProcess: source.visaProcess
|
|
? transformVisaProcess(source.visaProcess)
|
|
: null,
|
|
gallery: Array.isArray(source.gallery) ? source.gallery : [],
|
|
visaCategories: source.visaCategories
|
|
? transformVisaCategories(source.visaCategories)
|
|
: null,
|
|
visaService: source.visaService
|
|
? transformVisaService(source.visaService)
|
|
: null,
|
|
};
|
|
}
|
|
|
|
// Helper function: Transform VisaProcess
|
|
function transformVisaProcess(source) {
|
|
return {
|
|
title: source.title || "",
|
|
steps: Array.isArray(source.steps)
|
|
? source.steps.map((step) => ({
|
|
number: step.number || "",
|
|
title: step.title || "",
|
|
description: step.description || "",
|
|
}))
|
|
: [],
|
|
};
|
|
}
|
|
|
|
// Helper function: Transform VisaCategories
|
|
function transformVisaCategories(source) {
|
|
return {
|
|
title: source.title || "",
|
|
steps: Array.isArray(source.steps) ? source.steps : [],
|
|
};
|
|
}
|
|
|
|
// Helper function: Transform VisaService
|
|
function transformVisaService(source) {
|
|
return {
|
|
title: source.title || "",
|
|
steps: Array.isArray(source.steps)
|
|
? source.steps.map((step) => ({
|
|
number: step.number || "",
|
|
title: step.title || "",
|
|
description: step.description || "",
|
|
}))
|
|
: [],
|
|
};
|
|
}
|
|
|
|
// Helper function: Transform ContactInfo
|
|
function transformContactInfo(source) {
|
|
return {
|
|
img: source.img || "",
|
|
sectionTitle: source.sectionTitle || "Visa & Immigration",
|
|
helpText: source.helpText || "Need Help?",
|
|
phone: {
|
|
label: source.phone?.label || "Call Us",
|
|
value: source.phone?.value || "",
|
|
link: source.phone?.link || "",
|
|
},
|
|
email: {
|
|
label: source.email?.label || "Mail Us",
|
|
value: source.email?.value || "",
|
|
link: source.email?.link || "",
|
|
},
|
|
location: {
|
|
label: source.location?.label || "Location",
|
|
address: source.location?.address || "",
|
|
},
|
|
};
|
|
}
|
|
|
|
// 3. Validate data before migration
|
|
function validateVisaData(visaData) {
|
|
const errors = [];
|
|
|
|
if (!visaData.hero) {
|
|
errors.push("Missing hero section");
|
|
}
|
|
|
|
if (!visaData.hero?.title) {
|
|
console.warn("⚠️ Hero title is missing, using default 'Visa'");
|
|
}
|
|
|
|
if (!Array.isArray(visaData.hero?.summaryList)) {
|
|
errors.push("summaryList must be an array");
|
|
} else if (visaData.hero.summaryList.length === 0) {
|
|
errors.push("summaryList is empty");
|
|
} else {
|
|
// Validate each country
|
|
visaData.hero.summaryList.forEach((country, idx) => {
|
|
if (!country.name || !country.slug) {
|
|
errors.push(`Country at index ${idx}: missing name or slug`);
|
|
}
|
|
|
|
if (country.detailedView) {
|
|
if (!country.detailedView.activeCountry) {
|
|
console.warn(
|
|
`⚠️ Country "${country.name}" (${idx}): missing activeCountry details`,
|
|
);
|
|
}
|
|
if (!Array.isArray(country.detailedView.relatedCountries)) {
|
|
errors.push(
|
|
`Country "${country.name}" (${idx}): relatedCountries must be array`,
|
|
);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
return errors;
|
|
}
|
|
|
|
// Helper function: Get data summary
|
|
function getDataSummary(visaData) {
|
|
const summary = {
|
|
heroTitle: visaData.hero?.title || "N/A",
|
|
totalCountries: visaData.hero?.summaryList?.length || 0,
|
|
withDetails: 0,
|
|
withoutDetails: 0,
|
|
byCountry: [],
|
|
};
|
|
|
|
if (visaData.hero?.summaryList) {
|
|
visaData.hero.summaryList.forEach((country) => {
|
|
const hasDetails = !!country.detailedView?.activeCountry;
|
|
const relatedCount = country.detailedView?.relatedCountries?.length || 0;
|
|
|
|
if (hasDetails) {
|
|
summary.withDetails++;
|
|
} else {
|
|
summary.withoutDetails++;
|
|
}
|
|
|
|
summary.byCountry.push({
|
|
name: country.name,
|
|
slug: country.slug,
|
|
hasDetails,
|
|
relatedCountries: relatedCount,
|
|
services: country.services?.length || 0,
|
|
});
|
|
});
|
|
}
|
|
|
|
return summary;
|
|
}
|
|
|
|
// 4. Chạy Migration
|
|
async function migrate() {
|
|
try {
|
|
// Kết nối DB
|
|
console.log("🔗 Connecting to MongoDB...");
|
|
await mongoose.connect(process.env.MONGODB_URI);
|
|
console.log("✅ Connected to MongoDB\n");
|
|
|
|
// A. Lấy dữ liệu thô
|
|
console.log("📖 Loading visa data from JSON...");
|
|
const rawData = await loadVisaData();
|
|
console.log("✅ JSON data loaded\n");
|
|
|
|
// B. Chuẩn hóa dữ liệu theo Schema
|
|
console.log("🔄 Transforming data structure...");
|
|
const visaData = transformVisa(rawData);
|
|
console.log("✅ Data transformation completed\n");
|
|
|
|
// C. Validate dữ liệu
|
|
console.log("✔️ Validating data structure...");
|
|
const errors = validateVisaData(visaData);
|
|
if (errors.length > 0) {
|
|
console.error("❌ Validation errors found:");
|
|
errors.forEach((err, idx) => console.error(` ${idx + 1}. ${err}`));
|
|
process.exit(1);
|
|
}
|
|
console.log("✅ Data validation passed\n");
|
|
|
|
// D. Get summary
|
|
const summary = getDataSummary(visaData);
|
|
|
|
console.log("📊 Migration Summary:");
|
|
console.log(` Hero Title: "${summary.heroTitle}"`);
|
|
console.log(` Total countries: ${summary.totalCountries}`);
|
|
console.log(` With details: ${summary.withDetails}`);
|
|
console.log(` Without details: ${summary.withoutDetails}`);
|
|
console.log(`\n Country Details:`);
|
|
|
|
summary.byCountry.forEach((country) => {
|
|
const detailBadge = country.hasDetails ? "✅" : "❌";
|
|
const detailText = country.hasDetails
|
|
? `(${country.relatedCountries} related)`
|
|
: "(basic only)";
|
|
console.log(
|
|
` ${detailBadge} ${country.name.padEnd(20)} (${country.slug.padEnd(
|
|
12,
|
|
)}) - ${country.services} services ${detailText}`,
|
|
);
|
|
});
|
|
console.log("");
|
|
|
|
// E. Lưu vào DB (Upsert: Có rồi thì update, chưa có thì tạo)
|
|
const existingDoc = await Visa.findOne().sort({ updatedAt: -1 });
|
|
|
|
if (existingDoc) {
|
|
console.log("📝 Updating existing Visa document...");
|
|
console.log(` Document ID: ${existingDoc._id}`);
|
|
|
|
const updated = await Visa.findByIdAndUpdate(
|
|
existingDoc._id,
|
|
{ $set: visaData },
|
|
{ new: true },
|
|
);
|
|
|
|
console.log("✅ Visa document updated successfully");
|
|
console.log(` Updated at: ${updated.updatedAt}`);
|
|
} else {
|
|
console.log("📝 Creating NEW Visa document...");
|
|
|
|
const newDoc = await Visa.create(visaData);
|
|
|
|
console.log("✅ Visa document created successfully");
|
|
console.log(` Document ID: ${newDoc._id}`);
|
|
console.log(` Created at: ${newDoc.createdAt}`);
|
|
}
|
|
|
|
console.log("\n✨ Visa migration completed successfully!");
|
|
} catch (error) {
|
|
console.error("\n❌ Migration failed:");
|
|
console.error(` Error: ${error.message}`);
|
|
|
|
if (error.name === "ValidationError") {
|
|
console.error("\n Validation Errors:");
|
|
Object.keys(error.errors).forEach((field) => {
|
|
console.error(` - ${field}: ${error.errors[field].message}`);
|
|
});
|
|
}
|
|
|
|
if (error.stack) {
|
|
console.error("\n📋 Stack trace:");
|
|
console.error(error.stack);
|
|
}
|
|
|
|
process.exit(1);
|
|
} finally {
|
|
await mongoose.connection.close();
|
|
console.log("\n🔌 MongoDB connection closed");
|
|
process.exit(0);
|
|
}
|
|
}
|
|
|
|
// Run migration
|
|
console.log("🚀 Starting Visa Migration...\n");
|
|
migrate();
|