query.service.ts 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. import { isObject, get } from 'lodash'
  2. import { Observable, Subject, interval, map, of } from 'rxjs'
  3. import { DataPrepService } from './dataprep.service'
  4. import _ = require("lodash")
  5. export class SearchService {
  6. private dataPrepService: DataPrepService
  7. constructor() {
  8. this.dataPrepService = new DataPrepService()
  9. }
  10. public callFromOtherClass() {
  11. const t0 = performance.now()
  12. let i
  13. for (i = 0; i <= 6000000000; i++) {
  14. }
  15. const t1 = performance.now()
  16. const timeTakenInSeconds = (t1 - t0) / 1000;
  17. console.log(`Time taken: ${timeTakenInSeconds} seconds to run this function`);
  18. }
  19. public query(storage: Storage, ...conditions: Conditions[]): Observable<any> {
  20. let dataFromStorage: Subject<any> = new Subject()
  21. let filteredResult: Subject<any> = new Subject()
  22. // check if storaage is of type ObservableStorage
  23. if (typeof storage === "object" && storage !== null && "type" in storage && "ref" in storage) {
  24. let obsRef = storage.ref
  25. obsRef.subscribe((element) => {
  26. dataFromStorage.next(element)
  27. })
  28. } else {
  29. this.dataPrepService.loadObsData(storage, dataFromStorage)
  30. }
  31. this.filterFromObs(dataFromStorage, filteredResult, ...conditions)
  32. return filteredResult.pipe()
  33. }
  34. // Search and Filter: Pure Observables
  35. private filterFromObs(dataFromStorage: Subject<any>, filteredResult: Subject<any>, ...conditions: Conditions[]) {
  36. dataFromStorage.subscribe({
  37. next: element => {
  38. if (this.filterByKeyValue(element, ...conditions)) {
  39. filteredResult.next(element)
  40. } else {
  41. console.log(`${element.appData.msgId} does not match search criteria`)
  42. }
  43. }
  44. })
  45. }
  46. // Logic 1: Success. But argument must specifies header.messageID.... to search
  47. private hasMatchingProps(data, condition): boolean {
  48. // Merge all condtions into searchObj
  49. let result = _.every(condition, (val, key) => {
  50. const propKeys = key.split('.');
  51. let nestedObj = data;
  52. _.forEach(propKeys, propKey => {
  53. nestedObj = nestedObj[propKey];
  54. });
  55. if (_.isObject(val)) {
  56. return this.hasMatchingProps(nestedObj, val);
  57. }
  58. return nestedObj === val;
  59. });
  60. return result
  61. }
  62. // Logic 2: Success: More superior version than Logic 1 since it can perform flat searches like {messageID : 1234}
  63. // without specifying its parent property's name. eg: {header.messageID: 1234}
  64. private filterByKeyValue(data, ...conditions): boolean {
  65. try {
  66. // Merge all conditions into searchObj
  67. let searchObj = Object.assign({}, ...conditions)
  68. let recordFound = true
  69. // Check for data type. Can actually remove this code if dont want. Not that important anyways
  70. if (typeof data !== 'object' || typeof searchObj !== 'object') {
  71. return false;
  72. }
  73. // Check data to see if the given data is within the date range of the specified column
  74. if (recordFound == true) {
  75. if (searchObj.hasOwnProperty("$dateRange")) {
  76. recordFound = this.filterByDateRange(data, searchObj.$dateRange)
  77. delete searchObj.$dateRange
  78. }
  79. }
  80. // Check if the regular expression value matches any of the data string
  81. if (recordFound == true) {
  82. if (searchObj.hasOwnProperty("$regex")) {
  83. recordFound = this.filterViaRegex(data, searchObj.$regex)
  84. delete searchObj.$regex
  85. }
  86. }
  87. // Check if the key has parent key notation and then perform matching sequences. Eg : "header.appdata. etc etc"
  88. if (recordFound == true) {
  89. // check if key is header.is like 'propertyName1.propertyName2'
  90. let searchkey = Object.keys(searchObj)
  91. searchkey.every((key) => {
  92. if (key.includes('.')) {
  93. let condition = {
  94. key: searchObj[key]
  95. }
  96. this.hasMatchingProps(data, condition)
  97. delete searchObj[key]
  98. }
  99. })
  100. }
  101. // Check the rest of the key value pairs to see if the conditions are fulfilled(entries must matched)
  102. if (recordFound == true) {
  103. recordFound = this.matchValues(data, searchObj)
  104. }
  105. return recordFound
  106. }
  107. catch (e) {
  108. console.error(e.message)
  109. }
  110. }
  111. // Match the key values pair between conditions and the given data
  112. private matchValues(data, searchObj): boolean {
  113. let matchKeys = Object.keys(searchObj);
  114. let isMatchingObject = (object) => {
  115. return matchKeys.every((key) => {
  116. let lodashPath = key.replace(/\[(\w+)\]/g, '.$1').replace(/^\./, '');
  117. let objectValue = _.get(object, lodashPath);
  118. let searchValue = searchObj[key];
  119. if (Array.isArray(searchValue)) {
  120. // Check if any of the search values are included in the object value
  121. return searchValue.some((value) => {
  122. return Array.isArray(objectValue) ? objectValue.includes(value) : objectValue === value;
  123. });
  124. } else if (typeof searchValue === 'object' && typeof objectValue === 'object') {
  125. return isMatchingObject(objectValue);
  126. } else {
  127. return objectValue === searchValue;
  128. }
  129. });
  130. };
  131. let isObjectMatching = (object) => {
  132. if (typeof object !== 'object') {
  133. return false;
  134. }
  135. return isMatchingObject(object) || Object.values(object).some(isObjectMatching);
  136. };
  137. return isObjectMatching(data);
  138. }
  139. // 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
  140. private filterViaRegex(element: any, inquiry: any): boolean {
  141. // create a new regular expression to use regex.test
  142. const regex = new RegExp(inquiry);
  143. const hasMatchingSubstring = regex.test(JSON.stringify(element));
  144. return hasMatchingSubstring;
  145. }
  146. // 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
  147. private filterByDateRange(data: any, dateRange: DateRange): boolean {
  148. // Lodash implemetation to get the specific property of data
  149. let msgDate: string = get(data, dateRange.column)
  150. let date = new Date(msgDate)
  151. const start = new Date(dateRange.startDate);
  152. const end = new Date(dateRange.endDate);
  153. return date >= start && date <= end;
  154. }
  155. }
  156. // Entries that client will use. Subject to be improved later on
  157. export interface Conditions {
  158. $regex?: string,
  159. $dateRange?: DateRange,
  160. [key: string]: string | Date | DateRange | string[]
  161. }
  162. export interface DateRange {
  163. startDate: string | Date,
  164. endDate: string | Date,
  165. column: string
  166. }
  167. export interface StorageLocation {
  168. type: string,
  169. url: string
  170. }
  171. export interface ObservableStorage {
  172. type: "observable",
  173. ref: Observable<any>
  174. }
  175. export type Storage = ObservableStorage | StorageLocation