first commit

This commit is contained in:
r2xrzh9q2z-lab
2026-02-02 11:07:09 +07:00
commit d1b931d547
286 changed files with 53992 additions and 0 deletions

View File

@@ -0,0 +1,87 @@
require("dotenv").config();
const fs = require("fs").promises;
const path = require("path");
const connectDB = require("../config/database");
const AboutUs = require("../models/aboutUs");
/**
* Normalize the provided aboutUs.json into the AboutUs model shape.
*/
function transformAboutUs(source) {
const hero = {
banner: source?.hero?.banner || "",
title: source?.hero?.title || "",
breadcrumb: source?.hero?.breadcrumb || "",
};
// Introduce section
const introduce = {
header: source?.introduce?.header || {},
services: Array.isArray(source?.introduce?.services)
? source.introduce.services
: [],
};
// Stats
const stats = Array.isArray(source?.stats) ? source.stats : [];
// Features: header + items
const features = {
header: source?.features?.header || {},
items: Array.isArray(source?.features?.items) ? source.features.items : [],
};
// Activities
const activities = source?.activities || {};
// Newsletter
const newsletter = source?.newsletter || {};
// Events: header + items
const events = {
header: source?.events?.header || {},
items: Array.isArray(source?.events?.items) ? source.events.items : [],
};
return {
hero,
introduce,
stats,
features,
activities,
newsletter,
events,
updatedAt: new Date(),
};
}
/**
* Migration: aboutus
*/
async function migrate() {
try {
await connectDB();
const filePath = path.join(__dirname, "..", "data", "aboutUs.json");
const raw = await fs.readFile(filePath, "utf8");
const source = JSON.parse(raw);
const doc = transformAboutUs(source);
await AboutUs.create(doc);
const mongoose = require('mongoose');
await mongoose.disconnect();
process.exit(0);
} catch (error) {
console.error('Migration error:', error);
process.exit(1);
}
}
// Chạy migration nếu được gọi trực tiếp
if (require.main === module) {
migrate();
}
module.exports = { migrate };

View File

@@ -0,0 +1,32 @@
require('dotenv').config();
const connectDB = require('../config/database');
/**
* Migration: asd
* Created: 11:41:22 2/12/2025
*/
async function migrate() {
try {
// Kết nối database
await connectDB();
console.log('Starting migration: asd...');
// TODO: Thêm code migration của bạn ở đây
console.log('Migration asd completed successfully!');
const mongoose = require('mongoose');
await mongoose.disconnect();
process.exit(0);
} catch (error) {
console.error('Migration error:', error);
process.exit(1);
}
}
// Chạy migration nếu được gọi trực tiếp
if (require.main === module) {
migrate();
}
module.exports = { migrate };

View File

@@ -0,0 +1,38 @@
require("dotenv").config();
const fs = require("fs").promises;
const path = require("path");
const connectDB = require("../config/database");
const Contact = require("../models/contact");
const mongoose = require("mongoose");
/**
* Migration: contact
* Migrate contact data from contact-data.json
*/
async function migrate() {
try {
await connectDB();
// Read contact-data.json file
const contactJsonPath = path.join(__dirname, "../data/contact-data.json");
const contactData = JSON.parse(await fs.readFile(contactJsonPath, "utf8"));
// Migrate data using the model's static method
await Contact.migrateFromJson(contactData);
console.log("Contact migration completed successfully");
await mongoose.disconnect();
process.exit(0);
} catch (error) {
console.error("Migration error:", error);
process.exit(1);
}
}
// Chạy migration nếu được gọi trực tiếp
if (require.main === module) {
migrate();
}
module.exports = { migrate };

View File

@@ -0,0 +1,89 @@
require("dotenv").config();
const fs = require("fs").promises;
const path = require("path");
const connectDB = require("../config/database");
const Safety = require("../models/safety");
const mongoose = require("mongoose");
/**
* Chuẩn hóa safety.json đúng theo safetySchema
*/
function transformSafety(data) {
return {
hero: {
banner: data?.hero?.banner || "",
title: data?.hero?.title || "",
},
approach: {
badge: data?.approach?.badge || "",
title: data?.approach?.title || "",
description: data?.approach?.description || "",
imgs: {
img1: data?.approach?.imgs?.img1 || "",
img2: data?.approach?.imgs?.img2 || "",
},
stats: {
count: data?.approach?.stats?.count || "",
label: data?.approach?.stats?.label || "",
avatars: Array.isArray(data?.approach?.stats?.avatars)
? data.approach.stats.avatars
: [],
},
features: Array.isArray(data?.approach?.features)
? data.approach.features
: [],
cards: Array.isArray(data?.approach?.cards)
? data.approach.cards
: [],
},
philosophy: {
title: data?.philosophy?.title || "",
subtitle: data?.philosophy?.subtitle || "",
cards: Array.isArray(data?.philosophy?.cards)
? data.philosophy.cards
: [],
},
security: {
title: data?.security?.title || "",
subtitle: data?.security?.subtitle || "",
cards: Array.isArray(data?.security?.cards)
? data.security.cards
: [],
},
updatedAt: new Date(),
};
}
/**
* MIGRATION
*/
async function migrate() {
try {
console.log("Starting migration: create_safety_table...");
await connectDB();
const safetyJsonPath = path.join(__dirname, "../data/safety.json");
const raw = await fs.readFile(safetyJsonPath, "utf8");
const source = JSON.parse(raw);
const doc = transformSafety(source);
await Safety.create(doc);
console.log("Migration create_safety_table completed successfully!");
await mongoose.disconnect();
process.exit(0);
} catch (error) {
console.error("Migration error:", error);
process.exit(1);
}
}
if (require.main === module) migrate();
module.exports = { migrate };

View File

