浏览代码

Redis -> couchdb

Craig Fletcher 4 年之前
父节点
当前提交
7727186cf6
共有 13 个文件被更改,包括 185 次插入49 次删除
  1. 7 1
      .env.example
  2. 1 0
      .eslintrc.cjs
  3. 2 1
      .gitignore
  4. 14 4
      README.md
  5. 2 0
      couchdb/config/10-single-node.ini
  6. 21 7
      docker-compose.yml
  7. 72 2
      package-lock.json
  8. 3 2
      package.json
  9. 16 9
      src/easy-api.js
  10. 10 3
      src/index.js
  11. 28 11
      src/modules/db.js
  12. 5 5
      src/routes/goodbye.js
  13. 4 4
      src/routes/hello.js

+ 7 - 1
.env.example

@@ -1 +1,7 @@
-REDIS_PASS=password123
+COUCHDB_USER=admin
+COUCHDB_PASSWORD=password
+COUCHDB_PROTOCOL=http
+COUCHDB_HOST=couchdb
+COUCHDB_PORT=5984
+APP_PORT=8080
+APP_HOST=0.0.0.0

+ 1 - 0
.eslintrc.cjs

@@ -14,5 +14,6 @@ module.exports = {
   },
   rules: {
     'import/extensions': 0,
+    'no-underscore-dangle': 0,
   },
 };

+ 2 - 1
.gitignore

@@ -1,4 +1,5 @@
 node_modules
 logs
-redis
+couchdb/data
+couchdb/config/docker.ini
 .env

+ 14 - 4
README.md

@@ -1,7 +1,7 @@
 # Easy API boilerplate
 
 Quick and dirty API boilerplate, using:
-  * Redis for the DB 
+  * CouchDB for the DB 
   * Yup for validation
   * Restify for the web server/routing
   * Winston for logging
