diff --git a/src/contexts/WorkspaceContext.tsx b/src/contexts/WorkspaceContext.tsx index 59c16381..e045c1b7 100644 --- a/src/contexts/WorkspaceContext.tsx +++ b/src/contexts/WorkspaceContext.tsx @@ -14,7 +14,7 @@ import { useWorkspaceStore } from "@/lib/stores/workspace-store"; * - Workspace list: WorkspaceContext (this file) * - Current workspace & save status: Zustand (workspace-store.ts) * - UI state: Zustand (ui-store.ts) - * - Workspace data: Event sourcing + React Query + * - Workspace data: Zero sync + React Query */ interface WorkspaceContextType { // Workspace list diff --git a/src/lib/db/schema.ts b/src/lib/db/schema.ts index 4c8d1af0..fbac899a 100644 --- a/src/lib/db/schema.ts +++ b/src/lib/db/schema.ts @@ -247,53 +247,6 @@ export const userProfiles = pgTable( ], ); -// Retained temporarily for operational safety; runtime current-state reads no longer use snapshots. -export const workspaceSnapshots = pgTable( - "workspace_snapshots", - { - id: uuid().defaultRandom().primaryKey().notNull(), - workspaceId: uuid("workspace_id").notNull(), - snapshotVersion: integer("snapshot_version").notNull(), - state: jsonb().notNull(), - eventCount: integer("event_count").notNull(), - createdAt: timestamp("created_at", { - withTimezone: true, - mode: "string", - }).defaultNow(), - }, - (table) => [ - index("idx_workspace_snapshots_version").using( - "btree", - table.workspaceId.asc().nullsLast().op("uuid_ops"), - table.snapshotVersion.desc().nullsFirst().op("int4_ops"), - ), - index("idx_workspace_snapshots_workspace").using( - "btree", - table.workspaceId.asc().nullsLast().op("uuid_ops"), - ), - foreignKey({ - columns: [table.workspaceId], - foreignColumns: [workspaces.id], - name: "workspace_snapshots_workspace_id_fkey", - }).onDelete("cascade"), - unique("workspace_snapshots_workspace_id_snapshot_version_key").on( - table.workspaceId, - table.snapshotVersion, - ), - pgPolicy("Service role can insert workspace snapshots", { - as: "permissive", - for: "insert", - to: ["public"], - withCheck: sql`true`, - }), - pgPolicy("Users can read workspace snapshots they have access to", { - as: "permissive", - for: "select", - to: ["public"], - }), - ], -); - const workspaceItemsReadAccessSql = sql`(EXISTS ( SELECT 1 FROM workspaces WHERE ((workspaces.id = workspace_items.workspace_id) AND (workspaces.user_id = (auth.jwt() ->> 'sub'::text))))) OR (EXISTS ( SELECT 1 diff --git a/src/lib/zero/mutators.ts b/src/lib/zero/mutators.ts index eb14e7ef..47475e23 100644 --- a/src/lib/zero/mutators.ts +++ b/src/lib/zero/mutators.ts @@ -353,6 +353,33 @@ async function upsertItem( ); } +async function updateShellOnly( + tx: ZeroTx, + params: { + workspaceId: string; + itemId: string; + shellChanges: Partial<{ + layout: Item["layout"]; + folderId: string | undefined; + lastModified: number; + }>; + }, +) { + await tx.mutate.workspace_items.update({ + workspaceId: params.workspaceId, + itemId: params.itemId, + ...("layout" in params.shellChanges + ? { layout: params.shellChanges.layout ?? null } + : {}), + ...("folderId" in params.shellChanges + ? { folderId: params.shellChanges.folderId ?? null } + : {}), + ...(params.shellChanges.lastModified !== undefined + ? { lastModified: params.shellChanges.lastModified } + : {}), + } as Parameters[0]); +} + async function deleteItemById( tx: ZeroTx, params: { @@ -386,25 +413,12 @@ async function deleteItemById( .where("workspaceId", params.workspaceId) .where("folderId", params.itemId), ); - const now = Date.now(); - for (const child of children) { - const loadedChild = await loadItem(tx, { + await updateShellOnly(tx, { workspaceId: params.workspaceId, itemId: child.itemId, - }); - - if (!loadedChild) { - continue; - } - - await upsertItem(tx, { - workspaceId: params.workspaceId, - userId: null, - sourceVersion: loadedChild.sourceVersion, - item: { - ...loadedChild.item, + shellChanges: { folderId: undefined, layout: undefined, lastModified: now, @@ -545,23 +559,10 @@ export const mutators = defineMutators({ const now = Date.now(); for (const layoutUpdate of args.layoutUpdates) { - const existing = await loadItem(tx, { + await updateShellOnly(tx, { workspaceId: args.workspaceId, itemId: layoutUpdate.id, - }); - - if (!existing) { - throw new ApplicationError( - `Workspace item ${layoutUpdate.id} was not found.`, - ); - } - - await upsertItem(tx, { - workspaceId: args.workspaceId, - userId: ctx.userId, - sourceVersion: existing.sourceVersion, - item: { - ...existing.item, + shellChanges: { layout: { x: layoutUpdate.x, y: layoutUpdate.y, @@ -577,24 +578,11 @@ export const mutators = defineMutators({ ), move: defineMutator( zeroMutatorSchemas.item.move, - async ({ tx, ctx, args }) => { - const existing = await loadItem(tx, { + async ({ tx, args }) => { + await updateShellOnly(tx, { workspaceId: args.workspaceId, itemId: args.itemId, - }); - - if (!existing) { - throw new ApplicationError( - `Workspace item ${args.itemId} was not found.`, - ); - } - - await upsertItem(tx, { - workspaceId: args.workspaceId, - userId: ctx.userId, - sourceVersion: existing.sourceVersion, - item: { - ...existing.item, + shellChanges: { folderId: args.folderId ?? undefined, layout: undefined, lastModified: Date.now(), @@ -604,27 +592,14 @@ export const mutators = defineMutators({ ), moveMany: defineMutator( zeroMutatorSchemas.item.moveMany, - async ({ tx, ctx, args }) => { + async ({ tx, args }) => { const now = Date.now(); for (const itemId of args.itemIds) { - const existing = await loadItem(tx, { + await updateShellOnly(tx, { workspaceId: args.workspaceId, itemId, - }); - - if (!existing) { - throw new ApplicationError( - `Workspace item ${itemId} was not found.`, - ); - } - - await upsertItem(tx, { - workspaceId: args.workspaceId, - userId: ctx.userId, - sourceVersion: existing.sourceVersion, - item: { - ...existing.item, + shellChanges: { folderId: args.folderId ?? undefined, layout: undefined, lastModified: now, @@ -651,23 +626,10 @@ export const mutators = defineMutators({ }); for (const itemId of args.itemIds) { - const existing = await loadItem(tx, { + await updateShellOnly(tx, { workspaceId: args.workspaceId, itemId, - }); - - if (!existing) { - throw new ApplicationError( - `Workspace item ${itemId} was not found.`, - ); - } - - await upsertItem(tx, { - workspaceId: args.workspaceId, - userId: ctx.userId, - sourceVersion: existing.sourceVersion, - item: { - ...existing.item, + shellChanges: { folderId: args.folder.id, layout: undefined, lastModified: now, diff --git a/src/lib/zero/server-mutators.ts b/src/lib/zero/server-mutators.ts index e9672f37..11e7e575 100644 --- a/src/lib/zero/server-mutators.ts +++ b/src/lib/zero/server-mutators.ts @@ -321,14 +321,26 @@ export const serverMutators = defineMutators(sharedMutators, { sharedMutators.item.patchMany, (args) => args.updates.map((update) => update.id), ), - updateMany: withAuth( + updateMany: defineMutator( zeroMutatorSchemas.item.updateMany, - sharedMutators.item.updateMany, - (args) => [ - ...(args.deletedIds ?? []), - ...(args.addedItems ?? []).map((item) => item.id), - ...(args.layoutUpdates ?? []).map((update) => update.id), - ], + async ({ tx, ctx, args }) => { + const wrappedTx = getWrappedTransaction(tx as ServerZeroTx); + await assertWorkspaceWriteAccess(wrappedTx, args.workspaceId, ctx.userId); + await sharedMutators.item.updateMany.fn({ tx, ctx, args }); + + const contentAffectedIds = [ + ...(args.deletedIds ?? []), + ...(args.addedItems ?? []).map((item) => item.id), + ]; + + if (contentAffectedIds.length > 0) { + await syncExtractedRows(wrappedTx, { + workspaceId: args.workspaceId, + itemIds: [...new Set(contentAffectedIds)], + userId: ctx.userId, + }); + } + }, ), move: withAuthOnly(zeroMutatorSchemas.item.move, sharedMutators.item.move), moveMany: withAuthOnly(