Skip to content

Commit 183068e

Browse files
committed
:sparkles feat(sub-calendars): sub calendars WIP
1 parent 3a8ec1f commit 183068e

30 files changed

+994
-783
lines changed

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type {
1515
gSchema$EventBase,
1616
gSchema$Events,
1717
} from "@core/types/gcal";
18+
import { Resource_Sync } from "@core/types/sync.types";
1819
import {
1920
isBaseGCalEvent,
2021
isInstanceGCalEvent,
@@ -390,6 +391,23 @@ export const mockGcal = ({
390391
});
391392
},
392393
),
394+
watch: jest.fn(
395+
async (
396+
params: calendar_v3.Params$Resource$Calendarlist$Watch,
397+
options: MethodOptions = {},
398+
): GaxiosPromise<gSchema$Channel> =>
399+
Promise.resolve({
400+
config: options,
401+
statusText: "OK",
402+
status: 200,
403+
data: {
404+
...params.requestBody,
405+
resourceId: Resource_Sync.CALENDAR,
406+
},
407+
headers: options.headers!,
408+
request: { responseURL: params.requestBody!.address! },
409+
}),
410+
),
393411
},
394412
channels: {
395413
...calendar.channels,

packages/backend/src/auth/middleware/auth.middleware.ts

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { error } from "@backend/common/errors/handlers/error.handler";
66
import { GcalError } from "@backend/common/errors/integration/gcal/gcal.errors";
77
import { SReqBody } from "@backend/common/types/express.types";
88
import { hasGoogleHeaders } from "@backend/sync/util/sync.util";
9+
import { COMPASS_RESOURCE_HEADER } from "../../../../core/src/constants/core.constants";
10+
import { decodeChannelToken } from "../../sync/util/watch.util";
911

1012
class AuthMiddleware {
1113
verifyIsDev = (_req: Request, res: Response, next: NextFunction) => {
@@ -35,19 +37,22 @@ class AuthMiddleware {
3537
};
3638

3739
verifyIsFromGoogle = (req: Request, res: Response, next: NextFunction) => {
38-
const tokenIsInvalid =
39-
(req.headers["x-goog-channel-token"] as string) !==
40-
ENV.TOKEN_GCAL_NOTIFICATION;
41-
const isMissingHeaders = !hasGoogleHeaders(req.headers);
40+
try {
41+
const isMissingHeaders = !hasGoogleHeaders(req.headers);
4242

43-
if (isMissingHeaders || tokenIsInvalid) {
44-
res
45-
.status(Status.FORBIDDEN)
46-
.send({ error: error(GcalError.Unauthorized, "Notification Failed") });
47-
return;
48-
}
43+
if (isMissingHeaders) {
44+
throw error(GcalError.Unauthorized, "Notification Failed");
45+
}
4946

50-
next();
47+
const token = req.headers["x-goog-channel-token"] as string;
48+
const { resource } = decodeChannelToken(token);
49+
50+
res.set(COMPASS_RESOURCE_HEADER, resource);
51+
52+
next();
53+
} catch (e) {
54+
next(e);
55+
}
5156
};
5257

5358
verifyGoogleOauthCode = (

packages/backend/src/common/errors/sync/sync.errors.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { ErrorMetadata } from "@backend/common/types/error.types";
33

44
interface SyncErrors {
55
AccessRevoked: ErrorMetadata;
6-
CalendarWatchExists: ErrorMetadata;
76
NoGCalendarId: ErrorMetadata;
87
NoResourceId: ErrorMetadata;
98
NoSyncToken: ErrorMetadata;
@@ -17,11 +16,6 @@ export const SyncError: SyncErrors = {
1716
status: Status.GONE,
1817
isOperational: true,
1918
},
20-
CalendarWatchExists: {
21-
description: "Watch already exists",
22-
status: Status.BAD_REQUEST,
23-
isOperational: true,
24-
},
2519
NoGCalendarId: {
2620
description: "No gCalendarId",
2721
status: Status.NO_CONTENT,
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Status } from "@core/errors/status.codes";
2+
import { ErrorMetadata } from "@backend/common/types/error.types";
3+
4+
interface WatchErrors {
5+
EventWatchExists: ErrorMetadata;
6+
CalendarWatchExists: ErrorMetadata;
7+
}
8+
9+
export const WatchError: WatchErrors = {
10+
EventWatchExists: {
11+
description: "Event watch already exists",
12+
status: Status.BAD_REQUEST,
13+
isOperational: true,
14+
},
15+
CalendarWatchExists: {
16+
description: "Calendar watch already exists",
17+
status: Status.BAD_REQUEST,
18+
isOperational: true,
19+
},
20+
};

packages/backend/src/common/services/gcal/gcal.service.ts

Lines changed: 74 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,17 @@ import type {
66
gSchema$Event,
77
gSchema$Events,
88
} from "@core/types/gcal";
9-
import type { Params_WatchEvents } from "@core/types/sync.types";
9+
import {
10+
type Params_WatchEvents,
11+
Resource_Sync,
12+
SyncDetails,
13+
} from "@core/types/sync.types";
14+
import { IDSchemaV4 } from "@core/types/type.utils";
1015
import { GCAL_PRIMARY } from "@backend/common/constants/backend.constants";
11-
import { ENV } from "@backend/common/constants/env.constants";
1216
import { error } from "@backend/common/errors/handlers/error.handler";
1317
import { GcalError } from "@backend/common/errors/integration/gcal/gcal.errors";
1418
import { getBaseURL } from "@backend/servers/ngrok/ngrok.utils";
19+
import { encodeChannelToken } from "@backend/sync/util/watch.util";
1520

1621
class GCalService {
1722
private validateGCalResponse<T>(
@@ -31,22 +36,25 @@ class GCalService {
3136
gcal: gCalendar,
3237
gcalEventId: string,
3338
calendarId = GCAL_PRIMARY,
34-
) {
39+
): Promise<gSchema$Event> {
3540
const response = await gcal.events.get({
3641
calendarId,
3742
eventId: gcalEventId,
3843
});
3944

40-
return response.data;
45+
return this.validateGCalResponse(response).data;
4146
}
4247

43-
async createEvent(gcal: gCalendar, event: gSchema$Event) {
48+
async createEvent(
49+
gcal: gCalendar,
50+
event: gSchema$Event,
51+
): Promise<gSchema$Event> {
4452
const response = await gcal.events.insert({
4553
calendarId: GCAL_PRIMARY,
4654
requestBody: event,
4755
});
4856

49-
return response.data;
57+
return this.validateGCalResponse(response).data;
5058
}
5159

5260
async deleteEvent(gcal: gCalendar, gcalEventId: string) {
@@ -55,6 +63,7 @@ class GCalService {
5563
eventId: gcalEventId,
5664
sendUpdates: "all",
5765
});
66+
5867
return response;
5968
}
6069

@@ -75,12 +84,14 @@ class GCalService {
7584
pageToken,
7685
maxResults,
7786
});
78-
return response;
87+
88+
return this.validateGCalResponse(response);
7989
}
8090

8191
async getEvents(gcal: gCalendar, params: gParamsEventsList) {
8292
const response = await gcal.events.list(params);
83-
return response;
93+
94+
return this.validateGCalResponse(response);
8495
}
8596

8697
async *getBaseRecurringEventInstances({
@@ -181,8 +192,14 @@ class GCalService {
181192
} while (hasNextPage || !isLastPage);
182193
}
183194

184-
async getCalendarlist(gcal: gCalendar) {
185-
const response = await gcal.calendarList.list();
195+
async getCalendarlist(
196+
gcal: gCalendar,
197+
{
198+
nextSyncToken: syncToken,
199+
nextPageToken: pageToken,
200+
}: Partial<Pick<SyncDetails, "nextSyncToken" | "nextPageToken">> = {},
201+
) {
202+
const response = await gcal.calendarList.list({ syncToken, pageToken });
186203

187204
if (!response.data.nextSyncToken) {
188205
throw error(
@@ -203,24 +220,64 @@ class GCalService {
203220
eventId: gEventId,
204221
requestBody: event,
205222
});
206-
return response.data;
223+
224+
return this.validateGCalResponse(response).data;
207225
}
208226

209-
watchEvents = async (gcal: gCalendar, params: Params_WatchEvents) => {
210-
const { data } = await gcal.events.watch({
227+
watchCalendars = async (
228+
gcal: gCalendar,
229+
params: Omit<Params_WatchEvents, "gCalendarId" | "resourceId">,
230+
) => {
231+
const response = await gcal.calendarList.watch({
232+
quotaUser: params.quotaUser,
233+
requestBody: {
234+
// reminder: address always needs to be HTTPS
235+
address: getBaseURL() + GCAL_NOTIFICATION_ENDPOINT,
236+
expiration: params.expiration,
237+
id: IDSchemaV4.parse(params.channelId),
238+
token: encodeChannelToken({ resource: Resource_Sync.CALENDAR }),
239+
type: "web_hook",
240+
},
241+
});
242+
243+
return { watch: this.validateGCalResponse(response).data };
244+
};
245+
246+
watchEvents = async (
247+
gcal: gCalendar,
248+
params: Omit<Params_WatchEvents, "resourceId">,
249+
) => {
250+
const response = await gcal.events.watch({
211251
calendarId: params.gCalendarId,
252+
quotaUser: params.quotaUser,
212253
requestBody: {
213254
// reminder: address always needs to be HTTPS
214255
address: getBaseURL() + GCAL_NOTIFICATION_ENDPOINT,
215256
expiration: params.expiration,
216-
id: params.channelId,
217-
token: ENV.TOKEN_GCAL_NOTIFICATION,
257+
id: IDSchemaV4.parse(params.channelId),
258+
token: encodeChannelToken({ resource: Resource_Sync.EVENTS }),
218259
type: "web_hook",
219260
},
220-
syncToken: params.nextSyncToken,
221261
});
222262

223-
return { watch: data };
263+
return { watch: this.validateGCalResponse(response).data };
264+
};
265+
266+
stopWatch = async (
267+
gcal: gCalendar,
268+
params: Pick<Params_WatchEvents, "channelId" | "quotaUser"> & {
269+
resourceId: string;
270+
},
271+
) => {
272+
const response = await gcal.channels.stop({
273+
quotaUser: params.quotaUser,
274+
requestBody: {
275+
id: params.channelId,
276+
resourceId: params.resourceId,
277+
},
278+
});
279+
280+
return this.validateGCalResponse(response);
224281
};
225282
}
226283

packages/backend/src/common/services/mongo.service.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ interface InternalClient {
3434
sync: Collection<Schema_Sync>;
3535
user: Collection<Schema_User>;
3636
waitlist: Collection<Schema_Waitlist>;
37-
watch: Collection<Omit<Schema_Watch, "_id">>;
37+
watch: Collection<Schema_Watch>;
3838
}
3939

4040
class MongoService {
@@ -144,7 +144,7 @@ class MongoService {
144144
sync: db.collection<Schema_Sync>(Collections.SYNC),
145145
user: db.collection<Schema_User>(Collections.USER),
146146
waitlist: db.collection<Schema_Waitlist>(Collections.WAITLIST),
147-
watch: db.collection<Omit<Schema_Watch, "_id">>(Collections.WATCH),
147+
watch: db.collection<Schema_Watch>(Collections.WATCH),
148148
};
149149
}
150150

packages/backend/src/common/types/sync.types.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
import { DeleteResult, UpdateResult } from "mongodb";
1+
import { DeleteResult, InsertOneResult, UpdateResult } from "mongodb";
22
import { Result_Watch_Stop } from "@core/types/sync.types";
33

44
export interface Summary_Resync {
55
_delete: {
66
calendarlist?: UpdateResult;
77
events?: DeleteResult;
8-
eventWatches?: Result_Watch_Stop;
8+
watches?: Result_Watch_Stop;
99
sync?: UpdateResult;
1010
};
1111
recreate: {
1212
calendarlist?: UpdateResult;
13-
eventWatches?: UpdateResult[];
13+
watches?: InsertOneResult[];
1414
events?: "success";
1515
sync?: UpdateResult;
1616
};

packages/backend/src/event/services/event.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -528,7 +528,7 @@ export const _deleteGcal = async (
528528

529529
const response = await gcalService.deleteEvent(gcal, gEventId);
530530

531-
return response.status < 300;
531+
return response.status < 400;
532532
} catch (e) {
533533
const error = e as GaxiosError<gSchema$Event>;
534534

0 commit comments

Comments
 (0)