What do we need?
interface InterfaceEvent {
// ...existing event fields
isBaseRecurringEvent: boolean;
recurrenceRuleId: ObjectId;
baseRecurringEventId: ObjectId;
isRecurringEventException: boolean;
}
interface InterfaceRecurrenceRule {
recurrenceStartDate: Date;
recurrenceEndDate: Date;
recurrenceRuleString: string;
// ...recurrence specific properties (frequency, count, interval, etc.)
latestInstanceDate: Date;
baseRecurringEventId: ObjectId;
}
The purpose and need for each of the fields and Interfaces will be explained in the Approach as their necessity arises.
rrule libary and following the dynamic generation approach.Create event input:
EventInput, there would also be a RecurrenceRuleInput (which, if not provided, would default to infinite weekly recurrence), containing the recurrence pattern.After getting the input, we’d follow these steps (createRecurringEvent.ts):
Generate a recurrenceRuleString from our RecurrenceRuleInput that would specify our recurrence rule in rrule string format (generateRecurrenceRuleString.ts).
Create a BaseRecurringEvent that would just be like creating a normal event with isBaseRecurringEvent: true, let’s name it’s _id to be baseRecurringEventId (This is what we will use as the base event for generating instances.)
Get the dates of recurrence using the rrule library (getRecurringInstanceDates.ts):
limitEndDate, say X years ahead from the recurrence start date (depending on the recurrence frequency), that would help determine the date upto which we will generate instances in the createEvent mutation. We’ll leave the rest for dynamic generation during the events query.recurrenceEndDate: null or recurrenceEndDate > limitEndDate, we’d generate dates up to limitEndDate, and leave the rest for dynamic generation during queries.recurrenceEndDate < limitEndDate, then we just generate all the dates of recurrence.Both RecurrenceRule & BaseRecurringEvent will contain these recurrenceStartDate and the recurrenceEndDate values as provided in the RecurrenceRuleInput.
EventInput:
eventStartDate: Start Date of that event instance.eventEndDate: End Date of that event instance.
These dates will be selected from the create event modal, and would specify the event duration in days. i.e. If for an event, we select
eventStartDate: "2024-18-04"&eventEndDate: "2024-20-04", then all of the generated instances of that recurring event will have that two day gap between their start and end dates.
RecurrenceRuleInput:
recurrenceStartDate: Start Date of recurrence. It will be the same as the eventStartDate we select for the first instance.recurrenceEndDate: End Date of recurrence. By default, it will be null, i.e.
default infinite recurrence. It can be changed through the custom recurrence modal.
Only one of
recurrenceEndDateorcountwill exist. i.e. if we select a specific end date of recurrence,countwill be null, if we chose a specific count istead, thenrecurrenceEndDatewill be null.
Create a RecurrenceRule document that would contain the recurrenceRuleString and the recurrence fields for easy understanding and debugging, let’s name this document’s _id to be recurranceRuleId. Set it’s latestInstanceDate to be the last date generated during this mutation.
Generate the recurring event instances, make associations (attendees, user), and cache them (generateRecurringEventInstances.ts).
All of the instances (Event documents) we created in the previous step will be based on the EventInput data, and the remaining instances (if any) will be generated during queries, based on the BaseRecurringEvent document that we created above.
All of the instances would have their recurrenceRuleId field set to recurranceRuleId, and the BaseRecurringEventId set to baseRecurringEventId.
For single events made recurring (updateSingleEvent.ts):
While updating a recurring event, we will provide options to update thisInstance, thisAndFollowingInstances, & allInstances of the recurring event (updateRecurringEvent.ts).
Appropriate update options will be provided based on whether the recurrenceRule, or the instanceDuration (difference between event’s start and end dates), or both have changed.
recurrenceRule nor the instanceDuration have changed, then we will provide all three update options.RecurrenceRule has changed, then we will not provide the option to update thisInstance, i.e. only thisAndFollowingInstances & allInstances.instanceDuration has changed, then we will not provide the option to update allInstances, i.e. only thisInstance & thisAndFollowingInstances.Update Options:
thisInstance: Just make a regular update on this event instance (updateThisInstance.ts)
Updating a single recurring event instance will make it an exception instance.
thisAndFollowingInstances or allInstances (updateRecurringEventInstances.ts):
If neither of the recurrenceRule or the instanceDuration has changed, we will just perform a bulk update on the instances.
If either one of the recurrenceRule or the instanceDuration has changed, we will delete the current series, remove their associations and generate a new one:
RecurrenceRule (We can do this because we are generating events dynamically, i.e. we are only creating instances upto a certain date, so not many documents have to be deleted).RecurrenceRule, say latestInstance, and set the latestInstanceDate and the recurrenceEndDate of the old RecurrenceRule to be this latestInstance’s eventStartDate.RecurrenceRule and the updated event data.RecurrenceRule than the current and future ones.Update the BaseRecurringEvent document if required to have values of the current update input (which would then be used as the new base event).
Here we’re not creating a new
BaseRecurringEventdocument, just updating the existing one. i.e. For one recurring event, there would only be oneBaseRecurringEvent, which would connect all the instances, even accross different recurrence rules.
Deleting this instance only / deleting an exception instance (deleteSingleEvent.ts):
Deleting all instances / this and future instances (deleteRecurringEventInstances.ts):
For deleting all instances:
recurrenceRuleId.RecurrenceRule, and there exist one or more RecurrenceRules with the same baseRecurringEventId, find the last one of them (i.e. one before the current RecurrenceRule) and update the eventEndDate of the baseRecurringEvent to be that recurrence rule’s recurrenceEndDate.For this and future instances:
recurrenceRuleId, set the latestInstanceDate and the recurrenceEndDate of the RecurrenceRule to this instance’s eventStartDate. Update the BaseRecurringEvent accordingly if the current RecurrenceRule is the latest (i.e. modifying the eventEndDate of BaseRecurringEvent to this latestInstance’s eventStartDate).recurrenceRuleId as the current instance, starting from the current date.Updates would only be done on the
BaseRecurringEventif bulk operations being are done on the instances following the latestRecurrenceRule, because we want to generate new instances (during queries) based on theBaseRecurringEvent.How do we ensure that?
- By adding a check, of end dates. i.e. we would only modify the
BaseRecurringEventif itseventEndDatematches therecurrenceEndDateof the currentRecurrenceRule(shouldUpdateBaseRecurringEvent.ts).
In the query, we would add a function for generating recurring event instances, and then query all the events and return them. Here’s the two step process:
Generate recurring event instances (createRecurringEventInstancesDuringQuery.ts):
queryUptoDate.RecurrenceRule documents with the latestInstanceDate less than queryUptoDate.BaseRecurringEvent.latestInstanceDate.RecurrenceRule’s count (if specified).latestInstanceDate of the RecurrenceRule.BaseRecurringEvent.Query events according to the inputs (where and sort) and return them (eventsByOrganizationConnection.ts).
BaseRecurringEvent.RecurrenceRule, or other event specific parameters), then every instance conforming to the current RecurrenceRule is affected, even the ones that were edited seperately in single instance updates (their dates might have been changed, attendees list might have been modified, etc.), because they still follow that RecurrenceRule. i.e. the RecurrenceRule wins in the end. Same with deletion, all the events conforming to a RecurrenceRule are deleted on a bulk delete operation.isRecurringEventException: true for that instance. By doing that, we could make it completely independent (like a normal event), so that it won’t be affected by the bulk operations. If we want it to conform to the rrule again, we could just set the isRecurringEventException: false.BaseRecurringEvent, aside from being used as the base event to create new instances, also connects all the instances, even if their RecurrenceRule are different.The library we’re using that automatically generate the dates of recurrence given a RecurrenceRule.
Official repo: rrule
A document containing the properties that represents the recurrence rule followed by a recurring event.:
interface InterfaceRecurrenceRule {
recurrenceStartDate: Date
recurrenceEndDate: Date
recurrenceRuleString: string
frequency: ["DAILY", "WEEKLY", "MONTHLY", "YEARLY"]
weekdays: ["MONDAY", ... , "SUNDAY"]
interval: number
count: number
weekDayOccurenceInMonth: number
latestInstanceDate: Date
baseRecurringEventId: ObjectId
//...other fields
}
rrule string that would be used to generate an rrule object, from which we would generate the recurrence dates.frequency: MONTHLY and weekDays: ["MONDAY"]:
weekDayOccurenceInMonth:2, it would mean that the recurring event occurs every Second Monday every month.weekDayOccurenceInMonth:-1, it would mean every Last Monday every month.eventStartDate of the latest instances generated.BaseRecurringEvent for that recurring event.exception instance). BaseRecurringEvent: interface InterfaceEvent {
//...existing event fields
isBaseRecurringEvent: true
eventStartDate: Date // the start of recurrence
eventEndDate: Date // the `recurrenceEndDate` of the latest recurrence rule
}
Every instance of a recurring event would have these fields:
interface InterfaceEvent {
//...existing event fields
eventStartDate: Date
eventEndDate: Date
isBaseRecurringEvent: false
recurrenceRuleId: ObjectId
baseRecurringEvent: ObjectId
}
RecurrenceRule followed by the recurring event.BaseRecurringEvent for that recurring event.update/delete multiple instances) would not affect an exception instance. interface InterfaceEvent {
//...existing event fields
isRecurringEventException: true
}
false would again make the instance conform to the RecurrenceRule (i.e. it would not be an exception anymore).