UNPKG

9.49 kBPlain TextView Raw
1import {
2 TypeStoreFinderKey,
3 TypeStoreRepoKey,
4 TypeStoreFindersKey
5} from "./Constants"
6
7import {
8 IModel,
9 IFinderOptions,
10 IndexAction,
11 IRepoOptions,
12 IIndexOptions,
13 ISearchProvider,
14 IPlugin,
15 IModelMapper,
16 ISearchOptions,
17 IRepoPlugin,
18 IFinderPlugin,
19 IIndexerPlugin,
20 ModelPersistenceEventType,
21 PluginType,
22 IPredicate
23} from "./Types"
24
25import {Coordinator} from './Coordinator'
26import {NotImplemented} from "./Errors"
27import * as Log from './log'
28
29import {
30 isFunction,
31 isFinderPlugin,
32 PluginFilter,
33 PromiseMap
34} from "./Util"
35
36import {ModelMapper,getDefaultMapper} from "./ModelMapper"
37import {IModelType} from "./ModelTypes"
38import {IModelOptions, IModelKey, IKeyValue, TKeyValue, IModelAttributeOptions} from "./decorations/ModelDecorations";
39import {getMetadata} from "./MetadataManager";
40
41// Logger
42const log = Log.create(__filename)
43
44
45/**
46 * The core Repo implementation
47 *
48 * When requested from the coordinator,
49 * it offers itself to all configured plugins for
50 * them to attach to the model pipeline
51 *
52 *
53 */
54export class Repo<M extends IModel> {
55
56 modelOpts:IModelOptions
57 repoOpts:IRepoOptions
58 modelType:IModelType
59 mapper
60 coordinator:Coordinator
61 protected plugins = Array<IPlugin>()
62
63 /**
64 * Core repo is instantiated by providing the implementing/extending
65 * class and the model that will be supported
66 *
67 * @param repoClazz
68 * @param modelClazz
69 */
70 constructor(public repoClazz:any,public modelClazz:{new ():M;}) {
71 }
72
73 protected getRepoPlugins() {
74 return PluginFilter<IRepoPlugin<M>>(this.plugins,PluginType.Repo)
75 // return this.plugins
76 // .filter((plugin) => isRepoPlugin(plugin)) as IRepoPlugin<M>[]
77 }
78
79 protected getFinderPlugins():IFinderPlugin[] {
80 return PluginFilter<IFinderPlugin>(this.plugins,PluginType.Finder)
81 }
82
83 attr(name:string):IModelAttributeOptions {
84 return this.modelType.options.attrs.find(attr => attr.name === name)
85 }
86
87 init(coordinator) {
88 this.coordinator = coordinator
89 this.modelType = coordinator.getModel(this.modelClazz)
90 this.modelOpts = this.modelType.options
91 this.repoOpts = Reflect.getMetadata(TypeStoreRepoKey,this.repoClazz) || {}
92
93 }
94
95 start() {
96 // Grab a mapper
97 this.mapper = this.getMapper(this.modelClazz)
98
99 // Decorate all the finders
100 this.decorateFinders()
101 }
102
103
104 getMapper<M extends IModel>(clazz:{new():M;}):IModelMapper<M> {
105 return getDefaultMapper(clazz)
106 }
107
108
109
110 /**
111 * Attach a plugin to the repo - could be a store,
112 * indexer, etc, etc
113 *
114 * @param plugin
115 * @returns {Repo}
116 */
117 attach(plugin:IPlugin):this {
118 if (this.plugins.includes(plugin)) {
119 log.warn(`Trying to register repo plugin a second time`)
120 } else {
121 this.plugins.push(plugin)
122 }
123
124 return this
125 }
126
127 getFinderOptions(finderKey:string):IFinderOptions {
128 return getMetadata(
129 TypeStoreFinderKey,
130 this,
131 finderKey
132 ) as IFinderOptions
133 }
134
135 getPlugins = (predicate:IPredicate) => this.plugins.filter(predicate)
136
137
138 /**
139 * Decorate finder by iterating all finder plugins
140 * and trying until resolved
141 *
142 * @param finderKey
143 */
144 decorateFinder(finderKey) {
145 let finder
146
147 // Iterate all finder plugins
148 for (let plugin of this.getPlugins(isFinderPlugin)) {
149 if (!isFunction((plugin as any).decorateFinder))
150 continue
151
152 const finderPlugin = plugin as IFinderPlugin
153
154 //let finderResult
155
156 if (finder = finderPlugin.decorateFinder(this,finderKey)) {
157
158 /**
159 * If we got a promise back then we need to wait
160 *
161 * IE. pouch db creating an index
162 */
163 // if (isFunction(finderResult.then)) {
164 // finder = (...args) => {
165 // return (finderResult as Promise<any>).then(finderFn => {
166 // if (!finderFn)
167 // NotImplemented(`Promised finder is not available ${finderKey}`)
168 //
169 // return finderFn(...args)
170 // })
171 // }
172 // } else {
173 //
174 // }
175 //finder = finderResult
176
177 break
178 }
179 }
180
181 if (!finder && this.getFinderOptions(finderKey).optional !== true)
182 NotImplemented(`No plugin supports this finder ${finderKey}`)
183
184 this.setFinder(finderKey,finder)
185 }
186
187 /**
188 * Decorate all finders on Repo
189 */
190 decorateFinders() {
191 (Reflect.getMetadata(TypeStoreFindersKey,this) || [])
192 .forEach(finderKey => this.decorateFinder(finderKey))
193
194 }
195
196 /**
197 * Create a generic finder, in order
198 * to do this search options must have been
199 * annotated on the model
200 *
201 * @param finderKey
202 * @param searchProvider
203 * @param searchOpts
204 * @returns {any}
205 */
206 makeGenericFinder(
207 finderKey:string,
208 searchProvider:ISearchProvider,
209 searchOpts:ISearchOptions<any>
210 ) {
211
212 /**
213 * Get the finder options
214 * @type {any}
215 */
216 const opts:IFinderOptions = this.getFinderOptions(finderKey)
217
218 return async (...args) => {
219 let results = await searchProvider.search(
220 this.modelType,
221 searchOpts,
222 args
223 )
224
225
226 // Once the provider returns the resulting data,
227 // pass it to the mapper to get keys
228 const keys:IModelKey[] = results.map((result:any) => {
229 return searchOpts.resultKeyMapper(
230 this,
231 searchOpts.resultType,
232 result
233 )
234 })
235
236 return keys.map(async (key) => await this.get(key))
237
238 }
239 }
240
241 /**
242 * Set a finder function on the repo
243 *
244 * @param finderKey
245 * @param finderFn
246 */
247 protected setFinder(finderKey:string,finderFn:(...args) => any) {
248 this[finderKey] = finderFn
249 }
250
251 /**
252 * Triggers manually attached persistence callbacks
253 * - works for internal indexing solutions, etc
254 *
255 * @param type
256 * @param models
257 */
258 triggerPersistenceEvent(type:ModelPersistenceEventType,...models:any[]) {
259 if (models.length < 1)
260 return
261
262 const {onPersistenceEvent} = this.modelType.options
263 onPersistenceEvent && onPersistenceEvent(type,...models)
264 }
265
266 supportPersistenceEvents() {
267 const {onPersistenceEvent} = this.modelType.options
268 return typeof onPersistenceEvent !== 'undefined' && onPersistenceEvent !== null
269 }
270
271 /**
272 * Call out to the indexers
273 *
274 * @param type
275 * @param models
276 * @returns {Bluebird<boolean>}
277 */
278 async index(type:IndexAction,...models:IModel[]):Promise<boolean> {
279 const indexPlugins = PluginFilter<IIndexerPlugin>(this.plugins,PluginType.Indexer)
280
281 const doIndex = (indexConfig:IIndexOptions):Promise<any>[] => {
282 return indexPlugins.map(plugin => plugin.index(
283 type,
284 indexConfig,
285 this.modelType,
286 this,
287 ...models
288 ))
289 }
290
291 // Create all pending index promises
292 if (this.repoOpts && this.repoOpts.indexes)
293 await Promise.all(this.repoOpts.indexes.reduce((promises,indexConfig) => {
294 return promises.concat(doIndex(indexConfig))
295 },[]))
296
297 return Promise.resolve(true)
298 }
299
300 indexPromise(action:IndexAction) {
301 return async (models:M[]) => {
302 const indexPromise = this.index(action,...models.filter((model) => !!model))
303
304 await Promise.resolve(indexPromise)
305 return models
306 }
307 }
308
309 /**
310 * Not implemented
311 *
312 * @param args
313 * @returns {null}
314 */
315 key(...args):IKeyValue {
316 for (let plugin of this.getRepoPlugins()) {
317 const key = plugin.key(...args)
318 if (key)
319 return key
320 }
321
322 return NotImplemented('key')
323 }
324
325 /**
326 * Get one or more models with keys
327 *
328 * @param key
329 * @returns {null}
330 */
331 async get(key:TKeyValue):Promise<M> {
332 //const useKey = isNumberOrString(key) ? key |
333 let results = this.getRepoPlugins().map(async (plugin) => await plugin.get(key))
334 for (let result of results) {
335 if (result)
336 return result
337 }
338
339 return null
340 }
341
342
343
344 /**
345 * Save model
346 *
347 * @param o
348 * @returns {null}
349 */
350 async save(o:M):Promise<M> {
351 let results = await PromiseMap(this.getRepoPlugins(), plugin => plugin.save(o))
352 await this.indexPromise(IndexAction.Add)(results)
353 for (let result of results) {
354 if (result)
355 return result
356 }
357
358 return null
359
360 }
361
362
363 /**
364 * Remove a model
365 *
366 * @param key
367 * @returns {null}
368 */
369 async remove(key:TKeyValue):Promise<any> {
370 let model = await this.get(key)
371 if (!model) {
372 log.warn(`No model found to remove with key`,key)
373 return null
374 }
375
376 await PromiseMap(this.getRepoPlugins(), plugin => plugin.remove(key))
377 return this.indexPromise(IndexAction.Remove)([model])
378
379 }
380
381
382 /**
383 * Count models
384 *
385 * @returns {null}
386 */
387 async count():Promise<number> {
388 let results = await Promise.all(this.getRepoPlugins().map(async (plugin) => await plugin.count()))
389 return results.reduce((prev,current) => prev + current)
390
391 }
392
393 async bulkGet(...keys:TKeyValue[]):Promise<M[]> {
394 let results = await PromiseMap(
395 this.getRepoPlugins(), plugin => plugin.bulkGet(...keys)
396 )
397
398 return results.reduce((allResults,result) => {
399 return allResults.concat(result)
400 },[])
401
402 }
403
404 async bulkSave(...models:M[]):Promise<M[]> {
405 let results = await PromiseMap(
406 this.getRepoPlugins(), plugin => plugin.bulkSave(...models)
407 )
408
409 results = results.reduce((allResults,result) => {
410 return allResults.concat(result)
411 },[])
412
413 return this.indexPromise(IndexAction.Add)(results)
414
415 // for (let result of results) {
416 // if (result)
417 // return result
418 // }
419 //
420 // const promises = models.map(model => this.save(model))
421 // return await Promise.all(promises)
422 }
423
424 async bulkRemove(...keys:TKeyValue[]):Promise<any[]> {
425 const models = await this.bulkGet(...keys)
426 if (models.length != keys.length)
427 throw new Error('Not all keys exist')
428
429 await PromiseMap(
430 this.getRepoPlugins(), plugin => plugin.bulkRemove(...keys)
431 )
432
433 // results = results.reduce((allResults,result) => {
434 // return allResults.concat(result)
435 // },[])
436
437 return this.indexPromise(IndexAction.Remove)(models)
438
439
440 }
441}
442