Source: database.js

/**
- Export constructor to create instance of Database

@exports database
@author kevin leptons <kevin.leptons@gmail.com>
@example
const database = require('./lib/database')

var url = 'mongodb://localhost/gwisp'
var db = database(url)
*/
module.exports = function (url, username, password, logger) {
  return new Database(url, username, password, logger)
}

const async = require('async')
const mongodb = require('mongodb')

const flatlog = require('./flatlog')

const MongoClient = mongodb.MongoClient

/**
- Default logger use for database if logger parameter is not specify

@memberof module:database
@const {module:flatlog.FlatLog} DEFAULT_LOGGER
*/
const DEFAULT_LOGGER = flatlog({caller: 'database'})

/**
Path to directory contains an sample data of database.
It must contains files below

- region-iso-639.csv

This file contains region code follow iso 639. Database will use alpha-2 code.
For quickly, simply is copy from source code file
[asset/sample-data/region-iso-639.csv](sample-data/region-iso-639.csv)

- scheduler-001.json

This file contains an instance of scheduler. It must real scheduler. That mean
people can use them. see document for
{@link module:collection.SCHEDULER_COL}
for detail of an scheduler object. If have not idea to change that, copy
from source code in
[asset/sample-data/scheduler-001.json](sample-data/scheduler-001.json)

- scheduler-random.json

This file contains list of schedulers. It is generated by tools and have
about more than 100 elements. That data use for test purpose. If have not
test, create file with empty array. If have not tools to generate, copy it
from source code in
[asset/sample-data/scheduler-random.json](sample-data/scheduler-random.json)

@memberof module:database
@typedef {string} AssetDir
@example
var assetDir = 'asset/sample-data'
*/

/**
- Create a new Database from specify
- It is wrap of MongoClient
- All of operations with database must pass though this object to ensure that
  every thing is fine
- Do not use mongodb.MongoClient directly

@constructor
@memberof module:database
@class Database
@param {string} url - Specify address of mongodb server
@param {string} [username=null] - Account to access to mongodb server
@param {string} [password=null] - Credential to access to mongodb server
@param {module:flatlog.FlatLog} [logger=DEFAULT_LOGGER] - Use to make write
  message uniform
@return {Database} - The new database object
@example

// default database
var db1 = new Database('mongodb://localhost/gwisp')

// access database by credential
var db2 = new Database('mongodb://localhost/gwisp', 'kevin', 'secret')

// custom logger
const flatlog = require('./lib/flatlog')

var logger = flatlog({caller 'kevin'})
var db3 = new Database('mongodb://localhost/gwisp', null, null, logger)
*/
function Database (url, username, password, logger) {
  this._url = url
  this._username = username
  this._password = password
  this._logger = logger || DEFAULT_LOGGER
  this._dbclient = null
}

// Shortcut to database prototype
const prototype = Database.prototype

/**
- Async connect to database

@memberof module:database.Database
@param {StdCallback} callback - Function will be call after done
@example
// see constructor for detail
var db = new Database(url)

db.connect(function(err, dbclient) {
  if (err) return

  // do some thing with database here
})
*/
prototype.connect = function (callback) {
  const self = this
  MongoClient.connect(this._url, function (err, db) {
    if (err) return callback(err)

    self._dbclient = db
    callback(err, db)
  })
}

/**
- Close connection to database

@memberof module:database.Database
@param {StdCallback} callback - Function will be call after done
@example
// see constructor for detail
var db = new Database(url)

db.connect(function(err, dbclient) {
  if (err) return

  db.close(function(err) {
    if (err) return

    // connection to database server was close
  })
})
*/
prototype.close = function (callback) {
  const self = this

  if (!self._dbclient) return callback(null)

  self._dbclient.close(true, function (err) {
    if (err) return callback(err)
    self._dbclient = null

    callback(null)
  })
}

/**
- Setup new database
- If database is not exists, create new database then push sample data to it
- If database is early exists, drop database then push sample data to it

@memberof module:database.Database
@param {module:database.AssetDir} assetDir - Path to directory contains static data
@param {StdCallback} callback - Function will be call after done
@example
// see constructor for detail
var db = new Database(url)

db.setup('asset/static', function(err) {
  // handle for result of setup here
})
*/
prototype.setup = function (assetDir, callback) {
  const self = this

  self._logger.info('drop database')
  self.drop(function (err) {
    if (err) {
      self._logger.error('drop database')
      return callback(err)
    }

    self.connect(function (err, db) {
      if (err) return callback(err)
      self._createCollections(require('./collections'), function (err) {
        if (err) {
          console.log(err);
          self._logger.error('create collections')
          return callback(err)
        }

        callback()
      })
    })
  })
}

