fix: adds data migration script

This commit is contained in:
Piyush Gupta
2024-08-30 16:41:15 +05:30
parent b20ce46a7b
commit 0a8c5e384d
7 changed files with 198 additions and 88 deletions

View File

@@ -133,7 +133,7 @@ export function AdvancedLogicEditorConditions({
{connector}
</div>
)}
<div className="mt-2 rounded-lg border border-slate-200 bg-slate-100 p-4">
<div className="rounded-lg border border-slate-200 bg-slate-100 p-4">
<AdvancedLogicEditorConditions
conditions={condition}
updateQuestion={updateQuestion}
@@ -184,7 +184,7 @@ export function AdvancedLogicEditorConditions({
const { show, options, showInput = false, inputType } = getMatchValueProps(condition, localSurvey);
return (
<div key={condition.id} className="mt-2 flex items-center justify-between gap-4">
<div key={condition.id} className="flex items-center justify-between gap-4">
{index === 0 ? (
<div className="text-sm">When</div>
) : (

View File

@@ -292,7 +292,7 @@ export const getMatchValueProps = (
label: getLocalizedValue(choice.label, "default"),
value: choice.id,
meta: {
type: "choice",
type: "static",
},
};
});
@@ -308,7 +308,7 @@ export const getMatchValueProps = (
label: choice.imageUrl.split("/").pop() || `Image ${idx + 1}`,
value: choice.id,
meta: {
type: "choice",
type: "static",
},
};
});
@@ -324,7 +324,7 @@ export const getMatchValueProps = (
label: `${idx + 1}`,
value: idx + 1,
meta: {
type: "choice",
type: "static",
},
};
});
@@ -371,7 +371,7 @@ export const getMatchValueProps = (
label: `${idx}`,
value: idx,
meta: {
type: "choice",
type: "static",
},
};
});
@@ -1007,7 +1007,7 @@ export const findOptionUsedInLogic = (survey: TSurvey, questionId: string, optio
const isUsedInOperand = (condition: TSingleCondition): boolean => {
if (condition.leftOperand.type === "question" && condition.leftOperand.id === questionId) {
if (condition.rightOperand && condition.rightOperand.type === "choice") {
if (condition.rightOperand && condition.rightOperand.type === "static") {
if (Array.isArray(condition.rightOperand.value)) {
return condition.rightOperand.value.includes(optionId);
} else {

View File

@@ -1,17 +1,20 @@
/* eslint-disable @typescript-eslint/restrict-template-expressions -- string interpolation is allowed in migration scripts */
/* eslint-disable no-console -- logging is allowed in migration scripts */
// !@gupta-piyush19: WIP
// Pending:
// 1. Options assignment: text to id
// 2. have to check and modify data storing in right operand based on the saving pattern of current logic
import { createId } from "@paralleldrive/cuid2";
import { PrismaClient } from "@prisma/client";
import type {
TAction,
TRightOperand,
TSingleCondition,
TSurveyAdvancedLogic,
TSurveyLogicCondition,
} from "@formbricks/types/surveys/logic";
import type { TSurveyQuestion } from "@formbricks/types/surveys/types";
import {
type TSurveyMultipleChoiceQuestion,
type TSurveyQuestion,
TSurveyQuestionTypeEnum,
} from "@formbricks/types/surveys/types";
const prisma = new PrismaClient();
@@ -21,6 +24,10 @@ interface TOldLogic {
destination: string;
}
const isOldLogic = (logic: TOldLogic | TSurveyAdvancedLogic): logic is TOldLogic => {
return Object.keys(logic).some((key) => ["condition", "destination", "value"].includes(key));
};
const doesRightOperandExist = (operator: TSurveyLogicCondition): boolean => {
return ![
"isAccepted",
@@ -33,27 +40,122 @@ const doesRightOperandExist = (operator: TSurveyLogicCondition): boolean => {
].includes(operator);
};
// Helper function to convert old logic condition to new format
function convertLogicCondition(
const getChoiceId = (question: TSurveyMultipleChoiceQuestion, choiceText: string): string | undefined => {
const choiceOption = question.choices.find((choice) => choice.label.default === choiceText);
if (choiceOption) {
return choiceOption.id;
}
if (question.choices.at(-1)?.id === "other") {
return "other";
}
};
const getRightOperandValue = (
surveyId: string,
oldCondition: string,
oldValue: string | string[] | undefined,
questionId: string
): TSingleCondition {
question: TSurveyQuestion
): TRightOperand | undefined => {
if (["lessThan", "lessEqual", "greaterThan", "greaterEqual"].includes(oldCondition)) {
return {
type: "static",
value: parseInt(oldValue as string),
};
}
if (["equals", "notEquals"].includes(oldCondition)) {
if (["string", "number"].includes(typeof oldValue)) {
if (question.type === TSurveyQuestionTypeEnum.Rating || question.type === TSurveyQuestionTypeEnum.NPS) {
return {
type: "static",
value: parseInt(oldValue as string),
};
} else if (
question.type === TSurveyQuestionTypeEnum.MultipleChoiceSingle ||
question.type === TSurveyQuestionTypeEnum.MultipleChoiceMulti
) {
const choiceId = getChoiceId(question, oldValue as string);
if (choiceId) {
return {
type: "static",
value: choiceId,
};
}
return undefined;
} else if (question.type === TSurveyQuestionTypeEnum.PictureSelection) {
return {
type: "static",
value: oldValue as string,
};
}
}
throw new Error(`Invalid value for 'equals' or 'notEquals' condition in survey ${surveyId}`);
}
if (["includesAll", "includesOne"].includes(oldCondition)) {
let choiceIds: string[] = [];
if (oldValue && Array.isArray(oldValue)) {
if (
question.type === TSurveyQuestionTypeEnum.MultipleChoiceMulti ||
question.type === TSurveyQuestionTypeEnum.MultipleChoiceSingle
) {
oldValue.forEach((choiceText) => {
const choiceId = getChoiceId(question, choiceText);
if (choiceId) {
choiceIds.push(choiceId);
}
});
choiceIds = Array.from(new Set(choiceIds));
return {
type: "static",
value: choiceIds,
};
}
return {
type: "static",
value: oldValue,
};
}
throw new Error(`Invalid value for 'includesAll' or 'includesOne' condition in survey ${surveyId}`);
}
throw new Error(`Invalid condition ${oldCondition} in survey ${surveyId}`);
};
// Helper function to convert old logic condition to new format
function convertLogicCondition(
surveyId: string,
oldCondition: string,
oldValue: string | string[] | undefined,
question: TSurveyQuestion
): TSingleCondition | undefined {
const operator = mapOldConditionToNew(oldCondition);
let rightOperandValue: TRightOperand | undefined;
const doesRightOperandExistResult = doesRightOperandExist(operator);
if (doesRightOperandExistResult) {
rightOperandValue = getRightOperandValue(surveyId, oldCondition, oldValue, question);
if (!rightOperandValue) {
return undefined;
}
}
const newCondition: TSingleCondition = {
id: createId(),
leftOperand: {
type: "question",
id: questionId,
id: question.id,
},
operator,
...(doesRightOperandExist(operator) && {
rightOperand: {
type: "static",
value: oldValue ?? "",
},
}),
rightOperand: rightOperandValue,
};
return newCondition;
@@ -85,9 +187,16 @@ function mapOldConditionToNew(oldCondition: string): TSurveyLogicCondition {
}
// Helper function to convert old logic to new format
function convertLogic(oldLogic: TOldLogic, questionId: string): TSurveyAdvancedLogic {
const condition: TSingleCondition = convertLogicCondition(oldLogic.condition, oldLogic.value, questionId);
condition.leftOperand.id = questionId;
function convertLogic(
surveyId: string,
oldLogic: TOldLogic,
question: TSurveyQuestion
): TSurveyAdvancedLogic | undefined {
const condition = convertLogicCondition(surveyId, oldLogic.condition, oldLogic.value, question);
if (!condition) {
return undefined;
}
const action: TAction = {
id: createId(),
@@ -99,7 +208,7 @@ function convertLogic(oldLogic: TOldLogic, questionId: string): TSurveyAdvancedL
id: createId(),
conditions: {
id: createId(),
connector: null,
connector: "and",
conditions: [condition],
},
actions: [action],
@@ -114,11 +223,6 @@ async function runMigration(): Promise<void> {
// Get all surveys with questions containing old logic
const relevantSurveys = await tx.survey.findMany({
where: {
questions: {
array_contains: [{ logic: { $exists: true } }],
},
},
select: {
id: true,
questions: true,
@@ -126,22 +230,36 @@ async function runMigration(): Promise<void> {
});
// Process each survey
const migrationPromises = relevantSurveys.map(async (survey) => {
const updatedQuestions = survey.questions.map((question: TSurveyQuestion) => {
if (question.logic && Array.isArray(question.logic)) {
const newLogic = (question.logic as TOldLogic[]).map((oldLogic) =>
convertLogic(oldLogic, question.id)
);
return { ...question, logic: newLogic };
}
return question;
});
const migrationPromises = relevantSurveys
.map((survey) => {
let doesThisSurveyHasOldLogic = false;
const questions: TSurveyQuestion[] = [];
return tx.survey.update({
where: { id: survey.id },
data: { questions: updatedQuestions },
});
});
for (const question of survey.questions) {
if (question.logic && Array.isArray(question.logic) && question.logic.some(isOldLogic)) {
doesThisSurveyHasOldLogic = true;
const newLogic = (question.logic as unknown as TOldLogic[])
.map((oldLogic) => convertLogic(survey.id, oldLogic, question))
.filter((logic) => logic !== undefined);
questions.push({ ...question, logic: newLogic });
} else {
questions.push(question);
}
}
if (!doesThisSurveyHasOldLogic) {
return null;
}
return tx.survey.update({
where: { id: survey.id },
data: { questions },
});
})
.filter((promise) => promise !== null);
console.log(`Found ${migrationPromises.length} surveys with old logic`);
await Promise.all(migrationPromises);

View File

@@ -697,7 +697,7 @@ const churnSurvey = (): TTemplate => {
},
operator: "equals",
rightOperand: {
type: "choice",
type: "static",
value: reusableOptionIds[0],
},
},
@@ -725,7 +725,7 @@ const churnSurvey = (): TTemplate => {
},
operator: "equals",
rightOperand: {
type: "choice",
type: "static",
value: reusableOptionIds[1],
},
},
@@ -753,7 +753,7 @@ const churnSurvey = (): TTemplate => {
},
operator: "equals",
rightOperand: {
type: "choice",
type: "static",
value: reusableOptionIds[2],
},
},
@@ -781,7 +781,7 @@ const churnSurvey = (): TTemplate => {
},
operator: "equals",
rightOperand: {
type: "choice",
type: "static",
value: reusableOptionIds[3],
},
},
@@ -809,7 +809,7 @@ const churnSurvey = (): TTemplate => {
},
operator: "equals",
rightOperand: {
type: "choice",
type: "static",
value: reusableOptionIds[4],
},
},
@@ -1020,7 +1020,7 @@ const earnedAdvocacyScore = (): TTemplate => {
},
operator: "equals",
rightOperand: {
type: "choice",
type: "static",
value: reusableOptionIds[1],
},
},
@@ -1103,7 +1103,7 @@ const earnedAdvocacyScore = (): TTemplate => {
},
operator: "equals",
rightOperand: {
type: "choice",
type: "static",
value: reusableOptionIds[3],
},
},
@@ -1180,7 +1180,7 @@ const improveTrialConversion = (): TTemplate => {
},
operator: "equals",
rightOperand: {
type: "choice",
type: "static",
value: reusableOptionIds[0],
},
},
@@ -1208,7 +1208,7 @@ const improveTrialConversion = (): TTemplate => {
},
operator: "equals",
rightOperand: {
type: "choice",
type: "static",
value: reusableOptionIds[1],
},
},
@@ -1236,7 +1236,7 @@ const improveTrialConversion = (): TTemplate => {
},
operator: "equals",
rightOperand: {
type: "choice",
type: "static",
value: reusableOptionIds[2],
},
},
@@ -1264,7 +1264,7 @@ const improveTrialConversion = (): TTemplate => {
},
operator: "equals",
rightOperand: {
type: "choice",
type: "static",
value: reusableOptionIds[3],
},
},
@@ -1292,7 +1292,7 @@ const improveTrialConversion = (): TTemplate => {
},
operator: "equals",
rightOperand: {
type: "choice",
type: "static",
value: reusableOptionIds[4],
},
},
@@ -1688,7 +1688,7 @@ const improveActivationRate = (): TTemplate => {
},
operator: "equals",
rightOperand: {
type: "choice",
type: "static",
value: reusableOptionIds[1],
},
},
@@ -1716,7 +1716,7 @@ const improveActivationRate = (): TTemplate => {
},
operator: "equals",
rightOperand: {
type: "choice",
type: "static",
value: reusableOptionIds[2],
},
},
@@ -1744,7 +1744,7 @@ const improveActivationRate = (): TTemplate => {
},
operator: "equals",
rightOperand: {
type: "choice",
type: "static",
value: reusableOptionIds[3],
},
},
@@ -1772,7 +1772,7 @@ const improveActivationRate = (): TTemplate => {
},
operator: "equals",
rightOperand: {
type: "choice",
type: "static",
value: reusableOptionIds[4],
},
},
@@ -2328,7 +2328,7 @@ const feedbackBox = (): TTemplate => {
},
operator: "equals",
rightOperand: {
type: "choice",
type: "static",
value: reusableOptionIds[0],
},
},
@@ -2356,7 +2356,7 @@ const feedbackBox = (): TTemplate => {
},
operator: "equals",
rightOperand: {
type: "choice",
type: "static",
value: reusableOptionIds[1],
},
},
@@ -3497,7 +3497,7 @@ const measureTaskAccomplishment = (): TTemplate => {
},
operator: "equals",
rightOperand: {
type: "choice",
type: "static",
value: reusableOptionIds[1],
},
},
@@ -3525,7 +3525,7 @@ const measureTaskAccomplishment = (): TTemplate => {
},
operator: "equals",
rightOperand: {
type: "choice",
type: "static",
value: reusableOptionIds[0],
},
},
@@ -3553,7 +3553,7 @@ const measureTaskAccomplishment = (): TTemplate => {
},
operator: "equals",
rightOperand: {
type: "choice",
type: "static",
value: reusableOptionIds[2],
},
},
@@ -3839,7 +3839,7 @@ const identifySignUpBarriers = (): TTemplate => {
},
operator: "equals",
rightOperand: {
type: "choice",
type: "static",
value: reusableOptionIds[0],
},
},
@@ -3867,7 +3867,7 @@ const identifySignUpBarriers = (): TTemplate => {
},
operator: "equals",
rightOperand: {
type: "choice",
type: "static",
value: reusableOptionIds[1],
},
},
@@ -3895,7 +3895,7 @@ const identifySignUpBarriers = (): TTemplate => {
},
operator: "equals",
rightOperand: {
type: "choice",
type: "static",
value: reusableOptionIds[2],
},
},
@@ -3923,7 +3923,7 @@ const identifySignUpBarriers = (): TTemplate => {
},
operator: "equals",
rightOperand: {
type: "choice",
type: "static",
value: reusableOptionIds[3],
},
},
@@ -3951,7 +3951,7 @@ const identifySignUpBarriers = (): TTemplate => {
},
operator: "equals",
rightOperand: {
type: "choice",
type: "static",
value: reusableOptionIds[4],
},
},
@@ -4799,7 +4799,7 @@ const understandLowEngagement = (): TTemplate => {
},
operator: "equals",
rightOperand: {
type: "choice",
type: "static",
value: reusableOptionIds[0],
},
},
@@ -4827,7 +4827,7 @@ const understandLowEngagement = (): TTemplate => {
},
operator: "equals",
rightOperand: {
type: "choice",
type: "static",
value: reusableOptionIds[1],
},
},
@@ -4855,7 +4855,7 @@ const understandLowEngagement = (): TTemplate => {
},
operator: "equals",
rightOperand: {
type: "choice",
type: "static",
value: reusableOptionIds[2],
},
},
@@ -4883,7 +4883,7 @@ const understandLowEngagement = (): TTemplate => {
},
operator: "equals",
rightOperand: {
type: "choice",
type: "static",
value: reusableOptionIds[3],
},
},
@@ -4911,7 +4911,7 @@ const understandLowEngagement = (): TTemplate => {
},
operator: "equals",
rightOperand: {
type: "choice",
type: "static",
value: "other",
},
},

