mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-05 16:19:55 -06:00
feat: Filter Responses by hidden field values (#2662)
This commit is contained in:
@@ -3,11 +3,7 @@
|
||||
import { getServerSession } from "next-auth";
|
||||
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import {
|
||||
getResponseDownloadUrl,
|
||||
getResponseMeta,
|
||||
getResponsePersonAttributes,
|
||||
} from "@formbricks/lib/response/service";
|
||||
import { getResponseDownloadUrl, getResponseFilteringValues } from "@formbricks/lib/response/service";
|
||||
import { canUserAccessSurvey, verifyUserRoleAccess } from "@formbricks/lib/survey/auth";
|
||||
import { updateSurvey } from "@formbricks/lib/survey/service";
|
||||
import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service";
|
||||
@@ -36,13 +32,12 @@ export const getSurveyFilterDataAction = async (surveyId: string, environmentId:
|
||||
const isAuthorized = await canUserAccessSurvey(session.user.id, surveyId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const [tags, attributes, meta] = await Promise.all([
|
||||
const [tags, { personAttributes: attributes, meta, hiddenFields }] = await Promise.all([
|
||||
getTagsByEnvironmentId(environmentId),
|
||||
getResponsePersonAttributes(surveyId),
|
||||
getResponseMeta(surveyId),
|
||||
getResponseFilteringValues(surveyId),
|
||||
]);
|
||||
|
||||
return { environmentTags: tags, attributes, meta };
|
||||
return { environmentTags: tags, attributes, meta, hiddenFields };
|
||||
};
|
||||
|
||||
export const updateSurveyAction = async (survey: TSurvey): Promise<TSurvey> => {
|
||||
|
||||
@@ -23,13 +23,7 @@ type QuestionFilterComboBoxProps = {
|
||||
filterComboBoxValue: string | string[] | undefined;
|
||||
onChangeFilterValue: (o: string) => void;
|
||||
onChangeFilterComboBoxValue: (o: string | string[]) => void;
|
||||
type:
|
||||
| OptionsType.OTHERS
|
||||
| TSurveyQuestionType
|
||||
| OptionsType.ATTRIBUTES
|
||||
| OptionsType.TAGS
|
||||
| OptionsType.META
|
||||
| undefined;
|
||||
type?: TSurveyQuestionType | Omit<OptionsType, OptionsType.QUESTIONS>;
|
||||
handleRemoveMultiSelect: (value: string[]) => void;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
CheckIcon,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
EyeOff,
|
||||
GlobeIcon,
|
||||
GridIcon,
|
||||
HashIcon,
|
||||
@@ -41,6 +42,7 @@ export enum OptionsType {
|
||||
ATTRIBUTES = "Attributes",
|
||||
OTHERS = "Other Filters",
|
||||
META = "Meta",
|
||||
HIDDEN_FIELDS = "Hidden Fields",
|
||||
}
|
||||
|
||||
export type QuestionOption = {
|
||||
@@ -88,6 +90,9 @@ const SelectedCommandItem = ({ label, questionType, type }: Partial<QuestionOpti
|
||||
}
|
||||
case OptionsType.ATTRIBUTES:
|
||||
return <User width={18} height={18} className="text-white" />;
|
||||
|
||||
case OptionsType.HIDDEN_FIELDS:
|
||||
return <EyeOff width={18} height={18} className="text-white" />;
|
||||
case OptionsType.META:
|
||||
switch (label) {
|
||||
case "device":
|
||||
|
||||
@@ -46,7 +46,7 @@ export const ResponseFilter = ({ survey }: ResponseFilterProps) => {
|
||||
// Fetch the initial data for the filter and load it into the state
|
||||
const handleInitialData = async () => {
|
||||
if (isOpen) {
|
||||
const { attributes, meta, environmentTags } = isSharingPage
|
||||
const { attributes, meta, environmentTags, hiddenFields } = isSharingPage
|
||||
? await getSurveyFilterDataBySurveySharingKeyAction(sharingKey, survey.environmentId)
|
||||
: await getSurveyFilterDataAction(survey.id, survey.environmentId);
|
||||
|
||||
@@ -54,7 +54,8 @@ export const ResponseFilter = ({ survey }: ResponseFilterProps) => {
|
||||
survey,
|
||||
environmentTags,
|
||||
attributes,
|
||||
meta
|
||||
meta,
|
||||
hiddenFields
|
||||
);
|
||||
setSelectedOptions({ questionFilterOptions, questionOptions });
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { QuestionFilterOptions } from "@/app/(app)/environments/[environmentId]/
|
||||
|
||||
import {
|
||||
TResponseFilterCriteria,
|
||||
TResponseHiddenFieldsFilter,
|
||||
TSurveyMetaFieldFilter,
|
||||
TSurveyPersonAttributes,
|
||||
} from "@formbricks/types/responses";
|
||||
@@ -49,7 +50,8 @@ export const generateQuestionAndFilterOptions = (
|
||||
survey: TSurvey,
|
||||
environmentTags: TTag[] | undefined,
|
||||
attributes: TSurveyPersonAttributes,
|
||||
meta: TSurveyMetaFieldFilter
|
||||
meta: TSurveyMetaFieldFilter,
|
||||
hiddenFields: TResponseHiddenFieldsFilter
|
||||
): {
|
||||
questionOptions: QuestionOptions[];
|
||||
questionFilterOptions: QuestionFilterOptions[];
|
||||
@@ -164,6 +166,26 @@ export const generateQuestionAndFilterOptions = (
|
||||
});
|
||||
}
|
||||
|
||||
if (hiddenFields) {
|
||||
questionOptions = [
|
||||
...questionOptions,
|
||||
{
|
||||
header: OptionsType.HIDDEN_FIELDS,
|
||||
option: Object.keys(hiddenFields).map((hiddenField) => {
|
||||
return { label: hiddenField, type: OptionsType.HIDDEN_FIELDS, id: hiddenField };
|
||||
}),
|
||||
},
|
||||
];
|
||||
Object.keys(hiddenFields).forEach((hiddenField) => {
|
||||
questionFilterOptions.push({
|
||||
type: "Hidden Fields",
|
||||
filterOptions: ["Equals", "Not equals"],
|
||||
filterComboBoxOptions: hiddenFields[hiddenField],
|
||||
id: hiddenField,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
let languageQuestion: QuestionOption[] = [];
|
||||
|
||||
//can be extended to include more properties
|
||||
@@ -189,23 +211,29 @@ export const getFormattedFilters = (
|
||||
dateRange: DateRange
|
||||
): TResponseFilterCriteria => {
|
||||
const filters: TResponseFilterCriteria = {};
|
||||
const [questions, tags, attributes, others, meta] = selectedFilter.filter.reduce(
|
||||
(result: [FilterValue[], FilterValue[], FilterValue[], FilterValue[], FilterValue[]], filter) => {
|
||||
if (filter.questionType?.type === "Questions") {
|
||||
result[0].push(filter);
|
||||
} else if (filter.questionType?.type === "Tags") {
|
||||
result[1].push(filter);
|
||||
} else if (filter.questionType?.type === "Attributes") {
|
||||
result[2].push(filter);
|
||||
} else if (filter.questionType?.type === "Other Filters") {
|
||||
result[3].push(filter);
|
||||
} else if (filter.questionType?.type === "Meta") {
|
||||
result[4].push(filter);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
[[], [], [], [], []]
|
||||
);
|
||||
|
||||
const questions: FilterValue[] = [];
|
||||
const tags: FilterValue[] = [];
|
||||
const attributes: FilterValue[] = [];
|
||||
const others: FilterValue[] = [];
|
||||
const meta: FilterValue[] = [];
|
||||
const hiddenFields: FilterValue[] = [];
|
||||
|
||||
selectedFilter.filter.forEach((filter) => {
|
||||
if (filter.questionType?.type === "Questions") {
|
||||
questions.push(filter);
|
||||
} else if (filter.questionType?.type === "Tags") {
|
||||
tags.push(filter);
|
||||
} else if (filter.questionType?.type === "Attributes") {
|
||||
attributes.push(filter);
|
||||
} else if (filter.questionType?.type === "Other Filters") {
|
||||
others.push(filter);
|
||||
} else if (filter.questionType?.type === "Meta") {
|
||||
meta.push(filter);
|
||||
} else if (filter.questionType?.type === "Hidden Fields") {
|
||||
hiddenFields.push(filter);
|
||||
}
|
||||
});
|
||||
|
||||
// for completed responses
|
||||
if (selectedFilter.onlyComplete) {
|
||||
@@ -359,10 +387,30 @@ export const getFormattedFilters = (
|
||||
});
|
||||
}
|
||||
|
||||
// for hidden fields
|
||||
if (hiddenFields.length) {
|
||||
hiddenFields.forEach(({ filterType, questionType }) => {
|
||||
if (!filters.data) filters.data = {};
|
||||
if (!filterType.filterComboBoxValue) return;
|
||||
if (filterType.filterValue === "Equals") {
|
||||
filters.data[questionType.label ?? ""] = {
|
||||
op: "equals",
|
||||
value: filterType.filterComboBoxValue as string,
|
||||
};
|
||||
} else if (filterType.filterValue === "Not equals") {
|
||||
filters.data[questionType.label ?? ""] = {
|
||||
op: "notEquals",
|
||||
value: filterType.filterComboBoxValue as string,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// for attributes
|
||||
if (attributes.length) {
|
||||
attributes.forEach(({ filterType, questionType }) => {
|
||||
if (!filters.personAttributes) filters.personAttributes = {};
|
||||
if (!filterType.filterComboBoxValue) return;
|
||||
if (filterType.filterValue === "Equals") {
|
||||
filters.personAttributes[questionType.label ?? ""] = {
|
||||
op: "equals",
|
||||
@@ -381,6 +429,7 @@ export const getFormattedFilters = (
|
||||
if (others.length) {
|
||||
others.forEach(({ filterType, questionType }) => {
|
||||
if (!filters.others) filters.others = {};
|
||||
if (!filterType.filterComboBoxValue) return;
|
||||
if (filterType.filterValue === "Equals") {
|
||||
filters.others[questionType.label ?? ""] = {
|
||||
op: "equals",
|
||||
@@ -399,6 +448,7 @@ export const getFormattedFilters = (
|
||||
if (meta.length) {
|
||||
meta.forEach(({ filterType, questionType }) => {
|
||||
if (!filters.meta) filters.meta = {};
|
||||
if (!filterType.filterComboBoxValue) return;
|
||||
if (filterType.filterValue === "Equals") {
|
||||
filters.meta[questionType.label ?? ""] = {
|
||||
op: "equals",
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
import {
|
||||
getResponseCountBySurveyId,
|
||||
getResponseMeta,
|
||||
getResponsePersonAttributes,
|
||||
getResponseFilteringValues,
|
||||
getResponses,
|
||||
getSurveySummary,
|
||||
} from "@formbricks/lib/response/service";
|
||||
@@ -53,11 +52,10 @@ export const getSurveyFilterDataBySurveySharingKeyAction = async (
|
||||
const surveyId = await getSurveyIdByResultShareKey(sharingKey);
|
||||
if (!surveyId) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const [tags, attributes, meta] = await Promise.all([
|
||||
const [tags, { personAttributes: attributes, meta, hiddenFields }] = await Promise.all([
|
||||
getTagsByEnvironmentId(environmentId),
|
||||
getResponsePersonAttributes(surveyId),
|
||||
getResponseMeta(surveyId),
|
||||
getResponseFilteringValues(surveyId),
|
||||
]);
|
||||
|
||||
return { environmentTags: tags, attributes, meta };
|
||||
return { environmentTags: tags, attributes, meta, hiddenFields };
|
||||
};
|
||||
|
||||
@@ -14,8 +14,6 @@ import {
|
||||
TResponseInput,
|
||||
TResponseLegacyInput,
|
||||
TResponseUpdateInput,
|
||||
TSurveyMetaFieldFilter,
|
||||
TSurveyPersonAttributes,
|
||||
ZResponseFilterCriteria,
|
||||
ZResponseInput,
|
||||
ZResponseLegacyInput,
|
||||
@@ -43,6 +41,9 @@ import {
|
||||
calculateTtcTotal,
|
||||
extractSurveyDetails,
|
||||
getQuestionWiseSummary,
|
||||
getResponseHiddenFields,
|
||||
getResponseMeta,
|
||||
getResponsePersonAttributes,
|
||||
getResponsesFileName,
|
||||
getResponsesJson,
|
||||
getSurveySummaryDropOff,
|
||||
@@ -387,37 +388,33 @@ export const getResponse = (responseId: string): Promise<TResponse | null> =>
|
||||
}
|
||||
)();
|
||||
|
||||
export const getResponsePersonAttributes = (surveyId: string): Promise<TSurveyPersonAttributes> =>
|
||||
export const getResponseFilteringValues = async (surveyId: string) =>
|
||||
cache(
|
||||
async () => {
|
||||
validateInputs([surveyId, ZId]);
|
||||
|
||||
try {
|
||||
let attributes: TSurveyPersonAttributes = {};
|
||||
const responseAttributes = await prisma.response.findMany({
|
||||
const survey = await getSurvey(surveyId);
|
||||
if (!survey) {
|
||||
throw new ResourceNotFoundError("Survey", surveyId);
|
||||
}
|
||||
|
||||
const responses = await prisma.response.findMany({
|
||||
where: {
|
||||
surveyId: surveyId,
|
||||
surveyId,
|
||||
},
|
||||
select: {
|
||||
data: true,
|
||||
meta: true,
|
||||
personAttributes: true,
|
||||
},
|
||||
});
|
||||
|
||||
responseAttributes.forEach((response) => {
|
||||
Object.keys(response.personAttributes ?? {}).forEach((key) => {
|
||||
if (response.personAttributes && attributes[key]) {
|
||||
attributes[key].push(response.personAttributes[key].toString());
|
||||
} else if (response.personAttributes) {
|
||||
attributes[key] = [response.personAttributes[key].toString()];
|
||||
}
|
||||
});
|
||||
});
|
||||
const personAttributes = getResponsePersonAttributes(responses);
|
||||
const meta = getResponseMeta(responses);
|
||||
const hiddenFields = getResponseHiddenFields(survey, responses);
|
||||
|
||||
Object.keys(attributes).forEach((key) => {
|
||||
attributes[key] = Array.from(new Set(attributes[key]));
|
||||
});
|
||||
|
||||
return attributes;
|
||||
return { personAttributes, meta, hiddenFields };
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
@@ -426,67 +423,7 @@ export const getResponsePersonAttributes = (surveyId: string): Promise<TSurveyPe
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`getResponsePersonAttributes-${surveyId}`],
|
||||
{
|
||||
tags: [responseCache.tag.bySurveyId(surveyId)],
|
||||
}
|
||||
)();
|
||||
|
||||
export const getResponseMeta = (surveyId: string): Promise<TSurveyMetaFieldFilter> =>
|
||||
cache(
|
||||
async () => {
|
||||
validateInputs([surveyId, ZId]);
|
||||
|
||||
try {
|
||||
const responseMeta = await prisma.response.findMany({
|
||||
where: {
|
||||
surveyId: surveyId,
|
||||
},
|
||||
select: {
|
||||
meta: true,
|
||||
},
|
||||
});
|
||||
|
||||
const meta: { [key: string]: Set<string> } = {};
|
||||
|
||||
responseMeta.forEach((response) => {
|
||||
Object.entries(response.meta).forEach(([key, value]) => {
|
||||
// skip url
|
||||
if (key === "url") return;
|
||||
|
||||
// Handling nested objects (like userAgent)
|
||||
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) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`getResponseMeta-${surveyId}`],
|
||||
[`getResponseFilteringValues-${surveyId}`],
|
||||
{
|
||||
tags: [responseCache.tag.bySurveyId(surveyId)],
|
||||
}
|
||||
|
||||
@@ -2,12 +2,7 @@ import { Prisma } from "@prisma/client";
|
||||
import { isAfter, isBefore, isSameDay } from "date-fns";
|
||||
|
||||
import { TDisplay } from "@formbricks/types/displays";
|
||||
import {
|
||||
TResponse,
|
||||
TResponseFilterCriteria,
|
||||
TResponseUpdateInput,
|
||||
TSurveyPersonAttributes,
|
||||
} from "@formbricks/types/responses";
|
||||
import { TResponse, TResponseFilterCriteria, TResponseUpdateInput } from "@formbricks/types/responses";
|
||||
import { TSurveyQuestionType } from "@formbricks/types/surveys";
|
||||
import { TTag } from "@formbricks/types/tags";
|
||||
|
||||
@@ -104,98 +99,6 @@ export const mockResponse: ResponseMock = {
|
||||
ttc: {},
|
||||
};
|
||||
|
||||
export const mockResponsePersonAttributes: ResponseMock[] = [
|
||||
{
|
||||
id: mockResponseId,
|
||||
surveyId: mockSurveyId,
|
||||
singleUseId: mockSingleUseId,
|
||||
data: {},
|
||||
createdAt: new Date(),
|
||||
finished: constantsForTests.boolean,
|
||||
meta: mockMeta,
|
||||
notes: [mockResponseNote],
|
||||
tags: mockTags,
|
||||
personId: mockPersonId,
|
||||
updatedAt: new Date(),
|
||||
ttc: {},
|
||||
person: null,
|
||||
language: null,
|
||||
personAttributes: { Plan: "Paid", "Init Attribute 1": "one", "Init Attribute 2": "two" },
|
||||
},
|
||||
{
|
||||
id: mockResponseId,
|
||||
surveyId: mockSurveyId,
|
||||
singleUseId: mockSingleUseId,
|
||||
data: {},
|
||||
createdAt: new Date(),
|
||||
finished: constantsForTests.boolean,
|
||||
meta: mockMeta,
|
||||
notes: [mockResponseNote],
|
||||
tags: mockTags,
|
||||
personId: mockPersonId,
|
||||
updatedAt: new Date(),
|
||||
ttc: {},
|
||||
person: null,
|
||||
language: null,
|
||||
personAttributes: {
|
||||
Plan: "Paid",
|
||||
"Init Attribute 1": "three",
|
||||
"Init Attribute 2": "four",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: mockResponseId,
|
||||
surveyId: mockSurveyId,
|
||||
singleUseId: mockSingleUseId,
|
||||
data: {},
|
||||
createdAt: new Date(),
|
||||
finished: constantsForTests.boolean,
|
||||
meta: mockMeta,
|
||||
notes: [mockResponseNote],
|
||||
tags: mockTags,
|
||||
personId: mockPersonId,
|
||||
updatedAt: new Date(),
|
||||
ttc: {},
|
||||
person: null,
|
||||
language: null,
|
||||
personAttributes: { Plan: "Paid", "Init Attribute 1": "five", "Init Attribute 2": "six" },
|
||||
},
|
||||
{
|
||||
id: mockResponseId,
|
||||
surveyId: mockSurveyId,
|
||||
singleUseId: mockSingleUseId,
|
||||
data: {},
|
||||
createdAt: new Date(),
|
||||
finished: constantsForTests.boolean,
|
||||
meta: mockMeta,
|
||||
notes: [mockResponseNote],
|
||||
tags: mockTags,
|
||||
personId: mockPersonId,
|
||||
updatedAt: new Date(),
|
||||
ttc: {},
|
||||
person: null,
|
||||
language: null,
|
||||
personAttributes: { Plan: "Paid", "Init Attribute 1": "five", "Init Attribute 2": "four" },
|
||||
},
|
||||
{
|
||||
id: mockResponseId,
|
||||
surveyId: mockSurveyId,
|
||||
singleUseId: mockSingleUseId,
|
||||
data: {},
|
||||
createdAt: new Date(),
|
||||
finished: constantsForTests.boolean,
|
||||
meta: mockMeta,
|
||||
notes: [mockResponseNote],
|
||||
tags: mockTags,
|
||||
personId: mockPersonId,
|
||||
updatedAt: new Date(),
|
||||
ttc: {},
|
||||
person: null,
|
||||
language: null,
|
||||
personAttributes: { Plan: "Paid", "Init Attribute 1": "three", "Init Attribute 2": "two" },
|
||||
},
|
||||
];
|
||||
|
||||
const getMockTags = (tags: string[]): { tag: TTag }[] => {
|
||||
return tags.map((tag) => ({
|
||||
tag: {
|
||||
@@ -445,12 +348,6 @@ export const mockResponseData: TResponseUpdateInput["data"] = {
|
||||
key3: 20,
|
||||
};
|
||||
|
||||
export const mockPersonAttributesData: TSurveyPersonAttributes = {
|
||||
Plan: ["Paid"],
|
||||
"Init Attribute 1": ["one", "three", "five"],
|
||||
"Init Attribute 2": ["two", "four", "six"],
|
||||
};
|
||||
|
||||
export const getMockUpdateResponseInput = (finished: boolean = false): TResponseUpdateInput => ({
|
||||
data: mockResponseData,
|
||||
finished,
|
||||
|
||||
@@ -6,12 +6,10 @@ import {
|
||||
mockEnvironmentId,
|
||||
mockMeta,
|
||||
mockPerson,
|
||||
mockPersonAttributesData,
|
||||
mockPersonId,
|
||||
mockResponse,
|
||||
mockResponseData,
|
||||
mockResponseNote,
|
||||
mockResponsePersonAttributes,
|
||||
mockResponseWithMockPerson,
|
||||
mockSingleUseId,
|
||||
mockSurveyId,
|
||||
@@ -43,7 +41,6 @@ import {
|
||||
getResponseBySingleUseId,
|
||||
getResponseCountBySurveyId,
|
||||
getResponseDownloadUrl,
|
||||
getResponsePersonAttributes,
|
||||
getResponses,
|
||||
getResponsesByEnvironmentId,
|
||||
getResponsesByPersonId,
|
||||
@@ -317,46 +314,6 @@ describe("Tests for getResponse service", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Tests for getAttributesFromResponses service", () => {
|
||||
describe("Happy Path", () => {
|
||||
it("Retrieves all attributes from responses for a given survey ID", async () => {
|
||||
prisma.response.findMany.mockResolvedValue(mockResponsePersonAttributes);
|
||||
const attributes = await getResponsePersonAttributes(mockSurveyId);
|
||||
expect(attributes).toEqual(mockPersonAttributesData);
|
||||
});
|
||||
|
||||
it("Returns an empty Object when no responses with attributes are found for the given survey ID", async () => {
|
||||
prisma.response.findMany.mockResolvedValue([]);
|
||||
|
||||
const responses = await getResponsePersonAttributes(mockSurveyId);
|
||||
expect(responses).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Sad Path", () => {
|
||||
testInputValidation(getResponsePersonAttributes, "123#");
|
||||
|
||||
it("Throws DatabaseError on PrismaClientKnownRequestError", async () => {
|
||||
const mockErrorMessage = "Mock error message";
|
||||
const errToThrow = new Prisma.PrismaClientKnownRequestError(mockErrorMessage, {
|
||||
code: "P2002",
|
||||
clientVersion: "0.0.1",
|
||||
});
|
||||
|
||||
prisma.response.findMany.mockRejectedValue(errToThrow);
|
||||
|
||||
await expect(getResponsePersonAttributes(mockSurveyId)).rejects.toThrow(DatabaseError);
|
||||
});
|
||||
|
||||
it("Throws a generic Error for unexpected problems", async () => {
|
||||
const mockErrorMessage = "Mock error message";
|
||||
prisma.response.findMany.mockRejectedValue(new Error(mockErrorMessage));
|
||||
|
||||
await expect(getResponsePersonAttributes(mockSurveyId)).rejects.toThrow(Error);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Tests for getResponses service", () => {
|
||||
describe("Happy Path", () => {
|
||||
it("Fetches first 10 responses for a given survey ID", async () => {
|
||||
|
||||
@@ -2,7 +2,14 @@ import "server-only";
|
||||
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
import { TResponse, TResponseFilterCriteria, TResponseTtc } from "@formbricks/types/responses";
|
||||
import {
|
||||
TResponse,
|
||||
TResponseFilterCriteria,
|
||||
TResponseHiddenFieldsFilter,
|
||||
TResponseTtc,
|
||||
TSurveyMetaFieldFilter,
|
||||
TSurveyPersonAttributes,
|
||||
} from "@formbricks/types/responses";
|
||||
import {
|
||||
TSurvey,
|
||||
TSurveyLanguage,
|
||||
@@ -1210,3 +1217,110 @@ export const getQuestionWiseSummary = (
|
||||
|
||||
return summary;
|
||||
};
|
||||
|
||||
export const getResponsePersonAttributes = (
|
||||
responses: Pick<TResponse, "personAttributes" | "data" | "meta">[]
|
||||
): TSurveyPersonAttributes => {
|
||||
try {
|
||||
let attributes: TSurveyPersonAttributes = {};
|
||||
|
||||
responses.forEach((response) => {
|
||||
Object.keys(response.personAttributes ?? {}).forEach((key) => {
|
||||
if (response.personAttributes && attributes[key]) {
|
||||
attributes[key].push(response.personAttributes[key].toString());
|
||||
} else if (response.personAttributes) {
|
||||
attributes[key] = [response.personAttributes[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, "personAttributes" | "data" | "meta">[]
|
||||
): TSurveyMetaFieldFilter => {
|
||||
try {
|
||||
const meta: { [key: string]: Set<string> } = {};
|
||||
|
||||
responses.forEach((response) => {
|
||||
Object.entries(response.meta).forEach(([key, value]) => {
|
||||
// skip url
|
||||
if (key === "url") return;
|
||||
|
||||
// Handling nested objects (like userAgent)
|
||||
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, "personAttributes" | "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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -34,6 +34,10 @@ export const ZSurveyMetaFieldFilter = z.record(z.array(z.string()));
|
||||
|
||||
export type TSurveyMetaFieldFilter = z.infer<typeof ZSurveyMetaFieldFilter>;
|
||||
|
||||
export const ZResponseHiddenFieldsFilter = z.record(z.array(z.string()));
|
||||
|
||||
export type TResponseHiddenFieldsFilter = z.infer<typeof ZResponseHiddenFieldsFilter>;
|
||||
|
||||
const ZResponseFilterCriteriaDataLessThan = z.object({
|
||||
op: z.literal(ZSurveyLogicCondition.Values.lessThan),
|
||||
value: z.number(),
|
||||
|
||||
Reference in New Issue
Block a user