chore: add react cache to improve render performance (#2867)

This commit is contained in:
Matti Nannt
2024-07-10 11:43:59 +02:00
committed by GitHub
parent 1459229dde
commit 32ae38ebb2
30 changed files with 2503 additions and 2196 deletions

View File

@@ -1,4 +1,4 @@
import z from "zod";
import { z } from "zod";
export const ZActionProperties = z.record(z.string());
export { ZActionClassNoCodeConfig } from "@formbricks/types/actionClasses";

View File

@@ -8,7 +8,7 @@ import {
Users2Icon,
} from "lucide-react";
import { useEffect, useState } from "react";
import z from "zod";
import { z } from "zod";
import { cn } from "@formbricks/lib/cn";
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
import {

View File

@@ -1,6 +1,7 @@
import "server-only";
import { Prisma } from "@prisma/client";
import { differenceInDays } from "date-fns";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { TAction, TActionInput, ZActionInput } from "@formbricks/types/actions";
import { ZOptionalNumber } from "@formbricks/types/common";
@@ -16,96 +17,100 @@ import { validateInputs } from "../utils/validate";
import { actionCache } from "./cache";
import { getStartDateOfLastMonth, getStartDateOfLastQuarter, getStartDateOfLastWeek } from "./utils";
export const getActionsByPersonId = (personId: string, page?: number): Promise<TAction[]> =>
cache(
async () => {
validateInputs([personId, ZId], [page, ZOptionalNumber]);
export const getActionsByPersonId = reactCache(
async (personId: string, page?: number): Promise<TAction[]> =>
cache(
async () => {
validateInputs([personId, ZId], [page, ZOptionalNumber]);
try {
const actionsPrisma = await prisma.action.findMany({
where: {
person: {
id: personId,
try {
const actionsPrisma = await prisma.action.findMany({
where: {
person: {
id: personId,
},
},
},
orderBy: {
createdAt: "desc",
},
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
include: {
actionClass: true,
},
});
return actionsPrisma.map((action) => ({
id: action.id,
createdAt: action.createdAt,
personId: action.personId,
properties: action.properties,
actionClass: action.actionClass,
}));
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError("Database operation failed");
}
throw error;
}
},
[`getActionsByPersonId-${personId}-${page}`],
{
tags: [actionCache.tag.byPersonId(personId)],
}
)();
export const getActionsByEnvironmentId = (environmentId: string, page?: number): Promise<TAction[]> =>
cache(
async () => {
validateInputs([environmentId, ZId], [page, ZOptionalNumber]);
try {
const actionsPrisma = await prisma.action.findMany({
where: {
actionClass: {
environmentId: environmentId,
orderBy: {
createdAt: "desc",
},
},
orderBy: {
createdAt: "desc",
},
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
include: {
actionClass: true,
},
});
const actions: TAction[] = [];
// transforming response to type TAction[]
actionsPrisma.forEach((action) => {
actions.push({
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
include: {
actionClass: true,
},
});
return actionsPrisma.map((action) => ({
id: action.id,
createdAt: action.createdAt,
// sessionId: action.sessionId,
personId: action.personId,
properties: action.properties,
actionClass: action.actionClass,
});
});
return actions;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError("Database operation failed");
}
}));
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError("Database operation failed");
}
throw error;
throw error;
}
},
[`getActionsByPersonId-${personId}-${page}`],
{
tags: [actionCache.tag.byPersonId(personId)],
}
},
[`getActionsByEnvironmentId-${environmentId}-${page}`],
{
tags: [actionCache.tag.byEnvironmentId(environmentId)],
}
)();
)()
);
export const getActionsByEnvironmentId = reactCache(
async (environmentId: string, page?: number): Promise<TAction[]> =>
cache(
async () => {
validateInputs([environmentId, ZId], [page, ZOptionalNumber]);
try {
const actionsPrisma = await prisma.action.findMany({
where: {
actionClass: {
environmentId: environmentId,
},
},
orderBy: {
createdAt: "desc",
},
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
include: {
actionClass: true,
},
});
const actions: TAction[] = [];
// transforming response to type TAction[]
actionsPrisma.forEach((action) => {
actions.push({
id: action.id,
createdAt: action.createdAt,
// sessionId: action.sessionId,
personId: action.personId,
properties: action.properties,
actionClass: action.actionClass,
});
});
return actions;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError("Database operation failed");
}
throw error;
}
},
[`getActionsByEnvironmentId-${environmentId}-${page}`],
{
tags: [actionCache.tag.byEnvironmentId(environmentId)],
}
)()
);
export const createAction = async (data: TActionInput): Promise<TAction> => {
validateInputs([data, ZActionInput]);
@@ -165,255 +170,273 @@ export const createAction = async (data: TActionInput): Promise<TAction> => {
}
};
export const getActionCountInLastHour = async (actionClassId: string): Promise<number> =>
cache(
async () => {
validateInputs([actionClassId, ZId]);
export const getActionCountInLastHour = reactCache(
async (actionClassId: string): Promise<number> =>
cache(
async () => {
validateInputs([actionClassId, ZId]);
try {
const numEventsLastHour = await prisma.action.count({
where: {
actionClassId: actionClassId,
createdAt: {
gte: new Date(Date.now() - 60 * 60 * 1000),
try {
const numEventsLastHour = await prisma.action.count({
where: {
actionClassId: actionClassId,
createdAt: {
gte: new Date(Date.now() - 60 * 60 * 1000),
},
},
},
});
return numEventsLastHour;
} catch (error) {
throw error;
});
return numEventsLastHour;
} catch (error) {
throw error;
}
},
[`getActionCountInLastHour-${actionClassId}`],
{
tags: [actionClassCache.tag.byId(actionClassId)],
}
},
[`getActionCountInLastHour-${actionClassId}`],
{
tags: [actionClassCache.tag.byId(actionClassId)],
}
)();
)()
);
export const getActionCountInLast24Hours = async (actionClassId: string): Promise<number> =>
cache(
async () => {
validateInputs([actionClassId, ZId]);
export const getActionCountInLast24Hours = reactCache(
async (actionClassId: string): Promise<number> =>
cache(
async () => {
validateInputs([actionClassId, ZId]);
try {
const numEventsLast24Hours = await prisma.action.count({
where: {
actionClassId: actionClassId,
createdAt: {
gte: new Date(Date.now() - 24 * 60 * 60 * 1000),
try {
const numEventsLast24Hours = await prisma.action.count({
where: {
actionClassId: actionClassId,
createdAt: {
gte: new Date(Date.now() - 24 * 60 * 60 * 1000),
},
},
},
});
return numEventsLast24Hours;
} catch (error) {
throw error;
});
return numEventsLast24Hours;
} catch (error) {
throw error;
}
},
[`getActionCountInLast24Hours-${actionClassId}`],
{
tags: [actionClassCache.tag.byId(actionClassId)],
}
},
[`getActionCountInLast24Hours-${actionClassId}`],
{
tags: [actionClassCache.tag.byId(actionClassId)],
}
)();
)()
);
export const getActionCountInLast7Days = async (actionClassId: string): Promise<number> =>
cache(
async () => {
validateInputs([actionClassId, ZId]);
export const getActionCountInLast7Days = reactCache(
async (actionClassId: string): Promise<number> =>
cache(
async () => {
validateInputs([actionClassId, ZId]);
try {
const numEventsLast7Days = await prisma.action.count({
where: {
actionClassId: actionClassId,
createdAt: {
gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
try {
const numEventsLast7Days = await prisma.action.count({
where: {
actionClassId: actionClassId,
createdAt: {
gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
},
},
},
});
return numEventsLast7Days;
} catch (error) {
throw error;
});
return numEventsLast7Days;
} catch (error) {
throw error;
}
},
[`getActionCountInLast7Days-${actionClassId}`],
{
tags: [actionClassCache.tag.byId(actionClassId)],
}
},
[`getActionCountInLast7Days-${actionClassId}`],
{
tags: [actionClassCache.tag.byId(actionClassId)],
}
)();
)()
);
export const getActionCountInLastQuarter = (actionClassId: string, personId: string): Promise<number> =>
cache(
async () => {
validateInputs([actionClassId, ZId], [personId, ZId]);
export const getActionCountInLastQuarter = reactCache(
async (actionClassId: string, personId: string): Promise<number> =>
cache(
async () => {
validateInputs([actionClassId, ZId], [personId, ZId]);
try {
const numEventsLastQuarter = await prisma.action.count({
where: {
personId,
actionClass: {
id: actionClassId,
try {
const numEventsLastQuarter = await prisma.action.count({
where: {
personId,
actionClass: {
id: actionClassId,
},
createdAt: {
gte: getStartDateOfLastQuarter(),
},
},
createdAt: {
gte: getStartDateOfLastQuarter(),
},
},
});
});
return numEventsLastQuarter;
} catch (error) {
throw error;
return numEventsLastQuarter;
} catch (error) {
throw error;
}
},
[`getActionCountInLastQuarter-${actionClassId}-${personId}`],
{
tags: [actionClassCache.tag.byId(actionClassId)],
}
},
[`getActionCountInLastQuarter-${actionClassId}-${personId}`],
{
tags: [actionClassCache.tag.byId(actionClassId)],
}
)();
)()
);
export const getActionCountInLastMonth = (actionClassId: string, personId: string): Promise<number> =>
cache(
async () => {
validateInputs([actionClassId, ZId], [personId, ZId]);
export const getActionCountInLastMonth = reactCache(
async (actionClassId: string, personId: string): Promise<number> =>
cache(
async () => {
validateInputs([actionClassId, ZId], [personId, ZId]);
try {
const numEventsLastMonth = await prisma.action.count({
where: {
personId,
actionClass: {
id: actionClassId,
try {
const numEventsLastMonth = await prisma.action.count({
where: {
personId,
actionClass: {
id: actionClassId,
},
createdAt: {
gte: getStartDateOfLastMonth(),
},
},
createdAt: {
gte: getStartDateOfLastMonth(),
},
},
});
});
return numEventsLastMonth;
} catch (error) {
throw error;
return numEventsLastMonth;
} catch (error) {
throw error;
}
},
[`getActionCountInLastMonth-${actionClassId}-${personId}`],
{
tags: [actionClassCache.tag.byId(actionClassId)],
}
},
[`getActionCountInLastMonth-${actionClassId}-${personId}`],
{
tags: [actionClassCache.tag.byId(actionClassId)],
}
)();
)()
);
export const getActionCountInLastWeek = (actionClassId: string, personId: string): Promise<number> =>
cache(
async () => {
validateInputs([actionClassId, ZId], [personId, ZId]);
export const getActionCountInLastWeek = reactCache(
async (actionClassId: string, personId: string): Promise<number> =>
cache(
async () => {
validateInputs([actionClassId, ZId], [personId, ZId]);
try {
const numEventsLastWeek = await prisma.action.count({
where: {
personId,
actionClass: {
id: actionClassId,
try {
const numEventsLastWeek = await prisma.action.count({
where: {
personId,
actionClass: {
id: actionClassId,
},
createdAt: {
gte: getStartDateOfLastWeek(),
},
},
createdAt: {
gte: getStartDateOfLastWeek(),
},
},
});
return numEventsLastWeek;
} catch (error) {
throw error;
});
return numEventsLastWeek;
} catch (error) {
throw error;
}
},
[`getActionCountInLastWeek-${actionClassId}-${personId}`],
{
tags: [actionClassCache.tag.byId(actionClassId)],
}
},
[`getActionCountInLastWeek-${actionClassId}-${personId}`],
{
tags: [actionClassCache.tag.byId(actionClassId)],
}
)();
)()
);
export const getTotalOccurrencesForAction = (actionClassId: string, personId: string): Promise<number> =>
cache(
async () => {
validateInputs([actionClassId, ZId], [personId, ZId]);
export const getTotalOccurrencesForAction = reactCache(
async (actionClassId: string, personId: string): Promise<number> =>
cache(
async () => {
validateInputs([actionClassId, ZId], [personId, ZId]);
try {
const count = await prisma.action.count({
where: {
personId,
actionClass: {
id: actionClassId,
try {
const count = await prisma.action.count({
where: {
personId,
actionClass: {
id: actionClassId,
},
},
},
});
});
return count;
} catch (error) {
throw error;
return count;
} catch (error) {
throw error;
}
},
[`getTotalOccurrencesForAction-${actionClassId}-${personId}`],
{
tags: [actionClassCache.tag.byId(actionClassId)],
}
},
[`getTotalOccurrencesForAction-${actionClassId}-${personId}`],
{
tags: [actionClassCache.tag.byId(actionClassId)],
}
)();
)()
);
export const getLastOccurrenceDaysAgo = (actionClassId: string, personId: string): Promise<number | null> =>
cache(
async () => {
validateInputs([actionClassId, ZId], [personId, ZId]);
export const getLastOccurrenceDaysAgo = reactCache(
async (actionClassId: string, personId: string): Promise<number | null> =>
cache(
async () => {
validateInputs([actionClassId, ZId], [personId, ZId]);
try {
const lastEvent = await prisma.action.findFirst({
where: {
personId,
actionClass: {
id: actionClassId,
try {
const lastEvent = await prisma.action.findFirst({
where: {
personId,
actionClass: {
id: actionClassId,
},
},
},
orderBy: {
createdAt: "desc",
},
select: {
createdAt: true,
},
});
if (!lastEvent) return null;
return differenceInDays(new Date(), lastEvent.createdAt);
} catch (error) {
throw error;
}
},
[`getLastOccurrenceDaysAgo-${actionClassId}-${personId}`],
{
tags: [actionClassCache.tag.byId(actionClassId)],
}
)();
export const getFirstOccurrenceDaysAgo = (actionClassId: string, personId: string): Promise<number | null> =>
cache(
async () => {
validateInputs([actionClassId, ZId], [personId, ZId]);
try {
const firstEvent = await prisma.action.findFirst({
where: {
personId,
actionClass: {
id: actionClassId,
orderBy: {
createdAt: "desc",
},
},
orderBy: {
createdAt: "asc",
},
select: {
createdAt: true,
},
});
select: {
createdAt: true,
},
});
if (!firstEvent) return null;
return differenceInDays(new Date(), firstEvent.createdAt);
} catch (error) {
throw error;
if (!lastEvent) return null;
return differenceInDays(new Date(), lastEvent.createdAt);
} catch (error) {
throw error;
}
},
[`getLastOccurrenceDaysAgo-${actionClassId}-${personId}`],
{
tags: [actionClassCache.tag.byId(actionClassId)],
}
},
[`getFirstOccurrenceDaysAgo-${actionClassId}-${personId}`],
{
tags: [actionClassCache.tag.byId(actionClassId)],
}
)();
)()
);
export const getFirstOccurrenceDaysAgo = reactCache(
async (actionClassId: string, personId: string): Promise<number | null> =>
cache(
async () => {
validateInputs([actionClassId, ZId], [personId, ZId]);
try {
const firstEvent = await prisma.action.findFirst({
where: {
personId,
actionClass: {
id: actionClassId,
},
},
orderBy: {
createdAt: "asc",
},
select: {
createdAt: true,
},
});
if (!firstEvent) return null;
return differenceInDays(new Date(), firstEvent.createdAt);
} catch (error) {
throw error;
}
},
[`getFirstOccurrenceDaysAgo-${actionClassId}-${personId}`],
{
tags: [actionClassCache.tag.byId(actionClassId)],
}
)()
);

View File

@@ -2,6 +2,7 @@
import "server-only";
import { Prisma } from "@prisma/client";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { TActionClass, TActionClassInput, ZActionClassInput } from "@formbricks/types/actionClasses";
import { ZOptionalNumber, ZString } from "@formbricks/types/common";
@@ -24,85 +25,88 @@ const selectActionClass = {
environmentId: true,
} satisfies Prisma.ActionClassSelect;
export const getActionClasses = (environmentId: string, page?: number): Promise<TActionClass[]> =>
cache(
async () => {
validateInputs([environmentId, ZId], [page, ZOptionalNumber]);
export const getActionClasses = reactCache(
async (environmentId: string, page?: number): Promise<TActionClass[]> =>
cache(
async () => {
validateInputs([environmentId, ZId], [page, ZOptionalNumber]);
try {
return await prisma.actionClass.findMany({
where: {
environmentId: environmentId,
},
select: selectActionClass,
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
orderBy: {
createdAt: "asc",
},
});
} catch (error) {
throw new DatabaseError(`Database error when fetching actions for environment ${environmentId}`);
try {
return await prisma.actionClass.findMany({
where: {
environmentId: environmentId,
},
select: selectActionClass,
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
orderBy: {
createdAt: "asc",
},
});
} catch (error) {
throw new DatabaseError(`Database error when fetching actions for environment ${environmentId}`);
}
},
[`getActionClasses-${environmentId}-${page}`],
{
tags: [actionClassCache.tag.byEnvironmentId(environmentId)],
}
},
[`getActionClasses-${environmentId}-${page}`],
{
tags: [actionClassCache.tag.byEnvironmentId(environmentId)],
}
)();
)()
);
// This function is used to get an action by its name and environmentId(it can return private actions as well)
export const getActionClassByEnvironmentIdAndName = (
environmentId: string,
name: string
): Promise<TActionClass | null> =>
cache(
async () => {
validateInputs([environmentId, ZId], [name, ZString]);
export const getActionClassByEnvironmentIdAndName = reactCache(
async (environmentId: string, name: string): Promise<TActionClass | null> =>
cache(
async () => {
validateInputs([environmentId, ZId], [name, ZString]);
try {
const actionClass = await prisma.actionClass.findFirst({
where: {
name,
environmentId,
},
select: selectActionClass,
});
try {
const actionClass = await prisma.actionClass.findFirst({
where: {
name,
environmentId,
},
select: selectActionClass,
});
return actionClass;
} catch (error) {
throw new DatabaseError(`Database error when fetching action`);
return actionClass;
} catch (error) {
throw new DatabaseError(`Database error when fetching action`);
}
},
[`getActionClassByEnvironmentIdAndName-${environmentId}-${name}`],
{
tags: [actionClassCache.tag.byNameAndEnvironmentId(name, environmentId)],
}
},
[`getActionClassByEnvironmentIdAndName-${environmentId}-${name}`],
{
tags: [actionClassCache.tag.byNameAndEnvironmentId(name, environmentId)],
}
)();
)()
);
export const getActionClass = (actionClassId: string): Promise<TActionClass | null> =>
cache(
async () => {
validateInputs([actionClassId, ZId]);
export const getActionClass = reactCache(
async (actionClassId: string): Promise<TActionClass | null> =>
cache(
async () => {
validateInputs([actionClassId, ZId]);
try {
const actionClass = await prisma.actionClass.findUnique({
where: {
id: actionClassId,
},
select: selectActionClass,
});
try {
const actionClass = await prisma.actionClass.findUnique({
where: {
id: actionClassId,
},
select: selectActionClass,
});
return actionClass;
} catch (error) {
throw new DatabaseError(`Database error when fetching action`);
return actionClass;
} catch (error) {
throw new DatabaseError(`Database error when fetching action`);
}
},
[`getActionClass-${actionClassId}`],
{
tags: [actionClassCache.tag.byId(actionClassId)],
}
},
[`getActionClass-${actionClassId}`],
{
tags: [actionClassCache.tag.byId(actionClassId)],
}
)();
)()
);
export const deleteActionClass = async (
environmentId: string,

View File

@@ -1,6 +1,7 @@
import "server-only";
import { Prisma } from "@prisma/client";
import { createHash, randomBytes } from "crypto";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { TApiKey, TApiKeyCreateInput, ZApiKeyCreateInput } from "@formbricks/types/apiKeys";
import { ZOptionalNumber, ZString } from "@formbricks/types/common";
@@ -12,64 +13,68 @@ import { getHash } from "../crypto";
import { validateInputs } from "../utils/validate";
import { apiKeyCache } from "./cache";
export const getApiKey = (apiKeyId: string): Promise<TApiKey | null> =>
cache(
async () => {
validateInputs([apiKeyId, ZString]);
export const getApiKey = reactCache(
(apiKeyId: string): Promise<TApiKey | null> =>
cache(
async () => {
validateInputs([apiKeyId, ZString]);
if (!apiKeyId) {
throw new InvalidInputError("API key cannot be null or undefined.");
}
try {
const apiKeyData = await prisma.apiKey.findUnique({
where: {
id: apiKeyId,
},
});
return apiKeyData;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
if (!apiKeyId) {
throw new InvalidInputError("API key cannot be null or undefined.");
}
throw error;
}
},
[`getApiKey-${apiKeyId}`],
{
tags: [apiKeyCache.tag.byId(apiKeyId)],
}
)();
try {
const apiKeyData = await prisma.apiKey.findUnique({
where: {
id: apiKeyId,
},
});
export const getApiKeys = (environmentId: string, page?: number): Promise<TApiKey[]> =>
cache(
async () => {
validateInputs([environmentId, ZId], [page, ZOptionalNumber]);
return apiKeyData;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
try {
const apiKeys = await prisma.apiKey.findMany({
where: {
environmentId,
},
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
return apiKeys;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
throw error;
}
throw error;
},
[`getApiKey-${apiKeyId}`],
{
tags: [apiKeyCache.tag.byId(apiKeyId)],
}
},
[`getApiKeys-${environmentId}-${page}`],
{
tags: [apiKeyCache.tag.byEnvironmentId(environmentId)],
}
)();
)()
);
export const getApiKeys = reactCache(
(environmentId: string, page?: number): Promise<TApiKey[]> =>
cache(
async () => {
validateInputs([environmentId, ZId], [page, ZOptionalNumber]);
try {
const apiKeys = await prisma.apiKey.findMany({
where: {
environmentId,
},
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
return apiKeys;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
},
[`getApiKeys-${environmentId}-${page}`],
{
tags: [apiKeyCache.tag.byEnvironmentId(environmentId)],
}
)()
);
export const hashApiKey = (key: string): string => createHash("sha256").update(key).digest("hex");
@@ -105,9 +110,8 @@ export const createApiKey = async (
}
};
export const getApiKeyFromKey = (apiKey: string): Promise<TApiKey | null> => {
export const getApiKeyFromKey = reactCache((apiKey: string): Promise<TApiKey | null> => {
const hashedKey = getHash(apiKey);
return cache(
async () => {
validateInputs([apiKey, ZString]);
@@ -137,7 +141,7 @@ export const getApiKeyFromKey = (apiKey: string): Promise<TApiKey | null> => {
tags: [apiKeyCache.tag.byHashedKey(hashedKey)],
}
)();
};
});
export const deleteApiKey = async (id: string): Promise<TApiKey | null> => {
validateInputs([id, ZId]);

View File

@@ -1,5 +1,6 @@
import "server-only";
import { Prisma } from "@prisma/client";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { TAttributes, ZAttributes } from "@formbricks/types/attributes";
import { ZString } from "@formbricks/types/common";
@@ -38,67 +39,71 @@ const convertPrismaAttributes = (prismaAttributes: any): TAttributes => {
);
};
export const getAttributes = (personId: string): Promise<TAttributes> =>
cache(
async () => {
validateInputs([personId, ZId]);
export const getAttributes = reactCache(
(personId: string): Promise<TAttributes> =>
cache(
async () => {
validateInputs([personId, ZId]);
try {
const prismaAttributes = await prisma.attribute.findMany({
where: {
personId,
},
select: selectAttribute,
});
try {
const prismaAttributes = await prisma.attribute.findMany({
where: {
personId,
},
select: selectAttribute,
});
return convertPrismaAttributes(prismaAttributes);
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
return convertPrismaAttributes(prismaAttributes);
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
},
[`getAttributes-${personId}`],
{
tags: [attributeCache.tag.byPersonId(personId)],
}
)()
);
export const getAttributesByUserId = reactCache(
(environmentId: string, userId: string): Promise<TAttributes> =>
cache(
async () => {
validateInputs([environmentId, ZId], [userId, ZString]);
const person = await getPersonByUserId(environmentId, userId);
if (!person) {
throw new Error("Person not found");
}
throw error;
}
},
[`getAttributes-${personId}`],
{
tags: [attributeCache.tag.byPersonId(personId)],
}
)();
try {
const prismaAttributes = await prisma.attribute.findMany({
where: {
personId: person.id,
},
select: selectAttribute,
});
export const getAttributesByUserId = (environmentId: string, userId: string): Promise<TAttributes> =>
cache(
async () => {
validateInputs([environmentId, ZId], [userId, ZString]);
return convertPrismaAttributes(prismaAttributes);
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
const person = await getPersonByUserId(environmentId, userId);
if (!person) {
throw new Error("Person not found");
}
try {
const prismaAttributes = await prisma.attribute.findMany({
where: {
personId: person.id,
},
select: selectAttribute,
});
return convertPrismaAttributes(prismaAttributes);
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
throw error;
}
throw error;
},
[`getAttributesByUserId-${environmentId}-${userId}`],
{
tags: [attributeCache.tag.byEnvironmentIdAndUserId(environmentId, userId)],
}
},
[`getAttributesByUserId-${environmentId}-${userId}`],
{
tags: [attributeCache.tag.byEnvironmentIdAndUserId(environmentId, userId)],
}
)();
)()
);
export const getAttribute = (name: string, personId: string): Promise<string | undefined> =>
cache(

View File

@@ -2,6 +2,7 @@
import "server-only";
import { Prisma } from "@prisma/client";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import {
TAttributeClass,
@@ -18,68 +19,72 @@ import { ITEMS_PER_PAGE, MAX_ATTRIBUTE_CLASSES_PER_ENVIRONMENT } from "../consta
import { validateInputs } from "../utils/validate";
import { attributeClassCache } from "./cache";
export const getAttributeClass = async (attributeClassId: string): Promise<TAttributeClass | null> =>
cache(
async () => {
validateInputs([attributeClassId, ZId]);
export const getAttributeClass = reactCache(
async (attributeClassId: string): Promise<TAttributeClass | null> =>
cache(
async () => {
validateInputs([attributeClassId, ZId]);
try {
const attributeClass = await prisma.attributeClass.findFirst({
where: {
id: attributeClassId,
},
});
try {
const attributeClass = await prisma.attributeClass.findFirst({
where: {
id: attributeClassId,
},
});
return attributeClass;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
},
[`getAttributeClass-${attributeClassId}`],
{
tags: [attributeClassCache.tag.byId(attributeClassId)],
}
)();
export const getAttributeClasses = async (environmentId: string, page?: number): Promise<TAttributeClass[]> =>
cache(
async () => {
validateInputs([environmentId, ZId], [page, ZOptionalNumber]);
try {
const attributeClasses = await prisma.attributeClass.findMany({
where: {
environmentId: environmentId,
},
orderBy: {
createdAt: "asc",
},
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
return attributeClasses.filter((attributeClass) => {
if (attributeClass.name === "userId" && attributeClass.type === "automatic") {
return false;
return attributeClass;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
return true;
});
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
throw error;
}
throw error;
},
[`getAttributeClass-${attributeClassId}`],
{
tags: [attributeClassCache.tag.byId(attributeClassId)],
}
},
[`getAttributeClasses-${environmentId}-${page}`],
{
tags: [attributeClassCache.tag.byEnvironmentId(environmentId)],
}
)();
)()
);
export const getAttributeClasses = reactCache(
async (environmentId: string, page?: number): Promise<TAttributeClass[]> =>
cache(
async () => {
validateInputs([environmentId, ZId], [page, ZOptionalNumber]);
try {
const attributeClasses = await prisma.attributeClass.findMany({
where: {
environmentId: environmentId,
},
orderBy: {
createdAt: "asc",
},
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
return attributeClasses.filter((attributeClass) => {
if (attributeClass.name === "userId" && attributeClass.type === "automatic") {
return false;
}
return true;
});
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
},
[`getAttributeClasses-${environmentId}-${page}`],
{
tags: [attributeClassCache.tag.byEnvironmentId(environmentId)],
}
)()
);
export const updateAttributeClass = async (
attributeClassId: string,
@@ -113,7 +118,7 @@ export const updateAttributeClass = async (
}
};
export const getAttributeClassByName = async (environmentId: string, name: string) =>
export const getAttributeClassByName = reactCache((environmentId: string, name: string) =>
cache(
async (): Promise<TAttributeClass | null> => {
validateInputs([environmentId, ZId], [name, ZString]);
@@ -138,7 +143,8 @@ export const getAttributeClassByName = async (environmentId: string, name: strin
{
tags: [attributeClassCache.tag.byEnvironmentIdAndName(environmentId, name)],
}
)();
)()
);
export const createAttributeClass = async (
environmentId: string,
@@ -208,26 +214,28 @@ export const deleteAttributeClass = async (attributeClassId: string): Promise<TA
}
};
export const getAttributeClassesCount = async (environmentId: string): Promise<number> =>
cache(
async () => {
validateInputs([environmentId, ZId]);
export const getAttributeClassesCount = reactCache(
async (environmentId: string): Promise<number> =>
cache(
async () => {
validateInputs([environmentId, ZId]);
try {
return prisma.attributeClass.count({
where: {
environmentId,
},
});
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
try {
return prisma.attributeClass.count({
where: {
environmentId,
},
});
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
throw error;
},
[`getAttributeClassesCount-${environmentId}`],
{
tags: [attributeClassCache.tag.byEnvironmentId(environmentId)],
}
},
[`getAttributeClassesCount-${environmentId}`],
{
tags: [attributeClassCache.tag.byEnvironmentId(environmentId)],
}
)();
)()
);

View File

@@ -1,5 +1,6 @@
import "server-only";
import { Prisma } from "@prisma/client";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { ZOptionalNumber } from "@formbricks/types/common";
import {
@@ -33,33 +34,35 @@ export const selectDisplay = {
status: true,
};
export const getDisplay = (displayId: string): Promise<TDisplay | null> =>
cache(
async () => {
validateInputs([displayId, ZId]);
export const getDisplay = reactCache(
async (displayId: string): Promise<TDisplay | null> =>
cache(
async () => {
validateInputs([displayId, ZId]);
try {
const display = await prisma.display.findUnique({
where: {
id: displayId,
},
select: selectDisplay,
});
try {
const display = await prisma.display.findUnique({
where: {
id: displayId,
},
select: selectDisplay,
});
return display;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
return display;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
throw error;
},
[`getDisplay-${displayId}`],
{
tags: [displayCache.tag.byId(displayId)],
}
},
[`getDisplay-${displayId}`],
{
tags: [displayCache.tag.byId(displayId)],
}
)();
)()
);
export const updateDisplay = async (
displayId: string,
@@ -272,38 +275,40 @@ export const markDisplayRespondedLegacy = async (displayId: string): Promise<TDi
}
};
export const getDisplaysByPersonId = (personId: string, page?: number): Promise<TDisplay[]> =>
cache(
async () => {
validateInputs([personId, ZId], [page, ZOptionalNumber]);
export const getDisplaysByPersonId = reactCache(
async (personId: string, page?: number): Promise<TDisplay[]> =>
cache(
async () => {
validateInputs([personId, ZId], [page, ZOptionalNumber]);
try {
const displays = await prisma.display.findMany({
where: {
personId: personId,
},
select: selectDisplay,
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
orderBy: {
createdAt: "desc",
},
});
try {
const displays = await prisma.display.findMany({
where: {
personId: personId,
},
select: selectDisplay,
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
orderBy: {
createdAt: "desc",
},
});
return displays;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
return displays;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
throw error;
},
[`getDisplaysByPersonId-${personId}-${page}`],
{
tags: [displayCache.tag.byPersonId(personId)],
}
},
[`getDisplaysByPersonId-${personId}-${page}`],
{
tags: [displayCache.tag.byPersonId(personId)],
}
)();
)()
);
export const deleteDisplayByResponseId = async (
responseId: string,
@@ -333,34 +338,36 @@ export const deleteDisplayByResponseId = async (
}
};
export const getDisplayCountBySurveyId = (surveyId: string, filters?: TDisplayFilters): Promise<number> =>
cache(
async () => {
validateInputs([surveyId, ZId]);
export const getDisplayCountBySurveyId = reactCache(
(surveyId: string, filters?: TDisplayFilters): Promise<number> =>
cache(
async () => {
validateInputs([surveyId, ZId]);
try {
const displayCount = await prisma.display.count({
where: {
surveyId: surveyId,
...(filters &&
filters.createdAt && {
createdAt: {
gte: filters.createdAt.min,
lte: filters.createdAt.max,
},
}),
},
});
return displayCount;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
try {
const displayCount = await prisma.display.count({
where: {
surveyId: surveyId,
...(filters &&
filters.createdAt && {
createdAt: {
gte: filters.createdAt.min,
lte: filters.createdAt.max,
},
}),
},
});
return displayCount;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
throw error;
},
[`getDisplayCountBySurveyId-${surveyId}-${JSON.stringify(filters)}`],
{
tags: [displayCache.tag.bySurveyId(surveyId)],
}
},
[`getDisplayCountBySurveyId-${surveyId}-${JSON.stringify(filters)}`],
{
tags: [displayCache.tag.bySurveyId(surveyId)],
}
)();
)()
);

View File

@@ -1,5 +1,6 @@
import "server-only";
import { Prisma } from "@prisma/client";
import { cache as reactCache } from "react";
import { z } from "zod";
import { prisma } from "@formbricks/database";
import type {
@@ -20,78 +21,82 @@ import { getProducts } from "../product/service";
import { validateInputs } from "../utils/validate";
import { environmentCache } from "./cache";
export const getEnvironment = (environmentId: string): Promise<TEnvironment | null> =>
cache(
async () => {
validateInputs([environmentId, ZId]);
export const getEnvironment = reactCache(
(environmentId: string): Promise<TEnvironment | null> =>
cache(
async () => {
validateInputs([environmentId, ZId]);
try {
const environment = await prisma.environment.findUnique({
where: {
id: environmentId,
},
});
return environment;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
console.error(error);
throw new DatabaseError(error.message);
try {
const environment = await prisma.environment.findUnique({
where: {
id: environmentId,
},
});
return environment;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
console.error(error);
throw new DatabaseError(error.message);
}
throw error;
}
},
[`getEnvironment-${environmentId}`],
{
tags: [environmentCache.tag.byId(environmentId)],
}
)()
);
export const getEnvironments = reactCache(
(productId: string): Promise<TEnvironment[]> =>
cache(
async (): Promise<TEnvironment[]> => {
validateInputs([productId, ZId]);
let productPrisma;
try {
productPrisma = await prisma.product.findFirst({
where: {
id: productId,
},
include: {
environments: true,
},
});
if (!productPrisma) {
throw new ResourceNotFoundError("Product", productId);
}
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
throw error;
}
},
[`getEnvironment-${environmentId}`],
{
tags: [environmentCache.tag.byId(environmentId)],
}
)();
export const getEnvironments = async (productId: string): Promise<TEnvironment[]> =>
cache(
async (): Promise<TEnvironment[]> => {
validateInputs([productId, ZId]);
let productPrisma;
try {
productPrisma = await prisma.product.findFirst({
where: {
id: productId,
},
include: {
environments: true,
},
});
if (!productPrisma) {
throw new ResourceNotFoundError("Product", productId);
const environments: TEnvironment[] = [];
for (let environment of productPrisma.environments) {
let targetEnvironment: TEnvironment = ZEnvironment.parse(environment);
environments.push(targetEnvironment);
}
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
const environments: TEnvironment[] = [];
for (let environment of productPrisma.environments) {
let targetEnvironment: TEnvironment = ZEnvironment.parse(environment);
environments.push(targetEnvironment);
}
try {
return environments;
} catch (error) {
if (error instanceof z.ZodError) {
console.error(JSON.stringify(error.errors, null, 2));
try {
return environments;
} catch (error) {
if (error instanceof z.ZodError) {
console.error(JSON.stringify(error.errors, null, 2));
}
throw new ValidationError("Data validation of environments array failed");
}
throw new ValidationError("Data validation of environments array failed");
},
[`getEnvironments-${productId}`],
{
tags: [environmentCache.tag.byProductId(productId)],
}
},
[`getEnvironments-${productId}`],
{
tags: [environmentCache.tag.byProductId(productId)],
}
)();
)()
);
export const updateEnvironment = async (
environmentId: string,

View File

@@ -1,5 +1,6 @@
import "server-only";
import { Prisma } from "@prisma/client";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { DatabaseError } from "@formbricks/types/errors";
import { cache } from "../cache";
@@ -7,38 +8,42 @@ import { organizationCache } from "../organization/cache";
import { userCache } from "../user/cache";
// Function to check if there are any users in the database
export const getIsFreshInstance = (): Promise<boolean> =>
cache(
async () => {
try {
const userCount = await prisma.user.count();
if (userCount === 0) return true;
else return false;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
export const getIsFreshInstance = reactCache(
(): Promise<boolean> =>
cache(
async () => {
try {
const userCount = await prisma.user.count();
if (userCount === 0) return true;
else return false;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
throw error;
}
},
["getIsFreshInstance"],
{ tags: [userCache.tag.byCount()] }
)();
},
["getIsFreshInstance"],
{ tags: [userCache.tag.byCount()] }
)()
);
// Function to check if there are any organizations in the database
export const gethasNoOrganizations = (): Promise<boolean> =>
cache(
async () => {
try {
const organizationCount = await prisma.organization.count();
return organizationCount === 0;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
export const gethasNoOrganizations = reactCache(
(): Promise<boolean> =>
cache(
async () => {
try {
const organizationCount = await prisma.organization.count();
return organizationCount === 0;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
throw error;
}
},
["gethasNoOrganizations"],
{ tags: [organizationCache.tag.byCount()] }
)();
},
["gethasNoOrganizations"],
{ tags: [organizationCache.tag.byCount()] }
)()
);

View File

@@ -1,5 +1,6 @@
import "server-only";
import { Prisma } from "@prisma/client";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { ZOptionalNumber, ZString } from "@formbricks/types/common";
import { ZId } from "@formbricks/types/environment";
@@ -48,86 +49,89 @@ export const createOrUpdateIntegration = async (
}
};
export const getIntegrations = (environmentId: string, page?: number): Promise<TIntegration[]> =>
cache(
async () => {
validateInputs([environmentId, ZId], [page, ZOptionalNumber]);
export const getIntegrations = reactCache(
(environmentId: string, page?: number): Promise<TIntegration[]> =>
cache(
async () => {
validateInputs([environmentId, ZId], [page, ZOptionalNumber]);
try {
const integrations = await prisma.integration.findMany({
where: {
environmentId,
},
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
return integrations;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
},
[`getIntegrations-${environmentId}-${page}`],
{
tags: [integrationCache.tag.byEnvironmentId(environmentId)],
}
)();
export const getIntegration = (integrationId: string): Promise<TIntegration | null> =>
cache(
async () => {
try {
const integration = await prisma.integration.findUnique({
where: {
id: integrationId,
},
});
return integration;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
},
[`getIntegration-${integrationId}`],
{
tags: [integrationCache.tag.byId(integrationId)],
}
)();
export const getIntegrationByType = (
environmentId: string,
type: TIntegrationInput["type"]
): Promise<TIntegration | null> =>
cache(
async () => {
validateInputs([environmentId, ZId], [type, ZIntegrationType]);
try {
const integration = await prisma.integration.findUnique({
where: {
type_environmentId: {
try {
const integrations = await prisma.integration.findMany({
where: {
environmentId,
type,
},
},
});
return integration;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
return integrations;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
throw error;
},
[`getIntegrations-${environmentId}-${page}`],
{
tags: [integrationCache.tag.byEnvironmentId(environmentId)],
}
},
[`getIntegrationByType-${environmentId}-${type}`],
{
tags: [integrationCache.tag.byEnvironmentIdAndType(environmentId, type)],
}
)();
)()
);
export const getIntegration = reactCache(
(integrationId: string): Promise<TIntegration | null> =>
cache(
async () => {
try {
const integration = await prisma.integration.findUnique({
where: {
id: integrationId,
},
});
return integration;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
},
[`getIntegration-${integrationId}`],
{
tags: [integrationCache.tag.byId(integrationId)],
}
)()
);
export const getIntegrationByType = reactCache(
(environmentId: string, type: TIntegrationInput["type"]): Promise<TIntegration | null> =>
cache(
async () => {
validateInputs([environmentId, ZId], [type, ZIntegrationType]);
try {
const integration = await prisma.integration.findUnique({
where: {
type_environmentId: {
environmentId,
type,
},
},
});
return integration;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
},
[`getIntegrationByType-${environmentId}-${type}`],
{
tags: [integrationCache.tag.byEnvironmentIdAndType(environmentId, type)],
}
)()
);
export const deleteIntegration = async (integrationId: string): Promise<TIntegration> => {
validateInputs([integrationId, ZString]);

View File

@@ -1,6 +1,7 @@
import "server-only";
import { Prisma } from "@prisma/client";
import { getServerSession } from "next-auth";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { ZOptionalNumber, ZString } from "@formbricks/types/common";
import {
@@ -41,33 +42,35 @@ interface InviteWithCreator extends TInvite {
email: string;
};
}
export const getInvitesByOrganizationId = (organizationId: string, page?: number): Promise<TInvite[]> =>
cache(
async () => {
validateInputs([organizationId, ZString], [page, ZOptionalNumber]);
export const getInvitesByOrganizationId = reactCache(
(organizationId: string, page?: number): Promise<TInvite[]> =>
cache(
async () => {
validateInputs([organizationId, ZString], [page, ZOptionalNumber]);
try {
const invites = await prisma.invite.findMany({
where: { organizationId },
select: inviteSelect,
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
try {
const invites = await prisma.invite.findMany({
where: { organizationId },
select: inviteSelect,
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
return invites;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
return invites;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
throw error;
},
[`getInvitesByOrganizationId-${organizationId}-${page}`],
{
tags: [inviteCache.tag.byOrganizationId(organizationId)],
}
},
[`getInvitesByOrganizationId-${organizationId}-${page}`],
{
tags: [inviteCache.tag.byOrganizationId(organizationId)],
}
)();
)()
);
export const updateInvite = async (inviteId: string, data: TInviteUpdateInput): Promise<TInvite | null> => {
validateInputs([inviteId, ZString], [data, ZInviteUpdateInput]);
@@ -127,40 +130,42 @@ export const deleteInvite = async (inviteId: string): Promise<TInvite> => {
}
};
export const getInvite = (inviteId: string): Promise<InviteWithCreator | null> =>
cache(
async () => {
validateInputs([inviteId, ZString]);
export const getInvite = reactCache(
(inviteId: string): Promise<InviteWithCreator | null> =>
cache(
async () => {
validateInputs([inviteId, ZString]);
try {
const invite = await prisma.invite.findUnique({
where: {
id: inviteId,
},
include: {
creator: {
select: {
name: true,
email: true,
try {
const invite = await prisma.invite.findUnique({
where: {
id: inviteId,
},
include: {
creator: {
select: {
name: true,
email: true,
},
},
},
},
});
});
return invite;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
return invite;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
throw error;
},
[`getInvite-${inviteId}`],
{
tags: [inviteCache.tag.byId(inviteId)],
}
},
[`getInvite-${inviteId}`],
{
tags: [inviteCache.tag.byId(inviteId)],
}
)();
)()
);
export const resendInvite = async (inviteId: string): Promise<TInvite> => {
validateInputs([inviteId, ZString]);

View File

@@ -1,4 +1,5 @@
import { Prisma } from "@prisma/client";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { ZId } from "@formbricks/types/environment";
import { DatabaseError, ValidationError } from "@formbricks/types/errors";
@@ -58,7 +59,7 @@ export const createLanguage = async (
}
};
export const getSurveysUsingGivenLanguage = async (languageId: string): Promise<string[]> => {
export const getSurveysUsingGivenLanguage = reactCache(async (languageId: string): Promise<string[]> => {
try {
// Check if the language is used in any survey
const surveys = await prisma.surveyLanguage.findMany({
@@ -84,7 +85,7 @@ export const getSurveysUsingGivenLanguage = async (languageId: string): Promise<
}
throw error;
}
};
});
export const deleteLanguage = async (environmentId: string, languageId: string): Promise<TLanguage> => {
try {

View File

@@ -1,5 +1,6 @@
import "server-only";
import { Prisma } from "@prisma/client";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { ZOptionalNumber, ZString } from "@formbricks/types/common";
import { DatabaseError, ResourceNotFoundError, UnknownError } from "@formbricks/types/errors";
@@ -16,119 +17,122 @@ import { organizationCache } from "../organization/cache";
import { validateInputs } from "../utils/validate";
import { membershipCache } from "./cache";
export const getMembersByOrganizationId = async (organizationId: string, page?: number): Promise<TMember[]> =>
cache(
async () => {
validateInputs([organizationId, ZString], [page, ZOptionalNumber]);
export const getMembersByOrganizationId = reactCache(
(organizationId: string, page?: number): Promise<TMember[]> =>
cache(
async () => {
validateInputs([organizationId, ZString], [page, ZOptionalNumber]);
try {
const membersData = await prisma.membership.findMany({
where: { organizationId },
select: {
user: {
select: {
name: true,
email: true,
try {
const membersData = await prisma.membership.findMany({
where: { organizationId },
select: {
user: {
select: {
name: true,
email: true,
},
},
userId: true,
accepted: true,
role: true,
},
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
const members = membersData.map((member) => {
return {
name: member.user?.name || "",
email: member.user?.email || "",
userId: member.userId,
accepted: member.accepted,
role: member.role,
};
});
return members;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
console.error(error);
throw new DatabaseError(error.message);
}
throw new UnknownError("Error while fetching members");
}
},
[`getMembersByOrganizationId-${organizationId}-${page}`],
{
tags: [membershipCache.tag.byOrganizationId(organizationId)],
}
)()
);
export const getMembershipByUserIdOrganizationId = reactCache(
(userId: string, organizationId: string): Promise<TMembership | null> =>
cache(
async () => {
validateInputs([userId, ZString], [organizationId, ZString]);
try {
const membership = await prisma.membership.findUnique({
where: {
userId_organizationId: {
userId,
organizationId,
},
},
userId: true,
accepted: true,
role: true,
},
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
});
const members = membersData.map((member) => {
return {
name: member.user?.name || "",
email: member.user?.email || "",
userId: member.userId,
accepted: member.accepted,
role: member.role,
};
});
if (!membership) return null;
return members;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
console.error(error);
throw new DatabaseError(error.message);
return membership;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
console.error(error);
throw new DatabaseError(error.message);
}
throw new UnknownError("Error while fetching membership");
}
throw new UnknownError("Error while fetching members");
},
[`getMembershipByUserIdOrganizationId-${userId}-${organizationId}`],
{
tags: [membershipCache.tag.byUserId(userId), membershipCache.tag.byOrganizationId(organizationId)],
}
},
[`getMembersByOrganizationId-${organizationId}-${page}`],
{
tags: [membershipCache.tag.byOrganizationId(organizationId)],
}
)();
)()
);
export const getMembershipByUserIdOrganizationId = async (
userId: string,
organizationId: string
): Promise<TMembership | null> =>
cache(
async () => {
validateInputs([userId, ZString], [organizationId, ZString]);
export const getMembershipsByUserId = reactCache(
(userId: string, page?: number): Promise<TMembership[]> =>
cache(
async () => {
validateInputs([userId, ZString], [page, ZOptionalNumber]);
try {
const membership = await prisma.membership.findUnique({
where: {
userId_organizationId: {
try {
const memberships = await prisma.membership.findMany({
where: {
userId,
organizationId,
},
},
});
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
if (!membership) return null;
return memberships;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
return membership;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
console.error(error);
throw new DatabaseError(error.message);
throw error;
}
throw new UnknownError("Error while fetching membership");
},
[`getMembershipsByUserId-${userId}-${page}`],
{
tags: [membershipCache.tag.byUserId(userId)],
}
},
[`getMembershipByUserIdOrganizationId-${userId}-${organizationId}`],
{
tags: [membershipCache.tag.byUserId(userId), membershipCache.tag.byOrganizationId(organizationId)],
}
)();
export const getMembershipsByUserId = async (userId: string, page?: number): Promise<TMembership[]> =>
cache(
async () => {
validateInputs([userId, ZString], [page, ZOptionalNumber]);
try {
const memberships = await prisma.membership.findMany({
where: {
userId,
},
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
return memberships;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
},
[`getMembershipsByUserId-${userId}-${page}`],
{
tags: [membershipCache.tag.byUserId(userId)],
}
)();
)()
);
export const createMembership = async (
organizationId: string,

View File

@@ -1,5 +1,6 @@
import "server-only";
import { Prisma } from "@prisma/client";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { ZOptionalNumber, ZString } from "@formbricks/types/common";
import { ZId } from "@formbricks/types/environment";
@@ -32,105 +33,111 @@ export const getOrganizationsByUserIdCacheTag = (userId: string) => `users-${use
export const getOrganizationByEnvironmentIdCacheTag = (environmentId: string) =>
`environments-${environmentId}-organization`;
export const getOrganizationsByUserId = (userId: string, page?: number): Promise<TOrganization[]> =>
cache(
async () => {
validateInputs([userId, ZString], [page, ZOptionalNumber]);
export const getOrganizationsByUserId = reactCache(
(userId: string, page?: number): Promise<TOrganization[]> =>
cache(
async () => {
validateInputs([userId, ZString], [page, ZOptionalNumber]);
try {
const organizations = await prisma.organization.findMany({
where: {
memberships: {
some: {
userId,
try {
const organizations = await prisma.organization.findMany({
where: {
memberships: {
some: {
userId,
},
},
},
},
select,
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
if (!organizations) {
throw new ResourceNotFoundError("Organizations by UserId", userId);
}
return organizations;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
select,
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
if (!organizations) {
throw new ResourceNotFoundError("Organizations by UserId", userId);
}
return organizations;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
throw error;
}
},
[`getOrganizationsByUserId-${userId}-${page}`],
{
tags: [organizationCache.tag.byUserId(userId)],
}
},
[`getOrganizationsByUserId-${userId}-${page}`],
{
tags: [organizationCache.tag.byUserId(userId)],
}
)();
)()
);
export const getOrganizationByEnvironmentId = (environmentId: string): Promise<TOrganization | null> =>
cache(
async () => {
validateInputs([environmentId, ZId]);
export const getOrganizationByEnvironmentId = reactCache(
(environmentId: string): Promise<TOrganization | null> =>
cache(
async () => {
validateInputs([environmentId, ZId]);
try {
const organization = await prisma.organization.findFirst({
where: {
products: {
some: {
environments: {
some: {
id: environmentId,
try {
const organization = await prisma.organization.findFirst({
where: {
products: {
some: {
environments: {
some: {
id: environmentId,
},
},
},
},
},
},
select: { ...select, memberships: true }, // include memberships
});
select: { ...select, memberships: true }, // include memberships
});
return organization;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
console.error(error);
throw new DatabaseError(error.message);
return organization;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
console.error(error);
throw new DatabaseError(error.message);
}
throw error;
}
throw error;
},
[`getOrganizationByEnvironmentId-${environmentId}`],
{
tags: [organizationCache.tag.byEnvironmentId(environmentId)],
}
},
[`getOrganizationByEnvironmentId-${environmentId}`],
{
tags: [organizationCache.tag.byEnvironmentId(environmentId)],
}
)();
)()
);
export const getOrganization = (organizationId: string): Promise<TOrganization | null> =>
cache(
async () => {
validateInputs([organizationId, ZString]);
export const getOrganization = reactCache(
(organizationId: string): Promise<TOrganization | null> =>
cache(
async () => {
validateInputs([organizationId, ZString]);
try {
const organization = await prisma.organization.findUnique({
where: {
id: organizationId,
},
select,
});
return organization;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
try {
const organization = await prisma.organization.findUnique({
where: {
id: organizationId,
},
select,
});
return organization;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
throw error;
},
[`getOrganization-${organizationId}`],
{
tags: [organizationCache.tag.byId(organizationId)],
}
},
[`getOrganization-${organizationId}`],
{
tags: [organizationCache.tag.byId(organizationId)],
}
)();
)()
);
export const createOrganization = async (
organizationInput: TOrganizationCreateInput
@@ -271,79 +278,83 @@ export const deleteOrganization = async (organizationId: string): Promise<TOrgan
}
};
export const getMonthlyActiveOrganizationPeopleCount = (organizationId: string): Promise<number> =>
cache(
async () => {
validateInputs([organizationId, ZId]);
export const getMonthlyActiveOrganizationPeopleCount = reactCache(
(organizationId: string): Promise<number> =>
cache(
async () => {
validateInputs([organizationId, ZId]);
try {
// temporary solution until we have a better way to track active users
return 0;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
try {
// temporary solution until we have a better way to track active users
return 0;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
throw error;
},
[`getMonthlyActiveOrganizationPeopleCount-${organizationId}`],
{
revalidate: 60 * 60 * 2, // 2 hours
}
},
[`getMonthlyActiveOrganizationPeopleCount-${organizationId}`],
{
revalidate: 60 * 60 * 2, // 2 hours
}
)();
)()
);
export const getMonthlyOrganizationResponseCount = (organizationId: string): Promise<number> =>
cache(
async () => {
validateInputs([organizationId, ZId]);
export const getMonthlyOrganizationResponseCount = reactCache(
(organizationId: string): Promise<number> =>
cache(
async () => {
validateInputs([organizationId, ZId]);
try {
// Define the start of the month
// const now = new Date();
// const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
try {
// Define the start of the month
// const now = new Date();
// const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
const organization = await getOrganization(organizationId);
if (!organization) {
throw new ResourceNotFoundError("Organization", organizationId);
const organization = await getOrganization(organizationId);
if (!organization) {
throw new ResourceNotFoundError("Organization", organizationId);
}
if (!organization.billing.periodStart) {
throw new Error("Organization billing period start is not set");
}
// Get all environment IDs for the organization
const products = await getProducts(organizationId);
const environmentIds = products.flatMap((product) => product.environments.map((env) => env.id));
// Use Prisma's aggregate to count responses for all environments
const responseAggregations = await prisma.response.aggregate({
_count: {
id: true,
},
where: {
AND: [
{ survey: { environmentId: { in: environmentIds } } },
{ createdAt: { gte: organization.billing.periodStart } },
],
},
});
// The result is an aggregation of the total count
return responseAggregations._count.id;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
if (!organization.billing.periodStart) {
throw new Error("Organization billing period start is not set");
}
// Get all environment IDs for the organization
const products = await getProducts(organizationId);
const environmentIds = products.flatMap((product) => product.environments.map((env) => env.id));
// Use Prisma's aggregate to count responses for all environments
const responseAggregations = await prisma.response.aggregate({
_count: {
id: true,
},
where: {
AND: [
{ survey: { environmentId: { in: environmentIds } } },
{ createdAt: { gte: organization.billing.periodStart } },
],
},
});
// The result is an aggregation of the total count
return responseAggregations._count.id;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
},
[`getMonthlyOrganizationResponseCount-${organizationId}`],
{
revalidate: 60 * 60 * 2, // 2 hours
}
},
[`getMonthlyOrganizationResponseCount-${organizationId}`],
{
revalidate: 60 * 60 * 2, // 2 hours
}
)();
)()
);
export const subscribeOrganizationMembersToSurveyResponses = async (
surveyId: string,

View File

@@ -1,5 +1,6 @@
import "server-only";
import { Prisma } from "@prisma/client";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { ZOptionalNumber, ZString } from "@formbricks/types/common";
import { ZId } from "@formbricks/types/environment";
@@ -51,84 +52,90 @@ export const transformPrismaPerson = (person: TransformPersonInput): TPerson =>
} as TPerson;
};
export const getPerson = (personId: string): Promise<TPerson | null> =>
cache(
async () => {
validateInputs([personId, ZId]);
export const getPerson = reactCache(
(personId: string): Promise<TPerson | null> =>
cache(
async () => {
validateInputs([personId, ZId]);
try {
return await prisma.person.findUnique({
where: {
id: personId,
},
select: selectPerson,
});
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
try {
return await prisma.person.findUnique({
where: {
id: personId,
},
select: selectPerson,
});
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
throw error;
},
[`getPerson-${personId}`],
{
tags: [personCache.tag.byId(personId)],
}
},
[`getPerson-${personId}`],
{
tags: [personCache.tag.byId(personId)],
}
)();
)()
);
export const getPeople = (environmentId: string, page?: number): Promise<TPerson[]> =>
cache(
async () => {
validateInputs([environmentId, ZId], [page, ZOptionalNumber]);
export const getPeople = reactCache(
(environmentId: string, page?: number): Promise<TPerson[]> =>
cache(
async () => {
validateInputs([environmentId, ZId], [page, ZOptionalNumber]);
try {
return await prisma.person.findMany({
where: {
environmentId: environmentId,
},
select: selectPerson,
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
try {
return await prisma.person.findMany({
where: {
environmentId: environmentId,
},
select: selectPerson,
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
throw error;
},
[`getPeople-${environmentId}-${page}`],
{
tags: [personCache.tag.byEnvironmentId(environmentId)],
}
},
[`getPeople-${environmentId}-${page}`],
{
tags: [personCache.tag.byEnvironmentId(environmentId)],
}
)();
)()
);
export const getPeopleCount = (environmentId: string): Promise<number> =>
cache(
async () => {
validateInputs([environmentId, ZId]);
export const getPeopleCount = reactCache(
(environmentId: string): Promise<number> =>
cache(
async () => {
validateInputs([environmentId, ZId]);
try {
return await prisma.person.count({
where: {
environmentId: environmentId,
},
});
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
try {
return await prisma.person.count({
where: {
environmentId: environmentId,
},
});
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
throw error;
},
[`getPeopleCount-${environmentId}`],
{
tags: [personCache.tag.byEnvironmentId(environmentId)],
}
},
[`getPeopleCount-${environmentId}`],
{
tags: [personCache.tag.byEnvironmentId(environmentId)],
}
)();
)()
);
export const createPerson = async (environmentId: string, userId: string): Promise<TPerson> => {
validateInputs([environmentId, ZId]);
@@ -205,62 +212,66 @@ export const deletePerson = async (personId: string): Promise<TPerson | null> =>
}
};
export const getPersonByUserId = (environmentId: string, userId: string): Promise<TPerson | null> =>
cache(
async () => {
validateInputs([environmentId, ZId], [userId, ZString]);
export const getPersonByUserId = reactCache(
(environmentId: string, userId: string): Promise<TPerson | null> =>
cache(
async () => {
validateInputs([environmentId, ZId], [userId, ZString]);
// check if userId exists as a column
const personWithUserId = await prisma.person.findFirst({
where: {
environmentId,
userId,
},
select: selectPerson,
});
if (personWithUserId) {
return personWithUserId;
}
return null;
},
[`getPersonByUserId-${environmentId}-${userId}`],
{
tags: [personCache.tag.byEnvironmentIdAndUserId(environmentId, userId)],
}
)();
export const getIsPersonMonthlyActive = (personId: string): Promise<boolean> =>
cache(
async () => {
try {
const latestAction = await prisma.action.findFirst({
// check if userId exists as a column
const personWithUserId = await prisma.person.findFirst({
where: {
personId,
},
orderBy: {
createdAt: "desc",
},
select: {
createdAt: true,
environmentId,
userId,
},
select: selectPerson,
});
if (!latestAction || new Date(latestAction.createdAt).getMonth() !== new Date().getMonth()) {
return false;
}
return true;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
if (personWithUserId) {
return personWithUserId;
}
throw error;
return null;
},
[`getPersonByUserId-${environmentId}-${userId}`],
{
tags: [personCache.tag.byEnvironmentIdAndUserId(environmentId, userId)],
}
},
[`getIsPersonMonthlyActive-${personId}`],
{
tags: [activePersonCache.tag.byId(personId)],
revalidate: 60 * 60 * 24, // 24 hours
}
)();
)()
);
export const getIsPersonMonthlyActive = reactCache(
(personId: string): Promise<boolean> =>
cache(
async () => {
try {
const latestAction = await prisma.action.findFirst({
where: {
personId,
},
orderBy: {
createdAt: "desc",
},
select: {
createdAt: true,
},
});
if (!latestAction || new Date(latestAction.createdAt).getMonth() !== new Date().getMonth()) {
return false;
}
return true;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
},
[`getIsPersonMonthlyActive-${personId}`],
{
tags: [activePersonCache.tag.byId(personId)],
revalidate: 60 * 60 * 24, // 24 hours
}
)()
);

View File

@@ -1,5 +1,6 @@
import "server-only";
import { Prisma } from "@prisma/client";
import { cache as reactCache } from "react";
import { z } from "zod";
import { prisma } from "@formbricks/database";
import { ZOptionalNumber, ZString } from "@formbricks/types/common";
@@ -34,68 +35,72 @@ const selectProduct = {
logo: true,
};
export const getProducts = (organizationId: string, page?: number): Promise<TProduct[]> =>
cache(
async () => {
validateInputs([organizationId, ZId], [page, ZOptionalNumber]);
export const getProducts = reactCache(
(organizationId: string, page?: number): Promise<TProduct[]> =>
cache(
async () => {
validateInputs([organizationId, ZId], [page, ZOptionalNumber]);
try {
const products = await prisma.product.findMany({
where: {
organizationId,
},
select: selectProduct,
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
return products;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
try {
const products = await prisma.product.findMany({
where: {
organizationId,
},
select: selectProduct,
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
return products;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
throw error;
},
[`getProducts-${organizationId}-${page}`],
{
tags: [productCache.tag.byOrganizationId(organizationId)],
}
},
[`getProducts-${organizationId}-${page}`],
{
tags: [productCache.tag.byOrganizationId(organizationId)],
}
)();
)()
);
export const getProductByEnvironmentId = (environmentId: string): Promise<TProduct | null> =>
cache(
async () => {
validateInputs([environmentId, ZId]);
export const getProductByEnvironmentId = reactCache(
(environmentId: string): Promise<TProduct | null> =>
cache(
async () => {
validateInputs([environmentId, ZId]);
let productPrisma;
let productPrisma;
try {
productPrisma = await prisma.product.findFirst({
where: {
environments: {
some: {
id: environmentId,
try {
productPrisma = await prisma.product.findFirst({
where: {
environments: {
some: {
id: environmentId,
},
},
},
},
select: selectProduct,
});
select: selectProduct,
});
return productPrisma;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
console.error(error);
throw new DatabaseError(error.message);
return productPrisma;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
console.error(error);
throw new DatabaseError(error.message);
}
throw error;
}
throw error;
},
[`getProductByEnvironmentId-${environmentId}`],
{
tags: [productCache.tag.byEnvironmentId(environmentId)],
}
},
[`getProductByEnvironmentId-${environmentId}`],
{
tags: [productCache.tag.byEnvironmentId(environmentId)],
}
)();
)()
);
export const updateProduct = async (
productId: string,
@@ -148,31 +153,33 @@ export const updateProduct = async (
}
};
export const getProduct = async (productId: string): Promise<TProduct | null> =>
cache(
async () => {
let productPrisma;
try {
productPrisma = await prisma.product.findUnique({
where: {
id: productId,
},
select: selectProduct,
});
export const getProduct = reactCache(
(productId: string): Promise<TProduct | null> =>
cache(
async () => {
let productPrisma;
try {
productPrisma = await prisma.product.findUnique({
where: {
id: productId,
},
select: selectProduct,
});
return productPrisma;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
return productPrisma;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
throw error;
},
[`getProduct-${productId}`],
{
tags: [productCache.tag.byId(productId)],
}
},
[`getProduct-${productId}`],
{
tags: [productCache.tag.byId(productId)],
}
)();
)()
);
export const deleteProduct = async (productId: string): Promise<TProduct> => {
try {

View File

@@ -1,5 +1,6 @@
import "server-only";
import { Prisma } from "@prisma/client";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { TAttributes } from "@formbricks/types/attributes";
import { ZOptionalNumber, ZString } from "@formbricks/types/common";
@@ -100,92 +101,96 @@ export const responseSelection = {
},
};
export const getResponsesByPersonId = (personId: string, page?: number): Promise<TResponse[] | null> =>
cache(
async () => {
validateInputs([personId, ZId], [page, ZOptionalNumber]);
export const getResponsesByPersonId = reactCache(
(personId: string, page?: number): Promise<TResponse[] | null> =>
cache(
async () => {
validateInputs([personId, ZId], [page, ZOptionalNumber]);
try {
const responsePrisma = await prisma.response.findMany({
where: {
personId,
},
select: responseSelection,
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
orderBy: {
updatedAt: "asc",
},
});
try {
const responsePrisma = await prisma.response.findMany({
where: {
personId,
},
select: responseSelection,
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
orderBy: {
updatedAt: "asc",
},
});
if (!responsePrisma) {
throw new ResourceNotFoundError("Response from PersonId", personId);
if (!responsePrisma) {
throw new ResourceNotFoundError("Response from PersonId", personId);
}
let responses: TResponse[] = [];
await Promise.all(
responsePrisma.map(async (response) => {
const responseNotes = await getResponseNotes(response.id);
responses.push({
...response,
notes: responseNotes,
tags: response.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
});
})
);
return responses;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
let responses: TResponse[] = [];
await Promise.all(
responsePrisma.map(async (response) => {
const responseNotes = await getResponseNotes(response.id);
responses.push({
...response,
notes: responseNotes,
tags: response.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
});
})
);
return responses;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
},
[`getResponsesByPersonId-${personId}-${page}`],
{
tags: [responseCache.tag.byPersonId(personId)],
}
},
[`getResponsesByPersonId-${personId}-${page}`],
{
tags: [responseCache.tag.byPersonId(personId)],
}
)();
)()
);
export const getResponseBySingleUseId = (surveyId: string, singleUseId: string): Promise<TResponse | null> =>
cache(
async () => {
validateInputs([surveyId, ZId], [singleUseId, ZString]);
export const getResponseBySingleUseId = reactCache(
(surveyId: string, singleUseId: string): Promise<TResponse | null> =>
cache(
async () => {
validateInputs([surveyId, ZId], [singleUseId, ZString]);
try {
const responsePrisma = await prisma.response.findUnique({
where: {
surveyId_singleUseId: { surveyId, singleUseId },
},
select: responseSelection,
});
try {
const responsePrisma = await prisma.response.findUnique({
where: {
surveyId_singleUseId: { surveyId, singleUseId },
},
select: responseSelection,
});
if (!responsePrisma) {
return null;
if (!responsePrisma) {
return null;
}
const response: TResponse = {
...responsePrisma,
tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
};
return response;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
const response: TResponse = {
...responsePrisma,
tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
};
return response;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
},
[`getResponseBySingleUseId-${surveyId}-${singleUseId}`],
{
tags: [responseCache.tag.bySingleUseId(surveyId, singleUseId)],
}
},
[`getResponseBySingleUseId-${surveyId}-${singleUseId}`],
{
tags: [responseCache.tag.bySingleUseId(surveyId, singleUseId)],
}
)();
)()
);
export const createResponse = async (responseInput: TResponseInput): Promise<TResponse> => {
validateInputs([responseInput, ZResponseInput]);
@@ -378,44 +383,46 @@ export const createResponseLegacy = async (responseInput: TResponseLegacyInput):
}
};
export const getResponse = (responseId: string): Promise<TResponse | null> =>
cache(
async () => {
validateInputs([responseId, ZId]);
export const getResponse = reactCache(
(responseId: string): Promise<TResponse | null> =>
cache(
async () => {
validateInputs([responseId, ZId]);
try {
const responsePrisma = await prisma.response.findUnique({
where: {
id: responseId,
},
select: responseSelection,
});
try {
const responsePrisma = await prisma.response.findUnique({
where: {
id: responseId,
},
select: responseSelection,
});
if (!responsePrisma) {
return null;
if (!responsePrisma) {
return null;
}
const response: TResponse = {
...responsePrisma,
tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
};
return response;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
const response: TResponse = {
...responsePrisma,
tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
};
return response;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
},
[`getResponse-${responseId}`],
{
tags: [responseCache.tag.byId(responseId), responseNoteCache.tag.byResponseId(responseId)],
}
},
[`getResponse-${responseId}`],
{
tags: [responseCache.tag.byId(responseId), responseNoteCache.tag.byResponseId(responseId)],
}
)();
)()
);
export const getResponseFilteringValues = async (surveyId: string) =>
export const getResponseFilteringValues = reactCache((surveyId: string) =>
cache(
async () => {
validateInputs([surveyId, ZId]);
@@ -454,111 +461,113 @@ export const getResponseFilteringValues = async (surveyId: string) =>
{
tags: [responseCache.tag.bySurveyId(surveyId)],
}
)();
)()
);
export const getResponses = (
surveyId: string,
limit?: number,
offset?: number,
filterCriteria?: TResponseFilterCriteria
): Promise<TResponse[]> =>
cache(
async () => {
validateInputs(
[surveyId, ZId],
[limit, ZOptionalNumber],
[offset, ZOptionalNumber],
[filterCriteria, ZResponseFilterCriteria.optional()]
);
export const getResponses = reactCache(
(
surveyId: string,
limit?: number,
offset?: number,
filterCriteria?: TResponseFilterCriteria
): Promise<TResponse[]> =>
cache(
async () => {
validateInputs(
[surveyId, ZId],
[limit, ZOptionalNumber],
[offset, ZOptionalNumber],
[filterCriteria, ZResponseFilterCriteria.optional()]
);
limit = limit ?? RESPONSES_PER_PAGE;
try {
const responses = await prisma.response.findMany({
where: {
surveyId,
...buildWhereClause(filterCriteria),
},
select: responseSelection,
orderBy: [
{
createdAt: "desc",
limit = limit ?? RESPONSES_PER_PAGE;
try {
const responses = await prisma.response.findMany({
where: {
surveyId,
...buildWhereClause(filterCriteria),
},
],
take: limit ? limit : undefined,
skip: offset ? offset : undefined,
});
select: responseSelection,
orderBy: [
{
createdAt: "desc",
},
],
take: limit ? limit : undefined,
skip: offset ? offset : undefined,
});
const transformedResponses: TResponse[] = await Promise.all(
responses.map((responsePrisma) => {
return {
...responsePrisma,
tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
};
})
);
const transformedResponses: TResponse[] = await Promise.all(
responses.map((responsePrisma) => {
return {
...responsePrisma,
tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
};
})
);
return transformedResponses;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
return transformedResponses;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
throw error;
},
[`getResponses-${surveyId}-${limit}-${offset}-${JSON.stringify(filterCriteria)}`],
{
tags: [responseCache.tag.bySurveyId(surveyId)],
}
},
[`getResponses-${surveyId}-${limit}-${offset}-${JSON.stringify(filterCriteria)}`],
{
tags: [responseCache.tag.bySurveyId(surveyId)],
}
)();
)()
);
export const getSurveySummary = (
surveyId: string,
filterCriteria?: TResponseFilterCriteria
): Promise<TSurveySummary> =>
cache(
async () => {
validateInputs([surveyId, ZId], [filterCriteria, ZResponseFilterCriteria.optional()]);
export const getSurveySummary = reactCache(
(surveyId: string, filterCriteria?: TResponseFilterCriteria): Promise<TSurveySummary> =>
cache(
async () => {
validateInputs([surveyId, ZId], [filterCriteria, ZResponseFilterCriteria.optional()]);
try {
const survey = await getSurvey(surveyId);
if (!survey) {
throw new ResourceNotFoundError("Survey", surveyId);
try {
const survey = await getSurvey(surveyId);
if (!survey) {
throw new ResourceNotFoundError("Survey", surveyId);
}
const batchSize = 3000;
const responseCount = await getResponseCountBySurveyId(surveyId, filterCriteria);
const pages = Math.ceil(responseCount / batchSize);
const responsesArray = await Promise.all(
Array.from({ length: pages }, (_, i) => {
return getResponses(surveyId, batchSize, i * batchSize, filterCriteria);
})
);
const responses = responsesArray.flat();
const displayCount = await getDisplayCountBySurveyId(surveyId, {
createdAt: filterCriteria?.createdAt,
});
const dropOff = getSurveySummaryDropOff(survey, responses, displayCount);
const meta = getSurveySummaryMeta(responses, displayCount);
const questionWiseSummary = getQuestionWiseSummary(survey, responses, dropOff);
return { meta, dropOff, summary: questionWiseSummary };
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
const batchSize = 3000;
const responseCount = await getResponseCountBySurveyId(surveyId, filterCriteria);
const pages = Math.ceil(responseCount / batchSize);
const responsesArray = await Promise.all(
Array.from({ length: pages }, (_, i) => {
return getResponses(surveyId, batchSize, i * batchSize, filterCriteria);
})
);
const responses = responsesArray.flat();
const displayCount = await getDisplayCountBySurveyId(surveyId, {
createdAt: filterCriteria?.createdAt,
});
const dropOff = getSurveySummaryDropOff(survey, responses, displayCount);
const meta = getSurveySummaryMeta(responses, displayCount);
const questionWiseSummary = getQuestionWiseSummary(survey, responses, dropOff);
return { meta, dropOff, summary: questionWiseSummary };
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
},
[`getSurveySummary-${surveyId}-${JSON.stringify(filterCriteria)}`],
{
tags: [responseCache.tag.bySurveyId(surveyId), displayCache.tag.bySurveyId(surveyId)],
}
},
[`getSurveySummary-${surveyId}-${JSON.stringify(filterCriteria)}`],
{
tags: [responseCache.tag.bySurveyId(surveyId), displayCache.tag.bySurveyId(surveyId)],
}
)();
)()
);
export const getResponseDownloadUrl = async (
surveyId: string,
@@ -632,55 +641,53 @@ export const getResponseDownloadUrl = async (
}
};
export const getResponsesByEnvironmentId = (
environmentId: string,
limit?: number,
offset?: number
): Promise<TResponse[]> =>
cache(
async () => {
validateInputs([environmentId, ZId], [limit, ZOptionalNumber], [offset, ZOptionalNumber]);
export const getResponsesByEnvironmentId = reactCache(
(environmentId: string, limit?: number, offset?: number): Promise<TResponse[]> =>
cache(
async () => {
validateInputs([environmentId, ZId], [limit, ZOptionalNumber], [offset, ZOptionalNumber]);
try {
const responses = await prisma.response.findMany({
where: {
survey: {
environmentId,
try {
const responses = await prisma.response.findMany({
where: {
survey: {
environmentId,
},
},
},
select: responseSelection,
orderBy: [
{
createdAt: "desc",
},
],
take: limit ? limit : undefined,
skip: offset ? offset : undefined,
});
select: responseSelection,
orderBy: [
{
createdAt: "desc",
},
],
take: limit ? limit : undefined,
skip: offset ? offset : undefined,
});
const transformedResponses: TResponse[] = await Promise.all(
responses.map(async (responsePrisma) => {
return {
...responsePrisma,
tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
};
})
);
const transformedResponses: TResponse[] = await Promise.all(
responses.map(async (responsePrisma) => {
return {
...responsePrisma,
tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
};
})
);
return transformedResponses;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
return transformedResponses;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
throw error;
},
[`getResponsesByEnvironmentId-${environmentId}-${limit}-${offset}`],
{
tags: [responseCache.tag.byEnvironmentId(environmentId)],
}
},
[`getResponsesByEnvironmentId-${environmentId}-${limit}-${offset}`],
{
tags: [responseCache.tag.byEnvironmentId(environmentId)],
}
)();
)()
);
export const updateResponse = async (
responseId: string,
@@ -794,32 +801,31 @@ export const deleteResponse = async (responseId: string): Promise<TResponse> =>
}
};
export const getResponseCountBySurveyId = (
surveyId: string,
filterCriteria?: TResponseFilterCriteria
): Promise<number> =>
cache(
async () => {
validateInputs([surveyId, ZId], [filterCriteria, ZResponseFilterCriteria.optional()]);
export const getResponseCountBySurveyId = reactCache(
(surveyId: string, filterCriteria?: TResponseFilterCriteria): Promise<number> =>
cache(
async () => {
validateInputs([surveyId, ZId], [filterCriteria, ZResponseFilterCriteria.optional()]);
try {
const responseCount = await prisma.response.count({
where: {
surveyId: surveyId,
...buildWhereClause(filterCriteria),
},
});
return responseCount;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
try {
const responseCount = await prisma.response.count({
where: {
surveyId: surveyId,
...buildWhereClause(filterCriteria),
},
});
return responseCount;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
throw error;
},
[`getResponseCountBySurveyId-${surveyId}-${JSON.stringify(filterCriteria)}`],
{
tags: [responseCache.tag.bySurveyId(surveyId)],
}
},
[`getResponseCountBySurveyId-${surveyId}-${JSON.stringify(filterCriteria)}`],
{
tags: [responseCache.tag.bySurveyId(surveyId)],
}
)();
)()
);

View File

@@ -1,5 +1,6 @@
import "server-only";
import { Prisma } from "@prisma/client";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { ZString } from "@formbricks/types/common";
import { ZId } from "@formbricks/types/environment";
@@ -68,62 +69,66 @@ export const createResponseNote = async (
}
};
export const getResponseNote = (responseNoteId: string): Promise<TResponseNote | null> =>
cache(
async () => {
try {
const responseNote = await prisma.responseNote.findUnique({
where: {
id: responseNoteId,
},
select: responseNoteSelect,
});
return responseNote;
} catch (error) {
console.error(error);
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
export const getResponseNote = reactCache(
(responseNoteId: string): Promise<TResponseNote | null> =>
cache(
async () => {
try {
const responseNote = await prisma.responseNote.findUnique({
where: {
id: responseNoteId,
},
select: responseNoteSelect,
});
return responseNote;
} catch (error) {
console.error(error);
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
throw error;
}
},
[`getResponseNote-${responseNoteId}`],
{
tags: [responseNoteCache.tag.byId(responseNoteId)],
}
},
[`getResponseNote-${responseNoteId}`],
{
tags: [responseNoteCache.tag.byId(responseNoteId)],
}
)();
)()
);
export const getResponseNotes = (responseId: string): Promise<TResponseNote[]> =>
cache(
async () => {
try {
validateInputs([responseId, ZId]);
export const getResponseNotes = reactCache(
(responseId: string): Promise<TResponseNote[]> =>
cache(
async () => {
try {
validateInputs([responseId, ZId]);
const responseNotes = await prisma.responseNote.findMany({
where: {
responseId,
},
select: responseNoteSelect,
});
if (!responseNotes) {
throw new ResourceNotFoundError("Response Notes by ResponseId", responseId);
const responseNotes = await prisma.responseNote.findMany({
where: {
responseId,
},
select: responseNoteSelect,
});
if (!responseNotes) {
throw new ResourceNotFoundError("Response Notes by ResponseId", responseId);
}
return responseNotes;
} catch (error) {
console.error(error);
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
return responseNotes;
} catch (error) {
console.error(error);
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
},
[`getResponseNotes-${responseId}`],
{
tags: [responseNoteCache.tag.byResponseId(responseId)],
}
},
[`getResponseNotes-${responseId}`],
{
tags: [responseNoteCache.tag.byResponseId(responseId)],
}
)();
)()
);
export const updateResponseNote = async (responseNoteId: string, text: string): Promise<TResponseNote> => {
validateInputs([responseNoteId, ZString], [text, ZString]);

View File

@@ -1,4 +1,5 @@
import { Prisma } from "@prisma/client";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { ZString } from "@formbricks/types/common";
import { ZId } from "@formbricks/types/environment";
@@ -116,67 +117,71 @@ export const createSegment = async (segmentCreateInput: TSegmentCreateInput): Pr
}
};
export const getSegments = (environmentId: string): Promise<TSegment[]> =>
cache(
async () => {
validateInputs([environmentId, ZId]);
try {
const segments = await prisma.segment.findMany({
where: {
environmentId,
},
select: selectSegment,
});
export const getSegments = reactCache(
(environmentId: string): Promise<TSegment[]> =>
cache(
async () => {
validateInputs([environmentId, ZId]);
try {
const segments = await prisma.segment.findMany({
where: {
environmentId,
},
select: selectSegment,
});
if (!segments) {
return [];
if (!segments) {
return [];
}
return segments.map((segment) => transformPrismaSegment(segment));
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
return segments.map((segment) => transformPrismaSegment(segment));
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
},
[`getSegments-${environmentId}`],
{
tags: [segmentCache.tag.byEnvironmentId(environmentId)],
}
},
[`getSegments-${environmentId}`],
{
tags: [segmentCache.tag.byEnvironmentId(environmentId)],
}
)();
)()
);
export const getSegment = (segmentId: string): Promise<TSegment> =>
cache(
async () => {
validateInputs([segmentId, ZId]);
try {
const segment = await prisma.segment.findUnique({
where: {
id: segmentId,
},
select: selectSegment,
});
export const getSegment = reactCache(
(segmentId: string): Promise<TSegment> =>
cache(
async () => {
validateInputs([segmentId, ZId]);
try {
const segment = await prisma.segment.findUnique({
where: {
id: segmentId,
},
select: selectSegment,
});
if (!segment) {
throw new ResourceNotFoundError("segment", segmentId);
if (!segment) {
throw new ResourceNotFoundError("segment", segmentId);
}
return transformPrismaSegment(segment);
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
return transformPrismaSegment(segment);
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
},
[`getSegment-${segmentId}`],
{
tags: [segmentCache.tag.byId(segmentId)],
}
},
[`getSegment-${segmentId}`],
{
tags: [segmentCache.tag.byId(segmentId)],
}
)();
)()
);
export const updateSegment = async (segmentId: string, data: TSegmentUpdateInput): Promise<TSegment> => {
validateInputs([segmentId, ZId], [data, ZSegmentUpdateInput]);
@@ -330,43 +335,45 @@ export const cloneSegment = async (segmentId: string, surveyId: string): Promise
}
};
export const getSegmentsByAttributeClassName = (environmentId: string, attributeClassName: string) =>
cache(
async () => {
validateInputs([environmentId, ZId], [attributeClassName, ZString]);
export const getSegmentsByAttributeClassName = reactCache(
(environmentId: string, attributeClassName: string) =>
cache(
async () => {
validateInputs([environmentId, ZId], [attributeClassName, ZString]);
try {
const segments = await prisma.segment.findMany({
where: {
environmentId,
},
select: selectSegment,
});
try {
const segments = await prisma.segment.findMany({
where: {
environmentId,
},
select: selectSegment,
});
// search for attributeClassName in the filters
const clonedSegments = structuredClone(segments);
// search for attributeClassName in the filters
const clonedSegments = structuredClone(segments);
const filteredSegments = clonedSegments.filter((segment) => {
return searchForAttributeClassNameInSegment(segment.filters, attributeClassName);
});
const filteredSegments = clonedSegments.filter((segment) => {
return searchForAttributeClassNameInSegment(segment.filters, attributeClassName);
});
return filteredSegments;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
return filteredSegments;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
throw error;
},
[`getSegmentsByAttributeClassName-${environmentId}-${attributeClassName}`],
{
tags: [
segmentCache.tag.byEnvironmentId(environmentId),
segmentCache.tag.byAttributeClassName(attributeClassName),
],
}
},
[`getSegmentsByAttributeClassName-${environmentId}-${attributeClassName}`],
{
tags: [
segmentCache.tag.byEnvironmentId(environmentId),
segmentCache.tag.byAttributeClassName(attributeClassName),
],
}
)();
)()
);
export const resetSegmentInSurvey = async (surveyId: string): Promise<TSegment> => {
validateInputs([surveyId, ZId]);

View File

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

View File

@@ -1,10 +1,13 @@
import { Prisma } from "@prisma/client";
import { customAlphabet } from "nanoid";
import z from "zod";
import { cache as reactCache } from "react";
import { z } from "zod";
import { prisma } from "@formbricks/database";
import { DatabaseError } from "@formbricks/types/errors";
import { TShortUrl, ZShortUrlId } from "@formbricks/types/shortUrl";
import { cache } from "../cache";
import { validateInputs } from "../utils/validate";
import { shortUrlCache } from "./cache";
// Create the short url and return it
export const createShortUrl = async (url: string): Promise<TShortUrl> => {
@@ -21,12 +24,19 @@ export const createShortUrl = async (url: string): Promise<TShortUrl> => {
// If an entry with the provided fullUrl does not exist, create a new one.
const id = customAlphabet("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", 10)();
return await prisma.shortUrl.create({
const shortUrl = await prisma.shortUrl.create({
data: {
id,
url,
},
});
shortUrlCache.revalidate({
id: shortUrl.id,
url: shortUrl.url,
});
return shortUrl;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
@@ -37,36 +47,54 @@ export const createShortUrl = async (url: string): Promise<TShortUrl> => {
};
// Get the full url from short url and return it
export const getShortUrl = async (id: string): Promise<TShortUrl | null> => {
validateInputs([id, ZShortUrlId]);
try {
return await prisma.shortUrl.findUnique({
where: {
id,
export const getShortUrl = reactCache(
(id: string): Promise<TShortUrl | null> =>
cache(
async () => {
validateInputs([id, ZShortUrlId]);
try {
return await prisma.shortUrl.findUnique({
where: {
id,
},
});
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
},
});
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
[`getShortUrl-${id}`],
{
tags: [shortUrlCache.tag.byId(id)],
}
)()
);
throw error;
}
};
export const getShortUrlByUrl = reactCache(
(url: string): Promise<TShortUrl | null> =>
cache(
async () => {
validateInputs([url, z.string().url()]);
try {
return await prisma.shortUrl.findUnique({
where: {
url,
},
});
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
export const getShortUrlByUrl = async (url: string): Promise<TShortUrl | null> => {
validateInputs([url, z.string().url()]);
try {
return await prisma.shortUrl.findUnique({
where: {
url,
throw error;
}
},
});
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
};
[`getShortUrlByUrl-${url}`],
{
tags: [shortUrlCache.tag.byUrl(url)],
}
)()
);

View File

@@ -6,6 +6,7 @@ interface RevalidateProps {
actionClassId?: string;
environmentId?: string;
segmentId?: string;
resultShareKey?: string;
}
export const surveyCache = {
@@ -25,8 +26,18 @@ export const surveyCache = {
bySegmentId(segmentId: string) {
return `segments-${segmentId}-surveys`;
},
byResultShareKey(resultShareKey: string) {
return `surveys-resultShare-${resultShareKey}`;
},
},
revalidate({ id, attributeClassId, actionClassId, environmentId, segmentId }: RevalidateProps): void {
revalidate({
id,
attributeClassId,
actionClassId,
environmentId,
segmentId,
resultShareKey,
}: RevalidateProps): void {
if (id) {
revalidateTag(this.tag.byId(id));
}
@@ -44,6 +55,10 @@ export const surveyCache = {
}
if (segmentId) {
revalidateTag(this.tag.byResultShareKey(resultShareKey));
}
if (resultShareKey) {
revalidateTag(this.tag.bySegmentId(segmentId));
}
},

View File

@@ -1,5 +1,6 @@
import "server-only";
import { Prisma } from "@prisma/client";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { TActionClass } from "@formbricks/types/actionClasses";
import { ZOptionalNumber } from "@formbricks/types/common";
@@ -182,128 +183,134 @@ const handleTriggerUpdates = (
return triggersUpdate;
};
export const getSurvey = (surveyId: string): Promise<TSurvey | null> =>
cache(
async () => {
validateInputs([surveyId, ZId]);
export const getSurvey = reactCache(
(surveyId: string): Promise<TSurvey | null> =>
cache(
async () => {
validateInputs([surveyId, ZId]);
let surveyPrisma;
try {
surveyPrisma = await prisma.survey.findUnique({
where: {
id: surveyId,
},
select: selectSurvey,
});
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
console.error(error);
throw new DatabaseError(error.message);
let surveyPrisma;
try {
surveyPrisma = await prisma.survey.findUnique({
where: {
id: surveyId,
},
select: selectSurvey,
});
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
console.error(error);
throw new DatabaseError(error.message);
}
throw error;
}
throw error;
if (!surveyPrisma) {
return null;
}
return transformPrismaSurvey(surveyPrisma);
},
[`getSurvey-${surveyId}`],
{
tags: [surveyCache.tag.byId(surveyId)],
}
)()
);
if (!surveyPrisma) {
return null;
}
export const getSurveysByActionClassId = reactCache(
(actionClassId: string, page?: number): Promise<TSurvey[]> =>
cache(
async () => {
validateInputs([actionClassId, ZId], [page, ZOptionalNumber]);
return transformPrismaSurvey(surveyPrisma);
},
[`getSurvey-${surveyId}`],
{
tags: [surveyCache.tag.byId(surveyId)],
}
)();
export const getSurveysByActionClassId = (actionClassId: string, page?: number): Promise<TSurvey[]> =>
cache(
async () => {
validateInputs([actionClassId, ZId], [page, ZOptionalNumber]);
let surveysPrisma;
try {
surveysPrisma = await prisma.survey.findMany({
where: {
triggers: {
some: {
actionClass: {
id: actionClassId,
let surveysPrisma;
try {
surveysPrisma = await prisma.survey.findMany({
where: {
triggers: {
some: {
actionClass: {
id: actionClassId,
},
},
},
},
},
select: selectSurvey,
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
console.error(error);
throw new DatabaseError(error.message);
select: selectSurvey,
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
console.error(error);
throw new DatabaseError(error.message);
}
throw error;
}
throw error;
}
const surveys: TSurvey[] = [];
const surveys: TSurvey[] = [];
for (const surveyPrisma of surveysPrisma) {
const transformedSurvey = transformPrismaSurvey(surveyPrisma);
surveys.push(transformedSurvey);
}
return surveys;
},
[`getSurveysByActionClassId-${actionClassId}-${page}`],
{
tags: [surveyCache.tag.byActionClassId(actionClassId)],
}
)();
export const getSurveys = (
environmentId: string,
limit?: number,
offset?: number,
filterCriteria?: TSurveyFilterCriteria
): Promise<TSurvey[]> =>
cache(
async () => {
validateInputs([environmentId, ZId], [limit, ZOptionalNumber], [offset, ZOptionalNumber]);
let surveysPrisma;
try {
surveysPrisma = await prisma.survey.findMany({
where: {
environmentId,
...buildWhereClause(filterCriteria),
},
select: selectSurvey,
orderBy: buildOrderByClause(filterCriteria?.sortBy),
take: limit ? limit : undefined,
skip: offset ? offset : undefined,
});
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
console.error(error);
throw new DatabaseError(error.message);
for (const surveyPrisma of surveysPrisma) {
const transformedSurvey = transformPrismaSurvey(surveyPrisma);
surveys.push(transformedSurvey);
}
throw error;
return surveys;
},
[`getSurveysByActionClassId-${actionClassId}-${page}`],
{
tags: [surveyCache.tag.byActionClassId(actionClassId)],
}
)()
);
const surveys: TSurvey[] = [];
export const getSurveys = reactCache(
(
environmentId: string,
limit?: number,
offset?: number,
filterCriteria?: TSurveyFilterCriteria
): Promise<TSurvey[]> =>
cache(
async () => {
validateInputs([environmentId, ZId], [limit, ZOptionalNumber], [offset, ZOptionalNumber]);
let surveysPrisma;
for (const surveyPrisma of surveysPrisma) {
const transformedSurvey = transformPrismaSurvey(surveyPrisma);
surveys.push(transformedSurvey);
try {
surveysPrisma = await prisma.survey.findMany({
where: {
environmentId,
...buildWhereClause(filterCriteria),
},
select: selectSurvey,
orderBy: buildOrderByClause(filterCriteria?.sortBy),
take: limit ? limit : undefined,
skip: offset ? offset : undefined,
});
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
console.error(error);
throw new DatabaseError(error.message);
}
throw error;
}
const surveys: TSurvey[] = [];
for (const surveyPrisma of surveysPrisma) {
const transformedSurvey = transformPrismaSurvey(surveyPrisma);
surveys.push(transformedSurvey);
}
return surveys;
},
[`getSurveys-${environmentId}-${limit}-${offset}-${JSON.stringify(filterCriteria)}`],
{
tags: [surveyCache.tag.byEnvironmentId(environmentId)],
}
return surveys;
},
[`getSurveys-${environmentId}-${limit}-${offset}-${JSON.stringify(filterCriteria)}`],
{
tags: [surveyCache.tag.byEnvironmentId(environmentId)],
}
)();
)()
);
export const transformToLegacySurvey = async (
survey: TSurvey,
@@ -322,32 +329,34 @@ export const transformToLegacySurvey = async (
return transformedSurvey;
};
export const getSurveyCount = async (environmentId: string): Promise<number> =>
cache(
async () => {
validateInputs([environmentId, ZId]);
try {
const surveyCount = await prisma.survey.count({
where: {
environmentId: environmentId,
},
});
export const getSurveyCount = reactCache(
(environmentId: string): Promise<number> =>
cache(
async () => {
validateInputs([environmentId, ZId]);
try {
const surveyCount = await prisma.survey.count({
where: {
environmentId: environmentId,
},
});
return surveyCount;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
console.error(error);
throw new DatabaseError(error.message);
return surveyCount;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
console.error(error);
throw new DatabaseError(error.message);
}
throw error;
}
throw error;
},
[`getSurveyCount-${environmentId}`],
{
tags: [surveyCache.tag.byEnvironmentId(environmentId)],
}
},
[`getSurveyCount-${environmentId}`],
{
tags: [surveyCache.tag.byEnvironmentId(environmentId)],
}
)();
)()
);
export const updateSurvey = async (updatedSurvey: TSurvey): Promise<TSurvey> => {
validateInputs([updatedSurvey, ZSurvey]);
@@ -506,6 +515,7 @@ export const updateSurvey = async (updatedSurvey: TSurvey): Promise<TSurvey> =>
id: modifiedSurvey.id,
environmentId: modifiedSurvey.environmentId,
segmentId: modifiedSurvey.segment?.id,
resultShareKey: modifiedSurvey.resultShareKey ?? undefined,
});
return modifiedSurvey;
@@ -552,6 +562,7 @@ export const deleteSurvey = async (surveyId: string) => {
surveyCache.revalidate({
id: deletedSurvey.id,
environmentId: deletedSurvey.environmentId,
resultShareKey: deletedSurvey.resultShareKey ?? undefined,
});
if (deletedSurvey.segment?.id) {
@@ -665,6 +676,7 @@ export const createSurvey = async (environmentId: string, surveyBody: TSurveyInp
surveyCache.revalidate({
id: survey.id,
environmentId: survey.environmentId,
resultShareKey: survey.resultShareKey ?? undefined,
});
if (createdBy) {
@@ -796,6 +808,7 @@ export const duplicateSurvey = async (environmentId: string, surveyId: string, u
surveyCache.revalidate({
id: newSurvey.id,
environmentId: newSurvey.environmentId,
resultShareKey: newSurvey.resultShareKey ?? undefined,
});
existingSurvey.triggers.forEach((trigger) => {
@@ -815,226 +828,237 @@ export const duplicateSurvey = async (environmentId: string, surveyId: string, u
}
};
export const getSyncSurveys = (
environmentId: string,
personId: string,
deviceType: "phone" | "desktop" = "desktop",
options?: {
version?: string;
}
): Promise<TSurvey[] | TLegacySurvey[]> =>
cache(
async () => {
validateInputs([environmentId, ZId]);
try {
const product = await getProductByEnvironmentId(environmentId);
export const getSyncSurveys = reactCache(
(
environmentId: string,
personId: string,
deviceType: "phone" | "desktop" = "desktop",
options?: {
version?: string;
}
): Promise<TSurvey[] | TLegacySurvey[]> =>
cache(
async () => {
validateInputs([environmentId, ZId]);
try {
const product = await getProductByEnvironmentId(environmentId);
if (!product) {
throw new Error("Product not found");
}
if (!product) {
throw new Error("Product not found");
}
const person = personId === "legacy" ? ({ id: "legacy" } as TPerson) : await getPerson(personId);
const person = personId === "legacy" ? ({ id: "legacy" } as TPerson) : await getPerson(personId);
if (!person) {
throw new Error("Person not found");
}
if (!person) {
throw new Error("Person not found");
}
let surveys: TSurvey[] | TLegacySurvey[] = await getSurveys(environmentId);
let surveys: TSurvey[] | TLegacySurvey[] = await getSurveys(environmentId);
// filtered surveys for running and web
surveys = surveys.filter((survey) => survey.status === "inProgress" && survey.type === "app");
// filtered surveys for running and web
surveys = surveys.filter((survey) => survey.status === "inProgress" && survey.type === "app");
// if no surveys are left, return an empty array
if (surveys.length === 0) {
return [];
}
// if no surveys are left, return an empty array
if (surveys.length === 0) {
return [];
}
const displays = await getDisplaysByPersonId(person.id);
const displays = await getDisplaysByPersonId(person.id);
// filter surveys that meet the displayOption criteria
surveys = surveys.filter((survey) => {
switch (survey.displayOption) {
case "respondMultiple":
// filter surveys that meet the displayOption criteria
surveys = surveys.filter((survey) => {
switch (survey.displayOption) {
case "respondMultiple":
return true;
case "displayOnce":
return displays.filter((display) => display.surveyId === survey.id).length === 0;
case "displayMultiple":
return (
displays
.filter((display) => display.surveyId === survey.id)
.filter((display) => display.responseId).length === 0
);
case "displaySome":
if (survey.displayLimit === null) {
return true;
}
if (
displays
.filter((display) => display.surveyId === survey.id)
.some((display) => display.responseId)
) {
return false;
}
return (
displays.filter((display) => display.surveyId === survey.id).length < survey.displayLimit
);
default:
throw Error("Invalid displayOption");
}
});
const latestDisplay = displays[0];
// filter surveys that meet the recontactDays criteria
surveys = surveys.filter((survey) => {
if (!latestDisplay) {
return true;
case "displayOnce":
return displays.filter((display) => display.surveyId === survey.id).length === 0;
case "displayMultiple":
return (
displays
.filter((display) => display.surveyId === survey.id)
.filter((display) => display.responseId).length === 0
);
case "displaySome":
if (survey.displayLimit === null) {
} else if (survey.recontactDays !== null) {
const lastDisplaySurvey = displays.filter((display) => display.surveyId === survey.id)[0];
if (!lastDisplaySurvey) {
return true;
}
if (
displays
.filter((display) => display.surveyId === survey.id)
.some((display) => display.responseId)
) {
return false;
}
return (
displays.filter((display) => display.surveyId === survey.id).length < survey.displayLimit
);
default:
throw Error("Invalid displayOption");
}
});
const latestDisplay = displays[0];
// filter surveys that meet the recontactDays criteria
surveys = surveys.filter((survey) => {
if (!latestDisplay) {
return true;
} else if (survey.recontactDays !== null) {
const lastDisplaySurvey = displays.filter((display) => display.surveyId === survey.id)[0];
if (!lastDisplaySurvey) {
return diffInDays(new Date(), new Date(lastDisplaySurvey.createdAt)) >= survey.recontactDays;
} else if (product.recontactDays !== null) {
return diffInDays(new Date(), new Date(latestDisplay.createdAt)) >= product.recontactDays;
} else {
return true;
}
return diffInDays(new Date(), new Date(lastDisplaySurvey.createdAt)) >= survey.recontactDays;
} else if (product.recontactDays !== null) {
return diffInDays(new Date(), new Date(latestDisplay.createdAt)) >= product.recontactDays;
} else {
return true;
}
});
});
// if no surveys are left, return an empty array
if (surveys.length === 0) {
return [];
}
// if no surveys have segment filters, return the surveys
if (!anySurveyHasFilters(surveys)) {
return surveys;
}
const personActions = await getActionsByPersonId(person.id);
const personActionClassIds = Array.from(
new Set(personActions?.map((action) => action.actionClass?.id ?? ""))
);
const attributes = await getAttributes(person.id);
const personUserId = person.userId;
// the surveys now have segment filters, so we need to evaluate them
const surveyPromises = surveys.map(async (survey) => {
const { segment } = survey;
// if the survey has no segment, or the segment has no filters, we return the survey
if (!segment || !segment.filters?.length) {
return survey;
// if no surveys are left, return an empty array
if (surveys.length === 0) {
return [];
}
// backwards compatibility for older versions of the js package
// if the version is not provided, we will use the old method of evaluating the segment, which is attribute filters
// transform the segment filters to attribute filters and evaluate them
if (!options?.version) {
const attributeFilters = transformSegmentFiltersToAttributeFilters(segment.filters);
// if no surveys have segment filters, return the surveys
if (!anySurveyHasFilters(surveys)) {
return surveys;
}
// if the attribute filters are null, it means the segment filters don't match the expected format for attribute filters, so we skip this survey
if (attributeFilters === null) {
return null;
}
const personActions = await getActionsByPersonId(person.id);
const personActionClassIds = Array.from(
new Set(personActions?.map((action) => action.actionClass?.id ?? ""))
);
// if there are no attribute filters, we return the survey
if (!attributeFilters.length) {
const attributes = await getAttributes(person.id);
const personUserId = person.userId;
// the surveys now have segment filters, so we need to evaluate them
const surveyPromises = surveys.map(async (survey) => {
const { segment } = survey;
// if the survey has no segment, or the segment has no filters, we return the survey
if (!segment || !segment.filters?.length) {
return survey;
}
// we check if the person meets the attribute filters for all the attribute filters
const isEligible = attributeFilters.every((attributeFilter) => {
const personAttributeValue = attributes[attributeFilter.attributeClassName];
if (!personAttributeValue) {
return false;
// backwards compatibility for older versions of the js package
// if the version is not provided, we will use the old method of evaluating the segment, which is attribute filters
// transform the segment filters to attribute filters and evaluate them
if (!options?.version) {
const attributeFilters = transformSegmentFiltersToAttributeFilters(segment.filters);
// if the attribute filters are null, it means the segment filters don't match the expected format for attribute filters, so we skip this survey
if (attributeFilters === null) {
return null;
}
if (attributeFilter.operator === "equals") {
return personAttributeValue === attributeFilter.value;
} else if (attributeFilter.operator === "notEquals") {
return personAttributeValue !== attributeFilter.value;
} else {
// if the operator is not equals or not equals, we skip the survey, this means that new segment filter options are being used
return false;
// if there are no attribute filters, we return the survey
if (!attributeFilters.length) {
return survey;
}
});
return isEligible ? survey : null;
// we check if the person meets the attribute filters for all the attribute filters
const isEligible = attributeFilters.every((attributeFilter) => {
const personAttributeValue = attributes[attributeFilter.attributeClassName];
if (!personAttributeValue) {
return false;
}
if (attributeFilter.operator === "equals") {
return personAttributeValue === attributeFilter.value;
} else if (attributeFilter.operator === "notEquals") {
return personAttributeValue !== attributeFilter.value;
} else {
// if the operator is not equals or not equals, we skip the survey, this means that new segment filter options are being used
return false;
}
});
return isEligible ? survey : null;
}
// Evaluate the segment filters
const result = await evaluateSegment(
{
attributes: attributes ?? {},
actionIds: personActionClassIds,
deviceType,
environmentId,
personId: person.id,
userId: personUserId,
},
segment.filters
);
return result ? survey : null;
});
const resolvedSurveys = await Promise.all(surveyPromises);
surveys = resolvedSurveys.filter((survey) => !!survey) as TSurvey[];
if (!surveys) {
throw new ResourceNotFoundError("Survey", environmentId);
}
return surveys;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
console.error(error);
throw new DatabaseError(error.message);
}
// Evaluate the segment filters
const result = await evaluateSegment(
{
attributes: attributes ?? {},
actionIds: personActionClassIds,
deviceType,
environmentId,
personId: person.id,
userId: personUserId,
},
segment.filters
);
return result ? survey : null;
});
const resolvedSurveys = await Promise.all(surveyPromises);
surveys = resolvedSurveys.filter((survey) => !!survey) as TSurvey[];
if (!surveys) {
throw new ResourceNotFoundError("Survey", environmentId);
throw error;
}
return surveys;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
console.error(error);
throw new DatabaseError(error.message);
}
throw error;
},
[`getSyncSurveys-${environmentId}-${personId}`],
{
tags: [
personCache.tag.byEnvironmentId(environmentId),
personCache.tag.byId(personId),
displayCache.tag.byPersonId(personId),
surveyCache.tag.byEnvironmentId(environmentId),
productCache.tag.byEnvironmentId(environmentId),
attributeCache.tag.byPersonId(personId),
],
}
},
[`getSyncSurveys-${environmentId}-${personId}`],
{
tags: [
personCache.tag.byEnvironmentId(environmentId),
personCache.tag.byId(personId),
displayCache.tag.byPersonId(personId),
surveyCache.tag.byEnvironmentId(environmentId),
productCache.tag.byEnvironmentId(environmentId),
attributeCache.tag.byPersonId(personId),
],
}
)();
)()
);
export const getSurveyIdByResultShareKey = async (resultShareKey: string): Promise<string | null> => {
try {
const survey = await prisma.survey.findFirst({
where: {
resultShareKey,
export const getSurveyIdByResultShareKey = reactCache(
(resultShareKey: string): Promise<string | null> =>
cache(
async () => {
try {
const survey = await prisma.survey.findFirst({
where: {
resultShareKey,
},
select: {
id: true,
},
});
if (!survey) {
return null;
}
return survey.id;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
},
select: {
id: true,
},
});
if (!survey) {
return null;
}
return survey.id;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
};
[`getSurveyIdByResultShareKey-${resultShareKey}`],
{
tags: [surveyCache.tag.byResultShareKey(resultShareKey)],
}
)()
);
export const loadNewSegmentInSurvey = async (surveyId: string, newSegmentId: string): Promise<TSurvey> => {
validateInputs([surveyId, ZId], [newSegmentId, ZId]);
@@ -1091,33 +1115,35 @@ export const loadNewSegmentInSurvey = async (surveyId: string, newSegmentId: str
}
};
export const getSurveysBySegmentId = (segmentId: string): Promise<TSurvey[]> =>
cache(
async () => {
try {
const surveysPrisma = await prisma.survey.findMany({
where: { segmentId },
select: selectSurvey,
});
export const getSurveysBySegmentId = reactCache(
(segmentId: string): Promise<TSurvey[]> =>
cache(
async () => {
try {
const surveysPrisma = await prisma.survey.findMany({
where: { segmentId },
select: selectSurvey,
});
const surveys: TSurvey[] = [];
const surveys: TSurvey[] = [];
for (const surveyPrisma of surveysPrisma) {
const transformedSurvey = transformPrismaSurvey(surveyPrisma);
surveys.push(transformedSurvey);
for (const surveyPrisma of surveysPrisma) {
const transformedSurvey = transformPrismaSurvey(surveyPrisma);
surveys.push(transformedSurvey);
}
return surveys;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
return surveys;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
},
[`getSurveysBySegmentId-${segmentId}`],
{
tags: [surveyCache.tag.bySegmentId(segmentId), segmentCache.tag.byId(segmentId)],
}
},
[`getSurveysBySegmentId-${segmentId}`],
{
tags: [surveyCache.tag.bySegmentId(segmentId), segmentCache.tag.byId(segmentId)],
}
)();
)()
);

25
packages/lib/tag/cache.ts Normal file
View File

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

View File

@@ -1,44 +1,65 @@
import "server-only";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { ZOptionalNumber, ZString } from "@formbricks/types/common";
import { ZId } from "@formbricks/types/environment";
import { TTag } from "@formbricks/types/tags";
import { cache } from "../cache";
import { ITEMS_PER_PAGE } from "../constants";
import { validateInputs } from "../utils/validate";
import { tagCache } from "./cache";
export const getTagsByEnvironmentId = async (environmentId: string, page?: number): Promise<TTag[]> => {
validateInputs([environmentId, ZId], [page, ZOptionalNumber]);
export const getTagsByEnvironmentId = reactCache(
(environmentId: string, page?: number): Promise<TTag[]> =>
cache(
async () => {
validateInputs([environmentId, ZId], [page, ZOptionalNumber]);
try {
const tags = await prisma.tag.findMany({
where: {
environmentId,
try {
const tags = await prisma.tag.findMany({
where: {
environmentId,
},
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
return tags;
} catch (error) {
throw error;
}
},
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
[`getTagsByEnvironmentId-${environmentId}-${page}`],
{
tags: [tagCache.tag.byEnvironmentId(environmentId)],
}
)()
);
return tags;
} catch (error) {
throw error;
}
};
export const getTag = reactCache(
(id: string): Promise<TTag | null> =>
cache(
async () => {
validateInputs([id, ZId]);
export const getTag = async (tagId: string): Promise<TTag | null> => {
validateInputs([tagId, ZId]);
try {
const tag = await prisma.tag.findUnique({
where: {
id,
},
});
try {
const tag = await prisma.tag.findUnique({
where: {
id: tagId,
return tag;
} catch (error) {
throw error;
}
},
});
return tag;
} catch (error) {
throw error;
}
};
[`getTag-${id}`],
{
tags: [tagCache.tag.byId(id)],
}
)()
);
export const createTag = async (environmentId: string, name: string): Promise<TTag> => {
validateInputs([environmentId, ZId], [name, ZString]);
@@ -51,20 +72,9 @@ export const createTag = async (environmentId: string, name: string): Promise<TT
},
});
return tag;
} catch (error) {
throw error;
}
};
export const deleteTag = async (tagId: string): Promise<TTag> => {
validateInputs([tagId, ZId]);
try {
const tag = await prisma.tag.delete({
where: {
id: tagId,
},
tagCache.revalidate({
id: tag.id,
environmentId,
});
return tag;
@@ -73,19 +83,45 @@ export const deleteTag = async (tagId: string): Promise<TTag> => {
}
};
export const updateTagName = async (tagId: string, name: string): Promise<TTag> => {
validateInputs([tagId, ZId], [name, ZString]);
export const deleteTag = async (id: string): Promise<TTag> => {
validateInputs([id, ZId]);
try {
const tag = await prisma.tag.delete({
where: {
id,
},
});
tagCache.revalidate({
id,
environmentId: tag.environmentId,
});
return tag;
} catch (error) {
throw error;
}
};
export const updateTagName = async (id: string, name: string): Promise<TTag> => {
validateInputs([id, ZId], [name, ZString]);
try {
const tag = await prisma.tag.update({
where: {
id: tagId,
id,
},
data: {
name,
},
});
tagCache.revalidate({
id: tag.id,
environmentId: tag.environmentId,
});
return tag;
} catch (error) {
throw error;
@@ -206,6 +242,15 @@ export const mergeTags = async (originalTagId: string, newTagId: string): Promis
}),
]);
tagCache.revalidate({
id: originalTagId,
environmentId: originalTag.environmentId,
});
tagCache.revalidate({
id: newTagId,
});
return newTag;
} catch (error) {
throw error;

View File

@@ -1,5 +1,6 @@
import "server-only";
import { Prisma } from "@prisma/client";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { ZId } from "@formbricks/types/environment";
import { DatabaseError } from "@formbricks/types/errors";
@@ -91,38 +92,40 @@ export const deleteTagOnResponse = async (responseId: string, tagId: string): Pr
}
};
export const getTagsOnResponsesCount = async (environmentId: string): Promise<TTagsCount> =>
cache(
async () => {
validateInputs([environmentId, ZId]);
export const getTagsOnResponsesCount = reactCache(
(environmentId: string): Promise<TTagsCount> =>
cache(
async () => {
validateInputs([environmentId, ZId]);
try {
const tagsCount = await prisma.tagsOnResponses.groupBy({
by: ["tagId"],
where: {
response: {
survey: {
environment: {
id: environmentId,
try {
const tagsCount = await prisma.tagsOnResponses.groupBy({
by: ["tagId"],
where: {
response: {
survey: {
environment: {
id: environmentId,
},
},
},
},
},
_count: {
_all: true,
},
});
_count: {
_all: true,
},
});
return tagsCount.map((tagCount) => ({ tagId: tagCount.tagId, count: tagCount._count._all }));
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
return tagsCount.map((tagCount) => ({ tagId: tagCount.tagId, count: tagCount._count._all }));
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
throw error;
},
[`getTagsOnResponsesCount-${environmentId}`],
{
tags: [tagOnResponseCache.tag.byEnvironmentId(environmentId)],
}
},
[`getTagsOnResponsesCount-${environmentId}`],
{
tags: [tagOnResponseCache.tag.byEnvironmentId(environmentId)],
}
)();
)()
);

View File

@@ -1,5 +1,6 @@
import "server-only";
import { Prisma } from "@prisma/client";
import { cache as reactCache } from "react";
import { z } from "zod";
import { prisma } from "@formbricks/database";
import { ZId } from "@formbricks/types/environment";
@@ -29,64 +30,68 @@ const responseSelection = {
};
// function to retrive basic information about a user's user
export const getUser = (id: string): Promise<TUser | null> =>
cache(
async () => {
validateInputs([id, ZId]);
export const getUser = reactCache(
(id: string): Promise<TUser | null> =>
cache(
async () => {
validateInputs([id, ZId]);
try {
const user = await prisma.user.findUnique({
where: {
id,
},
select: responseSelection,
});
try {
const user = await prisma.user.findUnique({
where: {
id,
},
select: responseSelection,
});
if (!user) {
return null;
if (!user) {
return null;
}
return user;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
return user;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
},
[`getUser-${id}`],
{
tags: [userCache.tag.byId(id)],
}
},
[`getUser-${id}`],
{
tags: [userCache.tag.byId(id)],
}
)();
)()
);
export const getUserByEmail = (email: string): Promise<TUser | null> =>
cache(
async () => {
validateInputs([email, z.string().email()]);
export const getUserByEmail = reactCache(
(email: string): Promise<TUser | null> =>
cache(
async () => {
validateInputs([email, z.string().email()]);
try {
const user = await prisma.user.findFirst({
where: {
email,
},
select: responseSelection,
});
try {
const user = await prisma.user.findFirst({
where: {
email,
},
select: responseSelection,
});
return user;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
return user;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
throw error;
},
[`getUserByEmail-${email}`],
{
tags: [userCache.tag.byEmail(email)],
}
},
[`getUserByEmail-${email}`],
{
tags: [userCache.tag.byEmail(email)],
}
)();
)()
);
const getAdminMemberships = (memberships: TMembership[]): TMembership[] =>
memberships.filter((membership) => membership.role === "admin");

View File

@@ -1,4 +1,4 @@
import z from "zod";
import { z } from "zod";
import { ValidationError } from "@formbricks/types/errors";
type ValidationPair = [any, z.ZodSchema<any>];

View File

@@ -10,6 +10,18 @@ vi.mock("next/cache", () => ({
revalidateTag: vi.fn(),
}));
// mock react cache
const testCache = <T extends Function>(func: T) => func;
vi.mock("react", () => {
const originalModule = vi.importActual("react");
return {
...originalModule,
cache: testCache,
};
});
// mock server-only
vi.mock("server-only", () => {
return {};
});