'use strict'
const IDb = require('../idb')
const Hoek = require('@hapi/hoek')
const Loki = require('lokijs')
const LokiFS = Loki.LokiFsAdapter
const LFSA = require('lokijs/src/loki-fs-structured-adapter')
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.loki-db')
/**
* A db implementation based on LokiJS.
*
* Ideal for frontend apps with small datasets (smaller than total RAM). This is an in-memory db so it trades RAM efficiency for access speed.
* @see https://github.com/techfort/LokiJS
*
* @class module:Db.LokiDb
* @extends {module:Db.IDb}
* @link module:Db
* @see module:Party.LokiParty
*/
module.exports = class LokiDb extends IDb {
constructor ({path, factory, dbAdapter, lokiOptions, useUuid}) {
super(factory)
debug('constructor')
this.loki = null
this.lokiOptions = lokiOptions
this.path = path
this.dbAdapter = dbAdapter || new LFSA()
this.error = null
this.useUuid = (useUuid==undefined) ? true : useUuid
}
static get LokiLocalStorageAdapter(){
return Loki.LokiLocalStorageAdapter
}
async start(){
debug('starting')
debug('path',this.path)
this.loki = new Loki(
this.path,
{
adapter : this.dbAdapter,
...this.lokiOptions
}
)
await new Promise((resolve, reject)=>{
this.loki.loadDatabase({}, resolve)
debug('started with collections', this.loki.listCollections())
})
await super.start()
//await promisfy(this.loki.saveDatabase.bind(this.loki))
debug('started with collections', this.loki.listCollections())
}
async getCollectionNames(){
const names = this.loki.listCollections()
return names.map(obj=>{return obj.name.replace(this.prefix,'')})
}
async createCollection(name, indexSettings){
debug('createCollection', name, indexSettings)
/*const existing = this.loki.getCollection(name)
if(existing !== null){ return }*/
if(this.hasCollection(name) == true){ return }
const options = {
unique: ['$meta.id'].concat(indexSettings.unique),
indices: ['$meta.id'].concat(indexSettings.indices)
}
debug('createCollection', name, options)
options.unique.push('$meta.id')
let collection = this.loki.addCollection(this.prefix+name, options)
debug(collection)
}
async getCollection(name){
let collection = this.loki.getCollection(this.prefix+name)
debug('collections', this.loki.listCollections())
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.$loki
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 handleCall(ask){
debug('handleCall')
//debug('\task', JSON.stringify(ask,null,2))
let complete = true
let results = []
for(let crufl of ask.crufls){
let result = {
op: crufl.op,
uuid: crufl.uuid,
msgs: [],
complete: true,
error: null
}
debug('\tcrufl->', crufl.op, crufl.type)
//debug('\t\tcrufl ->', crufl)
switch(crufl.op){
case 'create':
result.msgs = await this.applyCreate(crufl)
break
case 'remove':
result.msgs = await this.applyRemove(crufl)
break
case 'find':
result.msgs = await this.applyFind(crufl, false)
break
case 'lookup':
result.msgs = await this.applyFind(crufl, true)
break
default:
break
}
results.push(result)
}
let freshness = {
uuid: ask.uuid,
results,
complete
}
//debug('replying', JSON.stringify(freshness,null,2))
return {freshness: results }
}*/
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.chain().find(query)
if(mongoQuery.hasLimit()){
resultSet = resultSet.limit(mongoQuery.getLimit())
}
if(mongoQuery.hasSort()){
let sortPath = Object.keys(mongoQuery.getSort())[0]
resultSet = resultSet.simplesort( sortPath )
}
return resultSet.data().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){
if(this.useUuid){
temp._id = uuidv4()
}
else{
temp._id = (new ObjectId()).toHexString()
}
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)
const result = collection.insert( dbDoc )
debug('inserted', result)
const finalDbDoc = result
const finalObj = this.documentToObject(finalDbDoc)
debug('returning', finalObj)
this.emitChange(finalObj, 'create')
resultSet.push(finalObj)
}
await promisfy(this.loki.saveDatabase.bind(this.loki))
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)
let old = collection.findOne( {'$meta.id': dbDoc._id})
let mergedDoc = {...old, ...dbDoc}
collection.update(mergedDoc)
const finalObj = this.documentToObject(mergedDoc)
this.emitChange(finalObj, 'update')
objs.push( finalObj )
}
await promisfy(this.loki.saveDatabase.bind(this.loki))
return objs
}
async findAndRemove(collectionName, obj){
debug('findAndRemove collection', collectionName, ' obj', obj)
let collection = await this.getCollection(collectionName)
const dbDoc = collection.findAndRemove( { '$meta.id': obj.$meta.id } )
let finalObj = {
$meta: obj.$meta
}
finalObj.$meta.removed = true
this.emitChange(finalObj, 'remove')
debug('finalObj', finalObj)
debug('obj', obj)
await promisfy(this.loki.saveDatabase.bind(this.loki))
return finalObj
}
/*async applyFind(crufl, includeData = false){
debug('find', JSON.stringify(crufl,null,2))
let query = new MongoQuery(crufl.spec)
let MongoQuery = query.getQueryDoc()
debug('loki-find', JSON.stringify(MongoQuery,null,2))
let collection = this.loki.getCollection(crufl.type)
let resultSet = collection.find(MongoQuery)
//debug(collection)
//debug('resultSet', resultSet)
let msgs = []
for(const result of resultSet){
if(includeData){
let msg = Object.assign({},result)
msg.$meta.revision = msg.meta.revision
msg.$meta.created = msg.meta.created
msg.$meta.version = msg.meta.version
delete msg.meta
delete msg.$loki
msgs.push(msg)
} else{
msgs.push({
$meta:{
id: result.$meta.id,
type: result.$meta.type
}
})
}
}
debug(msgs)
return msgs
}
async applyCreate(crufl){
let msgs = []
let collection = this.loki.getCollection(crufl.type)
for(let createMsg of crufl.msgs){
let raw = {...createMsg}
raw.$meta.id = (new ObjectId()).toHexString()
let doc = collection.insert(Object.assign({},raw))
let msg = Object.assign({},doc)
msg.$meta.revision = msg.meta.revision
msg.$meta.created = msg.meta.created
msg.$meta.version = msg.meta.version
delete msg.meta
delete msg.$loki
msgs.push(msg)
}
return msgs
}
async applyRemove(crufl){
let msgs = []
let collection = this.loki.getCollection(crufl.type)
for(let rmMsg of crufl.msgs){
let msg = { $meta: {
removed: true,
id: rmMsg.$meta.id,
type: rmMsg.$meta.type
}}
collection.findAndRemove(rmMsg)
msgs.push(msg)
}
return msgs
}
*/
}