Skip to content

Commit

Permalink
fix: Add functionality for creating recurring events (#1807)
Browse files Browse the repository at this point in the history
* create recurring event helpers and refactor

* remove unnecessary file

* add documentation and comments

* add tests

* fix comments

* update generateRecurrenceRuleString function

* speed up code

* fix formatting

* format fix

* restore package.json

* fix failing test

* better implementation and faster code

* minor refactoring

* minor correction

* fix imports

* return single recurring instance

* fix failing test

* Revert "return single recurring instance"

This reverts commit 60353fd.

* Reapply "return single recurring instance"

This reverts commit fc12a99.

* test commit

* update test

* fix test

* convert to UTC dates

* test commit with minor change

* limit instance generation date according to the recurrence frequency

* adjust for different timezones

* remove unnecessary code

* remove unnecessary formatting

* change variable names
  • Loading branch information
meetulr authored Feb 14, 2024
1 parent 65989b1 commit d38198a
Show file tree
Hide file tree
Showing 27 changed files with 1,333 additions and 210 deletions.
11 changes: 5 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,10 @@ Core features include:

<!-- toc -->

- [Talawa API](#talawa-api)
- [Talawa Components](#talawa-components)
- [Documentation](#documentation)
- [Installation](#installation)
- [Image Upload](#image-upload)
- [Talawa Components](#talawa-components)
- [Documentation](#documentation)
- [Installation](#installation)
- [Image Upload](#image-upload)

<!-- tocstop -->

Expand All @@ -53,4 +52,4 @@ Core features include:

## Image Upload

To enable image upload functionalities create an images folder in the root of the project
To enable image upload functionalities create an images folder in the root of the project
17 changes: 17 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
"nodemailer": "^6.9.9",
"pm2": "^5.2.0",
"redis": "^4.6.12",
"rrule": "^2.8.1",
"shortid": "^2.2.16",
"typedoc-plugin-markdown": "^3.17.1",
"uuid": "^9.0.0",
Expand Down
27 changes: 25 additions & 2 deletions schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ type Event {
createdAt: DateTime!
creator: User
description: String!
endDate: Date!
endDate: Date
endTime: Time
feedback: [Feedback!]!
isPublic: Boolean!
Expand Down Expand Up @@ -376,6 +376,13 @@ input ForgotPasswordData {
userOtp: String!
}

enum Frequency {
DAILY
MONTHLY
WEEKLY
YEARLY
}

enum Gender {
FEMALE
MALE
Expand Down Expand Up @@ -536,7 +543,7 @@ type Mutation {
createComment(data: CommentInput!, postId: ID!): Comment
createDirectChat(data: createChatInput!): DirectChat!
createDonation(amount: Float!, nameOfOrg: String!, nameOfUser: String!, orgId: ID!, payPalId: ID!, userId: ID!): Donation!
createEvent(data: EventInput): Event!
createEvent(data: EventInput!, recurrenceRuleData: RecurrenceRuleInput): Event!
createGroupChat(data: createGroupChatInput!): GroupChat!
createMember(input: UserAndOrganizationInput!): Organization!
createMessageChat(data: MessageChatInput!): MessageChat!
Expand Down Expand Up @@ -898,6 +905,12 @@ enum Recurrance {
YEARLY
}

input RecurrenceRuleInput {
count: Int
frequency: Frequency
weekDays: [WeekDays]
}

enum Status {
ACTIVE
BLOCKED
Expand Down Expand Up @@ -1207,6 +1220,16 @@ type UsersConnectionResult {
errors: [ConnectionError!]!
}

enum WeekDays {
FR
MO
SA
SU
TH
TU
WE
}

input createChatInput {
organizationId: ID!
userIds: [ID!]!
Expand Down
20 changes: 20 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,26 @@ export const REDIS_PASSWORD = process.env.REDIS_PASSWORD;

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

// 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",
"WEDNESDAY",
"THURSDAY",
"FRIDAY",
"SATURDAY",
"SUNDAY",
];

export const key = ENV.ENCRYPTION_KEY as string;
export const iv = crypto.randomBytes(16).toString("hex");

Expand Down
101 changes: 101 additions & 0 deletions src/helpers/event/createEventHelpers/createRecurringEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import type mongoose from "mongoose";
import type { InterfaceEvent } from "../../../models";
import { Event } from "../../../models";
import type { MutationCreateEventArgs } from "../../../types/generatedGraphQLTypes";
import {
generateRecurrenceRuleString,
getRecurringInstanceDates,
createRecurrenceRule,
generateRecurringEventInstances,
} from "../recurringEventHelpers";

/**
* This function creates the instances of a recurring event upto a certain date.
* @param args - payload of the createEvent mutation
* @param creatorId - _id of the creator
* @param organizationId - _id of the organization the events belongs to
* @remarks The following steps are followed:
* 1. Create a default recurrenceRuleData.
* 2. Generate a recurrence rule string based on the recurrenceRuleData.
* 3. Create a baseRecurringEvent on which recurring instances would be based.
* 4. Get the dates for recurring instances.
* 5. Create a recurrenceRule document.
* 6. Generate recurring instances according to the recurrence rule.
* @returns Created recurring event instance
*/

export const createRecurringEvent = async (
args: MutationCreateEventArgs,
creatorId: string,
organizationId: string,
session: mongoose.ClientSession
): Promise<InterfaceEvent> => {
const { data } = args;
let { recurrenceRuleData } = args;

if (!recurrenceRuleData) {
// create a default weekly recurrence rule
recurrenceRuleData = {
frequency: "WEEKLY",
};
}

// generate a recurrence rule string which would be used to generate rrule object
// and get recurrence dates
const recurrenceRuleString = generateRecurrenceRuleString(
recurrenceRuleData,
data?.startDate,
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,
recurring: true,
isBaseRecurringEvent: true,
creatorId,
admins: [creatorId],
organization: organizationId,
},
],
{ session }
);

// 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 = 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,
data.startDate,
data.endDate,
organizationId,
baseRecurringEvent[0]?._id.toString(),
latestInstanceDate,
session
);

// generate the recurring instances and get an instance back
const recurringEventInstance = await generateRecurringEventInstances({
data,
baseRecurringEventId: baseRecurringEvent[0]?._id.toString(),
recurrenceRuleId: recurrenceRule?._id.toString(),
recurringInstanceDates,
creatorId,
organizationId,
session,
});

return recurringEventInstance;
};
66 changes: 66 additions & 0 deletions src/helpers/event/createEventHelpers/createSingleEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import type mongoose from "mongoose";
import type { InterfaceEvent } from "../../../models";
import { Event, EventAttendee, User } from "../../../models";
import type { MutationCreateEventArgs } from "../../../types/generatedGraphQLTypes";
import { cacheEvents } from "../../../services/EventCache/cacheEvents";

/**
* This function generates a single non-recurring event.
* @param args - the arguments provided for the createEvent mutation.
* @param creatorId - _id of the current user.
* @param organizationId - _id of the current organization.
* @remarks The following steps are followed:
* 1. Create an event document.
* 2. Associate the event with the user
* 3. Cache the event.
* @returns The created event.
*/

export const createSingleEvent = async (
args: MutationCreateEventArgs,
creatorId: string,
organizationId: string,
session: mongoose.ClientSession
): Promise<InterfaceEvent> => {
// create the single event
const createdEvent = await Event.create(
[
{
...args.data,
creatorId,
admins: [creatorId],
organization: organizationId,
},
],
{ session }
);

// associate event with the user
await EventAttendee.create(
[
{
userId: creatorId,
eventId: createdEvent[0]?._id,
},
],
{ session }
);
await User.updateOne(
{
_id: creatorId,
},
{
$push: {
eventAdmin: createdEvent[0]?._id,
createdEvents: createdEvent[0]?._id,
registeredEvents: createdEvent[0]?._id,
},
},
{ session }
);

// cache the event
await cacheEvents([createdEvent[0]]);

return createdEvent[0];
};
2 changes: 2 additions & 0 deletions src/helpers/event/createEventHelpers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { createSingleEvent } from "./createSingleEvent";
export { createRecurringEvent } from "./createRecurringEvent";
65 changes: 65 additions & 0 deletions src/helpers/event/recurringEventHelpers/createRecurrenceRule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import type mongoose from "mongoose";
import { rrulestr } from "rrule";
import type { InterfaceRecurrenceRule } from "../../../models/RecurrenceRule";
import { RecurrenceRule } from "../../../models/RecurrenceRule";
import {
RECURRENCE_FREQUENCIES,
RECURRENCE_WEEKDAYS,
} from "../../../constants";

/**
* This function generates the recurrenceRule document.
* @param recurrenceRuleString - the rrule string containing the rules that the instances would follow.
* @param recurrenceStartDate - start date of recurrence.
* @param recurrenceEndDate - end date of recurrence.
* @param organizationId - _id of the current organization.
* @param baseRecurringEventId - _id of the base recurring event.
* @param latestInstanceDate - start date of the last instance generated during this operation.
* @remarks The following steps are followed:
* 1. Create an rrule object from the rrule string.
* 2. Get the fields for the RecurrenceRule document.
* 3. Create the RecurrenceRuleDocument.
* @returns The recurrence rule document.
*/

export const createRecurrenceRule = async (
recurrenceRuleString: string,
recurrenceStartDate: Date,
recurrenceEndDate: Date | null,
organizationId: string,
baseRecurringEventId: string,
latestInstanceDate: Date,
session: mongoose.ClientSession
): Promise<InterfaceRecurrenceRule> => {
const recurrenceRuleObject = rrulestr(recurrenceRuleString);

const { freq, byweekday } = recurrenceRuleObject.options;

const weekDays: string[] = [];
if (byweekday) {
for (const weekday of byweekday) {
weekDays.push(RECURRENCE_WEEKDAYS[weekday]);
}
}

const frequency = RECURRENCE_FREQUENCIES[freq];

const recurrenceRule = await RecurrenceRule.create(
[
{
organizationId,
baseRecurringEventId,
recurrenceRuleString,
startDate: recurrenceStartDate,
endDate: recurrenceEndDate,
frequency,
count: recurrenceRuleObject.options.count,
weekDays,
latestInstanceDate,
},
],
{ session }
);

return recurrenceRule[0].toObject();
};
Loading

0 comments on commit d38198a

Please sign in to comment.