@@ -0,0 +1,328 @@
require("dotenv").config();
const fs = require("fs").promises;
const path = require("path");
const connectDB = require("../config/database");
const FAQ = require("../models/faq");
const mongoose = require("mongoose");
/**
* Migration: faq
* Migrate FAQ data from faq-data.json
* Created: 16:22:58 4/12/2025
*/
async function migrate() {
try {
// Kết nối database
await connectDB();
console.log("Starting migration: faq...");
// Read faq-data.json file - cùng cấp với file này
const faqJsonPath = path.join(__dirname, "../data/faq-data.json");
try {
const faqData = JSON.parse(await fs.readFile(faqJsonPath, "utf8"));
console.log("FAQ JSON data loaded successfully");
// Đảm bảo có trường name
if (!faqData.name) {
faqData.name = "default";
}
// Migrate data using the model's static method
const result = await FAQ.importFromJson(faqData);
// Tính tổng số FAQ
const totalFaqs = result.faqSections.reduce((total, section) => {
return total + (section.faqs ? section.faqs.length : 0);
}, 0);
console.log("FAQ migration completed successfully");
console.log(`Total sections migrated: ${result.faqSections.length}`);
console.log(`Total FAQs migrated: ${totalFaqs}`);
console.log(`FAQ ID: ${result._id}`);
} catch (fileError) {
console.error("Error reading FAQ JSON file:", fileError.message);
// Nếu không có file JSON, tạo data mẫu với dữ liệu đầy đủ
console.log("Creating complete FAQ data...");
const defaultFaqData = {
name: "default",
hero: {
title: "Go and Grow Camp",
backgroundImage: "yootheme/cache/18/faqs_header_new.jpg",
overlayColor: "rgba(0, 0, 0, 0)",
sectionClass: "uk-section-secondary uk-section-overlap uk-preserve-color uk-light",
titleClass: "uk-heading-large uk-text-center !text-[5vw]",
enableScrollspy: true,
backgroundPosition: "top-center"
},
sidebarNav: [
{
id: "general-information",
label: "General Information"
},
{
id: "camps",
label: "Camps"
},
{
id: "camp-routine",
label: "Camp Routine"
},
{
id: "camp-counselors",
label: "Camp Counselors"
},
{
id: "camp-rules",
label: "Camp Rules"
},
{
id: "safety",
label: "Safety"
},
{
id: "accommodation-catering",
label: "Accommodation & Catering"
},
{
id: "transfers-shuttles",
label: "Transfers & Shuttles"
}
],
contactBox: {
title: "Let's plan your perfect nature escape",
phone: {
icon: "phone",
text: "+(123)-456-789"
},
email: {
icon: "email",
text: "hello@ggcamp.org"
}
},
faqSections: [
{
id: "general-information",
title: "General Information",
faqs: [
{
title: "What are FAQ?",
description: "FAQ are the initials for \"Frequently Asked Questions\".\n\nThe FAQ have been compiled by us over a long period of time and are intended to help give a general overview of our camps and clarify questions that arise before booking a camp."
},
{
title: "General booking process",
description: "Once the booking has been confirmed by us, you will receive an e-mail requesting a deposit. As soon as we have received this, you will receive an e-mail with a payment confirmation.\nPlease have a look at the welcome package, which will reach you by e-mail with the Last Travel Information. This contains information that applies to the camp you have booked.\n\nStep 1: Registration\nStep 2: Receipt of registration confirmation, total invoice and deposit request (e-mail)\nStep 3: Deposit of USD 50 (due within 7 days after booking)\nStep 4: Receiving an email with the latest important travel information, a packing list, addresses and important emergency phone numbers plus remaining payment request about 3-4 weeks before the camp starts."
},
{
title: "Terms & Conditions",
description: "Our Terms & Conditions can be found in our official documents section."
},
{
title: "Where can I find a packing guide for Camps?",
description: "Just click here to download our packing list."
},
{
title: "Where can I find contact information from Camps and addresses?",
description: "Here you can find all the necessary information if you want to drive to our camps or send something. If you want to send something please ALWAYS include the full name of your child on the letter/package and please only send it at the time when your kids are staying in camp as we cannot store it for a longer period of time.\n\nWalsrode/Lüneburger Heide - Germany:\nCamp Adventure, Vethem 58, 29664 Walsrode, Germany\nwalsrode@campadventure.de\n\nRegen/Bavarian Forest - Germany:\nCamp Adventure, Badstrasse 18, 94209 Regen\nregen@campadventure.de\n\nBarcelona - Spain:\nBISC International Sailing Center, c/o Camp Adventure, Parc del Fòrum Sota plaça fotovoltàica, 08930 Sant Adrià de Besòs, Barcelona, Spain\nbarcelona@campadventure.de\n\nBath - England:\nUniversity of Bath, c/o Camp Adventure, Claverton Down, Bath BA2 7AY, England\nengland@campadventure.de\n\nRossall - England:\nRossall School, Broadway, Fleetwood, Lancashire FY7 8JW, England\nengland@campadventure.de"
}
]
},
{
id: "camps",
title: "Camps",
faqs: [
{
title: "Where do kids and camp counselors come from?",
description: "Camp Adventure attaches great importance to internationality. The participants and supervisors in our camps come from many different countries. Last year, for example, we had participants from over 60 different countries and counselors from 25 different nations. Of course, we don't know where they will come from this year. So we are at least as excited as you are.\n\nThrough our office in Hamburg and our branch office in Canada, we reach motivated and committed counselors from all over the world. Canadian and Australian teamers can therefore be found as well as German or Spanish teamers.\n\nDue to the different experiences and cultural backgrounds an indescribably fantastic, international atmosphere is created."
},
{
title: "Which languages are spoken in camp?",
description: "The main language in all our camps is English. In addition, there is the language of the country in which the camp takes place. As we have our headquarters in Germany, German teamers are always present in all camps in Germany. All announcements and explanations are here therefore always in German and English. Of course, all our teamers with their different nationalities are also available for individual translations."
},
{
title: "Are there problems if children have low language skills?",
description: "No, because there are usually more participants and team members who speak the same language. We know from experience that children are excellent at communicating nonverbally. They often need a few days to warm up to it, but are then very open to other children as well."
},
{
title: "Are girls and boys separated?",
description: "Girls and boys are accommodated separately in the dormitories/tents. The program is completely mixed."
},
{
title: "How big are the camps? How high is the caregiver ratio?",
description: "Capacities range from around 30 participants in smaller language camps to a maximum of about 400 children in our camp Lueneburger Heide. However, the maximum capacity is not reached every week. However, a minimum number of participants must be guaranteed in order to run the camp.\n\nIt is important to us that all children are always grouped in small groups of 5-8, with a counselor as a contact person. This way homesickness doesn't stand a chance and despite the size of the camp in their group family, they experience a strong bond on which they can count on!"
},
{
title: "Should 12-year-olds go to Junior Camp or Senior Camp?",
description: "This question is not easy to answer and depends on the individual stage of development of your child. Therefore, as parents, we leave you the opportunity to decide for yourself. In the Junior Camp they belong to the older ones and can explore a lot in a playful way. In the Senior Camp they are the younger ones, who have role models through the older ones, whom they can emulate."
}
]
},
{
id: "camp-routine",
title: "Camp Routine",
faqs: [
{
title: "How is the choice of activities/courses in the camps made?",
description: "If your child would like to participate in a paid additional course (e.g. horse riding, language course, Survival etc.), this must be booked in advance when registering. In principle, no extra additional courses have to be booked. A program with a variety of activities is of course available to the participants in all camps. The various activities can be chosen by the participants on site in the respective camps. We present the offers to the participants, so that everyone gets an insight into the different courses. The children can then register in the lists of the respective courses."
},
{
title: "What is a hike?",
description: "The hike is a 1-3 day walking tour, in which all participants of the Adventure Camp who stay 2 weeks in the camp take part. On this hike the participants will not spend the night in a tent, but either in the open air or under a self-made shelter e.g. from tarpaulins. They will of course be accompanied by their teamers. The hike is a very special experience and a highlight for all participants. For this hike the participants need sturdy shoes and a big backpack."
},
{
title: "Can I wash my clothes during the camp?",
description: "In principle, participants should bring sufficient clothing and change of clothes for the entire camp period.\n\nOnly in the camps in Lüneburger Heide and Bayerischer Wald a laundry service will be offered for kids staying three weeks or more, which means that a laundry bag (approx. 3 kg) will be washed in the laundry centre of the next village at a price of USD 45. This service can be booked upon registration for three-week camps. Please note that the laundry will be done either after one week or after two weeks."
},
{
title: "Anti Homesick Adviser",
description: "Dear parents\n\nNow it's almost time: In summer your child travels for the first time with Camp Adventure. Maybe it will be the first time that he travels alone without parents or relatives. As we are getting more and more questions, we have decided to put together a small package for you parents with little tips from experts to make everything as easy as possible for you and your child. Follow our tips and your child will have a fantastic holiday, have many new experiences and make friends from all over the world! All these tips have been developed together with the International Camping Fellowship. And the more you think your child will be a \"homesick candidate\" - or your child even claims to be one - the more you consider the following tips."
}
]
},
{
id: "camp-counselors",
title: "Camp Counselors - Our Teamers",
faqs: [
{
title: "Who are the camp counselors?",
description: "Every year our team is made up of an international mix. The non-profit association Camp Europe e.V. with headquarters in Hamburg and a branch office in Canada takes care of the acquisition of national and international applicants. Since we have about 50% German-speaking children, there are also German carers in every location. But many also come from other countries, such as England, Spain, Canada and Australia, to name just a few."
},
{
title: "How are the teamers trained?",
description: "All counselors go through an extensive application process. For a successful application, not only an interesting curriculum vitae and a minimum age of 19 years are sufficient! We conduct a personal interview with each individual in which our employees get a first impression of the applicant.\n\nBefore the camp season, everyone, both the first supervisors (teamers) as well as many recomers, complete a one-week training in which they are prepared for their assignment by trained coaches. They must have a first aid certificate, which may not be older than two years, as well as an internationally flawless police clearance certificate. We know how important the teamers are for a great camp and therefore select them very conscientiously."
}
]
},
{
id: "camp-rules",
title: "Camp Rules",
faqs: [
{
title: "Drugs, Alcohol & Camp?",
description: "From our point of view an absolutely unacceptable and indiscutable combination! Due to our cooperation with the association \"Keine Macht den Drogen\" (No power to drugs) and our common opinion that all kinds of drugs do not belong in the hands of children & teenagers, any possession or consumption of drugs is forbidden for teenagers and children in the camp and also outside the camp.\n\nViolations can lead to exclusion or even to criminal charges. The term \"drugs\" also includes cigarettes and alcohol! Through our varied activities, we offer a much better alternative! We would like to make it clear from the outset that we are also against any form of discrimination or \"putting down\". This is - just like violence - immediately prevented by us, in order to offer each young person a relaxed and joyful time in the camp."
},
{
title: "Should I call my kid or write an old-fashioned letter?",
description: "We ask all parents to write to their child at least once. This is especially useful at the beginning, as it is a particularly upsetting experience for every child and every teenager when most of the participants receive a letter, but they do not.\n\nPlease note that there is NO public \"camp phone\" available for incoming or outgoing calls. If your child doesn't bring her/his own phone, she/he won't be able to call you. In case of any problems, we will of course contact you immediately.\n\nIf your child brings a mobile phone, we will collect it on arrival and store it with the valuables. Your child's Teamer may hand it over during the phone time after lunch. Please keep in mind: no news is good news (the location manager will contact you if it is necessary due to homesickness or illness). We kindly ask you not to call the office in Hamburg to ask about your kid's health and wellbeing, nor if you would like to know why your child hasn't called you yet. Please use our camp email service for such enquiries.\n\nOur recommendation is the following:\nWe recommend not to call your child (even if he or she has a mobile phone with him or her) and not to tell him or her to call you. Telephoning can in our experience promote homesickness very strongly and your child will be cured thereby if completely immersed in camp life! At noon after lunch, if absolutely necessary, your child can pick up his or her mobile phone from the counselors until the start of next program and make phone calls. Instead, you are welcome to bring a pre-stamped and addressed envelope with you. We will then make sure that your child has enough time to write letters. Since letters and postcards often arrive late at the camps, we also offer the e-mail service. You can send your child max. ONE email per day directly to the camp, which we then print out and give to your child. There is no way for them to reply, but your child will be happy to receive a small message from home. You can find the postal and email address in the info package of the booked camp."
},
{
title: "Are there any prohibited items?",
description: "Yes, there are. Not allowed are pocket knives with lockable blades, all weapons, lighters and matches (danger of fire in the forest!). Drugs of any kind, including alcohol and cigarettes, are also included."
}
]
},
{
id: "safety",
title: "Safety",
faqs: [
{
title: "Electronic equipment and valuables",
description: "We recommend that you do not take an MP3 player, e-book, tablet, etc. or any valuables with you. On the one hand we do not assume any liability and on the other hand there are no possibilities to charge the devices. We are of the opinion that the camp time is a special experience for the participants if they do not have the headphones in their ears all the time or are busy with their mobile phones. Instead they have the chance to deal with other topics and they find time to dedicate themselves to the new people in the camp."
},
{
title: "How do you provide safety for the kids?",
description: "Before our camp counselors start working with us, we check their police clearance certificates. You must be at least 19 years old to work for us as a teamer. They must also have a \"First Aid Certificate\", which must not be older than two years. In the camps we try to make sure that only adults from our camp or familiar faces are on the campground and that all our carers look after strangers.\n\nWe have many different camp sites. Some of them are fenced in, others are not. There are no armed guards or the like in our camps, as we believe that these conditions create a very insecure feeling. We do not have a high security zone in Germany, Northern Ireland or England, but we keep our eyes open and do everything we can to ensure that all participants have a great time."
},
{
title: "Insurance in case of illness?",
description: "If your child should fall ill during the camp and medical help is required, he or she will of course be taken to the doctor by our carers and cared for there as well. It is therefore necessary for each participant to take their insurance card with them to the camp. We offer all participants the possibility of taking out liability, casualty & health insurance for travel abroad with us. This covers all costs in case of illness and prevents international children in particular from having to \"advance\" their own cash. You can find more detailed information on insurance in our documents section."
}
]
},
{
id: "accommodation-catering",
title: "Accommodation & Catering",
faqs: [
{
title: "How's the food at the camps?",
description: "Full board for the entire duration of the camp is of course already included in the camp price. In addition, water and fruit are available for the participants around the clock. For us it is a matter of course to provide one variant for vegetarians and one pork-free with each meal. In case of special allergies or intolerances of your children let us know in advance and we will try to find a solution."
},
{
title: "How is my child accommodated in the camp?",
description: "In our Adventure Camp Bayerischer Wald and our Camp Lueneburger Heide, the Juniors (7-12) and the Seniors (12-16) can choose between tents and huts.\n\nThe tents are equipped with a floor and a wooden platform, up to 7 children can share one tent. The participants can make themselves comfortable with sleeping bag and sleeping mat. The wooden huts are equipped with bunk beds and can accommodate 4-8 children. At the other locations, participants will be accommodated in shared rooms in youth hostels, sports centres or boarding schools of private schools. You will find detailed information about the accommodation on the individual camp pages."
}
]
},
{
id: "transfers-shuttles",
title: "Transfers & Shuttles",
faqs: [
{
title: "Entry regulations/Travel Consent for group flights",
description: "All parents need to fill this out and bring it to camp:\n\nBelow is a summary of the travel requirements for minors from various EU countries traveling with Camp Adventure on group flights. Please note that regulations can change, so it's essential to consult the official resources provided for the most up-to-date information."
},
{
title: "Which transfers are offered?",
description: "The respective transfer possibilities depend on the period and venue of the camp. Check directly on the respective camp page under \"Arrival & Departure Services\"."
},
{
title: "Where can I find the exact arrival and departure times?",
description: "Information about the different arrival and departure times can be found on the respective camp page under \"Arrival & Departure Services\"."
},
{
title: "How do the transfer costs come about?",
description: "When booking a train or air trip, the indicated price includes the arrival and departure as well as the accompaniment by a supervisor."
},
{
title: "Where can I find the address/driving directions from the camp?",
description: "You will receive the exact address and directions of the camp with the Last Travel Information about 3-4 weeks before the camp starts."
}
]
}
],
video: {
url: "https://www.youtube.com/embed/3NtE5wSwYTo?list=PLSOedrxa1c-bxvH6uuz_oZdIfJkov66wB&disablekb=1",
title: "Anti Homesickness Adviser"
}
};
const result = await FAQ.importFromJson(defaultFaqData);
console.log("Complete FAQ data created successfully");
console.log(`FAQ ID: ${result._id}`);
}
// Kiểm tra lại data đã được lưu
const savedFaq = await FAQ.findOne({ name: "default" });
if (savedFaq) {
const totalFaqs = savedFaq.faqSections.reduce((total, section) => {
return total + (section.faqs ? section.faqs.length : 0);
}, 0);
console.log("\n=== Migration Summary ===");
console.log(`FAQ Name: ${savedFaq.name}`);
console.log(`Hero Title: ${savedFaq.hero.title}`);
console.log(`Sidebar Items: ${savedFaq.sidebarNav.length}`);
console.log(`FAQ Sections: ${savedFaq.faqSections.length}`);
console.log(`Total FAQ Items: ${totalFaqs}`);
console.log(`Created At: ${savedFaq.createdAt}`);
console.log(`Updated At: ${savedFaq.updatedAt}`);
// Hiển thị chi tiết từng section
console.log("\n=== FAQ Sections Details ===");
savedFaq.faqSections.forEach((section, index) => {
console.log(`Section ${index + 1}: ${section.title} (${section.faqs.length} FAQs)`);
});
console.log("=========================\n");
}
await mongoose.disconnect();
console.log("Migration faq completed successfully!");
process.exit(0);
} catch (error) {
console.error("Migration error:", error);
if (mongoose.connection.readyState !== 0) {
await mongoose.disconnect();
}
process.exit(1);
}
}
// Chạy migration nếu được gọi trực tiếp
if (require.main === module) {
migrate();
}
module.exports = { migrate };

