fix: adds try...catch in all services (#2494)

Co-authored-by: Matti Nannt <mail@matthiasnannt.com>
This commit is contained in:
Piyush Gupta
2024-04-22 12:45:37 +05:30
committed by GitHub
parent ed509d3a9a
commit 4ee27f9618
33 changed files with 1686 additions and 1229 deletions

View File

@@ -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
}

View File

@@ -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",

View File

@@ -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}`],
{

View File

@@ -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;
};

View File

@@ -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,

View File

@@ -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}`],

View File

@@ -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);

View File

@@ -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}`] }
)();

View File

@@ -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;
}
};

View File

@@ -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,

View File

@@ -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}`],
{

View File

@@ -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;
}
};

View File

@@ -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}`] }
)();

View File

@@ -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;
};

View File

@@ -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 (

View File

@@ -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}`],
{

View File

@@ -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

View File

@@ -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}`],
{

View File

@@ -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;
}
};

View File

@@ -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)] }

View File

@@ -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;
}
},

View File

@@ -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

View File

@@ -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;
}
};

View File

@@ -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)] }

View File

@@ -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);

View File

@@ -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,
}

View File

@@ -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)],

View File

@@ -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;
}
},

View File

@@ -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)] }

View File

@@ -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}`],
{

View File

@@ -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;
}
};

View File

@@ -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}`],
{

View File

@@ -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;
}
};