chore: refactor services with Formbricks best practices (#988)

Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
Rotimi Best
2023-10-10 14:54:23 +01:00
committed by GitHub
parent cff23f497c
commit 9063c8286c
51 changed files with 657 additions and 504 deletions

View File

@@ -14,6 +14,7 @@ import { AuthenticationError, AuthorizationError, ResourceNotFoundError } from "
import { Team } from "@prisma/client";
import { Prisma as prismaClient } from "@prisma/client/";
import { getServerSession } from "next-auth";
import { getActionClasses } from "@formbricks/lib/actionClass/service";
export const createShortUrlAction = async (url: string) => {
const session = await getServerSession(authOptions);
@@ -62,6 +63,8 @@ export async function duplicateSurveyAction(environmentId: string, surveyId: str
throw new ResourceNotFoundError("Survey", surveyId);
}
const actionClasses = await getActionClasses(environmentId);
// create new survey with the data of the existing survey
const newSurvey = await prisma.survey.create({
data: {
@@ -74,7 +77,7 @@ export async function duplicateSurveyAction(environmentId: string, surveyId: str
thankYouCard: JSON.parse(JSON.stringify(existingSurvey.thankYouCard)),
triggers: {
create: existingSurvey.triggers.map((trigger) => ({
eventClassId: trigger.id,
eventClassId: actionClasses.find((actionClass) => actionClass.name === trigger)!.id,
})),
},
attributeFilters: {

View File

@@ -3,6 +3,7 @@ import {
TGoogleSheetIntegration,
TGoogleSheetsConfigData,
TGoogleSpreadsheet,
TIntegrationInput,
} from "@formbricks/types/v1/integrations";
import { Button, Checkbox, Label } from "@formbricks/ui";
import GoogleSheetLogo from "@/images/google-sheets-small.png";
@@ -52,7 +53,7 @@ export default function AddIntegrationModal({
const [selectedSpreadsheet, setSelectedSpreadsheet] = useState<any>(null);
const [isDeleting, setIsDeleting] = useState<any>(null);
const existingIntegrationData = googleSheetIntegration?.config?.data;
const googleSheetIntegrationData: Partial<TGoogleSheetIntegration> = {
const googleSheetIntegrationData: TIntegrationInput = {
type: "googleSheets",
config: {
key: googleSheetIntegration?.config?.key,

View File

@@ -4,21 +4,12 @@ import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
import { getServerSession } from "next-auth";
import { getSpreadSheets } from "@formbricks/lib/googleSheet/service";
import { createOrUpdateIntegration, deleteIntegration } from "@formbricks/lib/integration/service";
import { TGoogleSheetIntegration } from "@formbricks/types/v1/integrations";
import { TIntegrationInput } from "@formbricks/types/v1/integrations";
import { canUserAccessIntegration } from "@formbricks/lib/integration/auth";
import { AuthorizationError } from "@formbricks/types/v1/errors";
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
export async function upsertIntegrationAction(
environmentId: string,
integrationData: Partial<TGoogleSheetIntegration>
) {
const session = await getServerSession(authOptions);
if (!session) throw new AuthorizationError("Not authorized");
const isAuthorized = await hasUserEnvironmentAccess(session.user.id, environmentId);
if (!isAuthorized) throw new AuthorizationError("Not authorized");
export async function upsertIntegrationAction(environmentId: string, integrationData: TIntegrationInput) {
return await createOrUpdateIntegration(environmentId, integrationData);
}

View File

@@ -2,7 +2,7 @@ export const revalidate = REVALIDATION_INTERVAL;
import EmptySpaceFiller from "@/components/shared/EmptySpaceFiller";
import { truncateMiddle } from "@/lib/utils";
import { PEOPLE_PER_PAGE, REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
import { ITEMS_PER_PAGE, REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getPeople, getPeopleCount } from "@formbricks/lib/person/service";
import { TPerson } from "@formbricks/types/v1/people";
@@ -27,7 +27,7 @@ export default async function PeoplePage({
if (!environment) {
throw new Error("Environment not found");
}
const maxPageNumber = Math.ceil(totalPeople / PEOPLE_PER_PAGE);
const maxPageNumber = Math.ceil(totalPeople / ITEMS_PER_PAGE);
let hidePagination = false;
let people: TPerson[] = [];
@@ -94,7 +94,7 @@ export default async function PeoplePage({
baseUrl={`/environments/${params.environmentId}/people`}
currentPage={pageNumber}
totalItems={totalPeople}
itemsPerPage={PEOPLE_PER_PAGE}
itemsPerPage={ITEMS_PER_PAGE}
/>
)}
</>

View File

@@ -5,7 +5,7 @@ import { createInviteToken } from "@formbricks/lib/jwt";
import { AuthenticationError, AuthorizationError, ValidationError } from "@formbricks/types/v1/errors";
import {
deleteInvite,
getInviteToken,
getInvite,
inviteUser,
resendInvite,
updateInvite,
@@ -136,7 +136,7 @@ export const leaveTeamAction = async (teamId: string) => {
};
export const createInviteTokenAction = async (inviteId: string) => {
const { email } = await getInviteToken(inviteId);
const { email } = await getInvite(inviteId);
const inviteToken = createInviteToken(inviteId, email, {
expiresIn: "7d",

View File

@@ -29,7 +29,7 @@ export default function SurveyStarter({
...template.preset,
type: surveyType,
autoComplete,
} as Partial<TSurveyInput>;
} as TSurveyInput;
try {
const survey = await createSurveyAction(environmentId, augmentedTemplate);
router.push(`/environments/${environmentId}/surveys/${survey.id}/edit`);

View File

@@ -308,7 +308,7 @@ export default function SurveyMenuBar({
disabled={
localSurvey.type === "web" &&
localSurvey.triggers &&
(localSurvey.triggers[0]?.id === "" || localSurvey.triggers.length === 0)
(localSurvey.triggers[0] === "" || localSurvey.triggers.length === 0)
}
variant="darkCTA"
loading={isMutatingSurvey}

View File

@@ -16,10 +16,9 @@ import {
} from "@formbricks/ui";
import { CheckCircleIcon, PlusIcon, TrashIcon } from "@heroicons/react/24/solid";
import * as Collapsible from "@radix-ui/react-collapsible";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { TSurveyWithAnalytics } from "@formbricks/types/v1/surveys";
import { TActionClass } from "@formbricks/types/v1/actionClasses";
interface WhenToSendCardProps {
localSurvey: TSurveyWithAnalytics;
setLocalSurvey: (survey: TSurveyWithAnalytics) => void;
@@ -40,32 +39,22 @@ export default function WhenToSendCard({
const autoClose = localSurvey.autoClose !== null;
let newTrigger = useMemo(
() => ({
id: "", // Set the appropriate value for the id
createdAt: new Date(),
updatedAt: new Date(),
name: "",
type: "code" as const, // Set the appropriate value for the type
environmentId: "",
description: null,
noCodeConfig: null,
}),
[]
);
const addTriggerEvent = useCallback(() => {
const updatedSurvey = { ...localSurvey };
updatedSurvey.triggers = [...localSurvey.triggers, newTrigger];
updatedSurvey.triggers = [...localSurvey.triggers, ""];
setLocalSurvey(updatedSurvey);
}, [newTrigger, localSurvey, setLocalSurvey]);
}, [localSurvey, setLocalSurvey]);
const setTriggerEvent = useCallback(
(idx: number, actionClassId: string) => {
(idx: number, actionClassName: string) => {
const updatedSurvey = { ...localSurvey };
updatedSurvey.triggers[idx] = actionClassArray!.find((actionClass) => {
return actionClass.id === actionClassId;
})!;
const newActionClass = actionClassArray!.find((actionClass) => {
return actionClass.name === actionClassName;
});
if (!newActionClass) {
throw new Error("Action class not found");
}
updatedSurvey.triggers[idx] = newActionClass.name;
setLocalSurvey(updatedSurvey);
},
[actionClassArray, localSurvey, setLocalSurvey]
@@ -104,11 +93,11 @@ export default function WhenToSendCard({
useEffect(() => {
if (activeIndex !== null) {
const newActionClassId = actionClassArray[actionClassArray.length - 1].id;
const currentActionClassId = localSurvey.triggers[activeIndex]?.id;
const newActionClass = actionClassArray[actionClassArray.length - 1].name;
const currentActionClass = localSurvey.triggers[activeIndex];
if (newActionClassId !== currentActionClassId) {
setTriggerEvent(activeIndex, newActionClassId);
if (newActionClass !== currentActionClass) {
setTriggerEvent(activeIndex, newActionClass);
}
setActiveIndex(null);
@@ -148,7 +137,7 @@ export default function WhenToSendCard({
)}>
<div className="inline-flex px-4 py-4">
<div className="flex items-center pl-2 pr-5">
{!localSurvey.triggers || localSurvey.triggers.length === 0 || !localSurvey.triggers[0]?.id ? (
{!localSurvey.triggers || localSurvey.triggers.length === 0 || !localSurvey.triggers[0] ? (
<div
className={cn(
localSurvey.type !== "link"
@@ -186,8 +175,8 @@ export default function WhenToSendCard({
<div className="inline-flex items-center">
<p className="mr-2 w-14 text-right text-sm">{idx === 0 ? "When" : "or"}</p>
<Select
value={triggerEventClass.id}
onValueChange={(actionClassId) => setTriggerEvent(idx, actionClassId)}>
value={triggerEventClass}
onValueChange={(actionClassName) => setTriggerEvent(idx, actionClassName)}>
<SelectTrigger className="w-[240px]">
<SelectValue />
</SelectTrigger>
@@ -205,8 +194,8 @@ export default function WhenToSendCard({
<SelectSeparator />
{actionClassArray.map((actionClass) => (
<SelectItem
value={actionClass.id}
key={actionClass.id}
value={actionClass.name}
key={actionClass.name}
title={actionClass.description ? actionClass.description : ""}>
{actionClass.name}
</SelectItem>

View File

@@ -14,6 +14,13 @@ export async function updateSurveyAction(survey: TSurvey): Promise<TSurvey> {
const isAuthorized = await canUserAccessSurvey(session.user.id, survey.id);
if (!isAuthorized) throw new AuthorizationError("Not authorized");
if (typeof survey.createdAt === "string") {
survey.createdAt = new Date(survey.createdAt);
}
if (typeof survey.updatedAt === "string") {
survey.updatedAt = new Date(survey.updatedAt);
}
return await updateSurvey(survey);
}

View File

@@ -5,9 +5,9 @@ import { getServerSession } from "next-auth";
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
import { createSurvey } from "@formbricks/lib/survey/service";
import { AuthorizationError } from "@formbricks/types/v1/errors";
import { TSurvey } from "@formbricks/types/v1/surveys";
import { TSurveyInput } from "@formbricks/types/v1/surveys";
export async function createSurveyAction(environmentId: string, surveyBody: Partial<TSurvey>) {
export async function createSurveyAction(environmentId: string, surveyBody: TSurveyInput) {
const session = await getServerSession(authOptions);
if (!session) throw new AuthorizationError("Not authorized");

View File

@@ -78,7 +78,7 @@ export default function TemplateList({
...activeTemplate.preset,
type: surveyType,
autoComplete,
} as Partial<TSurveyInput>;
} as TSurveyInput;
const survey = await createSurveyAction(environmentId, augmentedTemplate);
router.push(`/environments/${environmentId}/surveys/${survey.id}/edit`);
};

View File

@@ -5,9 +5,9 @@ import { authOptions } from "@formbricks/lib/authOptions";
import { getServerSession } from "next-auth";
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
import { AuthorizationError } from "@formbricks/types/v1/errors";
import { TSurvey } from "@formbricks/types/v1/surveys";
import { TSurveyInput } from "@formbricks/types/v1/surveys";
export async function createSurveyAction(environmentId: string, surveyBody: Partial<TSurvey>) {
export async function createSurveyAction(environmentId: string, surveyBody: TSurveyInput) {
const session = await getServerSession(authOptions);
if (!session) throw new AuthorizationError("Not authorized");

View File

@@ -41,6 +41,8 @@ export async function POST(request: Request): Promise<NextResponse> {
// find teamId & teamOwnerId from environmentId
const teamDetails = await getTeamDetails(survey.environmentId);
console.log("teamDetails", teamDetails);
// create display
let display: TDisplay;
try {

View File

@@ -1,7 +1,7 @@
import { responses } from "@/lib/api/response";
import { transformErrorToDetails } from "@/lib/api/validator";
import { createAction } from "@formbricks/lib/action/service";
import { ZJsActionInput } from "@formbricks/types/v1/js";
import { ZActionInput } from "@formbricks/types/v1/actions";
import { NextResponse } from "next/server";
export async function OPTIONS(): Promise<NextResponse> {
@@ -13,7 +13,7 @@ export async function POST(req: Request): Promise<NextResponse> {
const jsonInput = await req.json();
// validate using zod
const inputValidation = ZJsActionInput.safeParse(jsonInput);
const inputValidation = ZActionInput.safeParse(jsonInput);
if (!inputValidation.success) {
return responses.badRequestResponse(

View File

@@ -31,7 +31,7 @@ export async function POST(req: Request, { params }): Promise<NextResponse> {
let returnedPerson;
// check if person with this userId exists
const existingPerson = await prisma.person.findFirst({
const person = await prisma.person.findFirst({
where: {
environmentId,
attributes: {
@@ -46,7 +46,7 @@ export async function POST(req: Request, { params }): Promise<NextResponse> {
select: selectPerson,
});
// if person exists, reconnect session and delete old user
if (existingPerson) {
if (person) {
// reconnect session to new person
await prisma.session.update({
where: {
@@ -55,7 +55,7 @@ export async function POST(req: Request, { params }): Promise<NextResponse> {
data: {
person: {
connect: {
id: existingPerson.id,
id: person.id,
},
},
},
@@ -64,7 +64,7 @@ export async function POST(req: Request, { params }): Promise<NextResponse> {
// delete old person
await deletePerson(personId);
returnedPerson = existingPerson;
returnedPerson = person;
} else {
// update person with userId
returnedPerson = await prisma.person.update({
@@ -90,14 +90,14 @@ export async function POST(req: Request, { params }): Promise<NextResponse> {
});
}
const person = transformPrismaPerson(returnedPerson);
const transformedPerson = transformPrismaPerson(returnedPerson);
if (person) {
if (transformedPerson) {
// revalidate person
revalidateTag(person.id);
revalidateTag(transformedPerson.id);
}
const state = await getUpdatedState(environmentId, person.id, sessionId);
const state = await getUpdatedState(environmentId, transformedPerson.id, sessionId);
return responses.successResponse({ ...state }, true);
} catch (error) {

View File

@@ -165,7 +165,7 @@ export const getSurveys = async (environmentId: string, person: TPerson): Promis
.map((survey) => ({
...survey,
singleUse: survey.singleUse ? JSON.parse(JSON.stringify(survey.singleUse)) : null,
triggers: survey.triggers.map((trigger) => trigger.eventClass),
triggers: survey.triggers.map((trigger) => trigger.eventClass.name),
attributeFilters: survey.attributeFilters.map((af) => ({
...af,
attributeClassId: af.attributeClass.id,

View File

@@ -1,7 +1,7 @@
import { responses } from "@/lib/api/response";
import { NextResponse } from "next/server";
import { getSurvey, updateSurvey, deleteSurvey } from "@formbricks/lib/survey/service";
import { TSurvey, ZSurveyInput } from "@formbricks/types/v1/surveys";
import { TSurvey, ZSurvey } from "@formbricks/types/v1/surveys";
import { transformErrorToDetails } from "@/lib/api/validator";
import { authenticateRequest } from "@/app/api/v1/auth";
import { handleErrorResponse } from "@/app/api/v1/auth";
@@ -64,7 +64,7 @@ export async function PUT(
return responses.notFoundResponse("Survey", params.surveyId);
}
const surveyUpdate = await request.json();
const inputValidation = ZSurveyInput.safeParse(surveyUpdate);
const inputValidation = ZSurvey.safeParse(surveyUpdate);
if (!inputValidation.success) {
return responses.badRequestResponse(
"Fields are missing or incorrectly formatted",

View File

@@ -28,7 +28,7 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
if (!sessionId) {
return res.status(400).json({ message: "Missing sessionId" });
}
let returnedPerson;
let person;
// check if person exists
const existingPerson = await prisma.person.findFirst({
where: {
@@ -81,10 +81,10 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
id: personId,
},
});
returnedPerson = existingPerson;
person = existingPerson;
} else {
// update person
returnedPerson = await prisma.person.update({
person = await prisma.person.update({
where: {
id: personId,
},
@@ -122,10 +122,10 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
});
}
const settings = await getSettings(environmentId, returnedPerson.id);
const settings = await getSettings(environmentId, person.id);
// return updated person and settings
return res.json({ person: returnedPerson, settings });
return res.json({ person, settings });
}
// Unknown HTTP Method

View File

@@ -99,19 +99,19 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
// POST
else if (req.method === "PUT") {
const data = { ...req.body, updatedAt: new Date() };
const prismaRes = await prisma.person.update({
const person = await prisma.person.update({
where: { id: personId },
data,
});
return res.json(prismaRes);
return res.json(person);
}
// Delete
else if (req.method === "DELETE") {
const prismaRes = await prisma.person.delete({
const person = await prisma.person.delete({
where: { id: personId },
});
return res.json(prismaRes);
return res.json(person);
}
// Unknown HTTP Method

View File

@@ -4,25 +4,26 @@ import { prisma } from "@formbricks/database";
import { TAction } from "@formbricks/types/v1/actions";
import { ZId } from "@formbricks/types/v1/environment";
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/v1/errors";
import { TJsActionInput } from "@formbricks/types/v1/js";
import { EventType, Prisma } from "@prisma/client";
import { revalidateTag, unstable_cache } from "next/cache";
import { cache } from "react";
import z from "zod";
import { getActionClassCacheTag } from "../actionClass/service";
import { SERVICES_REVALIDATION_INTERVAL } from "../constants";
import { SERVICES_REVALIDATION_INTERVAL, ITEMS_PER_PAGE } from "../constants";
import { getSessionCached } from "../session/service";
import { validateInputs } from "../utils/validate";
import { TActionInput, ZActionInput } from "@formbricks/types/v1/actions";
import { ZOptionalNumber } from "@formbricks/types/v1/common";
export const getActionsCacheTag = (environmentId: string): string => `environments-${environmentId}-actions`;
export const getActionsByEnvironmentId = async (
environmentId: string,
limit?: number
limit?: number,
page?: number
): Promise<TAction[]> => {
const actions = await unstable_cache(
async () => {
validateInputs([environmentId, ZId], [limit, z.number().optional()]);
validateInputs([environmentId, ZId], [limit, ZOptionalNumber], [page, ZOptionalNumber]);
try {
const actionsPrisma = await prisma.event.findMany({
where: {
@@ -33,7 +34,8 @@ export const getActionsByEnvironmentId = async (
orderBy: {
createdAt: "desc",
},
take: limit ? limit : 20,
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
include: {
eventClass: true,
},
@@ -72,7 +74,8 @@ export const getActionsByEnvironmentId = async (
}));
};
export const createAction = async (data: TJsActionInput): Promise<TAction> => {
export const createAction = async (data: TActionInput): Promise<TAction> => {
validateInputs([data, ZActionInput]);
const { environmentId, name, properties, sessionId } = data;
let eventType: EventType = EventType.code;
@@ -133,7 +136,8 @@ export const createAction = async (data: TJsActionInput): Promise<TAction> => {
};
};
export const getActionCountInLastHour = cache(async (actionClassId: string) => {
export const getActionCountInLastHour = async (actionClassId: string): Promise<number> => {
validateInputs([actionClassId, ZId]);
try {
const numEventsLastHour = await prisma.event.count({
where: {
@@ -147,9 +151,10 @@ export const getActionCountInLastHour = cache(async (actionClassId: string) => {
} catch (error) {
throw error;
}
});
};
export const getActionCountInLast24Hours = cache(async (actionClassId: string) => {
export const getActionCountInLast24Hours = async (actionClassId: string): Promise<number> => {
validateInputs([actionClassId, ZId]);
try {
const numEventsLast24Hours = await prisma.event.count({
where: {
@@ -163,9 +168,10 @@ export const getActionCountInLast24Hours = cache(async (actionClassId: string) =
} catch (error) {
throw error;
}
});
};
export const getActionCountInLast7Days = cache(async (actionClassId: string) => {
export const getActionCountInLast7Days = async (actionClassId: string): Promise<number> => {
validateInputs([actionClassId, ZId]);
try {
const numEventsLast7Days = await prisma.event.count({
where: {
@@ -179,4 +185,4 @@ export const getActionCountInLast7Days = cache(async (actionClassId: string) =>
} catch (error) {
throw error;
}
});
};

View File

@@ -2,9 +2,10 @@
import "server-only";
import { prisma } from "@formbricks/database";
import { SERVICES_REVALIDATION_INTERVAL } from "../constants";
import { SERVICES_REVALIDATION_INTERVAL, ITEMS_PER_PAGE } from "../constants";
import { TActionClass, TActionClassInput, ZActionClassInput } from "@formbricks/types/v1/actionClasses";
import { ZId } from "@formbricks/types/v1/environment";
import { ZOptionalNumber, ZString } from "@formbricks/types/v1/common";
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/v1/errors";
import { revalidateTag, unstable_cache } from "next/cache";
import { validateInputs } from "../utils/validate";
@@ -26,16 +27,18 @@ const select = {
environmentId: true,
};
export const getActionClasses = (environmentId: string): Promise<TActionClass[]> =>
export const getActionClasses = (environmentId: string, page?: number): Promise<TActionClass[]> =>
unstable_cache(
async () => {
validateInputs([environmentId, ZId]);
validateInputs([environmentId, ZId], [page, ZOptionalNumber]);
try {
let actionClasses = await prisma.eventClass.findMany({
const actionClasses = await prisma.eventClass.findMany({
where: {
environmentId: environmentId,
},
select,
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
orderBy: {
createdAt: "asc",
},
@@ -156,7 +159,8 @@ export const updateActionClass = async (
export const getActionClassCached = async (name: string, environmentId: string) =>
unstable_cache(
async () => {
async (): Promise<TActionClass | null> => {
validateInputs([name, ZString], [environmentId, ZId]);
return await prisma.eventClass.findFirst({
where: {
name,

View File

@@ -4,10 +4,9 @@ import { prisma } from "@formbricks/database";
import { TActivityFeedItem } from "@formbricks/types/v1/activity";
import { validateInputs } from "../utils/validate";
import { ZId } from "@formbricks/types/v1/environment";
import { cache } from "react";
import { ResourceNotFoundError } from "@formbricks/types/v1/errors";
export const getActivityTimeline = cache(async (personId: string): Promise<TActivityFeedItem[]> => {
export const getActivityTimeline = async (personId: string): Promise<TActivityFeedItem[]> => {
validateInputs([personId, ZId]);
const person = await prisma.person.findUnique({
where: {
@@ -83,4 +82,4 @@ export const getActivityTimeline = cache(async (personId: string): Promise<TActi
const unifiedList: TActivityFeedItem[] = [...unifiedAttributes, ...unifiedDisplays, ...unifiedEvents];
return unifiedList;
});
};

View File

@@ -1,18 +1,18 @@
import "server-only";
import z from "zod";
import { prisma } from "@formbricks/database";
import { TApiKey, TApiKeyCreateInput, ZApiKeyCreateInput } from "@formbricks/types/v1/apiKeys";
import { Prisma } from "@prisma/client";
import { getHash } from "../crypto";
import { createHash, randomBytes } from "crypto";
import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/v1/errors";
import { cache } from "react";
import { validateInputs } from "../utils/validate";
import { ZId } from "@formbricks/types/v1/environment";
import { ZString, ZOptionalNumber } from "@formbricks/types/v1/common";
import { ITEMS_PER_PAGE } from "../constants";
export const getApiKey = async (apiKeyId: string): Promise<TApiKey | null> => {
validateInputs([apiKeyId, z.string()]);
validateInputs([apiKeyId, ZString]);
if (!apiKeyId) {
throw new InvalidInputError("API key cannot be null or undefined.");
}
@@ -38,13 +38,16 @@ export const getApiKey = async (apiKeyId: string): Promise<TApiKey | null> => {
}
};
export const getApiKeys = cache(async (environmentId: string): Promise<TApiKey[]> => {
validateInputs([environmentId, ZId]);
export const getApiKeys = async (environmentId: string, page?: number): Promise<TApiKey[]> => {
validateInputs([environmentId, ZId], [page, ZOptionalNumber]);
try {
const apiKeys = await prisma.apiKey.findMany({
where: {
environmentId,
},
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
return apiKeys;
@@ -54,7 +57,7 @@ export const getApiKeys = cache(async (environmentId: string): Promise<TApiKey[]
}
throw error;
}
});
};
export const hashApiKey = (key: string): string => createHash("sha256").update(key).digest("hex");
@@ -82,7 +85,7 @@ export async function createApiKey(environmentId: string, apiKeyData: TApiKeyCre
}
export const getApiKeyFromKey = async (apiKey: string): Promise<TApiKey | null> => {
validateInputs([apiKey, z.string()]);
validateInputs([apiKey, ZString]);
if (!apiKey) {
throw new InvalidInputError("API key cannot be null or undefined.");
}
@@ -104,14 +107,16 @@ export const getApiKeyFromKey = async (apiKey: string): Promise<TApiKey | null>
}
};
export const deleteApiKey = async (id: string): Promise<void> => {
export const deleteApiKey = async (id: string): Promise<TApiKey | null> => {
validateInputs([id, ZId]);
try {
await prisma.apiKey.delete({
const deletedApiKeyData = await prisma.apiKey.delete({
where: {
id: id,
},
});
return deletedApiKeyData;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError("Database operation failed");

View File

@@ -11,9 +11,9 @@ import {
import { ZId } from "@formbricks/types/v1/environment";
import { validateInputs } from "../utils/validate";
import { DatabaseError } from "@formbricks/types/v1/errors";
import { cache } from "react";
import { revalidateTag, unstable_cache } from "next/cache";
import { SERVICES_REVALIDATION_INTERVAL } from "../constants";
import { SERVICES_REVALIDATION_INTERVAL, ITEMS_PER_PAGE } from "../constants";
import { ZOptionalNumber } from "@formbricks/types/v1/common";
const attributeClassesCacheTag = (environmentId: string): string =>
`environments-${environmentId}-attributeClasses`;
@@ -22,19 +22,7 @@ const getAttributeClassesCacheKey = (environmentId: string): string[] => [
attributeClassesCacheTag(environmentId),
];
export const transformPrismaAttributeClass = (attributeClass: any): TAttributeClass | null => {
if (attributeClass === null) {
return null;
}
const transformedAttributeClass: TAttributeClass = {
...attributeClass,
};
return transformedAttributeClass;
};
export const getAttributeClass = cache(async (attributeClassId: string): Promise<TAttributeClass | null> => {
export const getAttributeClass = async (attributeClassId: string): Promise<TAttributeClass | null> => {
validateInputs([attributeClassId, ZId]);
try {
const attributeClass = await prisma.attributeClass.findFirst({
@@ -42,32 +30,35 @@ export const getAttributeClass = cache(async (attributeClassId: string): Promise
id: attributeClassId,
},
});
return transformPrismaAttributeClass(attributeClass);
return attributeClass;
} catch (error) {
throw new DatabaseError(`Database error when fetching attributeClass with id ${attributeClassId}`);
}
});
};
export const getAttributeClasses = async (
environmentId: string,
page?: number
): Promise<TAttributeClass[]> => {
validateInputs([environmentId, ZId], [page, ZOptionalNumber]);
export const getAttributeClasses = cache(async (environmentId: string): Promise<TAttributeClass[]> => {
validateInputs([environmentId, ZId]);
try {
let attributeClasses = await prisma.attributeClass.findMany({
const attributeClasses = await prisma.attributeClass.findMany({
where: {
environmentId: environmentId,
},
orderBy: {
createdAt: "asc",
},
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
const transformedAttributeClasses: TAttributeClass[] = attributeClasses
.map(transformPrismaAttributeClass)
.filter((attributeClass): attributeClass is TAttributeClass => attributeClass !== null);
return transformedAttributeClasses;
return attributeClasses;
} catch (error) {
throw new DatabaseError(`Database error when fetching attributeClasses for environment ${environmentId}`);
}
});
};
export const updatetAttributeClass = async (
attributeClassId: string,
@@ -75,7 +66,7 @@ export const updatetAttributeClass = async (
): Promise<TAttributeClass | null> => {
validateInputs([attributeClassId, ZId], [data, ZAttributeClassUpdateInput.partial()]);
try {
let attributeClass = await prisma.attributeClass.update({
const attributeClass = await prisma.attributeClass.update({
where: {
id: attributeClassId,
},
@@ -84,10 +75,10 @@ export const updatetAttributeClass = async (
archived: data.archived,
},
});
const transformedAttributeClass: TAttributeClass | null = transformPrismaAttributeClass(attributeClass);
revalidateTag(attributeClassesCacheTag(attributeClass.environmentId));
return transformedAttributeClass;
return attributeClass;
} catch (error) {
throw new DatabaseError(`Database error when updating attribute class with id ${attributeClassId}`);
}
@@ -95,7 +86,7 @@ export const updatetAttributeClass = async (
export const getAttributeClassByNameCached = async (environmentId: string, name: string) =>
await unstable_cache(
async () => {
async (): Promise<TAttributeClass | null> => {
return await getAttributeClassByName(environmentId, name);
},
[`environments-${environmentId}-attributeClass-${name}`],
@@ -105,17 +96,18 @@ export const getAttributeClassByNameCached = async (environmentId: string, name:
}
)();
export const getAttributeClassByName = cache(
async (environmentId: string, name: string): Promise<TAttributeClass | null> => {
const attributeClass = await prisma.attributeClass.findFirst({
where: {
environmentId,
name,
},
});
return transformPrismaAttributeClass(attributeClass);
}
);
export const getAttributeClassByName = async (
environmentId: string,
name: string
): Promise<TAttributeClass | null> => {
const attributeClass = await prisma.attributeClass.findFirst({
where: {
environmentId,
name,
},
});
return attributeClass;
};
export const createAttributeClass = async (
environmentId: string,
@@ -134,7 +126,7 @@ export const createAttributeClass = async (
},
});
revalidateTag(attributeClassesCacheTag(environmentId));
return transformPrismaAttributeClass(attributeClass);
return attributeClass;
};
export const deleteAttributeClass = async (attributeClassId: string): Promise<TAttributeClass> => {

View File

@@ -57,7 +57,7 @@ export const MAIL_FROM = env.MAIL_FROM;
export const NEXTAUTH_SECRET = env.NEXTAUTH_SECRET;
export const NEXTAUTH_URL = env.NEXTAUTH_URL;
export const PEOPLE_PER_PAGE = 50;
export const ITEMS_PER_PAGE = 50;
// Storage constants
export const UPLOADS_DIR = path.resolve("./uploads");

View File

@@ -11,9 +11,10 @@ import { ZId } from "@formbricks/types/v1/environment";
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/v1/errors";
import { Prisma } from "@prisma/client";
import { revalidateTag } from "next/cache";
import { cache } from "react";
import { validateInputs } from "../utils/validate";
import { transformPrismaPerson } from "../person/service";
import { ITEMS_PER_PAGE } from "../constants";
import { ZOptionalNumber } from "@formbricks/types/v1/common";
const selectDisplay = {
id: true,
@@ -150,58 +151,61 @@ export const markDisplayResponded = async (displayId: string): Promise<TDisplay>
}
};
export const getDisplaysOfPerson = cache(
async (personId: string): Promise<TDisplaysWithSurveyName[] | null> => {
validateInputs([personId, ZId]);
try {
const displaysPrisma = await prisma.display.findMany({
where: {
personId: personId,
},
select: {
id: true,
createdAt: true,
updatedAt: true,
surveyId: true,
responseId: true,
survey: {
select: {
name: true,
},
export const getDisplaysOfPerson = async (
personId: string,
page?: number
): Promise<TDisplaysWithSurveyName[] | null> => {
validateInputs([personId, ZId], [page, ZOptionalNumber]);
try {
const displaysPrisma = await prisma.display.findMany({
where: {
personId: personId,
},
select: {
id: true,
createdAt: true,
updatedAt: true,
surveyId: true,
responseId: true,
survey: {
select: {
name: true,
},
status: true,
},
});
status: true,
},
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
if (!displaysPrisma) {
throw new ResourceNotFoundError("Display from PersonId", personId);
}
let displays: TDisplaysWithSurveyName[] = [];
displaysPrisma.forEach((displayPrisma) => {
const display: TDisplaysWithSurveyName = {
id: displayPrisma.id,
createdAt: displayPrisma.createdAt,
updatedAt: displayPrisma.updatedAt,
person: null,
surveyId: displayPrisma.surveyId,
surveyName: displayPrisma.survey.name,
responseId: displayPrisma.responseId,
};
displays.push(display);
});
return displays;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError("Database operation failed");
}
throw error;
if (!displaysPrisma) {
throw new ResourceNotFoundError("Display from PersonId", personId);
}
let displays: TDisplaysWithSurveyName[] = [];
displaysPrisma.forEach((displayPrisma) => {
const display: TDisplaysWithSurveyName = {
id: displayPrisma.id,
createdAt: displayPrisma.createdAt,
updatedAt: displayPrisma.updatedAt,
person: null,
surveyId: displayPrisma.surveyId,
surveyName: displayPrisma.survey.name,
responseId: displayPrisma.responseId,
};
displays.push(display);
});
return displays;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError("Database operation failed");
}
throw error;
}
);
};
export const deleteDisplayByResponseId = async (responseId: string, surveyId: string): Promise<void> => {
validateInputs([responseId, ZId]);

View File

@@ -6,7 +6,7 @@ import { SERVICES_REVALIDATION_INTERVAL } from "../constants";
export const hasUserEnvironmentAccess = async (userId: string, environmentId: string) => {
return await unstable_cache(
async () => {
async (): Promise<boolean> => {
validateInputs([userId, ZId], [environmentId, ZId]);
const environment = await prisma.environment.findUnique({
where: {

View File

@@ -25,7 +25,7 @@ export const getEnvironmentsCacheTag = (productId: string) => `products-${produc
export const getEnvironment = (environmentId: string) =>
unstable_cache(
async () => {
async (): Promise<TEnvironment> => {
validateInputs([environmentId, ZId]);
let environmentPrisma;
@@ -62,7 +62,7 @@ export const getEnvironment = (environmentId: string) =>
export const getEnvironments = async (productId: string): Promise<TEnvironment[]> =>
unstable_cache(
async () => {
async (): Promise<TEnvironment[]> => {
validateInputs([productId, ZId]);
let productPrisma;
try {

View File

@@ -1,19 +1,24 @@
import "server-only";
import { z } from "zod";
import { validateInputs } from "../utils/validate";
import { prisma } from "@formbricks/database";
import { Prisma } from "@prisma/client";
import { DatabaseError, UnknownError } from "@formbricks/types/v1/errors";
import { cache } from "react";
import { ZId } from "@formbricks/types/v1/environment";
import {
ZGoogleCredential,
TGoogleCredential,
TGoogleSheetIntegration,
TGoogleSpreadsheet,
TIntegration,
TGoogleSheetIntegration,
} from "@formbricks/types/v1/integrations";
import {
GOOGLE_SHEETS_CLIENT_ID,
GOOGLE_SHEETS_CLIENT_SECRET,
GOOGLE_SHEETS_REDIRECT_URL,
} from "../constants";
import { ZString } from "@formbricks/types/v1/common";
const { google } = require("googleapis");
@@ -31,38 +36,36 @@ async function fetchSpreadsheets(auth: any) {
}
}
export const getGoogleSheetIntegration = cache(
async (environmentId: string): Promise<TGoogleSheetIntegration | null> => {
try {
const result = await prisma.integration.findUnique({
where: {
type_environmentId: {
environmentId,
type: "googleSheets",
},
export const getGoogleSheetIntegration = async (
environmentId: string
): Promise<TIntegration | TGoogleSheetIntegration | null> => {
validateInputs([environmentId, ZId]);
try {
const result = await prisma.integration.findUnique({
where: {
type_environmentId: {
environmentId,
type: "googleSheets",
},
});
// Type Guard
if (result && isGoogleSheetIntegration(result)) {
return result as TGoogleSheetIntegration; // Explicit casting
}
return null;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError("Database operation failed");
}
throw error;
},
});
return result;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError("Database operation failed");
}
throw error;
}
);
function isGoogleSheetIntegration(integration: any): integration is TGoogleSheetIntegration {
return integration.type === "googleSheets";
}
};
export const getSpreadSheets = async (environmentId: string): Promise<TGoogleSpreadsheet[]> => {
validateInputs([environmentId, ZId]);
let spreadsheets: TGoogleSpreadsheet[] = [];
try {
const googleIntegration = await getGoogleSheetIntegration(environmentId);
const googleIntegration = (await getGoogleSheetIntegration(environmentId)) as TGoogleSheetIntegration;
if (googleIntegration && googleIntegration.config?.key) {
spreadsheets = await fetchSpreadsheets(googleIntegration.config?.key);
}
@@ -75,6 +78,12 @@ export const getSpreadSheets = async (environmentId: string): Promise<TGoogleSpr
}
};
export async function writeData(credentials: TGoogleCredential, spreadsheetId: string, values: string[][]) {
validateInputs(
[credentials, ZGoogleCredential],
[spreadsheetId, ZString],
[values, z.array(z.array(ZString))]
);
try {
const authClient = authorize(credentials);
const sheets = google.sheets({ version: "v4", auth: authClient });

View File

@@ -3,13 +3,18 @@ import "server-only";
import { prisma } from "@formbricks/database";
import { Prisma } from "@prisma/client";
import { DatabaseError } from "@formbricks/types/v1/errors";
import { TIntegration } from "@formbricks/types/v1/integrations";
import { cache } from "react";
import { ZId } from "@formbricks/types/v1/environment";
import { TIntegration, TIntegrationInput } from "@formbricks/types/v1/integrations";
import { validateInputs } from "../utils/validate";
import { ZString, ZOptionalNumber } from "@formbricks/types/v1/common";
import { ITEMS_PER_PAGE } from "../constants";
export async function createOrUpdateIntegration(
environmentId: string,
integrationData: any
integrationData: TIntegrationInput
): Promise<TIntegration> {
validateInputs([environmentId, ZId]);
try {
const integration = await prisma.integration.upsert({
where: {
@@ -37,12 +42,16 @@ export async function createOrUpdateIntegration(
}
}
export const getIntegrations = cache(async (environmentId: string): Promise<TIntegration[]> => {
export const getIntegrations = async (environmentId: string, page?: number): Promise<TIntegration[]> => {
validateInputs([environmentId, ZId], [page, ZOptionalNumber]);
try {
const result = await prisma.integration.findMany({
where: {
environmentId,
},
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
return result;
} catch (error) {
@@ -51,9 +60,9 @@ export const getIntegrations = cache(async (environmentId: string): Promise<TInt
}
throw error;
}
});
};
export const getIntegration = cache(async (integrationId: string): Promise<TIntegration | null> => {
export const getIntegration = async (integrationId: string): Promise<TIntegration | null> => {
try {
const result = await prisma.integration.findUnique({
where: {
@@ -67,15 +76,19 @@ export const getIntegration = cache(async (integrationId: string): Promise<TInte
}
throw error;
}
});
};
export const deleteIntegration = async (integrationId: string): Promise<TIntegration> => {
validateInputs([integrationId, ZString]);
export const deleteIntegration = async (integrationId: string): Promise<void> => {
try {
await prisma.integration.delete({
const integrationData = await prisma.integration.delete({
where: {
id: integrationId,
},
});
return integrationData;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError("Database operation failed");

View File

@@ -2,11 +2,20 @@ import "server-only";
import { prisma } from "@formbricks/database";
import { Prisma } from "@prisma/client";
import { TInvite, TInviteUpdateInput } from "@formbricks/types/v1/invites";
import { cache } from "react";
import { ResourceNotFoundError, ValidationError } from "@formbricks/types/v1/errors";
import {
TInvite,
TInvitee,
ZInvitee,
TInviteUpdateInput,
ZInviteUpdateInput,
ZCurrentUser,
TCurrentUser,
} from "@formbricks/types/v1/invites";
import { ResourceNotFoundError, ValidationError, DatabaseError } from "@formbricks/types/v1/errors";
import { ZString, ZOptionalNumber } from "@formbricks/types/v1/common";
import { sendInviteMemberEmail } from "../emails/emails";
import { TMembershipRole } from "@formbricks/types/v1/memberships";
import { validateInputs } from "../utils/validate";
import { ITEMS_PER_PAGE } from "../constants";
const inviteSelect = {
id: true,
@@ -21,20 +30,22 @@ const inviteSelect = {
role: true,
};
export const getInvitesByTeamId = cache(async (teamId: string): Promise<TInvite[] | null> => {
export const getInvitesByTeamId = async (teamId: string, page?: number): Promise<TInvite[] | null> => {
validateInputs([teamId, ZString], [page, ZOptionalNumber]);
const invites = await prisma.invite.findMany({
where: { teamId },
select: inviteSelect,
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
if (!invites) {
return null;
}
return invites;
});
};
export const updateInvite = async (inviteId: string, data: TInviteUpdateInput): Promise<TInvite | null> => {
validateInputs([inviteId, ZString], [data, ZInviteUpdateInput]);
export const updateInvite = async (inviteId: string, data: TInviteUpdateInput): Promise<TInvite> => {
try {
const invite = await prisma.invite.update({
where: { id: inviteId },
@@ -53,16 +64,32 @@ export const updateInvite = async (inviteId: string, data: TInviteUpdateInput):
};
export const deleteInvite = async (inviteId: string): Promise<TInvite> => {
const deletedInvite = await prisma.invite.delete({
where: {
id: inviteId,
},
});
validateInputs([inviteId, ZString]);
return deletedInvite;
try {
const invite = await prisma.invite.delete({
where: {
id: inviteId,
},
});
if (invite === null) {
throw new ResourceNotFoundError("Invite", inviteId);
}
return invite;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError("Database operation failed");
}
throw error;
}
};
export const getInviteToken = cache(async (inviteId: string) => {
export const getInvite = async (inviteId: string): Promise<{ inviteId: string; email: string }> => {
validateInputs([inviteId, ZString]);
const invite = await prisma.invite.findUnique({
where: {
id: inviteId,
@@ -80,9 +107,10 @@ export const getInviteToken = cache(async (inviteId: string) => {
inviteId,
email: invite.email,
};
});
};
export const resendInvite = async (inviteId: string) => {
export const resendInvite = async (inviteId: string): Promise<TInvite> => {
validateInputs([inviteId, ZString]);
const invite = await prisma.invite.findUnique({
where: {
id: inviteId,
@@ -118,9 +146,11 @@ export const inviteUser = async ({
teamId,
}: {
teamId: string;
invitee: { name: string | null; email: string; role: TMembershipRole };
currentUser: { id: string; name: string | null };
}) => {
invitee: TInvitee;
currentUser: TCurrentUser;
}): Promise<TInvite> => {
validateInputs([teamId, ZString], [invitee, ZInvitee], [currentUser, ZCurrentUser]);
const { name, email, role } = invitee;
const { id: currentUserId, name: currentUserName } = currentUser;
const existingInvite = await prisma.invite.findFirst({ where: { email, teamId } });

View File

@@ -2,13 +2,23 @@ import "server-only";
import { prisma } from "@formbricks/database";
import { ResourceNotFoundError, DatabaseError, UnknownError } from "@formbricks/types/v1/errors";
import { TMember, TMembership, TMembershipUpdateInput } from "@formbricks/types/v1/memberships";
import {
TMember,
TMembership,
ZMembership,
TMembershipUpdateInput,
ZMembershipUpdateInput,
} from "@formbricks/types/v1/memberships";
import { Prisma } from "@prisma/client";
import { cache } from "react";
import { validateInputs } from "../utils/validate";
import { ZString, ZOptionalNumber } from "@formbricks/types/v1/common";
import { getTeamsByUserIdCacheTag } from "../team/service";
import { revalidateTag } from "next/cache";
import { ITEMS_PER_PAGE } from "../constants";
export const getMembersByTeamId = async (teamId: string, page?: number): Promise<TMember[]> => {
validateInputs([teamId, ZString], [page, ZOptionalNumber]);
export const getMembersByTeamId = cache(async (teamId: string): Promise<TMember[]> => {
const membersData = await prisma.membership.findMany({
where: { teamId },
select: {
@@ -22,6 +32,8 @@ export const getMembersByTeamId = cache(async (teamId: string): Promise<TMember[
accepted: true,
role: true,
},
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
const members = membersData.map((member) => {
@@ -35,26 +47,30 @@ export const getMembersByTeamId = cache(async (teamId: string): Promise<TMember[
});
return members;
});
};
export const getMembershipByUserIdTeamId = cache(
async (userId: string, teamId: string): Promise<TMembership | null> => {
const membership = await prisma.membership.findUnique({
where: {
userId_teamId: {
userId,
teamId,
},
export const getMembershipByUserIdTeamId = async (
userId: string,
teamId: string
): Promise<TMembership | null> => {
validateInputs([userId, ZString], [teamId, ZString]);
const membership = await prisma.membership.findUnique({
where: {
userId_teamId: {
userId,
teamId,
},
});
},
});
if (!membership) return null;
if (!membership) return null;
return membership;
}
);
return membership;
};
export const getMembershipsByUserId = cache(async (userId: string): Promise<TMembership[]> => {
export const getMembershipsByUserId = async (userId: string): Promise<TMembership[]> => {
validateInputs([userId, ZString]);
const memberships = await prisma.membership.findMany({
where: {
userId,
@@ -62,13 +78,14 @@ export const getMembershipsByUserId = cache(async (userId: string): Promise<TMem
});
return memberships;
});
};
export const createMembership = async (
teamId: string,
userId: string,
data: Partial<TMembership>
): Promise<TMembership> => {
validateInputs([teamId, ZString], [userId, ZString], [data, ZMembership.partial()]);
try {
const membership = await prisma.membership.create({
data: {
@@ -90,6 +107,8 @@ export const updateMembership = async (
teamId: string,
data: TMembershipUpdateInput
): Promise<TMembership> => {
validateInputs([userId, ZString], [teamId, ZString], [data, ZMembershipUpdateInput]);
try {
const membership = await prisma.membership.update({
where: {
@@ -112,6 +131,8 @@ export const updateMembership = async (
};
export const deleteMembership = async (userId: string, teamId: string): Promise<TMembership> => {
validateInputs([userId, ZString], [teamId, ZString]);
const deletedMembership = await prisma.membership.delete({
where: {
userId_teamId: {
@@ -124,9 +145,15 @@ export const deleteMembership = async (userId: string, teamId: string): Promise<
return deletedMembership;
};
export const transferOwnership = async (currentOwnerId: string, newOwnerId: string, teamId: string) => {
export const transferOwnership = async (
currentOwnerId: string,
newOwnerId: string,
teamId: string
): Promise<TMembership[]> => {
validateInputs([currentOwnerId, ZString], [newOwnerId, ZString], [teamId, ZString]);
try {
await prisma.$transaction([
const memberships = await prisma.$transaction([
prisma.membership.update({
where: {
userId_teamId: {
@@ -150,6 +177,8 @@ export const transferOwnership = async (currentOwnerId: string, newOwnerId: stri
},
}),
]);
return memberships;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError("Database operation failed");

View File

@@ -3,14 +3,13 @@ import "server-only";
import { prisma } from "@formbricks/database";
import { ZId } from "@formbricks/types/v1/environment";
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/v1/errors";
import { TPerson, TPersonUpdateInput } from "@formbricks/types/v1/people";
import { TPerson, TPersonUpdateInput, ZPersonUpdateInput } from "@formbricks/types/v1/people";
import { Prisma } from "@prisma/client";
import { revalidateTag, unstable_cache } from "next/cache";
import { cache } from "react";
import { PEOPLE_PER_PAGE } from "../constants";
import { validateInputs } from "../utils/validate";
import { getAttributeClassByName } from "../attributeClass/service";
import { SERVICES_REVALIDATION_INTERVAL } from "../constants";
import { SERVICES_REVALIDATION_INTERVAL, ITEMS_PER_PAGE } from "../constants";
import { ZString, ZOptionalNumber } from "@formbricks/types/v1/common";
export const selectPerson = {
id: true,
@@ -62,26 +61,25 @@ export const transformPrismaPerson = (person: TransformPersonInput): TPerson =>
environmentId: person.environmentId,
createdAt: person.createdAt,
updatedAt: person.updatedAt,
};
} as TPerson;
};
export const getPerson = cache(async (personId: string): Promise<TPerson | null> => {
export const getPerson = async (personId: string): Promise<TPerson | null> => {
validateInputs([personId, ZId]);
try {
const personPrisma = await prisma.person.findUnique({
const person = await prisma.person.findUnique({
where: {
id: personId,
},
select: selectPerson,
});
if (!personPrisma) {
if (!person) {
return null;
}
const person = transformPrismaPerson(personPrisma);
return person;
return transformPrismaPerson(person);
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError("Database operation failed");
@@ -89,13 +87,15 @@ export const getPerson = cache(async (personId: string): Promise<TPerson | null>
throw error;
}
});
};
const getPersonCacheKey = (personId: string): string[] => [personId];
export const getPersonCached = async (personId: string) =>
await unstable_cache(
async () => {
validateInputs([personId, ZId]);
return await getPerson(personId);
},
getPersonCacheKey(personId),
@@ -105,28 +105,26 @@ export const getPersonCached = async (personId: string) =>
}
)();
export const getPeople = cache(async (environmentId: string, page: number = 1): Promise<TPerson[]> => {
validateInputs([environmentId, ZId]);
export const getPeople = async (environmentId: string, page?: number): Promise<TPerson[]> => {
validateInputs([environmentId, ZId], [page, ZOptionalNumber]);
try {
const itemsPerPage = PEOPLE_PER_PAGE;
const people = await prisma.person.findMany({
where: {
environmentId: environmentId,
},
select: selectPerson,
take: itemsPerPage,
skip: itemsPerPage * (page - 1),
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
if (!people || people.length === 0) {
return [];
}
const transformedPeople: TPerson[] = people
return people
.map(transformPrismaPerson)
.filter((person: TPerson | null): person is TPerson => person !== null);
return transformedPeople;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError("Database operation failed");
@@ -134,17 +132,17 @@ export const getPeople = cache(async (environmentId: string, page: number = 1):
throw error;
}
});
};
export const getPeopleCount = cache(async (environmentId: string): Promise<number> => {
export const getPeopleCount = async (environmentId: string): Promise<number> => {
validateInputs([environmentId, ZId]);
try {
const totalCount = await prisma.person.count({
return await prisma.person.count({
where: {
environmentId: environmentId,
},
});
return totalCount;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
@@ -152,12 +150,13 @@ export const getPeopleCount = cache(async (environmentId: string): Promise<numbe
throw error;
}
});
};
export const createPerson = async (environmentId: string): Promise<TPerson> => {
validateInputs([environmentId, ZId]);
try {
const personPrisma = await prisma.person.create({
const person = await prisma.person.create({
data: {
environment: {
connect: {
@@ -168,14 +167,14 @@ export const createPerson = async (environmentId: string): Promise<TPerson> => {
select: selectPerson,
});
const person = transformPrismaPerson(personPrisma);
const transformedPerson = transformPrismaPerson(person);
if (person) {
if (transformedPerson) {
// revalidate person
revalidateTag(person.id);
revalidateTag(transformedPerson.id);
}
return person;
return transformedPerson;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
@@ -185,17 +184,24 @@ export const createPerson = async (environmentId: string): Promise<TPerson> => {
}
};
export const deletePerson = async (personId: string): Promise<void> => {
export const deletePerson = async (personId: string): Promise<TPerson | null> => {
validateInputs([personId, ZId]);
try {
await prisma.person.delete({
const person = await prisma.person.delete({
where: {
id: personId,
},
select: selectPerson,
});
const transformedPerson = transformPrismaPerson(person);
// revalidate person
revalidateTag(personId);
if (transformedPerson) {
// revalidate person
revalidateTag(personId);
}
return transformedPerson;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError("Database operation failed");
@@ -206,8 +212,10 @@ export const deletePerson = async (personId: string): Promise<void> => {
};
export const updatePerson = async (personId: string, personInput: TPersonUpdateInput): Promise<TPerson> => {
validateInputs([personId, ZId], [personInput, ZPersonUpdateInput]);
try {
const personPrisma = await prisma.person.update({
const person = await prisma.person.update({
where: {
id: personId,
},
@@ -215,8 +223,7 @@ export const updatePerson = async (personId: string, personInput: TPersonUpdateI
select: selectPerson,
});
const person = transformPrismaPerson(personPrisma);
return person;
return transformPrismaPerson(person);
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError("Database operation failed");
@@ -226,8 +233,10 @@ export const updatePerson = async (personId: string, personInput: TPersonUpdateI
}
};
export const getOrCreatePersonByUserId = async (userId: string, environmentId: string): Promise<TPerson> => {
validateInputs([userId, ZString], [environmentId, ZId]);
// Check if a person with the userId attribute exists
const personPrisma = await prisma.person.findFirst({
const person = await prisma.person.findFirst({
where: {
environmentId,
attributes: {
@@ -242,9 +251,8 @@ export const getOrCreatePersonByUserId = async (userId: string, environmentId: s
select: selectPerson,
});
if (personPrisma) {
const person = transformPrismaPerson(personPrisma);
return person;
if (person) {
return transformPrismaPerson(person);
} else {
// Create a new person with the userId attribute
const userIdAttributeClass = await getAttributeClassByName(environmentId, "userId");
@@ -253,7 +261,7 @@ export const getOrCreatePersonByUserId = async (userId: string, environmentId: s
throw new ResourceNotFoundError("Attribute class not found for the given environment", environmentId);
}
const personPrisma = await prisma.person.create({
const person = await prisma.person.create({
data: {
environment: {
connect: {
@@ -276,22 +284,24 @@ export const getOrCreatePersonByUserId = async (userId: string, environmentId: s
select: selectPerson,
});
if (personPrisma) {
if (person) {
// revalidate person
revalidateTag(personPrisma.id);
revalidateTag(person.id);
}
return transformPrismaPerson(personPrisma);
return transformPrismaPerson(person);
}
};
export const getMonthlyActivePeopleCount = async (environmentId: string): Promise<number> =>
await unstable_cache(
async () => {
validateInputs([environmentId, ZId]);
const now = new Date();
const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
const aggregations = await prisma.person.aggregate({
const personAggregations = await prisma.person.aggregate({
_count: {
id: true,
},
@@ -307,7 +317,7 @@ export const getMonthlyActivePeopleCount = async (environmentId: string): Promis
},
});
return aggregations._count.id;
return personAggregations._count.id;
},
[`environments-${environmentId}-mau`],
{
@@ -320,8 +330,9 @@ export const updatePersonAttribute = async (
personId: string,
attributeClassId: string,
value: string
): Promise<void> => {
await prisma.attribute.upsert({
): Promise<Partial<TPerson>> => {
validateInputs([personId, ZId], [attributeClassId, ZId], [value, ZString]);
const attributes = await prisma.attribute.upsert({
where: {
attributeClassId_personId: {
attributeClassId,
@@ -348,4 +359,6 @@ export const updatePersonAttribute = async (
// revalidate person
revalidateTag(personId);
return attributes;
};

View File

@@ -7,11 +7,11 @@ import type { TProduct, TProductUpdateInput } from "@formbricks/types/v1/product
import { ZProduct, ZProductUpdateInput } from "@formbricks/types/v1/product";
import { Prisma } from "@prisma/client";
import { revalidateTag, unstable_cache } from "next/cache";
import { cache } from "react";
import { z } from "zod";
import { SERVICES_REVALIDATION_INTERVAL } from "../constants";
import { SERVICES_REVALIDATION_INTERVAL, ITEMS_PER_PAGE } from "../constants";
import { validateInputs } from "../utils/validate";
import { createEnvironment, getEnvironmentCacheTag, getEnvironmentsCacheTag } from "../environment/service";
import { ZOptionalNumber } from "@formbricks/types/v1/common";
export const getProductsCacheTag = (teamId: string): string => `teams-${teamId}-products`;
export const getProductCacheTag = (environmentId: string): string => `environments-${environmentId}-product`;
@@ -33,16 +33,19 @@ const selectProduct = {
environments: true,
};
export const getProducts = async (teamId: string): Promise<TProduct[]> =>
export const getProducts = async (teamId: string, page?: number): Promise<TProduct[]> =>
unstable_cache(
async () => {
validateInputs([teamId, ZId]);
validateInputs([teamId, ZId], [page, ZOptionalNumber]);
try {
const products = await prisma.product.findMany({
where: {
teamId,
},
select: selectProduct,
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
return products;
@@ -61,7 +64,7 @@ export const getProducts = async (teamId: string): Promise<TProduct[]> =>
}
)();
export const getProductByEnvironmentId = cache(async (environmentId: string): Promise<TProduct | null> => {
export const getProductByEnvironmentId = async (environmentId: string): Promise<TProduct | null> => {
if (!environmentId) {
throw new ValidationError("EnvironmentId is required");
}
@@ -86,9 +89,9 @@ export const getProductByEnvironmentId = cache(async (environmentId: string): Pr
}
throw error;
}
});
};
export const getProductByEnvironmentIdCached = (environmentId: string) =>
export const getProductByEnvironmentIdCached = (environmentId: string): Promise<TProduct | null> =>
unstable_cache(
async () => {
return await getProductByEnvironmentId(environmentId);
@@ -144,7 +147,7 @@ export const updateProduct = async (
}
};
export const getProduct = cache(async (productId: string): Promise<TProduct | null> => {
export const getProduct = async (productId: string): Promise<TProduct | null> => {
let productPrisma;
try {
productPrisma = await prisma.product.findUnique({
@@ -161,9 +164,9 @@ export const getProduct = cache(async (productId: string): Promise<TProduct | nu
}
throw error;
}
});
};
export const deleteProduct = cache(async (productId: string): Promise<TProduct> => {
export const deleteProduct = async (productId: string): Promise<TProduct> => {
const product = await prisma.product.delete({
where: {
id: productId,
@@ -182,7 +185,7 @@ export const deleteProduct = cache(async (productId: string): Promise<TProduct>
}
return product;
});
};
export const createProduct = async (
teamId: string,

View File

@@ -170,7 +170,7 @@ export const createProfile = async (data: TProfileCreateInput): Promise<TProfile
};
// function to delete a user's profile including teams
export const deleteProfile = async (userId: string): Promise<void> => {
export const deleteProfile = async (userId: string): Promise<TProfile> => {
validateInputs([userId, ZId]);
try {
const currentUserMemberships = await prisma.membership.findMany({
@@ -209,7 +209,10 @@ export const deleteProfile = async (userId: string): Promise<void> => {
}
revalidateTag(getProfileCacheTag(userId));
await deleteUser(userId);
const deletedProfile = await deleteUser(userId);
return deletedProfile;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError("Database operation failed");

View File

@@ -11,15 +11,15 @@ import {
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/v1/errors";
import { TPerson } from "@formbricks/types/v1/people";
import { TTag } from "@formbricks/types/v1/tags";
import { z } from "zod";
import { cache } from "react";
import { Prisma } from "@prisma/client";
import { getPerson, transformPrismaPerson } from "../person/service";
import { captureTelemetry } from "../telemetry";
import { validateInputs } from "../utils/validate";
import { ZId } from "@formbricks/types/v1/environment";
import { revalidateTag } from "next/cache";
import { deleteDisplayByResponseId } from "../display/service";
import { Prisma } from "@prisma/client";
import { ZString, ZOptionalNumber } from "@formbricks/types/v1/common";
import { ITEMS_PER_PAGE } from "../constants";
const responseSelection = {
id: true,
@@ -84,14 +84,20 @@ export const getResponsesCacheTag = (surveyId: string) => `surveys-${surveyId}-r
export const getResponseCacheTag = (responseId: string) => `responses-${responseId}`;
export const getResponsesByPersonId = async (personId: string): Promise<Array<TResponse> | null> => {
validateInputs([personId, ZId]);
export const getResponsesByPersonId = async (
personId: string,
page?: number
): Promise<Array<TResponse> | null> => {
validateInputs([personId, ZId], [page, ZOptionalNumber]);
try {
const responsePrisma = await prisma.response.findMany({
where: {
personId,
},
select: responseSelection,
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
if (!responsePrisma) {
@@ -118,44 +124,47 @@ export const getResponsesByPersonId = async (personId: string): Promise<Array<TR
}
};
export const getResponseBySingleUseId = cache(
async (surveyId: string, singleUseId?: string): Promise<TResponse | null> => {
validateInputs([surveyId, ZId], [singleUseId, z.string()]);
try {
if (!singleUseId) {
return null;
}
const responsePrisma = await prisma.response.findUnique({
where: {
surveyId_singleUseId: { surveyId, singleUseId },
},
select: responseSelection,
});
export const getResponseBySingleUseId = async (
surveyId: string,
singleUseId?: string
): Promise<TResponse | null> => {
validateInputs([surveyId, ZId], [singleUseId, ZString]);
if (!responsePrisma) {
return null;
}
const response: TResponse = {
...responsePrisma,
person: responsePrisma.person ? transformPrismaPerson(responsePrisma.person) : null,
tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
};
return response;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError("Database operation failed");
}
throw error;
try {
if (!singleUseId) {
return null;
}
const responsePrisma = await prisma.response.findUnique({
where: {
surveyId_singleUseId: { surveyId, singleUseId },
},
select: responseSelection,
});
if (!responsePrisma) {
return null;
}
const response: TResponse = {
...responsePrisma,
person: responsePrisma.person ? transformPrismaPerson(responsePrisma.person) : null,
tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
};
return response;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError("Database operation failed");
}
throw error;
}
);
};
export const createResponse = async (responseInput: Partial<TResponseInput>): Promise<TResponse> => {
validateInputs([responseInput, ZResponseInput.partial()]);
captureTelemetry("response created");
try {
let person: TPerson | null = null;
@@ -208,6 +217,7 @@ export const createResponse = async (responseInput: Partial<TResponseInput>): Pr
export const getResponse = async (responseId: string): Promise<TResponse | null> => {
validateInputs([responseId, ZId]);
try {
const responsePrisma = await prisma.response.findUnique({
where: {
@@ -236,15 +246,11 @@ export const getResponse = async (responseId: string): Promise<TResponse | null>
}
};
export const preloadSurveyResponses = (surveyId: string) => {
validateInputs([surveyId, ZId]);
void getSurveyResponses(surveyId);
};
export const getSurveyResponses = async (surveyId: string, page?: number): Promise<TResponse[]> => {
validateInputs([surveyId, ZId], [page, ZOptionalNumber]);
export const getSurveyResponses = cache(async (surveyId: string): Promise<TResponse[]> => {
validateInputs([surveyId, ZId]);
try {
const responsesPrisma = await prisma.response.findMany({
const responses = await prisma.response.findMany({
where: {
surveyId,
},
@@ -254,15 +260,17 @@ export const getSurveyResponses = cache(async (surveyId: string): Promise<TRespo
createdAt: "desc",
},
],
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
const responses: TResponse[] = responsesPrisma.map((responsePrisma) => ({
const transformedResponses: TResponse[] = responses.map((responsePrisma) => ({
...responsePrisma,
person: responsePrisma.person ? transformPrismaPerson(responsePrisma.person) : null,
tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
}));
return responses;
return transformedResponses;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError("Database operation failed");
@@ -270,17 +278,13 @@ export const getSurveyResponses = cache(async (surveyId: string): Promise<TRespo
throw error;
}
});
export const preloadEnvironmentResponses = (environmentId: string) => {
validateInputs([environmentId, ZId]);
void getEnvironmentResponses(environmentId);
};
export const getEnvironmentResponses = cache(async (environmentId: string): Promise<TResponse[]> => {
validateInputs([environmentId, ZId]);
export const getEnvironmentResponses = async (environmentId: string, page?: number): Promise<TResponse[]> => {
validateInputs([environmentId, ZId], [page, ZOptionalNumber]);
try {
const responsesPrisma = await prisma.response.findMany({
const responses = await prisma.response.findMany({
where: {
survey: {
environmentId,
@@ -292,15 +296,17 @@ export const getEnvironmentResponses = cache(async (environmentId: string): Prom
createdAt: "desc",
},
],
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
const responses: TResponse[] = responsesPrisma.map((responsePrisma) => ({
const transformedResponses: TResponse[] = responses.map((responsePrisma) => ({
...responsePrisma,
person: responsePrisma.person ? transformPrismaPerson(responsePrisma.person) : null,
tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
}));
return responses;
return transformedResponses;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError("Database operation failed");
@@ -308,7 +314,7 @@ export const getEnvironmentResponses = cache(async (environmentId: string): Prom
throw error;
}
});
};
export const updateResponse = async (
responseId: string,

View File

@@ -7,10 +7,9 @@ import { DatabaseError } from "@formbricks/types/v1/errors";
import { TSession, TSessionWithActions } from "@formbricks/types/v1/sessions";
import { Prisma } from "@prisma/client";
import { revalidateTag, unstable_cache } from "next/cache";
import { cache } from "react";
import { validateInputs } from "../utils/validate";
const halfHourInSeconds = 60 * 30;
import { ZOptionalNumber } from "@formbricks/types/v1/common";
import { ITEMS_PER_PAGE, SERVICES_REVALIDATION_INTERVAL } from "../constants";
const getSessionCacheKey = (sessionId: string): string[] => [sessionId];
@@ -52,14 +51,15 @@ export const getSessionCached = (sessionId: string) =>
getSessionCacheKey(sessionId),
{
tags: getSessionCacheKey(sessionId),
revalidate: halfHourInSeconds, // 30 minutes
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
)();
export const getSessionWithActionsOfPerson = async (
personId: string
personId: string,
page?: number
): Promise<TSessionWithActions[] | null> => {
validateInputs([personId, ZId]);
validateInputs([personId, ZId], [page, ZOptionalNumber]);
try {
const sessionsWithActionsForPerson = await prisma.session.findMany({
where: {
@@ -81,6 +81,8 @@ export const getSessionWithActionsOfPerson = async (
},
},
},
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
if (!sessionsWithActionsForPerson) return null;
@@ -93,7 +95,7 @@ export const getSessionWithActionsOfPerson = async (
}
};
export const getSessionCount = cache(async (personId: string): Promise<number> => {
export const getSessionCount = async (personId: string): Promise<number> => {
validateInputs([personId, ZId]);
try {
const sessionCount = await prisma.session.count({
@@ -108,7 +110,7 @@ export const getSessionCount = cache(async (personId: string): Promise<number> =
}
throw error;
}
});
};
export const createSession = async (personId: string): Promise<TSession> => {
validateInputs([personId, ZId]);

View File

@@ -9,6 +9,7 @@ import {
TSurveyWithAnalytics,
ZSurvey,
ZSurveyWithAnalytics,
TSurveyInput,
} from "@formbricks/types/v1/surveys";
import { Prisma } from "@prisma/client";
import { revalidateTag, unstable_cache } from "next/cache";
@@ -17,6 +18,9 @@ import { captureTelemetry } from "../telemetry";
import { validateInputs } from "../utils/validate";
import { getDisplaysCacheTag } from "../display/service";
import { getResponsesCacheTag } from "../response/service";
import { ZString } from "@formbricks/types/v1/common";
import { SERVICES_REVALIDATION_INTERVAL } from "../constants";
import { getActionClasses } from "../actionClass/service";
// surveys cache key and tags
const getSurveysCacheTag = (environmentId: string): string => `environments-${environmentId}-surveys`;
@@ -88,6 +92,8 @@ export const selectSurveyWithAnalytics = {
};
export const getSurveyWithAnalytics = async (surveyId: string): Promise<TSurveyWithAnalytics | null> => {
validateInputs([surveyId, ZString]);
const survey = await unstable_cache(
async () => {
validateInputs([surveyId, ZId]);
@@ -126,7 +132,7 @@ export const getSurveyWithAnalytics = async (surveyId: string): Promise<TSurveyW
const transformedSurvey = {
...surveyPrismaFields,
triggers: surveyPrismaFields.triggers.map((trigger) => trigger.eventClass),
triggers: surveyPrismaFields.triggers.map((trigger) => trigger.eventClass.name),
analytics: {
numDisplays,
responseRate,
@@ -150,7 +156,7 @@ export const getSurveyWithAnalytics = async (surveyId: string): Promise<TSurveyW
[`surveyWithAnalytics-${surveyId}`],
{
tags: [getSurveyCacheTag(surveyId), getDisplaysCacheTag(surveyId), getResponsesCacheTag(surveyId)],
revalidate: 60 * 30,
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
)();
@@ -196,7 +202,7 @@ export const getSurvey = async (surveyId: string): Promise<TSurvey | null> => {
const transformedSurvey = {
...surveyPrisma,
triggers: surveyPrisma.triggers.map((trigger) => trigger.eventClass),
triggers: surveyPrisma.triggers.map((trigger) => trigger.eventClass.name),
};
try {
@@ -215,7 +221,7 @@ export const getSurvey = async (surveyId: string): Promise<TSurvey | null> => {
[`surveys-${surveyId}`],
{
tags: [getSurveyCacheTag(surveyId)],
revalidate: 60 * 30,
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
)();
@@ -250,7 +256,7 @@ export const getSurveysByAttributeClassId = async (attributeClassId: string): Pr
for (const surveyPrisma of surveysPrisma) {
const transformedSurvey = {
...surveyPrisma,
triggers: surveyPrisma.triggers.map((trigger) => trigger.eventClass),
triggers: surveyPrisma.triggers.map((trigger) => trigger.eventClass.name),
};
const survey = ZSurvey.parse(transformedSurvey);
surveys.push(survey);
@@ -287,7 +293,7 @@ export const getSurveysByActionClassId = async (actionClassId: string): Promise<
for (const surveyPrisma of surveysPrisma) {
const transformedSurvey = {
...surveyPrisma,
triggers: surveyPrisma.triggers.map((trigger) => trigger.eventClass),
triggers: surveyPrisma.triggers.map((trigger) => trigger.eventClass.name),
};
const survey = ZSurvey.parse(transformedSurvey);
surveys.push(survey);
@@ -333,7 +339,7 @@ export const getSurveys = async (environmentId: string): Promise<TSurvey[]> => {
for (const surveyPrisma of surveysPrisma) {
const transformedSurvey = {
...surveyPrisma,
triggers: surveyPrisma.triggers.map((trigger) => trigger.eventClass),
triggers: surveyPrisma.triggers.map((trigger) => trigger.eventClass.name),
};
const survey = ZSurvey.parse(transformedSurvey);
surveys.push(survey);
@@ -352,7 +358,7 @@ export const getSurveys = async (environmentId: string): Promise<TSurvey[]> => {
[`environments-${environmentId}-surveys`],
{
tags: [getSurveysCacheTag(environmentId)],
revalidate: 60 * 30,
revalidate: SERVICES_REVALIDATION_INTERVAL,
}
)();
@@ -400,7 +406,7 @@ export const getSurveysWithAnalytics = async (environmentId: string): Promise<TS
const transformedSurvey = {
...surveyPrisma,
triggers: surveyPrisma.triggers.map((trigger) => trigger.eventClass),
triggers: surveyPrisma.triggers.map((trigger) => trigger.eventClass.name),
analytics: {
numDisplays,
responseRate,
@@ -436,15 +442,17 @@ export const getSurveysWithAnalytics = async (environmentId: string): Promise<TS
}));
};
export async function updateSurvey(updatedSurvey: Partial<TSurvey>): Promise<TSurvey> {
export async function updateSurvey(updatedSurvey: TSurvey): Promise<TSurvey> {
validateInputs([updatedSurvey, ZSurvey]);
const surveyId = updatedSurvey.id;
let data: any = {};
let survey: any = { ...updatedSurvey };
if (updatedSurvey.triggers && updatedSurvey.triggers.length > 0) {
const modifiedTriggers = updatedSurvey.triggers.map((trigger) => {
if (typeof trigger === "object" && trigger.id) {
return trigger.id;
if (typeof trigger === "object" && trigger) {
return trigger;
} else if (typeof trigger === "string" && trigger !== undefined) {
return trigger;
}
@@ -453,10 +461,15 @@ export async function updateSurvey(updatedSurvey: Partial<TSurvey>): Promise<TSu
survey = { ...updatedSurvey, triggers: modifiedTriggers };
}
const actionClasses = await getActionClasses(updatedSurvey.environmentId);
const currentTriggers = await prisma.surveyTrigger.findMany({
where: {
surveyId,
},
include: {
eventClass: true,
},
});
const currentAttributeFilters = await prisma.surveyAttributeFilter.findMany({
where: {
@@ -481,30 +494,30 @@ export async function updateSurvey(updatedSurvey: Partial<TSurvey>): Promise<TSu
const newTriggers: string[] = [];
const removedTriggers: string[] = [];
// find added triggers
for (const eventClassId of survey.triggers) {
if (!eventClassId) {
for (const eventClassName of survey.triggers) {
if (!eventClassName) {
continue;
}
if (currentTriggers.find((t) => t.eventClassId === eventClassId)) {
if (currentTriggers.find((t) => t.eventClass.name === eventClassName)) {
continue;
} else {
newTriggers.push(eventClassId);
newTriggers.push(eventClassName);
}
}
// find removed triggers
for (const trigger of currentTriggers) {
if (survey.triggers.find((t: any) => t === trigger.eventClassId)) {
if (survey.triggers.find((t: any) => t === trigger.eventClass.name)) {
continue;
} else {
removedTriggers.push(trigger.eventClassId);
removedTriggers.push(trigger.eventClass.name);
}
}
// create new triggers
if (newTriggers.length > 0) {
data.triggers = {
...(data.triggers || []),
create: newTriggers.map((eventClassId) => ({
eventClassId,
create: newTriggers.map((eventClassName) => ({
eventClassId: actionClasses.find((actionClass) => actionClass.name === eventClassName)!.id,
})),
};
}
@@ -638,22 +651,37 @@ export async function deleteSurvey(surveyId: string) {
return deletedSurvey;
}
export async function createSurvey(environmentId: string, surveyBody: any) {
export async function createSurvey(environmentId: string, surveyBody: TSurveyInput): Promise<TSurvey> {
validateInputs([environmentId, ZId]);
// TODO: Create with triggers & attributeFilters
delete surveyBody.triggers;
delete surveyBody.attributeFilters;
const data: Omit<TSurveyInput, "triggers" | "attributeFilters"> = {
...surveyBody,
};
const survey = await prisma.survey.create({
data: {
...surveyBody,
...data,
environment: {
connect: {
id: environmentId,
},
},
},
select: selectSurvey,
});
const transformedSurvey = {
...survey,
triggers: survey.triggers.map((trigger) => trigger.eventClass.name),
};
captureTelemetry("survey created");
revalidateTag(getSurveysCacheTag(environmentId));
revalidateTag(getSurveyCacheTag(survey.id));
return survey;
return transformedSurvey;
}

View File

@@ -2,9 +2,8 @@ import "server-only";
import { prisma } from "@formbricks/database";
import { TTag } from "@formbricks/types/v1/tags";
import { cache } from "react";
export const getTagsByEnvironmentId = cache(async (environmentId: string): Promise<TTag[]> => {
export const getTagsByEnvironmentId = async (environmentId: string): Promise<TTag[]> => {
try {
const tags = await prisma.tag.findMany({
where: {
@@ -16,7 +15,7 @@ export const getTagsByEnvironmentId = cache(async (environmentId: string): Promi
} catch (error) {
throw error;
}
});
};
export const getTag = async (tagId: string): Promise<TTag | null> => {
try {

View File

@@ -1,13 +1,12 @@
import "server-only";
import { prisma } from "@formbricks/database";
import { TTagsCount } from "@formbricks/types/v1/tags";
import { cache } from "react";
import { TTagsCount, TTagsOnResponses } from "@formbricks/types/v1/tags";
export const getTagOnResponseCacheTag = (tagId: string, responseId: string) =>
`tagsOnResponse-${tagId}-${responseId}`;
export const addTagToRespone = async (responseId: string, tagId: string) => {
export const addTagToRespone = async (responseId: string, tagId: string): Promise<TTagsOnResponses> => {
try {
const tagOnResponse = await prisma.tagsOnResponses.create({
data: {
@@ -21,7 +20,7 @@ export const addTagToRespone = async (responseId: string, tagId: string) => {
}
};
export const deleteTagOnResponse = async (responseId: string, tagId: string) => {
export const deleteTagOnResponse = async (responseId: string, tagId: string): Promise<TTagsOnResponses> => {
try {
const deletedTag = await prisma.tagsOnResponses.delete({
where: {
@@ -37,7 +36,7 @@ export const deleteTagOnResponse = async (responseId: string, tagId: string) =>
}
};
export const getTagsOnResponsesCount = cache(async (): Promise<TTagsCount> => {
export const getTagsOnResponsesCount = async (): Promise<TTagsCount> => {
try {
const tagsCount = await prisma.tagsOnResponses.groupBy({
by: ["tagId"],
@@ -50,4 +49,4 @@ export const getTagsOnResponsesCount = cache(async (): Promise<TTagsCount> => {
} catch (error) {
throw error;
}
});
};

View File

@@ -4,6 +4,7 @@ import { prisma } from "@formbricks/database";
import { ZId } from "@formbricks/types/v1/environment";
import { DatabaseError, ResourceNotFoundError, ValidationError } from "@formbricks/types/v1/errors";
import { TTeam, TTeamUpdateInput } from "@formbricks/types/v1/teams";
import { TProductUpdateInput } from "@formbricks/types/v1/product";
import { createId } from "@paralleldrive/cuid2";
import { Prisma } from "@prisma/client";
import { revalidateTag, unstable_cache } from "next/cache";
@@ -27,6 +28,7 @@ import {
} from "../utils/createDemoProductHelpers";
import { validateInputs } from "../utils/validate";
import { SERVICES_REVALIDATION_INTERVAL } from "../constants";
import { getEnvironmentCacheTag } from "../environment/service";
export const select = {
id: true,
@@ -158,7 +160,7 @@ export const updateTeam = async (teamId: string, data: Partial<TTeamUpdateInput>
}
};
export const deleteTeam = async (teamId: string) => {
export const deleteTeam = async (teamId: string): Promise<TTeam> => {
validateInputs([teamId, ZId]);
try {
const deletedTeam = await prisma.team.delete({
@@ -177,6 +179,7 @@ export const deleteTeam = async (teamId: string) => {
deletedTeam?.products.forEach((product) => {
product.environments.forEach((environment) => {
revalidateTag(getTeamByEnvironmentIdCacheTag(environment.id));
revalidateTag(getEnvironmentCacheTag(environment.id));
});
});
@@ -196,7 +199,7 @@ export const deleteTeam = async (teamId: string) => {
}
};
export const createDemoProduct = async (teamId: string) => {
export const createDemoProduct = async (teamId: string): Promise<TProductUpdateInput> => {
validateInputs([teamId, ZId]);
const demoProduct = await prisma.product.create({
@@ -332,7 +335,7 @@ export const createDemoProduct = async (teamId: string) => {
})),
}),
]);
} catch (error: any) {
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError("Database operation failed");
}

View File

@@ -6,7 +6,9 @@ import { validateInputs } from "../utils/validate";
import { ZId } from "@formbricks/types/v1/environment";
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/v1/errors";
export const getTeamDetails = async (environmentId: string) => {
export const getTeamDetails = async (
environmentId: string
): Promise<{ teamId: string; teamOwnerId: string | undefined }> => {
validateInputs([environmentId, ZId]);
try {
const environment = await prisma.environment.findUnique({

View File

@@ -5,10 +5,9 @@ import { prisma } from "@formbricks/database";
import { Prisma } from "@prisma/client";
import { validateInputs } from "../utils/validate";
import { ZId } from "@formbricks/types/v1/environment";
import { cache } from "react";
import { ResourceNotFoundError, DatabaseError, InvalidInputError } from "@formbricks/types/v1/errors";
export const getWebhooks = cache(async (environmentId: string): Promise<TWebhook[]> => {
export const getWebhooks = async (environmentId: string): Promise<TWebhook[]> => {
validateInputs([environmentId, ZId]);
try {
const webhooks = await prisma.webhook.findMany({
@@ -20,7 +19,7 @@ export const getWebhooks = cache(async (environmentId: string): Promise<TWebhook
} catch (error) {
throw new DatabaseError(`Database error when fetching webhooks for environment ${environmentId}`);
}
});
};
export const getCountOfWebhooksBasedOnSource = async (
environmentId: string,

View File

@@ -10,3 +10,12 @@ export const ZAction = z.object({
});
export type TAction = z.infer<typeof ZAction>;
export const ZActionInput = z.object({
environmentId: z.string().cuid2(),
sessionId: z.string().cuid2(),
name: z.string(),
properties: z.record(z.string()),
});
export type TActionInput = z.infer<typeof ZActionInput>;

View File

@@ -1,4 +1,7 @@
import { z } from "zod";
export const ZString = z.string();
export const ZNumber = z.number();
export const ZOptionalNumber = z.number().optional();
export const ZColor = z.string().regex(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/);
export const ZSurveyPlacement = z.enum(["bottomLeft", "bottomRight", "topLeft", "topRight", "center"]);

View File

@@ -1,8 +1,6 @@
import { z } from "zod";
import { ZEnvironment } from "./environment";
// Define a specific schema for googleSheets config
/* GOOGLE SHEETS CONFIGURATIONS */
export const ZGoogleCredential = z.object({
scope: z.string(),
token_type: z.literal("Bearer"),
@@ -10,11 +8,13 @@ export const ZGoogleCredential = z.object({
access_token: z.string(),
refresh_token: z.string(),
});
export type TGoogleCredential = z.infer<typeof ZGoogleCredential>;
export const ZGoogleSpreadsheet = z.object({
name: z.string(),
id: z.string(),
});
export type TGoogleSpreadsheet = z.infer<typeof ZGoogleSpreadsheet>;
export const ZGoogleSheetsConfigData = z.object({
createdAt: z.date(),
@@ -25,26 +25,14 @@ export const ZGoogleSheetsConfigData = z.object({
surveyId: z.string(),
surveyName: z.string(),
});
export type TGoogleSheetsConfigData = z.infer<typeof ZGoogleSheetsConfigData>;
const ZGoogleSheetsConfig = z.object({
key: ZGoogleCredential,
data: z.array(ZGoogleSheetsConfigData),
email: z.string(),
});
// Define a dynamic schema for config based on integration type
const ZPlaceholderConfig = z.object({
placeholder: z.string(),
});
export const ZIntegrationConfig = z.union([ZGoogleSheetsConfig, ZPlaceholderConfig]);
export const ZIntegration = z.object({
id: z.string(),
type: z.enum(["googleSheets", "placeholder"]),
environmentId: z.string(),
config: ZIntegrationConfig,
});
export type TGoogleSheetsConfig = z.infer<typeof ZGoogleSheetsConfig>;
export const ZGoogleSheetIntegration = z.object({
id: z.string(),
@@ -52,20 +40,23 @@ export const ZGoogleSheetIntegration = z.object({
environmentId: z.string(),
config: ZGoogleSheetsConfig,
});
export const ZPlaceHolderIntegration = z.object({
id: z.string(),
type: z.enum(["placeholder"]),
environmentId: z.string(),
config: ZPlaceholderConfig,
environment: ZEnvironment,
});
export type TIntegration = z.infer<typeof ZIntegration>;
export type TIntegrationConfig = z.infer<typeof ZIntegrationConfig>;
export type TGoogleCredential = z.infer<typeof ZGoogleCredential>;
export type TGoogleSpreadsheet = z.infer<typeof ZGoogleSpreadsheet>;
export type TGoogleSheetsConfig = z.infer<typeof ZGoogleSheetsConfig>;
export type TGoogleSheetsConfigData = z.infer<typeof ZGoogleSheetsConfigData>;
export type TGoogleSheetIntegration = z.infer<typeof ZGoogleSheetIntegration>;
export type TPlaceHolderIntegration = z.infer<typeof ZPlaceHolderIntegration>;
// Define a specific schema for integration configs
// When we add other configurations it will be z.union([ZGoogleSheetsConfig, ZSlackConfig, ...])
export const ZIntegrationConfig = ZGoogleSheetsConfig;
export type TIntegrationConfig = z.infer<typeof ZIntegrationConfig>;
export const ZIntegration = z.object({
id: z.string(),
type: z.enum(["googleSheets"]),
environmentId: z.string(),
config: ZIntegrationConfig,
});
export type TIntegration = z.infer<typeof ZIntegration>;
export const ZIntegrationInput = z.object({
type: z.enum(["googleSheets"]),
config: ZIntegrationConfig,
});
export type TIntegrationInput = z.infer<typeof ZIntegrationInput>;

View File

@@ -1,5 +1,4 @@
import z from "zod";
import { Prisma } from "@prisma/client";
import { ZMembershipRole } from "./memberships";
const ZInvite = z.object({
@@ -14,6 +13,22 @@ const ZInvite = z.object({
expiresAt: z.date(),
role: ZMembershipRole,
});
export type TInvite = z.infer<typeof ZInvite>;
export type TInviteUpdateInput = Prisma.InviteUpdateInput;
export const ZInvitee = z.object({
email: z.string(),
name: z.string().nullable(),
role: ZMembershipRole,
});
export type TInvitee = z.infer<typeof ZInvitee>;
export const ZCurrentUser = z.object({
id: z.string(),
name: z.string().nullable(),
});
export type TCurrentUser = z.infer<typeof ZCurrentUser>;
export const ZInviteUpdateInput = z.object({
role: ZMembershipRole,
});
export type TInviteUpdateInput = z.infer<typeof ZInviteUpdateInput>;

View File

@@ -48,12 +48,3 @@ export const ZJsPeopleAttributeInput = z.object({
});
export type TJsPeopleAttributeInput = z.infer<typeof ZJsPeopleAttributeInput>;
export const ZJsActionInput = z.object({
environmentId: z.string().cuid2(),
sessionId: z.string().cuid2(),
name: z.string(),
properties: z.record(z.string()),
});
export type TJsActionInput = z.infer<typeof ZJsActionInput>;

View File

@@ -1,5 +1,4 @@
import z from "zod";
import { Prisma } from "@prisma/client";
export const ZMembershipRole = z.enum(["owner", "admin", "editor", "developer", "viewer"]);
@@ -24,4 +23,7 @@ export const ZMember = z.object({
export type TMember = z.infer<typeof ZMember>;
export type TMembershipUpdateInput = Prisma.MembershipUpdateInput;
export const ZMembershipUpdateInput = z.object({
role: ZMembershipRole,
});
export type TMembershipUpdateInput = z.infer<typeof ZMembershipUpdateInput>;

View File

@@ -1,5 +1,4 @@
import { z } from "zod";
import { ZActionClass } from "./actionClasses";
import { QuestionType } from "../questions";
import { ZColor, ZSurveyPlacement } from "./common";
@@ -276,7 +275,7 @@ export const ZSurvey = z.object({
attributeFilters: z.array(ZSurveyAttributeFilter),
displayOption: ZSurveyDisplayOption,
autoClose: z.number().nullable(),
triggers: z.array(ZActionClass),
triggers: z.array(z.string()),
redirectUrl: z.string().url().nullable(),
recontactDays: z.number().nullable(),
questions: ZSurveyQuestions,
@@ -293,7 +292,6 @@ export const ZSurvey = z.object({
export const ZSurveyInput = z.object({
name: z.string(),
type: ZSurveyType.optional(),
environmentId: z.string(),
status: ZSurveyStatus.optional(),
displayOption: ZSurveyDisplayOption.optional(),
autoClose: z.number().optional(),
@@ -306,9 +304,8 @@ export const ZSurveyInput = z.object({
closeOnDate: z.date().optional(),
surveyClosedMessage: ZSurveyClosedMessage.optional(),
verifyEmail: ZSurveyVerifyEmail.optional(),
// TODO: Update survey create endpoint to accept attributeFilters and triggers like the survey update endpoint
// attributeFilters: z.array(ZSurveyAttributeFilter).optional(),
//triggers: z.array(ZActionClass).optional(),
attributeFilters: z.array(ZSurveyAttributeFilter).optional(),
triggers: z.array(z.string()).optional(),
});
export type TSurvey = z.infer<typeof ZSurvey>;

View File

@@ -1,7 +1,5 @@
import { z } from "zod";
export type TTag = z.infer<typeof ZTag>;
export const ZTag = z.object({
id: z.string().cuid2(),
createdAt: z.date(),
@@ -9,8 +7,7 @@ export const ZTag = z.object({
name: z.string(),
environmentId: z.string(),
});
export type TTagsCount = z.infer<typeof ZTagsCount>;
export type TTag = z.infer<typeof ZTag>;
export const ZTagsCount = z.array(
z.object({
@@ -18,3 +15,10 @@ export const ZTagsCount = z.array(
count: z.number(),
})
);
export type TTagsCount = z.infer<typeof ZTagsCount>;
export const ZTagsOnResponses = z.object({
responseId: z.string(),
tagId: z.string(),
});
export type TTagsOnResponses = z.infer<typeof ZTagsOnResponses>;