Milestone v1.0: FamilySync MVP #1
@@ -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),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Generated
+3113
-12
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user