Skip to content

Commit 71c6c8f

Browse files
committed
fix: no longer we depend on postgres error details to decode problematic identity
Signed-off-by: Uroš Marolt <uros@marolt.me>
1 parent 2366cf4 commit 71c6c8f

2 files changed

Lines changed: 89 additions & 75 deletions

File tree

services/apps/data_sink_worker/src/service/activity.service.ts

Lines changed: 42 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1708,56 +1708,55 @@ export default class ActivityService extends LoggerBase {
17081708
return false
17091709
}
17101710

1711-
const extractMetadata = async (
1712-
error: any,
1713-
): Promise<string | Record<string, unknown> | undefined> => {
1711+
const extractMetadata = async (): Promise<string | Record<string, unknown> | undefined> => {
17141712
const metadata: Record<string, unknown> = {}
17151713

1716-
// extract the platform, value, type from the detail
1717-
const detail = error.detail
1718-
const regex = /\(platform, value, type\)=\((.*?)\)/
1719-
const match = detail.match(regex)
1714+
const incomingIdentities =
1715+
memberType === 'member'
1716+
? payload.activity.member.identities
1717+
: payload.activity.objectMember.identities
1718+
const verifiedIncoming = incomingIdentities.filter((i) => i.verified)
17201719

1721-
if (!match || match.length < 2) {
1722-
return
1723-
}
1724-
1725-
// Split the matched string by commas
1726-
const values = match[1].split(',').map((val) => val.trim())
1720+
metadata.verifiedIdentities = verifiedIncoming
17271721

1728-
// Extract platform, value, and type
1729-
const [platform, value, type] = values
1730-
1731-
metadata.erroredVerifiedIdentity = {
1732-
platform,
1733-
value,
1734-
type,
1722+
if (verifiedIncoming.length === 0) {
1723+
return undefined
17351724
}
17361725

1737-
const membersWithIdentity = await findMembersByIdentities(
1738-
this.pgQx,
1739-
[
1740-
{
1741-
platform,
1742-
value,
1743-
type,
1744-
verified: true,
1745-
} as IMemberIdentity,
1746-
],
1747-
undefined,
1748-
true,
1749-
)
1726+
// Use the structured identities array to find the owner — avoids fragile Postgres
1727+
// Detail text parsing (format not stable; breaks if value contains a comma).
1728+
const owners = await findMembersByIdentities(this.pgQx, verifiedIncoming, undefined, true)
1729+
1730+
// Map keys are `${platform}:${type}:${value}` (from db rows). Match case-insensitively.
1731+
let conflictIdentity: IMemberIdentity | undefined
1732+
let ownerId: string | undefined
1733+
outer: for (const id of verifiedIncoming) {
1734+
for (const [key, oid] of owners) {
1735+
const sep1 = key.indexOf(':')
1736+
const sep2 = key.indexOf(':', sep1 + 1)
1737+
if (sep1 < 0 || sep2 < 0) continue
1738+
if (
1739+
key.slice(0, sep1) === id.platform &&
1740+
key.slice(sep1 + 1, sep2) === id.type &&
1741+
key.slice(sep2 + 1).toLowerCase() === id.value.toLowerCase()
1742+
) {
1743+
conflictIdentity = id
1744+
ownerId = oid
1745+
break outer
1746+
}
1747+
}
1748+
}
17501749

1751-
if (memberType === 'member') {
1752-
metadata.verifiedIdentities = payload.activity.member.identities.filter((i) => i.verified)
1753-
} else {
1754-
metadata.verifiedIdentities = payload.activity.objectMember.identities.filter(
1755-
(i) => i.verified,
1756-
)
1750+
if (conflictIdentity) {
1751+
metadata.erroredVerifiedIdentity = {
1752+
platform: conflictIdentity.platform,
1753+
value: conflictIdentity.value,
1754+
type: conflictIdentity.type,
1755+
}
17571756
}
17581757

1759-
if (membersWithIdentity.size > 0) {
1760-
metadata.memberWithIdentity = membersWithIdentity.values().next().value
1758+
if (ownerId) {
1759+
metadata.memberWithIdentity = ownerId
17611760
}
17621761

17631762
if (dbMember) {
@@ -1860,15 +1859,15 @@ export default class ActivityService extends LoggerBase {
18601859

18611860
while (nextError) {
18621861
if (checkForIdentityConstraint(nextError)) {
1863-
return extractMetadata(nextError)
1862+
return extractMetadata()
18641863
} else if (nextError instanceof ApplicationError) {
18651864
nextError = nextError.originalError
18661865
} else {
18671866
nextError = undefined
18681867
}
18691868
}
18701869
} else if (checkForIdentityConstraint(error)) {
1871-
return extractMetadata(error)
1870+
return extractMetadata()
18721871
}
18731872

18741873
return undefined

services/apps/data_sink_worker/src/service/member.service.ts

Lines changed: 47 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
import { QueryExecutor, createMember, dbStoreQx, updateMember } from '@crowd/data-access-layer'
2222
import {
2323
findIdentitiesForMembers,
24-
findMemberIdByVerifiedIdentity,
24+
findMembersByIdentities,
2525
findMembersByVerifiedUsernames,
2626
} from '@crowd/data-access-layer'
2727
import { DbStore } from '@crowd/data-access-layer/src/database'
@@ -137,44 +137,59 @@ export default class MemberService extends LoggerBase {
137137
} catch (err) {
138138
if (
139139
!err?.constraint ||
140-
err.constraint !== 'uix_memberIdentities_platform_value_type_verified' ||
141-
!err.detail
140+
err.constraint !== 'uix_memberIdentities_platform_value_type_verified'
142141
) {
143142
throw err
144143
}
145-
const match = (err.detail as string).match(/\(platform, value, type\)=\((.*?)\)/)
146-
if (!match) throw err
147-
const [platform, value, type] = match[1].split(',').map((s: string) => s.trim())
148-
149-
const owner = await findMemberIdByVerifiedIdentity(
150-
this.pgQx,
151-
platform,
152-
value,
153-
type as MemberIdentityType,
154-
)
155144

156-
if (owner && owner !== memberId) {
157-
if (!attemptMerge) {
158-
return owner
145+
const verifiedIncoming = identities.filter((i) => i.verified)
146+
if (verifiedIncoming.length === 0) throw err
147+
148+
// Use the structured identities array to find the owner — avoids fragile Postgres
149+
// Detail text parsing (format not stable; breaks if value contains a comma).
150+
const owners = await findMembersByIdentities(this.pgQx, verifiedIncoming, undefined, true)
151+
152+
// Map keys are `${platform}:${type}:${value}` (from db rows). Match case-insensitively.
153+
let owner: string | undefined
154+
outer: for (const id of verifiedIncoming) {
155+
for (const [key, ownerId] of owners) {
156+
const sep1 = key.indexOf(':')
157+
const sep2 = key.indexOf(':', sep1 + 1)
158+
if (sep1 < 0 || sep2 < 0) continue
159+
if (
160+
key.slice(0, sep1) === id.platform &&
161+
key.slice(sep1 + 1, sep2) === id.type &&
162+
key.slice(sep2 + 1).toLowerCase() === id.value.toLowerCase() &&
163+
ownerId !== memberId
164+
) {
165+
owner = ownerId
166+
break outer
167+
}
159168
}
169+
}
160170

161-
let merged: boolean
162-
try {
163-
merged = await mergeIfAllowed(this.pgQx, this.temporal, this.log, owner, memberId)
164-
} catch (mergeErr) {
165-
// Re-throw the original constraint error, not the merge error. If we let the merge
166-
// error propagate, handleMemberIdentityError's checkForIdentityConstraint would
167-
// return false and no metadata would be stored in integration.results.error.
168-
// Re-throwing the constraint error lets handleMemberIdentityError retry the merge.
169-
this.log.warn(
170-
mergeErr,
171-
{ memberId, owner, platform, value, type },
172-
'merge threw during identity conflict — re-throwing constraint error for retry',
173-
)
174-
throw err
175-
}
176-
if (merged) return owner
171+
if (!owner) throw err
172+
173+
if (!attemptMerge) {
174+
return owner
175+
}
176+
177+
let merged: boolean
178+
try {
179+
merged = await mergeIfAllowed(this.pgQx, this.temporal, this.log, owner, memberId)
180+
} catch (mergeErr) {
181+
// Re-throw the original constraint error, not the merge error. If we let the merge
182+
// error propagate, handleMemberIdentityError's checkForIdentityConstraint would
183+
// return false and no metadata would be stored in integration.results.error.
184+
// Re-throwing the constraint error lets handleMemberIdentityError retry the merge.
185+
this.log.warn(
186+
mergeErr,
187+
{ memberId, owner },
188+
'merge threw during identity conflict — re-throwing constraint error for retry',
189+
)
190+
throw err
177191
}
192+
if (merged) return owner
178193

179194
// noMerge, owner not found, or stale prefetch (owner === memberId):
180195
// Re-throw original constraint DatabaseError. handleMemberIdentityError will call

0 commit comments

Comments
 (0)