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:
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(),
|
||||
|
||||
Reference in new issue
Block a user