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