diff --git a/CONTEXT.md b/CONTEXT.md index 2265edd79..3463501bf 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -126,9 +126,25 @@ A Google Calendar import into Compass. A Google sync recovery path that refreshes or restarts Google data when metadata says the existing sync needs repair. +**Google Watch repair**: +A repair path that recreates missing, expired, stale, or incomplete **Google +Watches** for a **Google-connected user**. Repair should happen only when watch +state is broken; regular health checks should not repeatedly call Google when +watches are already healthy. When sync tokens are usable, watch repair should +also catch up missed Google-side changes through incremental import; when sync +tokens are missing or invalid, fall back to full **Repair**. + **Public watch notifications**: Google-to-Compass webhook posts at `/api/sync/gcal/notifications`. +**Compass-to-Google event propagation**: +The backend flow that applies Compass Event writes and mirrors eligible Google +side effects. + +**Google-to-Compass event propagation**: +The backend flow that applies Google Calendar event changes to Compass data +during Import or Public watch notification handling. + ## Relationships - An **Anonymous user** stores **Events** and **Tasks** in browser IndexedDB. diff --git a/docs/architecture/event-and-task-domain-model.md b/docs/architecture/event-and-task-domain-model.md index 75757bcde..c77fc0d1b 100644 --- a/docs/architecture/event-and-task-domain-model.md +++ b/docs/architecture/event-and-task-domain-model.md @@ -59,7 +59,7 @@ If you change recurring edit behavior, check: - `packages/core/src/types/event.types.ts` - `packages/backend/src/event/controllers/event.controller.ts` -- `packages/backend/src/sync/services/sync/compass/compass.sync.processor.ts` +- `packages/backend/src/sync/services/event-propagation/compass-to-google/compass-to-google.event-propagation.ts` ## Backend Event Shape Semantics diff --git a/docs/backend/README.md b/docs/backend/README.md index b3fce81e5..ed37266c1 100644 --- a/docs/backend/README.md +++ b/docs/backend/README.md @@ -49,7 +49,7 @@ Important runtime behavior: ## Event Writes -- Event writes route through the Compass sync processor and can apply Google side effects. +- Event writes route through Compass-to-Google event propagation and can apply Google side effects. - Missing Google refresh token does not block Compass-local event writes; Google side effects are skipped. - Controllers use the shared `res.promise(...)` response helper and centralized error handling. @@ -57,16 +57,18 @@ Important runtime behavior: Key files: - `packages/backend/src/event/controllers/event.controller.ts` -- `packages/backend/src/sync/services/sync/compass/compass.sync.processor.ts` -- `packages/backend/src/sync/services/outbound/compass-google-mirror.service.ts` +- `packages/backend/src/sync/services/event-propagation/compass-to-google/compass-to-google.event-propagation.ts` +- `packages/backend/src/sync/services/event-propagation/compass-to-google/compass-to-google-backfill.ts` ## Google Notification Ingress - endpoint: `POST /api/sync/gcal/notifications` - source: `packages/backend/src/sync/controllers/sync.controller.ts` -- middleware: `authMiddleware.verifyIsFromGoogle` +- middleware: `publicWatchNotificationIngress.verify` +- ingress owner: `packages/backend/src/sync/services/public-watch-notifications/public-watch-notification.ingress.ts` - notification owner: `packages/backend/src/sync/services/watch/google-watch.service.ts` -- import/repair owner: `packages/backend/src/sync/services/google-calendar-sync/google-calendar-sync.service.ts` +- repair/setup owner: `packages/backend/src/sync/services/google-sync/google-sync.service.ts` +- import owner: `packages/backend/src/sync/services/import/google-import.service.ts` - maintenance owner: `packages/backend/src/sync/services/watch/google-watch-maintenance.service.ts` Observed outcomes include: diff --git a/docs/backend/backend-request-flow.md b/docs/backend/backend-request-flow.md index 2d30b99e2..9302a79e2 100644 --- a/docs/backend/backend-request-flow.md +++ b/docs/backend/backend-request-flow.md @@ -92,9 +92,9 @@ For `POST /api/event`: 1. route requires `verifySession()` 2. controller adds the authenticated user id 3. controller normalizes single vs array payloads -4. controller forwards the change set to `CompassSyncProcessor` +4. controller forwards the change set to `CompassToGoogleEventPropagation` 5. controller returns a status-only payload (`{ statusCode: 204 }`) through `res.promise(...)` -6. processor: +6. Compass-to-Google event propagation: - loads current Compass DB state - analyzes the transition into a persistence plan - applies Compass DB mutations first @@ -118,7 +118,8 @@ Frequently used middleware: - `requireGoogleConnectionSession`: active Google connection required for routes that call Google directly (for example `/api/sync/import-gcal`) - `authMiddleware.verifyIsDev`: development-only route - `authMiddleware.verifyIsFromCompass`: trusted internal caller -- `authMiddleware.verifyIsFromGoogle`: trusted Google notification source +- `publicWatchNotificationIngress.verify`: trusted Google notification source +- `packages/backend/src/sync/services/public-watch-notifications/public-watch-notification.ingress.ts`: Public watch notification header validation and payload parsing Intentional unauthenticated route: diff --git a/docs/development/common-change-recipes.md b/docs/development/common-change-recipes.md index fdf47909a..404bbf8b5 100644 --- a/docs/development/common-change-recipes.md +++ b/docs/development/common-change-recipes.md @@ -28,7 +28,7 @@ Rule: never treat event shape as web-only unless the field is strictly presentat 3. Read `packages/backend/src/event/classes/compass.event.generator.ts`. 4. Read `packages/backend/src/event/classes/compass.event.parser.ts`. 5. Read `packages/backend/src/event/classes/compass.event.executor.ts`. -6. Read `packages/backend/src/sync/services/sync/compass/compass.sync.processor.ts`. +6. Read `packages/backend/src/sync/services/event-propagation/compass-to-google/compass-to-google.event-propagation.ts`. 7. Update the planner, executor, or scope-expansion path that actually owns the behavior. 8. Add focused tests for the exact recurrence transition you changed. @@ -43,13 +43,13 @@ Do not edit recurring behavior from one layer only. ## Triage A Recurrence Sync Regression 1. Reproduce with one event and one expected transition outcome. -2. Capture processor logs for the transition key: +2. Capture Compass-to-Google event propagation logs for the transition key: - `Handle Compass event(): ` 3. Find the key in `PLAN_BUILDERS` in `packages/backend/src/event/classes/compass.event.parser.ts`. 4. Confirm planned `steps` and `googleEffect` match expected behavior. 5. Confirm executor step mapping in `packages/backend/src/event/classes/compass.event.executor.ts`. 6. Run focused tests: - - `bun run test:backend --runTestsByPath packages/backend/src/event/classes/compass.event.parser.test.ts packages/backend/src/event/classes/compass.event.executor.test.ts packages/backend/src/sync/services/sync/__tests__/compass.sync.processor.test.ts --runInBand` + - `bun run test:backend --runTestsByPath packages/backend/src/event/classes/compass.event.parser.test.ts packages/backend/src/event/classes/compass.event.executor.test.ts packages/backend/src/sync/services/event-propagation/__tests__/compass-to-google.event-propagation.test.ts --runInBand` ## Add An SSE Event diff --git a/docs/development/feature-file-map.md b/docs/development/feature-file-map.md index 707032f9e..d93ca8dc9 100644 --- a/docs/development/feature-file-map.md +++ b/docs/development/feature-file-map.md @@ -82,12 +82,14 @@ Use this document to find the first files to inspect for common Compass changes. - Backend SSE server: `packages/backend/src/servers/sse/sse.server.ts` - Events stream route: `packages/backend/src/events/events.routes.config.ts` - Backend sync routes: `packages/backend/src/sync/sync.routes.config.ts` +- Public watch notification ingress: `packages/backend/src/sync/services/public-watch-notifications/public-watch-notification.ingress.ts` - Google Watch lifecycle and notifications: `packages/backend/src/sync/services/watch` -- Google Calendar sync import and repair: `packages/backend/src/sync/services/google-calendar-sync/google-calendar-sync.service.ts` -- Compass-to-Google repair mirroring: `packages/backend/src/sync/services/outbound/compass-google-mirror.service.ts` -- Sync record persistence: `packages/backend/src/sync/services/records/sync.records.ts` +- Google Calendar sync import and repair: `packages/backend/src/sync/services/google-sync/google-sync.service.ts` +- Google sync health diagnosis: `packages/backend/src/sync/services/google-sync/google-sync.health.ts` +- Compass-to-Google repair mirroring: `packages/backend/src/sync/services/event-propagation/compass-to-google/compass-to-google-backfill.ts` +- Sync record persistence: `packages/backend/src/sync/services/records/sync-records.repository.ts` - Google import internals: `packages/backend/src/sync/services/import` -- Google/Compass event processors: `packages/backend/src/sync/services/sync` +- Google/Compass event propagation: `packages/backend/src/sync/services/event-propagation` ## Users / Metadata / Priority diff --git a/docs/development/testing-playbook.md b/docs/development/testing-playbook.md index 181aeaeaf..bc10f58eb 100644 --- a/docs/development/testing-playbook.md +++ b/docs/development/testing-playbook.md @@ -236,7 +236,7 @@ Preferred style: - realistic request flows when possible - mock only external services, not internal business logic -**Do not import `mongoService` (or other persistence implementations) directly in tests.** Use test drivers instead (e.g. `UserDriver`, `WatchDriver` in `packages/backend/src/__tests__/drivers/`). Drivers encapsulate persistence so that switching away from Mongo (or another store) in the future does not require changing test code. +**Do not import `mongoService` (or other persistence implementations) directly in tests.** Use test drivers instead (e.g. `UserDriver`, `GoogleWatchDriver` in `packages/backend/src/__tests__/drivers/`). Drivers encapsulate persistence so that switching away from Mongo (or another store) in the future does not require changing test code. Useful anchors: diff --git a/docs/development/types-and-validation.md b/docs/development/types-and-validation.md index 05e05ced4..d0f0f2f59 100644 --- a/docs/development/types-and-validation.md +++ b/docs/development/types-and-validation.md @@ -58,7 +58,7 @@ Use the generic base schema with `parseApiError(error, ApiErrorResponseSchema)` - `packages/backend/src/common/constants/env.constants.ts` - `packages/backend/src/common/validators` -- feature-specific query validators such as `packages/backend/src/sync/util/sync.queries.ts` +- feature-specific query validators such as `packages/backend/src/sync/services/records/sync-records.repository.ts` ### Web form/client validation diff --git a/docs/features/google-sync-and-sse-flow.md b/docs/features/google-sync-and-sse-flow.md index 83f88f4a6..e3af973ff 100644 --- a/docs/features/google-sync-and-sse-flow.md +++ b/docs/features/google-sync-and-sse-flow.md @@ -1,6 +1,6 @@ # Google Sync And Server-Sent Events (SSE) -Compass sync is bidirectional: +Compass Google Calendar integration is bidirectional: - Compass-originated event changes can propagate to Google and then notify web clients. - Google-originated changes can flow back into Compass and then notify web clients. @@ -78,7 +78,7 @@ Source: - `packages/core/src/types/sse.types.ts` - `packages/backend/src/user/services/user.service.ts` -- `packages/backend/src/sync/services/google-calendar-sync/google-calendar-sync.service.ts` +- `packages/backend/src/sync/services/google-sync/google-sync.service.ts` `IMPORT_GCAL_END` carries an explicit `operation` so the client can distinguish repair completion from incremental completion. @@ -114,7 +114,7 @@ High-level path: 3. The selected repository writes locally or remotely. 4. Remote event writes hit backend event routes. 5. `EventController` packages the change as a `CompassEvent`. -6. `CompassSyncProcessor.processEvents()` loads the DB event, plans work, applies persistence, and runs Google side effects. +6. `CompassToGoogleEventPropagation.processEvents()` loads the DB event, plans work, applies persistence, and runs Google side effects. 7. After commit, the backend calls `sseServer` to publish notifications based on whether the change affected normal or someday events (`EVENT_CHANGED` vs `SOMEDAY_EVENT_CHANGED`). Primary files: @@ -122,33 +122,34 @@ Primary files: - `packages/web/src/ducks/events/sagas/event.sagas.ts` - `packages/web/src/common/repositories/event` - `packages/backend/src/event/controllers/event.controller.ts` -- `packages/backend/src/sync/services/sync/compass/compass.sync.processor.ts` +- `packages/backend/src/sync/services/event-propagation/compass-to-google/compass-to-google.event-propagation.ts` ## Inbound Flow: Google Notifies Compass About Changes High-level path: 1. Google posts to the notification endpoint in sync routes. -2. Backend verifies the request origin. +2. `publicWatchNotificationIngress` validates and parses the Google headers. 3. `googleWatchService.handleGoogleWatchNotification()` locates the watch and sync record. 4. The service builds a Google Calendar client for the user. 5. `GCalNotificationHandler` fetches incremental changes using the stored sync token. -6. `GcalSyncProcessor` applies those changes to Compass data. +6. `GoogleToCompassEventPropagation` applies those changes to Compass data. 7. The backend publishes `EVENT_CHANGED` (or someday equivalent) so clients refetch. Primary files: - `packages/backend/src/sync/sync.routes.config.ts` +- `packages/backend/src/sync/services/public-watch-notifications/public-watch-notification.ingress.ts` - `packages/backend/src/sync/services/watch/google-watch.service.ts` -- `packages/backend/src/sync/services/google-calendar-sync/google-calendar-sync.service.ts` -- `packages/backend/src/sync/services/records/sync.records.ts` +- `packages/backend/src/sync/services/google-sync/google-sync.service.ts` +- `packages/backend/src/sync/services/records/sync-records.repository.ts` - `packages/backend/src/sync/services/notify/handler/gcal.notification.handler.ts` -- `packages/backend/src/sync/services/sync/google/gcal.sync.processor.ts` +- `packages/backend/src/sync/services/event-propagation/google-to-compass/google-to-compass.event-propagation.ts` Lifecycle and outbound repair paths live in: -- `packages/backend/src/sync/services/google-calendar-sync/google-calendar-sync.service.ts` -- `packages/backend/src/sync/services/outbound/compass-google-mirror.service.ts` +- `packages/backend/src/sync/services/google-sync/google-sync.service.ts` +- `packages/backend/src/sync/services/event-propagation/compass-to-google/compass-to-google-backfill.ts` - `packages/backend/src/sync/services/watch/google-watch-maintenance.service.ts` ### Notification Outcomes And Error Semantics @@ -206,9 +207,15 @@ Redux reasons for refetch (`Sync_AsyncStateContextReason`) reuse the same string Source files: - `packages/backend/src/user/services/user-metadata.service.ts` +- `packages/backend/src/sync/services/google-sync/google-sync.health.ts` - `packages/core/src/types/user.types.ts` - `packages/web/src/sse/hooks/useGcalSSE.ts` +The sidebar Google connection state is derived from user metadata. The backend +metadata service delegates HEALTHY vs ATTENTION diagnosis to Google sync health, +which checks stored sync tokens and, when public HTTPS watch notifications are +enabled, active Google watches. + Auto-import guardrail: - client auto-starts import only when `sync.importGCal === "RESTART"` **and** `google.connectionState` is not `NOT_CONNECTED` or `RECONNECT_REQUIRED` diff --git a/docs/features/password-auth-flow.md b/docs/features/password-auth-flow.md index eb40b67d8..eb4dc03fc 100644 --- a/docs/features/password-auth-flow.md +++ b/docs/features/password-auth-flow.md @@ -378,8 +378,8 @@ Password-only users can now mutate Compass events without a Google connection at Relevant changes: - `event.routes.config.ts` no longer requires route-level Google connection middleware for create/update/delete -- `CompassSyncProcessor` applies the Compass mutation first -- if the Google side effect fails only because the user has no Google refresh token, the processor keeps the Compass mutation and skips the Google effect +- `CompassToGoogleEventPropagation` applies the Compass mutation first +- if the Google side effect fails only because the user has no Google refresh token, Compass-to-Google event propagation keeps the Compass mutation and skips the Google effect That lets password-auth users use Compass without blocking on Google connectivity. diff --git a/docs/features/recurring-events-handling.md b/docs/features/recurring-events-handling.md index 374e270b0..cf8e908b4 100644 --- a/docs/features/recurring-events-handling.md +++ b/docs/features/recurring-events-handling.md @@ -19,7 +19,7 @@ Primary files: ## Recurrence Categories -Compass sync logic classifies event shape using `Categories_Recurrence`: +Compass-to-Google event propagation classifies event shape using `Categories_Recurrence`: - `STANDALONE` - `RECURRENCE_BASE` @@ -28,7 +28,7 @@ Compass sync logic classifies event shape using `Categories_Recurrence`: - `RECURRENCE_BASE_SOMEDAY` - `RECURRENCE_INSTANCE_SOMEDAY` -The Compass sync path treats recurrence handling as a transition problem: +The Compass-to-Google path treats recurrence handling as a transition problem: 1. build a transition context from the incoming Compass payload plus the current DB event 2. analyze that transition into a plain `CompassOperationPlan` @@ -39,7 +39,7 @@ Primary files: - `packages/backend/src/event/classes/compass.event.parser.ts` - `packages/backend/src/event/classes/compass.event.executor.ts` -- `packages/backend/src/sync/services/sync/compass/compass.sync.processor.ts` +- `packages/backend/src/sync/services/event-propagation/compass-to-google/compass-to-google.event-propagation.ts` ## Update Scopes @@ -95,7 +95,7 @@ Useful heuristics during Google sync: - cancelled instances should be handled as instance-level deletions - payload ordering is not reliable enough to infer user intent by itself -This is why Compass sync logic keys off persisted state plus event properties instead of trying to reconstruct a single high-level Google UI action. +This is why Compass-to-Google event propagation keys off persisted state plus event properties instead of trying to reconstruct a single high-level Google UI action. ## Someday And Provider Semantics @@ -114,7 +114,7 @@ Instead: - `analyzeCompassTransition(...)` describes the implied Google effect - `applyCompassPlan(...)` performs only Compass DB mutations -- `CompassSyncProcessor` executes Google create/update/delete after Compass persistence succeeds +- `CompassToGoogleEventPropagation` executes Google create/update/delete after Compass persistence succeeds Delete-oriented Google effects should prefer the persisted DB `gEventId` when available, then fall back to the incoming payload `gEventId`. @@ -143,7 +143,7 @@ Concrete examples from current tests: - last persisted Compass event when a step returns one - `googleDeleteEventId` resolved from persisted event first, otherwise planner fallback -The processor executes Google effects only after Compass persistence succeeds. +Compass-to-Google event propagation executes Google effects only after Compass persistence succeeds. ## Recurrence Sync Triage Runbook @@ -155,7 +155,7 @@ Use this sequence when recurring edits behave unexpectedly: 3. Verify the planned `steps` order and `googleEffect` in unit tests: - `compass.event.parser.test.ts` - `compass.event.executor.test.ts` - - `compass.sync.processor.test.ts` + - `compass-to-google.event-propagation.test.ts` 4. Map each step to persistence calls in `executeStep(...)`: - `create` -> `_createCompassEvent` - `update` -> `_updateCompassEvent` @@ -183,5 +183,5 @@ Good test anchors: - `packages/backend/src/event/classes/compass.event.parser.test.ts` - `packages/backend/src/event/classes/compass.event.executor.test.ts` -- `packages/backend/src/sync/services/sync/__tests__/compass.sync.processor.all-event.test.ts` -- `packages/backend/src/sync/services/sync/__tests__/compass-sync-processor-this-event/*.test.ts` +- `packages/backend/src/sync/services/event-propagation/__tests__/compass-to-google.all-event.test.ts` +- `packages/backend/src/sync/services/event-propagation/__tests__/compass-to-google-this-event/*.test.ts` diff --git a/packages/backend/src/__tests__/drivers/event.driver.ts b/packages/backend/src/__tests__/drivers/event.driver.ts index e583a16bd..2f831787d 100644 --- a/packages/backend/src/__tests__/drivers/event.driver.ts +++ b/packages/backend/src/__tests__/drivers/event.driver.ts @@ -6,7 +6,7 @@ import { UserDriver } from "@backend/__tests__/drivers/user.driver"; import { mockGcalEvents } from "@backend/__tests__/mocks.gcal/factories/gcal.event.factory"; import calendarService from "@backend/calendar/services/calendar.service"; import mongoService from "@backend/common/services/mongo.service"; -import { getGcalClient } from "@backend/sync/services/google-calendar-sync/google.calendar.client"; +import { getGcalClient } from "@backend/sync/services/google-sync/gcal.client"; export class EventDriver { static async generateV0Data(count = 3) { diff --git a/packages/backend/src/__tests__/drivers/sync.driver.ts b/packages/backend/src/__tests__/drivers/google-sync.driver.ts similarity index 90% rename from packages/backend/src/__tests__/drivers/sync.driver.ts rename to packages/backend/src/__tests__/drivers/google-sync.driver.ts index 4721cc869..7267872b2 100644 --- a/packages/backend/src/__tests__/drivers/sync.driver.ts +++ b/packages/backend/src/__tests__/drivers/google-sync.driver.ts @@ -5,12 +5,12 @@ import { type Schema_User } from "@core/types/user.types"; import dayjs from "@core/util/date/dayjs"; import { UserDriver } from "@backend/__tests__/drivers/user.driver"; import mongoService from "@backend/common/services/mongo.service"; -import { getGcalClient } from "@backend/sync/services/google-calendar-sync/google.calendar.client"; +import { getGcalClient } from "@backend/sync/services/google-sync/gcal.client"; +import { updateSync } from "@backend/sync/services/records/sync-records.repository"; import { googleWatchService } from "@backend/sync/services/watch/google-watch.service"; -import { updateSync } from "@backend/sync/util/sync.queries"; -export class SyncDriver { - static async createSync( +export class GoogleSyncDriver { + static async createHealthyGoogleSync( user: Pick, "_id">, defaultUser = false, ): Promise { @@ -31,7 +31,7 @@ export class SyncDriver { ); } - static async generateV0Data( + static async generateLegacySyncWatchData( numUsers = 3, generateExpiredWatches = false, ): Promise>>> { diff --git a/packages/backend/src/__tests__/drivers/google-watch.driver.ts b/packages/backend/src/__tests__/drivers/google-watch.driver.ts new file mode 100644 index 000000000..28d1683fa --- /dev/null +++ b/packages/backend/src/__tests__/drivers/google-watch.driver.ts @@ -0,0 +1,9 @@ +import mongoService from "@backend/common/services/mongo.service"; + +export class GoogleWatchDriver { + static removeActiveGoogleWatchesForUser( + userId: string, + ): ReturnType { + return mongoService.watch.deleteMany({ user: userId }); + } +} diff --git a/packages/backend/src/__tests__/drivers/sync.controller.driver.ts b/packages/backend/src/__tests__/drivers/sync.controller.driver.ts index bb8e0ae36..717cfd884 100644 --- a/packages/backend/src/__tests__/drivers/sync.controller.driver.ts +++ b/packages/backend/src/__tests__/drivers/sync.controller.driver.ts @@ -3,7 +3,7 @@ import { GCAL_NOTIFICATION_ENDPOINT } from "@core/constants/core.constants"; import { Status } from "@core/errors/status.codes"; import { type Payload_Sync_Notif } from "@core/types/sync.types"; import { type BaseDriver } from "@backend/__tests__/drivers/base.driver"; -import { encodeChannelToken } from "@backend/sync/util/watch.util"; +import { encodeChannelToken } from "@backend/sync/services/watch/google-watch-token"; export class SyncControllerDriver { constructor(private readonly baseDriver: BaseDriver) {} diff --git a/packages/backend/src/__tests__/drivers/util.driver.ts b/packages/backend/src/__tests__/drivers/util.driver.ts index babd1230e..b9b32a488 100644 --- a/packages/backend/src/__tests__/drivers/util.driver.ts +++ b/packages/backend/src/__tests__/drivers/util.driver.ts @@ -1,13 +1,13 @@ import { type WithId } from "mongodb"; import { type Schema_User } from "@core/types/user.types"; -import { SyncDriver } from "@backend/__tests__/drivers/sync.driver"; +import { GoogleSyncDriver } from "@backend/__tests__/drivers/google-sync.driver"; import { UserDriver } from "@backend/__tests__/drivers/user.driver"; export class UtilDriver { static async setupTestUser(): Promise<{ user: WithId }> { const user = await UserDriver.createUser(); - await SyncDriver.createSync(user, true); + await GoogleSyncDriver.createHealthyGoogleSync(user, true); return { user }; } diff --git a/packages/backend/src/__tests__/drivers/watch.driver.ts b/packages/backend/src/__tests__/drivers/watch.driver.ts deleted file mode 100644 index e3c6fc9d0..000000000 --- a/packages/backend/src/__tests__/drivers/watch.driver.ts +++ /dev/null @@ -1,14 +0,0 @@ -import mongoService from "@backend/common/services/mongo.service"; - -/** - * Test driver for the watch collection. - * Use this instead of importing mongoService in tests so persistence can be - * swapped (e.g. away from Mongo) without changing test code. - */ -export class WatchDriver { - static deleteManyByUser( - userId: string, - ): ReturnType { - return mongoService.watch.deleteMany({ user: userId }); - } -} diff --git a/packages/backend/src/auth/middleware/auth.middleware.ts b/packages/backend/src/auth/middleware/auth.middleware.ts index 9f79136de..8494e5a73 100644 --- a/packages/backend/src/auth/middleware/auth.middleware.ts +++ b/packages/backend/src/auth/middleware/auth.middleware.ts @@ -1,12 +1,8 @@ import { type NextFunction, type Request, type Response } from "express"; -import { COMPASS_RESOURCE_HEADER } from "@core/constants/core.constants"; import { Status } from "@core/errors/status.codes"; import { ENV, IS_DEV } from "@backend/common/constants/env.constants"; import { AuthError } from "@backend/common/errors/auth/auth.errors"; import { error } from "@backend/common/errors/handlers/error.handler"; -import { GcalError } from "@backend/common/errors/integration/gcal/gcal.errors"; -import { hasGoogleHeaders } from "@backend/sync/util/sync.util"; -import { decodeChannelToken } from "@backend/sync/util/watch.util"; class AuthMiddleware { verifyIsDev = (_req: Request, res: Response, next: NextFunction) => { @@ -34,27 +30,6 @@ class AuthMiddleware { next(); }; - - verifyIsFromGoogle = (req: Request, res: Response, next: NextFunction) => { - try { - const token = req.headers["x-goog-channel-token"] as string; - const isMissingHeaders = !hasGoogleHeaders(req.headers); - const decoded = decodeChannelToken(token); - - if (isMissingHeaders || !decoded) { - res.status(Status.FORBIDDEN).send({ - error: error(GcalError.Unauthorized, "Notification Failed"), - }); - return; - } - - res.set(COMPASS_RESOURCE_HEADER, decoded.resource); - - next(); - } catch (e) { - next(e); - } - }; } export default new AuthMiddleware(); diff --git a/packages/backend/src/auth/services/google/google.auth.service.test.ts b/packages/backend/src/auth/services/google/google.auth.service.test.ts index a1efac13a..375c932e6 100644 --- a/packages/backend/src/auth/services/google/google.auth.service.test.ts +++ b/packages/backend/src/auth/services/google/google.auth.service.test.ts @@ -12,7 +12,7 @@ import { determineGoogleAuthMode } from "@backend/auth/services/google/util/goog import { AuthError } from "@backend/common/errors/auth/auth.errors"; import mongoService from "@backend/common/services/mongo.service"; import EmailService from "@backend/email/email.service"; -import { googleCalendarSyncService } from "@backend/sync/services/google-calendar-sync/google-calendar-sync.service"; +import { googleCalendarSyncService } from "@backend/sync/services/google-sync/google-sync.service"; import userService from "@backend/user/services/user.service"; import userMetadataService from "@backend/user/services/user-metadata.service"; import { googleAuthService } from "./google.auth.service"; diff --git a/packages/backend/src/auth/services/google/google.auth.service.ts b/packages/backend/src/auth/services/google/google.auth.service.ts index 160024b49..7e7dcd14c 100644 --- a/packages/backend/src/auth/services/google/google.auth.service.ts +++ b/packages/backend/src/auth/services/google/google.auth.service.ts @@ -15,7 +15,7 @@ import { UserError } from "@backend/common/errors/user/user.errors"; import { normalizeEmail } from "@backend/common/helpers/email.util"; import mongoService from "@backend/common/services/mongo.service"; import EmailService from "@backend/email/email.service"; -import { googleCalendarSyncService } from "@backend/sync/services/google-calendar-sync/google-calendar-sync.service"; +import { googleCalendarSyncService } from "@backend/sync/services/google-sync/google-sync.service"; import { findCompassUserBy } from "@backend/user/queries/user.queries"; import userService from "@backend/user/services/user.service"; import userMetadataService from "@backend/user/services/user-metadata.service"; diff --git a/packages/backend/src/auth/services/google/google.auth.success.utils.test.ts b/packages/backend/src/auth/services/google/google.auth.success.utils.test.ts index 9ad1a04f5..75876df2a 100644 --- a/packages/backend/src/auth/services/google/google.auth.success.utils.test.ts +++ b/packages/backend/src/auth/services/google/google.auth.success.utils.test.ts @@ -1,18 +1,16 @@ import { faker } from "@faker-js/faker"; import { ObjectId } from "mongodb"; -import * as syncQueries from "@backend/sync/util/sync.queries"; -import * as syncUtil from "@backend/sync/util/sync.util"; +import * as syncRecords from "@backend/sync/services/records/sync-records.repository"; import * as userQueries from "@backend/user/queries/user.queries"; import { determineGoogleAuthMode } from "./util/google.auth.util"; jest.mock("@backend/user/queries/user.queries"); -jest.mock("@backend/sync/util/sync.queries"); -jest.mock("@backend/sync/util/sync.util"); +jest.mock("@backend/sync/services/records/sync-records.repository"); const mockFindCanonicalCompassUser = userQueries.findCanonicalCompassUser as jest.Mock; -const mockGetSync = syncQueries.getSync as jest.Mock; -const mockCanDoIncrementalSync = syncUtil.canDoIncrementalSync as jest.Mock; +const mockGetSync = syncRecords.getSync as jest.Mock; +const mockCanDoIncrementalSync = syncRecords.canDoIncrementalSync as jest.Mock; function makeCompassUser(overrides?: { googleId?: string; diff --git a/packages/backend/src/auth/services/google/util/google.auth.util.test.ts b/packages/backend/src/auth/services/google/util/google.auth.util.test.ts index 3c3bf65dd..1aaea0977 100644 --- a/packages/backend/src/auth/services/google/util/google.auth.util.test.ts +++ b/packages/backend/src/auth/services/google/util/google.auth.util.test.ts @@ -1,8 +1,7 @@ import { faker } from "@faker-js/faker"; import { type Credentials, type TokenPayload } from "google-auth-library"; import { ObjectId } from "mongodb"; -import * as syncQueries from "@backend/sync/util/sync.queries"; -import * as syncUtil from "@backend/sync/util/sync.util"; +import * as syncRecords from "@backend/sync/services/records/sync-records.repository"; import * as userQueries from "@backend/user/queries/user.queries"; import { determineGoogleAuthMode, @@ -10,13 +9,12 @@ import { } from "./google.auth.util"; jest.mock("@backend/user/queries/user.queries"); -jest.mock("@backend/sync/util/sync.queries"); -jest.mock("@backend/sync/util/sync.util"); +jest.mock("@backend/sync/services/records/sync-records.repository"); const mockFindCanonicalCompassUser = userQueries.findCanonicalCompassUser as jest.Mock; -const mockGetSync = syncQueries.getSync as jest.Mock; -const mockCanDoIncrementalSync = syncUtil.canDoIncrementalSync as jest.Mock; +const mockGetSync = syncRecords.getSync as jest.Mock; +const mockCanDoIncrementalSync = syncRecords.canDoIncrementalSync as jest.Mock; function makeCompassUser(overrides?: { googleId?: string; diff --git a/packages/backend/src/auth/services/google/util/google.auth.util.ts b/packages/backend/src/auth/services/google/util/google.auth.util.ts index cc164bf4b..6ec6fb629 100644 --- a/packages/backend/src/auth/services/google/util/google.auth.util.ts +++ b/packages/backend/src/auth/services/google/util/google.auth.util.ts @@ -1,7 +1,9 @@ import { type Credentials, type TokenPayload } from "google-auth-library"; import { StringV4Schema, zObjectId } from "@core/types/type.utils"; -import { getSync } from "@backend/sync/util/sync.queries"; -import { canDoIncrementalSync } from "@backend/sync/util/sync.util"; +import { + canDoIncrementalSync, + getSync, +} from "@backend/sync/services/records/sync-records.repository"; import { findCanonicalCompassUser } from "@backend/user/queries/user.queries"; import { type AuthDecision, diff --git a/packages/backend/src/calendar/services/calendar.service.test.ts b/packages/backend/src/calendar/services/calendar.service.test.ts index 106e446b0..2e076df1c 100644 --- a/packages/backend/src/calendar/services/calendar.service.test.ts +++ b/packages/backend/src/calendar/services/calendar.service.test.ts @@ -14,7 +14,7 @@ import { setupTestDb, } from "@backend/__tests__/helpers/mock.db.setup"; import calendarService from "@backend/calendar/services/calendar.service"; -import { getGcalClient } from "@backend/sync/services/google-calendar-sync/google.calendar.client"; +import { getGcalClient } from "@backend/sync/services/google-sync/gcal.client"; describe("CalendarService", () => { beforeEach(setupTestDb); diff --git a/packages/backend/src/calendar/services/calendar.service.ts b/packages/backend/src/calendar/services/calendar.service.ts index fd8e68697..8897c872e 100644 --- a/packages/backend/src/calendar/services/calendar.service.ts +++ b/packages/backend/src/calendar/services/calendar.service.ts @@ -10,8 +10,8 @@ import { type gCalendar } from "@core/types/gcal"; import { Resource_Sync } from "@core/types/sync.types"; import { zObjectId } from "@core/types/type.utils"; import mongoService from "@backend/common/services/mongo.service"; -import { getCalendarsToSync } from "@backend/sync/services/init/sync.init"; -import { updateSync } from "@backend/sync/util/sync.queries"; +import { getCalendarsToSync } from "@backend/sync/services/init/google-sync-init"; +import { updateSync } from "@backend/sync/services/records/sync-records.repository"; class CalendarService { private static readonly calendarSelectionToggleSchema = z diff --git a/packages/backend/src/common/errors/handlers/error.express.handler.ts b/packages/backend/src/common/errors/handlers/error.express.handler.ts index f60ed672e..77114de69 100644 --- a/packages/backend/src/common/errors/handlers/error.express.handler.ts +++ b/packages/backend/src/common/errors/handlers/error.express.handler.ts @@ -24,8 +24,8 @@ import { } from "@backend/common/types/error.types"; import { type SessionResponse } from "@backend/common/types/express.types"; import { sseServer } from "@backend/servers/sse/sse.server"; -import { googleCalendarSyncService } from "@backend/sync/services/google-calendar-sync/google-calendar-sync.service"; -import { getSyncByToken } from "@backend/sync/util/sync.queries"; +import { googleCalendarSyncService } from "@backend/sync/services/google-sync/google-sync.service"; +import { getSyncByToken } from "@backend/sync/services/records/sync-records.repository"; import { findCompassUserBy } from "@backend/user/queries/user.queries"; import userService from "@backend/user/services/user.service"; diff --git a/packages/backend/src/common/services/gcal/gcal.service.test.ts b/packages/backend/src/common/services/gcal/gcal.service.test.ts index 27b5e2903..d9d59b6a3 100644 --- a/packages/backend/src/common/services/gcal/gcal.service.test.ts +++ b/packages/backend/src/common/services/gcal/gcal.service.test.ts @@ -2,7 +2,7 @@ jest.mock("@backend/common/util/api-base-url.util", () => ({ getGcalWebhookBaseURL: jest.fn(() => "https://example.trycloudflare.com/api"), })); -jest.mock("@backend/sync/util/watch.util", () => ({ +jest.mock("@backend/sync/services/watch/google-watch-token", () => ({ encodeChannelToken: jest.fn(() => "encoded-token"), })); diff --git a/packages/backend/src/common/services/gcal/gcal.service.ts b/packages/backend/src/common/services/gcal/gcal.service.ts index 42265b4f6..b91f28582 100644 --- a/packages/backend/src/common/services/gcal/gcal.service.ts +++ b/packages/backend/src/common/services/gcal/gcal.service.ts @@ -16,7 +16,7 @@ import { GCAL_PRIMARY } from "@backend/common/constants/backend.constants"; import { error } from "@backend/common/errors/handlers/error.handler"; import { GcalError } from "@backend/common/errors/integration/gcal/gcal.errors"; import { getGcalWebhookBaseURL } from "@backend/common/util/api-base-url.util"; -import { encodeChannelToken } from "@backend/sync/util/watch.util"; +import { encodeChannelToken } from "@backend/sync/services/watch/google-watch-token"; const getGcalNotificationAddress = () => getGcalWebhookBaseURL() + GCAL_NOTIFICATION_ENDPOINT; diff --git a/packages/backend/src/sync/util/sync.util.test.ts b/packages/backend/src/common/util/concurrency-limiter.util.test.ts similarity index 90% rename from packages/backend/src/sync/util/sync.util.test.ts rename to packages/backend/src/common/util/concurrency-limiter.util.test.ts index 355d14840..8048c7a60 100644 --- a/packages/backend/src/sync/util/sync.util.test.ts +++ b/packages/backend/src/common/util/concurrency-limiter.util.test.ts @@ -1,12 +1,12 @@ -import { createConcurrencyLimiter } from "@backend/sync/util/sync.util"; +import { createConcurrencyLimiter } from "@backend/common/util/concurrency-limiter.util"; const waitForLimiterQueue = async () => { await Promise.resolve(); await Promise.resolve(); }; -describe("sync.util", () => { - describe("createConcurrencyLimiter", () => { +describe("createConcurrencyLimiter", () => { + describe("queueing", () => { it("limits concurrent task execution", async () => { const limit = createConcurrencyLimiter(2); let activeCount = 0; diff --git a/packages/backend/src/common/util/concurrency-limiter.util.ts b/packages/backend/src/common/util/concurrency-limiter.util.ts new file mode 100644 index 000000000..841446e10 --- /dev/null +++ b/packages/backend/src/common/util/concurrency-limiter.util.ts @@ -0,0 +1,37 @@ +type ConcurrencyLimiter = ( + task: () => PromiseLike | Result, +) => Promise; + +export const createConcurrencyLimiter = ( + concurrency: number, +): ConcurrencyLimiter => { + if (!Number.isInteger(concurrency) || concurrency < 1) { + throw new RangeError("Concurrency must be an integer greater than 0"); + } + + let activeCount = 0; + const queue: Array<() => void> = []; + + const runNext = () => { + activeCount -= 1; + queue.shift()?.(); + }; + + return async ( + task: () => PromiseLike | Result, + ): Promise => { + if (activeCount >= concurrency) { + await new Promise((resolve) => { + queue.push(resolve); + }); + } + + activeCount += 1; + + try { + return await task(); + } finally { + runNext(); + } + }; +}; diff --git a/packages/backend/src/event/classes/gcal.event.parser.ts b/packages/backend/src/event/classes/gcal.event.parser.ts index 99ca08ff4..cdb6cde29 100644 --- a/packages/backend/src/event/classes/gcal.event.parser.ts +++ b/packages/backend/src/event/classes/gcal.event.parser.ts @@ -31,7 +31,7 @@ import { mongoDateAggregation, stripReadonlyEventProps, } from "@backend/event/services/recur/util/recur.util"; -import { createSyncImport } from "@backend/sync/services/import/sync.import"; +import { createSyncImport } from "@backend/sync/services/import/google-import.service"; import { type Event_Transition, type Operation_Sync, diff --git a/packages/backend/src/event/controllers/event.controller.ts b/packages/backend/src/event/controllers/event.controller.ts index 778eb6db7..00a25381d 100644 --- a/packages/backend/src/event/controllers/event.controller.ts +++ b/packages/backend/src/event/controllers/event.controller.ts @@ -17,7 +17,7 @@ import { type SReqBody, } from "@backend/common/types/express.types"; import eventService from "@backend/event/services/event.service"; -import { CompassSyncProcessor } from "@backend/sync/services/sync/compass/compass.sync.processor"; +import { CompassToGoogleEventPropagation } from "@backend/sync/services/event-propagation/compass-to-google/compass-to-google.event-propagation"; /** * Event controller for CRUD operations on Compass events. @@ -39,7 +39,7 @@ class EventController { }), })) as CompassEvent[]; - await CompassSyncProcessor.processEvents(events); + await CompassToGoogleEventPropagation.processEvents(events); } create = ( diff --git a/packages/backend/src/event/services/event.find.test.ts b/packages/backend/src/event/services/event.find.test.ts index 60cbcd129..60419e694 100644 --- a/packages/backend/src/event/services/event.find.test.ts +++ b/packages/backend/src/event/services/event.find.test.ts @@ -23,7 +23,7 @@ import { import mongoService from "@backend/common/services/mongo.service"; import { testCompassSeries } from "@backend/event/classes/compass.event.parser.test.util"; import { getReadAllFilter } from "@backend/event/services/event.service.util"; -import { CompassSyncProcessor } from "@backend/sync/services/sync/compass/compass.sync.processor"; +import { CompassToGoogleEventPropagation } from "@backend/sync/services/event-propagation/compass-to-google/compass-to-google.event-propagation"; describe("Jan 2022: Many Formats", () => { const gBase = mockRecurringGcalBaseEvent({}, false, { count: 10 }); @@ -370,7 +370,7 @@ describe("Jan 2022: Many Formats", () => { const recurrence = { rule: ["RRULE:FREQ=WEEKLY;COUNT=10"] }; const payload = createMockBaseEvent({ isSomeday, user, recurrence }); - const changes = await CompassSyncProcessor.processEvents([ + const changes = await CompassToGoogleEventPropagation.processEvents([ { payload: payload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, diff --git a/packages/backend/src/event/services/event.service.ts b/packages/backend/src/event/services/event.service.ts index 1e2c454b0..ae9310ac4 100644 --- a/packages/backend/src/event/services/event.service.ts +++ b/packages/backend/src/event/services/event.service.ts @@ -37,7 +37,7 @@ import gcalService from "@backend/common/services/gcal/gcal.service"; import mongoService from "@backend/common/services/mongo.service"; import { reorderEvents } from "@backend/event/queries/event.queries"; import { getReadAllFilter } from "@backend/event/services/event.service.util"; -import { getGcalClient } from "@backend/sync/services/google-calendar-sync/google.calendar.client"; +import { getGcalClient } from "@backend/sync/services/google-sync/gcal.client"; class EventService { /* diff --git a/packages/backend/src/sync/controllers/sync.controller.test.ts b/packages/backend/src/sync/controllers/sync.controller.test.ts index 53a8a09ec..703612488 100644 --- a/packages/backend/src/sync/controllers/sync.controller.test.ts +++ b/packages/backend/src/sync/controllers/sync.controller.test.ts @@ -12,8 +12,8 @@ import { Resource_Sync, XGoogleResourceState } from "@core/types/sync.types"; import { type Schema_User } from "@core/types/user.types"; import { isBase, isInstance } from "@core/util/event/event.util"; import { BaseDriver } from "@backend/__tests__/drivers/base.driver"; +import { GoogleSyncDriver } from "@backend/__tests__/drivers/google-sync.driver"; import { SyncControllerDriver } from "@backend/__tests__/drivers/sync.controller.driver"; -import { SyncDriver } from "@backend/__tests__/drivers/sync.driver"; import { UserDriver } from "@backend/__tests__/drivers/user.driver"; import { UtilDriver } from "@backend/__tests__/drivers/util.driver"; import { @@ -30,11 +30,11 @@ import { missingRefreshTokenError } from "@backend/__tests__/mocks.gcal/errors/e import gcalService from "@backend/common/services/gcal/gcal.service"; import mongoService from "@backend/common/services/mongo.service"; import { sseServer } from "@backend/servers/sse/sse.server"; -import { googleCalendarSyncService } from "@backend/sync/services/google-calendar-sync/google-calendar-sync.service"; +import { googleCalendarSyncService } from "@backend/sync/services/google-sync/google-sync.service"; import { GCalNotificationHandler } from "@backend/sync/services/notify/handler/gcal.notification.handler"; +import * as syncQueries from "@backend/sync/services/records/sync-records.repository"; +import { updateSync } from "@backend/sync/services/records/sync-records.repository"; import { googleWatchService } from "@backend/sync/services/watch/google-watch.service"; -import * as syncQueries from "@backend/sync/util/sync.queries"; -import { updateSync } from "@backend/sync/util/sync.queries"; import userService from "@backend/user/services/user.service"; import userMetadataService from "@backend/user/services/user-metadata.service"; import { randomUUID } from "node:crypto"; @@ -669,7 +669,7 @@ describe("SyncController", () => { const user = await UserDriver.createUser(); const userId = user._id.toString(); - await SyncDriver.createSync(user); + await GoogleSyncDriver.createHealthyGoogleSync(user); await userMetadataService.updateUserMetadata({ userId, @@ -710,7 +710,7 @@ describe("SyncController", () => { const user = await UserDriver.createUser(); const userId = user._id.toString(); - await SyncDriver.createSync(user); + await GoogleSyncDriver.createHealthyGoogleSync(user); await userMetadataService.updateUserMetadata({ userId, @@ -751,7 +751,7 @@ describe("SyncController", () => { const user = await UserDriver.createUser(); const userId = user._id.toString(); - await SyncDriver.createSync(user); + await GoogleSyncDriver.createHealthyGoogleSync(user); await userMetadataService.updateUserMetadata({ userId, @@ -789,7 +789,7 @@ describe("SyncController", () => { const user = await UserDriver.createUser(); const userId = user._id.toString(); - await SyncDriver.createSync(user); + await GoogleSyncDriver.createHealthyGoogleSync(user); const importStartSpy = jest.spyOn(sseServer, "handleImportGCalStart"); @@ -807,7 +807,7 @@ describe("SyncController", () => { const user = await UserDriver.createUser(); const userId = user._id.toString(); - await SyncDriver.createSync(user); + await GoogleSyncDriver.createHealthyGoogleSync(user); const stream = baseDriver.openSSEStream({ userId, @@ -831,7 +831,7 @@ describe("SyncController", () => { const user = await UserDriver.createUser(); const userId = user._id.toString(); - await SyncDriver.createSync(user); + await GoogleSyncDriver.createHealthyGoogleSync(user); const backgroundChangeSpy = jest.spyOn( sseServer, diff --git a/packages/backend/src/sync/controllers/sync.controller.ts b/packages/backend/src/sync/controllers/sync.controller.ts index 1d107df65..277dcbb79 100644 --- a/packages/backend/src/sync/controllers/sync.controller.ts +++ b/packages/backend/src/sync/controllers/sync.controller.ts @@ -1,15 +1,8 @@ import { type NextFunction, type Request, type Response } from "express"; import { ObjectId } from "mongodb"; -import { ZodError } from "zod/v4"; -import { COMPASS_RESOURCE_HEADER } from "@core/constants/core.constants"; import { GOOGLE_REVOKED } from "@core/constants/sse.constants"; import { Status } from "@core/errors/status.codes"; import { Logger } from "@core/logger/winston.logger"; -import { - GcalNotificationSchema, - type Payload_Sync_Notif, - type Resource_Sync, -} from "@core/types/sync.types"; import { error } from "@backend/common/errors/handlers/error.handler"; import { SyncError } from "@backend/common/errors/sync/sync.errors"; import { WatchError } from "@backend/common/errors/sync/watch.errors"; @@ -20,11 +13,12 @@ import { } from "@backend/common/services/gcal/gcal.utils"; import mongoService from "@backend/common/services/mongo.service"; import { sseServer } from "@backend/servers/sse/sse.server"; -import { googleCalendarSyncService } from "@backend/sync/services/google-calendar-sync/google-calendar-sync.service"; +import { isMissingGoogleRefreshToken } from "@backend/sync/services/google-sync/google-sync.errors"; +import { googleCalendarSyncService } from "@backend/sync/services/google-sync/google-sync.service"; +import { publicWatchNotificationIngress } from "@backend/sync/services/public-watch-notifications/public-watch-notification.ingress"; +import { getSync } from "@backend/sync/services/records/sync-records.repository"; import { googleWatchService } from "@backend/sync/services/watch/google-watch.service"; import { googleWatchMaintenanceService } from "@backend/sync/services/watch/google-watch-maintenance.service"; -import { getSync } from "@backend/sync/util/sync.queries"; -import { isMissingGoogleRefreshToken } from "@backend/sync/util/sync.util"; import userService from "@backend/user/services/user.service"; import userMetadataService from "@backend/user/services/user-metadata.service"; import { ImportGCalRequestSchema } from "../sync.types"; @@ -179,39 +173,18 @@ export class SyncController { res: Response, next: NextFunction, ) => { - const resource = res.getHeader(COMPASS_RESOURCE_HEADER) as Exclude< - Resource_Sync, - Resource_Sync.SETTINGS - >; - const channelId = req.headers["x-goog-channel-id"] as string; const resourceId = req.headers["x-goog-resource-id"] as string; - res.removeHeader(COMPASS_RESOURCE_HEADER); - try { - const syncPayload: Payload_Sync_Notif = GcalNotificationSchema.parse({ - resource, - channelId, - resourceId, - resourceState: req.headers["x-goog-resource-state"] as string, - expiration: new Date( - req.headers["x-goog-channel-expiration"] as string, - ), - }); - - const response = - await googleWatchService.handleGoogleWatchNotification(syncPayload); + const response = await googleWatchService.handleGoogleWatchNotification( + publicWatchNotificationIngress.getNotification(res), + ); res.promise(response); } catch (e) { logger.error(e); - if (e instanceof ZodError) { - res.status(Status.FORBIDDEN).send("Invalid notification payload"); - return; - } - if (isMissingGoogleRefreshToken(e)) { await SyncController.handleMissingRefreshToken( res, diff --git a/packages/backend/src/sync/controllers/sync.debug.controller.ts b/packages/backend/src/sync/controllers/sync.debug.controller.ts index 6b0e051f2..d72e3a24d 100644 --- a/packages/backend/src/sync/controllers/sync.debug.controller.ts +++ b/packages/backend/src/sync/controllers/sync.debug.controller.ts @@ -6,11 +6,11 @@ import { type SReqBody, } from "@backend/common/types/express.types"; import { sseServer } from "@backend/servers/sse/sse.server"; -import { getGcalClient } from "@backend/sync/services/google-calendar-sync/google.calendar.client"; -import { googleCalendarSyncService } from "@backend/sync/services/google-calendar-sync/google-calendar-sync.service"; +import { getGcalClient } from "@backend/sync/services/google-sync/gcal.client"; +import { googleCalendarSyncService } from "@backend/sync/services/google-sync/google-sync.service"; +import { getSync } from "@backend/sync/services/records/sync-records.repository"; import { googleWatchService } from "@backend/sync/services/watch/google-watch.service"; import { googleWatchMaintenanceService } from "@backend/sync/services/watch/google-watch-maintenance.service"; -import { getSync } from "@backend/sync/util/sync.queries"; class SyncDebugController { dispatchEventToClient = (_req: Request, res: Response) => { diff --git a/packages/backend/src/sync/services/sync/__tests__/compass-sync-processor-this-event/base.test.ts b/packages/backend/src/sync/services/event-propagation/__tests__/compass-to-google-this-event/base.test.ts similarity index 82% rename from packages/backend/src/sync/services/sync/__tests__/compass-sync-processor-this-event/base.test.ts rename to packages/backend/src/sync/services/event-propagation/__tests__/compass-to-google-this-event/base.test.ts index 153066dc0..e84a54641 100644 --- a/packages/backend/src/sync/services/sync/__tests__/compass-sync-processor-this-event/base.test.ts +++ b/packages/backend/src/sync/services/event-propagation/__tests__/compass-to-google-this-event/base.test.ts @@ -23,11 +23,11 @@ import { testCompassSeries, testCompassSeriesInGcal, } from "@backend/event/classes/compass.event.parser.test.util"; -import { CompassSyncProcessor } from "@backend/sync/services/sync/compass/compass.sync.processor"; +import { CompassToGoogleEventPropagation } from "@backend/sync/services/event-propagation/compass-to-google/compass-to-google.event-propagation"; describe.each([ { calendarProvider: CalendarProvider.GOOGLE }, -])(`CompassSyncProcessor - $calendarProvider calendar: ${RecurringEventUpdateScope.THIS_EVENT} - Base Event: `, ({ +])(`CompassToGoogleEventPropagation - $calendarProvider calendar: ${RecurringEventUpdateScope.THIS_EVENT} - Base Event: `, ({ calendarProvider, }) => { beforeAll(setupTestDb); @@ -44,7 +44,7 @@ describe.each([ const recurrence = { rule: ["RRULE:FREQ=WEEKLY;COUNT=10"] }; const payload = createMockBaseEvent({ isSomeday, user, recurrence }); - const changes = await CompassSyncProcessor.processEvents([ + const changes = await CompassToGoogleEventPropagation.processEvents([ { payload: payload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -82,7 +82,7 @@ describe.each([ const recurrence = { rule: ["RRULE:FREQ=WEEKLY;COUNT=10"] }; const payload = createMockBaseEvent({ isSomeday, user, recurrence }); - const changes = await CompassSyncProcessor.processEvents([ + const changes = await CompassToGoogleEventPropagation.processEvents([ { payload: payload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -135,13 +135,15 @@ describe.each([ recurrence, }); - const changes = await CompassSyncProcessor.processEvents([ - { - payload: payload as CompassThisEvent["payload"], - applyTo: RecurringEventUpdateScope.THIS_EVENT, - status: CompassEventStatus.CONFIRMED, - }, - ]); + const changes = await CompassToGoogleEventPropagation.processEvents( + [ + { + payload: payload as CompassThisEvent["payload"], + applyTo: RecurringEventUpdateScope.THIS_EVENT, + status: CompassEventStatus.CONFIRMED, + }, + ], + ); expect(changes).toEqual( expect.arrayContaining([ @@ -163,7 +165,7 @@ describe.each([ } as WithCompassId; await expect( - CompassSyncProcessor.processEvents([ + CompassToGoogleEventPropagation.processEvents([ { payload: updatedPayload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -187,13 +189,15 @@ describe.each([ recurrence, }); - const changes = await CompassSyncProcessor.processEvents([ - { - payload: payload as CompassThisEvent["payload"], - applyTo: RecurringEventUpdateScope.THIS_EVENT, - status: CompassEventStatus.CONFIRMED, - }, - ]); + const changes = await CompassToGoogleEventPropagation.processEvents( + [ + { + payload: payload as CompassThisEvent["payload"], + applyTo: RecurringEventUpdateScope.THIS_EVENT, + status: CompassEventStatus.CONFIRMED, + }, + ], + ); expect(changes).toEqual( expect.arrayContaining([ @@ -215,7 +219,7 @@ describe.each([ } as WithCompassId; await expect( - CompassSyncProcessor.processEvents([ + CompassToGoogleEventPropagation.processEvents([ { payload: updatedPayload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -241,13 +245,15 @@ describe.each([ priority: Priorities.SELF, }); - const changes = await CompassSyncProcessor.processEvents([ - { - payload: payload as CompassThisEvent["payload"], - applyTo: RecurringEventUpdateScope.THIS_EVENT, - status: CompassEventStatus.CONFIRMED, - }, - ]); + const changes = await CompassToGoogleEventPropagation.processEvents( + [ + { + payload: payload as CompassThisEvent["payload"], + applyTo: RecurringEventUpdateScope.THIS_EVENT, + status: CompassEventStatus.CONFIRMED, + }, + ], + ); expect(changes).toEqual( expect.arrayContaining([ @@ -269,7 +275,7 @@ describe.each([ } as WithCompassId; await expect( - CompassSyncProcessor.processEvents([ + CompassToGoogleEventPropagation.processEvents([ { payload: updatedPayload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -293,13 +299,15 @@ describe.each([ recurrence, }); - const changes = await CompassSyncProcessor.processEvents([ - { - payload: payload as CompassThisEvent["payload"], - applyTo: RecurringEventUpdateScope.THIS_EVENT, - status: CompassEventStatus.CONFIRMED, - }, - ]); + const changes = await CompassToGoogleEventPropagation.processEvents( + [ + { + payload: payload as CompassThisEvent["payload"], + applyTo: RecurringEventUpdateScope.THIS_EVENT, + status: CompassEventStatus.CONFIRMED, + }, + ], + ); expect(changes).toEqual( expect.arrayContaining([ @@ -323,7 +331,7 @@ describe.each([ } as WithCompassId; await expect( - CompassSyncProcessor.processEvents([ + CompassToGoogleEventPropagation.processEvents([ { payload: updatedPayload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -347,13 +355,15 @@ describe.each([ recurrence, }); - const changes = await CompassSyncProcessor.processEvents([ - { - payload: payload as CompassThisEvent["payload"], - applyTo: RecurringEventUpdateScope.THIS_EVENT, - status: CompassEventStatus.CONFIRMED, - }, - ]); + const changes = await CompassToGoogleEventPropagation.processEvents( + [ + { + payload: payload as CompassThisEvent["payload"], + applyTo: RecurringEventUpdateScope.THIS_EVENT, + status: CompassEventStatus.CONFIRMED, + }, + ], + ); expect(changes).toEqual( expect.arrayContaining([ @@ -377,7 +387,7 @@ describe.each([ } as WithCompassId; await expect( - CompassSyncProcessor.processEvents([ + CompassToGoogleEventPropagation.processEvents([ { payload: updatedPayload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -403,13 +413,15 @@ describe.each([ priority: Priorities.SELF, }); - const changes = await CompassSyncProcessor.processEvents([ - { - payload: payload as CompassThisEvent["payload"], - applyTo: RecurringEventUpdateScope.THIS_EVENT, - status: CompassEventStatus.CONFIRMED, - }, - ]); + const changes = await CompassToGoogleEventPropagation.processEvents( + [ + { + payload: payload as CompassThisEvent["payload"], + applyTo: RecurringEventUpdateScope.THIS_EVENT, + status: CompassEventStatus.CONFIRMED, + }, + ], + ); expect(changes).toEqual( expect.arrayContaining([ @@ -431,7 +443,7 @@ describe.each([ }; await expect( - CompassSyncProcessor.processEvents([ + CompassToGoogleEventPropagation.processEvents([ { payload: updatedPayload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -460,13 +472,15 @@ describe.each([ recurrence, }); - const changes = await CompassSyncProcessor.processEvents([ - { - payload: payload as CompassThisEvent["payload"], - applyTo: RecurringEventUpdateScope.THIS_EVENT, - status: CompassEventStatus.CONFIRMED, - }, - ]); + const changes = await CompassToGoogleEventPropagation.processEvents( + [ + { + payload: payload as CompassThisEvent["payload"], + applyTo: RecurringEventUpdateScope.THIS_EVENT, + status: CompassEventStatus.CONFIRMED, + }, + ], + ); expect(changes).toEqual( expect.arrayContaining([ @@ -487,7 +501,7 @@ describe.each([ } as WithCompassId; await expect( - CompassSyncProcessor.processEvents([ + CompassToGoogleEventPropagation.processEvents([ { payload: deletePayload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, diff --git a/packages/backend/src/sync/services/sync/__tests__/compass-sync-processor-this-event/instance.test.ts b/packages/backend/src/sync/services/event-propagation/__tests__/compass-to-google-this-event/instance.test.ts similarity index 94% rename from packages/backend/src/sync/services/sync/__tests__/compass-sync-processor-this-event/instance.test.ts rename to packages/backend/src/sync/services/event-propagation/__tests__/compass-to-google-this-event/instance.test.ts index 41220cc95..069517e78 100644 --- a/packages/backend/src/sync/services/sync/__tests__/compass-sync-processor-this-event/instance.test.ts +++ b/packages/backend/src/sync/services/event-propagation/__tests__/compass-to-google-this-event/instance.test.ts @@ -25,11 +25,11 @@ import { testCompassSeriesInGcal, } from "@backend/event/classes/compass.event.parser.test.util"; import eventService, { _getGcal } from "@backend/event/services/event.service"; -import { CompassSyncProcessor } from "@backend/sync/services/sync/compass/compass.sync.processor"; +import { CompassToGoogleEventPropagation } from "@backend/sync/services/event-propagation/compass-to-google/compass-to-google.event-propagation"; describe.each([ { calendarProvider: CalendarProvider.GOOGLE }, -])(`CompassSyncProcessor - $calendarProvider calendar: ${RecurringEventUpdateScope.THIS_EVENT} - Instance Event: `, ({ +])(`CompassToGoogleEventPropagation - $calendarProvider calendar: ${RecurringEventUpdateScope.THIS_EVENT} - Instance Event: `, ({ calendarProvider, }) => { beforeAll(setupTestDb); @@ -52,7 +52,7 @@ describe.each([ recurrence, }); - const changes = await CompassSyncProcessor.processEvents([ + const changes = await CompassToGoogleEventPropagation.processEvents([ { payload: payload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -93,13 +93,14 @@ describe.each([ title: faker.lorem.sentence(3), }; - const updateChanges = await CompassSyncProcessor.processEvents([ - { - payload: updatedPayload as CompassThisEvent["payload"], - applyTo: RecurringEventUpdateScope.THIS_EVENT, - status: CompassEventStatus.CONFIRMED, - }, - ]); + const updateChanges = + await CompassToGoogleEventPropagation.processEvents([ + { + payload: updatedPayload as CompassThisEvent["payload"], + applyTo: RecurringEventUpdateScope.THIS_EVENT, + status: CompassEventStatus.CONFIRMED, + }, + ]); expect(updateChanges).toEqual( expect.arrayContaining([ @@ -192,7 +193,7 @@ describe.each([ recurrence, }); - const changes = await CompassSyncProcessor.processEvents([ + const changes = await CompassToGoogleEventPropagation.processEvents([ { payload: payload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -300,13 +301,14 @@ describe.each([ description: faker.lorem.sentence(3), }; - const updateChanges = await CompassSyncProcessor.processEvents([ - { - payload: updatedPayload as CompassThisEvent["payload"], - applyTo: RecurringEventUpdateScope.THIS_EVENT, - status: CompassEventStatus.CONFIRMED, - }, - ]); + const updateChanges = + await CompassToGoogleEventPropagation.processEvents([ + { + payload: updatedPayload as CompassThisEvent["payload"], + applyTo: RecurringEventUpdateScope.THIS_EVENT, + status: CompassEventStatus.CONFIRMED, + }, + ]); expect(updateChanges).toEqual( expect.arrayContaining([ @@ -429,7 +431,7 @@ describe.each([ priority: Priorities.RELATIONS, }); - const changes = await CompassSyncProcessor.processEvents([ + const changes = await CompassToGoogleEventPropagation.processEvents([ { payload: payload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -537,13 +539,14 @@ describe.each([ priority: Priorities.WORK, }; - const updateChanges = await CompassSyncProcessor.processEvents([ - { - payload: updatedPayload as CompassThisEvent["payload"], - applyTo: RecurringEventUpdateScope.THIS_EVENT, - status: CompassEventStatus.CONFIRMED, - }, - ]); + const updateChanges = + await CompassToGoogleEventPropagation.processEvents([ + { + payload: updatedPayload as CompassThisEvent["payload"], + applyTo: RecurringEventUpdateScope.THIS_EVENT, + status: CompassEventStatus.CONFIRMED, + }, + ]); expect(updateChanges).toEqual( expect.arrayContaining([ @@ -676,7 +679,7 @@ describe.each([ recurrence, }); - const changes = await CompassSyncProcessor.processEvents([ + const changes = await CompassToGoogleEventPropagation.processEvents([ { payload: payload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -786,13 +789,14 @@ describe.each([ .toISOString(), }; - const updateChanges = await CompassSyncProcessor.processEvents([ - { - payload: updatedPayload as CompassThisEvent["payload"], - applyTo: RecurringEventUpdateScope.THIS_EVENT, - status: CompassEventStatus.CONFIRMED, - }, - ]); + const updateChanges = + await CompassToGoogleEventPropagation.processEvents([ + { + payload: updatedPayload as CompassThisEvent["payload"], + applyTo: RecurringEventUpdateScope.THIS_EVENT, + status: CompassEventStatus.CONFIRMED, + }, + ]); expect(updateChanges).toEqual( expect.arrayContaining([ @@ -919,7 +923,7 @@ describe.each([ recurrence, }); - const changes = await CompassSyncProcessor.processEvents([ + const changes = await CompassToGoogleEventPropagation.processEvents([ { payload: payload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -1029,13 +1033,14 @@ describe.each([ .toISOString(), }; - const updateChanges = await CompassSyncProcessor.processEvents([ - { - payload: updatedPayload as CompassThisEvent["payload"], - applyTo: RecurringEventUpdateScope.THIS_EVENT, - status: CompassEventStatus.CONFIRMED, - }, - ]); + const updateChanges = + await CompassToGoogleEventPropagation.processEvents([ + { + payload: updatedPayload as CompassThisEvent["payload"], + applyTo: RecurringEventUpdateScope.THIS_EVENT, + status: CompassEventStatus.CONFIRMED, + }, + ]); expect(updateChanges).toEqual( expect.arrayContaining([ @@ -1162,7 +1167,7 @@ describe.each([ recurrence, }); - const changes = await CompassSyncProcessor.processEvents([ + const changes = await CompassToGoogleEventPropagation.processEvents([ { payload: payload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -1269,13 +1274,14 @@ describe.each([ }, }; - const updateChanges = await CompassSyncProcessor.processEvents([ - { - payload: updatedPayload as CompassThisEvent["payload"], - applyTo: RecurringEventUpdateScope.THIS_EVENT, - status: CompassEventStatus.CONFIRMED, - }, - ]); + const updateChanges = + await CompassToGoogleEventPropagation.processEvents([ + { + payload: updatedPayload as CompassThisEvent["payload"], + applyTo: RecurringEventUpdateScope.THIS_EVENT, + status: CompassEventStatus.CONFIRMED, + }, + ]); expect(updateChanges).toEqual( expect.arrayContaining([ @@ -1380,7 +1386,7 @@ describe.each([ recurrence, }); - const changes = await CompassSyncProcessor.processEvents([ + const changes = await CompassToGoogleEventPropagation.processEvents([ { payload: payload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -1486,7 +1492,7 @@ describe.each([ isSomeday: true, }; - const updateChanges = CompassSyncProcessor.processEvents([ + const updateChanges = CompassToGoogleEventPropagation.processEvents([ { payload: updatedPayload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -1510,7 +1516,7 @@ describe.each([ const recurrence = { rule: ["RRULE:FREQ=WEEKLY;COUNT=10"] }; const payload = createMockBaseEvent({ isSomeday, user, recurrence }); - const changes = await CompassSyncProcessor.processEvents([ + const changes = await CompassToGoogleEventPropagation.processEvents([ { payload: payload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -1610,20 +1616,22 @@ describe.each([ const deletedInstanceId = deletedInstance._id.toString(); - const deleteChanges = await CompassSyncProcessor.processEvents([ - { - payload: { - ...deletedInstance, - _id: deletedInstanceId, - recurrence: { - ...deletedInstance.recurrence, - rule: event.recurrence!.rule!, - }, - } as CompassThisEvent["payload"], - applyTo: RecurringEventUpdateScope.THIS_EVENT, - status: CompassEventStatus.CANCELLED, - }, - ]); + const deleteChanges = await CompassToGoogleEventPropagation.processEvents( + [ + { + payload: { + ...deletedInstance, + _id: deletedInstanceId, + recurrence: { + ...deletedInstance.recurrence, + rule: event.recurrence!.rule!, + }, + } as CompassThisEvent["payload"], + applyTo: RecurringEventUpdateScope.THIS_EVENT, + status: CompassEventStatus.CANCELLED, + }, + ], + ); expect(deleteChanges).toEqual( expect.arrayContaining([ @@ -1703,7 +1711,7 @@ describe.each([ }); // Create the base recurring someday event - const changes = await CompassSyncProcessor.processEvents([ + const changes = await CompassToGoogleEventPropagation.processEvents([ { payload: payload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -1742,13 +1750,14 @@ describe.each([ description: newDescription, }; - const updateChanges = await CompassSyncProcessor.processEvents([ - { - payload: updatedPayload as CompassThisEvent["payload"], - applyTo: RecurringEventUpdateScope.THIS_EVENT, - status: CompassEventStatus.CONFIRMED, - }, - ]); + const updateChanges = + await CompassToGoogleEventPropagation.processEvents([ + { + payload: updatedPayload as CompassThisEvent["payload"], + applyTo: RecurringEventUpdateScope.THIS_EVENT, + status: CompassEventStatus.CONFIRMED, + }, + ]); expect(updateChanges).toEqual( expect.arrayContaining([ diff --git a/packages/backend/src/sync/services/sync/__tests__/compass-sync-processor-this-event/regular.test.ts b/packages/backend/src/sync/services/event-propagation/__tests__/compass-to-google-this-event/regular.test.ts similarity index 84% rename from packages/backend/src/sync/services/sync/__tests__/compass-sync-processor-this-event/regular.test.ts rename to packages/backend/src/sync/services/event-propagation/__tests__/compass-to-google-this-event/regular.test.ts index 2c48e08c1..9f46ef5ce 100644 --- a/packages/backend/src/sync/services/sync/__tests__/compass-sync-processor-this-event/regular.test.ts +++ b/packages/backend/src/sync/services/event-propagation/__tests__/compass-to-google-this-event/regular.test.ts @@ -24,11 +24,11 @@ import { testCompassStandaloneEvent, } from "@backend/event/classes/compass.event.parser.test.util"; import eventService, { _getGcal } from "@backend/event/services/event.service"; -import { CompassSyncProcessor } from "@backend/sync/services/sync/compass/compass.sync.processor"; +import { CompassToGoogleEventPropagation } from "@backend/sync/services/event-propagation/compass-to-google/compass-to-google.event-propagation"; describe.each([ { calendarProvider: CalendarProvider.GOOGLE }, -])(`CompassSyncProcessor - $calendarProvider calendar: ${RecurringEventUpdateScope.THIS_EVENT} - Regular Event: `, ({ +])(`CompassToGoogleEventPropagation - $calendarProvider calendar: ${RecurringEventUpdateScope.THIS_EVENT} - Regular Event: `, ({ calendarProvider, }) => { beforeAll(setupTestDb); @@ -43,7 +43,7 @@ describe.each([ const user = _user._id.toString(); const payload = createMockStandaloneEvent({ isSomeday: true, user }); - const changes = await CompassSyncProcessor.processEvents([ + const changes = await CompassToGoogleEventPropagation.processEvents([ { payload: payload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -76,7 +76,7 @@ describe.each([ const user = _user._id.toString(); const payload = createMockStandaloneEvent({ user }); - const changes = await CompassSyncProcessor.processEvents([ + const changes = await CompassToGoogleEventPropagation.processEvents([ { payload: payload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -116,7 +116,7 @@ describe.each([ user, }); - const changes = await CompassSyncProcessor.processEvents([ + const changes = await CompassToGoogleEventPropagation.processEvents([ { payload: payload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -143,13 +143,14 @@ describe.each([ title: faker.lorem.sentence(3), }; - const updateChanges = await CompassSyncProcessor.processEvents([ - { - payload: updatedPayload as CompassThisEvent["payload"], - applyTo: RecurringEventUpdateScope.THIS_EVENT, - status: CompassEventStatus.CONFIRMED, - }, - ]); + const updateChanges = + await CompassToGoogleEventPropagation.processEvents([ + { + payload: updatedPayload as CompassThisEvent["payload"], + applyTo: RecurringEventUpdateScope.THIS_EVENT, + status: CompassEventStatus.CONFIRMED, + }, + ]); expect(updateChanges).toEqual( expect.arrayContaining([ @@ -183,7 +184,7 @@ describe.each([ user, }); - const changes = await CompassSyncProcessor.processEvents([ + const changes = await CompassToGoogleEventPropagation.processEvents([ { payload: payload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -210,13 +211,14 @@ describe.each([ description: faker.lorem.sentence(3), }; - const updateChanges = await CompassSyncProcessor.processEvents([ - { - payload: updatedPayload as CompassThisEvent["payload"], - applyTo: RecurringEventUpdateScope.THIS_EVENT, - status: CompassEventStatus.CONFIRMED, - }, - ]); + const updateChanges = + await CompassToGoogleEventPropagation.processEvents([ + { + payload: updatedPayload as CompassThisEvent["payload"], + applyTo: RecurringEventUpdateScope.THIS_EVENT, + status: CompassEventStatus.CONFIRMED, + }, + ]); expect(updateChanges).toEqual( expect.arrayContaining([ @@ -250,7 +252,7 @@ describe.each([ user, }); - const changes = await CompassSyncProcessor.processEvents([ + const changes = await CompassToGoogleEventPropagation.processEvents([ { payload: payload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -277,13 +279,14 @@ describe.each([ priority: Priorities.RELATIONS, }; - const updateChanges = await CompassSyncProcessor.processEvents([ - { - payload: updatedPayload as CompassThisEvent["payload"], - applyTo: RecurringEventUpdateScope.THIS_EVENT, - status: CompassEventStatus.CONFIRMED, - }, - ]); + const updateChanges = + await CompassToGoogleEventPropagation.processEvents([ + { + payload: updatedPayload as CompassThisEvent["payload"], + applyTo: RecurringEventUpdateScope.THIS_EVENT, + status: CompassEventStatus.CONFIRMED, + }, + ]); expect(updateChanges).toEqual( expect.arrayContaining([ @@ -317,7 +320,7 @@ describe.each([ user, }); - const changes = await CompassSyncProcessor.processEvents([ + const changes = await CompassToGoogleEventPropagation.processEvents([ { payload: payload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -346,13 +349,14 @@ describe.each([ .toISOString(), }; - const updateChanges = await CompassSyncProcessor.processEvents([ - { - payload: updatedPayload as CompassThisEvent["payload"], - applyTo: RecurringEventUpdateScope.THIS_EVENT, - status: CompassEventStatus.CONFIRMED, - }, - ]); + const updateChanges = + await CompassToGoogleEventPropagation.processEvents([ + { + payload: updatedPayload as CompassThisEvent["payload"], + applyTo: RecurringEventUpdateScope.THIS_EVENT, + status: CompassEventStatus.CONFIRMED, + }, + ]); expect(updateChanges).toEqual( expect.arrayContaining([ @@ -386,7 +390,7 @@ describe.each([ user, }); - const changes = await CompassSyncProcessor.processEvents([ + const changes = await CompassToGoogleEventPropagation.processEvents([ { payload: payload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -416,13 +420,14 @@ describe.each([ .toISOString(), }; - const updateChanges = await CompassSyncProcessor.processEvents([ - { - payload: updatedPayload as CompassThisEvent["payload"], - applyTo: RecurringEventUpdateScope.THIS_EVENT, - status: CompassEventStatus.CONFIRMED, - }, - ]); + const updateChanges = + await CompassToGoogleEventPropagation.processEvents([ + { + payload: updatedPayload as CompassThisEvent["payload"], + applyTo: RecurringEventUpdateScope.THIS_EVENT, + status: CompassEventStatus.CONFIRMED, + }, + ]); expect(updateChanges).toEqual( expect.arrayContaining([ @@ -458,7 +463,7 @@ describe.each([ user, }); - const changes = await CompassSyncProcessor.processEvents([ + const changes = await CompassToGoogleEventPropagation.processEvents([ { payload: payload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -491,13 +496,14 @@ describe.each([ recurrence: { rule: ["RRULE:FREQ=WEEKLY;COUNT=20"] }, }; - const updateChanges = await CompassSyncProcessor.processEvents([ - { - payload: updatedPayload as CompassThisEvent["payload"], - applyTo: RecurringEventUpdateScope.THIS_EVENT, - status: CompassEventStatus.CONFIRMED, - }, - ]); + const updateChanges = + await CompassToGoogleEventPropagation.processEvents([ + { + payload: updatedPayload as CompassThisEvent["payload"], + applyTo: RecurringEventUpdateScope.THIS_EVENT, + status: CompassEventStatus.CONFIRMED, + }, + ]); expect(updateChanges).toEqual( expect.arrayContaining([ @@ -530,7 +536,7 @@ describe.each([ user, }); - const changes = await CompassSyncProcessor.processEvents([ + const changes = await CompassToGoogleEventPropagation.processEvents([ { payload: payload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -564,13 +570,14 @@ describe.each([ isSomeday: false, }; - const updateChanges = await CompassSyncProcessor.processEvents([ - { - payload: updatedPayload as CompassThisEvent["payload"], - applyTo: RecurringEventUpdateScope.THIS_EVENT, - status: CompassEventStatus.CONFIRMED, - }, - ]); + const updateChanges = + await CompassToGoogleEventPropagation.processEvents([ + { + payload: updatedPayload as CompassThisEvent["payload"], + applyTo: RecurringEventUpdateScope.THIS_EVENT, + status: CompassEventStatus.CONFIRMED, + }, + ]); expect(updateChanges).toEqual( expect.arrayContaining([ @@ -605,7 +612,7 @@ describe.each([ user, }); - const changes = await CompassSyncProcessor.processEvents([ + const changes = await CompassToGoogleEventPropagation.processEvents([ { payload: payload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -642,13 +649,14 @@ describe.each([ title: faker.lorem.sentence(3), }; - const updateChanges = await CompassSyncProcessor.processEvents([ - { - payload: updatedPayload as CompassThisEvent["payload"], - applyTo: RecurringEventUpdateScope.THIS_EVENT, - status: CompassEventStatus.CONFIRMED, - }, - ]); + const updateChanges = + await CompassToGoogleEventPropagation.processEvents([ + { + payload: updatedPayload as CompassThisEvent["payload"], + applyTo: RecurringEventUpdateScope.THIS_EVENT, + status: CompassEventStatus.CONFIRMED, + }, + ]); expect(updateChanges).toEqual( expect.arrayContaining([ @@ -685,7 +693,7 @@ describe.each([ user, }); - const changes = await CompassSyncProcessor.processEvents([ + const changes = await CompassToGoogleEventPropagation.processEvents([ { payload: payload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -722,13 +730,14 @@ describe.each([ description: faker.lorem.sentence(3), }; - const updateChanges = await CompassSyncProcessor.processEvents([ - { - payload: updatedPayload as CompassThisEvent["payload"], - applyTo: RecurringEventUpdateScope.THIS_EVENT, - status: CompassEventStatus.CONFIRMED, - }, - ]); + const updateChanges = + await CompassToGoogleEventPropagation.processEvents([ + { + payload: updatedPayload as CompassThisEvent["payload"], + applyTo: RecurringEventUpdateScope.THIS_EVENT, + status: CompassEventStatus.CONFIRMED, + }, + ]); expect(updateChanges).toEqual( expect.arrayContaining([ @@ -763,7 +772,7 @@ describe.each([ user, }); - const changes = await CompassSyncProcessor.processEvents([ + const changes = await CompassToGoogleEventPropagation.processEvents([ { payload: payload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -803,13 +812,14 @@ describe.each([ priority: Priorities.RELATIONS, }; - const updateChanges = await CompassSyncProcessor.processEvents([ - { - payload: updatedPayload as CompassThisEvent["payload"], - applyTo: RecurringEventUpdateScope.THIS_EVENT, - status: CompassEventStatus.CONFIRMED, - }, - ]); + const updateChanges = + await CompassToGoogleEventPropagation.processEvents([ + { + payload: updatedPayload as CompassThisEvent["payload"], + applyTo: RecurringEventUpdateScope.THIS_EVENT, + status: CompassEventStatus.CONFIRMED, + }, + ]); expect(updateChanges).toEqual( expect.arrayContaining([ @@ -846,7 +856,7 @@ describe.each([ user, }); - const changes = await CompassSyncProcessor.processEvents([ + const changes = await CompassToGoogleEventPropagation.processEvents([ { payload: payload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -887,13 +897,14 @@ describe.each([ .toISOString(), }; - const updateChanges = await CompassSyncProcessor.processEvents([ - { - payload: updatedPayload as CompassThisEvent["payload"], - applyTo: RecurringEventUpdateScope.THIS_EVENT, - status: CompassEventStatus.CONFIRMED, - }, - ]); + const updateChanges = + await CompassToGoogleEventPropagation.processEvents([ + { + payload: updatedPayload as CompassThisEvent["payload"], + applyTo: RecurringEventUpdateScope.THIS_EVENT, + status: CompassEventStatus.CONFIRMED, + }, + ]); expect(updateChanges).toEqual( expect.arrayContaining([ @@ -929,7 +940,7 @@ describe.each([ user, }); - const changes = await CompassSyncProcessor.processEvents([ + const changes = await CompassToGoogleEventPropagation.processEvents([ { payload: payload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -971,13 +982,14 @@ describe.each([ .toISOString(), }; - const updateChanges = await CompassSyncProcessor.processEvents([ - { - payload: updatedPayload as CompassThisEvent["payload"], - applyTo: RecurringEventUpdateScope.THIS_EVENT, - status: CompassEventStatus.CONFIRMED, - }, - ]); + const updateChanges = + await CompassToGoogleEventPropagation.processEvents([ + { + payload: updatedPayload as CompassThisEvent["payload"], + applyTo: RecurringEventUpdateScope.THIS_EVENT, + status: CompassEventStatus.CONFIRMED, + }, + ]); expect(updateChanges).toEqual( expect.arrayContaining([ @@ -1015,7 +1027,7 @@ describe.each([ user, }); - const changes = await CompassSyncProcessor.processEvents([ + const changes = await CompassToGoogleEventPropagation.processEvents([ { payload: payload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -1054,13 +1066,14 @@ describe.each([ isSomeday: true, }; - const updateChanges = await CompassSyncProcessor.processEvents([ - { - payload: updatedPayload as CompassThisEvent["payload"], - applyTo: RecurringEventUpdateScope.THIS_EVENT, - status: CompassEventStatus.CONFIRMED, - }, - ]); + const updateChanges = + await CompassToGoogleEventPropagation.processEvents([ + { + payload: updatedPayload as CompassThisEvent["payload"], + applyTo: RecurringEventUpdateScope.THIS_EVENT, + status: CompassEventStatus.CONFIRMED, + }, + ]); expect(updateChanges).toEqual( expect.arrayContaining([ @@ -1102,7 +1115,7 @@ describe.each([ user, }); - const changes = await CompassSyncProcessor.processEvents([ + const changes = await CompassToGoogleEventPropagation.processEvents([ { payload: payload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -1142,13 +1155,14 @@ describe.each([ recurrence: { rule: ["RRULE:FREQ=DAILY;COUNT=5"] }, }; - const updateChanges = await CompassSyncProcessor.processEvents([ - { - payload: updatedPayload as CompassThisEvent["payload"], - applyTo: RecurringEventUpdateScope.THIS_EVENT, - status: CompassEventStatus.CONFIRMED, - }, - ]); + const updateChanges = + await CompassToGoogleEventPropagation.processEvents([ + { + payload: updatedPayload as CompassThisEvent["payload"], + applyTo: RecurringEventUpdateScope.THIS_EVENT, + status: CompassEventStatus.CONFIRMED, + }, + ]); expect(updateChanges).toEqual( expect.arrayContaining([ @@ -1182,7 +1196,7 @@ describe.each([ const user = _user._id.toString(); const payload = createMockStandaloneEvent({ isSomeday: true, user }); - const changes = await CompassSyncProcessor.processEvents([ + const changes = await CompassToGoogleEventPropagation.processEvents([ { payload: payload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -1217,13 +1231,15 @@ describe.each([ _id: somedayStandaloneEvent._id.toString(), }; - const deleteChanges = await CompassSyncProcessor.processEvents([ - { - payload: deletePayload as CompassThisEvent["payload"], - applyTo: RecurringEventUpdateScope.THIS_EVENT, - status: CompassEventStatus.CANCELLED, - }, - ]); + const deleteChanges = await CompassToGoogleEventPropagation.processEvents( + [ + { + payload: deletePayload as CompassThisEvent["payload"], + applyTo: RecurringEventUpdateScope.THIS_EVENT, + status: CompassEventStatus.CANCELLED, + }, + ], + ); expect(deleteChanges).toEqual( expect.arrayContaining([ @@ -1247,7 +1263,7 @@ describe.each([ const user = _user._id.toString(); const payload = createMockStandaloneEvent({ user }); - const changes = await CompassSyncProcessor.processEvents([ + const changes = await CompassToGoogleEventPropagation.processEvents([ { payload: payload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -1281,13 +1297,15 @@ describe.each([ _id: standaloneEvent._id.toString(), }; - const deleteChanges = await CompassSyncProcessor.processEvents([ - { - payload: deletePayload as CompassThisEvent["payload"], - applyTo: RecurringEventUpdateScope.THIS_EVENT, - status: CompassEventStatus.CANCELLED, - }, - ]); + const deleteChanges = await CompassToGoogleEventPropagation.processEvents( + [ + { + payload: deletePayload as CompassThisEvent["payload"], + applyTo: RecurringEventUpdateScope.THIS_EVENT, + status: CompassEventStatus.CANCELLED, + }, + ], + ); expect(deleteChanges).toEqual( expect.arrayContaining([ diff --git a/packages/backend/src/sync/services/sync/__tests__/compass.sync.processor.all-event.test.ts b/packages/backend/src/sync/services/event-propagation/__tests__/compass-to-google.all-event.test.ts similarity index 93% rename from packages/backend/src/sync/services/sync/__tests__/compass.sync.processor.all-event.test.ts rename to packages/backend/src/sync/services/event-propagation/__tests__/compass-to-google.all-event.test.ts index 62acc5984..7104e23ff 100644 --- a/packages/backend/src/sync/services/sync/__tests__/compass.sync.processor.all-event.test.ts +++ b/packages/backend/src/sync/services/event-propagation/__tests__/compass-to-google.all-event.test.ts @@ -24,11 +24,11 @@ import { testCompassSeriesInGcal, } from "@backend/event/classes/compass.event.parser.test.util"; import eventService, { _getGcal } from "@backend/event/services/event.service"; -import { CompassSyncProcessor } from "@backend/sync/services/sync/compass/compass.sync.processor"; +import { CompassToGoogleEventPropagation } from "@backend/sync/services/event-propagation/compass-to-google/compass-to-google.event-propagation"; describe.each([ { calendarProvider: CalendarProvider.GOOGLE }, -])(`CompassSyncProcessor - $calendarProvider calendar: ${RecurringEventUpdateScope.ALL_EVENTS}`, ({ +])(`CompassToGoogleEventPropagation - $calendarProvider calendar: ${RecurringEventUpdateScope.ALL_EVENTS}`, ({ calendarProvider, }) => { beforeAll(setupTestDb); @@ -51,7 +51,7 @@ describe.each([ recurrence, }); - const changes = await CompassSyncProcessor.processEvents([ + const changes = await CompassToGoogleEventPropagation.processEvents([ { payload: payload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -157,13 +157,14 @@ describe.each([ title: faker.lorem.sentence(3), }; - const updateChanges = await CompassSyncProcessor.processEvents([ - { - payload: updatedPayload as CompassAllEvents["payload"], - applyTo: RecurringEventUpdateScope.ALL_EVENTS, - status: CompassEventStatus.CONFIRMED, - }, - ]); + const updateChanges = + await CompassToGoogleEventPropagation.processEvents([ + { + payload: updatedPayload as CompassAllEvents["payload"], + applyTo: RecurringEventUpdateScope.ALL_EVENTS, + status: CompassEventStatus.CONFIRMED, + }, + ]); expect(updateChanges).toEqual( expect.arrayContaining([ @@ -256,7 +257,7 @@ describe.each([ recurrence, }); - const changes = await CompassSyncProcessor.processEvents([ + const changes = await CompassToGoogleEventPropagation.processEvents([ { payload: payload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -362,13 +363,14 @@ describe.each([ description: faker.lorem.sentence(3), }; - const updateChanges = await CompassSyncProcessor.processEvents([ - { - payload: updatedPayload as CompassAllEvents["payload"], - applyTo: RecurringEventUpdateScope.ALL_EVENTS, - status: CompassEventStatus.CONFIRMED, - }, - ]); + const updateChanges = + await CompassToGoogleEventPropagation.processEvents([ + { + payload: updatedPayload as CompassAllEvents["payload"], + applyTo: RecurringEventUpdateScope.ALL_EVENTS, + status: CompassEventStatus.CONFIRMED, + }, + ]); expect(updateChanges).toEqual( expect.arrayContaining([ @@ -465,7 +467,7 @@ describe.each([ priority: Priorities.SELF, }); - const changes = await CompassSyncProcessor.processEvents([ + const changes = await CompassToGoogleEventPropagation.processEvents([ { payload: payload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -582,13 +584,14 @@ describe.each([ priority: Priorities.RELATIONS, }; - const updateChanges = await CompassSyncProcessor.processEvents([ - { - payload: updatedPayload as CompassAllEvents["payload"], - applyTo: RecurringEventUpdateScope.ALL_EVENTS, - status: CompassEventStatus.CONFIRMED, - }, - ]); + const updateChanges = + await CompassToGoogleEventPropagation.processEvents([ + { + payload: updatedPayload as CompassAllEvents["payload"], + applyTo: RecurringEventUpdateScope.ALL_EVENTS, + status: CompassEventStatus.CONFIRMED, + }, + ]); expect(updateChanges).toEqual( expect.arrayContaining([ @@ -689,7 +692,7 @@ describe.each([ recurrence, }); - const changes = await CompassSyncProcessor.processEvents([ + const changes = await CompassToGoogleEventPropagation.processEvents([ { payload: payload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -797,13 +800,14 @@ describe.each([ .toISOString(), }; - const updateChanges = await CompassSyncProcessor.processEvents([ - { - payload: updatedPayload as CompassAllEvents["payload"], - applyTo: RecurringEventUpdateScope.ALL_EVENTS, - status: CompassEventStatus.CONFIRMED, - }, - ]); + const updateChanges = + await CompassToGoogleEventPropagation.processEvents([ + { + payload: updatedPayload as CompassAllEvents["payload"], + applyTo: RecurringEventUpdateScope.ALL_EVENTS, + status: CompassEventStatus.CONFIRMED, + }, + ]); expect(updateChanges).toEqual( expect.arrayContaining([ @@ -908,7 +912,7 @@ describe.each([ recurrence, }); - const changes = await CompassSyncProcessor.processEvents([ + const changes = await CompassToGoogleEventPropagation.processEvents([ { payload: payload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -1016,13 +1020,14 @@ describe.each([ .toISOString(), }; - const updateChanges = await CompassSyncProcessor.processEvents([ - { - payload: updatedPayload as CompassAllEvents["payload"], - applyTo: RecurringEventUpdateScope.ALL_EVENTS, - status: CompassEventStatus.CONFIRMED, - }, - ]); + const updateChanges = + await CompassToGoogleEventPropagation.processEvents([ + { + payload: updatedPayload as CompassAllEvents["payload"], + applyTo: RecurringEventUpdateScope.ALL_EVENTS, + status: CompassEventStatus.CONFIRMED, + }, + ]); expect(updateChanges).toEqual( expect.arrayContaining([ @@ -1125,7 +1130,7 @@ describe.each([ recurrence, }); - const changes = await CompassSyncProcessor.processEvents([ + const changes = await CompassToGoogleEventPropagation.processEvents([ { payload: payload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -1234,13 +1239,14 @@ describe.each([ isSomeday: true, }; - const updateChanges = await CompassSyncProcessor.processEvents([ - { - payload: updatedPayload as CompassAllEvents["payload"], - applyTo: RecurringEventUpdateScope.ALL_EVENTS, - status: CompassEventStatus.CONFIRMED, - }, - ]); + const updateChanges = + await CompassToGoogleEventPropagation.processEvents([ + { + payload: updatedPayload as CompassAllEvents["payload"], + applyTo: RecurringEventUpdateScope.ALL_EVENTS, + status: CompassEventStatus.CONFIRMED, + }, + ]); expect(updateChanges).toEqual( expect.arrayContaining([ @@ -1342,7 +1348,7 @@ describe.each([ const recurrence = { rule: ["RRULE:FREQ=WEEKLY;COUNT=10"] }; const payload = createMockBaseEvent({ recurrence, user }); - await CompassSyncProcessor.processEvents([ + await CompassToGoogleEventPropagation.processEvents([ { payload: payload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -1371,13 +1377,14 @@ describe.each([ }, }; - const updateChanges = await CompassSyncProcessor.processEvents([ - { - payload: updatedPayload as CompassAllEvents["payload"], - applyTo: RecurringEventUpdateScope.ALL_EVENTS, - status: CompassEventStatus.CONFIRMED, - }, - ]); + const updateChanges = + await CompassToGoogleEventPropagation.processEvents([ + { + payload: updatedPayload as CompassAllEvents["payload"], + applyTo: RecurringEventUpdateScope.ALL_EVENTS, + status: CompassEventStatus.CONFIRMED, + }, + ]); expect(updateChanges).toEqual( expect.arrayContaining([ @@ -1425,7 +1432,7 @@ describe.each([ recurrence, }); - const changes = await CompassSyncProcessor.processEvents([ + const changes = await CompassToGoogleEventPropagation.processEvents([ { payload: payload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -1530,13 +1537,14 @@ describe.each([ title: "Transitioned to Regular Event", }; - const updateChanges = await CompassSyncProcessor.processEvents([ - { - payload: updatedPayload as CompassAllEvents["payload"], - applyTo: RecurringEventUpdateScope.ALL_EVENTS, - status: CompassEventStatus.CONFIRMED, - }, - ]); + const updateChanges = + await CompassToGoogleEventPropagation.processEvents([ + { + payload: updatedPayload as CompassAllEvents["payload"], + applyTo: RecurringEventUpdateScope.ALL_EVENTS, + status: CompassEventStatus.CONFIRMED, + }, + ]); expect(updateChanges).toEqual( expect.arrayContaining([ @@ -1635,7 +1643,7 @@ describe.each([ const recurrence = { rule: ["RRULE:FREQ=WEEKLY;COUNT=10"] }; const payload = createMockBaseEvent({ recurrence, user }); - await CompassSyncProcessor.processEvents([ + await CompassToGoogleEventPropagation.processEvents([ { payload: payload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -1664,13 +1672,15 @@ describe.each([ _id: instanceToUpdate._id.toString(), }; - const updateChanges = await CompassSyncProcessor.processEvents([ - { - payload: updatedPayload as CompassAllEvents["payload"], - applyTo: RecurringEventUpdateScope.ALL_EVENTS, - status: CompassEventStatus.CANCELLED, - }, - ]); + const updateChanges = await CompassToGoogleEventPropagation.processEvents( + [ + { + payload: updatedPayload as CompassAllEvents["payload"], + applyTo: RecurringEventUpdateScope.ALL_EVENTS, + status: CompassEventStatus.CANCELLED, + }, + ], + ); expect(updateChanges).toEqual( expect.arrayContaining([ diff --git a/packages/backend/src/sync/services/sync/__tests__/compass.sync.processor.test.ts b/packages/backend/src/sync/services/event-propagation/__tests__/compass-to-google.event-propagation.test.ts similarity index 89% rename from packages/backend/src/sync/services/sync/__tests__/compass.sync.processor.test.ts rename to packages/backend/src/sync/services/event-propagation/__tests__/compass-to-google.event-propagation.test.ts index 0ee40b4c8..52f608f50 100644 --- a/packages/backend/src/sync/services/sync/__tests__/compass.sync.processor.test.ts +++ b/packages/backend/src/sync/services/event-propagation/__tests__/compass-to-google.event-propagation.test.ts @@ -23,12 +23,12 @@ import { type CompassApplyResult } from "@backend/event/classes/compass.event.ex import * as compassParser from "@backend/event/classes/compass.event.parser"; import * as eventService from "@backend/event/services/event.service"; import { sseServer } from "@backend/servers/sse/sse.server"; -import { CompassSyncProcessor } from "@backend/sync/services/sync/compass/compass.sync.processor"; +import { CompassToGoogleEventPropagation } from "@backend/sync/services/event-propagation/compass-to-google/compass-to-google.event-propagation"; import { type Event_Transition } from "@backend/sync/sync.types"; // Import the enum -describe("CompassSyncProcessor.getNotificationType", () => { +describe("CompassToGoogleEventPropagation.getNotificationType", () => { it("returns EVENT_CHANGED for non-SOMEDAY transitions", () => { const transition: Event_Transition = { transition: [Categories_Recurrence.STANDALONE, "STANDALONE_CONFIRMED"], @@ -37,10 +37,9 @@ describe("CompassSyncProcessor.getNotificationType", () => { category: Categories_Recurrence.STANDALONE, }; - expect(CompassSyncProcessor["getNotificationType"](transition)).toEqual([ - EVENT_CHANGED, - EVENT_CHANGED, - ]); + expect( + CompassToGoogleEventPropagation["getNotificationType"](transition), + ).toEqual([EVENT_CHANGED, EVENT_CHANGED]); }); it("returns SOMEDAY_EVENT_CHANGED for SOMEDAY transitions", () => { @@ -54,10 +53,9 @@ describe("CompassSyncProcessor.getNotificationType", () => { category: Categories_Recurrence.STANDALONE_SOMEDAY, }; - expect(CompassSyncProcessor["getNotificationType"](transition)).toEqual([ - SOMEDAY_EVENT_CHANGED, - SOMEDAY_EVENT_CHANGED, - ]); + expect( + CompassToGoogleEventPropagation["getNotificationType"](transition), + ).toEqual([SOMEDAY_EVENT_CHANGED, SOMEDAY_EVENT_CHANGED]); }); it("returns mixed notifications for mixed transitions", () => { @@ -71,14 +69,13 @@ describe("CompassSyncProcessor.getNotificationType", () => { category: Categories_Recurrence.STANDALONE_SOMEDAY, }; - expect(CompassSyncProcessor["getNotificationType"](transition)).toEqual([ - SOMEDAY_EVENT_CHANGED, - EVENT_CHANGED, - ]); + expect( + CompassToGoogleEventPropagation["getNotificationType"](transition), + ).toEqual([SOMEDAY_EVENT_CHANGED, EVENT_CHANGED]); }); }); -describe("CompassSyncProcessor.notifyClients", () => { +describe("CompassToGoogleEventPropagation.notifyClients", () => { beforeEach(() => { jest.spyOn(sseServer, "handleBackgroundCalendarChange").mockClear(); jest.spyOn(sseServer, "handleBackgroundSomedayChange").mockClear(); @@ -122,14 +119,14 @@ describe("CompassSyncProcessor.notifyClients", () => { }, ]; - CompassSyncProcessor["notifyClients"](events, summary); + CompassToGoogleEventPropagation["notifyClients"](events, summary); expect(calendarSpy).toHaveBeenCalledWith(userA); expect(somedaySpy).toHaveBeenCalledWith(userB); }); }); -describe("CompassSyncProcessor.handleCompassChange", () => { +describe("CompassToGoogleEventPropagation.handleCompassChange", () => { beforeEach(() => { jest.restoreAllMocks(); }); @@ -177,7 +174,7 @@ describe("CompassSyncProcessor.handleCompassChange", () => { .mockResolvedValueOnce(applyResult); await expect( - CompassSyncProcessor["handleCompassChange"](event), + CompassToGoogleEventPropagation["handleCompassChange"](event), ).resolves.toEqual([applyResult.summary]); expect(findOneSpy).toHaveBeenCalledWith( @@ -229,7 +226,7 @@ describe("CompassSyncProcessor.handleCompassChange", () => { ); await expect( - CompassSyncProcessor["handleCompassChange"](event), + CompassToGoogleEventPropagation["handleCompassChange"](event), ).resolves.toEqual([ { title: payload.title!, @@ -286,7 +283,7 @@ describe("CompassSyncProcessor.handleCompassChange", () => { .mockRejectedValueOnce(missingRefreshTokenError); await expect( - CompassSyncProcessor["handleCompassChange"](event), + CompassToGoogleEventPropagation["handleCompassChange"](event), ).resolves.toEqual([ { title: payload.title!, diff --git a/packages/backend/src/sync/services/sync/__tests__/compass.sync.processor.this-and-following-event.test.ts b/packages/backend/src/sync/services/event-propagation/__tests__/compass-to-google.this-and-following-event.test.ts similarity index 93% rename from packages/backend/src/sync/services/sync/__tests__/compass.sync.processor.this-and-following-event.test.ts rename to packages/backend/src/sync/services/event-propagation/__tests__/compass-to-google.this-and-following-event.test.ts index 58913045d..4e0ce2042 100644 --- a/packages/backend/src/sync/services/sync/__tests__/compass.sync.processor.this-and-following-event.test.ts +++ b/packages/backend/src/sync/services/event-propagation/__tests__/compass-to-google.this-and-following-event.test.ts @@ -27,11 +27,11 @@ import { testCompassSeries, } from "@backend/event/classes/compass.event.parser.test.util"; import eventService, { _getGcal } from "@backend/event/services/event.service"; -import { CompassSyncProcessor } from "@backend/sync/services/sync/compass/compass.sync.processor"; +import { CompassToGoogleEventPropagation } from "@backend/sync/services/event-propagation/compass-to-google/compass-to-google.event-propagation"; describe.each([ { calendarProvider: CalendarProvider.GOOGLE }, -])(`CompassSyncProcessor $calendarProvider calendar: ${RecurringEventUpdateScope.THIS_AND_FOLLOWING_EVENTS}`, ({ +])(`CompassToGoogleEventPropagation $calendarProvider calendar: ${RecurringEventUpdateScope.THIS_AND_FOLLOWING_EVENTS}`, ({ calendarProvider, }) => { beforeAll(setupTestDb); @@ -53,7 +53,7 @@ describe.each([ true, ); - const changes = await CompassSyncProcessor.processEvents([ + const changes = await CompassToGoogleEventPropagation.processEvents([ { payload: payload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -101,14 +101,15 @@ describe.each([ title: faker.lorem.sentence(3), }; - const updateChanges = await CompassSyncProcessor.processEvents([ - { - payload: - updatedPayload as CompassThisAndFollowingEvent["payload"], - applyTo: RecurringEventUpdateScope.THIS_AND_FOLLOWING_EVENTS, - status: CompassEventStatus.CONFIRMED, - }, - ]); + const updateChanges = + await CompassToGoogleEventPropagation.processEvents([ + { + payload: + updatedPayload as CompassThisAndFollowingEvent["payload"], + applyTo: RecurringEventUpdateScope.THIS_AND_FOLLOWING_EVENTS, + status: CompassEventStatus.CONFIRMED, + }, + ]); if (!isFirstInstance) { expect(updateChanges).toEqual( @@ -268,7 +269,7 @@ describe.each([ true, ); - const changes = await CompassSyncProcessor.processEvents([ + const changes = await CompassToGoogleEventPropagation.processEvents([ { payload: payload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -316,14 +317,15 @@ describe.each([ description: faker.lorem.sentence(3), }; - const updateChanges = await CompassSyncProcessor.processEvents([ - { - payload: - updatedPayload as CompassThisAndFollowingEvent["payload"], - applyTo: RecurringEventUpdateScope.THIS_AND_FOLLOWING_EVENTS, - status: CompassEventStatus.CONFIRMED, - }, - ]); + const updateChanges = + await CompassToGoogleEventPropagation.processEvents([ + { + payload: + updatedPayload as CompassThisAndFollowingEvent["payload"], + applyTo: RecurringEventUpdateScope.THIS_AND_FOLLOWING_EVENTS, + status: CompassEventStatus.CONFIRMED, + }, + ]); if (!isFirstInstance) { expect(updateChanges).toEqual( @@ -485,7 +487,7 @@ describe.each([ priority: Priorities.SELF, }); - const changes = await CompassSyncProcessor.processEvents([ + const changes = await CompassToGoogleEventPropagation.processEvents([ { payload: payload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -533,14 +535,15 @@ describe.each([ priority: Priorities.RELATIONS, }; - const updateChanges = await CompassSyncProcessor.processEvents([ - { - payload: - updatedPayload as CompassThisAndFollowingEvent["payload"], - applyTo: RecurringEventUpdateScope.THIS_AND_FOLLOWING_EVENTS, - status: CompassEventStatus.CONFIRMED, - }, - ]); + const updateChanges = + await CompassToGoogleEventPropagation.processEvents([ + { + payload: + updatedPayload as CompassThisAndFollowingEvent["payload"], + applyTo: RecurringEventUpdateScope.THIS_AND_FOLLOWING_EVENTS, + status: CompassEventStatus.CONFIRMED, + }, + ]); if (!isFirstInstance) { expect(updateChanges).toEqual( @@ -709,7 +712,7 @@ describe.each([ true, ); - const changes = await CompassSyncProcessor.processEvents([ + const changes = await CompassToGoogleEventPropagation.processEvents([ { payload: payload as CompassThisEvent["payload"], applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -753,13 +756,15 @@ describe.each([ _id: splitInstanceId, }; - const updateChanges = await CompassSyncProcessor.processEvents([ - { - payload: updatedPayload as CompassThisAndFollowingEvent["payload"], - applyTo: RecurringEventUpdateScope.THIS_AND_FOLLOWING_EVENTS, - status: CompassEventStatus.CANCELLED, - }, - ]); + const updateChanges = await CompassToGoogleEventPropagation.processEvents( + [ + { + payload: updatedPayload as CompassThisAndFollowingEvent["payload"], + applyTo: RecurringEventUpdateScope.THIS_AND_FOLLOWING_EVENTS, + status: CompassEventStatus.CANCELLED, + }, + ], + ); if (!isFirstInstance) { expect(updateChanges).toEqual( diff --git a/packages/backend/src/sync/services/sync/__tests__/gcal.sync.processor.delete.test.ts b/packages/backend/src/sync/services/event-propagation/__tests__/google-to-compass.delete.test.ts similarity index 92% rename from packages/backend/src/sync/services/sync/__tests__/gcal.sync.processor.delete.test.ts rename to packages/backend/src/sync/services/event-propagation/__tests__/google-to-compass.delete.test.ts index e05441db7..7b5963771 100644 --- a/packages/backend/src/sync/services/sync/__tests__/gcal.sync.processor.delete.test.ts +++ b/packages/backend/src/sync/services/event-propagation/__tests__/google-to-compass.delete.test.ts @@ -14,10 +14,10 @@ import { } from "@backend/__tests__/helpers/mock.db.setup"; import { simulateDbAfterGcalImport } from "@backend/__tests__/helpers/mock.events.init"; import { mockRecurringGcalBaseEvent } from "@backend/__tests__/mocks.gcal/factories/gcal.event.factory"; -import { createCompassSeriesFromGcalBase } from "@backend/sync/services/sync/__tests__/gcal.sync.processor.test.util"; -import { GcalSyncProcessor } from "@backend/sync/services/sync/google/gcal.sync.processor"; +import { createCompassSeriesFromGcalBase } from "@backend/sync/services/event-propagation/__tests__/google-to-compass.event-propagation.test.util"; +import { GoogleToCompassEventPropagation } from "@backend/sync/services/event-propagation/google-to-compass/google-to-compass.event-propagation"; -describe("GcalSyncProcessor: DELETE", () => { +describe("GoogleToCompassEventPropagation: DELETE", () => { beforeAll(setupTestDb); beforeEach(cleanupCollections); @@ -41,7 +41,7 @@ describe("GcalSyncProcessor: DELETE", () => { status: "cancelled", } as WithGcalId; - const processor = new GcalSyncProcessor(user._id.toString()); + const processor = new GoogleToCompassEventPropagation(user._id.toString()); const changes = await processor.processEvents([cancelledGStandalone]); /* Assert: Should return a DELETED change */ @@ -86,7 +86,7 @@ describe("GcalSyncProcessor: DELETE", () => { }; /* Act */ - const processor = new GcalSyncProcessor(user._id.toString()); + const processor = new GoogleToCompassEventPropagation(user._id.toString()); const changes = await processor.processEvents([ gcalEvents.recurring, @@ -149,7 +149,7 @@ describe("GcalSyncProcessor: DELETE", () => { })); /* Act */ - const processor = new GcalSyncProcessor(user._id.toString()); + const processor = new GoogleToCompassEventPropagation(user._id.toString()); const changes = await processor.processEvents([ gcalBaseEvent, @@ -208,7 +208,7 @@ describe("GcalSyncProcessor: DELETE", () => { status: "cancelled", }; - const processor = new GcalSyncProcessor(user._id.toString()); + const processor = new GoogleToCompassEventPropagation(user._id.toString()); const changes = await processor.processEvents([cancelledBase]); expect(changes).toHaveLength(1); diff --git a/packages/backend/src/sync/services/sync/__tests__/gcal.sync.processor.test.util.ts b/packages/backend/src/sync/services/event-propagation/__tests__/google-to-compass.event-propagation.test.util.ts similarity index 99% rename from packages/backend/src/sync/services/sync/__tests__/gcal.sync.processor.test.util.ts rename to packages/backend/src/sync/services/event-propagation/__tests__/google-to-compass.event-propagation.test.util.ts index eec0dea20..e52d17c33 100644 --- a/packages/backend/src/sync/services/sync/__tests__/gcal.sync.processor.test.util.ts +++ b/packages/backend/src/sync/services/event-propagation/__tests__/google-to-compass.event-propagation.test.util.ts @@ -23,7 +23,7 @@ import { mockRecurringGcalInstances } from "@backend/__tests__/mocks.gcal/factor import { type Event_API } from "@backend/common/types/backend.event.types"; import { validateEventSafely } from "@backend/common/validators/validate.event"; -/** Utility assertions for the gcal sync processor tests */ +/** Utility assertions for Google-to-Compass event propagation tests */ export const baseHasRecurrenceRule = async ( events: Event_Core[], rule: string[], diff --git a/packages/backend/src/sync/services/sync/__tests__/gcal.sync.processor.upsert.base.split.test.ts b/packages/backend/src/sync/services/event-propagation/__tests__/google-to-compass.upsert.base.split.test.ts similarity index 90% rename from packages/backend/src/sync/services/sync/__tests__/gcal.sync.processor.upsert.base.split.test.ts rename to packages/backend/src/sync/services/event-propagation/__tests__/google-to-compass.upsert.base.split.test.ts index d8c7a0e08..124d36b26 100644 --- a/packages/backend/src/sync/services/sync/__tests__/gcal.sync.processor.upsert.base.split.test.ts +++ b/packages/backend/src/sync/services/event-propagation/__tests__/google-to-compass.upsert.base.split.test.ts @@ -17,10 +17,10 @@ import { baseHasRecurrenceRule, noInstancesAfterSplitDate, updateBasePayloadToExpireOneDayAfterFirstInstance, -} from "@backend/sync/services/sync/__tests__/gcal.sync.processor.test.util"; -import { GcalSyncProcessor } from "@backend/sync/services/sync/google/gcal.sync.processor"; +} from "@backend/sync/services/event-propagation/__tests__/google-to-compass.event-propagation.test.util"; +import { GoogleToCompassEventPropagation } from "@backend/sync/services/event-propagation/google-to-compass/google-to-compass.event-propagation"; -describe("GcalSyncProcessor: UPSERT: BASE SPLIT", () => { +describe("GoogleToCompassEventPropagation: UPSERT: BASE SPLIT", () => { beforeAll(setupTestDb); beforeEach(cleanupCollections); @@ -46,7 +46,7 @@ describe("GcalSyncProcessor: UPSERT: BASE SPLIT", () => { /* Act */ const origEvents = await getEventsInDb({ user: user._id.toString() }); - const processor = new GcalSyncProcessor(user._id.toString()); + const processor = new GoogleToCompassEventPropagation(user._id.toString()); const changes = await processor.processEvents([gBaseWithUntil]); /* Assert */ @@ -123,7 +123,7 @@ describe("GcalSyncProcessor: UPSERT: BASE SPLIT", () => { status: "cancelled", }; - const processor = new GcalSyncProcessor(user._id.toString()); + const processor = new GoogleToCompassEventPropagation(user._id.toString()); const changes = await processor.processEvents([cancelledInstance]); /* Assert */ diff --git a/packages/backend/src/sync/services/sync/__tests__/gcal.sync.processor.upsert.base.test.ts b/packages/backend/src/sync/services/event-propagation/__tests__/google-to-compass.upsert.base.test.ts similarity index 90% rename from packages/backend/src/sync/services/sync/__tests__/gcal.sync.processor.upsert.base.test.ts rename to packages/backend/src/sync/services/event-propagation/__tests__/google-to-compass.upsert.base.test.ts index 211ef331e..b18c930ab 100644 --- a/packages/backend/src/sync/services/sync/__tests__/gcal.sync.processor.upsert.base.test.ts +++ b/packages/backend/src/sync/services/event-propagation/__tests__/google-to-compass.upsert.base.test.ts @@ -21,10 +21,10 @@ import { hasNewUpdatedAtTimestamp, instanceDataMatchCompassBase, instanceDataMatchesGcalBase, -} from "@backend/sync/services/sync/__tests__/gcal.sync.processor.test.util"; -import { GcalSyncProcessor } from "@backend/sync/services/sync/google/gcal.sync.processor"; +} from "@backend/sync/services/event-propagation/__tests__/google-to-compass.event-propagation.test.util"; +import { GoogleToCompassEventPropagation } from "@backend/sync/services/event-propagation/google-to-compass/google-to-compass.event-propagation"; -describe("GcalSyncProcessor UPSERT: BASE", () => { +describe("GoogleToCompassEventPropagation UPSERT: BASE", () => { beforeAll(setupTestDb); beforeEach(cleanupCollections); @@ -42,7 +42,7 @@ describe("GcalSyncProcessor UPSERT: BASE", () => { await simulateGoogleCalendarEventCreation(newBase); - const processor = new GcalSyncProcessor(user._id.toString()); + const processor = new GoogleToCompassEventPropagation(user._id.toString()); const changes = await processor.processEvents([newBase]); expect(changes).toHaveLength(1); @@ -76,7 +76,7 @@ describe("GcalSyncProcessor UPSERT: BASE", () => { await simulateGoogleCalendarEventCreation(allDayBase); - const processor = new GcalSyncProcessor(user._id.toString()); + const processor = new GoogleToCompassEventPropagation(user._id.toString()); const changes = await processor.processEvents([allDayBase]); expect(changes).toHaveLength(1); @@ -118,7 +118,7 @@ describe("GcalSyncProcessor UPSERT: BASE", () => { description: "ALL-DAY Description adjusted in Gcal", }; - const processor = new GcalSyncProcessor(user._id.toString()); + const processor = new GoogleToCompassEventPropagation(user._id.toString()); const changes = await processor.processEvents([updatedGcalAllDayBase]); /* Assert */ @@ -172,7 +172,7 @@ describe("GcalSyncProcessor UPSERT: BASE", () => { description: "Description adjusted in Gcal", }; - const processor = new GcalSyncProcessor(user._id.toString()); + const processor = new GoogleToCompassEventPropagation(user._id.toString()); const changes = await processor.processEvents([updatedGcalBase]); /* Assert */ diff --git a/packages/backend/src/sync/services/sync/__tests__/gcal.sync.processor.upsert.instance.test.ts b/packages/backend/src/sync/services/event-propagation/__tests__/google-to-compass.upsert.instance.test.ts similarity index 93% rename from packages/backend/src/sync/services/sync/__tests__/gcal.sync.processor.upsert.instance.test.ts rename to packages/backend/src/sync/services/event-propagation/__tests__/google-to-compass.upsert.instance.test.ts index 4819c8c8d..87a420822 100644 --- a/packages/backend/src/sync/services/sync/__tests__/gcal.sync.processor.upsert.instance.test.ts +++ b/packages/backend/src/sync/services/event-propagation/__tests__/google-to-compass.upsert.instance.test.ts @@ -8,9 +8,9 @@ import { } from "@backend/__tests__/helpers/mock.db.setup"; import { simulateDbAfterGcalImport } from "@backend/__tests__/helpers/mock.events.init"; import mongoService from "@backend/common/services/mongo.service"; -import { GcalSyncProcessor } from "@backend/sync/services/sync/google/gcal.sync.processor"; +import { GoogleToCompassEventPropagation } from "@backend/sync/services/event-propagation/google-to-compass/google-to-compass.event-propagation"; -describe("GcalSyncProcessor UPSERT: INSTANCE", () => { +describe("GoogleToCompassEventPropagation UPSERT: INSTANCE", () => { beforeAll(setupTestDb); beforeEach(cleanupCollections); @@ -34,7 +34,7 @@ describe("GcalSyncProcessor UPSERT: INSTANCE", () => { const instanceTitle = instance.summary; - const processor = new GcalSyncProcessor(user._id.toString()); + const processor = new GoogleToCompassEventPropagation(user._id.toString()); const changes = await processor.processEvents([instance]); // Verify the correct change was detected @@ -76,7 +76,7 @@ describe("GcalSyncProcessor UPSERT: INSTANCE", () => { delete (standalone as { recurringEventId?: string }).recurringEventId; delete (standalone as { recurrence?: string[] }).recurrence; - const processor = new GcalSyncProcessor(user._id.toString()); + const processor = new GoogleToCompassEventPropagation(user._id.toString()); const changes = await processor.processEvents([standalone]); expect(changes).toHaveLength(1); @@ -117,7 +117,7 @@ describe("GcalSyncProcessor UPSERT: INSTANCE", () => { const updatedGcalEvents = [regular, base, instance]; - const processor = new GcalSyncProcessor(user._id.toString()); + const processor = new GoogleToCompassEventPropagation(user._id.toString()); const changes = await processor.processEvents(updatedGcalEvents); expect(changes).toHaveLength(4); diff --git a/packages/backend/src/sync/services/sync/__tests__/gcal.sync.processor.upsert.standalone.test.ts b/packages/backend/src/sync/services/event-propagation/__tests__/google-to-compass.upsert.standalone.test.ts similarity index 89% rename from packages/backend/src/sync/services/sync/__tests__/gcal.sync.processor.upsert.standalone.test.ts rename to packages/backend/src/sync/services/event-propagation/__tests__/google-to-compass.upsert.standalone.test.ts index 593f5478b..ffc23c7a7 100644 --- a/packages/backend/src/sync/services/sync/__tests__/gcal.sync.processor.upsert.standalone.test.ts +++ b/packages/backend/src/sync/services/event-propagation/__tests__/google-to-compass.upsert.standalone.test.ts @@ -8,9 +8,9 @@ import { } from "@backend/__tests__/helpers/mock.db.setup"; import { simulateDbAfterGcalImport } from "@backend/__tests__/helpers/mock.events.init"; import { mockRegularGcalEvent } from "@backend/__tests__/mocks.gcal/factories/gcal.event.factory"; -import { GcalSyncProcessor } from "@backend/sync/services/sync/google/gcal.sync.processor"; +import { GoogleToCompassEventPropagation } from "@backend/sync/services/event-propagation/google-to-compass/google-to-compass.event-propagation"; -describe("GcalSyncProcessor UPSERT: STANDALONE", () => { +describe("GoogleToCompassEventPropagation UPSERT: STANDALONE", () => { beforeAll(setupTestDb); beforeEach(cleanupCollections); @@ -31,7 +31,7 @@ describe("GcalSyncProcessor UPSERT: STANDALONE", () => { const newStandalone = mockRegularGcalEvent({ summary: "New Standalone Event", }); - const processor = new GcalSyncProcessor(user._id.toString()); + const processor = new GoogleToCompassEventPropagation(user._id.toString()); const changes = await processor.processEvents([newStandalone]); /* Assert */ @@ -72,7 +72,7 @@ describe("GcalSyncProcessor UPSERT: STANDALONE", () => { const origEventsCount = (await getEventsInDb({ user: user._id.toString() })) .length; /* Act */ - const processor = new GcalSyncProcessor(user._id.toString()); + const processor = new GoogleToCompassEventPropagation(user._id.toString()); const changes = await processor.processEvents([updatedStandalone]); /* Assert */ diff --git a/packages/backend/src/sync/services/outbound/compass-google-mirror.service.test.ts b/packages/backend/src/sync/services/event-propagation/compass-to-google/compass-to-google-backfill.test.ts similarity index 87% rename from packages/backend/src/sync/services/outbound/compass-google-mirror.service.test.ts rename to packages/backend/src/sync/services/event-propagation/compass-to-google/compass-to-google-backfill.test.ts index 14f552822..6258bd619 100644 --- a/packages/backend/src/sync/services/outbound/compass-google-mirror.service.test.ts +++ b/packages/backend/src/sync/services/event-propagation/compass-to-google/compass-to-google-backfill.test.ts @@ -9,7 +9,7 @@ import { import { initSupertokens } from "@backend/common/middleware/supertokens.middleware"; import mongoService from "@backend/common/services/mongo.service"; import * as eventServiceModule from "@backend/event/services/event.service"; -import compassGoogleMirrorService from "@backend/sync/services/outbound/compass-google-mirror.service"; +import compassToGoogleBackfill from "@backend/sync/services/event-propagation/compass-to-google/compass-to-google-backfill"; const createEvent = (user: string, overrides = {}) => ({ _id: new ObjectId(), @@ -24,7 +24,7 @@ const createEvent = (user: string, overrides = {}) => ({ ...overrides, }); -describe("compassGoogleMirrorService", () => { +describe("compassToGoogleBackfill", () => { beforeAll(initSupertokens); beforeEach(setupTestDb); beforeEach(cleanupCollections); @@ -41,7 +41,7 @@ describe("compassGoogleMirrorService", () => { } as never); await expect( - compassGoogleMirrorService.syncCompassEventsToGoogle(userId), + compassToGoogleBackfill.syncCompassEventsToGoogle(userId), ).resolves.toBe(1); expect(await mongoService.event.findOne({ _id: event._id })).toEqual( @@ -59,7 +59,7 @@ describe("compassGoogleMirrorService", () => { const createSpy = jest.spyOn(eventServiceModule, "_createGcal"); await expect( - compassGoogleMirrorService.syncCompassEventsToGoogle(userId), + compassToGoogleBackfill.syncCompassEventsToGoogle(userId), ).resolves.toBe(0); expect(createSpy).not.toHaveBeenCalled(); diff --git a/packages/backend/src/sync/services/outbound/compass-google-mirror.service.ts b/packages/backend/src/sync/services/event-propagation/compass-to-google/compass-to-google-backfill.ts similarity index 96% rename from packages/backend/src/sync/services/outbound/compass-google-mirror.service.ts rename to packages/backend/src/sync/services/event-propagation/compass-to-google/compass-to-google-backfill.ts index f97ae2b8e..2db8abc0f 100644 --- a/packages/backend/src/sync/services/outbound/compass-google-mirror.service.ts +++ b/packages/backend/src/sync/services/event-propagation/compass-to-google/compass-to-google-backfill.ts @@ -87,8 +87,8 @@ export const syncCompassEventsToGoogle = async ( return syncedCount; }; -const compassGoogleMirrorService = { +const compassToGoogleBackfill = { syncCompassEventsToGoogle, }; -export default compassGoogleMirrorService; +export default compassToGoogleBackfill; diff --git a/packages/backend/src/sync/services/sync/compass/compass.sync.processor.ts b/packages/backend/src/sync/services/event-propagation/compass-to-google/compass-to-google.event-propagation.ts similarity index 87% rename from packages/backend/src/sync/services/sync/compass/compass.sync.processor.ts rename to packages/backend/src/sync/services/event-propagation/compass-to-google/compass-to-google.event-propagation.ts index ba80171d6..2d5b9157e 100644 --- a/packages/backend/src/sync/services/sync/compass/compass.sync.processor.ts +++ b/packages/backend/src/sync/services/event-propagation/compass-to-google/compass-to-google.event-propagation.ts @@ -20,16 +20,16 @@ import { _updateGcal, } from "@backend/event/services/event.service"; import { sseServer } from "@backend/servers/sse/sse.server"; +import { isMissingGoogleRefreshToken } from "@backend/sync/services/google-sync/google-sync.errors"; import { type Event_Transition } from "@backend/sync/sync.types"; -import { isMissingGoogleRefreshToken } from "@backend/sync/util/sync.util"; import { isPersistedCoreEvent, type PersistedCompassEvent, -} from "./compass.sync.processor.util"; +} from "./compass-to-google.event-propagation.util"; -const logger = Logger("app.compass.sync.processor"); +const logger = Logger("app:compass-to-google.event-propagation"); -export class CompassSyncProcessor { +export class CompassToGoogleEventPropagation { static async processEvents( events: CompassEvent[], _session?: ClientSession, @@ -50,10 +50,11 @@ export class CompassSyncProcessor { ).then((events) => events.flat()); for (const event of compassEvents) { - const changes = await CompassSyncProcessor.handleCompassChange( - event, - session, - ); + const changes = + await CompassToGoogleEventPropagation.handleCompassChange( + event, + session, + ); summary.push(...changes); } @@ -65,7 +66,8 @@ export class CompassSyncProcessor { throw error; } - if (!_session) CompassSyncProcessor.notifyClients(events, summary); + if (!_session) + CompassToGoogleEventPropagation.notifyClients(events, summary); return summary; } @@ -96,7 +98,9 @@ export class CompassSyncProcessor { summary: Event_Transition[], ): void { const notifications = [ - ...new Set(summary.flatMap(CompassSyncProcessor.getNotificationType)), + ...new Set( + summary.flatMap(CompassToGoogleEventPropagation.getNotificationType), + ), ]; const uniqueUserIds = new Set(events.map((e) => e.payload.user)); @@ -136,7 +140,10 @@ export class CompassSyncProcessor { if (!applyResult.applied) return []; const didExecuteGoogleEffect = - await CompassSyncProcessor.executeGoogleEffect(plan, applyResult); + await CompassToGoogleEventPropagation.executeGoogleEffect( + plan, + applyResult, + ); return didExecuteGoogleEffect ? [applyResult.summary] : []; } @@ -149,7 +156,7 @@ export class CompassSyncProcessor { }: Awaited>, ): Promise { try { - return await CompassSyncProcessor.handleGoogleEffectByType( + return await CompassToGoogleEventPropagation.handleGoogleEffectByType( plan, persistedEvent, googleDeleteEventId, diff --git a/packages/backend/src/sync/services/sync/compass/compass.sync.processor.util.ts b/packages/backend/src/sync/services/event-propagation/compass-to-google/compass-to-google.event-propagation.util.ts similarity index 100% rename from packages/backend/src/sync/services/sync/compass/compass.sync.processor.util.ts rename to packages/backend/src/sync/services/event-propagation/compass-to-google/compass-to-google.event-propagation.util.ts diff --git a/packages/backend/src/sync/services/sync/google/gcal.sync.processor.ts b/packages/backend/src/sync/services/event-propagation/google-to-compass/google-to-compass.event-propagation.ts similarity index 96% rename from packages/backend/src/sync/services/sync/google/gcal.sync.processor.ts rename to packages/backend/src/sync/services/event-propagation/google-to-compass/google-to-compass.event-propagation.ts index 76b0a9b92..e9c439391 100644 --- a/packages/backend/src/sync/services/sync/google/gcal.sync.processor.ts +++ b/packages/backend/src/sync/services/event-propagation/google-to-compass/google-to-compass.event-propagation.ts @@ -8,8 +8,8 @@ import mongoService from "@backend/common/services/mongo.service"; import { GcalEventParser } from "@backend/event/classes/gcal.event.parser"; import { type Event_Transition } from "@backend/sync/sync.types"; -const logger = Logger("app.gcal.sync.processor"); -export class GcalSyncProcessor { +const logger = Logger("app:google-to-compass.event-propagation"); +export class GoogleToCompassEventPropagation { constructor(private userId: string) {} async processEvents(events: gSchema$Event[]): Promise { diff --git a/packages/backend/src/sync/services/google-calendar-sync/google.calendar.client.test.ts b/packages/backend/src/sync/services/google-sync/gcal.client.test.ts similarity index 95% rename from packages/backend/src/sync/services/google-calendar-sync/google.calendar.client.test.ts rename to packages/backend/src/sync/services/google-sync/gcal.client.test.ts index 379500df7..607040280 100644 --- a/packages/backend/src/sync/services/google-calendar-sync/google.calendar.client.test.ts +++ b/packages/backend/src/sync/services/google-sync/gcal.client.test.ts @@ -3,7 +3,7 @@ import { GaxiosError } from "gaxios"; import { ObjectId } from "mongodb"; import { type Schema_User } from "@core/types/user.types"; import { UserError } from "@backend/common/errors/user/user.errors"; -import { getGcalClient } from "@backend/sync/services/google-calendar-sync/google.calendar.client"; +import { getGcalClient } from "@backend/sync/services/google-sync/gcal.client"; import { findCompassUserBy } from "@backend/user/queries/user.queries"; jest.mock("@backend/user/queries/user.queries", () => ({ diff --git a/packages/backend/src/sync/services/google-calendar-sync/google.calendar.client.ts b/packages/backend/src/sync/services/google-sync/gcal.client.ts similarity index 98% rename from packages/backend/src/sync/services/google-calendar-sync/google.calendar.client.ts rename to packages/backend/src/sync/services/google-sync/gcal.client.ts index 4d8ba86c0..080e83c05 100644 --- a/packages/backend/src/sync/services/google-calendar-sync/google.calendar.client.ts +++ b/packages/backend/src/sync/services/google-sync/gcal.client.ts @@ -10,7 +10,7 @@ import { error } from "@backend/common/errors/handlers/error.handler"; import { UserError } from "@backend/common/errors/user/user.errors"; import { findCompassUserBy } from "@backend/user/queries/user.queries"; -const logger = Logger("app:google.calendar.service"); +const logger = Logger("app:gcal.client"); export const getGAuthClientForUser = async ( user: WithId | { _id: string }, diff --git a/packages/backend/src/sync/services/google-sync/google-sync.errors.ts b/packages/backend/src/sync/services/google-sync/google-sync.errors.ts new file mode 100644 index 000000000..743281b65 --- /dev/null +++ b/packages/backend/src/sync/services/google-sync/google-sync.errors.ts @@ -0,0 +1,11 @@ +import { BaseError } from "@core/errors/errors.base"; +import { UserError } from "@backend/common/errors/user/user.errors"; + +export const isMissingGoogleRefreshToken = ( + error: unknown, +): error is BaseError => { + return ( + error instanceof BaseError && + error.description === UserError.MissingGoogleRefreshToken.description + ); +}; diff --git a/packages/backend/src/sync/services/google-sync/google-sync.health.test.ts b/packages/backend/src/sync/services/google-sync/google-sync.health.test.ts new file mode 100644 index 000000000..49fc39ac4 --- /dev/null +++ b/packages/backend/src/sync/services/google-sync/google-sync.health.test.ts @@ -0,0 +1,76 @@ +import { Resource_Sync } from "@core/types/sync.types"; +import { GoogleWatchDriver } from "@backend/__tests__/drivers/google-watch.driver"; +import { UserDriver } from "@backend/__tests__/drivers/user.driver"; +import { UtilDriver } from "@backend/__tests__/drivers/util.driver"; +import { + cleanupCollections, + cleanupTestDb, + setupTestDb, +} from "@backend/__tests__/helpers/mock.db.setup"; +import { initSupertokens } from "@backend/common/middleware/supertokens.middleware"; +import { updateSync } from "@backend/sync/services/records/sync-records.repository"; +import { isUsingGcalWebhookHttps } from "@backend/sync/services/watch/google-watch-config"; +import { isGoogleCalendarSyncHealthy } from "./google-sync.health"; + +jest.mock("@backend/sync/services/watch/google-watch-config", () => ({ + ...jest.requireActual("@backend/sync/services/watch/google-watch-config"), + isUsingGcalWebhookHttps: jest.fn(() => true), +})); + +describe("googleSyncHealth", () => { + beforeAll(initSupertokens); + beforeAll(setupTestDb); + beforeEach(() => { + (isUsingGcalWebhookHttps as jest.Mock).mockReturnValue(true); + }); + beforeEach(cleanupCollections); + afterEach(() => jest.restoreAllMocks()); + afterAll(cleanupTestDb); + + it("returns false when the user has no sync record", async () => { + const user = await UserDriver.createUser(); + + await expect( + isGoogleCalendarSyncHealthy(user._id.toString()), + ).resolves.toBe(false); + }); + + it("returns true when sync records and active Google Watches are present", async () => { + const { user } = await UtilDriver.setupTestUser(); + + await expect( + isGoogleCalendarSyncHealthy(user._id.toString()), + ).resolves.toBe(true); + }); + + it("returns false when a calendar event sync token is missing", async () => { + const { user } = await UtilDriver.setupTestUser(); + const userId = user._id.toString(); + + await updateSync(Resource_Sync.EVENTS, userId, "test-calendar", { + nextSyncToken: undefined, + }); + + await expect(isGoogleCalendarSyncHealthy(userId)).resolves.toBe(false); + }); + + it("returns true without active watches when Public watch notifications are not using HTTPS", async () => { + const { user } = await UtilDriver.setupTestUser(); + const userId = user._id.toString(); + + (isUsingGcalWebhookHttps as jest.Mock).mockReturnValue(false); + await GoogleWatchDriver.removeActiveGoogleWatchesForUser(userId); + + await expect(isGoogleCalendarSyncHealthy(userId)).resolves.toBe(true); + }); + + it("returns false without active watches when Public watch notifications are using HTTPS", async () => { + const { user } = await UtilDriver.setupTestUser(); + const userId = user._id.toString(); + + (isUsingGcalWebhookHttps as jest.Mock).mockReturnValue(true); + await GoogleWatchDriver.removeActiveGoogleWatchesForUser(userId); + + await expect(isGoogleCalendarSyncHealthy(userId)).resolves.toBe(false); + }); +}); diff --git a/packages/backend/src/sync/services/google-sync/google-sync.health.ts b/packages/backend/src/sync/services/google-sync/google-sync.health.ts new file mode 100644 index 000000000..2a315ca28 --- /dev/null +++ b/packages/backend/src/sync/services/google-sync/google-sync.health.ts @@ -0,0 +1,52 @@ +import { Resource_Sync } from "@core/types/sync.types"; +import dayjs from "@core/util/date/dayjs"; +import mongoService from "@backend/common/services/mongo.service"; +import { getSync } from "@backend/sync/services/records/sync-records.repository"; +import { isUsingGcalWebhookHttps } from "@backend/sync/services/watch/google-watch-config"; + +export const isGoogleCalendarSyncHealthy = async ( + userId: string, +): Promise => { + const sync = await getSync({ userId }); + + if (!sync?.google) { + return false; + } + + const eventSyncs = sync.google.events ?? []; + const calendarListSyncs = sync.google.calendarlist ?? []; + + if (eventSyncs.length === 0 || calendarListSyncs.length === 0) { + return false; + } + + if (calendarListSyncs.some(({ nextSyncToken }) => !nextSyncToken)) { + return false; + } + + if (eventSyncs.some(({ nextSyncToken }) => !nextSyncToken)) { + return false; + } + + if (!isUsingGcalWebhookHttps()) { + return true; + } + + const activeWatchCalendarIds = new Set( + (await mongoService.watch.find({ user: userId }).toArray()) + .filter(({ expiration }) => dayjs(expiration).isAfter(dayjs())) + .map(({ gCalendarId }) => gCalendarId), + ); + + if (!activeWatchCalendarIds.has(Resource_Sync.CALENDAR)) { + return false; + } + + return eventSyncs.every(({ gCalendarId }) => + activeWatchCalendarIds.has(gCalendarId), + ); +}; + +export const googleSyncHealth = { + isGoogleCalendarSyncHealthy, +}; diff --git a/packages/backend/src/sync/services/google-calendar-sync/google-calendar-sync.service.test.ts b/packages/backend/src/sync/services/google-sync/google-sync.service.test.ts similarity index 98% rename from packages/backend/src/sync/services/google-calendar-sync/google-calendar-sync.service.test.ts rename to packages/backend/src/sync/services/google-sync/google-sync.service.test.ts index b0ea7edd7..7f20cac42 100644 --- a/packages/backend/src/sync/services/google-calendar-sync/google-calendar-sync.service.test.ts +++ b/packages/backend/src/sync/services/google-sync/google-sync.service.test.ts @@ -7,8 +7,8 @@ import { } from "@backend/__tests__/helpers/mock.db.setup"; import { initSupertokens } from "@backend/common/middleware/supertokens.middleware"; import { sseServer } from "@backend/servers/sse/sse.server"; -import { googleCalendarSyncService } from "@backend/sync/services/google-calendar-sync/google-calendar-sync.service"; -import * as syncImportService from "@backend/sync/services/import/sync.import"; +import { googleCalendarSyncService } from "@backend/sync/services/google-sync/google-sync.service"; +import * as syncImportService from "@backend/sync/services/import/google-import.service"; import { googleWatchService } from "@backend/sync/services/watch/google-watch.service"; import userService from "@backend/user/services/user.service"; import userMetadataService from "@backend/user/services/user-metadata.service"; diff --git a/packages/backend/src/sync/services/google-calendar-sync/google-calendar-sync.service.ts b/packages/backend/src/sync/services/google-sync/google-sync.service.ts similarity index 94% rename from packages/backend/src/sync/services/google-calendar-sync/google-calendar-sync.service.ts rename to packages/backend/src/sync/services/google-sync/google-sync.service.ts index d093404e5..e477724e3 100644 --- a/packages/backend/src/sync/services/google-calendar-sync/google-calendar-sync.service.ts +++ b/packages/backend/src/sync/services/google-sync/google-sync.service.ts @@ -10,15 +10,15 @@ import { getGoogleRepairErrorMessage } from "@backend/common/errors/integration/ import { isInvalidGoogleToken } from "@backend/common/services/gcal/gcal.utils"; import mongoService from "@backend/common/services/mongo.service"; import { sseServer } from "@backend/servers/sse/sse.server"; -import { getGcalClient } from "@backend/sync/services/google-calendar-sync/google.calendar.client"; -import { createSyncImport } from "@backend/sync/services/import/sync.import"; -import compassGoogleMirrorService from "@backend/sync/services/outbound/compass-google-mirror.service"; +import compassToGoogleBackfill from "@backend/sync/services/event-propagation/compass-to-google/compass-to-google-backfill"; +import { getGcalClient } from "@backend/sync/services/google-sync/gcal.client"; +import { createSyncImport } from "@backend/sync/services/import/google-import.service"; +import { updateSync } from "@backend/sync/services/records/sync-records.repository"; import { googleWatchService } from "@backend/sync/services/watch/google-watch.service"; -import { updateSync } from "@backend/sync/util/sync.queries"; -import { isUsingGcalWebhookHttps } from "@backend/sync/util/sync.util"; +import { isUsingGcalWebhookHttps } from "@backend/sync/services/watch/google-watch-config"; import userMetadataService from "@backend/user/services/user-metadata.service"; -const logger = Logger("app:google-calendar-sync.service"); +const logger = Logger("app:google-sync.service"); const activeFullSyncRestarts = new Set(); @@ -190,7 +190,7 @@ async function runGoogleCalendarSyncSetup( const importResults = await googleCalendarSyncService.initializeGoogleCalendarSync(userId); - await compassGoogleMirrorService + await compassToGoogleBackfill .syncCompassEventsToGoogle(userId) .catch((err) => { logger.error( diff --git a/packages/backend/src/sync/services/import/sync.import.full.test.ts b/packages/backend/src/sync/services/import/google-import.full.test.ts similarity index 99% rename from packages/backend/src/sync/services/import/sync.import.full.test.ts rename to packages/backend/src/sync/services/import/google-import.full.test.ts index 2599f740f..718a23606 100644 --- a/packages/backend/src/sync/services/import/sync.import.full.test.ts +++ b/packages/backend/src/sync/services/import/google-import.full.test.ts @@ -9,7 +9,7 @@ import { cleanupTestDb, setupTestDb, } from "@backend/__tests__/helpers/mock.db.setup"; -import { createSyncImport } from "@backend/sync/services/import/sync.import"; +import { createSyncImport } from "@backend/sync/services/import/google-import.service"; describe("SyncImport: Full", () => { beforeAll(setupTestDb); diff --git a/packages/backend/src/sync/services/import/sync.import.series.test.ts b/packages/backend/src/sync/services/import/google-import.series.test.ts similarity index 99% rename from packages/backend/src/sync/services/import/sync.import.series.test.ts rename to packages/backend/src/sync/services/import/google-import.series.test.ts index f30e19310..1e830a2b3 100644 --- a/packages/backend/src/sync/services/import/sync.import.series.test.ts +++ b/packages/backend/src/sync/services/import/google-import.series.test.ts @@ -15,7 +15,7 @@ import { mockRecurringGcalBaseEvent, mockRecurringGcalInstances, } from "@backend/__tests__/mocks.gcal/factories/gcal.event.factory"; -import { createSyncImport } from "@backend/sync/services/import/sync.import"; +import { createSyncImport } from "@backend/sync/services/import/google-import.service"; describe("SyncImport: Series", () => { beforeAll(setupTestDb); diff --git a/packages/backend/src/sync/services/import/sync.import.ts b/packages/backend/src/sync/services/import/google-import.service.ts similarity index 98% rename from packages/backend/src/sync/services/import/sync.import.ts rename to packages/backend/src/sync/services/import/google-import.service.ts index bf4321d8d..273bb8b7a 100644 --- a/packages/backend/src/sync/services/import/sync.import.ts +++ b/packages/backend/src/sync/services/import/google-import.service.ts @@ -25,19 +25,19 @@ import { SyncError } from "@backend/common/errors/sync/sync.errors"; import gcalService from "@backend/common/services/gcal/gcal.service"; import mongoService from "@backend/common/services/mongo.service"; import { getGcalWebhookBaseURL } from "@backend/common/util/api-base-url.util"; -import { getGcalClient } from "@backend/sync/services/google-calendar-sync/google.calendar.client"; -import { type EventsToModify } from "@backend/sync/services/import/sync.import.types"; -import { organizeGcalEventsByType } from "@backend/sync/services/import/sync.import.util"; -import { getCalendarsToSync } from "@backend/sync/services/init/sync.init"; -import { googleWatchService } from "@backend/sync/services/watch/google-watch.service"; +import { getGcalClient } from "@backend/sync/services/google-sync/gcal.client"; +import { type EventsToModify } from "@backend/sync/services/import/google-import.types"; +import { organizeGcalEventsByType } from "@backend/sync/services/import/google-import.util"; +import { getCalendarsToSync } from "@backend/sync/services/init/google-sync-init"; import { getGCalEventsSyncPageToken, getSync, updateSync, -} from "@backend/sync/util/sync.queries"; -import { isUsingGcalWebhookHttps } from "@backend/sync/util/sync.util"; +} from "@backend/sync/services/records/sync-records.repository"; +import { googleWatchService } from "@backend/sync/services/watch/google-watch.service"; +import { isUsingGcalWebhookHttps } from "@backend/sync/services/watch/google-watch-config"; -const logger = Logger("app:sync.import"); +const logger = Logger("app:google-import"); export type WithCompassObjectId = Omit & { _id: ObjectId }; diff --git a/packages/backend/src/sync/services/import/sync.import.types.ts b/packages/backend/src/sync/services/import/google-import.types.ts similarity index 100% rename from packages/backend/src/sync/services/import/sync.import.types.ts rename to packages/backend/src/sync/services/import/google-import.types.ts diff --git a/packages/backend/src/sync/services/import/sync.import.util.test.ts b/packages/backend/src/sync/services/import/google-import.util.test.ts similarity index 53% rename from packages/backend/src/sync/services/import/sync.import.util.test.ts rename to packages/backend/src/sync/services/import/google-import.util.test.ts index 84c9acfd3..c548f8f90 100644 --- a/packages/backend/src/sync/services/import/sync.import.util.test.ts +++ b/packages/backend/src/sync/services/import/google-import.util.test.ts @@ -1,10 +1,7 @@ -import { faker } from "@faker-js/faker"; import { gcalEvents } from "@core/__mocks__/v1/events/gcal/gcal.event"; import { type gSchema$Event } from "@core/types/gcal"; -import dayjs from "@core/util/date/dayjs"; import { cancelledEventsIds } from "@backend/common/services/gcal/gcal.utils"; -import { organizeGcalEventsByType } from "@backend/sync/services/import/sync.import.util"; -import { syncExpired, syncExpiresSoon } from "@backend/sync/util/sync.util"; +import { organizeGcalEventsByType } from "@backend/sync/services/import/google-import.util"; describe("categorizeGcalEvents", () => { const { toDelete, toUpdate } = organizeGcalEventsByType(gcalEvents.items); @@ -50,40 +47,3 @@ describe("categorizeGcalEvents", () => { }); }); }); - -describe("Sync Expiry Checks", () => { - it("returns true if expiry before now", () => { - const expired = dayjs("1675097074000").toDate(); // Jan 30, 2023 - const isExpired = syncExpired(expired); - expect(isExpired).toBe(true); - }); - - it("returns true if expires soon - v1", () => { - const oneMinFromNow = dayjs().add(1, "second").toDate(); - const expiresSoon = syncExpiresSoon(oneMinFromNow); - expect(expiresSoon).toBe(true); - }); - - it("returns true if expires soon - v2", () => { - const oneMinFromNow = dayjs().add(1, "minute").toDate(); - const expiresSoon = syncExpiresSoon(oneMinFromNow); - expect(expiresSoon).toBe(true); - }); - - it("returns true if expires soon - v3", () => { - const oneMinFromNow = dayjs().add(1, "day").toDate(); - const expiresSoon = syncExpiresSoon(oneMinFromNow); - expect(expiresSoon).toBe(true); - }); - it("returns false if expiry after now", () => { - const notExpired = faker.date.future({ years: 10 }); - const isExpired = syncExpired(notExpired); - expect(isExpired).toBe(false); - }); - - it("returns false if doesnt expires soon - v2", () => { - const manyDaysFromNow = faker.date.future({ years: 10 }); - const expiresSoon = syncExpiresSoon(manyDaysFromNow); - expect(expiresSoon).toBe(false); - }); -}); diff --git a/packages/backend/src/sync/services/import/sync.import.util.ts b/packages/backend/src/sync/services/import/google-import.util.ts similarity index 100% rename from packages/backend/src/sync/services/import/sync.import.util.ts rename to packages/backend/src/sync/services/import/google-import.util.ts diff --git a/packages/backend/src/sync/services/init/sync.init.test.ts b/packages/backend/src/sync/services/init/google-sync-init.test.ts similarity index 94% rename from packages/backend/src/sync/services/init/sync.init.test.ts rename to packages/backend/src/sync/services/init/google-sync-init.test.ts index 7f81b249e..ffefcbb50 100644 --- a/packages/backend/src/sync/services/init/sync.init.test.ts +++ b/packages/backend/src/sync/services/init/google-sync-init.test.ts @@ -8,8 +8,8 @@ import { setupTestDb, } from "@backend/__tests__/helpers/mock.db.setup"; import gcalService from "@backend/common/services/gcal/gcal.service"; -import { getGcalClient } from "@backend/sync/services/google-calendar-sync/google.calendar.client"; -import { getCalendarsToSync } from "@backend/sync/services/init/sync.init"; +import { getGcalClient } from "@backend/sync/services/google-sync/gcal.client"; +import { getCalendarsToSync } from "@backend/sync/services/init/google-sync-init"; describe("getCalendarsToSync", () => { beforeEach(setupTestDb); diff --git a/packages/backend/src/sync/services/init/sync.init.ts b/packages/backend/src/sync/services/init/google-sync-init.ts similarity index 100% rename from packages/backend/src/sync/services/init/sync.init.ts rename to packages/backend/src/sync/services/init/google-sync-init.ts diff --git a/packages/backend/src/sync/services/notify/handler/gcal.notification.handler.ts b/packages/backend/src/sync/services/notify/handler/gcal.notification.handler.ts index 82a380a86..b93291a76 100644 --- a/packages/backend/src/sync/services/notify/handler/gcal.notification.handler.ts +++ b/packages/backend/src/sync/services/notify/handler/gcal.notification.handler.ts @@ -2,9 +2,9 @@ import { Logger } from "@core/logger/winston.logger"; import { type gCalendar } from "@core/types/gcal"; import { Resource_Sync } from "@core/types/sync.types"; import gcalService from "@backend/common/services/gcal/gcal.service"; -import { GcalSyncProcessor } from "@backend/sync/services/sync/google/gcal.sync.processor"; +import { GoogleToCompassEventPropagation } from "@backend/sync/services/event-propagation/google-to-compass/google-to-compass.event-propagation"; +import { updateSync } from "@backend/sync/services/records/sync-records.repository"; import { type Summary_Sync } from "@backend/sync/sync.types"; -import { updateSync } from "@backend/sync/util/sync.queries"; const logger = Logger("app:gcal.notification.handler"); @@ -29,7 +29,7 @@ export class GCalNotificationHandler { const { hasChanges, changes } = await this.getLatestChanges(); if (hasChanges) { - const processor = new GcalSyncProcessor(this.userId); + const processor = new GoogleToCompassEventPropagation(this.userId); const changeSummary = await processor.processEvents(changes); console.log("PROCESSED:", changeSummary); return { summary: "PROCESSED", changes: changeSummary }; diff --git a/packages/backend/src/sync/services/public-watch-notifications/public-watch-notification.ingress.test.ts b/packages/backend/src/sync/services/public-watch-notifications/public-watch-notification.ingress.test.ts new file mode 100644 index 000000000..fe44b679b --- /dev/null +++ b/packages/backend/src/sync/services/public-watch-notifications/public-watch-notification.ingress.test.ts @@ -0,0 +1,53 @@ +import { ObjectId } from "mongodb"; +import { Resource_Sync, XGoogleResourceState } from "@core/types/sync.types"; +import { encodeChannelToken } from "@backend/sync/services/watch/google-watch-token"; +import { + hasPublicWatchNotificationHeaders, + parsePublicWatchNotification, +} from "./public-watch-notification.ingress"; + +const makeHeaders = (overrides: Record = {}) => ({ + "x-goog-channel-id": new ObjectId().toString(), + "x-goog-channel-token": encodeChannelToken({ + resource: Resource_Sync.EVENTS, + }), + "x-goog-resource-id": "resource-id", + "x-goog-resource-state": XGoogleResourceState.EXISTS, + "x-goog-channel-expiration": new Date("2035-01-01").toISOString(), + ...overrides, +}); + +describe("publicWatchNotificationIngress", () => { + it("parses valid Public watch notification headers", () => { + const headers = makeHeaders(); + + expect(parsePublicWatchNotification(headers)).toEqual({ + resource: Resource_Sync.EVENTS, + channelId: new ObjectId(headers["x-goog-channel-id"]), + resourceId: "resource-id", + resourceState: XGoogleResourceState.EXISTS, + expiration: new Date("2035-01-01"), + }); + }); + + it("returns undefined when a required Google header is missing", () => { + const headers = makeHeaders({ "x-goog-resource-id": undefined }); + + expect(hasPublicWatchNotificationHeaders(headers)).toBe(false); + expect(parsePublicWatchNotification(headers)).toBeUndefined(); + }); + + it("returns undefined when the channel token is invalid", () => { + const headers = makeHeaders({ "x-goog-channel-token": "not-base64" }); + + expect(parsePublicWatchNotification(headers)).toBeUndefined(); + }); + + it("throws when the Google payload shape is invalid", () => { + const headers = makeHeaders({ + "x-goog-resource-state": "surprise", + }); + + expect(() => parsePublicWatchNotification(headers)).toThrow(); + }); +}); diff --git a/packages/backend/src/sync/services/public-watch-notifications/public-watch-notification.ingress.ts b/packages/backend/src/sync/services/public-watch-notifications/public-watch-notification.ingress.ts new file mode 100644 index 000000000..c96b9e0f7 --- /dev/null +++ b/packages/backend/src/sync/services/public-watch-notifications/public-watch-notification.ingress.ts @@ -0,0 +1,107 @@ +import { type NextFunction, type Request, type Response } from "express"; +import { ZodError } from "zod/v4"; +import { Status } from "@core/errors/status.codes"; +import { + GcalNotificationSchema, + type Payload_Sync_Notif, +} from "@core/types/sync.types"; +import { error } from "@backend/common/errors/handlers/error.handler"; +import { GcalError } from "@backend/common/errors/integration/gcal/gcal.errors"; +import { decodeChannelToken } from "@backend/sync/services/watch/google-watch-token"; +import { type IncomingHttpHeaders } from "node:http"; + +type PublicWatchNotificationLocals = { + publicWatchNotification?: Payload_Sync_Notif; +}; + +const GOOGLE_NOTIFICATION_HEADERS = [ + "x-goog-channel-id", + "x-goog-channel-token", + "x-goog-resource-id", + "x-goog-resource-state", + "x-goog-channel-expiration", +]; + +const getHeader = ( + headers: IncomingHttpHeaders, + name: string, +): string | undefined => { + const value = headers[name]; + + if (Array.isArray(value)) return value[0]; + + return value; +}; + +export const hasPublicWatchNotificationHeaders = ( + headers: IncomingHttpHeaders, +) => { + return GOOGLE_NOTIFICATION_HEADERS.every((header) => + Boolean(getHeader(headers, header)), + ); +}; + +export const parsePublicWatchNotification = ( + headers: IncomingHttpHeaders, +): Payload_Sync_Notif | undefined => { + if (!hasPublicWatchNotificationHeaders(headers)) { + return undefined; + } + + const token = getHeader(headers, "x-goog-channel-token"); + const decoded = token ? decodeChannelToken(token) : undefined; + + if (!decoded) { + return undefined; + } + + return GcalNotificationSchema.parse({ + resource: decoded.resource, + channelId: getHeader(headers, "x-goog-channel-id"), + resourceId: getHeader(headers, "x-goog-resource-id"), + resourceState: getHeader(headers, "x-goog-resource-state"), + expiration: new Date(getHeader(headers, "x-goog-channel-expiration") ?? ""), + }); +}; + +const getLocals = (res: Response): PublicWatchNotificationLocals => + res.locals as PublicWatchNotificationLocals; + +const verify = (req: Request, res: Response, next: NextFunction) => { + try { + const notification = parsePublicWatchNotification(req.headers); + + if (!notification) { + res.status(Status.FORBIDDEN).send({ + error: error(GcalError.Unauthorized, "Notification Failed"), + }); + return; + } + + getLocals(res).publicWatchNotification = notification; + + next(); + } catch (err) { + if (err instanceof ZodError) { + res.status(Status.FORBIDDEN).send("Invalid notification payload"); + return; + } + + next(err); + } +}; + +const getNotification = (res: Response): Payload_Sync_Notif => { + const notification = getLocals(res).publicWatchNotification; + + if (!notification) { + throw error(GcalError.Unauthorized, "Notification Failed"); + } + + return notification; +}; + +export const publicWatchNotificationIngress = { + getNotification, + verify, +}; diff --git a/packages/backend/src/sync/util/sync.queries.test.ts b/packages/backend/src/sync/services/records/sync-records.repository.persistence.test.ts similarity index 98% rename from packages/backend/src/sync/util/sync.queries.test.ts rename to packages/backend/src/sync/services/records/sync-records.repository.persistence.test.ts index 56a990a69..ec3b1e40e 100644 --- a/packages/backend/src/sync/util/sync.queries.test.ts +++ b/packages/backend/src/sync/services/records/sync-records.repository.persistence.test.ts @@ -11,9 +11,9 @@ import { getGCalEventsSyncPageToken, getSync, updateSync, -} from "@backend/sync/util/sync.queries"; +} from "@backend/sync/services/records/sync-records.repository"; -describe("sync.queries: ", () => { +describe("syncRecords: ", () => { describe("nextPageToken", () => { beforeAll(setupTestDb); diff --git a/packages/backend/src/sync/services/records/sync.records.test.ts b/packages/backend/src/sync/services/records/sync-records.repository.test.ts similarity index 98% rename from packages/backend/src/sync/services/records/sync.records.test.ts rename to packages/backend/src/sync/services/records/sync-records.repository.test.ts index 5a3ebd62b..b7570a7bf 100644 --- a/packages/backend/src/sync/services/records/sync.records.test.ts +++ b/packages/backend/src/sync/services/records/sync-records.repository.test.ts @@ -7,7 +7,7 @@ import { } from "@backend/__tests__/helpers/mock.db.setup"; import { initSupertokens } from "@backend/common/middleware/supertokens.middleware"; import mongoService from "@backend/common/services/mongo.service"; -import syncRecords from "./sync.records"; +import syncRecords from "./sync-records.repository"; describe("syncRecords", () => { beforeAll(initSupertokens); diff --git a/packages/backend/src/sync/util/sync.queries.ts b/packages/backend/src/sync/services/records/sync-records.repository.ts similarity index 78% rename from packages/backend/src/sync/util/sync.queries.ts rename to packages/backend/src/sync/services/records/sync-records.repository.ts index 2da4e70db..15412b30b 100644 --- a/packages/backend/src/sync/util/sync.queries.ts +++ b/packages/backend/src/sync/services/records/sync-records.repository.ts @@ -4,19 +4,14 @@ import { type UpdateResult, } from "mongodb"; import zod from "zod"; -import { Origin } from "@core/constants/core.constants"; import { Resource_Sync, type Schema_Sync, type SyncDetails, } from "@core/types/sync.types"; -import dayjs from "@core/util/date/dayjs"; +import { Collections } from "@backend/common/constants/collections"; import mongoService from "@backend/common/services/mongo.service"; -/** - * Helper funcs that predominately query/update the DB - */ - export const resourceValidationSchema = zod .enum([Resource_Sync.EVENTS, Resource_Sync.CALENDAR]) .default(Resource_Sync.EVENTS); @@ -91,43 +86,14 @@ export const getGCalEventsSyncPageToken = async ( ?.nextPageToken; }; -export const hasUpdatedCompassEventRecently = async ( - userId: string, - deadline: string, -) => { - const recentChanges = await mongoService.event.countDocuments({ - user: userId, - origin: Origin.COMPASS, - updatedAt: { $gt: new Date(deadline) }, - }); - - return recentChanges > 0; -}; - -export const isWatchingGoogleResource = async ( - userId: string, - gCalendarId: string, - session?: ClientSession, -) => { - const channel = await mongoService.watch.findOne( - { user: userId, gCalendarId }, - { session }, - ); - - if (!channel) return false; - - const expired = dayjs(channel.expiration).isSameOrBefore(dayjs()); - - if (expired) { - await mongoService.watch.deleteOne( - { user: userId, gCalendarId }, - { session }, - ); +export const canDoIncrementalSync = (sync: Schema_Sync) => { + const events = sync.google?.events; + if (!events || events.length === 0) { return false; } - return true; + return events.every((event) => event.nextSyncToken !== null); }; export const updateSync = async ( @@ -173,3 +139,36 @@ export const updateSync = async ( return response; }; + +export const deleteAllByGcalId = ( + gCalendarId: string, + session?: ClientSession, +) => { + return mongoService.sync.deleteMany( + { "google.events.gCalendarId": gCalendarId }, + { session }, + ); +}; + +export const deleteAllByUser = (userId: string, session?: ClientSession) => { + return mongoService.sync.deleteMany({ user: userId }, { session }); +}; + +export const deleteByIntegration = (integration: "google", userId: string) => { + return mongoService.db + .collection(Collections.SYNC) + .updateOne({ user: userId }, { $unset: { [integration]: "" } }); +}; + +const syncRecords = { + canDoIncrementalSync, + deleteAllByGcalId, + deleteAllByUser, + deleteByIntegration, + getGCalEventsSyncPageToken, + getSync, + getSyncByToken, + updateSync, +}; + +export default syncRecords; diff --git a/packages/backend/src/sync/services/records/sync.records.ts b/packages/backend/src/sync/services/records/sync.records.ts deleted file mode 100644 index fc01347dd..000000000 --- a/packages/backend/src/sync/services/records/sync.records.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { type ClientSession } from "mongodb"; -import { Collections } from "@backend/common/constants/collections"; -import mongoService from "@backend/common/services/mongo.service"; - -export const deleteAllByGcalId = ( - gCalendarId: string, - session?: ClientSession, -) => { - return mongoService.sync.deleteMany( - { "google.events.gCalendarId": gCalendarId }, - { session }, - ); -}; - -export const deleteAllByUser = (userId: string, session?: ClientSession) => { - return mongoService.sync.deleteMany({ user: userId }, { session }); -}; - -export const deleteByIntegration = (integration: "google", userId: string) => { - return mongoService.db - .collection(Collections.SYNC) - .updateOne({ user: userId }, { $unset: { [integration]: "" } }); -}; - -const syncRecords = { - deleteAllByGcalId, - deleteAllByUser, - deleteByIntegration, -}; - -export default syncRecords; diff --git a/packages/backend/src/sync/services/watch/google-watch-activity.ts b/packages/backend/src/sync/services/watch/google-watch-activity.ts new file mode 100644 index 000000000..4fe72bbab --- /dev/null +++ b/packages/backend/src/sync/services/watch/google-watch-activity.ts @@ -0,0 +1,15 @@ +import { Origin } from "@core/constants/core.constants"; +import mongoService from "@backend/common/services/mongo.service"; + +export const hasUpdatedCompassEventRecently = async ( + userId: string, + deadline: string, +) => { + const recentChanges = await mongoService.event.countDocuments({ + user: userId, + origin: Origin.COMPASS, + updatedAt: { $gt: new Date(deadline) }, + }); + + return recentChanges > 0; +}; diff --git a/packages/backend/src/sync/services/watch/google-watch-config.ts b/packages/backend/src/sync/services/watch/google-watch-config.ts new file mode 100644 index 000000000..ca3d4bb73 --- /dev/null +++ b/packages/backend/src/sync/services/watch/google-watch-config.ts @@ -0,0 +1,4 @@ +import { getGcalWebhookBaseURL } from "@backend/common/util/api-base-url.util"; + +export const isUsingGcalWebhookHttps = () => + getGcalWebhookBaseURL().startsWith("https://"); diff --git a/packages/backend/src/sync/services/watch/google-watch-maintenance.planner.ts b/packages/backend/src/sync/services/watch/google-watch-maintenance.planner.ts index 3577f6bf8..2247c0449 100644 --- a/packages/backend/src/sync/services/watch/google-watch-maintenance.planner.ts +++ b/packages/backend/src/sync/services/watch/google-watch-maintenance.planner.ts @@ -8,11 +8,14 @@ import { isInvalidGoogleToken, } from "@backend/common/services/gcal/gcal.utils"; import mongoService from "@backend/common/services/mongo.service"; -import { getGcalClient } from "@backend/sync/services/google-calendar-sync/google.calendar.client"; -import { googleCalendarSyncService } from "@backend/sync/services/google-calendar-sync/google-calendar-sync.service"; +import { getGcalClient } from "@backend/sync/services/google-sync/gcal.client"; +import { googleCalendarSyncService } from "@backend/sync/services/google-sync/google-sync.service"; import { googleWatchService } from "@backend/sync/services/watch/google-watch.service"; -import { hasUpdatedCompassEventRecently } from "@backend/sync/util/sync.queries"; -import { syncExpired, syncExpiresSoon } from "@backend/sync/util/sync.util"; +import { hasUpdatedCompassEventRecently } from "@backend/sync/services/watch/google-watch-activity"; +import { + syncExpired, + syncExpiresSoon, +} from "@backend/sync/services/watch/google-watch-timing"; import userService from "@backend/user/services/user.service"; const logger = Logger("app:google-watch-maintenance.planner"); diff --git a/packages/backend/src/sync/services/watch/google-watch-maintenance.service.ts b/packages/backend/src/sync/services/watch/google-watch-maintenance.service.ts index 30526e999..44fa4c0bc 100644 --- a/packages/backend/src/sync/services/watch/google-watch-maintenance.service.ts +++ b/packages/backend/src/sync/services/watch/google-watch-maintenance.service.ts @@ -2,12 +2,12 @@ import { type ObjectId } from "mongodb"; import { Logger } from "@core/logger/winston.logger"; import { MONGO_BATCH_SIZE } from "@backend/common/constants/backend.constants"; import mongoService from "@backend/common/services/mongo.service"; +import { createConcurrencyLimiter } from "@backend/common/util/concurrency-limiter.util"; import { prepWatchMaintenanceForUser, pruneSync, refreshWatch, } from "@backend/sync/services/watch/google-watch-maintenance.planner"; -import { createConcurrencyLimiter } from "@backend/sync/util/sync.util"; import { findCompassUserBy } from "@backend/user/queries/user.queries"; const logger = Logger("app:google-watch-maintenance.service"); diff --git a/packages/backend/src/sync/services/watch/google-watch-state.ts b/packages/backend/src/sync/services/watch/google-watch-state.ts new file mode 100644 index 000000000..54e9202ce --- /dev/null +++ b/packages/backend/src/sync/services/watch/google-watch-state.ts @@ -0,0 +1,29 @@ +import { type ClientSession } from "mongodb"; +import dayjs from "@core/util/date/dayjs"; +import mongoService from "@backend/common/services/mongo.service"; + +export const isWatchingGoogleResource = async ( + userId: string, + gCalendarId: string, + session?: ClientSession, +) => { + const watch = await mongoService.watch.findOne( + { user: userId, gCalendarId }, + { session }, + ); + + if (!watch) return false; + + const expired = dayjs(watch.expiration).isSameOrBefore(dayjs()); + + if (expired) { + await mongoService.watch.deleteOne( + { user: userId, gCalendarId }, + { session }, + ); + + return false; + } + + return true; +}; diff --git a/packages/backend/src/sync/services/watch/google-watch-timing.test.ts b/packages/backend/src/sync/services/watch/google-watch-timing.test.ts new file mode 100644 index 000000000..188bc0a9e --- /dev/null +++ b/packages/backend/src/sync/services/watch/google-watch-timing.test.ts @@ -0,0 +1,40 @@ +import { faker } from "@faker-js/faker"; +import dayjs from "@core/util/date/dayjs"; +import { + syncExpired, + syncExpiresSoon, +} from "@backend/sync/services/watch/google-watch-timing"; + +describe("googleWatchTiming", () => { + describe("syncExpired", () => { + it("returns true if expiry before now", () => { + const expired = dayjs("1675097074000").toDate(); // Jan 30, 2023 + const isExpired = syncExpired(expired); + + expect(isExpired).toBe(true); + }); + + it("returns false if expiry after now", () => { + const notExpired = faker.date.future({ years: 10 }); + const isExpired = syncExpired(notExpired); + + expect(isExpired).toBe(false); + }); + }); + + describe("syncExpiresSoon", () => { + it("returns true when expiration is close", () => { + const oneMinuteFromNow = dayjs().add(1, "minute").toDate(); + const expiresSoon = syncExpiresSoon(oneMinuteFromNow); + + expect(expiresSoon).toBe(true); + }); + + it("returns false when expiration is far away", () => { + const manyDaysFromNow = faker.date.future({ years: 10 }); + const expiresSoon = syncExpiresSoon(manyDaysFromNow); + + expect(expiresSoon).toBe(false); + }); + }); +}); diff --git a/packages/backend/src/sync/services/watch/google-watch-timing.ts b/packages/backend/src/sync/services/watch/google-watch-timing.ts new file mode 100644 index 000000000..db82c3eb3 --- /dev/null +++ b/packages/backend/src/sync/services/watch/google-watch-timing.ts @@ -0,0 +1,33 @@ +import { Logger } from "@core/logger/winston.logger"; +import dayjs from "@core/util/date/dayjs"; +import { SYNC_BUFFER_DAYS } from "@backend/common/constants/backend.constants"; +import { ENV } from "@backend/common/constants/env.constants"; + +const logger = Logger("app:google-watch-timing"); + +export const getChannelExpiration = (): string => { + const numMin = parseInt(ENV.CHANNEL_EXPIRATION_MIN, 10); + const expiration = dayjs().add(numMin, "minutes"); + + logExpirationReminder(numMin); + + return expiration.valueOf().toString(); +}; + +export const logExpirationReminder = (min: number) => { + const hours = Math.round((min / 60) * 100) / 100; + const days = Math.round((hours / 24) * 100) / 100; + + const label = hours > 24 ? `${days} days` : `${hours} hours`; + logger.debug(`REMINDER: Channel will expire in ${min} minutes (${label})`); +}; + +export const syncExpired = (expiration: Date) => { + return dayjs(expiration).isSameOrBefore(dayjs()); +}; + +export const syncExpiresSoon = (expiration: Date) => { + const deadline = dayjs().add(SYNC_BUFFER_DAYS, "days"); + + return dayjs(expiration).isSameOrBefore(deadline); +}; diff --git a/packages/backend/src/sync/util/watch.util.test.ts b/packages/backend/src/sync/services/watch/google-watch-token.test.ts similarity index 97% rename from packages/backend/src/sync/util/watch.util.test.ts rename to packages/backend/src/sync/services/watch/google-watch-token.test.ts index 5e66e3da8..ed365afa7 100644 --- a/packages/backend/src/sync/util/watch.util.test.ts +++ b/packages/backend/src/sync/services/watch/google-watch-token.test.ts @@ -4,14 +4,14 @@ import { ENV } from "@backend/common/constants/env.constants"; import { decodeChannelToken, encodeChannelToken, -} from "@backend/sync/util/watch.util"; +} from "@backend/sync/services/watch/google-watch-token"; // Mock ENV jest.mock("@backend/common/constants/env.constants", () => ({ ENV: { TOKEN_GCAL_NOTIFICATION: "test-notification-token" }, })); -describe("watch.util", () => { +describe("google-watch-token", () => { const notificationToken = ENV.TOKEN_GCAL_NOTIFICATION; describe("encodeChannelToken", () => { diff --git a/packages/backend/src/sync/util/watch.util.ts b/packages/backend/src/sync/services/watch/google-watch-token.ts similarity index 96% rename from packages/backend/src/sync/util/watch.util.ts rename to packages/backend/src/sync/services/watch/google-watch-token.ts index 38ef6f175..1cb4fd3c0 100644 --- a/packages/backend/src/sync/util/watch.util.ts +++ b/packages/backend/src/sync/services/watch/google-watch-token.ts @@ -3,7 +3,7 @@ import { zBase64String } from "@core/types/type.utils"; import { type ChannelToken, ChannelTokenSchema } from "@core/types/watch.types"; import { ENV } from "@backend/common/constants/env.constants"; -const logger = Logger("app:sync.watch.util"); +const logger = Logger("app:google-watch-token"); export function encodeChannelToken( _channelData: Omit & { token?: string }, diff --git a/packages/backend/src/sync/services/watch/google-watch.service.test.ts b/packages/backend/src/sync/services/watch/google-watch.service.test.ts index d1909aa2e..a984efe32 100644 --- a/packages/backend/src/sync/services/watch/google-watch.service.test.ts +++ b/packages/backend/src/sync/services/watch/google-watch.service.test.ts @@ -14,10 +14,12 @@ import { initSupertokens } from "@backend/common/middleware/supertokens.middlewa import gcalService from "@backend/common/services/gcal/gcal.service"; import mongoService from "@backend/common/services/mongo.service"; import { googleWatchService } from "@backend/sync/services/watch/google-watch.service"; -import { isUsingGcalWebhookHttps } from "@backend/sync/util/sync.util"; +import { isUsingGcalWebhookHttps } from "@backend/sync/services/watch/google-watch-config"; -jest.mock("@backend/sync/util/sync.util", () => { - const actual = jest.requireActual("@backend/sync/util/sync.util"); +jest.mock("@backend/sync/services/watch/google-watch-config", () => { + const actual = jest.requireActual( + "@backend/sync/services/watch/google-watch-config", + ); return { ...actual, isUsingGcalWebhookHttps: jest.fn(() => actual.isUsingGcalWebhookHttps()), diff --git a/packages/backend/src/sync/services/watch/google-watch.service.ts b/packages/backend/src/sync/services/watch/google-watch.service.ts index b91d5efd8..90b72d4c3 100644 --- a/packages/backend/src/sync/services/watch/google-watch.service.ts +++ b/packages/backend/src/sync/services/watch/google-watch.service.ts @@ -22,17 +22,13 @@ import { } from "@backend/common/services/gcal/gcal.utils"; import mongoService from "@backend/common/services/mongo.service"; import { sseServer } from "@backend/servers/sse/sse.server"; -import { getGcalClient } from "@backend/sync/services/google-calendar-sync/google.calendar.client"; +import { getGcalClient } from "@backend/sync/services/google-sync/gcal.client"; +import { isMissingGoogleRefreshToken } from "@backend/sync/services/google-sync/google-sync.errors"; import { GCalNotificationHandler } from "@backend/sync/services/notify/handler/gcal.notification.handler"; -import { - getSync, - isWatchingGoogleResource, -} from "@backend/sync/util/sync.queries"; -import { - getChannelExpiration, - isMissingGoogleRefreshToken, - isUsingGcalWebhookHttps, -} from "@backend/sync/util/sync.util"; +import { getSync } from "@backend/sync/services/records/sync-records.repository"; +import { isUsingGcalWebhookHttps } from "@backend/sync/services/watch/google-watch-config"; +import { isWatchingGoogleResource } from "@backend/sync/services/watch/google-watch-state"; +import { getChannelExpiration } from "@backend/sync/services/watch/google-watch-timing"; import { findCompassUserBy } from "@backend/user/queries/user.queries"; const logger = Logger("app:google-watch.service"); diff --git a/packages/backend/src/sync/sync.routes.config.ts b/packages/backend/src/sync/sync.routes.config.ts index 591e3e3b4..deffbbee1 100644 --- a/packages/backend/src/sync/sync.routes.config.ts +++ b/packages/backend/src/sync/sync.routes.config.ts @@ -12,6 +12,7 @@ import { } from "@backend/common/middleware/google.required.middleware"; import { SyncController } from "@backend/sync/controllers/sync.controller"; import syncDebugController from "@backend/sync/controllers/sync.debug.controller"; +import { publicWatchNotificationIngress } from "@backend/sync/services/public-watch-notifications/public-watch-notification.ingress"; export class SyncRoutes extends CommonRoutesConfig { constructor(app: express.Application) { @@ -25,7 +26,7 @@ export class SyncRoutes extends CommonRoutesConfig { this.app .route(`/api${GCAL_NOTIFICATION_ENDPOINT}`) .post([ - authMiddleware.verifyIsFromGoogle, + publicWatchNotificationIngress.verify, SyncController.handleGoogleNotification, ]); diff --git a/packages/backend/src/sync/util/sync.util.ts b/packages/backend/src/sync/util/sync.util.ts deleted file mode 100644 index c726080c7..000000000 --- a/packages/backend/src/sync/util/sync.util.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { BaseError } from "@core/errors/errors.base"; -import { Logger } from "@core/logger/winston.logger"; -import { type Schema_Sync } from "@core/types/sync.types"; -import dayjs from "@core/util/date/dayjs"; -import { SYNC_BUFFER_DAYS } from "@backend/common/constants/backend.constants"; -import { ENV, getApiBaseURL } from "@backend/common/constants/env.constants"; -import { UserError } from "@backend/common/errors/user/user.errors"; -import { getGcalWebhookBaseURL } from "@backend/common/util/api-base-url.util"; - -const logger = Logger("app:sync.helpers"); - -/** - * Helper functions that are used by the sync service - * or multiple parts of the sync service's components - */ - -/** - * getChannelExpiration - * - * Calculates the channel expiration date based on the - * CHANNEL_EXPIRATION_MIN environment variable. - * - * @returns {string} Channel expiration as a string representing a Unix timestamp in milliseconds. - */ -export const getChannelExpiration = (): string => { - const numMin = parseInt(ENV.CHANNEL_EXPIRATION_MIN, 10); - const expiration = dayjs().add(numMin, "minutes"); - - logExpirationReminder(numMin); - - return expiration.valueOf().toString(); -}; - -export const hasGoogleHeaders = (headers: object) => { - const expected = [ - "x-goog-channel-id", - "x-goog-resource-id", - "x-goog-resource-state", - "x-goog-channel-expiration", - ]; - - const hasHeaders = expected.every((i) => i in headers); - - return hasHeaders; -}; - -/** - * Determines if incremental sync can be performed for a sync record. - * - * Returns true only if: - * - Sync record exists - * - Google events array exists and is not empty - * - Every calendar event has a non-null nextSyncToken - * - * Returns false if: - * - Sync record is missing Google events data - * - Any calendar event is missing a sync token - * - Events array is empty (no calendars to sync) - * - * This is used to determine if a user needs a full restart sync - * (RECONNECT_REPAIR) vs incremental sync (SIGNIN_INCREMENTAL). - */ -export const canDoIncrementalSync = (sync: Schema_Sync) => { - const events = sync.google?.events; - - // If no events array exists, cannot do incremental sync - if (!events || events.length === 0) { - return false; - } - - // All events must have a sync token for incremental sync - return events.every((event) => event.nextSyncToken !== null); -}; - -type ConcurrencyLimiter = ( - task: () => PromiseLike | Result, -) => Promise; - -export const createConcurrencyLimiter = ( - concurrency: number, -): ConcurrencyLimiter => { - if (!Number.isInteger(concurrency) || concurrency < 1) { - throw new RangeError("Concurrency must be an integer greater than 0"); - } - - let activeCount = 0; - const queue: Array<() => void> = []; - - const runNext = () => { - activeCount -= 1; - queue.shift()?.(); - }; - - return async ( - task: () => PromiseLike | Result, - ): Promise => { - if (activeCount >= concurrency) { - await new Promise((resolve) => { - queue.push(resolve); - }); - } - - activeCount += 1; - - try { - return await task(); - } finally { - runNext(); - } - }; -}; - -export const isUsingHttps = () => getApiBaseURL().startsWith("https://"); - -export const isUsingGcalWebhookHttps = () => - getGcalWebhookBaseURL().startsWith("https://"); - -export const logExpirationReminder = (min: number) => { - const hours = Math.round((min / 60) * 100) / 100; - const days = Math.round((hours / 24) * 100) / 100; - - const label = hours > 24 ? `${days} days` : `${hours} hours`; - logger.debug(`REMINDER: Channel will expire in ${min} minutes (${label})`); -}; - -export const syncExpired = (expiration: Date) => { - return dayjs(expiration).isSameOrBefore(dayjs()); -}; - -export const syncExpiresSoon = (expiration: Date) => { - const deadline = dayjs().add(SYNC_BUFFER_DAYS, "days"); - - return dayjs(expiration).isSameOrBefore(deadline); -}; - -export const isMissingGoogleRefreshToken = ( - error: unknown, -): error is BaseError => { - return ( - error instanceof BaseError && - error.description === UserError.MissingGoogleRefreshToken.description - ); -}; diff --git a/packages/backend/src/user/services/user-metadata.service.test.ts b/packages/backend/src/user/services/user-metadata.service.test.ts index d6c205b25..7eef621d3 100644 --- a/packages/backend/src/user/services/user-metadata.service.test.ts +++ b/packages/backend/src/user/services/user-metadata.service.test.ts @@ -1,19 +1,19 @@ +import { GoogleWatchDriver } from "@backend/__tests__/drivers/google-watch.driver"; import { UserDriver } from "@backend/__tests__/drivers/user.driver"; import { UserMetadataServiceDriver } from "@backend/__tests__/drivers/user-metadata.service.driver"; import { UtilDriver } from "@backend/__tests__/drivers/util.driver"; -import { WatchDriver } from "@backend/__tests__/drivers/watch.driver"; import { cleanupCollections, cleanupTestDb, setupTestDb, } from "@backend/__tests__/helpers/mock.db.setup"; import { initSupertokens } from "@backend/common/middleware/supertokens.middleware"; -import { googleCalendarSyncService } from "@backend/sync/services/google-calendar-sync/google-calendar-sync.service"; -import { isUsingGcalWebhookHttps } from "@backend/sync/util/sync.util"; +import { googleCalendarSyncService } from "@backend/sync/services/google-sync/google-sync.service"; +import { isUsingGcalWebhookHttps } from "@backend/sync/services/watch/google-watch-config"; // eslint-disable-next-line @typescript-eslint/no-unsafe-return -- mock factory spreads requireActual -jest.mock("@backend/sync/util/sync.util", () => ({ - ...jest.requireActual("@backend/sync/util/sync.util"), +jest.mock("@backend/sync/services/watch/google-watch-config", () => ({ + ...jest.requireActual("@backend/sync/services/watch/google-watch-config"), isUsingGcalWebhookHttps: jest.fn(), })); @@ -93,7 +93,7 @@ describe("UserMetadataService", () => { const isUsingGcalWebhookHttpsSpy = isUsingGcalWebhookHttps as jest.Mock; isUsingGcalWebhookHttpsSpy.mockReturnValue(false); - await WatchDriver.deleteManyByUser(userId); + await GoogleWatchDriver.removeActiveGoogleWatchesForUser(userId); const metadata = await driver.fetchUserMetadata(userId); @@ -108,7 +108,7 @@ describe("UserMetadataService", () => { const isUsingGcalWebhookHttpsSpy = isUsingGcalWebhookHttps as jest.Mock; isUsingGcalWebhookHttpsSpy.mockReturnValue(true); - await WatchDriver.deleteManyByUser(userId); + await GoogleWatchDriver.removeActiveGoogleWatchesForUser(userId); const metadata = await driver.fetchUserMetadata(userId); diff --git a/packages/backend/src/user/services/user-metadata.service.ts b/packages/backend/src/user/services/user-metadata.service.ts index f325b5466..67e4f04c9 100644 --- a/packages/backend/src/user/services/user-metadata.service.ts +++ b/packages/backend/src/user/services/user-metadata.service.ts @@ -2,15 +2,11 @@ import mergeWith from "lodash.mergewith"; import SupertokensUserMetadata, { type JSONObject, } from "supertokens-node/recipe/usermetadata"; -import { Resource_Sync } from "@core/types/sync.types"; import { type GoogleConnectionState, type UserMetadata, } from "@core/types/user.types"; -import dayjs from "@core/util/date/dayjs"; -import mongoService from "@backend/common/services/mongo.service"; -import { getSync } from "@backend/sync/util/sync.queries"; -import { isUsingGcalWebhookHttps } from "@backend/sync/util/sync.util"; +import { isGoogleCalendarSyncHealthy } from "@backend/sync/services/google-sync/google-sync.health"; import { findCompassUserBy } from "@backend/user/queries/user.queries"; import { type GetUserMetadataResponse } from "@backend/user/types/user.types"; @@ -34,47 +30,6 @@ class UserMetadataService { return result.metadata; }; - private async isGoogleSyncHealthy(userId: string): Promise { - const sync = await getSync({ userId }); - - if (!sync?.google) { - return false; - } - - const eventSyncs = sync.google.events ?? []; - const calendarListSyncs = sync.google.calendarlist ?? []; - - if (eventSyncs.length === 0 || calendarListSyncs.length === 0) { - return false; - } - - if (calendarListSyncs.some(({ nextSyncToken }) => !nextSyncToken)) { - return false; - } - - if (eventSyncs.some(({ nextSyncToken }) => !nextSyncToken)) { - return false; - } - - if (!isUsingGcalWebhookHttps()) { - return true; - } - - const activeWatchCalendarIds = new Set( - (await mongoService.watch.find({ user: userId }).toArray()) - .filter(({ expiration }) => dayjs(expiration).isAfter(dayjs())) - .map(({ gCalendarId }) => gCalendarId), - ); - - if (!activeWatchCalendarIds.has(Resource_Sync.CALENDAR)) { - return false; - } - - return eventSyncs.every(({ gCalendarId }) => - activeWatchCalendarIds.has(gCalendarId), - ); - } - assessGoogleMetadata = async ( userId: string, metadata?: UserMetadata, @@ -102,7 +57,7 @@ class UserMetadataService { return { connectionState: "ATTENTION" }; } - const isHealthy = await this.isGoogleSyncHealthy(userId); + const isHealthy = await isGoogleCalendarSyncHealthy(userId); if (isHealthy) { return { connectionState: "HEALTHY" }; } diff --git a/packages/backend/src/user/services/user.service.test.ts b/packages/backend/src/user/services/user.service.test.ts index 46984b408..a00101ec5 100644 --- a/packages/backend/src/user/services/user.service.test.ts +++ b/packages/backend/src/user/services/user.service.test.ts @@ -4,7 +4,7 @@ import SupertokensUserMetadata from "supertokens-node/recipe/usermetadata"; import { CompassCalendarSchema } from "@core/types/calendar.types"; import { CalendarProvider } from "@core/types/event.types"; import { EmailDriver } from "@backend/__tests__/drivers/email.driver"; -import { SyncDriver } from "@backend/__tests__/drivers/sync.driver"; +import { GoogleSyncDriver } from "@backend/__tests__/drivers/google-sync.driver"; import { UserDriver } from "@backend/__tests__/drivers/user.driver"; import { cleanupCollections, @@ -19,7 +19,7 @@ import * as supertokensMiddleware from "@backend/common/middleware/supertokens.m import { initSupertokens } from "@backend/common/middleware/supertokens.middleware"; import mongoService from "@backend/common/services/mongo.service"; import priorityService from "@backend/priority/services/priority.service"; -import { googleCalendarSyncService } from "@backend/sync/services/google-calendar-sync/google-calendar-sync.service"; +import { googleCalendarSyncService } from "@backend/sync/services/google-sync/google-sync.service"; import { googleWatchService } from "@backend/sync/services/watch/google-watch.service"; import userService from "@backend/user/services/user.service"; import userMetadataService from "@backend/user/services/user-metadata.service"; @@ -258,7 +258,7 @@ describe("UserService", () => { expect(storedUser).not.toBeNull(); await priorityService.createDefaultPriorities(userId); - await SyncDriver.createSync(storedUser!, true); + await GoogleSyncDriver.createHealthyGoogleSync(storedUser!, true); await googleCalendarSyncService.initializeGoogleCalendarSync(userId); const summary: Summary_Delete = diff --git a/packages/backend/src/user/services/user.service.ts b/packages/backend/src/user/services/user.service.ts index 5894089fb..ba5b423af 100644 --- a/packages/backend/src/user/services/user.service.ts +++ b/packages/backend/src/user/services/user.service.ts @@ -20,7 +20,7 @@ import { normalizeEmail } from "@backend/common/helpers/email.util"; import mongoService from "@backend/common/services/mongo.service"; import eventService from "@backend/event/services/event.service"; import priorityService from "@backend/priority/services/priority.service"; -import syncRecords from "@backend/sync/services/records/sync.records"; +import syncRecords from "@backend/sync/services/records/sync-records.repository"; import { googleWatchService } from "@backend/sync/services/watch/google-watch.service"; import { findCanonicalCompassUser } from "@backend/user/queries/user.queries"; import userMetadataService from "@backend/user/services/user-metadata.service"; diff --git a/packages/core/src/constants/core.constants.ts b/packages/core/src/constants/core.constants.ts index 89aa492fc..2ec91d2fc 100644 --- a/packages/core/src/constants/core.constants.ts +++ b/packages/core/src/constants/core.constants.ts @@ -14,7 +14,6 @@ export const SOMEDAY_WEEK_LIMIT_MSG = `Sorry, you can only have ${SOMEDAY_WEEKLY export const SOMEDAY_MONTH_LIMIT_MSG = `Sorry, you can only have ${SOMEDAY_MONTHLY_LIMIT} unscheduled events per month.`; export const SYNC_DEBUG = "/api/sync/debug"; export const ID_OPTIMISTIC_PREFIX = "optimistic"; -export const COMPASS_RESOURCE_HEADER = "x-compass-resource"; export enum NodeEnv { Development = "development", diff --git a/packages/scripts/src/__tests__/integration/2025.10.13T14.22.21.migrate-sync-watch-data.test.ts b/packages/scripts/src/__tests__/integration/2025.10.13T14.22.21.migrate-sync-watch-data.test.ts index b8ae09518..a2dd03918 100644 --- a/packages/scripts/src/__tests__/integration/2025.10.13T14.22.21.migrate-sync-watch-data.test.ts +++ b/packages/scripts/src/__tests__/integration/2025.10.13T14.22.21.migrate-sync-watch-data.test.ts @@ -6,7 +6,7 @@ import { ObjectId } from "mongodb"; import { Logger } from "@core/logger/winston.logger"; import { Resource_Sync } from "@core/types/sync.types"; import { ExpirationDateSchema } from "@core/types/type.utils"; -import { SyncDriver } from "@backend/__tests__/drivers/sync.driver"; +import { GoogleSyncDriver } from "@backend/__tests__/drivers/google-sync.driver"; import { cleanupCollections, cleanupTestDb, @@ -38,7 +38,9 @@ describe.each([ }; beforeAll(setupTestDb); - beforeEach(() => SyncDriver.generateV0Data(count, generateExpiredWatches)); + beforeEach(() => + GoogleSyncDriver.generateLegacySyncWatchData(count, generateExpiredWatches), + ); beforeEach(WatchMigration.prototype.up); afterEach(cleanupCollections); afterEach(() => mongoService.sync.deleteMany()); diff --git a/packages/scripts/src/migrations/2025.10.13T14.22.21.migrate-sync-watch-data.ts b/packages/scripts/src/migrations/2025.10.13T14.22.21.migrate-sync-watch-data.ts index 13864a7c0..02eb7cfda 100644 --- a/packages/scripts/src/migrations/2025.10.13T14.22.21.migrate-sync-watch-data.ts +++ b/packages/scripts/src/migrations/2025.10.13T14.22.21.migrate-sync-watch-data.ts @@ -8,9 +8,9 @@ import { type Schema_Watch, WatchSchema } from "@core/types/watch.types"; import { MONGO_BATCH_SIZE } from "@backend/common/constants/backend.constants"; import gcalService from "@backend/common/services/gcal/gcal.service"; import mongoService from "@backend/common/services/mongo.service"; -import { getGcalClient } from "@backend/sync/services/google-calendar-sync/google.calendar.client"; +import { getGcalClient } from "@backend/sync/services/google-sync/gcal.client"; import { googleWatchService } from "@backend/sync/services/watch/google-watch.service"; -import { getChannelExpiration } from "@backend/sync/util/sync.util"; +import { getChannelExpiration } from "@backend/sync/services/watch/google-watch-timing"; export default class Migration implements RunnableMigration { readonly name: string = "2025.10.13T14.22.21.migrate-sync-watch-data"; diff --git a/packages/scripts/src/seeders/2025.10.01T10.09.22.seed-user-events.ts b/packages/scripts/src/seeders/2025.10.01T10.09.22.seed-user-events.ts index 8a5573190..bd2507359 100644 --- a/packages/scripts/src/seeders/2025.10.01T10.09.22.seed-user-events.ts +++ b/packages/scripts/src/seeders/2025.10.01T10.09.22.seed-user-events.ts @@ -13,7 +13,7 @@ import { createMockStandaloneEvent } from "@core/util/test/ccal.event.factory"; import { ENV } from "@backend/common/constants/env.constants"; import { error } from "@backend/common/errors/handlers/error.handler"; import { UserError } from "@backend/common/errors/user/user.errors"; -import { CompassSyncProcessor } from "@backend/sync/services/sync/compass/compass.sync.processor"; +import { CompassToGoogleEventPropagation } from "@backend/sync/services/event-propagation/compass-to-google/compass-to-google.event-propagation"; import { findCompassUserBy } from "@backend/user/queries/user.queries"; export default class Seeder implements RunnableMigration { @@ -136,7 +136,7 @@ export default class Seeder implements RunnableMigration { const userId = (await this.#findUserOrThrow(user!))._id.toString(); const events = this.#generateEvents(userId); - await CompassSyncProcessor.processEvents( + await CompassToGoogleEventPropagation.processEvents( events.map((payload) => ({ payload, applyTo: RecurringEventUpdateScope.THIS_EVENT, @@ -153,7 +153,7 @@ export default class Seeder implements RunnableMigration { const userId = (await this.#findUserOrThrow(user!))._id.toString(); const events = this.#generateEvents(userId); - await CompassSyncProcessor.processEvents( + await CompassToGoogleEventPropagation.processEvents( events.map((payload) => ({ payload, applyTo: RecurringEventUpdateScope.THIS_EVENT, diff --git a/packages/web/src/views/Calendar/components/Draft/hooks/actions/useDraftActions.ts b/packages/web/src/views/Calendar/components/Draft/hooks/actions/useDraftActions.ts index 181434ceb..7dfbc1ea9 100644 --- a/packages/web/src/views/Calendar/components/Draft/hooks/actions/useDraftActions.ts +++ b/packages/web/src/views/Calendar/components/Draft/hooks/actions/useDraftActions.ts @@ -18,15 +18,15 @@ import dayjs, { type Dayjs } from "@core/util/date/dayjs"; import { DirtyParser } from "@web/common/parsers/dirty.parser"; import { EventInViewParser } from "@web/common/parsers/view.parser"; import { type PartialMouseEvent } from "@web/common/types/util.types"; +import { + type Schema_GridEvent, + type Schema_WebEvent, +} from "@web/common/types/web.event.types"; import { assembleDefaultEvent } from "@web/common/utils/event/event.util"; import { type Payload_ConvertEvent, type Payload_EditEvent, } from "@web/ducks/events/event.types"; -import { - type Schema_GridEvent, - type Schema_WebEvent, -} from "@web/common/types/web.event.types"; import { selectDraft, selectDraftStatus, diff --git a/screenlog.0 b/screenlog.0 new file mode 100644 index 000000000..3a5b8f7bc --- /dev/null +++ b/screenlog.0 @@ -0,0 +1,141 @@ +40 | } +41 | } +42 | let er; +43 | if (args.length > 0) +44 | er = args[0]; +45 | throw er; + ^ +error: Failed to start server. Is port 3000 in use? + at emitError (node:http:350:13) +26-05-05 16:47:10 [info] app:root: Server running on port: 3000 +200 GET /api/health 27.943ms Tue, 05 May 2026 16:47:12 GMT +200 GET /api/config 1.189ms Tue, 05 May 2026 16:48:27 GMT +26-05-05 16:48:27 [debug] app:sse.server: SSE connection opened for user: 69f8d418b4fe08f399f1ee11 (total: 1) +200 GET /api/event?start=2026-05-05T00:00:00Z&end=2026-05-05T23:59:59Z 131.479ms Tue, 05 May 2026 16:48:27 GMT +200 GET /api/user/profile 424.844ms Tue, 05 May 2026 16:48:28 GMT +200 GET /api/user/metadata 748.787ms Tue, 05 May 2026 16:48:28 GMT +26-05-05 16:48:49 [debug] app:sse.server: SSE dead connection removed for user: 69f8d418b4fe08f399f1ee11 +26-05-05 16:48:49 [debug] app:sse.server: SSE connection closed for user: 69f8d418b4fe08f399f1ee11 +304 GET /api/config 0.679ms Tue, 05 May 2026 16:48:49 GMT +26-05-05 16:48:49 [debug] app:sse.server: SSE connection opened for user: 69f8d418b4fe08f399f1ee11 (total: 1) +304 GET /api/user/profile 32.407ms Tue, 05 May 2026 16:48:49 GMT +304 GET /api/event?start=2026-05-05T00:00:00Z&end=2026-05-05T23:59:59Z 82.540ms Tue, 05 May 2026 16:48:49 GMT +304 GET /api/user/metadata 140.929ms Tue, 05 May 2026 16:48:50 GMT +26-05-05 16:48:51 [debug] app:sse.server: SSE dead connection removed for user: 69f8d418b4fe08f399f1ee11 +26-05-05 16:48:51 [debug] app:sse.server: SSE connection closed for user: 69f8d418b4fe08f399f1ee11 +304 GET /api/config 0.240ms Tue, 05 May 2026 16:48:51 GMT +26-05-05 16:48:51 [debug] app:sse.server: SSE connection opened for user: 69f8d418b4fe08f399f1ee11 (total: 1) +304 GET /api/user/profile 38.515ms Tue, 05 May 2026 16:48:51 GMT +304 GET /api/event?start=2026-05-05T00:00:00Z&end=2026-05-05T23:59:59Z 87.519ms Tue, 05 May 2026 16:48:51 GMT +304 GET /api/user/metadata 206.690ms Tue, 05 May 2026 16:48:51 GMT +26-05-05 16:48:54 [debug] app:sse.server: SSE dead connection removed for user: 69f8d418b4fe08f399f1ee11 +26-05-05 16:48:54 [debug] app:sse.server: SSE connection closed for user: 69f8d418b4fe08f399f1ee11 +304 GET /api/config 0.242ms Tue, 05 May 2026 16:48:55 GMT +26-05-05 16:48:55 [debug] app:sse.server: SSE connection opened for user: 69f8d418b4fe08f399f1ee11 (total: 1) +304 GET /api/user/profile 35.480ms Tue, 05 May 2026 16:48:55 GMT +304 GET /api/event?start=2026-05-05T00:00:00Z&end=2026-05-05T23:59:59Z 70.288ms Tue, 05 May 2026 16:48:55 GMT +304 GET /api/user/metadata 153.772ms Tue, 05 May 2026 16:48:55 GMT +304 GET /api/event?start=2026-05-05T00:00:00Z&end=2026-05-05T23:59:59Z 79.333ms Tue, 05 May 2026 16:49:29 GMT +26-05-05 16:49:29 [debug] app:sse.server: SSE dead connection removed for user: 69f8d418b4fe08f399f1ee11 +26-05-05 16:49:29 [debug] app:sse.server: SSE connection closed for user: 69f8d418b4fe08f399f1ee11 +401 GET /api/events/stream 2.334ms Tue, 05 May 2026 16:49:29 GMT +26-05-05 16:49:40 [info] app:auth.google.service: google_auth_decision { + "event": "google_auth_decision", + "authMode": "SIGNIN_INCREMENTAL", + "createdNewRecipeUser": false, + "hasCompassUserId": true, + "hasGoogleUserId": true, + "hasHealthySync": true, + "hasProviderEmail": true, + "hasStoredRefreshToken": true, + "loginMethodsLength": 1, + "compassUserTraceId": "4e2b9966754580e1", + "googleUserTraceId": "4e4a0791a7272e76", + "providerEmailTraceId": "fd70f10a68324d8a" +} +26-05-05 16:49:40 [info] app:google-sync.service: Starting incremental Google Calendar sync for user: 69f8d418b4fe08f399f1ee11 +26-05-05 16:49:40 [debug] app:sse.server: SSE connection opened for user: 69f8d418b4fe08f399f1ee11 (total: 1) +26-05-05 16:49:40 [debug] app:compass-to-google.event-propagation: Processing 7 event(s)... +200 GET /api/user/metadata 246.707ms Tue, 05 May 2026 16:49:40 GMT +26-05-05 16:49:40 [info] app:compass-to-google.event-propagation: Handle Compass event(69f8e0916e516f3f35ce92f1): NIL->>STANDALONE_SOMEDAY_CONFIRMED +26-05-05 16:49:40 [info] app:compass-to-google.event-propagation: Handle Compass event(69f8e0916e516f3f35ce92f2): NIL->>STANDALONE_SOMEDAY_CONFIRMED +26-05-05 16:49:41 [info] app:compass-to-google.event-propagation: Handle Compass event(69f8e0916e516f3f35ce92f3): NIL->>STANDALONE_CONFIRMED +26-05-05 16:49:41 [error] app:google-watch.service: Skipped Start Watch for calendarlist { + "description": "Calendar watch already exists", + "status": 400, + "isOperational": true +} +26-05-05 16:49:41 [error] app:google-watch.service: Skipped Start Watch for ugurarmagan93@gmail.com events { + "description": "Event watch already exists", + "status": 400, + "isOperational": true +} +26-05-05 16:49:41 [info] app:compass-to-google.event-propagation: Handle Compass event(69f8e0916e516f3f35ce92f4): NIL->>STANDALONE_CONFIRMED +200 GET /api/event?start=2026-05-05T00:00:00Z&end=2026-05-05T23:59:59Z 77.976ms Tue, 05 May 2026 16:49:41 GMT +200 GET /api/user/metadata 148.779ms Tue, 05 May 2026 16:49:42 GMT +304 GET /api/event?start=2026-05-05T00:00:00Z&end=2026-05-05T23:59:59Z 78.555ms Tue, 05 May 2026 16:49:42 GMT +26-05-05 16:49:42 [info] app:compass-to-google.event-propagation: Handle Compass event(69f8e0916e516f3f35ce92f5): NIL->>STANDALONE_CONFIRMED +26-05-05 16:49:43 [info] app:compass-to-google.event-propagation: Handle Compass event(69f8e0916e516f3f35ce92f6): NIL->>STANDALONE_CONFIRMED +26-05-05 16:49:43 [info] app:compass-to-google.event-propagation: Handle Compass event(69f8e0916e516f3f35ce92f7): NIL->>STANDALONE_CONFIRMED +204 POST /api/event 3969.863ms Tue, 05 May 2026 16:49:44 GMT +200 GET /api/event?someday=true&start=2026-04-30T23:59:59-05:00&end=2026-05-31T23:59:59-05:00 143.231ms Tue, 05 May 2026 16:49:44 GMT +304 GET /api/event?start=2026-05-05T00:00:00Z&end=2026-05-05T23:59:59Z 162.736ms Tue, 05 May 2026 16:49:44 GMT +304 GET /api/event?start=2026-05-05T00:00:00Z&end=2026-05-05T23:59:59Z 77.707ms Tue, 05 May 2026 16:49:44 GMT +200 GET /api/health 44.635ms Tue, 05 May 2026 16:51:58 GMT +200 GET /api/health 104.498ms Tue, 05 May 2026 16:52:14 GMT +403 POST /api/sync/gcal/notifications 1.285ms Tue, 05 May 2026 16:52:40 GMT +26-05-05 16:55:02 [info] app:google-watch.service: calendarlist sync initialized for channelId: 69fa20e5e77014be764ffa0f +200 POST /api/sync/gcal/notifications 3.552ms Tue, 05 May 2026 16:55:02 GMT +26-05-05 16:55:02 [info] app:google-watch.service: events sync initialized for channelId: 69fa20e5e77014be764ffa0e +200 POST /api/sync/gcal/notifications 0.377ms Tue, 05 May 2026 16:55:02 GMT +26-05-05 16:55:30 [debug] app:google-to-compass.event-propagation: Processing 6 event(s)... +26-05-05 16:55:30 [info] app:google-to-compass.event-propagation: Handle Gcal event(69f8e0916e516f3f35ce92f3): STANDALONE->>STANDALONE_CONFIRMED +26-05-05 16:55:30 [info] app.event.classes.gcal.parser: UPSERTING STANDALONE->>STANDALONE_CONFIRMED: 69f8e0916e516f3f35ce92f3 (Gcal) +26-05-05 16:55:30 [info] app:google-to-compass.event-propagation: Handle Gcal event(69f8e0916e516f3f35ce92f4): STANDALONE->>STANDALONE_CONFIRMED +26-05-05 16:55:30 [info] app.event.classes.gcal.parser: UPSERTING STANDALONE->>STANDALONE_CONFIRMED: 69f8e0916e516f3f35ce92f4 (Gcal) +26-05-05 16:55:31 [info] app:google-to-compass.event-propagation: Handle Gcal event(69f8e0916e516f3f35ce92f5): STANDALONE->>STANDALONE_CONFIRMED +26-05-05 16:55:31 [info] app.event.classes.gcal.parser: UPSERTING STANDALONE->>STANDALONE_CONFIRMED: 69f8e0916e516f3f35ce92f5 (Gcal) +26-05-05 16:55:31 [info] app:google-to-compass.event-propagation: Handle Gcal event(69f8e0916e516f3f35ce92f6): STANDALONE->>STANDALONE_CONFIRMED +26-05-05 16:55:31 [info] app.event.classes.gcal.parser: UPSERTING STANDALONE->>STANDALONE_CONFIRMED: 69f8e0916e516f3f35ce92f6 (Gcal) +26-05-05 16:55:31 [info] app:google-to-compass.event-propagation: Handle Gcal event(69f8e0916e516f3f35ce92f7): STANDALONE->>STANDALONE_CONFIRMED +26-05-05 16:55:31 [info] app.event.classes.gcal.parser: UPSERTING STANDALONE->>STANDALONE_CONFIRMED: 69f8e0916e516f3f35ce92f7 (Gcal) +26-05-05 16:55:31 [info] app:google-to-compass.event-propagation: Handle Gcal event(7sv7i5o5lvq6lvpqq709s3obta): STANDALONE->>STANDALONE_CONFIRMED +26-05-05 16:55:31 [info] app.event.classes.gcal.parser: UPSERTING STANDALONE->>STANDALONE_CONFIRMED: 7sv7i5o5lvq6lvpqq709s3obta (Gcal) +PROCESSED: [ + { + title: "Morning standup", + transition: [ "STANDALONE", "STANDALONE_CONFIRMED" ], + category: "STANDALONE", + operation: "STANDALONE_UPDATED", + }, { + title: "Try Compass", + transition: [ "STANDALONE", "STANDALONE_CONFIRMED" ], + category: "STANDALONE", + operation: "STANDALONE_UPDATED", + }, { + title: "Exercise", + transition: [ "STANDALONE", "STANDALONE_CONFIRMED" ], + category: "STANDALONE", + operation: "STANDALONE_UPDATED", + }, { + title: "Call a friend", + transition: [ "STANDALONE", "STANDALONE_CONFIRMED" ], + category: "STANDALONE", + operation: "STANDALONE_UPDATED", + }, { + title: "Deep work day", + transition: [ "STANDALONE", "STANDALONE_CONFIRMED" ], + category: "STANDALONE", + operation: "STANDALONE_UPDATED", + }, { + title: "test", + transition: [ "STANDALONE", "STANDALONE_CONFIRMED" ], + category: "STANDALONE", + operation: "STANDALONE_UPDATED", + } +] +26-05-05 16:55:31 [info] app:google-watch.service: GCal Notification for user: 69f8d418b4fe08f399f1ee11, calendarId: ugurarmagan93@gmail.com PROCESSED +200 POST /api/sync/gcal/notifications 1260.910ms Tue, 05 May 2026 16:55:31 GMT +200 GET /api/event?start=2026-05-05T00:00:00Z&end=2026-05-05T23:59:59Z 80.666ms Tue, 05 May 2026 16:55:31 GMT +200 GET /api/health 28.211ms Tue, 05 May 2026 16:55:51 GMT +200 GET /api/health 31.178ms Tue, 05 May 2026 16:55:51 GMT