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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "core-services-sdk",
3
- "version": "1.3.67",
3
+ "version": "1.3.68",
4
4
  "main": "src/index.js",
5
5
  "type": "module",
6
6
  "types": "types/index.d.ts",
@@ -12,5 +12,6 @@ export function applyFilterSnakeCase({ query, filter }) {
12
12
  return applyFilter({
13
13
  query,
14
14
  filter: toSnakeCase(filter),
15
+ snakeCase: true,
15
16
  })
16
17
  }
@@ -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 = sub(new Date(), { years: 2 })
125
+ const minDate = new Date('2024-01-01')
127
126
  const result = await sqlPaginate({
128
127
  baseQuery: db('tenants'),
129
128
  filter: {