From a8f8261c01080f38d88044473c8d4e5b9396930d Mon Sep 17 00:00:00 2001 From: Yeganathan S <63534555+skwowet@users.noreply.github.com> Date: Mon, 22 Jun 2026 16:08:11 +0530 Subject: [PATCH 1/3] fix: prevent member identity existence check from timing out Use platform, type, and case-insensitive value matching to leverage existing indexes and avoid full table scans. Signed-off-by: Yeganathan S <63534555+skwowet@users.noreply.github.com> --- .../services/member/memberIdentityService.ts | 26 ++++++++++++++----- .../src/members/identities.ts | 16 ++++++------ 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/backend/src/services/member/memberIdentityService.ts b/backend/src/services/member/memberIdentityService.ts index bbd882b94e..c2aad39c85 100644 --- a/backend/src/services/member/memberIdentityService.ts +++ b/backend/src/services/member/memberIdentityService.ts @@ -2,7 +2,7 @@ import lodash from 'lodash' import { captureApiChange, memberEditIdentitiesAction } from '@crowd/audit-logs' -import { Error409 } from '@crowd/common' +import { Error404, Error409 } from '@crowd/common' import { createMemberIdentity, findIdentitiesForMembers, optionsQx } from '@crowd/data-access-layer' import { checkMemberIdentityExistence, @@ -62,7 +62,9 @@ export default class MemberIdentityService extends LoggerBase { qx, data.value, data.platform, + data.type, ) + if (existingIdentities.length > 0) { throw new Error409( this.options.language, @@ -134,6 +136,7 @@ export default class MemberIdentityService extends LoggerBase { qx, identity.value, identity.platform, + identity.type, ) if (existingIdentities.length > 0) { @@ -203,20 +206,29 @@ export default class MemberIdentityService extends LoggerBase { const qx = SequelizeRepository.getQueryExecutor(repoOptions) - // Check if identity already exists + const currentIdentity = memberIdentities.find((identity) => identity.id === id) + if (!currentIdentity) { + throw new Error404(this.options.language, 'errors.notFound.message') + } + + const value = data.value ?? currentIdentity.value + const platform = data.platform ?? currentIdentity.platform + const type = data.type ?? currentIdentity.type + const existingIdentities = await checkMemberIdentityExistence( qx, - data.value, - data.platform, + value, + platform, + type, ) - const filteredExistingIdentities = existingIdentities.filter((i) => i.id !== id) - if (filteredExistingIdentities.length > 0) { + const conflict = existingIdentities.find((identity) => identity.memberId !== memberId) + if (conflict) { throw new Error409( this.options.language, 'errors.alreadyExists', // @ts-ignore JSON.stringify({ - memberId: filteredExistingIdentities[0].memberId, + memberId: conflict.memberId, }), ) } diff --git a/services/libs/data-access-layer/src/members/identities.ts b/services/libs/data-access-layer/src/members/identities.ts index 53a3985c72..5723bd541b 100644 --- a/services/libs/data-access-layer/src/members/identities.ts +++ b/services/libs/data-access-layer/src/members/identities.ts @@ -53,16 +53,16 @@ export async function checkMemberIdentityExistence( qx: QueryExecutor, value: string, platform: string, - type?: MemberIdentityType, + type: MemberIdentityType, ): Promise { - return await qx.select( + return qx.select( ` - SELECT id, "memberId", verified - FROM "memberIdentities" - WHERE "value" = $(value) - AND "platform" = $(platform) - ${type ? 'AND "type" = $(type)' : ''} - AND "deletedAt" is null; + SELECT id, "memberId" + FROM "memberIdentities" + WHERE platform = $(platform) + AND type = $(type) + AND lower(value) = lower($(value)) + AND "deletedAt" IS NULL; `, { value, From f89106da3c4ea466d7a4ec01f230e46b9035ed10 Mon Sep 17 00:00:00 2001 From: Yeganathan S <63534555+skwowet@users.noreply.github.com> Date: Mon, 22 Jun 2026 16:12:30 +0530 Subject: [PATCH 2/3] fix: make prettier and linter happy Signed-off-by: Yeganathan S <63534555+skwowet@users.noreply.github.com> --- backend/src/services/member/memberIdentityService.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/backend/src/services/member/memberIdentityService.ts b/backend/src/services/member/memberIdentityService.ts index c2aad39c85..2f1603db46 100644 --- a/backend/src/services/member/memberIdentityService.ts +++ b/backend/src/services/member/memberIdentityService.ts @@ -64,7 +64,7 @@ export default class MemberIdentityService extends LoggerBase { data.platform, data.type, ) - + if (existingIdentities.length > 0) { throw new Error409( this.options.language, @@ -215,12 +215,7 @@ export default class MemberIdentityService extends LoggerBase { const platform = data.platform ?? currentIdentity.platform const type = data.type ?? currentIdentity.type - const existingIdentities = await checkMemberIdentityExistence( - qx, - value, - platform, - type, - ) + const existingIdentities = await checkMemberIdentityExistence(qx, value, platform, type) const conflict = existingIdentities.find((identity) => identity.memberId !== memberId) if (conflict) { throw new Error409( From 45a68dda6751983b19b9ea9e1631d87de2a15545 Mon Sep 17 00:00:00 2001 From: Yeganathan S <63534555+skwowet@users.noreply.github.com> Date: Mon, 22 Jun 2026 16:33:29 +0530 Subject: [PATCH 3/3] fix: resolve pr review comments Signed-off-by: Yeganathan S <63534555+skwowet@users.noreply.github.com> --- .../services/member/memberIdentityService.ts | 41 ++++++++++--------- .../src/members/identities.ts | 27 ++++++------ 2 files changed, 37 insertions(+), 31 deletions(-) diff --git a/backend/src/services/member/memberIdentityService.ts b/backend/src/services/member/memberIdentityService.ts index 2f1603db46..a039a43a25 100644 --- a/backend/src/services/member/memberIdentityService.ts +++ b/backend/src/services/member/memberIdentityService.ts @@ -5,10 +5,10 @@ import { captureApiChange, memberEditIdentitiesAction } from '@crowd/audit-logs' import { Error404, Error409 } from '@crowd/common' import { createMemberIdentity, findIdentitiesForMembers, optionsQx } from '@crowd/data-access-layer' import { - checkMemberIdentityExistence, deleteMemberIdentity, fetchMemberIdentities, findMemberIdentityById, + findMemberIdentityConflict, touchMemberUpdatedAt, updateMemberIdentity, } from '@crowd/data-access-layer/src/members' @@ -58,20 +58,19 @@ export default class MemberIdentityService extends LoggerBase { const qx = SequelizeRepository.getQueryExecutor(repoOptions) // Check if identity already exists - const existingIdentities = await checkMemberIdentityExistence( - qx, - data.value, - data.platform, - data.type, - ) + const conflict = await findMemberIdentityConflict(qx, { + value: data.value, + platform: data.platform, + type: data.type, + }) - if (existingIdentities.length > 0) { + if (conflict) { throw new Error409( this.options.language, 'errors.alreadyExists', // @ts-ignore JSON.stringify({ - memberId: existingIdentities[0].memberId, + memberId: conflict.memberId, }), ) } @@ -132,20 +131,19 @@ export default class MemberIdentityService extends LoggerBase { // Check if any of the identities already exist for (const identity of data) { - const existingIdentities = await checkMemberIdentityExistence( - qx, - identity.value, - identity.platform, - identity.type, - ) + const conflict = await findMemberIdentityConflict(qx, { + value: identity.value, + platform: identity.platform, + type: identity.type, + }) - if (existingIdentities.length > 0) { + if (conflict) { throw new Error409( this.options.language, 'errors.alreadyExists', // @ts-ignore JSON.stringify({ - memberId: existingIdentities[0].memberId, + memberId: conflict.memberId, }), ) } @@ -215,8 +213,13 @@ export default class MemberIdentityService extends LoggerBase { const platform = data.platform ?? currentIdentity.platform const type = data.type ?? currentIdentity.type - const existingIdentities = await checkMemberIdentityExistence(qx, value, platform, type) - const conflict = existingIdentities.find((identity) => identity.memberId !== memberId) + const conflict = await findMemberIdentityConflict(qx, { + value, + platform, + type, + excludeMemberId: memberId, + }) + if (conflict) { throw new Error409( this.options.language, diff --git a/services/libs/data-access-layer/src/members/identities.ts b/services/libs/data-access-layer/src/members/identities.ts index 5723bd541b..10a2280837 100644 --- a/services/libs/data-access-layer/src/members/identities.ts +++ b/services/libs/data-access-layer/src/members/identities.ts @@ -49,26 +49,29 @@ export async function fetchManyMemberIdentities( ) } -export async function checkMemberIdentityExistence( +interface FindMemberIdentityConflictParams { + value: IMemberIdentity['value'] + platform: IMemberIdentity['platform'] + type: IMemberIdentity['type'] + excludeMemberId?: string +} + +export async function findMemberIdentityConflict( qx: QueryExecutor, - value: string, - platform: string, - type: MemberIdentityType, -): Promise { - return qx.select( + params: FindMemberIdentityConflictParams, +): Promise<{ id: string; memberId: string } | null> { + return qx.selectOneOrNone( ` SELECT id, "memberId" FROM "memberIdentities" WHERE platform = $(platform) AND type = $(type) AND lower(value) = lower($(value)) - AND "deletedAt" IS NULL; + AND "deletedAt" IS NULL + ${params.excludeMemberId ? 'AND "memberId" <> $(excludeMemberId)' : ''} + LIMIT 1; `, - { - value, - platform, - type, - }, + params, ) }