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

fix: Add functionality for creating recurring events #1807

Merged
merged 31 commits into from
Feb 14, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
654bf4c
create recurring event helpers and refactor
meetulr Feb 7, 2024
9ae6d1d
remove unnecessary file
meetulr Feb 7, 2024
085880a
add documentation and comments
meetulr Feb 7, 2024
d80d6ad
add tests
meetulr Feb 7, 2024
61d47c5
Merge branch 'develop' of https://github.com/PalisadoesFoundation/tal…
meetulr Feb 7, 2024
9ae1754
fix comments
meetulr Feb 7, 2024
5afad7c
update generateRecurrenceRuleString function
meetulr Feb 7, 2024
694dd5c
speed up code
meetulr Feb 8, 2024
a02e30c
Merge branch 'develop' of https://github.com/PalisadoesFoundation/tal…
meetulr Feb 8, 2024
1caf67f
fix formatting
meetulr Feb 8, 2024
8378739
format fix
meetulr Feb 8, 2024
2352fd2
restore package.json
meetulr Feb 8, 2024
fe4de23
fix failing test
meetulr Feb 8, 2024
3d4c528
better implementation and faster code
meetulr Feb 8, 2024
e2f47be
minor refactoring
meetulr Feb 8, 2024
67d6777
minor correction
meetulr Feb 8, 2024
6154829
fix imports
meetulr Feb 8, 2024
60353fd
return single recurring instance
meetulr Feb 9, 2024
09100f0
fix failing test
meetulr Feb 9, 2024
fc12a99
Revert "return single recurring instance"
meetulr Feb 9, 2024
f06a72c
Reapply "return single recurring instance"
meetulr Feb 9, 2024
abf5d91
test commit
meetulr Feb 9, 2024
ee01799
update test
meetulr Feb 9, 2024
c65faf7
fix test
meetulr Feb 9, 2024
8cc123a
convert to UTC dates
meetulr Feb 9, 2024
a173b97
test commit with minor change
meetulr Feb 10, 2024
0f3fd2f
limit instance generation date according to the recurrence frequency
meetulr Feb 10, 2024
4cac65e
adjust for different timezones
meetulr Feb 11, 2024
8662fa1
remove unnecessary code
meetulr Feb 11, 2024
bef350c
remove unnecessary formatting
meetulr Feb 11, 2024
34c13c7
change variable names
meetulr Feb 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -529,8 +529,16 @@ export const REDIS_PASSWORD = process.env.REDIS_PASSWORD;

export const MILLISECONDS_IN_A_WEEK = 7 * 24 * 60 * 60 * 1000;

export const RECURRING_EVENT_INSTANCES_MONTH_LIMIT = 6;
// recurring event frequencies
export const RECURRENCE_FREQUENCIES = ["YEARLY", "MONTHLY", "WEEKLY", "DAILY"];

// recurring instance generation date limit in years based on it's frequency
export const RECURRING_EVENT_INSTANCES_DAILY_LIMIT = 1;
export const RECURRING_EVENT_INSTANCES_WEEKLY_LIMIT = 2;
export const RECURRING_EVENT_INSTANCES_MONTHLY_LIMIT = 5;
export const RECURRING_EVENT_INSTANCES_YEARLY_LIMIT = 10;

// recurring event days
export const RECURRENCE_WEEKDAYS = [
"MONDAY",
"TUESDAY",
Expand Down
29 changes: 12 additions & 17 deletions src/helpers/event/createEventHelpers/createRecurringEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
createRecurrenceRule,
generateRecurringEventInstances,
} from "../recurringEventHelpers";
import { convertToUTCDate } from "../../../utilities/convertToUTCDate";

