fix(03): CR-02 scope freshest-etag re-read to the writing member's calendar
This commit is contained in:
@@ -26,7 +26,7 @@
|
|||||||
import { schedule } from 'node-cron'
|
import { schedule } from 'node-cron'
|
||||||
import { and, eq, lte } from 'drizzle-orm'
|
import { and, eq, lte } from 'drizzle-orm'
|
||||||
import { db } from '../db/client.js'
|
import { db } from '../db/client.js'
|
||||||
import { calendarEvents, calendarOutbox, memberCredentials } from '../db/schema.js'
|
import { calendarEvents, calendarOutbox, calendars, memberCredentials } from '../db/schema.js'
|
||||||
import { createFastmailClient } from './client.js'
|
import { createFastmailClient } from './client.js'
|
||||||
import { decryptPassword } from './crypto.js'
|
import { decryptPassword } from './crypto.js'
|
||||||
import { syncCalendar } from './sync.js'
|
import { syncCalendar } from './sync.js'
|
||||||
@@ -201,11 +201,26 @@ async function dispatchRow(row: OutboxRow): Promise<DispatchResult> {
|
|||||||
// Using the freshest cached etag here prevents the spurious conflict toast
|
// Using the freshest cached etag here prevents the spurious conflict toast
|
||||||
// while still preserving genuine conflict detection (D-08): a real external
|
// while still preserving genuine conflict detection (D-08): a real external
|
||||||
// change updates calendarEvents.etag differently from any pending row's etag.
|
// change updates calendarEvents.etag differently from any pending row's etag.
|
||||||
|
// CR-02: calendar_events is keyed (calendarId, uid), and a shared Fastmail
|
||||||
|
// account (D-16) caches the same uid once per member's calendar. A uid-only
|
||||||
|
// re-read returns multiple rows and an arbitrary [0] — potentially the OTHER
|
||||||
|
// member's etag, which would spuriously 412 (false conflict → edit dropped,
|
||||||
|
// D-08) or coincidentally match and overwrite. Scope the re-read to THIS row's
|
||||||
|
// own calendar by joining through calendars on the outbox row's userId +
|
||||||
|
// calendarUrl so the freshest etag belongs to the writing member.
|
||||||
let etagForPut: string | null = row.etag ?? null
|
let etagForPut: string | null = row.etag ?? null
|
||||||
const freshEtagRows = (await db
|
const freshEtagRows = (await db
|
||||||
.select({ etag: calendarEvents.etag })
|
.select({ etag: calendarEvents.etag })
|
||||||
.from(calendarEvents)
|
.from(calendarEvents)
|
||||||
.where(eq(calendarEvents.uid, row.uid))) as Array<{ etag: string | null }>
|
.innerJoin(calendars, eq(calendarEvents.calendarId, calendars.id))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(calendarEvents.uid, row.uid),
|
||||||
|
eq(calendars.userId, row.userId),
|
||||||
|
eq(calendars.url, row.calendarUrl),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.limit(1)) as Array<{ etag: string | null }>
|
||||||
if (freshEtagRows.length > 0 && freshEtagRows[0].etag != null) {
|
if (freshEtagRows.length > 0 && freshEtagRows[0].etag != null) {
|
||||||
etagForPut = freshEtagRows[0].etag
|
etagForPut = freshEtagRows[0].etag
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user