11import commandLineArgs from 'command-line-args'
22
3+ import { isEmail } from '@crowd/common'
34import { pgpQx } from '@crowd/data-access-layer'
45import { getDbConnection } from '@crowd/data-access-layer/src/database'
56import { QueryExecutor } from '@crowd/data-access-layer/src/queryExecutor'
@@ -19,104 +20,58 @@ const options = [
1920
2021const parameters = commandLineArgs ( options )
2122
22- type CaseVariantGroup = {
23+ type MixedCaseEmailIdentity = {
24+ id : string
2325 memberId : string
2426 platform : string
2527 type : string
26- lv : string
27- }
28-
29- type IdentityRow = {
30- id : string
3128 value : string
32- verified : boolean
33- verifiedBy : string | null
34- updatedAt : Date
3529}
3630
3731const AR_UPDATE_BATCH_SIZE = 5000
32+ const LOAD_BATCH_SIZE = 500
3833
39- function pickKeeper ( rows : IdentityRow [ ] ) : IdentityRow {
40- return [ ...rows ] . sort ( ( a , b ) => {
41- if ( a . verified !== b . verified ) return a . verified ? - 1 : 1
42- if ( Boolean ( a . verifiedBy ) !== Boolean ( b . verifiedBy ) ) return a . verifiedBy ? - 1 : 1
43- const aUpdated = new Date ( a . updatedAt ) . getTime ( )
44- const bUpdated = new Date ( b . updatedAt ) . getTime ( )
45- if ( aUpdated !== bUpdated ) return bUpdated - aUpdated
46- return a . id < b . id ? - 1 : 1
47- } ) [ 0 ]
48- }
49-
50- async function findDuplicateCaseVariantGroups (
51- qx : QueryExecutor ,
52- limit ?: number ,
53- ) : Promise < CaseVariantGroup [ ] > {
54- const baseQuery = `
55- select
56- "memberId",
57- platform,
58- type,
59- lower(value) as lv
60- from "memberIdentities"
61- where "deletedAt" is null
62- group by "memberId", platform, type, lower(value)
63- having count(distinct value) > 1
64- order by "memberId", platform, type, lower(value)
65- `
66-
67- if ( limit != null ) {
68- return qx . select ( `${ baseQuery } limit $(limit)` , { limit } )
69- }
70-
71- return qx . select ( baseQuery )
72- }
73-
74- async function fetchGroupIdentities (
34+ async function findMixedCaseEmailIdentities (
7535 qx : QueryExecutor ,
76- group : CaseVariantGroup ,
77- ) : Promise < IdentityRow [ ] > {
36+ afterId : string | null ,
37+ limit : number ,
38+ ) : Promise < MixedCaseEmailIdentity [ ] > {
7839 return qx . select (
7940 `
80- select id, value, verified, "verifiedBy", "updatedAt"
41+ select id, "memberId", platform, type, value
8142 from "memberIdentities"
82- where "memberId" = $(memberId)
83- and platform = $(platform)
84- and type = $(type)
85- and lower(value) = $(lv)
86- and "deletedAt" is null
43+ where "deletedAt" is null
44+ and value <> lower(value)
45+ and position('@' in value) > 0
46+ ${ afterId ? 'and id > $(afterId)' : '' }
47+ order by id
48+ limit $(limit)
8749 ` ,
88- group ,
50+ { afterId , limit } ,
8951 )
9052}
9153
92- async function softDeleteIdentities ( qx : QueryExecutor , ids : string [ ] ) : Promise < number > {
93- if ( ids . length === 0 ) return 0
94-
54+ async function lowercaseIdentityValue ( qx : QueryExecutor , id : string , value : string ) : Promise < number > {
9555 return qx . result (
9656 `
9757 update "memberIdentities"
9858 set
99- "deletedAt" = now( ),
59+ value = $(value ),
10060 "updatedAt" = now()
101- where id in ($(ids:csv) )
61+ where id = $(id )
10262 and "deletedAt" is null
63+ and value <> $(value)
10364 ` ,
104- { ids } ,
65+ { id , value } ,
10566 )
10667}
10768
108- async function rewriteActivityRelationUsernames (
69+ async function rewriteActivityRelationUsernamesToLower (
10970 qx : QueryExecutor ,
11071 memberId : string ,
11172 platform : string ,
112- keeperUsername : string ,
113- deletedUsernames : string [ ] ,
73+ lowerValue : string ,
11474) : Promise < { usernameRows : number ; objectMemberUsernameRows : number } > {
115- const values = deletedUsernames . filter ( ( v ) => v !== keeperUsername )
116- if ( values . length === 0 ) {
117- return { usernameRows : 0 , objectMemberUsernameRows : 0 }
118- }
119-
12075 let usernameRows = 0
12176 let objectMemberUsernameRows = 0
12277 let updated : number
@@ -126,22 +81,22 @@ async function rewriteActivityRelationUsernames(
12681 `
12782 update "activityRelations"
12883 set
129- username = $(keeperUsername ),
84+ username = $(lowerValue ),
13085 "updatedAt" = now()
13186 where "activityId" in (
13287 select "activityId"
13388 from "activityRelations"
13489 where "memberId" = $(memberId)
13590 and platform = $(platform)
136- and username in ($(values:csv))
91+ and lower(username) = $(lowerValue)
92+ and username <> $(lowerValue)
13793 limit $(batchSize)
13894 )
13995 ` ,
14096 {
14197 memberId,
14298 platform,
143- keeperUsername,
144- values,
99+ lowerValue,
145100 batchSize : AR_UPDATE_BATCH_SIZE ,
146101 } ,
147102 )
@@ -153,22 +108,22 @@ async function rewriteActivityRelationUsernames(
153108 `
154109 update "activityRelations"
155110 set
156- "objectMemberUsername" = $(keeperUsername ),
111+ "objectMemberUsername" = $(lowerValue ),
157112 "updatedAt" = now()
158113 where "activityId" in (
159114 select "activityId"
160115 from "activityRelations"
161116 where "objectMemberId" = $(memberId)
162117 and platform = $(platform)
163- and "objectMemberUsername" in ($(values:csv))
118+ and lower("objectMemberUsername") = $(lowerValue)
119+ and "objectMemberUsername" <> $(lowerValue)
164120 limit $(batchSize)
165121 )
166122 ` ,
167123 {
168124 memberId,
169125 platform,
170- keeperUsername,
171- values,
126+ lowerValue,
172127 batchSize : AR_UPDATE_BATCH_SIZE ,
173128 } ,
174129 )
@@ -181,6 +136,7 @@ async function rewriteActivityRelationUsernames(
181136setImmediate ( async ( ) => {
182137 const testRun = parameters . testRun ?? false
183138 const PROCESS_BATCH_LOG_EVERY = testRun ? 1 : 200
139+ const batchSize = testRun ? 10 : LOAD_BATCH_SIZE
184140
185141 const db = await getDbConnection ( {
186142 host : DB_CONFIG . writeHost ,
@@ -192,79 +148,83 @@ setImmediate(async () => {
192148
193149 const qx = pgpQx ( db )
194150
195- log . info ( { testRun } , 'Running script with the following parameters!' )
196-
197- const groups = await findDuplicateCaseVariantGroups ( qx , testRun ? 10 : undefined )
198- log . info ( { groupCount : groups . length } , 'Loaded same-member case-variant groups!' )
151+ log . info ( { testRun, batchSize } , 'Lowercasing email-shaped identity values!' )
199152
153+ let afterId : string | null = null
200154 let processed = 0
201- let softDeleted = 0
155+ let identitiesUpdated = 0
202156 let skipped = 0
203157 let arUsernameUpdated = 0
204158 let arObjectUsernameUpdated = 0
205159
206- for ( const group of groups ) {
207- const rows = await fetchGroupIdentities ( qx , group )
208-
209- if ( rows . length < 2 ) {
210- skipped += 1
211- } else {
212- const keeper = pickKeeper ( rows )
213- const toDeleteRows = rows . filter ( ( r ) => r . id !== keeper . id )
214- const toDeleteIds = toDeleteRows . map ( ( r ) => r . id )
215- const deletedValues = toDeleteRows . map ( ( r ) => r . value )
160+ for ( ; ; ) {
161+ const candidates = await findMixedCaseEmailIdentities ( qx , afterId , batchSize )
162+ if ( candidates . length === 0 ) {
163+ break
164+ }
216165
217- if ( testRun ) {
218- log . info (
219- {
220- memberId : group . memberId ,
221- platform : group . platform ,
222- type : group . type ,
223- keep : keeper . value ,
224- softDelete : deletedValues ,
225- } ,
226- 'Soft-deleting case variants!' ,
227- )
166+ afterId = candidates [ candidates . length - 1 ] . id
167+
168+ for ( const row of candidates ) {
169+ const lowerValue = row . value . trim ( ) . toLowerCase ( )
170+
171+ if ( isEmail ( lowerValue ) && lowerValue !== row . value ) {
172+ if ( testRun ) {
173+ log . info (
174+ {
175+ memberId : row . memberId ,
176+ platform : row . platform ,
177+ type : row . type ,
178+ from : row . value ,
179+ to : lowerValue ,
180+ } ,
181+ 'Lowercasing email-shaped identity!' ,
182+ )
183+ }
184+
185+ const { updatedCount, ar } = await qx . tx ( async ( tx ) => {
186+ const updatedCount = await lowercaseIdentityValue ( tx , row . id , lowerValue )
187+ const ar = await rewriteActivityRelationUsernamesToLower (
188+ tx ,
189+ row . memberId ,
190+ row . platform ,
191+ lowerValue ,
192+ )
193+ return { updatedCount, ar }
194+ } )
195+
196+ identitiesUpdated += updatedCount
197+ arUsernameUpdated += ar . usernameRows
198+ arObjectUsernameUpdated += ar . objectMemberUsernameRows
199+ processed += 1
200+
201+ if ( processed % PROCESS_BATCH_LOG_EVERY === 0 ) {
202+ log . info (
203+ {
204+ processed,
205+ identitiesUpdated,
206+ skipped,
207+ arUsernameUpdated,
208+ arObjectUsernameUpdated,
209+ } ,
210+ 'Progress!' ,
211+ )
212+ }
213+ } else {
214+ skipped += 1
228215 }
216+ }
229217
230- const { deletedCount, ar } = await qx . tx ( async ( tx ) => {
231- const deletedCount = await softDeleteIdentities ( tx , toDeleteIds )
232- const ar = await rewriteActivityRelationUsernames (
233- tx ,
234- group . memberId ,
235- group . platform ,
236- keeper . value ,
237- deletedValues ,
238- )
239- return { deletedCount, ar }
240- } )
241-
242- softDeleted += deletedCount
243- arUsernameUpdated += ar . usernameRows
244- arObjectUsernameUpdated += ar . objectMemberUsernameRows
245-
246- processed += 1
247-
248- if ( processed % PROCESS_BATCH_LOG_EVERY === 0 ) {
249- log . info (
250- {
251- processed,
252- total : groups . length ,
253- softDeleted,
254- skipped,
255- arUsernameUpdated,
256- arObjectUsernameUpdated,
257- } ,
258- 'Progress!' ,
259- )
260- }
218+ if ( testRun ) {
219+ log . info ( 'Test run - stopping after first batch!' )
220+ break
261221 }
262222 }
263223
264224 log . info (
265225 {
266226 processed,
267- softDeleted ,
227+ identitiesUpdated ,
268228 skipped,
269229 arUsernameUpdated,
270230 arObjectUsernameUpdated,
0 commit comments