chore: caching for action and actionClass services (#1264)

This commit is contained in:
Rotimi Best
2023-10-18 15:56:56 +01:00
committed by GitHub
parent 332631907a
commit 1798e64331
6 changed files with 232 additions and 127 deletions

View File

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

View File

@@ -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<TAction> => {
validateInputs([data, ZActionInput]);
const { environmentId, name, properties, sessionId } = data;
let eventType: TActionClassType = "code";
@@ -89,6 +91,16 @@ export const createAction = async (data: TActionInput): Promise<TAction> => {
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<TAction> => {
},
},
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<number> => {
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<number> =>
unstable_cache(
async () => {
validateInputs([actionClassId, ZId]);
export const getActionCountInLast24Hours = async (actionClassId: string): Promise<number> => {
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<number> => {
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<number> =>
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<number> =>
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,
}
)();

View File

@@ -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<boolean> =>
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)],
}
)();

View File

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

View File

@@ -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<TActionClass | null> => {
validateInputs([actionClassId, ZId]);
try {
let actionClass = await prisma.eventClass.findUnique({
where: {
id: actionClassId,
},
select,
});
export const getActionClassByEnvironmentIdAndName = async (
environmentId: string,
name: string
): Promise<TActionClass | null> =>
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<TActionClass | null> =>
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<TActionClass> => {
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<TActionClass> => {
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<TActionClassInput>
): Promise<TActionClass> => {
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<TActionClass | null> => {
validateInputs([name, ZString], [environmentId, ZId]);
return await prisma.eventClass.findFirst({
where: {
name,
environmentId,
},
});
},
[`environments-${environmentId}-actionClasses-${name}`],
{
tags: [getActionClassesCacheTag(environmentId)],
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
)();

View File

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