core-services-sdk 1.3.60 → 1.3.62

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.60",
3
+ "version": "1.3.62",
4
4
  "main": "src/index.js",
5
5
  "type": "module",
6
6
  "types": "types/index.d.ts",
@@ -0,0 +1,275 @@
1
+ import { toSnakeCase } from '../core/case-mapper.js'
2
+
3
+ /**
4
+ * @typedef {Object} OperatorFunction
5
+ * @property {import('knex').Knex.QueryBuilder} query - Knex query builder instance
6
+ * @property {string} key - Column name (qualified with table name)
7
+ * @property {*} value - Value to compare against
8
+ * @returns {import('knex').Knex.QueryBuilder} Modified query builder
9
+ */
10
+
11
+ /**
12
+ * Map of filter operators to their corresponding Knex query builder methods.
13
+ * Each operator function applies a WHERE condition to the query.
14
+ *
15
+ * @type {Object<string, OperatorFunction>}
16
+ * @private
17
+ */
18
+ const OPERATORS = {
19
+ /**
20
+ * Array membership operator. Checks if column value is in the provided array.
21
+ * @param {import('knex').Knex.QueryBuilder} q - Query builder
22
+ * @param {string} key - Column name
23
+ * @param {Array<*>} value - Array of values to match
24
+ * @returns {import('knex').Knex.QueryBuilder} Query with WHERE IN clause
25
+ */
26
+ in: (q, key, value) => q.whereIn(key, value),
27
+
28
+ /**
29
+ * Not in array operator. Checks if column value is NOT in the provided array.
30
+ * @param {import('knex').Knex.QueryBuilder} q - Query builder
31
+ * @param {string} key - Column name
32
+ * @param {Array<*>} value - Array of values to exclude
33
+ * @returns {import('knex').Knex.QueryBuilder} Query with WHERE NOT IN clause
34
+ */
35
+ nin: (q, key, value) => q.whereNotIn(key, value),
36
+
37
+ /**
38
+ * Equality operator. Checks if column value equals the provided value.
39
+ * @param {import('knex').Knex.QueryBuilder} q - Query builder
40
+ * @param {string} key - Column name
41
+ * @param {*} value - Value to match
42
+ * @returns {import('knex').Knex.QueryBuilder} Query with WHERE = clause
43
+ */
44
+ eq: (q, key, value) => q.where(key, '=', value),
45
+
46
+ /**
47
+ * Not equal operator. Checks if column value does not equal the provided value.
48
+ * @param {import('knex').Knex.QueryBuilder} q - Query builder
49
+ * @param {string} key - Column name
50
+ * @param {*} value - Value to exclude
51
+ * @returns {import('knex').Knex.QueryBuilder} Query with WHERE != clause
52
+ */
53
+ ne: (q, key, value) => q.where(key, '!=', value),
54
+
55
+ /**
56
+ * Not equal operator (alias for `ne`). Checks if column value does not equal the provided value.
57
+ * @param {import('knex').Knex.QueryBuilder} q - Query builder
58
+ * @param {string} key - Column name
59
+ * @param {*} value - Value to exclude
60
+ * @returns {import('knex').Knex.QueryBuilder} Query with WHERE != clause
61
+ */
62
+ neq: (q, key, value) => q.where(key, '!=', value),
63
+
64
+ /**
65
+ * Greater than operator. Checks if column value is greater than the provided value.
66
+ * @param {import('knex').Knex.QueryBuilder} q - Query builder
67
+ * @param {string} key - Column name
68
+ * @param {number|string|Date} value - Value to compare against
69
+ * @returns {import('knex').Knex.QueryBuilder} Query with WHERE > clause
70
+ */
71
+ gt: (q, key, value) => q.where(key, '>', value),
72
+
73
+ /**
74
+ * Greater than or equal operator. Checks if column value is greater than or equal to the provided value.
75
+ * @param {import('knex').Knex.QueryBuilder} q - Query builder
76
+ * @param {string} key - Column name
77
+ * @param {number|string|Date} value - Value to compare against
78
+ * @returns {import('knex').Knex.QueryBuilder} Query with WHERE >= clause
79
+ */
80
+ gte: (q, key, value) => q.where(key, '>=', value),
81
+
82
+ /**
83
+ * Less than operator. Checks if column value is less than the provided value.
84
+ * @param {import('knex').Knex.QueryBuilder} q - Query builder
85
+ * @param {string} key - Column name
86
+ * @param {number|string|Date} value - Value to compare against
87
+ * @returns {import('knex').Knex.QueryBuilder} Query with WHERE < clause
88
+ */
89
+ lt: (q, key, value) => q.where(key, '<', value),
90
+
91
+ /**
92
+ * Less than or equal operator. Checks if column value is less than or equal to the provided value.
93
+ * @param {import('knex').Knex.QueryBuilder} q - Query builder
94
+ * @param {string} key - Column name
95
+ * @param {number|string|Date} value - Value to compare against
96
+ * @returns {import('knex').Knex.QueryBuilder} Query with WHERE <= clause
97
+ */
98
+ lte: (q, key, value) => q.where(key, '<=', value),
99
+
100
+ /**
101
+ * Case-sensitive pattern matching operator. Uses SQL LIKE for pattern matching.
102
+ * Supports wildcards: `%` (any sequence) and `_` (single character).
103
+ * @param {import('knex').Knex.QueryBuilder} q - Query builder
104
+ * @param {string} key - Column name
105
+ * @param {string} value - Pattern to match (e.g., '%invoice%')
106
+ * @returns {import('knex').Knex.QueryBuilder} Query with WHERE LIKE clause
107
+ */
108
+ like: (q, key, value) => q.where(key, 'like', value),
109
+
110
+ /**
111
+ * Case-insensitive pattern matching operator. Uses PostgreSQL ILIKE for pattern matching.
112
+ * Supports wildcards: `%` (any sequence) and `_` (single character).
113
+ * @param {import('knex').Knex.QueryBuilder} q - Query builder
114
+ * @param {string} key - Column name
115
+ * @param {string} value - Pattern to match (e.g., '%invoice%')
116
+ * @returns {import('knex').Knex.QueryBuilder} Query with WHERE ILIKE clause
117
+ */
118
+ ilike: (q, key, value) => q.where(key, 'ilike', value),
119
+
120
+ /**
121
+ * Null check operator. Checks if column value is NULL or NOT NULL based on the boolean value.
122
+ * @param {import('knex').Knex.QueryBuilder} q - Query builder
123
+ * @param {string} key - Column name
124
+ * @param {boolean} value - If true, checks for NULL; if false, checks for NOT NULL
125
+ * @returns {import('knex').Knex.QueryBuilder} Query with WHERE NULL or WHERE NOT NULL clause
126
+ */
127
+ isNull: (q, key, value) => (value ? q.whereNull(key) : q.whereNotNull(key)),
128
+
129
+ /**
130
+ * Not null check operator. Checks if column value is NOT NULL or NULL based on the boolean value.
131
+ * @param {import('knex').Knex.QueryBuilder} q - Query builder
132
+ * @param {string} key - Column name
133
+ * @param {boolean} value - If true, checks for NOT NULL; if false, checks for NULL
134
+ * @returns {import('knex').Knex.QueryBuilder} Query with WHERE NOT NULL or WHERE NULL clause
135
+ */
136
+ isNotNull: (q, key, value) =>
137
+ value ? q.whereNotNull(key) : q.whereNull(key),
138
+ }
139
+
140
+ /**
141
+ * Applies filter operators from an object to a Knex query.
142
+ * Processes each operator in the value object and applies the corresponding WHERE clause.
143
+ * Unknown operators are silently ignored.
144
+ *
145
+ * @param {import('knex').Knex.QueryBuilder} q - Knex query builder instance
146
+ * @param {string} qualifiedKey - Fully qualified column name (e.g., "table.column")
147
+ * @param {Object<string, *>} value - Object containing operator-value pairs
148
+ * @returns {import('knex').Knex.QueryBuilder} Modified query builder with applied filters
149
+ * @private
150
+ *
151
+ * @example
152
+ * // Apply multiple operators on the same field
153
+ * applyFilterObject(query, 'assets.price', { gte: 100, lte: 200 })
154
+ * // Results in: WHERE assets.price >= 100 AND assets.price <= 200
155
+ */
156
+ const applyFilterObject = (q, qualifiedKey, value) => {
157
+ return Object.entries(value).reduce(
158
+ (q, [operator, val]) =>
159
+ OPERATORS[operator] ? OPERATORS[operator](q, qualifiedKey, val) : q,
160
+ q,
161
+ )
162
+ }
163
+
164
+ /**
165
+ * Builds a Knex query with MongoDB-style filter operators.
166
+ * Pure utility function that can be used across repositories.
167
+ *
168
+ * This function converts camelCase filter keys to snake_case and applies
169
+ * various filter operators to build SQL WHERE clauses. All column names
170
+ * are automatically qualified with the provided table name.
171
+ *
172
+ * **Supported Operators:**
173
+ * - `eq` - Equality (default for simple values)
174
+ * - `ne` / `neq` - Not equal
175
+ * - `in` - Array membership (or pass array directly)
176
+ * - `nin` - Not in array
177
+ * - `gt` - Greater than
178
+ * - `gte` - Greater than or equal
179
+ * - `lt` - Less than
180
+ * - `lte` - Less than or equal
181
+ * - `like` - Case-sensitive pattern matching (SQL LIKE)
182
+ * - `ilike` - Case-insensitive pattern matching (PostgreSQL ILIKE)
183
+ * - `isNull` - Check if field is NULL
184
+ * - `isNotNull` - Check if field is NOT NULL
185
+ *
186
+ * **Key Features:**
187
+ * - Automatic camelCase to snake_case conversion for filter keys
188
+ * - Qualified column names (table.column format)
189
+ * - Multiple operators can be applied to the same field
190
+ * - Unknown operators are silently ignored
191
+ * - Arrays are automatically converted to IN clauses
192
+ *
193
+ * @param {Object} params - Function parameters
194
+ * @param {import('knex').Knex.QueryBuilder} params.query - Knex query builder instance to apply filters to
195
+ * @param {Object<string, *>} params.filter - Filter object with camelCase keys (will be converted to snake_case)
196
+ * Filter values can be:
197
+ * - Simple values (string, number, boolean) → treated as equality
198
+ * - Arrays → treated as IN operator
199
+ * - Objects with operator keys → apply specific operators
200
+ * @param {string} params.tableName - Table name used to qualify column names (e.g., "assets" → "assets.column_name")
201
+ * @returns {import('knex').Knex.QueryBuilder} Modified query builder with applied WHERE clauses
202
+ *
203
+ * @throws {TypeError} If query is not a valid Knex QueryBuilder instance
204
+ *
205
+ * @example
206
+ * // Simple equality - converts to WHERE assets.status = 'active'
207
+ * const query = db('assets').select('*')
208
+ * applyFilter({ query, filter: { status: 'active' }, tableName: 'assets' })
209
+ *
210
+ * @example
211
+ * // Not equal - converts to WHERE assets.status != 'deleted'
212
+ * applyFilter({ query, filter: { status: { ne: 'deleted' } }, tableName: 'assets' })
213
+ *
214
+ * @example
215
+ * // Array/IN operator - converts to WHERE assets.status IN ('active', 'pending')
216
+ * applyFilter({ query, filter: { status: ['active', 'pending'] }, tableName: 'assets' })
217
+ *
218
+ * @example
219
+ * // Explicit IN operator
220
+ * applyFilter({ query, filter: { status: { in: ['active', 'pending'] } }, tableName: 'assets' })
221
+ *
222
+ * @example
223
+ * // Not in array - converts to WHERE assets.status NOT IN ('deleted', 'archived')
224
+ * applyFilter({ query, filter: { status: { nin: ['deleted', 'archived'] } }, tableName: 'assets' })
225
+ *
226
+ * @example
227
+ * // Range operators - converts to WHERE assets.price >= 100 AND assets.price <= 200
228
+ * applyFilter({ query, filter: { price: { gte: 100, lte: 200 } }, tableName: 'assets' })
229
+ *
230
+ * @example
231
+ * // Pattern matching (case-sensitive) - converts to WHERE assets.name LIKE '%invoice%'
232
+ * applyFilter({ query, filter: { name: { like: '%invoice%' } }, tableName: 'assets' })
233
+ *
234
+ * @example
235
+ * // Pattern matching (case-insensitive) - converts to WHERE assets.name ILIKE '%invoice%'
236
+ * applyFilter({ query, filter: { name: { ilike: '%invoice%' } }, tableName: 'assets' })
237
+ *
238
+ * @example
239
+ * // Null checks - converts to WHERE assets.deleted_at IS NULL
240
+ * applyFilter({ query, filter: { deletedAt: { isNull: true } }, tableName: 'assets' })
241
+ *
242
+ * @example
243
+ * // Multiple filters - converts to WHERE assets.status = 'active' AND assets.type = 'invoice' AND assets.price >= 100
244
+ * applyFilter({
245
+ * query,
246
+ * filter: {
247
+ * status: 'active',
248
+ * type: 'invoice',
249
+ * price: { gte: 100 }
250
+ * },
251
+ * tableName: 'assets'
252
+ * })
253
+ *
254
+ * @example
255
+ * // camelCase conversion - deletedAt converts to deleted_at
256
+ * applyFilter({ query, filter: { deletedAt: { isNull: true } }, tableName: 'assets' })
257
+ * // SQL: WHERE assets.deleted_at IS NULL
258
+ */
259
+ export function applyFilter({ query, filter, tableName }) {
260
+ const convertedFilter = toSnakeCase(filter)
261
+
262
+ return Object.entries(convertedFilter).reduce((q, [key, value]) => {
263
+ const qualifiedKey = `${tableName}.${key}`
264
+
265
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
266
+ return applyFilterObject(q, qualifiedKey, value)
267
+ }
268
+
269
+ if (Array.isArray(value)) {
270
+ return OPERATORS.in(q, qualifiedKey, value)
271
+ }
272
+
273
+ return OPERATORS.eq(q, qualifiedKey, value)
274
+ }, query)
275
+ }
@@ -1,4 +1,5 @@
1
1
  export * from './paginate.js'
