Compare commits

...

3 Commits

7 changed files with 89 additions and 108 deletions

View File

@@ -682,10 +682,18 @@ export const createContactsFromCSV = async (
environmentId,
};
const contactPromises = csvData.map((record) => processCsvRecord(record, processingContext));
const CHUNK_SIZE = 50;
const allResults: (TContact | null)[] = [];
const results = await Promise.all(contactPromises);
return { contacts: results.filter((contact): contact is TContact => contact !== null) };
for (let i = 0; i < csvData.length; i += CHUNK_SIZE) {
const chunk = csvData.slice(i, i + CHUNK_SIZE);
const chunkResults = await Promise.all(
chunk.map((record) => processCsvRecord(record, processingContext))
);
allResults.push(...chunkResults);
}
return { contacts: allResults.filter((contact): contact is TContact => contact !== null) };
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);

View File

@@ -106,24 +106,23 @@ export const OrganizationActions = ({
toast.error(errorMessage);
}
} else {
const invitePromises = await Promise.all(
data.map(async ({ name, email, role, teamIds }) => {
const inviteUserActionResult = await inviteUserAction({
organizationId: organization.id,
email: email.toLowerCase(),
name,
role,
teamIds,
});
return {
email,
success: Boolean(inviteUserActionResult?.data),
};
})
);
let failedInvites: string[] = [];
let successInvites: string[] = [];
invitePromises.forEach((invite) => {
const inviteResults: { email: string; success: boolean }[] = [];
for (const { name, email, role, teamIds } of data) {
const inviteUserActionResult = await inviteUserAction({
organizationId: organization.id,
email: email.toLowerCase(),
name,
role,
teamIds,
});
inviteResults.push({
email,
success: Boolean(inviteUserActionResult?.data),
});
}
const failedInvites: string[] = [];
const successInvites: string[] = [];
inviteResults.forEach((invite) => {
if (!invite.success) {
failedInvites.push(invite.email);
} else {

View File

@@ -152,13 +152,13 @@ describe("tag lib", () => {
.mockResolvedValueOnce(baseTag as any)
.mockResolvedValueOnce(newTag as any);
vi.mocked(prisma.response.findMany).mockResolvedValueOnce([{ id: "resp1" }] as any);
vi.mocked(prisma.$transaction).mockResolvedValueOnce(undefined).mockResolvedValueOnce(undefined);
vi.mocked(prisma.$transaction).mockResolvedValueOnce(undefined);
const result = await mergeTags(baseTag.id, newTag.id);
expect(result).toEqual(ok(newTag));
expect(prisma.tag.findUnique).toHaveBeenCalledWith({ where: { id: baseTag.id } });
expect(prisma.tag.findUnique).toHaveBeenCalledWith({ where: { id: newTag.id } });
expect(prisma.response.findMany).toHaveBeenCalled();
expect(prisma.$transaction).toHaveBeenCalledTimes(2);
expect(prisma.$transaction).toHaveBeenCalledTimes(1);
});
test("merges tags with no responses with both tags", async () => {
vi.mocked(prisma.tag.findUnique)
@@ -195,6 +195,20 @@ describe("tag lib", () => {
});
}
});
test("returns error when merging a tag into itself", async () => {
const result = await mergeTags(baseTag.id, baseTag.id);
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error).toStrictEqual({
code: "merge_same_tag",
message: "Cannot merge a tag into itself",
});
}
expect(prisma.tag.findUnique).not.toHaveBeenCalled();
expect(prisma.$transaction).not.toHaveBeenCalled();
});
test("throws on prisma error", async () => {
vi.mocked(prisma.tag.findUnique).mockRejectedValueOnce(new Error("fail"));
const result = await mergeTags(baseTag.id, newTag.id);

View File

@@ -72,6 +72,13 @@ export const mergeTags = async (
): Promise<Result<TTag, { code: TagError; message: string; meta?: Record<string, string> }>> => {
validateInputs([originalTagId, ZId], [newTagId, ZId]);
if (originalTagId === newTagId) {
return err({
code: TagError.MERGE_SAME_TAG,
message: "Cannot merge a tag into itself",
});
}
try {
let originalTag: TTag | null;
@@ -103,90 +110,35 @@ export const mergeTags = async (
});
}
// finds all the responses that have both the tags
let responsesWithBothTags = await prisma.response.findMany({
// Find responses that have both tags to avoid unique constraint violations during merge
const responsesWithBothTags = await prisma.response.findMany({
where: {
AND: [
{
tags: {
some: {
tagId: {
in: [originalTagId],
},
},
},
},
{
tags: {
some: {
tagId: {
in: [newTagId],
},
},
},
},
],
AND: [{ tags: { some: { tagId: originalTagId } } }, { tags: { some: { tagId: newTagId } } }],
},
select: { id: true },
});
if (!!responsesWithBothTags?.length) {
await Promise.all(
responsesWithBothTags.map(async (response) => {
await prisma.$transaction([
prisma.tagsOnResponses.deleteMany({
where: {
responseId: response.id,
tagId: {
in: [originalTagId, newTagId],
},
},
}),
prisma.tagsOnResponses.create({
data: {
responseId: response.id,
tagId: newTagId,
},
}),
]);
})
);
await prisma.$transaction([
prisma.tagsOnResponses.updateMany({
where: {
tagId: originalTagId,
},
data: {
tagId: newTagId,
},
}),
prisma.tag.delete({
where: {
id: originalTagId,
},
}),
]);
return ok(newTag);
}
const conflictResponseIds = responsesWithBothTags.map((r) => r.id);
await prisma.$transaction([
// Remove originalTag from responses that already have newTag (prevents unique constraint violation)
...(conflictResponseIds.length > 0
? [
prisma.tagsOnResponses.deleteMany({
where: {
responseId: { in: conflictResponseIds },
tagId: originalTagId,
},
}),
]
: []),
// Move all remaining originalTag associations to newTag
prisma.tagsOnResponses.updateMany({
where: {
tagId: originalTagId,
},
data: {
tagId: newTagId,
},
}),
prisma.tag.delete({
where: {
id: originalTagId,
},
where: { tagId: originalTagId },
data: { tagId: newTagId },
}),
// Delete the original tag
prisma.tag.delete({ where: { id: originalTagId } }),
]);
return ok(newTag);

View File

@@ -1,5 +1,6 @@
export enum TagError {
TAG_NOT_FOUND = "tag_not_found",
TAG_NAME_ALREADY_EXISTS = "tag_name_already_exists",
MERGE_SAME_TAG = "merge_same_tag",
UNEXPECTED_ERROR = "unexpected_error",
}

View File

@@ -128,10 +128,6 @@ export const CopySurveyForm = ({ defaultProjects, survey, onCancel, setOpen }: C
: project?.environments[1];
return {
operation: copySurveyToOtherEnvironmentAction({
surveyId: survey.id,
targetEnvironmentId: environmentId,
}),
projectName: project?.name ?? "Unknown Project",
environmentType: environment?.type ?? "unknown",
environmentId,
@@ -139,7 +135,14 @@ export const CopySurveyForm = ({ defaultProjects, survey, onCancel, setOpen }: C
});
});
const results = await Promise.all(copyOperationsWithMetadata.map((item) => item.operation));
const results: Awaited<ReturnType<typeof copySurveyToOtherEnvironmentAction>>[] = [];
for (const item of copyOperationsWithMetadata) {
const result = await copySurveyToOtherEnvironmentAction({
surveyId: survey.id,
targetEnvironmentId: item.environmentId,
});
results.push(result);
}
let successCount = 0;
let errorCount = 0;

View File

@@ -66,10 +66,14 @@ export const SelectedRowSettings = <T,>({
setIsDeleting(true);
const rowsToBeDeleted = table.getFilteredSelectedRowModel().rows.map((row) => row.id);
if (type === "response") {
await Promise.all(rowsToBeDeleted.map((rowId) => deleteAction(rowId, { decrementQuotas })));
} else {
await Promise.all(rowsToBeDeleted.map((rowId) => deleteAction(rowId)));
const CHUNK_SIZE = 5;
for (let i = 0; i < rowsToBeDeleted.length; i += CHUNK_SIZE) {
const chunk = rowsToBeDeleted.slice(i, i + CHUNK_SIZE);
if (type === "response") {
await Promise.all(chunk.map((rowId) => deleteAction(rowId, { decrementQuotas })));
} else {
await Promise.all(chunk.map((rowId) => deleteAction(rowId)));
}
}
// Update the row list UI