diff options
| author | Jules Laplace <julescarbon@gmail.com> | 2021-10-17 02:52:05 +0200 |
|---|---|---|
| committer | Jules Laplace <julescarbon@gmail.com> | 2021-10-17 02:52:05 +0200 |
| commit | 06ecdf2af182034496e2123852deee4a58de1043 (patch) | |
| tree | c8d4eb9664dd368bee5a4bf73dd1e02015ecaf39 /src/app/db/service/base/helpers.js | |
making a shoebox
Diffstat (limited to 'src/app/db/service/base/helpers.js')
| -rw-r--r-- | src/app/db/service/base/helpers.js | 240 |
1 files changed, 240 insertions, 0 deletions
diff --git a/src/app/db/service/base/helpers.js b/src/app/db/service/base/helpers.js new file mode 100644 index 0000000..9ab1119 --- /dev/null +++ b/src/app/db/service/base/helpers.js @@ -0,0 +1,240 @@ +/** + * 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(); + }; +} |
