where-clause-tool.js 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. import {Service} from '@e22m4u/js-service';
  2. import {getValueByPath} from '../utils/index.js';
  3. import {InvalidArgumentError} from '../errors/index.js';
  4. import {OperatorClauseTool} from './operator-clause-tool.js';
  5. /**
  6. * Where clause tool.
  7. *
  8. * @typedef {object|Function} WhereClause
  9. */
  10. export class WhereClauseTool extends Service {
  11. /**
  12. * Filter by where clause.
  13. *
  14. * @example
  15. * ```
  16. * const entities = [
  17. * {foo: 1, bar: 'a'},
  18. * {foo: 2, bar: 'b'},
  19. * {foo: 3, bar: 'b'},
  20. * {foo: 4, bar: 'b'},
  21. * ];
  22. *
  23. * const result = filterByWhereClause(entities, {
  24. * foo: {gt: 2},
  25. * bar: 'b',
  26. * });
  27. *
  28. * console.log(result);
  29. * // [
  30. * // {foo: 3, bar: 'b'},
  31. * // {foo: 4, bar: 'b'},
  32. * // ];
  33. *
  34. * ```
  35. *
  36. * @param {object[]} entities
  37. * @param {WhereClause|undefined} where
  38. * @returns {object[]}
  39. */
  40. filter(entities, where = undefined) {
  41. if (!Array.isArray(entities))
  42. throw new InvalidArgumentError(
  43. 'The first argument of WhereClauseTool.filter should be ' +
  44. 'an Array of Object, but %v given.',
  45. entities,
  46. );
  47. if (where == null) return entities;
  48. return entities.filter(this._createFilter(where));
  49. }
  50. /**
  51. * Create where filter.
  52. *
  53. * @param {WhereClause} whereClause
  54. * @returns {Function}
  55. */
  56. _createFilter(whereClause) {
  57. if (typeof whereClause === 'function') return whereClause;
  58. if (typeof whereClause !== 'object' || Array.isArray(whereClause))
  59. throw new InvalidArgumentError(
  60. 'The provided option "where" should be an Object, but %v given.',
  61. whereClause,
  62. );
  63. const keys = Object.keys(whereClause);
  64. return data => {
  65. if (typeof data !== 'object')
  66. throw new InvalidArgumentError(
  67. 'The first argument of WhereClauseTool.filter should be ' +
  68. 'an Array of Object, but %v given.',
  69. data,
  70. );
  71. return keys.every(key => {
  72. // AndClause (recursion)
  73. if (key === 'and' && key in whereClause) {
  74. const andClause = whereClause[key];
  75. if (Array.isArray(andClause))
  76. return andClause.every(clause => this._createFilter(clause)(data));
  77. // OrClause (recursion)
  78. } else if (key === 'or' && key in whereClause) {
  79. const orClause = whereClause[key];
  80. if (Array.isArray(orClause))
  81. return orClause.some(clause => this._createFilter(clause)(data));
  82. }
  83. // PropertiesClause (properties)
  84. const value = getValueByPath(data, key);
  85. const matcher = whereClause[key];
  86. // Property value is an array.
  87. if (Array.isArray(value)) {
  88. // {neq: ...}
  89. if (
  90. typeof matcher === 'object' &&
  91. matcher !== null &&
  92. 'neq' in matcher &&
  93. matcher.neq !== undefined
  94. ) {
  95. // The following condition is for the case where
  96. // we are querying with a neq filter, and when
  97. // the value is an empty array ([]).
  98. if (value.length === 0) return true;
  99. // The neq operator requires each element
  100. // of the array to be excluded.
  101. return value.every((el, index) => {
  102. const where = {};
  103. where[index] = matcher;
  104. return this._createFilter(where)({...value});
  105. });
  106. }
  107. // Requires one of an array elements to be match.
  108. return value.some((el, index) => {
  109. const where = {};
  110. where[index] = matcher;
  111. return this._createFilter(where)({...value});
  112. });
  113. }
  114. // Test property value.
  115. if (this._test(matcher, value)) return true;
  116. });
  117. };
  118. }
  119. /**
  120. * Value testing.
  121. *
  122. * @param {*} example
  123. * @param {*} value
  124. * @returns {boolean}
  125. */
  126. _test(example, value) {
  127. // Test null.
  128. if (example == null) {
  129. return value == null;
  130. }
  131. // Test RegExp.
  132. // noinspection ALL
  133. if (example instanceof RegExp) {
  134. if (typeof value === 'string') return !!value.match(example);
  135. return false;
  136. }
  137. // Operator clause.
  138. if (typeof example === 'object') {
  139. const operatorsTest = this.getService(OperatorClauseTool).testAll(
  140. example,
  141. value,
  142. );
  143. if (operatorsTest !== undefined) return operatorsTest;
  144. }
  145. // Not strict equality.
  146. return example == value;
  147. }
  148. /**
  149. * Validate where clause.
  150. *
  151. * @param {WhereClause|undefined} clause
  152. */
  153. static validateWhereClause(clause) {
  154. if (clause == null || typeof clause === 'function') return;
  155. if (typeof clause !== 'object' || Array.isArray(clause))
  156. throw new InvalidArgumentError(
  157. 'The provided option "where" should be an Object, but %v given.',
  158. clause,
  159. );
  160. }
  161. }