Skip to content

Commit c648a4c

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

File tree

12 files changed

+1429
-85
lines changed

12 files changed

+1429
-85
lines changed

packages/backend/src/event/classes/compass.event.parser.test.ts

Lines changed: 778 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
import { ClientSession, ObjectId, WithId } from "mongodb";
2+
import { Logger } from "@core/logger/winston.logger";
3+
import {
4+
CalendarProvider,
5+
Categories_Recurrence,
6+
CompassEvent,
7+
Schema_Event,
8+
Schema_Event_Core,
9+
TransitionCategoriesRecurrence,
10+
TransitionStatus,
11+
} from "@core/types/event.types";
12+
import { gSchema$Event } from "@core/types/gcal";
13+
import {
14+
isBase,
15+
isInstance,
16+
isRegularEvent,
17+
} from "@core/util/event/event.util";
18+
import { GenericError } from "@backend/common/errors/generic/generic.errors";
19+
import { error } from "@backend/common/errors/handlers/error.handler";
20+
import mongoService from "@backend/common/services/mongo.service";
21+
import { CompassEventRRule } from "@backend/event/classes/compass.event.rrule";
22+
import {
23+
_createGcalEvent,
24+
_updateGcal,
25+
} from "@backend/event/services/event.service";
26+
import { Event_Transition, Operation_Sync } from "@backend/sync/sync.types";
27+
28+
export class CompassEventParser {
29+
#logger = Logger("app.event.classes.compass.event.parser");
30+
#_event: CompassEvent;
31+
#event!: WithId<Omit<Schema_Event, "_id">>;
32+
#title!: string;
33+
#dbEvent!: WithId<Omit<Schema_Event, "_id">> | null;
34+
#isInstance!: boolean;
35+
#isBase!: boolean;
36+
#isStandalone!: boolean;
37+
#isDbInstance!: boolean;
38+
#isDbBase!: boolean;
39+
#isDbStandalone!: boolean;
40+
#rrule!: CompassEventRRule | null;
41+
#dbRrule!: CompassEventRRule | null;
42+
#transition!: Event_Transition["transition"];
43+
#summary!: Omit<Event_Transition, "operation">;
44+
45+
constructor(event: CompassEvent) {
46+
this.#_event = event;
47+
48+
this.#event = {
49+
...event.payload,
50+
_id: new ObjectId(event.eventId ?? event.payload._id),
51+
};
52+
}
53+
54+
get isInstance(): boolean {
55+
return this.#ensureInitInvoked(this.#isInstance);
56+
}
57+
58+
get isBase(): boolean {
59+
return this.#ensureInitInvoked(this.#isBase);
60+
}
61+
62+
get isStandalone(): boolean {
63+
return this.#ensureInitInvoked(this.#isStandalone);
64+
}
65+
66+
get isDbInstance(): boolean {
67+
return this.#ensureInitInvoked(this.#isDbInstance);
68+
}
69+
70+
get isDbBase(): boolean {
71+
return this.#ensureInitInvoked(this.#isDbBase);
72+
}
73+
74+
get isDbStandalone(): boolean {
75+
return this.#ensureInitInvoked(this.#isDbStandalone);
76+
}
77+
78+
get rrule(): CompassEventRRule | null {
79+
return this.#ensureInitInvoked(this.#rrule);
80+
}
81+
82+
get dbRrule(): CompassEventRRule | null {
83+
return this.#ensureInitInvoked(this.#dbRrule);
84+
}
85+
86+
get transition(): Event_Transition["transition"] {
87+
return this.#ensureInitInvoked(this.#transition);
88+
}
89+
90+
get category(): Categories_Recurrence {
91+
return this.#transition?.[0] ?? this.#getCategory();
92+
}
93+
94+
get summary(): Omit<Event_Transition, "operation"> {
95+
return this.#ensureInitInvoked(this.#summary);
96+
}
97+
98+
getTransitionString(): `${Categories_Recurrence | "NIL"}->>${TransitionCategoriesRecurrence}` {
99+
return `${this.#transition[0] ?? "NIL"}->>${this.#transition[1]}`;
100+
}
101+
102+
/**
103+
* init
104+
*
105+
* we need to fetch the compass event first to properly discriminate
106+
* the event types since they can be ambiguous if interpreted from
107+
* gcal sync watch updates
108+
*/
109+
async init(session?: ClientSession): Promise<void> {
110+
if (this.#dbEvent === null || !!this.#dbEvent) return;
111+
112+
this.#title = this.#event.title ?? this.#_event.eventId ?? "unknown";
113+
114+
const status: TransitionStatus = this.#_event.status;
115+
const filter = this.getBaseEventFilter();
116+
117+
const cEvent = await mongoService.event.findOne(filter, { session });
118+
119+
this.#dbEvent = cEvent ?? null;
120+
121+
this.#isInstance = isInstance(this.#event);
122+
this.#isDbInstance = cEvent ? isInstance(cEvent) : false;
123+
124+
this.#isBase = isBase(this.#event);
125+
this.#isDbBase = cEvent ? isBase(cEvent) : false;
126+
127+
this.#isStandalone = isRegularEvent(this.#event);
128+
this.#isDbStandalone = cEvent ? isRegularEvent(cEvent) : false;
129+
130+
this.#rrule = this.#isBase ? new CompassEventRRule(this.#event) : null;
131+
132+
this.#dbRrule = this.#isDbBase
133+
? new CompassEventRRule(this.#dbEvent!)
134+
: null;
135+
136+
this.#transition = [
137+
this.#getDbCategory(),
138+
`${this.#getCategory()}_${status}`,
139+
];
140+
141+
this.#summary = {
142+
title: this.#title,
143+
transition: this.#transition,
144+
category: this.category,
145+
};
146+
}
147+
148+
private getBaseEventFilter(): { _id: ObjectId; user: string } {
149+
const _id = this.#event._id;
150+
const user = this.#event.user ?? this.#_event.userId;
151+
152+
return { _id, user };
153+
}
154+
155+
async createEvent(operation: Operation_Sync): Promise<Event_Transition[]> {
156+
this.#logger.info(
157+
`CREATING ${this.getTransitionString()}: ${this.#event._id} (Compass)`,
158+
);
159+
// create series in calendar providers
160+
const calendarProvider = CalendarProvider.GOOGLE;
161+
const userId = this.getBaseEventFilter().user;
162+
const baseEvent = this.rrule!.base() as unknown as Schema_Event_Core;
163+
164+
switch (calendarProvider) {
165+
case CalendarProvider.GOOGLE: {
166+
const event = await _createGcalEvent(userId, baseEvent);
167+
168+
return event ? [this.#getOperationSummary(operation)] : [];
169+
}
170+
default:
171+
return [];
172+
}
173+
}
174+
175+
async updateEvent(
176+
operation: Operation_Sync,
177+
extras?: Pick<gSchema$Event, "status" | "recurrence">,
178+
): Promise<Event_Transition[]> {
179+
this.#logger.info(
180+
`UPDATING ${this.getTransitionString()}: ${this.#event._id} (Compass)`,
181+
);
182+
183+
const calendarProvider = CalendarProvider.GOOGLE;
184+
const userId = this.getBaseEventFilter().user;
185+
const baseEvent = this.rrule!.base() as unknown as Schema_Event_Core;
186+
187+
switch (calendarProvider) {
188+
case CalendarProvider.GOOGLE: {
189+
const event = await _updateGcal(userId, baseEvent, extras);
190+
191+
return event ? [this.#getOperationSummary(operation)] : [];
192+
}
193+
default:
194+
return [];
195+
}
196+
}
197+
198+
async deleteEvent(operation: Operation_Sync): Promise<Event_Transition[]> {
199+
this.#logger.info(
200+
`DELETING ${this.getTransitionString()}: ${this.#event._id} (Compass)`,
201+
);
202+
203+
return this.updateEvent(operation, { status: "cancelled" });
204+
}
205+
206+
#getCategory(): Categories_Recurrence {
207+
switch (true) {
208+
case this.isStandalone:
209+
return Categories_Recurrence.STANDALONE;
210+
case this.isBase:
211+
return Categories_Recurrence.RECURRENCE_BASE;
212+
case this.isInstance:
213+
return Categories_Recurrence.RECURRENCE_INSTANCE;
214+
default:
215+
throw new Error("could not determine event category");
216+
}
217+
}
218+
219+
#getDbCategory(): Categories_Recurrence | null {
220+
switch (true) {
221+
case this.#isDbStandalone:
222+
return Categories_Recurrence.STANDALONE;
223+
case this.#isDbBase:
224+
return Categories_Recurrence.RECURRENCE_BASE;
225+
case this.#isDbInstance:
226+
return Categories_Recurrence.RECURRENCE_INSTANCE;
227+
default:
228+
return null;
229+
}
230+
}
231+
232+
#getOperationSummary(operation: Operation_Sync): Event_Transition {
233+
return this.#ensureInitInvoked({ ...this.summary, operation });
234+
}
235+
236+
#ensureInitInvoked<T = unknown>(value: T) {
237+
if (this.#dbEvent === undefined) {
238+
throw error(GenericError.DeveloperError, "did you call `init` yet");
239+
}
240+
241+
return value;
242+
}
243+
}

packages/backend/src/event/classes/compass.event.rrule.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,9 @@ export class CompassEventRRule extends RRule {
7979
* @description Returns all instances of the event based on the recurrence rule.
8080
* @note **This is a test-only method for now, it is not to be used in production.**
8181
*/
82-
instances(): WithId<Omit<Schema_Event_Recur_Instance, "_id">>[] {
82+
instances(
83+
baseEventId?: string,
84+
): WithId<Omit<Schema_Event_Recur_Instance, "_id">>[] {
8385
return this.all().map((date) => {
8486
const timezone = dayjs.tz.guess();
8587
const startDate = dayjs(date).tz(timezone);
@@ -92,7 +94,7 @@ export class CompassEventRRule extends RRule {
9294
_id: new ObjectId(),
9395
startDate: startDate.format(this.#dateFormat),
9496
endDate: endDate.format(this.#dateFormat),
95-
recurrence: { eventId: this.base()._id.toString() },
97+
recurrence: { eventId: baseEventId ?? this.base()._id.toString() },
9698
};
9799
});
98100
}

0 commit comments

Comments
 (0)