diff --git a/packages/database/zod-utils.ts b/packages/database/zod-utils.ts index 6dea2e77ae..e2be879bb3 100644 --- a/packages/database/zod-utils.ts +++ b/packages/database/zod-utils.ts @@ -1,4 +1,4 @@ -import z from "zod"; +import { z } from "zod"; export const ZActionProperties = z.record(z.string()); export { ZActionClassNoCodeConfig } from "@formbricks/types/actionClasses"; diff --git a/packages/ee/advanced-targeting/components/segment-filter.tsx b/packages/ee/advanced-targeting/components/segment-filter.tsx index 6722f0c76c..66035f496a 100644 --- a/packages/ee/advanced-targeting/components/segment-filter.tsx +++ b/packages/ee/advanced-targeting/components/segment-filter.tsx @@ -8,7 +8,7 @@ import { Users2Icon, } from "lucide-react"; import { useEffect, useState } from "react"; -import z from "zod"; +import { z } from "zod"; import { cn } from "@formbricks/lib/cn"; import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone"; import { diff --git a/packages/lib/action/service.ts b/packages/lib/action/service.ts index d324e058b6..81393cb7c2 100644 --- a/packages/lib/action/service.ts +++ b/packages/lib/action/service.ts @@ -1,6 +1,7 @@ import "server-only"; import { Prisma } from "@prisma/client"; import { differenceInDays } from "date-fns"; +import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; import { TAction, TActionInput, ZActionInput } from "@formbricks/types/actions"; import { ZOptionalNumber } from "@formbricks/types/common"; @@ -16,96 +17,100 @@ import { validateInputs } from "../utils/validate"; import { actionCache } from "./cache"; import { getStartDateOfLastMonth, getStartDateOfLastQuarter, getStartDateOfLastWeek } from "./utils"; -export const getActionsByPersonId = (personId: string, page?: number): Promise => - cache( - async () => { - validateInputs([personId, ZId], [page, ZOptionalNumber]); +export const getActionsByPersonId = reactCache( + async (personId: string, page?: number): Promise => + cache( + async () => { + validateInputs([personId, ZId], [page, ZOptionalNumber]); - try { - const actionsPrisma = await prisma.action.findMany({ - where: { - person: { - id: personId, + try { + const actionsPrisma = await prisma.action.findMany({ + where: { + person: { + id: personId, + }, }, - }, - orderBy: { - createdAt: "desc", - }, - take: page ? ITEMS_PER_PAGE : undefined, - skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined, - include: { - actionClass: true, - }, - }); - - return actionsPrisma.map((action) => ({ - id: action.id, - createdAt: action.createdAt, - personId: action.personId, - properties: action.properties, - actionClass: action.actionClass, - })); - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError("Database operation failed"); - } - - throw error; - } - }, - [`getActionsByPersonId-${personId}-${page}`], - { - tags: [actionCache.tag.byPersonId(personId)], - } - )(); - -export const getActionsByEnvironmentId = (environmentId: string, page?: number): Promise => - cache( - async () => { - validateInputs([environmentId, ZId], [page, ZOptionalNumber]); - - try { - const actionsPrisma = await prisma.action.findMany({ - where: { - actionClass: { - environmentId: environmentId, + orderBy: { + createdAt: "desc", }, - }, - orderBy: { - createdAt: "desc", - }, - take: page ? ITEMS_PER_PAGE : undefined, - skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined, - include: { - actionClass: true, - }, - }); - const actions: TAction[] = []; - // transforming response to type TAction[] - actionsPrisma.forEach((action) => { - actions.push({ + take: page ? ITEMS_PER_PAGE : undefined, + skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined, + include: { + actionClass: true, + }, + }); + + return actionsPrisma.map((action) => ({ id: action.id, createdAt: action.createdAt, - // sessionId: action.sessionId, personId: action.personId, properties: action.properties, actionClass: action.actionClass, - }); - }); - return actions; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError("Database operation failed"); - } + })); + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError("Database operation failed"); + } - throw error; + throw error; + } + }, + [`getActionsByPersonId-${personId}-${page}`], + { + tags: [actionCache.tag.byPersonId(personId)], } - }, - [`getActionsByEnvironmentId-${environmentId}-${page}`], - { - tags: [actionCache.tag.byEnvironmentId(environmentId)], - } - )(); + )() +); + +export const getActionsByEnvironmentId = reactCache( + async (environmentId: string, page?: number): Promise => + cache( + async () => { + validateInputs([environmentId, ZId], [page, ZOptionalNumber]); + + try { + const actionsPrisma = await prisma.action.findMany({ + where: { + actionClass: { + environmentId: environmentId, + }, + }, + orderBy: { + createdAt: "desc", + }, + take: page ? ITEMS_PER_PAGE : undefined, + skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined, + include: { + actionClass: true, + }, + }); + const actions: TAction[] = []; + // transforming response to type TAction[] + actionsPrisma.forEach((action) => { + actions.push({ + id: action.id, + createdAt: action.createdAt, + // sessionId: action.sessionId, + personId: action.personId, + properties: action.properties, + actionClass: action.actionClass, + }); + }); + return actions; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError("Database operation failed"); + } + + throw error; + } + }, + [`getActionsByEnvironmentId-${environmentId}-${page}`], + { + tags: [actionCache.tag.byEnvironmentId(environmentId)], + } + )() +); export const createAction = async (data: TActionInput): Promise => { validateInputs([data, ZActionInput]); @@ -165,255 +170,273 @@ export const createAction = async (data: TActionInput): Promise => { } }; -export const getActionCountInLastHour = async (actionClassId: string): Promise => - cache( - async () => { - validateInputs([actionClassId, ZId]); +export const getActionCountInLastHour = reactCache( + async (actionClassId: string): Promise => + cache( + async () => { + validateInputs([actionClassId, ZId]); - try { - const numEventsLastHour = await prisma.action.count({ - where: { - actionClassId: actionClassId, - createdAt: { - gte: new Date(Date.now() - 60 * 60 * 1000), + try { + const numEventsLastHour = await prisma.action.count({ + where: { + actionClassId: actionClassId, + createdAt: { + gte: new Date(Date.now() - 60 * 60 * 1000), + }, }, - }, - }); - return numEventsLastHour; - } catch (error) { - throw error; + }); + return numEventsLastHour; + } catch (error) { + throw error; + } + }, + [`getActionCountInLastHour-${actionClassId}`], + { + tags: [actionClassCache.tag.byId(actionClassId)], } - }, - [`getActionCountInLastHour-${actionClassId}`], - { - tags: [actionClassCache.tag.byId(actionClassId)], - } - )(); + )() +); -export const getActionCountInLast24Hours = async (actionClassId: string): Promise => - cache( - async () => { - validateInputs([actionClassId, ZId]); +export const getActionCountInLast24Hours = reactCache( + async (actionClassId: string): Promise => + cache( + async () => { + validateInputs([actionClassId, ZId]); - try { - const numEventsLast24Hours = await prisma.action.count({ - where: { - actionClassId: actionClassId, - createdAt: { - gte: new Date(Date.now() - 24 * 60 * 60 * 1000), + try { + const numEventsLast24Hours = await prisma.action.count({ + where: { + actionClassId: actionClassId, + createdAt: { + gte: new Date(Date.now() - 24 * 60 * 60 * 1000), + }, }, - }, - }); - return numEventsLast24Hours; - } catch (error) { - throw error; + }); + return numEventsLast24Hours; + } catch (error) { + throw error; + } + }, + [`getActionCountInLast24Hours-${actionClassId}`], + { + tags: [actionClassCache.tag.byId(actionClassId)], } - }, - [`getActionCountInLast24Hours-${actionClassId}`], - { - tags: [actionClassCache.tag.byId(actionClassId)], - } - )(); + )() +); -export const getActionCountInLast7Days = async (actionClassId: string): Promise => - cache( - async () => { - validateInputs([actionClassId, ZId]); +export const getActionCountInLast7Days = reactCache( + async (actionClassId: string): Promise => + cache( + async () => { + validateInputs([actionClassId, ZId]); - try { - const numEventsLast7Days = await prisma.action.count({ - where: { - actionClassId: actionClassId, - createdAt: { - gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), + try { + const numEventsLast7Days = await prisma.action.count({ + where: { + actionClassId: actionClassId, + createdAt: { + gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), + }, }, - }, - }); - return numEventsLast7Days; - } catch (error) { - throw error; + }); + return numEventsLast7Days; + } catch (error) { + throw error; + } + }, + [`getActionCountInLast7Days-${actionClassId}`], + { + tags: [actionClassCache.tag.byId(actionClassId)], } - }, - [`getActionCountInLast7Days-${actionClassId}`], - { - tags: [actionClassCache.tag.byId(actionClassId)], - } - )(); + )() +); -export const getActionCountInLastQuarter = (actionClassId: string, personId: string): Promise => - cache( - async () => { - validateInputs([actionClassId, ZId], [personId, ZId]); +export const getActionCountInLastQuarter = reactCache( + async (actionClassId: string, personId: string): Promise => + cache( + async () => { + validateInputs([actionClassId, ZId], [personId, ZId]); - try { - const numEventsLastQuarter = await prisma.action.count({ - where: { - personId, - actionClass: { - id: actionClassId, + try { + const numEventsLastQuarter = await prisma.action.count({ + where: { + personId, + actionClass: { + id: actionClassId, + }, + createdAt: { + gte: getStartDateOfLastQuarter(), + }, }, - createdAt: { - gte: getStartDateOfLastQuarter(), - }, - }, - }); + }); - return numEventsLastQuarter; - } catch (error) { - throw error; + return numEventsLastQuarter; + } catch (error) { + throw error; + } + }, + [`getActionCountInLastQuarter-${actionClassId}-${personId}`], + { + tags: [actionClassCache.tag.byId(actionClassId)], } - }, - [`getActionCountInLastQuarter-${actionClassId}-${personId}`], - { - tags: [actionClassCache.tag.byId(actionClassId)], - } - )(); + )() +); -export const getActionCountInLastMonth = (actionClassId: string, personId: string): Promise => - cache( - async () => { - validateInputs([actionClassId, ZId], [personId, ZId]); +export const getActionCountInLastMonth = reactCache( + async (actionClassId: string, personId: string): Promise => + cache( + async () => { + validateInputs([actionClassId, ZId], [personId, ZId]); - try { - const numEventsLastMonth = await prisma.action.count({ - where: { - personId, - actionClass: { - id: actionClassId, + try { + const numEventsLastMonth = await prisma.action.count({ + where: { + personId, + actionClass: { + id: actionClassId, + }, + createdAt: { + gte: getStartDateOfLastMonth(), + }, }, - createdAt: { - gte: getStartDateOfLastMonth(), - }, - }, - }); + }); - return numEventsLastMonth; - } catch (error) { - throw error; + return numEventsLastMonth; + } catch (error) { + throw error; + } + }, + [`getActionCountInLastMonth-${actionClassId}-${personId}`], + { + tags: [actionClassCache.tag.byId(actionClassId)], } - }, - [`getActionCountInLastMonth-${actionClassId}-${personId}`], - { - tags: [actionClassCache.tag.byId(actionClassId)], - } - )(); + )() +); -export const getActionCountInLastWeek = (actionClassId: string, personId: string): Promise => - cache( - async () => { - validateInputs([actionClassId, ZId], [personId, ZId]); +export const getActionCountInLastWeek = reactCache( + async (actionClassId: string, personId: string): Promise => + cache( + async () => { + validateInputs([actionClassId, ZId], [personId, ZId]); - try { - const numEventsLastWeek = await prisma.action.count({ - where: { - personId, - actionClass: { - id: actionClassId, + try { + const numEventsLastWeek = await prisma.action.count({ + where: { + personId, + actionClass: { + id: actionClassId, + }, + createdAt: { + gte: getStartDateOfLastWeek(), + }, }, - createdAt: { - gte: getStartDateOfLastWeek(), - }, - }, - }); - return numEventsLastWeek; - } catch (error) { - throw error; + }); + return numEventsLastWeek; + } catch (error) { + throw error; + } + }, + [`getActionCountInLastWeek-${actionClassId}-${personId}`], + { + tags: [actionClassCache.tag.byId(actionClassId)], } - }, - [`getActionCountInLastWeek-${actionClassId}-${personId}`], - { - tags: [actionClassCache.tag.byId(actionClassId)], - } - )(); + )() +); -export const getTotalOccurrencesForAction = (actionClassId: string, personId: string): Promise => - cache( - async () => { - validateInputs([actionClassId, ZId], [personId, ZId]); +export const getTotalOccurrencesForAction = reactCache( + async (actionClassId: string, personId: string): Promise => + cache( + async () => { + validateInputs([actionClassId, ZId], [personId, ZId]); - try { - const count = await prisma.action.count({ - where: { - personId, - actionClass: { - id: actionClassId, + try { + const count = await prisma.action.count({ + where: { + personId, + actionClass: { + id: actionClassId, + }, }, - }, - }); + }); - return count; - } catch (error) { - throw error; + return count; + } catch (error) { + throw error; + } + }, + [`getTotalOccurrencesForAction-${actionClassId}-${personId}`], + { + tags: [actionClassCache.tag.byId(actionClassId)], } - }, - [`getTotalOccurrencesForAction-${actionClassId}-${personId}`], - { - tags: [actionClassCache.tag.byId(actionClassId)], - } - )(); + )() +); -export const getLastOccurrenceDaysAgo = (actionClassId: string, personId: string): Promise => - cache( - async () => { - validateInputs([actionClassId, ZId], [personId, ZId]); +export const getLastOccurrenceDaysAgo = reactCache( + async (actionClassId: string, personId: string): Promise => + cache( + async () => { + validateInputs([actionClassId, ZId], [personId, ZId]); - try { - const lastEvent = await prisma.action.findFirst({ - where: { - personId, - actionClass: { - id: actionClassId, + try { + const lastEvent = await prisma.action.findFirst({ + where: { + personId, + actionClass: { + id: actionClassId, + }, }, - }, - orderBy: { - createdAt: "desc", - }, - select: { - createdAt: true, - }, - }); - - if (!lastEvent) return null; - return differenceInDays(new Date(), lastEvent.createdAt); - } catch (error) { - throw error; - } - }, - [`getLastOccurrenceDaysAgo-${actionClassId}-${personId}`], - { - tags: [actionClassCache.tag.byId(actionClassId)], - } - )(); - -export const getFirstOccurrenceDaysAgo = (actionClassId: string, personId: string): Promise => - cache( - async () => { - validateInputs([actionClassId, ZId], [personId, ZId]); - - try { - const firstEvent = await prisma.action.findFirst({ - where: { - personId, - actionClass: { - id: actionClassId, + orderBy: { + createdAt: "desc", }, - }, - orderBy: { - createdAt: "asc", - }, - select: { - createdAt: true, - }, - }); + select: { + createdAt: true, + }, + }); - if (!firstEvent) return null; - return differenceInDays(new Date(), firstEvent.createdAt); - } catch (error) { - throw error; + if (!lastEvent) return null; + return differenceInDays(new Date(), lastEvent.createdAt); + } catch (error) { + throw error; + } + }, + [`getLastOccurrenceDaysAgo-${actionClassId}-${personId}`], + { + tags: [actionClassCache.tag.byId(actionClassId)], } - }, - [`getFirstOccurrenceDaysAgo-${actionClassId}-${personId}`], - { - tags: [actionClassCache.tag.byId(actionClassId)], - } - )(); + )() +); + +export const getFirstOccurrenceDaysAgo = reactCache( + async (actionClassId: string, personId: string): Promise => + cache( + async () => { + validateInputs([actionClassId, ZId], [personId, ZId]); + + try { + const firstEvent = await prisma.action.findFirst({ + where: { + personId, + actionClass: { + id: actionClassId, + }, + }, + orderBy: { + createdAt: "asc", + }, + select: { + createdAt: true, + }, + }); + + if (!firstEvent) return null; + return differenceInDays(new Date(), firstEvent.createdAt); + } catch (error) { + throw error; + } + }, + [`getFirstOccurrenceDaysAgo-${actionClassId}-${personId}`], + { + tags: [actionClassCache.tag.byId(actionClassId)], + } + )() +); diff --git a/packages/lib/actionClass/service.ts b/packages/lib/actionClass/service.ts index 537b584bec..9801f2c511 100644 --- a/packages/lib/actionClass/service.ts +++ b/packages/lib/actionClass/service.ts @@ -2,6 +2,7 @@ import "server-only"; import { Prisma } from "@prisma/client"; +import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; import { TActionClass, TActionClassInput, ZActionClassInput } from "@formbricks/types/actionClasses"; import { ZOptionalNumber, ZString } from "@formbricks/types/common"; @@ -24,85 +25,88 @@ const selectActionClass = { environmentId: true, } satisfies Prisma.ActionClassSelect; -export const getActionClasses = (environmentId: string, page?: number): Promise => - cache( - async () => { - validateInputs([environmentId, ZId], [page, ZOptionalNumber]); +export const getActionClasses = reactCache( + async (environmentId: string, page?: number): Promise => + cache( + async () => { + validateInputs([environmentId, ZId], [page, ZOptionalNumber]); - try { - return await prisma.actionClass.findMany({ - where: { - environmentId: environmentId, - }, - select: selectActionClass, - take: page ? ITEMS_PER_PAGE : undefined, - skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined, - orderBy: { - createdAt: "asc", - }, - }); - } catch (error) { - throw new DatabaseError(`Database error when fetching actions for environment ${environmentId}`); + try { + return await prisma.actionClass.findMany({ + where: { + environmentId: environmentId, + }, + select: selectActionClass, + take: page ? ITEMS_PER_PAGE : undefined, + skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined, + orderBy: { + createdAt: "asc", + }, + }); + } catch (error) { + throw new DatabaseError(`Database error when fetching actions for environment ${environmentId}`); + } + }, + [`getActionClasses-${environmentId}-${page}`], + { + tags: [actionClassCache.tag.byEnvironmentId(environmentId)], } - }, - [`getActionClasses-${environmentId}-${page}`], - { - tags: [actionClassCache.tag.byEnvironmentId(environmentId)], - } - )(); + )() +); // This function is used to get an action by its name and environmentId(it can return private actions as well) -export const getActionClassByEnvironmentIdAndName = ( - environmentId: string, - name: string -): Promise => - cache( - async () => { - validateInputs([environmentId, ZId], [name, ZString]); +export const getActionClassByEnvironmentIdAndName = reactCache( + async (environmentId: string, name: string): Promise => + cache( + async () => { + validateInputs([environmentId, ZId], [name, ZString]); - try { - const actionClass = await prisma.actionClass.findFirst({ - where: { - name, - environmentId, - }, - select: selectActionClass, - }); + try { + const actionClass = await prisma.actionClass.findFirst({ + where: { + name, + environmentId, + }, + select: selectActionClass, + }); - return actionClass; - } catch (error) { - throw new DatabaseError(`Database error when fetching action`); + return actionClass; + } catch (error) { + throw new DatabaseError(`Database error when fetching action`); + } + }, + [`getActionClassByEnvironmentIdAndName-${environmentId}-${name}`], + { + tags: [actionClassCache.tag.byNameAndEnvironmentId(name, environmentId)], } - }, - [`getActionClassByEnvironmentIdAndName-${environmentId}-${name}`], - { - tags: [actionClassCache.tag.byNameAndEnvironmentId(name, environmentId)], - } - )(); + )() +); -export const getActionClass = (actionClassId: string): Promise => - cache( - async () => { - validateInputs([actionClassId, ZId]); +export const getActionClass = reactCache( + async (actionClassId: string): Promise => + cache( + async () => { + validateInputs([actionClassId, ZId]); - try { - const actionClass = await prisma.actionClass.findUnique({ - where: { - id: actionClassId, - }, - select: selectActionClass, - }); + try { + const actionClass = await prisma.actionClass.findUnique({ + where: { + id: actionClassId, + }, + select: selectActionClass, + }); - return actionClass; - } catch (error) { - throw new DatabaseError(`Database error when fetching action`); + return actionClass; + } catch (error) { + throw new DatabaseError(`Database error when fetching action`); + } + }, + [`getActionClass-${actionClassId}`], + { + tags: [actionClassCache.tag.byId(actionClassId)], } - }, - [`getActionClass-${actionClassId}`], - { - tags: [actionClassCache.tag.byId(actionClassId)], - } - )(); + )() +); export const deleteActionClass = async ( environmentId: string, diff --git a/packages/lib/apiKey/service.ts b/packages/lib/apiKey/service.ts index ba63059131..7a52567c2d 100644 --- a/packages/lib/apiKey/service.ts +++ b/packages/lib/apiKey/service.ts @@ -1,6 +1,7 @@ import "server-only"; import { Prisma } from "@prisma/client"; import { createHash, randomBytes } from "crypto"; +import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; import { TApiKey, TApiKeyCreateInput, ZApiKeyCreateInput } from "@formbricks/types/apiKeys"; import { ZOptionalNumber, ZString } from "@formbricks/types/common"; @@ -12,64 +13,68 @@ import { getHash } from "../crypto"; import { validateInputs } from "../utils/validate"; import { apiKeyCache } from "./cache"; -export const getApiKey = (apiKeyId: string): Promise => - cache( - async () => { - validateInputs([apiKeyId, ZString]); +export const getApiKey = reactCache( + (apiKeyId: string): Promise => + cache( + async () => { + validateInputs([apiKeyId, ZString]); - if (!apiKeyId) { - throw new InvalidInputError("API key cannot be null or undefined."); - } - - try { - const apiKeyData = await prisma.apiKey.findUnique({ - where: { - id: apiKeyId, - }, - }); - - return apiKeyData; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); + if (!apiKeyId) { + throw new InvalidInputError("API key cannot be null or undefined."); } - throw error; - } - }, - [`getApiKey-${apiKeyId}`], - { - tags: [apiKeyCache.tag.byId(apiKeyId)], - } - )(); + try { + const apiKeyData = await prisma.apiKey.findUnique({ + where: { + id: apiKeyId, + }, + }); -export const getApiKeys = (environmentId: string, page?: number): Promise => - cache( - async () => { - validateInputs([environmentId, ZId], [page, ZOptionalNumber]); + return apiKeyData; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } - try { - const apiKeys = await prisma.apiKey.findMany({ - where: { - environmentId, - }, - take: page ? ITEMS_PER_PAGE : undefined, - skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined, - }); - - return apiKeys; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); + throw error; } - throw error; + }, + [`getApiKey-${apiKeyId}`], + { + tags: [apiKeyCache.tag.byId(apiKeyId)], } - }, - [`getApiKeys-${environmentId}-${page}`], - { - tags: [apiKeyCache.tag.byEnvironmentId(environmentId)], - } - )(); + )() +); + +export const getApiKeys = reactCache( + (environmentId: string, page?: number): Promise => + cache( + async () => { + validateInputs([environmentId, ZId], [page, ZOptionalNumber]); + + try { + const apiKeys = await prisma.apiKey.findMany({ + where: { + environmentId, + }, + take: page ? ITEMS_PER_PAGE : undefined, + skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined, + }); + + return apiKeys; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + throw error; + } + }, + [`getApiKeys-${environmentId}-${page}`], + { + tags: [apiKeyCache.tag.byEnvironmentId(environmentId)], + } + )() +); export const hashApiKey = (key: string): string => createHash("sha256").update(key).digest("hex"); @@ -105,9 +110,8 @@ export const createApiKey = async ( } }; -export const getApiKeyFromKey = (apiKey: string): Promise => { +export const getApiKeyFromKey = reactCache((apiKey: string): Promise => { const hashedKey = getHash(apiKey); - return cache( async () => { validateInputs([apiKey, ZString]); @@ -137,7 +141,7 @@ export const getApiKeyFromKey = (apiKey: string): Promise => { tags: [apiKeyCache.tag.byHashedKey(hashedKey)], } )(); -}; +}); export const deleteApiKey = async (id: string): Promise => { validateInputs([id, ZId]); diff --git a/packages/lib/attribute/service.ts b/packages/lib/attribute/service.ts index 83f5c33f35..6d0961f0d4 100644 --- a/packages/lib/attribute/service.ts +++ b/packages/lib/attribute/service.ts @@ -1,5 +1,6 @@ import "server-only"; import { Prisma } from "@prisma/client"; +import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; import { TAttributes, ZAttributes } from "@formbricks/types/attributes"; import { ZString } from "@formbricks/types/common"; @@ -38,67 +39,71 @@ const convertPrismaAttributes = (prismaAttributes: any): TAttributes => { ); }; -export const getAttributes = (personId: string): Promise => - cache( - async () => { - validateInputs([personId, ZId]); +export const getAttributes = reactCache( + (personId: string): Promise => + cache( + async () => { + validateInputs([personId, ZId]); - try { - const prismaAttributes = await prisma.attribute.findMany({ - where: { - personId, - }, - select: selectAttribute, - }); + 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); + return convertPrismaAttributes(prismaAttributes); + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + + throw error; + } + }, + [`getAttributes-${personId}`], + { + tags: [attributeCache.tag.byPersonId(personId)], + } + )() +); + +export const getAttributesByUserId = reactCache( + (environmentId: string, userId: string): Promise => + cache( + async () => { + validateInputs([environmentId, ZId], [userId, ZString]); + + const person = await getPersonByUserId(environmentId, userId); + + if (!person) { + throw new Error("Person not found"); } - throw error; - } - }, - [`getAttributes-${personId}`], - { - tags: [attributeCache.tag.byPersonId(personId)], - } - )(); + try { + const prismaAttributes = await prisma.attribute.findMany({ + where: { + personId: person.id, + }, + select: selectAttribute, + }); -export const getAttributesByUserId = (environmentId: string, userId: string): Promise => - cache( - async () => { - validateInputs([environmentId, ZId], [userId, ZString]); + return convertPrismaAttributes(prismaAttributes); + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } - 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; } - - throw error; + }, + [`getAttributesByUserId-${environmentId}-${userId}`], + { + tags: [attributeCache.tag.byEnvironmentIdAndUserId(environmentId, userId)], } - }, - [`getAttributesByUserId-${environmentId}-${userId}`], - { - tags: [attributeCache.tag.byEnvironmentIdAndUserId(environmentId, userId)], - } - )(); + )() +); export const getAttribute = (name: string, personId: string): Promise => cache( diff --git a/packages/lib/attributeClass/service.ts b/packages/lib/attributeClass/service.ts index 59c77fe6b2..5d809cafc2 100644 --- a/packages/lib/attributeClass/service.ts +++ b/packages/lib/attributeClass/service.ts @@ -2,6 +2,7 @@ import "server-only"; import { Prisma } from "@prisma/client"; +import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; import { TAttributeClass, @@ -18,68 +19,72 @@ import { ITEMS_PER_PAGE, MAX_ATTRIBUTE_CLASSES_PER_ENVIRONMENT } from "../consta import { validateInputs } from "../utils/validate"; import { attributeClassCache } from "./cache"; -export const getAttributeClass = async (attributeClassId: string): Promise => - cache( - async () => { - validateInputs([attributeClassId, ZId]); +export const getAttributeClass = reactCache( + async (attributeClassId: string): Promise => + cache( + async () => { + validateInputs([attributeClassId, ZId]); - try { - const attributeClass = await prisma.attributeClass.findFirst({ - where: { - id: attributeClassId, - }, - }); + try { + const attributeClass = await prisma.attributeClass.findFirst({ + where: { + id: attributeClassId, + }, + }); - return attributeClass; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); - } - throw error; - } - }, - [`getAttributeClass-${attributeClassId}`], - { - tags: [attributeClassCache.tag.byId(attributeClassId)], - } - )(); - -export const getAttributeClasses = async (environmentId: string, page?: number): Promise => - cache( - async () => { - validateInputs([environmentId, ZId], [page, ZOptionalNumber]); - - try { - const attributeClasses = await prisma.attributeClass.findMany({ - where: { - environmentId: environmentId, - }, - orderBy: { - createdAt: "asc", - }, - take: page ? ITEMS_PER_PAGE : undefined, - skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined, - }); - - return attributeClasses.filter((attributeClass) => { - if (attributeClass.name === "userId" && attributeClass.type === "automatic") { - return false; + return attributeClass; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); } - - return true; - }); - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); + throw error; } - throw error; + }, + [`getAttributeClass-${attributeClassId}`], + { + tags: [attributeClassCache.tag.byId(attributeClassId)], } - }, - [`getAttributeClasses-${environmentId}-${page}`], - { - tags: [attributeClassCache.tag.byEnvironmentId(environmentId)], - } - )(); + )() +); + +export const getAttributeClasses = reactCache( + async (environmentId: string, page?: number): Promise => + cache( + async () => { + validateInputs([environmentId, ZId], [page, ZOptionalNumber]); + + try { + const attributeClasses = await prisma.attributeClass.findMany({ + where: { + environmentId: environmentId, + }, + orderBy: { + createdAt: "asc", + }, + take: page ? ITEMS_PER_PAGE : undefined, + skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined, + }); + + return attributeClasses.filter((attributeClass) => { + if (attributeClass.name === "userId" && attributeClass.type === "automatic") { + return false; + } + + return true; + }); + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + throw error; + } + }, + [`getAttributeClasses-${environmentId}-${page}`], + { + tags: [attributeClassCache.tag.byEnvironmentId(environmentId)], + } + )() +); export const updateAttributeClass = async ( attributeClassId: string, @@ -113,7 +118,7 @@ export const updateAttributeClass = async ( } }; -export const getAttributeClassByName = async (environmentId: string, name: string) => +export const getAttributeClassByName = reactCache((environmentId: string, name: string) => cache( async (): Promise => { validateInputs([environmentId, ZId], [name, ZString]); @@ -138,7 +143,8 @@ export const getAttributeClassByName = async (environmentId: string, name: strin { tags: [attributeClassCache.tag.byEnvironmentIdAndName(environmentId, name)], } - )(); + )() +); export const createAttributeClass = async ( environmentId: string, @@ -208,26 +214,28 @@ export const deleteAttributeClass = async (attributeClassId: string): Promise => - cache( - async () => { - validateInputs([environmentId, ZId]); +export const getAttributeClassesCount = reactCache( + async (environmentId: string): Promise => + cache( + async () => { + validateInputs([environmentId, ZId]); - try { - return prisma.attributeClass.count({ - where: { - environmentId, - }, - }); - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); + try { + return prisma.attributeClass.count({ + where: { + environmentId, + }, + }); + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + throw error; } - throw error; + }, + [`getAttributeClassesCount-${environmentId}`], + { + tags: [attributeClassCache.tag.byEnvironmentId(environmentId)], } - }, - [`getAttributeClassesCount-${environmentId}`], - { - tags: [attributeClassCache.tag.byEnvironmentId(environmentId)], - } - )(); + )() +); diff --git a/packages/lib/display/service.ts b/packages/lib/display/service.ts index 491fe9966e..1245e96e72 100644 --- a/packages/lib/display/service.ts +++ b/packages/lib/display/service.ts @@ -1,5 +1,6 @@ import "server-only"; import { Prisma } from "@prisma/client"; +import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; import { ZOptionalNumber } from "@formbricks/types/common"; import { @@ -33,33 +34,35 @@ export const selectDisplay = { status: true, }; -export const getDisplay = (displayId: string): Promise => - cache( - async () => { - validateInputs([displayId, ZId]); +export const getDisplay = reactCache( + async (displayId: string): Promise => + cache( + async () => { + validateInputs([displayId, ZId]); - try { - const display = await prisma.display.findUnique({ - where: { - id: displayId, - }, - select: selectDisplay, - }); + try { + const display = await prisma.display.findUnique({ + where: { + id: displayId, + }, + select: selectDisplay, + }); - return display; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); + return display; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + + throw error; } - - throw error; + }, + [`getDisplay-${displayId}`], + { + tags: [displayCache.tag.byId(displayId)], } - }, - [`getDisplay-${displayId}`], - { - tags: [displayCache.tag.byId(displayId)], - } - )(); + )() +); export const updateDisplay = async ( displayId: string, @@ -272,38 +275,40 @@ export const markDisplayRespondedLegacy = async (displayId: string): Promise => - cache( - async () => { - validateInputs([personId, ZId], [page, ZOptionalNumber]); +export const getDisplaysByPersonId = reactCache( + async (personId: string, page?: number): Promise => + cache( + async () => { + validateInputs([personId, ZId], [page, ZOptionalNumber]); - try { - const displays = await prisma.display.findMany({ - where: { - personId: personId, - }, - select: selectDisplay, - take: page ? ITEMS_PER_PAGE : undefined, - skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined, - orderBy: { - createdAt: "desc", - }, - }); + try { + const displays = await prisma.display.findMany({ + where: { + personId: personId, + }, + select: selectDisplay, + take: page ? ITEMS_PER_PAGE : undefined, + skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined, + orderBy: { + createdAt: "desc", + }, + }); - return displays; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); + return displays; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + + throw error; } - - throw error; + }, + [`getDisplaysByPersonId-${personId}-${page}`], + { + tags: [displayCache.tag.byPersonId(personId)], } - }, - [`getDisplaysByPersonId-${personId}-${page}`], - { - tags: [displayCache.tag.byPersonId(personId)], - } - )(); + )() +); export const deleteDisplayByResponseId = async ( responseId: string, @@ -333,34 +338,36 @@ export const deleteDisplayByResponseId = async ( } }; -export const getDisplayCountBySurveyId = (surveyId: string, filters?: TDisplayFilters): Promise => - cache( - async () => { - validateInputs([surveyId, ZId]); +export const getDisplayCountBySurveyId = reactCache( + (surveyId: string, filters?: TDisplayFilters): Promise => + cache( + async () => { + validateInputs([surveyId, ZId]); - try { - const displayCount = await prisma.display.count({ - where: { - surveyId: surveyId, - ...(filters && - filters.createdAt && { - createdAt: { - gte: filters.createdAt.min, - lte: filters.createdAt.max, - }, - }), - }, - }); - return displayCount; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); + try { + const displayCount = await prisma.display.count({ + where: { + surveyId: surveyId, + ...(filters && + filters.createdAt && { + createdAt: { + gte: filters.createdAt.min, + lte: filters.createdAt.max, + }, + }), + }, + }); + return displayCount; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + throw error; } - throw error; + }, + [`getDisplayCountBySurveyId-${surveyId}-${JSON.stringify(filters)}`], + { + tags: [displayCache.tag.bySurveyId(surveyId)], } - }, - [`getDisplayCountBySurveyId-${surveyId}-${JSON.stringify(filters)}`], - { - tags: [displayCache.tag.bySurveyId(surveyId)], - } - )(); + )() +); diff --git a/packages/lib/environment/service.ts b/packages/lib/environment/service.ts index ec5a231b16..603b8252e7 100644 --- a/packages/lib/environment/service.ts +++ b/packages/lib/environment/service.ts @@ -1,5 +1,6 @@ import "server-only"; import { Prisma } from "@prisma/client"; +import { cache as reactCache } from "react"; import { z } from "zod"; import { prisma } from "@formbricks/database"; import type { @@ -20,78 +21,82 @@ import { getProducts } from "../product/service"; import { validateInputs } from "../utils/validate"; import { environmentCache } from "./cache"; -export const getEnvironment = (environmentId: string): Promise => - cache( - async () => { - validateInputs([environmentId, ZId]); +export const getEnvironment = reactCache( + (environmentId: string): Promise => + cache( + async () => { + validateInputs([environmentId, ZId]); - try { - const environment = await prisma.environment.findUnique({ - where: { - id: environmentId, - }, - }); - return environment; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); - throw new DatabaseError(error.message); + try { + const environment = await prisma.environment.findUnique({ + where: { + id: environmentId, + }, + }); + return environment; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + console.error(error); + throw new DatabaseError(error.message); + } + + throw error; + } + }, + [`getEnvironment-${environmentId}`], + { + tags: [environmentCache.tag.byId(environmentId)], + } + )() +); + +export const getEnvironments = reactCache( + (productId: string): Promise => + cache( + async (): Promise => { + validateInputs([productId, ZId]); + let productPrisma; + try { + productPrisma = await prisma.product.findFirst({ + where: { + id: productId, + }, + include: { + environments: true, + }, + }); + + if (!productPrisma) { + throw new ResourceNotFoundError("Product", productId); + } + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + throw error; } - throw error; - } - }, - [`getEnvironment-${environmentId}`], - { - tags: [environmentCache.tag.byId(environmentId)], - } - )(); - -export const getEnvironments = async (productId: string): Promise => - cache( - async (): Promise => { - validateInputs([productId, ZId]); - let productPrisma; - try { - productPrisma = await prisma.product.findFirst({ - where: { - id: productId, - }, - include: { - environments: true, - }, - }); - - if (!productPrisma) { - throw new ResourceNotFoundError("Product", productId); + const environments: TEnvironment[] = []; + for (let environment of productPrisma.environments) { + let targetEnvironment: TEnvironment = ZEnvironment.parse(environment); + environments.push(targetEnvironment); } - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); - } - throw error; - } - const environments: TEnvironment[] = []; - for (let environment of productPrisma.environments) { - let targetEnvironment: TEnvironment = ZEnvironment.parse(environment); - environments.push(targetEnvironment); - } - - try { - return environments; - } catch (error) { - if (error instanceof z.ZodError) { - console.error(JSON.stringify(error.errors, null, 2)); + try { + return environments; + } catch (error) { + if (error instanceof z.ZodError) { + console.error(JSON.stringify(error.errors, null, 2)); + } + throw new ValidationError("Data validation of environments array failed"); } - throw new ValidationError("Data validation of environments array failed"); + }, + [`getEnvironments-${productId}`], + { + tags: [environmentCache.tag.byProductId(productId)], } - }, - [`getEnvironments-${productId}`], - { - tags: [environmentCache.tag.byProductId(productId)], - } - )(); + )() +); export const updateEnvironment = async ( environmentId: string, diff --git a/packages/lib/instance/service.ts b/packages/lib/instance/service.ts index 92ff92b488..62ddb61f61 100644 --- a/packages/lib/instance/service.ts +++ b/packages/lib/instance/service.ts @@ -1,5 +1,6 @@ import "server-only"; import { Prisma } from "@prisma/client"; +import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; import { DatabaseError } from "@formbricks/types/errors"; import { cache } from "../cache"; @@ -7,38 +8,42 @@ import { organizationCache } from "../organization/cache"; import { userCache } from "../user/cache"; // Function to check if there are any users in the database -export const getIsFreshInstance = (): Promise => - cache( - async () => { - try { - const userCount = await prisma.user.count(); - if (userCount === 0) return true; - else return false; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); +export const getIsFreshInstance = reactCache( + (): Promise => + cache( + async () => { + try { + const userCount = await prisma.user.count(); + if (userCount === 0) return true; + else return false; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + throw error; } - throw error; - } - }, - ["getIsFreshInstance"], - { tags: [userCache.tag.byCount()] } - )(); + }, + ["getIsFreshInstance"], + { tags: [userCache.tag.byCount()] } + )() +); // Function to check if there are any organizations in the database -export const gethasNoOrganizations = (): Promise => - cache( - async () => { - try { - const organizationCount = await prisma.organization.count(); - return organizationCount === 0; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); +export const gethasNoOrganizations = reactCache( + (): Promise => + cache( + async () => { + try { + const organizationCount = await prisma.organization.count(); + return organizationCount === 0; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + throw error; } - throw error; - } - }, - ["gethasNoOrganizations"], - { tags: [organizationCache.tag.byCount()] } - )(); + }, + ["gethasNoOrganizations"], + { tags: [organizationCache.tag.byCount()] } + )() +); diff --git a/packages/lib/integration/service.ts b/packages/lib/integration/service.ts index 83c525f734..2e6cc7198a 100644 --- a/packages/lib/integration/service.ts +++ b/packages/lib/integration/service.ts @@ -1,5 +1,6 @@ import "server-only"; import { Prisma } from "@prisma/client"; +import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; import { ZOptionalNumber, ZString } from "@formbricks/types/common"; import { ZId } from "@formbricks/types/environment"; @@ -48,86 +49,89 @@ export const createOrUpdateIntegration = async ( } }; -export const getIntegrations = (environmentId: string, page?: number): Promise => - cache( - async () => { - validateInputs([environmentId, ZId], [page, ZOptionalNumber]); +export const getIntegrations = reactCache( + (environmentId: string, page?: number): Promise => + cache( + async () => { + validateInputs([environmentId, ZId], [page, ZOptionalNumber]); - try { - const integrations = await prisma.integration.findMany({ - where: { - environmentId, - }, - take: page ? ITEMS_PER_PAGE : undefined, - skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined, - }); - return integrations; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); - } - throw error; - } - }, - [`getIntegrations-${environmentId}-${page}`], - { - tags: [integrationCache.tag.byEnvironmentId(environmentId)], - } - )(); - -export const getIntegration = (integrationId: string): Promise => - cache( - async () => { - try { - const integration = await prisma.integration.findUnique({ - where: { - id: integrationId, - }, - }); - return integration; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); - } - throw error; - } - }, - [`getIntegration-${integrationId}`], - { - tags: [integrationCache.tag.byId(integrationId)], - } - )(); - -export const getIntegrationByType = ( - environmentId: string, - type: TIntegrationInput["type"] -): Promise => - cache( - async () => { - validateInputs([environmentId, ZId], [type, ZIntegrationType]); - - try { - const integration = await prisma.integration.findUnique({ - where: { - type_environmentId: { + try { + const integrations = await prisma.integration.findMany({ + where: { environmentId, - type, }, - }, - }); - return integration; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); + take: page ? ITEMS_PER_PAGE : undefined, + skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined, + }); + return integrations; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + throw error; } - throw error; + }, + [`getIntegrations-${environmentId}-${page}`], + { + tags: [integrationCache.tag.byEnvironmentId(environmentId)], } - }, - [`getIntegrationByType-${environmentId}-${type}`], - { - tags: [integrationCache.tag.byEnvironmentIdAndType(environmentId, type)], - } - )(); + )() +); + +export const getIntegration = reactCache( + (integrationId: string): Promise => + cache( + async () => { + try { + const integration = await prisma.integration.findUnique({ + where: { + id: integrationId, + }, + }); + return integration; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + throw error; + } + }, + [`getIntegration-${integrationId}`], + { + tags: [integrationCache.tag.byId(integrationId)], + } + )() +); + +export const getIntegrationByType = reactCache( + (environmentId: string, type: TIntegrationInput["type"]): Promise => + cache( + async () => { + validateInputs([environmentId, ZId], [type, ZIntegrationType]); + + try { + const integration = await prisma.integration.findUnique({ + where: { + type_environmentId: { + environmentId, + type, + }, + }, + }); + return integration; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + throw error; + } + }, + [`getIntegrationByType-${environmentId}-${type}`], + { + tags: [integrationCache.tag.byEnvironmentIdAndType(environmentId, type)], + } + )() +); export const deleteIntegration = async (integrationId: string): Promise => { validateInputs([integrationId, ZString]); diff --git a/packages/lib/invite/service.ts b/packages/lib/invite/service.ts index 63b3978849..ece245bf72 100644 --- a/packages/lib/invite/service.ts +++ b/packages/lib/invite/service.ts @@ -1,6 +1,7 @@ import "server-only"; import { Prisma } from "@prisma/client"; import { getServerSession } from "next-auth"; +import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; import { ZOptionalNumber, ZString } from "@formbricks/types/common"; import { @@ -41,33 +42,35 @@ interface InviteWithCreator extends TInvite { email: string; }; } -export const getInvitesByOrganizationId = (organizationId: string, page?: number): Promise => - cache( - async () => { - validateInputs([organizationId, ZString], [page, ZOptionalNumber]); +export const getInvitesByOrganizationId = reactCache( + (organizationId: string, page?: number): Promise => + cache( + async () => { + validateInputs([organizationId, ZString], [page, ZOptionalNumber]); - try { - const invites = await prisma.invite.findMany({ - where: { organizationId }, - select: inviteSelect, - take: page ? ITEMS_PER_PAGE : undefined, - skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined, - }); + try { + const invites = await prisma.invite.findMany({ + where: { organizationId }, + select: inviteSelect, + take: page ? ITEMS_PER_PAGE : undefined, + skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined, + }); - return invites; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); + return invites; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + + throw error; } - - throw error; + }, + [`getInvitesByOrganizationId-${organizationId}-${page}`], + { + tags: [inviteCache.tag.byOrganizationId(organizationId)], } - }, - [`getInvitesByOrganizationId-${organizationId}-${page}`], - { - tags: [inviteCache.tag.byOrganizationId(organizationId)], - } - )(); + )() +); export const updateInvite = async (inviteId: string, data: TInviteUpdateInput): Promise => { validateInputs([inviteId, ZString], [data, ZInviteUpdateInput]); @@ -127,40 +130,42 @@ export const deleteInvite = async (inviteId: string): Promise => { } }; -export const getInvite = (inviteId: string): Promise => - cache( - async () => { - validateInputs([inviteId, ZString]); +export const getInvite = reactCache( + (inviteId: string): Promise => + cache( + async () => { + validateInputs([inviteId, ZString]); - try { - const invite = await prisma.invite.findUnique({ - where: { - id: inviteId, - }, - include: { - creator: { - select: { - name: true, - email: true, + try { + const invite = await prisma.invite.findUnique({ + where: { + id: inviteId, + }, + include: { + creator: { + select: { + name: true, + email: true, + }, }, }, - }, - }); + }); - return invite; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); + return invite; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + + throw error; } - - throw error; + }, + [`getInvite-${inviteId}`], + { + tags: [inviteCache.tag.byId(inviteId)], } - }, - [`getInvite-${inviteId}`], - { - tags: [inviteCache.tag.byId(inviteId)], - } - )(); + )() +); export const resendInvite = async (inviteId: string): Promise => { validateInputs([inviteId, ZString]); diff --git a/packages/lib/language/service.ts b/packages/lib/language/service.ts index 1d1554a6cf..4f72a67f64 100644 --- a/packages/lib/language/service.ts +++ b/packages/lib/language/service.ts @@ -1,4 +1,5 @@ import { Prisma } from "@prisma/client"; +import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; import { ZId } from "@formbricks/types/environment"; import { DatabaseError, ValidationError } from "@formbricks/types/errors"; @@ -58,7 +59,7 @@ export const createLanguage = async ( } }; -export const getSurveysUsingGivenLanguage = async (languageId: string): Promise => { +export const getSurveysUsingGivenLanguage = reactCache(async (languageId: string): Promise => { try { // Check if the language is used in any survey const surveys = await prisma.surveyLanguage.findMany({ @@ -84,7 +85,7 @@ export const getSurveysUsingGivenLanguage = async (languageId: string): Promise< } throw error; } -}; +}); export const deleteLanguage = async (environmentId: string, languageId: string): Promise => { try { diff --git a/packages/lib/membership/service.ts b/packages/lib/membership/service.ts index 3553474e63..c5dcc1ff58 100644 --- a/packages/lib/membership/service.ts +++ b/packages/lib/membership/service.ts @@ -1,5 +1,6 @@ import "server-only"; import { Prisma } from "@prisma/client"; +import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; import { ZOptionalNumber, ZString } from "@formbricks/types/common"; import { DatabaseError, ResourceNotFoundError, UnknownError } from "@formbricks/types/errors"; @@ -16,119 +17,122 @@ import { organizationCache } from "../organization/cache"; import { validateInputs } from "../utils/validate"; import { membershipCache } from "./cache"; -export const getMembersByOrganizationId = async (organizationId: string, page?: number): Promise => - cache( - async () => { - validateInputs([organizationId, ZString], [page, ZOptionalNumber]); +export const getMembersByOrganizationId = reactCache( + (organizationId: string, page?: number): Promise => + cache( + async () => { + validateInputs([organizationId, ZString], [page, ZOptionalNumber]); - try { - const membersData = await prisma.membership.findMany({ - where: { organizationId }, - select: { - user: { - select: { - name: true, - email: true, + try { + const membersData = await prisma.membership.findMany({ + where: { organizationId }, + select: { + user: { + select: { + name: true, + email: true, + }, + }, + userId: true, + accepted: true, + role: true, + }, + take: page ? ITEMS_PER_PAGE : undefined, + skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined, + }); + + const members = membersData.map((member) => { + return { + name: member.user?.name || "", + email: member.user?.email || "", + userId: member.userId, + accepted: member.accepted, + role: member.role, + }; + }); + + return members; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + console.error(error); + throw new DatabaseError(error.message); + } + + throw new UnknownError("Error while fetching members"); + } + }, + [`getMembersByOrganizationId-${organizationId}-${page}`], + { + tags: [membershipCache.tag.byOrganizationId(organizationId)], + } + )() +); + +export const getMembershipByUserIdOrganizationId = reactCache( + (userId: string, organizationId: string): Promise => + cache( + async () => { + validateInputs([userId, ZString], [organizationId, ZString]); + + try { + const membership = await prisma.membership.findUnique({ + where: { + userId_organizationId: { + userId, + organizationId, }, }, - userId: true, - accepted: true, - role: true, - }, - take: page ? ITEMS_PER_PAGE : undefined, - skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined, - }); + }); - const members = membersData.map((member) => { - return { - name: member.user?.name || "", - email: member.user?.email || "", - userId: member.userId, - accepted: member.accepted, - role: member.role, - }; - }); + if (!membership) return null; - return members; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); - throw new DatabaseError(error.message); + return membership; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + console.error(error); + throw new DatabaseError(error.message); + } + + throw new UnknownError("Error while fetching membership"); } - - throw new UnknownError("Error while fetching members"); + }, + [`getMembershipByUserIdOrganizationId-${userId}-${organizationId}`], + { + tags: [membershipCache.tag.byUserId(userId), membershipCache.tag.byOrganizationId(organizationId)], } - }, - [`getMembersByOrganizationId-${organizationId}-${page}`], - { - tags: [membershipCache.tag.byOrganizationId(organizationId)], - } - )(); + )() +); -export const getMembershipByUserIdOrganizationId = async ( - userId: string, - organizationId: string -): Promise => - cache( - async () => { - validateInputs([userId, ZString], [organizationId, ZString]); +export const getMembershipsByUserId = reactCache( + (userId: string, page?: number): Promise => + cache( + async () => { + validateInputs([userId, ZString], [page, ZOptionalNumber]); - try { - const membership = await prisma.membership.findUnique({ - where: { - userId_organizationId: { + try { + const memberships = await prisma.membership.findMany({ + where: { userId, - organizationId, }, - }, - }); + take: page ? ITEMS_PER_PAGE : undefined, + skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined, + }); - if (!membership) return null; + return memberships; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } - return membership; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); - throw new DatabaseError(error.message); + throw error; } - - throw new UnknownError("Error while fetching membership"); + }, + [`getMembershipsByUserId-${userId}-${page}`], + { + tags: [membershipCache.tag.byUserId(userId)], } - }, - [`getMembershipByUserIdOrganizationId-${userId}-${organizationId}`], - { - tags: [membershipCache.tag.byUserId(userId), membershipCache.tag.byOrganizationId(organizationId)], - } - )(); - -export const getMembershipsByUserId = async (userId: string, page?: number): Promise => - cache( - async () => { - validateInputs([userId, ZString], [page, ZOptionalNumber]); - - try { - const memberships = await prisma.membership.findMany({ - where: { - userId, - }, - take: page ? ITEMS_PER_PAGE : undefined, - skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined, - }); - - return memberships; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); - } - - throw error; - } - }, - [`getMembershipsByUserId-${userId}-${page}`], - { - tags: [membershipCache.tag.byUserId(userId)], - } - )(); + )() +); export const createMembership = async ( organizationId: string, diff --git a/packages/lib/organization/service.ts b/packages/lib/organization/service.ts index 93b027ffe1..72da377f94 100644 --- a/packages/lib/organization/service.ts +++ b/packages/lib/organization/service.ts @@ -1,5 +1,6 @@ import "server-only"; import { Prisma } from "@prisma/client"; +import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; import { ZOptionalNumber, ZString } from "@formbricks/types/common"; import { ZId } from "@formbricks/types/environment"; @@ -32,105 +33,111 @@ export const getOrganizationsByUserIdCacheTag = (userId: string) => `users-${use export const getOrganizationByEnvironmentIdCacheTag = (environmentId: string) => `environments-${environmentId}-organization`; -export const getOrganizationsByUserId = (userId: string, page?: number): Promise => - cache( - async () => { - validateInputs([userId, ZString], [page, ZOptionalNumber]); +export const getOrganizationsByUserId = reactCache( + (userId: string, page?: number): Promise => + cache( + async () => { + validateInputs([userId, ZString], [page, ZOptionalNumber]); - try { - const organizations = await prisma.organization.findMany({ - where: { - memberships: { - some: { - userId, + try { + const organizations = await prisma.organization.findMany({ + where: { + memberships: { + some: { + userId, + }, }, }, - }, - select, - take: page ? ITEMS_PER_PAGE : undefined, - skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined, - }); - if (!organizations) { - throw new ResourceNotFoundError("Organizations by UserId", userId); - } - return organizations; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); - } + select, + take: page ? ITEMS_PER_PAGE : undefined, + skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined, + }); + if (!organizations) { + throw new ResourceNotFoundError("Organizations by UserId", userId); + } + return organizations; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } - throw error; + throw error; + } + }, + [`getOrganizationsByUserId-${userId}-${page}`], + { + tags: [organizationCache.tag.byUserId(userId)], } - }, - [`getOrganizationsByUserId-${userId}-${page}`], - { - tags: [organizationCache.tag.byUserId(userId)], - } - )(); + )() +); -export const getOrganizationByEnvironmentId = (environmentId: string): Promise => - cache( - async () => { - validateInputs([environmentId, ZId]); +export const getOrganizationByEnvironmentId = reactCache( + (environmentId: string): Promise => + cache( + async () => { + validateInputs([environmentId, ZId]); - try { - const organization = await prisma.organization.findFirst({ - where: { - products: { - some: { - environments: { - some: { - id: environmentId, + try { + const organization = await prisma.organization.findFirst({ + where: { + products: { + some: { + environments: { + some: { + id: environmentId, + }, }, }, }, }, - }, - select: { ...select, memberships: true }, // include memberships - }); + select: { ...select, memberships: true }, // include memberships + }); - return organization; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); - throw new DatabaseError(error.message); + return organization; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + console.error(error); + throw new DatabaseError(error.message); + } + + throw error; } - - throw error; + }, + [`getOrganizationByEnvironmentId-${environmentId}`], + { + tags: [organizationCache.tag.byEnvironmentId(environmentId)], } - }, - [`getOrganizationByEnvironmentId-${environmentId}`], - { - tags: [organizationCache.tag.byEnvironmentId(environmentId)], - } - )(); + )() +); -export const getOrganization = (organizationId: string): Promise => - cache( - async () => { - validateInputs([organizationId, ZString]); +export const getOrganization = reactCache( + (organizationId: string): Promise => + cache( + async () => { + validateInputs([organizationId, ZString]); - try { - const organization = await prisma.organization.findUnique({ - where: { - id: organizationId, - }, - select, - }); - return organization; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); + try { + const organization = await prisma.organization.findUnique({ + where: { + id: organizationId, + }, + select, + }); + return organization; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + + throw error; } - - throw error; + }, + [`getOrganization-${organizationId}`], + { + tags: [organizationCache.tag.byId(organizationId)], } - }, - [`getOrganization-${organizationId}`], - { - tags: [organizationCache.tag.byId(organizationId)], - } - )(); + )() +); export const createOrganization = async ( organizationInput: TOrganizationCreateInput @@ -271,79 +278,83 @@ export const deleteOrganization = async (organizationId: string): Promise => - cache( - async () => { - validateInputs([organizationId, ZId]); +export const getMonthlyActiveOrganizationPeopleCount = reactCache( + (organizationId: string): Promise => + cache( + async () => { + validateInputs([organizationId, ZId]); - try { - // temporary solution until we have a better way to track active users - return 0; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); + try { + // temporary solution until we have a better way to track active users + return 0; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + + throw error; } - - throw error; + }, + [`getMonthlyActiveOrganizationPeopleCount-${organizationId}`], + { + revalidate: 60 * 60 * 2, // 2 hours } - }, - [`getMonthlyActiveOrganizationPeopleCount-${organizationId}`], - { - revalidate: 60 * 60 * 2, // 2 hours - } - )(); + )() +); -export const getMonthlyOrganizationResponseCount = (organizationId: string): Promise => - cache( - async () => { - validateInputs([organizationId, ZId]); +export const getMonthlyOrganizationResponseCount = reactCache( + (organizationId: string): Promise => + cache( + async () => { + validateInputs([organizationId, ZId]); - try { - // Define the start of the month - // const now = new Date(); - // const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); + try { + // Define the start of the month + // const now = new Date(); + // const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); - const organization = await getOrganization(organizationId); - if (!organization) { - throw new ResourceNotFoundError("Organization", organizationId); + const organization = await getOrganization(organizationId); + if (!organization) { + throw new ResourceNotFoundError("Organization", organizationId); + } + + if (!organization.billing.periodStart) { + throw new Error("Organization billing period start is not set"); + } + + // Get all environment IDs for the organization + const products = await getProducts(organizationId); + const environmentIds = products.flatMap((product) => product.environments.map((env) => env.id)); + + // Use Prisma's aggregate to count responses for all environments + const responseAggregations = await prisma.response.aggregate({ + _count: { + id: true, + }, + where: { + AND: [ + { survey: { environmentId: { in: environmentIds } } }, + { createdAt: { gte: organization.billing.periodStart } }, + ], + }, + }); + + // The result is an aggregation of the total count + return responseAggregations._count.id; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + + throw error; } - - if (!organization.billing.periodStart) { - throw new Error("Organization billing period start is not set"); - } - - // Get all environment IDs for the organization - const products = await getProducts(organizationId); - const environmentIds = products.flatMap((product) => product.environments.map((env) => env.id)); - - // Use Prisma's aggregate to count responses for all environments - const responseAggregations = await prisma.response.aggregate({ - _count: { - id: true, - }, - where: { - AND: [ - { survey: { environmentId: { in: environmentIds } } }, - { createdAt: { gte: organization.billing.periodStart } }, - ], - }, - }); - - // The result is an aggregation of the total count - return responseAggregations._count.id; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); - } - - throw error; + }, + [`getMonthlyOrganizationResponseCount-${organizationId}`], + { + revalidate: 60 * 60 * 2, // 2 hours } - }, - [`getMonthlyOrganizationResponseCount-${organizationId}`], - { - revalidate: 60 * 60 * 2, // 2 hours - } - )(); + )() +); export const subscribeOrganizationMembersToSurveyResponses = async ( surveyId: string, diff --git a/packages/lib/person/service.ts b/packages/lib/person/service.ts index 5d386cb531..0666068944 100644 --- a/packages/lib/person/service.ts +++ b/packages/lib/person/service.ts @@ -1,5 +1,6 @@ import "server-only"; import { Prisma } from "@prisma/client"; +import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; import { ZOptionalNumber, ZString } from "@formbricks/types/common"; import { ZId } from "@formbricks/types/environment"; @@ -51,84 +52,90 @@ export const transformPrismaPerson = (person: TransformPersonInput): TPerson => } as TPerson; }; -export const getPerson = (personId: string): Promise => - cache( - async () => { - validateInputs([personId, ZId]); +export const getPerson = reactCache( + (personId: string): Promise => + cache( + async () => { + validateInputs([personId, ZId]); - try { - return await prisma.person.findUnique({ - where: { - id: personId, - }, - select: selectPerson, - }); - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); + try { + return await prisma.person.findUnique({ + where: { + id: personId, + }, + select: selectPerson, + }); + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + + throw error; } - - throw error; + }, + [`getPerson-${personId}`], + { + tags: [personCache.tag.byId(personId)], } - }, - [`getPerson-${personId}`], - { - tags: [personCache.tag.byId(personId)], - } - )(); + )() +); -export const getPeople = (environmentId: string, page?: number): Promise => - cache( - async () => { - validateInputs([environmentId, ZId], [page, ZOptionalNumber]); +export const getPeople = reactCache( + (environmentId: string, page?: number): Promise => + cache( + async () => { + validateInputs([environmentId, ZId], [page, ZOptionalNumber]); - try { - return await prisma.person.findMany({ - where: { - environmentId: environmentId, - }, - select: selectPerson, - take: page ? ITEMS_PER_PAGE : undefined, - skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined, - }); - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); + try { + return await prisma.person.findMany({ + where: { + environmentId: environmentId, + }, + select: selectPerson, + take: page ? ITEMS_PER_PAGE : undefined, + skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined, + }); + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + + throw error; } - - throw error; + }, + [`getPeople-${environmentId}-${page}`], + { + tags: [personCache.tag.byEnvironmentId(environmentId)], } - }, - [`getPeople-${environmentId}-${page}`], - { - tags: [personCache.tag.byEnvironmentId(environmentId)], - } - )(); + )() +); -export const getPeopleCount = (environmentId: string): Promise => - cache( - async () => { - validateInputs([environmentId, ZId]); +export const getPeopleCount = reactCache( + (environmentId: string): Promise => + cache( + async () => { + validateInputs([environmentId, ZId]); - try { - return await prisma.person.count({ - where: { - environmentId: environmentId, - }, - }); - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); + try { + return await prisma.person.count({ + where: { + environmentId: environmentId, + }, + }); + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + + throw error; } - - throw error; + }, + [`getPeopleCount-${environmentId}`], + { + tags: [personCache.tag.byEnvironmentId(environmentId)], } - }, - [`getPeopleCount-${environmentId}`], - { - tags: [personCache.tag.byEnvironmentId(environmentId)], - } - )(); + )() +); export const createPerson = async (environmentId: string, userId: string): Promise => { validateInputs([environmentId, ZId]); @@ -205,62 +212,66 @@ export const deletePerson = async (personId: string): Promise => } }; -export const getPersonByUserId = (environmentId: string, userId: string): Promise => - cache( - async () => { - validateInputs([environmentId, ZId], [userId, ZString]); +export const getPersonByUserId = reactCache( + (environmentId: string, userId: string): Promise => + cache( + async () => { + validateInputs([environmentId, ZId], [userId, ZString]); - // check if userId exists as a column - const personWithUserId = await prisma.person.findFirst({ - where: { - environmentId, - userId, - }, - select: selectPerson, - }); - - if (personWithUserId) { - return personWithUserId; - } - - return null; - }, - [`getPersonByUserId-${environmentId}-${userId}`], - { - tags: [personCache.tag.byEnvironmentIdAndUserId(environmentId, userId)], - } - )(); - -export const getIsPersonMonthlyActive = (personId: string): Promise => - cache( - async () => { - try { - const latestAction = await prisma.action.findFirst({ + // check if userId exists as a column + const personWithUserId = await prisma.person.findFirst({ where: { - personId, - }, - orderBy: { - createdAt: "desc", - }, - select: { - createdAt: true, + environmentId, + userId, }, + select: selectPerson, }); - if (!latestAction || new Date(latestAction.createdAt).getMonth() !== new Date().getMonth()) { - return false; - } - return true; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); + + if (personWithUserId) { + return personWithUserId; } - throw error; + return null; + }, + [`getPersonByUserId-${environmentId}-${userId}`], + { + tags: [personCache.tag.byEnvironmentIdAndUserId(environmentId, userId)], } - }, - [`getIsPersonMonthlyActive-${personId}`], - { - tags: [activePersonCache.tag.byId(personId)], - revalidate: 60 * 60 * 24, // 24 hours - } - )(); + )() +); + +export const getIsPersonMonthlyActive = reactCache( + (personId: string): Promise => + cache( + async () => { + try { + const latestAction = await prisma.action.findFirst({ + where: { + personId, + }, + orderBy: { + createdAt: "desc", + }, + select: { + createdAt: true, + }, + }); + if (!latestAction || new Date(latestAction.createdAt).getMonth() !== new Date().getMonth()) { + return false; + } + return true; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + + throw error; + } + }, + [`getIsPersonMonthlyActive-${personId}`], + { + tags: [activePersonCache.tag.byId(personId)], + revalidate: 60 * 60 * 24, // 24 hours + } + )() +); diff --git a/packages/lib/product/service.ts b/packages/lib/product/service.ts index 92679006d2..7bc988f087 100644 --- a/packages/lib/product/service.ts +++ b/packages/lib/product/service.ts @@ -1,5 +1,6 @@ import "server-only"; import { Prisma } from "@prisma/client"; +import { cache as reactCache } from "react"; import { z } from "zod"; import { prisma } from "@formbricks/database"; import { ZOptionalNumber, ZString } from "@formbricks/types/common"; @@ -34,68 +35,72 @@ const selectProduct = { logo: true, }; -export const getProducts = (organizationId: string, page?: number): Promise => - cache( - async () => { - validateInputs([organizationId, ZId], [page, ZOptionalNumber]); +export const getProducts = reactCache( + (organizationId: string, page?: number): Promise => + cache( + async () => { + validateInputs([organizationId, ZId], [page, ZOptionalNumber]); - try { - const products = await prisma.product.findMany({ - where: { - organizationId, - }, - select: selectProduct, - take: page ? ITEMS_PER_PAGE : undefined, - skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined, - }); - return products; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); + try { + const products = await prisma.product.findMany({ + where: { + organizationId, + }, + select: selectProduct, + take: page ? ITEMS_PER_PAGE : undefined, + skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined, + }); + return products; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + + throw error; } - - throw error; + }, + [`getProducts-${organizationId}-${page}`], + { + tags: [productCache.tag.byOrganizationId(organizationId)], } - }, - [`getProducts-${organizationId}-${page}`], - { - tags: [productCache.tag.byOrganizationId(organizationId)], - } - )(); + )() +); -export const getProductByEnvironmentId = (environmentId: string): Promise => - cache( - async () => { - validateInputs([environmentId, ZId]); +export const getProductByEnvironmentId = reactCache( + (environmentId: string): Promise => + cache( + async () => { + validateInputs([environmentId, ZId]); - let productPrisma; + let productPrisma; - try { - productPrisma = await prisma.product.findFirst({ - where: { - environments: { - some: { - id: environmentId, + try { + productPrisma = await prisma.product.findFirst({ + where: { + environments: { + some: { + id: environmentId, + }, }, }, - }, - select: selectProduct, - }); + select: selectProduct, + }); - return productPrisma; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); - throw new DatabaseError(error.message); + return productPrisma; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + console.error(error); + throw new DatabaseError(error.message); + } + throw error; } - throw error; + }, + [`getProductByEnvironmentId-${environmentId}`], + { + tags: [productCache.tag.byEnvironmentId(environmentId)], } - }, - [`getProductByEnvironmentId-${environmentId}`], - { - tags: [productCache.tag.byEnvironmentId(environmentId)], - } - )(); + )() +); export const updateProduct = async ( productId: string, @@ -148,31 +153,33 @@ export const updateProduct = async ( } }; -export const getProduct = async (productId: string): Promise => - cache( - async () => { - let productPrisma; - try { - productPrisma = await prisma.product.findUnique({ - where: { - id: productId, - }, - select: selectProduct, - }); +export const getProduct = reactCache( + (productId: string): Promise => + cache( + async () => { + let productPrisma; + try { + productPrisma = await prisma.product.findUnique({ + where: { + id: productId, + }, + select: selectProduct, + }); - return productPrisma; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); + return productPrisma; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + throw error; } - throw error; + }, + [`getProduct-${productId}`], + { + tags: [productCache.tag.byId(productId)], } - }, - [`getProduct-${productId}`], - { - tags: [productCache.tag.byId(productId)], - } - )(); + )() +); export const deleteProduct = async (productId: string): Promise => { try { diff --git a/packages/lib/response/service.ts b/packages/lib/response/service.ts index 00064e4ded..0a3cbad330 100644 --- a/packages/lib/response/service.ts +++ b/packages/lib/response/service.ts @@ -1,5 +1,6 @@ import "server-only"; import { Prisma } from "@prisma/client"; +import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; import { TAttributes } from "@formbricks/types/attributes"; import { ZOptionalNumber, ZString } from "@formbricks/types/common"; @@ -100,92 +101,96 @@ export const responseSelection = { }, }; -export const getResponsesByPersonId = (personId: string, page?: number): Promise => - cache( - async () => { - validateInputs([personId, ZId], [page, ZOptionalNumber]); +export const getResponsesByPersonId = reactCache( + (personId: string, page?: number): Promise => + cache( + async () => { + validateInputs([personId, ZId], [page, ZOptionalNumber]); - try { - const responsePrisma = await prisma.response.findMany({ - where: { - personId, - }, - select: responseSelection, - take: page ? ITEMS_PER_PAGE : undefined, - skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined, - orderBy: { - updatedAt: "asc", - }, - }); + try { + const responsePrisma = await prisma.response.findMany({ + where: { + personId, + }, + select: responseSelection, + take: page ? ITEMS_PER_PAGE : undefined, + skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined, + orderBy: { + updatedAt: "asc", + }, + }); - if (!responsePrisma) { - throw new ResourceNotFoundError("Response from PersonId", personId); + if (!responsePrisma) { + throw new ResourceNotFoundError("Response from PersonId", personId); + } + + let responses: TResponse[] = []; + + await Promise.all( + responsePrisma.map(async (response) => { + const responseNotes = await getResponseNotes(response.id); + responses.push({ + ...response, + notes: responseNotes, + tags: response.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag), + }); + }) + ); + + return responses; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + + throw error; } - - let responses: TResponse[] = []; - - await Promise.all( - responsePrisma.map(async (response) => { - const responseNotes = await getResponseNotes(response.id); - responses.push({ - ...response, - notes: responseNotes, - tags: response.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag), - }); - }) - ); - - return responses; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); - } - - throw error; + }, + [`getResponsesByPersonId-${personId}-${page}`], + { + tags: [responseCache.tag.byPersonId(personId)], } - }, - [`getResponsesByPersonId-${personId}-${page}`], - { - tags: [responseCache.tag.byPersonId(personId)], - } - )(); + )() +); -export const getResponseBySingleUseId = (surveyId: string, singleUseId: string): Promise => - cache( - async () => { - validateInputs([surveyId, ZId], [singleUseId, ZString]); +export const getResponseBySingleUseId = reactCache( + (surveyId: string, singleUseId: string): Promise => + cache( + async () => { + validateInputs([surveyId, ZId], [singleUseId, ZString]); - try { - const responsePrisma = await prisma.response.findUnique({ - where: { - surveyId_singleUseId: { surveyId, singleUseId }, - }, - select: responseSelection, - }); + try { + const responsePrisma = await prisma.response.findUnique({ + where: { + surveyId_singleUseId: { surveyId, singleUseId }, + }, + select: responseSelection, + }); - if (!responsePrisma) { - return null; + if (!responsePrisma) { + return null; + } + + const response: TResponse = { + ...responsePrisma, + tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag), + }; + + return response; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + + throw error; } - - const response: TResponse = { - ...responsePrisma, - tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag), - }; - - return response; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); - } - - throw error; + }, + [`getResponseBySingleUseId-${surveyId}-${singleUseId}`], + { + tags: [responseCache.tag.bySingleUseId(surveyId, singleUseId)], } - }, - [`getResponseBySingleUseId-${surveyId}-${singleUseId}`], - { - tags: [responseCache.tag.bySingleUseId(surveyId, singleUseId)], - } - )(); + )() +); export const createResponse = async (responseInput: TResponseInput): Promise => { validateInputs([responseInput, ZResponseInput]); @@ -378,44 +383,46 @@ export const createResponseLegacy = async (responseInput: TResponseLegacyInput): } }; -export const getResponse = (responseId: string): Promise => - cache( - async () => { - validateInputs([responseId, ZId]); +export const getResponse = reactCache( + (responseId: string): Promise => + cache( + async () => { + validateInputs([responseId, ZId]); - try { - const responsePrisma = await prisma.response.findUnique({ - where: { - id: responseId, - }, - select: responseSelection, - }); + try { + const responsePrisma = await prisma.response.findUnique({ + where: { + id: responseId, + }, + select: responseSelection, + }); - if (!responsePrisma) { - return null; + if (!responsePrisma) { + return null; + } + + const response: TResponse = { + ...responsePrisma, + tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag), + }; + + return response; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + + throw error; } - - const response: TResponse = { - ...responsePrisma, - tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag), - }; - - return response; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); - } - - throw error; + }, + [`getResponse-${responseId}`], + { + tags: [responseCache.tag.byId(responseId), responseNoteCache.tag.byResponseId(responseId)], } - }, - [`getResponse-${responseId}`], - { - tags: [responseCache.tag.byId(responseId), responseNoteCache.tag.byResponseId(responseId)], - } - )(); + )() +); -export const getResponseFilteringValues = async (surveyId: string) => +export const getResponseFilteringValues = reactCache((surveyId: string) => cache( async () => { validateInputs([surveyId, ZId]); @@ -454,111 +461,113 @@ export const getResponseFilteringValues = async (surveyId: string) => { tags: [responseCache.tag.bySurveyId(surveyId)], } - )(); + )() +); -export const getResponses = ( - surveyId: string, - limit?: number, - offset?: number, - filterCriteria?: TResponseFilterCriteria -): Promise => - cache( - async () => { - validateInputs( - [surveyId, ZId], - [limit, ZOptionalNumber], - [offset, ZOptionalNumber], - [filterCriteria, ZResponseFilterCriteria.optional()] - ); +export const getResponses = reactCache( + ( + surveyId: string, + limit?: number, + offset?: number, + filterCriteria?: TResponseFilterCriteria + ): Promise => + cache( + async () => { + validateInputs( + [surveyId, ZId], + [limit, ZOptionalNumber], + [offset, ZOptionalNumber], + [filterCriteria, ZResponseFilterCriteria.optional()] + ); - limit = limit ?? RESPONSES_PER_PAGE; - try { - const responses = await prisma.response.findMany({ - where: { - surveyId, - ...buildWhereClause(filterCriteria), - }, - select: responseSelection, - orderBy: [ - { - createdAt: "desc", + limit = limit ?? RESPONSES_PER_PAGE; + try { + const responses = await prisma.response.findMany({ + where: { + surveyId, + ...buildWhereClause(filterCriteria), }, - ], - take: limit ? limit : undefined, - skip: offset ? offset : undefined, - }); + select: responseSelection, + orderBy: [ + { + createdAt: "desc", + }, + ], + take: limit ? limit : undefined, + skip: offset ? offset : undefined, + }); - const transformedResponses: TResponse[] = await Promise.all( - responses.map((responsePrisma) => { - return { - ...responsePrisma, - tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag), - }; - }) - ); + const transformedResponses: TResponse[] = await Promise.all( + responses.map((responsePrisma) => { + return { + ...responsePrisma, + tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag), + }; + }) + ); - return transformedResponses; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); + return transformedResponses; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + + throw error; } - - throw error; + }, + [`getResponses-${surveyId}-${limit}-${offset}-${JSON.stringify(filterCriteria)}`], + { + tags: [responseCache.tag.bySurveyId(surveyId)], } - }, - [`getResponses-${surveyId}-${limit}-${offset}-${JSON.stringify(filterCriteria)}`], - { - tags: [responseCache.tag.bySurveyId(surveyId)], - } - )(); + )() +); -export const getSurveySummary = ( - surveyId: string, - filterCriteria?: TResponseFilterCriteria -): Promise => - cache( - async () => { - validateInputs([surveyId, ZId], [filterCriteria, ZResponseFilterCriteria.optional()]); +export const getSurveySummary = reactCache( + (surveyId: string, filterCriteria?: TResponseFilterCriteria): Promise => + cache( + async () => { + validateInputs([surveyId, ZId], [filterCriteria, ZResponseFilterCriteria.optional()]); - try { - const survey = await getSurvey(surveyId); - if (!survey) { - throw new ResourceNotFoundError("Survey", surveyId); + try { + const survey = await getSurvey(surveyId); + if (!survey) { + throw new ResourceNotFoundError("Survey", surveyId); + } + + const batchSize = 3000; + const responseCount = await getResponseCountBySurveyId(surveyId, filterCriteria); + const pages = Math.ceil(responseCount / batchSize); + + const responsesArray = await Promise.all( + Array.from({ length: pages }, (_, i) => { + return getResponses(surveyId, batchSize, i * batchSize, filterCriteria); + }) + ); + const responses = responsesArray.flat(); + + const displayCount = await getDisplayCountBySurveyId(surveyId, { + createdAt: filterCriteria?.createdAt, + }); + + const dropOff = getSurveySummaryDropOff(survey, responses, displayCount); + const meta = getSurveySummaryMeta(responses, displayCount); + const questionWiseSummary = getQuestionWiseSummary(survey, responses, dropOff); + + return { meta, dropOff, summary: questionWiseSummary }; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + + throw error; } - - const batchSize = 3000; - const responseCount = await getResponseCountBySurveyId(surveyId, filterCriteria); - const pages = Math.ceil(responseCount / batchSize); - - const responsesArray = await Promise.all( - Array.from({ length: pages }, (_, i) => { - return getResponses(surveyId, batchSize, i * batchSize, filterCriteria); - }) - ); - const responses = responsesArray.flat(); - - const displayCount = await getDisplayCountBySurveyId(surveyId, { - createdAt: filterCriteria?.createdAt, - }); - - const dropOff = getSurveySummaryDropOff(survey, responses, displayCount); - const meta = getSurveySummaryMeta(responses, displayCount); - const questionWiseSummary = getQuestionWiseSummary(survey, responses, dropOff); - - return { meta, dropOff, summary: questionWiseSummary }; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); - } - - throw error; + }, + [`getSurveySummary-${surveyId}-${JSON.stringify(filterCriteria)}`], + { + tags: [responseCache.tag.bySurveyId(surveyId), displayCache.tag.bySurveyId(surveyId)], } - }, - [`getSurveySummary-${surveyId}-${JSON.stringify(filterCriteria)}`], - { - tags: [responseCache.tag.bySurveyId(surveyId), displayCache.tag.bySurveyId(surveyId)], - } - )(); + )() +); export const getResponseDownloadUrl = async ( surveyId: string, @@ -632,55 +641,53 @@ export const getResponseDownloadUrl = async ( } }; -export const getResponsesByEnvironmentId = ( - environmentId: string, - limit?: number, - offset?: number -): Promise => - cache( - async () => { - validateInputs([environmentId, ZId], [limit, ZOptionalNumber], [offset, ZOptionalNumber]); +export const getResponsesByEnvironmentId = reactCache( + (environmentId: string, limit?: number, offset?: number): Promise => + cache( + async () => { + validateInputs([environmentId, ZId], [limit, ZOptionalNumber], [offset, ZOptionalNumber]); - try { - const responses = await prisma.response.findMany({ - where: { - survey: { - environmentId, + try { + const responses = await prisma.response.findMany({ + where: { + survey: { + environmentId, + }, }, - }, - select: responseSelection, - orderBy: [ - { - createdAt: "desc", - }, - ], - take: limit ? limit : undefined, - skip: offset ? offset : undefined, - }); + select: responseSelection, + orderBy: [ + { + createdAt: "desc", + }, + ], + take: limit ? limit : undefined, + skip: offset ? offset : undefined, + }); - const transformedResponses: TResponse[] = await Promise.all( - responses.map(async (responsePrisma) => { - return { - ...responsePrisma, - tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag), - }; - }) - ); + const transformedResponses: TResponse[] = await Promise.all( + responses.map(async (responsePrisma) => { + return { + ...responsePrisma, + tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag), + }; + }) + ); - return transformedResponses; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); + return transformedResponses; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + + throw error; } - - throw error; + }, + [`getResponsesByEnvironmentId-${environmentId}-${limit}-${offset}`], + { + tags: [responseCache.tag.byEnvironmentId(environmentId)], } - }, - [`getResponsesByEnvironmentId-${environmentId}-${limit}-${offset}`], - { - tags: [responseCache.tag.byEnvironmentId(environmentId)], - } - )(); + )() +); export const updateResponse = async ( responseId: string, @@ -794,32 +801,31 @@ export const deleteResponse = async (responseId: string): Promise => } }; -export const getResponseCountBySurveyId = ( - surveyId: string, - filterCriteria?: TResponseFilterCriteria -): Promise => - cache( - async () => { - validateInputs([surveyId, ZId], [filterCriteria, ZResponseFilterCriteria.optional()]); +export const getResponseCountBySurveyId = reactCache( + (surveyId: string, filterCriteria?: TResponseFilterCriteria): Promise => + cache( + async () => { + validateInputs([surveyId, ZId], [filterCriteria, ZResponseFilterCriteria.optional()]); - try { - const responseCount = await prisma.response.count({ - where: { - surveyId: surveyId, - ...buildWhereClause(filterCriteria), - }, - }); - return responseCount; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); + try { + const responseCount = await prisma.response.count({ + where: { + surveyId: surveyId, + ...buildWhereClause(filterCriteria), + }, + }); + return responseCount; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + + throw error; } - - throw error; + }, + [`getResponseCountBySurveyId-${surveyId}-${JSON.stringify(filterCriteria)}`], + { + tags: [responseCache.tag.bySurveyId(surveyId)], } - }, - [`getResponseCountBySurveyId-${surveyId}-${JSON.stringify(filterCriteria)}`], - { - tags: [responseCache.tag.bySurveyId(surveyId)], - } - )(); + )() +); diff --git a/packages/lib/responseNote/service.ts b/packages/lib/responseNote/service.ts index 8e9429fffc..cf034c5be5 100644 --- a/packages/lib/responseNote/service.ts +++ b/packages/lib/responseNote/service.ts @@ -1,5 +1,6 @@ import "server-only"; import { Prisma } from "@prisma/client"; +import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; import { ZString } from "@formbricks/types/common"; import { ZId } from "@formbricks/types/environment"; @@ -68,62 +69,66 @@ export const createResponseNote = async ( } }; -export const getResponseNote = (responseNoteId: string): Promise => - cache( - async () => { - try { - const responseNote = await prisma.responseNote.findUnique({ - where: { - id: responseNoteId, - }, - select: responseNoteSelect, - }); - return responseNote; - } catch (error) { - console.error(error); - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); - } +export const getResponseNote = reactCache( + (responseNoteId: string): Promise => + cache( + async () => { + try { + const responseNote = await prisma.responseNote.findUnique({ + where: { + id: responseNoteId, + }, + select: responseNoteSelect, + }); + return responseNote; + } catch (error) { + console.error(error); + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } - throw error; + throw error; + } + }, + [`getResponseNote-${responseNoteId}`], + { + tags: [responseNoteCache.tag.byId(responseNoteId)], } - }, - [`getResponseNote-${responseNoteId}`], - { - tags: [responseNoteCache.tag.byId(responseNoteId)], - } - )(); + )() +); -export const getResponseNotes = (responseId: string): Promise => - cache( - async () => { - try { - validateInputs([responseId, ZId]); +export const getResponseNotes = reactCache( + (responseId: string): Promise => + cache( + async () => { + try { + validateInputs([responseId, ZId]); - const responseNotes = await prisma.responseNote.findMany({ - where: { - responseId, - }, - select: responseNoteSelect, - }); - if (!responseNotes) { - throw new ResourceNotFoundError("Response Notes by ResponseId", responseId); + const responseNotes = await prisma.responseNote.findMany({ + where: { + responseId, + }, + select: responseNoteSelect, + }); + if (!responseNotes) { + throw new ResourceNotFoundError("Response Notes by ResponseId", responseId); + } + return responseNotes; + } catch (error) { + console.error(error); + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + + throw error; } - return responseNotes; - } catch (error) { - console.error(error); - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); - } - - throw error; + }, + [`getResponseNotes-${responseId}`], + { + tags: [responseNoteCache.tag.byResponseId(responseId)], } - }, - [`getResponseNotes-${responseId}`], - { - tags: [responseNoteCache.tag.byResponseId(responseId)], - } - )(); + )() +); export const updateResponseNote = async (responseNoteId: string, text: string): Promise => { validateInputs([responseNoteId, ZString], [text, ZString]); diff --git a/packages/lib/segment/service.ts b/packages/lib/segment/service.ts index e1025fc6a0..77a7478c49 100644 --- a/packages/lib/segment/service.ts +++ b/packages/lib/segment/service.ts @@ -1,4 +1,5 @@ import { Prisma } from "@prisma/client"; +import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; import { ZString } from "@formbricks/types/common"; import { ZId } from "@formbricks/types/environment"; @@ -116,67 +117,71 @@ export const createSegment = async (segmentCreateInput: TSegmentCreateInput): Pr } }; -export const getSegments = (environmentId: string): Promise => - cache( - async () => { - validateInputs([environmentId, ZId]); - try { - const segments = await prisma.segment.findMany({ - where: { - environmentId, - }, - select: selectSegment, - }); +export const getSegments = reactCache( + (environmentId: string): Promise => + cache( + async () => { + validateInputs([environmentId, ZId]); + try { + const segments = await prisma.segment.findMany({ + where: { + environmentId, + }, + select: selectSegment, + }); - if (!segments) { - return []; + if (!segments) { + return []; + } + + return segments.map((segment) => transformPrismaSegment(segment)); + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + + throw error; } - - return segments.map((segment) => transformPrismaSegment(segment)); - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); - } - - throw error; + }, + [`getSegments-${environmentId}`], + { + tags: [segmentCache.tag.byEnvironmentId(environmentId)], } - }, - [`getSegments-${environmentId}`], - { - tags: [segmentCache.tag.byEnvironmentId(environmentId)], - } - )(); + )() +); -export const getSegment = (segmentId: string): Promise => - cache( - async () => { - validateInputs([segmentId, ZId]); - try { - const segment = await prisma.segment.findUnique({ - where: { - id: segmentId, - }, - select: selectSegment, - }); +export const getSegment = reactCache( + (segmentId: string): Promise => + cache( + async () => { + validateInputs([segmentId, ZId]); + try { + const segment = await prisma.segment.findUnique({ + where: { + id: segmentId, + }, + select: selectSegment, + }); - if (!segment) { - throw new ResourceNotFoundError("segment", segmentId); + if (!segment) { + throw new ResourceNotFoundError("segment", segmentId); + } + + return transformPrismaSegment(segment); + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + + throw error; } - - return transformPrismaSegment(segment); - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); - } - - throw error; + }, + [`getSegment-${segmentId}`], + { + tags: [segmentCache.tag.byId(segmentId)], } - }, - [`getSegment-${segmentId}`], - { - tags: [segmentCache.tag.byId(segmentId)], - } - )(); + )() +); export const updateSegment = async (segmentId: string, data: TSegmentUpdateInput): Promise => { validateInputs([segmentId, ZId], [data, ZSegmentUpdateInput]); @@ -330,43 +335,45 @@ export const cloneSegment = async (segmentId: string, surveyId: string): Promise } }; -export const getSegmentsByAttributeClassName = (environmentId: string, attributeClassName: string) => - cache( - async () => { - validateInputs([environmentId, ZId], [attributeClassName, ZString]); +export const getSegmentsByAttributeClassName = reactCache( + (environmentId: string, attributeClassName: string) => + cache( + async () => { + validateInputs([environmentId, ZId], [attributeClassName, ZString]); - try { - const segments = await prisma.segment.findMany({ - where: { - environmentId, - }, - select: selectSegment, - }); + try { + const segments = await prisma.segment.findMany({ + where: { + environmentId, + }, + select: selectSegment, + }); - // search for attributeClassName in the filters - const clonedSegments = structuredClone(segments); + // search for attributeClassName in the filters + const clonedSegments = structuredClone(segments); - const filteredSegments = clonedSegments.filter((segment) => { - return searchForAttributeClassNameInSegment(segment.filters, attributeClassName); - }); + const filteredSegments = clonedSegments.filter((segment) => { + return searchForAttributeClassNameInSegment(segment.filters, attributeClassName); + }); - return filteredSegments; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); + return filteredSegments; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + + throw error; } - - throw error; + }, + [`getSegmentsByAttributeClassName-${environmentId}-${attributeClassName}`], + { + tags: [ + segmentCache.tag.byEnvironmentId(environmentId), + segmentCache.tag.byAttributeClassName(attributeClassName), + ], } - }, - [`getSegmentsByAttributeClassName-${environmentId}-${attributeClassName}`], - { - tags: [ - segmentCache.tag.byEnvironmentId(environmentId), - segmentCache.tag.byAttributeClassName(attributeClassName), - ], - } - )(); + )() +); export const resetSegmentInSurvey = async (surveyId: string): Promise => { validateInputs([surveyId, ZId]); diff --git a/packages/lib/shortUrl/cache.ts b/packages/lib/shortUrl/cache.ts new file mode 100644 index 0000000000..a29de97706 --- /dev/null +++ b/packages/lib/shortUrl/cache.ts @@ -0,0 +1,26 @@ +import { revalidateTag } from "next/cache"; + +interface RevalidateProps { + id?: string; + url?: string; +} + +export const shortUrlCache = { + tag: { + byId(id: string) { + return `shortUrls-${id}`; + }, + byUrl(url: string) { + return `shortUrls-byUrl-${url}`; + }, + }, + revalidate({ id, url }: RevalidateProps): void { + if (id) { + revalidateTag(this.tag.byId(id)); + } + + if (url) { + revalidateTag(this.tag.byUrl(url)); + } + }, +}; diff --git a/packages/lib/shortUrl/service.ts b/packages/lib/shortUrl/service.ts index 10c0b3738e..498d062cc5 100644 --- a/packages/lib/shortUrl/service.ts +++ b/packages/lib/shortUrl/service.ts @@ -1,10 +1,13 @@ import { Prisma } from "@prisma/client"; import { customAlphabet } from "nanoid"; -import z from "zod"; +import { cache as reactCache } from "react"; +import { z } from "zod"; import { prisma } from "@formbricks/database"; import { DatabaseError } from "@formbricks/types/errors"; import { TShortUrl, ZShortUrlId } from "@formbricks/types/shortUrl"; +import { cache } from "../cache"; import { validateInputs } from "../utils/validate"; +import { shortUrlCache } from "./cache"; // Create the short url and return it export const createShortUrl = async (url: string): Promise => { @@ -21,12 +24,19 @@ export const createShortUrl = async (url: string): Promise => { // If an entry with the provided fullUrl does not exist, create a new one. const id = customAlphabet("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", 10)(); - return await prisma.shortUrl.create({ + const shortUrl = await prisma.shortUrl.create({ data: { id, url, }, }); + + shortUrlCache.revalidate({ + id: shortUrl.id, + url: shortUrl.url, + }); + + return shortUrl; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { throw new DatabaseError(error.message); @@ -37,36 +47,54 @@ export const createShortUrl = async (url: string): Promise => { }; // Get the full url from short url and return it -export const getShortUrl = async (id: string): Promise => { - validateInputs([id, ZShortUrlId]); - try { - return await prisma.shortUrl.findUnique({ - where: { - id, +export const getShortUrl = reactCache( + (id: string): Promise => + cache( + async () => { + validateInputs([id, ZShortUrlId]); + try { + return await prisma.shortUrl.findUnique({ + where: { + id, + }, + }); + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + + throw error; + } }, - }); - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); - } + [`getShortUrl-${id}`], + { + tags: [shortUrlCache.tag.byId(id)], + } + )() +); - throw error; - } -}; +export const getShortUrlByUrl = reactCache( + (url: string): Promise => + cache( + async () => { + validateInputs([url, z.string().url()]); + try { + return await prisma.shortUrl.findUnique({ + where: { + url, + }, + }); + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } -export const getShortUrlByUrl = async (url: string): Promise => { - validateInputs([url, z.string().url()]); - try { - return await prisma.shortUrl.findUnique({ - where: { - url, + throw error; + } }, - }); - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); - } - - throw error; - } -}; + [`getShortUrlByUrl-${url}`], + { + tags: [shortUrlCache.tag.byUrl(url)], + } + )() +); diff --git a/packages/lib/survey/cache.ts b/packages/lib/survey/cache.ts index 6afc3a52bd..0c5e065f73 100644 --- a/packages/lib/survey/cache.ts +++ b/packages/lib/survey/cache.ts @@ -6,6 +6,7 @@ interface RevalidateProps { actionClassId?: string; environmentId?: string; segmentId?: string; + resultShareKey?: string; } export const surveyCache = { @@ -25,8 +26,18 @@ export const surveyCache = { bySegmentId(segmentId: string) { return `segments-${segmentId}-surveys`; }, + byResultShareKey(resultShareKey: string) { + return `surveys-resultShare-${resultShareKey}`; + }, }, - revalidate({ id, attributeClassId, actionClassId, environmentId, segmentId }: RevalidateProps): void { + revalidate({ + id, + attributeClassId, + actionClassId, + environmentId, + segmentId, + resultShareKey, + }: RevalidateProps): void { if (id) { revalidateTag(this.tag.byId(id)); } @@ -44,6 +55,10 @@ export const surveyCache = { } if (segmentId) { + revalidateTag(this.tag.byResultShareKey(resultShareKey)); + } + + if (resultShareKey) { revalidateTag(this.tag.bySegmentId(segmentId)); } }, diff --git a/packages/lib/survey/service.ts b/packages/lib/survey/service.ts index d7dec737d5..897454a9ad 100644 --- a/packages/lib/survey/service.ts +++ b/packages/lib/survey/service.ts @@ -1,5 +1,6 @@ import "server-only"; import { Prisma } from "@prisma/client"; +import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; import { TActionClass } from "@formbricks/types/actionClasses"; import { ZOptionalNumber } from "@formbricks/types/common"; @@ -182,128 +183,134 @@ const handleTriggerUpdates = ( return triggersUpdate; }; -export const getSurvey = (surveyId: string): Promise => - cache( - async () => { - validateInputs([surveyId, ZId]); +export const getSurvey = reactCache( + (surveyId: string): Promise => + cache( + async () => { + validateInputs([surveyId, ZId]); - let surveyPrisma; - try { - surveyPrisma = await prisma.survey.findUnique({ - where: { - id: surveyId, - }, - select: selectSurvey, - }); - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); - throw new DatabaseError(error.message); + let surveyPrisma; + try { + surveyPrisma = await prisma.survey.findUnique({ + where: { + id: surveyId, + }, + select: selectSurvey, + }); + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + console.error(error); + throw new DatabaseError(error.message); + } + throw error; } - throw error; + + if (!surveyPrisma) { + return null; + } + + return transformPrismaSurvey(surveyPrisma); + }, + [`getSurvey-${surveyId}`], + { + tags: [surveyCache.tag.byId(surveyId)], } + )() +); - if (!surveyPrisma) { - return null; - } +export const getSurveysByActionClassId = reactCache( + (actionClassId: string, page?: number): Promise => + cache( + async () => { + validateInputs([actionClassId, ZId], [page, ZOptionalNumber]); - return transformPrismaSurvey(surveyPrisma); - }, - [`getSurvey-${surveyId}`], - { - tags: [surveyCache.tag.byId(surveyId)], - } - )(); - -export const getSurveysByActionClassId = (actionClassId: string, page?: number): Promise => - cache( - async () => { - validateInputs([actionClassId, ZId], [page, ZOptionalNumber]); - - let surveysPrisma; - try { - surveysPrisma = await prisma.survey.findMany({ - where: { - triggers: { - some: { - actionClass: { - id: actionClassId, + let surveysPrisma; + try { + surveysPrisma = await prisma.survey.findMany({ + where: { + triggers: { + some: { + actionClass: { + id: actionClassId, + }, }, }, }, - }, - select: selectSurvey, - take: page ? ITEMS_PER_PAGE : undefined, - skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined, - }); - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); - throw new DatabaseError(error.message); + select: selectSurvey, + take: page ? ITEMS_PER_PAGE : undefined, + skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined, + }); + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + console.error(error); + throw new DatabaseError(error.message); + } + + throw error; } - throw error; - } + const surveys: TSurvey[] = []; - const surveys: TSurvey[] = []; - - for (const surveyPrisma of surveysPrisma) { - const transformedSurvey = transformPrismaSurvey(surveyPrisma); - surveys.push(transformedSurvey); - } - - return surveys; - }, - [`getSurveysByActionClassId-${actionClassId}-${page}`], - { - tags: [surveyCache.tag.byActionClassId(actionClassId)], - } - )(); - -export const getSurveys = ( - environmentId: string, - limit?: number, - offset?: number, - filterCriteria?: TSurveyFilterCriteria -): Promise => - cache( - async () => { - validateInputs([environmentId, ZId], [limit, ZOptionalNumber], [offset, ZOptionalNumber]); - let surveysPrisma; - - try { - surveysPrisma = await prisma.survey.findMany({ - where: { - environmentId, - ...buildWhereClause(filterCriteria), - }, - select: selectSurvey, - orderBy: buildOrderByClause(filterCriteria?.sortBy), - take: limit ? limit : undefined, - skip: offset ? offset : undefined, - }); - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); - throw new DatabaseError(error.message); + for (const surveyPrisma of surveysPrisma) { + const transformedSurvey = transformPrismaSurvey(surveyPrisma); + surveys.push(transformedSurvey); } - throw error; + return surveys; + }, + [`getSurveysByActionClassId-${actionClassId}-${page}`], + { + tags: [surveyCache.tag.byActionClassId(actionClassId)], } + )() +); - const surveys: TSurvey[] = []; +export const getSurveys = reactCache( + ( + environmentId: string, + limit?: number, + offset?: number, + filterCriteria?: TSurveyFilterCriteria + ): Promise => + cache( + async () => { + validateInputs([environmentId, ZId], [limit, ZOptionalNumber], [offset, ZOptionalNumber]); + let surveysPrisma; - for (const surveyPrisma of surveysPrisma) { - const transformedSurvey = transformPrismaSurvey(surveyPrisma); - surveys.push(transformedSurvey); + try { + surveysPrisma = await prisma.survey.findMany({ + where: { + environmentId, + ...buildWhereClause(filterCriteria), + }, + select: selectSurvey, + orderBy: buildOrderByClause(filterCriteria?.sortBy), + take: limit ? limit : undefined, + skip: offset ? offset : undefined, + }); + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + console.error(error); + throw new DatabaseError(error.message); + } + + throw error; + } + + const surveys: TSurvey[] = []; + + for (const surveyPrisma of surveysPrisma) { + const transformedSurvey = transformPrismaSurvey(surveyPrisma); + surveys.push(transformedSurvey); + } + return surveys; + }, + [`getSurveys-${environmentId}-${limit}-${offset}-${JSON.stringify(filterCriteria)}`], + { + tags: [surveyCache.tag.byEnvironmentId(environmentId)], } - return surveys; - }, - [`getSurveys-${environmentId}-${limit}-${offset}-${JSON.stringify(filterCriteria)}`], - { - tags: [surveyCache.tag.byEnvironmentId(environmentId)], - } - )(); + )() +); export const transformToLegacySurvey = async ( survey: TSurvey, @@ -322,32 +329,34 @@ export const transformToLegacySurvey = async ( return transformedSurvey; }; -export const getSurveyCount = async (environmentId: string): Promise => - cache( - async () => { - validateInputs([environmentId, ZId]); - try { - const surveyCount = await prisma.survey.count({ - where: { - environmentId: environmentId, - }, - }); +export const getSurveyCount = reactCache( + (environmentId: string): Promise => + cache( + async () => { + validateInputs([environmentId, ZId]); + try { + const surveyCount = await prisma.survey.count({ + where: { + environmentId: environmentId, + }, + }); - return surveyCount; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); - throw new DatabaseError(error.message); + return surveyCount; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + console.error(error); + throw new DatabaseError(error.message); + } + + throw error; } - - throw error; + }, + [`getSurveyCount-${environmentId}`], + { + tags: [surveyCache.tag.byEnvironmentId(environmentId)], } - }, - [`getSurveyCount-${environmentId}`], - { - tags: [surveyCache.tag.byEnvironmentId(environmentId)], - } - )(); + )() +); export const updateSurvey = async (updatedSurvey: TSurvey): Promise => { validateInputs([updatedSurvey, ZSurvey]); @@ -506,6 +515,7 @@ export const updateSurvey = async (updatedSurvey: TSurvey): Promise => id: modifiedSurvey.id, environmentId: modifiedSurvey.environmentId, segmentId: modifiedSurvey.segment?.id, + resultShareKey: modifiedSurvey.resultShareKey ?? undefined, }); return modifiedSurvey; @@ -552,6 +562,7 @@ export const deleteSurvey = async (surveyId: string) => { surveyCache.revalidate({ id: deletedSurvey.id, environmentId: deletedSurvey.environmentId, + resultShareKey: deletedSurvey.resultShareKey ?? undefined, }); if (deletedSurvey.segment?.id) { @@ -665,6 +676,7 @@ export const createSurvey = async (environmentId: string, surveyBody: TSurveyInp surveyCache.revalidate({ id: survey.id, environmentId: survey.environmentId, + resultShareKey: survey.resultShareKey ?? undefined, }); if (createdBy) { @@ -796,6 +808,7 @@ export const duplicateSurvey = async (environmentId: string, surveyId: string, u surveyCache.revalidate({ id: newSurvey.id, environmentId: newSurvey.environmentId, + resultShareKey: newSurvey.resultShareKey ?? undefined, }); existingSurvey.triggers.forEach((trigger) => { @@ -815,226 +828,237 @@ export const duplicateSurvey = async (environmentId: string, surveyId: string, u } }; -export const getSyncSurveys = ( - environmentId: string, - personId: string, - deviceType: "phone" | "desktop" = "desktop", - options?: { - version?: string; - } -): Promise => - cache( - async () => { - validateInputs([environmentId, ZId]); - try { - const product = await getProductByEnvironmentId(environmentId); +export const getSyncSurveys = reactCache( + ( + environmentId: string, + personId: string, + deviceType: "phone" | "desktop" = "desktop", + options?: { + version?: string; + } + ): Promise => + cache( + async () => { + validateInputs([environmentId, ZId]); + try { + const product = await getProductByEnvironmentId(environmentId); - if (!product) { - throw new Error("Product not found"); - } + if (!product) { + throw new Error("Product not found"); + } - const person = personId === "legacy" ? ({ id: "legacy" } as TPerson) : await getPerson(personId); + const person = personId === "legacy" ? ({ id: "legacy" } as TPerson) : await getPerson(personId); - if (!person) { - throw new Error("Person not found"); - } + if (!person) { + throw new Error("Person not found"); + } - let surveys: TSurvey[] | TLegacySurvey[] = await getSurveys(environmentId); + let surveys: TSurvey[] | TLegacySurvey[] = await getSurveys(environmentId); - // filtered surveys for running and web - surveys = surveys.filter((survey) => survey.status === "inProgress" && survey.type === "app"); + // filtered surveys for running and web + surveys = surveys.filter((survey) => survey.status === "inProgress" && survey.type === "app"); - // if no surveys are left, return an empty array - if (surveys.length === 0) { - return []; - } + // if no surveys are left, return an empty array + if (surveys.length === 0) { + return []; + } - const displays = await getDisplaysByPersonId(person.id); + const displays = await getDisplaysByPersonId(person.id); - // filter surveys that meet the displayOption criteria - surveys = surveys.filter((survey) => { - switch (survey.displayOption) { - case "respondMultiple": + // filter surveys that meet the displayOption criteria + surveys = surveys.filter((survey) => { + switch (survey.displayOption) { + case "respondMultiple": + return true; + case "displayOnce": + return displays.filter((display) => display.surveyId === survey.id).length === 0; + case "displayMultiple": + return ( + displays + .filter((display) => display.surveyId === survey.id) + .filter((display) => display.responseId).length === 0 + ); + case "displaySome": + if (survey.displayLimit === null) { + return true; + } + + if ( + displays + .filter((display) => display.surveyId === survey.id) + .some((display) => display.responseId) + ) { + return false; + } + + return ( + displays.filter((display) => display.surveyId === survey.id).length < survey.displayLimit + ); + default: + throw Error("Invalid displayOption"); + } + }); + + const latestDisplay = displays[0]; + + // filter surveys that meet the recontactDays criteria + surveys = surveys.filter((survey) => { + if (!latestDisplay) { return true; - case "displayOnce": - return displays.filter((display) => display.surveyId === survey.id).length === 0; - case "displayMultiple": - return ( - displays - .filter((display) => display.surveyId === survey.id) - .filter((display) => display.responseId).length === 0 - ); - case "displaySome": - if (survey.displayLimit === null) { + } else if (survey.recontactDays !== null) { + const lastDisplaySurvey = displays.filter((display) => display.surveyId === survey.id)[0]; + if (!lastDisplaySurvey) { return true; } - - if ( - displays - .filter((display) => display.surveyId === survey.id) - .some((display) => display.responseId) - ) { - return false; - } - - return ( - displays.filter((display) => display.surveyId === survey.id).length < survey.displayLimit - ); - default: - throw Error("Invalid displayOption"); - } - }); - - const latestDisplay = displays[0]; - - // filter surveys that meet the recontactDays criteria - surveys = surveys.filter((survey) => { - if (!latestDisplay) { - return true; - } else if (survey.recontactDays !== null) { - const lastDisplaySurvey = displays.filter((display) => display.surveyId === survey.id)[0]; - if (!lastDisplaySurvey) { + 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 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; - } - }); + }); - // if no surveys are left, return an empty array - if (surveys.length === 0) { - return []; - } - - // if no surveys have segment filters, return the surveys - if (!anySurveyHasFilters(surveys)) { - return surveys; - } - - const personActions = await getActionsByPersonId(person.id); - const personActionClassIds = Array.from( - new Set(personActions?.map((action) => action.actionClass?.id ?? "")) - ); - - 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) => { - const { segment } = survey; - // if the survey has no segment, or the segment has no filters, we return the survey - if (!segment || !segment.filters?.length) { - return survey; + // if no surveys are left, return an empty array + if (surveys.length === 0) { + return []; } - // backwards compatibility for older versions of the js package - // if the version is not provided, we will use the old method of evaluating the segment, which is attribute filters - // transform the segment filters to attribute filters and evaluate them - if (!options?.version) { - const attributeFilters = transformSegmentFiltersToAttributeFilters(segment.filters); + // if no surveys have segment filters, return the surveys + if (!anySurveyHasFilters(surveys)) { + return surveys; + } - // if the attribute filters are null, it means the segment filters don't match the expected format for attribute filters, so we skip this survey - if (attributeFilters === null) { - return null; - } + const personActions = await getActionsByPersonId(person.id); + const personActionClassIds = Array.from( + new Set(personActions?.map((action) => action.actionClass?.id ?? "")) + ); - // if there are no attribute filters, we return the survey - if (!attributeFilters.length) { + 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) => { + const { segment } = survey; + // if the survey has no segment, or the segment has no filters, we return the survey + if (!segment || !segment.filters?.length) { return survey; } - // we check if the person meets the attribute filters for all the attribute filters - const isEligible = attributeFilters.every((attributeFilter) => { - const personAttributeValue = attributes[attributeFilter.attributeClassName]; - if (!personAttributeValue) { - return false; + // backwards compatibility for older versions of the js package + // if the version is not provided, we will use the old method of evaluating the segment, which is attribute filters + // transform the segment filters to attribute filters and evaluate them + if (!options?.version) { + const attributeFilters = transformSegmentFiltersToAttributeFilters(segment.filters); + + // if the attribute filters are null, it means the segment filters don't match the expected format for attribute filters, so we skip this survey + if (attributeFilters === null) { + return null; } - if (attributeFilter.operator === "equals") { - return personAttributeValue === attributeFilter.value; - } else if (attributeFilter.operator === "notEquals") { - return personAttributeValue !== attributeFilter.value; - } else { - // if the operator is not equals or not equals, we skip the survey, this means that new segment filter options are being used - return false; + // if there are no attribute filters, we return the survey + if (!attributeFilters.length) { + return survey; } - }); - return isEligible ? survey : null; + // we check if the person meets the attribute filters for all the attribute filters + const isEligible = attributeFilters.every((attributeFilter) => { + const personAttributeValue = attributes[attributeFilter.attributeClassName]; + if (!personAttributeValue) { + return false; + } + + if (attributeFilter.operator === "equals") { + return personAttributeValue === attributeFilter.value; + } else if (attributeFilter.operator === "notEquals") { + return personAttributeValue !== attributeFilter.value; + } else { + // if the operator is not equals or not equals, we skip the survey, this means that new segment filter options are being used + return false; + } + }); + + return isEligible ? survey : null; + } + + // Evaluate the segment filters + const result = await evaluateSegment( + { + attributes: attributes ?? {}, + actionIds: personActionClassIds, + deviceType, + environmentId, + personId: person.id, + userId: personUserId, + }, + segment.filters + ); + + return result ? survey : null; + }); + + const resolvedSurveys = await Promise.all(surveyPromises); + surveys = resolvedSurveys.filter((survey) => !!survey) as TSurvey[]; + + if (!surveys) { + throw new ResourceNotFoundError("Survey", environmentId); + } + return surveys; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + console.error(error); + throw new DatabaseError(error.message); } - // Evaluate the segment filters - const result = await evaluateSegment( - { - attributes: attributes ?? {}, - actionIds: personActionClassIds, - deviceType, - environmentId, - personId: person.id, - userId: personUserId, - }, - segment.filters - ); - - return result ? survey : null; - }); - - const resolvedSurveys = await Promise.all(surveyPromises); - surveys = resolvedSurveys.filter((survey) => !!survey) as TSurvey[]; - - if (!surveys) { - throw new ResourceNotFoundError("Survey", environmentId); + throw error; } - return surveys; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error(error); - throw new DatabaseError(error.message); - } - - throw error; + }, + [`getSyncSurveys-${environmentId}-${personId}`], + { + tags: [ + personCache.tag.byEnvironmentId(environmentId), + personCache.tag.byId(personId), + displayCache.tag.byPersonId(personId), + surveyCache.tag.byEnvironmentId(environmentId), + productCache.tag.byEnvironmentId(environmentId), + attributeCache.tag.byPersonId(personId), + ], } - }, - [`getSyncSurveys-${environmentId}-${personId}`], - { - tags: [ - personCache.tag.byEnvironmentId(environmentId), - personCache.tag.byId(personId), - displayCache.tag.byPersonId(personId), - surveyCache.tag.byEnvironmentId(environmentId), - productCache.tag.byEnvironmentId(environmentId), - attributeCache.tag.byPersonId(personId), - ], - } - )(); + )() +); -export const getSurveyIdByResultShareKey = async (resultShareKey: string): Promise => { - try { - const survey = await prisma.survey.findFirst({ - where: { - resultShareKey, +export const getSurveyIdByResultShareKey = reactCache( + (resultShareKey: string): Promise => + cache( + async () => { + try { + const survey = await prisma.survey.findFirst({ + where: { + resultShareKey, + }, + select: { + id: true, + }, + }); + + if (!survey) { + return null; + } + + return survey.id; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + + throw error; + } }, - select: { - id: true, - }, - }); - - if (!survey) { - return null; - } - - return survey.id; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); - } - - throw error; - } -}; + [`getSurveyIdByResultShareKey-${resultShareKey}`], + { + tags: [surveyCache.tag.byResultShareKey(resultShareKey)], + } + )() +); export const loadNewSegmentInSurvey = async (surveyId: string, newSegmentId: string): Promise => { validateInputs([surveyId, ZId], [newSegmentId, ZId]); @@ -1091,33 +1115,35 @@ export const loadNewSegmentInSurvey = async (surveyId: string, newSegmentId: str } }; -export const getSurveysBySegmentId = (segmentId: string): Promise => - cache( - async () => { - try { - const surveysPrisma = await prisma.survey.findMany({ - where: { segmentId }, - select: selectSurvey, - }); +export const getSurveysBySegmentId = reactCache( + (segmentId: string): Promise => + cache( + async () => { + try { + const surveysPrisma = await prisma.survey.findMany({ + where: { segmentId }, + select: selectSurvey, + }); - const surveys: TSurvey[] = []; + const surveys: TSurvey[] = []; - for (const surveyPrisma of surveysPrisma) { - const transformedSurvey = transformPrismaSurvey(surveyPrisma); - surveys.push(transformedSurvey); + for (const surveyPrisma of surveysPrisma) { + const transformedSurvey = transformPrismaSurvey(surveyPrisma); + surveys.push(transformedSurvey); + } + + return surveys; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + + throw error; } - - return surveys; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); - } - - throw error; + }, + [`getSurveysBySegmentId-${segmentId}`], + { + tags: [surveyCache.tag.bySegmentId(segmentId), segmentCache.tag.byId(segmentId)], } - }, - [`getSurveysBySegmentId-${segmentId}`], - { - tags: [surveyCache.tag.bySegmentId(segmentId), segmentCache.tag.byId(segmentId)], - } - )(); + )() +); diff --git a/packages/lib/tag/cache.ts b/packages/lib/tag/cache.ts new file mode 100644 index 0000000000..1a7e47c925 --- /dev/null +++ b/packages/lib/tag/cache.ts @@ -0,0 +1,25 @@ +import { revalidateTag } from "next/cache"; + +interface RevalidateProps { + id?: string; + environmentId?: string; +} + +export const tagCache = { + tag: { + byId(id: string) { + return `tags-${id}`; + }, + byEnvironmentId(environmentId: string) { + return `environments-${environmentId}-tags`; + }, + }, + revalidate({ id, environmentId }: RevalidateProps): void { + if (id) { + revalidateTag(this.tag.byId(id)); + } + if (environmentId) { + revalidateTag(this.tag.byEnvironmentId(environmentId)); + } + }, +}; diff --git a/packages/lib/tag/service.ts b/packages/lib/tag/service.ts index 9893e6fccd..44a9351851 100644 --- a/packages/lib/tag/service.ts +++ b/packages/lib/tag/service.ts @@ -1,44 +1,65 @@ import "server-only"; +import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; import { ZOptionalNumber, ZString } from "@formbricks/types/common"; import { ZId } from "@formbricks/types/environment"; import { TTag } from "@formbricks/types/tags"; +import { cache } from "../cache"; import { ITEMS_PER_PAGE } from "../constants"; import { validateInputs } from "../utils/validate"; +import { tagCache } from "./cache"; -export const getTagsByEnvironmentId = async (environmentId: string, page?: number): Promise => { - validateInputs([environmentId, ZId], [page, ZOptionalNumber]); +export const getTagsByEnvironmentId = reactCache( + (environmentId: string, page?: number): Promise => + cache( + async () => { + validateInputs([environmentId, ZId], [page, ZOptionalNumber]); - try { - const tags = await prisma.tag.findMany({ - where: { - environmentId, + try { + const tags = await prisma.tag.findMany({ + where: { + environmentId, + }, + take: page ? ITEMS_PER_PAGE : undefined, + skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined, + }); + + return tags; + } catch (error) { + throw error; + } }, - take: page ? ITEMS_PER_PAGE : undefined, - skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined, - }); + [`getTagsByEnvironmentId-${environmentId}-${page}`], + { + tags: [tagCache.tag.byEnvironmentId(environmentId)], + } + )() +); - return tags; - } catch (error) { - throw error; - } -}; +export const getTag = reactCache( + (id: string): Promise => + cache( + async () => { + validateInputs([id, ZId]); -export const getTag = async (tagId: string): Promise => { - validateInputs([tagId, ZId]); + try { + const tag = await prisma.tag.findUnique({ + where: { + id, + }, + }); - try { - const tag = await prisma.tag.findUnique({ - where: { - id: tagId, + return tag; + } catch (error) { + throw error; + } }, - }); - - return tag; - } catch (error) { - throw error; - } -}; + [`getTag-${id}`], + { + tags: [tagCache.tag.byId(id)], + } + )() +); export const createTag = async (environmentId: string, name: string): Promise => { validateInputs([environmentId, ZId], [name, ZString]); @@ -51,20 +72,9 @@ export const createTag = async (environmentId: string, name: string): Promise => { - validateInputs([tagId, ZId]); - - try { - const tag = await prisma.tag.delete({ - where: { - id: tagId, - }, + tagCache.revalidate({ + id: tag.id, + environmentId, }); return tag; @@ -73,19 +83,45 @@ export const deleteTag = async (tagId: string): Promise => { } }; -export const updateTagName = async (tagId: string, name: string): Promise => { - validateInputs([tagId, ZId], [name, ZString]); +export const deleteTag = async (id: string): Promise => { + validateInputs([id, ZId]); + + try { + const tag = await prisma.tag.delete({ + where: { + id, + }, + }); + + tagCache.revalidate({ + id, + environmentId: tag.environmentId, + }); + + return tag; + } catch (error) { + throw error; + } +}; + +export const updateTagName = async (id: string, name: string): Promise => { + validateInputs([id, ZId], [name, ZString]); try { const tag = await prisma.tag.update({ where: { - id: tagId, + id, }, data: { name, }, }); + tagCache.revalidate({ + id: tag.id, + environmentId: tag.environmentId, + }); + return tag; } catch (error) { throw error; @@ -206,6 +242,15 @@ export const mergeTags = async (originalTagId: string, newTagId: string): Promis }), ]); + tagCache.revalidate({ + id: originalTagId, + environmentId: originalTag.environmentId, + }); + + tagCache.revalidate({ + id: newTagId, + }); + return newTag; } catch (error) { throw error; diff --git a/packages/lib/tagOnResponse/service.ts b/packages/lib/tagOnResponse/service.ts index e116765b0c..7db0a254ae 100644 --- a/packages/lib/tagOnResponse/service.ts +++ b/packages/lib/tagOnResponse/service.ts @@ -1,5 +1,6 @@ import "server-only"; import { Prisma } from "@prisma/client"; +import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; import { ZId } from "@formbricks/types/environment"; import { DatabaseError } from "@formbricks/types/errors"; @@ -91,38 +92,40 @@ export const deleteTagOnResponse = async (responseId: string, tagId: string): Pr } }; -export const getTagsOnResponsesCount = async (environmentId: string): Promise => - cache( - async () => { - validateInputs([environmentId, ZId]); +export const getTagsOnResponsesCount = reactCache( + (environmentId: string): Promise => + cache( + async () => { + validateInputs([environmentId, ZId]); - try { - const tagsCount = await prisma.tagsOnResponses.groupBy({ - by: ["tagId"], - where: { - response: { - survey: { - environment: { - id: environmentId, + try { + const tagsCount = await prisma.tagsOnResponses.groupBy({ + by: ["tagId"], + where: { + response: { + survey: { + environment: { + id: environmentId, + }, }, }, }, - }, - _count: { - _all: true, - }, - }); + _count: { + _all: true, + }, + }); - return tagsCount.map((tagCount) => ({ tagId: tagCount.tagId, count: tagCount._count._all })); - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); + return tagsCount.map((tagCount) => ({ tagId: tagCount.tagId, count: tagCount._count._all })); + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + throw error; } - throw error; + }, + [`getTagsOnResponsesCount-${environmentId}`], + { + tags: [tagOnResponseCache.tag.byEnvironmentId(environmentId)], } - }, - [`getTagsOnResponsesCount-${environmentId}`], - { - tags: [tagOnResponseCache.tag.byEnvironmentId(environmentId)], - } - )(); + )() +); diff --git a/packages/lib/user/service.ts b/packages/lib/user/service.ts index c4898bc57e..5b8adcbdf0 100644 --- a/packages/lib/user/service.ts +++ b/packages/lib/user/service.ts @@ -1,5 +1,6 @@ import "server-only"; import { Prisma } from "@prisma/client"; +import { cache as reactCache } from "react"; import { z } from "zod"; import { prisma } from "@formbricks/database"; import { ZId } from "@formbricks/types/environment"; @@ -29,64 +30,68 @@ const responseSelection = { }; // function to retrive basic information about a user's user -export const getUser = (id: string): Promise => - cache( - async () => { - validateInputs([id, ZId]); +export const getUser = reactCache( + (id: string): Promise => + cache( + async () => { + validateInputs([id, ZId]); - try { - const user = await prisma.user.findUnique({ - where: { - id, - }, - select: responseSelection, - }); + try { + const user = await prisma.user.findUnique({ + where: { + id, + }, + select: responseSelection, + }); - if (!user) { - return null; + if (!user) { + return null; + } + return user; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + + throw error; } - return user; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); - } - - throw error; + }, + [`getUser-${id}`], + { + tags: [userCache.tag.byId(id)], } - }, - [`getUser-${id}`], - { - tags: [userCache.tag.byId(id)], - } - )(); + )() +); -export const getUserByEmail = (email: string): Promise => - cache( - async () => { - validateInputs([email, z.string().email()]); +export const getUserByEmail = reactCache( + (email: string): Promise => + cache( + async () => { + validateInputs([email, z.string().email()]); - try { - const user = await prisma.user.findFirst({ - where: { - email, - }, - select: responseSelection, - }); + try { + const user = await prisma.user.findFirst({ + where: { + email, + }, + select: responseSelection, + }); - return user; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); + return user; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + + throw error; } - - throw error; + }, + [`getUserByEmail-${email}`], + { + tags: [userCache.tag.byEmail(email)], } - }, - [`getUserByEmail-${email}`], - { - tags: [userCache.tag.byEmail(email)], - } - )(); + )() +); const getAdminMemberships = (memberships: TMembership[]): TMembership[] => memberships.filter((membership) => membership.role === "admin"); diff --git a/packages/lib/utils/validate.ts b/packages/lib/utils/validate.ts index f9ca6f7500..b195ecde93 100644 --- a/packages/lib/utils/validate.ts +++ b/packages/lib/utils/validate.ts @@ -1,4 +1,4 @@ -import z from "zod"; +import { z } from "zod"; import { ValidationError } from "@formbricks/types/errors"; type ValidationPair = [any, z.ZodSchema]; diff --git a/packages/lib/vitestSetup.ts b/packages/lib/vitestSetup.ts index 4820c5cc99..228a44552f 100644 --- a/packages/lib/vitestSetup.ts +++ b/packages/lib/vitestSetup.ts @@ -10,6 +10,18 @@ vi.mock("next/cache", () => ({ revalidateTag: vi.fn(), })); +// mock react cache +const testCache = (func: T) => func; + +vi.mock("react", () => { + const originalModule = vi.importActual("react"); + return { + ...originalModule, + cache: testCache, + }; +}); + +// mock server-only vi.mock("server-only", () => { return {}; });