Files
calendar/packages/features/busyTimes/services/getBusyTimes.integration-test.ts
T
Udit TakkarGitHubunknown <>Devin 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: udit@cal.com <udit222001@gmail.com>

* test: add unit tests for getBusyTimesForLimitChecks batched query optimization

Co-Authored-By: udit@cal.com <udit222001@gmail.com>

* 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: udit@cal.com <udit222001@gmail.com>

* 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

161 lines
4.6 KiB
TypeScript

import dayjs from "@calcom/dayjs";
import { getBusyTimesService } from "@calcom/features/di/containers/BusyTimes";
import { prisma } from "@calcom/prisma";
import { BookingStatus } from "@calcom/prisma/enums";
import { afterEach, describe, expect, it } from "vitest";
type CreatedResources = {
users: number[];
eventTypes: number[];
bookings: number[];
};
const createdResources: CreatedResources = {
users: [],
eventTypes: [],
bookings: [],
};
const createTestUser = async (overrides?: { email?: string; username?: string }) => {
const timestamp = `${Date.now()}-${Math.random()}`;
const user = await prisma.user.create({
data: {
email: overrides?.email ?? `busy-times-${timestamp}@example.com`,
username: overrides?.username ?? `busy-times-${timestamp}`,
},
});
createdResources.users.push(user.id);
return user;
};
const createTestEventType = async (userId: number) => {
const timestamp = `${Date.now()}-${Math.random()}`;
const eventType = await prisma.eventType.create({
data: {
title: "Busy Times Test Event",
slug: `busy-times-${timestamp}`,
length: 30,
userId,
users: {
connect: { id: userId },
},
},
});
createdResources.eventTypes.push(eventType.id);
return eventType;
};
const createTestBooking = async (params: {
userId: number;
eventTypeId: number;
uid: string;
startTime: Date;
endTime: Date;
}) => {
const booking = await prisma.booking.create({
data: {
userId: params.userId,
eventTypeId: params.eventTypeId,
uid: params.uid,
status: BookingStatus.ACCEPTED,
startTime: params.startTime,
endTime: params.endTime,
title: "Busy Times Test Booking",
},
});
createdResources.bookings.push(booking.id);
return booking;
};
afterEach(async () => {
if (createdResources.bookings.length > 0) {
await prisma.booking.deleteMany({
where: { id: { in: createdResources.bookings } },
});
createdResources.bookings = [];
}
if (createdResources.eventTypes.length > 0) {
await prisma.eventType.deleteMany({
where: { id: { in: createdResources.eventTypes } },
});
createdResources.eventTypes = [];
}
if (createdResources.users.length > 0) {
await prisma.user.deleteMany({
where: { id: { in: createdResources.users } },
});
createdResources.users = [];
}
});
describe("getBusyTimesForLimitChecks (integration)", () => {
it("returns bookings across batches for large user lists", async () => {
const user1 = await createTestUser();
const user2 = await createTestUser();
const eventType = await createTestEventType(user1.id);
const dayStart = dayjs().add(1, "day").startOf("day");
const dayEnd = dayStart.endOf("day");
await createTestBooking({
userId: user1.id,
eventTypeId: eventType.id,
uid: `busy-times-${Date.now()}-1`,
startTime: dayStart.set("hour", 9).toDate(),
endTime: dayStart.set("hour", 10).toDate(),
});
await createTestBooking({
userId: user2.id,
eventTypeId: eventType.id,
uid: `busy-times-${Date.now()}-2`,
startTime: dayStart.set("hour", 11).toDate(),
endTime: dayStart.set("hour", 12).toDate(),
});
const fillerIds = Array.from({ length: 58 }, (_, index) => 1_000_000 + index);
const userIds = [user1.id, ...fillerIds, user2.id];
const busyTimes = await getBusyTimesService().getBusyTimesForLimitChecks({
userIds,
eventTypeId: eventType.id,
startDate: dayStart.toISOString(),
endDate: dayEnd.toISOString(),
bookingLimits: { PER_DAY: 5 },
});
expect(busyTimes).toHaveLength(2);
expect(busyTimes.map((busyTime) => busyTime.userId).sort()).toEqual([user1.id, user2.id].sort());
});
it("excludes rescheduleUid from results", async () => {
const user = await createTestUser();
const eventType = await createTestEventType(user.id);
const dayStart = dayjs().add(2, "day").startOf("day");
const dayEnd = dayStart.endOf("day");
const rescheduleUid = `busy-times-${Date.now()}-reschedule`;
await createTestBooking({
userId: user.id,
eventTypeId: eventType.id,
uid: rescheduleUid,
startTime: dayStart.set("hour", 9).toDate(),
endTime: dayStart.set("hour", 10).toDate(),
});
const busyTimes = await getBusyTimesService().getBusyTimesForLimitChecks({
userIds: [user.id],
eventTypeId: eventType.id,
startDate: dayStart.toISOString(),
endDate: dayEnd.toISOString(),
rescheduleUid,
bookingLimits: { PER_DAY: 5 },
});
expect(busyTimes).toHaveLength(0);
});
});