mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-23 00:26:41 +02:00
feat(spaces): thread space_id through mana-sync protocol + storage
Server-side: - sync_changes gains a nullable space_id TEXT column + partial index on (user_id, space_id, app_id, created_at) WHERE space_id IS NOT NULL. - RecordChange takes spaceID as a first-class parameter; *string so empty strings land as real SQL NULL and the partial index skips them. - ChangeRow + all three SELECTs (GetChangesSince, GetAllChangesSince, StreamAllUserChanges) propagate space_id through to clients. - changeFromRow surfaces SpaceID on the wire Change shape. - New extractSpaceID helper reads the incoming payload — prefers top- level spaceId, falls back to data.spaceId (inserts) or fields.spaceId.value (updates). Tolerates pre-v28 clients. - 6 Go tests cover the helper + round-trip. Client-side: - PendingChange gains an optional spaceId. - Dexie creating hook stamps spaceId from the active record onto the pending-change row (already set by the v28 scope hook). - Dexie updating hook reads spaceId from the pre-update record and stamps it on the pending-change so updates carry space context even though spaceId itself is immutable and never in `fields`. - buildChangeset forwards spaceId to the server. Explicitly NOT in scope this pass: - RLS remains user_id-scoped; multi-member shared-space reads need a second policy that joins against auth.members. Follow-up once shared spaces are actually used — today everything is personal. - Subscription fan-out is still per-user; fan-out to all members of a shared space is part of the same follow-up. Go tests: 6/6 pass. Web type-check clean (0 errors across 7139 files). Plan: docs/plans/spaces-foundation.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9f7d2f24b3
commit
e10c2436a6
6 changed files with 205 additions and 11 deletions
|
|
@ -882,6 +882,7 @@ for (const [appId, tables] of Object.entries(SYNC_APP_MAP)) {
|
|||
data: dataForSync,
|
||||
actor,
|
||||
createdAt: now,
|
||||
spaceId: typeof objRecord.spaceId === 'string' ? (objRecord.spaceId as string) : undefined,
|
||||
});
|
||||
trackActivity(appId, tableName, obj.id, 'insert');
|
||||
trackFirstContent(appId);
|
||||
|
|
@ -937,6 +938,14 @@ for (const [appId, tables] of Object.entries(SYNC_APP_MAP)) {
|
|||
}
|
||||
|
||||
const op = (modifications as Record<string, unknown>).deletedAt ? 'delete' : 'update';
|
||||
// spaceId is immutable and therefore not in `fields` for updates —
|
||||
// but the server wants it as a first-class column on every row.
|
||||
// Read it from the pre-update record so the pending-change row
|
||||
// carries the right space for routing even when only a title changed.
|
||||
const existingSpaceId =
|
||||
typeof (obj as Record<string, unknown>).spaceId === 'string'
|
||||
? ((obj as Record<string, unknown>).spaceId as string)
|
||||
: undefined;
|
||||
trackPendingChange(tableName, {
|
||||
appId,
|
||||
collection: tableName,
|
||||
|
|
@ -946,6 +955,7 @@ for (const [appId, tables] of Object.entries(SYNC_APP_MAP)) {
|
|||
actor,
|
||||
deletedAt: (modifications as Record<string, unknown>).deletedAt as string | undefined,
|
||||
createdAt: now,
|
||||
spaceId: existingSpaceId,
|
||||
});
|
||||
trackActivity(appId, tableName, primKey as string, op);
|
||||
fireTrigger(appId, tableName, op, modifications as Record<string, unknown>);
|
||||
|
|
|
|||
|
|
@ -100,6 +100,14 @@ interface PendingChange {
|
|||
deletedAt?: string;
|
||||
actor?: Actor;
|
||||
createdAt: string;
|
||||
/**
|
||||
* The Space (Better Auth organization id) the record belongs to. Stamped
|
||||
* on the pending-change row at write time so the server gets it as a
|
||||
* first-class column even for updates (where it isn't in `fields`
|
||||
* because it's immutable). Empty string / undefined means "pre-v28
|
||||
* record" — the server tolerates NULL on the column.
|
||||
*/
|
||||
spaceId?: string;
|
||||
}
|
||||
|
||||
interface SyncMeta {
|
||||
|
|
@ -1120,6 +1128,7 @@ export function createUnifiedSync(
|
|||
data: p.data,
|
||||
deletedAt: p.deletedAt,
|
||||
actor: p.actor,
|
||||
spaceId: p.spaceId,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue