KolektivComputer / KolektivComputer/kalendee

OAuth sync M1: read-only mirror sync for Google and Microsoft

Open
#3 0 comments 0 reactions 1 assignee Claimed by @LizAinslie View on GitHub
c/enhancement c/feature i/gcal i/microsoft t/server
Dominant language
Kotlin
Stars
0
Forks
0
Avg merge
11h 20m
Merged PRs (30d)
23

Description

## Context

The OAuth connection/linking foundation already exists on branch `feat/oauth-foundation` (currently being restored): `TokenVault` (AES-256-GCM), the provider registry with Google/Microsoft providers, `ConnectionService`, the V17 migration, Keel page types/actions + DI, and 202 passing tests. What is missing is the actual mirror engine: `CalendarSyncAdapter` implementations, full and incremental pull (`syncToken` / Graph `deltaLink`), mapping remote calendars into normal `calendars` rows and events into `events` rows, tombstoning, read-only rendering, and the sync loop. This tracks **M1 — Connection + read-only mirror (Google + Graph)** in `docs/external-calendars.md` (4–6 weeks), whose guiding principle is that a mirrored calendar is just a regular `calendars` row.

## Tasks

- [ ] Implement `CalendarSyncAdapter` for Google: calendar list + events, incremental `syncToken`, persist the token only after a page is fully applied
- [ ] Implement `CalendarSyncAdapter` for Microsoft Graph: `/me/calendars` + `calendarView/delta`, persist `deltaLink` only after a page is fully applied
- [ ] Discovery/initial sync: map remote collections to `calendars` rows owned by the linking user (`external_calendars.sync_direction = 'pull'`)
- [ ] Event mapping: upsert `events` rows with `external_calendar_id`, `external_uid`, `external_etag`, `external_updated_at`; do not bump `updated_at` on pull
- [ ] Recurrence: master-only best effort, truncate/flag `BYDAY`/`BYSETPOS`/`EXDATE`/`RDATE`/per-instance overrides per the V17 notes
- [ ] Deletes/cancellations: tombstones, remove/flag mirrored rows, and full-resync fallback when a sync token expires (410 / `syncStateNotFound`)
- [ ] Read-only enforcement: mirrored calendars flagged provider-managed and rejected by `PostgresCalendarStore`/DTOs/UI
- [ ] Sync service loop: full sync on connect, periodic incremental sync, per-connection serialization, honor `Retry-After`, `needs_reauth` on refresh failure
- [ ] Tests: adapter mapping with a mock HTTP engine, token-persistence semantics, read-only rejection, tombstone handling

## References

- `docs/external-calendars.md` — M1 milestone, provider matrix, data model sketch, cross-cutting risks
- `GOALS.md` — External calendars section
- `server/src/main/kotlin/dev/kolektiv/kalendee/oauth/CalendarProvider.kt:54` — `CalendarSyncAdapter` seam (currently a placeholder)
- `server/src/main/kotlin/dev/kolektiv/kalendee/oauth/ConnectionService.kt` — connection/token lifecycle
- `server/src/main/kotlin/dev/kolektiv/kalendee/oauth/providers/GoogleProvider.kt`, `MicrosoftProvider.kt`
- `server/src/main/resources/db/migration/V17__external_calendars.sql` — `calendar_connections`, `external_calendars`, `external_event_tombstones`, `events` external columns
- `server/src/main/kotlin/dev/kolektiv/kalendee/store/PostgresCalendarStore.kt`
- `core/src/commonMain/kotlin/dev/kolektiv/kalendee/calendar/Models.kt`, `Recurrence.kt`

## Dependencies

- **Depends on branch `feat/oauth-foundation`** (TokenVault, providers, ConnectionService, V17, Keel actions/DI) — currently being restored.
- Google and Microsoft adapters can land as separate PRs behind the same interface.
- #4 (M2 two-way writes) builds directly on this; #8 (server CalDAV) is the inverse direction.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.