diff --git a/.gitignore b/.gitignore index 036ab1cd7e..b0a21d17e7 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,9 @@ yarn-error.log* .env.production.local !packages/database/.env +# Prisma generated files +packages/database/zod + # turbo .turbo diff --git a/apps/formbricks-com/pages/docs/contributing/setup/index.mdx b/apps/formbricks-com/pages/docs/contributing/setup/index.mdx index 13d1443f30..60068258b6 100644 --- a/apps/formbricks-com/pages/docs/contributing/setup/index.mdx +++ b/apps/formbricks-com/pages/docs/contributing/setup/index.mdx @@ -49,7 +49,7 @@ To get the project running locally on your machine you need to have the followin 1. Make sure your Docker containers are running. Then let prisma set up the database for you: ```bash - pnpm db:migrate:dev + pnpm prisma migrate dev ``` 1. Start the development server of the app: diff --git a/apps/web/app/api/v1/client/responses/[responseId]/route.ts b/apps/web/app/api/v1/client/responses/[responseId]/route.ts index 85396a1baa..ce97dd73a5 100644 --- a/apps/web/app/api/v1/client/responses/[responseId]/route.ts +++ b/apps/web/app/api/v1/client/responses/[responseId]/route.ts @@ -1,54 +1,49 @@ import { responses } from "@/lib/api/response"; -import { prisma } from "@formbricks/database"; +import { transformErrorToDetails } from "@/lib/api/validator"; import { INTERNAL_SECRET, WEBAPP_URL } from "@formbricks/lib/constants"; +import { getResponse, updateResponse } from "@formbricks/lib/services/response"; +import { getSurvey } from "@formbricks/lib/services/survey"; +import { ZResponseUpdateInput } from "@formbricks/types/v1/responses"; import { NextResponse } from "next/server"; export async function OPTIONS(): Promise { return responses.successResponse({}, true); } -export async function PUT(request: Request, params: { responseId: string }): Promise { +export async function PUT( + request: Request, + { params }: { params: { responseId: string } } +): Promise { const { responseId } = params; - const { response } = await request.json(); + const responseUpdate = await request.json(); - if (!response) { - return responses.missingFieldResponse("response", true); + const inputValidation = ZResponseUpdateInput.safeParse(responseUpdate); + + if (!inputValidation.success) { + return responses.badRequestResponse( + "Fields are missing or incorrectly formatted", + transformErrorToDetails(inputValidation.error), + true + ); } - const currentResponse = await prisma.response.findUnique({ - where: { - id: responseId, - }, - select: { - data: true, - survey: { - select: { - environmentId: true, - }, - }, - }, - }); + // get current response + const currentResponse = await getResponse(responseId); if (!currentResponse) { return responses.notFoundResponse("Response", responseId, true); } - const environmentId = currentResponse.survey.environmentId; - - const newResponseData = { - ...JSON.parse(JSON.stringify(currentResponse?.data)), - ...response.data, - }; + // get survey to get environmentId + const survey = await getSurvey(currentResponse.surveyId); + if (!survey) { + // shouldn't happen as survey relation is required + return responses.notFoundResponse("Survey", currentResponse.surveyId, true); + } + const environmentId = survey.environmentId; // update response - const responseData = await prisma.response.update({ - where: { - id: responseId, - }, - data: { - ...{ ...response, data: newResponseData }, - }, - }); + const response = await updateResponse(responseId, responseUpdate); // send response update to pipeline // don't await to not block the response @@ -60,8 +55,9 @@ export async function PUT(request: Request, params: { responseId: string }): Pro body: JSON.stringify({ internalSecret: INTERNAL_SECRET, environmentId, + surveyId: response.surveyId, event: "responseUpdated", - data: { id: responseId, ...response }, + data: response, }), }); @@ -76,11 +72,12 @@ export async function PUT(request: Request, params: { responseId: string }): Pro body: JSON.stringify({ internalSecret: INTERNAL_SECRET, environmentId, + surveyId: response.surveyId, event: "responseFinished", - data: responseData, + data: response, }), }); } - return responses.successResponse({ ...responseData }, true); + return responses.successResponse(response, true); } diff --git a/apps/web/app/api/v1/client/responses/route.ts b/apps/web/app/api/v1/client/responses/route.ts index 674bbb2e8d..792e810e75 100644 --- a/apps/web/app/api/v1/client/responses/route.ts +++ b/apps/web/app/api/v1/client/responses/route.ts @@ -1,13 +1,12 @@ -/* -THIS FILE IS WORK IN PROGRESS -PLEASE DO NOT USE IT YET -*/ - import { responses } from "@/lib/api/response"; import { prisma } from "@formbricks/database"; +import { transformErrorToDetails } from "@/lib/api/validator"; +import { createResponse } from "@formbricks/lib/services/response"; import { INTERNAL_SECRET, WEBAPP_URL } from "@formbricks/lib/constants"; -import { captureTelemetry } from "@formbricks/lib/telemetry"; +import { getSurvey } from "@formbricks/lib/services/survey"; +import { TResponseInput, ZResponseInput } from "@formbricks/types/v1/responses"; import { NextResponse } from "next/server"; +import { captureTelemetry } from "@formbricks/lib/telemetry"; import { capturePosthogEvent } from "@formbricks/lib/posthogServer"; export async function OPTIONS(): Promise { @@ -15,80 +14,67 @@ export async function OPTIONS(): Promise { } export async function POST(request: Request): Promise { - const { surveyId, userCuid, response } = await request.json(); + const responseInput: TResponseInput = await request.json(); + const inputValidation = ZResponseInput.safeParse(responseInput); - if (!surveyId) { - return responses.missingFieldResponse("surveyId", true); + if (!inputValidation.success) { + return responses.badRequestResponse( + "Fields are missing or incorrectly formatted", + transformErrorToDetails(inputValidation.error), + true + ); } - if (!response) { - return responses.missingFieldResponse("response", true); - } - - // userCuid can be null, e.g. for link surveys - // check if survey exists - const survey = await prisma.survey.findUnique({ - where: { - id: surveyId, - }, - select: { - id: true, - environment: { + const survey = await getSurvey(responseInput.surveyId); + + if (!survey) { + return responses.badRequestResponse( + "Linked ressource not found", + { + surveyId: "Survey not found", + }, + true + ); + } + + const environmentId = survey.environmentId; + + // prisma call to get the teamId + // TODO use services + const environment = await prisma.environment.findUnique({ + where: { id: environmentId }, + include: { + product: { select: { - id: true, - product: { + team: { select: { - team: { - select: { - id: true, - memberships: { - select: { - userId: true, - role: true, - }, - }, - }, + id: true, + memberships: { + where: { role: "owner" }, + select: { userId: true }, + take: 1, }, }, }, }, }, - type: true, }, }); - if (!survey) { - return responses.notFoundResponse("Survey", surveyId, true); + if (!environment) { + throw new Error("Environment not found"); } - const environmentId = survey.environment.id; - - const teamId = survey.environment.product.team.id; - // find team owner - const teamOwnerId = survey.environment.product.team.memberships.find((m) => m.role === "owner")?.userId; - - const createBody = { - data: { - survey: { - connect: { - id: surveyId, - }, - }, - ...response, + const { + product: { + team: { id: teamId, memberships }, }, - }; + } = environment; - if (userCuid) { - createBody.data.person = { - connect: { - id: userCuid, - }, - }; - } + const teamOwnerId = memberships[0]?.userId; - // create new response - const responseData = await prisma.response.create(createBody); + const response = await createResponse(responseInput); // send response to pipeline // don't await to not block the response @@ -100,12 +86,13 @@ export async function POST(request: Request): Promise { body: JSON.stringify({ internalSecret: INTERNAL_SECRET, environmentId, + surveyId: response.surveyId, event: "responseCreated", - data: responseData, + data: response, }), }); - if (response.finished) { + if (responseInput.finished) { // send response to pipeline // don't await to not block the response fetch(`${WEBAPP_URL}/api/pipeline`, { @@ -116,8 +103,9 @@ export async function POST(request: Request): Promise { body: JSON.stringify({ internalSecret: INTERNAL_SECRET, environmentId, + surveyId: response.surveyId, event: "responseFinished", - data: responseData, + data: response, }), }); } @@ -125,12 +113,12 @@ export async function POST(request: Request): Promise { captureTelemetry("response created"); if (teamOwnerId) { await capturePosthogEvent(teamOwnerId, "response created", teamId, { - surveyId, + surveyId: response.surveyId, surveyType: survey.type, }); } else { console.warn("Posthog capture not possible. No team owner found"); } - return responses.successResponse({ id: responseData.id }, true); + return responses.successResponse(response, true); } diff --git a/apps/web/app/environments/[environmentId]/surveys/[surveyId]/edit/SurveyMenuBar.tsx b/apps/web/app/environments/[environmentId]/surveys/[surveyId]/edit/SurveyMenuBar.tsx index 3bdaefd415..ca9e2b4221 100644 --- a/apps/web/app/environments/[environmentId]/surveys/[surveyId]/edit/SurveyMenuBar.tsx +++ b/apps/web/app/environments/[environmentId]/surveys/[surveyId]/edit/SurveyMenuBar.tsx @@ -138,6 +138,7 @@ export default function SurveyMenuBar({