chore: caching responseNote and attributeClass service (#1307)

This commit is contained in:
Rotimi Best
2023-10-27 08:45:39 +01:00
committed by GitHub
parent 7ad7a255b6
commit 94e872025d
8 changed files with 306 additions and 109 deletions
@@ -1,7 +1,7 @@
import { getUpdatedState } from "@/app/api/v1/js/sync/lib/sync";
import { responses } from "@/app/lib/api/response";
import { transformErrorToDetails } from "@/app/lib/api/validator";
import { createAttributeClass, getAttributeClassByNameCached } from "@formbricks/lib/attributeClass/service";
import { createAttributeClass, getAttributeClassByName } from "@formbricks/lib/attributeClass/service";
import { personCache } from "@formbricks/lib/person/cache";
import { getPerson, updatePersonAttribute } from "@formbricks/lib/person/service";
import { ZJsPeopleAttributeInput } from "@formbricks/types/js";
@@ -35,7 +35,7 @@ export async function POST(req: Request, { params }): Promise<NextResponse> {
return responses.notFoundResponse("Person", personId, true);
}
let attributeClass = await getAttributeClassByNameCached(environmentId, key);
let attributeClass = await getAttributeClassByName(environmentId, key);
// create new attribute class if not found
if (attributeClass === null) {
+34
View File
@@ -0,0 +1,34 @@
import { revalidateTag } from "next/cache";
interface RevalidateProps {
id?: string;
name?: string;
environmentId?: string;
}
export const attributeClassCache = {
tag: {
byId(id: string) {
return `attributeClass-${id}`;
},
byEnvironmentId(environmentId: string) {
return `environments-${environmentId}-attributeClasses`;
},
byEnvironmentIdAndName(environmentId: string, name: string) {
return `environments-${environmentId}-name-${name}-attributeClasses`;
},
},
revalidate({ id, environmentId, name }: RevalidateProps): void {
if (id) {
revalidateTag(this.tag.byId(id));
}
if (environmentId) {
revalidateTag(this.tag.byEnvironmentId(environmentId));
}
if (environmentId && name) {
revalidateTag(this.tag.byEnvironmentIdAndName(environmentId, name));
}
},
};
+92 -54
View File
@@ -7,57 +7,81 @@ import {
TAttributeClassUpdateInput,
ZAttributeClassUpdateInput,
TAttributeClassType,
ZAttributeClassType,
} from "@formbricks/types/attributeClasses";
import { ZId } from "@formbricks/types/environment";
import { validateInputs } from "../utils/validate";
import { DatabaseError } from "@formbricks/types/errors";
import { revalidateTag, unstable_cache } from "next/cache";
import { unstable_cache } from "next/cache";
import { SERVICES_REVALIDATION_INTERVAL, ITEMS_PER_PAGE } from "../constants";
import { ZOptionalNumber } from "@formbricks/types/common";
const attributeClassesCacheTag = (environmentId: string): string =>
`environments-${environmentId}-attributeClasses`;
const getAttributeClassesCacheKey = (environmentId: string): string[] => [
attributeClassesCacheTag(environmentId),
];
import { ZOptionalNumber, ZString } from "@formbricks/types/common";
import { attributeClassCache } from "./cache";
import { formatAttributeClassDateFields } from "./util";
export const getAttributeClass = async (attributeClassId: string): Promise<TAttributeClass | null> => {
validateInputs([attributeClassId, ZId]);
try {
const attributeClass = await prisma.attributeClass.findFirst({
where: {
id: attributeClassId,
},
});
return attributeClass;
} catch (error) {
throw new DatabaseError(`Database error when fetching attributeClass with id ${attributeClassId}`);
const attributeClass = await unstable_cache(
async () => {
validateInputs([attributeClassId, ZId]);
try {
return await prisma.attributeClass.findFirst({
where: {
id: attributeClassId,
},
});
} catch (error) {
throw new DatabaseError(`Database error when fetching attributeClass with id ${attributeClassId}`);
}
},
[`getAttributeClass-${attributeClassId}`],
{
tags: [attributeClassCache.tag.byId(attributeClassId)],
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
)();
if (!attributeClass) {
return null;
}
return formatAttributeClassDateFields(attributeClass);
};
export const getAttributeClasses = async (
environmentId: string,
page?: number
): Promise<TAttributeClass[]> => {
validateInputs([environmentId, ZId], [page, ZOptionalNumber]);
const attributeClasses = await unstable_cache(
async () => {
validateInputs([environmentId, ZId], [page, ZOptionalNumber]);
try {
const attributeClasses = await prisma.attributeClass.findMany({
where: {
environmentId: environmentId,
},
orderBy: {
createdAt: "asc",
},
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
try {
const attributeClasses = await prisma.attributeClass.findMany({
where: {
environmentId: environmentId,
},
orderBy: {
createdAt: "asc",
},
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
return attributeClasses;
} catch (error) {
throw new DatabaseError(`Database error when fetching attributeClasses for environment ${environmentId}`);
}
return attributeClasses;
} catch (error) {
throw new DatabaseError(
`Database error when fetching attributeClasses for environment ${environmentId}`
);
}
},
[`getAttributeClasses-${environmentId}-${page}`],
{
tags: [attributeClassCache.tag.byEnvironmentId(environmentId)],
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
)();
return attributeClasses.map(formatAttributeClassDateFields);
};
export const updatetAttributeClass = async (
@@ -65,6 +89,7 @@ export const updatetAttributeClass = async (
data: Partial<TAttributeClassUpdateInput>
): Promise<TAttributeClass | null> => {
validateInputs([attributeClassId, ZId], [data, ZAttributeClassUpdateInput.partial()]);
try {
const attributeClass = await prisma.attributeClass.update({
where: {
@@ -76,7 +101,11 @@ export const updatetAttributeClass = async (
},
});
revalidateTag(attributeClassesCacheTag(attributeClass.environmentId));
attributeClassCache.revalidate({
id: attributeClass.id,
environmentId: attributeClass.environmentId,
name: attributeClass.name,
});
return attributeClass;
} catch (error) {
@@ -84,36 +113,32 @@ export const updatetAttributeClass = async (
}
};
export const getAttributeClassByNameCached = async (environmentId: string, name: string) =>
export const getAttributeClassByName = async (environmentId: string, name: string) =>
await unstable_cache(
async (): Promise<TAttributeClass | null> => {
return await getAttributeClassByName(environmentId, name);
validateInputs([environmentId, ZId], [name, ZString]);
return await prisma.attributeClass.findFirst({
where: {
environmentId,
name,
},
});
},
[`environments-${environmentId}-attributeClass-${name}`],
[`getAttributeClassByName-${environmentId}-${name}`],
{
tags: getAttributeClassesCacheKey(environmentId),
tags: [attributeClassCache.tag.byEnvironmentIdAndName(environmentId, name)],
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
)();
export const getAttributeClassByName = async (
environmentId: string,
name: string
): Promise<TAttributeClass | null> => {
const attributeClass = await prisma.attributeClass.findFirst({
where: {
environmentId,
name,
},
});
return attributeClass;
};
export const createAttributeClass = async (
environmentId: string,
name: string,
type: TAttributeClassType
): Promise<TAttributeClass | null> => {
validateInputs([environmentId, ZId], [name, ZString], [type, ZAttributeClassType]);
const attributeClass = await prisma.attributeClass.create({
data: {
name,
@@ -125,12 +150,19 @@ export const createAttributeClass = async (
},
},
});
revalidateTag(attributeClassesCacheTag(environmentId));
attributeClassCache.revalidate({
id: attributeClass.id,
environmentId: attributeClass.environmentId,
name: attributeClass.name,
});
return attributeClass;
};
export const deleteAttributeClass = async (attributeClassId: string): Promise<TAttributeClass> => {
validateInputs([attributeClassId, ZId]);
try {
const deletedAttributeClass = await prisma.attributeClass.delete({
where: {
@@ -138,6 +170,12 @@ export const deleteAttributeClass = async (attributeClassId: string): Promise<TA
},
});
attributeClassCache.revalidate({
id: deletedAttributeClass.id,
environmentId: deletedAttributeClass.environmentId,
name: deletedAttributeClass.name,
});
return deletedAttributeClass;
} catch (error) {
throw new DatabaseError(`Database error when deleting webhook with ID ${attributeClassId}`);
+14
View File
@@ -0,0 +1,14 @@
import "server-only";
import { TAttributeClass } from "@formbricks/types/attributeClasses";
export const formatAttributeClassDateFields = (attributeClass: TAttributeClass): TAttributeClass => {
if (typeof attributeClass.createdAt === "string") {
attributeClass.createdAt = new Date(attributeClass.createdAt);
}
if (typeof attributeClass.updatedAt === "string") {
attributeClass.updatedAt = new Date(attributeClass.updatedAt);
}
return attributeClass;
};
+4 -4
View File
@@ -1,21 +1,21 @@
import { revalidateTag } from "next/cache";
interface RevalidateProps {
id?: string;
environmentId?: string;
personId?: string;
id?: string;
singleUseId?: string;
surveyId?: string;
}
export const responseCache = {
tag: {
byEnvironmentId(environmentId: string) {
return `environments-${environmentId}-responses`;
},
byId(responseId: string) {
return `responses-${responseId}`;
},
byEnvironmentId(environmentId: string) {
return `environments-${environmentId}-responses`;
},
byPersonId(personId: string) {
return `people-${personId}-responses`;
},
+59 -31
View File
@@ -22,6 +22,8 @@ import { ZString, ZOptionalNumber } from "@formbricks/types/common";
import { ITEMS_PER_PAGE, SERVICES_REVALIDATION_INTERVAL } from "../constants";
import { responseCache } from "./cache";
import { formatResponseDateFields } from "../response/util";
import { getResponseNotes } from "../responseNote/service";
import { responseNoteCache } from "../responseNote/cache";
const responseSelection = {
id: true,
@@ -51,22 +53,6 @@ const responseSelection = {
},
},
},
notes: {
select: {
id: true,
createdAt: true,
updatedAt: true,
text: true,
user: {
select: {
id: true,
name: true,
},
},
isResolved: true,
isEdited: true,
},
},
tags: {
select: {
tag: {
@@ -106,9 +92,11 @@ export const getResponsesByPersonId = async (
let responses: Array<TResponse> = [];
responsePrisma.forEach((response) => {
responsePrisma.forEach(async (response) => {
const responseNotes = await getResponseNotes(response.id);
responses.push({
...response,
notes: responseNotes,
person: response.person ? transformPrismaPerson(response.person) : null,
tags: response.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
});
@@ -156,8 +144,10 @@ export const getResponseBySingleUseId = async (
return null;
}
const responseNotes = await getResponseNotes(responsePrisma.id);
const response: TResponse = {
...responsePrisma,
notes: responseNotes,
person: responsePrisma.person ? transformPrismaPerson(responsePrisma.person) : null,
tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
};
@@ -222,18 +212,24 @@ export const createResponse = async (responseInput: TResponseInput): Promise<TRe
select: responseSelection,
});
const responseNotes = await getResponseNotes(responsePrisma.id);
const response: TResponse = {
...responsePrisma,
notes: responseNotes,
person: responsePrisma.person ? transformPrismaPerson(responsePrisma.person) : null,
tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
};
responseCache.revalidate({
personId: response.person?.id,
id: response.id,
personId: response.person?.id,
surveyId: response.surveyId,
});
responseNoteCache.revalidate({
responseId: response.id,
});
return response;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
@@ -261,8 +257,10 @@ export const getResponse = async (responseId: string): Promise<TResponse | null>
throw new ResourceNotFoundError("Response", responseId);
}
const responseNotes = await getResponseNotes(responsePrisma.id);
const response: TResponse = {
...responsePrisma,
notes: responseNotes,
person: responsePrisma.person ? transformPrismaPerson(responsePrisma.person) : null,
tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
};
@@ -277,7 +275,10 @@ export const getResponse = async (responseId: string): Promise<TResponse | null>
}
},
[`getResponse-${responseId}`],
{ tags: [responseCache.tag.byId(responseId)], revalidate: SERVICES_REVALIDATION_INTERVAL }
{
tags: [responseCache.tag.byId(responseId), responseNoteCache.tag.byResponseId(responseId)],
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
)();
if (!response) {
@@ -310,11 +311,18 @@ export const getResponses = async (surveyId: string, page?: number): Promise<TRe
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
const transformedResponses: TResponse[] = responses.map((responsePrisma) => ({
...responsePrisma,
person: responsePrisma.person ? transformPrismaPerson(responsePrisma.person) : null,
tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
}));
const transformedResponses: TResponse[] = await Promise.all(
responses.map(async (responsePrisma) => {
const responseNotes = await getResponseNotes(responsePrisma.id);
return {
...responsePrisma,
notes: responseNotes,
person: responsePrisma.person ? transformPrismaPerson(responsePrisma.person) : null,
tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
};
})
);
return transformedResponses;
} catch (error) {
@@ -363,11 +371,18 @@ export const getResponsesByEnvironmentId = async (
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
const transformedResponses: TResponse[] = responses.map((responsePrisma) => ({
...responsePrisma,
person: responsePrisma.person ? transformPrismaPerson(responsePrisma.person) : null,
tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
}));
const transformedResponses: TResponse[] = await Promise.all(
responses.map(async (responsePrisma) => {
const responseNotes = await getResponseNotes(responsePrisma.id);
return {
...responsePrisma,
notes: responseNotes,
person: responsePrisma.person ? transformPrismaPerson(responsePrisma.person) : null,
tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
};
})
);
return transformedResponses;
} catch (error) {
@@ -428,18 +443,24 @@ export const updateResponse = async (
select: responseSelection,
});
const responseNotes = await getResponseNotes(responsePrisma.id);
const response: TResponse = {
...responsePrisma,
notes: responseNotes,
person: responsePrisma.person ? transformPrismaPerson(responsePrisma.person) : null,
tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
};
responseCache.revalidate({
personId: response.person?.id,
id: response.id,
personId: response.person?.id,
surveyId: response.surveyId,
});
responseNoteCache.revalidate({
responseId: response.id,
});
return response;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
@@ -460,19 +481,26 @@ export const deleteResponse = async (responseId: string): Promise<TResponse> =>
select: responseSelection,
});
const responseNotes = await getResponseNotes(responsePrisma.id);
const response: TResponse = {
...responsePrisma,
notes: responseNotes,
person: responsePrisma.person ? transformPrismaPerson(responsePrisma.person) : null,
tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
};
deleteDisplayByResponseId(responseId, response.surveyId);
responseCache.revalidate({
personId: response.person?.id,
id: response.id,
personId: response.person?.id,
surveyId: response.surveyId,
});
responseNoteCache.revalidate({
responseId: response.id,
});
return response;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
+26
View File
@@ -0,0 +1,26 @@
import { revalidateTag } from "next/cache";
interface RevalidateProps {
id?: string;
responseId?: string;
}
export const responseNoteCache = {
tag: {
byId(id: string) {
return `responseNotes-${id}`;
},
byResponseId(responseId: string) {
return `responses-${responseId}-responseNote`;
},
},
revalidate({ id, responseId }: RevalidateProps): void {
if (id) {
revalidateTag(this.tag.byId(id));
}
if (responseId) {
revalidateTag(this.tag.byResponseId(responseId));
}
},
};
+75 -18
View File
@@ -6,6 +6,12 @@ import { DatabaseError } from "@formbricks/types/errors";
import { TResponseNote } from "@formbricks/types/responses";
import { Prisma } from "@prisma/client";
import { responseCache } from "../response/cache";
import { validateInputs } from "../utils/validate";
import { ZId } from "@formbricks/types/environment";
import { ZString } from "@formbricks/types/common";
import { SERVICES_REVALIDATION_INTERVAL } from "../constants";
import { unstable_cache } from "next/cache";
import { responseNoteCache } from "./cache";
const select = {
id: true,
@@ -33,6 +39,8 @@ export const createResponseNote = async (
userId: string,
text: string
): Promise<TResponseNote> => {
validateInputs([responseId, ZId], [userId, ZId], [text, ZString]);
try {
const responseNote = await prisma.responseNote.create({
data: {
@@ -44,28 +52,15 @@ export const createResponseNote = async (
});
responseCache.revalidate({
id: responseId,
id: responseNote.response.id,
surveyId: responseNote.response.surveyId,
});
return responseNote;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
};
export const getResponseNote = async (responseNoteId: string): Promise<TResponseNote | null> => {
try {
const responseNote = await prisma.responseNote.findUnique({
where: {
id: responseNoteId,
},
select,
responseNoteCache.revalidate({
id: responseNote.id,
responseId: responseNote.response.id,
});
return responseNote;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
@@ -76,7 +71,57 @@ export const getResponseNote = async (responseNoteId: string): Promise<TResponse
}
};
export const getResponseNote = async (responseNoteId: string): Promise<TResponseNote | null> =>
unstable_cache(
async () => {
try {
const responseNote = await prisma.responseNote.findUnique({
where: {
id: responseNoteId,
},
select,
});
return responseNote;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
},
[`getResponseNote-${responseNoteId}`],
{ tags: [responseNoteCache.tag.byId(responseNoteId)], revalidate: SERVICES_REVALIDATION_INTERVAL }
)();
export const getResponseNotes = async (responseId: string): Promise<TResponseNote[]> =>
unstable_cache(
async () => {
try {
validateInputs([responseId, ZId]);
const responseNotes = await prisma.responseNote.findMany({
where: {
responseId,
},
select,
});
return responseNotes;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
},
[`getResponseNotes-${responseId}`],
{ tags: [responseNoteCache.tag.byResponseId(responseId)], revalidate: SERVICES_REVALIDATION_INTERVAL }
)();
export const updateResponseNote = async (responseNoteId: string, text: string): Promise<TResponseNote> => {
validateInputs([responseNoteId, ZString], [text, ZString]);
try {
const updatedResponseNote = await prisma.responseNote.update({
where: {
@@ -95,6 +140,11 @@ export const updateResponseNote = async (responseNoteId: string, text: string):
surveyId: updatedResponseNote.response.surveyId,
});
responseNoteCache.revalidate({
id: updatedResponseNote.id,
responseId: updatedResponseNote.response.id,
});
return updatedResponseNote;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
@@ -106,6 +156,8 @@ export const updateResponseNote = async (responseNoteId: string, text: string):
};
export const resolveResponseNote = async (responseNoteId: string): Promise<TResponseNote> => {
validateInputs([responseNoteId, ZString]);
try {
const responseNote = await prisma.responseNote.update({
where: {
@@ -123,6 +175,11 @@ export const resolveResponseNote = async (responseNoteId: string): Promise<TResp
surveyId: responseNote.response.surveyId,
});
responseNoteCache.revalidate({
id: responseNote.id,
responseId: responseNote.response.id,
});
return responseNote;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {