diff --git a/packages/trpc/server/routers/viewer/eventTypes/getEventTypesFromGroup.handler.ts b/packages/trpc/server/routers/viewer/eventTypes/getEventTypesFromGroup.handler.ts index 4fd9313563..0709c361ab 100644 --- a/packages/trpc/server/routers/viewer/eventTypes/getEventTypesFromGroup.handler.ts +++ b/packages/trpc/server/routers/viewer/eventTypes/getEventTypesFromGroup.handler.ts @@ -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 }; }; diff --git a/packages/trpc/server/routers/viewer/eventTypes/getEventTypesFromGroup.integration-test.ts b/packages/trpc/server/routers/viewer/eventTypes/getEventTypesFromGroup.integration-test.ts index 049ec52e0a..53193710ca 100644 --- a/packages/trpc/server/routers/viewer/eventTypes/getEventTypesFromGroup.integration-test.ts +++ b/packages/trpc/server/routers/viewer/eventTypes/getEventTypesFromGroup.integration-test.ts @@ -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, + 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, + 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, - 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 } } }); +};