From e691c076a14032e2cd5dee2fe606df7e0ca4995b Mon Sep 17 00:00:00 2001 From: Piyush Gupta <56182734+gupta-piyush19@users.noreply.github.com> Date: Fri, 24 Jan 2025 17:53:07 +0530 Subject: [PATCH] chore: adds webhook types (#4606) Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com> --- .../integrations/lib/webhook.ts | 35 +++ .../[environmentId]/integrations/page.tsx | 2 +- .../integrations/webhooks/page.tsx | 77 +----- .../pipeline/lib/handleIntegrations.ts | 2 +- apps/web/app/api/(internal)/pipeline/route.ts | 10 +- .../(internal)/pipeline/types/pipelines.ts | 12 + .../v1/webhooks/[webhookId]/lib/webhook.ts | 59 ++++ .../app/api/v1/webhooks/[webhookId]/route.ts | 2 +- apps/web/app/api/v1/webhooks/lib/webhook.ts | 73 +++++ apps/web/app/api/v1/webhooks/route.ts | 4 +- .../web/app/api/v1/webhooks/types/webhooks.ts | 16 ++ apps/web/app/lib/pipelines.ts | 2 +- apps/web/app/lib/types/pipelines.ts | 9 + .../cache.ts => apps/web/lib/cache/webhook.ts | 6 +- apps/web/lib/utils/services.ts | 2 +- .../integrations/webhooks/actions.ts | 10 +- .../components/add-webhook-button.tsx} | 2 +- .../components/add-webhook-modal.tsx} | 14 +- .../components/survey-checkbox-group.tsx} | 0 .../components/trigger-checkbox-group.tsx} | 17 +- .../components/webhook-detail-modal.tsx} | 12 +- .../components/webhook-overview-tab.tsx} | 6 +- .../webhooks/components/webhook-row-data.tsx} | 8 +- .../components/webhook-settings-tab.tsx} | 14 +- .../components/webhook-table-heading.tsx} | 0 .../webhooks/components/webhook-table.tsx} | 10 +- .../integrations/webhooks/lib/utils.ts | 0 .../integrations/webhooks/lib/webhook.ts | 251 ++++++++---------- .../modules/integrations/webhooks/page.tsx | 68 +++++ .../integrations/webhooks/types/webhooks.ts | 16 ++ packages/database/zod/webhooks.ts | 14 + packages/lib/webhook/auth.ts | 29 -- packages/lib/webhook/utils.ts | 26 -- packages/types/pipelines.ts | 15 -- packages/types/webhooks.ts | 28 -- 35 files changed, 481 insertions(+), 370 deletions(-) create mode 100644 apps/web/app/(app)/environments/[environmentId]/integrations/lib/webhook.ts create mode 100644 apps/web/app/api/(internal)/pipeline/types/pipelines.ts create mode 100644 apps/web/app/api/v1/webhooks/[webhookId]/lib/webhook.ts create mode 100644 apps/web/app/api/v1/webhooks/lib/webhook.ts create mode 100644 apps/web/app/api/v1/webhooks/types/webhooks.ts create mode 100644 apps/web/app/lib/types/pipelines.ts rename packages/lib/webhook/cache.ts => apps/web/lib/cache/webhook.ts (80%) rename apps/web/{app/(app)/environments/[environmentId] => modules}/integrations/webhooks/actions.ts (92%) rename apps/web/{app/(app)/environments/[environmentId]/integrations/webhooks/components/AddWebhookButton.tsx => modules/integrations/webhooks/components/add-webhook-button.tsx} (95%) rename apps/web/{app/(app)/environments/[environmentId]/integrations/webhooks/components/AddWebhookModal.tsx => modules/integrations/webhooks/components/add-webhook-modal.tsx} (93%) rename apps/web/{app/(app)/environments/[environmentId]/integrations/webhooks/components/SurveyCheckboxGroup.tsx => modules/integrations/webhooks/components/survey-checkbox-group.tsx} (100%) rename apps/web/{app/(app)/environments/[environmentId]/integrations/webhooks/components/TriggerCheckboxGroup.tsx => modules/integrations/webhooks/components/trigger-checkbox-group.tsx} (83%) rename apps/web/{app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookDetailModal.tsx => modules/integrations/webhooks/components/webhook-detail-modal.tsx} (70%) rename apps/web/{app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookOverviewTab.tsx => modules/integrations/webhooks/components/webhook-overview-tab.tsx} (95%) rename apps/web/{app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookRowData.tsx => modules/integrations/webhooks/components/webhook-row-data.tsx} (93%) rename apps/web/{app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookSettingsTab.tsx => modules/integrations/webhooks/components/webhook-settings-tab.tsx} (93%) rename apps/web/{app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookTableHeading.tsx => modules/integrations/webhooks/components/webhook-table-heading.tsx} (100%) rename apps/web/{app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookTable.tsx => modules/integrations/webhooks/components/webhook-table.tsx} (85%) rename apps/web/{app/(app)/environments/[environmentId] => modules}/integrations/webhooks/lib/utils.ts (100%) rename packages/lib/webhook/service.ts => apps/web/modules/integrations/webhooks/lib/webhook.ts (50%) create mode 100644 apps/web/modules/integrations/webhooks/page.tsx create mode 100644 apps/web/modules/integrations/webhooks/types/webhooks.ts create mode 100644 packages/database/zod/webhooks.ts delete mode 100644 packages/lib/webhook/auth.ts delete mode 100644 packages/lib/webhook/utils.ts delete mode 100644 packages/types/pipelines.ts delete mode 100644 packages/types/webhooks.ts diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/lib/webhook.ts b/apps/web/app/(app)/environments/[environmentId]/integrations/lib/webhook.ts new file mode 100644 index 0000000000..f7b024ed66 --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/lib/webhook.ts @@ -0,0 +1,35 @@ +import { webhookCache } from "@/lib/cache/webhook"; +import { Prisma, Webhook } from "@prisma/client"; +import { z } from "zod"; +import { prisma } from "@formbricks/database"; +import { cache } from "@formbricks/lib/cache"; +import { validateInputs } from "@formbricks/lib/utils/validate"; +import { ZId } from "@formbricks/types/common"; +import { DatabaseError } from "@formbricks/types/errors"; + +export const getWebhookCountBySource = (environmentId: string, source?: Webhook["source"]): Promise => + cache( + async () => { + validateInputs([environmentId, ZId], [source, z.string().optional()]); + + try { + const count = await prisma.webhook.count({ + where: { + environmentId, + source, + }, + }); + return count; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + + throw error; + } + }, + [`getWebhookCountBySource-${environmentId}-${source}`], + { + tags: [webhookCache.tag.byEnvironmentIdAndSource(environmentId, source)], + } + )(); diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/page.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/page.tsx index 48b5be7dad..52598676e9 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/page.tsx @@ -1,3 +1,4 @@ +import { getWebhookCountBySource } from "@/app/(app)/environments/[environmentId]/integrations/lib/webhook"; import AirtableLogo from "@/images/airtableLogo.svg"; import GoogleSheetsLogo from "@/images/googleSheetsLogo.png"; import JsLogo from "@/images/jslogo.png"; @@ -22,7 +23,6 @@ import { getIntegrations } from "@formbricks/lib/integration/service"; import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; import { getAccessFlags } from "@formbricks/lib/membership/utils"; import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service"; -import { getWebhookCountBySource } from "@formbricks/lib/webhook/service"; import { TIntegrationType } from "@formbricks/types/integration"; const Page = async (props) => { diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/page.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/page.tsx index 4bd8609359..5f15b859bf 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/page.tsx @@ -1,76 +1,3 @@ -import { AddWebhookButton } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/components/AddWebhookButton"; -import { WebhookRowData } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookRowData"; -import { WebhookTable } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookTable"; -import { WebhookTableHeading } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookTableHeading"; -import { authOptions } from "@/modules/auth/lib/authOptions"; -import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles"; -import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams"; -import { GoBackButton } from "@/modules/ui/components/go-back-button"; -import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper"; -import { PageHeader } from "@/modules/ui/components/page-header"; -import { getServerSession } from "next-auth"; -import { getTranslations } from "next-intl/server"; -import { getEnvironment } from "@formbricks/lib/environment/service"; -import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; -import { getAccessFlags } from "@formbricks/lib/membership/utils"; -import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service"; -import { getSurveys } from "@formbricks/lib/survey/service"; -import { findMatchingLocale } from "@formbricks/lib/utils/locale"; -import { getWebhooks } from "@formbricks/lib/webhook/service"; +import { WebhooksPage } from "@/modules/integrations/webhooks/page"; -const Page = async (props) => { - const params = await props.params; - const t = await getTranslations(); - const [session, organization, webhooksUnsorted, surveys, environment] = await Promise.all([ - getServerSession(authOptions), - getOrganizationByEnvironmentId(params.environmentId), - getWebhooks(params.environmentId), - getSurveys(params.environmentId, 200), // HOTFIX: not getting all surveys for now since it's maxing out the prisma accelerate limit - getEnvironment(params.environmentId), - ]); - - if (!session) { - throw new Error(t("common.session_not_found")); - } - - if (!environment) { - throw new Error(t("common.environment_not_found")); - } - - if (!organization) { - throw new Error(t("common.organization_not_found")); - } - - const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id); - const { isMember } = getAccessFlags(currentUserMembership?.role); - - const projectPermission = await getProjectPermissionByUserId(session?.user.id, environment?.projectId); - - const { hasReadAccess } = getTeamPermissionFlags(projectPermission); - - const isReadOnly = isMember && hasReadAccess; - - const webhooks = webhooksUnsorted.sort((a, b) => { - if (a.createdAt > b.createdAt) return -1; - if (a.createdAt < b.createdAt) return 1; - return 0; - }); - - const renderAddWebhookButton = () => ; - const locale = await findMatchingLocale(); - - return ( - - - } /> - - - {webhooks.map((webhook) => ( - - ))} - - - ); -}; - -export default Page; +export default WebhooksPage; diff --git a/apps/web/app/api/(internal)/pipeline/lib/handleIntegrations.ts b/apps/web/app/api/(internal)/pipeline/lib/handleIntegrations.ts index 27e8834961..4b824b8050 100644 --- a/apps/web/app/api/(internal)/pipeline/lib/handleIntegrations.ts +++ b/apps/web/app/api/(internal)/pipeline/lib/handleIntegrations.ts @@ -1,3 +1,4 @@ +import { TPipelineInput } from "@/app/api/(internal)/pipeline/types/pipelines"; import { writeData as airtableWriteData } from "@formbricks/lib/airtable/service"; import { writeData } from "@formbricks/lib/googleSheet/service"; import { getLocalizedValue } from "@formbricks/lib/i18n/utils"; @@ -14,7 +15,6 @@ import { TIntegrationAirtable } from "@formbricks/types/integration/airtable"; import { TIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet"; import { TIntegrationNotion, TIntegrationNotionConfigData } from "@formbricks/types/integration/notion"; import { TIntegrationSlack } from "@formbricks/types/integration/slack"; -import { TPipelineInput } from "@formbricks/types/pipelines"; import { TResponseMeta } from "@formbricks/types/responses"; import { TSurvey, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types"; diff --git a/apps/web/app/api/(internal)/pipeline/route.ts b/apps/web/app/api/(internal)/pipeline/route.ts index f44eab0fa4..8e93ef127e 100644 --- a/apps/web/app/api/(internal)/pipeline/route.ts +++ b/apps/web/app/api/(internal)/pipeline/route.ts @@ -1,10 +1,13 @@ import { createDocumentAndAssignInsight } from "@/app/api/(internal)/pipeline/lib/documents"; import { sendSurveyFollowUps } from "@/app/api/(internal)/pipeline/lib/survey-follow-up"; +import { ZPipelineInput } from "@/app/api/(internal)/pipeline/types/pipelines"; import { responses } from "@/app/lib/api/response"; import { transformErrorToDetails } from "@/app/lib/api/validator"; +import { webhookCache } from "@/lib/cache/webhook"; import { getIsAIEnabled } from "@/modules/ee/license-check/lib/utils"; import { sendResponseFinishedEmail } from "@/modules/email"; import { getSurveyFollowUpsPermission } from "@/modules/survey-follow-ups/lib/utils"; +import { PipelineTriggers, Webhook } from "@prisma/client"; import { headers } from "next/headers"; import { prisma } from "@formbricks/database"; import { cache } from "@formbricks/lib/cache"; @@ -16,9 +19,6 @@ import { getSurvey, updateSurvey } from "@formbricks/lib/survey/service"; import { convertDatesInObject } from "@formbricks/lib/time"; import { getPromptText } from "@formbricks/lib/utils/ai"; import { parseRecallInfo } from "@formbricks/lib/utils/recall"; -import { webhookCache } from "@formbricks/lib/webhook/cache"; -import { TPipelineTrigger, ZPipelineInput } from "@formbricks/types/pipelines"; -import { TWebhook } from "@formbricks/types/webhooks"; import { getContactAttributes } from "./lib/contact-attribute"; import { handleIntegrations } from "./lib/handleIntegrations"; @@ -53,7 +53,7 @@ export const POST = async (request: Request) => { // Fetch webhooks const getWebhooksForPipeline = cache( - async (environmentId: string, event: TPipelineTrigger, surveyId: string) => { + async (environmentId: string, event: PipelineTriggers, surveyId: string) => { const webhooks = await prisma.webhook.findMany({ where: { environmentId, @@ -68,7 +68,7 @@ export const POST = async (request: Request) => { tags: [webhookCache.tag.byEnvironmentId(environmentId)], } ); - const webhooks: TWebhook[] = await getWebhooksForPipeline(environmentId, event, surveyId); + const webhooks: Webhook[] = await getWebhooksForPipeline(environmentId, event, surveyId); // Prepare webhook and email promises // Fetch with timeout of 5 seconds to prevent hanging diff --git a/apps/web/app/api/(internal)/pipeline/types/pipelines.ts b/apps/web/app/api/(internal)/pipeline/types/pipelines.ts new file mode 100644 index 0000000000..ab01cdbded --- /dev/null +++ b/apps/web/app/api/(internal)/pipeline/types/pipelines.ts @@ -0,0 +1,12 @@ +import { z } from "zod"; +import { ZWebhook } from "@formbricks/database/zod/webhooks"; +import { ZResponse } from "@formbricks/types/responses"; + +export const ZPipelineInput = z.object({ + event: ZWebhook.shape.triggers.element, + response: ZResponse, + environmentId: z.string(), + surveyId: z.string(), +}); + +export type TPipelineInput = z.infer; diff --git a/apps/web/app/api/v1/webhooks/[webhookId]/lib/webhook.ts b/apps/web/app/api/v1/webhooks/[webhookId]/lib/webhook.ts new file mode 100644 index 0000000000..9f299ee840 --- /dev/null +++ b/apps/web/app/api/v1/webhooks/[webhookId]/lib/webhook.ts @@ -0,0 +1,59 @@ +import { webhookCache } from "@/lib/cache/webhook"; +import { Prisma, Webhook } from "@prisma/client"; +import { prisma } from "@formbricks/database"; +import { cache } from "@formbricks/lib/cache"; +import { validateInputs } from "@formbricks/lib/utils/validate"; +import { ZId } from "@formbricks/types/common"; +import { DatabaseError } from "@formbricks/types/errors"; +import { ResourceNotFoundError } from "@formbricks/types/errors"; + +export const deleteWebhook = async (id: string): Promise => { + validateInputs([id, ZId]); + + try { + let deletedWebhook = await prisma.webhook.delete({ + where: { + id, + }, + }); + + webhookCache.revalidate({ + id: deletedWebhook.id, + environmentId: deletedWebhook.environmentId, + source: deletedWebhook.source, + }); + + return deletedWebhook; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2025") { + throw new ResourceNotFoundError("Webhook", id); + } + throw new DatabaseError(`Database error when deleting webhook with ID ${id}`); + } +}; + +export const getWebhook = async (id: string): Promise => + cache( + async () => { + validateInputs([id, ZId]); + + try { + const webhook = await prisma.webhook.findUnique({ + where: { + id, + }, + }); + return webhook; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + + throw error; + } + }, + [`getWebhook-${id}`], + { + tags: [webhookCache.tag.byId(id)], + } + )(); diff --git a/apps/web/app/api/v1/webhooks/[webhookId]/route.ts b/apps/web/app/api/v1/webhooks/[webhookId]/route.ts index 9502ab7483..a5a9ed9f43 100644 --- a/apps/web/app/api/v1/webhooks/[webhookId]/route.ts +++ b/apps/web/app/api/v1/webhooks/[webhookId]/route.ts @@ -1,7 +1,7 @@ import { getEnvironmentIdFromApiKey } from "@/app/api/v1/lib/api-key"; +import { deleteWebhook, getWebhook } from "@/app/api/v1/webhooks/[webhookId]/lib/webhook"; import { responses } from "@/app/lib/api/response"; import { headers } from "next/headers"; -import { deleteWebhook, getWebhook } from "@formbricks/lib/webhook/service"; export const GET = async (_: Request, props: { params: Promise<{ webhookId: string }> }) => { const params = await props.params; diff --git a/apps/web/app/api/v1/webhooks/lib/webhook.ts b/apps/web/app/api/v1/webhooks/lib/webhook.ts new file mode 100644 index 0000000000..66f546aa22 --- /dev/null +++ b/apps/web/app/api/v1/webhooks/lib/webhook.ts @@ -0,0 +1,73 @@ +import { TWebhookInput, ZWebhookInput } from "@/app/api/v1/webhooks/types/webhooks"; +import { webhookCache } from "@/lib/cache/webhook"; +import { Prisma, Webhook } from "@prisma/client"; +import { prisma } from "@formbricks/database"; +import { cache } from "@formbricks/lib/cache"; +import { ITEMS_PER_PAGE } from "@formbricks/lib/constants"; +import { validateInputs } from "@formbricks/lib/utils/validate"; +import { ZId, ZOptionalNumber } from "@formbricks/types/common"; +import { DatabaseError, InvalidInputError } from "@formbricks/types/errors"; + +export const createWebhook = async (environmentId: string, webhookInput: TWebhookInput): Promise => { + validateInputs([environmentId, ZId], [webhookInput, ZWebhookInput]); + + try { + const createdWebhook = await prisma.webhook.create({ + data: { + ...webhookInput, + surveyIds: webhookInput.surveyIds || [], + environment: { + connect: { + id: environmentId, + }, + }, + }, + }); + + webhookCache.revalidate({ + id: createdWebhook.id, + environmentId: createdWebhook.environmentId, + source: createdWebhook.source, + }); + + return createdWebhook; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + + if (!(error instanceof InvalidInputError)) { + throw new DatabaseError(`Database error when creating webhook for environment ${environmentId}`); + } + + throw error; + } +}; + +export const getWebhooks = (environmentId: string, page?: number): Promise => + cache( + async () => { + validateInputs([environmentId, ZId], [page, ZOptionalNumber]); + + try { + const webhooks = await prisma.webhook.findMany({ + where: { + environmentId: environmentId, + }, + take: page ? ITEMS_PER_PAGE : undefined, + skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined, + }); + return webhooks; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + + throw error; + } + }, + [`getWebhooks-${environmentId}-${page}`], + { + tags: [webhookCache.tag.byEnvironmentId(environmentId)], + } + )(); diff --git a/apps/web/app/api/v1/webhooks/route.ts b/apps/web/app/api/v1/webhooks/route.ts index 04a3d29fd4..8a4b58abc9 100644 --- a/apps/web/app/api/v1/webhooks/route.ts +++ b/apps/web/app/api/v1/webhooks/route.ts @@ -1,10 +1,10 @@ import { getEnvironmentIdFromApiKey } from "@/app/api/v1/lib/api-key"; +import { createWebhook, getWebhooks } from "@/app/api/v1/webhooks/lib/webhook"; +import { ZWebhookInput } from "@/app/api/v1/webhooks/types/webhooks"; import { responses } from "@/app/lib/api/response"; import { transformErrorToDetails } from "@/app/lib/api/validator"; import { headers } from "next/headers"; -import { createWebhook, getWebhooks } from "@formbricks/lib/webhook/service"; import { DatabaseError, InvalidInputError } from "@formbricks/types/errors"; -import { ZWebhookInput } from "@formbricks/types/webhooks"; export const GET = async () => { const headersList = await headers(); diff --git a/apps/web/app/api/v1/webhooks/types/webhooks.ts b/apps/web/app/api/v1/webhooks/types/webhooks.ts new file mode 100644 index 0000000000..a0c18a66d0 --- /dev/null +++ b/apps/web/app/api/v1/webhooks/types/webhooks.ts @@ -0,0 +1,16 @@ +import { z } from "zod"; +import { ZWebhook } from "@formbricks/database/zod/webhooks"; + +export const ZWebhookInput = ZWebhook.partial({ + name: true, + source: true, + surveyIds: true, +}).pick({ + name: true, + source: true, + surveyIds: true, + triggers: true, + url: true, +}); + +export type TWebhookInput = z.infer; diff --git a/apps/web/app/lib/pipelines.ts b/apps/web/app/lib/pipelines.ts index ee92ae8ea6..47a1de595b 100644 --- a/apps/web/app/lib/pipelines.ts +++ b/apps/web/app/lib/pipelines.ts @@ -1,5 +1,5 @@ +import { TPipelineInput } from "@/app/lib/types/pipelines"; import { CRON_SECRET, WEBAPP_URL } from "@formbricks/lib/constants"; -import { TPipelineInput } from "@formbricks/types/pipelines"; export const sendToPipeline = async ({ event, surveyId, environmentId, response }: TPipelineInput) => { return fetch(`${WEBAPP_URL}/api/pipeline`, { diff --git a/apps/web/app/lib/types/pipelines.ts b/apps/web/app/lib/types/pipelines.ts new file mode 100644 index 0000000000..1e256ce44e --- /dev/null +++ b/apps/web/app/lib/types/pipelines.ts @@ -0,0 +1,9 @@ +import { PipelineTriggers } from "@prisma/client"; +import { TResponse } from "@formbricks/types/responses"; + +export interface TPipelineInput { + event: PipelineTriggers; + response: TResponse; + environmentId: string; + surveyId: string; +} diff --git a/packages/lib/webhook/cache.ts b/apps/web/lib/cache/webhook.ts similarity index 80% rename from packages/lib/webhook/cache.ts rename to apps/web/lib/cache/webhook.ts index f3c07fe576..a56d21c473 100644 --- a/packages/lib/webhook/cache.ts +++ b/apps/web/lib/cache/webhook.ts @@ -1,10 +1,10 @@ +import { Webhook } from "@prisma/client"; import { revalidateTag } from "next/cache"; -import { TWebhookInput } from "@formbricks/types/webhooks"; interface RevalidateProps { id?: string; environmentId?: string; - source?: TWebhookInput["source"]; + source?: Webhook["source"]; } export const webhookCache = { @@ -15,7 +15,7 @@ export const webhookCache = { byEnvironmentId(environmentId: string) { return `environments-${environmentId}-webhooks`; }, - byEnvironmentIdAndSource(environmentId: string, source: TWebhookInput["source"]) { + byEnvironmentIdAndSource(environmentId: string, source?: Webhook["source"]) { return `environments-${environmentId}-sources-${source}-webhooks`; }, }, diff --git a/apps/web/lib/utils/services.ts b/apps/web/lib/utils/services.ts index a157afa30d..6829f1bd90 100644 --- a/apps/web/lib/utils/services.ts +++ b/apps/web/lib/utils/services.ts @@ -3,6 +3,7 @@ import { apiKeyCache } from "@/lib/cache/api-key"; import { contactCache } from "@/lib/cache/contact"; import { teamCache } from "@/lib/cache/team"; +import { webhookCache } from "@/lib/cache/webhook"; import { Prisma } from "@prisma/client"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; @@ -18,7 +19,6 @@ import { responseNoteCache } from "@formbricks/lib/responseNote/cache"; import { surveyCache } from "@formbricks/lib/survey/cache"; import { tagCache } from "@formbricks/lib/tag/cache"; import { validateInputs } from "@formbricks/lib/utils/validate"; -import { webhookCache } from "@formbricks/lib/webhook/cache"; import { ZId, ZString } from "@formbricks/types/common"; import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors"; diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/actions.ts b/apps/web/modules/integrations/webhooks/actions.ts similarity index 92% rename from apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/actions.ts rename to apps/web/modules/integrations/webhooks/actions.ts index a2fe97bf72..74c12f4a7b 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/actions.ts +++ b/apps/web/modules/integrations/webhooks/actions.ts @@ -8,11 +8,15 @@ import { getProjectIdFromEnvironmentId, getProjectIdFromWebhookId, } from "@/lib/utils/helper"; +import { + createWebhook, + deleteWebhook, + testEndpoint, + updateWebhook, +} from "@/modules/integrations/webhooks/lib/webhook"; +import { ZWebhookInput } from "@/modules/integrations/webhooks/types/webhooks"; import { z } from "zod"; -import { createWebhook, deleteWebhook, updateWebhook } from "@formbricks/lib/webhook/service"; -import { testEndpoint } from "@formbricks/lib/webhook/utils"; import { ZId } from "@formbricks/types/common"; -import { ZWebhookInput } from "@formbricks/types/webhooks"; const ZCreateWebhookAction = z.object({ environmentId: ZId, diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/components/AddWebhookButton.tsx b/apps/web/modules/integrations/webhooks/components/add-webhook-button.tsx similarity index 95% rename from apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/components/AddWebhookButton.tsx rename to apps/web/modules/integrations/webhooks/components/add-webhook-button.tsx index b76f7a4d7b..af966ed854 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/components/AddWebhookButton.tsx +++ b/apps/web/modules/integrations/webhooks/components/add-webhook-button.tsx @@ -6,7 +6,7 @@ import { useTranslations } from "next-intl"; import { useState } from "react"; import { TEnvironment } from "@formbricks/types/environment"; import { TSurvey } from "@formbricks/types/surveys/types"; -import { AddWebhookModal } from "./AddWebhookModal"; +import { AddWebhookModal } from "./add-webhook-modal"; interface AddWebhookButtonProps { environment: TEnvironment; diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/components/AddWebhookModal.tsx b/apps/web/modules/integrations/webhooks/components/add-webhook-modal.tsx similarity index 93% rename from apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/components/AddWebhookModal.tsx rename to apps/web/modules/integrations/webhooks/components/add-webhook-modal.tsx index 928eda928a..7511338a6f 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/components/AddWebhookModal.tsx +++ b/apps/web/modules/integrations/webhooks/components/add-webhook-modal.tsx @@ -1,11 +1,12 @@ -import { SurveyCheckboxGroup } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/components/SurveyCheckboxGroup"; -import { TriggerCheckboxGroup } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/components/TriggerCheckboxGroup"; -import { validWebHookURL } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/lib/utils"; import { getFormattedErrorMessage } from "@/lib/utils/helper"; +import { SurveyCheckboxGroup } from "@/modules/integrations/webhooks/components/survey-checkbox-group"; +import { TriggerCheckboxGroup } from "@/modules/integrations/webhooks/components/trigger-checkbox-group"; +import { validWebHookURL } from "@/modules/integrations/webhooks/lib/utils"; import { Button } from "@/modules/ui/components/button"; import { Input } from "@/modules/ui/components/input"; import { Label } from "@/modules/ui/components/label"; import { Modal } from "@/modules/ui/components/modal"; +import { PipelineTriggers } from "@prisma/client"; import clsx from "clsx"; import { Webhook } from "lucide-react"; import { useTranslations } from "next-intl"; @@ -13,10 +14,9 @@ import { useRouter } from "next/navigation"; import { useState } from "react"; import { useForm } from "react-hook-form"; import toast from "react-hot-toast"; -import { TPipelineTrigger } from "@formbricks/types/pipelines"; import { TSurvey } from "@formbricks/types/surveys/types"; -import { TWebhookInput } from "@formbricks/types/webhooks"; import { createWebhookAction, testEndpointAction } from "../actions"; +import { TWebhookInput } from "../types/webhooks"; interface AddWebhookModalProps { environmentId: string; @@ -37,7 +37,7 @@ export const AddWebhookModal = ({ environmentId, surveys, open, setOpen }: AddWe const [testEndpointInput, setTestEndpointInput] = useState(""); const [hittingEndpoint, setHittingEndpoint] = useState(false); const [endpointAccessible, setEndpointAccessible] = useState(); - const [selectedTriggers, setSelectedTriggers] = useState([]); + const [selectedTriggers, setSelectedTriggers] = useState([]); const [selectedSurveys, setSelectedSurveys] = useState([]); const [selectedAllSurveys, setSelectedAllSurveys] = useState(false); const [creatingWebhook, setCreatingWebhook] = useState(false); @@ -88,7 +88,7 @@ export const AddWebhookModal = ({ environmentId, surveys, open, setOpen }: AddWe ); }; - const handleCheckboxChange = (selectedValue: TPipelineTrigger) => { + const handleCheckboxChange = (selectedValue: PipelineTriggers) => { setSelectedTriggers((prevValues) => prevValues.includes(selectedValue) ? prevValues.filter((value) => value !== selectedValue) diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/components/SurveyCheckboxGroup.tsx b/apps/web/modules/integrations/webhooks/components/survey-checkbox-group.tsx similarity index 100% rename from apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/components/SurveyCheckboxGroup.tsx rename to apps/web/modules/integrations/webhooks/components/survey-checkbox-group.tsx diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/components/TriggerCheckboxGroup.tsx b/apps/web/modules/integrations/webhooks/components/trigger-checkbox-group.tsx similarity index 83% rename from apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/components/TriggerCheckboxGroup.tsx rename to apps/web/modules/integrations/webhooks/components/trigger-checkbox-group.tsx index 21e5f228e2..7256a32250 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/components/TriggerCheckboxGroup.tsx +++ b/apps/web/modules/integrations/webhooks/components/trigger-checkbox-group.tsx @@ -1,26 +1,29 @@ import { Checkbox } from "@/modules/ui/components/checkbox"; +import { PipelineTriggers } from "@prisma/client"; import { useTranslations } from "next-intl"; import React from "react"; -import { TPipelineTrigger } from "@formbricks/types/pipelines"; interface TriggerCheckboxGroupProps { - selectedTriggers: TPipelineTrigger[]; - onCheckboxChange: (selectedValue: TPipelineTrigger) => void; + selectedTriggers: PipelineTriggers[]; + onCheckboxChange: (selectedValue: PipelineTriggers) => void; allowChanges: boolean; } -const triggers = [ +const triggers: { + title: string; + value: PipelineTriggers; +}[] = [ { title: "environments.integrations.webhooks.response_created", - value: "responseCreated" as TPipelineTrigger, + value: "responseCreated", }, { title: "environments.integrations.webhooks.response_updated", - value: "responseUpdated" as TPipelineTrigger, + value: "responseUpdated", }, { title: "environments.integrations.webhooks.response_finished", - value: "responseFinished" as TPipelineTrigger, + value: "responseFinished", }, ]; diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookDetailModal.tsx b/apps/web/modules/integrations/webhooks/components/webhook-detail-modal.tsx similarity index 70% rename from apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookDetailModal.tsx rename to apps/web/modules/integrations/webhooks/components/webhook-detail-modal.tsx index a92274293f..c1149021e0 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookDetailModal.tsx +++ b/apps/web/modules/integrations/webhooks/components/webhook-detail-modal.tsx @@ -1,15 +1,15 @@ -import { WebhookOverviewTab } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookOverviewTab"; -import { WebhookSettingsTab } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookSettingsTab"; +import { WebhookOverviewTab } from "@/modules/integrations/webhooks/components/webhook-overview-tab"; +import { WebhookSettingsTab } from "@/modules/integrations/webhooks/components/webhook-settings-tab"; import { ModalWithTabs } from "@/modules/ui/components/modal-with-tabs"; -import { Webhook } from "lucide-react"; +import { Webhook } from "@prisma/client"; +import { WebhookIcon } from "lucide-react"; import { useTranslations } from "next-intl"; import { TSurvey } from "@formbricks/types/surveys/types"; -import { TWebhook } from "@formbricks/types/webhooks"; interface WebhookModalProps { open: boolean; setOpen: (v: boolean) => void; - webhook: TWebhook; + webhook: Webhook; surveys: TSurvey[]; isReadOnly: boolean; } @@ -35,7 +35,7 @@ export const WebhookModal = ({ open, setOpen, webhook, surveys, isReadOnly }: We open={open} setOpen={setOpen} tabs={tabs} - icon={} + icon={} label={webhook.name ? webhook.name : webhook.url} description={""} /> diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookOverviewTab.tsx b/apps/web/modules/integrations/webhooks/components/webhook-overview-tab.tsx similarity index 95% rename from apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookOverviewTab.tsx rename to apps/web/modules/integrations/webhooks/components/webhook-overview-tab.tsx index 47151a6259..c2d809708e 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookOverviewTab.tsx +++ b/apps/web/modules/integrations/webhooks/components/webhook-overview-tab.tsx @@ -1,16 +1,16 @@ import { Label } from "@/modules/ui/components/label"; +import { Webhook } from "@prisma/client"; import { useTranslations } from "next-intl"; import { convertDateTimeStringShort } from "@formbricks/lib/time"; import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings"; import { TSurvey } from "@formbricks/types/surveys/types"; -import { TWebhook } from "@formbricks/types/webhooks"; interface ActivityTabProps { - webhook: TWebhook; + webhook: Webhook; surveys: TSurvey[]; } -const getSurveyNamesForWebhook = (webhook: TWebhook, allSurveys: TSurvey[]): string[] => { +const getSurveyNamesForWebhook = (webhook: Webhook, allSurveys: TSurvey[]): string[] => { if (webhook.surveyIds.length === 0) { return allSurveys.map((survey) => survey.name); } else { diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookRowData.tsx b/apps/web/modules/integrations/webhooks/components/webhook-row-data.tsx similarity index 93% rename from apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookRowData.tsx rename to apps/web/modules/integrations/webhooks/components/webhook-row-data.tsx index 4beba77ecc..b7704d9be7 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookRowData.tsx +++ b/apps/web/modules/integrations/webhooks/components/webhook-row-data.tsx @@ -1,12 +1,12 @@ import { Badge } from "@/modules/ui/components/badge"; +import { Webhook } from "@prisma/client"; import { useTranslations } from "next-intl"; import { timeSince } from "@formbricks/lib/time"; import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings"; import { TSurvey } from "@formbricks/types/surveys/types"; import { TUserLocale } from "@formbricks/types/user"; -import { TWebhook } from "@formbricks/types/webhooks"; -const renderSelectedSurveysText = (webhook: TWebhook, allSurveys: TSurvey[]) => { +const renderSelectedSurveysText = (webhook: Webhook, allSurveys: TSurvey[]) => { if (webhook.surveyIds.length === 0) { const allSurveyNames = allSurveys.map((survey) => survey.name); return

{allSurveyNames.join(", ")}

; @@ -19,7 +19,7 @@ const renderSelectedSurveysText = (webhook: TWebhook, allSurveys: TSurvey[]) => } }; -const renderSelectedTriggersText = (webhook: TWebhook, t: (key: string) => string) => { +const renderSelectedTriggersText = (webhook: Webhook, t: (key: string) => string) => { if (webhook.triggers.length === 0) { return

No Triggers

; } else { @@ -58,7 +58,7 @@ export const WebhookRowData = ({ surveys, locale, }: { - webhook: TWebhook; + webhook: Webhook; surveys: TSurvey[]; locale: TUserLocale; }) => { diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookSettingsTab.tsx b/apps/web/modules/integrations/webhooks/components/webhook-settings-tab.tsx similarity index 93% rename from apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookSettingsTab.tsx rename to apps/web/modules/integrations/webhooks/components/webhook-settings-tab.tsx index 71e777e223..cdeb7b6115 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookSettingsTab.tsx +++ b/apps/web/modules/integrations/webhooks/components/webhook-settings-tab.tsx @@ -1,13 +1,14 @@ "use client"; -import { SurveyCheckboxGroup } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/components/SurveyCheckboxGroup"; -import { TriggerCheckboxGroup } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/components/TriggerCheckboxGroup"; -import { validWebHookURL } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/lib/utils"; import { getFormattedErrorMessage } from "@/lib/utils/helper"; +import { SurveyCheckboxGroup } from "@/modules/integrations/webhooks/components/survey-checkbox-group"; +import { TriggerCheckboxGroup } from "@/modules/integrations/webhooks/components/trigger-checkbox-group"; +import { validWebHookURL } from "@/modules/integrations/webhooks/lib/utils"; import { Button } from "@/modules/ui/components/button"; import { DeleteDialog } from "@/modules/ui/components/delete-dialog"; import { Input } from "@/modules/ui/components/input"; import { Label } from "@/modules/ui/components/label"; +import { PipelineTriggers, Webhook } from "@prisma/client"; import clsx from "clsx"; import { TrashIcon } from "lucide-react"; import { useTranslations } from "next-intl"; @@ -16,13 +17,12 @@ import { useRouter } from "next/navigation"; import { useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "react-hot-toast"; -import { TPipelineTrigger } from "@formbricks/types/pipelines"; import { TSurvey } from "@formbricks/types/surveys/types"; -import { TWebhook, TWebhookInput } from "@formbricks/types/webhooks"; import { deleteWebhookAction, testEndpointAction, updateWebhookAction } from "../actions"; +import { TWebhookInput } from "../types/webhooks"; interface ActionSettingsTabProps { - webhook: TWebhook; + webhook: Webhook; surveys: TSurvey[]; setOpen: (v: boolean) => void; isReadOnly: boolean; @@ -42,7 +42,7 @@ export const WebhookSettingsTab = ({ webhook, surveys, setOpen, isReadOnly }: Ac const [openDeleteDialog, setOpenDeleteDialog] = useState(false); const [isUpdatingWebhook, setIsUpdatingWebhook] = useState(false); - const [selectedTriggers, setSelectedTriggers] = useState(webhook.triggers); + const [selectedTriggers, setSelectedTriggers] = useState(webhook.triggers); const [selectedSurveys, setSelectedSurveys] = useState(webhook.surveyIds); const [testEndpointInput, setTestEndpointInput] = useState(webhook.url); const [endpointAccessible, setEndpointAccessible] = useState(); diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookTableHeading.tsx b/apps/web/modules/integrations/webhooks/components/webhook-table-heading.tsx similarity index 100% rename from apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookTableHeading.tsx rename to apps/web/modules/integrations/webhooks/components/webhook-table-heading.tsx diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookTable.tsx b/apps/web/modules/integrations/webhooks/components/webhook-table.tsx similarity index 85% rename from apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookTable.tsx rename to apps/web/modules/integrations/webhooks/components/webhook-table.tsx index 2306692220..68c2ef08c1 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookTable.tsx +++ b/apps/web/modules/integrations/webhooks/components/webhook-table.tsx @@ -1,16 +1,16 @@ "use client"; -import { WebhookModal } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookDetailModal"; +import { WebhookModal } from "@/modules/integrations/webhooks/components/webhook-detail-modal"; import { EmptySpaceFiller } from "@/modules/ui/components/empty-space-filler"; +import { Webhook } from "@prisma/client"; import { useTranslations } from "next-intl"; import { type JSX, useState } from "react"; import { TEnvironment } from "@formbricks/types/environment"; import { TSurvey } from "@formbricks/types/surveys/types"; -import { TWebhook } from "@formbricks/types/webhooks"; interface WebhookTableProps { environment: TEnvironment; - webhooks: TWebhook[]; + webhooks: Webhook[]; surveys: TSurvey[]; children: [JSX.Element, JSX.Element[]]; isReadOnly: boolean; @@ -25,7 +25,7 @@ export const WebhookTable = ({ }: WebhookTableProps) => { const [isWebhookDetailModalOpen, setWebhookDetailModalOpen] = useState(false); const t = useTranslations(); - const [activeWebhook, setActiveWebhook] = useState({ + const [activeWebhook, setActiveWebhook] = useState({ environmentId: environment.id, id: "", name: "", @@ -37,7 +37,7 @@ export const WebhookTable = ({ updatedAt: new Date(), }); - const handleOpenWebhookDetailModalClick = (e, webhook: TWebhook) => { + const handleOpenWebhookDetailModalClick = (e, webhook: Webhook) => { e.preventDefault(); setActiveWebhook(webhook); setWebhookDetailModalOpen(true); diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/lib/utils.ts b/apps/web/modules/integrations/webhooks/lib/utils.ts similarity index 100% rename from apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/lib/utils.ts rename to apps/web/modules/integrations/webhooks/lib/utils.ts diff --git a/packages/lib/webhook/service.ts b/apps/web/modules/integrations/webhooks/lib/webhook.ts similarity index 50% rename from packages/lib/webhook/service.ts rename to apps/web/modules/integrations/webhooks/lib/webhook.ts index c80b47ae72..054aa871dc 100644 --- a/packages/lib/webhook/service.ts +++ b/apps/web/modules/integrations/webhooks/lib/webhook.ts @@ -1,143 +1,21 @@ -import "server-only"; -import { Prisma } from "@prisma/client"; +import { webhookCache } from "@/lib/cache/webhook"; +import { Prisma, Webhook } from "@prisma/client"; import { prisma } from "@formbricks/database"; -import { ZOptionalNumber } from "@formbricks/types/common"; +import { cache } from "@formbricks/lib/cache"; +import { validateInputs } from "@formbricks/lib/utils/validate"; import { ZId } from "@formbricks/types/common"; -import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors"; -import { TWebhook, TWebhookInput, ZWebhookInput } from "@formbricks/types/webhooks"; -import { cache } from "../cache"; -import { ITEMS_PER_PAGE } from "../constants"; -import { validateInputs } from "../utils/validate"; -import { webhookCache } from "./cache"; - -export const getWebhooks = (environmentId: string, page?: number): Promise => - cache( - async () => { - validateInputs([environmentId, ZId], [page, ZOptionalNumber]); - - try { - const webhooks = await prisma.webhook.findMany({ - where: { - environmentId: environmentId, - }, - take: page ? ITEMS_PER_PAGE : undefined, - skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined, - }); - return webhooks; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); - } - - throw error; - } - }, - [`getWebhooks-${environmentId}-${page}`], - { - tags: [webhookCache.tag.byEnvironmentId(environmentId)], - } - )(); - -export const getWebhookCountBySource = ( - environmentId: string, - source: TWebhookInput["source"] -): Promise => - cache( - async () => { - validateInputs([environmentId, ZId], [source, ZId]); - - try { - const count = await prisma.webhook.count({ - where: { - environmentId, - source, - }, - }); - return count; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); - } - - throw error; - } - }, - [`getWebhookCountBySource-${environmentId}-${source}`], - { - tags: [webhookCache.tag.byEnvironmentIdAndSource(environmentId, source)], - } - )(); - -export const getWebhook = async (id: string): Promise => - cache( - async () => { - validateInputs([id, ZId]); - - try { - const webhook = await prisma.webhook.findUnique({ - where: { - id, - }, - }); - return webhook; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); - } - - throw error; - } - }, - [`getWebhook-${id}`], - { - tags: [webhookCache.tag.byId(id)], - } - )(); - -export const createWebhook = async ( - environmentId: string, - webhookInput: TWebhookInput -): Promise => { - validateInputs([environmentId, ZId], [webhookInput, ZWebhookInput]); - - try { - const createdWebhook = await prisma.webhook.create({ - data: { - ...webhookInput, - surveyIds: webhookInput.surveyIds || [], - environment: { - connect: { - id: environmentId, - }, - }, - }, - }); - - webhookCache.revalidate({ - id: createdWebhook.id, - environmentId: createdWebhook.environmentId, - source: createdWebhook.source, - }); - - return createdWebhook; - } catch (error) { - if (!(error instanceof InvalidInputError)) { - throw new DatabaseError(`Database error when creating webhook for environment ${environmentId}`); - } - - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); - } - - throw error; - } -}; +import { + DatabaseError, + InvalidInputError, + ResourceNotFoundError, + UnknownError, +} from "@formbricks/types/errors"; +import { TWebhookInput } from "../types/webhooks"; export const updateWebhook = async ( webhookId: string, webhookInput: Partial -): Promise => { - validateInputs([webhookId, ZId], [webhookInput, ZWebhookInput]); +): Promise => { try { const updatedWebhook = await prisma.webhook.update({ where: { @@ -157,7 +35,7 @@ export const updateWebhook = async ( source: updatedWebhook.source, }); - return updatedWebhook; + return true; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { throw new DatabaseError(error.message); @@ -167,9 +45,7 @@ export const updateWebhook = async ( } }; -export const deleteWebhook = async (id: string): Promise => { - validateInputs([id, ZId]); - +export const deleteWebhook = async (id: string): Promise => { try { let deletedWebhook = await prisma.webhook.delete({ where: { @@ -183,7 +59,7 @@ export const deleteWebhook = async (id: string): Promise => { source: deletedWebhook.source, }); - return deletedWebhook; + return true; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2025") { throw new ResourceNotFoundError("Webhook", id); @@ -191,3 +67,100 @@ export const deleteWebhook = async (id: string): Promise => { throw new DatabaseError(`Database error when deleting webhook with ID ${id}`); } }; + +export const createWebhook = async (environmentId: string, webhookInput: TWebhookInput): Promise => { + try { + const createdWebhook = await prisma.webhook.create({ + data: { + ...webhookInput, + surveyIds: webhookInput.surveyIds || [], + environment: { + connect: { + id: environmentId, + }, + }, + }, + }); + + webhookCache.revalidate({ + id: createdWebhook.id, + environmentId: createdWebhook.environmentId, + source: createdWebhook.source, + }); + + return true; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + + if (!(error instanceof InvalidInputError)) { + throw new DatabaseError(`Database error when creating webhook for environment ${environmentId}`); + } + + throw error; + } +}; + +export const getWebhooks = (environmentId: string): Promise => + cache( + async () => { + validateInputs([environmentId, ZId]); + + try { + const webhooks = await prisma.webhook.findMany({ + where: { + environmentId: environmentId, + }, + orderBy: { + createdAt: "desc", + }, + }); + return webhooks; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + + throw error; + } + }, + [`getWebhooks-${environmentId}`], + { + tags: [webhookCache.tag.byEnvironmentId(environmentId)], + } + )(); + +export const testEndpoint = async (url: string): Promise => { + try { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 5000); + + const response = await fetch(url, { + method: "POST", + body: JSON.stringify({ + event: "testEndpoint", + }), + headers: { + "Content-Type": "application/json", + }, + signal: controller.signal, + }); + clearTimeout(timeout); + const statusCode = response.status; + + if (statusCode >= 200 && statusCode < 300) { + return true; + } else { + const errorMessage = await response.text().then( + (text) => text.substring(0, 1000) // Limit error message size + ); + throw new UnknownError(`Request failed with status code ${statusCode}: ${errorMessage}`); + } + } catch (error) { + if (error.name === "AbortError") { + throw new UnknownError("Request timed out after 5 seconds"); + } + throw new UnknownError(`Error while fetching the URL: ${error.message}`); + } +}; diff --git a/apps/web/modules/integrations/webhooks/page.tsx b/apps/web/modules/integrations/webhooks/page.tsx new file mode 100644 index 0000000000..4fb8451dbb --- /dev/null +++ b/apps/web/modules/integrations/webhooks/page.tsx @@ -0,0 +1,68 @@ +import { authOptions } from "@/modules/auth/lib/authOptions"; +import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles"; +import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams"; +import { AddWebhookButton } from "@/modules/integrations/webhooks/components/add-webhook-button"; +import { WebhookRowData } from "@/modules/integrations/webhooks/components/webhook-row-data"; +import { WebhookTable } from "@/modules/integrations/webhooks/components/webhook-table"; +import { WebhookTableHeading } from "@/modules/integrations/webhooks/components/webhook-table-heading"; +import { getWebhooks } from "@/modules/integrations/webhooks/lib/webhook"; +import { GoBackButton } from "@/modules/ui/components/go-back-button"; +import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper"; +import { PageHeader } from "@/modules/ui/components/page-header"; +import { getServerSession } from "next-auth"; +import { getTranslations } from "next-intl/server"; +import { getEnvironment } from "@formbricks/lib/environment/service"; +import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; +import { getAccessFlags } from "@formbricks/lib/membership/utils"; +import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service"; +import { getSurveys } from "@formbricks/lib/survey/service"; +import { findMatchingLocale } from "@formbricks/lib/utils/locale"; + +export const WebhooksPage = async (props) => { + const params = await props.params; + const t = await getTranslations(); + const [session, organization, webhooks, surveys, environment] = await Promise.all([ + getServerSession(authOptions), + getOrganizationByEnvironmentId(params.environmentId), + getWebhooks(params.environmentId), + getSurveys(params.environmentId, 200), // HOTFIX: not getting all surveys for now since it's maxing out the prisma accelerate limit + getEnvironment(params.environmentId), + ]); + + if (!session) { + throw new Error(t("common.session_not_found")); + } + + if (!environment) { + throw new Error(t("common.environment_not_found")); + } + + if (!organization) { + throw new Error(t("common.organization_not_found")); + } + + const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id); + const { isMember } = getAccessFlags(currentUserMembership?.role); + + const projectPermission = await getProjectPermissionByUserId(session?.user.id, environment?.projectId); + + const { hasReadAccess } = getTeamPermissionFlags(projectPermission); + + const isReadOnly = isMember && hasReadAccess; + + const renderAddWebhookButton = () => ; + const locale = await findMatchingLocale(); + + return ( + + + } /> + + + {webhooks.map((webhook) => ( + + ))} + + + ); +}; diff --git a/apps/web/modules/integrations/webhooks/types/webhooks.ts b/apps/web/modules/integrations/webhooks/types/webhooks.ts new file mode 100644 index 0000000000..a0c18a66d0 --- /dev/null +++ b/apps/web/modules/integrations/webhooks/types/webhooks.ts @@ -0,0 +1,16 @@ +import { z } from "zod"; +import { ZWebhook } from "@formbricks/database/zod/webhooks"; + +export const ZWebhookInput = ZWebhook.partial({ + name: true, + source: true, + surveyIds: true, +}).pick({ + name: true, + source: true, + surveyIds: true, + triggers: true, + url: true, +}); + +export type TWebhookInput = z.infer; diff --git a/packages/database/zod/webhooks.ts b/packages/database/zod/webhooks.ts new file mode 100644 index 0000000000..959a3fd409 --- /dev/null +++ b/packages/database/zod/webhooks.ts @@ -0,0 +1,14 @@ +import type { Webhook } from "@prisma/client"; +import { z } from "zod"; + +export const ZWebhook = z.object({ + id: z.string().cuid2(), + name: z.string().nullable(), + createdAt: z.date(), + updatedAt: z.date(), + url: z.string().url(), + source: z.enum(["user", "zapier", "make", "n8n"]), + environmentId: z.string().cuid2(), + triggers: z.array(z.enum(["responseFinished", "responseCreated", "responseUpdated"])), + surveyIds: z.array(z.string().cuid2()), +}) satisfies z.ZodType; diff --git a/packages/lib/webhook/auth.ts b/packages/lib/webhook/auth.ts deleted file mode 100644 index f88839aa61..0000000000 --- a/packages/lib/webhook/auth.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { ZId } from "@formbricks/types/common"; -import { cache } from "../cache"; -import { hasUserEnvironmentAccess } from "../environment/auth"; -import { validateInputs } from "../utils/validate"; -import { webhookCache } from "./cache"; -import { getWebhook } from "./service"; - -export const canUserAccessWebhook = async (userId: string, webhookId: string): Promise => - cache( - async () => { - validateInputs([userId, ZId], [webhookId, ZId]); - - try { - const webhook = await getWebhook(webhookId); - if (!webhook) return false; - - const hasAccessToEnvironment = await hasUserEnvironmentAccess(userId, webhook.environmentId); - if (!hasAccessToEnvironment) return false; - - return true; - } catch (error) { - throw error; - } - }, - [`canUserAccessWebhook-${userId}-${webhookId}`], - { - tags: [webhookCache.tag.byId(webhookId)], - } - )(); diff --git a/packages/lib/webhook/utils.ts b/packages/lib/webhook/utils.ts deleted file mode 100644 index 8bfb955a85..0000000000 --- a/packages/lib/webhook/utils.ts +++ /dev/null @@ -1,26 +0,0 @@ -import "server-only"; -import { UnknownError } from "@formbricks/types/errors"; - -export const testEndpoint = async (url: string): Promise => { - try { - const response = await fetch(url, { - method: "POST", - body: JSON.stringify({ - event: "testEndpoint", - }), - headers: { - "Content-Type": "application/json", - }, - }); - const statusCode = response.status; - - if (statusCode >= 200 && statusCode < 300) { - return true; - } else { - const errorMessage = await response.text(); - throw new UnknownError(`Request failed with status code ${statusCode}: ${errorMessage}`); - } - } catch (error) { - throw new UnknownError(`Error while fetching the URL: ${error.message}`); - } -}; diff --git a/packages/types/pipelines.ts b/packages/types/pipelines.ts deleted file mode 100644 index c3450f07f1..0000000000 --- a/packages/types/pipelines.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { z } from "zod"; -import { ZResponse } from "./responses"; - -export const ZPipelineTrigger = z.enum(["responseFinished", "responseCreated", "responseUpdated"]); - -export type TPipelineTrigger = z.infer; - -export const ZPipelineInput = z.object({ - event: ZPipelineTrigger, - response: ZResponse, - environmentId: z.string(), - surveyId: z.string(), -}); - -export type TPipelineInput = z.infer; diff --git a/packages/types/webhooks.ts b/packages/types/webhooks.ts deleted file mode 100644 index ed97c90d90..0000000000 --- a/packages/types/webhooks.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { z } from "zod"; -import { ZPipelineTrigger } from "./pipelines"; - -export const ZWebhookSource = z.enum(["user", "zapier", "make", "n8n"]); - -export const ZWebhook = z.object({ - id: z.string().cuid2(), - name: z.string().nullish(), - createdAt: z.date(), - updatedAt: z.date(), - url: z.string().url(), - source: ZWebhookSource, - environmentId: z.string().cuid2(), - triggers: z.array(ZPipelineTrigger), - surveyIds: z.array(z.string().cuid2()), -}); - -export type TWebhook = z.infer; - -export const ZWebhookInput = z.object({ - url: z.string().url(), - name: z.string().nullish(), - triggers: z.array(ZPipelineTrigger), - source: ZWebhookSource.optional(), - surveyIds: z.array(z.string().cuid2()).optional(), -}); - -export type TWebhookInput = z.infer;