From f1d7d7f02a3264ea3297eadaf6b65de70b115352 Mon Sep 17 00:00:00 2001 From: Anirban Singha <143536290+SinghaAnirban005@users.noreply.github.com> Date: Thu, 28 Aug 2025 15:26:58 +0530 Subject: [PATCH] fix: Missing personal event types in All filter (#23343) * fix: Missing personal event types in All filter Signed-off-by: Anirban Singha * chore: coderabbit suggestion Signed-off-by: Anirban Singha * chore: export function * chore: Add test file Signed-off-by: Anirban Singha --------- Signed-off-by: Anirban Singha Co-authored-by: Kartik Saini <41051387+kart1ka@users.noreply.github.com> --- .../server/__tests__/trpc-router.test.ts | 199 ++++++++++++++++++ .../features/insights/server/trpc-router.ts | 103 +++++---- 2 files changed, 264 insertions(+), 38 deletions(-) create mode 100644 packages/features/insights/server/__tests__/trpc-router.test.ts diff --git a/packages/features/insights/server/__tests__/trpc-router.test.ts b/packages/features/insights/server/__tests__/trpc-router.test.ts new file mode 100644 index 0000000000..b01a4d92e6 --- /dev/null +++ b/packages/features/insights/server/__tests__/trpc-router.test.ts @@ -0,0 +1,199 @@ +import { describe, expect, it, vi, beforeEach } from "vitest"; + +import { readonlyPrisma } from "@calcom/prisma"; + +import { getEventTypeList } from "../trpc-router"; + +vi.mock("@calcom/prisma", () => ({ + readonlyPrisma: { + eventType: { + findMany: vi.fn(), + }, + team: { + findMany: vi.fn(), + }, + membership: { + findFirst: vi.fn(), + }, + }, +})); + +describe("getEventTypeList", () => { + beforeEach(() => { + vi.resetAllMocks(); + }); + + const mockUser = { + id: 1, + organizationId: 10, + isOwnerAdminOfParentTeam: false, + }; + + const mockEventTypes = [ + { + id: 1, + slug: "personal-event", + title: "Personal Event", + teamId: null, + userId: 1, + team: null, + }, + { + id: 2, + slug: "team-event", + title: "Team Event", + teamId: 5, + userId: null, + team: { name: "Team A" }, + }, + ]; + + describe("Early return scenarios", () => { + it("should return empty array when no teamId, userId, or isAll provided", async () => { + const result = await getEventTypeList({ + prisma: readonlyPrisma, + teamId: null, + userId: null, + isAll: false, + user: mockUser, + }); + + expect(result).toEqual([]); + expect(readonlyPrisma.eventType.findMany).not.toHaveBeenCalled(); + }); + }); + + describe("Personal events filtering", () => { + it("should return only user's personal events when userId provided", async () => { + const personalEvents = [mockEventTypes[0]]; + vi.mocked(readonlyPrisma.eventType.findMany).mockResolvedValue(personalEvents); + + const result = await getEventTypeList({ + prisma: readonlyPrisma, + teamId: null, + userId: 1, + isAll: false, + user: mockUser, + }); + + expect(readonlyPrisma.eventType.findMany).toHaveBeenCalledWith({ + select: { + id: true, + slug: true, + title: true, + teamId: true, + userId: true, + team: { + select: { + name: true, + }, + }, + }, + where: { + userId: mockUser.id, + teamId: null, + }, + }); + expect(result).toEqual(personalEvents); + }); + }); + + describe("Organization-wide view (isAll = true)", () => { + it("should return team events and user's personal events for owner/admin", async () => { + const childTeams = [{ id: 11 }, { id: 12 }]; + const allEvents = [...mockEventTypes]; + + vi.mocked(readonlyPrisma.team.findMany).mockResolvedValue(childTeams); + vi.mocked(readonlyPrisma.eventType.findMany).mockResolvedValue(allEvents); + + const ownerUser = { ...mockUser, isOwnerAdminOfParentTeam: true }; + + const result = await getEventTypeList({ + prisma: readonlyPrisma, + teamId: null, + userId: null, + isAll: true, + user: ownerUser, + }); + + expect(readonlyPrisma.eventType.findMany).toHaveBeenCalledWith({ + select: { + id: true, + slug: true, + title: true, + teamId: true, + userId: true, + team: { + select: { + name: true, + }, + }, + }, + where: { + OR: [ + { + teamId: { + in: [10, 11, 12], + }, + }, + { + userId: ownerUser.id, + teamId: null, + }, + ], + }, + }); + expect(result).toEqual(allEvents); + }); + }); + + describe("Team-specific view", () => { + it("should return team events for team members", async () => { + const membership = { teamId: 5, userId: 1, role: "MEMBER" }; + vi.mocked(readonlyPrisma.membership.findFirst).mockResolvedValue(membership); + vi.mocked(readonlyPrisma.eventType.findMany).mockResolvedValue([mockEventTypes[1]]); + + const result = await getEventTypeList({ + prisma: readonlyPrisma, + teamId: 5, + userId: null, + isAll: false, + user: mockUser, + }); + + expect(readonlyPrisma.eventType.findMany).toHaveBeenCalledWith({ + select: { + id: true, + slug: true, + title: true, + teamId: true, + userId: true, + team: { + select: { + name: true, + }, + }, + }, + where: { + teamId: 5, + OR: [{ userId: mockUser.id }, { users: { some: { id: mockUser.id } } }], + }, + }); + expect(result).toEqual([mockEventTypes[1]]); + }); + + it("should throw error when user is not part of team and not owner/admin", async () => { + vi.mocked(readonlyPrisma.membership.findFirst).mockResolvedValue(null); + + await expect( + getEventTypeList({ + prisma: readonlyPrisma, + teamId: 5, + userId: null, + isAll: false, + user: mockUser, + }) + ).rejects.toThrow("User is not part of a team/org"); + }); + }); +}); diff --git a/packages/features/insights/server/trpc-router.ts b/packages/features/insights/server/trpc-router.ts index 05d700d0ae..6dacde8283 100644 --- a/packages/features/insights/server/trpc-router.ts +++ b/packages/features/insights/server/trpc-router.ts @@ -1053,7 +1053,7 @@ export const insightsRouter = router({ }), }); -async function getEventTypeList({ +export async function getEventTypeList({ prisma, teamId, userId, @@ -1070,15 +1070,37 @@ async function getEventTypeList({ isOwnerAdminOfParentTeam: boolean; }; }) { - if (!teamId && !userId) { + if (!teamId && !userId && !isAll) { return []; } const membershipWhereConditional: Prisma.MembershipWhereInput = {}; - let childrenTeamIds: number[] = []; - if (isAll && teamId && user.organizationId && user.isOwnerAdminOfParentTeam) { + if (userId && !teamId && !isAll) { + const eventTypeResult = await prisma.eventType.findMany({ + select: { + id: true, + slug: true, + title: true, + teamId: true, + userId: true, + team: { + select: { + name: true, + }, + }, + }, + where: { + userId: user.id, + teamId: null, + }, + }); + + return eventTypeResult; + } + + if (isAll && user.organizationId && user.isOwnerAdminOfParentTeam) { const childTeams = await prisma.team.findMany({ where: { parentId: user.organizationId, @@ -1087,21 +1109,46 @@ async function getEventTypeList({ id: true, }, }); + if (childTeams.length > 0) { childrenTeamIds = childTeams.map((team) => team.id); } - membershipWhereConditional["teamId"] = { - in: [user.organizationId, ...childrenTeamIds], - }; + + const eventTypeResult = await prisma.eventType.findMany({ + select: { + id: true, + slug: true, + title: true, + teamId: true, + userId: true, + team: { + select: { + name: true, + }, + }, + }, + where: { + OR: [ + { + teamId: { + in: [user.organizationId, ...childrenTeamIds], + }, + }, + { + userId: user.id, + teamId: null, + }, + ], + }, + }); + + return eventTypeResult; } if (teamId && !isAll) { membershipWhereConditional["teamId"] = teamId; membershipWhereConditional["userId"] = user.id; } - if (userId) { - membershipWhereConditional["userId"] = userId; - } // I'm not using unique here since when userId comes from input we should look for every // event type that user owns @@ -1114,49 +1161,29 @@ async function getEventTypeList({ } const eventTypeWhereConditional: Prisma.EventTypeWhereInput = {}; - if (isAll && childrenTeamIds.length > 0 && user.organizationId && user.isOwnerAdminOfParentTeam) { - eventTypeWhereConditional["teamId"] = { - in: [user.organizationId, ...childrenTeamIds], - }; - } + if (teamId && !isAll) { eventTypeWhereConditional["teamId"] = teamId; } - if (userId) { - eventTypeWhereConditional["userId"] = userId; - } - let eventTypeResult: Prisma.EventTypeGetPayload<{ - select: { - id: true; - slug: true; - teamId: true; - title: true; - team: { - select: { - name: true; - }; - }; - }; - }>[] = []; let isMember = membership?.role === "MEMBER"; if (user.isOwnerAdminOfParentTeam) { isMember = false; } + if (isMember) { - eventTypeWhereConditional["OR"] = [ - { userId: user.id }, - { users: { some: { id: user.id } } }, - // @TODO this is not working as expected - // hosts: { some: { id: user.id } }, - ]; + eventTypeWhereConditional["OR"] = [{ userId: user.id }, { users: { some: { id: user.id } } }]; + // @TODO this is not working as expected + // hosts: { some: { id: user.id } }, } - eventTypeResult = await prisma.eventType.findMany({ + + const eventTypeResult = await prisma.eventType.findMany({ select: { id: true, slug: true, title: true, teamId: true, + userId: true, team: { select: { name: true,