fix(03): correct event-write timezone + per-user calendar identity (Gate 2 Part D)
BUG A — timed events written 4h off: EventForm sent a naive local wall-clock string with no offset; the UTC API container parsed it via new Date() as UTC, so 09:00 America/Toronto serialized to DTSTART:...090000Z. Fix: new apps/pwa/src/lib/eventDateTime.ts serializes timed events to an unambiguous UTC instant in the browser (where the operator's zone is known); all-day stays a DATE string. No backend change. BUG B — created events attached to the wrong user's calendar + duplicate calendar rows per poll: calendars had no unique key on url, and poller/sync matched calendars by url alone — so under the shared single Fastmail account (D-16) one member's collection resolved to the other member's row. Fix: composite unique(user_id, url); scope poller lookup + sync select to (userId, url); hand migration 0001 (dedup + add key), applied to the live DB. Regression tests fail against the buggy url-only predicate. API 98/98, PWA 140/140, tsc clean both packages.
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
-- BUG B — calendars: add composite unique key (user_id, url).
|
||||
--
|
||||
-- Context (D-16): the two household members share ONE Fastmail account, so the
|
||||
-- SAME collection URL is polled by both credentials. The calendar upsert keyed on
|
||||
-- url alone never triggered onDuplicateKeyUpdate (no unique key on url), so every
|
||||
-- poll inserted a fresh calendar row; the url-only lookup then resolved to the
|
||||
-- other member's row, caching events under the wrong calendarId.
|
||||
--
|
||||
-- This migration is hand-written (not drizzle-kit generated) because drizzle-kit
|
||||
-- push is unsafe on populated MariaDB (false destructive diffs) and there is no
|
||||
-- migrations baseline. Apply it directly to the live DB before/with the image
|
||||
-- rebuild that ships the schema + broker fixes.
|
||||
--
|
||||
-- Order matters: duplicate (user_id, url) rows must be collapsed BEFORE the unique
|
||||
-- key is added, or ADD UNIQUE fails. We keep the LOWEST id per (user_id, url),
|
||||
-- repoint any cached events from the loser rows onto the keeper, then delete losers.
|
||||
|
||||
-- 1. Repoint calendar_events from duplicate calendar rows onto the keeper
|
||||
-- (lowest id) for each (user_id, url) group.
|
||||
UPDATE calendar_events ce
|
||||
JOIN calendars dup ON dup.id = ce.calendar_id
|
||||
JOIN (
|
||||
SELECT user_id, url, MIN(id) AS keep_id
|
||||
FROM calendars
|
||||
GROUP BY user_id, url
|
||||
) keeper ON keeper.user_id = dup.user_id AND keeper.url = dup.url
|
||||
SET ce.calendar_id = keeper.keep_id
|
||||
WHERE ce.calendar_id <> keeper.keep_id;
|
||||
|
||||
-- 2. Delete the duplicate (loser) calendar rows, keeping the lowest id per group.
|
||||
DELETE c FROM calendars c
|
||||
JOIN (
|
||||
SELECT user_id, url, MIN(id) AS keep_id
|
||||
FROM calendars
|
||||
GROUP BY user_id, url
|
||||
) keeper ON keeper.user_id = c.user_id AND keeper.url = c.url
|
||||
WHERE c.id <> keeper.keep_id;
|
||||
|
||||
-- 3. Add the composite unique key that makes the upsert idempotent per (user_id, url).
|
||||
ALTER TABLE calendars
|
||||
ADD CONSTRAINT uniq_calendar_user_url UNIQUE (user_id, url);
|
||||
@@ -73,7 +73,16 @@ export const calendars = mysqlTable(
|
||||
lastSyncedAt: timestamp('last_synced_at'),
|
||||
isShared: boolean('is_shared').default(false).notNull(),
|
||||
},
|
||||
(t) => [index('idx_calendars_user_id').on(t.userId)],
|
||||
(t) => [
|
||||
index('idx_calendars_user_id').on(t.userId),
|
||||
// BUG B: calendar identity is (userId, url), not url alone. The two household
|
||||
// members share one Fastmail account (D-16), so the SAME collection URL is
|
||||
// polled by both credentials. Without this unique key the calendar upsert's
|
||||
// onDuplicateKeyUpdate never fired → a new row per poll, and the url-only
|
||||
// lookup resolved to the other member's row → events cached under the wrong
|
||||
// calendarId. Keying on (userId, url) makes the upsert idempotent per member.
|
||||
unique('uniq_calendar_user_url').on(t.userId, t.url),
|
||||
],
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user