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