feathers-utils 10.2.0 → 10.4.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 (50) hide show
  1. package/dist/{hooks-tfw03iVM.mjs → hooks-D_u2QFhM.mjs} +65 -65
  2. package/dist/{hooks-tfw03iVM.mjs.map → hooks-D_u2QFhM.mjs.map} +1 -1
  3. package/dist/hooks.d.mts +48 -48
  4. package/dist/hooks.mjs +4 -4
  5. package/dist/{index-CKAzIogj.d.mts → index-H1zXVhff.d.mts} +209 -105
  6. package/dist/index.d.mts +2 -2
  7. package/dist/index.mjs +7 -7
  8. package/dist/internal.utils-CWuBYzpQ.mjs +120 -0
  9. package/dist/internal.utils-CWuBYzpQ.mjs.map +1 -0
  10. package/dist/{mutate-result.util-Dqzepn1M.mjs → mutate-result.util-mxvMl6bw.mjs} +2 -2
  11. package/dist/mutate-result.util-mxvMl6bw.mjs.map +1 -0
  12. package/dist/{predicates-puYa4nkf.mjs → predicates-CR4O2nSr.mjs} +53 -53
  13. package/dist/{predicates-puYa4nkf.mjs.map → predicates-CR4O2nSr.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-BbbdWdlO.mjs} +2 -2
  17. package/dist/{resolve-B9hRleHY.mjs.map → resolve-BbbdWdlO.mjs.map} +1 -1
  18. package/dist/resolvers.mjs +1 -1
  19. package/dist/{transform-result.hook-B65pTRJO.mjs → transform-result.hook-0D676rcY.mjs} +2 -2
  20. package/dist/{transform-result.hook-B65pTRJO.mjs.map → transform-result.hook-0D676rcY.mjs.map} +1 -1
  21. package/dist/transformers.mjs +3 -3
  22. package/dist/{utils-BAIcSl7u.mjs → utils-sTvj8-Jy.mjs} +467 -209
  23. package/dist/utils-sTvj8-Jy.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/dedupe-branches.ts +42 -0
  28. package/src/common/flatten-and-branches.ts +56 -0
  29. package/src/common/flatten-or-branches.ts +52 -0
  30. package/src/common/index.ts +4 -0
  31. package/src/common/is-empty-object.ts +38 -0
  32. package/src/hooks/index.ts +1 -1
  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 +6 -4
  36. package/src/utils/merge-query/extract-query-filters.ts +80 -0
  37. package/src/utils/merge-query/has-conflict.ts +39 -0
  38. package/src/utils/merge-query/logical-branches.ts +39 -0
  39. package/src/utils/merge-query/merge-query-bodies.ts +152 -0
  40. package/src/utils/merge-query/merge-query.util.ts +105 -0
  41. package/src/utils/merge-query/merge-select.ts +64 -0
  42. package/src/utils/replace-data/replace-data.util.ts +4 -4
  43. package/src/utils/replace-result/replace-result.util.ts +18 -18
  44. package/src/utils/simplify-query/merge-and-branches-up.ts +86 -0
  45. package/src/utils/simplify-query/merge-or-branch-up.ts +74 -0
  46. package/src/utils/simplify-query/simplify-query.util.ts +98 -0
  47. package/dist/internal.utils-BMzV_-xp.mjs +0 -55
  48. package/dist/internal.utils-BMzV_-xp.mjs.map +0 -1
  49. package/dist/mutate-result.util-Dqzepn1M.mjs.map +0 -1
  50. package/dist/utils-BAIcSl7u.mjs.map +0 -1
@@ -1,6 +1,6 @@
1
- import { r as toArray } from "./internal.utils-BMzV_-xp.mjs";
2
- import { d as isPaginated, m as isContext, p as isMulti } from "./predicates-puYa4nkf.mjs";
3
- import { a as getResultIsArray, o as getDataIsArray } from "./mutate-result.util-Dqzepn1M.mjs";
1
+ import { a as flattenOrBranches, c as isEmptyObject, o as flattenAndBranches, r as toArray, s as dedupeBranches } from "./internal.utils-CWuBYzpQ.mjs";
2
+ import { d as isPaginated, m as isContext, p as isMulti } from "./predicates-CR4O2nSr.mjs";
3
+ import { a as getResultIsArray, o as getDataIsArray } from "./mutate-result.util-mxvMl6bw.mjs";
4
4
  import { dequal } from "dequal";