View File

@@ -0,0 +1,79 @@
const mongoose = require('mongoose');
const AboutUs = require('../models/aboutUs');
const fs = require('fs');
require('dotenv').config();
// Load and clean JSON data
const raw = fs.readFileSync(require('path').join(__dirname, '..', 'data', 'aboutUs.json'), 'utf8');
let data = JSON.parse(raw || '{}');
// Remove _id fields recursively to avoid conflicts
function stripIds(obj) {
if (Array.isArray(obj)) return obj.map(i => stripIds(i));
if (obj && typeof obj === 'object') {
const out = {};
for (const k in obj) {
if (k !== '_id') out[k] = stripIds(obj[k]);
}
return out;
}
return obj;
}
data = stripIds(data);
// Check for --dry-run flag
const dryRun = process.argv.includes('--dry-run') || process.argv.includes('-n');
async function importAboutUs() {
try {
const dbUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/ggcamps';
console.log('📍 Using DB URI:', dbUri);
if (dryRun) {
console.log('\n🔍 === DRY RUN MODE ===');
console.log('Document to be upserted (preview only, no DB changes):\n');
console.log(JSON.stringify(data, null, 2));
console.log('\n=== END DRY RUN ===\n');
console.log('To actually import, run without --dry-run flag');
process.exit(0);
}
console.log('🔄 Connecting to database...');
await mongoose.connect(dbUri);
console.log('✓ Connected to database');
// Safe upsert: update existing doc or create new one
console.log('📥 Upserting AboutUs document (safe mode)...');
const result = await AboutUs.findOneAndUpdate({}, data, {
upsert: true,
new: true,
setDefaultsOnInsert: true
});
console.log('✅ Successfully upserted AboutUs data!');
console.log('📝 Document ID:', result._id.toString());
console.log('📊 Data structure:');
console.log(' - Hero:', data.hero ? '✓' : '✗');
console.log(' - Introduction:', data.introduction ? '✓' : '✗');
console.log(' - Introduction Services:', data.introduction?.services?.length || 0, 'items');
console.log(' - Statistics:', data.statistics ? '✓' : '✗');
console.log(' - Statistics Items:', data.statistics?.items?.length || 0, 'items');
console.log(' - Accommodation:', data.accommodation ? '✓' : '✗');
console.log(' - Accommodation Features:', data.accommodation?.features?.length || 0, 'items');
console.log(' - Activities:', data.activities ? '✓' : '✗');
console.log(' - Activities Gallery:', data.activities?.gallery?.length || 0, 'items');
console.log(' - Newsletter:', data.newsletter ? '✓' : '✗');
console.log(' - Events:', data.events ? '✓' : '✗');
console.log(' - Events Items:', data.events?.items?.length || 0, 'items');
process.exit(0);
} catch (error) {
console.error('❌ Error:', error.message);
console.error(error);
process.exit(1);
}
}
// Run import
importAboutUs();

View File

@@ -0,0 +1,105 @@
require('dotenv').config();
const fs = require('fs').promises;
const path = require('path');
const connectDB = require('../config/database');
const CampLocation = require('../models/campLocation');
async function validateCampLocationData(data) {
const requiredFields = [
'metadata',
'hero',
'camps',
'locations',
'intro',
'faq',
'welcomeQuote',
'securityConcept'
];
const missingFields = requiredFields.filter(field => !data[field]);
if (missingFields.length > 0) {
throw new Error(`Missing required fields: ${missingFields.join(', ')}`);
}
// Validate camps array
if (!Array.isArray(data.camps)) {
throw new Error('Camps must be an array');
}
// Validate each camp has required fields
data.camps.forEach((camp, index) => {
if (!camp.id) {
throw new Error(`Camp at index ${index} is missing required field: id`);
}
if (!camp.title) {
throw new Error(`Camp at index ${index} is missing required field: title`);
}
});
// Validate locations array
if (!Array.isArray(data.locations) || data.locations.length === 0) {
throw new Error('Locations must be a non-empty array');
}
// Validate FAQ array
if (!Array.isArray(data.faq) || data.faq.length === 0) {
throw new Error('FAQ must be a non-empty array');
}
// Validate security concept items
if (!Array.isArray(data.securityConcept.items) || data.securityConcept.items.length === 0) {
throw new Error('Security concept items must be a non-empty array');
}
console.log('✓ Data validation passed');
}
/**
* Migration: camp_location
* Created: 13:18:38 9/12/2025
* Imports camp location data including camps, locations, FAQ, and security information
*/
async function migrate() {
try {
// Kết nối database
await connectDB();
console.log('Starting migration: camp_location...');
// Delete existing data
const deleteResult = await CampLocation.deleteMany({});
console.log(`✓ Deleted ${deleteResult.deletedCount} existing records`);
// Read JSON file
const campLocationData = JSON.parse(
await fs.readFile(path.join(__dirname, '../data/camp-location.json'), 'utf8')
);
console.log('✓ Loaded camp-location.json');
// Validate data
await validateCampLocationData(campLocationData);
// Create new record
const result = await CampLocation.create(campLocationData);
console.log('✓ Created camp location record');
console.log(` - ${result.camps.length} camps`);
console.log(` - ${result.locations.length} locations`);
console.log(` - ${result.faq.length} FAQ items`);
console.log(` - ${result.securityConcept.items.length} security measures`);
console.log('Migration camp_location completed successfully!');
const mongoose = require('mongoose');
await mongoose.disconnect();
process.exit(0);
} catch (error) {
console.error('Migration error:', error);
process.exit(1);
}
}
// Chạy migration nếu được gọi trực tiếp
if (require.main === module) {
migrate();
}
module.exports = { migrate };

View File

@@ -0,0 +1,81 @@
require('dotenv').config();
const fs = require('fs').promises;
const path = require('path');
const connectDB = require('../config/database');
const Insurance = require('../models/insurance');
const mongoose = require('mongoose');
/**
* Migration: insurance
* Created: 11:13:38 10/12/2025
* Updated: 14/12/2025 - Simplified for new structure only
*/
async function migrate() {
try {
console.log('Starting migration: insurance...');
await connectDB();
// Đọc file insurance.json
const insuranceJsonPath = path.join(__dirname, '../data/insurance.json');
console.log('Reading JSON file from:', insuranceJsonPath);
const insuranceData = JSON.parse(await fs.readFile(insuranceJsonPath, 'utf8'));
console.log('Insurance data loaded successfully');
// Sử dụng phương thức migrateFromJson của model
await Insurance.migrateFromJson(insuranceData);
console.log('Insurance migration completed successfully!');
await mongoose.disconnect();
process.exit(0);
} catch (error) {
console.error('Migration error:', error);
process.exit(1);
}
}
/**
* Custom migration logic cho insurance data
*/
async function migrateInsuranceData(insuranceData) {
try {
console.log('Starting custom migration logic...');
// Xóa dữ liệu cũ nếu có
const existingInsurance = await Insurance.find({});
if (existingInsurance.length > 0) {
console.log(`Found ${existingInsurance.length} existing insurance documents`);
await Insurance.deleteMany({});
console.log('Cleared existing insurance data');
}
// Tạo document mới
const insuranceDocument = new Insurance({
name: 'default',
version: '2.0.0',
language: 'en',
hero: insuranceData.hero,
page: insuranceData.page,
content: insuranceData.content,
createdAt: new Date(),
updatedAt: new Date(),
isActive: true
});
// Lưu vào database
await insuranceDocument.save();
console.log('Insurance document saved successfully');
} catch (error) {
console.error('Error in migrateInsuranceData:', error);
throw error;
}
}
// Chạy migration nếu được gọi trực tiếp
if (require.main === module) {
migrate();
}
module.exports = { migrate, migrateInsuranceData };