/**
* This function creates the instances of a recurring event upto a certain date.
Expand Down Expand Up @@ -49,20 +48,14 @@ export const createRecurringEvent = async (
data?.endDate
);

const convertedStartDate = convertToUTCDate(data.startDate);
let convertedEndDate = null;
if (data.endDate) {
convertedEndDate = convertToUTCDate(data.endDate);
}

// create a base recurring event first, based on which all the
// recurring instances would be dynamically generated
const baseRecurringEvent = await Event.create(
[
{
...data,
startDate: convertedStartDate,
endDate: convertedEndDate,
startDate: data.startDate,
endDate: data.endDate,
meetulr marked this conversation as resolved.
Show resolved Hide resolved
recurring: true,
isBaseRecurringEvent: true,
creatorId: currentUserId,
Expand All @@ -75,18 +68,20 @@ export const createRecurringEvent = async (

// get the dates for the recurringInstances, and the date of the last instance
// to be generated in this operation (rest would be generated dynamically during query)
const [recurringInstanceDates, latestInstanceDate] =
getRecurringInstanceDates(
recurrenceRuleString,
convertedStartDate,
convertedEndDate
);
const recurringInstanceDates = getRecurringInstanceDates(
recurrenceRuleString,
data.startDate,
data.endDate
);

// get the date for the latest created instance
const latestInstanceDate =
recurringInstanceDates[recurringInstanceDates.length - 1];
// create a recurrenceRule document that would contain the recurrence pattern
const recurrenceRule = await createRecurrenceRule(
recurrenceRuleString,
convertedStartDate,
convertedEndDate,
data.startDate,
data.endDate,
organizationId.toString(),
baseRecurringEvent[0]?._id.toString(),
latestInstanceDate,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { format } from "date-fns";
import type { RecurrenceRuleInput } from "../../../types/generatedGraphQLTypes";
import { adjustForTimezoneOffset } from "../../../utilities/recurrenceDatesUtil";

/**
* This function generates the recurrence rule (rrule) string.
* @param recurrenceRuleData - the recurrenceRuleInput provided in the args.
* @param recurrenceStartDate - start date of recurrence.
* @param recurrenceEndDate - end date of recurrence.
* @remarks The following steps are followed:
* 1. Initiate an empty recurrenceRule string.
* 2. Add the recurrence rules one by one.
* 1. Adjust the start and end dates of recurrence for timezone offsets.
* 2. Get the recurrence rules and make a recurrenceRuleString.
* @returns The recurrence rule string that would be used to create a valid rrule object.
*/

