feathers-utils 10.2.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 (41) hide show
  1. package/dist/{hooks-tfw03iVM.mjs → hooks-DpFQfcFa.mjs} +65 -65
  2. package/dist/{hooks-tfw03iVM.mjs.map → hooks-DpFQfcFa.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-C6MN6wag.d.mts} +162 -105
  6. package/dist/index.d.mts +2 -2
  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-BAIcSl7u.mjs → utils-DByCpAsf.mjs} +386 -209
  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 +1 -1
  30. package/src/predicates/index.ts +3 -3
  31. package/src/utils/add-to-query/add-to-query.util.ts +19 -1
  32. package/src/utils/index.ts +5 -4
  33. package/src/utils/merge-query/dedupe-branches.ts +42 -0
  34. package/src/utils/merge-query/extract-query-filters.ts +80 -0
  35. package/src/utils/merge-query/has-conflict.ts +39 -0
  36. package/src/utils/merge-query/logical-branches.ts +39 -0
  37. package/src/utils/merge-query/merge-query-bodies.ts +136 -0
  38. package/src/utils/merge-query/merge-query.util.ts +102 -0
  39. package/src/utils/merge-query/merge-select.ts +64 -0
  40. package/dist/internal.utils-BMzV_-xp.mjs.map +0 -1
  41. 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 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";
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,172 @@ async function* iterateFind(app, servicePath, options) {
315
326
  } while (result.total > params.query.$skip);
316
327
  }
317
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
318
495
  //#region src/utils/patch-batch/patch-batch.util.ts
