From 947472b58e866fdd87c8e0e9d5de68c67f624e27 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Tue, 7 Apr 2026 16:11:31 -0700 Subject: [PATCH 01/60] v0.6.29: login improvements, posthog telemetry (#4026) * feat(posthog): Add tracking on mothership abort (#4023) Co-authored-by: Theodore Li * fix(login): fix captcha headers for manual login (#4025) * fix(signup): fix turnstile key loading * fix(login): fix captcha header passing * Catch user already exists, remove login form captcha --- .../w/[workflowId]/components/panel/panel.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx index 237a861fe00..50417690d94 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx @@ -427,6 +427,14 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel copilotStopGeneration() }, [copilotStopGeneration, getCopilotCurrentRequestId, workspaceId]) + const handleCopilotStopGeneration = useCallback(() => { + captureEvent(posthogRef.current, 'task_generation_aborted', { + workspace_id: workspaceId, + view: 'copilot', + }) + copilotStopGeneration() + }, [copilotStopGeneration, workspaceId]) + const handleCopilotSubmit = useCallback( (text: string, fileAttachments?: FileAttachmentForApi[], contexts?: ChatContext[]) => { const trimmed = text.trim() From befeac919adb81aa7b356e2a8d9710b59967a2c3 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 12 May 2026 14:33:56 -0700 Subject: [PATCH 02/60] feat(files): folders + vfs update --- apps/sim/app/api/tools/file/manage/route.ts | 18 +- .../[id]/files/bulk-archive/route.ts | 43 + .../workspaces/[id]/files/download/route.ts | 113 + .../[id]/files/folders/[folderId]/route.ts | 86 + .../workspaces/[id]/files/folders/route.ts | 79 + .../api/workspaces/[id]/files/move/route.ts | 51 + .../workspaces/[id]/files/presigned/route.ts | 15 +- .../workspaces/[id]/files/register/route.ts | 3 +- .../app/api/workspaces/[id]/files/route.ts | 6 +- .../[workspaceId]/components/index.ts | 1 + .../components/resource/resource.tsx | 80 +- .../components/action-bar/action-bar.tsx | 88 + .../files-list-context-menu.tsx | 12 +- .../workspace/[workspaceId]/files/files.tsx | 730 +- .../hooks/queries/workspace-file-folders.ts | 154 + apps/sim/hooks/queries/workspace-files.ts | 15 +- apps/sim/lib/api/contracts/index.ts | 1 + .../api/contracts/workspace-file-folders.ts | 170 + apps/sim/lib/api/contracts/workspace-files.ts | 4 + .../copilot/tools/server/files/create-file.ts | 22 +- .../tools/server/files/workspace-file.ts | 39 +- apps/sim/lib/copilot/vfs/serializers.ts | 6 + apps/sim/lib/copilot/vfs/workspace-vfs.ts | 30 +- apps/sim/lib/uploads/client/direct-upload.ts | 14 +- apps/sim/lib/uploads/client/download.ts | 11 + .../lib/uploads/contexts/workspace/index.ts | 1 + .../workspace-file-folder-manager.test.ts | 20 + .../workspace-file-folder-manager.ts | 612 + .../workspace/workspace-file-manager.test.ts | 27 + .../workspace/workspace-file-manager.ts | 156 +- apps/sim/lib/uploads/server/metadata.ts | 6 +- .../db/migrations/meta/0207_snapshot.json | 15873 ---------------- packages/db/schema.ts | 50 +- scripts/check-api-validation-contracts.ts | 4 +- 34 files changed, 2454 insertions(+), 16086 deletions(-) create mode 100644 apps/sim/app/api/workspaces/[id]/files/bulk-archive/route.ts create mode 100644 apps/sim/app/api/workspaces/[id]/files/download/route.ts create mode 100644 apps/sim/app/api/workspaces/[id]/files/folders/[folderId]/route.ts create mode 100644 apps/sim/app/api/workspaces/[id]/files/folders/route.ts create mode 100644 apps/sim/app/api/workspaces/[id]/files/move/route.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/files/components/action-bar/action-bar.tsx create mode 100644 apps/sim/hooks/queries/workspace-file-folders.ts create mode 100644 apps/sim/lib/api/contracts/workspace-file-folders.ts create mode 100644 apps/sim/lib/uploads/contexts/workspace/workspace-file-folder-manager.test.ts create mode 100644 apps/sim/lib/uploads/contexts/workspace/workspace-file-folder-manager.ts delete mode 100644 packages/db/migrations/meta/0207_snapshot.json diff --git a/apps/sim/app/api/tools/file/manage/route.ts b/apps/sim/app/api/tools/file/manage/route.ts index 60082411473..1d92d090035 100644 --- a/apps/sim/app/api/tools/file/manage/route.ts +++ b/apps/sim/app/api/tools/file/manage/route.ts @@ -3,13 +3,16 @@ import { type NextRequest, NextResponse } from 'next/server' import { fileManageContract } from '@/lib/api/contracts/tools/file' import { parseRequest } from '@/lib/api/server' import { checkInternalAuth } from '@/lib/auth/hybrid' +import { splitWorkspaceFilePath } from '@/lib/copilot/tools/server/files/workspace-file' import { acquireLock, releaseLock } from '@/lib/core/config/redis' import { ensureAbsoluteUrl } from '@/lib/core/utils/urls' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { ensureWorkspaceFileFolderPath } from '@/lib/uploads/contexts/workspace/workspace-file-folder-manager' import { fetchWorkspaceFileBuffer, getWorkspaceFile, getWorkspaceFileByName, + resolveWorkspaceFileReference, updateWorkspaceFileContent, uploadWorkspaceFile, } from '@/lib/uploads/contexts/workspace/workspace-file-manager' @@ -91,14 +94,21 @@ export const POST = withRouteHandler(async (request: NextRequest) => { case 'write': { const { fileName, content, contentType } = body - const mimeType = contentType || getMimeTypeFromExtension(getFileExtension(fileName)) + const { folderSegments, leafName } = splitWorkspaceFilePath(fileName) + const folderId = await ensureWorkspaceFileFolderPath({ + workspaceId, + userId, + pathSegments: folderSegments, + }) + const mimeType = contentType || getMimeTypeFromExtension(getFileExtension(leafName)) const fileBuffer = Buffer.from(content ?? '', 'utf-8') const result = await uploadWorkspaceFile( workspaceId, userId, fileBuffer, - fileName, - mimeType + leafName, + mimeType, + { folderId } ) logger.info('File created', { @@ -121,7 +131,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { case 'append': { const { fileName, content } = body - const existing = await getWorkspaceFileByName(workspaceId, fileName) + const existing = await resolveWorkspaceFileReference(workspaceId, fileName) if (!existing) { return NextResponse.json( { success: false, error: `File not found: "${fileName}"` }, diff --git a/apps/sim/app/api/workspaces/[id]/files/bulk-archive/route.ts b/apps/sim/app/api/workspaces/[id]/files/bulk-archive/route.ts new file mode 100644 index 00000000000..c24d0e13524 --- /dev/null +++ b/apps/sim/app/api/workspaces/[id]/files/bulk-archive/route.ts @@ -0,0 +1,43 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { bulkArchiveWorkspaceFileItemsContract } from '@/lib/api/contracts/workspace-file-folders' +import { parseRequest } from '@/lib/api/server' +import { getSession } from '@/lib/auth' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { bulkArchiveWorkspaceFileItems } from '@/lib/uploads/contexts/workspace' +import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' + +const logger = createLogger('WorkspaceFileBulkArchiveAPI') + +export const POST = withRouteHandler( + async (request: NextRequest, context: { params: Promise<{ id: string }> }) => { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const parsed = await parseRequest(bulkArchiveWorkspaceFileItemsContract, request, context) + if (!parsed.success) return parsed.response + const { id: workspaceId } = parsed.data.params + const { fileIds, folderIds } = parsed.data.body + + const permission = await getUserEntityPermissions(session.user.id, 'workspace', workspaceId) + if (permission !== 'admin' && permission !== 'write') { + return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }) + } + + try { + const deletedItems = await bulkArchiveWorkspaceFileItems({ workspaceId, fileIds, folderIds }) + return NextResponse.json({ success: true, deletedItems }) + } catch (error) { + logger.error('Failed to bulk archive workspace file items:', error) + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Failed to archive items', + }, + { status: 400 } + ) + } + } +) diff --git a/apps/sim/app/api/workspaces/[id]/files/download/route.ts b/apps/sim/app/api/workspaces/[id]/files/download/route.ts new file mode 100644 index 00000000000..948ed23b313 --- /dev/null +++ b/apps/sim/app/api/workspaces/[id]/files/download/route.ts @@ -0,0 +1,113 @@ +import { createLogger } from '@sim/logger' +import JSZip from 'jszip' +import { type NextRequest, NextResponse } from 'next/server' +import { downloadWorkspaceFileItemsContract } from '@/lib/api/contracts/workspace-file-folders' +import { parseRequest } from '@/lib/api/server' +import { getSession } from '@/lib/auth' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { + fetchWorkspaceFileBuffer, + listWorkspaceFileFolders, + listWorkspaceFiles, +} from '@/lib/uploads/contexts/workspace' +import { verifyWorkspaceMembership } from '@/app/api/workflows/utils' + +const logger = createLogger('WorkspaceFilesDownloadAPI') + +function safeZipPath(path: string): string { + return path + .split('/') + .map((segment) => segment.trim().replace(/[<>:"\\|?*\x00-\x1f]/g, '_')) + .filter(Boolean) + .join('/') +} + +function collectDescendantFolderIds( + selectedFolderIds: string[], + folders: Array<{ id: string; parentId: string | null }> +): Set { + const folderIds = new Set(selectedFolderIds) + let changed = true + while (changed) { + changed = false + for (const folder of folders) { + if (folder.parentId && folderIds.has(folder.parentId) && !folderIds.has(folder.id)) { + folderIds.add(folder.id) + changed = true + } + } + } + return folderIds +} + +export const GET = withRouteHandler( + async (request: NextRequest, context: { params: Promise<{ id: string }> }) => { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const parsed = await parseRequest(downloadWorkspaceFileItemsContract, request, context) + if (!parsed.success) return parsed.response + const { id: workspaceId } = parsed.data.params + const { fileIds, folderIds } = parsed.data.query + + const permission = await verifyWorkspaceMembership(session.user.id, workspaceId) + if (!permission) { + return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }) + } + + try { + const [files, folders] = await Promise.all([ + listWorkspaceFiles(workspaceId), + listWorkspaceFileFolders(workspaceId), + ]) + const selectedFolderIds = collectDescendantFolderIds(folderIds, folders) + const requestedFileIds = new Set(fileIds) + const filesToZip = files.filter( + (file) => + requestedFileIds.has(file.id) || (file.folderId && selectedFolderIds.has(file.folderId)) + ) + + if (filesToZip.length === 0) { + return NextResponse.json({ error: 'No files selected for download' }, { status: 400 }) + } + + const zip = new JSZip() + const usedPaths = new Set() + for (const file of filesToZip) { + const buffer = await fetchWorkspaceFileBuffer(file) + const basePath = safeZipPath( + file.folderPath ? `${file.folderPath}/${file.name}` : file.name + ) + let zipPath = basePath || file.name + let suffix = 2 + while (usedPaths.has(zipPath)) { + const dotIndex = basePath.lastIndexOf('.') + zipPath = + dotIndex > 0 + ? `${basePath.slice(0, dotIndex)} (${suffix})${basePath.slice(dotIndex)}` + : `${basePath} (${suffix})` + suffix++ + } + usedPaths.add(zipPath) + zip.file(zipPath, buffer) + } + + const zipBuffer = await zip.generateAsync({ type: 'nodebuffer' }) + return new NextResponse(new Uint8Array(zipBuffer), { + headers: { + 'Content-Type': 'application/zip', + 'Content-Disposition': 'attachment; filename="workspace-files.zip"', + 'Cache-Control': 'no-store', + }, + }) + } catch (error) { + logger.error('Failed to download workspace file selection:', error) + return NextResponse.json( + { error: error instanceof Error ? error.message : 'Failed to download selected files' }, + { status: 500 } + ) + } + } +) diff --git a/apps/sim/app/api/workspaces/[id]/files/folders/[folderId]/route.ts b/apps/sim/app/api/workspaces/[id]/files/folders/[folderId]/route.ts new file mode 100644 index 00000000000..16856df790e --- /dev/null +++ b/apps/sim/app/api/workspaces/[id]/files/folders/[folderId]/route.ts @@ -0,0 +1,86 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { + deleteWorkspaceFileFolderContract, + updateWorkspaceFileFolderContract, +} from '@/lib/api/contracts/workspace-file-folders' +import { parseRequest } from '@/lib/api/server' +import { getSession } from '@/lib/auth' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { + archiveWorkspaceFileFolderRecursive, + updateWorkspaceFileFolder, + WorkspaceFileFolderConflictError, +} from '@/lib/uploads/contexts/workspace' +import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' + +const logger = createLogger('WorkspaceFileFolderAPI') + +async function assertWritePermission(userId: string, workspaceId: string) { + const permission = await getUserEntityPermissions(userId, 'workspace', workspaceId) + return permission === 'admin' || permission === 'write' +} + +export const PATCH = withRouteHandler( + async (request: NextRequest, context: { params: Promise<{ id: string; folderId: string }> }) => { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const parsed = await parseRequest(updateWorkspaceFileFolderContract, request, context) + if (!parsed.success) return parsed.response + const { id: workspaceId, folderId } = parsed.data.params + + if (!(await assertWritePermission(session.user.id, workspaceId))) { + return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }) + } + + try { + const folder = await updateWorkspaceFileFolder({ + workspaceId, + folderId, + ...parsed.data.body, + }) + return NextResponse.json({ success: true, folder }) + } catch (error) { + logger.error('Failed to update workspace file folder:', error) + const message = error instanceof Error ? error.message : 'Failed to update folder' + return NextResponse.json( + { success: false, error: message }, + { status: error instanceof WorkspaceFileFolderConflictError ? 409 : 400 } + ) + } + } +) + +export const DELETE = withRouteHandler( + async (request: NextRequest, context: { params: Promise<{ id: string; folderId: string }> }) => { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const parsed = await parseRequest(deleteWorkspaceFileFolderContract, request, context) + if (!parsed.success) return parsed.response + const { id: workspaceId, folderId } = parsed.data.params + + if (!(await assertWritePermission(session.user.id, workspaceId))) { + return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }) + } + + try { + const deletedItems = await archiveWorkspaceFileFolderRecursive(workspaceId, folderId) + return NextResponse.json({ success: true, deletedItems }) + } catch (error) { + logger.error('Failed to delete workspace file folder:', error) + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Failed to delete folder', + }, + { status: 400 } + ) + } + } +) diff --git a/apps/sim/app/api/workspaces/[id]/files/folders/route.ts b/apps/sim/app/api/workspaces/[id]/files/folders/route.ts new file mode 100644 index 00000000000..2d8999b737a --- /dev/null +++ b/apps/sim/app/api/workspaces/[id]/files/folders/route.ts @@ -0,0 +1,79 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { + createWorkspaceFileFolderContract, + listWorkspaceFileFoldersContract, +} from '@/lib/api/contracts/workspace-file-folders' +import { parseRequest } from '@/lib/api/server' +import { getSession } from '@/lib/auth' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { + createWorkspaceFileFolder, + listWorkspaceFileFolders, + WorkspaceFileFolderConflictError, +} from '@/lib/uploads/contexts/workspace' +import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' + +const logger = createLogger('WorkspaceFileFoldersAPI') + +async function getWorkspacePermission(userId: string, workspaceId: string) { + return getUserEntityPermissions(userId, 'workspace', workspaceId) +} + +export const GET = withRouteHandler( + async (request: NextRequest, context: { params: Promise<{ id: string }> }) => { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const parsed = await parseRequest(listWorkspaceFileFoldersContract, request, context) + if (!parsed.success) return parsed.response + const { id: workspaceId } = parsed.data.params + const { scope } = parsed.data.query + + const permission = await getWorkspacePermission(session.user.id, workspaceId) + if (!permission) { + return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }) + } + + const folders = await listWorkspaceFileFolders(workspaceId, { scope }) + return NextResponse.json({ success: true, folders }) + } +) + +export const POST = withRouteHandler( + async (request: NextRequest, context: { params: Promise<{ id: string }> }) => { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const parsed = await parseRequest(createWorkspaceFileFolderContract, request, context) + if (!parsed.success) return parsed.response + const { id: workspaceId } = parsed.data.params + const { name, parentId } = parsed.data.body + + const permission = await getWorkspacePermission(session.user.id, workspaceId) + if (permission !== 'admin' && permission !== 'write') { + return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }) + } + + try { + const folder = await createWorkspaceFileFolder({ + workspaceId, + userId: session.user.id, + name, + parentId, + }) + return NextResponse.json({ success: true, folder }) + } catch (error) { + logger.error('Failed to create workspace file folder:', error) + const message = error instanceof Error ? error.message : 'Failed to create folder' + return NextResponse.json( + { success: false, error: message }, + { status: error instanceof WorkspaceFileFolderConflictError ? 409 : 400 } + ) + } + } +) diff --git a/apps/sim/app/api/workspaces/[id]/files/move/route.ts b/apps/sim/app/api/workspaces/[id]/files/move/route.ts new file mode 100644 index 00000000000..2422a935d8b --- /dev/null +++ b/apps/sim/app/api/workspaces/[id]/files/move/route.ts @@ -0,0 +1,51 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { moveWorkspaceFileItemsContract } from '@/lib/api/contracts/workspace-file-folders' +import { parseRequest } from '@/lib/api/server' +import { getSession } from '@/lib/auth' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { + moveWorkspaceFileItems, + WorkspaceFileMoveConflictError, +} from '@/lib/uploads/contexts/workspace' +import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' + +const logger = createLogger('WorkspaceFileMoveAPI') + +export const POST = withRouteHandler( + async (request: NextRequest, context: { params: Promise<{ id: string }> }) => { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const parsed = await parseRequest(moveWorkspaceFileItemsContract, request, context) + if (!parsed.success) return parsed.response + const { id: workspaceId } = parsed.data.params + const { fileIds, folderIds, targetFolderId } = parsed.data.body + + const permission = await getUserEntityPermissions(session.user.id, 'workspace', workspaceId) + if (permission !== 'admin' && permission !== 'write') { + return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }) + } + + try { + const moved = await moveWorkspaceFileItems({ + workspaceId, + fileIds, + folderIds, + targetFolderId, + }) + return NextResponse.json({ + success: true, + movedItems: { files: moved.movedFiles, folders: moved.movedFolders }, + }) + } catch (error) { + logger.error('Failed to move workspace file items:', error) + return NextResponse.json( + { success: false, error: error instanceof Error ? error.message : 'Failed to move items' }, + { status: error instanceof WorkspaceFileMoveConflictError ? 409 : 400 } + ) + } + } +) diff --git a/apps/sim/app/api/workspaces/[id]/files/presigned/route.ts b/apps/sim/app/api/workspaces/[id]/files/presigned/route.ts index 332a9386ca7..1227f93c504 100644 --- a/apps/sim/app/api/workspaces/[id]/files/presigned/route.ts +++ b/apps/sim/app/api/workspaces/[id]/files/presigned/route.ts @@ -6,6 +6,7 @@ import { getSession } from '@/lib/auth' import { checkStorageQuota } from '@/lib/billing/storage' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { USE_BLOB_STORAGE } from '@/lib/uploads/config' +import { assertWorkspaceFileFolderTarget } from '@/lib/uploads/contexts/workspace' import { generateWorkspaceFileKey } from '@/lib/uploads/contexts/workspace/workspace-file-manager' import { generatePresignedUploadUrl, hasCloudStorage } from '@/lib/uploads/core/storage-service' import { MAX_WORKSPACE_FILE_SIZE } from '@/lib/uploads/shared/types' @@ -31,7 +32,7 @@ export const POST = withRouteHandler( if (!parsed.success) return parsed.response const { params, body } = parsed.data const workspaceId = params.id - const { fileName, contentType, fileSize } = body + const { fileName, contentType, fileSize, folderId } = body const permission = await getUserEntityPermissions(userId, 'workspace', workspaceId) if (permission !== 'admin' && permission !== 'write') { @@ -46,6 +47,16 @@ export const POST = withRouteHandler( ) } + let targetFolderId: string | null + try { + targetFolderId = await assertWorkspaceFileFolderTarget(workspaceId, folderId) + } catch (error) { + return NextResponse.json( + { error: error instanceof Error ? error.message : 'Invalid target folder' }, + { status: 400 } + ) + } + if (!hasCloudStorage()) { logger.info(`Local storage detected, signaling API fallback for ${fileName}`) return NextResponse.json({ @@ -73,7 +84,7 @@ export const POST = withRouteHandler( userId, customKey: key, expirationSeconds: 3600, - metadata: { workspaceId }, + metadata: { workspaceId, ...(targetFolderId ? { folderId: targetFolderId } : {}) }, }) const finalPath = `/api/files/serve/${USE_BLOB_STORAGE ? 'blob' : 's3'}/${encodeURIComponent(key)}?context=workspace` diff --git a/apps/sim/app/api/workspaces/[id]/files/register/route.ts b/apps/sim/app/api/workspaces/[id]/files/register/route.ts index dfcaa537b5e..0b6d4876ab3 100644 --- a/apps/sim/app/api/workspaces/[id]/files/register/route.ts +++ b/apps/sim/app/api/workspaces/[id]/files/register/route.ts @@ -33,7 +33,7 @@ export const POST = withRouteHandler( if (!parsed.success) return parsed.response const { params, body } = parsed.data const workspaceId = params.id - const { key, name, contentType } = body + const { key, name, contentType, folderId } = body const permission = await getUserEntityPermissions(userId, 'workspace', workspaceId) if (permission !== 'admin' && permission !== 'write') { @@ -56,6 +56,7 @@ export const POST = withRouteHandler( key, originalName: name, contentType, + folderId, }) if (created) { diff --git a/apps/sim/app/api/workspaces/[id]/files/route.ts b/apps/sim/app/api/workspaces/[id]/files/route.ts index d89b12118e8..d8d4c2e691c 100644 --- a/apps/sim/app/api/workspaces/[id]/files/route.ts +++ b/apps/sim/app/api/workspaces/[id]/files/route.ts @@ -123,6 +123,9 @@ export const POST = withRouteHandler( const formData = await request.formData() const rawFile = formData.get('file') + const rawFolderId = formData.get('folderId') + const folderId = + typeof rawFolderId === 'string' && rawFolderId.length > 0 ? rawFolderId : null if (!rawFile || !(rawFile instanceof File)) { return NextResponse.json({ error: 'No file provided' }, { status: 400 }) @@ -146,7 +149,8 @@ export const POST = withRouteHandler( session.user.id, buffer, fileName, - rawFile.type || 'application/octet-stream' + rawFile.type || 'application/octet-stream', + { folderId } ) logger.info(`[${requestId}] Uploaded workspace file: ${fileName}`) diff --git a/apps/sim/app/workspace/[workspaceId]/components/index.ts b/apps/sim/app/workspace/[workspaceId]/components/index.ts index a81ec62c746..66ba3cfdecf 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/components/index.ts @@ -22,6 +22,7 @@ export type { ResourceCell, ResourceColumn, ResourceRow, + RowDragDropConfig, SelectableConfig, } from './resource/resource' export { Resource, ResourceTable } from './resource/resource' diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/resource.tsx b/apps/sim/app/workspace/[workspaceId]/components/resource/resource.tsx index 6e3e6868f96..bd408bba4db 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/resource/resource.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/resource/resource.tsx @@ -1,5 +1,14 @@ 'use client' -import { memo, type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { + type DragEvent, + memo, + type ReactNode, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react' import { ChevronLeft, ChevronRight } from 'lucide-react' import { ArrowDown, ArrowUp, Button, Checkbox, Loader, Plus, Skeleton } from '@/components/emcn' import { cn } from '@/lib/core/utils/cn' @@ -36,6 +45,17 @@ export interface SelectableConfig { disabled?: boolean } +export interface RowDragDropConfig { + activeDropTargetId?: string | null + isRowDraggable?: (rowId: string) => boolean + isRowDropTarget?: (rowId: string) => boolean + onDragStart?: (e: DragEvent, rowId: string) => void + onDragOver?: (e: DragEvent, rowId: string) => void + onDragLeave?: (e: DragEvent, rowId: string) => void + onDrop?: (e: DragEvent, rowId: string) => void + onDragEnd?: (e: DragEvent, rowId: string) => void +} + export interface PaginationConfig { currentPage: number totalPages: number @@ -55,6 +75,7 @@ interface ResourceProps { rows: ResourceRow[] selectedRowId?: string | null selectable?: SelectableConfig + rowDragDrop?: RowDragDropConfig onRowClick?: (rowId: string) => void onRowHover?: (rowId: string) => void onRowContextMenu?: (e: React.MouseEvent, rowId: string) => void @@ -90,6 +111,7 @@ export const Resource = memo(function Resource({ rows, selectedRowId, selectable, + rowDragDrop, onRowClick, onRowHover, onRowContextMenu, @@ -128,6 +150,7 @@ export const Resource = memo(function Resource({ sort={sortOverride} selectedRowId={selectedRowId} selectable={selectable} + rowDragDrop={rowDragDrop} onRowClick={onRowClick} onRowHover={onRowHover} onRowContextMenu={onRowContextMenu} @@ -148,6 +171,7 @@ export interface ResourceTableProps { sort?: SortConfig selectedRowId?: string | null selectable?: SelectableConfig + rowDragDrop?: RowDragDropConfig onRowClick?: (rowId: string) => void onRowHover?: (rowId: string) => void onRowContextMenu?: (e: React.MouseEvent, rowId: string) => void @@ -172,6 +196,7 @@ export const ResourceTable = memo(function ResourceTable({ sort: externalSort, selectedRowId, selectable, + rowDragDrop, onRowClick, onRowHover, onRowContextMenu, @@ -327,6 +352,7 @@ export const ResourceTable = memo(function ResourceTable({ columns={columns} selectedRowId={selectedRowId} selectable={selectable} + rowDragDrop={rowDragDrop} onRowClick={onRowClick} onRowHover={onRowHover} onRowContextMenu={onRowContextMenu} @@ -442,6 +468,7 @@ interface DataRowProps { columns: ResourceColumn[] selectedRowId?: string | null selectable?: SelectableConfig + rowDragDrop?: RowDragDropConfig onRowClick?: (rowId: string) => void onRowHover?: (rowId: string) => void onRowContextMenu?: (e: React.MouseEvent, rowId: string) => void @@ -453,12 +480,16 @@ const DataRow = memo(function DataRow({ columns, selectedRowId, selectable, + rowDragDrop, onRowClick, onRowHover, onRowContextMenu, hasCheckbox, }: DataRowProps) { const isSelected = selectable?.selectedIds.has(row.id) ?? false + const isDraggable = rowDragDrop?.isRowDraggable?.(row.id) ?? false + const isDropTarget = rowDragDrop?.isRowDropTarget?.(row.id) ?? false + const isActiveDropTarget = rowDragDrop?.activeDropTargetId === row.id const handleClick = useCallback(() => { onRowClick?.(row.id) @@ -482,6 +513,41 @@ const DataRow = memo(function DataRow({ [selectable, row.id] ) + const handleDragStart = useCallback( + (e: DragEvent) => { + rowDragDrop?.onDragStart?.(e, row.id) + }, + [rowDragDrop, row.id] + ) + + const handleDragOver = useCallback( + (e: DragEvent) => { + rowDragDrop?.onDragOver?.(e, row.id) + }, + [rowDragDrop, row.id] + ) + + const handleDragLeave = useCallback( + (e: DragEvent) => { + rowDragDrop?.onDragLeave?.(e, row.id) + }, + [rowDragDrop, row.id] + ) + + const handleDrop = useCallback( + (e: DragEvent) => { + rowDragDrop?.onDrop?.(e, row.id) + }, + [rowDragDrop, row.id] + ) + + const handleDragEnd = useCallback( + (e: DragEvent) => { + rowDragDrop?.onDragEnd?.(e, row.id) + }, + [rowDragDrop, row.id] + ) + return ( {hasCheckbox && selectable && ( diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/action-bar/action-bar.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/action-bar/action-bar.tsx new file mode 100644 index 00000000000..3ae0067577f --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/files/components/action-bar/action-bar.tsx @@ -0,0 +1,88 @@ +'use client' + +import { domAnimation, LazyMotion, m } from 'framer-motion' +import { Button, Download, Tooltip, Trash2 } from '@/components/emcn' +import { Folder } from '@/components/emcn/icons' + +interface FilesActionBarProps { + selectedCount: number + onDownload?: () => void + onMove?: () => void + onDelete?: () => void + isLoading?: boolean +} + +export function FilesActionBar({ + selectedCount, + onDownload, + onMove, + onDelete, + isLoading = false, +}: FilesActionBarProps) { + if (selectedCount === 0) return null + + return ( + + +
+ + {selectedCount} selected + +
+ {onDownload && ( + + + + + Download + + )} + {onMove && ( + + + + + Move + + )} + {onDelete && ( + + + + + Delete + + )} +
+
+
+
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/files-list-context-menu/files-list-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/files-list-context-menu/files-list-context-menu.tsx index 031213ead7f..e0eecc9e5aa 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/files-list-context-menu/files-list-context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/files-list-context-menu/files-list-context-menu.tsx @@ -7,15 +7,17 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '@/components/emcn' -import { Plus, Upload } from '@/components/emcn/icons' +import { FolderPlus, Plus, Upload } from '@/components/emcn/icons' interface FilesListContextMenuProps { isOpen: boolean position: { x: number; y: number } onClose: () => void onCreateFile?: () => void + onCreateFolder?: () => void onUploadFile?: () => void disableCreate?: boolean + disableCreateFolder?: boolean disableUpload?: boolean } @@ -24,8 +26,10 @@ export const FilesListContextMenu = memo(function FilesListContextMenu({ position, onClose, onCreateFile, + onCreateFolder, onUploadFile, disableCreate = false, + disableCreateFolder = false, disableUpload = false, }: FilesListContextMenuProps) { return ( @@ -56,6 +60,12 @@ export const FilesListContextMenu = memo(function FilesListContextMenu({ New file )} + {onCreateFolder && ( + + + New folder + + )} {onUploadFile && ( diff --git a/apps/sim/app/workspace/[workspaceId]/files/files.tsx b/apps/sim/app/workspace/[workspaceId]/files/files.tsx index 7e304d786ba..4469a2179f8 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/files.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/files.tsx @@ -1,6 +1,6 @@ 'use client' -import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { type DragEvent, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { createLogger } from '@sim/logger' import { useParams, useRouter, useSearchParams } from 'next/navigation' import { @@ -26,7 +26,7 @@ import { toast, Upload, } from '@/components/emcn' -import { File as FilesIcon } from '@/components/emcn/icons' +import { File as FilesIcon, Folder, FolderPlus } from '@/components/emcn/icons' import { getDocumentIcon } from '@/components/icons/document-icons' import { triggerFileDownload } from '@/lib/uploads/client/download' import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace' @@ -51,6 +51,7 @@ import type { HeaderAction, ResourceColumn, ResourceRow, + RowDragDropConfig, SearchConfig, SortConfig, } from '@/app/workspace/[workspaceId]/components' @@ -61,6 +62,7 @@ import { ResourceHeader, timeCell, } from '@/app/workspace/[workspaceId]/components' +import { FilesActionBar } from '@/app/workspace/[workspaceId]/files/components/action-bar/action-bar' import type { PreviewMode } from '@/app/workspace/[workspaceId]/files/components/file-viewer' import { FileViewer, @@ -71,6 +73,14 @@ import { FilesListContextMenu } from '@/app/workspace/[workspaceId]/files/compon import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' import { useContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks' import { useWorkspaceMembersQuery } from '@/hooks/queries/workspace' +import { + useBulkArchiveWorkspaceFileItems, + useCreateWorkspaceFileFolder, + useMoveWorkspaceFileItems, + useUpdateWorkspaceFileFolder, + useWorkspaceFileFolders, + type WorkspaceFileFolderApi, +} from '@/hooks/queries/workspace-file-folders' import { useDeleteWorkspaceFile, useRenameWorkspaceFile, @@ -82,6 +92,9 @@ import { useInlineRename } from '@/hooks/use-inline-rename' import { usePermissionConfig } from '@/hooks/use-permission-config' type SaveStatus = 'idle' | 'saving' | 'saved' | 'error' +type FileResourceItem = + | { kind: 'file'; id: string; file: WorkspaceFileRecord } + | { kind: 'folder'; id: string; folder: WorkspaceFileFolderApi } const logger = createLogger('Files') @@ -120,6 +133,14 @@ const MIME_TYPE_LABELS: Record = { 'text/markdown': 'Markdown', } +const fileRowId = (id: string) => `file:${id}` +const folderRowId = (id: string) => `folder:${id}` +const parseRowId = (rowId: string): { kind: 'file' | 'folder'; id: string } => { + if (rowId.startsWith('folder:')) return { kind: 'folder', id: rowId.slice('folder:'.length) } + if (rowId.startsWith('file:')) return { kind: 'file', id: rowId.slice('file:'.length) } + return { kind: 'file', id: rowId } +} + function formatFileType(mimeType: string | null, filename: string): string { if (mimeType && MIME_TYPE_LABELS[mimeType]) { return MIME_TYPE_LABELS[mimeType] @@ -143,11 +164,13 @@ export function Files() { const router = useRouter() const searchParams = useSearchParams() const isNewFile = searchParams.get('new') === '1' + const currentFolderId = searchParams.get('folderId') const workspaceId = params?.workspaceId as string const fileIdFromRoute = typeof params?.fileId === 'string' && params.fileId.length > 0 ? params.fileId : null const userPermissions = useUserPermissionsContext() + const canEdit = userPermissions.canEdit === true const { config: permissionConfig } = usePermissionConfig() useEffect(() => { @@ -157,10 +180,15 @@ export function Files() { }, [permissionConfig.hideFilesTab, router, workspaceId]) const { data: files = [], isLoading, error } = useWorkspaceFiles(workspaceId) + const { data: folders = [], isLoading: foldersLoading } = useWorkspaceFileFolders(workspaceId) const { data: members } = useWorkspaceMembersQuery(workspaceId) const uploadFile = useUploadWorkspaceFile() const deleteFile = useDeleteWorkspaceFile() const renameFile = useRenameWorkspaceFile() + const createFolder = useCreateWorkspaceFileFolder() + const updateFolder = useUpdateWorkspaceFileFolder() + const moveItems = useMoveWorkspaceFileItems() + const bulkArchiveItems = useBulkArchiveWorkspaceFileItems() const { isOpen: isContextMenuOpen, @@ -205,6 +233,10 @@ export function Files() { const [creatingFile, setCreatingFile] = useState(false) const [isDirty, setIsDirty] = useState(false) const [saveStatus, setSaveStatus] = useState('idle') + const [selectedRowIds, setSelectedRowIds] = useState>(() => new Set()) + const [activeDropTargetId, setActiveDropTargetId] = useState(null) + const [showMoveModal, setShowMoveModal] = useState(false) + const [moveTargetFolderId, setMoveTargetFolderId] = useState(null) const [previewMode, setPreviewMode] = useState(() => { if (isNewFile) return 'editor' if (fileIdFromRoute) { @@ -216,13 +248,24 @@ export function Files() { }) const [showUnsavedChangesAlert, setShowUnsavedChangesAlert] = useState(false) const [showDeleteConfirm, setShowDeleteConfirm] = useState(false) - const contextMenuFileRef = useRef(null) - const [deleteTargetFile, setDeleteTargetFile] = useState<{ id: string; name: string } | null>( - null - ) + const contextMenuItemRef = useRef(null) + const draggedRowIdsRef = useRef([]) + const [deleteTarget, setDeleteTarget] = useState<{ + fileIds: string[] + folderIds: string[] + name: string + isFolder?: boolean + } | null>(null) const listRename = useInlineRename({ - onSave: (fileId, name) => renameFile.mutate({ workspaceId, fileId, name }), + onSave: (rowId, name) => { + const parsed = parseRowId(rowId) + if (parsed.kind === 'folder') { + updateFolder.mutate({ workspaceId, folderId: parsed.id, updates: { name } }) + return + } + renameFile.mutate({ workspaceId, fileId: parsed.id, name }) + }, }) const headerRename = useInlineRename({ @@ -238,10 +281,28 @@ export function Files() { const selectedFileRef = useRef(selectedFile) selectedFileRef.current = selectedFile + const folderById = useMemo(() => new Map(folders.map((folder) => [folder.id, folder])), [folders]) + const currentFolder = currentFolderId ? (folderById.get(currentFolderId) ?? null) : null + const currentFolderPath = currentFolder?.path ?? null + + const visibleFolders = useMemo(() => { + const siblings = folders.filter((folder) => (folder.parentId ?? null) === currentFolderId) + const searched = debouncedSearchTerm + ? siblings.filter((folder) => + folder.name.toLowerCase().includes(debouncedSearchTerm.toLowerCase()) + ) + : siblings + return [...searched].sort((a, b) => a.name.localeCompare(b.name)) + }, [folders, currentFolderId, debouncedSearchTerm]) + const filteredFiles = useMemo(() => { let result = debouncedSearchTerm - ? files.filter((f) => f.name.toLowerCase().includes(debouncedSearchTerm.toLowerCase())) - : files + ? files.filter( + (f) => + (f.folderId ?? null) === currentFolderId && + f.name.toLowerCase().includes(debouncedSearchTerm.toLowerCase()) + ) + : files.filter((f) => (f.folderId ?? null) === currentFolderId) if (typeFilter.length > 0) { result = result.filter((f) => { @@ -296,28 +357,48 @@ export function Files() { } return dir === 'asc' ? cmp : -cmp }) - }, [files, debouncedSearchTerm, typeFilter, sizeFilter, uploadedByFilter, activeSort, members]) - - const rowCacheRef = useRef( - new Map() - ) + }, [ + files, + currentFolderId, + debouncedSearchTerm, + typeFilter, + sizeFilter, + uploadedByFilter, + activeSort, + members, + ]) const baseRows: ResourceRow[] = useMemo(() => { - const prevCache = rowCacheRef.current - const nextCache = new Map< - string, - { row: ResourceRow; file: WorkspaceFileRecord; members: typeof members } - >() - - const result = filteredFiles.map((file) => { - const cached = prevCache.get(file.id) - if (cached && cached.file === file && cached.members === members) { - nextCache.set(file.id, cached) - return cached.row - } + const folderRows = visibleFolders.map((folder) => ({ + id: folderRowId(folder.id), + cells: { + name: { + icon: , + label: folder.name, + }, + size: { label: 'Folder' }, + type: { + icon: , + label: 'Folder', + }, + created: timeCell(folder.createdAt), + owner: ownerCell(folder.userId, members), + updated: timeCell(folder.updatedAt), + }, + sortValues: { + name: folder.name, + size: -1, + type: 'Folder', + created: new Date(folder.createdAt).getTime(), + updated: new Date(folder.updatedAt).getTime(), + owner: members?.find((m) => m.userId === folder.userId)?.name ?? '', + }, + })) + + const fileRows = filteredFiles.map((file) => { const Icon = getDocumentIcon(file.type || '', file.name) const row: ResourceRow = { - id: file.id, + id: fileRowId(file.id), cells: { name: { icon: , @@ -335,21 +416,21 @@ export function Files() { updated: timeCell(file.updatedAt), }, } - nextCache.set(file.id, { row, file, members }) return row }) - rowCacheRef.current = nextCache - return result - }, [filteredFiles, members]) + return [...folderRows, ...fileRows] + }, [visibleFolders, filteredFiles, members]) const rows: ResourceRow[] = useMemo(() => { if (!listRename.editingId) return baseRows return baseRows.map((row) => { if (row.id !== listRename.editingId) return row - const file = filteredFiles.find((f) => f.id === row.id) - if (!file) return row - const Icon = getDocumentIcon(file.type || '', file.name) + const parsed = parseRowId(row.id) + const file = parsed.kind === 'file' ? filteredFiles.find((f) => f.id === parsed.id) : null + const folder = + parsed.kind === 'folder' ? visibleFolders.find((item) => item.id === parsed.id) : null + const Icon = file ? getDocumentIcon(file.type || '', file.name) : Folder return { ...row, cells: { @@ -381,9 +462,233 @@ export function Files() { listRename.submitRename, listRename.cancelRename, filteredFiles, + visibleFolders, ]) - const uploadFiles = async (filesToUpload: File[]) => { + const visibleRowIds = useMemo(() => rows.map((row) => row.id), [rows]) + const isAllSelected = + visibleRowIds.length > 0 && visibleRowIds.every((id) => selectedRowIds.has(id)) + const selectedFileIds = useMemo( + () => + Array.from(selectedRowIds) + .map(parseRowId) + .filter((item) => item.kind === 'file') + .map((item) => item.id), + [selectedRowIds] + ) + const selectedFolderIds = useMemo( + () => + Array.from(selectedRowIds) + .map(parseRowId) + .filter((item) => item.kind === 'folder') + .map((item) => item.id), + [selectedRowIds] + ) + + const selectableConfig = useMemo( + () => ({ + selectedIds: selectedRowIds, + isAllSelected, + onSelectRow: (rowId: string, checked: boolean) => { + setSelectedRowIds((prev) => { + const next = new Set(prev) + if (checked) next.add(rowId) + else next.delete(rowId) + return next + }) + }, + onSelectAll: (checked: boolean) => { + setSelectedRowIds((prev) => { + const next = new Set(prev) + for (const rowId of visibleRowIds) { + if (checked) next.add(rowId) + else next.delete(rowId) + } + return next + }) + }, + disabled: false, + }), + [selectedRowIds, isAllSelected, visibleRowIds] + ) + + useEffect(() => { + setSelectedRowIds((prev) => { + const visible = new Set(visibleRowIds) + const next = new Set(Array.from(prev).filter((id) => visible.has(id))) + return next.size === prev.size ? prev : next + }) + }, [visibleRowIds]) + + const descendantFolderIdsByFolderId = useMemo(() => { + const childrenByParent = new Map() + for (const folder of folders) { + if (!folder.parentId) continue + const children = childrenByParent.get(folder.parentId) ?? [] + children.push(folder.id) + childrenByParent.set(folder.parentId, children) + } + + const result = new Map>() + const collect = (folderId: string): Set => { + const cached = result.get(folderId) + if (cached) return cached + + const descendants = new Set() + for (const childId of childrenByParent.get(folderId) ?? []) { + descendants.add(childId) + for (const nestedId of collect(childId)) { + descendants.add(nestedId) + } + } + result.set(folderId, descendants) + return descendants + } + + for (const folder of folders) { + collect(folder.id) + } + return result + }, [folders]) + + const isInvalidDropTarget = useCallback( + (targetRowId: string, sourceRowIds: string[]) => { + const target = parseRowId(targetRowId) + if (target.kind !== 'folder') return true + + for (const sourceRowId of sourceRowIds) { + const source = parseRowId(sourceRowId) + if (source.kind !== 'folder') continue + if (source.id === target.id) return true + if (descendantFolderIdsByFolderId.get(source.id)?.has(target.id)) return true + } + + return false + }, + [descendantFolderIdsByFolderId] + ) + + const rowDragDropConfig = useMemo( + () => ({ + activeDropTargetId, + isRowDraggable: (rowId) => canEdit && listRename.editingId !== rowId, + isRowDropTarget: (rowId) => canEdit && parseRowId(rowId).kind === 'folder', + onDragStart: (e: DragEvent, rowId) => { + if (!canEdit || listRename.editingId === rowId) { + e.preventDefault() + return + } + + const sourceRowIds = selectedRowIds.has(rowId) + ? visibleRowIds.filter((visibleRowId) => selectedRowIds.has(visibleRowId)) + : [rowId] + + draggedRowIdsRef.current = sourceRowIds + if (!selectedRowIds.has(rowId)) { + setSelectedRowIds(new Set([rowId])) + } + + e.dataTransfer.effectAllowed = 'move' + e.dataTransfer.setData( + 'application/x-sim-workspace-file-rows', + JSON.stringify(sourceRowIds) + ) + e.dataTransfer.setData('text/plain', sourceRowIds.join(',')) + }, + onDragOver: (e: DragEvent, rowId) => { + const sourceRowIds = draggedRowIdsRef.current + const hasExternalFiles = e.dataTransfer.types.includes('Files') + if (!hasExternalFiles && isInvalidDropTarget(rowId, sourceRowIds)) return + + e.preventDefault() + e.stopPropagation() + e.dataTransfer.dropEffect = hasExternalFiles ? 'copy' : 'move' + setActiveDropTargetId(rowId) + }, + onDragLeave: (e: DragEvent, rowId) => { + const relatedTarget = e.relatedTarget + if (relatedTarget instanceof Node && e.currentTarget.contains(relatedTarget)) return + setActiveDropTargetId((current) => (current === rowId ? null : current)) + }, + onDrop: (e: DragEvent, rowId) => { + e.preventDefault() + e.stopPropagation() + dragCounterRef.current = 0 + setIsDraggingOver(false) + setActiveDropTargetId(null) + const target = parseRowId(rowId) + if (target.kind !== 'folder') return + + const droppedFiles = Array.from(e.dataTransfer.files ?? []) + if (droppedFiles.length > 0) { + void uploadFiles(droppedFiles, target.id) + return + } + + let sourceRowIds = draggedRowIdsRef.current + const rawSource = e.dataTransfer.getData('application/x-sim-workspace-file-rows') + if (rawSource) { + try { + const parsedSource = JSON.parse(rawSource) + if (Array.isArray(parsedSource)) { + sourceRowIds = parsedSource.filter( + (source): source is string => typeof source === 'string' && source.length > 0 + ) + } + } catch { + sourceRowIds = draggedRowIdsRef.current + } + } + + if (isInvalidDropTarget(rowId, sourceRowIds)) return + + const fileIds = sourceRowIds + .map(parseRowId) + .filter((source) => source.kind === 'file') + .map((source) => source.id) + const folderIds = sourceRowIds + .map(parseRowId) + .filter((source) => source.kind === 'folder') + .map((source) => source.id) + + if (fileIds.length === 0 && folderIds.length === 0) return + + void moveItems + .mutateAsync({ + workspaceId, + fileIds, + folderIds, + targetFolderId: target.id, + }) + .then(() => { + setSelectedRowIds(new Set()) + }) + .catch((error) => { + logger.error('Failed to move items via drag and drop:', error) + toast.error(error instanceof Error ? error.message : 'Failed to move selected items') + }) + }, + onDragEnd: () => { + dragCounterRef.current = 0 + draggedRowIdsRef.current = [] + setIsDraggingOver(false) + setActiveDropTargetId(null) + }, + }), + [ + activeDropTargetId, + canEdit, + listRename.editingId, + selectedRowIds, + visibleRowIds, + isInvalidDropTarget, + uploadFiles, + moveItems, + workspaceId, + ] + ) + + async function uploadFiles(filesToUpload: File[], targetFolderId = currentFolderId) { if (!workspaceId || filesToUpload.length === 0) return const oversized: string[] = [] @@ -425,6 +730,7 @@ export function Files() { await uploadFile.mutateAsync({ workspaceId, file: allowedFiles[i], + folderId: targetFolderId, onProgress: ({ percent }) => { setUploadProgress((prev) => ({ ...prev, currentPercent: percent })) }, @@ -485,23 +791,32 @@ export function Files() { } }, []) - const deleteTargetFileRef = useRef(deleteTargetFile) - deleteTargetFileRef.current = deleteTargetFile + const deleteTargetRef = useRef(deleteTarget) + deleteTargetRef.current = deleteTarget const fileIdFromRouteRef = useRef(fileIdFromRoute) fileIdFromRouteRef.current = fileIdFromRoute const handleDelete = useCallback(async () => { - const target = deleteTargetFileRef.current + const target = deleteTargetRef.current if (!target) return try { - await deleteFile.mutateAsync({ - workspaceId, - fileId: target.id, - }) + if (target.folderIds.length > 0 || target.fileIds.length > 1) { + await bulkArchiveItems.mutateAsync({ + workspaceId, + fileIds: target.fileIds, + folderIds: target.folderIds, + }) + } else { + await deleteFile.mutateAsync({ + workspaceId, + fileId: target.fileIds[0], + }) + } setShowDeleteConfirm(false) - setDeleteTargetFile(null) - if (fileIdFromRouteRef.current === target.id) { + setDeleteTarget(null) + setSelectedRowIds(new Set()) + if (target.fileIds.includes(fileIdFromRouteRef.current ?? '')) { setIsDirty(false) setSaveStatus('idle') router.push(`/workspace/${workspaceId}/files`) @@ -509,7 +824,7 @@ export function Files() { } catch (err) { logger.error('Failed to delete file:', err) } - }, [workspaceId, router]) + }, [workspaceId, router, bulkArchiveItems, deleteFile]) const isDirtyRef = useRef(isDirty) isDirtyRef.current = isDirty @@ -522,13 +837,16 @@ export function Files() { }, []) const handleBackAttempt = useCallback(() => { + const backUrl = currentFolderId + ? `/workspace/${workspaceId}/files?folderId=${currentFolderId}` + : `/workspace/${workspaceId}/files` if (isDirtyRef.current) { setShowUnsavedChangesAlert(true) } else { setPreviewMode('editor') - router.push(`/workspace/${workspaceId}/files`) + router.push(backUrl) } - }, [router, workspaceId]) + }, [router, workspaceId, currentFolderId]) const handleStartHeaderRename = useCallback(() => { const file = selectedFileRef.current @@ -543,11 +861,59 @@ export function Files() { const handleDeleteSelected = useCallback(() => { const file = selectedFileRef.current if (file) { - setDeleteTargetFile({ id: file.id, name: file.name }) + setDeleteTarget({ fileIds: [file.id], folderIds: [], name: file.name }) setShowDeleteConfirm(true) } }, []) + const handleBulkDelete = useCallback(() => { + if (selectedFileIds.length === 0 && selectedFolderIds.length === 0) return + setDeleteTarget({ + fileIds: selectedFileIds, + folderIds: selectedFolderIds, + name: + selectedFileIds.length + selectedFolderIds.length === 1 + ? (files.find((file) => file.id === selectedFileIds[0])?.name ?? + folders.find((folder) => folder.id === selectedFolderIds[0])?.name ?? + 'selected item') + : `${selectedFileIds.length + selectedFolderIds.length} selected items`, + isFolder: selectedFolderIds.length > 0, + }) + setShowDeleteConfirm(true) + }, [selectedFileIds, selectedFolderIds, files, folders]) + + const handleBulkDownload = useCallback(() => { + const selectedFiles = files.filter((file) => selectedFileIds.includes(file.id)) + if (selectedFiles.length === 1 && selectedFolderIds.length === 0) { + handleDownload(selectedFiles[0]) + return + } + + const query = new URLSearchParams() + for (const fileId of selectedFileIds) query.append('fileIds', fileId) + for (const folderId of selectedFolderIds) query.append('folderIds', folderId) + + if (query.size === 0) return + window.location.href = `/api/workspaces/${workspaceId}/files/download?${query.toString()}` + }, [selectedFileIds, selectedFolderIds, files, handleDownload, workspaceId]) + + const handleOpenMoveModal = useCallback(() => { + if (selectedFileIds.length === 0 && selectedFolderIds.length === 0) return + setMoveTargetFolderId(currentFolderId) + setShowMoveModal(true) + }, [selectedFileIds, selectedFolderIds, currentFolderId]) + + const handleMoveSelected = useCallback(async () => { + await moveItems.mutateAsync({ + workspaceId, + fileIds: selectedFileIds, + folderIds: selectedFolderIds, + targetFolderId: moveTargetFolderId, + }) + setSelectedRowIds(new Set()) + setShowMoveModal(false) + }, [workspaceId, selectedFileIds, selectedFolderIds, moveTargetFolderId, moveItems]) + const fileDetailBreadcrumbs = useMemo( () => selectedFile @@ -600,7 +966,12 @@ export function Files() { setIsDirty(false) setSaveStatus('idle') setPreviewMode('editor') - router.push(`/workspace/${workspaceId}/files`) + const folderId = selectedFileRef.current?.folderId + router.push( + folderId + ? `/workspace/${workspaceId}/files?folderId=${folderId}` + : `/workspace/${workspaceId}/files` + ) } const creatingFileRef = useRef(creatingFile) @@ -611,7 +982,9 @@ export function Files() { setCreatingFile(true) try { - const existingNames = new Set(filesRef.current.map((f) => f.name)) + const existingNames = new Set( + filesRef.current.filter((f) => (f.folderId ?? null) === currentFolderId).map((f) => f.name) + ) let name = 'untitled.md' let counter = 1 while (existingNames.has(name)) { @@ -622,54 +995,107 @@ export function Files() { const mimeType = getMimeTypeFromExtension('md') const blob = new Blob([''], { type: mimeType }) const file = new File([blob], name, { type: mimeType }) - const result = await uploadFile.mutateAsync({ workspaceId, file, skipToast: true }) + const result = await uploadFile.mutateAsync({ + workspaceId, + file, + folderId: currentFolderId, + skipToast: true, + }) const fileId = result.file?.id if (fileId) { justCreatedFileIdRef.current = fileId - router.push(`/workspace/${workspaceId}/files/${fileId}?new=1`) + const params = new URLSearchParams({ new: '1' }) + if (currentFolderId) params.set('folderId', currentFolderId) + router.push(`/workspace/${workspaceId}/files/${fileId}?${params.toString()}`) } } catch (err) { logger.error('Failed to create file:', err) } finally { setCreatingFile(false) } - }, [workspaceId, router]) + }, [workspaceId, router, currentFolderId]) + + const handleCreateFolder = useCallback(async () => { + if (!workspaceId || createFolder.isPending) return + const existingNames = new Set( + folders + .filter((folder) => (folder.parentId ?? null) === currentFolderId) + .map((folder) => folder.name) + ) + let name = 'New folder' + let counter = 1 + while (existingNames.has(name)) { + name = `New folder (${counter})` + counter++ + } + + const folder = await createFolder.mutateAsync({ + workspaceId, + name, + parentId: currentFolderId, + }) + listRename.startRename(folderRowId(folder.id), folder.name) + }, [workspaceId, createFolder, folders, currentFolderId, listRename.startRename]) const handleRowContextMenu = useCallback( (e: React.MouseEvent, rowId: string) => { - const file = filesRef.current.find((f) => f.id === rowId) - if (file) { - contextMenuFileRef.current = file - openContextMenu(e) + const parsed = parseRowId(rowId) + const item = + parsed.kind === 'folder' + ? folders.find((folder) => folder.id === parsed.id) + : filesRef.current.find((file) => file.id === parsed.id) + if (!item) return + contextMenuItemRef.current = + parsed.kind === 'folder' + ? { kind: 'folder', id: parsed.id, folder: item as WorkspaceFileFolderApi } + : { kind: 'file', id: parsed.id, file: item as WorkspaceFileRecord } + if (!selectedRowIds.has(rowId)) { + setSelectedRowIds(new Set([rowId])) } + openContextMenu(e) }, - [openContextMenu] + [folders, openContextMenu, selectedRowIds] ) const handleContextMenuOpen = useCallback(() => { - const file = contextMenuFileRef.current - if (!file) return - router.push(`/workspace/${workspaceId}/files/${file.id}`) + const item = contextMenuItemRef.current + if (!item) return + if (item.kind === 'folder') { + router.push(`/workspace/${workspaceId}/files?folderId=${item.folder.id}`) + closeContextMenu() + return + } + router.push( + item.file.folderId + ? `/workspace/${workspaceId}/files/${item.file.id}?folderId=${item.file.folderId}` + : `/workspace/${workspaceId}/files/${item.file.id}` + ) closeContextMenu() }, [closeContextMenu, router, workspaceId]) const handleContextMenuDownload = useCallback(() => { - const file = contextMenuFileRef.current - if (!file) return - handleDownload(file) + const item = contextMenuItemRef.current + if (!item || item.kind !== 'file') return + handleDownload(item.file) closeContextMenu() }, [handleDownload, closeContextMenu]) const handleContextMenuRename = useCallback(() => { - const file = contextMenuFileRef.current - if (file) listRename.startRename(file.id, file.name) + const item = contextMenuItemRef.current + if (item?.kind === 'file') listRename.startRename(fileRowId(item.file.id), item.file.name) + if (item?.kind === 'folder') + listRename.startRename(folderRowId(item.folder.id), item.folder.name) closeContextMenu() }, [listRename.startRename, closeContextMenu]) const handleContextMenuDelete = useCallback(() => { - const file = contextMenuFileRef.current - if (!file) return - setDeleteTargetFile({ id: file.id, name: file.name }) + const item = contextMenuItemRef.current + if (!item) return + setDeleteTarget( + item.kind === 'file' + ? { fileIds: [item.file.id], folderIds: [], name: item.file.name } + : { fileIds: [], folderIds: [item.folder.id], name: item.folder.name, isFolder: true } + ) setShowDeleteConfirm(true) closeContextMenu() }, [closeContextMenu]) @@ -716,9 +1142,13 @@ export function Files() { useEffect(() => { if (isNewFile && fileIdFromRoute) { - router.replace(`/workspace/${workspaceId}/files/${fileIdFromRoute}`) + router.replace( + currentFolderId + ? `/workspace/${workspaceId}/files/${fileIdFromRoute}?folderId=${currentFolderId}` + : `/workspace/${workspaceId}/files/${fileIdFromRoute}` + ) } - }, [isNewFile, fileIdFromRoute, router, workspaceId]) + }, [isNewFile, fileIdFromRoute, router, workspaceId, currentFolderId]) useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { @@ -831,20 +1261,27 @@ export function Files() { headerRenameRef.current = headerRename const handleRowClick = useCallback( - (id: string) => { - if (listRenameRef.current.editingId !== id && !headerRenameRef.current.editingId) { - router.push(`/workspace/${workspaceId}/files/${id}`) + (rowId: string) => { + if (listRenameRef.current.editingId !== rowId && !headerRenameRef.current.editingId) { + const parsed = parseRowId(rowId) + if (parsed.kind === 'folder') { + router.push(`/workspace/${workspaceId}/files?folderId=${parsed.id}`) + return + } + router.push( + currentFolderId + ? `/workspace/${workspaceId}/files/${parsed.id}?folderId=${currentFolderId}` + : `/workspace/${workspaceId}/files/${parsed.id}` + ) } }, - [router, workspaceId] + [router, workspaceId, currentFolderId] ) const handleUploadClick = useCallback(() => { fileInputRef.current?.click() }, []) - const canEdit = userPermissions.canEdit === true - const searchConfig: SearchConfig = { value: inputValue, onChange: setInputValue, @@ -874,8 +1311,14 @@ export function Files() { icon: Upload, onClick: handleUploadClick, }, + { + label: 'New folder', + icon: FolderPlus, + onClick: handleCreateFolder, + disabled: createFolder.isPending || !canEdit, + }, ], - [uploadButtonLabel, handleUploadClick] + [uploadButtonLabel, handleUploadClick, handleCreateFolder, createFolder.isPending, canEdit] ) const handleNavigateToFiles = () => { @@ -884,6 +1327,26 @@ export function Files() { const loadingBreadcrumbs = [{ label: 'Files', onClick: handleNavigateToFiles }, { label: '...' }] + const listBreadcrumbs = useMemo(() => { + const breadcrumbs = [{ label: 'Files', onClick: handleNavigateToFiles }] + if (!currentFolderPath) return breadcrumbs + + const segments = currentFolderPath.split('/') + let parentId: string | null = null + for (const segment of segments) { + const folder = folders.find( + (item) => item.name === segment && (item.parentId ?? null) === parentId + ) + if (!folder) continue + breadcrumbs.push({ + label: folder.name, + onClick: () => router.push(`/workspace/${workspaceId}/files?folderId=${folder.id}`), + }) + parentId = folder.id + } + return breadcrumbs + }, [currentFolderPath, folders, router, workspaceId]) + const memberOptions: ComboboxOption[] = useMemo( () => (members ?? []).map((m) => ({ @@ -905,6 +1368,20 @@ export function Files() { [members] ) + const moveFolderOptions: ComboboxOption[] = useMemo( + () => [ + { value: '__root__', label: 'Files' }, + ...folders + .filter((folder) => !selectedFolderIds.includes(folder.id)) + .map((folder) => ({ + value: folder.id, + label: folder.path, + icon: Folder, + })), + ], + [folders, selectedFolderIds] + ) + const sortConfig: SortConfig = useMemo( () => ({ options: [ @@ -1132,9 +1609,10 @@ export function Files() { ) @@ -1151,6 +1629,7 @@ export function Files() { - -
-

Drop to upload

-

- Release files here to add them to this workspace -

+ <> + + {isDraggingOver ? ( +
+ +
+

Drop to upload

+

+ Release files here to add them to this workspace +

+
-
- ) : null + ) : null} + } /> @@ -1183,8 +1673,10 @@ export function Files() { position={listContextMenuPosition} onClose={closeListContextMenu} onCreateFile={handleCreateFile} + onCreateFolder={handleCreateFolder} onUploadFile={handleListUploadFile} disableCreate={uploading || creatingFile || !canEdit} + disableCreateFolder={createFolder.isPending || !canEdit} disableUpload={uploading || !canEdit} /> @@ -1202,11 +1694,41 @@ export function Files() { + + + Move Items + +
+ + Choose the folder to move {selectedRowIds.size} selected item + {selectedRowIds.size === 1 ? '' : 's'} into. + + setMoveTargetFolderId(value === '__root__' ? null : value)} + placeholder='Select destination' + filterOptions + /> +
+
+ + + + +
+
+ void onOpen: () => void - onDownload: () => void + onDownload?: () => void onRename: () => void onDelete: () => void canEdit: boolean @@ -1267,10 +1789,12 @@ const FileRowContextMenu = memo(function FileRowContextMenu({ Open
- - - Download - + {onDownload && ( + + + Download + + )} {canEdit && ( <> @@ -1293,6 +1817,7 @@ interface DeleteConfirmModalProps { open: boolean onOpenChange: (open: boolean) => void fileName?: string + isFolder?: boolean onDelete: () => void isPending: boolean } @@ -1301,18 +1826,21 @@ const DeleteConfirmModal = memo(function DeleteConfirmModal({ open, onOpenChange, fileName, + isFolder = false, onDelete, isPending, }: DeleteConfirmModalProps) { return ( - Delete File + {isFolder ? 'Delete Folder' : 'Delete File'}

Are you sure you want to delete{' '} - {fileName}? You can - restore it from Recently Deleted in Settings. + {fileName}?{' '} + {isFolder + ? 'This will also delete files and folders inside it.' + : 'You can restore it from Recently Deleted in Settings.'}

diff --git a/apps/sim/hooks/queries/workspace-file-folders.ts b/apps/sim/hooks/queries/workspace-file-folders.ts new file mode 100644 index 00000000000..a26ce18cc44 --- /dev/null +++ b/apps/sim/hooks/queries/workspace-file-folders.ts @@ -0,0 +1,154 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { requestJson } from '@/lib/api/client/request' +import { + bulkArchiveWorkspaceFileItemsContract, + createWorkspaceFileFolderContract, + deleteWorkspaceFileFolderContract, + listWorkspaceFileFoldersContract, + moveWorkspaceFileItemsContract, + updateWorkspaceFileFolderContract, + type WorkspaceFileFolderApi, +} from '@/lib/api/contracts/workspace-file-folders' +import { workspaceFilesKeys } from '@/hooks/queries/workspace-files' + +type WorkspaceFileFolderScope = 'active' | 'archived' | 'all' +export type { WorkspaceFileFolderApi } + +export const workspaceFileFolderKeys = { + all: ['workspaceFileFolders'] as const, + lists: () => [...workspaceFileFolderKeys.all, 'list'] as const, + list: (workspaceId: string, scope: WorkspaceFileFolderScope = 'active') => + [...workspaceFileFolderKeys.lists(), workspaceId, scope] as const, +} + +async function fetchWorkspaceFileFolders( + workspaceId: string, + scope: WorkspaceFileFolderScope, + signal?: AbortSignal +): Promise { + const data = await requestJson(listWorkspaceFileFoldersContract, { + params: { id: workspaceId }, + query: { scope }, + signal, + }) + return data.folders +} + +function invalidateWorkspaceFileBrowsers( + queryClient: ReturnType, + workspaceId: string +) { + queryClient.invalidateQueries({ queryKey: workspaceFileFolderKeys.lists() }) + queryClient.invalidateQueries({ queryKey: workspaceFilesKeys.lists() }) + queryClient.invalidateQueries({ queryKey: workspaceFilesKeys.storageInfo() }) + queryClient.invalidateQueries({ queryKey: workspaceFileFolderKeys.list(workspaceId) }) +} + +export function useWorkspaceFileFolders( + workspaceId: string, + scope: WorkspaceFileFolderScope = 'active' +) { + return useQuery({ + queryKey: workspaceFileFolderKeys.list(workspaceId, scope), + queryFn: ({ signal }) => fetchWorkspaceFileFolders(workspaceId, scope, signal), + enabled: Boolean(workspaceId), + staleTime: 30 * 1000, + }) +} + +export function useCreateWorkspaceFileFolder() { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: async (variables: { + workspaceId: string + name: string + parentId?: string | null + }) => { + const data = await requestJson(createWorkspaceFileFolderContract, { + params: { id: variables.workspaceId }, + body: { name: variables.name, parentId: variables.parentId }, + }) + return data.folder + }, + onSettled: (_data, _error, variables) => { + invalidateWorkspaceFileBrowsers(queryClient, variables.workspaceId) + }, + }) +} + +export function useUpdateWorkspaceFileFolder() { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: async (variables: { + workspaceId: string + folderId: string + updates: { name?: string; parentId?: string | null; sortOrder?: number } + }) => { + const data = await requestJson(updateWorkspaceFileFolderContract, { + params: { id: variables.workspaceId, folderId: variables.folderId }, + body: variables.updates, + }) + return data.folder + }, + onSettled: (_data, _error, variables) => { + invalidateWorkspaceFileBrowsers(queryClient, variables.workspaceId) + }, + }) +} + +export function useDeleteWorkspaceFileFolder() { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: async (variables: { workspaceId: string; folderId: string }) => { + return requestJson(deleteWorkspaceFileFolderContract, { + params: { id: variables.workspaceId, folderId: variables.folderId }, + }) + }, + onSettled: (_data, _error, variables) => { + invalidateWorkspaceFileBrowsers(queryClient, variables.workspaceId) + }, + }) +} + +export function useMoveWorkspaceFileItems() { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: async (variables: { + workspaceId: string + fileIds: string[] + folderIds: string[] + targetFolderId?: string | null + }) => { + return requestJson(moveWorkspaceFileItemsContract, { + params: { id: variables.workspaceId }, + body: { + fileIds: variables.fileIds, + folderIds: variables.folderIds, + targetFolderId: variables.targetFolderId, + }, + }) + }, + onSettled: (_data, _error, variables) => { + invalidateWorkspaceFileBrowsers(queryClient, variables.workspaceId) + }, + }) +} + +export function useBulkArchiveWorkspaceFileItems() { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: async (variables: { + workspaceId: string + fileIds: string[] + folderIds: string[] + }) => { + return requestJson(bulkArchiveWorkspaceFileItemsContract, { + params: { id: variables.workspaceId }, + body: { fileIds: variables.fileIds, folderIds: variables.folderIds }, + }) + }, + onSettled: (_data, _error, variables) => { + invalidateWorkspaceFileBrowsers(queryClient, variables.workspaceId) + }, + }) +} diff --git a/apps/sim/hooks/queries/workspace-files.ts b/apps/sim/hooks/queries/workspace-files.ts index 03c0bd69d17..480f867a3cf 100644 --- a/apps/sim/hooks/queries/workspace-files.ts +++ b/apps/sim/hooks/queries/workspace-files.ts @@ -203,6 +203,7 @@ export function useStorageInfo(enabled = true) { interface UploadFileParams { workspaceId: string file: File + folderId?: string | null onProgress?: (event: UploadProgressEvent) => void signal?: AbortSignal skipToast?: boolean @@ -217,10 +218,12 @@ interface UploadFileResponse { async function uploadViaApiFallback( workspaceId: string, file: File, + folderId?: string | null, signal?: AbortSignal ): Promise { const formData = new FormData() formData.append('file', file) + if (folderId) formData.append('folderId', folderId) // boundary-raw-fetch: multipart/form-data fallback upload, requestJson only supports JSON bodies const response = await fetch(`/api/workspaces/${workspaceId}/files`, { @@ -250,6 +253,7 @@ async function parseUploadResponse( async function uploadWorkspaceFile( workspaceId: string, file: File, + folderId?: string | null, onProgress?: (event: UploadProgressEvent) => void, signal?: AbortSignal ): Promise { @@ -258,6 +262,7 @@ async function uploadWorkspaceFile( result = await runUploadStrategy({ file, presignedEndpoint: `/api/workspaces/${workspaceId}/files/presigned`, + presignedBody: { folderId }, workspaceId, context: 'workspace', onProgress, @@ -265,12 +270,12 @@ async function uploadWorkspaceFile( }) } catch (error) { if (error instanceof DirectUploadError && error.code === 'FALLBACK_REQUIRED') { - return uploadViaApiFallback(workspaceId, file, signal) + return uploadViaApiFallback(workspaceId, file, folderId, signal) } throw error } - const data = await registerWithRetry(workspaceId, result, signal) + const data = await registerWithRetry(workspaceId, result, folderId, signal) if (!data.success || !data.file) { throw new Error(data.error || 'Failed to register file') @@ -289,6 +294,7 @@ const REGISTER_RETRY_DELAY_MS = 500 async function registerWithRetry( workspaceId: string, result: { key: string; name: string; contentType: string }, + folderId?: string | null, signal?: AbortSignal ) { let lastError: unknown @@ -300,6 +306,7 @@ async function registerWithRetry( key: result.key, name: result.name, contentType: result.contentType, + folderId, }, signal, }) @@ -319,8 +326,8 @@ export function useUploadWorkspaceFile() { const queryClient = useQueryClient() return useMutation({ - mutationFn: ({ workspaceId, file, onProgress, signal }: UploadFileParams) => - uploadWorkspaceFile(workspaceId, file, onProgress, signal), + mutationFn: ({ workspaceId, file, folderId, onProgress, signal }: UploadFileParams) => + uploadWorkspaceFile(workspaceId, file, folderId, onProgress, signal), onSettled: (_data, _error, variables) => { if (variables.skipInvalidation) return queryClient.invalidateQueries({ queryKey: workspaceFilesKeys.lists() }) diff --git a/apps/sim/lib/api/contracts/index.ts b/apps/sim/lib/api/contracts/index.ts index 4c9858af67c..9e27ab47723 100644 --- a/apps/sim/lib/api/contracts/index.ts +++ b/apps/sim/lib/api/contracts/index.ts @@ -32,5 +32,6 @@ export * from './types' export * from './user' export * from './v1' export * from './workflows' +export * from './workspace-file-folders' export * from './workspace-files' export * from './workspaces' diff --git a/apps/sim/lib/api/contracts/workspace-file-folders.ts b/apps/sim/lib/api/contracts/workspace-file-folders.ts new file mode 100644 index 00000000000..b2c685bcfdb --- /dev/null +++ b/apps/sim/lib/api/contracts/workspace-file-folders.ts @@ -0,0 +1,170 @@ +import { z } from 'zod' +import { defineRouteContract } from '@/lib/api/contracts/types' + +export const workspaceFileFolderScopeSchema = z.enum(['active', 'archived', 'all']) + +export const workspaceFileFoldersParamsSchema = z.object({ + id: z.string({ error: 'Workspace ID is required' }).min(1, 'Workspace ID is required'), +}) + +export const workspaceFileFolderParamsSchema = workspaceFileFoldersParamsSchema.extend({ + folderId: z.string({ error: 'Folder ID is required' }).min(1, 'Folder ID is required'), +}) + +export const listWorkspaceFileFoldersQuerySchema = z.object({ + scope: workspaceFileFolderScopeSchema.default('active'), +}) + +export const workspaceFileFolderSchema = z.object({ + id: z.string(), + workspaceId: z.string(), + userId: z.string(), + name: z.string(), + parentId: z.string().nullable(), + path: z.string(), + sortOrder: z.number(), + deletedAt: z.coerce.date().nullable(), + createdAt: z.coerce.date(), + updatedAt: z.coerce.date(), +}) + +export type WorkspaceFileFolderApi = z.output + +const workspaceFileFoldersSuccessSchema = z.object({ + success: z.boolean(), +}) + +export const createWorkspaceFileFolderBodySchema = z.object({ + name: z.string({ error: 'Name is required' }).trim().min(1, 'Name is required'), + parentId: z.string().nullable().optional(), +}) + +export const updateWorkspaceFileFolderBodySchema = z.object({ + name: z.string().trim().min(1, 'Name is required').optional(), + parentId: z.string().nullable().optional(), + sortOrder: z.number().int().optional(), +}) + +export const moveWorkspaceFileItemsBodySchema = z.object({ + fileIds: z.array(z.string()).default([]), + folderIds: z.array(z.string()).default([]), + targetFolderId: z.string().nullable().optional(), +}) + +export const bulkArchiveWorkspaceFileItemsBodySchema = z.object({ + fileIds: z.array(z.string()).default([]), + folderIds: z.array(z.string()).default([]), +}) + +const queryIdListSchema = z + .union([z.string(), z.array(z.string())]) + .optional() + .transform((value) => { + if (!value) return [] + const values = Array.isArray(value) ? value : [value] + return values + .flatMap((entry) => entry.split(',')) + .map((entry) => entry.trim()) + .filter(Boolean) + }) + +export const downloadWorkspaceFileItemsQuerySchema = z.object({ + fileIds: queryIdListSchema, + folderIds: queryIdListSchema, +}) + +export const listWorkspaceFileFoldersContract = defineRouteContract({ + method: 'GET', + path: '/api/workspaces/[id]/files/folders', + params: workspaceFileFoldersParamsSchema, + query: listWorkspaceFileFoldersQuerySchema, + response: { + mode: 'json', + schema: workspaceFileFoldersSuccessSchema.extend({ + folders: z.array(workspaceFileFolderSchema), + }), + }, +}) + +export const createWorkspaceFileFolderContract = defineRouteContract({ + method: 'POST', + path: '/api/workspaces/[id]/files/folders', + params: workspaceFileFoldersParamsSchema, + body: createWorkspaceFileFolderBodySchema, + response: { + mode: 'json', + schema: workspaceFileFoldersSuccessSchema.extend({ + folder: workspaceFileFolderSchema, + }), + }, +}) + +export const updateWorkspaceFileFolderContract = defineRouteContract({ + method: 'PATCH', + path: '/api/workspaces/[id]/files/folders/[folderId]', + params: workspaceFileFolderParamsSchema, + body: updateWorkspaceFileFolderBodySchema, + response: { + mode: 'json', + schema: workspaceFileFoldersSuccessSchema.extend({ + folder: workspaceFileFolderSchema, + }), + }, +}) + +export const deleteWorkspaceFileFolderContract = defineRouteContract({ + method: 'DELETE', + path: '/api/workspaces/[id]/files/folders/[folderId]', + params: workspaceFileFolderParamsSchema, + response: { + mode: 'json', + schema: workspaceFileFoldersSuccessSchema.extend({ + deletedItems: z.object({ + folders: z.number().int(), + files: z.number().int(), + }), + }), + }, +}) + +export const moveWorkspaceFileItemsContract = defineRouteContract({ + method: 'POST', + path: '/api/workspaces/[id]/files/move', + params: workspaceFileFoldersParamsSchema, + body: moveWorkspaceFileItemsBodySchema, + response: { + mode: 'json', + schema: workspaceFileFoldersSuccessSchema.extend({ + movedItems: z.object({ + files: z.number().int(), + folders: z.number().int(), + }), + }), + }, +}) + +export const bulkArchiveWorkspaceFileItemsContract = defineRouteContract({ + method: 'POST', + path: '/api/workspaces/[id]/files/bulk-archive', + params: workspaceFileFoldersParamsSchema, + body: bulkArchiveWorkspaceFileItemsBodySchema, + response: { + mode: 'json', + schema: workspaceFileFoldersSuccessSchema.extend({ + deletedItems: z.object({ + folders: z.number().int(), + files: z.number().int(), + }), + }), + }, +}) + +export const downloadWorkspaceFileItemsContract = defineRouteContract({ + method: 'GET', + path: '/api/workspaces/[id]/files/download', + params: workspaceFileFoldersParamsSchema, + query: downloadWorkspaceFileItemsQuerySchema, + response: { + mode: 'binary', + }, +}) diff --git a/apps/sim/lib/api/contracts/workspace-files.ts b/apps/sim/lib/api/contracts/workspace-files.ts index 3fc1a57d6e4..3f4498e6540 100644 --- a/apps/sim/lib/api/contracts/workspace-files.ts +++ b/apps/sim/lib/api/contracts/workspace-files.ts @@ -36,6 +36,8 @@ export const workspaceFileRecordSchema = z.object({ size: z.number(), type: z.string(), uploadedBy: z.string(), + folderId: z.string().nullable(), + folderPath: z.string().nullable().optional(), deletedAt: z.coerce.date().nullable().optional(), uploadedAt: z.coerce.date(), updatedAt: z.coerce.date(), @@ -167,6 +169,7 @@ export const workspacePresignedUploadBodySchema = z.object({ fileName: z.string().min(1, 'fileName is required'), contentType: z.string().min(1, 'contentType is required'), fileSize: z.number().nonnegative('fileSize must be a non-negative number'), + folderId: z.string().nullable().optional(), }) export type WorkspacePresignedUploadBody = z.input @@ -202,6 +205,7 @@ export const registerWorkspaceFileBodySchema = z.object({ key: z.string().min(1, 'key is required'), name: z.string().min(1, 'name is required'), contentType: z.string().min(1, 'contentType is required'), + folderId: z.string().nullable().optional(), }) export type RegisterWorkspaceFileBody = z.input diff --git a/apps/sim/lib/copilot/tools/server/files/create-file.ts b/apps/sim/lib/copilot/tools/server/files/create-file.ts index 997766ad1f8..c3d47877c23 100644 --- a/apps/sim/lib/copilot/tools/server/files/create-file.ts +++ b/apps/sim/lib/copilot/tools/server/files/create-file.ts @@ -4,11 +4,16 @@ import { type BaseServerTool, type ServerToolContext, } from '@/lib/copilot/tools/server/base-tool' +import { ensureWorkspaceFileFolderPath } from '@/lib/uploads/contexts/workspace/workspace-file-folder-manager' import { getWorkspaceFileByName, uploadWorkspaceFile, } from '@/lib/uploads/contexts/workspace/workspace-file-manager' -import { inferContentType, validateFlatWorkspaceFileName } from './workspace-file' +import { + inferContentType, + splitWorkspaceFilePath, + validateFlatWorkspaceFileName, +} from './workspace-file' const logger = createLogger('CreateFileServerTool') const CREATE_FILE_TOOL_ID = 'create_file' @@ -47,12 +52,18 @@ export const createFileServerTool: BaseServerTool !segment.trim())) + return 'File path cannot contain empty segments' return null } +export function splitWorkspaceFilePath(fileName: string): { + folderSegments: string[] + leafName: string +} { + const segments = fileName + .trim() + .replace(/^files\//, '') + .split('/') + .map((segment) => segment.trim()) + .filter(Boolean) + return { + folderSegments: segments.slice(0, -1), + leafName: segments[segments.length - 1] ?? '', + } +} + export interface DocumentFormatInfo { isDoc: boolean formatName?: 'PPTX' | 'DOCX' | 'PDF' @@ -186,15 +202,21 @@ export const workspaceFileServerTool: BaseServerTool sanitizeName(segment)) + .join('/') + const fileVfsPath = safeFolderPath ? `${safeFolderPath}/${safeName}` : safeName this.files.set( - `files/${safeName}/meta.json`, + `files/${fileVfsPath}/meta.json`, serializeFileMeta({ id: file.id, name: file.name, + folderId: file.folderId, + folderPath: file.folderPath, + vfsPath: `files/${fileVfsPath}`, contentType: file.type, size: file.size, uploadedAt: file.uploadedAt, @@ -881,6 +889,9 @@ export class WorkspaceVFS { serializeFileMeta({ id: file.id, name: file.name, + folderId: file.folderId, + folderPath: file.folderPath, + vfsPath: `files/${fileVfsPath}`, contentType: file.type, size: file.size, uploadedAt: file.uploadedAt, @@ -888,7 +899,12 @@ export class WorkspaceVFS { ) } - return files.map((f) => ({ id: f.id, name: f.name, type: f.type, size: f.size })) + return files.map((f) => ({ + id: f.id, + name: f.folderPath ? `${f.folderPath}/${f.name}` : f.name, + type: f.type, + size: f.size, + })) } catch (err) { logger.warn('Failed to materialize files', { workspaceId, @@ -1379,11 +1395,19 @@ export class WorkspaceVFS { for (const file of archivedFiles) { const safeName = sanitizeName(file.name) + const safeFolderPath = file.folderPath + ?.split('/') + .map((segment) => sanitizeName(segment)) + .join('/') + const fileVfsPath = safeFolderPath ? `${safeFolderPath}/${safeName}` : safeName this.files.set( - `recently-deleted/files/${safeName}/meta.json`, + `recently-deleted/files/${fileVfsPath}/meta.json`, serializeFileMeta({ id: file.id, name: file.name, + folderId: file.folderId, + folderPath: file.folderPath, + vfsPath: `recently-deleted/files/${fileVfsPath}`, contentType: file.type, size: file.size, uploadedAt: file.uploadedAt, diff --git a/apps/sim/lib/uploads/client/direct-upload.ts b/apps/sim/lib/uploads/client/direct-upload.ts index ae2d52406be..ad448f891e3 100644 --- a/apps/sim/lib/uploads/client/direct-upload.ts +++ b/apps/sim/lib/uploads/client/direct-upload.ts @@ -166,6 +166,7 @@ export const normalizePresignedData = (data: unknown, context: string): Presigne interface GetPresignedOptions { endpoint: string file: File + body?: Record signal?: AbortSignal } @@ -176,7 +177,7 @@ interface GetPresignedOptions { export const getPresignedUploadInfo = async ( opts: GetPresignedOptions ): Promise => { - const { endpoint, file, signal } = opts + const { endpoint, file, body, signal } = opts const response = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -184,6 +185,7 @@ export const getPresignedUploadInfo = async ( fileName: file.name, contentType: getFileContentType(file), fileSize: file.size, + ...body, }), signal, }) @@ -538,6 +540,8 @@ export interface RunUploadStrategyOptions { presignedEndpoint?: string /** Pre-fetched presigned data (e.g. from a batch endpoint). Skips per-file fetch. */ presignedOverride?: PresignedUploadInfo + /** Extra JSON body fields for the presigned endpoint. */ + presignedBody?: Record /** Required when context is `execution`; forwarded to the multipart route to scope the storage key. */ workflowId?: string /** Required when context is `execution`; forwarded to the multipart route to scope the storage key. */ @@ -561,6 +565,7 @@ export const runUploadStrategy = async ( file, presignedEndpoint, presignedOverride, + presignedBody, workspaceId, context, workflowId, @@ -597,7 +602,12 @@ export const runUploadStrategy = async ( 'PRESIGNED_URL_ERROR' ) } - presigned = await getPresignedUploadInfo({ endpoint: presignedEndpoint, file, signal }) + presigned = await getPresignedUploadInfo({ + endpoint: presignedEndpoint, + file, + body: presignedBody, + signal, + }) } if (!presigned.directUploadSupported) { diff --git a/apps/sim/lib/uploads/client/download.ts b/apps/sim/lib/uploads/client/download.ts index 9cbdd88d263..1b7925b1a84 100644 --- a/apps/sim/lib/uploads/client/download.ts +++ b/apps/sim/lib/uploads/client/download.ts @@ -24,3 +24,14 @@ export async function triggerFileDownload(record: WorkspaceFileRecord): Promise< document.body.removeChild(a) URL.revokeObjectURL(objectUrl) } + +export function triggerBlobDownload(blob: Blob, fileName: string): void { + const objectUrl = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = objectUrl + a.download = fileName + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(objectUrl) +} diff --git a/apps/sim/lib/uploads/contexts/workspace/index.ts b/apps/sim/lib/uploads/contexts/workspace/index.ts index 4d2d50a4a2a..b7dbcdee264 100644 --- a/apps/sim/lib/uploads/contexts/workspace/index.ts +++ b/apps/sim/lib/uploads/contexts/workspace/index.ts @@ -1 +1,2 @@ +export * from './workspace-file-folder-manager' export * from './workspace-file-manager' diff --git a/apps/sim/lib/uploads/contexts/workspace/workspace-file-folder-manager.test.ts b/apps/sim/lib/uploads/contexts/workspace/workspace-file-folder-manager.test.ts new file mode 100644 index 00000000000..8d637f8d06d --- /dev/null +++ b/apps/sim/lib/uploads/contexts/workspace/workspace-file-folder-manager.test.ts @@ -0,0 +1,20 @@ +/** + * @vitest-environment node + */ + +import { describe, expect, it } from 'vitest' +import { buildWorkspaceFileFolderPathMap } from './workspace-file-folder-manager' + +describe('workspace file folder paths', () => { + it('builds nested paths from parent relationships', () => { + const paths = buildWorkspaceFileFolderPathMap([ + { id: 'reports', name: 'Reports', parentId: null }, + { id: 'quarterly', name: 'Quarterly', parentId: 'reports' }, + { id: 'archive', name: 'Archive', parentId: null }, + ]) + + expect(paths.get('reports')).toBe('Reports') + expect(paths.get('quarterly')).toBe('Reports/Quarterly') + expect(paths.get('archive')).toBe('Archive') + }) +}) diff --git a/apps/sim/lib/uploads/contexts/workspace/workspace-file-folder-manager.ts b/apps/sim/lib/uploads/contexts/workspace/workspace-file-folder-manager.ts new file mode 100644 index 00000000000..b101d569691 --- /dev/null +++ b/apps/sim/lib/uploads/contexts/workspace/workspace-file-folder-manager.ts @@ -0,0 +1,612 @@ +import { db } from '@sim/db' +import { workspaceFileFolder, workspaceFiles } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { generateId } from '@sim/utils/id' +import { and, asc, eq, inArray, isNull, min, sql } from 'drizzle-orm' + +const logger = createLogger('WorkspaceFileFolders') + +export type WorkspaceFileFolderScope = 'active' | 'archived' | 'all' + +export class WorkspaceFileFolderConflictError extends Error { + readonly code = 'FOLDER_CONFLICT' as const + + constructor(name: string) { + super(`A folder named "${name}" already exists in this location`) + } +} + +export class WorkspaceFileMoveConflictError extends Error { + readonly code = 'FILE_MOVE_CONFLICT' as const + + constructor(name: string) { + super(`A file named "${name}" already exists in the destination folder`) + } +} + +export interface WorkspaceFileFolderRecord { + id: string + workspaceId: string + userId: string + name: string + parentId: string | null + path: string + sortOrder: number + deletedAt: Date | null + createdAt: Date + updatedAt: Date +} + +interface RawWorkspaceFileFolder { + id: string + workspaceId: string + userId: string + name: string + parentId: string | null + sortOrder: number + deletedAt: Date | null + createdAt: Date + updatedAt: Date +} + +export interface WorkspaceFileArchiveResult { + folders: number + files: number +} + +function normalizeParentId(parentId?: string | null): string | null { + return parentId && parentId.length > 0 ? parentId : null +} + +function folderParentCondition(parentId?: string | null) { + const normalized = normalizeParentId(parentId) + return normalized + ? eq(workspaceFileFolder.parentId, normalized) + : isNull(workspaceFileFolder.parentId) +} + +function fileFolderCondition(folderId?: string | null) { + const normalized = normalizeParentId(folderId) + return normalized ? eq(workspaceFiles.folderId, normalized) : isNull(workspaceFiles.folderId) +} + +export function buildWorkspaceFileFolderPathMap( + folders: Array> +): Map { + const folderMap = new Map(folders.map((folder) => [folder.id, folder])) + const paths = new Map() + + const resolve = (folderId: string, seen = new Set()): string => { + const cached = paths.get(folderId) + if (cached != null) return cached + + const folder = folderMap.get(folderId) + if (!folder || seen.has(folderId)) return '' + + const nextSeen = new Set(seen) + nextSeen.add(folderId) + const parentPath = folder.parentId ? resolve(folder.parentId, nextSeen) : '' + const path = parentPath ? `${parentPath}/${folder.name}` : folder.name + paths.set(folderId, path) + return path + } + + for (const folder of folders) { + resolve(folder.id) + } + + return paths +} + +function mapFolder( + folder: RawWorkspaceFileFolder, + paths: Map +): WorkspaceFileFolderRecord { + return { + id: folder.id, + workspaceId: folder.workspaceId, + userId: folder.userId, + name: folder.name, + parentId: folder.parentId, + path: paths.get(folder.id) ?? folder.name, + sortOrder: folder.sortOrder, + deletedAt: folder.deletedAt, + createdAt: folder.createdAt, + updatedAt: folder.updatedAt, + } +} + +export async function listWorkspaceFileFolders( + workspaceId: string, + options?: { scope?: WorkspaceFileFolderScope } +): Promise { + const { scope = 'active' } = options ?? {} + const rows = await db + .select() + .from(workspaceFileFolder) + .where( + scope === 'all' + ? eq(workspaceFileFolder.workspaceId, workspaceId) + : scope === 'archived' + ? and( + eq(workspaceFileFolder.workspaceId, workspaceId), + sql`${workspaceFileFolder.deletedAt} IS NOT NULL` + ) + : and( + eq(workspaceFileFolder.workspaceId, workspaceId), + isNull(workspaceFileFolder.deletedAt) + ) + ) + .orderBy(asc(workspaceFileFolder.sortOrder), asc(workspaceFileFolder.createdAt)) + + const paths = buildWorkspaceFileFolderPathMap(rows) + return rows.map((row) => mapFolder(row, paths)) +} + +export async function getWorkspaceFileFolder( + workspaceId: string, + folderId: string, + options?: { includeDeleted?: boolean } +): Promise { + const { includeDeleted = false } = options ?? {} + const rows = await db + .select() + .from(workspaceFileFolder) + .where( + includeDeleted + ? and( + eq(workspaceFileFolder.id, folderId), + eq(workspaceFileFolder.workspaceId, workspaceId) + ) + : and( + eq(workspaceFileFolder.id, folderId), + eq(workspaceFileFolder.workspaceId, workspaceId), + isNull(workspaceFileFolder.deletedAt) + ) + ) + .limit(1) + + if (rows.length === 0) return null + + const folders = await listWorkspaceFileFolders(workspaceId, { + scope: includeDeleted ? 'all' : 'active', + }) + return folders.find((folder) => folder.id === folderId) ?? null +} + +export async function assertWorkspaceFileFolderTarget( + workspaceId: string, + folderId?: string | null +): Promise { + const normalized = normalizeParentId(folderId) + if (!normalized) return null + + const folder = await getWorkspaceFileFolder(workspaceId, normalized) + if (!folder) { + throw new Error('Target folder not found') + } + + return normalized +} + +async function workspaceFileFolderExists( + workspaceId: string, + name: string, + parentId?: string | null, + excludeFolderId?: string +): Promise { + const rows = await db + .select({ id: workspaceFileFolder.id }) + .from(workspaceFileFolder) + .where( + and( + eq(workspaceFileFolder.workspaceId, workspaceId), + eq(workspaceFileFolder.name, name), + folderParentCondition(parentId), + isNull(workspaceFileFolder.deletedAt) + ) + ) + .limit(2) + + return rows.some((row) => row.id !== excludeFolderId) +} + +async function nextFolderSortOrder(workspaceId: string, parentId?: string | null): Promise { + const [result] = await db + .select({ minSortOrder: min(workspaceFileFolder.sortOrder) }) + .from(workspaceFileFolder) + .where( + and( + eq(workspaceFileFolder.workspaceId, workspaceId), + folderParentCondition(parentId), + isNull(workspaceFileFolder.deletedAt) + ) + ) + + return result?.minSortOrder != null ? result.minSortOrder - 1 : 0 +} + +export async function createWorkspaceFileFolder(params: { + workspaceId: string + userId: string + name: string + parentId?: string | null + sortOrder?: number +}): Promise { + const parentId = await assertWorkspaceFileFolderTarget(params.workspaceId, params.parentId) + const name = params.name.trim() + if (!name) throw new Error('Folder name is required') + + if (await workspaceFileFolderExists(params.workspaceId, name, parentId)) { + throw new WorkspaceFileFolderConflictError(name) + } + + const id = generateId() + const [folder] = await db + .insert(workspaceFileFolder) + .values({ + id, + name, + userId: params.userId, + workspaceId: params.workspaceId, + parentId, + sortOrder: params.sortOrder ?? (await nextFolderSortOrder(params.workspaceId, parentId)), + }) + .returning() + + const paths = buildWorkspaceFileFolderPathMap([folder]) + return mapFolder(folder, paths) +} + +export async function ensureWorkspaceFileFolderPath(params: { + workspaceId: string + userId: string + pathSegments: string[] +}): Promise { + let parentId: string | null = null + for (const rawSegment of params.pathSegments) { + const name = rawSegment.trim() + if (!name) throw new Error('Folder path cannot contain empty segments') + + const folders = await listWorkspaceFileFolders(params.workspaceId) + const existing = folders.find( + (folder) => folder.name === name && (folder.parentId ?? null) === parentId + ) + parentId = existing + ? existing.id + : ( + await createWorkspaceFileFolder({ + workspaceId: params.workspaceId, + userId: params.userId, + name, + parentId, + }) + ).id + } + + return parentId +} + +async function getDescendantFolderIds( + workspaceId: string, + folderId: string, + options?: { includeDeleted?: boolean } +): Promise { + const folders = await listWorkspaceFileFolders(workspaceId, { + scope: options?.includeDeleted ? 'all' : 'active', + }) + const childrenByParent = new Map() + + for (const folder of folders) { + if (!folder.parentId) continue + const children = childrenByParent.get(folder.parentId) ?? [] + children.push(folder.id) + childrenByParent.set(folder.parentId, children) + } + + const descendants: string[] = [] + const visit = (id: string) => { + for (const childId of childrenByParent.get(id) ?? []) { + descendants.push(childId) + visit(childId) + } + } + visit(folderId) + + return descendants +} + +export async function updateWorkspaceFileFolder(params: { + workspaceId: string + folderId: string + name?: string + parentId?: string | null + sortOrder?: number +}): Promise { + const existing = await getWorkspaceFileFolder(params.workspaceId, params.folderId) + if (!existing) throw new Error('Folder not found') + + const updates: Partial = { updatedAt: new Date() } + + if (params.name !== undefined) { + const name = params.name.trim() + if (!name) throw new Error('Folder name is required') + if ( + name !== existing.name && + (await workspaceFileFolderExists( + params.workspaceId, + name, + existing.parentId, + params.folderId + )) + ) { + throw new WorkspaceFileFolderConflictError(name) + } + updates.name = name + } + + if (params.parentId !== undefined) { + const parentId = await assertWorkspaceFileFolderTarget(params.workspaceId, params.parentId) + if (parentId === params.folderId) throw new Error('Folder cannot be its own parent') + + const descendants = await getDescendantFolderIds(params.workspaceId, params.folderId) + if (parentId && descendants.includes(parentId)) { + throw new Error('Cannot move a folder into one of its descendants') + } + + if ( + await workspaceFileFolderExists( + params.workspaceId, + params.name?.trim() || existing.name, + parentId, + params.folderId + ) + ) { + throw new WorkspaceFileFolderConflictError(params.name?.trim() || existing.name) + } + updates.parentId = parentId + } + + if (params.sortOrder !== undefined) { + updates.sortOrder = params.sortOrder + } + + const [folder] = await db + .update(workspaceFileFolder) + .set(updates) + .where( + and( + eq(workspaceFileFolder.id, params.folderId), + eq(workspaceFileFolder.workspaceId, params.workspaceId), + isNull(workspaceFileFolder.deletedAt) + ) + ) + .returning() + + if (!folder) throw new Error('Folder not found') + return ( + (await getWorkspaceFileFolder(params.workspaceId, folder.id)) ?? mapFolder(folder, new Map()) + ) +} + +export async function fileNameExistsInWorkspaceFolder( + workspaceId: string, + fileName: string, + folderId?: string | null, + excludeFileId?: string +): Promise { + const rows = await db + .select({ id: workspaceFiles.id }) + .from(workspaceFiles) + .where( + and( + eq(workspaceFiles.workspaceId, workspaceId), + eq(workspaceFiles.originalName, fileName), + eq(workspaceFiles.context, 'workspace'), + fileFolderCondition(folderId), + isNull(workspaceFiles.deletedAt) + ) + ) + .limit(2) + + return rows.some((row) => row.id !== excludeFileId) +} + +export async function moveWorkspaceFileItems(params: { + workspaceId: string + fileIds?: string[] + folderIds?: string[] + targetFolderId?: string | null +}): Promise<{ movedFiles: number; movedFolders: number }> { + const fileIds = Array.from(new Set(params.fileIds ?? [])) + const folderIds = Array.from(new Set(params.folderIds ?? [])) + const targetFolderId = await assertWorkspaceFileFolderTarget( + params.workspaceId, + params.targetFolderId + ) + + if (folderIds.includes(targetFolderId ?? '')) { + throw new Error('Cannot move a folder into itself') + } + + for (const folderId of folderIds) { + const descendants = await getDescendantFolderIds(params.workspaceId, folderId) + if (targetFolderId && descendants.includes(targetFolderId)) { + throw new Error('Cannot move a folder into one of its descendants') + } + } + + const movingFiles = + fileIds.length > 0 + ? await db + .select({ id: workspaceFiles.id, name: workspaceFiles.originalName }) + .from(workspaceFiles) + .where( + and( + inArray(workspaceFiles.id, fileIds), + eq(workspaceFiles.workspaceId, params.workspaceId), + eq(workspaceFiles.context, 'workspace'), + isNull(workspaceFiles.deletedAt) + ) + ) + : [] + + for (const file of movingFiles) { + if ( + await fileNameExistsInWorkspaceFolder(params.workspaceId, file.name, targetFolderId, file.id) + ) { + throw new WorkspaceFileMoveConflictError(file.name) + } + } + + const movedFiles = + fileIds.length > 0 + ? await db + .update(workspaceFiles) + .set({ folderId: targetFolderId, updatedAt: new Date() }) + .where( + and( + inArray(workspaceFiles.id, fileIds), + eq(workspaceFiles.workspaceId, params.workspaceId), + eq(workspaceFiles.context, 'workspace'), + isNull(workspaceFiles.deletedAt) + ) + ) + .returning({ id: workspaceFiles.id }) + : [] + + const movedFolders = + folderIds.length > 0 + ? await db + .update(workspaceFileFolder) + .set({ parentId: targetFolderId, updatedAt: new Date() }) + .where( + and( + inArray(workspaceFileFolder.id, folderIds), + eq(workspaceFileFolder.workspaceId, params.workspaceId), + isNull(workspaceFileFolder.deletedAt) + ) + ) + .returning({ id: workspaceFileFolder.id }) + : [] + + return { movedFiles: movedFiles.length, movedFolders: movedFolders.length } +} + +export async function archiveWorkspaceFileFolderRecursive( + workspaceId: string, + folderId: string +): Promise { + const folder = await getWorkspaceFileFolder(workspaceId, folderId) + if (!folder) throw new Error('Folder not found') + + const now = new Date() + const folderIds = [folderId, ...(await getDescendantFolderIds(workspaceId, folderId))] + + return db.transaction(async (tx) => { + const archivedFiles = await tx + .update(workspaceFiles) + .set({ deletedAt: now, updatedAt: now }) + .where( + and( + inArray(workspaceFiles.folderId, folderIds), + eq(workspaceFiles.workspaceId, workspaceId), + eq(workspaceFiles.context, 'workspace'), + isNull(workspaceFiles.deletedAt) + ) + ) + .returning({ id: workspaceFiles.id }) + + const archivedFolders = await tx + .update(workspaceFileFolder) + .set({ deletedAt: now, updatedAt: now }) + .where( + and( + inArray(workspaceFileFolder.id, folderIds), + eq(workspaceFileFolder.workspaceId, workspaceId), + isNull(workspaceFileFolder.deletedAt) + ) + ) + .returning({ id: workspaceFileFolder.id }) + + logger.info('Archived workspace file folder recursively', { + workspaceId, + folderId, + folders: archivedFolders.length, + files: archivedFiles.length, + }) + + return { folders: archivedFolders.length, files: archivedFiles.length } + }) +} + +export async function bulkArchiveWorkspaceFileItems(params: { + workspaceId: string + fileIds?: string[] + folderIds?: string[] +}): Promise { + const now = new Date() + const explicitFileIds = Array.from(new Set(params.fileIds ?? [])) + const explicitFolderIds = Array.from(new Set(params.folderIds ?? [])) + const descendantFolderIds = ( + await Promise.all( + explicitFolderIds.map((folderId) => getDescendantFolderIds(params.workspaceId, folderId)) + ) + ).flat() + const allFolderIds = Array.from(new Set([...explicitFolderIds, ...descendantFolderIds])) + + return db.transaction(async (tx) => { + const archivedExplicitFiles = + explicitFileIds.length > 0 + ? await tx + .update(workspaceFiles) + .set({ deletedAt: now, updatedAt: now }) + .where( + and( + inArray(workspaceFiles.id, explicitFileIds), + eq(workspaceFiles.workspaceId, params.workspaceId), + eq(workspaceFiles.context, 'workspace'), + isNull(workspaceFiles.deletedAt) + ) + ) + .returning({ id: workspaceFiles.id }) + : [] + + const archivedDescendantFiles = + allFolderIds.length > 0 + ? await tx + .update(workspaceFiles) + .set({ deletedAt: now, updatedAt: now }) + .where( + and( + inArray(workspaceFiles.folderId, allFolderIds), + eq(workspaceFiles.workspaceId, params.workspaceId), + eq(workspaceFiles.context, 'workspace'), + isNull(workspaceFiles.deletedAt) + ) + ) + .returning({ id: workspaceFiles.id }) + : [] + + const archivedFolders = + allFolderIds.length > 0 + ? await tx + .update(workspaceFileFolder) + .set({ deletedAt: now, updatedAt: now }) + .where( + and( + inArray(workspaceFileFolder.id, allFolderIds), + eq(workspaceFileFolder.workspaceId, params.workspaceId), + isNull(workspaceFileFolder.deletedAt) + ) + ) + .returning({ id: workspaceFileFolder.id }) + : [] + + return { + folders: archivedFolders.length, + files: new Set([...archivedExplicitFiles, ...archivedDescendantFiles].map((file) => file.id)) + .size, + } + }) +} diff --git a/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.test.ts b/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.test.ts index bd2cf91cf35..3e8d95423e3 100644 --- a/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.test.ts +++ b/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.test.ts @@ -21,7 +21,10 @@ function makeFileRecord(): WorkspaceFileRecord { size: 128, type: 'text/markdown', uploadedBy: 'user_123', + folderId: null, + folderPath: null, uploadedAt: new Date('2026-04-13T00:00:00.000Z'), + updatedAt: new Date('2026-04-13T00:00:00.000Z'), } } @@ -48,4 +51,28 @@ describe('workspace file reference normalization', () => { name: 'the_last_cartographer_of_vael.md', }) }) + + it('resolves duplicate names by folder-aware VFS path', () => { + const reportsFile: WorkspaceFileRecord = { + ...makeFileRecord(), + id: 'file-reports', + name: 'q1.csv', + folderId: 'folder-reports', + folderPath: 'Reports', + } + const archiveFile: WorkspaceFileRecord = { + ...makeFileRecord(), + id: 'file-archive', + name: 'q1.csv', + folderId: 'folder-archive', + folderPath: 'Archive', + } + + expect( + findWorkspaceFileRecord([reportsFile, archiveFile], 'files/Reports/q1.csv/content') + ).toBe(reportsFile) + expect(findWorkspaceFileRecord([reportsFile, archiveFile], 'files/Archive/q1.csv')).toBe( + archiveFile + ) + }) }) diff --git a/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts b/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts index fda46789dce..1a5c14b1e27 100644 --- a/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts +++ b/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts @@ -29,6 +29,12 @@ import { MAX_WORKSPACE_FILE_SIZE } from '@/lib/uploads/shared/types' import { getWorkspaceWithOwner } from '@/lib/workspaces/permissions/utils' import { isUuid, sanitizeFileName } from '@/executor/constants' import type { UserFile } from '@/executor/types' +import { + assertWorkspaceFileFolderTarget, + buildWorkspaceFileFolderPathMap, + fileNameExistsInWorkspaceFolder, + listWorkspaceFileFolders, +} from './workspace-file-folder-manager' const logger = createLogger('WorkspaceFileStorage') @@ -51,6 +57,8 @@ export interface WorkspaceFileRecord { size: number type: string uploadedBy: string + folderId?: string | null + folderPath?: string | null deletedAt?: Date | null uploadedAt: Date updatedAt: Date @@ -124,14 +132,15 @@ function withCopySuffix(fileName: string, n: number): string { */ async function allocateUniqueWorkspaceFileName( workspaceId: string, - baseName: string + baseName: string, + folderId?: string | null ): Promise { - if (!(await fileExistsInWorkspace(workspaceId, baseName))) { + if (!(await fileExistsInWorkspace(workspaceId, baseName, folderId))) { return baseName } for (let n = 1; n <= MAX_COPY_SUFFIX; n++) { const candidate = withCopySuffix(baseName, n) - if (!(await fileExistsInWorkspace(workspaceId, candidate))) { + if (!(await fileExistsInWorkspace(workspaceId, candidate, folderId))) { return candidate } } @@ -146,10 +155,12 @@ export async function uploadWorkspaceFile( userId: string, fileBuffer: Buffer, fileName: string, - contentType: string + contentType: string, + options?: { folderId?: string | null } ): Promise { logger.info(`Uploading workspace file: ${fileName} for workspace ${workspaceId}`) + const folderId = await assertWorkspaceFileFolderTarget(workspaceId, options?.folderId) const quotaCheck = await checkStorageQuota(userId, fileBuffer.length) if (!quotaCheck.allowed) { @@ -158,7 +169,7 @@ export async function uploadWorkspaceFile( let lastError: unknown for (let attempt = 0; attempt < MAX_UPLOAD_UNIQUE_RETRIES; attempt++) { - const uniqueName = await allocateUniqueWorkspaceFileName(workspaceId, fileName) + const uniqueName = await allocateUniqueWorkspaceFileName(workspaceId, fileName, folderId) const storageKey = generateWorkspaceFileKey(workspaceId, uniqueName) let fileId = `wf_${generateShortId()}` @@ -171,6 +182,7 @@ export async function uploadWorkspaceFile( purpose: 'workspace', userId: userId, workspaceId: workspaceId, + ...(folderId ? { folderId } : {}), } const uploadResult = await uploadFile({ @@ -193,6 +205,7 @@ export async function uploadWorkspaceFile( key: uploadResult.key, userId, workspaceId, + folderId, context: 'workspace', originalName: uniqueName, contentType, @@ -210,6 +223,7 @@ export async function uploadWorkspaceFile( key: uploadResult.key, userId, workspaceId, + folderId, context: 'workspace', originalName: uniqueName, contentType, @@ -290,8 +304,10 @@ export async function registerUploadedWorkspaceFile(params: { key: string originalName: string contentType: string + folderId?: string | null }): Promise { const { workspaceId, userId, key, originalName, contentType } = params + const folderId = await assertWorkspaceFileFolderTarget(workspaceId, params.folderId) if (!hasCloudStorage()) { throw new Error('Direct-upload registration requires cloud storage') @@ -340,13 +356,14 @@ export async function registerUploadedWorkspaceFile(params: { let lastInsertError: unknown for (let attempt = 0; attempt < MAX_UPLOAD_UNIQUE_RETRIES; attempt++) { fileId = `wf_${generateShortId()}` - displayName = await allocateUniqueWorkspaceFileName(workspaceId, originalName) + displayName = await allocateUniqueWorkspaceFileName(workspaceId, originalName, folderId) try { await insertFileMetadata({ id: fileId, key, userId, workspaceId, + folderId, context: 'workspace', originalName: displayName, contentType, @@ -503,29 +520,40 @@ export async function trackChatUpload( */ export async function fileExistsInWorkspace( workspaceId: string, - fileName: string + fileName: string, + folderId?: string | null ): Promise { try { - const existing = await db - .select() - .from(workspaceFiles) - .where( - and( - eq(workspaceFiles.workspaceId, workspaceId), - eq(workspaceFiles.originalName, fileName), - eq(workspaceFiles.context, 'workspace'), - isNull(workspaceFiles.deletedAt) - ) - ) - .limit(1) - - return existing.length > 0 + return fileNameExistsInWorkspaceFolder(workspaceId, fileName, folderId) } catch (error) { logger.error(`Failed to check file existence for ${fileName}:`, error) return false } } +function mapWorkspaceFileRecord( + file: typeof workspaceFiles.$inferSelect, + workspaceId: string, + folderPaths: Map +): WorkspaceFileRecord { + const pathPrefix = getServePathPrefix() + return { + id: file.id, + workspaceId: file.workspaceId || workspaceId, + name: file.originalName, + key: file.key, + path: `${pathPrefix}${encodeURIComponent(file.key)}?context=workspace`, + size: file.size, + type: file.contentType, + uploadedBy: file.userId, + folderId: file.folderId, + folderPath: file.folderId ? (folderPaths.get(file.folderId) ?? null) : null, + deletedAt: file.deletedAt, + uploadedAt: file.uploadedAt, + updatedAt: file.updatedAt, + } +} + /** * Look up a single active workspace file by its original name. * Returns the record if found, or null if no matching file exists. @@ -533,8 +561,10 @@ export async function fileExistsInWorkspace( */ export async function getWorkspaceFileByName( workspaceId: string, - fileName: string + fileName: string, + options?: { folderId?: string | null } ): Promise { + const folderId = options?.folderId ?? null const files = await db .select() .from(workspaceFiles) @@ -543,6 +573,7 @@ export async function getWorkspaceFileByName( eq(workspaceFiles.workspaceId, workspaceId), eq(workspaceFiles.originalName, fileName), eq(workspaceFiles.context, 'workspace'), + folderId ? eq(workspaceFiles.folderId, folderId) : isNull(workspaceFiles.folderId), isNull(workspaceFiles.deletedAt) ) ) @@ -550,22 +581,9 @@ export async function getWorkspaceFileByName( if (files.length === 0) return null - const pathPrefix = getServePathPrefix() - - const file = files[0] - return { - id: file.id, - workspaceId: file.workspaceId || workspaceId, - name: file.originalName, - key: file.key, - path: `${pathPrefix}${encodeURIComponent(file.key)}?context=workspace`, - size: file.size, - type: file.contentType, - uploadedBy: file.userId, - deletedAt: file.deletedAt, - uploadedAt: file.uploadedAt, - updatedAt: file.updatedAt, - } + const folders = await listWorkspaceFileFolders(workspaceId, { scope: 'all' }) + const folderPaths = buildWorkspaceFileFolderPathMap(folders) + return mapWorkspaceFileRecord(files[0], workspaceId, folderPaths) } /** @@ -600,21 +618,10 @@ export async function listWorkspaceFiles( ) .orderBy(workspaceFiles.uploadedAt) - const pathPrefix = getServePathPrefix() - - return files.map((file) => ({ - id: file.id, - workspaceId: file.workspaceId || workspaceId, // Use query workspaceId as fallback (should never be null for workspace files) - name: file.originalName, - key: file.key, - path: `${pathPrefix}${encodeURIComponent(file.key)}?context=workspace`, - size: file.size, - type: file.contentType, - uploadedBy: file.userId, - deletedAt: file.deletedAt, - uploadedAt: file.uploadedAt, - updatedAt: file.updatedAt, - })) + const folders = await listWorkspaceFileFolders(workspaceId, { scope: 'all' }) + const folderPaths = buildWorkspaceFileFolderPathMap(folders) + + return files.map((file) => mapWorkspaceFileRecord(file, workspaceId, folderPaths)) } catch (error) { logger.error(`Failed to list workspace files for ${workspaceId}:`, error) return [] @@ -691,7 +698,22 @@ export function findWorkspaceFileRecord( return normalizedIdMatch } - const segmentKey = normalizeVfsSegment(normalizedReference) + const segmentKey = normalizedReference + .split('/') + .map((segment) => normalizeVfsSegment(segment)) + .join('/') + const normalizedPathMatch = files.find((file) => { + const folderPath = file.folderPath + ?.split('/') + .map((segment) => normalizeVfsSegment(segment)) + .join('/') + const fullPath = folderPath + ? `${folderPath}/${normalizeVfsSegment(file.name)}` + : normalizeVfsSegment(file.name) + return fullPath === segmentKey + }) + if (normalizedPathMatch) return normalizedPathMatch + return files.find((file) => normalizeVfsSegment(file.name) === segmentKey) ?? null } @@ -737,22 +759,9 @@ export async function getWorkspaceFile( if (files.length === 0) return null - const pathPrefix = getServePathPrefix() - - const file = files[0] - return { - id: file.id, - workspaceId: file.workspaceId || workspaceId, // Use query workspaceId as fallback (should never be null for workspace files) - name: file.originalName, - key: file.key, - path: `${pathPrefix}${encodeURIComponent(file.key)}?context=workspace`, - size: file.size, - type: file.contentType, - uploadedBy: file.userId, - deletedAt: file.deletedAt, - uploadedAt: file.uploadedAt, - updatedAt: file.updatedAt, - } + const folders = await listWorkspaceFileFolders(workspaceId, { scope: 'all' }) + const folderPaths = buildWorkspaceFileFolderPathMap(folders) + return mapWorkspaceFileRecord(files[0], workspaceId, folderPaths) } catch (error) { logger.error(`Failed to get workspace file ${fileId}:`, error) return null @@ -816,6 +825,7 @@ export async function updateWorkspaceFileContent( purpose: 'workspace', userId, workspaceId, + ...(fileRecord.folderId ? { folderId: fileRecord.folderId } : {}), } await uploadFile({ @@ -890,7 +900,7 @@ export async function renameWorkspaceFile( return fileRecord } - const exists = await fileExistsInWorkspace(workspaceId, trimmedName) + const exists = await fileExistsInWorkspace(workspaceId, trimmedName, fileRecord.folderId) if (exists) { throw new FileConflictError(trimmedName) } @@ -992,14 +1002,14 @@ export async function restoreWorkspaceFile(workspaceId: string, fileId: string): try { const newName = await generateRestoreName( fileRecord.name, - (candidate) => fileExistsInWorkspace(workspaceId, candidate), + (candidate) => fileExistsInWorkspace(workspaceId, candidate, null), { hasExtension: true } ) attemptedRestoreName = newName await db .update(workspaceFiles) - .set({ deletedAt: null, originalName: newName, updatedAt: new Date() }) + .set({ deletedAt: null, folderId: null, originalName: newName, updatedAt: new Date() }) .where( and( eq(workspaceFiles.id, fileId), diff --git a/apps/sim/lib/uploads/server/metadata.ts b/apps/sim/lib/uploads/server/metadata.ts index fedcdaea25d..2d30a2e8ede 100644 --- a/apps/sim/lib/uploads/server/metadata.ts +++ b/apps/sim/lib/uploads/server/metadata.ts @@ -17,6 +17,7 @@ export interface FileMetadataInsertOptions { originalName: string contentType: string size: number + folderId?: string | null /** Optional — a UUID is generated when omitted. */ id?: string } @@ -34,7 +35,8 @@ interface FileMetadataQueryOptions { export async function insertFileMetadata( options: FileMetadataInsertOptions ): Promise { - const { key, userId, workspaceId, context, originalName, contentType, size, id } = options + const { key, userId, workspaceId, context, originalName, contentType, size, folderId, id } = + options const existingDeleted = await db .select() @@ -48,6 +50,7 @@ export async function insertFileMetadata( .set({ userId, workspaceId: workspaceId || null, + folderId: folderId ?? null, context, originalName, displayName: originalName, @@ -84,6 +87,7 @@ export async function insertFileMetadata( key, userId, workspaceId: workspaceId || null, + folderId: folderId ?? null, context, originalName, displayName: originalName, diff --git a/packages/db/migrations/meta/0207_snapshot.json b/packages/db/migrations/meta/0207_snapshot.json deleted file mode 100644 index 2216a54866d..00000000000 --- a/packages/db/migrations/meta/0207_snapshot.json +++ /dev/null @@ -1,15873 +0,0 @@ -{ - "id": "c9500637-c9b7-4bf8-b78c-1887420b845f", - "prevId": "9f14d579-f456-4491-83ae-811130223dae", - "version": "7", - "dialect": "postgresql", - "tables": { - "public.a2a_agent": { - "name": "a2a_agent", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "workspace_id": { - "name": "workspace_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "workflow_id": { - "name": "workflow_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_by": { - "name": "created_by", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "version": { - "name": "version", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'1.0.0'" - }, - "capabilities": { - "name": "capabilities", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'{}'" - }, - "skills": { - "name": "skills", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'[]'" - }, - "authentication": { - "name": "authentication", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'{}'" - }, - "signatures": { - "name": "signatures", - "type": "jsonb", - "primaryKey": false, - "notNull": false, - "default": "'[]'" - }, - "is_published": { - "name": "is_published", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "published_at": { - "name": "published_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "archived_at": { - "name": "archived_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": { - "a2a_agent_workflow_id_idx": { - "name": "a2a_agent_workflow_id_idx", - "columns": [ - { - "expression": "workflow_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "a2a_agent_created_by_idx": { - "name": "a2a_agent_created_by_idx", - "columns": [ - { - "expression": "created_by", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "a2a_agent_workspace_workflow_unique": { - "name": "a2a_agent_workspace_workflow_unique", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "workflow_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "where": "\"a2a_agent\".\"archived_at\" IS NULL", - "concurrently": false, - "method": "btree", - "with": {} - }, - "a2a_agent_archived_at_idx": { - "name": "a2a_agent_archived_at_idx", - "columns": [ - { - "expression": "archived_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "a2a_agent_workspace_archived_partial_idx": { - "name": "a2a_agent_workspace_archived_partial_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "archived_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "where": "\"a2a_agent\".\"archived_at\" IS NOT NULL", - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "a2a_agent_workspace_id_workspace_id_fk": { - "name": "a2a_agent_workspace_id_workspace_id_fk", - "tableFrom": "a2a_agent", - "tableTo": "workspace", - "columnsFrom": ["workspace_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "a2a_agent_workflow_id_workflow_id_fk": { - "name": "a2a_agent_workflow_id_workflow_id_fk", - "tableFrom": "a2a_agent", - "tableTo": "workflow", - "columnsFrom": ["workflow_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "a2a_agent_created_by_user_id_fk": { - "name": "a2a_agent_created_by_user_id_fk", - "tableFrom": "a2a_agent", - "tableTo": "user", - "columnsFrom": ["created_by"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.a2a_push_notification_config": { - "name": "a2a_push_notification_config", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "task_id": { - "name": "task_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "url": { - "name": "url", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "token": { - "name": "token", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "auth_schemes": { - "name": "auth_schemes", - "type": "jsonb", - "primaryKey": false, - "notNull": false, - "default": "'[]'" - }, - "auth_credentials": { - "name": "auth_credentials", - "type": "text", - "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": { - "a2a_push_notification_config_task_unique": { - "name": "a2a_push_notification_config_task_unique", - "columns": [ - { - "expression": "task_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "a2a_push_notification_config_task_id_a2a_task_id_fk": { - "name": "a2a_push_notification_config_task_id_a2a_task_id_fk", - "tableFrom": "a2a_push_notification_config", - "tableTo": "a2a_task", - "columnsFrom": ["task_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.a2a_task": { - "name": "a2a_task", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "agent_id": { - "name": "agent_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "session_id": { - "name": "session_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "status": { - "name": "status", - "type": "a2a_task_status", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'submitted'" - }, - "messages": { - "name": "messages", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'[]'" - }, - "artifacts": { - "name": "artifacts", - "type": "jsonb", - "primaryKey": false, - "notNull": false, - "default": "'[]'" - }, - "execution_id": { - "name": "execution_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "metadata": { - "name": "metadata", - "type": "jsonb", - "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()" - }, - "completed_at": { - "name": "completed_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "a2a_task_agent_id_idx": { - "name": "a2a_task_agent_id_idx", - "columns": [ - { - "expression": "agent_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "a2a_task_session_id_idx": { - "name": "a2a_task_session_id_idx", - "columns": [ - { - "expression": "session_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "a2a_task_status_idx": { - "name": "a2a_task_status_idx", - "columns": [ - { - "expression": "status", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "a2a_task_execution_id_idx": { - "name": "a2a_task_execution_id_idx", - "columns": [ - { - "expression": "execution_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "a2a_task_created_at_idx": { - "name": "a2a_task_created_at_idx", - "columns": [ - { - "expression": "created_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "a2a_task_agent_id_a2a_agent_id_fk": { - "name": "a2a_task_agent_id_a2a_agent_id_fk", - "tableFrom": "a2a_task", - "tableTo": "a2a_agent", - "columnsFrom": ["agent_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.academy_certificate": { - "name": "academy_certificate", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "course_id": { - "name": "course_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "status": { - "name": "status", - "type": "academy_cert_status", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'active'" - }, - "issued_at": { - "name": "issued_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "certificate_number": { - "name": "certificate_number", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "metadata": { - "name": "metadata", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "academy_certificate_user_id_idx": { - "name": "academy_certificate_user_id_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "academy_certificate_course_id_idx": { - "name": "academy_certificate_course_id_idx", - "columns": [ - { - "expression": "course_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "academy_certificate_user_course_unique": { - "name": "academy_certificate_user_course_unique", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "course_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - }, - "academy_certificate_number_idx": { - "name": "academy_certificate_number_idx", - "columns": [ - { - "expression": "certificate_number", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "academy_certificate_status_idx": { - "name": "academy_certificate_status_idx", - "columns": [ - { - "expression": "status", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "academy_certificate_user_id_user_id_fk": { - "name": "academy_certificate_user_id_user_id_fk", - "tableFrom": "academy_certificate", - "tableTo": "user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "academy_certificate_certificate_number_unique": { - "name": "academy_certificate_certificate_number_unique", - "nullsNotDistinct": false, - "columns": ["certificate_number"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "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": { - "account_user_id_idx": { - "name": "account_user_id_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_account_on_account_id_provider_id": { - "name": "idx_account_on_account_id_provider_id", - "columns": [ - { - "expression": "account_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "provider_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "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 - }, - "workspace_id": { - "name": "workspace_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_by": { - "name": "created_by", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "key": { - "name": "key", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "key_hash": { - "name": "key_hash", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'personal'" - }, - "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": { - "api_key_workspace_type_idx": { - "name": "api_key_workspace_type_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "type", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "api_key_user_type_idx": { - "name": "api_key_user_type_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "type", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "api_key_key_hash_idx": { - "name": "api_key_key_hash_idx", - "columns": [ - { - "expression": "key_hash", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "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" - }, - "api_key_workspace_id_workspace_id_fk": { - "name": "api_key_workspace_id_workspace_id_fk", - "tableFrom": "api_key", - "tableTo": "workspace", - "columnsFrom": ["workspace_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "api_key_created_by_user_id_fk": { - "name": "api_key_created_by_user_id_fk", - "tableFrom": "api_key", - "tableTo": "user", - "columnsFrom": ["created_by"], - "columnsTo": ["id"], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "api_key_key_unique": { - "name": "api_key_key_unique", - "nullsNotDistinct": false, - "columns": ["key"] - } - }, - "policies": {}, - "checkConstraints": { - "workspace_type_check": { - "name": "workspace_type_check", - "value": "(type = 'workspace' AND workspace_id IS NOT NULL) OR (type = 'personal' AND workspace_id IS NULL)" - } - }, - "isRLSEnabled": false - }, - "public.async_jobs": { - "name": "async_jobs", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "payload": { - "name": "payload", - "type": "jsonb", - "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()" - }, - "started_at": { - "name": "started_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "completed_at": { - "name": "completed_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "run_at": { - "name": "run_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "attempts": { - "name": "attempts", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "max_attempts": { - "name": "max_attempts", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 3 - }, - "error": { - "name": "error", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "output": { - "name": "output", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "metadata": { - "name": "metadata", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'{}'" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "async_jobs_status_started_at_idx": { - "name": "async_jobs_status_started_at_idx", - "columns": [ - { - "expression": "status", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "started_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "async_jobs_status_completed_at_idx": { - "name": "async_jobs_status_completed_at_idx", - "columns": [ - { - "expression": "status", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "completed_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.audit_log": { - "name": "audit_log", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "workspace_id": { - "name": "workspace_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "actor_id": { - "name": "actor_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "action": { - "name": "action", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "resource_type": { - "name": "resource_type", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "resource_id": { - "name": "resource_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "actor_name": { - "name": "actor_name", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "actor_email": { - "name": "actor_email", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "resource_name": { - "name": "resource_name", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "metadata": { - "name": "metadata", - "type": "jsonb", - "primaryKey": false, - "notNull": false, - "default": "'{}'" - }, - "ip_address": { - "name": "ip_address", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "user_agent": { - "name": "user_agent", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "audit_log_workspace_created_idx": { - "name": "audit_log_workspace_created_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "created_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "audit_log_workspace_created_at_id_idx": { - "name": "audit_log_workspace_created_at_id_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "date_trunc('milliseconds', \"created_at\")", - "asc": true, - "isExpression": true, - "nulls": "last" - }, - { - "expression": "id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "audit_log_actor_created_idx": { - "name": "audit_log_actor_created_idx", - "columns": [ - { - "expression": "actor_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "created_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "audit_log_resource_idx": { - "name": "audit_log_resource_idx", - "columns": [ - { - "expression": "resource_type", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "resource_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "audit_log_action_idx": { - "name": "audit_log_action_idx", - "columns": [ - { - "expression": "action", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "audit_log_workspace_id_workspace_id_fk": { - "name": "audit_log_workspace_id_workspace_id_fk", - "tableFrom": "audit_log", - "tableTo": "workspace", - "columnsFrom": ["workspace_id"], - "columnsTo": ["id"], - "onDelete": "set null", - "onUpdate": "no action" - }, - "audit_log_actor_id_user_id_fk": { - "name": "audit_log_actor_id_user_id_fk", - "tableFrom": "audit_log", - "tableTo": "user", - "columnsFrom": ["actor_id"], - "columnsTo": ["id"], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "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 - }, - "identifier": { - "name": "identifier", - "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": "'[]'" - }, - "archived_at": { - "name": "archived_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": { - "identifier_idx": { - "name": "identifier_idx", - "columns": [ - { - "expression": "identifier", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "where": "\"chat\".\"archived_at\" IS NULL", - "concurrently": false, - "method": "btree", - "with": {} - }, - "chat_archived_at_partial_idx": { - "name": "chat_archived_at_partial_idx", - "columns": [ - { - "expression": "archived_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "where": "\"chat\".\"archived_at\" IS NOT NULL", - "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.copilot_async_tool_calls": { - "name": "copilot_async_tool_calls", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "run_id": { - "name": "run_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "checkpoint_id": { - "name": "checkpoint_id", - "type": "uuid", - "primaryKey": false, - "notNull": false - }, - "tool_call_id": { - "name": "tool_call_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "tool_name": { - "name": "tool_name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "args": { - "name": "args", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'{}'" - }, - "status": { - "name": "status", - "type": "copilot_async_tool_status", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'pending'" - }, - "result": { - "name": "result", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "error": { - "name": "error", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "claimed_at": { - "name": "claimed_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "claimed_by": { - "name": "claimed_by", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "completed_at": { - "name": "completed_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": { - "copilot_async_tool_calls_run_id_idx": { - "name": "copilot_async_tool_calls_run_id_idx", - "columns": [ - { - "expression": "run_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "copilot_async_tool_calls_checkpoint_id_idx": { - "name": "copilot_async_tool_calls_checkpoint_id_idx", - "columns": [ - { - "expression": "checkpoint_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "copilot_async_tool_calls_tool_call_id_idx": { - "name": "copilot_async_tool_calls_tool_call_id_idx", - "columns": [ - { - "expression": "tool_call_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "copilot_async_tool_calls_status_idx": { - "name": "copilot_async_tool_calls_status_idx", - "columns": [ - { - "expression": "status", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "copilot_async_tool_calls_run_status_idx": { - "name": "copilot_async_tool_calls_run_status_idx", - "columns": [ - { - "expression": "run_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "status", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "copilot_async_tool_calls_tool_call_id_unique": { - "name": "copilot_async_tool_calls_tool_call_id_unique", - "columns": [ - { - "expression": "tool_call_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "copilot_async_tool_calls_run_id_copilot_runs_id_fk": { - "name": "copilot_async_tool_calls_run_id_copilot_runs_id_fk", - "tableFrom": "copilot_async_tool_calls", - "tableTo": "copilot_runs", - "columnsFrom": ["run_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk": { - "name": "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk", - "tableFrom": "copilot_async_tool_calls", - "tableTo": "copilot_run_checkpoints", - "columnsFrom": ["checkpoint_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.copilot_chats": { - "name": "copilot_chats", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "workflow_id": { - "name": "workflow_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "workspace_id": { - "name": "workspace_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "type": { - "name": "type", - "type": "chat_type", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'copilot'" - }, - "title": { - "name": "title", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "messages": { - "name": "messages", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'[]'" - }, - "model": { - "name": "model", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'claude-3-7-sonnet-latest'" - }, - "conversation_id": { - "name": "conversation_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "preview_yaml": { - "name": "preview_yaml", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "plan_artifact": { - "name": "plan_artifact", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "config": { - "name": "config", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "resources": { - "name": "resources", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'[]'" - }, - "last_seen_at": { - "name": "last_seen_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "pinned": { - "name": "pinned", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": 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": { - "copilot_chats_user_id_idx": { - "name": "copilot_chats_user_id_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "copilot_chats_workflow_id_idx": { - "name": "copilot_chats_workflow_id_idx", - "columns": [ - { - "expression": "workflow_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "copilot_chats_user_workflow_idx": { - "name": "copilot_chats_user_workflow_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "workflow_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "copilot_chats_user_workspace_idx": { - "name": "copilot_chats_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": {} - }, - "copilot_chats_created_at_idx": { - "name": "copilot_chats_created_at_idx", - "columns": [ - { - "expression": "created_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "copilot_chats_updated_at_idx": { - "name": "copilot_chats_updated_at_idx", - "columns": [ - { - "expression": "updated_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "copilot_chats_workspace_created_at_id_idx": { - "name": "copilot_chats_workspace_created_at_id_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "date_trunc('milliseconds', \"created_at\")", - "asc": true, - "isExpression": true, - "nulls": "last" - }, - { - "expression": "id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "copilot_chats_user_id_user_id_fk": { - "name": "copilot_chats_user_id_user_id_fk", - "tableFrom": "copilot_chats", - "tableTo": "user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "copilot_chats_workflow_id_workflow_id_fk": { - "name": "copilot_chats_workflow_id_workflow_id_fk", - "tableFrom": "copilot_chats", - "tableTo": "workflow", - "columnsFrom": ["workflow_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "copilot_chats_workspace_id_workspace_id_fk": { - "name": "copilot_chats_workspace_id_workspace_id_fk", - "tableFrom": "copilot_chats", - "tableTo": "workspace", - "columnsFrom": ["workspace_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.copilot_feedback": { - "name": "copilot_feedback", - "schema": "", - "columns": { - "feedback_id": { - "name": "feedback_id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "chat_id": { - "name": "chat_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "user_query": { - "name": "user_query", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "agent_response": { - "name": "agent_response", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "is_positive": { - "name": "is_positive", - "type": "boolean", - "primaryKey": false, - "notNull": true - }, - "feedback": { - "name": "feedback", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "workflow_yaml": { - "name": "workflow_yaml", - "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": { - "copilot_feedback_user_id_idx": { - "name": "copilot_feedback_user_id_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "copilot_feedback_chat_id_idx": { - "name": "copilot_feedback_chat_id_idx", - "columns": [ - { - "expression": "chat_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "copilot_feedback_user_chat_idx": { - "name": "copilot_feedback_user_chat_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "chat_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "copilot_feedback_is_positive_idx": { - "name": "copilot_feedback_is_positive_idx", - "columns": [ - { - "expression": "is_positive", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "copilot_feedback_created_at_idx": { - "name": "copilot_feedback_created_at_idx", - "columns": [ - { - "expression": "created_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "copilot_feedback_user_id_user_id_fk": { - "name": "copilot_feedback_user_id_user_id_fk", - "tableFrom": "copilot_feedback", - "tableTo": "user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "copilot_feedback_chat_id_copilot_chats_id_fk": { - "name": "copilot_feedback_chat_id_copilot_chats_id_fk", - "tableFrom": "copilot_feedback", - "tableTo": "copilot_chats", - "columnsFrom": ["chat_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.copilot_run_checkpoints": { - "name": "copilot_run_checkpoints", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "run_id": { - "name": "run_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "pending_tool_call_id": { - "name": "pending_tool_call_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "conversation_snapshot": { - "name": "conversation_snapshot", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'{}'" - }, - "agent_state": { - "name": "agent_state", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'{}'" - }, - "provider_request": { - "name": "provider_request", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "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": { - "copilot_run_checkpoints_run_id_idx": { - "name": "copilot_run_checkpoints_run_id_idx", - "columns": [ - { - "expression": "run_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "copilot_run_checkpoints_pending_tool_call_id_idx": { - "name": "copilot_run_checkpoints_pending_tool_call_id_idx", - "columns": [ - { - "expression": "pending_tool_call_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "copilot_run_checkpoints_run_pending_tool_unique": { - "name": "copilot_run_checkpoints_run_pending_tool_unique", - "columns": [ - { - "expression": "run_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "pending_tool_call_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "copilot_run_checkpoints_run_id_copilot_runs_id_fk": { - "name": "copilot_run_checkpoints_run_id_copilot_runs_id_fk", - "tableFrom": "copilot_run_checkpoints", - "tableTo": "copilot_runs", - "columnsFrom": ["run_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.copilot_runs": { - "name": "copilot_runs", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "execution_id": { - "name": "execution_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "parent_run_id": { - "name": "parent_run_id", - "type": "uuid", - "primaryKey": false, - "notNull": false - }, - "chat_id": { - "name": "chat_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "workflow_id": { - "name": "workflow_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "workspace_id": { - "name": "workspace_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "stream_id": { - "name": "stream_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "agent": { - "name": "agent", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "model": { - "name": "model", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "provider": { - "name": "provider", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "status": { - "name": "status", - "type": "copilot_run_status", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'active'" - }, - "request_context": { - "name": "request_context", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'{}'" - }, - "started_at": { - "name": "started_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "completed_at": { - "name": "completed_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()" - }, - "error": { - "name": "error", - "type": "text", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "copilot_runs_execution_id_idx": { - "name": "copilot_runs_execution_id_idx", - "columns": [ - { - "expression": "execution_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "copilot_runs_parent_run_id_idx": { - "name": "copilot_runs_parent_run_id_idx", - "columns": [ - { - "expression": "parent_run_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "copilot_runs_chat_id_idx": { - "name": "copilot_runs_chat_id_idx", - "columns": [ - { - "expression": "chat_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "copilot_runs_user_id_idx": { - "name": "copilot_runs_user_id_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "copilot_runs_workflow_id_idx": { - "name": "copilot_runs_workflow_id_idx", - "columns": [ - { - "expression": "workflow_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "copilot_runs_workspace_id_idx": { - "name": "copilot_runs_workspace_id_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "copilot_runs_status_idx": { - "name": "copilot_runs_status_idx", - "columns": [ - { - "expression": "status", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "copilot_runs_chat_execution_idx": { - "name": "copilot_runs_chat_execution_idx", - "columns": [ - { - "expression": "chat_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "execution_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "copilot_runs_execution_started_at_idx": { - "name": "copilot_runs_execution_started_at_idx", - "columns": [ - { - "expression": "execution_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "started_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "copilot_runs_workspace_completed_at_id_idx": { - "name": "copilot_runs_workspace_completed_at_id_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "date_trunc('milliseconds', \"completed_at\")", - "asc": true, - "isExpression": true, - "nulls": "last" - }, - { - "expression": "id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "copilot_runs_stream_id_unique": { - "name": "copilot_runs_stream_id_unique", - "columns": [ - { - "expression": "stream_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "copilot_runs_chat_id_copilot_chats_id_fk": { - "name": "copilot_runs_chat_id_copilot_chats_id_fk", - "tableFrom": "copilot_runs", - "tableTo": "copilot_chats", - "columnsFrom": ["chat_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "copilot_runs_user_id_user_id_fk": { - "name": "copilot_runs_user_id_user_id_fk", - "tableFrom": "copilot_runs", - "tableTo": "user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "copilot_runs_workflow_id_workflow_id_fk": { - "name": "copilot_runs_workflow_id_workflow_id_fk", - "tableFrom": "copilot_runs", - "tableTo": "workflow", - "columnsFrom": ["workflow_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "copilot_runs_workspace_id_workspace_id_fk": { - "name": "copilot_runs_workspace_id_workspace_id_fk", - "tableFrom": "copilot_runs", - "tableTo": "workspace", - "columnsFrom": ["workspace_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.copilot_workflow_read_hashes": { - "name": "copilot_workflow_read_hashes", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "chat_id": { - "name": "chat_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "workflow_id": { - "name": "workflow_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "hash": { - "name": "hash", - "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": { - "copilot_workflow_read_hashes_chat_id_idx": { - "name": "copilot_workflow_read_hashes_chat_id_idx", - "columns": [ - { - "expression": "chat_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "copilot_workflow_read_hashes_workflow_id_idx": { - "name": "copilot_workflow_read_hashes_workflow_id_idx", - "columns": [ - { - "expression": "workflow_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "copilot_workflow_read_hashes_chat_workflow_unique": { - "name": "copilot_workflow_read_hashes_chat_workflow_unique", - "columns": [ - { - "expression": "chat_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "workflow_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk": { - "name": "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk", - "tableFrom": "copilot_workflow_read_hashes", - "tableTo": "copilot_chats", - "columnsFrom": ["chat_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "copilot_workflow_read_hashes_workflow_id_workflow_id_fk": { - "name": "copilot_workflow_read_hashes_workflow_id_workflow_id_fk", - "tableFrom": "copilot_workflow_read_hashes", - "tableTo": "workflow", - "columnsFrom": ["workflow_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.credential": { - "name": "credential", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "workspace_id": { - "name": "workspace_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "type": { - "name": "type", - "type": "credential_type", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "provider_id": { - "name": "provider_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "account_id": { - "name": "account_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "env_key": { - "name": "env_key", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "env_owner_user_id": { - "name": "env_owner_user_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "encrypted_service_account_key": { - "name": "encrypted_service_account_key", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_by": { - "name": "created_by", - "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": { - "credential_workspace_id_idx": { - "name": "credential_workspace_id_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "credential_type_idx": { - "name": "credential_type_idx", - "columns": [ - { - "expression": "type", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "credential_provider_id_idx": { - "name": "credential_provider_id_idx", - "columns": [ - { - "expression": "provider_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "credential_account_id_idx": { - "name": "credential_account_id_idx", - "columns": [ - { - "expression": "account_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "credential_env_owner_user_id_idx": { - "name": "credential_env_owner_user_id_idx", - "columns": [ - { - "expression": "env_owner_user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "credential_workspace_account_unique": { - "name": "credential_workspace_account_unique", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "account_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "where": "account_id IS NOT NULL", - "concurrently": false, - "method": "btree", - "with": {} - }, - "credential_workspace_env_unique": { - "name": "credential_workspace_env_unique", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "type", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "env_key", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "where": "type = 'env_workspace'", - "concurrently": false, - "method": "btree", - "with": {} - }, - "credential_workspace_personal_env_unique": { - "name": "credential_workspace_personal_env_unique", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "type", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "env_key", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "env_owner_user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "where": "type = 'env_personal'", - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "credential_workspace_id_workspace_id_fk": { - "name": "credential_workspace_id_workspace_id_fk", - "tableFrom": "credential", - "tableTo": "workspace", - "columnsFrom": ["workspace_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "credential_account_id_account_id_fk": { - "name": "credential_account_id_account_id_fk", - "tableFrom": "credential", - "tableTo": "account", - "columnsFrom": ["account_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "credential_env_owner_user_id_user_id_fk": { - "name": "credential_env_owner_user_id_user_id_fk", - "tableFrom": "credential", - "tableTo": "user", - "columnsFrom": ["env_owner_user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "credential_created_by_user_id_fk": { - "name": "credential_created_by_user_id_fk", - "tableFrom": "credential", - "tableTo": "user", - "columnsFrom": ["created_by"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": { - "credential_oauth_source_check": { - "name": "credential_oauth_source_check", - "value": "(type <> 'oauth') OR (account_id IS NOT NULL AND provider_id IS NOT NULL)" - }, - "credential_workspace_env_source_check": { - "name": "credential_workspace_env_source_check", - "value": "(type <> 'env_workspace') OR (env_key IS NOT NULL AND env_owner_user_id IS NULL)" - }, - "credential_personal_env_source_check": { - "name": "credential_personal_env_source_check", - "value": "(type <> 'env_personal') OR (env_key IS NOT NULL AND env_owner_user_id IS NOT NULL)" - } - }, - "isRLSEnabled": false - }, - "public.credential_member": { - "name": "credential_member", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "credential_id": { - "name": "credential_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "role": { - "name": "role", - "type": "credential_member_role", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'member'" - }, - "status": { - "name": "status", - "type": "credential_member_status", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'active'" - }, - "joined_at": { - "name": "joined_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "invited_by": { - "name": "invited_by", - "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": { - "credential_member_user_id_idx": { - "name": "credential_member_user_id_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "credential_member_role_idx": { - "name": "credential_member_role_idx", - "columns": [ - { - "expression": "role", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "credential_member_status_idx": { - "name": "credential_member_status_idx", - "columns": [ - { - "expression": "status", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "credential_member_unique": { - "name": "credential_member_unique", - "columns": [ - { - "expression": "credential_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "credential_member_credential_id_credential_id_fk": { - "name": "credential_member_credential_id_credential_id_fk", - "tableFrom": "credential_member", - "tableTo": "credential", - "columnsFrom": ["credential_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "credential_member_user_id_user_id_fk": { - "name": "credential_member_user_id_user_id_fk", - "tableFrom": "credential_member", - "tableTo": "user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "credential_member_invited_by_user_id_fk": { - "name": "credential_member_invited_by_user_id_fk", - "tableFrom": "credential_member", - "tableTo": "user", - "columnsFrom": ["invited_by"], - "columnsTo": ["id"], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.credential_set": { - "name": "credential_set", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "organization_id": { - "name": "organization_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "provider_id": { - "name": "provider_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_by": { - "name": "created_by", - "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": { - "credential_set_created_by_idx": { - "name": "credential_set_created_by_idx", - "columns": [ - { - "expression": "created_by", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "credential_set_org_name_unique": { - "name": "credential_set_org_name_unique", - "columns": [ - { - "expression": "organization_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "name", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - }, - "credential_set_provider_id_idx": { - "name": "credential_set_provider_id_idx", - "columns": [ - { - "expression": "provider_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "credential_set_organization_id_organization_id_fk": { - "name": "credential_set_organization_id_organization_id_fk", - "tableFrom": "credential_set", - "tableTo": "organization", - "columnsFrom": ["organization_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "credential_set_created_by_user_id_fk": { - "name": "credential_set_created_by_user_id_fk", - "tableFrom": "credential_set", - "tableTo": "user", - "columnsFrom": ["created_by"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.credential_set_invitation": { - "name": "credential_set_invitation", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "credential_set_id": { - "name": "credential_set_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "token": { - "name": "token", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "invited_by": { - "name": "invited_by", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "status": { - "name": "status", - "type": "credential_set_invitation_status", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'pending'" - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "accepted_at": { - "name": "accepted_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "accepted_by_user_id": { - "name": "accepted_by_user_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "credential_set_invitation_set_id_idx": { - "name": "credential_set_invitation_set_id_idx", - "columns": [ - { - "expression": "credential_set_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "credential_set_invitation_token_idx": { - "name": "credential_set_invitation_token_idx", - "columns": [ - { - "expression": "token", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "credential_set_invitation_status_idx": { - "name": "credential_set_invitation_status_idx", - "columns": [ - { - "expression": "status", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "credential_set_invitation_expires_at_idx": { - "name": "credential_set_invitation_expires_at_idx", - "columns": [ - { - "expression": "expires_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "credential_set_invitation_credential_set_id_credential_set_id_fk": { - "name": "credential_set_invitation_credential_set_id_credential_set_id_fk", - "tableFrom": "credential_set_invitation", - "tableTo": "credential_set", - "columnsFrom": ["credential_set_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "credential_set_invitation_invited_by_user_id_fk": { - "name": "credential_set_invitation_invited_by_user_id_fk", - "tableFrom": "credential_set_invitation", - "tableTo": "user", - "columnsFrom": ["invited_by"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "credential_set_invitation_accepted_by_user_id_user_id_fk": { - "name": "credential_set_invitation_accepted_by_user_id_user_id_fk", - "tableFrom": "credential_set_invitation", - "tableTo": "user", - "columnsFrom": ["accepted_by_user_id"], - "columnsTo": ["id"], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "credential_set_invitation_token_unique": { - "name": "credential_set_invitation_token_unique", - "nullsNotDistinct": false, - "columns": ["token"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.credential_set_member": { - "name": "credential_set_member", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "credential_set_id": { - "name": "credential_set_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "status": { - "name": "status", - "type": "credential_set_member_status", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'pending'" - }, - "joined_at": { - "name": "joined_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "invited_by": { - "name": "invited_by", - "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": { - "credential_set_member_user_id_idx": { - "name": "credential_set_member_user_id_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "credential_set_member_unique": { - "name": "credential_set_member_unique", - "columns": [ - { - "expression": "credential_set_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - }, - "credential_set_member_status_idx": { - "name": "credential_set_member_status_idx", - "columns": [ - { - "expression": "status", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "credential_set_member_credential_set_id_credential_set_id_fk": { - "name": "credential_set_member_credential_set_id_credential_set_id_fk", - "tableFrom": "credential_set_member", - "tableTo": "credential_set", - "columnsFrom": ["credential_set_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "credential_set_member_user_id_user_id_fk": { - "name": "credential_set_member_user_id_user_id_fk", - "tableFrom": "credential_set_member", - "tableTo": "user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "credential_set_member_invited_by_user_id_fk": { - "name": "credential_set_member_invited_by_user_id_fk", - "tableFrom": "credential_set_member", - "tableTo": "user", - "columnsFrom": ["invited_by"], - "columnsTo": ["id"], - "onDelete": "set null", - "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 - }, - "workspace_id": { - "name": "workspace_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "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": { - "custom_tools_workspace_id_idx": { - "name": "custom_tools_workspace_id_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "custom_tools_workspace_title_unique": { - "name": "custom_tools_workspace_title_unique", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "title", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "custom_tools_workspace_id_workspace_id_fk": { - "name": "custom_tools_workspace_id_workspace_id_fk", - "tableFrom": "custom_tools", - "tableTo": "workspace", - "columnsFrom": ["workspace_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "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": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.data_drain_runs": { - "name": "data_drain_runs", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "drain_id": { - "name": "drain_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "status": { - "name": "status", - "type": "data_drain_run_status", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "trigger": { - "name": "trigger", - "type": "data_drain_run_trigger", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "started_at": { - "name": "started_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "finished_at": { - "name": "finished_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "rows_exported": { - "name": "rows_exported", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "bytes_written": { - "name": "bytes_written", - "type": "bigint", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "cursor_before": { - "name": "cursor_before", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "cursor_after": { - "name": "cursor_after", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "error": { - "name": "error", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "locators": { - "name": "locators", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'[]'::jsonb" - } - }, - "indexes": { - "data_drain_runs_drain_started_idx": { - "name": "data_drain_runs_drain_started_idx", - "columns": [ - { - "expression": "drain_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "started_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "data_drain_runs_drain_id_data_drains_id_fk": { - "name": "data_drain_runs_drain_id_data_drains_id_fk", - "tableFrom": "data_drain_runs", - "tableTo": "data_drains", - "columnsFrom": ["drain_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.data_drains": { - "name": "data_drains", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "organization_id": { - "name": "organization_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "source": { - "name": "source", - "type": "data_drain_source", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "destination_type": { - "name": "destination_type", - "type": "data_drain_destination", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "destination_config": { - "name": "destination_config", - "type": "jsonb", - "primaryKey": false, - "notNull": true - }, - "destination_credentials": { - "name": "destination_credentials", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "schedule_cadence": { - "name": "schedule_cadence", - "type": "data_drain_cadence", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "enabled": { - "name": "enabled", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "cursor": { - "name": "cursor", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "last_run_at": { - "name": "last_run_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "last_success_at": { - "name": "last_success_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "created_by": { - "name": "created_by", - "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": { - "data_drains_org_idx": { - "name": "data_drains_org_idx", - "columns": [ - { - "expression": "organization_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "data_drains_due_idx": { - "name": "data_drains_due_idx", - "columns": [ - { - "expression": "enabled", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "last_run_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "data_drains_org_name_unique": { - "name": "data_drains_org_name_unique", - "columns": [ - { - "expression": "organization_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "name", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "data_drains_organization_id_organization_id_fk": { - "name": "data_drains_organization_id_organization_id_fk", - "tableFrom": "data_drains", - "tableTo": "organization", - "columnsFrom": ["organization_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "data_drains_created_by_user_id_fk": { - "name": "data_drains_created_by_user_id_fk", - "tableFrom": "data_drains", - "tableTo": "user", - "columnsFrom": ["created_by"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.docs_embeddings": { - "name": "docs_embeddings", - "schema": "", - "columns": { - "chunk_id": { - "name": "chunk_id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "chunk_text": { - "name": "chunk_text", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "source_document": { - "name": "source_document", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "source_link": { - "name": "source_link", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "header_text": { - "name": "header_text", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "header_level": { - "name": "header_level", - "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": true - }, - "embedding_model": { - "name": "embedding_model", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'text-embedding-3-small'" - }, - "metadata": { - "name": "metadata", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'{}'" - }, - "chunk_text_tsv": { - "name": "chunk_text_tsv", - "type": "tsvector", - "primaryKey": false, - "notNull": false, - "generated": { - "as": "to_tsvector('english', \"docs_embeddings\".\"chunk_text\")", - "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": { - "docs_emb_source_document_idx": { - "name": "docs_emb_source_document_idx", - "columns": [ - { - "expression": "source_document", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "docs_emb_header_level_idx": { - "name": "docs_emb_header_level_idx", - "columns": [ - { - "expression": "header_level", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "docs_emb_source_header_idx": { - "name": "docs_emb_source_header_idx", - "columns": [ - { - "expression": "source_document", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "header_level", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "docs_emb_model_idx": { - "name": "docs_emb_model_idx", - "columns": [ - { - "expression": "embedding_model", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "docs_emb_created_at_idx": { - "name": "docs_emb_created_at_idx", - "columns": [ - { - "expression": "created_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "docs_embedding_vector_hnsw_idx": { - "name": "docs_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 - } - }, - "docs_emb_metadata_gin_idx": { - "name": "docs_emb_metadata_gin_idx", - "columns": [ - { - "expression": "metadata", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "gin", - "with": {} - }, - "docs_emb_chunk_text_fts_idx": { - "name": "docs_emb_chunk_text_fts_idx", - "columns": [ - { - "expression": "chunk_text_tsv", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "gin", - "with": {} - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": { - "docs_embedding_not_null_check": { - "name": "docs_embedding_not_null_check", - "value": "\"embedding\" IS NOT NULL" - }, - "docs_header_level_check": { - "name": "docs_header_level_check", - "value": "\"header_level\" >= 1 AND \"header_level\" <= 6" - } - }, - "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 - }, - "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 - }, - "archived_at": { - "name": "archived_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "deleted_at": { - "name": "deleted_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "user_excluded": { - "name": "user_excluded", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "tag1": { - "name": "tag1", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "tag2": { - "name": "tag2", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "tag3": { - "name": "tag3", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "tag4": { - "name": "tag4", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "tag5": { - "name": "tag5", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "tag6": { - "name": "tag6", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "tag7": { - "name": "tag7", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "number1": { - "name": "number1", - "type": "double precision", - "primaryKey": false, - "notNull": false - }, - "number2": { - "name": "number2", - "type": "double precision", - "primaryKey": false, - "notNull": false - }, - "number3": { - "name": "number3", - "type": "double precision", - "primaryKey": false, - "notNull": false - }, - "number4": { - "name": "number4", - "type": "double precision", - "primaryKey": false, - "notNull": false - }, - "number5": { - "name": "number5", - "type": "double precision", - "primaryKey": false, - "notNull": false - }, - "date1": { - "name": "date1", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "date2": { - "name": "date2", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "boolean1": { - "name": "boolean1", - "type": "boolean", - "primaryKey": false, - "notNull": false - }, - "boolean2": { - "name": "boolean2", - "type": "boolean", - "primaryKey": false, - "notNull": false - }, - "boolean3": { - "name": "boolean3", - "type": "boolean", - "primaryKey": false, - "notNull": false - }, - "connector_id": { - "name": "connector_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "external_id": { - "name": "external_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "content_hash": { - "name": "content_hash", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "source_url": { - "name": "source_url", - "type": "text", - "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_filename_idx": { - "name": "doc_filename_idx", - "columns": [ - { - "expression": "filename", - "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": {} - }, - "doc_connector_external_id_idx": { - "name": "doc_connector_external_id_idx", - "columns": [ - { - "expression": "connector_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "external_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "where": "\"document\".\"deleted_at\" IS NULL", - "concurrently": false, - "method": "btree", - "with": {} - }, - "doc_connector_id_idx": { - "name": "doc_connector_id_idx", - "columns": [ - { - "expression": "connector_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "doc_archived_at_partial_idx": { - "name": "doc_archived_at_partial_idx", - "columns": [ - { - "expression": "archived_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "where": "\"document\".\"archived_at\" IS NOT NULL", - "concurrently": false, - "method": "btree", - "with": {} - }, - "doc_deleted_at_partial_idx": { - "name": "doc_deleted_at_partial_idx", - "columns": [ - { - "expression": "deleted_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "where": "\"document\".\"deleted_at\" IS NOT NULL", - "concurrently": false, - "method": "btree", - "with": {} - }, - "doc_tag1_idx": { - "name": "doc_tag1_idx", - "columns": [ - { - "expression": "tag1", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "doc_tag2_idx": { - "name": "doc_tag2_idx", - "columns": [ - { - "expression": "tag2", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "doc_tag3_idx": { - "name": "doc_tag3_idx", - "columns": [ - { - "expression": "tag3", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "doc_tag4_idx": { - "name": "doc_tag4_idx", - "columns": [ - { - "expression": "tag4", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "doc_tag5_idx": { - "name": "doc_tag5_idx", - "columns": [ - { - "expression": "tag5", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "doc_tag6_idx": { - "name": "doc_tag6_idx", - "columns": [ - { - "expression": "tag6", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "doc_tag7_idx": { - "name": "doc_tag7_idx", - "columns": [ - { - "expression": "tag7", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "doc_number1_idx": { - "name": "doc_number1_idx", - "columns": [ - { - "expression": "number1", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "doc_number2_idx": { - "name": "doc_number2_idx", - "columns": [ - { - "expression": "number2", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "doc_number3_idx": { - "name": "doc_number3_idx", - "columns": [ - { - "expression": "number3", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "doc_number4_idx": { - "name": "doc_number4_idx", - "columns": [ - { - "expression": "number4", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "doc_number5_idx": { - "name": "doc_number5_idx", - "columns": [ - { - "expression": "number5", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "doc_date1_idx": { - "name": "doc_date1_idx", - "columns": [ - { - "expression": "date1", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "doc_date2_idx": { - "name": "doc_date2_idx", - "columns": [ - { - "expression": "date2", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "doc_boolean1_idx": { - "name": "doc_boolean1_idx", - "columns": [ - { - "expression": "boolean1", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "doc_boolean2_idx": { - "name": "doc_boolean2_idx", - "columns": [ - { - "expression": "boolean2", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "doc_boolean3_idx": { - "name": "doc_boolean3_idx", - "columns": [ - { - "expression": "boolean3", - "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" - }, - "document_connector_id_knowledge_connector_id_fk": { - "name": "document_connector_id_knowledge_connector_id_fk", - "tableFrom": "document", - "tableTo": "knowledge_connector", - "columnsFrom": ["connector_id"], - "columnsTo": ["id"], - "onDelete": "set null", - "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 - }, - "tag1": { - "name": "tag1", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "tag2": { - "name": "tag2", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "tag3": { - "name": "tag3", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "tag4": { - "name": "tag4", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "tag5": { - "name": "tag5", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "tag6": { - "name": "tag6", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "tag7": { - "name": "tag7", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "number1": { - "name": "number1", - "type": "double precision", - "primaryKey": false, - "notNull": false - }, - "number2": { - "name": "number2", - "type": "double precision", - "primaryKey": false, - "notNull": false - }, - "number3": { - "name": "number3", - "type": "double precision", - "primaryKey": false, - "notNull": false - }, - "number4": { - "name": "number4", - "type": "double precision", - "primaryKey": false, - "notNull": false - }, - "number5": { - "name": "number5", - "type": "double precision", - "primaryKey": false, - "notNull": false - }, - "date1": { - "name": "date1", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "date2": { - "name": "date2", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "boolean1": { - "name": "boolean1", - "type": "boolean", - "primaryKey": false, - "notNull": false - }, - "boolean2": { - "name": "boolean2", - "type": "boolean", - "primaryKey": false, - "notNull": false - }, - "boolean3": { - "name": "boolean3", - "type": "boolean", - "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_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_tag1_idx": { - "name": "emb_tag1_idx", - "columns": [ - { - "expression": "tag1", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "emb_tag2_idx": { - "name": "emb_tag2_idx", - "columns": [ - { - "expression": "tag2", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "emb_tag3_idx": { - "name": "emb_tag3_idx", - "columns": [ - { - "expression": "tag3", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "emb_tag4_idx": { - "name": "emb_tag4_idx", - "columns": [ - { - "expression": "tag4", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "emb_tag5_idx": { - "name": "emb_tag5_idx", - "columns": [ - { - "expression": "tag5", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "emb_tag6_idx": { - "name": "emb_tag6_idx", - "columns": [ - { - "expression": "tag6", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "emb_tag7_idx": { - "name": "emb_tag7_idx", - "columns": [ - { - "expression": "tag7", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "emb_number1_idx": { - "name": "emb_number1_idx", - "columns": [ - { - "expression": "number1", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "emb_number2_idx": { - "name": "emb_number2_idx", - "columns": [ - { - "expression": "number2", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "emb_number3_idx": { - "name": "emb_number3_idx", - "columns": [ - { - "expression": "number3", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "emb_number4_idx": { - "name": "emb_number4_idx", - "columns": [ - { - "expression": "number4", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "emb_number5_idx": { - "name": "emb_number5_idx", - "columns": [ - { - "expression": "number5", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "emb_date1_idx": { - "name": "emb_date1_idx", - "columns": [ - { - "expression": "date1", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "emb_date2_idx": { - "name": "emb_date2_idx", - "columns": [ - { - "expression": "date2", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "emb_boolean1_idx": { - "name": "emb_boolean1_idx", - "columns": [ - { - "expression": "boolean1", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "emb_boolean2_idx": { - "name": "emb_boolean2_idx", - "columns": [ - { - "expression": "boolean2", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "emb_boolean3_idx": { - "name": "emb_boolean3_idx", - "columns": [ - { - "expression": "boolean3", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "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.form": { - "name": "form", - "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 - }, - "identifier": { - "name": "identifier", - "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": "'[]'" - }, - "show_branding": { - "name": "show_branding", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "archived_at": { - "name": "archived_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": { - "form_identifier_idx": { - "name": "form_identifier_idx", - "columns": [ - { - "expression": "identifier", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "where": "\"form\".\"archived_at\" IS NULL", - "concurrently": false, - "method": "btree", - "with": {} - }, - "form_workflow_id_idx": { - "name": "form_workflow_id_idx", - "columns": [ - { - "expression": "workflow_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "form_user_id_idx": { - "name": "form_user_id_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "form_archived_at_partial_idx": { - "name": "form_archived_at_partial_idx", - "columns": [ - { - "expression": "archived_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "where": "\"form\".\"archived_at\" IS NOT NULL", - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "form_workflow_id_workflow_id_fk": { - "name": "form_workflow_id_workflow_id_fk", - "tableFrom": "form", - "tableTo": "workflow", - "columnsFrom": ["workflow_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "form_user_id_user_id_fk": { - "name": "form_user_id_user_id_fk", - "tableFrom": "form", - "tableTo": "user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.idempotency_key": { - "name": "idempotency_key", - "schema": "", - "columns": { - "key": { - "name": "key", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "result": { - "name": "result", - "type": "json", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "idempotency_key_created_at_idx": { - "name": "idempotency_key_created_at_idx", - "columns": [ - { - "expression": "created_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.invitation": { - "name": "invitation", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "kind": { - "name": "kind", - "type": "invitation_kind", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'organization'" - }, - "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": false - }, - "membership_intent": { - "name": "membership_intent", - "type": "invitation_membership_intent", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'internal'" - }, - "role": { - "name": "role", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "status": { - "name": "status", - "type": "invitation_status", - "typeSchema": "public", - "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": { - "invitation_email_idx": { - "name": "invitation_email_idx", - "columns": [ - { - "expression": "email", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "invitation_organization_id_idx": { - "name": "invitation_organization_id_idx", - "columns": [ - { - "expression": "organization_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "invitation_status_idx": { - "name": "invitation_status_idx", - "columns": [ - { - "expression": "status", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "invitation_pending_email_org_unique": { - "name": "invitation_pending_email_org_unique", - "columns": [ - { - "expression": "email", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "organization_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "where": "\"invitation\".\"status\" = 'pending' AND \"invitation\".\"organization_id\" IS NOT NULL", - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "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": { - "invitation_token_unique": { - "name": "invitation_token_unique", - "nullsNotDistinct": false, - "columns": ["token"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.invitation_workspace_grant": { - "name": "invitation_workspace_grant", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "invitation_id": { - "name": "invitation_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "workspace_id": { - "name": "workspace_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "permission": { - "name": "permission", - "type": "permission_type", - "typeSchema": "public", - "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": { - "invitation_workspace_grant_unique": { - "name": "invitation_workspace_grant_unique", - "columns": [ - { - "expression": "invitation_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - }, - "invitation_workspace_grant_workspace_id_idx": { - "name": "invitation_workspace_grant_workspace_id_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "invitation_workspace_grant_invitation_id_invitation_id_fk": { - "name": "invitation_workspace_grant_invitation_id_invitation_id_fk", - "tableFrom": "invitation_workspace_grant", - "tableTo": "invitation", - "columnsFrom": ["invitation_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "invitation_workspace_grant_workspace_id_workspace_id_fk": { - "name": "invitation_workspace_grant_workspace_id_workspace_id_fk", - "tableFrom": "invitation_workspace_grant", - "tableTo": "workspace", - "columnsFrom": ["workspace_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.job_execution_logs": { - "name": "job_execution_logs", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "schedule_id": { - "name": "schedule_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "workspace_id": { - "name": "workspace_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "execution_id": { - "name": "execution_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "level": { - "name": "level", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'running'" - }, - "trigger": { - "name": "trigger", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "started_at": { - "name": "started_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "ended_at": { - "name": "ended_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "total_duration_ms": { - "name": "total_duration_ms", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "execution_data": { - "name": "execution_data", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'{}'" - }, - "cost": { - "name": "cost", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "job_execution_logs_schedule_id_idx": { - "name": "job_execution_logs_schedule_id_idx", - "columns": [ - { - "expression": "schedule_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "job_execution_logs_workspace_started_at_idx": { - "name": "job_execution_logs_workspace_started_at_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "started_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "job_execution_logs_workspace_ended_at_id_idx": { - "name": "job_execution_logs_workspace_ended_at_id_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "date_trunc('milliseconds', \"ended_at\")", - "asc": true, - "isExpression": true, - "nulls": "last" - }, - { - "expression": "id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "job_execution_logs_execution_id_unique": { - "name": "job_execution_logs_execution_id_unique", - "columns": [ - { - "expression": "execution_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - }, - "job_execution_logs_trigger_idx": { - "name": "job_execution_logs_trigger_idx", - "columns": [ - { - "expression": "trigger", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "job_execution_logs_schedule_id_workflow_schedule_id_fk": { - "name": "job_execution_logs_schedule_id_workflow_schedule_id_fk", - "tableFrom": "job_execution_logs", - "tableTo": "workflow_schedule", - "columnsFrom": ["schedule_id"], - "columnsTo": ["id"], - "onDelete": "set null", - "onUpdate": "no action" - }, - "job_execution_logs_workspace_id_workspace_id_fk": { - "name": "job_execution_logs_workspace_id_workspace_id_fk", - "tableFrom": "job_execution_logs", - "tableTo": "workspace", - "columnsFrom": ["workspace_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.jwks": { - "name": "jwks", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "public_key": { - "name": "public_key", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "private_key": { - "name": "private_key", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": {}, - "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\": 1, \"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": {} - }, - "kb_workspace_deleted_partial_idx": { - "name": "kb_workspace_deleted_partial_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "deleted_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "where": "\"knowledge_base\".\"deleted_at\" IS NOT NULL", - "concurrently": false, - "method": "btree", - "with": {} - }, - "kb_workspace_name_active_unique": { - "name": "kb_workspace_name_active_unique", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "name", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "where": "\"knowledge_base\".\"deleted_at\" IS NULL", - "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": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.knowledge_base_tag_definitions": { - "name": "knowledge_base_tag_definitions", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "knowledge_base_id": { - "name": "knowledge_base_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "tag_slot": { - "name": "tag_slot", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "field_type": { - "name": "field_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'text'" - }, - "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_tag_definitions_kb_slot_idx": { - "name": "kb_tag_definitions_kb_slot_idx", - "columns": [ - { - "expression": "knowledge_base_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "tag_slot", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - }, - "kb_tag_definitions_kb_display_name_idx": { - "name": "kb_tag_definitions_kb_display_name_idx", - "columns": [ - { - "expression": "knowledge_base_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "display_name", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - }, - "kb_tag_definitions_kb_id_idx": { - "name": "kb_tag_definitions_kb_id_idx", - "columns": [ - { - "expression": "knowledge_base_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk": { - "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk", - "tableFrom": "knowledge_base_tag_definitions", - "tableTo": "knowledge_base", - "columnsFrom": ["knowledge_base_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.knowledge_connector": { - "name": "knowledge_connector", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "knowledge_base_id": { - "name": "knowledge_base_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "connector_type": { - "name": "connector_type", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "credential_id": { - "name": "credential_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "encrypted_api_key": { - "name": "encrypted_api_key", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "source_config": { - "name": "source_config", - "type": "json", - "primaryKey": false, - "notNull": true - }, - "sync_mode": { - "name": "sync_mode", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'full'" - }, - "sync_interval_minutes": { - "name": "sync_interval_minutes", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 1440 - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'active'" - }, - "last_sync_at": { - "name": "last_sync_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "last_sync_error": { - "name": "last_sync_error", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "last_sync_doc_count": { - "name": "last_sync_doc_count", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "next_sync_at": { - "name": "next_sync_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "consecutive_failures": { - "name": "consecutive_failures", - "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()" - }, - "archived_at": { - "name": "archived_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "deleted_at": { - "name": "deleted_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "kc_knowledge_base_id_idx": { - "name": "kc_knowledge_base_id_idx", - "columns": [ - { - "expression": "knowledge_base_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "kc_status_next_sync_idx": { - "name": "kc_status_next_sync_idx", - "columns": [ - { - "expression": "status", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "next_sync_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "kc_archived_at_partial_idx": { - "name": "kc_archived_at_partial_idx", - "columns": [ - { - "expression": "archived_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "where": "\"knowledge_connector\".\"archived_at\" IS NOT NULL", - "concurrently": false, - "method": "btree", - "with": {} - }, - "kc_deleted_at_partial_idx": { - "name": "kc_deleted_at_partial_idx", - "columns": [ - { - "expression": "deleted_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "where": "\"knowledge_connector\".\"deleted_at\" IS NOT NULL", - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "knowledge_connector_knowledge_base_id_knowledge_base_id_fk": { - "name": "knowledge_connector_knowledge_base_id_knowledge_base_id_fk", - "tableFrom": "knowledge_connector", - "tableTo": "knowledge_base", - "columnsFrom": ["knowledge_base_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.knowledge_connector_sync_log": { - "name": "knowledge_connector_sync_log", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "connector_id": { - "name": "connector_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "started_at": { - "name": "started_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "completed_at": { - "name": "completed_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "docs_added": { - "name": "docs_added", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "docs_updated": { - "name": "docs_updated", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "docs_deleted": { - "name": "docs_deleted", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "docs_unchanged": { - "name": "docs_unchanged", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "docs_failed": { - "name": "docs_failed", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "error_message": { - "name": "error_message", - "type": "text", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "kcsl_connector_id_idx": { - "name": "kcsl_connector_id_idx", - "columns": [ - { - "expression": "connector_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk": { - "name": "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk", - "tableFrom": "knowledge_connector_sync_log", - "tableTo": "knowledge_connector", - "columnsFrom": ["connector_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.mcp_servers": { - "name": "mcp_servers", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "workspace_id": { - "name": "workspace_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_by": { - "name": "created_by", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "transport": { - "name": "transport", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "url": { - "name": "url", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "headers": { - "name": "headers", - "type": "json", - "primaryKey": false, - "notNull": false, - "default": "'{}'" - }, - "timeout": { - "name": "timeout", - "type": "integer", - "primaryKey": false, - "notNull": false, - "default": 30000 - }, - "retries": { - "name": "retries", - "type": "integer", - "primaryKey": false, - "notNull": false, - "default": 3 - }, - "enabled": { - "name": "enabled", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "last_connected": { - "name": "last_connected", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "connection_status": { - "name": "connection_status", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "'disconnected'" - }, - "last_error": { - "name": "last_error", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "status_config": { - "name": "status_config", - "type": "jsonb", - "primaryKey": false, - "notNull": false, - "default": "'{}'" - }, - "tool_count": { - "name": "tool_count", - "type": "integer", - "primaryKey": false, - "notNull": false, - "default": 0 - }, - "last_tools_refresh": { - "name": "last_tools_refresh", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "total_requests": { - "name": "total_requests", - "type": "integer", - "primaryKey": false, - "notNull": false, - "default": 0 - }, - "last_used": { - "name": "last_used", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "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": { - "mcp_servers_workspace_enabled_idx": { - "name": "mcp_servers_workspace_enabled_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "enabled", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "mcp_servers_workspace_deleted_partial_idx": { - "name": "mcp_servers_workspace_deleted_partial_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "deleted_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "where": "\"mcp_servers\".\"deleted_at\" IS NOT NULL", - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "mcp_servers_workspace_id_workspace_id_fk": { - "name": "mcp_servers_workspace_id_workspace_id_fk", - "tableFrom": "mcp_servers", - "tableTo": "workspace", - "columnsFrom": ["workspace_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "mcp_servers_created_by_user_id_fk": { - "name": "mcp_servers_created_by_user_id_fk", - "tableFrom": "mcp_servers", - "tableTo": "user", - "columnsFrom": ["created_by"], - "columnsTo": ["id"], - "onDelete": "set null", - "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": { - "member_user_id_unique": { - "name": "member_user_id_unique", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - }, - "member_organization_id_idx": { - "name": "member_organization_id_idx", - "columns": [ - { - "expression": "organization_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "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 - }, - "workspace_id": { - "name": "workspace_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "key": { - "name": "key", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "data": { - "name": "data", - "type": "jsonb", - "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_workspace_idx": { - "name": "memory_workspace_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "memory_workspace_key_idx": { - "name": "memory_workspace_key_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "key", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - }, - "memory_workspace_deleted_partial_idx": { - "name": "memory_workspace_deleted_partial_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "deleted_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "where": "\"memory\".\"deleted_at\" IS NOT NULL", - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "memory_workspace_id_workspace_id_fk": { - "name": "memory_workspace_id_workspace_id_fk", - "tableFrom": "memory", - "tableTo": "workspace", - "columnsFrom": ["workspace_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.mothership_inbox_allowed_sender": { - "name": "mothership_inbox_allowed_sender", - "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 - }, - "label": { - "name": "label", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "added_by": { - "name": "added_by", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "inbox_sender_ws_email_idx": { - "name": "inbox_sender_ws_email_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "email", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk": { - "name": "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk", - "tableFrom": "mothership_inbox_allowed_sender", - "tableTo": "workspace", - "columnsFrom": ["workspace_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "mothership_inbox_allowed_sender_added_by_user_id_fk": { - "name": "mothership_inbox_allowed_sender_added_by_user_id_fk", - "tableFrom": "mothership_inbox_allowed_sender", - "tableTo": "user", - "columnsFrom": ["added_by"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.mothership_inbox_task": { - "name": "mothership_inbox_task", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "workspace_id": { - "name": "workspace_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "from_email": { - "name": "from_email", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "from_name": { - "name": "from_name", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "subject": { - "name": "subject", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "body_preview": { - "name": "body_preview", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "body_text": { - "name": "body_text", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "body_html": { - "name": "body_html", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "email_message_id": { - "name": "email_message_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "in_reply_to": { - "name": "in_reply_to", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "response_message_id": { - "name": "response_message_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "agentmail_message_id": { - "name": "agentmail_message_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'received'" - }, - "chat_id": { - "name": "chat_id", - "type": "uuid", - "primaryKey": false, - "notNull": false - }, - "trigger_job_id": { - "name": "trigger_job_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "result_summary": { - "name": "result_summary", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "error_message": { - "name": "error_message", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "rejection_reason": { - "name": "rejection_reason", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "has_attachments": { - "name": "has_attachments", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "cc_recipients": { - "name": "cc_recipients", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "processing_started_at": { - "name": "processing_started_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "completed_at": { - "name": "completed_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "inbox_task_ws_created_at_idx": { - "name": "inbox_task_ws_created_at_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "created_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "inbox_task_ws_status_idx": { - "name": "inbox_task_ws_status_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "status", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "inbox_task_response_msg_id_idx": { - "name": "inbox_task_response_msg_id_idx", - "columns": [ - { - "expression": "response_message_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "inbox_task_email_msg_id_idx": { - "name": "inbox_task_email_msg_id_idx", - "columns": [ - { - "expression": "email_message_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "mothership_inbox_task_workspace_id_workspace_id_fk": { - "name": "mothership_inbox_task_workspace_id_workspace_id_fk", - "tableFrom": "mothership_inbox_task", - "tableTo": "workspace", - "columnsFrom": ["workspace_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "mothership_inbox_task_chat_id_copilot_chats_id_fk": { - "name": "mothership_inbox_task_chat_id_copilot_chats_id_fk", - "tableFrom": "mothership_inbox_task", - "tableTo": "copilot_chats", - "columnsFrom": ["chat_id"], - "columnsTo": ["id"], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.mothership_inbox_webhook": { - "name": "mothership_inbox_webhook", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "workspace_id": { - "name": "workspace_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "webhook_id": { - "name": "webhook_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "secret": { - "name": "secret", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "mothership_inbox_webhook_workspace_id_workspace_id_fk": { - "name": "mothership_inbox_webhook_workspace_id_workspace_id_fk", - "tableFrom": "mothership_inbox_webhook", - "tableTo": "workspace", - "columnsFrom": ["workspace_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "mothership_inbox_webhook_workspace_id_unique": { - "name": "mothership_inbox_webhook_workspace_id_unique", - "nullsNotDistinct": false, - "columns": ["workspace_id"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.mothership_settings": { - "name": "mothership_settings", - "schema": "", - "columns": { - "workspace_id": { - "name": "workspace_id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "mcp_tool_refs": { - "name": "mcp_tool_refs", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'[]'::jsonb" - }, - "custom_tool_refs": { - "name": "custom_tool_refs", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'[]'::jsonb" - }, - "skill_refs": { - "name": "skill_refs", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'[]'::jsonb" - }, - "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": { - "mothership_settings_workspace_id_idx": { - "name": "mothership_settings_workspace_id_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "mothership_settings_workspace_id_workspace_id_fk": { - "name": "mothership_settings_workspace_id_workspace_id_fk", - "tableFrom": "mothership_settings", - "tableTo": "workspace", - "columnsFrom": ["workspace_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.oauth_access_token": { - "name": "oauth_access_token", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "access_token": { - "name": "access_token", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "refresh_token": { - "name": "refresh_token", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "access_token_expires_at": { - "name": "access_token_expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "refresh_token_expires_at": { - "name": "refresh_token_expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "client_id": { - "name": "client_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "scopes": { - "name": "scopes", - "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 - } - }, - "indexes": { - "oauth_access_token_access_token_idx": { - "name": "oauth_access_token_access_token_idx", - "columns": [ - { - "expression": "access_token", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "oauth_access_token_refresh_token_idx": { - "name": "oauth_access_token_refresh_token_idx", - "columns": [ - { - "expression": "refresh_token", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "oauth_access_token_client_id_oauth_application_client_id_fk": { - "name": "oauth_access_token_client_id_oauth_application_client_id_fk", - "tableFrom": "oauth_access_token", - "tableTo": "oauth_application", - "columnsFrom": ["client_id"], - "columnsTo": ["client_id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "oauth_access_token_user_id_user_id_fk": { - "name": "oauth_access_token_user_id_user_id_fk", - "tableFrom": "oauth_access_token", - "tableTo": "user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "oauth_access_token_access_token_unique": { - "name": "oauth_access_token_access_token_unique", - "nullsNotDistinct": false, - "columns": ["access_token"] - }, - "oauth_access_token_refresh_token_unique": { - "name": "oauth_access_token_refresh_token_unique", - "nullsNotDistinct": false, - "columns": ["refresh_token"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.oauth_application": { - "name": "oauth_application", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "icon": { - "name": "icon", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "metadata": { - "name": "metadata", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "client_id": { - "name": "client_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "client_secret": { - "name": "client_secret", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "redirect_urls": { - "name": "redirect_urls", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "disabled": { - "name": "disabled", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": false - }, - "user_id": { - "name": "user_id", - "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": { - "oauth_application_client_id_idx": { - "name": "oauth_application_client_id_idx", - "columns": [ - { - "expression": "client_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "oauth_application_user_id_user_id_fk": { - "name": "oauth_application_user_id_user_id_fk", - "tableFrom": "oauth_application", - "tableTo": "user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "oauth_application_client_id_unique": { - "name": "oauth_application_client_id_unique", - "nullsNotDistinct": false, - "columns": ["client_id"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.oauth_consent": { - "name": "oauth_consent", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "client_id": { - "name": "client_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "scopes": { - "name": "scopes", - "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 - }, - "consent_given": { - "name": "consent_given", - "type": "boolean", - "primaryKey": false, - "notNull": true - } - }, - "indexes": { - "oauth_consent_user_client_idx": { - "name": "oauth_consent_user_client_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "client_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "oauth_consent_client_id_oauth_application_client_id_fk": { - "name": "oauth_consent_client_id_oauth_application_client_id_fk", - "tableFrom": "oauth_consent", - "tableTo": "oauth_application", - "columnsFrom": ["client_id"], - "columnsTo": ["client_id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "oauth_consent_user_id_user_id_fk": { - "name": "oauth_consent_user_id_user_id_fk", - "tableFrom": "oauth_consent", - "tableTo": "user", - "columnsFrom": ["user_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 - }, - "whitelabel_settings": { - "name": "whitelabel_settings", - "type": "json", - "primaryKey": false, - "notNull": false - }, - "data_retention_settings": { - "name": "data_retention_settings", - "type": "json", - "primaryKey": false, - "notNull": false - }, - "org_usage_limit": { - "name": "org_usage_limit", - "type": "numeric", - "primaryKey": false, - "notNull": false - }, - "storage_used_bytes": { - "name": "storage_used_bytes", - "type": "bigint", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "departed_member_usage": { - "name": "departed_member_usage", - "type": "numeric", - "primaryKey": false, - "notNull": true, - "default": "'0'" - }, - "credit_balance": { - "name": "credit_balance", - "type": "numeric", - "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": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.outbox_event": { - "name": "outbox_event", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "event_type": { - "name": "event_type", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "payload": { - "name": "payload", - "type": "json", - "primaryKey": false, - "notNull": true - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'pending'" - }, - "attempts": { - "name": "attempts", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "max_attempts": { - "name": "max_attempts", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 10 - }, - "available_at": { - "name": "available_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "locked_at": { - "name": "locked_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "last_error": { - "name": "last_error", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "processed_at": { - "name": "processed_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "outbox_event_status_available_idx": { - "name": "outbox_event_status_available_idx", - "columns": [ - { - "expression": "status", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "available_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "outbox_event_locked_at_idx": { - "name": "outbox_event_locked_at_idx", - "columns": [ - { - "expression": "locked_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.paused_executions": { - "name": "paused_executions", - "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": true - }, - "execution_snapshot": { - "name": "execution_snapshot", - "type": "jsonb", - "primaryKey": false, - "notNull": true - }, - "pause_points": { - "name": "pause_points", - "type": "jsonb", - "primaryKey": false, - "notNull": true - }, - "total_pause_count": { - "name": "total_pause_count", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "resumed_count": { - "name": "resumed_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'paused'" - }, - "metadata": { - "name": "metadata", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'{}'::jsonb" - }, - "paused_at": { - "name": "paused_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 - }, - "next_resume_at": { - "name": "next_resume_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "paused_executions_workflow_id_idx": { - "name": "paused_executions_workflow_id_idx", - "columns": [ - { - "expression": "workflow_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "paused_executions_status_idx": { - "name": "paused_executions_status_idx", - "columns": [ - { - "expression": "status", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "paused_executions_execution_id_unique": { - "name": "paused_executions_execution_id_unique", - "columns": [ - { - "expression": "execution_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - }, - "paused_executions_next_resume_at_idx": { - "name": "paused_executions_next_resume_at_idx", - "columns": [ - { - "expression": "next_resume_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "where": "status = 'paused' AND next_resume_at IS NOT NULL", - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "paused_executions_workflow_id_workflow_id_fk": { - "name": "paused_executions_workflow_id_workflow_id_fk", - "tableFrom": "paused_executions", - "tableTo": "workflow", - "columnsFrom": ["workflow_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.pending_credential_draft": { - "name": "pending_credential_draft", - "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": true - }, - "provider_id": { - "name": "provider_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "credential_id": { - "name": "credential_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "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": { - "pending_draft_user_provider_ws": { - "name": "pending_draft_user_provider_ws", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "provider_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "pending_credential_draft_user_id_user_id_fk": { - "name": "pending_credential_draft_user_id_user_id_fk", - "tableFrom": "pending_credential_draft", - "tableTo": "user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "pending_credential_draft_workspace_id_workspace_id_fk": { - "name": "pending_credential_draft_workspace_id_workspace_id_fk", - "tableFrom": "pending_credential_draft", - "tableTo": "workspace", - "columnsFrom": ["workspace_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "pending_credential_draft_credential_id_credential_id_fk": { - "name": "pending_credential_draft_credential_id_credential_id_fk", - "tableFrom": "pending_credential_draft", - "tableTo": "credential", - "columnsFrom": ["credential_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.permission_group": { - "name": "permission_group", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "workspace_id": { - "name": "workspace_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "config": { - "name": "config", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'{}'" - }, - "created_by": { - "name": "created_by", - "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()" - }, - "auto_add_new_members": { - "name": "auto_add_new_members", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - } - }, - "indexes": { - "permission_group_created_by_idx": { - "name": "permission_group_created_by_idx", - "columns": [ - { - "expression": "created_by", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "permission_group_workspace_name_unique": { - "name": "permission_group_workspace_name_unique", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "name", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - }, - "permission_group_workspace_auto_add_unique": { - "name": "permission_group_workspace_auto_add_unique", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "where": "auto_add_new_members = true", - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "permission_group_workspace_id_workspace_id_fk": { - "name": "permission_group_workspace_id_workspace_id_fk", - "tableFrom": "permission_group", - "tableTo": "workspace", - "columnsFrom": ["workspace_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "permission_group_created_by_user_id_fk": { - "name": "permission_group_created_by_user_id_fk", - "tableFrom": "permission_group", - "tableTo": "user", - "columnsFrom": ["created_by"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.permission_group_member": { - "name": "permission_group_member", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "permission_group_id": { - "name": "permission_group_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "workspace_id": { - "name": "workspace_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "assigned_by": { - "name": "assigned_by", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "assigned_at": { - "name": "assigned_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "permission_group_member_group_id_idx": { - "name": "permission_group_member_group_id_idx", - "columns": [ - { - "expression": "permission_group_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "permission_group_member_group_user_unique": { - "name": "permission_group_member_group_user_unique", - "columns": [ - { - "expression": "permission_group_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - }, - "permission_group_member_workspace_user_unique": { - "name": "permission_group_member_workspace_user_unique", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "permission_group_member_permission_group_id_permission_group_id_fk": { - "name": "permission_group_member_permission_group_id_permission_group_id_fk", - "tableFrom": "permission_group_member", - "tableTo": "permission_group", - "columnsFrom": ["permission_group_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "permission_group_member_workspace_id_workspace_id_fk": { - "name": "permission_group_member_workspace_id_workspace_id_fk", - "tableFrom": "permission_group_member", - "tableTo": "workspace", - "columnsFrom": ["workspace_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "permission_group_member_user_id_user_id_fk": { - "name": "permission_group_member_user_id_user_id_fk", - "tableFrom": "permission_group_member", - "tableTo": "user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "permission_group_member_assigned_by_user_id_fk": { - "name": "permission_group_member_assigned_by_user_id_fk", - "tableFrom": "permission_group_member", - "tableTo": "user", - "columnsFrom": ["assigned_by"], - "columnsTo": ["id"], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.permissions": { - "name": "permissions", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "entity_type": { - "name": "entity_type", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "entity_id": { - "name": "entity_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "permission_type": { - "name": "permission_type", - "type": "permission_type", - "typeSchema": "public", - "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": { - "permissions_user_id_idx": { - "name": "permissions_user_id_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "permissions_entity_idx": { - "name": "permissions_entity_idx", - "columns": [ - { - "expression": "entity_type", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "entity_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "permissions_user_entity_type_idx": { - "name": "permissions_user_entity_type_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "entity_type", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "permissions_user_entity_permission_idx": { - "name": "permissions_user_entity_permission_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "entity_type", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "permission_type", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "permissions_user_entity_idx": { - "name": "permissions_user_entity_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "entity_type", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "entity_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "permissions_unique_constraint": { - "name": "permissions_unique_constraint", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "entity_type", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "entity_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "permissions_user_id_user_id_fk": { - "name": "permissions_user_id_user_id_fk", - "tableFrom": "permissions", - "tableTo": "user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.rate_limit_bucket": { - "name": "rate_limit_bucket", - "schema": "", - "columns": { - "key": { - "name": "key", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "tokens": { - "name": "tokens", - "type": "numeric", - "primaryKey": false, - "notNull": true - }, - "last_refill_at": { - "name": "last_refill_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.resume_queue": { - "name": "resume_queue", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "paused_execution_id": { - "name": "paused_execution_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "parent_execution_id": { - "name": "parent_execution_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "new_execution_id": { - "name": "new_execution_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "context_id": { - "name": "context_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "resume_input": { - "name": "resume_input", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'pending'" - }, - "queued_at": { - "name": "queued_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "claimed_at": { - "name": "claimed_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "completed_at": { - "name": "completed_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "failure_reason": { - "name": "failure_reason", - "type": "text", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "resume_queue_parent_status_idx": { - "name": "resume_queue_parent_status_idx", - "columns": [ - { - "expression": "parent_execution_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "status", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "queued_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "resume_queue_new_execution_idx": { - "name": "resume_queue_new_execution_idx", - "columns": [ - { - "expression": "new_execution_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "resume_queue_paused_execution_id_paused_executions_id_fk": { - "name": "resume_queue_paused_execution_id_paused_executions_id_fk", - "tableFrom": "resume_queue", - "tableTo": "paused_executions", - "columnsFrom": ["paused_execution_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "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 - }, - "impersonated_by": { - "name": "impersonated_by", - "type": "text", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "session_user_id_idx": { - "name": "session_user_id_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "session_token_idx": { - "name": "session_token_idx", - "columns": [ - { - "expression": "token", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "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'" - }, - "auto_connect": { - "name": "auto_connect", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "telemetry_enabled": { - "name": "telemetry_enabled", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "email_preferences": { - "name": "email_preferences", - "type": "json", - "primaryKey": false, - "notNull": true, - "default": "'{}'" - }, - "billing_usage_notifications_enabled": { - "name": "billing_usage_notifications_enabled", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "show_training_controls": { - "name": "show_training_controls", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "super_user_mode_enabled": { - "name": "super_user_mode_enabled", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "mothership_environment": { - "name": "mothership_environment", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'default'" - }, - "error_notifications_enabled": { - "name": "error_notifications_enabled", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "snap_to_grid_size": { - "name": "snap_to_grid_size", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "show_action_bar": { - "name": "show_action_bar", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "copilot_enabled_models": { - "name": "copilot_enabled_models", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'{}'" - }, - "copilot_auto_allowed_tools": { - "name": "copilot_auto_allowed_tools", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'[]'" - }, - "last_active_workspace_id": { - "name": "last_active_workspace_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "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.skill": { - "name": "skill", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "workspace_id": { - "name": "workspace_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "content": { - "name": "content", - "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": { - "skill_workspace_name_unique": { - "name": "skill_workspace_name_unique", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "name", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "skill_workspace_id_workspace_id_fk": { - "name": "skill_workspace_id_workspace_id_fk", - "tableFrom": "skill", - "tableTo": "workspace", - "columnsFrom": ["workspace_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "skill_user_id_user_id_fk": { - "name": "skill_user_id_user_id_fk", - "tableFrom": "skill", - "tableTo": "user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.sso_provider": { - "name": "sso_provider", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "issuer": { - "name": "issuer", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "domain": { - "name": "domain", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "oidc_config": { - "name": "oidc_config", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "saml_config": { - "name": "saml_config", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "provider_id": { - "name": "provider_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "organization_id": { - "name": "organization_id", - "type": "text", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "sso_provider_provider_id_idx": { - "name": "sso_provider_provider_id_idx", - "columns": [ - { - "expression": "provider_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "sso_provider_domain_idx": { - "name": "sso_provider_domain_idx", - "columns": [ - { - "expression": "domain", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "sso_provider_user_id_idx": { - "name": "sso_provider_user_id_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "sso_provider_organization_id_idx": { - "name": "sso_provider_organization_id_idx", - "columns": [ - { - "expression": "organization_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "sso_provider_user_id_user_id_fk": { - "name": "sso_provider_user_id_user_id_fk", - "tableFrom": "sso_provider", - "tableTo": "user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "sso_provider_organization_id_organization_id_fk": { - "name": "sso_provider_organization_id_organization_id_fk", - "tableFrom": "sso_provider", - "tableTo": "organization", - "columnsFrom": ["organization_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "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": { - "subscription_reference_status_idx": { - "name": "subscription_reference_status_idx", - "columns": [ - { - "expression": "reference_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "status", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": { - "check_enterprise_metadata": { - "name": "check_enterprise_metadata", - "value": "plan != 'enterprise' OR metadata IS NOT NULL" - } - }, - "isRLSEnabled": false - }, - "public.template_creators": { - "name": "template_creators", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "reference_type": { - "name": "reference_type", - "type": "template_creator_type", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "reference_id": { - "name": "reference_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "profile_image_url": { - "name": "profile_image_url", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "details": { - "name": "details", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "verified": { - "name": "verified", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "created_by": { - "name": "created_by", - "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": { - "template_creators_reference_idx": { - "name": "template_creators_reference_idx", - "columns": [ - { - "expression": "reference_type", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "reference_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - }, - "template_creators_reference_id_idx": { - "name": "template_creators_reference_id_idx", - "columns": [ - { - "expression": "reference_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "template_creators_created_by_idx": { - "name": "template_creators_created_by_idx", - "columns": [ - { - "expression": "created_by", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "template_creators_created_by_user_id_fk": { - "name": "template_creators_created_by_user_id_fk", - "tableFrom": "template_creators", - "tableTo": "user", - "columnsFrom": ["created_by"], - "columnsTo": ["id"], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.template_stars": { - "name": "template_stars", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "template_id": { - "name": "template_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "starred_at": { - "name": "starred_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "template_stars_user_id_idx": { - "name": "template_stars_user_id_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "template_stars_template_id_idx": { - "name": "template_stars_template_id_idx", - "columns": [ - { - "expression": "template_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "template_stars_user_template_idx": { - "name": "template_stars_user_template_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "template_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "template_stars_template_user_idx": { - "name": "template_stars_template_user_idx", - "columns": [ - { - "expression": "template_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "template_stars_starred_at_idx": { - "name": "template_stars_starred_at_idx", - "columns": [ - { - "expression": "starred_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "template_stars_template_starred_at_idx": { - "name": "template_stars_template_starred_at_idx", - "columns": [ - { - "expression": "template_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "starred_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "template_stars_user_template_unique": { - "name": "template_stars_user_template_unique", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "template_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "template_stars_user_id_user_id_fk": { - "name": "template_stars_user_id_user_id_fk", - "tableFrom": "template_stars", - "tableTo": "user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "template_stars_template_id_templates_id_fk": { - "name": "template_stars_template_id_templates_id_fk", - "tableFrom": "template_stars", - "tableTo": "templates", - "columnsFrom": ["template_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.templates": { - "name": "templates", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "workflow_id": { - "name": "workflow_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "details": { - "name": "details", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "creator_id": { - "name": "creator_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "views": { - "name": "views", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "stars": { - "name": "stars", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "status": { - "name": "status", - "type": "template_status", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'pending'" - }, - "tags": { - "name": "tags", - "type": "text[]", - "primaryKey": false, - "notNull": true, - "default": "'{}'::text[]" - }, - "required_credentials": { - "name": "required_credentials", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'[]'" - }, - "state": { - "name": "state", - "type": "jsonb", - "primaryKey": false, - "notNull": true - }, - "og_image_url": { - "name": "og_image_url", - "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": { - "templates_status_idx": { - "name": "templates_status_idx", - "columns": [ - { - "expression": "status", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "templates_creator_id_idx": { - "name": "templates_creator_id_idx", - "columns": [ - { - "expression": "creator_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "templates_views_idx": { - "name": "templates_views_idx", - "columns": [ - { - "expression": "views", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "templates_stars_idx": { - "name": "templates_stars_idx", - "columns": [ - { - "expression": "stars", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "templates_status_views_idx": { - "name": "templates_status_views_idx", - "columns": [ - { - "expression": "status", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "views", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "templates_status_stars_idx": { - "name": "templates_status_stars_idx", - "columns": [ - { - "expression": "status", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "stars", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "templates_created_at_idx": { - "name": "templates_created_at_idx", - "columns": [ - { - "expression": "created_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "templates_updated_at_idx": { - "name": "templates_updated_at_idx", - "columns": [ - { - "expression": "updated_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "templates_workflow_id_workflow_id_fk": { - "name": "templates_workflow_id_workflow_id_fk", - "tableFrom": "templates", - "tableTo": "workflow", - "columnsFrom": ["workflow_id"], - "columnsTo": ["id"], - "onDelete": "set null", - "onUpdate": "no action" - }, - "templates_creator_id_template_creators_id_fk": { - "name": "templates_creator_id_template_creators_id_fk", - "tableFrom": "templates", - "tableTo": "template_creators", - "columnsFrom": ["creator_id"], - "columnsTo": ["id"], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.usage_log": { - "name": "usage_log", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "category": { - "name": "category", - "type": "usage_log_category", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "source": { - "name": "source", - "type": "usage_log_source", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "metadata": { - "name": "metadata", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "cost": { - "name": "cost", - "type": "numeric", - "primaryKey": false, - "notNull": true - }, - "workspace_id": { - "name": "workspace_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "workflow_id": { - "name": "workflow_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "execution_id": { - "name": "execution_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "usage_log_user_created_at_idx": { - "name": "usage_log_user_created_at_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "created_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "usage_log_source_idx": { - "name": "usage_log_source_idx", - "columns": [ - { - "expression": "source", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "usage_log_workspace_id_idx": { - "name": "usage_log_workspace_id_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "usage_log_workflow_id_idx": { - "name": "usage_log_workflow_id_idx", - "columns": [ - { - "expression": "workflow_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "usage_log_workspace_created_at_idx": { - "name": "usage_log_workspace_created_at_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "created_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "usage_log_user_id_user_id_fk": { - "name": "usage_log_user_id_user_id_fk", - "tableFrom": "usage_log", - "tableTo": "user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "usage_log_workspace_id_workspace_id_fk": { - "name": "usage_log_workspace_id_workspace_id_fk", - "tableFrom": "usage_log", - "tableTo": "workspace", - "columnsFrom": ["workspace_id"], - "columnsTo": ["id"], - "onDelete": "set null", - "onUpdate": "no action" - }, - "usage_log_workflow_id_workflow_id_fk": { - "name": "usage_log_workflow_id_workflow_id_fk", - "tableFrom": "usage_log", - "tableTo": "workflow", - "columnsFrom": ["workflow_id"], - "columnsTo": ["id"], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "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 - }, - "normalized_email": { - "name": "normalized_email", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "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 - }, - "role": { - "name": "role", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "'user'" - }, - "banned": { - "name": "banned", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": false - }, - "ban_reason": { - "name": "ban_reason", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "ban_expires": { - "name": "ban_expires", - "type": "timestamp", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "user_email_unique": { - "name": "user_email_unique", - "nullsNotDistinct": false, - "columns": ["email"] - }, - "user_normalized_email_unique": { - "name": "user_normalized_email_unique", - "nullsNotDistinct": false, - "columns": ["normalized_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_mcp_executions": { - "name": "total_mcp_executions", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "total_a2a_executions": { - "name": "total_a2a_executions", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "total_tokens_used": { - "name": "total_tokens_used", - "type": "bigint", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "total_cost": { - "name": "total_cost", - "type": "numeric", - "primaryKey": false, - "notNull": true, - "default": "'0'" - }, - "current_usage_limit": { - "name": "current_usage_limit", - "type": "numeric", - "primaryKey": false, - "notNull": false, - "default": "'5'" - }, - "usage_limit_updated_at": { - "name": "usage_limit_updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "default": "now()" - }, - "current_period_cost": { - "name": "current_period_cost", - "type": "numeric", - "primaryKey": false, - "notNull": true, - "default": "'0'" - }, - "last_period_cost": { - "name": "last_period_cost", - "type": "numeric", - "primaryKey": false, - "notNull": false, - "default": "'0'" - }, - "billed_overage_this_period": { - "name": "billed_overage_this_period", - "type": "numeric", - "primaryKey": false, - "notNull": true, - "default": "'0'" - }, - "pro_period_cost_snapshot": { - "name": "pro_period_cost_snapshot", - "type": "numeric", - "primaryKey": false, - "notNull": false, - "default": "'0'" - }, - "pro_period_cost_snapshot_at": { - "name": "pro_period_cost_snapshot_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "credit_balance": { - "name": "credit_balance", - "type": "numeric", - "primaryKey": false, - "notNull": true, - "default": "'0'" - }, - "total_copilot_cost": { - "name": "total_copilot_cost", - "type": "numeric", - "primaryKey": false, - "notNull": true, - "default": "'0'" - }, - "current_period_copilot_cost": { - "name": "current_period_copilot_cost", - "type": "numeric", - "primaryKey": false, - "notNull": true, - "default": "'0'" - }, - "last_period_copilot_cost": { - "name": "last_period_copilot_cost", - "type": "numeric", - "primaryKey": false, - "notNull": false, - "default": "'0'" - }, - "total_copilot_tokens": { - "name": "total_copilot_tokens", - "type": "bigint", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "total_copilot_calls": { - "name": "total_copilot_calls", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "total_mcp_copilot_calls": { - "name": "total_mcp_copilot_calls", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "total_mcp_copilot_cost": { - "name": "total_mcp_copilot_cost", - "type": "numeric", - "primaryKey": false, - "notNull": true, - "default": "'0'" - }, - "current_period_mcp_copilot_cost": { - "name": "current_period_mcp_copilot_cost", - "type": "numeric", - "primaryKey": false, - "notNull": true, - "default": "'0'" - }, - "storage_used_bytes": { - "name": "storage_used_bytes", - "type": "bigint", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "last_active": { - "name": "last_active", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "billing_blocked": { - "name": "billing_blocked", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "billing_blocked_reason": { - "name": "billing_blocked_reason", - "type": "billing_blocked_reason", - "typeSchema": "public", - "primaryKey": false, - "notNull": false - } - }, - "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.user_table_definitions": { - "name": "user_table_definitions", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "workspace_id": { - "name": "workspace_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "schema": { - "name": "schema", - "type": "jsonb", - "primaryKey": false, - "notNull": true - }, - "metadata": { - "name": "metadata", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "max_rows": { - "name": "max_rows", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 10000 - }, - "row_count": { - "name": "row_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "archived_at": { - "name": "archived_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "created_by": { - "name": "created_by", - "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": { - "user_table_def_workspace_id_idx": { - "name": "user_table_def_workspace_id_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "user_table_def_workspace_name_unique": { - "name": "user_table_def_workspace_name_unique", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "name", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "where": "\"user_table_definitions\".\"archived_at\" IS NULL", - "concurrently": false, - "method": "btree", - "with": {} - }, - "user_table_def_archived_at_idx": { - "name": "user_table_def_archived_at_idx", - "columns": [ - { - "expression": "archived_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "user_table_def_workspace_archived_partial_idx": { - "name": "user_table_def_workspace_archived_partial_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "archived_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "where": "\"user_table_definitions\".\"archived_at\" IS NOT NULL", - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "user_table_definitions_workspace_id_workspace_id_fk": { - "name": "user_table_definitions_workspace_id_workspace_id_fk", - "tableFrom": "user_table_definitions", - "tableTo": "workspace", - "columnsFrom": ["workspace_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "user_table_definitions_created_by_user_id_fk": { - "name": "user_table_definitions_created_by_user_id_fk", - "tableFrom": "user_table_definitions", - "tableTo": "user", - "columnsFrom": ["created_by"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.user_table_rows": { - "name": "user_table_rows", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "table_id": { - "name": "table_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "workspace_id": { - "name": "workspace_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "data": { - "name": "data", - "type": "jsonb", - "primaryKey": false, - "notNull": true - }, - "executions": { - "name": "executions", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'{}'::jsonb" - }, - "position": { - "name": "position", - "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()" - }, - "created_by": { - "name": "created_by", - "type": "text", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "user_table_rows_table_id_idx": { - "name": "user_table_rows_table_id_idx", - "columns": [ - { - "expression": "table_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "user_table_rows_data_gin_idx": { - "name": "user_table_rows_data_gin_idx", - "columns": [ - { - "expression": "data", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "gin", - "with": {} - }, - "user_table_rows_workspace_table_idx": { - "name": "user_table_rows_workspace_table_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "table_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "user_table_rows_table_position_idx": { - "name": "user_table_rows_table_position_idx", - "columns": [ - { - "expression": "table_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "position", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "user_table_rows_table_id_user_table_definitions_id_fk": { - "name": "user_table_rows_table_id_user_table_definitions_id_fk", - "tableFrom": "user_table_rows", - "tableTo": "user_table_definitions", - "columnsFrom": ["table_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "user_table_rows_workspace_id_workspace_id_fk": { - "name": "user_table_rows_workspace_id_workspace_id_fk", - "tableFrom": "user_table_rows", - "tableTo": "workspace", - "columnsFrom": ["workspace_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "user_table_rows_created_by_user_id_fk": { - "name": "user_table_rows_created_by_user_id_fk", - "tableFrom": "user_table_rows", - "tableTo": "user", - "columnsFrom": ["created_by"], - "columnsTo": ["id"], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "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": { - "verification_identifier_idx": { - "name": "verification_identifier_idx", - "columns": [ - { - "expression": "identifier", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "verification_expires_at_idx": { - "name": "verification_expires_at_idx", - "columns": [ - { - "expression": "expires_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "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 - }, - "deployment_version_id": { - "name": "deployment_version_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "block_id": { - "name": "block_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "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 - }, - "failed_count": { - "name": "failed_count", - "type": "integer", - "primaryKey": false, - "notNull": false, - "default": 0 - }, - "last_failed_at": { - "name": "last_failed_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "credential_set_id": { - "name": "credential_set_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "archived_at": { - "name": "archived_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": { - "path_deployment_unique": { - "name": "path_deployment_unique", - "columns": [ - { - "expression": "path", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "deployment_version_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "where": "\"webhook\".\"archived_at\" IS NULL", - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_webhook_on_workflow_id_block_id": { - "name": "idx_webhook_on_workflow_id_block_id", - "columns": [ - { - "expression": "workflow_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "block_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "webhook_workflow_deployment_idx": { - "name": "webhook_workflow_deployment_idx", - "columns": [ - { - "expression": "workflow_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "deployment_version_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "webhook_credential_set_id_idx": { - "name": "webhook_credential_set_id_idx", - "columns": [ - { - "expression": "credential_set_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "webhook_archived_at_partial_idx": { - "name": "webhook_archived_at_partial_idx", - "columns": [ - { - "expression": "archived_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "where": "\"webhook\".\"archived_at\" IS NOT NULL", - "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" - }, - "webhook_deployment_version_id_workflow_deployment_version_id_fk": { - "name": "webhook_deployment_version_id_workflow_deployment_version_id_fk", - "tableFrom": "webhook", - "tableTo": "workflow_deployment_version", - "columnsFrom": ["deployment_version_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "webhook_credential_set_id_credential_set_id_fk": { - "name": "webhook_credential_set_id_credential_set_id_fk", - "tableFrom": "webhook", - "tableTo": "credential_set", - "columnsFrom": ["credential_set_id"], - "columnsTo": ["id"], - "onDelete": "set null", - "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 - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "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_at": { - "name": "deployed_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "is_public_api": { - "name": "is_public_api", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "locked": { - "name": "locked", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "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": "'{}'" - }, - "archived_at": { - "name": "archived_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "workflow_user_id_idx": { - "name": "workflow_user_id_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_workspace_id_idx": { - "name": "workflow_workspace_id_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_user_workspace_idx": { - "name": "workflow_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": {} - }, - "workflow_workspace_folder_name_active_unique": { - "name": "workflow_workspace_folder_name_active_unique", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "coalesce(\"folder_id\", '')", - "asc": true, - "isExpression": true, - "nulls": "last" - }, - { - "expression": "name", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "where": "\"workflow\".\"archived_at\" IS NULL", - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_folder_sort_idx": { - "name": "workflow_folder_sort_idx", - "columns": [ - { - "expression": "folder_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "sort_order", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_archived_at_idx": { - "name": "workflow_archived_at_idx", - "columns": [ - { - "expression": "archived_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_workspace_archived_partial_idx": { - "name": "workflow_workspace_archived_partial_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "archived_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "where": "\"workflow\".\"archived_at\" IS NOT NULL", - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "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_blocks": { - "name": "workflow_blocks", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "workflow_id": { - "name": "workflow_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "position_x": { - "name": "position_x", - "type": "numeric", - "primaryKey": false, - "notNull": true - }, - "position_y": { - "name": "position_y", - "type": "numeric", - "primaryKey": false, - "notNull": true - }, - "enabled": { - "name": "enabled", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "horizontal_handles": { - "name": "horizontal_handles", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "is_wide": { - "name": "is_wide", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "advanced_mode": { - "name": "advanced_mode", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "trigger_mode": { - "name": "trigger_mode", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "locked": { - "name": "locked", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "height": { - "name": "height", - "type": "numeric", - "primaryKey": false, - "notNull": true, - "default": "'0'" - }, - "sub_blocks": { - "name": "sub_blocks", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'{}'" - }, - "outputs": { - "name": "outputs", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'{}'" - }, - "data": { - "name": "data", - "type": "jsonb", - "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": { - "workflow_blocks_workflow_id_idx": { - "name": "workflow_blocks_workflow_id_idx", - "columns": [ - { - "expression": "workflow_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_blocks_type_idx": { - "name": "workflow_blocks_type_idx", - "columns": [ - { - "expression": "type", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "workflow_blocks_workflow_id_workflow_id_fk": { - "name": "workflow_blocks_workflow_id_workflow_id_fk", - "tableFrom": "workflow_blocks", - "tableTo": "workflow", - "columnsFrom": ["workflow_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.workflow_checkpoints": { - "name": "workflow_checkpoints", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "workflow_id": { - "name": "workflow_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "chat_id": { - "name": "chat_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "message_id": { - "name": "message_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "workflow_state": { - "name": "workflow_state", - "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()" - } - }, - "indexes": { - "workflow_checkpoints_user_id_idx": { - "name": "workflow_checkpoints_user_id_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_checkpoints_workflow_id_idx": { - "name": "workflow_checkpoints_workflow_id_idx", - "columns": [ - { - "expression": "workflow_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_checkpoints_chat_id_idx": { - "name": "workflow_checkpoints_chat_id_idx", - "columns": [ - { - "expression": "chat_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_checkpoints_message_id_idx": { - "name": "workflow_checkpoints_message_id_idx", - "columns": [ - { - "expression": "message_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_checkpoints_user_workflow_idx": { - "name": "workflow_checkpoints_user_workflow_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "workflow_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_checkpoints_workflow_chat_idx": { - "name": "workflow_checkpoints_workflow_chat_idx", - "columns": [ - { - "expression": "workflow_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "chat_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_checkpoints_created_at_idx": { - "name": "workflow_checkpoints_created_at_idx", - "columns": [ - { - "expression": "created_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_checkpoints_chat_created_at_idx": { - "name": "workflow_checkpoints_chat_created_at_idx", - "columns": [ - { - "expression": "chat_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "created_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "workflow_checkpoints_user_id_user_id_fk": { - "name": "workflow_checkpoints_user_id_user_id_fk", - "tableFrom": "workflow_checkpoints", - "tableTo": "user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "workflow_checkpoints_workflow_id_workflow_id_fk": { - "name": "workflow_checkpoints_workflow_id_workflow_id_fk", - "tableFrom": "workflow_checkpoints", - "tableTo": "workflow", - "columnsFrom": ["workflow_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "workflow_checkpoints_chat_id_copilot_chats_id_fk": { - "name": "workflow_checkpoints_chat_id_copilot_chats_id_fk", - "tableFrom": "workflow_checkpoints", - "tableTo": "copilot_chats", - "columnsFrom": ["chat_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.workflow_deployment_version": { - "name": "workflow_deployment_version", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "workflow_id": { - "name": "workflow_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "version": { - "name": "version", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "state": { - "name": "state", - "type": "json", - "primaryKey": false, - "notNull": true - }, - "is_active": { - "name": "is_active", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "created_by": { - "name": "created_by", - "type": "text", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "workflow_deployment_version_workflow_version_unique": { - "name": "workflow_deployment_version_workflow_version_unique", - "columns": [ - { - "expression": "workflow_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "version", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_deployment_version_workflow_active_idx": { - "name": "workflow_deployment_version_workflow_active_idx", - "columns": [ - { - "expression": "workflow_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "is_active", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_deployment_version_created_at_idx": { - "name": "workflow_deployment_version_created_at_idx", - "columns": [ - { - "expression": "created_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "workflow_deployment_version_workflow_id_workflow_id_fk": { - "name": "workflow_deployment_version_workflow_id_workflow_id_fk", - "tableFrom": "workflow_deployment_version", - "tableTo": "workflow", - "columnsFrom": ["workflow_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.workflow_edges": { - "name": "workflow_edges", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "workflow_id": { - "name": "workflow_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "source_block_id": { - "name": "source_block_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "target_block_id": { - "name": "target_block_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "source_handle": { - "name": "source_handle", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "target_handle": { - "name": "target_handle", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "workflow_edges_workflow_id_idx": { - "name": "workflow_edges_workflow_id_idx", - "columns": [ - { - "expression": "workflow_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_edges_workflow_source_idx": { - "name": "workflow_edges_workflow_source_idx", - "columns": [ - { - "expression": "workflow_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "source_block_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_edges_workflow_target_idx": { - "name": "workflow_edges_workflow_target_idx", - "columns": [ - { - "expression": "workflow_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "target_block_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "workflow_edges_workflow_id_workflow_id_fk": { - "name": "workflow_edges_workflow_id_workflow_id_fk", - "tableFrom": "workflow_edges", - "tableTo": "workflow", - "columnsFrom": ["workflow_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "workflow_edges_source_block_id_workflow_blocks_id_fk": { - "name": "workflow_edges_source_block_id_workflow_blocks_id_fk", - "tableFrom": "workflow_edges", - "tableTo": "workflow_blocks", - "columnsFrom": ["source_block_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "workflow_edges_target_block_id_workflow_blocks_id_fk": { - "name": "workflow_edges_target_block_id_workflow_blocks_id_fk", - "tableFrom": "workflow_edges", - "tableTo": "workflow_blocks", - "columnsFrom": ["target_block_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.workflow_execution_logs": { - "name": "workflow_execution_logs", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "workflow_id": { - "name": "workflow_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "workspace_id": { - "name": "workspace_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "execution_id": { - "name": "execution_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "state_snapshot_id": { - "name": "state_snapshot_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "deployment_version_id": { - "name": "deployment_version_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "level": { - "name": "level", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'running'" - }, - "trigger": { - "name": "trigger", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "started_at": { - "name": "started_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "ended_at": { - "name": "ended_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "total_duration_ms": { - "name": "total_duration_ms", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "execution_data": { - "name": "execution_data", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'{}'" - }, - "cost": { - "name": "cost", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "files": { - "name": "files", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "workflow_execution_logs_workflow_id_idx": { - "name": "workflow_execution_logs_workflow_id_idx", - "columns": [ - { - "expression": "workflow_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_execution_logs_state_snapshot_id_idx": { - "name": "workflow_execution_logs_state_snapshot_id_idx", - "columns": [ - { - "expression": "state_snapshot_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_execution_logs_deployment_version_id_idx": { - "name": "workflow_execution_logs_deployment_version_id_idx", - "columns": [ - { - "expression": "deployment_version_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_execution_logs_trigger_idx": { - "name": "workflow_execution_logs_trigger_idx", - "columns": [ - { - "expression": "trigger", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_execution_logs_level_idx": { - "name": "workflow_execution_logs_level_idx", - "columns": [ - { - "expression": "level", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_execution_logs_started_at_idx": { - "name": "workflow_execution_logs_started_at_idx", - "columns": [ - { - "expression": "started_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_execution_logs_execution_id_unique": { - "name": "workflow_execution_logs_execution_id_unique", - "columns": [ - { - "expression": "execution_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_execution_logs_workflow_started_at_idx": { - "name": "workflow_execution_logs_workflow_started_at_idx", - "columns": [ - { - "expression": "workflow_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "started_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_execution_logs_workspace_started_at_idx": { - "name": "workflow_execution_logs_workspace_started_at_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "started_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_execution_logs_workspace_ended_at_id_idx": { - "name": "workflow_execution_logs_workspace_ended_at_id_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "date_trunc('milliseconds', \"ended_at\")", - "asc": true, - "isExpression": true, - "nulls": "last" - }, - { - "expression": "id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_execution_logs_running_started_at_idx": { - "name": "workflow_execution_logs_running_started_at_idx", - "columns": [ - { - "expression": "started_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "where": "status = 'running'", - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "workflow_execution_logs_workflow_id_workflow_id_fk": { - "name": "workflow_execution_logs_workflow_id_workflow_id_fk", - "tableFrom": "workflow_execution_logs", - "tableTo": "workflow", - "columnsFrom": ["workflow_id"], - "columnsTo": ["id"], - "onDelete": "set null", - "onUpdate": "no action" - }, - "workflow_execution_logs_workspace_id_workspace_id_fk": { - "name": "workflow_execution_logs_workspace_id_workspace_id_fk", - "tableFrom": "workflow_execution_logs", - "tableTo": "workspace", - "columnsFrom": ["workspace_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk": { - "name": "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk", - "tableFrom": "workflow_execution_logs", - "tableTo": "workflow_execution_snapshots", - "columnsFrom": ["state_snapshot_id"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - }, - "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk": { - "name": "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk", - "tableFrom": "workflow_execution_logs", - "tableTo": "workflow_deployment_version", - "columnsFrom": ["deployment_version_id"], - "columnsTo": ["id"], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.workflow_execution_snapshots": { - "name": "workflow_execution_snapshots", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "workflow_id": { - "name": "workflow_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "state_hash": { - "name": "state_hash", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "state_data": { - "name": "state_data", - "type": "jsonb", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "workflow_snapshots_workflow_id_idx": { - "name": "workflow_snapshots_workflow_id_idx", - "columns": [ - { - "expression": "workflow_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_snapshots_hash_idx": { - "name": "workflow_snapshots_hash_idx", - "columns": [ - { - "expression": "state_hash", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_snapshots_workflow_hash_idx": { - "name": "workflow_snapshots_workflow_hash_idx", - "columns": [ - { - "expression": "workflow_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "state_hash", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_snapshots_created_at_idx": { - "name": "workflow_snapshots_created_at_idx", - "columns": [ - { - "expression": "created_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "workflow_execution_snapshots_workflow_id_workflow_id_fk": { - "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk", - "tableFrom": "workflow_execution_snapshots", - "tableTo": "workflow", - "columnsFrom": ["workflow_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 - }, - "locked": { - "name": "locked", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "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()" - }, - "archived_at": { - "name": "archived_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "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_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_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": {} - }, - "workflow_folder_archived_at_idx": { - "name": "workflow_folder_archived_at_idx", - "columns": [ - { - "expression": "archived_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_folder_workspace_archived_partial_idx": { - "name": "workflow_folder_workspace_archived_partial_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "archived_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "where": "\"workflow_folder\".\"archived_at\" IS NOT NULL", - "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_mcp_server": { - "name": "workflow_mcp_server", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "workspace_id": { - "name": "workspace_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_by": { - "name": "created_by", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "is_public": { - "name": "is_public", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "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": { - "workflow_mcp_server_workspace_id_idx": { - "name": "workflow_mcp_server_workspace_id_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_mcp_server_created_by_idx": { - "name": "workflow_mcp_server_created_by_idx", - "columns": [ - { - "expression": "created_by", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_mcp_server_deleted_at_idx": { - "name": "workflow_mcp_server_deleted_at_idx", - "columns": [ - { - "expression": "deleted_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_mcp_server_workspace_deleted_partial_idx": { - "name": "workflow_mcp_server_workspace_deleted_partial_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "deleted_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "where": "\"workflow_mcp_server\".\"deleted_at\" IS NOT NULL", - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "workflow_mcp_server_workspace_id_workspace_id_fk": { - "name": "workflow_mcp_server_workspace_id_workspace_id_fk", - "tableFrom": "workflow_mcp_server", - "tableTo": "workspace", - "columnsFrom": ["workspace_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "workflow_mcp_server_created_by_user_id_fk": { - "name": "workflow_mcp_server_created_by_user_id_fk", - "tableFrom": "workflow_mcp_server", - "tableTo": "user", - "columnsFrom": ["created_by"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.workflow_mcp_tool": { - "name": "workflow_mcp_tool", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "server_id": { - "name": "server_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "workflow_id": { - "name": "workflow_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "tool_name": { - "name": "tool_name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "tool_description": { - "name": "tool_description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "parameter_schema": { - "name": "parameter_schema", - "type": "json", - "primaryKey": false, - "notNull": true, - "default": "'{}'" - }, - "archived_at": { - "name": "archived_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": { - "workflow_mcp_tool_server_id_idx": { - "name": "workflow_mcp_tool_server_id_idx", - "columns": [ - { - "expression": "server_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_mcp_tool_workflow_id_idx": { - "name": "workflow_mcp_tool_workflow_id_idx", - "columns": [ - { - "expression": "workflow_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_mcp_tool_server_workflow_unique": { - "name": "workflow_mcp_tool_server_workflow_unique", - "columns": [ - { - "expression": "server_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "workflow_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "where": "\"workflow_mcp_tool\".\"archived_at\" IS NULL", - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_mcp_tool_archived_at_partial_idx": { - "name": "workflow_mcp_tool_archived_at_partial_idx", - "columns": [ - { - "expression": "archived_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "where": "\"workflow_mcp_tool\".\"archived_at\" IS NOT NULL", - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk": { - "name": "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk", - "tableFrom": "workflow_mcp_tool", - "tableTo": "workflow_mcp_server", - "columnsFrom": ["server_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "workflow_mcp_tool_workflow_id_workflow_id_fk": { - "name": "workflow_mcp_tool_workflow_id_workflow_id_fk", - "tableFrom": "workflow_mcp_tool", - "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": false - }, - "deployment_version_id": { - "name": "deployment_version_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "block_id": { - "name": "block_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "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 - }, - "last_queued_at": { - "name": "last_queued_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 - }, - "source_type": { - "name": "source_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'workflow'" - }, - "job_title": { - "name": "job_title", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "prompt": { - "name": "prompt", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "lifecycle": { - "name": "lifecycle", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'persistent'" - }, - "success_condition": { - "name": "success_condition", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "max_runs": { - "name": "max_runs", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "run_count": { - "name": "run_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "source_chat_id": { - "name": "source_chat_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "source_task_name": { - "name": "source_task_name", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "source_user_id": { - "name": "source_user_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "source_workspace_id": { - "name": "source_workspace_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "job_history": { - "name": "job_history", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "archived_at": { - "name": "archived_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": { - "workflow_schedule_workflow_block_deployment_unique": { - "name": "workflow_schedule_workflow_block_deployment_unique", - "columns": [ - { - "expression": "workflow_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "block_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "deployment_version_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "where": "\"workflow_schedule\".\"archived_at\" IS NULL", - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_schedule_workflow_deployment_idx": { - "name": "workflow_schedule_workflow_deployment_idx", - "columns": [ - { - "expression": "workflow_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "deployment_version_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_schedule_archived_at_partial_idx": { - "name": "workflow_schedule_archived_at_partial_idx", - "columns": [ - { - "expression": "archived_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "where": "\"workflow_schedule\".\"archived_at\" IS NOT NULL", - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "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" - }, - "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk": { - "name": "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk", - "tableFrom": "workflow_schedule", - "tableTo": "workflow_deployment_version", - "columnsFrom": ["deployment_version_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "workflow_schedule_source_user_id_user_id_fk": { - "name": "workflow_schedule_source_user_id_user_id_fk", - "tableFrom": "workflow_schedule", - "tableTo": "user", - "columnsFrom": ["source_user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "workflow_schedule_source_workspace_id_workspace_id_fk": { - "name": "workflow_schedule_source_workspace_id_workspace_id_fk", - "tableFrom": "workflow_schedule", - "tableTo": "workspace", - "columnsFrom": ["source_workspace_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.workflow_subflows": { - "name": "workflow_subflows", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "workflow_id": { - "name": "workflow_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "config": { - "name": "config", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "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": { - "workflow_subflows_workflow_id_idx": { - "name": "workflow_subflows_workflow_id_idx", - "columns": [ - { - "expression": "workflow_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workflow_subflows_workflow_type_idx": { - "name": "workflow_subflows_workflow_type_idx", - "columns": [ - { - "expression": "workflow_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "type", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "workflow_subflows_workflow_id_workflow_id_fk": { - "name": "workflow_subflows_workflow_id_workflow_id_fk", - "tableFrom": "workflow_subflows", - "tableTo": "workflow", - "columnsFrom": ["workflow_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "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 - }, - "color": { - "name": "color", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'#33C482'" - }, - "logo_url": { - "name": "logo_url", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "owner_id": { - "name": "owner_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "organization_id": { - "name": "organization_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "workspace_mode": { - "name": "workspace_mode", - "type": "workspace_mode", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'grandfathered_shared'" - }, - "billed_account_user_id": { - "name": "billed_account_user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "allow_personal_api_keys": { - "name": "allow_personal_api_keys", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "inbox_enabled": { - "name": "inbox_enabled", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "inbox_address": { - "name": "inbox_address", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "inbox_provider_id": { - "name": "inbox_provider_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "archived_at": { - "name": "archived_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": { - "workspace_owner_id_idx": { - "name": "workspace_owner_id_idx", - "columns": [ - { - "expression": "owner_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workspace_organization_id_idx": { - "name": "workspace_organization_id_idx", - "columns": [ - { - "expression": "organization_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workspace_mode_idx": { - "name": "workspace_mode_idx", - "columns": [ - { - "expression": "workspace_mode", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "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" - }, - "workspace_organization_id_organization_id_fk": { - "name": "workspace_organization_id_organization_id_fk", - "tableFrom": "workspace", - "tableTo": "organization", - "columnsFrom": ["organization_id"], - "columnsTo": ["id"], - "onDelete": "set null", - "onUpdate": "no action" - }, - "workspace_billed_account_user_id_user_id_fk": { - "name": "workspace_billed_account_user_id_user_id_fk", - "tableFrom": "workspace", - "tableTo": "user", - "columnsFrom": ["billed_account_user_id"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.workspace_byok_keys": { - "name": "workspace_byok_keys", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "workspace_id": { - "name": "workspace_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "provider_id": { - "name": "provider_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "encrypted_api_key": { - "name": "encrypted_api_key", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_by": { - "name": "created_by", - "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": { - "workspace_byok_provider_unique": { - "name": "workspace_byok_provider_unique", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "provider_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workspace_byok_workspace_idx": { - "name": "workspace_byok_workspace_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "workspace_byok_keys_workspace_id_workspace_id_fk": { - "name": "workspace_byok_keys_workspace_id_workspace_id_fk", - "tableFrom": "workspace_byok_keys", - "tableTo": "workspace", - "columnsFrom": ["workspace_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "workspace_byok_keys_created_by_user_id_fk": { - "name": "workspace_byok_keys_created_by_user_id_fk", - "tableFrom": "workspace_byok_keys", - "tableTo": "user", - "columnsFrom": ["created_by"], - "columnsTo": ["id"], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.workspace_environment": { - "name": "workspace_environment", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "workspace_id": { - "name": "workspace_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "variables": { - "name": "variables", - "type": "json", - "primaryKey": false, - "notNull": true, - "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": { - "workspace_environment_workspace_unique": { - "name": "workspace_environment_workspace_unique", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "workspace_environment_workspace_id_workspace_id_fk": { - "name": "workspace_environment_workspace_id_workspace_id_fk", - "tableFrom": "workspace_environment", - "tableTo": "workspace", - "columnsFrom": ["workspace_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.workspace_file": { - "name": "workspace_file", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "workspace_id": { - "name": "workspace_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 - }, - "size": { - "name": "size", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "uploaded_by": { - "name": "uploaded_by", - "type": "text", - "primaryKey": false, - "notNull": 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": { - "workspace_file_workspace_id_idx": { - "name": "workspace_file_workspace_id_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workspace_file_key_idx": { - "name": "workspace_file_key_idx", - "columns": [ - { - "expression": "key", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workspace_file_deleted_at_idx": { - "name": "workspace_file_deleted_at_idx", - "columns": [ - { - "expression": "deleted_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workspace_file_workspace_deleted_partial_idx": { - "name": "workspace_file_workspace_deleted_partial_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "deleted_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "where": "\"workspace_file\".\"deleted_at\" IS NOT NULL", - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "workspace_file_workspace_id_workspace_id_fk": { - "name": "workspace_file_workspace_id_workspace_id_fk", - "tableFrom": "workspace_file", - "tableTo": "workspace", - "columnsFrom": ["workspace_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "workspace_file_uploaded_by_user_id_fk": { - "name": "workspace_file_uploaded_by_user_id_fk", - "tableFrom": "workspace_file", - "tableTo": "user", - "columnsFrom": ["uploaded_by"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "workspace_file_key_unique": { - "name": "workspace_file_key_unique", - "nullsNotDistinct": false, - "columns": ["key"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.workspace_files": { - "name": "workspace_files", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "key": { - "name": "key", - "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": false - }, - "context": { - "name": "context", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "chat_id": { - "name": "chat_id", - "type": "uuid", - "primaryKey": false, - "notNull": false - }, - "original_name": { - "name": "original_name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "content_type": { - "name": "content_type", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "size": { - "name": "size", - "type": "integer", - "primaryKey": false, - "notNull": 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()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "workspace_files_key_active_unique": { - "name": "workspace_files_key_active_unique", - "columns": [ - { - "expression": "key", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "where": "\"workspace_files\".\"deleted_at\" IS NULL", - "concurrently": false, - "method": "btree", - "with": {} - }, - "workspace_files_workspace_name_active_unique": { - "name": "workspace_files_workspace_name_active_unique", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "original_name", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "where": "\"workspace_files\".\"deleted_at\" IS NULL AND \"workspace_files\".\"context\" = 'workspace' AND \"workspace_files\".\"workspace_id\" IS NOT NULL", - "concurrently": false, - "method": "btree", - "with": {} - }, - "workspace_files_chat_display_name_unique": { - "name": "workspace_files_chat_display_name_unique", - "columns": [ - { - "expression": "chat_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "display_name", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "where": "\"workspace_files\".\"context\" = 'mothership' AND \"workspace_files\".\"chat_id\" IS NOT NULL", - "concurrently": false, - "method": "btree", - "with": {} - }, - "workspace_files_key_idx": { - "name": "workspace_files_key_idx", - "columns": [ - { - "expression": "key", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workspace_files_user_id_idx": { - "name": "workspace_files_user_id_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workspace_files_workspace_id_idx": { - "name": "workspace_files_workspace_id_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workspace_files_context_idx": { - "name": "workspace_files_context_idx", - "columns": [ - { - "expression": "context", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workspace_files_chat_id_idx": { - "name": "workspace_files_chat_id_idx", - "columns": [ - { - "expression": "chat_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workspace_files_deleted_at_idx": { - "name": "workspace_files_deleted_at_idx", - "columns": [ - { - "expression": "deleted_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workspace_files_workspace_deleted_partial_idx": { - "name": "workspace_files_workspace_deleted_partial_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "deleted_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "where": "\"workspace_files\".\"deleted_at\" IS NOT NULL", - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "workspace_files_user_id_user_id_fk": { - "name": "workspace_files_user_id_user_id_fk", - "tableFrom": "workspace_files", - "tableTo": "user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "workspace_files_workspace_id_workspace_id_fk": { - "name": "workspace_files_workspace_id_workspace_id_fk", - "tableFrom": "workspace_files", - "tableTo": "workspace", - "columnsFrom": ["workspace_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "workspace_files_chat_id_copilot_chats_id_fk": { - "name": "workspace_files_chat_id_copilot_chats_id_fk", - "tableFrom": "workspace_files", - "tableTo": "copilot_chats", - "columnsFrom": ["chat_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.workspace_notification_delivery": { - "name": "workspace_notification_delivery", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "subscription_id": { - "name": "subscription_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "workflow_id": { - "name": "workflow_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "execution_id": { - "name": "execution_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "status": { - "name": "status", - "type": "notification_delivery_status", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'pending'" - }, - "attempts": { - "name": "attempts", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "last_attempt_at": { - "name": "last_attempt_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "next_attempt_at": { - "name": "next_attempt_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "response_status": { - "name": "response_status", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "response_body": { - "name": "response_body", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "error_message": { - "name": "error_message", - "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": { - "workspace_notification_delivery_subscription_id_idx": { - "name": "workspace_notification_delivery_subscription_id_idx", - "columns": [ - { - "expression": "subscription_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workspace_notification_delivery_execution_id_idx": { - "name": "workspace_notification_delivery_execution_id_idx", - "columns": [ - { - "expression": "execution_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workspace_notification_delivery_status_idx": { - "name": "workspace_notification_delivery_status_idx", - "columns": [ - { - "expression": "status", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workspace_notification_delivery_next_attempt_idx": { - "name": "workspace_notification_delivery_next_attempt_idx", - "columns": [ - { - "expression": "next_attempt_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "workspace_notification_delivery_subscription_id_workspace_notification_subscription_id_fk": { - "name": "workspace_notification_delivery_subscription_id_workspace_notification_subscription_id_fk", - "tableFrom": "workspace_notification_delivery", - "tableTo": "workspace_notification_subscription", - "columnsFrom": ["subscription_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "workspace_notification_delivery_workflow_id_workflow_id_fk": { - "name": "workspace_notification_delivery_workflow_id_workflow_id_fk", - "tableFrom": "workspace_notification_delivery", - "tableTo": "workflow", - "columnsFrom": ["workflow_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.workspace_notification_subscription": { - "name": "workspace_notification_subscription", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "workspace_id": { - "name": "workspace_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "notification_type": { - "name": "notification_type", - "type": "notification_type", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "workflow_ids": { - "name": "workflow_ids", - "type": "text[]", - "primaryKey": false, - "notNull": true, - "default": "'{}'::text[]" - }, - "all_workflows": { - "name": "all_workflows", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "level_filter": { - "name": "level_filter", - "type": "text[]", - "primaryKey": false, - "notNull": true, - "default": "ARRAY['info', 'error']::text[]" - }, - "trigger_filter": { - "name": "trigger_filter", - "type": "text[]", - "primaryKey": false, - "notNull": true, - "default": "ARRAY['api', 'webhook', 'schedule', 'manual', 'chat']::text[]" - }, - "include_final_output": { - "name": "include_final_output", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "include_trace_spans": { - "name": "include_trace_spans", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "include_rate_limits": { - "name": "include_rate_limits", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "include_usage_data": { - "name": "include_usage_data", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "webhook_config": { - "name": "webhook_config", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "email_recipients": { - "name": "email_recipients", - "type": "text[]", - "primaryKey": false, - "notNull": false - }, - "slack_config": { - "name": "slack_config", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "alert_config": { - "name": "alert_config", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "last_alert_at": { - "name": "last_alert_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "active": { - "name": "active", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "created_by": { - "name": "created_by", - "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": { - "workspace_notification_workspace_id_idx": { - "name": "workspace_notification_workspace_id_idx", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workspace_notification_active_idx": { - "name": "workspace_notification_active_idx", - "columns": [ - { - "expression": "active", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "workspace_notification_type_idx": { - "name": "workspace_notification_type_idx", - "columns": [ - { - "expression": "notification_type", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "workspace_notification_subscription_workspace_id_workspace_id_fk": { - "name": "workspace_notification_subscription_workspace_id_workspace_id_fk", - "tableFrom": "workspace_notification_subscription", - "tableTo": "workspace", - "columnsFrom": ["workspace_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "workspace_notification_subscription_created_by_user_id_fk": { - "name": "workspace_notification_subscription_created_by_user_id_fk", - "tableFrom": "workspace_notification_subscription", - "tableTo": "user", - "columnsFrom": ["created_by"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - } - }, - "enums": { - "public.a2a_task_status": { - "name": "a2a_task_status", - "schema": "public", - "values": [ - "submitted", - "working", - "input-required", - "completed", - "failed", - "canceled", - "rejected", - "auth-required", - "unknown" - ] - }, - "public.academy_cert_status": { - "name": "academy_cert_status", - "schema": "public", - "values": ["active", "revoked", "expired"] - }, - "public.billing_blocked_reason": { - "name": "billing_blocked_reason", - "schema": "public", - "values": ["payment_failed", "dispute"] - }, - "public.chat_type": { - "name": "chat_type", - "schema": "public", - "values": ["mothership", "copilot"] - }, - "public.copilot_async_tool_status": { - "name": "copilot_async_tool_status", - "schema": "public", - "values": ["pending", "running", "completed", "failed", "cancelled", "delivered"] - }, - "public.copilot_run_status": { - "name": "copilot_run_status", - "schema": "public", - "values": ["active", "paused_waiting_for_tool", "resuming", "complete", "error", "cancelled"] - }, - "public.credential_member_role": { - "name": "credential_member_role", - "schema": "public", - "values": ["admin", "member"] - }, - "public.credential_member_status": { - "name": "credential_member_status", - "schema": "public", - "values": ["active", "pending", "revoked"] - }, - "public.credential_set_invitation_status": { - "name": "credential_set_invitation_status", - "schema": "public", - "values": ["pending", "accepted", "expired", "cancelled"] - }, - "public.credential_set_member_status": { - "name": "credential_set_member_status", - "schema": "public", - "values": ["active", "pending", "revoked"] - }, - "public.credential_type": { - "name": "credential_type", - "schema": "public", - "values": ["oauth", "env_workspace", "env_personal", "service_account"] - }, - "public.data_drain_cadence": { - "name": "data_drain_cadence", - "schema": "public", - "values": ["hourly", "daily"] - }, - "public.data_drain_destination": { - "name": "data_drain_destination", - "schema": "public", - "values": ["s3", "gcs", "azure_blob", "datadog", "bigquery", "snowflake", "webhook"] - }, - "public.data_drain_run_status": { - "name": "data_drain_run_status", - "schema": "public", - "values": ["running", "success", "failed"] - }, - "public.data_drain_run_trigger": { - "name": "data_drain_run_trigger", - "schema": "public", - "values": ["cron", "manual"] - }, - "public.data_drain_source": { - "name": "data_drain_source", - "schema": "public", - "values": ["workflow_logs", "job_logs", "audit_logs", "copilot_chats", "copilot_runs"] - }, - "public.invitation_kind": { - "name": "invitation_kind", - "schema": "public", - "values": ["organization", "workspace"] - }, - "public.invitation_membership_intent": { - "name": "invitation_membership_intent", - "schema": "public", - "values": ["internal", "external"] - }, - "public.invitation_status": { - "name": "invitation_status", - "schema": "public", - "values": ["pending", "accepted", "rejected", "cancelled", "expired"] - }, - "public.notification_delivery_status": { - "name": "notification_delivery_status", - "schema": "public", - "values": ["pending", "in_progress", "success", "failed"] - }, - "public.notification_type": { - "name": "notification_type", - "schema": "public", - "values": ["webhook", "email", "slack"] - }, - "public.permission_type": { - "name": "permission_type", - "schema": "public", - "values": ["admin", "write", "read"] - }, - "public.template_creator_type": { - "name": "template_creator_type", - "schema": "public", - "values": ["user", "organization"] - }, - "public.template_status": { - "name": "template_status", - "schema": "public", - "values": ["pending", "approved", "rejected"] - }, - "public.usage_log_category": { - "name": "usage_log_category", - "schema": "public", - "values": ["model", "fixed"] - }, - "public.usage_log_source": { - "name": "usage_log_source", - "schema": "public", - "values": [ - "workflow", - "wand", - "copilot", - "workspace-chat", - "mcp_copilot", - "mothership_block", - "knowledge-base", - "voice-input" - ] - }, - "public.workspace_mode": { - "name": "workspace_mode", - "schema": "public", - "values": ["personal", "organization", "grandfathered_shared"] - } - }, - "schemas": {}, - "sequences": {}, - "roles": {}, - "policies": {}, - "views": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} diff --git a/packages/db/schema.ts b/packages/db/schema.ts index c3dce4901ab..46717c43188 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -1167,6 +1167,44 @@ export const workspaceFile = pgTable( }) ) +export const workspaceFileFolder = pgTable( + 'workspace_file_folders', + { + 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'), + sortOrder: integer('sort_order').notNull().default(0), + deletedAt: timestamp('deleted_at'), + createdAt: timestamp('created_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at').notNull().defaultNow(), + }, + (table) => ({ + workspaceParentIdx: index('workspace_file_folders_workspace_parent_idx').on( + table.workspaceId, + table.parentId + ), + parentSortIdx: index('workspace_file_folders_parent_sort_idx').on( + table.parentId, + table.sortOrder + ), + deletedAtIdx: index('workspace_file_folders_deleted_at_idx').on(table.deletedAt), + workspaceDeletedAtPartialIdx: index('workspace_file_folders_workspace_deleted_partial_idx') + .on(table.workspaceId, table.deletedAt) + .where(sql`${table.deletedAt} IS NOT NULL`), + workspaceParentNameActiveUnique: uniqueIndex( + 'workspace_file_folders_workspace_parent_name_active_unique' + ) + .on(table.workspaceId, sql`coalesce(${table.parentId}, '')`, table.name) + .where(sql`${table.deletedAt} IS NULL`), + }) +) + export const workspaceFiles = pgTable( 'workspace_files', { @@ -1176,6 +1214,9 @@ export const workspaceFiles = pgTable( .notNull() .references(() => user.id, { onDelete: 'cascade' }), workspaceId: text('workspace_id').references(() => workspace.id, { onDelete: 'cascade' }), + folderId: text('folder_id').references(() => workspaceFileFolder.id, { + onDelete: 'set null', + }), context: text('context').notNull(), // 'workspace', 'mothership', 'copilot', 'chat', 'knowledge-base', 'profile-pictures', 'general', 'execution' chatId: uuid('chat_id').references(() => copilotChats.id, { onDelete: 'cascade' }), originalName: text('original_name').notNull(), @@ -1199,9 +1240,11 @@ export const workspaceFiles = pgTable( keyActiveUniqueIdx: uniqueIndex('workspace_files_key_active_unique') .on(table.key) .where(sql`${table.deletedAt} IS NULL`), - /** One active display name per workspace for workspace-scoped files (VFS / file picker). */ - workspaceOriginalNameActiveUnique: uniqueIndex('workspace_files_workspace_name_active_unique') - .on(table.workspaceId, table.originalName) + /** One active display name per workspace file folder. */ + workspaceFolderOriginalNameActiveUnique: uniqueIndex( + 'workspace_files_workspace_folder_name_active_unique' + ) + .on(table.workspaceId, sql`coalesce(${table.folderId}, '')`, table.originalName) .where( sql`${table.deletedAt} IS NULL AND ${table.context} = 'workspace' AND ${table.workspaceId} IS NOT NULL` ), @@ -1219,6 +1262,7 @@ export const workspaceFiles = pgTable( keyIdx: index('workspace_files_key_idx').on(table.key), userIdIdx: index('workspace_files_user_id_idx').on(table.userId), workspaceIdIdx: index('workspace_files_workspace_id_idx').on(table.workspaceId), + folderIdIdx: index('workspace_files_folder_id_idx').on(table.folderId), contextIdx: index('workspace_files_context_idx').on(table.context), chatIdIdx: index('workspace_files_chat_id_idx').on(table.chatId), deletedAtIdx: index('workspace_files_deleted_at_idx').on(table.deletedAt), diff --git a/scripts/check-api-validation-contracts.ts b/scripts/check-api-validation-contracts.ts index 1081576504d..88b4de992b0 100644 --- a/scripts/check-api-validation-contracts.ts +++ b/scripts/check-api-validation-contracts.ts @@ -9,8 +9,8 @@ const QUERY_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/queries') const SELECTOR_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/selectors') const BASELINE = { - totalRoutes: 736, - zodRoutes: 736, + totalRoutes: 741, + zodRoutes: 741, nonZodRoutes: 0, } as const From c76f7ae87b70151e6357007bc53b74dd34848432 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 12 May 2026 14:43:31 -0700 Subject: [PATCH 03/60] address comments --- .../workspaces/[id]/files/download/route.ts | 5 +- .../workspace/[workspaceId]/files/files.tsx | 65 +++++-- .../api/contracts/workspace-file-folders.ts | 13 +- apps/sim/lib/api/contracts/workspace-files.ts | 17 +- .../tools/server/files/workspace-file.ts | 7 +- .../workspace-file-folder-manager.test.ts | 15 +- .../workspace-file-folder-manager.ts | 164 +++++++++++------- .../workspace/workspace-file-manager.ts | 35 ++-- apps/sim/lib/uploads/core/storage-service.ts | 1 + 9 files changed, 217 insertions(+), 105 deletions(-) diff --git a/apps/sim/app/api/workspaces/[id]/files/download/route.ts b/apps/sim/app/api/workspaces/[id]/files/download/route.ts index 948ed23b313..3813d2fe8f1 100644 --- a/apps/sim/app/api/workspaces/[id]/files/download/route.ts +++ b/apps/sim/app/api/workspaces/[id]/files/download/route.ts @@ -17,7 +17,10 @@ const logger = createLogger('WorkspaceFilesDownloadAPI') function safeZipPath(path: string): string { return path .split('/') - .map((segment) => segment.trim().replace(/[<>:"\\|?*\x00-\x1f]/g, '_')) + .map((segment) => { + const cleaned = segment.trim().replace(/[<>:"\\|?*\x00-\x1f]/g, '_') + return cleaned === '.' || cleaned === '..' ? '_' : cleaned + }) .filter(Boolean) .join('/') } diff --git a/apps/sim/app/workspace/[workspaceId]/files/files.tsx b/apps/sim/app/workspace/[workspaceId]/files/files.tsx index 4469a2179f8..bc894bea5bc 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/files.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/files.tsx @@ -689,7 +689,7 @@ export function Files() { ) async function uploadFiles(filesToUpload: File[], targetFolderId = currentFolderId) { - if (!workspaceId || filesToUpload.length === 0) return + if (!workspaceId || filesToUpload.length === 0 || !canEdit) return const oversized: string[] = [] const sizeFiltered = filesToUpload.filter((f) => { @@ -904,14 +904,19 @@ export function Files() { }, [selectedFileIds, selectedFolderIds, currentFolderId]) const handleMoveSelected = useCallback(async () => { - await moveItems.mutateAsync({ - workspaceId, - fileIds: selectedFileIds, - folderIds: selectedFolderIds, - targetFolderId: moveTargetFolderId, - }) - setSelectedRowIds(new Set()) - setShowMoveModal(false) + try { + await moveItems.mutateAsync({ + workspaceId, + fileIds: selectedFileIds, + folderIds: selectedFolderIds, + targetFolderId: moveTargetFolderId, + }) + setSelectedRowIds(new Set()) + setShowMoveModal(false) + } catch (error) { + logger.error('Failed to move selected items:', error) + toast.error(error instanceof Error ? error.message : 'Failed to move selected items') + } }, [workspaceId, selectedFileIds, selectedFolderIds, moveTargetFolderId, moveItems]) const fileDetailBreadcrumbs = useMemo( @@ -1075,10 +1080,21 @@ export function Files() { const handleContextMenuDownload = useCallback(() => { const item = contextMenuItemRef.current - if (!item || item.kind !== 'file') return + if (!item) return + const rowId = item.kind === 'file' ? fileRowId(item.file.id) : folderRowId(item.folder.id) + if (selectedRowIds.has(rowId) && selectedRowIds.size > 1) { + handleBulkDownload() + closeContextMenu() + return + } + if (item.kind === 'folder') { + window.location.href = `/api/workspaces/${workspaceId}/files/download?folderIds=${encodeURIComponent(item.folder.id)}` + closeContextMenu() + return + } handleDownload(item.file) closeContextMenu() - }, [handleDownload, closeContextMenu]) + }, [selectedRowIds, handleBulkDownload, closeContextMenu, workspaceId, handleDownload]) const handleContextMenuRename = useCallback(() => { const item = contextMenuItemRef.current @@ -1091,6 +1107,12 @@ export function Files() { const handleContextMenuDelete = useCallback(() => { const item = contextMenuItemRef.current if (!item) return + const rowId = item.kind === 'file' ? fileRowId(item.file.id) : folderRowId(item.folder.id) + if (selectedRowIds.has(rowId) && selectedRowIds.size > 1) { + handleBulkDelete() + closeContextMenu() + return + } setDeleteTarget( item.kind === 'file' ? { fileIds: [item.file.id], folderIds: [], name: item.file.name } @@ -1098,7 +1120,7 @@ export function Files() { ) setShowDeleteConfirm(true) closeContextMenu() - }, [closeContextMenu]) + }, [selectedRowIds, handleBulkDelete, closeContextMenu]) const handleContentContextMenu = useCallback( (e: React.MouseEvent) => { @@ -1115,9 +1137,10 @@ export function Files() { ) const handleListUploadFile = useCallback(() => { + if (!canEdit || uploading) return fileInputRef.current?.click() closeListContextMenu() - }, [closeListContextMenu]) + }, [canEdit, uploading, closeListContextMenu]) const prevFileIdRef = useRef(fileIdFromRoute) if (fileIdFromRoute !== prevFileIdRef.current) { @@ -1279,8 +1302,9 @@ export function Files() { ) const handleUploadClick = useCallback(() => { + if (!canEdit || uploading) return fileInputRef.current?.click() - }, []) + }, [canEdit, uploading]) const searchConfig: SearchConfig = { value: inputValue, @@ -1310,6 +1334,7 @@ export function Files() { label: uploadButtonLabel, icon: Upload, onClick: handleUploadClick, + disabled: uploading || !canEdit, }, { label: 'New folder', @@ -1372,14 +1397,20 @@ export function Files() { () => [ { value: '__root__', label: 'Files' }, ...folders - .filter((folder) => !selectedFolderIds.includes(folder.id)) + .filter((folder) => { + if (selectedFolderIds.includes(folder.id)) return false + return selectedFolderIds.every( + (selectedFolderId) => + !descendantFolderIdsByFolderId.get(selectedFolderId)?.has(folder.id) + ) + }) .map((folder) => ({ value: folder.id, label: folder.path, icon: Folder, })), ], - [folders, selectedFolderIds] + [folders, selectedFolderIds, descendantFolderIdsByFolderId] ) const sortConfig: SortConfig = useMemo( @@ -1734,7 +1765,7 @@ export function Files() { type='file' className='hidden' onChange={handleFileChange} - disabled={uploading} + disabled={uploading || !canEdit} accept={ACCEPT_ATTR} multiple /> diff --git a/apps/sim/lib/api/contracts/workspace-file-folders.ts b/apps/sim/lib/api/contracts/workspace-file-folders.ts index b2c685bcfdb..bae72cee1da 100644 --- a/apps/sim/lib/api/contracts/workspace-file-folders.ts +++ b/apps/sim/lib/api/contracts/workspace-file-folders.ts @@ -34,13 +34,22 @@ const workspaceFileFoldersSuccessSchema = z.object({ success: z.boolean(), }) +const workspaceFileFolderNameSchema = z + .string({ error: 'Name is required' }) + .trim() + .min(1, 'Name is required') + .refine( + (name) => name !== '.' && name !== '..' && !name.includes('/') && !name.includes('\\'), + 'Name cannot contain path separators or dot segments' + ) + export const createWorkspaceFileFolderBodySchema = z.object({ - name: z.string({ error: 'Name is required' }).trim().min(1, 'Name is required'), + name: workspaceFileFolderNameSchema, parentId: z.string().nullable().optional(), }) export const updateWorkspaceFileFolderBodySchema = z.object({ - name: z.string().trim().min(1, 'Name is required').optional(), + name: workspaceFileFolderNameSchema.optional(), parentId: z.string().nullable().optional(), sortOrder: z.number().int().optional(), }) diff --git a/apps/sim/lib/api/contracts/workspace-files.ts b/apps/sim/lib/api/contracts/workspace-files.ts index 3f4498e6540..7ea83ca4c5f 100644 --- a/apps/sim/lib/api/contracts/workspace-files.ts +++ b/apps/sim/lib/api/contracts/workspace-files.ts @@ -15,10 +15,17 @@ export const listWorkspaceFilesQuerySchema = z.object({ scope: workspaceFileScopeSchema.default('active'), }) +const workspaceFileNameSchema = z + .string({ error: 'Name is required' }) + .trim() + .min(1, 'Name is required') + .refine( + (name) => name !== '.' && name !== '..' && !name.includes('/') && !name.includes('\\'), + 'Name cannot contain path separators or dot segments' + ) + export const renameWorkspaceFileBodySchema = z.object({ - name: z - .string({ error: 'Name is required' }) - .refine((name) => name.trim().length > 0, { message: 'Name is required' }), + name: workspaceFileNameSchema, }) export const updateWorkspaceFileContentBodySchema = z.object({ @@ -166,7 +173,7 @@ export const workspaceFileCompiledCheckContract = defineRouteContract({ }) export const workspacePresignedUploadBodySchema = z.object({ - fileName: z.string().min(1, 'fileName is required'), + fileName: workspaceFileNameSchema, contentType: z.string().min(1, 'contentType is required'), fileSize: z.number().nonnegative('fileSize must be a non-negative number'), folderId: z.string().nullable().optional(), @@ -203,7 +210,7 @@ export const workspacePresignedUploadContract = defineRouteContract({ export const registerWorkspaceFileBodySchema = z.object({ key: z.string().min(1, 'key is required'), - name: z.string().min(1, 'name is required'), + name: workspaceFileNameSchema, contentType: z.string().min(1, 'contentType is required'), folderId: z.string().nullable().optional(), }) diff --git a/apps/sim/lib/copilot/tools/server/files/workspace-file.ts b/apps/sim/lib/copilot/tools/server/files/workspace-file.ts index 4e43a72375b..5d2912502fe 100644 --- a/apps/sim/lib/copilot/tools/server/files/workspace-file.ts +++ b/apps/sim/lib/copilot/tools/server/files/workspace-file.ts @@ -97,8 +97,13 @@ export function inferContentType(fileName: string, explicitType?: string): strin export function validateFlatWorkspaceFileName(fileName: string): string | null { const trimmed = fileName.trim() if (!trimmed) return 'File name cannot be empty' - if (trimmed.split('/').some((segment) => !segment.trim())) + const segments = trimmed.split('/').map((segment) => segment.trim()) + if (segments.some((segment) => !segment)) { return 'File path cannot contain empty segments' + } + if (segments.some((segment) => segment === '.' || segment === '..' || segment.includes('\\'))) { + return 'File path cannot contain dot segments or backslashes' + } return null } diff --git a/apps/sim/lib/uploads/contexts/workspace/workspace-file-folder-manager.test.ts b/apps/sim/lib/uploads/contexts/workspace/workspace-file-folder-manager.test.ts index 8d637f8d06d..e0f9347d1fd 100644 --- a/apps/sim/lib/uploads/contexts/workspace/workspace-file-folder-manager.test.ts +++ b/apps/sim/lib/uploads/contexts/workspace/workspace-file-folder-manager.test.ts @@ -3,7 +3,10 @@ */ import { describe, expect, it } from 'vitest' -import { buildWorkspaceFileFolderPathMap } from './workspace-file-folder-manager' +import { + buildWorkspaceFileFolderPathMap, + normalizeWorkspaceFileItemName, +} from './workspace-file-folder-manager' describe('workspace file folder paths', () => { it('builds nested paths from parent relationships', () => { @@ -17,4 +20,14 @@ describe('workspace file folder paths', () => { expect(paths.get('quarterly')).toBe('Reports/Quarterly') expect(paths.get('archive')).toBe('Archive') }) + + it('rejects names that would create ambiguous paths', () => { + expect(normalizeWorkspaceFileItemName('Reports', 'Folder')).toBe('Reports') + expect(() => normalizeWorkspaceFileItemName('A/B', 'Folder')).toThrow( + 'Folder name cannot contain path separators or dot segments' + ) + expect(() => normalizeWorkspaceFileItemName('..', 'File')).toThrow( + 'File name cannot contain path separators or dot segments' + ) + }) }) diff --git a/apps/sim/lib/uploads/contexts/workspace/workspace-file-folder-manager.ts b/apps/sim/lib/uploads/contexts/workspace/workspace-file-folder-manager.ts index b101d569691..5ba8f757b1e 100644 --- a/apps/sim/lib/uploads/contexts/workspace/workspace-file-folder-manager.ts +++ b/apps/sim/lib/uploads/contexts/workspace/workspace-file-folder-manager.ts @@ -54,6 +54,17 @@ export interface WorkspaceFileArchiveResult { files: number } +export function normalizeWorkspaceFileItemName(name: string, itemLabel: 'File' | 'Folder'): string { + const trimmed = name.trim() + if (!trimmed) { + throw new Error(`${itemLabel} name is required`) + } + if (trimmed === '.' || trimmed === '..' || trimmed.includes('/') || trimmed.includes('\\')) { + throw new Error(`${itemLabel} name cannot contain path separators or dot segments`) + } + return trimmed +} + function normalizeParentId(parentId?: string | null): string | null { return parentId && parentId.length > 0 ? parentId : null } @@ -234,8 +245,7 @@ export async function createWorkspaceFileFolder(params: { sortOrder?: number }): Promise { const parentId = await assertWorkspaceFileFolderTarget(params.workspaceId, params.parentId) - const name = params.name.trim() - if (!name) throw new Error('Folder name is required') + const name = normalizeWorkspaceFileItemName(params.name, 'Folder') if (await workspaceFileFolderExists(params.workspaceId, name, parentId)) { throw new WorkspaceFileFolderConflictError(name) @@ -254,8 +264,8 @@ export async function createWorkspaceFileFolder(params: { }) .returning() - const paths = buildWorkspaceFileFolderPathMap([folder]) - return mapFolder(folder, paths) + const folders = await listWorkspaceFileFolders(params.workspaceId) + return folders.find((item) => item.id === folder.id) ?? mapFolder(folder, new Map()) } export async function ensureWorkspaceFileFolderPath(params: { @@ -265,8 +275,7 @@ export async function ensureWorkspaceFileFolderPath(params: { }): Promise { let parentId: string | null = null for (const rawSegment of params.pathSegments) { - const name = rawSegment.trim() - if (!name) throw new Error('Folder path cannot contain empty segments') + const name = normalizeWorkspaceFileItemName(rawSegment, 'Folder') const folders = await listWorkspaceFileFolders(params.workspaceId) const existing = folders.find( @@ -327,44 +336,37 @@ export async function updateWorkspaceFileFolder(params: { if (!existing) throw new Error('Folder not found') const updates: Partial = { updatedAt: new Date() } + const finalName = + params.name !== undefined + ? normalizeWorkspaceFileItemName(params.name, 'Folder') + : existing.name + const finalParentId = + params.parentId !== undefined + ? await assertWorkspaceFileFolderTarget(params.workspaceId, params.parentId) + : existing.parentId - if (params.name !== undefined) { - const name = params.name.trim() - if (!name) throw new Error('Folder name is required') - if ( - name !== existing.name && - (await workspaceFileFolderExists( - params.workspaceId, - name, - existing.parentId, - params.folderId - )) - ) { - throw new WorkspaceFileFolderConflictError(name) - } - updates.name = name - } + if (finalParentId === params.folderId) throw new Error('Folder cannot be its own parent') if (params.parentId !== undefined) { - const parentId = await assertWorkspaceFileFolderTarget(params.workspaceId, params.parentId) - if (parentId === params.folderId) throw new Error('Folder cannot be its own parent') - const descendants = await getDescendantFolderIds(params.workspaceId, params.folderId) - if (parentId && descendants.includes(parentId)) { + if (finalParentId && descendants.includes(finalParentId)) { throw new Error('Cannot move a folder into one of its descendants') } + } - if ( - await workspaceFileFolderExists( - params.workspaceId, - params.name?.trim() || existing.name, - parentId, - params.folderId - ) - ) { - throw new WorkspaceFileFolderConflictError(params.name?.trim() || existing.name) - } - updates.parentId = parentId + if ( + (finalName !== existing.name || finalParentId !== existing.parentId) && + (await workspaceFileFolderExists(params.workspaceId, finalName, finalParentId, params.folderId)) + ) { + throw new WorkspaceFileFolderConflictError(finalName) + } + + if (params.name !== undefined) { + updates.name = finalName + } + + if (params.parentId !== undefined) { + updates.parentId = finalParentId } if (params.sortOrder !== undefined) { @@ -451,6 +453,20 @@ export async function moveWorkspaceFileItems(params: { ) : [] + const movingFolders = + folderIds.length > 0 + ? await db + .select({ id: workspaceFileFolder.id, name: workspaceFileFolder.name }) + .from(workspaceFileFolder) + .where( + and( + inArray(workspaceFileFolder.id, folderIds), + eq(workspaceFileFolder.workspaceId, params.workspaceId), + isNull(workspaceFileFolder.deletedAt) + ) + ) + : [] + for (const file of movingFiles) { if ( await fileNameExistsInWorkspaceFolder(params.workspaceId, file.name, targetFolderId, file.id) @@ -459,38 +475,56 @@ export async function moveWorkspaceFileItems(params: { } } - const movedFiles = - fileIds.length > 0 - ? await db - .update(workspaceFiles) - .set({ folderId: targetFolderId, updatedAt: new Date() }) - .where( - and( - inArray(workspaceFiles.id, fileIds), - eq(workspaceFiles.workspaceId, params.workspaceId), - eq(workspaceFiles.context, 'workspace'), - isNull(workspaceFiles.deletedAt) + const movingFolderNameCounts = new Map() + for (const folder of movingFolders) { + movingFolderNameCounts.set(folder.name, (movingFolderNameCounts.get(folder.name) ?? 0) + 1) + if ( + await workspaceFileFolderExists(params.workspaceId, folder.name, targetFolderId, folder.id) + ) { + throw new WorkspaceFileFolderConflictError(folder.name) + } + } + + for (const [name, count] of movingFolderNameCounts) { + if (count > 1) { + throw new WorkspaceFileFolderConflictError(name) + } + } + + return db.transaction(async (tx) => { + const movedFiles = + fileIds.length > 0 + ? await tx + .update(workspaceFiles) + .set({ folderId: targetFolderId, updatedAt: new Date() }) + .where( + and( + inArray(workspaceFiles.id, fileIds), + eq(workspaceFiles.workspaceId, params.workspaceId), + eq(workspaceFiles.context, 'workspace'), + isNull(workspaceFiles.deletedAt) + ) ) - ) - .returning({ id: workspaceFiles.id }) - : [] + .returning({ id: workspaceFiles.id }) + : [] - const movedFolders = - folderIds.length > 0 - ? await db - .update(workspaceFileFolder) - .set({ parentId: targetFolderId, updatedAt: new Date() }) - .where( - and( - inArray(workspaceFileFolder.id, folderIds), - eq(workspaceFileFolder.workspaceId, params.workspaceId), - isNull(workspaceFileFolder.deletedAt) + const movedFolders = + folderIds.length > 0 + ? await tx + .update(workspaceFileFolder) + .set({ parentId: targetFolderId, updatedAt: new Date() }) + .where( + and( + inArray(workspaceFileFolder.id, folderIds), + eq(workspaceFileFolder.workspaceId, params.workspaceId), + isNull(workspaceFileFolder.deletedAt) + ) ) - ) - .returning({ id: workspaceFileFolder.id }) - : [] + .returning({ id: workspaceFileFolder.id }) + : [] - return { movedFiles: movedFiles.length, movedFolders: movedFolders.length } + return { movedFiles: movedFiles.length, movedFolders: movedFolders.length } + }) } export async function archiveWorkspaceFileFolderRecursive( diff --git a/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts b/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts index 1a5c14b1e27..858b4b164df 100644 --- a/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts +++ b/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts @@ -34,6 +34,7 @@ import { buildWorkspaceFileFolderPathMap, fileNameExistsInWorkspaceFolder, listWorkspaceFileFolders, + normalizeWorkspaceFileItemName, } from './workspace-file-folder-manager' const logger = createLogger('WorkspaceFileStorage') @@ -161,6 +162,7 @@ export async function uploadWorkspaceFile( logger.info(`Uploading workspace file: ${fileName} for workspace ${workspaceId}`) const folderId = await assertWorkspaceFileFolderTarget(workspaceId, options?.folderId) + const normalizedFileName = normalizeWorkspaceFileItemName(fileName, 'File') const quotaCheck = await checkStorageQuota(userId, fileBuffer.length) if (!quotaCheck.allowed) { @@ -169,7 +171,11 @@ export async function uploadWorkspaceFile( let lastError: unknown for (let attempt = 0; attempt < MAX_UPLOAD_UNIQUE_RETRIES; attempt++) { - const uniqueName = await allocateUniqueWorkspaceFileName(workspaceId, fileName, folderId) + const uniqueName = await allocateUniqueWorkspaceFileName( + workspaceId, + normalizedFileName, + folderId + ) const storageKey = generateWorkspaceFileKey(workspaceId, uniqueName) let fileId = `wf_${generateShortId()}` @@ -308,6 +314,7 @@ export async function registerUploadedWorkspaceFile(params: { }): Promise { const { workspaceId, userId, key, originalName, contentType } = params const folderId = await assertWorkspaceFileFolderTarget(workspaceId, params.folderId) + const normalizedOriginalName = normalizeWorkspaceFileItemName(originalName, 'File') if (!hasCloudStorage()) { throw new Error('Direct-upload registration requires cloud storage') @@ -356,7 +363,11 @@ export async function registerUploadedWorkspaceFile(params: { let lastInsertError: unknown for (let attempt = 0; attempt < MAX_UPLOAD_UNIQUE_RETRIES; attempt++) { fileId = `wf_${generateShortId()}` - displayName = await allocateUniqueWorkspaceFileName(workspaceId, originalName, folderId) + displayName = await allocateUniqueWorkspaceFileName( + workspaceId, + normalizedOriginalName, + folderId + ) try { await insertFileMetadata({ id: fileId, @@ -391,7 +402,7 @@ export async function registerUploadedWorkspaceFile(params: { ) await cleanupOrphan('metadata insert failure') if (getPostgresErrorCode(lastInsertError) === '23505') { - throw new FileConflictError(originalName) + throw new FileConflictError(normalizedOriginalName) } throw lastInsertError instanceof Error ? lastInsertError @@ -887,29 +898,27 @@ export async function renameWorkspaceFile( logger.info(`Renaming workspace file: ${fileId} to "${newName}" in workspace ${workspaceId}`) const trimmedName = newName.trim() - if (!trimmedName) { - throw new Error('File name cannot be empty') - } + const normalizedName = normalizeWorkspaceFileItemName(trimmedName, 'File') const fileRecord = await getWorkspaceFile(workspaceId, fileId) if (!fileRecord) { throw new Error('File not found') } - if (fileRecord.name === trimmedName) { + if (fileRecord.name === normalizedName) { return fileRecord } - const exists = await fileExistsInWorkspace(workspaceId, trimmedName, fileRecord.folderId) + const exists = await fileExistsInWorkspace(workspaceId, normalizedName, fileRecord.folderId) if (exists) { - throw new FileConflictError(trimmedName) + throw new FileConflictError(normalizedName) } let updated: { id: string }[] try { updated = await db .update(workspaceFiles) - .set({ originalName: trimmedName, updatedAt: new Date() }) + .set({ originalName: normalizedName, updatedAt: new Date() }) .where( and( eq(workspaceFiles.id, fileId), @@ -920,7 +929,7 @@ export async function renameWorkspaceFile( .returning({ id: workspaceFiles.id }) } catch (error: unknown) { if (getPostgresErrorCode(error) === '23505') { - throw new FileConflictError(trimmedName) + throw new FileConflictError(normalizedName) } throw error } @@ -929,11 +938,11 @@ export async function renameWorkspaceFile( throw new Error('File not found or could not be renamed') } - logger.info(`Successfully renamed workspace file ${fileId} to "${trimmedName}"`) + logger.info(`Successfully renamed workspace file ${fileId} to "${normalizedName}"`) return { ...fileRecord, - name: trimmedName, + name: normalizedName, } } diff --git a/apps/sim/lib/uploads/core/storage-service.ts b/apps/sim/lib/uploads/core/storage-service.ts index 93ee3c734b7..7414b6be8bb 100644 --- a/apps/sim/lib/uploads/core/storage-service.ts +++ b/apps/sim/lib/uploads/core/storage-service.ts @@ -74,6 +74,7 @@ async function insertFileMetadataHelper( key, userId: metadata.userId, workspaceId: metadata.workspaceId || null, + folderId: metadata.folderId || null, context, originalName: metadata.originalName || fileName, contentType, From fe11df22ac01db63ffc91eb9ae05c2e1f84ff00e Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 12 May 2026 15:20:06 -0700 Subject: [PATCH 04/60] address comments --- .../api/workspaces/[id]/files/move/route.ts | 19 +- .../workspace/[workspaceId]/files/files.tsx | 139 ++++++------- .../workspace-file-folder-manager.ts | 186 +++++++++++++----- packages/db/schema.ts | 5 +- 4 files changed, 228 insertions(+), 121 deletions(-) diff --git a/apps/sim/app/api/workspaces/[id]/files/move/route.ts b/apps/sim/app/api/workspaces/[id]/files/move/route.ts index 2422a935d8b..6e626caee43 100644 --- a/apps/sim/app/api/workspaces/[id]/files/move/route.ts +++ b/apps/sim/app/api/workspaces/[id]/files/move/route.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { getPostgresErrorCode } from '@sim/utils/errors' import { type NextRequest, NextResponse } from 'next/server' import { moveWorkspaceFileItemsContract } from '@/lib/api/contracts/workspace-file-folders' import { parseRequest } from '@/lib/api/server' @@ -6,6 +7,7 @@ import { getSession } from '@/lib/auth' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { moveWorkspaceFileItems, + WorkspaceFileFolderConflictError, WorkspaceFileMoveConflictError, } from '@/lib/uploads/contexts/workspace' import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' @@ -42,9 +44,24 @@ export const POST = withRouteHandler( }) } catch (error) { logger.error('Failed to move workspace file items:', error) + if ( + error instanceof WorkspaceFileMoveConflictError || + error instanceof WorkspaceFileFolderConflictError + ) { + return NextResponse.json({ success: false, error: error.message }, { status: 409 }) + } + if (getPostgresErrorCode(error) === '23505') { + return NextResponse.json( + { + success: false, + error: 'A file or folder with this name already exists in the destination folder', + }, + { status: 409 } + ) + } return NextResponse.json( { success: false, error: error instanceof Error ? error.message : 'Failed to move items' }, - { status: error instanceof WorkspaceFileMoveConflictError ? 409 : 400 } + { status: 400 } ) } } diff --git a/apps/sim/app/workspace/[workspaceId]/files/files.tsx b/apps/sim/app/workspace/[workspaceId]/files/files.tsx index bc894bea5bc..cd78239f08f 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/files.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/files.tsx @@ -568,6 +568,73 @@ export function Files() { [descendantFolderIdsByFolderId] ) + const uploadFiles = useCallback( + async (filesToUpload: File[], targetFolderId = currentFolderId) => { + if (!workspaceId || filesToUpload.length === 0 || !canEdit) return + + const oversized: string[] = [] + const sizeFiltered = filesToUpload.filter((f) => { + if (f.size > MAX_WORKSPACE_FILE_SIZE) { + oversized.push(f.name) + return false + } + return true + }) + if (oversized.length > 0) { + toast.error( + oversized.length === 1 + ? `${oversized[0]} exceeds the 5 GiB upload limit` + : `${oversized.length} files exceed the 5 GiB upload limit` + ) + } + + const unsupported: string[] = [] + const allowedFiles = sizeFiltered.filter((f) => { + const ext = getFileExtension(f.name) + const ok = SUPPORTED_EXTENSIONS.includes(ext as (typeof SUPPORTED_EXTENSIONS)[number]) + if (!ok) unsupported.push(f.name) + return ok + }) + + if (unsupported.length > 0) { + logger.warn('Unsupported file types skipped:', unsupported) + } + + if (allowedFiles.length === 0) return + + try { + setUploading(true) + setUploadProgress({ completed: 0, total: allowedFiles.length, currentPercent: 0 }) + + for (let i = 0; i < allowedFiles.length; i++) { + try { + await uploadFile.mutateAsync({ + workspaceId, + file: allowedFiles[i], + folderId: targetFolderId, + onProgress: ({ percent }) => { + setUploadProgress((prev) => ({ ...prev, currentPercent: percent })) + }, + }) + setUploadProgress({ + completed: i + 1, + total: allowedFiles.length, + currentPercent: 0, + }) + } catch (err) { + logger.error('Error uploading file:', err) + } + } + } catch (err) { + logger.error('Error uploading file:', err) + } finally { + setUploading(false) + setUploadProgress({ completed: 0, total: 0, currentPercent: 0 }) + } + }, + [workspaceId, canEdit, currentFolderId, uploadFile.mutateAsync] + ) + const rowDragDropConfig = useMemo( () => ({ activeDropTargetId, @@ -688,70 +755,6 @@ export function Files() { ] ) - async function uploadFiles(filesToUpload: File[], targetFolderId = currentFolderId) { - if (!workspaceId || filesToUpload.length === 0 || !canEdit) return - - const oversized: string[] = [] - const sizeFiltered = filesToUpload.filter((f) => { - if (f.size > MAX_WORKSPACE_FILE_SIZE) { - oversized.push(f.name) - return false - } - return true - }) - if (oversized.length > 0) { - toast.error( - oversized.length === 1 - ? `${oversized[0]} exceeds the 5 GiB upload limit` - : `${oversized.length} files exceed the 5 GiB upload limit` - ) - } - - const unsupported: string[] = [] - const allowedFiles = sizeFiltered.filter((f) => { - const ext = getFileExtension(f.name) - const ok = SUPPORTED_EXTENSIONS.includes(ext as (typeof SUPPORTED_EXTENSIONS)[number]) - if (!ok) unsupported.push(f.name) - return ok - }) - - if (unsupported.length > 0) { - logger.warn('Unsupported file types skipped:', unsupported) - } - - if (allowedFiles.length === 0) return - - try { - setUploading(true) - setUploadProgress({ completed: 0, total: allowedFiles.length, currentPercent: 0 }) - - for (let i = 0; i < allowedFiles.length; i++) { - try { - await uploadFile.mutateAsync({ - workspaceId, - file: allowedFiles[i], - folderId: targetFolderId, - onProgress: ({ percent }) => { - setUploadProgress((prev) => ({ ...prev, currentPercent: percent })) - }, - }) - setUploadProgress({ - completed: i + 1, - total: allowedFiles.length, - currentPercent: 0, - }) - } catch (err) { - logger.error('Error uploading file:', err) - } - } - } catch (err) { - logger.error('Error uploading file:', err) - } finally { - setUploading(false) - setUploadProgress({ completed: 0, total: 0, currentPercent: 0 }) - } - } - const handleFileChange = async (e: React.ChangeEvent) => { const list = e.target.files if (!list || list.length === 0) return @@ -819,12 +822,16 @@ export function Files() { if (target.fileIds.includes(fileIdFromRouteRef.current ?? '')) { setIsDirty(false) setSaveStatus('idle') - router.push(`/workspace/${workspaceId}/files`) + router.push( + currentFolderId + ? `/workspace/${workspaceId}/files?folderId=${currentFolderId}` + : `/workspace/${workspaceId}/files` + ) } } catch (err) { logger.error('Failed to delete file:', err) } - }, [workspaceId, router, bulkArchiveItems, deleteFile]) + }, [workspaceId, router, currentFolderId, bulkArchiveItems, deleteFile]) const isDirtyRef = useRef(isDirty) isDirtyRef.current = isDirty diff --git a/apps/sim/lib/uploads/contexts/workspace/workspace-file-folder-manager.ts b/apps/sim/lib/uploads/contexts/workspace/workspace-file-folder-manager.ts index 5ba8f757b1e..7258da6c1b8 100644 --- a/apps/sim/lib/uploads/contexts/workspace/workspace-file-folder-manager.ts +++ b/apps/sim/lib/uploads/contexts/workspace/workspace-file-folder-manager.ts @@ -1,6 +1,7 @@ import { db } from '@sim/db' import { workspaceFileFolder, workspaceFiles } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { getPostgresErrorCode } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' import { and, asc, eq, inArray, isNull, min, sql } from 'drizzle-orm' @@ -127,6 +128,82 @@ function mapFolder( } } +async function getRawWorkspaceFileFolder( + workspaceId: string, + folderId: string, + options?: { includeDeleted?: boolean } +): Promise { + const { includeDeleted = false } = options ?? {} + const [folder] = await db + .select() + .from(workspaceFileFolder) + .where( + includeDeleted + ? and( + eq(workspaceFileFolder.id, folderId), + eq(workspaceFileFolder.workspaceId, workspaceId) + ) + : and( + eq(workspaceFileFolder.id, folderId), + eq(workspaceFileFolder.workspaceId, workspaceId), + isNull(workspaceFileFolder.deletedAt) + ) + ) + .limit(1) + + return folder ?? null +} + +async function findRawWorkspaceFileFolderByName( + workspaceId: string, + name: string, + parentId?: string | null +): Promise { + const [folder] = await db + .select() + .from(workspaceFileFolder) + .where( + and( + eq(workspaceFileFolder.workspaceId, workspaceId), + eq(workspaceFileFolder.name, name), + folderParentCondition(parentId), + isNull(workspaceFileFolder.deletedAt) + ) + ) + .limit(1) + + return folder ?? null +} + +async function buildWorkspaceFileFolderPath( + workspaceId: string, + folder: Pick, + options?: { includeDeleted?: boolean } +): Promise { + const segments: string[] = [] + const seen = new Set() + let current: Pick | null = folder + + while (current && !seen.has(current.id)) { + segments.unshift(current.name) + seen.add(current.id) + current = current.parentId + ? await getRawWorkspaceFileFolder(workspaceId, current.parentId, options) + : null + } + + return segments.join('/') +} + +async function mapFolderWithPath( + workspaceId: string, + folder: RawWorkspaceFileFolder, + options?: { includeDeleted?: boolean } +): Promise { + const path = await buildWorkspaceFileFolderPath(workspaceId, folder, options) + return mapFolder(folder, new Map([[folder.id, path]])) +} + export async function listWorkspaceFileFolders( workspaceId: string, options?: { scope?: WorkspaceFileFolderScope } @@ -160,29 +237,8 @@ export async function getWorkspaceFileFolder( options?: { includeDeleted?: boolean } ): Promise { const { includeDeleted = false } = options ?? {} - const rows = await db - .select() - .from(workspaceFileFolder) - .where( - includeDeleted - ? and( - eq(workspaceFileFolder.id, folderId), - eq(workspaceFileFolder.workspaceId, workspaceId) - ) - : and( - eq(workspaceFileFolder.id, folderId), - eq(workspaceFileFolder.workspaceId, workspaceId), - isNull(workspaceFileFolder.deletedAt) - ) - ) - .limit(1) - - if (rows.length === 0) return null - - const folders = await listWorkspaceFileFolders(workspaceId, { - scope: includeDeleted ? 'all' : 'active', - }) - return folders.find((folder) => folder.id === folderId) ?? null + const folder = await getRawWorkspaceFileFolder(workspaceId, folderId, { includeDeleted }) + return folder ? mapFolderWithPath(workspaceId, folder, { includeDeleted }) : null } export async function assertWorkspaceFileFolderTarget( @@ -252,20 +308,28 @@ export async function createWorkspaceFileFolder(params: { } const id = generateId() - const [folder] = await db - .insert(workspaceFileFolder) - .values({ - id, - name, - userId: params.userId, - workspaceId: params.workspaceId, - parentId, - sortOrder: params.sortOrder ?? (await nextFolderSortOrder(params.workspaceId, parentId)), - }) - .returning() + let folder: RawWorkspaceFileFolder + try { + const [inserted] = await db + .insert(workspaceFileFolder) + .values({ + id, + name, + userId: params.userId, + workspaceId: params.workspaceId, + parentId, + sortOrder: params.sortOrder ?? (await nextFolderSortOrder(params.workspaceId, parentId)), + }) + .returning() + folder = inserted + } catch (error) { + if (getPostgresErrorCode(error) === '23505') { + throw new WorkspaceFileFolderConflictError(name) + } + throw error + } - const folders = await listWorkspaceFileFolders(params.workspaceId) - return folders.find((item) => item.id === folder.id) ?? mapFolder(folder, new Map()) + return mapFolderWithPath(params.workspaceId, folder) } export async function ensureWorkspaceFileFolderPath(params: { @@ -277,20 +341,38 @@ export async function ensureWorkspaceFileFolderPath(params: { for (const rawSegment of params.pathSegments) { const name = normalizeWorkspaceFileItemName(rawSegment, 'Folder') - const folders = await listWorkspaceFileFolders(params.workspaceId) - const existing = folders.find( - (folder) => folder.name === name && (folder.parentId ?? null) === parentId - ) - parentId = existing - ? existing.id - : ( - await createWorkspaceFileFolder({ - workspaceId: params.workspaceId, - userId: params.userId, - name, - parentId, - }) - ).id + const existing = await findRawWorkspaceFileFolderByName(params.workspaceId, name, parentId) + if (existing) { + parentId = existing.id + continue + } + + try { + parentId = ( + await createWorkspaceFileFolder({ + workspaceId: params.workspaceId, + userId: params.userId, + name, + parentId, + }) + ).id + } catch (error) { + if ( + error instanceof WorkspaceFileFolderConflictError || + getPostgresErrorCode(error) === '23505' + ) { + const concurrentExisting = await findRawWorkspaceFileFolderByName( + params.workspaceId, + name, + parentId + ) + if (concurrentExisting) { + parentId = concurrentExisting.id + continue + } + } + throw error + } } return parentId @@ -386,9 +468,7 @@ export async function updateWorkspaceFileFolder(params: { .returning() if (!folder) throw new Error('Folder not found') - return ( - (await getWorkspaceFileFolder(params.workspaceId, folder.id)) ?? mapFolder(folder, new Map()) - ) + return mapFolderWithPath(params.workspaceId, folder) } export async function fileNameExistsInWorkspaceFolder( diff --git a/packages/db/schema.ts b/packages/db/schema.ts index 46717c43188..deedc0c27e2 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -1,5 +1,6 @@ import { type SQL, sql } from 'drizzle-orm' import { + type AnyPgColumn, bigint, boolean, check, @@ -1178,7 +1179,9 @@ export const workspaceFileFolder = pgTable( workspaceId: text('workspace_id') .notNull() .references(() => workspace.id, { onDelete: 'cascade' }), - parentId: text('parent_id'), + parentId: text('parent_id').references((): AnyPgColumn => workspaceFileFolder.id, { + onDelete: 'set null', + }), sortOrder: integer('sort_order').notNull().default(0), deletedAt: timestamp('deleted_at'), createdAt: timestamp('created_at').notNull().defaultNow(), From 3601e93359ca048c9bced9268c943e91bd6e8591 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 12 May 2026 15:38:04 -0700 Subject: [PATCH 05/60] cleanup unnused code --- .../workspace/[workspaceId]/files/files.tsx | 31 ++-- .../hooks/queries/workspace-file-folders.ts | 15 -- apps/sim/lib/uploads/client/download.ts | 11 -- .../workspace-file-folder-manager.ts | 143 +++++++++++++----- 4 files changed, 129 insertions(+), 71 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/files/files.tsx b/apps/sim/app/workspace/[workspaceId]/files/files.tsx index cd78239f08f..e77dd695aa8 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/files.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/files.tsx @@ -254,7 +254,6 @@ export function Files() { fileIds: string[] folderIds: string[] name: string - isFolder?: boolean } | null>(null) const listRename = useInlineRename({ @@ -884,7 +883,6 @@ export function Files() { folders.find((folder) => folder.id === selectedFolderIds[0])?.name ?? 'selected item') : `${selectedFileIds.length + selectedFolderIds.length} selected items`, - isFolder: selectedFolderIds.length > 0, }) setShowDeleteConfirm(true) }, [selectedFileIds, selectedFolderIds, files, folders]) @@ -1123,7 +1121,7 @@ export function Files() { setDeleteTarget( item.kind === 'file' ? { fileIds: [item.file.id], folderIds: [], name: item.file.name } - : { fileIds: [], folderIds: [item.folder.id], name: item.folder.name, isFolder: true } + : { fileIds: [], folderIds: [item.folder.id], name: item.folder.name } ) setShowDeleteConfirm(true) closeContextMenu() @@ -1648,7 +1646,8 @@ export function Files() { open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm} fileName={deleteTarget?.name} - isFolder={deleteTarget?.isFolder} + fileCount={deleteTarget?.fileIds.length ?? 0} + folderCount={deleteTarget?.folderIds.length ?? 0} onDelete={handleDelete} isPending={deleteFile.isPending || bulkArchiveItems.isPending} /> @@ -1733,7 +1732,8 @@ export function Files() { open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm} fileName={deleteTarget?.name} - isFolder={deleteTarget?.isFolder} + fileCount={deleteTarget?.fileIds.length ?? 0} + folderCount={deleteTarget?.folderIds.length ?? 0} onDelete={handleDelete} isPending={deleteFile.isPending || bulkArchiveItems.isPending} /> @@ -1855,7 +1855,8 @@ interface DeleteConfirmModalProps { open: boolean onOpenChange: (open: boolean) => void fileName?: string - isFolder?: boolean + fileCount: number + folderCount: number onDelete: () => void isPending: boolean } @@ -1864,21 +1865,29 @@ const DeleteConfirmModal = memo(function DeleteConfirmModal({ open, onOpenChange, fileName, - isFolder = false, + fileCount, + folderCount, onDelete, isPending, }: DeleteConfirmModalProps) { + const totalCount = fileCount + folderCount + const hasFolders = folderCount > 0 + const title = totalCount > 1 ? 'Delete Items' : hasFolders ? 'Delete Folder' : 'Delete File' + const consequence = hasFolders + ? totalCount > 1 + ? 'This will also delete files and folders inside any selected folders.' + : 'This will also delete files and folders inside it.' + : 'You can restore it from Recently Deleted in Settings.' + return ( - {isFolder ? 'Delete Folder' : 'Delete File'} + {title}

Are you sure you want to delete{' '} {fileName}?{' '} - {isFolder - ? 'This will also delete files and folders inside it.' - : 'You can restore it from Recently Deleted in Settings.'} + {consequence}

diff --git a/apps/sim/hooks/queries/workspace-file-folders.ts b/apps/sim/hooks/queries/workspace-file-folders.ts index a26ce18cc44..cbb5fe380e0 100644 --- a/apps/sim/hooks/queries/workspace-file-folders.ts +++ b/apps/sim/hooks/queries/workspace-file-folders.ts @@ -3,7 +3,6 @@ import { requestJson } from '@/lib/api/client/request' import { bulkArchiveWorkspaceFileItemsContract, createWorkspaceFileFolderContract, - deleteWorkspaceFileFolderContract, listWorkspaceFileFoldersContract, moveWorkspaceFileItemsContract, updateWorkspaceFileFolderContract, @@ -96,20 +95,6 @@ export function useUpdateWorkspaceFileFolder() { }) } -export function useDeleteWorkspaceFileFolder() { - const queryClient = useQueryClient() - return useMutation({ - mutationFn: async (variables: { workspaceId: string; folderId: string }) => { - return requestJson(deleteWorkspaceFileFolderContract, { - params: { id: variables.workspaceId, folderId: variables.folderId }, - }) - }, - onSettled: (_data, _error, variables) => { - invalidateWorkspaceFileBrowsers(queryClient, variables.workspaceId) - }, - }) -} - export function useMoveWorkspaceFileItems() { const queryClient = useQueryClient() return useMutation({ diff --git a/apps/sim/lib/uploads/client/download.ts b/apps/sim/lib/uploads/client/download.ts index 1b7925b1a84..9cbdd88d263 100644 --- a/apps/sim/lib/uploads/client/download.ts +++ b/apps/sim/lib/uploads/client/download.ts @@ -24,14 +24,3 @@ export async function triggerFileDownload(record: WorkspaceFileRecord): Promise< document.body.removeChild(a) URL.revokeObjectURL(objectUrl) } - -export function triggerBlobDownload(blob: Blob, fileName: string): void { - const objectUrl = URL.createObjectURL(blob) - const a = document.createElement('a') - a.href = objectUrl - a.download = fileName - document.body.appendChild(a) - a.click() - document.body.removeChild(a) - URL.revokeObjectURL(objectUrl) -} diff --git a/apps/sim/lib/uploads/contexts/workspace/workspace-file-folder-manager.ts b/apps/sim/lib/uploads/contexts/workspace/workspace-file-folder-manager.ts index 7258da6c1b8..7c1806b7195 100644 --- a/apps/sim/lib/uploads/contexts/workspace/workspace-file-folder-manager.ts +++ b/apps/sim/lib/uploads/contexts/workspace/workspace-file-folder-manager.ts @@ -3,7 +3,7 @@ import { workspaceFileFolder, workspaceFiles } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { getPostgresErrorCode } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' -import { and, asc, eq, inArray, isNull, min, sql } from 'drizzle-orm' +import { and, asc, eq, inArray, isNull, min, type SQL, sql } from 'drizzle-orm' const logger = createLogger('WorkspaceFileFolders') @@ -50,6 +50,10 @@ interface RawWorkspaceFileFolder { updatedAt: Date } +interface WorkspaceFileFolderLockTx { + execute(query: SQL): Promise +} + export interface WorkspaceFileArchiveResult { folders: number files: number @@ -82,6 +86,15 @@ function fileFolderCondition(folderId?: string | null) { return normalized ? eq(workspaceFiles.folderId, normalized) : isNull(workspaceFiles.folderId) } +async function acquireWorkspaceFileFolderMutationLock( + tx: WorkspaceFileFolderLockTx, + workspaceId: string +) { + await tx.execute( + sql`SELECT pg_advisory_xact_lock(hashtextextended(${`workspace_file_folders:${workspaceId}`}, 0))` + ) +} + export function buildWorkspaceFileFolderPathMap( folders: Array> ): Map { @@ -300,34 +313,38 @@ export async function createWorkspaceFileFolder(params: { parentId?: string | null sortOrder?: number }): Promise { - const parentId = await assertWorkspaceFileFolderTarget(params.workspaceId, params.parentId) const name = normalizeWorkspaceFileItemName(params.name, 'Folder') - if (await workspaceFileFolderExists(params.workspaceId, name, parentId)) { - throw new WorkspaceFileFolderConflictError(name) - } + const folder = await db.transaction(async (tx) => { + await acquireWorkspaceFileFolderMutationLock(tx, params.workspaceId) - const id = generateId() - let folder: RawWorkspaceFileFolder - try { - const [inserted] = await db - .insert(workspaceFileFolder) - .values({ - id, - name, - userId: params.userId, - workspaceId: params.workspaceId, - parentId, - sortOrder: params.sortOrder ?? (await nextFolderSortOrder(params.workspaceId, parentId)), - }) - .returning() - folder = inserted - } catch (error) { - if (getPostgresErrorCode(error) === '23505') { + const parentId = await assertWorkspaceFileFolderTarget(params.workspaceId, params.parentId) + + if (await workspaceFileFolderExists(params.workspaceId, name, parentId)) { throw new WorkspaceFileFolderConflictError(name) } - throw error - } + + const id = generateId() + try { + const [inserted] = await tx + .insert(workspaceFileFolder) + .values({ + id, + name, + userId: params.userId, + workspaceId: params.workspaceId, + parentId, + sortOrder: params.sortOrder ?? (await nextFolderSortOrder(params.workspaceId, parentId)), + }) + .returning() + return inserted + } catch (error) { + if (getPostgresErrorCode(error) === '23505') { + throw new WorkspaceFileFolderConflictError(name) + } + throw error + } + }) return mapFolderWithPath(params.workspaceId, folder) } @@ -407,6 +424,31 @@ async function getDescendantFolderIds( return descendants } +function collectDescendantFolderIds( + folders: Array>, + folderId: string +): string[] { + const childrenByParent = new Map() + + for (const folder of folders) { + if (!folder.parentId) continue + const children = childrenByParent.get(folder.parentId) ?? [] + children.push(folder.id) + childrenByParent.set(folder.parentId, children) + } + + const descendants: string[] = [] + const visit = (id: string) => { + for (const childId of childrenByParent.get(id) ?? []) { + descendants.push(childId) + visit(childId) + } + } + visit(folderId) + + return descendants +} + export async function updateWorkspaceFileFolder(params: { workspaceId: string folderId: string @@ -611,13 +653,33 @@ export async function archiveWorkspaceFileFolderRecursive( workspaceId: string, folderId: string ): Promise { - const folder = await getWorkspaceFileFolder(workspaceId, folderId) - if (!folder) throw new Error('Folder not found') - const now = new Date() - const folderIds = [folderId, ...(await getDescendantFolderIds(workspaceId, folderId))] return db.transaction(async (tx) => { + await acquireWorkspaceFileFolderMutationLock(tx, workspaceId) + + const [folder] = await tx + .select({ id: workspaceFileFolder.id }) + .from(workspaceFileFolder) + .where( + and( + eq(workspaceFileFolder.id, folderId), + eq(workspaceFileFolder.workspaceId, workspaceId), + isNull(workspaceFileFolder.deletedAt) + ) + ) + .limit(1) + + if (!folder) throw new Error('Folder not found') + + const activeFolders = await tx + .select({ id: workspaceFileFolder.id, parentId: workspaceFileFolder.parentId }) + .from(workspaceFileFolder) + .where( + and(eq(workspaceFileFolder.workspaceId, workspaceId), isNull(workspaceFileFolder.deletedAt)) + ) + const folderIds = [folderId, ...collectDescendantFolderIds(activeFolders, folderId)] + const archivedFiles = await tx .update(workspaceFiles) .set({ deletedAt: now, updatedAt: now }) @@ -662,14 +724,27 @@ export async function bulkArchiveWorkspaceFileItems(params: { const now = new Date() const explicitFileIds = Array.from(new Set(params.fileIds ?? [])) const explicitFolderIds = Array.from(new Set(params.folderIds ?? [])) - const descendantFolderIds = ( - await Promise.all( - explicitFolderIds.map((folderId) => getDescendantFolderIds(params.workspaceId, folderId)) - ) - ).flat() - const allFolderIds = Array.from(new Set([...explicitFolderIds, ...descendantFolderIds])) return db.transaction(async (tx) => { + await acquireWorkspaceFileFolderMutationLock(tx, params.workspaceId) + + const activeFolders = + explicitFolderIds.length > 0 + ? await tx + .select({ id: workspaceFileFolder.id, parentId: workspaceFileFolder.parentId }) + .from(workspaceFileFolder) + .where( + and( + eq(workspaceFileFolder.workspaceId, params.workspaceId), + isNull(workspaceFileFolder.deletedAt) + ) + ) + : [] + const descendantFolderIds = explicitFolderIds.flatMap((folderId) => + collectDescendantFolderIds(activeFolders, folderId) + ) + const allFolderIds = Array.from(new Set([...explicitFolderIds, ...descendantFolderIds])) + const archivedExplicitFiles = explicitFileIds.length > 0 ? await tx From 237461c87be284c2ffd55c956a7420b4dd4582cd Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 12 May 2026 16:51:49 -0700 Subject: [PATCH 06/60] address comments --- .../[id]/files/folders/[folderId]/route.ts | 21 +- .../workspace-file-folder-manager.ts | 435 +++++++++++------- .../workspace/workspace-file-manager.ts | 2 +- 3 files changed, 282 insertions(+), 176 deletions(-) diff --git a/apps/sim/app/api/workspaces/[id]/files/folders/[folderId]/route.ts b/apps/sim/app/api/workspaces/[id]/files/folders/[folderId]/route.ts index 16856df790e..065159ba43f 100644 --- a/apps/sim/app/api/workspaces/[id]/files/folders/[folderId]/route.ts +++ b/apps/sim/app/api/workspaces/[id]/files/folders/[folderId]/route.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { getPostgresErrorCode } from '@sim/utils/errors' import { type NextRequest, NextResponse } from 'next/server' import { deleteWorkspaceFileFolderContract, @@ -45,10 +46,24 @@ export const PATCH = withRouteHandler( return NextResponse.json({ success: true, folder }) } catch (error) { logger.error('Failed to update workspace file folder:', error) - const message = error instanceof Error ? error.message : 'Failed to update folder' + if (error instanceof WorkspaceFileFolderConflictError) { + return NextResponse.json({ success: false, error: error.message }, { status: 409 }) + } + if (getPostgresErrorCode(error) === '23505') { + return NextResponse.json( + { + success: false, + error: 'A folder with this name already exists in this location', + }, + { status: 409 } + ) + } return NextResponse.json( - { success: false, error: message }, - { status: error instanceof WorkspaceFileFolderConflictError ? 409 : 400 } + { + success: false, + error: error instanceof Error ? error.message : 'Failed to update folder', + }, + { status: 400 } ) } } diff --git a/apps/sim/lib/uploads/contexts/workspace/workspace-file-folder-manager.ts b/apps/sim/lib/uploads/contexts/workspace/workspace-file-folder-manager.ts index 7c1806b7195..2a0617fe43f 100644 --- a/apps/sim/lib/uploads/contexts/workspace/workspace-file-folder-manager.ts +++ b/apps/sim/lib/uploads/contexts/workspace/workspace-file-folder-manager.ts @@ -269,43 +269,6 @@ export async function assertWorkspaceFileFolderTarget( return normalized } -async function workspaceFileFolderExists( - workspaceId: string, - name: string, - parentId?: string | null, - excludeFolderId?: string -): Promise { - const rows = await db - .select({ id: workspaceFileFolder.id }) - .from(workspaceFileFolder) - .where( - and( - eq(workspaceFileFolder.workspaceId, workspaceId), - eq(workspaceFileFolder.name, name), - folderParentCondition(parentId), - isNull(workspaceFileFolder.deletedAt) - ) - ) - .limit(2) - - return rows.some((row) => row.id !== excludeFolderId) -} - -async function nextFolderSortOrder(workspaceId: string, parentId?: string | null): Promise { - const [result] = await db - .select({ minSortOrder: min(workspaceFileFolder.sortOrder) }) - .from(workspaceFileFolder) - .where( - and( - eq(workspaceFileFolder.workspaceId, workspaceId), - folderParentCondition(parentId), - isNull(workspaceFileFolder.deletedAt) - ) - ) - - return result?.minSortOrder != null ? result.minSortOrder - 1 : 0 -} - export async function createWorkspaceFileFolder(params: { workspaceId: string userId: string @@ -318,12 +281,53 @@ export async function createWorkspaceFileFolder(params: { const folder = await db.transaction(async (tx) => { await acquireWorkspaceFileFolderMutationLock(tx, params.workspaceId) - const parentId = await assertWorkspaceFileFolderTarget(params.workspaceId, params.parentId) + const parentId = normalizeParentId(params.parentId) + if (parentId) { + const [target] = await tx + .select({ id: workspaceFileFolder.id }) + .from(workspaceFileFolder) + .where( + and( + eq(workspaceFileFolder.id, parentId), + eq(workspaceFileFolder.workspaceId, params.workspaceId), + isNull(workspaceFileFolder.deletedAt) + ) + ) + .limit(1) + + if (!target) { + throw new Error('Target folder not found') + } + } - if (await workspaceFileFolderExists(params.workspaceId, name, parentId)) { + const existingFolders = await tx + .select({ id: workspaceFileFolder.id }) + .from(workspaceFileFolder) + .where( + and( + eq(workspaceFileFolder.workspaceId, params.workspaceId), + eq(workspaceFileFolder.name, name), + folderParentCondition(parentId), + isNull(workspaceFileFolder.deletedAt) + ) + ) + .limit(1) + + if (existingFolders.length > 0) { throw new WorkspaceFileFolderConflictError(name) } + const [sortOrderResult] = await tx + .select({ minSortOrder: min(workspaceFileFolder.sortOrder) }) + .from(workspaceFileFolder) + .where( + and( + eq(workspaceFileFolder.workspaceId, params.workspaceId), + folderParentCondition(parentId), + isNull(workspaceFileFolder.deletedAt) + ) + ) + const id = generateId() try { const [inserted] = await tx @@ -334,7 +338,9 @@ export async function createWorkspaceFileFolder(params: { userId: params.userId, workspaceId: params.workspaceId, parentId, - sortOrder: params.sortOrder ?? (await nextFolderSortOrder(params.workspaceId, parentId)), + sortOrder: + params.sortOrder ?? + (sortOrderResult?.minSortOrder != null ? sortOrderResult.minSortOrder - 1 : 0), }) .returning() return inserted @@ -395,35 +401,6 @@ export async function ensureWorkspaceFileFolderPath(params: { return parentId } -async function getDescendantFolderIds( - workspaceId: string, - folderId: string, - options?: { includeDeleted?: boolean } -): Promise { - const folders = await listWorkspaceFileFolders(workspaceId, { - scope: options?.includeDeleted ? 'all' : 'active', - }) - const childrenByParent = new Map() - - for (const folder of folders) { - if (!folder.parentId) continue - const children = childrenByParent.get(folder.parentId) ?? [] - children.push(folder.id) - childrenByParent.set(folder.parentId, children) - } - - const descendants: string[] = [] - const visit = (id: string) => { - for (const childId of childrenByParent.get(id) ?? []) { - descendants.push(childId) - visit(childId) - } - } - visit(folderId) - - return descendants -} - function collectDescendantFolderIds( folders: Array>, folderId: string @@ -456,60 +433,122 @@ export async function updateWorkspaceFileFolder(params: { parentId?: string | null sortOrder?: number }): Promise { - const existing = await getWorkspaceFileFolder(params.workspaceId, params.folderId) - if (!existing) throw new Error('Folder not found') - - const updates: Partial = { updatedAt: new Date() } - const finalName = - params.name !== undefined - ? normalizeWorkspaceFileItemName(params.name, 'Folder') - : existing.name - const finalParentId = - params.parentId !== undefined - ? await assertWorkspaceFileFolderTarget(params.workspaceId, params.parentId) - : existing.parentId - - if (finalParentId === params.folderId) throw new Error('Folder cannot be its own parent') - - if (params.parentId !== undefined) { - const descendants = await getDescendantFolderIds(params.workspaceId, params.folderId) - if (finalParentId && descendants.includes(finalParentId)) { - throw new Error('Cannot move a folder into one of its descendants') + const folder = await db.transaction(async (tx) => { + await acquireWorkspaceFileFolderMutationLock(tx, params.workspaceId) + + const [existing] = await tx + .select() + .from(workspaceFileFolder) + .where( + and( + eq(workspaceFileFolder.id, params.folderId), + eq(workspaceFileFolder.workspaceId, params.workspaceId), + isNull(workspaceFileFolder.deletedAt) + ) + ) + .limit(1) + + if (!existing) throw new Error('Folder not found') + + const updates: Partial = { updatedAt: new Date() } + const finalName = + params.name !== undefined + ? normalizeWorkspaceFileItemName(params.name, 'Folder') + : existing.name + const finalParentId = + params.parentId !== undefined ? normalizeParentId(params.parentId) : existing.parentId + + if (finalParentId === params.folderId) throw new Error('Folder cannot be its own parent') + + if (finalParentId) { + const [target] = await tx + .select({ id: workspaceFileFolder.id }) + .from(workspaceFileFolder) + .where( + and( + eq(workspaceFileFolder.id, finalParentId), + eq(workspaceFileFolder.workspaceId, params.workspaceId), + isNull(workspaceFileFolder.deletedAt) + ) + ) + .limit(1) + + if (!target) { + throw new Error('Target folder not found') + } } - } - if ( - (finalName !== existing.name || finalParentId !== existing.parentId) && - (await workspaceFileFolderExists(params.workspaceId, finalName, finalParentId, params.folderId)) - ) { - throw new WorkspaceFileFolderConflictError(finalName) - } + if (params.parentId !== undefined) { + const activeFolders = await tx + .select({ id: workspaceFileFolder.id, parentId: workspaceFileFolder.parentId }) + .from(workspaceFileFolder) + .where( + and( + eq(workspaceFileFolder.workspaceId, params.workspaceId), + isNull(workspaceFileFolder.deletedAt) + ) + ) - if (params.name !== undefined) { - updates.name = finalName - } + const descendants = collectDescendantFolderIds(activeFolders, params.folderId) + if (finalParentId && descendants.includes(finalParentId)) { + throw new Error('Cannot move a folder into one of its descendants') + } + } - if (params.parentId !== undefined) { - updates.parentId = finalParentId - } + if (finalName !== existing.name || finalParentId !== existing.parentId) { + const conflictingFolders = await tx + .select({ id: workspaceFileFolder.id }) + .from(workspaceFileFolder) + .where( + and( + eq(workspaceFileFolder.workspaceId, params.workspaceId), + eq(workspaceFileFolder.name, finalName), + folderParentCondition(finalParentId), + isNull(workspaceFileFolder.deletedAt) + ) + ) + .limit(2) - if (params.sortOrder !== undefined) { - updates.sortOrder = params.sortOrder - } + if (conflictingFolders.some((row) => row.id !== params.folderId)) { + throw new WorkspaceFileFolderConflictError(finalName) + } + } - const [folder] = await db - .update(workspaceFileFolder) - .set(updates) - .where( - and( - eq(workspaceFileFolder.id, params.folderId), - eq(workspaceFileFolder.workspaceId, params.workspaceId), - isNull(workspaceFileFolder.deletedAt) - ) - ) - .returning() + if (params.name !== undefined) { + updates.name = finalName + } + + if (params.parentId !== undefined) { + updates.parentId = finalParentId + } + + if (params.sortOrder !== undefined) { + updates.sortOrder = params.sortOrder + } + + try { + const [updatedFolder] = await tx + .update(workspaceFileFolder) + .set(updates) + .where( + and( + eq(workspaceFileFolder.id, params.folderId), + eq(workspaceFileFolder.workspaceId, params.workspaceId), + isNull(workspaceFileFolder.deletedAt) + ) + ) + .returning() + + if (!updatedFolder) throw new Error('Folder not found') + return updatedFolder + } catch (error) { + if (getPostgresErrorCode(error) === '23505') { + throw new WorkspaceFileFolderConflictError(finalName) + } + throw error + } + }) - if (!folder) throw new Error('Folder not found') return mapFolderWithPath(params.workspaceId, folder) } @@ -544,76 +583,128 @@ export async function moveWorkspaceFileItems(params: { }): Promise<{ movedFiles: number; movedFolders: number }> { const fileIds = Array.from(new Set(params.fileIds ?? [])) const folderIds = Array.from(new Set(params.folderIds ?? [])) - const targetFolderId = await assertWorkspaceFileFolderTarget( - params.workspaceId, - params.targetFolderId - ) + const targetFolderId = normalizeParentId(params.targetFolderId) - if (folderIds.includes(targetFolderId ?? '')) { - throw new Error('Cannot move a folder into itself') - } + return db.transaction(async (tx) => { + await acquireWorkspaceFileFolderMutationLock(tx, params.workspaceId) - for (const folderId of folderIds) { - const descendants = await getDescendantFolderIds(params.workspaceId, folderId) - if (targetFolderId && descendants.includes(targetFolderId)) { - throw new Error('Cannot move a folder into one of its descendants') + if (targetFolderId) { + const [target] = await tx + .select({ id: workspaceFileFolder.id }) + .from(workspaceFileFolder) + .where( + and( + eq(workspaceFileFolder.id, targetFolderId), + eq(workspaceFileFolder.workspaceId, params.workspaceId), + isNull(workspaceFileFolder.deletedAt) + ) + ) + .limit(1) + + if (!target) { + throw new Error('Target folder not found') + } } - } - const movingFiles = - fileIds.length > 0 - ? await db - .select({ id: workspaceFiles.id, name: workspaceFiles.originalName }) - .from(workspaceFiles) - .where( - and( - inArray(workspaceFiles.id, fileIds), - eq(workspaceFiles.workspaceId, params.workspaceId), - eq(workspaceFiles.context, 'workspace'), - isNull(workspaceFiles.deletedAt) - ) + if (folderIds.includes(targetFolderId ?? '')) { + throw new Error('Cannot move a folder into itself') + } + + if (folderIds.length > 0) { + const activeFolders = await tx + .select({ id: workspaceFileFolder.id, parentId: workspaceFileFolder.parentId }) + .from(workspaceFileFolder) + .where( + and( + eq(workspaceFileFolder.workspaceId, params.workspaceId), + isNull(workspaceFileFolder.deletedAt) ) - : [] - - const movingFolders = - folderIds.length > 0 - ? await db - .select({ id: workspaceFileFolder.id, name: workspaceFileFolder.name }) - .from(workspaceFileFolder) - .where( - and( - inArray(workspaceFileFolder.id, folderIds), - eq(workspaceFileFolder.workspaceId, params.workspaceId), - isNull(workspaceFileFolder.deletedAt) + ) + + for (const folderId of folderIds) { + const descendants = collectDescendantFolderIds(activeFolders, folderId) + if (targetFolderId && descendants.includes(targetFolderId)) { + throw new Error('Cannot move a folder into one of its descendants') + } + } + } + + const movingFiles = + fileIds.length > 0 + ? await tx + .select({ id: workspaceFiles.id, name: workspaceFiles.originalName }) + .from(workspaceFiles) + .where( + and( + inArray(workspaceFiles.id, fileIds), + eq(workspaceFiles.workspaceId, params.workspaceId), + eq(workspaceFiles.context, 'workspace'), + isNull(workspaceFiles.deletedAt) + ) ) + : [] + + const movingFolders = + folderIds.length > 0 + ? await tx + .select({ id: workspaceFileFolder.id, name: workspaceFileFolder.name }) + .from(workspaceFileFolder) + .where( + and( + inArray(workspaceFileFolder.id, folderIds), + eq(workspaceFileFolder.workspaceId, params.workspaceId), + isNull(workspaceFileFolder.deletedAt) + ) + ) + : [] + + for (const file of movingFiles) { + const conflictingFiles = await tx + .select({ id: workspaceFiles.id }) + .from(workspaceFiles) + .where( + and( + eq(workspaceFiles.workspaceId, params.workspaceId), + eq(workspaceFiles.originalName, file.name), + eq(workspaceFiles.context, 'workspace'), + fileFolderCondition(targetFolderId), + isNull(workspaceFiles.deletedAt) ) - : [] + ) + .limit(2) - for (const file of movingFiles) { - if ( - await fileNameExistsInWorkspaceFolder(params.workspaceId, file.name, targetFolderId, file.id) - ) { - throw new WorkspaceFileMoveConflictError(file.name) + if (conflictingFiles.some((row) => row.id !== file.id)) { + throw new WorkspaceFileMoveConflictError(file.name) + } } - } - const movingFolderNameCounts = new Map() - for (const folder of movingFolders) { - movingFolderNameCounts.set(folder.name, (movingFolderNameCounts.get(folder.name) ?? 0) + 1) - if ( - await workspaceFileFolderExists(params.workspaceId, folder.name, targetFolderId, folder.id) - ) { - throw new WorkspaceFileFolderConflictError(folder.name) + const movingFolderNameCounts = new Map() + for (const folder of movingFolders) { + movingFolderNameCounts.set(folder.name, (movingFolderNameCounts.get(folder.name) ?? 0) + 1) + const conflictingFolders = await tx + .select({ id: workspaceFileFolder.id }) + .from(workspaceFileFolder) + .where( + and( + eq(workspaceFileFolder.workspaceId, params.workspaceId), + eq(workspaceFileFolder.name, folder.name), + folderParentCondition(targetFolderId), + isNull(workspaceFileFolder.deletedAt) + ) + ) + .limit(2) + + if (conflictingFolders.some((row) => row.id !== folder.id)) { + throw new WorkspaceFileFolderConflictError(folder.name) + } } - } - for (const [name, count] of movingFolderNameCounts) { - if (count > 1) { - throw new WorkspaceFileFolderConflictError(name) + for (const [name, count] of movingFolderNameCounts) { + if (count > 1) { + throw new WorkspaceFileFolderConflictError(name) + } } - } - return db.transaction(async (tx) => { const movedFiles = fileIds.length > 0 ? await tx diff --git a/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts b/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts index 858b4b164df..23623ce1fad 100644 --- a/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts +++ b/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts @@ -535,7 +535,7 @@ export async function fileExistsInWorkspace( folderId?: string | null ): Promise { try { - return fileNameExistsInWorkspaceFolder(workspaceId, fileName, folderId) + return await fileNameExistsInWorkspaceFolder(workspaceId, fileName, folderId) } catch (error) { logger.error(`Failed to check file existence for ${fileName}:`, error) return false From 6031ddd51488d4e35443d2cecdcaf1e51537b06f Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 12 May 2026 18:54:17 -0700 Subject: [PATCH 07/60] perf improvements --- .../workspaces/[id]/files/download/route.ts | 22 +++++++ .../workspace/[workspaceId]/files/files.tsx | 8 +++ .../workspace-file-folder-manager.ts | 31 ++++++++++ .../workspace/workspace-file-manager.ts | 58 +++++++++++++++++-- 4 files changed, 113 insertions(+), 6 deletions(-) diff --git a/apps/sim/app/api/workspaces/[id]/files/download/route.ts b/apps/sim/app/api/workspaces/[id]/files/download/route.ts index 3813d2fe8f1..e572b901949 100644 --- a/apps/sim/app/api/workspaces/[id]/files/download/route.ts +++ b/apps/sim/app/api/workspaces/[id]/files/download/route.ts @@ -10,9 +10,12 @@ import { listWorkspaceFileFolders, listWorkspaceFiles, } from '@/lib/uploads/contexts/workspace' +import { formatFileSize } from '@/lib/uploads/utils/file-utils' import { verifyWorkspaceMembership } from '@/app/api/workflows/utils' const logger = createLogger('WorkspaceFilesDownloadAPI') +const MAX_ZIP_DOWNLOAD_FILES = 100 +const MAX_ZIP_DOWNLOAD_BYTES = 250 * 1024 * 1024 function safeZipPath(path: string): string { return path @@ -76,6 +79,25 @@ export const GET = withRouteHandler( return NextResponse.json({ error: 'No files selected for download' }, { status: 400 }) } + if (filesToZip.length > MAX_ZIP_DOWNLOAD_FILES) { + return NextResponse.json( + { + error: `Too many files selected for download. Select ${MAX_ZIP_DOWNLOAD_FILES} or fewer files.`, + }, + { status: 413 } + ) + } + + const totalBytes = filesToZip.reduce((sum, file) => sum + file.size, 0) + if (totalBytes > MAX_ZIP_DOWNLOAD_BYTES) { + return NextResponse.json( + { + error: `Selected files total ${formatFileSize(totalBytes)}, which exceeds the ${formatFileSize(MAX_ZIP_DOWNLOAD_BYTES)} download limit.`, + }, + { status: 413 } + ) + } + const zip = new JSZip() const usedPaths = new Set() for (const file of filesToZip) { diff --git a/apps/sim/app/workspace/[workspaceId]/files/files.tsx b/apps/sim/app/workspace/[workspaceId]/files/files.tsx index e77dd695aa8..b258a373b8c 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/files.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/files.tsx @@ -414,6 +414,14 @@ export function Files() { owner: ownerCell(file.uploadedBy, members), updated: timeCell(file.updatedAt), }, + sortValues: { + name: file.name, + size: file.size, + type: formatFileType(file.type, file.name), + created: new Date(file.uploadedAt).getTime(), + updated: new Date(file.updatedAt).getTime(), + owner: members?.find((m) => m.userId === file.uploadedBy)?.name ?? '', + }, } return row }) diff --git a/apps/sim/lib/uploads/contexts/workspace/workspace-file-folder-manager.ts b/apps/sim/lib/uploads/contexts/workspace/workspace-file-folder-manager.ts index 2a0617fe43f..d238c9186a2 100644 --- a/apps/sim/lib/uploads/contexts/workspace/workspace-file-folder-manager.ts +++ b/apps/sim/lib/uploads/contexts/workspace/workspace-file-folder-manager.ts @@ -217,6 +217,37 @@ async function mapFolderWithPath( return mapFolder(folder, new Map([[folder.id, path]])) } +export async function getWorkspaceFileFolderPath( + workspaceId: string, + folderId: string, + options?: { includeDeleted?: boolean } +): Promise { + const folder = await getRawWorkspaceFileFolder(workspaceId, folderId, options) + return folder ? buildWorkspaceFileFolderPath(workspaceId, folder, options) : null +} + +export async function findWorkspaceFileFolderIdByPath( + workspaceId: string, + pathSegments: string[] +): Promise { + let parentId: string | null = null + + for (const rawSegment of pathSegments) { + let name: string + try { + name = normalizeWorkspaceFileItemName(rawSegment, 'Folder') + } catch { + return null + } + + const folder = await findRawWorkspaceFileFolderByName(workspaceId, name, parentId) + if (!folder) return null + parentId = folder.id + } + + return parentId +} + export async function listWorkspaceFileFolders( workspaceId: string, options?: { scope?: WorkspaceFileFolderScope } diff --git a/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts b/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts index 23623ce1fad..6fcf2e015d1 100644 --- a/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts +++ b/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts @@ -33,6 +33,8 @@ import { assertWorkspaceFileFolderTarget, buildWorkspaceFileFolderPathMap, fileNameExistsInWorkspaceFolder, + findWorkspaceFileFolderIdByPath, + getWorkspaceFileFolderPath, listWorkspaceFileFolders, normalizeWorkspaceFileItemName, } from './workspace-file-folder-manager' @@ -565,6 +567,24 @@ function mapWorkspaceFileRecord( } } +async function mapSingleWorkspaceFileRecord( + file: typeof workspaceFiles.$inferSelect, + workspaceId: string +): Promise { + if (!file.folderId) { + return mapWorkspaceFileRecord(file, workspaceId, new Map()) + } + + const folderPath = await getWorkspaceFileFolderPath(workspaceId, file.folderId, { + includeDeleted: true, + }) + return mapWorkspaceFileRecord( + file, + workspaceId, + folderPath ? new Map([[file.folderId, folderPath]]) : new Map() + ) +} + /** * Look up a single active workspace file by its original name. * Returns the record if found, or null if no matching file exists. @@ -592,9 +612,7 @@ export async function getWorkspaceFileByName( if (files.length === 0) return null - const folders = await listWorkspaceFileFolders(workspaceId, { scope: 'all' }) - const folderPaths = buildWorkspaceFileFolderPathMap(folders) - return mapWorkspaceFileRecord(files[0], workspaceId, folderPaths) + return mapSingleWorkspaceFileRecord(files[0], workspaceId) } /** @@ -728,6 +746,24 @@ export function findWorkspaceFileRecord( return files.find((file) => normalizeVfsSegment(file.name) === segmentKey) ?? null } +async function getWorkspaceFileByExactReference( + workspaceId: string, + fileReference: string +): Promise { + const segments = fileReference + .split('/') + .map((segment) => segment.trim()) + .filter(Boolean) + + if (segments.length === 0) return null + if (segments.length === 1) { + return getWorkspaceFileByName(workspaceId, segments[0], { folderId: null }) + } + + const folderId = await findWorkspaceFileFolderIdByPath(workspaceId, segments.slice(0, -1)) + return folderId ? getWorkspaceFileByName(workspaceId, segments.at(-1) ?? '', { folderId }) : null +} + /** * Resolve a workspace file record from either its id or a VFS/name reference. */ @@ -735,6 +771,18 @@ export async function resolveWorkspaceFileReference( workspaceId: string, fileReference: string ): Promise { + const normalizedReference = normalizeWorkspaceFileReference(fileReference) + if (normalizedReference.startsWith('wf_')) { + const file = await getWorkspaceFile(workspaceId, normalizedReference) + if (file) return file + } + + const exactReferenceFile = await getWorkspaceFileByExactReference( + workspaceId, + normalizedReference + ) + if (exactReferenceFile) return exactReferenceFile + const files = await listWorkspaceFiles(workspaceId) return findWorkspaceFileRecord(files, fileReference) } @@ -770,9 +818,7 @@ export async function getWorkspaceFile( if (files.length === 0) return null - const folders = await listWorkspaceFileFolders(workspaceId, { scope: 'all' }) - const folderPaths = buildWorkspaceFileFolderPathMap(folders) - return mapWorkspaceFileRecord(files[0], workspaceId, folderPaths) + return mapSingleWorkspaceFileRecord(files[0], workspaceId) } catch (error) { logger.error(`Failed to get workspace file ${fileId}:`, error) return null From 20971a728464ad9f05c4f537c47988633e3069d8 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 12 May 2026 19:10:35 -0700 Subject: [PATCH 08/60] address next set --- apps/sim/app/api/workspaces/[id]/files/download/route.ts | 9 +++++---- apps/sim/app/workspace/[workspaceId]/files/files.tsx | 9 +++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/apps/sim/app/api/workspaces/[id]/files/download/route.ts b/apps/sim/app/api/workspaces/[id]/files/download/route.ts index e572b901949..ee53ff82c1f 100644 --- a/apps/sim/app/api/workspaces/[id]/files/download/route.ts +++ b/apps/sim/app/api/workspaces/[id]/files/download/route.ts @@ -102,10 +102,11 @@ export const GET = withRouteHandler( const usedPaths = new Set() for (const file of filesToZip) { const buffer = await fetchWorkspaceFileBuffer(file) - const basePath = safeZipPath( - file.folderPath ? `${file.folderPath}/${file.name}` : file.name - ) - let zipPath = basePath || file.name + const basePath = + safeZipPath(file.folderPath ? `${file.folderPath}/${file.name}` : file.name) || + safeZipPath(file.name) || + file.id + let zipPath = basePath let suffix = 2 while (usedPaths.has(zipPath)) { const dotIndex = basePath.lastIndexOf('.') diff --git a/apps/sim/app/workspace/[workspaceId]/files/files.tsx b/apps/sim/app/workspace/[workspaceId]/files/files.tsx index b258a373b8c..92aa0d49ad4 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/files.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/files.tsx @@ -435,8 +435,6 @@ export function Files() { if (row.id !== listRename.editingId) return row const parsed = parseRowId(row.id) const file = parsed.kind === 'file' ? filteredFiles.find((f) => f.id === parsed.id) : null - const folder = - parsed.kind === 'folder' ? visibleFolders.find((item) => item.id === parsed.id) : null const Icon = file ? getDocumentIcon(file.type || '', file.name) : Folder return { ...row, @@ -469,7 +467,6 @@ export function Files() { listRename.submitRename, listRename.cancelRename, filteredFiles, - visibleFolders, ]) const visibleRowIds = useMemo(() => rows.map((row) => row.id), [rows]) @@ -817,11 +814,15 @@ export function Files() { fileIds: target.fileIds, folderIds: target.folderIds, }) - } else { + } else if (target.fileIds.length === 1) { await deleteFile.mutateAsync({ workspaceId, fileId: target.fileIds[0], }) + } else { + setShowDeleteConfirm(false) + setDeleteTarget(null) + return } setShowDeleteConfirm(false) setDeleteTarget(null) From 4055ce09402525e0a691a38a1c023b20dadbcf1b Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 12 May 2026 19:21:41 -0700 Subject: [PATCH 09/60] cycle detect --- .../workspaces/[id]/files/download/route.ts | 7 +++++-- .../workspace/[workspaceId]/files/files.tsx | 8 ++++++-- .../workspace/workspace-file-folder-manager.ts | 3 +++ .../workspace/workspace-file-manager.ts | 18 ++++++++++++++---- 4 files changed, 28 insertions(+), 8 deletions(-) diff --git a/apps/sim/app/api/workspaces/[id]/files/download/route.ts b/apps/sim/app/api/workspaces/[id]/files/download/route.ts index ee53ff82c1f..964545be69a 100644 --- a/apps/sim/app/api/workspaces/[id]/files/download/route.ts +++ b/apps/sim/app/api/workspaces/[id]/files/download/route.ts @@ -6,6 +6,7 @@ import { parseRequest } from '@/lib/api/server' import { getSession } from '@/lib/auth' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { + buildWorkspaceFileFolderPathMap, fetchWorkspaceFileBuffer, listWorkspaceFileFolders, listWorkspaceFiles, @@ -65,9 +66,10 @@ export const GET = withRouteHandler( try { const [files, folders] = await Promise.all([ - listWorkspaceFiles(workspaceId), + listWorkspaceFiles(workspaceId, { hydrateFolderPaths: false }), listWorkspaceFileFolders(workspaceId), ]) + const folderPaths = buildWorkspaceFileFolderPathMap(folders) const selectedFolderIds = collectDescendantFolderIds(folderIds, folders) const requestedFileIds = new Set(fileIds) const filesToZip = files.filter( @@ -102,8 +104,9 @@ export const GET = withRouteHandler( const usedPaths = new Set() for (const file of filesToZip) { const buffer = await fetchWorkspaceFileBuffer(file) + const folderPath = file.folderId ? folderPaths.get(file.folderId) : null const basePath = - safeZipPath(file.folderPath ? `${file.folderPath}/${file.name}` : file.name) || + safeZipPath(folderPath ? `${folderPath}/${file.name}` : file.name) || safeZipPath(file.name) || file.id let zipPath = basePath diff --git a/apps/sim/app/workspace/[workspaceId]/files/files.tsx b/apps/sim/app/workspace/[workspaceId]/files/files.tsx index 92aa0d49ad4..8ffbe5222d7 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/files.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/files.tsx @@ -534,14 +534,18 @@ export function Files() { } const result = new Map>() - const collect = (folderId: string): Set => { + const collect = (folderId: string, seen = new Set()): Set => { const cached = result.get(folderId) if (cached) return cached + if (seen.has(folderId)) return new Set() + const nextSeen = new Set(seen) + nextSeen.add(folderId) const descendants = new Set() for (const childId of childrenByParent.get(folderId) ?? []) { + if (nextSeen.has(childId)) continue descendants.add(childId) - for (const nestedId of collect(childId)) { + for (const nestedId of collect(childId, nextSeen)) { descendants.add(nestedId) } } diff --git a/apps/sim/lib/uploads/contexts/workspace/workspace-file-folder-manager.ts b/apps/sim/lib/uploads/contexts/workspace/workspace-file-folder-manager.ts index d238c9186a2..bffb2749709 100644 --- a/apps/sim/lib/uploads/contexts/workspace/workspace-file-folder-manager.ts +++ b/apps/sim/lib/uploads/contexts/workspace/workspace-file-folder-manager.ts @@ -446,8 +446,11 @@ function collectDescendantFolderIds( } const descendants: string[] = [] + const seen = new Set([folderId]) const visit = (id: string) => { for (const childId of childrenByParent.get(id) ?? []) { + if (seen.has(childId)) continue + seen.add(childId) descendants.push(childId) visit(childId) } diff --git a/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts b/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts index 6fcf2e015d1..757142b3d01 100644 --- a/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts +++ b/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts @@ -29,6 +29,7 @@ import { MAX_WORKSPACE_FILE_SIZE } from '@/lib/uploads/shared/types' import { getWorkspaceWithOwner } from '@/lib/workspaces/permissions/utils' import { isUuid, sanitizeFileName } from '@/executor/constants' import type { UserFile } from '@/executor/types' +import type { WorkspaceFileFolderRecord } from './workspace-file-folder-manager' import { assertWorkspaceFileFolderTarget, buildWorkspaceFileFolderPathMap, @@ -69,6 +70,12 @@ export interface WorkspaceFileRecord { storageContext?: 'workspace' | 'mothership' } +interface ListWorkspaceFilesOptions { + scope?: WorkspaceFileScope + folders?: WorkspaceFileFolderRecord[] + hydrateFolderPaths?: boolean +} + /** * Workspace file key pattern: workspace/{workspaceId}/{timestamp}-{random}-{filename} */ @@ -620,10 +627,10 @@ export async function getWorkspaceFileByName( */ export async function listWorkspaceFiles( workspaceId: string, - options?: { scope?: WorkspaceFileScope } + options?: ListWorkspaceFilesOptions ): Promise { try { - const { scope = 'active' } = options ?? {} + const { scope = 'active', hydrateFolderPaths = true } = options ?? {} const files = await db .select() .from(workspaceFiles) @@ -647,8 +654,11 @@ export async function listWorkspaceFiles( ) .orderBy(workspaceFiles.uploadedAt) - const folders = await listWorkspaceFileFolders(workspaceId, { scope: 'all' }) - const folderPaths = buildWorkspaceFileFolderPathMap(folders) + const needsFolderPaths = hydrateFolderPaths && files.some((file) => file.folderId) + const folders = needsFolderPaths + ? (options?.folders ?? (await listWorkspaceFileFolders(workspaceId, { scope: 'all' }))) + : [] + const folderPaths = needsFolderPaths ? buildWorkspaceFileFolderPathMap(folders) : new Map() return files.map((file) => mapWorkspaceFileRecord(file, workspaceId, folderPaths)) } catch (error) { From c2880c58e23f25167a16d01590deeb9f33282247 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 12 May 2026 19:28:38 -0700 Subject: [PATCH 10/60] error handling --- .../app/workspace/[workspaceId]/files/files.tsx | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/files/files.tsx b/apps/sim/app/workspace/[workspaceId]/files/files.tsx index 8ffbe5222d7..137695e334c 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/files.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/files.tsx @@ -1052,12 +1052,17 @@ export function Files() { counter++ } - const folder = await createFolder.mutateAsync({ - workspaceId, - name, - parentId: currentFolderId, - }) - listRename.startRename(folderRowId(folder.id), folder.name) + try { + const folder = await createFolder.mutateAsync({ + workspaceId, + name, + parentId: currentFolderId, + }) + listRename.startRename(folderRowId(folder.id), folder.name) + } catch (error) { + logger.error('Failed to create folder:', error) + toast.error(error instanceof Error ? error.message : 'Failed to create folder') + } }, [workspaceId, createFolder, folders, currentFolderId, listRename.startRename]) const handleRowContextMenu = useCallback( From 26cfe570eacdae84daddda1f7fe783485dfc2e5b Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 12 May 2026 19:39:39 -0700 Subject: [PATCH 11/60] path improvements --- .../api/workspaces/[id]/files/download/route.ts | 17 ++++++++++++----- .../app/workspace/[workspaceId]/files/files.tsx | 17 ++++++++++++----- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/apps/sim/app/api/workspaces/[id]/files/download/route.ts b/apps/sim/app/api/workspaces/[id]/files/download/route.ts index 964545be69a..3fc0f6d1688 100644 --- a/apps/sim/app/api/workspaces/[id]/files/download/route.ts +++ b/apps/sim/app/api/workspaces/[id]/files/download/route.ts @@ -29,6 +29,17 @@ function safeZipPath(path: string): string { .join('/') } +function withZipPathSuffix(path: string, suffix: number): string { + const slashIndex = path.lastIndexOf('/') + const directory = slashIndex >= 0 ? `${path.slice(0, slashIndex + 1)}` : '' + const filename = slashIndex >= 0 ? path.slice(slashIndex + 1) : path + const dotIndex = filename.lastIndexOf('.') + + return dotIndex > 0 + ? `${directory}${filename.slice(0, dotIndex)} (${suffix})${filename.slice(dotIndex)}` + : `${directory}${filename} (${suffix})` +} + function collectDescendantFolderIds( selectedFolderIds: string[], folders: Array<{ id: string; parentId: string | null }> @@ -112,11 +123,7 @@ export const GET = withRouteHandler( let zipPath = basePath let suffix = 2 while (usedPaths.has(zipPath)) { - const dotIndex = basePath.lastIndexOf('.') - zipPath = - dotIndex > 0 - ? `${basePath.slice(0, dotIndex)} (${suffix})${basePath.slice(dotIndex)}` - : `${basePath} (${suffix})` + zipPath = withZipPathSuffix(basePath, suffix) suffix++ } usedPaths.add(zipPath) diff --git a/apps/sim/app/workspace/[workspaceId]/files/files.tsx b/apps/sim/app/workspace/[workspaceId]/files/files.tsx index 137695e334c..90154834aea 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/files.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/files.tsx @@ -141,6 +141,9 @@ const parseRowId = (rowId: string): { kind: 'file' | 'folder'; id: string } => { return { kind: 'file', id: rowId } } +const hasExternalFiles = (dataTransfer: DataTransfer): boolean => + dataTransfer.types.includes('Files') + function formatFileType(mimeType: string | null, filename: string): string { if (mimeType && MIME_TYPE_LABELS[mimeType]) { return MIME_TYPE_LABELS[mimeType] @@ -672,12 +675,12 @@ export function Files() { }, onDragOver: (e: DragEvent, rowId) => { const sourceRowIds = draggedRowIdsRef.current - const hasExternalFiles = e.dataTransfer.types.includes('Files') - if (!hasExternalFiles && isInvalidDropTarget(rowId, sourceRowIds)) return + const isExternalFileDrag = hasExternalFiles(e.dataTransfer) + if (!isExternalFileDrag && isInvalidDropTarget(rowId, sourceRowIds)) return e.preventDefault() e.stopPropagation() - e.dataTransfer.dropEffect = hasExternalFiles ? 'copy' : 'move' + e.dataTransfer.dropEffect = isExternalFileDrag ? 'copy' : 'move' setActiveDropTargetId(rowId) }, onDragLeave: (e: DragEvent, rowId) => { @@ -771,22 +774,26 @@ export function Files() { } const handleDragEnter = (e: React.DragEvent) => { + if (!hasExternalFiles(e.dataTransfer)) return e.preventDefault() dragCounterRef.current++ - if (e.dataTransfer.types.includes('Files')) setIsDraggingOver(true) + setIsDraggingOver(true) } - const handleDragLeave = () => { + const handleDragLeave = (e: React.DragEvent) => { + if (!hasExternalFiles(e.dataTransfer)) return dragCounterRef.current-- if (dragCounterRef.current === 0) setIsDraggingOver(false) } const handleDragOver = (e: React.DragEvent) => { + if (!hasExternalFiles(e.dataTransfer)) return e.preventDefault() e.dataTransfer.dropEffect = 'copy' } const handleDrop = async (e: React.DragEvent) => { + if (!hasExternalFiles(e.dataTransfer)) return e.preventDefault() dragCounterRef.current = 0 setIsDraggingOver(false) From 7f1a01f179c300b75a88fbea30135e4184be7db7 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 12 May 2026 21:08:33 -0700 Subject: [PATCH 12/60] cleanup, best practices --- .../components/action-bar/action-bar.tsx | 5 +- .../delete-confirm-modal.tsx | 56 +++++ .../components/delete-confirm-modal/index.ts | 1 + .../file-row-context-menu.tsx | 85 +++++++ .../components/file-row-context-menu/index.ts | 1 + .../workspace/[workspaceId]/files/files.tsx | 214 ++++-------------- 6 files changed, 193 insertions(+), 169 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/files/components/delete-confirm-modal/delete-confirm-modal.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/files/components/delete-confirm-modal/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/files/components/file-row-context-menu/file-row-context-menu.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/files/components/file-row-context-menu/index.ts diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/action-bar/action-bar.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/action-bar/action-bar.tsx index 3ae0067577f..1890d558194 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/action-bar/action-bar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/action-bar/action-bar.tsx @@ -3,6 +3,7 @@ import { domAnimation, LazyMotion, m } from 'framer-motion' import { Button, Download, Tooltip, Trash2 } from '@/components/emcn' import { Folder } from '@/components/emcn/icons' +import { cn } from '@/lib/core/utils/cn' interface FilesActionBarProps { selectedCount: number @@ -10,6 +11,7 @@ interface FilesActionBarProps { onMove?: () => void onDelete?: () => void isLoading?: boolean + className?: string } export function FilesActionBar({ @@ -18,6 +20,7 @@ export function FilesActionBar({ onMove, onDelete, isLoading = false, + className, }: FilesActionBarProps) { if (selectedCount === 0) return null @@ -28,7 +31,7 @@ export function FilesActionBar({ animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: 10 }} transition={{ duration: 0.2 }} - className='-translate-x-1/2 fixed bottom-6 left-1/2 z-50 transform' + className={cn('-translate-x-1/2 fixed bottom-6 left-1/2 z-50 transform', className)} >
diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/delete-confirm-modal/delete-confirm-modal.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/delete-confirm-modal/delete-confirm-modal.tsx new file mode 100644 index 00000000000..947b559f476 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/files/components/delete-confirm-modal/delete-confirm-modal.tsx @@ -0,0 +1,56 @@ +'use client' + +import { memo } from 'react' +import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from '@/components/emcn' + +interface DeleteConfirmModalProps { + open: boolean + onOpenChange: (open: boolean) => void + fileName?: string + fileCount: number + folderCount: number + onDelete: () => void + isPending: boolean +} + +export const DeleteConfirmModal = memo(function DeleteConfirmModal({ + open, + onOpenChange, + fileName, + fileCount, + folderCount, + onDelete, + isPending, +}: DeleteConfirmModalProps) { + const totalCount = fileCount + folderCount + const hasFolders = folderCount > 0 + const title = totalCount > 1 ? 'Delete Items' : hasFolders ? 'Delete Folder' : 'Delete File' + const consequence = hasFolders + ? totalCount > 1 + ? 'This will also delete files and folders inside any selected folders.' + : 'This will also delete files and folders inside it.' + : 'You can restore it from Recently Deleted in Settings.' + + return ( + + + {title} + +

+ Are you sure you want to delete{' '} + {fileName}?{' '} + {consequence} +

+
+ + + + +
+
+ ) +}) diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/delete-confirm-modal/index.ts b/apps/sim/app/workspace/[workspaceId]/files/components/delete-confirm-modal/index.ts new file mode 100644 index 00000000000..23b57d9365a --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/files/components/delete-confirm-modal/index.ts @@ -0,0 +1 @@ +export { DeleteConfirmModal } from './delete-confirm-modal' diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-row-context-menu/file-row-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-row-context-menu/file-row-context-menu.tsx new file mode 100644 index 00000000000..7c45bf9020d --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-row-context-menu/file-row-context-menu.tsx @@ -0,0 +1,85 @@ +'use client' + +import { memo } from 'react' +import { + Download, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, + Eye, + Pencil, + Trash2, +} from '@/components/emcn' + +interface FileRowContextMenuProps { + isOpen: boolean + position: { x: number; y: number } + onClose: () => void + onOpen: () => void + onDownload?: () => void + onRename: () => void + onDelete: () => void + canEdit: boolean +} + +export const FileRowContextMenu = memo(function FileRowContextMenu({ + isOpen, + position, + onClose, + onOpen, + onDownload, + onRename, + onDelete, + canEdit, +}: FileRowContextMenuProps) { + return ( + !open && onClose()} modal={false}> + +
+ + e.preventDefault()} + > + + + Open + + {onDownload && ( + + + Download + + )} + {canEdit && ( + <> + + + + Rename + + + + Delete + + + )} + + + ) +}) diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-row-context-menu/index.ts b/apps/sim/app/workspace/[workspaceId]/files/components/file-row-context-menu/index.ts new file mode 100644 index 00000000000..d53dca37c19 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-row-context-menu/index.ts @@ -0,0 +1 @@ +export { FileRowContextMenu } from './file-row-context-menu' diff --git a/apps/sim/app/workspace/[workspaceId]/files/files.tsx b/apps/sim/app/workspace/[workspaceId]/files/files.tsx index 90154834aea..9472cab1628 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/files.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/files.tsx @@ -1,7 +1,8 @@ 'use client' -import { type DragEvent, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { type DragEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { createLogger } from '@sim/logger' +import { toError } from '@sim/utils/errors' import { useParams, useRouter, useSearchParams } from 'next/navigation' import { Button, @@ -9,11 +10,6 @@ import { Combobox, type ComboboxOption, Download, - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, Eye, Loader, Modal, @@ -22,7 +18,7 @@ import { ModalFooter, ModalHeader, Pencil, - Trash, + Trash2, toast, Upload, } from '@/components/emcn' @@ -63,6 +59,8 @@ import { timeCell, } from '@/app/workspace/[workspaceId]/components' import { FilesActionBar } from '@/app/workspace/[workspaceId]/files/components/action-bar/action-bar' +import { DeleteConfirmModal } from '@/app/workspace/[workspaceId]/files/components/delete-confirm-modal' +import { FileRowContextMenu } from '@/app/workspace/[workspaceId]/files/components/file-row-context-menu' import type { PreviewMode } from '@/app/workspace/[workspaceId]/files/components/file-viewer' import { FileViewer, @@ -743,7 +741,7 @@ export function Files() { }) .catch((error) => { logger.error('Failed to move items via drag and drop:', error) - toast.error(error instanceof Error ? error.message : 'Failed to move selected items') + toast.error(toError(error).message) }) }, onDragEnd: () => { @@ -761,7 +759,7 @@ export function Files() { visibleRowIds, isInvalidDropTarget, uploadFiles, - moveItems, + moveItems.mutateAsync, workspaceId, ] ) @@ -850,7 +848,7 @@ export function Files() { } catch (err) { logger.error('Failed to delete file:', err) } - }, [workspaceId, router, currentFolderId, bulkArchiveItems, deleteFile]) + }, [workspaceId, router, currentFolderId, bulkArchiveItems.mutateAsync, deleteFile.mutateAsync]) const isDirtyRef = useRef(isDirty) isDirtyRef.current = isDirty @@ -922,13 +920,13 @@ export function Files() { window.location.href = `/api/workspaces/${workspaceId}/files/download?${query.toString()}` }, [selectedFileIds, selectedFolderIds, files, handleDownload, workspaceId]) - const handleOpenMoveModal = useCallback(() => { + const handleOpenMoveModal = () => { if (selectedFileIds.length === 0 && selectedFolderIds.length === 0) return setMoveTargetFolderId(currentFolderId) setShowMoveModal(true) - }, [selectedFileIds, selectedFolderIds, currentFolderId]) + } - const handleMoveSelected = useCallback(async () => { + const handleMoveSelected = async () => { try { await moveItems.mutateAsync({ workspaceId, @@ -940,9 +938,9 @@ export function Files() { setShowMoveModal(false) } catch (error) { logger.error('Failed to move selected items:', error) - toast.error(error instanceof Error ? error.message : 'Failed to move selected items') + toast.error(toError(error).message) } - }, [workspaceId, selectedFileIds, selectedFolderIds, moveTargetFolderId, moveItems]) + } const fileDetailBreadcrumbs = useMemo( () => @@ -961,30 +959,26 @@ export function Files() { } : undefined, dropdownItems: [ - { - label: 'Rename', - icon: Pencil, - onClick: handleStartHeaderRename, - }, - { - label: 'Download', - icon: Download, - onClick: handleDownloadSelected, - }, - { - label: 'Delete', - icon: Trash, - onClick: handleDeleteSelected, - }, + { label: 'Download', icon: Download, onClick: handleDownloadSelected }, + ...(canEdit + ? [ + { label: 'Rename', icon: Pencil, onClick: handleStartHeaderRename }, + { label: 'Delete', icon: Trash2, onClick: handleDeleteSelected }, + ] + : []), ], }, ] : [], [ selectedFile, + canEdit, handleBackAttempt, headerRename.editingId, headerRename.editValue, + headerRename.setEditValue, + headerRename.submitRename, + headerRename.cancelRename, handleStartHeaderRename, handleDownloadSelected, handleDeleteSelected, @@ -1046,7 +1040,7 @@ export function Files() { }, [workspaceId, router, currentFolderId]) const handleCreateFolder = useCallback(async () => { - if (!workspaceId || createFolder.isPending) return + if (!workspaceId) return const existingNames = new Set( folders .filter((folder) => (folder.parentId ?? null) === currentFolderId) @@ -1068,9 +1062,9 @@ export function Files() { listRename.startRename(folderRowId(folder.id), folder.name) } catch (error) { logger.error('Failed to create folder:', error) - toast.error(error instanceof Error ? error.message : 'Failed to create folder') + toast.error(toError(error).message) } - }, [workspaceId, createFolder, folders, currentFolderId, listRename.startRename]) + }, [workspaceId, createFolder.mutateAsync, folders, currentFolderId, listRename.startRename]) const handleRowContextMenu = useCallback( (e: React.MouseEvent, rowId: string) => { @@ -1290,14 +1284,19 @@ export function Files() { icon: Download, onClick: handleDownloadSelected, }, - { - label: 'Delete', - icon: Trash, - onClick: handleDeleteSelected, - }, + ...(canEdit + ? [ + { + label: 'Delete', + icon: Trash2, + onClick: handleDeleteSelected, + }, + ] + : []), ] }, [ selectedFile, + canEdit, saveStatus, previewMode, isDirty, @@ -1376,11 +1375,14 @@ export function Files() { [uploadButtonLabel, handleUploadClick, handleCreateFolder, createFolder.isPending, canEdit] ) - const handleNavigateToFiles = () => { + const handleNavigateToFiles = useCallback(() => { router.push(`/workspace/${workspaceId}/files`) - } + }, [router, workspaceId]) - const loadingBreadcrumbs = [{ label: 'Files', onClick: handleNavigateToFiles }, { label: '...' }] + const loadingBreadcrumbs = useMemo( + () => [{ label: 'Files', onClick: handleNavigateToFiles }, { label: '...' }], + [handleNavigateToFiles] + ) const listBreadcrumbs = useMemo(() => { const breadcrumbs = [{ label: 'Files', onClick: handleNavigateToFiles }] @@ -1400,7 +1402,7 @@ export function Files() { parentId = folder.id } return breadcrumbs - }, [currentFolderPath, folders, router, workspaceId]) + }, [currentFolderPath, folders, handleNavigateToFiles, router, workspaceId]) const memberOptions: ComboboxOption[] = useMemo( () => @@ -1564,13 +1566,12 @@ export function Files() { {hasActiveFilters && ( @@ -1785,7 +1786,7 @@ export function Files() { - @@ -1804,126 +1805,3 @@ export function Files() {
) } - -interface FileRowContextMenuProps { - isOpen: boolean - position: { x: number; y: number } - onClose: () => void - onOpen: () => void - onDownload?: () => void - onRename: () => void - onDelete: () => void - canEdit: boolean -} - -const FileRowContextMenu = memo(function FileRowContextMenu({ - isOpen, - position, - onClose, - onOpen, - onDownload, - onRename, - onDelete, - canEdit, -}: FileRowContextMenuProps) { - return ( - !open && onClose()} modal={false}> - -
- - e.preventDefault()} - > - - - Open - - {onDownload && ( - - - Download - - )} - {canEdit && ( - <> - - - - Rename - - - - Delete - - - )} - - - ) -}) - -interface DeleteConfirmModalProps { - open: boolean - onOpenChange: (open: boolean) => void - fileName?: string - fileCount: number - folderCount: number - onDelete: () => void - isPending: boolean -} - -const DeleteConfirmModal = memo(function DeleteConfirmModal({ - open, - onOpenChange, - fileName, - fileCount, - folderCount, - onDelete, - isPending, -}: DeleteConfirmModalProps) { - const totalCount = fileCount + folderCount - const hasFolders = folderCount > 0 - const title = totalCount > 1 ? 'Delete Items' : hasFolders ? 'Delete Folder' : 'Delete File' - const consequence = hasFolders - ? totalCount > 1 - ? 'This will also delete files and folders inside any selected folders.' - : 'This will also delete files and folders inside it.' - : 'You can restore it from Recently Deleted in Settings.' - - return ( - - - {title} - -

- Are you sure you want to delete{' '} - {fileName}?{' '} - {consequence} -

-
- - - - -
-
- ) -}) From dcd6d289e534e38019f2cf3282ae7489b6c60f12 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 12 May 2026 23:15:48 -0700 Subject: [PATCH 13/60] react query best practices: targeted invalidation, optimistic updates, key factory hierarchy - Add workspaceLists(workspaceId) intermediate key level to both workspaceFilesKeys and workspaceFileFolderKeys so invalidation targets only the affected workspace instead of all workspaces - Replace all lists() invalidation calls with workspaceLists(workspaceId) across every mutation (upload, rename, delete, restore, update content, folder mutations) - Add optimistic updates to useRenameWorkspaceFile and useUpdateWorkspaceFileFolder with onMutate snapshot, onError rollback, onSettled reconciliation - Move storage key into the content() factory as optional param so query keys are always built through the factory (useWorkspaceFileContent, useWorkspaceFileBinary) - Fix AnimatePresence wrapping in FilesActionBar so exit animation fires on deselect - Fix ResourceColGroup to use percentage weights instead of pixel widths to prevent horizontal scroll on narrow viewports --- .../components/resource/resource.tsx | 28 ++-- .../components/action-bar/action-bar.tsx | 128 +++++++++--------- .../files/components/action-bar/index.ts | 1 + .../workspace/[workspaceId]/files/files.tsx | 35 ++++- .../hooks/queries/workspace-file-folders.ts | 32 ++++- apps/sim/hooks/queries/workspace-files.ts | 69 ++++++++-- 6 files changed, 199 insertions(+), 94 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/files/components/action-bar/index.ts diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/resource.tsx b/apps/sim/app/workspace/[workspaceId]/components/resource/resource.tsx index bd408bba4db..bf94cb8083e 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/resource/resource.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/resource/resource.tsx @@ -47,6 +47,8 @@ export interface SelectableConfig { export interface RowDragDropConfig { activeDropTargetId?: string | null + draggedRowIds?: Set + isAnyDragActive?: boolean isRowDraggable?: (rowId: string) => boolean isRowDropTarget?: (rowId: string) => boolean onDragStart?: (e: DragEvent, rowId: string) => void @@ -490,6 +492,8 @@ const DataRow = memo(function DataRow({ const isDraggable = rowDragDrop?.isRowDraggable?.(row.id) ?? false const isDropTarget = rowDragDrop?.isRowDropTarget?.(row.id) ?? false const isActiveDropTarget = rowDragDrop?.activeDropTargetId === row.id + const isDragging = rowDragDrop?.draggedRowIds?.has(row.id) ?? false + const isAnyDragActive = rowDragDrop?.isAnyDragActive ?? false const handleClick = useCallback(() => { onRowClick?.(row.id) @@ -553,12 +557,14 @@ const DataRow = memo(function DataRow({ data-resource-row data-row-id={row.id} className={cn( - 'transition-colors hover-hover:bg-[var(--surface-3)]', + 'transition-colors', + !isAnyDragActive && 'hover-hover:bg-[var(--surface-3)]', onRowClick && 'cursor-pointer', isDraggable && 'cursor-grab active:cursor-grabbing', isDropTarget && 'data-[drop-target=true]:outline-offset-[-1px]', (selectedRowId === row.id || isSelected) && 'bg-[var(--surface-3)]', - isActiveDropTarget && 'bg-[var(--surface-4)] outline outline-1 outline-[var(--accent)]' + isActiveDropTarget && 'bg-[var(--surface-4)] outline outline-1 outline-[var(--accent)]', + (isDragging || (isAnyDragActive && isSelected && !isActiveDropTarget)) && 'opacity-50' )} data-drop-target={isDropTarget || undefined} draggable={isDraggable} @@ -629,22 +635,22 @@ interface ResourceColGroupProps { hasCheckbox?: boolean } +const CHECKBOX_WEIGHT = 0.4 + const ResourceColGroup = memo(function ResourceColGroup({ columns, hasCheckbox, }: ResourceColGroupProps) { + const weights = columns.map( + (col, colIdx) => (colIdx === 0 ? 2.5 : 1.0) * (col.widthMultiplier ?? 1) + ) + const total = (hasCheckbox ? CHECKBOX_WEIGHT : 0) + weights.reduce((s, w) => s + w, 0) + return ( - {hasCheckbox && } + {hasCheckbox && } {columns.map((col, colIdx) => ( - + ))} ) diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/action-bar/action-bar.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/action-bar/action-bar.tsx index 1890d558194..28bcc3a087c 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/action-bar/action-bar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/action-bar/action-bar.tsx @@ -1,6 +1,6 @@ 'use client' -import { domAnimation, LazyMotion, m } from 'framer-motion' +import { AnimatePresence, domAnimation, LazyMotion, m } from 'framer-motion' import { Button, Download, Tooltip, Trash2 } from '@/components/emcn' import { Folder } from '@/components/emcn/icons' import { cn } from '@/lib/core/utils/cn' @@ -22,70 +22,72 @@ export function FilesActionBar({ isLoading = false, className, }: FilesActionBarProps) { - if (selectedCount === 0) return null - return ( - -
- - {selectedCount} selected - -
- {onDownload && ( - - - - - Download - - )} - {onMove && ( - - - - - Move - - )} - {onDelete && ( - - - - - Delete - - )} -
-
-
+ + {selectedCount > 0 && ( + +
+ + {selectedCount} selected + +
+ {onDownload && ( + + + + + Download + + )} + {onMove && ( + + + + + Move + + )} + {onDelete && ( + + + + + Delete + + )} +
+
+
+ )} +
) } diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/action-bar/index.ts b/apps/sim/app/workspace/[workspaceId]/files/components/action-bar/index.ts new file mode 100644 index 00000000000..aa19162a077 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/files/components/action-bar/index.ts @@ -0,0 +1 @@ +export { FilesActionBar } from './action-bar' diff --git a/apps/sim/app/workspace/[workspaceId]/files/files.tsx b/apps/sim/app/workspace/[workspaceId]/files/files.tsx index 9472cab1628..e89980f9491 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/files.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/files.tsx @@ -58,7 +58,7 @@ import { ResourceHeader, timeCell, } from '@/app/workspace/[workspaceId]/components' -import { FilesActionBar } from '@/app/workspace/[workspaceId]/files/components/action-bar/action-bar' +import { FilesActionBar } from '@/app/workspace/[workspaceId]/files/components/action-bar' import { DeleteConfirmModal } from '@/app/workspace/[workspaceId]/files/components/delete-confirm-modal' import { FileRowContextMenu } from '@/app/workspace/[workspaceId]/files/components/file-row-context-menu' import type { PreviewMode } from '@/app/workspace/[workspaceId]/files/components/file-viewer' @@ -212,6 +212,8 @@ export function Files() { const justCreatedFileIdRef = useRef(null) const filesRef = useRef(files) filesRef.current = files + const foldersRef = useRef(folders) + foldersRef.current = folders const [uploading, setUploading] = useState(false) const [uploadProgress, setUploadProgress] = useState({ @@ -236,6 +238,7 @@ export function Files() { const [saveStatus, setSaveStatus] = useState('idle') const [selectedRowIds, setSelectedRowIds] = useState>(() => new Set()) const [activeDropTargetId, setActiveDropTargetId] = useState(null) + const [draggedRowIds, setDraggedRowIds] = useState>(() => new Set()) const [showMoveModal, setShowMoveModal] = useState(false) const [moveTargetFolderId, setMoveTargetFolderId] = useState(null) const [previewMode, setPreviewMode] = useState(() => { @@ -251,6 +254,7 @@ export function Files() { const [showDeleteConfirm, setShowDeleteConfirm] = useState(false) const contextMenuItemRef = useRef(null) const draggedRowIdsRef = useRef([]) + const dragGhostRef = useRef(null) const [deleteTarget, setDeleteTarget] = useState<{ fileIds: string[] folderIds: string[] @@ -647,6 +651,8 @@ export function Files() { const rowDragDropConfig = useMemo( () => ({ activeDropTargetId, + draggedRowIds, + isAnyDragActive: draggedRowIds.size > 0, isRowDraggable: (rowId) => canEdit && listRename.editingId !== rowId, isRowDropTarget: (rowId) => canEdit && parseRowId(rowId).kind === 'folder', onDragStart: (e: DragEvent, rowId) => { @@ -660,6 +666,7 @@ export function Files() { : [rowId] draggedRowIdsRef.current = sourceRowIds + setDraggedRowIds(new Set(sourceRowIds)) if (!selectedRowIds.has(rowId)) { setSelectedRowIds(new Set([rowId])) } @@ -670,6 +677,26 @@ export function Files() { JSON.stringify(sourceRowIds) ) e.dataTransfer.setData('text/plain', sourceRowIds.join(',')) + + const count = sourceRowIds.length + const firstParsed = parseRowId(sourceRowIds[0]) + const firstName = + firstParsed.kind === 'file' + ? filesRef.current.find((f) => f.id === firstParsed.id)?.name + : foldersRef.current.find((f) => f.id === firstParsed.id)?.name + const ghostLabel = + count > 1 ? `${firstName ?? 'Items'} +${count - 1} more` : (firstName ?? 'Item') + const ghost = document.createElement('div') + ghost.style.cssText = + 'position:fixed;top:-500px;left:0;display:inline-flex;align-items:center;padding:4px 10px;background:var(--surface-active);border:1px solid rgba(255,255,255,0.08);border-radius:8px;font-family:system-ui,-apple-system,sans-serif;font-size:13px;color:var(--text-body);white-space:nowrap;pointer-events:none;box-shadow:0 4px 12px rgba(0,0,0,0.4);z-index:9999' + const text = document.createElement('span') + text.style.cssText = 'max-width:200px;overflow:hidden;text-overflow:ellipsis' + text.textContent = ghostLabel + ghost.appendChild(text) + document.body.appendChild(ghost) + void ghost.offsetHeight + e.dataTransfer.setDragImage(ghost, ghost.offsetWidth / 2, ghost.offsetHeight / 2) + dragGhostRef.current = ghost }, onDragOver: (e: DragEvent, rowId) => { const sourceRowIds = draggedRowIdsRef.current @@ -745,14 +772,20 @@ export function Files() { }) }, onDragEnd: () => { + if (dragGhostRef.current) { + dragGhostRef.current.remove() + dragGhostRef.current = null + } dragCounterRef.current = 0 draggedRowIdsRef.current = [] + setDraggedRowIds(new Set()) setIsDraggingOver(false) setActiveDropTargetId(null) }, }), [ activeDropTargetId, + draggedRowIds, canEdit, listRename.editingId, selectedRowIds, diff --git a/apps/sim/hooks/queries/workspace-file-folders.ts b/apps/sim/hooks/queries/workspace-file-folders.ts index cbb5fe380e0..f8203317956 100644 --- a/apps/sim/hooks/queries/workspace-file-folders.ts +++ b/apps/sim/hooks/queries/workspace-file-folders.ts @@ -16,8 +16,10 @@ export type { WorkspaceFileFolderApi } export const workspaceFileFolderKeys = { all: ['workspaceFileFolders'] as const, lists: () => [...workspaceFileFolderKeys.all, 'list'] as const, + workspaceLists: (workspaceId: string) => + [...workspaceFileFolderKeys.lists(), workspaceId] as const, list: (workspaceId: string, scope: WorkspaceFileFolderScope = 'active') => - [...workspaceFileFolderKeys.lists(), workspaceId, scope] as const, + [...workspaceFileFolderKeys.workspaceLists(workspaceId), scope] as const, } async function fetchWorkspaceFileFolders( @@ -37,10 +39,9 @@ function invalidateWorkspaceFileBrowsers( queryClient: ReturnType, workspaceId: string ) { - queryClient.invalidateQueries({ queryKey: workspaceFileFolderKeys.lists() }) - queryClient.invalidateQueries({ queryKey: workspaceFilesKeys.lists() }) + queryClient.invalidateQueries({ queryKey: workspaceFileFolderKeys.workspaceLists(workspaceId) }) + queryClient.invalidateQueries({ queryKey: workspaceFilesKeys.workspaceLists(workspaceId) }) queryClient.invalidateQueries({ queryKey: workspaceFilesKeys.storageInfo() }) - queryClient.invalidateQueries({ queryKey: workspaceFileFolderKeys.list(workspaceId) }) } export function useWorkspaceFileFolders( @@ -89,6 +90,29 @@ export function useUpdateWorkspaceFileFolder() { }) return data.folder }, + onMutate: async ({ workspaceId, folderId, updates }) => { + await queryClient.cancelQueries({ + queryKey: workspaceFileFolderKeys.workspaceLists(workspaceId), + }) + const previous = queryClient.getQueryData( + workspaceFileFolderKeys.list(workspaceId, 'active') + ) + if (previous) { + queryClient.setQueryData( + workspaceFileFolderKeys.list(workspaceId, 'active'), + previous.map((f) => (f.id === folderId ? { ...f, ...updates } : f)) + ) + } + return { previous } + }, + onError: (_err, variables, context) => { + if (context?.previous) { + queryClient.setQueryData( + workspaceFileFolderKeys.list(variables.workspaceId, 'active'), + context.previous + ) + } + }, onSettled: (_data, _error, variables) => { invalidateWorkspaceFileBrowsers(queryClient, variables.workspaceId) }, diff --git a/apps/sim/hooks/queries/workspace-files.ts b/apps/sim/hooks/queries/workspace-files.ts index 480f867a3cf..d88b0140e7d 100644 --- a/apps/sim/hooks/queries/workspace-files.ts +++ b/apps/sim/hooks/queries/workspace-files.ts @@ -31,13 +31,23 @@ type WorkspaceFileQueryScope = 'active' | 'archived' | 'all' export const workspaceFilesKeys = { all: ['workspaceFiles'] as const, lists: () => [...workspaceFilesKeys.all, 'list'] as const, + workspaceLists: (workspaceId: string) => [...workspaceFilesKeys.lists(), workspaceId] as const, list: (workspaceId: string, scope: WorkspaceFileQueryScope = 'active') => - [...workspaceFilesKeys.lists(), workspaceId, scope] as const, + [...workspaceFilesKeys.workspaceLists(workspaceId), scope] as const, contents: () => [...workspaceFilesKeys.all, 'content'] as const, contentFile: (workspaceId: string, fileId: string) => [...workspaceFilesKeys.contents(), workspaceId, fileId] as const, - content: (workspaceId: string, fileId: string, mode: 'text' | 'raw' | 'binary' = 'text') => - [...workspaceFilesKeys.contentFile(workspaceId, fileId), mode] as const, + content: ( + workspaceId: string, + fileId: string, + mode: 'text' | 'raw' | 'binary' = 'text', + storageKey?: string + ) => + [ + ...workspaceFilesKeys.contentFile(workspaceId, fileId), + mode, + ...(storageKey ? [storageKey] : []), + ] as const, storageInfo: () => [...workspaceFilesKeys.all, 'storageInfo'] as const, } @@ -118,7 +128,7 @@ async function fetchWorkspaceFileContent( /** * Hook to fetch workspace file content as text. - * `key` (the storage object key) is included in the query key so that a new + * `key` (the storage object key) is forwarded into the query key factory so that a new * storage key (e.g. after a file is re-uploaded) correctly busts the cache. */ export function useWorkspaceFileContent( @@ -128,7 +138,7 @@ export function useWorkspaceFileContent( raw?: boolean ) { return useQuery({ - queryKey: [...workspaceFilesKeys.content(workspaceId, fileId, raw ? 'raw' : 'text'), key], + queryKey: workspaceFilesKeys.content(workspaceId, fileId, raw ? 'raw' : 'text', key), queryFn: ({ signal }) => fetchWorkspaceFileContent(key, signal, raw), enabled: !!workspaceId && !!fileId && !!key, staleTime: 30 * 1000, @@ -146,12 +156,12 @@ async function fetchWorkspaceFileBinary(key: string, signal?: AbortSignal): Prom /** * Hook to fetch workspace file content as binary (ArrayBuffer). - * `key` (the storage object key) is included in the query key so that a new + * `key` (the storage object key) is forwarded into the query key factory so that a new * storage key (e.g. after a file is re-uploaded) correctly busts the cache. */ export function useWorkspaceFileBinary(workspaceId: string, fileId: string, key: string) { return useQuery({ - queryKey: [...workspaceFilesKeys.content(workspaceId, fileId, 'binary'), key], + queryKey: workspaceFilesKeys.content(workspaceId, fileId, 'binary', key), queryFn: ({ signal }) => fetchWorkspaceFileBinary(key, signal), enabled: !!workspaceId && !!fileId && !!key, staleTime: 30 * 1000, @@ -330,7 +340,9 @@ export function useUploadWorkspaceFile() { uploadWorkspaceFile(workspaceId, file, folderId, onProgress, signal), onSettled: (_data, _error, variables) => { if (variables.skipInvalidation) return - queryClient.invalidateQueries({ queryKey: workspaceFilesKeys.lists() }) + queryClient.invalidateQueries({ + queryKey: workspaceFilesKeys.workspaceLists(variables.workspaceId), + }) queryClient.invalidateQueries({ queryKey: workspaceFilesKeys.storageInfo() }) }, onSuccess: (_data, variables) => { @@ -373,7 +385,9 @@ export function useUpdateWorkspaceFileContent() { queryClient.invalidateQueries({ queryKey: workspaceFilesKeys.contentFile(variables.workspaceId, variables.fileId), }) - queryClient.invalidateQueries({ queryKey: workspaceFilesKeys.lists() }) + queryClient.invalidateQueries({ + queryKey: workspaceFilesKeys.workspaceLists(variables.workspaceId), + }) queryClient.invalidateQueries({ queryKey: workspaceFilesKeys.storageInfo() }) }, onError: (error) => { @@ -400,11 +414,32 @@ export function useRenameWorkspaceFile() { params: { id: workspaceId, fileId }, body: { name }, }), - onError: (error) => { + onMutate: async ({ workspaceId, fileId, name }) => { + await queryClient.cancelQueries({ queryKey: workspaceFilesKeys.workspaceLists(workspaceId) }) + const previous = queryClient.getQueryData( + workspaceFilesKeys.list(workspaceId, 'active') + ) + if (previous) { + queryClient.setQueryData( + workspaceFilesKeys.list(workspaceId, 'active'), + previous.map((f) => (f.id === fileId ? { ...f, name } : f)) + ) + } + return { previous } + }, + onError: (error, variables, context) => { + if (context?.previous) { + queryClient.setQueryData( + workspaceFilesKeys.list(variables.workspaceId, 'active'), + context.previous + ) + } toast.error(error.message, { duration: 5000 }) }, onSettled: (_data, _error, variables) => { - queryClient.invalidateQueries({ queryKey: workspaceFilesKeys.lists() }) + queryClient.invalidateQueries({ + queryKey: workspaceFilesKeys.workspaceLists(variables.workspaceId), + }) }, }) } @@ -426,7 +461,7 @@ export function useDeleteWorkspaceFile() { params: { id: workspaceId, fileId }, }), onMutate: async ({ workspaceId, fileId }) => { - await queryClient.cancelQueries({ queryKey: workspaceFilesKeys.lists() }) + await queryClient.cancelQueries({ queryKey: workspaceFilesKeys.workspaceLists(workspaceId) }) const previousFiles = queryClient.getQueryData( workspaceFilesKeys.list(workspaceId, 'active') @@ -451,7 +486,9 @@ export function useDeleteWorkspaceFile() { logger.error('Failed to delete file') }, onSettled: (_data, _error, variables) => { - queryClient.invalidateQueries({ queryKey: workspaceFilesKeys.lists() }) + queryClient.invalidateQueries({ + queryKey: workspaceFilesKeys.workspaceLists(variables.workspaceId), + }) queryClient.removeQueries({ queryKey: workspaceFilesKeys.contentFile(variables.workspaceId, variables.fileId), }) @@ -468,8 +505,10 @@ export function useRestoreWorkspaceFile() { requestJson(restoreWorkspaceFileContract, { params: { id: workspaceId, fileId }, }), - onSettled: () => { - queryClient.invalidateQueries({ queryKey: workspaceFilesKeys.lists() }) + onSettled: (_data, _error, variables) => { + queryClient.invalidateQueries({ + queryKey: workspaceFilesKeys.workspaceLists(variables.workspaceId), + }) queryClient.invalidateQueries({ queryKey: workspaceFilesKeys.storageInfo() }) }, }) From 99b0fe4a7d16dc851be6b5adb3a24bb361c5fe01 Mon Sep 17 00:00:00 2001 From: waleed Date: Wed, 13 May 2026 11:23:59 -0700 Subject: [PATCH 14/60] add shift-click range selection and selection-aware context menu for files - Extend SelectableConfig.onSelectRow with optional shiftKey param; DataRow captures shiftKey before onCheckedChange fires via a ref so the Radix Checkbox interaction chain stays intact - Implement shift-click range selection in files.tsx using lastSelectedIndexRef; tracks last-selected index in visibleRowIds to compute the range - Reset lastSelectedIndexRef on deselect and select-all - Add selectedCount prop to FileRowContextMenu; hide Open and Rename when multiple items are selected, show "Delete N items" / "Download N items" labels in multi-select mode --- .../components/resource/resource.tsx | 14 +++++++-- .../file-row-context-menu.tsx | 28 ++++++++++------- .../workspace/[workspaceId]/files/files.tsx | 30 ++++++++++++++----- 3 files changed, 52 insertions(+), 20 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/resource.tsx b/apps/sim/app/workspace/[workspaceId]/components/resource/resource.tsx index bf94cb8083e..683447d8dc7 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/resource/resource.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/resource/resource.tsx @@ -39,7 +39,7 @@ export interface ResourceRow { export interface SelectableConfig { selectedIds: Set - onSelectRow: (id: string, checked: boolean) => void + onSelectRow: (id: string, checked: boolean, shiftKey?: boolean) => void onSelectAll: (checked: boolean) => void isAllSelected: boolean disabled?: boolean @@ -510,9 +510,17 @@ const DataRow = memo(function DataRow({ [onRowContextMenu, row.id] ) + const shiftKeyRef = useRef(false) + + const handleSelectRowClick = useCallback((e: React.MouseEvent) => { + e.stopPropagation() + shiftKeyRef.current = e.shiftKey + }, []) + const handleSelectRow = useCallback( (checked: boolean | 'indeterminate') => { - selectable?.onSelectRow(row.id, checked as boolean) + selectable?.onSelectRow(row.id, checked as boolean, shiftKeyRef.current) + shiftKeyRef.current = false }, [selectable, row.id] ) @@ -585,7 +593,7 @@ const DataRow = memo(function DataRow({ onCheckedChange={handleSelectRow} disabled={selectable.disabled} aria-label='Select row' - onClick={stopPropagation} + onClick={handleSelectRowClick} /> )} diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-row-context-menu/file-row-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-row-context-menu/file-row-context-menu.tsx index 7c45bf9020d..6cc6c9f8a5a 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-row-context-menu/file-row-context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-row-context-menu/file-row-context-menu.tsx @@ -22,6 +22,7 @@ interface FileRowContextMenuProps { onRename: () => void onDelete: () => void canEdit: boolean + selectedCount: number } export const FileRowContextMenu = memo(function FileRowContextMenu({ @@ -33,7 +34,10 @@ export const FileRowContextMenu = memo(function FileRowContextMenu({ onRename, onDelete, canEdit, + selectedCount, }: FileRowContextMenuProps) { + const isMultiSelect = selectedCount > 1 + return ( !open && onClose()} modal={false}> @@ -56,26 +60,30 @@ export const FileRowContextMenu = memo(function FileRowContextMenu({ sideOffset={4} onCloseAutoFocus={(e) => e.preventDefault()} > - - - Open - + {!isMultiSelect && ( + + + Open + + )} {onDownload && ( - Download + {isMultiSelect ? `Download ${selectedCount} items` : 'Download'} )} {canEdit && ( <> - - - Rename - + {!isMultiSelect && ( + + + Rename + + )} - Delete + {isMultiSelect ? `Delete ${selectedCount} items` : 'Delete'} )} diff --git a/apps/sim/app/workspace/[workspaceId]/files/files.tsx b/apps/sim/app/workspace/[workspaceId]/files/files.tsx index e89980f9491..b9452ac4b51 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/files.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/files.tsx @@ -253,6 +253,7 @@ export function Files() { const [showUnsavedChangesAlert, setShowUnsavedChangesAlert] = useState(false) const [showDeleteConfirm, setShowDeleteConfirm] = useState(false) const contextMenuItemRef = useRef(null) + const lastSelectedIndexRef = useRef(-1) const draggedRowIdsRef = useRef([]) const dragGhostRef = useRef(null) const [deleteTarget, setDeleteTarget] = useState<{ @@ -498,15 +499,29 @@ export function Files() { () => ({ selectedIds: selectedRowIds, isAllSelected, - onSelectRow: (rowId: string, checked: boolean) => { - setSelectedRowIds((prev) => { - const next = new Set(prev) - if (checked) next.add(rowId) - else next.delete(rowId) - return next - }) + onSelectRow: (rowId: string, checked: boolean, shiftKey?: boolean) => { + const currentIndex = visibleRowIds.indexOf(rowId) + if (shiftKey && lastSelectedIndexRef.current !== -1 && currentIndex !== -1) { + const start = Math.min(lastSelectedIndexRef.current, currentIndex) + const end = Math.max(lastSelectedIndexRef.current, currentIndex) + setSelectedRowIds((prev) => { + const next = new Set(prev) + for (let i = start; i <= end; i++) next.add(visibleRowIds[i]) + return next + }) + } else { + setSelectedRowIds((prev) => { + const next = new Set(prev) + if (checked) next.add(rowId) + else next.delete(rowId) + return next + }) + if (checked) lastSelectedIndexRef.current = currentIndex + else lastSelectedIndexRef.current = -1 + } }, onSelectAll: (checked: boolean) => { + lastSelectedIndexRef.current = -1 setSelectedRowIds((prev) => { const next = new Set(prev) for (const rowId of visibleRowIds) { @@ -1785,6 +1800,7 @@ export function Files() { onRename={handleContextMenuRename} onDelete={handleContextMenuDelete} canEdit={canEdit} + selectedCount={selectedRowIds.size} /> Date: Wed, 13 May 2026 11:29:06 -0700 Subject: [PATCH 15/60] add Move submenu to file context menu and fix shift-click anchor update - Add nested Move submenu to FileRowContextMenu using DropdownMenuSub/SubTrigger/SubContent; shows available folders filtered by selection, converts '__root__' -> null for moving to the root level - Add handleContextMenuMove in files.tsx that calls moveItems.mutateAsync directly (no modal) and clears selection on success - Fix shift-click range selection: update lastSelectedIndexRef after range select so chained shift-clicks extend from the new anchor point correctly --- .../file-row-context-menu.tsx | 30 +++++++++++++++++++ .../workspace/[workspaceId]/files/files.tsx | 23 ++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-row-context-menu/file-row-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-row-context-menu/file-row-context-menu.tsx index 6cc6c9f8a5a..26935d9c7da 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-row-context-menu/file-row-context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-row-context-menu/file-row-context-menu.tsx @@ -1,17 +1,27 @@ 'use client' import { memo } from 'react' +import { FolderInput } from 'lucide-react' import { Download, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, DropdownMenuTrigger, Eye, Pencil, Trash2, } from '@/components/emcn' +import { Folder } from '@/components/emcn/icons' + +interface MoveOption { + value: string + label: string +} interface FileRowContextMenuProps { isOpen: boolean @@ -21,6 +31,8 @@ interface FileRowContextMenuProps { onDownload?: () => void onRename: () => void onDelete: () => void + onMove?: (optionValue: string) => void + moveOptions?: MoveOption[] canEdit: boolean selectedCount: number } @@ -33,6 +45,8 @@ export const FileRowContextMenu = memo(function FileRowContextMenu({ onDownload, onRename, onDelete, + onMove, + moveOptions, canEdit, selectedCount, }: FileRowContextMenuProps) { @@ -81,6 +95,22 @@ export const FileRowContextMenu = memo(function FileRowContextMenu({ Rename )} + {onMove && moveOptions && moveOptions.length > 0 && ( + + + + {isMultiSelect ? `Move ${selectedCount} items` : 'Move to'} + + + {moveOptions.map((option) => ( + onMove(option.value)}> + + {option.label} + + ))} + + + )} {isMultiSelect ? `Delete ${selectedCount} items` : 'Delete'} diff --git a/apps/sim/app/workspace/[workspaceId]/files/files.tsx b/apps/sim/app/workspace/[workspaceId]/files/files.tsx index b9452ac4b51..9a633332cd6 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/files.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/files.tsx @@ -509,6 +509,7 @@ export function Files() { for (let i = start; i <= end; i++) next.add(visibleRowIds[i]) return next }) + lastSelectedIndexRef.current = currentIndex } else { setSelectedRowIds((prev) => { const next = new Set(prev) @@ -1194,6 +1195,26 @@ export function Files() { closeContextMenu() }, [selectedRowIds, handleBulkDelete, closeContextMenu]) + const handleContextMenuMove = useCallback( + async (optionValue: string) => { + const targetFolderId = optionValue === '__root__' ? null : optionValue + try { + await moveItems.mutateAsync({ + workspaceId, + fileIds: selectedFileIds, + folderIds: selectedFolderIds, + targetFolderId, + }) + setSelectedRowIds(new Set()) + closeContextMenu() + } catch (error) { + logger.error('Failed to move items:', error) + toast.error(toError(error).message) + } + }, + [moveItems.mutateAsync, workspaceId, selectedFileIds, selectedFolderIds, closeContextMenu] + ) + const handleContentContextMenu = useCallback( (e: React.MouseEvent) => { const target = e.target as HTMLElement @@ -1799,6 +1820,8 @@ export function Files() { onDownload={handleContextMenuDownload} onRename={handleContextMenuRename} onDelete={handleContextMenuDelete} + onMove={handleContextMenuMove} + moveOptions={moveFolderOptions} canEdit={canEdit} selectedCount={selectedRowIds.size} /> From 339835cb72235738034e4a72c9bc775dcc2d3ac0 Mon Sep 17 00:00:00 2001 From: waleed Date: Wed, 13 May 2026 11:33:59 -0700 Subject: [PATCH 16/60] fix move submenu: use folder names with tree-ordered indentation instead of stale paths - Compute folder depth from parentId chain client-side (avoids stale server-computed path field) - Tree-order folders so parents appear before their children, sorted by sortOrder then name - Show folder.name instead of folder.path so optimistic renames are reflected immediately - Indent each folder by depth * 12px in the submenu so po/shit renders as 'shit' indented under 'po' - MoveOption gains optional depth field; contextMenuMoveOptions is a separate memo from moveFolderOptions (modal keeps its existing path-label behavior) --- .../file-row-context-menu.tsx | 9 +++- .../workspace/[workspaceId]/files/files.tsx | 42 ++++++++++++++++++- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-row-context-menu/file-row-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-row-context-menu/file-row-context-menu.tsx index 26935d9c7da..faf51307476 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-row-context-menu/file-row-context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-row-context-menu/file-row-context-menu.tsx @@ -21,6 +21,7 @@ import { Folder } from '@/components/emcn/icons' interface MoveOption { value: string label: string + depth?: number } interface FileRowContextMenuProps { @@ -103,7 +104,13 @@ export const FileRowContextMenu = memo(function FileRowContextMenu({ {moveOptions.map((option) => ( - onMove(option.value)}> + onMove(option.value)} + style={ + option.depth ? { paddingLeft: `${option.depth * 12 + 8}px` } : undefined + } + > {option.label} diff --git a/apps/sim/app/workspace/[workspaceId]/files/files.tsx b/apps/sim/app/workspace/[workspaceId]/files/files.tsx index 9a633332cd6..85b1ba37aca 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/files.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/files.tsx @@ -1514,6 +1514,46 @@ export function Files() { [folders, selectedFolderIds, descendantFolderIdsByFolderId] ) + const contextMenuMoveOptions = useMemo(() => { + const depthById = new Map() + const getDepth = (id: string): number => { + if (depthById.has(id)) return depthById.get(id)! + const f = folders.find((folder) => folder.id === id) + const d = f?.parentId ? getDepth(f.parentId) + 1 : 0 + depthById.set(id, d) + return d + } + for (const f of folders) getDepth(f.id) + + const treeOrdered: WorkspaceFileFolderApi[] = [] + const addChildren = (parentId: string | null) => { + const children = folders + .filter((f) => f.parentId === parentId) + .sort((a, b) => a.sortOrder - b.sortOrder || a.name.localeCompare(b.name)) + for (const child of children) { + treeOrdered.push(child) + addChildren(child.id) + } + } + addChildren(null) + + const filtered = treeOrdered.filter((folder) => { + if (selectedFolderIds.includes(folder.id)) return false + return selectedFolderIds.every( + (selectedFolderId) => !descendantFolderIdsByFolderId.get(selectedFolderId)?.has(folder.id) + ) + }) + + return [ + { value: '__root__', label: 'Files', depth: 0 }, + ...filtered.map((folder) => ({ + value: folder.id, + label: folder.name, + depth: depthById.get(folder.id) ?? 0, + })), + ] + }, [folders, selectedFolderIds, descendantFolderIdsByFolderId]) + const sortConfig: SortConfig = useMemo( () => ({ options: [ @@ -1821,7 +1861,7 @@ export function Files() { onRename={handleContextMenuRename} onDelete={handleContextMenuDelete} onMove={handleContextMenuMove} - moveOptions={moveFolderOptions} + moveOptions={contextMenuMoveOptions} canEdit={canEdit} selectedCount={selectedRowIds.size} /> From 8190daae5451f181fd37f43ee3f862016bf94ffa Mon Sep 17 00:00:00 2001 From: waleed Date: Wed, 13 May 2026 11:38:40 -0700 Subject: [PATCH 17/60] fix shift-click anchor drift and remove dead stopPropagation constant - Remove dead stopPropagation const in resource.tsx (replaced by handleSelectRowClick) - Reset lastSelectedIndexRef when visibleRowIds changes so search/filter/folder navigation doesn't leave a stale anchor that produces wrong ranges on the next shift-click - Update lastSelectedIndexRef in handleRowContextMenu when right-clicking resets selection to a single item, so the anchor matches the newly-selected row - Add visibleRowIds to handleRowContextMenu deps (now reads it to compute anchor index) - Remove moveItems.mutateAsync from handleContextMenuMove deps per project convention (.mutateAsync is stable in TanStack v5) --- .../[workspaceId]/components/resource/resource.tsx | 2 -- apps/sim/app/workspace/[workspaceId]/files/files.tsx | 6 ++++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/resource.tsx b/apps/sim/app/workspace/[workspaceId]/components/resource/resource.tsx index 683447d8dc7..c078a78d329 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/resource/resource.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/resource/resource.tsx @@ -94,8 +94,6 @@ interface ResourceProps { const EMPTY_CELL_PLACEHOLDER = '- - -' const SKELETON_ROW_COUNT = 5 -const stopPropagation = (e: React.MouseEvent) => e.stopPropagation() - /** * Shared page shell for resource list pages (tables, files, knowledge, schedules, logs). * Renders the header, toolbar with search, and a data table from column/row definitions. diff --git a/apps/sim/app/workspace/[workspaceId]/files/files.tsx b/apps/sim/app/workspace/[workspaceId]/files/files.tsx index 85b1ba37aca..ad636641204 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/files.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/files.tsx @@ -538,6 +538,7 @@ export function Files() { ) useEffect(() => { + lastSelectedIndexRef.current = -1 setSelectedRowIds((prev) => { const visible = new Set(visibleRowIds) const next = new Set(Array.from(prev).filter((id) => visible.has(id))) @@ -1128,11 +1129,12 @@ export function Files() { ? { kind: 'folder', id: parsed.id, folder: item as WorkspaceFileFolderApi } : { kind: 'file', id: parsed.id, file: item as WorkspaceFileRecord } if (!selectedRowIds.has(rowId)) { + lastSelectedIndexRef.current = visibleRowIds.indexOf(rowId) setSelectedRowIds(new Set([rowId])) } openContextMenu(e) }, - [folders, openContextMenu, selectedRowIds] + [folders, openContextMenu, selectedRowIds, visibleRowIds] ) const handleContextMenuOpen = useCallback(() => { @@ -1212,7 +1214,7 @@ export function Files() { toast.error(toError(error).message) } }, - [moveItems.mutateAsync, workspaceId, selectedFileIds, selectedFolderIds, closeContextMenu] + [workspaceId, selectedFileIds, selectedFolderIds, closeContextMenu] ) const handleContentContextMenu = useCallback( From a5c27a16dfa10088b92404166dfc2b36a4a74fd8 Mon Sep 17 00:00:00 2001 From: waleed Date: Wed, 13 May 2026 12:59:37 -0700 Subject: [PATCH 18/60] complete workspace files feature: audit logs, posthog events, folder restore, empty state, keyboard shortcuts, storage indicator, breadcrumb rename - Audit + PostHog: wire file_renamed, file_deleted, file_moved, file_bulk_deleted, folder_created, folder_renamed, folder_deleted, folder_moved events to all file/folder API routes - Add AuditAction.FOLDER_UPDATED, FILE_MOVED, FOLDER_MOVED to audit types - Folder restore: server function, contract, API route (POST /files/folders/[folderId]/restore), hook (useRestoreWorkspaceFileFolder), Recently Deleted integration with new File Folders tab - Empty state: contextual emptyMessage passed to based on search/filters/folder context - Keyboard shortcuts: Delete/Backspace deletes selection, Escape deselects, Cmd+A selects all (list view only, input-aware guard) - Storage indicator: useStorageInfo drives compact "used / limit" display in file list header via leadingActions - Breadcrumb rename: current folder breadcrumb gains Rename dropdown + inline editing via breadcrumbRename (useInlineRename) - Resource: thread leadingActions prop from ResourceProps to ResourceHeader --- .../workspaces/[id]/files/[fileId]/route.ts | 13 ++ .../[id]/files/bulk-archive/route.ts | 32 +++++ .../files/folders/[folderId]/restore/route.ts | 66 +++++++++ .../[id]/files/folders/[folderId]/route.ts | 35 +++++ .../workspaces/[id]/files/folders/route.ts | 19 +++ .../api/workspaces/[id]/files/move/route.ts | 32 +++++ .../components/resource/resource.tsx | 19 ++- .../components/action-bar/action-bar.tsx | 5 +- .../delete-confirm-modal.tsx | 18 ++- .../workspace/[workspaceId]/files/files.tsx | 129 ++++++++++++++++-- .../recently-deleted/recently-deleted.tsx | 44 +++++- .../hooks/queries/workspace-file-folders.ts | 17 ++- .../api/contracts/workspace-file-folders.ts | 12 ++ apps/sim/lib/posthog/events.ts | 30 ++++ .../workspace-file-folder-manager.ts | 36 +++++ packages/audit/src/types.ts | 3 + scripts/check-api-validation-contracts.ts | 4 +- 17 files changed, 486 insertions(+), 28 deletions(-) create mode 100644 apps/sim/app/api/workspaces/[id]/files/folders/[folderId]/restore/route.ts diff --git a/apps/sim/app/api/workspaces/[id]/files/[fileId]/route.ts b/apps/sim/app/api/workspaces/[id]/files/[fileId]/route.ts index dea5882bc0d..39dd1215da6 100644 --- a/apps/sim/app/api/workspaces/[id]/files/[fileId]/route.ts +++ b/apps/sim/app/api/workspaces/[id]/files/[fileId]/route.ts @@ -9,6 +9,7 @@ import { getValidationErrorMessage, parseRequest } from '@/lib/api/server' import { getSession } from '@/lib/auth' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { captureServerEvent } from '@/lib/posthog/server' import { deleteWorkspaceFile, FileConflictError, @@ -55,6 +56,12 @@ export const PATCH = withRouteHandler( logger.info(`[${requestId}] Renamed workspace file: ${fileId} to "${updatedFile.name}"`) + captureServerEvent( + session.user.id, + 'file_renamed', + { workspace_id: workspaceId }, + { groups: { workspace: workspaceId } } + ) recordAudit({ workspaceId, actorId: session.user.id, @@ -124,6 +131,12 @@ export const DELETE = withRouteHandler( logger.info(`[${requestId}] Archived workspace file: ${fileId}`) + captureServerEvent( + session.user.id, + 'file_deleted', + { workspace_id: workspaceId }, + { groups: { workspace: workspaceId } } + ) recordAudit({ workspaceId, actorId: session.user.id, diff --git a/apps/sim/app/api/workspaces/[id]/files/bulk-archive/route.ts b/apps/sim/app/api/workspaces/[id]/files/bulk-archive/route.ts index c24d0e13524..d064595fa29 100644 --- a/apps/sim/app/api/workspaces/[id]/files/bulk-archive/route.ts +++ b/apps/sim/app/api/workspaces/[id]/files/bulk-archive/route.ts @@ -1,9 +1,11 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { bulkArchiveWorkspaceFileItemsContract } from '@/lib/api/contracts/workspace-file-folders' import { parseRequest } from '@/lib/api/server' import { getSession } from '@/lib/auth' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { captureServerEvent } from '@/lib/posthog/server' import { bulkArchiveWorkspaceFileItems } from '@/lib/uploads/contexts/workspace' import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' @@ -28,6 +30,36 @@ export const POST = withRouteHandler( try { const deletedItems = await bulkArchiveWorkspaceFileItems({ workspaceId, fileIds, folderIds }) + captureServerEvent( + session.user.id, + 'file_bulk_deleted', + { workspace_id: workspaceId, file_count: fileIds.length, folder_count: folderIds.length }, + { groups: { workspace: workspaceId } } + ) + if (fileIds.length > 0) { + recordAudit({ + workspaceId, + actorId: session.user.id, + actorName: session.user.name, + actorEmail: session.user.email, + action: AuditAction.FILE_DELETED, + resourceType: AuditResourceType.FILE, + description: `Deleted ${fileIds.length} file${fileIds.length === 1 ? '' : 's'}`, + metadata: { fileIds }, + }) + } + if (folderIds.length > 0) { + recordAudit({ + workspaceId, + actorId: session.user.id, + actorName: session.user.name, + actorEmail: session.user.email, + action: AuditAction.FOLDER_DELETED, + resourceType: AuditResourceType.FOLDER, + description: `Deleted ${folderIds.length} folder${folderIds.length === 1 ? '' : 's'}`, + metadata: { folderIds }, + }) + } return NextResponse.json({ success: true, deletedItems }) } catch (error) { logger.error('Failed to bulk archive workspace file items:', error) diff --git a/apps/sim/app/api/workspaces/[id]/files/folders/[folderId]/restore/route.ts b/apps/sim/app/api/workspaces/[id]/files/folders/[folderId]/restore/route.ts new file mode 100644 index 00000000000..401ab3c7968 --- /dev/null +++ b/apps/sim/app/api/workspaces/[id]/files/folders/[folderId]/restore/route.ts @@ -0,0 +1,66 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { restoreWorkspaceFileFolderContract } from '@/lib/api/contracts/workspace-file-folders' +import { parseRequest } from '@/lib/api/server' +import { getSession } from '@/lib/auth' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { captureServerEvent } from '@/lib/posthog/server' +import { restoreWorkspaceFileFolder } from '@/lib/uploads/contexts/workspace' +import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' + +const logger = createLogger('WorkspaceFileFolderRestoreAPI') + +export const POST = withRouteHandler( + async (request: NextRequest, context: { params: Promise<{ id: string; folderId: string }> }) => { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const parsed = await parseRequest(restoreWorkspaceFileFolderContract, request, context) + if (!parsed.success) return parsed.response + const { id: workspaceId, folderId } = parsed.data.params + + const permission = await getUserEntityPermissions(session.user.id, 'workspace', workspaceId) + if (permission !== 'admin' && permission !== 'write') { + return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }) + } + + try { + const folder = await restoreWorkspaceFileFolder(workspaceId, folderId) + + logger.info(`Restored workspace file folder: ${folderId}`) + + captureServerEvent( + session.user.id, + 'folder_restored', + { folder_id: folderId, workspace_id: workspaceId }, + { groups: { workspace: workspaceId } } + ) + recordAudit({ + workspaceId, + actorId: session.user.id, + actorName: session.user.name, + actorEmail: session.user.email, + action: AuditAction.FOLDER_UPDATED, + resourceType: AuditResourceType.FOLDER, + resourceId: folderId, + resourceName: folder.name, + description: `Restored folder "${folder.name}"`, + request, + }) + + return NextResponse.json({ success: true, folder }) + } catch (error) { + logger.error('Failed to restore workspace file folder:', error) + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Failed to restore folder', + }, + { status: 400 } + ) + } + } +) diff --git a/apps/sim/app/api/workspaces/[id]/files/folders/[folderId]/route.ts b/apps/sim/app/api/workspaces/[id]/files/folders/[folderId]/route.ts index 065159ba43f..be834f2fb3d 100644 --- a/apps/sim/app/api/workspaces/[id]/files/folders/[folderId]/route.ts +++ b/apps/sim/app/api/workspaces/[id]/files/folders/[folderId]/route.ts @@ -1,3 +1,4 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { createLogger } from '@sim/logger' import { getPostgresErrorCode } from '@sim/utils/errors' import { type NextRequest, NextResponse } from 'next/server' @@ -8,6 +9,7 @@ import { import { parseRequest } from '@/lib/api/server' import { getSession } from '@/lib/auth' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { captureServerEvent } from '@/lib/posthog/server' import { archiveWorkspaceFileFolderRecursive, updateWorkspaceFileFolder, @@ -43,6 +45,23 @@ export const PATCH = withRouteHandler( folderId, ...parsed.data.body, }) + captureServerEvent( + session.user.id, + 'folder_renamed', + { workspace_id: workspaceId }, + { groups: { workspace: workspaceId } } + ) + recordAudit({ + workspaceId, + actorId: session.user.id, + actorName: session.user.name, + actorEmail: session.user.email, + action: AuditAction.FOLDER_UPDATED, + resourceType: AuditResourceType.FOLDER, + resourceId: folderId, + resourceName: folder.name, + description: `Updated folder "${folder.name}"`, + }) return NextResponse.json({ success: true, folder }) } catch (error) { logger.error('Failed to update workspace file folder:', error) @@ -86,6 +105,22 @@ export const DELETE = withRouteHandler( try { const deletedItems = await archiveWorkspaceFileFolderRecursive(workspaceId, folderId) + captureServerEvent( + session.user.id, + 'folder_deleted', + { workspace_id: workspaceId }, + { groups: { workspace: workspaceId } } + ) + recordAudit({ + workspaceId, + actorId: session.user.id, + actorName: session.user.name, + actorEmail: session.user.email, + action: AuditAction.FOLDER_DELETED, + resourceType: AuditResourceType.FOLDER, + resourceId: folderId, + description: `Deleted folder`, + }) return NextResponse.json({ success: true, deletedItems }) } catch (error) { logger.error('Failed to delete workspace file folder:', error) diff --git a/apps/sim/app/api/workspaces/[id]/files/folders/route.ts b/apps/sim/app/api/workspaces/[id]/files/folders/route.ts index 2d8999b737a..d4a76aefe5e 100644 --- a/apps/sim/app/api/workspaces/[id]/files/folders/route.ts +++ b/apps/sim/app/api/workspaces/[id]/files/folders/route.ts @@ -1,3 +1,4 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { @@ -7,6 +8,7 @@ import { import { parseRequest } from '@/lib/api/server' import { getSession } from '@/lib/auth' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { captureServerEvent } from '@/lib/posthog/server' import { createWorkspaceFileFolder, listWorkspaceFileFolders, @@ -66,6 +68,23 @@ export const POST = withRouteHandler( name, parentId, }) + captureServerEvent( + session.user.id, + 'folder_created', + { workspace_id: workspaceId }, + { groups: { workspace: workspaceId } } + ) + recordAudit({ + workspaceId, + actorId: session.user.id, + actorName: session.user.name, + actorEmail: session.user.email, + action: AuditAction.FOLDER_CREATED, + resourceType: AuditResourceType.FOLDER, + resourceId: folder.id, + resourceName: folder.name, + description: `Created folder "${folder.name}"`, + }) return NextResponse.json({ success: true, folder }) } catch (error) { logger.error('Failed to create workspace file folder:', error) diff --git a/apps/sim/app/api/workspaces/[id]/files/move/route.ts b/apps/sim/app/api/workspaces/[id]/files/move/route.ts index 6e626caee43..ab32759fcbc 100644 --- a/apps/sim/app/api/workspaces/[id]/files/move/route.ts +++ b/apps/sim/app/api/workspaces/[id]/files/move/route.ts @@ -1,3 +1,4 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { createLogger } from '@sim/logger' import { getPostgresErrorCode } from '@sim/utils/errors' import { type NextRequest, NextResponse } from 'next/server' @@ -5,6 +6,7 @@ import { moveWorkspaceFileItemsContract } from '@/lib/api/contracts/workspace-fi import { parseRequest } from '@/lib/api/server' import { getSession } from '@/lib/auth' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { captureServerEvent } from '@/lib/posthog/server' import { moveWorkspaceFileItems, WorkspaceFileFolderConflictError, @@ -38,6 +40,36 @@ export const POST = withRouteHandler( folderIds, targetFolderId, }) + captureServerEvent( + session.user.id, + 'file_moved', + { workspace_id: workspaceId, file_count: fileIds.length, folder_count: folderIds.length }, + { groups: { workspace: workspaceId } } + ) + if (fileIds.length > 0) { + recordAudit({ + workspaceId, + actorId: session.user.id, + actorName: session.user.name, + actorEmail: session.user.email, + action: AuditAction.FILE_MOVED, + resourceType: AuditResourceType.FILE, + description: `Moved ${fileIds.length} file${fileIds.length === 1 ? '' : 's'}${targetFolderId ? ' to folder' : ' to root'}`, + metadata: { fileIds, targetFolderId }, + }) + } + if (folderIds.length > 0) { + recordAudit({ + workspaceId, + actorId: session.user.id, + actorName: session.user.name, + actorEmail: session.user.email, + action: AuditAction.FOLDER_MOVED, + resourceType: AuditResourceType.FOLDER, + description: `Moved ${folderIds.length} folder${folderIds.length === 1 ? '' : 's'}${targetFolderId ? ' to folder' : ' to root'}`, + metadata: { folderIds, targetFolderId }, + }) + } return NextResponse.json({ success: true, movedItems: { files: moved.movedFiles, folders: moved.movedFolders }, diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/resource.tsx b/apps/sim/app/workspace/[workspaceId]/components/resource/resource.tsx index c078a78d329..fc7c42bc9b2 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/resource/resource.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/resource/resource.tsx @@ -73,6 +73,7 @@ interface ResourceProps { defaultSort?: string sort?: SortConfig headerActions?: HeaderAction[] + leadingActions?: ReactNode columns: ResourceColumn[] rows: ResourceRow[] selectedRowId?: string | null @@ -107,6 +108,7 @@ export const Resource = memo(function Resource({ defaultSort, sort: sortOverride, headerActions, + leadingActions, columns, rows, selectedRowId, @@ -135,6 +137,7 @@ export const Resource = memo(function Resource({ breadcrumbs={breadcrumbs} create={create} actions={headerActions} + leadingActions={leadingActions} /> { - onRowClick?.(row.id) - }, [onRowClick, row.id]) + const handleClick = useCallback( + (e: React.MouseEvent) => { + if (e.shiftKey && selectable && !selectable.disabled) { + e.preventDefault() + selectable.onSelectRow(row.id, true, true) + return + } + onRowClick?.(row.id) + }, + [onRowClick, row.id, selectable] + ) const handleMouseEnter = useCallback(() => { onRowHover?.(row.id) @@ -574,7 +585,7 @@ const DataRow = memo(function DataRow({ )} data-drop-target={isDropTarget || undefined} draggable={isDraggable} - onClick={onRowClick ? handleClick : undefined} + onClick={onRowClick || selectable ? handleClick : undefined} onMouseEnter={handleMouseEnter} onContextMenu={onRowContextMenu ? handleContextMenu : undefined} onDragStart={isDraggable ? handleDragStart : undefined} diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/action-bar/action-bar.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/action-bar/action-bar.tsx index 28bcc3a087c..dd3ed96c2c5 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/action-bar/action-bar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/action-bar/action-bar.tsx @@ -31,7 +31,10 @@ export function FilesActionBar({ animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: 10 }} transition={{ duration: 0.2 }} - className={cn('-translate-x-1/2 fixed bottom-6 left-1/2 z-50 transform', className)} + className={cn( + '-translate-x-1/2 fixed bottom-6 left-1/2 z-[var(--z-dropdown)] transform', + className + )} >
diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/delete-confirm-modal/delete-confirm-modal.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/delete-confirm-modal/delete-confirm-modal.tsx index 947b559f476..4c398421e26 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/delete-confirm-modal/delete-confirm-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/delete-confirm-modal/delete-confirm-modal.tsx @@ -1,7 +1,15 @@ 'use client' import { memo } from 'react' -import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from '@/components/emcn' +import { + Button, + Modal, + ModalBody, + ModalContent, + ModalDescription, + ModalFooter, + ModalHeader, +} from '@/components/emcn' interface DeleteConfirmModalProps { open: boolean @@ -29,18 +37,20 @@ export const DeleteConfirmModal = memo(function DeleteConfirmModal({ ? totalCount > 1 ? 'This will also delete files and folders inside any selected folders.' : 'This will also delete files and folders inside it.' - : 'You can restore it from Recently Deleted in Settings.' + : totalCount > 1 + ? 'You can restore them from Recently Deleted in Settings.' + : 'You can restore it from Recently Deleted in Settings.' return ( {title} -

+ Are you sure you want to delete{' '} {fileName}?{' '} {consequence} -

+
@@ -882,6 +885,9 @@ export default function PlaygroundPage() { Advanced + + Modal settings with general and advanced tabs +

General settings content

diff --git a/apps/sim/app/workspace/[workspaceId]/components/message-actions/message-actions.tsx b/apps/sim/app/workspace/[workspaceId]/components/message-actions/message-actions.tsx index 298aafbf722..359f152fa0f 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/message-actions/message-actions.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/message-actions/message-actions.tsx @@ -10,6 +10,7 @@ import { Modal, ModalBody, ModalContent, + ModalDescription, ModalFooter, ModalHeader, Textarea, @@ -239,6 +240,9 @@ export const MessageActions = memo(function MessageActions({ Give feedback + + Submit feedback about this response +

diff --git a/apps/sim/app/workspace/[workspaceId]/components/oauth-modal.tsx b/apps/sim/app/workspace/[workspaceId]/components/oauth-modal.tsx index 5d1b1a76179..4f028948794 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/oauth-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/oauth-modal.tsx @@ -11,6 +11,7 @@ import { Modal, ModalBody, ModalContent, + ModalDescription, ModalFooter, ModalHeader, } from '@/components/emcn' @@ -230,6 +231,9 @@ export function OAuthModal(props: OAuthModalProps) { Connect {providerName} + + Connect your {providerName} account to grant access +

diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/delete-chunk-modal/delete-chunk-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/delete-chunk-modal/delete-chunk-modal.tsx index aa30ce5761f..bd10d154ee5 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/delete-chunk-modal/delete-chunk-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/delete-chunk-modal/delete-chunk-modal.tsx @@ -1,6 +1,14 @@ 'use client' -import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from '@/components/emcn' +import { + Button, + Modal, + ModalBody, + ModalContent, + ModalDescription, + ModalFooter, + ModalHeader, +} from '@/components/emcn' import type { ChunkData } from '@/lib/knowledge/types' import { useDeleteChunk } from '@/hooks/queries/kb/knowledge' @@ -34,9 +42,9 @@ export function DeleteChunkModal({ Delete Chunk -

+ Are you sure you want to delete this chunk? This action cannot be undone. -

+
+ + View and edit tags assigned to this document +
diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx index 22b22551a29..e7e38119d63 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx @@ -11,6 +11,7 @@ import { Modal, ModalBody, ModalContent, + ModalDescription, ModalFooter, ModalHeader, Trash, @@ -72,9 +73,9 @@ function UnsavedChangesModal({ Unsaved Changes -

+ You have unsaved changes. Are you sure you want to discard them? -

+