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