View File

@@ -0,0 +1,308 @@
require('dotenv').config();
const fs = require('fs').promises;
const path = require('path');
const connectDB = require('../config/database');
const Terms = require('../models/terms');
const mongoose = require('mongoose');
/**
* Migration: terms
* Migrate Terms & Conditions data từ terms-conditions.json
* Đã sửa để phù hợp với cấu trúc mới: hero, page, content
* Created: 12:00:47 10/12/2025
*/
async function migrate() {
try {
// Kết nối database
await connectDB();
console.log('Starting migration: terms...');
// Đọc file terms-conditions.json
const termsJsonPath = path.join(__dirname, '../data/terms-conditions.json');
console.log('Reading JSON file from:', termsJsonPath);
const termsData = JSON.parse(await fs.readFile(termsJsonPath, 'utf8'));
console.log('Terms data loaded successfully');
console.log('Data structure keys:', Object.keys(termsData));
// Kiểm tra cấu trúc và gọi method phù hợp
if (termsData.hero && termsData.page && termsData.content) {
// Cấu trúc mới - sử dụng migrateFromNewJson
console.log('Detected new structure, using migrateFromNewJson...');
if (typeof Terms.migrateFromNewJson === 'function') {
await Terms.migrateFromNewJson(termsData);
console.log('Migration completed using migrateFromNewJson method');
} else {
console.log('migrateFromNewJson not found, using custom logic...');
await migrateTermsData(termsData);
}
} else if (termsData.hero && termsData.termsHeader && termsData.sections) {
// Cấu trúc cũ - sử dụng migrateFromJson
console.log('Detected old structure, using migrateFromJson...');
if (typeof Terms.migrateFromJson === 'function') {
await Terms.migrateFromJson(termsData);
console.log('Migration completed using migrateFromJson method');
} else {
await migrateTermsData(termsData);
}
} else {
// Không xác định được cấu trúc
console.error('Unknown data structure. Keys:', Object.keys(termsData));
throw new Error('Unknown terms data structure');
}
console.log('Terms migration completed successfully!');
await mongoose.disconnect();
process.exit(0);
} catch (error) {
console.error('Migration error:', error);
process.exit(1);
}
}
/**
* Custom migration logic cho terms data với cấu trúc mới
* Chỉ có 3 phần: hero, page, content
*/
async function migrateTermsData(termsData) {
try {
console.log('Starting custom migration logic with new structure...');
// 1. Xóa dữ liệu cũ (tùy chọn)
const existingTerms = await Terms.find({});
if (existingTerms.length > 0) {
console.log(`Found ${existingTerms.length} existing terms documents`);
// Có thể bỏ comment để xóa dữ liệu cũ nếu cần
// await Terms.deleteMany({});
// console.log('Cleared existing terms data');
}
// 2. Chuyển đổi từ cấu trúc cũ sang cấu trúc mới nếu cần
let heroData, pageData, contentData;
// Kiểm tra xem data có cấu trúc cũ hay mới
if (termsData.hero && termsData.page && termsData.content) {
// Đây là cấu trúc mới, sử dụng trực tiếp
console.log('Using new structure (hero, page, content)');
heroData = termsData.hero;
pageData = termsData.page;
contentData = termsData.content;
// Debug: kiểm tra content data
console.log('contentData keys:', Object.keys(contentData));
console.log('contentData.content exists?', !!contentData.content);
console.log('contentData.content length:', contentData.content ? contentData.content.length : 0);
if (contentData.content && contentData.content.length > 0) {
console.log('First content item type:', contentData.content[0].type);
}
} else if (termsData.hero && termsData.termsHeader && termsData.sections) {
// Đây là cấu trúc cũ, cần chuyển đổi sang cấu trúc mới
console.log('Converting from old structure to new structure...');
heroData = termsData.hero;
pageData = convertOldPageToNew(termsData);
contentData = convertOldSectionsToNew(termsData);
} else {
throw new Error('Unknown terms data structure');
}
// 3. Tạo document mới cho terms
const termsDocument = new Terms({
version: '2.0.0', // Tăng version vì cấu trúc thay đổi
language: 'en',
// Cấu trúc mới chỉ có 3 phần chính
hero: {
title: heroData.title,
backgroundImage: heroData.backgroundImage,
sectionClass: heroData.sectionClass || 'uk-section-default uk-section-overlap uk-preserve-color uk-light uk-position-relative',
backgroundClasses: heroData.backgroundClasses || 'uk-background-norepeat uk-background-cover uk-background-top-center uk-section uk-section-xlarge',
overlayStyle: heroData.overlayStyle || { backgroundColor: 'rgba(0, 0, 0, 0)' },
titleClass: heroData.titleClass || 'text-white text-[5vw] uk-text-center',
enableScrollspy: heroData.enableScrollspy !== undefined ? heroData.enableScrollspy : true
},
page: {
title: pageData.title,
divider: pageData.divider !== undefined ? pageData.divider : true,
sectionClass: pageData.sectionClass || 'uk-section-default uk-section-overlap uk-section',
titleClass: pageData.titleClass || 'text-[2.5vw] text-[#292c3d] uk-text-left@m uk-text-center',
dividerClass: pageData.dividerClass || 'uk-divider-small uk-text-left@m uk-text-center'
},
content: {
sectionClass: contentData.sectionClass || 'uk-section-muted uk-section-overlap uk-section',
textClass: contentData.textClass || 'uk-panel uk-margin text-[1vw]',
content: contentData.content || []
},
// Metadata
createdAt: new Date(),
updatedAt: new Date(),
isActive: true,
migratedFromOldStructure: !termsData.content // Đánh dấu nếu được chuyển từ cấu trúc cũ
});
// 4. Lưu vào database
await termsDocument.save();
console.log('Terms document saved successfully with new structure');
// 5. Log thông tin
console.log(`Created terms document with ID: ${termsDocument._id}`);
console.log(`Hero title: ${termsDocument.hero.title}`);
console.log(`Page title: ${termsDocument.page.title}`);
console.log(`Content items count: ${termsDocument.content.content.length}`);
// 6. Tạo thêm bản German nếu có
const germanJsonPath = path.join(__dirname, '../data/terms-conditions.de.json');
try {
const germanData = JSON.parse(await fs.readFile(germanJsonPath, 'utf8'));
// Xác định cấu trúc của German data
let germanHero, germanPage, germanContent;
if (germanData.hero && germanData.page && germanData.content) {
germanHero = germanData.hero;
germanPage = germanData.page;
germanContent = germanData.content;
} else if (germanData.hero && germanData.termsHeader && germanData.sections) {
germanHero = germanData.hero;
germanPage = convertOldPageToNew(germanData);
germanContent = convertOldSectionsToNew(germanData);
}
const germanTerms = new Terms({
...termsDocument.toObject(),
_id: new mongoose.Types.ObjectId(), // Tạo ID mới
language: 'de',
hero: {
...termsDocument.hero,
title: germanHero.title || termsDocument.hero.title
},
page: {
...termsDocument.page,
title: germanPage.title || termsDocument.page.title
},
content: {
...termsDocument.content,
content: germanContent.content || termsDocument.content.content
},
isActive: true
});
await germanTerms.save();
console.log('German terms document created successfully');
} catch (error) {
console.log('German version not found or error:', error.message);
console.log('Continuing with English version only...');
}
} catch (error) {
console.error('Error in migrateTermsData:', error);
throw error;
}
}
/**
* Chuyển đổi từ cấu trúc page cũ sang cấu trúc page mới
*/
function convertOldPageToNew(oldData) {
return {
title: oldData.termsHeader?.title || 'Terms & Conditions',
divider: oldData.termsHeader?.divider !== false,
sectionClass: oldData.termsHeader?.sectionClass || 'uk-section-default uk-section-overlap uk-section',
titleClass: oldData.termsHeader?.titleClass || 'text-[2.5vw] text-[#292c3d] uk-text-left@m uk-text-center',
dividerClass: oldData.termsHeader?.dividerClass || 'uk-divider-small uk-text-left@m uk-text-center'
};
}
/**
* Chuyển đổi từ cấu trúc sections cũ sang cấu trúc content mới
*/
function convertOldSectionsToNew(oldData) {
const contentItems = [];
// Thêm disclaimer đầu tiên nếu có
if (oldData.disclaimer?.text) {
contentItems.push({
type: 'paragraph',
text: oldData.disclaimer.text
});
}
// Thêm các sections
if (oldData.sections && Array.isArray(oldData.sections)) {
oldData.sections.forEach(section => {
if (section.title && section.content) {
const contentItem = {
type: 'section',
title: section.title,
content: section.content
};
// Thêm subsections nếu có
if (section.subsections && section.subsections.length > 0) {
contentItem.subsections = section.subsections.map(sub => ({
type: 'note',
text: sub
}));
}
// Thêm cancellation table nếu có
if (section.fees) {
contentItem.subsections = contentItem.subsections || [];
contentItem.subsections.push({
type: 'cancellation_table',
title: 'Standard Cancellation Fees',
items: Object.entries(section.fees).map(([key, value]) => `${key}: ${value}`)
});
}
contentItems.push(contentItem);
}
});
}
// Thêm footer note nếu có
if (oldData.footerNote?.text) {
contentItems.push({
type: 'paragraph',
text: oldData.footerNote.text
});
}
return {
sectionClass: oldData.layout?.termsSectionClass || 'uk-section-muted uk-section-overlap uk-section',
textClass: oldData.layout?.textContentClass || 'uk-panel uk-margin text-[1vw]',
content: contentItems
};
}
/**
* Hàm backup data trước khi migration
*/
async function backupExistingData() {
try {
console.log('Creating backup of existing terms data...');
const existingTerms = await Terms.find({});
if (existingTerms.length > 0) {
const backupPath = path.join(__dirname, '../backups/terms-backup-' + Date.now() + '.json');
// Tạo thư mục backup nếu chưa có
await fs.mkdir(path.dirname(backupPath), { recursive: true });
await fs.writeFile(backupPath, JSON.stringify(existingTerms, null, 2));
console.log(`Backup created at: ${backupPath}`);
}
} catch (error) {
console.error('Backup error:', error);
}
}
// Chạy migration nếu được gọi trực tiếp
if (require.main === module) {
migrate();
}
module.exports = { migrate, migrateTermsData };

