mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-30 03:33:48 -05:00
chore: handle people and attributes separately to improve sync performance (#2476)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
import { MonitorIcon } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
+8
-7
@@ -1,9 +1,10 @@
|
||||
import { getAttributes } from "@formbricks/lib/attribute/service";
|
||||
import { getPerson } from "@formbricks/lib/person/service";
|
||||
import { getResponsesByPersonId } from "@formbricks/lib/response/service";
|
||||
import { capitalizeFirstLetter } from "@formbricks/lib/strings";
|
||||
|
||||
export default async function AttributesSection({ personId }: { personId: string }) {
|
||||
const person = await getPerson(personId);
|
||||
const [person, attributes] = await Promise.all([getPerson(personId), getAttributes(personId)]);
|
||||
if (!person) {
|
||||
throw new Error("No such person found");
|
||||
}
|
||||
@@ -18,8 +19,8 @@ export default async function AttributesSection({ personId }: { personId: string
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-slate-500">Email</dt>
|
||||
<dd className="ph-no-capture mt-1 text-sm text-slate-900">
|
||||
{person.attributes.email ? (
|
||||
<span>{person.attributes.email}</span>
|
||||
{attributes.email ? (
|
||||
<span>{attributes.email}</span>
|
||||
) : (
|
||||
<span className="text-slate-300">Not provided</span>
|
||||
)}
|
||||
@@ -28,8 +29,8 @@ export default async function AttributesSection({ personId }: { personId: string
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-slate-500">Language</dt>
|
||||
<dd className="ph-no-capture mt-1 text-sm text-slate-900">
|
||||
{person.attributes.language ? (
|
||||
<span>{person.attributes.language}</span>
|
||||
{attributes.language ? (
|
||||
<span>{attributes.language}</span>
|
||||
) : (
|
||||
<span className="text-slate-300">Not provided</span>
|
||||
)}
|
||||
@@ -50,8 +51,8 @@ export default async function AttributesSection({ personId }: { personId: string
|
||||
<dd className="ph-no-capture mt-1 text-sm text-slate-900">{person.id}</dd>
|
||||
</div>
|
||||
|
||||
{Object.entries(person.attributes)
|
||||
.filter(([key, _]) => key !== "email" && key !== "language")
|
||||
{Object.entries(attributes)
|
||||
.filter(([key, _]) => key !== "email" && key !== "userId" && key !== "language")
|
||||
.map(([key, value]) => (
|
||||
<div key={key}>
|
||||
<dt className="text-sm font-medium text-slate-500">{capitalizeFirstLetter(key.toString())}</dt>
|
||||
|
||||
+5
-1
@@ -1,5 +1,6 @@
|
||||
import { getServerSession } from "next-auth";
|
||||
|
||||
import { getAttributes } from "@formbricks/lib/attribute/service";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/service";
|
||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||
@@ -31,6 +32,9 @@ export default async function HeadingSection({ environmentId, personId }: Headin
|
||||
if (!person) {
|
||||
throw new Error("No such person found");
|
||||
}
|
||||
|
||||
const personAttributes = await getAttributes(person.id);
|
||||
|
||||
const currentUserMembership = await getMembershipByUserIdTeamId(session?.user.id, team.id);
|
||||
const { isViewer } = getAccessFlags(currentUserMembership?.role);
|
||||
|
||||
@@ -39,7 +43,7 @@ export default async function HeadingSection({ environmentId, personId }: Headin
|
||||
<GoBackButton />
|
||||
<div className="flex items-baseline justify-between border-b border-slate-200 pb-6 pt-4">
|
||||
<h1 className="ph-no-capture text-4xl font-bold tracking-tight text-slate-900">
|
||||
<span>{getPersonIdentifier(person)}</span>
|
||||
<span>{getPersonIdentifier(person, personAttributes)}</span>
|
||||
</h1>
|
||||
{!isViewer && (
|
||||
<div className="flex items-center space-x-3">
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
|
||||
import { getAttributes } from "@formbricks/lib/attribute/service";
|
||||
import { getPersonIdentifier } from "@formbricks/lib/person/util";
|
||||
import { TPerson } from "@formbricks/types/people";
|
||||
import { PersonAvatar } from "@formbricks/ui/Avatars";
|
||||
|
||||
export const PersonCard = async ({ person }: { person: TPerson }) => {
|
||||
const attributes = await getAttributes(person.id);
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={`/environments/${person.environmentId}/people/${person.id}`}
|
||||
key={person.id}
|
||||
className="w-full">
|
||||
<div className="m-2 grid h-16 grid-cols-7 content-center rounded-lg hover:bg-slate-100">
|
||||
<div className="col-span-3 flex items-center pl-6 text-sm">
|
||||
<div className="flex items-center">
|
||||
<div className="ph-no-capture h-10 w-10 flex-shrink-0">
|
||||
<PersonAvatar personId={person.id} />
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<div className="ph-no-capture font-medium text-slate-900">
|
||||
<span>{getPersonIdentifier({ id: person.id, userId: person.userId }, attributes)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2 my-auto hidden whitespace-nowrap text-center text-sm text-slate-500 sm:block">
|
||||
<div className="ph-no-capture text-slate-900">{person.userId}</div>
|
||||
</div>
|
||||
<div className="col-span-2 my-auto hidden whitespace-nowrap text-center text-sm text-slate-500 sm:block">
|
||||
<div className="ph-no-capture text-slate-900">{attributes.email}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
+2
-33
@@ -1,16 +1,13 @@
|
||||
import HowToAddPeopleButton from "@/app/(app)/environments/[environmentId]/components/HowToAddPeopleButton";
|
||||
import Link from "next/link";
|
||||
|
||||
import { ITEMS_PER_PAGE } from "@formbricks/lib/constants";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getPeople, getPeopleCount } from "@formbricks/lib/person/service";
|
||||
import { TPerson } from "@formbricks/types/people";
|
||||
import { PersonAvatar } from "@formbricks/ui/Avatars";
|
||||
import EmptySpaceFiller from "@formbricks/ui/EmptySpaceFiller";
|
||||
import { Pagination } from "@formbricks/ui/Pagination";
|
||||
|
||||
const getAttributeValue = (person: TPerson, attributeName: string) =>
|
||||
person.attributes[attributeName]?.toString();
|
||||
import { PersonCard } from "./components/PersonCard";
|
||||
|
||||
export default async function PeoplePage({
|
||||
params,
|
||||
@@ -60,35 +57,7 @@ export default async function PeoplePage({
|
||||
<div className="col-span-2 hidden text-center sm:block">Email</div>
|
||||
</div>
|
||||
{people.map((person) => (
|
||||
<Link
|
||||
href={`/environments/${params.environmentId}/people/${person.id}`}
|
||||
key={person.id}
|
||||
className="w-full">
|
||||
<div className="m-2 grid h-16 grid-cols-7 content-center rounded-lg hover:bg-slate-100">
|
||||
<div className="col-span-3 flex items-center pl-6 text-sm">
|
||||
<div className="flex items-center">
|
||||
<div className="ph-no-capture h-10 w-10 flex-shrink-0">
|
||||
<PersonAvatar personId={person.id} />
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<div className="ph-no-capture font-medium text-slate-900">
|
||||
{getAttributeValue(person, "email") ? (
|
||||
<span>{getAttributeValue(person, "email")}</span>
|
||||
) : (
|
||||
<span>{person.id}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2 my-auto hidden whitespace-nowrap text-center text-sm text-slate-500 sm:block">
|
||||
<div className="ph-no-capture text-slate-900">{person.userId}</div>
|
||||
</div>
|
||||
<div className="col-span-2 my-auto hidden whitespace-nowrap text-center text-sm text-slate-500 sm:block">
|
||||
<div className="ph-no-capture text-slate-900">{getAttributeValue(person, "email")}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
<PersonCard person={person} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ export const AddressSummary = ({ questionSummary, environmentId }: AddressSummar
|
||||
<PersonAvatar personId={response.person.id} />
|
||||
</div>
|
||||
<p className="ph-no-capture break-all text-slate-600 group-hover:underline md:ml-2">
|
||||
{getPersonIdentifier(response.person)}
|
||||
{getPersonIdentifier(response.person, response.personAttributes)}
|
||||
</p>
|
||||
</Link>
|
||||
) : (
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@ export const DateQuestionSummary = ({ questionSummary, environmentId }: DateQues
|
||||
<PersonAvatar personId={response.person.id} />
|
||||
</div>
|
||||
<p className="ph-no-capture break-all text-slate-600 group-hover:underline md:ml-2">
|
||||
{getPersonIdentifier(response.person)}
|
||||
{getPersonIdentifier(response.person, response.personAttributes)}
|
||||
</p>
|
||||
</Link>
|
||||
) : (
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ export const FileUploadSummary = ({ questionSummary, environmentId }: FileUpload
|
||||
<PersonAvatar personId={response.person.id} />
|
||||
</div>
|
||||
<p className="ph-no-capture break-all text-slate-600 group-hover:underline md:ml-2">
|
||||
{getPersonIdentifier(response.person)}
|
||||
{getPersonIdentifier(response.person, response.personAttributes)}
|
||||
</p>
|
||||
</Link>
|
||||
) : (
|
||||
|
||||
+1
-1
@@ -59,7 +59,7 @@ export const HiddenFieldsSummary = ({ environment, questionSummary }: HiddenFiel
|
||||
<PersonAvatar personId={response.person.id} />
|
||||
</div>
|
||||
<p className="ph-no-capture break-all text-slate-600 group-hover:underline md:ml-2">
|
||||
{getPersonIdentifier(response.person)}
|
||||
{getPersonIdentifier(response.person, response.personAttributes)}
|
||||
</p>
|
||||
</Link>
|
||||
) : (
|
||||
|
||||
+1
-1
@@ -97,7 +97,7 @@ export const MultipleChoiceSummary = ({
|
||||
</div>
|
||||
<div className="ph-no-capture col-span-1 flex items-center space-x-4 pl-6 font-medium text-slate-900">
|
||||
{otherValue.person.id && <PersonAvatar personId={otherValue.person.id} />}
|
||||
<span>{getPersonIdentifier(otherValue.person)}</span>
|
||||
<span>{getPersonIdentifier(otherValue.person, otherValue.personAttributes)}</span>
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
+1
-1
@@ -47,7 +47,7 @@ export const OpenTextSummary = ({ questionSummary, environmentId }: OpenTextSumm
|
||||
<PersonAvatar personId={response.person.id} />
|
||||
</div>
|
||||
<p className="ph-no-capture break-all text-slate-600 group-hover:underline md:ml-2">
|
||||
{getPersonIdentifier(response.person)}
|
||||
{getPersonIdentifier(response.person, response.personAttributes)}
|
||||
</p>
|
||||
</Link>
|
||||
) : (
|
||||
|
||||
+11
-6
@@ -1,8 +1,12 @@
|
||||
// Deprecated since 2024-04-13
|
||||
// last supported js version 1.6.5
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createPerson, getPersonByUserId, updatePerson } from "@formbricks/lib/person/service";
|
||||
import { ZPersonUpdateInput } from "@formbricks/types/people";
|
||||
import { getAttributesByUserId, updateAttributes } from "@formbricks/lib/attribute/service";
|
||||
import { createPerson, getPersonByUserId } from "@formbricks/lib/person/service";
|
||||
import { ZAttributes } from "@formbricks/types/attributes";
|
||||
|
||||
interface Context {
|
||||
params: {
|
||||
@@ -21,7 +25,7 @@ export async function POST(req: Request, context: Context): Promise<Response> {
|
||||
const jsonInput = await req.json();
|
||||
|
||||
// validate using zod
|
||||
const inputValidation = ZPersonUpdateInput.safeParse(jsonInput);
|
||||
const inputValidation = z.object({ attributes: ZAttributes }).safeParse(jsonInput);
|
||||
|
||||
if (!inputValidation.success) {
|
||||
return responses.badRequestResponse(
|
||||
@@ -42,8 +46,8 @@ export async function POST(req: Request, context: Context): Promise<Response> {
|
||||
person = await createPerson(environmentId, userId);
|
||||
}
|
||||
|
||||
// Check if the person is already up to date
|
||||
const oldAttributes = person.attributes;
|
||||
const oldAttributes = await getAttributesByUserId(environmentId, userId);
|
||||
|
||||
let isUpToDate = true;
|
||||
for (const key in updatedAttributes) {
|
||||
if (updatedAttributes[key] !== oldAttributes[key]) {
|
||||
@@ -51,6 +55,7 @@ export async function POST(req: Request, context: Context): Promise<Response> {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isUpToDate) {
|
||||
return responses.successResponse(
|
||||
{
|
||||
@@ -61,7 +66,7 @@ export async function POST(req: Request, context: Context): Promise<Response> {
|
||||
);
|
||||
}
|
||||
|
||||
await updatePerson(person.id, inputValidation.data);
|
||||
await updateAttributes(person.id, updatedAttributes);
|
||||
|
||||
return responses.successResponse(
|
||||
{
|
||||
+5
-17
@@ -2,14 +2,14 @@ import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
|
||||
import { getActionClasses } from "@formbricks/lib/actionClass/service";
|
||||
import { createAttributeClass, getAttributeClassByName } from "@formbricks/lib/attributeClass/service";
|
||||
import { updateAttributes } from "@formbricks/lib/attribute/service";
|
||||
import { personCache } from "@formbricks/lib/person/cache";
|
||||
import { getPerson, updatePersonAttribute } from "@formbricks/lib/person/service";
|
||||
import { getPerson } from "@formbricks/lib/person/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { surveyCache } from "@formbricks/lib/survey/cache";
|
||||
import { getSyncSurveys } from "@formbricks/lib/survey/service";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
import { TJsAppStateSync, ZJsPeopleAttributeInput } from "@formbricks/types/js";
|
||||
import { ZJsPeopleAttributeInput } from "@formbricks/types/js";
|
||||
|
||||
interface Context {
|
||||
params: {
|
||||
@@ -47,19 +47,7 @@ export async function POST(req: Request, context: Context): Promise<Response> {
|
||||
return responses.notFoundResponse("Person", personId, true);
|
||||
}
|
||||
|
||||
let attributeClass = await getAttributeClassByName(environmentId, key);
|
||||
|
||||
// create new attribute class if not found
|
||||
if (attributeClass === null) {
|
||||
attributeClass = await createAttributeClass(environmentId, key, "code");
|
||||
}
|
||||
|
||||
if (!attributeClass) {
|
||||
return responses.internalServerErrorResponse("Unable to create attribute class", true);
|
||||
}
|
||||
|
||||
// upsert attribute (update or create)
|
||||
await updatePersonAttribute(personId, attributeClass.id, value);
|
||||
await updateAttributes(personId, { [key]: value });
|
||||
|
||||
personCache.revalidate({
|
||||
id: personId,
|
||||
@@ -87,7 +75,7 @@ export async function POST(req: Request, context: Context): Promise<Response> {
|
||||
}
|
||||
|
||||
// return state
|
||||
const state: TJsAppStateSync = {
|
||||
const state = {
|
||||
person: { id: person.id, userId: person.userId },
|
||||
surveys,
|
||||
noCodeActionClasses: noCodeActionClasses.filter((actionClass) => actionClass.type === "noCode"),
|
||||
|
||||
@@ -2,14 +2,14 @@ import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
|
||||
import { getActionClasses } from "@formbricks/lib/actionClass/service";
|
||||
import { createAttributeClass, getAttributeClassByName } from "@formbricks/lib/attributeClass/service";
|
||||
import { updateAttributes } from "@formbricks/lib/attribute/service";
|
||||
import { personCache } from "@formbricks/lib/person/cache";
|
||||
import { getPerson, updatePersonAttribute } from "@formbricks/lib/person/service";
|
||||
import { getPerson } from "@formbricks/lib/person/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { surveyCache } from "@formbricks/lib/survey/cache";
|
||||
import { getSyncSurveys } from "@formbricks/lib/survey/service";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
import { TJsAppStateSync, ZJsPeopleAttributeInput } from "@formbricks/types/js";
|
||||
import { ZJsPeopleAttributeInput } from "@formbricks/types/js";
|
||||
|
||||
interface Context {
|
||||
params: {
|
||||
@@ -46,19 +46,7 @@ export async function POST(req: Request, context: Context): Promise<Response> {
|
||||
return responses.notFoundResponse("Person", personId, true);
|
||||
}
|
||||
|
||||
let attributeClass = await getAttributeClassByName(environmentId, key);
|
||||
|
||||
// create new attribute class if not found
|
||||
if (attributeClass === null) {
|
||||
attributeClass = await createAttributeClass(environmentId, key, "code");
|
||||
}
|
||||
|
||||
if (!attributeClass) {
|
||||
return responses.internalServerErrorResponse("Unable to create attribute class", true);
|
||||
}
|
||||
|
||||
// upsert attribute (update or create)
|
||||
await updatePersonAttribute(personId, attributeClass.id, value);
|
||||
await updateAttributes(personId, { [key]: value });
|
||||
|
||||
personCache.revalidate({
|
||||
id: personId,
|
||||
@@ -86,7 +74,7 @@ export async function POST(req: Request, context: Context): Promise<Response> {
|
||||
}
|
||||
|
||||
// return state
|
||||
const state: TJsAppStateSync = {
|
||||
const state = {
|
||||
person: { id: person.id, userId: person.userId },
|
||||
surveys,
|
||||
noCodeActionClasses: noCodeActionClasses.filter((actionClass) => actionClass.type === "noCode"),
|
||||
|
||||
@@ -2,12 +2,11 @@ import { getUpdatedState } from "@/app/api/v1/(legacy)/js/sync/lib/sync";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
|
||||
import { createAttributeClass, getAttributeClassByName } from "@formbricks/lib/attributeClass/service";
|
||||
import { updateAttributes } from "@formbricks/lib/attribute/service";
|
||||
import { personCache } from "@formbricks/lib/person/cache";
|
||||
import { getPerson, updatePersonAttribute } from "@formbricks/lib/person/service";
|
||||
import { getPerson } from "@formbricks/lib/person/service";
|
||||
import { surveyCache } from "@formbricks/lib/survey/cache";
|
||||
import { ZJsPeopleLegacyAttributeInput } from "@formbricks/types/js";
|
||||
import { TPersonClient } from "@formbricks/types/people";
|
||||
|
||||
export async function OPTIONS(): Promise<Response> {
|
||||
return responses.successResponse({}, true);
|
||||
@@ -42,19 +41,7 @@ export async function POST(req: Request, { params }): Promise<Response> {
|
||||
return responses.notFoundResponse("Person", personId, true);
|
||||
}
|
||||
|
||||
let attributeClass = await getAttributeClassByName(environmentId, key);
|
||||
|
||||
// create new attribute class if not found
|
||||
if (attributeClass === null) {
|
||||
attributeClass = await createAttributeClass(environmentId, key, "code");
|
||||
}
|
||||
|
||||
if (!attributeClass) {
|
||||
return responses.internalServerErrorResponse("Unable to create attribute class", true);
|
||||
}
|
||||
|
||||
// upsert attribute (update or create)
|
||||
await updatePersonAttribute(personId, attributeClass.id, value);
|
||||
await updateAttributes(personId, { [key]: value });
|
||||
|
||||
personCache.revalidate({
|
||||
id: personId,
|
||||
@@ -67,7 +54,7 @@ export async function POST(req: Request, { params }): Promise<Response> {
|
||||
|
||||
const state = await getUpdatedState(environmentId, personId);
|
||||
|
||||
let person: TPersonClient | null = null;
|
||||
let person: { id: string; userId: string } | null = null;
|
||||
if (state.person && "id" in state.person && "userId" in state.person) {
|
||||
person = {
|
||||
id: state.person.id,
|
||||
|
||||
@@ -3,7 +3,6 @@ import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
|
||||
import { ZJsSyncLegacyInput } from "@formbricks/types/js";
|
||||
import { TPersonClient } from "@formbricks/types/people";
|
||||
|
||||
export async function OPTIONS(): Promise<Response> {
|
||||
return responses.successResponse({}, true);
|
||||
@@ -28,7 +27,7 @@ export async function POST(req: Request): Promise<Response> {
|
||||
|
||||
const state = await getUpdatedState(environmentId, personId);
|
||||
|
||||
let person: TPersonClient | null = null;
|
||||
let person: { id: string; userId: string } | null = null;
|
||||
if (state.person && "id" in state.person && "userId" in state.person) {
|
||||
person = {
|
||||
id: state.person.id,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { NextRequest, userAgent } from "next/server";
|
||||
|
||||
import { getActionClasses } from "@formbricks/lib/actionClass/service";
|
||||
import { getAttribute } from "@formbricks/lib/attribute/service";
|
||||
import {
|
||||
IS_FORMBRICKS_CLOUD,
|
||||
PRICING_APPSURVEYS_FREE_RESPONSES,
|
||||
@@ -149,18 +150,6 @@ export async function GET(
|
||||
if (!product) {
|
||||
throw new Error("Product not found");
|
||||
}
|
||||
const languageAttribute = person.attributes.language;
|
||||
const isLanguageAvailable = Boolean(languageAttribute);
|
||||
|
||||
const personData = version
|
||||
? {
|
||||
...(isLanguageAvailable && { attributes: { language: languageAttribute } }),
|
||||
}
|
||||
: {
|
||||
id: person.id,
|
||||
userId: person.userId,
|
||||
...(isLanguageAvailable && { attributes: { language: languageAttribute } }),
|
||||
};
|
||||
|
||||
// Define 'transformedSurveys' which can be an array of either TLegacySurvey or TSurvey.
|
||||
let transformedSurveys: TLegacySurvey[] | TSurvey[];
|
||||
@@ -189,11 +178,14 @@ export async function GET(
|
||||
}),
|
||||
};
|
||||
|
||||
const language = await getAttribute("language", person.id);
|
||||
|
||||
// return state
|
||||
const state: TJsAppStateSync = {
|
||||
person: personData,
|
||||
...(version && !isVersionGreaterThanOrEqualTo(version, "2.0.0") && { person }),
|
||||
surveys: !isInAppSurveyLimitReached ? transformedSurveys : [],
|
||||
noCodeActionClasses: noCodeActionClasses.filter((actionClass) => actionClass.type === "noCode"),
|
||||
language,
|
||||
product: updatedProduct,
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
import { getAttributesByUserId, updateAttributes } from "@formbricks/lib/attribute/service";
|
||||
import { createPerson, getPersonByUserId } from "@formbricks/lib/person/service";
|
||||
import { ZJsPeopleUpdateAttributeInput } from "@formbricks/types/js";
|
||||
|
||||
export async function OPTIONS() {
|
||||
// cors headers
|
||||
return responses.successResponse({}, true);
|
||||
}
|
||||
|
||||
export async function PUT(req: NextRequest, context: { params: { environmentId: string; userId: string } }) {
|
||||
try {
|
||||
const environmentId = context.params.environmentId;
|
||||
if (!environmentId) {
|
||||
return responses.badRequestResponse("environmentId is required", { environmentId }, true);
|
||||
}
|
||||
|
||||
const userId = context.params.userId;
|
||||
if (!userId) {
|
||||
return responses.badRequestResponse("userId is required", { userId }, true);
|
||||
}
|
||||
|
||||
const jsonInput = await req.json();
|
||||
|
||||
const parsedInput = ZJsPeopleUpdateAttributeInput.safeParse(jsonInput);
|
||||
if (!parsedInput.success) {
|
||||
return responses.badRequestResponse(
|
||||
"Fields are missing or incorrectly formatted",
|
||||
transformErrorToDetails(parsedInput.error),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
const { userId: userIdAttr, ...updatedAttributes } = parsedInput.data.attributes;
|
||||
|
||||
let person = await getPersonByUserId(environmentId, userId);
|
||||
|
||||
if (!person) {
|
||||
// return responses.notFoundResponse("PersonByUserId", userId, true);
|
||||
// HOTFIX: create person if not found to work around caching issue
|
||||
person = await createPerson(environmentId, userId);
|
||||
}
|
||||
|
||||
const oldAttributes = await getAttributesByUserId(environmentId, userId);
|
||||
|
||||
let isUpToDate = true;
|
||||
for (const key in updatedAttributes) {
|
||||
if (updatedAttributes[key] !== oldAttributes[key]) {
|
||||
isUpToDate = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isUpToDate) {
|
||||
return responses.successResponse(
|
||||
{
|
||||
changed: false,
|
||||
message: "No updates were necessary; the person is already up to date.",
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
await updateAttributes(person.id, updatedAttributes);
|
||||
|
||||
return responses.successResponse(
|
||||
{
|
||||
changed: true,
|
||||
message: "The person was successfully updated.",
|
||||
},
|
||||
true
|
||||
);
|
||||
} catch (err) {
|
||||
return responses.internalServerErrorResponse("Something went wrong", true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { TAttributeUpdateInput } from "@formbricks/types/attributes";
|
||||
import { Result } from "@formbricks/types/errorHandlers";
|
||||
import { NetworkError } from "@formbricks/types/errors";
|
||||
|
||||
import { makeRequest } from "../../utils/makeRequest";
|
||||
|
||||
export class AttributeAPI {
|
||||
private apiHost: string;
|
||||
private environmentId: string;
|
||||
|
||||
constructor(apiHost: string, environmentId: string) {
|
||||
this.apiHost = apiHost;
|
||||
this.environmentId = environmentId;
|
||||
}
|
||||
|
||||
async update(
|
||||
attributeUpdateInput: Omit<TAttributeUpdateInput, "environmentId">
|
||||
): Promise<Result<{ changed: boolean; message: string }, NetworkError | Error>> {
|
||||
return makeRequest(
|
||||
this.apiHost,
|
||||
`/api/v1/client/${this.environmentId}/people/${attributeUpdateInput.userId}/attributes`,
|
||||
"PUT",
|
||||
{ attributes: attributeUpdateInput.attributes }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ApiConfig } from "../../types";
|
||||
import { ActionAPI } from "./action";
|
||||
import { AttributeAPI } from "./attribute";
|
||||
import { DisplayAPI } from "./display";
|
||||
import { PeopleAPI } from "./people";
|
||||
import { ResponseAPI } from "./response";
|
||||
@@ -11,6 +12,7 @@ export class Client {
|
||||
action: ActionAPI;
|
||||
people: PeopleAPI;
|
||||
storage: StorageAPI;
|
||||
attribute: AttributeAPI;
|
||||
|
||||
constructor(options: ApiConfig) {
|
||||
const { apiHost, environmentId } = options;
|
||||
@@ -19,6 +21,7 @@ export class Client {
|
||||
this.display = new DisplayAPI(apiHost, environmentId);
|
||||
this.action = new ActionAPI(apiHost, environmentId);
|
||||
this.people = new PeopleAPI(apiHost, environmentId);
|
||||
this.attribute = new AttributeAPI(apiHost, environmentId);
|
||||
this.storage = new StorageAPI(apiHost, environmentId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Result } from "@formbricks/types/errorHandlers";
|
||||
import { NetworkError } from "@formbricks/types/errors";
|
||||
import { TPersonUpdateInput } from "@formbricks/types/people";
|
||||
|
||||
import { makeRequest } from "../../utils/makeRequest";
|
||||
|
||||
@@ -19,16 +18,4 @@ export class PeopleAPI {
|
||||
userId,
|
||||
});
|
||||
}
|
||||
|
||||
async update(
|
||||
userId: string,
|
||||
personInput: TPersonUpdateInput
|
||||
): Promise<Result<{ changed: boolean; message: string }, NetworkError | Error>> {
|
||||
return makeRequest(
|
||||
this.apiHost,
|
||||
`/api/v1/client/${this.environmentId}/people/${userId}`,
|
||||
"POST",
|
||||
personInput
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,7 +172,7 @@ export const sendResponseFinishedEmail = async (
|
||||
response: TResponse,
|
||||
responseCount: number
|
||||
) => {
|
||||
const personEmail = response.person?.attributes["email"];
|
||||
const personEmail = response.personAttributes?.email;
|
||||
const team = await getTeamByEnvironmentId(environmentId);
|
||||
|
||||
await sendEmail({
|
||||
|
||||
@@ -5,9 +5,10 @@ import { ErrorHandler } from "../shared/errors";
|
||||
import { Logger } from "../shared/logger";
|
||||
import { trackAction } from "./lib/actions";
|
||||
import { getApi } from "./lib/api";
|
||||
import { setAttributeInApp } from "./lib/attributes";
|
||||
import { initialize } from "./lib/initialize";
|
||||
import { checkPageUrl } from "./lib/noCodeActions";
|
||||
import { logoutPerson, resetPerson, setPersonAttribute } from "./lib/person";
|
||||
import { logoutPerson, resetPerson } from "./lib/person";
|
||||
|
||||
const logger = Logger.getInstance();
|
||||
|
||||
@@ -26,7 +27,7 @@ const setEmail = async (email: string): Promise<void> => {
|
||||
};
|
||||
|
||||
const setAttribute = async (key: string, value: any): Promise<void> => {
|
||||
queue.add(true, "app", setPersonAttribute, key, value);
|
||||
queue.add(true, "app", setAttributeInApp, key, value);
|
||||
await queue.wait();
|
||||
};
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ export const trackAction = async (name: string): Promise<Result<void, NetworkErr
|
||||
environmentId: inAppConfig.get().environmentId,
|
||||
apiHost: inAppConfig.get().apiHost,
|
||||
userId,
|
||||
attributes: inAppConfig.get().state.attributes,
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import { FormbricksAPI } from "@formbricks/api";
|
||||
import { TAttributes } from "@formbricks/types/attributes";
|
||||
|
||||
import { MissingPersonError, NetworkError, Result, err, ok, okVoid } from "../../shared/errors";
|
||||
import { Logger } from "../../shared/logger";
|
||||
import { AppConfig } from "./config";
|
||||
|
||||
const appConfig = AppConfig.getInstance();
|
||||
const logger = Logger.getInstance();
|
||||
|
||||
export const updateAttribute = async (key: string, value: string): Promise<Result<void, NetworkError>> => {
|
||||
const { apiHost, environmentId, userId } = appConfig.get();
|
||||
|
||||
const api = new FormbricksAPI({
|
||||
apiHost,
|
||||
environmentId,
|
||||
});
|
||||
|
||||
const res = await api.client.attribute.update({ userId, attributes: { [key]: value } });
|
||||
|
||||
if (!res.ok) {
|
||||
return err({
|
||||
code: "network_error",
|
||||
status: 500,
|
||||
message: `Error updating person with userId ${userId}`,
|
||||
url: `${appConfig.get().apiHost}/api/v1/client/${environmentId}/people/${userId}/attributes`,
|
||||
responseMessage: res.error.message,
|
||||
});
|
||||
}
|
||||
|
||||
if (res.data.changed) {
|
||||
logger.debug("Attribute updated in Formbricks");
|
||||
}
|
||||
|
||||
return okVoid();
|
||||
};
|
||||
|
||||
export const updateAttributes = async (
|
||||
apiHost: string,
|
||||
environmentId: string,
|
||||
userId: string,
|
||||
attributes: TAttributes
|
||||
): Promise<Result<TAttributes, NetworkError>> => {
|
||||
// clean attributes and remove existing attributes if config already exists
|
||||
const updatedAttributes = { ...attributes };
|
||||
|
||||
try {
|
||||
const existingAttributes = appConfig.get()?.state?.attributes;
|
||||
if (existingAttributes) {
|
||||
for (const [key, value] of Object.entries(existingAttributes)) {
|
||||
if (updatedAttributes[key] === value) {
|
||||
delete updatedAttributes[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logger.debug("config not set; sending all attributes to backend");
|
||||
}
|
||||
|
||||
// send to backend if updatedAttributes is not empty
|
||||
if (Object.keys(updatedAttributes).length === 0) {
|
||||
logger.debug("No attributes to update. Skipping update.");
|
||||
return ok(updatedAttributes);
|
||||
}
|
||||
|
||||
logger.debug("Updating attributes: " + JSON.stringify(updatedAttributes));
|
||||
|
||||
const api = new FormbricksAPI({
|
||||
apiHost,
|
||||
environmentId,
|
||||
});
|
||||
|
||||
const res = await api.client.attribute.update({ userId, attributes: updatedAttributes });
|
||||
|
||||
if (res.ok) {
|
||||
return ok(updatedAttributes);
|
||||
}
|
||||
|
||||
return err({
|
||||
code: "network_error",
|
||||
status: 500,
|
||||
message: `Error updating person with userId ${userId}`,
|
||||
url: `${apiHost}/api/v1/client/${environmentId}/people/${userId}/attributes`,
|
||||
responseMessage: res.error.message,
|
||||
});
|
||||
};
|
||||
|
||||
export const isExistingAttribute = (key: string, value: string): boolean => {
|
||||
if (appConfig.get().state.attributes[key] === value) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const setAttributeInApp = async (
|
||||
key: string,
|
||||
value: any
|
||||
): Promise<Result<void, NetworkError | MissingPersonError>> => {
|
||||
if (key === "userId") {
|
||||
logger.error("Setting userId is no longer supported. Please set the userId in the init call instead.");
|
||||
return okVoid();
|
||||
}
|
||||
|
||||
logger.debug("Setting attribute: " + key + " to value: " + value);
|
||||
// check if attribute already exists with this value
|
||||
if (isExistingAttribute(key, value.toString())) {
|
||||
logger.debug("Attribute already set to this value. Skipping update.");
|
||||
return okVoid();
|
||||
}
|
||||
|
||||
const result = await updateAttribute(key, value.toString());
|
||||
|
||||
if (result.ok) {
|
||||
// udpdate attribute in config
|
||||
appConfig.update({
|
||||
environmentId: appConfig.get().environmentId,
|
||||
apiHost: appConfig.get().apiHost,
|
||||
userId: appConfig.get().userId,
|
||||
state: {
|
||||
...appConfig.get().state,
|
||||
attributes: {
|
||||
...appConfig.get().state.attributes,
|
||||
[key]: value.toString(),
|
||||
},
|
||||
},
|
||||
expiresAt: appConfig.get().expiresAt,
|
||||
});
|
||||
return okVoid();
|
||||
}
|
||||
|
||||
return err(result.error);
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TAttributes } from "@formbricks/types/attributes";
|
||||
import type { TJSAppConfig, TJsAppConfigInput } from "@formbricks/types/js";
|
||||
import { TPersonAttributes } from "@formbricks/types/people";
|
||||
|
||||
import {
|
||||
ErrorHandler,
|
||||
@@ -15,14 +15,14 @@ import {
|
||||
import { Logger } from "../../shared/logger";
|
||||
import { getIsDebug } from "../../shared/utils";
|
||||
import { trackAction } from "./actions";
|
||||
import { updateAttributes } from "./attributes";
|
||||
import { AppConfig, IN_APP_LOCAL_STORAGE_KEY } from "./config";
|
||||
import { addCleanupEventListeners, addEventListeners, removeAllEventListeners } from "./eventListeners";
|
||||
import { checkPageUrl } from "./noCodeActions";
|
||||
import { updatePersonAttributes } from "./person";
|
||||
import { sync } from "./sync";
|
||||
import { addWidgetContainer, removeWidgetContainer, setIsSurveyRunning } from "./widget";
|
||||
|
||||
const inAppConfig = AppConfig.getInstance();
|
||||
const appConfig = AppConfig.getInstance();
|
||||
const logger = Logger.getInstance();
|
||||
|
||||
let isInitialized = false;
|
||||
@@ -45,7 +45,7 @@ export const initialize = async (
|
||||
|
||||
let existingConfig: TJSAppConfig | undefined;
|
||||
try {
|
||||
existingConfig = inAppConfig.get();
|
||||
existingConfig = appConfig.get();
|
||||
logger.debug("Found existing configuration.");
|
||||
} catch (e) {
|
||||
logger.debug("No existing configuration found.");
|
||||
@@ -95,25 +95,18 @@ export const initialize = async (
|
||||
logger.debug("Adding widget container to DOM");
|
||||
addWidgetContainer();
|
||||
|
||||
let updatedAttributes: TPersonAttributes | null = null;
|
||||
let updatedAttributes: TAttributes | null = null;
|
||||
if (configInput.attributes) {
|
||||
if (!configInput.userId) {
|
||||
// Allow setting attributes for unidentified users
|
||||
updatedAttributes = { ...configInput.attributes };
|
||||
}
|
||||
// If userId is available, update attributes in backend
|
||||
else {
|
||||
const res = await updatePersonAttributes(
|
||||
configInput.apiHost,
|
||||
configInput.environmentId,
|
||||
configInput.userId,
|
||||
configInput.attributes
|
||||
);
|
||||
if (res.ok !== true) {
|
||||
return err(res.error);
|
||||
}
|
||||
updatedAttributes = res.value;
|
||||
const res = await updateAttributes(
|
||||
configInput.apiHost,
|
||||
configInput.environmentId,
|
||||
configInput.userId,
|
||||
configInput.attributes
|
||||
);
|
||||
if (res.ok !== true) {
|
||||
return err(res.error);
|
||||
}
|
||||
updatedAttributes = res.value;
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -139,13 +132,13 @@ export const initialize = async (
|
||||
}
|
||||
} else {
|
||||
logger.debug("Configuration not expired. Extending expiration.");
|
||||
inAppConfig.update(existingConfig);
|
||||
appConfig.update(existingConfig);
|
||||
}
|
||||
} else {
|
||||
logger.debug(
|
||||
"No valid configuration found or it has been expired. Resetting config and creating new one."
|
||||
);
|
||||
inAppConfig.resetConfig();
|
||||
appConfig.resetConfig();
|
||||
logger.debug("Syncing.");
|
||||
|
||||
try {
|
||||
@@ -162,15 +155,15 @@ export const initialize = async (
|
||||
}
|
||||
// update attributes in config
|
||||
if (updatedAttributes && Object.keys(updatedAttributes).length > 0) {
|
||||
inAppConfig.update({
|
||||
environmentId: inAppConfig.get().environmentId,
|
||||
apiHost: inAppConfig.get().apiHost,
|
||||
userId: inAppConfig.get().userId,
|
||||
appConfig.update({
|
||||
environmentId: appConfig.get().environmentId,
|
||||
apiHost: appConfig.get().apiHost,
|
||||
userId: appConfig.get().userId,
|
||||
state: {
|
||||
...inAppConfig.get().state,
|
||||
attributes: { ...inAppConfig.get().state.attributes, ...configInput.attributes },
|
||||
...appConfig.get().state,
|
||||
attributes: { ...appConfig.get().state.attributes, ...configInput.attributes },
|
||||
},
|
||||
expiresAt: inAppConfig.get().expiresAt,
|
||||
expiresAt: appConfig.get().expiresAt,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -221,8 +214,8 @@ export const deinitalize = (): void => {
|
||||
export const putFormbricksInErrorState = (): void => {
|
||||
logger.debug("Putting formbricks in error state");
|
||||
// change formbricks status to error
|
||||
inAppConfig.update({
|
||||
...inAppConfig.get(),
|
||||
appConfig.update({
|
||||
...appConfig.get(),
|
||||
status: "error",
|
||||
expiresAt: new Date(new Date().getTime() + 10 * 60000), // 10 minutes in the future
|
||||
});
|
||||
|
||||
@@ -1,164 +1,25 @@
|
||||
import { FormbricksAPI } from "@formbricks/api";
|
||||
import { TPersonAttributes, TPersonUpdateInput } from "@formbricks/types/people";
|
||||
|
||||
import { MissingPersonError, NetworkError, Result, err, ok, okVoid } from "../../shared/errors";
|
||||
import { NetworkError, Result, err, okVoid } from "../../shared/errors";
|
||||
import { Logger } from "../../shared/logger";
|
||||
import { AppConfig } from "./config";
|
||||
import { deinitalize, initialize } from "./initialize";
|
||||
import { closeSurvey } from "./widget";
|
||||
|
||||
const inAppConfig = AppConfig.getInstance();
|
||||
const appConfig = AppConfig.getInstance();
|
||||
const logger = Logger.getInstance();
|
||||
|
||||
export const updatePersonAttribute = async (
|
||||
key: string,
|
||||
value: string
|
||||
): Promise<Result<void, NetworkError | MissingPersonError>> => {
|
||||
const { apiHost, environmentId, userId } = inAppConfig.get();
|
||||
|
||||
const input: TPersonUpdateInput = {
|
||||
attributes: {
|
||||
[key]: value,
|
||||
},
|
||||
};
|
||||
|
||||
const api = new FormbricksAPI({
|
||||
apiHost,
|
||||
environmentId,
|
||||
});
|
||||
|
||||
const res = await api.client.people.update(userId, input);
|
||||
|
||||
if (!res.ok) {
|
||||
return err({
|
||||
code: "network_error",
|
||||
status: 500,
|
||||
message: `Error updating person with userId ${userId}`,
|
||||
url: `${inAppConfig.get().apiHost}/api/v1/client/${environmentId}/people/${userId}`,
|
||||
responseMessage: res.error.message,
|
||||
});
|
||||
}
|
||||
|
||||
if (res.data.changed) {
|
||||
logger.debug("Attribute updated in Formbricks");
|
||||
}
|
||||
|
||||
return okVoid();
|
||||
};
|
||||
|
||||
export const updatePersonAttributes = async (
|
||||
apiHost: string,
|
||||
environmentId: string,
|
||||
userId: string,
|
||||
attributes: TPersonAttributes
|
||||
): Promise<Result<TPersonAttributes, NetworkError | MissingPersonError>> => {
|
||||
// clean attributes and remove existing attributes if config already exists
|
||||
const updatedAttributes = { ...attributes };
|
||||
|
||||
try {
|
||||
const existingAttributes = inAppConfig.get()?.state?.attributes;
|
||||
if (existingAttributes) {
|
||||
for (const [key, value] of Object.entries(existingAttributes)) {
|
||||
if (updatedAttributes[key] === value) {
|
||||
delete updatedAttributes[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logger.debug("config not set; sending all attributes to backend");
|
||||
}
|
||||
|
||||
// send to backend if updatedAttributes is not empty
|
||||
if (Object.keys(updatedAttributes).length === 0) {
|
||||
logger.debug("No attributes to update. Skipping update.");
|
||||
return ok(updatedAttributes);
|
||||
}
|
||||
|
||||
logger.debug("Updating attributes: " + JSON.stringify(updatedAttributes));
|
||||
|
||||
const input: TPersonUpdateInput = {
|
||||
attributes: updatedAttributes,
|
||||
};
|
||||
|
||||
const api = new FormbricksAPI({
|
||||
apiHost,
|
||||
environmentId,
|
||||
});
|
||||
|
||||
const res = await api.client.people.update(userId, input);
|
||||
|
||||
if (res.ok) {
|
||||
return ok(updatedAttributes);
|
||||
}
|
||||
|
||||
return err({
|
||||
code: "network_error",
|
||||
status: 500,
|
||||
message: `Error updating person with userId ${userId}`,
|
||||
url: `${apiHost}/api/v1/client/${environmentId}/people/${userId}`,
|
||||
responseMessage: res.error.message,
|
||||
});
|
||||
};
|
||||
|
||||
export const isExistingAttribute = (key: string, value: string): boolean => {
|
||||
if (inAppConfig.get().state.attributes[key] === value) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const setPersonAttribute = async (
|
||||
key: string,
|
||||
value: any
|
||||
): Promise<Result<void, NetworkError | MissingPersonError>> => {
|
||||
if (key === "userId") {
|
||||
logger.error("Setting userId is no longer supported. Please set the userId in the init call instead.");
|
||||
return okVoid();
|
||||
}
|
||||
|
||||
logger.debug("Setting attribute: " + key + " to value: " + value);
|
||||
// check if attribute already exists with this value
|
||||
if (isExistingAttribute(key, value.toString())) {
|
||||
logger.debug("Attribute already set to this value. Skipping update.");
|
||||
return okVoid();
|
||||
}
|
||||
|
||||
const result = await updatePersonAttribute(key, value.toString());
|
||||
|
||||
if (result.ok) {
|
||||
// udpdate attribute in config
|
||||
inAppConfig.update({
|
||||
environmentId: inAppConfig.get().environmentId,
|
||||
apiHost: inAppConfig.get().apiHost,
|
||||
userId: inAppConfig.get().userId,
|
||||
state: {
|
||||
...inAppConfig.get().state,
|
||||
attributes: {
|
||||
...inAppConfig.get().state.attributes,
|
||||
[key]: value.toString(),
|
||||
},
|
||||
},
|
||||
expiresAt: inAppConfig.get().expiresAt,
|
||||
});
|
||||
return okVoid();
|
||||
}
|
||||
|
||||
return err(result.error);
|
||||
};
|
||||
|
||||
export const logoutPerson = async (): Promise<void> => {
|
||||
deinitalize();
|
||||
inAppConfig.resetConfig();
|
||||
appConfig.resetConfig();
|
||||
};
|
||||
|
||||
export const resetPerson = async (): Promise<Result<void, NetworkError>> => {
|
||||
logger.debug("Resetting state & getting new state from backend");
|
||||
closeSurvey();
|
||||
const syncParams = {
|
||||
environmentId: inAppConfig.get().environmentId,
|
||||
apiHost: inAppConfig.get().apiHost,
|
||||
userId: inAppConfig.get().userId,
|
||||
attributes: inAppConfig.get().state.attributes,
|
||||
environmentId: appConfig.get().environmentId,
|
||||
apiHost: appConfig.get().apiHost,
|
||||
userId: appConfig.get().userId,
|
||||
attributes: appConfig.get().state.attributes,
|
||||
};
|
||||
await logoutPerson();
|
||||
try {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { TAttributes } from "@formbricks/types/attributes";
|
||||
import { TJsAppState, TJsAppStateSync, TJsAppSyncParams } from "@formbricks/types/js";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
|
||||
@@ -6,7 +7,7 @@ import { Logger } from "../../shared/logger";
|
||||
import { getIsDebug } from "../../shared/utils";
|
||||
import { AppConfig } from "./config";
|
||||
|
||||
const config = AppConfig.getInstance();
|
||||
const appConfig = AppConfig.getInstance();
|
||||
const logger = Logger.getInstance();
|
||||
|
||||
let syncIntervalId: number | null = null;
|
||||
@@ -56,17 +57,23 @@ export const sync = async (params: TJsAppSyncParams, noCache = false): Promise<v
|
||||
throw syncResult.error;
|
||||
}
|
||||
|
||||
let attributes: TAttributes = params.attributes || {};
|
||||
|
||||
if (syncResult.value.language) {
|
||||
attributes.language = syncResult.value.language;
|
||||
}
|
||||
|
||||
let state: TJsAppState = {
|
||||
surveys: syncResult.value.surveys as TSurvey[],
|
||||
noCodeActionClasses: syncResult.value.noCodeActionClasses,
|
||||
product: syncResult.value.product,
|
||||
attributes: syncResult.value.person?.attributes || {},
|
||||
attributes,
|
||||
};
|
||||
|
||||
const surveyNames = state.surveys.map((s) => s.name);
|
||||
logger.debug("Fetched " + surveyNames.length + " surveys during sync: " + surveyNames.join(", "));
|
||||
|
||||
config.update({
|
||||
appConfig.update({
|
||||
apiHost: params.apiHost,
|
||||
environmentId: params.environmentId,
|
||||
userId: params.userId,
|
||||
@@ -86,20 +93,21 @@ export const addExpiryCheckListener = (): void => {
|
||||
syncIntervalId = window.setInterval(async () => {
|
||||
try {
|
||||
// check if the config has not expired yet
|
||||
if (config.get().expiresAt && new Date(config.get().expiresAt) >= new Date()) {
|
||||
if (appConfig.get().expiresAt && new Date(appConfig.get().expiresAt) >= new Date()) {
|
||||
return;
|
||||
}
|
||||
logger.debug("Config has expired. Starting sync.");
|
||||
await sync({
|
||||
apiHost: config.get().apiHost,
|
||||
environmentId: config.get().environmentId,
|
||||
userId: config.get().userId,
|
||||
apiHost: appConfig.get().apiHost,
|
||||
environmentId: appConfig.get().environmentId,
|
||||
userId: appConfig.get().userId,
|
||||
attributes: appConfig.get().state.attributes,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(`Error during expiry check: ${e}`);
|
||||
logger.debug("Extending config and try again later.");
|
||||
const existingConfig = config.get();
|
||||
config.update(existingConfig);
|
||||
const existingConfig = appConfig.get();
|
||||
appConfig.update(existingConfig);
|
||||
}
|
||||
}, updateInterval);
|
||||
}
|
||||
|
||||
@@ -194,6 +194,7 @@ export const closeSurvey = async (): Promise<void> => {
|
||||
apiHost: inAppConfig.get().apiHost,
|
||||
environmentId: inAppConfig.get().environmentId,
|
||||
userId: inAppConfig.get().userId,
|
||||
attributes: inAppConfig.get().state.attributes,
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { TPersonAttributes } from "@formbricks/types/people";
|
||||
import { TAttributes } from "@formbricks/types/attributes";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
|
||||
export const getIsDebug = () => window.location.search.includes("formbricksDebug=true");
|
||||
|
||||
export const getLanguageCode = (survey: TSurvey, attributes: TPersonAttributes): string | undefined => {
|
||||
export const getLanguageCode = (survey: TSurvey, attributes: TAttributes): string | undefined => {
|
||||
const language = attributes.language;
|
||||
const availableLanguageCodes = survey.languages.map((surveyLanguage) => surveyLanguage.language.code);
|
||||
if (!language) return "default";
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { revalidateTag } from "next/cache";
|
||||
|
||||
interface RevalidateProps {
|
||||
id?: string;
|
||||
environmentId?: string;
|
||||
userId?: string;
|
||||
personId?: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const attributeCache = {
|
||||
tag: {
|
||||
byEnvironmentIdAndUserId(environmentId: string, userId: string): string {
|
||||
return `environments-${environmentId}-personByUserId-${userId}-attributes`;
|
||||
},
|
||||
byPersonId(personId: string): string {
|
||||
return `person-${personId}-attributes`;
|
||||
},
|
||||
byNameAndPersonId(name: string, personId: string): string {
|
||||
return `person-${personId}-attribute-${name}`;
|
||||
},
|
||||
},
|
||||
revalidate({ environmentId, userId, personId, name }: RevalidateProps): void {
|
||||
if (environmentId && userId) {
|
||||
revalidateTag(this.tag.byEnvironmentIdAndUserId(environmentId, userId));
|
||||
}
|
||||
if (personId) {
|
||||
revalidateTag(this.tag.byPersonId(personId));
|
||||
}
|
||||
if (personId && name) {
|
||||
revalidateTag(this.tag.byNameAndPersonId(name, personId));
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,235 @@
|
||||
import "server-only";
|
||||
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { unstable_cache } from "next/cache";
|
||||
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { TAttributes, ZAttributes } from "@formbricks/types/attributes";
|
||||
import { ZString } from "@formbricks/types/common";
|
||||
import { ZId } from "@formbricks/types/environment";
|
||||
import { DatabaseError } from "@formbricks/types/errors";
|
||||
|
||||
import { attributeCache } from "../attribute/cache";
|
||||
import { attributeClassCache } from "../attributeClass/cache";
|
||||
import { getAttributeClassByName } from "../attributeClass/service";
|
||||
import { SERVICES_REVALIDATION_INTERVAL } from "../constants";
|
||||
import { getPerson, getPersonByUserId } from "../person/service";
|
||||
import { validateInputs } from "../utils/validate";
|
||||
|
||||
export const selectAttribute: Prisma.AttributeSelect = {
|
||||
value: true,
|
||||
attributeClass: {
|
||||
select: {
|
||||
name: true,
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// convert prisma attributes to a key-value object
|
||||
const convertPrismaAttributes = (prismaAttributes: any): TAttributes => {
|
||||
return prismaAttributes.reduce(
|
||||
(acc, attr) => {
|
||||
acc[attr.attributeClass.name] = attr.value;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string | number>
|
||||
);
|
||||
};
|
||||
|
||||
export const getAttributes = async (personId: string): Promise<TAttributes> => {
|
||||
return await unstable_cache(
|
||||
async () => {
|
||||
validateInputs([personId, ZId]);
|
||||
|
||||
try {
|
||||
const prismaAttributes = await prisma.attribute.findMany({
|
||||
where: {
|
||||
personId,
|
||||
},
|
||||
select: selectAttribute,
|
||||
});
|
||||
|
||||
return convertPrismaAttributes(prismaAttributes);
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`getAttributes-${personId}`],
|
||||
{
|
||||
tags: [attributeCache.tag.byPersonId(personId)],
|
||||
revalidate: SERVICES_REVALIDATION_INTERVAL,
|
||||
}
|
||||
)();
|
||||
};
|
||||
|
||||
export const getAttributesByUserId = async (environmentId: string, userId: string): Promise<TAttributes> => {
|
||||
return await unstable_cache(
|
||||
async () => {
|
||||
validateInputs([environmentId, ZId], [userId, ZString]);
|
||||
|
||||
const person = await getPersonByUserId(environmentId, userId);
|
||||
|
||||
if (!person) {
|
||||
throw new Error("Person not found");
|
||||
}
|
||||
|
||||
try {
|
||||
const prismaAttributes = await prisma.attribute.findMany({
|
||||
where: {
|
||||
personId: person.id,
|
||||
},
|
||||
select: selectAttribute,
|
||||
});
|
||||
|
||||
return convertPrismaAttributes(prismaAttributes);
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`getAttributesByUserId-${environmentId}-${userId}`],
|
||||
{
|
||||
tags: [attributeCache.tag.byEnvironmentIdAndUserId(environmentId, userId)],
|
||||
revalidate: SERVICES_REVALIDATION_INTERVAL,
|
||||
}
|
||||
)();
|
||||
};
|
||||
|
||||
export const getAttribute = async (name: string, personId: string): Promise<string | undefined> => {
|
||||
return await unstable_cache(
|
||||
async () => {
|
||||
validateInputs([name, ZString], [personId, ZId]);
|
||||
|
||||
const person = await getPerson(personId);
|
||||
|
||||
if (!person) {
|
||||
throw new Error("Person not found");
|
||||
}
|
||||
|
||||
const attributeClass = await getAttributeClassByName(person?.environmentId, name);
|
||||
|
||||
if (!attributeClass) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
const prismaAttributes = await prisma.attribute.findFirst({
|
||||
where: {
|
||||
attributeClassId: attributeClass.id,
|
||||
personId,
|
||||
},
|
||||
select: { value: true },
|
||||
});
|
||||
|
||||
return prismaAttributes?.value;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`getAttribute-${name}-${personId}`],
|
||||
{
|
||||
tags: [attributeCache.tag.byNameAndPersonId(name, personId)],
|
||||
revalidate: SERVICES_REVALIDATION_INTERVAL,
|
||||
}
|
||||
)();
|
||||
};
|
||||
|
||||
export const updateAttributes = async (personId: string, attributes: TAttributes): Promise<boolean> => {
|
||||
validateInputs([personId, ZId], [attributes, ZAttributes]);
|
||||
|
||||
const person = await getPerson(personId);
|
||||
|
||||
if (!person) {
|
||||
throw new Error("Person not found");
|
||||
}
|
||||
|
||||
const environmentId = person.environmentId;
|
||||
const userId = person.userId;
|
||||
|
||||
const attributeClassNames = Object.keys(attributes);
|
||||
const existingClasses = await prisma.attributeClass.findMany({
|
||||
where: {
|
||||
name: { in: attributeClassNames },
|
||||
},
|
||||
});
|
||||
|
||||
const attributeClassMap = new Map(existingClasses.map((ac) => [ac.name, ac.id]));
|
||||
const upsertOperations: Promise<any>[] = [];
|
||||
|
||||
for (const [name, value] of Object.entries(attributes)) {
|
||||
const attributeClassId = attributeClassMap.get(name);
|
||||
|
||||
if (attributeClassId) {
|
||||
// Class exists, perform an upsert operation
|
||||
upsertOperations.push(
|
||||
prisma.attribute
|
||||
.upsert({
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
where: {
|
||||
personId_attributeClassId: {
|
||||
personId,
|
||||
attributeClassId,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
value,
|
||||
},
|
||||
create: {
|
||||
personId,
|
||||
attributeClassId,
|
||||
value,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
attributeCache.revalidate({ environmentId, personId, userId, name });
|
||||
})
|
||||
);
|
||||
} else {
|
||||
// Class does not exist, create new class and attribute
|
||||
upsertOperations.push(
|
||||
prisma.attributeClass
|
||||
.create({
|
||||
select: { id: true },
|
||||
data: {
|
||||
name,
|
||||
type: "code",
|
||||
environment: {
|
||||
connect: {
|
||||
id: environmentId,
|
||||
},
|
||||
},
|
||||
attributes: {
|
||||
create: {
|
||||
personId,
|
||||
value,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(({ id }) => {
|
||||
attributeClassCache.revalidate({ environmentId, name });
|
||||
attributeCache.revalidate({ id, environmentId, personId, userId, name });
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Execute all upsert operations concurrently
|
||||
await Promise.all(upsertOperations);
|
||||
|
||||
return true;
|
||||
};
|
||||
+20
-168
@@ -7,9 +7,8 @@ import { prisma } from "@formbricks/database";
|
||||
import { ZOptionalNumber, ZString } from "@formbricks/types/common";
|
||||
import { ZId } from "@formbricks/types/environment";
|
||||
import { DatabaseError } from "@formbricks/types/errors";
|
||||
import { TPerson, TPersonUpdateInput, ZPerson, ZPersonUpdateInput } from "@formbricks/types/people";
|
||||
import { TPerson, ZPerson } from "@formbricks/types/people";
|
||||
|
||||
import { createAttributeClass, getAttributeClassByName } from "../attributeClass/service";
|
||||
import { ITEMS_PER_PAGE, SERVICES_REVALIDATION_INTERVAL } from "../constants";
|
||||
import { formatDateFields } from "../utils/datetime";
|
||||
import { validateInputs } from "../utils/validate";
|
||||
@@ -21,22 +20,6 @@ export const selectPerson = {
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
environmentId: true,
|
||||
attributes: {
|
||||
where: {
|
||||
attributeClass: {
|
||||
archived: false,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
value: true,
|
||||
attributeClass: {
|
||||
select: {
|
||||
name: true,
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
type TransformPersonInput = {
|
||||
@@ -96,7 +79,7 @@ export const getPerson = async (personId: string): Promise<TPerson | null> => {
|
||||
{ tags: [personCache.tag.byId(personId)], revalidate: SERVICES_REVALIDATION_INTERVAL }
|
||||
)();
|
||||
|
||||
return prismaPerson ? formatDateFields(transformPrismaPerson(prismaPerson), ZPerson) : null;
|
||||
return prismaPerson ? formatDateFields(prismaPerson, ZPerson) : null;
|
||||
};
|
||||
|
||||
export const getPeople = async (environmentId: string, page?: number): Promise<TPerson[]> => {
|
||||
@@ -129,7 +112,7 @@ export const getPeople = async (environmentId: string, page?: number): Promise<T
|
||||
)();
|
||||
|
||||
return peoplePrisma
|
||||
.map((prismaPerson) => formatDateFields(transformPrismaPerson(prismaPerson), ZPerson))
|
||||
.map((prismaPerson) => formatDateFields(prismaPerson, ZPerson))
|
||||
.filter((person: TPerson | null): person is TPerson => person !== null);
|
||||
};
|
||||
|
||||
@@ -175,15 +158,13 @@ export const createPerson = async (environmentId: string, userId: string): Promi
|
||||
select: selectPerson,
|
||||
});
|
||||
|
||||
const transformedPerson = transformPrismaPerson(person);
|
||||
|
||||
personCache.revalidate({
|
||||
id: transformedPerson.id,
|
||||
id: person.id,
|
||||
environmentId,
|
||||
userId,
|
||||
});
|
||||
|
||||
return transformedPerson;
|
||||
return person;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
// If the person already exists, return it
|
||||
@@ -199,7 +180,7 @@ export const createPerson = async (environmentId: string, userId: string): Promi
|
||||
});
|
||||
|
||||
if (existingPerson) {
|
||||
return transformPrismaPerson(existingPerson);
|
||||
return existingPerson;
|
||||
}
|
||||
}
|
||||
throw new DatabaseError(error.message);
|
||||
@@ -219,85 +200,14 @@ export const deletePerson = async (personId: string): Promise<TPerson | null> =>
|
||||
},
|
||||
select: selectPerson,
|
||||
});
|
||||
const transformedPerson = transformPrismaPerson(person);
|
||||
|
||||
personCache.revalidate({
|
||||
id: transformedPerson.id,
|
||||
userId: transformedPerson.userId,
|
||||
environmentId: transformedPerson.environmentId,
|
||||
});
|
||||
|
||||
return transformedPerson;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const updatePerson = async (personId: string, personInput: TPersonUpdateInput): Promise<TPerson> => {
|
||||
validateInputs([personId, ZId], [personInput, ZPersonUpdateInput]);
|
||||
|
||||
try {
|
||||
const person = await getPerson(personId);
|
||||
if (!person) {
|
||||
throw new Error(`Person ${personId} not found`);
|
||||
}
|
||||
|
||||
// Process each attribute
|
||||
const attributeUpdates = Object.entries(personInput.attributes).map(async ([attributeName, value]) => {
|
||||
let attributeClass = await getAttributeClassByName(person.environmentId, attributeName);
|
||||
|
||||
// Create new attribute class if not found
|
||||
if (attributeClass === null) {
|
||||
attributeClass = await createAttributeClass(person.environmentId, attributeName, "code");
|
||||
}
|
||||
|
||||
// Now perform the upsert for the attribute with the found or created attributeClassId
|
||||
await prisma.attribute.upsert({
|
||||
where: {
|
||||
personId_attributeClassId: {
|
||||
attributeClassId: attributeClass!.id,
|
||||
personId,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
value: value.toString(),
|
||||
},
|
||||
create: {
|
||||
attributeClass: {
|
||||
connect: {
|
||||
id: attributeClass!.id,
|
||||
},
|
||||
},
|
||||
person: {
|
||||
connect: {
|
||||
id: personId,
|
||||
},
|
||||
},
|
||||
value: value.toString(),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// Execute all attribute updates
|
||||
await Promise.all(attributeUpdates);
|
||||
|
||||
personCache.revalidate({
|
||||
id: personId,
|
||||
id: person.id,
|
||||
userId: person.userId,
|
||||
environmentId: person.environmentId,
|
||||
});
|
||||
|
||||
const updatedPerson = await getPerson(personId);
|
||||
|
||||
if (!updatedPerson) {
|
||||
throw new Error(`Person ${personId} not found`);
|
||||
}
|
||||
|
||||
return updatedPerson;
|
||||
return person;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
@@ -312,28 +222,20 @@ export const getPersonByUserId = async (environmentId: string, userId: string):
|
||||
async () => {
|
||||
validateInputs([environmentId, ZId], [userId, ZString]);
|
||||
|
||||
try {
|
||||
// check if userId exists as a column
|
||||
const personWithUserId = await prisma.person.findFirst({
|
||||
where: {
|
||||
environmentId,
|
||||
userId,
|
||||
},
|
||||
select: selectPerson,
|
||||
});
|
||||
// check if userId exists as a column
|
||||
const personWithUserId = await prisma.person.findFirst({
|
||||
where: {
|
||||
environmentId,
|
||||
userId,
|
||||
},
|
||||
select: selectPerson,
|
||||
});
|
||||
|
||||
if (personWithUserId) {
|
||||
return transformPrismaPerson(personWithUserId);
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
if (personWithUserId) {
|
||||
return personWithUserId;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
[`getPersonByUserId-${environmentId}-${userId}`],
|
||||
{
|
||||
@@ -344,56 +246,6 @@ export const getPersonByUserId = async (environmentId: string, userId: string):
|
||||
return person ? formatDateFields(person, ZPerson) : null;
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated This function is deprecated and only used in legacy endpoints. Use `updatePerson` instead.
|
||||
*/
|
||||
export const updatePersonAttribute = async (
|
||||
personId: string,
|
||||
attributeClassId: string,
|
||||
value: string
|
||||
): Promise<Partial<TPerson>> => {
|
||||
validateInputs([personId, ZId], [attributeClassId, ZId], [value, ZString]);
|
||||
|
||||
try {
|
||||
const attributes = await prisma.attribute.upsert({
|
||||
where: {
|
||||
personId_attributeClassId: {
|
||||
attributeClassId,
|
||||
personId,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
value,
|
||||
},
|
||||
create: {
|
||||
attributeClass: {
|
||||
connect: {
|
||||
id: attributeClassId,
|
||||
},
|
||||
},
|
||||
person: {
|
||||
connect: {
|
||||
id: personId,
|
||||
},
|
||||
},
|
||||
value,
|
||||
},
|
||||
});
|
||||
|
||||
personCache.revalidate({
|
||||
id: personId,
|
||||
});
|
||||
|
||||
return attributes;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getIsPersonMonthlyActive = async (personId: string): Promise<boolean> =>
|
||||
unstable_cache(
|
||||
async () => {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { TPerson } from "@formbricks/types/people";
|
||||
import { TAttributes } from "@formbricks/types/attributes";
|
||||
import { TResponsePerson } from "@formbricks/types/responses";
|
||||
|
||||
export const getPersonIdentifier = (person: TPerson): string | number | null => {
|
||||
return person.attributes.email || person.userId;
|
||||
export const getPersonIdentifier = (
|
||||
person: TResponsePerson | null,
|
||||
personAttributes: TAttributes | null
|
||||
): string => {
|
||||
return personAttributes?.email || person?.userId || "";
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Prisma } from "@prisma/client";
|
||||
import { unstable_cache } from "next/cache";
|
||||
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { TAttributes } from "@formbricks/types/attributes";
|
||||
import { ZOptionalNumber, ZString } from "@formbricks/types/common";
|
||||
import { ZId } from "@formbricks/types/environment";
|
||||
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
@@ -26,10 +27,11 @@ import {
|
||||
import { TSurveySummary } from "@formbricks/types/surveys";
|
||||
import { TTag } from "@formbricks/types/tags";
|
||||
|
||||
import { getAttributes } from "../attribute/service";
|
||||
import { ITEMS_PER_PAGE, SERVICES_REVALIDATION_INTERVAL, WEBAPP_URL } from "../constants";
|
||||
import { displayCache } from "../display/cache";
|
||||
import { deleteDisplayByResponseId, getDisplayCountBySurveyId } from "../display/service";
|
||||
import { createPerson, getPerson, getPersonByUserId, transformPrismaPerson } from "../person/service";
|
||||
import { createPerson, getPerson, getPersonByUserId } from "../person/service";
|
||||
import {
|
||||
buildWhereClause,
|
||||
calculateTtcTotal,
|
||||
@@ -69,19 +71,6 @@ export const responseSelection = {
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
environmentId: true,
|
||||
attributes: {
|
||||
select: {
|
||||
value: true,
|
||||
attributeClass: {
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: {
|
||||
@@ -148,7 +137,6 @@ export const getResponsesByPersonId = async (
|
||||
responses.push({
|
||||
...response,
|
||||
notes: responseNotes,
|
||||
person: response.person ? transformPrismaPerson(response.person) : null,
|
||||
tags: response.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
|
||||
});
|
||||
})
|
||||
@@ -198,7 +186,6 @@ export const getResponseBySingleUseId = async (
|
||||
|
||||
const response: TResponse = {
|
||||
...responsePrisma,
|
||||
person: responsePrisma.person ? transformPrismaPerson(responsePrisma.person) : null,
|
||||
tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
|
||||
};
|
||||
|
||||
@@ -243,6 +230,7 @@ export const createResponse = async (responseInput: TResponseInput): Promise<TRe
|
||||
} = responseInput;
|
||||
try {
|
||||
let person: TPerson | null = null;
|
||||
let attributes: TAttributes | null = null;
|
||||
|
||||
if (userId) {
|
||||
person = await getPersonByUserId(environmentId, userId);
|
||||
@@ -252,6 +240,10 @@ export const createResponse = async (responseInput: TResponseInput): Promise<TRe
|
||||
}
|
||||
}
|
||||
|
||||
if (person?.id) {
|
||||
attributes = await getAttributes(person?.id as string);
|
||||
}
|
||||
|
||||
const ttc = initialTtc ? (finished ? calculateTtcTotal(initialTtc) : initialTtc) : {};
|
||||
|
||||
const responsePrisma = await prisma.response.create({
|
||||
@@ -270,7 +262,7 @@ export const createResponse = async (responseInput: TResponseInput): Promise<TRe
|
||||
id: person.id,
|
||||
},
|
||||
},
|
||||
personAttributes: person?.attributes,
|
||||
personAttributes: attributes,
|
||||
}),
|
||||
...(meta && ({ meta } as Prisma.JsonObject)),
|
||||
singleUseId,
|
||||
@@ -281,7 +273,6 @@ export const createResponse = async (responseInput: TResponseInput): Promise<TRe
|
||||
|
||||
const response: TResponse = {
|
||||
...responsePrisma,
|
||||
person: responsePrisma.person ? transformPrismaPerson(responsePrisma.person) : null,
|
||||
tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
|
||||
};
|
||||
|
||||
@@ -313,6 +304,7 @@ export const createResponseLegacy = async (responseInput: TResponseLegacyInput):
|
||||
|
||||
try {
|
||||
let person: TPerson | null = null;
|
||||
let attributes: TAttributes | null = null;
|
||||
|
||||
if (responseInput.personId) {
|
||||
person = await getPerson(responseInput.personId);
|
||||
@@ -326,6 +318,11 @@ export const createResponseLegacy = async (responseInput: TResponseLegacyInput):
|
||||
_total: ttcTemp[questionId], // Add _total property with the same value
|
||||
}
|
||||
: ttcTemp;
|
||||
|
||||
if (person?.id) {
|
||||
attributes = await getAttributes(person?.id as string);
|
||||
}
|
||||
|
||||
const responsePrisma = await prisma.response.create({
|
||||
data: {
|
||||
survey: {
|
||||
@@ -342,7 +339,7 @@ export const createResponseLegacy = async (responseInput: TResponseLegacyInput):
|
||||
id: responseInput.personId,
|
||||
},
|
||||
},
|
||||
personAttributes: person?.attributes,
|
||||
personAttributes: attributes,
|
||||
}),
|
||||
|
||||
...(responseInput.meta && ({ meta: responseInput?.meta } as Prisma.JsonObject)),
|
||||
@@ -351,9 +348,9 @@ export const createResponseLegacy = async (responseInput: TResponseLegacyInput):
|
||||
},
|
||||
select: responseSelection,
|
||||
});
|
||||
|
||||
const response: TResponse = {
|
||||
...responsePrisma,
|
||||
person: responsePrisma.person ? transformPrismaPerson(responsePrisma.person) : null,
|
||||
tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
|
||||
};
|
||||
|
||||
@@ -395,7 +392,6 @@ export const getResponse = async (responseId: string): Promise<TResponse | null>
|
||||
|
||||
const response: TResponse = {
|
||||
...responsePrisma,
|
||||
person: responsePrisma.person ? transformPrismaPerson(responsePrisma.person) : null,
|
||||
tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
|
||||
};
|
||||
|
||||
@@ -571,7 +567,6 @@ export const getResponses = async (
|
||||
responses.map((responsePrisma) => {
|
||||
return {
|
||||
...responsePrisma,
|
||||
person: responsePrisma.person ? transformPrismaPerson(responsePrisma.person) : null,
|
||||
tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
|
||||
};
|
||||
})
|
||||
@@ -592,6 +587,7 @@ export const getResponses = async (
|
||||
revalidate: SERVICES_REVALIDATION_INTERVAL,
|
||||
}
|
||||
)();
|
||||
|
||||
return responses.map((response) => ({
|
||||
...formatDateFields(response, ZResponse),
|
||||
notes: response.notes.map((note) => formatDateFields(note, ZResponseNote)),
|
||||
@@ -755,7 +751,6 @@ export const getResponsesByEnvironmentId = async (
|
||||
responses.map(async (responsePrisma) => {
|
||||
return {
|
||||
...responsePrisma,
|
||||
person: responsePrisma.person ? transformPrismaPerson(responsePrisma.person) : null,
|
||||
tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
|
||||
};
|
||||
})
|
||||
@@ -830,7 +825,6 @@ export const updateResponse = async (
|
||||
|
||||
const response: TResponse = {
|
||||
...responsePrisma,
|
||||
person: responsePrisma.person ? transformPrismaPerson(responsePrisma.person) : null,
|
||||
tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
|
||||
};
|
||||
|
||||
@@ -868,7 +862,6 @@ export const deleteResponse = async (responseId: string): Promise<TResponse> =>
|
||||
const response: TResponse = {
|
||||
...responsePrisma,
|
||||
notes: responseNotes,
|
||||
person: responsePrisma.person ? transformPrismaPerson(responsePrisma.person) : null,
|
||||
tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
|
||||
};
|
||||
|
||||
|
||||
@@ -8,10 +8,9 @@ import {
|
||||
TResponseUpdateInput,
|
||||
TSurveyPersonAttributes,
|
||||
} from "@formbricks/types/responses";
|
||||
import { TSurveyQuestionType, TSurveySummary } from "@formbricks/types/surveys";
|
||||
import { TSurveyQuestionType } from "@formbricks/types/surveys";
|
||||
import { TTag } from "@formbricks/types/tags";
|
||||
|
||||
import { transformPrismaPerson } from "../../../person/service";
|
||||
import { responseNoteSelect } from "../../../responseNote/service";
|
||||
import { responseSelection } from "../../service";
|
||||
import { constantsForTests } from "../constants";
|
||||
@@ -22,9 +21,6 @@ type ResponseMock = Prisma.ResponseGetPayload<{
|
||||
type ResponseNoteMock = Prisma.ResponseNoteGetPayload<{
|
||||
include: typeof responseNoteSelect;
|
||||
}>;
|
||||
type ResponsePersonMock = Prisma.PersonGetPayload<{
|
||||
select: typeof responseSelection.person.select;
|
||||
}>;
|
||||
|
||||
export const mockEnvironmentId = "ars2tjk8hsi8oqk1uac00mo7";
|
||||
export const mockPersonId = "lhwy39ga2zy8by1ol1bnaiso";
|
||||
@@ -63,20 +59,9 @@ export const mockResponseNote: ResponseNoteMock = {
|
||||
},
|
||||
};
|
||||
|
||||
export const mockPerson: ResponsePersonMock = {
|
||||
export const mockPerson = {
|
||||
id: mockPersonId,
|
||||
userId: mockUserId,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
environmentId: mockEnvironmentId,
|
||||
attributes: [
|
||||
{
|
||||
value: "attribute1",
|
||||
attributeClass: {
|
||||
name: "attributeClass1",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const mockTags = [
|
||||
@@ -442,7 +427,7 @@ export const getFilteredMockResponses = (
|
||||
if (format) {
|
||||
return result.map((response) => ({
|
||||
...response,
|
||||
person: response.person ? transformPrismaPerson(response.person) : null,
|
||||
person: response.person ? { id: response.person.id, userId: response.person.userId } : null,
|
||||
tags: response.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
|
||||
}));
|
||||
}
|
||||
@@ -471,7 +456,7 @@ export const getMockUpdateResponseInput = (finished: boolean = false): TResponse
|
||||
finished,
|
||||
});
|
||||
|
||||
export const mockSurveySummaryOutput: TSurveySummary = {
|
||||
export const mockSurveySummaryOutput = {
|
||||
dropOff: [
|
||||
{
|
||||
dropOffCount: 0,
|
||||
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
} from "@formbricks/types/responses";
|
||||
import { TTag } from "@formbricks/types/tags";
|
||||
|
||||
import { selectPerson, transformPrismaPerson } from "../../person/service";
|
||||
import { selectPerson } from "../../person/service";
|
||||
import { mockSurveyOutput } from "../../survey/tests/__mock__/survey.mock";
|
||||
import {
|
||||
createResponse,
|
||||
@@ -61,7 +61,7 @@ const expectedResponseWithoutPerson: TResponse = {
|
||||
|
||||
const expectedResponseWithPerson: TResponse = {
|
||||
...mockResponse,
|
||||
person: transformPrismaPerson(mockPerson),
|
||||
person: mockPerson,
|
||||
tags: mockTags?.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
|
||||
};
|
||||
|
||||
@@ -209,6 +209,7 @@ describe("Tests for getResponsesBySingleUseId", () => {
|
||||
describe("Tests for createResponse service", () => {
|
||||
describe("Happy Path", () => {
|
||||
it("Creates a response linked to an existing user", async () => {
|
||||
prisma.attribute.findMany.mockResolvedValue([]);
|
||||
const response = await createResponse(mockResponseInputWithUserId);
|
||||
expect(response).toEqual(expectedResponseWithPerson);
|
||||
});
|
||||
@@ -221,6 +222,7 @@ describe("Tests for createResponse service", () => {
|
||||
it("Creates a new person and response when the person does not exist", async () => {
|
||||
prisma.person.findFirst.mockResolvedValue(null);
|
||||
prisma.person.create.mockResolvedValue(mockPerson);
|
||||
prisma.attribute.findMany.mockResolvedValue([]);
|
||||
const response = await createResponse(mockResponseInputWithUserId);
|
||||
|
||||
expect(response).toEqual(expectedResponseWithPerson);
|
||||
@@ -249,6 +251,7 @@ describe("Tests for createResponse service", () => {
|
||||
});
|
||||
|
||||
prisma.response.create.mockRejectedValue(errToThrow);
|
||||
prisma.attribute.findMany.mockResolvedValue([]);
|
||||
|
||||
await expect(createResponse(mockResponseInputWithUserId)).rejects.toThrow(DatabaseError);
|
||||
});
|
||||
|
||||
@@ -2,7 +2,6 @@ import "server-only";
|
||||
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
import { TPerson } from "@formbricks/types/people";
|
||||
import { TResponse, TResponseFilterCriteria, TResponseTtc } from "@formbricks/types/responses";
|
||||
import {
|
||||
TSurvey,
|
||||
@@ -703,6 +702,7 @@ export const getQuestionWiseSummary = (
|
||||
updatedAt: response.updatedAt,
|
||||
value: answer,
|
||||
person: response.person,
|
||||
personAttributes: response.personAttributes,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -734,7 +734,8 @@ export const getQuestionWiseSummary = (
|
||||
acc[choice] = 0;
|
||||
return acc;
|
||||
}, {});
|
||||
const otherValues: { value: string; person: TPerson | null }[] = [];
|
||||
|
||||
const otherValues: TSurveyQuestionSummaryMultipleChoice["choices"][number]["others"] = [];
|
||||
responses.forEach((response) => {
|
||||
const responseLanguageCode = getLanguageCode(survey.languages, response.language);
|
||||
|
||||
@@ -752,6 +753,7 @@ export const getQuestionWiseSummary = (
|
||||
otherValues.push({
|
||||
value,
|
||||
person: response.person,
|
||||
personAttributes: response.personAttributes,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -763,6 +765,7 @@ export const getQuestionWiseSummary = (
|
||||
otherValues.push({
|
||||
value: answer,
|
||||
person: response.person,
|
||||
personAttributes: response.personAttributes,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1021,6 +1024,7 @@ export const getQuestionWiseSummary = (
|
||||
updatedAt: response.updatedAt,
|
||||
value: answer,
|
||||
person: response.person,
|
||||
personAttributes: response.personAttributes,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1045,6 +1049,7 @@ export const getQuestionWiseSummary = (
|
||||
updatedAt: response.updatedAt,
|
||||
value: answer,
|
||||
person: response.person,
|
||||
personAttributes: response.personAttributes,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1159,6 +1164,7 @@ export const getQuestionWiseSummary = (
|
||||
updatedAt: response.updatedAt,
|
||||
value: answer,
|
||||
person: response.person,
|
||||
personAttributes: response.personAttributes,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1185,6 +1191,7 @@ export const getQuestionWiseSummary = (
|
||||
updatedAt: response.updatedAt,
|
||||
value: answer,
|
||||
person: response.person,
|
||||
personAttributes: response.personAttributes,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -20,6 +20,8 @@ import {
|
||||
|
||||
import { getActionsByPersonId } from "../action/service";
|
||||
import { getActionClasses } from "../actionClass/service";
|
||||
import { attributeCache } from "../attribute/cache";
|
||||
import { getAttributes } from "../attribute/service";
|
||||
import { ITEMS_PER_PAGE, SERVICES_REVALIDATION_INTERVAL } from "../constants";
|
||||
import { displayCache } from "../display/cache";
|
||||
import { getDisplaysByPersonId } from "../display/service";
|
||||
@@ -965,7 +967,9 @@ export const getSyncSurveys = async (
|
||||
const personActionClassIds = Array.from(
|
||||
new Set(personActions?.map((action) => action.actionClass?.id ?? ""))
|
||||
);
|
||||
const personUserId = person.userId ?? person.attributes?.userId ?? "";
|
||||
|
||||
const attributes = await getAttributes(person.id);
|
||||
const personUserId = person.userId;
|
||||
|
||||
// the surveys now have segment filters, so we need to evaluate them
|
||||
const surveyPromises = surveys.map(async (survey) => {
|
||||
@@ -992,7 +996,7 @@ export const getSyncSurveys = async (
|
||||
|
||||
// we check if the person meets the attribute filters for all the attribute filters
|
||||
const isEligible = attributeFilters.every((attributeFilter) => {
|
||||
const personAttributeValue = person?.attributes?.[attributeFilter.attributeClassName];
|
||||
const personAttributeValue = attributes[attributeFilter.attributeClassName];
|
||||
if (!personAttributeValue) {
|
||||
return false;
|
||||
}
|
||||
@@ -1013,7 +1017,7 @@ export const getSyncSurveys = async (
|
||||
// Evaluate the segment filters
|
||||
const result = await evaluateSegment(
|
||||
{
|
||||
attributes: person.attributes ?? {},
|
||||
attributes: attributes ?? {},
|
||||
actionIds: personActionClassIds,
|
||||
deviceType,
|
||||
environmentId,
|
||||
@@ -1050,6 +1054,8 @@ export const getSyncSurveys = async (
|
||||
displayCache.tag.byPersonId(personId),
|
||||
surveyCache.tag.byEnvironmentId(environmentId),
|
||||
productCache.tag.byEnvironmentId(environmentId),
|
||||
// ? Should this be included?
|
||||
attributeCache.tag.byPersonId(personId),
|
||||
],
|
||||
revalidate: SERVICES_REVALIDATION_INTERVAL,
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import "server-only";
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
import { TLegacySurvey } from "@formbricks/types/LegacySurvey";
|
||||
import { TPerson } from "@formbricks/types/people";
|
||||
import { TSurvey, TSurveyFilterCriteria } from "@formbricks/types/surveys";
|
||||
|
||||
export const formatSurveyDateFields = (survey: TSurvey): TSurvey => {
|
||||
@@ -99,21 +98,3 @@ export const anySurveyHasFilters = (surveys: TSurvey[] | TLegacySurvey[]): boole
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
export const determineLanguageCode = (person: TPerson, survey: TSurvey) => {
|
||||
// Default to 'default' if person.attributes.language is not set or not a string
|
||||
if (!person.attributes?.language) return "default";
|
||||
const languageCodeOrAlias =
|
||||
typeof person.attributes?.language === "string" ? person.attributes.language : "default";
|
||||
|
||||
// Find the matching language in the survey
|
||||
const selectedLanguage = survey.languages.find(
|
||||
(surveyLanguage) =>
|
||||
surveyLanguage.language.code === languageCodeOrAlias ||
|
||||
surveyLanguage.language.alias === languageCodeOrAlias
|
||||
);
|
||||
if (!selectedLanguage) return;
|
||||
|
||||
// Determine and return the language code to use
|
||||
return selectedLanguage.default ? "default" : selectedLanguage.language.code;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { ZId } from "./environment";
|
||||
|
||||
export const ZAttributes = z.record(z.string());
|
||||
|
||||
export type TAttributes = z.infer<typeof ZAttributes>;
|
||||
|
||||
export const ZAttributeUpdateInput = z.object({
|
||||
environmentId: ZId,
|
||||
userId: z.string(),
|
||||
attributes: ZAttributes,
|
||||
});
|
||||
|
||||
export type TAttributeUpdateInput = z.infer<typeof ZAttributeUpdateInput>;
|
||||
+23
-6
@@ -2,10 +2,18 @@ import z from "zod";
|
||||
|
||||
import { ZLegacySurvey } from "./LegacySurvey";
|
||||
import { ZActionClass } from "./actionClasses";
|
||||
import { ZPerson, ZPersonAttributes, ZPersonClient } from "./people";
|
||||
import { ZAttributes } from "./attributes";
|
||||
import { ZPerson } from "./people";
|
||||
import { ZProduct } from "./product";
|
||||
import { ZSurvey } from "./surveys";
|
||||
|
||||
export const ZJsPerson = z.object({
|
||||
id: z.string().cuid2().optional(),
|
||||
userId: z.string().optional(),
|
||||
});
|
||||
|
||||
export type TJsPerson = z.infer<typeof ZJsPerson>;
|
||||
|
||||
const ZSurveyWithTriggers = ZSurvey.extend({
|
||||
triggers: z.array(ZActionClass).or(z.array(z.string())),
|
||||
});
|
||||
@@ -21,10 +29,12 @@ export const ZJSWebsiteStateDisplay = z.object({
|
||||
export type TJSWebsiteStateDisplay = z.infer<typeof ZJSWebsiteStateDisplay>;
|
||||
|
||||
export const ZJsAppStateSync = z.object({
|
||||
person: ZPersonClient.nullish(),
|
||||
person: ZJsPerson.nullish(),
|
||||
userId: z.string().optional(),
|
||||
surveys: z.union([z.array(ZSurvey), z.array(ZLegacySurvey)]),
|
||||
noCodeActionClasses: z.array(ZActionClass),
|
||||
product: ZProduct,
|
||||
language: z.string().optional(),
|
||||
});
|
||||
|
||||
export type TJsAppStateSync = z.infer<typeof ZJsAppStateSync>;
|
||||
@@ -34,7 +44,7 @@ export const ZJsWebsiteStateSync = ZJsAppStateSync.omit({ person: true });
|
||||
export type TJsWebsiteStateSync = z.infer<typeof ZJsWebsiteStateSync>;
|
||||
|
||||
export const ZJsAppState = z.object({
|
||||
attributes: ZPersonAttributes,
|
||||
attributes: ZAttributes,
|
||||
surveys: z.array(ZSurvey),
|
||||
noCodeActionClasses: z.array(ZActionClass),
|
||||
product: ZProduct,
|
||||
@@ -47,7 +57,7 @@ export const ZJsWebsiteState = z.object({
|
||||
noCodeActionClasses: z.array(ZActionClass),
|
||||
product: ZProduct,
|
||||
displays: z.array(ZJSWebsiteStateDisplay),
|
||||
attributes: ZPersonAttributes.optional(),
|
||||
attributes: ZAttributes.optional(),
|
||||
});
|
||||
|
||||
export type TJsWebsiteState = z.infer<typeof ZJsWebsiteState>;
|
||||
@@ -125,7 +135,7 @@ export const ZJsWebsiteConfigInput = z.object({
|
||||
environmentId: z.string().cuid(),
|
||||
apiHost: z.string(),
|
||||
errorHandler: z.function().args(z.any()).returns(z.void()).optional(),
|
||||
attributes: ZPersonAttributes.optional(),
|
||||
attributes: ZAttributes.optional(),
|
||||
});
|
||||
|
||||
export type TJsWebsiteConfigInput = z.infer<typeof ZJsWebsiteConfigInput>;
|
||||
@@ -135,7 +145,7 @@ export const ZJsAppConfigInput = z.object({
|
||||
apiHost: z.string(),
|
||||
errorHandler: z.function().args(z.any()).returns(z.void()).optional(),
|
||||
userId: z.string(),
|
||||
attributes: ZPersonAttributes.optional(),
|
||||
attributes: ZAttributes.optional(),
|
||||
});
|
||||
|
||||
export type TJsAppConfigInput = z.infer<typeof ZJsAppConfigInput>;
|
||||
@@ -146,6 +156,12 @@ export const ZJsPeopleUserIdInput = z.object({
|
||||
version: z.string().optional(),
|
||||
});
|
||||
|
||||
export const ZJsPeopleUpdateAttributeInput = z.object({
|
||||
attributes: ZAttributes,
|
||||
});
|
||||
|
||||
export type TJsPeopleUpdateAttributeInput = z.infer<typeof ZJsPeopleUpdateAttributeInput>;
|
||||
|
||||
export type TJsPeopleUserIdInput = z.infer<typeof ZJsPeopleUserIdInput>;
|
||||
|
||||
export const ZJsPeopleAttributeInput = z.object({
|
||||
@@ -179,6 +195,7 @@ export const ZJsAppSyncParams = z.object({
|
||||
environmentId: z.string().cuid(),
|
||||
apiHost: z.string(),
|
||||
userId: z.string(),
|
||||
attributes: ZAttributes.optional(),
|
||||
});
|
||||
|
||||
export type TJsAppSyncParams = z.infer<typeof ZJsAppSyncParams>;
|
||||
|
||||
@@ -1,29 +1,11 @@
|
||||
import z from "zod";
|
||||
|
||||
export const ZPersonAttributes = z.record(z.union([z.string(), z.number()]));
|
||||
export type TPersonAttributes = z.infer<typeof ZPersonAttributes>;
|
||||
|
||||
export const ZPerson = z.object({
|
||||
id: z.string().cuid2(),
|
||||
userId: z.string(),
|
||||
attributes: ZPersonAttributes,
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
environmentId: z.string().cuid2(),
|
||||
});
|
||||
|
||||
export type TPerson = z.infer<typeof ZPerson>;
|
||||
|
||||
export const ZPersonUpdateInput = z.object({
|
||||
attributes: ZPersonAttributes,
|
||||
});
|
||||
|
||||
export type TPersonUpdateInput = z.infer<typeof ZPersonUpdateInput>;
|
||||
|
||||
export const ZPersonClient = z.object({
|
||||
id: z.string().cuid2().optional(),
|
||||
userId: z.string().optional(),
|
||||
attributes: ZPersonAttributes.optional(),
|
||||
});
|
||||
|
||||
export type TPersonClient = z.infer<typeof ZPersonClient>;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { ZPerson, ZPersonAttributes } from "./people";
|
||||
import { ZAttributes } from "./attributes";
|
||||
import { ZId } from "./environment";
|
||||
import { ZSurvey, ZSurveyLogicCondition } from "./surveys";
|
||||
import { ZTag } from "./tags";
|
||||
|
||||
@@ -14,7 +15,7 @@ export const ZResponseTtc = z.record(z.number());
|
||||
|
||||
export type TResponseTtc = z.infer<typeof ZResponseTtc>;
|
||||
|
||||
export const ZResponsePersonAttributes = ZPersonAttributes.nullable();
|
||||
export const ZResponsePersonAttributes = ZAttributes.nullable();
|
||||
|
||||
export type TResponsePersonAttributes = z.infer<typeof ZResponsePersonAttributes>;
|
||||
|
||||
@@ -166,6 +167,13 @@ export const ZResponseFilterCriteria = z.object({
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const ZResponsePerson = z.object({
|
||||
id: ZId,
|
||||
userId: z.string(),
|
||||
});
|
||||
|
||||
export type TResponsePerson = z.infer<typeof ZResponsePerson>;
|
||||
|
||||
export type TResponseFilterCriteria = z.infer<typeof ZResponseFilterCriteria>;
|
||||
|
||||
export const ZResponseNoteUser = z.object({
|
||||
@@ -208,7 +216,7 @@ export const ZResponse = z.object({
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
surveyId: z.string().cuid2(),
|
||||
person: ZPerson.nullable(),
|
||||
person: ZResponsePerson.nullable(),
|
||||
personAttributes: ZResponsePersonAttributes,
|
||||
finished: z.boolean(),
|
||||
data: ZResponseData,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { ZNoCodeConfig } from "./actionClasses";
|
||||
import { ZAttributes } from "./attributes";
|
||||
import { ZAllowedFileExtension, ZColor, ZPlacement } from "./common";
|
||||
import { ZPerson } from "./people";
|
||||
import { ZId } from "./environment";
|
||||
import { ZLanguage } from "./product";
|
||||
import { ZSegment } from "./segment";
|
||||
import { ZBaseStyling } from "./styling";
|
||||
@@ -584,7 +585,13 @@ export const ZSurveyQuestionSummaryOpenText = z.object({
|
||||
id: z.string(),
|
||||
updatedAt: z.date(),
|
||||
value: z.string(),
|
||||
person: ZPerson.nullable(),
|
||||
person: z
|
||||
.object({
|
||||
id: ZId,
|
||||
userId: z.string(),
|
||||
})
|
||||
.nullable(),
|
||||
personAttributes: ZAttributes.nullable(),
|
||||
})
|
||||
),
|
||||
});
|
||||
@@ -604,7 +611,13 @@ export const ZSurveyQuestionSummaryMultipleChoice = z.object({
|
||||
.array(
|
||||
z.object({
|
||||
value: z.string(),
|
||||
person: ZPerson.nullable(),
|
||||
person: z
|
||||
.object({
|
||||
id: ZId,
|
||||
userId: z.string(),
|
||||
})
|
||||
.nullable(),
|
||||
personAttributes: ZAttributes.nullable(),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
@@ -716,7 +729,13 @@ export const ZSurveyQuestionSummaryDate = z.object({
|
||||
id: z.string(),
|
||||
updatedAt: z.date(),
|
||||
value: z.string(),
|
||||
person: ZPerson.nullable(),
|
||||
person: z
|
||||
.object({
|
||||
id: ZId,
|
||||
userId: z.string(),
|
||||
})
|
||||
.nullable(),
|
||||
personAttributes: ZAttributes.nullable(),
|
||||
})
|
||||
),
|
||||
});
|
||||
@@ -732,7 +751,13 @@ export const ZSurveyQuestionSummaryFileUpload = z.object({
|
||||
id: z.string(),
|
||||
updatedAt: z.date(),
|
||||
value: z.array(z.string()),
|
||||
person: ZPerson.nullable(),
|
||||
person: z
|
||||
.object({
|
||||
id: ZId,
|
||||
userId: z.string(),
|
||||
})
|
||||
.nullable(),
|
||||
personAttributes: ZAttributes.nullable(),
|
||||
})
|
||||
),
|
||||
});
|
||||
@@ -778,7 +803,13 @@ export const ZSurveyQuestionSummaryHiddenFields = z.object({
|
||||
z.object({
|
||||
updatedAt: z.date(),
|
||||
value: z.string(),
|
||||
person: ZPerson.nullable(),
|
||||
person: z
|
||||
.object({
|
||||
id: ZId,
|
||||
userId: z.string(),
|
||||
})
|
||||
.nullable(),
|
||||
personAttributes: ZAttributes.nullable(),
|
||||
})
|
||||
),
|
||||
});
|
||||
@@ -792,10 +823,15 @@ export const ZSurveyQuestionSummaryAddress = z.object({
|
||||
samples: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
|
||||
updatedAt: z.date(),
|
||||
value: z.array(z.string()),
|
||||
person: ZPerson.nullable(),
|
||||
person: z
|
||||
.object({
|
||||
id: ZId,
|
||||
userId: z.string(),
|
||||
})
|
||||
.nullable(),
|
||||
personAttributes: ZAttributes.nullable(),
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
@@ -100,7 +100,9 @@ export default function SingleResponseCard({
|
||||
}: SingleResponseCardProps) {
|
||||
const environmentId = survey.environmentId;
|
||||
const router = useRouter();
|
||||
const displayIdentifier = response.person ? getPersonIdentifier(response.person) : null;
|
||||
const displayIdentifier = response.person
|
||||
? getPersonIdentifier(response.person, response.personAttributes)
|
||||
: null;
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
Reference in New Issue
Block a user