const express = require('express'); const { wrapAsync, commonErrorHandler, isUuidV4, assertRouteIdMatchesBody, } = require('../helpers'); const { checkCrudPermissions } = require('../middlewares/check-permissions'); const { parse } = require('json2csv'); const { logger } = require('../utils/logger'); const { validateRequest } = require('../middlewares/validate-request'); const { crud: crudSchemas } = require('../validators/request-schemas'); const DEFAULT_LIST_LIMIT = 50; const MAX_LIST_LIMIT = 1000; const MAX_AUTOCOMPLETE_LIMIT = 50; const MAX_CSV_LIMIT = 1000; function clampLimit(value, { defaultLimit, maxLimit }) { const parsed = Number.parseInt(value, 10); if (!Number.isFinite(parsed) || parsed <= 0) return defaultLimit; return Math.min(parsed, maxLimit); } function getSortableFields(DBApi) { if (Array.isArray(DBApi.SORTABLE_FIELDS)) return DBApi.SORTABLE_FIELDS; if (DBApi.MODEL?.rawAttributes) return Object.keys(DBApi.MODEL.rawAttributes); return []; } function normalizeQuery(query = {}, DBApi, { csv = false } = {}) { const normalized = { ...query }; const maxLimit = csv ? MAX_CSV_LIMIT : MAX_LIST_LIMIT; normalized.limit = clampLimit(normalized.limit, { defaultLimit: DEFAULT_LIST_LIMIT, maxLimit, }); const page = Number.parseInt(normalized.page, 10); normalized.page = Number.isFinite(page) && page > 0 ? page : 1; if (normalized.sort) { const sort = String(normalized.sort).toUpperCase(); normalized.sort = sort === 'ASC' ? 'ASC' : 'DESC'; } const sortableFields = getSortableFields(DBApi); if (normalized.field && !sortableFields.includes(normalized.field)) { delete normalized.field; } return normalized; } function createEntityRouter(entityName, Service, DBApi, options = {}) { const router = express.Router(); const permissionEntity = options.permissionEntity || entityName; const validation = options.validation || {}; const schemaFor = (name) => validation[name] || crudSchemas[name]; router.use(checkCrudPermissions(permissionEntity)); router.post( '/', validateRequest(schemaFor('create')), wrapAsync(async (req, res) => { const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; const link = new URL(referer); const payload = await Service.create( req.body.data, req.currentUser, true, link.origin, ); res.status(200).send(payload); }), ); router.post( '/bulk-import', wrapAsync(async (req, res) => { const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; const link = new URL(referer); await Service.bulkImport(req, res, true, link.origin); res.status(200).send(true); }), ); router.put( '/:id', validateRequest(schemaFor('update')), wrapAsync(async (req, res) => { assertRouteIdMatchesBody(req); await Service.update(req.body.data, req.params.id, req.currentUser); res.status(200).send(true); }), ); router.delete( '/:id', validateRequest(schemaFor('remove')), wrapAsync(async (req, res) => { await Service.remove(req.params.id, req.currentUser); res.status(200).send(true); }), ); router.post( '/deleteByIds', validateRequest(schemaFor('deleteByIds')), wrapAsync(async (req, res) => { await Service.deleteByIds(req.body.data, req.currentUser); res.status(200).send(true); }), ); router.get( '/', validateRequest(schemaFor('list')), wrapAsync(async (req, res) => { const filetype = req.query.filetype; const currentUser = req.currentUser; const runtimeContext = req.runtimeContext; const normalizedQuery = normalizeQuery(req.query, DBApi, { csv: filetype === 'csv', }); const payload = await DBApi.findAll(normalizedQuery, { currentUser, runtimeContext, }); if (filetype === 'csv') { const fields = options.csvFields || DBApi.CSV_FIELDS || ['id', 'createdAt']; const opts = { fields }; try { const csv = parse(payload.rows, opts); res.status(200).attachment('export.csv').send(csv); } catch (err) { logger.error({ err, entityName }, 'CSV export error'); res.status(500).send('CSV export error'); } } else { res.status(200).send(payload); } }), ); router.get( '/count', validateRequest(schemaFor('count')), wrapAsync(async (req, res) => { const currentUser = req.currentUser; const runtimeContext = req.runtimeContext; const payload = await DBApi.findAll(normalizeQuery(req.query, DBApi), { countOnly: true, currentUser, runtimeContext, }); res.status(200).send(payload); }), ); router.get( '/autocomplete', validateRequest(schemaFor('autocomplete')), wrapAsync(async (req, res) => { const limit = clampLimit(req.query.limit, { defaultLimit: 20, maxLimit: MAX_AUTOCOMPLETE_LIMIT, }); const payload = await DBApi.findAllAutocomplete( req.query.query, limit, req.query.offset, ); res.status(200).send(payload); }), ); router.get( '/:id', validateRequest(schemaFor('findOne')), wrapAsync(async (req, res) => { const runtimeContext = req.runtimeContext; const payload = await DBApi.findBy( { id: req.params.id }, { runtimeContext }, ); res.status(200).send(payload); }), ); if (options.customRoutes) { options.customRoutes(router, Service, DBApi); } router.use('/', commonErrorHandler); return router; } module.exports = { createEntityRouter, isUuidV4 };