feathers-utils 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -5,10 +5,11 @@ const _merge = require('lodash/merge.js');
5
5
  const _isEmpty = require('lodash/isEmpty.js');
6
6
  const _get = require('lodash/get.js');
7
7
  const _has = require('lodash/has.js');
8
- const _isEqual = require('lodash/isEqual.js');
9
8
  const _set = require('lodash/set.js');
10
9
  const _uniqWith = require('lodash/uniqWith.js');
10
+ const fastEquals = require('fast-equals');
11
11
  const adapterCommons = require('@feathersjs/adapter-commons');
12
+ const _isEqual = require('lodash/isEqual.js');
12
13
  const commons = require('@feathersjs/commons');
13
14
  const feathersHooksCommon = require('feathers-hooks-common');
14
15
  const _debounce = require('lodash/debounce.js');
@@ -53,9 +54,10 @@ function mergeArrays(targetArr, sourceArr, handle, prependKey, actionOnEmptyInte
53
54
  return void 0;
54
55
  }
55
56
 
56
- const hasOwnProperty = (obj, key) => {
57
- return Object.prototype.hasOwnProperty.call(obj, key);
57
+ const hasOwnProperty = (obj, ...keys) => {
58
+ return keys.some((x) => Object.prototype.hasOwnProperty.call(obj, x));
58
59
  };
60
+
59
61
  function handleArray(target, source, key, options) {
60
62
  const targetVal = _get(target, key);
61
63
  const sourceVal = _get(source, key);
@@ -120,7 +122,7 @@ function handleCircular(target, source, prependKey, options) {
120
122
  };
121
123
  const targetVal = getTargetVal();
122
124
  const sourceVal = getSourceVal();
123
- if (_isEqual(targetVal, sourceVal)) {
125
+ if (fastEquals.deepEqual(targetVal, sourceVal)) {
124
126
  return;
125
127
  }
126
128
  if (defaultHandle === "source") {
@@ -156,17 +158,17 @@ function handleCircular(target, source, prependKey, options) {
156
158
  const targetHasIn = hasOwnProperty(targetVal, "$in");
157
159
  const $in = targetHasIn ? targetVal["$in"] : sourceVal["$in"];
158
160
  const otherVal = isTargetSimple ? targetVal : sourceVal;
159
- if ($in.length === 1 && _isEqual($in[0], otherVal)) {
161
+ if ($in.length === 1 && fastEquals.deepEqual($in[0], otherVal)) {
160
162
  _set(target, prependKey, otherVal);
161
163
  return;
162
164
  } else if (defaultHandle === "combine") {
163
- if (!$in.some((x) => _isEqual(x, otherVal))) {
165
+ if (!$in.some((x) => fastEquals.deepEqual(x, otherVal))) {
164
166
  $in.push(otherVal);
165
167
  }
166
168
  _set(target, `${prependKey}.$in`, $in);
167
169
  return;
168
170
  } else if (defaultHandle === "intersect") {
169
- if ($in.some((x) => _isEqual(x, otherVal))) {
171
+ if ($in.some((x) => fastEquals.deepEqual(x, otherVal))) {
170
172
  _set(target, prependKey, otherVal);
171
173
  } else {
172
174
  actionOnEmptyIntersect(target, source, prependKey);
@@ -183,7 +185,7 @@ function handleCircular(target, source, prependKey, options) {
183
185
  if (key === "$or") {
184
186
  if (defaultHandle === "combine") {
185
187
  const newVals = sourceVal.filter(
186
- (x) => !targetVal.some((y) => _isEqual(x, y))
188
+ (x) => !targetVal.some((y) => fastEquals.deepEqual(x, y))
187
189
  );
188
190
  targetVal.push(...newVals);
189
191
  } else if (defaultHandle === "intersect") {
@@ -217,7 +219,7 @@ function handleCircular(target, source, prependKey, options) {
217
219
  return;
218
220
  } else if (defaultHandle === "intersect") {
219
221
  const newVals = sourceVal.filter(
220
- (x) => !targetVal.some((y) => _isEqual(x, y))
222
+ (x) => !targetVal.some((y) => fastEquals.deepEqual(x, y))
221
223
  );
222
224
  targetVal.push(...newVals);
223
225
  return;
@@ -230,7 +232,7 @@ function handleCircular(target, source, prependKey, options) {
230
232
  return;
231
233
  } else if (defaultHandle === "intersect") {
232
234
  const $in = targetVal.filter(
233
- (x) => sourceVal.some((y) => _isEqual(x, y))
235
+ (x) => sourceVal.some((y) => fastEquals.deepEqual(x, y))
234
236
  );
235
237
  if ($in.length === 0) {
236
238
  actionOnEmptyIntersect(target, source, prependKey);
@@ -257,13 +259,10 @@ function handleCircular(target, source, prependKey, options) {
257
259
  }
258
260
  }
259
261
  function makeDefaultOptions$1(options) {
260
- options = options || {};
261
- options.defaultHandle = options.defaultHandle || "combine";
262
- options.useLogicalConjunction = Object.prototype.hasOwnProperty.call(
263
- options,
264
- "useLogicalConjunction"
265
- ) ? options.useLogicalConjunction : false;
266
- options.actionOnEmptyIntersect = options.actionOnEmptyIntersect || (() => {
262
+ options ?? (options = {});
263
+ options.defaultHandle ?? (options.defaultHandle = "combine");
264
+ options.useLogicalConjunction ?? (options.useLogicalConjunction = false);
265
+ options.actionOnEmptyIntersect ?? (options.actionOnEmptyIntersect = () => {
267
266
  throw new errors.Forbidden("You're not allowed to make this request");
268
267
  });
269
268
  options.handle = options.handle || {};
@@ -272,12 +271,14 @@ function makeDefaultOptions$1(options) {
272
271
  }
273
272
  return options;
274
273
  }
275
- function moveProperty(source, target, key) {
276
- if (!Object.prototype.hasOwnProperty.call(source, key)) {
277
- return;
278
- }
279
- target[key] = source[key];
280
- delete source[key];
274
+ function moveProperty(from, to, ...keys) {
275
+ keys.forEach((key) => {
276
+ if (!hasOwnProperty(from, key)) {
277
+ return;
278
+ }
279
+ to[key] = from[key];
280
+ delete from[key];
281
+ });
281
282
  }
282
283
  function getParentProp(target, path) {
283
284
  if (path.length <= 1) {
@@ -311,7 +312,41 @@ function arrayWithoutDuplicates(target) {
311
312
  if (!target || !Array.isArray(target)) {
312
313
  return target;
313
314
  }
314
- return _uniqWith(target, _isEqual);
315
+ return _uniqWith(target, fastEquals.deepEqual);
316
+ }
317
+ function isQueryMoreExplicitThanQuery(target, source) {
318
+ if (!target || !source) {
319
+ return;
320
+ }
321
+ const targetKeys = Object.keys(target);
322
+ const sourceKeys = Object.keys(source);
323
+ if (!sourceKeys.length) {
324
+ return target;
325
+ }
326
+ if (!targetKeys.length) {
327
+ return source;
328
+ }
329
+ if (targetKeys.every((key) => fastEquals.deepEqual(target[key], source[key]))) {
330
+ return source;
331
+ }
332
+ if (sourceKeys.every((key) => fastEquals.deepEqual(target[key], source[key]))) {
333
+ return target;
334
+ }
335
+ return;
336
+ }
337
+ function areQueriesOverlapping(target, source) {
338
+ if (!target || !source) {
339
+ return false;
340
+ }
341
+ const targetKeys = Object.keys(target);
342
+ const sourceKeys = Object.keys(source);
343
+ if (!sourceKeys.length || !targetKeys.length) {
344
+ return false;
345
+ }
346
+ if (targetKeys.some((x) => sourceKeys.includes(x)) || sourceKeys.some((x) => targetKeys.includes(x))) {
347
+ return true;
348
+ }
349
+ return false;
315
350
  }
316
351
 
317
352
  function filterQuery(query, _options) {
@@ -337,16 +372,15 @@ function filterQuery(query, _options) {
337
372
  return adapterCommons.filterQuery(query, options);
338
373
  }
339
374
 
340
- function mergeQuery(target, source, options) {
341
- const fullOptions = makeDefaultOptions$1(options);
375
+ function mergeQuery(target, source, _options) {
376
+ const options = makeDefaultOptions$1(_options);
342
377
  const { filters: targetFilters, query: targetQuery } = filterQuery(target, {
343
- operators: fullOptions.operators,
344
- filters: fullOptions.filters,
345
- service: fullOptions.service
378
+ operators: options.operators,
379
+ filters: options.filters,
380
+ service: options.service
346
381
  });
347
- moveProperty(targetFilters, targetQuery, "$or");
348
- moveProperty(targetFilters, targetQuery, "$and");
349
- if (target.$limit) {
382
+ moveProperty(targetFilters, targetQuery, "$or", "$and");
383
+ if ("$limit" in target) {
350
384
  targetFilters.$limit = target.$limit;
351
385
  }
352
386
  let {
@@ -354,27 +388,40 @@ function mergeQuery(target, source, options) {
354
388
  filters: sourceFilters,
355
389
  query: sourceQuery
356
390
  } = filterQuery(source, {
357
- operators: fullOptions.operators,
358
- filters: fullOptions.filters,
359
- service: fullOptions.service
391
+ operators: options.operators,
392
+ filters: options.filters,
393
+ service: options.service
360
394
  });
361
- moveProperty(sourceFilters, sourceQuery, "$or");
362
- moveProperty(sourceFilters, sourceQuery, "$and");
395
+ moveProperty(sourceFilters, sourceQuery, "$or", "$and");
363
396
  if (source.$limit) {
364
397
  sourceFilters.$limit = source.$limit;
365
398
  }
366
- if (target && !Object.prototype.hasOwnProperty.call(target, "$limit") && Object.prototype.hasOwnProperty.call(targetFilters, "$limit")) {
399
+ if (target && !hasOwnProperty(target, "$limit") && hasOwnProperty(targetFilters, "$limit")) {
367
400
  delete targetFilters.$limit;
368
401
  }
369
- if (source && !Object.prototype.hasOwnProperty.call(source, "$limit") && Object.prototype.hasOwnProperty.call(sourceFilters, "$limit")) {
402
+ if (source && !hasOwnProperty(source, "$limit") && hasOwnProperty(sourceFilters, "$limit")) {
370
403
  delete sourceFilters.$limit;
371
404
  }
372
- handleArray(targetFilters, sourceFilters, ["$select"], fullOptions);
405
+ handleArray(targetFilters, sourceFilters, ["$select"], options);
373
406
  delete sourceFilters["$select"];
374
407
  _merge(targetFilters, sourceFilters);
375
- if (options?.useLogicalConjunction && (options.defaultHandle === "combine" || options.defaultHandle === "intersect") && !_isEmpty(targetQuery)) {
408
+ const subsetQuery = isQueryMoreExplicitThanQuery(targetQuery, sourceQuery);
409
+ if (options.defaultHandle === "intersect" && !!subsetQuery && !hasOwnProperty(targetQuery, "$or", "$and") && !hasOwnProperty(sourceQuery, "$or", "$and")) {
410
+ return {
411
+ ...targetFilters,
412
+ ...subsetQuery
413
+ };
414
+ }
415
+ if (options.defaultHandle === "intersect" && !areQueriesOverlapping(targetQuery, sourceQuery) && !hasOwnProperty(targetQuery, "$or", "$and") && !hasOwnProperty(sourceQuery, "$or", "$and")) {
416
+ return {
417
+ ...targetFilters,
418
+ ...targetQuery,
419
+ ...sourceQuery
420
+ };
421
+ }
422
+ if (options.useLogicalConjunction && (options.defaultHandle === "combine" || options.defaultHandle === "intersect") && !_isEmpty(targetQuery)) {
376
423
  const logicalOp = options.defaultHandle === "combine" ? "$or" : "$and";
377
- if (Object.prototype.hasOwnProperty.call(sourceQuery, logicalOp)) {
424
+ if (hasOwnProperty(sourceQuery, logicalOp)) {
378
425
  const andOr = sourceQuery[logicalOp];
379
426
  delete sourceQuery[logicalOp];
380
427
  andOr.push(sourceQuery);
@@ -386,10 +433,12 @@ function mergeQuery(target, source, options) {
386
433
  const keys = Object.keys(sourceQuery);
387
434
  for (let i = 0, n = keys.length; i < n; i++) {
388
435
  const key = keys[i];
389
- handleCircular(targetQuery, sourceQuery, [key], fullOptions);
436
+ handleCircular(targetQuery, sourceQuery, [key], options);
390
437
  }
391
- const result = Object.assign({}, targetFilters, targetQuery);
392
- return result;
438
+ return {
439
+ ...targetFilters,
440
+ ...targetQuery
441
+ };
393
442
  }
394
443
 
395
444
  const getItemsIsArray = (context, options) => {
@@ -413,7 +462,7 @@ const getItemsIsArray = (context, options) => {
413
462
  };
414
463
 
415
464
  const getPaginate = (context) => {
416
- if (Object.prototype.hasOwnProperty.call(context.params, "paginate")) {
465
+ if (hasOwnProperty(context.params, "paginate")) {
417
466
  return context.params.paginate || void 0;
418
467
  }
419
468
  if (context.params.paginate === false) {
@@ -717,12 +766,10 @@ function removeRelated({
717
766
 
718
767
  const makeOptions = (options) => {
719
768
  options = options || {};
720
- return Object.assign(
721
- {
722
- wait: true
723
- },
724
- options
725
- );
769
+ return {
770
+ wait: true,
771
+ ...options
772
+ };
726
773
  };
727
774
  const runPerItem = (actionPerItem, _options) => {
728
775
  const options = makeOptions(_options);
@@ -746,11 +793,10 @@ const defaultOptions = {
746
793
  overwrite: true
747
794
  };
748
795
  function setData(from, to, _options) {
749
- const options = Object.assign(
750
- {},
751
- defaultOptions,
752
- _options
753
- );
796
+ const options = {
797
+ ...defaultOptions,
798
+ ..._options
799
+ };
754
800
  return (context) => {
755
801
  if (shouldSkip("setData", context)) {
756
802
  return context;
package/dist/index.d.ts CHANGED
@@ -145,7 +145,14 @@ interface MergeQueryOptions<T> extends FilterQueryOptions<T> {
145
145
 
146
146
  declare function mergeArrays<T>(targetArr: T[], sourceArr: T[], handle: Handle, prependKey?: Path, actionOnEmptyIntersect?: ActionOnEmptyIntersect): T[] | undefined;
147
147
 
148
- declare function mergeQuery<T>(target: Query, source: Query, options?: Partial<MergeQueryOptions<T>>): Query;
148
+ /**
149
+ * Merges two queries into one.
150
+ * @param target Query to be merged into
151
+ * @param source Query to be merged from
152
+ * @param _options
153
+ * @returns Query
154
+ */
155
+ declare function mergeQuery<T = any>(target: Query, source: Query, _options?: Partial<MergeQueryOptions<T>>): Query;
149
156
 
150
157
  /**
151
158
  * util to get paginate options from context
package/dist/index.mjs CHANGED
@@ -3,10 +3,11 @@ import _merge from 'lodash/merge.js';
3
3
  import _isEmpty from 'lodash/isEmpty.js';
4
4
  import _get from 'lodash/get.js';
5
5
  import _has from 'lodash/has.js';
6
- import _isEqual from 'lodash/isEqual.js';
7
6
  import _set from 'lodash/set.js';
8
7
  import _uniqWith from 'lodash/uniqWith.js';
8
+ import { deepEqual } from 'fast-equals';
9
9
  import { filterQuery as filterQuery$1 } from '@feathersjs/adapter-commons';
10
+ import _isEqual from 'lodash/isEqual.js';
10
11
  import { _ } from '@feathersjs/commons';
11
12
  import { checkContext } from 'feathers-hooks-common';
12
13
  import _debounce from 'lodash/debounce.js';
@@ -51,9 +52,10 @@ function mergeArrays(targetArr, sourceArr, handle, prependKey, actionOnEmptyInte
51
52
  return void 0;
52
53
  }
53
54
 
54
- const hasOwnProperty = (obj, key) => {
55
- return Object.prototype.hasOwnProperty.call(obj, key);
55
+ const hasOwnProperty = (obj, ...keys) => {
56
+ return keys.some((x) => Object.prototype.hasOwnProperty.call(obj, x));
56
57
  };
58
+
57
59
  function handleArray(target, source, key, options) {
58
60
  const targetVal = _get(target, key);
59
61
  const sourceVal = _get(source, key);
@@ -118,7 +120,7 @@ function handleCircular(target, source, prependKey, options) {
118
120
  };
119
121
  const targetVal = getTargetVal();
120
122
  const sourceVal = getSourceVal();
121
- if (_isEqual(targetVal, sourceVal)) {
123
+ if (deepEqual(targetVal, sourceVal)) {
122
124
  return;
123
125
  }
124
126
  if (defaultHandle === "source") {
@@ -154,17 +156,17 @@ function handleCircular(target, source, prependKey, options) {
154
156
  const targetHasIn = hasOwnProperty(targetVal, "$in");
155
157
  const $in = targetHasIn ? targetVal["$in"] : sourceVal["$in"];
156
158
  const otherVal = isTargetSimple ? targetVal : sourceVal;
157
- if ($in.length === 1 && _isEqual($in[0], otherVal)) {
159
+ if ($in.length === 1 && deepEqual($in[0], otherVal)) {
158
160
  _set(target, prependKey, otherVal);
159
161
  return;
160
162
  } else if (defaultHandle === "combine") {
161
- if (!$in.some((x) => _isEqual(x, otherVal))) {
163
+ if (!$in.some((x) => deepEqual(x, otherVal))) {
162
164
  $in.push(otherVal);
163
165
  }
164
166
  _set(target, `${prependKey}.$in`, $in);
165
167
  return;
166
168
  } else if (defaultHandle === "intersect") {
167
- if ($in.some((x) => _isEqual(x, otherVal))) {
169
+ if ($in.some((x) => deepEqual(x, otherVal))) {
168
170
  _set(target, prependKey, otherVal);
169
171
  } else {
170
172
  actionOnEmptyIntersect(target, source, prependKey);
@@ -181,7 +183,7 @@ function handleCircular(target, source, prependKey, options) {
181
183
  if (key === "$or") {
182
184
  if (defaultHandle === "combine") {
183
185
  const newVals = sourceVal.filter(
184
- (x) => !targetVal.some((y) => _isEqual(x, y))
186
+ (x) => !targetVal.some((y) => deepEqual(x, y))
185
187
  );
186
188
  targetVal.push(...newVals);
187
189
  } else if (defaultHandle === "intersect") {
@@ -215,7 +217,7 @@ function handleCircular(target, source, prependKey, options) {
215
217
  return;
216
218
  } else if (defaultHandle === "intersect") {
217
219
  const newVals = sourceVal.filter(
218
- (x) => !targetVal.some((y) => _isEqual(x, y))
220
+ (x) => !targetVal.some((y) => deepEqual(x, y))
219
221
  );
220
222
  targetVal.push(...newVals);
221
223
  return;
@@ -228,7 +230,7 @@ function handleCircular(target, source, prependKey, options) {
228
230
  return;
229
231
  } else if (defaultHandle === "intersect") {
230
232
  const $in = targetVal.filter(
231
- (x) => sourceVal.some((y) => _isEqual(x, y))
233
+ (x) => sourceVal.some((y) => deepEqual(x, y))
232
234
  );
233
235
  if ($in.length === 0) {
234
236
  actionOnEmptyIntersect(target, source, prependKey);
@@ -255,13 +257,10 @@ function handleCircular(target, source, prependKey, options) {
255
257
  }
256
258
  }
257
259
  function makeDefaultOptions$1(options) {
258
- options = options || {};
259
- options.defaultHandle = options.defaultHandle || "combine";
260
- options.useLogicalConjunction = Object.prototype.hasOwnProperty.call(
261
- options,
262
- "useLogicalConjunction"
263
- ) ? options.useLogicalConjunction : false;
264
- options.actionOnEmptyIntersect = options.actionOnEmptyIntersect || (() => {
260
+ options ?? (options = {});
261
+ options.defaultHandle ?? (options.defaultHandle = "combine");
262
+ options.useLogicalConjunction ?? (options.useLogicalConjunction = false);
263
+ options.actionOnEmptyIntersect ?? (options.actionOnEmptyIntersect = () => {
265
264
  throw new Forbidden("You're not allowed to make this request");
266
265
  });
267
266
  options.handle = options.handle || {};
@@ -270,12 +269,14 @@ function makeDefaultOptions$1(options) {
270
269
  }
271
270
  return options;
272
271
  }
273
- function moveProperty(source, target, key) {
274
- if (!Object.prototype.hasOwnProperty.call(source, key)) {
275
- return;
276
- }
277
- target[key] = source[key];
278
- delete source[key];
272
+ function moveProperty(from, to, ...keys) {
273
+ keys.forEach((key) => {
274
+ if (!hasOwnProperty(from, key)) {
275
+ return;
276
+ }
277
+ to[key] = from[key];
278
+ delete from[key];
279
+ });
279
280
  }
280
281
  function getParentProp(target, path) {
281
282
  if (path.length <= 1) {
@@ -309,7 +310,41 @@ function arrayWithoutDuplicates(target) {
309
310
  if (!target || !Array.isArray(target)) {
310
311
  return target;
311
312
  }
312
- return _uniqWith(target, _isEqual);
313
+ return _uniqWith(target, deepEqual);
314
+ }
315
+ function isQueryMoreExplicitThanQuery(target, source) {
316
+ if (!target || !source) {
317
+ return;
318
+ }
319
+ const targetKeys = Object.keys(target);
320
+ const sourceKeys = Object.keys(source);
321
+ if (!sourceKeys.length) {
322
+ return target;
323
+ }
324
+ if (!targetKeys.length) {
325
+ return source;
326
+ }
327
+ if (targetKeys.every((key) => deepEqual(target[key], source[key]))) {
328
+ return source;
329
+ }
330
+ if (sourceKeys.every((key) => deepEqual(target[key], source[key]))) {
331
+ return target;
332
+ }
333
+ return;
334
+ }
335
+ function areQueriesOverlapping(target, source) {
336
+ if (!target || !source) {
337
+ return false;
338
+ }
339
+ const targetKeys = Object.keys(target);
340
+ const sourceKeys = Object.keys(source);
341
+ if (!sourceKeys.length || !targetKeys.length) {
342
+ return false;
343
+ }
344
+ if (targetKeys.some((x) => sourceKeys.includes(x)) || sourceKeys.some((x) => targetKeys.includes(x))) {
345
+ return true;
346
+ }
347
+ return false;
313
348
  }
314
349
 
315
350
  function filterQuery(query, _options) {
@@ -335,16 +370,15 @@ function filterQuery(query, _options) {
335
370
  return filterQuery$1(query, options);
336
371
  }
337
372
 
338
- function mergeQuery(target, source, options) {
339
- const fullOptions = makeDefaultOptions$1(options);
373
+ function mergeQuery(target, source, _options) {
374
+ const options = makeDefaultOptions$1(_options);
340
375
  const { filters: targetFilters, query: targetQuery } = filterQuery(target, {
341
- operators: fullOptions.operators,
342
- filters: fullOptions.filters,
343
- service: fullOptions.service
376
+ operators: options.operators,
377
+ filters: options.filters,
378
+ service: options.service
344
379
  });
345
- moveProperty(targetFilters, targetQuery, "$or");
346
- moveProperty(targetFilters, targetQuery, "$and");
347
- if (target.$limit) {
380
+ moveProperty(targetFilters, targetQuery, "$or", "$and");
381
+ if ("$limit" in target) {
348
382
  targetFilters.$limit = target.$limit;
349
383
  }
350
384
  let {
@@ -352,27 +386,40 @@ function mergeQuery(target, source, options) {
352
386
  filters: sourceFilters,
353
387
  query: sourceQuery
354
388
  } = filterQuery(source, {
355
- operators: fullOptions.operators,
356
- filters: fullOptions.filters,
357
- service: fullOptions.service
389
+ operators: options.operators,
390
+ filters: options.filters,
391
+ service: options.service
358
392
  });
359
- moveProperty(sourceFilters, sourceQuery, "$or");
360
- moveProperty(sourceFilters, sourceQuery, "$and");
393
+ moveProperty(sourceFilters, sourceQuery, "$or", "$and");
361
394
  if (source.$limit) {
362
395
  sourceFilters.$limit = source.$limit;
363
396
  }
364
- if (target && !Object.prototype.hasOwnProperty.call(target, "$limit") && Object.prototype.hasOwnProperty.call(targetFilters, "$limit")) {
397
+ if (target && !hasOwnProperty(target, "$limit") && hasOwnProperty(targetFilters, "$limit")) {
365
398
  delete targetFilters.$limit;
366
399
  }
367
- if (source && !Object.prototype.hasOwnProperty.call(source, "$limit") && Object.prototype.hasOwnProperty.call(sourceFilters, "$limit")) {
400
+ if (source && !hasOwnProperty(source, "$limit") && hasOwnProperty(sourceFilters, "$limit")) {
368
401
  delete sourceFilters.$limit;
369
402
  }
370
- handleArray(targetFilters, sourceFilters, ["$select"], fullOptions);
403
+ handleArray(targetFilters, sourceFilters, ["$select"], options);
371
404
  delete sourceFilters["$select"];
372
405
  _merge(targetFilters, sourceFilters);
373
- if (options?.useLogicalConjunction && (options.defaultHandle === "combine" || options.defaultHandle === "intersect") && !_isEmpty(targetQuery)) {
406
+ const subsetQuery = isQueryMoreExplicitThanQuery(targetQuery, sourceQuery);
407
+ if (options.defaultHandle === "intersect" && !!subsetQuery && !hasOwnProperty(targetQuery, "$or", "$and") && !hasOwnProperty(sourceQuery, "$or", "$and")) {
408
+ return {
409
+ ...targetFilters,
410
+ ...subsetQuery
411
+ };
412
+ }
413
+ if (options.defaultHandle === "intersect" && !areQueriesOverlapping(targetQuery, sourceQuery) && !hasOwnProperty(targetQuery, "$or", "$and") && !hasOwnProperty(sourceQuery, "$or", "$and")) {
414
+ return {
415
+ ...targetFilters,
416
+ ...targetQuery,
417
+ ...sourceQuery
418
+ };
419
+ }
420
+ if (options.useLogicalConjunction && (options.defaultHandle === "combine" || options.defaultHandle === "intersect") && !_isEmpty(targetQuery)) {
374
421
  const logicalOp = options.defaultHandle === "combine" ? "$or" : "$and";
375
- if (Object.prototype.hasOwnProperty.call(sourceQuery, logicalOp)) {
422
+ if (hasOwnProperty(sourceQuery, logicalOp)) {
376
423
  const andOr = sourceQuery[logicalOp];
377
424
  delete sourceQuery[logicalOp];
378
425
  andOr.push(sourceQuery);
@@ -384,10 +431,12 @@ function mergeQuery(target, source, options) {
384
431
  const keys = Object.keys(sourceQuery);
385
432
  for (let i = 0, n = keys.length; i < n; i++) {
386
433
  const key = keys[i];
387
- handleCircular(targetQuery, sourceQuery, [key], fullOptions);
434
+ handleCircular(targetQuery, sourceQuery, [key], options);
388
435
  }
389
- const result = Object.assign({}, targetFilters, targetQuery);
390
- return result;
436
+ return {
437
+ ...targetFilters,
438
+ ...targetQuery
439
+ };
391
440
  }
392
441
 
393
442
  const getItemsIsArray = (context, options) => {
@@ -411,7 +460,7 @@ const getItemsIsArray = (context, options) => {
411
460
  };
412
461
 
413
462
  const getPaginate = (context) => {
414
- if (Object.prototype.hasOwnProperty.call(context.params, "paginate")) {
463
+ if (hasOwnProperty(context.params, "paginate")) {
415
464
  return context.params.paginate || void 0;
416
465
  }
417
466
  if (context.params.paginate === false) {
@@ -715,12 +764,10 @@ function removeRelated({
715
764
 
716
765
  const makeOptions = (options) => {
717
766
  options = options || {};
718
- return Object.assign(
719
- {
720
- wait: true
721
- },
722
- options
723
- );
767
+ return {
768
+ wait: true,
769
+ ...options
770
+ };
724
771
  };
725
772
  const runPerItem = (actionPerItem, _options) => {
726
773
  const options = makeOptions(_options);
@@ -744,11 +791,10 @@ const defaultOptions = {
744
791
  overwrite: true
745
792
  };
746
793
  function setData(from, to, _options) {
747
- const options = Object.assign(
748
- {},
749
- defaultOptions,
750
- _options
751
- );
794
+ const options = {
795
+ ...defaultOptions,
796
+ ..._options
797
+ };
752
798
  return (context) => {
753
799
  if (shouldSkip("setData", context)) {
754
800
  return context;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "feathers-utils",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Some utils for projects using '@feathersjs/feathers'",
5
5
  "author": "fratzinger",
6
6
  "repository": {
@@ -43,6 +43,7 @@
43
43
  "@feathersjs/commons": "^5.0.0",
44
44
  "@feathersjs/errors": "5.0.0",
45
45
  "@feathersjs/feathers": "5.0.0",
46
+ "fast-equals": "^5.0.1",
46
47
  "feathers-hooks-common": "^7.0.0",
47
48
  "lodash": "^4.17.21"
48
49
  },
@@ -11,12 +11,10 @@ const makeOptions = (
11
11
  options?: HookRunPerItemOptions
12
12
  ): Required<HookRunPerItemOptions> => {
13
13
  options = options || {};
14
- return Object.assign(
15
- {
16
- wait: true,
17
- },
18
- options
19
- );
14
+ return {
15
+ wait: true,
16
+ ...options,
17
+ };
20
18
  };
21
19
 
22
20
  /**
@@ -27,11 +27,10 @@ export function setData<H extends HookContext = HookContext>(
27
27
  to: PropertyPath,
28
28
  _options?: HookSetDataOptions
29
29
  ) {
30
- const options: Required<HookSetDataOptions> = Object.assign(
31
- {},
32
- defaultOptions,
33
- _options
34
- );
30
+ const options: Required<HookSetDataOptions> = {
31
+ ...defaultOptions,
32
+ ..._options,
33
+ };
35
34
  return (context: H) => {
36
35
  if (shouldSkip("setData", context)) {
37
36
  return context;
@@ -1,5 +1,6 @@
1
1
  import type { PaginationOptions } from "@feathersjs/adapter-commons";
2
2
  import type { HookContext } from "@feathersjs/feathers";
3
+ import { hasOwnProperty } from "./internal.utils";
3
4
 
4
5
  /**
5
6
  * util to get paginate options from context
@@ -10,7 +11,7 @@ import type { HookContext } from "@feathersjs/feathers";
10
11
  export const getPaginate = <H extends HookContext = HookContext>(
11
12
  context: H
12
13
  ): PaginationOptions | undefined => {
13
- if (Object.prototype.hasOwnProperty.call(context.params, "paginate")) {
14
+ if (hasOwnProperty(context.params, "paginate")) {
14
15
  return (context.params.paginate as PaginationOptions) || undefined;
15
16
  }
16
17
 
@@ -0,0 +1,6 @@
1
+ export const hasOwnProperty = (
2
+ obj: Record<string, unknown>,
3
+ ...keys: string[]
4
+ ): boolean => {
5
+ return keys.some((x) => Object.prototype.hasOwnProperty.call(obj, x));
6
+ };
@@ -2,30 +2,39 @@ import _merge from "lodash/merge.js";
2
2
  import _isEmpty from "lodash/isEmpty.js";
3
3
  import type { Query } from "@feathersjs/feathers";
4
4
  import {
5
+ areQueriesOverlapping,
5
6
  handleArray,
6
7
  handleCircular,
8
+ isQueryMoreExplicitThanQuery,
7
9
  makeDefaultOptions,
8
10
  moveProperty,
9
11
  } from "./utils";
10
12
  import type { MergeQueryOptions } from "./types";
11
13
  import { filterQuery } from "../filterQuery";
12
-
13
- export function mergeQuery<T>(
14
+ import { hasOwnProperty } from "../internal.utils";
15
+
16
+ /**
17
+ * Merges two queries into one.
18
+ * @param target Query to be merged into
19
+ * @param source Query to be merged from
20
+ * @param _options
21
+ * @returns Query
22
+ */
23
+ export function mergeQuery<T = any>(
14
24
  target: Query,
15
25
  source: Query,
16
- options?: Partial<MergeQueryOptions<T>>
26
+ _options?: Partial<MergeQueryOptions<T>>
17
27
  ): Query {
18
- const fullOptions = makeDefaultOptions(options);
28
+ const options = makeDefaultOptions(_options);
19
29
  const { filters: targetFilters, query: targetQuery } = filterQuery(target, {
20
- operators: fullOptions.operators,
21
- filters: fullOptions.filters,
22
- service: fullOptions.service,
30
+ operators: options.operators,
31
+ filters: options.filters,
32
+ service: options.service,
23
33
  });
24
34
 
25
- moveProperty(targetFilters, targetQuery, "$or");
26
- moveProperty(targetFilters, targetQuery, "$and");
35
+ moveProperty(targetFilters, targetQuery, "$or", "$and");
27
36
 
28
- if (target.$limit) {
37
+ if ("$limit" in target) {
29
38
  targetFilters.$limit = target.$limit;
30
39
  }
31
40
 
@@ -34,13 +43,12 @@ export function mergeQuery<T>(
34
43
  filters: sourceFilters,
35
44
  query: sourceQuery,
36
45
  } = filterQuery(source, {
37
- operators: fullOptions.operators,
38
- filters: fullOptions.filters,
39
- service: fullOptions.service,
46
+ operators: options.operators,
47
+ filters: options.filters,
48
+ service: options.service,
40
49
  });
41
50
 
42
- moveProperty(sourceFilters, sourceQuery, "$or");
43
- moveProperty(sourceFilters, sourceQuery, "$and");
51
+ moveProperty(sourceFilters, sourceQuery, "$or", "$and");
44
52
 
45
53
  if (source.$limit) {
46
54
  sourceFilters.$limit = source.$limit;
@@ -50,37 +58,64 @@ export function mergeQuery<T>(
50
58
 
51
59
  if (
52
60
  target &&
53
- !Object.prototype.hasOwnProperty.call(target, "$limit") &&
54
- Object.prototype.hasOwnProperty.call(targetFilters, "$limit")
61
+ !hasOwnProperty(target, "$limit") &&
62
+ hasOwnProperty(targetFilters, "$limit")
55
63
  ) {
56
64
  delete targetFilters.$limit;
57
65
  }
58
66
 
59
67
  if (
60
68
  source &&
61
- !Object.prototype.hasOwnProperty.call(source, "$limit") &&
62
- Object.prototype.hasOwnProperty.call(sourceFilters, "$limit")
69
+ !hasOwnProperty(source, "$limit") &&
70
+ hasOwnProperty(sourceFilters, "$limit")
63
71
  ) {
64
72
  delete sourceFilters.$limit;
65
73
  }
66
74
 
67
- handleArray(targetFilters, sourceFilters, ["$select"], fullOptions);
75
+ handleArray(targetFilters, sourceFilters, ["$select"], options);
68
76
  // remaining filters
69
77
  delete sourceFilters["$select"];
70
78
  _merge(targetFilters, sourceFilters);
71
79
 
72
80
  //#endregion
73
81
 
82
+ const subsetQuery = isQueryMoreExplicitThanQuery(targetQuery, sourceQuery);
83
+
84
+ if (
85
+ options.defaultHandle === "intersect" &&
86
+ !!subsetQuery &&
87
+ !hasOwnProperty(targetQuery, "$or", "$and") &&
88
+ !hasOwnProperty(sourceQuery, "$or", "$and")
89
+ ) {
90
+ return {
91
+ ...targetFilters,
92
+ ...subsetQuery,
93
+ };
94
+ }
95
+
96
+ if (
97
+ options.defaultHandle === "intersect" &&
98
+ !areQueriesOverlapping(targetQuery, sourceQuery) &&
99
+ !hasOwnProperty(targetQuery, "$or", "$and") &&
100
+ !hasOwnProperty(sourceQuery, "$or", "$and")
101
+ ) {
102
+ return {
103
+ ...targetFilters,
104
+ ...targetQuery,
105
+ ...sourceQuery,
106
+ };
107
+ }
108
+
74
109
  //#region '$or' / '$and'
75
110
 
76
111
  if (
77
- options?.useLogicalConjunction &&
112
+ options.useLogicalConjunction &&
78
113
  (options.defaultHandle === "combine" ||
79
114
  options.defaultHandle === "intersect") &&
80
115
  !_isEmpty(targetQuery)
81
116
  ) {
82
117
  const logicalOp = options.defaultHandle === "combine" ? "$or" : "$and";
83
- if (Object.prototype.hasOwnProperty.call(sourceQuery, logicalOp)) {
118
+ if (hasOwnProperty(sourceQuery, logicalOp)) {
84
119
  // omit '$or'/'$and' and put all other props into '$or'/'$and'
85
120
  const andOr = sourceQuery[logicalOp] as unknown[];
86
121
  delete sourceQuery[logicalOp];
@@ -96,9 +131,11 @@ export function mergeQuery<T>(
96
131
  const keys = Object.keys(sourceQuery);
97
132
  for (let i = 0, n = keys.length; i < n; i++) {
98
133
  const key = keys[i];
99
- handleCircular(targetQuery, sourceQuery, [key], fullOptions);
134
+ handleCircular(targetQuery, sourceQuery, [key], options);
100
135
  }
101
- const result = Object.assign({}, targetFilters, targetQuery) as Query;
102
136
 
103
- return result;
137
+ return {
138
+ ...targetFilters,
139
+ ...targetQuery,
140
+ };
104
141
  }
@@ -2,20 +2,15 @@ import { Forbidden } from "@feathersjs/errors";
2
2
  import _get from "lodash/get.js";
3
3
  import _has from "lodash/has.js";
4
4
  import _isEmpty from "lodash/isEmpty.js";
5
- import _isEqual from "lodash/isEqual.js";
6
5
 
7
6
  import _set from "lodash/set.js";
8
7
  import _uniqWith from "lodash/uniqWith.js";
9
8
  import type { Path } from "../../typesInternal";
10
9
  import { mergeArrays } from "./mergeArrays";
11
10
  import type { Handle, MergeQueryOptions } from "./types";
12
-
13
- export const hasOwnProperty = (
14
- obj: Record<string, unknown>,
15
- key: string
16
- ): boolean => {
17
- return Object.prototype.hasOwnProperty.call(obj, key);
18
- };
11
+ import { deepEqual as _isEqual } from "fast-equals";
12
+ import type { Query } from "@feathersjs/feathers";
13
+ import { hasOwnProperty } from "../internal.utils";
19
14
 
20
15
  export function handleArray<T>(
21
16
  target: Record<string, unknown>,
@@ -265,19 +260,12 @@ export function handleCircular<T>(
265
260
  export function makeDefaultOptions<T>(
266
261
  options?: Partial<MergeQueryOptions<T>>
267
262
  ): MergeQueryOptions<T> {
268
- options = options || ({} as MergeQueryOptions<T>);
269
- options.defaultHandle = options.defaultHandle || "combine";
270
- options.useLogicalConjunction = Object.prototype.hasOwnProperty.call(
271
- options,
272
- "useLogicalConjunction"
273
- )
274
- ? options.useLogicalConjunction
275
- : false;
276
- options.actionOnEmptyIntersect =
277
- options.actionOnEmptyIntersect ||
278
- (() => {
279
- throw new Forbidden("You're not allowed to make this request");
280
- });
263
+ options ??= {} as MergeQueryOptions<T>;
264
+ options.defaultHandle ??= "combine";
265
+ options.useLogicalConjunction ??= false;
266
+ options.actionOnEmptyIntersect ??= () => {
267
+ throw new Forbidden("You're not allowed to make this request");
268
+ };
281
269
  options.handle = options.handle || {};
282
270
  if (options.defaultHandle === "intersect") {
283
271
  options.handle.$select = options.handle.$select || "intersectOrFull";
@@ -286,15 +274,17 @@ export function makeDefaultOptions<T>(
286
274
  }
287
275
 
288
276
  export function moveProperty(
289
- source: Record<string, any>,
290
- target: Record<string, any>,
291
- key: string
277
+ from: Record<string, any>,
278
+ to: Record<string, any>,
279
+ ...keys: string[]
292
280
  ): void {
293
- if (!Object.prototype.hasOwnProperty.call(source, key)) {
294
- return;
295
- }
296
- target[key] = source[key];
297
- delete source[key];
281
+ keys.forEach((key) => {
282
+ if (!hasOwnProperty(from, key)) {
283
+ return;
284
+ }
285
+ to[key] = from[key];
286
+ delete from[key];
287
+ });
298
288
  }
299
289
 
300
290
  export function getParentProp(target: Record<string, unknown>, path: Path) {
@@ -338,5 +328,68 @@ export function arrayWithoutDuplicates<T>(target: T[]): T[] {
338
328
  if (!target || !Array.isArray(target)) {
339
329
  return target;
340
330
  }
331
+
341
332
  return _uniqWith(target, _isEqual);
342
333
  }
334
+
335
+ /**
336
+ * Checks if one query is a superset of the target query
337
+ * @param target The target query
338
+ * @param source The source query
339
+ * @returns The query that is the superset of the other query, returns undefined otherwise
340
+ */
341
+ export function isQueryMoreExplicitThanQuery(
342
+ target: Query,
343
+ source: Query
344
+ ): Query | undefined {
345
+ if (!target || !source) {
346
+ return;
347
+ }
348
+
349
+ const targetKeys = Object.keys(target);
350
+ const sourceKeys = Object.keys(source);
351
+
352
+ // sourceQuery: {}; targetQuery: { something }
353
+ if (!sourceKeys.length) {
354
+ return target;
355
+ }
356
+
357
+ // sourceQuery: { something }; targetQuery: {}
358
+ if (!targetKeys.length) {
359
+ return source;
360
+ }
361
+
362
+ if (targetKeys.every((key) => _isEqual(target[key], source[key]))) {
363
+ // every property of target is exactly in source
364
+ return source;
365
+ }
366
+
367
+ if (sourceKeys.every((key) => _isEqual(target[key], source[key]))) {
368
+ // every property of source is exactly in target
369
+ return target;
370
+ }
371
+
372
+ return;
373
+ }
374
+
375
+ export function areQueriesOverlapping(target: Query, source: Query): boolean {
376
+ if (!target || !source) {
377
+ return false;
378
+ }
379
+
380
+ const targetKeys = Object.keys(target);
381
+ const sourceKeys = Object.keys(source);
382
+
383
+ if (!sourceKeys.length || !targetKeys.length) {
384
+ return false;
385
+ }
386
+
387
+ if (
388
+ targetKeys.some((x) => sourceKeys.includes(x)) ||
389
+ sourceKeys.some((x) => targetKeys.includes(x))
390
+ ) {
391
+ return true;
392
+ }
393
+
394
+ return false;
395
+ }