From f4c66241ca48fa982aa65fb77d2431a9ae313702 Mon Sep 17 00:00:00 2001 From: Till JS Date: Wed, 22 Apr 2026 18:43:09 +0200 Subject: [PATCH] =?UTF-8?q?feat(db):=20Phase=202c=20follow-up=20=E2=80=94?= =?UTF-8?q?=20Dexie=20v35=20hard=20userId=20drop=20on=20data=20tables?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes the userId cleanup Phase 2c left half-done. The creating-hook (commit e9b9544ea) stopped stamping userId on new writes, but existing rows still carried the column from v28 onwards — mixed state. This migration removes the column from every data-record row and drops the articles module's userId indexes that are now dead. v35.stores(): Re-declares articles / articleHighlights / articleTags without the `userId` index. Other indexes (status, savedAt, isFavorite, siteName, originalUrl, [articleId+startOffset], [articleId+tagId]) stay identical. v35.upgrade(): Iterates every SYNC_APP_MAP table that isn't on the USER_LEVEL list, calls `.modify()` to `delete record.userId` on every row. User-level tables (userSettings, userContext, newsPreferences, meditate/sleep/ mood/time/invoice/broadcast/wetterSettings, userTagPresets) keep their userId — their ownership model is user-scoped by design. The USER_LEVEL set is duplicated inside the upgrade closure because the hook-registration loop (where the runtime USER_LEVEL_TABLES const lives) hasn't run yet when the upgrade fires — Dexie applies upgrades before we call `db.table(...).hook()`. Public-type converters (tags-local's toTag/toTagGroup, calc's toCalculation/toSavedFormula) already fall back to 'guest' / '' when userId is absent, so the field's disappearance doesn't break downstream reads. After this ships, the "no table has both userId AND spaceId" invariant from the plan is truly met on data records. User-level tables still have both (v28 stamped spaceId onto them) but that's a separate, lower-priority cleanup. Type-check clean. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/mana/apps/web/src/lib/data/database.ts | 70 +++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/apps/mana/apps/web/src/lib/data/database.ts b/apps/mana/apps/web/src/lib/data/database.ts index ef99b0ad6..4257b80d9 100644 --- a/apps/mana/apps/web/src/lib/data/database.ts +++ b/apps/mana/apps/web/src/lib/data/database.ts @@ -782,6 +782,76 @@ db.version(34).stores({ tagGroups: 'id, [spaceId+sortOrder]', }); +// v35 — Phase 2c follow-up: hard drop of the `userId` column from +// every data-record row, and drop the now-unused userId indexes on +// the articles module tables. +// +// Phase 2c (commit e9b9544ea) stopped the creating-hook from stamping +// `userId` on new writes to data tables, leaving existing rows +// untouched (mixed state). This migration completes the cleanup: the +// `no table has both userId AND spaceId` invariant from the plan is +// now truly met on data records. +// +// Migration shape: +// 1. Re-declare articles / articleHighlights / articleTags without +// the `userId` index so the dropped column stops showing in the +// Dexie schema. The other indexes stay the same. +// 2. Upgrade function: iterate every table in SYNC_APP_MAP that is +// NOT in USER_LEVEL_TABLES (see below), and `delete record.userId` +// on every row. +// +// User-level tables (userSettings, userContext, newsPreferences, +// meditateSettings, sleepSettings, moodSettings, timeSettings, +// invoiceSettings, broadcastSettings, wetterSettings, userTagPresets) +// keep their userId — their ownership model is user-scoped by design. +// +// This migration is destructive at the row level (field is removed). +// Downstream converters (tags-local's toTag / toTagGroup, calc's +// toCalculation / toSavedFormula) already fall back to `'guest'` / +// `''` when userId is absent, so public-type consumers don't break. +// Rollback plan: revert to v34 + restore-from-backup; the `userId` +// field can't be recovered from a forward revert alone. +db.version(35) + .stores({ + articles: 'id, status, savedAt, isFavorite, siteName, originalUrl', + articleHighlights: 'id, articleId, [articleId+startOffset]', + articleTags: 'id, articleId, tagId, [articleId+tagId]', + }) + .upgrade(async (tx) => { + // Mirror of USER_LEVEL_TABLES below — duplicated here because the + // hook-registration loop hasn't run yet when the upgrade fires + // (Dexie applies upgrades before `db.table(...).hook()` calls). + // Keep this list in sync with the one at the hook site. + const USER_LEVEL = new Set([ + 'userSettings', + 'userContext', + 'newsPreferences', + 'meditateSettings', + 'sleepSettings', + 'moodSettings', + 'timeSettings', + 'invoiceSettings', + 'broadcastSettings', + 'wetterSettings', + 'userTagPresets', + ]); + + const dataTables = new Set(); + for (const tables of Object.values(SYNC_APP_MAP)) { + for (const t of tables) if (!USER_LEVEL.has(t)) dataTables.add(t); + } + + for (const name of dataTables) { + if (!tx.db.tables.find((t) => t.name === name)) continue; + await tx + .table(name) + .toCollection() + .modify((record: Record) => { + if ('userId' in record) delete record.userId; + }); + } + }); + // ─── Sync Routing ────────────────────────────────────────── // SYNC_APP_MAP, TABLE_TO_SYNC_NAME, TABLE_TO_APP, SYNC_NAME_TO_TABLE, // toSyncName() and fromSyncName() are now derived from per-module