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

37
utils/imageHelper.js Normal file
View File

@@ -0,0 +1,37 @@
/**
* Thêm BACKEND_URL vào đường dẫn hình ảnh
* @param {Object} data - Dữ liệu cần xử lý
* @returns {Object} - Dữ liệu đã được xử lý với đường dẫn hình ảnh đầy đủ
*/
function addBaseUrlToImages(data, baseUrl) {
// baseUrl can be passed explicitly (e.g., from req), otherwise fall back to env
const BACKEND_URL = baseUrl || process.env.BACKEND_URL || '';
// Tạo bản sao sâu để tránh thay đổi dữ liệu gốc
const processedData = JSON.parse(JSON.stringify(data));
// Hàm đệ quy để xử lý tất cả các URL hình ảnh trong đối tượng
const processObject = (obj) => {
if (!obj || typeof obj !== 'object') return;
Object.keys(obj).forEach(key => {
// Kiểm tra nếu thuộc tính chứa đường dẫn hình ảnh bắt đầu bằng /uploads/
if (typeof obj[key] === 'string' && obj[key].startsWith('/uploads/')) {
// Thêm BACKEND_URL nếu đường dẫn chưa có http
if (!obj[key].startsWith('http')) {
obj[key] = `${BACKEND_URL}${obj[key]}`;
}
} else if (typeof obj[key] === 'object') {
// Đệ quy xử lý các đối tượng và mảng lồng nhau
processObject(obj[key]);
}
});
};
processObject(processedData);
return processedData;
}
module.exports = {
addBaseUrlToImages
};

40
utils/jsonHelper.js Normal file
View File

@@ -0,0 +1,40 @@
const fs = require('fs');
const path = require('path');
/**
* Đọc dữ liệu từ file JSON
* @param {string} fileName - Tên file JSON cần đọc (không cần đuôi .json)
* @returns {Object} - Dữ liệu từ file JSON
*/
function readJsonFile(fileName) {
try {
const filePath = path.join(__dirname, '../data', `${fileName}.json`);
const data = fs.readFileSync(filePath, 'utf8');
return JSON.parse(data);
} catch (error) {
console.error(`Lỗi khi đọc file ${fileName}.json:`, error);
return null;
}
}
/**
* Ghi dữ liệu vào file JSON
* @param {string} fileName - Tên file JSON cần ghi (không cần đuôi .json)
* @param {Object} data - Dữ liệu cần ghi vào file
* @returns {boolean} - Kết quả ghi file
*/
function writeJsonFile(fileName, data) {
try {
const filePath = path.join(__dirname, '../data', `${fileName}.json`);
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
return true;
} catch (error) {
console.error(`Lỗi khi ghi file ${fileName}.json:`, error);
return false;
}
}
module.exports = {
readJsonFile,
writeJsonFile
};

168
utils/migrationHelper.js Normal file
View File

