feathers-utils 10.0.1 → 10.0.2
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-CzxNpt2c.mjs → hooks-yFJ5FmU5.mjs} +37 -33
- package/dist/hooks-yFJ5FmU5.mjs.map +1 -0
- package/dist/hooks.d.mts +47 -7
- package/dist/hooks.mjs +4 -4
- package/dist/{index-CNrzxmWo.d.mts → index-CgAuzxiv.d.mts} +49 -4
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +7 -7
- package/dist/{internal.utils-BWQ25nOd.mjs → internal.utils-3BrB8lRY.mjs} +8 -2
- package/dist/internal.utils-3BrB8lRY.mjs.map +1 -0
- package/dist/{mutate-result.util-CCBWix-G.mjs → mutate-result.util-B6FOkwrd.mjs} +73 -11
- package/dist/mutate-result.util-B6FOkwrd.mjs.map +1 -0
- package/dist/{predicates-C2SeOKGd.mjs → predicates-BC8p_YLo.mjs} +13 -15
- package/dist/predicates-BC8p_YLo.mjs.map +1 -0
- package/dist/predicates.d.mts +2 -2
- package/dist/predicates.mjs +1 -1
- package/dist/{resolve-B81gQqXW.mjs → resolve-ClHfNFlm.mjs} +3 -3
- package/dist/{resolve-B81gQqXW.mjs.map → resolve-ClHfNFlm.mjs.map} +1 -1
- package/dist/resolvers.mjs +1 -1
- package/dist/{transform-result.hook-DifNj7zf.mjs → transform-result.hook-CanJtTPV.mjs} +2 -2
- package/dist/{transform-result.hook-DifNj7zf.mjs.map → transform-result.hook-CanJtTPV.mjs.map} +1 -1
- package/dist/transformers.mjs +8 -4
- package/dist/transformers.mjs.map +1 -1
- package/dist/{unless.hook-CVD7SrZh.d.mts → unless.hook-BX0mOvdN.d.mts} +2 -2
- package/dist/{utils-ByzrJKGQ.mjs → utils-BrvpblXB.mjs} +38 -19
- package/dist/utils-BrvpblXB.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/clone.ts +9 -7
- package/src/hooks/cache/cache-utils.ts +5 -18
- package/src/hooks/cache/cache.hook.ts +28 -9
- package/src/hooks/disallow/disallow.hook.ts +3 -1
- package/src/hooks/on-delete/on-delete.hook.ts +60 -40
- package/src/hooks/params-for-server/params-for-server.hook.ts +4 -0
- package/src/hooks/set-field/set-field.hook.ts +30 -8
- package/src/hooks/set-result/set-result.hook.ts +7 -0
- package/src/hooks/skippable/skippable.hook.ts +6 -4
- package/src/hooks/traverse/traverse.hook.ts +20 -1
- package/src/internal.utils.ts +6 -1
- package/src/predicates/and/and.predicate.ts +5 -5
- package/src/predicates/is-context/is-context.predicate.ts +1 -1
- package/src/predicates/or/or.predicate.ts +6 -8
- package/src/transformers/parse-date/parse-date.transformer.ts +6 -1
- package/src/utils/chunk-find/chunk-find.util.ts +7 -0
- package/src/utils/combine/combine.util.ts +8 -15
- package/src/utils/get-paginate/get-paginate.util.ts +1 -3
- package/src/utils/index.ts +2 -0
- package/src/utils/iterate-find/iterate-find.util.ts +8 -0
- package/src/utils/mutate-data/mutate-data.util.ts +21 -4
- package/src/utils/mutate-result/mutate-result.util.ts +4 -10
- package/src/utils/patch-batch/patch-batch.util.ts +32 -14
- package/src/utils/replace-data/replace-data.util.ts +26 -0
- package/src/utils/replace-result/replace-result.util.ts +60 -0
- package/src/utils/sort-query-properties/sort-query-properties.util.ts +15 -8
- package/src/utils/walk-query/walk-query.util.ts +2 -2
- package/dist/hooks-CzxNpt2c.mjs.map +0 -1
- package/dist/internal.utils-BWQ25nOd.mjs.map +0 -1
- package/dist/mutate-result.util-CCBWix-G.mjs.map +0 -1
- package/dist/predicates-C2SeOKGd.mjs.map +0 -1
- package/dist/utils-ByzrJKGQ.mjs.map +0 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import _get from 'lodash/get.js'
|
|
2
2
|
import _set from 'lodash/set.js'
|
|
3
3
|
import _has from 'lodash/has.js'
|
|
4
|
+
import { copy } from 'fast-copy'
|
|
4
5
|
|
|
5
6
|
import type { FeathersError } from '@feathersjs/errors'
|
|
6
7
|
import { Forbidden } from '@feathersjs/errors'
|
|
@@ -110,6 +111,12 @@ export function setResult<H extends HookContext = HookContext>(
|
|
|
110
111
|
}
|
|
111
112
|
|
|
112
113
|
const fn = (context: H) => {
|
|
114
|
+
// Seed context.dispatch from result so the dispatch branch mutates (and
|
|
115
|
+
// persists to) a real object instead of a throwaway copy. Mirrors mutateResult.
|
|
116
|
+
if (!!options?.dispatch && !context.dispatch) {
|
|
117
|
+
context.dispatch = copy(context.result)
|
|
118
|
+
}
|
|
119
|
+
|
|
113
120
|
if (options?.dispatch === 'both') {
|
|
114
121
|
forResultOrDispatch(context, true)
|
|
115
122
|
return forResultOrDispatch(context, false)
|
|
@@ -19,18 +19,20 @@ export const skippable = <H extends HookContext = HookContext>(
|
|
|
19
19
|
innerHook: HookFunction<H>,
|
|
20
20
|
predicate: PredicateFn<H>,
|
|
21
21
|
) => {
|
|
22
|
-
function hook(context: H): void
|
|
22
|
+
function hook(context: H): H | void | Promise<H | void>
|
|
23
23
|
function hook(context: H, next: NextFunction): Promise<void>
|
|
24
|
-
function hook(context: H, next?: NextFunction): void | Promise<void> {
|
|
24
|
+
function hook(context: H, next?: NextFunction): H | void | Promise<H | void> {
|
|
25
25
|
const skip = predicate(context)
|
|
26
26
|
|
|
27
|
-
const skipOrRun = (shouldSkip: boolean): void | Promise<void> => {
|
|
27
|
+
const skipOrRun = (shouldSkip: boolean): H | void | Promise<H | void> => {
|
|
28
28
|
if (shouldSkip) {
|
|
29
29
|
if (next) return next()
|
|
30
30
|
return
|
|
31
31
|
}
|
|
32
32
|
if (next) return innerHook(context, next) as Promise<void>
|
|
33
|
-
|
|
33
|
+
// before/after mode: return the inner hook's result so an async hook is
|
|
34
|
+
// awaited and a returned/modified context is propagated to the pipeline.
|
|
35
|
+
return innerHook(context)
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
if (!skip || typeof skip === 'boolean') {
|
|
@@ -6,6 +6,15 @@ export type TraverseOptions = {
|
|
|
6
6
|
getObject: (
|
|
7
7
|
context: HookContext,
|
|
8
8
|
) => Record<string, any> | Record<string, any>[]
|
|
9
|
+
/**
|
|
10
|
+
* For `around` hooks only: run the traversal *after* `next()` instead of before.
|
|
11
|
+
* Required when `getObject` targets `context.result`, which is only populated
|
|
12
|
+
* once the service method has run. Defaults to `false` (run before `next()`),
|
|
13
|
+
* which is correct for `context.data`/`context.params.query` targets.
|
|
14
|
+
*
|
|
15
|
+
* @default false
|
|
16
|
+
*/
|
|
17
|
+
runAfter?: boolean
|
|
9
18
|
}
|
|
10
19
|
|
|
11
20
|
/**
|
|
@@ -29,11 +38,21 @@ export type TraverseOptions = {
|
|
|
29
38
|
export const traverse = <H extends HookContext = HookContext>({
|
|
30
39
|
transformer,
|
|
31
40
|
getObject,
|
|
41
|
+
runAfter = false,
|
|
32
42
|
}: TraverseOptions) => {
|
|
43
|
+
const runTraverse = (context: H) => _traverse(getObject(context), transformer)
|
|
44
|
+
|
|
33
45
|
function hook(context: H): void
|
|
34
46
|
function hook(context: H, next: NextFunction): Promise<void>
|
|
35
47
|
function hook(context: H, next?: NextFunction): void | Promise<void> {
|
|
36
|
-
|
|
48
|
+
if (next && runAfter) {
|
|
49
|
+
// around hook targeting context.result: transform after the method ran
|
|
50
|
+
return next().then(() => {
|
|
51
|
+
runTraverse(context)
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
runTraverse(context)
|
|
37
56
|
|
|
38
57
|
if (next) return next()
|
|
39
58
|
|
package/src/internal.utils.ts
CHANGED
|
@@ -9,8 +9,13 @@ export const hasOwnProperty = (
|
|
|
9
9
|
|
|
10
10
|
export type MaybeArray<T> = T | readonly T[]
|
|
11
11
|
export type UnpackMaybeArray<T> = T extends readonly (infer E)[] ? E : T
|
|
12
|
+
/**
|
|
13
|
+
* Normalizes a value or array into an array. The returned array MUST be treated
|
|
14
|
+
* as read-only — when the input is already an array it is returned as-is (no copy)
|
|
15
|
+
* to avoid a per-call allocation on hook hot paths.
|
|
16
|
+
*/
|
|
12
17
|
export const toArray = <T>(value: T | readonly T[]): T[] =>
|
|
13
|
-
Array.isArray(value) ? [
|
|
18
|
+
Array.isArray(value) ? (value as T[]) : [value as T]
|
|
14
19
|
|
|
15
20
|
export type Promisable<T> = T | Promise<T>
|
|
16
21
|
export type KeyOf<T> = Extract<keyof T, string>
|
|
@@ -26,8 +26,7 @@ export const and = <H extends HookContext = HookContext>(
|
|
|
26
26
|
)
|
|
27
27
|
|
|
28
28
|
return (context: H): boolean | Promise<boolean> => {
|
|
29
|
-
//
|
|
30
|
-
// see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/every#description
|
|
29
|
+
// The identity element of logical AND is `true` (an empty AND is true).
|
|
31
30
|
if (!filtered.length) {
|
|
32
31
|
return true
|
|
33
32
|
}
|
|
@@ -36,10 +35,11 @@ export const and = <H extends HookContext = HookContext>(
|
|
|
36
35
|
|
|
37
36
|
for (const predicate of filtered) {
|
|
38
37
|
const result = predicate(context)
|
|
39
|
-
if (result
|
|
40
|
-
return false
|
|
41
|
-
} else if (isPromise(result)) {
|
|
38
|
+
if (isPromise(result)) {
|
|
42
39
|
promises.push(result)
|
|
40
|
+
} else if (!result) {
|
|
41
|
+
// any falsy sync result short-circuits to false
|
|
42
|
+
return false
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
|
|
@@ -32,7 +32,7 @@ export const isContext = <H extends HookContext = HookContext>(
|
|
|
32
32
|
const method = options.method != null ? toArray(options.method) : undefined
|
|
33
33
|
|
|
34
34
|
return (context: any): boolean => {
|
|
35
|
-
if (path && !path.some((x) => context.path
|
|
35
|
+
if (path && !path.some((x) => context.path === x)) {
|
|
36
36
|
return false
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -26,10 +26,9 @@ export const or = <H extends HookContext = HookContext>(
|
|
|
26
26
|
)
|
|
27
27
|
|
|
28
28
|
return (context: H): boolean | Promise<boolean> => {
|
|
29
|
-
//
|
|
30
|
-
// see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some#description
|
|
29
|
+
// The identity element of logical OR is `false` (an empty OR is false).
|
|
31
30
|
if (!filtered.length) {
|
|
32
|
-
return
|
|
31
|
+
return false
|
|
33
32
|
}
|
|
34
33
|
|
|
35
34
|
const promises: Promise<boolean>[] = []
|
|
@@ -37,12 +36,11 @@ export const or = <H extends HookContext = HookContext>(
|
|
|
37
36
|
for (const predicate of filtered) {
|
|
38
37
|
const result = predicate(context)
|
|
39
38
|
|
|
40
|
-
if (result
|
|
41
|
-
return true
|
|
42
|
-
} else if (result === false) {
|
|
43
|
-
continue
|
|
44
|
-
} else if (isPromise(result)) {
|
|
39
|
+
if (isPromise(result)) {
|
|
45
40
|
promises.push(result)
|
|
41
|
+
} else if (result) {
|
|
42
|
+
// any truthy sync result short-circuits to true
|
|
43
|
+
return true
|
|
46
44
|
}
|
|
47
45
|
}
|
|
48
46
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { BadRequest } from '@feathersjs/errors'
|
|
1
2
|
import { toArray, type MaybeArray } from '../../internal.utils.js'
|
|
2
3
|
import _get from 'lodash/get.js'
|
|
3
4
|
import _set from 'lodash/set.js'
|
|
@@ -25,7 +26,11 @@ export function parseDate<T extends Record<string, any>>(
|
|
|
25
26
|
const key = fieldNamesArr[i]
|
|
26
27
|
const value = _get(item, key)
|
|
27
28
|
if (value) {
|
|
28
|
-
|
|
29
|
+
const date = new Date(value)
|
|
30
|
+
if (Number.isNaN(date.getTime())) {
|
|
31
|
+
throw new BadRequest(`Expected valid date (parseDate '${key}')`)
|
|
32
|
+
}
|
|
33
|
+
_set(item, key, date)
|
|
29
34
|
}
|
|
30
35
|
}
|
|
31
36
|
}
|
|
@@ -68,6 +68,13 @@ export async function* chunkFind<
|
|
|
68
68
|
do {
|
|
69
69
|
result = await (service as any).find(params)
|
|
70
70
|
|
|
71
|
+
// Guard against an infinite loop: an empty page never advances $skip, so
|
|
72
|
+
// `total > $skip` could stay true forever (e.g. $limit:0, or a stale total
|
|
73
|
+
// when items are concurrently removed / filtered out by hooks).
|
|
74
|
+
if (!result.data.length) {
|
|
75
|
+
break
|
|
76
|
+
}
|
|
77
|
+
|
|
71
78
|
yield result.data
|
|
72
79
|
|
|
73
80
|
params.query.$skip = (params.query.$skip ?? 0) + result.data.length
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { HookContext } from '@feathersjs/feathers'
|
|
2
2
|
import type { HookFunction } from '../../types.js'
|
|
3
|
+
import { isPromise } from '../../common/index.js'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Sequentially executes multiple hooks, passing the updated context from one to the next.
|
|
@@ -42,22 +43,14 @@ export function combine<H extends HookContext = HookContext>(
|
|
|
42
43
|
return ctx
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
// @ts-expect-error TODO
|
|
48
|
-
const promise = serviceHooks.reduce(async (current, fn) => {
|
|
49
|
-
// @ts-expect-error TODO
|
|
50
|
-
const hook = fn.bind(this)
|
|
51
|
-
|
|
52
|
-
// Use the returned hook object or the old one
|
|
53
|
-
|
|
54
|
-
const currentHook = await current
|
|
55
|
-
const currentCtx = await hook(currentHook)
|
|
56
|
-
return updateCurrentHook(currentCtx)
|
|
57
|
-
}, Promise.resolve(ctx))
|
|
58
|
-
|
|
46
|
+
// Run the hooks sequentially, only awaiting when a hook is actually async.
|
|
47
|
+
// Avoids a microtask hop per hook and a per-hook `bind` allocation.
|
|
59
48
|
try {
|
|
60
|
-
|
|
49
|
+
for (const fn of serviceHooks) {
|
|
50
|
+
// @ts-expect-error `this` is the bound service-hook context
|
|
51
|
+
const currentCtx = fn.call(this, ctx)
|
|
52
|
+
updateCurrentHook(isPromise(currentCtx) ? await currentCtx : currentCtx)
|
|
53
|
+
}
|
|
61
54
|
return ctx
|
|
62
55
|
} catch (error: any) {
|
|
63
56
|
// Add the hook information to any errors
|
|
@@ -23,12 +23,10 @@ export const getPaginate = <H extends HookContext = HookContext>(
|
|
|
23
23
|
context: H,
|
|
24
24
|
): PaginationOptions | undefined => {
|
|
25
25
|
if (hasOwnProperty(context.params, 'paginate')) {
|
|
26
|
+
// covers `paginate: false` too (`false || undefined` -> undefined)
|
|
26
27
|
return (context.params.paginate as PaginationOptions) || undefined
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
if (context.params.paginate === false) {
|
|
30
|
-
return undefined
|
|
31
|
-
}
|
|
32
30
|
let options = context.service?.options || {}
|
|
33
31
|
|
|
34
32
|
options = {
|
package/src/utils/index.ts
CHANGED
|
@@ -12,6 +12,8 @@ export * from './iterate-find/iterate-find.util.js'
|
|
|
12
12
|
export * from './mutate-data/mutate-data.util.js'
|
|
13
13
|
export * from './mutate-result/mutate-result.util.js'
|
|
14
14
|
export * from './patch-batch/patch-batch.util.js'
|
|
15
|
+
export * from './replace-data/replace-data.util.js'
|
|
16
|
+
export * from './replace-result/replace-result.util.js'
|
|
15
17
|
export * from './skip-result/skip-result.util.js'
|
|
16
18
|
export * from './to-paginated/to-paginated.util.js'
|
|
17
19
|
export * from './transform-params/transform-params.util.js'
|
|
@@ -59,6 +59,7 @@ export async function* iterateFind<
|
|
|
59
59
|
},
|
|
60
60
|
paginate: {
|
|
61
61
|
default: options?.params?.paginate?.default ?? 10,
|
|
62
|
+
max: options?.params?.paginate?.max ?? 100,
|
|
62
63
|
},
|
|
63
64
|
}
|
|
64
65
|
|
|
@@ -67,6 +68,13 @@ export async function* iterateFind<
|
|
|
67
68
|
do {
|
|
68
69
|
result = await (service as any).find(params)
|
|
69
70
|
|
|
71
|
+
// Guard against an infinite loop: an empty page never advances $skip, so
|
|
72
|
+
// `total > $skip` could stay true forever (e.g. $limit:0, or a stale total
|
|
73
|
+
// when items are concurrently removed / filtered out by hooks).
|
|
74
|
+
if (!result.data.length) {
|
|
75
|
+
break
|
|
76
|
+
}
|
|
77
|
+
|
|
70
78
|
for (const item of result.data) {
|
|
71
79
|
yield item
|
|
72
80
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { HookContext } from '@feathersjs/feathers'
|
|
2
2
|
import { getDataIsArray } from '../get-data-is-array/get-data-is-array.util.js'
|
|
3
|
+
import { replaceData } from '../replace-data/replace-data.util.js'
|
|
3
4
|
import { isPromise } from '../../common/index.js'
|
|
4
5
|
import type { Promisable } from '../../internal.utils.js'
|
|
5
6
|
import type { TransformerInputFn } from '../../types.js'
|
|
@@ -26,7 +27,24 @@ export function mutateData<H extends HookContext = HookContext>(
|
|
|
26
27
|
return context
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
// single-item fast path: avoid allocating a wrapper array + a mapped array
|
|
31
|
+
// for the common single create/update/patch case.
|
|
32
|
+
if (!Array.isArray(context.data)) {
|
|
33
|
+
const item = context.data
|
|
34
|
+
const result = transformer(item, { context, i: 0 })
|
|
35
|
+
|
|
36
|
+
if (isPromise(result)) {
|
|
37
|
+
return result.then((res: any) => {
|
|
38
|
+
context.data = res ?? item
|
|
39
|
+
return context
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
context.data = result ?? item
|
|
44
|
+
return context
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const { data } = getDataIsArray(context)
|
|
30
48
|
|
|
31
49
|
if (!data.length) {
|
|
32
50
|
return context
|
|
@@ -46,9 +64,8 @@ export function mutateData<H extends HookContext = HookContext>(
|
|
|
46
64
|
})
|
|
47
65
|
|
|
48
66
|
function mutate(data: any) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return context
|
|
67
|
+
// delegate the array writeback (single is handled by the fast path above)
|
|
68
|
+
return replaceData(context, data)
|
|
52
69
|
}
|
|
53
70
|
|
|
54
71
|
if (hasPromises) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { HookContext, NextFunction } from '@feathersjs/feathers'
|
|
2
2
|
import { getResultIsArray } from '../get-result-is-array/get-result-is-array.util.js'
|
|
3
|
+
import { replaceResult } from '../replace-result/replace-result.util.js'
|
|
3
4
|
import { isPromise } from '../../common/index.js'
|
|
4
5
|
import { copy } from 'fast-copy'
|
|
5
6
|
import type { Promisable } from '../../internal.utils.js'
|
|
@@ -35,7 +36,7 @@ export function mutateResult<H extends HookContext = HookContext>(
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
function forResult(dispatch: boolean): Promisable<H> {
|
|
38
|
-
const { result
|
|
39
|
+
const { result } = getResultIsArray(context, { dispatch })
|
|
39
40
|
|
|
40
41
|
if (!result.length) {
|
|
41
42
|
return context
|
|
@@ -59,15 +60,8 @@ export function mutateResult<H extends HookContext = HookContext>(
|
|
|
59
60
|
r = options.transform(r)
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
} else if (isArray && !Array.isArray(context[key]) && context[key].data) {
|
|
65
|
-
context[key].data = r
|
|
66
|
-
} else {
|
|
67
|
-
context[key] = r
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return context
|
|
63
|
+
// delegate the single/array/paginated writeback to replaceResult
|
|
64
|
+
return replaceResult(context, r, { dispatch })
|
|
71
65
|
}
|
|
72
66
|
|
|
73
67
|
if (hasPromises) {
|
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
import type { Id, Params } from '@feathersjs/feathers'
|
|
2
|
-
import { dequal as deepEqual } from 'dequal'
|
|
3
2
|
import type { KeyOf } from '../../internal.utils.js'
|
|
4
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Deterministic, key-order-independent serialization used to group items with
|
|
6
|
+
* equal patch data in O(1) per item.
|
|
7
|
+
*/
|
|
8
|
+
const stableKey = (value: any): string =>
|
|
9
|
+
JSON.stringify(value, (_key, val) =>
|
|
10
|
+
val && typeof val === 'object' && !Array.isArray(val)
|
|
11
|
+
? Object.keys(val)
|
|
12
|
+
.sort()
|
|
13
|
+
.reduce<Record<string, any>>((acc, k) => {
|
|
14
|
+
acc[k] = val[k]
|
|
15
|
+
return acc
|
|
16
|
+
}, {})
|
|
17
|
+
: val,
|
|
18
|
+
)
|
|
19
|
+
|
|
5
20
|
export type PatchBatchOptions<IdKey extends string> = {
|
|
6
21
|
/** the key of the id property */
|
|
7
22
|
id?: IdKey
|
|
@@ -47,27 +62,30 @@ export function patchBatch<
|
|
|
47
62
|
items: T[],
|
|
48
63
|
options?: PatchBatchOptions<IdKey>,
|
|
49
64
|
): PatchBatchResultItem<R, P>[] {
|
|
50
|
-
const map: { ids: Id[]; data: R }[] = []
|
|
51
|
-
|
|
52
65
|
const idKey = options?.id ?? 'id'
|
|
53
66
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
67
|
+
// group items with identical (id-stripped) data in O(n) via a Map keyed by a
|
|
68
|
+
// stable serialization, instead of an O(n^2) findIndex + deepEqual scan.
|
|
69
|
+
const groups = new Map<string, { ids: Id[]; data: R }>()
|
|
70
|
+
|
|
71
|
+
for (const item of items) {
|
|
72
|
+
const source = item as Record<string, any>
|
|
73
|
+
const id = source[idKey] as Id
|
|
74
|
+
// shallow copy then drop the id key, so the caller's input is never mutated.
|
|
75
|
+
const data = { ...source }
|
|
76
|
+
delete data[idKey as any]
|
|
58
77
|
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
})
|
|
78
|
+
const key = stableKey(data)
|
|
79
|
+
const existing = groups.get(key)
|
|
62
80
|
|
|
63
|
-
if (
|
|
64
|
-
|
|
81
|
+
if (existing) {
|
|
82
|
+
existing.ids.push(id)
|
|
65
83
|
} else {
|
|
66
|
-
|
|
84
|
+
groups.set(key, { ids: [id], data: data as unknown as R })
|
|
67
85
|
}
|
|
68
86
|
}
|
|
69
87
|
|
|
70
|
-
return
|
|
88
|
+
return [...groups.values()].map(({ ids, data }) => {
|
|
71
89
|
return ids.length === 1
|
|
72
90
|
? ([ids[0], data, undefined] as PatchBatchResultItem<R, P>)
|
|
73
91
|
: ([
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { HookContext } from "@feathersjs/feathers";
|
|
2
|
+
import type { DataSingleHookContext } from "../../utility-types/hook-context.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Replaces `context.data` wholesale with the given items, preserving the original
|
|
6
|
+
* single-vs-array shape. This is the explicit inverse of `getDataIsArray`: get the
|
|
7
|
+
* data as an array, modify or replace the items, then write them back.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { getDataIsArray, replaceData } from 'feathers-utils/utils'
|
|
12
|
+
*
|
|
13
|
+
* const { data } = getDataIsArray(context)
|
|
14
|
+
* const next = data.map((item) => ({ ...item, slug: slugify(item.name) }))
|
|
15
|
+
* replaceData(context, next)
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* @see https://utils.feathersjs.com/utils/replace-data.html
|
|
19
|
+
*/
|
|
20
|
+
export function replaceData<H extends HookContext = HookContext>(
|
|
21
|
+
context: H,
|
|
22
|
+
data: DataSingleHookContext<H>[],
|
|
23
|
+
): H {
|
|
24
|
+
context.data = Array.isArray(context.data) ? data : data[0];
|
|
25
|
+
return context;
|
|
26
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { HookContext } from "@feathersjs/feathers";
|
|
2
|
+
import { copy } from "fast-copy";
|
|
3
|
+
import { getResultIsArray } from "../get-result-is-array/get-result-is-array.util.js";
|
|
4
|
+
import type { DispatchOption } from "../../types.js";
|
|
5
|
+
import type { ResultSingleHookContext } from "../../utility-types/hook-context.js";
|
|
6
|
+
|
|
7
|
+
export type ReplaceResultOptions = {
|
|
8
|
+
/**
|
|
9
|
+
* Also (or only) write to `context.dispatch`. `true` writes dispatch, `'both'`
|
|
10
|
+
* writes both `result` and `dispatch`. When dispatch is requested and not yet
|
|
11
|
+
* present, it is seeded from a clone of `context.result`.
|
|
12
|
+
*/
|
|
13
|
+
dispatch?: DispatchOption;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Replaces `context.result` (and/or `context.dispatch`) wholesale with the given
|
|
18
|
+
* items, preserving the original shape: single item, array, or paginated `{ data }`.
|
|
19
|
+
* This is the explicit inverse of `getResultIsArray`.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* import { getResultIsArray, replaceResult } from 'feathers-utils/utils'
|
|
24
|
+
*
|
|
25
|
+
* const { result } = getResultIsArray(context)
|
|
26
|
+
* replaceResult(context, result.filter((item) => item.public))
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* @see https://utils.feathersjs.com/utils/replace-result.html
|
|
30
|
+
*/
|
|
31
|
+
export function replaceResult<H extends HookContext = HookContext>(
|
|
32
|
+
context: H,
|
|
33
|
+
result: ResultSingleHookContext<H>[],
|
|
34
|
+
options?: ReplaceResultOptions,
|
|
35
|
+
): H {
|
|
36
|
+
if (!!options?.dispatch && !context.dispatch) {
|
|
37
|
+
context.dispatch = copy(context.result);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const write = (dispatch: boolean) => {
|
|
41
|
+
const { isArray, key } = getResultIsArray(context, { dispatch });
|
|
42
|
+
|
|
43
|
+
if (!isArray) {
|
|
44
|
+
context[key] = result[0];
|
|
45
|
+
} else if (!Array.isArray(context[key]) && context[key]?.data) {
|
|
46
|
+
context[key].data = result;
|
|
47
|
+
} else {
|
|
48
|
+
context[key] = result;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
if (options?.dispatch === "both") {
|
|
53
|
+
write(true);
|
|
54
|
+
write(false);
|
|
55
|
+
} else {
|
|
56
|
+
write(!!options?.dispatch);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return context;
|
|
60
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { Query } from '@feathersjs/feathers'
|
|
2
|
-
import isObject from 'lodash/isObject.js'
|
|
3
2
|
|
|
4
3
|
const arrayOperators = new Set(['$or', '$and', '$nor', '$not', '$in', '$nin'])
|
|
5
4
|
|
|
5
|
+
const isPlainObjectLike = (value: unknown): value is Record<string, any> =>
|
|
6
|
+
value !== null && typeof value === 'object'
|
|
7
|
+
|
|
6
8
|
/**
|
|
7
9
|
* Recursively normalizes a Feathers query object for order-independent comparison.
|
|
8
10
|
* Sorts object keys and sorts arrays within `$or`, `$and`, `$nor`, `$not`, `$in`,
|
|
@@ -35,21 +37,26 @@ const normalize = (value: any): any => {
|
|
|
35
37
|
return value.map(normalize)
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
if (!
|
|
40
|
+
if (!isPlainObjectLike(value)) {
|
|
39
41
|
return value
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
const sorted: Record<string, any> = {}
|
|
43
45
|
|
|
44
|
-
for (const key of Object.keys(value
|
|
45
|
-
const val =
|
|
46
|
+
for (const key of Object.keys(value).sort()) {
|
|
47
|
+
const val = value[key]
|
|
46
48
|
|
|
47
49
|
if (arrayOperators.has(key) && Array.isArray(val)) {
|
|
50
|
+
// Schwartzian transform: serialize each normalized element once, sort by
|
|
51
|
+
// that key, then unwrap. Avoids the O(n log n) repeated JSON.stringify of
|
|
52
|
+
// the previous comparator (which also returned 1 for equal elements).
|
|
48
53
|
sorted[key] = val
|
|
49
|
-
.map(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
)
|
|
54
|
+
.map((el) => {
|
|
55
|
+
const normalized = normalize(el)
|
|
56
|
+
return { k: JSON.stringify(normalized), v: normalized }
|
|
57
|
+
})
|
|
58
|
+
.sort((a, b) => (a.k < b.k ? -1 : a.k > b.k ? 1 : 0))
|
|
59
|
+
.map((entry) => entry.v)
|
|
53
60
|
} else {
|
|
54
61
|
sorted[key] = normalize(val)
|
|
55
62
|
}
|
|
@@ -43,7 +43,7 @@ const _walkQueryUtil = <Q extends Query>(
|
|
|
43
43
|
|
|
44
44
|
for (const key in query) {
|
|
45
45
|
if (
|
|
46
|
-
(key === '$or' || key === '$and' || key === '$nor'
|
|
46
|
+
(key === '$or' || key === '$and' || key === '$nor') &&
|
|
47
47
|
Array.isArray(query[key])
|
|
48
48
|
) {
|
|
49
49
|
let array = query[key]
|
|
@@ -122,7 +122,7 @@ const _walkQueryUtil = <Q extends Query>(
|
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
/**
|
|
125
|
-
* Walks every property of a Feathers query (including nested `$and`/`$or`/`$nor
|
|
125
|
+
* Walks every property of a Feathers query (including nested `$and`/`$or`/`$nor` arrays)
|
|
126
126
|
* and calls the `walker` function for each one. The walker receives the property name, operator,
|
|
127
127
|
* value, and path, and can return a replacement value. Returns a new query only if changes were made.
|
|
128
128
|
*
|