mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-03 11:30:50 -05:00
chore: Comprehensive Cache Optimization & Performance Enhancement (#5926)
Co-authored-by: Piyush Gupta <piyushguptaa2z123@gmail.com>
This commit is contained in:
+23
-81
@@ -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) {
|
||||
|
||||
-22
@@ -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 () => {
|
||||
|
||||
+6
-5
@@ -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);
|
||||
|
||||
+19
-36
@@ -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) {
|
||||
|
||||
-13
@@ -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)) {
|
||||
|
||||
+20
-32
@@ -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 }] });
|
||||
}
|
||||
});
|
||||
|
||||
+20
-32
@@ -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 }] });
|
||||
}
|
||||
});
|
||||
|
||||
+18
-30
@@ -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 }] });
|
||||
}
|
||||
});
|
||||
|
||||
+3
-2
@@ -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;
|
||||
|
||||
+18
-29
@@ -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 }],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
+105
-117
@@ -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);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
+19
-31
@@ -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 }] });
|
||||
}
|
||||
});
|
||||
|
||||
+20
-34
@@ -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 }] });
|
||||
}
|
||||
});
|
||||
|
||||
-26
@@ -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}`],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
-30
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
+2
-1
@@ -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({
|
||||
|
||||
-26
@@ -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);
|
||||
|
||||
+19
-52
@@ -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) {
|
||||
|
||||
+5
-23
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user