@@ -23,19 +23,19 @@ start({
   host, 
   port,
   middlewares[],
-  redisOpts{}, 
+  couchDbOpts{}, 
   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:8080`. You'll need to rename
+"couchdb" 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
 
-Objects are stored in redis by calculating the sha256 hash, hex digest from a
+Objects are stored in couchdb by calculating the sha256 hash, hex digest from a
 "path" array for convenience (e.g. `["users", req.params.userName, "stats",
 "visits"]`).
 
@@ -43,6 +43,16 @@ There is no hardening or security intention behind this - it's simply a fast
 hash to use as an ID.
 
 
+## CouchDB options
+
+* protocol: protocol to access couchdb, e.g. http
+* host: couchdb host, e.g. couchdb.mydomain.com
+* port: couchdb port, e.g. 5984
+* dbs: array of DB names to connect to (created if they don't exist), e.g. ['visits']
+* username: couchdb username, e.g. admin
+* password: copuchbase password, e.g. password123
+
+
 ## Routes
 
 Routes are defined as simple factories, passed a db wrapper, yup and log instance as

+ 2 - 0
couchdb/config/10-single-node.ini

@@ -0,0 +1,2 @@
+[couchdb]
+single_node = true

+ 21 - 7
docker-compose.yml

@@ -2,20 +2,34 @@ version: '3.2'
 
 services:
 
-  redis:
-    image: "redis:alpine"
-    command: redis-server --requirepass ${REDIS_PASS}
+  couchdb:
+    image: couchdb
+    environment:
+      - COUCHDB_PASSWORD=${COUCHDB_PASSWORD}
+      - COUCHDB_USER=${COUCHDB_USER}
     expose:
-      - "6379"
+      - '5984'
+    ports:
+      - '5984:5984'
     volumes:
-      - ${PWD}/redis/data:/data
-      - ${PWD}/redis/conf:/usr/local/etc/redis/redis.conf
+      - ${PWD}/couchdb/data:/opt/couchdb/data
+      - ${PWD}/couchdb/config:/opt/couchdb/etc/local.d
   app:
     image: "node:lts-alpine"
     working_dir: /app
+    restart: unless-stopped
+    depends_on:
+      - couchdb
     environment:
       - NODE_ENV=production
-      - REDIS_PASS=${REDIS_PASS}
+      - COUCHDB_PASSWORD=${COUCHDB_PASSWORD}
+      - COUCHDB_USER=${COUCHDB_USER}
+      - COUCHDB_PROTOCOL=${COUCHDB_PROTOCOL}
+      - COUCHDB_HOST=${COUCHDB_HOST}
+      - COUCHDB_PORT=${COUCHDB_PORT}
+      - APP_PORT=${APP_PORT}
+      - APP_HOST=${APP_HOST}
+
     volumes:
       - ${PWD}:/app
     ports:

+ 72 - 2
package-lock.json

@@ -138,6 +138,11 @@
       "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.170.tgz",
       "integrity": "sha512-bpcvu/MKHHeYX+qeEN8GE7DIravODWdACVA1ctevD8CN24RhPZIKMn9ntfAsrvLfSX3cR5RrBKAbYm9bGs0A+Q=="
     },
+    "@types/tough-cookie": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.0.tgz",
+      "integrity": "sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A=="
+    },
     "acorn": {
       "version": "7.4.1",
       "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
@@ -245,6 +250,30 @@
       "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz",
       "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw=="
     },
+    "axios": {
+      "version": "0.21.1",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
+      "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==",
+      "requires": {
+        "follow-redirects": "^1.10.0"
+      }
+    },
+    "axios-cookiejar-support": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/axios-cookiejar-support/-/axios-cookiejar-support-1.0.1.tgz",
+      "integrity": "sha512-IZJxnAJ99XxiLqNeMOqrPbfR7fRyIfaoSLdPUf4AMQEGkH8URs0ghJK/xtqBsD+KsSr3pKl4DEQjCn834pHMig==",
+      "requires": {
+        "is-redirect": "^1.0.0",
+        "pify": "^5.0.0"
+      },
+      "dependencies": {
+        "pify": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz",
+          "integrity": "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA=="
+        }
+      }
+    },
     "balanced-match": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -1015,6 +1044,11 @@
       "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
       "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="
     },
+    "follow-redirects": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.1.tgz",
+      "integrity": "sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg=="
+    },
     "formidable": {
       "version": "1.2.2",
       "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.2.tgz",
@@ -1326,6 +1360,11 @@
       "integrity": "sha512-RU0lI/n95pMoUKu9v1BZP5MBcZuNSVJkMkAG2dJqC4z2GlkGUNeH68SuHuBKBD/XFe+LHZ+f9BKkLET60Niedw==",
       "dev": true
     },
+    "is-redirect": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz",
+      "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ="
+    },
     "is-regex": {
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.3.tgz",
@@ -1588,6 +1627,18 @@
       "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==",
       "optional": true
     },
+    "nano": {
+      "version": "9.0.3",
+      "resolved": "https://registry.npmjs.org/nano/-/nano-9.0.3.tgz",
+      "integrity": "sha512-NFI8+6q5ihnozH6qK+BJ+ilnPfZzBhlUswaFgqUvSp2EN5eJ2BMxbzkYiBsN+waa+N95FculCdbneDmzLWfXaQ==",
+      "requires": {
+        "@types/tough-cookie": "^4.0.0",
+        "axios": "^0.21.1",
+        "axios-cookiejar-support": "^1.0.1",
+        "qs": "^6.9.4",
+        "tough-cookie": "^4.0.0"
+      }
+    },
     "nanoclone": {
       "version": "0.2.1",
       "resolved": "https://registry.npmjs.org/nanoclone/-/nanoclone-0.2.1.tgz",
@@ -1849,11 +1900,15 @@
       "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.4.tgz",
       "integrity": "sha512-sFPkHQjVKheDNnPvotjQmm3KD3uk1fWKUN7CrpdbwmUx3CrG3QiM8QpTSimvig5vTXmTvjz7+TDvXOI9+4rkcg=="
     },
+    "psl": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz",
+      "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ=="
+    },
     "punycode": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
-      "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
-      "dev": true
+      "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
     },
     "qs": {
       "version": "6.10.1",
@@ -2416,6 +2471,16 @@
       "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
       "integrity": "sha1-riF2gXXRVZ1IvvNUILL0li8JwzA="
     },
+    "tough-cookie": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz",
+      "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==",
+      "requires": {
+        "psl": "^1.1.33",
+        "punycode": "^2.1.1",
+        "universalify": "^0.1.2"
+      }
+    },
     "triple-beam": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz",
@@ -2464,6 +2529,11 @@
         "which-boxed-primitive": "^1.0.2"
       }
     },
+    "universalify": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
+      "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="
+    },
     "uri-js": {
       "version": "4.4.1",
       "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",

+ 3 - 2
package.json

@@ -14,10 +14,11 @@
   "dependencies": {
     "app-root-path": "^3.0.0",
     "hasha": "^5.2.2",
+    "nano": "^9.0.3",
     "redis": "^3.1.2",
     "restify": "^8.5.1",
-    "yup": "^0.32.9",
-    "winston": "^3.3.3"
+    "winston": "^3.3.3",
+    "yup": "^0.32.9"
   },
   "devDependencies": {
     "eslint": "^7.27.0",

+ 16 - 9
src/easy-api.js

@@ -1,13 +1,13 @@
 import restify from 'restify';
-import redis from 'redis';
+import nano from 'nano';
 import yup from 'yup';
 
 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 });
+function setupRoute(routeFactory, dbs, server, log) {
+  const route = routeFactory({ dbs, yup, log });
   const handlers = route.schema ? [validationFactory(route, log), ...route.handlers] : route.handlers;
 
   switch (route.verb) {
@@ -22,26 +22,33 @@ function setupRoute(routeFactory, db, server, log) {
   }
 }
 
-function start({ routes, host, port, middlewares, redisOpts, restifyOpts }) {
-  const client = redis.createClient(redisOpts);
+async function connectDbs(client, dbs) {
+  return dbs.reduce(async (wrappedDbs, dbName) => ({ ...wrappedDbs, [dbName]: await dbWrapper(client, dbName) }), {});
+}
+
+async function start({ routes, host, port, middlewares, couchDbOpts, restifyOpts }) {
+  try {
+    const connStr = `${couchDbOpts.protocol}://${couchDbOpts.username}:${couchDbOpts.password}@${couchDbOpts.host}:${couchDbOpts.port}`;
+    const client = await nano(connStr);
+    const dbs = await connectDbs(client, couchDbOpts.dbs);
 
-  client.on('connect', () => {
     logger.info('connected to db, starting server...');
     const server = restify.createServer(restifyOpts);
-    const db = dbWrapper(client);
 
     middlewares.forEach(middlewareFactory => {
       server.use(middlewareFactory({ server, restify }));
     });
 
     routes.forEach(routeFactory => {
-      setupRoute(routeFactory, db, server, logger);
+      setupRoute(routeFactory, dbs, server, logger);
     });
 
     server.listen(port, host, () => {
       logger.info(`${server.name} listening at ${server.url}`);
     });
-  });
+  } catch (e) {
+    throw new Error(e);
+  }
 }
 
 export default start;

