lib/api/routes/meetings/delete.tsx
import { NextApiRequest as Req, NextApiResponse as Res } from 'next';
import { RRule } from 'rrule';
import { Meeting, MeetingAction, MeetingJSON } from 'lib/model/meeting';
import { deleteMeeting, getMeeting, updateMeeting } from 'lib/api/db/meeting';
import getLastTime from 'lib/api/get/last-time';
import getMeetingVenue from 'lib/api/get/meeting-venue';
import { getOrg } from 'lib/api/db/org';
import getPeople from 'lib/api/get/people';
import getPerson from 'lib/api/get/person';
import { handle } from 'lib/api/error';
import logger from 'lib/api/logger';
import mail from 'lib/mail/meetings/delete';
import segment from 'lib/api/segment';
import updatePeopleTags from 'lib/api/update/people-tags';
import { updateUser } from 'lib/api/db/user';
import verifyAuth from 'lib/api/verify/auth';
import verifyOptions from 'lib/api/verify/options';
import { verifyQueryIdNum } from 'lib/api/verify/query-id';
export type DeleteMeetingRes = void;
export interface DeleteMeetingOptions {
deleting: MeetingJSON;
action: MeetingAction;
}
export default async function deleteMeetingAPI(
req: Req,
res: Res<DeleteMeetingRes>
): Promise<void> {
try {
const id = verifyQueryIdNum(req.query);
const meeting = await getMeeting(id);
logger.info(`Deleting ${meeting.toString()}...`);
// TODO: Verify the option data types just like we do for the request body.
const options = verifyOptions<DeleteMeetingOptions>(req.body, {
deleting: meeting.toJSON(),
action: 'future',
});
const deleting = Meeting.fromJSON(options.deleting);
const { uid } = await verifyAuth(req.headers, {
userIds: meeting.people.map((p) => p.id),
orgIds: [meeting.org],
});
const org = await getOrg(meeting.org);
const people = await getPeople(meeting.people);
const deleter = await getPerson({ id: uid }, people);
// User is deleting a recurring meeting. We will either:
// - Delete all meetings.
// - Only delete this meeting.
// - Delete this and following meetings.
const isRecurring = meeting.time.recur && meeting.id !== deleting.id;
if (isRecurring && options.action === 'this') {
// Delete this meeting only:
// 1. Add date exception to parent meeting instance.
// TODO: Exdates have to be exact dates that would otherwise be
// generated by the RRuleSet. This makes excluded dates re-appear when
// the parent recurring meeting's time is changed. Instead, we want to
// exclude all instances on a given date, regardless of exact time.
//
// To recreate issue:
// 1. Create a new daily recurring meeting.
// 2. Reschedule a single meeting instance.
// 3. Reschedule the original recurring meeting.
// 4. Notice how the single meeting exception disappears.
meeting.time.exdates = [
...(meeting.time.exdates || []),
deleting.time.from,
];
meeting.time.last = getLastTime(meeting.time);
meeting.venue = getMeetingVenue(meeting, org, people);
// TODO: Specify in email that this is only canceling this meeting.
await Promise.all([
updateMeeting(meeting),
mail(deleting, deleter),
]);
} else if (isRecurring && options.action === 'future') {
// Delete this and all following meetings:
// 1. Add 'until' to parent meeting's recur rule to exclude this meeting.
// TODO: This `until` property should be 12am (on the original meeting
// date) in the user's local timezone (NOT the server timezone).
meeting.time.recur = RRule.optionsToString({
...RRule.parseString(meeting.time.recur as string),
until: new Date(
deleting.time.from.getFullYear(),
deleting.time.from.getMonth(),
deleting.time.from.getDate()
),
});
meeting.time.last = getLastTime(meeting.time);
meeting.venue = getMeetingVenue(meeting, org, people);
// TODO: Specify in email that this is canceling all following meetings.
await Promise.all([
updateMeeting(meeting),
mail(deleting, deleter),
]);
} else {
// Delete all meetings. Identical to deleting a non-recurring meeting.
await Promise.all([
deleteMeeting(meeting.id),
mail(meeting, deleter),
]);
}
res.status(200).end();
logger.info(`Deleted ${deleting.toString()}.`);
segment.track({
userId: uid,
event: 'Meeting Deleted',
properties: deleting.toSegment(),
});
// TODO: Ensure that this updates the org statistics as expected (e.g. we
// don't want to decrease the total # of meetings if only a single meeting
// instance is deleted and it's parent recurring meeting remains).
// TODO: We shouldn't remove the `meeting` tag from a user if they still
// have other meetings. Perhaps calculate this using a CRON job instead.
await Promise.all([
updatePeopleTags(people, { remove: ['meeting'] }),
Promise.all(people.map((p) => updateUser(p))),
]);
} catch (e) {
handle(e, res);
}
}