core-services-sdk 1.3.64 → 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 (32) hide show
  1. package/package.json +1 -1
  2. package/src/postgresql/core/get-table-name.js +10 -0
  3. package/src/postgresql/filters/apply-filter-object.js +19 -0
  4. package/src/postgresql/filters/apply-filter-snake-case.js +16 -0
  5. package/src/postgresql/filters/apply-filter.js +33 -0
  6. package/src/postgresql/filters/operators.js +32 -0
  7. package/src/postgresql/index.js +5 -2
  8. package/src/postgresql/modifiers/apply-order-by.js +19 -0
  9. package/src/postgresql/modifiers/apply-pagination.js +12 -0
  10. package/src/postgresql/pagination/paginate.js +48 -0
  11. package/tests/core/normalize-phone-number.unit.test.js +1 -1
  12. package/tests/postgresql/apply-filter-snake-case.integration.test.js +25 -354
  13. package/tests/postgresql/apply-filter.integration.test.js +28 -78
  14. package/tests/postgresql/core/get-table-name.unit.test.js +20 -0
  15. package/tests/postgresql/filters/apply-filter-object.test.js +23 -0
  16. package/tests/postgresql/filters/operators.unit.test.js +23 -0
  17. package/tests/postgresql/modifiers/apply-order-by.test.js +80 -0
  18. package/tests/postgresql/modifiers/apply-pagination.unit.test.js +18 -0
  19. package/tests/postgresql/paginate.integration.test.js +8 -16
  20. package/tests/postgresql/pagination/paginate.js +48 -0
  21. package/tests/postgresql/validate-schema.integration.test.js +7 -3
  22. package/types/postgresql/core/get-table-name.d.ts +10 -0
  23. package/types/postgresql/filters/apply-filter-object.d.ts +15 -0
  24. package/types/postgresql/filters/apply-filter-snake-case.d.ts +14 -0
  25. package/types/postgresql/filters/apply-filter.d.ts +15 -0
  26. package/types/postgresql/filters/operators.d.ts +14 -0
  27. package/types/postgresql/index.d.ts +5 -2
  28. package/types/postgresql/modifiers/apply-order-by.d.ts +17 -0
  29. package/types/postgresql/modifiers/apply-pagination.d.ts +17 -0
  30. package/types/postgresql/pagination/paginate.d.ts +29 -0
  31. package/src/postgresql/apply-filter.js +0 -401
  32. package/src/postgresql/paginate.js +0 -61
