Skip to content

Commit 9d4cf09

Browse files
authored
fix: prevent duplicate member merge operations (#2853)
1 parent 90e1aa5 commit 9d4cf09

File tree

5 files changed

+72
-14
lines changed

5 files changed

+72
-14
lines changed

backend/src/services/memberService.ts

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import validator from 'validator'
77
import { captureApiChange, memberMergeAction, memberUnmergeAction } from '@crowd/audit-logs'
88
import {
99
Error400,
10+
Error409,
1011
getEarliestValidDate,
1112
getProperDisplayName,
1213
isDomainExcluded,
@@ -23,6 +24,7 @@ import {
2324
queryMembersAdvanced,
2425
removeMemberTags,
2526
} from '@crowd/data-access-layer/src/members'
27+
import { findMergeAction } from '@crowd/data-access-layer/src/mergeActions/repo'
2628
import { QueryExecutor, optionsQx } from '@crowd/data-access-layer/src/queryExecutor'
2729
// import { getActivityCountOfMemberIdentities } from '@crowd/data-access-layer'
2830
import { fetchManySegments } from '@crowd/data-access-layer/src/segments'
@@ -1204,10 +1206,21 @@ export default class MemberService extends LoggerBase {
12041206
}
12051207
}
12061208

1209+
const qx = SequelizeRepository.getQueryExecutor(this.options)
1210+
1211+
const mergeAction = await findMergeAction(qx, originalId, toMergeId)
1212+
1213+
// prevent multiple merge operations
1214+
if (
1215+
mergeAction?.state === MergeActionState.IN_PROGRESS ||
1216+
mergeAction?.state === MergeActionState.PENDING
1217+
) {
1218+
throw new Error409(this.options.language, 'merge.errors.multipleMerge', mergeAction?.state)
1219+
}
1220+
12071221
let tx
12081222

12091223
const getMemberById = async (memberId: string) => {
1210-
const qx = SequelizeRepository.getQueryExecutor(this.options)
12111224
const member = await findMemberById(qx, memberId, [
12121225
MemberField.ID,
12131226
MemberField.DISPLAY_NAME,
@@ -1346,21 +1359,21 @@ export default class MemberService extends LoggerBase {
13461359
currentSegments: secondMemberSegments,
13471360
})
13481361

1362+
await MergeActionsRepository.setMergeAction(
1363+
MergeActionType.MEMBER,
1364+
originalId,
1365+
toMergeId,
1366+
repoOptions,
1367+
{
1368+
step: MergeActionStep.MERGE_SYNC_DONE,
1369+
},
1370+
)
1371+
13491372
await SequelizeRepository.commitTransaction(tx)
13501373
return { original, toMerge }
13511374
}),
13521375
)
13531376

1354-
await MergeActionsRepository.setMergeAction(
1355-
MergeActionType.MEMBER,
1356-
originalId,
1357-
toMergeId,
1358-
this.options,
1359-
{
1360-
step: MergeActionStep.MERGE_SYNC_DONE,
1361-
},
1362-
)
1363-
13641377
await this.options.temporal.workflow.start('finishMemberMerging', {
13651378
taskQueue: 'entity-merging',
13661379
workflowId: `finishMemberMerging/${originalId}/${toMergeId}`,

backend/src/services/organizationService.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import {
66
organizationMergeAction,
77
organizationUnmergeAction,
88
} from '@crowd/audit-logs'
9-
import { websiteNormalizer } from '@crowd/common'
9+
import { Error409, websiteNormalizer } from '@crowd/common'
1010
import { hasLfxMembership } from '@crowd/data-access-layer/src/lfx_memberships'
11+
import { findMergeAction } from '@crowd/data-access-layer/src/mergeActions/repo'
1112
import { findOrgAttributes, upsertOrgIdentities } from '@crowd/data-access-layer/src/organizations'
1213
import { LoggerBase } from '@crowd/logging'
1314
import { WorkflowIdReusePolicy } from '@crowd/temporal'
@@ -402,6 +403,16 @@ export default class OrganizationService extends LoggerBase {
402403
const qx = SequelizeRepository.getQueryExecutor(this.options)
403404
const tenantId = this.options.currentTenant.id
404405

406+
const mergeAction = await findMergeAction(qx, originalId, toMergeId)
407+
408+
// prevent multiple merge operations
409+
if (
410+
mergeAction?.state === MergeActionState.IN_PROGRESS ||
411+
mergeAction?.state === MergeActionState.PENDING
412+
) {
413+
throw new Error409(this.options.language, 'merge.errors.multipleMerge', mergeAction?.state)
414+
}
415+
405416
try {
406417
const { original, toMerge } = await captureApiChange(
407418
this.options,

services/libs/common/src/i18n/en.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,12 @@ const en = {
155155
alreadyExists: '{0}',
156156
},
157157

158+
merge: {
159+
errors: {
160+
multipleMerge: 'Cannot merge suggestions - found existing merge with {0} state',
161+
},
162+
},
163+
158164
email: {
159165
error: `Email provider is not configured.`,
160166
},

services/libs/data-access-layer/src/mergeActions/repo.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import validator from 'validator'
22

3-
import { IMergeAction } from '@crowd/types'
3+
import { IMergeAction, MergeActionState } from '@crowd/types'
44

55
import { QueryExecutor } from '../queryExecutor'
66

@@ -50,3 +50,32 @@ export async function queryMergeActions(
5050

5151
return result
5252
}
53+
54+
export async function findMergeAction(
55+
qx: QueryExecutor,
56+
primaryId: string,
57+
secondaryId: string,
58+
{ state }: { state?: MergeActionState } = {},
59+
): Promise<IMergeAction> {
60+
let where = ''
61+
62+
const params = {
63+
primaryId,
64+
secondaryId,
65+
}
66+
67+
if (state) {
68+
where += ` and "state" = $(state)`
69+
params['state'] = state
70+
}
71+
72+
return qx.selectOneOrNone(
73+
`
74+
select * from "mergeActions"
75+
where "primaryId" = $(primaryId)
76+
and "secondaryId" = $(secondaryId)
77+
${where}
78+
`,
79+
params,
80+
)
81+
}

services/libs/types/src/enums/merging.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ export enum MergeActionState {
88
IN_PROGRESS = 'in-progress',
99
MERGED = 'merged',
1010
UNMERGED = 'unmerged',
11-
FINISHING = 'finishing',
1211
ERROR = 'error',
1312
}
1413

0 commit comments

Comments
 (0)