feathers-utils 10.1.0 → 10.3.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.
Files changed (48) hide show
  1. package/dist/{hooks-Bg5XWcV7.mjs → hooks-DpFQfcFa.mjs} +122 -59
  2. package/dist/hooks-DpFQfcFa.mjs.map +1 -0
  3. package/dist/hooks.d.mts +110 -36
  4. package/dist/hooks.mjs +5 -5
  5. package/dist/{index-DKA0E_ad.d.mts → index-C6MN6wag.d.mts} +181 -64
  6. package/dist/index.d.mts +3 -3
  7. package/dist/index.mjs +7 -7
  8. package/dist/{internal.utils-BMzV_-xp.mjs → internal.utils-BBB-b6Ud.mjs} +16 -2
  9. package/dist/internal.utils-BBB-b6Ud.mjs.map +1 -0
  10. package/dist/{mutate-result.util-Dqzepn1M.mjs → mutate-result.util-C0nY6L7i.mjs} +2 -2
  11. package/dist/{mutate-result.util-Dqzepn1M.mjs.map → mutate-result.util-C0nY6L7i.mjs.map} +1 -1
  12. package/dist/{predicates-puYa4nkf.mjs → predicates-NOnUyMic.mjs} +53 -53
  13. package/dist/{predicates-puYa4nkf.mjs.map → predicates-NOnUyMic.mjs.map} +1 -1
  14. package/dist/predicates.d.mts +18 -18
  15. package/dist/predicates.mjs +1 -1
  16. package/dist/{resolve-B9hRleHY.mjs → resolve-BflgIVD8.mjs} +2 -2
  17. package/dist/{resolve-B9hRleHY.mjs.map → resolve-BflgIVD8.mjs.map} +1 -1
  18. package/dist/resolvers.mjs +1 -1
  19. package/dist/{transform-result.hook-B65pTRJO.mjs → transform-result.hook-V2QYN2K0.mjs} +2 -2
  20. package/dist/{transform-result.hook-B65pTRJO.mjs.map → transform-result.hook-V2QYN2K0.mjs.map} +1 -1
  21. package/dist/transformers.mjs +3 -3
  22. package/dist/{utils-Br6DNQ1B.mjs → utils-DByCpAsf.mjs} +407 -146
  23. package/dist/utils-DByCpAsf.mjs.map +1 -0
  24. package/dist/utils.d.mts +2 -2
  25. package/dist/utils.mjs +4 -4
  26. package/package.json +1 -1
  27. package/src/common/index.ts +1 -0
  28. package/src/common/is-empty-object.ts +38 -0
  29. package/src/hooks/index.ts +3 -1
  30. package/src/hooks/mute-event/mute-event.hook.ts +56 -0
  31. package/src/hooks/set-query-defaults/set-query-defaults.hook.ts +37 -0
  32. package/src/hooks/soft-delete/soft-delete.hook.ts +17 -3
  33. package/src/predicates/index.ts +3 -3
  34. package/src/utils/add-to-query/add-to-query.util.ts +19 -1
  35. package/src/utils/index.ts +5 -2
  36. package/src/utils/merge-query/dedupe-branches.ts +42 -0
  37. package/src/utils/merge-query/extract-query-filters.ts +80 -0
  38. package/src/utils/merge-query/has-conflict.ts +39 -0
  39. package/src/utils/merge-query/logical-branches.ts +39 -0
  40. package/src/utils/merge-query/merge-query-bodies.ts +136 -0
  41. package/src/utils/merge-query/merge-query.util.ts +102 -0
  42. package/src/utils/merge-query/merge-select.ts +64 -0
  43. package/src/utils/query-defaults/query-defaults.util.ts +39 -0
  44. package/src/utils/query-has-property/query-has-property.util.ts +37 -0
  45. package/src/utils/walk-query/walk-query.util.ts +43 -3
  46. package/dist/hooks-Bg5XWcV7.mjs.map +0 -1
  47. package/dist/internal.utils-BMzV_-xp.mjs.map +0 -1
  48. package/dist/utils-Br6DNQ1B.mjs.map +0 -1
