Revert event-type profile based querying (#13588)

This commit is contained in:
Hariom Balhara
2024-02-08 09:40:17 -03:00
committed by GitHub
parent 3348e7be2f
commit c0c40185fc
4 changed files with 242 additions and 171 deletions
@@ -811,7 +811,7 @@ const CTA = ({ data }: { data: GetByViewerResponse }) => {
const Actions = () => {
return (
<div className="hidden items-center md:flex">
<TeamsFilter useProfileFilter popoverTriggerClassNames="mb-0" showVerticalDivider={true} />
<TeamsFilter popoverTriggerClassNames="mb-0" showVerticalDivider={true} />
</div>
);
};
+16
View File
@@ -337,6 +337,22 @@ 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
| {
@@ -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<TrpcSessionUser>;
@@ -33,6 +28,57 @@ 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);
@@ -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<TeamEventTypeChildren>) => ({
...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<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 })),
};
}),
};
};
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<ReturnType<typeof mapEventType>>) => {
const filterTeamsEventTypesBasedOnInput = (eventType: ReturnType<typeof mapEventType>) => {
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<TEventTypeInputSchema>["filters"] | undefined
) {
if (!filters || !hasFilter(filters)) {
return userId;
} else if (filters.userIds?.[0] === userId) {
return userId;
}
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
upIds: z.string().array().max(1).optional(),
userIds: z.number().array().max(1).optional(),
});
export const ZEventTypeInputSchema = z