controllers/DatabaseController.js

const PouchDB = require('pouchdb')
const fs = require('fs')

/**
 * Controller for {@link https://couchdb.apache.org/|CouchDB} (remote) and {@link https://pouchdb.com/|PouchDB} (local) databases
 *
 * @class
 * @extends Map
 */
class DatabaseController extends Map {
  /**
   * Initializes a new DatabaseController
   *
   * @param {Object} config configuration object
   * @param {Esdi} config.server Esdi server instance
   * @param {String} [config.dbAddress = 'local'] address of remote database, or `local` for local database
   * @param {String} [config.dbPort = 5984] port of remote database
   * @param {String} [config.dbNamespace = 'esdi_'] namespace prefix for database names
   * @memberof DatabaseController
   */
  init ({ server, dbAddress = 'local', dbPort = 5984, dbNamespace = 'esdi_' }) {
    this.server = server
    this.dbAddress = dbAddress
    this.dbPort = dbPort
    this.dbNamespace = dbNamespace

    if (!fs.existsSync('./data')) {
      fs.mkdirSync('./data')
    }
  }

  /**
   * Starts the DatabaseController
   *
   * @listens Esdi#start
   * @memberof DatabaseController
   */
  start () {
    console.log('[#] Starting DatabaseController...')

    this.server.models.forEach((model, name) => {
      const lowercaseName = name.toLowerCase()
      this.set(lowercaseName, this.loadDb(lowercaseName, this.dbAddress, this.dbPort))
    })
  }

  /**
   * Stops the DatabaseController
   *
   * @listens Esdi#stop
   * @memberof DatabaseController
   */
  stop () {
    console.log('[#] Stopping DatabaseController...')
    this.stopSyncing()
  }

  /**
   * Loads and syncs with a database
   *
   * @param {String} name database name
   * @param {String} [dbAddress='local'] remote database address, or `local` for local database
   * @param {Number} [port=5984] remote database port
   * @returns {Object}
   * @memberof DatabaseController
   */
  loadDb (name, dbAddress = 'local', port = 5984) {
    let DB
    if (dbAddress === 'local') {
      DB = new PouchDB(`data/${this.dbNamespace}${name}`)
      console.log(`=== Syncing local ${name} database...`)
    } else {
      DB = new PouchDB(`http://${process.env.DB_USERNAME}:${process.env.DB_PASSWORD}@${dbAddress}:${port}/${this.dbNamespace}${name}`)
      console.log(`=== Syncing remote ${name} database...`)
    }

    const changes = DB.changes({
      since: 'now',
      live: true,
      include_docs: true
    }).on('change', function (change) {
      if (change.doc._rev.split('-')[0] === '1') {
        console.log(`[~] "${change.id}" document in ${name} database created (rev-${change.doc._rev.split('-')[0]})`)
      } else {
        console.log(`[~] "${change.id}" document in ${name} database updated (rev-${change.doc._rev.split('-')[0]})`)
      }
    }).on('complete', function (info) {
      console.log(`=/= No longer syncing ${name} database!`)
    }).on('error', function (err) {
      console.log(err)
    })
    return {
      DB,
      changes
    }
  }

  /**
   * Fetches a document from a database
   *
   * @param {Object} config configuration object
   * @param {String} config.db database name
   * @param {String} config.id document `_id`
   * @param {Object} [config.options = {}] PouchDB config object
   * @returns {Object|Error}
   * @memberof DatabaseController
   */
  fetchDoc ({ db, id, options = {} }) {
    return this.get(db).DB
      .get(id, options)
      .then(data => { return data })
      .catch(er => { return er })
  }

  /**
   * Updates or creates a document in a database
   *
   * @param {Object} config configuration object
   * @param {String} config.db database name
   * @param {String} config.id document `_id`
   * @param {Object} config.payload payload for document
   * @memberof DatabaseController
   */
  updateDoc ({ db, id, payload }) {
    return this.fetchDoc({ db, id })
      .then(data => {
        if (data.status === 404 && data.reason === 'deleted') {
          return this.get(db).DB.put({
            _id: data.docId,
            ...payload
          })
            .then(() => { return this.fetchDoc({ db, id }) })
        }
        if (data.status === 404 && data.reason === 'missing') {
          return this.get(db).DB.put({
            _id: id,
            ...payload
          })
            .then(() => { return this.fetchDoc({ db, id }) })
        }

        // keep pre-existing data
        const dataPayload = { ...data }
        delete dataPayload._id
        delete dataPayload._rev

        return this.get(db).DB.put({
          _id: data._id,
          _rev: data._rev,
          ...dataPayload,
          ...payload
        })
          .then(() => { return this.fetchDoc({ db, id }) })
      })
      .catch(er => { return er })
  }

  /**
   * Stops syncing all databases
   *
   * @memberof DatabaseController
   */
  stopSyncing () {
    this.forEach((db) => {
      db.changes.cancel()
    })
  }
}

// factory
module.exports = new DatabaseController()