refactor: convert getBusyTimes to service class with dependency injection (#22949)

* refactor: convert getBusyTimes to service class with dependency injection

- Create BusyTimesService following UserAvailabilityService pattern
- Add DI tokens, module, and container setup for BusyTimesService
- Update all usage locations to use service instead of direct function calls
- Maintain existing function signatures for backward compatibility
- Add legacy exports to ensure smooth transition
- Update type references in trpc util to use service prototype
- Fix linting issues by making legacy exports async with proper imports
- All tests pass and type checking succeeds

Co-Authored-By: morgan@cal.com <morgan@cal.com>

* refactor: busy time service

* fix: api v2 eslint plugins version mismatch

* chore: bump platform libs

* chore: bump platform libs

* fix: missing di busyTimesModule in slots service

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: morgan@cal.com <morgan@cal.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
This commit is contained in:
devin-ai-integration[bot]
2025-08-08 12:14:08 +03:00
committed by GitHub
co-authored by morgan@cal.com Morgan
parent 3ea73e75d6
commit 29314cde75
19 changed files with 533 additions and 384 deletions
+3 -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.288",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.291",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
"@calcom/prisma": "*",
@@ -98,6 +98,8 @@
"@types/luxon": "^3.3.7",
"@types/passport-jwt": "^3.0.13",
"@types/supertest": "^2.0.12",
"@typescript-eslint/eslint-plugin": "^6",
"@typescript-eslint/parser": "^6",
"jest": "^29.7.0",
"jest-date-mock": "^1.0.10",
"node-mocks-http": "^1.16.2",
@@ -8,12 +8,13 @@ import { PrismaSelectedSlotRepository } from "@/lib/repositories/prisma-selected
import { PrismaTeamRepository } from "@/lib/repositories/prisma-team.repository";
import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository";
import { AvailableSlotsService } from "@/lib/services/available-slots.service";
import { BusyTimesService } from "@/lib/services/busy-times.service";
import { CacheService } from "@/lib/services/cache.service";
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
import { UserAvailabilityService } from "@/lib/services/user-availability.service";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { RedisService } from "@/modules/redis/redis.service";
import { Module } from "@nestjs/common";
import { UserAvailabilityService } from "@/lib/services/user-availability.service";
@Module({
imports: [PrismaModule],
@@ -31,7 +32,8 @@ import { UserAvailabilityService } from "@/lib/services/user-availability.servic
CheckBookingLimitsService,
CacheService,
AvailableSlotsService,
UserAvailabilityService
UserAvailabilityService,
BusyTimesService,
],
exports: [AvailableSlotsService],
})
@@ -7,6 +7,7 @@ import { PrismaScheduleRepository } from "@/lib/repositories/prisma-schedule.rep
import { PrismaSelectedSlotRepository } from "@/lib/repositories/prisma-selected-slot.repository";
import { PrismaTeamRepository } from "@/lib/repositories/prisma-team.repository";
import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository";
import { BusyTimesService } from "@/lib/services/busy-times.service";
import { CacheService } from "@/lib/services/cache.service";
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
import { RedisService } from "@/modules/redis/redis.service";
@@ -48,6 +49,7 @@ export class AvailableSlotsService extends BaseAvailableSlotsService {
eventTypeRepository,
redisService
),
busyTimesService: new BusyTimesService(bookingRepository),
});
}
}
@@ -0,0 +1,13 @@
import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository";
import { Injectable } from "@nestjs/common";
import { BusyTimesService as BaseBusyTimesService } from "@calcom/platform-libraries/slots";
@Injectable()
export class BusyTimesService extends BaseBusyTimesService {
constructor(bookingRepository: PrismaBookingRepository) {
super({
bookingRepo: bookingRepository,
});
}
}
@@ -4,9 +4,9 @@ import dayjs from "@calcom/dayjs";
import type { Dayjs } from "@calcom/dayjs";
import { checkForConflicts } from "@calcom/features/bookings/lib/conflictChecker/checkForConflicts";
import { buildDateRanges } from "@calcom/lib/date-ranges";
import { ErrorCode } from "@calcom/lib/errorCodes";
import { getBusyTimesForLimitChecks } from "@calcom/lib/getBusyTimes";
import { getBusyTimesService } from "@calcom/lib/di/containers/busy-times";
import { getUserAvailabilityService } from "@calcom/lib/di/containers/get-user-availability";
import { ErrorCode } from "@calcom/lib/errorCodes";
import { parseBookingLimit } from "@calcom/lib/intervalLimits/isBookingLimits";
import { parseDurationLimit } from "@calcom/lib/intervalLimits/isDurationLimits";
import { getPiiFreeUser } from "@calcom/lib/piiFreeData";
@@ -62,7 +62,7 @@ const _ensureAvailableUsers = async (
shouldServeCache?: boolean
// ReturnType hint of at least one IsFixedAwareUser, as it's made sure at least one entry exists
): Promise<[IsFixedAwareUser, ...IsFixedAwareUser[]]> => {
const userAvailabilityService = getUserAvailabilityService()
const userAvailabilityService = getUserAvailabilityService();
const availableUsers: IsFixedAwareUser[] = [];
const startDateTimeUtc = getDateTimeInUtc(input.dateFrom, input.timeZone);
@@ -74,9 +74,12 @@ const _ensureAvailableUsers = async (
const bookingLimits = parseBookingLimit(eventType?.bookingLimits);
const durationLimits = parseDurationLimit(eventType?.durationLimits);
const busyTimesFromLimitsBookingsAllUsers: Awaited<ReturnType<typeof getBusyTimesForLimitChecks>> =
const busyTimesService = getBusyTimesService();
const busyTimesFromLimitsBookingsAllUsers: Awaited<
ReturnType<typeof busyTimesService.getBusyTimesForLimitChecks>
> =
eventType && (bookingLimits || durationLimits)
? await getBusyTimesForLimitChecks({
? await busyTimesService.getBusyTimesForLimitChecks({
userIds: eventType.users.map((u) => u.id),
eventTypeId: eventType.id,
startDate: startDateTimeUtc.format(),
@@ -7,17 +7,18 @@ import type { AvailableSlotsService } from "@calcom/trpc/server/routers/viewer/s
import { availableSlotsModule } from "../modules/available-slots";
import { bookingRepositoryModule } from "../modules/booking";
import { busyTimesModule } from "../modules/busy-times";
import { cacheModule } from "../modules/cache";
import { checkBookingLimitsModule } from "../modules/check-booking-limits";
import { eventTypeRepositoryModule } from "../modules/eventType";
import { featuresRepositoryModule } from "../modules/features";
import { getUserAvailabilityModule } from "../modules/get-user-availability";
import { oooRepositoryModule } from "../modules/ooo";
import { routingFormResponseRepositoryModule } from "../modules/routingFormResponse";
import { scheduleRepositoryModule } from "../modules/schedule";
import { selectedSlotsRepositoryModule } from "../modules/selectedSlots";
import { teamRepositoryModule } from "../modules/team";
import { userRepositoryModule } from "../modules/user";
import { getUserAvailabilityModule } from "../modules/get-user-availability";
const container = createContainer();
container.load(DI_TOKENS.REDIS_CLIENT, redisModule);
@@ -34,7 +35,9 @@ container.load(DI_TOKENS.FEATURES_REPOSITORY_MODULE, featuresRepositoryModule);
container.load(DI_TOKENS.CACHE_SERVICE_MODULE, cacheModule);
container.load(DI_TOKENS.CHECK_BOOKING_LIMITS_SERVICE_MODULE, checkBookingLimitsModule);
container.load(DI_TOKENS.AVAILABLE_SLOTS_SERVICE_MODULE, availableSlotsModule);
container.load(DI_TOKENS.GET_USER_AVAILABILITY_SERVICE_MODULE, getUserAvailabilityModule)
container.load(DI_TOKENS.GET_USER_AVAILABILITY_SERVICE_MODULE, getUserAvailabilityModule);
container.load(DI_TOKENS.BUSY_TIMES_SERVICE_MODULE, busyTimesModule);
export function getAvailableSlotsService() {
return container.get<AvailableSlotsService>(DI_TOKENS.AVAILABLE_SLOTS_SERVICE);
}
+17
View File
@@ -0,0 +1,17 @@
import { createContainer } from "@evyweb/ioctopus";
import { DI_TOKENS } from "@calcom/lib/di/tokens";
import { prismaModule } from "@calcom/prisma/prisma.module";
import type { BusyTimesService } from "../../getBusyTimes";
import { bookingRepositoryModule } from "../modules/booking";
import { busyTimesModule } from "../modules/busy-times";
const container = createContainer();
container.load(DI_TOKENS.PRISMA_MODULE, prismaModule);
container.load(DI_TOKENS.BOOKING_REPOSITORY_MODULE, bookingRepositoryModule);
container.load(DI_TOKENS.BUSY_TIMES_SERVICE_MODULE, busyTimesModule);
export function getBusyTimesService() {
return container.get<BusyTimesService>(DI_TOKENS.BUSY_TIMES_SERVICE);
}
@@ -1,16 +1,15 @@
import { createContainer } from "@evyweb/ioctopus";
import { redisModule } from "@calcom/features/redis/di/redisModule";
import { DI_TOKENS } from "@calcom/lib/di/tokens";
import { prismaModule } from "@calcom/prisma/prisma.module";
import { getUserAvailabilityModule } from "../modules/get-user-availability";
import type { UserAvailabilityService } from "../../getUserAvailability";
import { bookingRepositoryModule } from "../modules/booking";
import { busyTimesModule } from "../modules/busy-times";
import { eventTypeRepositoryModule } from "../modules/eventType";
import { redisModule } from "@calcom/features/redis/di/redisModule";
import { getUserAvailabilityModule } from "../modules/get-user-availability";
import { oooRepositoryModule } from "../modules/ooo";
import { UserAvailabilityService } from "../../getUserAvailability";
const container = createContainer();
container.load(DI_TOKENS.PRISMA_MODULE, prismaModule);
@@ -18,9 +17,9 @@ container.load(DI_TOKENS.OOO_REPOSITORY_MODULE, oooRepositoryModule);
container.load(DI_TOKENS.BOOKING_REPOSITORY_MODULE, bookingRepositoryModule);
container.load(DI_TOKENS.EVENT_TYPE_REPOSITORY_MODULE, eventTypeRepositoryModule);
container.load(DI_TOKENS.GET_USER_AVAILABILITY_SERVICE_MODULE, getUserAvailabilityModule);
container.load(DI_TOKENS.BUSY_TIMES_SERVICE_MODULE, busyTimesModule);
container.load(DI_TOKENS.REDIS_CLIENT, redisModule);
export function getUserAvailabilityService() {
return container.get<UserAvailabilityService>(DI_TOKENS.GET_USER_AVAILABILITY_SERVICE);
}
+2 -1
View File
@@ -18,5 +18,6 @@ availableSlotsModule.bind(DI_TOKENS.AVAILABLE_SLOTS_SERVICE).toClass(AvailableSl
redisClient: DI_TOKENS.REDIS_CLIENT,
cacheService: DI_TOKENS.CACHE_SERVICE,
checkBookingLimitsService: DI_TOKENS.CHECK_BOOKING_LIMITS_SERVICE,
userAvailabilityService: DI_TOKENS.GET_USER_AVAILABILITY_SERVICE
userAvailabilityService: DI_TOKENS.GET_USER_AVAILABILITY_SERVICE,
busyTimesService: DI_TOKENS.BUSY_TIMES_SERVICE,
} satisfies Record<keyof IAvailableSlotsService, symbol>);
+10
View File
@@ -0,0 +1,10 @@
import { createModule } from "@evyweb/ioctopus";
import type { IBusyTimesService } from "../../getBusyTimes";
import { BusyTimesService } from "../../getBusyTimes";
import { DI_TOKENS } from "../tokens";
export const busyTimesModule = createModule();
busyTimesModule.bind(DI_TOKENS.BUSY_TIMES_SERVICE).toClass(BusyTimesService, {
bookingRepo: DI_TOKENS.BOOKING_REPOSITORY,
} satisfies Record<keyof IBusyTimesService, symbol>);
@@ -1,7 +1,8 @@
import { createModule } from "@evyweb/ioctopus";
import type { IUserAvailabilityService } from "../../getUserAvailability";
import { UserAvailabilityService } from "../../getUserAvailability";
import { DI_TOKENS } from "../tokens";
import { IUserAvailabilityService, UserAvailabilityService } from "../../getUserAvailability";
export const getUserAvailabilityModule = createModule();
getUserAvailabilityModule.bind(DI_TOKENS.GET_USER_AVAILABILITY_SERVICE).toClass(UserAvailabilityService, {
+2
View File
@@ -35,4 +35,6 @@ export const DI_TOKENS = {
CHECK_BOOKING_AND_DURATION_LIMITS_SERVICE_MODULE: Symbol("CheckBookingAndDurationLimitsServiceModule"),
GET_USER_AVAILABILITY_SERVICE: Symbol("GetUserAvailabilityService"),
GET_USER_AVAILABILITY_SERVICE_MODULE: Symbol("GetUserAvailabilityModule"),
BUSY_TIMES_SERVICE: Symbol("BusyTimesService"),
BUSY_TIMES_SERVICE_MODULE: Symbol("BusyTimesServiceModule"),
};
+7 -4
View File
@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
import dayjs from "@calcom/dayjs";
import { getBusyTimes } from "./getBusyTimes";
import { getBusyTimesService } from "./di/containers/busy-times";
const startOfTomorrow = dayjs().add(1, "day").startOf("day");
const tomorrowDate = startOfTomorrow.format("YYYY-MM-DD");
@@ -58,7 +58,8 @@ const mockBookings = ({
describe("getBusyTimes", () => {
it("blocks a regular time slot", async () => {
const busyTimes = await getBusyTimes({
const busyTimesService = getBusyTimesService();
const busyTimes = await busyTimesService.getBusyTimes({
credentials: [],
userId: 1,
userEmail: "exampleuser1@example.com",
@@ -85,7 +86,8 @@ describe("getBusyTimes", () => {
]);
});
it("should block before and after buffer times", async () => {
const busyTimes = await getBusyTimes({
const busyTimesService = getBusyTimesService();
const busyTimes = await busyTimesService.getBusyTimes({
credentials: [],
userId: 1,
userEmail: "exampleuser1@example.com",
@@ -106,7 +108,8 @@ describe("getBusyTimes", () => {
]);
});
it("should have busy times only if seated with remaining seats when buffers exist", async () => {
const busyTimes = await getBusyTimes({
const busyTimesService = getBusyTimesService();
const busyTimes = await busyTimesService.getBusyTimes({
credentials: [],
userId: 1,
eventTypeId: 1,
+338 -330
View File
@@ -11,6 +11,7 @@ import logger from "@calcom/lib/logger";
import { getPiiFreeBooking } from "@calcom/lib/piiFreeData";
import { withReporting } from "@calcom/lib/sentryWrapper";
import { performance } from "@calcom/lib/server/perfObserver";
import type { BookingRepository } from "@calcom/lib/server/repository/booking";
import prisma from "@calcom/prisma";
import type { SelectedCalendar } from "@calcom/prisma/client";
import { BookingStatus } from "@calcom/prisma/enums";
@@ -18,367 +19,374 @@ import type { EventBusyDetails } from "@calcom/types/Calendar";
import type { CredentialForCalendarService } from "@calcom/types/Credential";
import { getDefinedBufferTimes } from "../features/eventtypes/lib/getDefinedBufferTimes";
import { BookingRepository } from "./server/repository/booking";
const _getBusyTimes = async (params: {
credentials: CredentialForCalendarService[];
userId: number;
userEmail: string;
username: string;
eventTypeId?: number;
startTime: string;
beforeEventBuffer?: number;
afterEventBuffer?: number;
endTime: string;
selectedCalendars: SelectedCalendar[];
seatedEvent?: boolean;
rescheduleUid?: string | null;
duration?: number | null;
currentBookings?:
| (Pick<Booking, "id" | "uid" | "userId" | "startTime" | "endTime" | "title"> & {
eventType: Pick<
EventType,
"id" | "beforeEventBuffer" | "afterEventBuffer" | "seatsPerTimeSlot"
> | null;
_count?: {
seatsReferences: number;
};
})[]
| null;
bypassBusyCalendarTimes: boolean;
shouldServeCache?: boolean;
}) => {
const {
credentials,
userId,
userEmail,
username,
eventTypeId,
startTime,
endTime,
beforeEventBuffer,
afterEventBuffer,
selectedCalendars,
seatedEvent,
rescheduleUid,
duration,
bypassBusyCalendarTimes = false,
shouldServeCache,
} = params;
export interface IBusyTimesService {
bookingRepo: BookingRepository;
}
logger.silly(
`Checking Busy time from Cal Bookings in range ${startTime} to ${endTime} for input ${JSON.stringify({
userId,
eventTypeId,
status: BookingStatus.ACCEPTED,
})}`
);
export class BusyTimesService {
constructor(public readonly dependencies: IBusyTimesService) {}
/**
* A user is considered busy within a given time period if there
* is a booking they own OR attend.
*
* Performs a query for all bookings where:
* - The given booking is owned by this user, or..
* - The current user has a different booking at this time he/she attends
*
* See further discussion within this GH issue:
* https://github.com/calcom/cal.com/issues/6374
*
* NOTE: Changes here will likely require changes to some mocking
* logic within getSchedule.test.ts:addBookings
*/
performance.mark("prismaBookingGetStart");
const startTimeDate =
rescheduleUid && duration ? dayjs(startTime).subtract(duration, "minute").toDate() : new Date(startTime);
const endTimeDate =
rescheduleUid && duration ? dayjs(endTime).add(duration, "minute").toDate() : new Date(endTime);
// to also get bookings that are outside of start and end time, but the buffer falls within the start and end time
const definedBufferTimes = getDefinedBufferTimes();
const maxBuffer = definedBufferTimes[definedBufferTimes.length - 1];
const startTimeAdjustedWithMaxBuffer = dayjs(startTimeDate).subtract(maxBuffer, "minute").toDate();
const endTimeAdjustedWithMaxBuffer = dayjs(endTimeDate).add(maxBuffer, "minute").toDate();
// INFO: Refactored to allow this method to take in a list of current bookings for the user.
// Will keep support for retrieving a user's bookings if the caller does not already supply them.
// This function is called from multiple places but we aren't refactoring all of them at this moment
// to avoid potential side effects.
let bookings = params.currentBookings;
if (!bookings) {
const bookingRepo = new BookingRepository(prisma);
bookings = await bookingRepo.findAllExistingBookingsForEventTypeBetween({
userIdAndEmailMap: new Map([[userId, userEmail]]),
eventTypeId,
startDate: startTimeAdjustedWithMaxBuffer,
endDate: endTimeAdjustedWithMaxBuffer,
seatedEvent,
});
}
const bookingSeatCountMap: { [x: string]: number } = {};
const busyTimes = bookings.reduce((aggregate: EventBusyDetails[], booking) => {
const { id, startTime, endTime, eventType, title, ...rest } = booking;
const minutesToBlockBeforeEvent = (eventType?.beforeEventBuffer || 0) + (afterEventBuffer || 0);
const minutesToBlockAfterEvent = (eventType?.afterEventBuffer || 0) + (beforeEventBuffer || 0);
if (rest._count?.seatsReferences) {
const bookedAt = `${dayjs(startTime).utc().format()}<>${dayjs(endTime).utc().format()}`;
bookingSeatCountMap[bookedAt] = bookingSeatCountMap[bookedAt] || 0;
bookingSeatCountMap[bookedAt]++;
// Seat references on the current event are non-blocking until the event is fully booked.
if (
// there are still seats available.
bookingSeatCountMap[bookedAt] < (eventType?.seatsPerTimeSlot || 1) &&
// and this is the seated event, other event types should be blocked.
eventTypeId === eventType?.id
) {
// then we ONLY add the before/after buffer times as busy times.
if (minutesToBlockBeforeEvent) {
aggregate.push({
start: dayjs(startTime).subtract(minutesToBlockBeforeEvent, "minute").toDate(),
end: dayjs(startTime).toDate(), // The event starts after the buffer
});
}
if (minutesToBlockAfterEvent) {
aggregate.push({
start: dayjs(endTime).toDate(), // The event ends before the buffer
end: dayjs(endTime).add(minutesToBlockAfterEvent, "minute").toDate(),
});
}
return aggregate;
}
// if it does get blocked at this point; we remove the bookingSeatCountMap entry
// doing this allows using the map later to remove the ranges from calendar busy times.
delete bookingSeatCountMap[bookedAt];
}
// rescheduling the same booking to the same time should be possible. Why?
if (rest.uid === rescheduleUid) {
return aggregate;
}
aggregate.push({
start: dayjs(startTime).subtract(minutesToBlockBeforeEvent, "minute").toDate(),
end: dayjs(endTime).add(minutesToBlockAfterEvent, "minute").toDate(),
title,
source: `eventType-${eventType?.id}-booking-${id}`,
});
return aggregate;
}, []);
logger.debug(
`Busy Time from Cal Bookings ${JSON.stringify({
busyTimes,
bookings: bookings?.map((booking) => getPiiFreeBooking(booking)),
numCredentials: credentials?.length,
})}`
);
performance.mark("prismaBookingGetEnd");
performance.measure(`prisma booking get took $1'`, "prismaBookingGetStart", "prismaBookingGetEnd");
if (credentials?.length > 0 && !bypassBusyCalendarTimes) {
const startConnectedCalendarsGet = performance.now();
const calendarBusyTimesQuery = await getBusyCalendarTimes(
async _getBusyTimes(params: {
credentials: CredentialForCalendarService[];
userId: number;
userEmail: string;
username: string;
eventTypeId?: number;
startTime: string;
beforeEventBuffer?: number;
afterEventBuffer?: number;
endTime: string;
selectedCalendars: SelectedCalendar[];
seatedEvent?: boolean;
rescheduleUid?: string | null;
duration?: number | null;
currentBookings?:
| (Pick<Booking, "id" | "uid" | "userId" | "startTime" | "endTime" | "title"> & {
eventType: Pick<
EventType,
"id" | "beforeEventBuffer" | "afterEventBuffer" | "seatsPerTimeSlot"
> | null;
_count?: {
seatsReferences: number;
};
})[]
| null;
bypassBusyCalendarTimes: boolean;
shouldServeCache?: boolean;
}) {
const {
credentials,
userId,
userEmail,
username,
eventTypeId,
startTime,
endTime,
beforeEventBuffer,
afterEventBuffer,
selectedCalendars,
shouldServeCache
);
seatedEvent,
rescheduleUid,
duration,
bypassBusyCalendarTimes = false,
shouldServeCache,
} = params;
if (!calendarBusyTimesQuery.success) {
throw new Error(
`Failed to fetch busy calendar times for selected calendars ${selectedCalendars.map(
(calendar) => calendar.id
)}`
);
}
const calendarBusyTimes = calendarBusyTimesQuery.data;
const endConnectedCalendarsGet = performance.now();
logger.debug(
`Connected Calendars get took ${
endConnectedCalendarsGet - startConnectedCalendarsGet
} ms for user ${username}`,
JSON.stringify({
logger.silly(
`Checking Busy time from Cal Bookings in range ${startTime} to ${endTime} for input ${JSON.stringify({
userId,
eventTypeId,
startTimeDate,
endTimeDate,
calendarBusyTimes,
})
status: BookingStatus.ACCEPTED,
})}`
);
const openSeatsDateRanges = Object.keys(bookingSeatCountMap).map((key) => {
const [start, end] = key.split("<>");
return {
start: dayjs(start),
end: dayjs(end),
};
});
/**
* A user is considered busy within a given time period if there
* is a booking they own OR attend.
*
* Performs a query for all bookings where:
* - The given booking is owned by this user, or..
* - The current user has a different booking at this time he/she attends
*
* See further discussion within this GH issue:
* https://github.com/calcom/cal.com/issues/6374
*
* NOTE: Changes here will likely require changes to some mocking
* logic within getSchedule.test.ts:addBookings
*/
performance.mark("prismaBookingGetStart");
if (rescheduleUid) {
const originalRescheduleBooking = bookings.find((booking) => booking.uid === rescheduleUid);
// calendar busy time from original rescheduled booking should not be blocked
if (originalRescheduleBooking) {
openSeatsDateRanges.push({
start: dayjs(originalRescheduleBooking.startTime),
end: dayjs(originalRescheduleBooking.endTime),
});
}
const startTimeDate =
rescheduleUid && duration
? dayjs(startTime).subtract(duration, "minute").toDate()
: new Date(startTime);
const endTimeDate =
rescheduleUid && duration ? dayjs(endTime).add(duration, "minute").toDate() : new Date(endTime);
// to also get bookings that are outside of start and end time, but the buffer falls within the start and end time
const definedBufferTimes = getDefinedBufferTimes();
const maxBuffer = definedBufferTimes[definedBufferTimes.length - 1];
const startTimeAdjustedWithMaxBuffer = dayjs(startTimeDate).subtract(maxBuffer, "minute").toDate();
const endTimeAdjustedWithMaxBuffer = dayjs(endTimeDate).add(maxBuffer, "minute").toDate();
// INFO: Refactored to allow this method to take in a list of current bookings for the user.
// Will keep support for retrieving a user's bookings if the caller does not already supply them.
// This function is called from multiple places but we aren't refactoring all of them at this moment
// to avoid potential side effects.
let bookings = params.currentBookings;
if (!bookings) {
const bookingRepo = this.dependencies.bookingRepo;
bookings = await bookingRepo.findAllExistingBookingsForEventTypeBetween({
userIdAndEmailMap: new Map([[userId, userEmail]]),
eventTypeId,
startDate: startTimeAdjustedWithMaxBuffer,
endDate: endTimeAdjustedWithMaxBuffer,
seatedEvent,
});
}
const result = subtract(
calendarBusyTimes.map((value) => ({
...value,
end: dayjs(value.end),
start: dayjs(value.start),
})),
openSeatsDateRanges
);
const bookingSeatCountMap: { [x: string]: number } = {};
const busyTimes = bookings.reduce((aggregate: EventBusyDetails[], booking) => {
const { id, startTime, endTime, eventType, title, ...rest } = booking;
busyTimes.push(
...result.map((busyTime) => ({
...busyTime,
start: busyTime.start.subtract(afterEventBuffer || 0, "minute").toDate(),
end: busyTime.end.add(beforeEventBuffer || 0, "minute").toDate(),
}))
);
const minutesToBlockBeforeEvent = (eventType?.beforeEventBuffer || 0) + (afterEventBuffer || 0);
const minutesToBlockAfterEvent = (eventType?.afterEventBuffer || 0) + (beforeEventBuffer || 0);
/*
if (rest._count?.seatsReferences) {
const bookedAt = `${dayjs(startTime).utc().format()}<>${dayjs(endTime).utc().format()}`;
bookingSeatCountMap[bookedAt] = bookingSeatCountMap[bookedAt] || 0;
bookingSeatCountMap[bookedAt]++;
// Seat references on the current event are non-blocking until the event is fully booked.
if (
// there are still seats available.
bookingSeatCountMap[bookedAt] < (eventType?.seatsPerTimeSlot || 1) &&
// and this is the seated event, other event types should be blocked.
eventTypeId === eventType?.id
) {
// then we ONLY add the before/after buffer times as busy times.
if (minutesToBlockBeforeEvent) {
aggregate.push({
start: dayjs(startTime).subtract(minutesToBlockBeforeEvent, "minute").toDate(),
end: dayjs(startTime).toDate(), // The event starts after the buffer
});
}
if (minutesToBlockAfterEvent) {
aggregate.push({
start: dayjs(endTime).toDate(), // The event ends before the buffer
end: dayjs(endTime).add(minutesToBlockAfterEvent, "minute").toDate(),
});
}
return aggregate;
}
// if it does get blocked at this point; we remove the bookingSeatCountMap entry
// doing this allows using the map later to remove the ranges from calendar busy times.
delete bookingSeatCountMap[bookedAt];
}
// rescheduling the same booking to the same time should be possible. Why?
if (rest.uid === rescheduleUid) {
return aggregate;
}
aggregate.push({
start: dayjs(startTime).subtract(minutesToBlockBeforeEvent, "minute").toDate(),
end: dayjs(endTime).add(minutesToBlockAfterEvent, "minute").toDate(),
title,
source: `eventType-${eventType?.id}-booking-${id}`,
});
return aggregate;
}, []);
logger.debug(
`Busy Time from Cal Bookings ${JSON.stringify({
busyTimes,
bookings: bookings?.map((booking) => getPiiFreeBooking(booking)),
numCredentials: credentials?.length,
})}`
);
performance.mark("prismaBookingGetEnd");
performance.measure(`prisma booking get took $1'`, "prismaBookingGetStart", "prismaBookingGetEnd");
if (credentials?.length > 0 && !bypassBusyCalendarTimes) {
const startConnectedCalendarsGet = performance.now();
const calendarBusyTimesQuery = await getBusyCalendarTimes(
credentials,
startTime,
endTime,
selectedCalendars,
shouldServeCache
);
if (!calendarBusyTimesQuery.success) {
throw new Error(
`Failed to fetch busy calendar times for selected calendars ${selectedCalendars.map(
(calendar) => calendar.id
)}`
);
}
const calendarBusyTimes = calendarBusyTimesQuery.data;
const endConnectedCalendarsGet = performance.now();
logger.debug(
`Connected Calendars get took ${
endConnectedCalendarsGet - startConnectedCalendarsGet
} ms for user ${username}`,
JSON.stringify({
eventTypeId,
startTimeDate,
endTimeDate,
calendarBusyTimes,
})
);
const openSeatsDateRanges = Object.keys(bookingSeatCountMap).map((key) => {
const [start, end] = key.split("<>");
return {
start: dayjs(start),
end: dayjs(end),
};
});
if (rescheduleUid) {
const originalRescheduleBooking = bookings.find((booking) => booking.uid === rescheduleUid);
// calendar busy time from original rescheduled booking should not be blocked
if (originalRescheduleBooking) {
openSeatsDateRanges.push({
start: dayjs(originalRescheduleBooking.startTime),
end: dayjs(originalRescheduleBooking.endTime),
});
}
}
const result = subtract(
calendarBusyTimes.map((value) => ({
...value,
end: dayjs(value.end),
start: dayjs(value.start),
})),
openSeatsDateRanges
);
busyTimes.push(
...result.map((busyTime) => ({
...busyTime,
start: busyTime.start.subtract(afterEventBuffer || 0, "minute").toDate(),
end: busyTime.end.add(beforeEventBuffer || 0, "minute").toDate(),
}))
);
/*
// TODO: Disabled until we can filter Zoom events by date. Also this is adding too much latency.
const videoBusyTimes = (await getBusyVideoTimes(credentials)).filter(notEmpty);
console.log("videoBusyTimes", videoBusyTimes);
busyTimes.push(...videoBusyTimes);
*/
} else {
logger.warn(`No credentials found for user ${userId}`, {
selectedCalendarIds: selectedCalendars.map((calendar) => calendar.id),
});
}
logger.debug(
"getBusyTimes:",
JSON.stringify({
allBusyTimes: busyTimes,
})
);
return busyTimes;
};
export const getBusyTimes = withReporting(_getBusyTimes, "getBusyTimes");
export function getStartEndDateforLimitCheck(
startDate: string,
endDate: string,
bookingLimits?: IntervalLimit | null,
durationLimits?: IntervalLimit | null
) {
const startTimeAsDayJs = stringToDayjs(startDate);
const endTimeAsDayJs = stringToDayjs(endDate);
let limitDateFrom = stringToDayjs(startDate);
let limitDateTo = stringToDayjs(endDate);
// expand date ranges by absolute minimum required to apply limits
// (yearly limits are handled separately for performance)
for (const key of ["PER_MONTH", "PER_WEEK", "PER_DAY"] as Exclude<keyof IntervalLimit, "PER_YEAR">[]) {
if (bookingLimits?.[key] || durationLimits?.[key]) {
const unit = intervalLimitKeyToUnit(key);
limitDateFrom = dayjs.min(limitDateFrom, startTimeAsDayJs.startOf(unit));
limitDateTo = dayjs.max(limitDateTo, endTimeAsDayJs.endOf(unit));
} else {
logger.warn(`No credentials found for user ${userId}`, {
selectedCalendarIds: selectedCalendars.map((calendar) => calendar.id),
});
}
}
return { limitDateFrom, limitDateTo };
}
export async function getBusyTimesForLimitChecks(params: {
userIds: number[];
eventTypeId: number;
startDate: string;
endDate: string;
rescheduleUid?: string | null;
bookingLimits?: IntervalLimit | null;
durationLimits?: IntervalLimit | null;
}) {
const { userIds, eventTypeId, startDate, endDate, rescheduleUid, bookingLimits, durationLimits } = params;
performance.mark("getBusyTimesForLimitChecksStart");
let busyTimes: EventBusyDetails[] = [];
if (!bookingLimits && !durationLimits) {
logger.debug(
"getBusyTimes:",
JSON.stringify({
allBusyTimes: busyTimes,
})
);
return busyTimes;
}
const { limitDateFrom, limitDateTo } = getStartEndDateforLimitCheck(
startDate,
endDate,
bookingLimits,
durationLimits
);
getBusyTimes = withReporting(this._getBusyTimes.bind(this), "getBusyTimes");
logger.silly(
`Fetch limit checks bookings in range ${limitDateFrom} to ${limitDateTo} for input ${JSON.stringify({
eventTypeId,
status: BookingStatus.ACCEPTED,
})}`
);
getStartEndDateforLimitCheck(
startDate: string,
endDate: string,
bookingLimits?: IntervalLimit | null,
durationLimits?: IntervalLimit | null
) {
const startTimeAsDayJs = stringToDayjs(startDate);
const endTimeAsDayJs = stringToDayjs(endDate);
const where: Prisma.BookingWhereInput = {
userId: {
in: userIds,
},
eventTypeId,
status: BookingStatus.ACCEPTED,
// FIXME: bookings that overlap on one side will never be counted
startTime: {
gte: limitDateFrom.toDate(),
},
endTime: {
lte: limitDateTo.toDate(),
},
};
let limitDateFrom = stringToDayjs(startDate);
let limitDateTo = stringToDayjs(endDate);
if (rescheduleUid) {
where.NOT = {
uid: rescheduleUid,
};
// expand date ranges by absolute minimum required to apply limits
// (yearly limits are handled separately for performance)
for (const key of ["PER_MONTH", "PER_WEEK", "PER_DAY"] as Exclude<keyof IntervalLimit, "PER_YEAR">[]) {
if (bookingLimits?.[key] || durationLimits?.[key]) {
const unit = intervalLimitKeyToUnit(key);
limitDateFrom = dayjs.min(limitDateFrom, startTimeAsDayJs.startOf(unit));
limitDateTo = dayjs.max(limitDateTo, endTimeAsDayJs.endOf(unit));
}
}
return { limitDateFrom, limitDateTo };
}
const bookings = await prisma.booking.findMany({
where,
select: {
id: true,
startTime: true,
endTime: true,
eventType: {
select: {
id: true,
},
async getBusyTimesForLimitChecks(params: {
userIds: number[];
eventTypeId: number;
startDate: string;
endDate: string;
rescheduleUid?: string | null;
bookingLimits?: IntervalLimit | null;
durationLimits?: IntervalLimit | null;
}) {
const { userIds, eventTypeId, startDate, endDate, rescheduleUid, bookingLimits, durationLimits } = params;
performance.mark("getBusyTimesForLimitChecksStart");
let busyTimes: EventBusyDetails[] = [];
if (!bookingLimits && !durationLimits) {
return busyTimes;
}
const { limitDateFrom, limitDateTo } = this.getStartEndDateforLimitCheck(
startDate,
endDate,
bookingLimits,
durationLimits
);
logger.silly(
`Fetch limit checks bookings in range ${limitDateFrom} to ${limitDateTo} for input ${JSON.stringify({
eventTypeId,
status: BookingStatus.ACCEPTED,
})}`
);
const where: Prisma.BookingWhereInput = {
userId: {
in: userIds,
},
title: true,
userId: true,
},
});
eventTypeId,
status: BookingStatus.ACCEPTED,
// FIXME: bookings that overlap on one side will never be counted
startTime: {
gte: limitDateFrom.toDate(),
},
endTime: {
lte: limitDateTo.toDate(),
},
};
busyTimes = bookings.map(({ id, startTime, endTime, eventType, title, userId }) => ({
start: dayjs(startTime).toDate(),
end: dayjs(endTime).toDate(),
title,
source: `eventType-${eventType?.id}-booking-${id}`,
userId,
}));
if (rescheduleUid) {
where.NOT = {
uid: rescheduleUid,
};
}
logger.silly(`Fetch limit checks bookings for eventId: ${eventTypeId} ${JSON.stringify(busyTimes)}`);
performance.mark("getBusyTimesForLimitChecksEnd");
performance.measure(
`prisma booking get for limits took $1'`,
"getBusyTimesForLimitChecksStart",
"getBusyTimesForLimitChecksEnd"
);
return busyTimes;
const bookings = await prisma.booking.findMany({
where,
select: {
id: true,
startTime: true,
endTime: true,
eventType: {
select: {
id: true,
},
},
title: true,
userId: true,
},
});
busyTimes = bookings.map(({ id, startTime, endTime, eventType, title, userId }) => ({
start: dayjs(startTime).toDate(),
end: dayjs(endTime).toDate(),
title,
source: `eventType-${eventType?.id}-booking-${id}`,
userId,
}));
logger.silly(`Fetch limit checks bookings for eventId: ${eventTypeId} ${JSON.stringify(busyTimes)}`);
performance.mark("getBusyTimesForLimitChecksEnd");
performance.measure(
`prisma booking get for limits took $1'`,
"getBusyTimesForLimitChecksStart",
"getBusyTimesForLimitChecksEnd"
);
return busyTimes;
}
}
export default withReporting(_getBusyTimes, "getBusyTimes");
+3 -2
View File
@@ -36,7 +36,7 @@ import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import type { EventBusyDetails, IntervalLimitUnit } from "@calcom/types/Calendar";
import type { TimeRange } from "@calcom/types/schedule";
import { getBusyTimes } from "./getBusyTimes";
import { getBusyTimesService } from "./di/containers/busy-times";
import { getPeriodStartDatesBetween as getPeriodStartDatesBetweenUtil } from "./intervalLimits/utils/getPeriodStartDatesBetween";
import { withReporting } from "./sentryWrapper";
@@ -440,7 +440,8 @@ export class UserAvailabilityService {
let busyTimes = [];
try {
busyTimes = await getBusyTimes({
const busyTimesService = getBusyTimesService();
busyTimes = await busyTimesService.getBusyTimes({
credentials: user.credentials,
startTime: getBusyTimesStart,
endTime: getBusyTimesEnd,
@@ -1,7 +1,7 @@
import type { Dayjs } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs";
import { getCheckBookingLimitsService } from "@calcom/lib/di/containers/booking-limits";
import { getStartEndDateforLimitCheck } from "@calcom/lib/getBusyTimes";
import { getBusyTimesService } from "@calcom/lib/di/containers/busy-times";
import type { EventType } from "@calcom/lib/getUserAvailability";
import { getPeriodStartDatesBetween } from "@calcom/lib/intervalLimits/utils/getPeriodStartDatesBetween";
import { withReporting } from "@calcom/lib/sentryWrapper";
@@ -229,7 +229,8 @@ const _getBusyTimesFromTeamLimits = async (
timeZone: string,
rescheduleUid?: string
) => {
const { limitDateFrom, limitDateTo } = getStartEndDateforLimitCheck(
const busyTimesService = getBusyTimesService();
const { limitDateFrom, limitDateTo } = busyTimesService.getStartEndDateforLimitCheck(
dateFrom.toISOString(),
dateTo.toISOString(),
bookingLimits
+3
View File
@@ -1,3 +1,6 @@
import { BusyTimesService } from "@calcom/lib/getBusyTimes";
import { AvailableSlotsService } from "@calcom/trpc/server/routers/viewer/slots/util";
export { AvailableSlotsService };
export { BusyTimesService };
@@ -15,8 +15,9 @@ import { RESERVED_SUBDOMAINS } from "@calcom/lib/constants";
import { buildDateRanges } from "@calcom/lib/date-ranges";
import { getUTCOffsetByTimezone } from "@calcom/lib/dayjs";
import { getDefaultEvent } from "@calcom/lib/defaultEvents";
import type { getBusyTimesService } from "@calcom/lib/di/containers/busy-times";
import { getAggregatedAvailability } from "@calcom/lib/getAggregatedAvailability";
import { getBusyTimesForLimitChecks, getStartEndDateforLimitCheck } from "@calcom/lib/getBusyTimes";
import type { BusyTimesService } from "@calcom/lib/getBusyTimes";
import type {
CurrentSeats,
EventType,
@@ -103,6 +104,7 @@ export interface IAvailableSlotsService {
cacheService: CacheService;
checkBookingLimitsService: CheckBookingLimitsService;
userAvailabilityService: UserAvailabilityService;
busyTimesService: BusyTimesService;
redisClient: IRedisService;
}
@@ -373,13 +375,13 @@ export class AvailableSlotsService {
return userBusyTimesMap;
}
const { limitDateFrom, limitDateTo } = getStartEndDateforLimitCheck(
const { limitDateFrom, limitDateTo } = this.dependencies.busyTimesService.getStartEndDateforLimitCheck(
dateFrom.toISOString(),
dateTo.toISOString(),
bookingLimits || durationLimits
);
const busyTimesFromLimitsBookings = await getBusyTimesForLimitChecks({
const busyTimesFromLimitsBookings = await this.dependencies.busyTimesService.getBusyTimesForLimitChecks({
userIds: users.map((user) => user.id),
eventTypeId: eventType.id,
startDate: limitDateFrom.format(),
@@ -566,7 +568,7 @@ export class AvailableSlotsService {
timeZone: string,
rescheduleUid?: string
) {
const { limitDateFrom, limitDateTo } = getStartEndDateforLimitCheck(
const { limitDateFrom, limitDateTo } = this.dependencies.busyTimesService.getStartEndDateforLimitCheck(
dateFrom.toISOString(),
dateTo.toISOString(),
bookingLimits
@@ -797,19 +799,21 @@ export class AvailableSlotsService {
? parseDurationLimit(eventType?.durationLimits)
: null;
let busyTimesFromLimitsBookingsAllUsers: Awaited<ReturnType<typeof getBusyTimesForLimitChecks>> = [];
let busyTimesFromLimitsBookingsAllUsers: Awaited<
ReturnType<typeof getBusyTimesService.prototype.getBusyTimesForLimitChecks>
> = [];
if (eventType && (bookingLimits || durationLimits)) {
// TODO: DI getBusyTimesForLimitChecks
busyTimesFromLimitsBookingsAllUsers = await getBusyTimesForLimitChecks({
userIds: allUserIds,
eventTypeId: eventType.id,
startDate: startTime.format(),
endDate: endTime.format(),
rescheduleUid: input.rescheduleUid,
bookingLimits,
durationLimits,
});
busyTimesFromLimitsBookingsAllUsers =
await this.dependencies.busyTimesService.getBusyTimesForLimitChecks({
userIds: allUserIds,
eventTypeId: eventType.id,
startDate: startTime.format(),
endDate: endTime.format(),
rescheduleUid: input.rescheduleUid,
bookingLimits,
durationLimits,
});
}
let busyTimesFromLimitsMap: Map<number, EventBusyDetails[]> | undefined = undefined;
@@ -868,7 +872,6 @@ export class AvailableSlotsService {
const enrichUsersWithData = withReporting(_enrichUsersWithData.bind(this), "enrichUsersWithData");
const users = enrichUsersWithData();
// TODO: DI getUsersAvailability
const premappedUsersAvailability = await this.dependencies.userAvailabilityService.getUsersAvailability({
users,
query: {
+87 -12
View File
@@ -2500,7 +2500,7 @@ __metadata:
"@axiomhq/winston": ^1.2.0
"@calcom/platform-constants": "*"
"@calcom/platform-enums": "*"
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.288"
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.291"
"@calcom/platform-types": "*"
"@calcom/platform-utils": "*"
"@calcom/prisma": "*"
@@ -2531,6 +2531,8 @@ __metadata:
"@types/luxon": ^3.3.7
"@types/passport-jwt": ^3.0.13
"@types/supertest": ^2.0.12
"@typescript-eslint/eslint-plugin": ^6
"@typescript-eslint/parser": ^6
body-parser: ^1.20.2
bull: ^4.12.4
class-transformer: ^0.5.1
@@ -3556,13 +3558,13 @@ __metadata:
languageName: unknown
linkType: soft
"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.288":
version: 0.0.288
resolution: "@calcom/platform-libraries@npm:0.0.288"
"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.291":
version: 0.0.291
resolution: "@calcom/platform-libraries@npm:0.0.291"
dependencies:
"@calcom/features": "*"
"@calcom/lib": "*"
checksum: 9f44ba9e258a94c9b8bb5388c400696f6e6acd20dbf73865d62739b1aad212626bb181d88b38b30d08659c8b9183d27aba94bdd52b36e6c6c3cc7594d1af29d7
checksum: 09cb5db584698acccd4447b4007df2fe2d66352c6ba823ef8c354fd3458e59414a9502f6950438e1260df71613b95f1e5a14bb54e2c7ffe6cc1dafe8b643fb9d
languageName: node
linkType: hard
@@ -5715,7 +5717,7 @@ __metadata:
languageName: node
linkType: hard
"@eslint-community/regexpp@npm:^4.10.0":
"@eslint-community/regexpp@npm:^4.10.0, @eslint-community/regexpp@npm:^4.5.1":
version: 4.12.1
resolution: "@eslint-community/regexpp@npm:4.12.1"
checksum: 0d628680e204bc316d545b4993d3658427ca404ae646ce541fcc65306b8c712c340e5e573e30fb9f85f4855c0c5f6dca9868931f2fcced06417fbe1a0c6cd2d6
@@ -17629,6 +17631,13 @@ __metadata:
languageName: node
linkType: hard
"@types/json-schema@npm:^7.0.12":
version: 7.0.15
resolution: "@types/json-schema@npm:7.0.15"
checksum: 97ed0cb44d4070aecea772b7b2e2ed971e10c81ec87dd4ecc160322ffa55ff330dace1793489540e3e318d90942064bb697cc0f8989391797792d919737b3b98
languageName: node
linkType: hard
"@types/json5@npm:^0.0.29":
version: 0.0.29
resolution: "@types/json5@npm:0.0.29"
@@ -18156,6 +18165,13 @@ __metadata:
languageName: node
linkType: hard
"@types/semver@npm:^7.5.0":
version: 7.7.0
resolution: "@types/semver@npm:7.7.0"
checksum: d488eaeddb23879a0a8a759bed667e1a76cb0dd4d23e3255538e24c189db387357953ca9e7a3bda2bb7f95e84cac8fe0db4fbe6b3456e893043337732d1d23cc
languageName: node
linkType: hard
"@types/send@npm:*":
version: 0.17.4
resolution: "@types/send@npm:0.17.4"
@@ -18462,21 +18478,28 @@ __metadata:
languageName: node
linkType: hard
"@typescript-eslint/parser@npm:^5.4.2 || ^6.0.0":
"@typescript-eslint/eslint-plugin@npm:^6":
version: 6.21.0
resolution: "@typescript-eslint/parser@npm:6.21.0"
resolution: "@typescript-eslint/eslint-plugin@npm:6.21.0"
dependencies:
"@eslint-community/regexpp": ^4.5.1
"@typescript-eslint/scope-manager": 6.21.0
"@typescript-eslint/types": 6.21.0
"@typescript-eslint/typescript-estree": 6.21.0
"@typescript-eslint/type-utils": 6.21.0
"@typescript-eslint/utils": 6.21.0
"@typescript-eslint/visitor-keys": 6.21.0
debug: ^4.3.4
graphemer: ^1.4.0
ignore: ^5.2.4
natural-compare: ^1.4.0
semver: ^7.5.4
ts-api-utils: ^1.0.1
peerDependencies:
"@typescript-eslint/parser": ^6.0.0 || ^6.0.0-alpha
eslint: ^7.0.0 || ^8.0.0
peerDependenciesMeta:
typescript:
optional: true
checksum: 162fe3a867eeeffda7328bce32dae45b52283c68c8cb23258fb9f44971f761991af61f71b8c9fe1aa389e93dfe6386f8509c1273d870736c507d76dd40647b68
checksum: 5ef2c502255e643e98051e87eb682c2a257e87afd8ec3b9f6274277615e1c2caf3131b352244cfb1987b8b2c415645eeacb9113fa841fc4c9b2ac46e8aed6efd
languageName: node
linkType: hard
@@ -18496,6 +18519,24 @@ __metadata:
languageName: node
linkType: hard
"@typescript-eslint/parser@npm:^5.4.2 || ^6.0.0, @typescript-eslint/parser@npm:^6":
version: 6.21.0
resolution: "@typescript-eslint/parser@npm:6.21.0"
dependencies:
"@typescript-eslint/scope-manager": 6.21.0
"@typescript-eslint/types": 6.21.0
"@typescript-eslint/typescript-estree": 6.21.0
"@typescript-eslint/visitor-keys": 6.21.0
debug: ^4.3.4
peerDependencies:
eslint: ^7.0.0 || ^8.0.0
peerDependenciesMeta:
typescript:
optional: true
checksum: 162fe3a867eeeffda7328bce32dae45b52283c68c8cb23258fb9f44971f761991af61f71b8c9fe1aa389e93dfe6386f8509c1273d870736c507d76dd40647b68
languageName: node
linkType: hard
"@typescript-eslint/parser@npm:^5.52.0":
version: 5.52.0
resolution: "@typescript-eslint/parser@npm:5.52.0"
@@ -18560,6 +18601,23 @@ __metadata:
languageName: node
linkType: hard
"@typescript-eslint/type-utils@npm:6.21.0":
version: 6.21.0
resolution: "@typescript-eslint/type-utils@npm:6.21.0"
dependencies:
"@typescript-eslint/typescript-estree": 6.21.0
"@typescript-eslint/utils": 6.21.0
debug: ^4.3.4
ts-api-utils: ^1.0.1
peerDependencies:
eslint: ^7.0.0 || ^8.0.0
peerDependenciesMeta:
typescript:
optional: true
checksum: 77025473f4d80acf1fafcce99c5c283e557686a61861febeba9c9913331f8a41e930bf5cd8b7a54db502a57b6eb8ea6d155cbd4f41349ed00e3d7aeb1f477ddc
languageName: node
linkType: hard
"@typescript-eslint/type-utils@npm:8.28.0":
version: 8.28.0
resolution: "@typescript-eslint/type-utils@npm:8.28.0"
@@ -18694,6 +18752,23 @@ __metadata:
languageName: node
linkType: hard
"@typescript-eslint/utils@npm:6.21.0":
version: 6.21.0
resolution: "@typescript-eslint/utils@npm:6.21.0"
dependencies:
"@eslint-community/eslint-utils": ^4.4.0
"@types/json-schema": ^7.0.12
"@types/semver": ^7.5.0
"@typescript-eslint/scope-manager": 6.21.0
"@typescript-eslint/types": 6.21.0
"@typescript-eslint/typescript-estree": 6.21.0
semver: ^7.5.4
peerDependencies:
eslint: ^7.0.0 || ^8.0.0
checksum: b129b3a4aebec8468259f4589985cb59ea808afbfdb9c54f02fad11e17d185e2bf72bb332f7c36ec3c09b31f18fc41368678b076323e6e019d06f74ee93f7bf2
languageName: node
linkType: hard
"@typescript-eslint/utils@npm:8.28.0":
version: 8.28.0
resolution: "@typescript-eslint/utils@npm:8.28.0"
@@ -29699,7 +29774,7 @@ __metadata:
languageName: node
linkType: hard
"ignore@npm:^5.3.1":
"ignore@npm:^5.2.4, ignore@npm:^5.3.1":
version: 5.3.2
resolution: "ignore@npm:5.3.2"
checksum: 2acfd32a573260ea522ea0bfeff880af426d68f6831f973129e2ba7363f422923cf53aab62f8369cbf4667c7b25b6f8a3761b34ecdb284ea18e87a5262a865be