refactor: getAvailableSlots use repositories (#22290)

This commit is contained in:
Morgan
2025-07-07 10:57:01 -07:00
committed by GitHub
parent cd6293d220
commit 52940ce254
8 changed files with 225 additions and 171 deletions
+1 -1
View File
@@ -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": "*",
@@ -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: {
+82
View File
@@ -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,
},
},
},
});
}
}
@@ -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 },
},
});
}
}
+18
View File
@@ -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({
+26 -1
View File
@@ -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,
},
},
});
}
}
+33 -164
View File
@@ -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<GetAvailabilityUser, "credentials"> & {
credentials: CredentialPayload[];
};
type GetAvailabilityUserWithDelegationCredentials = Omit<GetAvailabilityUser, "credentials"> & {
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<typeof selectSelectedSlots>;
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<string, number>();
occupiedSeats.forEach((item) => {
@@ -502,37 +465,10 @@ const _getAvailableSlots = async ({ input, ctx }: GetScheduleOptions): Promise<I
const bookerClientUid = ctx?.req?.cookies?.uid;
const isRestrictionScheduleFeatureEnabled = await isRestrictionScheduleEnabled(eventType.team?.id);
if (eventType.restrictionScheduleId && isRestrictionScheduleFeatureEnabled) {
const restrictionSchedule = await prisma.schedule.findUnique({
where: { id: eventType.restrictionScheduleId },
select: {
id: true,
timeZone: true,
userId: true,
availability: {
select: {
days: true,
startTime: true,
endTime: true,
date: true,
},
},
user: {
select: {
id: true,
defaultScheduleId: true,
travelSchedules: {
select: {
id: true,
timeZone: true,
startDate: true,
endDate: true,
},
},
},
},
},
const scheduleRepo = new ScheduleRepository(prisma);
const restrictionSchedule = await scheduleRepo.findScheduleByIdForBuildDateRanges({
scheduleId: eventType.restrictionScheduleId,
});
if (restrictionSchedule) {
if (!eventType.useBookerTimezone && !restrictionSchedule.timeZone) {
throw new TRPCError({
@@ -859,85 +795,18 @@ async function getTeamIdFromSlug(
organizationDetails: { currentOrgDomain: string | null; isValidOrgDomain: boolean }
) {
const { currentOrgDomain, isValidOrgDomain } = organizationDetails;
const team = await prisma.team.findFirst({
where: {
slug,
parent: isValidOrgDomain && currentOrgDomain ? getSlugOrRequestedSlug(currentOrgDomain) : null,
},
select: {
id: true,
},
const team = await TeamRepository.findFirstBySlugAndParentSlug({
slug,
parentSlug: isValidOrgDomain && currentOrgDomain ? currentOrgDomain : null,
select: { id: true },
});
return team?.id;
}
const _getOOODates = async (startTimeDate: Date, endTimeDate: Date, allUserIds: number[]) => {
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");
+5 -5
View File
@@ -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