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:
co-authored by
Hariom
Keith Williams
parent
5c98861f6f
commit
1e610ea684
+4
-4
@@ -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"];
|
||||
}
|
||||
|
||||
@@ -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 }[];
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user