Fix: about feature

This commit is contained in:
2026-04-23 01:17:51 +07:00
parent 2094ada80f
commit 9c266f71f3
14 changed files with 504 additions and 419 deletions
+75
View File
@@ -0,0 +1,75 @@
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);
+2 -10
View File
@@ -10,12 +10,8 @@ async function validateAboutData(data) {
}
// Tuỳ schema bạn chỉnh lại cho chuẩn hơn
if (!data.title || !data.sections) {
throw new Error('Missing required fields: title or sections');
}
if (!Array.isArray(data.sections)) {
throw new Error('sections must be an array');
if (!data.hero || !data.leadership) {
throw new Error('Missing required fields: hero or leadership');
}
}
@@ -24,11 +20,9 @@ async function migrateAboutData() {
await connectDB();
console.log('Đã kết nối MongoDB...');
// Xóa dữ liệu cũ
await About.deleteMany({});
console.log('Đã xóa dữ liệu About cũ');
// Đọc file JSON
const aboutData = JSON.parse(
await fs.readFile(
path.join(__dirname, '../data/about.json'),
@@ -38,13 +32,11 @@ async function migrateAboutData() {
// Validate
await validateAboutData(aboutData);
// Transform (optional)
const finalData = {
...aboutData,
updatedAt: new Date()
};
// Insert
await About.create(finalData);
+8 -7
View File
@@ -1,7 +1,7 @@
require("dotenv").config();
const path = require("path");
const fs = require("fs");
const {execSync} = require("child_process");
const { execSync } = require("child_process");
const connectDB = require("../config/database");
const migrationHelper = require("../utils/migrationHelper");
@@ -16,9 +16,10 @@ function discoverMigrations() {
// Danh sách các file quản lý migration cần loại trừ
const excludeFiles = [
"migrate-all.js",
"migrate-home.js",
"migrate-about.js",
"migrate-status.js",
"migrate-rollback.js",
"migrate-fresh.js",
"make-migration.js",
];
const migrations = files
@@ -77,7 +78,7 @@ async function runMigrationScript(migration) {
async function runAllMigrations() {
const mongoose = require("mongoose");
let ownConn = false;
try {
const wasConnected = mongoose.connection.readyState === 1;
await connectDB();
@@ -95,7 +96,7 @@ async function runAllMigrations() {
const hasRun = await migrationHelper.hasRun(migration.name);
if (hasRun) {
results.push({name: migration.name, status: "SKIPPED"});
results.push({ name: migration.name, status: "SKIPPED" });
continue;
}
@@ -107,7 +108,7 @@ async function runAllMigrations() {
}
await migrationHelper.markAsRun(migration.name, batch);
results.push({name: migration.name, status: "DONE"});
results.push({ name: migration.name, status: "DONE" });
} catch (error) {
results.push({
name: migration.name,
+177
View File
@@ -0,0 +1,177 @@
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();
+105
View File
@@ -0,0 +1,105 @@
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]);
}
+114
View File
@@ -0,0 +1,114 @@
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 };