Collective events are now considered from getBusyTimes booking query (#6911)

* Updated the busyTimes query to include bookings owned OR hosted by the given user

* Updated getSchedule tests to account for the new where condition

* Ignoring typing within a mocked function

* Further type checks, figured out how to run the command locally
This commit is contained in:
Aaron Presley
2023-02-13 12:46:37 +00:00
committed by GitHub
parent 1cae2b6f3d
commit d59cda3286
2 changed files with 222 additions and 42 deletions
+134 -8
View File
@@ -183,14 +183,14 @@ type InputEventType = {
length?: number;
slotInterval?: number;
minimumBookingNotice?: number;
users: { id: number }[];
users?: { id: number }[];
schedulingType?: SchedulingType;
beforeEventBuffer?: number;
afterEventBuffer?: number;
};
type InputBooking = {
userId: number;
userId?: number;
eventTypeId: number;
startTime: string;
endTime: string;
@@ -198,6 +198,13 @@ type InputBooking = {
status: BookingStatus;
};
type InputHost = {
id: number;
userId: number;
eventTypeId: number;
isFixed: boolean;
};
const cleanup = async () => {
await prisma.eventType.deleteMany();
await prisma.user.deleteMany();
@@ -223,6 +230,7 @@ describe("getSchedule", () => {
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
const scenarioData = {
hosts: [],
eventTypes: [
{
id: 1,
@@ -337,6 +345,7 @@ describe("getSchedule", () => {
endTime: `${plus2DateString}T06:15:00.000Z`,
},
],
hosts: [],
});
// Day Plus 2 is completely free - It only has non accepted bookings
@@ -434,6 +443,7 @@ describe("getSchedule", () => {
schedules: [TestData.schedules.IstWorkHours],
},
],
hosts: [],
});
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
@@ -530,6 +540,7 @@ describe("getSchedule", () => {
schedules: [TestData.schedules.IstWorkHours],
},
],
hosts: [],
});
const { dateString: todayDateString } = getDate();
const { dateString: minus1DateString } = getDate({ dateIncrement: -1 });
@@ -606,6 +617,7 @@ describe("getSchedule", () => {
selectedCalendars: [TestData.selectedCalendars.google],
},
],
hosts: [],
apps: [TestData.apps.googleCalendar],
};
@@ -681,6 +693,7 @@ describe("getSchedule", () => {
},
],
apps: [TestData.apps.googleCalendar],
hosts: [],
};
createBookingScenario(scenarioData);
@@ -740,6 +753,7 @@ describe("getSchedule", () => {
schedules: [TestData.schedules.IstWorkHoursWithDateOverride(plus2DateString)],
},
],
hosts: [],
};
createBookingScenario(scenarioData);
@@ -762,6 +776,87 @@ describe("getSchedule", () => {
}
);
});
test("that a user is considered busy when there's a booking they host", async () => {
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
createBookingScenario({
eventTypes: [
// A Collective Event Type hosted by this user
{
id: 1,
slotInterval: 45,
schedulingType: "COLLECTIVE",
},
// A default Event Type which this user owns
{
id: 2,
slotInterval: 45,
users: [{ id: 101 }],
},
],
users: [
{
...TestData.users.example,
id: 101,
schedules: [TestData.schedules.IstWorkHours],
},
],
bookings: [
// Create a booking on our Collective Event Type
{
// userId: XX, <- No owner since this is a Collective Event Type
eventTypeId: 1,
status: "ACCEPTED",
startTime: `${plus2DateString}T04:00:00.000Z`,
endTime: `${plus2DateString}T04:15:00.000Z`,
},
],
hosts: [
// This user is a host of our Collective event
{
id: 1,
eventTypeId: 1,
userId: 101,
isFixed: true,
},
],
});
// Requesting this user's availability for their
// individual Event Type
const thisUserAvailability = await getSchedule(
{
eventTypeId: 2,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
},
ctx
);
expect(thisUserAvailability).toHaveTimeSlots(
[
// `04:00:00.000Z`, // <- This slot should be occupied by the Collective Event
`04:45:00.000Z`,
`05:30:00.000Z`,
`06:15:00.000Z`,
`07:00:00.000Z`,
`07:45:00.000Z`,
`08:30:00.000Z`,
`09:15:00.000Z`,
`10:00:00.000Z`,
`10:45:00.000Z`,
`11:30:00.000Z`,
`12:15:00.000Z`,
],
{
dateString: plus2DateString,
}
);
});
});
describe("Team Event", () => {
@@ -826,6 +921,7 @@ describe("getSchedule", () => {
endTime: `${plus2DateString}T05:45:00.000Z`,
},
],
hosts: [],
});
const scheduleForTeamEventOnADayWithNoBooking = await getSchedule(
@@ -963,6 +1059,7 @@ describe("getSchedule", () => {
endTime: `${plus3DateString}T04:15:00.000Z`,
},
],
hosts: [],
});
const scheduleForTeamEventOnADayWithOneBookingForEachUserButOnDifferentTimeslots = await getSchedule(
{
@@ -1064,9 +1161,10 @@ function addEventTypes(eventTypes: InputEventType[], usersStore: InputUser[]) {
throw new Error(`eventTypes[number]: id ${eventType.id} is not unique`);
}
foundEvents[eventType.id] = true;
const users = eventType.users.map((userWithJustId) => {
return usersStore.find((user) => user.id === userWithJustId.id);
});
const users =
eventType.users?.map((userWithJustId) => {
return usersStore.find((user) => user.id === userWithJustId.id);
}) || [];
return {
...baseEventType,
...eventType,
@@ -1099,11 +1197,32 @@ async function addBookings(bookings: InputBooking[], eventTypes: InputEventType[
bookings
// We can improve this filter to support the entire where clause but that isn't necessary yet. So, handle what we know we pass to `findMany` and is needed
.filter((booking) => {
/**
* A user is considered busy within a given time period if there
* is a booking they own OR host. This function mocks some of the logic
* for each condition. For details see the following ticket:
* https://github.com/calcom/cal.com/issues/6374
*/
// ~~ FIRST CONDITION ensures that this booking is owned by this user
// and that the status is what we want
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const statusIn = where.status?.in || [];
// Return bookings passing status prisma where
return statusIn.includes(booking.status) && booking.userId === where.userId;
const statusIn = where.OR[0].status?.in || [];
const firstConditionMatches =
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
statusIn.includes(booking.status) && booking.userId === where.OR[0].userId;
// ~~ SECOND CONDITION checks whether this user is a host of this Event Type
// and that booking.status is a match for the returned query
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
let secondConditionMatches = where.OR[1].eventTypeId.in.includes(booking.eventTypeId);
secondConditionMatches = secondConditionMatches && statusIn.includes(booking.status);
// We return this booking if either condition is met
return firstConditionMatches || secondConditionMatches;
})
.map((booking) => ({
uid: uuidv4(),
@@ -1116,6 +1235,10 @@ async function addBookings(bookings: InputBooking[], eventTypes: InputEventType[
});
}
function addHosts(hosts: InputHost[]) {
prismaMock.host.findMany.mockResolvedValue(hosts);
}
function addUsers(users: InputUser[]) {
prismaMock.user.findMany.mockResolvedValue(
users.map((user) => {
@@ -1131,6 +1254,7 @@ type ScenarioData = {
// TODO: Support multiple bookings and add tests with that.
bookings?: InputBooking[];
users: InputUser[];
hosts: InputHost[];
credentials?: InputCredential[];
apps?: App[];
selectedCalendars?: InputSelectedCalendar[];
@@ -1146,6 +1270,8 @@ function createBookingScenario(data: ScenarioData) {
addUsers(data.users);
addHosts(data.hosts);
const eventType = addEventTypes(data.eventTypes, data.users);
if (data.apps) {
prismaMock.app.findMany.mockResolvedValue(data.apps as PrismaApp[]);
+88 -34
View File
@@ -34,43 +34,97 @@ export async function getBusyTimes(params: {
status: BookingStatus.ACCEPTED,
})}`
);
/**
* A user is considered busy within a given time period if there
* is a booking they own OR host.
*
* Therefore this query does the following:
* - Performs a query for all EventType id's where this user is a host
* - Performs a query for all bookings where:
* - The given booking is owned by this user, or..
* - The given booking's EventType is hosted by this user
*
* 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 busyTimes: EventBusyDetails[] = await prisma.booking
.findMany({
where: {
userId,
startTime: { gte: new Date(startTime) },
endTime: { lte: new Date(endTime) },
status: {
in: [BookingStatus.ACCEPTED],
},
},
select: {
id: true,
startTime: true,
endTime: true,
title: true,
eventType: {
select: {
id: true,
afterEventBuffer: true,
beforeEventBuffer: true,
const busyTimes: EventBusyDetails[] =
// Getting all EventTypes ID's hosted by this user
await prisma.host
.findMany({
where: {
userId: {
equals: userId,
},
},
},
})
.then((bookings) =>
bookings.map(({ startTime, endTime, title, id, eventType }) => ({
start: dayjs(startTime)
.subtract((eventType?.beforeEventBuffer || 0) + (afterEventBuffer || 0), "minute")
.toDate(),
end: dayjs(endTime)
.add((eventType?.afterEventBuffer || 0) + (beforeEventBuffer || 0), "minute")
.toDate(),
title,
source: `eventType-${eventType?.id}-booking-${id}`,
}))
);
select: {
eventTypeId: true,
},
})
// Converting the response object into an array
.then((thisUserHostedEvents) => thisUserHostedEvents.map((e) => e.eventTypeId))
// Finding all bookings owned OR hosted by this user
.then((thisUserHostedEventIds) => {
// This gets applied to both conditions
const sharedQuery = {
startTime: { gte: new Date(startTime) },
endTime: { lte: new Date(endTime) },
status: {
in: [BookingStatus.ACCEPTED],
},
};
return prisma.booking.findMany({
where: {
OR: [
// Bookings owned by this user
{
...sharedQuery,
userId,
},
// Bookings with an EventType ID that's hosted by this user
{
...sharedQuery,
eventTypeId: {
in: thisUserHostedEventIds,
},
},
],
},
select: {
id: true,
startTime: true,
endTime: true,
title: true,
eventType: {
select: {
id: true,
afterEventBuffer: true,
beforeEventBuffer: true,
},
},
},
});
})
.then((bookings) =>
bookings.map(({ startTime, endTime, title, id, eventType }) => ({
start: dayjs(startTime)
.subtract((eventType?.beforeEventBuffer || 0) + (afterEventBuffer || 0), "minute")
.toDate(),
end: dayjs(endTime)
.add((eventType?.afterEventBuffer || 0) + (beforeEventBuffer || 0), "minute")
.toDate(),
title,
source: `eventType-${eventType?.id}-booking-${id}`,
}))
);
logger.silly(`Busy Time from Cal Bookings ${JSON.stringify(busyTimes)}`);
performance.mark("prismaBookingGetEnd");
performance.measure(`prisma booking get took $1'`, "prismaBookingGetStart", "prismaBookingGetEnd");