feat: enhance type definitions in generated API and data model
- Added new module imports for canvases, credits, edges, helpers, and nodes in api.d.ts - Improved type safety in dataModel.d.ts by utilizing DataModelFromSchemaDefinition and DocumentByName - Updated Doc and Id types to reflect schema definitions for better type checking
This commit is contained in:
327
convex/nodes.ts
Normal file
327
convex/nodes.ts
Normal file
@@ -0,0 +1,327 @@
|
||||
import { query, mutation, QueryCtx, MutationCtx } from "./_generated/server";
|
||||
import { v } from "convex/values";
|
||||
import { requireAuth } from "./helpers";
|
||||
import type { Doc, Id } from "./_generated/dataModel";
|
||||
|
||||
// ============================================================================
|
||||
// Interne Helpers
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Prüft ob der User Zugriff auf den Canvas hat und gibt ihn zurück.
|
||||
*/
|
||||
async function getCanvasOrThrow(
|
||||
ctx: QueryCtx | MutationCtx,
|
||||
canvasId: Id<"canvases">,
|
||||
userId: string
|
||||
) {
|
||||
const canvas = await ctx.db.get(canvasId);
|
||||
if (!canvas || canvas.ownerId !== userId) {
|
||||
throw new Error("Canvas not found");
|
||||
}
|
||||
return canvas;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Queries
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Alle Nodes eines Canvas laden.
|
||||
*/
|
||||
export const list = query({
|
||||
args: { canvasId: v.id("canvases") },
|
||||
handler: async (ctx, { canvasId }) => {
|
||||
const user = await requireAuth(ctx);
|
||||
await getCanvasOrThrow(ctx, canvasId, user.userId);
|
||||
|
||||
return await ctx.db
|
||||
.query("nodes")
|
||||
.withIndex("by_canvas", (q) => q.eq("canvasId", canvasId))
|
||||
.collect();
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Einzelnen Node laden.
|
||||
*/
|
||||
export const get = query({
|
||||
args: { nodeId: v.id("nodes") },
|
||||
handler: async (ctx, { nodeId }) => {
|
||||
const user = await requireAuth(ctx);
|
||||
const node = await ctx.db.get(nodeId);
|
||||
if (!node) return null;
|
||||
|
||||
await getCanvasOrThrow(ctx, node.canvasId, user.userId);
|
||||
return node;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Nodes nach Typ filtern (z.B. alle ai-image Nodes eines Canvas).
|
||||
*/
|
||||
export const listByType = query({
|
||||
args: {
|
||||
canvasId: v.id("canvases"),
|
||||
type: v.string(),
|
||||
},
|
||||
handler: async (ctx, { canvasId, type }) => {
|
||||
const user = await requireAuth(ctx);
|
||||
await getCanvasOrThrow(ctx, canvasId, user.userId);
|
||||
|
||||
return await ctx.db
|
||||
.query("nodes")
|
||||
.withIndex("by_canvas_type", (q) =>
|
||||
q.eq("canvasId", canvasId).eq("type", type as Doc<"nodes">["type"])
|
||||
)
|
||||
.collect();
|
||||
},
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Mutations
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Neuen Node auf dem Canvas erstellen.
|
||||
*/
|
||||
export const create = mutation({
|
||||
args: {
|
||||
canvasId: v.id("canvases"),
|
||||
type: v.string(),
|
||||
positionX: v.number(),
|
||||
positionY: v.number(),
|
||||
width: v.number(),
|
||||
height: v.number(),
|
||||
data: v.any(),
|
||||
parentId: v.optional(v.id("nodes")),
|
||||
zIndex: v.optional(v.number()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const user = await requireAuth(ctx);
|
||||
await getCanvasOrThrow(ctx, args.canvasId, user.userId);
|
||||
|
||||
const nodeId = await ctx.db.insert("nodes", {
|
||||
canvasId: args.canvasId,
|
||||
type: args.type as Doc<"nodes">["type"],
|
||||
positionX: args.positionX,
|
||||
positionY: args.positionY,
|
||||
width: args.width,
|
||||
height: args.height,
|
||||
status: "idle",
|
||||
data: args.data,
|
||||
parentId: args.parentId,
|
||||
zIndex: args.zIndex,
|
||||
});
|
||||
|
||||
// Canvas updatedAt aktualisieren
|
||||
await ctx.db.patch(args.canvasId, { updatedAt: Date.now() });
|
||||
|
||||
return nodeId;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Node-Position auf dem Canvas verschieben.
|
||||
*/
|
||||
export const move = mutation({
|
||||
args: {
|
||||
nodeId: v.id("nodes"),
|
||||
positionX: v.number(),
|
||||
positionY: v.number(),
|
||||
},
|
||||
handler: async (ctx, { nodeId, positionX, positionY }) => {
|
||||
const user = await requireAuth(ctx);
|
||||
const node = await ctx.db.get(nodeId);
|
||||
if (!node) throw new Error("Node not found");
|
||||
|
||||
await getCanvasOrThrow(ctx, node.canvasId, user.userId);
|
||||
await ctx.db.patch(nodeId, { positionX, positionY });
|
||||
await ctx.db.patch(node.canvasId, { updatedAt: Date.now() });
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Node-Größe ändern.
|
||||
*/
|
||||
export const resize = mutation({
|
||||
args: {
|
||||
nodeId: v.id("nodes"),
|
||||
width: v.number(),
|
||||
height: v.number(),
|
||||
},
|
||||
handler: async (ctx, { nodeId, width, height }) => {
|
||||
const user = await requireAuth(ctx);
|
||||
const node = await ctx.db.get(nodeId);
|
||||
if (!node) throw new Error("Node not found");
|
||||
|
||||
await getCanvasOrThrow(ctx, node.canvasId, user.userId);
|
||||
await ctx.db.patch(nodeId, { width, height });
|
||||
await ctx.db.patch(node.canvasId, { updatedAt: Date.now() });
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Mehrere Nodes gleichzeitig verschieben (Batch Move, z.B. nach Multiselect-Drag).
|
||||
*/
|
||||
export const batchMove = mutation({
|
||||
args: {
|
||||
moves: v.array(
|
||||
v.object({
|
||||
nodeId: v.id("nodes"),
|
||||
positionX: v.number(),
|
||||
positionY: v.number(),
|
||||
})
|
||||
),
|
||||
},
|
||||
handler: async (ctx, { moves }) => {
|
||||
const user = await requireAuth(ctx);
|
||||
if (moves.length === 0) return;
|
||||
|
||||
// Canvas-Zugriff über den ersten Node prüfen
|
||||
const firstNode = await ctx.db.get(moves[0].nodeId);
|
||||
if (!firstNode) throw new Error("Node not found");
|
||||
await getCanvasOrThrow(ctx, firstNode.canvasId, user.userId);
|
||||
|
||||
for (const { nodeId, positionX, positionY } of moves) {
|
||||
await ctx.db.patch(nodeId, { positionX, positionY });
|
||||
}
|
||||
|
||||
await ctx.db.patch(firstNode.canvasId, { updatedAt: Date.now() });
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Node-Daten aktualisieren (typ-spezifische Payload).
|
||||
*/
|
||||
export const updateData = mutation({
|
||||
args: {
|
||||
nodeId: v.id("nodes"),
|
||||
data: v.any(),
|
||||
},
|
||||
handler: async (ctx, { nodeId, data }) => {
|
||||
const user = await requireAuth(ctx);
|
||||
const node = await ctx.db.get(nodeId);
|
||||
if (!node) throw new Error("Node not found");
|
||||
|
||||
await getCanvasOrThrow(ctx, node.canvasId, user.userId);
|
||||
await ctx.db.patch(nodeId, { data });
|
||||
await ctx.db.patch(node.canvasId, { updatedAt: Date.now() });
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Node-Status aktualisieren (UX-Strategie: Status direkt am Node).
|
||||
*/
|
||||
export const updateStatus = mutation({
|
||||
args: {
|
||||
nodeId: v.id("nodes"),
|
||||
status: v.union(
|
||||
v.literal("idle"),
|
||||
v.literal("analyzing"),
|
||||
v.literal("clarifying"),
|
||||
v.literal("executing"),
|
||||
v.literal("done"),
|
||||
v.literal("error")
|
||||
),
|
||||
statusMessage: v.optional(v.string()),
|
||||
},
|
||||
handler: async (ctx, { nodeId, status, statusMessage }) => {
|
||||
const user = await requireAuth(ctx);
|
||||
const node = await ctx.db.get(nodeId);
|
||||
if (!node) throw new Error("Node not found");
|
||||
|
||||
await getCanvasOrThrow(ctx, node.canvasId, user.userId);
|
||||
await ctx.db.patch(nodeId, { status, statusMessage });
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Node-Z-Index ändern (Layering).
|
||||
*/
|
||||
export const updateZIndex = mutation({
|
||||
args: {
|
||||
nodeId: v.id("nodes"),
|
||||
zIndex: v.number(),
|
||||
},
|
||||
handler: async (ctx, { nodeId, zIndex }) => {
|
||||
const user = await requireAuth(ctx);
|
||||
const node = await ctx.db.get(nodeId);
|
||||
if (!node) throw new Error("Node not found");
|
||||
|
||||
await getCanvasOrThrow(ctx, node.canvasId, user.userId);
|
||||
await ctx.db.patch(nodeId, { zIndex });
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Node in eine Gruppe/Frame verschieben oder aus Gruppe entfernen.
|
||||
*/
|
||||
export const setParent = mutation({
|
||||
args: {
|
||||
nodeId: v.id("nodes"),
|
||||
parentId: v.optional(v.id("nodes")),
|
||||
},
|
||||
handler: async (ctx, { nodeId, parentId }) => {
|
||||
const user = await requireAuth(ctx);
|
||||
const node = await ctx.db.get(nodeId);
|
||||
if (!node) throw new Error("Node not found");
|
||||
|
||||
await getCanvasOrThrow(ctx, node.canvasId, user.userId);
|
||||
|
||||
// Prüfen ob Parent existiert und zum gleichen Canvas gehört
|
||||
if (parentId) {
|
||||
const parent = await ctx.db.get(parentId);
|
||||
if (!parent || parent.canvasId !== node.canvasId) {
|
||||
throw new Error("Parent not found");
|
||||
}
|
||||
}
|
||||
|
||||
await ctx.db.patch(nodeId, { parentId });
|
||||
await ctx.db.patch(node.canvasId, { updatedAt: Date.now() });
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Node löschen — entfernt auch alle verbundenen Edges.
|
||||
*/
|
||||
export const remove = mutation({
|
||||
args: { nodeId: v.id("nodes") },
|
||||
handler: async (ctx, { nodeId }) => {
|
||||
const user = await requireAuth(ctx);
|
||||
const node = await ctx.db.get(nodeId);
|
||||
if (!node) throw new Error("Node not found");
|
||||
|
||||
await getCanvasOrThrow(ctx, node.canvasId, user.userId);
|
||||
|
||||
// Alle Edges entfernen, die diesen Node als Source oder Target haben
|
||||
const sourceEdges = await ctx.db
|
||||
.query("edges")
|
||||
.withIndex("by_source", (q) => q.eq("sourceNodeId", nodeId))
|
||||
.collect();
|
||||
for (const edge of sourceEdges) {
|
||||
await ctx.db.delete(edge._id);
|
||||
}
|
||||
|
||||
const targetEdges = await ctx.db
|
||||
.query("edges")
|
||||
.withIndex("by_target", (q) => q.eq("targetNodeId", nodeId))
|
||||
.collect();
|
||||
for (const edge of targetEdges) {
|
||||
await ctx.db.delete(edge._id);
|
||||
}
|
||||
|
||||
// Kind-Nodes aus Gruppe/Frame lösen (parentId auf undefined setzen)
|
||||
const children = await ctx.db
|
||||
.query("nodes")
|
||||
.withIndex("by_parent", (q) => q.eq("parentId", nodeId))
|
||||
.collect();
|
||||
for (const child of children) {
|
||||
await ctx.db.patch(child._id, { parentId: undefined });
|
||||
}
|
||||
|
||||
// Node löschen
|
||||
await ctx.db.delete(nodeId);
|
||||
await ctx.db.patch(node.canvasId, { updatedAt: Date.now() });
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user