fix: Missing personal event types in All filter (#23343)

* fix: Missing personal event types in All filter

Signed-off-by: Anirban Singha <anirbansingha20@gmail.com>

* chore: coderabbit suggestion

Signed-off-by: Anirban Singha <anirbansingha20@gmail.com>

* chore: export function

* chore: Add test file

Signed-off-by: Anirban Singha <anirbansingha20@gmail.com>

---------

Signed-off-by: Anirban Singha <anirbansingha20@gmail.com>
Co-authored-by: Kartik Saini <41051387+kart1ka@users.noreply.github.com>
This commit is contained in:
Anirban Singha
2025-08-28 11:56:58 +02:00
committed by GitHub
co-authored by Kartik Saini
parent 84bd4d3d38
commit f1d7d7f02a
2 changed files with 264 additions and 38 deletions
@@ -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");
});
});
});
@@ -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,