Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: delete recurring events #1917

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -935,7 +935,7 @@ type Mutation {
removeAdmin(data: UserAndOrganizationInput!): User!
removeComment(id: ID!): Comment
removeDirectChat(chatId: ID!, organizationId: ID!): DirectChat!
removeEvent(id: ID!): Event!
removeEvent(id: ID!, recurringEventDeleteType: RecurringEventMutationType): Event!
removeEventAttendee(data: EventAttendeeInput!): User!
removeEventVolunteer(id: ID!): EventVolunteer!
removeFund(id: ID!): Fund!
Expand Down Expand Up @@ -970,7 +970,7 @@ type Mutation {
updateActionItemCategory(data: UpdateActionItemCategoryInput!, id: ID!): ActionItemCategory
updateAdvertisement(input: UpdateAdvertisementInput!): UpdateAdvertisementPayload
updateAgendaCategory(id: ID!, input: UpdateAgendaCategoryInput!): AgendaCategory
updateEvent(data: UpdateEventInput, id: ID!, recurrenceRuleData: RecurrenceRuleInput, recurringEventUpdateType: RecurringEventUpdateType): Event!
updateEvent(data: UpdateEventInput, id: ID!, recurrenceRuleData: RecurrenceRuleInput, recurringEventUpdateType: RecurringEventMutationType): Event!
updateEventVolunteer(data: UpdateEventVolunteerInput, id: ID!): EventVolunteer!
updateFund(data: UpdateFundInput!, id: ID!): Fund!
updateFundraisingCampaign(data: UpdateFundCampaignInput!, id: ID!): FundraisingCampaign!
Expand Down Expand Up @@ -1291,7 +1291,7 @@ input RecurrenceRuleInput {
weekDays: [WeekDays]
}

enum RecurringEventUpdateType {
enum RecurringEventMutationType {
AllInstances
ThisAndFollowingInstances
ThisInstance
Expand Down
59 changes: 59 additions & 0 deletions src/helpers/event/deleteEventHelpers/deleteRecurringEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type mongoose from "mongoose";
import type { MutationRemoveEventArgs } from "../../../types/generatedGraphQLTypes";
import { RecurrenceRule } from "../../../models/RecurrenceRule";
import type { InterfaceEvent } from "../../../models";
import { Event } from "../../../models";
import { deleteSingleEvent, deleteRecurringEventInstances } from "./index";

/**
* This function deletes thisInstance / allInstances / thisAndFollowingInstances of a recurring event.
* @param args - removeEventArgs
* @param event - an instance of the recurring event to be deleted.
* @remarks The following steps are followed:
* 1. get the recurrence rule and the base recurring event.
* 2. if the instance is an exception instance or if we're deleting thisInstance only, just delete that single instance.
* 3. if it's a bulk delete operation, handle it accordingly.
*/

export const deleteRecurringEvent = async (
args: MutationRemoveEventArgs,
event: InterfaceEvent,
session: mongoose.ClientSession,
): Promise<void> => {
// get the recurrenceRule
const recurrenceRule = await RecurrenceRule.find({
_id: event.recurrenceRuleId,
});

// get the baseRecurringEvent
const baseRecurringEvent = await Event.find({
_id: event.baseRecurringEventId,
});

if (
event.isRecurringEventException ||
args.recurringEventDeleteType === "ThisInstance"
) {
// if the event is an exception or if it's deleting thisInstance only,
// just delete this single instance
await deleteSingleEvent(event._id.toString(), session);
} else if (args.recurringEventDeleteType === "AllInstances") {
// delete all the instances
// and update the recurrenceRule and baseRecurringEvent accordingly
await deleteRecurringEventInstances(
null, // because we're going to delete all the instances, which we could get from the recurrence rule
recurrenceRule[0],
baseRecurringEvent[0],
session,
);
} else {
// delete this and following the instances
// and update the recurrenceRule and baseRecurringEvent accordingly
await deleteRecurringEventInstances(
event, // we'll find all the instances after(and including) this one and delete them
recurrenceRule[0],
baseRecurringEvent[0],
session,
);
}
};
178 changes: 178 additions & 0 deletions src/helpers/event/deleteEventHelpers/deleteRecurringEventInstances.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import type mongoose from "mongoose";
import type { InterfaceRecurrenceRule } from "../../../models/RecurrenceRule";
import { RecurrenceRule } from "../../../models/RecurrenceRule";
import type { InterfaceEvent } from "../../../models";
import { ActionItem, Event, EventAttendee, User } from "../../../models";
import { shouldUpdateBaseRecurringEvent } from "../updateEventHelpers";
import type { Types } from "mongoose";

/**
* This function deletes allInstances / thisAndFollowingInstances of a recurring event.
* @param event - the event to be deleted:
* - in case of deleting thisAndFollowingInstances, it would represent this instance.
* - in case of deleting allInstances, it would be null.
* @param recurrenceRule - the recurrence rule followed by the instances.
* @param baseRecurringEvent - the base recurring event.
* @remarks The following steps are followed:
* 1. get the instances to be deleted.
* 2. remove the associations of the instances.
* 3. delete the instances.
* 4. update the recurrenceRule and baseRecurringEvent accordingly.
*/

export const deleteRecurringEventInstances = async (
event: InterfaceEvent | null,
recurrenceRule: InterfaceRecurrenceRule,
baseRecurringEvent: InterfaceEvent,
session: mongoose.ClientSession,
): Promise<void> => {
// get the query object:
// if we're deleting thisAndFollowingInstance, it would find all the instances after(and including) this one
// if we're deleting allInstances, it would find all the instances
const query: {
recurrenceRuleId: Types.ObjectId;
isRecurringEventException: boolean;
startDate?: { $gte: string };
} = {
recurrenceRuleId: recurrenceRule._id,
isRecurringEventException: false,
};

if (event) {
query.startDate = { $gte: event.startDate };
}

// get all the instances to be deleted
const recurringEventInstances = await Event.find(query);

// get the ids of those instances
const recurringEventInstancesIds = recurringEventInstances.map(
(recurringEventInstance) => recurringEventInstance._id,
);

// remove all the associations for the instances that are deleted
await Promise.all([
EventAttendee.deleteMany(
{ eventId: { $in: recurringEventInstancesIds } },
{ session },
),
User.updateMany(
{
$or: [
{ createdEvents: { $in: recurringEventInstancesIds } },
{ eventAdmin: { $in: recurringEventInstancesIds } },
{ registeredEvents: { $in: recurringEventInstancesIds } },
],
},
{
$pull: {
createdEvents: { $in: recurringEventInstancesIds },
eventAdmin: { $in: recurringEventInstancesIds },
registeredEvents: { $in: recurringEventInstancesIds },
},
},
{ session },
),
ActionItem.deleteMany(
{ eventId: { $in: recurringEventInstancesIds } },
{ session },
),
]);

// delete the instances
await Event.deleteMany(
{
_id: { $in: recurringEventInstancesIds },
},
{
session,
},
);

// get the instances following the current recurrence rule (if any)
const instancesFollowingCurrentRecurrence = await Event.find(
{
recurrenceRuleId: recurrenceRule._id,
isRecurringEventException: false,
},
null,
{ session },
).sort({ startDate: -1 });

// check if more instances following this recurrence rule still exist
const moreInstancesExist =
instancesFollowingCurrentRecurrence &&
instancesFollowingCurrentRecurrence.length;

if (moreInstancesExist) {
// get the latest instance following the old recurrence rule
const updatedEndDateString =
instancesFollowingCurrentRecurrence[0].startDate;
const updatedEndDate = new Date(updatedEndDateString);

// update the latestInstanceDate and endDate of the current recurrenceRule
await RecurrenceRule.findOneAndUpdate(
{
_id: recurrenceRule._id,
},
{
latestInstanceDate: updatedEndDate,
endDate: updatedEndDate,
},
{ session },
).lean();

// update the baseRecurringEvent if it is the latest recurrence rule that the instances were following
if (
shouldUpdateBaseRecurringEvent(
recurrenceRule.endDate?.toString(),
baseRecurringEvent.endDate?.toString(),
)
) {
await Event.updateOne(
{
_id: baseRecurringEvent._id,
},
{
endDate: updatedEndDateString,
},
{
session,
},
);
}
} else {
// if no instances conforming to the current recurrence rule exist
// find any previous recurrence rules that were associated with this baseRecurringEvent
const previousRecurrenceRules = await RecurrenceRule.find(
{
baseRecurringEventId: baseRecurringEvent._id,
endDate: { $lt: recurrenceRule.startDate },
},
null,
{ session },
)
.sort({ endDate: -1 })
.lean();

const previousRecurrenceRulesExist =
previousRecurrenceRules && previousRecurrenceRules.length;
if (previousRecurrenceRulesExist) {
// update the baseRecurringEvent if it is the latest recurrence rule that the instances were following
if (
shouldUpdateBaseRecurringEvent(
recurrenceRule.endDate?.toString(),
baseRecurringEvent.endDate?.toString(),
)
) {
await Event.updateOne(
{
_id: baseRecurringEvent._id,
},
{ endDate: previousRecurrenceRules[0].endDate.toISOString() },
{ session },
);
}
}
}
};
53 changes: 53 additions & 0 deletions src/helpers/event/deleteEventHelpers/deleteSingleEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type mongoose from "mongoose";
import { ActionItem, Event, EventAttendee, User } from "../../../models";

/**
* This function deletes a single event.
* @param event - the event to be deleted:
* @remarks The following steps are followed:
* 1. remove the associations of the event.
* 2. delete the event.
*/

export const deleteSingleEvent = async (
eventId: string,
session: mongoose.ClientSession,
): Promise<void> => {
// remove the associations of the current event
await Promise.all([
EventAttendee.deleteMany(
{
eventId,
},
{ session },
),
User.updateMany(
{
$or: [
{ createdEvents: eventId },
{ eventAdmin: eventId },
{ registeredEvents: eventId },
],
},
{
$pull: {
createdEvents: eventId,
eventAdmin: eventId,
registeredEvents: eventId,
},
},
{ session },
),
ActionItem.deleteMany({ eventId }, { session }),
]);

// delete the event
await Event.deleteOne(
{
_id: eventId,
},
{
session,
},
);
};
3 changes: 3 additions & 0 deletions src/helpers/event/deleteEventHelpers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { deleteSingleEvent } from "./deleteSingleEvent";
export { deleteRecurringEvent } from "./deleteRecurringEvent";
export { deleteRecurringEventInstances } from "./deleteRecurringEventInstances";
2 changes: 2 additions & 0 deletions src/helpers/event/updateEventHelpers/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export { getEventData } from "./getEventData";
export { updateSingleEvent } from "./updateSingleEvent";
export { updateRecurringEvent } from "./updateRecurringEvent";
export { shouldUpdateBaseRecurringEvent } from "./shouldUpdateBaseRecurringEvent";
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* This function checks whether the baseRecurringEvent should be updated.
* @param recurrenceRuleEndDate - the end date of the recurrence rule.
* @param baseRecurringEventEndDate - the end date of the base recurring event.
* @returns true if the recurrence rule is the latest rule that the instances were following, false otherwise.
*/

export const shouldUpdateBaseRecurringEvent = (
recurrenceRuleEndDate: string | null | undefined,
baseRecurringEventEndDate: string | null | undefined,
): boolean => {
// if the endDate matches then return true, otherwise false
return (!recurrenceRuleEndDate && !baseRecurringEventEndDate) ||
(recurrenceRuleEndDate &&
baseRecurringEventEndDate &&
recurrenceRuleEndDate === baseRecurringEventEndDate)
? true
: false;
};
10 changes: 5 additions & 5 deletions src/helpers/event/updateEventHelpers/updateAllInstances.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { InterfaceEvent } from "../../../models";
import { Event } from "../../../models";
import type { MutationUpdateEventArgs } from "../../../types/generatedGraphQLTypes";
import type { InterfaceRecurrenceRule } from "../../../models/RecurrenceRule";
import { shouldUpdateBaseRecurringEvent } from "./index";

/**
* This function updates all instances of the recurring event following the given recurrenceRule.
Expand All @@ -24,11 +25,10 @@ export const updateAllInstances = async (
session: mongoose.ClientSession,
): Promise<InterfaceEvent> => {
if (
(!recurrenceRule.endDate && !baseRecurringEvent.endDate) ||
(recurrenceRule.endDate &&
baseRecurringEvent.endDate &&
recurrenceRule.endDate.toString() ===
baseRecurringEvent.endDate.toString())
shouldUpdateBaseRecurringEvent(
recurrenceRule?.endDate?.toString(),
baseRecurringEvent?.endDate?.toString(),
)
) {
// if this was the latest recurrence rule, then update the baseRecurringEvent
// because new instances following this recurrence rule would be generated based on baseRecurringEvent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
generateRecurringEventInstances,
getRecurringInstanceDates,
} from "../recurringEventHelpers";
import { getEventData } from "./getEventData";
import { getEventData, shouldUpdateBaseRecurringEvent } from "./index";

/**
* This function updates this and the following instances of a recurring event.
Expand Down Expand Up @@ -198,12 +198,10 @@ export const updateThisAndFollowingInstances = async (

// update the baseRecurringEvent if it is the latest recurrence rule that the instances are following
if (
recurrenceRule &&
((!recurrenceRule.endDate && !baseRecurringEvent.endDate) ||
(recurrenceRule.endDate &&
baseRecurringEvent.endDate &&
recurrenceRule.endDate.toString() ===
baseRecurringEvent.endDate.toString()))
shouldUpdateBaseRecurringEvent(
recurrenceRule?.endDate?.toString(),
baseRecurringEvent?.endDate?.toString(),
)
) {
await Event.updateOne(
{
Expand Down
Loading
Loading