View File

@@ -0,0 +1,219 @@
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};

View File

@@ -0,0 +1,192 @@
require('dotenv').config();
const fs = require('fs').promises;
const path = require('path');
const mongoose = require('mongoose');
const Home = require('../models/home'); // Đảm bảo đường dẫn đúng tới file model
// 1. Đọc file JSON
async function loadHomeData() {
// Đảm bảo đường dẫn đúng tới file json
const filePath = path.join(__dirname, '..', 'data', 'home.json');
const raw = await fs.readFile(filePath, 'utf8');
return JSON.parse(raw);
}
// 2. Hàm Transform: Đổ dữ liệu từ JSON (source) vào đúng Schema
function transformHome(source) {
return {
// --- Hero Section ---
hero: {
title: source.hero?.title || "",
description: source.hero?.description || "",
backgroundImage: source.hero?.backgroundImage || "",
button: {
label: source.hero?.button?.label || "Book Now",
href: source.hero?.button?.href || "/booking"
},
contactBox: {
welcomeText: source.hero?.contactBox?.welcomeText || "",
phone: {
label: source.hero?.contactBox?.phone?.label || "Call us",
number: source.hero?.contactBox?.phone?.number || "",
href: source.hero?.contactBox?.phone?.href || ""
},
email: {
label: source.hero?.contactBox?.email?.label || "Email",
address: source.hero?.contactBox?.email?.address || "",
href: source.hero?.contactBox?.email?.href || ""
},
workingHours: {
label: source.hero?.contactBox?.workingHours?.label || "Working Hours",
hours: source.hero?.contactBox?.workingHours?.hours || ""
}
}
},
// --- About Section ---
about: {
title: source.about?.title || "",
subtitle: source.about?.subtitle || "",
description: source.about?.description || "",
images: {
mainImage1: source.about?.images?.mainImage1 || "",
mainImage2: source.about?.images?.mainImage2 || "",
avatars: Array.isArray(source.about?.images?.avatars) ? source.about.images.avatars : []
},
features: Array.isArray(source.about?.features) ? source.about.features : [],
quote: source.about?.quote || "",
button: {
label: source.about?.button?.label || "",
href: source.about?.button?.href || ""
},
stats: {
customerCount: source.about?.stats?.customerCount || 0,
customerLabel: source.about?.stats?.customerLabel || ""
}
},
// --- Mission & Vision ---
missionVision: {
title: source.missionVision?.title || "",
subtitle: source.missionVision?.subtitle || "",
backgroundImage: source.missionVision?.backgroundImage || "",
cards: Array.isArray(source.missionVision?.cards) ? source.missionVision.cards : []
},
// --- Why Choose Us ---
whyChooseUs: {
title: source.whyChooseUs?.title || "",
subtitle: source.whyChooseUs?.subtitle || "",
description: source.whyChooseUs?.description || "",
button: source.whyChooseUs?.button || {},
features: Array.isArray(source.whyChooseUs?.features) ? source.whyChooseUs.features : [],
tags: Array.isArray(source.whyChooseUs?.tags) ? source.whyChooseUs.tags : [],
cta: source.whyChooseUs?.cta || {}
},
// --- Activities ---
activities: {
cards: Array.isArray(source.activities?.cards) ? source.activities.cards : []
},
// --- FAQ ---
faq: {
title: source.faq?.title || "",
subtitle: source.faq?.subtitle || "",
description: source.faq?.description || "",
image: source.faq?.image || "",
contact: source.faq?.contact || {},
questions: Array.isArray(source.faq?.questions) ? source.faq.questions : []
},
// --- Partners ---
partners: {
title: source.partners?.title || "",
subtitle: source.partners?.subtitle || "",
backgroundImage: source.partners?.backgroundImage || "",
logos: Array.isArray(source.partners?.logos) ? source.partners.logos : [],
cta: source.partners?.cta || {}
},
// --- Programs ---
programs: {
title: source.programs?.title || "",
subtitle: source.programs?.subtitle || "",
button: source.programs?.button || {},
card: {
pricePrefix: source.programs?.card?.pricePrefix || "from",
priceSuffix: source.programs?.card?.priceSuffix || "USD",
buttonLabel: source.programs?.card?.buttonLabel || "Camp Detail",
buttonHref: source.programs?.card?.buttonHref || "/camp-profiles"
},
items: Array.isArray(source.programs?.items) ? source.programs.items : []
},
// --- Newsletter ---
newsletter: {
title: source.newsletter?.title || "",
subtitle: source.newsletter?.subtitle || "",
description: source.newsletter?.description || "",
image: source.newsletter?.image || "",
decorativeImage: source.newsletter?.decorativeImage || "",
button: {
label: source.newsletter?.button?.label || "",
placeholder: source.newsletter?.button?.placeholder || "",
href: source.newsletter?.button?.href || ""
}
},
// --- Latest Posts ---
latestPosts: {
title: source.latestPosts?.title || "",
subtitle: source.latestPosts?.subtitle || "",
searchPlaceholder: source.latestPosts?.searchPlaceholder || "",
sidebarTitle: source.latestPosts?.sidebarTitle || "",
blogPosts: Array.isArray(source.latestPosts?.blogPosts) ? source.latestPosts.blogPosts : [],
sidebarPosts: Array.isArray(source.latestPosts?.sidebarPosts) ? source.latestPosts.sidebarPosts : [],
featuredCard: source.latestPosts?.featuredCard || {}
},
updatedAt: new Date()
};
}
// 3. Chạy Migration
async function migrate() {
try {
// Kết nối DB
await mongoose.connect(process.env.MONGODB_URI);
console.log('✅ Connected to MongoDB');
// A. Lấy dữ liệu thô
const rawData = await loadHomeData();
console.log('📖 Data loaded from JSON');
// B. Chuẩn hóa dữ liệu theo Schema
const homeData = transformHome(rawData);
// C. Lưu vào DB (Upsert: Có rồi thì update, chưa có thì tạo)
const existingDoc = await Home.findOne().sort({ updatedAt: -1 });
if (existingDoc) {
console.log('📝 Updating existing Home document...');
await Home.findByIdAndUpdate(existingDoc._id, { $set: homeData }, { new: true });
} else {
console.log('📝 Creating NEW Home document...');
await Home.create(homeData);
}
console.log('✨ Migration completed successfully!');
} catch (error) {
console.error('❌ Migration failed:', error);
process.exit(1);
} finally {
await mongoose.connection.close();
process.exit(0);
}
}
migrate();

View File

@@ -0,0 +1,178 @@
const mongoose = require('mongoose');
const Booking = require('../models/booking');
const fs = require('fs');
const path = require('path');
require('dotenv').config();
const filePath = path.join(__dirname, '..', 'data', 'booking.json');
let raw = '{}';
try {
raw = fs.readFileSync(filePath, 'utf8');
} catch (e) {
console.error('Could not read booking.json at', filePath);
process.exit(2);
}
let data;
try {
data = JSON.parse(raw || '{}');
} catch (e) {
console.error('Invalid JSON in booking.json:', e.message);
process.exit(3);
}
// Remove _id fields recursively to avoid conflicts
function stripIds(obj) {
if (Array.isArray(obj)) return obj.map(i => stripIds(i));
if (obj && typeof obj === 'object') {
const out = {};
for (const k in obj) {
if (k !== '_id') out[k] = stripIds(obj[k]);
}
return out;
}
return obj;
}
data = stripIds(data);
// Normalize vouchers to an array of objects so Mongoose casting won't fail
function normalizeVouchers(doc) {
if (!doc) return;
// support root-level `vouchers` and `configuration.vouchers`
let v = doc.vouchers || (doc.configuration && doc.configuration.vouchers);
if (!v) return;
// Try to parse stringified arrays (may use single quotes or JS literal)
if (typeof v === 'string') {
try {
v = JSON.parse(v);
} catch (e1) {
try {
// try parsing JS object-literal style strings
// eslint-disable-next-line no-new-func
v = (new Function('return ' + v))();
} catch (e2) {
// fallback: attempt to extract codes by splitting on commas
v = v.split && v.split(',').map(s => s.trim()).filter(Boolean).map(s => ({ validCodes: s, type: 'unknown', value: null }));
}
}
}
// Normalize array items into objects
if (Array.isArray(v)) {
v = v.map(item => {
if (typeof item === 'string') return { validCodes: item, type: 'unknown', value: null };
if (item && typeof item === 'object') {
return {
validCodes: item.validCodes || item.code || '',
type: item.type || '',
value: typeof item.value === 'number' ? item.value : (item.amount && Number(item.amount)) || null,
};
}
return item;
});
}
// If Booking schema expects array of strings, convert objects -> string codes
try {
const Booking = require('../models/booking');
const pathType = Booking.schema.path('vouchers') || Booking.schema.path('configuration.vouchers');
if (pathType && pathType.instance === 'Array' && pathType.caster && pathType.caster.instance === 'String') {
const mapped = (v || []).map(it => (typeof it === 'string' ? it : (it && typeof it === 'object' ? it.validCodes || JSON.stringify(it) : String(it))));
if (doc.vouchers) doc.vouchers = mapped;
else if (doc.configuration) doc.configuration.vouchers = mapped;
return;
}
} catch (e) {
// ignore and keep object form
}
if (doc.vouchers) doc.vouchers = v;
else if (doc.configuration) doc.configuration.vouchers = v;
}
normalizeVouchers(data);
// Also normalize discounts (some inputs have them stringified or as objects)
function normalizeDiscounts(doc) {
if (!doc) return;
let d = doc.discounts || (doc.configuration && doc.configuration.discounts);
if (!d) return;
if (typeof d === 'string') {
try {
d = JSON.parse(d);
} catch (e1) {
try {
// eslint-disable-next-line no-new-func
d = (new Function('return ' + d))();
} catch (e2) {
d = d.split && d.split('\n').map(s => s.trim()).filter(Boolean).map(s => ({ id: '', name: s }));
}
}
}
if (Array.isArray(d)) {
d = d.map(item => {
if (typeof item === 'string') return { id: '', name: item };
if (item && typeof item === 'object') return item;
return item;
});
}
try {
const Booking = require('../models/booking');
const pathType = Booking.schema.path('discounts') || Booking.schema.path('configuration.discounts');
if (pathType && pathType.instance === 'Array' && pathType.caster && pathType.caster.instance === 'String') {
const mapped = (d || []).map(it => (typeof it === 'string' ? it : (it.id || it.name || JSON.stringify(it))));
if (doc.discounts) doc.discounts = mapped;
else if (doc.configuration) doc.configuration.discounts = mapped;
return;
}
} catch (e) {
// ignore
}
if (doc.discounts) doc.discounts = d;
else if (doc.configuration) doc.configuration.discounts = d;
}
normalizeDiscounts(data);
const dryRun = process.argv.includes('--dry-run') || process.argv.includes('-n');
async function run() {
try {
const dbUri = process.env.MONGODB_URI || process.env.MONGO_URL || 'mongodb://127.0.0.1:27017/cms';
console.log('Using DB URI:', dbUri);
if (dryRun) {
console.log('\nDRY RUN - preview of document to upsert:\n');
console.log(JSON.stringify(data, null, 2));
process.exit(0);
}
await mongoose.connect(dbUri, { useNewUrlParser: true, useUnifiedTopology: true });
console.log('Connected to MongoDB');
const result = await Booking.findOneAndUpdate({}, data, {
upsert: true,
new: true,
setDefaultsOnInsert: true,
});
console.log('Upsert complete. Document id:', result._id.toString());
console.log('Summary: programs=', (data.programs || []).length, 'camps=', (data.camps || []).length);
await mongoose.disconnect();
process.exit(0);
} catch (err) {
console.error('Migration failed:', err && err.message ? err.message : err);
process.exit(1);
}
}
run();

