mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-25 10:30:30 -06:00
feat: Add caching to more services (survey, environment, team, profile) (#835)
* feat: caching in surveys * fix: fixes unstable_cache date parsing error * fix: fixes survey revalidation in displays and responses * fix: fixes survey cache * fix: adds comments * fix: response cache tag * fix cache validation and tag naming * move TSurveysWithAnalytics to TSurveys * add caching to more services --------- Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 <SurveyStarter environmentId={environmentId} environment={environment} product={product} />;
|
||||
@@ -45,7 +44,7 @@ export default async function SurveysList({ environmentId }: { environmentId: st
|
||||
</li>
|
||||
</Link>
|
||||
{surveys
|
||||
.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime())
|
||||
.sort((a, b) => b.updatedAt?.getTime() - a.updatedAt?.getTime())
|
||||
.map((survey) => (
|
||||
<li key={survey.id} className="relative col-span-1 h-56">
|
||||
<div className="delay-50 flex h-full flex-col justify-between rounded-md bg-white shadow transition ease-in-out hover:scale-105">
|
||||
@@ -82,9 +81,6 @@ export default async function SurveysList({ environmentId }: { environmentId: st
|
||||
tooltip
|
||||
environmentId={environmentId}
|
||||
/>
|
||||
<p className="ml-2 text-xs text-slate-400 ">
|
||||
{survey.analytics.numResponses} responses
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
{survey.status === "draft" && (
|
||||
@@ -93,7 +89,7 @@ export default async function SurveysList({ environmentId }: { environmentId: st
|
||||
</div>
|
||||
<SurveyDropDownMenu
|
||||
survey={survey}
|
||||
key={`survey-${survey.id}`}
|
||||
key={`surveys-${survey.id}`}
|
||||
environmentId={environmentId}
|
||||
environment={environment}
|
||||
otherEnvironment={otherEnvironment!}
|
||||
@@ -105,7 +101,7 @@ export default async function SurveysList({ environmentId }: { environmentId: st
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<UsageAttributesUpdater numSurveys={surveys.length} totalSubmissions={totalSubmissions} />
|
||||
<UsageAttributesUpdater numSurveys={surveys.length} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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),
|
||||
];
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -39,6 +39,8 @@ const selectDisplay = {
|
||||
status: true,
|
||||
};
|
||||
|
||||
export const getDisplaysCacheTag = (surveyId: string) => `surveys-${surveyId}-displays`;
|
||||
|
||||
export const createDisplay = async (displayInput: TDisplayInput): Promise<TDisplay> => {
|
||||
validateInputs([displayInput, ZDisplayInput]);
|
||||
try {
|
||||
@@ -71,6 +73,10 @@ export const createDisplay = async (displayInput: TDisplayInput): Promise<TDispl
|
||||
revalidateTag(displayInput.personId);
|
||||
}
|
||||
|
||||
if (displayInput.surveyId) {
|
||||
revalidateTag(getDisplaysCacheTag(displayInput.surveyId));
|
||||
}
|
||||
|
||||
return display;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
|
||||
@@ -7,88 +7,94 @@ import type { TEnvironment, TEnvironmentId, TEnvironmentUpdateInput } from "@for
|
||||
import { populateEnvironment } from "../utils/createDemoProductHelpers";
|
||||
import { ZEnvironment, ZEnvironmentUpdateInput, ZId } from "@formbricks/types/v1/environment";
|
||||
import { validateInputs } from "../utils/validate";
|
||||
import { cache } from "react";
|
||||
import { unstable_cache } from "next/cache";
|
||||
import { unstable_cache, revalidateTag } from "next/cache";
|
||||
|
||||
export const getEnvironment = cache(async (environmentId: string): Promise<TEnvironment | null> => {
|
||||
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<TEnvironment[]> => {
|
||||
validateInputs([productId, ZId]);
|
||||
let productPrisma;
|
||||
try {
|
||||
productPrisma = await prisma.product.findFirst({
|
||||
where: {
|
||||
id: productId,
|
||||
},
|
||||
include: {
|
||||
environments: true,
|
||||
},
|
||||
});
|
||||
export const getEnvironments = async (productId: string): Promise<TEnvironment[]> =>
|
||||
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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
)();
|
||||
|
||||
@@ -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<TProduct[]> => {
|
||||
validateInputs([teamId, ZId]);
|
||||
try {
|
||||
const products = await prisma.product.findMany({
|
||||
where: {
|
||||
teamId,
|
||||
},
|
||||
select: selectProduct,
|
||||
});
|
||||
export const getProducts = async (teamId: string): Promise<TProduct[]> =>
|
||||
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<TProduct | null> => {
|
||||
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<TProduct>
|
||||
});
|
||||
|
||||
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));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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<TProfile | null> => {
|
||||
validateInputs([userId, ZId]);
|
||||
try {
|
||||
const profile = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
select: responseSelection,
|
||||
});
|
||||
export const getProfile = async (userId: string): Promise<TProfile | null> =>
|
||||
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<TProfile | null> =>
|
||||
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<TProfile> => {
|
||||
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<void> => {
|
||||
validateInputs([personId, ZId]);
|
||||
export const deleteProfile = async (userId: string): Promise<void> => {
|
||||
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<void> => {
|
||||
}
|
||||
}
|
||||
|
||||
await deleteUser(personId);
|
||||
revalidateTag(getProfileCacheTag(userId));
|
||||
await deleteUser(userId);
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError("Database operation failed");
|
||||
|
||||
@@ -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<Array<TResponse> | null> => {
|
||||
validateInputs([personId, ZId]);
|
||||
try {
|
||||
@@ -147,6 +150,10 @@ export const createResponse = async (responseInput: Partial<TResponseInput>): 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) {
|
||||
|
||||
@@ -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<TSurveyWithAnalytics | null> => {
|
||||
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<TSurveyWithAnalytics | null> => {
|
||||
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<TSurvey | null> => {
|
||||
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<TSurvey | null> => {
|
||||
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<TSurvey[]> => {
|
||||
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<TSurveyWithAnalytics[]> => {
|
||||
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<TSurvey[]> => {
|
||||
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<TSurveyWithAnalytics[]> => {
|
||||
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<TSurvey>): Promise<TSurvey> {
|
||||
const surveyId = updatedSurvey.id;
|
||||
@@ -432,6 +512,7 @@ export async function updateSurvey(updatedSurvey: Partial<TSurvey>): Promise<TSu
|
||||
};
|
||||
|
||||
revalidateTag(getSurveysCacheTag(modifiedSurvey.environmentId));
|
||||
revalidateTag(getSurveyCacheTag(modifiedSurvey.id));
|
||||
|
||||
return modifiedSurvey;
|
||||
} catch (error) {
|
||||
@@ -452,7 +533,10 @@ export async function deleteSurvey(surveyId: string) {
|
||||
},
|
||||
select: selectSurvey,
|
||||
});
|
||||
|
||||
revalidateTag(getSurveysCacheTag(deletedSurvey.environmentId));
|
||||
revalidateTag(getSurveyCacheTag(surveyId));
|
||||
|
||||
return deletedSurvey;
|
||||
}
|
||||
|
||||
@@ -469,7 +553,9 @@ export async function createSurvey(environmentId: string, surveyBody: any) {
|
||||
},
|
||||
});
|
||||
captureTelemetry("survey created");
|
||||
|
||||
revalidateTag(getSurveysCacheTag(environmentId));
|
||||
revalidateTag(getSurveyCacheTag(survey.id));
|
||||
|
||||
return survey;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/v1/error
|
||||
import { TTeam, TTeamUpdateInput } from "@formbricks/types/v1/teams";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { cache } from "react";
|
||||
import { revalidateTag, unstable_cache } from "next/cache";
|
||||
import {
|
||||
ChurnResponses,
|
||||
ChurnSurvey,
|
||||
@@ -34,67 +34,105 @@ export const select = {
|
||||
stripeCustomerId: true,
|
||||
};
|
||||
|
||||
export const getTeamsByUserId = cache(async (userId: string): Promise<TTeam[]> => {
|
||||
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<TTeam | null> => {
|
||||
validateInputs([environmentId, ZId]);
|
||||
try {
|
||||
const team = await prisma.team.findFirst({
|
||||
where: {
|
||||
products: {
|
||||
some: {
|
||||
environments: {
|
||||
export const getTeamsByUserId = async (userId: string): Promise<TTeam[]> =>
|
||||
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<TTeam | null> =>
|
||||
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<TTeam> => {
|
||||
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<Prisma.ProductArgs>()({
|
||||
include: {
|
||||
@@ -300,4 +359,4 @@ export const createDemoProduct = cache(async (teamId: string) => {
|
||||
);
|
||||
|
||||
return demoProduct;
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user