5
5
  import * as feathers from "@feathersjs/feathers";
6
6
  //#region src/utils/sort-query-properties/sort-query-properties.util.ts
@@ -89,58 +89,13 @@ const addSkip = (context, hooks) => {
89
89
  }
90
90
  };
91
91
  //#endregion
92
- //#region src/utils/chunk-find/chunk-find.util.ts
93
- /**
94
- * Use `for await` to iterate over chunks (pages) of results from a `find` method.
95
- *
96
- * This function is useful for processing large datasets in batches without loading everything into memory at once.
97
- * It uses pagination to fetch results in chunks, yielding each page's data array.
98
- *
99
- * @example
100
- * ```ts
101
- * import { chunkFind } from 'feathers-utils/utils'
102
- *
103
- * const app = feathers()
104
- *
105
- * // Assuming 'users' service has many records
106
- * for await (const users of chunkFind(app, 'users', {
107
- * params: { query: { active: true }, // Custom query parameters
108
- * } })) {
109
- * console.log(users) // Process each chunk of user records
110
- * }
111
- * ```
112
- *
113
- * @see https://utils.feathersjs.com/utils/chunk-find.html
114
- */
115
- async function* chunkFind(app, servicePath, options) {
116
- const service = app.service(servicePath);
117
- if (!service || !("find" in service)) throw new Error(`Service '${servicePath}' does not have a 'find' method.`);
118
- const params = {
119
- ...options?.params,
120
- query: {
121
- ...options?.params?.query ?? {},
122
- $limit: options?.params?.query?.$limit ?? 10,
123
- $skip: options?.params?.query?.$skip ?? 0
124
- },
125
- paginate: {
126
- default: options?.params?.paginate?.default ?? 10,
127
- max: options?.params?.paginate?.max ?? 100
128
- }
129
- };
130
- let result;
131
- do {
132
- result = await service.find(params);
133
- if (!result.data.length) break;
134
- yield result.data;
135
- params.query.$skip = (params.query.$skip ?? 0) + result.data.length;
136
- } while (result.total > params.query.$skip);
137
- }
138
- //#endregion
139
92
  //#region src/utils/add-to-query/add-to-query.util.ts
