diff --git a/apps/web/modules/event-types/views/event-types-listing-view.tsx b/apps/web/modules/event-types/views/event-types-listing-view.tsx
index 0000969f14..90127e1d3a 100644
--- a/apps/web/modules/event-types/views/event-types-listing-view.tsx
+++ b/apps/web/modules/event-types/views/event-types-listing-view.tsx
@@ -811,7 +811,7 @@ const CTA = ({ data }: { data: GetByViewerResponse }) => {
const Actions = () => {
return (
-
+
);
};
diff --git a/packages/lib/server/repository/user.ts b/packages/lib/server/repository/user.ts
index 9b7195e8fa..dacfe01be6 100644
--- a/packages/lib/server/repository/user.ts
+++ b/packages/lib/server/repository/user.ts
@@ -337,6 +337,22 @@ export class UserRepository {
};
}
+ static enrichUserWithItsProfileBuiltFromUser({
+ 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
| {
diff --git a/packages/trpc/server/routers/viewer/eventTypes/getByViewer.handler.ts b/packages/trpc/server/routers/viewer/eventTypes/getByViewer.handler.ts
index d0422a96d8..13f5fe1af6 100644
--- a/packages/trpc/server/routers/viewer/eventTypes/getByViewer.handler.ts
+++ b/packages/trpc/server/routers/viewer/eventTypes/getByViewer.handler.ts
@@ -1,20 +1,16 @@
+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";
@@ -24,7 +20,6 @@ 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;
@@ -33,6 +28,57 @@ type GetByViewerOptions = {
input: TEventTypeInputSchema;
};
+const userSelect = Prisma.validator()({
+ id: true,
+ username: true,
+ name: true,
+ organizationId: true,
+ avatarUrl: true,
+});
+
+const userEventTypeSelect = Prisma.validator()({
+ // 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()({
+ ...userEventTypeSelect,
+ children: {
+ include: {
+ users: {
+ select: userSelect,
+ },
+ },
+ },
+});
+
export const compareMembership = (mship1: MembershipRole, mship2: MembershipRole) => {
const mshipToNumber = (mship: MembershipRole) =>
Object.keys(MembershipRole).findIndex((mmship) => mmship === mship);
@@ -40,54 +86,87 @@ 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 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,
- },
- {
+ 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: {
where: {
accepted: true,
},
- }
- ),
- shouldListUserEvents
- ? EventTypeRepository.findAllByUpId(
+ 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",
+ },
+ ],
+ },
+ },
+ },
+ },
+ },
+ eventTypes: {
+ where: {
+ teamId: null,
+ userId: getPrismaWhereUserIdFromFilter(ctx.user.id, input?.filters),
+ },
+ select: {
+ ...userEventTypeSelect,
+ },
+ orderBy: [
{
- upId: lightProfile.upId,
+ position: "desc",
},
{
- where: {
- teamId: null,
- },
- orderBy: [
- {
- position: "desc",
- },
- {
- id: "asc",
- },
- ],
- }
- )
- : [],
- ]);
+ id: "asc",
+ },
+ ],
+ },
+ },
+ });
- if (!profile) {
+ if (!unenrichedUser) {
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
}
+ const user = UserRepository.enrichUserWithItsProfileBuiltFromUser({ user: unenrichedUser });
- const memberships = profileMemberships.map((membership) => ({
+ const memberships = user.teams.map((membership) => ({
...membership,
team: {
...membership.team,
@@ -95,43 +174,26 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) =>
},
}));
- log.debug(
- safeStringify({
- profileMemberships,
- profileEventTypes,
- })
- );
+ type UserEventTypes = (typeof user.eventTypes)[number];
+ type TeamEventTypeChildren = (typeof user.teams)[number]["team"]["eventTypes"][number];
- type UserEventTypes = (typeof profileEventTypes)[number];
- type TeamEventTypeChildren = NonNullable<(typeof profileEventTypes)[number]["team"]>["eventTypes"][number];
- const mapEventType = async (eventType: UserEventTypes & Partial) => ({
- ...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 mapEventType = (eventType: UserEventTypes & Partial) => {
+ 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 })),
+ };
+ }),
+ };
+ };
- const userEventTypes = await Promise.all(profileEventTypes.map(mapEventType));
+ const userEventTypes = user.eventTypes.map(mapEventType);
type EventTypeGroup = {
teamId?: number | null;
@@ -139,8 +201,8 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) =>
bookerUrl: string;
membershipRole?: MembershipRole | null;
profile: {
- slug: (typeof profile)["username"] | null;
- name: (typeof profile)["name"];
+ slug: (typeof user)["username"];
+ name: (typeof user)["name"];
image: string;
};
metadata: {
@@ -156,30 +218,16 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) =>
(evType) => evType.schedulingType !== SchedulingType.MANAGED
);
- log.debug(safeStringify({ profileMemberships, profileEventTypes, profile }));
-
- log.debug(
- "Filter Settings",
- safeStringify({
- isFilterSet,
- isProfileIdInFilter: isUpIdInFilter,
- })
- );
-
- if (!isFilterSet || isUpIdInFilter) {
- const bookerUrl = await getBookerBaseUrl(profile.organizationId ?? null);
+ if (!input?.filters || !hasFilter(input?.filters) || input?.filters?.userIds?.includes(user.id)) {
+ const bookerUrl = await getBookerBaseUrl(user.profile.organizationId);
eventTypeGroups.push({
teamId: null,
bookerUrl,
membershipRole: null,
profile: {
- slug: profile.username,
- name: profile.name,
- image: getUserAvatarUrl({
- username: profile.username,
- avatarUrl: profile.avatarUrl,
- profile: profile,
- }),
+ slug: user.username,
+ name: user.name,
+ image: getUserAvatarUrl(user),
},
eventTypes: orderBy(unmanagedEventTypes, ["position", "id"], ["desc", "asc"]),
metadata: {
@@ -189,12 +237,12 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) =>
});
}
- const teamMemberships = profileMemberships.map((membership) => ({
+ const teamMemberships = user.teams.map((membership) => ({
teamId: membership.team.id,
membershipRole: membership.role,
}));
- const filterTeamsEventTypesBasedOnInput = async (eventType: Awaited>) => {
+ const filterTeamsEventTypesBasedOnInput = (eventType: ReturnType) => {
if (!input?.filters || !hasFilter(input?.filters)) {
return true;
}
@@ -202,81 +250,76 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) =>
};
eventTypeGroups = ([] as EventTypeGroup[]).concat(
eventTypeGroups,
- 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;
+ memberships
+ .filter((mmship) => {
+ const metadata = mmship.team.metadata;
+ if (metadata?.isOrganization) {
+ return false;
+ } else {
+ if (!input?.filters || !hasFilter(input?.filters)) {
+ return true;
}
- })
- .map(async (membership) => {
- const orgMembership = teamMemberships.find(
- (teamM) => teamM.teamId === membership.team.parentId
- )?.membershipRole;
+ return input?.filters?.teamIds?.includes(mmship?.team?.id || 0) ?? false;
+ }
+ })
+ .map((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;
- }
-
- 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
- ),
- };
- })
- )
+ 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
+ ),
+ };
+ })
);
return {
@@ -290,3 +333,15 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) =>
})),
};
};
+
+export function getPrismaWhereUserIdFromFilter(
+ userId: number,
+ filters: NonNullable["filters"] | undefined
+) {
+ if (!filters || !hasFilter(filters)) {
+ return userId;
+ } else if (filters.userIds?.[0] === userId) {
+ return userId;
+ }
+ return 0;
+}
diff --git a/packages/trpc/server/routers/viewer/eventTypes/getByViewer.schema.ts b/packages/trpc/server/routers/viewer/eventTypes/getByViewer.schema.ts
index 082b5df9dd..b7a7424552 100644
--- a/packages/trpc/server/routers/viewer/eventTypes/getByViewer.schema.ts
+++ b/packages/trpc/server/routers/viewer/eventTypes/getByViewer.schema.ts
@@ -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
- upIds: z.string().array().max(1).optional(),
+ userIds: z.number().array().max(1).optional(),
});
export const ZEventTypeInputSchema = z