Skip to content

Commit adfd4f4

Browse files
committed
✨ feat(edit-recurrence): WIP
1 parent 3bd0438 commit adfd4f4

19 files changed

Lines changed: 1887 additions & 187 deletions

packages/backend/src/__tests__/mocks.gcal/factories/gcal.factory.ts

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import {
2020
isRegularGCalEvent,
2121
} from "@core/util/event/gcal.event.util";
2222
import { compassTestState } from "@backend/__tests__/helpers/mock.setup";
23-
import { mockRecurringGcalEvents } from "@backend/__tests__/mocks.gcal/factories/gcal.event.factory";
2423

2524
/**
2625
* Generates a paginated items for the Google Calendar API.
@@ -156,6 +155,38 @@ export const mockGcal = ({
156155
});
157156
},
158157
),
158+
update: jest.fn(
159+
async (
160+
params: calendar_v3.Params$Resource$Events$Update,
161+
options: MethodOptions = {},
162+
): GaxiosPromise<gSchema$Event> => {
163+
const testState = compassTestState();
164+
const { all: events } = testState.events.gcalEvents;
165+
const { eventId } = params;
166+
const eventIndex = events.findIndex((e) => e.id === eventId);
167+
168+
if (eventIndex === -1) {
169+
throw new Error(`Event with id ${eventId} not found`);
170+
}
171+
172+
const updatedEvent = { ...events[eventIndex], ...params.requestBody };
173+
174+
events.splice(
175+
eventIndex,
176+
1,
177+
updatedEvent as WithGcalId<gSchema$Event>,
178+
);
179+
180+
return Promise.resolve({
181+
config: options,
182+
statusText: "OK",
183+
status: 200,
184+
data: updatedEvent,
185+
headers: options.headers!,
186+
request: { responseURL: updatedEvent.id! },
187+
});
188+
},
189+
),
159190
delete: jest.fn(
160191
async (params: calendar_v3.Params$Resource$Events$Delete) => {
161192
const testState = compassTestState();
@@ -167,8 +198,23 @@ export const mockGcal = ({
167198
throw new Error(`Event with id ${eventId} not found`);
168199
}
169200

201+
const event = events[eventIndex]!;
202+
const isRecurring = isBaseGCalEvent(event);
203+
170204
events.splice(eventIndex, 1);
171205

206+
if (isRecurring) {
207+
// Also delete all instances of the recurring event
208+
const instanceEvents = events.filter(isInstanceGCalEvent);
209+
210+
instanceEvents.forEach((instance) => {
211+
const index = events.findIndex((e) => e.id === instance.id);
212+
if (index !== -1) {
213+
events.splice(index, 1);
214+
}
215+
});
216+
}
217+
172218
return Promise.resolve({
173219
statusText: "OK",
174220
status: 204,
@@ -211,6 +257,7 @@ export const mockGcal = ({
211257
params.maxResults ?? pageSize,
212258
params.pageToken,
213259
);
260+
214261
return {
215262
statusText: "OK",
216263
status: 200,
@@ -227,7 +274,9 @@ export const mockGcal = ({
227274

228275
if (!baseEvent) throw new Error(`Event with id ${id} not found`);
229276

230-
const { instances } = mockRecurringGcalEvents({ ...baseEvent, id });
277+
const instances = events.filter(
278+
({ recurringEventId }) => recurringEventId === id,
279+
);
231280

232281
const eventsPage = generatePaginatedGcalItems(
233282
instances,
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { ClientSession, Filter, ObjectId, WithId } from "mongodb";
2+
import {
3+
CompassEvent,
4+
EventUpdateSchema,
5+
Event_Core,
6+
RecurringEventUpdateScope,
7+
Schema_Event,
8+
} from "@core/types/event.types";
9+
import { parseCompassEventDate } from "@core/util/event/event.util";
10+
import mongoService from "@backend/common/services/mongo.service";
11+
import { CompassEventRRule } from "@backend/event/classes/compass.event.rrule";
12+
import { MapEvent } from "../../../../core/src/mappers/map.event";
13+
14+
export class CompassEventFactory {
15+
private static async findCompassEvent(
16+
{ eventId, userId: user }: Pick<CompassEvent, "userId" | "eventId">,
17+
session?: ClientSession,
18+
throwIfNotFound = true,
19+
): Promise<WithId<Omit<Schema_Event, "_id">> | null> {
20+
const _id = new ObjectId(eventId);
21+
const filter: Filter<Omit<Schema_Event, "_id">> = { _id, user };
22+
23+
const event = await mongoService.event.findOne(filter, { session });
24+
25+
if (throwIfNotFound && !event) {
26+
throw new Error(`Compass event not found for id: ${eventId}`);
27+
}
28+
29+
return event;
30+
}
31+
32+
private static async findCompassBaseAndInstanceEvent(
33+
event: Pick<CompassEvent, "userId" | "eventId">,
34+
session?: ClientSession,
35+
): Promise<{
36+
baseEvent: WithId<Omit<Schema_Event, "_id">>;
37+
instanceEvent: WithId<Omit<Schema_Event, "_id">>;
38+
}> {
39+
const { userId } = event;
40+
41+
// get instance event or throw
42+
const instanceEvent = await CompassEventFactory.findCompassEvent(
43+
event,
44+
session,
45+
);
46+
47+
const baseEventId = instanceEvent!.recurrence!.eventId!.toString();
48+
49+
if (!baseEventId) throw new Error("event is not a recurring instance");
50+
51+
// get base event in series or throw
52+
const baseEvent = await CompassEventFactory.findCompassEvent(
53+
{ userId, eventId: baseEventId },
54+
session,
55+
);
56+
57+
return { baseEvent: baseEvent!, instanceEvent: instanceEvent! };
58+
}
59+
60+
private static async genThisAndFollowingEvents(
61+
event: CompassEvent,
62+
session?: ClientSession,
63+
): Promise<CompassEvent[]> {
64+
const { baseEvent, instanceEvent } =
65+
await CompassEventFactory.findCompassBaseAndInstanceEvent(event, session);
66+
67+
const rruleOldSeries = new CompassEventRRule(baseEvent, {
68+
until: parseCompassEventDate(instanceEvent.startDate!)
69+
.subtract(1, baseEvent.isAllDay ? "day" : "milliseconds")
70+
.toDate(),
71+
});
72+
73+
const baseEventId = baseEvent._id.toString();
74+
75+
const compassBaseEventWithUntil: CompassEvent = {
76+
...event,
77+
eventId: baseEventId,
78+
payload: { ...rruleOldSeries.base(), _id: baseEventId } as Event_Core,
79+
};
80+
81+
const payload = EventUpdateSchema.parse(event.payload);
82+
83+
const rruleNewSeries = new CompassEventRRule({
84+
...MapEvent.removeIdentifyingData(instanceEvent),
85+
...payload,
86+
_id: instanceEvent._id,
87+
});
88+
89+
const newBase = rruleNewSeries.base();
90+
91+
// new series
92+
const compassEvent: CompassEvent = {
93+
...event,
94+
payload: { ...newBase, _id: newBase._id.toString() } as Event_Core,
95+
};
96+
97+
return [compassBaseEventWithUntil, compassEvent];
98+
}
99+
100+
private static async genAllEvents(
101+
event: CompassEvent,
102+
session?: ClientSession,
103+
): Promise<CompassEvent[]> {
104+
const { baseEvent } =
105+
await CompassEventFactory.findCompassBaseAndInstanceEvent(event, session);
106+
107+
const payload = EventUpdateSchema.parse(event.payload);
108+
const eventId = baseEvent._id.toString();
109+
110+
const compassEvent: CompassEvent = {
111+
...event,
112+
eventId,
113+
payload: { ...baseEvent, ...payload, _id: eventId } as Event_Core,
114+
};
115+
116+
return [compassEvent];
117+
}
118+
119+
private static async genThisEvent(
120+
event: CompassEvent,
121+
): Promise<CompassEvent[]> {
122+
const { recurrence } = event.payload;
123+
const hasRule = Array.isArray(recurrence?.rule);
124+
const hasRecurringEvent = typeof recurrence?.eventId === "string";
125+
const isExistingInstanceUpdate = !hasRule && hasRecurringEvent;
126+
127+
if (isExistingInstanceUpdate) delete event.payload.recurrence?.rule;
128+
129+
return Promise.resolve([event]);
130+
}
131+
132+
static async generateEvents(
133+
event: CompassEvent,
134+
session?: ClientSession,
135+
): Promise<CompassEvent[]> {
136+
switch (event.applyTo) {
137+
case RecurringEventUpdateScope.ALL_EVENTS:
138+
return CompassEventFactory.genAllEvents(event, session);
139+
case RecurringEventUpdateScope.THIS_AND_FOLLOWING_EVENTS:
140+
return CompassEventFactory.genThisAndFollowingEvents(event, session);
141+
case RecurringEventUpdateScope.THIS_EVENT:
142+
default:
143+
return CompassEventFactory.genThisEvent(event);
144+
}
145+
}
146+
}

0 commit comments

Comments
 (0)