import * as fs from 'fs' import { _, isObject, get } from 'lodash' import { Observable, Subject, interval, map, of } from 'rxjs' export class queryService { public query(storageAddress: Storage, ...conditions: Conditions[]): Observable { let dataFromStorage: Subject = new Subject() let filteredResult: Subject = new Subject() this.loadObsData(storageAddress.address, dataFromStorage) this.filterFromObs(dataFromStorage, filteredResult, ...conditions) return filteredResult.pipe() } // Data preparations: Purely Observables private loadObsData(location: string, dataFromStorage: Subject) { // Temporary version. More defined design will be implemented to cater for different storage locations let data = fs.readFileSync(location, 'utf-8') let dataJson = JSON.parse(data) let count = 0 const intervalId = setInterval(() => { dataFromStorage.next(dataJson[count]); count++; if (count >= 100) { clearInterval(intervalId); dataFromStorage.complete(); } }, 250) } // Search and Filter: Pure Observables. To be moved out to become a separate service again. private filterFromObs(dataFromStorage: Subject, filteredResult: Subject, ...conditions: Conditions[]) { dataFromStorage.subscribe({ next: element => { if (this.filterByKeyValue(element, ...conditions)) { filteredResult.next(element) } else { // console.log(`${element.header.messageName} does not match search criteria`) } } }) } // Logic 1: Success. But argument must specifies header.messageID.... to search private hasMatchingProps(data, ...conditions): boolean { // Merge all condtions into searchObj let searchObj = Object.assign({}, ...conditions) let result = _.every(searchObj, (val, key) => { const propKeys = key.split('.'); let nestedObj = data; _.forEach(propKeys, propKey => { nestedObj = nestedObj[propKey]; }); if (_.isObject(val)) { return this.hasMatchingProps(nestedObj, val); } return nestedObj === val; }); return result } // Logic 2: Success: More superior version than Logic 1 since it can perform flat searches like {messageID : 1234} // without specifying its parent property's name. eg: {header.messageID: 1234} private filterByKeyValue(data, ...conditions): boolean { // Merge all conditions into searchObj let searchObj = Object.assign({}, ...conditions) let dateCheck = true let regexCheck = true if (searchObj.hasOwnProperty("dateRange")) { dateCheck = this.filterByDateRange(data, searchObj.dateRange) // Must delete, otherwise the function below will attempt to match date range with the date property and it will inevitably returns false delete searchObj.dateRange } if (searchObj.hasOwnProperty("regex")) { dateCheck = this.filterViaRegex(data, searchObj.regex) // Must delete, otherwise the function below will attempt to match date range with the date property and it will inevitably returns false delete searchObj.regex } if (typeof data !== 'object' || typeof searchObj !== 'object') { return false; } if (dateCheck == true && regexCheck == true) { let matchKeys = Object.keys(searchObj); let isMatchingObject = (object) => { return matchKeys.every((key) => { let lodashPath = key.replace(/\[(\w+)\]/g, '.$1').replace(/^\./, ''); let objectValue = _.get(object, lodashPath); let searchValue = searchObj[key]; if (Array.isArray(searchValue) && key === 'msgTag') { // Check if any of the search values are included in the object value return searchValue.some((value) => { return Array.isArray(objectValue) ? objectValue.includes(value) : objectValue === value; }); } else if (typeof searchValue === 'object' && typeof objectValue === 'object') { return isMatchingObject(objectValue); } else { return objectValue === searchValue; } }); }; let isObjectMatching = (object) => { if (typeof object !== 'object') { return false; } return isMatchingObject(object) || Object.values(object).some(isObjectMatching); }; return isObjectMatching(data); } else { return false } /* This function first merges all the ...conditions objects into a single searchObj object using the Object.assign() method. It then checks whether both data and searchObj are of type object. If either one is not an object, the function returns false. Next, the function defines two helper functions: isMatchingObject and isObjectMatching. isMatchingObject takes an object and returns true if all the key-value pairs in searchObj are present in the object. isObjectMatching takes an object and returns true if the object itself or any of its nested objects satisfy the conditions specified in searchObj. The isMatchingObject function uses the every() method to iterate through each key in searchObj and check if the corresponding value in the object matches the value in searchObj. The function also uses the _.get() method from the Lodash library to get the value of nested object properties using a string path. If the value in searchObj or the object is an object itself, isMatchingObject calls itself recursively to check for nested properties. The isObjectMatching function first checks if the input object is of type object. If not, it returns false. If the object is an object, it checks whether the object or any of its nested objects satisfy the conditions in searchObj by calling isMatchingObject and isObjectMatching recursively. Finally, the hasKeyAndValue function returns the result of isObjectMatching(data), which is a boolean indicating whether data satisfies the conditions specified in searchObj. PS: this function is not my code. */ } private filterViaRegex(element: any, inquiry: any): boolean { // create a new regular expression to use regex.test const regex = new RegExp(inquiry); const hasMatchingSubstring = regex.test(JSON.stringify(element)); return hasMatchingSubstring; } private filterByDateRange(data: any, dateRange: DateRange): boolean { // Lodash implemetation to get the specific property of data let msgDate : string = get(data, 'data.data.appData.msgDateTime') // console.log(msgDate) const start = new Date(dateRange.startDate); const end = new Date(dateRange.endDate); const target = new Date(data.header.dateCreated); return target >= start && target <= end; } } // Entries that client will use. Subject to be improved later on export interface Conditions { _id?: string, appLogLocId?: string, msgId?: string, msgLogDateTime?: Date | string, msgDateTime?: Date | string, msgTag?: string[], msgPayload?: string, messageID?: string, regex?: string, dateRange?: DateRange } export interface DateRange { startDate?: string | Date, endDate?: string | Date } export interface Storage { type: string, address: string }