mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-13 19:00:23 -06:00
fix: adds try...catch in all services (#2494)
Co-authored-by: Matti Nannt <mail@matthiasnannt.com>
This commit is contained in:
@@ -8,12 +8,17 @@ export const sendFreeLimitReachedEventToPosthogBiWeekly = async (
|
||||
): Promise<string> =>
|
||||
unstable_cache(
|
||||
async () => {
|
||||
await capturePosthogEnvironmentEvent(environmentId, "free limit reached", {
|
||||
plan,
|
||||
});
|
||||
return "success";
|
||||
try {
|
||||
await capturePosthogEnvironmentEvent(environmentId, "free limit reached", {
|
||||
plan,
|
||||
});
|
||||
return "success";
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`posthog-${plan}-limitReached-${environmentId}`],
|
||||
[`sendFreeLimitReachedEventToPosthogBiWeekly-${plan}-${environmentId}`],
|
||||
{
|
||||
revalidate: 60 * 60 * 24 * 15, // 15 days
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ export async function POST(request: Request) {
|
||||
|
||||
return Response.json(user);
|
||||
} catch (e) {
|
||||
if (e.code === "P2002") {
|
||||
if (e.message === "User with this email already exists") {
|
||||
return Response.json(
|
||||
{
|
||||
error: "user with this email address already exists",
|
||||
|
||||
@@ -26,29 +26,37 @@ export const getActionsByPersonId = async (personId: string, page?: number): Pro
|
||||
async () => {
|
||||
validateInputs([personId, ZId], [page, ZOptionalNumber]);
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
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,
|
||||
}));
|
||||
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}`],
|
||||
{
|
||||
@@ -119,58 +127,66 @@ export const getActionsByEnvironmentId = async (environmentId: string, page?: nu
|
||||
export const createAction = async (data: TActionInput): Promise<TAction> => {
|
||||
validateInputs([data, ZActionInput]);
|
||||
|
||||
const { environmentId, name, userId } = data;
|
||||
try {
|
||||
const { environmentId, name, userId } = data;
|
||||
|
||||
let actionType: TActionClassType = "code";
|
||||
if (name === "Exit Intent (Desktop)" || name === "50% Scroll") {
|
||||
actionType = "automatic";
|
||||
}
|
||||
let actionType: TActionClassType = "code";
|
||||
if (name === "Exit Intent (Desktop)" || name === "50% Scroll") {
|
||||
actionType = "automatic";
|
||||
}
|
||||
|
||||
let actionClass = await getActionClassByEnvironmentIdAndName(environmentId, name);
|
||||
let actionClass = await getActionClassByEnvironmentIdAndName(environmentId, name);
|
||||
|
||||
if (!actionClass) {
|
||||
actionClass = await createActionClass(environmentId, {
|
||||
name,
|
||||
type: actionType,
|
||||
environmentId,
|
||||
});
|
||||
}
|
||||
if (!actionClass) {
|
||||
actionClass = await createActionClass(environmentId, {
|
||||
name,
|
||||
type: actionType,
|
||||
environmentId,
|
||||
});
|
||||
}
|
||||
|
||||
const action = await prisma.action.create({
|
||||
data: {
|
||||
person: {
|
||||
connect: {
|
||||
environmentId_userId: {
|
||||
environmentId,
|
||||
userId,
|
||||
const action = await prisma.action.create({
|
||||
data: {
|
||||
person: {
|
||||
connect: {
|
||||
environmentId_userId: {
|
||||
environmentId,
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
actionClass: {
|
||||
connect: {
|
||||
id: actionClass.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
actionClass: {
|
||||
connect: {
|
||||
id: actionClass.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const isPersonMonthlyActive = await getIsPersonMonthlyActive(action.personId);
|
||||
if (!isPersonMonthlyActive) {
|
||||
activePersonCache.revalidate({ id: action.personId });
|
||||
const isPersonMonthlyActive = await getIsPersonMonthlyActive(action.personId);
|
||||
if (!isPersonMonthlyActive) {
|
||||
activePersonCache.revalidate({ id: action.personId });
|
||||
}
|
||||
|
||||
actionCache.revalidate({
|
||||
environmentId,
|
||||
personId: action.personId,
|
||||
});
|
||||
|
||||
return {
|
||||
id: action.id,
|
||||
createdAt: action.createdAt,
|
||||
personId: action.personId,
|
||||
properties: action.properties,
|
||||
actionClass,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError("Database operation failed");
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
actionCache.revalidate({
|
||||
environmentId,
|
||||
personId: action.personId,
|
||||
});
|
||||
|
||||
return {
|
||||
id: action.id,
|
||||
createdAt: action.createdAt,
|
||||
personId: action.personId,
|
||||
properties: action.properties,
|
||||
actionClass,
|
||||
};
|
||||
};
|
||||
|
||||
export const getActionCountInLastHour = async (actionClassId: string): Promise<number> =>
|
||||
@@ -254,17 +270,25 @@ export const getActionCountInLast7Days = async (actionClassId: string): Promise<
|
||||
export const getActionCountInLastQuarter = async (actionClassId: string, personId: string): Promise<number> =>
|
||||
await unstable_cache(
|
||||
async () => {
|
||||
return await prisma.action.count({
|
||||
where: {
|
||||
personId,
|
||||
actionClass: {
|
||||
id: actionClassId,
|
||||
validateInputs([actionClassId, ZId], [personId, ZId]);
|
||||
|
||||
try {
|
||||
const numEventsLastQuarter = await prisma.action.count({
|
||||
where: {
|
||||
personId,
|
||||
actionClass: {
|
||||
id: actionClassId,
|
||||
},
|
||||
createdAt: {
|
||||
gte: getStartDateOfLastQuarter(),
|
||||
},
|
||||
},
|
||||
createdAt: {
|
||||
gte: getStartDateOfLastQuarter(),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return numEventsLastQuarter;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`getActionCountInLastQuarter-${actionClassId}-${personId}`],
|
||||
{
|
||||
@@ -276,17 +300,25 @@ export const getActionCountInLastQuarter = async (actionClassId: string, personI
|
||||
export const getActionCountInLastMonth = async (actionClassId: string, personId: string): Promise<number> =>
|
||||
await unstable_cache(
|
||||
async () => {
|
||||
return await prisma.action.count({
|
||||
where: {
|
||||
personId,
|
||||
actionClass: {
|
||||
id: actionClassId,
|
||||
validateInputs([actionClassId, ZId], [personId, ZId]);
|
||||
|
||||
try {
|
||||
const numEventsLastMonth = await prisma.action.count({
|
||||
where: {
|
||||
personId,
|
||||
actionClass: {
|
||||
id: actionClassId,
|
||||
},
|
||||
createdAt: {
|
||||
gte: getStartDateOfLastMonth(),
|
||||
},
|
||||
},
|
||||
createdAt: {
|
||||
gte: getStartDateOfLastMonth(),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return numEventsLastMonth;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`getActionCountInLastMonth-${actionClassId}-${personId}`],
|
||||
{
|
||||
@@ -298,17 +330,24 @@ export const getActionCountInLastMonth = async (actionClassId: string, personId:
|
||||
export const getActionCountInLastWeek = async (actionClassId: string, personId: string): Promise<number> =>
|
||||
await unstable_cache(
|
||||
async () => {
|
||||
return await prisma.action.count({
|
||||
where: {
|
||||
personId,
|
||||
actionClass: {
|
||||
id: actionClassId,
|
||||
validateInputs([actionClassId, ZId], [personId, ZId]);
|
||||
|
||||
try {
|
||||
const numEventsLastWeek = await prisma.action.count({
|
||||
where: {
|
||||
personId,
|
||||
actionClass: {
|
||||
id: actionClassId,
|
||||
},
|
||||
createdAt: {
|
||||
gte: getStartDateOfLastWeek(),
|
||||
},
|
||||
},
|
||||
createdAt: {
|
||||
gte: getStartDateOfLastWeek(),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
return numEventsLastWeek;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`getActionCountInLastWeek-${actionClassId}-${personId}`],
|
||||
{
|
||||
@@ -323,16 +362,22 @@ export const getTotalOccurrencesForAction = async (
|
||||
): Promise<number> =>
|
||||
await unstable_cache(
|
||||
async () => {
|
||||
const count = await prisma.action.count({
|
||||
where: {
|
||||
personId,
|
||||
actionClass: {
|
||||
id: actionClassId,
|
||||
},
|
||||
},
|
||||
});
|
||||
validateInputs([actionClassId, ZId], [personId, ZId]);
|
||||
|
||||
return count;
|
||||
try {
|
||||
const count = await prisma.action.count({
|
||||
where: {
|
||||
personId,
|
||||
actionClass: {
|
||||
id: actionClassId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return count;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`getTotalOccurrencesForAction-${actionClassId}-${personId}`],
|
||||
{
|
||||
@@ -347,23 +392,29 @@ export const getLastOccurrenceDaysAgo = async (
|
||||
): Promise<number | null> =>
|
||||
await unstable_cache(
|
||||
async () => {
|
||||
const lastEvent = await prisma.action.findFirst({
|
||||
where: {
|
||||
personId,
|
||||
actionClass: {
|
||||
id: actionClassId,
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
select: {
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
validateInputs([actionClassId, ZId], [personId, ZId]);
|
||||
|
||||
if (!lastEvent) return null;
|
||||
return differenceInDays(new Date(), lastEvent.createdAt);
|
||||
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}`],
|
||||
{
|
||||
@@ -378,23 +429,29 @@ export const getFirstOccurrenceDaysAgo = async (
|
||||
): Promise<number | null> =>
|
||||
await unstable_cache(
|
||||
async () => {
|
||||
const firstEvent = await prisma.action.findFirst({
|
||||
where: {
|
||||
personId,
|
||||
actionClass: {
|
||||
id: actionClassId,
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "asc",
|
||||
},
|
||||
select: {
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
validateInputs([actionClassId, ZId], [personId, ZId]);
|
||||
|
||||
if (!firstEvent) return null;
|
||||
return differenceInDays(new Date(), firstEvent.createdAt);
|
||||
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}`],
|
||||
{
|
||||
|
||||
@@ -17,19 +17,24 @@ export const canUserUpdateActionClass = async (userId: string, actionClassId: st
|
||||
await unstable_cache(
|
||||
async () => {
|
||||
validateInputs([userId, ZId], [actionClassId, ZId]);
|
||||
if (!userId) return false;
|
||||
|
||||
const actionClass = await getActionClass(actionClassId);
|
||||
if (!actionClass) return false;
|
||||
try {
|
||||
if (!userId) return false;
|
||||
|
||||
const hasAccessToEnvironment = await hasUserEnvironmentAccess(userId, actionClass.environmentId);
|
||||
const actionClass = await getActionClass(actionClassId);
|
||||
if (!actionClass) return false;
|
||||
|
||||
if (!hasAccessToEnvironment) return false;
|
||||
const hasAccessToEnvironment = await hasUserEnvironmentAccess(userId, actionClass.environmentId);
|
||||
|
||||
return true;
|
||||
if (!hasAccessToEnvironment) return false;
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
[`users-${userId}-actionClasses-${actionClassId}`],
|
||||
[`canUserUpdateActionClass-${userId}-${actionClassId}`],
|
||||
{
|
||||
revalidate: SERVICES_REVALIDATION_INTERVAL,
|
||||
tags: [actionClassCache.tag.byId(actionClassId)],
|
||||
@@ -43,22 +48,26 @@ export const verifyUserRoleAccess = async (
|
||||
hasCreateOrUpdateAccess: boolean;
|
||||
hasDeleteAccess: boolean;
|
||||
}> => {
|
||||
const accessObject = {
|
||||
hasCreateOrUpdateAccess: true,
|
||||
hasDeleteAccess: true,
|
||||
};
|
||||
try {
|
||||
const accessObject = {
|
||||
hasCreateOrUpdateAccess: true,
|
||||
hasDeleteAccess: true,
|
||||
};
|
||||
|
||||
const team = await getTeamByEnvironmentId(environmentId);
|
||||
if (!team) {
|
||||
throw new Error("Team not found");
|
||||
const team = await getTeamByEnvironmentId(environmentId);
|
||||
if (!team) {
|
||||
throw new Error("Team not found");
|
||||
}
|
||||
|
||||
const currentUserMembership = await getMembershipByUserIdTeamId(userId, team.id);
|
||||
const { isViewer } = getAccessFlags(currentUserMembership?.role);
|
||||
|
||||
if (isViewer) {
|
||||
accessObject.hasCreateOrUpdateAccess = false;
|
||||
accessObject.hasDeleteAccess = false;
|
||||
}
|
||||
return accessObject;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const currentUserMembership = await getMembershipByUserIdTeamId(userId, team.id);
|
||||
const { isViewer } = getAccessFlags(currentUserMembership?.role);
|
||||
|
||||
if (isViewer) {
|
||||
accessObject.hasCreateOrUpdateAccess = false;
|
||||
accessObject.hasDeleteAccess = false;
|
||||
}
|
||||
return accessObject;
|
||||
};
|
||||
|
||||
@@ -86,7 +86,7 @@ export const getActionClassByEnvironmentIdAndName = async (
|
||||
throw new DatabaseError(`Database error when fetching action`);
|
||||
}
|
||||
},
|
||||
[`getActionClass-${environmentId}-${name}`],
|
||||
[`getActionClassByEnvironmentIdAndName-${environmentId}-${name}`],
|
||||
{
|
||||
tags: [actionClassCache.tag.byNameAndEnvironmentId(name, environmentId)],
|
||||
revalidate: SERVICES_REVALIDATION_INTERVAL,
|
||||
|
||||
@@ -15,13 +15,17 @@ export const canUserAccessApiKey = async (userId: string, apiKeyId: string): Pro
|
||||
async () => {
|
||||
validateInputs([userId, ZId], [apiKeyId, ZId]);
|
||||
|
||||
const apiKeyFromServer = await getApiKey(apiKeyId);
|
||||
if (!apiKeyFromServer) return false;
|
||||
try {
|
||||
const apiKeyFromServer = await getApiKey(apiKeyId);
|
||||
if (!apiKeyFromServer) return false;
|
||||
|
||||
const hasAccessToEnvironment = await hasUserEnvironmentAccess(userId, apiKeyFromServer.environmentId);
|
||||
if (!hasAccessToEnvironment) return false;
|
||||
const hasAccessToEnvironment = await hasUserEnvironmentAccess(userId, apiKeyFromServer.environmentId);
|
||||
if (!hasAccessToEnvironment) return false;
|
||||
|
||||
return true;
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
[`canUserAccessApiKey-${userId}-${apiKeyId}`],
|
||||
|
||||
@@ -83,7 +83,10 @@ export const getApiKeys = async (environmentId: string, page?: number): Promise<
|
||||
|
||||
export const hashApiKey = (key: string): string => createHash("sha256").update(key).digest("hex");
|
||||
|
||||
export async function createApiKey(environmentId: string, apiKeyData: TApiKeyCreateInput): Promise<TApiKey> {
|
||||
export const createApiKey = async (
|
||||
environmentId: string,
|
||||
apiKeyData: TApiKeyCreateInput
|
||||
): Promise<TApiKey> => {
|
||||
validateInputs([environmentId, ZId], [apiKeyData, ZApiKeyCreateInput]);
|
||||
try {
|
||||
const key = randomBytes(16).toString("hex");
|
||||
@@ -110,7 +113,7 @@ export async function createApiKey(environmentId: string, apiKeyData: TApiKeyCre
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const getApiKeyFromKey = async (apiKey: string): Promise<TApiKey | null> => {
|
||||
const hashedKey = getHash(apiKey);
|
||||
|
||||
@@ -18,15 +18,19 @@ export const canUserAccessAttributeClass = async (
|
||||
validateInputs([userId, ZId], [attributeClassId, ZId]);
|
||||
if (!userId) return false;
|
||||
|
||||
const attributeClass = await getAttributeClass(attributeClassId);
|
||||
if (!attributeClass) return false;
|
||||
try {
|
||||
const attributeClass = await getAttributeClass(attributeClassId);
|
||||
if (!attributeClass) return false;
|
||||
|
||||
const hasAccessToEnvironment = await hasUserEnvironmentAccess(userId, attributeClass.environmentId);
|
||||
if (!hasAccessToEnvironment) return false;
|
||||
const hasAccessToEnvironment = await hasUserEnvironmentAccess(userId, attributeClass.environmentId);
|
||||
if (!hasAccessToEnvironment) return false;
|
||||
|
||||
return true;
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
[`users-${userId}-attributeClasses-${attributeClassId}`],
|
||||
[`canUserAccessAttributeClass-${userId}-${attributeClassId}`],
|
||||
{ revalidate: SERVICES_REVALIDATION_INTERVAL, tags: [`attributeClasses-${attributeClassId}`] }
|
||||
)();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import "server-only";
|
||||
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { unstable_cache } from "next/cache";
|
||||
|
||||
import { prisma } from "@formbricks/database";
|
||||
@@ -28,13 +29,18 @@ export const getAttributeClass = async (attributeClassId: string): Promise<TAttr
|
||||
validateInputs([attributeClassId, ZId]);
|
||||
|
||||
try {
|
||||
return await prisma.attributeClass.findFirst({
|
||||
const attributeClass = await prisma.attributeClass.findFirst({
|
||||
where: {
|
||||
id: attributeClassId,
|
||||
},
|
||||
});
|
||||
|
||||
return attributeClass;
|
||||
} catch (error) {
|
||||
throw new DatabaseError(`Database error when fetching attributeClass with id ${attributeClassId}`);
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`getAttributeClass-${attributeClassId}`],
|
||||
@@ -75,9 +81,10 @@ export const getAttributeClasses = async (
|
||||
return true;
|
||||
});
|
||||
} catch (error) {
|
||||
throw new DatabaseError(
|
||||
`Database error when fetching attributeClasses for environment ${environmentId}`
|
||||
);
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`getAttributeClasses-${environmentId}-${page}`],
|
||||
@@ -114,7 +121,10 @@ export const updateAttributeClass = async (
|
||||
|
||||
return attributeClass;
|
||||
} catch (error) {
|
||||
throw new DatabaseError(`Database error when updating attribute class with id ${attributeClassId}`);
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -123,14 +133,21 @@ export const getAttributeClassByName = async (environmentId: string, name: strin
|
||||
async (): Promise<TAttributeClass | null> => {
|
||||
validateInputs([environmentId, ZId], [name, ZString]);
|
||||
|
||||
const attributeClass = await prisma.attributeClass.findFirst({
|
||||
where: {
|
||||
environmentId,
|
||||
name,
|
||||
},
|
||||
});
|
||||
try {
|
||||
const attributeClass = await prisma.attributeClass.findFirst({
|
||||
where: {
|
||||
environmentId,
|
||||
name,
|
||||
},
|
||||
});
|
||||
|
||||
return attributeClass;
|
||||
return attributeClass;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`getAttributeClassByName-${environmentId}-${name}`],
|
||||
{
|
||||
@@ -148,25 +165,32 @@ export const createAttributeClass = async (
|
||||
): Promise<TAttributeClass | null> => {
|
||||
validateInputs([environmentId, ZId], [name, ZString], [type, ZAttributeClassType]);
|
||||
|
||||
const attributeClass = await prisma.attributeClass.create({
|
||||
data: {
|
||||
name,
|
||||
type,
|
||||
environment: {
|
||||
connect: {
|
||||
id: environmentId,
|
||||
try {
|
||||
const attributeClass = await prisma.attributeClass.create({
|
||||
data: {
|
||||
name,
|
||||
type,
|
||||
environment: {
|
||||
connect: {
|
||||
id: environmentId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
attributeClassCache.revalidate({
|
||||
id: attributeClass.id,
|
||||
environmentId: attributeClass.environmentId,
|
||||
name: attributeClass.name,
|
||||
});
|
||||
attributeClassCache.revalidate({
|
||||
id: attributeClass.id,
|
||||
environmentId: attributeClass.environmentId,
|
||||
name: attributeClass.name,
|
||||
});
|
||||
|
||||
return attributeClass;
|
||||
return attributeClass;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteAttributeClass = async (attributeClassId: string): Promise<TAttributeClass> => {
|
||||
@@ -187,6 +211,9 @@ export const deleteAttributeClass = async (attributeClassId: string): Promise<TA
|
||||
|
||||
return deletedAttributeClass;
|
||||
} catch (error) {
|
||||
throw new DatabaseError(`Database error when deleting webhook with ID ${attributeClassId}`);
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -367,10 +367,13 @@ export const getDisplayCountBySurveyId = async (
|
||||
});
|
||||
return displayCount;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`getDisplayCountBySurveyId-${surveyId}`],
|
||||
[`getDisplayCountBySurveyId-${surveyId}-${JSON.stringify(filters)}`],
|
||||
{
|
||||
tags: [displayCache.tag.bySurveyId(surveyId)],
|
||||
revalidate: SERVICES_REVALIDATION_INTERVAL,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { unstable_cache } from "next/cache";
|
||||
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { ZId } from "@formbricks/types/environment";
|
||||
import { DatabaseError } from "@formbricks/types/errors";
|
||||
|
||||
import { SERVICES_REVALIDATION_INTERVAL } from "../constants";
|
||||
import { teamCache } from "../team/cache";
|
||||
@@ -12,29 +14,36 @@ export const hasUserEnvironmentAccess = async (userId: string, environmentId: st
|
||||
async (): Promise<boolean> => {
|
||||
validateInputs([userId, ZId], [environmentId, ZId]);
|
||||
|
||||
const environment = await prisma.environment.findUnique({
|
||||
where: {
|
||||
id: environmentId,
|
||||
},
|
||||
select: {
|
||||
product: {
|
||||
select: {
|
||||
team: {
|
||||
select: {
|
||||
memberships: {
|
||||
select: {
|
||||
userId: true,
|
||||
try {
|
||||
const environment = await prisma.environment.findUnique({
|
||||
where: {
|
||||
id: environmentId,
|
||||
},
|
||||
select: {
|
||||
product: {
|
||||
select: {
|
||||
team: {
|
||||
select: {
|
||||
memberships: {
|
||||
select: {
|
||||
userId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const environmentUsers = environment?.product.team.memberships.map((member) => member.userId) || [];
|
||||
return environmentUsers.includes(userId);
|
||||
const environmentUsers = environment?.product.team.memberships.map((member) => member.userId) || [];
|
||||
return environmentUsers.includes(userId);
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`hasUserEnvironmentAccess-${userId}-${environmentId}`],
|
||||
{
|
||||
|
||||
@@ -134,25 +134,29 @@ export const updateEnvironment = async (
|
||||
};
|
||||
|
||||
export const getFirstEnvironmentByUserId = async (userId: string): Promise<TEnvironment | null> => {
|
||||
const teams = await getTeamsByUserId(userId);
|
||||
if (teams.length === 0) {
|
||||
throw new Error(`Unable to get first environment: User ${userId} has no teams`);
|
||||
}
|
||||
const firstTeam = teams[0];
|
||||
const products = await getProducts(firstTeam.id);
|
||||
if (products.length === 0) {
|
||||
throw new Error(`Unable to get first environment: Team ${firstTeam.id} has no products`);
|
||||
}
|
||||
const firstProduct = products[0];
|
||||
const productionEnvironment = firstProduct.environments.find(
|
||||
(environment) => environment.type === "production"
|
||||
);
|
||||
if (!productionEnvironment) {
|
||||
throw new Error(
|
||||
`Unable to get first environment: Product ${firstProduct.id} has no production environment`
|
||||
try {
|
||||
const teams = await getTeamsByUserId(userId);
|
||||
if (teams.length === 0) {
|
||||
throw new Error(`Unable to get first environment: User ${userId} has no teams`);
|
||||
}
|
||||
const firstTeam = teams[0];
|
||||
const products = await getProducts(firstTeam.id);
|
||||
if (products.length === 0) {
|
||||
throw new Error(`Unable to get first environment: Team ${firstTeam.id} has no products`);
|
||||
}
|
||||
const firstProduct = products[0];
|
||||
const productionEnvironment = firstProduct.environments.find(
|
||||
(environment) => environment.type === "production"
|
||||
);
|
||||
if (!productionEnvironment) {
|
||||
throw new Error(
|
||||
`Unable to get first environment: Product ${firstProduct.id} has no production environment`
|
||||
);
|
||||
}
|
||||
return productionEnvironment;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
return productionEnvironment;
|
||||
};
|
||||
|
||||
export const createEnvironment = async (
|
||||
@@ -161,44 +165,51 @@ export const createEnvironment = async (
|
||||
): Promise<TEnvironment> => {
|
||||
validateInputs([productId, ZId], [environmentInput, ZEnvironmentCreateInput]);
|
||||
|
||||
const environment = await prisma.environment.create({
|
||||
data: {
|
||||
type: environmentInput.type || "development",
|
||||
product: { connect: { id: productId } },
|
||||
widgetSetupCompleted: environmentInput.widgetSetupCompleted || false,
|
||||
actionClasses: {
|
||||
create: [
|
||||
{
|
||||
name: "New Session",
|
||||
description: "Gets fired when a new session is created",
|
||||
type: "automatic",
|
||||
},
|
||||
{
|
||||
name: "Exit Intent (Desktop)",
|
||||
description: "A user on Desktop leaves the website with the cursor.",
|
||||
type: "automatic",
|
||||
},
|
||||
{
|
||||
name: "50% Scroll",
|
||||
description: "A user scrolled 50% of the current page",
|
||||
type: "automatic",
|
||||
},
|
||||
],
|
||||
try {
|
||||
const environment = await prisma.environment.create({
|
||||
data: {
|
||||
type: environmentInput.type || "development",
|
||||
product: { connect: { id: productId } },
|
||||
widgetSetupCompleted: environmentInput.widgetSetupCompleted || false,
|
||||
actionClasses: {
|
||||
create: [
|
||||
{
|
||||
name: "New Session",
|
||||
description: "Gets fired when a new session is created",
|
||||
type: "automatic",
|
||||
},
|
||||
{
|
||||
name: "Exit Intent (Desktop)",
|
||||
description: "A user on Desktop leaves the website with the cursor.",
|
||||
type: "automatic",
|
||||
},
|
||||
{
|
||||
name: "50% Scroll",
|
||||
description: "A user scrolled 50% of the current page",
|
||||
type: "automatic",
|
||||
},
|
||||
],
|
||||
},
|
||||
attributeClasses: {
|
||||
create: [
|
||||
// { name: "userId", description: "The internal ID of the person", type: "automatic" },
|
||||
{ name: "email", description: "The email of the person", type: "automatic" },
|
||||
{ name: "language", description: "The language used by the person", type: "automatic" },
|
||||
],
|
||||
},
|
||||
},
|
||||
attributeClasses: {
|
||||
create: [
|
||||
// { name: "userId", description: "The internal ID of the person", type: "automatic" },
|
||||
{ name: "email", description: "The email of the person", type: "automatic" },
|
||||
{ name: "language", description: "The language used by the person", type: "automatic" },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
environmentCache.revalidate({
|
||||
id: environment.id,
|
||||
productId: environment.productId,
|
||||
});
|
||||
environmentCache.revalidate({
|
||||
id: environment.id,
|
||||
productId: environment.productId,
|
||||
});
|
||||
|
||||
return environment;
|
||||
return environment;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -15,15 +15,19 @@ export const canUserAccessIntegration = async (userId: string, integrationId: st
|
||||
validateInputs([userId, ZId], [integrationId, ZId]);
|
||||
if (!userId) return false;
|
||||
|
||||
const integration = await getIntegration(integrationId);
|
||||
if (!integration) return false;
|
||||
try {
|
||||
const integration = await getIntegration(integrationId);
|
||||
if (!integration) return false;
|
||||
|
||||
const hasAccessToEnvironment = await hasUserEnvironmentAccess(userId, integration.environmentId);
|
||||
if (!hasAccessToEnvironment) return false;
|
||||
const hasAccessToEnvironment = await hasUserEnvironmentAccess(userId, integration.environmentId);
|
||||
if (!hasAccessToEnvironment) return false;
|
||||
|
||||
return true;
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
[`users-${userId}-integrations-${integrationId}`],
|
||||
[`canUserAccessIntegration-${userId}-${integrationId}`],
|
||||
{ revalidate: SERVICES_REVALIDATION_INTERVAL, tags: [`integrations-${integrationId}`] }
|
||||
)();
|
||||
|
||||
@@ -47,12 +47,22 @@ export const getInvitesByTeamId = async (teamId: string, page?: number): Promise
|
||||
async () => {
|
||||
validateInputs([teamId, ZString], [page, ZOptionalNumber]);
|
||||
|
||||
return prisma.invite.findMany({
|
||||
where: { teamId },
|
||||
select: inviteSelect,
|
||||
take: page ? ITEMS_PER_PAGE : undefined,
|
||||
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
|
||||
});
|
||||
try {
|
||||
const invites = await prisma.invite.findMany({
|
||||
where: { teamId },
|
||||
select: inviteSelect,
|
||||
take: page ? ITEMS_PER_PAGE : undefined,
|
||||
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
|
||||
});
|
||||
|
||||
return invites;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`getInvitesByTeamId-${teamId}-${page}`],
|
||||
{
|
||||
@@ -126,21 +136,29 @@ export const getInvite = async (inviteId: string): Promise<InviteWithCreator | n
|
||||
async () => {
|
||||
validateInputs([inviteId, ZString]);
|
||||
|
||||
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;
|
||||
return invite;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`getInvite-${inviteId}`],
|
||||
{ tags: [inviteCache.tag.byId(inviteId)], revalidate: SERVICES_REVALIDATION_INTERVAL }
|
||||
@@ -156,38 +174,47 @@ export const getInvite = async (inviteId: string): Promise<InviteWithCreator | n
|
||||
|
||||
export const resendInvite = async (inviteId: string): Promise<TInvite> => {
|
||||
validateInputs([inviteId, ZString]);
|
||||
const invite = await prisma.invite.findUnique({
|
||||
where: {
|
||||
id: inviteId,
|
||||
},
|
||||
select: {
|
||||
email: true,
|
||||
name: true,
|
||||
creator: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!invite) {
|
||||
throw new ResourceNotFoundError("Invite", inviteId);
|
||||
try {
|
||||
const invite = await prisma.invite.findUnique({
|
||||
where: {
|
||||
id: inviteId,
|
||||
},
|
||||
select: {
|
||||
email: true,
|
||||
name: true,
|
||||
creator: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!invite) {
|
||||
throw new ResourceNotFoundError("Invite", inviteId);
|
||||
}
|
||||
|
||||
await sendInviteMemberEmail(inviteId, invite.email, invite.creator?.name ?? "", invite.name ?? "");
|
||||
|
||||
const updatedInvite = await prisma.invite.update({
|
||||
where: {
|
||||
id: inviteId,
|
||||
},
|
||||
data: {
|
||||
expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7),
|
||||
},
|
||||
});
|
||||
|
||||
inviteCache.revalidate({
|
||||
id: updatedInvite.id,
|
||||
teamId: updatedInvite.teamId,
|
||||
});
|
||||
|
||||
return updatedInvite;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
await sendInviteMemberEmail(inviteId, invite.email, invite.creator?.name ?? "", invite.name ?? "");
|
||||
|
||||
const updatedInvite = await prisma.invite.update({
|
||||
where: {
|
||||
id: inviteId,
|
||||
},
|
||||
data: {
|
||||
expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7),
|
||||
},
|
||||
});
|
||||
|
||||
inviteCache.revalidate({
|
||||
id: updatedInvite.id,
|
||||
teamId: updatedInvite.teamId,
|
||||
});
|
||||
|
||||
return updatedInvite;
|
||||
};
|
||||
|
||||
export const inviteUser = async ({
|
||||
@@ -205,44 +232,52 @@ export const inviteUser = async ({
|
||||
}): Promise<TInvite> => {
|
||||
validateInputs([teamId, ZString], [invitee, ZInvitee], [currentUser, ZCurrentUser]);
|
||||
|
||||
const { name, email, role } = invitee;
|
||||
const { id: currentUserId, name: currentUserName } = currentUser;
|
||||
const existingInvite = await prisma.invite.findFirst({ where: { email, teamId } });
|
||||
try {
|
||||
const { name, email, role } = invitee;
|
||||
const { id: currentUserId, name: currentUserName } = currentUser;
|
||||
const existingInvite = await prisma.invite.findFirst({ where: { email, teamId } });
|
||||
|
||||
if (existingInvite) {
|
||||
throw new ValidationError("Invite already exists");
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { email } });
|
||||
|
||||
if (user) {
|
||||
const member = await getMembershipByUserIdTeamId(user.id, teamId);
|
||||
|
||||
if (member) {
|
||||
throw new ValidationError("User is already a member of this team");
|
||||
if (existingInvite) {
|
||||
throw new ValidationError("Invite already exists");
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { email } });
|
||||
|
||||
if (user) {
|
||||
const member = await getMembershipByUserIdTeamId(user.id, teamId);
|
||||
|
||||
if (member) {
|
||||
throw new ValidationError("User is already a member of this team");
|
||||
}
|
||||
}
|
||||
|
||||
const expiresIn = 7 * 24 * 60 * 60 * 1000; // 7 days
|
||||
const expiresAt = new Date(Date.now() + expiresIn);
|
||||
|
||||
const invite = await prisma.invite.create({
|
||||
data: {
|
||||
email,
|
||||
name,
|
||||
team: { connect: { id: teamId } },
|
||||
creator: { connect: { id: currentUserId } },
|
||||
acceptor: user ? { connect: { id: user.id } } : undefined,
|
||||
role,
|
||||
expiresAt,
|
||||
},
|
||||
});
|
||||
|
||||
inviteCache.revalidate({
|
||||
id: invite.id,
|
||||
teamId: invite.teamId,
|
||||
});
|
||||
|
||||
await sendInviteMemberEmail(invite.id, email, currentUserName, name, isOnboardingInvite, inviteMessage);
|
||||
return invite;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
const expiresIn = 7 * 24 * 60 * 60 * 1000; // 7 days
|
||||
const expiresAt = new Date(Date.now() + expiresIn);
|
||||
|
||||
const invite = await prisma.invite.create({
|
||||
data: {
|
||||
email,
|
||||
name,
|
||||
team: { connect: { id: teamId } },
|
||||
creator: { connect: { id: currentUserId } },
|
||||
acceptor: user ? { connect: { id: user.id } } : undefined,
|
||||
role,
|
||||
expiresAt,
|
||||
},
|
||||
});
|
||||
|
||||
inviteCache.revalidate({
|
||||
id: invite.id,
|
||||
teamId: invite.teamId,
|
||||
});
|
||||
|
||||
await sendInviteMemberEmail(invite.id, email, currentUserName, name, isOnboardingInvite, inviteMessage);
|
||||
return invite;
|
||||
};
|
||||
|
||||
@@ -24,34 +24,43 @@ export const getMembersByTeamId = async (teamId: string, page?: number): Promise
|
||||
async () => {
|
||||
validateInputs([teamId, ZString], [page, ZOptionalNumber]);
|
||||
|
||||
const membersData = await prisma.membership.findMany({
|
||||
where: { teamId },
|
||||
select: {
|
||||
user: {
|
||||
select: {
|
||||
name: true,
|
||||
email: true,
|
||||
try {
|
||||
const membersData = await prisma.membership.findMany({
|
||||
where: { teamId },
|
||||
select: {
|
||||
user: {
|
||||
select: {
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
userId: true,
|
||||
accepted: true,
|
||||
role: true,
|
||||
},
|
||||
userId: true,
|
||||
accepted: true,
|
||||
role: true,
|
||||
},
|
||||
take: page ? ITEMS_PER_PAGE : undefined,
|
||||
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
|
||||
});
|
||||
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,
|
||||
};
|
||||
});
|
||||
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;
|
||||
return members;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
console.error(error);
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw new UnknownError("Error while fetching members");
|
||||
}
|
||||
},
|
||||
[`getMembersByTeamId-${teamId}-${page}`],
|
||||
{
|
||||
@@ -68,18 +77,27 @@ export const getMembershipByUserIdTeamId = async (
|
||||
async () => {
|
||||
validateInputs([userId, ZString], [teamId, ZString]);
|
||||
|
||||
const membership = await prisma.membership.findUnique({
|
||||
where: {
|
||||
userId_teamId: {
|
||||
userId,
|
||||
teamId,
|
||||
try {
|
||||
const membership = await prisma.membership.findUnique({
|
||||
where: {
|
||||
userId_teamId: {
|
||||
userId,
|
||||
teamId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
if (!membership) return null;
|
||||
if (!membership) return null;
|
||||
|
||||
return membership;
|
||||
return membership;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
console.error(error);
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw new UnknownError("Error while fetching membership");
|
||||
}
|
||||
},
|
||||
[`getMembershipByUserIdTeamId-${userId}-${teamId}`],
|
||||
{
|
||||
@@ -93,15 +111,23 @@ export const getMembershipsByUserId = async (userId: string, page?: number): Pro
|
||||
async () => {
|
||||
validateInputs([userId, ZString], [page, ZOptionalNumber]);
|
||||
|
||||
const memberships = await prisma.membership.findMany({
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
take: page ? ITEMS_PER_PAGE : undefined,
|
||||
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
|
||||
});
|
||||
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;
|
||||
return memberships;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`getMembershipsByUserId-${userId}-${page}`],
|
||||
{
|
||||
@@ -137,6 +163,10 @@ export const createMembership = async (
|
||||
|
||||
return membership;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -181,25 +211,33 @@ export const updateMembership = async (
|
||||
export const deleteMembership = async (userId: string, teamId: string): Promise<TMembership> => {
|
||||
validateInputs([userId, ZString], [teamId, ZString]);
|
||||
|
||||
const deletedMembership = await prisma.membership.delete({
|
||||
where: {
|
||||
userId_teamId: {
|
||||
teamId,
|
||||
userId,
|
||||
try {
|
||||
const deletedMembership = await prisma.membership.delete({
|
||||
where: {
|
||||
userId_teamId: {
|
||||
teamId,
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
teamCache.revalidate({
|
||||
userId,
|
||||
});
|
||||
teamCache.revalidate({
|
||||
userId,
|
||||
});
|
||||
|
||||
membershipCache.revalidate({
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
membershipCache.revalidate({
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
return deletedMembership;
|
||||
return deletedMembership;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const transferOwnership = async (
|
||||
|
||||
@@ -16,13 +16,17 @@ export const canUserAccessPerson = async (userId: string, personId: string): Pro
|
||||
validateInputs([userId, ZId], [personId, ZId]);
|
||||
if (!userId) return false;
|
||||
|
||||
const person = await getPerson(personId);
|
||||
if (!person) return false;
|
||||
try {
|
||||
const person = await getPerson(personId);
|
||||
if (!person) return false;
|
||||
|
||||
const hasAccessToEnvironment = await hasUserEnvironmentAccess(userId, person.environmentId);
|
||||
if (!hasAccessToEnvironment) return false;
|
||||
const hasAccessToEnvironment = await hasUserEnvironmentAccess(userId, person.environmentId);
|
||||
if (!hasAccessToEnvironment) return false;
|
||||
|
||||
return true;
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`canUserAccessPerson-${userId}-people-${personId}`],
|
||||
{
|
||||
|
||||
@@ -312,20 +312,28 @@ export const getPersonByUserId = async (environmentId: string, userId: string):
|
||||
async () => {
|
||||
validateInputs([environmentId, ZId], [userId, ZString]);
|
||||
|
||||
// check if userId exists as a column
|
||||
const personWithUserId = await prisma.person.findFirst({
|
||||
where: {
|
||||
environmentId,
|
||||
userId,
|
||||
},
|
||||
select: selectPerson,
|
||||
});
|
||||
try {
|
||||
// check if userId exists as a column
|
||||
const personWithUserId = await prisma.person.findFirst({
|
||||
where: {
|
||||
environmentId,
|
||||
userId,
|
||||
},
|
||||
select: selectPerson,
|
||||
});
|
||||
|
||||
if (personWithUserId) {
|
||||
return transformPrismaPerson(personWithUserId);
|
||||
if (personWithUserId) {
|
||||
return transformPrismaPerson(personWithUserId);
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
[`getPersonByUserId-${environmentId}-${userId}`],
|
||||
{
|
||||
@@ -346,58 +354,74 @@ export const updatePersonAttribute = async (
|
||||
): Promise<Partial<TPerson>> => {
|
||||
validateInputs([personId, ZId], [attributeClassId, ZId], [value, ZString]);
|
||||
|
||||
const attributes = await prisma.attribute.upsert({
|
||||
where: {
|
||||
personId_attributeClassId: {
|
||||
attributeClassId,
|
||||
personId,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
value,
|
||||
},
|
||||
create: {
|
||||
attributeClass: {
|
||||
connect: {
|
||||
id: attributeClassId,
|
||||
try {
|
||||
const attributes = await prisma.attribute.upsert({
|
||||
where: {
|
||||
personId_attributeClassId: {
|
||||
attributeClassId,
|
||||
personId,
|
||||
},
|
||||
},
|
||||
person: {
|
||||
connect: {
|
||||
id: personId,
|
||||
},
|
||||
update: {
|
||||
value,
|
||||
},
|
||||
value,
|
||||
},
|
||||
});
|
||||
create: {
|
||||
attributeClass: {
|
||||
connect: {
|
||||
id: attributeClassId,
|
||||
},
|
||||
},
|
||||
person: {
|
||||
connect: {
|
||||
id: personId,
|
||||
},
|
||||
},
|
||||
value,
|
||||
},
|
||||
});
|
||||
|
||||
personCache.revalidate({
|
||||
id: personId,
|
||||
});
|
||||
personCache.revalidate({
|
||||
id: personId,
|
||||
});
|
||||
|
||||
return attributes;
|
||||
return attributes;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getIsPersonMonthlyActive = async (personId: string): Promise<boolean> =>
|
||||
unstable_cache(
|
||||
async () => {
|
||||
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;
|
||||
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;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
[`isPersonActive-${personId}`],
|
||||
[`getIsPersonMonthlyActive-${personId}`],
|
||||
{
|
||||
tags: [activePersonCache.tag.byId(personId)],
|
||||
revalidate: 60 * 60 * 24, // 24 hours
|
||||
|
||||
@@ -17,11 +17,15 @@ export const canUserAccessProduct = async (userId: string, productId: string): P
|
||||
|
||||
if (!userId || !productId) return false;
|
||||
|
||||
const product = await getProduct(productId);
|
||||
if (!product) return false;
|
||||
try {
|
||||
const product = await getProduct(productId);
|
||||
if (!product) return false;
|
||||
|
||||
const teamIds = (await getTeamsByUserId(userId)).map((team) => team.id);
|
||||
return teamIds.includes(product.teamId);
|
||||
const teamIds = (await getTeamsByUserId(userId)).map((team) => team.id);
|
||||
return teamIds.includes(product.teamId);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`canUserAccessProduct-${userId}-${productId}`],
|
||||
{
|
||||
|
||||
@@ -130,6 +130,7 @@ export const updateProduct = async (
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -186,61 +187,68 @@ export const getProduct = async (productId: string): Promise<TProduct | null> =>
|
||||
};
|
||||
|
||||
export const deleteProduct = async (productId: string): Promise<TProduct> => {
|
||||
const product = await prisma.product.delete({
|
||||
where: {
|
||||
id: productId,
|
||||
},
|
||||
select: selectProduct,
|
||||
});
|
||||
try {
|
||||
const product = await prisma.product.delete({
|
||||
where: {
|
||||
id: productId,
|
||||
},
|
||||
select: selectProduct,
|
||||
});
|
||||
|
||||
if (product) {
|
||||
// delete all files from storage related to this product
|
||||
if (product) {
|
||||
// delete all files from storage related to this product
|
||||
|
||||
if (isS3Configured()) {
|
||||
const s3FilesPromises = product.environments.map(async (environment) => {
|
||||
return deleteS3FilesByEnvironmentId(environment.id);
|
||||
if (isS3Configured()) {
|
||||
const s3FilesPromises = product.environments.map(async (environment) => {
|
||||
return deleteS3FilesByEnvironmentId(environment.id);
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.all(s3FilesPromises);
|
||||
} catch (err) {
|
||||
// fail silently because we don't want to throw an error if the files are not deleted
|
||||
console.error(err);
|
||||
}
|
||||
} else {
|
||||
const localFilesPromises = product.environments.map(async (environment) => {
|
||||
return deleteLocalFilesByEnvironmentId(environment.id);
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.all(localFilesPromises);
|
||||
} catch (err) {
|
||||
// fail silently because we don't want to throw an error if the files are not deleted
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
productCache.revalidate({
|
||||
id: product.id,
|
||||
teamId: product.teamId,
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.all(s3FilesPromises);
|
||||
} catch (err) {
|
||||
// fail silently because we don't want to throw an error if the files are not deleted
|
||||
console.error(err);
|
||||
}
|
||||
} else {
|
||||
const localFilesPromises = product.environments.map(async (environment) => {
|
||||
return deleteLocalFilesByEnvironmentId(environment.id);
|
||||
environmentCache.revalidate({
|
||||
productId: product.id,
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.all(localFilesPromises);
|
||||
} catch (err) {
|
||||
// fail silently because we don't want to throw an error if the files are not deleted
|
||||
console.error(err);
|
||||
}
|
||||
product.environments.forEach((environment) => {
|
||||
// revalidate product cache
|
||||
productCache.revalidate({
|
||||
environmentId: environment.id,
|
||||
});
|
||||
environmentCache.revalidate({
|
||||
id: environment.id,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
productCache.revalidate({
|
||||
id: product.id,
|
||||
teamId: product.teamId,
|
||||
});
|
||||
|
||||
environmentCache.revalidate({
|
||||
productId: product.id,
|
||||
});
|
||||
|
||||
product.environments.forEach((environment) => {
|
||||
// revalidate product cache
|
||||
productCache.revalidate({
|
||||
environmentId: environment.id,
|
||||
});
|
||||
environmentCache.revalidate({
|
||||
id: environment.id,
|
||||
});
|
||||
});
|
||||
return product;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return product;
|
||||
};
|
||||
|
||||
export const createProduct = async (
|
||||
@@ -255,24 +263,31 @@ export const createProduct = async (
|
||||
|
||||
const { environments, ...data } = productInput;
|
||||
|
||||
let product = await prisma.product.create({
|
||||
data: {
|
||||
...data,
|
||||
name: productInput.name,
|
||||
teamId,
|
||||
},
|
||||
select: selectProduct,
|
||||
});
|
||||
try {
|
||||
let product = await prisma.product.create({
|
||||
data: {
|
||||
...data,
|
||||
name: productInput.name,
|
||||
teamId,
|
||||
},
|
||||
select: selectProduct,
|
||||
});
|
||||
|
||||
const devEnvironment = await createEnvironment(product.id, {
|
||||
type: "development",
|
||||
});
|
||||
const devEnvironment = await createEnvironment(product.id, {
|
||||
type: "development",
|
||||
});
|
||||
|
||||
const prodEnvironment = await createEnvironment(product.id, {
|
||||
type: "production",
|
||||
});
|
||||
const prodEnvironment = await createEnvironment(product.id, {
|
||||
type: "production",
|
||||
});
|
||||
|
||||
return await updateProduct(product.id, {
|
||||
environments: [devEnvironment, prodEnvironment],
|
||||
});
|
||||
return await updateProduct(product.id, {
|
||||
environments: [devEnvironment, prodEnvironment],
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -18,16 +18,20 @@ export const canUserAccessResponse = async (userId: string, responseId: string):
|
||||
|
||||
if (!userId) return false;
|
||||
|
||||
const response = await getResponse(responseId);
|
||||
if (!response) return false;
|
||||
try {
|
||||
const response = await getResponse(responseId);
|
||||
if (!response) return false;
|
||||
|
||||
const survey = await getSurvey(response.surveyId);
|
||||
if (!survey) return false;
|
||||
const survey = await getSurvey(response.surveyId);
|
||||
if (!survey) return false;
|
||||
|
||||
const hasAccessToEnvironment = await hasUserEnvironmentAccess(userId, survey.environmentId);
|
||||
if (!hasAccessToEnvironment) return false;
|
||||
const hasAccessToEnvironment = await hasUserEnvironmentAccess(userId, survey.environmentId);
|
||||
if (!hasAccessToEnvironment) return false;
|
||||
|
||||
return true;
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`canUserAccessResponse-${userId}-${responseId}`],
|
||||
{ revalidate: SERVICES_REVALIDATION_INTERVAL, tags: [responseCache.tag.byId(responseId)] }
|
||||
|
||||
@@ -448,7 +448,7 @@ export const getResponsePersonAttributes = async (surveyId: string): Promise<TSu
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`getAttributesFromResponses-${surveyId}`],
|
||||
[`getResponsePersonAttributes-${surveyId}`],
|
||||
{
|
||||
tags: [responseCache.tag.bySurveyId(surveyId)],
|
||||
revalidate: SERVICES_REVALIDATION_INTERVAL,
|
||||
@@ -592,35 +592,43 @@ export const getSurveySummary = (
|
||||
async () => {
|
||||
validateInputs([surveyId, ZId], [filterCriteria, ZResponseFilterCriteria.optional()]);
|
||||
|
||||
const survey = await getSurvey(surveyId);
|
||||
try {
|
||||
const survey = await getSurvey(surveyId);
|
||||
|
||||
if (!survey) {
|
||||
throw new ResourceNotFoundError("Survey", 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, i + 1, batchSize, filterCriteria);
|
||||
})
|
||||
);
|
||||
const responses = responsesArray.flat();
|
||||
|
||||
const displayCount = await getDisplayCountBySurveyId(surveyId, {
|
||||
createdAt: filterCriteria?.createdAt,
|
||||
});
|
||||
|
||||
const meta = getSurveySummaryMeta(responses, displayCount);
|
||||
const dropOff = getSurveySummaryDropOff(survey, responses, displayCount);
|
||||
const questionWiseSummary = getQuestionWiseSummary(
|
||||
checkForRecallInHeadline(survey, "default"),
|
||||
responses
|
||||
);
|
||||
|
||||
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, i + 1, batchSize, filterCriteria);
|
||||
})
|
||||
);
|
||||
const responses = responsesArray.flat();
|
||||
|
||||
const displayCount = await getDisplayCountBySurveyId(surveyId, {
|
||||
createdAt: filterCriteria?.createdAt,
|
||||
});
|
||||
|
||||
const meta = getSurveySummaryMeta(responses, displayCount);
|
||||
const dropOff = getSurveySummaryDropOff(survey, responses, displayCount);
|
||||
const questionWiseSummary = getQuestionWiseSummary(
|
||||
checkForRecallInHeadline(survey, "default"),
|
||||
responses
|
||||
);
|
||||
|
||||
return { meta, dropOff, summary: questionWiseSummary };
|
||||
},
|
||||
[`getSurveySummary-${surveyId}-${JSON.stringify(filterCriteria)}`],
|
||||
{
|
||||
@@ -637,8 +645,8 @@ export const getResponseDownloadUrl = async (
|
||||
format: "csv" | "xlsx",
|
||||
filterCriteria?: TResponseFilterCriteria
|
||||
): Promise<string> => {
|
||||
validateInputs([surveyId, ZId], [format, ZString], [filterCriteria, ZResponseFilterCriteria.optional()]);
|
||||
try {
|
||||
validateInputs([surveyId, ZId], [format, ZString], [filterCriteria, ZResponseFilterCriteria.optional()]);
|
||||
const survey = await getSurvey(surveyId);
|
||||
|
||||
if (!survey) {
|
||||
@@ -747,7 +755,7 @@ export const getResponsesByEnvironmentId = async (
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`getResponsesByEnvironmentId-${environmentId}`],
|
||||
[`getResponsesByEnvironmentId-${environmentId}-${page}`],
|
||||
{
|
||||
tags: [responseCache.tag.byEnvironmentId(environmentId)],
|
||||
revalidate: SERVICES_REVALIDATION_INTERVAL,
|
||||
@@ -891,6 +899,10 @@ export const getResponseCountBySurveyId = async (
|
||||
});
|
||||
return responseCount;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -14,12 +14,16 @@ export const canUserModifyResponseNote = async (userId: string, responseNoteId:
|
||||
|
||||
if (!userId || !responseNoteId) return false;
|
||||
|
||||
const responseNote = await getResponseNote(responseNoteId);
|
||||
if (!responseNote) return false;
|
||||
try {
|
||||
const responseNote = await getResponseNote(responseNoteId);
|
||||
if (!responseNote) return false;
|
||||
|
||||
return responseNote.user.id === userId;
|
||||
return responseNote.user.id === userId;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`users-${userId}-responseNotes-${responseNoteId}`],
|
||||
[`canUserModifyResponseNote-${userId}-${responseNoteId}`],
|
||||
{ revalidate: 30 * 60, tags: [`responseNotes-${responseNoteId}`] }
|
||||
)(); // 30 minutes
|
||||
|
||||
@@ -34,22 +38,26 @@ export const canUserResolveResponseNote = async (
|
||||
|
||||
if (!userId || !responseId || !responseNoteId) return false;
|
||||
|
||||
const response = await getResponse(responseId);
|
||||
try {
|
||||
const response = await getResponse(responseId);
|
||||
|
||||
let noteExistsOnResponse = false;
|
||||
let noteExistsOnResponse = false;
|
||||
|
||||
response?.notes.forEach((note) => {
|
||||
if (note.id === responseNoteId) {
|
||||
noteExistsOnResponse = true;
|
||||
}
|
||||
});
|
||||
response?.notes.forEach((note) => {
|
||||
if (note.id === responseNoteId) {
|
||||
noteExistsOnResponse = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (!noteExistsOnResponse) return false;
|
||||
if (!noteExistsOnResponse) return false;
|
||||
|
||||
const canAccessResponse = await canUserAccessResponse(userId, responseId);
|
||||
const canAccessResponse = await canUserAccessResponse(userId, responseId);
|
||||
|
||||
return canAccessResponse;
|
||||
return canAccessResponse;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`users-${userId}-responseNotes-${responseNoteId}`],
|
||||
[`canUserResolveResponseNote-${userId}-${responseNoteId}`],
|
||||
{ revalidate: 30 * 60, tags: [`responseNotes-${responseNoteId}`] }
|
||||
)(); // 30 minutes
|
||||
|
||||
@@ -519,13 +519,17 @@ const evaluateActionFilter = async (
|
||||
return false;
|
||||
}
|
||||
|
||||
// we have the action metric and we'll need to find out the values for those metrics from the db
|
||||
const actionValue = await getResolvedActionValue(actionClassId, personId, metric);
|
||||
try {
|
||||
// we have the action metric and we'll need to find out the values for those metrics from the db
|
||||
const actionValue = await getResolvedActionValue(actionClassId, personId, metric);
|
||||
|
||||
const actionResult =
|
||||
actionValue !== undefined && compareValues(actionValue ?? 0, value, qualifier.operator);
|
||||
const actionResult =
|
||||
actionValue !== undefined && compareValues(actionValue ?? 0, value, qualifier.operator);
|
||||
|
||||
return actionResult;
|
||||
return actionResult;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const evaluateSegmentFilter = async (
|
||||
@@ -611,88 +615,92 @@ export const evaluateSegment = async (
|
||||
): Promise<boolean> => {
|
||||
let resultPairs: ResultConnectorPair[] = [];
|
||||
|
||||
for (let filterItem of filters) {
|
||||
const { resource } = filterItem;
|
||||
try {
|
||||
for (let filterItem of filters) {
|
||||
const { resource } = filterItem;
|
||||
|
||||
let result: boolean;
|
||||
let result: boolean;
|
||||
|
||||
if (isResourceFilter(resource)) {
|
||||
const { root } = resource;
|
||||
const { type } = root;
|
||||
if (isResourceFilter(resource)) {
|
||||
const { root } = resource;
|
||||
const { type } = root;
|
||||
|
||||
if (type === "attribute") {
|
||||
result = evaluateAttributeFilter(userData.attributes, resource as TSegmentAttributeFilter);
|
||||
if (type === "attribute") {
|
||||
result = evaluateAttributeFilter(userData.attributes, resource as TSegmentAttributeFilter);
|
||||
resultPairs.push({
|
||||
result,
|
||||
connector: filterItem.connector,
|
||||
});
|
||||
}
|
||||
|
||||
if (type === "person") {
|
||||
result = evaluatePersonFilter(userData.userId, resource as TSegmentPersonFilter);
|
||||
resultPairs.push({
|
||||
result,
|
||||
connector: filterItem.connector,
|
||||
});
|
||||
}
|
||||
|
||||
if (type === "action") {
|
||||
result = await evaluateActionFilter(
|
||||
userData.actionIds,
|
||||
resource as TSegmentActionFilter,
|
||||
userData.personId
|
||||
);
|
||||
|
||||
resultPairs.push({
|
||||
result,
|
||||
connector: filterItem.connector,
|
||||
});
|
||||
}
|
||||
|
||||
if (type === "segment") {
|
||||
result = await evaluateSegmentFilter(userData, resource as TSegmentSegmentFilter);
|
||||
resultPairs.push({
|
||||
result,
|
||||
connector: filterItem.connector,
|
||||
});
|
||||
}
|
||||
|
||||
if (type === "device") {
|
||||
result = evaluateDeviceFilter(userData.deviceType, resource as TSegmentDeviceFilter);
|
||||
resultPairs.push({
|
||||
result,
|
||||
connector: filterItem.connector,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
result = await evaluateSegment(userData, resource);
|
||||
|
||||
// this is a sub-group and we need to evaluate the sub-group
|
||||
resultPairs.push({
|
||||
result,
|
||||
connector: filterItem.connector,
|
||||
});
|
||||
}
|
||||
|
||||
if (type === "person") {
|
||||
result = evaluatePersonFilter(userData.userId, resource as TSegmentPersonFilter);
|
||||
resultPairs.push({
|
||||
result,
|
||||
connector: filterItem.connector,
|
||||
});
|
||||
}
|
||||
|
||||
if (type === "action") {
|
||||
result = await evaluateActionFilter(
|
||||
userData.actionIds,
|
||||
resource as TSegmentActionFilter,
|
||||
userData.personId
|
||||
);
|
||||
|
||||
resultPairs.push({
|
||||
result,
|
||||
connector: filterItem.connector,
|
||||
});
|
||||
}
|
||||
|
||||
if (type === "segment") {
|
||||
result = await evaluateSegmentFilter(userData, resource as TSegmentSegmentFilter);
|
||||
resultPairs.push({
|
||||
result,
|
||||
connector: filterItem.connector,
|
||||
});
|
||||
}
|
||||
|
||||
if (type === "device") {
|
||||
result = evaluateDeviceFilter(userData.deviceType, resource as TSegmentDeviceFilter);
|
||||
resultPairs.push({
|
||||
result,
|
||||
connector: filterItem.connector,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
result = await evaluateSegment(userData, resource);
|
||||
|
||||
// this is a sub-group and we need to evaluate the sub-group
|
||||
resultPairs.push({
|
||||
result,
|
||||
connector: filterItem.connector,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!resultPairs.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Given that the first filter in every group/sub-group always has a connector value of "null",
|
||||
// we initialize the finalResult with the result of the first filter.
|
||||
|
||||
let finalResult = resultPairs[0].result;
|
||||
|
||||
for (let i = 1; i < resultPairs.length; i++) {
|
||||
const { result, connector } = resultPairs[i];
|
||||
|
||||
if (connector === "and") {
|
||||
finalResult = finalResult && result;
|
||||
} else if (connector === "or") {
|
||||
finalResult = finalResult || result;
|
||||
if (!resultPairs.length) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return finalResult;
|
||||
// Given that the first filter in every group/sub-group always has a connector value of "null",
|
||||
// we initialize the finalResult with the result of the first filter.
|
||||
|
||||
let finalResult = resultPairs[0].result;
|
||||
|
||||
for (let i = 1; i < resultPairs.length; i++) {
|
||||
const { result, connector } = resultPairs[i];
|
||||
|
||||
if (connector === "and") {
|
||||
finalResult = finalResult && result;
|
||||
} else if (connector === "or") {
|
||||
finalResult = finalResult || result;
|
||||
}
|
||||
}
|
||||
|
||||
return finalResult;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -18,13 +18,17 @@ export const canUserAccessSurvey = async (userId: string, surveyId: string): Pro
|
||||
|
||||
if (!userId) return false;
|
||||
|
||||
const survey = await getSurvey(surveyId);
|
||||
if (!survey) throw new Error("Survey not found");
|
||||
try {
|
||||
const survey = await getSurvey(surveyId);
|
||||
if (!survey) throw new Error("Survey not found");
|
||||
|
||||
const hasAccessToEnvironment = await hasUserEnvironmentAccess(userId, survey.environmentId);
|
||||
if (!hasAccessToEnvironment) return false;
|
||||
const hasAccessToEnvironment = await hasUserEnvironmentAccess(userId, survey.environmentId);
|
||||
if (!hasAccessToEnvironment) return false;
|
||||
|
||||
return true;
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`canUserAccessSurvey-${userId}-${surveyId}`],
|
||||
{ revalidate: SERVICES_REVALIDATION_INTERVAL, tags: [surveyCache.tag.byId(surveyId)] }
|
||||
|
||||
@@ -240,20 +240,30 @@ export const getSurveysByActionClassId = async (actionClassId: string, page?: nu
|
||||
async () => {
|
||||
validateInputs([actionClassId, ZId], [page, ZOptionalNumber]);
|
||||
|
||||
const 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,
|
||||
});
|
||||
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;
|
||||
}
|
||||
|
||||
const surveys: TSurvey[] = [];
|
||||
|
||||
@@ -395,105 +405,106 @@ export const getSurveyCount = async (environmentId: string): Promise<number> =>
|
||||
export const updateSurvey = async (updatedSurvey: TSurvey): Promise<TSurvey> => {
|
||||
validateInputs([updatedSurvey, ZSurveyWithRefinements]);
|
||||
|
||||
const surveyId = updatedSurvey.id;
|
||||
let data: any = {};
|
||||
|
||||
const actionClasses = await getActionClasses(updatedSurvey.environmentId);
|
||||
const currentSurvey = await getSurvey(surveyId);
|
||||
|
||||
if (!currentSurvey) {
|
||||
throw new ResourceNotFoundError("Survey", surveyId);
|
||||
}
|
||||
|
||||
const { triggers, environmentId, segment, languages, ...surveyData } = updatedSurvey;
|
||||
|
||||
if (languages) {
|
||||
// Process languages update logic here
|
||||
// Extract currentLanguageIds and updatedLanguageIds
|
||||
const currentLanguageIds = currentSurvey.languages
|
||||
? currentSurvey.languages.map((l) => l.language.id)
|
||||
: [];
|
||||
const updatedLanguageIds = languages.length > 1 ? updatedSurvey.languages.map((l) => l.language.id) : [];
|
||||
const enabledLangaugeIds = languages.map((language) => {
|
||||
if (language.enabled) return language.language.id;
|
||||
});
|
||||
|
||||
// Determine languages to add and remove
|
||||
const languagesToAdd = updatedLanguageIds.filter((id) => !currentLanguageIds.includes(id));
|
||||
const languagesToRemove = currentLanguageIds.filter((id) => !updatedLanguageIds.includes(id));
|
||||
|
||||
const defaultLanguageId = updatedSurvey.languages.find((l) => l.default)?.language.id;
|
||||
|
||||
// Prepare data for Prisma update
|
||||
data.languages = {};
|
||||
|
||||
// Update existing languages for default value changes
|
||||
data.languages.updateMany = currentSurvey.languages.map((surveyLanguage) => ({
|
||||
where: { languageId: surveyLanguage.language.id },
|
||||
data: {
|
||||
default: surveyLanguage.language.id === defaultLanguageId,
|
||||
enabled: enabledLangaugeIds.includes(surveyLanguage.language.id),
|
||||
},
|
||||
}));
|
||||
|
||||
// Add new languages
|
||||
if (languagesToAdd.length > 0) {
|
||||
data.languages.create = languagesToAdd.map((languageId) => ({
|
||||
languageId: languageId,
|
||||
default: languageId === defaultLanguageId,
|
||||
enabled: enabledLangaugeIds.includes(languageId),
|
||||
}));
|
||||
}
|
||||
|
||||
// Remove languages no longer associated with the survey
|
||||
if (languagesToRemove.length > 0) {
|
||||
data.languages.deleteMany = languagesToRemove.map((languageId) => ({
|
||||
languageId: languageId,
|
||||
enabled: enabledLangaugeIds.includes(languageId),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if (triggers) {
|
||||
data.triggers = processTriggerUpdates(triggers, currentSurvey.triggers, actionClasses);
|
||||
}
|
||||
|
||||
if (segment) {
|
||||
// parse the segment filters:
|
||||
const parsedFilters = ZSegmentFilters.safeParse(segment.filters);
|
||||
if (!parsedFilters.success) {
|
||||
throw new InvalidInputError("Invalid user segment filters");
|
||||
}
|
||||
|
||||
try {
|
||||
await updateSegment(segment.id, segment);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw new Error("Error updating survey");
|
||||
}
|
||||
}
|
||||
|
||||
surveyData.updatedAt = new Date();
|
||||
|
||||
data = {
|
||||
...surveyData,
|
||||
...data,
|
||||
};
|
||||
|
||||
// Remove scheduled status when runOnDate is not set
|
||||
if (data.status === "scheduled" && data.runOnDate === null) {
|
||||
data.status = "inProgress";
|
||||
}
|
||||
// Set scheduled status when runOnDate is set and in the future on completed surveys
|
||||
if (
|
||||
(data.status === "completed" || data.status === "paused" || data.status === "inProgress") &&
|
||||
data.runOnDate &&
|
||||
data.runOnDate > new Date()
|
||||
) {
|
||||
data.status = "scheduled";
|
||||
}
|
||||
|
||||
try {
|
||||
const surveyId = updatedSurvey.id;
|
||||
let data: any = {};
|
||||
|
||||
const actionClasses = await getActionClasses(updatedSurvey.environmentId);
|
||||
const currentSurvey = await getSurvey(surveyId);
|
||||
|
||||
if (!currentSurvey) {
|
||||
throw new ResourceNotFoundError("Survey", surveyId);
|
||||
}
|
||||
|
||||
const { triggers, environmentId, segment, languages, ...surveyData } = updatedSurvey;
|
||||
|
||||
if (languages) {
|
||||
// Process languages update logic here
|
||||
// Extract currentLanguageIds and updatedLanguageIds
|
||||
const currentLanguageIds = currentSurvey.languages
|
||||
? currentSurvey.languages.map((l) => l.language.id)
|
||||
: [];
|
||||
const updatedLanguageIds =
|
||||
languages.length > 1 ? updatedSurvey.languages.map((l) => l.language.id) : [];
|
||||
const enabledLangaugeIds = languages.map((language) => {
|
||||
if (language.enabled) return language.language.id;
|
||||
});
|
||||
|
||||
// Determine languages to add and remove
|
||||
const languagesToAdd = updatedLanguageIds.filter((id) => !currentLanguageIds.includes(id));
|
||||
const languagesToRemove = currentLanguageIds.filter((id) => !updatedLanguageIds.includes(id));
|
||||
|
||||
const defaultLanguageId = updatedSurvey.languages.find((l) => l.default)?.language.id;
|
||||
|
||||
// Prepare data for Prisma update
|
||||
data.languages = {};
|
||||
|
||||
// Update existing languages for default value changes
|
||||
data.languages.updateMany = currentSurvey.languages.map((surveyLanguage) => ({
|
||||
where: { languageId: surveyLanguage.language.id },
|
||||
data: {
|
||||
default: surveyLanguage.language.id === defaultLanguageId,
|
||||
enabled: enabledLangaugeIds.includes(surveyLanguage.language.id),
|
||||
},
|
||||
}));
|
||||
|
||||
// Add new languages
|
||||
if (languagesToAdd.length > 0) {
|
||||
data.languages.create = languagesToAdd.map((languageId) => ({
|
||||
languageId: languageId,
|
||||
default: languageId === defaultLanguageId,
|
||||
enabled: enabledLangaugeIds.includes(languageId),
|
||||
}));
|
||||
}
|
||||
|
||||
// Remove languages no longer associated with the survey
|
||||
if (languagesToRemove.length > 0) {
|
||||
data.languages.deleteMany = languagesToRemove.map((languageId) => ({
|
||||
languageId: languageId,
|
||||
enabled: enabledLangaugeIds.includes(languageId),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if (triggers) {
|
||||
data.triggers = processTriggerUpdates(triggers, currentSurvey.triggers, actionClasses);
|
||||
}
|
||||
|
||||
if (segment) {
|
||||
// parse the segment filters:
|
||||
const parsedFilters = ZSegmentFilters.safeParse(segment.filters);
|
||||
if (!parsedFilters.success) {
|
||||
throw new InvalidInputError("Invalid user segment filters");
|
||||
}
|
||||
|
||||
try {
|
||||
await updateSegment(segment.id, segment);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw new Error("Error updating survey");
|
||||
}
|
||||
}
|
||||
|
||||
surveyData.updatedAt = new Date();
|
||||
|
||||
data = {
|
||||
...surveyData,
|
||||
...data,
|
||||
};
|
||||
|
||||
// Remove scheduled status when runOnDate is not set
|
||||
if (data.status === "scheduled" && data.runOnDate === null) {
|
||||
data.status = "inProgress";
|
||||
}
|
||||
// Set scheduled status when runOnDate is set and in the future on completed surveys
|
||||
if (
|
||||
(data.status === "completed" || data.status === "paused" || data.status === "inProgress") &&
|
||||
data.runOnDate &&
|
||||
data.runOnDate > new Date()
|
||||
) {
|
||||
data.status = "scheduled";
|
||||
}
|
||||
|
||||
const prismaSurvey = await prisma.survey.update({
|
||||
where: { id: surveyId },
|
||||
data,
|
||||
@@ -533,223 +544,253 @@ export const updateSurvey = async (updatedSurvey: TSurvey): Promise<TSurvey> =>
|
||||
export async function deleteSurvey(surveyId: string) {
|
||||
validateInputs([surveyId, ZId]);
|
||||
|
||||
const deletedSurvey = await prisma.survey.delete({
|
||||
where: {
|
||||
id: surveyId,
|
||||
},
|
||||
select: selectSurvey,
|
||||
});
|
||||
try {
|
||||
const deletedSurvey = await prisma.survey.delete({
|
||||
where: {
|
||||
id: surveyId,
|
||||
},
|
||||
select: selectSurvey,
|
||||
});
|
||||
|
||||
responseCache.revalidate({
|
||||
surveyId,
|
||||
environmentId: deletedSurvey.environmentId,
|
||||
});
|
||||
surveyCache.revalidate({
|
||||
id: deletedSurvey.id,
|
||||
environmentId: deletedSurvey.environmentId,
|
||||
});
|
||||
|
||||
if (deletedSurvey.segment?.id) {
|
||||
segmentCache.revalidate({
|
||||
id: deletedSurvey.segment.id,
|
||||
responseCache.revalidate({
|
||||
surveyId,
|
||||
environmentId: deletedSurvey.environmentId,
|
||||
});
|
||||
}
|
||||
|
||||
// Revalidate triggers by actionClassId
|
||||
deletedSurvey.triggers.forEach((trigger) => {
|
||||
surveyCache.revalidate({
|
||||
actionClassId: trigger.actionClass.id,
|
||||
id: deletedSurvey.id,
|
||||
environmentId: deletedSurvey.environmentId,
|
||||
});
|
||||
});
|
||||
|
||||
return deletedSurvey;
|
||||
if (deletedSurvey.segment?.id) {
|
||||
segmentCache.revalidate({
|
||||
id: deletedSurvey.segment.id,
|
||||
environmentId: deletedSurvey.environmentId,
|
||||
});
|
||||
}
|
||||
|
||||
// Revalidate triggers by actionClassId
|
||||
deletedSurvey.triggers.forEach((trigger) => {
|
||||
surveyCache.revalidate({
|
||||
actionClassId: trigger.actionClass.id,
|
||||
});
|
||||
});
|
||||
|
||||
return deletedSurvey;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
console.error(error);
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export const createSurvey = async (environmentId: string, surveyBody: TSurveyInput): Promise<TSurvey> => {
|
||||
validateInputs([environmentId, ZId]);
|
||||
|
||||
// if the survey body has both triggers and inlineTriggers, we throw an error
|
||||
if (surveyBody.triggers && surveyBody.inlineTriggers) {
|
||||
throw new InvalidInputError("Survey body cannot have both triggers and inlineTriggers");
|
||||
}
|
||||
try {
|
||||
// if the survey body has both triggers and inlineTriggers, we throw an error
|
||||
if (surveyBody.triggers && surveyBody.inlineTriggers) {
|
||||
throw new InvalidInputError("Survey body cannot have both triggers and inlineTriggers");
|
||||
}
|
||||
|
||||
if (surveyBody.triggers) {
|
||||
const actionClasses = await getActionClasses(environmentId);
|
||||
revalidateSurveyByActionClassName(actionClasses, surveyBody.triggers);
|
||||
}
|
||||
const createdBy = surveyBody.createdBy;
|
||||
delete surveyBody.createdBy;
|
||||
if (surveyBody.triggers) {
|
||||
const actionClasses = await getActionClasses(environmentId);
|
||||
revalidateSurveyByActionClassName(actionClasses, surveyBody.triggers);
|
||||
}
|
||||
const createdBy = surveyBody.createdBy;
|
||||
delete surveyBody.createdBy;
|
||||
|
||||
const data: Omit<Prisma.SurveyCreateInput, "environment"> = {
|
||||
...surveyBody,
|
||||
// TODO: Create with attributeFilters
|
||||
triggers: surveyBody.triggers
|
||||
? processTriggerUpdates(surveyBody.triggers, [], await getActionClasses(environmentId))
|
||||
: undefined,
|
||||
attributeFilters: undefined,
|
||||
};
|
||||
|
||||
if (surveyBody.type === "web" && data.thankYouCard) {
|
||||
data.thankYouCard.buttonLabel = undefined;
|
||||
data.thankYouCard.buttonLink = undefined;
|
||||
}
|
||||
|
||||
if (createdBy) {
|
||||
data.creator = {
|
||||
connect: {
|
||||
id: createdBy,
|
||||
},
|
||||
const data: Omit<Prisma.SurveyCreateInput, "environment"> = {
|
||||
...surveyBody,
|
||||
// TODO: Create with attributeFilters
|
||||
triggers: surveyBody.triggers
|
||||
? processTriggerUpdates(surveyBody.triggers, [], await getActionClasses(environmentId))
|
||||
: undefined,
|
||||
attributeFilters: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const survey = await prisma.survey.create({
|
||||
data: {
|
||||
...data,
|
||||
environment: {
|
||||
if (surveyBody.type === "web" && data.thankYouCard) {
|
||||
data.thankYouCard.buttonLabel = undefined;
|
||||
data.thankYouCard.buttonLink = undefined;
|
||||
}
|
||||
|
||||
if (createdBy) {
|
||||
data.creator = {
|
||||
connect: {
|
||||
id: environmentId,
|
||||
id: createdBy,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const survey = await prisma.survey.create({
|
||||
data: {
|
||||
...data,
|
||||
environment: {
|
||||
connect: {
|
||||
id: environmentId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
select: selectSurvey,
|
||||
});
|
||||
select: selectSurvey,
|
||||
});
|
||||
|
||||
const transformedSurvey: TSurvey = {
|
||||
...survey,
|
||||
triggers: survey.triggers.map((trigger) => trigger.actionClass.name),
|
||||
segment: null,
|
||||
};
|
||||
const transformedSurvey: TSurvey = {
|
||||
...survey,
|
||||
triggers: survey.triggers.map((trigger) => trigger.actionClass.name),
|
||||
segment: null,
|
||||
};
|
||||
|
||||
await subscribeTeamMembersToSurveyResponses(environmentId, survey.id);
|
||||
await subscribeTeamMembersToSurveyResponses(environmentId, survey.id);
|
||||
|
||||
surveyCache.revalidate({
|
||||
id: survey.id,
|
||||
environmentId: survey.environmentId,
|
||||
});
|
||||
surveyCache.revalidate({
|
||||
id: survey.id,
|
||||
environmentId: survey.environmentId,
|
||||
});
|
||||
|
||||
return transformedSurvey;
|
||||
return transformedSurvey;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
console.error(error);
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const duplicateSurvey = async (environmentId: string, surveyId: string, userId: string) => {
|
||||
validateInputs([environmentId, ZId], [surveyId, ZId]);
|
||||
const existingSurvey = await getSurvey(surveyId);
|
||||
const currentDate = new Date();
|
||||
if (!existingSurvey) {
|
||||
throw new ResourceNotFoundError("Survey", surveyId);
|
||||
}
|
||||
|
||||
const defaultLanguageId = existingSurvey.languages.find((l) => l.default)?.language.id;
|
||||
|
||||
const actionClasses = await getActionClasses(environmentId);
|
||||
|
||||
// create new survey with the data of the existing survey
|
||||
const newSurvey = await prisma.survey.create({
|
||||
data: {
|
||||
...existingSurvey,
|
||||
id: undefined, // id is auto-generated
|
||||
environmentId: undefined, // environmentId is set below
|
||||
createdAt: currentDate,
|
||||
updatedAt: currentDate,
|
||||
createdBy: undefined,
|
||||
name: `${existingSurvey.name} (copy)`,
|
||||
status: "draft",
|
||||
questions: structuredClone(existingSurvey.questions),
|
||||
thankYouCard: structuredClone(existingSurvey.thankYouCard),
|
||||
languages: {
|
||||
create: existingSurvey.languages?.map((surveyLanguage) => ({
|
||||
languageId: surveyLanguage.language.id,
|
||||
default: surveyLanguage.language.id === defaultLanguageId,
|
||||
})),
|
||||
},
|
||||
triggers: {
|
||||
create: existingSurvey.triggers.map((trigger) => ({
|
||||
actionClassId: getActionClassIdFromName(actionClasses, trigger),
|
||||
})),
|
||||
},
|
||||
inlineTriggers: existingSurvey.inlineTriggers ?? undefined,
|
||||
environment: {
|
||||
connect: {
|
||||
id: environmentId,
|
||||
},
|
||||
},
|
||||
creator: {
|
||||
connect: {
|
||||
id: userId,
|
||||
},
|
||||
},
|
||||
surveyClosedMessage: existingSurvey.surveyClosedMessage
|
||||
? structuredClone(existingSurvey.surveyClosedMessage)
|
||||
: Prisma.JsonNull,
|
||||
singleUse: existingSurvey.singleUse ? structuredClone(existingSurvey.singleUse) : Prisma.JsonNull,
|
||||
productOverwrites: existingSurvey.productOverwrites
|
||||
? structuredClone(existingSurvey.productOverwrites)
|
||||
: Prisma.JsonNull,
|
||||
styling: existingSurvey.styling ? structuredClone(existingSurvey.styling) : Prisma.JsonNull,
|
||||
verifyEmail: existingSurvey.verifyEmail ? structuredClone(existingSurvey.verifyEmail) : Prisma.JsonNull,
|
||||
// we'll update the segment later
|
||||
segment: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
// if the existing survey has an inline segment, we copy the filters and create a new inline segment and connect it to the new survey
|
||||
if (existingSurvey.segment) {
|
||||
if (existingSurvey.segment.isPrivate) {
|
||||
const newInlineSegment = await createSegment({
|
||||
environmentId,
|
||||
title: `${newSurvey.id}`,
|
||||
isPrivate: true,
|
||||
surveyId: newSurvey.id,
|
||||
filters: existingSurvey.segment.filters,
|
||||
});
|
||||
|
||||
await prisma.survey.update({
|
||||
where: {
|
||||
id: newSurvey.id,
|
||||
},
|
||||
data: {
|
||||
segment: {
|
||||
connect: {
|
||||
id: newInlineSegment.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
segmentCache.revalidate({
|
||||
id: newInlineSegment.id,
|
||||
environmentId: newSurvey.environmentId,
|
||||
});
|
||||
} else {
|
||||
await prisma.survey.update({
|
||||
where: {
|
||||
id: newSurvey.id,
|
||||
},
|
||||
data: {
|
||||
segment: {
|
||||
connect: {
|
||||
id: existingSurvey.segment.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
segmentCache.revalidate({
|
||||
id: existingSurvey.segment.id,
|
||||
environmentId: newSurvey.environmentId,
|
||||
});
|
||||
try {
|
||||
const existingSurvey = await getSurvey(surveyId);
|
||||
const currentDate = new Date();
|
||||
if (!existingSurvey) {
|
||||
throw new ResourceNotFoundError("Survey", surveyId);
|
||||
}
|
||||
|
||||
const defaultLanguageId = existingSurvey.languages.find((l) => l.default)?.language.id;
|
||||
|
||||
const actionClasses = await getActionClasses(environmentId);
|
||||
|
||||
// create new survey with the data of the existing survey
|
||||
const newSurvey = await prisma.survey.create({
|
||||
data: {
|
||||
...existingSurvey,
|
||||
id: undefined, // id is auto-generated
|
||||
environmentId: undefined, // environmentId is set below
|
||||
createdAt: currentDate,
|
||||
updatedAt: currentDate,
|
||||
createdBy: undefined,
|
||||
name: `${existingSurvey.name} (copy)`,
|
||||
status: "draft",
|
||||
questions: structuredClone(existingSurvey.questions),
|
||||
thankYouCard: structuredClone(existingSurvey.thankYouCard),
|
||||
languages: {
|
||||
create: existingSurvey.languages?.map((surveyLanguage) => ({
|
||||
languageId: surveyLanguage.language.id,
|
||||
default: surveyLanguage.language.id === defaultLanguageId,
|
||||
})),
|
||||
},
|
||||
triggers: {
|
||||
create: existingSurvey.triggers.map((trigger) => ({
|
||||
actionClassId: getActionClassIdFromName(actionClasses, trigger),
|
||||
})),
|
||||
},
|
||||
inlineTriggers: existingSurvey.inlineTriggers ?? undefined,
|
||||
environment: {
|
||||
connect: {
|
||||
id: environmentId,
|
||||
},
|
||||
},
|
||||
creator: {
|
||||
connect: {
|
||||
id: userId,
|
||||
},
|
||||
},
|
||||
surveyClosedMessage: existingSurvey.surveyClosedMessage
|
||||
? structuredClone(existingSurvey.surveyClosedMessage)
|
||||
: Prisma.JsonNull,
|
||||
singleUse: existingSurvey.singleUse ? structuredClone(existingSurvey.singleUse) : Prisma.JsonNull,
|
||||
productOverwrites: existingSurvey.productOverwrites
|
||||
? structuredClone(existingSurvey.productOverwrites)
|
||||
: Prisma.JsonNull,
|
||||
styling: existingSurvey.styling ? structuredClone(existingSurvey.styling) : Prisma.JsonNull,
|
||||
verifyEmail: existingSurvey.verifyEmail
|
||||
? structuredClone(existingSurvey.verifyEmail)
|
||||
: Prisma.JsonNull,
|
||||
// we'll update the segment later
|
||||
segment: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
// if the existing survey has an inline segment, we copy the filters and create a new inline segment and connect it to the new survey
|
||||
if (existingSurvey.segment) {
|
||||
if (existingSurvey.segment.isPrivate) {
|
||||
const newInlineSegment = await createSegment({
|
||||
environmentId,
|
||||
title: `${newSurvey.id}`,
|
||||
isPrivate: true,
|
||||
surveyId: newSurvey.id,
|
||||
filters: existingSurvey.segment.filters,
|
||||
});
|
||||
|
||||
await prisma.survey.update({
|
||||
where: {
|
||||
id: newSurvey.id,
|
||||
},
|
||||
data: {
|
||||
segment: {
|
||||
connect: {
|
||||
id: newInlineSegment.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
segmentCache.revalidate({
|
||||
id: newInlineSegment.id,
|
||||
environmentId: newSurvey.environmentId,
|
||||
});
|
||||
} else {
|
||||
await prisma.survey.update({
|
||||
where: {
|
||||
id: newSurvey.id,
|
||||
},
|
||||
data: {
|
||||
segment: {
|
||||
connect: {
|
||||
id: existingSurvey.segment.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
segmentCache.revalidate({
|
||||
id: existingSurvey.segment.id,
|
||||
environmentId: newSurvey.environmentId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
surveyCache.revalidate({
|
||||
id: newSurvey.id,
|
||||
environmentId: newSurvey.environmentId,
|
||||
});
|
||||
|
||||
// Revalidate surveys by actionClassId
|
||||
revalidateSurveyByActionClassName(actionClasses, existingSurvey.triggers);
|
||||
|
||||
return newSurvey;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
console.error(error);
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
surveyCache.revalidate({
|
||||
id: newSurvey.id,
|
||||
environmentId: newSurvey.environmentId,
|
||||
});
|
||||
|
||||
// Revalidate surveys by actionClassId
|
||||
revalidateSurveyByActionClassName(actionClasses, existingSurvey.triggers);
|
||||
|
||||
return newSurvey;
|
||||
};
|
||||
|
||||
export const getSyncSurveys = async (
|
||||
@@ -764,147 +805,156 @@ export const getSyncSurveys = async (
|
||||
|
||||
const surveys = await unstable_cache(
|
||||
async () => {
|
||||
const product = await getProductByEnvironmentId(environmentId);
|
||||
try {
|
||||
const product = await getProductByEnvironmentId(environmentId);
|
||||
|
||||
if (!product) {
|
||||
throw new Error("Product not found");
|
||||
}
|
||||
|
||||
const person = personId === "legacy" ? ({ id: "legacy" } as TPerson) : await getPerson(personId);
|
||||
|
||||
if (!person) {
|
||||
throw new Error("Person not found");
|
||||
}
|
||||
|
||||
let surveys: TSurvey[] | TLegacySurvey[] = await getSurveys(environmentId);
|
||||
|
||||
// filtered surveys for running and web
|
||||
surveys = surveys.filter((survey) => survey.status === "inProgress" && survey.type === "web");
|
||||
|
||||
// if no surveys are left, return an empty array
|
||||
if (surveys.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const displays = await getDisplaysByPersonId(person.id);
|
||||
|
||||
// filter surveys that meet the displayOption criteria
|
||||
surveys = surveys.filter((survey) => {
|
||||
if (survey.displayOption === "respondMultiple") {
|
||||
return true;
|
||||
} else if (survey.displayOption === "displayOnce") {
|
||||
return displays.filter((display) => display.surveyId === survey.id).length === 0;
|
||||
} else if (survey.displayOption === "displayMultiple") {
|
||||
return (
|
||||
displays.filter((display) => display.surveyId === survey.id && display.responseId !== null)
|
||||
.length === 0
|
||||
);
|
||||
} else {
|
||||
throw Error("Invalid displayOption");
|
||||
if (!product) {
|
||||
throw new Error("Product not found");
|
||||
}
|
||||
});
|
||||
|
||||
const latestDisplay = displays[0];
|
||||
const person = personId === "legacy" ? ({ id: "legacy" } as TPerson) : await getPerson(personId);
|
||||
|
||||
// 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) {
|
||||
if (!person) {
|
||||
throw new Error("Person not found");
|
||||
}
|
||||
|
||||
let surveys: TSurvey[] | TLegacySurvey[] = await getSurveys(environmentId);
|
||||
|
||||
// filtered surveys for running and web
|
||||
surveys = surveys.filter((survey) => survey.status === "inProgress" && survey.type === "web");
|
||||
|
||||
// if no surveys are left, return an empty array
|
||||
if (surveys.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const displays = await getDisplaysByPersonId(person.id);
|
||||
|
||||
// filter surveys that meet the displayOption criteria
|
||||
surveys = surveys.filter((survey) => {
|
||||
if (survey.displayOption === "respondMultiple") {
|
||||
return true;
|
||||
} else if (survey.displayOption === "displayOnce") {
|
||||
return displays.filter((display) => display.surveyId === survey.id).length === 0;
|
||||
} else if (survey.displayOption === "displayMultiple") {
|
||||
return (
|
||||
displays.filter((display) => display.surveyId === survey.id && display.responseId !== null)
|
||||
.length === 0
|
||||
);
|
||||
} else {
|
||||
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 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;
|
||||
}
|
||||
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 personUserId = person.userId ?? person.attributes?.userId ?? "";
|
||||
|
||||
// the surveys now have segment filters, so we need to evaluate them
|
||||
const surveyPromises = surveys.map(async (survey) => {
|
||||
const { segment } = survey;
|
||||
if (!segment) {
|
||||
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 ?? ""))
|
||||
);
|
||||
const personUserId = person.userId ?? person.attributes?.userId ?? "";
|
||||
|
||||
// if there are no attribute filters, we return the survey
|
||||
if (!attributeFilters.length) {
|
||||
// the surveys now have segment filters, so we need to evaluate them
|
||||
const surveyPromises = surveys.map(async (survey) => {
|
||||
const { segment } = survey;
|
||||
if (!segment) {
|
||||
return survey;
|
||||
}
|
||||
|
||||
// we check if the person meets the attribute filters for all the attribute filters
|
||||
const isEligible = attributeFilters.every((attributeFilter) => {
|
||||
const personAttributeValue = person?.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 = person?.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: person.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: person.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;
|
||||
},
|
||||
[`getSyncSurveys-${environmentId}-${personId}`],
|
||||
{
|
||||
@@ -948,9 +998,8 @@ export const getSurveyIdByResultShareKey = async (resultShareKey: string): Promi
|
||||
};
|
||||
|
||||
export const loadNewSegmentInSurvey = async (surveyId: string, newSegmentId: string): Promise<TSurvey> => {
|
||||
validateInputs([surveyId, ZId], [newSegmentId, ZId]);
|
||||
try {
|
||||
validateInputs([surveyId, ZId], [newSegmentId, ZId]);
|
||||
|
||||
const currentSurvey = await getSurvey(surveyId);
|
||||
if (!currentSurvey) {
|
||||
throw new ResourceNotFoundError("survey", surveyId);
|
||||
|
||||
@@ -17,15 +17,19 @@ export const canUserAccessTag = async (userId: string, tagId: string): Promise<b
|
||||
async () => {
|
||||
validateInputs([userId, ZId], [tagId, ZId]);
|
||||
|
||||
const tag = await getTag(tagId);
|
||||
if (!tag) return false;
|
||||
try {
|
||||
const tag = await getTag(tagId);
|
||||
if (!tag) return false;
|
||||
|
||||
const hasAccessToEnvironment = await hasUserEnvironmentAccess(userId, tag.environmentId);
|
||||
if (!hasAccessToEnvironment) return false;
|
||||
const hasAccessToEnvironment = await hasUserEnvironmentAccess(userId, tag.environmentId);
|
||||
if (!hasAccessToEnvironment) return false;
|
||||
|
||||
return true;
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`${userId}-${tagId}`],
|
||||
[`canUserAccessTag-${userId}-${tagId}`],
|
||||
{
|
||||
revalidate: SERVICES_REVALIDATION_INTERVAL,
|
||||
}
|
||||
|
||||
@@ -22,12 +22,16 @@ export const canUserAccessTagOnResponse = async (
|
||||
async () => {
|
||||
validateInputs([userId, ZId], [tagId, ZId], [responseId, ZId]);
|
||||
|
||||
const isAuthorizedForTag = await canUserAccessTag(userId, tagId);
|
||||
const isAuthorizedForResponse = await canUserAccessResponse(userId, responseId);
|
||||
try {
|
||||
const isAuthorizedForTag = await canUserAccessTag(userId, tagId);
|
||||
const isAuthorizedForResponse = await canUserAccessResponse(userId, responseId);
|
||||
|
||||
return isAuthorizedForTag && isAuthorizedForResponse;
|
||||
return isAuthorizedForTag && isAuthorizedForResponse;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`users-${userId}-tagOnResponse-${tagId}-${responseId}`],
|
||||
[`canUserAccessTagOnResponse-${userId}-${tagId}-${responseId}`],
|
||||
{
|
||||
revalidate: SERVICES_REVALIDATION_INTERVAL,
|
||||
tags: [tagOnResponseCache.tag.byResponseIdAndTagId(responseId, tagId)],
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import "server-only";
|
||||
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { unstable_cache } from "next/cache";
|
||||
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { ZId } from "@formbricks/types/environment";
|
||||
import { DatabaseError } from "@formbricks/types/errors";
|
||||
import { TTagsCount, TTagsOnResponses } from "@formbricks/types/tags";
|
||||
|
||||
import { SERVICES_REVALIDATION_INTERVAL } from "../constants";
|
||||
@@ -48,6 +50,10 @@ export const addTagToRespone = async (responseId: string, tagId: string): Promis
|
||||
tagId,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -82,6 +88,9 @@ export const deleteTagOnResponse = async (responseId: string, tagId: string): Pr
|
||||
responseId,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -110,6 +119,9 @@ export const getTagsOnResponsesCount = async (environmentId: string): Promise<TT
|
||||
|
||||
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;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -16,13 +16,17 @@ export const canUserAccessTeam = async (userId: string, teamId: string): Promise
|
||||
async () => {
|
||||
validateInputs([userId, ZId], [teamId, ZId]);
|
||||
|
||||
const userTeams = await getTeamsByUserId(userId);
|
||||
try {
|
||||
const userTeams = await getTeamsByUserId(userId);
|
||||
|
||||
const givenTeamExists = userTeams.filter((team) => (team.id = teamId));
|
||||
if (!givenTeamExists) {
|
||||
return false;
|
||||
const givenTeamExists = userTeams.filter((team) => (team.id = teamId));
|
||||
if (!givenTeamExists) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
[`canUserAccessTeam-${userId}-${teamId}`],
|
||||
{ revalidate: SERVICES_REVALIDATION_INTERVAL, tags: [teamCache.tag.byId(teamId)] }
|
||||
|
||||
@@ -161,6 +161,10 @@ export const createTeam = async (teamInput: TTeamCreateInput): Promise<TTeam> =>
|
||||
|
||||
return team;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -205,9 +209,8 @@ export const updateTeam = async (teamId: string, data: Partial<TTeamUpdateInput>
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2016") {
|
||||
throw new ResourceNotFoundError("Team", teamId);
|
||||
} else {
|
||||
throw error; // Re-throw any other errors
|
||||
}
|
||||
throw error; // Re-throw any other errors
|
||||
}
|
||||
};
|
||||
|
||||
@@ -308,34 +311,42 @@ export const getMonthlyActiveTeamPeopleCount = async (teamId: string): Promise<n
|
||||
async () => {
|
||||
validateInputs([teamId, ZId]);
|
||||
|
||||
// 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);
|
||||
|
||||
// Get all environment IDs for the team
|
||||
const products = await getProducts(teamId);
|
||||
const environmentIds = products.flatMap((product) => product.environments.map((env) => env.id));
|
||||
// Get all environment IDs for the team
|
||||
const products = await getProducts(teamId);
|
||||
const environmentIds = products.flatMap((product) => product.environments.map((env) => env.id));
|
||||
|
||||
// Aggregate the count of active people across all environments
|
||||
const peopleAggregations = await prisma.person.aggregate({
|
||||
_count: {
|
||||
id: true,
|
||||
},
|
||||
where: {
|
||||
AND: [
|
||||
{ environmentId: { in: environmentIds } },
|
||||
{
|
||||
actions: {
|
||||
some: {
|
||||
createdAt: { gte: firstDayOfMonth },
|
||||
// Aggregate the count of active people across all environments
|
||||
const peopleAggregations = await prisma.person.aggregate({
|
||||
_count: {
|
||||
id: true,
|
||||
},
|
||||
where: {
|
||||
AND: [
|
||||
{ environmentId: { in: environmentIds } },
|
||||
{
|
||||
actions: {
|
||||
some: {
|
||||
createdAt: { gte: firstDayOfMonth },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
return peopleAggregations._count.id;
|
||||
return peopleAggregations._count.id;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`getMonthlyActiveTeamPeopleCount-${teamId}`],
|
||||
{
|
||||
@@ -348,30 +359,38 @@ export const getMonthlyTeamResponseCount = async (teamId: string): Promise<numbe
|
||||
async () => {
|
||||
validateInputs([teamId, ZId]);
|
||||
|
||||
// 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);
|
||||
|
||||
// Get all environment IDs for the team
|
||||
const products = await getProducts(teamId);
|
||||
const environmentIds = products.flatMap((product) => product.environments.map((env) => env.id));
|
||||
// Get all environment IDs for the team
|
||||
const products = await getProducts(teamId);
|
||||
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 } } },
|
||||
{ survey: { type: "web" } },
|
||||
{ createdAt: { gte: firstDayOfMonth } },
|
||||
],
|
||||
},
|
||||
});
|
||||
// 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 } } },
|
||||
{ survey: { type: "web" } },
|
||||
{ createdAt: { gte: firstDayOfMonth } },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
// The result is an aggregation of the total count
|
||||
return responseAggregations._count.id;
|
||||
// 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;
|
||||
}
|
||||
},
|
||||
[`getMonthlyTeamResponseCount-${teamId}`],
|
||||
{
|
||||
@@ -382,13 +401,23 @@ export const getMonthlyTeamResponseCount = async (teamId: string): Promise<numbe
|
||||
export const getTeamBillingInfo = async (teamId: string): Promise<TTeamBilling | null> =>
|
||||
await unstable_cache(
|
||||
async () => {
|
||||
const billingInfo = await prisma.team.findUnique({
|
||||
where: {
|
||||
id: teamId,
|
||||
},
|
||||
});
|
||||
validateInputs([teamId, ZId]);
|
||||
|
||||
return billingInfo?.billing ?? null;
|
||||
try {
|
||||
const billingInfo = await prisma.team.findUnique({
|
||||
where: {
|
||||
id: teamId,
|
||||
},
|
||||
});
|
||||
|
||||
return billingInfo?.billing ?? null;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`getTeamBillingInfo-${teamId}`],
|
||||
{
|
||||
|
||||
@@ -137,47 +137,66 @@ export const updateUser = async (personId: string, data: TUserUpdateInput): Prom
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2016") {
|
||||
throw new ResourceNotFoundError("User", personId);
|
||||
} else {
|
||||
throw error; // Re-throw any other errors
|
||||
}
|
||||
throw error; // Re-throw any other errors
|
||||
}
|
||||
};
|
||||
|
||||
const deleteUserById = async (id: string): Promise<TUser> => {
|
||||
validateInputs([id, ZId]);
|
||||
|
||||
const user = await prisma.user.delete({
|
||||
where: {
|
||||
try {
|
||||
const user = await prisma.user.delete({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
select: responseSelection,
|
||||
});
|
||||
|
||||
userCache.revalidate({
|
||||
email: user.email,
|
||||
id,
|
||||
},
|
||||
select: responseSelection,
|
||||
});
|
||||
});
|
||||
|
||||
userCache.revalidate({
|
||||
email: user.email,
|
||||
id,
|
||||
});
|
||||
return user;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
return user;
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const createUser = async (data: TUserCreateInput): Promise<TUser> => {
|
||||
validateInputs([data, ZUserUpdateInput]);
|
||||
|
||||
const user = await prisma.user.create({
|
||||
data: data,
|
||||
select: responseSelection,
|
||||
});
|
||||
try {
|
||||
const user = await prisma.user.create({
|
||||
data: data,
|
||||
select: responseSelection,
|
||||
});
|
||||
|
||||
userCache.revalidate({
|
||||
email: user.email,
|
||||
id: user.id,
|
||||
});
|
||||
userCache.revalidate({
|
||||
email: user.email,
|
||||
id: user.id,
|
||||
});
|
||||
|
||||
// send new user customer.io to customer.io
|
||||
createCustomerIoCustomer(user);
|
||||
// send new user customer.io to customer.io
|
||||
createCustomerIoCustomer(user);
|
||||
|
||||
return user;
|
||||
return user;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") {
|
||||
throw new DatabaseError("User with this email already exists");
|
||||
}
|
||||
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// function to delete a user's user including teams
|
||||
@@ -236,34 +255,51 @@ export const deleteUser = async (id: string): Promise<TUser> => {
|
||||
export const getUsersWithTeam = async (teamId: string): Promise<TUser[]> => {
|
||||
validateInputs([teamId, ZId]);
|
||||
|
||||
const users = await prisma.user.findMany({
|
||||
where: {
|
||||
memberships: {
|
||||
some: {
|
||||
teamId,
|
||||
try {
|
||||
const users = await prisma.user.findMany({
|
||||
where: {
|
||||
memberships: {
|
||||
some: {
|
||||
teamId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
select: responseSelection,
|
||||
});
|
||||
select: responseSelection,
|
||||
});
|
||||
|
||||
return users;
|
||||
return users;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const userIdRelatedToApiKey = async (apiKey: string) => {
|
||||
const userId = await prisma.apiKey.findUnique({
|
||||
where: { id: apiKey },
|
||||
select: {
|
||||
environment: {
|
||||
select: {
|
||||
people: {
|
||||
select: {
|
||||
userId: true,
|
||||
validateInputs([apiKey, z.string()]);
|
||||
|
||||
try {
|
||||
const userId = await prisma.apiKey.findUnique({
|
||||
where: { id: apiKey },
|
||||
select: {
|
||||
environment: {
|
||||
select: {
|
||||
people: {
|
||||
select: {
|
||||
userId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
return userId;
|
||||
});
|
||||
return userId;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -13,13 +13,17 @@ export const canUserAccessWebhook = async (userId: string, webhookId: string): P
|
||||
async () => {
|
||||
validateInputs([userId, ZId], [webhookId, ZId]);
|
||||
|
||||
const webhook = await getWebhook(webhookId);
|
||||
if (!webhook) return false;
|
||||
try {
|
||||
const webhook = await getWebhook(webhookId);
|
||||
if (!webhook) return false;
|
||||
|
||||
const hasAccessToEnvironment = await hasUserEnvironmentAccess(userId, webhook.environmentId);
|
||||
if (!hasAccessToEnvironment) return false;
|
||||
const hasAccessToEnvironment = await hasUserEnvironmentAccess(userId, webhook.environmentId);
|
||||
if (!hasAccessToEnvironment) return false;
|
||||
|
||||
return true;
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`canUserAccessWebhook-${userId}-${webhookId}`],
|
||||
{
|
||||
|
||||
@@ -29,7 +29,11 @@ export const getWebhooks = async (environmentId: string, page?: number): Promise
|
||||
});
|
||||
return webhooks;
|
||||
} catch (error) {
|
||||
throw new DatabaseError(`Database error when fetching webhooks for environment ${environmentId}`);
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`getWebhooks-${environmentId}-${page}`],
|
||||
@@ -58,10 +62,14 @@ export const getWebhookCountBySource = async (
|
||||
});
|
||||
return count;
|
||||
} catch (error) {
|
||||
throw new DatabaseError(`Database error when fetching webhooks for environment ${environmentId}`);
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`getCountOfWebhooksBasedOnSource-${environmentId}-${source}`],
|
||||
[`getWebhookCountBySource-${environmentId}-${source}`],
|
||||
{
|
||||
tags: [webhookCache.tag.byEnvironmentIdAndSource(environmentId, source)],
|
||||
revalidate: SERVICES_REVALIDATION_INTERVAL,
|
||||
@@ -73,12 +81,20 @@ export const getWebhook = async (id: string): Promise<TWebhook | null> => {
|
||||
async () => {
|
||||
validateInputs([id, ZId]);
|
||||
|
||||
const webhook = await prisma.webhook.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
return webhook;
|
||||
try {
|
||||
const webhook = await prisma.webhook.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
return webhook;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`getWebhook-${id}`],
|
||||
{
|
||||
@@ -119,6 +135,11 @@ export const createWebhook = async (
|
||||
if (!(error instanceof InvalidInputError)) {
|
||||
throw new DatabaseError(`Database error when creating webhook for environment ${environmentId}`);
|
||||
}
|
||||
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -150,9 +171,11 @@ export const updateWebhook = async (
|
||||
|
||||
return updatedWebhook;
|
||||
} catch (error) {
|
||||
throw new DatabaseError(
|
||||
`Database error when updating webhook with ID ${webhookId} for environment ${environmentId}`
|
||||
);
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user