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};