140
93
  /**
141
94
  * Safely merges properties into a Feathers query object. If a property already exists
142
95
  * with a different value, it wraps both in a `$and` array to preserve both conditions.
143
- * 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.
144
99
  *
145
100
  * @example
146
101
  * ```ts
@@ -165,6 +120,15 @@ function addToQuery(targetQuery, query) {
165
120
  return entries.every(([property, value]) => property in targetQuery && dequal(targetQuery[property], value));
166
121
  }
167
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
+ }
168
132
  if (!targetQuery.$and) return {
169
133
  ...targetQuery,
170
134
  $and: [{ ...query }]
@@ -200,6 +164,53 @@ function checkContext(context, typeOrOptions, methods, label = "anonymous") {
200
164
  }
201
165
  }
202
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
203
214
  //#region src/utils/context-to-json/context-to-json.util.ts
204
215
  /**
205
216
  * Converts a FeathersJS HookContext to a plain JSON object by calling `toJSON()` if available.
@@ -315,6 +326,253 @@ async function* iterateFind(app, servicePath, options) {
315
326
  } while (result.total > params.query.$skip);
316
327
  }
317
328
  //#endregion
329
+ //#region src/utils/simplify-query/merge-and-branches-up.ts
330
+ /**
331
+ * `$and` is an implicit AND with the rest of the query, so ALL of its branches can
332
+ * be merged up into the parent at once — as long as no key would be set to two
333
+ * different values (across the branches or the existing keys). On any such collision
334
+ * the `$and` is kept intact. Internal helper for {@link simplifyQuery}.
335
+ */
336
+ function mergeAndBranchesUp(result, enabled) {
337
+ if (!enabled || !Array.isArray(result.$and) || result.$and.length === 0) return result;
338
+ const rest = { ...result };
339
+ delete rest.$and;
340
+ const seen = new Map(Object.entries(rest));
341
+ for (const branch of result.$and) for (const [key, value] of Object.entries(branch)) {
342
+ if (seen.has(key) && !dequal(seen.get(key), value)) return result;
343
+ seen.set(key, value);
344
+ }
345
+ return Object.assign(rest, ...result.$and);
346
+ }
347
+ //#endregion
348
+ //#region src/utils/simplify-query/merge-or-branch-up.ts
349
+ /**
350
+ * `$or` is a disjunction, so only a *single* branch can be merged up (an `$or` of
351
+ * one is just that branch) — and only when no key would collide. This is the key
352
+ * asymmetry with `$and` ({@link mergeAndBranchesUp}). Internal helper for
353
+ * {@link simplifyQuery}.
354
+ */
355
+ function mergeOrBranchUp(result, enabled) {
356
+ if (!enabled || !Array.isArray(result.$or) || result.$or.length !== 1) return result;
357
+ const branch = result.$or[0];
358
+ const rest = { ...result };
359
+ delete rest.$or;
360
+ for (const key of Object.keys(branch)) if (key in rest && !dequal(rest[key], branch[key])) return result;
361
+ return {
362
+ ...rest,
363
+ ...branch
364
+ };
365
+ }
366
+ //#endregion
367
+ //#region src/utils/simplify-query/simplify-query.util.ts
368
+ /**
369
+ * Normalizes the logical structure of a Feathers query without changing what it
370
+ * matches: empty `$and`/`$or` are dropped, duplicate branches removed, nested
371
+ * same-operator branches hoisted (`$and`-in-`$and`, pure `$or`-in-`$or`), and
372
+ * branches merged up into the parent where it is safe — all of an `$and` when no
373
+ * key collides, a single-branch `$or`. Runs recursively. Inputs are not mutated;
374
+ * a query with nothing to simplify is returned unchanged.
375
+ *
376
+ * @param query the query to simplify (a falsy query is returned as-is)
377
+ * @param options
378
+ * @returns the simplified query
379
+ *
380
+ * @example
381
+ * ```ts
382
+ * import { simplifyQuery } from 'feathers-utils/utils'
383
+ *
384
+ * // non-colliding $and branches (here also a hoisted nested $and) merge up
385
+ * simplifyQuery({ $and: [{ id: 1 }, { $and: [{ status: 'a' }] }] })
386
+ * // => { id: 1, status: 'a' }
387
+ *
388
+ * simplifyQuery({ $or: [{ id: 1 }] })
389
+ * // => { id: 1 }
390
+ *
391
+ * // a colliding key keeps the $and intact
392
+ * simplifyQuery({ $and: [{ price: { $gt: 1 } }, { price: { $lt: 9 } }] })
393
+ * // => { $and: [{ price: { $gt: 1 } }, { price: { $lt: 9 } }] }
394
+ * ```
395
+ *
396
+ * @see https://utils.feathersjs.com/utils/simplify-query.html
397
+ */
398
+ function simplifyQuery(query, options = {}) {
399
+ const { replaceAnd = true, replaceOr = true } = options;
400
+ return simplify(query, replaceAnd, replaceOr);
401
+ }
402
+ function simplify(query, replaceAnd, replaceOr) {
403
+ if (!query || typeof query !== "object" || Array.isArray(query)) return query;
404
+ const hasAnd = Array.isArray(query.$and);
405
+ const hasOr = Array.isArray(query.$or);
406
+ if (!hasAnd && !hasOr) return query;
407
+ const { $and, $or, ...rest } = query;
408
+ const result = { ...rest };
409
+ if (hasAnd) {
410
+ const branches = dedupeBranches(flattenAndBranches($and.map((b) => simplify(b, true, true))));
411
+ if (branches.length > 0) result.$and = branches;
412
+ }
413
+ if (hasOr) {
414
+ const simplified = $or.map((b) => simplify(b, true, true));
415
+ if (!simplified.some(isEmptyObject)) {
416
+ const branches = dedupeBranches(flattenOrBranches(simplified));
417
+ if (branches.length > 0) result.$or = branches;
418
+ }
419
+ }
420
+ return mergeOrBranchUp(mergeAndBranchesUp(result, replaceAnd), replaceOr);
421
+ }
422
+ //#endregion
423
+ //#region src/utils/merge-query/extract-query-filters.ts
424
+ /**
425
+ * Splits a query into its special filters ($select, $limit, $skip, $sort) and the
426
+ * remaining query body. Internal helper for {@link mergeQuery} — not part of the
427
+ * public API.
428
+ */
429
+ function extractQueryFilters(providedQuery) {
430
+ providedQuery ??= {};
431
+ const { $select, $limit, $skip, $sort, ...query } = providedQuery;
432
+ const result = { query };
433
+ if ("$select" in providedQuery) result.$select = $select;
434
+ if ("$limit" in providedQuery) result.$limit = $limit;
435
+ if ("$skip" in providedQuery) result.$skip = $skip;
436
+ if ("$sort" in providedQuery) result.$sort = $sort;
437
+ return result;
438
+ }
439
+ //#endregion
440
+ //#region src/utils/merge-query/logical-branches.ts
441
+ /**
442
+ * Returns the branches of a logical-only query (a query whose single key is `op`),
443
+ * or `null` when the query is not purely `{ [op]: [...] }`. Internal helper for
444
+ * {@link mergeQuery}.
445
+ */
446
+ function logicalBranches(query, op) {
447
+ const keys = Object.keys(query);
448
+ if (keys.length === 1 && keys[0] === op && Array.isArray(query[op])) return query[op];
449
+ return null;
450
+ }
451
+ //#endregion
452
+ //#region src/utils/merge-query/has-conflict.ts
453
+ /**
454
+ * Two query bodies conflict when they share at least one key whose values are not
455
+ * deep-equal. Internal helper for {@link mergeQuery}.
456
+ */
457
+ function hasConflict(target, source) {
458
+ for (const key of Object.keys(target)) if (key in source && !dequal(target[key], source[key])) return true;
459
+ return false;
460
+ }
461
+ //#endregion
462
+ //#region src/utils/merge-query/merge-query-bodies.ts
463
+ /**
464
+ * Merges two query bodies (filters already removed) according to the mode.
465
+ * Internal helper for {@link mergeQuery}.
466
+ *
467
+ * - `target` / `source`: precedence merge (that side wins on conflict).
468
+ * - `combine`: the two bodies always become branches of a single `$or`.
469
+ * - `intersect`: non-conflicting bodies merge flat; on conflict they become
470
+ * branches of a single `$and`.
471
+ *
472
+ * Logical-only bodies (`{ $or: [...] }` for combine, `{ $and: [...] }` for
473
+ * intersect) are flattened into the result and their branches de-duplicated.
474
+ */
475
+ function mergeQueryBodies(target, source, mode) {
476
+ if (mode === "target") return {
477
+ ...source,
478
+ ...target
479
+ };
480
+ if (mode === "source") return {
481
+ ...target,
482
+ ...source
483
+ };
484
+ if (isEmptyObject(target)) return { ...source };
485
+ if (isEmptyObject(source)) return { ...target };
486
+ const op = mode === "combine" ? "$or" : "$and";
487
+ const targetBranches = logicalBranches(target, op);
488
+ const sourceBranches = logicalBranches(source, op);
489
+ if (op === "$and" && !targetBranches && !sourceBranches && !hasConflict(target, source)) return {
490
+ ...target,
491
+ ...source
492
+ };
493
+ const collected = [...targetBranches ?? [target], ...sourceBranches ?? [source]];
494
+ const branches = dedupeBranches(op === "$and" ? flattenAndBranches(collected) : collected);
495
+ if (branches.length === 0) return {};
496
+ if (branches.length === 1) return { ...branches[0] };
497
+ return { [op]: branches };
498
+ }
499
+ //#endregion
500
+ //#region src/utils/merge-query/merge-select.ts
501
+ /**
502
+ * Merges two `$select` filters according to the mode: `combine` → union,
503
+ * `intersect` → intersection, `target`/`source` → that side. When only one side
504
+ * provides a `$select`, that one is used. Internal helper for {@link mergeQuery}.
505
+ */
506
+ function mergeSelect(target, source, mode) {
507
+ if (target === void 0) return source;
508
+ if (source === void 0) return target;
509
+ if (mode === "target") return target;
510
+ if (mode === "source") return source;
511
+ const targetArr = Array.isArray(target) ? target : [target];
512
+ const sourceArr = Array.isArray(source) ? source : [source];
513
+ if (mode === "combine") return [...new Set([...targetArr, ...sourceArr])];
514
+ return targetArr.filter((value) => sourceArr.includes(value));
515
+ }
516
+ //#endregion
517
+ //#region src/utils/merge-query/merge-query.util.ts
518
+ /**
519
+ * Properties are combined with a logical operator rather than merged at the value
520
+ * level, so the result is always a valid query: `combine` always wraps the two
521
+ * queries in `$or` (broaden — OR has no flat form), while `intersect` merges
522
+ * non-conflicting properties flat and wraps conflicts in `$and` (narrow). The
523
+ * special filters `$select`, `$limit`, `$skip` and `$sort` are merged separately.
524
+ * Inputs are never mutated.
525
+ *
526
+ * This is well suited to merging a client-provided query with a server-side
527
+ * restriction inside a hook.
528
+ *
529
+ * @param target Query to be merged into
530
+ * @param source Query to be merged from
531
+ * @param options
532
+ * @returns the merged query
533
+ *
534
+ * @example
535
+ * ```ts
536
+ * import { mergeQuery } from 'feathers-utils/utils'
537
+ *
538
+ * // combine (default): the two queries always become an $or
539
+ * mergeQuery({ id: 1 }, { id: 2 })
540
+ * // => { $or: [{ id: 1 }, { id: 2 }] }
541
+ *
542
+ * mergeQuery({ status: 'active' }, { authorId: 5 })
543
+ * // => { $or: [{ status: 'active' }, { authorId: 5 }] }
544
+ * ```
545
+ *
546
+ * @example
547
+ * ```ts
548
+ * // intersect: non-conflicting properties merge flat, conflicts become an $and
549
+ * mergeQuery({ status: 'active' }, { authorId: 5 }, { mode: 'intersect' })
550
+ * // => { status: 'active', authorId: 5 }
551
+ *
552
+ * mergeQuery({ id: 1 }, { id: 2 }, { mode: 'intersect' })
553
+ * // => { $and: [{ id: 1 }, { id: 2 }] }
554
+ * ```
555
+ *
556
+ * @see https://utils.feathersjs.com/utils/merge-query.html
557
+ */
558
+ function mergeQuery(target, source, options) {
559
+ const mode = options?.mode ?? "combine";
560
+ const targetFilters = extractQueryFilters(simplifyQuery(target));
561
+ const sourceFilters = extractQueryFilters(simplifyQuery(source));
562
+ const result = mergeQueryBodies(targetFilters.query, sourceFilters.query, mode);
563
+ const $select = mergeSelect(targetFilters.$select, sourceFilters.$select, mode);
564
+ if ($select !== void 0) result.$select = $select;
565
+ if ("$limit" in sourceFilters) result.$limit = sourceFilters.$limit;
566
+ else if ("$limit" in targetFilters) result.$limit = targetFilters.$limit;
567
+ if ("$skip" in sourceFilters) result.$skip = sourceFilters.$skip;
568
+ else if ("$skip" in targetFilters) result.$skip = targetFilters.$skip;
569
+ if ("$sort" in targetFilters || "$sort" in sourceFilters) result.$sort = {
570
+ ...targetFilters.$sort,
571
+ ...sourceFilters.$sort
572
+ };
573
+ return result;
574
+ }
575
+ //#endregion
318
576
  //#region src/utils/patch-batch/patch-batch.util.ts