+ 10 - 3
src/index.js

@@ -9,8 +9,15 @@ start({
     ({ 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 },
+  host: env.APP_HOST,
+  port: env.APP_PORT,
+  couchDbOpts: {
+    protocol: env.COUCHDB_PROTOCOL,
+    host: env.COUCHDB_HOST,
+    port: env.COUCHDB_PORT,
+    dbs: ['visits'],
+    username: env.COUCHDB_USER,
+    password: env.COUCHDB_PASSWORD,
+  },
   restifyOpts: { name: 'example-easy-api' },
 });

+ 28 - 11
src/modules/db.js

@@ -4,26 +4,43 @@ function getLeafId(nodePath) {
   return hasha(nodePath.join('#'), { algorithm: 'sha256', encoding: 'hex' });
 }
 
-function dbWrapper(client) {
+async function dbWrapper(client, dbName) {
+  try {
+    await client.db.get(dbName);
+  } catch (e) {
+    await client.db.create(dbName);
+  }
+  const db = await client.use(dbName);
   return {
-    get(nodePath, callback) {
+    get: async function get(nodePath, callback) {
       const contentId = getLeafId(nodePath);
-      client.get(contentId, (err, reply) => {
+      db.get(contentId, (err, reply) => {
         if (err) {
-          console.log(err);
-          throw new Error();
+          callback(null);
         } else {
           callback(reply);
         }
       });
     },
-    set(nodePath, data, callback) {
+    set: async function set(nodePath, data, callback) {
       const contentId = getLeafId(nodePath);
-      client.set(contentId, data, (err, reply) => {
-        if (err) {
-          throw new Error();
-        } else if (callback) {
-          callback(reply);
+      db.get(contentId, (getError, existingDoc) => {
+        if (getError) {
+          db.insert({ ...data, _id: contentId }, (insertNewError, reply) => {
+            if (insertNewError) {
+              throw new Error(insertNewError);
+            } else if (callback) {
+              callback(reply);
+            }
+          });
+        } else {
+          db.insert({ ...data, _id: contentId, _rev: existingDoc._rev }, (updateError, reply) => {
+            if (updateError) {
+              throw new Error(updateError);
+            } else if (callback) {
+              callback(reply);
+            }
+          });
         }
       });
     },

+ 5 - 5
src/routes/goodbye.js

@@ -1,16 +1,16 @@
-function routeFactory({ db, yup, log }) {
+function routeFactory({ dbs, yup, log }) {
   return {
     verb: 'post',
     path: '/goodbye',
     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}.`);
+        dbs.visits.get(contentPath, reply => {
+          const visits = reply && reply.visits ? Number(reply.visits) + 1 : 1;
+          log.info(`Visited by ${req.params.name}.`);
           res.send(`hello ${req.body.name}. You have visited ${visits} times.`);
           next();
-          db.set(contentPath, visits);
+          dbs.visits.set(contentPath, { visits });
         });
       },
     ],

+ 4 - 4
src/routes/hello.js

@@ -1,16 +1,16 @@
-function routeFactory({ db, yup, log }) {
+function routeFactory({ dbs, yup, log }) {
   return {
     verb: 'get',
     path: '/hello/:name',
     handlers: [
       function handler(req, res, next) {
         const contentPath = ['visits', req.params.name];
-        db.get(contentPath, reply => {
-          const visits = reply >= 0 ? Number(reply) + 1 : 1;
+        dbs.visits.get(contentPath, reply => {
+          const visits = reply && reply.visits ? Number(reply.visits) + 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);
+          dbs.visits.set(contentPath, { visits });
         });
       },
     ],