Cuts the CalDAVService sync engine over from the Phase F scalar calendar_path to the binding-row model introduced in Slice 1 (mig 101). Invisible-but-shippable: existing Phase F users keep their backfilled all_visible binding, new users hitting the legacy PUT /api/caldav-config get an auto-created all_visible binding so the "configure → it just works" UX survives. Slice 2b adds the picker UI and write APIs on top. Schema (mig 107) - paliad.caldav_sync_log.binding_id (nullable, FK ON DELETE SET NULL so audit history survives binding deletes). - Per-binding index for the read path. - Idempotent (column-exists DO block) + assertion. Services - CalendarBindingService: ListForUser, ListEnabled, ListAllEnabled, Get, Create, Update, Delete, SetSyncStatus. Mirrors the table CHECK constraints client-side so the API returns useful 400s. - AppointmentTargetService: UpsertAfterPush, FindByUIDAndBinding, ListForBinding, DeleteByAppointmentAndBinding, StaleForBinding. Replaces SetCalDAVMeta as the authoritative source of per-target state; legacy scalar columns still written for back-compat. - AppointmentService.ForBinding: scope filter implementing all_visible, personal_only, project. Hierarchy scopes (client/litigation/patent/case) return ErrUnsupportedScope — Slice 3 wires them via the existing path-based descendant predicate. Sync engine rewrite - CalDAVService.Start iterates ListAllEnabled to discover users with at least one enabled binding. - runSyncOnce loops bindings, writes one caldav_sync_log row per (user, binding) tick, rolls the worst-case error up onto user_caldav_config.last_sync_error so /api/caldav-config still shows aggregate status. - pushBinding pushes the ForBinding() slice + cleans up stale-target rows (project unshared, scope PATCHed). - pullBinding swaps the N×GET pattern for REPORT calendar-multiget (RFC 4791 §7.9; chunked at 100 hrefs to stay inside provider rate limits) and reconciles via per-target etag comparison. - Hooks (OnAppointmentCreated/Updated/Deleted) fan out across the user's matching bindings using appointmentInBinding() — best effort per binding, same 30s timeout as Phase F. - SaveConfig auto-creates an all_visible binding on first-time configure so Phase F "configure → events appear" survives the cut-over. CalDAV client - New ReportMultiget verb implementing RFC 4791 §7.9 calendar-multiget. Chunked at multigetMaxHrefs=100 to fit Google Calendar's per-request cap. HTTP API - GET /api/caldav-bindings — read-only list of the authenticated user's bindings. Slice 2b adds POST/PATCH/DELETE. Verification - BEGIN..ROLLBACK against live Supabase (PG 15.8): mig 107 applies cleanly + the synthetic two-binding scenario lands the project appointment in both bindings while keeping the personal one in master only; cascade on appointment-delete drops targets; cascade on binding-delete drops targets AND sets sync_log.binding_id NULL. - go build ./..., go test ./internal/..., bun run build all clean. Backwards-compat - paliad.appointments.caldav_uid / caldav_etag still written in pushBinding so legacy readers see fresh values. Slice 4 drops them after telemetry confirms no path still reads them.
54 lines
1.9 KiB
SQL
54 lines
1.9 KiB
SQL
-- t-paliad-212 — Slice 2a of CalDAV multi-calendar.
|
|
--
|
|
-- Adds paliad.caldav_sync_log.binding_id so the per-tick sync log
|
|
-- records which binding the entry belongs to. NULL for legacy rows
|
|
-- and for "global" log entries that aren't per-binding (Slice 2a
|
|
-- still writes one row per user per tick — Slice 2b's sync rewrite
|
|
-- moves to one row per (user, binding) per tick).
|
|
--
|
|
-- FK uses ON DELETE SET NULL so deleting a binding doesn't blow away
|
|
-- its historical sync log (audit trail wins over referential tidiness).
|
|
--
|
|
-- Idempotent: column added via DO block with information_schema check.
|
|
|
|
SELECT set_config(
|
|
'paliad.audit_reason',
|
|
'mig 107: add caldav_sync_log.binding_id for per-binding sync log entries (t-paliad-212 Slice 2a)',
|
|
true);
|
|
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM information_schema.columns
|
|
WHERE table_schema = 'paliad'
|
|
AND table_name = 'caldav_sync_log'
|
|
AND column_name = 'binding_id'
|
|
) THEN
|
|
ALTER TABLE paliad.caldav_sync_log
|
|
ADD COLUMN binding_id uuid
|
|
REFERENCES paliad.user_calendar_bindings(id) ON DELETE SET NULL;
|
|
END IF;
|
|
END $$;
|
|
|
|
CREATE INDEX IF NOT EXISTS caldav_sync_log_binding_idx
|
|
ON paliad.caldav_sync_log (binding_id, occurred_at DESC)
|
|
WHERE binding_id IS NOT NULL;
|
|
|
|
-- Assertion: column exists and is nullable.
|
|
DO $$
|
|
DECLARE
|
|
col_nullable text;
|
|
BEGIN
|
|
SELECT is_nullable INTO col_nullable
|
|
FROM information_schema.columns
|
|
WHERE table_schema = 'paliad'
|
|
AND table_name = 'caldav_sync_log'
|
|
AND column_name = 'binding_id';
|
|
IF col_nullable IS NULL THEN
|
|
RAISE EXCEPTION 'mig 107 assertion failed: caldav_sync_log.binding_id missing';
|
|
END IF;
|
|
IF col_nullable <> 'YES' THEN
|
|
RAISE EXCEPTION 'mig 107 assertion failed: caldav_sync_log.binding_id is NOT NULL (must be nullable)';
|
|
END IF;
|
|
END $$;
|