This commit is contained in:
pandeymangg
2025-03-28 14:13:54 +05:30
parent 0c6c554cef
commit d05a7c6d98
2 changed files with 125 additions and 117 deletions

View File

@@ -24,48 +24,72 @@ export const upsertBulkContacts = async (
const emailAttributeKey = "email";
const contactIdxWithConflictingUserIds: number[] = [];
const userIdsInContacts = contacts.flatMap((contact) =>
contact.attributes.filter((attr) => attr.attributeKey.key === "userId").map((attr) => attr.value)
);
let userIdsInContacts: string[] = [];
let attributeKeysSet: Set<string> = new Set();
let attributeKeys: string[] = [];
const existingUserIds = await prisma.contactAttribute.findMany({
where: {
attributeKey: {
// both can be done with a single loop:
contacts.forEach((contact) => {
contact.attributes.forEach((attr) => {
if (attr.attributeKey.key === "userId") {
userIdsInContacts.push(attr.value);
}
if (!attributeKeysSet.has(attr.attributeKey.key)) {
attributeKeys.push(attr.attributeKey.key);
}
// Add the attribute key to the set
attributeKeysSet.add(attr.attributeKey.key);
});
});
const [existingUserIds, existingContactsByEmail, existingAttributeKeys] = await Promise.all([
prisma.contactAttribute.findMany({
where: {
attributeKey: {
environmentId,
key: "userId",
},
value: {
in: userIdsInContacts,
},
},
select: {
value: true,
},
}),
prisma.contact.findMany({
where: {
environmentId,
key: "userId",
attributes: {
some: {
attributeKey: { key: emailAttributeKey },
value: { in: parsedEmails },
},
},
},
value: {
in: userIdsInContacts,
select: {
attributes: {
select: {
attributeKey: { select: { key: true } },
createdAt: true,
id: true,
value: true,
},
},
id: true,
},
},
select: {
value: true,
},
});
}),
// Find existing contacts by matching email attribute
const existingContactsByEmail = await prisma.contact.findMany({
where: {
environmentId,
attributes: {
some: {
attributeKey: { key: emailAttributeKey },
value: { in: parsedEmails },
},
prisma.contactAttributeKey.findMany({
where: {
key: { in: attributeKeys },
environmentId,
},
},
select: {
attributes: {
select: {
attributeKey: { select: { key: true } },
createdAt: true,
id: true,
value: true,
},
},
id: true,
},
});
}),
]);
// Build a map from email to contact id (if the email attribute exists)
const contactMap = new Map<
@@ -92,19 +116,6 @@ export const upsertBulkContacts = async (
}
});
// Get unique attribute keys from the payload
const attributeKeys = Array.from(
new Set(contacts.flatMap((contact) => contact.attributes.map((attr) => attr.attributeKey.key)))
);
// Fetch attribute key records for these keys in this environment
const existingAttributeKeys = await prisma.contactAttributeKey.findMany({
where: {
key: { in: attributeKeys },
environmentId,
},
});
// Split contacts into ones to update and ones to create
const contactsToUpdate: {
contactId: string;
@@ -127,6 +138,8 @@ export const upsertBulkContacts = async (
}[];
}[] = [];
let filteredContacts: TContactBulkUploadContact[] = [];
contacts.forEach((contact, idx) => {
const emailAttr = contact.attributes.find((attr) => attr.attributeKey.key === emailAttributeKey);
@@ -139,21 +152,20 @@ export const upsertBulkContacts = async (
existingContact.attributes.map((attr) => [attr.attributeKey.key, attr.value])
);
// Check if any attributes need updating
const needsUpdate = contact.attributes.some(
(attr) => existingAttributesByKey.get(attr.attributeKey.key) !== attr.value
);
if (!needsUpdate) {
// No attributes need to be updated
return;
}
// which attributes need to be updated?
// Determine which attributes need updating by comparing values.
const attributesToUpdate = contact.attributes.filter(
(attr) => existingAttributesByKey.get(attr.attributeKey.key) !== attr.value
);
// Check if any attributes need updating
const needsUpdate = attributesToUpdate.length > 0;
if (!needsUpdate) {
filteredContacts.push(contact);
// No attributes need to be updated
return;
}
// if the attributes to update have a userId that exists in the db, we need to skip the update
const userIdAttr = attributesToUpdate.find((attr) => attr.attributeKey.key === "userId");
@@ -168,6 +180,7 @@ export const upsertBulkContacts = async (
}
}
filteredContacts.push(contact);
contactsToUpdate.push({
contactId: existingContact.contactId,
attributes: attributesToUpdate.map((attr) => {
@@ -209,12 +222,11 @@ export const upsertBulkContacts = async (
}
}
filteredContacts.push(contact);
contactsToCreate.push(contact);
}
});
const filteredContacts = contacts.filter((_, idx) => !contactIdxWithConflictingUserIds.includes(idx));
try {
// Execute everything in ONE transaction
await prisma.$transaction(async (tx) => {

View File

@@ -131,55 +131,71 @@ export const ZContactBulkUploadRequest = z.object({
.array(ZContactBulkUploadContact)
.max(1000, { message: "Maximum 1000 contacts allowed at a time." })
.superRefine((contacts, ctx) => {
// every contact must have an email attribute
// Track all data in a single pass
const seenEmails = new Set<string>();
const duplicateEmails = new Set<string>();
const seenUserIds = new Set<string>();
const duplicateUserIds = new Set<string>();
const contactsWithDuplicateKeys: { idx: number; duplicateKeys: string[] }[] = [];
// Process each contact in a single pass
contacts.forEach((contact, idx) => {
const email = contact.attributes.find((attr) => attr.attributeKey.key === "email");
if (!email?.value) {
// 1. Check email existence and validity
const emailAttr = contact.attributes.find((attr) => attr.attributeKey.key === "email");
if (!emailAttr?.value) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Missing email attribute for contact at index ${idx}`,
});
}
if (email?.value) {
// parse the email:
const parsedEmail = z.string().email().safeParse(email.value);
} else {
// Check email format
const parsedEmail = z.string().email().safeParse(emailAttr.value);
if (!parsedEmail.success) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Invalid email for contact at index ${idx}`,
});
}
// Check for duplicate emails
if (seenEmails.has(emailAttr.value)) {
duplicateEmails.add(emailAttr.value);
} else {
seenEmails.add(emailAttr.value);
}
}
// 2. Check for userId duplicates
const userIdAttr = contact.attributes.find((attr) => attr.attributeKey.key === "userId");
if (userIdAttr?.value) {
if (seenUserIds.has(userIdAttr.value)) {
duplicateUserIds.add(userIdAttr.value);
} else {
seenUserIds.add(userIdAttr.value);
}
}
// 3. Check for duplicate attribute keys within the same contact
const keyOccurrences = new Map<string, number>();
const duplicateKeysForContact: string[] = [];
contact.attributes.forEach((attr) => {
const key = attr.attributeKey.key;
const count = (keyOccurrences.get(key) || 0) + 1;
keyOccurrences.set(key, count);
// If this is the second occurrence, add to duplicates
if (count === 2) {
duplicateKeysForContact.push(key);
}
});
if (duplicateKeysForContact.length > 0) {
contactsWithDuplicateKeys.push({ idx, duplicateKeys: duplicateKeysForContact });
}
});
const seenEmails = new Set<string>();
const duplicateEmails = new Set<string>();
const seenUserIds = new Set<string>();
const duplicateUserIds = new Set<string>();
for (const contact of contacts) {
const email = contact.attributes.find((attr) => attr.attributeKey.key === "email")?.value;
const userId = contact.attributes.find((attr) => attr.attributeKey.key === "userId")?.value;
if (email) {
if (seenEmails.has(email)) {
duplicateEmails.add(email);
} else {
seenEmails.add(email);
}
}
if (userId) {
if (seenUserIds.has(userId)) {
duplicateUserIds.add(userId);
} else {
seenUserIds.add(userId);
}
}
}
// Report all validation issues after the single pass
if (duplicateEmails.size > 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
@@ -190,7 +206,6 @@ export const ZContactBulkUploadRequest = z.object({
});
}
// if userId is present, check for duplicate userIds
if (duplicateUserIds.size > 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
@@ -201,25 +216,6 @@ export const ZContactBulkUploadRequest = z.object({
});
}
const contactsWithDuplicateKeys = contacts
.map((contact, idx) => {
// Count how many times each attribute key appears
const keyCounts = contact.attributes.reduce<Record<string, number>>((acc, attr) => {
const key = attr.attributeKey.key;
acc[key] = (acc[key] || 0) + 1;
return acc;
}, {});
// Find attribute keys that appear more than once
const duplicateKeys = Object.entries(keyCounts)
.filter(([_, count]) => count > 1)
.map(([key]) => key);
return { idx, duplicateKeys };
})
// Only keep contacts that have at least one duplicate key
.filter(({ duplicateKeys }) => duplicateKeys.length > 0);
if (contactsWithDuplicateKeys.length > 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,