Skip to content

Commit 600ee6f

Browse files
authored
fix: improve member identity resolve and create api (#4117)
Signed-off-by: Yeganathan S <63534555+skwowet@users.noreply.github.com>
1 parent cd27dc9 commit 600ee6f

3 files changed

Lines changed: 83 additions & 39 deletions

File tree

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

Lines changed: 76 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ import { ConflictError, NotFoundError } from '@crowd/common'
66
import {
77
MemberField,
88
findMemberById,
9+
findMemberIdentitiesByValue,
910
createMemberIdentity as insertMemberIdentity,
1011
optionsQx,
1112
touchMemberUpdatedAt,
13+
updateMemberIdentity,
1214
} from '@crowd/data-access-layer'
1315
import { IMemberIdentity, MemberIdentityType } from '@crowd/types'
1416

15-
import { created } from '@/utils/api'
17+
import { created, ok } from '@/utils/api'
1618
import { validateOrThrow } from '@/utils/validation'
1719

1820
const paramsSchema = z.object({
@@ -33,72 +35,103 @@ const bodySchema = z
3335
path: ['verifiedBy'],
3436
})
3537

38+
type DbConstraintError = Error & {
39+
constraint?: string
40+
original?: { constraint?: string }
41+
parent?: { constraint?: string }
42+
}
43+
44+
function throwIdentityConflict(
45+
error: DbConstraintError,
46+
data: { platform: string; value: string; type: MemberIdentityType },
47+
): never {
48+
const constraint = error.constraint ?? error.original?.constraint ?? error.parent?.constraint
49+
50+
if (constraint === 'uix_memberIdentities_platform_value_type_verified') {
51+
throw new ConflictError('Identity already verified on another member', data)
52+
}
53+
54+
throw error
55+
}
56+
3657
export async function createMemberIdentity(req: Request, res: Response): Promise<void> {
3758
const { memberId } = validateOrThrow(paramsSchema, req.params)
3859
const data = validateOrThrow(bodySchema, req.body)
3960

4061
const qx = optionsQx(req)
41-
4262
const member = await findMemberById(qx, memberId, [MemberField.ID])
4363
if (!member) {
4464
throw new NotFoundError('Member not found')
4565
}
4666

67+
// The data-sink writes identity values as trimmed lowercase, so normalize here
68+
// to keep idempotency checks reliable against existing rows.
69+
const normalizedValue = data.value.trim().toLowerCase()
70+
const conflictContext = { platform: data.platform, value: normalizedValue, type: data.type }
71+
4772
let result!: IMemberIdentity
73+
let alreadyExisted = false
4874

4975
await captureApiChange(
5076
req,
5177
memberEditIdentitiesAction(memberId, async (captureOldState, captureNewState) => {
5278
captureOldState({})
5379

5480
await qx.tx(async (tx) => {
81+
const existing = await findMemberIdentitiesByValue(tx, memberId, normalizedValue, {
82+
type: data.type,
83+
})
84+
const exactMatch = existing.find((i) => i.platform === data.platform)
85+
5586
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') {
75-
throw new ConflictError('Identity already exists on this member', {
76-
platform: data.platform,
77-
value: data.value,
78-
type: data.type,
79-
})
87+
if (exactMatch) {
88+
alreadyExisted = true
89+
result = exactMatch
90+
} else {
91+
result = await insertMemberIdentity(
92+
tx,
93+
{
94+
memberId,
95+
platform: data.platform,
96+
value: normalizedValue,
97+
type: data.type,
98+
source: data.source,
99+
verified: data.verified,
100+
verifiedBy: data.verifiedBy,
101+
},
102+
true,
103+
true,
104+
)
80105
}
81106

82-
if (constraint === 'uix_memberIdentities_platform_value_type_verified') {
83-
throw new ConflictError('Identity already verified on another member', {
84-
platform: data.platform,
85-
value: data.value,
86-
type: data.type,
87-
})
107+
// A verified identity confirms the same value for this member, so keep same-value
108+
// identities in sync instead of leaving stale unverified duplicates behind.
109+
if (data.verified && existing.length > 0) {
110+
const updatedResults: IMemberIdentity[] = []
111+
for (const identity of existing) {
112+
const updated = await updateMemberIdentity(tx, memberId, identity.id, {
113+
verified: true,
114+
verifiedBy: data.verifiedBy,
115+
})
116+
if (updated) updatedResults.push(updated)
117+
}
118+
119+
if (alreadyExisted) {
120+
result = updatedResults.find((r) => r.id === exactMatch.id) ?? result
121+
}
88122
}
89-
90-
throw error
123+
} catch (error) {
124+
throwIdentityConflict(error as DbConstraintError, conflictContext)
91125
}
92126

93-
// touch member updated at to trigger merge suggestion
94127
await touchMemberUpdatedAt(tx, memberId)
95128
})
96129

97130
captureNewState(result)
98131
}),
99132
)
100133

101-
created(res, {
134+
const response = {
102135
id: result.id,
103136
value: result.value,
104137
platform: result.platform,
@@ -108,5 +141,11 @@ export async function createMemberIdentity(req: Request, res: Response): Promise
108141
source: result.source ?? null,
109142
createdAt: result.createdAt,
110143
updatedAt: result.updatedAt,
111-
})
144+
}
145+
146+
if (alreadyExisted) {
147+
ok(res, response)
148+
} else {
149+
created(res, response)
150+
}
112151
}

backend/src/api/public/v1/members/resolveMember.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,13 @@ export async function resolveMemberByIdentities(req: Request, res: Response): Pr
2323
platform: PlatformType.LFID,
2424
type: MemberIdentityType.USERNAME,
2525
value: lfid,
26+
verified: true,
2627
})),
27-
...(emails?.map((email) => ({ type: MemberIdentityType.EMAIL, value: email })) ?? []),
28+
...(emails?.map((email) => ({
29+
type: MemberIdentityType.EMAIL,
30+
value: email,
31+
verified: true,
32+
})) ?? []),
2833
]
2934

3035
const memberIds = await findMemberIdsByIdentities(qx, identities)

services/libs/data-access-layer/src/members/identities.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export async function findMemberIdentitiesByValue(
102102
): Promise<IMemberIdentity[]> {
103103
return qx.select(
104104
`
105-
SELECT id, platform, "sourceId", type, value, verified
105+
SELECT *
106106
FROM "memberIdentities"
107107
WHERE value = $(value)
108108
AND "memberId" = $(memberId)

0 commit comments

Comments
 (0)