Skip to content

Commit c3cf218

Browse files
committed
chore: clean up script for the email identity shape
Signed-off-by: Yeganathan S <63534555+skwowet@users.noreply.github.com>
1 parent 7a6f158 commit c3cf218

1 file changed

Lines changed: 94 additions & 134 deletions

File tree

backend/src/bin/scripts/cleanup-same-member-case-variant-identities.ts

Lines changed: 94 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import commandLineArgs from 'command-line-args'
22

3+
import { isEmail } from '@crowd/common'
34
import { pgpQx } from '@crowd/data-access-layer'
45
import { getDbConnection } from '@crowd/data-access-layer/src/database'
56
import { QueryExecutor } from '@crowd/data-access-layer/src/queryExecutor'
@@ -19,104 +20,58 @@ const options = [
1920

2021
const 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

3731
const 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(
181136
setImmediate(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

Comments
 (0)