mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-17 11:31:09 -05:00
perf
This commit is contained in:
@@ -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) => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user