39948-vm/backend/src/factories/router.factory.js

209 lines
5.7 KiB
JavaScript

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 };