perf: don't fetch all the hosts (#18319)

* perf: don't fetch all the hosts

* fix: type err

* use appschema only when necessary

* Variable casing change and type fixes

* Another missing ts fix

* chore: update variable name

* chore: remove from personal event types

* chore: remove limitHostsToThree

* chore: make it a variable

* chore: move it below

* chore: change name to firstThreeHosts

* chore: update to first three users

* fix: type error

* chore: add firstThreeHosts

* fix: type error

* fix: type error

* fix: support users

* chore: update metadata

* fix: API v2 build error

* chore: change var name

* chore: remove cursorrules

* chore: undo API v2 changes

* chore: add type

---------

Co-authored-by: Hariom <hariombalhara@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
This commit is contained in:
Udit Takkar
2025-01-29 19:05:54 +02:00
committed by GitHub
co-authored by Hariom Keith Williams
parent 5c98861f6f
commit 1e610ea684
12 changed files with 159 additions and 124 deletions
@@ -399,8 +399,8 @@ class PublicEventTypeOutput {
workflows!: any[];
@IsArray()
@ApiProperty()
hosts!: any[];
@ApiPropertyOptional()
hosts?: any[];
@ValidateNested()
@Type(() => Owner)
@@ -427,8 +427,8 @@ class PublicEventTypeOutput {
@IsArray()
@ValidateNested({ each: true })
@Type(() => User)
@ApiProperty({ type: [User] })
users!: User[];
@ApiPropertyOptional({ type: [User] })
users?: User[];
@IsObject()
@ApiProperty({ type: Object })
@@ -20,9 +20,9 @@ type Props = {
eventData: Omit<
Pick<
NonNullable<Awaited<ReturnType<typeof getPublicEvent>>>,
"id" | "length" | "metadata" | "entity" | "profile" | "title" | "users" | "hidden"
"id" | "length" | "metadata" | "entity" | "profile" | "title" | "subsetOfUsers" | "hidden"
>,
"profile" | "users"
"profile" | "subsetOfUsers"
> & {
profile: {
image: string | undefined;
@@ -163,7 +163,10 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
username: eventData.profile.username ?? null,
},
title: eventData.title,
users: eventData.users.map((user) => ({ username: user.username ?? "", name: user.name ?? "" })),
users: eventData.subsetOfUsers.map((user) => ({
username: user.username ?? "",
name: user.name ?? "",
})),
hidden: eventData.hidden,
},
user: usernames.join("+"),
@@ -259,7 +262,10 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
username: eventData.profile.username ?? null,
},
title: eventData.title,
users: eventData.users.map((user) => ({ username: user.username ?? "", name: user.name ?? "" })),
users: eventData.subsetOfUsers.map((user) => ({
username: user.username ?? "",
name: user.name ?? "",
})),
hidden: eventData.hidden,
},
user: username,
@@ -18,7 +18,7 @@ export const DatePicker = ({
scrollToTimeSlots,
}: {
event: {
data?: { users: Pick<User, "weekStart">[] } | null;
data?: { subsetOfUsers: Pick<User, "weekStart">[] } | null;
};
schedule: useScheduleForEventReturnType;
classNames?: {
@@ -85,7 +85,7 @@ export const DatePicker = ({
locale={i18n.language}
browsingDate={month ? dayjs(month) : undefined}
selected={dayjs(selectedDate)}
weekStart={weekdayToWeekIndex(event?.data?.users?.[0]?.weekStart)}
weekStart={weekdayToWeekIndex(event?.data?.subsetOfUsers?.[0]?.weekStart)}
slots={schedule?.data?.slots}
scrollToTimeSlots={scrollToTimeSlots}
/>
@@ -55,7 +55,7 @@ export const EventMeta = ({
| "lockTimeZoneToggleOnBookingPage"
| "schedule"
| "seatsPerTimeSlot"
| "users"
| "subsetOfUsers"
| "length"
| "schedulingType"
| "profile"
@@ -150,7 +150,7 @@ export const EventMeta = ({
<m.div {...fadeInUp} layout transition={{ ...fadeInUp.transition, delay: 0.3 }}>
<EventMembers
schedulingType={event.schedulingType}
users={event.users}
users={event.subsetOfUsers}
profile={event.profile}
entity={event.entity}
/>
@@ -6,7 +6,7 @@ import { Button, UserAvatarGroupWithOrg } from "@calcom/ui";
interface IInstantBookingProps {
onConnectNow: () => void;
event: Pick<BookerEvent, "entity" | "schedulingType"> & {
users: (Pick<User, "name" | "username" | "avatarUrl"> & { bookerUrl: string })[];
subsetOfUsers: (Pick<User, "name" | "username" | "avatarUrl"> & { bookerUrl: string })[];
};
}
@@ -25,7 +25,7 @@ export const InstantBooking = ({ onConnectNow, event }: IInstantBookingProps) =>
name: event.entity.name || "",
logoUrl: event.entity.logoUrl ?? null,
}}
users={event.users.slice(0, 2)}
users={event.subsetOfUsers.slice(0, 2)}
disableHref
/>
<div className="border-muted absolute -bottom-0.5 -right-1 h-2 w-2 rounded-full border bg-green-500" />
@@ -28,7 +28,7 @@ export interface IUseBookings {
BookerEvent,
| "id"
| "slug"
| "hosts"
| "subsetOfHosts"
| "requiresConfirmation"
| "isDynamic"
| "metadata"
@@ -38,8 +38,8 @@ export interface IUseBookings {
| "recurringEvent"
| "schedulingType"
> & {
users: Pick<
BookerEvent["users"][number],
subsetOfUsers: Pick<
BookerEvent["subsetOfUsers"][number],
"name" | "username" | "avatarUrl" | "weekStart" | "profile" | "bookerUrl"
>[];
})
@@ -187,9 +187,9 @@ export const useBookings = ({ event, hashedLink, bookingForm, metadata, teamMemb
const { uid, paymentUid } = booking;
const fullName = getFullName(bookingForm.getValues("responses.name"));
const users = !!event.data?.hosts?.length
? event.data?.hosts.map((host) => host.user)
: event.data?.users;
const users = !!event.data?.subsetOfHosts?.length
? event.data?.subsetOfHosts.map((host) => host.user)
: event.data?.subsetOfUsers;
const validDuration = event.data?.isDynamic
? duration || event.data?.length
@@ -14,7 +14,7 @@ export interface EventMembersProps {
* In case of Round Robin type, members aren't shown.
*/
schedulingType: BookerEvent["schedulingType"];
users: BookerEvent["users"];
users: BookerEvent["subsetOfUsers"];
profile: BookerEvent["profile"];
entity: BookerEvent["entity"];
}
+6 -3
View File
@@ -15,7 +15,7 @@ export type BookerEventQuery = {
};
type BookerEventUser = Pick<
PublicEvent["users"][number],
PublicEvent["subsetOfUsers"][number],
"name" | "username" | "avatarUrl" | "weekStart" | "profile"
> & {
metadata?: undefined;
@@ -47,14 +47,17 @@ export type BookerEvent = Pick<
| "description"
| "forwardParamsSuccessRedirect"
| "successRedirectUrl"
| "hosts"
| "subsetOfHosts"
| "bookingFields"
| "seatsShowAvailabilityCount"
| "isInstantEvent"
| "instantMeetingParameters"
| "fieldTranslations"
| "autoTranslateDescriptionEnabled"
> & { users: BookerEventUser[]; showInstantEventConnectNowModal: boolean } & { profile: BookerEventProfile };
> & {
subsetOfUsers: BookerEventUser[];
showInstantEventConnectNowModal: boolean;
} & { profile: BookerEventProfile };
export type ValidationErrors<T extends object> = { key: FieldPath<T>; error: ErrorOption }[];
+1 -1
View File
@@ -280,7 +280,7 @@ const EmailEmbed = ({
locale={i18n.language}
browsingDate={month ? dayjs(month) : undefined}
selected={dayjs(selectedDate)}
weekStart={weekdayToWeekIndex(event?.data?.users?.[0]?.weekStart)}
weekStart={weekdayToWeekIndex(event?.data?.subsetOfUsers?.[0]?.weekStart)}
eventSlug={eventType?.slug}
/>
</CollapsibleContent>
@@ -50,98 +50,100 @@ const userSelect = Prisma.validator<Prisma.UserSelect>()({
defaultScheduleId: true,
});
const publicEventSelect = Prisma.validator<Prisma.EventTypeSelect>()({
id: true,
title: true,
description: true,
eventName: true,
slug: true,
isInstantEvent: true,
instantMeetingParameters: true,
aiPhoneCallConfig: true,
schedulingType: true,
length: true,
locations: true,
customInputs: true,
disableGuests: true,
metadata: true,
lockTimeZoneToggleOnBookingPage: true,
requiresConfirmation: true,
autoTranslateDescriptionEnabled: true,
fieldTranslations: {
select: {
translatedText: true,
targetLocale: true,
field: true,
},
},
requiresBookerEmailVerification: true,
recurringEvent: true,
price: true,
currency: true,
seatsPerTimeSlot: true,
seatsShowAvailabilityCount: true,
bookingFields: true,
teamId: true,
team: {
select: {
parentId: true,
metadata: true,
brandColor: true,
darkBrandColor: true,
slug: true,
name: true,
logoUrl: true,
theme: true,
parent: {
select: {
slug: true,
name: true,
bannerUrl: true,
logoUrl: true,
},
const getPublicEventSelect = (fetchAllUsers: boolean) => {
return Prisma.validator<Prisma.EventTypeSelect>()({
id: true,
title: true,
description: true,
eventName: true,
slug: true,
isInstantEvent: true,
instantMeetingParameters: true,
aiPhoneCallConfig: true,
schedulingType: true,
length: true,
locations: true,
customInputs: true,
disableGuests: true,
metadata: true,
lockTimeZoneToggleOnBookingPage: true,
requiresConfirmation: true,
autoTranslateDescriptionEnabled: true,
fieldTranslations: {
select: {
translatedText: true,
targetLocale: true,
field: true,
},
isPrivate: true,
},
},
successRedirectUrl: true,
forwardParamsSuccessRedirect: true,
workflows: {
include: {
workflow: {
include: {
steps: true,
requiresBookerEmailVerification: true,
recurringEvent: true,
price: true,
currency: true,
seatsPerTimeSlot: true,
seatsShowAvailabilityCount: true,
bookingFields: true,
teamId: true,
team: {
select: {
parentId: true,
metadata: true,
brandColor: true,
darkBrandColor: true,
slug: true,
name: true,
logoUrl: true,
theme: true,
parent: {
select: {
slug: true,
name: true,
bannerUrl: true,
logoUrl: true,
},
},
isPrivate: true,
},
},
successRedirectUrl: true,
forwardParamsSuccessRedirect: true,
workflows: {
include: {
workflow: {
include: {
steps: true,
},
},
},
},
},
hosts: {
select: {
user: {
select: userSelect,
hosts: {
select: {
user: {
select: userSelect,
},
},
...(fetchAllUsers ? {} : { take: 3 }),
},
owner: {
select: userSelect,
},
schedule: {
select: {
id: true,
timeZone: true,
},
},
},
owner: {
select: userSelect,
},
schedule: {
select: {
id: true,
timeZone: true,
instantMeetingSchedule: {
select: {
id: true,
timeZone: true,
},
},
},
instantMeetingSchedule: {
select: {
id: true,
timeZone: true,
},
},
hidden: true,
assignAllTeamMembers: true,
rescheduleWithSameRoundRobinHost: true,
});
hidden: true,
assignAllTeamMembers: true,
rescheduleWithSameRoundRobinHost: true,
});
};
export async function isCurrentlyAvailable({
prisma,
@@ -217,7 +219,8 @@ export const getPublicEvent = async (
org: string | null,
prisma: PrismaClient,
fromRedirectOfNonOrgLink: boolean,
currentUserId?: number
currentUserId?: number,
fetchAllUsers = false
) => {
const usernameList = getUsernameList(username);
const orgQuery = org ? getSlugOrRequestedSlug(org) : null;
@@ -270,11 +273,18 @@ export const getPublicEvent = async (
...defaultEvent,
bookingFields: getBookingFieldsWithSystemFields({ ...defaultEvent, disableBookingTitle }),
// Clears meta data since we don't want to send this in the public api.
users: users.map((user) => ({
subsetOfUsers: users.map((user) => ({
...user,
metadata: undefined,
bookerUrl: getBookerBaseUrlSync(user.profile?.organization?.slug ?? null),
})),
users: fetchAllUsers
? users.map((user) => ({
...user,
metadata: undefined,
bookerUrl: getBookerBaseUrlSync(user.profile?.organization?.slug ?? null),
}))
: undefined,
locations: privacyFilteredLocations(locations),
profile: {
weekStart: users[0].weekStart,
@@ -342,7 +352,7 @@ export const getPublicEvent = async (
slug: eventSlug,
...usersOrTeamQuery,
},
select: publicEventSelect,
select: getPublicEventSelect(fetchAllUsers),
});
// If no event was found, check for platform org user event
@@ -362,7 +372,7 @@ export const getPublicEvent = async (
},
},
},
select: publicEventSelect,
select: getPublicEventSelect(fetchAllUsers),
});
}
@@ -388,7 +398,8 @@ export const getPublicEvent = async (
user: event.owner,
})
: null,
hosts: hosts,
subsetOfHosts: hosts,
hosts: fetchAllUsers ? hosts : undefined,
};
let users =
@@ -471,7 +482,8 @@ export const getPublicEvent = async (
: null,
// Sets user data on profile object for easier access
profile: getProfileFromEvent(eventWithUserProfiles),
users,
subsetOfUsers: users,
users: fetchAllUsers ? users : undefined,
entity: {
fromRedirectOfNonOrgLink,
considerUnpublished:
@@ -493,7 +505,6 @@ export const getPublicEvent = async (
}
: {}),
},
isDynamic: false,
isInstantEvent: eventWithUserProfiles.isInstantEvent,
showInstantEventConnectNowModal,
@@ -504,13 +515,18 @@ export const getPublicEvent = async (
};
const eventData = Prisma.validator<Prisma.EventTypeArgs>()({
select: publicEventSelect,
select: getPublicEventSelect(true),
});
type Event = Prisma.EventTypeGetPayload<typeof eventData>;
function getProfileFromEvent(event: Event) {
const { team, hosts, owner } = event;
type GetProfileFromEventInput = Omit<Event, "hosts"> & {
hosts?: Event["hosts"];
subsetOfHosts: Event["hosts"];
};
function getProfileFromEvent(event: GetProfileFromEventInput) {
const { team, subsetOfHosts: hosts, owner } = event;
const nonTeamprofile = hosts?.[0]?.user || owner;
const profile = team || nonTeamprofile;
if (!profile) throw new Error("Event has no owner");
@@ -538,6 +554,7 @@ function getProfileFromEvent(event: Event) {
),
};
}
async function getUsersFromEvent(
event: Omit<Event, "owner" | "hosts"> & {
owner:
@@ -545,7 +562,12 @@ async function getUsersFromEvent(
profile: UserProfile;
})
| null;
hosts: (Omit<Event["hosts"][number], "user"> & {
hosts?: (Omit<Event["hosts"][number], "user"> & {
user: Event["hosts"][number]["user"] & {
profile: UserProfile;
};
})[];
subsetOfHosts: (Omit<Event["hosts"][number], "user"> & {
user: Event["hosts"][number]["user"] & {
profile: UserProfile;
};
@@ -553,11 +575,12 @@ async function getUsersFromEvent(
},
prisma: PrismaClient
) {
const { team, hosts, owner, id } = event;
const { team, hosts, subsetOfHosts, owner, id } = event;
if (team) {
const eventHosts = !!hosts?.length ? hosts : subsetOfHosts;
// getOwnerFromUsersArray is used here for backward compatibility when team event type has users[] but not hosts[]
return hosts.length
? hosts.filter((host) => host.user.username).map(mapHostsToUsers)
return eventHosts.length
? eventHosts.filter((host) => host.user.username).map(mapHostsToUsers)
: (await getOwnerFromUsersArray(prisma, id)) ?? [];
}
if (!owner) {
+1
View File
@@ -109,6 +109,7 @@ const commons = {
workflows: [],
users: [user],
hosts: [],
subsetOfHosts: [],
metadata: EventTypeMetaDataSchema.parse({}),
bookingFields: [],
assignAllTeamMembers: false,
@@ -31,7 +31,9 @@ export const BookerWebWrapper = (props: BookerWebWrapperAtomProps) => {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const event = useEvent({ fromRedirectOfNonOrgLink: props.entity.fromRedirectOfNonOrgLink });
const event = useEvent({
fromRedirectOfNonOrgLink: props.entity.fromRedirectOfNonOrgLink,
});
const bookerLayout = useBookerLayout(event.data);
const selectedDate = searchParams?.get("date");