mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 22:01:09 +02:00
refactor(scope): explicit spaceId stamping at every space-scoped write
Schicht A Etappe 1 of the workbench-seeding-cleanup plan: every module store that writes to a space-scoped Dexie table now sets `spaceId` explicitly via `getEffectiveSpaceId()` instead of relying on the creating-hook's silent auto-stamp. Etappe 2 (flip the hook to throw on missing spaceId) follows after a soak day. New helper: - `data/scope/scoped-db.ts` — `getEffectiveSpaceId()` returns the active Space's id when one is loaded, falling back to the personal sentinel `_personal:<userId>` for guests / pre-bootstrap windows. Symmetric with `getEffectiveUserId()`. Re-exported from `data/scope/index.ts`. Migrated call sites (16 writes across 10 modules): picture: boards, boardItems events: eventGuests, eventItems companion: companionConversations, companionMessages calc: savedFormulas, calculations quotes: quotesFavorites, customQuotes skilltree: skills, achievements moodlit: moods plants: wateringSchedules questions: answers (manual + research-driven paths) data/ai: agents, agentKontextDocs The audit also flagged `_serverIterationExecutions`, but it's an internal infra table (leading underscore, not in SYNC_APP_MAP, never synced) — the creating-hook doesn't run on it, so no change needed. No behaviour change today: the hook still falls through to sentinel- stamping when spaceId is missing, so existing rows + any unmigrated caller keep working. Etappe 2 flips that fallback to a hard error, turning silent omissions into write-time failures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
42828434a4
commit
43bef2b24b
17 changed files with 60 additions and 16 deletions
|
|
@ -27,6 +27,7 @@ import { encryptRecord } from '../../crypto';
|
|||
import type { Mission } from '../missions/types';
|
||||
import { MISSIONS_TABLE } from '../missions/types';
|
||||
import { getActiveSpace } from '../../scope/active-space.svelte';
|
||||
import { getEffectiveSpaceId } from '../../scope/scoped-db';
|
||||
import { DEFAULT_AI_POLICY } from '../policy';
|
||||
import { getAgent } from './store';
|
||||
import type { Agent } from './types';
|
||||
|
|
@ -105,7 +106,7 @@ export async function ensureDefaultAgent(): Promise<Agent> {
|
|||
const toWrite: Agent = { ...agent };
|
||||
await encryptRecord(AGENTS_TABLE, toWrite);
|
||||
try {
|
||||
await db.table(AGENTS_TABLE).add(toWrite);
|
||||
await db.table(AGENTS_TABLE).add({ ...toWrite, spaceId: getEffectiveSpaceId() });
|
||||
} catch (err) {
|
||||
// Race: another tab just wrote the same id. Re-fetch and return
|
||||
// that tab's record.
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
import { db } from '../../database';
|
||||
import { encryptRecord, decryptRecords } from '../../crypto';
|
||||
import { getEffectiveSpaceId } from '../../scope/scoped-db';
|
||||
|
||||
const TABLE = 'agentKontextDocs';
|
||||
|
||||
|
|
@ -65,6 +66,6 @@ export async function saveAgentKontext(agentId: string, content: string): Promis
|
|||
content,
|
||||
};
|
||||
await encryptRecord(TABLE, doc);
|
||||
await db.table<LocalAgentKontextDoc>(TABLE).add(doc);
|
||||
await db.table(TABLE).add({ ...doc, spaceId: getEffectiveSpaceId() });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ export {
|
|||
scopedGet,
|
||||
assertModuleAllowed,
|
||||
getInScopeSpaceIds,
|
||||
getEffectiveSpaceId,
|
||||
ScopeNotReadyError,
|
||||
ModuleNotInSpaceError,
|
||||
} from './scoped-db';
|
||||
|
|
|
|||
|
|
@ -66,6 +66,25 @@ export function getInScopeSpaceIds(): string[] {
|
|||
return [sentinel];
|
||||
}
|
||||
|
||||
/**
|
||||
* The spaceId every new write to a space-scoped table should carry.
|
||||
* Returns the active Space's id when one is loaded, falling back to the
|
||||
* personal sentinel `_personal:<userId>` for guests / pre-bootstrap
|
||||
* windows. The sentinel value matches what `reconcileSentinels` rewrites
|
||||
* to the real personal-space id once `loadActiveSpace` resolves, so no
|
||||
* row gets stranded.
|
||||
*
|
||||
* Module stores call this and stamp `spaceId` on the record explicitly
|
||||
* before `.add()` / `.put()`. Once Schicht A flips the creating-hook to
|
||||
* throw on missing spaceId (see docs/plans/workbench-seeding-cleanup.md),
|
||||
* forgetting this call is a hard error instead of silent corruption.
|
||||
*/
|
||||
export function getEffectiveSpaceId(): string {
|
||||
const active = getActiveSpaceId();
|
||||
if (active) return active;
|
||||
return personalSpaceSentinel(getEffectiveUserId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a Collection that applies the space filter — chainable with any
|
||||
* further `.where()`, `.filter()`, `.toArray()`, `.modify()`.
|
||||
|
|
|
|||
|
|
@ -4,17 +4,19 @@
|
|||
|
||||
import { db } from '$lib/data/database';
|
||||
import { CalcEvents } from '@mana/shared-utils/analytics';
|
||||
import { getEffectiveSpaceId } from '$lib/data/scope';
|
||||
import type { LocalCalculation } from '../types';
|
||||
import type { CreateCalculationInput } from '@calc/shared';
|
||||
|
||||
export const calculationsStore = {
|
||||
async addCalculation(input: CreateCalculationInput) {
|
||||
await db.table<LocalCalculation>('calculations').add({
|
||||
await db.table('calculations').add({
|
||||
id: crypto.randomUUID(),
|
||||
mode: input.mode,
|
||||
expression: input.expression,
|
||||
result: input.result,
|
||||
skin: input.skin,
|
||||
spaceId: getEffectiveSpaceId(),
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,17 +4,19 @@
|
|||
|
||||
import { db } from '$lib/data/database';
|
||||
import { CalcEvents } from '@mana/shared-utils/analytics';
|
||||
import { getEffectiveSpaceId } from '$lib/data/scope';
|
||||
import type { LocalSavedFormula } from '../types';
|
||||
import type { CreateFormulaInput, UpdateFormulaInput } from '@calc/shared';
|
||||
|
||||
export const savedFormulasStore = {
|
||||
async saveFormula(input: CreateFormulaInput) {
|
||||
await db.table<LocalSavedFormula>('savedFormulas').add({
|
||||
await db.table('savedFormulas').add({
|
||||
id: crypto.randomUUID(),
|
||||
name: input.name,
|
||||
expression: input.expression,
|
||||
description: input.description ?? null,
|
||||
mode: input.mode,
|
||||
spaceId: getEffectiveSpaceId(),
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import { db } from '$lib/data/database';
|
||||
import { emitDomainEvent } from '$lib/data/events';
|
||||
import { getEffectiveSpaceId } from '$lib/data/scope';
|
||||
import type { LocalConversation, LocalMessage } from '../types';
|
||||
|
||||
const CONV_TABLE = 'companionConversations';
|
||||
|
|
@ -24,7 +25,7 @@ export const chatStore = {
|
|||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
await db.table<LocalConversation>(CONV_TABLE).add(conv);
|
||||
await db.table(CONV_TABLE).add({ ...conv, spaceId: getEffectiveSpaceId() });
|
||||
emitDomainEvent('CompanionConversationStarted', 'companion', CONV_TABLE, conv.id, {
|
||||
conversationId: conv.id,
|
||||
title: conv.title,
|
||||
|
|
@ -66,7 +67,7 @@ export const chatStore = {
|
|||
toolResult: extra?.toolResult,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
await db.table<LocalMessage>(MSG_TABLE).add(msg);
|
||||
await db.table(MSG_TABLE).add({ ...msg, spaceId: getEffectiveSpaceId() });
|
||||
|
||||
// Touch conversation updatedAt
|
||||
await db.table<LocalConversation>(CONV_TABLE).update(conversationId, {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
import { db } from '$lib/data/database';
|
||||
import { encryptRecord } from '$lib/data/crypto';
|
||||
import { getEffectiveSpaceId } from '$lib/data/scope';
|
||||
import type { LocalEventGuest, RsvpStatus } from '../types';
|
||||
|
||||
let error = $state<string | null>(null);
|
||||
|
|
@ -44,7 +45,7 @@ export const eventGuestsStore = {
|
|||
// records stay local-only — they're never pushed to the
|
||||
// public RSVP snapshot, so no decrypt-before-publish here.
|
||||
await encryptRecord('eventGuests', newGuest);
|
||||
await db.table<LocalEventGuest>('eventGuests').add(newGuest);
|
||||
await db.table('eventGuests').add({ ...newGuest, spaceId: getEffectiveSpaceId() });
|
||||
return { success: true as const, id };
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to add guest';
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
*/
|
||||
|
||||
import { db } from '$lib/data/database';
|
||||
import { getEffectiveSpaceId } from '$lib/data/scope';
|
||||
import type { LocalEventItem } from '../types';
|
||||
import { eventsStore } from './events.svelte';
|
||||
|
||||
|
|
@ -47,7 +48,7 @@ export const eventItemsStore = {
|
|||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
await db.table<LocalEventItem>('eventItems').add(newItem);
|
||||
await db.table('eventItems').add({ ...newItem, spaceId: getEffectiveSpaceId() });
|
||||
void eventsStore.syncItems(input.eventId);
|
||||
return { success: true as const, id };
|
||||
} catch (e) {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
import { db } from '$lib/data/database';
|
||||
import { MoodlitEvents } from '@mana/shared-utils/analytics';
|
||||
import { createBlock, updateBlock } from '$lib/data/time-blocks/service';
|
||||
import { getEffectiveSpaceId } from '$lib/data/scope';
|
||||
import type { LocalMood } from '../types';
|
||||
import type { Mood, MoodSettings } from '../types';
|
||||
|
||||
|
|
@ -128,12 +129,13 @@ function createMoodsStore() {
|
|||
|
||||
// IndexedDB mutation methods
|
||||
async createMood(data: { name: string; colors: string[]; animation: string }) {
|
||||
await db.table<LocalMood>('moods').add({
|
||||
await db.table('moods').add({
|
||||
id: crypto.randomUUID(),
|
||||
name: data.name,
|
||||
colors: data.colors,
|
||||
animation: data.animation,
|
||||
isDefault: false,
|
||||
spaceId: getEffectiveSpaceId(),
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
import { db } from '$lib/data/database';
|
||||
import { encryptRecord, decryptRecord } from '$lib/data/crypto';
|
||||
import { emitDomainEvent } from '$lib/data/events';
|
||||
import { getActiveSpace } from '$lib/data/scope';
|
||||
import { getActiveSpace, getEffectiveSpaceId } from '$lib/data/scope';
|
||||
import { getEffectiveUserId } from '$lib/data/current-user';
|
||||
import {
|
||||
defaultVisibilityFor,
|
||||
|
|
@ -56,7 +56,7 @@ export const boardsStore = {
|
|||
// mutates `newLocal` in place — UI consumers expect plaintext.
|
||||
const plaintextSnapshot = toBoard({ ...newLocal });
|
||||
await encryptRecord('boards', newLocal);
|
||||
await db.table<LocalBoard>('boards').add(newLocal);
|
||||
await db.table('boards').add({ ...newLocal, spaceId: getEffectiveSpaceId() });
|
||||
return { success: true, data: plaintextSnapshot };
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to create board';
|
||||
|
|
@ -170,7 +170,7 @@ export const boardsStore = {
|
|||
updatedAt: now,
|
||||
};
|
||||
await encryptRecord('boardItems', newItem);
|
||||
await db.table<LocalBoardItem>('boardItems').add(newItem);
|
||||
await db.table('boardItems').add({ ...newItem, spaceId: getEffectiveSpaceId() });
|
||||
}
|
||||
|
||||
return { success: true, data: plaintextSnapshot };
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { PlantsEvents } from '@mana/shared-utils/analytics';
|
|||
import { encryptRecord, decryptRecord } from '$lib/data/crypto';
|
||||
import { emitDomainEvent } from '$lib/data/events';
|
||||
import { createBlock } from '$lib/data/time-blocks/service';
|
||||
import { getEffectiveSpaceId } from '$lib/data/scope';
|
||||
import { uploadPlantPhoto, identifyPlant, type IdentifyResult } from './api';
|
||||
import type {
|
||||
LocalPlant,
|
||||
|
|
@ -194,6 +195,7 @@ export const wateringMutations = {
|
|||
nextWateringAt: nextDate.toISOString(),
|
||||
reminderEnabled: false,
|
||||
reminderHoursBefore: 0,
|
||||
spaceId: getEffectiveSpaceId(),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
import { db } from '$lib/data/database';
|
||||
import { encryptRecord, decryptRecord } from '$lib/data/crypto';
|
||||
import { emitDomainEvent } from '$lib/data/events';
|
||||
import { getEffectiveSpaceId } from '$lib/data/scope';
|
||||
import { researchApi, type ResearchEvent, type ResearchSource } from '$lib/api/research';
|
||||
import type { LocalAnswer, LocalQuestion } from '../types';
|
||||
|
||||
|
|
@ -43,6 +44,7 @@ async function createManual(input: CreateManualAnswerInput): Promise<string> {
|
|||
citations: [],
|
||||
rating: null,
|
||||
isAccepted: false,
|
||||
spaceId: getEffectiveSpaceId(),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
|
@ -108,6 +110,7 @@ async function startResearch(opts: StartResearchOptions): Promise<ResearchHandle
|
|||
citations: [],
|
||||
rating: null,
|
||||
isAccepted: false,
|
||||
spaceId: getEffectiveSpaceId(),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
*/
|
||||
|
||||
import { db } from '$lib/data/database';
|
||||
import { getEffectiveSpaceId } from '$lib/data/scope';
|
||||
import type { LocalCustomQuote } from '../types';
|
||||
|
||||
export interface CustomQuoteInput {
|
||||
|
|
@ -18,13 +19,14 @@ export const customQuotesStore = {
|
|||
async create(input: CustomQuoteInput): Promise<string> {
|
||||
const now = new Date().toISOString();
|
||||
const id = `custom-${crypto.randomUUID()}`;
|
||||
await db.table<LocalCustomQuote>('customQuotes').add({
|
||||
await db.table('customQuotes').add({
|
||||
id,
|
||||
text: input.text,
|
||||
author: input.author,
|
||||
category: input.category ?? null,
|
||||
source: input.source ?? null,
|
||||
year: input.year ?? null,
|
||||
spaceId: getEffectiveSpaceId(),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,15 +5,17 @@
|
|||
*/
|
||||
|
||||
import { db } from '$lib/data/database';
|
||||
import { getEffectiveSpaceId } from '$lib/data/scope';
|
||||
import type { LocalFavorite } from '../types';
|
||||
import type { Favorite } from '../queries';
|
||||
|
||||
export const favoritesStore = {
|
||||
async add(quoteId: string) {
|
||||
const now = new Date().toISOString();
|
||||
await db.table<LocalFavorite>('quotesFavorites').add({
|
||||
await db.table('quotesFavorites').add({
|
||||
id: crypto.randomUUID(),
|
||||
quoteId,
|
||||
spaceId: getEffectiveSpaceId(),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { db } from '$lib/data/database';
|
||||
import { getEffectiveSpaceId } from '$lib/data/scope';
|
||||
import type {
|
||||
AchievementWithStatus,
|
||||
AchievementUnlockResult,
|
||||
|
|
@ -95,13 +96,14 @@ async function seedIfEmpty() {
|
|||
const active = stored.filter((a) => !a.deletedAt);
|
||||
if (active.length === 0) {
|
||||
for (const def of ACHIEVEMENT_DEFINITIONS) {
|
||||
await db.table<LocalAchievement>('achievements').add({
|
||||
await db.table('achievements').add({
|
||||
id: def.id,
|
||||
key: def.id,
|
||||
name: def.name,
|
||||
description: def.description,
|
||||
icon: def.icon,
|
||||
unlockedAt: '',
|
||||
spaceId: getEffectiveSpaceId(),
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { db } from '$lib/data/database';
|
||||
import { emitDomainEvent } from '$lib/data/events';
|
||||
import { getEffectiveSpaceId } from '$lib/data/scope';
|
||||
import type { Skill } from '../types';
|
||||
import { calculateLevel, createDefaultSkill, createActivity } from '../types';
|
||||
import type { LocalSkill, LocalActivity } from '../types';
|
||||
|
|
@ -29,8 +30,9 @@ async function addSkill(data: Partial<Skill>): Promise<Skill> {
|
|||
totalXp: skill.totalXp,
|
||||
level: skill.level,
|
||||
};
|
||||
await db.table<LocalSkill>('skills').add({
|
||||
await db.table('skills').add({
|
||||
...localSkill,
|
||||
spaceId: getEffectiveSpaceId(),
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue