import PouchFactory from './PouchFactory' ; import BaseEntity from './BaseEntity' ; import { Subscriber , ChangeAction , ActionType } from './changes' ; import EntityConstructor from '../helpers/EntityConstructor' ; import getProtoData from '../utils/getProtoData' ; import { join } from 'path' ; import Dict from '../helpers/Dict' ; import Debug from 'debug' ; const debug = Debug ( 'slothdb' ) ; typeparam typeparam typeparam export default class SlothDatabase < S extends { _id : string ; } , E extends BaseEntity < S > , V extends string = never > { _root : string ; private type { string } memberof _name : string ; type { T } memberof private _model : EntityConstructor < S , E > ; private _subscribers : { factory : any ; sub : Subscriber < S > ; changes : PouchDB . Core . Changes < S > ; } [ ] = [ ] ; private _setupPromise ? : Promise < any > ; param factory param name param model param root constructor ( model , root = '' ) { this . _model = model ; this . _root = root ; const { name } = getProtoData ( model . prototype ) ; if ( ! name ) { throw new Error ( 'SlothEntity decorator is required' ) ; } this . _name = name ; } param props param field joinURIParams ( props , field = '_id' ) { const idURI = getProtoData ( this . _model . prototype ) . uris . find ( ( { name } ) => name === field ) ; if ( ! idURI ) { throw new Error ( `Field ${ field } not found in URIs` ) ; } return join ( idURI . prefix , ... idURI . propsKeys . map ( key => ( props as any ) [ key ] ) ) ; } param factory param view param startKey param endKey param includeDocs query ( factory , view , startKey ? , endKey = typeof startKey === 'string' ? join ( startKey , '\uffff' ) : undefined , includeDocs = false ) { return factory ( this . _name ) . query < S > ( view , { startkey : startKey , endkey : endKey , include_docs : includeDocs } ) . catch ( err => { if ( err . name === 'not_found' ) { debug ( `Design document '%s' is missing, generating views...` , view ) ; if ( this . _setupPromise ) { this . _setupPromise . then ( ( ) => { return this . query ( factory , view , startKey , endKey , includeDocs ) ; } ) ; } this . _setupPromise = this . initSetup ( factory ) ; return this . _setupPromise . then ( ( ) => { debug ( 'Created design documents' ) ; this . _setupPromise = undefined ; return this . query ( factory , view , startKey , endKey , includeDocs ) ; } ) ; } throw err ; } ) ; } param factory param view param startKey param endKey queryDocs ( factory , view , startKey ? , endKey ? ) { return this . query ( factory , view , startKey , endKey , true ) . then ( ( { rows } ) => { return rows . map ( ( { doc } ) => new this . _model ( factory , doc as any ) ) ; } ) ; } param factory param view param startKey param endKey queryKeys ( factory , view , startKey ? , endKey ? ) { return this . query ( factory , view , startKey , endKey , false ) . then ( ( { rows } ) => { return rows . map ( ( { key } ) => key ) ; } ) ; } param factory param view param startKey param endKey queryKeysIDs ( factory , view , startKey ? , endKey ? ) { return this . query ( factory , view , startKey , endKey , false ) . then ( ( { rows } ) => { return rows . reduce ( ( acc , { key , id } ) => ( { ... acc , [ key ] : id } ) , { } as Dict < string > ) ; } ) ; } param root withRoot ( root ) { return new SlothDatabase < S , E > ( this . _model , join ( this . _root , root ) ) ; } param { PouchFactory < S > } factory param { string } startKey param { string } endKey returns see PouchDB memberof findAllIDs ( factory , startKey = this . _root || '' , endKey = join ( startKey , '\uffff' ) ) { const db = factory ( this . _name ) ; return db . allDocs ( { include_docs : false , startkey : startKey , endkey : endKey } ) . then ( ( { rows } ) => { return rows . map ( ( { id } ) => id ) ; } ) ; } param { PouchFactory < S > } factory param { string } startKey param { string } endKey returns see PouchDB memberof findAllDocs ( factory , startKey = this . _root || '' , endKey = join ( startKey , '\uffff' ) ) { const db = factory ( this . _name ) ; return db . allDocs ( { include_docs : true , startkey : startKey , endkey : endKey } ) . then ( ( { rows } ) => { return rows . map ( ( { doc } ) => this . create ( factory , doc as S ) ) ; } ) ; } param factory param id return findById ( factory , id ) { return factory ( this . _name ) . get ( id ) . then ( res => { return new this . _model ( factory , res ) ; } ) ; } param factory param props returns create ( factory , props ) { return new this . _model ( factory , props ) ; } param factory param props returns put ( factory , props ) { const doc = new this . _model ( factory , props ) ; return doc . save ( ) . then ( ( ) => doc ) ; } param factory param sub see see subscribe ( factory , sub ) { if ( ! this . getSubscriberFor ( factory ) ) { debug ( 'Creating changes ' ) ; const changes = factory ( this . _name ) . changes ( { since : 'now' , live : true , include_docs : true } ) . on ( 'change' , ( { deleted , doc , id } ) => { if ( deleted || ! doc ) { return this . dispatch ( factory , { type : ActionType . REMOVED , payload : { [ this . _name ] : id } , meta : { } } ) ; } if ( doc . _rev . match ( /^1-/ ) ) { return this . dispatch ( factory , { type : ActionType . ADDED , payload : { [ this . _name ] : doc } , meta : { revision : doc . _rev } } ) ; } return this . dispatch ( factory , { type : ActionType . CHANGED , payload : { [ this . _name ] : doc } , meta : { revision : doc . _rev } } ) ; } ) ; this . _subscribers . push ( { factory , sub , changes } ) ; return ; } const { changes } = this . getSubscriberFor ( factory ) ! ; this . _subscribers . push ( { factory , sub , changes } ) ; } param factory param sub cancel ( factory , sub ) { const index = this . _subscribers . findIndex ( el => el . factory === factory && el . sub === sub ) ; const [ { changes } ] = this . _subscribers . splice ( index , 1 ) ; if ( ! this . getSubscriberFor ( factory ) ) { changes . cancel ( ) ; } } param factory async initSetup ( factory ) { await this . setupViews ( factory ) ; } protected getSubscriberFor ( factory ) { return this . _subscribers . find ( el => el . factory === factory ) ; } protected dispatch ( facto , action ) { this . _subscribers . forEach ( ( { sub , factory } ) => facto === factory && sub ( action ) ) ; } private setupViews ( factory ) { const { views } = getProtoData ( this . _model . prototype ) ; const db = factory ( this . _name ) ; const promises = views . map ( ( { name , id , code } ) => async ( ) => { const views = { } ; let _rev ; try { const doc = ( await db . get ( `_design/${ id }` ) ) as any ; if ( doc . views [ name ] && doc . views [ name ] . map === code ) { return ; } Object . assign ( views , doc . views ) ; _rev = doc . _rev ; } catch ( err ) { } await db . put ( Object . assign ( { } , { _id : `_design/${ id }` , views : { ... views , [ name ] : { map : code } } } , _rev ? { _rev } : { } ) as any ) ; } ) ; return promises . reduce ( ( acc , fn ) => { return acc . then ( ( ) => fn ( ) ) ; } , Promise . resolve ( ) ) ; } }