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.
Files changed (35) hide show
  1. package/package.json +1 -1
  2. package/src/core/normalize-phone-number.js +144 -25
  3. package/src/postgresql/core/get-table-name.js +10 -0
  4. package/src/postgresql/filters/apply-filter-object.js +19 -0
  5. package/src/postgresql/filters/apply-filter-snake-case.js +16 -0
  6. package/src/postgresql/filters/apply-filter.js +33 -0
  7. package/src/postgresql/filters/operators.js +32 -0
  8. package/src/postgresql/index.js +5 -2
  9. package/src/postgresql/modifiers/apply-order-by.js +19 -0
  10. package/src/postgresql/modifiers/apply-pagination.js +12 -0
  11. package/src/postgresql/pagination/paginate.js +48 -0
  12. package/tests/core/normalize-phone-number.unit.test.js +45 -0
  13. package/tests/postgresql/apply-filter-snake-case.integration.test.js +220 -0
  14. package/tests/postgresql/apply-filter.integration.test.js +34 -353
  15. package/tests/postgresql/core/get-table-name.unit.test.js +20 -0
  16. package/tests/postgresql/filters/apply-filter-object.test.js +23 -0
  17. package/tests/postgresql/filters/operators.unit.test.js +23 -0
  18. package/tests/postgresql/modifiers/apply-order-by.test.js +80 -0
  19. package/tests/postgresql/modifiers/apply-pagination.unit.test.js +18 -0
  20. package/tests/postgresql/paginate.integration.test.js +10 -18
  21. package/tests/postgresql/pagination/paginate.js +48 -0
  22. package/tests/postgresql/validate-schema.integration.test.js +9 -5
  23. package/types/core/normalize-phone-number.d.ts +97 -27
  24. package/types/postgresql/apply-filter.d.ts +131 -20
  25. package/types/postgresql/core/get-table-name.d.ts +10 -0
  26. package/types/postgresql/filters/apply-filter-object.d.ts +15 -0
  27. package/types/postgresql/filters/apply-filter-snake-case.d.ts +14 -0
  28. package/types/postgresql/filters/apply-filter.d.ts +15 -0
  29. package/types/postgresql/filters/operators.d.ts +14 -0
  30. package/types/postgresql/index.d.ts +5 -2
  31. package/types/postgresql/modifiers/apply-order-by.d.ts +17 -0
  32. package/types/postgresql/modifiers/apply-pagination.d.ts +17 -0
  33. package/types/postgresql/pagination/paginate.d.ts +29 -0
  34. package/src/postgresql/apply-filter.js +0 -275
  35. 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: 5444,
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.0,
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.0,
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.0,
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.0,
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.0,
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.0,
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.0,
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.0,
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.0,
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.0,
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 simple equality filter (eq)', async () => {
168
+ describe('applyFilter integration (snake_case only)', () => {
169
+ it('applies equality filter', async () => {
170
170
  const query = db('assets').select('*')
171
- const filteredQuery = applyFilter({
171
+
172
+ applyFilter({
172
173
  query,
173
174
  filter: { status: 'active' },
174
- tableName: 'assets',
175
175
  })
176
176
 
177
- const results = await filteredQuery
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 not equal filter (ne)', async () => {
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
- 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 = applyFilter({
184
+ applyFilter({
228
185
  query,
229
186
  filter: { status: ['active', 'pending'] },
230
- tableName: 'assets',
231
187
  })
232
188
 
233
- const results = await filteredQuery
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 IN filter with operator', async () => {
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
- 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 = 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 filteredQuery
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('applies isNotNull filter when value is true', async () => {
408
- const query = db('assets').select('*')
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
- it('applies multiple filters together', async () => {
436
- const query = db('assets').select('*')
437
- const filteredQuery = applyFilter({
208
+ applyFilter({
438
209
  query,
439
210
  filter: {
440
- status: 'active',
441
- type: 'invoice',
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 filteredQuery
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('returns empty results when filter matches nothing', async () => {
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
- 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 = applyFilter({
223
+ applyFilter({
471
224
  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 = 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
- 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)
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
+ })