mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 00:01:10 +02:00
feat(db): Phase 2c — stop stamping userId on data-record writes
The creating-hook now splits its user-stamping behaviour by table: - USER_LEVEL_TABLES (userSettings, userContext, newsPreferences, meditateSettings, sleepSettings, moodSettings, timeSettings, invoiceSettings, broadcastSettings, wetterSettings, userTagPresets) still get userId stamped — these rows are primarily scoped to the signed-in user rather than a Space. - All other sync tables (the ~53 data-record tables) no longer receive userId on new writes. Attribution is the Actor system's job (__lastActor + __fieldActors are already stamped on every write); tenancy is the spaceId column's job (stamped below in the same hook). Keeping both userId and spaceId on data records was redundant. Migration approach — lenient, no Dexie bump: existing rows keep the userId they were stamped with in v28. New writes don't have it. The three public type converters that exposed userId (tags-local's toTag/toTagGroup, calc's toCalculation/toSavedFormula) use a `?? 'guest'` / `?? ''` fallback, so rows without userId stay readable. The 16-site codebase audit in phase 2c found no load- bearing reader: the few sites that reference record.userId are either one-time migration code (v28/v31/guest-migration), manifest metadata (backup format — different userId field), or the hook's own immutability guard. authorId stamping now derives from effectiveUserId directly instead of reading objRecord.userId — the previous chain relied on the userId stamp having just happened, which no longer holds for data tables. The "no table has both userId AND spaceId" invariant from the plan is now partially met: data tables will converge on it as old rows cycle out. User-level tables still have both but that's by design (userId = ownership, spaceId = v28 Personal-sentinel carried through the hook; a future cleanup could drop the spaceId on user-level tables but it's harmless today). Tests: 20/20 agents + workbench-scenes pass. Type-check clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d5b889ac58
commit
e9b9544ea3
1 changed files with 47 additions and 8 deletions
|
|
@ -936,6 +936,36 @@ function isInternalKey(key: string): boolean {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tables whose rows are scoped to a specific user rather than to a
|
||||
* Space. These are singletons or small lookups tied to the signed-in
|
||||
* identity (preferences, the profile hub, per-user templates). The
|
||||
* creating-hook continues to stamp `userId` on these; data tables
|
||||
* (tasks, events, tags, …) stopped carrying `userId` in Phase 2c of
|
||||
* the space-scoped data model rollout — attribution there lives on
|
||||
* the Actor fields (`__lastActor` / `__fieldActors`) and tenancy on
|
||||
* `spaceId`.
|
||||
*
|
||||
* Keeping this list explicit instead of inferring by naming
|
||||
* convention: the audit in docs/plans/space-scoped-data-model.md
|
||||
* appendix enumerates exactly which tables need user-level stamping,
|
||||
* and a typo here would silently re-introduce `userId` on a data
|
||||
* table.
|
||||
*/
|
||||
const USER_LEVEL_TABLES: ReadonlySet<string> = new Set([
|
||||
'userSettings',
|
||||
'userContext',
|
||||
'newsPreferences',
|
||||
'meditateSettings',
|
||||
'sleepSettings',
|
||||
'moodSettings',
|
||||
'timeSettings',
|
||||
'invoiceSettings',
|
||||
'broadcastSettings',
|
||||
'wetterSettings',
|
||||
'userTagPresets',
|
||||
]);
|
||||
|
||||
for (const [appId, tables] of Object.entries(SYNC_APP_MAP)) {
|
||||
for (const tableName of tables) {
|
||||
const table = db.table(tableName);
|
||||
|
|
@ -948,12 +978,20 @@ for (const [appId, tables] of Object.entries(SYNC_APP_MAP)) {
|
|||
// trackPendingChange below. Freezing it here is the authoritative step.
|
||||
const actor: Actor = getCurrentActor();
|
||||
|
||||
// Auto-stamp the active user. Module stores never set userId themselves,
|
||||
// preventing accidental impersonation and removing all hardcoded
|
||||
// 'guest'/'local' fallbacks scattered across query files.
|
||||
// Auto-stamp the active user. Module stores never set userId
|
||||
// themselves. After Phase 2c, user-level tables (userSettings,
|
||||
// invoiceSettings, userTagPresets, …) continue to carry a
|
||||
// userId column because their records are primarily scoped to
|
||||
// the user, not a Space. Data tables (tasks, events, tags, …)
|
||||
// no longer get userId stamped — attribution on data records
|
||||
// lives on the Actor fields (__lastActor / __fieldActors) and
|
||||
// tenancy on spaceId.
|
||||
const objRecord = obj as Record<string, unknown>;
|
||||
if (objRecord.userId === undefined || objRecord.userId === null) {
|
||||
objRecord.userId = getEffectiveUserId();
|
||||
const effectiveUserId = getEffectiveUserId();
|
||||
if (USER_LEVEL_TABLES.has(tableName)) {
|
||||
if (objRecord.userId === undefined || objRecord.userId === null) {
|
||||
objRecord.userId = effectiveUserId;
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-stamp the Space-scope fields. Until the scope bootstrap
|
||||
|
|
@ -962,12 +1000,13 @@ for (const [appId, tables] of Object.entries(SYNC_APP_MAP)) {
|
|||
// 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.
|
||||
// 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:${objRecord.userId as string}`;
|
||||
objRecord.spaceId = `_personal:${effectiveUserId}`;
|
||||
}
|
||||
if (objRecord.authorId === undefined || objRecord.authorId === null) {
|
||||
objRecord.authorId = objRecord.userId as string;
|
||||
objRecord.authorId = effectiveUserId;
|
||||
}
|
||||
if (objRecord.visibility === undefined || objRecord.visibility === null) {
|
||||
objRecord.visibility = 'space';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue