From 1798e6433132fbf58c88d8bd33a4645f5e7a0d7a Mon Sep 17 00:00:00 2001 From: Rotimi Best Date: Wed, 18 Oct 2023 15:56:56 +0100 Subject: [PATCH] chore: caching for action and actionClass services (#1264) --- packages/lib/action/cache.ts | 18 +++ packages/lib/action/service.ts | 174 ++++++++++++++++------------ packages/lib/actionClass/auth.ts | 6 +- packages/lib/actionClass/cache.ts | 34 ++++++ packages/lib/actionClass/service.ts | 125 ++++++++++++-------- packages/types/v1/actionClasses.ts | 2 +- 6 files changed, 232 insertions(+), 127 deletions(-) create mode 100644 packages/lib/action/cache.ts create mode 100644 packages/lib/actionClass/cache.ts diff --git a/packages/lib/action/cache.ts b/packages/lib/action/cache.ts new file mode 100644 index 0000000000..aad03dfdb9 --- /dev/null +++ b/packages/lib/action/cache.ts @@ -0,0 +1,18 @@ +import { revalidateTag } from "next/cache"; + +interface RevalidateProps { + environmentId?: string; +} + +export const actionCache = { + tag: { + byEnvironmentId(environmentId: string): string { + return `environments-${environmentId}-actions`; + }, + }, + revalidate({ environmentId }: RevalidateProps): void { + if (environmentId) { + revalidateTag(this.tag.byEnvironmentId(environmentId)); + } + }, +}; diff --git a/packages/lib/action/service.ts b/packages/lib/action/service.ts index f2761ba6cc..2c59d217a0 100644 --- a/packages/lib/action/service.ts +++ b/packages/lib/action/service.ts @@ -8,12 +8,12 @@ import { ZId } from "@formbricks/types/v1/environment"; import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/v1/errors"; import { Prisma } from "@prisma/client"; import { revalidateTag, unstable_cache } from "next/cache"; -import { getActionClassCacheTag } from "../actionClass/service"; +import { actionClassCache } from "../actionClass/cache"; import { ITEMS_PER_PAGE, SERVICES_REVALIDATION_INTERVAL } from "../constants"; import { getSessionCached } from "../session/service"; +import { createActionClass, getActionClassByEnvironmentIdAndName } from "../actionClass/service"; import { validateInputs } from "../utils/validate"; - -export const getActionsCacheTag = (environmentId: string): string => `environments-${environmentId}-actions`; +import { actionCache } from "./cache"; export const getActionsByEnvironmentId = async ( environmentId: string, @@ -60,12 +60,13 @@ export const getActionsByEnvironmentId = async ( throw error; } }, - [`environments-${environmentId}-actionClasses`], + [`getActionsByEnvironmentId-${environmentId}-${page}`], { - tags: [getActionsCacheTag(environmentId)], + tags: [actionCache.tag.byEnvironmentId(environmentId)], revalidate: SERVICES_REVALIDATION_INTERVAL, } )(); + // since the unstable_cache function does not support deserialization of dates, we need to manually deserialize them // https://github.com/vercel/next.js/issues/51613 return actions.map((action) => ({ @@ -76,6 +77,7 @@ export const getActionsByEnvironmentId = async ( export const createAction = async (data: TActionInput): Promise => { validateInputs([data, ZActionInput]); + const { environmentId, name, properties, sessionId } = data; let eventType: TActionClassType = "code"; @@ -89,6 +91,16 @@ export const createAction = async (data: TActionInput): Promise => { throw new ResourceNotFoundError("Session", sessionId); } + let actionClass = await getActionClassByEnvironmentIdAndName(environmentId, name); + + if (!actionClass) { + actionClass = await createActionClass(environmentId, { + name, + type: eventType, + environmentId, + }); + } + const action = await prisma.event.create({ data: { properties, @@ -98,91 +110,101 @@ export const createAction = async (data: TActionInput): Promise => { }, }, eventClass: { - connectOrCreate: { - where: { - name_environmentId: { - name, - environmentId, - }, - }, - create: { - name, - type: eventType, - environment: { - connect: { - id: environmentId, - }, - }, - }, + connect: { + id: actionClass.id, }, }, }, - include: { - eventClass: true, - }, }); - // revalidate cache revalidateTag(sessionId); - revalidateTag(getActionClassCacheTag(name, environmentId)); - revalidateTag(getActionsCacheTag(environmentId)); + actionCache.revalidate({ + environmentId, + }); return { id: action.id, createdAt: action.createdAt, sessionId: action.sessionId, properties: action.properties, - actionClass: action.eventClass, + actionClass, }; }; -export const getActionCountInLastHour = async (actionClassId: string): Promise => { - validateInputs([actionClassId, ZId]); - try { - const numEventsLastHour = await prisma.event.count({ - where: { - eventClassId: actionClassId, - createdAt: { - gte: new Date(Date.now() - 60 * 60 * 1000), - }, - }, - }); - return numEventsLastHour; - } catch (error) { - throw error; - } -}; +export const getActionCountInLastHour = async (actionClassId: string): Promise => + unstable_cache( + async () => { + validateInputs([actionClassId, ZId]); -export const getActionCountInLast24Hours = async (actionClassId: string): Promise => { - validateInputs([actionClassId, ZId]); - try { - const numEventsLast24Hours = await prisma.event.count({ - where: { - eventClassId: actionClassId, - createdAt: { - gte: new Date(Date.now() - 24 * 60 * 60 * 1000), - }, - }, - }); - return numEventsLast24Hours; - } catch (error) { - throw error; - } -}; + try { + const numEventsLastHour = await prisma.event.count({ + where: { + eventClassId: actionClassId, + createdAt: { + gte: new Date(Date.now() - 60 * 60 * 1000), + }, + }, + }); + return numEventsLastHour; + } catch (error) { + throw error; + } + }, + [`getActionCountInLastHour-${actionClassId}`], + { + tags: [actionClassCache.tag.byId(actionClassId)], + revalidate: SERVICES_REVALIDATION_INTERVAL, + } + )(); -export const getActionCountInLast7Days = async (actionClassId: string): Promise => { - validateInputs([actionClassId, ZId]); - try { - const numEventsLast7Days = await prisma.event.count({ - where: { - eventClassId: actionClassId, - createdAt: { - gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), - }, - }, - }); - return numEventsLast7Days; - } catch (error) { - throw error; - } -}; +export const getActionCountInLast24Hours = async (actionClassId: string): Promise => + unstable_cache( + async () => { + validateInputs([actionClassId, ZId]); + + try { + const numEventsLast24Hours = await prisma.event.count({ + where: { + eventClassId: actionClassId, + createdAt: { + gte: new Date(Date.now() - 24 * 60 * 60 * 1000), + }, + }, + }); + return numEventsLast24Hours; + } catch (error) { + throw error; + } + }, + [`getActionCountInLast24Hours-${actionClassId}`], + { + tags: [actionClassCache.tag.byId(actionClassId)], + revalidate: SERVICES_REVALIDATION_INTERVAL, + } + )(); + +export const getActionCountInLast7Days = async (actionClassId: string): Promise => + unstable_cache( + async () => { + validateInputs([actionClassId, ZId]); + + try { + const numEventsLast7Days = await prisma.event.count({ + where: { + eventClassId: actionClassId, + createdAt: { + gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), + }, + }, + }); + return numEventsLast7Days; + } catch (error) { + throw error; + } + }, + [`getActionCountInLast7Days-${actionClassId}`], + { + tags: [actionClassCache.tag.byId(actionClassId)], + revalidate: SERVICES_REVALIDATION_INTERVAL, + } + )(); diff --git a/packages/lib/actionClass/auth.ts b/packages/lib/actionClass/auth.ts index 381d981b46..28c26d2470 100644 --- a/packages/lib/actionClass/auth.ts +++ b/packages/lib/actionClass/auth.ts @@ -6,6 +6,7 @@ import { hasUserEnvironmentAccess } from "../environment/auth"; import { getActionClass } from "./service"; import { unstable_cache } from "next/cache"; import { SERVICES_REVALIDATION_INTERVAL } from "../constants"; +import { actionClassCache } from "./cache"; export const canUserAccessActionClass = async (userId: string, actionClassId: string): Promise => await unstable_cache( @@ -23,5 +24,8 @@ export const canUserAccessActionClass = async (userId: string, actionClassId: st }, [`users-${userId}-actionClasses-${actionClassId}`], - { revalidate: SERVICES_REVALIDATION_INTERVAL, tags: [`actionClasses-${actionClassId}`] } + { + revalidate: SERVICES_REVALIDATION_INTERVAL, + tags: [actionClassCache.tag.byId(actionClassId)], + } )(); diff --git a/packages/lib/actionClass/cache.ts b/packages/lib/actionClass/cache.ts new file mode 100644 index 0000000000..1fc678353c --- /dev/null +++ b/packages/lib/actionClass/cache.ts @@ -0,0 +1,34 @@ +import { revalidateTag } from "next/cache"; + +interface RevalidateProps { + environmentId?: string; + name?: string; + id?: string; +} + +export const actionClassCache = { + tag: { + byNameAndEnvironmentId(environmentId: string, name: string): string { + return `environments-${environmentId}-actionClass-${name}`; + }, + byEnvironmentId(environmentId: string): string { + return `environments-${environmentId}-actionClasses`; + }, + byId(id: string): string { + return `actionClasses-${id}`; + }, + }, + revalidate({ environmentId, name, id }: RevalidateProps): void { + if (environmentId) { + revalidateTag(this.tag.byEnvironmentId(environmentId)); + } + + if (id) { + revalidateTag(this.tag.byId(id)); + } + + if (name && environmentId) { + revalidateTag(this.tag.byNameAndEnvironmentId(name, environmentId)); + } + }, +}; diff --git a/packages/lib/actionClass/service.ts b/packages/lib/actionClass/service.ts index da5e3b546a..2f550dc526 100644 --- a/packages/lib/actionClass/service.ts +++ b/packages/lib/actionClass/service.ts @@ -7,14 +7,9 @@ import { TActionClass, TActionClassInput, ZActionClassInput } from "@formbricks/ import { ZId } from "@formbricks/types/v1/environment"; import { ZOptionalNumber, ZString } from "@formbricks/types/v1/common"; import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/v1/errors"; -import { revalidateTag, unstable_cache } from "next/cache"; +import { unstable_cache } from "next/cache"; import { validateInputs } from "../utils/validate"; - -export const getActionClassCacheTag = (name: string, environmentId: string): string => - `environments-${environmentId}-actionClass-${name}`; - -const getActionClassesCacheTag = (environmentId: string): string => - `environments-${environmentId}-actionClasses`; +import { actionClassCache } from "./cache"; const select = { id: true, @@ -31,6 +26,7 @@ export const getActionClasses = (environmentId: string, page?: number): Promise< unstable_cache( async () => { validateInputs([environmentId, ZId], [page, ZOptionalNumber]); + try { const actionClasses = await prisma.eventClass.findMany({ where: { @@ -49,34 +45,73 @@ export const getActionClasses = (environmentId: string, page?: number): Promise< throw new DatabaseError(`Database error when fetching actions for environment ${environmentId}`); } }, - [`environments-${environmentId}-actionClasses`], + [`getActionClasses-${environmentId}-${page}`], { - tags: [getActionClassesCacheTag(environmentId)], + tags: [actionClassCache.tag.byEnvironmentId(environmentId)], revalidate: SERVICES_REVALIDATION_INTERVAL, } )(); -export const getActionClass = async (actionClassId: string): Promise => { - validateInputs([actionClassId, ZId]); - try { - let actionClass = await prisma.eventClass.findUnique({ - where: { - id: actionClassId, - }, - select, - }); +export const getActionClassByEnvironmentIdAndName = async ( + environmentId: string, + name: string +): Promise => + unstable_cache( + async () => { + validateInputs([environmentId, ZId], [name, ZString]); - return actionClass; - } catch (error) { - throw new DatabaseError(`Database error when fetching action`); - } -}; + try { + const actionClass = await prisma.eventClass.findFirst({ + where: { + name, + environmentId, + }, + select, + }); + + return actionClass; + } catch (error) { + throw new DatabaseError(`Database error when fetching action`); + } + }, + [`getActionClass-${environmentId}-${name}`], + { + tags: [actionClassCache.tag.byNameAndEnvironmentId(environmentId, name)], + revalidate: SERVICES_REVALIDATION_INTERVAL, + } + )(); + +export const getActionClass = async (actionClassId: string): Promise => + unstable_cache( + async () => { + validateInputs([actionClassId, ZId]); + + try { + const actionClass = await prisma.eventClass.findUnique({ + where: { + id: actionClassId, + }, + select, + }); + + return actionClass; + } catch (error) { + throw new DatabaseError(`Database error when fetching action`); + } + }, + [`getActionClass-${actionClassId}`], + { + tags: [actionClassCache.tag.byId(actionClassId)], + revalidate: SERVICES_REVALIDATION_INTERVAL, + } + )(); export const deleteActionClass = async ( environmentId: string, actionClassId: string ): Promise => { validateInputs([environmentId, ZId], [actionClassId, ZId]); + try { const result = await prisma.eventClass.delete({ where: { @@ -86,8 +121,10 @@ export const deleteActionClass = async ( }); if (result === null) throw new ResourceNotFoundError("Action", actionClassId); - // revalidate cache - revalidateTag(getActionClassesCacheTag(result.environmentId)); + actionClassCache.revalidate({ + environmentId, + id: actionClassId, + }); return result; } catch (error) { @@ -102,8 +139,9 @@ export const createActionClass = async ( actionClass: TActionClassInput ): Promise => { validateInputs([environmentId, ZId], [actionClass, ZActionClassInput]); + try { - const result = await prisma.eventClass.create({ + const actionClassPrisma = await prisma.eventClass.create({ data: { name: actionClass.name, description: actionClass.description, @@ -116,10 +154,13 @@ export const createActionClass = async ( select, }); - // revalidate cache - revalidateTag(getActionClassesCacheTag(environmentId)); + actionClassCache.revalidate({ + name: actionClassPrisma.name, + environmentId: actionClassPrisma.environmentId, + id: actionClassPrisma.id, + }); - return result; + return actionClassPrisma; } catch (error) { throw new DatabaseError(`Database error when creating an action for environment ${environmentId}`); } @@ -131,6 +172,7 @@ export const updateActionClass = async ( inputActionClass: Partial ): Promise => { validateInputs([environmentId, ZId], [actionClassId, ZId], [inputActionClass, ZActionClassInput.partial()]); + try { const result = await prisma.eventClass.update({ where: { @@ -148,29 +190,14 @@ export const updateActionClass = async ( }); // revalidate cache - revalidateTag(getActionClassCacheTag(result.name, result.environmentId)); - revalidateTag(getActionClassesCacheTag(result.environmentId)); + actionClassCache.revalidate({ + environmentId: result.environmentId, + name: result.name, + id: result.id, + }); return result; } catch (error) { throw new DatabaseError(`Database error when updating an action for environment ${environmentId}`); } }; - -export const getActionClassCached = async (name: string, environmentId: string) => - unstable_cache( - async (): Promise => { - validateInputs([name, ZString], [environmentId, ZId]); - return await prisma.eventClass.findFirst({ - where: { - name, - environmentId, - }, - }); - }, - [`environments-${environmentId}-actionClasses-${name}`], - { - tags: [getActionClassesCacheTag(environmentId)], - revalidate: SERVICES_REVALIDATION_INTERVAL, - } - )(); diff --git a/packages/types/v1/actionClasses.ts b/packages/types/v1/actionClasses.ts index f2d75acf95..72a2b0b61e 100644 --- a/packages/types/v1/actionClasses.ts +++ b/packages/types/v1/actionClasses.ts @@ -49,7 +49,7 @@ export const ZActionClassInput = z.object({ name: z.string(), description: z.string().optional(), noCodeConfig: ZActionClassNoCodeConfig.nullish(), - type: z.enum(["code", "noCode"]), + type: z.enum(["code", "noCode", "automatic"]), }); export const ZActionClassAutomaticInput = z.object({