View File

@@ -0,0 +1,48 @@
require('dotenv').config();
const connectDB = require('../config/database');
/**
* Migration: migrate_header_ggcamp
* Created: 21:40:26 11/12/2025
*/
async function migrate() {
try {
// Kết nối database
await connectDB();
console.log('Starting migration: migrate_header_ggcamp...');
const mongoose = require('mongoose');
const fs = require('fs').promises;
const path = require('path');
const Header = require('../models/header');
// Đọc dữ liệu từ header.json
const headerDataPath = path.join(__dirname, '../data/header.json');
const headerData = JSON.parse(await fs.readFile(headerDataPath, 'utf8'));
// Xóa tất cả documents header cũ
await Header.deleteMany({});
// Tạo header mới với dữ liệu từ JSON (topbar và logo)
await Header.create({
name: 'default',
topbar: headerData.topbar,
logo: headerData.logo
});
console.log('✅ Header migration completed successfully!');
await mongoose.disconnect();
process.exit(0);
} catch (error) {
console.error('❌ Migration error:', error);
process.exit(1);
}
}
// Chạy migration nếu được gọi trực tiếp
if (require.main === module) {
migrate();
}
module.exports = { migrate };

View File

@@ -0,0 +1,57 @@
require('dotenv').config();
const connectDB = require('../config/database');
/**
* Migration: migrate_menu_header_ggcamp
* Created: 21:40:38 11/12/2025
*/
async function migrate() {
try {
// Kết nối database
await connectDB();
console.log('Starting migration: migrate_menu_header_ggcamp...');
const mongoose = require('mongoose');
const fs = require('fs').promises;
const path = require('path');
const Menu = require('../models/menuHeader');
// Xóa tất cả dữ liệu menu cũ
await Menu.deleteMany({});
// Đọc JSON file
const jsonPath = path.join(__dirname, '../data/menu-header.json');
const jsonData = JSON.parse(await fs.readFile(jsonPath, 'utf8'));
// Tạo menu items (đơn giản, không có fetch/programmes)
for (const menuData of jsonData.menus) {
// Chỉ giữ lại các field cần thiết: menuid, parent, title, url, order, type
const menuItem = {
menuid: menuData.menuid,
parent: menuData.parent || null,
title: menuData.title,
url: menuData.url,
order: menuData.order || 0,
type: menuData.type || 'static',
fetch: false, // Không dùng fetch
isActive: true // Mặc định active
};
await Menu.create(menuItem);
}
console.log('✅ Menu header migration completed successfully!');
await mongoose.disconnect();
process.exit(0);
} catch (error) {
console.error('❌ Migration error:', error);
process.exit(1);
}
}
// Chạy migration nếu được gọi trực tiếp
if (require.main === module) {
migrate();
}
module.exports = { migrate };

View File

@@ -0,0 +1,41 @@
require('dotenv').config();
const connectDB = require('../config/database');
/**
* Migration: migrate_footer_ggcamp
* Created: 21:41:03 11/12/2025
*/
async function migrate() {
try {
// Kết nối database
await connectDB();
console.log('Starting migration: migrate_footer_ggcamp...');
const mongoose = require('mongoose');
const fs = require('fs').promises;
const path = require('path');
const Footer = require('../models/footer');
// Read footer.json file
const footerJsonPath = path.join(__dirname, '../data/footer.json');
const footerData = JSON.parse(await fs.readFile(footerJsonPath, 'utf8'));
// Migrate data using the model's static method
await Footer.migrateFromJson(footerData);
console.log('✅ Footer migration completed successfully!');
await mongoose.disconnect();
process.exit(0);
} catch (error) {
console.error('❌ Migration error:', error);
process.exit(1);
}
}
// Chạy migration nếu được gọi trực tiếp
if (require.main === module) {
migrate();
}
module.exports = { migrate };

View File

@@ -0,0 +1,273 @@
require('dotenv').config();
const fs = require('fs').promises;
const path = require('path');
const connectDB = require('../config/database');
const Travel = require('../models/travel');
const mongoose = require('mongoose');
/**
* Migration: travel
* Migrate Travel data từ travel.json
* Created: 2025-12-13
*/
async function migrate() {
try {
// Kết nối database
await connectDB();
console.log('Starting migration: travel...');
// Đọc file travel.json
const travelJsonPath = path.join(__dirname, '../data/travel.json');
console.log('Reading JSON file from:', travelJsonPath);
const travelData = JSON.parse(await fs.readFile(travelJsonPath, 'utf8'));
console.log('Travel data loaded successfully');
console.log('Data structure keys:', Object.keys(travelData));
// Thực hiện migration
await migrateTravelData(travelData);
console.log('Travel migration completed successfully!');
await mongoose.disconnect();
process.exit(0);
} catch (error) {
console.error('Migration error:', error);
process.exit(1);
}
}
/**
* Custom migration logic cho travel data
*/
async function migrateTravelData(travelData) {
try {
console.log('Starting custom migration logic...');
// 1. Kiểm tra dữ liệu cũ
const existingTravel = await Travel.findOne({});
if (existingTravel) {
console.log(`Found existing travel document: ${existingTravel._id}`);
console.log('Deleting existing travel data...');
await Travel.deleteMany({});
console.log('Cleared existing travel data');
}
// 2. Chuyển đổi locations thành blog content blocks
const contentBlocks = [];
// If travelData already has posts (blog format), use the first post
if (Array.isArray(travelData.posts) && travelData.posts.length > 0) {
const firstPost = travelData.posts[0];
if (firstPost.content && Array.isArray(firstPost.content.blocks)) {
contentBlocks.push(...firstPost.content.blocks);
}
} else {
// Thêm general description (legacy format)
if (travelData.general) {
contentBlocks.push({
type: 'paragraph',
data: {
text: travelData.general.description
}
});
// Thêm additional info như conclusion
if (travelData.general.additionalInfo) {
contentBlocks.push({
type: 'conclusion',
data: {
text: travelData.general.additionalInfo,
callToAction: {
text: '',
link: ''
}
}
});
}
}
// Chuyển đổi từng location thành blog blocks
if (travelData.locations && Array.isArray(travelData.locations)) {
travelData.locations.forEach(location => {
// Header cho location
contentBlocks.push({
type: 'header',
data: {
text: location.title,
level: 2
}
});
// Address information
const addressItems = [];
if (location.address) {
if (location.address.name) {
addressItems.push(`Name: ${location.address.name}`);
}
if (location.address.line2) {
addressItems.push(location.address.line2);
}
if (location.address.street) {
addressItems.push(`Street: ${location.address.street}`);
}
if (location.address.postalCode && location.address.city) {
const country = location.address.country ? `, ${location.address.country}` : '';
addressItems.push(`Location: ${location.address.postalCode} ${location.address.city}${country}`);
}
if (location.address.googleMapsUrl) {
addressItems.push(`Google Maps: <a href="${location.address.googleMapsUrl}" target="_blank">View on Map</a>`);
}
}
if (addressItems.length > 0) {
contentBlocks.push({
type: 'list',
data: {
style: 'unordered',
items: addressItems
}
});
}
// Address note as conclusion
if (location.address?.note) {
contentBlocks.push({
type: 'conclusion',
data: {
text: location.address.note,
callToAction: {
text: '',
link: ''
}
}
});
}
// Contact information
const contactItems = [];
if (location.contact) {
if (location.contact.email) {
contactItems.push(`Email: ${location.contact.email}`);
}
if (location.contact.phone) {
contactItems.push(`Phone: ${location.contact.phone}`);
}
}
if (contactItems.length > 0) {
contentBlocks.push({
type: 'paragraph',
data: {
text: 'Contact Information:'
}
});
contentBlocks.push({
type: 'list',
data: {
style: 'unordered',
items: contactItems
}
});
}
// Schedule information
const scheduleItems = [];
if (location.schedule) {
if (location.schedule.arrival) {
scheduleItems.push(`Arrival: ${location.schedule.arrival}`);
}
if (location.schedule.departure) {
scheduleItems.push(`Departure: ${location.schedule.departure}`);
}
}
if (scheduleItems.length > 0) {
contentBlocks.push({
type: 'paragraph',
data: {
text: 'Schedule:'
}
});
contentBlocks.push({
type: 'list',
data: {
style: 'unordered',
items: scheduleItems
}
});
}
});
}
}
// 3. Tạo document mới cho travel
const travelDocument = new Travel({
hero: {
title: travelData.hero?.title || 'Travel Information',
backgroundImage: travelData.hero?.backgroundImage || ''
},
page: {
title: travelData.page?.title || travelData.general?.title || 'Go and Grow Camp Travel Information',
description: travelData.page?.description || travelData.general?.description || '',
year: travelData.page?.year || travelData.pageYear || undefined,
metadata: {
title: 'Travel Guide - Go and Grow Camp',
description: 'Everything you need to know about traveling to our camps'
}
},
content: {
blocks: contentBlocks
},
enableScrollspy: true,
lastUpdated: new Date()
});
// 4. Lưu vào database
await travelDocument.save();
console.log('Travel document saved successfully');
// 5. Log thông tin
console.log(`Created travel document with ID: ${travelDocument._id}`);
console.log(`Hero title: ${travelDocument.hero.title}`);
console.log(`Page title: ${travelDocument.page.title}`);
console.log(`Content blocks count: ${travelDocument.content.blocks.length}`);
console.log(`Converted ${travelData.locations?.length || 0} locations to blog blocks`);
} catch (error) {
console.error('Error in migrateTravelData:', error);
throw error;
}
}
/**
* Hàm backup data trước khi migration
*/
async function backupExistingData() {
try {
console.log('Creating backup of existing travel data...');
const existingTravel = await Travel.find({});
if (existingTravel.length > 0) {
const backupPath = path.join(__dirname, '../backups/travel-backup-' + Date.now() + '.json');
// Tạo thư mục backup nếu chưa có
await fs.mkdir(path.dirname(backupPath), { recursive: true });
await fs.writeFile(backupPath, JSON.stringify(existingTravel, null, 2));
console.log(`Backup created at: ${backupPath}`);
}
} catch (error) {
console.error('Backup error:', error);
}
}
// Chạy migration nếu được gọi trực tiếp
if (require.main === module) {
migrate();
}
module.exports = { migrate, migrateTravelData };

