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: Update Single and Recurring Events #1884

Merged
9 changes: 8 additions & 1 deletion schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -919,7 +919,7 @@ type Mutation {
updateActionItemCategory(data: UpdateActionItemCategoryInput!, id: ID!): ActionItemCategory
updateAdvertisement(input: UpdateAdvertisementInput!): UpdateAdvertisementPayload
updateAgendaCategory(id: ID!, input: UpdateAgendaCategoryInput!): AgendaCategory
updateEvent(data: UpdateEventInput, id: ID!): Event!
updateEvent(data: UpdateEventInput, id: ID!, recurrenceRuleData: RecurrenceRuleInput, recurringEventUpdateType: RecurringEventUpdateType): Event!
updateEventVolunteer(data: UpdateEventVolunteerInput, id: ID!): EventVolunteer!
updateFund(data: UpdateFundInput!, id: ID!): Fund!
updateLanguage(languageCode: String!): User!
Expand Down Expand Up @@ -1235,6 +1235,12 @@ input RecurrenceRuleInput {
weekDays: [WeekDays]
}

enum RecurringEventUpdateType {
AllInstances
ThisAndFollowingInstances
ThisInstance
}

"""
Possible variants of ordering in which sorting on a field should be
applied for a connection or other list type data structures.
Expand Down Expand Up @@ -1333,6 +1339,7 @@ input UpdateEventInput {
endDate: Date
endTime: Time
isPublic: Boolean
isRecurringEventException: Boolean
isRegisterable: Boolean
latitude: Latitude
location: String
Expand Down
51 changes: 51 additions & 0 deletions src/helpers/event/updateEventHelpers/getEventData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { InterfaceEvent } from "../../../models";
import type {
Recurrance,
UpdateEventInput,
} from "../../../types/generatedGraphQLTypes";
import type { InterfaceRecurringEvent } from "../recurringEventHelpers/generateRecurringEventInstances";

/**
* This function get the data to be used for generating the recurring event instances.
* @param updateEventInputData - the update event input data.
* @param event - the event to be updated.
* @remarks The following steps are followed:
* 1. get the current event data.
* 2. update the data provided in the input.
* @returns The updated event data.
*/

export const getEventData = (
updateEventInputData: UpdateEventInput | undefined | null,
event: InterfaceEvent,
): InterfaceRecurringEvent => {
// get the event's current data
const eventCurrentData = {
title: event.title,
description: event.description,
startDate: event.startDate,
endDate: event.endDate,
startTime: event.startTime,
endTime: event.endTime,
allDay: event.allDay,
recurring: event.recurring,
recurrance: event.recurrance,
isPublic: event.isPublic,
isRegisterable: event.isRegisterable,
admins: event.admins,
location: event.location,
latitude: event.latitude,
longitude: event.longitude,
creatorId: event.creatorId,
organizationId: event.organization,
};

// get the updated event data
const updatedEventData: InterfaceRecurringEvent = {
...eventCurrentData,
...(updateEventInputData as Partial<InterfaceRecurringEvent>),
recurrance: eventCurrentData.recurrance as Recurrance,
};

return updatedEventData;
};
2 changes: 2 additions & 0 deletions src/helpers/event/updateEventHelpers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { updateSingleEvent } from "./updateSingleEvent";
export { updateRecurringEvent } from "./updateRecurringEvent";
67 changes: 67 additions & 0 deletions src/helpers/event/updateEventHelpers/updateAllInstances.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import type mongoose from "mongoose";
import type { InterfaceEvent } from "../../../models";
import { Event } from "../../../models";
import type { MutationUpdateEventArgs } from "../../../types/generatedGraphQLTypes";
import type { InterfaceRecurrenceRule } from "../../../models/RecurrenceRule";

/**
* This function updates all instances of the recurring event following the given recurrenceRule.
* @param args - update event args.
* @param event - the event to be updated.
* @param recurrenceRule - the recurrence rule followed by the instances.
* @param baseRecurringEvent - the base recurring event.
* @remarks The following steps are followed:
* 1. get the current event data.
* 2. update the data provided in the input.
* @returns The updated event.
*/

export const updateAllInstances = async (
args: MutationUpdateEventArgs,
event: InterfaceEvent,
recurrenceRule: InterfaceRecurrenceRule,
baseRecurringEvent: InterfaceEvent,
session: mongoose.ClientSession,
): Promise<InterfaceEvent> => {
if (
(!recurrenceRule.endDate && !baseRecurringEvent.endDate) ||
(recurrenceRule.endDate &&
baseRecurringEvent.endDate &&
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
await Event.updateOne(
{
_id: baseRecurringEvent._id,
},
{
...(args.data as Partial<InterfaceEvent>),
},
{
session,
},
);
}

// update all the instances following this recurrence rule and are not exceptions
await Event.updateMany(
{
recurrenceRuleId: recurrenceRule._id,
isRecurringEventException: false,
},
{
...(args.data as Partial<InterfaceEvent>),
},
{
session,
},
);

const updatedEvent = await Event.findOne({
_id: event._id,
}).lean();

return updatedEvent as InterfaceEvent;
};
67 changes: 67 additions & 0 deletions src/helpers/event/updateEventHelpers/updateRecurringEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import type mongoose from "mongoose";
import type { InterfaceEvent } from "../../../models";
import { Event } from "../../../models";
import type { MutationUpdateEventArgs } from "../../../types/generatedGraphQLTypes";
import { updateThisInstance } from "./updateThisInstance";
import { updateAllInstances } from "./updateAllInstances";
import { updateThisAndFollowingInstances } from "./updateThisAndFollowingInstances";
import { RecurrenceRule } from "../../../models/RecurrenceRule";

/**
* This function updates the recurring event.
* @param args - update event args.
* @param event - the event to be updated.
* @remarks The following steps are followed:
* 1. get the recurrence rule.
* 2. get the base recurring event.
* 3. based on the type of update, call the function required.
* @returns The updated event.
*/

export const updateRecurringEvent = async (
args: MutationUpdateEventArgs,
event: InterfaceEvent,
session: mongoose.ClientSession,
): Promise<InterfaceEvent> => {
let updatedEvent: InterfaceEvent = event;

// get the recurrenceRule
const recurrenceRule = await RecurrenceRule.find({
_id: event.recurrenceRuleId,
});

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

if (
(args.data?.isRecurringEventException !== undefined &&
args.data?.isRecurringEventException !==
event.isRecurringEventException) ||
args.recurringEventUpdateType === "ThisInstance"
) {
// if this is a single update or if the event's exception status has changed
updatedEvent = await updateThisInstance(args, event, session);
} else if (args.recurringEventUpdateType === "AllInstances") {
// perform a regular bulk update on all the instances
updatedEvent = await updateAllInstances(
args,
event,
recurrenceRule[0],
baseRecurringEvent[0],
session,
);
} else {
// update current and following events
updatedEvent = await updateThisAndFollowingInstances(
args,
event,
recurrenceRule[0],
baseRecurringEvent[0],
session,
);
}

return updatedEvent;
};
155 changes: 155 additions & 0 deletions src/helpers/event/updateEventHelpers/updateSingleEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import type mongoose from "mongoose";
import type { InterfaceEvent } from "../../../models";
import { Event, EventAttendee, User } from "../../../models";
import { cacheEvents } from "../../../services/EventCache/cacheEvents";
import type { MutationUpdateEventArgs } from "../../../types/generatedGraphQLTypes";
import { getEventData } from "./getEventData";
import {
createRecurrenceRule,
generateRecurrenceRuleString,
generateRecurringEventInstances,
getRecurringInstanceDates,
} from "../recurringEventHelpers";

/**
* This function updates a single non-recurring event.
* @param args - the arguments provided for the updateEvent mutation.
* @param event - the single event to be updated.
* @remarks The following steps are followed:
* 1. If the single event is made recurring with this update:
* - get the appropriate data to create the baseRecurringEvent and recurring event instances.
* - generate the instances with createRecurringEvent function.
* - remove the current event and its associations as a new series has been created.
* 2. If it's still a non-recurring event:
* - just perform a regular update.
* @returns The updated event.
*/

export const updateSingleEvent = async (
args: MutationUpdateEventArgs,
event: InterfaceEvent,
session: mongoose.ClientSession,
): Promise<InterfaceEvent> => {
let updatedEvent: InterfaceEvent = event;

if (args.data?.recurring) {
// get the data from args
const { data: updateEventInputData } = args;
let { recurrenceRuleData } = args;

// get latest eventData to be used for baseRecurringEvent and recurring instances
const eventData = getEventData(updateEventInputData, event);

if (!recurrenceRuleData) {
// create a default weekly recurrence rule
recurrenceRuleData = {
frequency: "WEEKLY",
};
}

// get the recurrence startDate, if provided, else, use event startDate
const startDate = new Date(eventData.startDate);
// get the recurrence endDate, if provided or made null (infinitely recurring)
const endDate = eventData.endDate ? new Date(eventData.endDate) : null;

// generate a recurrence rule string which would be used to generate rrule object
const recurrenceRuleString = generateRecurrenceRuleString(
recurrenceRuleData,
startDate,
endDate ? endDate : undefined,
);

// create a baseRecurringEvent
const baseRecurringEvent = await Event.create(
[
{
...eventData,
organization: eventData.organizationId,
recurring: true,
isBaseRecurringEvent: true,
},
],
{ session },
);

// get recurrence dates
const recurringInstanceDates = getRecurringInstanceDates(
recurrenceRuleString,
startDate,
endDate,
);

// get the startDate of the latest instance following the recurrence
const latestInstanceDate =
recurringInstanceDates[recurringInstanceDates.length - 1];

// create the recurrencerule
const recurrenceRule = await createRecurrenceRule(
recurrenceRuleString,
startDate,
endDate,
eventData.organizationId,
baseRecurringEvent[0]._id.toString(),
latestInstanceDate,
session,
);

// generate the recurring instances and get an instance back
updatedEvent = await generateRecurringEventInstances({
data: eventData,
baseRecurringEventId: baseRecurringEvent[0]._id.toString(),
recurrenceRuleId: recurrenceRule?._id.toString(),
recurringInstanceDates,
creatorId: event.creatorId,
organizationId: eventData.organizationId,
session,
});

// remove the current event and its association
await Promise.all([
Event.deleteOne(
{
_id: event._id,
},
{ session },
),
EventAttendee.deleteOne({
eventId: event._id,
userId: event.creatorId,
}),
User.updateOne(
{
_id: event.creatorId,
},
{
$pull: {
eventAdmin: event._id,
createdEvents: event._id,
registeredEvents: event._id,
},
},
{ session },
),
]);
} else {
// else (i.e. the event is still non-recurring), just perform a regular update
updatedEvent = await Event.findOneAndUpdate(
{
_id: args.id,
},
{
...(args.data as Partial<InterfaceEvent>),
},
{
new: true,
session,
},
).lean();

if (updatedEvent !== null) {
await cacheEvents([updatedEvent]);
}
}

return updatedEvent;
};
Loading
Loading