@@ -1,401 +0,0 @@
1
- /**
2
- * @fileoverview Provides utilities for applying MongoDB-style filter operators to Knex query builders.
3
- *
4
- * This module contains functions to convert filter objects with various operators (equality,
5
- * comparison, pattern matching, null checks, etc.) into SQL WHERE clauses using Knex.
6
- * Supports automatic camelCase to snake_case conversion for database column names.
7
- *
8
- * @module postgresql/apply-filter
9
- */
10
-
11
- import { toSnakeCase } from '../core/case-mapper.js'
12
-
13
- /**
14
- * Type definition for filter operator functions.
15
- * Each operator function applies a WHERE condition to a Knex query builder.
16
- *
17
- * @typedef {Function} OperatorFunction
18
- * @param {import('knex').Knex.QueryBuilder} q - Knex query builder instance
19
- * @param {string} key - Column name (qualified with table name, e.g., "table.column")
20
- * @param {*} value - Value to compare against (type depends on operator)
21
- * @returns {import('knex').Knex.QueryBuilder} Modified query builder with WHERE clause applied
22
- */
23
-
24
- /**
25
- * Map of filter operators to their corresponding Knex query builder methods.
26
- * Each operator function applies a WHERE condition to the query.
27
- *
28
- * @type {Object<string, OperatorFunction>}
29
- * @private
30
- */
31
- const OPERATORS = {
32
- /**
33
- * Array membership operator. Checks if column value is in the provided array.
34
- * @param {import('knex').Knex.QueryBuilder} q - Query builder
35
- * @param {string} key - Column name
36
- * @param {Array<*>} value - Array of values to match
37
- * @returns {import('knex').Knex.QueryBuilder} Query with WHERE IN clause
38
- */
39
- in: (q, key, value) => q.whereIn(key, value),
40
-
41
- /**
42
- * Not in array operator. Checks if column value is NOT in the provided array.
43
- * @param {import('knex').Knex.QueryBuilder} q - Query builder
44
- * @param {string} key - Column name
45
- * @param {Array<*>} value - Array of values to exclude
46
- * @returns {import('knex').Knex.QueryBuilder} Query with WHERE NOT IN clause
47
- */
48
- nin: (q, key, value) => q.whereNotIn(key, value),
49
-
50
- /**
51
- * Equality operator. Checks if column value equals the provided value.
52
- * @param {import('knex').Knex.QueryBuilder} q - Query builder
53
- * @param {string} key - Column name
54
- * @param {*} value - Value to match
55
- * @returns {import('knex').Knex.QueryBuilder} Query with WHERE = clause
56
- */
57
- eq: (q, key, value) => q.where(key, '=', value),
58
-
59
- /**
60
- * Not equal operator. Checks if column value does not equal the provided value.
61
- * @param {import('knex').Knex.QueryBuilder} q - Query builder
62
- * @param {string} key - Column name
63
- * @param {*} value - Value to exclude
64
- * @returns {import('knex').Knex.QueryBuilder} Query with WHERE != clause
65
- */
66
- ne: (q, key, value) => q.where(key, '!=', value),
67
-
68
- /**
69
- * Not equal operator (alias for `ne`). Checks if column value does not equal the provided value.
70
- * @param {import('knex').Knex.QueryBuilder} q - Query builder
71
- * @param {string} key - Column name
72
- * @param {*} value - Value to exclude
73
- * @returns {import('knex').Knex.QueryBuilder} Query with WHERE != clause
74
- */
75
- neq: (q, key, value) => q.where(key, '!=', value),
76
-
77
- /**
78
- * Greater than operator. Checks if column value is greater than the provided value.
79
- * @param {import('knex').Knex.QueryBuilder} q - Query builder
80
- * @param {string} key - Column name
81
- * @param {number|string|Date} value - Value to compare against
82
- * @returns {import('knex').Knex.QueryBuilder} Query with WHERE > clause
83
- */
84
- gt: (q, key, value) => q.where(key, '>', value),
85
-
86
- /**
87
- * Greater than or equal operator. Checks if column value is greater than or equal to the provided value.
88
- * @param {import('knex').Knex.QueryBuilder} q - Query builder
89
- * @param {string} key - Column name
90
- * @param {number|string|Date} value - Value to compare against
91
- * @returns {import('knex').Knex.QueryBuilder} Query with WHERE >= clause
92
- */
93
- gte: (q, key, value) => q.where(key, '>=', value),
94
-
95
- /**
96
- * Less than operator. Checks if column value is less than the provided value.
97
- * @param {import('knex').Knex.QueryBuilder} q - Query builder
98
- * @param {string} key - Column name
99
- * @param {number|string|Date} value - Value to compare against
100
- * @returns {import('knex').Knex.QueryBuilder} Query with WHERE < clause
101
- */
102
- lt: (q, key, value) => q.where(key, '<', value),
103
-
104
- /**
105
- * Less than or equal operator. Checks if column value is less than or equal to the provided value.
106
- * @param {import('knex').Knex.QueryBuilder} q - Query builder
107
- * @param {string} key - Column name
108
- * @param {number|string|Date} value - Value to compare against
109
- * @returns {import('knex').Knex.QueryBuilder} Query with WHERE <= clause
110
- */
111
- lte: (q, key, value) => q.where(key, '<=', value),
112
-
113
- /**
114
- * Case-sensitive pattern matching operator. Uses SQL LIKE for pattern matching.
115
- * Supports wildcards: `%` (any sequence) and `_` (single character).
116
- * @param {import('knex').Knex.QueryBuilder} q - Query builder
117
- * @param {string} key - Column name
118
- * @param {string} value - Pattern to match (e.g., '%invoice%')
119
- * @returns {import('knex').Knex.QueryBuilder} Query with WHERE LIKE clause
120
- */
121
- like: (q, key, value) => q.where(key, 'like', value),
122
-
123
- /**
124
- * Case-insensitive pattern matching operator. Uses PostgreSQL ILIKE for pattern matching.
125
- * Supports wildcards: `%` (any sequence) and `_` (single character).
126
- * @param {import('knex').Knex.QueryBuilder} q - Query builder
127
- * @param {string} key - Column name
128
- * @param {string} value - Pattern to match (e.g., '%invoice%')
129
- * @returns {import('knex').Knex.QueryBuilder} Query with WHERE ILIKE clause
130
- */
131
- ilike: (q, key, value) => q.where(key, 'ilike', value),
132
-
133
- /**
134
- * Null check operator. Checks if column value is NULL or NOT NULL based on the boolean value.
135
- * @param {import('knex').Knex.QueryBuilder} q - Query builder
136
- * @param {string} key - Column name
137
- * @param {boolean} value - If true, checks for NULL; if false, checks for NOT NULL
138
- * @returns {import('knex').Knex.QueryBuilder} Query with WHERE NULL or WHERE NOT NULL clause
139
- */
140
- isNull: (q, key, value) => (value ? q.whereNull(key) : q.whereNotNull(key)),
141
-
142
- /**
143
- * Not null check operator. Checks if column value is NOT NULL or NULL based on the boolean value.
144
- * @param {import('knex').Knex.QueryBuilder} q - Query builder
145
- * @param {string} key - Column name
146
- * @param {boolean} value - If true, checks for NOT NULL; if false, checks for NULL
147
- * @returns {import('knex').Knex.QueryBuilder} Query with WHERE NOT NULL or WHERE NULL clause
148
- */
149
- isNotNull: (q, key, value) =>
150
- value ? q.whereNotNull(key) : q.whereNull(key),
151
- }
152
-
153
- /**
154
- * Applies filter operators from an object to a Knex query.
155
- * Processes each operator in the value object and applies the corresponding WHERE clause.
156
- * Unknown operators are silently ignored.
157
- *
158
- * @param {import('knex').Knex.QueryBuilder} q - Knex query builder instance
159
- * @param {string} qualifiedKey - Fully qualified column name (e.g., "table.column")
160
- * @param {Object<string, *>} value - Object containing operator-value pairs
161
- * @returns {import('knex').Knex.QueryBuilder} Modified query builder with applied filters
162
- * @private
163
- *
164
- * @example
165
- * // Apply multiple operators on the same field
166
- * applyFilterObject(query, 'assets.price', { gte: 100, lte: 200 })
167
- * // Results in: WHERE assets.price >= 100 AND assets.price <= 200
168
- */
169
- const applyFilterObject = (q, qualifiedKey, value) => {
170
- return Object.entries(value).reduce(
171
- (q, [operator, val]) =>
172
- OPERATORS[operator] ? OPERATORS[operator](q, qualifiedKey, val) : q,
173
- q,
174
- )
175
- }
176
-
177
- /**
178
- * Builds a Knex query with MongoDB-style filter operators.
179
- * Pure utility function that can be used across repositories.
180
- *
181
- * This function applies various filter operators to build SQL WHERE clauses. All column names
182
- * are automatically qualified with the provided table name. Filter keys are used as-is without
183
- * any case conversion. If you need automatic camelCase to snake_case conversion, use
184
- * {@link applyFilterSnakeCase} instead.
185
- *
186
- * **Supported Operators:**
187
- * - `eq` - Equality (default for simple values)
188
- * - `ne` / `neq` - Not equal
189
- * - `in` - Array membership (or pass array directly)
190
- * - `nin` - Not in array
191
- * - `gt` - Greater than
192
- * - `gte` - Greater than or equal
193
- * - `lt` - Less than
194
- * - `lte` - Less than or equal
195
- * - `like` - Case-sensitive pattern matching (SQL LIKE)
196
- * - `ilike` - Case-insensitive pattern matching (PostgreSQL ILIKE)
197
- * - `isNull` - Check if field is NULL
198
- * - `isNotNull` - Check if field is NOT NULL
199
- *
200
- * **Key Features:**
201
- * - Filter keys are used as-is (no automatic case conversion)
202
- * - Qualified column names (table.column format)
203
- * - Multiple operators can be applied to the same field
204
- * - Unknown operators are silently ignored
205
- * - Arrays are automatically converted to IN clauses
206
- *
207
- * **Note:** This function does NOT convert filter keys. If your database uses snake_case columns
208
- * but your filter keys are in camelCase, use {@link applyFilterSnakeCase} instead, or ensure
209
- * your filter keys already match your database column names.
210
- *
211
- * @param {Object} params - Function parameters
212
- * @param {import('knex').Knex.QueryBuilder} params.query - Knex query builder instance to apply filters to
213
- * @param {Object<string, *>} params.filter - Filter object with keys matching database column names (used as-is)
214
- * Filter values can be:
215
- * - Simple values (string, number, boolean) → treated as equality
216
- * - Arrays → treated as IN operator
217
- * - Objects with operator keys → apply specific operators
218
- * @param {string} params.tableName - Table name used to qualify column names (e.g., "assets" → "assets.column_name")
219
- * @returns {import('knex').Knex.QueryBuilder} Modified query builder with applied WHERE clauses
220
- *
221
- * @throws {TypeError} If query is not a valid Knex QueryBuilder instance
222
- *
223
- * @example
224
- * // Simple equality - converts to WHERE assets.status = 'active'
225
- * const query = db('assets').select('*')
226
- * applyFilter({ query, filter: { status: 'active' }, tableName: 'assets' })
227
- *
228
- * @example
229
- * // Not equal - converts to WHERE assets.status != 'deleted'
230
- * applyFilter({ query, filter: { status: { ne: 'deleted' } }, tableName: 'assets' })
231
- *
232
- * @example
233
- * // Array/IN operator - converts to WHERE assets.status IN ('active', 'pending')
234
- * applyFilter({ query, filter: { status: ['active', 'pending'] }, tableName: 'assets' })
235
- *
236
- * @example
237
- * // Explicit IN operator
238
- * applyFilter({ query, filter: { status: { in: ['active', 'pending'] } }, tableName: 'assets' })
239
- *
240
- * @example
241
- * // Not in array - converts to WHERE assets.status NOT IN ('deleted', 'archived')
242
- * applyFilter({ query, filter: { status: { nin: ['deleted', 'archived'] } }, tableName: 'assets' })
243
- *
244
- * @example
245
- * // Range operators - converts to WHERE assets.price >= 100 AND assets.price <= 200
246
- * applyFilter({ query, filter: { price: { gte: 100, lte: 200 } }, tableName: 'assets' })
247
- *
248
- * @example
249
- * // Pattern matching (case-sensitive) - converts to WHERE assets.name LIKE '%invoice%'
250
- * applyFilter({ query, filter: { name: { like: '%invoice%' } }, tableName: 'assets' })
251
- *
252
- * @example
253
- * // Pattern matching (case-insensitive) - converts to WHERE assets.name ILIKE '%invoice%'
254
- * applyFilter({ query, filter: { name: { ilike: '%invoice%' } }, tableName: 'assets' })
255
- *
256
- * @example
257
- * // Null checks - converts to WHERE assets.deleted_at IS NULL
258
- * applyFilter({ query, filter: { deletedAt: { isNull: true } }, tableName: 'assets' })
259
- *
260
- * @example
261
- * // Multiple filters - converts to WHERE assets.status = 'active' AND assets.type = 'invoice' AND assets.price >= 100
262
- * applyFilter({
263
- * query,
264
- * filter: {
265
- * status: 'active',
266
- * type: 'invoice',
267
- * price: { gte: 100 }
268
- * },
269
- * tableName: 'assets'
270
- * })
271
- *
272
- * @example
273
- * // Filter keys are used as-is - ensure they match your database column names
274
- * applyFilter({ query, filter: { deleted_at: { isNull: true } }, tableName: 'assets' })
275
- * // SQL: WHERE assets.deleted_at IS NULL
276
- * // Note: If you need camelCase conversion, use applyFilterSnakeCase instead
277
- */
278
- export function applyFilter({ query, filter, tableName }) {
279
- return Object.entries(filter).reduce((q, [key, value]) => {
280
- const qualifiedKey = `${tableName}.${key}`
281
-
282
- if (value && typeof value === 'object' && !Array.isArray(value)) {
283
- return applyFilterObject(q, qualifiedKey, value)
284
- }
285
-
286
- if (Array.isArray(value)) {
287
- return OPERATORS.in(q, qualifiedKey, value)
288
- }
289
-
290
- return OPERATORS.eq(q, qualifiedKey, value)
291
- }, query)
292
- }
293
-
294
- /**
295
- * Applies MongoDB-style filter operators to a Knex query with automatic camelCase to snake_case conversion.
296
- *
297
- * This function is a convenience wrapper around {@link applyFilter} that automatically converts
298
- * all filter keys from camelCase to snake_case before applying the filters. This is useful when
299
- * your application code uses camelCase naming conventions but your database columns use snake_case.
300
- *
301
- * The function first converts all filter object keys using `toSnakeCase`, then applies the filters
302
- * using the standard `applyFilter` function. This ensures that filter keys like `userId` are
303
- * converted to `user_id` before being used in SQL queries.
304
- *
305
- * **Key Features:**
306
- * - Automatic camelCase to snake_case key conversion
307
- * - Same operator support as `applyFilter`
308
- * - Qualified column names (table.column format)
309
- * - Multiple operators can be applied to the same field
310
- * - Arrays are automatically converted to IN clauses
311
- *
312
- * **Supported Operators:**
313
- * Same as {@link applyFilter}:
314
- * - `eq` - Equality (default for simple values)
315
- * - `ne` / `neq` - Not equal
316
- * - `in` - Array membership (or pass array directly)
317
- * - `nin` - Not in array
318
- * - `gt` - Greater than
319
- * - `gte` - Greater than or equal
320
- * - `lt` - Less than
321
- * - `lte` - Less than or equal
322
- * - `like` - Case-sensitive pattern matching (SQL LIKE)
323
- * - `ilike` - Case-insensitive pattern matching (PostgreSQL ILIKE)
324
- * - `isNull` - Check if field is NULL
325
- * - `isNotNull` - Check if field is NOT NULL
326
- *
327
- * @param {Object} params - Function parameters
328
- * @param {import('knex').Knex.QueryBuilder} params.query - Knex query builder instance to apply filters to
329
- * @param {Object<string, *>} params.filter - Filter object with camelCase keys (will be converted to snake_case)
330
- * Filter values can be:
331
- * - Simple values (string, number, boolean) → treated as equality
332
- * - Arrays → treated as IN operator
333
- * - Objects with operator keys → apply specific operators
334
- * @param {string} params.tableName - Table name used to qualify column names (e.g., "assets" → "assets.column_name")
335
- * @returns {import('knex').Knex.QueryBuilder} Modified query builder with applied WHERE clauses (using snake_case column names)
336
- *
337
- * @throws {TypeError} If query is not a valid Knex QueryBuilder instance
338
- *
339
- * @example
340
- * // Simple equality with camelCase key - converts to WHERE assets.user_id = 1
341
- * const query = db('assets').select('*')
342
- * applyFilterSnakeCase({ query, filter: { userId: 1 }, tableName: 'assets' })
343
- * // SQL: WHERE assets.user_id = 1
344
- *
345
- * @example
346
- * // Not equal with camelCase key - converts to WHERE assets.status != 'deleted'
347
- * applyFilterSnakeCase({
348
- * query,
349
- * filter: { status: { ne: 'deleted' } },
350
- * tableName: 'assets'
351
- * })
352
- * // SQL: WHERE assets.status != 'deleted'
353
- *
354
- * @example
355
- * // Array/IN operator with camelCase key - converts to WHERE assets.status IN ('active', 'pending')
356
- * applyFilterSnakeCase({
357
- * query,
358
- * filter: { status: ['active', 'pending'] },
359
- * tableName: 'assets'
360
- * })
361
- * // SQL: WHERE assets.status IN ('active', 'pending')
362
- *
363
- * @example
364
- * // Range operators with camelCase keys - converts to WHERE assets.price >= 100 AND assets.price <= 200
365
- * applyFilterSnakeCase({
366
- * query,
367
- * filter: { price: { gte: 100, lte: 200 } },
368
- * tableName: 'assets'
369
- * })
370
- * // SQL: WHERE assets.price >= 100 AND assets.price <= 200
371
- *
372
- * @example
373
- * // Null checks with camelCase key - converts to WHERE assets.deleted_at IS NULL
374
- * applyFilterSnakeCase({
375
- * query,
376
- * filter: { deletedAt: { isNull: true } },
377
- * tableName: 'assets'
378
- * })
379
- * // SQL: WHERE assets.deleted_at IS NULL
380
- *
381
- * @example
382
- * // Multiple filters with camelCase keys
383
- * applyFilterSnakeCase({
384
- * query,
385
- * filter: {
386
- * userId: 123, // Converts to user_id
387
- * createdAt: { gte: '2024-01-01' }, // Converts to created_at
388
- * status: 'active'
389
- * },
390
- * tableName: 'assets'
391
- * })
392
- * // SQL: WHERE assets.user_id = 123 AND assets.created_at >= '2024-01-01' AND assets.status = 'active'
393
- *
394
- * @see {@link applyFilter} For the base function without key conversion
395
- * @see {@link toSnakeCase} For details on the key conversion process
396
- */
397
- export function applyFilterSnakeCase({ query, filter, tableName }) {
398
- const convertedFilter = toSnakeCase(filter)
399
-
400
- return applyFilter({ query, filter: convertedFilter, tableName })
401
- }
@@ -1,61 +0,0 @@
1
- import { toSnakeCase } from '../core/case-mapper.js'
2
- import { normalizeNumberOrDefault } from '../core/normalize-premitives-types-or-default.js'
3
-
4
- /**
5
- * Generic pagination utility.
6
- *
7
- * @async
8
- * @param {Object} params
9
- * @param {Object} params.db
10
- * @param {string} params.tableName
11
- * @param {number} [params.page=1]
12
- * @param {number} [params.limit=10]
13
- * @param {Object} [params.filter={}]
14
- * @param {Object} [params.orderBy]
15
- * @param {string} params.orderBy.column
16
- * @param {'asc'|'desc'} [params.orderBy.direction='asc']
17
- * @returns {Promise<{
18
- * list: any[],
19
- * totalCount: number,
20
- * totalPages: number,
21
- * currentPage: number,
22
- * hasNext: boolean,
23
- * hasPrevious: boolean
24
- * }>}
25
- */
26
-
27
- export const sqlPaginate = async ({
28
- db,
29
- mapRow,
30
- orderBy,
31
- page = 1,
32
- limit = 10,
33
- tableName,
34
- filter = {},
35
- } = {}) => {
36
- const offset = (page - 1) * limit
37
-
38
- const query = orderBy?.column
39
- ? db(tableName)
40
- .select('*')
41
- .where(toSnakeCase(filter))
42
- .orderBy(orderBy.column, orderBy.direction || 'asc')
43
- : db(tableName).select('*').where(toSnakeCase(filter))
44
-
45
- const [rows, countResult] = await Promise.all([
46
- query.limit(limit).offset(offset),
47
- db(tableName).where(toSnakeCase(filter)).count('* as count').first(),
48
- ])
49
-
50
- const totalCount = normalizeNumberOrDefault(countResult?.count || 0)
51
- const totalPages = Math.ceil(totalCount / limit)
52
-
53
- return {
54
- list: mapRow ? rows.map(mapRow) : rows,
55
- totalCount,
56
- totalPages,
57
- currentPage: page,
58
- hasPrevious: page > 1,
59
- hasNext: page < totalPages,
60
- }
61
- }