/**
 * Created by tsturzl on 4/11/17.
 */ import Index from "./indices" ; import { IindexOptions , IStorageDriver , IRange , IupdateOptions , Isanitize , Iexist } from "./types" ; import { Cursor , Ioptions } from "./index" ; import { $set , $inc , $mul , $unset , $rename } from "./updateOperators" ; import { getUUID , getDate } from "./utils" ; import * as BTT from "binary-type-tree" ; import { isEmpty , compressObj , expandObj , rmArrObjDups , flattenArr , saveArrDups , getObjValue } from "tedb-utils" ; /**
 * ~~~
 * Array String Number, Date, Boolean, -> symbol was redacted. : Used for keys
 * BTT.ASNDBS = Array<any[]|string|number|Date|boolean|null>|string|number|Date|boolean|null
 * -> redacted symbol, Number, Date, Boolean, String, Array : Used for values
 * BTT.SNDBSA = Array<{}|any[]|string|number|Date|boolean|null>;
 * ~~~
 */ export interface IDatastore { insert ( doc ) : Promise < any > ; find ( query ) : Cursor ; count ( query ) : Cursor ; update ( query , operation , options ) : Promise < any > ; remove ( query ) : Promise < number > ; ensureIndex ( options ) : Promise < null > ; san ( fieldName , index ) : Promise < any > ; sanitize ( ) : Promise < any > ; storageSan ( fieldName ) : Promise < any > ; storageSanitize ( ) : any ; removeIndex ( fieldName ) : Promise < null > ; saveIndex ( fieldName ) : Promise < null > ; insertIndex ( key , index ) : Promise < null > ; getIndices ( ) : Promise < any > ; getDocs ( options , ids ) : Promise < any [ ] > ; search ( fieldName , value ) : Promise < string [ ] > ; } /**
 * Datastore class
 *
 * Example:
 * ~~~
 * const UserStorage = new yourStorageClass("users");
 * const Users = new Datastore({storage: UserStorage});
 * ~~~
 * Creates a new Datastore using a specified storageDriver
 */ export default class Datastore implements IDatastore { /** A HashMap of all the indices keyed by the fieldName. <fieldName, Index> */ private indices : Map < string , Index > ; /** StorageDriver that is used for this Datastore */ private storage : IStorageDriver ; /** whether or not to generate IDs automatically */ private generateId : boolean ; param config constructor ( config ) { this . storage = config . storage ; this . generateId = true ; this . indices = new Map ( ) ; } param doc returns { Promise < any > } public insert ( doc ) { return new Promise < any > ( ( resolve , reject ) => { if ( isEmpty ( doc ) ) { return reject ( new Error ( "Cannot insert empty document" ) ) ; } if ( ! doc . hasOwnProperty ( "_id" ) ) { doc . _id = this . createId ( ) ; } else if ( doc . _id . length !== 64 ) { doc . _id = this . createId ( ) ; } const indexPromises = [ ] ; this . indices . forEach ( ( v ) => { indexPromises . push ( v . insert ( doc ) ) ; } ) ; Promise . all ( indexPromises ) . then ( ( ) : any => { return this . storage . setItem ( doc . _id , doc ) ; } ) . then ( resolve ) . catch ( reject ) ; } ) ; } param query returns { Cursor } public find ( query = { } ) { return new Cursor ( this , query ) ; } param query public count ( query = { } ) { return new Cursor ( this , query , true ) ; } param query param operation param options returns { Promise < any > } public update ( query , operation , options = { } ) { return new Promise < any > ( ( resolve , reject ) => { if ( isEmpty ( operation ) ) { return reject ( new Error ( "No update without update operation" ) ) ; } const promises = [ ] ; const indexPromises = [ ] ; const operators = [ "$set" , "$mul" , "$inc" , "$unset" , "$rename" ] ; const multi = options . multi || false ; const upsert = options . upsert || false ; const exactObjectFind = options . exactObjectFind || false ; const returnUpdatedDocs = options . returnUpdatedDocs || false ; const operationKeys = Object . keys ( operation ) ; if ( exactObjectFind ) { const target = { } ; compressObj ( query , target ) ; query = target ; } if ( multi ) { return this . find ( query ) . exec ( ) . then ( ( res ) : any => { res = res as any [ ] ; if ( res . length === 0 ) { if ( upsert ) { if ( exactObjectFind ) { query = expandObj ( query ) ; } query . _id = this . createId ( ) ; this . indices . forEach ( ( v ) => indexPromises . push ( v . insert ( query ) ) ) ; this . updateDocsIndices ( [ query ] , promises , indexPromises , operation , operators , operationKeys , reject ) ; } else { return [ ] ; } } else { this . updateDocsIndices ( res , promises , indexPromises , operation , operators , operationKeys , reject ) ; } return Promise . all ( indexPromises ) ; } ) . then ( ( ) => { return Promise . all ( promises ) ; } ) . then ( ( docs ) => rmArrObjDups ( docs , "_id" ) ) . then ( ( docs ) => { const docPromises = [ ] ; docs . forEach ( ( doc ) => { docPromises . push ( this . storage . setItem ( doc . _id , doc ) ) ; } ) ; return Promise . all ( docPromises ) ; } ) . then ( ( res ) => { if ( returnUpdatedDocs ) { resolve ( res ) ; } else { resolve ( ) ; } } ) . catch ( reject ) ; } else { return this . find ( query ) . limit ( 1 ) . exec ( ) . then ( ( res ) => { res = res as any [ ] ; if ( res . length === 0 ) { if ( upsert ) { if ( exactObjectFind ) { query = expandObj ( query ) ; } query . _id = this . createId ( ) ; this . indices . forEach ( ( v ) => { indexPromises . push ( v . insert ( query ) ) ; } ) ; this . updateDocsIndices ( [ query ] , promises , indexPromises , operation , operators , operationKeys , reject ) ; } else { return [ ] ; } } else { this . updateDocsIndices ( res , promises , indexPromises , operation , operators , operationKeys , reject ) ; } return Promise . all ( indexPromises ) ; } ) . then ( ( ) => { return Promise . all ( promises ) ; } ) . then ( ( docs ) => { return rmArrObjDups ( docs , "_id" ) ; } ) . then ( ( docs ) => { const docPromises = [ ] ; docs . forEach ( ( doc ) => { docPromises . push ( this . storage . setItem ( doc . _id , doc ) ) ; } ) ; return Promise . all ( docPromises ) ; } ) . then ( ( res ) => { if ( returnUpdatedDocs ) { resolve ( res ) ; } else { resolve ( ) ; } } ) . catch ( reject ) ; } } ) ; } public san ( fieldName , index ) { return new Promise ( ( resolve , reject ) => { return this . getIndices ( ) . then ( ( indices ) => indices . get ( fieldName ) ) . then ( ( INDEX ) => { const values = [ ] ; INDEX . traverse ( ( ind ) => { ind . value . forEach ( ( i ) => { if ( i !== undefined && i !== null && i !== false ) { values . push ( { key : ind . key , value : i } ) ; } } ) ; } ) ; return values ; } ) . then ( ( values ) => { return Promise . all ( values . map ( ( obj ) => { return this . storage . exists ( obj , index , fieldName ) ; } ) ) ; } ) . then ( ( data ) => { return Promise . all ( data . map ( ( d ) => { if ( d . doesExist ) { return new Promise ( ( res ) => res ( ) ) ; } else { if ( d . key === null ) { return new Promise ( ( res ) => res ( ) ) ; } else { return d . index . removeByPair ( d . key , d . value ) ; } } } ) ) ; } ) . then ( resolve ) . catch ( reject ) ; } ) ; } public storageSan ( fieldName ) { return new Promise ( ( resolve , reject ) => { return this . getIndices ( ) . then ( ( indices ) => indices . get ( fieldName ) ) . then ( ( INDEX ) => { const values = [ ] ; INDEX . traverse ( ( ind ) => { values . push ( ... ind . value . filter ( ( i ) => { if ( i !== undefined && i !== null && i !== false ) { return i ; } } ) ) ; } ) ; return values ; } ) . then ( ( values ) : Promise < any > => { return this . storage . collectionSanitize ( values ) ; } ) . then ( resolve ) . catch ( reject ) ; } ) ; } public storageSanitize ( ) { return new Promise ( ( resolve , reject ) => { if ( this . indices . size !== 0 ) { const fieldNames = [ ] ; this . indices . forEach ( ( i , fieldName ) => { fieldNames . push ( this . storageSan ( fieldName ) ) ; } ) ; return Promise . all ( fieldNames ) . then ( resolve ) . catch ( reject ) ; } else { return resolve ( ) ; } } ) ; } returns { Promise < any > } public sanitize ( ) { return new Promise ( ( resolve , reject ) => { const fieldNames = [ ] ; this . indices . forEach ( ( i , fieldName ) => { fieldNames . push ( this . san ( fieldName , i ) ) ; } ) ; return Promise . all ( fieldNames ) . then ( resolve ) . catch ( reject ) ; } ) ; } param query returns { Promise < number > } public remove ( query = { } ) { return new Promise ( ( resolve , reject ) => { const uniqueIds = [ ] ; this . find ( query ) . exec ( ) . then ( ( docs ) : Promise < any [ ] [ ] > | any [ ] => { if ( this . indices . size !== 0 ) { return Promise . all ( docs . map ( ( document ) => { return Promise . all ( Array . from ( this . indices ) . map ( ( [ key , value ] ) => { return value . remove ( document ) ; } ) ) ; } ) ) ; } else { return docs ; } } ) . then ( ( docs ) => { docs = flattenArr ( docs ) ; return rmArrObjDups ( docs , "_id" ) ; } ) . then ( ( docs ) => { docs . forEach ( ( document ) => { if ( uniqueIds . indexOf ( document . _id ) === - 1 ) { uniqueIds . push ( document . _id ) ; } } ) ; return Promise . all ( uniqueIds . map ( ( id ) => { if ( id && ( Object . prototype . toString . call ( id ) === "[object String]" ) ) { return this . storage . removeItem ( id ) ; } else { return new Promise ( ( res ) => res ( ) ) ; } } ) ) ; } ) . then ( ( ) : any => { resolve ( uniqueIds . length ) ; } ) . catch ( reject ) ; } ) ; } param options returns { Promise < null > } public ensureIndex ( options ) { return new Promise < null > ( ( resolve , reject ) => { try { this . indices . set ( options . fieldName , new Index ( this , options ) ) ; } catch ( e ) { return reject ( e ) ; } resolve ( ) ; } ) ; } param fieldName returns { Promise < null > } public removeIndex ( fieldName ) { return new Promise < null > ( ( resolve , reject ) => { try { this . indices . delete ( fieldName ) ; } catch ( e ) { return reject ( e ) ; } resolve ( ) ; } ) ; } param fieldName returns { Promise < any > } public saveIndex ( fieldName ) { return new Promise < any > ( ( resolve , reject ) => { const index = this . indices . get ( fieldName ) ; if ( index ) { index . toJSON ( ) . then ( ( res ) => { return this . storage . storeIndex ( fieldName , res ) ; } ) . then ( resolve ) . catch ( reject ) ; } else { return reject ( new Error ( `No index with name ${ fieldName } for this datastore` ) ) ; } } ) ; } param key param index returns { Promise < null > } public insertIndex ( key , index ) { return new Promise < null > ( ( resolve , reject ) => { try { const indices = this . indices . get ( key ) ; if ( indices !== undefined ) { indices . insertMany ( key , index ) . then ( resolve ) . catch ( ( err ) => { return reject ( err ) ; } ) ; } else { return reject ( new Error ( "No Index for this key was created on this datastore." ) ) ; } } catch ( e ) { return reject ( e ) ; } } ) ; } returns { Promise < any > } public getIndices ( ) { return new Promise < any > ( ( resolve , reject ) => { if ( this . indices ) { resolve ( this . indices ) ; } else { reject ( `No indices found in TeDB: ${ this . indices }` ) ; } } ) ; } param options param ids returns { Promise < any > } public getDocs ( options , ids ) { return new Promise < any > ( ( resolve , reject ) => { let idsArr = ( typeof ids === "string" ) ? [ ids ] : ids ; if ( options . skip && options . limit ) { idsArr = idsArr . splice ( options . skip , options . limit ) ; } else if ( options . skip && ! options . limit ) { idsArr . splice ( 0 , options . skip ) ; } else if ( options . limit && ! options . skip ) { idsArr = idsArr . splice ( 0 , options . limit ) ; } return this . createIdsArray ( idsArr ) . then ( resolve ) . catch ( reject ) ; } ) ; } param fieldName param value returns { Promise < T > } public search ( fieldName ? , value ? ) { return new Promise ( ( resolve , reject ) => { if ( fieldName === "$or" && value instanceof Array ) { const promises = [ ] ; value . forEach ( ( query ) : void => { for ( const field in query ) { if ( typeof field === "string" && query . hasOwnProperty ( field ) ) { promises . push ( this . searchField ( field , query [ field ] ) ) ; } } } ) ; Promise . all ( promises ) . then ( ( idsArr ) => flattenArr ( idsArr ) ) . then ( resolve ) . catch ( reject ) ; } else if ( fieldName === "$and" && value instanceof Array ) { const promises = [ ] ; value . forEach ( ( query ) : void => { for ( const field in query ) { if ( typeof field === "string" && query . hasOwnProperty ( field ) ) { promises . push ( this . searchField ( field , query [ field ] ) ) ; } } } ) ; Promise . all ( promises ) . then ( ( idsArr ) => saveArrDups ( idsArr ) ) . then ( resolve ) . catch ( reject ) ; } else if ( fieldName !== "$or" && fieldName !== "$and" && fieldName ) { this . searchField ( fieldName , value ) . then ( resolve ) . catch ( reject ) ; } else if ( ( fieldName === undefined ) && ( value === undefined ) ) { this . searchField ( ) . then ( resolve ) . catch ( reject ) ; } else { return reject ( new Error ( "Logical operators expect an Array" ) ) ; } } ) ; } param id returns { Date } public getIdDate ( id ) { return getDate ( id ) ; } param fieldName param value returns { Promise < BTT . SNDBSA > } private searchField ( fieldName ? , value ? ) { if ( fieldName && ( value !== undefined ) ) { return this . indices . has ( fieldName ) ? this . searchIndices ( fieldName , value ) : this . searchCollection ( fieldName , value ) ; } else { return this . searchCollection ( ) ; } } param fieldName param value returns { Promise < BTT . SNDBSA > } private searchIndices ( fieldName , value ) { return new Promise < BTT . SNDBSA > ( ( resolve , reject ) => { const index = this . indices . get ( fieldName ) ; if ( ! index ) { return resolve ( undefined ) ; } if ( typeof value === "object" ) { resolve ( index . searchRange ( value ) ) ; } else { resolve ( index . search ( value ) ) ; } } ) ; } param fieldName param value returns { Promise < T > } private searchCollection ( fieldName ? , value ? ) { return new Promise ( ( resolve , reject ) : any => { const ids = [ ] ; if ( fieldName && ( value !== undefined ) ) { const queryObj = { } ; let lt ; let lte ; let gt ; let gte ; let ne ; if ( value !== null && Object . prototype . toString . call ( value ) === "[object Object]" ) { lt = ( value . hasOwnProperty ( "$lt" ) && value . $lt !== undefined ) ? value . $lt : null ; lte = ( value . hasOwnProperty ( "$lte" ) && value . $lte !== undefined ) ? value . $lte : null ; gt = ( value . hasOwnProperty ( "$gt" ) && value . $gt !== undefined ) ? value . $gt : null ; gte = ( value . hasOwnProperty ( "$gte" ) && value . $gte !== undefined ) ? value . $gte : null ; ne = value . hasOwnProperty ( "$ne" ) ? value . $ne : null ; } else { lt = null ; lte = null ; gt = null ; gte = null ; ne = null ; } const queryArray = [ { name : "lt" , value : lt } , { name : "lte" , value : lte } , { name : "gt" , value : gt } , { name : "gte" , value : gte } , { name : "ne" , value : ne } ] ; queryArray . forEach ( ( q ) => { if ( q . value !== null ) { queryObj [ q . name ] = { value : q . value , flag : false } ; } } ) ; const allTrue = ( obj ) => { for ( const o in obj ) { if ( obj . hasOwnProperty ( o ) ) { if ( ! obj [ o ] . flag ) { return false ; } } } return true ; } ; this . storage . iterate ( ( v , k ) => { const field = getObjValue ( v , fieldName ) ; if ( field !== undefined ) { if ( lt === null && lte === null && gt === null && gte === null && ne === null && ( k === value || field === value ) ) { ids . push ( k ) ; } else { let flag ; if ( Object . prototype . toString . call ( value ) === "[object Object]" ) { for ( const item in queryObj ) { if ( queryObj . hasOwnProperty ( item ) ) { switch ( item ) { case "gt" : queryObj [ item ] . flag = ( ( field > gt ) && ( gt !== null ) ) ; break ; case "gte" : queryObj [ item ] . flag = ( ( field >= gte ) && ( gte !== null ) ) ; break ; case "lt" : queryObj [ item ] . flag = ( ( field < lt ) && ( lt !== null ) ) ; break ; case "lte" : queryObj [ item ] . flag = ( ( field <= lte ) && ( lte !== null ) ) ; break ; case "ne" : queryObj [ item ] . flag = ( ( field !== ne ) && ( ne !== null ) ) ; break ; } } } flag = allTrue ( queryObj ) ; } else { flag = ( ( ( field < lt ) && ( lt !== null ) ) || ( ( field <= lte ) && ( lte !== null ) ) || ( ( field > gt ) && ( gt !== null ) ) || ( ( field >= gte ) && ( gte !== null ) ) || ( ( field !== ne ) && ( ne !== null ) ) || ( field === value ) ) ; } if ( flag ) { ids . push ( k ) ; } } } } ) . then ( ( ) => { resolve ( ids ) ; } ) . catch ( ( e ) => { reject ( e ) ; } ) ; } else { this . storage . keys ( ) . then ( resolve ) . catch ( reject ) ; } } ) ; } param docs param promises param indexPromises param operation param operators param operationKeys param reject private updateDocsIndices ( docs , promises , indexPromises , operation , operators , operationKeys , reject ) { docs . forEach ( ( doc ) => { let mathed ; let preMath ; this . indices . forEach ( ( index , field ) => { operationKeys . forEach ( ( k ) => { if ( operationKeys . indexOf ( k ) !== - 1 ) { const setKeys = Object . keys ( operation [ k ] ) ; switch ( k ) { case "$set" : setKeys . forEach ( ( sk ) => { if ( field === sk ) { indexPromises . push ( index . updateKey ( getObjValue ( doc , field ) , operation [ k ] [ sk ] ) ) ; } } ) ; break ; case "$mul" : setKeys . forEach ( ( mk ) => { if ( field === mk ) { if ( mathed ) { preMath = mathed ; mathed = mathed * operation [ k ] [ mk ] ; } else { mathed = getObjValue ( doc , field ) * operation [ k ] [ mk ] ; } const indexed = preMath ? preMath : getObjValue ( doc , field ) ; indexPromises . push ( index . updateKey ( indexed , mathed ) ) ; } } ) ; break ; case "$inc" : setKeys . forEach ( ( ik ) => { if ( field === ik ) { if ( mathed ) { preMath = mathed ; mathed = mathed + operation [ k ] [ ik ] ; } else { mathed = getObjValue ( doc , field ) + operation [ k ] [ ik ] ; } const indexed = preMath ? preMath : getObjValue ( doc , field ) ; indexPromises . push ( index . updateKey ( indexed , mathed ) ) ; } } ) ; break ; case "$unset" : setKeys . forEach ( ( ik ) => { if ( field === ik ) { indexPromises . push ( index . remove ( doc ) ) ; } } ) ; break ; case "$rename" : setKeys . forEach ( ( rn ) => { if ( field === rn ) { const indexValue = this . indices . get ( field ) ; this . removeIndex ( field ) . then ( ( ) => { if ( indexValue ) { this . indices . set ( operation [ k ] [ rn ] , indexValue ) ; } else { return reject ( new Error ( `Cannot rename index of ${ field } that does not exist` ) ) ; } } ) . catch ( ( e ) => reject ( e ) ) ; } } ) ; break ; } } } ) ; } ) ; operationKeys . forEach ( ( k ) => { if ( operators . indexOf ( k ) !== - 1 ) { switch ( k ) { case "$set" : promises . push ( $set ( doc , operation [ k ] ) ) ; break ; case "$mul" : promises . push ( $mul ( doc , operation [ k ] ) ) ; break ; case "$inc" : promises . push ( $inc ( doc , operation [ k ] ) ) ; break ; case "$unset" : promises . push ( $unset ( doc , operation [ k ] ) ) ; break ; case "$rename" : promises . push ( $rename ( doc , operation [ k ] ) ) ; break ; } } } ) ; } ) ; } param promises param ids private createIdsArray ( ids ) { return new Promise ( ( resolve , reject ) => { return Promise . all ( ids . map ( ( id ) => { return this . storage . getItem ( id ) ; } ) ) . then ( ( docs ) => { docs = docs . filter ( ( doc ) => ! isEmpty ( doc ) ) ; resolve ( docs ) ; } ) . catch ( reject ) ; } ) ; } returns { string } private createId ( ) { return getUUID ( ) ; } }