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.
- package/.claude/settings.local.json +22 -0
- package/package.json +1 -1
- package/src/core/normalize-phone-number.js +144 -25
- package/src/postgresql/apply-filter.js +141 -15
- package/tests/core/normalize-phone-number.unit.test.js +45 -0
- package/tests/postgresql/apply-filter-snake-case.integration.test.js +549 -0
- package/tests/postgresql/apply-filter.integration.test.js +56 -325
- package/tests/postgresql/paginate.integration.test.js +2 -2
- package/tests/postgresql/validate-schema.integration.test.js +2 -2
- package/types/core/normalize-phone-number.d.ts +97 -27
- package/types/postgresql/apply-filter.d.ts +231 -0
- package/types/postgresql/index.d.ts +1 -0
|
@@ -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
|
@@ -3,7 +3,46 @@
|
|
|
3
3
|
|
|
4
4
|
import * as raw from 'google-libphonenumber'
|
|
5
5
|
|
|
6
|
-
/**
|
|
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
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
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
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
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
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* @
|
|
8
|
-
* @
|
|
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
|
|
169
|
-
*
|
|
170
|
-
*
|
|
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
|
-
* -
|
|
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
|
|
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
|
-
* //
|
|
256
|
-
* applyFilter({ query, filter: {
|
|
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
|
-
|
|
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,
|