UNPKG

@tucmc/hazel

Version:
230 lines (199 loc) 7.86 kB
import path from 'path' import * as process from 'process' import type { DataType } from '../data/DataType' import { DMap } from '../data/DMap' import type { ChangeList, DataChanges } from '../data/DMapUtil' import { LiveDMap } from '../data/LiveDMap' import { ReferableMapEntity } from '../data/ReferableEntity' import { ConsoleColour } from '../debugger/Colour' import { Debugger } from '../debugger/Debugger' import { Files } from '../io/Files' /** * @template T - A received collection type definition * @category Lib */ export type CollectionMutator<T> = (d: T) => DataType /** * @template T - A collection data type definition. * @template M - A {@link CollectionMutator} received collection type definition. * @group Abstracts */ export abstract class Collection<T extends DataType, M = any, C = any> { protected readonly name: string protected debug = new Debugger(this.constructor.name || 'Collection') protected abstract collectionMutator: CollectionMutator<M> private rootPath = `resource/collection/${process.env.RUNTIME_MODE}` private readonly resourcePath: string protected readonly dbInstance: C /** * The **Collection<T>()** class hold data resources. User can choose either fetch from database or cache. * @template T - A collection data type definition. * @template M - A {@link CollectionMutator} received collection type definition. * @param name - The collection name. */ constructor(name: string) { this.name = name this.resourcePath = path.join(this.rootPath, `${this.name}.json`) this.dbInstance = this.initInstance(name) } protected abstract initInstance(collectionName: string): C /** * The **retrieveCollection()** abstract method will be called internally to fetch collection data. * Note: Different databases require different procedures to access its collection. * @protected * @override * @abstract */ protected abstract retrieveCollection(): Promise<M> /** * The **handleChanges()** abstract method will be used to handle {@link DataChanges} to match the database required form. * Note: Different databases require different procedures to process DataChanges. * @param changes - Received changes. * @protected * @override * @abstract */ protected abstract handleChanges( changes: DataChanges[] ): Promise<DataChanges[]> /** * The **verifyChanges()** abstract method will be used to verify changes pushed to the database. * Note: Different databases may require different procedures. * @param changes - Expected changes. * @protected * @override * @abstract */ protected abstract verifyChanges(changes: DataChanges[]): Promise<boolean> /** * The **setDefaultMutator()** method set Collection's defaultMutator, which will be used when resources are being fetched automatically. * @param mutator - Mutator mutates raw collection data from the database to certain format check {@link Mutator} for some built-in mutators. */ public setDefaultMutator(mutator: CollectionMutator<M>) { this.collectionMutator = mutator return this } /** * The **pushChanges()** method push incoming changes to the database. * @param changes - Received changes. * @param strict - Only allow LiveDMap changes to be pushed to the database. */ public async pushChanges(changes: ChangeList, strict = true) { if (!changes.isLive && strict) { throw Error( `${ConsoleColour.BGRED}${ConsoleColour.BOLD}only changes from LiveDMap are pushable by default.${ConsoleColour.RESET} \n${ConsoleColour.BGYELLOW}Please do not push cached data to the database.${ConsoleColour.RESET}` ) } return await this.verifyChanges(await this.handleChanges(changes.changes)) } /** * The **makeReferableEntities()** method converts fetched data to {@link ReferableMapEntity}. * @param data - Fetched data */ private makeReferableEntities<G extends DataType>( data: DataType ): Record<string, ReferableMapEntity<G>> { const refMap = new DMap<string, G>(data) const nMap: Record<string, ReferableMapEntity<G>> = {} as Record< string, ReferableMapEntity<G> > refMap.iterateSync((k, v) => { const id = v._docID delete v._docID const entity = new ReferableMapEntity<G>(v, id) entity.setSynthesized(false) nMap[k] = entity }) return nMap } /** * The **fetch()** method fetch the collection from database, mutate, and then save it as a cache. * @param [mutator] - Mutator mutates raw collection data from the database to certain format check {@link Mutator} for some built-in mutators. * @return Promise<T> */ public async fetch( mutator: CollectionMutator<M> = this.collectionMutator ): Promise<LiveDMap<string, ReferableMapEntity<T[keyof T]>>> { const loader = this.debug.loadingInfo( `fetching collection ${this.name} from database.` ) const data = await this.retrieveCollection() loader.succeed() const mutated = mutator(data) Files.writeFile(mutator(data), this.resourcePath) return new LiveDMap(this.makeReferableEntities(<T>mutated)) } /** * The **readFromCache()** method retrieve data from saved cache. * @param autoFetch - Automatically fetch data from the database if it is not presented. */ public async readFromCache( autoFetch = false ): Promise<DMap<string, ReferableMapEntity<T[keyof T]>> | null> { this.debug.info(`reading collection ${this.name} from cache.`) const data = Files.readFile<T>(this.resourcePath) if (!data) { this.debug.warn( `cached collection ${this.name} is not presented. autoFetch: ${ autoFetch ? 'true' : 'false' }` ) if (!autoFetch) return null return await this.fetch() } return new DMap(this.makeReferableEntities(data.content)) } /** * The **readFromCacheNoRef()** method retrieve data from saved cache without referable property. * @param autoFetch - Automatically fetch data from the database if it is not presented. */ public async readFromCacheNoRef( autoFetch = false ): Promise<DMap<string, T[keyof T]> | null> { this.debug.info(`reading collection ${this.name} from cache.`) const data = Files.readFile<T>(this.resourcePath) if (!data) { this.debug.warn( `cached collection ${this.name} is not presented. autoFetch: ${ autoFetch ? 'true' : 'false' }` ) if (!autoFetch) return null return await this.fetchNoRef() } return new DMap(this.clearEntityReference(data.content)) } /** * The **fetchNoRef()** method fetch the collection from database without referable property, mutate, and then save it as a cache. * @param [mutator] - Mutator mutates raw collection data from the database to certain format check {@link Mutator} for some built-in mutators. * @return Promise<T> */ public async fetchNoRef( mutator: CollectionMutator<M> = this.collectionMutator ): Promise<LiveDMap<string, T[keyof T]>> { const loader = this.debug.loadingInfo( `fetching collection ${this.name} from database.` ) const data = await this.retrieveCollection() loader.succeed() const mutated = mutator(data) Files.writeFile(mutator(data), this.resourcePath) return new LiveDMap(this.clearEntityReference(mutated)) } /** * The **makeReferableEntities()** method converts fetched data to {@link ReferableMapEntity}. * @param data - Fetched data */ protected clearEntityReference<G extends DataType>( data: G ): Record<string, G[keyof G]> { const clearedMap = new DMap<string, G[keyof G]>(data) clearedMap.iterateSync((k, v) => { if (v._docID) { delete v._docID } }) return clearedMap.getRecord() } }