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:
@@ -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;
|
||||
}),
|
||||
};
|
||||
}),
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
| {
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user