perf: Server-Side Data Fetching in App Router: /availability (#16536)

* wip

* add method to OrganizationRepository

* server side fetching for availability

* fix error

* server side fetching finish

* remove log

* fix

* fix

* fix type

* no need to export

* fix type

* address nit comments
This commit is contained in:
Benny Joo authored and GitHub committed 2024-09-10 19:23:42 -07:00
1 parent 7725adedc8
commit d9777a3011
10 files changed
+208 -92

No files matched your search

@@ -1,8 +1,13 @@
import type { PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { getServerSession } from "next-auth";
import { notFound } from "next/navigation";
import { AUTH_OPTIONS } from "@calcom/features/auth/lib/next-auth-options";
import { ScheduleRepository } from "@calcom/lib/server/repository/schedule";
import { TravelScheduleRepository } from "@calcom/lib/server/repository/travelSchedule";
import { UserRepository } from "@calcom/lib/server/repository/user";
import { AvailabilitySettingsWebWrapper } from "~/availability/[schedule]/schedule-view";
@@ -23,4 +28,50 @@ export const generateMetadata = async ({ params }: PageProps) => {
);
};
export default WithLayout({ getLayout: null, Page: AvailabilitySettingsWebWrapper });
const Page = async ({ params }: PageProps) => {
if (params?.schedule) {
notFound();
}
const scheduleId = Number(params.schedule);
const session = await getServerSession(AUTH_OPTIONS);
const userId = session?.user?.id;
if (!userId) {
notFound();
}
let userData, schedule, travelSchedules;
try {
userData = await UserRepository.findById({
id: userId,
select: {
timeZone: true,
defaultScheduleId: true,
},
});
if (!userData?.timeZone || !userData?.defaultScheduleId) {
throw new Error("timeZone and defaultScheduleId not found");
}
} catch (e) {
notFound();
}
try {
schedule = await ScheduleRepository.findDetailedScheduleById({
scheduleId,
isManagedEventType: false,
userId,
timeZone: userData.timeZone,
defaultScheduleId: userData.defaultScheduleId,
});
} catch (e) {}
try {
travelSchedules = await TravelScheduleRepository.findTravelSchedulesByUserId(userId);
} catch (e) {}
return <AvailabilitySettingsWebWrapper schedule={schedule} travelSchedules={travelSchedules} />;
};
export default WithLayout({ ServerPage: Page });
+25 -1
View File
@@ -1,5 +1,10 @@
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { getServerSession } from "next-auth";
import { notFound } from "next/navigation";
import { AUTH_OPTIONS } from "@calcom/features/auth/lib/next-auth-options";
import { OrganizationRepository } from "@calcom/lib/server/repository/organization";
import AvailabilityPage from "~/availability/availability-view";
@@ -10,4 +15,23 @@ export const generateMetadata = async () => {
);
};
export default WithLayout({ getLayout: null, Page: AvailabilityPage });
const Page = async () => {
const session = await getServerSession(AUTH_OPTIONS);
const userId = session?.user?.id;
const orgId = session?.user?.org?.id;
if (!userId || !orgId) {
notFound();
}
try {
const currentOrg = await OrganizationRepository.findCurrentOrg({
orgId,
userId,
});
return <AvailabilityPage currentOrg={currentOrg} />;
} catch {
notFound();
}
};
export default WithLayout({ ServerPage: Page });
@@ -8,11 +8,21 @@ import { withErrorFromUnknown } from "@calcom/lib/getClientErrorFromUnknown";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HttpError } from "@calcom/lib/http-error";
import type { ScheduleRepository } from "@calcom/lib/server/repository/schedule";
import type { TravelScheduleRepository } from "@calcom/lib/server/repository/travelSchedule";
import { trpc } from "@calcom/trpc/react";
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
import { showToast } from "@calcom/ui";
export const AvailabilitySettingsWebWrapper = () => {
type PageProps = {
schedule?: Awaited<ReturnType<typeof ScheduleRepository.findDetailedScheduleById>>;
travelSchedules?: Awaited<ReturnType<typeof TravelScheduleRepository.findTravelSchedulesByUserId>>;
};
export const AvailabilitySettingsWebWrapper = ({
schedule: scheduleProp,
travelSchedules: travelSchedulesProp,
}: PageProps) => {
const searchParams = useCompatSearchParams();
const { t } = useLocale();
const router = useRouter();
@@ -21,14 +31,19 @@ export const AvailabilitySettingsWebWrapper = () => {
const scheduleId = searchParams?.get("schedule") ? Number(searchParams.get("schedule")) : -1;
const fromEventType = searchParams?.get("fromEventType");
const { timeFormat } = me.data || { timeFormat: null };
const { data: schedule, isPending } = trpc.viewer.availability.schedule.get.useQuery(
const { data: scheduleData, isPending: isFetchingPending } = trpc.viewer.availability.schedule.get.useQuery(
{ scheduleId },
{
enabled: !!scheduleId,
enabled: !!scheduleId && !scheduleProp,
}
);
const isPending = isFetchingPending && !scheduleProp;
const schedule = scheduleProp ?? scheduleData;
const { data: travelSchedules } = trpc.viewer.getTravelSchedules.useQuery();
const { data: travelSchedulesData } = trpc.viewer.getTravelSchedules.useQuery(undefined, {
enabled: !travelSchedulesProp,
});
const travelSchedules = travelSchedulesProp ?? travelSchedulesData;
const [isBulkUpdateModalOpen, setIsBulkUpdateModalOpen] = useState(false);
const bulkUpdateDefaultAvailabilityMutation =
@@ -13,6 +13,7 @@ import { AvailabilitySliderTable } from "@calcom/features/timezone-buddy/compone
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HttpError } from "@calcom/lib/http-error";
import type { OrganizationRepository } from "@calcom/lib/server/repository/organization";
import { MembershipRole } from "@calcom/prisma/enums";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
@@ -170,13 +171,18 @@ function AvailabilityListWithQuery() {
);
}
export default function AvailabilityPage() {
type PageProps = {
currentOrg?: Awaited<ReturnType<typeof OrganizationRepository.findCurrentOrg>> | null;
};
export default function AvailabilityPage({ currentOrg }: PageProps) {
const { t } = useLocale();
const searchParams = useCompatSearchParams();
const router = useRouter();
const pathname = usePathname();
const me = useMeQuery();
const { data } = trpc.viewer.organizations.listCurrent.useQuery();
const { data: _data } = trpc.viewer.organizations.listCurrent.useQuery(undefined, { enabled: !currentOrg });
const data = currentOrg ?? _data;
// Get a new searchParams string by merging the current
// searchParams with a provided key/value pair
+6 -5
View File
@@ -38,19 +38,20 @@ export async function hasEditPermissionForUserID({ ctx, input }: InputOptions) {
return teamIdOverlaps;
}
export async function hasReadPermissionsForUserId({ ctx, input }: InputOptions) {
const { user } = ctx;
export async function hasReadPermissionsForUserId({
userId,
memberId,
}: InputOptions["input"] & { userId: number }) {
const authedUsersTeams = await prisma.membership.findMany({
where: {
userId: user.id,
userId,
accepted: true,
},
});
const targetUsersTeams = await prisma.membership.findMany({
where: {
userId: input.memberId,
userId: memberId,
accepted: true,
},
});
@@ -1,4 +1,12 @@
import prisma from "@calcom/prisma";
import { getDefaultScheduleId } from "@calcom/trpc/server/routers/viewer/availability/util";
import { hasReadPermissionsForUserId } from "../../hasEditPermissionForUser";
import {
transformAvailabilityForAtom,
transformDateOverridesForAtom,
transformWorkingHoursForAtom,
} from "../../schedules";
export class ScheduleRepository {
static async findScheduleById({ id }: { id: number }) {
@@ -17,4 +25,64 @@ export class ScheduleRepository {
return schedule;
}
static async findDetailedScheduleById({
isManagedEventType,
scheduleId,
userId,
defaultScheduleId,
timeZone: userTimeZone,
}: {
timeZone: string;
userId: number;
defaultScheduleId: number | null;
scheduleId?: number;
isManagedEventType?: boolean;
}) {
const schedule = await prisma.schedule.findUnique({
where: {
id: scheduleId || (await getDefaultScheduleId(userId, prisma)),
},
select: {
id: true,
userId: true,
name: true,
availability: true,
timeZone: true,
},
});
if (!schedule) {
throw new Error("Schedule not found");
}
const isCurrentUserPartOfTeam = hasReadPermissionsForUserId({ memberId: schedule?.userId, userId });
const isCurrentUserOwner = schedule?.userId === userId;
if (!isCurrentUserPartOfTeam && !isCurrentUserOwner) {
throw new Error("UNAUTHORIZED");
}
const timeZone = schedule.timeZone || userTimeZone;
const schedulesCount = await prisma.schedule.count({
where: {
userId: userId,
},
});
// disabling utc casting while fetching WorkingHours
return {
id: schedule.id,
name: schedule.name,
isManaged: schedule.userId !== userId,
workingHours: transformWorkingHoursForAtom(schedule),
schedule: schedule.availability,
availability: transformAvailabilityForAtom(schedule),
timeZone,
dateOverrides: transformDateOverridesForAtom(schedule, timeZone),
isDefault: !scheduleId || defaultScheduleId === schedule.id,
isLastSchedule: schedulesCount <= 1,
readOnly: schedule.userId !== userId && !isManagedEventType,
};
}
}
@@ -0,0 +1,19 @@
import { prisma } from "@calcom/prisma";
export class TravelScheduleRepository {
static async findTravelSchedulesByUserId(userId: number) {
const allTravelSchedules = await prisma.travelSchedule.findMany({
where: {
userId,
},
select: {
id: true,
startDate: true,
endDate: true,
timeZone: true,
},
});
return allTravelSchedules;
}
}
+2 -2
View File
@@ -238,12 +238,12 @@ export class UserRepository {
};
}
static async findById({ id }: { id: number }) {
static async findById({ id, select }: { id: number; select?: Prisma.UserSelect }) {
const user = await prisma.user.findUnique({
where: {
id,
},
select: userSelect,
select: { ...userSelect, ...select },
});
if (!user) {
@@ -1,4 +1,4 @@
import { prisma } from "@calcom/prisma";
import { TravelScheduleRepository } from "@calcom/lib/server/repository/travelSchedule";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
type GetTravelSchedulesOptions = {
@@ -8,17 +8,5 @@ type GetTravelSchedulesOptions = {
};
export const getTravelSchedulesHandler = async ({ ctx }: GetTravelSchedulesOptions) => {
const allTravelSchedules = await prisma.travelSchedule.findMany({
where: {
userId: ctx.user.id,
},
select: {
id: true,
startDate: true,
endDate: true,
timeZone: true,
},
});
return allTravelSchedules;
return await TravelScheduleRepository.findTravelSchedulesByUserId(ctx.user.id);
};
@@ -1,15 +1,6 @@
import {
transformAvailabilityForAtom,
transformDateOverridesForAtom,
transformWorkingHoursForAtom,
} from "@calcom/lib";
import { hasReadPermissionsForUserId } from "@calcom/lib/hasEditPermissionForUser";
import { prisma } from "@calcom/prisma";
import { TRPCError } from "@trpc/server";
import { ScheduleRepository } from "@calcom/lib/server/repository/schedule";
import type { TrpcSessionUser } from "../../../../trpc";
import { getDefaultScheduleId } from "../util";
import type { TGetInputSchema } from "./get.schema";
type GetOptions = {
@@ -20,58 +11,11 @@ type GetOptions = {
};
export const getHandler = async ({ ctx, input }: GetOptions) => {
const { user } = ctx;
const schedule = await prisma.schedule.findUnique({
where: {
id: input.scheduleId || (await getDefaultScheduleId(user.id, prisma)),
},
select: {
id: true,
userId: true,
name: true,
availability: true,
timeZone: true,
},
return await ScheduleRepository.findDetailedScheduleById({
scheduleId: input.scheduleId,
isManagedEventType: input.isManagedEventType,
userId: ctx.user.id,
timeZone: ctx.user.timeZone,
defaultScheduleId: ctx.user.defaultScheduleId,
});
if (!schedule) {
throw new TRPCError({
code: "UNAUTHORIZED",
});
}
const isCurrentUserPartOfTeam = hasReadPermissionsForUserId({
ctx,
input: { memberId: schedule?.userId },
});
const isCurrentUserOwner = schedule?.userId === user.id;
if (!isCurrentUserPartOfTeam && !isCurrentUserOwner) {
throw new TRPCError({
code: "UNAUTHORIZED",
});
}
const timeZone = schedule.timeZone || user.timeZone;
const schedulesCount = await prisma.schedule.count({
where: {
userId: user.id,
},
});
// disabling utc casting while fetching WorkingHours
return {
id: schedule.id,
name: schedule.name,
isManaged: schedule.userId !== user.id,
workingHours: transformWorkingHoursForAtom(schedule),
schedule: schedule.availability,
availability: transformAvailabilityForAtom(schedule),
timeZone,
dateOverrides: transformDateOverridesForAtom(schedule, timeZone),
isDefault: !input.scheduleId || user.defaultScheduleId === schedule.id,
isLastSchedule: schedulesCount <= 1,
readOnly: schedule.userId !== user.id && !input.isManagedEventType,
};
};