query.service.ts 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. import * as fs from 'fs'
  2. import { _, isObject, get } from 'lodash'
  3. import { Observable, Subject, interval, map, of } from 'rxjs'
  4. export class queryService {
  5. public query(storageAddress: Storage, ...conditions: Conditions[]): Observable<any> {
  6. let dataFromStorage: Subject<any> = new Subject()
  7. let filteredResult: Subject<any> = new Subject()
  8. this.loadObsData(storageAddress.address, dataFromStorage)
  9. this.filterFromObs(dataFromStorage, filteredResult, ...conditions)
  10. return filteredResult.pipe()
  11. }
  12. // Data preparations: Purely Observables
  13. private loadObsData(location: string, dataFromStorage: Subject<any>) {
  14. // Temporary version. More defined design will be implemented to cater for different storage locations
  15. let data = fs.readFileSync(location, 'utf-8')
  16. let dataJson = JSON.parse(data)
  17. let count = 0
  18. const intervalId = setInterval(() => {
  19. dataFromStorage.next(dataJson[count]);
  20. count++;
  21. if (count >= 100) {
  22. clearInterval(intervalId);
  23. dataFromStorage.complete();
  24. }
  25. }, 250)
  26. }
  27. // Search and Filter: Pure Observables. To be moved out to become a separate service again.
  28. private filterFromObs(dataFromStorage: Subject<any>, filteredResult: Subject<any>, ...conditions: Conditions[]) {
  29. dataFromStorage.subscribe({
  30. next: element => {
  31. if (this.filterByKeyValue(element, ...conditions)) {
  32. filteredResult.next(element)
  33. } else {
  34. // console.log(`${element.header.messageName} does not match search criteria`)
  35. }
  36. }
  37. })
  38. }
  39. // Logic 1: Success. But argument must specifies header.messageID.... to search
  40. private hasMatchingProps(data, ...conditions): boolean {
  41. // Merge all condtions into searchObj
  42. let searchObj = Object.assign({}, ...conditions)
  43. let result = _.every(searchObj, (val, key) => {
  44. const propKeys = key.split('.');
  45. let nestedObj = data;
  46. _.forEach(propKeys, propKey => {
  47. nestedObj = nestedObj[propKey];
  48. });
  49. if (_.isObject(val)) {
  50. return this.hasMatchingProps(nestedObj, val);
  51. }
  52. return nestedObj === val;
  53. });
  54. return result
  55. }
  56. // Logic 2: Success: More superior version than Logic 1 since it can perform flat searches like {messageID : 1234}
  57. // without specifying its parent property's name. eg: {header.messageID: 1234}
  58. private filterByKeyValue(data, ...conditions): boolean {
  59. // Merge all conditions into searchObj
  60. let searchObj = Object.assign({}, ...conditions)
  61. let dateCheck = true
  62. let regexCheck = true
  63. if (searchObj.hasOwnProperty("dateRange")) {
  64. dateCheck = this.filterByDateRange(data, searchObj.dateRange)
  65. // Must delete, otherwise the function below will attempt to match date range with the date property and it will inevitably returns false
  66. delete searchObj.dateRange
  67. }
  68. if (searchObj.hasOwnProperty("regex")) {
  69. dateCheck = this.filterViaRegex(data, searchObj.regex)
  70. // Must delete, otherwise the function below will attempt to match date range with the date property and it will inevitably returns false
  71. delete searchObj.regex
  72. }
  73. if (typeof data !== 'object' || typeof searchObj !== 'object') {
  74. return false;
  75. }
  76. if (dateCheck == true && regexCheck == true) {
  77. let matchKeys = Object.keys(searchObj);
  78. let isMatchingObject = (object) => {
  79. return matchKeys.every((key) => {
  80. let lodashPath = key.replace(/\[(\w+)\]/g, '.$1').replace(/^\./, '');
  81. let objectValue = _.get(object, lodashPath);
  82. let searchValue = searchObj[key];
  83. if (Array.isArray(searchValue) && key === 'msgTag') {
  84. // Check if any of the search values are included in the object value
  85. return searchValue.some((value) => {
  86. return Array.isArray(objectValue) ? objectValue.includes(value) : objectValue === value;
  87. });
  88. } else if (typeof searchValue === 'object' && typeof objectValue === 'object') {
  89. return isMatchingObject(objectValue);
  90. } else {
  91. return objectValue === searchValue;
  92. }
  93. });
  94. };
  95. let isObjectMatching = (object) => {
  96. if (typeof object !== 'object') {
  97. return false;
  98. }
  99. return isMatchingObject(object) || Object.values(object).some(isObjectMatching);
  100. };
  101. return isObjectMatching(data);
  102. } else {
  103. return false
  104. }
  105. /* This function first merges all the ...conditions objects into a single searchObj object using the Object.assign() method.
  106. It then checks whether both data and searchObj are of type object. If either one is not an object, the function returns false.
  107. Next, the function defines two helper functions: isMatchingObject and isObjectMatching. isMatchingObject takes an object and
  108. returns true if all the key-value pairs in searchObj are present in the object. isObjectMatching takes an object and returns
  109. true if the object itself or any of its nested objects satisfy the conditions specified in searchObj.
  110. The isMatchingObject function uses the every() method to iterate through each key in searchObj and check if the corresponding
  111. value in the object matches the value in searchObj. The function also uses the _.get() method from the Lodash library to get
  112. the value of nested object properties using a string path. If the value in searchObj or the object is an object itself,
  113. isMatchingObject calls itself recursively to check for nested properties.
  114. The isObjectMatching function first checks if the input object is of type object. If not, it returns false. If the object is an object,
  115. it checks whether the object or any of its nested objects satisfy the conditions in searchObj by calling isMatchingObject and isObjectMatching recursively.
  116. Finally, the hasKeyAndValue function returns the result of isObjectMatching(data), which is a boolean indicating whether data satisfies
  117. the conditions specified in searchObj.
  118. PS: this function is not my code. */
  119. }
  120. private filterViaRegex(element: any, inquiry: any): boolean {
  121. // create a new regular expression to use regex.test
  122. const regex = new RegExp(inquiry);
  123. const hasMatchingSubstring = regex.test(JSON.stringify(element));
  124. return hasMatchingSubstring;
  125. }
  126. private filterByDateRange(data: any, dateRange: DateRange): boolean {
  127. // Lodash implemetation to get the specific property of data
  128. let msgDate : string = get(data, 'data.data.appData.msgDateTime')
  129. // console.log(msgDate)
  130. const start = new Date(dateRange.startDate);
  131. const end = new Date(dateRange.endDate);
  132. const target = new Date(data.header.dateCreated);
  133. return target >= start && target <= end;
  134. }
  135. }
  136. // Entries that client will use. Subject to be improved later on
  137. export interface Conditions {
  138. _id?: string,
  139. appLogLocId?: string,
  140. msgId?: string,
  141. msgLogDateTime?: Date | string,
  142. msgDateTime?: Date | string,
  143. msgTag?: string[],
  144. msgPayload?: string,
  145. messageID?: string,
  146. regex?: string,
  147. dateRange?: DateRange
  148. }
  149. export interface DateRange {
  150. startDate?: string | Date,
  151. endDate?: string | Date
  152. }
  153. export interface Storage {
  154. type: string,
  155. address: string
  156. }