@@ -1,5 +1,6 @@
1
- import { d as isPaginated, m as isContext, p as isMulti } from "./predicates-puYa4nkf.mjs";
2
- import { a as getResultIsArray, o as getDataIsArray } from "./mutate-result.util-Dqzepn1M.mjs";
1
+ import { a as isEmptyObject, r as toArray } from "./internal.utils-BBB-b6Ud.mjs";
2
+ import { d as isPaginated, m as isContext, p as isMulti } from "./predicates-NOnUyMic.mjs";
3
+ import { a as getResultIsArray, o as getDataIsArray } from "./mutate-result.util-C0nY6L7i.mjs";
3
4
  import { dequal } from "dequal";
4
5
  import * as feathers from "@feathersjs/feathers";
5
6
  //#region src/utils/sort-query-properties/sort-query-properties.util.ts
@@ -88,58 +89,13 @@ const addSkip = (context, hooks) => {
88
89
  }
89
90
  };
90
91
  //#endregion
91
- //#region src/utils/chunk-find/chunk-find.util.ts
92
- /**
93
- * Use `for await` to iterate over chunks (pages) of results from a `find` method.
94
- *
95
- * This function is useful for processing large datasets in batches without loading everything into memory at once.
96
- * It uses pagination to fetch results in chunks, yielding each page's data array.
97
- *
98
- * @example
99
- * ```ts
100
- * import { chunkFind } from 'feathers-utils/utils'
101
- *
102
- * const app = feathers()
103
- *
104
- * // Assuming 'users' service has many records
105
- * for await (const users of chunkFind(app, 'users', {
106
- * params: { query: { active: true }, // Custom query parameters
107
- * } })) {
108
- * console.log(users) // Process each chunk of user records
109
- * }
110
- * ```
111
- *
112
- * @see https://utils.feathersjs.com/utils/chunk-find.html
113
- */
114
- async function* chunkFind(app, servicePath, options) {
115
- const service = app.service(servicePath);
116
- if (!service || !("find" in service)) throw new Error(`Service '${servicePath}' does not have a 'find' method.`);
117
- const params = {
118
- ...options?.params,
119
- query: {
120
- ...options?.params?.query ?? {},
121
- $limit: options?.params?.query?.$limit ?? 10,
122
- $skip: options?.params?.query?.$skip ?? 0
123
- },
124
- paginate: {
125
- default: options?.params?.paginate?.default ?? 10,
126
- max: options?.params?.paginate?.max ?? 100
127
- }
128
- };
129
- let result;
130
- do {
131
- result = await service.find(params);
132
- if (!result.data.length) break;
133
- yield result.data;
134
- params.query.$skip = (params.query.$skip ?? 0) + result.data.length;
135
- } while (result.total > params.query.$skip);
136
- }
137
- //#endregion
138
92
  //#region src/utils/add-to-query/add-to-query.util.ts
