core-services-sdk 1.3.63 → 1.3.64

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.
@@ -0,0 +1,549 @@
1
+ // @ts-nocheck
2
+ import { describe, it, beforeAll, afterAll, beforeEach, expect } from 'vitest'
3
+ import knex from 'knex'
4
+
5
+ import {
6
+ stopPostgres,
7
+ startPostgres,
8
+ buildPostgresUri,
9
+ } from '../../src/postgresql/start-stop-postgres-docker.js'
10
+
11
+ import { applyFilterSnakeCase } from '../../src/postgresql/apply-filter.js'
12
+
13
+ const PG_OPTIONS = {
14
+ port: 5444,
15
+ containerName: 'postgres-apply-filter-test-5444',
16
+ user: 'testuser',
17
+ pass: 'testpass',
18
+ db: 'testdb',
19
+ }
20
+
21
+ const DATABASE_URI = buildPostgresUri(PG_OPTIONS)
22
+
23
+ let db
24
+
25
+ beforeAll(async () => {
26
+ startPostgres(PG_OPTIONS)
27
+
28
+ db = knex({
29
+ client: 'pg',
30
+ connection: DATABASE_URI,
31
+ })
32
+
33
+ await db.schema.createTable('assets', (table) => {
34
+ table.uuid('id').primary()
35
+ table.string('name').notNullable()
36
+ table.string('status')
37
+ table.string('type')
38
+ table.decimal('price', 10, 2)
39
+ table.timestamp('created_at').notNullable()
40
+ table.timestamp('deleted_at')
41
+ })
42
+
43
+ await db.schema.createTable('invoices', (table) => {
44
+ table.uuid('id').primary()
45
+ table.string('invoice_number').notNullable()
46
+ table.decimal('amount', 10, 2).notNullable()
47
+ table.string('status')
48
+ table.uuid('customer_id')
49
+ table.timestamp('created_at').notNullable()
50
+ table.timestamp('paid_at')
51
+ table.timestamp('deleted_at')
52
+ })
53
+ })
54
+
55
+ afterAll(async () => {
56
+ if (db) {
57
+ await db.destroy()
58
+ }
59
+ stopPostgres(PG_OPTIONS.containerName)
60
+ })
61
+
62
+ beforeEach(async () => {
63
+ await db('assets').truncate()
64
+ await db('invoices').truncate()
65
+
66
+ await db('assets').insert([
67
+ {
68
+ id: '00000000-0000-0000-0000-000000000001',
69
+ name: 'Asset One',
70
+ status: 'active',
71
+ type: 'invoice',
72
+ price: 100.0,
73
+ created_at: new Date('2024-01-01'),
74
+ deleted_at: null,
75
+ },
76
+ {
77
+ id: '00000000-0000-0000-0000-000000000002',
78
+ name: 'Asset Two',
79
+ status: 'active',
80
+ type: 'receipt',
81
+ price: 200.0,
82
+ created_at: new Date('2024-01-02'),
83
+ deleted_at: null,
84
+ },
85
+ {
86
+ id: '00000000-0000-0000-0000-000000000003',
87
+ name: 'Asset Three',
88
+ status: 'pending',
89
+ type: 'invoice',
90
+ price: 150.0,
91
+ created_at: new Date('2024-01-03'),
92
+ deleted_at: null,
93
+ },
94
+ {
95
+ id: '00000000-0000-0000-0000-000000000004',
96
+ name: 'Deleted Asset',
97
+ status: 'deleted',
98
+ type: 'receipt',
99
+ price: 50.0,
100
+ created_at: new Date('2024-01-04'),
101
+ deleted_at: new Date('2024-01-05'),
102
+ },
103
+ {
104
+ id: '00000000-0000-0000-0000-000000000005',
105
+ name: 'Expensive Asset',
106
+ status: 'active',
107
+ type: 'invoice',
108
+ price: 500.0,
109
+ created_at: new Date('2024-01-05'),
110
+ deleted_at: null,
111
+ },
112
+ ])
113
+
114
+ await db('invoices').insert([
115
+ {
116
+ id: '00000000-0000-0000-0000-000000000101',
117
+ invoice_number: 'INV-001',
118
+ amount: 1000.0,
119
+ status: 'paid',
120
+ customer_id: '00000000-0000-0000-0000-000000000201',
121
+ created_at: new Date('2024-01-01'),
122
+ paid_at: new Date('2024-01-02'),
123
+ deleted_at: null,
124
+ },
125
+ {
126
+ id: '00000000-0000-0000-0000-000000000102',
127
+ invoice_number: 'INV-002',
128
+ amount: 2500.0,
129
+ status: 'pending',
130
+ customer_id: '00000000-0000-0000-0000-000000000202',
131
+ created_at: new Date('2024-01-02'),
132
+ paid_at: null,
133
+ deleted_at: null,
134
+ },
135
+ {
136
+ id: '00000000-0000-0000-0000-000000000103',
137
+ invoice_number: 'INV-003',
138
+ amount: 500.0,
139
+ status: 'paid',
140
+ customer_id: '00000000-0000-0000-0000-000000000201',
141
+ created_at: new Date('2024-01-03'),
142
+ paid_at: new Date('2024-01-04'),
143
+ deleted_at: null,
144
+ },
145
+ {
146
+ id: '00000000-0000-0000-0000-000000000104',
147
+ invoice_number: 'INV-004',
148
+ amount: 3000.0,
149
+ status: 'overdue',
150
+ customer_id: '00000000-0000-0000-0000-000000000203',
151
+ created_at: new Date('2024-01-04'),
152
+ paid_at: null,
153
+ deleted_at: null,
154
+ },
155
+ {
156
+ id: '00000000-0000-0000-0000-000000000105',
157
+ invoice_number: 'INV-005',
158
+ amount: 750.0,
159
+ status: 'cancelled',
160
+ customer_id: '00000000-0000-0000-0000-000000000201',
161
+ created_at: new Date('2024-01-05'),
162
+ paid_at: null,
163
+ deleted_at: new Date('2024-01-06'),
164
+ },
165
+ ])
166
+ })
167
+
168
+ describe('applyFilterSnakeCase integration', () => {
169
+ it('applies simple equality filter (eq)', async () => {
170
+ const query = db('assets').select('*')
171
+ const filteredQuery = applyFilterSnakeCase({
172
+ query,
173
+ filter: { status: 'active' },
174
+ tableName: 'assets',
175
+ })
176
+
177
+ const results = await filteredQuery
178
+
179
+ expect(results).toHaveLength(3)
180
+ expect(results.every((r) => r.status === 'active')).toBe(true)
181
+ })
182
+
183
+ it('converts camelCase keys to snake_case', async () => {
184
+ const query = db('assets').select('*')
185
+ const filteredQuery = applyFilterSnakeCase({
186
+ query,
187
+ filter: { deletedAt: { isNull: true }, name: 'Asset One' },
188
+ tableName: 'assets',
189
+ })
190
+
191
+ const results = await filteredQuery
192
+
193
+ expect(results).toHaveLength(1)
194
+ expect(results[0].name).toBe('Asset One')
195
+ })
196
+
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
+ it('ignores unknown operators', async () => {
483
+ const query = db('assets').select('*')
484
+ const filteredQuery = applyFilterSnakeCase({
485
+ query,
486
+ filter: { status: { unknownOperator: 'value' } },
487
+ tableName: 'assets',
488
+ })
489
+
490
+ const results = await filteredQuery
491
+
492
+ // Should return all records since unknown operator is ignored
493
+ expect(results).toHaveLength(5)
494
+ })
495
+
496
+ it('works with invoices table', async () => {
497
+ 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
+
511
+ it('works with invoices table using camelCase conversion', async () => {
512
+ const query = db('invoices').select('*')
513
+ const filteredQuery = applyFilterSnakeCase({
514
+ query,
515
+ filter: {
516
+ customerId: '00000000-0000-0000-0000-000000000201',
517
+ amount: { gte: 500 },
518
+ },
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
+ })
541
+
542
+ const results = await filteredQuery
543
+
544
+ expect(results).toHaveLength(3)
545
+ expect(
546
+ results.every((r) => r.status === 'paid' || r.status === 'pending'),
547
+ ).toBe(true)
548
+ })
549
+ })