feathers-utils 5.1.0 → 6.0.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.
@@ -76,3 +76,512 @@ export function setData<H extends HookContext = HookContext>(
76
76
  return context;
77
77
  };
78
78
  }
79
+
80
+ if (import.meta.vitest) {
81
+ const { describe, it, assert, expect } = import.meta.vitest;
82
+
83
+ it("sets userId for single item", function () {
84
+ const methodsByType = {
85
+ before: ["create", "update", "patch", "remove"],
86
+ after: ["find", "get", "create", "update", "patch", "remove"],
87
+ };
88
+ Object.keys(methodsByType).forEach((type) => {
89
+ methodsByType[type].forEach((method) => {
90
+ const context = {
91
+ method,
92
+ type,
93
+ params: {
94
+ user: {
95
+ id: 1,
96
+ },
97
+ },
98
+ } as HookContext;
99
+
100
+ const dataOrResult = type === "before" ? "data" : "result";
101
+ context[dataOrResult] = {};
102
+
103
+ const result = setData("params.user.id", "userId")(context);
104
+ assert.strictEqual(
105
+ result[dataOrResult].userId,
106
+ 1,
107
+ `'${type}/${method}': ${dataOrResult} has 'userId:1'`,
108
+ );
109
+ });
110
+ });
111
+ });
112
+
113
+ it("overwrites userId for single item", function () {
114
+ const methodsByType = {
115
+ before: ["create", "update", "patch", "remove"],
116
+ after: ["find", "get", "create", "update", "patch", "remove"],
117
+ };
118
+ Object.keys(methodsByType).forEach((type) => {
119
+ methodsByType[type].forEach((method) => {
120
+ const context = {
121
+ method,
122
+ type,
123
+ params: {
124
+ user: {
125
+ id: 1,
126
+ },
127
+ },
128
+ } as HookContext;
129
+
130
+ const dataOrResult = type === "before" ? "data" : "result";
131
+ context[dataOrResult] = { userId: 2 };
132
+
133
+ const result = setData("params.user.id", "userId")(context);
134
+ assert.strictEqual(
135
+ result[dataOrResult].userId,
136
+ 1,
137
+ `'${type}/${method}': ${dataOrResult} has 'userId:1'`,
138
+ );
139
+ });
140
+ });
141
+ });
142
+
143
+ it("sets userId for multiple items", function () {
144
+ const methodsByType = {
145
+ before: ["create", "update", "patch", "remove"],
146
+ after: ["find", "get", "create", "update", "patch", "remove"],
147
+ };
148
+ Object.keys(methodsByType).forEach((type) => {
149
+ methodsByType[type].forEach((method) => {
150
+ const context = {
151
+ method,
152
+ type,
153
+ params: {
154
+ user: {
155
+ id: 1,
156
+ },
157
+ },
158
+ } as HookContext;
159
+
160
+ const dataOrResult = type === "before" ? "data" : "result";
161
+ context[dataOrResult] = [{}, {}, {}];
162
+
163
+ const result = setData("params.user.id", "userId")(context);
164
+ result[dataOrResult].forEach((item) => {
165
+ assert.strictEqual(
166
+ item.userId,
167
+ 1,
168
+ `'${type}/${method}': ${dataOrResult} has 'userId:1'`,
169
+ );
170
+ });
171
+ });
172
+ });
173
+ });
174
+
175
+ it("overwrites userId for multiple items", function () {
176
+ const methodsByType = {
177
+ before: ["create", "update", "patch", "remove"],
178
+ after: ["find", "get", "create", "update", "patch", "remove"],
179
+ };
180
+ Object.keys(methodsByType).forEach((type) => {
181
+ methodsByType[type].forEach((method) => {
182
+ const context = {
183
+ method,
184
+ type,
185
+ params: {
186
+ user: {
187
+ id: 1,
188
+ },
189
+ },
190
+ } as HookContext;
191
+
192
+ const dataOrResult = type === "before" ? "data" : "result";
193
+ context[dataOrResult] = [{ userId: 2 }, {}, { userId: "abc" }];
194
+
195
+ const result = setData("params.user.id", "userId")(context);
196
+ result[dataOrResult].forEach((item) => {
197
+ assert.strictEqual(
198
+ item.userId,
199
+ 1,
200
+ `'${type}/${method}': ${dataOrResult} has 'userId:1'`,
201
+ );
202
+ });
203
+ });
204
+ });
205
+ });
206
+
207
+ it("does not change createdById if 'params.user.id' is not provided", function () {
208
+ const methodsByType = {
209
+ before: ["create", "update", "patch", "remove"],
210
+ after: ["find", "get", "create", "update", "patch", "remove"],
211
+ };
212
+ Object.keys(methodsByType).forEach((type) => {
213
+ methodsByType[type].forEach((method) => {
214
+ const context = {
215
+ method,
216
+ type,
217
+ params: {},
218
+ } as HookContext;
219
+
220
+ const dataOrResult = type === "before" ? "data" : "result";
221
+ context[dataOrResult] = { userId: 2 };
222
+
223
+ const result = setData("params.user.id", "userId")(context);
224
+
225
+ assert.strictEqual(
226
+ result[dataOrResult].userId,
227
+ 2,
228
+ `'${type}/${method}': ${dataOrResult} has 'userId:2'`,
229
+ );
230
+ });
231
+ });
232
+ });
233
+
234
+ it("throws if 'external' is set and context.user.id is undefined", function () {
235
+ const methodsByType = {
236
+ before: ["create", "update", "patch", "remove"],
237
+ after: ["find", "get", "create", "update", "patch", "remove"],
238
+ };
239
+ Object.keys(methodsByType).forEach((type) => {
240
+ methodsByType[type].forEach((method) => {
241
+ const context = {
242
+ method,
243
+ type,
244
+ params: {
245
+ provider: "socket.io",
246
+ },
247
+ } as HookContext;
248
+
249
+ const dataOrResult = type === "before" ? "data" : "result";
250
+ context[dataOrResult] = {};
251
+
252
+ expect(() => setData("params.user.id", "userId")(context)).toThrow(
253
+ Forbidden,
254
+ );
255
+ });
256
+ });
257
+ });
258
+
259
+ it("passes if 'external' and 'allowUndefined: true'", function () {
260
+ const methodsByType = {
261
+ before: ["create", "update", "patch", "remove"],
262
+ after: ["find", "get", "create", "update", "patch", "remove"],
263
+ };
264
+ Object.keys(methodsByType).forEach((type) => {
265
+ methodsByType[type].forEach((method) => {
266
+ const context = {
267
+ method,
268
+ type,
269
+ provider: "socket.io",
270
+ params: {},
271
+ data: {},
272
+ } as unknown as HookContext;
273
+
274
+ assert.doesNotThrow(
275
+ () =>
276
+ setData("params.user.id", "userId", { allowUndefined: true })(
277
+ context,
278
+ ),
279
+ `'${type}/${method}': passes`,
280
+ );
281
+ });
282
+ });
283
+ });
284
+
285
+ it("passes if 'external' is set and context.user.id is undefined but overwrite: false", function () {
286
+ const methodsByType = {
287
+ before: ["create", "update", "patch", "remove"],
288
+ after: ["find", "get", "create", "update", "patch", "remove"],
289
+ };
290
+ Object.keys(methodsByType).forEach((type) => {
291
+ methodsByType[type].forEach((method) => {
292
+ const context = {
293
+ method,
294
+ type,
295
+ params: {
296
+ provider: "socket.io",
297
+ },
298
+ } as unknown as HookContext;
299
+
300
+ const dataOrResult = type === "before" ? "data" : "result";
301
+ context[dataOrResult] = { userId: 1 };
302
+
303
+ assert.doesNotThrow(
304
+ () =>
305
+ setData("params.user.id", "userId", { overwrite: false })(context),
306
+ `'${type}/${method}': passes`,
307
+ );
308
+ });
309
+ });
310
+ });
311
+
312
+ describe("overwrite: false", function () {
313
+ it("sets userId for single item", function () {
314
+ const methodsByType = {
315
+ before: ["create", "update", "patch", "remove"],
316
+ after: ["find", "get", "create", "update", "patch", "remove"],
317
+ };
318
+ Object.keys(methodsByType).forEach((type) => {
319
+ methodsByType[type].forEach((method) => {
320
+ const context = {
321
+ method,
322
+ type,
323
+ params: {
324
+ user: {
325
+ id: 1,
326
+ },
327
+ },
328
+ } as unknown as HookContext;
329
+
330
+ const dataOrResult = type === "before" ? "data" : "result";
331
+ context[dataOrResult] = {};
332
+
333
+ const result = setData("params.user.id", "userId", {
334
+ overwrite: false,
335
+ })(context);
336
+ assert.strictEqual(
337
+ result[dataOrResult].userId,
338
+ 1,
339
+ `'${type}/${method}': ${dataOrResult} has 'userId:1'`,
340
+ );
341
+ });
342
+ });
343
+ });
344
+
345
+ it("does not overwrite userId for single item", function () {
346
+ const methodsByType = {
347
+ before: ["create", "update", "patch", "remove"],
348
+ after: ["find", "get", "create", "update", "patch", "remove"],
349
+ };
350
+ Object.keys(methodsByType).forEach((type) => {
351
+ methodsByType[type].forEach((method) => {
352
+ const context = {
353
+ method,
354
+ type,
355
+ params: {
356
+ user: {
357
+ id: 1,
358
+ },
359
+ },
360
+ } as unknown as HookContext;
361
+
362
+ const dataOrResult = type === "before" ? "data" : "result";
363
+ context[dataOrResult] = { userId: 2 };
364
+
365
+ const result = setData("params.user.id", "userId", {
366
+ overwrite: false,
367
+ })(context);
368
+ assert.strictEqual(
369
+ result[dataOrResult].userId,
370
+ 2,
371
+ `'${type}/${method}': ${dataOrResult} has 'userId:2'`,
372
+ );
373
+ });
374
+ });
375
+ });
376
+
377
+ it("sets userId for multiple items", function () {
378
+ const methodsByType = {
379
+ before: ["create", "update", "patch", "remove"],
380
+ after: ["find", "get", "create", "update", "patch", "remove"],
381
+ };
382
+ Object.keys(methodsByType).forEach((type) => {
383
+ methodsByType[type].forEach((method) => {
384
+ const context = {
385
+ method,
386
+ type,
387
+ params: {
388
+ user: {
389
+ id: 1,
390
+ },
391
+ },
392
+ } as unknown as HookContext;
393
+
394
+ const dataOrResult = type === "before" ? "data" : "result";
395
+ context[dataOrResult] = [{}, {}, {}];
396
+
397
+ const result = setData("params.user.id", "userId", {
398
+ overwrite: false,
399
+ })(context);
400
+ result[dataOrResult].forEach((item) => {
401
+ assert.strictEqual(
402
+ item.userId,
403
+ 1,
404
+ `${type}/${method}': ${dataOrResult} has 'userId:1'`,
405
+ );
406
+ });
407
+ });
408
+ });
409
+ });
410
+
411
+ it("overwrites userId for multiple items", function () {
412
+ const methodsByType = {
413
+ before: ["create", "update", "patch", "remove"],
414
+ after: ["find", "get", "create", "update", "patch", "remove"],
415
+ };
416
+ Object.keys(methodsByType).forEach((type) => {
417
+ methodsByType[type].forEach((method) => {
418
+ const context = {
419
+ method,
420
+ type,
421
+ params: {
422
+ user: {
423
+ id: 1,
424
+ },
425
+ },
426
+ } as unknown as HookContext;
427
+
428
+ const dataOrResult = type === "before" ? "data" : "result";
429
+ context[dataOrResult] = [{ userId: 0 }, {}, { userId: 2 }];
430
+
431
+ const result = setData("params.user.id", "userId", {
432
+ overwrite: false,
433
+ })(context);
434
+ result[dataOrResult].forEach((item, i) => {
435
+ assert.strictEqual(
436
+ item.userId,
437
+ i,
438
+ `${type}/${method}': ${dataOrResult} has 'userId:${i}`,
439
+ );
440
+ });
441
+ });
442
+ });
443
+ });
444
+ });
445
+
446
+ describe("overwrite: predicate", function () {
447
+ it("overwrites userId for multiple items per predicate", function () {
448
+ const methodsByType = {
449
+ before: ["create", "update", "patch", "remove"],
450
+ after: ["find", "get", "create", "update", "patch", "remove"],
451
+ };
452
+ Object.keys(methodsByType).forEach((type) => {
453
+ methodsByType[type].forEach((method) => {
454
+ const context = {
455
+ method,
456
+ type,
457
+ params: {
458
+ user: {
459
+ id: 1,
460
+ },
461
+ },
462
+ } as unknown as HookContext;
463
+
464
+ const dataOrResult = type === "before" ? "data" : "result";
465
+ context[dataOrResult] = [{ userId: 2 }, {}, { userId: "abc" }];
466
+
467
+ const result = setData("params.user.id", "userId", {
468
+ overwrite: () => true,
469
+ })(context);
470
+ result[dataOrResult].forEach((item) => {
471
+ assert.strictEqual(
472
+ item.userId,
473
+ 1,
474
+ `'${type}/${method}': ${dataOrResult} has 'userId:1'`,
475
+ );
476
+ });
477
+ });
478
+ });
479
+ });
480
+
481
+ it("does not overwrite userId for single item by predicate", function () {
482
+ const methodsByType = {
483
+ before: ["create", "update", "patch", "remove"],
484
+ after: ["find", "get", "create", "update", "patch", "remove"],
485
+ };
486
+ Object.keys(methodsByType).forEach((type) => {
487
+ methodsByType[type].forEach((method) => {
488
+ const context = {
489
+ method,
490
+ type,
491
+ params: {
492
+ user: {
493
+ id: 1,
494
+ },
495
+ },
496
+ } as unknown as HookContext;
497
+
498
+ const dataOrResult = type === "before" ? "data" : "result";
499
+ context[dataOrResult] = { userId: 2 };
500
+
501
+ const result = setData("params.user.id", "userId", {
502
+ overwrite: (item) => item.userId == null,
503
+ })(context);
504
+ assert.strictEqual(
505
+ result[dataOrResult].userId,
506
+ 2,
507
+ `'${type}/${method}': ${dataOrResult} has 'userId:2'`,
508
+ );
509
+ });
510
+ });
511
+ });
512
+
513
+ it("predicate based on context", function () {
514
+ const methodsByType = {
515
+ before: ["create", "update", "patch", "remove"],
516
+ after: ["find", "get", "create", "update", "patch", "remove"],
517
+ };
518
+ Object.keys(methodsByType).forEach((type) => {
519
+ methodsByType[type].forEach((method) => {
520
+ const context = {
521
+ method,
522
+ type,
523
+ params: {
524
+ user: {
525
+ id: 1,
526
+ },
527
+ },
528
+ } as unknown as HookContext;
529
+
530
+ const dataOrResult = type === "before" ? "data" : "result";
531
+ context[dataOrResult] = { userId: 2 };
532
+
533
+ const result = setData("params.user.id", "userId", {
534
+ overwrite: (item, context) => context.type === "before",
535
+ })(context);
536
+ if (type === "before") {
537
+ assert.strictEqual(
538
+ result[dataOrResult].userId,
539
+ 1,
540
+ `'${type}/${method}': ${dataOrResult} has 'userId:1'`,
541
+ );
542
+ } else {
543
+ assert.strictEqual(
544
+ result[dataOrResult].userId,
545
+ 2,
546
+ `'${type}/${method}': ${dataOrResult} has 'userId:2'`,
547
+ );
548
+ }
549
+ });
550
+ });
551
+ });
552
+
553
+ it("overwrites userId for multiple items by predicate", function () {
554
+ const methodsByType = {
555
+ before: ["create", "update", "patch", "remove"],
556
+ after: ["find", "get", "create", "update", "patch", "remove"],
557
+ };
558
+ Object.keys(methodsByType).forEach((type) => {
559
+ methodsByType[type].forEach((method) => {
560
+ const context = {
561
+ method,
562
+ type,
563
+ params: {
564
+ user: {
565
+ id: 1,
566
+ },
567
+ },
568
+ } as unknown as HookContext;
569
+
570
+ const dataOrResult = type === "before" ? "data" : "result";
571
+ context[dataOrResult] = [{ userId: 0 }, {}, { userId: 2 }];
572
+
573
+ const result = setData("params.user.id", "userId", {
574
+ overwrite: (item) => item.userId == null,
575
+ })(context);
576
+ result[dataOrResult].forEach((item, i) => {
577
+ assert.strictEqual(
578
+ item.userId,
579
+ i,
580
+ `${type}/${method}': ${dataOrResult} has 'userId:${i}`,
581
+ );
582
+ });
583
+ });
584
+ });
585
+ });
586
+ });
587
+ }
@@ -3,15 +3,7 @@ import _debounce from "lodash/debounce.js";
3
3
  import type { DebouncedFunc } from "lodash";
