Skip to content

Commit 7a9354e

Browse files
committed
Merge branch 'main' into bugfix/DE-931
2 parents ba435cc + c2aeddf commit 7a9354e

26 files changed

Lines changed: 1200 additions & 355 deletions

File tree

backend/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@
3232
"script:refreshGithubRepoSettings": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true tsx src/bin/scripts/refresh-github-repo-settings.ts",
3333
"script:fix-duplicate-members": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true tsx src/bin/scripts/fix-duplicate-members.ts",
3434
"script:fix-members-activities-after-unaffilation": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true tsx src/bin/scripts/fix-members-activities-after-unaffilation.ts",
35-
"script:process-bot-members": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true tsx src/bin/scripts/process-bot-members.ts"
35+
"script:process-bot-members": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true tsx src/bin/scripts/process-bot-members.ts",
36+
"script:backfill-email-domain-member-organization-dates": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true tsx src/bin/scripts/backfill-email-domain-member-organization-dates.ts"
3637
},
3738
"lint-staged": {
3839
"**/*.ts": [

backend/src/api/public/v1/members/identities/createMemberIdentity.ts

Lines changed: 23 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { captureApiChange, memberEditIdentitiesAction } from '@crowd/audit-logs'
55
import { ConflictError, NotFoundError } from '@crowd/common'
66
import {
77
MemberField,
8-
checkMemberIdentityExistence,
98
findMemberById,
109
createMemberIdentity as insertMemberIdentity,
1110
optionsQx,
@@ -53,37 +52,35 @@ export async function createMemberIdentity(req: Request, res: Response): Promise
5352
captureOldState({})
5453

5554
await qx.tx(async (tx) => {
56-
const existing = await checkMemberIdentityExistence(
57-
tx,
58-
data.value,
59-
data.platform,
60-
data.type,
61-
)
62-
63-
for (const identity of existing) {
64-
if (identity.memberId === memberId) {
55+
try {
56+
result = await insertMemberIdentity(
57+
tx,
58+
{
59+
memberId,
60+
platform: data.platform,
61+
value: data.value,
62+
type: data.type,
63+
source: data.source,
64+
verified: data.verified,
65+
verifiedBy: data.verifiedBy,
66+
},
67+
true,
68+
true,
69+
)
70+
} catch (error) {
71+
const constraint =
72+
error.constraint ?? error.original?.constraint ?? error.parent?.constraint
73+
74+
if (constraint === 'uix_memberIdentities_memberId_platform_value_type') {
6575
throw new ConflictError('Identity already exists on this member')
6676
}
6777

68-
if (identity.verified) {
78+
if (constraint === 'uix_memberIdentities_platform_value_type_verified') {
6979
throw new ConflictError('Identity already verified on another member')
7080
}
71-
}
7281

73-
result = await insertMemberIdentity(
74-
tx,
75-
{
76-
memberId,
77-
platform: data.platform,
78-
value: data.value,
79-
type: data.type,
80-
source: data.source,
81-
verified: data.verified,
82-
verifiedBy: data.verifiedBy,
83-
},
84-
true,
85-
true,
86-
)
82+
throw error
83+
}
8784

8885
// touch member updated at to trigger merge suggestion
8986
await touchMemberUpdatedAt(tx, memberId)
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import commandLineArgs from 'command-line-args'
2+
3+
import { inferMemberOrganizationStintChanges } from '@crowd/common_services'
4+
import {
5+
changeMemberOrganizationAffiliationOverrides,
6+
checkOrganizationAffiliationPolicy,
7+
createMemberOrganization,
8+
fetchEmailDomainMemberOrganizationActivityDates,
9+
fetchEmailDomainMemberOrganizationsWithoutDates,
10+
fetchMemberOrganizationsBySource,
11+
pgpQx,
12+
updateMemberOrganization,
13+
} from '@crowd/data-access-layer'
14+
import { getDbConnection } from '@crowd/data-access-layer/src/database'
15+
import { chunkArray } from '@crowd/data-access-layer/src/old/apps/merge_suggestions_worker/utils'
16+
import { getServiceLogger } from '@crowd/logging'
17+
import { getRedisClient } from '@crowd/redis'
18+
import { OrganizationSource } from '@crowd/types'
19+
20+
import { DB_CONFIG, REDIS_CONFIG } from '@/conf'
21+
22+
const log = getServiceLogger()
23+
24+
const options = [
25+
{
26+
name: 'testRun',
27+
alias: 't',
28+
type: Boolean,
29+
description: 'Run in test mode (limit to 1 batch and 10 members).',
30+
},
31+
{
32+
name: 'afterMemberId',
33+
alias: 'a',
34+
type: String,
35+
description: 'The member ID to start processing after.',
36+
},
37+
{
38+
name: 'batchSize',
39+
alias: 'b',
40+
type: Number,
41+
description: 'The number of members to fetch in each batch.',
42+
},
43+
{
44+
name: 'help',
45+
alias: 'h',
46+
type: Boolean,
47+
description: 'Print this usage guide.',
48+
},
49+
]
50+
51+
const parameters = commandLineArgs(options)
52+
53+
setImmediate(async () => {
54+
const testRun = parameters.testRun ?? false
55+
const BATCH_SIZE = parameters.batchSize ?? (testRun ? 10 : 500)
56+
let afterMemberId = parameters.afterMemberId ?? undefined
57+
58+
const db = await getDbConnection({
59+
host: DB_CONFIG.writeHost,
60+
port: DB_CONFIG.port,
61+
database: DB_CONFIG.database,
62+
user: DB_CONFIG.username,
63+
password: DB_CONFIG.password,
64+
})
65+
66+
const qx = pgpQx(db)
67+
const redis = await getRedisClient(REDIS_CONFIG, true)
68+
69+
log.info({ testRun, BATCH_SIZE, afterMemberId }, 'Running script with the following parameters!')
70+
71+
let hasMore = true
72+
73+
while (hasMore) {
74+
const memberIds = await fetchEmailDomainMemberOrganizationsWithoutDates(
75+
qx,
76+
BATCH_SIZE,
77+
afterMemberId,
78+
)
79+
80+
if (memberIds.length > 0) {
81+
for (const chunk of chunkArray(memberIds, 50)) {
82+
await Promise.all(
83+
chunk.map(async (memberId) => {
84+
if (testRun) {
85+
log.info({ memberId }, 'Processing member!')
86+
}
87+
88+
try {
89+
const [existingMemberOrganizations, activityDates] = await Promise.all([
90+
fetchMemberOrganizationsBySource(qx, memberId, OrganizationSource.EMAIL_DOMAIN),
91+
fetchEmailDomainMemberOrganizationActivityDates(qx, memberId),
92+
])
93+
94+
const changes = inferMemberOrganizationStintChanges(
95+
memberId,
96+
existingMemberOrganizations,
97+
activityDates,
98+
)
99+
100+
if (testRun) {
101+
log.info(
102+
{ existingMemberOrganizations, activityDates, changes },
103+
'Previewing changes for member.',
104+
)
105+
}
106+
107+
if (changes.length > 0) {
108+
await qx.tx(async (tx) => {
109+
for (const change of changes) {
110+
if (change.type === 'insert') {
111+
const memberOrganizationId = await createMemberOrganization(tx, memberId, {
112+
organizationId: change.organizationId,
113+
dateStart: change.dateStart,
114+
dateEnd: change.dateEnd,
115+
source: OrganizationSource.EMAIL_DOMAIN,
116+
})
117+
118+
const isAffiliationBlocked = await checkOrganizationAffiliationPolicy(
119+
tx,
120+
change.organizationId,
121+
)
122+
123+
if (memberOrganizationId && isAffiliationBlocked) {
124+
await changeMemberOrganizationAffiliationOverrides(tx, [
125+
{
126+
memberId,
127+
memberOrganizationId,
128+
allowAffiliation: false,
129+
},
130+
])
131+
}
132+
} else if (change.type === 'update') {
133+
await updateMemberOrganization(tx, memberId, change.id, {
134+
dateStart: change.dateStart,
135+
dateEnd: change.dateEnd,
136+
})
137+
}
138+
139+
if (testRun) {
140+
log.info(
141+
{ memberId, orgId: change.organizationId, type: change.type },
142+
'Member organization updated.',
143+
)
144+
}
145+
}
146+
})
147+
await redis.sAdd('recalculate-member-affiliations', [memberId])
148+
} else if (testRun) {
149+
log.info({ memberId }, 'No changes found for member!')
150+
}
151+
} catch (err) {
152+
log.error({ memberId, err }, 'Failed to process for member!')
153+
throw err
154+
}
155+
}),
156+
)
157+
}
158+
159+
const lastMemberId = memberIds[memberIds.length - 1]
160+
afterMemberId = lastMemberId
161+
162+
log.info({ lastMemberId, count: memberIds.length }, 'Batch processed!')
163+
164+
if (testRun || memberIds.length < BATCH_SIZE) {
165+
hasMore = false
166+
}
167+
} else {
168+
hasMore = false
169+
}
170+
}
171+
172+
process.exit(0)
173+
})
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
CREATE INDEX CONCURRENTLY IF NOT EXISTS "ix_memberOrganizations_memberId_emailDomain"
2+
ON "memberOrganizations" ("memberId")
3+
WHERE "source" = 'email-domain' AND "deletedAt" IS NULL;

backend/src/services/member/memberOrganizationsService.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
IOrganization,
2525
IRenderFriendlyMemberOrganization,
2626
MemberOrganizationUpdate,
27+
OrganizationSource,
2728
} from '@crowd/types'
2829

2930
import SequelizeRepository from '@/database/repositories/sequelizeRepository'
@@ -247,8 +248,13 @@ export default class MemberOrganizationsService extends LoggerBase {
247248
(v) => v !== undefined,
248249
) as MemberOrganizationUpdate
249250

250-
await cleanSoftDeletedMemberOrganization(qx, memberId, data.organizationId, update)
251-
await updateMemberOrganization(qx, memberId, id, update)
251+
await cleanSoftDeletedMemberOrganization(qx, memberId, data.organizationId, data)
252+
// Any manual edit from the frontend promotes ownership to UI so automated
253+
// sources (e.g. email-domain inference) no longer overwrite user intent.
254+
await updateMemberOrganization(qx, memberId, id, {
255+
...update,
256+
source: OrganizationSource.UI,
257+
})
252258

253259
// Trigger recalculation for old and new orgs if changed
254260
const orgsToRecalculate = Array.from(

0 commit comments

Comments
 (0)