Skip to content

Commit

Permalink
feat: Dynamic Recurring Event Instances Generation during Queries (#1853
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

* update inputs

* add tests

* correct test

* format files

* update variable name

* add dynamic recurring event instances generation during query

* account for how many instances to generate based on recurrenceRule's specified count

* add constant for query limit date
  • Loading branch information
meetulr authored Feb 19, 2024
1 parent 6984780 commit bd1e6c5
Show file tree
Hide file tree
Showing 6 changed files with 487 additions and 31 deletions.
5 changes: 5 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,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

0 comments on commit bd1e6c5

Please sign in to comment.