86
scripts/make-migration.js Normal file
View File

@@ -0,0 +1,86 @@
const fs = require('fs');
const path = require('path');
/**
* Tạo migration file mới với format giống Laravel
* Format: YYYY_MM_DD_HHMMSS_migration_name.js
*/
function makeMigration(migrationName) {
if (!migrationName) {
console.error('Error: Migration name is required');
console.log('\nUsage: node scripts/make-migration.js <migration-name>');
console.log('Example: node scripts/make-migration.js create_users_table');
process.exit(1);
}
// Tạo timestamp theo format Laravel: YYYY_MM_DD_HHMMSS
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
const timestamp = `${year}_${month}_${day}_${hours}${minutes}${seconds}`;
const fileName = `${timestamp}_${migrationName}.js`;
const filePath = path.join(__dirname, fileName);
// Template migration mẫu
const template = `require('dotenv').config();
const connectDB = require('../config/database');
/**
* Migration: ${migrationName}
* Created: ${now.toLocaleString('vi-VN')}
*/
async function migrate() {
try {
// Kết nối database
await connectDB();
console.log('Starting migration: ${migrationName}...');
// TODO: Thêm code migration của bạn ở đây
console.log('Migration ${migrationName} completed successfully!');
const mongoose = require('mongoose');
await mongoose.disconnect();
process.exit(0);
} catch (error) {
console.error('Migration error:', error);
process.exit(1);
}
}
// Chạy migration nếu được gọi trực tiếp
if (require.main === module) {
migrate();
}
module.exports = { migrate };
`;
// Kiểm tra file đã tồn tại chưa
if (fs.existsSync(filePath)) {
console.error(`Error: Migration file already exists: ${fileName}`);
process.exit(1);
}
// Tạo file migration
try {
fs.writeFileSync(filePath, template, 'utf8');
console.log(`Migration created successfully: ${fileName}`);
console.log(`Path: ${filePath}`);
} catch (error) {
console.error('Error creating migration file:', error.message);
process.exit(1);
}
}
// Lấy migration name từ command line arguments
const migrationName = process.argv[2];
// Chạy hàm tạo migration
makeMigration(migrationName);

204
scripts/migrate-all.js Normal file
View File

@@ -0,0 +1,204 @@
require("dotenv").config();
const path = require("path");
const fs = require("fs");
const {execSync} = require("child_process");
const connectDB = require("../config/database");
const migrationHelper = require("../utils/migrationHelper");
/**
* Tự động phát hiện tất cả các file script trong thư mục scripts
* Loại trừ các file quản lý migration và file không phải .js
*/
function discoverMigrations() {
const scriptsDir = __dirname;
const files = fs.readdirSync(scriptsDir);
// Danh sách các file quản lý migration cần loại trừ
const excludeFiles = [
"migrate-all.js",
"migrate-status.js",
"migrate-rollback.js",
"migrate-fresh.js",
"make-migration.js",
];
const migrations = files
.filter((file) => {
// Lấy tất cả file .js, trừ các file quản lý
return file.endsWith(".js") && !excludeFiles.includes(file);
})
.map((file) => {
// Tạo tên migration từ tên file (bỏ .js)
const name = file.replace(".js", "");
return {
name: name,
script: file,
};
})
.sort((a, b) => {
// Sắp xếp theo tên để đảm bảo thứ tự nhất quán
return a.name.localeCompare(b.name);
});
return migrations;
}
// Tự động phát hiện migrations
const migrations = discoverMigrations();
/**
* Chạy migration script (suppress output)
* Sử dụng child_process để chạy script độc lập vì các script tự quản lý DB connection
* Output từ script sẽ bị suppress để chỉ hiển thị status
*/
async function runMigrationScript(migration) {
return new Promise((resolve, reject) => {
try {
// Chạy script bằng child_process với stdio: 'pipe' để suppress output
// Nhưng vẫn capture stderr để có thể hiển thị lỗi nếu cần
const result = execSync(`node scripts/${migration.script}`, {
stdio: ["ignore", "pipe", "pipe"], // stdin: ignore, stdout: pipe, stderr: pipe
cwd: path.join(__dirname, ".."),
encoding: "utf8",
});
resolve();
} catch (error) {
// Attach stderr vào error để có thể hiển thị sau
if (error.stderr) {
error.stderr = error.stderr;
}
reject(error);
}
});
}
/**
* Hàm chính để chạy tất cả migrations với tracking
*/
async function runAllMigrations() {
const mongoose = require("mongoose");
let ownConn = false;
try {
const wasConnected = mongoose.connection.readyState === 1;
await connectDB();
if (!wasConnected) ownConn = true;
const batch = (await migrationHelper.getLastBatch()) + 1;
const results = [];
// Kiểm tra và chạy từng migration
for (let i = 0; i < migrations.length; i++) {
const migration = migrations[i];
try {
// Kiểm tra xem migration đã chạy chưa
const hasRun = await migrationHelper.hasRun(migration.name);
if (hasRun) {
results.push({name: migration.name, status: "SKIPPED"});
continue;
}
// Chạy migration script (output bị suppress)
await runMigrationScript(migration);
if (mongoose.connection.readyState !== 1) {
await connectDB();
}
await migrationHelper.markAsRun(migration.name, batch);
results.push({name: migration.name, status: "DONE"});
} catch (error) {
results.push({
name: migration.name,
status: "FAIL",
error: error.message,
});
// Hiển thị bảng kết quả trước khi exit
displayResults(results);
console.error(
`\n❌ Migration "${migration.name}" failed: ${error.message}`
);
if (error.stderr) {
console.error(error.stderr.toString());
}
if (ownConn && mongoose.connection.readyState === 1) {
await mongoose.disconnect();
}
process.exit(1);
}
}
// Hiển thị bảng kết quả
displayResults(results);
if (ownConn && mongoose.connection.readyState === 1) {
await mongoose.disconnect();
}
process.exit(0);
} catch (error) {
console.error("\n❌ Error:", error.message);
if (ownConn && mongoose.connection.readyState === 1) {
await mongoose.disconnect();
}
process.exit(1);
}
}
/**
* Hiển thị bảng kết quả migration đơn giản
*/
function displayResults(results) {
console.log("\nRunning migrations...\n");
// Tìm độ dài tên migration dài nhất để format bảng
const maxNameLength = Math.max(...results.map((r) => r.name.length), 20);
const statusWidth = 10;
const totalWidth = maxNameLength + statusWidth + 7; // 7 = spaces and separators
// Header
console.log("=".repeat(totalWidth));
console.log(
`${"Migration".padEnd(maxNameLength)} | ${"Status".padEnd(statusWidth)}`
);
console.log("=".repeat(totalWidth));
// Rows
results.forEach((result) => {
let statusText = "";
if (result.status === "DONE") {
statusText = "DONE".padEnd(statusWidth);
} else if (result.status === "SKIPPED") {
statusText = "SKIPPED".padEnd(statusWidth);
} else if (result.status === "FAIL") {
statusText = "FAIL".padEnd(statusWidth);
}
console.log(`${result.name.padEnd(maxNameLength)} | ${statusText}`);
});
// Footer
console.log("=".repeat(totalWidth));
// Summary
const doneCount = results.filter((r) => r.status === "DONE").length;
const skippedCount = results.filter((r) => r.status === "SKIPPED").length;
const failCount = results.filter((r) => r.status === "FAIL").length;
console.log("");
if (doneCount > 0) {
console.log(`${doneCount} migration(s) completed`);
}
if (skippedCount > 0) {
console.log(`${skippedCount} migration(s) skipped`);
}
if (failCount > 0) {
console.log(`${failCount} migration(s) failed`);
}
console.log("");
}
// Chạy hàm chính
runAllMigrations();

189
scripts/migrate-fresh.js Normal file
View File

