fix: eventType filter on /insights and add unit tests for buildBaseWhereCondition (#21154)

* test: Add unit tests for buildBaseWhereCondition function

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* refactor: Export buildBaseWhereCondition function instead of copying it

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* refactor: Remove implementation detail checks from tests

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* test: Fix tests to match actual function behavior

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* test: Fix test issues with buildBaseWhereCondition

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* test: Update tests to match actual function behavior

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* fix: Address security issues in buildBaseWhereCondition function

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* refactor: Simplify buildBaseWhereCondition function

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* refactor: Simplify code and remove isEmptyResponse property

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* refactor: Use array of conditions approach for buildBaseWhereCondition

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* refactor: Make buildBaseWhereCondition implementation consistent and update tests

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* fix: Fix incorrect usage of buildBaseWhereCondition function

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* chore: Revert yarn.lock changes

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* fix: Comment out non-existent Sentry API method to fix type error

Co-Authored-By: eunjae@cal.com <eunjae@cal.com>

* Revert "fix: Comment out non-existent Sentry API method to fix type error"

This reverts commit 44c21a7396a518c63047b33967cd8af7e6e4f268.

* remove unnecessary if-statement

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: eunjae@cal.com <eunjae@cal.com>
Co-authored-by: Eunjae Lee <hey@eunjae.dev>
This commit is contained in:
authored and GitHub committed 2025-05-16 10:10:57 +02:00
1 parent fab364f084
commit 8889e8566c
2 files changed
+368 -61

No files matched your search

@@ -0,0 +1,297 @@
import { describe, expect, it, vi, beforeEach } from "vitest";
import type { readonlyPrisma } from "@calcom/prisma";
import { buildBaseWhereCondition } from "../trpc-router";
const mockTeamFindMany = vi.fn();
const mockMembershipFindMany = vi.fn();
const mockInsightsDb = {
team: {
findMany: mockTeamFindMany,
},
membership: {
findMany: mockMembershipFindMany,
},
} as unknown as typeof readonlyPrisma;
const createMockContext = (overrides = {}) => ({
userIsOwnerAdminOfParentTeam: false,
userOrganizationId: null,
insightsDb: mockInsightsDb,
...overrides,
});
describe("buildBaseWhereCondition", () => {
beforeEach(() => {
vi.resetAllMocks();
});
describe("Basic filtering", () => {
it("should set eventTypeId condition when eventTypeId is provided", async () => {
const ctx = createMockContext();
const result = await buildBaseWhereCondition({
eventTypeId: 123,
ctx,
});
expect(result.whereCondition).toEqual({
OR: [{ eventTypeId: 123 }, { eventParentId: 123 }],
});
});
it("should set userId condition when memberUserId is provided", async () => {
const ctx = createMockContext();
const result = await buildBaseWhereCondition({
memberUserId: 456,
ctx,
});
expect(result.whereCondition).toEqual({
userId: 456,
});
});
it("should set userId and teamId conditions when userId is provided", async () => {
const ctx = createMockContext();
const result = await buildBaseWhereCondition({
userId: 789,
ctx,
});
expect(result.whereCondition).toEqual({
userId: 789,
teamId: null,
});
});
});
describe("Organization-wide queries", () => {
it("should return appropriate where condition when no teams found in organization", async () => {
mockTeamFindMany.mockResolvedValue([]);
const ctx = createMockContext({
userIsOwnerAdminOfParentTeam: true,
userOrganizationId: 100,
});
const result = await buildBaseWhereCondition({
isAll: true,
ctx,
});
expect(result.whereCondition).toEqual({
OR: [
{
teamId: { in: [100] },
isTeamBooking: true,
},
],
});
});
it("should build complex where condition for organization-wide query", async () => {
mockTeamFindMany.mockResolvedValue([{ id: 101 }, { id: 102 }]);
mockMembershipFindMany.mockResolvedValue([{ userId: 201 }, { userId: 202 }]);
const ctx = createMockContext({
userIsOwnerAdminOfParentTeam: true,
userOrganizationId: 100,
});
const result = await buildBaseWhereCondition({
isAll: true,
ctx,
});
expect(result.whereCondition).toEqual({
OR: [
{
teamId: {
in: [100, 101, 102],
},
isTeamBooking: true,
},
{
userId: {
in: [201, 202],
},
isTeamBooking: false,
},
],
});
});
});
describe("Team-specific queries", () => {
it("should build where condition for team-specific query", async () => {
mockMembershipFindMany.mockResolvedValue([{ userId: 301 }, { userId: 302 }]);
const ctx = createMockContext();
const result = await buildBaseWhereCondition({
teamId: 200,
isAll: false,
ctx,
});
expect(result.whereCondition).toEqual({
OR: [
{
teamId: 200,
isTeamBooking: true,
},
{
userId: {
in: [301, 302],
},
isTeamBooking: false,
},
],
});
});
it("should apply both team and eventTypeId conditions when both are provided", async () => {
mockMembershipFindMany.mockResolvedValue([{ userId: 301 }, { userId: 302 }]);
const ctx = createMockContext();
const result = await buildBaseWhereCondition({
teamId: 200,
eventTypeId: 500,
isAll: false,
ctx,
});
expect(result.whereCondition).toEqual({
AND: [
{
OR: [{ eventTypeId: 500 }, { eventParentId: 500 }],
},
{
OR: [
{
teamId: 200,
isTeamBooking: true,
},
{
userId: {
in: [301, 302],
},
isTeamBooking: false,
},
],
},
],
});
});
});
describe("Combined filtering", () => {
it("should combine eventTypeId and memberUserId conditions", async () => {
const ctx = createMockContext();
const result = await buildBaseWhereCondition({
eventTypeId: 123,
memberUserId: 456,
ctx,
});
expect(result.whereCondition).toEqual({
AND: [
{
OR: [{ eventTypeId: 123 }, { eventParentId: 123 }],
},
{
userId: 456,
},
],
});
});
});
describe("Invalid parameters", () => {
it("should handle missing parameters and return restrictive where condition", async () => {
const ctx = createMockContext();
const result = await buildBaseWhereCondition({
ctx,
});
expect(result.whereCondition).toEqual({ id: -1 });
});
it("should handle null teamId with restrictive where condition", async () => {
const ctx = createMockContext();
const result = await buildBaseWhereCondition({
teamId: null,
ctx,
});
expect(result.whereCondition).toEqual({ id: -1 });
});
it("should handle empty team members with proper team condition", async () => {
mockMembershipFindMany.mockResolvedValue([]);
const ctx = createMockContext();
const result = await buildBaseWhereCondition({
teamId: 200,
isAll: false,
ctx,
});
expect(result.whereCondition).toEqual({
OR: [
{
teamId: 200,
isTeamBooking: true,
},
{
userId: {
in: [],
},
isTeamBooking: false,
},
],
});
});
it("should handle rejected team membership differently from empty team", async () => {
mockMembershipFindMany.mockImplementation((params) => {
if (params?.where?.accepted === true) {
// No accepted members
return Promise.resolve([]);
}
return Promise.resolve([{ userId: 601 }, { userId: 602 }]);
});
const ctx = createMockContext();
const result = await buildBaseWhereCondition({
teamId: 200,
isAll: false,
ctx,
});
// Should only include team bookings since there are no accepted members
expect(result.whereCondition).toEqual({
OR: [
{
teamId: 200,
isTeamBooking: true,
},
{
userId: {
in: [], // Empty because no accepted members
},
isTeamBooking: false,
},
],
});
});
});
});
@@ -41,7 +41,7 @@ interface BuildBaseWhereConditionType {
const bookingStatusSchema = z.enum(["NO_BOOKING", ...Object.values(BookingStatus)]).optional();
const buildBaseWhereCondition = async ({
export const buildBaseWhereCondition = async ({
teamId,
eventTypeId,
memberUserId,
@@ -50,17 +50,28 @@ const buildBaseWhereCondition = async ({
ctx,
}: BuildBaseWhereConditionType): Promise<{
whereCondition: Prisma.BookingTimeStatusWhereInput;
isEmptyResponse?: boolean;
}> => {
let whereCondition: Prisma.BookingTimeStatusWhereInput = {};
const conditions: Prisma.BookingTimeStatusWhereInput[] = [];
// EventType Filter
if (eventTypeId) whereCondition.OR = [{ eventTypeId }, { eventParentId: eventTypeId }];
// User/Member filter
if (memberUserId) whereCondition.userId = memberUserId;
if (userId) {
whereCondition.teamId = null;
whereCondition.userId = userId;
if (eventTypeId) {
conditions.push({
OR: [{ eventTypeId }, { eventParentId: eventTypeId }],
});
}
// User/Member filter
if (memberUserId) {
conditions.push({ userId: memberUserId });
}
if (userId) {
conditions.push({
teamId: null,
userId: userId,
});
}
// organization-wide queries condition
if (isAll && ctx.userIsOwnerAdminOfParentTeam && ctx.userOrganizationId) {
const teamsFromOrg = await ctx.insightsDb.team.findMany({
@@ -72,57 +83,48 @@ const buildBaseWhereCondition = async ({
},
});
if (teamsFromOrg.length === 0) {
return {
whereCondition: {
...whereCondition,
OR: [
...(whereCondition.OR ?? []),
{
teamId: ctx.userOrganizationId,
isTeamBooking: true,
const teamIds = [ctx.userOrganizationId, ...teamsFromOrg.map((t) => t.id)];
const usersFromOrg =
teamsFromOrg.length > 0
? await ctx.insightsDb.membership.findMany({
where: {
team: {
id: {
in: teamIds,
},
},
accepted: true,
},
],
},
isEmptyResponse: true,
};
}
select: {
userId: true,
},
})
: [];
const teamConditional = {
id: {
in: [ctx.userOrganizationId, ...teamsFromOrg.map((t) => t.id)],
},
};
const usersFromOrg = await ctx.insightsDb.membership.findMany({
where: {
team: teamConditional,
accepted: true,
},
select: {
userId: true,
},
});
const userIdsFromOrg = usersFromOrg.map((u) => u.userId);
whereCondition = {
...whereCondition,
conditions.push({
OR: [
{
teamId: {
in: [ctx.userOrganizationId, ...teamsFromOrg.map((t) => t.id)],
in: teamIds,
},
isTeamBooking: true,
},
{
userId: {
in: userIdsFromOrg,
},
isTeamBooking: false,
},
...(usersFromOrg.length > 0
? [
{
userId: {
in: usersFromOrg.map((u) => u.userId),
},
isTeamBooking: false,
},
]
: []),
],
};
});
}
if (teamId && !isAll && !eventTypeId) {
// Team-specific queries condition
if (!isAll && teamId) {
const usersFromTeam = await ctx.insightsDb.membership.findMany({
where: {
teamId: teamId,
@@ -133,8 +135,8 @@ const buildBaseWhereCondition = async ({
},
});
const userIdsFromTeam = usersFromTeam.map((u) => u.userId);
whereCondition = {
...whereCondition,
conditions.push({
OR: [
{
teamId,
@@ -147,9 +149,23 @@ const buildBaseWhereCondition = async ({
isTeamBooking: false,
},
],
};
});
}
return { whereCondition };
let whereCondition: Prisma.BookingTimeStatusWhereInput = {};
if (conditions.length === 1) {
whereCondition = conditions[0];
} else if (conditions.length > 1) {
whereCondition = { AND: conditions };
}
return {
whereCondition:
Object.keys(whereCondition).length === 0
? { id: -1 } // Ensure no data is returned for invalid parameters
: whereCondition,
};
};
const buildHashMapForUsers = <
@@ -1200,8 +1216,6 @@ export const insightsRouter = router({
bookingWhere = {
...bookingWhere,
teamId,
eventTypeId,
createdAt: {
gte: dayjs(startDate).startOf("day").toDate(),
lte: dayjs(endDate).endOf("day").toDate(),
@@ -1318,8 +1332,6 @@ export const insightsRouter = router({
bookingWhere = {
...bookingWhere,
teamId,
eventTypeId,
createdAt: {
gte: dayjs(startDate).startOf("day").toDate(),
lte: dayjs(endDate).endOf("day").toDate(),
@@ -1397,8 +1409,7 @@ export const insightsRouter = router({
let { whereCondition: bookingWhere } = r;
bookingWhere = {
teamId,
eventTypeId,
...bookingWhere,
createdAt: {
gte: dayjs(startDate).startOf("day").toDate(),
lte: dayjs(endDate).endOf("day").toDate(),
@@ -1476,8 +1487,7 @@ export const insightsRouter = router({
let { whereCondition: bookingWhere } = r;
bookingWhere = {
teamId,
eventTypeId,
...bookingWhere,
createdAt: {
gte: dayjs(startDate).startOf("day").toDate(),
lte: dayjs(endDate).endOf("day").toDate(),