import { v } from "convex/values"; import { query, mutation } from "./_generated/server"; /** * Workflow CRUD Operations */ // Get all workflows (filtered by user if authenticated) export const list = query({ args: {}, handler: async (ctx) => { const identity = await ctx.auth.getUserIdentity(); // If authenticated, only show user's workflows (exclude templates and undefined userId) if (identity) { const workflows = await ctx.db .query("workflows") .filter((q) => q.and( q.eq(q.field("userId"), identity.subject), q.neq(q.field("isTemplate"), true) ) ) .order("desc") .collect(); return workflows; } // If not authenticated, return empty array return []; }, }); // Alias for backwards compatibility export const listWorkflows = list; // Get workflow by Convex ID export const getWorkflow = query({ args: { id: v.id("workflows") }, handler: async ({ db }, { id }) => { const workflow = await db.get(id); return workflow; }, }); // Get workflow by custom ID (like "workflow_123" or "amazon-product-research") export const getWorkflowByCustomId = query({ args: { customId: v.string() }, handler: async ({ db }, { customId }) => { const workflow = await db .query("workflows") .withIndex("by_customId", (q) => q.eq("customId", customId)) .first(); return workflow; }, }); // Create or update workflow export const saveWorkflow = mutation({ args: { customId: v.optional(v.string()), // Optional - the workflow's custom ID name: v.string(), description: v.optional(v.string()), category: v.optional(v.string()), tags: v.optional(v.array(v.string())), difficulty: v.optional(v.string()), estimatedTime: v.optional(v.string()), nodes: v.array(v.any()), edges: v.array(v.any()), version: v.optional(v.string()), isTemplate: v.optional(v.boolean()), }, handler: async (ctx, args) => { const identity = await ctx.auth.getUserIdentity(); // Check if workflow with this customId already exists (if customId provided) if (args.customId) { const existing = await ctx.db .query("workflows") .withIndex("by_customId", (q) => q.eq("customId", args.customId)) .first(); if (existing) { // Check ownership if user is authenticated if (identity && existing.userId && existing.userId !== identity.subject) { throw new Error("Unauthorized: workflow belongs to another user"); } // Update existing workflow await ctx.db.patch(existing._id, { ...args, updatedAt: new Date().toISOString(), }); return existing._id; } } // Create new workflow with user ownership const newId = await ctx.db.insert("workflows", { ...args, userId: identity?.subject, // Add user ownership if authenticated createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }); return newId; }, }); // Delete workflow export const deleteWorkflow = mutation({ args: { id: v.id("workflows") }, handler: async ({ db }, { id }) => { await db.delete(id); return { success: true }; }, }); // Get workflows by category export const getWorkflowsByCategory = query({ args: { category: v.string() }, handler: async ({ db }, { category }) => { const workflows = await db .query("workflows") .withIndex("by_category", (q) => q.eq("category", category)) .collect(); return workflows; }, }); // Get template workflows export const getTemplates = query({ args: {}, handler: async ({ db }) => { const templates = await db .query("workflows") .withIndex("by_template", (q) => q.eq("isTemplate", true)) .collect(); return templates; }, }); // Seed a single official template export const seedOfficialTemplate = mutation({ args: { customId: v.string(), name: v.string(), description: v.optional(v.string()), category: v.optional(v.string()), tags: v.optional(v.array(v.string())), difficulty: v.optional(v.string()), estimatedTime: v.optional(v.string()), nodes: v.array(v.any()), edges: v.array(v.any()), }, handler: async (ctx, template) => { // Check if this template already exists const existing = await ctx.db .query("workflows") .withIndex("by_customId", (q) => q.eq("customId", template.customId)) .first(); if (existing) { // Template already exists, skip return { success: false, message: "Template already exists" }; } // Create the template in Convex const newId = await ctx.db.insert("workflows", { customId: template.customId, name: template.name, description: template.description, category: template.category || "Templates", tags: template.tags || [], difficulty: template.difficulty, estimatedTime: template.estimatedTime, nodes: template.nodes, edges: template.edges, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), version: "1.0.0", isTemplate: true, isPublic: true, // Make templates public by default }); return { success: true, id: newId.toString(), message: `Seeded template: ${template.name}`, }; }, }); // Get all official templates export const getAllOfficialTemplates = query({ args: {}, handler: async ({ db }) => { const templates = await db .query("workflows") .filter((q) => q.and( q.eq(q.field("isTemplate"), true), q.eq(q.field("isPublic"), true) ) ) .collect(); return templates.map(t => ({ ...t, id: t.customId || t._id, // Use customId as the primary identifier })); }, }); // Get template by custom ID (e.g., 'multi-company-stock-analysis') export const getTemplateByCustomId = query({ args: { customId: v.string() }, handler: async ({ db }, { customId }) => { const template = await db .query("workflows") .withIndex("by_customId", (q) => q.eq("customId", customId)) .filter((q) => q.eq(q.field("isTemplate"), true)) .first(); if (template) { return { ...template, id: template.customId || template._id, }; } return null; }, }); // Update template structure (nodes and edges) export const updateTemplateStructure = mutation({ args: { customId: v.string(), nodes: v.array(v.any()), edges: v.array(v.any()), }, handler: async (ctx, { customId, nodes, edges }) => { // Find the template const template = await ctx.db .query("workflows") .withIndex("by_customId", (q) => q.eq("customId", customId)) .filter((q) => q.eq(q.field("isTemplate"), true)) .first(); if (!template) { throw new Error(`Template ${customId} not found`); } // Update the template await ctx.db.patch(template._id, { nodes, edges, updatedAt: new Date().toISOString(), }); return { success: true, message: `Updated template ${customId}` }; }, }); // Reset template to original from static file export const resetTemplateToDefault = mutation({ args: { customId: v.string() }, handler: async (ctx, { customId }) => { // Get the original template from static file const originalTemplate = await import("@/lib/workflow/templates/examples").then((mod) => mod.exampleTemplates[customId] ); if (!originalTemplate) { throw new Error(`Original template ${customId} not found`); } // Find the template in database const template = await ctx.db .query("workflows") .withIndex("by_customId", (q) => q.eq("customId", customId)) .filter((q) => q.eq(q.field("isTemplate"), true)) .first(); if (!template) { throw new Error(`Template ${customId} not found in database`); } // Reset to original await ctx.db.patch(template._id, { nodes: originalTemplate.nodes, edges: originalTemplate.edges, updatedAt: new Date().toISOString(), }); return { success: true, message: `Reset template ${customId} to default` }; }, }); // Clean up workflows without userId (admin/development only) export const deleteWorkflowsWithoutUserId = mutation({ args: {}, handler: async (ctx) => { const identity = await ctx.auth.getUserIdentity(); // Only allow if authenticated (add admin check in production) if (!identity) { throw new Error("Unauthorized: must be authenticated"); } // Find all workflows without userId and not templates const workflows = await ctx.db .query("workflows") .filter((q) => q.and( q.eq(q.field("userId"), undefined), q.neq(q.field("isTemplate"), true) ) ) .collect(); // Delete them const deletePromises = workflows.map((w) => ctx.db.delete(w._id)); await Promise.all(deletePromises); return { success: true, count: workflows.length, message: `Deleted ${workflows.length} workflows without userId`, }; }, });