@@ -0,0 +1,189 @@
require('dotenv').config();
const path = require("path");
const fs = require("fs");
const { execSync } = require("child_process");
const connectDB = require("../config/database");
const migrationHelper = require("../utils/migrationHelper");
/**
* Tự động phát hiện tất cả các file script trong thư mục scripts
* Loại trừ các file quản lý migration và file không phải .js
*/
function discoverMigrations() {
const scriptsDir = __dirname;
const files = fs.readdirSync(scriptsDir);
// Danh sách các file quản lý migration cần loại trừ
const excludeFiles = [
'migrate-all.js',
'migrate-status.js',
'migrate-rollback.js',
'migrate-fresh.js',
'make-migration.js',
'MIGRATION_README.md'
];
const migrations = files
.filter(file => {
// Lấy tất cả file .js, trừ các file quản lý
return file.endsWith('.js') && !excludeFiles.includes(file);
})
.map(file => {
// Tạo tên migration từ tên file (bỏ .js)
const name = file.replace('.js', '');
return {
name: name,
script: file
};
})
.sort((a, b) => {
// Sắp xếp theo tên để đảm bảo thứ tự nhất quán
return a.name.localeCompare(b.name);
});
return migrations;
}
/**
* Chạy migration script (suppress output)
* Sử dụng child_process để chạy script độc lập vì các script tự quản lý DB connection
* Output từ script sẽ bị suppress để chỉ hiển thị status
*/
async function runMigrationScript(migration) {
return new Promise((resolve, reject) => {
try {
// Chạy script bằng child_process với stdio: 'pipe' để suppress output
// Nhưng vẫn capture stderr để có thể hiển thị lỗi nếu cần
const result = execSync(`node scripts/${migration.script}`, {
stdio: ['ignore', 'pipe', 'pipe'], // stdin: ignore, stdout: pipe, stderr: pipe
cwd: path.join(__dirname, ".."),
encoding: 'utf8'
});
resolve();
} catch (error) {
// Attach stderr vào error để có thể hiển thị sau
if (error.stderr) {
error.stderr = error.stderr;
}
reject(error);
}
});
}
/**
* Hiển thị bảng kết quả migration đơn giản
*/
function displayResults(results) {
console.log("\nRunning migrations...\n");
// Tìm độ dài tên migration dài nhất để format bảng
const maxNameLength = Math.max(...results.map(r => r.name.length), 20);
const statusWidth = 10;
const totalWidth = maxNameLength + statusWidth + 7; // 7 = spaces and separators
// Header
console.log("=".repeat(totalWidth));
console.log(`${'Migration'.padEnd(maxNameLength)} | ${'Status'.padEnd(statusWidth)}`);
console.log("=".repeat(totalWidth));
// Rows
results.forEach(result => {
let statusText = "";
if (result.status === 'DONE') {
statusText = "DONE".padEnd(statusWidth);
} else if (result.status === 'FAIL') {
statusText = "FAIL".padEnd(statusWidth);
}
console.log(`${result.name.padEnd(maxNameLength)} | ${statusText}`);
});
// Footer
console.log("=".repeat(totalWidth));
// Summary
const doneCount = results.filter(r => r.status === 'DONE').length;
const failCount = results.filter(r => r.status === 'FAIL').length;
console.log("");
if (doneCount > 0) {
console.log(`${doneCount} migration(s) completed`);
}
if (failCount > 0) {
console.log(`${failCount} migration(s) failed`);
}
console.log("");
}
/**
* Hàm chính để chạy lại tất cả migrations từ đầu (fresh)
* Xóa tất cả tracking và chạy lại từ đầu
*/
async function runFreshMigrations() {
const mongoose = require('mongoose');
let ownConn = false;
try {
const wasConnected = mongoose.connection.readyState === 1;
await connectDB();
if (!wasConnected) ownConn = true;
// Tự động phát hiện migrations
const migrations = discoverMigrations();
// Xóa tất cả tracking migrations
const Migration = require('../models/migration');
await Migration.deleteMany({});
const batch = 1; // Batch mới bắt đầu từ 1
const results = [];
// Chạy từng migration
for (let i = 0; i < migrations.length; i++) {
const migration = migrations[i];
try {
// Chạy migration script (output bị suppress)
await runMigrationScript(migration);
if (mongoose.connection.readyState !== 1) {
await connectDB();
}
await migrationHelper.markAsRun(migration.name, batch);
results.push({ name: migration.name, status: 'DONE' });
} catch (error) {
results.push({ name: migration.name, status: 'FAIL', error: error.message });
// Hiển thị bảng kết quả trước khi exit
displayResults(results);
console.error(`\n❌ Migration "${migration.name}" failed: ${error.message}`);
if (error.stderr) {
console.error(error.stderr.toString());
}
if (ownConn && mongoose.connection.readyState === 1) {
await mongoose.disconnect();
}
process.exit(1);
}
}
// Hiển thị bảng kết quả
displayResults(results);
if (ownConn && mongoose.connection.readyState === 1) {
await mongoose.disconnect();
}
process.exit(0);
} catch (error) {
console.error("\n❌ Error:", error.message);
if (ownConn && mongoose.connection.readyState === 1) {
await mongoose.disconnect();
}
process.exit(1);
}
}
// Chạy hàm chính
runFreshMigrations();

111
scripts/migrate-rollback.js Normal file
View File

@@ -0,0 +1,111 @@
require('dotenv').config();
const connectDB = require('../config/database');
const migrationHelper = require('../utils/migrationHelper');
/**
* Rollback một migration cụ thể
*/
async function rollbackMigration(migrationName) {
const mongoose = require('mongoose');
let ownConn = false;
try {
const wasConnected = mongoose.connection.readyState === 1;
await connectDB();
if (!wasConnected) ownConn = true;
const hasRun = await migrationHelper.hasRun(migrationName);
if (!hasRun) {
console.log(`⚠️ Migration "${migrationName}" chưa được chạy, không thể rollback.`);
if (ownConn && mongoose.connection.readyState === 1) {
await mongoose.disconnect();
}
process.exit(0);
}
const result = await migrationHelper.rollback(migrationName);
if (result) {
console.log(`✅ Đã rollback migration: ${migrationName}`);
} else {
console.log(`❌ Không thể rollback migration: ${migrationName}`);
}
if (ownConn && mongoose.connection.readyState === 1) {
await mongoose.disconnect();
}
} catch (error) {
console.error('❌ Lỗi:', error.message);
if (ownConn && mongoose.connection.readyState === 1) {
await mongoose.disconnect();
}
process.exit(1);
}
}
/**
* Rollback batch cuối cùng
*/
async function rollbackLastBatch() {
const mongoose = require('mongoose');
let ownConn = false;
try {
const wasConnected = mongoose.connection.readyState === 1;
await connectDB();
if (!wasConnected) ownConn = true;
const lastBatch = await migrationHelper.getLastBatch();
if (lastBatch === 0) {
console.log('⚠️ Không có batch nào để rollback.');
if (ownConn && mongoose.connection.readyState === 1) {
await mongoose.disconnect();
}
process.exit(0);
}
const migrations = await migrationHelper.getMigrationsByBatch(lastBatch);
if (migrations.length === 0) {
console.log(`⚠️ Batch ${lastBatch} không có migration nào.`);
if (ownConn && mongoose.connection.readyState === 1) {
await mongoose.disconnect();
}
process.exit(0);
}
console.log(`\n🔄 Đang rollback batch ${lastBatch}...`);
console.log(`📋 Các migration sẽ được rollback:`);
migrations.forEach(m => {
console.log(` - ${m.name}`);
});
const deletedCount = await migrationHelper.rollbackBatch(lastBatch);
console.log(`\n✅ Đã rollback ${deletedCount} migration(s) trong batch ${lastBatch}`);
if (ownConn && mongoose.connection.readyState === 1) {
await mongoose.disconnect();
}
} catch (error) {
console.error('❌ Lỗi:', error.message);
if (ownConn && mongoose.connection.readyState === 1) {
await mongoose.disconnect();
}
process.exit(1);
}
}
// Xử lý command line arguments
const args = process.argv.slice(2);
if (args.length === 0) {
console.log('Usage:');
console.log(' node scripts/migrate-rollback.js <migration-name> - Rollback một migration cụ thể');
console.log(' node scripts/migrate-rollback.js --batch - Rollback batch cuối cùng');
process.exit(0);
}
if (args[0] === '--batch') {
rollbackLastBatch();
} else {
rollbackMigration(args[0]);
}

120
scripts/migrate-status.js Normal file
View File

@@ -0,0 +1,120 @@
require('dotenv').config();
const connectDB = require('../config/database');
const migrationHelper = require('../utils/migrationHelper');
const path = require('path');
const fs = require('fs');
/**
* Tự động phát hiện tất cả các file script trong thư mục scripts
* Loại trừ các file quản lý migration và file không phải .js
*/
function discoverMigrations() {
const scriptsDir = __dirname;
const files = fs.readdirSync(scriptsDir);
// Danh sách các file quản lý migration cần loại trừ
const excludeFiles = [
'migrate-all.js',
'migrate-status.js',
'migrate-rollback.js',
'migrate-fresh.js',
'make-migration.js',
'MIGRATION_README.md'
];
const migrations = files
.filter(file => {
// Lấy tất cả file .js, trừ các file quản lý
return file.endsWith('.js') && !excludeFiles.includes(file);
})
.map(file => file.replace('.js', ''))
.sort();
return migrations;
}
// Tự động phát hiện migrations
const availableMigrations = discoverMigrations();
/**
* Hiển thị trạng thái của tất cả migrations
*/
async function showStatus() {
const mongoose = require('mongoose');
let ownConn = false;
try {
const wasConnected = mongoose.connection.readyState === 1;
await connectDB();
if (!wasConnected) ownConn = true;
console.log('\nMigration Status:\n');
const ranMigrations = await migrationHelper.getRanMigrations();
const ranMap = new Map();
ranMigrations.forEach(m => ranMap.set(m.name, m));
// Tính toán độ rộng cột
const maxNameLength = Math.max(...availableMigrations.map(name => name.length), 20);
const statusWidth = 10;
const batchWidth = 6;
const ranAtWidth = 20;
const totalWidth = maxNameLength + statusWidth + batchWidth + ranAtWidth + 11; // 11 = spaces and separators
// Header
console.log('='.repeat(totalWidth));
console.log(
`${'Migration Name'.padEnd(maxNameLength)} | ${'Status'.padEnd(statusWidth)} | ${'Batch'.padEnd(batchWidth)} | Ran At`
);
console.log('='.repeat(totalWidth));
let pendingCount = 0;
let ranCount = 0;
for (const migrationName of availableMigrations) {
const migration = ranMap.get(migrationName);
if (migration) {
const ranAt = new Date(migration.ranAt).toLocaleString('vi-VN');
console.log(
`${migrationName.padEnd(maxNameLength)} | ${'Ran'.padEnd(statusWidth)} | ${String(migration.batch).padEnd(batchWidth)} | ${ranAt}`
);
ranCount++;
} else {
console.log(
`${migrationName.padEnd(maxNameLength)} | ${'Pending'.padEnd(statusWidth)} | ${'-'.padEnd(batchWidth)} | -`
);
pendingCount++;
}
}
console.log('='.repeat(totalWidth));
console.log(`\nSummary:`);
console.log(` Ran: ${ranCount} migration(s)`);
console.log(` Pending: ${pendingCount} migration(s)`);
const lastBatch = await migrationHelper.getLastBatch();
if (lastBatch > 0) {
console.log(` Last batch: ${lastBatch}`);
}
console.log('');
if (ownConn && mongoose.connection.readyState === 1) {
await mongoose.disconnect();
}
} catch (error) {
console.error('Error:', error.message);
if (ownConn && mongoose.connection.readyState === 1) {
await mongoose.disconnect();
}
process.exit(1);
}
}
// Chạy nếu được gọi trực tiếp
if (require.main === module) {
showStatus();
}
module.exports = { showStatus };