perf: use repository for queries & caching in /teams RSC (#21399)
* cache trpc queries in /teams * fix * revalidate * refactor: migrate listHandler to TeamRepository.findTeamsByUserId Co-Authored-By: benny@cal.com <benny@cal.com> * add comment * refactor * refactor * make it even more performant --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: benny@cal.com <benny@cal.com>
This commit is contained in:
co-authored by
benny@cal.com
Devin AI
parent
14b9e0c032
commit
13966e0ec8
@@ -1,7 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { revalidateTag } from "next/cache";
|
||||
|
||||
export async function revalidateTeamsList() {
|
||||
revalidatePath("/teams");
|
||||
revalidateTag("viewer.teams.list");
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ const ServerPage = async ({ searchParams: _searchParams }: ServerPageProps) => {
|
||||
}
|
||||
|
||||
const t = await getTranslate();
|
||||
const { Main, CTA } = await ServerTeamsListing({ searchParams });
|
||||
const { Main, CTA } = await ServerTeamsListing({ searchParams, session });
|
||||
|
||||
return (
|
||||
<ShellMainAppDir CTA={CTA} heading={t("teams")} subtitle={t("create_manage_teams_collaborative")}>
|
||||
|
||||
@@ -1,44 +1,51 @@
|
||||
import { createRouterCaller } from "app/_trpc/context";
|
||||
import type { SearchParams } from "app/_types";
|
||||
import type { Session } from "next-auth";
|
||||
import { unstable_cache } from "next/cache";
|
||||
|
||||
import { TeamsListing } from "@calcom/features/ee/teams/components/TeamsListing";
|
||||
import { CreationSource } from "@calcom/prisma/enums";
|
||||
import { TeamRepository } from "@calcom/lib/server/repository/team";
|
||||
import { meRouter } from "@calcom/trpc/server/routers/viewer/me/_router";
|
||||
import { viewerTeamsRouter } from "@calcom/trpc/server/routers/viewer/teams/_router";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import { TeamsCTA } from "./CTA";
|
||||
|
||||
export const ServerTeamsListing = async ({ searchParams }: { searchParams: SearchParams }) => {
|
||||
const token = Array.isArray(searchParams?.token) ? searchParams.token[0] : searchParams?.token;
|
||||
const getCachedTeams = unstable_cache(
|
||||
async (userId: number) => {
|
||||
return await TeamRepository.findTeamsByUserId({
|
||||
userId,
|
||||
includeOrgs: true,
|
||||
});
|
||||
},
|
||||
undefined,
|
||||
{ revalidate: 3600, tags: ["viewer.teams.list"] } // Cache for 1 hour
|
||||
);
|
||||
|
||||
const [teamsCaller, meCaller] = await Promise.all([
|
||||
createRouterCaller(viewerTeamsRouter),
|
||||
createRouterCaller(meRouter),
|
||||
]);
|
||||
export const ServerTeamsListing = async ({
|
||||
searchParams,
|
||||
session,
|
||||
}: {
|
||||
searchParams: SearchParams;
|
||||
session: Session;
|
||||
}) => {
|
||||
const token = Array.isArray(searchParams?.token) ? searchParams.token[0] : searchParams?.token;
|
||||
const userId = session.user.id;
|
||||
|
||||
let teamNameFromInvite,
|
||||
errorMsgFromInvite = null;
|
||||
|
||||
if (token) {
|
||||
try {
|
||||
teamNameFromInvite = await teamsCaller.inviteMemberByToken({
|
||||
token,
|
||||
creationSource: CreationSource.WEBAPP,
|
||||
});
|
||||
teamNameFromInvite = await TeamRepository.inviteMemberByToken(token, userId);
|
||||
} catch (e) {
|
||||
errorMsgFromInvite = "Error while fetching teams";
|
||||
if (e instanceof TRPCError) errorMsgFromInvite = e.message;
|
||||
}
|
||||
}
|
||||
|
||||
const [user, teams] = await Promise.all([
|
||||
meCaller.get(),
|
||||
teamsCaller.list({
|
||||
includeOrgs: true,
|
||||
}),
|
||||
]);
|
||||
const meCaller = await createRouterCaller(meRouter);
|
||||
const [user, teams] = await Promise.all([meCaller.get(), getCachedTeams(userId)]);
|
||||
|
||||
return {
|
||||
Main: (
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Form } from "@calcom/ui/components/form";
|
||||
import { Label } from "@calcom/ui/components/form";
|
||||
import { Select } from "@calcom/ui/components/form";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
import { revalidateTeamsList } from "@calcom/web/app/(use-page-wrapper)/(main-nav)/teams/actions";
|
||||
|
||||
type InvitationLinkSettingsModalProps = {
|
||||
isOpen: boolean;
|
||||
@@ -32,6 +33,7 @@ export default function InviteLinkSettingsModal(props: InvitationLinkSettingsMod
|
||||
showToast(t("invite_link_deleted"), "success");
|
||||
trpcContext.viewer.teams.get.invalidate();
|
||||
trpcContext.viewer.teams.list.invalidate();
|
||||
revalidateTeamsList();
|
||||
props.onExit();
|
||||
},
|
||||
onError: (e) => {
|
||||
@@ -44,6 +46,7 @@ export default function InviteLinkSettingsModal(props: InvitationLinkSettingsMod
|
||||
showToast(t("invite_link_updated"), "success");
|
||||
trpcContext.viewer.teams.get.invalidate();
|
||||
trpcContext.viewer.teams.list.invalidate();
|
||||
revalidateTeamsList();
|
||||
},
|
||||
onError: (e) => {
|
||||
showToast(e.message, "error");
|
||||
|
||||
@@ -26,6 +26,7 @@ import { Select } from "@calcom/ui/components/form";
|
||||
import { ToggleGroup } from "@calcom/ui/components/form";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
import { revalidateTeamsList } from "@calcom/web/app/(use-page-wrapper)/(main-nav)/teams/actions";
|
||||
|
||||
import type { PendingMember } from "../lib/types";
|
||||
import { GoogleWorkspaceInviteButton } from "./GoogleWorkspaceInviteButton";
|
||||
@@ -92,6 +93,7 @@ export default function MemberInvitationModal(props: MemberInvitationModalProps)
|
||||
async onSuccess({ inviteLink }) {
|
||||
trpcContext.viewer.teams.get.invalidate();
|
||||
trpcContext.viewer.teams.list.invalidate();
|
||||
revalidateTeamsList();
|
||||
},
|
||||
onError: (error) => {
|
||||
showToast(error.message, "error");
|
||||
|
||||
@@ -318,4 +318,46 @@ export class TeamRepository {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static async findTeamsByUserId({ userId, includeOrgs }: { userId: number; includeOrgs?: boolean }) {
|
||||
const memberships = await prisma.membership.findMany({
|
||||
where: {
|
||||
// Show all the teams this user belongs to regardless of the team being part of the user's org or not
|
||||
// We don't want to restrict in the listing here. If we need to restrict a situation where a user is part of the org along with being part of a non-org team, we should do that instead of filtering out from here
|
||||
// This became necessary when we started migrating user to Org, without migrating some teams of the user to the org
|
||||
// Also, we would allow a user to be part of multiple orgs, then also it would be necessary.
|
||||
userId: userId,
|
||||
},
|
||||
include: {
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
slug: true,
|
||||
logoUrl: true,
|
||||
isOrganization: true,
|
||||
metadata: true,
|
||||
inviteTokens: true,
|
||||
parent: true,
|
||||
parentId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { role: "desc" },
|
||||
});
|
||||
|
||||
return memberships
|
||||
.filter((mmship) => {
|
||||
if (includeOrgs) return true;
|
||||
return !mmship.team.isOrganization;
|
||||
})
|
||||
.map(({ team: { inviteTokens, ...team }, ...membership }) => ({
|
||||
role: membership.role,
|
||||
accepted: membership.accepted,
|
||||
...team,
|
||||
metadata: teamMetadataSchema.parse(team.metadata),
|
||||
/** To prevent breaking we only return non-email attached token here, if we have one */
|
||||
inviteToken: inviteTokens.find((token) => token.identifier === `invite-link-for-teamId-${team.id}`),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
import { TeamRepository } from "@calcom/lib/server/repository/team";
|
||||
|
||||
import type { TrpcSessionUser } from "../../../types";
|
||||
import type { TGetListSchema } from "./list.schema";
|
||||
@@ -12,45 +11,10 @@ type ListOptions = {
|
||||
};
|
||||
|
||||
export const listHandler = async ({ ctx, input }: ListOptions) => {
|
||||
const memberships = await prisma.membership.findMany({
|
||||
where: {
|
||||
// Show all the teams this user belongs to regardless of the team being part of the user's org or not
|
||||
// We don't want to restrict in the listing here. If we need to restrict a situation where a user is part of the org along with being part of a non-org team, we should do that instead of filtering out from here
|
||||
// This became necessary when we started migrating user to Org, without migrating some teams of the user to the org
|
||||
// Also, we would allow a user to be part of multiple orgs, then also it would be necessary.
|
||||
userId: ctx.user.id,
|
||||
},
|
||||
include: {
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
slug: true,
|
||||
logoUrl: true,
|
||||
isOrganization: true,
|
||||
metadata: true,
|
||||
inviteTokens: true,
|
||||
parent: true,
|
||||
parentId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { role: "desc" },
|
||||
return TeamRepository.findTeamsByUserId({
|
||||
userId: ctx.user.id,
|
||||
includeOrgs: input?.includeOrgs,
|
||||
});
|
||||
|
||||
return memberships
|
||||
.filter((mmship) => {
|
||||
if (input?.includeOrgs) return true;
|
||||
return !mmship.team.isOrganization;
|
||||
})
|
||||
.map(({ team: { inviteTokens, ...team }, ...membership }) => ({
|
||||
role: membership.role,
|
||||
accepted: membership.accepted,
|
||||
...team,
|
||||
metadata: teamMetadataSchema.parse(team.metadata),
|
||||
/** To prevent breaking we only return non-email attached token here, if we have one */
|
||||
inviteToken: inviteTokens.find((token) => token.identifier === `invite-link-for-teamId-${team.id}`),
|
||||
}));
|
||||
};
|
||||
|
||||
export default listHandler;
|
||||
|
||||
Reference in New Issue
Block a user