139
93
  /**
140
94
  * Safely merges properties into a Feathers query object. If a property already exists
141
95
  * with a different value, it wraps both in a `$and` array to preserve both conditions.
142
- * If the exact same key-value pair already exists, no changes are made.
96
+ * If the exact same key-value pair already exists, no changes are made. When the added
97
+ * query is itself a pure `$and` (`{ $and: [...] }`), its branches are flattened into the
98
+ * target's `$and` rather than nested.
143
99
  *
144
100
  * @example
145
101
  * ```ts
@@ -164,6 +120,15 @@ function addToQuery(targetQuery, query) {
164
120
  return entries.every(([property, value]) => property in targetQuery && dequal(targetQuery[property], value));
165
121
  }
166
122
  if (isAlreadyInQuery(targetQuery, entries)) return targetQuery;
123
+ if (entries.length === 1 && Array.isArray(query.$and)) {
124
+ const existing = targetQuery.$and ?? [];
125
+ const newBranches = query.$and.filter((branch) => !existing.some((q) => dequal(q, branch)));
126
+ if (newBranches.length === 0) return targetQuery;
127
+ return {
128
+ ...targetQuery,
129
+ $and: [...existing, ...newBranches]
130
+ };
131
+ }
167
132
  if (!targetQuery.$and) return {
168
133
  ...targetQuery,
169
134
  $and: [{ ...query }]
@@ -199,6 +164,53 @@ function checkContext(context, typeOrOptions, methods, label = "anonymous") {
199
164
  }
200
165
  }
201
166
  //#endregion
167
+ //#region src/utils/chunk-find/chunk-find.util.ts
168
+ /**
169
+ * Use `for await` to iterate over chunks (pages) of results from a `find` method.
170
+ *
171
+ * This function is useful for processing large datasets in batches without loading everything into memory at once.
172
+ * It uses pagination to fetch results in chunks, yielding each page's data array.
173
+ *
174
+ * @example
175
+ * ```ts
176
+ * import { chunkFind } from 'feathers-utils/utils'
177
+ *
178
+ * const app = feathers()
179
+ *
180
+ * // Assuming 'users' service has many records
181
+ * for await (const users of chunkFind(app, 'users', {
182
+ * params: { query: { active: true }, // Custom query parameters
183
+ * } })) {
184
+ * console.log(users) // Process each chunk of user records
185
+ * }
186
+ * ```
187
+ *
188
+ * @see https://utils.feathersjs.com/utils/chunk-find.html
189
+ */
190
+ async function* chunkFind(app, servicePath, options) {
191
+ const service = app.service(servicePath);
192
+ if (!service || !("find" in service)) throw new Error(`Service '${servicePath}' does not have a 'find' method.`);
193
+ const params = {
194
+ ...options?.params,
195
+ query: {
196
+ ...options?.params?.query ?? {},
197
+ $limit: options?.params?.query?.$limit ?? 10,
198
+ $skip: options?.params?.query?.$skip ?? 0
199
+ },
200
+ paginate: {
201
+ default: options?.params?.paginate?.default ?? 10,
202
+ max: options?.params?.paginate?.max ?? 100
203
+ }
204
+ };
205
+ let result;
206
+ do {
207
+ result = await service.find(params);
208
+ if (!result.data.length) break;
209
+ yield result.data;
210
+ params.query.$skip = (params.query.$skip ?? 0) + result.data.length;
211
+ } while (result.total > params.query.$skip);
212
+ }
213
+ //#endregion
202
214
  //#region src/utils/context-to-json/context-to-json.util.ts