319
496
  /**
320
497
  * Deterministic, key-order-independent serialization used to group items with
@@ -378,162 +555,6 @@ function patchBatch(items, options) {
378
555
  });
379
556
  }
380
557
  //#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
558
  //#region src/utils/walk-query/walk-query.util.ts
538
559
  const _walkQueryUtil = (query, walker, state, options) => {
539
560
  const stop = () => {
@@ -711,6 +732,162 @@ const queryDefaults = (query, defaults) => {
711
732
  return addToQuery(source, toAdd);
712
733
  };
713
734
  //#endregion
735
+ //#region src/utils/skip-result/skip-result.util.ts
736
+ /**
737
+ * Sets `context.result` to an appropriate empty value based on the hook method.
738
+ * Returns an empty paginated object for paginated `find`, an empty array for multi
739
+ * operations, or `null` for single-item operations. Does nothing if a result already exists.
740
+ *
741
+ * @example
742
+ * ```ts
743
+ * import { skipResult } from 'feathers-utils/utils'
744
+ *
745
+ * // In a before hook to skip the actual database call:
746
+ * skipResult(context)
747
+ * ```
748
+ *
749
+ * @see https://utils.feathersjs.com/utils/skip-result.html
750
+ */
751
+ const skipResult = (context) => {
752
+ if (context.result) return context;
753
+ if (isMulti(context)) if (context.method === "find" && isPaginated(context)) context.result = {
754
+ total: 0,
755
+ skip: 0,
756
+ limit: 0,
757
+ data: []
758
+ };
759
+ else context.result = [];
760
+ else context.result = null;
761
+ return context;
762
+ };
763
+ //#endregion
764
+ //#region src/utils/to-paginated/to-paginated.util.ts
765
+ /**
766
+ * Ensures a result is in Feathers paginated format (`{ total, limit, skip, data }`).
767
+ * If the input is already paginated, it is returned as-is. If it is a plain array,
768
+ * it is wrapped in a paginated object with `total` and `limit` set to the array length.
769
+ *
770
+ * @example
771
+ * ```ts
772
+ * import { toPaginated } from 'feathers-utils/utils'
773
+ *
774
+ * const paginated = toPaginated([{ id: 1 }, { id: 2 }])
775
+ * // => { total: 2, limit: 2, skip: 0, data: [{ id: 1 }, { id: 2 }] }
776
+ * ```
777
+ *
778
+ * @see https://utils.feathersjs.com/utils/to-paginated.html
779
+ */
780
+ function toPaginated(result) {
781
+ if (Array.isArray(result)) return {
782
+ total: result.length,
783
+ limit: result.length,
784
+ skip: 0,
785
+ data: result
786
+ };
787
+ return result;
788
+ }
789
+ //#endregion
790
+ //#region src/utils/transform-params/transform-params.util.ts
791
+ /**
792
+ * Safely applies a `transformParams` function to a params object.
793
+ * If no function is provided, the original params are returned unchanged.
794
+ * The function receives a shallow copy of params, so the original is not mutated.
795
+ *
796
+ * @example
797
+ * ```ts
798
+ * import { transformParams } from 'feathers-utils/utils'
799
+ *
800
+ * const params = transformParams(context.params, (p) => { delete p.provider; return p })
801
+ * ```
802
+ *
803
+ * @see https://utils.feathersjs.com/utils/transform-params.html
804
+ */
805
+ const transformParams = (params, fn) => {
806
+ if (!fn) return params;
807
+ return fn({ ...params }) ?? params;
808
+ };
809
+ //#endregion
810
+ //#region src/utils/wait-for-service-event/wait-for-service-event.util.ts
811
+ /**
812
+ * Wait for a service event to fire and resolve with the emitted record. Useful
813
+ * in tests to await the result of an asynchronous service event, a bit like
814
+ * `promisify` for Feathers events.
815
+ *
816
+ * Curried: bind the `app` (and optional defaults) once, then call the returned
817
+ * function per service/event. Resolves with a `[data, { event, context }]`
818
+ * tuple: `data` is typed as the service's record type, and `event` is the union
819
+ * of the requested events.
820
+ *
821
+ * Feathers emits events as `emit(event, record, context)` and fires one event
822
+ * per record, so each resolution carries a single record and its `HookContext`.
823
+ *
824
+ * @example
825
+ * ```ts
826
+ * import { waitForServiceEvent } from 'feathers-utils/utils'
827
+ *
828
+ * const app = feathers()
829
+ * const waitForEvent = waitForServiceEvent(app)
830
+ *
831
+ * // Wait for the next `users` record to be created.
832
+ * const [user] = await waitForEvent('users', 'created')
833
+ *
834
+ * // Wait for a specific record, with a custom timeout and filter.
835
+ * const [data, { event }] = await waitForEvent(
836
+ * 'users',
837
+ * ['created', 'patched'],
838
+ * { filter: (user) => user.email === 'jane@example.com', timeout: 1000 },
839
+ * )
840
+ * ```
841
+ *
842
+ * @see https://utils.feathersjs.com/utils/wait-for-service-event.html
843
+ */
844
+ function waitForServiceEvent(app, defaultOptions) {
845
+ return function waitForEvent(servicePath, eventOrEvents, options) {
846
+ const events = Array.isArray(eventOrEvents) ? eventOrEvents : [eventOrEvents];
847
+ const timeout = options?.timeout ?? defaultOptions?.timeout ?? 5e3;
848
+ const filter = options?.filter;
849
+ const signal = options?.signal ?? defaultOptions?.signal;
850
+ const service = app.service(servicePath);
851
+ return new Promise((resolve, reject) => {
852
+ let timer;
853
+ const listeners = events.map((event) => {
854
+ const listener = (data, context) => {
855
+ if (filter && !filter(data, context)) return;
856
+ cleanup();
857
+ resolve([data, {
858
+ event,
859
+ context
860
+ }]);
861
+ };
862
+ return [event, listener];
863
+ });
864
+ const abortError = () => signal?.reason ?? /* @__PURE__ */ new Error(`Aborted waiting for event "${events.join(", ")}" on service "${String(servicePath)}"`);
865
+ const onAbort = () => {
866
+ cleanup();
867
+ reject(abortError());
868
+ };
869
+ function cleanup() {
870
+ if (timer) {
871
+ clearTimeout(timer);
872
+ timer = void 0;
873
+ }
874
+ for (const [event, listener] of listeners) service.off(event, listener);
875
+ signal?.removeEventListener("abort", onAbort);
876
+ }
877
+ if (signal?.aborted) {
878
+ reject(abortError());
879
+ return;
880
+ }
881
+ if (timeout !== false) timer = setTimeout(() => {
882
+ cleanup();
883
+ reject(/* @__PURE__ */ new Error(`Timeout waiting for event "${events.join(", ")}" on service "${String(servicePath)}"`));
884
+ }, timeout);
885
+ signal?.addEventListener("abort", onAbort, { once: true });
886
+ for (const [event, listener] of listeners) service.on(event, listener);
887
+ });
888
+ };
889
+ }
890
+ //#endregion
714
891
  //#region src/utils/zip-data-result/zip-data-result.util.ts
715
892
  /**
716
893
  * Pairs each item in `context.data` with its corresponding item in `context.result` by index.
@@ -749,6 +926,6 @@ function zipDataResult(context, options) {
749
926
  return result;
750
927
  }
751
928
  //#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 };
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 };
753
930
 
754
- //# sourceMappingURL=utils-BAIcSl7u.mjs.map
931
+ //# sourceMappingURL=utils-DByCpAsf.mjs.map