feat(03-01): extend schema with calendarOutbox table + calendarEvents.objectUrl; install vite-plugin-pwa

- Add mysqlEnum import to drizzle-orm/mysql-core import block
- Add objectUrl varchar(1024) to calendarEvents after etag column (D-08)
- Add calendarOutbox table with status machine columns, groupId for edit-as-move (D-04)
- Add indexes: idx_outbox_user_status, idx_outbox_next_attempt, idx_outbox_uid
- Install vite-plugin-pwa@1.3.0 (supply-chain gate T-03-SC cleared by Task 1)
This commit is contained in:
Lucas Berger
2026-06-05 17:22:37 -04:00
parent 93302cf942
commit 78f0deefac
3 changed files with 3156 additions and 12 deletions
+42
View File
@@ -1,6 +1,7 @@
// Source: https://orm.drizzle.team/docs/sql-schema-declaration // Source: https://orm.drizzle.team/docs/sql-schema-declaration
import { import {
mysqlTable, mysqlTable,
mysqlEnum,
varchar, varchar,
text, text,
int, int,
@@ -92,6 +93,7 @@ export const calendarEvents = mysqlTable(
.references(() => calendars.id, { onDelete: 'cascade' }), .references(() => calendars.id, { onDelete: 'cascade' }),
uid: varchar('uid', { length: 512 }).notNull(), // VEVENT UID — natural idempotency key uid: varchar('uid', { length: 512 }).notNull(), // VEVENT UID — natural idempotency key
etag: varchar('etag', { length: 256 }), 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 rawVevent: text('raw_vevent').notNull(), // full VCALENDAR/VEVENT string for ical.js
dtstartUtc: timestamp('dtstart_utc'), // NULL for all-day events dtstartUtc: timestamp('dtstart_utc'), // NULL for all-day events
dtstartDate: date('dtstart_date'), // set for all-day events; NULL for timed 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), 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": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"temporal-polyfill": "0.3.2", "temporal-polyfill": "0.3.2",
"vite-plugin-pwa": "^1.3.0",
"zustand": "5.0.14" "zustand": "5.0.14"
}, },
"devDependencies": { "devDependencies": {
+3113 -12
View File
File diff suppressed because it is too large Load Diff