diff --git a/apps/sim/app/api/folders/[id]/route.ts b/apps/sim/app/api/folders/[id]/route.ts new file mode 100644 index 00000000000..341c5b9d6ee --- /dev/null +++ b/apps/sim/app/api/folders/[id]/route.ts @@ -0,0 +1,182 @@ +import { and, eq } from 'drizzle-orm' +import { type NextRequest, NextResponse } from 'next/server' +import { getSession } from '@/lib/auth' +import { createLogger } from '@/lib/logs/console-logger' +import { db } from '@/db' +import { workflow, workflowFolder } from '@/db/schema' + +const logger = createLogger('FoldersIDAPI') + +// PUT - Update a folder +export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { + try { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const { id } = await params + const body = await request.json() + const { name, color, isExpanded, parentId } = body + + // Verify the folder exists and belongs to the user + const existingFolder = await db + .select() + .from(workflowFolder) + .where(and(eq(workflowFolder.id, id), eq(workflowFolder.userId, session.user.id))) + .then((rows) => rows[0]) + + if (!existingFolder) { + return NextResponse.json({ error: 'Folder not found' }, { status: 404 }) + } + + // Prevent setting a folder as its own parent or creating circular references + if (parentId && parentId === id) { + return NextResponse.json({ error: 'Folder cannot be its own parent' }, { status: 400 }) + } + + // Check for circular references if parentId is provided + if (parentId) { + const wouldCreateCycle = await checkForCircularReference(id, parentId) + if (wouldCreateCycle) { + return NextResponse.json( + { error: 'Cannot create circular folder reference' }, + { status: 400 } + ) + } + } + + // Update the folder + const updates: any = { updatedAt: new Date() } + if (name !== undefined) updates.name = name.trim() + if (color !== undefined) updates.color = color + if (isExpanded !== undefined) updates.isExpanded = isExpanded + if (parentId !== undefined) updates.parentId = parentId || null + + const [updatedFolder] = await db + .update(workflowFolder) + .set(updates) + .where(eq(workflowFolder.id, id)) + .returning() + + logger.info('Updated folder:', { id, updates }) + + return NextResponse.json({ folder: updatedFolder }) + } catch (error) { + logger.error('Error updating folder:', { error }) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} + +// DELETE - Delete a folder +export async function DELETE( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const { id } = await params + const { searchParams } = new URL(request.url) + const moveWorkflowsTo = searchParams.get('moveWorkflowsTo') // Optional: move workflows to another folder + + // Verify the folder exists and belongs to the user + const existingFolder = await db + .select() + .from(workflowFolder) + .where(and(eq(workflowFolder.id, id), eq(workflowFolder.userId, session.user.id))) + .then((rows) => rows[0]) + + if (!existingFolder) { + return NextResponse.json({ error: 'Folder not found' }, { status: 404 }) + } + + // Check if folder has child folders + const childFolders = await db + .select({ id: workflowFolder.id }) + .from(workflowFolder) + .where(eq(workflowFolder.parentId, id)) + + // Check if folder has workflows + const workflowsInFolder = await db + .select({ id: workflow.id }) + .from(workflow) + .where(eq(workflow.folderId, id)) + + // Handle child folders - move them to parent or root + if (childFolders.length > 0) { + await db + .update(workflowFolder) + .set({ + parentId: existingFolder.parentId, // Move to the parent of the deleted folder + updatedAt: new Date(), + }) + .where(eq(workflowFolder.parentId, id)) + } + + // Handle workflows in the folder + if (workflowsInFolder.length > 0) { + const newFolderId = moveWorkflowsTo || null // Move to specified folder or root + await db + .update(workflow) + .set({ + folderId: newFolderId, + updatedAt: new Date(), + }) + .where(eq(workflow.folderId, id)) + } + + // Delete the folder + await db.delete(workflowFolder).where(eq(workflowFolder.id, id)) + + logger.info('Deleted folder:', { + id, + childFoldersCount: childFolders.length, + workflowsCount: workflowsInFolder.length, + movedWorkflowsTo: moveWorkflowsTo, + }) + + return NextResponse.json({ + success: true, + movedItems: { + childFolders: childFolders.length, + workflows: workflowsInFolder.length, + }, + }) + } catch (error) { + logger.error('Error deleting folder:', { error }) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} + +// Helper function to check for circular references +async function checkForCircularReference(folderId: string, parentId: string): Promise { + let currentParentId: string | null = parentId + const visited = new Set() + + while (currentParentId) { + if (visited.has(currentParentId)) { + return true // Circular reference detected + } + + if (currentParentId === folderId) { + return true // Would create a cycle + } + + visited.add(currentParentId) + + // Get the parent of the current parent + const parent: { parentId: string | null } | undefined = await db + .select({ parentId: workflowFolder.parentId }) + .from(workflowFolder) + .where(eq(workflowFolder.id, currentParentId)) + .then((rows) => rows[0]) + + currentParentId = parent?.parentId || null + } + + return false +} diff --git a/apps/sim/app/api/folders/route.ts b/apps/sim/app/api/folders/route.ts new file mode 100644 index 00000000000..5bcb94ae461 --- /dev/null +++ b/apps/sim/app/api/folders/route.ts @@ -0,0 +1,101 @@ +import { and, asc, desc, eq, isNull } from 'drizzle-orm' +import { type NextRequest, NextResponse } from 'next/server' +import { getSession } from '@/lib/auth' +import { createLogger } from '@/lib/logs/console-logger' +import { db } from '@/db' +import { workflowFolder } from '@/db/schema' + +const logger = createLogger('FoldersAPI') + +// GET - Fetch folders for a workspace +export async function GET(request: NextRequest) { + try { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const { searchParams } = new URL(request.url) + const workspaceId = searchParams.get('workspaceId') + + if (!workspaceId) { + return NextResponse.json({ error: 'Workspace ID is required' }, { status: 400 }) + } + + // Fetch all folders for the workspace, ordered by sortOrder and createdAt + const folders = await db + .select() + .from(workflowFolder) + .where( + and(eq(workflowFolder.workspaceId, workspaceId), eq(workflowFolder.userId, session.user.id)) + ) + .orderBy(asc(workflowFolder.sortOrder), asc(workflowFolder.createdAt)) + + return NextResponse.json({ folders }) + } catch (error) { + logger.error('Error fetching folders:', { error }) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} + +// POST - Create a new folder +export async function POST(request: NextRequest) { + try { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const { name, workspaceId, parentId, color } = body + + if (!name || !workspaceId) { + return NextResponse.json({ error: 'Name and workspace ID are required' }, { status: 400 }) + } + + // Generate a new ID + const id = crypto.randomUUID() + + // Use transaction to ensure sortOrder consistency + const newFolder = await db.transaction(async (tx) => { + // Get the next sort order for the parent (or root level) + const existingFolders = await tx + .select({ sortOrder: workflowFolder.sortOrder }) + .from(workflowFolder) + .where( + and( + eq(workflowFolder.workspaceId, workspaceId), + eq(workflowFolder.userId, session.user.id), + parentId ? eq(workflowFolder.parentId, parentId) : isNull(workflowFolder.parentId) + ) + ) + .orderBy(desc(workflowFolder.sortOrder)) + .limit(1) + + const nextSortOrder = existingFolders.length > 0 ? existingFolders[0].sortOrder + 1 : 0 + + // Insert the new folder within the same transaction + const [folder] = await tx + .insert(workflowFolder) + .values({ + id, + name: name.trim(), + userId: session.user.id, + workspaceId, + parentId: parentId || null, + color: color || '#6B7280', + sortOrder: nextSortOrder, + }) + .returning() + + return folder + }) + + logger.info('Created new folder:', { id, name, workspaceId, parentId }) + + return NextResponse.json({ folder: newFolder }) + } catch (error) { + logger.error('Error creating folder:', { error }) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/logs/route.test.ts b/apps/sim/app/api/logs/route.test.ts index 662e0971d25..1cf185d0e50 100644 --- a/apps/sim/app/api/logs/route.test.ts +++ b/apps/sim/app/api/logs/route.test.ts @@ -38,12 +38,23 @@ describe('Workflow Logs API Route', () => { trigger: 'api', createdAt: new Date('2024-01-01T10:02:00.000Z'), }, + { + id: 'log-4', + workflowId: 'workflow-3', + executionId: 'exec-3', + level: 'info', + message: 'Root workflow executed', + duration: '0.8s', + trigger: 'webhook', + createdAt: new Date('2024-01-01T10:03:00.000Z'), + }, ] const mockWorkflows = [ { id: 'workflow-1', userId: 'user-123', + folderId: 'folder-1', name: 'Test Workflow 1', color: '#3972F6', description: 'First test workflow', @@ -54,6 +65,7 @@ describe('Workflow Logs API Route', () => { { id: 'workflow-2', userId: 'user-123', + folderId: 'folder-2', name: 'Test Workflow 2', color: '#FF6B6B', description: 'Second test workflow', @@ -61,6 +73,17 @@ describe('Workflow Logs API Route', () => { createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-01T00:00:00.000Z'), }, + { + id: 'workflow-3', + userId: 'user-123', + folderId: null, + name: 'Test Workflow 3', + color: '#22C55E', + description: 'Third test workflow (no folder)', + state: {}, + createdAt: new Date('2024-01-01T00:00:00.000Z'), + updatedAt: new Date('2024-01-01T00:00:00.000Z'), + }, ] beforeEach(() => { @@ -123,7 +146,9 @@ describe('Workflow Logs API Route', () => { // First call: get user workflows if (dbCallCount === 1) { - return createChainableMock(userWorkflows.map((w) => ({ id: w.id }))) + return createChainableMock( + userWorkflows.map((w) => ({ id: w.id, folderId: w.folderId })) + ) } // Second call: get logs @@ -195,12 +220,12 @@ describe('Workflow Logs API Route', () => { expect(response.status).toBe(200) expect(data).toHaveProperty('data') - expect(data).toHaveProperty('total', 3) + expect(data).toHaveProperty('total', 4) expect(data).toHaveProperty('page', 1) expect(data).toHaveProperty('pageSize', 100) expect(data).toHaveProperty('totalPages', 1) expect(Array.isArray(data.data)).toBe(true) - expect(data.data).toHaveLength(3) + expect(data.data).toHaveLength(4) }) it('should include workflow data when includeWorkflow=true', async () => { @@ -252,7 +277,11 @@ describe('Workflow Logs API Route', () => { }) it('should filter logs by multiple workflow IDs', async () => { - setupDatabaseMock() + // Only get logs for workflow-1 and workflow-2 (not workflow-3) + const filteredLogs = mockWorkflowLogs.filter( + (log) => log.workflowId === 'workflow-1' || log.workflowId === 'workflow-2' + ) + setupDatabaseMock({ logs: filteredLogs }) const url = new URL('http://localhost:3000/api/logs?workflowIds=workflow-1,workflow-2') const req = new Request(url.toString()) @@ -280,7 +309,7 @@ describe('Workflow Logs API Route', () => { const data = await response.json() expect(response.status).toBe(200) - expect(data.data).toHaveLength(2) + expect(data.data).toHaveLength(filteredLogs.length) }) it('should search logs by message content', async () => { @@ -527,5 +556,167 @@ describe('Workflow Logs API Route', () => { expect(data.data[0].level).toBe('info') expect(data.data[0].workflowId).toBe('workflow-1') }) + + it('should filter logs by single folder ID', async () => { + const folder1Logs = mockWorkflowLogs.filter((log) => log.workflowId === 'workflow-1') + setupDatabaseMock({ logs: folder1Logs }) + + const url = new URL('http://localhost:3000/api/logs?folderIds=folder-1') + const req = new Request(url.toString()) + + const { GET } = await import('./route') + const response = await GET(req as any) + const data = await response.json() + + expect(response.status).toBe(200) + expect(data.data).toHaveLength(2) + expect(data.data.every((log: any) => log.workflowId === 'workflow-1')).toBe(true) + }) + + it('should filter logs by multiple folder IDs', async () => { + const folder1And2Logs = mockWorkflowLogs.filter( + (log) => log.workflowId === 'workflow-1' || log.workflowId === 'workflow-2' + ) + setupDatabaseMock({ logs: folder1And2Logs }) + + const url = new URL('http://localhost:3000/api/logs?folderIds=folder-1,folder-2') + const req = new Request(url.toString()) + + const { GET } = await import('./route') + const response = await GET(req as any) + const data = await response.json() + + expect(response.status).toBe(200) + expect(data.data).toHaveLength(3) + expect( + data.data.every((log: any) => ['workflow-1', 'workflow-2'].includes(log.workflowId)) + ).toBe(true) + }) + + it('should filter logs by root folder (workflows without folders)', async () => { + const rootLogs = mockWorkflowLogs.filter((log) => log.workflowId === 'workflow-3') + setupDatabaseMock({ logs: rootLogs }) + + const url = new URL('http://localhost:3000/api/logs?folderIds=root') + const req = new Request(url.toString()) + + const { GET } = await import('./route') + const response = await GET(req as any) + const data = await response.json() + + expect(response.status).toBe(200) + expect(data.data).toHaveLength(1) + expect(data.data[0].workflowId).toBe('workflow-3') + expect(data.data[0].message).toContain('Root workflow executed') + }) + + it('should combine root folder with other folders', async () => { + const rootAndFolder1Logs = mockWorkflowLogs.filter( + (log) => log.workflowId === 'workflow-1' || log.workflowId === 'workflow-3' + ) + setupDatabaseMock({ logs: rootAndFolder1Logs }) + + const url = new URL('http://localhost:3000/api/logs?folderIds=root,folder-1') + const req = new Request(url.toString()) + + const { GET } = await import('./route') + const response = await GET(req as any) + const data = await response.json() + + expect(response.status).toBe(200) + expect(data.data).toHaveLength(3) + expect( + data.data.every((log: any) => ['workflow-1', 'workflow-3'].includes(log.workflowId)) + ).toBe(true) + }) + + it('should combine folder filter with workflow filter', async () => { + // Filter by folder-1 and specific workflow-1 (should return same results) + const filteredLogs = mockWorkflowLogs.filter((log) => log.workflowId === 'workflow-1') + setupDatabaseMock({ logs: filteredLogs }) + + const url = new URL( + 'http://localhost:3000/api/logs?folderIds=folder-1&workflowIds=workflow-1' + ) + const req = new Request(url.toString()) + + const { GET } = await import('./route') + const response = await GET(req as any) + const data = await response.json() + + expect(response.status).toBe(200) + expect(data.data).toHaveLength(2) + expect(data.data.every((log: any) => log.workflowId === 'workflow-1')).toBe(true) + }) + + it('should return empty when folder and workflow filters conflict', async () => { + // Try to filter by folder-1 but workflow-2 (which is in folder-2) + setupDatabaseMock({ logs: [] }) + + const url = new URL( + 'http://localhost:3000/api/logs?folderIds=folder-1&workflowIds=workflow-2' + ) + const req = new Request(url.toString()) + + const { GET } = await import('./route') + const response = await GET(req as any) + const data = await response.json() + + expect(response.status).toBe(200) + expect(data.data).toEqual([]) + expect(data.total).toBe(0) + }) + + it('should combine folder filter with other filters', async () => { + const filteredLogs = mockWorkflowLogs.filter( + (log) => log.workflowId === 'workflow-1' && log.level === 'info' + ) + setupDatabaseMock({ logs: filteredLogs }) + + const url = new URL('http://localhost:3000/api/logs?folderIds=folder-1&level=info') + const req = new Request(url.toString()) + + const { GET } = await import('./route') + const response = await GET(req as any) + const data = await response.json() + + expect(response.status).toBe(200) + expect(data.data).toHaveLength(1) + expect(data.data[0].workflowId).toBe('workflow-1') + expect(data.data[0].level).toBe('info') + }) + + it('should return empty result when no workflows match folder filter', async () => { + setupDatabaseMock({ logs: [] }) + + const url = new URL('http://localhost:3000/api/logs?folderIds=non-existent-folder') + const req = new Request(url.toString()) + + const { GET } = await import('./route') + const response = await GET(req as any) + const data = await response.json() + + expect(response.status).toBe(200) + expect(data.data).toEqual([]) + expect(data.total).toBe(0) + }) + + it('should handle folder filter with includeWorkflow=true', async () => { + const folder1Logs = mockWorkflowLogs.filter((log) => log.workflowId === 'workflow-1') + setupDatabaseMock({ logs: folder1Logs }) + + const url = new URL('http://localhost:3000/api/logs?folderIds=folder-1&includeWorkflow=true') + const req = new Request(url.toString()) + + const { GET } = await import('./route') + const response = await GET(req as any) + const data = await response.json() + + expect(response.status).toBe(200) + expect(data.data).toHaveLength(2) + expect(data.data[0]).toHaveProperty('workflow') + expect(data.data[0].workflow).toHaveProperty('name') + expect(data.data.every((log: any) => log.workflowId === 'workflow-1')).toBe(true) + }) }) }) diff --git a/apps/sim/app/api/logs/route.ts b/apps/sim/app/api/logs/route.ts index 2f1626a680b..24e5c53734e 100644 --- a/apps/sim/app/api/logs/route.ts +++ b/apps/sim/app/api/logs/route.ts @@ -17,6 +17,7 @@ const QueryParamsSchema = z.object({ offset: z.coerce.number().optional().default(0), level: z.string().optional(), workflowIds: z.string().optional(), // Comma-separated list of workflow IDs + folderIds: z.string().optional(), // Comma-separated list of folder IDs triggers: z.string().optional(), // Comma-separated list of trigger types startDate: z.string().optional(), endDate: z.string().optional(), @@ -41,7 +42,7 @@ export async function GET(request: NextRequest) { const params = QueryParamsSchema.parse(Object.fromEntries(searchParams.entries())) const userWorkflows = await db - .select({ id: workflow.id }) + .select({ id: workflow.id, folderId: workflow.folderId }) .from(workflow) .where(eq(workflow.userId, userId)) @@ -51,6 +52,36 @@ export async function GET(request: NextRequest) { return NextResponse.json({ data: [], total: 0 }, { status: 200 }) } + // Handle folder filtering + let targetWorkflowIds = userWorkflowIds + if (params.folderIds) { + const requestedFolderIds = params.folderIds.split(',').map((id) => id.trim()) + + // Filter workflows by folder IDs (including 'root' for workflows without folders) + const workflowsInFolders = userWorkflows.filter((w) => { + if (requestedFolderIds.includes('root')) { + return requestedFolderIds.includes('root') && w.folderId === null + } + return w.folderId && requestedFolderIds.includes(w.folderId) + }) + + // Handle 'root' folder (workflows without folders) + if (requestedFolderIds.includes('root')) { + const rootWorkflows = userWorkflows.filter((w) => w.folderId === null) + const folderWorkflows = userWorkflows.filter( + (w) => + w.folderId && requestedFolderIds.filter((id) => id !== 'root').includes(w.folderId!) + ) + targetWorkflowIds = [...rootWorkflows, ...folderWorkflows].map((w) => w.id) + } else { + targetWorkflowIds = workflowsInFolders.map((w) => w.id) + } + + if (targetWorkflowIds.length === 0) { + return NextResponse.json({ data: [], total: 0 }, { status: 200 }) + } + } + // Build the conditions for the query let conditions: SQL | undefined @@ -65,13 +96,21 @@ export async function GET(request: NextRequest) { }) return NextResponse.json({ error: 'Unauthorized access to workflows' }, { status: 403 }) } - conditions = or(...requestedWorkflowIds.map((id) => eq(workflowLogs.workflowId, id))) + // Further filter by folder constraints if both filters are active + const finalWorkflowIds = params.folderIds + ? requestedWorkflowIds.filter((id) => targetWorkflowIds.includes(id)) + : requestedWorkflowIds + + if (finalWorkflowIds.length === 0) { + return NextResponse.json({ data: [], total: 0 }, { status: 200 }) + } + conditions = or(...finalWorkflowIds.map((id) => eq(workflowLogs.workflowId, id))) } else { - // No specific workflows requested, filter by all user workflows - if (userWorkflowIds.length === 1) { - conditions = eq(workflowLogs.workflowId, userWorkflowIds[0]) + // No specific workflows requested, filter by target workflows (considering folder filter) + if (targetWorkflowIds.length === 1) { + conditions = eq(workflowLogs.workflowId, targetWorkflowIds[0]) } else { - conditions = or(...userWorkflowIds.map((id) => eq(workflowLogs.workflowId, id))) + conditions = or(...targetWorkflowIds.map((id) => eq(workflowLogs.workflowId, id))) } } diff --git a/apps/sim/app/api/workflows/sync/route.ts b/apps/sim/app/api/workflows/sync/route.ts index f17dbb967c4..7b72967ce6e 100644 --- a/apps/sim/app/api/workflows/sync/route.ts +++ b/apps/sim/app/api/workflows/sync/route.ts @@ -41,6 +41,7 @@ const WorkflowSchema = z.object({ state: WorkflowStateSchema, marketplaceData: MarketplaceDataSchema, workspaceId: z.string().optional(), + folderId: z.string().nullable().optional(), }) const SyncPayloadSchema = z.object({ @@ -392,6 +393,7 @@ export async function POST(req: NextRequest) { id: clientWorkflow.id, userId: session.user.id, workspaceId: effectiveWorkspaceId, + folderId: clientWorkflow.folderId || null, name: clientWorkflow.name, description: clientWorkflow.description, color: clientWorkflow.color, @@ -422,6 +424,7 @@ export async function POST(req: NextRequest) { dbWorkflow.description !== clientWorkflow.description || dbWorkflow.color !== clientWorkflow.color || dbWorkflow.workspaceId !== effectiveWorkspaceId || + dbWorkflow.folderId !== (clientWorkflow.folderId || null) || JSON.stringify(dbWorkflow.marketplaceData) !== JSON.stringify(clientWorkflow.marketplaceData) @@ -434,6 +437,7 @@ export async function POST(req: NextRequest) { description: clientWorkflow.description, color: clientWorkflow.color, workspaceId: effectiveWorkspaceId, + folderId: clientWorkflow.folderId || null, state: clientWorkflow.state, marketplaceData: clientWorkflow.marketplaceData || null, lastSynced: now, diff --git a/apps/sim/app/w/components/sidebar/components/create-menu/create-menu.tsx b/apps/sim/app/w/components/sidebar/components/create-menu/create-menu.tsx new file mode 100644 index 00000000000..d13e7723a5b --- /dev/null +++ b/apps/sim/app/w/components/sidebar/components/create-menu/create-menu.tsx @@ -0,0 +1,171 @@ +'use client' + +import { useState } from 'react' +import { File, Folder, Plus } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { useFolderStore } from '@/stores/folders/store' +import { useWorkflowRegistry } from '@/stores/workflows/registry/store' + +interface CreateMenuProps { + onCreateWorkflow: (folderId?: string) => void + isCollapsed?: boolean +} + +export function CreateMenu({ onCreateWorkflow, isCollapsed }: CreateMenuProps) { + const [showFolderDialog, setShowFolderDialog] = useState(false) + const [folderName, setFolderName] = useState('') + const [isCreating, setIsCreating] = useState(false) + + const { activeWorkspaceId } = useWorkflowRegistry() + const { createFolder } = useFolderStore() + + const handleCreateWorkflow = () => { + onCreateWorkflow() + } + + const handleCreateFolder = () => { + setShowFolderDialog(true) + } + + const handleFolderSubmit = async (e: React.FormEvent) => { + e.preventDefault() + if (!folderName.trim() || !activeWorkspaceId) return + + setIsCreating(true) + try { + await createFolder({ + name: folderName.trim(), + workspaceId: activeWorkspaceId, + }) + setFolderName('') + setShowFolderDialog(false) + } catch (error) { + console.error('Failed to create folder:', error) + // You could add toast notification here + } finally { + setIsCreating(false) + } + } + + const handleCancel = () => { + setFolderName('') + setShowFolderDialog(false) + } + + if (isCollapsed) { + return ( + <> + + + + + + + + New Workflow + + + + New Folder + + + + + {/* Folder creation dialog */} + + + + Create New Folder + +
+
+ + setFolderName(e.target.value)} + placeholder='Enter folder name...' + autoFocus + required + /> +
+
+ + +
+
+
+
+ + ) + } + + return ( + <> + + + + + + + + New Workflow + + + + New Folder + + + + + {/* Folder creation dialog */} + + + + Create New Folder + +
+
+ + setFolderName(e.target.value)} + placeholder='Enter folder name...' + autoFocus + required + /> +
+
+ + +
+
+
+
+ + ) +} diff --git a/apps/sim/app/w/components/sidebar/components/folder-context-menu/folder-context-menu.tsx b/apps/sim/app/w/components/sidebar/components/folder-context-menu/folder-context-menu.tsx new file mode 100644 index 00000000000..fcda8a79bec --- /dev/null +++ b/apps/sim/app/w/components/sidebar/components/folder-context-menu/folder-context-menu.tsx @@ -0,0 +1,209 @@ +'use client' + +import { useState } from 'react' +import { File, Folder, MoreHorizontal, Pencil, Trash2 } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { useFolderStore } from '@/stores/folders/store' +import { useWorkflowRegistry } from '@/stores/workflows/registry/store' + +interface FolderContextMenuProps { + folderId: string + folderName: string + onCreateWorkflow: (folderId: string) => void + onRename?: (folderId: string, newName: string) => void + onDelete?: (folderId: string) => void +} + +export function FolderContextMenu({ + folderId, + folderName, + onCreateWorkflow, + onRename, + onDelete, +}: FolderContextMenuProps) { + const [showSubfolderDialog, setShowSubfolderDialog] = useState(false) + const [showRenameDialog, setShowRenameDialog] = useState(false) + const [subfolderName, setSubfolderName] = useState('') + const [renameName, setRenameName] = useState(folderName) + const [isCreating, setIsCreating] = useState(false) + const [isRenaming, setIsRenaming] = useState(false) + + const { activeWorkspaceId } = useWorkflowRegistry() + const { createFolder, updateFolder, deleteFolder } = useFolderStore() + + const handleCreateWorkflow = () => { + onCreateWorkflow(folderId) + } + + const handleCreateSubfolder = () => { + setShowSubfolderDialog(true) + } + + const handleRename = () => { + setRenameName(folderName) + setShowRenameDialog(true) + } + + const handleDelete = () => { + if (onDelete) { + onDelete(folderId) + } else { + // Default delete behavior + deleteFolder(folderId) + } + } + + const handleSubfolderSubmit = async (e: React.FormEvent) => { + e.preventDefault() + if (!subfolderName.trim() || !activeWorkspaceId) return + + setIsCreating(true) + try { + await createFolder({ + name: subfolderName.trim(), + workspaceId: activeWorkspaceId, + parentId: folderId, + }) + setSubfolderName('') + setShowSubfolderDialog(false) + } catch (error) { + console.error('Failed to create subfolder:', error) + } finally { + setIsCreating(false) + } + } + + const handleRenameSubmit = async (e: React.FormEvent) => { + e.preventDefault() + if (!renameName.trim()) return + + setIsRenaming(true) + try { + if (onRename) { + onRename(folderId, renameName.trim()) + } else { + // Default rename behavior + await updateFolder(folderId, { name: renameName.trim() }) + } + setShowRenameDialog(false) + } catch (error) { + console.error('Failed to rename folder:', error) + } finally { + setIsRenaming(false) + } + } + + const handleCancel = () => { + setSubfolderName('') + setShowSubfolderDialog(false) + setRenameName(folderName) + setShowRenameDialog(false) + } + + return ( + <> + + + + + e.stopPropagation()}> + + + New Workflow + + + + New Subfolder + + + + + Rename + + + + Delete + + + + + {/* Subfolder creation dialog */} + + e.stopPropagation()}> + + Create New Subfolder + +
+
+ + setSubfolderName(e.target.value)} + placeholder='Enter folder name...' + autoFocus + required + /> +
+
+ + +
+
+
+
+ + {/* Rename dialog */} + + e.stopPropagation()}> + + Rename Folder + +
+
+ + setRenameName(e.target.value)} + placeholder='Enter folder name...' + autoFocus + required + /> +
+
+ + +
+
+
+
+ + ) +} diff --git a/apps/sim/app/w/components/sidebar/components/folder-tree/folder-tree.tsx b/apps/sim/app/w/components/sidebar/components/folder-tree/folder-tree.tsx new file mode 100644 index 00000000000..92a906c3260 --- /dev/null +++ b/apps/sim/app/w/components/sidebar/components/folder-tree/folder-tree.tsx @@ -0,0 +1,383 @@ +'use client' + +import { useEffect, useState } from 'react' +import clsx from 'clsx' +import { ChevronDown, ChevronRight, Folder, FolderOpen } from 'lucide-react' +import Link from 'next/link' +import { usePathname } from 'next/navigation' +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' +import { type FolderTreeNode, useFolderStore } from '@/stores/folders/store' +import { useWorkflowRegistry } from '@/stores/workflows/registry/store' +import type { WorkflowMetadata } from '@/stores/workflows/registry/types' +import { FolderContextMenu } from '../folder-context-menu/folder-context-menu' + +interface FolderItemProps { + folder: FolderTreeNode + isCollapsed?: boolean + onCreateWorkflow: (folderId?: string) => void +} + +function FolderItem({ folder, isCollapsed, onCreateWorkflow }: FolderItemProps) { + const [dragOver, setDragOver] = useState(false) + const { expandedFolders, toggleExpanded, updateFolderAPI, deleteFolder } = useFolderStore() + const { updateWorkflow } = useWorkflowRegistry() + + const isExpanded = expandedFolders.has(folder.id) + + const handleToggleExpanded = () => { + toggleExpanded(folder.id) + // Persist to server + updateFolderAPI(folder.id, { isExpanded: !isExpanded }).catch(console.error) + } + + const handleRename = async (folderId: string, newName: string) => { + try { + await updateFolderAPI(folderId, { name: newName }) + } catch (error) { + console.error('Failed to rename folder:', error) + } + } + + const handleDelete = async (folderId: string) => { + if ( + confirm( + `Are you sure you want to delete "${folder.name}"? Child folders and workflows will be moved to the parent folder.` + ) + ) { + try { + await deleteFolder(folderId) + } catch (error) { + console.error('Failed to delete folder:', error) + } + } + } + + // Drag and drop handlers + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + setDragOver(true) + } + + const handleDragLeave = (e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + setDragOver(false) + } + + const handleDrop = async (e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + setDragOver(false) + + const workflowId = e.dataTransfer.getData('workflow-id') + if (workflowId && workflowId !== folder.id) { + try { + // Update workflow to be in this folder + await updateWorkflow(workflowId, { folderId: folder.id }) + console.log(`Moved workflow ${workflowId} to folder ${folder.id}`) + } catch (error) { + console.error('Failed to move workflow to folder:', error) + } + } + } + + if (isCollapsed) { + return ( + + +
+
+ {isExpanded ? ( + + ) : ( + + )} +
+
+
+ +

