Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: handle people and attributes separately to improve sync performance #2476

Merged
merged 21 commits into from
Apr 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion apps/demo/pages/website/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { MonitorIcon } from "lucide-react";
import Image from "next/image";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
Expand Down
Original file line number Diff line number Diff line change
@@ -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");
}
Expand All @@ -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>
)}
Expand All @@ -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>
)}
Expand All @@ -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>
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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);

Expand All @@ -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">
Expand Down
Original file line number Diff line number Diff line change
@@ -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>
);
};
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
) : (
Expand Down
Original file line number Diff line number Diff line change
@@ -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: {
Expand All @@ -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(
Expand All @@ -42,15 +46,16 @@ 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]) {
isUpToDate = false;
break;
}
}

if (isUpToDate) {
return responses.successResponse(
{
Expand All @@ -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(
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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"),
Expand Down
Loading
Loading