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 30 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
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.8",
"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
103 changes: 103 additions & 0 deletions src/helpers/event/createEventHelpers/createRecurringEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
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 currentUserId - _id of the current user
* @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,
currentUserId: 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,
startDate: data.startDate,
endDate: data.endDate,
recurring: true,
isBaseRecurringEvent: true,
creatorId: currentUserId,
admins: [currentUserId],
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.toString(),
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,
currentUserId: currentUserId.toString(),
organizationId: organizationId.toString(),
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 currentUserId - _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,
currentUserId: string,
organizationId: string,
session: mongoose.ClientSession
): Promise<InterfaceEvent> => {
// create the single event
const createdEvent = await Event.create(
[
{
...args.data,
creatorId: currentUserId,
admins: [currentUserId],
organization: organizationId,
},
],
{ session }
);

// associate event with the user
await EventAttendee.create(
[
{
userId: currentUserId,
eventId: createdEvent[0]?._id,
},
],
{ session }
);
await User.updateOne(
{
_id: currentUserId,
},
{
$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
Loading