203
215
  /**
204
216
  * Converts a FeathersJS HookContext to a plain JSON object by calling `toJSON()` if available.
@@ -314,6 +326,172 @@ async function* iterateFind(app, servicePath, options) {
314
326
  } while (result.total > params.query.$skip);
315
327
  }
316
328
  //#endregion
329
+ //#region src/utils/merge-query/extract-query-filters.ts
330
+ /**
331
+ * Splits a query into its special filters ($select, $limit, $skip, $sort) and the
332
+ * remaining query body. Internal helper for {@link mergeQuery} — not part of the
333
+ * public API.
334
+ */
335
+ function extractQueryFilters(providedQuery) {
336
+ providedQuery ??= {};
337
+ const { $select, $limit, $skip, $sort, ...query } = providedQuery;
338
+ const result = { query };
339
+ if ("$select" in providedQuery) result.$select = $select;
340
+ if ("$limit" in providedQuery) result.$limit = $limit;
341
+ if ("$skip" in providedQuery) result.$skip = $skip;
342
+ if ("$sort" in providedQuery) result.$sort = $sort;
343
+ return result;
344
+ }
345
+ //#endregion
346
+ //#region src/utils/merge-query/logical-branches.ts
347
+ /**
348
+ * Returns the branches of a logical-only query (a query whose single key is `op`),
349
+ * or `null` when the query is not purely `{ [op]: [...] }`. Internal helper for
350
+ * {@link mergeQuery}.
351
+ */
352
+ function logicalBranches(query, op) {
353
+ const keys = Object.keys(query);
354
+ if (keys.length === 1 && keys[0] === op && Array.isArray(query[op])) return query[op];
355
+ return null;
356
+ }
357
+ //#endregion
358
+ //#region src/utils/merge-query/dedupe-branches.ts
359
+ /**
360
+ * Removes empty (`{}`) and deep-equal duplicate branches, preserving order.
361
+ * Internal helper for {@link mergeQuery}.
362
+ */
363
+ function dedupeBranches(branches) {
364
+ const result = [];
365
+ for (const branch of branches) {
366
+ if (isEmptyObject(branch)) continue;
367
+ if (!result.some((existing) => dequal(existing, branch))) result.push(branch);
368
+ }
369
+ return result;
370
+ }
371
+ //#endregion
372
+ //#region src/utils/merge-query/has-conflict.ts
373
+ /**
374
+ * Two query bodies conflict when they share at least one key whose values are not
375
+ * deep-equal. Internal helper for {@link mergeQuery}.
376
+ */
377
+ function hasConflict(target, source) {
378
+ for (const key of Object.keys(target)) if (key in source && !dequal(target[key], source[key])) return true;
379
+ return false;
380
+ }
381
+ //#endregion
382
+ //#region src/utils/merge-query/merge-query-bodies.ts
383
+ /**
384
+ * Merges two query bodies (filters already removed) according to the mode.
385
+ * Internal helper for {@link mergeQuery}.
386
+ *
387
+ * - `target` / `source`: precedence merge (that side wins on conflict).
388
+ * - `combine`: the two bodies always become branches of a single `$or`.
389
+ * - `intersect`: non-conflicting bodies merge flat; on conflict they become
390
+ * branches of a single `$and`.
391
+ *
392
+ * Logical-only bodies (`{ $or: [...] }` for combine, `{ $and: [...] }` for
393
+ * intersect) are flattened into the result and their branches de-duplicated.
394
+ */
395
+ function mergeQueryBodies(target, source, mode) {
396
+ if (mode === "target") return {
397
+ ...source,
398
+ ...target
399
+ };
400
+ if (mode === "source") return {
401
+ ...target,
402
+ ...source
403
+ };
404
+ if (isEmptyObject(target)) return { ...source };
405
+ if (isEmptyObject(source)) return { ...target };
406
+ const op = mode === "combine" ? "$or" : "$and";
407
+ const targetBranches = logicalBranches(target, op);
408
+ const sourceBranches = logicalBranches(source, op);
409
+ if (op === "$and" && !targetBranches && !sourceBranches && !hasConflict(target, source)) return {
410
+ ...target,
411
+ ...source
412
+ };
413
+ const branches = dedupeBranches([...targetBranches ?? [target], ...sourceBranches ?? [source]]);
414
+ if (branches.length === 0) return {};
415
+ if (branches.length === 1) return { ...branches[0] };
416
+ return { [op]: branches };
417
+ }
418
+ //#endregion
419
+ //#region src/utils/merge-query/merge-select.ts
420
+ /**
421
+ * Merges two `$select` filters according to the mode: `combine` → union,
422
+ * `intersect` → intersection, `target`/`source` → that side. When only one side
423
+ * provides a `$select`, that one is used. Internal helper for {@link mergeQuery}.
424
+ */
425
+ function mergeSelect(target, source, mode) {
426
+ if (target === void 0) return source;
427
+ if (source === void 0) return target;
428
+ if (mode === "target") return target;
429
+ if (mode === "source") return source;
430
+ const targetArr = Array.isArray(target) ? target : [target];
431
+ const sourceArr = Array.isArray(source) ? source : [source];
432
+ if (mode === "combine") return [...new Set([...targetArr, ...sourceArr])];
433
+ return targetArr.filter((value) => sourceArr.includes(value));
434
+ }
435
+ //#endregion
436
+ //#region src/utils/merge-query/merge-query.util.ts
437
+ /**
438
+ * Properties are combined with a logical operator rather than merged at the value
439
+ * level, so the result is always a valid query: `combine` always wraps the two
440
+ * queries in `$or` (broaden — OR has no flat form), while `intersect` merges
441
+ * non-conflicting properties flat and wraps conflicts in `$and` (narrow). The
442
+ * special filters `$select`, `$limit`, `$skip` and `$sort` are merged separately.
443
+ * Inputs are never mutated.
444
+ *
445
+ * This is well suited to merging a client-provided query with a server-side
446
+ * restriction inside a hook.
447
+ *
448
+ * @param target Query to be merged into
449
+ * @param source Query to be merged from
450
+ * @param options
451
+ * @returns the merged query
452
+ *
453
+ * @example
454
+ * ```ts
455
+ * import { mergeQuery } from 'feathers-utils/utils'
456
+ *
457
+ * // combine (default): the two queries always become an $or
458
+ * mergeQuery({ id: 1 }, { id: 2 })
459
+ * // => { $or: [{ id: 1 }, { id: 2 }] }
460
+ *
461
+ * mergeQuery({ status: 'active' }, { authorId: 5 })
462
+ * // => { $or: [{ status: 'active' }, { authorId: 5 }] }
463
+ * ```
464
+ *
465
+ * @example
466
+ * ```ts
467
+ * // intersect: non-conflicting properties merge flat, conflicts become an $and
468
+ * mergeQuery({ status: 'active' }, { authorId: 5 }, { mode: 'intersect' })
469
+ * // => { status: 'active', authorId: 5 }
470
+ *
471
+ * mergeQuery({ id: 1 }, { id: 2 }, { mode: 'intersect' })
472
+ * // => { $and: [{ id: 1 }, { id: 2 }] }
473
+ * ```
474
+ *
475
+ * @see https://utils.feathersjs.com/utils/merge-query.html
476
+ */
477
+ function mergeQuery(target, source, options) {
478
+ const mode = options?.mode ?? "combine";
479
+ const targetFilters = extractQueryFilters(target);
480
+ const sourceFilters = extractQueryFilters(source);
481
+ const result = mergeQueryBodies(targetFilters.query, sourceFilters.query, mode);
482
+ const $select = mergeSelect(targetFilters.$select, sourceFilters.$select, mode);
483
+ if ($select !== void 0) result.$select = $select;
484
+ if ("$limit" in sourceFilters) result.$limit = sourceFilters.$limit;
485
+ else if ("$limit" in targetFilters) result.$limit = targetFilters.$limit;
486
+ if ("$skip" in sourceFilters) result.$skip = sourceFilters.$skip;
487
+ else if ("$skip" in targetFilters) result.$skip = targetFilters.$skip;
488
+ if ("$sort" in targetFilters || "$sort" in sourceFilters) result.$sort = {
489
+ ...targetFilters.$sort,
490
+ ...sourceFilters.$sort
491
+ };
492
+ return result;
493
+ }
494
+ //#endregion
317
495
  //#region src/utils/patch-batch/patch-batch.util.ts
