summaryrefslogtreecommitdiff
path: root/src/app/db/service/base/helpers.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/app/db/service/base/helpers.js')
-rw-r--r--src/app/db/service/base/helpers.js240
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();
+ };
+}