Files
formbricks-formbricks/packages/lib/utils/evaluateLogic.ts
2024-09-12 15:57:11 +05:30

456 lines
15 KiB
TypeScript

import { TResponseData, TResponseVariables } from "@formbricks/types/responses";
import {
TAction,
TActionCalculate,
TConditionGroup,
TSingleCondition,
} from "@formbricks/types/surveys/logic";
import {
TSurvey,
TSurveyQuestion,
TSurveyQuestionTypeEnum,
TSurveyVariable,
} from "@formbricks/types/surveys/types";
import { getLocalizedValue } from "../i18n/utils";
import { isConditionsGroup } from "../survey/logic/utils";
export const evaluateAdvancedLogic = (
localSurvey: TSurvey,
data: TResponseData,
variablesData: TResponseVariables,
conditions: TConditionGroup,
selectedLanguage: string
): boolean => {
const evaluateConditionGroup = (group: TConditionGroup): boolean => {
const results = group.conditions.map((condition) => {
if (isConditionsGroup(condition)) {
return evaluateConditionGroup(condition);
} else {
return evaluateSingleCondition(localSurvey, data, variablesData, condition, selectedLanguage);
}
});
return group.connector === "or" ? results.some((r) => r) : results.every((r) => r);
};
return evaluateConditionGroup(conditions);
};
const evaluateSingleCondition = (
localSurvey: TSurvey,
data: TResponseData,
variablesData: TResponseVariables,
condition: TSingleCondition,
selectedLanguage: string
): boolean => {
try {
const leftValue = getLeftOperandValue(
localSurvey,
data,
variablesData,
condition.leftOperand,
selectedLanguage
);
const rightValue = condition.rightOperand
? getRightOperandValue(localSurvey, data, variablesData, condition.rightOperand)
: undefined;
let leftField: TSurveyQuestion | TSurveyVariable | string;
if (condition.leftOperand?.type === "question") {
leftField = localSurvey.questions.find((q) => q.id === condition.leftOperand?.value) as TSurveyQuestion;
} else if (condition.leftOperand?.type === "variable") {
leftField = localSurvey.variables.find((v) => v.id === condition.leftOperand?.value) as TSurveyVariable;
} else if (condition.leftOperand?.type === "hiddenField") {
leftField = condition.leftOperand.value as string;
} else {
leftField = "";
}
let rightField: TSurveyQuestion | TSurveyVariable | string;
if (condition.rightOperand?.type === "question") {
rightField = localSurvey.questions.find(
(q) => q.id === condition.rightOperand?.value
) as TSurveyQuestion;
} else if (condition.rightOperand?.type === "variable") {
rightField = localSurvey.variables.find(
(v) => v.id === condition.rightOperand?.value
) as TSurveyVariable;
} else if (condition.rightOperand?.type === "hiddenField") {
rightField = condition.rightOperand.value as string;
} else {
rightField = "";
}
switch (condition.operator) {
case "equals":
if (condition.leftOperand.type === "question") {
if (
(leftField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.Date &&
typeof leftValue === "string" &&
typeof rightValue === "string"
) {
// when left value is of date question and right value is string
return new Date(leftValue).getTime() === new Date(rightValue).getTime();
}
}
// when left value is of openText, hiddenField, variable and right value is of multichoice
if (condition.rightOperand?.type === "question") {
if ((rightField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.MultipleChoiceMulti) {
if (Array.isArray(rightValue) && typeof leftValue === "string" && rightValue.length === 1) {
return rightValue.includes(leftValue as string);
} else return false;
} else if (
(rightField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.Date &&
typeof leftValue === "string" &&
typeof rightValue === "string"
) {
return new Date(leftValue).getTime() === new Date(rightValue).getTime();
}
}
return (
(Array.isArray(leftValue) &&
leftValue.length === 1 &&
typeof rightValue === "string" &&
leftValue.includes(rightValue)) ||
leftValue === rightValue
);
case "doesNotEqual":
// when left value is of picture selection question and right value is its option
if (
condition.leftOperand.type === "question" &&
(leftField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.PictureSelection &&
Array.isArray(leftValue) &&
leftValue.length > 0 &&
typeof rightValue === "string"
) {
return !leftValue.includes(rightValue);
}
// when left value is of date question and right value is string
if (
condition.leftOperand.type === "question" &&
(leftField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.Date &&
typeof leftValue === "string" &&
typeof rightValue === "string"
) {
return new Date(leftValue).getTime() !== new Date(rightValue).getTime();
}
// when left value is of openText, hiddenField, variable and right value is of multichoice
if (condition.rightOperand?.type === "question") {
if ((rightField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.MultipleChoiceMulti) {
if (Array.isArray(rightValue) && typeof leftValue === "string" && rightValue.length === 1) {
return !rightValue.includes(leftValue as string);
} else return false;
} else if (
(rightField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.Date &&
typeof leftValue === "string" &&
typeof rightValue === "string"
) {
return new Date(leftValue).getTime() !== new Date(rightValue).getTime();
}
}
return (
(Array.isArray(leftValue) &&
leftValue.length === 1 &&
typeof rightValue === "string" &&
!leftValue.includes(rightValue)) ||
leftValue !== rightValue
);
case "contains":
return String(leftValue).includes(String(rightValue));
case "doesNotContain":
return !String(leftValue).includes(String(rightValue));
case "startsWith":
return String(leftValue).startsWith(String(rightValue));
case "doesNotStartWith":
return !String(leftValue).startsWith(String(rightValue));
case "endsWith":
return String(leftValue).endsWith(String(rightValue));
case "doesNotEndWith":
return !String(leftValue).endsWith(String(rightValue));
case "isSubmitted":
if (typeof leftValue === "string") {
if (
condition.leftOperand.type === "question" &&
(leftField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.FileUpload &&
leftValue
) {
return leftValue !== "skipped";
}
return leftValue !== "" && leftValue !== null;
} else if (Array.isArray(leftValue)) {
return leftValue.length > 0;
} else if (typeof leftValue === "number") {
return leftValue !== null;
}
return false;
case "isSkipped":
return (
(Array.isArray(leftValue) && leftValue.length === 0) ||
leftValue === "" ||
leftValue === null ||
leftValue === undefined ||
(typeof leftValue === "object" && Object.entries(leftValue).length === 0)
);
case "isGreaterThan":
return Number(leftValue) > Number(rightValue);
case "isLessThan":
return Number(leftValue) < Number(rightValue);
case "isGreaterThanOrEqual":
return Number(leftValue) >= Number(rightValue);
case "isLessThanOrEqual":
return Number(leftValue) <= Number(rightValue);
case "equalsOneOf":
return Array.isArray(rightValue) && typeof leftValue === "string" && rightValue.includes(leftValue);
case "includesAllOf":
return (
Array.isArray(leftValue) &&
Array.isArray(rightValue) &&
rightValue.every((v) => leftValue.includes(v))
);
case "includesOneOf":
return (
Array.isArray(leftValue) &&
Array.isArray(rightValue) &&
rightValue.some((v) => leftValue.includes(v))
);
case "isAccepted":
return leftValue === "accepted";
case "isClicked":
return leftValue === "clicked";
case "isAfter":
return new Date(String(leftValue)) > new Date(String(rightValue));
case "isBefore":
return new Date(String(leftValue)) < new Date(String(rightValue));
case "isBooked":
return leftValue === "booked" || !!(leftValue && leftValue !== "");
case "isPartiallySubmitted":
if (typeof leftValue === "object") {
return Object.values(leftValue).includes("");
} else return false;
case "isCompletelySubmitted":
if (typeof leftValue === "object") {
const values = Object.values(leftValue);
return values.length > 0 && !values.includes("");
} else return false;
default:
return false;
}
} catch (e) {
return false;
}
};
const getLeftOperandValue = (
localSurvey: TSurvey,
data: TResponseData,
variablesData: TResponseVariables,
leftOperand: TSingleCondition["leftOperand"],
selectedLanguage: string
) => {
switch (leftOperand.type) {
case "question":
const currentQuestion = localSurvey.questions.find((q) => q.id === leftOperand.value);
if (!currentQuestion) return undefined;
const responseValue = data[leftOperand.value];
if (currentQuestion.type === "openText" && currentQuestion.inputType === "number") {
return Number(responseValue) || 0;
}
if (currentQuestion.type === "multipleChoiceSingle" || currentQuestion.type === "multipleChoiceMulti") {
const isOthersEnabled = currentQuestion.choices.at(-1)?.id === "other";
if (typeof responseValue === "string") {
const choice = currentQuestion.choices.find((choice) => {
return getLocalizedValue(choice.label, selectedLanguage) === responseValue;
});
if (!choice) {
if (isOthersEnabled) {
return "other";
}
return undefined;
}
return choice.id;
} else if (Array.isArray(responseValue)) {
let choice: string[] = [];
responseValue.forEach((value) => {
const foundChoice = currentQuestion.choices.find((choice) => {
return getLocalizedValue(choice.label, selectedLanguage) === value;
});
if (foundChoice) {
choice.push(foundChoice.id);
} else if (isOthersEnabled) {
choice.push("other");
}
});
if (choice) {
return Array.from(new Set(choice));
}
}
}
return data[leftOperand.value];
case "variable":
const variables = localSurvey.variables || [];
const variable = variables.find((v) => v.id === leftOperand.value);
if (!variable) return undefined;
const variableValue = variablesData[leftOperand.value];
if (variable.type === "number") return Number(variableValue) || 0;
return variableValue || "";
case "hiddenField":
return data[leftOperand.value];
default:
return undefined;
}
};
const getRightOperandValue = (
localSurvey: TSurvey,
data: TResponseData,
variablesData: TResponseVariables,
rightOperand: TSingleCondition["rightOperand"]
) => {
if (!rightOperand) return undefined;
switch (rightOperand.type) {
case "question":
return data[rightOperand.value];
case "variable":
const variables = localSurvey.variables || [];
const variable = variables.find((v) => v.id === rightOperand.value);
if (!variable) return undefined;
const variableValue = variablesData[rightOperand.value];
if (variable.type === "number") return Number(variableValue) || 0;
return variableValue || "";
case "hiddenField":
return !isNaN(data[rightOperand.value] as number)
? Number(data[rightOperand.value])
: data[rightOperand.value];
case "static":
return rightOperand.value;
default:
return undefined;
}
};
export const performActions = (
survey: TSurvey,
actions: TAction[],
data: TResponseData,
calculationResults: TResponseVariables
): {
jumpTarget: string | undefined;
requiredQuestionIds: string[];
calculations: TResponseVariables;
} => {
let jumpTarget: string | undefined;
const requiredQuestionIds: string[] = [];
const calculations: TResponseVariables = { ...calculationResults };
actions.forEach((action) => {
switch (action.objective) {
case "calculate":
const result = performCalculation(survey, action, data, calculations);
if (result !== undefined) calculations[action.variableId] = result;
break;
case "requireAnswer":
requiredQuestionIds.push(action.target);
break;
case "jumpToQuestion":
if (!jumpTarget) {
jumpTarget = action.target;
}
break;
}
});
return { jumpTarget, requiredQuestionIds, calculations };
};
const performCalculation = (
survey: TSurvey,
action: TActionCalculate,
data: TResponseData,
calculations: Record<string, number | string>
): number | string | undefined => {
const variables = survey.variables || [];
const variable = variables.find((v) => v.id === action.variableId);
if (!variable) return undefined;
let currentValue = calculations[action.variableId];
if (currentValue === undefined) {
currentValue = variable.type === "number" ? 0 : "";
}
let operandValue: string | number | undefined;
// Determine the operand value based on the action.value type
switch (action.value.type) {
case "static":
operandValue = action.value.value;
break;
case "variable":
const value = calculations[action.value.value];
if (typeof value === "number" || typeof value === "string") {
operandValue = value;
}
break;
case "question":
case "hiddenField":
const val = data[action.value.value];
if (typeof val === "number" || typeof val === "string") {
if (variable.type === "number" && !isNaN(Number(val))) {
operandValue = Number(val);
}
operandValue = val;
}
break;
}
if (operandValue === undefined || operandValue === null) return undefined;
let result: number | string;
switch (action.operator) {
case "add":
result = Number(currentValue) + Number(operandValue);
break;
case "subtract":
result = Number(currentValue) - Number(operandValue);
break;
case "multiply":
result = Number(currentValue) * Number(operandValue);
break;
case "divide":
if (Number(operandValue) === 0) return undefined;
result = Number(currentValue) / Number(operandValue);
break;
case "assign":
result = operandValue;
break;
case "concat":
result = String(currentValue) + String(operandValue);
break;
}
return result;
};