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.
- package/dist/{hooks-tfw03iVM.mjs → hooks-DpFQfcFa.mjs} +65 -65
- package/dist/{hooks-tfw03iVM.mjs.map → hooks-DpFQfcFa.mjs.map} +1 -1
- package/dist/hooks.d.mts +48 -48
- package/dist/hooks.mjs +4 -4
- package/dist/{index-CKAzIogj.d.mts → index-C6MN6wag.d.mts} +162 -105
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +7 -7
- package/dist/{internal.utils-BMzV_-xp.mjs → internal.utils-BBB-b6Ud.mjs} +16 -2
- package/dist/internal.utils-BBB-b6Ud.mjs.map +1 -0
- package/dist/{mutate-result.util-Dqzepn1M.mjs → mutate-result.util-C0nY6L7i.mjs} +2 -2
- package/dist/{mutate-result.util-Dqzepn1M.mjs.map → mutate-result.util-C0nY6L7i.mjs.map} +1 -1
- package/dist/{predicates-puYa4nkf.mjs → predicates-NOnUyMic.mjs} +53 -53
- package/dist/{predicates-puYa4nkf.mjs.map → predicates-NOnUyMic.mjs.map} +1 -1
- package/dist/predicates.d.mts +18 -18
- package/dist/predicates.mjs +1 -1
- package/dist/{resolve-B9hRleHY.mjs → resolve-BflgIVD8.mjs} +2 -2
- package/dist/{resolve-B9hRleHY.mjs.map → resolve-BflgIVD8.mjs.map} +1 -1
- package/dist/resolvers.mjs +1 -1
- package/dist/{transform-result.hook-B65pTRJO.mjs → transform-result.hook-V2QYN2K0.mjs} +2 -2
- package/dist/{transform-result.hook-B65pTRJO.mjs.map → transform-result.hook-V2QYN2K0.mjs.map} +1 -1
- package/dist/transformers.mjs +3 -3
- package/dist/{utils-BAIcSl7u.mjs → utils-DByCpAsf.mjs} +386 -209
- package/dist/utils-DByCpAsf.mjs.map +1 -0
- package/dist/utils.d.mts +2 -2
- package/dist/utils.mjs +4 -4
- package/package.json +1 -1
- package/src/common/index.ts +1 -0
- package/src/common/is-empty-object.ts +38 -0
- package/src/hooks/index.ts +1 -1
- package/src/predicates/index.ts +3 -3
- package/src/utils/add-to-query/add-to-query.util.ts +19 -1
- package/src/utils/index.ts +5 -4
- package/src/utils/merge-query/dedupe-branches.ts +42 -0
- package/src/utils/merge-query/extract-query-filters.ts +80 -0
- package/src/utils/merge-query/has-conflict.ts +39 -0
- package/src/utils/merge-query/logical-branches.ts +39 -0
- package/src/utils/merge-query/merge-query-bodies.ts +136 -0
- package/src/utils/merge-query/merge-query.util.ts +102 -0
- package/src/utils/merge-query/merge-select.ts +64 -0
- package/dist/internal.utils-BMzV_-xp.mjs.map +0 -1
- package/dist/utils-BAIcSl7u.mjs.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { r as toArray } from "./internal.utils-
|
|
2
|
-
import { d as isPaginated, m as isContext, p as isMulti } from "./predicates-
|
|
3
|
-
import { a as getResultIsArray, o as getDataIsArray } from "./mutate-result.util-
|
|
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 {
|
|
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-
|
|
931
|
+
//# sourceMappingURL=utils-DByCpAsf.mjs.map
|