/**
- Drop database on server then close connection

@memberof module:database.Database
@param {StdCallback} callback - Function will be call after done
@example
// see constructor for detail
var db = new Database(url)

db.drop(function(err) {
  // handle for result of drop here
})
*/
prototype.drop = function (callback) {
  const self = this

  self.connect(function (err, db) {
    if (err) return callback(err)

    db.dropDatabase(function (err, res) {
      if (err) return callback(err)

      self.close(function (err) {
        if (err) return callback(err)

        self._dbclient = null
        callback(null)
      })
    })
  })
}

/**
- Specification of collection
- It is defined by mongodb
- This is `option` parameter of `db.createCollection`

@memberof module:database
@typedef {object} CollectionSchema
@see {@link https://docs.mongodb.com/manual/reference/method/db.createCollection}
@example
var schema = {
  autoIndexId: false,
  validator: {
    $and: [
      {id: {$type: 'string', $regex: /^[a-z]{2}$/}},
      {name: {$type: 'string', $regex: /^[a-zA-Z0-9 ]{1,16}$/}}
    ]
  }
}
*/

/**
- Specify indexing of collection
- It is defined by mongodb
- This wrap `keys`, `options` parameters of `db.collection.createIndex`
  into object {keys: keys, options: options}

@memberof module:database
@typedef {object} CollectionIndex
@see {@link https://docs.mongodb.com/manual/reference/method/db.collection.createIndex}
@example
var index = {
  keys: {id: 1},
  options: {unique: true}
}
*/

/**
- Create collection in database from specification
- Create indexing of collection in database from specification

@memberof module:database.Database
@param {string} name - Name of collection
@param {module:database.CollectionSchema} schema - Specify field of collection
@param {module:database.CollectionIndex} indexing - Specify indexing of collection
@param {StdCallback} callback
- Function will be call after done
- Result contains collection object
@example
// see constructor for detail
var db = new Database(url)

var schema = {
  autoIndexId: false,
  validator: {
    $and: [
      {id: {$type: 'string', $regex: /^[a-z]{2}$/}},
      {name: {$type: 'string', $regex: /^[a-zA-Z0-9 ]{1,16}$/}}
    ]
  }
}
var index = {
  keys: {id: 1},
  options: {unique: true}
}

db.createCollection('region', schema, index, function(err, collection) {
  // handle for result of operation here
})
*/
prototype._createCollection = function (name, schema, indexing, callback) {
  const self = this

  self._dbclient.createCollection(name, schema, function (err, collection) {
    if (err) return callback(err)

    collection.createIndex(indexing.keys, indexing.options, function (err) {
      callback(err)
    })
  })
}

/**
@memberof module:database
@typedef {object} CollectionSpec
@property {string} name - Name of collection
@property {module:database.CollectionSchema} schema - Specification of field of collection
@property {module:database.CollectionIndex} indexing - Specification of indexing of collection
*/

/**
- Create multi collection from array of specification

@memberof module:database.Database
@param {module:database.CollectionSpec[]} collections - List of specifications
@param {StdCallback} callback - Function will be call after done
@example
// see constructor for detail
var db = new Database(url)

var collections = [
  {
    name: 'region',
    schema: {
      autoIndexId: false,
      validator: {
        $and: [
          {id: {$type: 'string', $regex: /^[a-z]{2}$/}},
          {name: {$type: 'string', $regex: /^[a-zA-Z0-9 ]{1,16}$/}}
        ]
      }
    },
    index: {
      keys: {id: 1},
      options: {unique: true}
    }
  },
  {
    name: 'book',
    schema: {
      validator: {
        $and: [
          {name: {$type: 'string', $regex: /^[a-zA-Z0-9 ]{1,16}$/}},
          {price: {$type: 'number'}}
        ]
      }
    },
    index: {
      keys: {name: 1},
      options: {unique: true}
    }
  }
]

db._createCollections(collections, function(err) {
  // handle for result here
})
*/
prototype._createCollections = function (collections, callback) {
  const self = this
  const calls = []

  self._logger.info('create collections')
  collections.forEach(function (item) {
    calls.push(function (callback) {
      self._createCollection(item.name, item.schema, item.indexing, callback)
    })
  })

  async.series(calls, function (err) {
    // todo: push sample data to database
    self._logger.info('push sample data to database')

    callback(err)
  })
}