Skip to content

Commit 6548d68

Browse files
authored
feat: Normalise phoneNumber received in the input before processing + Migration script (#885)
* feat: Normalise phoneNumber received in the input before processing (#881) * feat: normalise phoneNumber received in the input before processing * feat: PR changes * feat: PR changes * feat: Improve test for invalid phone number inputs (#884) * feat: Add migration script for phone number normalisation (#882) * feat: Add migration script for phone number normalisation * feat: PR changes * feat: PR changes * feat: PR changes * feat: PR changes * feat: PR changes * feat: PR changes
1 parent b0fa0dd commit 6548d68

File tree

20 files changed

+1336
-20
lines changed

20 files changed

+1336
-20
lines changed

CHANGELOG.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,30 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres
66
to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [7.0.12] - 2023-11-16
9+
10+
In this release, the core API routes have been updated to incorporate phone number normalization before processing. Consequently, existing entries in the database also need to undergo normalization. To facilitate this, we have included a migration script to normalize phone numbers for all the existing entries.
11+
12+
**NOTE**: You can skip the migration if you are not using passwordless via phone number.
13+
14+
### Migration steps
15+
16+
This script updates the `phone_number` column in the `passwordless_users`, `passwordless_user_to_tenant`, and `passwordless_devices` tables with their respective normalized values. This script is idempotent and can be run multiple times without any issue. Follow the steps below to run the script:
17+
18+
1. Ensure that the core is already upgraded to version 7.0.12 (CDI version 4.0)
19+
2. Run the migration script
20+
21+
Make sure your Node.js version is 16 or above to run the script. Locate the migration script at `supertokens-core/migration_scripts/to_version_7_0_12/index.js`. Modify the script by updating the `DB_HOST`, `DB_USER`, `DB_PASSWORD`, and `DB_NAME` variables with the correct values. Subsequently, run the following commands to initiate the script:
22+
23+
```bash
24+
$ git clone https://github.com/supertokens/supertokens-core.git
25+
$ cd supertokens-core/migration_scripts/to_version_7_0_12
26+
$ npm install
27+
$ npm start
28+
```
29+
30+
Performance Note: On average, the script takes 19s for every 1000 rows with a maximum of 1 connection, 4.7s with a maximum of 5 connections (default), and 4.5s with a maximum of 10 connections. Increasing the `MAX_POOL_SIZE` allows the script to leverage more connections simultaneously, potentially improving execution speed.
31+
832
## [7.0.11] - 2023-11-10
933

1034
- Fixes email verification behaviour with user id mapping

build.gradle

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ compileTestJava { options.encoding = "UTF-8" }
1919
// }
2020
//}
2121

22-
version = "7.0.11"
22+
version = "7.0.12"
2323

2424

2525
repositories {
@@ -71,6 +71,9 @@ dependencies {
7171
// https://mvnrepository.com/artifact/commons-codec/commons-codec
7272
implementation group: 'commons-codec', name: 'commons-codec', version: '1.15'
7373

74+
// https://mvnrepository.com/artifact/com.googlecode.libphonenumber/libphonenumber/
75+
implementation group: 'com.googlecode.libphonenumber', name: 'libphonenumber', version: '8.13.25'
76+
7477
compileOnly project(":supertokens-plugin-interface")
7578
testImplementation project(":supertokens-plugin-interface")
7679

implementationDependencies.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,11 @@
110110
"jar": "https://repo1.maven.org/maven2/commons-codec/commons-codec/1.15/commons-codec-1.15.jar",
111111
"name": "Commons Codec 1.15",
112112
"src": "https://repo1.maven.org/maven2/commons-codec/commons-codec/1.15/commons-codec-1.15-sources.jar"
113+
},
114+
{
115+
"jar": "https://repo1.maven.org/maven2/com/googlecode/libphonenumber/libphonenumber/8.13.25/libphonenumber-8.13.25.jar",
116+
"name": "Libphonenumber 8.13.25",
117+
"src": "https://repo1.maven.org/maven2/com/googlecode/libphonenumber/libphonenumber/8.13.25/libphonenumber-8.13.25-sources.jar"
113118
}
114119
]
115120
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
* Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved.
3+
*
4+
* This software is licensed under the Apache License, Version 2.0 (the
5+
* "License") as published by the Apache Software Foundation.
6+
*
7+
* You may not use this file except in compliance with the License. You may
8+
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
17+
const libphonenumber = require('libphonenumber-js/max');
18+
19+
// Update the following credentials before running the script
20+
const DB_HOST = "";
21+
const DB_USER = "";
22+
const DB_PASSWORD = "";
23+
const DB_NAME = "";
24+
const CLIENT = ""; // Use "pg" for PostgreSQL and "mysql2" for MySQL DB
25+
26+
const MIN_POOL_SIZE = 0;
27+
const MAX_POOL_SIZE = 5;
28+
const QUERY_TIMEOUT = 60000;
29+
30+
if (!DB_HOST || !CLIENT) {
31+
console.error('Please update the DB_HOST, DB_USER, DB_PASSWORD, DB_DATABASE and CLIENT variables before running the script.');
32+
return;
33+
}
34+
35+
const knex = require('knex')({
36+
client: CLIENT,
37+
connection: {
38+
host: DB_HOST,
39+
user: DB_USER,
40+
password: DB_PASSWORD,
41+
database: DB_NAME,
42+
},
43+
pool: { min: MIN_POOL_SIZE, max: MAX_POOL_SIZE }
44+
});
45+
46+
function getUpdatePromise(table, entry, normalizedPhoneNumber) {
47+
if (table === 'passwordless_devices') {
48+
return knex.raw(`UPDATE ${table} SET phone_number = ? WHERE app_id = ? AND tenant_id = ? AND device_id_hash = ?`, [normalizedPhoneNumber, entry.app_id, entry.tenant_id, entry.device_id_hash]).timeout(QUERY_TIMEOUT, { cancel: true });
49+
} else if (table === 'passwordless_users') {
50+
// Since passwordless_users and passwordless_user_to_tenant are consistent. We can update both tables at the same time. For consistency, we will use a transaction.
51+
return knex.transaction(async trx => {
52+
await trx.raw(`UPDATE passwordless_users SET phone_number = ? WHERE app_id = ? AND user_id = ?`, [normalizedPhoneNumber, entry.app_id, entry.user_id]).timeout(QUERY_TIMEOUT, { cancel: true });
53+
await trx.raw(`UPDATE passwordless_user_to_tenant SET phone_number = ? WHERE app_id = ? AND user_id = ?`, [normalizedPhoneNumber, entry.app_id, entry.user_id]).timeout(QUERY_TIMEOUT, { cancel: true });
54+
});
55+
} else {
56+
throw new Error(`Invalid table name: ${table}`);
57+
}
58+
}
59+
60+
function getNormalizedPhoneNumber(phoneNumber) {
61+
try {
62+
return libphonenumber.parsePhoneNumber(phoneNumber, { extract: false }).format('E.164');
63+
} catch (error) {
64+
return null;
65+
}
66+
}
67+
68+
async function updatePhoneNumbers(table) {
69+
const batchSize = 1000;
70+
let offset = 0;
71+
let totalUpdatedRows = 0;
72+
73+
try {
74+
let totalRows = await knex.raw(`SELECT COUNT(*) as count FROM ${table} WHERE phone_number is NOT NULL`);
75+
totalRows = totalRows.rows ? totalRows.rows[0].count : totalRows[0][0].count;
76+
77+
while (true) {
78+
const entries = await knex.raw(`SELECT * FROM ${table} WHERE phone_number is NOT NULL LIMIT ${batchSize} OFFSET ${offset}`);
79+
// In PostgreSQL, all rows are returned in `entries.rows`, whereas in MySQL, they can be found in `entries[0]`.
80+
const rows = entries.rows ? entries.rows : entries[0];
81+
82+
const batchUpdates = [];
83+
84+
for (const entry of rows) {
85+
const currentPhoneNumber = entry.phone_number;
86+
const normalizedPhoneNumber = getNormalizedPhoneNumber(currentPhoneNumber);
87+
88+
if (normalizedPhoneNumber && normalizedPhoneNumber !== currentPhoneNumber) {
89+
const updatePromise = getUpdatePromise(table, entry, normalizedPhoneNumber);
90+
batchUpdates.push(updatePromise);
91+
}
92+
}
93+
94+
await Promise.all(batchUpdates);
95+
96+
offset += rows.length;
97+
totalUpdatedRows += batchUpdates.length;
98+
99+
console.log(`Processed ${offset} out of ${totalRows} rows in table ${table}; ${totalUpdatedRows} rows updated`);
100+
101+
if (rows.length < batchSize) {
102+
break;
103+
}
104+
}
105+
} catch (error) {
106+
console.error(`Error normalising phone numbers for table ${table}: Retry running the script and if the error persists after retrying then create an issue at https://github.com/supertokens/supertokens-core/issues`);
107+
throw error;
108+
}
109+
}
110+
111+
async function runScript() {
112+
const tables = ['passwordless_users', 'passwordless_devices'];
113+
114+
try {
115+
for (const table of tables) {
116+
await updatePhoneNumbers(table);
117+
console.log(`\n\n\n`);
118+
}
119+
console.log('Finished normalising phone numbers!');
120+
} catch (error) {
121+
console.error(error);
122+
} finally {
123+
knex.destroy();
124+
}
125+
126+
}
127+
128+
runScript();

0 commit comments

Comments
 (0)