query.service.ts 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  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, condition): boolean {
  41. // Merge all condtions into searchObj
  42. let result = _.every(condition, (val, key) => {
  43. const propKeys = key.split('.');
  44. let nestedObj = data;
  45. _.forEach(propKeys, propKey => {
  46. nestedObj = nestedObj[propKey];
  47. });
  48. if (_.isObject(val)) {
  49. return this.hasMatchingProps(nestedObj, val);
  50. }
  51. return nestedObj === val;
  52. });
  53. return result
  54. }
  55. // Logic 2: Success: More superior version than Logic 1 since it can perform flat searches like {messageID : 1234}
  56. // without specifying its parent property's name. eg: {header.messageID: 1234}
  57. private filterByKeyValue(data, ...conditions): boolean {
  58. try {
  59. // Merge all conditions into searchObj
  60. let searchObj = Object.assign({}, ...conditions)
  61. let recordFound = true
  62. // Check for data type. Can actually remove this code if dont want. Not that important anyways
  63. if (typeof data !== 'object' || typeof searchObj !== 'object') {
  64. return false;
  65. }
  66. // Check data to see if it is within the date range.
  67. if (recordFound == true) {
  68. if (searchObj.hasOwnProperty("$dateRange")) {
  69. recordFound = this.filterByDateRange(data, searchObj.$dateRange)
  70. delete searchObj.$dateRange
  71. }
  72. }
  73. // Check data if there is any $msgTag property and match it
  74. if (recordFound == true) {
  75. if (searchObj.hasOwnProperty('$msgTag')) {
  76. let keyToExtract = '$msgTag';
  77. let [, extractedValue] = Object.entries(searchObj).find(([key]) => key === keyToExtract)
  78. let newObj = { [keyToExtract]: extractedValue };
  79. let oldKey = '$msgTag';
  80. let newKey = 'msgTag';
  81. let newPair = { [newKey]: newObj[oldKey] };
  82. delete newObj[oldKey];
  83. // console.log(newPair)
  84. recordFound = this.matchValues(data, newPair)
  85. delete searchObj.$msgTag
  86. }
  87. }
  88. // Check if the regular expression value matches any of the data string
  89. if (recordFound == true) {
  90. if (searchObj.hasOwnProperty("$regex")) {
  91. recordFound = this.filterViaRegex(data, searchObj.$regex)
  92. delete searchObj.$regex
  93. }
  94. }
  95. // Check if the key has parent key notation and then perform matching sequences. Eg : "header.appdata. etc etc"
  96. if (recordFound == true) {
  97. // check if key is header.is like 'propertyName1.propertyName2'
  98. let searchkey = Object.keys(searchObj)
  99. searchkey.every((key) => {
  100. if (key.includes('.')) {
  101. let condition = {
  102. key: searchObj[key]
  103. }
  104. this.hasMatchingProps(data, condition)
  105. delete searchObj[key]
  106. }
  107. })
  108. }
  109. // Check the rest of the key value pairs to see if the conditions are fulfilled(entries must matched)
  110. if (recordFound == true) {
  111. recordFound = this.matchValues(data, searchObj)
  112. }
  113. return recordFound
  114. }
  115. catch (e) {
  116. console.error(e.message)
  117. }
  118. }
  119. // Match the key values pair between conditions and the given data
  120. private matchValues(data, searchObj): boolean {
  121. let matchKeys = Object.keys(searchObj);
  122. let isMatchingObject = (object) => {
  123. return matchKeys.every((key) => {
  124. let lodashPath = key.replace(/\[(\w+)\]/g, '.$1').replace(/^\./, '');
  125. let objectValue = _.get(object, lodashPath);
  126. let searchValue = searchObj[key];
  127. if (Array.isArray(searchValue)) {
  128. // Check if any of the search values are included in the object value
  129. return searchValue.some((value) => {
  130. return Array.isArray(objectValue) ? objectValue.includes(value) : objectValue === value;
  131. });
  132. } else if (typeof searchValue === 'object' && typeof objectValue === 'object') {
  133. return isMatchingObject(objectValue);
  134. } else {
  135. return objectValue === searchValue;
  136. }
  137. });
  138. };
  139. let isObjectMatching = (object) => {
  140. if (typeof object !== 'object') {
  141. return false;
  142. }
  143. return isMatchingObject(object) || Object.values(object).some(isObjectMatching);
  144. };
  145. return isObjectMatching(data);
  146. }
  147. // Matching the regex args to see if it matches the data that is now converted to string. As long as partial match, it will return true
  148. private filterViaRegex(element: any, inquiry: any): boolean {
  149. // create a new regular expression to use regex.test
  150. const regex = new RegExp(inquiry);
  151. const hasMatchingSubstring = regex.test(JSON.stringify(element));
  152. return hasMatchingSubstring;
  153. }
  154. // Check if the data's date is within the date range provided and also the column in which the data is to be compared with
  155. private filterByDateRange(data: any, dateRange: DateRange): boolean {
  156. // Lodash implemetation to get the specific property of data
  157. let msgDate: string = get(data, dateRange.column)
  158. let date = new Date(msgDate)
  159. const start = new Date(dateRange.startDate);
  160. const end = new Date(dateRange.endDate);
  161. return date >= start && date <= end;
  162. }
  163. }
  164. // Entries that client will use. Subject to be improved later on
  165. export interface Conditions {
  166. $regex?: string,
  167. $dateRange?: DateRange,
  168. $msgTag?: string[],
  169. [key: string]: string | Date | DateRange | string[]
  170. }
  171. export interface DateRange {
  172. startDate: string | Date,
  173. endDate: string | Date,
  174. column: string
  175. }
  176. export interface Storage {
  177. type: string,
  178. address: string
  179. }