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.
- package/bin/main.js +31 -19
- package/bin/main.js.map +1 -1
- package/package.json +1 -1
- package/src/database/dbUtils/partnerAccess/accessScope.js +53 -24
- package/src/database/dbUtils/partnerAccess/accessScope.test.js +287 -0
- package/src/database/dbUtils/partnerAccess/createAccessHelpers.js +38 -38
- package/src/database/dbUtils/queryStringUtils/createFilters.js +311 -311
- package/src/database/dbUtils/queryStringUtils/createFilters.test.js +682 -682
- package/src/requestResponse/requestResponse.js +6 -1
- package/src/requestResponse/requestResponse.test.js +58 -0
|
@@ -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
|
+
});
|