'use strict'
const IDb = require('../idb')
const Hoek = require('@hapi/hoek')
const uuidv4 = require('uuid/v4')
const {promisfy} = require('promisfy')
const debug = require('debug')('bouncer.db.tingo-db')
/**
* Ideal for extremely large datasets with frequent document additions. Has a very efficient append-only file system driver which is ideal for embedded platforms. All database indexes must fit into RAM and are re-computed at db load time.
*
* @class module:Db.TingoDb
* @extends {module:Db.IDb}
* @link module:Db
* @see module:Party.TingoParty
*/
module.exports = class TingoDb extends IDb {
constructor ({path, factory, tingoOptions, prefix, useUuid}) {
super(factory, prefix)
debug('constructor path=',path, tingoOptions)
this.tingo = null
this.path = path
this.tingoOptions = tingoOptions || {nativeObjectID: true}
this.error = null
this.useUuid = (useUuid==undefined) ? true : useUuid
}
async start(){
if(this.tingo != null){ return }
debug('starting', this.tingoOptions)
await new Promise((resolve,reject)=>{
try{
const Tingo = require('tingodb')(this.tingoOptions)
this.tingo = new Tingo.Db(this.path, this.tingoOptions)
resolve()
}
catch(err){ this.error = err; reject(err) }
})
await super.start()
}
async compactDatabase(){
debug('compactDatabase ...')
await promisfy(this.tingo.compactDatabase.bind(this.tingo))()
debug('compactDatabase done')
}
/** 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(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 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 this.tingo.ObjectID().valueOf()
}
}
let dbDoc = this.documentFromObject(temp)
return dbDoc
}
async getCollectionNames(){
let names = await promisfy(this.tingo.collectionNames.bind(this.tingo))({})
return names.map(col=>{
return col.name.replace(this.prefix, '')
})
}
async ensureIndex(nameOrCollection, indexSettings){
let collection = typeof nameOrCollection == 'string' ? await this.getCollection(nameOrCollection) : nameOrCollection
indexSettings.indices.map(index=>{
let obj={}
obj[index]=1
collection.createIndex(obj, {unique: false})
})
indexSettings.unique.map(index=>{
let obj={}
obj[index]=1
collection.createIndex(obj, {unique: true})
})
collection.createIndex({'_id': 1}, {unique: true})
}
async createCollection(name, indexSettings){
debug('createCollection', name, indexSettings)
if(this.hasCollection(name) !== null){
await this.ensureIndex(name, indexSettings)
return
}
let collection = await promisfy(this.tingo.createCollection.bind(this.tingo))(this.prefix+name)
await this.ensureIndex(collection, indexSettings)
}
async getCollection(name){
let collection = await promisfy(this.tingo.collection.bind(this.tingo))(this.prefix+name)
return collection
}
async find(collectionName, mongoQuery){
let query = mongoQuery.getQueryDoc()
debug('query', query)
debug('find collection=', collectionName, ' query=', JSON.stringify(query,null,2))
let collection = await this.getCollection(collectionName)
let cursor = await promisfy(collection.find.bind(collection))(
query,
mongoQuery.hasSort() ? mongoQuery.getSort() : undefined
)
if(mongoQuery.hasLimit()){
cursor = cursor.limit(mongoQuery.getLimit())
}
let resultArray = await promisfy(cursor.toArray.bind(cursor))()
return resultArray.map(this.documentToObject) || []
}
/*async insert(collectionName, obj){
debug('insert collection=', collectionName, ' doc=', JSON.stringify(obj,null,2))
let collection = await this.getCollection(collectionName)
let dbDoc = this.ensureId(obj)
const validatedDbDoc = await this.factory.validate(collectionName, this.stripMeta(dbDoc))
const docs = await promisfy(collection.insert.bind(collection))( dbDoc )
const finalDbDoc = docs[0]
const finalObj = this.documentToObject(finalDbDoc)
this.emitChange(finalObj, 'create')
return finalObj
}*/
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){
if(this.useUuid){
temp._id = uuidv4()
}
else{
temp._id = (new this.tingo.ObjectID()).toString();
}
temp.$meta.id=temp._id;
}
let dbDoc = this.documentFromObject(temp)
/*dbDoc.meta = {
id: dbDoc._id,
type: collectionName,
created: Hoek.reach(doc,'$meta.created', {default: Date.now()}),
revision: Hoek.reach(doc,'$meta.revision', {default: 1})
}*/
const stripped = this.stripMeta(temp)
debug('validating', stripped,'from', temp)
await this.factory.validate(collectionName, stripped)
debug('its good, inserting', dbDoc)
const result = await promisfy(collection.insert.bind(collection))( dbDoc )
debug('inserted', result)
const finalDbDoc = result[0]
const finalObj = this.documentToObject(finalDbDoc)
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)
dbDoc.meta.revision++
debug('updating',obj, 'to', dbDoc)
const stripped = this.stripMeta(dbDoc)
debug('validating', stripped,'from', dbDoc)
await this.factory.validate(collectionName, stripped)
debug('its good, updating', dbDoc)
const result = await promisfy(collection.update.bind(collection))( {_id: dbDoc._id}, dbDoc )
const finalObj = this.documentToObject(dbDoc)
this.emitChange(finalObj, 'update')
objs.push( finalObj )
}
/*const dbDocs = docs.map(this.documentFromObject)
const docs = await promisfy(collection.update.bind(collection))( dbRmMsg )
let objs = docs.map(doc=>{
let obj = this.documentToObject(doc)
this.emitChange(obj, 'update')
return obj
})*/
return objs
}
async findAndRemove(collectionName, obj){
debug('findAndRemove collection', collectionName, ' obj', obj)
let collection = await this.getCollection(collectionName)
const dbDoc = await promisfy(collection.findAndRemove.bind(collection))( { _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
}
}