diff --git a/apps/web/app/(app)/FormbricksClient.tsx b/apps/web/app/(app)/FormbricksClient.tsx
index ebc694e2da..6289105b52 100644
--- a/apps/web/app/(app)/FormbricksClient.tsx
+++ b/apps/web/app/(app)/FormbricksClient.tsx
@@ -7,7 +7,6 @@ import { useEffect } from "react";
type UsageAttributesUpdaterProps = {
numSurveys: number;
- totalSubmissions: number;
};
export default function FormbricksClient({ session }) {
@@ -19,31 +18,23 @@ export default function FormbricksClient({ session }) {
});
formbricks.setUserId(session.user.id);
formbricks.setEmail(session.user.email);
- if (session.user.teams?.length > 0) {
- formbricks.setAttribute("Plan", session.user.teams[0].plan);
- formbricks.setAttribute("Name", session?.user?.name);
- }
}
}, [session]);
return null;
}
-const updateUsageAttributes = (numSurveys, totalSubmissions) => {
+const updateUsageAttributes = (numSurveys) => {
if (!formbricksEnabled) return;
if (numSurveys >= 3) {
formbricks.setAttribute("HasThreeSurveys", "true");
}
-
- if (totalSubmissions >= 20) {
- formbricks.setAttribute("HasTwentySubmissions", "true");
- }
};
-export function UsageAttributesUpdater({ numSurveys, totalSubmissions }: UsageAttributesUpdaterProps) {
+export function UsageAttributesUpdater({ numSurveys }: UsageAttributesUpdaterProps) {
useEffect(() => {
- updateUsageAttributes(numSurveys, totalSubmissions);
- }, [numSurveys, totalSubmissions]);
+ updateUsageAttributes(numSurveys);
+ }, [numSurveys]);
return null;
}
diff --git a/apps/web/app/(app)/PosthogIdentify.tsx b/apps/web/app/(app)/PosthogIdentify.tsx
index b022fb4056..dc055535d5 100644
--- a/apps/web/app/(app)/PosthogIdentify.tsx
+++ b/apps/web/app/(app)/PosthogIdentify.tsx
@@ -12,9 +12,6 @@ export default function PosthogIdentify({ session }: { session: Session }) {
useEffect(() => {
if (posthogEnabled && session.user && posthog) {
posthog.identify(session.user.id);
- if (session.user.teams?.length > 0) {
- posthog?.group("team", session.user.teams[0].id);
- }
}
}, [session, posthog]);
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyDropDownMenu.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyDropDownMenu.tsx
index 0e711af727..3df34eca02 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyDropDownMenu.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyDropDownMenu.tsx
@@ -15,7 +15,7 @@ import {
} from "@/components/shared/DropdownMenu";
import LoadingSpinner from "@/components/shared/LoadingSpinner";
import type { TEnvironment } from "@formbricks/types/v1/environment";
-import type { TSurveyWithAnalytics } from "@formbricks/types/v1/surveys";
+import type { TSurvey } from "@formbricks/types/v1/surveys";
import {
ArrowUpOnSquareStackIcon,
DocumentDuplicateIcon,
@@ -32,7 +32,7 @@ import toast from "react-hot-toast";
interface SurveyDropDownMenuProps {
environmentId: string;
- survey: TSurveyWithAnalytics;
+ survey: TSurvey;
environment: TEnvironment;
otherEnvironment: TEnvironment;
surveyBaseUrl: string;
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyList.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyList.tsx
index 5058fbb62a..5665ec320b 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyList.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyList.tsx
@@ -2,15 +2,14 @@ import { UsageAttributesUpdater } from "@/app/(app)/FormbricksClient";
import SurveyDropDownMenu from "@/app/(app)/environments/[environmentId]/surveys/SurveyDropDownMenu";
import SurveyStarter from "@/app/(app)/environments/[environmentId]/surveys/SurveyStarter";
import SurveyStatusIndicator from "@/components/shared/SurveyStatusIndicator";
+import { SURVEY_BASE_URL } from "@formbricks/lib/constants";
import { getEnvironment, getEnvironments } from "@formbricks/lib/services/environment";
import { getProductByEnvironmentId } from "@formbricks/lib/services/product";
-import { getSurveysWithAnalytics } from "@formbricks/lib/services/survey";
+import { getSurveys } from "@formbricks/lib/services/survey";
import type { TEnvironment } from "@formbricks/types/v1/environment";
-import type { TSurveyWithAnalytics } from "@formbricks/types/v1/surveys";
import { Badge } from "@formbricks/ui";
import { ComputerDesktopIcon, LinkIcon, PlusIcon } from "@heroicons/react/24/solid";
import Link from "next/link";
-import { SURVEY_BASE_URL } from "@formbricks/lib/constants";
export default async function SurveysList({ environmentId }: { environmentId: string }) {
const product = await getProductByEnvironmentId(environmentId);
@@ -22,10 +21,10 @@ export default async function SurveysList({ environmentId }: { environmentId: st
if (!environment) {
throw new Error("Environment not found");
}
- const surveys: TSurveyWithAnalytics[] = await getSurveysWithAnalytics(environmentId);
+ const surveys = await getSurveys(environmentId);
+
const environments: TEnvironment[] = await getEnvironments(product.id);
const otherEnvironment = environments.find((e) => e.type !== environment.type)!;
- const totalSubmissions = surveys.reduce((acc, survey) => acc + (survey.analytics?.numResponses || 0), 0);
if (surveys.length === 0) {
return ;
@@ -45,7 +44,7 @@ export default async function SurveysList({ environmentId }: { environmentId: st
{surveys
- .sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime())
+ .sort((a, b) => b.updatedAt?.getTime() - a.updatedAt?.getTime())
.map((survey) => (
@@ -82,9 +81,6 @@ export default async function SurveysList({ environmentId }: { environmentId: st
tooltip
environmentId={environmentId}
/>
-
- {survey.analytics.numResponses} responses
-
>
)}
{survey.status === "draft" && (
@@ -93,7 +89,7 @@ export default async function SurveysList({ environmentId }: { environmentId: st
))}
-
+
>
);
}
diff --git a/apps/web/app/api/auth/[...nextauth]/authOptions.ts b/apps/web/app/api/auth/[...nextauth]/authOptions.ts
index 3c61f999d4..0351fb872a 100644
--- a/apps/web/app/api/auth/[...nextauth]/authOptions.ts
+++ b/apps/web/app/api/auth/[...nextauth]/authOptions.ts
@@ -1,8 +1,9 @@
import { env } from "@/env.mjs";
import { verifyPassword } from "@/lib/auth";
-import { verifyToken } from "@formbricks/lib/jwt";
import { prisma } from "@formbricks/database";
import { INTERNAL_SECRET, WEBAPP_URL } from "@formbricks/lib/constants";
+import { verifyToken } from "@formbricks/lib/jwt";
+import { getProfileByEmail } from "@formbricks/lib/services/profile";
import type { IdentityProvider } from "@prisma/client";
import type { NextAuthOptions } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
@@ -133,42 +134,16 @@ export const authOptions: NextAuthOptions = {
],
callbacks: {
async jwt({ token }) {
- const existingUser = await prisma.user.findFirst({
- where: { email: token.email! },
- select: {
- id: true,
- createdAt: true,
- onboardingCompleted: true,
- memberships: {
- select: {
- teamId: true,
- role: true,
- team: {
- select: {
- plan: true,
- },
- },
- },
- },
- name: true,
- },
- });
+ const existingUser = await getProfileByEmail(token?.email!);
if (!existingUser) {
return token;
}
- const teams = existingUser.memberships.map((membership) => ({
- id: membership.teamId,
- role: membership.role,
- plan: membership.team.plan,
- }));
-
const additionalAttributs = {
id: existingUser.id,
createdAt: existingUser.createdAt,
onboardingCompleted: existingUser.onboardingCompleted,
- teams,
name: existingUser.name,
};
@@ -185,7 +160,6 @@ export const authOptions: NextAuthOptions = {
// @ts-ignore
session.user.onboardingCompleted = token?.onboardingCompleted;
// @ts-ignore
- session.user.teams = token?.teams;
session.user.name = token.name || "";
return session;
diff --git a/apps/web/app/api/v1/js/surveys.ts b/apps/web/app/api/v1/js/surveys.ts
index 8b53d212ce..21095d928a 100644
--- a/apps/web/app/api/v1/js/surveys.ts
+++ b/apps/web/app/api/v1/js/surveys.ts
@@ -5,13 +5,13 @@ import { TSurvey } from "@formbricks/types/v1/surveys";
import { unstable_cache } from "next/cache";
const getSurveysCacheTags = (environmentId: string, personId: string): string[] => [
- `env-${environmentId}-surveys`,
- `env-${environmentId}-product`,
+ `environments-${environmentId}-surveys`,
+ `environments-${environmentId}-product`,
personId,
];
const getSurveysCacheKey = (environmentId: string, personId: string): string[] => [
- `env-${environmentId}-person-${personId}-syncSurveys`,
+ `environments-${environmentId}-person-${personId}-syncSurveys`,
];
export const getSurveysCached = (environmentId: string, person: TPerson) =>
diff --git a/apps/web/app/api/v1/js/sync/lib/sync.ts b/apps/web/app/api/v1/js/sync/lib/sync.ts
index e1b4385c31..a9ee6f7ec2 100644
--- a/apps/web/app/api/v1/js/sync/lib/sync.ts
+++ b/apps/web/app/api/v1/js/sync/lib/sync.ts
@@ -1,7 +1,7 @@
import { getSurveysCached } from "@/app/api/v1/js/surveys";
import { MAU_LIMIT } from "@formbricks/lib/constants";
import { getActionClassesCached } from "@formbricks/lib/services/actionClass";
-import { getEnvironmentCached } from "@formbricks/lib/services/environment";
+import { getEnvironment } from "@formbricks/lib/services/environment";
import { createPerson, getMonthlyActivePeopleCount, getPersonCached } from "@formbricks/lib/services/person";
import { getProductByEnvironmentIdCached } from "@formbricks/lib/services/product";
import { createSession, extendSession, getSessionCached } from "@formbricks/lib/services/session";
@@ -26,7 +26,7 @@ export const getUpdatedState = async (
let session: TSession | null;
// check if environment exists
- environment = await getEnvironmentCached(environmentId);
+ environment = await getEnvironment(environmentId);
if (!environment) {
throw new Error("Environment does not exist");
diff --git a/apps/web/components/shared/WidgetStatusIndicator.tsx b/apps/web/components/shared/WidgetStatusIndicator.tsx
index 1f2bb68785..4f0fa8eb04 100644
--- a/apps/web/components/shared/WidgetStatusIndicator.tsx
+++ b/apps/web/components/shared/WidgetStatusIndicator.tsx
@@ -31,7 +31,7 @@ export default function WidgetStatusIndicator({
if (!environment?.widgetSetupCompleted && actions && actions.length > 0) {
updateEnvironmentAction(environment.id, { widgetSetupCompleted: true });
}
- }, [environment, actions]);
+ }, [environment, actions, updateEnvironmentAction]);
const stati = {
notImplemented: {
diff --git a/packages/lib/services/actionClass.ts b/packages/lib/services/actionClass.ts
index c0595b81a1..f6e02c7db5 100644
--- a/packages/lib/services/actionClass.ts
+++ b/packages/lib/services/actionClass.ts
@@ -12,12 +12,13 @@ import { validateInputs } from "../utils/validate";
const halfHourInSeconds = 60 * 30;
export const getActionClassCacheTag = (name: string, environmentId: string): string =>
- `env-${environmentId}-actionClass-${name}`;
+ `environments-${environmentId}-actionClass-${name}`;
const getActionClassCacheKey = (name: string, environmentId: string): string[] => [
getActionClassCacheTag(name, environmentId),
];
-const getActionClassesCacheTag = (environmentId: string): string => `env-${environmentId}-actionClasses`;
+const getActionClassesCacheTag = (environmentId: string): string =>
+ `environments-${environmentId}-actionClasses`;
const getActionClassesCacheKey = (environmentId: string): string[] => [
getActionClassesCacheTag(environmentId),
];
diff --git a/packages/lib/services/attributeClass.ts b/packages/lib/services/attributeClass.ts
index 672481b804..28774ceec8 100644
--- a/packages/lib/services/attributeClass.ts
+++ b/packages/lib/services/attributeClass.ts
@@ -13,7 +13,8 @@ import { DatabaseError } from "@formbricks/types/v1/errors";
import { cache } from "react";
import { revalidateTag, unstable_cache } from "next/cache";
-const attributeClassesCacheTag = (environmentId: string): string => `env-${environmentId}-attributeClasses`;
+const attributeClassesCacheTag = (environmentId: string): string =>
+ `environments-${environmentId}-attributeClasses`;
const getAttributeClassesCacheKey = (environmentId: string): string[] => [
attributeClassesCacheTag(environmentId),
diff --git a/packages/lib/services/displays.ts b/packages/lib/services/displays.ts
index db0ff1b74b..e07d0f54a5 100644
--- a/packages/lib/services/displays.ts
+++ b/packages/lib/services/displays.ts
@@ -39,6 +39,8 @@ const selectDisplay = {
status: true,
};
+export const getDisplaysCacheTag = (surveyId: string) => `surveys-${surveyId}-displays`;
+
export const createDisplay = async (displayInput: TDisplayInput): Promise => {
validateInputs([displayInput, ZDisplayInput]);
try {
@@ -71,6 +73,10 @@ export const createDisplay = async (displayInput: TDisplayInput): Promise => {
- validateInputs([environmentId, ZId]);
- let environmentPrisma;
+export const getEnvironmentCacheTag = (environmentId: string) => `environments-${environmentId}`;
+export const getEnvironmentsCacheTag = (productId: string) => `products-${productId}-environments`;
- try {
- environmentPrisma = await prisma.environment.findUnique({
- where: {
- id: environmentId,
- },
- });
- } catch (error) {
- if (error instanceof Prisma.PrismaClientKnownRequestError) {
- throw new DatabaseError("Database operation failed");
- }
-
- throw error;
- }
-
- try {
- const environment = ZEnvironment.parse(environmentPrisma);
- return environment;
- } catch (error) {
- if (error instanceof z.ZodError) {
- console.error(JSON.stringify(error.errors, null, 2));
- }
- throw new ValidationError("Data validation of environment failed");
- }
-});
-
-export const getEnvironmentCached = (environmentId: string) =>
+export const getEnvironment = (environmentId: string) =>
unstable_cache(
async () => {
- return await getEnvironment(environmentId);
+ validateInputs([environmentId, ZId]);
+ let environmentPrisma;
+
+ try {
+ environmentPrisma = await prisma.environment.findUnique({
+ where: {
+ id: environmentId,
+ },
+ });
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
+ throw new DatabaseError("Database operation failed");
+ }
+
+ throw error;
+ }
+
+ try {
+ const environment = ZEnvironment.parse(environmentPrisma);
+ return environment;
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ console.error(JSON.stringify(error.errors, null, 2));
+ }
+ throw new ValidationError("Data validation of environment failed");
+ }
},
- [environmentId],
+ [`environments-${environmentId}`],
{
- tags: [environmentId],
+ tags: [getEnvironmentCacheTag(environmentId)],
revalidate: 30 * 60, // 30 minutes
}
)();
-export const getEnvironments = cache(async (productId: string): Promise => {
- validateInputs([productId, ZId]);
- let productPrisma;
- try {
- productPrisma = await prisma.product.findFirst({
- where: {
- id: productId,
- },
- include: {
- environments: true,
- },
- });
+export const getEnvironments = async (productId: string): Promise =>
+ unstable_cache(
+ async () => {
+ 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("Database operation failed");
- }
- throw error;
- }
+ if (!productPrisma) {
+ throw new ResourceNotFoundError("Product", productId);
+ }
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
+ throw new DatabaseError("Database operation failed");
+ }
+ throw error;
+ }
- const environments: TEnvironment[] = [];
- for (let environment of productPrisma.environments) {
- let targetEnvironment: TEnvironment = ZEnvironment.parse(environment);
- environments.push(targetEnvironment);
- }
+ 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");
+ }
+ },
+ [`products-${productId}-environments`],
+ {
+ tags: [getEnvironmentsCacheTag(productId)],
+ revalidate: 30 * 60, // 30 minutes
}
- throw new ValidationError("Data validation of environments array failed");
- }
-});
+ )();
export const updateEnvironment = async (
environmentId: string,
@@ -104,6 +110,10 @@ export const updateEnvironment = async (
},
data: newData,
});
+
+ revalidateTag(getEnvironmentsCacheTag(updatedEnvironment.productId));
+ revalidateTag(getEnvironmentCacheTag(environmentId));
+
return updatedEnvironment;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
diff --git a/packages/lib/services/person.ts b/packages/lib/services/person.ts
index 61dd47a489..c8e80598ae 100644
--- a/packages/lib/services/person.ts
+++ b/packages/lib/services/person.ts
@@ -308,9 +308,9 @@ export const getMonthlyActivePeopleCount = async (environmentId: string): Promis
return aggregations._count.id;
},
- [`env-${environmentId}-mau`],
+ [`environments-${environmentId}-mau`],
{
- tags: [`env-${environmentId}-mau`],
+ tags: [`environments-${environmentId}-mau`],
revalidate: 60 * 60 * 6, // 6 hours
}
)();
diff --git a/packages/lib/services/product.ts b/packages/lib/services/product.ts
index daeb672076..f7179928dd 100644
--- a/packages/lib/services/product.ts
+++ b/packages/lib/services/product.ts
@@ -9,8 +9,10 @@ import { cache } from "react";
import "server-only";
import { z } from "zod";
import { validateInputs } from "../utils/validate";
+import { getEnvironmentCacheTag, getEnvironmentsCacheTag } from "./environment";
-const getProductCacheTag = (environmentId: string): string => `env-${environmentId}-product`;
+export const getProductsCacheTag = (teamId: string): string => `teams-${teamId}-products`;
+const getProductCacheTag = (environmentId: string): string => `environments-${environmentId}-product`;
const getProductCacheKey = (environmentId: string): string[] => [getProductCacheTag(environmentId)];
const selectProduct = {
@@ -29,25 +31,33 @@ const selectProduct = {
environments: true,
};
-export const getProducts = cache(async (teamId: string): Promise => {
- validateInputs([teamId, ZId]);
- try {
- const products = await prisma.product.findMany({
- where: {
- teamId,
- },
- select: selectProduct,
- });
+export const getProducts = async (teamId: string): Promise =>
+ unstable_cache(
+ async () => {
+ validateInputs([teamId, ZId]);
+ try {
+ const products = await prisma.product.findMany({
+ where: {
+ teamId,
+ },
+ select: selectProduct,
+ });
- return products;
- } catch (error) {
- if (error instanceof Prisma.PrismaClientKnownRequestError) {
- throw new DatabaseError("Database operation failed");
+ return products;
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
+ throw new DatabaseError("Database operation failed");
+ }
+
+ throw error;
+ }
+ },
+ [`teams-${teamId}-products`],
+ {
+ tags: [getProductsCacheTag(teamId)],
+ revalidate: 30 * 60, // 30 minutes
}
-
- throw error;
- }
-});
+ )();
export const getProductByEnvironmentId = cache(async (environmentId: string): Promise => {
if (!environmentId) {
@@ -113,6 +123,7 @@ export const updateProduct = async (
try {
const product = ZProduct.parse(updatedProduct);
+ revalidateTag(getProductsCacheTag(product.teamId));
product.environments.forEach((environment) => {
// revalidate environment cache
revalidateTag(getProductCacheTag(environment.id));
@@ -155,10 +166,12 @@ export const deleteProduct = cache(async (productId: string): Promise
});
if (product) {
+ revalidateTag(getProductsCacheTag(product.teamId));
+ revalidateTag(getEnvironmentsCacheTag(product.id));
product.environments.forEach((environment) => {
// revalidate product cache
revalidateTag(getProductCacheTag(environment.id));
- revalidateTag(environment.id);
+ revalidateTag(getEnvironmentCacheTag(environment.id));
});
}
diff --git a/packages/lib/services/profile.ts b/packages/lib/services/profile.ts
index 8380ae3b74..f11cbc4efe 100644
--- a/packages/lib/services/profile.ts
+++ b/packages/lib/services/profile.ts
@@ -4,9 +4,10 @@ import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/v1/error
import { TMembership, TMembershipRole, ZMembershipRole } from "@formbricks/types/v1/memberships";
import { TProfile, TProfileUpdateInput, ZProfileUpdateInput } from "@formbricks/types/v1/profile";
import { MembershipRole, Prisma } from "@prisma/client";
-import { cache } from "react";
+import { unstable_cache, revalidateTag } from "next/cache";
import { validateInputs } from "../utils/validate";
import { deleteTeam } from "./team";
+import { z } from "zod";
const responseSelection = {
id: true,
@@ -17,30 +18,73 @@ const responseSelection = {
onboardingCompleted: true,
};
+export const getProfileCacheTag = (userId: string): string => `profiles-${userId}`;
+export const getProfileByEmailCacheTag = (email: string): string => `profiles-${email}`;
+
// function to retrive basic information about a user's profile
-export const getProfile = cache(async (userId: string): Promise => {
- validateInputs([userId, ZId]);
- try {
- const profile = await prisma.user.findUnique({
- where: {
- id: userId,
- },
- select: responseSelection,
- });
+export const getProfile = async (userId: string): Promise =>
+ unstable_cache(
+ async () => {
+ validateInputs([userId, ZId]);
+ try {
+ const profile = await prisma.user.findUnique({
+ where: {
+ id: userId,
+ },
+ select: responseSelection,
+ });
- if (!profile) {
- return null;
+ if (!profile) {
+ return null;
+ }
+
+ return profile;
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
+ throw new DatabaseError("Database operation failed");
+ }
+
+ throw error;
+ }
+ },
+ [`profiles-${userId}`],
+ {
+ tags: [getProfileByEmailCacheTag(userId)],
+ revalidate: 30 * 60, // 30 minutes
}
+ )();
- return profile;
- } catch (error) {
- if (error instanceof Prisma.PrismaClientKnownRequestError) {
- throw new DatabaseError("Database operation failed");
+export const getProfileByEmail = async (email: string): Promise =>
+ unstable_cache(
+ async () => {
+ validateInputs([email, z.string().email()]);
+ try {
+ const profile = await prisma.user.findFirst({
+ where: {
+ email,
+ },
+ select: responseSelection,
+ });
+
+ if (!profile) {
+ return null;
+ }
+
+ return profile;
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
+ throw new DatabaseError("Database operation failed");
+ }
+
+ throw error;
+ }
+ },
+ [`profiles-${email}`],
+ {
+ tags: [getProfileCacheTag(email)],
+ revalidate: 30 * 60, // 30 minutes
}
-
- throw error;
- }
-});
+ )();
const updateUserMembership = async (teamId: string, userId: string, role: TMembershipRole) => {
validateInputs([teamId, ZId], [userId, ZId], [role, ZMembershipRole]);
@@ -74,6 +118,9 @@ export const updateProfile = async (
data: data,
});
+ revalidateTag(getProfileByEmailCacheTag(updatedProfile.email));
+ revalidateTag(getProfileCacheTag(personId));
+
return updatedProfile;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2016") {
@@ -83,22 +130,27 @@ export const updateProfile = async (
}
}
};
-const deleteUser = async (userId: string) => {
+
+const deleteUser = async (userId: string): Promise => {
validateInputs([userId, ZId]);
- await prisma.user.delete({
+ const profile = await prisma.user.delete({
where: {
id: userId,
},
});
+ revalidateTag(getProfileByEmailCacheTag(profile.email));
+ revalidateTag(getProfileCacheTag(userId));
+
+ return profile;
};
// function to delete a user's profile including teams
-export const deleteProfile = async (personId: string): Promise => {
- validateInputs([personId, ZId]);
+export const deleteProfile = async (userId: string): Promise => {
+ validateInputs([userId, ZId]);
try {
const currentUserMemberships = await prisma.membership.findMany({
where: {
- userId: personId,
+ userId: userId,
},
include: {
team: {
@@ -131,7 +183,8 @@ export const deleteProfile = async (personId: string): Promise => {
}
}
- await deleteUser(personId);
+ revalidateTag(getProfileCacheTag(userId));
+ await deleteUser(userId);
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError("Database operation failed");
diff --git a/packages/lib/services/response.ts b/packages/lib/services/response.ts
index 4267dcd98f..008a7e3e32 100644
--- a/packages/lib/services/response.ts
+++ b/packages/lib/services/response.ts
@@ -16,6 +16,7 @@ import { getPerson, transformPrismaPerson } from "./person";
import { captureTelemetry } from "../telemetry";
import { validateInputs } from "../utils/validate";
import { ZId } from "@formbricks/types/v1/environment";
+import { revalidateTag } from "next/cache";
const responseSelection = {
id: true,
@@ -75,6 +76,8 @@ const responseSelection = {
},
};
+export const getResponsesCacheTag = (surveyId: string) => `surveys-${surveyId}-responses`;
+
export const getResponsesByPersonId = async (personId: string): Promise | null> => {
validateInputs([personId, ZId]);
try {
@@ -147,6 +150,10 @@ export const createResponse = async (responseInput: Partial): Pr
tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
};
+ if (response.surveyId) {
+ revalidateTag(getResponsesCacheTag(response.surveyId));
+ }
+
return response;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
@@ -296,6 +303,10 @@ export const updateResponse = async (
tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
};
+ if (response.surveyId) {
+ revalidateTag(getResponsesCacheTag(response.surveyId));
+ }
+
return response;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
diff --git a/packages/lib/services/survey.ts b/packages/lib/services/survey.ts
index 495799b675..93cec78f89 100644
--- a/packages/lib/services/survey.ts
+++ b/packages/lib/services/survey.ts
@@ -9,14 +9,26 @@ import {
ZSurveyWithAnalytics,
} from "@formbricks/types/v1/surveys";
import { Prisma } from "@prisma/client";
-import { revalidateTag } from "next/cache";
-import { cache } from "react";
+import { revalidateTag, unstable_cache } from "next/cache";
import "server-only";
import { z } from "zod";
import { captureTelemetry } from "../telemetry";
import { validateInputs } from "../utils/validate";
+import { getDisplaysCacheTag } from "./displays";
+import { getResponsesCacheTag } from "./response";
-const getSurveysCacheTag = (environmentId: string): string => `env-${environmentId}-surveys`;
+// surveys cache key and tags
+const getSurveysCacheKey = (environmentId: string): string => `environments-${environmentId}-surveys`;
+const getSurveysCacheTag = (environmentId: string): string => `environments-${environmentId}-surveys`;
+
+// survey cache key and tags
+export const getSurveyCacheKey = (surveyId: string): string => `surveys-${surveyId}`;
+export const getSurveyCacheTag = (surveyId: string): string => `surveys-${surveyId}`;
+
+// survey with analytics cache key
+const getSurveysWithAnalyticsCacheKey = (environmentId: string): string =>
+ `environments-${environmentId}-surveysWithAnalytics`;
+const getSurveyWithAnalyticsCacheKey = (surveyId: string): string => `surveyWithAnalytics-${surveyId}`;
export const selectSurvey = {
id: true,
@@ -78,187 +90,255 @@ export const selectSurveyWithAnalytics = {
},
};
-export const preloadSurveyWithAnalytics = (surveyId: string) => {
- validateInputs([surveyId, ZId]);
- void getSurveyWithAnalytics(surveyId);
-};
+export const getSurveyWithAnalytics = async (surveyId: string): Promise => {
+ const survey = await unstable_cache(
+ async () => {
+ validateInputs([surveyId, ZId]);
+ let surveyPrisma;
+ try {
+ surveyPrisma = await prisma.survey.findUnique({
+ where: {
+ id: surveyId,
+ },
+ select: selectSurveyWithAnalytics,
+ });
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
+ throw new DatabaseError("Database operation failed");
+ }
-export const getSurveyWithAnalytics = cache(
- async (surveyId: string): Promise => {
- validateInputs([surveyId, ZId]);
- let surveyPrisma;
- try {
- surveyPrisma = await prisma.survey.findUnique({
- where: {
- id: surveyId,
+ throw error;
+ }
+
+ if (!surveyPrisma) {
+ throw new ResourceNotFoundError("Survey", surveyId);
+ }
+
+ let { _count, displays, ...surveyPrismaFields } = surveyPrisma;
+
+ const numDisplays = displays.length;
+ const numDisplaysResponded = displays.filter((item) => item.status === "responded").length;
+ const numResponses = _count.responses;
+ // responseRate, rounded to 2 decimal places
+ const responseRate = numDisplays ? Math.round((numDisplaysResponded / numDisplays) * 100) / 100 : 0;
+
+ const transformedSurvey = {
+ ...surveyPrismaFields,
+ triggers: surveyPrismaFields.triggers.map((trigger) => trigger.eventClass),
+ analytics: {
+ numDisplays,
+ responseRate,
+ numResponses,
},
- select: selectSurveyWithAnalytics,
- });
- } catch (error) {
- if (error instanceof Prisma.PrismaClientKnownRequestError) {
- throw new DatabaseError("Database operation failed");
+ };
+
+ try {
+ const survey = ZSurveyWithAnalytics.parse(transformedSurvey);
+ return survey;
+ } catch (error) {
+ console.log(error);
+ if (error instanceof z.ZodError) {
+ console.error(JSON.stringify(error.errors, null, 2)); // log the detailed error information
+ }
+ throw new ValidationError("Data validation of survey failed");
}
-
- throw error;
+ },
+ [getSurveyWithAnalyticsCacheKey(surveyId)],
+ {
+ tags: [getSurveyCacheTag(surveyId), getDisplaysCacheTag(surveyId), getResponsesCacheTag(surveyId)],
+ revalidate: 60 * 30,
}
+ )();
- if (!surveyPrisma) {
- throw new ResourceNotFoundError("Survey", surveyId);
- }
-
- let { _count, displays, ...surveyPrismaFields } = surveyPrisma;
-
- const numDisplays = displays.length;
- const numDisplaysResponded = displays.filter((item) => item.status === "responded").length;
- const numResponses = _count.responses;
- // responseRate, rounded to 2 decimal places
- const responseRate = numDisplays ? Math.round((numDisplaysResponded / numDisplays) * 100) / 100 : 0;
-
- const transformedSurvey = {
- ...surveyPrismaFields,
- triggers: surveyPrismaFields.triggers.map((trigger) => trigger.eventClass),
- analytics: {
- numDisplays,
- responseRate,
- numResponses,
- },
- };
-
- try {
- const survey = ZSurveyWithAnalytics.parse(transformedSurvey);
- return survey;
- } catch (error) {
- if (error instanceof z.ZodError) {
- console.error(JSON.stringify(error.errors, null, 2)); // log the detailed error information
- }
- throw new ValidationError("Data validation of survey failed");
- }
- }
-);
-
-export const getSurvey = cache(async (surveyId: string): Promise => {
- validateInputs([surveyId, ZId]);
- let surveyPrisma;
- try {
- surveyPrisma = await prisma.survey.findUnique({
- where: {
- id: surveyId,
- },
- select: selectSurvey,
- });
- } catch (error) {
- if (error instanceof Prisma.PrismaClientKnownRequestError) {
- throw new DatabaseError("Database operation failed");
- }
-
- throw error;
- }
-
- if (!surveyPrisma) {
+ if (!survey) {
return null;
}
- const transformedSurvey = {
- ...surveyPrisma,
- triggers: surveyPrisma.triggers.map((trigger) => trigger.eventClass),
+ // since the unstable_cache function does not support deserialization of dates, we need to manually deserialize them
+ // https://github.com/vercel/next.js/issues/51613
+ return {
+ ...survey,
+ createdAt: new Date(survey.createdAt),
+ updatedAt: new Date(survey.updatedAt),
};
+};
- try {
- const survey = ZSurvey.parse(transformedSurvey);
- return survey;
- } catch (error) {
- if (error instanceof z.ZodError) {
- console.error(JSON.stringify(error.errors, null, 2)); // log the detailed error information
- }
- throw new ValidationError("Data validation of survey failed");
- }
-});
+export const getSurvey = async (surveyId: string): Promise => {
+ const survey = await unstable_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) {
+ throw new DatabaseError("Database operation failed");
+ }
-export const getSurveys = cache(async (environmentId: string): Promise => {
- validateInputs([environmentId, ZId]);
- let surveysPrisma;
- try {
- surveysPrisma = await prisma.survey.findMany({
- where: {
- environmentId,
- },
- select: selectSurvey,
- });
- } catch (error) {
- if (error instanceof Prisma.PrismaClientKnownRequestError) {
- throw new DatabaseError("Database operation failed");
- }
+ throw error;
+ }
- throw error;
- }
+ if (!surveyPrisma) {
+ return null;
+ }
- const surveys: TSurvey[] = [];
-
- try {
- for (const surveyPrisma of surveysPrisma) {
const transformedSurvey = {
...surveyPrisma,
triggers: surveyPrisma.triggers.map((trigger) => trigger.eventClass),
};
- const survey = ZSurvey.parse(transformedSurvey);
- surveys.push(survey);
- }
- return surveys;
- } catch (error) {
- if (error instanceof z.ZodError) {
- console.error(JSON.stringify(error.errors, null, 2)); // log the detailed error information
- }
- throw new ValidationError("Data validation of survey failed");
- }
-});
-export const getSurveysWithAnalytics = cache(
- async (environmentId: string): Promise => {
- validateInputs([environmentId, ZId]);
- let surveysPrisma;
- try {
- surveysPrisma = await prisma.survey.findMany({
- where: {
- environmentId,
- },
- select: selectSurveyWithAnalytics,
- });
- } catch (error) {
- if (error instanceof Prisma.PrismaClientKnownRequestError) {
- throw new DatabaseError("Database operation failed");
+ try {
+ const survey = ZSurvey.parse(transformedSurvey);
+ return survey;
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ console.error(JSON.stringify(error.errors, null, 2)); // log the detailed error information
+ }
+ throw new ValidationError("Data validation of survey failed");
}
-
- throw error;
+ },
+ [getSurveyCacheKey(surveyId)],
+ {
+ tags: [getSurveyCacheTag(surveyId)],
+ revalidate: 60 * 30,
}
+ )();
- try {
- const surveys: TSurveyWithAnalytics[] = [];
- for (const { _count, displays, ...surveyPrisma } of surveysPrisma) {
- const numDisplays = displays.length;
- const numDisplaysResponded = displays.filter((item) => item.status === "responded").length;
- const responseRate = numDisplays ? Math.round((numDisplaysResponded / numDisplays) * 100) / 100 : 0;
+ if (!survey) {
+ return null;
+ }
- const transformedSurvey = {
- ...surveyPrisma,
- triggers: surveyPrisma.triggers.map((trigger) => trigger.eventClass),
- analytics: {
- numDisplays,
- responseRate,
- numResponses: _count.responses,
+ // since the unstable_cache function does not support deserialization of dates, we need to manually deserialize them
+ // https://github.com/vercel/next.js/issues/51613
+ return {
+ ...survey,
+ createdAt: new Date(survey.createdAt),
+ updatedAt: new Date(survey.updatedAt),
+ };
+};
+
+export const getSurveys = async (environmentId: string): Promise => {
+ const surveys = await unstable_cache(
+ async () => {
+ validateInputs([environmentId, ZId]);
+ let surveysPrisma;
+ try {
+ surveysPrisma = await prisma.survey.findMany({
+ where: {
+ environmentId,
},
- };
- const survey = ZSurveyWithAnalytics.parse(transformedSurvey);
- surveys.push(survey);
+ select: selectSurvey,
+ });
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
+ throw new DatabaseError("Database operation failed");
+ }
+
+ throw error;
}
- return surveys;
- } catch (error) {
- if (error instanceof z.ZodError) {
- console.error(JSON.stringify(error.errors, null, 2)); // log the detailed error information
+
+ const surveys: TSurvey[] = [];
+
+ try {
+ for (const surveyPrisma of surveysPrisma) {
+ const transformedSurvey = {
+ ...surveyPrisma,
+ triggers: surveyPrisma.triggers.map((trigger) => trigger.eventClass),
+ };
+ const survey = ZSurvey.parse(transformedSurvey);
+ surveys.push(survey);
+ }
+ return surveys;
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ console.error(JSON.stringify(error.errors, null, 2)); // log the detailed error information
+ }
+ throw new ValidationError("Data validation of survey failed");
}
- throw new ValidationError("Data validation of survey failed");
+ },
+ [getSurveysCacheKey(environmentId)],
+ {
+ tags: [getSurveysCacheTag(environmentId)],
+ revalidate: 60 * 30,
}
- }
-);
+ )();
+
+ // since the unstable_cache function does not support deserialization of dates, we need to manually deserialize them
+ // https://github.com/vercel/next.js/issues/51613
+ return surveys.map((survey) => ({
+ ...survey,
+ createdAt: new Date(survey.createdAt),
+ updatedAt: new Date(survey.updatedAt),
+ }));
+};
+
+// TODO: Cache doesn't work for updated displays & responses
+export const getSurveysWithAnalytics = async (environmentId: string): Promise => {
+ const surveysWithAnalytics = await unstable_cache(
+ async () => {
+ validateInputs([environmentId, ZId]);
+ let surveysPrisma;
+ try {
+ surveysPrisma = await prisma.survey.findMany({
+ where: {
+ environmentId,
+ },
+ select: selectSurveyWithAnalytics,
+ });
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
+ throw new DatabaseError("Database operation failed");
+ }
+
+ throw error;
+ }
+
+ try {
+ const surveys: TSurveyWithAnalytics[] = [];
+ for (const { _count, displays, ...surveyPrisma } of surveysPrisma) {
+ const numDisplays = displays.length;
+ const numDisplaysResponded = displays.filter((item) => item.status === "responded").length;
+ const responseRate = numDisplays ? Math.round((numDisplaysResponded / numDisplays) * 100) / 100 : 0;
+
+ const transformedSurvey = {
+ ...surveyPrisma,
+ triggers: surveyPrisma.triggers.map((trigger) => trigger.eventClass),
+ analytics: {
+ numDisplays,
+ responseRate,
+ numResponses: _count.responses,
+ },
+ };
+ const survey = ZSurveyWithAnalytics.parse(transformedSurvey);
+ surveys.push(survey);
+ }
+ return surveys;
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ console.error(JSON.stringify(error.errors, null, 2)); // log the detailed error information
+ }
+ throw new ValidationError("Data validation of survey failed");
+ }
+ },
+ [getSurveysWithAnalyticsCacheKey(environmentId)],
+ {
+ tags: [getSurveysCacheTag(environmentId)], // TODO: add tags for displays and responses
+ }
+ )();
+
+ // since the unstable_cache function does not support deserialization of dates, we need to manually deserialize them
+ // https://github.com/vercel/next.js/issues/51613
+ return surveysWithAnalytics.map((survey) => ({
+ ...survey,
+ createdAt: new Date(survey.createdAt),
+ updatedAt: new Date(survey.updatedAt),
+ }));
+};
export async function updateSurvey(updatedSurvey: Partial): Promise {
const surveyId = updatedSurvey.id;
@@ -432,6 +512,7 @@ export async function updateSurvey(updatedSurvey: Partial): Promise => {
- try {
- const teams = await prisma.team.findMany({
- where: {
- memberships: {
- some: {
- userId,
- },
- },
- },
- select,
- });
+export const getTeamsByUserIdCacheTag = (userId: string) => `users-${userId}-teams`;
+export const getTeamByEnvironmentIdCacheTag = (environmentId: string) => `environments-${environmentId}-team`;
- return teams;
- } catch (error) {
- if (error instanceof Prisma.PrismaClientKnownRequestError) {
- throw new DatabaseError("Database operation failed");
- }
-
- throw error;
- }
-});
-
-export const getTeamByEnvironmentId = cache(async (environmentId: string): Promise => {
- validateInputs([environmentId, ZId]);
- try {
- const team = await prisma.team.findFirst({
- where: {
- products: {
- some: {
- environments: {
+export const getTeamsByUserId = async (userId: string): Promise =>
+ unstable_cache(
+ async () => {
+ try {
+ const teams = await prisma.team.findMany({
+ where: {
+ memberships: {
some: {
- id: environmentId,
+ userId,
},
},
},
- },
- },
- select,
- });
+ select,
+ });
- return team;
- } catch (error) {
- if (error instanceof Prisma.PrismaClientKnownRequestError) {
- throw new DatabaseError("Database operation failed");
+ return teams;
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
+ throw new DatabaseError("Database operation failed");
+ }
+
+ throw error;
+ }
+ },
+ [`users-${userId}-teams`],
+ {
+ tags: [getTeamsByUserIdCacheTag(userId)],
+ revalidate: 30 * 60, // 30 minutes
}
+ )();
- throw error;
- }
-});
+export const getTeamByEnvironmentId = async (environmentId: string): Promise =>
+ unstable_cache(
+ async () => {
+ validateInputs([environmentId, ZId]);
+ try {
+ const team = await prisma.team.findFirst({
+ where: {
+ products: {
+ some: {
+ environments: {
+ some: {
+ id: environmentId,
+ },
+ },
+ },
+ },
+ },
+ select: { ...select, memberships: true }, // include memberships
+ });
-export const updateTeam = async (teamId: string, data: TTeamUpdateInput) => {
+ return team;
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
+ throw new DatabaseError("Database operation failed");
+ }
+
+ throw error;
+ }
+ },
+ [`environments-${environmentId}-team`],
+ {
+ tags: [getTeamByEnvironmentIdCacheTag(environmentId)],
+ revalidate: 30 * 60, // 30 minutes
+ }
+ )();
+
+export const updateTeam = async (teamId: string, data: TTeamUpdateInput): Promise => {
try {
const updatedTeam = await prisma.team.update({
where: {
id: teamId,
},
data,
+ select: { ...select, memberships: true, products: { select: { environments: true } } }, // include memberships & environments
});
- return updatedTeam;
+ // revalidate cache for members
+ updatedTeam?.memberships.forEach((membership) => {
+ revalidateTag(getTeamsByUserIdCacheTag(membership.userId));
+ });
+
+ // revalidate cache for environments
+ updatedTeam?.products.forEach((product) => {
+ product.environments.forEach((environment) => {
+ revalidateTag(getTeamByEnvironmentIdCacheTag(environment.id));
+ });
+ });
+
+ const team = {
+ ...updatedTeam,
+ memberships: undefined,
+ products: undefined,
+ };
+
+ return team;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2016") {
throw new ResourceNotFoundError("Team", teamId);
@@ -107,11 +145,32 @@ export const updateTeam = async (teamId: string, data: TTeamUpdateInput) => {
export const deleteTeam = async (teamId: string) => {
validateInputs([teamId, ZId]);
try {
- await prisma.team.delete({
+ const deletedTeam = await prisma.team.delete({
where: {
id: teamId,
},
+ select: { ...select, memberships: true, products: { select: { environments: true } } }, // include memberships & environments
});
+
+ // revalidate cache for members
+ deletedTeam?.memberships.forEach((membership) => {
+ revalidateTag(getTeamsByUserIdCacheTag(membership.userId));
+ });
+
+ // revalidate cache for environments
+ deletedTeam?.products.forEach((product) => {
+ product.environments.forEach((environment) => {
+ revalidateTag(getTeamByEnvironmentIdCacheTag(environment.id));
+ });
+ });
+
+ const team = {
+ ...deletedTeam,
+ memberships: undefined,
+ products: undefined,
+ };
+
+ return team;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError("Database operation failed");
@@ -121,7 +180,7 @@ export const deleteTeam = async (teamId: string) => {
}
};
-export const createDemoProduct = cache(async (teamId: string) => {
+export const createDemoProduct = async (teamId: string) => {
validateInputs([teamId, ZId]);
const productWithEnvironment = Prisma.validator()({
include: {
@@ -300,4 +359,4 @@ export const createDemoProduct = cache(async (teamId: string) => {
);
return demoProduct;
-});
+};