Recurring Events
Introduction
Recurring events in Google Calendar allow users to create events that repeat on a regular basis (daily, weekly, monthly, etc.). This feature is useful for scheduling regular meetings, appointments, or reminders.
Purpose
The purpose of this documentation is to explain how to create, manage, and retrieve recurring events using the Google Calendar API and sync them with Scrims Calendar API.
Key Concepts
- Recurrence Rules:
- Defined using the
RRULE
(Recurrence Rule) property in the iCalendar format. - Commonly used rules include
FREQ
(frequency),INTERVAL
,BYDAY
(day of the week), andUNTIL
(end date).
- Defined using the
- Master Event:
- The main event that defines the recurrence pattern. All occurrences of the event are generated based on this master event.
- Exception Events:
- Individual occurrences of the recurring event that have been modified (e.g., rescheduled or cancelled). These are treated as exceptions to the recurrence rule. Exceptions are identified by recurrence_id property on Scrims.
- On Google calendar exceptions are identified by the composition of the events "id" field, for example an exception of a recurring event will contain {parentId}_R{timeString} as an id.
- For nested exceptions on Google calendar recurringEventId field will be present. However, note that exceptions that are recurring (created with this-and-upcoming events update) can not have recurringEventId.
Implementation Steps
The upsertGoogleEvents
function manages the creation, updating, and deletion of events on Google Calendar. Here's how recurring events are handled within the function:
Identifying Recurring Events
The function checks if an event is recurring by examining the recurrence
array in the localEvent
object. It looks for recurrence rules (RRULE) to identify recurring events:
let localEventRecurrence = localEvent.recurrence.find(r => r.includes("RRULE"));
Handling Recurring Exceptions
If the event is an exception to a recurring event (has a recurrence_id
but no localEventRecurrence
), the function prepares to handle it differently:
if (localEvent.recurrence_id !== "" && !localEventRecurrence) { ... }
Creating New Exceptions
For exceptions, the function generates a new instance ID based on the recurrence_id
:
if (!existingEvent) {
let underlyingGoogleEvent = await Events.findOne({ atomcal_event_id: underlyingEventId }).lean();
if (underlyingGoogleEvent) {
let timestamp = localEvent.recurrence_id.split("_R")[1];
let newInstanceId = underlyingGoogleEvent.google_event_id.split(/(?:_|_r|_R|r|R)(?=\d{8}T\d{6}Z?$)/)[0] + "_" + timestamp;
finalId = newInstanceId;
}
}
Handling Existing Events
For existing events, the function tries to find the underlying event ID and set the recurringEventId
appropriately:
if (existingEvent) {
let underlyingEventId = localEvent.recurrence_id.split("_")[0];
let underlyingGoogleEvent = await Events.findOne({ atomcal_event_id: underlyingEventId }).lean();
if (underlyingGoogleEvent) {
recurringEventIdObj = { recurringEventId: underlyingGoogleEvent.google_event_id };
}
}
Determining the HTTP Method
The function decides whether to use POST
or PATCH
based on whether it is creating a new event or updating an existing one:
let method = finalId === "" ? got.post : got.patch;
crudEventUri = crudEventUri + finalId;
Creating the Request Body
The function constructs the request body with all necessary details, including the recurrence rules and any necessary time zone adjustments:
body = { ...body, ...location, extendedProperties: { shared: { image_url: localEvent.image_url } }, recurrence, ...recurringEventIdObj, summary: localEvent.title, description: localEvent.description, end: { dateTime: isAllDay || isMultiDay ? undefined : new Date(endDateTime).toISOString(), timeZone: isAllDay || isMultiDay || localEvent.end_timezone === '' ? 'UTC' : localEvent.end_timezone, date: isAllDay || isMultiDay ? new Date(endDateTimeWithOffset).toISOString().split('T')[0] : undefined }, start: { dateTime: isAllDay || isMultiDay ? undefined : new Date(startDateTime).toISOString(), timeZone: isAllDay || isMultiDay || localEvent.start_timezone === '' ? 'UTC' : localEvent.start_timezone, date: isAllDay || isMultiDay ? new Date(startDateTimeWithOffset).toISOString().split('T')[0] : undefined }, };
Handling Archived Events
If the event is archived, the function deletes the event or marks it as canceled:
if (localEvent.archived) {
if (existingEvent) {
method = got.delete;
bodyObj = {};
if (body.recurringEventId !== existingEvent.recurringEventId && body.recurringEventId && body.recurringEventId !== "") {
let timestamp = localEvent.recurrence_id.split("_R")[1];
crudEventUri = `https://www.googleapis.com/calendar/v3/calendars/${calendarId}/events/` + body.recurringEventId + "_" + timestamp;
}
} else {
bodyObj = { body: JSON.stringify({ ...body, status: "cancelled" }) };
}
}
Executing the Request
The function executes the HTTP request to Google Calendar API using the appropriate method (POST
, PATCH
, or DELETE
):
let gotGoogleEvent = await method(crudEventUri, { headers: { Authorization, "Content-Type": "application/json" }, ...bodyObj });
Updating the Local Database
Finally, the function updates the local database with the event details returned from Google API:
let googleEvent = await Events.findOneAndUpdate({ google_event_id: googleAPIEvent.id }, { atomcal_event_id: localEvent._id, google_calendar_id: calendar_id, raw_json: googleAPIEvent }, { new: true, upsert: true });
Summary
The upsertGoogleEvents
function carefully handles recurring events by checking for recurrence rules, managing exceptions, setting appropriate IDs, constructing the correct request body, and finally executing the necessary API requests to Google Calendar. This ensures that recurring events are created, updated, or deleted accurately, reflecting the local application's event details on Google Calendar.