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
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 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
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 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 RecurrenceRule
s 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
BaseRecurringEvent
if 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
BaseRecurringEvent
if itseventEndDate
matches therecurrenceEndDate
of 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).