core-services-sdk 1.3.62 → 1.3.64

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.
@@ -0,0 +1,22 @@
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "core-services-sdk",
3
- "version": "1.3.62",
3
+ "version": "1.3.64",
4
4
  "main": "src/index.js",
5
5
  "type": "module",
6
6
  "types": "types/index.d.ts",
@@ -3,7 +3,46 @@
3
3
 
4
4
  import * as raw from 'google-libphonenumber'
5
5
 
6
- /** Resolve libphonenumber regardless of interop shape */
6
+ /**
7
+ * @typedef {Object} NormalizedPhone
8
+ * @property {string} e164
9
+ * @property {number} type
10
+ * @property {string} national
11
+ * @property {number} countryCode
12
+ * @property {string} nationalClean
13
+ * @property {string} international
14
+ * @property {string} countryCodeE164
15
+ * @property {string} internationalClean
16
+ * @property {string | undefined} regionCode
17
+ */
18
+
19
+ /**
20
+ * normalize-phone-number.js
21
+ *
22
+ * Utilities for parsing, validating, and normalizing phone numbers
23
+ * using google-libphonenumber.
24
+ *
25
+ * Supports both ESM and CJS interop builds.
26
+ * All normalization outputs are canonical and safe for persistence,
27
+ * comparison, indexing, and login identifiers.
28
+ */
29
+
30
+ /**
31
+ * Resolve google-libphonenumber exports regardless of ESM / CJS shape.
32
+ *
33
+ * Some builds expose:
34
+ * - raw.PhoneNumberUtil
35
+ * Others expose:
36
+ * - raw.default.PhoneNumberUtil
37
+ *
38
+ * This helper guarantees a consistent API.
39
+ *
40
+ * @returns {{
41
+ * PhoneNumberUtil: any,
42
+ * PhoneNumberFormat: any
43
+ * }}
44
+ * @throws {Error} If required exports are missing
45
+ */
7
46
  export function getLib() {
8
47
  // Prefer direct (CJS-style or ESM w/ named), else default
9
48
  // e.g. raw.PhoneNumberUtil OR raw.default.PhoneNumberUtil
@@ -19,6 +58,15 @@ export function getLib() {
19
58
 
20
59
  let _util // lazy singleton
21
60
 
61
+ /**
62
+ * Lazy singleton accessor for PhoneNumberUtil.
63
+ *
64
+ * Ensures:
65
+ * - Single instance per process
66
+ * - No eager initialization cost
67
+ *
68
+ * @returns {any} PhoneNumberUtil instance
69
+ */
22
70
  export function phoneUtil() {
23
71
  if (!_util) {
24
72
  const { PhoneNumberUtil } = getLib()
@@ -27,15 +75,25 @@ export function phoneUtil() {
27
75
  return _util
28
76
  }
29
77
 
78
+ /**
79
+ * Internal helper returning PhoneNumberFormat enum.
80
+ *
81
+ * @returns {any} PhoneNumberFormat enum
82
+ */
30
83
  function formats() {
31
84
  const { PhoneNumberFormat } = getLib()
32
85
  return PhoneNumberFormat
33
86
  }
34
87
 
35
88
  /**
36
- * Trim and remove invisible RTL markers.
37
- * @param {string} input
38
- * @returns {string}
89
+ * Cleans user input before parsing:
90
+ * - Trims whitespace
91
+ * - Removes invisible RTL/LTR markers
92
+ *
93
+ * Does NOT remove digits, plus sign, or formatting characters.
94
+ *
95
+ * @param {string} input Raw user input
96
+ * @returns {string} Cleaned input string
39
97
  */
40
98
  function clean(input) {
41
99
  return String(input)
@@ -44,27 +102,69 @@ function clean(input) {
44
102
  }
45
103
 
46
104
  /**
47
- * Convert a parsed libphonenumber object into a normalized result.
105
+ * Converts a parsed google-libphonenumber object into a normalized result.
106
+ *
107
+ * Returned formats:
108
+ * - e164: Canonical phone number in E.164 format.
109
+ * Intended for storage, indexing, comparison, login identifiers, and OTP flows.
110
+ *
111
+ * - national: Local, human-readable phone number representation.
112
+ * May include formatting characters such as dashes or spaces.
113
+ *
114
+ * - nationalClean: Local phone number containing digits only.
115
+ *
116
+ * - international: Human-readable international representation.
117
+ *
118
+ * - internationalClean: International phone number containing digits only,
119
+ * without '+' or formatting characters.
120
+ *
121
+ * - regionCode: ISO 3166-1 alpha-2 region code (e.g. "IL").
122
+ *
123
+ * - countryCallingCode: Numeric international dialing code (e.g. 972).
124
+ *
125
+ * - countryCallingCodeE164: International dialing code with '+' prefix (e.g. "+972").
126
+ *
127
+ * Notes:
128
+ * - Only `e164` should be persisted or used for identity comparison.
129
+ * - All other formats are intended strictly for UI, display, copy, or integrations.
130
+ *
48
131
  * @param {any} parsed
49
- * @returns {{e164:string,national:string,international:string,regionCode:string|undefined,type:number}}
132
+ * Parsed phone number object returned by google-libphonenumber.
133
+ *
134
+ * @returns {NormalizedPhone}
50
135
  */
136
+
51
137
  function toResult(parsed) {
52
138
  const PNF = formats()
53
139
  const util = phoneUtil()
54
- return {
140
+
141
+ const results = {
142
+ type: util.getNumberType(parsed),
55
143
  e164: util.format(parsed, PNF.E164),
56
144
  national: util.format(parsed, PNF.NATIONAL),
57
- international: util.format(parsed, PNF.INTERNATIONAL),
58
145
  regionCode: util.getRegionCodeForNumber(parsed),
59
- type: util.getNumberType(parsed),
146
+ international: util.format(parsed, PNF.INTERNATIONAL),
147
+ }
148
+
149
+ const countryCode = `${parsed.getCountryCode()}`
150
+
151
+ return {
152
+ ...results,
153
+ countryCode,
154
+ countryCodeE164: `+${countryCode}`,
155
+ nationalClean: results.national.replace(/\D/g, ''),
156
+ internationalClean: results.e164.replace(/\D/g, ''),
60
157
  }
61
158
  }
62
159
 
63
160
  /**
64
- * Parse & validate an international number (must start with '+').
65
- * @param {string} input
66
- * @returns {{e164:string,national:string,international:string,regionCode:string|undefined,type:number}}
67
- * @throws {Error} If the number is invalid
161
+ * Normalize and validate an international phone number.
162
+ *
163
+ * Input MUST start with '+'.
164
+ *
165
+ * @param {string} input International phone number (E.164-like)
166
+ * @returns {NormalizedPhone}
167
+ * @throws {Error} If the phone number is invalid
68
168
  */
69
169
  export function normalizePhoneOrThrowIntl(input) {
70
170
  try {
@@ -82,11 +182,22 @@ export function normalizePhoneOrThrowIntl(input) {
82
182
  }
83
183
 
84
184
  /**
85
- * Parse & validate a national number using a region hint.
86
- * @param {string} input
87
- * @param {string} defaultRegion
88
- * @returns {{e164:string,national:string,international:string,regionCode:string|undefined,type:number}}
89
- * @throws {Error} If the number is invalid
185
+ * Normalize and validate a national phone number using a region hint.
186
+ *
187
+ * Example:
188
+ * input: "0523444444"
189
+ * defaultRegion: "IL"
190
+ *
191
+ * @param {string} input National phone number
192
+ * @param {string} defaultRegion ISO 3166-1 alpha-2 country code
193
+ * @returns {{
194
+ * e164: string,
195
+ * national: string,
196
+ * international: string,
197
+ * regionCode: string | undefined,
198
+ * type: number
199
+ * }}
200
+ * @throws {Error} If the phone number is invalid
90
201
  */
91
202
  export function normalizePhoneOrThrowWithRegion(input, defaultRegion) {
92
203
  try {
@@ -104,13 +215,21 @@ export function normalizePhoneOrThrowWithRegion(input, defaultRegion) {
104
215
  }
105
216
 
106
217
  /**
107
- * Smart normalization:
108
- * - If input starts with '+', parse as international.
109
- * - Otherwise require a defaultRegion and parse as national.
110
- * @param {string} input
111
- * @param {{ defaultRegion?: string }} [opts]
112
- * @returns {{e164:string,national:string,international:string,regionCode:string|undefined,type:number}}
113
- * @throws {Error} If invalid or defaultRegion is missing for non-international input
218
+ * Smart normalization entry point.
219
+ *
220
+ * Behavior:
221
+ * - If input starts with '+', parses as international
222
+ * - Otherwise requires defaultRegion and parses as national
223
+ *
224
+ * This is the recommended function for login, signup, and verification flows.
225
+ *
226
+ * @param {string} input Phone number (international or national)
227
+ * @param {{
228
+ * defaultRegion?: string
229
+ * }} [opts]
230
+ *
231
+ * @returns {NormalizedPhone}
232
+ * @throws {Error} If invalid or defaultRegion is missing
114
233
  */
115
234
  export function normalizePhoneOrThrow(input, opts = {}) {
116
235
  const cleaned = clean(input)
@@ -1,11 +1,24 @@
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
+
1
11
  import { toSnakeCase } from '../core/case-mapper.js'
2
12
 
3
13
  /**
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
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
9
22
  */
10
23
 
11
24
  /**
@@ -165,9 +178,10 @@ const applyFilterObject = (q, qualifiedKey, value) => {
165
178
  * Builds a Knex query with MongoDB-style filter operators.
166
179
  * Pure utility function that can be used across repositories.
167
180
  *
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.
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.
171
185
  *
172
186
  * **Supported Operators:**
173
187
  * - `eq` - Equality (default for simple values)
@@ -184,15 +198,19 @@ const applyFilterObject = (q, qualifiedKey, value) => {
184
198
  * - `isNotNull` - Check if field is NOT NULL
185
199
  *
186
200
  * **Key Features:**
187
- * - Automatic camelCase to snake_case conversion for filter keys
201
+ * - Filter keys are used as-is (no automatic case conversion)
188
202
  * - Qualified column names (table.column format)
189
203
  * - Multiple operators can be applied to the same field
190
204
  * - Unknown operators are silently ignored
191
205
  * - Arrays are automatically converted to IN clauses
192
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
+ *
193
211
  * @param {Object} params - Function parameters
194
212
  * @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)
213
+ * @param {Object<string, *>} params.filter - Filter object with keys matching database column names (used as-is)
196
214
  * Filter values can be:
197
215
  * - Simple values (string, number, boolean) → treated as equality
198
216
  * - Arrays → treated as IN operator
@@ -252,14 +270,13 @@ const applyFilterObject = (q, qualifiedKey, value) => {
252
270
  * })
253
271
  *
254
272
  * @example
255
- * // camelCase conversion - deletedAt converts to deleted_at
256
- * applyFilter({ query, filter: { deletedAt: { isNull: true } }, tableName: 'assets' })
273
+ * // Filter keys are used as-is - ensure they match your database column names
274
+ * applyFilter({ query, filter: { deleted_at: { isNull: true } }, tableName: 'assets' })
257
275
  * // SQL: WHERE assets.deleted_at IS NULL
276
+ * // Note: If you need camelCase conversion, use applyFilterSnakeCase instead
258
277
  */
259
278
  export function applyFilter({ query, filter, tableName }) {
260
- const convertedFilter = toSnakeCase(filter)
261
-
262
- return Object.entries(convertedFilter).reduce((q, [key, value]) => {
279
+ return Object.entries(filter).reduce((q, [key, value]) => {
263
280
  const qualifiedKey = `${tableName}.${key}`
264
281
 
265
282
  if (value && typeof value === 'object' && !Array.isArray(value)) {
@@ -273,3 +290,112 @@ export function applyFilter({ query, filter, tableName }) {
273
290
  return OPERATORS.eq(q, qualifiedKey, value)
274
291
  }, query)
275
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
+ }
@@ -58,6 +58,51 @@ describe('phone normalization', () => {
58
58
  expect(out.regionCode).toBe('IL')
59
59
  })
60
60
 
61
+ describe('normalizePhoneOrThrow - international input', () => {
62
+ it('returns full normalized data for international input without defaultRegion', () => {
63
+ const out = normalizePhoneOrThrow('+972523443413')
64
+
65
+ // canonical
66
+ expect(out.e164).toBe('+972523443413')
67
+ // clean formats
68
+
69
+ expect(out.internationalClean).toBe('972523443413')
70
+ expect(out.nationalClean).toBe('0523443413')
71
+
72
+ // formatted (do not assert exact formatting)
73
+ expect(typeof out.national).toBe('string')
74
+ expect(typeof out.international).toBe('string')
75
+
76
+ // metadata
77
+ expect(out.regionCode).toBe('IL')
78
+ expect(out.countryCode).toBe('972')
79
+ expect(out.countryCodeE164).toBe('+972')
80
+ expect(typeof out.type).toBe('number')
81
+ })
82
+
83
+ it('returns identical normalized data when defaultRegion is provided', () => {
84
+ const out = normalizePhoneOrThrow('+972523443413', {
85
+ defaultRegion: 'IL',
86
+ })
87
+
88
+ // canonical
89
+ expect(out.e164).toBe('+972523443413')
90
+
91
+ // clean formats
92
+ expect(out.internationalClean).toBe('972523443413')
93
+ expect(out.nationalClean).toBe('0523443413')
94
+
95
+ // formatted (do not assert exact formatting)
96
+ expect(typeof out.national).toBe('string')
97
+ expect(typeof out.international).toBe('string')
98
+
99
+ // metadata
100
+ expect(out.regionCode).toBe('IL')
101
+ expect(out.countryCode).toBe('972')
102
+ expect(out.countryCodeE164).toBe('+972')
103
+ expect(typeof out.type).toBe('number')
104
+ })
105
+ })
61
106
  it('normalizePhoneOrThrow (smart): throws if national with no defaultRegion', () => {
62
107
  expect(() => normalizePhoneOrThrow('054-123-4567')).toThrow(
63
108
  /defaultRegion is required/i,