Эх сурвалжийг харах

Add middlewares, multiple handlers

Craig Fletcher 4 жил өмнө
parent
commit
4c9c8d3549

+ 23 - 10
README.md

@@ -10,26 +10,27 @@ Quick and dirty API boilerplate, using:
 There are many limitations to this configuration, it is intended as a
 debugging/prototyping tool.
 
-There are fairly few dependencies and most things are
-simply factories with instances passed in.
+There are fairly few npm modules, not a lot of code, and most things are
+simply factories with dependencies passed in.
 
 
 ## Usage
 
-Import start from easy-api.js, call like so:
+Import `start` from `easy-api.js`, call like so:
 ```
 start({
   routes[], 
   host, 
-  port, 
+  port,
+  middlewares[],
   redisOpts{}, 
   restifyOpts{}
 })
 ```
 
 The included example uses docker and docker-compose, so the db host is set to
-"redis" and the restify server listens on 0.0.0.0. You'll need to rename
-.env.example to .env and change the password.
+"redis" and the restify server listens on `0.0.0.0:8080`. You'll need to rename
+`.env.example` to `.env` and change the password.
 
 
 ## Storage
@@ -45,18 +46,30 @@ hash to use as an ID.
 ## Routes
 
 Routes are defined as simple factories, passed a db wrapper, yup and log instance as
-an object ({db, yup, log}) and expected to return:
+an object (`{db, yup, log}`) and expected to return:
   * verb: HTTP verb, e.g. `get`
   * path: restify path string, e.g. `/api/hello/:name`
-  * handler: restify handler function for route
+  * handlers: array of restify handler functions for route
   * schema (optional): yup schema to validate against
 
 
 ## Validation
 
-If a route passes a schema, the request will be validated against it:
+If a route passes a schema, the request will be validated against it before any
+other handlers are called.
+
   * get requests validate against `req.params`
   * post requests validate against `req.body`
 
-When validation fails, a HTTP 400 is returned and the handler is not
+When validation fails, a HTTP 400 is returned and the remaining handlers are not
 processed.
+
+
+## Middlewares
+
+Middlewares are defined as factories, returning a restify-compatible middleware.
+They are passed the server instance and restify, like so:
+```
+const middlewareToBeApplied = middlewareFactory({server, restify});
+server.use(middlewareToBeApplied);
+```

+ 15 - 41
package-lock.json

@@ -61,7 +61,6 @@
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.2.tgz",
       "integrity": "sha512-+A1YivoVDNNVCdfozHSR8v/jyuuLTMXwjWuxPFlFlUapXoGc+Gj9mDlTDDfrwl7rXCl2tNZ0kE8sIBO6YOn96Q==",
