diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/developer/webhooks/(with-loader)/actions.ts b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/developer/webhooks/(with-loader)/actions.ts index fd9836607c..adab4c4a7e 100644 --- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/developer/webhooks/(with-loader)/actions.ts +++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/developer/webhooks/(with-loader)/actions.ts @@ -1,7 +1,7 @@ "use server"; -import { revalidatePath } from "next/cache"; +import { revalidateTag } from "next/cache"; -export async function revalidateWebhooksList() { - revalidatePath("/settings/developer/webhooks"); +export async function revalidateWebhooksListGetByViewer() { + revalidateTag("viewer.webhook.getByViewer"); } diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/developer/webhooks/(with-loader)/page.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/developer/webhooks/(with-loader)/page.tsx index 93174bcd5f..aedc702dfb 100644 --- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/developer/webhooks/(with-loader)/page.tsx +++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/developer/webhooks/(with-loader)/page.tsx @@ -1,13 +1,13 @@ -import { createRouterCaller } from "app/_trpc/context"; import { _generateMetadata } from "app/_utils"; +import { unstable_cache } from "next/cache"; import { cookies, headers } from "next/headers"; import { redirect } from "next/navigation"; import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import WebhooksView from "@calcom/features/webhooks/pages/webhooks-view"; import { APP_NAME } from "@calcom/lib/constants"; +import { WebhookRepository } from "@calcom/lib/server/repository/webhook"; import { UserPermissionRole } from "@calcom/prisma/enums"; -import { webhookRouter } from "@calcom/trpc/server/routers/viewer/webhook/_router"; import { buildLegacyRequest } from "@lib/buildLegacyCtx"; @@ -20,6 +20,17 @@ export const generateMetadata = async () => "/settings/developer/webhooks" ); +const getCachedWebhooksList = unstable_cache( + async ({ userId, userRole }: { userId: number; userRole?: UserPermissionRole }) => { + return await WebhookRepository.getAllWebhooksByUserId({ + userId, + userRole, + }); + }, + undefined, + { revalidate: 3600, tags: ["viewer.webhook.getByViewer"] } +); + const WebhooksViewServerWrapper = async () => { const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) }); if (!session?.user?.id) { @@ -27,8 +38,9 @@ const WebhooksViewServerWrapper = async () => { } const isAdmin = session.user.role === UserPermissionRole.ADMIN; - const caller = await createRouterCaller(webhookRouter); - const data = await caller.getByViewer(); + const userRole = session.user.role !== "INACTIVE_ADMIN" ? session.user.role : undefined; + + const data = await getCachedWebhooksList({ userId: session.user.id, userRole }); return ; }; diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/developer/webhooks/[id]/actions.ts b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/developer/webhooks/[id]/actions.ts new file mode 100644 index 0000000000..0c0de6176c --- /dev/null +++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/developer/webhooks/[id]/actions.ts @@ -0,0 +1,7 @@ +"use server"; + +import { revalidateTag } from "next/cache"; + +export async function revalidateWebhookById(id: string) { + revalidateTag(`viewer.webhook.get:${id}`); +} diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/developer/webhooks/[id]/page.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/developer/webhooks/[id]/page.tsx index 173de93ab2..37fce01dce 100644 --- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/developer/webhooks/[id]/page.tsx +++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/developer/webhooks/[id]/page.tsx @@ -1,5 +1,6 @@ import type { PageProps } from "app/_types"; import { getTranslate, _generateMetadata } from "app/_utils"; +import { unstable_cache } from "next/cache"; import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; import { EditWebhookView } from "@calcom/features/webhooks/pages/webhook-edit-view"; @@ -15,12 +16,24 @@ export const generateMetadata = async ({ params }: { params: Promise<{ id: strin `/settings/developer/webhooks/${(await params).id}` ); +const getCachedWebhook = (id?: string) => { + const fn = unstable_cache( + async () => { + return await WebhookRepository.findByWebhookId(id); + }, + undefined, + { revalidate: 3600, tags: [`viewer.webhook.get:${id}`] } + ); + + return fn(); +}; + const Page = async ({ params: _params }: PageProps) => { const t = await getTranslate(); const params = await _params; const id = typeof params?.id === "string" ? params.id : undefined; - const webhook = await WebhookRepository.findByWebhookId(id); + const webhook = await getCachedWebhook(id); return ( await _generateMetadata( @@ -15,15 +21,25 @@ export const generateMetadata = async () => "/settings/developer/webhooks/new" ); +const getCachedWebhooksList = unstable_cache( + async ({ userId }: { userId: number }) => { + return await WebhookRepository.findWebhooksByFilters({ userId }); + }, + undefined, + { revalidate: 3600, tags: ["viewer.webhook.list"] } +); + const Page = async () => { - const [appsCaller, webhookCaller] = await Promise.all([ - createRouterCaller(appsRouter), - createRouterCaller(webhookRouter), - ]); + const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) }); + if (!session?.user?.id) { + redirect("/auth/login"); + } + + const appsCaller = await createRouterCaller(appsRouter); const [installedApps, webhooks] = await Promise.all([ appsCaller.integrations({ variant: "other", onlyInstalled: true }), - webhookCaller.list(), + getCachedWebhooksList({ userId: session.user.id }), ]); return ; diff --git a/packages/features/webhooks/components/WebhookListItem.tsx b/packages/features/webhooks/components/WebhookListItem.tsx index 1d354da071..aa9177d853 100644 --- a/packages/features/webhooks/components/WebhookListItem.tsx +++ b/packages/features/webhooks/components/WebhookListItem.tsx @@ -18,7 +18,8 @@ import { Switch } from "@calcom/ui/components/form"; import { showToast } from "@calcom/ui/components/toast"; import { Tooltip } from "@calcom/ui/components/tooltip"; import { revalidateEventTypeEditPage } from "@calcom/web/app/(use-page-wrapper)/event-types/[type]/actions"; -import { revalidateWebhooksList } from "@calcom/web/app/(use-page-wrapper)/settings/(settings-layout)/developer/webhooks/(with-loader)/actions"; +import { revalidateWebhooksListGetByViewer } from "@calcom/web/app/(use-page-wrapper)/settings/(settings-layout)/developer/webhooks/(with-loader)/actions"; +import { revalidateWebhookList } from "@calcom/web/app/(use-page-wrapper)/settings/(settings-layout)/developer/webhooks/new/actions"; type WebhookProps = { id: string; @@ -46,21 +47,19 @@ export default function WebhookListItem(props: { const deleteWebhook = trpc.viewer.webhook.delete.useMutation({ async onSuccess() { if (webhook.eventTypeId) revalidateEventTypeEditPage(webhook.eventTypeId); - revalidateWebhooksList(); + revalidateWebhooksListGetByViewer(); + revalidateWebhookList(); showToast(t("webhook_removed_successfully"), "success"); - await utils.viewer.webhook.getByViewer.invalidate(); - await utils.viewer.webhook.list.invalidate(); await utils.viewer.eventTypes.get.invalidate(); }, }); const toggleWebhook = trpc.viewer.webhook.edit.useMutation({ async onSuccess(data) { if (webhook.eventTypeId) revalidateEventTypeEditPage(webhook.eventTypeId); - revalidateWebhooksList(); + revalidateWebhooksListGetByViewer(); + revalidateWebhookList(); // TODO: Better success message showToast(t(data?.active ? "enabled" : "disabled"), "success"); - await utils.viewer.webhook.getByViewer.invalidate(); - await utils.viewer.webhook.list.invalidate(); await utils.viewer.eventTypes.get.invalidate(); }, }); diff --git a/packages/features/webhooks/pages/webhook-edit-view.tsx b/packages/features/webhooks/pages/webhook-edit-view.tsx index 48d87332cb..c09eb4cd57 100644 --- a/packages/features/webhooks/pages/webhook-edit-view.tsx +++ b/packages/features/webhooks/pages/webhook-edit-view.tsx @@ -7,7 +7,9 @@ import type { WebhookTriggerEvents } from "@calcom/prisma/enums"; import { trpc } from "@calcom/trpc/react"; import { SkeletonContainer } from "@calcom/ui/components/skeleton"; import { showToast } from "@calcom/ui/components/toast"; -import { revalidateWebhooksList } from "@calcom/web/app/(use-page-wrapper)/settings/(settings-layout)/developer/webhooks/(with-loader)/actions"; +import { revalidateWebhooksListGetByViewer } from "@calcom/web/app/(use-page-wrapper)/settings/(settings-layout)/developer/webhooks/(with-loader)/actions"; +import { revalidateWebhookById } from "@calcom/web/app/(use-page-wrapper)/settings/(settings-layout)/developer/webhooks/[id]/actions"; +import { revalidateWebhookList } from "@calcom/web/app/(use-page-wrapper)/settings/(settings-layout)/developer/webhooks/new/actions"; import type { WebhookFormSubmitData } from "../components/WebhookForm"; import WebhookForm from "../components/WebhookForm"; @@ -43,10 +45,10 @@ export function EditWebhookView({ webhook }: { webhook?: WebhookProps }) { }); const editWebhookMutation = trpc.viewer.webhook.edit.useMutation({ async onSuccess() { - await utils.viewer.webhook.list.invalidate(); - await utils.viewer.webhook.get.invalidate({ webhookId: webhook?.id }); + revalidateWebhookById(webhook?.id ?? ""); + revalidateWebhookList(); showToast(t("webhook_updated_successfully"), "success"); - revalidateWebhooksList(); + revalidateWebhooksListGetByViewer(); router.push("/settings/developer/webhooks"); }, onError(error) { diff --git a/packages/features/webhooks/pages/webhook-new-view.tsx b/packages/features/webhooks/pages/webhook-new-view.tsx index c2f5551197..06cc853641 100644 --- a/packages/features/webhooks/pages/webhook-new-view.tsx +++ b/packages/features/webhooks/pages/webhook-new-view.tsx @@ -10,7 +10,8 @@ import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc/react"; import type { RouterOutputs } from "@calcom/trpc/react"; import { showToast } from "@calcom/ui/components/toast"; -import { revalidateWebhooksList } from "@calcom/web/app/(use-page-wrapper)/settings/(settings-layout)/developer/webhooks/(with-loader)/actions"; +import { revalidateWebhooksListGetByViewer } from "@calcom/web/app/(use-page-wrapper)/settings/(settings-layout)/developer/webhooks/(with-loader)/actions"; +import { revalidateWebhookList } from "@calcom/web/app/(use-page-wrapper)/settings/(settings-layout)/developer/webhooks/new/actions"; import type { WebhookFormSubmitData } from "../components/WebhookForm"; import WebhookForm from "../components/WebhookForm"; @@ -35,7 +36,8 @@ export const NewWebhookView = ({ webhooks, installedApps }: Props) => { async onSuccess() { showToast(t("webhook_created_successfully"), "success"); await utils.viewer.webhook.list.invalidate(); - revalidateWebhooksList(); + revalidateWebhookList(); + revalidateWebhooksListGetByViewer(); router.push("/settings/developer/webhooks"); }, onError(error) { diff --git a/packages/lib/server/repository/webhook.ts b/packages/lib/server/repository/webhook.ts index a17cf3f86a..48eaca8293 100644 --- a/packages/lib/server/repository/webhook.ts +++ b/packages/lib/server/repository/webhook.ts @@ -1,3 +1,7 @@ +import type { Prisma } from "@prisma/client"; +import { z } from "zod"; + +import { WEBHOOK_TRIGGER_EVENTS } from "@calcom/features/webhooks/lib/constants"; import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage"; import { compareMembership } from "@calcom/lib/event-types/getEventTypesByViewer"; import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl"; @@ -19,6 +23,24 @@ type WebhookGroup = { webhooks: Webhook[]; }; +const webhookIdAndEventTypeIdSchema = z.object({ + // Webhook ID + id: z.string().optional(), + eventTypeId: z.number().optional(), + teamId: z.number().optional(), +}); + +const ZFindWebhooksByFiltersInputSchema = webhookIdAndEventTypeIdSchema + .extend({ + appId: z.string().optional(), + teamId: z.number().optional(), + eventTypeId: z.number().optional(), + eventTriggers: z.enum(WEBHOOK_TRIGGER_EVENTS).array().optional(), + }) + .optional(); + +export type TFindWebhooksByFiltersInputSchema = z.infer; + const filterWebhooks = (webhook: Webhook) => { const appIds = [ "zapier", @@ -180,4 +202,65 @@ export class WebhookRepository { }, }); } + + static async findWebhooksByFilters({ + userId, + input, + }: { + userId: number; + input?: TFindWebhooksByFiltersInputSchema; + }) { + const where: Prisma.WebhookWhereInput = { + /* Don't mixup zapier webhooks with normal ones */ + AND: [{ appId: !input?.appId ? null : input.appId }], + }; + + const user = await prisma.user.findUnique({ + where: { + id: userId, + }, + select: { + teams: true, + }, + }); + + if (Array.isArray(where.AND)) { + if (input?.eventTypeId) { + const managedParentEvt = await prisma.eventType.findFirst({ + where: { + id: input.eventTypeId, + parentId: { + not: null, + }, + }, + select: { + parentId: true, + }, + }); + + if (managedParentEvt?.parentId) { + where.AND?.push({ + OR: [ + { eventTypeId: input.eventTypeId }, + { eventTypeId: managedParentEvt.parentId, active: true }, + ], + }); + } else { + where.AND?.push({ eventTypeId: input.eventTypeId }); + } + } else { + where.AND?.push({ + OR: [{ userId }, { teamId: { in: user?.teams.map((membership) => membership.teamId) } }], + }); + } + + if (input?.eventTriggers) { + where.AND?.push({ eventTriggers: { hasEvery: input.eventTriggers } }); + } + } + + return await prisma.webhook.findMany({ + where, + }); + } } diff --git a/packages/trpc/server/routers/viewer/webhook/list.handler.ts b/packages/trpc/server/routers/viewer/webhook/list.handler.ts index 120223ec81..11205add04 100644 --- a/packages/trpc/server/routers/viewer/webhook/list.handler.ts +++ b/packages/trpc/server/routers/viewer/webhook/list.handler.ts @@ -1,6 +1,4 @@ -import type { Prisma } from "@prisma/client"; - -import { prisma } from "@calcom/prisma"; +import { WebhookRepository } from "@calcom/lib/server/repository/webhook"; import type { TrpcSessionUser } from "@calcom/trpc/server/types"; import type { TListInputSchema } from "./list.schema"; @@ -13,53 +11,5 @@ type ListOptions = { }; export const listHandler = async ({ ctx, input }: ListOptions) => { - const where: Prisma.WebhookWhereInput = { - /* Don't mixup zapier webhooks with normal ones */ - AND: [{ appId: !input?.appId ? null : input.appId }], - }; - - const user = await prisma.user.findUnique({ - where: { - id: ctx.user.id, - }, - select: { - teams: true, - }, - }); - - if (Array.isArray(where.AND)) { - if (input?.eventTypeId) { - const managedParentEvt = await prisma.eventType.findFirst({ - where: { - id: input.eventTypeId, - parentId: { - not: null, - }, - }, - select: { - parentId: true, - }, - }); - - if (managedParentEvt?.parentId) { - where.AND?.push({ - OR: [{ eventTypeId: input.eventTypeId }, { eventTypeId: managedParentEvt.parentId, active: true }], - }); - } else { - where.AND?.push({ eventTypeId: input.eventTypeId }); - } - } else { - where.AND?.push({ - OR: [{ userId: ctx.user.id }, { teamId: { in: user?.teams.map((membership) => membership.teamId) } }], - }); - } - - if (input?.eventTriggers) { - where.AND?.push({ eventTriggers: { hasEvery: input.eventTriggers } }); - } - } - - return await prisma.webhook.findMany({ - where, - }); + return await WebhookRepository.findWebhooksByFilters({ userId: ctx.user.id, input }); };