Recurring Events
Goals
What do we need?
- The functionalities for creating recurring events, with custom recurrence patterns, like the top calendar apps out there.
- The functionalities for updating / deleting:
- a single instance
- this and following instances
- all instances
of a recurring event
- A way to track the historical records of a recurring event.
Interfaces
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.
Approach
- We are using the
rrule
libary and following the dynamic generation approach.
Creating recurring events
-
Create event input:
- For recurring events, along with the general
EventInput
, there would also be aRecurrenceRuleInput
(which, if not provided, would default to infinite weekly recurrence), containing the recurrence pattern.
- For recurring events, along with the general
-
After getting the input, we'd follow these steps (createRecurringEvent.ts):
-
Generate a
recurrenceRuleString
from ourRecurrenceRuleInput
that would specify our recurrence rule inrrule
string format (generateRecurrenceRuleString.ts). -
Create a
BaseRecurringEvent
that would just be like creating a normal event withisBaseRecurringEvent: true
, let's name it's_id
to bebaseRecurringEventId
(This is what we will use as the base event for generating instances.) -
Get the dates of recurrence using the
rrule
library (getRecurringInstanceDates.ts):- Fix a
limitEndDate
, sayX
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 thecreateEvent
mutation. We'll leave the rest for dynamic generation during the eventsquery
. - If
recurrenceEndDate: null
orrecurrenceEndDate > limitEndDate
, we'd generate dates uptolimitEndDate
, and leave the rest for dynamic generation during queries. - If
recurrenceEndDate < limitEndDate
, then we just generate all the dates of recurrence.
- Fix a
-
Both
RecurrenceRule
&BaseRecurringEvent
will contain theserecurrenceStartDate
and therecurrenceEndDate
values as provided in theRecurrenceRuleInput
.-
EventInput
:-
startDate
: Start Date of that event instance. -
endDate
: End Date of that event instance.noteThese 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
startDate: "2024-18-04"
&endDate: "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 thestartDate
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.noteOnly one of
recurrenceEndDate
orcount
will exist. i.e. if we select a specific end date of recurrence,count
will be null, if we chose a specific count istead, thenrecurrenceEndDate
will be null.
-
-
-
Create a
RecurrenceRule
document that would contain therecurrenceRuleString
and the recurrence fields for easy understanding and debugging, let's name this document's_id
to berecurranceRuleId
. Set it'slatestInstanceDate
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 theBaseRecurringEvent
document that we created above. -
All of the instances would have their recurrenceRuleId field set to
recurranceRuleId
, and the BaseRecurringEventId set tobaseRecurringEventId
.
-
Updating recurring events
-
For single events made recurring (updateSingleEvent.ts):
- Get the data used to generate the instances (i.e. the current data of the event, and the latest data from the update input).
- Follow the steps for creating a recurring event.
- Delete the current event and its associations, as new ones would be made while generating new instances.
-
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 theinstanceDuration
(difference between event's start and end dates), or both have changed.- If neither the
recurrenceRule
nor theinstanceDuration
have changed, then we will provide all three update options. - If the
RecurrenceRule
has changed, then we will not provide the option to updatethisInstance
, i.e. onlythisAndFollowingInstances
&allInstances
. - If the
instanceDuration
has changed, then we will not provide the option to updateallInstances
, i.e. onlythisInstance
&thisAndFollowingInstances
.
- If neither the
-
Update Options:
-
thisInstance
: Just make a regular update on this event instance (updateThisInstance.ts)noteUpdating a single recurring event instance will make it an exception instance.
-
thisAndFollowingInstances
orallInstances
(updateRecurringEventInstances.ts):-
If neither of the
recurrenceRule
or theinstanceDuration
has changed, we will just perform a bulk update on the instances. -
If either one of the
recurrenceRule
or theinstanceDuration
has changed, we will delete the current series, remove their associations and generate a new one:- Delete instances conforming to the old
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). - Find the latest instance that was following the old
RecurrenceRule
, saylatestInstance
, and set thelatestInstanceDate
and therecurrenceEndDate
of the oldRecurrenceRule
to be thislatestInstance
'sstartDate
. - Generate new instances based on the new
RecurrenceRule
and the updated event data. - Now, all the previous instances would have a different
RecurrenceRule
than the current and future ones.
- Delete instances conforming to the old
-
Update the
BaseRecurringEvent
document if required to have values of the current update input (which would then be used as the new base event).
noteHere we're not creating a new
BaseRecurringEvent
document, 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 recurring events
-
Deleting this instance only / deleting an exception instance (deleteSingleEvent.ts):
- Make a regular deletion.
-
Deleting all instances / this and future instances (deleteRecurringEventInstances.ts):
-
For deleting all instances:
- Delete all the recurring instances with the current
recurrenceRuleId
. - If this was the latest
RecurrenceRule
, and there exist one or moreRecurrenceRule
s with the samebaseRecurringEventId
, find the last one of them (i.e. one before the currentRecurrenceRule
) and update theendDate
of thebaseRecurringEvent
to be that recurrence rule'srecurrenceEndDate
.
- Delete all the recurring instances with the current
-
For this and future instances:
- Find the event instance that was created previously to the current instance with the current
recurrenceRuleId
, set thelatestInstanceDate
and therecurrenceEndDate
of theRecurrenceRule
to this instance'sstartDate
. Update theBaseRecurringEvent
accordingly if the currentRecurrenceRule
is the latest (i.e. modifying theendDate
ofBaseRecurringEvent
to this latestInstance'sstartDate
). - Delete all the recurring instances with the same
recurrenceRuleId
as the current instance, starting from the current date.
- Find the event instance that was created previously to the current instance with the current
-
Updates would only be done on the BaseRecurringEvent
if bulk operations being are done on the instances following the latest RecurrenceRule
, because we want to generate new instances (during queries) based on the BaseRecurringEvent
.
How do we ensure that?
- By adding a check, of end dates. i.e. we would only modify the
BaseRecurringEvent
if itsendDate
matches therecurrenceEndDate
of the currentRecurrenceRule
(shouldUpdateBaseRecurringEvent.ts).
Querying events
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):
- Fix a
queryUptoDate
. - Find all the
RecurrenceRule
documents with thelatestInstanceDate
less thanqueryUptoDate
. - For every recurrenceRule document queried:
- Find the
BaseRecurringEvent
. - Generate new recurring instance dates after the
latestInstanceDate
. - Account for the number of existing instances following that recurrence pattern and how many more to generate based on the
RecurrenceRule
'scount
(if specified). - Update the
latestInstanceDate
of theRecurrenceRule
. - Generate more instances based on the
BaseRecurringEvent
.
- Find the
- Fix a
-
Query events according to the inputs (
where
andsort
) and return them (eventsByOrganizationConnection.ts).
Handling exception instances
- With this approach, we don't have to worry about the single instances that have been updated/deleted, because the new instances are to be generated with
BaseRecurringEvent
. - However, if a bulk operation is made (changing the
RecurrenceRule
, or other event specific parameters), then every instance conforming to the currentRecurrenceRule
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 thatRecurrenceRule
. i.e. theRecurrenceRule
wins in the end. Same with deletion, all the events conforming to aRecurrenceRule
are deleted on a bulk delete operation. - If we want to exclude a certain instance from such operations, we could add the
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 theisRecurringEventException: false
.
Historical Records
BaseRecurringEvent
, aside from being used as the base event to create new instances, also connects all the instances, even if theirRecurrenceRule
are different.- Which means we could also use it to track the historical records for a recurring event, accross all the instances, no matter what recurrence pattern it followed at any point.
References
rrule
The library we're using that automatically generate the dates of recurrence given a RecurrenceRule
.
Official repo: rrule
RecurrenceRule
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
}
- recurrenceStartDate: the start of recurrence.
- recurrenceEndDate: the end of recurrence.
- recurrenceRuleString: an
rrule
string that would be used to generate anrrule
object, from which we would generate the recurrence dates. - frequency: Frequency of recurrence.
- weekDays: The days of the week at which the instances would be scheduled.
- interval: Interval of recurrence, i.e every day, every other day, every 5th day, etc.
- count: The number of instances following that recurrence rule.
- weekDayOccurenceInMonth: The occurence of weekDay in month, i.e whether it's the first Monday, third Monday, or last Monday. It is to be used with Monthly frequency, and a weekDay, e.g.:
- for
frequency: MONTHLY
andweekDays: ["MONDAY"]
:- if
weekDayOccurenceInMonth:2
, it would mean that the recurring event occurs every Second Monday every month. - if
weekDayOccurenceInMonth:-1
, it would mean every Last Monday every month.
- if
- for
- latestInstanceDate: the
startDate
of the latest instances generated. - baseRecurringEventId: The
BaseRecurringEvent
for that recurring event.
BaseRecurringEvent
- A special type of event, that connects all the instances of a recurring event, even across different recurrence patterns, which is useful for tracking the historical records of a recurring event.
- It is also used as the base event to generate new recurring event instances during queries (As we can't just use the latest instance, which could be an
exception
instance).
There would be a flag in the event interface indicating whether it's aBaseRecurringEvent
:
interface InterfaceEvent {
//...existing event fields
isBaseRecurringEvent: true
startDate: Date // the start of recurrence
endDate: Date // the `recurrenceEndDate` of the latest recurrence rule
}
Recurring Event Instance
Every instance of a recurring event would have these fields:
interface InterfaceEvent {
//...existing event fields
startDate: Date
endDate: Date
isBaseRecurringEvent: false
recurrenceRuleId: ObjectId
baseRecurringEvent: ObjectId
}
- startDate: The start date of the event instance.
- endDate: The end date of the event instance.
- isBaseRecurringEvent: The instance itself would not be the base recurring event.
- recurrenceRuleId: Representing the
RecurrenceRule
followed by the recurring event. - baseRecurringEventId: Representing the
BaseRecurringEvent
for that recurring event.
Recurring Event Exception Instance
- The bulk operations on a recurring event (
update
/delete
multiple instances) would not affect anexception
instance. - There would be a flag to mark an exception instance:
interface InterfaceEvent {
//...existing event fields
isRecurringEventException: true
}
- With this flag, a recurring event instance could be turned into an independent non-recurring event.
- Changing this field to
false
would again make the instance conform to theRecurrenceRule
(i.e. it would not be an exception anymore).
That explains the approach we're following for creation and management of recurring events.
The functionalities have been implemented. Check out these parent issues:
Go through these and their associated issues and PRs for more details.
Last updated on April 20, 2024