query.service.ts 7.6 KB

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