mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-30 02:10:12 -06:00
Co-authored-by: Matti Nannt <matti@formbricks.com> Co-authored-by: Johannes <johannes@formbricks.com> Co-authored-by: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com> Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
956 lines
27 KiB
TypeScript
956 lines
27 KiB
TypeScript
import { Prisma } from "@prisma/client";
|
|
import {
|
|
TResponse,
|
|
TResponseDataValue,
|
|
TResponseFilterCriteria,
|
|
TResponseHiddenFieldsFilter,
|
|
TResponseTtc,
|
|
TResponseWithQuotas,
|
|
TSurveyContactAttributes,
|
|
TSurveyMetaFieldFilter,
|
|
} from "@formbricks/types/responses";
|
|
import {
|
|
TSurveyElement,
|
|
TSurveyMultipleChoiceElement,
|
|
TSurveyPictureSelectionElement,
|
|
TSurveyRankingElement,
|
|
} from "@formbricks/types/surveys/elements";
|
|
import { TSurvey } from "@formbricks/types/surveys/types";
|
|
import { getTextContent } from "@formbricks/types/surveys/validation";
|
|
import { getLocalizedValue } from "@/lib/i18n/utils";
|
|
import { replaceHeadlineRecall } from "@/lib/utils/recall";
|
|
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
|
|
import { processResponseData } from "../responses";
|
|
import { getTodaysDateTimeFormatted } from "../time";
|
|
import { getFormattedDateTimeString } from "../utils/datetime";
|
|
import { sanitizeString } from "../utils/strings";
|
|
|
|
/**
|
|
* Extracts choice IDs from response values for multiple choice elements
|
|
* @param responseValue - The response value (string for single choice, array for multi choice)
|
|
* @param element - The survey element containing choices
|
|
* @param language - The language to match against (defaults to "default")
|
|
* @returns Array of choice IDs
|
|
*/
|
|
export const extractChoiceIdsFromResponse = (
|
|
responseValue: TResponseDataValue,
|
|
element: TSurveyElement,
|
|
language: string = "default"
|
|
): string[] => {
|
|
if (
|
|
element.type !== "multipleChoiceMulti" &&
|
|
element.type !== "multipleChoiceSingle" &&
|
|
element.type !== "ranking" &&
|
|
element.type !== "pictureSelection"
|
|
) {
|
|
return [];
|
|
}
|
|
|
|
const isPictureSelection = element.type === "pictureSelection";
|
|
|
|
if (!responseValue) {
|
|
return [];
|
|
}
|
|
|
|
// For picture selection elements, the response value is already choice ID(s)
|
|
if (isPictureSelection) {
|
|
if (Array.isArray(responseValue)) {
|
|
// Multi-selection: array of choice IDs
|
|
return responseValue.filter((id): id is string => typeof id === "string");
|
|
} else if (typeof responseValue === "string") {
|
|
// Single selection: single choice ID
|
|
return [responseValue];
|
|
}
|
|
return [];
|
|
}
|
|
|
|
const defaultLanguage = language ?? "default";
|
|
|
|
// Helper function to find choice by label - eliminates duplication
|
|
const findChoiceByLabel = (choiceLabel: string): string | null => {
|
|
const targetChoice = element.choices.find((c) => {
|
|
// Try exact language match first
|
|
if (c.label[defaultLanguage] === choiceLabel) {
|
|
return true;
|
|
}
|
|
// Fall back to checking all language values
|
|
return Object.values(c.label).includes(choiceLabel);
|
|
});
|
|
return targetChoice?.id || "other";
|
|
};
|
|
|
|
if (Array.isArray(responseValue)) {
|
|
// Multiple choice case - response is an array of selected choice labels
|
|
return responseValue.map(findChoiceByLabel).filter((choiceId): choiceId is string => choiceId !== null);
|
|
} else if (typeof responseValue === "string") {
|
|
// Single choice case - response is a single choice label
|
|
const choiceId = findChoiceByLabel(responseValue);
|
|
return choiceId ? [choiceId] : [];
|
|
}
|
|
|
|
return [];
|
|
};
|
|
|
|
export const getChoiceIdByValue = (
|
|
value: string,
|
|
element: TSurveyMultipleChoiceElement | TSurveyRankingElement | TSurveyPictureSelectionElement
|
|
) => {
|
|
if (element.type === "pictureSelection") {
|
|
return element.choices.find((choice) => choice.imageUrl === value)?.id ?? "other";
|
|
}
|
|
|
|
return element.choices.find((choice) => choice.label.default === value)?.id ?? "other";
|
|
};
|
|
|
|
export const calculateTtcTotal = (ttc: TResponseTtc) => {
|
|
const result = { ...ttc };
|
|
result._total = Object.values(result).reduce((acc: number, val: number) => acc + val, 0);
|
|
|
|
return result;
|
|
};
|
|
|
|
const createFilterTags = (tags: TResponseFilterCriteria["tags"]) => {
|
|
if (!tags) return [];
|
|
|
|
const filterTags: Record<string, any>[] = [];
|
|
|
|
if (tags?.applied) {
|
|
const appliedTags = tags.applied.map((name) => ({
|
|
tags: {
|
|
some: {
|
|
tag: {
|
|
name,
|
|
},
|
|
},
|
|
},
|
|
}));
|
|
filterTags.push(appliedTags);
|
|
}
|
|
|
|
if (tags?.notApplied) {
|
|
const notAppliedTags = {
|
|
tags: {
|
|
every: {
|
|
tag: {
|
|
name: {
|
|
notIn: tags.notApplied,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
filterTags.push(notAppliedTags);
|
|
}
|
|
|
|
return filterTags.flat();
|
|
};
|
|
|
|
export const buildWhereClause = (survey: TSurvey, filterCriteria?: TResponseFilterCriteria) => {
|
|
const whereClause: Prisma.ResponseWhereInput["AND"] = [];
|
|
|
|
// For finished
|
|
if (filterCriteria?.finished !== undefined) {
|
|
whereClause.push({
|
|
finished: filterCriteria?.finished,
|
|
});
|
|
}
|
|
|
|
// For Date range
|
|
if (filterCriteria?.createdAt) {
|
|
const createdAt: { lte?: Date; gte?: Date } = {};
|
|
if (filterCriteria?.createdAt?.max) {
|
|
createdAt.lte = filterCriteria?.createdAt?.max;
|
|
}
|
|
if (filterCriteria?.createdAt?.min) {
|
|
createdAt.gte = filterCriteria?.createdAt?.min;
|
|
}
|
|
|
|
whereClause.push({
|
|
createdAt,
|
|
});
|
|
}
|
|
|
|
// For Tags
|
|
if (filterCriteria?.tags) {
|
|
const tagFilters = createFilterTags(filterCriteria.tags);
|
|
whereClause.push({
|
|
AND: tagFilters,
|
|
});
|
|
}
|
|
|
|
// For Person Attributes
|
|
if (filterCriteria?.contactAttributes) {
|
|
const contactAttributes: Prisma.ResponseWhereInput[] = [];
|
|
|
|
Object.entries(filterCriteria.contactAttributes).forEach(([key, val]) => {
|
|
switch (val.op) {
|
|
case "equals":
|
|
contactAttributes.push({
|
|
contactAttributes: {
|
|
path: [key],
|
|
equals: val.value,
|
|
},
|
|
});
|
|
break;
|
|
case "notEquals":
|
|
contactAttributes.push({
|
|
contactAttributes: {
|
|
path: [key],
|
|
not: val.value,
|
|
},
|
|
});
|
|
break;
|
|
}
|
|
});
|
|
|
|
whereClause.push({
|
|
AND: contactAttributes,
|
|
});
|
|
}
|
|
|
|
// for meta
|
|
if (filterCriteria?.meta) {
|
|
const meta: Prisma.ResponseWhereInput[] = [];
|
|
|
|
Object.entries(filterCriteria.meta).forEach(([key, val]) => {
|
|
let updatedKey: string[] = [];
|
|
if (["browser", "os", "device"].includes(key)) {
|
|
updatedKey = ["userAgent", key];
|
|
} else {
|
|
updatedKey = [key];
|
|
}
|
|
|
|
switch (val.op) {
|
|
case "equals":
|
|
meta.push({
|
|
meta: {
|
|
path: updatedKey,
|
|
equals: val.value,
|
|
},
|
|
});
|
|
break;
|
|
case "notEquals":
|
|
meta.push({
|
|
meta: {
|
|
path: updatedKey,
|
|
not: val.value,
|
|
},
|
|
});
|
|
break;
|
|
case "contains":
|
|
meta.push({
|
|
meta: {
|
|
path: updatedKey,
|
|
string_contains: val.value,
|
|
},
|
|
});
|
|
break;
|
|
case "doesNotContain":
|
|
meta.push({
|
|
NOT: {
|
|
meta: {
|
|
path: updatedKey,
|
|
string_contains: val.value,
|
|
},
|
|
},
|
|
});
|
|
break;
|
|
case "startsWith":
|
|
meta.push({
|
|
meta: {
|
|
path: updatedKey,
|
|
string_starts_with: val.value,
|
|
},
|
|
});
|
|
break;
|
|
case "doesNotStartWith":
|
|
meta.push({
|
|
NOT: {
|
|
meta: {
|
|
path: updatedKey,
|
|
string_starts_with: val.value,
|
|
},
|
|
},
|
|
});
|
|
break;
|
|
case "endsWith":
|
|
meta.push({
|
|
meta: {
|
|
path: updatedKey,
|
|
string_ends_with: val.value,
|
|
},
|
|
});
|
|
break;
|
|
case "doesNotEndWith":
|
|
meta.push({
|
|
NOT: {
|
|
meta: {
|
|
path: updatedKey,
|
|
string_ends_with: val.value,
|
|
},
|
|
},
|
|
});
|
|
break;
|
|
}
|
|
});
|
|
|
|
whereClause.push({
|
|
AND: meta,
|
|
});
|
|
}
|
|
|
|
// For Language
|
|
if (filterCriteria?.others) {
|
|
const others: Prisma.ResponseWhereInput[] = [];
|
|
|
|
Object.entries(filterCriteria.others).forEach(([key, val]) => {
|
|
switch (val.op) {
|
|
case "equals":
|
|
others.push({
|
|
[key.toLocaleLowerCase()]: val.value,
|
|
});
|
|
break;
|
|
case "notEquals":
|
|
others.push({
|
|
[key.toLocaleLowerCase()]: {
|
|
not: val.value,
|
|
},
|
|
});
|
|
break;
|
|
}
|
|
});
|
|
whereClause.push({
|
|
AND: others,
|
|
});
|
|
}
|
|
|
|
if (filterCriteria?.data) {
|
|
const data: Prisma.ResponseWhereInput[] = [];
|
|
|
|
Object.entries(filterCriteria.data).forEach(([key, val]) => {
|
|
const elements = getElementsFromBlocks(survey.blocks);
|
|
const element = elements.find((element) => element.id === key);
|
|
|
|
switch (val.op) {
|
|
case "submitted":
|
|
data.push({
|
|
data: {
|
|
path: [key],
|
|
not: Prisma.DbNull,
|
|
},
|
|
});
|
|
break;
|
|
case "filledOut":
|
|
data.push({
|
|
data: {
|
|
path: [key],
|
|
not: [],
|
|
},
|
|
});
|
|
break;
|
|
case "skipped":
|
|
data.push({
|
|
OR: [
|
|
{
|
|
data: {
|
|
path: [key],
|
|
equals: Prisma.DbNull,
|
|
},
|
|
},
|
|
{
|
|
data: {
|
|
path: [key],
|
|
equals: "",
|
|
},
|
|
},
|
|
// For address element
|
|
{
|
|
data: {
|
|
path: [key],
|
|
equals: [],
|
|
},
|
|
},
|
|
],
|
|
});
|
|
break;
|
|
case "equals":
|
|
data.push({
|
|
data: {
|
|
path: [key],
|
|
equals: val.value,
|
|
},
|
|
});
|
|
break;
|
|
case "notEquals":
|
|
data.push({
|
|
OR: [
|
|
{
|
|
// for value not equal to val.value
|
|
data: {
|
|
path: [key],
|
|
not: val.value,
|
|
},
|
|
},
|
|
{
|
|
// for not answered
|
|
data: {
|
|
path: [key],
|
|
equals: Prisma.DbNull,
|
|
},
|
|
},
|
|
],
|
|
});
|
|
break;
|
|
case "lessThan":
|
|
data.push({
|
|
data: {
|
|
path: [key],
|
|
lt: val.value,
|
|
},
|
|
});
|
|
break;
|
|
case "lessEqual":
|
|
data.push({
|
|
data: {
|
|
path: [key],
|
|
lte: val.value,
|
|
},
|
|
});
|
|
break;
|
|
case "greaterThan":
|
|
data.push({
|
|
data: {
|
|
path: [key],
|
|
gt: val.value,
|
|
},
|
|
});
|
|
break;
|
|
case "greaterEqual":
|
|
data.push({
|
|
data: {
|
|
path: [key],
|
|
gte: val.value,
|
|
},
|
|
});
|
|
break;
|
|
case "includesAll":
|
|
data.push({
|
|
data: {
|
|
path: [key],
|
|
array_contains: val.value,
|
|
},
|
|
});
|
|
break;
|
|
case "includesOne":
|
|
// * If the element includes an 'other' choice and the user has selected it:
|
|
// * - `predefinedLabels`: Collects labels from the element's choices that aren't selected by the user.
|
|
// * - `subsets`: Generates all possible non-empty permutations of subsets of these predefined labels.
|
|
// *
|
|
// * Depending on the element type (multiple or single choice), the filter is constructed:
|
|
// * - For "multipleChoiceMulti": Filters out any combinations of choices that match the subsets of predefined labels.
|
|
// * - For "multipleChoiceSingle": Filters out any single predefined labels that match the user's selection.
|
|
const values: string[] = val.value.map((v) => v.toString());
|
|
const otherChoice =
|
|
element && (element.type === "multipleChoiceMulti" || element.type === "multipleChoiceSingle")
|
|
? element.choices.find((choice) => choice.id === "other")
|
|
: null;
|
|
|
|
if (
|
|
element &&
|
|
(element.type === "multipleChoiceMulti" || element.type === "multipleChoiceSingle") &&
|
|
element.choices.map((choice) => choice.id).includes("other") &&
|
|
otherChoice &&
|
|
values.includes(otherChoice.label.default)
|
|
) {
|
|
const predefinedLabels: string[] = [];
|
|
|
|
element.choices.forEach((choice) => {
|
|
Object.values(choice.label).forEach((label) => {
|
|
if (!values.includes(label)) {
|
|
predefinedLabels.push(label);
|
|
}
|
|
});
|
|
});
|
|
|
|
const subsets = generateAllPermutationsOfSubsets(predefinedLabels);
|
|
if (element.type === "multipleChoiceMulti") {
|
|
const subsetConditions = subsets.map((subset) => ({
|
|
data: { path: [key], equals: subset },
|
|
}));
|
|
data.push({
|
|
NOT: {
|
|
OR: subsetConditions,
|
|
},
|
|
});
|
|
} else {
|
|
data.push(
|
|
// for MultipleChoiceSingle
|
|
{
|
|
AND: predefinedLabels.map((label) => ({
|
|
NOT: {
|
|
data: {
|
|
path: [key],
|
|
equals: label,
|
|
},
|
|
},
|
|
})),
|
|
}
|
|
);
|
|
}
|
|
} else {
|
|
data.push({
|
|
OR: val.value.map((value: string | number) => ({
|
|
OR: [
|
|
// for MultipleChoiceMulti
|
|
{
|
|
data: {
|
|
path: [key],
|
|
array_contains: [value],
|
|
},
|
|
},
|
|
// for MultipleChoiceSingle
|
|
{
|
|
data: {
|
|
path: [key],
|
|
equals: value,
|
|
},
|
|
},
|
|
],
|
|
})),
|
|
});
|
|
}
|
|
|
|
break;
|
|
case "uploaded":
|
|
data.push({
|
|
data: {
|
|
path: [key],
|
|
not: "skipped",
|
|
},
|
|
});
|
|
break;
|
|
case "notUploaded":
|
|
data.push({
|
|
OR: [
|
|
{
|
|
// for skipped
|
|
data: {
|
|
path: [key],
|
|
equals: "skipped",
|
|
},
|
|
},
|
|
{
|
|
// for not answered
|
|
data: {
|
|
path: [key],
|
|
equals: Prisma.DbNull,
|
|
},
|
|
},
|
|
],
|
|
});
|
|
break;
|
|
case "clicked":
|
|
data.push({
|
|
data: {
|
|
path: [key],
|
|
equals: "clicked",
|
|
},
|
|
});
|
|
break;
|
|
case "accepted":
|
|
data.push({
|
|
data: {
|
|
path: [key],
|
|
equals: "accepted",
|
|
},
|
|
});
|
|
break;
|
|
case "booked":
|
|
data.push({
|
|
data: {
|
|
path: [key],
|
|
equals: "booked",
|
|
},
|
|
});
|
|
break;
|
|
case "matrix":
|
|
const rowLabel = Object.keys(val.value)[0];
|
|
data.push({
|
|
data: {
|
|
path: [key, rowLabel],
|
|
equals: val.value[rowLabel],
|
|
},
|
|
});
|
|
break;
|
|
}
|
|
});
|
|
|
|
whereClause.push({
|
|
AND: data,
|
|
});
|
|
}
|
|
|
|
// filter by explicit response IDs
|
|
if (filterCriteria?.responseIds) {
|
|
whereClause.push({
|
|
id: { in: filterCriteria.responseIds },
|
|
});
|
|
}
|
|
|
|
// For quota filters
|
|
if (filterCriteria?.quotas) {
|
|
const quotaFilters: Prisma.ResponseWhereInput[] = [];
|
|
|
|
Object.entries(filterCriteria.quotas).forEach(([quotaId, { op }]) => {
|
|
if (op === "screenedOutNotInQuota") {
|
|
// Responses that don't have any quota link with this quota
|
|
quotaFilters.push({
|
|
NOT: {
|
|
quotaLinks: {
|
|
some: {
|
|
quotaId,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
} else {
|
|
// Responses that have a quota link with this quota and the specified status
|
|
quotaFilters.push({
|
|
quotaLinks: {
|
|
some: {
|
|
quotaId,
|
|
status: op,
|
|
},
|
|
},
|
|
});
|
|
}
|
|
});
|
|
|
|
if (quotaFilters.length > 0) {
|
|
whereClause.push({
|
|
AND: quotaFilters,
|
|
});
|
|
}
|
|
}
|
|
|
|
return { AND: whereClause };
|
|
};
|
|
|
|
export const getResponsesFileName = (surveyName: string, extension: string) => {
|
|
const sanitizedSurveyName = sanitizeString(surveyName);
|
|
|
|
const formattedDateString = getTodaysDateTimeFormatted("-");
|
|
return `export-${sanitizedSurveyName.split(" ").join("-")}-${formattedDateString}.${extension}`.toLocaleLowerCase();
|
|
};
|
|
|
|
export const extracMetadataKeys = (obj: TResponse["meta"]) => {
|
|
let keys: string[] = [];
|
|
|
|
Object.entries(obj ?? {}).forEach(([key, value]) => {
|
|
if (typeof value === "object" && value !== null) {
|
|
Object.entries(value).forEach(([subKey]) => {
|
|
keys.push(key + " - " + subKey);
|
|
});
|
|
} else {
|
|
keys.push(key);
|
|
}
|
|
});
|
|
|
|
return keys;
|
|
};
|
|
|
|
export const extractSurveyDetails = (survey: TSurvey, responses: TResponse[]) => {
|
|
const metaDataFields = responses.length > 0 ? extracMetadataKeys(responses[0].meta) : [];
|
|
const modifiedSurvey = replaceHeadlineRecall(survey, "default");
|
|
|
|
const modifiedElements = getElementsFromBlocks(modifiedSurvey.blocks);
|
|
|
|
const elements = modifiedElements.map((element, idx) => {
|
|
const headline = getTextContent(getLocalizedValue(element.headline, "default")) ?? element.id;
|
|
if (element.type === "matrix") {
|
|
return element.rows.map((row) => {
|
|
return `${idx + 1}. ${headline} - ${getTextContent(getLocalizedValue(row.label, "default"))}`;
|
|
});
|
|
} else if (
|
|
element.type === "multipleChoiceMulti" ||
|
|
element.type === "multipleChoiceSingle" ||
|
|
element.type === "ranking"
|
|
) {
|
|
return [`${idx + 1}. ${headline}`, `${idx + 1}. ${headline} - Option ID`];
|
|
} else {
|
|
return [`${idx + 1}. ${headline}`];
|
|
}
|
|
});
|
|
|
|
const hiddenFields = survey.hiddenFields?.fieldIds || [];
|
|
const userAttributes =
|
|
survey.type === "app"
|
|
? Array.from(new Set(responses.map((response) => Object.keys(response.contactAttributes ?? {})).flat()))
|
|
: [];
|
|
const variables = survey.variables?.map((variable) => variable.name) || [];
|
|
|
|
return { metaDataFields, elements, hiddenFields, variables, userAttributes };
|
|
};
|
|
|
|
export const getResponsesJson = (
|
|
survey: TSurvey,
|
|
responses: TResponseWithQuotas[],
|
|
elementsHeadlines: string[][],
|
|
userAttributes: string[],
|
|
hiddenFields: string[],
|
|
isQuotasAllowed: boolean = false
|
|
): Record<string, string | number>[] => {
|
|
const jsonData: Record<string, string | number>[] = [];
|
|
|
|
responses.forEach((response, idx) => {
|
|
// basic response details
|
|
jsonData.push({
|
|
"No.": idx + 1,
|
|
"Response ID": response.id,
|
|
Timestamp: getFormattedDateTimeString(response.createdAt),
|
|
Finished: response.finished ? "Yes" : "No",
|
|
"Survey ID": response.surveyId,
|
|
"Formbricks ID (internal)": response.contact?.id || "",
|
|
"User ID": response.contact?.userId || "",
|
|
Tags: response.tags.map((tag) => tag.name).join(", "),
|
|
});
|
|
|
|
if (isQuotasAllowed) {
|
|
jsonData[idx]["Quotas"] = response.quotas?.map((quota) => quota.name).join(", ") || "";
|
|
}
|
|
|
|
// meta details
|
|
Object.entries(response.meta ?? {}).forEach(([key, value]) => {
|
|
if (typeof value === "object" && value !== null) {
|
|
Object.entries(value).forEach(([subKey, subValue]) => {
|
|
jsonData[idx][key + " - " + subKey] = subValue;
|
|
});
|
|
} else {
|
|
jsonData[idx][key] = value;
|
|
}
|
|
});
|
|
|
|
// survey response data
|
|
elementsHeadlines.forEach((elementHeadline) => {
|
|
const elementIndex = parseInt(elementHeadline[0]) - 1;
|
|
const elements = getElementsFromBlocks(survey.blocks);
|
|
const element = elements[elementIndex];
|
|
const answer = response.data[element.id];
|
|
|
|
if (element.type === "matrix") {
|
|
// For matrix elements, we need to handle each row separately
|
|
elementHeadline.forEach((headline, index) => {
|
|
if (answer) {
|
|
const row = element.rows[index];
|
|
if (row && row.label.default && answer[row.label.default] !== undefined) {
|
|
jsonData[idx][headline] = answer[row.label.default];
|
|
} else {
|
|
jsonData[idx][headline] = "";
|
|
}
|
|
}
|
|
});
|
|
} else if (
|
|
element.type === "multipleChoiceMulti" ||
|
|
element.type === "multipleChoiceSingle" ||
|
|
element.type === "ranking"
|
|
) {
|
|
// Set the main response value
|
|
jsonData[idx][elementHeadline[0]] = processResponseData(answer);
|
|
|
|
// Set the option IDs using the reusable function
|
|
if (elementHeadline[1]) {
|
|
const choiceIds = extractChoiceIdsFromResponse(answer, element, response.language || "default");
|
|
jsonData[idx][elementHeadline[1]] = choiceIds.join(", ");
|
|
}
|
|
} else {
|
|
jsonData[idx][elementHeadline[0]] = processResponseData(answer);
|
|
}
|
|
});
|
|
|
|
survey.variables?.forEach((variable) => {
|
|
const answer = response.variables[variable.id];
|
|
jsonData[idx][variable.name] = answer;
|
|
});
|
|
|
|
// user attributes
|
|
userAttributes.forEach((attribute) => {
|
|
jsonData[idx][attribute] = response.contactAttributes?.[attribute] || "";
|
|
});
|
|
|
|
// hidden fields
|
|
hiddenFields.forEach((field) => {
|
|
const value = response.data[field];
|
|
if (Array.isArray(value)) {
|
|
jsonData[idx][field] = value.join("; ");
|
|
} else {
|
|
jsonData[idx][field] = processResponseData(value);
|
|
}
|
|
});
|
|
|
|
if (survey.isVerifyEmailEnabled) {
|
|
const verifiedEmail = response.data["verifiedEmail"];
|
|
jsonData[idx]["Verified Email"] = processResponseData(verifiedEmail);
|
|
}
|
|
});
|
|
|
|
return jsonData;
|
|
};
|
|
|
|
export const getResponseContactAttributes = (
|
|
responses: Pick<TResponse, "contactAttributes" | "data" | "meta">[]
|
|
): TSurveyContactAttributes => {
|
|
try {
|
|
let attributes: TSurveyContactAttributes = {};
|
|
|
|
responses.forEach((response) => {
|
|
Object.keys(response.contactAttributes ?? {}).forEach((key) => {
|
|
if (response.contactAttributes && attributes[key]) {
|
|
attributes[key].push(response.contactAttributes[key].toString());
|
|
} else if (response.contactAttributes) {
|
|
attributes[key] = [response.contactAttributes[key].toString()];
|
|
}
|
|
});
|
|
});
|
|
|
|
Object.keys(attributes).forEach((key) => {
|
|
attributes[key] = Array.from(new Set(attributes[key]));
|
|
});
|
|
|
|
return attributes;
|
|
} catch (error) {
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
export const getResponseMeta = (
|
|
responses: Pick<TResponse, "contactAttributes" | "data" | "meta">[]
|
|
): TSurveyMetaFieldFilter => {
|
|
try {
|
|
const meta: { [key: string]: Set<string> } = {};
|
|
|
|
responses.forEach((response) => {
|
|
Object.entries(response.meta).forEach(([key, value]) => {
|
|
// Handling nested objects (like userAgent)
|
|
if (key === "url") {
|
|
if (!meta[key]) {
|
|
meta[key] = new Set();
|
|
}
|
|
return;
|
|
}
|
|
if (typeof value === "object" && value !== null) {
|
|
Object.entries(value).forEach(([nestedKey, nestedValue]) => {
|
|
if (typeof nestedValue === "string" && nestedValue) {
|
|
if (!meta[nestedKey]) {
|
|
meta[nestedKey] = new Set();
|
|
}
|
|
meta[nestedKey].add(nestedValue);
|
|
}
|
|
});
|
|
} else if (typeof value === "string" && value) {
|
|
if (!meta[key]) {
|
|
meta[key] = new Set();
|
|
}
|
|
meta[key].add(value);
|
|
}
|
|
});
|
|
});
|
|
|
|
// Convert Set to Array
|
|
const result = Object.fromEntries(
|
|
Object.entries(meta).map(([key, valueSet]) => [key, Array.from(valueSet)])
|
|
);
|
|
|
|
return result;
|
|
} catch (error) {
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
export const getResponseHiddenFields = (
|
|
survey: TSurvey,
|
|
responses: Pick<TResponse, "contactAttributes" | "data" | "meta">[]
|
|
): TResponseHiddenFieldsFilter => {
|
|
try {
|
|
const hiddenFields: { [key: string]: Set<string> } = {};
|
|
|
|
const surveyHiddenFields = survey?.hiddenFields.fieldIds;
|
|
const hasHiddenFields = surveyHiddenFields && surveyHiddenFields.length > 0;
|
|
|
|
if (hasHiddenFields) {
|
|
// adding hidden fields to meta
|
|
survey?.hiddenFields.fieldIds?.forEach((fieldId) => {
|
|
hiddenFields[fieldId] = new Set();
|
|
});
|
|
|
|
responses.forEach((response) => {
|
|
// Handling data fields(Hidden fields)
|
|
surveyHiddenFields?.forEach((fieldId) => {
|
|
const hiddenFieldValue = response.data[fieldId];
|
|
if (hiddenFieldValue) {
|
|
if (typeof hiddenFieldValue === "string") {
|
|
hiddenFields[fieldId].add(hiddenFieldValue);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// Convert Set to Array
|
|
const result = Object.fromEntries(
|
|
Object.entries(hiddenFields).map(([key, valueSet]) => [key, Array.from(valueSet)])
|
|
);
|
|
|
|
return result;
|
|
} catch (error) {
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
export const generateAllPermutationsOfSubsets = (array: string[]): string[][] => {
|
|
const subsets: string[][] = [];
|
|
|
|
// Helper function to generate permutations of an array
|
|
const generatePermutations = (arr: string[]): string[][] => {
|
|
const permutations: string[][] = [];
|
|
|
|
// Recursive function to generate permutations
|
|
const permute = (current: string[], remaining: string[]): void => {
|
|
if (remaining.length === 0) {
|
|
permutations.push(current.slice()); // Make a copy of the current permutation
|
|
return;
|
|
}
|
|
|
|
for (let i = 0; i < remaining.length; i++) {
|
|
current.push(remaining[i]);
|
|
permute(current, remaining.slice(0, i).concat(remaining.slice(i + 1)));
|
|
current.pop();
|
|
}
|
|
};
|
|
|
|
permute([], arr);
|
|
return permutations;
|
|
};
|
|
|
|
// Recursive function to generate subsets
|
|
const findSubsets = (currentIndex: number, currentSubset: string[]): void => {
|
|
if (currentIndex === array.length) {
|
|
if (currentSubset.length > 0) {
|
|
// Skip empty subset if not needed
|
|
const allPermutations = generatePermutations(currentSubset);
|
|
subsets.push(...allPermutations); // Spread operator to add all permutations individually
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Include the current element
|
|
findSubsets(currentIndex + 1, currentSubset.concat(array[currentIndex]));
|
|
|
|
// Exclude the current element
|
|
findSubsets(currentIndex + 1, currentSubset);
|
|
};
|
|
|
|
findSubsets(0, []);
|
|
return subsets;
|
|
};
|