summaryrefslogtreecommitdiff
path: root/src/app/db/service/base
diff options
context:
space:
mode:
Diffstat (limited to 'src/app/db/service/base')
-rw-r--r--src/app/db/service/base/helpers.js240
-rw-r--r--src/app/db/service/base/index.js137
-rw-r--r--src/app/db/service/base/many.js205
-rw-r--r--src/app/db/service/base/methods.js217
-rw-r--r--src/app/db/service/base/response.js118
5 files changed, 917 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();
+ };
+}
diff --git a/src/app/db/service/base/index.js b/src/app/db/service/base/index.js
new file mode 100644
index 0000000..188d86a
--- /dev/null
+++ b/src/app/db/service/base/index.js
@@ -0,0 +1,137 @@
+/**
+ * Abstract API service
+ * @module app/db/service/base/index
+ */
+
+import { Router } from "express";
+import {
+ checkAccessToken,
+ checkUserIsActive,
+} from "app/services/authentication/helpers";
+import { indexQueryBuilder, loadColumns } from "app/db/helpers";
+
+import * as serviceMethods from "app/db/service/base/methods";
+import { handleError } from "app/db/service/base/response";
+import {
+ createCustomRoute,
+ createCrudRoute,
+} from "app/db/service/base/helpers";
+
+/**
+ * By default, no API routes are enabled.
+ */
+// const enableAll = ["index", "show", "create", "update", "destroy"];
+const noRoutesEnabled = [];
+
+/**
+ * Create an API service.
+ * @param {Object} options the options dictionary
+ * @param {bookshelf} options.bookshelf the Bookshelf instance created by the server
+ * @param {Service} options.parent the Service that this inherits from
+ * @param {Model|string} options.Model the name of a Bookshelf model, or the model itself
+ * @param {string} options.name optional, the name of this resource. Used for logging
+ * @param {string[]} options.enabled[] specific CRUD APIs to enable
+ * @param {Object} options.hooks extra middleware functions for each endpoint
+ * @param {Object} options.hooks.before middleware called before a DB call
+ * @param {function[]} options.hooks.before.index[] middleware called before index queries
+ * @param {function[]} options.hooks.before.show[] middleware called before show queries
+ * @param {function[]} options.hooks.before.create[] middleware called before create queries
+ * @param {function[]} options.hooks.before.update[] middleware called before update queries
+ * @param {function[]} options.hooks.before.destroy[] middleware called before destroy queries
+ * @param {function} options.logging middleware hook which runs after create/update/destroy queries, to record events
+ * @param {Object} options.hooks.after middleware called after a DB call, similar to `before`
+ * @param {boolean} options.authenticate set to false to disable authentication for this service
+ * @param {Object} options.permissions per-API permissions
+ * @param {Object} options.permissions.read permissions applied when reading records
+ * @param {number[]} options.permissions.read.roles[] allow access with matching user role
+ * @param {boolean} options.permissions.read.owner allow access with matching `user_id`
+ * @param {Object} options.permissions.create permissions applied when creating records
+ * @param {Object} options.permissions.update permissions applied when editing records
+ * @param {Object} options.permissions.destroy permissions applied when destroying records
+ * @param {Object[]} options.routes[] additional list of non-CRUD routes
+ * @param {string} options.routes[].method method to use, get/post/put/delete
+ * @param {route} options.routes[].route endpoint to use, e.g. `/login`
+ * @param {function[]} options.routes[].handlers[] list of middleware functions
+ * @param {privateFields} options.privateFields list of fields which should not be editable through the API
+ * @param {Object} methods optional method lookup, for alternative CRUD middleware
+ * @return {Service} the service object
+ */
+export default async function Service(options, methods = serviceMethods) {
+ const service = { type: "base", options, children: {} };
+
+ const { bookshelf, parent, Model, enabled, routes, authenticate } = options;
+
+ /** Locate the Model specified in the configuration */
+ service.Model =
+ Model && typeof Model === "string" ? bookshelf.model(Model) : options.Model;
+
+ /** Use the model to identify the service's resource name */
+ service.resource = options.name || service.Model?.prototype.tableName;
+ if (!service.resource) {
+ throw new Error("No name or model defined for resource");
+ }
+
+ /** Get the ID attributes of this model, and any parent models */
+ if (service.Model) {
+ if (parent) {
+ service.idAttributes = [service.Model.prototype.idAttribute].concat(
+ parent.idAttributes
+ );
+ } else {
+ service.idAttributes = [service.Model.prototype.idAttribute];
+ }
+ }
+
+ /** Load the column names for this service's Model */
+ service.columns =
+ service.Model && (await loadColumns(bookshelf, service.Model));
+
+ /** Specify the `index` queryBuilder */
+ service.queryBuilder = options.queryBuilder || indexQueryBuilder;
+
+ // console.log("* Creating", service.resource, "service");
+
+ /** Instantiate the Express router. If this is a sub-router, merge the parent's params. */
+ service.router = Router({ mergeParams: !!parent });
+
+ /** Associate the parent resource with this resource */
+ if (parent) {
+ service.parent = parent;
+ }
+
+ /** Most APIs need access to the database, so supply this on the request object */
+ service.router.use((request, response, next) => {
+ request.service = service;
+ request.bookshelf = bookshelf;
+ next();
+ });
+
+ /** Most APIs authenticate, but this can be disabled (for the authentication service) */
+ if (authenticate !== false) {
+ service.router.use(checkAccessToken(options.authenticationOptions || {}));
+ service.router.use(checkUserIsActive);
+ }
+
+ /** Add custom routes first */
+ if (routes) {
+ routes.forEach(createCustomRoute(service, methods));
+ }
+
+ /** Add all enabled CRUD routes, if this service has a Model. */
+ const enabledRoutes = (service.Model && enabled) || noRoutesEnabled;
+ enabledRoutes.forEach(createCrudRoute(service, methods));
+
+ /** Add error-handling middleware */
+ service.router.use(handleError);
+
+ /** Method to attach a child service to this service's router
+ * @param {string} route base route of this service
+ * @param {Service} childService instance of the child service
+ */
+ service.use = (route, childService) => {
+ service.router.use(`${route}`, childService.router);
+ service.children[route] = childService;
+ };
+
+ return service;
+}
diff --git a/src/app/db/service/base/many.js b/src/app/db/service/base/many.js
new file mode 100644
index 0000000..1791694
--- /dev/null
+++ b/src/app/db/service/base/many.js
@@ -0,0 +1,205 @@
+/**
+ * Service API methods that affect multiple records
+ * @module app/db/service/base/methods
+ */
+
+import * as db from "app/db/query";
+import { reduceValidColumns } from "app/db/helpers";
+import { PERMISSIONS } from "app/constants";
+import debugModule from "debug";
+
+/**
+ * Debug logger
+ */
+const debug = debugModule("shoebox:service");
+
+/**
+ * API to query for multiple records by ID
+ */
+export function showMany(service) {
+ const { Model, parent, resource } = service;
+ const { childRelation } = service.options;
+ const idAttribute = service.idAttributes[0];
+ return async function showManyMiddleware(request, response, next) {
+ const { user, permission, body } = request;
+ const ids = body.map((item) => item[idAttribute]).filter((id) => !!id);
+ let data;
+ try {
+ if (parent) {
+ // Fetch the immediate parent of the pivot table based on the name of the parent resource.
+ // This instance is added to the `request.parents` object when performing the permissions check.
+ const parentInstance = request.parents[parent.resource];
+ data = await parentInstance
+ .related(childRelation)
+ .query((builder) => builder.whereIn(idAttribute, ids));
+ } else {
+ data = await db.showIDs({
+ Model,
+ ids,
+ });
+ }
+ } catch (error) {
+ debug(`${resource} Show error`);
+ debug(error);
+ return next(error);
+ }
+ if (!data) {
+ response.locals = { data: [] };
+ next();
+ } else if (
+ permission === PERMISSIONS.ALLOW_FOR_OWNER &&
+ data.some((item) => item.get("user_id") !== user.user_id)
+ ) {
+ next(new Error("PermissionsError"));
+ } else {
+ response.locals = { data };
+ next();
+ }
+ };
+}
+
+/**
+ * API to update multiple records
+ */
+export function updateMany(service) {
+ return async function updateManyMiddleware(request, response, next) {
+ const data = await handleUpdateManyWithTransaction(
+ service,
+ request,
+ response
+ );
+ response.locals.data = data;
+ next();
+ };
+}
+
+/**
+ * Update multiple records using a transaction
+ */
+export async function handleUpdateManyWithTransaction(
+ service,
+ request,
+ response
+) {
+ const { Model, idAttributes, columns } = service;
+ const { bookshelf, privateFields } = service.options;
+ const idAttribute = idAttributes[0];
+ return await bookshelf.transaction((transaction) => {
+ const { data: instances } = response.locals;
+ const { params, user, permission, body } = request;
+ const instanceLookup = instances.reduce((lookup, instance) => {
+ lookup[instance.id] = instance;
+ return lookup;
+ }, {});
+ const promises = body.map((item) => {
+ const itemId = item[idAttribute];
+ if (!itemId) {
+ return handleCreate({
+ Model,
+ body: item,
+ idAttributes,
+ params,
+ columns,
+ privateFields,
+ transaction,
+ });
+ } else if (itemId in instanceLookup) {
+ return handleUpdate({
+ instance: instanceLookup[itemId],
+ body: item,
+ user,
+ permission,
+ columns,
+ privateFields,
+ transaction,
+ });
+ } else {
+ throw new Error("item id not found on this record");
+ }
+ });
+ return Promise.all(promises);
+ });
+}
+
+/**
+ * API to destroy multiple records
+ */
+export function destroyMany(service) {
+ const { Model, idAttributes } = service;
+ const [idAttribute, ...parentIdAttributes] = idAttributes;
+ return async function destroyManyMiddleware(request, response, next) {
+ const idsToDelete = request.body[idAttribute];
+ let instances;
+ try {
+ instances = await db.showIDs({ Model, ids: idsToDelete });
+ instances.forEach((instance) => {
+ parentIdAttributes.forEach((parentIdAttribute) => {
+ if (
+ instance.get(parentIdAttribute) !==
+ request.params[parentIdAttribute]
+ ) {
+ throw new Error("parent mismatch");
+ }
+ });
+ });
+ await Promise.all(instances.map((instance) => instance.destroy()));
+ } catch (error) {
+ debug(`${service.resource} destroy many error`);
+ debug(error);
+ return next(new Error(error));
+ }
+ response.locals.data = instances;
+ response.locals.success = true;
+ response.locals.id = idsToDelete;
+ next();
+ };
+}
+
+/**
+ * Insert a single record
+ */
+export function handleCreate({
+ idAttributes,
+ params,
+ columns,
+ body,
+ Model,
+ privateFields,
+ transaction,
+}) {
+ body = reduceValidColumns(body, columns, privateFields);
+ if (idAttributes) {
+ idAttributes.forEach((idAttribute) => {
+ if (idAttribute in params && idAttribute in columns) {
+ body[idAttribute] = parseInt(params[idAttribute]);
+ }
+ });
+ }
+ return db.create({ Model: Model, data: body, transaction });
+}
+
+/**
+ * Update a single record
+ */
+export function handleUpdate({
+ instance,
+ body,
+ user,
+ permission,
+ columns,
+ privateFields,
+ transaction,
+}) {
+ if (
+ permission === PERMISSIONS.ALLOW_FOR_OWNER &&
+ instance.get("user_id") !== user.user_id
+ ) {
+ throw new Error("PermissionsError");
+ }
+ body = reduceValidColumns(body, columns, privateFields);
+ return db.update({
+ instance,
+ data: body,
+ transaction,
+ });
+}
diff --git a/src/app/db/service/base/methods.js b/src/app/db/service/base/methods.js
new file mode 100644
index 0000000..be84d0c
--- /dev/null
+++ b/src/app/db/service/base/methods.js
@@ -0,0 +1,217 @@
+/**
+ * Service API methods
+ * @module app/db/service/base/methods
+ */
+
+import * as db from "app/db/query";
+import { reduceValidColumns } from "app/db/helpers";
+import { PERMISSIONS } from "app/constants";
+import debugModule from "debug";
+
+/**
+ * Export methods for dealing with multiple records
+ */
+export { showMany, updateMany, destroyMany } from "app/db/service/base/many";
+
+/**
+ * Debug logger
+ */
+const debug = debugModule("shoebox:service");
+
+/**
+ * API to filter and paginate lists of resources
+ */
+export function index(service) {
+ const { columns } = service;
+ return async function indexMiddleware(request, response, next) {
+ const { query: userQuery, params, permission, user } = request;
+ const { paginate } = service.options;
+ const queryBuilder = service.queryBuilder;
+ const query = {
+ ...userQuery,
+ ...params,
+ };
+ const withRelated = userQuery.related ? userQuery.related.split(",") : [];
+ if (permission === PERMISSIONS.ALLOW_FOR_OWNER) {
+ query.user_id = user.user_id;
+ }
+ let data, pagination;
+ let promises = [
+ db.index({
+ Model: service.Model,
+ query,
+ paginate,
+ user,
+ columns,
+ queryBuilder,
+ withRelated,
+ }),
+ ];
+ if (paginate) {
+ promises.push(
+ db.count({
+ Model: service.Model,
+ query,
+ paginate,
+ user,
+ columns,
+ queryBuilder,
+ })
+ );
+ } else {
+ promises.push(new Promise((resolve) => resolve({})));
+ }
+ try {
+ [data, pagination] = await Promise.all(promises);
+ } catch (error) {
+ debug(`${service.resource} Index error`);
+ debug(error);
+ console.error(error);
+ return next(error);
+ }
+ response.locals = {
+ data,
+ pagination,
+ query,
+ };
+ next();
+ };
+}
+
+/**
+ * API to query for a single record by ID
+ */
+export function show(service) {
+ return async function showMiddleware(request, response, next) {
+ const { user, permission, query } = request;
+ const withRelated = query.related ? query.related.split(",") : [];
+ let data;
+ try {
+ data = await db.show({
+ Model: service.Model,
+ objectID: parseInt(request.params.id),
+ withRelated,
+ });
+ } catch (error) {
+ debug(`${service.resource} Show error`);
+ debug(error);
+ return next(error);
+ }
+ if (!data) {
+ response.locals = {};
+ next();
+ } else if (
+ permission === PERMISSIONS.ALLOW_FOR_OWNER &&
+ data.get("user_id") !== user.user_id
+ ) {
+ next(new Error("PermissionsError"));
+ } else {
+ response.locals = { data };
+ next();
+ }
+ };
+}
+
+/**
+ * API to insert a new record
+ */
+export function create(service) {
+ return async function createMiddleware(request, response, next) {
+ const body = reduceValidColumns(
+ request.body,
+ service.columns,
+ service.options.privateFields
+ );
+ if (service.options.parent) {
+ service.idAttributes.forEach((idAttribute) => {
+ if (idAttribute in request.params && idAttribute in service.columns) {
+ body[idAttribute] = request.params[idAttribute];
+ }
+ });
+ }
+ let data;
+ try {
+ data = await db.create({ Model: service.Model, data: body });
+ } catch (error) {
+ debug(`${service.resource} create error`);
+ console.error(error);
+ return next(error);
+ }
+ response.locals = { data };
+ next();
+ };
+}
+
+/**
+ * API to update a single record
+ */
+export function update(service) {
+ return async function updateMiddleware(request, response, next) {
+ const { data: instance } = response.locals;
+ const { user, permission } = request;
+ if (
+ permission === PERMISSIONS.ALLOW_FOR_OWNER &&
+ instance.get("user_id") !== user.user_id
+ ) {
+ return next(new Error("PermissionsError"));
+ }
+ const body = reduceValidColumns(
+ request.body,
+ service.columns,
+ service.options.privateFields
+ );
+ let data;
+ try {
+ data = await db.update({
+ instance,
+ data: body,
+ });
+ } catch (error) {
+ debug(`${service.resource} update error`);
+ debug(error);
+ next(new Error(error));
+ }
+ response.locals = { data };
+ next();
+ };
+}
+
+/**
+ * API to sort records
+ */
+export function sort(service) {
+ return async function sortMiddleware(request, response, next) {
+ const { ids } = request.body;
+ const items = await db.showIDs({
+ Model: service.Model,
+ ids,
+ });
+ for (let item of items) {
+ item.set("sort_order", ids.indexOf(item.id));
+ }
+ await Promise.all(items.map((item) => item.save()));
+ response.locals.success = true;
+ next();
+ };
+}
+
+/**
+ * API to destroy a single record
+ */
+export function destroy(service) {
+ return async function destroyMiddleware(request, response, next) {
+ try {
+ await db.destroy({
+ Model: service.Model,
+ objectID: request.params.id,
+ });
+ } catch (error) {
+ debug(`${service.resource} destroy error`);
+ console.error(error);
+ return next(new Error(error));
+ }
+ response.locals.success = true;
+ response.locals.id = request.params.id;
+ next();
+ };
+}
diff --git a/src/app/db/service/base/response.js b/src/app/db/service/base/response.js
new file mode 100644
index 0000000..a4bac60
--- /dev/null
+++ b/src/app/db/service/base/response.js
@@ -0,0 +1,118 @@
+/**
+ * Service API responses
+ * @module app/db/service/base/response
+ */
+
+import { zipCSVs, stringifyCSV, sanitizeCSV } from "app/utils/file_utils";
+import debugModule from "debug";
+
+/**
+ * Debug logger
+ */
+const debug = debugModule("shoebox:service");
+
+/**
+ * Middleware to return the response as JSON or CSV
+ */
+export async function sendResponse(request, response) {
+ if (response.locals.csv) {
+ if (response.locals.csv.files) {
+ await sendCSVZipResponse(response);
+ } else {
+ await sendCSVResponse(response);
+ }
+ } else {
+ await sendJSONResponse(response);
+ }
+}
+
+/**
+ * Respond with a ZIP file containing multiple CSVs
+ * @param {Response} response the response object
+ */
+async function sendCSVZipResponse(response) {
+ const zipData = await zipCSVs(response.locals.csv.files);
+ response.set("Content-Type", "application/zip");
+ response.set(
+ "Content-Disposition",
+ `attachment; filename=${response.locals.csv.filename}`
+ );
+ response.set("Content-Length", zipData.length);
+ response.set("Access-Control-Expose-Headers", "Content-Disposition");
+ response.write(zipData, "binary");
+ response.end(null, "binary");
+}
+
+/**
+ * Respond with a single CSV
+ * @param {Response} response the response object
+ */
+async function sendCSVResponse(response) {
+ const csvData = await stringifyCSV(sanitizeCSV(response.locals.csv.data));
+ response.set("Content-Type", "text/csv; charset=utf-8");
+ response.set(
+ "Content-Disposition",
+ `attachment; filename=${response.locals.csv.filename}`
+ );
+ response.set("Content-Length", csvData.length);
+ response.set("Access-Control-Expose-Headers", "Content-Disposition");
+ response.send(csvData);
+}
+
+/**
+ * Respond with JSON
+ * @param {Response} response the response object
+ */
+async function sendJSONResponse(response) {
+ response.json(response.locals);
+}
+
+/**
+ * Error-handling middleware
+ * @param {Error} error an error object
+ * @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 handleError(error, request, response, next) {
+ const { errors } = request.service.options;
+ debug("Error", error.name, error.message);
+ let message;
+
+ if (errors) {
+ Object.keys(errors).some((key) => {
+ if (error.message.match(key)) {
+ message = errors[key];
+ return true;
+ }
+ return false;
+ });
+ if (message) {
+ response.status(403).send({ code: 403, error: message });
+ return;
+ }
+ }
+
+ if (error.name === "UnauthorizedError") {
+ debug("Unauthorized");
+ response.status(401).send({ code: 401, error: error.message });
+ } else if (error.message === "EmptyResponse") {
+ debug("Not found");
+ response.status(404).send({ code: 404, error: error.message });
+ } else if (error.message === "UserNotActive") {
+ debug("User not active");
+ response.status(401).send({ code: 401, error: error.message });
+ } else if (error.message === "PermissionsError") {
+ debug("Insufficient permissions");
+ response.status(401).send({ code: 401, error: error.message });
+ } else {
+ debug("Unexpected error");
+ // debug(error);
+ response.status(403).send({
+ code: 403,
+ error: error.name || error.message,
+ message: error.message,
+ });
+ next();
+ }
+}