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

feat: Dynamic Recurring Event Instances Generation during Queries #1853

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 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
92750dc
Merge branch 'recurring-events' into recurring-events-query
meetulr Feb 13, 2024
13f8903
update inputs
meetulr Feb 14, 2024
d8c8bcf
Merge branch 'develop' of https://github.com/PalisadoesFoundation/tal…
meetulr Feb 14, 2024
0e4fb12
add tests
meetulr Feb 14, 2024
2bbc113
correct test
meetulr Feb 14, 2024
24bf959
Merge branch 'develop' of https://github.com/PalisadoesFoundation/tal…
meetulr Feb 15, 2024
f5ee5c3
Merge branch 'develop' of https://github.com/PalisadoesFoundation/tal…
meetulr Feb 15, 2024
a900152
Merge branch 'develop' of https://github.com/PalisadoesFoundation/tal…
meetulr Feb 16, 2024
9677b0f
Merge branch 'develop' of https://github.com/PalisadoesFoundation/tal…
meetulr Feb 17, 2024
0061f63
format files
meetulr Feb 17, 2024
605b47f
Merge branch 'develop' of https://github.com/PalisadoesFoundation/tal…
meetulr Feb 18, 2024
e3a2c7b
Merge branch 'develop' of https://github.com/PalisadoesFoundation/tal…
meetulr Feb 18, 2024
6414efe
update variable name
meetulr Feb 19, 2024
4d59783
add dynamic recurring event instances generation during query
meetulr Feb 13, 2026
2f5989e
account for how many instances to generate based on recurrenceRule's …
meetulr Feb 13, 2026
ce055e7
add constant for query limit date
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
2 changes: 1 addition & 1 deletion INSTALLATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ This document provides instructions on how to set up and start a running instanc
- [Setting up the RECAPTCHA_SECRET_KEY](#setting-up-the-recaptcha_secret_key)
- [Setting up .env MAIL_USERNAME and MAIL_PASSWORD ReCAPTCHA Parameters](#setting-up-env-mail_username-and-mail_password-recaptcha-parameters)
- [Setting up SMTP Email Variables in the .env File](#setting-up-smtp-email-variables-in-the-env-file)
- [Setting up Logger configurations _(optional)_](#setting-up-logger-configurations-_optional_)
- [Setting up Logger configurations](#setting-up-logger-configurations)
- [Setting up COLORIZE_LOGS in .env file](#setting-up-colorize_logs-in-env-file)
- [Setting up LOG_LEVEL in .env file](#setting-up-log_level-in-env-file)
- [Importing Sample Database](#importing-sample-database)
Expand Down
5 changes: 5 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,11 @@ export const RECURRING_EVENT_INSTANCES_WEEKLY_LIMIT = 2;
export const RECURRING_EVENT_INSTANCES_MONTHLY_LIMIT = 5;
export const RECURRING_EVENT_INSTANCES_YEARLY_LIMIT = 10;

// recurrence rules query date limit in years
// i.e. query limit date to find the pending recurrence patterns
// and then generate new instances ahead of this date
export const RECURRING_EVENT_INSTANCES_QUERY_LIMIT = 1;

// recurring event days
export const RECURRENCE_WEEKDAYS = [
"MONDAY",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { addDays, addYears } from "date-fns";
import { RecurrenceRule } from "../../../models/RecurrenceRule";
import { convertToUTCDate } from "../../../utilities/recurrenceDatesUtil";
import { Event } from "../../../models";
import {
generateRecurringEventInstances,
getRecurringInstanceDates,
} from "../recurringEventHelpers";
import { session } from "../../../db";
import type { Recurrance } from "../../../types/generatedGraphQLTypes";
import type { InterfaceRecurringEvent } from "../recurringEventHelpers/generateRecurringEventInstances";
import { RECURRING_EVENT_INSTANCES_QUERY_LIMIT } from "../../../constants";

/**
* This function creates the instances of a recurring event upto a certain date during queries.
* @param organizationId - _id of the organization the events belong to
* @remarks The following steps are followed:
* 1. Get the limit date upto which we would want to query the recurrenceRules and generate new instances.
* 2. Get the recurrence rules to be used for instance generation during this query.
* 3. For every recurrence rule found:
* - find the base recurring event to get the data to be used for new instance generation.
* - get the number of existing instances and how many more to generate based on the recurrenceRule's count (if specified).
* - generate new instances after their latestInstanceDates.
* - update the latestInstanceDate.
*/

export const createRecurringEventInstancesDuringQuery = async (
organizationId: string | undefined | null,
): Promise<void> => {
if (!organizationId) {
return;
}

// get the current calendar date in UTC midnight
const calendarDate = convertToUTCDate(new Date());
const queryUptoDate = addYears(
calendarDate,
RECURRING_EVENT_INSTANCES_QUERY_LIMIT,
);

// get the recurrenceRules
const recurrenceRules = await RecurrenceRule.find({
organizationId,
latestInstanceDate: { $lt: queryUptoDate },
}).lean();

await Promise.all(
recurrenceRules.map(async (recurrenceRule) => {
// find the baseRecurringEvent for the recurrenceRule
const baseRecurringEvent = await Event.find({
_id: recurrenceRule.baseRecurringEventId,
}).lean();

// get the data from the baseRecurringEvent
const {
_id: baseRecurringEventId,
recurrance,
...data
} = baseRecurringEvent[0];

// get the input data for the generateRecurringEventInstances function
const currentInputData: InterfaceRecurringEvent = {
...data,
organizationId: recurrenceRule.organizationId.toString(),
recurrance: recurrance as Recurrance,
};

// get the properties from recurrenceRule
const {
_id: recurrenceRuleId,
latestInstanceDate,
recurrenceRuleString,
endDate: recurrenceEndDate,
count: totalInstancesCount,
} = recurrenceRule;

// get the date from which new instances would be generated
const currentRecurrenceStartDate = addDays(latestInstanceDate, 1);

// get the dates for recurrence
let recurringInstanceDates = getRecurringInstanceDates(
recurrenceRuleString,
currentRecurrenceStartDate,
recurrenceEndDate,
queryUptoDate,
);

// find out how many instances following the recurrence rule already exist and how many more to generate
if (totalInstancesCount) {
const totalExistingInstances = await Event.countDocuments({
recurrenceRuleId,
});

const remainingInstances = totalInstancesCount - totalExistingInstances;

recurringInstanceDates = recurringInstanceDates.slice(
0,
Math.min(recurringInstanceDates.length, remainingInstances),
);
}

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

/* c8 ignore stop */
try {
if (recurringInstanceDates && recurringInstanceDates.length) {
const updatedLatestRecurringInstanceDate =
recurringInstanceDates[recurringInstanceDates.length - 1];

// update the latestInstanceDate of the recurrenceRule
await RecurrenceRule.updateOne(
{
_id: recurrenceRuleId,
},
{
latestInstanceDate: updatedLatestRecurringInstanceDate,
},
{ session },
);

// generate recurring event instances
await generateRecurringEventInstances({
data: currentInputData,
baseRecurringEventId: baseRecurringEventId.toString(),
recurrenceRuleId: recurrenceRuleId.toString(),
recurringInstanceDates,
creatorId: baseRecurringEvent[0].creatorId.toString(),
organizationId,
session,
});
}

/* c8 ignore start */
if (session) {
// commit transaction if everything's successful
await session.commitTransaction();
}
} catch (error) {
if (session) {
// abort transaction if something fails
await session.abortTransaction();
}

throw error;
}

/* c8 ignore stop */
}),
);
};
1 change: 1 addition & 0 deletions src/helpers/event/createEventHelpers/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { createSingleEvent } from "./createSingleEvent";
export { createRecurringEvent } from "./createRecurringEvent";
export { createRecurringEventInstancesDuringQuery } from "./createRecurringEventInstancesDuringQuery";
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,24 @@ import { cacheEvents } from "../../../services/EventCache/cacheEvents";
*/

interface InterfaceGenerateRecurringInstances {
data: EventInput;
data: InterfaceRecurringEvent;
baseRecurringEventId: string;
recurrenceRuleId: string;
recurringInstanceDates: Date[];
creatorId: string;
organizationId: string;
status?: string;
session: mongoose.ClientSession;
}

interface InterfaceRecurringEvent extends EventInput {
isBaseRecurringEvent: boolean;
recurrenceRuleId: string;
baseRecurringEventId: string;
creatorId: string;
admins: string[];
organization: string;
export interface InterfaceRecurringEvent extends EventInput {
isBaseRecurringEvent?: boolean;
recurrenceRuleId?: string;
baseRecurringEventId?: string;
creatorId?: string;
admins?: string[];
organization?: string;
status?: string;
}

export const generateRecurringEventInstances = async ({
Expand All @@ -49,7 +51,7 @@ export const generateRecurringEventInstances = async ({
session,
}: InterfaceGenerateRecurringInstances): Promise<InterfaceEvent> => {
const recurringInstances: InterfaceRecurringEvent[] = [];
recurringInstanceDates.map((date) => {
recurringInstanceDates.map((date): void => {
const createdEventInstance = {
...data,
startDate: date,
Expand All @@ -59,8 +61,9 @@ export const generateRecurringEventInstances = async ({
recurrenceRuleId,
baseRecurringEventId,
creatorId,
admins: [creatorId],
admins: data.admins && data.admins.length ? data.admins : [creatorId],
organization: organizationId,
status: data.status,
};

recurringInstances.push(createdEventInstance);
Expand Down
6 changes: 6 additions & 0 deletions src/resolvers/Query/eventsByOrganizationConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@ import type { InterfaceEvent } from "../../models";
import { Event } from "../../models";
import { getSort } from "./helperFunctions/getSort";
import { getWhere } from "./helperFunctions/getWhere";
import { createRecurringEventInstancesDuringQuery } from "../../helpers/event/createEventHelpers";

export const eventsByOrganizationConnection: QueryResolvers["eventsByOrganizationConnection"] =
async (_parent, args) => {
// dynamically generate recurring event instances upto a certain date during this query
await createRecurringEventInstancesDuringQuery(args.where?.organization_id);

// get the where and sort
let where = getWhere<InterfaceEvent>(args.where);
const sort = getSort(args.orderBy);

Expand All @@ -15,6 +20,7 @@ export const eventsByOrganizationConnection: QueryResolvers["eventsByOrganizatio
isBaseRecurringEvent: false,
};

// find all the events according to the requirements
const events = await Event.find(where)
.sort(sort)
.limit(args.first ?? 0)
Expand Down
Loading
Loading