View File

@@ -196,8 +196,6 @@ const getRightOperandValue = (
return variableValue || "";
case "hiddenField":
return data[rightOperand.value];
case "choice":
return rightOperand.value;
case "static":
return rightOperand.value;
default:

View File

@@ -189,8 +189,6 @@ const getRightOperandValue = (
switch (rightOperand.type) {
case "question":
return data[rightOperand.value];
case "choice":
return rightOperand.value;
case "variable":
const variables = localSurvey.variables || [];
const variable = variables.find((v) => v.id === rightOperand.value);

View File

@@ -354,10 +354,6 @@ export const ZRightOperand = z.discriminatedUnion("type", [
type: z.literal("static"),
value: z.union([z.string(), z.number(), z.array(z.string())]),
}),
z.object({
type: z.literal("choice"),
value: z.string().cuid2(),
}),
z.object({
type: z.literal("question"),
value: z.string().cuid2(),
@@ -391,14 +387,14 @@ export type TSingleCondition = z.infer<typeof ZSingleCondition>;
export interface TConditionGroup {
id: string;
connector: "and" | "or" | null;
connector: "and" | "or";
conditions: (TSingleCondition | TConditionGroup)[];
}
const ZConditionGroup: z.ZodType<TConditionGroup> = z.lazy(() =>
z.object({
id: z.string().cuid2(),
connector: z.enum(["and", "or"]).nullable(),
connector: z.enum(["and", "or"]),
conditions: z.array(z.union([ZSingleCondition, ZConditionGroup])),
})
);