diff --git a/apps/web/app/api/v1/js/people/[personId]/set-attribute/route.ts b/apps/web/app/api/v1/js/people/[personId]/set-attribute/route.ts index 22dace843d..26bbc92325 100644 --- a/apps/web/app/api/v1/js/people/[personId]/set-attribute/route.ts +++ b/apps/web/app/api/v1/js/people/[personId]/set-attribute/route.ts @@ -2,6 +2,7 @@ import { getUpdatedState } from "@/app/api/v1/js/sync/lib/sync"; import { responses } from "@/app/lib/api/response"; import { transformErrorToDetails } from "@/app/lib/api/validator"; import { createAttributeClass, getAttributeClassByNameCached } from "@formbricks/lib/attributeClass/service"; +import { personCache } from "@formbricks/lib/person/cache"; import { getPerson, updatePersonAttribute } from "@formbricks/lib/person/service"; import { ZJsPeopleAttributeInput } from "@formbricks/types/js"; import { NextResponse } from "next/server"; @@ -50,6 +51,11 @@ export async function POST(req: Request, { params }): Promise { const state = await getUpdatedState(environmentId, personId, sessionId); + personCache.revalidate({ + id: state.person.id, + environmentId, + }); + return responses.successResponse({ ...state }, true); } catch (error) { console.error(error); diff --git a/apps/web/app/api/v1/js/people/[personId]/set-user-id/route.ts b/apps/web/app/api/v1/js/people/[personId]/set-user-id/route.ts index b9cae7f7cc..09c901fbb5 100644 --- a/apps/web/app/api/v1/js/people/[personId]/set-user-id/route.ts +++ b/apps/web/app/api/v1/js/people/[personId]/set-user-id/route.ts @@ -2,9 +2,9 @@ import { getUpdatedState } from "@/app/api/v1/js/sync/lib/sync"; import { responses } from "@/app/lib/api/response"; import { transformErrorToDetails } from "@/app/lib/api/validator"; import { prisma } from "@formbricks/database"; +import { personCache } from "@formbricks/lib/person/cache"; import { deletePerson, selectPerson, transformPrismaPerson } from "@formbricks/lib/person/service"; import { ZJsPeopleUserIdInput } from "@formbricks/types/js"; -import { revalidateTag } from "next/cache"; import { NextResponse } from "next/server"; export async function OPTIONS(): Promise { @@ -92,13 +92,13 @@ export async function POST(req: Request, { params }): Promise { const transformedPerson = transformPrismaPerson(returnedPerson); - if (transformedPerson) { - // revalidate person - revalidateTag(transformedPerson.id); - } - const state = await getUpdatedState(environmentId, transformedPerson.id, sessionId); + personCache.revalidate({ + id: transformedPerson.id, + environmentId: environmentId, + }); + return responses.successResponse({ ...state }, true); } catch (error) { console.error(error); diff --git a/apps/web/app/api/v1/js/sync/lib/surveys.ts b/apps/web/app/api/v1/js/sync/lib/surveys.ts index f5ae4cd619..1a896ec1a7 100644 --- a/apps/web/app/api/v1/js/sync/lib/surveys.ts +++ b/apps/web/app/api/v1/js/sync/lib/surveys.ts @@ -1,127 +1,86 @@ -import { prisma } from "@formbricks/database"; -import { selectSurvey } from "@formbricks/lib/survey/service"; +import { getAttributeClasses } from "@formbricks/lib/attributeClass/service"; +import { SERVICES_REVALIDATION_INTERVAL } from "@formbricks/lib/constants"; +import { displayCache } from "@formbricks/lib/display/cache"; +import { getDisplaysByPersonId } from "@formbricks/lib/display/service"; +import { getProductByEnvironmentIdCached, getProductCacheTag } from "@formbricks/lib/product/service"; +import { getSurveyCacheTag, getSurveys } from "@formbricks/lib/survey/service"; import { TSurveyWithTriggers } from "@formbricks/types/js"; import { TPerson } from "@formbricks/types/people"; import { unstable_cache } from "next/cache"; -const getSurveysCacheTags = (environmentId: string, personId: string): string[] => [ - `environments-${environmentId}-surveys`, - `environments-${environmentId}-product`, - personId, -]; +// Helper function to calculate difference in days between two dates +const diffInDays = (date1: Date, date2: Date) => { + const diffTime = Math.abs(date2.getTime() - date1.getTime()); + return Math.floor(diffTime / (1000 * 60 * 60 * 24)); +}; -const getSurveysCacheKey = (environmentId: string, personId: string): string[] => [ - `environments-${environmentId}-person-${personId}-syncSurveys`, -]; - -export const getSurveysCached = (environmentId: string, person: TPerson) => +export const getSyncSurveysCached = (environmentId: string, person: TPerson) => unstable_cache( async () => { - return await getSurveys(environmentId, person); + return await getSyncSurveys(environmentId, person); }, - getSurveysCacheKey(environmentId, person.id), + [`getSyncSurveysCached-${environmentId}-${person.id}`], { - tags: getSurveysCacheTags(environmentId, person.id), - revalidate: 30 * 60, + tags: [ + displayCache.tag.byPersonId(person.id), + getSurveyCacheTag(environmentId), + getProductCacheTag(environmentId), + ], + revalidate: SERVICES_REVALIDATION_INTERVAL, } )(); -export const getSurveys = async (environmentId: string, person: TPerson): Promise => { +export const getSyncSurveys = async ( + environmentId: string, + person: TPerson +): Promise => { // get recontactDays from product - const product = await prisma.product.findFirst({ - where: { - environments: { - some: { - id: environmentId, - }, - }, - }, - select: { - recontactDays: true, - }, - }); + const product = await getProductByEnvironmentIdCached(environmentId); if (!product) { throw new Error("Product not found"); } - // get all surveys that meet the displayOption criteria - const potentialSurveys = await prisma.survey.findMany({ - where: { - OR: [ - { - environmentId, - type: "web", - status: "inProgress", - displayOption: "respondMultiple", - }, - { - environmentId, - type: "web", - status: "inProgress", - displayOption: "displayOnce", - displays: { none: { personId: person.id } }, - }, - { - environmentId, - type: "web", - status: "inProgress", - displayOption: "displayMultiple", - displays: { none: { personId: person.id, status: "responded" } }, - }, - ], - }, - select: { - ...selectSurvey, - attributeFilters: { - select: { - id: true, - condition: true, - value: true, - attributeClass: { - select: { - id: true, - name: true, - }, - }, - }, - }, - displays: { - where: { - personId: person.id, - }, - orderBy: { - createdAt: "desc", - }, - take: 1, - select: { - createdAt: true, - }, - }, - }, + + let surveys = await getSurveys(environmentId); + + // filtered surveys for running and web + surveys = surveys.filter((survey) => survey.status === "inProgress" && survey.type === "web"); + + const displays = await getDisplaysByPersonId(person.id); + + // filter surveys that meet the displayOption criteria + surveys = surveys.filter((survey) => { + if (survey.displayOption === "respondMultiple") { + return true; + } else if (survey.displayOption === "displayOnce") { + return displays.filter((display) => display.surveyId === survey.id).length === 0; + } else if (survey.displayOption === "displayMultiple") { + return ( + displays.filter((display) => display.surveyId === survey.id && display.responseId !== null).length === + 0 + ); + } else { + throw Error("Invalid displayOption"); + } }); - // get last display for this person - const lastDisplayPerson = await prisma.display.findFirst({ - where: { - personId: person.id, - }, - orderBy: { - createdAt: "desc", - }, - select: { - createdAt: true, - }, - }); + const attributeClasses = await getAttributeClasses(environmentId); // filter surveys that meet the attributeFilters criteria - const potentialSurveysWithAttributes = potentialSurveys.filter((survey) => { + const potentialSurveysWithAttributes = surveys.filter((survey) => { const attributeFilters = survey.attributeFilters; if (attributeFilters.length === 0) { return true; } // check if meets all attribute filters criterias return attributeFilters.every((attributeFilter) => { - const personAttributeValue = person.attributes[attributeFilter.attributeClass.name]; + const attributeClassName = attributeClasses.find( + (attributeClass) => attributeClass.id === attributeFilter.attributeClassId + )?.name; + if (!attributeClassName) { + throw Error("Invalid attribute filter class"); + } + const personAttributeValue = person.attributes[attributeClassName]; if (attributeFilter.condition === "equals") { return personAttributeValue === attributeFilter.value; } else if (attributeFilter.condition === "notEquals") { @@ -132,46 +91,24 @@ export const getSurveys = async (environmentId: string, person: TPerson): Promis }); }); + const latestDisplay = displays[0]; + // filter surveys that meet the recontactDays criteria - const surveys: TSurveyWithTriggers[] = potentialSurveysWithAttributes - .filter((survey) => { - if (!lastDisplayPerson) { - // no display yet - always display - return true; - } else if (survey.recontactDays !== null) { - // if recontactDays is set on survey, use that - const lastDisplaySurvey = survey.displays[0]; - if (!lastDisplaySurvey) { - // no display yet - always display - return true; - } - const lastDisplayDate = new Date(lastDisplaySurvey.createdAt); - const currentDate = new Date(); - const diffTime = Math.abs(currentDate.getTime() - lastDisplayDate.getTime()); - const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24)); - return diffDays >= survey.recontactDays; - } else if (product.recontactDays !== null) { - // if recontactDays is not set in survey, use product recontactDays - const lastDisplayDate = new Date(lastDisplayPerson.createdAt); - const currentDate = new Date(); - const diffTime = Math.abs(currentDate.getTime() - lastDisplayDate.getTime()); - const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24)); - return diffDays >= product.recontactDays; - } else { - // if recontactDays is not set in survey or product, always display + surveys = potentialSurveysWithAttributes.filter((survey) => { + if (!latestDisplay) { + return true; + } else if (survey.recontactDays !== null) { + const lastDisplaySurvey = displays.filter((display) => display.surveyId === survey.id)[0]; + if (!lastDisplaySurvey) { return true; } - }) - .map((survey) => ({ - ...survey, - singleUse: survey.singleUse ? JSON.parse(JSON.stringify(survey.singleUse)) : null, - triggers: survey.triggers.map((trigger) => trigger.eventClass), - attributeFilters: survey.attributeFilters.map((af) => ({ - ...af, - attributeClassId: af.attributeClass.id, - attributeClass: undefined, - })), - })); + return diffInDays(new Date(), new Date(lastDisplaySurvey.createdAt)) >= survey.recontactDays; + } else if (product.recontactDays !== null) { + return diffInDays(new Date(), new Date(latestDisplay.createdAt)) >= product.recontactDays; + } else { + return true; + } + }); return surveys; }; diff --git a/apps/web/app/api/v1/js/sync/lib/sync.ts b/apps/web/app/api/v1/js/sync/lib/sync.ts index a37670a00a..697107d39d 100644 --- a/apps/web/app/api/v1/js/sync/lib/sync.ts +++ b/apps/web/app/api/v1/js/sync/lib/sync.ts @@ -1,4 +1,4 @@ -import { getSurveysCached } from "@/app/api/v1/js/sync/lib/surveys"; +import { getSyncSurveysCached } from "@/app/api/v1/js/sync/lib/surveys"; import { MAU_LIMIT } from "@formbricks/lib/constants"; import { getActionClasses } from "@formbricks/lib/actionClass/service"; import { getEnvironment } from "@formbricks/lib/environment/service"; @@ -100,7 +100,7 @@ export const getUpdatedState = async ( // get/create rest of the state const [surveys, noCodeActionClasses, product] = await Promise.all([ - getSurveysCached(environmentId, person), + getSyncSurveysCached(environmentId, person), getActionClasses(environmentId), getProductByEnvironmentIdCached(environmentId), ]); diff --git a/apps/web/pages/api/v1/client/environments/[environmentId]/people/[personId]/attribute.ts b/apps/web/pages/api/v1/client/environments/[environmentId]/people/[personId]/attribute.ts index 5a0a1ab1ec..208e4abbdf 100644 --- a/apps/web/pages/api/v1/client/environments/[environmentId]/people/[personId]/attribute.ts +++ b/apps/web/pages/api/v1/client/environments/[environmentId]/people/[personId]/attribute.ts @@ -1,5 +1,6 @@ import { getSettings } from "@/app/lib/api/clientSettings"; import { prisma } from "@formbricks/database"; +import { personCache } from "@formbricks/lib/person/cache"; import type { NextApiRequest, NextApiResponse } from "next"; export default async function handle(req: NextApiRequest, res: NextApiResponse) { @@ -128,6 +129,11 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse) const person = attribute.person; + personCache.revalidate({ + id: person.id, + environmentId: person.environmentId, + }); + const settings = await getSettings(environmentId, person.id); // return updated person diff --git a/apps/web/pages/api/v1/client/environments/[environmentId]/people/[personId]/user-id.ts b/apps/web/pages/api/v1/client/environments/[environmentId]/people/[personId]/user-id.ts index ff9c962ee6..0b316dfaa2 100644 --- a/apps/web/pages/api/v1/client/environments/[environmentId]/people/[personId]/user-id.ts +++ b/apps/web/pages/api/v1/client/environments/[environmentId]/people/[personId]/user-id.ts @@ -1,5 +1,7 @@ import { getSettings } from "@/app/lib/api/clientSettings"; import { prisma } from "@formbricks/database"; +import { personCache } from "@formbricks/lib/person/cache"; +import { deletePerson } from "@formbricks/lib/person/service"; import type { NextApiRequest, NextApiResponse } from "next"; export default async function handle(req: NextApiRequest, res: NextApiResponse) { @@ -76,11 +78,7 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse) }); // delete old person - await prisma.person.delete({ - where: { - id: personId, - }, - }); + await deletePerson(personId); person = existingPerson; } else { // update person @@ -122,6 +120,11 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse) }); } + personCache.revalidate({ + id: person.id, + environmentId: person.environmentId, + }); + const settings = await getSettings(environmentId, person.id); // return updated person and settings diff --git a/packages/lib/display/service.ts b/packages/lib/display/service.ts index 84eac8a084..8e2f839de8 100644 --- a/packages/lib/display/service.ts +++ b/packages/lib/display/service.ts @@ -159,10 +159,10 @@ export const markDisplayResponded = async (displayId: string): Promise } }; -export const getDisplaysOfPerson = async ( +export const getDisplaysByPersonId = async ( personId: string, page?: number -): Promise => { +): Promise => { const displays = await unstable_cache( async () => { validateInputs([personId, ZId], [page, ZOptionalNumber]); @@ -187,6 +187,9 @@ export const getDisplaysOfPerson = async ( }, take: page ? ITEMS_PER_PAGE : undefined, skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined, + orderBy: { + createdAt: "desc", + }, }); if (!displaysPrisma) { @@ -217,7 +220,7 @@ export const getDisplaysOfPerson = async ( throw error; } }, - [`getDisplaysOfPerson-${personId}-${page}`], + [`getDisplaysByPersonId-${personId}-${page}`], { tags: [displayCache.tag.byPersonId(personId)], revalidate: SERVICES_REVALIDATION_INTERVAL,