{folder.name}

+
+
+ ) + } + + return ( +
+
+
+ {isExpanded ? : } +
+ +
+ {isExpanded ? ( + + ) : ( + + )} +
+ + + {folder.name} + + +
e.stopPropagation()}> + +
+
+
+ ) +} + +interface WorkflowItemProps { + workflow: WorkflowMetadata + active: boolean + isMarketplace?: boolean + isCollapsed?: boolean + level: number +} + +function WorkflowItem({ workflow, active, isMarketplace, isCollapsed, level }: WorkflowItemProps) { + const [isDragging, setIsDragging] = useState(false) + + const handleDragStart = (e: React.DragEvent) => { + if (isMarketplace) return // Don't allow dragging marketplace workflows + + e.dataTransfer.setData('workflow-id', workflow.id) + e.dataTransfer.effectAllowed = 'move' + setIsDragging(true) + } + + const handleDragEnd = () => { + setIsDragging(false) + } + + if (isCollapsed) { + return ( + + + +
+ + + +

+ {workflow.name} + {isMarketplace && ' (Preview)'} +

+
+ + ) + } + + return ( + +
+ + {workflow.name} + {isMarketplace && ' (Preview)'} + + + ) +} + +interface FolderTreeProps { + regularWorkflows: WorkflowMetadata[] + marketplaceWorkflows: WorkflowMetadata[] + isCollapsed?: boolean + isLoading?: boolean + onCreateWorkflow: (folderId?: string) => void +} + +export function FolderTree({ + regularWorkflows, + marketplaceWorkflows, + isCollapsed = false, + isLoading = false, + onCreateWorkflow, +}: FolderTreeProps) { + const pathname = usePathname() + const { activeWorkspaceId } = useWorkflowRegistry() + const { + getFolderTree, + expandedFolders, + fetchFolders, + isLoading: foldersLoading, + } = useFolderStore() + + // Fetch folders when workspace changes + useEffect(() => { + if (activeWorkspaceId) { + fetchFolders(activeWorkspaceId) + } + }, [activeWorkspaceId, fetchFolders]) + + const folderTree = activeWorkspaceId ? getFolderTree(activeWorkspaceId) : [] + + // Group workflows by folder + const workflowsByFolder = regularWorkflows.reduce( + (acc, workflow) => { + const folderId = workflow.folderId || 'root' + if (!acc[folderId]) acc[folderId] = [] + acc[folderId].push(workflow) + return acc + }, + {} as Record + ) + + const renderFolderTree = (nodes: FolderTreeNode[], level = 0): React.ReactNode[] => { + const result: React.ReactNode[] = [] + + nodes.forEach((folder) => { + // Render folder + result.push( +
+ +
+ ) + + // Render workflows in this folder + const workflowsInFolder = workflowsByFolder[folder.id] || [] + if (expandedFolders.has(folder.id) && workflowsInFolder.length > 0) { + workflowsInFolder.forEach((workflow) => { + result.push( + + ) + }) + } + + // Render child folders + if (expandedFolders.has(folder.id) && folder.children.length > 0) { + result.push(...renderFolderTree(folder.children, level + 1)) + } + }) + + return result + } + + const showLoading = isLoading || foldersLoading + + return ( +
+ {/* Folder tree */} + {renderFolderTree(folderTree)} + + {/* Root level workflows (no folder) */} + {(workflowsByFolder.root || []).map((workflow) => ( + + ))} + + {/* Marketplace workflows */} + {marketplaceWorkflows.length > 0 && ( +
+

+ {isCollapsed ? '' : 'Marketplace'} +

+ {marketplaceWorkflows.map((workflow) => ( + + ))} +
+ )} + + {/* Empty state */} + {!showLoading && + regularWorkflows.length === 0 && + marketplaceWorkflows.length === 0 && + folderTree.length === 0 && + !isCollapsed && ( +
+ No workflows or folders in {activeWorkspaceId ? 'this workspace' : 'your account'}. + Create one to get started. +
+ )} +
+ ) +} diff --git a/apps/sim/app/w/components/sidebar/components/workspace-header/workspace-header.tsx b/apps/sim/app/w/components/sidebar/components/workspace-header/workspace-header.tsx index 838b8d58f36..56a7d059eac 100644 --- a/apps/sim/app/w/components/sidebar/components/workspace-header/workspace-header.tsx +++ b/apps/sim/app/w/components/sidebar/components/workspace-header/workspace-header.tsx @@ -1,7 +1,7 @@ 'use client' import { useEffect, useState } from 'react' -import { ChevronDown, Pencil, Plus, Trash2, X } from 'lucide-react' +import { ChevronDown, Pencil, Trash2, X } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/navigation' import { AgentIcon } from '@/components/icons' @@ -27,7 +27,6 @@ import { } from '@/components/ui/dropdown-menu' import { Input } from '@/components/ui/input' import { Skeleton } from '@/components/ui/skeleton' -import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { useSession } from '@/lib/auth-client' import { cn } from '@/lib/utils' import { useSidebarStore } from '@/stores/sidebar/store' @@ -535,34 +534,6 @@ export function WorkspaceHeader({
- - {/* Plus button positioned absolutely */} - {!isCollapsed && ( -
- - -
- {isClientLoading ? ( - - ) : ( - - )} -
-
- New Workflow -
-
- )} )} diff --git a/apps/sim/app/w/components/sidebar/sidebar.tsx b/apps/sim/app/w/components/sidebar/sidebar.tsx index 7f755b764b3..6d8ce037df8 100644 --- a/apps/sim/app/w/components/sidebar/sidebar.tsx +++ b/apps/sim/app/w/components/sidebar/sidebar.tsx @@ -12,12 +12,13 @@ import { useSidebarStore } from '@/stores/sidebar/store' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import type { WorkflowMetadata } from '@/stores/workflows/registry/types' import { useRegistryLoading } from '../../hooks/use-registry-loading' +import { CreateMenu } from './components/create-menu/create-menu' +import { FolderTree } from './components/folder-tree/folder-tree' import { HelpModal } from './components/help-modal/help-modal' import { InviteModal } from './components/invite-modal/invite-modal' import { NavSection } from './components/nav-section/nav-section' import { SettingsModal } from './components/settings-modal/settings-modal' import { SidebarControl } from './components/sidebar-control/sidebar-control' -import { WorkflowList } from './components/workflow-list/workflow-list' import { WorkspaceHeader } from './components/workspace-header/workspace-header' export function Sidebar() { @@ -126,7 +127,7 @@ export function Sidebar() { }, [workflows, isLoading, activeWorkspaceId]) // Create workflow - const handleCreateWorkflow = async () => { + const handleCreateWorkflow = async (folderId?: string) => { try { // Import the isActivelyLoadingFromDB function to check sync status const { isActivelyLoadingFromDB } = await import('@/stores/workflows/sync') @@ -137,9 +138,10 @@ export function Sidebar() { return } - // Create the workflow and ensure it's associated with the active workspace + // Create the workflow and ensure it's associated with the active workspace and folder const id = createWorkflow({ workspaceId: activeWorkspaceId || undefined, + folderId: folderId || undefined, // Associate with folder if provided }) router.push(`/w/${id}`) @@ -224,26 +226,25 @@ export function Sidebar() {
{/* Workflows Section */}
-

