From 52940ce2546cfb4f7e2082b7b376ba0f75763a69 Mon Sep 17 00:00:00 2001 From: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Date: Mon, 7 Jul 2025 20:57:01 +0300 Subject: [PATCH] refactor: getAvailableSlots use repositories (#22290) --- apps/api/v2/package.json | 2 +- packages/lib/server/repository/eventType.ts | 21 ++ packages/lib/server/repository/ooo.ts | 82 ++++++++ .../lib/server/repository/selectedSlots.ts | 39 ++++ packages/lib/server/repository/team.ts | 18 ++ packages/lib/server/repository/user.ts | 27 ++- .../trpc/server/routers/viewer/slots/util.ts | 197 +++--------------- yarn.lock | 10 +- 8 files changed, 225 insertions(+), 171 deletions(-) create mode 100644 packages/lib/server/repository/ooo.ts diff --git a/apps/api/v2/package.json b/apps/api/v2/package.json index 8f0d567894..6fda2e479c 100644 --- a/apps/api/v2/package.json +++ b/apps/api/v2/package.json @@ -38,7 +38,7 @@ "@axiomhq/winston": "^1.2.0", "@calcom/platform-constants": "*", "@calcom/platform-enums": "*", - "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.239", + "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.241", "@calcom/platform-types": "*", "@calcom/platform-utils": "*", "@calcom/prisma": "*", diff --git a/packages/lib/server/repository/eventType.ts b/packages/lib/server/repository/eventType.ts index e2314195b0..f4b9fe33ee 100644 --- a/packages/lib/server/repository/eventType.ts +++ b/packages/lib/server/repository/eventType.ts @@ -753,6 +753,27 @@ export class EventTypeRepository { }); } + static async findFirstEventTypeId({ + slug, + teamId, + userId, + }: { + slug: string; + teamId?: number; + userId?: number; + }) { + return prisma.eventType.findFirst({ + where: { + slug, + ...(teamId ? { teamId } : {}), + ...(userId ? { userId } : {}), + }, + select: { + id: true, + }, + }); + } + static async findByIdIncludeHostsAndTeam({ id }: { id: number }) { const eventType = await prisma.eventType.findUnique({ where: { diff --git a/packages/lib/server/repository/ooo.ts b/packages/lib/server/repository/ooo.ts new file mode 100644 index 0000000000..a3b79626d3 --- /dev/null +++ b/packages/lib/server/repository/ooo.ts @@ -0,0 +1,82 @@ +import type { PrismaClient } from "@calcom/prisma"; + +export class PrismaOOORepository { + constructor(private prismaClient: PrismaClient) {} + + async findManyOOO({ + startTimeDate, + endTimeDate, + allUserIds, + }: { + startTimeDate: Date; + endTimeDate: Date; + allUserIds: number[]; + }) { + return this.prismaClient.outOfOfficeEntry.findMany({ + where: { + userId: { + in: allUserIds, + }, + OR: [ + // outside of range + // (start <= 'dateTo' AND end >= 'dateFrom') + { + start: { + lte: endTimeDate, + }, + end: { + gte: startTimeDate, + }, + }, + // start is between dateFrom and dateTo but end is outside of range + // (start <= 'dateTo' AND end >= 'dateTo') + { + start: { + lte: endTimeDate, + }, + + end: { + gte: endTimeDate, + }, + }, + // end is between dateFrom and dateTo but start is outside of range + // (start <= 'dateFrom' OR end <= 'dateTo') + { + start: { + lte: startTimeDate, + }, + + end: { + lte: endTimeDate, + }, + }, + ], + }, + select: { + id: true, + start: true, + end: true, + user: { + select: { + id: true, + name: true, + }, + }, + toUser: { + select: { + id: true, + username: true, + name: true, + }, + }, + reason: { + select: { + id: true, + emoji: true, + reason: true, + }, + }, + }, + }); + } +} diff --git a/packages/lib/server/repository/selectedSlots.ts b/packages/lib/server/repository/selectedSlots.ts index 09385e3806..c99d85a890 100644 --- a/packages/lib/server/repository/selectedSlots.ts +++ b/packages/lib/server/repository/selectedSlots.ts @@ -70,4 +70,43 @@ export class SelectedSlotsRepository { }, }); } + + static async findManyUnexpiredSlots({ + userIds, + currentTimeInUtc, + }: { + userIds: number[]; + currentTimeInUtc: string; + }) { + return prisma.selectedSlots.findMany({ + where: { + userId: { in: userIds }, + releaseAt: { gt: currentTimeInUtc }, + }, + select: { + id: true, + slotUtcStartDate: true, + slotUtcEndDate: true, + userId: true, + isSeat: true, + eventTypeId: true, + uid: true, + }, + }); + } + + static async deleteManyExpiredSlots({ + eventTypeId, + currentTimeInUtc, + }: { + eventTypeId: number; + currentTimeInUtc: string; + }) { + return prisma.selectedSlots.deleteMany({ + where: { + eventTypeId: { equals: eventTypeId }, + releaseAt: { lt: currentTimeInUtc }, + }, + }); + } } diff --git a/packages/lib/server/repository/team.ts b/packages/lib/server/repository/team.ts index 2661586b78..3bc10350b0 100644 --- a/packages/lib/server/repository/team.ts +++ b/packages/lib/server/repository/team.ts @@ -212,6 +212,24 @@ export class TeamRepository { }); } + static async findFirstBySlugAndParentSlug({ + slug, + parentSlug, + select = teamSelect, + }: { + slug: string; + parentSlug: string | null; + select?: Prisma.TeamSelect; + }) { + return await prisma.team.findFirst({ + where: { + slug, + parent: parentSlug ? whereClauseForOrgWithSlugOrRequestedSlug(parentSlug) : null, + }, + select, + }); + } + static async deleteById({ id }: { id: number }) { const deletedTeam = await prisma.$transaction(async (tx) => { await tx.eventType.deleteMany({ diff --git a/packages/lib/server/repository/user.ts b/packages/lib/server/repository/user.ts index c4f0b2c681..323ae2b181 100644 --- a/packages/lib/server/repository/user.ts +++ b/packages/lib/server/repository/user.ts @@ -4,7 +4,7 @@ import { whereClauseForOrgWithSlugOrRequestedSlug } from "@calcom/ee/organizatio import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; import { getTranslation } from "@calcom/lib/server/i18n"; -import prisma from "@calcom/prisma"; +import prisma, { availabilityUserSelect } from "@calcom/prisma"; import type { Prisma } from "@calcom/prisma/client"; import type { User as UserType } from "@calcom/prisma/client"; import type { CreationSource } from "@calcom/prisma/enums"; @@ -965,4 +965,29 @@ export class UserRepository { data: { whitelistWorkflows }, }); } + + static async findManyUsersForDynamicEventType({ + currentOrgDomain, + usernameList, + }: { + currentOrgDomain: string | null; + usernameList: string[]; + }) { + const { where } = await UserRepository._getWhereClauseForFindingUsersByUsername({ + orgSlug: currentOrgDomain, + usernameList, + }); + + // TODO: Should be moved to UserRepository + return prisma.user.findMany({ + where, + select: { + allowDynamicBooking: true, + ...availabilityUserSelect, + credentials: { + select: credentialForCalendarServiceSelect, + }, + }, + }); + } } diff --git a/packages/trpc/server/routers/viewer/slots/util.ts b/packages/trpc/server/routers/viewer/slots/util.ts index 1d0b87c1bc..809cf85a17 100644 --- a/packages/trpc/server/routers/viewer/slots/util.ts +++ b/packages/trpc/server/routers/viewer/slots/util.ts @@ -4,7 +4,7 @@ import { v4 as uuid } from "uuid"; import type { Dayjs } from "@calcom/dayjs"; import dayjs from "@calcom/dayjs"; -import { getSlugOrRequestedSlug, orgDomainConfig } from "@calcom/ee/organizations/lib/orgDomains"; +import { orgDomainConfig } from "@calcom/ee/organizations/lib/orgDomains"; import { checkForConflicts } from "@calcom/features/bookings/lib/conflictChecker/checkForConflicts"; import { isEventTypeLoggingEnabled } from "@calcom/features/bookings/lib/isEventTypeLoggingEnabled"; import { getShouldServeCache } from "@calcom/features/calendar-cache/lib/getShouldServeCache"; @@ -44,15 +44,17 @@ import { getTotalBookingDuration } from "@calcom/lib/server/queries/booking"; import { BookingRepository as BookingRepo } from "@calcom/lib/server/repository/booking"; import { EventTypeRepository } from "@calcom/lib/server/repository/eventType"; import { RoutingFormResponseRepository } from "@calcom/lib/server/repository/formResponse"; +import { PrismaOOORepository } from "@calcom/lib/server/repository/ooo"; +import { ScheduleRepository } from "@calcom/lib/server/repository/schedule"; +import { SelectedSlotsRepository } from "@calcom/lib/server/repository/selectedSlots"; +import { TeamRepository } from "@calcom/lib/server/repository/team"; import { UserRepository, withSelectedCalendars } from "@calcom/lib/server/repository/user"; import getSlots from "@calcom/lib/slots"; -import prisma, { availabilityUserSelect } from "@calcom/prisma"; -import type { Prisma } from "@calcom/prisma/client"; +import prisma from "@calcom/prisma"; import { PeriodType } from "@calcom/prisma/client"; import { SchedulingType } from "@calcom/prisma/enums"; -import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential"; import type { EventBusyDate, EventBusyDetails } from "@calcom/types/Calendar"; -import type { CredentialForCalendarService, CredentialPayload } from "@calcom/types/Credential"; +import type { CredentialForCalendarService } from "@calcom/types/Credential"; import { TRPCError } from "@trpc/server"; @@ -61,25 +63,10 @@ import { handleNotificationWhenNoSlots } from "./handleNotificationWhenNoSlots"; import type { GetScheduleOptions } from "./types"; const log = logger.getSubLogger({ prefix: ["[slots/util]"] }); -type GetAvailabilityUserWithoutDelegationCredentials = Omit & { - credentials: CredentialPayload[]; -}; + type GetAvailabilityUserWithDelegationCredentials = Omit & { credentials: CredentialForCalendarService[]; }; -const selectSelectedSlots = { - select: { - id: true, - slotUtcStartDate: true, - slotUtcEndDate: true, - userId: true, - isSeat: true, - eventTypeId: true, - uid: true, - }, -} satisfies Prisma.SelectedSlotsDefaultArgs; - -type SelectedSlots = Prisma.SelectedSlotsGetPayload; async function getEventTypeId({ slug, @@ -107,16 +94,7 @@ async function getEventTypeId({ organizationDetails ?? { currentOrgDomain: null, isValidOrgDomain: false } ); } - const eventType = await prisma.eventType.findFirst({ - where: { - slug: eventTypeSlug, - ...(teamId ? { teamId } : {}), - ...(userId ? { userId } : {}), - }, - select: { - id: true, - }, - }); + const eventType = await EventTypeRepository.findFirstEventTypeId({ slug: eventTypeSlug, teamId, userId }); if (!eventType) { throw new TRPCError({ code: "NOT_FOUND" }); } @@ -135,12 +113,9 @@ async function _getReservedSlotsAndCleanupExpired({ const currentTimeInUtc = dayjs.utc().format(); const unexpiredSelectedSlots = - (await prisma.selectedSlots.findMany({ - where: { - userId: { in: usersWithCredentials.map((user) => user.id) }, - releaseAt: { gt: currentTimeInUtc }, - }, - ...selectSelectedSlots, + (await SelectedSlotsRepository.findManyUnexpiredSlots({ + userIds: usersWithCredentials.map((user) => user.id), + currentTimeInUtc, })) || []; const slotsSelectedByOtherUsers = unexpiredSelectedSlots.filter((slot) => slot.uid !== bookerClientUid); @@ -152,12 +127,7 @@ async function _getReservedSlotsAndCleanupExpired({ return reservedSlots; async function _cleanupExpiredSlots({ eventTypeId }: { eventTypeId: number }) { - await prisma.selectedSlots.deleteMany({ - where: { - eventTypeId: { equals: eventTypeId }, - releaseAt: { lt: currentTimeInUtc }, - }, - }); + await SelectedSlotsRepository.deleteManyExpiredSlots({ eventTypeId, currentTimeInUtc }); } } @@ -199,8 +169,9 @@ const _getDynamicEventType = async ( }); } const dynamicEventType = getDefaultEvent(input.eventTypeSlug); - const { where } = await UserRepository._getWhereClauseForFindingUsersByUsername({ - orgSlug: isValidOrgDomain ? currentOrgDomain : null, + + const usersForDynamicEventType = await UserRepository.findManyUsersForDynamicEventType({ + currentOrgDomain: isValidOrgDomain ? currentOrgDomain : null, usernameList: Array.isArray(input.usernameList) ? input.usernameList : input.usernameList @@ -208,20 +179,9 @@ const _getDynamicEventType = async ( : [], }); - // TODO: Should be moved to UserRepository - const usersWithOldSelectedCalendars = await prisma.user.findMany({ - where, - select: { - allowDynamicBooking: true, - ...availabilityUserSelect, - credentials: { - select: credentialForCalendarServiceSelect, - }, - }, - }); - const users = usersWithOldSelectedCalendars.map((user) => withSelectedCalendars(user)); + const usersWithOldSelectedCalendars = usersForDynamicEventType.map((user) => withSelectedCalendars(user)); - const isDynamicAllowed = !users.some((user) => !user.allowDynamicBooking); + const isDynamicAllowed = !usersWithOldSelectedCalendars.some((user) => !user.allowDynamicBooking); if (!isDynamicAllowed) { throw new TRPCError({ message: "Some of the users in this group do not allow dynamic booking", @@ -229,7 +189,7 @@ const _getDynamicEventType = async ( }); } return Object.assign({}, dynamicEventType, { - users, + users: usersWithOldSelectedCalendars, }); }; @@ -250,7 +210,10 @@ export const getRegularOrDynamicEventType = withReporting( "getRegularOrDynamicEventType" ); -function applyOccupiedSeatsToCurrentSeats(currentSeats: CurrentSeats, occupiedSeats: SelectedSlots[]) { +function applyOccupiedSeatsToCurrentSeats( + currentSeats: CurrentSeats, + occupiedSeats: { slotUtcStartDate: Date }[] +) { const occupiedSeatsMap = new Map(); occupiedSeats.forEach((item) => { @@ -502,37 +465,10 @@ const _getAvailableSlots = async ({ input, ctx }: GetScheduleOptions): Promise { - return await prisma.outOfOfficeEntry.findMany({ - where: { - userId: { - in: allUserIds, - }, - OR: [ - // outside of range - // (start <= 'dateTo' AND end >= 'dateFrom') - { - start: { - lte: endTimeDate, - }, - end: { - gte: startTimeDate, - }, - }, - // start is between dateFrom and dateTo but end is outside of range - // (start <= 'dateTo' AND end >= 'dateTo') - { - start: { - lte: endTimeDate, - }, - - end: { - gte: endTimeDate, - }, - }, - // end is between dateFrom and dateTo but start is outside of range - // (start <= 'dateFrom' OR end <= 'dateTo') - { - start: { - lte: startTimeDate, - }, - - end: { - lte: endTimeDate, - }, - }, - ], - }, - select: { - id: true, - start: true, - end: true, - user: { - select: { - id: true, - name: true, - }, - }, - toUser: { - select: { - id: true, - username: true, - name: true, - }, - }, - reason: { - select: { - id: true, - emoji: true, - reason: true, - }, - }, - }, - }); + const oooRepo = new PrismaOOORepository(prisma); + return oooRepo.findManyOOO({ startTimeDate, endTimeDate, allUserIds }); }; export const getOOODates = withReporting(_getOOODates, "getOOODates"); diff --git a/yarn.lock b/yarn.lock index 6ce9903217..d7981f9b19 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2519,7 +2519,7 @@ __metadata: "@axiomhq/winston": ^1.2.0 "@calcom/platform-constants": "*" "@calcom/platform-enums": "*" - "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.239" + "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.241" "@calcom/platform-types": "*" "@calcom/platform-utils": "*" "@calcom/prisma": "*" @@ -3565,13 +3565,13 @@ __metadata: languageName: unknown linkType: soft -"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.239": - version: 0.0.239 - resolution: "@calcom/platform-libraries@npm:0.0.239" +"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.241": + version: 0.0.241 + resolution: "@calcom/platform-libraries@npm:0.0.241" dependencies: "@calcom/features": "*" "@calcom/lib": "*" - checksum: ddc4b74a41cf477c81024ae7faf26a9b6778213b74c33e056ec543a43b0f959b917bcede820677e040684dfb2bed95831ac01218fa08c2f96ee39063afae9c40 + checksum: 1eec9dce749b6678427e00d0f2d2f383d798066f2f7ef7aca1ce90d5c649ed41d2fc1496be93c7448e7c22d2e015e333c8fe0b81b7c3b04b9637f6ea1910a898 languageName: node linkType: hard