UNPKG

@tucmc/hazel

Version:
253 lines (203 loc) 7.08 kB
import * as crypto from 'crypto' import fs from 'fs' import { hasher } from 'node-object-hash' import * as process from 'process' import type { DataType } from '../../util/data/DataType' import { DMap } from '../../util/data/DMap' import type { DataChanges } from '../../util/data/DMapUtil' import { FieldDelete } from '../../util/data/FieldDelete' import { LiveDMap } from '../../util/data/LiveDMap' import { ReferableMapEntity } from '../../util/data/ReferableEntity' import { Collection } from '../../util/database/Collection' import { ConsoleColour } from '../../util/debugger/Colour' import { Files } from '../../util/io/Files' import type { SimulatedDataPreset } from './SimulatedDataPresets' /** * The **SimulatedCollection<T>()** class extends from {@link Collection}. It shares similar properties except the data is simulated locally. * @extends Collection * @category Built-in */ export class SimulatedCollection<T extends DataType> extends Collection< T, DMap<string, T[keyof T]>, null > { private readonly simulatedContent: DMap<string, T[keyof T]> constructor( name: string, preset: SimulatedDataPreset<T>, instructor?: (simulated: DMap<string, T[keyof T]>) => void ) { super(name) const MODE = process.env.RUNTIME_MODE if (MODE === 'PROD') { throw Error( `${ConsoleColour.RED}Simulated collection must not be used on PRODUCTION runtime.${ConsoleColour.RESET}` ) } const buildCache = this.loadBuildCache() if (buildCache) { this.simulatedContent = new DMap(buildCache.content) return } this.simulatedContent = new DMap<string, T[keyof T]>(preset) instructor && instructor(this.simulatedContent) this.saveBuildCache() } protected async handleChanges( changes: DataChanges[] ): Promise<DataChanges[]> { const object = new DMap(this.simulatedContent.getRecord()) changes.forEach((v) => { if (!v._docID) { v._docID = `simulated-${crypto.randomUUID()}` } switch (v.type) { case 'create': object.set(v._docID, <T[keyof T]>v.to) break case 'delete': object.remove(v._docID) break case 'update': { const parsedTo: DataType = {} const original = object.get(v._docID) if (!original) break Object.keys(v.to).forEach((k) => { const d = v.to[k] if (d instanceof FieldDelete) { delete original[k] return } parsedTo[k] = d }) object.set(v._docID, { ...original, ...parsedTo }) } } }) this.debug.info( `${ConsoleColour.BGYELLOW}pushing changes to the database...` ) Files.writeFile( object.getRecord(), `resource/collection/simulated/${this.name}` ) return changes } protected initInstance(collectionName: string): null { return null } protected retrieveCollection(): Promise<DMap<string, T[keyof T]>> { return Promise.resolve(new DMap({})) } protected collectionMutator = (d: DMap<string, T[keyof T]>) => d.getRecord() as DataType protected async verifyChanges(changes: DataChanges[]): Promise<boolean> { const data = this.loadBuildCache() if (!data) { return false } const allDocs = new DMap(data.content).map((k, v) => { return { id: k, ...v } }) const evalResult = changes.map((v) => { const possibleDbDoc = allDocs.filter((d) => d.id === v._docID) const dbDoc = possibleDbDoc[0] if (!dbDoc) { if (v.type === 'delete') { return { id: v._docID, report: [], reference: '--deleted--' } } return { id: v._docID, report: [{ present: '--document-not-existed--', expect: v.to }], reference: '--document-not-existed--' } } const mismatch: { key: string; present: any; expect: any }[] = [] Object.keys(v.to).forEach((k) => { const refVal = v.to[k] const dbD = dbDoc[k] // Validate deleted field if (refVal instanceof FieldDelete) { if (!dbD) return } const refValHash = hasher().hash(refVal) const dbDHash = hasher().hash(dbD) if (dbDHash !== refValHash) { mismatch.push({ key: k, present: dbD, expect: refVal }) } }) return { id: v._docID, report: mismatch, reference: dbDoc } }) const total = changes.length const missing = evalResult.filter((d) => d === null).length const passed = evalResult.filter( (d) => d !== null && d.report.length === 0 ).length const failed = evalResult.filter((d) => d !== null && d.report.length > 0) this.debug.info(`changes verified successfully (total ${total} entities)`) this.debug.info( `${ConsoleColour.GREEN}passed ${passed}, ${ConsoleColour.RED}failed ${failed.length},${ConsoleColour.RESET} missing ${missing}` ) if (failed.length > 0) { this.debug.warn( `${failed.length} changes results were found not updated. \nPlease check ${ConsoleColour.BOLD}./review/DEV/.verify_log.json${ConsoleColour.RESET}` ) fs.writeFileSync( `review/${process.env.RUNTIME_MODE}/.verify_log.json`, JSON.stringify(failed, null, 4) ) } return total === passed } private loadBuildCache() { return Files.readFile(`resource/collection/simulated/${this.name}`) } private saveBuildCache() { Files.writeFile( this.simulatedContent.getRecord(), `resource/collection/simulated/${this.name}` ) } private buildRef(): LiveDMap<string, ReferableMapEntity<T[keyof T]>> { const mutated = this.collectionMutator(this.simulatedContent) const refMap = new DMap(mutated) const nMap: LiveDMap<string, ReferableMapEntity<T[keyof T]>> = new LiveDMap< string, any >({}) refMap.iterateSync((k, v) => { let id = k if (v._docID) { id = v._docID } const entity = new ReferableMapEntity<T[keyof T]>(v, id) entity.setSynthesized(false) nMap.set(k, entity) }) return nMap } /** * @deprecated This method should not be used since Simulated Collections are built and saved locally. The method **fetch()** would be more versatile in this case. */ async readFromCache( autoFetch = false ): Promise<DMap<string, ReferableMapEntity<T[keyof T]>> | null> { return this.fetch() } async fetch(): Promise<DMap<string, ReferableMapEntity<T[keyof T]>>> { const loader = this.debug.loadingInfo('fetching collection from database.') loader.succeed() return this.buildRef() } async fetchNoRef(): Promise<DMap<string, T[keyof T]>> { const loader = this.debug.loadingInfo('fetching collection from database.') loader.succeed() const mutated = this.collectionMutator(this.simulatedContent) console.log(Object.keys(mutated)) return new DMap(this.clearEntityReference(mutated)) } }