- {isLoading ? ( - isCollapsed ? ( - '' - ) : ( - - ) - ) : isCollapsed ? ( - '' - ) : ( - 'Workflows' +

+ {isLoading ? : 'Workflows'} +

+ {!isCollapsed && !isLoading && ( + )} -

- +
diff --git a/apps/sim/app/w/logs/components/filters/components/folder.tsx b/apps/sim/app/w/logs/components/filters/components/folder.tsx new file mode 100644 index 00000000000..025e20a7193 --- /dev/null +++ b/apps/sim/app/w/logs/components/filters/components/folder.tsx @@ -0,0 +1,168 @@ +import { useEffect, useState } from 'react' +import { Check, ChevronDown, Folder } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu' +import { useFilterStore } from '@/app/w/logs/stores/store' +import { useFolderStore } from '@/stores/folders/store' +import { useWorkflowRegistry } from '@/stores/workflows/registry/store' + +interface FolderOption { + id: string + name: string + color: string + path: string // For nested folders, show full path +} + +export default function FolderFilter() { + const { folderIds, toggleFolderId, setFolderIds } = useFilterStore() + const { getFolderTree, getFolderPath, fetchFolders } = useFolderStore() + const { activeWorkspaceId } = useWorkflowRegistry() + const [folders, setFolders] = useState([]) + const [loading, setLoading] = useState(true) + + // Fetch all available folders from the API + useEffect(() => { + const fetchFoldersData = async () => { + try { + setLoading(true) + if (activeWorkspaceId) { + await fetchFolders(activeWorkspaceId) + const folderTree = getFolderTree(activeWorkspaceId) + + // Flatten the folder tree and create options with full paths + const flattenFolders = (nodes: any[], parentPath = ''): FolderOption[] => { + const result: FolderOption[] = [] + + for (const node of nodes) { + const currentPath = parentPath ? `${parentPath} / ${node.name}` : node.name + result.push({ + id: node.id, + name: node.name, + color: node.color || '#6B7280', + path: currentPath, + }) + + // Add children recursively + if (node.children && node.children.length > 0) { + result.push(...flattenFolders(node.children, currentPath)) + } + } + + return result + } + + const folderOptions = flattenFolders(folderTree) + setFolders(folderOptions) + } + } catch (error) { + console.error('Failed to fetch folders:', error) + } finally { + setLoading(false) + } + } + + fetchFoldersData() + }, [activeWorkspaceId, fetchFolders, getFolderTree]) + + // Get display text for the dropdown button + const getSelectedFoldersText = () => { + if (folderIds.length === 0) return 'All folders' + if (folderIds.length === 1) { + const selected = folders.find((f) => f.id === folderIds[0]) + return selected ? selected.name : 'All folders' + } + return `${folderIds.length} folders selected` + } + + // Check if a folder is selected + const isFolderSelected = (folderId: string) => { + return folderIds.includes(folderId) + } + + // Clear all selections + const clearSelections = () => { + setFolderIds([]) + } + + // Add special option for workflows without folders + const includeRootOption = true + + return ( + + + + + + { + e.preventDefault() + clearSelections() + }} + className='flex cursor-pointer items-center justify-between p-2 text-sm' + > + All folders + {folderIds.length === 0 && } + + + {/* Option for workflows without folders */} + {includeRootOption && ( + { + e.preventDefault() + toggleFolderId('root') + }} + className='flex cursor-pointer items-center justify-between p-2 text-sm' + > +
+ + No folder +
+ {isFolderSelected('root') && } +
+ )} + + {(!loading && folders.length > 0) || includeRootOption ? : null} + + {!loading && + folders.map((folder) => ( + { + e.preventDefault() + toggleFolderId(folder.id) + }} + className='flex cursor-pointer items-center justify-between p-2 text-sm' + > +
+
+ + {folder.path} + +
+ {isFolderSelected(folder.id) && } + + ))} + + {loading && ( + + Loading folders... + + )} + + + ) +} diff --git a/apps/sim/app/w/logs/components/filters/filters.tsx b/apps/sim/app/w/logs/components/filters/filters.tsx index 04be4538c45..853ce8bd599 100644 --- a/apps/sim/app/w/logs/components/filters/filters.tsx +++ b/apps/sim/app/w/logs/components/filters/filters.tsx @@ -5,6 +5,7 @@ import { Button } from '@/components/ui/button' import { isProd } from '@/lib/environment' import { useUserSubscription } from '@/hooks/use-user-subscription' import FilterSection from './components/filter-section' +import FolderFilter from './components/folder' import Level from './components/level' import Timeline from './components/timeline' import Trigger from './components/trigger' @@ -62,6 +63,9 @@ export function Filters() { {/* Trigger Filter */} } /> + {/* Folder Filter */} + } /> + {/* Workflow Filter */} } />
diff --git a/apps/sim/app/w/logs/logs.tsx b/apps/sim/app/w/logs/logs.tsx index b4dc0e1d3d4..a19d0ed0784 100644 --- a/apps/sim/app/w/logs/logs.tsx +++ b/apps/sim/app/w/logs/logs.tsx @@ -72,6 +72,7 @@ export default function Logs() { timeRange, level, workflowIds, + folderIds, searchQuery, triggers, } = useFilterStore() @@ -225,6 +226,7 @@ export default function Logs() { timeRange, level, workflowIds, + folderIds, searchQuery, triggers, setPage, diff --git a/apps/sim/app/w/logs/stores/store.ts b/apps/sim/app/w/logs/stores/store.ts index fe717abe284..95347860682 100644 --- a/apps/sim/app/w/logs/stores/store.ts +++ b/apps/sim/app/w/logs/stores/store.ts @@ -6,6 +6,7 @@ export const useFilterStore = create((set, get) => ({ timeRange: 'All time', level: 'all', workflowIds: [], + folderIds: [], searchQuery: '', triggers: [], loading: true, @@ -53,6 +54,25 @@ export const useFilterStore = create((set, get) => ({ get().resetPagination() }, + setFolderIds: (folderIds) => { + set({ folderIds }) + get().resetPagination() + }, + + toggleFolderId: (folderId) => { + const currentFolderIds = [...get().folderIds] + const index = currentFolderIds.indexOf(folderId) + + if (index === -1) { + currentFolderIds.push(folderId) + } else { + currentFolderIds.splice(index, 1) + } + + set({ folderIds: currentFolderIds }) + get().resetPagination() + }, + setSearchQuery: (searchQuery) => { set({ searchQuery }) get().resetPagination() @@ -91,7 +111,7 @@ export const useFilterStore = create((set, get) => ({ // Build query parameters for server-side filtering buildQueryParams: (page: number, limit: number) => { - const { timeRange, level, workflowIds, searchQuery, triggers } = get() + const { timeRange, level, workflowIds, folderIds, searchQuery, triggers } = get() const params = new URLSearchParams() params.set('includeWorkflow', 'true') @@ -113,6 +133,11 @@ export const useFilterStore = create((set, get) => ({ params.set('workflowIds', workflowIds.join(',')) } + // Add folder filter + if (folderIds.length > 0) { + params.set('folderIds', folderIds.join(',')) + } + // Add time range filter if (timeRange !== 'All time') { const now = new Date() diff --git a/apps/sim/app/w/logs/stores/types.ts b/apps/sim/app/w/logs/stores/types.ts index 86b9ce43f3c..c6266fd3967 100644 --- a/apps/sim/app/w/logs/stores/types.ts +++ b/apps/sim/app/w/logs/stores/types.ts @@ -93,6 +93,7 @@ export interface FilterState { timeRange: TimeRange level: LogLevel workflowIds: string[] + folderIds: string[] searchQuery: string triggers: TriggerType[] @@ -111,6 +112,8 @@ export interface FilterState { setLevel: (level: LogLevel) => void setWorkflowIds: (workflowIds: string[]) => void toggleWorkflowId: (workflowId: string) => void + setFolderIds: (folderIds: string[]) => void + toggleFolderId: (folderId: string) => void setSearchQuery: (query: string) => void setTriggers: (triggers: TriggerType[]) => void toggleTrigger: (trigger: TriggerType) => void diff --git a/apps/sim/db/migrations/0042_breezy_miracleman.sql b/apps/sim/db/migrations/0042_breezy_miracleman.sql new file mode 100644 index 00000000000..67d58a2e9e0 --- /dev/null +++ b/apps/sim/db/migrations/0042_breezy_miracleman.sql @@ -0,0 +1,21 @@ +CREATE TABLE "workflow_folder" ( + "id" text PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "user_id" text NOT NULL, + "workspace_id" text NOT NULL, + "parent_id" text, + "color" text DEFAULT '#6B7280', + "is_expanded" boolean DEFAULT true NOT NULL, + "sort_order" integer DEFAULT 0 NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "workflow" ADD COLUMN "folder_id" text;--> statement-breakpoint +ALTER TABLE "workflow_folder" ADD CONSTRAINT "workflow_folder_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "workflow_folder" ADD CONSTRAINT "workflow_folder_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "workflow_folder" ADD CONSTRAINT "workflow_folder_parent_id_workflow_folder_id_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."workflow_folder"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "workflow_folder_workspace_parent_idx" ON "workflow_folder" USING btree ("workspace_id","parent_id");--> statement-breakpoint +CREATE INDEX "workflow_folder_user_idx" ON "workflow_folder" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "workflow_folder_parent_sort_idx" ON "workflow_folder" USING btree ("parent_id","sort_order");--> statement-breakpoint +ALTER TABLE "workflow" ADD CONSTRAINT "workflow_folder_id_workflow_folder_id_fk" FOREIGN KEY ("folder_id") REFERENCES "public"."workflow_folder"("id") ON DELETE set null ON UPDATE no action; \ No newline at end of file diff --git a/apps/sim/db/migrations/meta/0042_snapshot.json b/apps/sim/db/migrations/meta/0042_snapshot.json new file mode 100644 index 00000000000..40abb4d8424 --- /dev/null +++ b/apps/sim/db/migrations/meta/0042_snapshot.json @@ -0,0 +1,3082 @@ +{ + "id": "5a104de1-5afa-46be-bbe8-5a8759024b15", + "prevId": "01a747d8-d7e0-4f49-af52-b45e0f4343a9", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_key": { + "name": "api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "api_key_user_id_user_id_fk": { + "name": "api_key_user_id_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_key_key_unique": { + "name": "api_key_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat": { + "name": "chat", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subdomain": { + "name": "subdomain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "output_configs": { + "name": "output_configs", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "subdomain_idx": { + "name": "subdomain_idx", + "columns": [ + { + "expression": "subdomain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_workflow_id_workflow_id_fk": { + "name": "chat_workflow_id_workflow_id_fk", + "tableFrom": "chat", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_user_id_user_id_fk": { + "name": "chat_user_id_user_id_fk", + "tableFrom": "chat", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_tools": { + "name": "custom_tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "custom_tools_user_id_user_id_fk": { + "name": "custom_tools_user_id_user_id_fk", + "tableFrom": "custom_tools", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.document": { + "name": "document", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_url": { + "name": "file_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_hash": { + "name": "file_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "character_count": { + "name": "character_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_status": { + "name": "processing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_completed_at": { + "name": "processing_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_error": { + "name": "processing_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "doc_kb_id_idx": { + "name": "doc_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_file_hash_idx": { + "name": "doc_file_hash_idx", + "columns": [ + { + "expression": "file_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_filename_idx": { + "name": "doc_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_uploaded_at_idx": { + "name": "doc_kb_uploaded_at_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "uploaded_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_processing_status_idx": { + "name": "doc_processing_status_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_knowledge_base_id_knowledge_base_id_fk": { + "name": "document_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.embedding": { + "name": "embedding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_index": { + "name": "chunk_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chunk_hash": { + "name": "chunk_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_length": { + "name": "content_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "start_offset": { + "name": "start_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_offset": { + "name": "end_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "overlap_tokens": { + "name": "overlap_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "search_rank": { + "name": "search_rank", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'1.0'" + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "quality_score": { + "name": "quality_score", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content_tsv": { + "name": "content_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"embedding\".\"content\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "emb_kb_id_idx": { + "name": "emb_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_id_idx": { + "name": "emb_doc_id_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_chunk_idx": { + "name": "emb_doc_chunk_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chunk_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_model_idx": { + "name": "emb_kb_model_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_chunk_hash_idx": { + "name": "emb_chunk_hash_idx", + "columns": [ + { + "expression": "chunk_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_access_idx": { + "name": "emb_kb_access_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_accessed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_rank_idx": { + "name": "emb_kb_rank_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "search_rank", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_enabled_idx": { + "name": "emb_kb_enabled_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_enabled_idx": { + "name": "emb_doc_enabled_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "embedding_vector_hnsw_idx": { + "name": "embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "emb_metadata_gin_idx": { + "name": "emb_metadata_gin_idx", + "columns": [ + { + "expression": "metadata", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "emb_content_fts_idx": { + "name": "emb_content_fts_idx", + "columns": [ + { + "expression": "content_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "embedding_knowledge_base_id_knowledge_base_id_fk": { + "name": "embedding_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "embedding", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "embedding_document_id_document_id_fk": { + "name": "embedding_document_id_document_id_fk", + "tableFrom": "embedding", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_not_null_check": { + "name": "embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_user_id_user_id_fk": { + "name": "environment_user_id_user_id_fk", + "tableFrom": "environment", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_user_id_unique": { + "name": "environment_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base": { + "name": "knowledge_base", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "embedding_dimension": { + "name": "embedding_dimension", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1536 + }, + "chunking_config": { + "name": "chunking_config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"maxSize\": 1024, \"minSize\": 100, \"overlap\": 200}'" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_user_id_idx": { + "name": "kb_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_id_idx": { + "name": "kb_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_user_workspace_idx": { + "name": "kb_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_deleted_at_idx": { + "name": "kb_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_user_id_user_id_fk": { + "name": "knowledge_base_user_id_user_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_workspace_id_workspace_id_fk": { + "name": "knowledge_base_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.marketplace": { + "name": "marketplace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_name": { + "name": "author_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "views": { + "name": "views", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "marketplace_workflow_id_workflow_id_fk": { + "name": "marketplace_workflow_id_workflow_id_fk", + "tableFrom": "marketplace", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "marketplace_author_id_user_id_fk": { + "name": "marketplace_author_id_user_id_fk", + "tableFrom": "marketplace", + "tableTo": "user", + "columnsFrom": ["author_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory": { + "name": "memory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "memory_key_idx": { + "name": "memory_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workflow_idx": { + "name": "memory_workflow_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workflow_key_idx": { + "name": "memory_workflow_key_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_workflow_id_workflow_id_fk": { + "name": "memory_workflow_id_workflow_id_fk", + "tableFrom": "memory", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_active_organization_id_organization_id_fk": { + "name": "session_active_organization_id_organization_id_fk", + "tableFrom": "session", + "tableTo": "organization", + "columnsFrom": ["active_organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "debug_mode": { + "name": "debug_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "auto_connect": { + "name": "auto_connect", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "auto_fill_env_vars": { + "name": "auto_fill_env_vars", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_enabled": { + "name": "telemetry_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_notified_user": { + "name": "telemetry_notified_user", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "email_preferences": { + "name": "email_preferences", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "general": { + "name": "general", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "settings_user_id_user_id_fk": { + "name": "settings_user_id_user_id_fk", + "tableFrom": "settings", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_user_id_unique": { + "name": "settings_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "period_start": { + "name": "period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "period_end": { + "name": "period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "seats": { + "name": "seats", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "trial_start": { + "name": "trial_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trial_end": { + "name": "trial_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_stats": { + "name": "user_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_manual_executions": { + "name": "total_manual_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_api_calls": { + "name": "total_api_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_webhook_triggers": { + "name": "total_webhook_triggers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_scheduled_executions": { + "name": "total_scheduled_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_chat_executions": { + "name": "total_chat_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_tokens_used": { + "name": "total_tokens_used", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost": { + "name": "total_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_active": { + "name": "last_active", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_stats_user_id_user_id_fk": { + "name": "user_stats_user_id_user_id_fk", + "tableFrom": "user_stats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_stats_user_id_unique": { + "name": "user_stats_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.waitlist": { + "name": "waitlist", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "waitlist_email_unique": { + "name": "waitlist_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook": { + "name": "webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_config": { + "name": "provider_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "path_idx": { + "name": "path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_workflow_id_workflow_id_fk": { + "name": "webhook_workflow_id_workflow_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#3972F6'" + }, + "last_synced": { + "name": "last_synced", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "is_deployed": { + "name": "is_deployed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deployed_state": { + "name": "deployed_state", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "deployed_at": { + "name": "deployed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "collaborators": { + "name": "collaborators", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "marketplace_data": { + "name": "marketplace_data", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "is_published": { + "name": "is_published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "workflow_user_id_user_id_fk": { + "name": "workflow_user_id_user_id_fk", + "tableFrom": "workflow", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_workspace_id_workspace_id_fk": { + "name": "workflow_workspace_id_workspace_id_fk", + "tableFrom": "workflow", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_id_workflow_folder_id_fk": { + "name": "workflow_folder_id_workflow_folder_id_fk", + "tableFrom": "workflow", + "tableTo": "workflow_folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_folder": { + "name": "workflow_folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'#6B7280'" + }, + "is_expanded": { + "name": "is_expanded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_folder_workspace_parent_idx": { + "name": "workflow_folder_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_user_idx": { + "name": "workflow_folder_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_parent_sort_idx": { + "name": "workflow_folder_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_folder_user_id_user_id_fk": { + "name": "workflow_folder_user_id_user_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_workspace_id_workspace_id_fk": { + "name": "workflow_folder_workspace_id_workspace_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_logs": { + "name": "workflow_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "duration": { + "name": "duration", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "workflow_logs_workflow_id_workflow_id_fk": { + "name": "workflow_logs_workflow_id_workflow_id_fk", + "tableFrom": "workflow_logs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_schedule": { + "name": "workflow_schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_ran_at": { + "name": "last_ran_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workflow_schedule_workflow_id_workflow_id_fk": { + "name": "workflow_schedule_workflow_id_workflow_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workflow_schedule_workflow_id_unique": { + "name": "workflow_schedule_workflow_id_unique", + "nullsNotDistinct": false, + "columns": ["workflow_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_owner_id_user_id_fk": { + "name": "workspace_owner_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_invitation": { + "name": "workspace_invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_invitation_workspace_id_workspace_id_fk": { + "name": "workspace_invitation_workspace_id_workspace_id_fk", + "tableFrom": "workspace_invitation", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_invitation_inviter_id_user_id_fk": { + "name": "workspace_invitation_inviter_id_user_id_fk", + "tableFrom": "workspace_invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_invitation_token_unique": { + "name": "workspace_invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_member": { + "name": "workspace_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_workspace_idx": { + "name": "user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_member_workspace_id_workspace_id_fk": { + "name": "workspace_member_workspace_id_workspace_id_fk", + "tableFrom": "workspace_member", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_member_user_id_user_id_fk": { + "name": "workspace_member_user_id_user_id_fk", + "tableFrom": "workspace_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/apps/sim/db/migrations/meta/_journal.json b/apps/sim/db/migrations/meta/_journal.json index 9afe5b351d0..ff79c9ee703 100644 --- a/apps/sim/db/migrations/meta/_journal.json +++ b/apps/sim/db/migrations/meta/_journal.json @@ -288,6 +288,13 @@ "when": 1749514555378, "tag": "0041_sparkling_ma_gnuci", "breakpoints": true + }, + { + "idx": 42, + "version": "7", + "when": 1749784177503, + "tag": "0042_breezy_miracleman", + "breakpoints": true } ] } diff --git a/apps/sim/db/migrations/relations.ts b/apps/sim/db/migrations/relations.ts deleted file mode 100644 index d043340e263..00000000000 --- a/apps/sim/db/migrations/relations.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { relations } from 'drizzle-orm/relations' -import { account, session, user } from './schema' - -export const accountRelations = relations(account, ({ one }) => ({ - user: one(user, { - fields: [account.userId], - references: [user.id], - }), -})) - -export const userRelations = relations(user, ({ many }) => ({ - accounts: many(account), - sessions: many(session), -})) - -export const sessionRelations = relations(session, ({ one }) => ({ - user: one(user, { - fields: [session.userId], - references: [user.id], - }), -})) diff --git a/apps/sim/db/migrations/schema.ts b/apps/sim/db/migrations/schema.ts deleted file mode 100644 index 6454c56d725..00000000000 --- a/apps/sim/db/migrations/schema.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { boolean, foreignKey, pgTable, text, timestamp, unique } from 'drizzle-orm/pg-core' - -export const verification = pgTable('verification', { - id: text().primaryKey().notNull(), - identifier: text().notNull(), - value: text().notNull(), - expiresAt: timestamp('expires_at', { mode: 'string' }).notNull(), - createdAt: timestamp('created_at', { mode: 'string' }), - updatedAt: timestamp('updated_at', { mode: 'string' }), -}) - -export const user = pgTable( - 'user', - { - id: text().primaryKey().notNull(), - name: text().notNull(), - email: text().notNull(), - emailVerified: boolean('email_verified').notNull(), - image: text(), - createdAt: timestamp('created_at', { mode: 'string' }).notNull(), - updatedAt: timestamp('updated_at', { mode: 'string' }).notNull(), - }, - (table) => [unique('user_email_unique').on(table.email)] -) - -export const account = pgTable( - 'account', - { - id: text().primaryKey().notNull(), - accountId: text('account_id').notNull(), - providerId: text('provider_id').notNull(), - userId: text('user_id').notNull(), - accessToken: text('access_token'), - refreshToken: text('refresh_token'), - idToken: text('id_token'), - accessTokenExpiresAt: timestamp('access_token_expires_at', { - mode: 'string', - }), - refreshTokenExpiresAt: timestamp('refresh_token_expires_at', { - mode: 'string', - }), - scope: text(), - password: text(), - createdAt: timestamp('created_at', { mode: 'string' }).notNull(), - updatedAt: timestamp('updated_at', { mode: 'string' }).notNull(), - }, - (table) => [ - foreignKey({ - columns: [table.userId], - foreignColumns: [user.id], - name: 'account_user_id_user_id_fk', - }).onDelete('cascade'), - ] -) - -export const session = pgTable( - 'session', - { - id: text().primaryKey().notNull(), - expiresAt: timestamp('expires_at', { mode: 'string' }).notNull(), - token: text().notNull(), - createdAt: timestamp('created_at', { mode: 'string' }).notNull(), - updatedAt: timestamp('updated_at', { mode: 'string' }).notNull(), - ipAddress: text('ip_address'), - userAgent: text('user_agent'), - userId: text('user_id').notNull(), - }, - (table) => [ - foreignKey({ - columns: [table.userId], - foreignColumns: [user.id], - name: 'session_user_id_user_id_fk', - }).onDelete('cascade'), - unique('session_token_unique').on(table.token), - ] -) diff --git a/apps/sim/db/schema.ts b/apps/sim/db/schema.ts index a8c74a8f6eb..c0094992c6b 100644 --- a/apps/sim/db/schema.ts +++ b/apps/sim/db/schema.ts @@ -78,12 +78,41 @@ export const verification = pgTable('verification', { updatedAt: timestamp('updated_at'), }) +export const workflowFolder = pgTable( + 'workflow_folder', + { + id: text('id').primaryKey(), + name: text('name').notNull(), + userId: text('user_id') + .notNull() + .references(() => user.id, { onDelete: 'cascade' }), + workspaceId: text('workspace_id') + .notNull() + .references(() => workspace.id, { onDelete: 'cascade' }), + parentId: text('parent_id'), // Self-reference will be handled by foreign key constraint + color: text('color').default('#6B7280'), + isExpanded: boolean('is_expanded').notNull().default(true), + sortOrder: integer('sort_order').notNull().default(0), + createdAt: timestamp('created_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at').notNull().defaultNow(), + }, + (table) => ({ + userIdx: index('workflow_folder_user_idx').on(table.userId), + workspaceParentIdx: index('workflow_folder_workspace_parent_idx').on( + table.workspaceId, + table.parentId + ), + parentSortIdx: index('workflow_folder_parent_sort_idx').on(table.parentId, table.sortOrder), + }) +) + export const workflow = pgTable('workflow', { id: text('id').primaryKey(), userId: text('user_id') .notNull() .references(() => user.id, { onDelete: 'cascade' }), workspaceId: text('workspace_id').references(() => workspace.id, { onDelete: 'cascade' }), + folderId: text('folder_id').references(() => workflowFolder.id, { onDelete: 'set null' }), name: text('name').notNull(), description: text('description'), state: json('state').notNull(), @@ -98,11 +127,8 @@ export const workflow = pgTable('workflow', { runCount: integer('run_count').notNull().default(0), lastRunAt: timestamp('last_run_at'), variables: json('variables').default('{}'), - marketplaceData: json('marketplace_data'), // Format: { id: string, status: 'owner' | 'temp' } - - // These columns are kept for backward compatibility during migration - // @deprecated - Use marketplaceData instead isPublished: boolean('is_published').notNull().default(false), + marketplaceData: json('marketplace_data'), }) export const waitlist = pgTable('waitlist', { diff --git a/apps/sim/stores/folders/store.ts b/apps/sim/stores/folders/store.ts new file mode 100644 index 00000000000..de063d4ddc1 --- /dev/null +++ b/apps/sim/stores/folders/store.ts @@ -0,0 +1,270 @@ +import { create } from 'zustand' +import { devtools } from 'zustand/middleware' + +export interface WorkflowFolder { + id: string + name: string + userId: string + workspaceId: string + parentId: string | null + color: string + isExpanded: boolean + sortOrder: number + createdAt: Date + updatedAt: Date +} + +export interface FolderTreeNode extends WorkflowFolder { + children: FolderTreeNode[] + level: number +} + +interface FolderState { + folders: Record + isLoading: boolean + expandedFolders: Set + + // Actions + setFolders: (folders: WorkflowFolder[]) => void + addFolder: (folder: WorkflowFolder) => void + updateFolder: (id: string, updates: Partial) => void + removeFolder: (id: string) => void + setLoading: (loading: boolean) => void + toggleExpanded: (folderId: string) => void + setExpanded: (folderId: string, expanded: boolean) => void + + // Computed values + getFolderTree: (workspaceId: string) => FolderTreeNode[] + getFolderById: (id: string) => WorkflowFolder | undefined + getChildFolders: (parentId: string | null) => WorkflowFolder[] + getFolderPath: (folderId: string) => WorkflowFolder[] + + // API actions + fetchFolders: (workspaceId: string) => Promise + createFolder: (data: { + name: string + workspaceId: string + parentId?: string + color?: string + }) => Promise + updateFolderAPI: (id: string, updates: Partial) => Promise + deleteFolder: (id: string, moveWorkflowsTo?: string) => Promise +} + +export const useFolderStore = create()( + devtools( + (set, get) => ({ + folders: {}, + isLoading: false, + expandedFolders: new Set(), + + setFolders: (folders) => + set(() => ({ + folders: folders.reduce( + (acc, folder) => { + acc[folder.id] = folder + return acc + }, + {} as Record + ), + })), + + addFolder: (folder) => + set((state) => ({ + folders: { ...state.folders, [folder.id]: folder }, + })), + + updateFolder: (id, updates) => + set((state) => ({ + folders: { + ...state.folders, + [id]: state.folders[id] ? { ...state.folders[id], ...updates } : state.folders[id], + }, + })), + + removeFolder: (id) => + set((state) => { + const newFolders = { ...state.folders } + delete newFolders[id] + return { folders: newFolders } + }), + + setLoading: (loading) => set({ isLoading: loading }), + + toggleExpanded: (folderId) => + set((state) => { + const newExpanded = new Set(state.expandedFolders) + if (newExpanded.has(folderId)) { + newExpanded.delete(folderId) + } else { + newExpanded.add(folderId) + } + return { expandedFolders: newExpanded } + }), + + setExpanded: (folderId, expanded) => + set((state) => { + const newExpanded = new Set(state.expandedFolders) + if (expanded) { + newExpanded.add(folderId) + } else { + newExpanded.delete(folderId) + } + return { expandedFolders: newExpanded } + }), + + getFolderTree: (workspaceId) => { + const folders = Object.values(get().folders).filter((f) => f.workspaceId === workspaceId) + + const buildTree = (parentId: string | null, level = 0): FolderTreeNode[] => { + return folders + .filter((folder) => folder.parentId === parentId) + .sort((a, b) => a.sortOrder - b.sortOrder || a.name.localeCompare(b.name)) + .map((folder) => ({ + ...folder, + children: buildTree(folder.id, level + 1), + level, + })) + } + + return buildTree(null) + }, + + getFolderById: (id) => get().folders[id], + + getChildFolders: (parentId) => + Object.values(get().folders) + .filter((folder) => folder.parentId === parentId) + .sort((a, b) => a.sortOrder - b.sortOrder || a.name.localeCompare(b.name)), + + getFolderPath: (folderId) => { + const folders = get().folders + const path: WorkflowFolder[] = [] + let currentId: string | null = folderId + + while (currentId && folders[currentId]) { + const folder: WorkflowFolder = folders[currentId] + path.unshift(folder) + currentId = folder.parentId + } + + return path + }, + + fetchFolders: async (workspaceId) => { + set({ isLoading: true }) + try { + const response = await fetch(`/api/folders?workspaceId=${workspaceId}`) + if (!response.ok) { + throw new Error('Failed to fetch folders') + } + const { folders }: { folders: any[] } = await response.json() + + // Convert date strings to Date objects + const processedFolders: WorkflowFolder[] = folders.map((folder: any) => ({ + id: folder.id, + name: folder.name, + userId: folder.userId, + workspaceId: folder.workspaceId, + parentId: folder.parentId, + color: folder.color, + isExpanded: folder.isExpanded, + sortOrder: folder.sortOrder, + createdAt: new Date(folder.createdAt), + updatedAt: new Date(folder.updatedAt), + })) + + get().setFolders(processedFolders) + + // Initialize expanded state from folder data + const expandedSet = new Set() + processedFolders.forEach((folder: WorkflowFolder) => { + if (folder.isExpanded) { + expandedSet.add(folder.id) + } + }) + set({ expandedFolders: expandedSet }) + } catch (error) { + console.error('Error fetching folders:', error) + } finally { + set({ isLoading: false }) + } + }, + + createFolder: async (data) => { + const response = await fetch('/api/folders', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }) + + if (!response.ok) { + const error = await response.json() + throw new Error(error.error || 'Failed to create folder') + } + + const { folder } = await response.json() + const processedFolder = { + ...folder, + createdAt: new Date(folder.createdAt), + updatedAt: new Date(folder.updatedAt), + } + + get().addFolder(processedFolder) + return processedFolder + }, + + updateFolderAPI: async (id, updates) => { + const response = await fetch(`/api/folders/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(updates), + }) + + if (!response.ok) { + const error = await response.json() + throw new Error(error.error || 'Failed to update folder') + } + + const { folder } = await response.json() + const processedFolder = { + ...folder, + createdAt: new Date(folder.createdAt), + updatedAt: new Date(folder.updatedAt), + } + + get().updateFolder(id, processedFolder) + + // Update expanded state if isExpanded was changed + if (updates.isExpanded !== undefined) { + get().setExpanded(id, updates.isExpanded) + } + + return processedFolder + }, + + deleteFolder: async (id, moveWorkflowsTo) => { + const url = moveWorkflowsTo + ? `/api/folders/${id}?moveWorkflowsTo=${moveWorkflowsTo}` + : `/api/folders/${id}` + + const response = await fetch(url, { method: 'DELETE' }) + + if (!response.ok) { + const error = await response.json() + throw new Error(error.error || 'Failed to delete folder') + } + + get().removeFolder(id) + + // Remove from expanded state + set((state) => { + const newExpanded = new Set(state.expandedFolders) + newExpanded.delete(id) + return { expandedFolders: newExpanded } + }) + }, + }), + { name: 'folder-store' } + ) +) diff --git a/apps/sim/stores/workflows/index.ts b/apps/sim/stores/workflows/index.ts index 5586b69df82..f00b26f4499 100644 --- a/apps/sim/stores/workflows/index.ts +++ b/apps/sim/stores/workflows/index.ts @@ -67,6 +67,8 @@ export function getWorkflowWithValues(workflowId: string) { description: metadata.description, color: metadata.color || '#3972F6', marketplaceData: metadata.marketplaceData || null, + workspaceId: metadata.workspaceId, + folderId: metadata.folderId, state: { blocks: mergedBlocks, edges: workflowState.edges, @@ -161,6 +163,7 @@ export function getAllWorkflowsWithValues() { color: metadata.color || '#3972F6', marketplaceData: metadata.marketplaceData || null, workspaceId: metadata.workspaceId, // Include workspaceId in the result + folderId: metadata.folderId, // Include folderId in the result state: { blocks: mergedBlocks, edges: workflowState.edges, diff --git a/apps/sim/stores/workflows/registry/store.ts b/apps/sim/stores/workflows/registry/store.ts index 157fca4c7f6..70d7cf13291 100644 --- a/apps/sim/stores/workflows/registry/store.ts +++ b/apps/sim/stores/workflows/registry/store.ts @@ -669,6 +669,7 @@ export const useWorkflowRegistry = create()( ? { id: options.marketplaceId, status: 'temp' as const } : undefined, workspaceId, // Associate with workspace + folderId: options.folderId || null, // Associate with folder if provided } let initialState: any diff --git a/apps/sim/stores/workflows/registry/types.ts b/apps/sim/stores/workflows/registry/types.ts index 2027a0cacc2..facb08ce540 100644 --- a/apps/sim/stores/workflows/registry/types.ts +++ b/apps/sim/stores/workflows/registry/types.ts @@ -18,6 +18,7 @@ export interface WorkflowMetadata { color: string marketplaceData?: MarketplaceData | null workspaceId?: string + folderId?: string | null } export interface WorkflowRegistryState { @@ -43,6 +44,7 @@ export interface WorkflowRegistryActions { name?: string description?: string workspaceId?: string + folderId?: string | null }) => string duplicateWorkflow: (sourceId: string) => string | null getWorkflowDeploymentStatus: (workflowId: string | null) => DeploymentStatus | null diff --git a/apps/sim/stores/workflows/sync.ts b/apps/sim/stores/workflows/sync.ts index c2fbeb412b0..9eea7f8b58f 100644 --- a/apps/sim/stores/workflows/sync.ts +++ b/apps/sim/stores/workflows/sync.ts @@ -237,6 +237,7 @@ export async function fetchWorkflowsFromDB(): Promise { createdAt, marketplaceData, workspaceId, // Extract workspaceId + folderId, // Extract folderId } = workflow // Ensure this workflow belongs to the current workspace @@ -257,6 +258,7 @@ export async function fetchWorkflowsFromDB(): Promise { lastModified: createdAt ? new Date(createdAt) : new Date(lastSynced), marketplaceData: marketplaceData || null, workspaceId, // Include workspaceId in metadata + folderId: folderId || null, // Include folderId in metadata } // 2. Prepare workflow state data