mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 00:01:10 +02:00
feat(db): Phase 2c-followup #2 — strip Space-scope fields from user-level tables
Completes the "no table has both userId AND spaceId" invariant from
the space-scoped plan. Phase 2c-followup v35 cleaned the userId
column off data-record tables; this follow-up cleans the inverse off
user-level singleton tables (userSettings, invoiceSettings, …).
Hook change: user-level tables no longer receive spaceId / authorId /
visibility stamps on new writes. Those three fields are only
meaningful for tenant-scoped data; stamping them on user-level rows
was v28 collateral damage from the blanket migration.
Dexie v36 upgrade: deletes spaceId + authorId + visibility from
every row in the 11 user-level tables. No schema change — these
fields were never indexed on user-level tables, so .stores() stays
untouched.
Safety check before shipping: grep showed zero callers use
scopedTable(<user-level-table>) or .where('spaceId') against these
tables. They're queried directly by userId (via shared-stores or
singleton lookups), so dropping the space columns is a pure cleanup.
After this ships, user-level tables have {userId, …fields} and data
tables have {spaceId, __lastActor, …fields} — the invariant is
truly met app-wide.
Type-check clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
59a679a2f9
commit
ce5d1f1a2a
1 changed files with 71 additions and 19 deletions
|
|
@ -852,6 +852,49 @@ db.version(35)
|
|||
}
|
||||
});
|
||||
|
||||
// v36 — Phase 2c-followup #2: strip the Space-scope fields from
|
||||
// user-level singleton tables. v28 blanket-stamped every row with
|
||||
// `spaceId=_personal:<userId>` + `authorId` + `visibility`, but
|
||||
// user-level tables (userSettings, invoiceSettings, …) are genuinely
|
||||
// user-scoped — they're never queried through scopedTable. Those
|
||||
// fields were dead weight + a subtle invariant violation ("user-level
|
||||
// table with a spaceId stamp is either user-level or space-level,
|
||||
// pick one"). Hook was updated in the same commit to stop stamping
|
||||
// them on user-level tables going forward; this upgrade cleans up
|
||||
// the historical rows.
|
||||
//
|
||||
// Grep verified: zero callers use `scopedTable(<user-level-table>)`
|
||||
// or `.where('spaceId')` against these tables, so dropping is safe.
|
||||
// No schema change — the columns weren't indexed on user-level
|
||||
// tables, so there's nothing to re-declare in .stores().
|
||||
db.version(36).upgrade(async (tx) => {
|
||||
const USER_LEVEL = [
|
||||
'userSettings',
|
||||
'userContext',
|
||||
'newsPreferences',
|
||||
'meditateSettings',
|
||||
'sleepSettings',
|
||||
'moodSettings',
|
||||
'timeSettings',
|
||||
'invoiceSettings',
|
||||
'broadcastSettings',
|
||||
'wetterSettings',
|
||||
'userTagPresets',
|
||||
];
|
||||
|
||||
for (const name of USER_LEVEL) {
|
||||
if (!tx.db.tables.find((t) => t.name === name)) continue;
|
||||
await tx
|
||||
.table(name)
|
||||
.toCollection()
|
||||
.modify((record: Record<string, unknown>) => {
|
||||
if ('spaceId' in record) delete record.spaceId;
|
||||
if ('authorId' in record) delete record.authorId;
|
||||
if ('visibility' in record) delete record.visibility;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ─── 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
|
||||
|
|
@ -1058,28 +1101,37 @@ for (const [appId, tables] of Object.entries(SYNC_APP_MAP)) {
|
|||
// tenancy on spaceId.
|
||||
const objRecord = obj as Record<string, unknown>;
|
||||
const effectiveUserId = getEffectiveUserId();
|
||||
if (USER_LEVEL_TABLES.has(tableName)) {
|
||||
const isUserLevel = USER_LEVEL_TABLES.has(tableName);
|
||||
|
||||
if (isUserLevel) {
|
||||
if (objRecord.userId === undefined || objRecord.userId === null) {
|
||||
objRecord.userId = effectiveUserId;
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-stamp the Space-scope fields. Until the scope bootstrap
|
||||
// (see `./scope/active-space.svelte.ts`) resolves the user's
|
||||
// personal-space id from Better Auth, new records carry a
|
||||
// deterministic sentinel `_personal:<userId>` that the bootstrap
|
||||
// rewrites in a single pass. Module stores set spaceId explicitly
|
||||
// once they start writing into non-personal spaces — this stamp
|
||||
// only fills the gap. Sentinel uses `effectiveUserId` directly
|
||||
// now that `userId` may not be present on the record itself.
|
||||
if (objRecord.spaceId === undefined || objRecord.spaceId === null) {
|
||||
objRecord.spaceId = `_personal:${effectiveUserId}`;
|
||||
}
|
||||
if (objRecord.authorId === undefined || objRecord.authorId === null) {
|
||||
objRecord.authorId = effectiveUserId;
|
||||
}
|
||||
if (objRecord.visibility === undefined || objRecord.visibility === null) {
|
||||
objRecord.visibility = 'space';
|
||||
// User-level tables DON'T get Space-scope fields — they're
|
||||
// genuinely user-scoped, not tenant-scoped. v28 stamped
|
||||
// them anyway as a byproduct of the blanket migration;
|
||||
// Phase 2c-followup removed those fields retroactively
|
||||
// (see v36 below). Skipping the stamps here keeps future
|
||||
// rows clean.
|
||||
} else {
|
||||
// Auto-stamp the Space-scope fields on data tables. Until
|
||||
// the scope bootstrap (see `./scope/active-space.svelte.ts`)
|
||||
// resolves the user's personal-space id from Better Auth,
|
||||
// new records carry a deterministic sentinel
|
||||
// `_personal:<userId>` that the bootstrap rewrites in a
|
||||
// single pass. Module stores set spaceId explicitly once
|
||||
// they start writing into non-personal spaces — this stamp
|
||||
// only fills the gap. Sentinel uses `effectiveUserId`
|
||||
// directly since `userId` isn't on data records anymore.
|
||||
if (objRecord.spaceId === undefined || objRecord.spaceId === null) {
|
||||
objRecord.spaceId = `_personal:${effectiveUserId}`;
|
||||
}
|
||||
if (objRecord.authorId === undefined || objRecord.authorId === null) {
|
||||
objRecord.authorId = effectiveUserId;
|
||||
}
|
||||
if (objRecord.visibility === undefined || objRecord.visibility === null) {
|
||||
objRecord.visibility = 'space';
|
||||
}
|
||||
}
|
||||
|
||||
// Stamp every real field with the create-time so future LWW comparisons
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue