forked from UKSOURCE/cms.hailearning.edu.vn
220 lines
7.7 KiB
JavaScript
220 lines
7.7 KiB
JavaScript
require("dotenv").config();
|
|
const fs = require("fs").promises;
|
|
const path = require("path");
|
|
const connectDB = require("../config/database");
|
|
const Activity = require("../models/activity");
|
|
|
|
/**
|
|
* Transform activities.json data to match Activity model schema
|
|
*/
|
|
function transformActivity(source, index, heroData) {
|
|
// Return a document that preserves the main activity fields and also
|
|
// keeps the detailed camp information (from `camp-detail`) under the
|
|
// `campDetail` key so it can be queried later.
|
|
return {
|
|
// Add hero section from global hero data if available (support activities/booking variants)
|
|
hero: heroData && Array.isArray(heroData) && heroData.length > 0 ? {
|
|
titleActivities: heroData[0].titleActivities || heroData[0].title || "",
|
|
titleBooking: heroData[0].titleBooking || heroData[0].title || "",
|
|
bannerImageActivities: heroData[0].bannerImageActivities || heroData[0].bannerImage || "",
|
|
bannerImageBooking: heroData[0].bannerImageBooking || heroData[0].bannerImage || "",
|
|
} : {
|
|
titleActivities: "",
|
|
titleBooking: "",
|
|
bannerImageActivities: "",
|
|
bannerImageBooking: "",
|
|
},
|
|
name: source.name || "",
|
|
price: source.price || 0,
|
|
priceText: source.priceText || `from ${source.price || 0} USD`,
|
|
season: Array.isArray(source.season) ? source.season : [],
|
|
age:
|
|
Array.isArray(source.age) && source.age.length === 2
|
|
? source.age
|
|
: [12, 18],
|
|
locations: Array.isArray(source.locations) ? source.locations : [],
|
|
image: source.image || "",
|
|
link: source.link || "",
|
|
program: source.program || "",
|
|
rating: source.rating || 4,
|
|
isActive: typeof source.isActive === 'boolean' ? source.isActive : true,
|
|
order: typeof source.order === 'number' ? source.order : index,
|
|
// Keep the rich camp detail under a schema-friendly key
|
|
campDetail: source['camp-detail'] || source.campDetail || {},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Migration: activities
|
|
* Import activities from data/activities.json into MongoDB
|
|
*/
|
|
async function migrate() {
|
|
try {
|
|
await connectDB();
|
|
console.log("Starting migration: activities...");
|
|
|
|
// Read data file
|
|
const dataPath = path.join(__dirname, "../data/activities.json");
|
|
console.log(`Reading data from ${dataPath}...`);
|
|
|
|
// Use fs.existsSync and fs.readFileSync for synchronous check and read
|
|
const fsSync = require("fs");
|
|
if (!fsSync.existsSync(dataPath)) {
|
|
throw new Error("Data file not found!");
|
|
}
|
|
|
|
const rawData = fsSync.readFileSync(dataPath, "utf8");
|
|
const data = JSON.parse(rawData);
|
|
|
|
// Handle new data structure
|
|
const activitiesData = Array.isArray(data) ? data : data.camps || [];
|
|
const filtersData = Array.isArray(data) ? [] : data.filter || [];
|
|
const heroData = Array.isArray(data) ? null : data.hero || null;
|
|
|
|
console.log(
|
|
`Found ${activitiesData.length} activities and ${filtersData.length} filter groups to migrate.`
|
|
);
|
|
|
|
// --- Migrate Activities ---
|
|
if (activitiesData.length > 0) {
|
|
console.log("Migrating activities...");
|
|
|
|
// Transform data if needed (using the existing transformActivity for consistency, or a new one if structure changed)
|
|
const activitiesToInsert = activitiesData.map(
|
|
(source, index) => transformActivity(source, index, heroData) // Pass heroData to transform function
|
|
);
|
|
|
|
const insertedActivities = await Activity.insertMany(activitiesToInsert, {
|
|
ordered: false,
|
|
});
|
|
console.log(`Inserted ${insertedActivities.length} activities.`);
|
|
} else {
|
|
console.log("No activities to migrate.");
|
|
}
|
|
|
|
// --- Migrate Filters ---
|
|
if (filtersData.length > 0) {
|
|
console.log("Migrating activity filters...");
|
|
|
|
// Deduplicate filters by value (value must be unique per model)
|
|
const seen = new Map();
|
|
const filtersToUpsert = [];
|
|
filtersData.forEach((item, index) => {
|
|
// sanitize incoming filter items (remove any unexpected keys such as `count`)
|
|
const sanitizeItems = (arr) =>
|
|
(Array.isArray(arr) ? arr : [])
|
|
.map((it) => ({
|
|
value: (it && it.value) ? it.value.toString().trim() : "",
|
|
label: (it && it.label) ? it.label.toString().trim() : "",
|
|
}))
|
|
.filter((it) => it.value && it.label);
|
|
|
|
const f = {
|
|
label: item.label || item.name || `Filter ${index + 1}`,
|
|
value: (item.value || (item.label || item.name || `filter-${index + 1}`))
|
|
.toString()
|
|
.trim(),
|
|
items: sanitizeItems(item.items),
|
|
order: item.order || index + 1,
|
|
};
|
|
|
|
if (!f.value) return; // skip invalid
|
|
|
|
if (seen.has(f.value)) {
|
|
// merge items if duplicate in source (merge by `value`, prefer first occurrence)
|
|
const existing = seen.get(f.value);
|
|
const mergedMap = new Map();
|
|
[...existing.items, ...f.items].forEach((it) => {
|
|
if (it && it.value) mergedMap.set(it.value, it);
|
|
});
|
|
existing.items = Array.from(mergedMap.values());
|
|
existing.order = Math.min(existing.order, f.order);
|
|
} else {
|
|
seen.set(f.value, f);
|
|
filtersToUpsert.push(f);
|
|
}
|
|
});
|
|
|
|
if (filtersToUpsert.length === 0) {
|
|
console.log("No valid activity filters to migrate after dedupe.");
|
|
} else {
|
|
// Use bulkWrite with upsert to avoid duplicate-key errors and to update existing docs
|
|
const bulkOps = filtersToUpsert.map((f) => ({
|
|
updateOne: {
|
|
filter: { value: f.value },
|
|
update: { $set: { label: f.label, items: f.items, order: f.order } },
|
|
upsert: true,
|
|
},
|
|
}));
|
|
|
|
// Upsert the consolidated filters into a single Activity document
|
|
// that is used to store global filter definitions (marked by isFiltersDoc: true)
|
|
const upsertResult = await Activity.findOneAndUpdate(
|
|
{ isFiltersDoc: true },
|
|
{ $set: { filters: filtersToUpsert, isFiltersDoc: true } },
|
|
{ upsert: true, new: true }
|
|
);
|
|
console.log(`Upserted filters into Activity document id=${upsertResult._id} groups=${(upsertResult.filters || []).length}`);
|
|
}
|
|
} else {
|
|
console.log("No activity filters to migrate.");
|
|
}
|
|
|
|
console.log("Migration activities completed successfully!");
|
|
|
|
const mongoose = require("mongoose");
|
|
await mongoose.disconnect();
|
|
process.exit(0);
|
|
} catch (error) {
|
|
console.error("Migration error:", error);
|
|
|
|
// If some documents failed but others succeeded, log partial success
|
|
if (error.insertedDocs && error.insertedDocs.length > 0) {
|
|
console.log(
|
|
`Partial success: ${error.insertedDocs.length} documents inserted`
|
|
);
|
|
}
|
|
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Rollback: Delete all activities (use with caution!)
|
|
*/
|
|
async function rollback() {
|
|
try {
|
|
await connectDB();
|
|
console.log("Starting rollback...");
|
|
|
|
const actResult = await Activity.deleteMany({});
|
|
console.log(`✅ Deleted ${actResult.deletedCount} activities`);
|
|
|
|
// Remove any filters document stored as an Activity with isFiltersDoc=true
|
|
const filterResult = await Activity.deleteMany({ isFiltersDoc: true });
|
|
console.log(`✅ Deleted ${filterResult.deletedCount} activity filters documents`);
|
|
|
|
console.log("Rollback completed successfully!");
|
|
|
|
const mongoose = require("mongoose");
|
|
await mongoose.disconnect();
|
|
process.exit(0);
|
|
} catch (error) {
|
|
console.error("Rollback error:", error);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
// Run migration or rollback based on command line arguments
|
|
if (require.main === module) {
|
|
const args = process.argv.slice(2);
|
|
|
|
if (args.includes("--rollback")) {
|
|
rollback();
|
|
} else {
|
|
migrate();
|
|
}
|
|
}
|
|
|
|
module.exports = {migrate, rollback};
|