refactor: convert EventTypeRepository to use dependency injection pattern (#22397)

- Add constructor with PrismaClient dependency injection
- Convert all 14 static methods to instance methods using this.prismaClient
- Update all usage sites to use two-step instantiation pattern:
  const eventTypeRepo = new EventTypeRepository(prisma); eventTypeRepo.method(...)
- Keep getSelectedCalendarsFromUser as static utility method
- Follow same pattern as UserRepository, TeamRepository, SelectedSlotsRepository, and BookingRepository
- Maintain all existing method signatures and functionality

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: morgan@cal.com <morgan@cal.com>
This commit is contained in:
devin-ai-integration[bot]
2025-07-11 10:39:22 +03:00
committed by GitHub
co-authored by morgan@cal.com
parent b5911a9a2d
commit 758143ef70
14 changed files with 68 additions and 59 deletions
@@ -94,7 +94,8 @@ const checkIfUserIsHost = async ({
return booking.user?.id === sessionUserId;
}
const eventType = await EventTypeRepository.findByIdWithUserAccess({
const eventTypeRepo = new EventTypeRepository(prisma);
const eventType = await eventTypeRepo.findByIdWithUserAccess({
id: booking.eventTypeId,
userId: sessionUserId,
});
@@ -1,5 +1,6 @@
import { getCRMContactOwnerForRRLeadSkip } from "@calcom/app-store/_utils/CRMRoundRobinSkip";
import { EventTypeRepository } from "@calcom/lib/server/repository/eventType";
import { prisma } from "@calcom/prisma";
import { SchedulingType } from "@calcom/prisma/enums";
import type { LocalRoute } from "../../types/types";
@@ -32,7 +33,8 @@ export default async function routerGetCrmContactOwnerEmail({
// Determine if the action is an event type redirect
if (action.type !== "eventTypeRedirectUrl" || !action.eventTypeId) return null;
const eventType = await EventTypeRepository.findByIdIncludeHostsAndTeam({ id: action.eventTypeId });
const eventTypeRepo = new EventTypeRepository(prisma);
const eventType = await eventTypeRepo.findByIdIncludeHostsAndTeam({ id: action.eventTypeId });
if (!eventType || eventType.schedulingType !== SchedulingType.ROUND_ROBIN) return null;
const eventTypeMetadata = eventType.metadata;
@@ -61,7 +61,8 @@ describe("deleteCredential", () => {
await setupCredential({ userId: user.id, type: "zoom_video", appId: "zoom" });
await handleDeleteCredential({ userId: user.id, userMetadata: user.metadata, credentialId: 123 });
const eventTypeQuery = await EventTypeRepository.findAllByUserId({ userId: user.id });
const eventTypeRepo = new EventTypeRepository(prisma);
const eventTypeQuery = await eventTypeRepo.findAllByUserId({ userId: user.id });
// Ensure that the event type with the deleted app was converted back to daily
const changedEventType = eventTypeQuery.find((eventType) => eventType.id === 1)?.locations;
+2 -1
View File
@@ -52,7 +52,8 @@ export const getEventTypeById = async ({
isPlatformManaged: true,
} satisfies Prisma.UserSelect;
const rawEventType = await EventTypeRepository.findById({ id: eventTypeId, userId });
const eventTypeRepo = new EventTypeRepository(prisma);
const rawEventType = await eventTypeRepo.findById({ id: eventTypeId, userId });
if (!rawEventType) {
if (isTrpcCall) {
@@ -51,6 +51,7 @@ export const getEventTypesByViewer = async (user: User, filters?: Filters, forRo
if (isFilterSet && filters?.upIds && !isUpIdInFilter) {
shouldListUserEvents = true;
}
const eventTypeRepo = new EventTypeRepository(prisma);
const [profileMemberships, profileEventTypes] = await Promise.all([
MembershipRepository.findAllByUpIdIncludeTeamWithMembersAndEventTypes(
{
@@ -63,7 +64,7 @@ export const getEventTypesByViewer = async (user: User, filters?: Filters, forRo
}
),
shouldListUserEvents
? EventTypeRepository.findAllByUpId(
? eventTypeRepo.findAllByUpId(
{
upId: userProfile.upId,
userId: user.id,
+37 -42
View File
@@ -1,8 +1,9 @@
import type { EventType as PrismaEventType } from "@prisma/client";
import type { Prisma } from "@prisma/client";
import logger from "@calcom/lib/logger";
import type { PrismaClient } from "@calcom/prisma";
import { prisma, availabilityUserSelect } from "@calcom/prisma";
import type { Prisma } from "@calcom/prisma/client";
import { MembershipRole } from "@calcom/prisma/enums";
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
import { EventTypeMetaDataSchema, rrSegmentQueryValueSchema } from "@calcom/prisma/zod-utils";
@@ -68,7 +69,9 @@ function usersWithSelectedCalendars<
}
export class EventTypeRepository {
private static generateCreateEventTypeData = (eventTypeCreateData: IEventType) => {
constructor(private prismaClient: PrismaClient) {}
private generateCreateEventTypeData = (eventTypeCreateData: IEventType) => {
const {
userId,
profileId,
@@ -122,8 +125,8 @@ export class EventTypeRepository {
};
};
static async create(data: IEventType) {
return await prisma.eventType.create({
async create(data: IEventType) {
return await this.prismaClient.eventType.create({
data: this.generateCreateEventTypeData(data),
include: {
calVideoSettings: true,
@@ -131,13 +134,13 @@ export class EventTypeRepository {
});
}
static async createMany(data: IEventType[]) {
return await prisma.eventType.createMany({
async createMany(data: IEventType[]) {
return await this.prismaClient.eventType.createMany({
data: data.map((d) => this.generateCreateEventTypeData(d)),
});
}
static async findAllByUpId(
async findAllByUpId(
{ upId, userId }: { upId: string; userId: number },
{
orderBy,
@@ -184,7 +187,7 @@ export class EventTypeRepository {
if (!profileId) {
// Lookup is by userId
return await prisma.eventType.findMany({
return await this.prismaClient.eventType.findMany({
where: {
userId: lookupTarget.id,
...where,
@@ -200,7 +203,7 @@ export class EventTypeRepository {
if (profile?.movedFromUser) {
// Because the user has been moved to this profile, we need to get all user events except those that belong to some other profile
// This is because those event-types that are created after moving to profile would have profileId but existing event-types would have profileId set to null
return await prisma.eventType.findMany({
return await this.prismaClient.eventType.findMany({
where: {
OR: [
// Existing events
@@ -228,7 +231,7 @@ export class EventTypeRepository {
orderBy,
});
} else {
return await prisma.eventType.findMany({
return await this.prismaClient.eventType.findMany({
where: {
OR: [
{
@@ -252,7 +255,7 @@ export class EventTypeRepository {
}
}
static async findAllByUpIdWithMinimalData(
async findAllByUpIdWithMinimalData(
{ upId, userId }: { upId: string; userId: number },
{
orderBy,
@@ -288,7 +291,7 @@ export class EventTypeRepository {
if (!profileId) {
// Lookup is by userId
return await prisma.eventType.findMany({
return await this.prismaClient.eventType.findMany({
where: {
userId: lookupTarget.id,
...where,
@@ -304,7 +307,7 @@ export class EventTypeRepository {
if (profile?.movedFromUser) {
// Because the user has been moved to this profile, we need to get all user events except those that belong to some other profile
// This is because those event-types that are created after moving to profile would have profileId but existing event-types would have profileId set to null
return await prisma.eventType.findMany({
return await this.prismaClient.eventType.findMany({
where: {
OR: [
// Existing events
@@ -332,7 +335,7 @@ export class EventTypeRepository {
orderBy,
});
} else {
return await prisma.eventType.findMany({
return await this.prismaClient.eventType.findMany({
where: {
OR: [
{
@@ -356,7 +359,7 @@ export class EventTypeRepository {
}
}
static async findTeamEventTypes({
async findTeamEventTypes({
teamId,
parentId,
userId,
@@ -397,7 +400,7 @@ export class EventTypeRepository {
},
};
const teamMembership = await prisma.membership.findFirst({
const teamMembership = await this.prismaClient.membership.findFirst({
where: {
OR: [
{
@@ -437,16 +440,16 @@ export class EventTypeRepository {
});
}
static async findAllByUserId({ userId }: { userId: number }) {
return await prisma.eventType.findMany({
async findAllByUserId({ userId }: { userId: number }) {
return await this.prismaClient.eventType.findMany({
where: {
userId,
},
});
}
static async findTitleById({ id }: { id: number }) {
return await prisma.eventType.findUnique({
async findTitleById({ id }: { id: number }) {
return await this.prismaClient.eventType.findUnique({
where: {
id,
},
@@ -456,8 +459,8 @@ export class EventTypeRepository {
});
}
static async findByIdWithUserAccess({ id, userId }: { id: number; userId: number }) {
return await prisma.eventType.findUnique({
async findByIdWithUserAccess({ id, userId }: { id: number; userId: number }) {
return await this.prismaClient.eventType.findUnique({
where: {
id,
OR: [{ userId }, { hosts: { some: { userId } } }, { users: { some: { id: userId } } }],
@@ -465,7 +468,7 @@ export class EventTypeRepository {
});
}
static async findById({ id, userId }: { id: number; userId: number }) {
async findById({ id, userId }: { id: number; userId: number }) {
const userSelect = {
name: true,
avatarUrl: true,
@@ -716,7 +719,7 @@ export class EventTypeRepository {
// This is more efficient than using a complex join with team.members in the query
const userTeamIds = await MembershipRepository.findUserTeamIds({ userId });
return await prisma.eventType.findFirst({
return await this.prismaClient.eventType.findFirst({
where: {
AND: [
{
@@ -745,24 +748,16 @@ export class EventTypeRepository {
});
}
static async findByIdMinimal({ id }: { id: number }) {
return await prisma.eventType.findUnique({
async findByIdMinimal({ id }: { id: number }) {
return await this.prismaClient.eventType.findUnique({
where: {
id,
},
});
}
static async findFirstEventTypeId({
slug,
teamId,
userId,
}: {
slug: string;
teamId?: number;
userId?: number;
}) {
return prisma.eventType.findFirst({
async findFirstEventTypeId({ slug, teamId, userId }: { slug: string; teamId?: number; userId?: number }) {
return this.prismaClient.eventType.findFirst({
where: {
slug,
...(teamId ? { teamId } : {}),
@@ -774,8 +769,8 @@ export class EventTypeRepository {
});
}
static async findByIdIncludeHostsAndTeam({ id }: { id: number }) {
const eventType = await prisma.eventType.findUnique({
async findByIdIncludeHostsAndTeam({ id }: { id: number }) {
const eventType = await this.prismaClient.eventType.findUnique({
where: {
id,
},
@@ -818,8 +813,8 @@ export class EventTypeRepository {
};
}
static async findAllByTeamIdIncludeManagedEventTypes({ teamId }: { teamId?: number }) {
return await prisma.eventType.findMany({
async findAllByTeamIdIncludeManagedEventTypes({ teamId }: { teamId?: number }) {
return await this.prismaClient.eventType.findMany({
where: {
OR: [
{
@@ -835,8 +830,8 @@ export class EventTypeRepository {
});
}
static async findForSlots({ id }: { id: number }) {
const eventType = await prisma.eventType.findUnique({
async findForSlots({ id }: { id: number }) {
const eventType = await this.prismaClient.eventType.findUnique({
where: {
id,
},
@@ -126,7 +126,8 @@ export const createHandler = async ({ ctx, input }: CreateOptions) => {
const profile = ctx.user.profile;
try {
const eventType = await EventTypeRepository.create({
const eventTypeRepo = new EventTypeRepository(ctx.prisma);
const eventType = await eventTypeRepo.create({
...data,
profileId: profile.id,
});
@@ -151,7 +151,8 @@ export const duplicateHandler = async ({ ctx, input }: DuplicateOptions) => {
}
}
const newEventType = await EventTypeRepository.create(data);
const eventTypeRepo = new EventTypeRepository(prisma);
const newEventType = await eventTypeRepo.create(data);
// Create custom inputs
if (customInputs) {
@@ -21,7 +21,7 @@ type GetByViewerOptions = {
input: TGetEventTypesFromGroupSchema;
};
type EventType = Awaited<ReturnType<typeof EventTypeRepository.findAllByUpId>>[number];
type EventType = Awaited<ReturnType<EventTypeRepository["findAllByUpId"]>>[number];
type MappedEventType = Awaited<ReturnType<typeof mapEventType>>;
export const getEventTypesFromGroup = async ({
@@ -47,6 +47,7 @@ export const getEventTypesFromGroup = async ({
!isFilterSet || isUpIdInFilter || (isFilterSet && filters?.upIds && !isUpIdInFilter);
const eventTypes: EventType[] = [];
const eventTypeRepo = new EventTypeRepository(ctx.prisma);
if (shouldListUserEvents || !teamId) {
const baseQueryConditions = {
@@ -56,7 +57,7 @@ export const getEventTypesFromGroup = async ({
};
const [nonChildEventTypes, childEventTypes] = await Promise.all([
EventTypeRepository.findAllByUpId(
eventTypeRepo.findAllByUpId(
{
upId: userProfile.upId,
userId: ctx.user.id,
@@ -78,7 +79,7 @@ export const getEventTypesFromGroup = async ({
cursor,
}
),
EventTypeRepository.findAllByUpId(
eventTypeRepo.findAllByUpId(
{
upId: userProfile.upId,
userId: ctx.user.id,
@@ -117,7 +118,7 @@ export const getEventTypesFromGroup = async ({
if (teamId) {
const teamEventTypes =
(await EventTypeRepository.findTeamEventTypes({
(await eventTypeRepo.findTeamEventTypes({
teamId,
parentId,
userId: ctx.user.id,
@@ -53,6 +53,7 @@ export const getTeamAndEventTypeOptions = async ({ ctx, input }: GetTeamAndEvent
const parentOrgHasLockedEventTypes =
profile?.organization?.organizationSettings?.lockEventTypeCreationForUsers;
const eventTypeRepo = new EventTypeRepository(ctx.prisma);
const [profileMemberships, profileEventTypes] = await Promise.all([
MembershipRepository.findAllByUpIdIncludeMinimalEventTypes(
{
@@ -67,7 +68,7 @@ export const getTeamAndEventTypeOptions = async ({ ctx, input }: GetTeamAndEvent
),
teamId
? []
: EventTypeRepository.findAllByUpIdWithMinimalData(
: eventTypeRepo.findAllByUpIdWithMinimalData(
{
upId: userProfile.upId,
userId: user.id,
@@ -14,7 +14,7 @@ import { TRPCError } from "@trpc/server";
import authedProcedure from "../../../procedures/authedProcedure";
import type { TUpdateInputSchema } from "./types";
type EventType = Awaited<ReturnType<typeof EventTypeRepository.findAllByUpId>>[number];
type EventType = Awaited<ReturnType<EventTypeRepository["findAllByUpId"]>>[number];
export const eventOwnerProcedure = authedProcedure
.input(
@@ -177,7 +177,8 @@ export const findTeamMembersMatchingAttributeLogicOfRouteHandler = async ({
});
}
const eventType = await EventTypeRepository.findByIdIncludeHostsAndTeam({ id: eventTypeId });
const eventTypeRepo = new EventTypeRepository(prisma);
const eventType = await eventTypeRepo.findByIdIncludeHostsAndTeam({ id: eventTypeId });
if (!eventType) {
throw new TRPCError({
@@ -32,7 +32,8 @@ export const isAvailableHandler = async ({
const { slots, eventTypeId } = input;
// Get event type details for time bounds validation
const eventType = await EventTypeRepository.findByIdMinimal({ id: eventTypeId });
const eventTypeRepo = new EventTypeRepository(ctx.prisma);
const eventType = await eventTypeRepo.findByIdMinimal({ id: eventTypeId });
if (!eventType) {
throw new HttpError({ statusCode: 404, message: "Event type not found" });
@@ -211,7 +211,8 @@ export class AvailableSlotsService {
return null;
}
return await EventTypeRepository.findForSlots({ id: eventTypeId });
const eventTypeRepo = new EventTypeRepository(prisma);
return await eventTypeRepo.findForSlots({ id: eventTypeId });
}
private getEventType = withReporting(this._getEventType.bind(this), "getEventType");
@@ -281,7 +282,8 @@ export class AvailableSlotsService {
organizationDetails ?? { currentOrgDomain: null, isValidOrgDomain: false }
);
}
const eventType = await EventTypeRepository.findFirstEventTypeId({ slug: eventTypeSlug, teamId, userId });
const eventTypeRepo = new EventTypeRepository(prisma);
const eventType = await eventTypeRepo.findFirstEventTypeId({ slug: eventTypeSlug, teamId, userId });
if (!eventType) {
throw new TRPCError({ code: "NOT_FOUND" });
}