Browse Source

lodash filtering implementation for nested entries

Enzo 1 year ago
parent
commit
c69996427f
2 changed files with 82 additions and 10 deletions
  1. 76 6
      services/query.service.ts
  2. 6 4
      test/test1.ts

+ 76 - 6
services/query.service.ts

@@ -1,12 +1,12 @@
 import * as fs from 'fs'
-import { filter, isMatch } from 'lodash'
+import { _, isObject } from 'lodash'
 import { Observable, Subject, interval, map, of } from 'rxjs'
 
 export class queryService {
     private dataFromStorage: Subject<any> = new Subject()
     private filteredResult: Subject<any> = new Subject()
 
-    public query(storageAddress: Storage, ...conditions: Entries[]) : Observable<any> {
+    public query(storageAddress: Storage, ...conditions: Entries[]): Observable<any> {
         this.loadObsData(storageAddress.address)
         this.filterFromObs(...conditions)
         return this.filteredResult.pipe()
@@ -14,6 +14,7 @@ export class queryService {
 
     // Data preparations: Purely Observables
     private loadObsData(location: string) {
+        //  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
@@ -27,20 +28,88 @@ export class queryService {
         }, 1000)
     }
 
-    // Search and Filter: Pure Observables. To be moved out to become a separate library again.
+    // Search and Filter: Pure Observables. To be moved out to become a separate service again.
     private filterFromObs(...conditions: Entries[]) {
         this.dataFromStorage.subscribe({
             next: element => {
-                if(isMatch(element, conditions)){
-                    // Logic to check if data meets the conditions, if so, put it into result.next{}
+                // Logic to check if data meets the conditions, if so, put it into result.next{}
+                if (this.hasKeyAndValue(element, ...conditions)) {
                     this.filteredResult.next(element)
+                } else {
+                    console.log(`${element.header.messageID} 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 nested properties
+    private hasKeyAndValue(data, ...conditions): boolean {
+        // Merge all condtions into searchObj
+        let searchObj = Object.assign({}, ...conditions)
+        if (typeof data !== 'object' || typeof searchObj !== 'object') {
+            return false;
+        }
+
+        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 (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);
+
+        /* 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. */
+    }
 }
 
+
+
 // Entries that client will use. Subject to be improved later on
 export interface Entries {
     _id?: string,
@@ -49,7 +118,8 @@ export interface Entries {
     msgLogDateTime?: Date | string,
     msgDateTime?: Date | string,
     msgTag?: string[],
-    msgPayload?: string
+    msgPayload?: string,
+    messageID: string
 }
 
 export interface Storage {

+ 6 - 4
test/test1.ts

@@ -1,6 +1,7 @@
 import { Observable } from "rxjs"
 import { queryService } from "../services/query.service"
 import { Entries, Storage } from "../services/query.service"
+import { _, isObject } from 'lodash'
 
 let query = new queryService()
 
@@ -9,12 +10,13 @@ let storageAddress: Storage = {
     address: "payload.json"
 }
 
-let conditions: Entries[] = [
-    { msgId: "1e4d25a0-f30f-4590-be24-d43f246cd8c9" }
+let conditions: any[] = [
+    { "msgDateTime": "2023-01-25T02:54:01.434Z" },
+    { "msgTag": "enterprise" }
 ]
-query.query(storageAddress, ...conditions).subscribe((element) => {console.log(element.header.messageID)})
+query.query(storageAddress, ...conditions).subscribe((element) => { console.log(`${element.header.messageName} is matched`) })
 
 // the key is to do it in one line. Client just pass 2 arguments, one is the location of the data, which could be file, sql or mongodb, and also
-// pass in the conditions of their search enquiries. We will aslo have to cater to dffernt file storage location to determine how to prep the
+// pass in the conditions of their search enquiries. We will aslo have to cater to different file storage location to determine how to prep the
 // data to be filtered