fix(convex): validate every node in batch mutations

This commit is contained in:
2026-04-03 17:42:47 +02:00
parent c094f6c80b
commit d3a4c4d335

View File

@@ -40,6 +40,36 @@ async function getCanvasIfAuthorized(
return canvas; return canvas;
} }
async function getValidatedBatchNodesOrThrow(
ctx: MutationCtx,
userId: string,
nodeIds: Id<"nodes">[],
): Promise<{ canvasId: Id<"canvases">; nodes: Doc<"nodes">[] }> {
if (nodeIds.length === 0) {
throw new Error("Batch must contain at least one node id");
}
const nodes: Doc<"nodes">[] = [];
for (const nodeId of nodeIds) {
const node = await ctx.db.get(nodeId);
if (!node) {
throw new Error("Node not found");
}
nodes.push(node);
}
const canvasId = nodes[0].canvasId;
for (const node of nodes) {
if (node.canvasId !== canvasId) {
throw new Error("All nodes must belong to the same canvas");
}
}
await getCanvasOrThrow(ctx, canvasId, userId);
return { canvasId, nodes };
}
type NodeCreateMutationName = type NodeCreateMutationName =
| "nodes.create" | "nodes.create"
| "nodes.createWithEdgeSplit" | "nodes.createWithEdgeSplit"
@@ -1252,16 +1282,18 @@ export const batchMove = mutation({
const user = await requireAuth(ctx); const user = await requireAuth(ctx);
if (moves.length === 0) return; if (moves.length === 0) return;
// Canvas-Zugriff über den ersten Node prüfen const nodeIds = moves.map((move) => move.nodeId);
const firstNode = await ctx.db.get(moves[0].nodeId); const { canvasId } = await getValidatedBatchNodesOrThrow(
if (!firstNode) throw new Error("Node not found"); ctx,
await getCanvasOrThrow(ctx, firstNode.canvasId, user.userId); user.userId,
nodeIds,
);
for (const { nodeId, positionX, positionY } of moves) { for (const { nodeId, positionX, positionY } of moves) {
await ctx.db.patch(nodeId, { positionX, positionY }); await ctx.db.patch(nodeId, { positionX, positionY });
} }
await ctx.db.patch(firstNode.canvasId, { updatedAt: Date.now() }); await ctx.db.patch(canvasId, { updatedAt: Date.now() });
}, },
}); });
@@ -1427,23 +1459,19 @@ export const batchRemove = mutation({
const user = await requireAuth(ctx); const user = await requireAuth(ctx);
if (nodeIds.length === 0) return; if (nodeIds.length === 0) return;
// Idempotent: wenn alle Nodes bereits weg sind, no-op. const { canvasId, nodes } = await getValidatedBatchNodesOrThrow(
const firstExistingNode = await (async () => { ctx,
for (const nodeId of nodeIds) { user.userId,
const node = await ctx.db.get(nodeId); nodeIds,
if (node) return node; );
const uniqueNodes = new Map<Id<"nodes">, Doc<"nodes">>();
for (const node of nodes) {
uniqueNodes.set(node._id, node);
} }
return null;
})();
if (!firstExistingNode) return;
// Canvas-Zugriff über den ersten vorhandenen Node prüfen for (const node of uniqueNodes.values()) {
const firstNode = firstExistingNode; const nodeId = node._id;
await getCanvasOrThrow(ctx, firstNode.canvasId, user.userId);
for (const nodeId of nodeIds) {
const node = await ctx.db.get(nodeId);
if (!node) continue;
// Alle Edges entfernen, die diesen Node als Source oder Target haben // Alle Edges entfernen, die diesen Node als Source oder Target haben
const sourceEdges = await ctx.db const sourceEdges = await ctx.db
@@ -1475,6 +1503,6 @@ export const batchRemove = mutation({
await ctx.db.delete(nodeId); await ctx.db.delete(nodeId);
} }
await ctx.db.patch(firstNode.canvasId, { updatedAt: Date.now() }); await ctx.db.patch(canvasId, { updatedAt: Date.now() });
}, },
}); });