2
+ export * from './apply-filter.js'
2
3
  export * from './connect-to-pg.js'
3
4
  export * from './validate-schema.js'
4
5
  export * from './start-stop-postgres-docker.js'
@@ -1,11 +1,11 @@
1
+ import { fileURLToPath } from 'url'
1
2
  import { describe, it, expect } from 'vitest'
3
+ import { dirname, join, extname } from 'path'
2
4
  import { readFileSync, readdirSync } from 'fs'
3
- import { join, extname } from 'path'
4
- import { fileURLToPath } from 'url'
5
- import { dirname } from 'path'
6
- import { mapMessageTelegram } from '../../src/instant-messages/message-unified-mapper.js'
7
5
 
6
+ import { writeFilesReceiveMapped } from '../resources/create-im-mapp-diff.js'
8
7
  import { getTelegramMessageType } from '../../src/instant-messages/message-type.js'
8
+ import { mapMessageTelegram } from '../../src/instant-messages/message-unified-mapper.js'
9
9
 
10
10
  import {
11
11
  MESSAGE_MEDIA_TYPE,
@@ -24,7 +24,6 @@ describe('Telegram unified message mapper – all mock samples', () => {
24
24
  if (files.length === 0) {
25
25
  throw new Error('No Telegram mock message files found in directory.')
26
26
  }
27
-
28
27
  for (const file of files) {
29
28
  const filePath = join(MOCK_DIR_TELEGRAM, file)
30
29
  const raw = JSON.parse(readFileSync(filePath, 'utf8'))
@@ -36,7 +35,7 @@ describe('Telegram unified message mapper – all mock samples', () => {
36
35
  const unifiedMessage = mapMessageTelegram({
37
36
  imMessage: raw,
38
37
  })
39
-
38
+ writeFilesReceiveMapped({ raw, prefix: 'telegram', unifiedMessage })
40
39
  expect(unifiedMessage).toBeTypeOf('object')
41
40
  expect(unifiedType).toBeTypeOf('string')
42
41
  expect(unifiedMessage.type).toBe(
@@ -7,7 +7,7 @@ import {
7
7
  getMessageType,
8
8
  mapMessageWhatsApp,
9
9
  } from '../../src/instant-messages/message-unified-mapper.js'
10
-
10
+ import { writeFilesReceiveMapped } from '../resources/create-im-mapp-diff.js'
11
11
  import {
12
12
  MESSAGE_MEDIA_TYPE,
13
13
  MESSAGE_MEDIA_TYPE_MAPPER,
@@ -38,7 +38,7 @@ describe('WhatsApp unified message mapper – all mock samples', () => {
38
38
  const unifiedMessage = mapMessageWhatsApp({
39
39
  imMessage: raw,
40
40
  })
41
-
41
+ writeFilesReceiveMapped({ raw, prefix: 'whatsapp', unifiedMessage })
42
42
  expect(unifiedMessage).toBeTypeOf('object')
43
43
  expect(unifiedType).toBeTypeOf('string')
44
44
 
@@ -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 { applyFilter } from '../../src/postgresql/apply-filter.js'
12
+
13
+ const PG_OPTIONS = {
14
+ port: 5444,
15
+ containerName: 'postgres-apply-filter-test',
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('applyFilter integration', () => {
169
+ it('applies simple equality filter (eq)', async () => {
170
+ const query = db('assets').select('*')
171
+ const filteredQuery = applyFilter({
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 = 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
+ })
196
+
197
+ it('applies not equal filter (ne)', async () => {
198
+ 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
+
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({
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 = applyFilter({
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 = 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({
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 = 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
+ })
406
+
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
+ })
434
+
435
+ it('applies multiple filters together', async () => {
436
+ const query = db('assets').select('*')
437
+ const filteredQuery = applyFilter({
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 = applyFilter({
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 = applyFilter({
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 = 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',
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
+ })
@@ -0,0 +1,15 @@
1
+ import { writeFileSync } from 'fs'
2
+ // @ts-ignore
3
+ export const writeFilesReceiveMapped = ({ prefix, raw, unifiedMessage }) => {
4
+ writeFileSync(
5
+ `./.exclude/${prefix}.${unifiedMessage.type}.json`,
6
+ JSON.stringify(
7
+ {
8
+ received: raw,
9
+ mapped: unifiedMessage,
10
+ },
11
+ null,
12
+ 2,
13
+ ),
14
+ )
15
+ }
@@ -4,6 +4,7 @@ export * from './otp-generators.js'
4
4
  export * from './sanitize-objects.js'
5
5
  export * from './normalize-min-max.js'
6
6
  export * from './normalize-to-array.js'
7
+ export * from './parse-json-if-string.js'
7
8
  export * from './combine-unique-arrays.js'
8
9
  export * from './normalize-phone-number.js'
9
10
  export * from './normalize-array-operators.js'
@@ -0,0 +1 @@
1
+ export function parseJsonIfString(value: any): any
@@ -1,22 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(npm test:*)",
5
- "Bash(docker rm:*)",
6
- "Bash(docker run:*)",
7
- "Bash(mongosh:*)",
8
- "Bash(for:*)",
9
- "Bash(do if mongosh --port 27099 --eval \"db.runCommand({ ping: 1 })\")",
10
- "Bash(then echo \"Connected after $i attempts\")",
11
- "Bash(break)",
12
- "Bash(fi)",
13
- "Bash(echo:*)",
14
- "Bash(done)",
15
- "Bash(docker logs:*)",
16
- "Bash(docker system:*)",
17
- "Bash(docker volume prune:*)",
18
- "Bash(docker image prune:*)",
19
- "Bash(docker builder prune:*)"
20
- ]
21
- }
22
- }