feathers-utils 5.0.2 → 5.2.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 (39) hide show
  1. package/README.md +4 -0
  2. package/dist/index.cjs +181 -26
  3. package/dist/index.d.cts +20 -5
  4. package/dist/index.d.mts +20 -5
  5. package/dist/index.d.ts +20 -5
  6. package/dist/index.mjs +175 -23
  7. package/package.json +18 -20
  8. package/src/filters/object.ts +2 -2
  9. package/src/hooks/checkMulti.ts +151 -0
  10. package/src/hooks/from-client-for-server/common.ts +1 -0
  11. package/src/hooks/from-client-for-server/index.ts +2 -0
  12. package/src/hooks/from-client-for-server/paramsForServer.ts +100 -0
  13. package/src/hooks/from-client-for-server/paramsFromClient.ts +105 -0
  14. package/src/hooks/index.ts +1 -0
  15. package/src/hooks/setData.ts +509 -0
  16. package/src/mixins/debounce-mixin/DebouncedStore.ts +1 -9
  17. package/src/mixins/debounce-mixin/debounceMixin.ts +2 -1
  18. package/src/mixins/debounce-mixin/utils.ts +10 -0
  19. package/src/types.ts +1 -0
  20. package/src/utils/_utils.internal.ts +16 -0
  21. package/src/utils/deflattenQuery.ts +109 -0
  22. package/src/utils/filterQuery.ts +112 -40
  23. package/src/utils/flattenQuery.ts +198 -0
  24. package/src/utils/getItemsIsArray.ts +279 -0
  25. package/src/utils/getPaginate.ts +74 -1
  26. package/src/utils/index.ts +2 -0
  27. package/src/utils/isMulti.ts +51 -0
  28. package/src/utils/isPaginated.ts +72 -0
  29. package/src/utils/markHookForSkip.ts +411 -0
  30. package/src/utils/mergeQuery/mergeArrays.ts +68 -0
  31. package/src/utils/mergeQuery/mergeQuery.ts +464 -3
  32. package/src/utils/mergeQuery/types.ts +0 -1
  33. package/src/utils/mergeQuery/utils.ts +93 -5
  34. package/src/utils/pushSet.ts +67 -1
  35. package/src/utils/setQueryKeySafely.ts +169 -4
  36. package/src/utils/setResultEmpty.ts +260 -0
  37. package/src/utils/shouldSkip.ts +121 -0
  38. package/src/utils/validateQueryProperty.ts +3 -6
  39. package/src/utils/internal.utils.ts +0 -9
@@ -0,0 +1,105 @@
1
+ import type { HookContext } from "@feathersjs/feathers";
2
+ import { FROM_CLIENT_FOR_SERVER_DEFAULT_KEY } from "./common";
3
+
4
+ export function defineParamsFromClient(keyToHide: string) {
5
+ return function paramsFromClient(
6
+ ...whitelist: string[]
7
+ ): (context: HookContext) => HookContext {
8
+ return (context: HookContext): HookContext => {
9
+ if (
10
+ !context.params?.query?.[keyToHide] ||
11
+ typeof context.params.query[keyToHide] !== "object"
12
+ ) {
13
+ return context;
14
+ }
15
+
16
+ const params = {
17
+ ...context.params,
18
+ query: {
19
+ ...context.params.query,
20
+ [keyToHide]: {
21
+ ...context.params.query[keyToHide],
22
+ },
23
+ },
24
+ };
25
+
26
+ const client = params.query[keyToHide];
27
+
28
+ whitelist.forEach((key) => {
29
+ if (key in client) {
30
+ params[key] = client[key];
31
+ delete client[key];
32
+ }
33
+ });
34
+
35
+ if (Object.keys(client).length === 0) {
36
+ delete params.query[keyToHide];
37
+ }
38
+
39
+ context.params = params;
40
+
41
+ return context;
42
+ };
43
+ };
44
+ }
45
+
46
+ export const paramsFromClient = defineParamsFromClient(
47
+ FROM_CLIENT_FOR_SERVER_DEFAULT_KEY,
48
+ );
49
+
50
+ if (import.meta.vitest) {
51
+ const { it, expect } = import.meta.vitest;
52
+
53
+ it("should move params to query._$client", () => {
54
+ expect(
55
+ paramsFromClient(
56
+ "a",
57
+ "b",
58
+ )({
59
+ params: {
60
+ query: {
61
+ _$client: {
62
+ a: 1,
63
+ b: 2,
64
+ },
65
+ c: 3,
66
+ },
67
+ },
68
+ } as HookContext),
69
+ ).toEqual({
70
+ params: {
71
+ a: 1,
72
+ b: 2,
73
+ query: {
74
+ c: 3,
75
+ },
76
+ },
77
+ });
78
+ });
79
+
80
+ it("should move params to query._$client and leave remaining", () => {
81
+ expect(
82
+ paramsFromClient("a")({
83
+ params: {
84
+ query: {
85
+ _$client: {
86
+ a: 1,
87
+ b: 2,
88
+ },
89
+ c: 3,
90
+ },
91
+ },
92
+ } as HookContext),
93
+ ).toEqual({
94
+ params: {
95
+ a: 1,
96
+ query: {
97
+ _$client: {
98
+ b: 2,
99
+ },
100
+ c: 3,
101
+ },
102
+ },
103
+ });
104
+ });
105
+ }
@@ -6,3 +6,4 @@ export * from "./parseFields";
6
6
  export * from "./removeRelated";
7
7
  export * from "./runPerItem";
8
8
  export * from "./setData";
9
+ export * from "./from-client-for-server";
@@ -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,
@@ -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;