core-services-sdk 1.3.63 → 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/core/normalize-phone-number.js +144 -25
- 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 +45 -0
- package/tests/postgresql/apply-filter-snake-case.integration.test.js +220 -0
- package/tests/postgresql/apply-filter.integration.test.js +34 -353
- 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 +10 -18
- package/tests/postgresql/pagination/paginate.js +48 -0
- package/tests/postgresql/validate-schema.integration.test.js +9 -5
- package/types/core/normalize-phone-number.d.ts +97 -27
- package/types/postgresql/apply-filter.d.ts +131 -20
- 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 -275
- package/src/postgresql/paginate.js +0 -61
|
@@ -3,15 +3,15 @@ import { describe, it, beforeAll, afterAll, beforeEach, expect } from 'vitest'
|
|
|
3
3
|
import knex from 'knex'
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
|
-
stopPostgres,
|
|
7
6
|
startPostgres,
|
|
7
|
+
stopPostgres,
|
|
8
8
|
buildPostgresUri,
|
|
9
9
|
} from '../../src/postgresql/start-stop-postgres-docker.js'
|
|
10
10
|
|
|
11
|
-
import { applyFilter } from '../../src/postgresql/apply-filter.js'
|
|
11
|
+
import { applyFilter } from '../../src/postgresql/filters/apply-filter.js'
|
|
12
12
|
|
|
13
13
|
const PG_OPTIONS = {
|
|
14
|
-
port:
|
|
14
|
+
port: 5443,
|
|
15
15
|
containerName: 'postgres-apply-filter-test',
|
|
16
16
|
user: 'testuser',
|
|
17
17
|
pass: 'testpass',
|
|
@@ -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'),
|
|
@@ -165,385 +165,66 @@ beforeEach(async () => {
|
|
|
165
165
|
])
|
|
166
166
|
})
|
|
167
167
|
|
|
168
|
-
describe('applyFilter integration', () => {
|
|
169
|
-
it('applies
|
|
168
|
+
describe('applyFilter integration (snake_case only)', () => {
|
|
169
|
+
it('applies equality filter', async () => {
|
|
170
170
|
const query = db('assets').select('*')
|
|
171
|
-
|
|
171
|
+
|
|
172
|
+
applyFilter({
|
|
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
|
-
})
|
|
182
|
-
|
|
183
|
-
it('converts camelCase keys to snake_case', async () => {
|
|
184
|
-
const query = db('assets').select('*')
|
|
185
|
-
const filteredQuery = applyFilter({
|
|
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
179
|
})
|
|
196
180
|
|
|
197
|
-
it('applies
|
|
181
|
+
it('applies IN filter', async () => {
|
|
198
182
|
const query = db('assets').select('*')
|
|
199
|
-
const filteredQuery = applyFilter({
|
|
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 = applyFilter({
|
|
214
|
-
query,
|
|
215
|
-
filter: { status: { neq: 'deleted' } },
|
|
216
|
-
tableName: 'assets',
|
|
217
|
-
})
|
|
218
183
|
|
|
219
|
-
|
|
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 = applyFilter({
|
|
184
|
+
applyFilter({
|
|
228
185
|
query,
|
|
229
186
|
filter: { status: ['active', 'pending'] },
|
|
230
|
-
tableName: 'assets',
|
|
231
187
|
})
|
|
232
188
|
|
|
233
|
-
const results = await
|
|
234
|
-
|
|
189
|
+
const results = await query
|
|
235
190
|
expect(results).toHaveLength(4)
|
|
236
|
-
expect(
|
|
237
|
-
results.every((r) => r.status === 'active' || r.status === 'pending'),
|
|
238
|
-
).toBe(true)
|
|
239
191
|
})
|
|
240
192
|
|
|
241
|
-
it('applies
|
|
193
|
+
it('applies numeric range filter', async () => {
|
|
242
194
|
const query = db('assets').select('*')
|
|
243
|
-
const filteredQuery = applyFilter({
|
|
244
|
-
query,
|
|
245
|
-
filter: { status: { in: ['active', 'pending'] } },
|
|
246
|
-
tableName: 'assets',
|
|
247
|
-
})
|
|
248
|
-
|
|
249
|
-
const results = await filteredQuery
|
|
250
195
|
|
|
251
|
-
|
|
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 = applyFilter({
|
|
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 = applyFilter({
|
|
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 = applyFilter({
|
|
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 = applyFilter({
|
|
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 = applyFilter({
|
|
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 = applyFilter({
|
|
196
|
+
applyFilter({
|
|
332
197
|
query,
|
|
333
198
|
filter: { price: { gte: 100, lte: 200 } },
|
|
334
|
-
tableName: 'assets',
|
|
335
199
|
})
|
|
336
200
|
|
|
337
|
-
const results = await
|
|
338
|
-
|
|
201
|
+
const results = await query
|
|
339
202
|
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 = applyFilter({
|
|
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 = applyFilter({
|
|
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 = applyFilter({
|
|
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 = applyFilter({
|
|
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
203
|
})
|
|
406
204
|
|
|
407
|
-
it('
|
|
408
|
-
const query = db('
|
|
409
|
-
const filteredQuery = applyFilter({
|
|
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 = applyFilter({
|
|
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
|
-
})
|
|
205
|
+
it('works with invoices table', async () => {
|
|
206
|
+
const query = db('invoices').select('*')
|
|
434
207
|
|
|
435
|
-
|
|
436
|
-
const query = db('assets').select('*')
|
|
437
|
-
const filteredQuery = applyFilter({
|
|
208
|
+
applyFilter({
|
|
438
209
|
query,
|
|
439
210
|
filter: {
|
|
440
|
-
status: '
|
|
441
|
-
|
|
442
|
-
price: { gte: 100 },
|
|
211
|
+
status: 'paid',
|
|
212
|
+
deleted_at: { isNull: true },
|
|
443
213
|
},
|
|
444
|
-
tableName: 'assets',
|
|
445
214
|
})
|
|
446
215
|
|
|
447
|
-
const results = await
|
|
448
|
-
|
|
216
|
+
const results = await query
|
|
449
217
|
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
218
|
})
|
|
454
219
|
|
|
455
|
-
it('
|
|
220
|
+
it('throws when using camelCase keys', async () => {
|
|
456
221
|
const query = db('assets').select('*')
|
|
457
|
-
const filteredQuery = applyFilter({
|
|
458
|
-
query,
|
|
459
|
-
filter: { status: 'non-existing' },
|
|
460
|
-
tableName: 'assets',
|
|
461
|
-
})
|
|
462
222
|
|
|
463
|
-
|
|
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 = applyFilter({
|
|
223
|
+
applyFilter({
|
|
471
224
|
query,
|
|
472
|
-
filter: {
|
|
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 = applyFilter({
|
|
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 = applyFilter({
|
|
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 = applyFilter({
|
|
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 = applyFilter({
|
|
537
|
-
query,
|
|
538
|
-
filter: { status: ['paid', 'pending'] },
|
|
539
|
-
tableName: 'invoices',
|
|
225
|
+
filter: { deletedAt: { isNull: true } },
|
|
540
226
|
})
|
|
541
227
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
expect(results).toHaveLength(3)
|
|
545
|
-
expect(
|
|
546
|
-
results.every((r) => r.status === 'paid' || r.status === 'pending'),
|
|
547
|
-
).toBe(true)
|
|
228
|
+
await expect(query).rejects.toThrow(/deletedat/i)
|
|
548
229
|
})
|
|
549
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
|
+
})
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { OPERATORS } from '../../../src/postgresql/filters/operators.js'
|
|
4
|
+
|
|
5
|
+
describe('OPERATORS', () => {
|
|
6
|
+
it('eq calls where with "="', () => {
|
|
7
|
+
const q = { where: vi.fn(() => q) }
|
|
8
|
+
OPERATORS.eq(q, 'a', 1)
|
|
9
|
+
expect(q.where).toHaveBeenCalledWith('a', '=', 1)
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('in calls whereIn', () => {
|
|
13
|
+
const q = { whereIn: vi.fn(() => q) }
|
|
14
|
+
OPERATORS.in(q, 'a', [1, 2])
|
|
15
|
+
expect(q.whereIn).toHaveBeenCalledWith('a', [1, 2])
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('isNull true calls whereNull', () => {
|
|
19
|
+
const q = { whereNull: vi.fn(() => q) }
|
|
20
|
+
OPERATORS.isNull(q, 'a', true)
|
|
21
|
+
expect(q.whereNull).toHaveBeenCalledWith('a')
|
|
22
|
+
})
|
|
23
|
+
})
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
3
|
+
import { applyOrderBy } from '../../../src/postgresql/modifiers/apply-order-by.js'
|
|
4
|
+
|
|
5
|
+
describe('applyOrderBy', () => {
|
|
6
|
+
it('does nothing when orderBy is not provided', () => {
|
|
7
|
+
const query = {}
|
|
8
|
+
|
|
9
|
+
// @ts-ignore
|
|
10
|
+
const result = applyOrderBy({ query })
|
|
11
|
+
|
|
12
|
+
expect(result).toBe(query)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('does nothing when orderBy.column is missing', () => {
|
|
16
|
+
const query = {}
|
|
17
|
+
|
|
18
|
+
const result = applyOrderBy({
|
|
19
|
+
query,
|
|
20
|
+
orderBy: {},
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
expect(result).toBe(query)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('applies ORDER BY with default direction asc', () => {
|
|
27
|
+
const query = {
|
|
28
|
+
orderBy: vi.fn(() => query),
|
|
29
|
+
_single: { table: 'assets' },
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
applyOrderBy({
|
|
33
|
+
query,
|
|
34
|
+
orderBy: { column: 'created_at' },
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
expect(query.orderBy).toHaveBeenCalledWith('assets.created_at', 'asc')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('applies ORDER BY with explicit direction', () => {
|
|
41
|
+
const query = {
|
|
42
|
+
orderBy: vi.fn(() => query),
|
|
43
|
+
_single: { table: 'assets' },
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
applyOrderBy({
|
|
47
|
+
query,
|
|
48
|
+
orderBy: { column: 'created_at', direction: 'desc' },
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
expect(query.orderBy).toHaveBeenCalledWith('assets.created_at', 'desc')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('uses unqualified column when table name cannot be resolved', () => {
|
|
55
|
+
const query = {
|
|
56
|
+
orderBy: vi.fn(() => query),
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
applyOrderBy({
|
|
60
|
+
query,
|
|
61
|
+
orderBy: { column: 'created_at' },
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
expect(query.orderBy).toHaveBeenCalledWith('created_at', 'asc')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('returns the same query instance for chaining', () => {
|
|
68
|
+
const query = {
|
|
69
|
+
orderBy: vi.fn(() => query),
|
|
70
|
+
_single: { table: 'assets' },
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const result = applyOrderBy({
|
|
74
|
+
query,
|
|
75
|
+
orderBy: { column: 'id' },
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
expect(result).toBe(query)
|
|
79
|
+
})
|
|
80
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { applyPagination } from '../../../src/postgresql/modifiers/apply-pagination.js'
|
|
4
|
+
|
|
5
|
+
describe('applyPagination', () => {
|
|
6
|
+
it('applies limit and offset', () => {
|
|
7
|
+
const q = {
|
|
8
|
+
limit: vi.fn(() => q),
|
|
9
|
+
offset: vi.fn(() => q),
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// @ts-ignore
|
|
13
|
+
applyPagination({ query: q, page: 2, limit: 10 })
|
|
14
|
+
|
|
15
|
+
expect(q.limit).toHaveBeenCalledWith(10)
|
|
16
|
+
expect(q.offset).toHaveBeenCalledWith(10)
|
|
17
|
+
})
|
|
18
|
+
})
|