319
577
  /**
320
578
  * Deterministic, key-order-independent serialization used to group items with
@@ -378,162 +636,6 @@ function patchBatch(items, options) {
378
636
  });
379
637
  }
380
638
  //#endregion
381
- //#region src/utils/skip-result/skip-result.util.ts
382
- /**
383
- * Sets `context.result` to an appropriate empty value based on the hook method.
384
- * Returns an empty paginated object for paginated `find`, an empty array for multi
385
- * operations, or `null` for single-item operations. Does nothing if a result already exists.
386
- *
387
- * @example
388
- * ```ts
389
- * import { skipResult } from 'feathers-utils/utils'
390
- *
391
- * // In a before hook to skip the actual database call:
392
- * skipResult(context)
393
- * ```
394
- *
395
- * @see https://utils.feathersjs.com/utils/skip-result.html
396
- */
397
- const skipResult = (context) => {
398
- if (context.result) return context;
399
- if (isMulti(context)) if (context.method === "find" && isPaginated(context)) context.result = {
400
- total: 0,
401
- skip: 0,
402
- limit: 0,
403
- data: []
404
- };
405
- else context.result = [];
406
- else context.result = null;
407
- return context;
408
- };
409
- //#endregion
410
- //#region src/utils/to-paginated/to-paginated.util.ts
411
- /**
412
- * Ensures a result is in Feathers paginated format (`{ total, limit, skip, data }`).
413
- * If the input is already paginated, it is returned as-is. If it is a plain array,
414
- * it is wrapped in a paginated object with `total` and `limit` set to the array length.
415
- *
416
- * @example
417
- * ```ts
418
- * import { toPaginated } from 'feathers-utils/utils'
419
- *
420
- * const paginated = toPaginated([{ id: 1 }, { id: 2 }])
421
- * // => { total: 2, limit: 2, skip: 0, data: [{ id: 1 }, { id: 2 }] }
422
- * ```
423
- *
424
- * @see https://utils.feathersjs.com/utils/to-paginated.html
425
- */
426
- function toPaginated(result) {
427
- if (Array.isArray(result)) return {
428
- total: result.length,
429
- limit: result.length,
430
- skip: 0,
431
- data: result
432
- };
433
- return result;
434
- }
435
- //#endregion
436
- //#region src/utils/transform-params/transform-params.util.ts
437
- /**
438
- * Safely applies a `transformParams` function to a params object.
439
- * If no function is provided, the original params are returned unchanged.
440
- * The function receives a shallow copy of params, so the original is not mutated.
441
- *
442
- * @example
443
- * ```ts
444
- * import { transformParams } from 'feathers-utils/utils'
445
- *
446
- * const params = transformParams(context.params, (p) => { delete p.provider; return p })
447
- * ```
448
- *
449
- * @see https://utils.feathersjs.com/utils/transform-params.html
450
- */
451
- const transformParams = (params, fn) => {
452
- if (!fn) return params;
453
- return fn({ ...params }) ?? params;
454
- };
455
- //#endregion
456
- //#region src/utils/wait-for-service-event/wait-for-service-event.util.ts
457
- /**
458
- * Wait for a service event to fire and resolve with the emitted record. Useful
459
- * in tests to await the result of an asynchronous service event, a bit like
460
- * `promisify` for Feathers events.
461
- *
462
- * Curried: bind the `app` (and optional defaults) once, then call the returned
463
- * function per service/event. Resolves with a `[data, { event, context }]`
464
- * tuple: `data` is typed as the service's record type, and `event` is the union
465
- * of the requested events.
466
- *
467
- * Feathers emits events as `emit(event, record, context)` and fires one event
468
- * per record, so each resolution carries a single record and its `HookContext`.
469
- *
470
- * @example
471
- * ```ts
472
- * import { waitForServiceEvent } from 'feathers-utils/utils'
473
- *
474
- * const app = feathers()
475
- * const waitForEvent = waitForServiceEvent(app)
476
- *
477
- * // Wait for the next `users` record to be created.
478
- * const [user] = await waitForEvent('users', 'created')
479
- *
480
- * // Wait for a specific record, with a custom timeout and filter.
481
- * const [data, { event }] = await waitForEvent(
482
- * 'users',
483
- * ['created', 'patched'],
484
- * { filter: (user) => user.email === 'jane@example.com', timeout: 1000 },
485
- * )
486
- * ```
487
- *
488
- * @see https://utils.feathersjs.com/utils/wait-for-service-event.html
489
- */
490
- function waitForServiceEvent(app, defaultOptions) {
491
- return function waitForEvent(servicePath, eventOrEvents, options) {
492
- const events = Array.isArray(eventOrEvents) ? eventOrEvents : [eventOrEvents];
493
- const timeout = options?.timeout ?? defaultOptions?.timeout ?? 5e3;
494
- const filter = options?.filter;
495
- const signal = options?.signal ?? defaultOptions?.signal;
496
- const service = app.service(servicePath);
497
- return new Promise((resolve, reject) => {
498
- let timer;
499
- const listeners = events.map((event) => {
500
- const listener = (data, context) => {
501
- if (filter && !filter(data, context)) return;
502
- cleanup();
503
- resolve([data, {
504
- event,
505
- context
506
- }]);
507
- };
508
- return [event, listener];
509
- });
510
- const abortError = () => signal?.reason ?? /* @__PURE__ */ new Error(`Aborted waiting for event "${events.join(", ")}" on service "${String(servicePath)}"`);
511
- const onAbort = () => {
512
- cleanup();
513
- reject(abortError());
514
- };
515
- function cleanup() {
516
- if (timer) {
517
- clearTimeout(timer);
518
- timer = void 0;
519
- }
520
- for (const [event, listener] of listeners) service.off(event, listener);
521
- signal?.removeEventListener("abort", onAbort);
522
- }
523
- if (signal?.aborted) {
524
- reject(abortError());
525
- return;
526
- }
527
- if (timeout !== false) timer = setTimeout(() => {
528
- cleanup();
529
- reject(/* @__PURE__ */ new Error(`Timeout waiting for event "${events.join(", ")}" on service "${String(servicePath)}"`));
530
- }, timeout);
531
- signal?.addEventListener("abort", onAbort, { once: true });
532
- for (const [event, listener] of listeners) service.on(event, listener);
533
- });
534
- };
535
- }
536
- //#endregion
537
639
  //#region src/utils/walk-query/walk-query.util.ts
