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
3 changed files with 3156 additions and 12 deletions
Showing only changes of commit 78f0deefac - Show all commits
+42
View File
@@ -1,6 +1,7 @@
// Source: https://orm.drizzle.team/docs/sql-schema-declaration
import {
mysqlTable,
mysqlEnum,
varchar,
text,
int,
@@ -92,6 +93,7 @@ export const calendarEvents = mysqlTable(
.references(() => calendars.id, { onDelete: 'cascade' }),
uid: varchar('uid', { length: 512 }).notNull(), // VEVENT UID — natural idempotency key
etag: varchar('etag', { length: 256 }),
objectUrl: varchar('object_url', { length: 1024 }), // CalDAV object URL; populated from obj.url by sync.ts (D-08)
rawVevent: text('raw_vevent').notNull(), // full VCALENDAR/VEVENT string for ical.js
dtstartUtc: timestamp('dtstart_utc'), // NULL for all-day events
dtstartDate: date('dtstart_date'), // set for all-day events; NULL for timed
@@ -110,3 +112,43 @@ export const calendarEvents = mysqlTable(
unique('uniq_calendar_uid').on(t.calendarId, t.uid),
],
)
/**
* Transactional outbox for CalDAV write-back (D-05).
* Pending rows are drained by the outbox worker (broker/outboxWorker.ts).
* Status machine: pending → done | failed | dead (D-07, D-08).
*
* groupId links the delete+create pair for edit-as-move (D-04, Pitfall 5).
* calendarObjectUrl is null for creates (URL constructed from calendarUrl + uid + '.ics').
* etag enables If-Match on update/delete to enforce conflict detection (D-08).
*/
export const calendarOutbox = mysqlTable(
'calendar_outbox',
{
id: int().primaryKey().autoincrement(),
userId: int('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
// 'create' | 'update' | 'delete'
operation: mysqlEnum(['create', 'update', 'delete']).notNull(),
// 'pending' | 'done' | 'failed' | 'dead'
status: mysqlEnum(['pending', 'done', 'failed', 'dead']).notNull().default('pending'),
uid: varchar('uid', { length: 512 }).notNull(),
calendarUrl: varchar('calendar_url', { length: 1024 }).notNull(),
calendarObjectUrl: varchar('calendar_object_url', { length: 1024 }), // null for creates
etag: varchar('etag', { length: 256 }), // cached etag for If-Match (D-08)
payload: text('payload'), // icsString for create/update; null for delete
attemptCount: int('attempt_count').notNull().default(0),
nextAttemptAt: timestamp('next_attempt_at').defaultNow().notNull(),
lastError: text('last_error'),
// groupId links the delete+create pair for edit-as-move (D-04)
groupId: varchar('group_id', { length: 64 }), // nullable
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().onUpdateNow(),
},
(t) => [
index('idx_outbox_user_status').on(t.userId, t.status),
index('idx_outbox_next_attempt').on(t.nextAttemptAt, t.status),
index('idx_outbox_uid').on(t.uid),
],
)
+1
View File
@@ -23,6 +23,7 @@
"react": "^19.0.0",
"react-dom": "^19.0.0",
"temporal-polyfill": "0.3.2",
"vite-plugin-pwa": "^1.3.0",
"zustand": "5.0.14"
},
"devDependencies": {
+3113 -12
View File
File diff suppressed because it is too large Load Diff