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:
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 });
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
Reference in new issue
Block a user