chore: Comprehensive Cache Optimization & Performance Enhancement (#5926)

Co-authored-by: Piyush Gupta <piyushguptaa2z123@gmail.com>
This commit is contained in:
Matti Nannt
2025-06-04 20:33:17 +02:00
committed by GitHub
parent 45fec0e184
commit c0b8edfdf2
318 changed files with 7388 additions and 12603 deletions
@@ -1,7 +1,3 @@
import { cache } from "@/lib/cache";
import { contactCache } from "@/lib/cache/contact";
import { contactAttributeCache } from "@/lib/cache/contact-attribute";
import { contactAttributeKeyCache } from "@/lib/cache/contact-attribute-key";
import { TContactAttributeKeyUpdateSchema } from "@/modules/api/v2/management/contact-attribute-keys/[contactAttributeKeyId]/types/contact-attribute-keys";
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
import { ContactAttributeKey } from "@prisma/client";
@@ -11,37 +7,29 @@ import { prisma } from "@formbricks/database";
import { PrismaErrorType } from "@formbricks/database/types/error";
import { Result, err, ok } from "@formbricks/types/error-handlers";
export const getContactAttributeKey = reactCache(async (contactAttributeKeyId: string) =>
cache(
async (): Promise<Result<ContactAttributeKey, ApiErrorResponseV2>> => {
try {
const contactAttributeKey = await prisma.contactAttributeKey.findUnique({
where: {
id: contactAttributeKeyId,
},
});
export const getContactAttributeKey = reactCache(async (contactAttributeKeyId: string) => {
try {
const contactAttributeKey = await prisma.contactAttributeKey.findUnique({
where: {
id: contactAttributeKeyId,
},
});
if (!contactAttributeKey) {
return err({
type: "not_found",
details: [{ field: "contactAttributeKey", issue: "not found" }],
});
}
return ok(contactAttributeKey);
} catch (error) {
return err({
type: "internal_server_error",
details: [{ field: "contactAttributeKey", issue: error.message }],
});
}
},
[`management-getContactAttributeKey-${contactAttributeKeyId}`],
{
tags: [contactAttributeKeyCache.tag.byId(contactAttributeKeyId)],
if (!contactAttributeKey) {
return err({
type: "not_found",
details: [{ field: "contactAttributeKey", issue: "not found" }],
});
}
)()
);
return ok(contactAttributeKey);
} catch (error) {
return err({
type: "internal_server_error",
details: [{ field: "contactAttributeKey", issue: error.message }],
});
}
});
export const updateContactAttributeKey = async (
contactAttributeKeyId: string,
@@ -55,7 +43,7 @@ export const updateContactAttributeKey = async (
data: contactAttributeKeyInput,
});
const associatedContactAttributes = await prisma.contactAttribute.findMany({
await prisma.contactAttribute.findMany({
where: {
attributeKeyId: updatedKey.id,
},
@@ -65,29 +53,6 @@ export const updateContactAttributeKey = async (
},
});
contactAttributeKeyCache.revalidate({
id: contactAttributeKeyId,
environmentId: updatedKey.environmentId,
key: updatedKey.key,
});
contactAttributeCache.revalidate({
key: updatedKey.key,
environmentId: updatedKey.environmentId,
});
contactCache.revalidate({
environmentId: updatedKey.environmentId,
});
associatedContactAttributes.forEach((contactAttribute) => {
contactAttributeCache.revalidate({
contactId: contactAttribute.contactId,
});
contactCache.revalidate({
id: contactAttribute.contactId,
});
});
return ok(updatedKey);
} catch (error) {
if (error instanceof PrismaClientKnownRequestError) {
@@ -129,7 +94,7 @@ export const deleteContactAttributeKey = async (
},
});
const associatedContactAttributes = await prisma.contactAttribute.findMany({
await prisma.contactAttribute.findMany({
where: {
attributeKeyId: deletedKey.id,
},
@@ -139,29 +104,6 @@ export const deleteContactAttributeKey = async (
},
});
contactAttributeKeyCache.revalidate({
id: contactAttributeKeyId,
environmentId: deletedKey.environmentId,
key: deletedKey.key,
});
contactAttributeCache.revalidate({
key: deletedKey.key,
environmentId: deletedKey.environmentId,
});
contactCache.revalidate({
environmentId: deletedKey.environmentId,
});
associatedContactAttributes.forEach((contactAttribute) => {
contactAttributeCache.revalidate({
contactId: contactAttribute.contactId,
});
contactCache.revalidate({
id: contactAttribute.contactId,
});
});
return ok(deletedKey);
} catch (error) {
if (error instanceof PrismaClientKnownRequestError) {
@@ -1,4 +1,3 @@
import { contactAttributeKeyCache } from "@/lib/cache/contact-attribute-key";
import { TContactAttributeKeyUpdateSchema } from "@/modules/api/v2/management/contact-attribute-keys/[contactAttributeKeyId]/types/contact-attribute-keys";
import { ContactAttributeKey } from "@prisma/client";
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
@@ -26,15 +25,6 @@ vi.mock("@formbricks/database", () => ({
},
}));
vi.mock("@/lib/cache/contact-attribute-key", () => ({
contactAttributeKeyCache: {
tag: {
byId: () => "mockTag",
},
revalidate: vi.fn(),
},
}));
// Mock data
const mockContactAttributeKey: ContactAttributeKey = {
id: "cak123",
@@ -118,12 +108,6 @@ describe("updateContactAttributeKey", () => {
if (result.ok) {
expect(result.data).toEqual(updatedKey);
}
expect(contactAttributeKeyCache.revalidate).toHaveBeenCalledWith({
id: "cak123",
environmentId: mockContactAttributeKey.environmentId,
key: mockUpdateInput.key,
});
});
test("returns not_found if record does not exist", async () => {
@@ -184,12 +168,6 @@ describe("deleteContactAttributeKey", () => {
if (result.ok) {
expect(result.data).toEqual(mockContactAttributeKey);
}
expect(contactAttributeKeyCache.revalidate).toHaveBeenCalledWith({
id: "cak123",
environmentId: mockContactAttributeKey.environmentId,
key: mockContactAttributeKey.key,
});
});
test("returns not_found if record does not exist", async () => {
@@ -10,6 +10,7 @@ import {
ZContactAttributeKeyIdSchema,
ZContactAttributeKeyUpdateSchema,
} from "@/modules/api/v2/management/contact-attribute-keys/[contactAttributeKeyId]/types/contact-attribute-keys";
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils";
import { NextRequest } from "next/server";
import { z } from "zod";
@@ -30,7 +31,7 @@ export const GET = async (
const res = await getContactAttributeKey(params.contactAttributeKeyId);
if (!res.ok) {
return handleApiError(request, res.error);
return handleApiError(request, res.error as ApiErrorResponseV2);
}
if (!hasPermission(authentication.environmentPermissions, res.data.environmentId, "GET")) {
@@ -61,7 +62,7 @@ export const PUT = async (
const res = await getContactAttributeKey(params.contactAttributeKeyId);
if (!res.ok) {
return handleApiError(request, res.error);
return handleApiError(request, res.error as ApiErrorResponseV2);
}
if (!hasPermission(authentication.environmentPermissions, res.data.environmentId, "PUT")) {
return handleApiError(request, {
@@ -80,7 +81,7 @@ export const PUT = async (
const updatedContactAttributeKey = await updateContactAttributeKey(params.contactAttributeKeyId, body);
if (!updatedContactAttributeKey.ok) {
return handleApiError(request, updatedContactAttributeKey.error);
return handleApiError(request, updatedContactAttributeKey.error as ApiErrorResponseV2);
}
return responses.successResponse(updatedContactAttributeKey);
@@ -103,7 +104,7 @@ export const DELETE = async (
const res = await getContactAttributeKey(params.contactAttributeKeyId);
if (!res.ok) {
return handleApiError(request, res.error);
return handleApiError(request, res.error as ApiErrorResponseV2);
}
if (!hasPermission(authentication.environmentPermissions, res.data.environmentId, "DELETE")) {
@@ -123,7 +124,7 @@ export const DELETE = async (
const deletedContactAttributeKey = await deleteContactAttributeKey(params.contactAttributeKeyId);
if (!deletedContactAttributeKey.ok) {
return handleApiError(request, deletedContactAttributeKey.error);
return handleApiError(request, deletedContactAttributeKey.error as ApiErrorResponseV2);
}
return responses.successResponse(deletedContactAttributeKey);
@@ -1,12 +1,9 @@
import { cache } from "@/lib/cache";
import { contactAttributeKeyCache } from "@/lib/cache/contact-attribute-key";
import { getContactAttributeKeysQuery } from "@/modules/api/v2/management/contact-attribute-keys/lib/utils";
import {
TContactAttributeKeyInput,
TGetContactAttributeKeysFilter,
} from "@/modules/api/v2/management/contact-attribute-keys/types/contact-attribute-keys";
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
import { ApiResponseWithMeta } from "@/modules/api/v2/types/api-success";
import { ContactAttributeKey, Prisma } from "@prisma/client";
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
import { cache as reactCache } from "react";
@@ -15,36 +12,27 @@ import { PrismaErrorType } from "@formbricks/database/types/error";
import { Result, err, ok } from "@formbricks/types/error-handlers";
export const getContactAttributeKeys = reactCache(
async (environmentIds: string[], params: TGetContactAttributeKeysFilter) =>
cache(
async (): Promise<Result<ApiResponseWithMeta<ContactAttributeKey[]>, ApiErrorResponseV2>> => {
try {
const query = getContactAttributeKeysQuery(environmentIds, params);
async (environmentIds: string[], params: TGetContactAttributeKeysFilter) => {
try {
const query = getContactAttributeKeysQuery(environmentIds, params);
const [keys, count] = await prisma.$transaction([
prisma.contactAttributeKey.findMany({
...query,
}),
prisma.contactAttributeKey.count({
where: query.where,
}),
]);
const [keys, count] = await prisma.$transaction([
prisma.contactAttributeKey.findMany({
...query,
}),
prisma.contactAttributeKey.count({
where: query.where,
}),
]);
return ok({ data: keys, meta: { total: count, limit: params.limit, offset: params.skip } });
} catch (error) {
return err({
type: "internal_server_error",
details: [{ field: "contactAttributeKeys", issue: error.message }],
});
}
},
[`management-getContactAttributeKeys-${environmentIds.join(",")}-${JSON.stringify(params)}`],
{
tags: environmentIds.map((environmentId) =>
contactAttributeKeyCache.tag.byEnvironmentId(environmentId)
),
}
)()
return ok({ data: keys, meta: { total: count, limit: params.limit, offset: params.skip } });
} catch (error) {
return err({
type: "internal_server_error",
details: [{ field: "contactAttributeKeys", issue: error.message }],
});
}
}
);
export const createContactAttributeKey = async (
@@ -68,11 +56,6 @@ export const createContactAttributeKey = async (
data: prismaData,
});
contactAttributeKeyCache.revalidate({
environmentId: createdContactAttributeKey.environmentId,
key: createdContactAttributeKey.key,
});
return ok(createdContactAttributeKey);
} catch (error) {
if (error instanceof PrismaClientKnownRequestError) {
@@ -1,4 +1,3 @@
import { contactAttributeKeyCache } from "@/lib/cache/contact-attribute-key";
import {
TContactAttributeKeyInput,
TGetContactAttributeKeysFilter,
@@ -20,14 +19,6 @@ vi.mock("@formbricks/database", () => ({
},
},
}));
vi.mock("@/lib/cache/contact-attribute-key", () => ({
contactAttributeKeyCache: {
revalidate: vi.fn(),
tag: {
byEnvironmentId: vi.fn(),
},
},
}));
describe("getContactAttributeKeys", () => {
const environmentIds = ["env1", "env2"];
@@ -96,10 +87,6 @@ describe("createContactAttributeKey", () => {
const result = await createContactAttributeKey(inputContactAttributeKey);
expect(prisma.contactAttributeKey.create).toHaveBeenCalled();
expect(contactAttributeKeyCache.revalidate).toHaveBeenCalledWith({
environmentId: createdContactAttributeKey.environmentId,
key: createdContactAttributeKey.key,
});
expect(result.ok).toBe(true);
if (result.ok) {
@@ -9,6 +9,7 @@ import {
ZContactAttributeKeyInput,
ZGetContactAttributeKeysFilter,
} from "@/modules/api/v2/management/contact-attribute-keys/types/contact-attribute-keys";
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils";
import { NextRequest } from "next/server";
@@ -37,7 +38,7 @@ export const GET = async (request: NextRequest) =>
const res = await getContactAttributeKeys(environmentIds, query);
if (!res.ok) {
return handleApiError(request, res.error);
return handleApiError(request, res.error as ApiErrorResponseV2);
}
return responses.successResponse(res.data);
@@ -65,7 +66,7 @@ export const POST = async (request: NextRequest) =>
const createContactAttributeKeyResult = await createContactAttributeKey(body);
if (!createContactAttributeKeyResult.ok) {
return handleApiError(request, createContactAttributeKeyResult.error);
return handleApiError(request, createContactAttributeKeyResult.error as ApiErrorResponseV2);
}
return responses.createdResponse(createContactAttributeKeyResult);
@@ -12,7 +12,7 @@ export const getEnvironmentId = async (
const result = await fetchEnvironmentId(id, isResponseId);
if (!result.ok) {
return result;
return { ok: false, error: result.error as ApiErrorResponseV2 };
}
return ok(result.data.environmentId);
@@ -29,7 +29,7 @@ export const getEnvironmentIdFromSurveyIds = async (
const result = await fetchEnvironmentIdFromSurveyIds(surveyIds);
if (!result.ok) {
return result;
return { ok: false, error: result.error as ApiErrorResponseV2 };
}
// Check if all items in the array are the same
@@ -1,76 +1,55 @@
"use server";
import { cache } from "@/lib/cache";
import { responseCache } from "@/lib/response/cache";
import { responseNoteCache } from "@/lib/responseNote/cache";
import { surveyCache } from "@/lib/survey/cache";
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { Result, err, ok } from "@formbricks/types/error-handlers";
import { err, ok } from "@formbricks/types/error-handlers";
export const fetchEnvironmentId = reactCache(async (id: string, isResponseId: boolean) =>
cache(
async (): Promise<Result<{ environmentId: string }, ApiErrorResponseV2>> => {
try {
const result = await prisma.survey.findFirst({
where: isResponseId ? { responses: { some: { id } } } : { id },
select: {
environmentId: true,
},
});
export const fetchEnvironmentId = reactCache(async (id: string, isResponseId: boolean) => {
try {
const result = await prisma.survey.findFirst({
where: isResponseId ? { responses: { some: { id } } } : { id },
select: {
environmentId: true,
},
});
if (!result) {
return err({
type: "not_found",
details: [{ field: isResponseId ? "response" : "survey", issue: "not found" }],
});
}
return ok({ environmentId: result.environmentId });
} catch (error) {
return err({
type: "internal_server_error",
details: [{ field: isResponseId ? "response" : "survey", issue: error.message }],
});
}
},
[`services-getEnvironmentId-${id}-${isResponseId}`],
{
tags: [responseCache.tag.byId(id), responseNoteCache.tag.byResponseId(id), surveyCache.tag.byId(id)],
if (!result) {
return err({
type: "not_found",
details: [{ field: isResponseId ? "response" : "survey", issue: "not found" }],
});
}
)()
);
export const fetchEnvironmentIdFromSurveyIds = reactCache(async (surveyIds: string[]) =>
cache(
async (): Promise<Result<string[], ApiErrorResponseV2>> => {
try {
const results = await prisma.survey.findMany({
where: { id: { in: surveyIds } },
select: {
environmentId: true,
},
});
return ok({ environmentId: result.environmentId });
} catch (error) {
return err({
type: "internal_server_error",
details: [{ field: isResponseId ? "response" : "survey", issue: error.message }],
});
}
});
if (results.length !== surveyIds.length) {
return err({
type: "not_found",
details: [{ field: "survey", issue: "not found" }],
});
}
export const fetchEnvironmentIdFromSurveyIds = reactCache(async (surveyIds: string[]) => {
try {
const results = await prisma.survey.findMany({
where: { id: { in: surveyIds } },
select: {
environmentId: true,
},
});
return ok(results.map((result) => result.environmentId));
} catch (error) {
return err({
type: "internal_server_error",
details: [{ field: "survey", issue: error.message }],
});
}
},
[`services-fetchEnvironmentIdFromSurveyIds-${surveyIds.join("-")}`],
{
tags: surveyIds.map((surveyId) => surveyCache.tag.byId(surveyId)),
if (results.length !== surveyIds.length) {
return err({
type: "not_found",
details: [{ field: "survey", issue: "not found" }],
});
}
)()
);
return ok(results.map((result) => result.environmentId));
} catch (error) {
return err({
type: "internal_server_error",
details: [{ field: "survey", issue: error.message }],
});
}
});
@@ -1,4 +1,3 @@
import { displayCache } from "@/lib/display/cache";
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
import { prisma } from "@formbricks/database";
@@ -7,7 +6,7 @@ import { Result, err, ok } from "@formbricks/types/error-handlers";
export const deleteDisplay = async (displayId: string): Promise<Result<boolean, ApiErrorResponseV2>> => {
try {
const display = await prisma.display.delete({
await prisma.display.delete({
where: {
id: displayId,
},
@@ -18,12 +17,6 @@ export const deleteDisplay = async (displayId: string): Promise<Result<boolean,
},
});
displayCache.revalidate({
id: display.id,
contactId: display.contactId,
surveyId: display.surveyId,
});
return ok(true);
} catch (error) {
if (error instanceof PrismaClientKnownRequestError) {
@@ -1,6 +1,3 @@
import { cache } from "@/lib/cache";
import { responseCache } from "@/lib/response/cache";
import { responseNoteCache } from "@/lib/responseNote/cache";
import { deleteDisplay } from "@/modules/api/v2/management/responses/[responseId]/lib/display";
import { getSurveyQuestions } from "@/modules/api/v2/management/responses/[responseId]/lib/survey";
import { findAndDeleteUploadedFilesInResponse } from "@/modules/api/v2/management/responses/[responseId]/lib/utils";
@@ -14,34 +11,26 @@ import { prisma } from "@formbricks/database";
import { PrismaErrorType } from "@formbricks/database/types/error";
import { Result, err, ok } from "@formbricks/types/error-handlers";
export const getResponse = reactCache(async (responseId: string) =>
cache(
async (): Promise<Result<Response, ApiErrorResponseV2>> => {
try {
const responsePrisma = await prisma.response.findUnique({
where: {
id: responseId,
},
});
export const getResponse = reactCache(async (responseId: string) => {
try {
const responsePrisma = await prisma.response.findUnique({
where: {
id: responseId,
},
});
if (!responsePrisma) {
return err({ type: "not_found", details: [{ field: "response", issue: "not found" }] });
}
return ok(responsePrisma);
} catch (error) {
return err({
type: "internal_server_error",
details: [{ field: "response", issue: error.message }],
});
}
},
[`management-getResponse-${responseId}`],
{
tags: [responseCache.tag.byId(responseId), responseNoteCache.tag.byResponseId(responseId)],
if (!responsePrisma) {
return err({ type: "not_found", details: [{ field: "response", issue: "not found" }] });
}
)()
);
return ok(responsePrisma);
} catch (error) {
return err({
type: "internal_server_error",
details: [{ field: "response", issue: error.message }],
});
}
});
export const deleteResponse = async (responseId: string): Promise<Result<Response, ApiErrorResponseV2>> => {
try {
@@ -60,21 +49,11 @@ export const deleteResponse = async (responseId: string): Promise<Result<Respons
const surveyQuestionsResult = await getSurveyQuestions(deletedResponse.surveyId);
if (!surveyQuestionsResult.ok) {
return surveyQuestionsResult;
return { ok: false, error: surveyQuestionsResult.error as ApiErrorResponseV2 };
}
await findAndDeleteUploadedFilesInResponse(deletedResponse.data, surveyQuestionsResult.data.questions);
responseCache.revalidate({
environmentId: surveyQuestionsResult.data.environmentId,
id: deletedResponse.id,
surveyId: deletedResponse.surveyId,
});
responseNoteCache.revalidate({
responseId: deletedResponse.id,
});
return ok(deletedResponse);
} catch (error) {
if (error instanceof PrismaClientKnownRequestError) {
@@ -108,16 +87,6 @@ export const updateResponse = async (
data: responseInput,
});
responseCache.revalidate({
id: updatedResponse.id,
surveyId: updatedResponse.surveyId,
...(updatedResponse.singleUseId ? { singleUseId: updatedResponse.singleUseId } : {}),
});
responseNoteCache.revalidate({
responseId: updatedResponse.id,
});
return ok(updatedResponse);
} catch (error) {
if (error instanceof PrismaClientKnownRequestError) {
@@ -1,37 +1,25 @@
import { cache } from "@/lib/cache";
import { surveyCache } from "@/lib/survey/cache";
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
import { Survey } from "@prisma/client";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { Result, err, ok } from "@formbricks/types/error-handlers";
import { err, ok } from "@formbricks/types/error-handlers";
export const getSurveyQuestions = reactCache(async (surveyId: string) =>
cache(
async (): Promise<Result<Pick<Survey, "questions" | "environmentId">, ApiErrorResponseV2>> => {
try {
const survey = await prisma.survey.findUnique({
where: {
id: surveyId,
},
select: {
environmentId: true,
questions: true,
},
});
export const getSurveyQuestions = reactCache(async (surveyId: string) => {
try {
const survey = await prisma.survey.findUnique({
where: {
id: surveyId,
},
select: {
environmentId: true,
questions: true,
},
});
if (!survey) {
return err({ type: "not_found", details: [{ field: "survey", issue: "not found" }] });
}
return ok(survey);
} catch (error) {
return err({ type: "internal_server_error", details: [{ field: "survey", issue: error.message }] });
}
},
[`management-getSurveyQuestions-${surveyId}`],
{
tags: [surveyCache.tag.byId(surveyId)],
if (!survey) {
return err({ type: "not_found", details: [{ field: "survey", issue: "not found" }] });
}
)()
);
return ok(survey);
} catch (error) {
return err({ type: "internal_server_error", details: [{ field: "survey", issue: error.message }] });
}
});
@@ -1,5 +1,4 @@
import { response, responseId, responseInput, survey } from "./__mocks__/response.mock";
import { responseCache } from "@/lib/response/cache";
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { prisma } from "@formbricks/database";
@@ -22,16 +21,6 @@ vi.mock("../utils", () => ({
findAndDeleteUploadedFilesInResponse: vi.fn(),
}));
vi.mock("@/lib/response/cache", () => ({
responseCache: {
revalidate: vi.fn(),
tag: {
byId: vi.fn(),
byResponseId: vi.fn(),
},
},
}));
vi.mock("@formbricks/database", () => ({
prisma: {
response: {
@@ -195,12 +184,6 @@ describe("Response Lib", () => {
data: responseInput,
});
expect(responseCache.revalidate).toHaveBeenCalledWith({
id: response.id,
surveyId: response.surveyId,
singleUseId: response.singleUseId,
});
expect(result.ok).toBe(true);
if (result.ok) {
expect(result.data).toEqual(response);
@@ -217,11 +200,6 @@ describe("Response Lib", () => {
data: responseInput,
});
expect(responseCache.revalidate).toHaveBeenCalledWith({
id: response.id,
surveyId: response.surveyId,
});
expect(result.ok).toBe(true);
if (result.ok) {
expect(result.data).toEqual(responseWithoutSingleUseId);
@@ -10,6 +10,7 @@ import {
updateResponse,
} from "@/modules/api/v2/management/responses/[responseId]/lib/response";
import { getSurveyQuestions } from "@/modules/api/v2/management/responses/[responseId]/lib/survey";
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils";
import { z } from "zod";
import { ZResponseIdSchema, ZResponseUpdateSchema } from "./types/responses";
@@ -44,7 +45,7 @@ export const GET = async (request: Request, props: { params: Promise<{ responseI
const response = await getResponse(params.responseId);
if (!response.ok) {
return handleApiError(request, response.error);
return handleApiError(request, response.error as ApiErrorResponseV2);
}
return responses.successResponse(response);
@@ -82,7 +83,7 @@ export const DELETE = async (request: Request, props: { params: Promise<{ respon
const response = await deleteResponse(params.responseId);
if (!response.ok) {
return handleApiError(request, response.error);
return handleApiError(request, response.error as ApiErrorResponseV2);
}
return responses.successResponse(response);
@@ -121,13 +122,13 @@ export const PUT = (request: Request, props: { params: Promise<{ responseId: str
const existingResponse = await getResponse(params.responseId);
if (!existingResponse.ok) {
return handleApiError(request, existingResponse.error);
return handleApiError(request, existingResponse.error as ApiErrorResponseV2);
}
const questionsResponse = await getSurveyQuestions(existingResponse.data.surveyId);
if (!questionsResponse.ok) {
return handleApiError(request, questionsResponse.error);
return handleApiError(request, questionsResponse.error as ApiErrorResponseV2);
}
if (!validateFileUploads(body.data, questionsResponse.data.questions)) {
@@ -162,7 +163,7 @@ export const PUT = (request: Request, props: { params: Promise<{ responseId: str
const response = await updateResponse(params.responseId, body);
if (!response.ok) {
return handleApiError(request, response.error);
return handleApiError(request, response.error as ApiErrorResponseV2);
}
return responses.successResponse(response);
@@ -1,172 +1,136 @@
import { cache } from "@/lib/cache";
import { organizationCache } from "@/lib/organization/cache";
import { getBillingPeriodStartDate } from "@/lib/utils/billing";
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
import { Organization } from "@prisma/client";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { Result, err, ok } from "@formbricks/types/error-handlers";
import { err, ok } from "@formbricks/types/error-handlers";
export const getOrganizationIdFromEnvironmentId = reactCache(async (environmentId: string) =>
cache(
async (): Promise<Result<string, ApiErrorResponseV2>> => {
try {
const organization = await prisma.organization.findFirst({
where: {
projects: {
export const getOrganizationIdFromEnvironmentId = reactCache(async (environmentId: string) => {
try {
const organization = await prisma.organization.findFirst({
where: {
projects: {
some: {
environments: {
some: {
environments: {
some: {
id: environmentId,
},
},
id: environmentId,
},
},
},
select: {
id: true,
},
});
},
},
select: {
id: true,
},
});
if (!organization) {
return err({ type: "not_found", details: [{ field: "organization", issue: "not found" }] });
}
return ok(organization.id);
} catch (error) {
return err({
type: "internal_server_error",
details: [{ field: "organization", issue: error.message }],
});
}
},
[`management-getOrganizationIdFromEnvironmentId-${environmentId}`],
{
tags: [organizationCache.tag.byEnvironmentId(environmentId)],
if (!organization) {
return err({ type: "not_found", details: [{ field: "organization", issue: "not found" }] });
}
)()
);
export const getOrganizationBilling = reactCache(async (organizationId: string) =>
cache(
async (): Promise<Result<Organization["billing"], ApiErrorResponseV2>> => {
try {
const organization = await prisma.organization.findFirst({
where: {
id: organizationId,
},
select: {
billing: true,
},
});
return ok(organization.id);
} catch (error) {
return err({
type: "internal_server_error",
details: [{ field: "organization", issue: error.message }],
});
}
});
if (!organization) {
return err({ type: "not_found", details: [{ field: "organization", issue: "not found" }] });
}
export const getOrganizationBilling = reactCache(async (organizationId: string) => {
try {
const organization = await prisma.organization.findFirst({
where: {
id: organizationId,
},
select: {
billing: true,
},
});
return ok(organization.billing);
} catch (error) {
return err({
type: "internal_server_error",
details: [{ field: "organization", issue: error.message }],
});
}
},
[`management-getOrganizationBilling-${organizationId}`],
{
tags: [organizationCache.tag.byId(organizationId)],
if (!organization) {
return err({ type: "not_found", details: [{ field: "organization", issue: "not found" }] });
}
)()
);
export const getAllEnvironmentsFromOrganizationId = reactCache(async (organizationId: string) =>
cache(
async (): Promise<Result<string[], ApiErrorResponseV2>> => {
try {
const organization = await prisma.organization.findUnique({
where: {
id: organizationId,
},
return ok(organization.billing);
} catch (error) {
return err({
type: "internal_server_error",
details: [{ field: "organization", issue: error.message }],
});
}
});
export const getAllEnvironmentsFromOrganizationId = reactCache(async (organizationId: string) => {
try {
const organization = await prisma.organization.findUnique({
where: {
id: organizationId,
},
select: {
projects: {
select: {
projects: {
environments: {
select: {
environments: {
select: {
id: true,
},
},
id: true,
},
},
},
});
},
},
});
if (!organization) {
return err({ type: "not_found", details: [{ field: "organization", issue: "not found" }] });
}
const environmentIds = organization.projects
.flatMap((project) => project.environments)
.map((environment) => environment.id);
return ok(environmentIds);
} catch (error) {
return err({
type: "internal_server_error",
details: [{ field: "organization", issue: error.message }],
});
}
},
[`management-getAllEnvironmentsFromOrganizationId-${organizationId}`],
{
tags: [organizationCache.tag.byId(organizationId)],
if (!organization) {
return err({ type: "not_found", details: [{ field: "organization", issue: "not found" }] });
}
)()
);
export const getMonthlyOrganizationResponseCount = reactCache(async (organizationId: string) =>
cache(
async (): Promise<Result<number, ApiErrorResponseV2>> => {
try {
const billing = await getOrganizationBilling(organizationId);
if (!billing.ok) {
return err(billing.error);
}
const environmentIds = organization.projects
.flatMap((project) => project.environments)
.map((environment) => environment.id);
// Determine the start date based on the plan type
const startDate = getBillingPeriodStartDate(billing.data);
return ok(environmentIds);
} catch (error) {
return err({
type: "internal_server_error",
details: [{ field: "organization", issue: error.message }],
});
}
});
// Get all environment IDs for the organization
const environmentIdsResult = await getAllEnvironmentsFromOrganizationId(organizationId);
if (!environmentIdsResult.ok) {
return err(environmentIdsResult.error);
}
// Use Prisma's aggregate to count responses for all environments
const responseAggregations = await prisma.response.aggregate({
_count: {
id: true,
},
where: {
AND: [
{ survey: { environmentId: { in: environmentIdsResult.data } } },
{ createdAt: { gte: startDate } },
],
},
});
// The result is an aggregation of the total count
return ok(responseAggregations._count.id);
} catch (error) {
return err({
type: "internal_server_error",
details: [{ field: "organization", issue: error.message }],
});
}
},
[`management-getMonthlyOrganizationResponseCount-${organizationId}`],
{
revalidate: 60 * 60 * 2, // 2 hours
export const getMonthlyOrganizationResponseCount = reactCache(async (organizationId: string) => {
try {
const billing = await getOrganizationBilling(organizationId);
if (!billing.ok) {
return err(billing.error);
}
)()
);
// Determine the start date based on the plan type
const startDate = getBillingPeriodStartDate(billing.data);
// Get all environment IDs for the organization
const environmentIdsResult = await getAllEnvironmentsFromOrganizationId(organizationId);
if (!environmentIdsResult.ok) {
return err(environmentIdsResult.error);
}
// Use Prisma's aggregate to count responses for all environments
const responseAggregations = await prisma.response.aggregate({
_count: {
id: true,
},
where: {
AND: [
{ survey: { environmentId: { in: environmentIdsResult.data } } },
{ createdAt: { gte: startDate } },
],
},
});
// The result is an aggregation of the total count
return ok(responseAggregations._count.id);
} catch (error) {
return err({
type: "internal_server_error",
details: [{ field: "organization", issue: error.message }],
});
}
});
@@ -1,9 +1,7 @@
import "server-only";
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
import { sendPlanLimitsReachedEventToPosthogWeekly } from "@/lib/posthogServer";
import { responseCache } from "@/lib/response/cache";
import { calculateTtcTotal } from "@/lib/response/utils";
import { responseNoteCache } from "@/lib/responseNote/cache";
import { captureTelemetry } from "@/lib/telemetry";
import {
getMonthlyOrganizationResponseCount,
@@ -71,12 +69,12 @@ export const createResponse = async (
const organizationIdResult = await getOrganizationIdFromEnvironmentId(environmentId);
if (!organizationIdResult.ok) {
return err(organizationIdResult.error);
return err(organizationIdResult.error as ApiErrorResponseV2);
}
const billing = await getOrganizationBilling(organizationIdResult.data);
if (!billing.ok) {
return err(billing.error);
return err(billing.error as ApiErrorResponseV2);
}
const billingData = billing.data;
@@ -84,21 +82,10 @@ export const createResponse = async (
data: prismaData,
});
responseCache.revalidate({
environmentId,
id: response.id,
...(singleUseId && { singleUseId }),
surveyId,
});
responseNoteCache.revalidate({
responseId: response.id,
});
if (IS_FORMBRICKS_CLOUD) {
const responsesCountResult = await getMonthlyOrganizationResponseCount(organizationIdResult.data);
if (!responsesCountResult.ok) {
return err(responsesCountResult.error);
return err(responsesCountResult.error as ApiErrorResponseV2);
}
const responsesCount = responsesCountResult.data;
@@ -6,6 +6,7 @@ import { handleApiError } from "@/modules/api/v2/lib/utils";
import { getEnvironmentId } from "@/modules/api/v2/management/lib/helper";
import { getSurveyQuestions } from "@/modules/api/v2/management/responses/[responseId]/lib/survey";
import { ZGetResponsesFilter, ZResponseInput } from "@/modules/api/v2/management/responses/types/responses";
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils";
import { Response } from "@prisma/client";
import { NextRequest } from "next/server";
@@ -81,7 +82,7 @@ export const POST = async (request: Request) =>
const surveyQuestions = await getSurveyQuestions(body.surveyId);
if (!surveyQuestions.ok) {
return handleApiError(request, surveyQuestions.error);
return handleApiError(request, surveyQuestions.error as ApiErrorResponseV2);
}
if (!validateFileUploads(body.data, surveyQuestions.data.questions)) {
@@ -1,37 +1,25 @@
import { cache } from "@/lib/cache";
import { contactCache } from "@/lib/cache/contact";
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
import { Contact } from "@prisma/client";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { Result, err, ok } from "@formbricks/types/error-handlers";
import { err, ok } from "@formbricks/types/error-handlers";
export const getContact = reactCache(async (contactId: string, environmentId: string) =>
cache(
async (): Promise<Result<Pick<Contact, "id">, ApiErrorResponseV2>> => {
try {
const contact = await prisma.contact.findUnique({
where: {
id: contactId,
environmentId,
},
select: {
id: true,
},
});
export const getContact = reactCache(async (contactId: string, environmentId: string) => {
try {
const contact = await prisma.contact.findUnique({
where: {
id: contactId,
environmentId,
},
select: {
id: true,
},
});
if (!contact) {
return err({ type: "not_found", details: [{ field: "contact", issue: "not found" }] });
}
return ok(contact);
} catch (error) {
return err({ type: "internal_server_error", details: [{ field: "contact", issue: error.message }] });
}
},
[`contact-link-getContact-${contactId}-${environmentId}`],
{
tags: [contactCache.tag.byId(contactId), contactCache.tag.byEnvironmentId(environmentId)],
if (!contact) {
return err({ type: "not_found", details: [{ field: "contact", issue: "not found" }] });
}
)()
);
return ok(contact);
} catch (error) {
return err({ type: "internal_server_error", details: [{ field: "contact", issue: error.message }] });
}
});
@@ -1,37 +1,25 @@
import { cache } from "@/lib/cache";
import { responseCache } from "@/lib/response/cache";
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
import { Response } from "@prisma/client";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { Result, err, ok } from "@formbricks/types/error-handlers";
import { err, ok } from "@formbricks/types/error-handlers";
export const getResponse = reactCache(async (contactId: string, surveyId: string) =>
cache(
async (): Promise<Result<Pick<Response, "id">, ApiErrorResponseV2>> => {
try {
const response = await prisma.response.findFirst({
where: {
contactId,
surveyId,
},
select: {
id: true,
},
});
export const getResponse = reactCache(async (contactId: string, surveyId: string) => {
try {
const response = await prisma.response.findFirst({
where: {
contactId,
surveyId,
},
select: {
id: true,
},
});
if (!response) {
return err({ type: "not_found", details: [{ field: "response", issue: "not found" }] });
}
return ok(response);
} catch (error) {
return err({ type: "internal_server_error", details: [{ field: "response", issue: error.message }] });
}
},
[`contact-link-getResponse-${contactId}-${surveyId}`],
{
tags: [responseCache.tag.byId(contactId), responseCache.tag.bySurveyId(surveyId)],
if (!response) {
return err({ type: "not_found", details: [{ field: "response", issue: "not found" }] });
}
)()
);
return ok(response);
} catch (error) {
return err({ type: "internal_server_error", details: [{ field: "response", issue: error.message }] });
}
});
@@ -1,35 +1,23 @@
import { cache } from "@/lib/cache";
import { surveyCache } from "@/lib/survey/cache";
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
import { Survey } from "@prisma/client";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { Result, err, ok } from "@formbricks/types/error-handlers";
import { err, ok } from "@formbricks/types/error-handlers";
export const getSurvey = reactCache(async (surveyId: string) =>
cache(
async (): Promise<Result<Pick<Survey, "id" | "type">, ApiErrorResponseV2>> => {
try {
const survey = await prisma.survey.findUnique({
where: { id: surveyId },
select: {
id: true,
type: true,
},
});
export const getSurvey = reactCache(async (surveyId: string) => {
try {
const survey = await prisma.survey.findUnique({
where: { id: surveyId },
select: {
id: true,
type: true,
},
});
if (!survey) {
return err({ type: "not_found", details: [{ field: "survey", issue: "not found" }] });
}
return ok(survey);
} catch (error) {
return err({ type: "internal_server_error", details: [{ field: "survey", issue: error.message }] });
}
},
[`contact-link-getSurvey-${surveyId}`],
{
tags: [surveyCache.tag.byId(surveyId)],
if (!survey) {
return err({ type: "not_found", details: [{ field: "survey", issue: "not found" }] });
}
)()
);
return ok(survey);
} catch (error) {
return err({ type: "internal_server_error", details: [{ field: "survey", issue: error.message }] });
}
});
@@ -9,6 +9,7 @@ import {
TContactLinkParams,
ZContactLinkParams,
} from "@/modules/api/v2/management/surveys/[surveyId]/contact-links/contacts/[contactId]/types/survey";
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
import { getContactSurveyLink } from "@/modules/ee/contacts/lib/contact-survey-link";
import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils";
@@ -46,7 +47,7 @@ export const GET = async (request: Request, props: { params: Promise<TContactLin
const surveyResult = await getSurvey(params.surveyId);
if (!surveyResult.ok) {
return handleApiError(request, surveyResult.error);
return handleApiError(request, surveyResult.error as ApiErrorResponseV2);
}
const survey = surveyResult.data;
@@ -69,7 +70,7 @@ export const GET = async (request: Request, props: { params: Promise<TContactLin
const contactResult = await getContact(params.contactId, environmentId);
if (!contactResult.ok) {
return handleApiError(request, contactResult.error);
return handleApiError(request, contactResult.error as ApiErrorResponseV2);
}
const contact = contactResult.data;
@@ -1,33 +1,22 @@
import { cache } from "@/lib/cache";
import { contactAttributeKeyCache } from "@/lib/cache/contact-attribute-key";
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { Result, err, ok } from "@formbricks/types/error-handlers";
import { err, ok } from "@formbricks/types/error-handlers";
export const getContactAttributeKeys = reactCache((environmentId: string) =>
cache(
async (): Promise<Result<string[], ApiErrorResponseV2>> => {
try {
const contactAttributeKeys = await prisma.contactAttributeKey.findMany({
where: { environmentId },
select: {
key: true,
},
});
export const getContactAttributeKeys = reactCache(async (environmentId: string) => {
try {
const contactAttributeKeys = await prisma.contactAttributeKey.findMany({
where: { environmentId },
select: {
key: true,
},
});
const keys = contactAttributeKeys.map((key) => key.key);
return ok(keys);
} catch (error) {
return err({
type: "internal_server_error",
details: [{ field: "contact attribute keys", issue: error.message }],
});
}
},
[`getContactAttributeKeys-contact-links-${environmentId}`],
{
tags: [contactAttributeKeyCache.tag.byEnvironmentId(environmentId)],
}
)()
);
const keys = contactAttributeKeys.map((key) => key.key);
return ok(keys);
} catch (error) {
return err({
type: "internal_server_error",
details: [{ field: "contact attribute keys", issue: error.message }],
});
}
});
@@ -1,147 +1,135 @@
import { cache } from "@/lib/cache";
import { segmentCache } from "@/lib/cache/segment";
import { surveyCache } from "@/lib/survey/cache";
import { getContactAttributeKeys } from "@/modules/api/v2/management/surveys/[surveyId]/contact-links/segments/[segmentId]/lib/contact-attribute-key";
import { getSegment } from "@/modules/api/v2/management/surveys/[surveyId]/contact-links/segments/[segmentId]/lib/segment";
import { getSurvey } from "@/modules/api/v2/management/surveys/[surveyId]/contact-links/segments/[segmentId]/lib/surveys";
import { TContactWithAttributes } from "@/modules/api/v2/management/surveys/[surveyId]/contact-links/segments/[segmentId]/types/contact";
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
import { ApiResponseWithMeta } from "@/modules/api/v2/types/api-success";
import { segmentFilterToPrismaQuery } from "@/modules/ee/contacts/segments/lib/filter/prisma-query";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { logger } from "@formbricks/logger";
import { Result, err, ok } from "@formbricks/types/error-handlers";
import { err, ok } from "@formbricks/types/error-handlers";
export const getContactsInSegment = reactCache(
(surveyId: string, segmentId: string, limit: number, skip: number, attributeKeys?: string) =>
cache(
async (): Promise<Result<ApiResponseWithMeta<TContactWithAttributes[]>, ApiErrorResponseV2>> => {
try {
const surveyResult = await getSurvey(surveyId);
if (!surveyResult.ok) {
return err(surveyResult.error);
}
async (surveyId: string, segmentId: string, limit: number, skip: number, attributeKeys?: string) => {
try {
const surveyResult = await getSurvey(surveyId);
if (!surveyResult.ok) {
return err(surveyResult.error);
}
const survey = surveyResult.data;
const survey = surveyResult.data;
if (survey.type !== "link" || survey.status !== "inProgress") {
logger.error({ surveyId, segmentId }, "Survey is not a link survey or is not in progress");
const error: ApiErrorResponseV2 = {
type: "forbidden",
details: [{ field: "surveyId", issue: "Invalid survey" }],
};
return err(error);
}
if (survey.type !== "link" || survey.status !== "inProgress") {
logger.error({ surveyId, segmentId }, "Survey is not a link survey or is not in progress");
const error: ApiErrorResponseV2 = {
type: "forbidden",
details: [{ field: "surveyId", issue: "Invalid survey" }],
};
return err(error);
}
const segmentResult = await getSegment(segmentId);
if (!segmentResult.ok) {
return err(segmentResult.error);
}
const segmentResult = await getSegment(segmentId);
if (!segmentResult.ok) {
return err(segmentResult.error);
}
const segment = segmentResult.data;
const segment = segmentResult.data;
if (survey.environmentId !== segment.environmentId) {
logger.error({ surveyId, segmentId }, "Survey and segment are not in the same environment");
const error: ApiErrorResponseV2 = {
type: "bad_request",
details: [{ field: "segmentId", issue: "Environment mismatch" }],
};
return err(error);
}
if (survey.environmentId !== segment.environmentId) {
logger.error({ surveyId, segmentId }, "Survey and segment are not in the same environment");
const error: ApiErrorResponseV2 = {
type: "bad_request",
details: [{ field: "segmentId", issue: "Environment mismatch" }],
};
return err(error);
}
const segmentFilterToPrismaQueryResult = await segmentFilterToPrismaQuery(
segment.id,
segment.filters,
segment.environmentId
);
const segmentFilterToPrismaQueryResult = await segmentFilterToPrismaQuery(
segment.id,
segment.filters,
segment.environmentId
);
if (!segmentFilterToPrismaQueryResult.ok) {
return err(segmentFilterToPrismaQueryResult.error);
}
if (!segmentFilterToPrismaQueryResult.ok) {
return err(segmentFilterToPrismaQueryResult.error);
}
const { whereClause } = segmentFilterToPrismaQueryResult.data;
const { whereClause } = segmentFilterToPrismaQueryResult.data;
const contactAttributeKeysResult = await getContactAttributeKeys(segment.environmentId);
if (!contactAttributeKeysResult.ok) {
return err(contactAttributeKeysResult.error);
}
const contactAttributeKeysResult = await getContactAttributeKeys(segment.environmentId);
if (!contactAttributeKeysResult.ok) {
return err(contactAttributeKeysResult.error);
}
const allAttributeKeys = contactAttributeKeysResult.data;
const allAttributeKeys = contactAttributeKeysResult.data;
const fieldArray = (attributeKeys || "").split(",").map((field) => field.trim());
const attributesToInclude = fieldArray.filter((field) => allAttributeKeys.includes(field));
const fieldArray = (attributeKeys || "").split(",").map((field) => field.trim());
const attributesToInclude = fieldArray.filter((field) => allAttributeKeys.includes(field));
const allowedAttributes = attributesToInclude.slice(0, 20);
const allowedAttributes = attributesToInclude.slice(0, 20);
const [totalContacts, contacts] = await prisma.$transaction([
prisma.contact.count({
where: whereClause,
}),
const [totalContacts, contacts] = await prisma.$transaction([
prisma.contact.count({
where: whereClause,
}),
prisma.contact.findMany({
where: whereClause,
select: {
id: true,
attributes: {
where: {
attributeKey: {
key: {
in: allowedAttributes,
},
},
},
select: {
attributeKey: {
select: {
key: true,
},
},
value: true,
prisma.contact.findMany({
where: whereClause,
select: {
id: true,
attributes: {
where: {
attributeKey: {
key: {
in: allowedAttributes,
},
},
},
take: limit,
skip: skip,
orderBy: {
createdAt: "desc",
select: {
attributeKey: {
select: {
key: true,
},
},
value: true,
},
}),
]);
const contactsWithAttributes = contacts.map((contact) => {
const attributes = contact.attributes.reduce(
(acc, attr) => {
acc[attr.attributeKey.key] = attr.value;
return acc;
},
{} as Record<string, string>
);
return {
contactId: contact.id,
...(Object.keys(attributes).length > 0 ? { attributes } : {}),
};
});
return ok({
data: contactsWithAttributes,
meta: {
total: totalContacts,
limit: limit,
offset: skip,
},
});
} catch (error) {
logger.error({ error, surveyId, segmentId }, "Error getting contacts in segment");
const apiError: ApiErrorResponseV2 = {
type: "internal_server_error",
};
return err(apiError);
}
},
[`getContactsInSegment-${surveyId}-${segmentId}-${attributeKeys}-${limit}-${skip}`],
{
tags: [segmentCache.tag.byId(segmentId), surveyCache.tag.byId(surveyId)],
}
)()
},
take: limit,
skip: skip,
orderBy: {
createdAt: "desc",
},
}),
]);
const contactsWithAttributes = contacts.map((contact) => {
const attributes = contact.attributes.reduce(
(acc, attr) => {
acc[attr.attributeKey.key] = attr.value;
return acc;
},
{} as Record<string, string>
);
return {
contactId: contact.id,
...(Object.keys(attributes).length > 0 ? { attributes } : {}),
};
});
return ok({
data: contactsWithAttributes,
meta: {
total: totalContacts,
limit: limit,
offset: skip,
},
});
} catch (error) {
logger.error({ error, surveyId, segmentId }, "Error getting contacts in segment");
const apiError: ApiErrorResponseV2 = {
type: "internal_server_error",
};
return err(apiError);
}
}
);
@@ -1,36 +1,24 @@
import { cache } from "@/lib/cache";
import { segmentCache } from "@/lib/cache/segment";
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
import { Segment } from "@prisma/client";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { Result, err, ok } from "@formbricks/types/error-handlers";
import { err, ok } from "@formbricks/types/error-handlers";
export const getSegment = reactCache(async (segmentId: string) =>
cache(
async (): Promise<Result<Pick<Segment, "id" | "environmentId" | "filters">, ApiErrorResponseV2>> => {
try {
const segment = await prisma.segment.findUnique({
where: { id: segmentId },
select: {
id: true,
environmentId: true,
filters: true,
},
});
export const getSegment = reactCache(async (segmentId: string) => {
try {
const segment = await prisma.segment.findUnique({
where: { id: segmentId },
select: {
id: true,
environmentId: true,
filters: true,
},
});
if (!segment) {
return err({ type: "not_found", details: [{ field: "segment", issue: "not found" }] });
}
return ok(segment);
} catch (error) {
return err({ type: "internal_server_error", details: [{ field: "segment", issue: error.message }] });
}
},
[`contact-link-getSegment-${segmentId}`],
{
tags: [segmentCache.tag.byId(segmentId)],
if (!segment) {
return err({ type: "not_found", details: [{ field: "segment", issue: "not found" }] });
}
)()
);
return ok(segment);
} catch (error) {
return err({ type: "internal_server_error", details: [{ field: "segment", issue: error.message }] });
}
});
@@ -1,39 +1,25 @@
import { cache } from "@/lib/cache";
import { surveyCache } from "@/lib/survey/cache";
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
import { Survey } from "@prisma/client";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { Result, err, ok } from "@formbricks/types/error-handlers";
import { err, ok } from "@formbricks/types/error-handlers";
export const getSurvey = reactCache(async (surveyId: string) =>
cache(
async (): Promise<
Result<Pick<Survey, "id" | "environmentId" | "type" | "status">, ApiErrorResponseV2>
> => {
try {
const survey = await prisma.survey.findUnique({
where: { id: surveyId },
select: {
id: true,
environmentId: true,
type: true,
status: true,
},
});
export const getSurvey = reactCache(async (surveyId: string) => {
try {
const survey = await prisma.survey.findUnique({
where: { id: surveyId },
select: {
id: true,
environmentId: true,
type: true,
status: true,
},
});
if (!survey) {
return err({ type: "not_found", details: [{ field: "survey", issue: "not found" }] });
}
return ok(survey);
} catch (error) {
return err({ type: "internal_server_error", details: [{ field: "survey", issue: error.message }] });
}
},
[`contact-link-getSurvey-${surveyId}`],
{
tags: [surveyCache.tag.byId(surveyId)],
if (!survey) {
return err({ type: "not_found", details: [{ field: "survey", issue: "not found" }] });
}
)()
);
return ok(survey);
} catch (error) {
return err({ type: "internal_server_error", details: [{ field: "survey", issue: error.message }] });
}
});
@@ -1,5 +1,3 @@
import { cache } from "@/lib/cache";
import { segmentCache } from "@/lib/cache/segment";
import { Segment } from "@prisma/client";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { prisma } from "@formbricks/database";
@@ -14,18 +12,6 @@ vi.mock("@formbricks/database", () => ({
},
}));
vi.mock("@/lib/cache", () => ({
cache: vi.fn((fn) => fn),
}));
vi.mock("@/lib/cache/segment", () => ({
segmentCache: {
tag: {
byId: vi.fn((id) => `segment-${id}`),
},
},
}));
describe("getSegment", () => {
const mockSegmentId = "segment-123";
const mockSegment: Pick<Segment, "id" | "environmentId" | "filters"> = {
@@ -74,8 +60,6 @@ describe("getSegment", () => {
if (result.ok) {
expect(result.data).toEqual(mockSegment);
}
expect(segmentCache.tag.byId).toHaveBeenCalledWith(mockSegmentId);
});
test("should return not_found error when segment doesn't exist", async () => {
@@ -116,14 +100,4 @@ describe("getSegment", () => {
});
}
});
test("should use correct cache key", async () => {
vi.mocked(prisma.segment.findUnique).mockResolvedValueOnce(mockSegment);
await getSegment(mockSegmentId);
expect(cache).toHaveBeenCalledWith(expect.any(Function), [`contact-link-getSegment-${mockSegmentId}`], {
tags: [`segment-${mockSegmentId}`],
});
});
});
@@ -1,5 +1,3 @@
import { cache } from "@/lib/cache";
import { surveyCache } from "@/lib/survey/cache";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { prisma } from "@formbricks/database";
import { getSurvey } from "../surveys";
@@ -13,18 +11,6 @@ vi.mock("@formbricks/database", () => ({
},
}));
vi.mock("@/lib/cache", () => ({
cache: vi.fn((fn) => fn),
}));
vi.mock("@/lib/survey/cache", () => ({
surveyCache: {
tag: {
byId: vi.fn((id) => `survey-${id}`),
},
},
}));
describe("getSurvey", () => {
const mockSurveyId = "survey-123";
const mockEnvironmentId = "env-456";
@@ -60,11 +46,6 @@ describe("getSurvey", () => {
if (result.ok) {
expect(result.data).toEqual(mockSurvey);
}
expect(surveyCache.tag.byId).toHaveBeenCalledWith(mockSurveyId);
expect(cache).toHaveBeenCalledWith(expect.any(Function), [`contact-link-getSurvey-${mockSurveyId}`], {
tags: [`survey-${mockSurveyId}`],
});
});
test("should return not_found error when survey doesn't exist", async () => {
@@ -106,15 +87,4 @@ describe("getSurvey", () => {
});
}
});
test("should use correct cache key and tags", async () => {
vi.mocked(prisma.survey.findUnique).mockResolvedValueOnce(mockSurvey);
await getSurvey(mockSurveyId);
expect(cache).toHaveBeenCalledWith(expect.any(Function), [`contact-link-getSurvey-${mockSurveyId}`], {
tags: [`survey-${mockSurveyId}`],
});
expect(surveyCache.tag.byId).toHaveBeenCalledWith(mockSurveyId);
});
});
@@ -7,6 +7,7 @@ import {
ZContactLinksBySegmentParams,
ZContactLinksBySegmentQuery,
} from "@/modules/api/v2/management/surveys/[surveyId]/contact-links/segments/[segmentId]/types/contact";
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
import { getContactSurveyLink } from "@/modules/ee/contacts/lib/contact-survey-link";
import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils";
import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils";
@@ -67,7 +68,7 @@ export const GET = async (
);
if (!contactsResult.ok) {
return handleApiError(request, contactsResult.error);
return handleApiError(request, contactsResult.error as ApiErrorResponseV2);
}
const { data: contacts, meta } = contactsResult.data;
@@ -1,4 +1,3 @@
import { webhookCache } from "@/lib/cache/webhook";
import {
mockedPrismaWebhookUpdateReturn,
prismaNotFoundError,
@@ -19,15 +18,6 @@ vi.mock("@formbricks/database", () => ({
},
}));
vi.mock("@/lib/cache/webhook", () => ({
webhookCache: {
tag: {
byId: () => "mockTag",
},
revalidate: vi.fn(),
},
}));
describe("getWebhook", () => {
test("returns ok if webhook is found", async () => {
vi.mocked(prisma.webhook.findUnique).mockResolvedValueOnce({ id: "123" });
@@ -71,8 +61,6 @@ describe("updateWebhook", () => {
if (result.ok) {
expect(result.data).toEqual(mockedPrismaWebhookUpdateReturn);
}
expect(webhookCache.revalidate).toHaveBeenCalled();
});
test("returns not_found if record does not exist", async () => {
@@ -101,7 +89,6 @@ describe("deleteWebhook", () => {
vi.mocked(prisma.webhook.delete).mockResolvedValueOnce(mockedPrismaWebhookUpdateReturn);
const result = await deleteWebhook("123");
expect(result.ok).toBe(true);
expect(webhookCache.revalidate).toHaveBeenCalled();
});
test("returns not_found if record does not exist", async () => {
@@ -1,5 +1,3 @@
import { cache } from "@/lib/cache";
import { webhookCache } from "@/lib/cache/webhook";
import { ZWebhookUpdateSchema } from "@/modules/api/v2/management/webhooks/[webhookId]/types/webhooks";
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
import { Webhook } from "@prisma/client";
@@ -9,36 +7,29 @@ import { prisma } from "@formbricks/database";
import { PrismaErrorType } from "@formbricks/database/types/error";
import { Result, err, ok } from "@formbricks/types/error-handlers";
export const getWebhook = async (webhookId: string) =>
cache(
async (): Promise<Result<Webhook, ApiErrorResponseV2>> => {
try {
const webhook = await prisma.webhook.findUnique({
where: {
id: webhookId,
},
});
export const getWebhook = async (webhookId: string) => {
try {
const webhook = await prisma.webhook.findUnique({
where: {
id: webhookId,
},
});
if (!webhook) {
return err({
type: "not_found",
details: [{ field: "webhook", issue: "not found" }],
});
}
return ok(webhook);
} catch (error) {
return err({
type: "internal_server_error",
details: [{ field: "webhook", issue: error.message }],
});
}
},
[`management-getWebhook-${webhookId}`],
{
tags: [webhookCache.tag.byId(webhookId)],
if (!webhook) {
return err({
type: "not_found",
details: [{ field: "webhook", issue: "not found" }],
});
}
)();
return ok(webhook);
} catch (error) {
return err({
type: "internal_server_error",
details: [{ field: "webhook", issue: error.message }],
});
}
};
export const updateWebhook = async (
webhookId: string,
@@ -52,10 +43,6 @@ export const updateWebhook = async (
data: webhookInput,
});
webhookCache.revalidate({
id: webhookId,
});
return ok(updatedWebhook);
} catch (error) {
if (error instanceof PrismaClientKnownRequestError) {
@@ -84,12 +71,6 @@ export const deleteWebhook = async (webhookId: string): Promise<Result<Webhook,
},
});
webhookCache.revalidate({
id: deletedWebhook.id,
environmentId: deletedWebhook.environmentId,
source: deletedWebhook.source,
});
return ok(deletedWebhook);
} catch (error) {
if (error instanceof PrismaClientKnownRequestError) {
@@ -11,6 +11,7 @@ import {
ZWebhookIdSchema,
ZWebhookUpdateSchema,
} from "@/modules/api/v2/management/webhooks/[webhookId]/types/webhooks";
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils";
import { NextRequest } from "next/server";
import { z } from "zod";
@@ -35,7 +36,7 @@ export const GET = async (request: NextRequest, props: { params: Promise<{ webho
const webhook = await getWebhook(params.webhookId);
if (!webhook.ok) {
return handleApiError(request, webhook.error);
return handleApiError(request, webhook.error as ApiErrorResponseV2);
}
if (!hasPermission(authentication.environmentPermissions, webhook.data.environmentId, "GET")) {
@@ -78,7 +79,7 @@ export const PUT = async (request: NextRequest, props: { params: Promise<{ webho
const webhook = await getWebhook(params.webhookId);
if (!webhook.ok) {
return handleApiError(request, webhook.error);
return handleApiError(request, webhook.error as ApiErrorResponseV2);
}
if (!hasPermission(authentication.environmentPermissions, webhook.data.environmentId, "PUT")) {
@@ -101,7 +102,7 @@ export const PUT = async (request: NextRequest, props: { params: Promise<{ webho
const updatedWebhook = await updateWebhook(params.webhookId, body);
if (!updatedWebhook.ok) {
return handleApiError(request, updatedWebhook.error);
return handleApiError(request, updatedWebhook.error as ApiErrorResponseV2);
}
return responses.successResponse(updatedWebhook);
@@ -128,7 +129,7 @@ export const DELETE = async (request: NextRequest, props: { params: Promise<{ we
const webhook = await getWebhook(params.webhookId);
if (!webhook.ok) {
return handleApiError(request, webhook.error);
return handleApiError(request, webhook.error as ApiErrorResponseV2);
}
if (!hasPermission(authentication.environmentPermissions, webhook.data.environmentId, "DELETE")) {
@@ -141,7 +142,7 @@ export const DELETE = async (request: NextRequest, props: { params: Promise<{ we
const deletedWebhook = await deleteWebhook(params.webhookId);
if (!deletedWebhook.ok) {
return handleApiError(request, deletedWebhook.error);
return handleApiError(request, deletedWebhook.error as ApiErrorResponseV2);
}
return responses.successResponse(deletedWebhook);
@@ -1,4 +1,3 @@
import { webhookCache } from "@/lib/cache/webhook";
import { captureTelemetry } from "@/lib/telemetry";
import { TGetWebhooksFilter, TWebhookInput } from "@/modules/api/v2/management/webhooks/types/webhooks";
import { WebhookSource } from "@prisma/client";
@@ -16,11 +15,7 @@ vi.mock("@formbricks/database", () => ({
},
},
}));
vi.mock("@/lib/cache/webhook", () => ({
webhookCache: {
revalidate: vi.fn(),
},
}));
vi.mock("@/lib/telemetry", () => ({
captureTelemetry: vi.fn(),
}));
@@ -87,16 +82,12 @@ describe("createWebhook", () => {
updatedAt: new Date(),
};
test("creates a webhook and revalidates cache", async () => {
test("creates a webhook", async () => {
vi.mocked(prisma.webhook.create).mockResolvedValueOnce(createdWebhook);
const result = await createWebhook(inputWebhook);
expect(captureTelemetry).toHaveBeenCalledWith("webhook_created");
expect(prisma.webhook.create).toHaveBeenCalled();
expect(webhookCache.revalidate).toHaveBeenCalledWith({
environmentId: createdWebhook.environmentId,
source: createdWebhook.source,
});
expect(result.ok).toBe(true);
if (result.ok) {
@@ -1,4 +1,3 @@
import { webhookCache } from "@/lib/cache/webhook";
import { captureTelemetry } from "@/lib/telemetry";
import { getWebhooksQuery } from "@/modules/api/v2/management/webhooks/lib/utils";
import { TGetWebhooksFilter, TWebhookInput } from "@/modules/api/v2/management/webhooks/types/webhooks";
@@ -70,11 +69,6 @@ export const createWebhook = async (webhook: TWebhookInput): Promise<Result<Webh
data: prismaData,
});
webhookCache.revalidate({
environmentId: createdWebhook.environmentId,
source: createdWebhook.source,
});
return ok(createdWebhook);
} catch (error) {
return err({
@@ -1,5 +1,3 @@
import { teamCache } from "@/lib/cache/team";
import { projectCache } from "@/lib/project/cache";
import { captureTelemetry } from "@/lib/telemetry";
import { getProjectTeamsQuery } from "@/modules/api/v2/organizations/[organizationId]/project-teams/lib/utils";
import {
@@ -59,14 +57,6 @@ export const createProjectTeam = async (
},
});
projectCache.revalidate({
id: projectId,
});
teamCache.revalidate({
id: teamId,
});
return ok(projectTeam);
} catch (error) {
return err({ type: "internal_server_error", details: [{ field: "projectTeam", issue: error.message }] });
@@ -89,14 +79,6 @@ export const updateProjectTeam = async (
data: teamInput,
});
projectCache.revalidate({
id: projectId,
});
teamCache.revalidate({
id: teamId,
});
return ok(updatedProjectTeam);
} catch (error) {
return err({ type: "internal_server_error", details: [{ field: "projectTeam", issue: error.message }] });
@@ -117,14 +99,6 @@ export const deleteProjectTeam = async (
},
});
projectCache.revalidate({
id: projectId,
});
teamCache.revalidate({
id: teamId,
});
return ok(deletedProjectTeam);
} catch (error) {
return err({ type: "internal_server_error", details: [{ field: "projectTeam", issue: error.message }] });
@@ -1,7 +1,3 @@
import { cache } from "@/lib/cache";
import { teamCache } from "@/lib/cache/team";
import { organizationCache } from "@/lib/organization/cache";
import { projectCache } from "@/lib/project/cache";
import { buildCommonFilterQuery, pickCommonFilter } from "@/modules/api/v2/management/lib/utils";
import { TGetProjectTeamsFilter } from "@/modules/api/v2/organizations/[organizationId]/project-teams/types/project-teams";
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
@@ -55,47 +51,36 @@ export const getProjectTeamsQuery = (organizationId: string, params: TGetProject
};
export const validateTeamIdAndProjectId = reactCache(
async (organizationId: string, teamId: string, projectId: string) =>
cache(
async (): Promise<Result<boolean, ApiErrorResponseV2>> => {
try {
const hasAccess = await prisma.organization.findFirst({
where: {
id: organizationId,
teams: {
some: {
id: teamId,
},
},
projects: {
some: {
id: projectId,
},
},
async (organizationId: string, teamId: string, projectId: string) => {
try {
const hasAccess = await prisma.organization.findFirst({
where: {
id: organizationId,
teams: {
some: {
id: teamId,
},
});
},
projects: {
some: {
id: projectId,
},
},
},
});
if (!hasAccess) {
return err({ type: "not_found", details: [{ field: "teamId/projectId", issue: "not_found" }] });
}
return ok(true);
} catch (error) {
return err({
type: "internal_server_error",
details: [{ field: "teamId/projectId", issue: error.message }],
});
}
},
[`validateTeamIdAndProjectId-${organizationId}-${teamId}-${projectId}`],
{
tags: [
teamCache.tag.byId(teamId),
projectCache.tag.byId(projectId),
organizationCache.tag.byId(organizationId),
],
if (!hasAccess) {
return err({ type: "not_found", details: [{ field: "teamId/projectId", issue: "not_found" }] });
}
)()
return ok(true);
} catch (error) {
return err({
type: "internal_server_error",
details: [{ field: "teamId/projectId", issue: error.message }],
});
}
}
);
export const checkAuthenticationAndAccess = async (
@@ -106,7 +91,7 @@ export const checkAuthenticationAndAccess = async (
const hasAccess = await validateTeamIdAndProjectId(authentication.organizationId, teamId, projectId);
if (!hasAccess.ok) {
return err(hasAccess.error);
return err(hasAccess.error as ApiErrorResponseV2);
}
return ok(true);
@@ -1,6 +1,3 @@
import { cache } from "@/lib/cache";
import { organizationCache } from "@/lib/cache/organization";
import { teamCache } from "@/lib/cache/team";
import { ZTeamUpdateSchema } from "@/modules/api/v2/organizations/[organizationId]/teams/[teamId]/types/teams";
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
import { Team } from "@prisma/client";
@@ -11,35 +8,27 @@ import { prisma } from "@formbricks/database";
import { PrismaErrorType } from "@formbricks/database/types/error";
import { Result, err, ok } from "@formbricks/types/error-handlers";
export const getTeam = reactCache(async (organizationId: string, teamId: string) =>
cache(
async (): Promise<Result<Team, ApiErrorResponseV2>> => {
try {
const responsePrisma = await prisma.team.findUnique({
where: {
id: teamId,
organizationId,
},
});
export const getTeam = reactCache(async (organizationId: string, teamId: string) => {
try {
const responsePrisma = await prisma.team.findUnique({
where: {
id: teamId,
organizationId,
},
});
if (!responsePrisma) {
return err({ type: "not_found", details: [{ field: "team", issue: "not found" }] });
}
return ok(responsePrisma);
} catch (error) {
return err({
type: "internal_server_error",
details: [{ field: "team", issue: error.message }],
});
}
},
[`organizationId-${organizationId}-getTeam-${teamId}`],
{
tags: [teamCache.tag.byId(teamId), organizationCache.tag.byId(organizationId)],
if (!responsePrisma) {
return err({ type: "not_found", details: [{ field: "team", issue: "not found" }] });
}
)()
);
return ok(responsePrisma);
} catch (error) {
return err({
type: "internal_server_error",
details: [{ field: "team", issue: error.message }],
});
}
});
export const deleteTeam = async (
organizationId: string,
@@ -60,17 +49,6 @@ export const deleteTeam = async (
},
});
teamCache.revalidate({
id: deletedTeam.id,
organizationId: deletedTeam.organizationId,
});
for (const projectTeam of deletedTeam.projectTeams) {
teamCache.revalidate({
projectId: projectTeam.projectId,
});
}
return ok(deletedTeam);
} catch (error) {
if (error instanceof PrismaClientKnownRequestError) {
@@ -109,17 +87,6 @@ export const updateTeam = async (
},
});
teamCache.revalidate({
id: updatedTeam.id,
organizationId: updatedTeam.organizationId,
});
for (const projectTeam of updatedTeam.projectTeams) {
teamCache.revalidate({
projectId: projectTeam.projectId,
});
}
return ok(updatedTeam);
} catch (error) {
if (error instanceof PrismaClientKnownRequestError) {
@@ -1,4 +1,3 @@
import { teamCache } from "@/lib/cache/team";
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
import { describe, expect, test, vi } from "vitest";
import { prisma } from "@formbricks/database";
@@ -54,28 +53,19 @@ describe("Teams Lib", () => {
const result = await getTeam("org456", "team123");
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error.type).toBe("internal_server_error");
expect((result.error as any).type).toBe("internal_server_error");
}
});
});
describe("deleteTeam", () => {
test("deletes the team and revalidates cache", async () => {
test("deletes the team", async () => {
(prisma.team.delete as any).mockResolvedValueOnce(mockTeam);
// Mock teamCache.revalidate
const revalidateMock = vi.spyOn(teamCache, "revalidate").mockImplementation(() => {});
const result = await deleteTeam("org456", "team123");
expect(prisma.team.delete).toHaveBeenCalledWith({
where: { id: "team123", organizationId: "org456" },
include: { projectTeams: { select: { projectId: true } } },
});
expect(revalidateMock).toHaveBeenCalledWith({
id: mockTeam.id,
organizationId: mockTeam.organizationId,
});
for (const pt of mockTeam.projectTeams) {
expect(revalidateMock).toHaveBeenCalledWith({ projectId: pt.projectId });
}
expect(result.ok).toBe(true);
if (result.ok) {
expect(result.data).toEqual(mockTeam);
@@ -105,7 +95,7 @@ describe("Teams Lib", () => {
const result = await deleteTeam("org456", "team123");
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error.type).toBe("internal_server_error");
expect((result.error as any).type).toBe("internal_server_error");
}
});
});
@@ -114,22 +104,14 @@ describe("Teams Lib", () => {
const updateInput = { name: "Updated Team" };
const updatedTeam = { ...mockTeam, ...updateInput };
test("updates the team successfully and revalidates cache", async () => {
test("updates the team successfully", async () => {
(prisma.team.update as any).mockResolvedValueOnce(updatedTeam);
const revalidateMock = vi.spyOn(teamCache, "revalidate").mockImplementation(() => {});
const result = await updateTeam("org456", "team123", updateInput);
expect(prisma.team.update).toHaveBeenCalledWith({
where: { id: "team123", organizationId: "org456" },
data: updateInput,
include: { projectTeams: { select: { projectId: true } } },
});
expect(revalidateMock).toHaveBeenCalledWith({
id: updatedTeam.id,
organizationId: updatedTeam.organizationId,
});
for (const pt of updatedTeam.projectTeams) {
expect(revalidateMock).toHaveBeenCalledWith({ projectId: pt.projectId });
}
expect(result.ok).toBe(true);
if (result.ok) {
expect(result.data).toEqual(updatedTeam);
@@ -159,7 +141,7 @@ describe("Teams Lib", () => {
const result = await updateTeam("org456", "team123", updateInput);
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error.type).toBe("internal_server_error");
expect((result.error as any).type).toBe("internal_server_error");
}
});
});
@@ -12,6 +12,7 @@ import {
ZTeamUpdateSchema,
} from "@/modules/api/v2/organizations/[organizationId]/teams/[teamId]/types/teams";
import { ZOrganizationIdSchema } from "@/modules/api/v2/organizations/[organizationId]/types/organizations";
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
import { z } from "zod";
import { OrganizationAccessType } from "@formbricks/types/api-key";
@@ -35,7 +36,7 @@ export const GET = async (
const team = await getTeam(params!.organizationId, params!.teamId);
if (!team.ok) {
return handleApiError(request, team.error);
return handleApiError(request, team.error as ApiErrorResponseV2);
}
return responses.successResponse(team);
@@ -63,7 +64,7 @@ export const DELETE = async (
const team = await deleteTeam(params!.organizationId, params!.teamId);
if (!team.ok) {
return handleApiError(request, team.error);
return handleApiError(request, team.error as ApiErrorResponseV2);
}
return responses.successResponse(team);
@@ -92,7 +93,7 @@ export const PUT = (
const team = await updateTeam(params!.organizationId, params!.teamId, body!);
if (!team.ok) {
return handleApiError(request, team.error);
return handleApiError(request, team.error as ApiErrorResponseV2);
}
return responses.successResponse(team);
@@ -1,6 +1,4 @@
import "server-only";
import { teamCache } from "@/lib/cache/team";
import { organizationCache } from "@/lib/organization/cache";
import { captureTelemetry } from "@/lib/telemetry";
import { getTeamsQuery } from "@/modules/api/v2/organizations/[organizationId]/teams/lib/utils";
import {
@@ -29,14 +27,6 @@ export const createTeam = async (
},
});
organizationCache.revalidate({
id: organizationId,
});
teamCache.revalidate({
organizationId: organizationId,
});
return ok(team);
} catch (error) {
return err({ type: "internal_server_error", details: [{ field: "team", issue: error.message }] });
@@ -1,4 +1,3 @@
import { organizationCache } from "@/lib/organization/cache";
import { TGetTeamsFilter } from "@/modules/api/v2/organizations/[organizationId]/teams/types/teams";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { prisma } from "@formbricks/database";
@@ -27,9 +26,6 @@ vi.mock("@formbricks/database", () => ({
},
}));
// Mock organizationCache.revalidate
vi.spyOn(organizationCache, "revalidate").mockImplementation(() => {});
describe("Teams Lib", () => {
describe("createTeam", () => {
test("creates a team successfully and revalidates cache", async () => {
@@ -44,7 +40,6 @@ describe("Teams Lib", () => {
organizationId: organizationId,
},
});
expect(organizationCache.revalidate).toHaveBeenCalledWith({ id: organizationId });
expect(result.ok).toBe(true);
if (result.ok) expect(result.data).toEqual(mockTeam);
});
@@ -1,6 +1,3 @@
import { teamCache } from "@/lib/cache/team";
import { membershipCache } from "@/lib/membership/cache";
import { userCache } from "@/lib/user/cache";
import { TGetUsersFilter } from "@/modules/api/v2/organizations/[organizationId]/users/types/users";
import { describe, expect, test, vi } from "vitest";
import { prisma } from "@formbricks/database";
@@ -39,10 +36,6 @@ vi.mock("@formbricks/database", () => ({
},
}));
vi.spyOn(membershipCache, "revalidate").mockImplementation(() => {});
vi.spyOn(userCache, "revalidate").mockImplementation(() => {});
vi.spyOn(teamCache, "revalidate").mockImplementation(() => {});
describe("Users Lib", () => {
describe("getUsers", () => {
test("returns users with meta on success", async () => {
@@ -150,8 +143,6 @@ describe("Users Lib", () => {
);
expect(prisma.user.create).toHaveBeenCalled();
expect(teamCache.revalidate).toHaveBeenCalled();
expect(membershipCache.revalidate).toHaveBeenCalled();
expect(result.ok).toBe(true);
});
});
@@ -182,9 +173,6 @@ describe("Users Lib", () => {
);
expect(prisma.user.findUnique).toHaveBeenCalled();
expect(teamCache.revalidate).toHaveBeenCalledTimes(3);
expect(membershipCache.revalidate).toHaveBeenCalled();
expect(userCache.revalidate).toHaveBeenCalled();
expect(result.ok).toBe(true);
if (result.ok) {
expect(result.data.teams).toContain("NewTeam");
@@ -1,7 +1,4 @@
import { teamCache } from "@/lib/cache/team";
import { membershipCache } from "@/lib/membership/cache";
import { captureTelemetry } from "@/lib/telemetry";
import { userCache } from "@/lib/user/cache";
import { getUsersQuery } from "@/modules/api/v2/organizations/[organizationId]/users/lib/utils";
import {
TGetUsersFilter,
@@ -131,25 +128,6 @@ export const createUser = async (
},
});
existingTeams?.forEach((team) => {
teamCache.revalidate({
id: team.id,
organizationId: organizationId,
});
for (const projectTeam of team.projectTeams) {
teamCache.revalidate({
projectId: projectTeam.projectId,
});
}
});
// revalidate membership cache
membershipCache.revalidate({
organizationId: organizationId,
userId: user.id,
});
const returnedUser = {
id: user.id,
createdAt: user.createdAt,
@@ -298,47 +276,6 @@ export const updateUser = async (
// Retrieve the updated user result. Since the update was the last operation, it is the last item.
const updatedUser = results[results.length - 1];
// For each deletion, revalidate the corresponding team and its project caches.
for (const opResult of results.slice(0, deleteTeamOps.length)) {
const deletedTeamUser = opResult;
teamCache.revalidate({
id: deletedTeamUser.team.id,
userId: existingUser.id,
organizationId,
});
deletedTeamUser.team.projectTeams.forEach((projectTeam) => {
teamCache.revalidate({
projectId: projectTeam.projectId,
});
});
}
// For each creation, do the same.
for (const opResult of results.slice(deleteTeamOps.length, deleteTeamOps.length + createTeamOps.length)) {
const newTeamUser = opResult;
teamCache.revalidate({
id: newTeamUser.team.id,
userId: existingUser.id,
organizationId,
});
newTeamUser.team.projectTeams.forEach((projectTeam) => {
teamCache.revalidate({
projectId: projectTeam.projectId,
});
});
}
// Revalidate membership and user caches for the updated user.
membershipCache.revalidate({
organizationId,
userId: updatedUser.id,
});
userCache.revalidate({
id: updatedUser.id,
email: updatedUser.email,
});
const returnedUser = {
id: updatedUser.id,
createdAt: updatedUser.createdAt,