318
496
  /**
319
497
  * Deterministic, key-order-independent serialization used to group items with
@@ -377,6 +555,183 @@ function patchBatch(items, options) {
377
555
  });
378
556
  }
379
557
  //#endregion
558
+ //#region src/utils/walk-query/walk-query.util.ts
559
+ const _walkQueryUtil = (query, walker, state, options) => {
560
+ const stop = () => {
561
+ state.stopped = true;
562
+ };
563
+ let cloned = false;
564
+ const clonedSecond = {};
565
+ function set(key, value, secondKey) {
566
+ if (!cloned) {
567
+ query = { ...query };
568
+ cloned = true;
569
+ }
570
+ if (secondKey !== void 0) {
571
+ if (!clonedSecond[key]) {
572
+ query[key] = { ...query[key] };
573
+ clonedSecond[key] = true;
574
+ }
575
+ query[key][secondKey] = value;
576
+ return;
577
+ }
578
+ query[key] = value;
579
+ }
580
+ for (const key in query) {
581
+ if (state.stopped) break;
582
+ if ((key === "$or" || key === "$and" || key === "$nor") && Array.isArray(query[key])) {
583
+ let array = query[key];
584
+ let copiedArray = false;
585
+ for (let i = 0, n = array.length; i < n; i++) {
586
+ if (state.stopped) break;
587
+ const nestedQuery = array[i];
588
+ const transformed = _walkQueryUtil(nestedQuery, walker, state, {
589
+ ...options,
590
+ path: [
591
+ ...options?.path || [],
592
+ key,
593
+ i
594
+ ]
595
+ });
596
+ if (transformed !== nestedQuery) {
597
+ if (!copiedArray) {
598
+ array = [...array];
599
+ copiedArray = true;
600
+ }
601
+ array[i] = transformed;
602
+ }
603
+ }
604
+ if (copiedArray) set(key, array);
605
+ } else if (typeof query[key] === "object" && query[key] !== null && !Array.isArray(query[key])) {
606
+ let hasOperator = false;
607
+ for (const operator in query[key]) {
608
+ if (state.stopped) break;
609
+ if (operator.startsWith("$")) {
610
+ hasOperator = true;
611
+ const value = walker({
612
+ operator,
613
+ path: [...options?.path ?? [], key],
614
+ property: key,
615
+ value: query[key][operator],
616
+ stop
617
+ });
618
+ if (value !== void 0 && value !== query[key][operator]) set(key, value, operator);
619
+ }
620
+ }
621
+ if (!hasOperator) {
622
+ const value = walker({
623
+ operator: void 0,
624
+ path: [...options?.path ?? [], key],
625
+ property: key,
626
+ value: query[key],
627
+ stop
628
+ });
629
+ if (value !== void 0 && value !== query[key]) set(key, value);
630
+ }
631
+ } else {
632
+ const value = walker({
633
+ operator: void 0,
634
+ path: [...options?.path ?? [], key],
635
+ property: key,
636
+ value: query[key],
637
+ stop
638
+ });
639
+ if (value !== void 0 && value !== query[key]) set(key, value);
640
+ }
641
+ }
642
+ return query;
643
+ };
644
+ /**
645
+ * Walks every property of a Feathers query (including nested `$and`/`$or`/`$nor` arrays)
646
+ * and calls the `walker` function for each one. The walker receives the property name, operator,
647
+ * value, path, and a `stop` function, and can return a replacement value. Calling `stop()` halts
648
+ * the traversal early. Returns a new query only if changes were made.
649
+ *
650
+ * @example
651
+ * ```ts
652
+ * import { walkQuery } from 'feathers-utils/utils'
653
+ *
654
+ * const query = walkQuery({ age: { $gt: '18' } }, ({ value, operator }) => {
655
+ * if (operator === '$gt') return Number(value)
656
+ * })
657
+ * // => { age: { $gt: 18 } }
658
+ * ```
659
+ *
660
+ * @example
661
+ * ```ts
662
+ * // stop early once a property is found
663
+ * let found = false
664
+ * walkQuery(query, ({ property, stop }) => {
665
+ * if (property === 'isTemplate') {
666
+ * found = true
667
+ * stop()
668
+ * }
669
+ * })
670
+ * ```
671
+ *
672
+ * @see https://utils.feathersjs.com/utils/walk-query.html
673
+ */
674
+ const walkQuery = (query, walker) => {
675
+ return _walkQueryUtil(query, walker, { stopped: false });
676
+ };
677
+ //#endregion
678
+ //#region src/utils/query-has-property/query-has-property.util.ts
679
+ /**
680
+ * Checks whether a Feathers query contains one or more properties — including
681
+ * properties nested inside `$and`/`$or`/`$nor` arrays. Returns `true` as soon as
682
+ * any of the given property names is found. The query is not mutated.
683
+ *
684
+ * @example
685
+ * ```ts
686
+ * import { queryHasProperty } from 'feathers-utils/utils'
687
+ *
688
+ * queryHasProperty({ isTemplate: true }, 'isTemplate') // true
689
+ * queryHasProperty({ $or: [{ isTemplate: true }] }, 'isTemplate') // true
690
+ * queryHasProperty({ age: { $gt: 18 } }, ['isTemplate', 'status']) // false
691
+ * ```
692
+ *
693
+ * @see https://utils.feathersjs.com/utils/query-has-property.html
694
+ */
695
+ const queryHasProperty = (query, property) => {
696
+ const properties = new Set(toArray(property));
697
+ let found = false;
698
+ walkQuery(query, ({ property: key, stop }) => {
699
+ if (properties.has(key)) {
700
+ found = true;
701
+ stop();
702
+ }
703
+ });
704
+ return found;
705
+ };
706
+ //#endregion
707
+ //#region src/utils/query-defaults/query-defaults.util.ts
708
+ /**
709
+ * Adds default properties to a Feathers query — but only for fields the query does
710
+ * not already constrain. Presence is checked with {@link queryHasProperty}, so a field
711
+ * referenced anywhere (including nested in `$and`/`$or`/`$nor`) is left untouched and
712
+ * the caller keeps control over it. The query is treated as the `data` equivalent of
713
+ * the `defaults` transformer. Each default is applied independently (per-field).
714
+ *
715
+ * @example
716
+ * ```ts
717
+ * import { queryDefaults } from 'feathers-utils/utils'
718
+ *
719
+ * queryDefaults({ status: 'active' }, { isTemplate: false })
720
+ * // => { status: 'active', isTemplate: false }
721
+ *
722
+ * queryDefaults({ $or: [{ isTemplate: true }] }, { isTemplate: false })
723
+ * // => { $or: [{ isTemplate: true }] } (untouched — already referenced)
724
+ * ```
725
+ *
726
+ * @see https://utils.feathersjs.com/utils/query-defaults.html
727
+ */
728
+ const queryDefaults = (query, defaults) => {
729
+ const source = query ?? {};
730
+ const toAdd = {};
731
+ for (const key in defaults) if (!queryHasProperty(source, key)) toAdd[key] = defaults[key];
732
+ return addToQuery(source, toAdd);
733
+ };
734
+ //#endregion
380
735
  //#region src/utils/skip-result/skip-result.util.ts
