perf: get event types (#17689)

* refactor: get event types

* chore

* test: add team and managed team event types

* perf: use Promise.all

* fix: type err
This commit is contained in:
Udit Takkar
2024-11-18 11:05:12 -03:00
committed by GitHub
parent fb4cd7f0e4
commit ecac331fd1
2 changed files with 192 additions and 105 deletions
@@ -1,7 +1,8 @@
import type { Prisma } from "@prisma/client";
import { hasFilter } from "@calcom/features/filters/lib/hasFilter";
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { EventTypeRepository } from "@calcom/lib/server/repository/eventType";
import { prisma } from "@calcom/prisma";
import type { PrismaClient } from "@calcom/prisma";
@@ -37,7 +38,7 @@ export const getEventTypesFromGroup = async ({
const userProfile = ctx.user.profile;
const { group, limit, cursor, filters, searchQuery } = input;
const { teamId } = group;
const { teamId, parentId } = group;
const isFilterSet = (filters && hasFilter(filters)) || !!teamId;
const isUpIdInFilter = filters?.upIds?.includes(userProfile.upId);
@@ -45,56 +46,25 @@ export const getEventTypesFromGroup = async ({
const shouldListUserEvents =
!isFilterSet || isUpIdInFilter || (isFilterSet && filters?.upIds && !isUpIdInFilter);
const eventTypes: MappedEventType[] = [];
let paginationCursor = cursor;
let hasMoreResults = true;
let isFirstFetch = true;
const fetchAndFilterEventTypes = async () => {
const batch = await fetchEventTypesBatch(ctx, input, shouldListUserEvents, paginationCursor, searchQuery);
const filteredBatch = await filterEventTypes(batch.eventTypes, ctx.user.id, shouldListUserEvents, teamId);
eventTypes.push(...filteredBatch);
paginationCursor = batch.nextCursor;
hasMoreResults = !!batch.nextCursor;
};
while (eventTypes.length < limit && (hasMoreResults || isFirstFetch)) {
await fetchAndFilterEventTypes();
isFirstFetch = false;
}
return {
eventTypes,
nextCursor: paginationCursor ?? undefined,
};
};
const fetchEventTypesBatch = async (
ctx: GetByViewerOptions["ctx"],
input: GetByViewerOptions["input"],
shouldListUserEvents: boolean | undefined,
cursor: TGetEventTypesFromGroupSchema["cursor"],
searchQuery: TGetEventTypesFromGroupSchema["searchQuery"]
) => {
const userProfile = ctx.user.profile;
const { group, limit, filters } = input;
const { teamId, parentId } = group;
const isFilterSet = (filters && hasFilter(filters)) || !!teamId;
const eventTypes: EventType[] = [];
if (shouldListUserEvents || !teamId) {
const userEventTypes =
(await EventTypeRepository.findAllByUpId(
const baseQueryConditions = {
teamId: null,
schedulingType: null,
...(searchQuery ? { title: { contains: searchQuery, mode: "insensitive" as Prisma.QueryMode } } : {}),
};
const [nonChildEventTypes, childEventTypes] = await Promise.all([
EventTypeRepository.findAllByUpId(
{
upId: userProfile.upId,
userId: ctx.user.id,
},
{
where: {
teamId: null,
schedulingType: null,
...(searchQuery ? { title: { contains: searchQuery, mode: "insensitive" } } : {}),
...baseQueryConditions,
parentId: null,
},
orderBy: [
{
@@ -107,7 +77,40 @@ const fetchEventTypesBatch = async (
limit,
cursor,
}
)) ?? [];
),
EventTypeRepository.findAllByUpId(
{
upId: userProfile.upId,
userId: ctx.user.id,
},
{
where: {
...baseQueryConditions,
parentId: { not: null },
userId: ctx.user.id,
},
orderBy: [
{
position: "desc",
},
{
id: "asc",
},
],
limit,
cursor,
}
),
]);
const userEventTypes = [...(nonChildEventTypes ?? []), ...(childEventTypes ?? [])].sort((a, b) => {
// First sort by position in descending order
if (a.position !== b.position) {
return b.position - a.position;
}
// Then by id in ascending order
return a.id - b.id;
});
eventTypes.push(...userEventTypes);
}
@@ -147,48 +150,11 @@ const fetchEventTypesBatch = async (
nextCursor = nextItem?.id;
}
const mappedEventTypes = await Promise.all(eventTypes.map(mapEventType));
log.info(
"fetchEventTypesBatch",
safeStringify({
mappedEventTypes,
})
);
return { eventTypes: mappedEventTypes, nextCursor: nextCursor ?? undefined };
};
const filterEventTypes = async (
eventTypes: MappedEventType[],
userId: number,
shouldListUserEvents: boolean | undefined,
teamId: number | null | undefined
) => {
const filteredEventTypes = eventTypes.filter((eventType) => {
if (!eventType.parentId) {
return true;
}
// A child event only has one user
const childEventAssignee = eventType.users[0];
if (!childEventAssignee || childEventAssignee.id !== userId) {
return false;
}
return true;
});
log.info(
"mappedEventTypes before and after filtering",
safeStringify({
beforeFiltering: eventTypes,
afterFiltering: filteredEventTypes,
})
);
const mappedEventTypes: MappedEventType[] = await Promise.all(eventTypes.map(mapEventType));
const membership = await prisma.membership.findFirst({
where: {
userId,
userId: ctx.user.id,
teamId: teamId ?? 0,
accepted: true,
role: "MEMBER",
@@ -203,17 +169,10 @@ const filterEventTypes = async (
});
if (membership && membership.team.isPrivate)
filteredEventTypes.forEach((evType) => {
mappedEventTypes.forEach((evType) => {
evType.users = [];
evType.hosts = [];
});
log.info(
"filteredEventTypes",
safeStringify({
filteredEventTypes,
})
);
return filteredEventTypes;
return { eventTypes: mappedEventTypes, nextCursor: nextCursor ?? undefined };
};
@@ -1,6 +1,7 @@
import { describe, it, expect } from "vitest";
import prisma from "@calcom/prisma";
import { SchedulingType } from "@calcom/prisma/enums";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import { getEventTypesFromGroup } from "./getEventTypesFromGroup.handler";
@@ -9,22 +10,88 @@ describe("getEventTypesFromGroup", async () => {
const proUser = await prisma.user.findFirstOrThrow({ where: { email: "pro@example.com" } });
const proUserEventTypes = await prisma.eventType.findMany({ where: { userId: proUser.id } });
it("should return personal event types for a user", async () => {
const ctx = {
user: {
id: proUser.id,
const teamProUser = await prisma.user.findFirstOrThrow({ where: { email: "teampro@example.com" } });
const teamProMembership = await prisma.membership.findFirstOrThrow({
where: { userId: teamProUser.id, accepted: true },
});
const teamId = teamProMembership.teamId;
const proUserCtx = {
user: {
id: proUser.id,
name: proUser.name,
profile: {
name: proUser.name,
profile: {
name: proUser.name,
organizationId: null,
organization: null,
username: proUser.username,
id: null,
upId: "usr-4",
organizationId: null,
organization: null,
username: proUser.username,
id: null,
upId: "usr-4",
},
} as NonNullable<TrpcSessionUser>,
prisma,
};
const teamProUserCtx = {
user: {
id: teamProUser.id,
name: teamProUser.name,
profile: {
name: teamProUser.name,
organizationId: null,
organization: null,
username: teamProUser.username,
id: null,
upId: "usr-9",
},
} as NonNullable<TrpcSessionUser>,
prisma,
};
const createManagedEventTypes = async () => {
const childEventType = await prisma.eventType.create({
data: {
title: "Child Event Type",
slug: "managed-event-type",
schedulingType: null,
length: 30,
userId: teamProUser.id,
},
});
const parentEventType = await prisma.eventType.create({
data: {
title: "Managed Event Type",
slug: "managed-event-type",
schedulingType: SchedulingType.MANAGED,
length: 30,
teamId,
},
});
// Connect child event type to parent event type
await prisma.eventType.update({
where: {
id: childEventType.id,
},
data: {
parent: {
connect: {
id: parentEventType.id,
},
},
} as NonNullable<TrpcSessionUser>,
prisma,
},
});
return {
parentEventType,
childEventType,
};
};
it("should return personal event types for a user", async () => {
const ctx = proUserCtx;
const res = await getEventTypesFromGroup({
ctx,
@@ -46,4 +113,65 @@ describe("getEventTypesFromGroup", async () => {
expect(resEventTypeIds).toEqual(expect.arrayContaining(proUserEventTypeIds));
expect(resEventTypeIds.length).toBe(proUserEventTypeIds.length);
});
it("should return team event types for a user", async () => {
const response = await getEventTypesFromGroup({
ctx: teamProUserCtx,
input: {
group: {
teamId,
parentId: null,
},
limit: 10,
cursor: null,
},
});
const resEventTypeIds = response.eventTypes.map((et) => et.id);
const seededTeamEventTypes = await prisma.eventType.findMany({ where: { teamId } });
const teamProUserEventTypeIds = seededTeamEventTypes.map((et) => et.id);
expect(response.eventTypes).toBeDefined();
expect(response.eventTypes.length).toBeGreaterThan(0);
expect(resEventTypeIds).toEqual(expect.arrayContaining(teamProUserEventTypeIds));
expect(resEventTypeIds.length).toBe(teamProUserEventTypeIds.length);
});
it("should return managed event types event types", async () => {
const { parentEventType, childEventType } = await createManagedEventTypes();
const res = await getEventTypesFromGroup({
ctx: teamProUserCtx,
input: {
group: {
teamId: null,
parentId: null,
},
limit: 10,
cursor: null,
},
});
const resEventTypeIds = res.eventTypes.map((et) => et.id);
const managedEventType = await prisma.eventType.findFirstOrThrow({
where: {
parentId: {
not: null,
},
schedulingType: null,
userId: teamProUser.id,
},
});
expect(res.eventTypes).toBeDefined();
expect(resEventTypeIds).toContain(managedEventType.id);
await deleteEventTypes({ ids: [parentEventType.id, childEventType.id] });
});
});
const deleteEventTypes = async ({ ids }: { ids: number[] }) => {
await prisma.eventType.deleteMany({ where: { id: { in: ids } } });
};