query.service.ts 6.8 KB

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