core-services-sdk 1.3.64 → 1.3.65
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/package.json +1 -1
- package/src/postgresql/core/get-table-name.js +10 -0
- package/src/postgresql/filters/apply-filter-object.js +19 -0
- package/src/postgresql/filters/apply-filter-snake-case.js +16 -0
- package/src/postgresql/filters/apply-filter.js +33 -0
- package/src/postgresql/filters/operators.js +32 -0
- package/src/postgresql/index.js +5 -2
- package/src/postgresql/modifiers/apply-order-by.js +19 -0
- package/src/postgresql/modifiers/apply-pagination.js +12 -0
- package/src/postgresql/pagination/paginate.js +48 -0
- package/tests/core/normalize-phone-number.unit.test.js +1 -1
- package/tests/postgresql/apply-filter-snake-case.integration.test.js +25 -354
- package/tests/postgresql/apply-filter.integration.test.js +28 -78
- package/tests/postgresql/core/get-table-name.unit.test.js +20 -0
- package/tests/postgresql/filters/apply-filter-object.test.js +23 -0
- package/tests/postgresql/filters/operators.unit.test.js +23 -0
- package/tests/postgresql/modifiers/apply-order-by.test.js +80 -0
- package/tests/postgresql/modifiers/apply-pagination.unit.test.js +18 -0
- package/tests/postgresql/paginate.integration.test.js +8 -16
- package/tests/postgresql/pagination/paginate.js +48 -0
- package/tests/postgresql/validate-schema.integration.test.js +7 -3
- package/types/postgresql/core/get-table-name.d.ts +10 -0
- package/types/postgresql/filters/apply-filter-object.d.ts +15 -0
- package/types/postgresql/filters/apply-filter-snake-case.d.ts +14 -0
- package/types/postgresql/filters/apply-filter.d.ts +15 -0
- package/types/postgresql/filters/operators.d.ts +14 -0
- package/types/postgresql/index.d.ts +5 -2
- package/types/postgresql/modifiers/apply-order-by.d.ts +17 -0
- package/types/postgresql/modifiers/apply-pagination.d.ts +17 -0
- package/types/postgresql/pagination/paginate.d.ts +29 -0
- package/src/postgresql/apply-filter.js +0 -401
- package/src/postgresql/paginate.js +0 -61
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
buildPostgresUri,
|
|
9
9
|
} from '../../src/postgresql/start-stop-postgres-docker.js'
|
|
10
10
|
|
|
11
|
-
import { applyFilterSnakeCase } from '../../src/postgresql/apply-filter.js'
|
|
11
|
+
import { applyFilterSnakeCase } from '../../src/postgresql/filters/apply-filter-snake-case.js'
|
|
12
12
|
|
|
13
13
|
const PG_OPTIONS = {
|
|
14
14
|
port: 5444,
|
|
@@ -69,7 +69,7 @@ beforeEach(async () => {
|
|
|
69
69
|
name: 'Asset One',
|
|
70
70
|
status: 'active',
|
|
71
71
|
type: 'invoice',
|
|
72
|
-
price: 100
|
|
72
|
+
price: 100,
|
|
73
73
|
created_at: new Date('2024-01-01'),
|
|
74
74
|
deleted_at: null,
|
|
75
75
|
},
|
|
@@ -78,7 +78,7 @@ beforeEach(async () => {
|
|
|
78
78
|
name: 'Asset Two',
|
|
79
79
|
status: 'active',
|
|
80
80
|
type: 'receipt',
|
|
81
|
-
price: 200
|
|
81
|
+
price: 200,
|
|
82
82
|
created_at: new Date('2024-01-02'),
|
|
83
83
|
deleted_at: null,
|
|
84
84
|
},
|
|
@@ -87,7 +87,7 @@ beforeEach(async () => {
|
|
|
87
87
|
name: 'Asset Three',
|
|
88
88
|
status: 'pending',
|
|
89
89
|
type: 'invoice',
|
|
90
|
-
price: 150
|
|
90
|
+
price: 150,
|
|
91
91
|
created_at: new Date('2024-01-03'),
|
|
92
92
|
deleted_at: null,
|
|
93
93
|
},
|
|
@@ -96,7 +96,7 @@ beforeEach(async () => {
|
|
|
96
96
|
name: 'Deleted Asset',
|
|
97
97
|
status: 'deleted',
|
|
98
98
|
type: 'receipt',
|
|
99
|
-
price: 50
|
|
99
|
+
price: 50,
|
|
100
100
|
created_at: new Date('2024-01-04'),
|
|
101
101
|
deleted_at: new Date('2024-01-05'),
|
|
102
102
|
},
|
|
@@ -105,7 +105,7 @@ beforeEach(async () => {
|
|
|
105
105
|
name: 'Expensive Asset',
|
|
106
106
|
status: 'active',
|
|
107
107
|
type: 'invoice',
|
|
108
|
-
price: 500
|
|
108
|
+
price: 500,
|
|
109
109
|
created_at: new Date('2024-01-05'),
|
|
110
110
|
deleted_at: null,
|
|
111
111
|
},
|
|
@@ -115,7 +115,7 @@ beforeEach(async () => {
|
|
|
115
115
|
{
|
|
116
116
|
id: '00000000-0000-0000-0000-000000000101',
|
|
117
117
|
invoice_number: 'INV-001',
|
|
118
|
-
amount: 1000
|
|
118
|
+
amount: 1000,
|
|
119
119
|
status: 'paid',
|
|
120
120
|
customer_id: '00000000-0000-0000-0000-000000000201',
|
|
121
121
|
created_at: new Date('2024-01-01'),
|
|
@@ -125,7 +125,7 @@ beforeEach(async () => {
|
|
|
125
125
|
{
|
|
126
126
|
id: '00000000-0000-0000-0000-000000000102',
|
|
127
127
|
invoice_number: 'INV-002',
|
|
128
|
-
amount: 2500
|
|
128
|
+
amount: 2500,
|
|
129
129
|
status: 'pending',
|
|
130
130
|
customer_id: '00000000-0000-0000-0000-000000000202',
|
|
131
131
|
created_at: new Date('2024-01-02'),
|
|
@@ -135,7 +135,7 @@ beforeEach(async () => {
|
|
|
135
135
|
{
|
|
136
136
|
id: '00000000-0000-0000-0000-000000000103',
|
|
137
137
|
invoice_number: 'INV-003',
|
|
138
|
-
amount: 500
|
|
138
|
+
amount: 500,
|
|
139
139
|
status: 'paid',
|
|
140
140
|
customer_id: '00000000-0000-0000-0000-000000000201',
|
|
141
141
|
created_at: new Date('2024-01-03'),
|
|
@@ -145,7 +145,7 @@ beforeEach(async () => {
|
|
|
145
145
|
{
|
|
146
146
|
id: '00000000-0000-0000-0000-000000000104',
|
|
147
147
|
invoice_number: 'INV-004',
|
|
148
|
-
amount: 3000
|
|
148
|
+
amount: 3000,
|
|
149
149
|
status: 'overdue',
|
|
150
150
|
customer_id: '00000000-0000-0000-0000-000000000203',
|
|
151
151
|
created_at: new Date('2024-01-04'),
|
|
@@ -155,7 +155,7 @@ beforeEach(async () => {
|
|
|
155
155
|
{
|
|
156
156
|
id: '00000000-0000-0000-0000-000000000105',
|
|
157
157
|
invoice_number: 'INV-005',
|
|
158
|
-
amount: 750
|
|
158
|
+
amount: 750,
|
|
159
159
|
status: 'cancelled',
|
|
160
160
|
customer_id: '00000000-0000-0000-0000-000000000201',
|
|
161
161
|
created_at: new Date('2024-01-05'),
|
|
@@ -166,384 +166,55 @@ beforeEach(async () => {
|
|
|
166
166
|
})
|
|
167
167
|
|
|
168
168
|
describe('applyFilterSnakeCase integration', () => {
|
|
169
|
-
it('applies
|
|
169
|
+
it('applies equality filter', async () => {
|
|
170
170
|
const query = db('assets').select('*')
|
|
171
|
-
|
|
171
|
+
|
|
172
|
+
applyFilterSnakeCase({
|
|
172
173
|
query,
|
|
173
174
|
filter: { status: 'active' },
|
|
174
|
-
tableName: 'assets',
|
|
175
175
|
})
|
|
176
176
|
|
|
177
|
-
const results = await
|
|
178
|
-
|
|
177
|
+
const results = await query
|
|
179
178
|
expect(results).toHaveLength(3)
|
|
180
|
-
expect(results.every((r) => r.status === 'active')).toBe(true)
|
|
181
179
|
})
|
|
182
180
|
|
|
183
181
|
it('converts camelCase keys to snake_case', async () => {
|
|
184
182
|
const query = db('assets').select('*')
|
|
185
|
-
|
|
183
|
+
|
|
184
|
+
applyFilterSnakeCase({
|
|
186
185
|
query,
|
|
187
186
|
filter: { deletedAt: { isNull: true }, name: 'Asset One' },
|
|
188
|
-
tableName: 'assets',
|
|
189
187
|
})
|
|
190
188
|
|
|
191
|
-
const results = await
|
|
192
|
-
|
|
189
|
+
const results = await query
|
|
193
190
|
expect(results).toHaveLength(1)
|
|
194
191
|
expect(results[0].name).toBe('Asset One')
|
|
195
192
|
})
|
|
196
193
|
|
|
197
|
-
it('applies not equal filter (ne)', async () => {
|
|
198
|
-
const query = db('assets').select('*')
|
|
199
|
-
const filteredQuery = applyFilterSnakeCase({
|
|
200
|
-
query,
|
|
201
|
-
filter: { status: { ne: 'deleted' } },
|
|
202
|
-
tableName: 'assets',
|
|
203
|
-
})
|
|
204
|
-
|
|
205
|
-
const results = await filteredQuery
|
|
206
|
-
|
|
207
|
-
expect(results).toHaveLength(4)
|
|
208
|
-
expect(results.every((r) => r.status !== 'deleted')).toBe(true)
|
|
209
|
-
})
|
|
210
|
-
|
|
211
|
-
it('applies not equal filter (neq alias)', async () => {
|
|
212
|
-
const query = db('assets').select('*')
|
|
213
|
-
const filteredQuery = applyFilterSnakeCase({
|
|
214
|
-
query,
|
|
215
|
-
filter: { status: { neq: 'deleted' } },
|
|
216
|
-
tableName: 'assets',
|
|
217
|
-
})
|
|
218
|
-
|
|
219
|
-
const results = await filteredQuery
|
|
220
|
-
|
|
221
|
-
expect(results).toHaveLength(4)
|
|
222
|
-
expect(results.every((r) => r.status !== 'deleted')).toBe(true)
|
|
223
|
-
})
|
|
224
|
-
|
|
225
|
-
it('applies IN filter with array directly', async () => {
|
|
226
|
-
const query = db('assets').select('*')
|
|
227
|
-
const filteredQuery = applyFilterSnakeCase({
|
|
228
|
-
query,
|
|
229
|
-
filter: { status: ['active', 'pending'] },
|
|
230
|
-
tableName: 'assets',
|
|
231
|
-
})
|
|
232
|
-
|
|
233
|
-
const results = await filteredQuery
|
|
234
|
-
|
|
235
|
-
expect(results).toHaveLength(4)
|
|
236
|
-
expect(
|
|
237
|
-
results.every((r) => r.status === 'active' || r.status === 'pending'),
|
|
238
|
-
).toBe(true)
|
|
239
|
-
})
|
|
240
|
-
|
|
241
|
-
it('applies IN filter with operator', async () => {
|
|
242
|
-
const query = db('assets').select('*')
|
|
243
|
-
const filteredQuery = applyFilterSnakeCase({
|
|
244
|
-
query,
|
|
245
|
-
filter: { status: { in: ['active', 'pending'] } },
|
|
246
|
-
tableName: 'assets',
|
|
247
|
-
})
|
|
248
|
-
|
|
249
|
-
const results = await filteredQuery
|
|
250
|
-
|
|
251
|
-
expect(results).toHaveLength(4)
|
|
252
|
-
expect(
|
|
253
|
-
results.every((r) => r.status === 'active' || r.status === 'pending'),
|
|
254
|
-
).toBe(true)
|
|
255
|
-
})
|
|
256
|
-
|
|
257
|
-
it('applies NOT IN filter (nin)', async () => {
|
|
258
|
-
const query = db('assets').select('*')
|
|
259
|
-
const filteredQuery = applyFilterSnakeCase({
|
|
260
|
-
query,
|
|
261
|
-
filter: { status: { nin: ['deleted', 'archived'] } },
|
|
262
|
-
tableName: 'assets',
|
|
263
|
-
})
|
|
264
|
-
|
|
265
|
-
const results = await filteredQuery
|
|
266
|
-
|
|
267
|
-
expect(results).toHaveLength(4)
|
|
268
|
-
expect(
|
|
269
|
-
results.every((r) => r.status !== 'deleted' && r.status !== 'archived'),
|
|
270
|
-
).toBe(true)
|
|
271
|
-
})
|
|
272
|
-
|
|
273
|
-
it('applies greater than filter (gt)', async () => {
|
|
274
|
-
const query = db('assets').select('*')
|
|
275
|
-
const filteredQuery = applyFilterSnakeCase({
|
|
276
|
-
query,
|
|
277
|
-
filter: { price: { gt: 150 } },
|
|
278
|
-
tableName: 'assets',
|
|
279
|
-
})
|
|
280
|
-
|
|
281
|
-
const results = await filteredQuery
|
|
282
|
-
|
|
283
|
-
expect(results).toHaveLength(2)
|
|
284
|
-
expect(results.every((r) => parseFloat(r.price) > 150)).toBe(true)
|
|
285
|
-
})
|
|
286
|
-
|
|
287
|
-
it('applies greater than or equal filter (gte)', async () => {
|
|
288
|
-
const query = db('assets').select('*')
|
|
289
|
-
const filteredQuery = applyFilterSnakeCase({
|
|
290
|
-
query,
|
|
291
|
-
filter: { price: { gte: 150 } },
|
|
292
|
-
tableName: 'assets',
|
|
293
|
-
})
|
|
294
|
-
|
|
295
|
-
const results = await filteredQuery
|
|
296
|
-
|
|
297
|
-
expect(results).toHaveLength(3)
|
|
298
|
-
expect(results.every((r) => parseFloat(r.price) >= 150)).toBe(true)
|
|
299
|
-
})
|
|
300
|
-
|
|
301
|
-
it('applies less than filter (lt)', async () => {
|
|
302
|
-
const query = db('assets').select('*')
|
|
303
|
-
const filteredQuery = applyFilterSnakeCase({
|
|
304
|
-
query,
|
|
305
|
-
filter: { price: { lt: 150 } },
|
|
306
|
-
tableName: 'assets',
|
|
307
|
-
})
|
|
308
|
-
|
|
309
|
-
const results = await filteredQuery
|
|
310
|
-
|
|
311
|
-
expect(results).toHaveLength(2)
|
|
312
|
-
expect(results.every((r) => parseFloat(r.price) < 150)).toBe(true)
|
|
313
|
-
})
|
|
314
|
-
|
|
315
|
-
it('applies less than or equal filter (lte)', async () => {
|
|
316
|
-
const query = db('assets').select('*')
|
|
317
|
-
const filteredQuery = applyFilterSnakeCase({
|
|
318
|
-
query,
|
|
319
|
-
filter: { price: { lte: 150 } },
|
|
320
|
-
tableName: 'assets',
|
|
321
|
-
})
|
|
322
|
-
|
|
323
|
-
const results = await filteredQuery
|
|
324
|
-
|
|
325
|
-
expect(results).toHaveLength(3)
|
|
326
|
-
expect(results.every((r) => parseFloat(r.price) <= 150)).toBe(true)
|
|
327
|
-
})
|
|
328
|
-
|
|
329
|
-
it('applies range filter with gte and lte', async () => {
|
|
330
|
-
const query = db('assets').select('*')
|
|
331
|
-
const filteredQuery = applyFilterSnakeCase({
|
|
332
|
-
query,
|
|
333
|
-
filter: { price: { gte: 100, lte: 200 } },
|
|
334
|
-
tableName: 'assets',
|
|
335
|
-
})
|
|
336
|
-
|
|
337
|
-
const results = await filteredQuery
|
|
338
|
-
|
|
339
|
-
expect(results).toHaveLength(3)
|
|
340
|
-
expect(
|
|
341
|
-
results.every(
|
|
342
|
-
(r) => parseFloat(r.price) >= 100 && parseFloat(r.price) <= 200,
|
|
343
|
-
),
|
|
344
|
-
).toBe(true)
|
|
345
|
-
})
|
|
346
|
-
|
|
347
|
-
it('applies case-sensitive LIKE filter', async () => {
|
|
348
|
-
const query = db('assets').select('*')
|
|
349
|
-
const filteredQuery = applyFilterSnakeCase({
|
|
350
|
-
query,
|
|
351
|
-
filter: { name: { like: '%Asset%' } },
|
|
352
|
-
tableName: 'assets',
|
|
353
|
-
})
|
|
354
|
-
|
|
355
|
-
const results = await filteredQuery
|
|
356
|
-
|
|
357
|
-
expect(results.length).toBeGreaterThan(0)
|
|
358
|
-
expect(results.every((r) => r.name.includes('Asset'))).toBe(true)
|
|
359
|
-
})
|
|
360
|
-
|
|
361
|
-
it('applies case-insensitive ILIKE filter', async () => {
|
|
362
|
-
const query = db('assets').select('*')
|
|
363
|
-
const filteredQuery = applyFilterSnakeCase({
|
|
364
|
-
query,
|
|
365
|
-
filter: { name: { ilike: '%asset%' } },
|
|
366
|
-
tableName: 'assets',
|
|
367
|
-
})
|
|
368
|
-
|
|
369
|
-
const results = await filteredQuery
|
|
370
|
-
|
|
371
|
-
expect(results.length).toBeGreaterThan(0)
|
|
372
|
-
expect(
|
|
373
|
-
results.every((r) =>
|
|
374
|
-
r.name.toLowerCase().includes('asset'.toLowerCase()),
|
|
375
|
-
),
|
|
376
|
-
).toBe(true)
|
|
377
|
-
})
|
|
378
|
-
|
|
379
|
-
it('applies isNull filter when value is true', async () => {
|
|
380
|
-
const query = db('assets').select('*')
|
|
381
|
-
const filteredQuery = applyFilterSnakeCase({
|
|
382
|
-
query,
|
|
383
|
-
filter: { deletedAt: { isNull: true } },
|
|
384
|
-
tableName: 'assets',
|
|
385
|
-
})
|
|
386
|
-
|
|
387
|
-
const results = await filteredQuery
|
|
388
|
-
|
|
389
|
-
expect(results).toHaveLength(4)
|
|
390
|
-
expect(results.every((r) => r.deleted_at === null)).toBe(true)
|
|
391
|
-
})
|
|
392
|
-
|
|
393
|
-
it('applies isNotNull filter when isNull value is false', async () => {
|
|
394
|
-
const query = db('assets').select('*')
|
|
395
|
-
const filteredQuery = applyFilterSnakeCase({
|
|
396
|
-
query,
|
|
397
|
-
filter: { deletedAt: { isNull: false } },
|
|
398
|
-
tableName: 'assets',
|
|
399
|
-
})
|
|
400
|
-
|
|
401
|
-
const results = await filteredQuery
|
|
402
|
-
|
|
403
|
-
expect(results).toHaveLength(1)
|
|
404
|
-
expect(results[0].deleted_at).not.toBe(null)
|
|
405
|
-
})
|
|
406
|
-
|
|
407
|
-
it('applies isNotNull filter when value is true', async () => {
|
|
408
|
-
const query = db('assets').select('*')
|
|
409
|
-
const filteredQuery = applyFilterSnakeCase({
|
|
410
|
-
query,
|
|
411
|
-
filter: { deletedAt: { isNotNull: true } },
|
|
412
|
-
tableName: 'assets',
|
|
413
|
-
})
|
|
414
|
-
|
|
415
|
-
const results = await filteredQuery
|
|
416
|
-
|
|
417
|
-
expect(results).toHaveLength(1)
|
|
418
|
-
expect(results[0].deleted_at).not.toBe(null)
|
|
419
|
-
})
|
|
420
|
-
|
|
421
|
-
it('applies isNull filter when isNotNull value is false', async () => {
|
|
422
|
-
const query = db('assets').select('*')
|
|
423
|
-
const filteredQuery = applyFilterSnakeCase({
|
|
424
|
-
query,
|
|
425
|
-
filter: { deletedAt: { isNotNull: false } },
|
|
426
|
-
tableName: 'assets',
|
|
427
|
-
})
|
|
428
|
-
|
|
429
|
-
const results = await filteredQuery
|
|
430
|
-
|
|
431
|
-
expect(results).toHaveLength(4)
|
|
432
|
-
expect(results.every((r) => r.deleted_at === null)).toBe(true)
|
|
433
|
-
})
|
|
434
|
-
|
|
435
|
-
it('applies multiple filters together', async () => {
|
|
436
|
-
const query = db('assets').select('*')
|
|
437
|
-
const filteredQuery = applyFilterSnakeCase({
|
|
438
|
-
query,
|
|
439
|
-
filter: {
|
|
440
|
-
status: 'active',
|
|
441
|
-
type: 'invoice',
|
|
442
|
-
price: { gte: 100 },
|
|
443
|
-
},
|
|
444
|
-
tableName: 'assets',
|
|
445
|
-
})
|
|
446
|
-
|
|
447
|
-
const results = await filteredQuery
|
|
448
|
-
|
|
449
|
-
expect(results).toHaveLength(2)
|
|
450
|
-
expect(results.every((r) => r.status === 'active')).toBe(true)
|
|
451
|
-
expect(results.every((r) => r.type === 'invoice')).toBe(true)
|
|
452
|
-
expect(results.every((r) => parseFloat(r.price) >= 100)).toBe(true)
|
|
453
|
-
})
|
|
454
|
-
|
|
455
|
-
it('returns empty results when filter matches nothing', async () => {
|
|
456
|
-
const query = db('assets').select('*')
|
|
457
|
-
const filteredQuery = applyFilterSnakeCase({
|
|
458
|
-
query,
|
|
459
|
-
filter: { status: 'non-existing' },
|
|
460
|
-
tableName: 'assets',
|
|
461
|
-
})
|
|
462
|
-
|
|
463
|
-
const results = await filteredQuery
|
|
464
|
-
|
|
465
|
-
expect(results).toHaveLength(0)
|
|
466
|
-
})
|
|
467
|
-
|
|
468
|
-
it('uses qualified column names with tableName', async () => {
|
|
469
|
-
const query = db('assets').select('assets.*')
|
|
470
|
-
const filteredQuery = applyFilterSnakeCase({
|
|
471
|
-
query,
|
|
472
|
-
filter: { status: 'active' },
|
|
473
|
-
tableName: 'assets',
|
|
474
|
-
})
|
|
475
|
-
|
|
476
|
-
const results = await filteredQuery
|
|
477
|
-
|
|
478
|
-
expect(results).toHaveLength(3)
|
|
479
|
-
expect(results.every((r) => r.status === 'active')).toBe(true)
|
|
480
|
-
})
|
|
481
|
-
|
|
482
194
|
it('ignores unknown operators', async () => {
|
|
483
195
|
const query = db('assets').select('*')
|
|
484
|
-
|
|
196
|
+
|
|
197
|
+
applyFilterSnakeCase({
|
|
485
198
|
query,
|
|
486
|
-
filter: { status: {
|
|
487
|
-
tableName: 'assets',
|
|
199
|
+
filter: { status: { unknown: 'x' } },
|
|
488
200
|
})
|
|
489
201
|
|
|
490
|
-
const results = await
|
|
491
|
-
|
|
492
|
-
// Should return all records since unknown operator is ignored
|
|
202
|
+
const results = await query
|
|
493
203
|
expect(results).toHaveLength(5)
|
|
494
204
|
})
|
|
495
205
|
|
|
496
|
-
it('works with invoices table', async () => {
|
|
206
|
+
it('works with invoices table and camelCase conversion', async () => {
|
|
497
207
|
const query = db('invoices').select('*')
|
|
498
|
-
const filteredQuery = applyFilterSnakeCase({
|
|
499
|
-
query,
|
|
500
|
-
filter: { status: 'paid', deletedAt: { isNull: true } },
|
|
501
|
-
tableName: 'invoices',
|
|
502
|
-
})
|
|
503
|
-
|
|
504
|
-
const results = await filteredQuery
|
|
505
|
-
|
|
506
|
-
expect(results).toHaveLength(2)
|
|
507
|
-
expect(results.every((r) => r.status === 'paid')).toBe(true)
|
|
508
|
-
expect(results.every((r) => r.deleted_at === null)).toBe(true)
|
|
509
|
-
})
|
|
510
208
|
|
|
511
|
-
|
|
512
|
-
const query = db('invoices').select('*')
|
|
513
|
-
const filteredQuery = applyFilterSnakeCase({
|
|
209
|
+
applyFilterSnakeCase({
|
|
514
210
|
query,
|
|
515
211
|
filter: {
|
|
516
212
|
customerId: '00000000-0000-0000-0000-000000000201',
|
|
517
213
|
amount: { gte: 500 },
|
|
518
214
|
},
|
|
519
|
-
tableName: 'invoices',
|
|
520
|
-
})
|
|
521
|
-
|
|
522
|
-
const results = await filteredQuery
|
|
523
|
-
|
|
524
|
-
expect(results).toHaveLength(3)
|
|
525
|
-
expect(
|
|
526
|
-
results.every(
|
|
527
|
-
(r) =>
|
|
528
|
-
r.customer_id === '00000000-0000-0000-0000-000000000201' &&
|
|
529
|
-
parseFloat(r.amount) >= 500,
|
|
530
|
-
),
|
|
531
|
-
).toBe(true)
|
|
532
|
-
})
|
|
533
|
-
|
|
534
|
-
it('works with invoices table using IN filter', async () => {
|
|
535
|
-
const query = db('invoices').select('*')
|
|
536
|
-
const filteredQuery = applyFilterSnakeCase({
|
|
537
|
-
query,
|
|
538
|
-
filter: { status: ['paid', 'pending'] },
|
|
539
|
-
tableName: 'invoices',
|
|
540
215
|
})
|
|
541
216
|
|
|
542
|
-
const results = await
|
|
543
|
-
|
|
217
|
+
const results = await query
|
|
544
218
|
expect(results).toHaveLength(3)
|
|
545
|
-
expect(
|
|
546
|
-
results.every((r) => r.status === 'paid' || r.status === 'pending'),
|
|
547
|
-
).toBe(true)
|
|
548
219
|
})
|
|
549
220
|
})
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
// apply-filter.integration.test.js
|
|
2
1
|
// @ts-nocheck
|
|
3
|
-
|
|
4
2
|
import { describe, it, beforeAll, afterAll, beforeEach, expect } from 'vitest'
|
|
5
3
|
import knex from 'knex'
|
|
6
4
|
|
|
@@ -10,7 +8,7 @@ import {
|
|
|
10
8
|
buildPostgresUri,
|
|
11
9
|
} from '../../src/postgresql/start-stop-postgres-docker.js'
|
|
12
10
|
|
|
13
|
-
import { applyFilter } from '../../src/postgresql/apply-filter.js'
|
|
11
|
+
import { applyFilter } from '../../src/postgresql/filters/apply-filter.js'
|
|
14
12
|
|
|
15
13
|
const PG_OPTIONS = {
|
|
16
14
|
port: 5443,
|
|
@@ -169,112 +167,64 @@ beforeEach(async () => {
|
|
|
169
167
|
|
|
170
168
|
describe('applyFilter integration (snake_case only)', () => {
|
|
171
169
|
it('applies equality filter', async () => {
|
|
172
|
-
const
|
|
173
|
-
|
|
170
|
+
const query = db('assets').select('*')
|
|
171
|
+
|
|
172
|
+
applyFilter({
|
|
173
|
+
query,
|
|
174
174
|
filter: { status: 'active' },
|
|
175
|
-
tableName: 'assets',
|
|
176
175
|
})
|
|
177
176
|
|
|
177
|
+
const results = await query
|
|
178
178
|
expect(results).toHaveLength(3)
|
|
179
179
|
})
|
|
180
180
|
|
|
181
181
|
it('applies IN filter', async () => {
|
|
182
|
-
const
|
|
183
|
-
query: db('assets').select('*'),
|
|
184
|
-
filter: { status: ['active', 'pending'] },
|
|
185
|
-
tableName: 'assets',
|
|
186
|
-
})
|
|
182
|
+
const query = db('assets').select('*')
|
|
187
183
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
it('applies NOT IN filter', async () => {
|
|
192
|
-
const results = await applyFilter({
|
|
193
|
-
query: db('assets').select('*'),
|
|
194
|
-
filter: { status: { nin: ['deleted'] } },
|
|
195
|
-
tableName: 'assets',
|
|
184
|
+
applyFilter({
|
|
185
|
+
query,
|
|
186
|
+
filter: { status: ['active', 'pending'] },
|
|
196
187
|
})
|
|
197
188
|
|
|
189
|
+
const results = await query
|
|
198
190
|
expect(results).toHaveLength(4)
|
|
199
191
|
})
|
|
200
192
|
|
|
201
193
|
it('applies numeric range filter', async () => {
|
|
202
|
-
const
|
|
203
|
-
|
|
194
|
+
const query = db('assets').select('*')
|
|
195
|
+
|
|
196
|
+
applyFilter({
|
|
197
|
+
query,
|
|
204
198
|
filter: { price: { gte: 100, lte: 200 } },
|
|
205
|
-
tableName: 'assets',
|
|
206
199
|
})
|
|
207
200
|
|
|
201
|
+
const results = await query
|
|
208
202
|
expect(results).toHaveLength(3)
|
|
209
203
|
})
|
|
210
204
|
|
|
211
|
-
it('applies isNull filter', async () => {
|
|
212
|
-
const results = await applyFilter({
|
|
213
|
-
query: db('assets').select('*'),
|
|
214
|
-
filter: { deleted_at: { isNull: true } },
|
|
215
|
-
tableName: 'assets',
|
|
216
|
-
})
|
|
217
|
-
|
|
218
|
-
expect(results).toHaveLength(4)
|
|
219
|
-
})
|
|
220
|
-
|
|
221
|
-
it('applies isNotNull filter', async () => {
|
|
222
|
-
const results = await applyFilter({
|
|
223
|
-
query: db('assets').select('*'),
|
|
224
|
-
filter: { deleted_at: { isNotNull: true } },
|
|
225
|
-
tableName: 'assets',
|
|
226
|
-
})
|
|
227
|
-
|
|
228
|
-
expect(results).toHaveLength(1)
|
|
229
|
-
})
|
|
230
|
-
|
|
231
|
-
it('applies multiple filters together', async () => {
|
|
232
|
-
const results = await applyFilter({
|
|
233
|
-
query: db('assets').select('*'),
|
|
234
|
-
filter: {
|
|
235
|
-
status: 'active',
|
|
236
|
-
type: 'invoice',
|
|
237
|
-
price: { gte: 100 },
|
|
238
|
-
},
|
|
239
|
-
tableName: 'assets',
|
|
240
|
-
})
|
|
241
|
-
|
|
242
|
-
expect(results).toHaveLength(2)
|
|
243
|
-
})
|
|
244
|
-
|
|
245
205
|
it('works with invoices table', async () => {
|
|
246
|
-
const
|
|
247
|
-
|
|
206
|
+
const query = db('invoices').select('*')
|
|
207
|
+
|
|
208
|
+
applyFilter({
|
|
209
|
+
query,
|
|
248
210
|
filter: {
|
|
249
211
|
status: 'paid',
|
|
250
212
|
deleted_at: { isNull: true },
|
|
251
213
|
},
|
|
252
|
-
tableName: 'invoices',
|
|
253
214
|
})
|
|
254
215
|
|
|
216
|
+
const results = await query
|
|
255
217
|
expect(results).toHaveLength(2)
|
|
256
218
|
})
|
|
257
219
|
|
|
258
|
-
it('
|
|
259
|
-
const
|
|
260
|
-
query: db('invoices').select('*'),
|
|
261
|
-
filter: {
|
|
262
|
-
customer_id: '00000000-0000-0000-0000-000000000201',
|
|
263
|
-
amount: { gte: 500 },
|
|
264
|
-
},
|
|
265
|
-
tableName: 'invoices',
|
|
266
|
-
})
|
|
220
|
+
it('throws when using camelCase keys', async () => {
|
|
221
|
+
const query = db('assets').select('*')
|
|
267
222
|
|
|
268
|
-
|
|
269
|
-
|
|
223
|
+
applyFilter({
|
|
224
|
+
query,
|
|
225
|
+
filter: { deletedAt: { isNull: true } },
|
|
226
|
+
})
|
|
270
227
|
|
|
271
|
-
|
|
272
|
-
await expect(
|
|
273
|
-
applyFilter({
|
|
274
|
-
query: db('assets').select('*'),
|
|
275
|
-
filter: { deletedAt: { isNull: true } },
|
|
276
|
-
tableName: 'assets',
|
|
277
|
-
}),
|
|
278
|
-
).rejects.toThrow(/column .*deletedAt.* does not exist/i)
|
|
228
|
+
await expect(query).rejects.toThrow(/deletedat/i)
|
|
279
229
|
})
|
|
280
230
|
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { getTableNameFromQuery } from '../../../src/postgresql/core/get-table-name.js'
|
|
3
|
+
|
|
4
|
+
describe('getTableNameFromQuery', () => {
|
|
5
|
+
it('extracts table name from knex query mock', () => {
|
|
6
|
+
const mockQuery = {
|
|
7
|
+
_single: {
|
|
8
|
+
table: 'assets',
|
|
9
|
+
},
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// @ts-ignore
|
|
13
|
+
expect(getTableNameFromQuery(mockQuery)).toBe('assets')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('returns undefined when table is missing', () => {
|
|
17
|
+
// @ts-ignore
|
|
18
|
+
expect(getTableNameFromQuery({})).toBeUndefined()
|
|
19
|
+
})
|
|
20
|
+
})
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { applyFilterObject } from '../../../src/postgresql/filters/apply-filter-object.js'
|
|
3
|
+
import { OPERATORS } from '../../../src/postgresql/filters/operators.js'
|
|
4
|
+
|
|
5
|
+
describe('applyFilterObject', () => {
|
|
6
|
+
it('applies multiple operators', () => {
|
|
7
|
+
const q = {}
|
|
8
|
+
// @ts-ignore
|
|
9
|
+
const spy = vi.spyOn(OPERATORS, 'gte').mockReturnValue(q)
|
|
10
|
+
|
|
11
|
+
// @ts-ignore
|
|
12
|
+
applyFilterObject(q, 'price', { gte: 100 })
|
|
13
|
+
|
|
14
|
+
expect(spy).toHaveBeenCalledWith(q, 'price', 100)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('ignores unknown operators', () => {
|
|
18
|
+
const q = {}
|
|
19
|
+
// @ts-ignore
|
|
20
|
+
const result = applyFilterObject(q, 'x', { unknown: 1 })
|
|
21
|
+
expect(result).toBe(q)
|
|
22
|
+
})
|
|
23
|
+
})
|