Files
Udit TakkarGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
3908cda372 perf: optimize getBusyTimesForLimitChecks with batched parallel queries (#27399)
* perf: optimize getBusyTimesForLimitChecks with batched parallel queries and raw SQL

Co-Authored-By: [email protected] <[email protected]>

* test: add unit tests for getBusyTimesForLimitChecks batched query optimization

Co-Authored-By: [email protected] <[email protected]>

* refactor: use Prisma findMany instead of raw SQL for better testability

The batching (50 user chunks) and parallel execution (Promise.all) provide
the main performance optimization. Using findMany instead of $queryRaw
maintains the same performance characteristics while being compatible
with prismock for testing.

Co-Authored-By: [email protected] <[email protected]>

* fix: type error

* refactor: add integration test

* chore: remove log

* chore: remove log

* fix: avoid logging PII (userId) in busyTimes debug log

Replace JSON.stringify(busyTimes) with count to avoid exposing userId
in logs. Addresses Cubic AI review feedback (confidence 9/10).

Co-Authored-By: unknown <>

* chore: undo

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-10 15:56:30 +00:00

385 lines
13 KiB
TypeScript

import { prisma } from "@calcom/prisma/__mocks__/prisma";
import dayjs from "@calcom/dayjs";
import { getBusyTimesService } from "@calcom/features/di/containers/BusyTimes";
import { beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("@calcom/prisma", () => ({
default: prisma,
prisma,
}));
const startOfTomorrow = dayjs().add(1, "day").startOf("day");
const tomorrowDate = startOfTomorrow.format("YYYY-MM-DD");
const mockBookings = ({
beforeEventBuffer = 0,
afterEventBuffer = 0,
seatsPerTimeSlot,
}: {
beforeEventBuffer?: number;
afterEventBuffer?: number;
seatsPerTimeSlot?: number;
}) => [
{
id: 1,
startTime: startOfTomorrow.set("hour", 10).toDate(),
endTime: startOfTomorrow.set("hour", 11).toDate(),
title: "Booking Between X and Y",
userId: 1,
uid: "xxxx1",
eventType: {
id: 1,
beforeEventBuffer,
afterEventBuffer,
seatsPerTimeSlot: seatsPerTimeSlot ?? null,
},
_count: {
seatsReferences: 1,
},
},
{
id: 2,
startTime: startOfTomorrow.set("hour", 14).toDate(),
endTime: startOfTomorrow.set("hour", 15).toDate(),
title: "Booking Between X and Y",
userId: 1,
uid: "xxxx2",
eventType: {
id: 1,
beforeEventBuffer,
afterEventBuffer,
seatsPerTimeSlot: seatsPerTimeSlot ?? null,
},
...(seatsPerTimeSlot
? {
_count: {
seatsReferences: 1,
},
}
: {}),
},
];
describe("getBusyTimes", () => {
it("blocks a regular time slot", async () => {
const busyTimesService = getBusyTimesService();
const busyTimes = await busyTimesService.getBusyTimes({
credentials: [],
userId: 1,
userEmail: "[email protected]",
username: "exampleuser1",
bypassBusyCalendarTimes: false,
selectedCalendars: [],
startTime: startOfTomorrow.format(),
endTime: startOfTomorrow.endOf("day").format(),
currentBookings: mockBookings({}),
});
expect(busyTimes).toEqual([
expect.objectContaining({
start: dayjs(`${tomorrowDate}`).startOf("day").set("hour", 10).toDate(),
end: dayjs(`${tomorrowDate}`).startOf("day").set("hour", 11).toDate(),
source: "eventType-1-booking-1",
title: "Booking Between X and Y",
}),
expect.objectContaining({
start: dayjs(`${tomorrowDate}`).startOf("day").set("hour", 14).toDate(),
end: dayjs(`${tomorrowDate}`).startOf("day").set("hour", 15).toDate(),
source: "eventType-1-booking-2",
title: "Booking Between X and Y",
}),
]);
});
it("should block before and after buffer times", async () => {
const busyTimesService = getBusyTimesService();
const busyTimes = await busyTimesService.getBusyTimes({
credentials: [],
userId: 1,
userEmail: "[email protected]",
username: "exampleuser1",
bypassBusyCalendarTimes: false,
selectedCalendars: [],
startTime: startOfTomorrow.format(),
endTime: startOfTomorrow.endOf("day").format(),
currentBookings: [mockBookings({ beforeEventBuffer: 10 })[0]],
});
expect(busyTimes).toEqual([
expect.objectContaining({
start: dayjs(`${tomorrowDate}`).startOf("day").set("hour", 9).set("minute", 50).toDate(),
end: dayjs(`${tomorrowDate}`).startOf("day").set("hour", 11).toDate(),
source: "eventType-1-booking-1",
title: "Booking Between X and Y",
}),
]);
});
it("should have busy times only if seated with remaining seats when buffers exist", async () => {
const busyTimesService = getBusyTimesService();
const busyTimes = await busyTimesService.getBusyTimes({
credentials: [],
userId: 1,
eventTypeId: 1,
userEmail: "[email protected]",
username: "exampleuser1",
bypassBusyCalendarTimes: false,
selectedCalendars: [],
startTime: startOfTomorrow.format(),
endTime: startOfTomorrow.endOf("day").format(),
currentBookings: [mockBookings({ beforeEventBuffer: 10, seatsPerTimeSlot: 10 })[0]],
});
expect(busyTimes).toEqual([
expect.objectContaining({
start: dayjs(`${tomorrowDate}`).startOf("day").set("hour", 9).set("minute", 50).toDate(),
end: dayjs(`${tomorrowDate}`).startOf("day").set("hour", 10).toDate(),
}),
]);
});
});
describe("getBusyTimesForLimitChecks", () => {
beforeEach(() => {
vi.clearAllMocks();
});
const createMockBookingResult = (
overrides: Partial<{
id: number;
startTime: Date;
endTime: Date;
eventTypeId: number | null;
title: string;
userId: number | null;
}> = {}
) => ({
id: 1,
startTime: startOfTomorrow.set("hour", 10).toDate(),
endTime: startOfTomorrow.set("hour", 11).toDate(),
eventTypeId: 1,
title: "Test Booking",
userId: 1,
...overrides,
});
it("should return empty array when no booking or duration limits are provided", async () => {
const busyTimesService = getBusyTimesService();
const busyTimes = await busyTimesService.getBusyTimesForLimitChecks({
userIds: [1, 2, 3],
eventTypeId: 1,
startDate: startOfTomorrow.format(),
endDate: startOfTomorrow.endOf("day").format(),
bookingLimits: null,
durationLimits: null,
});
expect(busyTimes).toEqual([]);
expect(prisma.booking.findMany).not.toHaveBeenCalled();
});
it("should return empty array when userIds is empty", async () => {
const busyTimesService = getBusyTimesService();
const busyTimes = await busyTimesService.getBusyTimesForLimitChecks({
userIds: [],
eventTypeId: 1,
startDate: startOfTomorrow.format(),
endDate: startOfTomorrow.endOf("day").format(),
bookingLimits: { PER_DAY: 5 },
});
expect(busyTimes).toEqual([]);
});
it("should fetch bookings for a single user with booking limits", async () => {
const mockBooking = createMockBookingResult();
vi.mocked(prisma.booking.findMany).mockResolvedValue([mockBooking]);
const busyTimesService = getBusyTimesService();
const busyTimes = await busyTimesService.getBusyTimesForLimitChecks({
userIds: [1],
eventTypeId: 1,
startDate: startOfTomorrow.format(),
endDate: startOfTomorrow.endOf("day").format(),
bookingLimits: { PER_DAY: 5 },
});
expect(busyTimes).toHaveLength(1);
expect(busyTimes[0]).toEqual(
expect.objectContaining({
start: mockBooking.startTime,
end: mockBooking.endTime,
title: mockBooking.title,
source: `eventType-${mockBooking.eventTypeId}-booking-${mockBooking.id}`,
userId: mockBooking.userId,
})
);
expect(prisma.booking.findMany).toHaveBeenCalledTimes(1);
});
it("should fetch bookings for multiple users with duration limits", async () => {
const mockBookings = [
createMockBookingResult({ id: 1, userId: 1 }),
createMockBookingResult({
id: 2,
userId: 2,
startTime: startOfTomorrow.set("hour", 14).toDate(),
endTime: startOfTomorrow.set("hour", 15).toDate(),
}),
];
vi.mocked(prisma.booking.findMany).mockResolvedValue(mockBookings);
const busyTimesService = getBusyTimesService();
const busyTimes = await busyTimesService.getBusyTimesForLimitChecks({
userIds: [1, 2],
eventTypeId: 1,
startDate: startOfTomorrow.format(),
endDate: startOfTomorrow.endOf("day").format(),
durationLimits: { PER_DAY: 120 },
});
expect(busyTimes).toHaveLength(2);
expect(busyTimes[0].userId).toBe(1);
expect(busyTimes[1].userId).toBe(2);
});
it("should batch queries when userIds exceeds batch size (50)", async () => {
const userIds = Array.from({ length: 75 }, (_, i) => i + 1);
const mockBookings = [createMockBookingResult()];
vi.mocked(prisma.booking.findMany).mockResolvedValue(mockBookings);
const busyTimesService = getBusyTimesService();
await busyTimesService.getBusyTimesForLimitChecks({
userIds,
eventTypeId: 1,
startDate: startOfTomorrow.format(),
endDate: startOfTomorrow.endOf("day").format(),
bookingLimits: { PER_DAY: 5 },
});
// Should be called twice: once for first 50 users, once for remaining 25
expect(prisma.booking.findMany).toHaveBeenCalledTimes(2);
});
it("should batch queries correctly for exactly 100 users (2 full batches)", async () => {
const userIds = Array.from({ length: 100 }, (_, i) => i + 1);
vi.mocked(prisma.booking.findMany).mockResolvedValue([]);
const busyTimesService = getBusyTimesService();
await busyTimesService.getBusyTimesForLimitChecks({
userIds,
eventTypeId: 1,
startDate: startOfTomorrow.format(),
endDate: startOfTomorrow.endOf("day").format(),
bookingLimits: { PER_WEEK: 10 },
});
expect(prisma.booking.findMany).toHaveBeenCalledTimes(2);
});
it("should batch queries correctly for 150 users (3 batches)", async () => {
const userIds = Array.from({ length: 150 }, (_, i) => i + 1);
vi.mocked(prisma.booking.findMany).mockResolvedValue([]);
const busyTimesService = getBusyTimesService();
await busyTimesService.getBusyTimesForLimitChecks({
userIds,
eventTypeId: 1,
startDate: startOfTomorrow.format(),
endDate: startOfTomorrow.endOf("day").format(),
bookingLimits: { PER_MONTH: 20 },
});
expect(prisma.booking.findMany).toHaveBeenCalledTimes(3);
});
it("should merge results from multiple batches correctly", async () => {
const userIds = Array.from({ length: 75 }, (_, i) => i + 1);
// First batch returns 2 bookings, second batch returns 1 booking
vi.mocked(prisma.booking.findMany)
.mockResolvedValueOnce([
createMockBookingResult({ id: 1, userId: 1 }),
createMockBookingResult({ id: 2, userId: 25 }),
])
.mockResolvedValueOnce([createMockBookingResult({ id: 3, userId: 60 })]);
const busyTimesService = getBusyTimesService();
const busyTimes = await busyTimesService.getBusyTimesForLimitChecks({
userIds,
eventTypeId: 1,
startDate: startOfTomorrow.format(),
endDate: startOfTomorrow.endOf("day").format(),
bookingLimits: { PER_DAY: 5 },
});
expect(busyTimes).toHaveLength(3);
expect(busyTimes.map((b) => b.userId)).toEqual([1, 25, 60]);
});
it("should exclude rescheduleUid from results", async () => {
const mockBooking = createMockBookingResult();
vi.mocked(prisma.booking.findMany).mockResolvedValue([mockBooking]);
const busyTimesService = getBusyTimesService();
await busyTimesService.getBusyTimesForLimitChecks({
userIds: [1],
eventTypeId: 1,
startDate: startOfTomorrow.format(),
endDate: startOfTomorrow.endOf("day").format(),
rescheduleUid: "existing-booking-uid",
bookingLimits: { PER_DAY: 5 },
});
// Verify the query was called (the actual exclusion happens in the Prisma query)
expect(prisma.booking.findMany).toHaveBeenCalledTimes(1);
});
it("should handle both booking and duration limits together", async () => {
const mockBooking = createMockBookingResult();
vi.mocked(prisma.booking.findMany).mockResolvedValue([mockBooking]);
const busyTimesService = getBusyTimesService();
const busyTimes = await busyTimesService.getBusyTimesForLimitChecks({
userIds: [1],
eventTypeId: 1,
startDate: startOfTomorrow.format(),
endDate: startOfTomorrow.endOf("day").format(),
bookingLimits: { PER_DAY: 5 },
durationLimits: { PER_DAY: 120 },
});
expect(busyTimes).toHaveLength(1);
expect(prisma.booking.findMany).toHaveBeenCalledTimes(1);
});
it("should handle null eventTypeId in booking results", async () => {
const mockBooking = createMockBookingResult({ eventTypeId: null });
vi.mocked(prisma.booking.findMany).mockResolvedValue([mockBooking]);
const busyTimesService = getBusyTimesService();
const busyTimes = await busyTimesService.getBusyTimesForLimitChecks({
userIds: [1],
eventTypeId: 1,
startDate: startOfTomorrow.format(),
endDate: startOfTomorrow.endOf("day").format(),
bookingLimits: { PER_DAY: 5 },
});
expect(busyTimes).toHaveLength(1);
expect(busyTimes[0].source).toBe(`eventType-null-booking-${mockBooking.id}`);
});
it("should handle null userId in booking results", async () => {
const mockBooking = createMockBookingResult({ userId: null });
vi.mocked(prisma.booking.findMany).mockResolvedValue([mockBooking]);
const busyTimesService = getBusyTimesService();
const busyTimes = await busyTimesService.getBusyTimesForLimitChecks({
userIds: [1],
eventTypeId: 1,
startDate: startOfTomorrow.format(),
endDate: startOfTomorrow.endOf("day").format(),
bookingLimits: { PER_DAY: 5 },
});
expect(busyTimes).toHaveLength(1);
expect(busyTimes[0].userId).toBeNull();
});
});