-      "dev": true,
       "requires": {
         "colorspace": "1.1.x",
         "enabled": "2.0.x",
@@ -187,8 +186,7 @@
     "app-root-path": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.0.0.tgz",
-      "integrity": "sha512-qMcx+Gy2UZynHjOHOIXPNvpf+9cjvk3cWrBBK7zg4gH9+clobJRb9NGzcT7mQTcV/6Gm/1WelUtqxVXnNlrwcw==",
-      "dev": true
+      "integrity": "sha512-qMcx+Gy2UZynHjOHOIXPNvpf+9cjvk3cWrBBK7zg4gH9+clobJRb9NGzcT7mQTcV/6Gm/1WelUtqxVXnNlrwcw=="
     },
     "argparse": {
       "version": "1.0.10",
@@ -245,8 +243,7 @@
     "async": {
       "version": "3.2.0",
       "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz",
-      "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==",
-      "dev": true
+      "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw=="
     },
     "balanced-match": {
       "version": "1.0.2",
@@ -351,7 +348,6 @@
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/color/-/color-3.0.0.tgz",
       "integrity": "sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w==",
-      "dev": true,
       "requires": {
         "color-convert": "^1.9.1",
         "color-string": "^1.5.2"
@@ -361,7 +357,6 @@
       "version": "1.9.3",
       "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
       "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
-      "dev": true,
       "requires": {
         "color-name": "1.1.3"
       }
@@ -369,14 +364,12 @@
     "color-name": {
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
-      "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
-      "dev": true
+      "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
     },
     "color-string": {
       "version": "1.5.5",
       "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.5.tgz",
       "integrity": "sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg==",
-      "dev": true,
       "requires": {
         "color-name": "^1.0.0",
         "simple-swizzle": "^0.2.2"
@@ -385,14 +378,12 @@
     "colors": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
-      "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==",
-      "dev": true
+      "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA=="
     },
     "colorspace": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.2.tgz",
       "integrity": "sha512-vt+OoIP2d76xLhjwbBaucYlNSpPsrJWPlBTtwCpQKIu6/CSMutyzX93O/Do0qzpH3YoHEes8YEFXyZ797rEhzQ==",
-      "dev": true,
       "requires": {
         "color": "3.0.x",
         "text-hex": "1.0.x"
@@ -543,8 +534,7 @@
     "enabled": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz",
-      "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==",
-      "dev": true
+      "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="
     },
     "encodeurl": {
       "version": "1.0.2",
@@ -944,14 +934,12 @@
     "fast-safe-stringify": {
       "version": "2.0.7",
       "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz",
-      "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==",
-      "dev": true
+      "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA=="
     },
     "fecha": {
       "version": "4.2.1",
       "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.1.tgz",
-      "integrity": "sha512-MMMQ0ludy/nBs1/o0zVOiKTpG7qMbonKUzjJgQFEuvq6INZ1OraKPRAWkBq5vlKLOUMpmNYG1JoN3oDPUQ9m3Q==",
-      "dev": true
+      "integrity": "sha512-MMMQ0ludy/nBs1/o0zVOiKTpG7qMbonKUzjJgQFEuvq6INZ1OraKPRAWkBq5vlKLOUMpmNYG1JoN3oDPUQ9m3Q=="
     },
     "file-entry-cache": {
       "version": "6.0.1",
@@ -1025,8 +1013,7 @@
     "fn.name": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
-      "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==",
-      "dev": true
+      "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="
     },
     "formidable": {
       "version": "1.2.2",
@@ -1447,8 +1434,7 @@
     "kuler": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz",
-      "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==",
-      "dev": true
+      "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="
     },
     "levn": {
       "version": "0.4.1",
@@ -1514,7 +1500,6 @@
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/logform/-/logform-2.2.0.tgz",
       "integrity": "sha512-N0qPlqfypFx7UHNn4B3lzS/b0uLqt2hmuoa+PpuXNYgozdJYAyauF5Ky0BWVjrxDlMWiT3qN4zPq3vVAfZy7Yg==",
-      "dev": true,
       "requires": {
         "colors": "^1.2.1",
         "fast-safe-stringify": "^2.0.4",
@@ -1526,8 +1511,7 @@
         "ms": {
           "version": "2.1.3",
           "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
-          "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
-          "dev": true
+          "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
         }
       }
     },
@@ -1718,7 +1702,6 @@
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz",
       "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==",
-      "dev": true,
       "requires": {
         "fn.name": "1.x.x"
       }
@@ -2132,7 +2115,6 @@
       "version": "0.2.2",
       "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
       "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=",
-      "dev": true,
       "requires": {
         "is-arrayish": "^0.3.1"
       },
@@ -2140,8 +2122,7 @@
         "is-arrayish": {
           "version": "0.3.2",
           "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
-          "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
-          "dev": true
+          "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
         }
       }
     },
@@ -2294,8 +2275,7 @@
     "stack-trace": {
       "version": "0.0.10",
       "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
-      "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=",
-      "dev": true
+      "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA="
     },
     "statuses": {
       "version": "1.4.0",
@@ -2423,8 +2403,7 @@
     "text-hex": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
-      "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==",
-      "dev": true
+      "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="
     },
     "text-table": {
       "version": "0.2.0",
@@ -2440,8 +2419,7 @@
     "triple-beam": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz",
-      "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==",
-      "dev": true
+      "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw=="
     },
     "tsconfig-paths": {
       "version": "3.9.0",
@@ -2573,7 +2551,6 @@
       "version": "3.3.3",
       "resolved": "https://registry.npmjs.org/winston/-/winston-3.3.3.tgz",
       "integrity": "sha512-oEXTISQnC8VlSAKf1KYSSd7J6IWuRPQqDdo8eoRNaYKLvwSb5+79Z3Yi1lrl6KDpU6/VWaxpakDAtb1oQ4n9aw==",
-      "dev": true,
       "requires": {
         "@dabh/diagnostics": "^2.0.2",
         "async": "^3.1.0",
@@ -2590,7 +2567,6 @@
       "version": "4.4.0",
       "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.4.0.tgz",
       "integrity": "sha512-Lc7/p3GtqtqPBYYtS6KCN3c77/2QCev51DvcJKbkFPQNoj1sinkGwLGFDxkXY9J6p9+EPnYs+D90uwbnaiURTw==",
-      "dev": true,
       "requires": {
         "readable-stream": "^2.3.7",
         "triple-beam": "^1.2.0"
@@ -2600,7 +2576,6 @@
           "version": "2.3.7",
           "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
           "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
-          "dev": true,
           "requires": {
             "core-util-is": "~1.0.0",
             "inherits": "~2.0.3",
@@ -2614,8 +2589,7 @@
         "safe-buffer": {
           "version": "5.1.2",
           "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
-          "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
-          "dev": true
+          "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
         }
       }
     },

+ 17 - 26
src/easy-api.js

@@ -2,46 +2,37 @@ import restify from 'restify';
 import redis from 'redis';
 import yup from 'yup';
 
-import { checkReqBody, checkParams } from './modules/check-schema.js';
+import validationFactory from './modules/validations.js';
 import dbWrapper from './modules/db.js';
 import logger from './modules/logging.js';
 
 function setupRoute(routeFactory, db, server, log) {
   const route = routeFactory({ db, yup, log });
-
-  if (route.schema) {
-    switch (route.verb) {
-      case 'get':
-        server.get(route.path, checkParams(route.schema, log, route.path), route.handler);
-        break;
-      case 'post':
-        server.post(route.path, checkReqBody(route.schema, log, route.path), route.handler);
-        break;
-      default:
-        throw new Error();
-    }
-  } else {
-    switch (route.verb) {
-      case 'get':
-        server.get(route.path, route.handler);
-        break;
-      case 'post':
-        server.post(route.path, route.handler);
-        break;
-      default:
-        throw new Error();
-    }
+  const handlers = route.schema ? [validationFactory(route, log), ...route.handlers] : route.handlers;
+
+  switch (route.verb) {
+    case 'get':
+      server.get(route.path, ...handlers);
+      break;
+    case 'post':
+      server.post(route.path, ...handlers);
+      break;
+    default:
+      throw new Error();
   }
 }
 
-function start({ routes, host, port, redisOpts, restifyOpts }) {
+function start({ routes, host, port, middlewares, redisOpts, restifyOpts }) {
   const client = redis.createClient(redisOpts);
 
   client.on('connect', () => {
     logger.info('connected to db, starting server...');
     const server = restify.createServer(restifyOpts);
     const db = dbWrapper(client);
-    server.use(restify.plugins.bodyParser());
+
+    middlewares.forEach(middlewareFactory => {
+      server.use(middlewareFactory({ server, restify }));
+    });
 
     routes.forEach(routeFactory => {
       setupRoute(routeFactory, db, server, logger);

+ 5 - 0
src/index.js

@@ -4,6 +4,11 @@ import routeFactories from './routes/index.js';
 
 start({
   routes: routeFactories,
+  middlewares: [
+    ({ restify }) => restify.plugins.bodyParser(),
+    ({ restify }) => restify.plugins.gzipResponse(),
+    ({ server, restify }) => restify.plugins.acceptParser(server.acceptable),
+  ],
   host: '0.0.0.0',
   port: 8080,
   redisOpts: { host: 'redis', password: env.REDIS_PASS },

+ 15 - 0
src/modules/check-schema.js → src/modules/validations.js

@@ -5,6 +5,7 @@ export function checkReqBody(schema, log, path) {
         next();
       } else {
         res.send(400, { error: 'Bad request' });
+        next(false);
         log.debug(`Invalid request for route: ${path}, ${JSON.stringify(req.params)}`);
       }
     });
@@ -18,8 +19,22 @@ export function checkParams(schema, log, path) {
         next();
       } else {
         res.send(400, { error: 'Bad request' });
+        next(false);
         log.debug(`Invalid request for route: ${path}, ${JSON.stringify(req.params)}`);
       }
     });
   };
 }
+
+function validationFactory(route, log) {
+  switch (route.verb) {
+    case 'get':
+      return checkParams(route.schema, log, route.path);
+    case 'post':
+      return checkReqBody(route.schema, log, route.path);
+    default:
+      throw new Error();
+  }
+}
+
+export default validationFactory;

+ 12 - 10
src/routes/goodbye.js

@@ -2,16 +2,18 @@ function routeFactory({ db, yup, log }) {
   return {
     verb: 'post',
     path: '/goodbye',
-    handler(req, res, next) {
-      const contentPath = ['visits', req.body.name];
-      db.get(contentPath, reply => {
-        const visits = reply >= 0 ? Number(reply) + 1 : 1;
-        log.info(`Visited by ${req.body.name}.`);
-        res.send(`hello ${req.body.name}. You have visited ${visits} times.`);
-        next();
-        db.set(contentPath, visits);
-      });
-    },
+    handlers: [
+      function handler(req, res, next) {
+        const contentPath = ['visits', req.body.name];
+        db.get(contentPath, reply => {
+          const visits = reply >= 0 ? Number(reply) + 1 : 1;
+          log.info(`Visited by ${req.body.name}.`);
+          res.send(`hello ${req.body.name}. You have visited ${visits} times.`);
+          next();
+          db.set(contentPath, visits);
+        });
+      },
+    ],
     schema: yup.object().shape({
       name: yup.string().required(),
     }),

+ 12 - 10
src/routes/hello.js

@@ -2,16 +2,18 @@ function routeFactory({ db, yup, log }) {
   return {
     verb: 'get',
     path: '/hello/:name',
-    handler(req, res, next) {
-      const contentPath = ['visits', req.params.name];
-      db.get(contentPath, reply => {
-        const visits = reply >= 0 ? Number(reply) + 1 : 1;
-        log.info(`Visited by ${req.params.name}.`);
-        res.send(`hello ${req.params.name}. You have visited ${visits} times.`);
-        next();
-        db.set(contentPath, visits);
-      });
-    },
+    handlers: [
+      function handler(req, res, next) {
+        const contentPath = ['visits', req.params.name];
+        db.get(contentPath, reply => {
+          const visits = reply >= 0 ? Number(reply) + 1 : 1;
+          log.info(`Visited by ${req.params.name}.`);
+          res.send(`hello ${req.params.name}. You have visited ${visits} times.`);
+          next();
+          db.set(contentPath, visits);
+        });
+      },
+    ],
     schema: yup.object().shape({
       name: yup.string().required(),
     }),