fix: surveys with no segment fixes (#2903)

Co-authored-by: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com>
This commit is contained in:
Anshuman Pandey
2024-07-31 15:34:56 +05:30
committed by GitHub
parent 5ef61f4621
commit 2e4317a80c
7 changed files with 134 additions and 49 deletions

View File

@@ -0,0 +1,66 @@
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
async function main() {
await prisma.$transaction(
async (tx) => {
console.log("starting migration");
const segmentsWithNoSurveys = await tx.segment.findMany({
where: {
surveys: {
none: {},
},
},
});
const surveyIds = segmentsWithNoSurveys.map((segment) => segment.id);
await tx.segment.deleteMany({
where: {
id: {
in: surveyIds,
},
},
});
console.log(`Deleted ${segmentsWithNoSurveys.length} segments with no surveys`);
const appSurveysWithoutSegment = await tx.survey.findMany({
where: {
type: "app",
segmentId: null,
},
});
console.log(`Found ${appSurveysWithoutSegment.length} app surveys without a segment`);
const segmentPromises = [];
for (const appSurvey of appSurveysWithoutSegment) {
// create a new private segment for each app survey
segmentPromises.push(
tx.segment.create({
data: {
title: appSurvey.id,
isPrivate: true,
environment: { connect: { id: appSurvey.environmentId } },
surveys: { connect: { id: appSurvey.id } },
},
})
);
}
await Promise.all(segmentPromises);
},
{ timeout: 50000 }
);
}
main()
.catch(async (e) => {
console.error(e);
process.exit(1);
})
.finally(async () => await prisma.$disconnect());

View File

@@ -40,7 +40,8 @@
"data-migration:product-config": "ts-node ./data-migrations/20240612115151_adds_product_config/data-migration.ts",
"data-migration:v2.2": "pnpm data-migration:adds_app_and_website_status_indicator && pnpm data-migration:product-config && pnpm data-migration:pricing-v2",
"data-migration:zh-to-zh-Hans": "ts-node ./data-migrations/20240625101352_update_zh_to_zh-Hans/data-migration.ts",
"data-migration:v2.3": "pnpm data-migration:zh-to-zh-Hans"
"data-migration:v2.3": "pnpm data-migration:zh-to-zh-Hans",
"data-migration:segments-cleanup": "ts-node ./data-migrations/20240712123456_segments_cleanup/data-migration.ts"
},
"dependencies": {
"@prisma/client": "^5.16.2",

View File

@@ -3,7 +3,12 @@ import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { ZString } from "@formbricks/types/common";
import { ZId } from "@formbricks/types/environment";
import { DatabaseError, ResourceNotFoundError, ValidationError } from "@formbricks/types/errors";
import {
DatabaseError,
OperationNotAllowedError,
ResourceNotFoundError,
ValidationError,
} from "@formbricks/types/errors";
import {
TActionMetric,
TAllOperators,
@@ -236,6 +241,10 @@ export const deleteSegment = async (segmentId: string): Promise<TSegment> => {
throw new ResourceNotFoundError("segment", segmentId);
}
if (currentSegment.surveys?.length) {
throw new OperationNotAllowedError("Cannot delete a segment that is associated with a survey");
}
const segment = await prisma.segment.delete({
where: {
id: segmentId,
@@ -243,20 +252,6 @@ export const deleteSegment = async (segmentId: string): Promise<TSegment> => {
select: selectSegment,
});
// pause all the running surveys that are using this segment
const surveyIds = segment.surveys.map((survey) => survey.id);
if (!!surveyIds?.length) {
await prisma.survey.updateMany({
where: {
id: { in: surveyIds },
status: "inProgress",
},
data: {
status: "paused",
},
});
}
segmentCache.revalidate({ id: segmentId, environmentId: segment.environmentId });
segment.surveys.map((survey) => surveyCache.revalidate({ id: survey.id }));

View File

@@ -10,6 +10,7 @@ import {
} from "@formbricks/types/segment";
export const mockSegmentId = "rh2eual2apby2bx0r027ru70";
export const mockDeleteSegmentId = "to336z1uth9cvyb1sh7k9i77";
export const mockEnvironmentId = "t7fszh4tsotoe87ppa6lqhie";
export const mockSurveyId = "phz5mjwvatwc0dqwuip90qpv";
export const mockFilterGroupId = "wi6zz4ekmcwi08bhv1hmgqcr";
@@ -164,6 +165,18 @@ export const mockSegmentPrisma = {
surveys: [{ id: mockSurveyId }],
};
export const mockDeleteSegmentPrisma = {
...mockSegmentPrisma,
id: mockDeleteSegmentId,
surveys: [],
};
export const mockDeleteSegment = {
...mockSegment,
id: mockDeleteSegmentId,
surveys: [],
};
export const mockSegmentActiveInactiveSurves = {
activeSurveys: ["Churn Survey"],
inactiveSurveys: ["NPS Survey"],

View File

@@ -1,6 +1,9 @@
import { prisma } from "../../__mocks__/database";
import {
getMockSegmentFilters,
mockDeleteSegment,
mockDeleteSegmentId,
mockDeleteSegmentPrisma,
mockEnvironmentId,
mockEvaluateSegmentUserData,
mockSegment,
@@ -13,7 +16,7 @@ import {
import { Prisma } from "@prisma/client";
import { beforeEach, describe, expect, it } from "vitest";
import { testInputValidation } from "vitestSetup";
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
import { DatabaseError, OperationNotAllowedError, ResourceNotFoundError } from "@formbricks/types/errors";
import {
cloneSegment,
createSegment,
@@ -255,9 +258,10 @@ describe("Tests for updateSegment service", () => {
describe("Tests for deleteSegment service", () => {
describe("Happy Path", () => {
it("Deletes a user segment", async () => {
prisma.segment.delete.mockResolvedValue(mockSegmentPrisma);
const result = await deleteSegment(mockSegmentId);
expect(result).toEqual(mockSegment);
prisma.segment.findUnique.mockResolvedValue(mockDeleteSegmentPrisma);
prisma.segment.delete.mockResolvedValue(mockDeleteSegmentPrisma);
const result = await deleteSegment(mockDeleteSegmentId);
expect(result).toEqual(mockDeleteSegment);
});
});
@@ -266,7 +270,7 @@ describe("Tests for deleteSegment service", () => {
it("Throws a ResourceNotFoundError error if the user segment does not exist", async () => {
prisma.segment.findUnique.mockResolvedValue(null);
await expect(deleteSegment(mockSegmentId)).rejects.toThrow(ResourceNotFoundError);
await expect(deleteSegment(mockDeleteSegmentId)).rejects.toThrow(ResourceNotFoundError);
});
it("Throws a DatabaseError error if there is a PrismaClientKnownRequestError", async () => {
@@ -276,16 +280,21 @@ describe("Tests for deleteSegment service", () => {
clientVersion: "0.0.1",
});
prisma.segment.findUnique.mockResolvedValue(mockDeleteSegmentPrisma);
prisma.segment.delete.mockRejectedValue(errToThrow);
await expect(deleteSegment(mockSegmentId)).rejects.toThrow(DatabaseError);
await expect(deleteSegment(mockDeleteSegmentId)).rejects.toThrow(DatabaseError);
});
it("Throws an OperationNotAllowedError if the segment is associated with a survey", async () => {
await expect(deleteSegment(mockSegmentId)).rejects.toThrow(OperationNotAllowedError);
});
it("Throws a generic Error for unexpected exceptions", async () => {
const mockErrorMessage = "Mock error message";
prisma.segment.delete.mockRejectedValue(new Error(mockErrorMessage));
await expect(deleteSegment(mockSegmentId)).rejects.toThrow(Error);
await expect(deleteSegment(mockDeleteSegmentId)).rejects.toThrow(Error);
});
});
});

View File

@@ -1068,8 +1068,10 @@ export const loadNewSegmentInSurvey = async (surveyId: string, newSegmentId: str
throw new ResourceNotFoundError("survey", surveyId);
}
const currentSegment = await getSegment(newSegmentId);
if (!currentSegment) {
const currentSurveySegment = currentSurvey.segment;
const newSegment = await getSegment(newSegmentId);
if (!newSegment) {
throw new ResourceNotFoundError("segment", newSegmentId);
}
@@ -1087,6 +1089,14 @@ export const loadNewSegmentInSurvey = async (surveyId: string, newSegmentId: str
},
});
if (
currentSurveySegment &&
currentSurveySegment.isPrivate &&
currentSurveySegment.title === currentSurvey.id
) {
await deleteSegment(currentSurveySegment.id);
}
segmentCache.revalidate({ id: newSegmentId });
surveyCache.revalidate({ id: surveyId });

View File

@@ -29,38 +29,29 @@ export const ConfirmDeleteSegmentModal = ({
<div className="text-slate-900">
{segmentHasSurveys && (
<div className="space-y-2">
<p>If you delete this segment, this will happen:</p>
<ul className="ml-4 list-disc">
<li>
This segment will be <b>removed</b> from these surveys:
<ol className="my-2 ml-4 list-decimal text-sm">
{segment.activeSurveys.map((survey) => (
<li key={survey}>{survey}</li>
))}
{segment.inactiveSurveys.map((survey) => (
<li key={survey}>{survey}</li>
))}
</ol>
</li>
<li>
These surveys will be <b>paused.</b>
</li>
</ul>
<p>You cannot delete this segment since its still used in these surveys:</p>
<ol className="my-2 ml-4 list-decimal">
{segment.activeSurveys.map((survey) => (
<li key={survey}>{survey}</li>
))}
{segment.inactiveSurveys.map((survey) => (
<li key={survey}>{survey}</li>
))}
</ol>
</div>
)}
<p className="mt-2">This action cannot be undone.</p>
<p className="mt-2">
{segmentHasSurveys
? "Please remove the segment from these surveys in order to delete it."
: "Are you sure you want to delete this segment? This action cannot be undone."}
</p>
</div>
<div className="mt-4 space-x-2 text-right">
<Button variant="minimal" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button
variant="warn"
onClick={() => {
handleDelete();
}}>
<Button variant="warn" onClick={handleDelete} disabled={segmentHasSurveys}>
Delete
</Button>
</div>