summaryrefslogtreecommitdiff
path: root/src/app/db/service/base/index.js
blob: 188d86a929137710a578c094531c0612b87d973f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
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;
}