Expand All @@ -17,21 +18,39 @@ export const generateRecurrenceRuleString = (
recurrenceStartDate: Date,
recurrenceEndDate?: Date
): string => {
// destructure the rules
const { frequency, count, weekDays } = recurrenceRuleData;
// adjust the dates according to the timezone offset
recurrenceStartDate = adjustForTimezoneOffset(recurrenceStartDate);
if (recurrenceEndDate) {
recurrenceEndDate = adjustForTimezoneOffset(recurrenceEndDate);
}

// recurrence start date
// (not necessarily the start date of the first recurring instance)
const formattedRecurrenceStartDate = format(
let formattedRecurrenceStartDate = format(
recurrenceStartDate,
"yyyyMMdd'T'HHmmss'Z'"
);

// format it to be UTC midnight
formattedRecurrenceStartDate = formattedRecurrenceStartDate.replace(
/T\d{6}Z/,
"T000000Z"
);

// date upto which instances would be generated
const formattedRecurrenceEndDate = recurrenceEndDate
let formattedRecurrenceEndDate = recurrenceEndDate
? format(recurrenceEndDate, "yyyyMMdd'T'HHmmss'Z'")
: "";

// format it to be UTC midnight
formattedRecurrenceEndDate = formattedRecurrenceEndDate.replace(
/T\d{6}Z/,
"T000000Z"
);

// destructure the recurrence rules
const { frequency, count, weekDays } = recurrenceRuleData;

// string representing the days of the week the event would recur
const weekDaysString = weekDays?.length ? weekDays.join(",") : "";

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type mongoose from "mongoose";
import { format } from "date-fns";
import type { InterfaceEvent } from "../../../models";
import { Event, EventAttendee, User } from "../../../models";
import type { EventInput } from "../../../types/generatedGraphQLTypes";
Expand Down Expand Up @@ -51,12 +50,10 @@ export const generateRecurringEventInstances = async ({
}: InterfaceGenerateRecurringInstances): Promise<InterfaceEvent> => {
const recurringInstances: InterfaceRecurringEvent[] = [];
recurringInstanceDates.map((date) => {
const formattedInstanceDate = format(date, "yyyy-MM-dd");

const createdEventInstance = {
...data,
startDate: formattedInstanceDate,
endDate: formattedInstanceDate,
startDate: date,
endDate: date,
meetulr marked this conversation as resolved.
Show resolved Hide resolved
recurring: true,
isBaseRecurringEvent: false,
recurrenceRuleId,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,49 +1,74 @@
import { addMonths } from "date-fns";
import { rrulestr } from "rrule";
import { RECURRING_EVENT_INSTANCES_MONTH_LIMIT } from "../../../constants";
import { addYears } from "date-fns";
import { Frequency, rrulestr } from "rrule";
import type { RRule } from "rrule";
import {
RECURRING_EVENT_INSTANCES_DAILY_LIMIT,
RECURRING_EVENT_INSTANCES_WEEKLY_LIMIT,
RECURRING_EVENT_INSTANCES_MONTHLY_LIMIT,
RECURRING_EVENT_INSTANCES_YEARLY_LIMIT,
} from "../../../constants";

/**
* This function generates the dates of recurrence for the recurring event.
* @param recurrenceRuleString - the rrule string for the recurrenceRule.
* @param recurrenceStartDate - the starting date from which we want to generate instances.
* @param eventEndDate - the end date of the event
* @param calendarDate - the last date of the current calendar month (To be used during query).
* @param calendarDate - the calendar date (To be used for dynamic instance generation during queries).
* @remarks The following steps are followed:
* 1. Limit the end date for instance creation.
* 2. Getting the date upto which we would generate instances during this operation (leaving the rest for dynamic generation).
* 3. Getting the dates for recurring instances.
* @returns The recurring instance dates and the date of last instance generated during this operation.
* 1. Get the date limit for instance generation based on its recurrence frequency.
* 2. Get the dates for recurring event instances.
* @returns Dates for recurring instances to be generated during this operation.
*/

export function getRecurringInstanceDates(
recurrenceRuleString: string,
recurrenceStartDate: Date,
eventEndDate: Date | null,
calendarDate: Date = recurrenceStartDate
): [Date[], Date] {
const limitEndDate = addMonths(
): Date[] {
// get the rrule object
const recurrenceRuleObject: RRule = rrulestr(recurrenceRuleString);

// get the recurrence frequency
const { freq: recurrenceFrequency } = recurrenceRuleObject.options;

// set limitEndDate according to the recurrence frequency
let limitEndDate = addYears(
calendarDate,
RECURRING_EVENT_INSTANCES_MONTH_LIMIT
// generate instances upto this many months ahead
// leave the rest for dynamic generation during queries
RECURRING_EVENT_INSTANCES_DAILY_LIMIT
);

if (recurrenceFrequency === Frequency.WEEKLY) {
limitEndDate = addYears(
calendarDate,
RECURRING_EVENT_INSTANCES_WEEKLY_LIMIT
);
} else if (recurrenceFrequency === Frequency.MONTHLY) {
limitEndDate = addYears(
calendarDate,
RECURRING_EVENT_INSTANCES_MONTHLY_LIMIT
);
} else if (recurrenceFrequency === Frequency.YEARLY) {
limitEndDate = addYears(
calendarDate,
RECURRING_EVENT_INSTANCES_YEARLY_LIMIT
);
}

// if the event has no endDate
eventEndDate = eventEndDate || limitEndDate;

// the date upto which we would generate the instances in this operation
const generateUptoDate = new Date(
Math.min(eventEndDate.getTime(), limitEndDate.getTime())
);

const recurrenceRuleObject = rrulestr(recurrenceRuleString);

// get the dates of recurrence
const recurringInstanceDates = recurrenceRuleObject.between(
recurrenceStartDate,
generateUptoDate,
true
);

const latestInstanceDate =
recurringInstanceDates[recurringInstanceDates.length - 1];

return [recurringInstanceDates, latestInstanceDate];
return recurringInstanceDates;
}
12 changes: 11 additions & 1 deletion src/resolvers/Mutation/createEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,10 +126,13 @@ export const createEvent: MutationResolvers["createEvent"] = async (
);
}

/* c8 ignore start */
if (session) {
// start a transaction
session.startTransaction();
}

/* c8 ignore stop */
try {
let createdEvent: InterfaceEvent;

Expand All @@ -151,16 +154,23 @@ export const createEvent: MutationResolvers["createEvent"] = async (
);
}

/* c8 ignore start */
if (session) {
// commit transaction if everything's successful
await session.commitTransaction();
}

// Returns the createdEvent.
/* c8 ignore stop */
return createdEvent;
} catch (error) {
/* c8 ignore start */
if (session) {
// abort transaction if something fails
await session.abortTransaction();
}

throw error;
}

/* c8 ignore stop */
};
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,20 @@ export const convertToUTCDate = (date: Date): Date => {

return utcMidnight;
};

/**
* This function adjusts for the timezone offset.
* @param date - the date to be adjusted.
* @returns adjusted date.
*/
export const adjustForTimezoneOffset = (date: Date): Date => {
const timeZoneOffset = new Date().getTimezoneOffset();

/* c8 ignore start */
if (timeZoneOffset > 0) {
date = new Date(date.getTime() + timeZoneOffset * 60 * 1000);
}

/* c8 ignore stop */
return date;
};
Loading
Loading