core-services-sdk 1.3.67 → 1.3.68
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/filters/apply-filter-snake-case.js +1 -0
- package/src/postgresql/filters/apply-filter.js +6 -1
- package/src/postgresql/filters/apply-or-filter.js +39 -0
- package/tests/postgresql/filters/apply-or-filter.integration.test.js +104 -0
- package/tests/postgresql/filters/apply-or-filter.unit.test.js +112 -0
- package/tests/postgresql/paginate.integration.test.js +1 -2
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getTableNameFromQuery } from '../core/get-table-name.js'
|
|
2
2
|
import { OPERATORS } from './operators.js'
|
|
3
3
|
import { applyFilterObject } from './apply-filter-object.js'
|
|
4
|
+
import { applyOrFilter } from './apply-or-filter.js'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Applies MongoDB-style filters to a Knex QueryBuilder.
|
|
@@ -10,7 +11,7 @@ import { applyFilterObject } from './apply-filter-object.js'
|
|
|
10
11
|
* @param {Object} [params.filter]
|
|
11
12
|
* @returns {import('knex').Knex.QueryBuilder}
|
|
12
13
|
*/
|
|
13
|
-
export function applyFilter({ query, filter = {} }) {
|
|
14
|
+
export function applyFilter({ query, filter = {}, snakeCase = false }) {
|
|
14
15
|
const tableName = getTableNameFromQuery(query)
|
|
15
16
|
|
|
16
17
|
if (!filter || Object.keys(filter).length === 0) {
|
|
@@ -18,6 +19,10 @@ export function applyFilter({ query, filter = {} }) {
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
return Object.entries(filter).reduce((q, [key, value]) => {
|
|
22
|
+
if (key === 'or' && Array.isArray(value)) {
|
|
23
|
+
return applyOrFilter(q, value, tableName, snakeCase)
|
|
24
|
+
}
|
|
25
|
+
|
|
21
26
|
const qualifiedKey = tableName ? `${tableName}.${key}` : key
|
|
22
27
|
|
|
23
28
|
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { snakeCase } from 'lodash-es'
|
|
2
|
+
|
|
3
|
+
import { OPERATORS } from './operators.js'
|
|
4
|
+
import { applyFilterObject } from './apply-filter-object.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Applies OR filters.
|
|
8
|
+
*
|
|
9
|
+
* @param {import('knex').Knex.QueryBuilder} query
|
|
10
|
+
* @param {Array<Object>} orFilters
|
|
11
|
+
* @param {string | null} tableName
|
|
12
|
+
* @returns {import('knex').Knex.QueryBuilder}
|
|
13
|
+
*/
|
|
14
|
+
export function applyOrFilter(
|
|
15
|
+
query,
|
|
16
|
+
orFilters,
|
|
17
|
+
tableName,
|
|
18
|
+
snakeCaseFields = false,
|
|
19
|
+
) {
|
|
20
|
+
return query.where(function () {
|
|
21
|
+
orFilters.forEach((filterObj, index) => {
|
|
22
|
+
this[index === 0 ? 'where' : 'orWhere'](function () {
|
|
23
|
+
Object.entries(filterObj).forEach(([key, value]) => {
|
|
24
|
+
const qualifiedKey = tableName
|
|
25
|
+
? `${tableName}.${snakeCaseFields ? snakeCase(key) : key}`
|
|
26
|
+
: key
|
|
27
|
+
|
|
28
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
29
|
+
applyFilterObject(this, qualifiedKey, value)
|
|
30
|
+
} else if (Array.isArray(value)) {
|
|
31
|
+
OPERATORS.in(this, qualifiedKey, value)
|
|
32
|
+
} else {
|
|
33
|
+
OPERATORS.eq(this, qualifiedKey, value)
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { describe, it, beforeAll, afterAll, beforeEach, expect } from 'vitest'
|
|
3
|
+
import knex from 'knex'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
startPostgres,
|
|
7
|
+
stopPostgres,
|
|
8
|
+
buildPostgresUri,
|
|
9
|
+
} from '../../../src/postgresql/start-stop-postgres-docker.js'
|
|
10
|
+
|
|
11
|
+
import { applyOrFilter } from '../../../src/postgresql/filters/apply-or-filter.js'
|
|
12
|
+
|
|
13
|
+
const PG_OPTIONS = {
|
|
14
|
+
port: 5445,
|
|
15
|
+
db: 'testdb',
|
|
16
|
+
user: 'testuser',
|
|
17
|
+
pass: 'testpass',
|
|
18
|
+
containerName: 'postgres-or-filter-test-5445',
|
|
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('tenants', (table) => {
|
|
34
|
+
table.uuid('id').primary()
|
|
35
|
+
table.string('name').notNullable()
|
|
36
|
+
table.string('type').notNullable()
|
|
37
|
+
table.integer('age').notNullable()
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
afterAll(async () => {
|
|
42
|
+
if (db) {
|
|
43
|
+
await db.destroy()
|
|
44
|
+
}
|
|
45
|
+
stopPostgres(PG_OPTIONS.containerName)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
beforeEach(async () => {
|
|
49
|
+
await db('tenants').truncate()
|
|
50
|
+
|
|
51
|
+
await db('tenants').insert([
|
|
52
|
+
{
|
|
53
|
+
id: '00000000-0000-0000-0000-000000000001',
|
|
54
|
+
name: 'Tenant A',
|
|
55
|
+
type: 'business',
|
|
56
|
+
age: 2,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: '00000000-0000-0000-0000-000000000002',
|
|
60
|
+
name: 'Tenant B',
|
|
61
|
+
type: 'business',
|
|
62
|
+
age: 5,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: '00000000-0000-0000-0000-000000000003',
|
|
66
|
+
name: 'Tenant C',
|
|
67
|
+
type: 'cpa',
|
|
68
|
+
age: 7,
|
|
69
|
+
},
|
|
70
|
+
])
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
describe('applyOrFilter (integration)', () => {
|
|
74
|
+
it('returns records matching OR conditions', async () => {
|
|
75
|
+
const query = db('tenants')
|
|
76
|
+
|
|
77
|
+
applyOrFilter(query, [{ type: 'cpa' }, { age: { gte: 5 } }], 'tenants')
|
|
78
|
+
|
|
79
|
+
const rows = await query.select('*')
|
|
80
|
+
|
|
81
|
+
const names = rows.map((r) => r.name).sort()
|
|
82
|
+
expect(names).toEqual(['Tenant B', 'Tenant C'])
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('returns empty result when no OR conditions match', async () => {
|
|
86
|
+
const query = db('tenants')
|
|
87
|
+
|
|
88
|
+
applyOrFilter(query, [{ age: { gt: 100 } }], 'tenants')
|
|
89
|
+
|
|
90
|
+
const rows = await query.select('*')
|
|
91
|
+
expect(rows).toEqual([])
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('supports array IN operator inside OR', async () => {
|
|
95
|
+
const query = db('tenants')
|
|
96
|
+
|
|
97
|
+
applyOrFilter(query, [{ name: ['Tenant A', 'Tenant C'] }], 'tenants')
|
|
98
|
+
|
|
99
|
+
const rows = await query.select('*')
|
|
100
|
+
const names = rows.map((r) => r.name).sort()
|
|
101
|
+
|
|
102
|
+
expect(names).toEqual(['Tenant A', 'Tenant C'])
|
|
103
|
+
})
|
|
104
|
+
})
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { OPERATORS } from '../../../src/postgresql/filters/operators.js'
|
|
5
|
+
import { applyOrFilter } from '../../../src/postgresql/filters/apply-or-filter.js'
|
|
6
|
+
import * as applyFilterObjectModule from '../../../src/postgresql/filters/apply-filter-object.js'
|
|
7
|
+
|
|
8
|
+
describe('applyOrFilter (unit)', () => {
|
|
9
|
+
it('applies eq operator for primitive values', () => {
|
|
10
|
+
const query = {
|
|
11
|
+
where: vi.fn(function (cb) {
|
|
12
|
+
// @ts-ignore
|
|
13
|
+
cb.call(this)
|
|
14
|
+
|
|
15
|
+
// @ts-ignore
|
|
16
|
+
return this
|
|
17
|
+
}),
|
|
18
|
+
orWhere: vi.fn(function (cb) {
|
|
19
|
+
// @ts-ignore
|
|
20
|
+
cb.call(this)
|
|
21
|
+
|
|
22
|
+
// @ts-ignore
|
|
23
|
+
return this
|
|
24
|
+
}),
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// @ts-ignore
|
|
28
|
+
const eqSpy = vi.spyOn(OPERATORS, 'eq').mockImplementation(() => query)
|
|
29
|
+
|
|
30
|
+
// @ts-ignore
|
|
31
|
+
applyOrFilter(query, [{ name: 'A' }, { name: 'B' }], null)
|
|
32
|
+
|
|
33
|
+
expect(eqSpy).toHaveBeenCalledTimes(2)
|
|
34
|
+
expect(eqSpy).toHaveBeenCalledWith(expect.anything(), 'name', 'A')
|
|
35
|
+
expect(eqSpy).toHaveBeenCalledWith(expect.anything(), 'name', 'B')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('applies IN operator for array values', () => {
|
|
39
|
+
const query = {
|
|
40
|
+
where: vi.fn(function (cb) {
|
|
41
|
+
// @ts-ignore
|
|
42
|
+
cb.call(this)
|
|
43
|
+
|
|
44
|
+
// @ts-ignore
|
|
45
|
+
return this
|
|
46
|
+
}),
|
|
47
|
+
orWhere: vi.fn(function (cb) {
|
|
48
|
+
// @ts-ignore
|
|
49
|
+
cb.call(this)
|
|
50
|
+
|
|
51
|
+
// @ts-ignore
|
|
52
|
+
return this
|
|
53
|
+
}),
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// @ts-ignore
|
|
57
|
+
const inSpy = vi.spyOn(OPERATORS, 'in').mockImplementation(() => query)
|
|
58
|
+
|
|
59
|
+
// @ts-ignore
|
|
60
|
+
applyOrFilter(query, [{ id: [1, 2, 3] }], null)
|
|
61
|
+
|
|
62
|
+
expect(inSpy).toHaveBeenCalledWith(expect.anything(), 'id', [1, 2, 3])
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('delegates object values to applyFilterObject', () => {
|
|
66
|
+
const query = {
|
|
67
|
+
where: vi.fn(function (cb) {
|
|
68
|
+
cb.call(this)
|
|
69
|
+
return this
|
|
70
|
+
}),
|
|
71
|
+
orWhere: vi.fn(function (cb) {
|
|
72
|
+
cb.call(this)
|
|
73
|
+
return this
|
|
74
|
+
}),
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const spy = vi
|
|
78
|
+
.spyOn(applyFilterObjectModule, 'applyFilterObject')
|
|
79
|
+
.mockImplementation(() => query)
|
|
80
|
+
|
|
81
|
+
applyOrFilter(query, [{ age: { gte: 18 } }], null)
|
|
82
|
+
|
|
83
|
+
expect(spy).toHaveBeenCalledWith(expect.anything(), 'age', { gte: 18 })
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('applies snake_case when enabled', () => {
|
|
87
|
+
const query = {
|
|
88
|
+
where: vi.fn(function (cb) {
|
|
89
|
+
// @ts-ignore
|
|
90
|
+
cb.call(this)
|
|
91
|
+
|
|
92
|
+
// @ts-ignore
|
|
93
|
+
return this
|
|
94
|
+
}),
|
|
95
|
+
orWhere: vi.fn(function (cb) {
|
|
96
|
+
// @ts-ignore
|
|
97
|
+
cb.call(this)
|
|
98
|
+
|
|
99
|
+
// @ts-ignore
|
|
100
|
+
return this
|
|
101
|
+
}),
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// @ts-ignore
|
|
105
|
+
const eqSpy = vi.spyOn(OPERATORS, 'eq').mockImplementation(() => query)
|
|
106
|
+
|
|
107
|
+
// @ts-ignore
|
|
108
|
+
applyOrFilter(query, [{ createdAt: 1 }], 't', true)
|
|
109
|
+
|
|
110
|
+
expect(eqSpy).toHaveBeenCalledWith(expect.anything(), 't.created_at', 1)
|
|
111
|
+
})
|
|
112
|
+
})
|
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
} from '../../src/postgresql/start-stop-postgres-docker.js'
|
|
10
10
|
|
|
11
11
|
import { sqlPaginate } from '../../src/postgresql/pagination/paginate.js'
|
|
12
|
-
import { sub } from 'date-fns'
|
|
13
12
|
|
|
14
13
|
const PG_OPTIONS = {
|
|
15
14
|
port: 5442,
|
|
@@ -123,7 +122,7 @@ describe('paginate integration', () => {
|
|
|
123
122
|
})
|
|
124
123
|
|
|
125
124
|
it('applies filters correctly', async () => {
|
|
126
|
-
const minDate =
|
|
125
|
+
const minDate = new Date('2024-01-01')
|
|
127
126
|
const result = await sqlPaginate({
|
|
128
127
|
baseQuery: db('tenants'),
|
|
129
128
|
filter: {
|