import { TypeStoreFinderKey, TypeStoreRepoKey, TypeStoreFindersKey } from "./Constants" import { IModel, IFinderOptions, IndexAction, IRepoOptions, IIndexOptions, ISearchProvider, IPlugin, IModelMapper, ISearchOptions, IRepoPlugin, IFinderPlugin, IIndexerPlugin, ModelPersistenceEventType, PluginType, IPredicate } from "./Types" import {Coordinator} from './Coordinator' import {NotImplemented} from "./Errors" import * as Log from './log' import { isFinderPlugin, PluginFilter, PromiseMap } from "./Util" import {ModelMapper,getDefaultMapper} from "./ModelMapper" import {IModelType} from "./ModelTypes" import {IModelOptions, IModelKey, IKeyValue, TKeyValue, IModelAttributeOptions} from "./decorations/ModelDecorations"; import {getMetadata} from "./MetadataManager"; import { isFunction } from "typeguard" // Logger const log = Log.create(__filename) /** * The core Repo implementation * * When requested from the coordinator, * it offers itself to all configured plugins for * them to attach to the model pipeline * * */ export class Repo { modelOpts:IModelOptions repoOpts:IRepoOptions modelType:IModelType mapper coordinator:Coordinator protected plugins = Array() /** * Core repo is instantiated by providing the implementing/extending * class and the model that will be supported * * @param repoClazz * @param modelClazz */ constructor(public repoClazz:any,public modelClazz:{new ():M;}) { } protected getRepoPlugins() { return PluginFilter>(this.plugins,PluginType.Repo) // return this.plugins // .filter((plugin) => isRepoPlugin(plugin)) as IRepoPlugin[] } protected getFinderPlugins():IFinderPlugin[] { return PluginFilter(this.plugins,PluginType.Finder) } attr(name:string):IModelAttributeOptions { return this.modelType.options.attrs.find(attr => attr.name === name) } init(coordinator) { this.coordinator = coordinator this.modelType = coordinator.getModel(this.modelClazz) this.modelOpts = this.modelType.options this.repoOpts = Reflect.getMetadata(TypeStoreRepoKey,this.repoClazz) || {} } start() { // Grab a mapper this.mapper = this.getMapper(this.modelClazz) // Decorate all the finders this.decorateFinders() } getMapper(clazz:{new():M;}):IModelMapper { return getDefaultMapper(clazz) } /** * Attach a plugin to the repo - could be a store, * indexer, etc, etc * * @param plugin * @returns {Repo} */ attach(plugin:IPlugin):this { if (this.plugins.includes(plugin)) { log.warn(`Trying to register repo plugin a second time`) } else { this.plugins.push(plugin) } return this } getFinderOptions(finderKey:string):IFinderOptions { return getMetadata( TypeStoreFinderKey, this, finderKey ) as IFinderOptions } getPlugins = (predicate:IPredicate) => this.plugins.filter(predicate) /** * Decorate finder by iterating all finder plugins * and trying until resolved * * @param finderKey */ decorateFinder(finderKey) { let finder // Iterate all finder plugins for (let plugin of this.getPlugins(isFinderPlugin)) { if (!isFunction((plugin as any).decorateFinder)) continue const finderPlugin = plugin as IFinderPlugin //let finderResult if (finder = finderPlugin.decorateFinder(this,finderKey)) { /** * If we got a promise back then we need to wait * * IE. pouch db creating an index */ // if (isFunction(finderResult.then)) { // finder = (...args) => { // return (finderResult as Promise).then(finderFn => { // if (!finderFn) // NotImplemented(`Promised finder is not available ${finderKey}`) // // return finderFn(...args) // }) // } // } else { // // } //finder = finderResult break } } if (!finder && this.getFinderOptions(finderKey).optional !== true) NotImplemented(`No plugin supports this finder ${finderKey}`) this.setFinder(finderKey,finder) } /** * Decorate all finders on Repo */ decorateFinders() { (Reflect.getMetadata(TypeStoreFindersKey,this) || []) .forEach(finderKey => this.decorateFinder(finderKey)) } /** * Create a generic finder, in order * to do this search options must have been * annotated on the model * * @param finderKey * @param searchProvider * @param searchOpts * @returns {any} */ makeGenericFinder( finderKey:string, searchProvider:ISearchProvider, searchOpts:ISearchOptions ) { /** * Get the finder options * @type {any} */ const opts:IFinderOptions = this.getFinderOptions(finderKey) return async (...args) => { let results = await searchProvider.search( this.modelType, searchOpts, args ) // Once the provider returns the resulting data, // pass it to the mapper to get keys const keys:IModelKey[] = results.map((result:any) => { return searchOpts.resultKeyMapper( this, searchOpts.resultType, result ) }) return keys.map(async (key) => await this.get(key)) } } /** * Set a finder function on the repo * * @param finderKey * @param finderFn */ protected setFinder(finderKey:string,finderFn:(...args) => any) { this[finderKey] = finderFn } /** * Triggers manually attached persistence callbacks * - works for internal indexing solutions, etc * * @param type * @param models */ triggerPersistenceEvent(type:ModelPersistenceEventType,...models:any[]) { if (models.length < 1) return const {onPersistenceEvent} = this.modelType.options onPersistenceEvent && onPersistenceEvent(type,...models) } supportPersistenceEvents() { const {onPersistenceEvent} = this.modelType.options return typeof onPersistenceEvent !== 'undefined' && onPersistenceEvent !== null } /** * Call out to the indexers * * @param type * @param models * @returns {Bluebird} */ async index(type:IndexAction,...models:IModel[]):Promise { const indexPlugins = PluginFilter(this.plugins,PluginType.Indexer) const doIndex = (indexConfig:IIndexOptions):Promise[] => { return indexPlugins.map(plugin => plugin.index( type, indexConfig, this.modelType, this, ...models )) } // Create all pending index promises if (this.repoOpts && this.repoOpts.indexes) await Promise.all(this.repoOpts.indexes.reduce((promises,indexConfig) => { return promises.concat(doIndex(indexConfig)) },[])) return Promise.resolve(true) } indexPromise(action:IndexAction) { return async (models:M[]) => { const indexPromise = this.index(action,...models.filter((model) => !!model)) await Promise.resolve(indexPromise) return models } } /** * Not implemented * * @param args * @returns {null} */ key(...args):IKeyValue { for (let plugin of this.getRepoPlugins()) { const key = plugin.key(...args) if (key) return key } return NotImplemented('key') } /** * Get one or more models with keys * * @param key * @returns {null} */ async get(key:TKeyValue):Promise { //const useKey = isNumberOrString(key) ? key | let results = this.getRepoPlugins().map(async (plugin) => await plugin.get(key)) for (let result of results) { if (result) return result } return null } /** * Save model * * @param o * @returns {null} */ async save(o:M):Promise { let results = await PromiseMap(this.getRepoPlugins(), plugin => plugin.save(o)) await this.indexPromise(IndexAction.Add)(results) for (let result of results) { if (result) return result } return null } /** * Remove a model * * @param key * @returns {null} */ async remove(key:TKeyValue):Promise { let model = await this.get(key) if (!model) { log.warn(`No model found to remove with key`,key) return null } await PromiseMap(this.getRepoPlugins(), plugin => plugin.remove(key)) return this.indexPromise(IndexAction.Remove)([model]) } /** * Count models * * @returns {null} */ async count():Promise { let results = await Promise.all(this.getRepoPlugins().map(async (plugin) => await plugin.count())) return results.reduce((prev,current) => prev + current) } async bulkGet(...keys:TKeyValue[]):Promise { let results = await PromiseMap( this.getRepoPlugins(), plugin => plugin.bulkGet(...keys) ) return results.reduce((allResults,result) => { return allResults.concat(result) },[]) } async bulkSave(...models:M[]):Promise { let results = await PromiseMap( this.getRepoPlugins(), plugin => plugin.bulkSave(...models) ) results = results.reduce((allResults,result) => { return allResults.concat(result) },[]) return this.indexPromise(IndexAction.Add)(results) // for (let result of results) { // if (result) // return result // } // // const promises = models.map(model => this.save(model)) // return await Promise.all(promises) } async bulkRemove(...keys:TKeyValue[]):Promise { const models = await this.bulkGet(...keys) if (models.length != keys.length) throw new Error('Not all keys exist') await PromiseMap( this.getRepoPlugins(), plugin => plugin.bulkRemove(...keys) ) // results = results.reduce((allResults,result) => { // return allResults.concat(result) // },[]) return this.indexPromise(IndexAction.Remove)(models) } }