piper-utils 1.1.68 → 1.1.70

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.
@@ -1,682 +1,682 @@
1
- import { createFilters } from './createFilters.js';
2
- import { defaultFilters } from './defaultFilters.js';
3
- import { defaultFiltersWithSubSchema, event, eventWithSearchString, eventWithSearchStringNumber, eventWithSingleElementAsArray, eventWithSingleElementInArray, manyStringFields } from './mocks/mocks.js';
4
- import DB from 'sequelize';
5
-
6
- describe('createFilters', function () {
7
- it('should produce case insensitive string filters using LOWER and Op.like', function () {
8
- // Arrange
9
- const mockEvent = {
10
- ...event,
11
- queryStringParameters: {
12
- ...event.queryStringParameters,
13
- char: 'MiXeDcAsE'
14
- }
15
- };
16
-
17
- // Act
18
- const where = createFilters(mockEvent, defaultFiltersWithSubSchema);
19
-
20
- // Assert
21
- expect(where[DB.Op.and]).toBeDefined();
22
- const charCondition = where[DB.Op.and].find(c => c.attribute?.args?.[0]?.col === 'char');
23
- expect(charCondition).toBeDefined();
24
- expect(charCondition.attribute.fn).toBe('LOWER');
25
- expect(charCondition.logic[DB.Op.like]).toBe('%mixedcase%');
26
- });
27
-
28
- it('should return a Sequelize WHERE object and include createdAt', function () {
29
- // Act
30
- const where = createFilters(event, defaultFiltersWithSubSchema);
31
-
32
- // Assert
33
- expect(where.createdAt).toBeDefined();
34
- expect(where.decimal).toEqual({ [DB.Op.or]: [-4565, 4565] });
35
- expect(where[DB.Op.and]).toBeDefined();
36
- expect(where.boolean).toEqual({ [DB.Op.eq]: true });
37
- expect(where.array).toEqual({ [DB.Op.in]: ['10', '20', '30'] });
38
- expect(where['$snowman.snowmanType$']).toEqual({ [DB.Op.eq]: 10 });
39
- expect(where.notOnObject).not.toBeDefined();
40
- expect(where.date).not.toBeDefined();
41
- });
42
-
43
- it('should handle single elements in the in array', function () {
44
- // Act
45
- const where = createFilters(eventWithSingleElementInArray, defaultFiltersWithSubSchema);
46
-
47
- // Assert
48
- expect(where.array).toEqual({ [DB.Op.in]: ['10'] });
49
- expect(where.notOnObject).not.toBeDefined();
50
- expect(where.date).not.toBeDefined();
51
- });
52
-
53
- it('should handle the array value being one value', function () {
54
- // Act
55
- const where = createFilters(eventWithSingleElementAsArray, defaultFiltersWithSubSchema);
56
-
57
- // Assert
58
- expect(where.array).toEqual({ [DB.Op.in]: ['10'] });
59
- expect(where.notOnObject).not.toBeDefined();
60
- expect(where.date).not.toBeDefined();
61
- });
62
-
63
- it('should handle searchString with case insensitive LOWER + Op.like conditions', function () {
64
- // Act
65
- const where = createFilters(eventWithSearchString, manyStringFields);
66
-
67
- // Assert
68
- const orSymbols = Object.getOwnPropertySymbols(where);
69
- expect(orSymbols.length).toBeGreaterThan(0);
70
- const orConditions = where[DB.Op.or];
71
- const lowerConditions = orConditions.filter(c => c.attribute?.fn === 'LOWER');
72
- expect(lowerConditions.length).toBeGreaterThan(0);
73
- lowerConditions.forEach(c => {
74
- expect(c.logic[DB.Op.like]).toBe('%alpha-search%');
75
- });
76
- });
77
-
78
- it('should handle searchString if value is a number', function () {
79
- // Act
80
- const where = createFilters(eventWithSearchStringNumber, manyStringFields);
81
-
82
- // Assert
83
- const orSymbols = Object.getOwnPropertySymbols(where);
84
- expect(orSymbols.length).toBeGreaterThan(0);
85
- });
86
-
87
- it('should handle searchString sub fields', function () {
88
- // Act
89
- const where = createFilters(eventWithSearchStringNumber, defaultFiltersWithSubSchema);
90
-
91
- // Assert
92
- const orSymbols = Object.getOwnPropertySymbols(where);
93
- expect(orSymbols.length).toBeGreaterThan(0);
94
- });
95
-
96
- it('should not place string filter fields as direct keys on the where object', function () {
97
- // Act
98
- const where = createFilters(event, defaultFiltersWithSubSchema);
99
-
100
- // Assert - char is a string field, it should be in Op.and not as a direct key
101
- expect(where.char).not.toBeDefined();
102
- expect(where[DB.Op.and]).toBeDefined();
103
- });
104
- });
105
-
106
- describe('JSONB case insensitive filters', () => {
107
- it('should lowercase a single-level JSONB path search', function () {
108
- // Arrange
109
- const mockEvent = {
110
- queryStringParameters: {
111
- 'token.cardHolderName': 'GREGORY HERBERT'
112
- },
113
- requestContext: {
114
- authorizer: {
115
- claims: {
116
- 'custom:businessIds': '[\'1\']',
117
- 'custom:AR': '{"businessIds":{"1":"A"}}'
118
- }
119
- }
120
- }
121
- };
122
- const mockFilter = {
123
- token: { type: 'jsonb', field: 'token' }
124
- };
125
-
126
- // Act
127
- const where = createFilters(mockEvent, mockFilter);
128
-
129
- // Assert
130
- expect(where[DB.Op.and]).toBeDefined();
131
- expect(where[DB.Op.and].length).toBe(1);
132
- const condition = where[DB.Op.and][0];
133
- expect(condition.attribute.fn).toBe('LOWER');
134
- expect(condition.logic[DB.Op.like]).toBe('%gregory herbert%');
135
- });
136
-
137
- it('should use jsonb_extract_path_text with the correct column and path parts', function () {
138
- // Arrange
139
- const mockEvent = {
140
- queryStringParameters: {
141
- 'token.cardHolderName': 'Test Name'
142
- },
143
- requestContext: {
144
- authorizer: {
145
- claims: {
146
- 'custom:businessIds': '[\'1\']',
147
- 'custom:AR': '{"businessIds":{"1":"A"}}'
148
- }
149
- }
150
- }
151
- };
152
- const mockFilter = {
153
- token: { type: 'jsonb', field: 'token' }
154
- };
155
-
156
- // Act
157
- const where = createFilters(mockEvent, mockFilter);
158
-
159
- // Assert
160
- const condition = where[DB.Op.and][0];
161
- const innerFn = condition.attribute.args[0];
162
- expect(innerFn.fn).toBe('jsonb_extract_path_text');
163
- expect(innerFn.args[0].col).toBe('token');
164
- expect(innerFn.args[1]).toBe('cardHolderName');
165
- });
166
-
167
- it('should pass each segment as a separate arg for deeply nested JSONB paths', function () {
168
- // Arrange
169
- const mockEvent = {
170
- queryStringParameters: {
171
- 'paymentProviderData.payment.result.message': 'THE NAME'
172
- },
173
- requestContext: {
174
- authorizer: {
175
- claims: {
176
- 'custom:businessIds': '[\'1\']',
177
- 'custom:AR': '{"businessIds":{"1":"A"}}'
178
- }
179
- }
180
- }
181
- };
182
- const mockFilter = {
183
- paymentProviderData: { type: 'jsonb', field: 'paymentProviderData' }
184
- };
185
-
186
- // Act
187
- const where = createFilters(mockEvent, mockFilter);
188
-
189
- // Assert
190
- const condition = where[DB.Op.and][0];
191
- const innerFn = condition.attribute.args[0];
192
- expect(innerFn.fn).toBe('jsonb_extract_path_text');
193
- expect(innerFn.args[0].col).toBe('paymentProviderData');
194
- expect(innerFn.args[1]).toBe('payment');
195
- expect(innerFn.args[2]).toBe('result');
196
- expect(innerFn.args[3]).toBe('message');
197
- expect(condition.logic[DB.Op.like]).toBe('%the name%');
198
- });
199
-
200
- it('should create a condition for each JSONB query param', function () {
201
- // Arrange
202
- const mockEvent = {
203
- queryStringParameters: {
204
- 'token.cardHolderName': 'Gregory',
205
- 'token.cardType': 'MASTERCARD'
206
- },
207
- requestContext: {
208
- authorizer: {
209
- claims: {
210
- 'custom:businessIds': '[\'1\']',
211
- 'custom:AR': '{"businessIds":{"1":"A"}}'
212
- }
213
- }
214
- }
215
- };
216
- const mockFilter = {
217
- token: { type: 'jsonb', field: 'token' }
218
- };
219
-
220
- // Act
221
- const where = createFilters(mockEvent, mockFilter);
222
-
223
- // Assert
224
- expect(where[DB.Op.and].length).toBe(2);
225
- const values = where[DB.Op.and].map(c => c.logic[DB.Op.like]);
226
- expect(values).toContain('%gregory%');
227
- expect(values).toContain('%mastercard%');
228
- });
229
-
230
- it('should not add Op.and when no JSONB query params match the filter prefix', function () {
231
- // Arrange
232
- const mockEvent = {
233
- queryStringParameters: {
234
- 'unrelated': 'value'
235
- },
236
- requestContext: {
237
- authorizer: {
238
- claims: {
239
- 'custom:businessIds': '[\'1\']',
240
- 'custom:AR': '{"businessIds":{"1":"A"}}'
241
- }
242
- }
243
- }
244
- };
245
- const mockFilter = {
246
- token: { type: 'jsonb', field: 'token' }
247
- };
248
-
249
- // Act
250
- const where = createFilters(mockEvent, mockFilter);
251
-
252
- // Assert
253
- expect(where[DB.Op.and]).not.toBeDefined();
254
- });
255
-
256
- it('should combine JSONB conditions with string field conditions in Op.and', function () {
257
- // Arrange
258
- const mockEvent = {
259
- queryStringParameters: {
260
- name: 'John',
261
- 'token.cardHolderName': 'GREGORY'
262
- },
263
- requestContext: {
264
- authorizer: {
265
- claims: {
266
- 'custom:businessIds': '[\'1\']',
267
- 'custom:AR': '{"businessIds":{"1":"A"}}'
268
- }
269
- }
270
- }
271
- };
272
- const mockFilter = {
273
- name: { filterType: DB.Op.iLike },
274
- token: { type: 'jsonb', field: 'token' }
275
- };
276
-
277
- // Act
278
- const where = createFilters(mockEvent, mockFilter);
279
-
280
- // Assert
281
- expect(where[DB.Op.and].length).toBe(2);
282
- const stringCondition = where[DB.Op.and].find(c => c.attribute?.args?.[0]?.col === 'name');
283
- expect(stringCondition).toBeDefined();
284
- expect(stringCondition.logic[DB.Op.like]).toBe('%john%');
285
- const jsonbCondition = where[DB.Op.and].find(c => c.attribute?.args?.[0]?.fn === 'jsonb_extract_path_text');
286
- expect(jsonbCondition).toBeDefined();
287
- expect(jsonbCondition.logic[DB.Op.like]).toBe('%gregory%');
288
- });
289
-
290
- it('should handle a JSON object passed as the JSONB field value (e.g. ?token={"cardHolderName":"Test"})', function () {
291
- // Arrange — simulates ?token={"cardHolderName":"Test"} (URL-decoded)
292
- const mockEvent = {
293
- queryStringParameters: {
294
- token: '{"cardHolderName":"Test"}'
295
- },
296
- requestContext: {
297
- authorizer: {
298
- claims: {
299
- 'custom:businessIds': '[\'1\']',
300
- 'custom:AR': '{"businessIds":{"1":"A"}}'
301
- }
302
- }
303
- }
304
- };
305
- const mockFilter = {
306
- token: { type: 'jsonb', field: 'token' }
307
- };
308
-
309
- // Act
310
- const where = createFilters(mockEvent, mockFilter);
311
-
312
- // Assert — should NOT place token as a direct key
313
- expect(where.token).not.toBeDefined();
314
- // Should create a JSONB condition in Op.and
315
- expect(where[DB.Op.and]).toBeDefined();
316
- expect(where[DB.Op.and].length).toBe(1);
317
- const condition = where[DB.Op.and][0];
318
- expect(condition.attribute.fn).toBe('LOWER');
319
- const innerFn = condition.attribute.args[0];
320
- expect(innerFn.fn).toBe('jsonb_extract_path_text');
321
- expect(innerFn.args[0].col).toBe('token');
322
- expect(innerFn.args[1]).toBe('cardHolderName');
323
- expect(condition.logic[DB.Op.like]).toBe('%test%');
324
- });
325
-
326
- it('should handle a JSON object with multiple keys passed as the JSONB field value', function () {
327
- // Arrange — simulates ?token={"cardHolderName":"Test","cardType":"Mastercard"}
328
- const mockEvent = {
329
- queryStringParameters: {
330
- token: '{"cardHolderName":"Test","cardType":"Mastercard"}'
331
- },
332
- requestContext: {
333
- authorizer: {
334
- claims: {
335
- 'custom:businessIds': '[\'1\']',
336
- 'custom:AR': '{"businessIds":{"1":"A"}}'
337
- }
338
- }
339
- }
340
- };
341
- const mockFilter = {
342
- token: { type: 'jsonb', field: 'token' }
343
- };
344
-
345
- // Act
346
- const where = createFilters(mockEvent, mockFilter);
347
-
348
- // Assert
349
- expect(where[DB.Op.and].length).toBe(2);
350
- const values = where[DB.Op.and].map(c => c.logic[DB.Op.like]);
351
- expect(values).toContain('%test%');
352
- expect(values).toContain('%mastercard%');
353
- });
354
-
355
- it('should not place the JSONB field as a direct key on the where object', function () {
356
- // Arrange
357
- const mockEvent = {
358
- queryStringParameters: {
359
- 'token.cardHolderName': 'Test'
360
- },
361
- requestContext: {
362
- authorizer: {
363
- claims: {
364
- 'custom:businessIds': '[\'1\']',
365
- 'custom:AR': '{"businessIds":{"1":"A"}}'
366
- }
367
- }
368
- }
369
- };
370
- const mockFilter = {
371
- token: { type: 'jsonb', field: 'token' }
372
- };
373
-
374
- // Act
375
- const where = createFilters(mockEvent, mockFilter);
376
-
377
- // Assert
378
- expect(where.token).not.toBeDefined();
379
- expect(where[DB.Op.and]).toBeDefined();
380
- });
381
- });
382
-
383
- describe('JSONB filters via defaultFilters integration', () => {
384
- it('should case-insensitive search a JSONB field using a JSON object query param', function () {
385
- // Arrange — schema mirrors payment.js: token is DB.JSONB
386
- const schema = {
387
- token: { type: DB.JSONB(), defaultValue: {} },
388
- amount: { type: DB.DECIMAL() }
389
- };
390
- const filters = defaultFilters(schema);
391
- const mockEvent = {
392
- queryStringParameters: {
393
- token: '{"cardHolderName":"Test"}'
394
- },
395
- requestContext: {
396
- authorizer: {
397
- claims: {
398
- 'custom:AR': '{"businessIds":{"1":"A"}}'
399
- }
400
- }
401
- }
402
- };
403
-
404
- // Act
405
- const where = createFilters(mockEvent, filters);
406
-
407
- // Assert — token should not be a direct key (no Op.contains)
408
- expect(where.token).not.toBeDefined();
409
- // Should produce a case-insensitive jsonb_extract_path_text condition
410
- expect(where[DB.Op.and]).toBeDefined();
411
- const condition = where[DB.Op.and][0];
412
- expect(condition.attribute.fn).toBe('LOWER');
413
- const innerFn = condition.attribute.args[0];
414
- expect(innerFn.fn).toBe('jsonb_extract_path_text');
415
- expect(innerFn.args[0].col).toBe('token');
416
- expect(innerFn.args[1]).toBe('cardHolderName');
417
- expect(condition.logic[DB.Op.like]).toBe('%test%');
418
- });
419
-
420
- it('should case-insensitive search a JSONB field using dot-notation query param', function () {
421
- // Arrange
422
- const schema = {
423
- token: { type: DB.JSONB(), defaultValue: {} }
424
- };
425
- const filters = defaultFilters(schema);
426
- const mockEvent = {
427
- queryStringParameters: {
428
- 'token.lastFour': '8473'
429
- },
430
- requestContext: {
431
- authorizer: {
432
- claims: {
433
- 'custom:AR': '{"businessIds":{"1":"A"}}'
434
- }
435
- }
436
- }
437
- };
438
-
439
- // Act
440
- const where = createFilters(mockEvent, filters);
441
-
442
- // Assert
443
- expect(where.token).not.toBeDefined();
444
- expect(where[DB.Op.and]).toBeDefined();
445
- const condition = where[DB.Op.and][0];
446
- const innerFn = condition.attribute.args[0];
447
- expect(innerFn.fn).toBe('jsonb_extract_path_text');
448
- expect(innerFn.args[0].col).toBe('token');
449
- expect(innerFn.args[1]).toBe('lastFour');
450
- expect(condition.logic[DB.Op.like]).toBe('%8473%');
451
- });
452
-
453
- it('should handle multiple JSONB columns in the same schema', function () {
454
- // Arrange — mirrors payment schema with token and paymentProviderData
455
- const schema = {
456
- token: { type: DB.JSONB(), defaultValue: {} },
457
- paymentProviderData: { type: DB.JSONB() }
458
- };
459
- const filters = defaultFilters(schema);
460
- const mockEvent = {
461
- queryStringParameters: {
462
- 'token.cardHolderName': 'Gregory',
463
- 'paymentProviderData.payment.result.message': 'APPROVED'
464
- },
465
- requestContext: {
466
- authorizer: {
467
- claims: {
468
- 'custom:AR': '{"businessIds":{"1":"A"}}'
469
- }
470
- }
471
- }
472
- };
473
-
474
- // Act
475
- const where = createFilters(mockEvent, filters);
476
-
477
- // Assert
478
- expect(where.token).not.toBeDefined();
479
- expect(where.paymentProviderData).not.toBeDefined();
480
- expect(where[DB.Op.and].length).toBe(2);
481
- const tokenCondition = where[DB.Op.and].find(c => c.attribute?.args?.[0]?.args?.[0]?.col === 'token');
482
- expect(tokenCondition).toBeDefined();
483
- expect(tokenCondition.logic[DB.Op.like]).toBe('%gregory%');
484
- const providerCondition = where[DB.Op.and].find(c => c.attribute?.args?.[0]?.args?.[0]?.col === 'paymentProviderData');
485
- expect(providerCondition).toBeDefined();
486
- expect(providerCondition.logic[DB.Op.like]).toBe('%approved%');
487
- });
488
-
489
- it('should combine JSONB filters with non-JSONB filters from the same schema', function () {
490
- // Arrange
491
- const schema = {
492
- token: { type: DB.JSONB(), defaultValue: {} },
493
- amount: { type: DB.DECIMAL() },
494
- status: { type: DB.STRING(20) }
495
- };
496
- const filters = defaultFilters(schema);
497
- const mockEvent = {
498
- queryStringParameters: {
499
- token: '{"cardType":"Mastercard"}',
500
- amount: '100',
501
- status: 'Complete'
502
- },
503
- requestContext: {
504
- authorizer: {
505
- claims: {
506
- 'custom:AR': '{"businessIds":{"1":"A"}}'
507
- }
508
- }
509
- }
510
- };
511
-
512
- // Act
513
- const where = createFilters(mockEvent, filters);
514
-
515
- // Assert — amount uses Op.or (DECIMAL), status uses LOWER+LIKE (STRING)
516
- expect(where.amount).toEqual({ [DB.Op.or]: [-100, 100] });
517
- expect(where.token).not.toBeDefined();
518
- // Op.and should contain both the string condition (status) and the JSONB condition (token)
519
- expect(where[DB.Op.and]).toBeDefined();
520
- const statusCondition = where[DB.Op.and].find(c => c.attribute?.args?.[0]?.col === 'status');
521
- expect(statusCondition).toBeDefined();
522
- expect(statusCondition.logic[DB.Op.like]).toBe('%complete%');
523
- const jsonbCondition = where[DB.Op.and].find(c => c.attribute?.args?.[0]?.fn === 'jsonb_extract_path_text');
524
- expect(jsonbCondition).toBeDefined();
525
- expect(jsonbCondition.logic[DB.Op.like]).toBe('%mastercard%');
526
- });
527
-
528
- it('should handle JSONB type passed as class reference instead of instance', function () {
529
- // Arrange — real schemas like payment.js use db.JSONB (class) not db.JSONB() (instance)
530
- const schema = {
531
- token: { type: DB.JSONB, defaultValue: {} }
532
- };
533
- const filters = defaultFilters(schema);
534
- const mockEvent = {
535
- queryStringParameters: {
536
- token: '{"cardHolderName":"Test"}'
537
- },
538
- requestContext: {
539
- authorizer: {
540
- claims: {
541
- 'custom:AR': '{"businessIds":{"1":"A"}}'
542
- }
543
- }
544
- }
545
- };
546
-
547
- // Act
548
- const where = createFilters(mockEvent, filters);
549
-
550
- // Assert — should work the same as the instance form
551
- expect(where.token).not.toBeDefined();
552
- expect(where[DB.Op.and]).toBeDefined();
553
- const condition = where[DB.Op.and][0];
554
- expect(condition.attribute.fn).toBe('LOWER');
555
- expect(condition.logic[DB.Op.like]).toBe('%test%');
556
- });
557
-
558
- it('should handle numeric values in a JSON object query param', function () {
559
- // Arrange — e.g. ?token={"firstSix":123456}
560
- const mockFilter = {
561
- token: { type: 'jsonb', field: 'token' }
562
- };
563
- const mockEvent = {
564
- queryStringParameters: {
565
- token: '{"firstSix":123456}'
566
- },
567
- requestContext: {
568
- authorizer: {
569
- claims: {
570
- 'custom:AR': '{"businessIds":{"1":"A"}}'
571
- }
572
- }
573
- }
574
- };
575
-
576
- // Act
577
- const where = createFilters(mockEvent, mockFilter);
578
-
579
- // Assert — number should be stringified correctly
580
- expect(where[DB.Op.and]).toBeDefined();
581
- expect(where[DB.Op.and].length).toBe(1);
582
- const condition = where[DB.Op.and][0];
583
- const innerFn = condition.attribute.args[0];
584
- expect(innerFn.fn).toBe('jsonb_extract_path_text');
585
- expect(innerFn.args[1]).toBe('firstSix');
586
- expect(condition.logic[DB.Op.like]).toBe('%123456%');
587
- });
588
-
589
- it('should recursively flatten nested objects into deeper jsonb_extract_path_text paths', function () {
590
- // Arrange — e.g. ?paymentProviderData={"payment":{"result":{"message":"Approved"}}}
591
- const mockFilter = {
592
- paymentProviderData: { type: 'jsonb', field: 'paymentProviderData' }
593
- };
594
- const mockEvent = {
595
- queryStringParameters: {
596
- paymentProviderData: '{"payment":{"result":{"message":"Approved"}}}'
597
- },
598
- requestContext: {
599
- authorizer: {
600
- claims: {
601
- 'custom:AR': '{"businessIds":{"1":"A"}}'
602
- }
603
- }
604
- }
605
- };
606
-
607
- // Act
608
- const where = createFilters(mockEvent, mockFilter);
609
-
610
- // Assert — should flatten to jsonb_extract_path_text("paymentProviderData", 'payment', 'result', 'message')
611
- expect(where[DB.Op.and]).toBeDefined();
612
- expect(where[DB.Op.and].length).toBe(1);
613
- const condition = where[DB.Op.and][0];
614
- const innerFn = condition.attribute.args[0];
615
- expect(innerFn.fn).toBe('jsonb_extract_path_text');
616
- expect(innerFn.args[0].col).toBe('paymentProviderData');
617
- expect(innerFn.args[1]).toBe('payment');
618
- expect(innerFn.args[2]).toBe('result');
619
- expect(innerFn.args[3]).toBe('message');
620
- expect(condition.logic[DB.Op.like]).toBe('%approved%');
621
- });
622
-
623
- it('should skip null and array values in a JSON object query param', function () {
624
- // Arrange — null and array values should be ignored
625
- const mockFilter = {
626
- token: { type: 'jsonb', field: 'token' }
627
- };
628
- const mockEvent = {
629
- queryStringParameters: {
630
- token: '{"cardHolderName":"Test","tags":["a","b"],"removed":null}'
631
- },
632
- requestContext: {
633
- authorizer: {
634
- claims: {
635
- 'custom:AR': '{"businessIds":{"1":"A"}}'
636
- }
637
- }
638
- }
639
- };
640
-
641
- // Act
642
- const where = createFilters(mockEvent, mockFilter);
643
-
644
- // Assert — only cardHolderName should produce a condition
645
- expect(where[DB.Op.and]).toBeDefined();
646
- expect(where[DB.Op.and].length).toBe(1);
647
- expect(where[DB.Op.and][0].logic[DB.Op.like]).toBe('%test%');
648
- });
649
-
650
- it('should handle a mix of nested and primitive values in a JSON object', function () {
651
- // Arrange — e.g. ?data={"status":"active","meta":{"region":"US"}}
652
- const mockFilter = {
653
- data: { type: 'jsonb', field: 'data' }
654
- };
655
- const mockEvent = {
656
- queryStringParameters: {
657
- data: '{"status":"active","meta":{"region":"US"}}'
658
- },
659
- requestContext: {
660
- authorizer: {
661
- claims: {
662
- 'custom:AR': '{"businessIds":{"1":"A"}}'
663
- }
664
- }
665
- }
666
- };
667
-
668
- // Act
669
- const where = createFilters(mockEvent, mockFilter);
670
-
671
- // Assert — should produce two conditions: one flat, one nested
672
- expect(where[DB.Op.and]).toBeDefined();
673
- expect(where[DB.Op.and].length).toBe(2);
674
- const statusCondition = where[DB.Op.and].find(c => c.logic[DB.Op.like] === '%active%');
675
- expect(statusCondition).toBeDefined();
676
- expect(statusCondition.attribute.args[0].args[1]).toBe('status');
677
- const regionCondition = where[DB.Op.and].find(c => c.logic[DB.Op.like] === '%us%');
678
- expect(regionCondition).toBeDefined();
679
- expect(regionCondition.attribute.args[0].args[1]).toBe('meta');
680
- expect(regionCondition.attribute.args[0].args[2]).toBe('region');
681
- });
682
- });
1
+ import { createFilters } from './createFilters.js';
2
+ import { defaultFilters } from './defaultFilters.js';
3
+ import { defaultFiltersWithSubSchema, event, eventWithSearchString, eventWithSearchStringNumber, eventWithSingleElementAsArray, eventWithSingleElementInArray, manyStringFields } from './mocks/mocks.js';
4
+ import DB from 'sequelize';
5
+
6
+ describe('createFilters', function () {
7
+ it('should produce case insensitive string filters using LOWER and Op.like', function () {
8
+ // Arrange
9
+ const mockEvent = {
10
+ ...event,
11
+ queryStringParameters: {
12
+ ...event.queryStringParameters,
13
+ char: 'MiXeDcAsE'
14
+ }
15
+ };
16
+
17
+ // Act
18
+ const where = createFilters(mockEvent, defaultFiltersWithSubSchema);
19
+
20
+ // Assert
21
+ expect(where[DB.Op.and]).toBeDefined();
22
+ const charCondition = where[DB.Op.and].find(c => c.attribute?.args?.[0]?.col === 'char');
23
+ expect(charCondition).toBeDefined();
24
+ expect(charCondition.attribute.fn).toBe('LOWER');
25
+ expect(charCondition.logic[DB.Op.like]).toBe('%mixedcase%');
26
+ });
27
+
28
+ it('should return a Sequelize WHERE object and include createdAt', function () {
29
+ // Act
30
+ const where = createFilters(event, defaultFiltersWithSubSchema);
31
+
32
+ // Assert
33
+ expect(where.createdAt).toBeDefined();
34
+ expect(where.decimal).toEqual({ [DB.Op.or]: [-4565, 4565] });
35
+ expect(where[DB.Op.and]).toBeDefined();
36
+ expect(where.boolean).toEqual({ [DB.Op.eq]: true });
37
+ expect(where.array).toEqual({ [DB.Op.in]: ['10', '20', '30'] });
38
+ expect(where['$snowman.snowmanType$']).toEqual({ [DB.Op.eq]: 10 });
39
+ expect(where.notOnObject).not.toBeDefined();
40
+ expect(where.date).not.toBeDefined();
41
+ });
42
+
43
+ it('should handle single elements in the in array', function () {
44
+ // Act
45
+ const where = createFilters(eventWithSingleElementInArray, defaultFiltersWithSubSchema);
46
+
47
+ // Assert
48
+ expect(where.array).toEqual({ [DB.Op.in]: ['10'] });
49
+ expect(where.notOnObject).not.toBeDefined();
50
+ expect(where.date).not.toBeDefined();
51
+ });
52
+
53
+ it('should handle the array value being one value', function () {
54
+ // Act
55
+ const where = createFilters(eventWithSingleElementAsArray, defaultFiltersWithSubSchema);
56
+
57
+ // Assert
58
+ expect(where.array).toEqual({ [DB.Op.in]: ['10'] });
59
+ expect(where.notOnObject).not.toBeDefined();
60
+ expect(where.date).not.toBeDefined();
61
+ });
62
+
63
+ it('should handle searchString with case insensitive LOWER + Op.like conditions', function () {
64
+ // Act
65
+ const where = createFilters(eventWithSearchString, manyStringFields);
66
+
67
+ // Assert
68
+ const orSymbols = Object.getOwnPropertySymbols(where);
69
+ expect(orSymbols.length).toBeGreaterThan(0);
70
+ const orConditions = where[DB.Op.or];
71
+ const lowerConditions = orConditions.filter(c => c.attribute?.fn === 'LOWER');
72
+ expect(lowerConditions.length).toBeGreaterThan(0);
73
+ lowerConditions.forEach(c => {
74
+ expect(c.logic[DB.Op.like]).toBe('%alpha-search%');
75
+ });
76
+ });
77
+
78
+ it('should handle searchString if value is a number', function () {
79
+ // Act
80
+ const where = createFilters(eventWithSearchStringNumber, manyStringFields);
81
+
82
+ // Assert
83
+ const orSymbols = Object.getOwnPropertySymbols(where);
84
+ expect(orSymbols.length).toBeGreaterThan(0);
85
+ });
86
+
87
+ it('should handle searchString sub fields', function () {
88
+ // Act
89
+ const where = createFilters(eventWithSearchStringNumber, defaultFiltersWithSubSchema);
90
+
91
+ // Assert
92
+ const orSymbols = Object.getOwnPropertySymbols(where);
93
+ expect(orSymbols.length).toBeGreaterThan(0);
94
+ });
95
+
96
+ it('should not place string filter fields as direct keys on the where object', function () {
97
+ // Act
98
+ const where = createFilters(event, defaultFiltersWithSubSchema);
99
+
100
+ // Assert - char is a string field, it should be in Op.and not as a direct key
101
+ expect(where.char).not.toBeDefined();
102
+ expect(where[DB.Op.and]).toBeDefined();
103
+ });
104
+ });
105
+
106
+ describe('JSONB case insensitive filters', () => {
107
+ it('should lowercase a single-level JSONB path search', function () {
108
+ // Arrange
109
+ const mockEvent = {
110
+ queryStringParameters: {
111
+ 'token.cardHolderName': 'GREGORY HERBERT'
112
+ },
113
+ requestContext: {
114
+ authorizer: {
115
+ claims: {
116
+ 'custom:businessIds': '[\'1\']',
117
+ 'custom:AR': '{"businessIds":{"1":"A"}}'
118
+ }
119
+ }
120
+ }
121
+ };
122
+ const mockFilter = {
123
+ token: { type: 'jsonb', field: 'token' }
124
+ };
125
+
126
+ // Act
127
+ const where = createFilters(mockEvent, mockFilter);
128
+
129
+ // Assert
130
+ expect(where[DB.Op.and]).toBeDefined();
131
+ expect(where[DB.Op.and].length).toBe(1);
132
+ const condition = where[DB.Op.and][0];
133
+ expect(condition.attribute.fn).toBe('LOWER');
134
+ expect(condition.logic[DB.Op.like]).toBe('%gregory herbert%');
135
+ });
136
+
137
+ it('should use jsonb_extract_path_text with the correct column and path parts', function () {
138
+ // Arrange
139
+ const mockEvent = {
140
+ queryStringParameters: {
141
+ 'token.cardHolderName': 'Test Name'
142
+ },
143
+ requestContext: {
144
+ authorizer: {
145
+ claims: {
146
+ 'custom:businessIds': '[\'1\']',
147
+ 'custom:AR': '{"businessIds":{"1":"A"}}'
148
+ }
149
+ }
150
+ }
151
+ };
152
+ const mockFilter = {
153
+ token: { type: 'jsonb', field: 'token' }
154
+ };
155
+
156
+ // Act
157
+ const where = createFilters(mockEvent, mockFilter);
158
+
159
+ // Assert
160
+ const condition = where[DB.Op.and][0];
161
+ const innerFn = condition.attribute.args[0];
162
+ expect(innerFn.fn).toBe('jsonb_extract_path_text');
163
+ expect(innerFn.args[0].col).toBe('token');
164
+ expect(innerFn.args[1]).toBe('cardHolderName');
165
+ });
166
+
167
+ it('should pass each segment as a separate arg for deeply nested JSONB paths', function () {
168
+ // Arrange
169
+ const mockEvent = {
170
+ queryStringParameters: {
171
+ 'paymentProviderData.payment.result.message': 'THE NAME'
172
+ },
173
+ requestContext: {
174
+ authorizer: {
175
+ claims: {
176
+ 'custom:businessIds': '[\'1\']',
177
+ 'custom:AR': '{"businessIds":{"1":"A"}}'
178
+ }
179
+ }
180
+ }
181
+ };
182
+ const mockFilter = {
183
+ paymentProviderData: { type: 'jsonb', field: 'paymentProviderData' }
184
+ };
185
+
186
+ // Act
187
+ const where = createFilters(mockEvent, mockFilter);
188
+
189
+ // Assert
190
+ const condition = where[DB.Op.and][0];
191
+ const innerFn = condition.attribute.args[0];
192
+ expect(innerFn.fn).toBe('jsonb_extract_path_text');
193
+ expect(innerFn.args[0].col).toBe('paymentProviderData');
194
+ expect(innerFn.args[1]).toBe('payment');
195
+ expect(innerFn.args[2]).toBe('result');
196
+ expect(innerFn.args[3]).toBe('message');
197
+ expect(condition.logic[DB.Op.like]).toBe('%the name%');
198
+ });
199
+
200
+ it('should create a condition for each JSONB query param', function () {
201
+ // Arrange
202
+ const mockEvent = {
203
+ queryStringParameters: {
204
+ 'token.cardHolderName': 'Gregory',
205
+ 'token.cardType': 'MASTERCARD'
206
+ },
207
+ requestContext: {
208
+ authorizer: {
209
+ claims: {
210
+ 'custom:businessIds': '[\'1\']',
211
+ 'custom:AR': '{"businessIds":{"1":"A"}}'
212
+ }
213
+ }
214
+ }
215
+ };
216
+ const mockFilter = {
217
+ token: { type: 'jsonb', field: 'token' }
218
+ };
219
+
220
+ // Act
221
+ const where = createFilters(mockEvent, mockFilter);
222
+
223
+ // Assert
224
+ expect(where[DB.Op.and].length).toBe(2);
225
+ const values = where[DB.Op.and].map(c => c.logic[DB.Op.like]);
226
+ expect(values).toContain('%gregory%');
227
+ expect(values).toContain('%mastercard%');
228
+ });
229
+
230
+ it('should not add Op.and when no JSONB query params match the filter prefix', function () {
231
+ // Arrange
232
+ const mockEvent = {
233
+ queryStringParameters: {
234
+ 'unrelated': 'value'
235
+ },
236
+ requestContext: {
237
+ authorizer: {
238
+ claims: {
239
+ 'custom:businessIds': '[\'1\']',
240
+ 'custom:AR': '{"businessIds":{"1":"A"}}'
241
+ }
242
+ }
243
+ }
244
+ };
245
+ const mockFilter = {
246
+ token: { type: 'jsonb', field: 'token' }
247
+ };
248
+
249
+ // Act
250
+ const where = createFilters(mockEvent, mockFilter);
251
+
252
+ // Assert
253
+ expect(where[DB.Op.and]).not.toBeDefined();
254
+ });
255
+
256
+ it('should combine JSONB conditions with string field conditions in Op.and', function () {
257
+ // Arrange
258
+ const mockEvent = {
259
+ queryStringParameters: {
260
+ name: 'John',
261
+ 'token.cardHolderName': 'GREGORY'
262
+ },
263
+ requestContext: {
264
+ authorizer: {
265
+ claims: {
266
+ 'custom:businessIds': '[\'1\']',
267
+ 'custom:AR': '{"businessIds":{"1":"A"}}'
268
+ }
269
+ }
270
+ }
271
+ };
272
+ const mockFilter = {
273
+ name: { filterType: DB.Op.iLike },
274
+ token: { type: 'jsonb', field: 'token' }
275
+ };
276
+
277
+ // Act
278
+ const where = createFilters(mockEvent, mockFilter);
279
+
280
+ // Assert
281
+ expect(where[DB.Op.and].length).toBe(2);
282
+ const stringCondition = where[DB.Op.and].find(c => c.attribute?.args?.[0]?.col === 'name');
283
+ expect(stringCondition).toBeDefined();
284
+ expect(stringCondition.logic[DB.Op.like]).toBe('%john%');
285
+ const jsonbCondition = where[DB.Op.and].find(c => c.attribute?.args?.[0]?.fn === 'jsonb_extract_path_text');
286
+ expect(jsonbCondition).toBeDefined();
287
+ expect(jsonbCondition.logic[DB.Op.like]).toBe('%gregory%');
288
+ });
289
+
290
+ it('should handle a JSON object passed as the JSONB field value (e.g. ?token={"cardHolderName":"Test"})', function () {
291
+ // Arrange — simulates ?token={"cardHolderName":"Test"} (URL-decoded)
292
+ const mockEvent = {
293
+ queryStringParameters: {
294
+ token: '{"cardHolderName":"Test"}'
295
+ },
296
+ requestContext: {
297
+ authorizer: {
298
+ claims: {
299
+ 'custom:businessIds': '[\'1\']',
300
+ 'custom:AR': '{"businessIds":{"1":"A"}}'
301
+ }
302
+ }
303
+ }
304
+ };
305
+ const mockFilter = {
306
+ token: { type: 'jsonb', field: 'token' }
307
+ };
308
+
309
+ // Act
310
+ const where = createFilters(mockEvent, mockFilter);
311
+
312
+ // Assert — should NOT place token as a direct key
313
+ expect(where.token).not.toBeDefined();
314
+ // Should create a JSONB condition in Op.and
315
+ expect(where[DB.Op.and]).toBeDefined();
316
+ expect(where[DB.Op.and].length).toBe(1);
317
+ const condition = where[DB.Op.and][0];
318
+ expect(condition.attribute.fn).toBe('LOWER');
319
+ const innerFn = condition.attribute.args[0];
320
+ expect(innerFn.fn).toBe('jsonb_extract_path_text');
321
+ expect(innerFn.args[0].col).toBe('token');
322
+ expect(innerFn.args[1]).toBe('cardHolderName');
323
+ expect(condition.logic[DB.Op.like]).toBe('%test%');
324
+ });
325
+
326
+ it('should handle a JSON object with multiple keys passed as the JSONB field value', function () {
327
+ // Arrange — simulates ?token={"cardHolderName":"Test","cardType":"Mastercard"}
328
+ const mockEvent = {
329
+ queryStringParameters: {
330
+ token: '{"cardHolderName":"Test","cardType":"Mastercard"}'
331
+ },
332
+ requestContext: {
333
+ authorizer: {
334
+ claims: {
335
+ 'custom:businessIds': '[\'1\']',
336
+ 'custom:AR': '{"businessIds":{"1":"A"}}'
337
+ }
338
+ }
339
+ }
340
+ };
341
+ const mockFilter = {
342
+ token: { type: 'jsonb', field: 'token' }
343
+ };
344
+
345
+ // Act
346
+ const where = createFilters(mockEvent, mockFilter);
347
+
348
+ // Assert
349
+ expect(where[DB.Op.and].length).toBe(2);
350
+ const values = where[DB.Op.and].map(c => c.logic[DB.Op.like]);
351
+ expect(values).toContain('%test%');
352
+ expect(values).toContain('%mastercard%');
353
+ });
354
+
355
+ it('should not place the JSONB field as a direct key on the where object', function () {
356
+ // Arrange
357
+ const mockEvent = {
358
+ queryStringParameters: {
359
+ 'token.cardHolderName': 'Test'
360
+ },
361
+ requestContext: {
362
+ authorizer: {
363
+ claims: {
364
+ 'custom:businessIds': '[\'1\']',
365
+ 'custom:AR': '{"businessIds":{"1":"A"}}'
366
+ }
367
+ }
368
+ }
369
+ };
370
+ const mockFilter = {
371
+ token: { type: 'jsonb', field: 'token' }
372
+ };
373
+
374
+ // Act
375
+ const where = createFilters(mockEvent, mockFilter);
376
+
377
+ // Assert
378
+ expect(where.token).not.toBeDefined();
379
+ expect(where[DB.Op.and]).toBeDefined();
380
+ });
381
+ });
382
+
383
+ describe('JSONB filters via defaultFilters integration', () => {
384
+ it('should case-insensitive search a JSONB field using a JSON object query param', function () {
385
+ // Arrange — schema mirrors payment.js: token is DB.JSONB
386
+ const schema = {
387
+ token: { type: DB.JSONB(), defaultValue: {} },
388
+ amount: { type: DB.DECIMAL() }
389
+ };
390
+ const filters = defaultFilters(schema);
391
+ const mockEvent = {
392
+ queryStringParameters: {
393
+ token: '{"cardHolderName":"Test"}'
394
+ },
395
+ requestContext: {
396
+ authorizer: {
397
+ claims: {
398
+ 'custom:AR': '{"businessIds":{"1":"A"}}'
399
+ }
400
+ }
401
+ }
402
+ };
403
+
404
+ // Act
405
+ const where = createFilters(mockEvent, filters);
406
+
407
+ // Assert — token should not be a direct key (no Op.contains)
408
+ expect(where.token).not.toBeDefined();
409
+ // Should produce a case-insensitive jsonb_extract_path_text condition
410
+ expect(where[DB.Op.and]).toBeDefined();
411
+ const condition = where[DB.Op.and][0];
412
+ expect(condition.attribute.fn).toBe('LOWER');
413
+ const innerFn = condition.attribute.args[0];
414
+ expect(innerFn.fn).toBe('jsonb_extract_path_text');
415
+ expect(innerFn.args[0].col).toBe('token');
416
+ expect(innerFn.args[1]).toBe('cardHolderName');
417
+ expect(condition.logic[DB.Op.like]).toBe('%test%');
418
+ });
419
+
420
+ it('should case-insensitive search a JSONB field using dot-notation query param', function () {
421
+ // Arrange
422
+ const schema = {
423
+ token: { type: DB.JSONB(), defaultValue: {} }
424
+ };
425
+ const filters = defaultFilters(schema);
426
+ const mockEvent = {
427
+ queryStringParameters: {
428
+ 'token.lastFour': '8473'
429
+ },
430
+ requestContext: {
431
+ authorizer: {
432
+ claims: {
433
+ 'custom:AR': '{"businessIds":{"1":"A"}}'
434
+ }
435
+ }
436
+ }
437
+ };
438
+
439
+ // Act
440
+ const where = createFilters(mockEvent, filters);
441
+
442
+ // Assert
443
+ expect(where.token).not.toBeDefined();
444
+ expect(where[DB.Op.and]).toBeDefined();
445
+ const condition = where[DB.Op.and][0];
446
+ const innerFn = condition.attribute.args[0];
447
+ expect(innerFn.fn).toBe('jsonb_extract_path_text');
448
+ expect(innerFn.args[0].col).toBe('token');
449
+ expect(innerFn.args[1]).toBe('lastFour');
450
+ expect(condition.logic[DB.Op.like]).toBe('%8473%');
451
+ });
452
+
453
+ it('should handle multiple JSONB columns in the same schema', function () {
454
+ // Arrange — mirrors payment schema with token and paymentProviderData
455
+ const schema = {
456
+ token: { type: DB.JSONB(), defaultValue: {} },
457
+ paymentProviderData: { type: DB.JSONB() }
458
+ };
459
+ const filters = defaultFilters(schema);
460
+ const mockEvent = {
461
+ queryStringParameters: {
462
+ 'token.cardHolderName': 'Gregory',
463
+ 'paymentProviderData.payment.result.message': 'APPROVED'
464
+ },
465
+ requestContext: {
466
+ authorizer: {
467
+ claims: {
468
+ 'custom:AR': '{"businessIds":{"1":"A"}}'
469
+ }
470
+ }
471
+ }
472
+ };
473
+
474
+ // Act
475
+ const where = createFilters(mockEvent, filters);
476
+
477
+ // Assert
478
+ expect(where.token).not.toBeDefined();
479
+ expect(where.paymentProviderData).not.toBeDefined();
480
+ expect(where[DB.Op.and].length).toBe(2);
481
+ const tokenCondition = where[DB.Op.and].find(c => c.attribute?.args?.[0]?.args?.[0]?.col === 'token');
482
+ expect(tokenCondition).toBeDefined();
483
+ expect(tokenCondition.logic[DB.Op.like]).toBe('%gregory%');
484
+ const providerCondition = where[DB.Op.and].find(c => c.attribute?.args?.[0]?.args?.[0]?.col === 'paymentProviderData');
485
+ expect(providerCondition).toBeDefined();
486
+ expect(providerCondition.logic[DB.Op.like]).toBe('%approved%');
487
+ });
488
+
489
+ it('should combine JSONB filters with non-JSONB filters from the same schema', function () {
490
+ // Arrange
491
+ const schema = {
492
+ token: { type: DB.JSONB(), defaultValue: {} },
493
+ amount: { type: DB.DECIMAL() },
494
+ status: { type: DB.STRING(20) }
495
+ };
496
+ const filters = defaultFilters(schema);
497
+ const mockEvent = {
498
+ queryStringParameters: {
499
+ token: '{"cardType":"Mastercard"}',
500
+ amount: '100',
501
+ status: 'Complete'
502
+ },
503
+ requestContext: {
504
+ authorizer: {
505
+ claims: {
506
+ 'custom:AR': '{"businessIds":{"1":"A"}}'
507
+ }
508
+ }
509
+ }
510
+ };
511
+
512
+ // Act
513
+ const where = createFilters(mockEvent, filters);
514
+
515
+ // Assert — amount uses Op.or (DECIMAL), status uses LOWER+LIKE (STRING)
516
+ expect(where.amount).toEqual({ [DB.Op.or]: [-100, 100] });
517
+ expect(where.token).not.toBeDefined();
518
+ // Op.and should contain both the string condition (status) and the JSONB condition (token)
519
+ expect(where[DB.Op.and]).toBeDefined();
520
+ const statusCondition = where[DB.Op.and].find(c => c.attribute?.args?.[0]?.col === 'status');
521
+ expect(statusCondition).toBeDefined();
522
+ expect(statusCondition.logic[DB.Op.like]).toBe('%complete%');
523
+ const jsonbCondition = where[DB.Op.and].find(c => c.attribute?.args?.[0]?.fn === 'jsonb_extract_path_text');
524
+ expect(jsonbCondition).toBeDefined();
525
+ expect(jsonbCondition.logic[DB.Op.like]).toBe('%mastercard%');
526
+ });
527
+
528
+ it('should handle JSONB type passed as class reference instead of instance', function () {
529
+ // Arrange — real schemas like payment.js use db.JSONB (class) not db.JSONB() (instance)
530
+ const schema = {
531
+ token: { type: DB.JSONB, defaultValue: {} }
532
+ };
533
+ const filters = defaultFilters(schema);
534
+ const mockEvent = {
535
+ queryStringParameters: {
536
+ token: '{"cardHolderName":"Test"}'
537
+ },
538
+ requestContext: {
539
+ authorizer: {
540
+ claims: {
541
+ 'custom:AR': '{"businessIds":{"1":"A"}}'
542
+ }
543
+ }
544
+ }
545
+ };
546
+
547
+ // Act
548
+ const where = createFilters(mockEvent, filters);
549
+
550
+ // Assert — should work the same as the instance form
551
+ expect(where.token).not.toBeDefined();
552
+ expect(where[DB.Op.and]).toBeDefined();
553
+ const condition = where[DB.Op.and][0];
554
+ expect(condition.attribute.fn).toBe('LOWER');
555
+ expect(condition.logic[DB.Op.like]).toBe('%test%');
556
+ });
557
+
558
+ it('should handle numeric values in a JSON object query param', function () {
559
+ // Arrange — e.g. ?token={"firstSix":123456}
560
+ const mockFilter = {
561
+ token: { type: 'jsonb', field: 'token' }
562
+ };
563
+ const mockEvent = {
564
+ queryStringParameters: {
565
+ token: '{"firstSix":123456}'
566
+ },
567
+ requestContext: {
568
+ authorizer: {
569
+ claims: {
570
+ 'custom:AR': '{"businessIds":{"1":"A"}}'
571
+ }
572
+ }
573
+ }
574
+ };
575
+
576
+ // Act
577
+ const where = createFilters(mockEvent, mockFilter);
578
+
579
+ // Assert — number should be stringified correctly
580
+ expect(where[DB.Op.and]).toBeDefined();
581
+ expect(where[DB.Op.and].length).toBe(1);
582
+ const condition = where[DB.Op.and][0];
583
+ const innerFn = condition.attribute.args[0];
584
+ expect(innerFn.fn).toBe('jsonb_extract_path_text');
585
+ expect(innerFn.args[1]).toBe('firstSix');
586
+ expect(condition.logic[DB.Op.like]).toBe('%123456%');
587
+ });
588
+
589
+ it('should recursively flatten nested objects into deeper jsonb_extract_path_text paths', function () {
590
+ // Arrange — e.g. ?paymentProviderData={"payment":{"result":{"message":"Approved"}}}
591
+ const mockFilter = {
592
+ paymentProviderData: { type: 'jsonb', field: 'paymentProviderData' }
593
+ };
594
+ const mockEvent = {
595
+ queryStringParameters: {
596
+ paymentProviderData: '{"payment":{"result":{"message":"Approved"}}}'
597
+ },
598
+ requestContext: {
599
+ authorizer: {
600
+ claims: {
601
+ 'custom:AR': '{"businessIds":{"1":"A"}}'
602
+ }
603
+ }
604
+ }
605
+ };
606
+
607
+ // Act
608
+ const where = createFilters(mockEvent, mockFilter);
609
+
610
+ // Assert — should flatten to jsonb_extract_path_text("paymentProviderData", 'payment', 'result', 'message')
611
+ expect(where[DB.Op.and]).toBeDefined();
612
+ expect(where[DB.Op.and].length).toBe(1);
613
+ const condition = where[DB.Op.and][0];
614
+ const innerFn = condition.attribute.args[0];
615
+ expect(innerFn.fn).toBe('jsonb_extract_path_text');
616
+ expect(innerFn.args[0].col).toBe('paymentProviderData');
617
+ expect(innerFn.args[1]).toBe('payment');
618
+ expect(innerFn.args[2]).toBe('result');
619
+ expect(innerFn.args[3]).toBe('message');
620
+ expect(condition.logic[DB.Op.like]).toBe('%approved%');
621
+ });
622
+
623
+ it('should skip null and array values in a JSON object query param', function () {
624
+ // Arrange — null and array values should be ignored
625
+ const mockFilter = {
626
+ token: { type: 'jsonb', field: 'token' }
627
+ };
628
+ const mockEvent = {
629
+ queryStringParameters: {
630
+ token: '{"cardHolderName":"Test","tags":["a","b"],"removed":null}'
631
+ },
632
+ requestContext: {
633
+ authorizer: {
634
+ claims: {
635
+ 'custom:AR': '{"businessIds":{"1":"A"}}'
636
+ }
637
+ }
638
+ }
639
+ };
640
+
641
+ // Act
642
+ const where = createFilters(mockEvent, mockFilter);
643
+
644
+ // Assert — only cardHolderName should produce a condition
645
+ expect(where[DB.Op.and]).toBeDefined();
646
+ expect(where[DB.Op.and].length).toBe(1);
647
+ expect(where[DB.Op.and][0].logic[DB.Op.like]).toBe('%test%');
648
+ });
649
+
650
+ it('should handle a mix of nested and primitive values in a JSON object', function () {
651
+ // Arrange — e.g. ?data={"status":"active","meta":{"region":"US"}}
652
+ const mockFilter = {
653
+ data: { type: 'jsonb', field: 'data' }
654
+ };
655
+ const mockEvent = {
656
+ queryStringParameters: {
657
+ data: '{"status":"active","meta":{"region":"US"}}'
658
+ },
659
+ requestContext: {
660
+ authorizer: {
661
+ claims: {
662
+ 'custom:AR': '{"businessIds":{"1":"A"}}'
663
+ }
664
+ }
665
+ }
666
+ };
667
+
668
+ // Act
669
+ const where = createFilters(mockEvent, mockFilter);
670
+
671
+ // Assert — should produce two conditions: one flat, one nested
672
+ expect(where[DB.Op.and]).toBeDefined();
673
+ expect(where[DB.Op.and].length).toBe(2);
674
+ const statusCondition = where[DB.Op.and].find(c => c.logic[DB.Op.like] === '%active%');
675
+ expect(statusCondition).toBeDefined();
676
+ expect(statusCondition.attribute.args[0].args[1]).toBe('status');
677
+ const regionCondition = where[DB.Op.and].find(c => c.logic[DB.Op.like] === '%us%');
678
+ expect(regionCondition).toBeDefined();
679
+ expect(regionCondition.attribute.args[0].args[1]).toBe('meta');
680
+ expect(regionCondition.attribute.args[0].args[2]).toBe('region');
681
+ });
682
+ });