perf: batch user queries in schedules output service (#25901)

Replace N sequential queries with single batch query for fetching
user default schedule IDs

Changes:
- Add getUsersScheduleDefaultIds batch method to UsersRepository
- Add getResponseSchedules batch method to OutputSchedulesService
- Extract transformScheduleToOutput for reuse
- Simplify OrganizationsSchedulesService and TeamsSchedulesService
- Remove dead code (formatInput no-op method)

Reduces database round-trips from O(n) to O(1) for schedule lookups
This commit is contained in:
Pedro Castro
2026-01-13 19:21:17 +00:00
committed by GitHub
parent 69695d3022
commit 4226231b8a
4 changed files with 33 additions and 39 deletions
@@ -4,18 +4,34 @@ import { Injectable } from "@nestjs/common";
import type { WeekDay } from "@calcom/platform-types";
import type { Availability, Schedule } from "@calcom/prisma/client";
type DatabaseSchedule = Schedule & { availability: Availability[] };
@Injectable()
export class OutputSchedulesService_2024_06_11 {
constructor(private readonly usersRepository: UsersRepository) {}
async getResponseSchedule(databaseSchedule: Schedule & { availability: Availability[] }) {
if (!databaseSchedule.timeZone) {
databaseSchedule.timeZone = "Europe/London";
}
async getResponseSchedules(databaseSchedules: DatabaseSchedule[]) {
if (databaseSchedules.length === 0) return [];
const userIds = [...new Set(databaseSchedules.map((schedule) => schedule.userId))];
const defaultScheduleIds = await this.usersRepository.getUsersScheduleDefaultIds(userIds);
return databaseSchedules.map((schedule) =>
this.transformScheduleToOutput(schedule, defaultScheduleIds.get(schedule.userId) ?? null)
);
}
async getResponseSchedule(databaseSchedule: DatabaseSchedule) {
const ownerDefaultScheduleId = await this.usersRepository.getUserScheduleDefaultId(
databaseSchedule.userId
);
return this.transformScheduleToOutput(databaseSchedule, ownerDefaultScheduleId);
}
private transformScheduleToOutput(
databaseSchedule: DatabaseSchedule,
ownerDefaultScheduleId: number | null
) {
const timeZone = databaseSchedule.timeZone || "Europe/London";
const createdScheduleAvailabilities = databaseSchedule.availability.filter(
(availability) => !!availability.days.length
@@ -26,7 +42,7 @@ export class OutputSchedulesService_2024_06_11 {
id: databaseSchedule.id,
ownerId: databaseSchedule.userId,
name: databaseSchedule.name,
timeZone: databaseSchedule.timeZone,
timeZone,
availability: createdScheduleAvailabilities.map((availability) => ({
days: availability.days.map(transformNumberToDay),
startTime: this.padHoursMinutesWithZeros(
@@ -54,13 +70,9 @@ export class OutputSchedulesService_2024_06_11 {
};
}
padHoursMinutesWithZeros(hhMM: string) {
private padHoursMinutesWithZeros(hhMM: string) {
const [hours, minutes] = hhMM.split(":");
const formattedHours = hours.padStart(2, "0");
const formattedMinutes = minutes.padStart(2, "0");
return `${formattedHours}:${formattedMinutes}`;
return `${hours.padStart(2, "0")}:${minutes.padStart(2, "0")}`;
}
}
@@ -3,8 +3,6 @@ import { OutputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024
import { UsersRepository } from "@/modules/users/users.repository";
import { Injectable } from "@nestjs/common";
import { ScheduleOutput_2024_06_11 } from "@calcom/platform-types";
@Injectable()
export class OrganizationsSchedulesService {
constructor(
@@ -19,12 +17,6 @@ export class OrganizationsSchedulesService {
const schedules = await this.schedulesRepository.getSchedulesByUserIds(usersIds, skip, take);
const responseSchedules: ScheduleOutput_2024_06_11[] = [];
for (const schedule of schedules) {
responseSchedules.push(await this.outputSchedulesService.getResponseSchedule(schedule));
}
return responseSchedules;
return this.outputSchedulesService.getResponseSchedules(schedules);
}
}
@@ -3,8 +3,6 @@ import { OutputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
import { Injectable } from "@nestjs/common";
import { ScheduleOutput_2024_06_11 } from "@calcom/platform-types";
@Injectable()
export class TeamsSchedulesService {
constructor(
@@ -17,12 +15,6 @@ export class TeamsSchedulesService {
const userIds = await this.teamsRepository.getTeamUsersIds(teamId);
const schedules = await this.schedulesRepository.getSchedulesByUserIds(userIds, skip, take);
const responseSchedules: ScheduleOutput_2024_06_11[] = [];
for (const schedule of schedules) {
responseSchedules.push(await this.outputSchedulesService.getResponseSchedule(schedule));
}
return responseSchedules;
return this.outputSchedulesService.getResponseSchedules(schedules);
}
}
@@ -22,8 +22,6 @@ export class UsersRepository {
oAuthClientId: string,
isPlatformManaged: boolean
) {
this.formatInput(user);
return this.dbWrite.prisma.user.create({
data: {
...user,
@@ -227,8 +225,6 @@ export class UsersRepository {
}
async update(userId: number, updateData: UpdateManagedUserInput) {
this.formatInput(updateData);
return this.dbWrite.prisma.user.update({
where: { id: userId },
data: updateData,
@@ -257,12 +253,6 @@ export class UsersRepository {
});
}
formatInput(userInput: CreateManagedUserInput | UpdateManagedUserInput) {
if (userInput.weekStart) {
userInput.weekStart = userInput.weekStart;
}
}
setDefaultSchedule(userId: number, scheduleId: number) {
return this.dbWrite.prisma.user.update({
where: { id: userId },
@@ -280,6 +270,14 @@ export class UsersRepository {
return user?.defaultScheduleId;
}
async getUsersScheduleDefaultIds(userIds: number[]): Promise<Map<number, number | null>> {
const users = await this.dbRead.prisma.user.findMany({
where: { id: { in: userIds } },
select: { id: true, defaultScheduleId: true },
});
return new Map(users.map((user) => [user.id, user.defaultScheduleId]));
}
async getOrganizationUsers(organizationId: number) {
const profiles = await this.dbRead.prisma.profile.findMany({
where: {