fix: Reduce payload even further. (#13599)

* Revert "Revert event-type profile based querying (#13588)"

This reverts commit c0c40185fc.

* Add huge event-types seeder

* Fix missing where causing wrong event-types for user
This commit is contained in:
Hariom Balhara
2024-02-09 09:07:26 -05:00
committed by GitHub
parent 296a367955
commit 7fef9db4ce
9 changed files with 458 additions and 277 deletions
@@ -25,9 +25,11 @@ import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
import { HttpError } from "@calcom/lib/http-error";
import type { User } from "@calcom/prisma/client";
import { SchedulingType } from "@calcom/prisma/enums";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc, TRPCClientError } from "@calcom/trpc/react";
import type { UserProfile } from "@calcom/types/UserProfile";
import {
Alert,
Avatar,
@@ -76,6 +78,7 @@ import useMeQuery from "@lib/hooks/useMeQuery";
import SkeletonLoader from "@components/eventtype/SkeletonLoader";
type EventTypeGroups = RouterOutputs["viewer"]["eventTypes"]["getByViewer"]["eventTypeGroups"];
type EventTypeGroupProfile = EventTypeGroups[number]["profile"];
type GetByViewerResponse = RouterOutputs["viewer"]["eventTypes"]["getByViewer"] | undefined;
@@ -89,16 +92,27 @@ interface EventTypeListHeadingProps {
type EventTypeGroup = EventTypeGroups[number];
type EventType = EventTypeGroup["eventTypes"][number];
type DeNormalizedEventType = Omit<EventType, "userIds"> & {
users: (Pick<User, "id" | "name" | "username" | "avatarUrl"> & {
nonProfileUsername: string | null;
profile: UserProfile;
})[];
};
type DeNormalizedEventTypeGroup = Omit<EventTypeGroup, "eventTypes"> & {
eventTypes: DeNormalizedEventType[];
};
interface EventTypeListProps {
group: EventTypeGroup;
group: DeNormalizedEventTypeGroup;
groupIndex: number;
readOnly: boolean;
bookerUrl: string | null;
types: EventType[];
types: DeNormalizedEventType[];
}
interface MobileTeamsTabProps {
eventTypeGroups: EventTypeGroups;
eventTypeGroups: DeNormalizedEventTypeGroup[];
}
const querySchema = z.object({
@@ -136,7 +150,15 @@ const MobileTeamsTab: FC<MobileTeamsTabProps> = (props) => {
);
};
const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGroup; readOnly: boolean }) => {
const Item = ({
type,
group,
readOnly,
}: {
type: DeNormalizedEventType;
group: DeNormalizedEventTypeGroup;
readOnly: boolean;
}) => {
const { t } = useLocale();
const content = () => (
@@ -238,7 +260,7 @@ export const EventTypeList = ({
await utils.viewer.eventTypes.getByViewer.cancel();
const previousValue = utils.viewer.eventTypes.getByViewer.getData();
if (previousValue) {
const newList = [...types];
const newList = [...types.map(normalizeEventType)];
const itemIndex = newList.findIndex((item) => item.id === id);
if (itemIndex !== -1 && newList[itemIndex]) {
newList[itemIndex].hidden = !newList[itemIndex].hidden;
@@ -267,13 +289,13 @@ export const EventTypeList = ({
});
async function moveEventType(index: number, increment: 1 | -1) {
const newList = [...types];
const newList = [...types.map(normalizeEventType)];
const type = types[index];
const tmp = types[index + increment];
if (tmp) {
newList[index] = tmp;
newList[index + increment] = type;
newList[index] = normalizeEventType(tmp);
newList[index + increment] = normalizeEventType(type);
}
await utils.viewer.eventTypes.getByViewer.cancel();
@@ -301,7 +323,7 @@ export const EventTypeList = ({
}
// inject selection data into url for correct router history
const openDuplicateModal = (eventType: EventType, group: EventTypeGroup) => {
const openDuplicateModal = (eventType: DeNormalizedEventType, group: DeNormalizedEventTypeGroup) => {
const newSearchParams = new URLSearchParams(searchParams ?? undefined);
function setParamsIfDefined(key: string, value: string | number | boolean | null | undefined) {
if (value) newSearchParams.set(key, value.toString());
@@ -326,7 +348,7 @@ export const EventTypeList = ({
await utils.viewer.eventTypes.getByViewer.cancel();
const previousValue = utils.viewer.eventTypes.getByViewer.getData();
if (previousValue) {
const newList = types.filter((item) => item.id !== id);
const newList = types.filter((item) => item.id !== id).map(normalizeEventType);
utils.viewer.eventTypes.getByViewer.setData(undefined, {
...previousValue,
@@ -811,12 +833,12 @@ const CTA = ({ data }: { data: GetByViewerResponse }) => {
const Actions = () => {
return (
<div className="hidden items-center md:flex">
<TeamsFilter popoverTriggerClassNames="mb-0" showVerticalDivider={true} />
<TeamsFilter useProfileFilter popoverTriggerClassNames="mb-0" showVerticalDivider={true} />
</div>
);
};
const EmptyEventTypeList = ({ group }: { group: EventTypeGroup }) => {
const EmptyEventTypeList = ({ group }: { group: DeNormalizedEventTypeGroup }) => {
const { t } = useLocale();
return (
<>
@@ -838,7 +860,7 @@ const EmptyEventTypeList = ({ group }: { group: EventTypeGroup }) => {
const Main = ({
status,
errorMessage,
data,
data: rawData,
filters,
}: {
status: string;
@@ -849,7 +871,7 @@ const Main = ({
const isMobile = useMediaQuery("(max-width: 768px)");
const searchParams = useCompatSearchParams();
if (!data || status === "pending") {
if (!rawData || status === "pending") {
return <SkeletonLoader />;
}
@@ -858,7 +880,11 @@ const Main = ({
}
const isFilteredByOnlyOneItem =
(filters?.teamIds?.length === 1 || filters?.userIds?.length === 1) && data.eventTypeGroups.length === 1;
(filters?.teamIds?.length === 1 || filters?.userIds?.length === 1) &&
rawData.eventTypeGroups.length === 1;
const data = denormalizePayload(rawData);
return (
<>
{data.eventTypeGroups.length > 1 || isFilteredByOnlyOneItem ? (
@@ -866,7 +892,7 @@ const Main = ({
{isMobile ? (
<MobileTeamsTab eventTypeGroups={data.eventTypeGroups} />
) : (
data.eventTypeGroups.map((group: EventTypeGroup, index: number) => (
data.eventTypeGroups.map((group, index: number) => (
<div
className="mt-4 flex flex-col"
data-testid={`slug-${group.profile.slug}`}
@@ -968,3 +994,34 @@ const EventTypesPage: React.FC & {
};
export default EventTypesPage;
function normalizeEventType(eventType: DeNormalizedEventType): EventType {
return {
...eventType,
userIds: eventType.users.map((user) => user.id),
};
}
function denormalizePayload(data: NonNullable<GetByViewerResponse>) {
return {
...data,
eventTypeGroups: data.eventTypeGroups.map((eventTypeGroup) => {
return {
...eventTypeGroup,
eventTypes: eventTypeGroup.eventTypes.map((eventType) => {
const { userIds, ...rest } = eventType;
return {
...rest,
users: userIds.map((userId) => {
const user = data.allUsersAcrossAllEventTypes.get(userId);
if (!user) {
throw new Error(`User with id ${userId} not found in allUsersAcrossAllEventTypes`);
}
return user;
}),
};
}),
};
}),
};
}
+1 -9
View File
@@ -29,9 +29,6 @@ const userSelect = Prisma.validator<Prisma.UserSelect>()({
avatarUrl: true,
username: true,
id: true,
email: true,
locale: true,
defaultScheduleId: true,
});
export class EventTypeRepository {
@@ -106,12 +103,6 @@ export class EventTypeRepository {
team: {
select: {
id: true,
eventTypes: {
select: {
id: true,
users: { select: userSelect },
},
},
},
},
hashedLink: true,
@@ -175,6 +166,7 @@ export class EventTypeRepository {
return await prisma.eventType.findMany({
where: {
profileId,
...where,
},
select,
orderBy,
@@ -38,9 +38,6 @@ const userSelect = Prisma.validator<Prisma.UserSelect>()({
avatarUrl: true,
username: true,
id: true,
email: true,
locale: true,
defaultScheduleId: true,
});
export class MembershipRepository {
@@ -109,12 +106,6 @@ export class MembershipRepository {
team: {
select: {
id: true,
eventTypes: {
select: {
id: true,
users: { select: userSelect },
},
},
},
},
hashedLink: true,
-16
View File
@@ -337,22 +337,6 @@ export class UserRepository {
};
}
static enrichUserWithItsProfileBuiltFromUser<T extends { id: number; username: string | null }>({
user,
}: {
user: T;
}): T & {
nonProfileUsername: string | null;
profile: UserProfile;
} {
// If no organization profile exists, use the personal profile so that the returned user is normalized to have a profile always
return {
...user,
nonProfileUsername: user.username,
profile: ProfileRepository.buildPersonalProfileFromUser({ user }),
};
}
static async enrichEntityWithProfile<
T extends
| {
+94
View File
@@ -0,0 +1,94 @@
/**
* This script can be used to seed the database with a lot of data for performance testing.
* TODO: Make it more structured and configurable from CLI
* Run it as `npx ts-node --transpile-only ./seed-huge-event-types.ts`
*/
import { createTeamAndAddUsers, createUserAndEventType } from "./seed-utils";
const avatar = "";
const getEventTypes = (numberOfEventTypes: number) => {
const eventTypes = Array<{
title: string;
slug: string;
length: number;
}>(numberOfEventTypes)
.fill({
title: "30min",
slug: "30min",
length: 30,
})
.map((event, index) => {
return {
...event,
title: `30min-${index}`,
slug: `30min-${index}`,
};
});
return eventTypes;
};
async function createTeamsWithEventTypes({
owner,
numberOfTeams,
numberOfEventTypes,
}: {
owner: { id: number; username: string | null };
numberOfTeams: number;
numberOfEventTypes: number;
}) {
const members = [owner];
for (let i = 0; i < 10; i++) {
members.push(
await createUserAndEventType({
user: {
email: `enterprise-member-${i + 1}@example.com`,
name: `Enterprise Member ${i + 1}`,
password: `enterprise-member-${i + 1}`,
username: `enterprise-member-${i + 1}`,
theme: "light",
avatar,
},
})
);
}
for (let i = 0; i < numberOfTeams; i++) {
await createTeamAndAddUsers(
{
name: `Enterprise Team-${i + 1}`,
slug: `enterprise-team-${i + 1}`,
eventTypes: {
createMany: {
data: getEventTypes(numberOfEventTypes).map((eventType) => {
return {
...eventType,
schedulingType: "COLLECTIVE",
};
}),
},
},
createdAt: new Date(),
},
members.map((member) => ({
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
username: member.username!,
id: member.id,
}))
);
}
}
export default async function main() {
const owner = await createUserAndEventType({
user: {
email: `enterprise@example.com`,
name: "Enterprise Owner",
password: "enterprise",
username: `enterprise`,
theme: "light",
avatar,
},
eventTypes: getEventTypes(100),
});
await createTeamsWithEventTypes({ owner, numberOfTeams: 10, numberOfEventTypes: 100 });
}
+82
View File
@@ -1,11 +1,14 @@
import type { Prisma, UserPermissionRole } from "@prisma/client";
import { uuid } from "short-uuid";
import type z from "zod";
import dayjs from "@calcom/dayjs";
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability";
import { MembershipRole } from "@calcom/prisma/enums";
import prisma from ".";
import type { teamMetadataSchema } from "./zod-utils";
export async function createUserAndEventType({
user,
@@ -21,6 +24,7 @@ export async function createUserAndEventType({
timeZone?: string;
role?: UserPermissionRole;
theme?: "dark" | "light";
avatar?: string;
};
eventTypes?: Array<
Prisma.EventTypeUncheckedCreateInput & {
@@ -167,3 +171,81 @@ export async function createUserAndEventType({
}
return theUser;
}
export async function createTeamAndAddUsers(
teamInput: Prisma.TeamCreateInput,
users: { id: number; username: string; role?: MembershipRole }[] = []
) {
const checkUnpublishedTeam = async (slug: string) => {
return await prisma.team.findFirst({
where: {
metadata: {
path: ["requestedSlug"],
equals: slug,
},
},
});
};
const createTeam = async (team: Prisma.TeamCreateInput) => {
try {
const requestedSlug = (team.metadata as z.infer<typeof teamMetadataSchema>)?.requestedSlug;
if (requestedSlug) {
const unpublishedTeam = await checkUnpublishedTeam(requestedSlug);
if (unpublishedTeam) {
throw Error("Unique constraint failed on the fields");
}
}
return await prisma.team.create({
data: {
...team,
},
include: {
eventTypes: true,
},
});
} catch (_err) {
if (_err instanceof Error && _err.message.indexOf("Unique constraint failed on the fields") !== -1) {
console.log(`Team '${team.name}' already exists, skipping.`);
return;
}
throw _err;
}
};
const team = await createTeam(teamInput);
if (!team) {
return;
}
console.log(
`🏢 Created team '${teamInput.name}' - ${process.env.NEXT_PUBLIC_WEBAPP_URL}/team/${team.slug}`
);
for (const eventType of team.eventTypes) {
await prisma.eventType.update({
where: {
id: eventType.id,
},
data: {
users: {
connect: users.map((user) => ({ id: user.id })),
},
},
});
}
for (const user of users) {
const { role = MembershipRole.OWNER, id, username } = user;
await prisma.membership.create({
data: {
teamId: team.id,
userId: id,
role: role,
accepted: true,
},
});
console.log(`\t👤 Added '${teamInput.name}' membership for '${username}' with role '${role}'`);
}
return team;
}
+2
View File
@@ -13,6 +13,7 @@ import type { Ensure } from "@calcom/types/utils";
import prisma from ".";
import mainAppStore from "./seed-app-store";
import mainHugeEventTypesSeed from "./seed-huge-event-types";
import { createUserAndEventType } from "./seed-utils";
import type { teamMetadataSchema } from "./zod-utils";
@@ -913,6 +914,7 @@ async function main() {
main()
.then(() => mainAppStore())
.then(() => mainHugeEventTypesSeed())
.catch((e) => {
console.error(e);
process.exit(1);
@@ -1,16 +1,20 @@
import { Prisma } from "@prisma/client";
// eslint-disable-next-line no-restricted-imports
import { orderBy } from "lodash";
import { hasFilter } from "@calcom/features/filters/lib/hasFilter";
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
import { isOrganization } from "@calcom/lib/entityPermissionUtils";
import { getTeamAvatarUrl, getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
import { getBookerBaseUrlSync } from "@calcom/lib/getBookerUrl/client";
import { getBookerBaseUrl } from "@calcom/lib/getBookerUrl/server";
import logger from "@calcom/lib/logger";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import { safeStringify } from "@calcom/lib/safeStringify";
import { EventTypeRepository } from "@calcom/lib/server/repository/eventType";
import { MembershipRepository } from "@calcom/lib/server/repository/membership";
import { ProfileRepository } from "@calcom/lib/server/repository/profile";
import { UserRepository } from "@calcom/lib/server/repository/user";
import type { PrismaClient } from "@calcom/prisma";
import { baseEventTypeSelect } from "@calcom/prisma";
import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
@@ -20,6 +24,7 @@ import { TRPCError } from "@trpc/server";
import type { TrpcSessionUser } from "../../../trpc";
import type { TEventTypeInputSchema } from "./getByViewer.schema";
const log = logger.getSubLogger({ prefix: ["viewer.eventTypes.getByViewer"] });
type GetByViewerOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
@@ -28,57 +33,6 @@ type GetByViewerOptions = {
input: TEventTypeInputSchema;
};
const userSelect = Prisma.validator<Prisma.UserSelect>()({
id: true,
username: true,
name: true,
organizationId: true,
avatarUrl: true,
});
const userEventTypeSelect = Prisma.validator<Prisma.EventTypeSelect>()({
// Position is required by lodash to sort on it. Don't remove it, TS won't complain but it would silently break reordering
position: true,
hashedLink: true,
destinationCalendar: true,
userId: true,
team: {
select: {
id: true,
name: true,
slug: true,
// logo: true, // Skipping to avoid 4mb limit
bio: true,
hideBranding: true,
},
},
metadata: true,
users: {
select: userSelect,
},
parentId: true,
hosts: {
select: {
user: {
select: userSelect,
},
},
},
seatsPerTimeSlot: true,
...baseEventTypeSelect,
});
const teamEventTypeSelect = Prisma.validator<Prisma.EventTypeSelect>()({
...userEventTypeSelect,
children: {
include: {
users: {
select: userSelect,
},
},
},
});
export const compareMembership = (mship1: MembershipRole, mship2: MembershipRole) => {
const mshipToNumber = (mship: MembershipRole) =>
Object.keys(MembershipRole).findIndex((mmship) => mmship === mship);
@@ -86,87 +40,54 @@ export const compareMembership = (mship1: MembershipRole, mship2: MembershipRole
};
export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) => {
const { prisma } = ctx;
await checkRateLimitAndThrowError({
identifier: `eventTypes:getByViewer:${ctx.user.id}`,
rateLimitingType: "common",
});
const lightProfile = ctx.user.profile;
const unenrichedUser = await prisma.user.findUnique({
where: {
id: ctx.user.id,
},
select: {
id: true,
username: true,
name: true,
startTime: true,
endTime: true,
bufferTime: true,
avatar: true,
avatarUrl: true,
organizationId: true,
teams: {
const profile = await ProfileRepository.findByUpId(lightProfile.upId);
const isFilterSet = input?.filters && hasFilter(input.filters);
const isUpIdInFilter = input?.filters?.upIds?.includes(lightProfile.upId);
const shouldListUserEvents = !isFilterSet || isUpIdInFilter;
const [profileMemberships, profileEventTypes] = await Promise.all([
MembershipRepository.findAllByUpIdIncludeTeamWithMembersAndEventTypes(
{
upId: lightProfile.upId,
},
{
where: {
accepted: true,
},
select: {
role: true,
team: {
select: {
id: true,
name: true,
slug: true,
parentId: true,
metadata: true,
parent: true,
members: {
select: {
userId: true,
},
},
eventTypes: {
select: teamEventTypeSelect,
orderBy: [
{
position: "desc",
},
{
id: "asc",
},
],
},
}
),
shouldListUserEvents
? EventTypeRepository.findAllByUpId(
{
upId: lightProfile.upId,
},
{
where: {
teamId: null,
},
},
},
},
eventTypes: {
where: {
teamId: null,
userId: getPrismaWhereUserIdFromFilter(ctx.user.id, input?.filters),
},
select: {
...userEventTypeSelect,
},
orderBy: [
{
position: "desc",
},
{
id: "asc",
},
],
},
},
});
orderBy: [
{
position: "desc",
},
{
id: "asc",
},
],
}
)
: [],
]);
if (!unenrichedUser) {
if (!profile) {
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
}
const user = UserRepository.enrichUserWithItsProfileBuiltFromUser({ user: unenrichedUser });
const memberships = user.teams.map((membership) => ({
const memberships = profileMemberships.map((membership) => ({
...membership,
team: {
...membership.team,
@@ -174,26 +95,43 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) =>
},
}));
type UserEventTypes = (typeof user.eventTypes)[number];
type TeamEventTypeChildren = (typeof user.teams)[number]["team"]["eventTypes"][number];
log.debug(
safeStringify({
profileMemberships,
profileEventTypes,
})
);
const mapEventType = (eventType: UserEventTypes & Partial<TeamEventTypeChildren>) => {
const users = !!eventType?.hosts?.length ? eventType?.hosts.map((host) => host.user) : eventType.users;
return {
...eventType,
safeDescription: eventType?.description ? markdownToSafeHTML(eventType.description) : undefined,
users: users.map((user) => UserRepository.enrichUserWithItsProfileBuiltFromUser({ user })),
metadata: eventType.metadata ? EventTypeMetaDataSchema.parse(eventType.metadata) : null,
children: eventType.children?.map((child) => {
return {
...child,
users: child.users.map((user) => UserRepository.enrichUserWithItsProfileBuiltFromUser({ user })),
};
}),
};
};
type UserEventTypes = (typeof profileEventTypes)[number];
const userEventTypes = user.eventTypes.map(mapEventType);
const mapEventType = async (eventType: UserEventTypes) => ({
...eventType,
safeDescription: eventType?.description ? markdownToSafeHTML(eventType.description) : undefined,
users: await Promise.all(
(!!eventType?.hosts?.length ? eventType?.hosts.map((host) => host.user) : eventType.users).map(
async (u) =>
await UserRepository.enrichUserWithItsProfile({
user: u,
})
)
),
metadata: eventType.metadata ? EventTypeMetaDataSchema.parse(eventType.metadata) : null,
children: await Promise.all(
(eventType.children || []).map(async (c) => ({
...c,
users: await Promise.all(
c.users.map(
async (u) =>
await UserRepository.enrichUserWithItsProfile({
user: u,
})
)
),
}))
),
});
const userEventTypes = await Promise.all(profileEventTypes.map(mapEventType));
type EventTypeGroup = {
teamId?: number | null;
@@ -201,8 +139,8 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) =>
bookerUrl: string;
membershipRole?: MembershipRole | null;
profile: {
slug: (typeof user)["username"];
name: (typeof user)["name"];
slug: (typeof profile)["username"] | null;
name: (typeof profile)["name"];
image: string;
};
metadata: {
@@ -218,16 +156,30 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) =>
(evType) => evType.schedulingType !== SchedulingType.MANAGED
);
if (!input?.filters || !hasFilter(input?.filters) || input?.filters?.userIds?.includes(user.id)) {
const bookerUrl = await getBookerBaseUrl(user.profile.organizationId);
log.debug(safeStringify({ profileMemberships, profileEventTypes, profile }));
log.debug(
"Filter Settings",
safeStringify({
isFilterSet,
isProfileIdInFilter: isUpIdInFilter,
})
);
if (!isFilterSet || isUpIdInFilter) {
const bookerUrl = await getBookerBaseUrl(profile.organizationId ?? null);
eventTypeGroups.push({
teamId: null,
bookerUrl,
membershipRole: null,
profile: {
slug: user.username,
name: user.name,
image: getUserAvatarUrl(user),
slug: profile.username,
name: profile.name,
image: getUserAvatarUrl({
username: profile.username,
avatarUrl: profile.avatarUrl,
profile: profile,
}),
},
eventTypes: orderBy(unmanagedEventTypes, ["position", "id"], ["desc", "asc"]),
metadata: {
@@ -237,12 +189,12 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) =>
});
}
const teamMemberships = user.teams.map((membership) => ({
const teamMemberships = profileMemberships.map((membership) => ({
teamId: membership.team.id,
membershipRole: membership.role,
}));
const filterTeamsEventTypesBasedOnInput = (eventType: ReturnType<typeof mapEventType>) => {
const filterTeamsEventTypesBasedOnInput = async (eventType: Awaited<ReturnType<typeof mapEventType>>) => {
if (!input?.filters || !hasFilter(input?.filters)) {
return true;
}
@@ -250,79 +202,84 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) =>
};
eventTypeGroups = ([] as EventTypeGroup[]).concat(
eventTypeGroups,
memberships
.filter((mmship) => {
const metadata = mmship.team.metadata;
if (metadata?.isOrganization) {
return false;
} else {
if (!input?.filters || !hasFilter(input?.filters)) {
return true;
await Promise.all(
memberships
.filter((mmship) => {
if (isOrganization({ team: mmship.team })) {
return false;
} else {
if (!input?.filters || !hasFilter(input?.filters)) {
return true;
}
return input?.filters?.teamIds?.includes(mmship?.team?.id || 0) ?? false;
}
return input?.filters?.teamIds?.includes(mmship?.team?.id || 0) ?? false;
}
})
.map((membership) => {
const orgMembership = teamMemberships.find(
(teamM) => teamM.teamId === membership.team.parentId
)?.membershipRole;
})
.map(async (membership) => {
const orgMembership = teamMemberships.find(
(teamM) => teamM.teamId === membership.team.parentId
)?.membershipRole;
const team = {
...membership.team,
metadata: teamMetadataSchema.parse(membership.team.metadata),
};
const team = {
...membership.team,
metadata: teamMetadataSchema.parse(membership.team.metadata),
};
let slug;
let slug;
if (input?.forRoutingForms) {
// For Routing form we want to ensure that after migration of team to an org, the URL remains same for the team
// Once we solve this https://github.com/calcom/cal.com/issues/12399, we can remove this conditional change in slug
slug = `team/${team.slug}`;
} else {
// In an Org, a team can be accessed without /team prefix as well as with /team prefix
slug = team.slug ? (!team.parentId ? `team/${team.slug}` : `${team.slug}`) : null;
}
return {
teamId: team.id,
parentId: team.parentId,
bookerUrl: getBookerBaseUrlSync(team.parent?.slug ?? null),
membershipRole:
orgMembership && compareMembership(orgMembership, membership.role)
? orgMembership
: membership.role,
profile: {
image: getTeamAvatarUrl({
slug: team.slug,
requestedSlug: team.metadata?.requestedSlug ?? null,
organizationId: team.parentId,
}),
name: team.name,
slug,
},
metadata: {
membershipCount: team.members.length,
readOnly:
membership.role ===
(team.parentId
? orgMembership && compareMembership(orgMembership, membership.role)
? orgMembership
: MembershipRole.MEMBER
: MembershipRole.MEMBER),
},
eventTypes: team.eventTypes
.map(mapEventType)
.filter(filterTeamsEventTypesBasedOnInput)
.filter((evType) => evType.userId === null || evType.userId === ctx.user.id)
.filter((evType) =>
membership.role === MembershipRole.MEMBER
? evType.schedulingType !== SchedulingType.MANAGED
: true
),
};
})
if (input?.forRoutingForms) {
// For Routing form we want to ensure that after migration of team to an org, the URL remains same for the team
// Once we solve this https://github.com/calcom/cal.com/issues/12399, we can remove this conditional change in slug
slug = `team/${team.slug}`;
} else {
// In an Org, a team can be accessed without /team prefix as well as with /team prefix
slug = team.slug ? (!team.parentId ? `team/${team.slug}` : `${team.slug}`) : null;
}
const eventTypes = await Promise.all(team.eventTypes.map(mapEventType));
return {
teamId: team.id,
parentId: team.parentId,
bookerUrl: getBookerBaseUrlSync(team.parent?.slug ?? null),
membershipRole:
orgMembership && compareMembership(orgMembership, membership.role)
? orgMembership
: membership.role,
profile: {
image: getTeamAvatarUrl({
slug: team.slug,
requestedSlug: team.metadata?.requestedSlug ?? null,
organizationId: team.parentId,
}),
name: team.name,
slug,
},
metadata: {
membershipCount: team.members.length,
readOnly:
membership.role ===
(team.parentId
? orgMembership && compareMembership(orgMembership, membership.role)
? orgMembership
: MembershipRole.MEMBER
: MembershipRole.MEMBER),
},
eventTypes: eventTypes
.filter(filterTeamsEventTypesBasedOnInput)
.filter((evType) => {
const res = evType.userId === null || evType.userId === ctx.user.id;
return res;
})
.filter((evType) =>
membership.role === MembershipRole.MEMBER
? evType.schedulingType !== SchedulingType.MANAGED
: true
),
};
})
)
);
return {
const denormalizedPayload = {
eventTypeGroups,
// so we can show a dropdown when the user has teams
profiles: eventTypeGroups.map((group) => ({
@@ -332,16 +289,38 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) =>
membershipRole: group.membershipRole,
})),
};
};
export function getPrismaWhereUserIdFromFilter(
userId: number,
filters: NonNullable<TEventTypeInputSchema>["filters"] | undefined
) {
if (!filters || !hasFilter(filters)) {
return userId;
} else if (filters.userIds?.[0] === userId) {
return userId;
return normalizePayload(denormalizedPayload);
/**
* Reduces the size of payload
*/
function normalizePayload(payload: typeof denormalizedPayload) {
const allUsersAcrossAllEventTypes = new Map<
number,
EventTypeGroup["eventTypes"][number]["users"][number]
>();
const eventTypeGroups = payload.eventTypeGroups.map((group) => {
return {
...group,
eventTypes: group.eventTypes.map((eventType) => {
const { users, ...rest } = eventType;
return {
...rest,
// Send userIds per event and keep the actual users object outside
userIds: users.map((user) => {
allUsersAcrossAllEventTypes.set(user.id, user);
return user.id;
}),
};
}),
};
});
return {
...payload,
allUsersAcrossAllEventTypes,
eventTypeGroups,
};
}
return 0;
}
};
@@ -3,7 +3,7 @@ import { z } from "zod";
export const filterQuerySchemaStrict = z.object({
teamIds: z.number().array().optional(),
// A user can only filter by only his userId
userIds: z.number().array().max(1).optional(),
upIds: z.string().array().max(1).optional(),
});
export const ZEventTypeInputSchema = z