perf: use repository for webhooks list query & caching in /settings/developer/webhooks/... RSC (#21781)

* perf: use repository for webhooks list query & caching in /settings/developer/webhooks/... RSC

* fix type check

* invalidate cache on webhook list item actions

* fix review comment

* fix merge conflicts
This commit is contained in:
Amit Sharma
2025-08-05 01:25:31 +01:00
committed by GitHub
parent a61148cb51
commit 0829929e98
11 changed files with 170 additions and 79 deletions
@@ -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");
}
@@ -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 <WebhooksView data={data} isAdmin={isAdmin} />;
};
@@ -0,0 +1,7 @@
"use server";
import { revalidateTag } from "next/cache";
export async function revalidateWebhookById(id: string) {
revalidateTag(`viewer.webhook.get:${id}`);
}
@@ -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 (
<SettingsHeader
@@ -0,0 +1,7 @@
"use server";
import { revalidateTag } from "next/cache";
export async function revalidateWebhookList() {
revalidateTag("viewer.webhook.list");
}
@@ -1,10 +1,16 @@
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 { NewWebhookView } from "@calcom/features/webhooks/pages/webhook-new-view";
import { APP_NAME } from "@calcom/lib/constants";
import { WebhookRepository } from "@calcom/lib/server/repository/webhook";
import { appsRouter } from "@calcom/trpc/server/routers/viewer/apps/_router";
import { webhookRouter } from "@calcom/trpc/server/routers/viewer/webhook/_router";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
export const generateMetadata = async () =>
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 <NewWebhookView webhooks={webhooks} installedApps={installedApps} />;
@@ -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();
},
});
@@ -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) {
@@ -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) {
+83
View File
@@ -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<typeof ZFindWebhooksByFiltersInputSchema>;
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,
});
}
}
@@ -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 });
};