381
736
  /**
382
737
  * Sets `context.result` to an appropriate empty value based on the hook method.
@@ -533,100 +888,6 @@ function waitForServiceEvent(app, defaultOptions) {
533
888
  };
534
889
  }
535
890
  //#endregion
536
- //#region src/utils/walk-query/walk-query.util.ts
537
- const _walkQueryUtil = (query, walker, options) => {
538
- let cloned = false;
539
- const clonedSecond = {};
540
- function set(key, value, secondKey) {
541
- if (!cloned) {
542
- query = { ...query };
543
- cloned = true;
544
- }
545
- if (secondKey !== void 0) {
546
- if (!clonedSecond[key]) {
547
- query[key] = { ...query[key] };
548
- clonedSecond[key] = true;
549
- }
550
- query[key][secondKey] = value;
551
- return;
552
- }
553
- query[key] = value;
554
- }
555
- for (const key in query) if ((key === "$or" || key === "$and" || key === "$nor") && Array.isArray(query[key])) {
556
- let array = query[key];
557
- let copiedArray = false;
558
- for (let i = 0, n = array.length; i < n; i++) {
559
- const nestedQuery = array[i];
560
- const transformed = _walkQueryUtil(nestedQuery, walker, {
561
- ...options,
562
- path: [
563
- ...options?.path || [],
564
- key,
565
- i
566
- ]
567
- });
568
- if (transformed !== nestedQuery) {
569
- if (!copiedArray) {
570
- array = [...array];
571
- copiedArray = true;
572
- }
573
- array[i] = transformed;
574
- }
575
- }
576
- if (copiedArray) set(key, array);
577
- } else if (typeof query[key] === "object" && query[key] !== null && !Array.isArray(query[key])) {
578
- let hasOperator = false;
579
- for (const operator in query[key]) if (operator.startsWith("$")) {
580
- hasOperator = true;
581
- const value = walker({
582
- operator,
583
- path: [...options?.path ?? [], key],
584
- property: key,
585
- value: query[key][operator]
586
- });
587
- if (value !== void 0 && value !== query[key][operator]) set(key, value, operator);
588
- }
589
- if (!hasOperator) {
590
- const value = walker({
591
- operator: void 0,
592
- path: [...options?.path ?? [], key],
593
- property: key,
594
- value: query[key]
595
- });
596
- if (value !== void 0 && value !== query[key]) set(key, value);
597
- }
598
- } else {
599
- const value = walker({
600
- operator: void 0,
601
- path: [...options?.path ?? [], key],
602
- property: key,
603
- value: query[key]
604
- });
605
- if (value !== void 0 && value !== query[key]) set(key, value);
606
- }
607
- return query;
608
- };
609
- /**
610
- * Walks every property of a Feathers query (including nested `$and`/`$or`/`$nor` arrays)
611
- * and calls the `walker` function for each one. The walker receives the property name, operator,
612
- * value, and path, and can return a replacement value. Returns a new query only if changes were made.
613
- *
614
- * @example
615
- * ```ts
616
- * import { walkQuery } from 'feathers-utils/utils'
617
- *
618
- * const query = walkQuery({ age: { $gt: '18' } }, ({ value, operator }) => {
619
- * if (operator === '$gt') return Number(value)
620
- * })
621
- * // => { age: { $gt: 18 } }
622
- * ```
623
- *
624
- * @see https://utils.feathersjs.com/utils/walk-query.html
625
- */
626
- const walkQuery = (query, walker) => {
627
- return _walkQueryUtil(query, walker);
628
- };
629
- //#endregion
630
891
  //#region src/utils/zip-data-result/zip-data-result.util.ts
631
892
  /**
632
893
  * Pairs each item in `context.data` with its corresponding item in `context.result` by index.
@@ -665,6 +926,6 @@ function zipDataResult(context, options) {
665
926
  return result;
666
927
  }
667
928
  //#endregion
668
- export { toPaginated as a, iterateFind as c, contextToJson as d, checkContext as f, sortQueryProperties as g, addSkip as h, transformParams as i, getExposedMethods as l, chunkFind as m, walkQuery as n, skipResult as o, addToQuery as p, waitForServiceEvent as r, patchBatch as s, zipDataResult as t, defineHooks as u };
929
+ export { addToQuery as _, skipResult as a, walkQuery as c, iterateFind as d, getExposedMethods as f, checkContext as g, chunkFind as h, toPaginated as i, patchBatch as l, contextToJson as m, waitForServiceEvent as n, queryDefaults as o, defineHooks as p, transformParams as r, queryHasProperty as s, zipDataResult as t, mergeQuery as u, addSkip as v, sortQueryProperties as y };
669
930
 
670
- //# sourceMappingURL=utils-Br6DNQ1B.mjs.map
931
+ //# sourceMappingURL=utils-DByCpAsf.mjs.map