mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-25 16:00:16 -06:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c47face662 | ||
|
|
2361cf4b5a | ||
|
|
3720c7690d | ||
|
|
3de073f93a | ||
|
|
841b96c5bb | ||
|
|
3bb6ce3250 | ||
|
|
82c986baa4 | ||
|
|
d72283df55 | ||
|
|
fcfea44d7f | ||
|
|
5f71b91704 | ||
|
|
94e872025d | ||
|
|
7ad7a255b6 |
3
.github/workflows/release-docker.yml
vendored
3
.github/workflows/release-docker.yml
vendored
@@ -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 }}
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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`
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 . .
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)] }
|
||||
)();
|
||||
|
||||
34
packages/lib/apiKey/cache.ts
Normal file
34
packages/lib/apiKey/cache.ts
Normal 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));
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -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) {
|
||||
|
||||
34
packages/lib/attributeClass/cache.ts
Normal file
34
packages/lib/attributeClass/cache.ts
Normal 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));
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -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}`);
|
||||
|
||||
14
packages/lib/attributeClass/util.ts
Normal file
14
packages/lib/attributeClass/util.ts
Normal 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;
|
||||
};
|
||||
@@ -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",
|
||||
|
||||
@@ -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)],
|
||||
}
|
||||
)();
|
||||
};
|
||||
|
||||
34
packages/lib/environment/cache.ts
Normal file
34
packages/lib/environment/cache.ts
Normal 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));
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
14
packages/lib/environment/util.ts
Normal file
14
packages/lib/environment/util.ts
Normal 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;
|
||||
};
|
||||
34
packages/lib/integration/cache.ts
Normal file
34
packages/lib/integration/cache.ts
Normal 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));
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -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) {
|
||||
|
||||
26
packages/lib/invite/cache.ts
Normal file
26
packages/lib/invite/cache.ts
Normal 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));
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
14
packages/lib/invite/util.ts
Normal file
14
packages/lib/invite/util.ts
Normal 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;
|
||||
};
|
||||
26
packages/lib/membership/cache.ts
Normal file
26
packages/lib/membership/cache.ts
Normal 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));
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)],
|
||||
}
|
||||
)();
|
||||
|
||||
42
packages/lib/product/cache.ts
Normal file
42
packages/lib/product/cache.ts
Normal 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));
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
26
packages/lib/profile/cache.ts
Normal file
26
packages/lib/profile/cache.ts
Normal 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));
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -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) {
|
||||
|
||||
@@ -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`;
|
||||
},
|
||||
|
||||
@@ -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) {
|
||||
|
||||
26
packages/lib/responseNote/cache.ts
Normal file
26
packages/lib/responseNote/cache.ts
Normal 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));
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
26
packages/lib/session/cache.ts
Normal file
26
packages/lib/session/cache.ts
Normal 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));
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -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) {
|
||||
|
||||
17
packages/lib/session/util.ts
Normal file
17
packages/lib/session/util.ts
Normal 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;
|
||||
};
|
||||
@@ -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)] }
|
||||
)();
|
||||
|
||||
42
packages/lib/survey/cache.ts
Normal file
42
packages/lib/survey/cache.ts
Normal 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));
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)],
|
||||
}
|
||||
)();
|
||||
|
||||
27
packages/lib/tagOnResponse/cache.ts
Normal file
27
packages/lib/tagOnResponse/cache.ts
Normal 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));
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -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,
|
||||
}
|
||||
)();
|
||||
|
||||
34
packages/lib/team/cache.ts
Normal file
34
packages/lib/team/cache.ts
Normal 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));
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
)();
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
)();
|
||||
|
||||
35
packages/lib/webhook/cache.ts
Normal file
35
packages/lib/webhook/cache.ts
Normal 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));
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -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
99
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user