/** * Service construction helpers * @module app/db/service/base/helpers */ import { checkPermission, checkParentPermission, } from "app/services/permission/helpers"; import { sendResponse } from "app/db/service/base/response"; /** * Returns a helper function used by an iterator when creating custom routes. * @param {Service} service the affected service * @param {Service} methods the CRUD methods associated with the service * @returns {Function} the method called on the iterable */ export function createCustomRoute(service, methods) { const { parent } = service.options; /** * Create a custom route. If the route contains `/:id`, the record will be fetched before calling the handlers. * @param {string} method the method name * @param {string} route the route to mount on * @param {Array} handlers a list of middleware * @param {Object} permissions a permissions object */ return function addCustomRouteHandler({ method, route, handlers, permissions, }) { service.router[(method || "get").toLowerCase()]( route, (parent ? [checkParentPermission(parent)] : []) .concat(permissions ? [checkPermission(permissions)] : []) .concat(route.match(/^\/:id/) ? methods.show(service) : []) .concat(handlers) .concat([sendResponse]) ); }; } /** * Returns a helper function used by an iterator when creating CRUD routes. * @param {Service} service the affected service * @param {Object} methods a set of methods to use * @returns {Function} the method called on the iterable */ export function createCrudRoute(service, methods) { const { parent, permissions, hooks, logging } = service.options; const loggingHook = logging ? [logging] : []; /** * Create a CRUD route * @param {string} type the type of route to create */ return function addEnabledRoute(type) { const beforeHooks = hooks?.before[type] || []; const afterHooks = hooks?.after[type] || []; let verb, route, handlers; switch (type) { case "index": verb = "get"; route = "/"; handlers = [ checkPermission(permissions, "read"), ...beforeHooks, methods.index(service), ]; break; case "show": verb = "get"; route = "/:id"; handlers = [ checkPermission(permissions, "read"), ...beforeHooks, methods.show(service), ]; break; case "create": verb = "post"; route = "/"; handlers = [ checkPermission(permissions, "create"), ...beforeHooks, methods.create(service), ...loggingHook, ]; break; case "update": verb = "put"; route = "/:id"; handlers = [ checkPermission(permissions, "read"), ...(hooks?.before?.show || []), methods.show(service), ...beforeHooks, checkPermission(permissions, "update"), methods.update(service), ...loggingHook, ]; break; case "updateMany": verb = "put"; route = "/"; handlers = [ checkPermission(permissions, "read"), methods.showMany(service), ...beforeHooks, checkPermission(permissions, "update"), methods.updateMany(service), ...loggingHook, ]; break; case "sort": verb = "post"; route = "/sort"; handlers = [ checkPermission(permissions, "read"), ...beforeHooks, checkPermission(permissions, "update"), methods.sort(service), // ...loggingHook, ]; break; case "destroy": verb = "delete"; route = "/:id"; handlers = [ checkPermission(permissions, "read"), methods.show(service), ...beforeHooks, checkPermission(permissions, "destroy"), methods.destroy(service), ...loggingHook, ]; break; case "destroyMany": verb = "delete"; route = "/"; handlers = [ checkPermission(permissions, "read"), ...beforeHooks, // above, i'm using show to see if the records exist before deleting them // but if deleting many, this is probably being done on the pivot table, // and where the check is accomplished implicitly by the destroy middleware. // if delete permission is ALLOW_FOR_OWNER, we still have to make the check. // methods.showMany(service), checkPermission(permissions, "destroy"), methods.destroyMany(service), ...loggingHook, ]; break; default: console.error(`Unknown route: ${type}`); return; } let routeHandlers = [] .concat( parent ? [formatParentParameters(service), checkParentPermission(parent)] : [] ) .concat(handlers) .concat(afterHooks) .concat(type === "destroy" ? [onDestroyCleanupResponse] : []) .concat([sendResponse]); service.router[verb]( route, routeHandlers.filter((handler) => !!handler) ); }; } /** * Helper function to convert request.params to integers where appropriate */ export function formatParentParameters(service) { const parentIdAttributes = service.idAttributes.slice(1); return function formatParentParametersMiddleware(request, response, next) { const hasInvalidParams = parentIdAttributes.some((parentIdAttribute) => { const value = parseInt(request.params[parentIdAttribute]); if (!value) { return true; } request.params[parentIdAttribute] = value; return false; }); if (hasInvalidParams) { return next(new Error("malformed parent ID")); } next(); }; } /** * Helper function to clean up local data. We make the deleted object available to middleware, but not to the client. * @param {express.request} request the request object * @param {express.response} response the response object * @param {function} next function to proceed to the next middleware */ export function onDestroyCleanupResponse(request, response, next) { delete response.locals.data; delete response.locals.child; next(); } /** * For each returned object, fetch the count of a particular relation * @param {string} relation name of relation * @param {string} valueName (optional) name of the value to set on the output object, defaults to `{relation}_count`` * @return {Function} middleware function (`after` hook) */ export function getRelationCount(relation, as, where) { return async function getRelationCountMiddleware(request, response, next) { if (!response.locals.data) { return next(); } const countField = relation + "_count"; const items = "length" in response.locals.data ? response.locals.data : [response.locals.data]; const getCountPromises = items.map(async (item) => { if (!item || !item.related || countField in item) return; const itemRelation = item.related(relation); if (where) { itemRelation.where(where); } const count = await itemRelation.count(); item.set(as || countField, count); }); try { await Promise.all(getCountPromises); } catch (error) { console.error(error); } next(); }; }