@tasolutions/express-core
Version:
All libs for express
742 lines (640 loc) • 28.3 kB
JavaScript
//crud.js
const express = require('express');
const { httpStatus } = require('./httpStatus');
const queryUtil = require('./query.util');
const _ = require('lodash');
const { uploadFiles } = require('./upload');
const actionsDefault = ['CREATE', 'LIST', 'DETAIL', 'UPDATE', 'DELETE', 'COUNT', 'EXPORT'];
const mongoose = require('mongoose');
const excel = require('exceljs');
const Response = require('./response');
const actionList = require('./actionList');
const { authInfo } = require('../config');
const { mapHeadersAndCheckAuthorization, mapHeadersToQuery } = require('./requestHeader');
const { logger } = require('../loggers');
module.exports = (Collection, auth = false, configs = { actions: actionsDefault, onlyOwner: false, onlyAdminOwner: false, isFilterMerchant: true, includeFields: [], query: false, sort: false }) => {
// ======
// Create
// ======
const create = async (req, res) => {
try {
await mapHeadersToQuery(req, Collection);
if (req.userLogin && req.userLogin.userId) {
req.body.creatorId = req.userLogin.userId;
}
// Merchant
if (authInfo.enableMerchant && !req.isSystemAdmin && configs.isFilterMerchant) {
const { merchant } = req.headers;
req.body.merchant_id = merchant;
}
// Remove fields with empty string values from req.body
Object.keys(req.body).forEach((key) => {
if (req.body[key] === "") {
req.body[key] = null;
}
});
// Phân tích cú pháp req.body thành cấu trúc đối tượng
const parsedBody = parseFormData(req.body);
const newEntry = await uploadFiles(req, Collection);
const createdEntry = await Collection.create({ ...newEntry, ...parsedBody }); // Kết hợp các trường với parsedBody
return Response.success(res, createdEntry);
} catch (e) {
return Response.error(res, e.message);
}
};
// =========
// Read many
// =========
const readMany = async (req, res) => {
try {
if (configs.onlyOwner && req.userLogin && req.userLogin.userId) {
req.query.user_id = req.userLogin.userId;
}
const sort = getSort(req, configs.sort);
let query = await getQuery(Collection, req);
if (!configs.isFilterMerchant) delete query['merchant_id'];
if (configs.query) {
query = Object.assign({}, configs.query, query);
};
let andConditions = []; // Mảng để lưu điều kiện $and cho truy vấn
// Thêm dịch vụ loại nếu có
if (req.query.service_type) {
andConditions.push({ service_type: req.query.service_type });
}
// Xử lý tìm kiếm
if (req.query.search) {
const searchQuery = queryUtil.search(Collection, req.query.search);
delete req.query.search; // Xoá search từ query
// Thêm searchQuery vào mảng điều kiện
andConditions.push(searchQuery);
}
// Kiểm tra nếu có điều kiện $and
if (andConditions.length > 0) {
query = { $and: andConditions };
}
// Trở về các thuộc tính khác nếu có
query = Object.assign(await mapHeadersToQuery(req, Collection), query);
const populate = configs.populate ? configs.populate : queryUtil.populateField(Collection);
let results = await Collection.find(query).populate(populate).sort(sort);
if (Array.isArray(configs.includeFields) && configs.includeFields.length > 0) {
results = results.map(result => _.pick(result, configs.includeFields));
}
return Response.success(res, results);
} catch (e) {
return Response.error(res, e.message);
}
};
// =========
// Get list by pagination and filter
// =========
const getList = async (req, res) => {
try {
if (configs.onlyOwner && req.userLogin && req.userLogin.userId) {
req.query.user_id = req.userLogin.userId;
}
if (configs.onlyAdminOwner && req.userLogin && req.userLogin.userId) {
req.query.admin_user_id = req.userLogin.userId;
}
const sort = getSort(req, configs.sort);
let query = await getQuery(Collection, req);
if (!configs.isFilterMerchant) delete query['merchant_id'];
if (configs.query) {
query = Object.assign({}, configs.query, query);
};
const options = {
lean: true,
page: req.query.page || 1,
limit: req.query.limit || 10,
sort: sort,
};
delete query['page'];
delete query['limit'];
delete req.query.limit;
delete req.query.page;
let andConditions = []; // Mảng để lưu điều kiện $and cho truy vấn
// Thêm dịch vụ loại nếu có
if (req.query.service_type) {
andConditions.push({ service_type: req.query.service_type });
}
// Xử lý tìm kiếm
if (req.query.search) {
const searchQuery = queryUtil.search(Collection, req.query.search);
delete req.query.search; // Xoá search từ query
// Thêm searchQuery vào mảng điều kiện
andConditions.push(searchQuery);
}
// Kiểm tra nếu có điều kiện $and
if (andConditions.length > 0) {
query = { $and: andConditions };
}
// Trở về các thuộc tính khác nếu có
query = Object.assign(await mapHeadersToQuery(req, Collection), query);
options.populate = configs.populate ? configs.populate : queryUtil.populateField(Collection);
let results;
if (configs.callbackLIST) {
results = await Collection.paginate(query, options).then(configs.callbackLIST);
} else {
results = await Collection.paginate(query, options);
}
if (Array.isArray(configs.includeFields) && configs.includeFields.length > 0) {
results.docs = results.docs.map(result => _.pick(result, configs.includeFields));
}
return Response.success(res, results);
} catch (e) {
return Response.error(res, e.message);
}
};
// ========
// Read one
// ========
const readOne = async (req, res) => {
let payloadFilter = { _id: req.params._id };
if (configs.onlyOwner && req.userLogin && req.userLogin.userId) {
payloadFilter.user_id = req.userLogin.userId;
}
// Ánh xạ headers và kiểm tra quyền truy cập
const isAuthorized = await mapHeadersAndCheckAuthorization(req, payloadFilter, Collection);
if (!isAuthorized) {
return Response.error(res, 'User does not have permission to update the record.', httpStatus.FORBIDDEN);
}
// Merchant
if (authInfo.enableMerchant && !req.isSystemAdmin && configs.isFilterMerchant) {
const { merchant } = req.headers;
payloadFilter['merchant_id'] = merchant;
}
try {
if (!mongoose.Types.ObjectId.isValid(payloadFilter._id)) {
switch (payloadFilter._id) {
case 'count':
return count(req, res);
case 'export':
return exportToExcel(req, res);
default:
res.status(httpStatus.BAD_REQUEST).json({
statusCode: httpStatus.BAD_REQUEST,
success: false,
status: 'error',
message: 'Something went wrong',
});
}
}
let result = await Collection.findOne(payloadFilter).populate(queryUtil.populateField(Collection));
if (result && configs.callbackDETAIL) {
result = await configs.callbackDETAIL(result);
}
return result ? Response.success(res, result) : Response.error(res, 'Not found', httpStatus.NOT_FOUND);
} catch (e) {
return Response.error(res, e.message);
}
};
// ======
// Update
// ======
const update = async (req, res) => {
if (req.userLogin && req.userLogin.userId) {
req.body.creatorId = req.userLogin.userId;
}
let payloadFilter = { _id: req.params._id };
// Ánh xạ headers và kiểm tra quyền truy cập
const isAuthorized = await mapHeadersAndCheckAuthorization(req, payloadFilter, Collection);
if (!isAuthorized) {
return Response.error(res, 'User does not have permission to update the record.', httpStatus.FORBIDDEN);
}
// Merchant
if (authInfo.enableMerchant && !req.isSystemAdmin) {
const { merchant } = req.headers;
payloadFilter['merchant_id'] = merchant;
req.body.merchant_id = merchant;
}
// Remove fields with empty string values from req.body
Object.keys(req.body).forEach((key) => {
if (req.body[key] === "") {
req.body[key] = null;
}
});
// Phân tích cú pháp req.body thành cấu trúc đối tượng
const parsedBody = parseFormData(req.body);
try {
const oldData = await Collection.findById(req.params._id);
if (!oldData) {
return res.status(httpStatus.NOT_FOUND).json({
statusCode: httpStatus.NOT_FOUND,
success: false,
message: "Entry not found",
});
}
const payload = await uploadFiles(req, Collection);
const result = await Collection.updateOne(payloadFilter, { $set: { ...payload, ...parsedBody } });
if (result.nModified === 0) {
return res.status(httpStatus.NOT_FOUND).json({
statusCode: httpStatus.NOT_FOUND,
success: false,
message: "Entry not found",
});
}
const changedEntry = await Collection.findById(req.params._id);
logger.info({
module: Collection.collection.collectionName,
action: 'UPDATE',
oldData: oldData,
newData: changedEntry,
updatedBy: req.userLogin && req.userLogin.userId ? req.userLogin.userId : null,
});
return Response.success(res, changedEntry);
} catch (e) {
return Response.error(res, e.message);
}
};
// ======
// Remove
// ======
const remove = async (req, res) => {
try {
let payloadFilter = { _id: req.params._id };
// Ánh xạ headers và kiểm tra quyền truy cập
const isAuthorized = await mapHeadersAndCheckAuthorization(req, payloadFilter, Collection);
if (!isAuthorized) {
return Response.error(res, 'User does not have permission to update the record.', httpStatus.FORBIDDEN);
}
// Merchant
if (authInfo.enableMerchant && !req.isSystemAdmin) {
const { merchant } = req.headers;
payloadFilter['merchant_id'] = merchant;
}
await Collection.deleteOne(payloadFilter);
return Response.success(res);
} catch (e) {
return Response.error(res, e.message);
}
};
// ======
// Get filter by in ids field
// ======
const getByIds = async (req, res) => {
try {
const hash = req.body;
let option = {};
for (var key of Object.keys(hash)) {
option[`${key}`] = { $in: hash[key] };
}
const results = Object.entries(option).length ? await Collection.find(option) : [];
return Response.success(res, results);
} catch (e) {
return Response.error(res, e.message);
}
};
// ========
// Count
// ========
const count = async (req, res) => {
await mapHeadersToQuery(req, Collection);
if (configs.onlyOwner && req.userLogin && req.userLogin.userId) {
req.query.user_id = req.userLogin.userId;
}
if (configs.onlyAdminOwner && req.userLogin && req.userLogin.userId) {
req.query.admin_user_id = req.userLogin.userId;
}
let query = await getQuery(Collection, req);
try {
const result = {
total: await Collection.count(),
count: await Collection.count(query),
}
return Response.success(res, result);
} catch (e) {
return Response.error(res, e.message);
}
};
const exportToExcel = async (req, res) => {
try {
await mapHeadersToQuery(req, Collection);
if (configs.onlyOwner && req.userLogin && req.userLogin.userId) {
req.query.user_id = req.userLogin.userId;
}
if (configs.onlyAdminOwner && req.userLogin && req.userLogin.userId) {
req.query.admin_user_id = req.userLogin.userId;
}
const sort = getSort(req);
const query = await getQuery(Collection, req);
if (req.query.search) {
query = queryUtil.search(Collection, req.query.search);
}
const populate = configs.populate ? configs.populate : queryUtil.populateField(Collection);
const results = await Collection.find(query).populate(populate).sort(sort);
const workbook = new excel.Workbook();
const worksheet = workbook.addWorksheet('Data');
const generateHeaders = (schema, prefix = '') => {
const headers = [];
for (const path in schema.paths) {
const fieldSchema = schema.paths[path];
if (fieldSchema.instance === 'ObjectID' && fieldSchema.options.ref) {
const refSchema = mongoose.model(fieldSchema.options.ref).schema;
const refHeaders = generateHeaders(refSchema, `${prefix}${path}.`);
headers.push(...refHeaders);
} else {
headers.push(`${prefix}${path}`);
}
}
return headers;
};
const headers = Array.isArray(configs.includeFields) && configs.includeFields.length > 0 ? headersToInclude(Collection.schema, configs.includeFields) : generateHeaders(Collection.schema);
worksheet.columns = headers.map((key) => {
return { header: key, key: key };
});
results.forEach((item) => {
const rowData = {};
headers.forEach((key) => {
rowData[key] = deepGet(item, key);
});
worksheet.addRow(rowData);
});
const buffer = await workbook.xlsx.writeBuffer();
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
res.setHeader('Content-Disposition', `attachment; filename=${Collection.modelName}.xlsx`);
res.send(buffer);
} catch (e) {
return Response.error(res, e.message);
}
};
// Hàm để lấy schema của collection
const getSchema = (req, res) => {
try {
const schema = Collection.schema.paths; // Get the fields from the schema
const formattedSchema = [];
const excludeFields = configs.excludeFields && configs.excludeFields.length ? configs.excludeFields : [];
// Iterate over each field and format it as required
Object.keys(schema).forEach((key) => {
// Skip excluded fields
if (excludeFields.includes(key)) {
return;
}
const field = schema[key];
// Create a base schema object
const fieldObject = {
key: key,
type: field.instance.toLowerCase(), // Get data type
...(field.isRequired && { required: true }), // Check if required
...(field.defaultValue !== undefined && { default: field.defaultValue }) // Check for default value
};
// Add additional custom properties if they exist
const customProperties = ['title', 'group', 'order'];
customProperties.forEach(prop => {
if (field.options && field.options[prop]) {
fieldObject[prop] = field.options[prop];
}
});
formattedSchema.push(fieldObject); // Add the formatted field object to the array
});
return Response.success(res, formattedSchema); // Return the formatted schema
} catch (e) {
return Response.error(res, e.message); // Handle errors
}
};
// ======
// Routes
// ======
let router = express.Router();
const actions = configs.actions ? configs.actions : actionsDefault;
const path = configs.path;
router.get('/schema', getSchema);
if (auth && actions) {
if (actions.includes('CREATE')) {
addActionToList(Collection, 'CREATE', 'POST', path);
router.post('', setAction(Collection, 'CREATE'), auth, create);
}
if (actions.includes('LIST')) {
addActionToList(Collection, 'LIST', 'GET', `${path}/paginate`);
router.get('/paginate', setAction(Collection, 'LIST'), auth, getList);
addActionToList(Collection, 'LIST', 'GET', path);
router.get('', setAction(Collection, 'LIST'), auth, readMany);
}
if (actions.includes('UPDATE')) {
addActionToList(Collection, 'UPDATE', 'PUT', `${path}/:_id`);
router.put('/:_id', setAction(Collection, 'UPDATE'), auth, update);
}
if (actions.includes('DETAIL')) {
addActionToList(Collection, 'DETAIL', 'POST', `${path}/find-in-ids`);
router.post('/find-in-ids', setAction(Collection, 'DETAIL'), auth, getByIds);
addActionToList(Collection, 'DETAIL', 'GET', `${path}/:_id`);
router.get('/:_id', setAction(Collection, 'DETAIL'), auth, readOne);
}
if (actions.includes('DELETE')) {
addActionToList(Collection, 'DELETE', 'DELETE', `${path}/:_id`);
router.delete('/:_id', setAction(Collection, 'DELETE'), auth, remove);
}
if (actions.includes('COUNT')) {
addActionToList(Collection, 'COUNT', 'GET', `${path}/count`);
router.get('/count', setAction(Collection, 'COUNT'), auth, count);
}
if (actions.includes('EXPORT')) {
addActionToList(Collection, 'EXPORT', 'GET', `${path}/export`);
router.get('/export', setAction(Collection, 'EXPORT'), auth, exportToExcel);
}
} else if (actions) {
if (actions.includes('CREATE')) {
addActionToList(Collection, 'CREATE', 'POST', path);
router.post('', create);
}
if (actions.includes('UPDATE')) {
addActionToList(Collection, 'UPDATE', 'PUT', `${path}/:_id`);
router.put('/:_id', update);
}
if (actions.includes('LIST')) {
addActionToList(Collection, 'LIST', 'GET', `${path}/paginate`);
router.get('/paginate', getList);
addActionToList(Collection, 'LIST', 'GET', path);
router.get('', readMany);
}
if (actions.includes('DETAIL')) {
addActionToList(Collection, 'DETAIL', 'POST', `${path}/find-in-ids`);
router.post('/find-in-ids', getByIds);
addActionToList(Collection, 'DETAIL', 'GET', `${path}/:_id`);
router.get('/:_id', readOne);
}
if (actions.includes('DELETE')) {
addActionToList(Collection, 'DELETE', 'DELETE', `${path}/:_id`);
router.delete('/:_id', remove);
}
if (actions.includes('COUNT')) {
addActionToList(Collection, 'COUNT', 'GET', `${path}/count`);
router.get('/count', count);
}
if (actions.includes('EXPORT')) {
addActionToList(Collection, 'EXPORT', 'GET', `${path}/export`);
router.get('/export', exportToExcel);
}
}
return router;
};
/**
* This function get query
*/
const getQuery = async (Collection, req) => {
let query = { ...req.query };
// Merchant
if (authInfo.enableMerchant && !req.isSystemAdmin) {
const { merchant } = req.headers;
query['merchant_id'] = merchant;
}
const sortFields = Object.keys(query).filter(key => key.startsWith('sort_'));
sortFields.forEach(field => delete query[field]);
// Gọi hàm tìm kiếm chung
query = await searchByObjectIdField(query, req, Collection);
let keySchema = Object.keys(Collection.schema.tree);
keySchema = _.without(keySchema, 'id', '_id', '__v');
const keyVirtuals = Object.keys(Collection.schema.virtuals);
keySchema = keySchema.filter(item => !keyVirtuals.includes(item));
keySchema.forEach(key => {
const path = Collection.schema.path(key);
if (!path) return;
switch (path.instance) {
case 'Date':
query = queryUtil.queryDateFromTo(query, key);
delete req.query[`${key}_from`];
delete req.query[`${key}_to`];
break;
case 'String':
if (path.enumValues) {
query = queryUtil.queryInEnum(query, key);
} else {
query = queryUtil.whereLike(query, key);
}
break;
case 'Number':
query = queryUtil.queryNumberFromTo(query, key);
delete req.query[`${key}_from`];
delete req.query[`${key}_to`];
break;
default:
break;
}
});
return query;
};
const getModelFromRef = (modelName) => {
if (mongoose.models[modelName]) {
return mongoose.models[modelName];
} else {
return false;
}
};
const searchByObjectIdField = async (query, req, collection) => {
const { search } = req.query;
if (search) {
const regexSearch = { $regex: search, $options: 'i' };
// Lấy tất cả các trường có kiểu ObjectId từ schema
const objectIdFields = Object.keys(collection.schema.tree).filter(key => {
const path = collection.schema.path(key);
return path && ['ObjectId', 'ObjectID'].includes(path.instance) && path.options && path.options.ref; // Kiểm tra nếu có ref
});
// Tìm kiếm trên mỗi trường ObjectId mà có ref
for (const field of objectIdFields) {
const modelName = collection.schema.path(field).options.ref; // Lấy tên model từ options.ref
const Model = getModelFromRef(modelName); // Nhận mô hình
if (!Model) return query;
// Lấy các trường của Model để tìm kiếm
let keySchema = Object.keys(Model.schema.tree);
keySchema = _.without(keySchema, 'id', '_id', '__v');
const keyVirtuals = Object.keys(Model.schema.virtuals);
keySchema = keySchema.filter(item => !keyVirtuals.includes(item));
// Tạo điều kiện tìm kiếm cho tất cả các trường của model
const searchConditions = keySchema.map(key => {
const path = Model.schema.path(key);
if (path && path.instance === 'String') {
return { [key]: regexSearch }; // Tìm kiếm cho các trường kiểu String
}
return null; // Tránh thêm điều kiện nếu không phải String
}).filter(condition => condition !== null); // Lọc các điều kiện null
// Thực hiện tìm kiếm nếu có điều kiện
if (searchConditions.length > 0) {
const foundDocument = await Model.findOne({ $or: searchConditions }).select('_id');
if (foundDocument) {
query[field] = foundDocument._id; // Cập nhật query với ObjectId tìm được
}
}
}
}
return query;
};
/**
* This function get getSort
*/
const getSort = (req, sorts = {}) => {
const newSorts = Object.keys(sorts).reduce((acc, key) => {
const newKey = 'sort_' + key;
acc[newKey] = sorts[key];
return acc;
}, {});
const query = Object.assign({ ...req.query }, newSorts);
const sortFields = Object.keys(query).filter(key => key.startsWith('sort_'));
return sortFields.map(field => {
const strippedField = field.slice(5);
return [strippedField, query[field]];
});
};
const deepGet = (obj, path) => {
const keys = path.split('.');
let value = obj;
for (const key of keys) {
if (!value || typeof value !== 'object') {
return undefined;
}
value = value[key];
}
return value;
};
const setAction = (Collection, action) => {
return async (req, res, next) => {
req.key = `${Collection.modelName}_${action}`.toUpperCase();
next();
};
};
const addActionToList = (Collection, action, method, path) => {
const controllerKey = Collection.modelName.toUpperCase();
actionList.push({
controller: controllerKey,
controller_name: _.startCase(controllerKey.toLowerCase()),
key: `${Collection.modelName}_${action}`.toUpperCase(),
name: _.startCase(`${Collection.modelName} ${action}`.toLocaleLowerCase()),
path,
method
});
};
const headersToInclude = (schema, includeFields) => {
const headers = [];
for (const path of includeFields) {
const fieldSchema = schema.paths[path];
if (fieldSchema) {
headers.push(path);
}
}
return headers;
};
const parseFormData = (data) => {
const result = {};
Object.keys(data).forEach(key => {
const matches = key.match(/([^\[]+)\[([^\]]*)\]/); // Tìm nhóm và subKey
if (matches) {
const group = matches[1]; // Phần trước dấu '['
const subKey = matches[2]; // Phần giữa dấu '[' và ']'
if (!result[group]) {
if (subKey === "") {
result[group] = []; // Khởi tạo mảng nếu không có subKey
} else {
result[group] = {}; // Khởi tạo đối tượng nếu có subKey
}
}
if (subKey === "") { // Nếu subKey rỗng
if (data[key] !== null) {
result[group] = result[group].concat(data[key]); // Kết hợp mảng nếu không phải null
}
// Nếu null, không làm gì cả, giữ nguyên mảng rỗng
} else {
result[group][subKey] = data[key]; // Gán giá trị cho khóa con
}
} else {
result[key] = data[key]; // Nếu không có lồng nhau thì thêm trực tiếp
}
});
return result;
};