Milestone v1.0: FamilySync MVP #1

Merged
luckberg merged 376 commits from gsd/v1.0-milestone into main 2026-06-10 17:39:19 -04:00
Showing only changes of commit 00a0454514 - Show all commits
+33 -22
View File
@@ -126,6 +126,10 @@ const syncStatusQuerySchema = z.object({
// Response shape: { occurrences: CalendarOccurrence[] }
// ---------------------------------------------------------------------------
eventsRouter.get('/', zValidator('query', eventsQuerySchema), async (c) => {
// Resolve the current user first — only return events for owned + shared calendars (T-03-06).
const currentUserId = await resolveUserId(c)
if (currentUserId === null) return c.json({ error: 'Unauthorized' }, 401)
const { start, end } = c.req.valid('query')
// --- Window span guard (T-02b-02) ---
@@ -163,32 +167,39 @@ eventsRouter.get('/', zValidator('query', eventsQuerySchema), async (c) => {
.innerJoin(calendars, eq(calendarEvents.calendarId, calendars.id))
.innerJoin(users, eq(calendars.userId, users.id))
.where(
or(
// Recurring masters: may have occurrences inside the window even if dtstartUtc is old.
// Two sub-cases:
// (a) Timed recurring masters: dtstartUtc < windowEnd
// (b) All-day recurring masters: dtstartUtc is NULL (DATE-only), use dtstartDate < end
// A NULL dtstartUtc causes the timed comparison to be NULL/false, so (b) carries it.
and(
sql`${calendarEvents.hasRrule} = 1`,
or(
and(
// Ownership predicate (BUG 3 fix): restrict to calendars owned by the current user
// OR shared-family calendars (isShared=true). Mirrors the /writable-calendars idiom
// (~line 509) so both endpoints agree on the authoritative writable set (D-03).
or(eq(calendars.userId, currentUserId), eq(calendars.isShared, true)),
// Date-window pre-filter (RESEARCH.md §Open Questions 3 / Pitfall 5):
or(
// Recurring masters: may have occurrences inside the window even if dtstartUtc is old.
// Two sub-cases:
// (a) Timed recurring masters: dtstartUtc < windowEnd
// (b) All-day recurring masters: dtstartUtc is NULL (DATE-only), use dtstartDate < end
// A NULL dtstartUtc causes the timed comparison to be NULL/false, so (b) carries it.
and(
sql`${calendarEvents.hasRrule} = 1`,
or(
sql`${calendarEvents.dtstartUtc} < ${windowEndDate}`,
sql`${calendarEvents.dtstartDate} < ${end}`,
),
),
// Non-recurring timed events: dtstartUtc falls in [windowStart, windowEnd)
and(
sql`${calendarEvents.hasRrule} = 0`,
sql`${calendarEvents.dtstartUtc} IS NOT NULL`,
sql`${calendarEvents.dtstartUtc} >= ${windowStartDate}`,
sql`${calendarEvents.dtstartUtc} < ${windowEndDate}`,
),
// All-day events: dtstartDate falls in [start, end) — DATE comparison, no time component
and(
sql`${calendarEvents.dtstartDate} IS NOT NULL`,
sql`${calendarEvents.dtstartDate} >= ${start}`,
sql`${calendarEvents.dtstartDate} < ${end}`,
),
),
// Non-recurring timed events: dtstartUtc falls in [windowStart, windowEnd)
and(
sql`${calendarEvents.hasRrule} = 0`,
sql`${calendarEvents.dtstartUtc} IS NOT NULL`,
sql`${calendarEvents.dtstartUtc} >= ${windowStartDate}`,
sql`${calendarEvents.dtstartUtc} < ${windowEndDate}`,
),
// All-day events: dtstartDate falls in [start, end) — DATE comparison, no time component
and(
sql`${calendarEvents.dtstartDate} IS NOT NULL`,
sql`${calendarEvents.dtstartDate} >= ${start}`,
sql`${calendarEvents.dtstartDate} < ${end}`,
),
),
)