UNPKG

@tasolutions/express-core

Version:
742 lines (640 loc) 28.3 kB
//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; };