refactor: migrate schedule utils from TRPC to ScheduleRepository to break circular dependency (#24764)

* remove util files

* wip

* wip

* wip

* wip

* wip

* wip

* add test file

* refactors

* refactors

* fix type error

* simplify

* host repository test

* di
This commit is contained in:
Benny Joo
2025-12-10 12:01:48 +02:00
committed by GitHub
parent eb01e2f9d8
commit 09a4247aa1
13 changed files with 210 additions and 147 deletions
@@ -1,6 +1,7 @@
import { SchedulesController_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/controllers/schedules.controller";
import { SchedulesRepository_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.repository";
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
import { PrismaScheduleRepository } from "@/lib/repositories/prisma-schedule.repository";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersModule } from "@/modules/users/users.module";
@@ -8,7 +9,7 @@ import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule, UsersModule, TokensModule],
providers: [SchedulesRepository_2024_04_15, SchedulesService_2024_04_15],
providers: [SchedulesRepository_2024_04_15, SchedulesService_2024_04_15, PrismaScheduleRepository],
controllers: [SchedulesController_2024_04_15],
exports: [SchedulesService_2024_04_15, SchedulesRepository_2024_04_15],
})
@@ -1,12 +1,12 @@
import { CreateAvailabilityInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-availability.input";
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
import { SchedulesRepository_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.repository";
import { PrismaScheduleRepository } from "@/lib/repositories/prisma-schedule.repository";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
import { updateSchedule } from "@calcom/platform-libraries/schedules";
import { ScheduleRepository } from "@calcom/platform-libraries/schedules";
import type { UpdateScheduleInput_2024_04_15 } from "@calcom/platform-types";
import type { PrismaClient } from "@calcom/prisma";
import type { Schedule } from "@calcom/prisma/client";
@@ -16,7 +16,8 @@ export class SchedulesService_2024_04_15 {
constructor(
private readonly schedulesRepository: SchedulesRepository_2024_04_15,
private readonly usersRepository: UsersRepository,
private readonly dbWrite: PrismaWriteService
private readonly dbWrite: PrismaWriteService,
private readonly prismaScheduleRepository: PrismaScheduleRepository
) {}
async createUserDefaultSchedule(userId: number, timeZone: string) {
@@ -53,8 +54,7 @@ export class SchedulesService_2024_04_15 {
const user = await this.usersRepository.findById(userId);
if (!user?.defaultScheduleId) return null;
return await ScheduleRepository.findDetailedScheduleById({
return await this.prismaScheduleRepository.findDetailedScheduleById({
scheduleId: user.defaultScheduleId,
isManagedEventType: undefined,
userId,
@@ -70,7 +70,7 @@ export class SchedulesService_2024_04_15 {
throw new NotFoundException(`User with ID=${userId} does not exist.`);
}
const existingSchedule = await ScheduleRepository.findDetailedScheduleById({
const existingSchedule = await this.prismaScheduleRepository.findDetailedScheduleById({
scheduleId: scheduleId,
isManagedEventType: undefined,
userId,
@@ -88,7 +88,11 @@ export class SchedulesService_2024_04_15 {
}
async getUserSchedules(userId: number, timeZone: string, defaultScheduleId: number | null) {
return ScheduleRepository.findManyDetailedScheduleByUserId({ userId, timeZone, defaultScheduleId });
return this.prismaScheduleRepository.findManyDetailedScheduleByUserId({
userId,
timeZone,
defaultScheduleId,
});
}
async updateUserSchedule(
@@ -32,8 +32,8 @@ export class SchedulesAtomsService {
const user = await this.usersRepository.findById(userId);
if (!user?.defaultScheduleId) return null;
return await ScheduleRepository.findDetailedScheduleById({
const scheduleRepo = new ScheduleRepository(this.dbWrite.prisma as unknown as PrismaClient);
return await scheduleRepo.findDetailedScheduleById({
scheduleId: scheduleId ?? user.defaultScheduleId,
isManagedEventType,
userId,
@@ -3,8 +3,8 @@ import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import dayjs from "@calcom/dayjs";
import { ScheduleRepository } from "@calcom/features/schedules/repositories/ScheduleRepository";
import prisma from "@calcom/prisma";
import { getDefaultScheduleId } from "@calcom/trpc/server/routers/viewer/availability/util";
const travelScheduleSelect = {
id: true,
@@ -40,7 +40,8 @@ async function postHandler(request: NextRequest) {
},
});
const defaultScheduleId = await getDefaultScheduleId(user.id, prisma);
const scheduleRepository = new ScheduleRepository(prisma);
const defaultScheduleId = await scheduleRepository.getDefaultScheduleId(user.id);
if (!user.defaultScheduleId) {
// set default schedule if not already set
@@ -1,9 +1,8 @@
import { describe, expect, it, vi, beforeEach } from "vitest";
import { HostRepository } from "@calcom/lib/server/repository/host";
import { PrismaClient } from "@calcom/prisma";
import { getDefaultScheduleId, hasDefaultSchedule, setupDefaultSchedule } from "./util";
import { ScheduleRepository } from "./ScheduleRepository";
vi.mock("@calcom/prisma", () => {
const mockPrisma = {
@@ -14,9 +13,6 @@ vi.mock("@calcom/prisma", () => {
schedule: {
findFirst: vi.fn(),
},
host: {
updateMany: vi.fn(),
},
};
return {
__esModule: true,
@@ -25,16 +21,36 @@ vi.mock("@calcom/prisma", () => {
};
});
describe("Availability Utils", () => {
vi.mock("@calcom/lib/hasEditPermissionForUser", () => ({
hasReadPermissionsForUserId: vi.fn().mockResolvedValue(true),
}));
describe("ScheduleRepository", () => {
let prisma: PrismaClient;
let hostRepo: HostRepository;
let scheduleRepository: ScheduleRepository;
beforeEach(() => {
prisma = new PrismaClient();
hostRepo = new HostRepository(prisma);
scheduleRepository = new ScheduleRepository(prisma);
vi.clearAllMocks();
});
describe("constructor", () => {
it("should throw error if prismaClient is not provided", () => {
expect(() => new ScheduleRepository(null as any)).toThrow(
"PrismaClient is required for ScheduleRepository"
);
expect(() => new ScheduleRepository(undefined as any)).toThrow(
"PrismaClient is required for ScheduleRepository"
);
});
it("should create instance successfully with valid prismaClient", () => {
const repo = new ScheduleRepository(prisma);
expect(repo).toBeInstanceOf(ScheduleRepository);
});
});
describe("getDefaultScheduleId", () => {
it("should return defaultScheduleId if user has one", async () => {
const userId = 1;
@@ -44,7 +60,7 @@ describe("Availability Utils", () => {
defaultScheduleId,
});
const result = await getDefaultScheduleId(userId, prisma);
const result = await scheduleRepository.getDefaultScheduleId(userId);
expect(prisma.user.findUnique).toHaveBeenCalledWith({
where: { id: userId },
@@ -65,7 +81,7 @@ describe("Availability Utils", () => {
id: scheduleId,
});
const result = await getDefaultScheduleId(userId, prisma);
const result = await scheduleRepository.getDefaultScheduleId(userId);
expect(prisma.user.findUnique).toHaveBeenCalledWith({
where: { id: userId },
@@ -87,7 +103,9 @@ describe("Availability Utils", () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(prisma.schedule.findFirst as any).mockResolvedValue(null);
await expect(getDefaultScheduleId(userId, prisma)).rejects.toThrow("No schedules found for user");
await expect(scheduleRepository.getDefaultScheduleId(userId)).rejects.toThrow(
"No schedules found for user"
);
});
});
@@ -95,7 +113,7 @@ describe("Availability Utils", () => {
it("should return true if user has defaultScheduleId", async () => {
const user = { id: 1, defaultScheduleId: 123 };
const result = await hasDefaultSchedule(user, prisma);
const result = await scheduleRepository.hasDefaultSchedule(user);
expect(result).toBe(true);
});
@@ -107,7 +125,7 @@ describe("Availability Utils", () => {
id: 456,
});
const result = await hasDefaultSchedule(user, prisma);
const result = await scheduleRepository.hasDefaultSchedule(user);
expect(prisma.schedule.findFirst).toHaveBeenCalledWith({
where: { userId: user.id },
@@ -120,7 +138,7 @@ describe("Availability Utils", () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(prisma.schedule.findFirst as any).mockResolvedValue(null);
const result = await hasDefaultSchedule(user, prisma);
const result = await scheduleRepository.hasDefaultSchedule(user);
expect(prisma.schedule.findFirst).toHaveBeenCalledWith({
where: { userId: user.id },
@@ -137,7 +155,7 @@ describe("Availability Utils", () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(prisma.user.update as any).mockResolvedValue(updatedUser);
const result = await setupDefaultSchedule(userId, scheduleId, prisma);
const result = await scheduleRepository.setupDefaultSchedule(userId, scheduleId);
expect(prisma.user.update).toHaveBeenCalledWith({
where: { id: userId },
@@ -146,50 +164,4 @@ describe("Availability Utils", () => {
expect(result).toEqual(updatedUser);
});
});
describe("updateHostsWithNewDefaultSchedule", () => {
it("should update hosts with new scheduleId", async () => {
const userId = 1;
const oldScheduleId = 123;
const newScheduleId = 456;
const updateResult = { count: 2 }; // 2 hosts updated
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(prisma.host.updateMany as any).mockResolvedValue(updateResult);
const result = await hostRepo.updateHostsSchedule(userId, oldScheduleId, newScheduleId);
expect(prisma.host.updateMany).toHaveBeenCalledWith({
where: {
userId,
scheduleId: oldScheduleId,
},
data: {
scheduleId: newScheduleId,
},
});
expect(result).toEqual(updateResult);
});
it("should handle case with no hosts to update", async () => {
const userId = 1;
const oldScheduleId = 123;
const newScheduleId = 456;
const updateResult = { count: 0 }; // No hosts updated
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(prisma.host.updateMany as any).mockResolvedValue(updateResult);
const result = await hostRepo.updateHostsSchedule(userId, oldScheduleId, newScheduleId);
expect(prisma.host.updateMany).toHaveBeenCalledWith({
where: {
userId,
scheduleId: oldScheduleId,
},
data: {
scheduleId: newScheduleId,
},
});
expect(result).toEqual(updateResult);
});
});
});
@@ -5,16 +5,19 @@ import {
transformWorkingHoursForAtom,
} from "@calcom/lib/schedules/transformers";
import type { PrismaClient } from "@calcom/prisma";
import prisma from "@calcom/prisma";
import { getDefaultScheduleId } from "@calcom/trpc/server/routers/viewer/availability/util";
import type { User } from "@calcom/prisma/client";
export type FindDetailedScheduleByIdReturnType = Awaited<
ReturnType<typeof ScheduleRepository.findDetailedScheduleById>
ReturnType<ScheduleRepository["findDetailedScheduleById"]>
>;
export class ScheduleRepository {
// when instantiating, prismaClient injection is required
constructor(private prismaClient: PrismaClient) {}
constructor(private readonly prismaClient: PrismaClient) {
if (!prismaClient) {
throw new Error("PrismaClient is required for ScheduleRepository");
}
}
async findScheduleByIdForBuildDateRanges({ scheduleId }: { scheduleId: number }) {
const schedule = await this.prismaClient.schedule.findUnique({
@@ -63,8 +66,8 @@ export class ScheduleRepository {
return schedule;
}
static async findScheduleById({ id }: { id: number }) {
const schedule = await prisma.schedule.findUnique({
async findScheduleById({ id }: { id: number }) {
const schedule = await this.prismaClient.schedule.findUnique({
where: {
id,
},
@@ -80,7 +83,7 @@ export class ScheduleRepository {
return schedule;
}
static async findDetailedScheduleById({
async findDetailedScheduleById({
isManagedEventType,
scheduleId,
userId,
@@ -93,9 +96,9 @@ export class ScheduleRepository {
scheduleId?: number;
isManagedEventType?: boolean;
}) {
const schedule = await prisma.schedule.findUnique({
const schedule = await this.prismaClient.schedule.findUnique({
where: {
id: scheduleId || (await getDefaultScheduleId(userId, prisma)),
id: scheduleId || (await this.getDefaultScheduleId(userId)),
},
select: {
id: true,
@@ -119,7 +122,7 @@ export class ScheduleRepository {
const timeZone = schedule.timeZone || userTimeZone;
const schedulesCount = await prisma.schedule.count({
const schedulesCount = await this.prismaClient.schedule.count({
where: {
userId: userId,
},
@@ -141,7 +144,7 @@ export class ScheduleRepository {
};
}
static async findManyDetailedScheduleByUserId({
async findManyDetailedScheduleByUserId({
isManagedEventType,
userId,
defaultScheduleId,
@@ -154,7 +157,7 @@ export class ScheduleRepository {
isManagedEventType?: boolean;
}) {
const schedules = await prisma.schedule.findMany({
const schedules = await this.prismaClient.schedule.findMany({
where: {
userId: userId,
},
@@ -203,4 +206,56 @@ export class ScheduleRepository {
return schedulesFormatted;
}
async getDefaultScheduleId(userId: number) {
const user = await this.prismaClient.user.findUnique({
where: {
id: userId,
},
select: {
defaultScheduleId: true,
},
});
if (user?.defaultScheduleId) {
return user.defaultScheduleId;
}
// If we're returning the default schedule for the first time then we should set it in the user record
const defaultSchedule = await this.prismaClient.schedule.findFirst({
where: {
userId,
},
select: {
id: true,
},
});
if (!defaultSchedule) {
// Handle case where defaultSchedule is null by throwing an error
throw new Error("No schedules found for user");
}
return defaultSchedule.id;
}
async hasDefaultSchedule(user: Partial<User>) {
const defaultSchedule = await this.prismaClient.schedule.findFirst({
where: {
userId: user.id,
},
});
return !!user.defaultScheduleId || !!defaultSchedule;
}
async setupDefaultSchedule(userId: number, scheduleId: number) {
return this.prismaClient.user.update({
where: {
id: userId,
},
data: {
defaultScheduleId: scheduleId,
},
});
}
}
@@ -4,9 +4,10 @@ import { HttpError } from "@calcom/lib/http-error";
import { transformScheduleToAvailabilityForAtom } from "@calcom/lib/schedules/transformers/for-atom";
import type { PrismaClient } from "@calcom/prisma";
import type { TUpdateInputSchema } from "@calcom/trpc/server/routers/viewer/availability/schedule/update.schema";
import { setupDefaultSchedule } from "@calcom/trpc/server/routers/viewer/availability/util";
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
import { ScheduleRepository } from "../repositories/ScheduleRepository";
interface IUpdateScheduleOptions {
input: TUpdateInputSchema;
user: Pick<NonNullable<TrpcSessionUser>, "id" | "defaultScheduleId" | "timeZone">;
@@ -64,7 +65,9 @@ export class ScheduleService {
let updatedUser;
if (input.isDefault) {
const setupDefault = await setupDefaultSchedule(user.id, input.scheduleId, this.prisma);
const scheduleRepo = new ScheduleRepository(this.prisma);
const setupDefault = await scheduleRepo.setupDefaultSchedule(user.id, input.scheduleId);
updatedUser = setupDefault;
}
@@ -0,0 +1,76 @@
import { describe, expect, it, vi, beforeEach } from "vitest";
import { PrismaClient } from "@calcom/prisma";
import { HostRepository } from "./host";
vi.mock("@calcom/prisma", () => {
const mockPrisma = {
host: {
updateMany: vi.fn(),
findMany: vi.fn(),
},
};
return {
__esModule: true,
default: mockPrisma,
PrismaClient: vi.fn(() => mockPrisma),
};
});
describe("HostRepository", () => {
let prisma: PrismaClient;
let hostRepo: HostRepository;
beforeEach(() => {
prisma = new PrismaClient();
hostRepo = new HostRepository(prisma);
vi.clearAllMocks();
});
describe("updateHostsWithNewDefaultSchedule", () => {
it("should update hosts with new scheduleId", async () => {
const userId = 1;
const oldScheduleId = 123;
const newScheduleId = 456;
const updateResult = { count: 2 }; // 2 hosts updated
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(prisma.host.updateMany as any).mockResolvedValue(updateResult);
const result = await hostRepo.updateHostsSchedule(userId, oldScheduleId, newScheduleId);
expect(prisma.host.updateMany).toHaveBeenCalledWith({
where: {
userId,
scheduleId: oldScheduleId,
},
data: {
scheduleId: newScheduleId,
},
});
expect(result).toEqual(updateResult);
});
it("should handle case with no hosts to update", async () => {
const userId = 1;
const oldScheduleId = 123;
const newScheduleId = 456;
const updateResult = { count: 0 }; // No hosts updated
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(prisma.host.updateMany as any).mockResolvedValue(updateResult);
const result = await hostRepo.updateHostsSchedule(userId, oldScheduleId, newScheduleId);
expect(prisma.host.updateMany).toHaveBeenCalledWith({
where: {
userId,
scheduleId: oldScheduleId,
},
data: {
scheduleId: newScheduleId,
},
});
expect(result).toEqual(updateResult);
});
});
});
@@ -1,7 +1,7 @@
import { ScheduleRepository } from "@calcom/features/schedules/repositories/ScheduleRepository";
import { prisma } from "@calcom/prisma";
import type { TrpcSessionUser } from "../../../types";
import { getDefaultScheduleId } from "./util";
type ListOptions = {
ctx: {
@@ -37,7 +37,8 @@ export const listHandler = async ({ ctx }: ListOptions) => {
let defaultScheduleId: number | null;
try {
defaultScheduleId = await getDefaultScheduleId(user.id, prisma);
const scheduleRepository = new ScheduleRepository(prisma);
defaultScheduleId = await scheduleRepository.getDefaultScheduleId(user.id);
if (!user.defaultScheduleId) {
await prisma.user.update({
@@ -1,4 +1,5 @@
import { ScheduleRepository } from "@calcom/features/schedules/repositories/ScheduleRepository";
import { prisma } from "@calcom/prisma";
import type { TrpcSessionUser } from "../../../../types";
import type { TGetInputSchema } from "./get.schema";
@@ -11,7 +12,8 @@ type GetOptions = {
};
export const getHandler = async ({ ctx, input }: GetOptions) => {
return await ScheduleRepository.findDetailedScheduleById({
const scheduleRepo = new ScheduleRepository(prisma);
return await scheduleRepo.findDetailedScheduleById({
scheduleId: input.scheduleId,
isManagedEventType: input.isManagedEventType,
userId: ctx.user.id,
@@ -1,3 +1,4 @@
import { ScheduleRepository } from "@calcom/features/schedules/repositories/ScheduleRepository";
import { hasReadPermissionsForUserId } from "@calcom/lib/hasEditPermissionForUser";
import logger from "@calcom/lib/logger";
import { prisma } from "@calcom/prisma";
@@ -5,7 +6,6 @@ import { prisma } from "@calcom/prisma";
import { TRPCError } from "@trpc/server";
import type { TrpcSessionUser } from "../../../../types";
import { getDefaultScheduleId } from "../util";
import type { TGetAllByUserIdInputSchema } from "./getAllSchedulesByUserId.schema";
const log = logger.getSubLogger({ prefix: ["getAllSchedulesByUserIdHandler"] });
@@ -48,7 +48,8 @@ export const getAllSchedulesByUserIdHandler = async ({ ctx, input }: GetOptions)
});
}
const defaultScheduleId = await getDefaultScheduleId(input.userId, prisma);
const scheduleRepository = new ScheduleRepository(prisma);
const defaultScheduleId = await scheduleRepository.getDefaultScheduleId(input.userId);
return {
schedules: schedules.map((schedule) => {
@@ -1,54 +0,0 @@
import type { PrismaClient } from "@calcom/prisma";
import type { User } from "@calcom/prisma/client";
export const getDefaultScheduleId = async (userId: number, prisma: PrismaClient) => {
const user = await prisma.user.findUnique({
where: {
id: userId,
},
select: {
defaultScheduleId: true,
},
});
if (user?.defaultScheduleId) {
return user.defaultScheduleId;
}
// If we're returning the default schedule for the first time then we should set it in the user record
const defaultSchedule = await prisma.schedule.findFirst({
where: {
userId,
},
select: {
id: true,
},
});
if (!defaultSchedule) {
// Handle case where defaultSchedule is null by throwing an error
throw new Error("No schedules found for user");
}
return defaultSchedule.id;
};
export const hasDefaultSchedule = async (user: Partial<User>, prisma: PrismaClient) => {
const defaultSchedule = await prisma.schedule.findFirst({
where: {
userId: user.id,
},
});
return !!user.defaultScheduleId || !!defaultSchedule;
};
export const setupDefaultSchedule = async (userId: number, scheduleId: number, prisma: PrismaClient) => {
return prisma.user.update({
where: {
id: userId,
},
data: {
defaultScheduleId: scheduleId,
},
});
};
@@ -7,6 +7,7 @@ import { sendChangeOfEmailVerification } from "@calcom/features/auth/lib/verifyE
import { updateNewTeamMemberEventTypes } from "@calcom/features/ee/teams/lib/queries";
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import { checkUsername } from "@calcom/features/profile/lib/checkUsername";
import { ScheduleRepository } from "@calcom/features/schedules/repositories/ScheduleRepository";
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
import { HttpError } from "@calcom/lib/http-error";
import logger from "@calcom/lib/logger";
@@ -22,7 +23,6 @@ import type { TrpcSessionUser } from "@calcom/trpc/server/types";
import { TRPCError } from "@trpc/server";
import { getDefaultScheduleId } from "../availability/util";
import { updateUserMetadataAllowedKeys, type TUpdateProfileInputSchema } from "./updateProfile.schema";
const log = logger.getSubLogger({ prefix: ["updateProfile"] });
@@ -263,7 +263,8 @@ export const updateProfileHandler = async ({ ctx, input }: UpdateProfileOptions)
if (user.timeZone !== data.timeZone && updatedUser.schedules.length > 0) {
// on timezone change update timezone of default schedule
const defaultScheduleId = await getDefaultScheduleId(user.id, prisma);
const scheduleRepository = new ScheduleRepository(prisma);
const defaultScheduleId = await scheduleRepository.getDefaultScheduleId(user.id);
if (!user.defaultScheduleId) {
// set default schedule if not already set