538
640
  const _walkQueryUtil = (query, walker, state, options) => {
539
641
  const stop = () => {
@@ -711,6 +813,162 @@ const queryDefaults = (query, defaults) => {
711
813
  return addToQuery(source, toAdd);
712
814
  };
713
815
  //#endregion
816
+ //#region src/utils/skip-result/skip-result.util.ts
817
+ /**
818
+ * Sets `context.result` to an appropriate empty value based on the hook method.
819
+ * Returns an empty paginated object for paginated `find`, an empty array for multi
820
+ * operations, or `null` for single-item operations. Does nothing if a result already exists.
821
+ *
822
+ * @example
823
+ * ```ts
824
+ * import { skipResult } from 'feathers-utils/utils'
825
+ *
826
+ * // In a before hook to skip the actual database call:
827
+ * skipResult(context)
828
+ * ```
829
+ *
830
+ * @see https://utils.feathersjs.com/utils/skip-result.html
831
+ */
832
+ const skipResult = (context) => {
833
+ if (context.result) return context;
834
+ if (isMulti(context)) if (context.method === "find" && isPaginated(context)) context.result = {
835
+ total: 0,
836
+ skip: 0,
837
+ limit: 0,
838
+ data: []
839
+ };
840
+ else context.result = [];
841
+ else context.result = null;
842
+ return context;
843
+ };
844
+ //#endregion
845
+ //#region src/utils/to-paginated/to-paginated.util.ts
846
+ /**
847
+ * Ensures a result is in Feathers paginated format (`{ total, limit, skip, data }`).
848
+ * If the input is already paginated, it is returned as-is. If it is a plain array,
849
+ * it is wrapped in a paginated object with `total` and `limit` set to the array length.
850
+ *
851
+ * @example
852
+ * ```ts
853
+ * import { toPaginated } from 'feathers-utils/utils'
854
+ *
855
+ * const paginated = toPaginated([{ id: 1 }, { id: 2 }])
856
+ * // => { total: 2, limit: 2, skip: 0, data: [{ id: 1 }, { id: 2 }] }
857
+ * ```
858
+ *
859
+ * @see https://utils.feathersjs.com/utils/to-paginated.html
860
+ */
861
+ function toPaginated(result) {
862
+ if (Array.isArray(result)) return {
863
+ total: result.length,
864
+ limit: result.length,
865
+ skip: 0,
866
+ data: result
867
+ };
868
+ return result;
869
+ }
870
+ //#endregion
871
+ //#region src/utils/transform-params/transform-params.util.ts
872
+ /**
873
+ * Safely applies a `transformParams` function to a params object.
874
+ * If no function is provided, the original params are returned unchanged.
875
+ * The function receives a shallow copy of params, so the original is not mutated.
876
+ *
877
+ * @example
878
+ * ```ts
879
+ * import { transformParams } from 'feathers-utils/utils'
880
+ *
881
+ * const params = transformParams(context.params, (p) => { delete p.provider; return p })
882
+ * ```
883
+ *
884
+ * @see https://utils.feathersjs.com/utils/transform-params.html
885
+ */
886
+ const transformParams = (params, fn) => {
887
+ if (!fn) return params;
888
+ return fn({ ...params }) ?? params;
889
+ };
890
+ //#endregion
891
+ //#region src/utils/wait-for-service-event/wait-for-service-event.util.ts
892
+ /**
893
+ * Wait for a service event to fire and resolve with the emitted record. Useful
894
+ * in tests to await the result of an asynchronous service event, a bit like
895
+ * `promisify` for Feathers events.
896
+ *
897
+ * Curried: bind the `app` (and optional defaults) once, then call the returned
898
+ * function per service/event. Resolves with a `[data, { event, context }]`
899
+ * tuple: `data` is typed as the service's record type, and `event` is the union
900
+ * of the requested events.
901
+ *
902
+ * Feathers emits events as `emit(event, record, context)` and fires one event
903
+ * per record, so each resolution carries a single record and its `HookContext`.
904
+ *
905
+ * @example
906
+ * ```ts
907
+ * import { waitForServiceEvent } from 'feathers-utils/utils'
908
+ *
909
+ * const app = feathers()
910
+ * const waitForEvent = waitForServiceEvent(app)
911
+ *
912
+ * // Wait for the next `users` record to be created.
913
+ * const [user] = await waitForEvent('users', 'created')
914
+ *
915
+ * // Wait for a specific record, with a custom timeout and filter.
916
+ * const [data, { event }] = await waitForEvent(
917
+ * 'users',
918
+ * ['created', 'patched'],
919
+ * { filter: (user) => user.email === 'jane@example.com', timeout: 1000 },
920
+ * )
921
+ * ```
922
+ *
923
+ * @see https://utils.feathersjs.com/utils/wait-for-service-event.html
924
+ */
925
+ function waitForServiceEvent(app, defaultOptions) {
926
+ return function waitForEvent(servicePath, eventOrEvents, options) {
927
+ const events = Array.isArray(eventOrEvents) ? eventOrEvents : [eventOrEvents];
928
+ const timeout = options?.timeout ?? defaultOptions?.timeout ?? 5e3;
929
+ const filter = options?.filter;
930
+ const signal = options?.signal ?? defaultOptions?.signal;
931
+ const service = app.service(servicePath);
932
+ return new Promise((resolve, reject) => {
933
+ let timer;
934
+ const listeners = events.map((event) => {
935
+ const listener = (data, context) => {
936
+ if (filter && !filter(data, context)) return;
937
+ cleanup();
938
+ resolve([data, {
939
+ event,
940
+ context
941
+ }]);
942
+ };
943
+ return [event, listener];
944
+ });
945
+ const abortError = () => signal?.reason ?? /* @__PURE__ */ new Error(`Aborted waiting for event "${events.join(", ")}" on service "${String(servicePath)}"`);
946
+ const onAbort = () => {
947
+ cleanup();
948
+ reject(abortError());
949
+ };
950
+ function cleanup() {
951
+ if (timer) {
952
+ clearTimeout(timer);
953
+ timer = void 0;
954
+ }
955
+ for (const [event, listener] of listeners) service.off(event, listener);
956
+ signal?.removeEventListener("abort", onAbort);
957
+ }
958
+ if (signal?.aborted) {
959
+ reject(abortError());
960
+ return;
961
+ }
962
+ if (timeout !== false) timer = setTimeout(() => {
963
+ cleanup();
964
+ reject(/* @__PURE__ */ new Error(`Timeout waiting for event "${events.join(", ")}" on service "${String(servicePath)}"`));
965
+ }, timeout);
966
+ signal?.addEventListener("abort", onAbort, { once: true });
967
+ for (const [event, listener] of listeners) service.on(event, listener);
968
+ });
969
+ };
970
+ }
971
+ //#endregion
714
972
  //#region src/utils/zip-data-result/zip-data-result.util.ts
715
973
  /**
716
974
  * Pairs each item in `context.data` with its corresponding item in `context.result` by index.
@@ -749,6 +1007,6 @@ function zipDataResult(context, options) {
749
1007
  return result;
750
1008
  }
751
1009
  //#endregion
752
- export { addSkip as _, waitForServiceEvent as a, skipResult as c, getExposedMethods as d, defineHooks as f, chunkFind as g, addToQuery as h, walkQuery as i, patchBatch as l, checkContext as m, queryDefaults as n, transformParams as o, contextToJson as p, queryHasProperty as r, toPaginated as s, zipDataResult as t, iterateFind as u, sortQueryProperties as v };
1010
+ export { checkContext as _, skipResult as a, sortQueryProperties as b, walkQuery as c, simplifyQuery as d, iterateFind as f, chunkFind as g, contextToJson as h, toPaginated as i, patchBatch as l, defineHooks as m, waitForServiceEvent as n, queryDefaults as o, getExposedMethods as p, transformParams as r, queryHasProperty as s, zipDataResult as t, mergeQuery as u, addToQuery as v, addSkip as y };
753
1011
 
754
- //# sourceMappingURL=utils-BAIcSl7u.mjs.map
1012
+ //# sourceMappingURL=utils-sTvj8-Jy.mjs.map