Compare commits

...

12 Commits

Author SHA1 Message Date
Matti Nannt
c47face662 chore: remove experimental server actions flag (#1484) 2023-10-27 21:12:17 +00:00
Matti Nannt
2361cf4b5a chore: upgrade next.js (#1483) 2023-10-27 20:52:58 +00:00
Matti Nannt
3720c7690d fix: Add ENCRYPTION_KEY to Dockerfile args (#1482) 2023-10-27 20:08:48 +00:00
Matti Nannt
3de073f93a fix: summary loading infinitely (#1481) 2023-10-27 18:27:52 +00:00
Matti Nannt
841b96c5bb chore: improve error logging in responseNote service (#1480) 2023-10-27 16:49:13 +00:00
Rotimi Best
3bb6ce3250 chore: caching for environment, integration, invite, membership, product & profile (#1324)
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2023-10-27 13:31:15 +00:00
Johannes
82c986baa4 update side quests (#1473) 2023-10-27 09:10:22 +00:00
Shreya Ranpariya
d72283df55 doc: update Firecamp and Spark.net in ascending order in oss-friends page (#1425)
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
2023-10-27 08:55:04 +00:00
Sandy-1711
fcfea44d7f fix: [BUG] In Dark Mode Navbar is not Properly Visible #1463 (#1467)
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
2023-10-27 08:42:28 +00:00
Rotimi Best
5f71b91704 chore: session, survey, tagOnResponse, webhook, apiKey caching (#1363) 2023-10-27 07:48:26 +00:00
Rotimi Best
94e872025d chore: caching responseNote and attributeClass service (#1307) 2023-10-27 07:45:39 +00:00
Matti Nannt
7ad7a255b6 fix: docker release github action (#1468) 2023-10-26 13:00:13 +00:00
58 changed files with 1911 additions and 864 deletions

View File

@@ -19,7 +19,7 @@ jobs:
SECRET=$(openssl rand -hex 32)
echo "NEXTAUTH_SECRET=$SECRET" >> $GITHUB_ENV
- name: Generate Random NEXTAUTH_SECRET
- name: Generate Random ENCRYPTION_KEY
run: |
SECRET=$(openssl rand -hex 32)
echo "ENCRYPTION_KEY=$SECRET" >> $GITHUB_ENV
@@ -55,3 +55,4 @@ jobs:
build-args: |
NEXTAUTH_SECRET=${{ env.NEXTAUTH_SECRET }}
DATABASE_URL=${{ env.DATABASE_URL }}
ENCRYPTION_KEY=${{ env.ENCRYPTION_KEY }}

View File

@@ -13,7 +13,7 @@
"dependencies": {
"@formbricks/js": "workspace:*",
"@heroicons/react": "^2.0.18",
"next": "13.5.5",
"next": "14.0.0",
"react": "18.2.0",
"react-dom": "18.2.0"
},

View File

@@ -81,7 +81,7 @@ You should store constants in `packages/lib/constants`
## Types should be in the packages folder
You should store type in `packages/types/v1`
You should store type in `packages/types`
## Read environment variables from `.env.mjs`

View File

@@ -116,7 +116,7 @@ export default function Header() {
}, []);
const stickyNavClass = stickyNav
? `bg-transparent shadow-md backdrop-blur-lg fixed top-0 z-30 w-full`
? `bg-transparent dark:bg-slate-900/[0.8] shadow-md backdrop-blur-lg fixed top-0 z-30 w-full`
: "relative";
return (
<Popover className={`${stickyNavClass}`} as="header">

View File

@@ -52,6 +52,11 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
"Survey granular user segments at any point in the user journey. Gather up to 6x more insights with targeted micro-surveys. All open-source.",
href: "https://formbricks.com",
},
{
name: "Firecamp",
description: "vscode for apis, open-source postman/insomnia alternative",
href: "https://firecamp.io",
},
{
name: "Ghostfolio",
description:
@@ -138,6 +143,12 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
description:
"Sniffnet is a network monitoring tool to help you easily keep track of your Internet traffic.",
href: "https://www.sniffnet.net",
},
{
name: "Spark.NET",
description:
"The .NET Web Framework for Makers. Build production ready, full-stack web applications fast without sweating the small stuff.",
href: "https://spark-framework.net",
},
{
name: "Tolgee",
@@ -173,17 +184,6 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
description: "Webstudio is an open source alternative to Webflow",
href: "https://webstudio.is",
},
{
name: "Spark.NET",
description:
"The .NET Web Framework for Makers. Build production ready, full-stack web applications fast without sweating the small stuff.",
href: "https://spark-framework.net",
},
{
name: "Firecamp",
description: "vscode for apis, open-source postman/insomnia alternative",
href: "https://firecamp.io",
},
],
});
}

View File

@@ -50,21 +50,11 @@ const HowTo = [
];
const SideQuests = [
{
points: "Join the Tribe Tweet (100 Points)",
quest: "Tweet a single “🧱” emoji before the 7th of October EOD to join the #FormTribe.",
proof: "Share the link to the tweet in the “side-quest” channel.",
},
{
points: "Spread the Word Tweet (100 Points)",
quest: "Tweet “🧱🚀” on the day of the ProductHunt launch to spread the word.",
proof: "Share the link to the tweet in the “side-quest” channel.",
},
{
points: "Setup Insights (200 Points)",
quest: "Screen record yourself setting up the Formbricks dev environment.",
proof: "Upload to WeTransfer and send to johannes@formbricks.com",
},
{
points: "Meme Magic (50 Points + up to 100 Points)",
quest:
@@ -82,25 +72,15 @@ const SideQuests = [
quest: "Illustrate a captivating background for survey enthusiasts (more infos on Notion).",
proof: "Share the design in the “side-quest” channel.",
},
{
points: "Transform Animation to CSS (350 Points per background)",
quest: "Animate an existing background to CSS versions (more infos on Notion).",
proof: "Share the animated background.",
},
{
points: "Enhance Docs (50-250 Points)",
quest:
"Add a new section to our docs where you see gaps. Follow the current style of documentation incl. code snippets and screenshots. Pls no spam.",
proof: "Open a PR with “docs” in the title",
},
{
points: "Starry-eyed Supporter (250 Points)",
quest: "Get five friends to star our repository.",
proof: "Share 5 screenshots of the chats where you asked them and they confirmed + their GitHub names",
},
{
points: "Bug Hunter (50-250 Points)",
quest: "Find and report any functionality bugs.",
points: "Bug Hunter (100 Points)",
quest:
"Find and report any bugs in our core product. We will close all bugs on the landing page bc we don't have time for that before the launch :)",
proof: "Open a bug issue in our repository.",
},
{
@@ -109,11 +89,6 @@ const SideQuests = [
"Find someone whose name would be funny as a play on words with “brick”. Then, with the help of AI, create a brick version of this person like Brick Astley, Brickj Minaj, etc. For extra points, tweet it, tag us and score +5 for each like.",
proof: "Share your art or link to the tweet in the “side-quest” channel.",
},
{
points: "SEO Sage (50-250 Points)",
quest: "Provide detailed SEO recommendations or improvements for our main website.",
proof: "Share your insights.",
},
{
points: "Community Connector (50 points each, up to 250 points)",
quest:

View File

@@ -7,6 +7,9 @@ ENV DATABASE_URL=$DATABASE_URL
ARG NEXTAUTH_SECRET
ENV NEXTAUTH_SECRET=$NEXTAUTH_SECRET
ARG ENCRYPTION_KEY
ENV ENCRYPTION_KEY=$ENCRYPTION_KEY
WORKDIR /app
COPY . .

View File

@@ -10,7 +10,7 @@ export default async function MembersSettingsPage({ params }) {
throw new Error("Environment not found");
}
const tags = await getTagsByEnvironmentId(params.environmentId);
const environmentTagsCount = await getTagsOnResponsesCount();
const environmentTagsCount = await getTagsOnResponsesCount(params.environmentId);
return (
<div>

View File

@@ -1,7 +1,7 @@
import { getUpdatedState } from "@/app/api/v1/js/sync/lib/sync";
import { responses } from "@/app/lib/api/response";
import { transformErrorToDetails } from "@/app/lib/api/validator";
import { createAttributeClass, getAttributeClassByNameCached } from "@formbricks/lib/attributeClass/service";
import { createAttributeClass, getAttributeClassByName } from "@formbricks/lib/attributeClass/service";
import { personCache } from "@formbricks/lib/person/cache";
import { getPerson, updatePersonAttribute } from "@formbricks/lib/person/service";
import { ZJsPeopleAttributeInput } from "@formbricks/types/js";
@@ -35,7 +35,7 @@ export async function POST(req: Request, { params }): Promise<NextResponse> {
return responses.notFoundResponse("Person", personId, true);
}
let attributeClass = await getAttributeClassByNameCached(environmentId, key);
let attributeClass = await getAttributeClassByName(environmentId, key);
// create new attribute class if not found
if (attributeClass === null) {

View File

@@ -2,8 +2,10 @@ import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
import { SERVICES_REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
import { displayCache } from "@formbricks/lib/display/cache";
import { getDisplaysByPersonId } from "@formbricks/lib/display/service";
import { getProductByEnvironmentIdCached, getProductCacheTag } from "@formbricks/lib/product/service";
import { getSurveyCacheTag, getSurveys } from "@formbricks/lib/survey/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { productCache } from "@formbricks/lib/product/cache";
import { getSurveys } from "@formbricks/lib/survey/service";
import { surveyCache } from "@formbricks/lib/survey/cache";
import { TSurveyWithTriggers } from "@formbricks/types/js";
import { TPerson } from "@formbricks/types/people";
import { unstable_cache } from "next/cache";
@@ -23,8 +25,8 @@ export const getSyncSurveysCached = (environmentId: string, person: TPerson) =>
{
tags: [
displayCache.tag.byPersonId(person.id),
getSurveyCacheTag(environmentId),
getProductCacheTag(environmentId),
surveyCache.tag.byEnvironmentId(environmentId),
productCache.tag.byEnvironmentId(environmentId),
],
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
@@ -35,7 +37,7 @@ export const getSyncSurveys = async (
person: TPerson
): Promise<TSurveyWithTriggers[]> => {
// get recontactDays from product
const product = await getProductByEnvironmentIdCached(environmentId);
const product = await getProductByEnvironmentId(environmentId);
if (!product) {
throw new Error("Product not found");

View File

@@ -3,8 +3,8 @@ import { MAU_LIMIT } from "@formbricks/lib/constants";
import { getActionClasses } from "@formbricks/lib/actionClass/service";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { createPerson, getMonthlyActivePeopleCount, getPerson } from "@formbricks/lib/person/service";
import { getProductByEnvironmentIdCached } from "@formbricks/lib/product/service";
import { createSession, extendSession, getSessionCached } from "@formbricks/lib/session/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { createSession, extendSession, getSession } from "@formbricks/lib/session/service";
import { captureTelemetry } from "@formbricks/lib/telemetry";
import { TEnvironment } from "@formbricks/types/environment";
import { TJsState } from "@formbricks/types/js";
@@ -41,7 +41,7 @@ export const getUpdatedState = async (
// don't allow new people or sessions
throw new Error(errorMessage);
}
const session = await getSessionCached(sessionId);
const session = await getSession(sessionId);
if (!session) {
// don't allow new sessions
throw new Error(errorMessage);
@@ -74,7 +74,7 @@ export const getUpdatedState = async (
session = await createSession(person.id);
} else {
// check validity of person & session
session = await getSessionCached(sessionId);
session = await getSession(sessionId);
if (!session) {
// create a new session
session = await createSession(person.id);
@@ -102,7 +102,7 @@ export const getUpdatedState = async (
const [surveys, noCodeActionClasses, product] = await Promise.all([
getSyncSurveysCached(environmentId, person),
getActionClasses(environmentId),
getProductByEnvironmentIdCached(environmentId),
getProductByEnvironmentId(environmentId),
]);
if (!product) {

View File

@@ -1,4 +1,5 @@
import { ImageResponse, NextRequest } from "next/server";
import { NextRequest } from "next/server";
import { ImageResponse } from "next/og";
// App router includes @vercel/og.
// No need to install it.

View File

@@ -7,9 +7,6 @@ import { createId } from "@paralleldrive/cuid2";
const nextConfig = {
assetPrefix: process.env.ASSET_PREFIX_URL || undefined,
output: "standalone",
experimental: {
serverActions: true,
},
transpilePackages: ["@formbricks/database", "@formbricks/ee", "@formbricks/ui", "@formbricks/lib"],
images: {
remotePatterns: [

View File

@@ -1,6 +1,6 @@
{
"name": "@formbricks/web",
"version": "1.1.0",
"version": "1.2.1",
"private": true,
"scripts": {
"clean": "rimraf .turbo node_modules .next",
@@ -39,7 +39,7 @@
"lru-cache": "^10.0.1",
"lucide-react": "^0.288.0",
"mime": "^3.0.0",
"next": "13.5.6",
"next": "14.0.0",
"nodemailer": "^6.9.7",
"otplib": "^12.0.1",
"posthog-js": "^1.85.1",

View File

@@ -10,7 +10,7 @@ import { Prisma } from "@prisma/client";
import { revalidateTag, unstable_cache } from "next/cache";
import { actionClassCache } from "../actionClass/cache";
import { ITEMS_PER_PAGE, SERVICES_REVALIDATION_INTERVAL } from "../constants";
import { getSessionCached } from "../session/service";
import { getSession } from "../session/service";
import { createActionClass, getActionClassByEnvironmentIdAndName } from "../actionClass/service";
import { validateInputs } from "../utils/validate";
import { actionCache } from "./cache";
@@ -136,7 +136,7 @@ export const createAction = async (data: TActionInput): Promise<TAction> => {
eventType = "automatic";
}
const session = await getSessionCached(sessionId);
const session = await getSession(sessionId);
if (!session) {
throw new ResourceNotFoundError("Session", sessionId);

View File

@@ -6,6 +6,7 @@ import { hasUserEnvironmentAccess } from "../environment/auth";
import { getApiKey } from "./service";
import { unstable_cache } from "next/cache";
import { SERVICES_REVALIDATION_INTERVAL } from "../constants";
import { apiKeyCache } from "./cache";
export const canUserAccessApiKey = async (userId: string, apiKeyId: string): Promise<boolean> =>
await unstable_cache(
@@ -21,6 +22,6 @@ export const canUserAccessApiKey = async (userId: string, apiKeyId: string): Pro
return true;
},
[`users-${userId}-apiKeys-${apiKeyId}`],
{ revalidate: SERVICES_REVALIDATION_INTERVAL, tags: [`apiKeys-${apiKeyId}`] }
[`canUserAccessApiKey-${userId}-${apiKeyId}`],
{ revalidate: SERVICES_REVALIDATION_INTERVAL, tags: [apiKeyCache.tag.byId(apiKeyId)] }
)();

View File

@@ -0,0 +1,34 @@
import { revalidateTag } from "next/cache";
interface RevalidateProps {
id?: string;
environmentId?: string;
hashedKey?: string;
}
export const apiKeyCache = {
tag: {
byId(id: string) {
return `apiKeys-${id}`;
},
byEnvironmentId(environmentId: string) {
return `environments-${environmentId}-apiKeys`;
},
byHashedKey(hashedKey: string) {
return `apiKeys-${hashedKey}-apiKey`;
},
},
revalidate({ id, environmentId, hashedKey }: RevalidateProps): void {
if (id) {
revalidateTag(this.tag.byId(id));
}
if (environmentId) {
revalidateTag(this.tag.byEnvironmentId(environmentId));
}
if (hashedKey) {
revalidateTag(this.tag.byHashedKey(hashedKey));
}
},
};

View File

@@ -9,55 +9,74 @@ import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbr
import { validateInputs } from "../utils/validate";
import { ZId } from "@formbricks/types/environment";
import { ZString, ZOptionalNumber } from "@formbricks/types/common";
import { ITEMS_PER_PAGE } from "../constants";
import { ITEMS_PER_PAGE, SERVICES_REVALIDATION_INTERVAL } from "../constants";
import { unstable_cache } from "next/cache";
import { apiKeyCache } from "./cache";
export const getApiKey = async (apiKeyId: string): Promise<TApiKey | null> => {
validateInputs([apiKeyId, ZString]);
if (!apiKeyId) {
throw new InvalidInputError("API key cannot be null or undefined.");
}
export const getApiKey = async (apiKeyId: string): Promise<TApiKey | null> =>
unstable_cache(
async () => {
validateInputs([apiKeyId, ZString]);
try {
const apiKeyData = await prisma.apiKey.findUnique({
where: {
id: apiKeyId,
},
});
if (!apiKeyId) {
throw new InvalidInputError("API key cannot be null or undefined.");
}
if (!apiKeyData) {
throw new ResourceNotFoundError("API Key from ID", apiKeyId);
try {
const apiKeyData = await prisma.apiKey.findUnique({
where: {
id: apiKeyId,
},
});
if (!apiKeyData) {
throw new ResourceNotFoundError("API Key from ID", apiKeyId);
}
return apiKeyData;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
},
[`getApiKey-${apiKeyId}`],
{
tags: [apiKeyCache.tag.byId(apiKeyId)],
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
)();
return apiKeyData;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
export const getApiKeys = async (environmentId: string, page?: number): Promise<TApiKey[]> =>
unstable_cache(
async () => {
validateInputs([environmentId, ZId], [page, ZOptionalNumber]);
try {
const apiKeys = await prisma.apiKey.findMany({
where: {
environmentId,
},
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
return apiKeys;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
},
[`getApiKeys-${environmentId}-${page}`],
{
tags: [apiKeyCache.tag.byEnvironmentId(environmentId)],
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
throw error;
}
};
export const getApiKeys = async (environmentId: string, page?: number): Promise<TApiKey[]> => {
validateInputs([environmentId, ZId], [page, ZOptionalNumber]);
try {
const apiKeys = await prisma.apiKey.findMany({
where: {
environmentId,
},
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
return apiKeys;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
};
)();
export const hashApiKey = (key: string): string => createHash("sha256").update(key).digest("hex");
@@ -75,6 +94,12 @@ export async function createApiKey(environmentId: string, apiKeyData: TApiKeyCre
},
});
apiKeyCache.revalidate({
id: result.id,
hashedKey: result.hashedKey,
environmentId: result.environmentId,
});
return { ...result, apiKey: key };
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
@@ -85,30 +110,43 @@ export async function createApiKey(environmentId: string, apiKeyData: TApiKeyCre
}
export const getApiKeyFromKey = async (apiKey: string): Promise<TApiKey | null> => {
validateInputs([apiKey, ZString]);
if (!apiKey) {
throw new InvalidInputError("API key cannot be null or undefined.");
}
const hashedKey = getHash(apiKey);
try {
const apiKeyData = await prisma.apiKey.findUnique({
where: {
hashedKey: getHash(apiKey),
},
});
return unstable_cache(
async () => {
validateInputs([apiKey, ZString]);
return apiKeyData;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
if (!apiKey) {
throw new InvalidInputError("API key cannot be null or undefined.");
}
try {
const apiKeyData = await prisma.apiKey.findUnique({
where: {
hashedKey,
},
});
return apiKeyData;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
},
[`getApiKeyFromKey-${apiKey}`],
{
tags: [apiKeyCache.tag.byHashedKey(hashedKey)],
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
throw error;
}
)();
};
export const deleteApiKey = async (id: string): Promise<TApiKey | null> => {
validateInputs([id, ZId]);
try {
const deletedApiKeyData = await prisma.apiKey.delete({
where: {
@@ -116,6 +154,12 @@ export const deleteApiKey = async (id: string): Promise<TApiKey | null> => {
},
});
apiKeyCache.revalidate({
id: deletedApiKeyData.id,
hashedKey: deletedApiKeyData.hashedKey,
environmentId: deletedApiKeyData.environmentId,
});
return deletedApiKeyData;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {

View File

@@ -0,0 +1,34 @@
import { revalidateTag } from "next/cache";
interface RevalidateProps {
id?: string;
name?: string;
environmentId?: string;
}
export const attributeClassCache = {
tag: {
byId(id: string) {
return `attributeClass-${id}`;
},
byEnvironmentId(environmentId: string) {
return `environments-${environmentId}-attributeClasses`;
},
byEnvironmentIdAndName(environmentId: string, name: string) {
return `environments-${environmentId}-name-${name}-attributeClasses`;
},
},
revalidate({ id, environmentId, name }: RevalidateProps): void {
if (id) {
revalidateTag(this.tag.byId(id));
}
if (environmentId) {
revalidateTag(this.tag.byEnvironmentId(environmentId));
}
if (environmentId && name) {
revalidateTag(this.tag.byEnvironmentIdAndName(environmentId, name));
}
},
};

View File

@@ -7,57 +7,81 @@ import {
TAttributeClassUpdateInput,
ZAttributeClassUpdateInput,
TAttributeClassType,
ZAttributeClassType,
} from "@formbricks/types/attributeClasses";
import { ZId } from "@formbricks/types/environment";
import { validateInputs } from "../utils/validate";
import { DatabaseError } from "@formbricks/types/errors";
import { revalidateTag, unstable_cache } from "next/cache";
import { unstable_cache } from "next/cache";
import { SERVICES_REVALIDATION_INTERVAL, ITEMS_PER_PAGE } from "../constants";
import { ZOptionalNumber } from "@formbricks/types/common";
const attributeClassesCacheTag = (environmentId: string): string =>
`environments-${environmentId}-attributeClasses`;
const getAttributeClassesCacheKey = (environmentId: string): string[] => [
attributeClassesCacheTag(environmentId),
];
import { ZOptionalNumber, ZString } from "@formbricks/types/common";
import { attributeClassCache } from "./cache";
import { formatAttributeClassDateFields } from "./util";
export const getAttributeClass = async (attributeClassId: string): Promise<TAttributeClass | null> => {
validateInputs([attributeClassId, ZId]);
try {
const attributeClass = await prisma.attributeClass.findFirst({
where: {
id: attributeClassId,
},
});
return attributeClass;
} catch (error) {
throw new DatabaseError(`Database error when fetching attributeClass with id ${attributeClassId}`);
const attributeClass = await unstable_cache(
async () => {
validateInputs([attributeClassId, ZId]);
try {
return await prisma.attributeClass.findFirst({
where: {
id: attributeClassId,
},
});
} catch (error) {
throw new DatabaseError(`Database error when fetching attributeClass with id ${attributeClassId}`);
}
},
[`getAttributeClass-${attributeClassId}`],
{
tags: [attributeClassCache.tag.byId(attributeClassId)],
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
)();
if (!attributeClass) {
return null;
}
return formatAttributeClassDateFields(attributeClass);
};
export const getAttributeClasses = async (
environmentId: string,
page?: number
): Promise<TAttributeClass[]> => {
validateInputs([environmentId, ZId], [page, ZOptionalNumber]);
const attributeClasses = await unstable_cache(
async () => {
validateInputs([environmentId, ZId], [page, ZOptionalNumber]);
try {
const attributeClasses = await prisma.attributeClass.findMany({
where: {
environmentId: environmentId,
},
orderBy: {
createdAt: "asc",
},
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
try {
const attributeClasses = await prisma.attributeClass.findMany({
where: {
environmentId: environmentId,
},
orderBy: {
createdAt: "asc",
},
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
return attributeClasses;
} catch (error) {
throw new DatabaseError(`Database error when fetching attributeClasses for environment ${environmentId}`);
}
return attributeClasses;
} catch (error) {
throw new DatabaseError(
`Database error when fetching attributeClasses for environment ${environmentId}`
);
}
},
[`getAttributeClasses-${environmentId}-${page}`],
{
tags: [attributeClassCache.tag.byEnvironmentId(environmentId)],
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
)();
return attributeClasses.map(formatAttributeClassDateFields);
};
export const updatetAttributeClass = async (
@@ -65,6 +89,7 @@ export const updatetAttributeClass = async (
data: Partial<TAttributeClassUpdateInput>
): Promise<TAttributeClass | null> => {
validateInputs([attributeClassId, ZId], [data, ZAttributeClassUpdateInput.partial()]);
try {
const attributeClass = await prisma.attributeClass.update({
where: {
@@ -76,7 +101,11 @@ export const updatetAttributeClass = async (
},
});
revalidateTag(attributeClassesCacheTag(attributeClass.environmentId));
attributeClassCache.revalidate({
id: attributeClass.id,
environmentId: attributeClass.environmentId,
name: attributeClass.name,
});
return attributeClass;
} catch (error) {
@@ -84,36 +113,32 @@ export const updatetAttributeClass = async (
}
};
export const getAttributeClassByNameCached = async (environmentId: string, name: string) =>
export const getAttributeClassByName = async (environmentId: string, name: string) =>
await unstable_cache(
async (): Promise<TAttributeClass | null> => {
return await getAttributeClassByName(environmentId, name);
validateInputs([environmentId, ZId], [name, ZString]);
return await prisma.attributeClass.findFirst({
where: {
environmentId,
name,
},
});
},
[`environments-${environmentId}-attributeClass-${name}`],
[`getAttributeClassByName-${environmentId}-${name}`],
{
tags: getAttributeClassesCacheKey(environmentId),
tags: [attributeClassCache.tag.byEnvironmentIdAndName(environmentId, name)],
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
)();
export const getAttributeClassByName = async (
environmentId: string,
name: string
): Promise<TAttributeClass | null> => {
const attributeClass = await prisma.attributeClass.findFirst({
where: {
environmentId,
name,
},
});
return attributeClass;
};
export const createAttributeClass = async (
environmentId: string,
name: string,
type: TAttributeClassType
): Promise<TAttributeClass | null> => {
validateInputs([environmentId, ZId], [name, ZString], [type, ZAttributeClassType]);
const attributeClass = await prisma.attributeClass.create({
data: {
name,
@@ -125,12 +150,19 @@ export const createAttributeClass = async (
},
},
});
revalidateTag(attributeClassesCacheTag(environmentId));
attributeClassCache.revalidate({
id: attributeClass.id,
environmentId: attributeClass.environmentId,
name: attributeClass.name,
});
return attributeClass;
};
export const deleteAttributeClass = async (attributeClassId: string): Promise<TAttributeClass> => {
validateInputs([attributeClassId, ZId]);
try {
const deletedAttributeClass = await prisma.attributeClass.delete({
where: {
@@ -138,6 +170,12 @@ export const deleteAttributeClass = async (attributeClassId: string): Promise<TA
},
});
attributeClassCache.revalidate({
id: deletedAttributeClass.id,
environmentId: deletedAttributeClass.environmentId,
name: deletedAttributeClass.name,
});
return deletedAttributeClass;
} catch (error) {
throw new DatabaseError(`Database error when deleting webhook with ID ${attributeClassId}`);

View File

@@ -0,0 +1,14 @@
import "server-only";
import { TAttributeClass } from "@formbricks/types/attributeClasses";
export const formatAttributeClassDateFields = (attributeClass: TAttributeClass): TAttributeClass => {
if (typeof attributeClass.createdAt === "string") {
attributeClass.createdAt = new Date(attributeClass.createdAt);
}
if (typeof attributeClass.updatedAt === "string") {
attributeClass.updatedAt = new Date(attributeClass.updatedAt);
}
return attributeClass;
};

View File

@@ -5,8 +5,7 @@ import { prisma } from "@formbricks/database";
import { symmetricDecrypt, symmetricEncrypt } from "../crypto";
import { verifyPassword } from "../auth";
import { totpAuthenticatorCheck } from "../totp";
import { revalidateTag } from "next/cache";
import { getProfileCacheTag } from "../profile/service";
import { profileCache } from "../profile/cache";
import { ENCRYPTION_KEY } from "../constants";
export const setupTwoFactorAuth = async (
@@ -71,10 +70,10 @@ export const setupTwoFactorAuth = async (
return { secret, keyUri, dataUri, backupCodes };
};
export const enableTwoFactorAuth = async (userId: string, code: string) => {
export const enableTwoFactorAuth = async (id: string, code: string) => {
const user = await prisma.user.findUnique({
where: {
id: userId,
id,
},
});
@@ -114,14 +113,16 @@ export const enableTwoFactorAuth = async (userId: string, code: string) => {
await prisma.user.update({
where: {
id: userId,
id,
},
data: {
twoFactorEnabled: true,
},
});
revalidateTag(getProfileCacheTag(userId));
profileCache.revalidate({
id,
});
return {
message: "Two factor authentication enabled",
@@ -134,10 +135,10 @@ type TDisableTwoFactorAuthParams = {
backupCode?: string;
};
export const disableTwoFactorAuth = async (userId: string, params: TDisableTwoFactorAuthParams) => {
export const disableTwoFactorAuth = async (id: string, params: TDisableTwoFactorAuthParams) => {
const user = await prisma.user.findUnique({
where: {
id: userId,
id,
},
});
@@ -211,7 +212,7 @@ export const disableTwoFactorAuth = async (userId: string, params: TDisableTwoFa
await prisma.user.update({
where: {
id: userId,
id,
},
data: {
backupCodes: null,
@@ -220,7 +221,9 @@ export const disableTwoFactorAuth = async (userId: string, params: TDisableTwoFa
},
});
revalidateTag(getProfileCacheTag(userId));
profileCache.revalidate({
id,
});
return {
message: "Two factor authentication disabled",

View File

@@ -3,13 +3,13 @@ import { ZId } from "@formbricks/types/environment";
import { unstable_cache } from "next/cache";
import { validateInputs } from "../utils/validate";
import { SERVICES_REVALIDATION_INTERVAL } from "../constants";
import { getTeamsByUserIdCacheTag } from "../team/service";
import { revalidateTag } from "next/cache";
import { teamCache } from "../team/cache";
export const hasUserEnvironmentAccess = async (userId: string, environmentId: string) => {
return await unstable_cache(
async (): Promise<boolean> => {
validateInputs([userId, ZId], [environmentId, ZId]);
const environment = await prisma.environment.findUnique({
where: {
id: environmentId,
@@ -30,12 +30,14 @@ export const hasUserEnvironmentAccess = async (userId: string, environmentId: st
},
},
});
revalidateTag(getTeamsByUserIdCacheTag(userId));
const environmentUsers = environment?.product.team.memberships.map((member) => member.userId) || [];
return environmentUsers.includes(userId);
},
[`users-${userId}-environments-${environmentId}`],
{ revalidate: SERVICES_REVALIDATION_INTERVAL, tags: [`environments-${environmentId}`] }
[`hasUserEnvironmentAccess-${userId}-${environmentId}`],
{
revalidate: SERVICES_REVALIDATION_INTERVAL,
tags: [teamCache.tag.byEnvironmentId(environmentId), teamCache.tag.byUserId(userId)],
}
)();
};

View File

@@ -0,0 +1,34 @@
import { revalidateTag } from "next/cache";
interface RevalidateProps {
id?: string;
productId?: string;
userId?: string;
}
export const environmentCache = {
tag: {
byId(id: string) {
return `environments-${id}`;
},
byProductId(productId: string) {
return `products-${productId}-environments`;
},
byUserId(userId: string) {
return `users-${userId}-environments`;
},
},
revalidate({ id, productId, userId }: RevalidateProps): void {
if (id) {
revalidateTag(this.tag.byId(id));
}
if (productId) {
revalidateTag(this.tag.byProductId(productId));
}
if (userId) {
revalidateTag(this.tag.byUserId(userId));
}
},
};

View File

@@ -14,14 +14,13 @@ import {
} from "@formbricks/types/environment";
import { DatabaseError, ResourceNotFoundError, ValidationError } from "@formbricks/types/errors";
import { Prisma } from "@prisma/client";
import { revalidateTag, unstable_cache } from "next/cache";
import { unstable_cache } from "next/cache";
import "server-only";
import { z } from "zod";
import { SERVICES_REVALIDATION_INTERVAL } from "../constants";
import { validateInputs } from "../utils/validate";
export const getEnvironmentCacheTag = (environmentId: string) => `environments-${environmentId}`;
export const getEnvironmentsCacheTag = (productId: string) => `products-${productId}-environments`;
import { environmentCache } from "./cache";
import { formatEnvironmentDateFields } from "./util";
export const getEnvironment = (environmentId: string) =>
unstable_cache(
@@ -54,9 +53,9 @@ export const getEnvironment = (environmentId: string) =>
throw new ValidationError("Data validation of environment failed");
}
},
[`environments-${environmentId}`],
[`getEnvironment-${environmentId}`],
{
tags: [getEnvironmentCacheTag(environmentId)],
tags: [environmentCache.tag.byId(environmentId)],
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
)();
@@ -101,9 +100,9 @@ export const getEnvironments = async (productId: string): Promise<TEnvironment[]
throw new ValidationError("Data validation of environments array failed");
}
},
[`products-${productId}-environments`],
[`getEnvironments-${productId}`],
{
tags: [getEnvironmentsCacheTag(productId)],
tags: [environmentCache.tag.byProductId(productId)],
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
)();
@@ -123,8 +122,10 @@ export const updateEnvironment = async (
data: newData,
});
revalidateTag(getEnvironmentsCacheTag(updatedEnvironment.productId));
revalidateTag(getEnvironmentCacheTag(environmentId));
environmentCache.revalidate({
id: environmentId,
productId: updatedEnvironment.productId,
});
return updatedEnvironment;
} catch (error) {
@@ -136,29 +137,40 @@ export const updateEnvironment = async (
};
export const getFirstEnvironmentByUserId = async (userId: string): Promise<TEnvironment | null> => {
validateInputs([userId, ZId]);
try {
return await prisma.environment.findFirst({
where: {
type: "production",
product: {
team: {
memberships: {
some: {
userId,
const environment = await unstable_cache(
async () => {
validateInputs([userId, ZId]);
try {
return await prisma.environment.findFirst({
where: {
type: "production",
product: {
team: {
memberships: {
some: {
userId,
},
},
},
},
},
},
},
});
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
});
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
throw error;
}
},
[`getFirstEnvironmentByUserId-${userId}`],
{
tags: [environmentCache.tag.byUserId(userId)],
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
)();
return environment ? formatEnvironmentDateFields(environment) : environment;
};
export const createEnvironment = async (
@@ -167,7 +179,7 @@ export const createEnvironment = async (
): Promise<TEnvironment> => {
validateInputs([productId, ZId], [environmentInput, ZEnvironmentCreateInput]);
return await prisma.environment.create({
const environment = await prisma.environment.create({
data: {
type: environmentInput.type || "development",
product: { connect: { id: productId } },
@@ -199,4 +211,11 @@ export const createEnvironment = async (
},
},
});
environmentCache.revalidate({
id: environment.id,
productId: environment.productId,
});
return environment;
};

View File

@@ -0,0 +1,14 @@
import "server-only";
import { TEnvironment } from "@formbricks/types/environment";
export const formatEnvironmentDateFields = (environemt: TEnvironment): TEnvironment => {
if (typeof environemt.createdAt === "string") {
environemt.createdAt = new Date(environemt.createdAt);
}
if (typeof environemt.updatedAt === "string") {
environemt.updatedAt = new Date(environemt.updatedAt);
}
return environemt;
};

View File

@@ -0,0 +1,34 @@
import { revalidateTag } from "next/cache";
interface RevalidateProps {
id?: string;
environmentId?: string;
type?: string;
}
export const integrationCache = {
tag: {
byId(id: string) {
return `integrations-${id}`;
},
byEnvironmentId(environmentId: string) {
return `environments-${environmentId}-integrations`;
},
byEnvironmentIdAndType(environmentId: string, type: string) {
return `environments-${environmentId}-type-${type}-integrations`;
},
},
revalidate({ id, environmentId, type }: RevalidateProps): void {
if (id) {
revalidateTag(this.tag.byId(id));
}
if (environmentId) {
revalidateTag(this.tag.byEnvironmentId(environmentId));
}
if (environmentId && type) {
revalidateTag(this.tag.byEnvironmentIdAndType(environmentId, type));
}
},
};

View File

@@ -7,7 +7,9 @@ import { ZId } from "@formbricks/types/environment";
import { TIntegration, TIntegrationInput, ZIntegrationType } from "@formbricks/types/integration";
import { validateInputs } from "../utils/validate";
import { ZString, ZOptionalNumber } from "@formbricks/types/common";
import { ITEMS_PER_PAGE } from "../constants";
import { ITEMS_PER_PAGE, SERVICES_REVALIDATION_INTERVAL } from "../constants";
import { integrationCache } from "./cache";
import { unstable_cache } from "next/cache";
export async function createOrUpdateIntegration(
environmentId: string,
@@ -32,6 +34,10 @@ export async function createOrUpdateIntegration(
environment: { connect: { id: environmentId } },
},
});
integrationCache.revalidate({
environmentId,
});
return integration;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
@@ -42,66 +48,87 @@ export async function createOrUpdateIntegration(
}
}
export const getIntegrations = async (environmentId: string, page?: number): Promise<TIntegration[]> => {
validateInputs([environmentId, ZId], [page, ZOptionalNumber]);
export const getIntegrations = async (environmentId: string, page?: number): Promise<TIntegration[]> =>
unstable_cache(
async () => {
validateInputs([environmentId, ZId], [page, ZOptionalNumber]);
try {
const result = await prisma.integration.findMany({
where: {
environmentId,
},
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
return result;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
try {
const result = await prisma.integration.findMany({
where: {
environmentId,
},
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
return result;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
},
[`getIntegrations-${environmentId}-${page}`],
{
tags: [integrationCache.tag.byEnvironmentId(environmentId)],
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
throw error;
}
};
)();
export const getIntegration = async (integrationId: string): Promise<TIntegration | null> => {
try {
const result = await prisma.integration.findUnique({
where: {
id: integrationId,
},
});
return result;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
};
export const getIntegration = async (integrationId: string): Promise<TIntegration | null> =>
unstable_cache(
async () => {
try {
const result = await prisma.integration.findUnique({
where: {
id: integrationId,
},
});
return result;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
},
[`getIntegration-${integrationId}`],
{ tags: [integrationCache.tag.byId(integrationId)], revalidate: SERVICES_REVALIDATION_INTERVAL }
)();
export const getIntegrationByType = async (
environmentId: string,
type: TIntegrationInput["type"]
): Promise<TIntegration | null> => {
validateInputs([environmentId, ZId], [type, ZIntegrationType]);
): Promise<TIntegration | null> =>
unstable_cache(
async () => {
validateInputs([environmentId, ZId], [type, ZIntegrationType]);
try {
const result = await prisma.integration.findUnique({
where: {
type_environmentId: {
environmentId,
type,
},
},
});
try {
const result = await prisma.integration.findUnique({
where: {
type_environmentId: {
environmentId,
type,
},
},
});
return result;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
return result;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
},
[`getIntegrationByType-${environmentId}-${type}`],
{
tags: [integrationCache.tag.byEnvironmentIdAndType(environmentId, type)],
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
throw error;
}
};
)();
export const deleteIntegration = async (integrationId: string): Promise<TIntegration> => {
validateInputs([integrationId, ZString]);
@@ -113,6 +140,12 @@ export const deleteIntegration = async (integrationId: string): Promise<TIntegra
},
});
integrationCache.revalidate({
id: integrationData.id,
environmentId: integrationData.environmentId,
type: integrationData.type,
});
return integrationData;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {

View File

@@ -0,0 +1,26 @@
import { revalidateTag } from "next/cache";
interface RevalidateProps {
id?: string;
teamId?: string;
}
export const inviteCache = {
tag: {
byId(id: string) {
return `invites-${id}`;
},
byTeamId(teamId: string) {
return `teams-${teamId}-invites`;
},
},
revalidate({ id, teamId }: RevalidateProps): void {
if (id) {
revalidateTag(this.tag.byId(id));
}
if (teamId) {
revalidateTag(this.tag.byTeamId(teamId));
}
},
};

View File

@@ -15,7 +15,11 @@ import { ResourceNotFoundError, ValidationError, DatabaseError } from "@formbric
import { ZString, ZOptionalNumber } from "@formbricks/types/common";
import { sendInviteMemberEmail } from "../emails/emails";
import { validateInputs } from "../utils/validate";
import { ITEMS_PER_PAGE } from "../constants";
import { ITEMS_PER_PAGE, SERVICES_REVALIDATION_INTERVAL } from "../constants";
import { unstable_cache } from "next/cache";
import { inviteCache } from "./cache";
import { formatInviteDateFields } from "./util";
import { getMembershipByUserIdTeamId } from "../membership/service";
const inviteSelect = {
id: true,
@@ -31,16 +35,25 @@ const inviteSelect = {
};
export const getInvitesByTeamId = async (teamId: string, page?: number): Promise<TInvite[] | null> => {
validateInputs([teamId, ZString], [page, ZOptionalNumber]);
const invites = await unstable_cache(
async () => {
validateInputs([teamId, ZString], [page, ZOptionalNumber]);
const invites = await prisma.invite.findMany({
where: { teamId },
select: inviteSelect,
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
return prisma.invite.findMany({
where: { teamId },
select: inviteSelect,
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
},
[`getInvitesByTeamId-${teamId}-${page}`],
{
tags: [inviteCache.tag.byTeamId(teamId)],
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
)();
return invites;
return invites.map(formatInviteDateFields);
};
export const updateInvite = async (inviteId: string, data: TInviteUpdateInput): Promise<TInvite | null> => {
@@ -53,6 +66,15 @@ export const updateInvite = async (inviteId: string, data: TInviteUpdateInput):
select: inviteSelect,
});
if (invite === null) {
throw new ResourceNotFoundError("Invite", inviteId);
}
inviteCache.revalidate({
id: invite.id,
teamId: invite.teamId,
});
return invite;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2016") {
@@ -77,6 +99,11 @@ export const deleteInvite = async (inviteId: string): Promise<TInvite> => {
throw new ResourceNotFoundError("Invite", inviteId);
}
inviteCache.revalidate({
id: invite.id,
teamId: invite.teamId,
});
return invite;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
@@ -87,27 +114,32 @@ export const deleteInvite = async (inviteId: string): Promise<TInvite> => {
}
};
export const getInvite = async (inviteId: string): Promise<{ inviteId: string; email: string }> => {
validateInputs([inviteId, ZString]);
export const getInvite = async (inviteId: string): Promise<{ inviteId: string; email: string }> =>
unstable_cache(
async () => {
validateInputs([inviteId, ZString]);
const invite = await prisma.invite.findUnique({
where: {
id: inviteId,
const invite = await prisma.invite.findUnique({
where: {
id: inviteId,
},
select: {
email: true,
},
});
if (!invite) {
throw new ResourceNotFoundError("Invite", inviteId);
}
return {
inviteId,
email: invite.email,
};
},
select: {
email: true,
},
});
if (!invite) {
throw new ResourceNotFoundError("Invite", inviteId);
}
return {
inviteId,
email: invite.email,
};
};
[`getInvite-${inviteId}`],
{ tags: [inviteCache.tag.byId(inviteId)], revalidate: SERVICES_REVALIDATION_INTERVAL }
)();
export const resendInvite = async (inviteId: string): Promise<TInvite> => {
validateInputs([inviteId, ZString]);
@@ -137,6 +169,11 @@ export const resendInvite = async (inviteId: string): Promise<TInvite> => {
},
});
inviteCache.revalidate({
id: updatedInvite.id,
teamId: updatedInvite.teamId,
});
return updatedInvite;
};
@@ -162,11 +199,8 @@ export const inviteUser = async ({
const user = await prisma.user.findUnique({ where: { email } });
if (user) {
const member = await prisma.membership.findUnique({
where: {
userId_teamId: { teamId, userId: user.id },
},
});
const member = await getMembershipByUserIdTeamId(user.id, teamId);
if (member) {
throw new ValidationError("User is already a member of this team");
}
@@ -189,5 +223,10 @@ export const inviteUser = async ({
await sendInviteMemberEmail(invite.id, email, currentUserName, name);
inviteCache.revalidate({
id: invite.id,
teamId: invite.teamId,
});
return invite;
};

View File

@@ -0,0 +1,14 @@
import "server-only";
import { TInvite } from "@formbricks/types/invites";
export const formatInviteDateFields = (invite: TInvite): TInvite => {
if (typeof invite.createdAt === "string") {
invite.createdAt = new Date(invite.createdAt);
}
if (typeof invite.expiresAt === "string") {
invite.expiresAt = new Date(invite.expiresAt);
}
return invite;
};

View File

@@ -0,0 +1,26 @@
import { revalidateTag } from "next/cache";
interface RevalidateProps {
userId?: string;
teamId?: string;
}
export const membershipCache = {
tag: {
byTeamId(teamId: string) {
return `teams-${teamId}-memberships`;
},
byUserId(userId: string) {
return `users-${userId}-memberships`;
},
},
revalidate({ teamId, userId }: RevalidateProps): void {
if (teamId) {
revalidateTag(this.tag.byTeamId(teamId));
}
if (userId) {
revalidateTag(this.tag.byUserId(userId));
}
},
};

View File

@@ -12,76 +12,101 @@ import {
import { Prisma } from "@prisma/client";
import { validateInputs } from "../utils/validate";
import { ZString, ZOptionalNumber } from "@formbricks/types/common";
import { getTeamsByUserIdCacheTag } from "../team/service";
import { revalidateTag } from "next/cache";
import { ITEMS_PER_PAGE } from "../constants";
import { ITEMS_PER_PAGE, SERVICES_REVALIDATION_INTERVAL } from "../constants";
import { unstable_cache } from "next/cache";
import { membershipCache } from "./cache";
import { teamCache } from "../team/cache";
export const getMembersByTeamId = async (teamId: string, page?: number): Promise<TMember[]> => {
validateInputs([teamId, ZString], [page, ZOptionalNumber]);
export const getMembersByTeamId = async (teamId: string, page?: number): Promise<TMember[]> =>
unstable_cache(
async () => {
validateInputs([teamId, ZString], [page, ZOptionalNumber]);
const membersData = await prisma.membership.findMany({
where: { teamId },
select: {
user: {
const membersData = await prisma.membership.findMany({
where: { teamId },
select: {
name: true,
email: true,
user: {
select: {
name: true,
email: true,
},
},
userId: true,
accepted: true,
role: true,
},
},
userId: true,
accepted: true,
role: true,
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
const members = membersData.map((member) => {
return {
name: member.user?.name || "",
email: member.user?.email || "",
userId: member.userId,
accepted: member.accepted,
role: member.role,
};
});
return members;
},
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
const members = membersData.map((member) => {
return {
name: member.user?.name || "",
email: member.user?.email || "",
userId: member.userId,
accepted: member.accepted,
role: member.role,
};
});
return members;
};
[`getMembersByTeamId-${teamId}-${page}`],
{
tags: [membershipCache.tag.byTeamId(teamId)],
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
)();
export const getMembershipByUserIdTeamId = async (
userId: string,
teamId: string
): Promise<TMembership | null> => {
validateInputs([userId, ZString], [teamId, ZString]);
): Promise<TMembership | null> =>
unstable_cache(
async () => {
validateInputs([userId, ZString], [teamId, ZString]);
const membership = await prisma.membership.findUnique({
where: {
userId_teamId: {
userId,
teamId,
},
const membership = await prisma.membership.findUnique({
where: {
userId_teamId: {
userId,
teamId,
},
},
});
if (!membership) return null;
return membership;
},
});
[`getMembershipByUserIdTeamId-${userId}-${teamId}`],
{
tags: [membershipCache.tag.byUserId(userId), membershipCache.tag.byTeamId(teamId)],
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
)();
if (!membership) return null;
export const getMembershipsByUserId = async (userId: string, page?: number): Promise<TMembership[]> =>
unstable_cache(
async () => {
validateInputs([userId, ZString], [page, ZOptionalNumber]);
return membership;
};
const memberships = await prisma.membership.findMany({
where: {
userId,
},
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
export const getMembershipsByUserId = async (userId: string, page?: number): Promise<TMembership[]> => {
validateInputs([userId, ZString], [page, ZOptionalNumber]);
const memberships = await prisma.membership.findMany({
where: {
userId,
return memberships;
},
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
return memberships;
};
[`getMembershipsByUserId-${userId}-${page}`],
{
tags: [membershipCache.tag.byUserId(userId)],
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
)();
export const createMembership = async (
teamId: string,
@@ -89,6 +114,7 @@ export const createMembership = async (
data: Partial<TMembership>
): Promise<TMembership> => {
validateInputs([teamId, ZString], [userId, ZString], [data, ZMembership.partial()]);
try {
const membership = await prisma.membership.create({
data: {
@@ -98,13 +124,21 @@ export const createMembership = async (
role: data.role as TMembership["role"],
},
});
revalidateTag(getTeamsByUserIdCacheTag(userId));
teamCache.revalidate({
userId,
});
membershipCache.revalidate({
userId,
teamId,
});
return membership;
} catch (error) {
throw error;
}
};
export const updateMembership = async (
userId: string,
teamId: string,
@@ -122,7 +156,15 @@ export const updateMembership = async (
},
data,
});
revalidateTag(getTeamsByUserIdCacheTag(userId));
teamCache.revalidate({
userId,
});
membershipCache.revalidate({
userId,
teamId,
});
return membership;
} catch (error) {
@@ -145,7 +187,15 @@ export const deleteMembership = async (userId: string, teamId: string): Promise<
},
},
});
revalidateTag(getTeamsByUserIdCacheTag(userId));
teamCache.revalidate({
userId,
});
membershipCache.revalidate({
userId,
teamId,
});
return deletedMembership;
};
@@ -182,7 +232,17 @@ export const transferOwnership = async (
},
}),
]);
revalidateTag(getTeamsByUserIdCacheTag(teamId));
memberships.forEach((membership) => {
teamCache.revalidate({
userId: membership.userId,
});
membershipCache.revalidate({
userId: membership.userId,
teamId: membership.teamId,
});
});
return memberships;
} catch (error) {

View File

@@ -1,9 +1,10 @@
import { ZId } from "@formbricks/types/environment";
import { validateInputs } from "../utils/validate";
import { getProduct, getProductCacheTag } from "./service";
import { getProduct } from "./service";
import { unstable_cache } from "next/cache";
import { getTeamsByUserId } from "../team/service";
import { SERVICES_REVALIDATION_INTERVAL } from "../constants";
import { productCache } from "./cache";
export const canUserAccessProduct = async (userId: string, productId: string): Promise<boolean> =>
await unstable_cache(
@@ -18,6 +19,9 @@ export const canUserAccessProduct = async (userId: string, productId: string): P
const teamIds = (await getTeamsByUserId(userId)).map((team) => team.id);
return teamIds.includes(product.teamId);
},
[`users-${userId}-products-${productId}`],
{ revalidate: SERVICES_REVALIDATION_INTERVAL, tags: [getProductCacheTag(productId)] }
[`canUserAccessProduct-${userId}-${productId}`],
{
revalidate: SERVICES_REVALIDATION_INTERVAL,
tags: [productCache.tag.byId(productId), productCache.tag.byUserId(userId)],
}
)();

View File

@@ -0,0 +1,42 @@
import { revalidateTag } from "next/cache";
interface RevalidateProps {
id?: string;
userId?: string;
teamId?: string;
environmentId?: string;
}
export const productCache = {
tag: {
byId(id: string) {
return `product-${id}`;
},
byUserId(userId: string) {
return `users-${userId}-products`;
},
byTeamId(teamId: string) {
return `teams-${teamId}-products`;
},
byEnvironmentId(environmentId: string) {
return `environments-${environmentId}-products`;
},
},
revalidate({ id, userId, teamId, environmentId }: RevalidateProps): void {
if (id) {
revalidateTag(this.tag.byId(id));
}
if (teamId) {
revalidateTag(this.tag.byTeamId(teamId));
}
if (environmentId) {
revalidateTag(this.tag.byEnvironmentId(environmentId));
}
if (userId) {
revalidateTag(this.tag.byUserId(userId));
}
},
};

View File

@@ -6,17 +6,15 @@ import { DatabaseError, ValidationError } from "@formbricks/types/errors";
import type { TProduct, TProductUpdateInput } from "@formbricks/types/product";
import { ZProduct, ZProductUpdateInput } from "@formbricks/types/product";
import { Prisma } from "@prisma/client";
import { revalidateTag, unstable_cache } from "next/cache";
import { unstable_cache } from "next/cache";
import { z } from "zod";
import { SERVICES_REVALIDATION_INTERVAL, ITEMS_PER_PAGE, IS_S3_CONFIGURED } from "../constants";
import { validateInputs } from "../utils/validate";
import { createEnvironment, getEnvironmentCacheTag, getEnvironmentsCacheTag } from "../environment/service";
import { ZOptionalNumber } from "@formbricks/types/common";
import { createEnvironment } from "../environment/service";
import { environmentCache } from "../environment/cache";
import { ZOptionalNumber, ZString } from "@formbricks/types/common";
import { deleteLocalFilesByEnvironmentId, deleteS3FilesByEnvironmentId } from "../storage/service";
export const getProductsCacheTag = (teamId: string): string => `teams-${teamId}-products`;
export const getProductCacheTag = (environmentId: string): string => `environments-${environmentId}-product`;
const getProductCacheKey = (environmentId: string): string[] => [getProductCacheTag(environmentId)];
import { productCache } from "./cache";
const selectProduct = {
id: true,
@@ -58,49 +56,44 @@ export const getProducts = async (teamId: string, page?: number): Promise<TProdu
throw error;
}
},
[`teams-${teamId}-products`],
[`getProducts-${teamId}-${page}`],
{
tags: [getProductsCacheTag(teamId)],
tags: [productCache.tag.byTeamId(teamId)],
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
)();
export const getProductByEnvironmentId = async (environmentId: string): Promise<TProduct | null> => {
if (!environmentId) {
throw new ValidationError("EnvironmentId is required");
}
let productPrisma;
try {
productPrisma = await prisma.product.findFirst({
where: {
environments: {
some: {
id: environmentId,
},
},
},
select: selectProduct,
});
return productPrisma;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
console.error(error);
throw new DatabaseError(error.message);
}
throw error;
}
};
export const getProductByEnvironmentIdCached = (environmentId: string): Promise<TProduct | null> =>
export const getProductByEnvironmentId = async (environmentId: string): Promise<TProduct | null> =>
unstable_cache(
async () => {
return await getProductByEnvironmentId(environmentId);
validateInputs([environmentId, ZId]);
let productPrisma;
try {
productPrisma = await prisma.product.findFirst({
where: {
environments: {
some: {
id: environmentId,
},
},
},
select: selectProduct,
});
return productPrisma;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
console.error(error);
throw new DatabaseError(error.message);
}
throw error;
}
},
getProductCacheKey(environmentId),
[`getProductByEnvironmentId-${environmentId}`],
{
tags: getProductCacheKey(environmentId),
tags: [productCache.tag.byEnvironmentId(environmentId)],
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
)();
@@ -110,6 +103,7 @@ export const updateProduct = async (
inputProduct: Partial<TProductUpdateInput>
): Promise<TProduct> => {
validateInputs([productId, ZId], [inputProduct, ZProductUpdateInput.partial()]);
const { environments, ...data } = inputProduct;
let updatedProduct;
try {
@@ -134,10 +128,16 @@ export const updateProduct = async (
try {
const product = ZProduct.parse(updatedProduct);
revalidateTag(getProductsCacheTag(product.teamId));
productCache.revalidate({
id: product.id,
teamId: product.teamId,
});
product.environments.forEach((environment) => {
// revalidate environment cache
revalidateTag(getProductCacheTag(environment.id));
productCache.revalidate({
environmentId: environment.id,
});
});
return product;
@@ -149,24 +149,32 @@ export const updateProduct = async (
}
};
export const getProduct = async (productId: string): Promise<TProduct | null> => {
let productPrisma;
try {
productPrisma = await prisma.product.findUnique({
where: {
id: productId,
},
select: selectProduct,
});
export const getProduct = async (productId: string): Promise<TProduct | null> =>
unstable_cache(
async () => {
let productPrisma;
try {
productPrisma = await prisma.product.findUnique({
where: {
id: productId,
},
select: selectProduct,
});
return productPrisma;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
return productPrisma;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
},
[`getProduct-${productId}`],
{
tags: [productCache.tag.byId(productId)],
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
throw error;
}
};
)();
export const deleteProduct = async (productId: string): Promise<TProduct> => {
const product = await prisma.product.delete({
@@ -203,12 +211,23 @@ export const deleteProduct = async (productId: string): Promise<TProduct> => {
}
}
revalidateTag(getProductsCacheTag(product.teamId));
revalidateTag(getEnvironmentsCacheTag(product.id));
productCache.revalidate({
id: product.id,
teamId: product.teamId,
});
environmentCache.revalidate({
productId: product.id,
});
product.environments.forEach((environment) => {
// revalidate product cache
revalidateTag(getProductCacheTag(environment.id));
revalidateTag(getEnvironmentCacheTag(environment.id));
productCache.revalidate({
environmentId: environment.id,
});
environmentCache.revalidate({
id: environment.id,
});
});
}
@@ -219,6 +238,8 @@ export const createProduct = async (
teamId: string,
productInput: Partial<TProductUpdateInput>
): Promise<TProduct> => {
validateInputs([teamId, ZString], [productInput, ZProductUpdateInput.partial()]);
if (!productInput.name) {
throw new ValidationError("Product Name is required");
}

View File

@@ -0,0 +1,26 @@
import { revalidateTag } from "next/cache";
interface RevalidateProps {
id?: string;
email?: string;
}
export const profileCache = {
tag: {
byId(id: string) {
return `profiles-${id}`;
},
byEmail(email: string) {
return `profiles-${email}`;
},
},
revalidate({ id, email }: RevalidateProps): void {
if (id) {
revalidateTag(this.tag.byId(id));
}
if (email) {
revalidateTag(this.tag.byEmail(email));
}
},
};

View File

@@ -3,7 +3,7 @@ import "server-only";
import { prisma } from "@formbricks/database";
import { ZId } from "@formbricks/types/environment";
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
import { TMembership, TMembershipRole, ZMembershipRole } from "@formbricks/types/memberships";
import { TMembership } from "@formbricks/types/memberships";
import {
TProfile,
TProfileCreateInput,
@@ -11,11 +11,13 @@ import {
ZProfileUpdateInput,
} from "@formbricks/types/profile";
import { Prisma } from "@prisma/client";
import { revalidateTag, unstable_cache } from "next/cache";
import { unstable_cache } from "next/cache";
import { z } from "zod";
import { SERVICES_REVALIDATION_INTERVAL } from "../constants";
import { deleteTeam } from "../team/service";
import { validateInputs } from "../utils/validate";
import { profileCache } from "./cache";
import { updateMembership } from "../membership/service";
const responseSelection = {
id: true,
@@ -29,18 +31,16 @@ const responseSelection = {
objective: 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 = async (userId: string): Promise<TProfile | null> =>
export const getProfile = async (id: string): Promise<TProfile | null> =>
unstable_cache(
async () => {
validateInputs([userId, ZId]);
validateInputs([id, ZId]);
try {
const profile = await prisma.user.findUnique({
where: {
id: userId,
id,
},
select: responseSelection,
});
@@ -58,9 +58,9 @@ export const getProfile = async (userId: string): Promise<TProfile | null> =>
throw error;
}
},
[`profiles-${userId}`],
[`getProfile-${id}`],
{
tags: [getProfileByEmailCacheTag(userId)],
tags: [profileCache.tag.byId(id)],
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
)();
@@ -69,6 +69,7 @@ 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: {
@@ -90,28 +91,13 @@ export const getProfileByEmail = async (email: string): Promise<TProfile | null>
throw error;
}
},
[`profiles-${email}`],
[`getProfileByEmail-${email}`],
{
tags: [getProfileCacheTag(email)],
tags: [profileCache.tag.byEmail(email)],
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
)();
const updateUserMembership = async (teamId: string, userId: string, role: TMembershipRole) => {
validateInputs([teamId, ZId], [userId, ZId], [role, ZMembershipRole]);
await prisma.membership.update({
where: {
userId_teamId: {
userId,
teamId,
},
},
data: {
role,
},
});
};
const getAdminMemberships = (memberships: TMembership[]): TMembership[] =>
memberships.filter((membership) => membership.role === "admin");
@@ -121,6 +107,7 @@ export const updateProfile = async (
data: Partial<TProfileUpdateInput>
): Promise<TProfile> => {
validateInputs([personId, ZId], [data, ZProfileUpdateInput.partial()]);
try {
const updatedProfile = await prisma.user.update({
where: {
@@ -130,8 +117,10 @@ export const updateProfile = async (
select: responseSelection,
});
revalidateTag(getProfileByEmailCacheTag(updatedProfile.email));
revalidateTag(getProfileCacheTag(personId));
profileCache.revalidate({
email: updatedProfile.email,
id: updatedProfile.id,
});
return updatedProfile;
} catch (error) {
@@ -143,40 +132,48 @@ export const updateProfile = async (
}
};
const deleteUser = async (userId: string): Promise<TProfile> => {
validateInputs([userId, ZId]);
const deleteUser = async (id: string): Promise<TProfile> => {
validateInputs([id, ZId]);
const profile = await prisma.user.delete({
where: {
id: userId,
id,
},
select: responseSelection,
});
revalidateTag(getProfileByEmailCacheTag(profile.email));
revalidateTag(getProfileCacheTag(userId));
profileCache.revalidate({
email: profile.email,
id,
});
return profile;
};
export const createProfile = async (data: TProfileCreateInput): Promise<TProfile> => {
validateInputs([data, ZProfileUpdateInput]);
const profile = await prisma.user.create({
data: data,
select: responseSelection,
});
revalidateTag(getProfileByEmailCacheTag(profile.email));
revalidateTag(getProfileCacheTag(profile.id));
profileCache.revalidate({
email: profile.email,
id: profile.id,
});
return profile;
};
// function to delete a user's profile including teams
export const deleteProfile = async (userId: string): Promise<TProfile> => {
validateInputs([userId, ZId]);
export const deleteProfile = async (id: string): Promise<TProfile> => {
validateInputs([id, ZId]);
try {
const currentUserMemberships = await prisma.membership.findMany({
where: {
userId: userId,
userId: id,
},
include: {
team: {
@@ -203,15 +200,13 @@ export const deleteProfile = async (userId: string): Promise<TProfile> => {
await deleteTeam(teamId);
} else if (currentUserIsTeamOwner && teamHasAtLeastOneAdmin) {
const firstAdmin = teamAdminMemberships[0];
await updateUserMembership(teamId, firstAdmin.userId, "owner");
await updateMembership(firstAdmin.userId, teamId, { role: "owner" });
} else if (currentUserIsTeamOwner) {
await deleteTeam(teamId);
}
}
revalidateTag(getProfileCacheTag(userId));
const deletedProfile = await deleteUser(userId);
const deletedProfile = await deleteUser(id);
return deletedProfile;
} catch (error) {

View File

@@ -1,21 +1,21 @@
import { revalidateTag } from "next/cache";
interface RevalidateProps {
id?: string;
environmentId?: string;
personId?: string;
id?: string;
singleUseId?: string;
surveyId?: string;
}
export const responseCache = {
tag: {
byEnvironmentId(environmentId: string) {
return `environments-${environmentId}-responses`;
},
byId(responseId: string) {
return `responses-${responseId}`;
},
byEnvironmentId(environmentId: string) {
return `environments-${environmentId}-responses`;
},
byPersonId(personId: string) {
return `people-${personId}-responses`;
},

View File

@@ -1,6 +1,10 @@
import "server-only";
import { prisma } from "@formbricks/database";
import { ZOptionalNumber, ZString } from "@formbricks/types/common";
import { ZId } from "@formbricks/types/environment";
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
import { TPerson } from "@formbricks/types/people";
import {
TResponse,
TResponseInput,
@@ -8,20 +12,18 @@ import {
ZResponseInput,
ZResponseUpdateInput,
} from "@formbricks/types/responses";
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
import { TPerson } from "@formbricks/types/people";
import { TTag } from "@formbricks/types/tags";
import { Prisma } from "@prisma/client";
import { unstable_cache } from "next/cache";
import { ITEMS_PER_PAGE, SERVICES_REVALIDATION_INTERVAL } from "../constants";
import { deleteDisplayByResponseId } from "../display/service";
import { getPerson, transformPrismaPerson } from "../person/service";
import { formatResponseDateFields } from "../response/util";
import { responseNoteCache } from "../responseNote/cache";
import { getResponseNotes } from "../responseNote/service";
import { captureTelemetry } from "../telemetry";
import { validateInputs } from "../utils/validate";
import { ZId } from "@formbricks/types/environment";
import { unstable_cache } from "next/cache";
import { deleteDisplayByResponseId } from "../display/service";
import { ZString, ZOptionalNumber } from "@formbricks/types/common";
import { ITEMS_PER_PAGE, SERVICES_REVALIDATION_INTERVAL } from "../constants";
import { responseCache } from "./cache";
import { formatResponseDateFields } from "../response/util";
const responseSelection = {
id: true,
@@ -51,6 +53,19 @@ const responseSelection = {
},
},
},
tags: {
select: {
tag: {
select: {
id: true,
createdAt: true,
updatedAt: true,
name: true,
environmentId: true,
},
},
},
},
notes: {
select: {
id: true,
@@ -67,19 +82,6 @@ const responseSelection = {
isEdited: true,
},
},
tags: {
select: {
tag: {
select: {
id: true,
createdAt: true,
updatedAt: true,
name: true,
environmentId: true,
},
},
},
},
};
export const getResponsesByPersonId = async (
@@ -229,11 +231,15 @@ export const createResponse = async (responseInput: TResponseInput): Promise<TRe
};
responseCache.revalidate({
personId: response.person?.id,
id: response.id,
personId: response.person?.id,
surveyId: response.surveyId,
});
responseNoteCache.revalidate({
responseId: response.id,
});
return response;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
@@ -277,7 +283,10 @@ export const getResponse = async (responseId: string): Promise<TResponse | null>
}
},
[`getResponse-${responseId}`],
{ tags: [responseCache.tag.byId(responseId)], revalidate: SERVICES_REVALIDATION_INTERVAL }
{
tags: [responseCache.tag.byId(responseId), responseNoteCache.tag.byResponseId(responseId)],
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
)();
if (!response) {
@@ -310,11 +319,15 @@ export const getResponses = async (surveyId: string, page?: number): Promise<TRe
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
const transformedResponses: TResponse[] = responses.map((responsePrisma) => ({
...responsePrisma,
person: responsePrisma.person ? transformPrismaPerson(responsePrisma.person) : null,
tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
}));
const transformedResponses: TResponse[] = await Promise.all(
responses.map(async (responsePrisma) => {
return {
...responsePrisma,
person: responsePrisma.person ? transformPrismaPerson(responsePrisma.person) : null,
tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
};
})
);
return transformedResponses;
} catch (error) {
@@ -363,11 +376,15 @@ export const getResponsesByEnvironmentId = async (
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
const transformedResponses: TResponse[] = responses.map((responsePrisma) => ({
...responsePrisma,
person: responsePrisma.person ? transformPrismaPerson(responsePrisma.person) : null,
tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
}));
const transformedResponses: TResponse[] = await Promise.all(
responses.map(async (responsePrisma) => {
return {
...responsePrisma,
person: responsePrisma.person ? transformPrismaPerson(responsePrisma.person) : null,
tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
};
})
);
return transformedResponses;
} catch (error) {
@@ -435,11 +452,15 @@ export const updateResponse = async (
};
responseCache.revalidate({
personId: response.person?.id,
id: response.id,
personId: response.person?.id,
surveyId: response.surveyId,
});
responseNoteCache.revalidate({
responseId: response.id,
});
return response;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
@@ -460,19 +481,26 @@ export const deleteResponse = async (responseId: string): Promise<TResponse> =>
select: responseSelection,
});
const responseNotes = await getResponseNotes(responsePrisma.id);
const response: TResponse = {
...responsePrisma,
notes: responseNotes,
person: responsePrisma.person ? transformPrismaPerson(responsePrisma.person) : null,
tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
};
deleteDisplayByResponseId(responseId, response.surveyId);
responseCache.revalidate({
personId: response.person?.id,
id: response.id,
personId: response.person?.id,
surveyId: response.surveyId,
});
responseNoteCache.revalidate({
responseId: response.id,
});
return response;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {

View File

@@ -0,0 +1,26 @@
import { revalidateTag } from "next/cache";
interface RevalidateProps {
id?: string;
responseId?: string;
}
export const responseNoteCache = {
tag: {
byId(id: string) {
return `responseNotes-${id}`;
},
byResponseId(responseId: string) {
return `responses-${responseId}-responseNote`;
},
},
revalidate({ id, responseId }: RevalidateProps): void {
if (id) {
revalidateTag(this.tag.byId(id));
}
if (responseId) {
revalidateTag(this.tag.byResponseId(responseId));
}
},
};

View File

@@ -6,6 +6,12 @@ import { DatabaseError } from "@formbricks/types/errors";
import { TResponseNote } from "@formbricks/types/responses";
import { Prisma } from "@prisma/client";
import { responseCache } from "../response/cache";
import { validateInputs } from "../utils/validate";
import { ZId } from "@formbricks/types/environment";
import { ZString } from "@formbricks/types/common";
import { SERVICES_REVALIDATION_INTERVAL } from "../constants";
import { unstable_cache } from "next/cache";
import { responseNoteCache } from "./cache";
const select = {
id: true,
@@ -33,6 +39,8 @@ export const createResponseNote = async (
userId: string,
text: string
): Promise<TResponseNote> => {
validateInputs([responseId, ZId], [userId, ZId], [text, ZString]);
try {
const responseNote = await prisma.responseNote.create({
data: {
@@ -44,30 +52,18 @@ export const createResponseNote = async (
});
responseCache.revalidate({
id: responseId,
id: responseNote.response.id,
surveyId: responseNote.response.surveyId,
});
return responseNote;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
};
export const getResponseNote = async (responseNoteId: string): Promise<TResponseNote | null> => {
try {
const responseNote = await prisma.responseNote.findUnique({
where: {
id: responseNoteId,
},
select,
responseNoteCache.revalidate({
id: responseNote.id,
responseId: responseNote.response.id,
});
return responseNote;
} catch (error) {
console.error(error);
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
@@ -76,7 +72,59 @@ export const getResponseNote = async (responseNoteId: string): Promise<TResponse
}
};
export const getResponseNote = async (responseNoteId: string): Promise<TResponseNote | null> =>
unstable_cache(
async () => {
try {
const responseNote = await prisma.responseNote.findUnique({
where: {
id: responseNoteId,
},
select,
});
return responseNote;
} catch (error) {
console.error(error);
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
},
[`getResponseNote-${responseNoteId}`],
{ tags: [responseNoteCache.tag.byId(responseNoteId)], revalidate: SERVICES_REVALIDATION_INTERVAL }
)();
export const getResponseNotes = async (responseId: string): Promise<TResponseNote[]> =>
unstable_cache(
async () => {
try {
validateInputs([responseId, ZId]);
const responseNotes = await prisma.responseNote.findMany({
where: {
responseId,
},
select,
});
return responseNotes;
} catch (error) {
console.error(error);
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
},
[`getResponseNotes-${responseId}`],
{ tags: [responseNoteCache.tag.byResponseId(responseId)], revalidate: SERVICES_REVALIDATION_INTERVAL }
)();
export const updateResponseNote = async (responseNoteId: string, text: string): Promise<TResponseNote> => {
validateInputs([responseNoteId, ZString], [text, ZString]);
try {
const updatedResponseNote = await prisma.responseNote.update({
where: {
@@ -95,8 +143,14 @@ export const updateResponseNote = async (responseNoteId: string, text: string):
surveyId: updatedResponseNote.response.surveyId,
});
responseNoteCache.revalidate({
id: updatedResponseNote.id,
responseId: updatedResponseNote.response.id,
});
return updatedResponseNote;
} catch (error) {
console.error(error);
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
@@ -106,6 +160,8 @@ export const updateResponseNote = async (responseNoteId: string, text: string):
};
export const resolveResponseNote = async (responseNoteId: string): Promise<TResponseNote> => {
validateInputs([responseNoteId, ZString]);
try {
const responseNote = await prisma.responseNote.update({
where: {
@@ -123,8 +179,14 @@ export const resolveResponseNote = async (responseNoteId: string): Promise<TResp
surveyId: responseNote.response.surveyId,
});
responseNoteCache.revalidate({
id: responseNote.id,
responseId: responseNote.response.id,
});
return responseNote;
} catch (error) {
console.error(error);
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}

View File

@@ -0,0 +1,26 @@
import { revalidateTag } from "next/cache";
interface RevalidateProps {
id?: string;
personId?: string;
}
export const sessionCache = {
tag: {
byId(id: string) {
return `sessions-${id}`;
},
byPersonId(personId: string) {
return `people-${personId}-sessions`;
},
},
revalidate({ id, personId }: RevalidateProps): void {
if (id) {
revalidateTag(this.tag.byId(id));
}
if (personId) {
revalidateTag(this.tag.byPersonId(personId));
}
},
};

View File

@@ -4,14 +4,13 @@ import "server-only";
import { prisma } from "@formbricks/database";
import { ZId } from "@formbricks/types/environment";
import { DatabaseError } from "@formbricks/types/errors";
import { TSession, TSessionWithActions } from "@formbricks/types/sessions";
import { TSession } from "@formbricks/types/sessions";
import { Prisma } from "@prisma/client";
import { revalidateTag, unstable_cache } from "next/cache";
import { unstable_cache } from "next/cache";
import { validateInputs } from "../utils/validate";
import { ZOptionalNumber } from "@formbricks/types/common";
import { ITEMS_PER_PAGE, SERVICES_REVALIDATION_INTERVAL } from "../constants";
const getSessionCacheKey = (sessionId: string): string[] => [sessionId];
import { SERVICES_REVALIDATION_INTERVAL } from "../constants";
import { sessionCache } from "./cache";
import { formatSessionDateFields } from "./util";
const select = {
id: true,
@@ -24,93 +23,64 @@ const select = {
const oneHour = 1000 * 60 * 60;
export const getSession = async (sessionId: string): Promise<TSession | null> => {
validateInputs([sessionId, ZId]);
try {
const session = await prisma.session.findUnique({
where: {
id: sessionId,
},
select,
});
return session;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
};
export const getSessionCached = (sessionId: string) =>
unstable_cache(
const session = await unstable_cache(
async () => {
return await getSession(sessionId);
validateInputs([sessionId, ZId]);
try {
const session = await prisma.session.findUnique({
where: {
id: sessionId,
},
select,
});
return session;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
},
getSessionCacheKey(sessionId),
[`getSession-${sessionId}`],
{
tags: getSessionCacheKey(sessionId),
tags: [sessionCache.tag.byId(sessionId)],
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
)();
export const getSessionWithActionsOfPerson = async (
personId: string,
page?: number
): Promise<TSessionWithActions[] | null> => {
validateInputs([personId, ZId], [page, ZOptionalNumber]);
try {
const sessionsWithActionsForPerson = await prisma.session.findMany({
where: {
personId,
},
select: {
id: true,
events: {
select: {
id: true,
createdAt: true,
eventClass: {
select: {
name: true,
description: true,
type: true,
},
},
if (!session) return null;
return formatSessionDateFields(session);
};
export const getSessionCount = async (personId: string): Promise<number> =>
unstable_cache(
async () => {
validateInputs([personId, ZId]);
try {
const sessionCount = await prisma.session.count({
where: {
personId,
},
},
},
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
if (!sessionsWithActionsForPerson) return null;
return sessionsWithActionsForPerson;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
});
return sessionCount;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
},
[`getSessionCount-${personId}`],
{
tags: [sessionCache.tag.byPersonId(personId)],
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
throw error;
}
};
export const getSessionCount = async (personId: string): Promise<number> => {
validateInputs([personId, ZId]);
try {
const sessionCount = await prisma.session.count({
where: {
personId,
},
});
return sessionCount;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
};
)();
export const createSession = async (personId: string): Promise<TSession> => {
validateInputs([personId, ZId]);
@@ -128,8 +98,10 @@ export const createSession = async (personId: string): Promise<TSession> => {
});
if (session) {
// revalidate session cache
revalidateTag(session.id);
sessionCache.revalidate({
id: session.id,
personId,
});
}
return session;
@@ -144,6 +116,7 @@ export const createSession = async (personId: string): Promise<TSession> => {
export const extendSession = async (sessionId: string): Promise<TSession> => {
validateInputs([sessionId, ZId]);
try {
const session = await prisma.session.update({
where: {
@@ -156,7 +129,10 @@ export const extendSession = async (sessionId: string): Promise<TSession> => {
});
// revalidate session cache
revalidateTag(sessionId);
sessionCache.revalidate({
id: sessionId,
personId: session.personId,
});
return session;
} catch (error) {

View File

@@ -0,0 +1,17 @@
import "server-only";
import { TSession } from "@formbricks/types/sessions";
export const formatSessionDateFields = (session: TSession): TSession => {
if (typeof session.createdAt === "string") {
session.createdAt = new Date(session.createdAt);
}
if (typeof session.updatedAt === "string") {
session.updatedAt = new Date(session.updatedAt);
}
if (typeof session.expiresAt === "string") {
session.expiresAt = new Date(session.expiresAt);
}
return session;
};

View File

@@ -1,7 +1,8 @@
import { ZId } from "@formbricks/types/environment";
import { validateInputs } from "../utils/validate";
import { hasUserEnvironmentAccess } from "../environment/auth";
import { getSurvey, getSurveyCacheTag } from "./service";
import { getSurvey } from "./service";
import { surveyCache } from "./cache";
import { unstable_cache } from "next/cache";
import { SERVICES_REVALIDATION_INTERVAL } from "../constants";
@@ -20,6 +21,6 @@ export const canUserAccessSurvey = async (userId: string, surveyId: string): Pro
return true;
},
[`users-${userId}-surveys-${surveyId}`],
{ revalidate: SERVICES_REVALIDATION_INTERVAL, tags: [getSurveyCacheTag(surveyId)] }
[`canUserAccessSurvey-${userId}-${surveyId}`],
{ revalidate: SERVICES_REVALIDATION_INTERVAL, tags: [surveyCache.tag.byId(surveyId)] }
)();

View File

@@ -0,0 +1,42 @@
import { revalidateTag } from "next/cache";
interface RevalidateProps {
id?: string;
attributeClassId?: string;
actionClassId?: string;
environmentId?: string;
}
export const surveyCache = {
tag: {
byId(id: string) {
return `surveys-${id}`;
},
byEnvironmentId(environmentId: string): string {
return `environments-${environmentId}-surveys`;
},
byAttributeClassId(attributeClassId: string) {
return `attributeFilters-${attributeClassId}-surveys`;
},
byActionClassId(actionClassId: string) {
return `actionClasses-${actionClassId}-surveys`;
},
},
revalidate({ id, attributeClassId, actionClassId, environmentId }: RevalidateProps): void {
if (id) {
revalidateTag(this.tag.byId(id));
}
if (attributeClassId) {
revalidateTag(this.tag.byAttributeClassId(attributeClassId));
}
if (actionClassId) {
revalidateTag(this.tag.byActionClassId(actionClassId));
}
if (environmentId) {
revalidateTag(this.tag.byEnvironmentId(environmentId));
}
},
};

View File

@@ -5,20 +5,16 @@ import { ZOptionalNumber } from "@formbricks/types/common";
import { ZId } from "@formbricks/types/environment";
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
import { TSurvey, TSurveyAttributeFilter, TSurveyInput, ZSurvey } from "@formbricks/types/surveys";
import { TActionClass } from "@formbricks/types/actionClasses";
import { Prisma } from "@prisma/client";
import { revalidateTag, unstable_cache } from "next/cache";
import { unstable_cache } from "next/cache";
import { getActionClasses } from "../actionClass/service";
import { ITEMS_PER_PAGE, SERVICES_REVALIDATION_INTERVAL } from "../constants";
import { responseCache } from "../response/cache";
import { captureTelemetry } from "../telemetry";
import { validateInputs } from "../utils/validate";
import { formatSurveyDateFields } from "./util";
// surveys cache key and tags
const getSurveysCacheTag = (environmentId: string): string => `environments-${environmentId}-surveys`;
// survey cache key and tags
export const getSurveyCacheTag = (surveyId: string): string => `surveys-${surveyId}`;
import { surveyCache } from "./cache";
export const selectSurvey = {
id: true,
@@ -70,10 +66,32 @@ export const selectSurvey = {
},
};
const getActionClassIdFromName = (actionClasses: TActionClass[], actionClassName: string): string => {
return actionClasses.find((actionClass) => actionClass.name === actionClassName)!.id;
};
const revalidateSurveyByActionClassId = (actionClasses: TActionClass[], actionClassNames: string[]): void => {
for (const actionClassName of actionClassNames) {
const actionClassId: string = getActionClassIdFromName(actionClasses, actionClassName);
surveyCache.revalidate({
actionClassId,
});
}
};
const revalidateSurveyByAttributeClassId = (attributeFilters: TSurveyAttributeFilter[]): void => {
for (const attributeFilter of attributeFilters) {
surveyCache.revalidate({
attributeClassId: attributeFilter.attributeClassId,
});
}
};
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({
@@ -102,9 +120,9 @@ export const getSurvey = async (surveyId: string): Promise<TSurvey | null> => {
return transformedSurvey;
},
[`surveys-${surveyId}`],
[`getSurvey-${surveyId}`],
{
tags: [getSurveyCacheTag(surveyId)],
tags: [surveyCache.tag.byId(surveyId)],
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
)();
@@ -125,61 +143,91 @@ export const getSurveysByAttributeClassId = async (
attributeClassId: string,
page?: number
): Promise<TSurvey[]> => {
validateInputs([attributeClassId, ZId], [page, ZOptionalNumber]);
const surveys = await unstable_cache(
async () => {
validateInputs([attributeClassId, ZId], [page, ZOptionalNumber]);
const surveysPrisma = await prisma.survey.findMany({
where: {
attributeFilters: {
some: {
attributeClassId,
const surveysPrisma = await prisma.survey.findMany({
where: {
attributeFilters: {
some: {
attributeClassId,
},
},
},
},
select: selectSurvey,
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
const surveys: TSurvey[] = [];
for (const surveyPrisma of surveysPrisma) {
const transformedSurvey = {
...surveyPrisma,
triggers: surveyPrisma.triggers.map((trigger) => trigger.eventClass.name),
};
surveys.push(transformedSurvey);
}
return surveys;
},
select: selectSurvey,
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
[`getSurveysByAttributeClassId-${attributeClassId}-${page}`],
{
tags: [surveyCache.tag.byAttributeClassId(attributeClassId)],
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
)();
const surveys: TSurvey[] = [];
for (const surveyPrisma of surveysPrisma) {
const transformedSurvey = {
...surveyPrisma,
triggers: surveyPrisma.triggers.map((trigger) => trigger.eventClass.name),
};
surveys.push(transformedSurvey);
}
return surveys;
return surveys.map((survey) => ({
...survey,
...formatSurveyDateFields(survey),
}));
};
export const getSurveysByActionClassId = async (actionClassId: string, page?: number): Promise<TSurvey[]> => {
validateInputs([actionClassId, ZId], [page, ZOptionalNumber]);
const surveys = await unstable_cache(
async () => {
validateInputs([actionClassId, ZId], [page, ZOptionalNumber]);
const surveysPrisma = await prisma.survey.findMany({
where: {
triggers: {
some: {
eventClass: {
id: actionClassId,
const surveysPrisma = await prisma.survey.findMany({
where: {
triggers: {
some: {
eventClass: {
id: actionClassId,
},
},
},
},
},
select: selectSurvey,
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
const surveys: TSurvey[] = [];
for (const surveyPrisma of surveysPrisma) {
const transformedSurvey = {
...surveyPrisma,
triggers: surveyPrisma.triggers.map((trigger) => trigger.eventClass.name),
};
surveys.push(transformedSurvey);
}
return surveys;
},
select: selectSurvey,
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
[`getSurveysByActionClassId-${actionClassId}-${page}`],
{
tags: [surveyCache.tag.byActionClassId(actionClassId)],
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
)();
const surveys: TSurvey[] = [];
for (const surveyPrisma of surveysPrisma) {
const transformedSurvey = {
...surveyPrisma,
triggers: surveyPrisma.triggers.map((trigger) => trigger.eventClass.name),
};
surveys.push(transformedSurvey);
}
return surveys;
return surveys.map((survey) => ({
...survey,
...formatSurveyDateFields(survey),
}));
};
export const getSurveys = async (environmentId: string, page?: number): Promise<TSurvey[]> => {
@@ -216,9 +264,9 @@ export const getSurveys = async (environmentId: string, page?: number): Promise<
}
return surveys;
},
[`environments-${environmentId}-surveys`],
[`getSurveys-${environmentId}-${page}`],
{
tags: [getSurveysCacheTag(environmentId)],
tags: [surveyCache.tag.byEnvironmentId(environmentId)],
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
)();
@@ -249,6 +297,7 @@ export async function updateSurvey(updatedSurvey: TSurvey): Promise<TSurvey> {
if (triggers) {
const newTriggers: string[] = [];
const removedTriggers: string[] = [];
// find added triggers
for (const trigger of triggers) {
if (!trigger) {
@@ -260,6 +309,7 @@ export async function updateSurvey(updatedSurvey: TSurvey): Promise<TSurvey> {
newTriggers.push(trigger);
}
}
// find removed triggers
for (const trigger of currentSurvey.triggers) {
if (triggers.find((t: any) => t === trigger)) {
@@ -273,7 +323,7 @@ export async function updateSurvey(updatedSurvey: TSurvey): Promise<TSurvey> {
data.triggers = {
...(data.triggers || []),
create: newTriggers.map((trigger) => ({
eventClassId: actionClasses.find((actionClass) => actionClass.name === trigger)!.id,
eventClassId: getActionClassIdFromName(actionClasses, trigger),
})),
};
}
@@ -283,23 +333,26 @@ export async function updateSurvey(updatedSurvey: TSurvey): Promise<TSurvey> {
...(data.triggers || []),
deleteMany: {
eventClassId: {
in: removedTriggers.map(
(trigger) => actionClasses.find((actionClass) => actionClass.name === trigger)!.id
),
in: removedTriggers.map((trigger) => getActionClassIdFromName(actionClasses, trigger)),
},
},
};
}
// Revalidation for newly added/removed actionClassId
revalidateSurveyByActionClassId(actionClasses, [...newTriggers, ...removedTriggers]);
}
if (attributeFilters) {
const newFilters: TSurveyAttributeFilter[] = [];
const removedFilterIds: string[] = [];
const removedFilters: TSurveyAttributeFilter[] = [];
// find added attribute filters
for (const attributeFilter of attributeFilters) {
if (!attributeFilter.attributeClassId || !attributeFilter.condition || !attributeFilter.value) {
continue;
}
if (
currentSurvey.attributeFilters.find(
(f) =>
@@ -329,9 +382,14 @@ export async function updateSurvey(updatedSurvey: TSurvey): Promise<TSurvey> {
) {
continue;
} else {
removedFilterIds.push(attributeFilter.attributeClassId);
removedFilters.push({
attributeClassId: attributeFilter.attributeClassId,
condition: attributeFilter.condition,
value: attributeFilter.value,
});
}
}
// create new attribute filters
if (newFilters.length > 0) {
data.attributeFilters = {
@@ -343,19 +401,21 @@ export async function updateSurvey(updatedSurvey: TSurvey): Promise<TSurvey> {
})),
};
}
// delete removed triggers
if (removedFilterIds.length > 0) {
// delete removed attribute filter
if (removedFilters.length > 0) {
// delete all attribute filters that match the removed attribute classes
await Promise.all(
removedFilterIds.map(async (attributeClassId) => {
removedFilters.map(async (attributeFilter) => {
await prisma.surveyAttributeFilter.deleteMany({
where: {
attributeClassId,
attributeClassId: attributeFilter.attributeClassId,
},
});
})
);
}
revalidateSurveyByAttributeClassId([...newFilters, ...removedFilters]);
}
data = {
@@ -375,8 +435,10 @@ export async function updateSurvey(updatedSurvey: TSurvey): Promise<TSurvey> {
attributeFilters: updatedSurvey.attributeFilters ? updatedSurvey.attributeFilters : [], // Include attributeFilters from updatedSurvey
};
revalidateTag(getSurveysCacheTag(modifiedSurvey.environmentId));
revalidateTag(getSurveyCacheTag(modifiedSurvey.id));
surveyCache.revalidate({
id: modifiedSurvey.id,
environmentId: modifiedSurvey.environmentId,
});
return modifiedSurvey;
} catch (error) {
@@ -399,13 +461,27 @@ export async function deleteSurvey(surveyId: string) {
select: selectSurvey,
});
revalidateTag(getSurveysCacheTag(deletedSurvey.environmentId));
revalidateTag(getSurveyCacheTag(surveyId));
responseCache.revalidate({
surveyId,
environmentId: deletedSurvey.environmentId,
});
surveyCache.revalidate({
id: deletedSurvey.id,
environmentId: deletedSurvey.environmentId,
});
// Revalidate triggers by actionClassId
deletedSurvey.triggers.forEach((trigger) => {
surveyCache.revalidate({
actionClassId: trigger.eventClass.id,
});
});
// Revalidate surveys by attributeClassId
deletedSurvey.attributeFilters.forEach((attributeFilter) => {
surveyCache.revalidate({
attributeClassId: attributeFilter.attributeClassId,
});
});
return deletedSurvey;
}
@@ -413,6 +489,15 @@ export async function deleteSurvey(surveyId: string) {
export async function createSurvey(environmentId: string, surveyBody: TSurveyInput): Promise<TSurvey> {
validateInputs([environmentId, ZId]);
if (surveyBody.attributeFilters) {
revalidateSurveyByAttributeClassId(surveyBody.attributeFilters);
}
if (surveyBody.triggers) {
const actionClasses = await getActionClasses(environmentId);
revalidateSurveyByActionClassId(actionClasses, surveyBody.triggers);
}
// TODO: Create with triggers & attributeFilters
delete surveyBody.triggers;
delete surveyBody.attributeFilters;
@@ -439,8 +524,10 @@ export async function createSurvey(environmentId: string, surveyBody: TSurveyInp
captureTelemetry("survey created");
revalidateTag(getSurveysCacheTag(environmentId));
revalidateTag(getSurveyCacheTag(survey.id));
surveyCache.revalidate({
id: survey.id,
environmentId: survey.environmentId,
});
return transformedSurvey;
}
@@ -453,6 +540,11 @@ export async function duplicateSurvey(environmentId: string, surveyId: string) {
}
const actionClasses = await getActionClasses(environmentId);
const newAttributeFilters = existingSurvey.attributeFilters.map((attributeFilter) => ({
attributeClassId: attributeFilter.attributeClassId,
condition: attributeFilter.condition,
value: attributeFilter.value,
}));
// create new survey with the data of the existing survey
const newSurvey = await prisma.survey.create({
@@ -466,15 +558,11 @@ export async function duplicateSurvey(environmentId: string, surveyId: string) {
thankYouCard: JSON.parse(JSON.stringify(existingSurvey.thankYouCard)),
triggers: {
create: existingSurvey.triggers.map((trigger) => ({
eventClassId: actionClasses.find((actionClass) => actionClass.name === trigger)!.id,
eventClassId: getActionClassIdFromName(actionClasses, trigger),
})),
},
attributeFilters: {
create: existingSurvey.attributeFilters.map((attributeFilter) => ({
attributeClassId: attributeFilter.attributeClassId,
condition: attributeFilter.condition,
value: attributeFilter.value,
})),
create: newAttributeFilters,
},
environment: {
connect: {
@@ -496,8 +584,16 @@ export async function duplicateSurvey(environmentId: string, surveyId: string) {
},
});
revalidateTag(getSurveysCacheTag(environmentId));
revalidateTag(getSurveyCacheTag(surveyId));
surveyCache.revalidate({
id: newSurvey.id,
environmentId: newSurvey.environmentId,
});
// Revalidate surveys by actionClassId
revalidateSurveyByActionClassId(actionClasses, existingSurvey.triggers);
// Revalidate surveys by attributeClassId
revalidateSurveyByAttributeClassId(newAttributeFilters);
return newSurvey;
}

View File

@@ -5,7 +5,7 @@ import { unstable_cache } from "next/cache";
import { ZId } from "@formbricks/types/environment";
import { canUserAccessResponse } from "../response/auth";
import { canUserAccessTag } from "../tag/auth";
import { getTagOnResponseCacheTag } from "./service";
import { tagOnResponseCache } from "./cache";
import { SERVICES_REVALIDATION_INTERVAL } from "../constants";
export const canUserAccessTagOnResponse = async (
@@ -23,5 +23,8 @@ export const canUserAccessTagOnResponse = async (
return isAuthorizedForTag && isAuthorizedForResponse;
},
[`users-${userId}-tagOnResponse-${tagId}-${responseId}`],
{ revalidate: SERVICES_REVALIDATION_INTERVAL, tags: [getTagOnResponseCacheTag(tagId, responseId)] }
{
revalidate: SERVICES_REVALIDATION_INTERVAL,
tags: [tagOnResponseCache.tag.byResponseIdAndTagId(responseId, tagId)],
}
)();

View File

@@ -0,0 +1,27 @@
import { revalidateTag } from "next/cache";
interface RevalidateProps {
tagId?: string;
responseId?: string;
environmentId?: string;
}
export const tagOnResponseCache = {
tag: {
byResponseIdAndTagId(responseId: string, tagId: string) {
return `responses-${responseId}-tagOnResponses-${tagId}`;
},
byEnvironmentId(environmentId: string) {
return `environments-${environmentId}-tagOnResponses`;
},
},
revalidate({ tagId, responseId, environmentId }: RevalidateProps): void {
if (responseId && tagId) {
revalidateTag(this.tag.byResponseIdAndTagId(responseId, tagId));
}
if (environmentId) {
revalidateTag(this.tag.byEnvironmentId(environmentId));
}
},
};

View File

@@ -3,9 +3,19 @@ import "server-only";
import { prisma } from "@formbricks/database";
import { TTagsCount, TTagsOnResponses } from "@formbricks/types/tags";
import { responseCache } from "../response/cache";
import { SERVICES_REVALIDATION_INTERVAL } from "../constants";
import { unstable_cache } from "next/cache";
import { tagOnResponseCache } from "./cache";
import { validateInputs } from "../utils/validate";
import { ZId } from "@formbricks/types/environment";
export const getTagOnResponseCacheTag = (tagId: string, responseId: string) =>
`tagsOnResponse-${tagId}-${responseId}`;
const selectTagsOnResponse = {
tag: {
select: {
environmentId: true,
},
},
};
export const addTagToRespone = async (responseId: string, tagId: string): Promise<TTagsOnResponses> => {
try {
@@ -14,12 +24,23 @@ export const addTagToRespone = async (responseId: string, tagId: string): Promis
responseId,
tagId,
},
select: selectTagsOnResponse,
});
responseCache.revalidate({
id: responseId,
});
return tagOnResponse;
tagOnResponseCache.revalidate({
tagId,
responseId,
environmentId: tagOnResponse.tag.environmentId,
});
return {
responseId,
tagId,
};
} catch (error) {
throw error;
}
@@ -34,28 +55,58 @@ export const deleteTagOnResponse = async (responseId: string, tagId: string): Pr
tagId,
},
},
select: selectTagsOnResponse,
});
responseCache.revalidate({
id: responseId,
});
return deletedTag;
} catch (error) {
throw error;
}
};
export const getTagsOnResponsesCount = async (): Promise<TTagsCount> => {
try {
const tagsCount = await prisma.tagsOnResponses.groupBy({
by: ["tagId"],
_count: {
_all: true,
},
tagOnResponseCache.revalidate({
tagId,
responseId,
environmentId: deletedTag.tag.environmentId,
});
return tagsCount.map((tagCount) => ({ tagId: tagCount.tagId, count: tagCount._count._all }));
return {
tagId,
responseId,
};
} catch (error) {
throw error;
}
};
export const getTagsOnResponsesCount = async (environmentId: string): Promise<TTagsCount> =>
unstable_cache(
async () => {
validateInputs([environmentId, ZId]);
try {
const tagsCount = await prisma.tagsOnResponses.groupBy({
by: ["tagId"],
where: {
response: {
survey: {
environment: {
id: environmentId,
},
},
},
},
_count: {
_all: true,
},
});
return tagsCount.map((tagCount) => ({ tagId: tagCount.tagId, count: tagCount._count._all }));
} catch (error) {
throw error;
}
},
[`getTagsOnResponsesCount-${environmentId}`],
{
tags: [tagOnResponseCache.tag.byEnvironmentId(environmentId)],
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
)();

View File

@@ -0,0 +1,34 @@
import { revalidateTag } from "next/cache";
interface RevalidateProps {
id?: string;
userId?: string;
environmentId?: string;
}
export const teamCache = {
tag: {
byId(id: string) {
return `teams-${id}`;
},
byUserId(userId: string) {
return `users-${userId}-teams`;
},
byEnvironmentId(environmentId: string) {
return `environments-${environmentId}-teams`;
},
},
revalidate({ id, userId, environmentId }: RevalidateProps): void {
if (id) {
revalidateTag(this.tag.byId(id));
}
if (userId) {
revalidateTag(this.tag.byUserId(userId));
}
if (environmentId) {
revalidateTag(this.tag.byEnvironmentId(environmentId));
}
},
};

View File

@@ -5,11 +5,12 @@ import { ZId } from "@formbricks/types/environment";
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
import { TTeam, TTeamUpdateInput, ZTeamUpdateInput } from "@formbricks/types/teams";
import { Prisma } from "@prisma/client";
import { revalidateTag, unstable_cache } from "next/cache";
import { unstable_cache } from "next/cache";
import { SERVICES_REVALIDATION_INTERVAL, ITEMS_PER_PAGE } from "../constants";
import { getEnvironmentCacheTag } from "../environment/service";
import { ZOptionalNumber, ZString } from "@formbricks/types/common";
import { validateInputs } from "../utils/validate";
import { environmentCache } from "../environment/cache";
import { teamCache } from "./cache";
export const select = {
id: true,
@@ -20,9 +21,6 @@ export const select = {
stripeCustomerId: true,
};
export const getTeamsByUserIdCacheTag = (userId: string) => `users-${userId}-teams`;
export const getTeamByEnvironmentIdCacheTag = (environmentId: string) => `environments-${environmentId}-team`;
export const getTeamsByUserId = async (userId: string, page?: number): Promise<TTeam[]> =>
unstable_cache(
async () => {
@@ -41,7 +39,6 @@ export const getTeamsByUserId = async (userId: string, page?: number): Promise<T
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
revalidateTag(getTeamsByUserIdCacheTag(userId));
return teams;
} catch (error) {
@@ -52,9 +49,9 @@ export const getTeamsByUserId = async (userId: string, page?: number): Promise<T
throw error;
}
},
[`users-${userId}-teams`],
[`getTeamsByUserId-${userId}-${page}`],
{
tags: [getTeamsByUserIdCacheTag(userId)],
tags: [teamCache.tag.byUserId(userId)],
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
)();
@@ -79,7 +76,6 @@ export const getTeamByEnvironmentId = async (environmentId: string): Promise<TTe
},
select: { ...select, memberships: true }, // include memberships
});
revalidateTag(getTeamByEnvironmentIdCacheTag(environmentId));
return team;
} catch (error) {
@@ -91,9 +87,9 @@ export const getTeamByEnvironmentId = async (environmentId: string): Promise<TTe
throw error;
}
},
[`environments-${environmentId}-team`],
[`getTeamByEnvironmentId-${environmentId}`],
{
tags: [getTeamByEnvironmentIdCacheTag(environmentId)],
tags: [teamCache.tag.byEnvironmentId(environmentId)],
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
)();
@@ -107,6 +103,10 @@ export const createTeam = async (teamInput: TTeamUpdateInput): Promise<TTeam> =>
select,
});
teamCache.revalidate({
id: team.id,
});
return team;
} catch (error) {
throw error;
@@ -125,13 +125,17 @@ export const updateTeam = async (teamId: string, data: Partial<TTeamUpdateInput>
// revalidate cache for members
updatedTeam?.memberships.forEach((membership) => {
revalidateTag(getTeamsByUserIdCacheTag(membership.userId));
teamCache.revalidate({
userId: membership.userId,
});
});
// revalidate cache for environments
updatedTeam?.products.forEach((product) => {
product.environments.forEach((environment) => {
revalidateTag(getTeamByEnvironmentIdCacheTag(environment.id));
teamCache.revalidate({
environmentId: environment.id,
});
});
});
@@ -141,6 +145,10 @@ export const updateTeam = async (teamId: string, data: Partial<TTeamUpdateInput>
products: undefined,
};
teamCache.revalidate({
id: team.id,
});
return team;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2016") {
@@ -163,14 +171,21 @@ export const deleteTeam = async (teamId: string): Promise<TTeam> => {
// revalidate cache for members
deletedTeam?.memberships.forEach((membership) => {
revalidateTag(getTeamsByUserIdCacheTag(membership.userId));
teamCache.revalidate({
userId: membership.userId,
});
});
// revalidate cache for environments
deletedTeam?.products.forEach((product) => {
product.environments.forEach((environment) => {
revalidateTag(getTeamByEnvironmentIdCacheTag(environment.id));
revalidateTag(getEnvironmentCacheTag(environment.id));
environmentCache.revalidate({
id: environment.id,
});
teamCache.revalidate({
environmentId: environment.id,
});
});
});
@@ -180,6 +195,10 @@ export const deleteTeam = async (teamId: string): Promise<TTeam> => {
products: undefined,
};
teamCache.revalidate({
id: team.id,
});
return team;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {

View File

@@ -5,53 +5,65 @@ import { Prisma } from "@prisma/client";
import { validateInputs } from "../utils/validate";
import { ZId } from "@formbricks/types/environment";
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
import { SERVICES_REVALIDATION_INTERVAL } from "../constants";
import { unstable_cache } from "next/cache";
import { teamCache } from "../team/cache";
export const getTeamDetails = async (
environmentId: string
): Promise<{ teamId: string; teamOwnerId: string | undefined }> => {
validateInputs([environmentId, ZId]);
try {
const environment = await prisma.environment.findUnique({
where: {
id: environmentId,
},
select: {
product: {
): Promise<{ teamId: string; teamOwnerId: string | undefined }> =>
unstable_cache(
async () => {
validateInputs([environmentId, ZId]);
try {
const environment = await prisma.environment.findUnique({
where: {
id: environmentId,
},
select: {
team: {
product: {
select: {
id: true,
memberships: {
team: {
select: {
userId: true,
role: true,
id: true,
memberships: {
select: {
userId: true,
role: true,
},
},
},
},
},
},
},
},
},
});
});
if (!environment) {
throw new ResourceNotFoundError("Environment", environmentId);
if (!environment) {
throw new ResourceNotFoundError("Environment", environmentId);
}
const teamId: string = environment.product.team.id;
// find team owner
const teamOwnerId: string | undefined = environment.product.team.memberships.find(
(m) => m.role === "owner"
)?.userId;
return {
teamId: teamId,
teamOwnerId: teamOwnerId,
};
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
},
[`getTeamDetails-${environmentId}`],
{
tags: [teamCache.tag.byEnvironmentId(environmentId)],
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
const teamId: string = environment.product.team.id;
// find team owner
const teamOwnerId: string | undefined = environment.product.team.memberships.find(
(m) => m.role === "owner"
)?.userId;
return {
teamId: teamId,
teamOwnerId: teamOwnerId,
};
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
};
)();

View File

@@ -4,6 +4,7 @@ import { getWebhook } from "./service";
import { unstable_cache } from "next/cache";
import { ZId } from "@formbricks/types/environment";
import { SERVICES_REVALIDATION_INTERVAL } from "../constants";
import { webhookCache } from "./cache";
export const canUserAccessWebhook = async (userId: string, webhookId: string): Promise<boolean> =>
await unstable_cache(
@@ -18,8 +19,9 @@ export const canUserAccessWebhook = async (userId: string, webhookId: string): P
return true;
},
[`${userId}-${webhookId}`],
[`canUserAccessWebhook-${userId}-${webhookId}`],
{
tags: [webhookCache.tag.byId(webhookId)],
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
)();

View File

@@ -0,0 +1,35 @@
import { revalidateTag } from "next/cache";
import { TWebhookInput } from "@formbricks/types/webhooks";
interface RevalidateProps {
id?: string;
environmentId?: string;
source?: TWebhookInput["source"];
}
export const webhookCache = {
tag: {
byId(id: string) {
return `webhooks-${id}`;
},
byEnvironmentId(environmentId: string) {
return `environments-${environmentId}-webhooks`;
},
byEnvironmentIdAndSource(environmentId: string, source: TWebhookInput["source"]) {
return `environments-${environmentId}-sources-${source}-webhooks`;
},
},
revalidate({ id, environmentId, source }: RevalidateProps): void {
if (id) {
revalidateTag(this.tag.byId(id));
}
if (environmentId) {
revalidateTag(this.tag.byEnvironmentId(environmentId));
}
if (environmentId && source) {
revalidateTag(this.tag.byEnvironmentIdAndSource(environmentId, source));
}
},
};

View File

@@ -7,60 +7,89 @@ import { validateInputs } from "../utils/validate";
import { ZId } from "@formbricks/types/environment";
import { ResourceNotFoundError, DatabaseError, InvalidInputError } from "@formbricks/types/errors";
import { ZOptionalNumber } from "@formbricks/types/common";
import { ITEMS_PER_PAGE } from "../constants";
import { ITEMS_PER_PAGE, SERVICES_REVALIDATION_INTERVAL } from "../constants";
import { webhookCache } from "./cache";
import { unstable_cache } from "next/cache";
export const getWebhooks = async (environmentId: string, page?: number): Promise<TWebhook[]> => {
validateInputs([environmentId, ZId], [page, ZOptionalNumber]);
export const getWebhooks = async (environmentId: string, page?: number): Promise<TWebhook[]> =>
unstable_cache(
async () => {
validateInputs([environmentId, ZId], [page, ZOptionalNumber]);
try {
const webhooks = await prisma.webhook.findMany({
where: {
environmentId: environmentId,
},
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
return webhooks;
} catch (error) {
throw new DatabaseError(`Database error when fetching webhooks for environment ${environmentId}`);
}
};
try {
const webhooks = await prisma.webhook.findMany({
where: {
environmentId: environmentId,
},
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
return webhooks;
} catch (error) {
throw new DatabaseError(`Database error when fetching webhooks for environment ${environmentId}`);
}
},
[`getWebhooks-${environmentId}-${page}`],
{
tags: [webhookCache.tag.byEnvironmentId(environmentId)],
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
)();
export const getCountOfWebhooksBasedOnSource = async (
environmentId: string,
source: TWebhookInput["source"]
): Promise<number> => {
validateInputs([environmentId, ZId], [source, ZId]);
try {
const count = await prisma.webhook.count({
where: {
environmentId,
source,
},
});
return count;
} catch (error) {
throw new DatabaseError(`Database error when fetching webhooks for environment ${environmentId}`);
}
};
): Promise<number> =>
unstable_cache(
async () => {
validateInputs([environmentId, ZId], [source, ZId]);
export const getWebhook = async (id: string): Promise<TWebhook | null> => {
validateInputs([id, ZId]);
const webhook = await prisma.webhook.findUnique({
where: {
id,
try {
const count = await prisma.webhook.count({
where: {
environmentId,
source,
},
});
return count;
} catch (error) {
throw new DatabaseError(`Database error when fetching webhooks for environment ${environmentId}`);
}
},
});
return webhook;
};
[`getCountOfWebhooksBasedOnSource-${environmentId}-${source}`],
{
tags: [webhookCache.tag.byEnvironmentIdAndSource(environmentId, source)],
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
)();
export const getWebhook = async (id: string): Promise<TWebhook | null> =>
unstable_cache(
async () => {
validateInputs([id, ZId]);
const webhook = await prisma.webhook.findUnique({
where: {
id,
},
});
return webhook;
},
[`getWebhook-${id}`],
{
tags: [webhookCache.tag.byId(id)],
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
)();
export const createWebhook = async (
environmentId: string,
webhookInput: TWebhookInput
): Promise<TWebhook> => {
validateInputs([environmentId, ZId], [webhookInput, ZWebhookInput]);
try {
let createdWebhook = await prisma.webhook.create({
const createdWebhook = await prisma.webhook.create({
data: {
...webhookInput,
surveyIds: webhookInput.surveyIds || [],
@@ -71,6 +100,13 @@ export const createWebhook = async (
},
},
});
webhookCache.revalidate({
id: createdWebhook.id,
environmentId: createdWebhook.environmentId,
source: createdWebhook.source,
});
return createdWebhook;
} catch (error) {
if (!(error instanceof InvalidInputError)) {
@@ -87,7 +123,7 @@ export const updateWebhook = async (
): Promise<TWebhook> => {
validateInputs([environmentId, ZId], [webhookId, ZId], [webhookInput, ZWebhookInput]);
try {
const webhook = await prisma.webhook.update({
const updatedWebhook = await prisma.webhook.update({
where: {
id: webhookId,
},
@@ -98,7 +134,14 @@ export const updateWebhook = async (
surveyIds: webhookInput.surveyIds || [],
},
});
return webhook;
webhookCache.revalidate({
id: updatedWebhook.id,
environmentId: updatedWebhook.environmentId,
source: updatedWebhook.source,
});
return updatedWebhook;
} catch (error) {
throw new DatabaseError(
`Database error when updating webhook with ID ${webhookId} for environment ${environmentId}`
@@ -108,12 +151,20 @@ export const updateWebhook = async (
export const deleteWebhook = async (id: string): Promise<TWebhook> => {
validateInputs([id, ZId]);
try {
let deletedWebhook = await prisma.webhook.delete({
where: {
id,
},
});
webhookCache.revalidate({
id: deletedWebhook.id,
environmentId: deletedWebhook.environmentId,
source: deletedWebhook.source,
});
return deletedWebhook;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2025") {

99
pnpm-lock.yaml generated
View File

@@ -39,8 +39,8 @@ importers:
specifier: ^2.0.18
version: 2.0.18(react@18.2.0)
next:
specifier: 13.5.5
version: 13.5.5(react-dom@18.2.0)(react@18.2.0)
specifier: 14.0.0
version: 14.0.0(react-dom@18.2.0)(react@18.2.0)
react:
specifier: 18.2.0
version: 18.2.0
@@ -331,7 +331,7 @@ importers:
version: 0.0.7
'@sentry/nextjs':
specifier: ^7.75.0
version: 7.75.0(encoding@0.1.13)(next@13.5.6)(react@18.2.0)
version: 7.75.0(encoding@0.1.13)(next@14.0.0)(react@18.2.0)
'@t3-oss/env-nextjs':
specifier: ^0.7.1
version: 0.7.1(zod@3.22.4)
@@ -366,8 +366,8 @@ importers:
specifier: ^3.0.0
version: 3.0.0
next:
specifier: 13.5.6
version: 13.5.6(react-dom@18.2.0)(react@18.2.0)
specifier: 14.0.0
version: 14.0.0(react-dom@18.2.0)(react@18.2.0)
nodemailer:
specifier: ^6.9.7
version: 6.9.7
@@ -520,7 +520,7 @@ importers:
version: 9.0.0(eslint@8.51.0)
eslint-config-turbo:
specifier: latest
version: 1.10.14(eslint@8.51.0)
version: 1.8.8(eslint@8.51.0)
eslint-plugin-react:
specifier: 7.33.2
version: 7.33.2(eslint@8.51.0)
@@ -640,7 +640,7 @@ importers:
version: 5.0.2
next-auth:
specifier: ^4.23.2
version: 4.23.2(next@13.5.6)(nodemailer@6.9.6)(react-dom@18.2.0)(react@18.2.0)
version: 4.23.2(next@13.5.5)(nodemailer@6.9.6)(react-dom@18.2.0)(react@18.2.0)
nodemailer:
specifier: ^6.9.6
version: 6.9.6
@@ -5114,6 +5114,10 @@ packages:
resolution: {integrity: sha512-Yac/bV5sBGkkEXmAX5FWPS9Mmo2rthrOPRQQNfycJPkjUAUclomCPH7QFVCDQ4Mp2k2K1SSM6m0zrxYrOwtFQw==}
dev: false
/@next/env@14.0.0:
resolution: {integrity: sha512-cIKhxkfVELB6hFjYsbtEeTus2mwrTC+JissfZYM0n+8Fv+g8ucUfOlm3VEDtwtwydZ0Nuauv3bl0qF82nnCAqA==}
dev: false
/@next/eslint-plugin-next@13.5.5:
resolution: {integrity: sha512-S/32s4S+SCOpW58lHKdmILAAPRdnsSei7Y3L1oZSoe5Eh0QSlzbG1nYyIpnpwWgz3T7qe3imdq7cJ6Hf29epRA==}
dependencies:
@@ -5145,8 +5149,8 @@ packages:
dev: false
optional: true
/@next/swc-darwin-arm64@13.5.6:
resolution: {integrity: sha512-5nvXMzKtZfvcu4BhtV0KH1oGv4XEW+B+jOfmBdpFI3C7FrB/MfujRpWYSBBO64+qbW8pkZiSyQv9eiwnn5VIQA==}
/@next/swc-darwin-arm64@14.0.0:
resolution: {integrity: sha512-HQKi159jCz4SRsPesVCiNN6tPSAFUkOuSkpJsqYTIlbHLKr1mD6be/J0TvWV6fwJekj81bZV9V/Tgx3C2HO9lA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
@@ -5163,8 +5167,8 @@ packages:
dev: false
optional: true
/@next/swc-darwin-x64@13.5.6:
resolution: {integrity: sha512-6cgBfxg98oOCSr4BckWjLLgiVwlL3vlLj8hXg2b+nDgm4bC/qVXXLfpLB9FHdoDu4057hzywbxKvmYGmi7yUzA==}
/@next/swc-darwin-x64@14.0.0:
resolution: {integrity: sha512-4YyQLMSaCgX/kgC1jjF3s3xSoBnwHuDhnF6WA1DWNEYRsbOOPWjcYhv8TKhRe2ApdOam+VfQSffC4ZD+X4u1Cg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
@@ -5181,8 +5185,8 @@ packages:
dev: false
optional: true
/@next/swc-linux-arm64-gnu@13.5.6:
resolution: {integrity: sha512-txagBbj1e1w47YQjcKgSU4rRVQ7uF29YpnlHV5xuVUsgCUf2FmyfJ3CPjZUvpIeXCJAoMCFAoGnbtX86BK7+sg==}
/@next/swc-linux-arm64-gnu@14.0.0:
resolution: {integrity: sha512-io7fMkJ28Glj7SH8yvnlD6naIhRDnDxeE55CmpQkj3+uaA2Hko6WGY2pT5SzpQLTnGGnviK85cy8EJ2qsETj/g==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
@@ -5199,8 +5203,8 @@ packages:
dev: false
optional: true
/@next/swc-linux-arm64-musl@13.5.6:
resolution: {integrity: sha512-cGd+H8amifT86ZldVJtAKDxUqeFyLWW+v2NlBULnLAdWsiuuN8TuhVBt8ZNpCqcAuoruoSWynvMWixTFcroq+Q==}
/@next/swc-linux-arm64-musl@14.0.0:
resolution: {integrity: sha512-nC2h0l1Jt8LEzyQeSs/BKpXAMe0mnHIMykYALWaeddTqCv5UEN8nGO3BG8JAqW/Y8iutqJsaMe2A9itS0d/r8w==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
@@ -5217,8 +5221,8 @@ packages:
dev: false
optional: true
/@next/swc-linux-x64-gnu@13.5.6:
resolution: {integrity: sha512-Mc2b4xiIWKXIhBy2NBTwOxGD3nHLmq4keFk+d4/WL5fMsB8XdJRdtUlL87SqVCTSaf1BRuQQf1HvXZcy+rq3Nw==}
/@next/swc-linux-x64-gnu@14.0.0:
resolution: {integrity: sha512-Wf+WjXibJQ7hHXOdNOmSMW5bxeJHVf46Pwb3eLSD2L76NrytQlif9NH7JpHuFlYKCQGfKfgSYYre5rIfmnSwQw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
@@ -5235,8 +5239,8 @@ packages:
dev: false
optional: true
/@next/swc-linux-x64-musl@13.5.6:
resolution: {integrity: sha512-CFHvP9Qz98NruJiUnCe61O6GveKKHpJLloXbDSWRhqhkJdZD2zU5hG+gtVJR//tyW897izuHpM6Gtf6+sNgJPQ==}
/@next/swc-linux-x64-musl@14.0.0:
resolution: {integrity: sha512-WTZb2G7B+CTsdigcJVkRxfcAIQj7Lf0ipPNRJ3vlSadU8f0CFGv/ST+sJwF5eSwIe6dxKoX0DG6OljDBaad+rg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
@@ -5253,8 +5257,8 @@ packages:
dev: false
optional: true
/@next/swc-win32-arm64-msvc@13.5.6:
resolution: {integrity: sha512-aFv1ejfkbS7PUa1qVPwzDHjQWQtknzAZWGTKYIAaS4NMtBlk3VyA6AYn593pqNanlicewqyl2jUhQAaFV/qXsg==}
/@next/swc-win32-arm64-msvc@14.0.0:
resolution: {integrity: sha512-7R8/x6oQODmNpnWVW00rlWX90sIlwluJwcvMT6GXNIBOvEf01t3fBg0AGURNKdTJg2xNuP7TyLchCL7Lh2DTiw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
@@ -5271,8 +5275,8 @@ packages:
dev: false
optional: true
/@next/swc-win32-ia32-msvc@13.5.6:
resolution: {integrity: sha512-XqqpHgEIlBHvzwG8sp/JXMFkLAfGLqkbVsyN+/Ih1mR8INb6YCc2x/Mbwi6hsAgUnqQztz8cvEbHJUbSl7RHDg==}
/@next/swc-win32-ia32-msvc@14.0.0:
resolution: {integrity: sha512-RLK1nELvhCnxaWPF07jGU4x3tjbyx2319q43loZELqF0+iJtKutZ+Lk8SVmf/KiJkYBc7Cragadz7hb3uQvz4g==}
engines: {node: '>= 10'}
cpu: [ia32]
os: [win32]
@@ -5289,8 +5293,8 @@ packages:
dev: false
optional: true
/@next/swc-win32-x64-msvc@13.5.6:
resolution: {integrity: sha512-Cqfe1YmOS7k+5mGu92nl5ULkzpKuxJrP3+4AEuPmrpFZ3BHxTY3TnHmU1On3bFmFFs6FbTcdF58CCUProGpIGQ==}
/@next/swc-win32-x64-msvc@14.0.0:
resolution: {integrity: sha512-g6hLf1SUko+hnnaywQQZzzb3BRecQsoKkF3o/C+F+dOA4w/noVAJngUVkfwF0+2/8FzNznM7ofM6TGZO9svn7w==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@@ -7293,7 +7297,7 @@ packages:
localforage: 1.10.0
dev: false
/@sentry/nextjs@7.75.0(encoding@0.1.13)(next@13.5.6)(react@18.2.0):
/@sentry/nextjs@7.75.0(encoding@0.1.13)(next@14.0.0)(react@18.2.0):
resolution: {integrity: sha512-EKdTUe5Q48qRgFM7T9s9sXwOEMvaouepHF5m343jSuTugTQ7CCJIR9jLGgUuRPgaUdE0F+PyJWopgVAZpaVFSg==}
engines: {node: '>=8'}
peerDependencies:
@@ -7314,7 +7318,7 @@ packages:
'@sentry/vercel-edge': 7.75.0
'@sentry/webpack-plugin': 1.20.0(encoding@0.1.13)
chalk: 3.0.0
next: 13.5.6(react-dom@18.2.0)(react@18.2.0)
next: 14.0.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
resolve: 1.22.8
rollup: 2.78.0
@@ -13035,13 +13039,13 @@ packages:
resolution: {integrity: sha512-NB/L/1Y30qyJcG5xZxCJKW/+bqyj+llbcCwo9DEz8bESIP0SLTOQ8T1DWCCFc+wJ61AMEstj4511PSScqMMfCw==}
dev: true
/eslint-config-turbo@1.10.14(eslint@8.51.0):
resolution: {integrity: sha512-ZeB+IcuFXy1OICkLuAplVa0euoYbhK+bMEQd0nH9+Lns18lgZRm33mVz/iSoH9VdUzl/1ZmFmoK+RpZc+8R80A==}
/eslint-config-turbo@1.8.8(eslint@8.51.0):
resolution: {integrity: sha512-+yT22sHOT5iC1sbBXfLIdXfbZuiv9bAyOXsxTxFCWelTeFFnANqmuKB3x274CFvf7WRuZ/vYP/VMjzU9xnFnxA==}
peerDependencies:
eslint: '>6.6.0'
dependencies:
eslint: 8.51.0
eslint-plugin-turbo: 1.10.14(eslint@8.51.0)
eslint-plugin-turbo: 1.8.8(eslint@8.51.0)
dev: true
/eslint-import-resolver-node@0.3.9:
@@ -13243,12 +13247,11 @@ packages:
- typescript
dev: true
/eslint-plugin-turbo@1.10.14(eslint@8.51.0):
resolution: {integrity: sha512-sBdBDnYr9AjT1g4lR3PBkZDonTrMnR4TvuGv5W0OiF7z9az1rI68yj2UHJZvjkwwcGu5mazWA1AfB0oaagpmfg==}
/eslint-plugin-turbo@1.8.8(eslint@8.51.0):
resolution: {integrity: sha512-zqyTIvveOY4YU5jviDWw9GXHd4RiKmfEgwsjBrV/a965w0PpDwJgEUoSMB/C/dU310Sv9mF3DSdEjxjJLaw6rA==}
peerDependencies:
eslint: '>6.6.0'
dependencies:
dotenv: 16.0.3
eslint: 8.51.0
dev: true
@@ -18220,7 +18223,7 @@ packages:
engines: {node: '>=10'}
dev: true
/next-auth@4.23.2(next@13.5.6)(nodemailer@6.9.6)(react-dom@18.2.0)(react@18.2.0):
/next-auth@4.23.2(next@13.5.5)(nodemailer@6.9.6)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-VRmInu0r/yZNFQheDFeOKtiugu3bt90Po3owAQDnFQ3YLQFmUKgFjcE2+3L0ny5jsJpBXaKbm7j7W2QTc6Ye2A==}
peerDependencies:
next: ^12.2.5 || ^13
@@ -18235,7 +18238,7 @@ packages:
'@panva/hkdf': 1.1.1
cookie: 0.5.0
jose: 4.15.4
next: 13.5.6(react-dom@18.2.0)(react@18.2.0)
next: 13.5.5(react-dom@18.2.0)(react@18.2.0)
nodemailer: 6.9.6
oauth: 0.9.15
openid-client: 5.6.1
@@ -18339,9 +18342,9 @@ packages:
- babel-plugin-macros
dev: false
/next@13.5.6(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-Y2wTcTbO4WwEsVb4A8VSnOsG1I9ok+h74q0ZdxkwM3EODqrs4pasq7O0iUxbcS9VtWMicG7f3+HAj0r1+NtKSw==}
engines: {node: '>=16.14.0'}
/next@14.0.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-J0jHKBJpB9zd4+c153sair0sz44mbaCHxggs8ryVXSFBuBqJ8XdE9/ozoV85xGh2VnSjahwntBZZgsihL9QznA==}
engines: {node: '>=18.17.0'}
hasBin: true
peerDependencies:
'@opentelemetry/api': ^1.1.0
@@ -18354,7 +18357,7 @@ packages:
sass:
optional: true
dependencies:
'@next/env': 13.5.6
'@next/env': 14.0.0
'@swc/helpers': 0.5.2
busboy: 1.6.0
caniuse-lite: 1.0.30001551
@@ -18364,15 +18367,15 @@ packages:
styled-jsx: 5.1.1(react@18.2.0)
watchpack: 2.4.0
optionalDependencies:
'@next/swc-darwin-arm64': 13.5.6
'@next/swc-darwin-x64': 13.5.6
'@next/swc-linux-arm64-gnu': 13.5.6
'@next/swc-linux-arm64-musl': 13.5.6
'@next/swc-linux-x64-gnu': 13.5.6
'@next/swc-linux-x64-musl': 13.5.6
'@next/swc-win32-arm64-msvc': 13.5.6
'@next/swc-win32-ia32-msvc': 13.5.6
'@next/swc-win32-x64-msvc': 13.5.6
'@next/swc-darwin-arm64': 14.0.0
'@next/swc-darwin-x64': 14.0.0
'@next/swc-linux-arm64-gnu': 14.0.0
'@next/swc-linux-arm64-musl': 14.0.0
'@next/swc-linux-x64-gnu': 14.0.0
'@next/swc-linux-x64-musl': 14.0.0
'@next/swc-win32-arm64-msvc': 14.0.0
'@next/swc-win32-ia32-msvc': 14.0.0
'@next/swc-win32-x64-msvc': 14.0.0
transitivePeerDependencies:
- '@babel/core'
- babel-plugin-macros