diff --git a/apps/web/app/api/v1/client/[environmentId]/in-app/sync/lib/posthog.ts b/apps/web/app/api/v1/client/[environmentId]/in-app/sync/lib/posthog.ts index 3c7e5ff3bf..b4f015e498 100644 --- a/apps/web/app/api/v1/client/[environmentId]/in-app/sync/lib/posthog.ts +++ b/apps/web/app/api/v1/client/[environmentId]/in-app/sync/lib/posthog.ts @@ -8,12 +8,17 @@ export const sendFreeLimitReachedEventToPosthogBiWeekly = async ( ): Promise => 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 } diff --git a/apps/web/app/api/v1/users/route.ts b/apps/web/app/api/v1/users/route.ts index 48876bfd34..55d61b0464 100644 --- a/apps/web/app/api/v1/users/route.ts +++ b/apps/web/app/api/v1/users/route.ts @@ -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", diff --git a/packages/lib/action/service.ts b/packages/lib/action/service.ts index bcd82e1201..7705d263f3 100644 --- a/packages/lib/action/service.ts +++ b/packages/lib/action/service.ts @@ -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 => { 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 => @@ -254,17 +270,25 @@ export const getActionCountInLast7Days = async (actionClassId: string): Promise< export const getActionCountInLastQuarter = async (actionClassId: string, personId: string): Promise => 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 => 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 => 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 => 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 => 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 => 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}`], { diff --git a/packages/lib/actionClass/auth.ts b/packages/lib/actionClass/auth.ts index 99c0abeef0..5130d67cc1 100644 --- a/packages/lib/actionClass/auth.ts +++ b/packages/lib/actionClass/auth.ts @@ -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; }; diff --git a/packages/lib/actionClass/service.ts b/packages/lib/actionClass/service.ts index 462f066f1b..c07ebe0bf2 100644 --- a/packages/lib/actionClass/service.ts +++ b/packages/lib/actionClass/service.ts @@ -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, diff --git a/packages/lib/apiKey/auth.ts b/packages/lib/apiKey/auth.ts index 77698bba7d..9f2ca4834f 100644 --- a/packages/lib/apiKey/auth.ts +++ b/packages/lib/apiKey/auth.ts @@ -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}`], diff --git a/packages/lib/apiKey/service.ts b/packages/lib/apiKey/service.ts index c6da78815a..46ab7f6dd6 100644 --- a/packages/lib/apiKey/service.ts +++ b/packages/lib/apiKey/service.ts @@ -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 { +export const createApiKey = async ( + environmentId: string, + apiKeyData: TApiKeyCreateInput +): Promise => { 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 => { const hashedKey = getHash(apiKey); diff --git a/packages/lib/attributeClass/auth.ts b/packages/lib/attributeClass/auth.ts index 7fa819d975..3a736504fb 100644 --- a/packages/lib/attributeClass/auth.ts +++ b/packages/lib/attributeClass/auth.ts @@ -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}`] } )(); diff --git a/packages/lib/attributeClass/service.ts b/packages/lib/attributeClass/service.ts index f46650b6b6..287e86bec0 100644 --- a/packages/lib/attributeClass/service.ts +++ b/packages/lib/attributeClass/service.ts @@ -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 => { 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 => { 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 => { @@ -187,6 +211,9 @@ export const deleteAttributeClass = async (attributeClassId: string): Promise => { 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}`], { diff --git a/packages/lib/environment/service.ts b/packages/lib/environment/service.ts index 164acf7efd..1b43527ed6 100644 --- a/packages/lib/environment/service.ts +++ b/packages/lib/environment/service.ts @@ -134,25 +134,29 @@ export const updateEnvironment = async ( }; export const getFirstEnvironmentByUserId = async (userId: string): Promise => { - 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 => { 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; + } }; diff --git a/packages/lib/integration/auth.ts b/packages/lib/integration/auth.ts index 9ba1f326b4..4bc31934a3 100644 --- a/packages/lib/integration/auth.ts +++ b/packages/lib/integration/auth.ts @@ -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}`] } )(); diff --git a/packages/lib/invite/service.ts b/packages/lib/invite/service.ts index c942a93e54..03b86026c8 100644 --- a/packages/lib/invite/service.ts +++ b/packages/lib/invite/service.ts @@ -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 { 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 => { 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 => { 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; }; diff --git a/packages/lib/membership/service.ts b/packages/lib/membership/service.ts index 6f482fc94f..434641bbd7 100644 --- a/packages/lib/membership/service.ts +++ b/packages/lib/membership/service.ts @@ -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 => { 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 ( diff --git a/packages/lib/person/auth.ts b/packages/lib/person/auth.ts index 10dc75b16c..bc8731d630 100644 --- a/packages/lib/person/auth.ts +++ b/packages/lib/person/auth.ts @@ -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}`], { diff --git a/packages/lib/person/service.ts b/packages/lib/person/service.ts index 2e04a6691a..21a22038ce 100644 --- a/packages/lib/person/service.ts +++ b/packages/lib/person/service.ts @@ -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> => { 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 => 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 diff --git a/packages/lib/product/auth.ts b/packages/lib/product/auth.ts index bb24edd79b..3aa78967ba 100644 --- a/packages/lib/product/auth.ts +++ b/packages/lib/product/auth.ts @@ -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}`], { diff --git a/packages/lib/product/service.ts b/packages/lib/product/service.ts index 5764f80c96..02f26974fe 100644 --- a/packages/lib/product/service.ts +++ b/packages/lib/product/service.ts @@ -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 => }; export const deleteProduct = async (productId: string): Promise => { - 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; + } }; diff --git a/packages/lib/response/auth.ts b/packages/lib/response/auth.ts index 996ebb2696..8127ae2771 100644 --- a/packages/lib/response/auth.ts +++ b/packages/lib/response/auth.ts @@ -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)] } diff --git a/packages/lib/response/service.ts b/packages/lib/response/service.ts index 8de3fa427a..db92620a76 100644 --- a/packages/lib/response/service.ts +++ b/packages/lib/response/service.ts @@ -448,7 +448,7 @@ export const getResponsePersonAttributes = async (surveyId: string): Promise { 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 => { + 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; } }, diff --git a/packages/lib/responseNote/auth.ts b/packages/lib/responseNote/auth.ts index cb6e277d88..e9aa9959ae 100644 --- a/packages/lib/responseNote/auth.ts +++ b/packages/lib/responseNote/auth.ts @@ -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 diff --git a/packages/lib/segment/service.ts b/packages/lib/segment/service.ts index d7491da2de..05c1d7d536 100644 --- a/packages/lib/segment/service.ts +++ b/packages/lib/segment/service.ts @@ -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 => { 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; + } }; diff --git a/packages/lib/survey/auth.ts b/packages/lib/survey/auth.ts index b168272e76..04ab4720e1 100644 --- a/packages/lib/survey/auth.ts +++ b/packages/lib/survey/auth.ts @@ -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)] } diff --git a/packages/lib/survey/service.ts b/packages/lib/survey/service.ts index add80d362d..bc65b4c04b 100644 --- a/packages/lib/survey/service.ts +++ b/packages/lib/survey/service.ts @@ -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 => export const updateSurvey = async (updatedSurvey: TSurvey): Promise => { 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 => 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 => { 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 = { - ...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 = { + ...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 => { + validateInputs([surveyId, ZId], [newSegmentId, ZId]); try { - validateInputs([surveyId, ZId], [newSegmentId, ZId]); - const currentSurvey = await getSurvey(surveyId); if (!currentSurvey) { throw new ResourceNotFoundError("survey", surveyId); diff --git a/packages/lib/tag/auth.ts b/packages/lib/tag/auth.ts index c0ea563e8b..0eaca64480 100644 --- a/packages/lib/tag/auth.ts +++ b/packages/lib/tag/auth.ts @@ -17,15 +17,19 @@ export const canUserAccessTag = async (userId: string, tagId: string): Promise { 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, } diff --git a/packages/lib/tagOnResponse/auth.ts b/packages/lib/tagOnResponse/auth.ts index d7e1bc3e29..c6051ed7eb 100644 --- a/packages/lib/tagOnResponse/auth.ts +++ b/packages/lib/tagOnResponse/auth.ts @@ -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)], diff --git a/packages/lib/tagOnResponse/service.ts b/packages/lib/tagOnResponse/service.ts index 2c302d413b..e307c31ae1 100644 --- a/packages/lib/tagOnResponse/service.ts +++ b/packages/lib/tagOnResponse/service.ts @@ -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 ({ tagId: tagCount.tagId, count: tagCount._count._all })); } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } throw error; } }, diff --git a/packages/lib/team/auth.ts b/packages/lib/team/auth.ts index 717fd3e6a3..f1b5e34e5f 100644 --- a/packages/lib/team/auth.ts +++ b/packages/lib/team/auth.ts @@ -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)] } diff --git a/packages/lib/team/service.ts b/packages/lib/team/service.ts index c929fe072b..0d1084f980 100644 --- a/packages/lib/team/service.ts +++ b/packages/lib/team/service.ts @@ -161,6 +161,10 @@ export const createTeam = async (teamInput: TTeamCreateInput): Promise => 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 } 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 { 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 { 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 => 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}`], { diff --git a/packages/lib/user/service.ts b/packages/lib/user/service.ts index cbef1ef9e8..e7f5f1cb65 100644 --- a/packages/lib/user/service.ts +++ b/packages/lib/user/service.ts @@ -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 => { 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 => { 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 => { export const getUsersWithTeam = async (teamId: string): Promise => { 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; + } }; diff --git a/packages/lib/webhook/auth.ts b/packages/lib/webhook/auth.ts index a7ebbc3991..f410c796fa 100644 --- a/packages/lib/webhook/auth.ts +++ b/packages/lib/webhook/auth.ts @@ -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}`], { diff --git a/packages/lib/webhook/service.ts b/packages/lib/webhook/service.ts index b53f8297c8..f29b919dce 100644 --- a/packages/lib/webhook/service.ts +++ b/packages/lib/webhook/service.ts @@ -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 => { 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; } };