@@ -0,0 +1,168 @@
const Migration = require('../models/migration');
/**
* Kiểm tra xem migration đã chạy chưa
* @param {string} migrationName - Tên của migration
* @returns {Promise<boolean>} - true nếu đã chạy, false nếu chưa
*/
async function hasRun(migrationName) {
try {
const migration = await Migration.findOne({ name: migrationName });
return !!migration;
} catch (error) {
console.error(`Error checking migration ${migrationName}:`, error);
throw error;
}
}
/**
* Đánh dấu migration đã chạy
* @param {string} migrationName - Tên của migration
* @param {number} batch - Số batch (mặc định là batch hiện tại + 1)
* @returns {Promise<Object>} - Migration document đã tạo
*/
async function markAsRun(migrationName, batch = null) {
try {
// Nếu không có batch, lấy batch cao nhất + 1
if (batch === null) {
const lastBatch = await Migration.findOne().sort({ batch: -1 });
batch = lastBatch ? lastBatch.batch + 1 : 1;
}
const migration = await Migration.create({
name: migrationName,
batch: batch,
ranAt: new Date()
});
return migration;
} catch (error) {
// Nếu migration đã tồn tại (unique constraint), trả về migration hiện có
if (error.code === 11000) {
return await Migration.findOne({ name: migrationName });
}
console.error(`Error marking migration ${migrationName} as run:`, error);
throw error;
}
}
/**
* Lấy danh sách tất cả các migration đã chạy
* @returns {Promise<Array>} - Danh sách các migration đã chạy
*/
async function getRanMigrations() {
try {
const migrations = await Migration.find().sort({ batch: 1, ranAt: 1 });
return migrations;
} catch (error) {
console.error('Error getting ran migrations:', error);
throw error;
}
}
/**
* Lấy danh sách các migration đã chạy theo batch
* @param {number} batch - Số batch
* @returns {Promise<Array>} - Danh sách các migration trong batch
*/
async function getMigrationsByBatch(batch) {
try {
const migrations = await Migration.find({ batch }).sort({ ranAt: 1 });
return migrations;
} catch (error) {
console.error(`Error getting migrations for batch ${batch}:`, error);
throw error;
}
}
/**
* Xóa migration khỏi bảng tracking (rollback)
* @param {string} migrationName - Tên của migration
* @returns {Promise<boolean>} - true nếu xóa thành công
*/
async function rollback(migrationName) {
try {
const result = await Migration.deleteOne({ name: migrationName });
return result.deletedCount > 0;
} catch (error) {
console.error(`Error rolling back migration ${migrationName}:`, error);
throw error;
}
}
/**
* Xóa tất cả migration trong một batch (rollback batch)
* @param {number} batch - Số batch cần rollback
* @returns {Promise<number>} - Số lượng migration đã xóa
*/
async function rollbackBatch(batch) {
try {
const result = await Migration.deleteMany({ batch });
return result.deletedCount;
} catch (error) {
console.error(`Error rolling back batch ${batch}:`, error);
throw error;
}
}
/**
* Lấy batch số cao nhất
* @returns {Promise<number>} - Batch số cao nhất
*/
async function getLastBatch() {
try {
const lastBatch = await Migration.findOne().sort({ batch: -1 });
return lastBatch ? lastBatch.batch : 0;
} catch (error) {
console.error('Error getting last batch:', error);
throw error;
}
}
/**
* Chạy migration với tracking tự động
* @param {string} migrationName - Tên của migration
* @param {Function} migrationFunction - Hàm migration cần chạy
* @returns {Promise<Object>} - Kết quả migration
*/
async function runMigration(migrationName, migrationFunction) {
try {
// Kiểm tra xem đã chạy chưa
if (await hasRun(migrationName)) {
console.log(`⏭️ Migration ${migrationName} đã chạy, bỏ qua...`);
return { skipped: true, name: migrationName };
}
console.log(`🔄 Đang chạy migration: ${migrationName}...`);
// Chạy migration function
await migrationFunction();
// Đánh dấu đã chạy
const migration = await markAsRun(migrationName);
console.log(`✅ Migration ${migrationName} đã chạy thành công! (Batch: ${migration.batch})`);
return {
success: true,
name: migrationName,
batch: migration.batch,
ranAt: migration.ranAt
};
} catch (error) {
console.error(`❌ Lỗi khi chạy migration ${migrationName}:`, error);
throw error;
}
}
module.exports = {
hasRun,
markAsRun,
getRanMigrations,
getMigrationsByBatch,
rollback,
rollbackBatch,
getLastBatch,
runMigration
};

49
utils/shareLinkHelper.js Normal file
View File

@@ -0,0 +1,49 @@
/**
* Prefix FRONTEND_URL to social share links stored as relative paths.
* - Deep-clones the input and only modifies values under `social.shareLinks` that
* are strings starting with '/'.
* - Leaves absolute URLs (http/https) unchanged.
* - Normalizes to avoid double slashes between FRONTEND_URL and the path.
*
* @param {Object} data - object that may contain `social.shareLinks` (e.g., blog object)
* @returns {Object} - deep-cloned object with prefixed share links
*/
function addFrontendUrlToShareLinks(data) {
const FRONTEND_URL = (process.env.FRONTEND_URL || '').replace(/\/$/, '');
if (!data || typeof data !== 'object') return data;
// Deep clone to avoid mutating original
const cloned = JSON.parse(JSON.stringify(data));
try {
if (cloned.social && typeof cloned.social === 'object') {
const share = cloned.social.shareLinks;
if (share && typeof share === 'object') {
Object.keys(share).forEach(key => {
const val = share[key];
if (typeof val === 'string' && val.length > 0) {
// If already absolute URL, leave it
if (/^https?:\/\//i.test(val)) return;
// Only prefix relative paths that start with '/'
if (val.startsWith('/')) {
// Avoid double slashes
share[key] = FRONTEND_URL ? `${FRONTEND_URL}${val}` : val;
}
}
});
}
}
} catch (err) {
// In case of unexpected structure, return cloned original
console.error('addFrontendUrlToShareLinks error:', err);
return cloned;
}
return cloned;
}
module.exports = {
addFrontendUrlToShareLinks,
};