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:
@@ -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[]);
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user