|
| 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 | +} |
0 commit comments