4
4
  import type { Application, Id } from "@feathersjs/feathers";
5
5
  import type { DebouncedFunctionApp, DebouncedStoreOptions } from "./types";
6
-
7
- export const makeDefaultOptions = (): DebouncedStoreOptions => {
8
- return {
9
- leading: false,
10
- maxWait: undefined,
11
- trailing: true,
12
- wait: 100,
13
- };
14
- };
6
+ import { makeDefaultOptions } from "./utils";
15
7
 
16
8
  export type DebouncedService<T = any> = T & {
17
9
  debouncedStore: DebouncedStore;
@@ -1,6 +1,7 @@
1
1
  import type { Application } from "@feathersjs/feathers/lib";
2
- import { DebouncedStore, makeDefaultOptions } from "./DebouncedStore";
2
+ import { DebouncedStore } from "./DebouncedStore";
3
3
  import type { DebouncedStoreOptions, InitDebounceMixinOptions } from "./types";
4
+ import { makeDefaultOptions } from "./utils";
4
5
 
5
6
  export function debounceMixin(
6
7
  options?: Partial<InitDebounceMixinOptions>,
@@ -0,0 +1,10 @@
1
+ import type { DebouncedStoreOptions } from "./types";
2
+
3
+ export const makeDefaultOptions = (): DebouncedStoreOptions => {
4
+ return {
5
+ leading: false,
6
+ maxWait: undefined,
7
+ trailing: true,
8
+ wait: 100,
9
+ };
10
+ };
package/src/types.ts CHANGED
@@ -2,6 +2,7 @@ import type { HookContext } from "@feathersjs/feathers";
2
2
 
3
3
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
4
4
  export type Predicate<T = any> = (item: T) => boolean;
5
+
5
6
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
6
7
  export type PredicateWithContext<T = any> = (
7
8
  item: T,
@@ -22,3 +22,5 @@ export type ReturnSyncHook<H extends HookContext = HookContext> = (
22
22
  export type ReturnAsyncHook<H extends HookContext = HookContext> = (
23
23
  context: H,
24
24
  ) => Promise<H>;
25
+
26
+ export type KeyOf<T> = Extract<keyof T, string>;
@@ -0,0 +1,16 @@
1
+ export const hasOwnProperty = (
2
+ obj: Record<string, unknown>,
3
+ ...keys: string[]
4
+ ): boolean => {
5
+ return keys.some((x) => Object.prototype.hasOwnProperty.call(obj, x));
6
+ };
7
+
8
+ export const isObject = (item: unknown): boolean =>
9
+ !!item && typeof item === "object" && !Array.isArray(item);
10
+
11
+ export const isPlainObject = (value: unknown): boolean =>
12
+ isObject(value) && value.constructor === {}.constructor;
13
+
14
+ export const isEmpty = (obj: unknown): boolean =>
15
+ [Object, Array].includes((obj || {}).constructor as any) &&
16
+ !Object.keys(obj || {}).length;
@@ -0,0 +1,109 @@
1
+ import type { Query } from "@feathersjs/feathers";
2
+ import _set from "lodash/set.js";
3
+ import { isObject } from "./_utils.internal";
4
+
5
+ export function deflattenQuery(query: Query) {
6
+ const result: Query = {};
7
+
8
+ Object.keys(query).forEach((key) => {
9
+ const value = query[key];
10
+
11
+ if (Array.isArray(value)) {
12
+ _set(result, key, value.map(deflattenQuery));
13
+ return;
14
+ }
15
+
16
+ if (isObject(value)) {
17
+ _set(result, key, deflattenQuery(value));
18
+ return;
19
+ }
20
+
21
+ _set(result, key, query[key]);
22
+ });
23
+
24
+ return result;
25
+ }
26
+
27
+ if (import.meta.vitest) {
28
+ const { describe, it, expect } = import.meta.vitest;
29
+
30
+ describe("deflattenQuery", () => {
31
+ it("should deflatten a query", () => {
32
+ expect(
33
+ deflattenQuery({
34
+ a: 1,
35
+ "b.c": 1,
36
+ "b.d": 1,
37
+ "e.f.g": 1,
38
+ }),
39
+ ).toEqual({
40
+ a: 1,
41
+ b: {
42
+ c: 1,
43
+ d: 1,
44
+ },
45
+ e: {
46
+ f: {
47
+ g: 1,
48
+ },
49
+ },
50
+ });
51
+ });
52
+ });
53
+
54
+ it("should handle operators", () => {
55
+ expect(
56
+ deflattenQuery({
57
+ "a.b": 1,
58
+ c: {
59
+ $gt: 1,
60
+ $lte: 1,
61
+ },
62
+ }),
63
+ ).toEqual({
64
+ a: {
65
+ b: 1,
66
+ },
67
+ c: {
68
+ $gt: 1,
69
+ $lte: 1,
70
+ },
71
+ });
72
+ });
73
+
74
+ it("should handle $or / $and", () => {
75
+ expect(
76
+ deflattenQuery({
77
+ "a.b": 1,
78
+ $and: [
79
+ {
80
+ "c.d": 1,
81
+ },
82
+ ],
83
+ $or: [
84
+ {
85
+ "e.f": 1,
86
+ },
87
+ ],
88
+ }),
89
+ ).toEqual({
90
+ a: {
91
+ b: 1,
92
+ },
93
+ $and: [
94
+ {
95
+ c: {
96
+ d: 1,
97
+ },
98
+ },
99
+ ],
100
+ $or: [
101
+ {
102
+ e: {
103
+ f: 1,
104
+ },
105
+ },
106
+ ],
107
+ });
108
+ });
109
+ }