bouncer_db_zango-db.js

'use strict'

const IDb = require('../idb')
const Hoek = require('@hapi/hoek')
const zango = require('zangodb')
const reach = require('../../utils/reach')
const ObjectId = require('bson-objectid')
const uuidv4 = require('uuid/v4')

const MongoQuery = require('../mongo-query')
const { promisfy } = require('promisfy')
const debug = require('debug')('bouncer.db.zango-db')

/**
 * Ideal for frontend apps with large datasets (larger then total RAM). This is an IndexedDb based driver so it span to nearly 1/3 of total system storage spave available to the browser/app.
 * 
* @class  module:Db.ZangoDb
* @extends {module:Db.IDb}
* @link module:Db
* @see module:Party.ZangoParty
*/
module.exports = class ZangoDb extends IDb {

  constructor ({dbname, factory, useUuid}) {
    super(factory)
    debug('constructor')
    this.zango = null
    this.dbname = dbname
    this.error = null
    this.useUuid = (useUuid==undefined) ? true : useUuid
  }


  async start(){

    debug('starting')


    let collectionSettings = {}

    for(const name of this.factory.getValidators()){
      debug('creating collection', name)

      const indexSettings = reach(this.factory, 'schemas.IndexSettings.'+name)

      const indices = ['$meta.id'].concat(indexSettings.unique).concat(indexSettings.indices)

      collectionSettings[this.prefix+name] = indices.length > 0 ? indices : true
    }

    debug('dbname',this.dbname, collectionSettings)

    this.zango = new zango.Db(this.dbname, collectionSettings)
    
  }

  async getCollectionNames(){

    const names = this.factory.getValidators()

    return names.map(name=>{return name.replace(this.prefix,'')})
  }


  async getCollection(name){ 
    let collection = this.zango.collection(this.prefix+name)

    return collection
  }

  /** convert db documnet to plain object with $meta field */
  documentToObject(doc){
    let obj = Object.assign({},doc)
    obj.$meta = {
      id: Hoek.reach(obj,'meta.id', {default: obj._id}),
      type: Hoek.reach(doc,'meta.type'),
      created: Hoek.reach(obj,'meta.created', {default: (new Date()).toISOString()}),
      revision: Hoek.reach(obj,'meta.revision', {default: 1}),
      removed: Hoek.reach(obj,'meta.removed')
    }
    
    delete obj.meta
    delete obj._id

    return obj
  }


  /** convert object with $meta field to db representation*/
  documentFromObject(obj){ 
    let doc = Object.assign({},obj)
    doc._id = Hoek.reach(obj,'$meta.id', {default: obj._id}),
    doc.meta = {
      id: Hoek.reach(obj,'$meta.id', {default: obj._id}),
      type: Hoek.reach(obj,'$meta.type'),
      created: Hoek.reach(obj,'$meta.created', {default: (new Date()).toISOString()}),
      revision: Hoek.reach(obj,'$meta.revision', {default: 1}),
      removed: Hoek.reach(obj,'$meta.removed')
    }
    

    delete doc.$meta

    return doc
  }

  ensureId(obj){
    let temp = {...obj}
    if(!reach(temp,'$meta.id')){

      if(this.useUuid){
        temp.$meta.id = uuidv4()
      }
      else{
        temp.$meta.id = (new ObjectId()).toHexString()
      }
    }

    let dbDoc = this.documentFromObject(temp)
    
    return dbDoc
  }

  async find(collectionName, mongoQuery){

    let query = mongoQuery.getQueryDoc()

    debug('find collection=', collectionName, ' query=', JSON.stringify(query,null,2))
    let collection = await this.getCollection(collectionName)
    let resultSet = collection.find(query)


    if(mongoQuery.hasLimit()){
      resultSet = resultSet.limit(mongoQuery.getLimit())
    }

    if(mongoQuery.hasSort()){
      resultSet = resultSet.sort( mongoQuery.getSort() )
    }

    return (await resultSet.toArray()).map(this.documentToObject) || []
  }

  async insertMany(collectionName, docs){ 
    debug('insert collection=', collectionName, ' docs=', JSON.stringify(docs,null,2))
    let collection = await this.getCollection(collectionName)

    let resultSet = []

    for(let obj of docs){
      let temp = {...obj}
      if(temp._id===undefined){ temp._id = (new ObjectId()).toString(); temp.$meta.id=temp._id;  }

      let dbDoc = this.documentFromObject(temp)

      const stripped = this.stripMeta(temp)

      debug('validating', stripped,'from', temp)

      await this.factory.validate(collectionName, stripped)

      debug('its good, inserting', dbDoc)

      await collection.insert( dbDoc )

      debug('inserted', dbDoc)

      const finalObj = this.documentToObject(dbDoc)

      debug('returning', finalObj)

      this.emitChange(finalObj, 'create')

      resultSet.push(finalObj)
    }


    return resultSet

  }
  
  async update(collectionName, docs){ 
    debug('update collection', collectionName, ' docs', docs)

    let collection = await this.getCollection(collectionName)

    let objs = []

    for(let obj of docs){
      let dbDoc = this.documentFromObject(obj)

      debug('updating',obj, 'to', dbDoc)

      const stripped = this.stripMeta(dbDoc)
      const meta = this.onlyMeta(obj)

      debug('validating', stripped,'from', dbDoc)

      await this.factory.validate(collectionName, stripped)

      debug('its good, updating', dbDoc)

      let old = await collection.findOne( {'_id': dbDoc._id})

      debug('found old', old)
      dbDoc.meta.revision = old.meta.revision++

      
      let mergedDoc = {...old, ...dbDoc}

      await collection.update({'_id': dbDoc._id}, mergedDoc)

      const finalObj = this.documentToObject(mergedDoc)

      this.emitChange(finalObj, 'update')

      objs.push( finalObj )

    }

    return objs
  }

  async findAndRemove(collectionName, obj){ 
    debug('findAndRemove collection', collectionName, ' obj', obj)

    let collection = await this.getCollection(collectionName)

    const dbDoc = await collection.findOne( {'_id': obj.$meta.id})

    debug('found old doc', dbDoc)

    await collection.remove( { '_id': obj.$meta.id } )

    debug('dbDoc', dbDoc)

    let finalObj = this.documentToObject(dbDoc)

    finalObj.$meta.removed = true

    this.emitChange(finalObj, 'remove')

    debug('obj', finalObj)

    return finalObj
  }
}