From 35c551984f7d5463751cf890211ed8b5b23f6c1f Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Fri, 17 Oct 2025 14:44:22 -0700 Subject: [PATCH] feat(files): gmail upload attachment, workspace files, file storage limits (#1666) * feat(gmail): add attachment uploads * add workspace files * update landing page * fix lint * fix test * fixed UI * added additional S3 tools to upload files * added search filters for gmail trigger * added files to every block * works * fix * register sharepoint tool --------- Co-authored-by: waleed --- apps/docs/content/docs/en/tools/gmail.mdx | 2 + apps/docs/content/docs/en/tools/s3.mdx | 96 +- .../landing-pricing/landing-pricing.tsx | 2 + apps/sim/app/api/files/parse/route.ts | 84 +- apps/sim/app/api/files/upload/route.ts | 44 + .../knowledge/[id]/documents/route.test.ts | 18 +- .../app/api/knowledge/[id]/documents/route.ts | 21 +- .../api/tools/discord/send-message/route.ts | 175 + apps/sim/app/api/tools/gmail/draft/route.ts | 201 + apps/sim/app/api/tools/gmail/send/route.ts | 196 + .../api/tools/google_drive/upload/route.ts | 298 + .../microsoft_teams/write_channel/route.ts | 218 + .../tools/microsoft_teams/write_chat/route.ts | 215 + apps/sim/app/api/tools/mistral/parse/route.ts | 149 + .../app/api/tools/onedrive/upload/route.ts | 201 + apps/sim/app/api/tools/outlook/draft/route.ts | 185 + apps/sim/app/api/tools/outlook/send/route.ts | 198 + .../sim/app/api/tools/s3/copy-object/route.ts | 115 + .../app/api/tools/s3/delete-object/route.ts | 106 + .../app/api/tools/s3/list-objects/route.ts | 116 + apps/sim/app/api/tools/s3/put-object/route.ts | 153 + .../app/api/tools/sharepoint/upload/route.ts | 216 + .../app/api/tools/slack/send-message/route.ts | 227 + .../api/tools/telegram/send-document/route.ts | 149 + .../sim/app/api/tools/vision/analyze/route.ts | 231 + .../[id]/files/[fileId]/download/route.ts | 91 + .../workspaces/[id]/files/[fileId]/route.ts | 55 + .../app/api/workspaces/[id]/files/route.ts | 127 + .../components/upload-modal/upload-modal.tsx | 20 +- .../components/create-modal/create-modal.tsx | 25 +- .../sub-block/components/file-upload.tsx | 465 +- .../components/sub-block/sub-block.tsx | 1 + .../components/file-uploads/file-uploads.tsx | 319 + .../settings-modal/components/index.ts | 1 + .../settings-navigation.tsx | 8 + .../components/subscription/plan-configs.ts | 4 + .../settings-modal/settings-modal.tsx | 7 + .../subscription-modal/subscription-modal.tsx | 5 + apps/sim/blocks/blocks/discord.ts | 32 +- apps/sim/blocks/blocks/file.ts | 5 +- apps/sim/blocks/blocks/gmail.ts | 26 + apps/sim/blocks/blocks/google_drive.ts | 55 +- apps/sim/blocks/blocks/microsoft_teams.ts | 37 +- apps/sim/blocks/blocks/onedrive.ts | 47 +- apps/sim/blocks/blocks/outlook.ts | 27 + apps/sim/blocks/blocks/s3.ts | 408 +- apps/sim/blocks/blocks/sharepoint.ts | 87 +- apps/sim/blocks/blocks/slack.ts | 37 +- apps/sim/blocks/blocks/telegram.ts | 46 + apps/sim/blocks/blocks/vision.ts | 34 +- apps/sim/lib/billing/storage/index.ts | 2 + apps/sim/lib/billing/storage/limits.ts | 190 + apps/sim/lib/billing/storage/tracking.ts | 83 + apps/sim/lib/env.ts | 4 + apps/sim/lib/file-parsers/pdf-parser.ts | 5 +- apps/sim/lib/knowledge/documents/service.ts | 148 +- apps/sim/lib/uploads/file-processing.ts | 103 + apps/sim/lib/uploads/file-utils.ts | 17 + apps/sim/lib/uploads/workspace-files.ts | 303 + .../sim/lib/webhooks/gmail-polling-service.ts | 58 +- apps/sim/lib/workflows/execution-files.ts | 16 + apps/sim/tools/discord/send_message.ts | 54 +- apps/sim/tools/discord/types.ts | 1 + apps/sim/tools/file/parser.ts | 9 +- apps/sim/tools/gmail/draft.ts | 67 +- apps/sim/tools/gmail/send.ts | 61 +- apps/sim/tools/gmail/types.ts | 2 + apps/sim/tools/gmail/utils.ts | 88 + apps/sim/tools/google_drive/types.ts | 1 + apps/sim/tools/google_drive/upload.ts | 63 +- apps/sim/tools/microsoft_teams/types.ts | 1 + .../tools/microsoft_teams/write_channel.ts | 29 +- apps/sim/tools/microsoft_teams/write_chat.ts | 28 +- apps/sim/tools/mistral/parser.ts | 21 +- apps/sim/tools/mistral/types.ts | 3 + apps/sim/tools/onedrive/types.ts | 1 + apps/sim/tools/onedrive/upload.ts | 74 +- apps/sim/tools/outlook/draft.ts | 76 +- apps/sim/tools/outlook/send.ts | 114 +- apps/sim/tools/outlook/types.ts | 2 + apps/sim/tools/registry.ts | 18 +- apps/sim/tools/s3/copy_object.ts | 117 + apps/sim/tools/s3/delete_object.ts | 96 + apps/sim/tools/s3/index.ts | 6 +- apps/sim/tools/s3/list_objects.ts | 120 + apps/sim/tools/s3/put_object.ts | 125 + apps/sim/tools/s3/types.ts | 26 +- apps/sim/tools/sharepoint/index.ts | 2 + apps/sim/tools/sharepoint/types.ts | 22 + apps/sim/tools/sharepoint/upload_file.ts | 115 + apps/sim/tools/slack/message.ts | 27 +- apps/sim/tools/slack/types.ts | 1 + apps/sim/tools/telegram/index.ts | 2 + apps/sim/tools/telegram/send_document.ts | 143 + apps/sim/tools/telegram/types.ts | 13 + apps/sim/tools/vision/tool.ts | 98 +- apps/sim/tools/vision/types.ts | 3 +- apps/sim/triggers/gmail/poller.ts | 8 + packages/db/consts.ts | 9 + .../db/migrations/0100_public_black_cat.sql | 18 + .../db/migrations/meta/0100_snapshot.json | 7096 +++++++++++++++++ packages/db/migrations/meta/_journal.json | 7 + packages/db/schema.ts | 26 + 103 files changed, 14681 insertions(+), 626 deletions(-) create mode 100644 apps/sim/app/api/tools/discord/send-message/route.ts create mode 100644 apps/sim/app/api/tools/gmail/draft/route.ts create mode 100644 apps/sim/app/api/tools/gmail/send/route.ts create mode 100644 apps/sim/app/api/tools/google_drive/upload/route.ts create mode 100644 apps/sim/app/api/tools/microsoft_teams/write_channel/route.ts create mode 100644 apps/sim/app/api/tools/microsoft_teams/write_chat/route.ts create mode 100644 apps/sim/app/api/tools/mistral/parse/route.ts create mode 100644 apps/sim/app/api/tools/onedrive/upload/route.ts create mode 100644 apps/sim/app/api/tools/outlook/draft/route.ts create mode 100644 apps/sim/app/api/tools/outlook/send/route.ts create mode 100644 apps/sim/app/api/tools/s3/copy-object/route.ts create mode 100644 apps/sim/app/api/tools/s3/delete-object/route.ts create mode 100644 apps/sim/app/api/tools/s3/list-objects/route.ts create mode 100644 apps/sim/app/api/tools/s3/put-object/route.ts create mode 100644 apps/sim/app/api/tools/sharepoint/upload/route.ts create mode 100644 apps/sim/app/api/tools/slack/send-message/route.ts create mode 100644 apps/sim/app/api/tools/telegram/send-document/route.ts create mode 100644 apps/sim/app/api/tools/vision/analyze/route.ts create mode 100644 apps/sim/app/api/workspaces/[id]/files/[fileId]/download/route.ts create mode 100644 apps/sim/app/api/workspaces/[id]/files/[fileId]/route.ts create mode 100644 apps/sim/app/api/workspaces/[id]/files/route.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/file-uploads/file-uploads.tsx create mode 100644 apps/sim/lib/billing/storage/index.ts create mode 100644 apps/sim/lib/billing/storage/limits.ts create mode 100644 apps/sim/lib/billing/storage/tracking.ts create mode 100644 apps/sim/lib/uploads/file-processing.ts create mode 100644 apps/sim/lib/uploads/workspace-files.ts create mode 100644 apps/sim/tools/s3/copy_object.ts create mode 100644 apps/sim/tools/s3/delete_object.ts create mode 100644 apps/sim/tools/s3/list_objects.ts create mode 100644 apps/sim/tools/s3/put_object.ts create mode 100644 apps/sim/tools/sharepoint/upload_file.ts create mode 100644 apps/sim/tools/telegram/send_document.ts create mode 100644 packages/db/migrations/0100_public_black_cat.sql create mode 100644 packages/db/migrations/meta/0100_snapshot.json diff --git a/apps/docs/content/docs/en/tools/gmail.mdx b/apps/docs/content/docs/en/tools/gmail.mdx index 2d71786c2..70ea404e2 100644 --- a/apps/docs/content/docs/en/tools/gmail.mdx +++ b/apps/docs/content/docs/en/tools/gmail.mdx @@ -70,6 +70,7 @@ Send emails using Gmail | `body` | string | Yes | Email body content | | `cc` | string | No | CC recipients \(comma-separated\) | | `bcc` | string | No | BCC recipients \(comma-separated\) | +| `attachments` | file[] | No | Files to attach to the email | #### Output @@ -91,6 +92,7 @@ Draft emails using Gmail | `body` | string | Yes | Email body content | | `cc` | string | No | CC recipients \(comma-separated\) | | `bcc` | string | No | BCC recipients \(comma-separated\) | +| `attachments` | file[] | No | Files to attach to the email draft | #### Output diff --git a/apps/docs/content/docs/en/tools/s3.mdx b/apps/docs/content/docs/en/tools/s3.mdx index ab549c5cd..9c18acea3 100644 --- a/apps/docs/content/docs/en/tools/s3.mdx +++ b/apps/docs/content/docs/en/tools/s3.mdx @@ -1,6 +1,6 @@ --- title: S3 -description: View S3 files +description: Upload, download, list, and manage S3 files --- import { BlockInfoCard } from "@/components/ui/block-info-card" @@ -62,12 +62,37 @@ In Sim, the S3 integration enables your agents to retrieve and access files stor ## Usage Instructions -Integrate S3 into the workflow. Can get presigned URLs for S3 objects. Requires access key and secret access key. +Integrate S3 into the workflow. Upload files, download objects, list bucket contents, delete objects, and copy objects between buckets. Requires AWS access key and secret access key. ## Tools +### `s3_put_object` + +Upload a file to an AWS S3 bucket + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `accessKeyId` | string | Yes | Your AWS Access Key ID | +| `secretAccessKey` | string | Yes | Your AWS Secret Access Key | +| `region` | string | Yes | AWS region \(e.g., us-east-1\) | +| `bucketName` | string | Yes | S3 bucket name | +| `objectKey` | string | Yes | Object key/path in S3 \(e.g., folder/filename.ext\) | +| `file` | file | No | File to upload | +| `content` | string | No | Text content to upload \(alternative to file\) | +| `contentType` | string | No | Content-Type header \(auto-detected from file if not provided\) | +| `acl` | string | No | Access control list \(e.g., private, public-read\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `url` | string | URL of the uploaded S3 object | +| `metadata` | object | Upload metadata including ETag and location | + ### `s3_get_object` Retrieve an object from an AWS S3 bucket @@ -87,6 +112,73 @@ Retrieve an object from an AWS S3 bucket | `url` | string | Pre-signed URL for downloading the S3 object | | `metadata` | object | File metadata including type, size, name, and last modified date | +### `s3_list_objects` + +List objects in an AWS S3 bucket + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `accessKeyId` | string | Yes | Your AWS Access Key ID | +| `secretAccessKey` | string | Yes | Your AWS Secret Access Key | +| `region` | string | Yes | AWS region \(e.g., us-east-1\) | +| `bucketName` | string | Yes | S3 bucket name | +| `prefix` | string | No | Prefix to filter objects \(e.g., folder/\) | +| `maxKeys` | number | No | Maximum number of objects to return \(default: 1000\) | +| `continuationToken` | string | No | Token for pagination | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `objects` | array | List of S3 objects | + +### `s3_delete_object` + +Delete an object from an AWS S3 bucket + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `accessKeyId` | string | Yes | Your AWS Access Key ID | +| `secretAccessKey` | string | Yes | Your AWS Secret Access Key | +| `region` | string | Yes | AWS region \(e.g., us-east-1\) | +| `bucketName` | string | Yes | S3 bucket name | +| `objectKey` | string | Yes | Object key/path to delete | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `deleted` | boolean | Whether the object was successfully deleted | +| `metadata` | object | Deletion metadata | + +### `s3_copy_object` + +Copy an object within or between AWS S3 buckets + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `accessKeyId` | string | Yes | Your AWS Access Key ID | +| `secretAccessKey` | string | Yes | Your AWS Secret Access Key | +| `region` | string | Yes | AWS region \(e.g., us-east-1\) | +| `sourceBucket` | string | Yes | Source bucket name | +| `sourceKey` | string | Yes | Source object key/path | +| `destinationBucket` | string | Yes | Destination bucket name | +| `destinationKey` | string | Yes | Destination object key/path | +| `acl` | string | No | Access control list for the copied object \(e.g., private, public-read\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `url` | string | URL of the copied S3 object | +| `metadata` | object | Copy operation metadata | + ## Notes diff --git a/apps/sim/app/(landing)/components/landing-pricing/landing-pricing.tsx b/apps/sim/app/(landing)/components/landing-pricing/landing-pricing.tsx index 0809e76db..fedc031a5 100644 --- a/apps/sim/app/(landing)/components/landing-pricing/landing-pricing.tsx +++ b/apps/sim/app/(landing)/components/landing-pricing/landing-pricing.tsx @@ -8,6 +8,7 @@ import { Code2, Database, DollarSign, + HardDrive, Users, Workflow, } from 'lucide-react' @@ -42,6 +43,7 @@ interface PricingTier { */ const FREE_PLAN_FEATURES: PricingFeature[] = [ { icon: DollarSign, text: '$10 usage limit' }, + { icon: HardDrive, text: '5GB file storage' }, { icon: Workflow, text: 'Public template access' }, { icon: Users, text: 'Community support' }, { icon: Database, text: 'Limited log retention' }, diff --git a/apps/sim/app/api/files/parse/route.ts b/apps/sim/app/api/files/parse/route.ts index a8c8b7437..23a75c124 100644 --- a/apps/sim/app/api/files/parse/route.ts +++ b/apps/sim/app/api/files/parse/route.ts @@ -8,6 +8,7 @@ import { isSupportedFileType, parseFile } from '@/lib/file-parsers' import { createLogger } from '@/lib/logs/console/logger' import { validateExternalUrl } from '@/lib/security/input-validation' import { downloadFile, isUsingCloudStorage } from '@/lib/uploads' +import { extractStorageKey } from '@/lib/uploads/file-utils' import { UPLOAD_DIR_SERVER } from '@/lib/uploads/setup.server' import '@/lib/uploads/setup.server' @@ -69,13 +70,13 @@ export async function POST(request: NextRequest) { try { const requestData = await request.json() - const { filePath, fileType } = requestData + const { filePath, fileType, workspaceId } = requestData if (!filePath || (typeof filePath === 'string' && filePath.trim() === '')) { return NextResponse.json({ success: false, error: 'No file path provided' }, { status: 400 }) } - logger.info('File parse request received:', { filePath, fileType }) + logger.info('File parse request received:', { filePath, fileType, workspaceId }) if (Array.isArray(filePath)) { const results = [] @@ -89,7 +90,7 @@ export async function POST(request: NextRequest) { continue } - const result = await parseFileSingle(path, fileType) + const result = await parseFileSingle(path, fileType, workspaceId) if (result.metadata) { result.metadata.processingTime = Date.now() - startTime } @@ -117,7 +118,7 @@ export async function POST(request: NextRequest) { }) } - const result = await parseFileSingle(filePath, fileType) + const result = await parseFileSingle(filePath, fileType, workspaceId) if (result.metadata) { result.metadata.processingTime = Date.now() - startTime @@ -153,7 +154,11 @@ export async function POST(request: NextRequest) { /** * Parse a single file and return its content */ -async function parseFileSingle(filePath: string, fileType?: string): Promise { +async function parseFileSingle( + filePath: string, + fileType?: string, + workspaceId?: string +): Promise { logger.info('Parsing file:', filePath) if (!filePath || filePath.trim() === '') { @@ -174,7 +179,7 @@ async function parseFileSingle(filePath: string, fileType?: string): Promise { +async function handleExternalUrl( + url: string, + fileType?: string, + workspaceId?: string +): Promise { try { logger.info('Fetching external URL:', url) + logger.info('WorkspaceId for URL save:', workspaceId) const urlValidation = validateExternalUrl(url, 'fileUrl') if (!urlValidation.isValid) { @@ -231,6 +242,34 @@ async function handleExternalUrl(url: string, fileType?: string): Promise f.name === filename) + + if (existingFile) { + // Parse from workspace storage instead of re-downloading + const storageFilePath = `/api/files/serve/${existingFile.key}` + return handleCloudFile(storageFilePath, fileType) + } + } + } + const response = await fetch(url, { signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS), }) @@ -251,9 +290,23 @@ async function handleExternalUrl(url: string, fileType?: string): Promise { try { - let cloudKey: string - if (filePath.includes('/api/files/serve/s3/')) { - cloudKey = decodeURIComponent(filePath.split('/api/files/serve/s3/')[1]) - } else if (filePath.includes('/api/files/serve/blob/')) { - cloudKey = decodeURIComponent(filePath.split('/api/files/serve/blob/')[1]) - } else if (filePath.startsWith('/api/files/serve/')) { - cloudKey = decodeURIComponent(filePath.substring('/api/files/serve/'.length)) - } else { - cloudKey = filePath - } + const cloudKey = extractStorageKey(filePath) logger.info('Extracted cloud key:', cloudKey) diff --git a/apps/sim/app/api/files/upload/route.ts b/apps/sim/app/api/files/upload/route.ts index e0155ddc5..10f219f70 100644 --- a/apps/sim/app/api/files/upload/route.ts +++ b/apps/sim/app/api/files/upload/route.ts @@ -66,6 +66,8 @@ export async function POST(request: NextRequest) { logger.info( `Uploading files for execution-scoped storage: workflow=${workflowId}, execution=${executionId}` ) + } else if (workspaceId) { + logger.info(`Uploading files for workspace-scoped storage: workspace=${workspaceId}`) } const uploadResults = [] @@ -83,6 +85,7 @@ export async function POST(request: NextRequest) { const bytes = await file.arrayBuffer() const buffer = Buffer.from(bytes) + // Priority 1: Execution-scoped storage (temporary, 5 min expiry) if (workflowId && executionId) { const { uploadExecutionFile } = await import('@/lib/workflows/execution-file-storage') const userFile = await uploadExecutionFile( @@ -100,6 +103,47 @@ export async function POST(request: NextRequest) { continue } + // Priority 2: Workspace-scoped storage (persistent, no expiry) + if (workspaceId) { + try { + const { uploadWorkspaceFile } = await import('@/lib/uploads/workspace-files') + const userFile = await uploadWorkspaceFile( + workspaceId, + session.user.id, + buffer, + originalName, + file.type || 'application/octet-stream' + ) + + uploadResults.push(userFile) + continue + } catch (workspaceError) { + // Check error type + const errorMessage = + workspaceError instanceof Error ? workspaceError.message : 'Upload failed' + const isDuplicate = errorMessage.includes('already exists') + const isStorageLimitError = + errorMessage.includes('Storage limit exceeded') || + errorMessage.includes('storage limit') + + logger.warn(`Workspace file upload failed: ${errorMessage}`) + + // Determine appropriate status code + let statusCode = 500 + if (isDuplicate) statusCode = 409 + else if (isStorageLimitError) statusCode = 413 + + return NextResponse.json( + { + success: false, + error: errorMessage, + isDuplicate, + }, + { status: statusCode } + ) + } + } + try { logger.info(`Uploading file: ${originalName}`) const result = await uploadFile(buffer, originalName, file.type, file.size) diff --git a/apps/sim/app/api/knowledge/[id]/documents/route.test.ts b/apps/sim/app/api/knowledge/[id]/documents/route.test.ts index 8709c1ba2..1ff73940f 100644 --- a/apps/sim/app/api/knowledge/[id]/documents/route.test.ts +++ b/apps/sim/app/api/knowledge/[id]/documents/route.test.ts @@ -67,12 +67,20 @@ describe('Knowledge Base Documents API Route', () => { chunkCount: 5, tokenCount: 100, characterCount: 500, - processingStatus: 'completed', + processingStatus: 'completed' as const, processingStartedAt: new Date(), processingCompletedAt: new Date(), processingError: null, enabled: true, uploadedAt: new Date(), + tag1: null, + tag2: null, + tag3: null, + tag4: null, + tag5: null, + tag6: null, + tag7: null, + deletedAt: null, } const resetMocks = () => { @@ -343,7 +351,8 @@ describe('Knowledge Base Documents API Route', () => { expect(vi.mocked(createSingleDocument)).toHaveBeenCalledWith( validDocumentData, 'kb-123', - expect.any(String) + expect.any(String), + 'user-123' ) }) @@ -451,7 +460,8 @@ describe('Knowledge Base Documents API Route', () => { expect(vi.mocked(createDocumentRecords)).toHaveBeenCalledWith( validBulkData.documents, 'kb-123', - expect.any(String) + expect.any(String), + 'user-123' ) expect(vi.mocked(processDocumentsWithQueue)).toHaveBeenCalled() }) @@ -605,7 +615,7 @@ describe('Knowledge Base Documents API Route', () => { const data = await response.json() expect(response.status).toBe(500) - expect(data.error).toBe('Failed to create document') + expect(data.error).toBe('Database error') }) }) }) diff --git a/apps/sim/app/api/knowledge/[id]/documents/route.ts b/apps/sim/app/api/knowledge/[id]/documents/route.ts index 84c330d74..4c93d1dd2 100644 --- a/apps/sim/app/api/knowledge/[id]/documents/route.ts +++ b/apps/sim/app/api/knowledge/[id]/documents/route.ts @@ -179,7 +179,8 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: const createdDocuments = await createDocumentRecords( validatedData.documents, knowledgeBaseId, - requestId + requestId, + userId ) logger.info( @@ -243,7 +244,12 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: try { const validatedData = CreateDocumentSchema.parse(body) - const newDocument = await createSingleDocument(validatedData, knowledgeBaseId, requestId) + const newDocument = await createSingleDocument( + validatedData, + knowledgeBaseId, + requestId, + userId + ) // Track single document upload try { @@ -278,7 +284,13 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: } } catch (error) { logger.error(`[${requestId}] Error creating document`, error) - return NextResponse.json({ error: 'Failed to create document' }, { status: 500 }) + + // Check if it's a storage limit error + const errorMessage = error instanceof Error ? error.message : 'Failed to create document' + const isStorageLimitError = + errorMessage.includes('Storage limit exceeded') || errorMessage.includes('storage limit') + + return NextResponse.json({ error: errorMessage }, { status: isStorageLimitError ? 413 : 500 }) } } @@ -317,7 +329,8 @@ export async function PATCH(req: NextRequest, { params }: { params: Promise<{ id knowledgeBaseId, operation, documentIds, - requestId + requestId, + session.user.id ) return NextResponse.json({ diff --git a/apps/sim/app/api/tools/discord/send-message/route.ts b/apps/sim/app/api/tools/discord/send-message/route.ts new file mode 100644 index 000000000..7ea185751 --- /dev/null +++ b/apps/sim/app/api/tools/discord/send-message/route.ts @@ -0,0 +1,175 @@ +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkHybridAuth } from '@/lib/auth/hybrid' +import { createLogger } from '@/lib/logs/console/logger' +import { downloadFileFromStorage, processFilesToUserFiles } from '@/lib/uploads/file-processing' +import { generateRequestId } from '@/lib/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('DiscordSendMessageAPI') + +const DiscordSendMessageSchema = z.object({ + botToken: z.string().min(1, 'Bot token is required'), + channelId: z.string().min(1, 'Channel ID is required'), + content: z.string().optional().nullable(), + files: z.array(z.any()).optional().nullable(), +}) + +export async function POST(request: NextRequest) { + const requestId = generateRequestId() + + try { + const authResult = await checkHybridAuth(request, { requireWorkflowId: false }) + + if (!authResult.success) { + logger.warn(`[${requestId}] Unauthorized Discord send attempt: ${authResult.error}`) + return NextResponse.json( + { + success: false, + error: authResult.error || 'Authentication required', + }, + { status: 401 } + ) + } + + logger.info(`[${requestId}] Authenticated Discord send request via ${authResult.authType}`, { + userId: authResult.userId, + }) + + const body = await request.json() + const validatedData = DiscordSendMessageSchema.parse(body) + + logger.info(`[${requestId}] Sending Discord message`, { + channelId: validatedData.channelId, + hasFiles: !!(validatedData.files && validatedData.files.length > 0), + fileCount: validatedData.files?.length || 0, + }) + + const discordApiUrl = `https://discord.com/api/v10/channels/${validatedData.channelId}/messages` + + if (!validatedData.files || validatedData.files.length === 0) { + logger.info(`[${requestId}] No files, using JSON POST`) + + const response = await fetch(discordApiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bot ${validatedData.botToken}`, + }, + body: JSON.stringify({ + content: validatedData.content || '', + }), + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + logger.error(`[${requestId}] Discord API error:`, errorData) + return NextResponse.json( + { + success: false, + error: errorData.message || 'Failed to send message', + }, + { status: response.status } + ) + } + + const data = await response.json() + logger.info(`[${requestId}] Message sent successfully`) + return NextResponse.json({ + success: true, + output: { + message: data.content, + data: data, + }, + }) + } + + logger.info(`[${requestId}] Processing ${validatedData.files.length} file(s)`) + + const userFiles = processFilesToUserFiles(validatedData.files, requestId, logger) + + if (userFiles.length === 0) { + logger.warn(`[${requestId}] No valid files to upload, falling back to text-only`) + const response = await fetch(discordApiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bot ${validatedData.botToken}`, + }, + body: JSON.stringify({ + content: validatedData.content || '', + }), + }) + + const data = await response.json() + return NextResponse.json({ + success: true, + output: { + message: data.content, + data: data, + }, + }) + } + + const formData = new FormData() + + const payload = { + content: validatedData.content || '', + } + formData.append('payload_json', JSON.stringify(payload)) + + for (let i = 0; i < userFiles.length; i++) { + const userFile = userFiles[i] + logger.info(`[${requestId}] Downloading file ${i}: ${userFile.name}`) + + const buffer = await downloadFileFromStorage(userFile, requestId, logger) + + const blob = new Blob([new Uint8Array(buffer)], { type: userFile.type }) + formData.append(`files[${i}]`, blob, userFile.name) + logger.info(`[${requestId}] Added file ${i}: ${userFile.name} (${buffer.length} bytes)`) + } + + logger.info(`[${requestId}] Sending multipart request with ${userFiles.length} file(s)`) + const response = await fetch(discordApiUrl, { + method: 'POST', + headers: { + Authorization: `Bot ${validatedData.botToken}`, + }, + body: formData, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + logger.error(`[${requestId}] Discord API error:`, errorData) + return NextResponse.json( + { + success: false, + error: errorData.message || 'Failed to send message with files', + }, + { status: response.status } + ) + } + + const data = await response.json() + logger.info(`[${requestId}] Message with files sent successfully`) + + return NextResponse.json({ + success: true, + output: { + message: data.content, + data: data, + fileCount: userFiles.length, + }, + }) + } catch (error) { + logger.error(`[${requestId}] Error sending Discord message:`, error) + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Unknown error occurred', + }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/gmail/draft/route.ts b/apps/sim/app/api/tools/gmail/draft/route.ts new file mode 100644 index 000000000..15fbf7eef --- /dev/null +++ b/apps/sim/app/api/tools/gmail/draft/route.ts @@ -0,0 +1,201 @@ +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkHybridAuth } from '@/lib/auth/hybrid' +import { createLogger } from '@/lib/logs/console/logger' +import { downloadFileFromStorage, processFilesToUserFiles } from '@/lib/uploads/file-processing' +import { generateRequestId } from '@/lib/utils' +import { base64UrlEncode, buildMimeMessage } from '@/tools/gmail/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('GmailDraftAPI') + +const GMAIL_API_BASE = 'https://gmail.googleapis.com/gmail/v1/users/me' + +const GmailDraftSchema = z.object({ + accessToken: z.string().min(1, 'Access token is required'), + to: z.string().min(1, 'Recipient email is required'), + subject: z.string().min(1, 'Subject is required'), + body: z.string().min(1, 'Email body is required'), + cc: z.string().optional().nullable(), + bcc: z.string().optional().nullable(), + attachments: z.array(z.any()).optional().nullable(), +}) + +export async function POST(request: NextRequest) { + const requestId = generateRequestId() + + try { + const authResult = await checkHybridAuth(request, { requireWorkflowId: false }) + + if (!authResult.success) { + logger.warn(`[${requestId}] Unauthorized Gmail draft attempt: ${authResult.error}`) + return NextResponse.json( + { + success: false, + error: authResult.error || 'Authentication required', + }, + { status: 401 } + ) + } + + logger.info(`[${requestId}] Authenticated Gmail draft request via ${authResult.authType}`, { + userId: authResult.userId, + }) + + const body = await request.json() + const validatedData = GmailDraftSchema.parse(body) + + logger.info(`[${requestId}] Creating Gmail draft`, { + to: validatedData.to, + subject: validatedData.subject, + hasAttachments: !!(validatedData.attachments && validatedData.attachments.length > 0), + attachmentCount: validatedData.attachments?.length || 0, + }) + + let rawMessage: string | undefined + + if (validatedData.attachments && validatedData.attachments.length > 0) { + const rawAttachments = validatedData.attachments + logger.info(`[${requestId}] Processing ${rawAttachments.length} attachment(s)`) + + const attachments = processFilesToUserFiles(rawAttachments, requestId, logger) + + if (attachments.length === 0) { + logger.warn(`[${requestId}] No valid attachments found after processing`) + } else { + const totalSize = attachments.reduce((sum, file) => sum + file.size, 0) + const maxSize = 25 * 1024 * 1024 // 25MB + + if (totalSize > maxSize) { + const sizeMB = (totalSize / (1024 * 1024)).toFixed(2) + return NextResponse.json( + { + success: false, + error: `Total attachment size (${sizeMB}MB) exceeds Gmail's limit of 25MB`, + }, + { status: 400 } + ) + } + + const attachmentBuffers = await Promise.all( + attachments.map(async (file) => { + try { + logger.info( + `[${requestId}] Downloading attachment: ${file.name} (${file.size} bytes)` + ) + + const buffer = await downloadFileFromStorage(file, requestId, logger) + + return { + filename: file.name, + mimeType: file.type || 'application/octet-stream', + content: buffer, + } + } catch (error) { + logger.error(`[${requestId}] Failed to download attachment ${file.name}:`, error) + throw new Error( + `Failed to download attachment "${file.name}": ${error instanceof Error ? error.message : 'Unknown error'}` + ) + } + }) + ) + + const mimeMessage = buildMimeMessage({ + to: validatedData.to, + cc: validatedData.cc ?? undefined, + bcc: validatedData.bcc ?? undefined, + subject: validatedData.subject, + body: validatedData.body, + attachments: attachmentBuffers, + }) + + logger.info(`[${requestId}] Built MIME message for draft (${mimeMessage.length} bytes)`) + rawMessage = base64UrlEncode(mimeMessage) + } + } + + if (!rawMessage) { + const emailHeaders = [ + 'Content-Type: text/plain; charset="UTF-8"', + 'MIME-Version: 1.0', + `To: ${validatedData.to}`, + ] + + if (validatedData.cc) { + emailHeaders.push(`Cc: ${validatedData.cc}`) + } + if (validatedData.bcc) { + emailHeaders.push(`Bcc: ${validatedData.bcc}`) + } + + emailHeaders.push(`Subject: ${validatedData.subject}`, '', validatedData.body) + const email = emailHeaders.join('\n') + rawMessage = Buffer.from(email).toString('base64url') + } + + const gmailResponse = await fetch(`${GMAIL_API_BASE}/drafts`, { + method: 'POST', + headers: { + Authorization: `Bearer ${validatedData.accessToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + message: { raw: rawMessage }, + }), + }) + + if (!gmailResponse.ok) { + const errorText = await gmailResponse.text() + logger.error(`[${requestId}] Gmail API error:`, errorText) + return NextResponse.json( + { + success: false, + error: `Gmail API error: ${gmailResponse.statusText}`, + }, + { status: gmailResponse.status } + ) + } + + const data = await gmailResponse.json() + + logger.info(`[${requestId}] Draft created successfully`, { draftId: data.id }) + + return NextResponse.json({ + success: true, + output: { + content: 'Email drafted successfully', + metadata: { + id: data.id, + message: { + id: data.message?.id, + threadId: data.message?.threadId, + labelIds: data.message?.labelIds, + }, + }, + }, + }) + } catch (error) { + if (error instanceof z.ZodError) { + logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors }) + return NextResponse.json( + { + success: false, + error: 'Invalid request data', + details: error.errors, + }, + { status: 400 } + ) + } + + logger.error(`[${requestId}] Error creating Gmail draft:`, error) + + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Internal server error', + }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/gmail/send/route.ts b/apps/sim/app/api/tools/gmail/send/route.ts new file mode 100644 index 000000000..337e2591d --- /dev/null +++ b/apps/sim/app/api/tools/gmail/send/route.ts @@ -0,0 +1,196 @@ +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkHybridAuth } from '@/lib/auth/hybrid' +import { createLogger } from '@/lib/logs/console/logger' +import { downloadFileFromStorage, processFilesToUserFiles } from '@/lib/uploads/file-processing' +import { generateRequestId } from '@/lib/utils' +import { base64UrlEncode, buildMimeMessage } from '@/tools/gmail/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('GmailSendAPI') + +const GMAIL_API_BASE = 'https://gmail.googleapis.com/gmail/v1/users/me' + +const GmailSendSchema = z.object({ + accessToken: z.string().min(1, 'Access token is required'), + to: z.string().min(1, 'Recipient email is required'), + subject: z.string().min(1, 'Subject is required'), + body: z.string().min(1, 'Email body is required'), + cc: z.string().optional().nullable(), + bcc: z.string().optional().nullable(), + attachments: z.array(z.any()).optional().nullable(), +}) + +export async function POST(request: NextRequest) { + const requestId = generateRequestId() + + try { + const authResult = await checkHybridAuth(request, { requireWorkflowId: false }) + + if (!authResult.success) { + logger.warn(`[${requestId}] Unauthorized Gmail send attempt: ${authResult.error}`) + return NextResponse.json( + { + success: false, + error: authResult.error || 'Authentication required', + }, + { status: 401 } + ) + } + + logger.info(`[${requestId}] Authenticated Gmail send request via ${authResult.authType}`, { + userId: authResult.userId, + }) + + const body = await request.json() + const validatedData = GmailSendSchema.parse(body) + + logger.info(`[${requestId}] Sending Gmail email`, { + to: validatedData.to, + subject: validatedData.subject, + hasAttachments: !!(validatedData.attachments && validatedData.attachments.length > 0), + attachmentCount: validatedData.attachments?.length || 0, + }) + + let rawMessage: string | undefined + + if (validatedData.attachments && validatedData.attachments.length > 0) { + const rawAttachments = validatedData.attachments + logger.info(`[${requestId}] Processing ${rawAttachments.length} attachment(s)`) + + const attachments = processFilesToUserFiles(rawAttachments, requestId, logger) + + if (attachments.length === 0) { + logger.warn(`[${requestId}] No valid attachments found after processing`) + } else { + const totalSize = attachments.reduce((sum, file) => sum + file.size, 0) + const maxSize = 25 * 1024 * 1024 // 25MB + + if (totalSize > maxSize) { + const sizeMB = (totalSize / (1024 * 1024)).toFixed(2) + return NextResponse.json( + { + success: false, + error: `Total attachment size (${sizeMB}MB) exceeds Gmail's limit of 25MB`, + }, + { status: 400 } + ) + } + + const attachmentBuffers = await Promise.all( + attachments.map(async (file) => { + try { + logger.info( + `[${requestId}] Downloading attachment: ${file.name} (${file.size} bytes)` + ) + + const buffer = await downloadFileFromStorage(file, requestId, logger) + + return { + filename: file.name, + mimeType: file.type || 'application/octet-stream', + content: buffer, + } + } catch (error) { + logger.error(`[${requestId}] Failed to download attachment ${file.name}:`, error) + throw new Error( + `Failed to download attachment "${file.name}": ${error instanceof Error ? error.message : 'Unknown error'}` + ) + } + }) + ) + + const mimeMessage = buildMimeMessage({ + to: validatedData.to, + cc: validatedData.cc ?? undefined, + bcc: validatedData.bcc ?? undefined, + subject: validatedData.subject, + body: validatedData.body, + attachments: attachmentBuffers, + }) + + logger.info(`[${requestId}] Built MIME message (${mimeMessage.length} bytes)`) + rawMessage = base64UrlEncode(mimeMessage) + } + } + + if (!rawMessage) { + const emailHeaders = [ + 'Content-Type: text/plain; charset="UTF-8"', + 'MIME-Version: 1.0', + `To: ${validatedData.to}`, + ] + + if (validatedData.cc) { + emailHeaders.push(`Cc: ${validatedData.cc}`) + } + if (validatedData.bcc) { + emailHeaders.push(`Bcc: ${validatedData.bcc}`) + } + + emailHeaders.push(`Subject: ${validatedData.subject}`, '', validatedData.body) + const email = emailHeaders.join('\n') + rawMessage = Buffer.from(email).toString('base64url') + } + + const gmailResponse = await fetch(`${GMAIL_API_BASE}/messages/send`, { + method: 'POST', + headers: { + Authorization: `Bearer ${validatedData.accessToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ raw: rawMessage }), + }) + + if (!gmailResponse.ok) { + const errorText = await gmailResponse.text() + logger.error(`[${requestId}] Gmail API error:`, errorText) + return NextResponse.json( + { + success: false, + error: `Gmail API error: ${gmailResponse.statusText}`, + }, + { status: gmailResponse.status } + ) + } + + const data = await gmailResponse.json() + + logger.info(`[${requestId}] Email sent successfully`, { messageId: data.id }) + + return NextResponse.json({ + success: true, + output: { + content: 'Email sent successfully', + metadata: { + id: data.id, + threadId: data.threadId, + labelIds: data.labelIds, + }, + }, + }) + } catch (error) { + if (error instanceof z.ZodError) { + logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors }) + return NextResponse.json( + { + success: false, + error: 'Invalid request data', + details: error.errors, + }, + { status: 400 } + ) + } + + logger.error(`[${requestId}] Error sending Gmail email:`, error) + + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Internal server error', + }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/google_drive/upload/route.ts b/apps/sim/app/api/tools/google_drive/upload/route.ts new file mode 100644 index 000000000..a3e1ed655 --- /dev/null +++ b/apps/sim/app/api/tools/google_drive/upload/route.ts @@ -0,0 +1,298 @@ +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkHybridAuth } from '@/lib/auth/hybrid' +import { createLogger } from '@/lib/logs/console/logger' +import { downloadFileFromStorage, processSingleFileToUserFile } from '@/lib/uploads/file-processing' +import { generateRequestId } from '@/lib/utils' +import { + GOOGLE_WORKSPACE_MIME_TYPES, + handleSheetsFormat, + SOURCE_MIME_TYPES, +} from '@/tools/google_drive/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('GoogleDriveUploadAPI') + +const GOOGLE_DRIVE_API_BASE = 'https://www.googleapis.com/upload/drive/v3/files' + +const GoogleDriveUploadSchema = z.object({ + accessToken: z.string().min(1, 'Access token is required'), + fileName: z.string().min(1, 'File name is required'), + file: z.any().optional().nullable(), + mimeType: z.string().optional().nullable(), + folderId: z.string().optional().nullable(), +}) + +/** + * Build multipart upload body for Google Drive API + */ +function buildMultipartBody( + metadata: Record, + fileBuffer: Buffer, + mimeType: string, + boundary: string +): string { + const parts: string[] = [] + + parts.push(`--${boundary}`) + parts.push('Content-Type: application/json; charset=UTF-8') + parts.push('') + parts.push(JSON.stringify(metadata)) + + parts.push(`--${boundary}`) + parts.push(`Content-Type: ${mimeType}`) + parts.push('Content-Transfer-Encoding: base64') + parts.push('') + parts.push(fileBuffer.toString('base64')) + + parts.push(`--${boundary}--`) + + return parts.join('\r\n') +} + +export async function POST(request: NextRequest) { + const requestId = generateRequestId() + + try { + const authResult = await checkHybridAuth(request, { requireWorkflowId: false }) + + if (!authResult.success) { + logger.warn(`[${requestId}] Unauthorized Google Drive upload attempt: ${authResult.error}`) + return NextResponse.json( + { + success: false, + error: authResult.error || 'Authentication required', + }, + { status: 401 } + ) + } + + logger.info( + `[${requestId}] Authenticated Google Drive upload request via ${authResult.authType}`, + { + userId: authResult.userId, + } + ) + + const body = await request.json() + const validatedData = GoogleDriveUploadSchema.parse(body) + + logger.info(`[${requestId}] Uploading file to Google Drive`, { + fileName: validatedData.fileName, + mimeType: validatedData.mimeType, + folderId: validatedData.folderId, + hasFile: !!validatedData.file, + }) + + if (!validatedData.file) { + return NextResponse.json( + { + success: false, + error: 'No file provided. Use the text content field for text-only uploads.', + }, + { status: 400 } + ) + } + + // Process file - convert to UserFile format if needed + const fileData = validatedData.file + + let userFile + try { + userFile = processSingleFileToUserFile(fileData, requestId, logger) + } catch (error) { + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Failed to process file', + }, + { status: 400 } + ) + } + + logger.info(`[${requestId}] Downloading file from storage`, { + fileName: userFile.name, + key: userFile.key, + size: userFile.size, + }) + + let fileBuffer: Buffer + + try { + fileBuffer = await downloadFileFromStorage(userFile, requestId, logger) + } catch (error) { + logger.error(`[${requestId}] Failed to download file:`, error) + return NextResponse.json( + { + success: false, + error: `Failed to download file: ${error instanceof Error ? error.message : 'Unknown error'}`, + }, + { status: 500 } + ) + } + + let uploadMimeType = validatedData.mimeType || userFile.type || 'application/octet-stream' + const requestedMimeType = validatedData.mimeType || userFile.type || 'application/octet-stream' + + if (GOOGLE_WORKSPACE_MIME_TYPES.includes(requestedMimeType)) { + uploadMimeType = SOURCE_MIME_TYPES[requestedMimeType] || 'text/plain' + logger.info(`[${requestId}] Converting to Google Workspace type`, { + requestedMimeType, + uploadMimeType, + }) + } + + if (requestedMimeType === 'application/vnd.google-apps.spreadsheet') { + try { + const textContent = fileBuffer.toString('utf-8') + const { csv } = handleSheetsFormat(textContent) + if (csv !== undefined) { + fileBuffer = Buffer.from(csv, 'utf-8') + uploadMimeType = 'text/csv' + logger.info(`[${requestId}] Converted to CSV for Google Sheets upload`) + } + } catch (error) { + logger.warn(`[${requestId}] Could not convert to CSV, uploading as-is:`, error) + } + } + + const metadata: { + name: string + mimeType: string + parents?: string[] + } = { + name: validatedData.fileName, + mimeType: requestedMimeType, + } + + if (validatedData.folderId && validatedData.folderId.trim() !== '') { + metadata.parents = [validatedData.folderId.trim()] + } + + const boundary = `boundary_${Date.now()}_${Math.random().toString(36).substring(7)}` + + const multipartBody = buildMultipartBody(metadata, fileBuffer, uploadMimeType, boundary) + + logger.info(`[${requestId}] Uploading to Google Drive via multipart upload`, { + fileName: validatedData.fileName, + size: fileBuffer.length, + uploadMimeType, + requestedMimeType, + }) + + const uploadResponse = await fetch( + `${GOOGLE_DRIVE_API_BASE}?uploadType=multipart&supportsAllDrives=true`, + { + method: 'POST', + headers: { + Authorization: `Bearer ${validatedData.accessToken}`, + 'Content-Type': `multipart/related; boundary=${boundary}`, + 'Content-Length': Buffer.byteLength(multipartBody, 'utf-8').toString(), + }, + body: multipartBody, + } + ) + + if (!uploadResponse.ok) { + const errorText = await uploadResponse.text() + logger.error(`[${requestId}] Google Drive API error:`, { + status: uploadResponse.status, + statusText: uploadResponse.statusText, + error: errorText, + }) + return NextResponse.json( + { + success: false, + error: `Google Drive API error: ${uploadResponse.statusText}`, + }, + { status: uploadResponse.status } + ) + } + + const uploadData = await uploadResponse.json() + const fileId = uploadData.id + + logger.info(`[${requestId}] File uploaded successfully`, { fileId }) + + if (GOOGLE_WORKSPACE_MIME_TYPES.includes(requestedMimeType)) { + logger.info(`[${requestId}] Updating file name to ensure it persists after conversion`) + + const updateNameResponse = await fetch( + `https://www.googleapis.com/drive/v3/files/${fileId}?supportsAllDrives=true`, + { + method: 'PATCH', + headers: { + Authorization: `Bearer ${validatedData.accessToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name: validatedData.fileName, + }), + } + ) + + if (!updateNameResponse.ok) { + logger.warn( + `[${requestId}] Failed to update filename after conversion, but content was uploaded` + ) + } + } + + const finalFileResponse = await fetch( + `https://www.googleapis.com/drive/v3/files/${fileId}?supportsAllDrives=true&fields=id,name,mimeType,webViewLink,webContentLink,size,createdTime,modifiedTime,parents`, + { + headers: { + Authorization: `Bearer ${validatedData.accessToken}`, + }, + } + ) + + const finalFile = await finalFileResponse.json() + + logger.info(`[${requestId}] Upload complete`, { + fileId: finalFile.id, + fileName: finalFile.name, + webViewLink: finalFile.webViewLink, + }) + + return NextResponse.json({ + success: true, + output: { + file: { + id: finalFile.id, + name: finalFile.name, + mimeType: finalFile.mimeType, + webViewLink: finalFile.webViewLink, + webContentLink: finalFile.webContentLink, + size: finalFile.size, + createdTime: finalFile.createdTime, + modifiedTime: finalFile.modifiedTime, + parents: finalFile.parents, + }, + }, + }) + } catch (error) { + if (error instanceof z.ZodError) { + logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors }) + return NextResponse.json( + { + success: false, + error: 'Invalid request data', + details: error.errors, + }, + { status: 400 } + ) + } + + logger.error(`[${requestId}] Error uploading file to Google Drive:`, error) + + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Internal server error', + }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/microsoft_teams/write_channel/route.ts b/apps/sim/app/api/tools/microsoft_teams/write_channel/route.ts new file mode 100644 index 000000000..ef56d496b --- /dev/null +++ b/apps/sim/app/api/tools/microsoft_teams/write_channel/route.ts @@ -0,0 +1,218 @@ +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkHybridAuth } from '@/lib/auth/hybrid' +import { createLogger } from '@/lib/logs/console/logger' +import { downloadFileFromStorage, processFilesToUserFiles } from '@/lib/uploads/file-processing' +import { generateRequestId } from '@/lib/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('TeamsWriteChannelAPI') + +const TeamsWriteChannelSchema = z.object({ + accessToken: z.string().min(1, 'Access token is required'), + teamId: z.string().min(1, 'Team ID is required'), + channelId: z.string().min(1, 'Channel ID is required'), + content: z.string().min(1, 'Message content is required'), + files: z.array(z.any()).optional().nullable(), +}) + +export async function POST(request: NextRequest) { + const requestId = generateRequestId() + + try { + const authResult = await checkHybridAuth(request, { requireWorkflowId: false }) + + if (!authResult.success) { + logger.warn(`[${requestId}] Unauthorized Teams channel write attempt: ${authResult.error}`) + return NextResponse.json( + { + success: false, + error: authResult.error || 'Authentication required', + }, + { status: 401 } + ) + } + + logger.info( + `[${requestId}] Authenticated Teams channel write request via ${authResult.authType}`, + { + userId: authResult.userId, + } + ) + + const body = await request.json() + const validatedData = TeamsWriteChannelSchema.parse(body) + + logger.info(`[${requestId}] Sending Teams channel message`, { + teamId: validatedData.teamId, + channelId: validatedData.channelId, + hasFiles: !!(validatedData.files && validatedData.files.length > 0), + fileCount: validatedData.files?.length || 0, + }) + + const attachments: any[] = [] + if (validatedData.files && validatedData.files.length > 0) { + const rawFiles = validatedData.files + logger.info(`[${requestId}] Processing ${rawFiles.length} file(s) for upload to OneDrive`) + + const userFiles = processFilesToUserFiles(rawFiles, requestId, logger) + + for (const file of userFiles) { + try { + logger.info(`[${requestId}] Uploading file to Teams: ${file.name} (${file.size} bytes)`) + + const buffer = await downloadFileFromStorage(file, requestId, logger) + + const uploadUrl = + 'https://graph.microsoft.com/v1.0/me/drive/root:/TeamsAttachments/' + + encodeURIComponent(file.name) + + ':/content' + + logger.info(`[${requestId}] Uploading to Teams: ${uploadUrl}`) + + const uploadResponse = await fetch(uploadUrl, { + method: 'PUT', + headers: { + Authorization: `Bearer ${validatedData.accessToken}`, + 'Content-Type': file.type || 'application/octet-stream', + }, + body: new Uint8Array(buffer), + }) + + if (!uploadResponse.ok) { + const errorData = await uploadResponse.json().catch(() => ({})) + logger.error(`[${requestId}] Teams upload failed:`, errorData) + throw new Error( + `Failed to upload file to Teams: ${errorData.error?.message || 'Unknown error'}` + ) + } + + const uploadedFile = await uploadResponse.json() + logger.info(`[${requestId}] File uploaded to Teams successfully`, { + id: uploadedFile.id, + webUrl: uploadedFile.webUrl, + }) + + const fileDetailsUrl = `https://graph.microsoft.com/v1.0/me/drive/items/${uploadedFile.id}?$select=id,name,webDavUrl,eTag,size` + + const fileDetailsResponse = await fetch(fileDetailsUrl, { + headers: { + Authorization: `Bearer ${validatedData.accessToken}`, + }, + }) + + if (!fileDetailsResponse.ok) { + const errorData = await fileDetailsResponse.json().catch(() => ({})) + logger.error(`[${requestId}] Failed to get file details:`, errorData) + throw new Error( + `Failed to get file details: ${errorData.error?.message || 'Unknown error'}` + ) + } + + const fileDetails = await fileDetailsResponse.json() + logger.info(`[${requestId}] Got file details`, { + webDavUrl: fileDetails.webDavUrl, + eTag: fileDetails.eTag, + }) + + const attachmentId = fileDetails.eTag?.match(/\{([a-f0-9-]+)\}/i)?.[1] || fileDetails.id + + attachments.push({ + id: attachmentId, + contentType: 'reference', + contentUrl: fileDetails.webDavUrl, + name: file.name, + }) + + logger.info(`[${requestId}] Created attachment reference for ${file.name}`) + } catch (error) { + logger.error(`[${requestId}] Failed to process file ${file.name}:`, error) + throw new Error( + `Failed to process file "${file.name}": ${error instanceof Error ? error.message : 'Unknown error'}` + ) + } + } + + logger.info( + `[${requestId}] All ${attachments.length} file(s) uploaded and attachment references created` + ) + } + + let messageContent = validatedData.content + + if (attachments.length > 0) { + const attachmentTags = attachments + .map((att) => ``) + .join(' ') + messageContent = `${validatedData.content}
${attachmentTags}` + } + + const messageBody = { + body: { + contentType: attachments.length > 0 ? 'html' : 'text', + content: messageContent, + }, + } + + if (attachments.length > 0) { + ;(messageBody as any).attachments = attachments + } + + logger.info(`[${requestId}] Sending message to Teams channel: ${validatedData.channelId}`) + + const teamsUrl = `https://graph.microsoft.com/v1.0/teams/${encodeURIComponent(validatedData.teamId)}/channels/${encodeURIComponent(validatedData.channelId)}/messages` + + const teamsResponse = await fetch(teamsUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${validatedData.accessToken}`, + }, + body: JSON.stringify(messageBody), + }) + + if (!teamsResponse.ok) { + const errorData = await teamsResponse.json().catch(() => ({})) + logger.error(`[${requestId}] Microsoft Teams API error:`, errorData) + return NextResponse.json( + { + success: false, + error: errorData.error?.message || 'Failed to send Teams channel message', + }, + { status: teamsResponse.status } + ) + } + + const responseData = await teamsResponse.json() + logger.info(`[${requestId}] Teams channel message sent successfully`, { + messageId: responseData.id, + attachmentCount: attachments.length, + }) + + return NextResponse.json({ + success: true, + output: { + updatedContent: true, + metadata: { + messageId: responseData.id, + teamId: responseData.channelIdentity?.teamId || validatedData.teamId, + channelId: responseData.channelIdentity?.channelId || validatedData.channelId, + content: responseData.body?.content || validatedData.content, + createdTime: responseData.createdDateTime || new Date().toISOString(), + url: responseData.webUrl || '', + attachmentCount: attachments.length, + }, + }, + }) + } catch (error) { + logger.error(`[${requestId}] Error sending Teams channel message:`, error) + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Unknown error occurred', + }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/microsoft_teams/write_chat/route.ts b/apps/sim/app/api/tools/microsoft_teams/write_chat/route.ts new file mode 100644 index 000000000..f7dd51976 --- /dev/null +++ b/apps/sim/app/api/tools/microsoft_teams/write_chat/route.ts @@ -0,0 +1,215 @@ +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkHybridAuth } from '@/lib/auth/hybrid' +import { createLogger } from '@/lib/logs/console/logger' +import { downloadFileFromStorage, processFilesToUserFiles } from '@/lib/uploads/file-processing' +import { generateRequestId } from '@/lib/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('TeamsWriteChatAPI') + +const TeamsWriteChatSchema = z.object({ + accessToken: z.string().min(1, 'Access token is required'), + chatId: z.string().min(1, 'Chat ID is required'), + content: z.string().min(1, 'Message content is required'), + files: z.array(z.any()).optional().nullable(), +}) + +export async function POST(request: NextRequest) { + const requestId = generateRequestId() + + try { + const authResult = await checkHybridAuth(request, { requireWorkflowId: false }) + + if (!authResult.success) { + logger.warn(`[${requestId}] Unauthorized Teams chat write attempt: ${authResult.error}`) + return NextResponse.json( + { + success: false, + error: authResult.error || 'Authentication required', + }, + { status: 401 } + ) + } + + logger.info( + `[${requestId}] Authenticated Teams chat write request via ${authResult.authType}`, + { + userId: authResult.userId, + } + ) + + const body = await request.json() + const validatedData = TeamsWriteChatSchema.parse(body) + + logger.info(`[${requestId}] Sending Teams chat message`, { + chatId: validatedData.chatId, + hasFiles: !!(validatedData.files && validatedData.files.length > 0), + fileCount: validatedData.files?.length || 0, + }) + + const attachments: any[] = [] + if (validatedData.files && validatedData.files.length > 0) { + const rawFiles = validatedData.files + logger.info(`[${requestId}] Processing ${rawFiles.length} file(s) for upload to Teams`) + + const userFiles = processFilesToUserFiles(rawFiles, requestId, logger) + + for (const file of userFiles) { + try { + logger.info(`[${requestId}] Uploading file to Teams: ${file.name} (${file.size} bytes)`) + + const buffer = await downloadFileFromStorage(file, requestId, logger) + + const uploadUrl = + 'https://graph.microsoft.com/v1.0/me/drive/root:/TeamsAttachments/' + + encodeURIComponent(file.name) + + ':/content' + + logger.info(`[${requestId}] Uploading to Teams: ${uploadUrl}`) + + const uploadResponse = await fetch(uploadUrl, { + method: 'PUT', + headers: { + Authorization: `Bearer ${validatedData.accessToken}`, + 'Content-Type': file.type || 'application/octet-stream', + }, + body: new Uint8Array(buffer), + }) + + if (!uploadResponse.ok) { + const errorData = await uploadResponse.json().catch(() => ({})) + logger.error(`[${requestId}] Teams upload failed:`, errorData) + throw new Error( + `Failed to upload file to Teams: ${errorData.error?.message || 'Unknown error'}` + ) + } + + const uploadedFile = await uploadResponse.json() + logger.info(`[${requestId}] File uploaded to Teams successfully`, { + id: uploadedFile.id, + webUrl: uploadedFile.webUrl, + }) + + const fileDetailsUrl = `https://graph.microsoft.com/v1.0/me/drive/items/${uploadedFile.id}?$select=id,name,webDavUrl,eTag,size` + + const fileDetailsResponse = await fetch(fileDetailsUrl, { + headers: { + Authorization: `Bearer ${validatedData.accessToken}`, + }, + }) + + if (!fileDetailsResponse.ok) { + const errorData = await fileDetailsResponse.json().catch(() => ({})) + logger.error(`[${requestId}] Failed to get file details:`, errorData) + throw new Error( + `Failed to get file details: ${errorData.error?.message || 'Unknown error'}` + ) + } + + const fileDetails = await fileDetailsResponse.json() + logger.info(`[${requestId}] Got file details`, { + webDavUrl: fileDetails.webDavUrl, + eTag: fileDetails.eTag, + }) + + const attachmentId = fileDetails.eTag?.match(/\{([a-f0-9-]+)\}/i)?.[1] || fileDetails.id + + attachments.push({ + id: attachmentId, + contentType: 'reference', + contentUrl: fileDetails.webDavUrl, + name: file.name, + }) + + logger.info(`[${requestId}] Created attachment reference for ${file.name}`) + } catch (error) { + logger.error(`[${requestId}] Failed to process file ${file.name}:`, error) + throw new Error( + `Failed to process file "${file.name}": ${error instanceof Error ? error.message : 'Unknown error'}` + ) + } + } + + logger.info( + `[${requestId}] All ${attachments.length} file(s) uploaded and attachment references created` + ) + } + + let messageContent = validatedData.content + + if (attachments.length > 0) { + const attachmentTags = attachments + .map((att) => ``) + .join(' ') + messageContent = `${validatedData.content}
${attachmentTags}` + } + + const messageBody = { + body: { + contentType: attachments.length > 0 ? 'html' : 'text', + content: messageContent, + }, + } + + if (attachments.length > 0) { + ;(messageBody as any).attachments = attachments + } + + logger.info(`[${requestId}] Sending message to Teams chat: ${validatedData.chatId}`) + + const teamsUrl = `https://graph.microsoft.com/v1.0/chats/${encodeURIComponent(validatedData.chatId)}/messages` + + const teamsResponse = await fetch(teamsUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${validatedData.accessToken}`, + }, + body: JSON.stringify(messageBody), + }) + + if (!teamsResponse.ok) { + const errorData = await teamsResponse.json().catch(() => ({})) + logger.error(`[${requestId}] Microsoft Teams API error:`, errorData) + return NextResponse.json( + { + success: false, + error: errorData.error?.message || 'Failed to send Teams message', + }, + { status: teamsResponse.status } + ) + } + + const responseData = await teamsResponse.json() + logger.info(`[${requestId}] Teams message sent successfully`, { + messageId: responseData.id, + attachmentCount: attachments.length, + }) + + return NextResponse.json({ + success: true, + output: { + updatedContent: true, + metadata: { + messageId: responseData.id, + chatId: responseData.chatId || validatedData.chatId, + content: responseData.body?.content || validatedData.content, + createdTime: responseData.createdDateTime || new Date().toISOString(), + url: responseData.webUrl || '', + attachmentCount: attachments.length, + }, + }, + }) + } catch (error) { + logger.error(`[${requestId}] Error sending Teams chat message:`, error) + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Unknown error occurred', + }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/mistral/parse/route.ts b/apps/sim/app/api/tools/mistral/parse/route.ts new file mode 100644 index 000000000..ef77a867a --- /dev/null +++ b/apps/sim/app/api/tools/mistral/parse/route.ts @@ -0,0 +1,149 @@ +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkHybridAuth } from '@/lib/auth/hybrid' +import { createLogger } from '@/lib/logs/console/logger' +import { getPresignedUrl } from '@/lib/uploads' +import { extractStorageKey } from '@/lib/uploads/file-utils' +import { getBaseUrl } from '@/lib/urls/utils' +import { generateRequestId } from '@/lib/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('MistralParseAPI') + +const MistralParseSchema = z.object({ + apiKey: z.string().min(1, 'API key is required'), + filePath: z.string().min(1, 'File path is required'), + resultType: z.string().optional(), + pages: z.array(z.number()).optional(), + includeImageBase64: z.boolean().optional(), + imageLimit: z.number().optional(), + imageMinSize: z.number().optional(), +}) + +export async function POST(request: NextRequest) { + const requestId = generateRequestId() + + try { + const authResult = await checkHybridAuth(request, { requireWorkflowId: false }) + + if (!authResult.success) { + logger.warn(`[${requestId}] Unauthorized Mistral parse attempt: ${authResult.error}`) + return NextResponse.json( + { + success: false, + error: authResult.error || 'Authentication required', + }, + { status: 401 } + ) + } + + const body = await request.json() + const validatedData = MistralParseSchema.parse(body) + + logger.info(`[${requestId}] Mistral parse request`, { + filePath: validatedData.filePath, + isWorkspaceFile: validatedData.filePath.includes('/api/files/serve/'), + }) + + let fileUrl = validatedData.filePath + + // Check if it's an internal workspace file path + if (validatedData.filePath?.includes('/api/files/serve/')) { + try { + const storageKey = extractStorageKey(validatedData.filePath) + // Generate 5-minute presigned URL for external API access + fileUrl = await getPresignedUrl(storageKey, 5 * 60) + logger.info(`[${requestId}] Generated presigned URL for workspace file`) + } catch (error) { + logger.error(`[${requestId}] Failed to generate presigned URL:`, error) + return NextResponse.json( + { + success: false, + error: 'Failed to generate file access URL', + }, + { status: 500 } + ) + } + } else if (validatedData.filePath?.startsWith('/')) { + // Convert relative path to absolute URL + const baseUrl = getBaseUrl() + fileUrl = `${baseUrl}${validatedData.filePath}` + } + + // Call Mistral API with the resolved URL + const mistralBody: any = { + model: 'mistral-ocr-latest', + document: { + type: 'document_url', + document_url: fileUrl, + }, + } + + if (validatedData.pages) { + mistralBody.pages = validatedData.pages + } + if (validatedData.includeImageBase64 !== undefined) { + mistralBody.include_image_base64 = validatedData.includeImageBase64 + } + if (validatedData.imageLimit) { + mistralBody.image_limit = validatedData.imageLimit + } + if (validatedData.imageMinSize) { + mistralBody.image_min_size = validatedData.imageMinSize + } + + const mistralResponse = await fetch('https://api.mistral.ai/v1/ocr', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + Authorization: `Bearer ${validatedData.apiKey}`, + }, + body: JSON.stringify(mistralBody), + }) + + if (!mistralResponse.ok) { + const errorText = await mistralResponse.text() + logger.error(`[${requestId}] Mistral API error:`, errorText) + return NextResponse.json( + { + success: false, + error: `Mistral API error: ${mistralResponse.statusText}`, + }, + { status: mistralResponse.status } + ) + } + + const mistralData = await mistralResponse.json() + + logger.info(`[${requestId}] Mistral parse successful`) + + return NextResponse.json({ + success: true, + output: mistralData, + }) + } catch (error) { + if (error instanceof z.ZodError) { + logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors }) + return NextResponse.json( + { + success: false, + error: 'Invalid request data', + details: error.errors, + }, + { status: 400 } + ) + } + + logger.error(`[${requestId}] Error in Mistral parse:`, error) + + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Internal server error', + }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/onedrive/upload/route.ts b/apps/sim/app/api/tools/onedrive/upload/route.ts new file mode 100644 index 000000000..38437bf58 --- /dev/null +++ b/apps/sim/app/api/tools/onedrive/upload/route.ts @@ -0,0 +1,201 @@ +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkHybridAuth } from '@/lib/auth/hybrid' +import { createLogger } from '@/lib/logs/console/logger' +import { downloadFileFromStorage, processSingleFileToUserFile } from '@/lib/uploads/file-processing' +import { generateRequestId } from '@/lib/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('OneDriveUploadAPI') + +const MICROSOFT_GRAPH_BASE = 'https://graph.microsoft.com/v1.0' + +const OneDriveUploadSchema = z.object({ + accessToken: z.string().min(1, 'Access token is required'), + fileName: z.string().min(1, 'File name is required'), + file: z.any(), // UserFile object + folderId: z.string().optional().nullable(), +}) + +export async function POST(request: NextRequest) { + const requestId = generateRequestId() + + try { + const authResult = await checkHybridAuth(request, { requireWorkflowId: false }) + + if (!authResult.success) { + logger.warn(`[${requestId}] Unauthorized OneDrive upload attempt: ${authResult.error}`) + return NextResponse.json( + { + success: false, + error: authResult.error || 'Authentication required', + }, + { status: 401 } + ) + } + + logger.info(`[${requestId}] Authenticated OneDrive upload request via ${authResult.authType}`, { + userId: authResult.userId, + }) + + const body = await request.json() + const validatedData = OneDriveUploadSchema.parse(body) + + logger.info(`[${requestId}] Uploading file to OneDrive`, { + fileName: validatedData.fileName, + folderId: validatedData.folderId || 'root', + }) + + // Handle array or single file + const rawFile = validatedData.file + let fileToProcess + + if (Array.isArray(rawFile)) { + if (rawFile.length === 0) { + return NextResponse.json( + { + success: false, + error: 'No file provided', + }, + { status: 400 } + ) + } + fileToProcess = rawFile[0] + } else { + fileToProcess = rawFile + } + + // Convert to UserFile format + let userFile + try { + userFile = processSingleFileToUserFile(fileToProcess, requestId, logger) + } catch (error) { + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Failed to process file', + }, + { status: 400 } + ) + } + + logger.info(`[${requestId}] Downloading file from storage: ${userFile.key}`) + + let fileBuffer: Buffer + + try { + fileBuffer = await downloadFileFromStorage(userFile, requestId, logger) + } catch (error) { + logger.error(`[${requestId}] Failed to download file from storage:`, error) + return NextResponse.json( + { + success: false, + error: `Failed to download file: ${error instanceof Error ? error.message : 'Unknown error'}`, + }, + { status: 500 } + ) + } + + const maxSize = 250 * 1024 * 1024 // 250MB + if (fileBuffer.length > maxSize) { + const sizeMB = (fileBuffer.length / (1024 * 1024)).toFixed(2) + logger.warn(`[${requestId}] File too large: ${sizeMB}MB`) + return NextResponse.json( + { + success: false, + error: `File size (${sizeMB}MB) exceeds OneDrive's limit of 250MB for simple uploads. Use chunked upload for larger files.`, + }, + { status: 400 } + ) + } + + const fileName = validatedData.fileName || userFile.name + + let uploadUrl: string + const folderId = validatedData.folderId?.trim() + + if (folderId && folderId !== '') { + uploadUrl = `${MICROSOFT_GRAPH_BASE}/me/drive/items/${encodeURIComponent(folderId)}:/${encodeURIComponent(fileName)}:/content` + } else { + uploadUrl = `${MICROSOFT_GRAPH_BASE}/me/drive/root:/${encodeURIComponent(fileName)}:/content` + } + + logger.info(`[${requestId}] Uploading to OneDrive: ${uploadUrl}`) + + const mimeType = userFile.type || 'application/octet-stream' + + const uploadResponse = await fetch(uploadUrl, { + method: 'PUT', + headers: { + Authorization: `Bearer ${validatedData.accessToken}`, + 'Content-Type': mimeType, + }, + body: new Uint8Array(fileBuffer), + }) + + if (!uploadResponse.ok) { + const errorText = await uploadResponse.text() + logger.error(`[${requestId}] OneDrive upload failed:`, { + status: uploadResponse.status, + statusText: uploadResponse.statusText, + error: errorText, + }) + return NextResponse.json( + { + success: false, + error: `OneDrive upload failed: ${uploadResponse.statusText}`, + details: errorText, + }, + { status: uploadResponse.status } + ) + } + + const fileData = await uploadResponse.json() + + logger.info(`[${requestId}] File uploaded successfully to OneDrive`, { + fileId: fileData.id, + fileName: fileData.name, + size: fileData.size, + }) + + return NextResponse.json({ + success: true, + output: { + file: { + id: fileData.id, + name: fileData.name, + mimeType: fileData.file?.mimeType || mimeType, + webViewLink: fileData.webUrl, + webContentLink: fileData['@microsoft.graph.downloadUrl'], + size: fileData.size, + createdTime: fileData.createdDateTime, + modifiedTime: fileData.lastModifiedDateTime, + parentReference: fileData.parentReference, + }, + }, + }) + } catch (error) { + if (error instanceof z.ZodError) { + logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors }) + return NextResponse.json( + { + success: false, + error: 'Invalid request data', + details: error.errors, + }, + { status: 400 } + ) + } + + logger.error(`[${requestId}] Error uploading file to OneDrive:`, error) + + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Internal server error', + }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/outlook/draft/route.ts b/apps/sim/app/api/tools/outlook/draft/route.ts new file mode 100644 index 000000000..3d141287e --- /dev/null +++ b/apps/sim/app/api/tools/outlook/draft/route.ts @@ -0,0 +1,185 @@ +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkHybridAuth } from '@/lib/auth/hybrid' +import { createLogger } from '@/lib/logs/console/logger' +import { downloadFileFromStorage, processFilesToUserFiles } from '@/lib/uploads/file-processing' +import { generateRequestId } from '@/lib/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('OutlookDraftAPI') + +const OutlookDraftSchema = z.object({ + accessToken: z.string().min(1, 'Access token is required'), + to: z.string().min(1, 'Recipient email is required'), + subject: z.string().min(1, 'Subject is required'), + body: z.string().min(1, 'Email body is required'), + cc: z.string().optional().nullable(), + bcc: z.string().optional().nullable(), + attachments: z.array(z.any()).optional().nullable(), +}) + +export async function POST(request: NextRequest) { + const requestId = generateRequestId() + + try { + const authResult = await checkHybridAuth(request, { requireWorkflowId: false }) + + if (!authResult.success) { + logger.warn(`[${requestId}] Unauthorized Outlook draft attempt: ${authResult.error}`) + return NextResponse.json( + { + success: false, + error: authResult.error || 'Authentication required', + }, + { status: 401 } + ) + } + + logger.info(`[${requestId}] Authenticated Outlook draft request via ${authResult.authType}`, { + userId: authResult.userId, + }) + + const body = await request.json() + const validatedData = OutlookDraftSchema.parse(body) + + logger.info(`[${requestId}] Creating Outlook draft`, { + to: validatedData.to, + subject: validatedData.subject, + hasAttachments: !!(validatedData.attachments && validatedData.attachments.length > 0), + attachmentCount: validatedData.attachments?.length || 0, + }) + + const toRecipients = validatedData.to.split(',').map((email) => ({ + emailAddress: { address: email.trim() }, + })) + + const ccRecipients = validatedData.cc + ? validatedData.cc.split(',').map((email) => ({ + emailAddress: { address: email.trim() }, + })) + : undefined + + const bccRecipients = validatedData.bcc + ? validatedData.bcc.split(',').map((email) => ({ + emailAddress: { address: email.trim() }, + })) + : undefined + + const message: any = { + subject: validatedData.subject, + body: { + contentType: 'Text', + content: validatedData.body, + }, + toRecipients, + } + + if (ccRecipients) { + message.ccRecipients = ccRecipients + } + + if (bccRecipients) { + message.bccRecipients = bccRecipients + } + + if (validatedData.attachments && validatedData.attachments.length > 0) { + const rawAttachments = validatedData.attachments + logger.info(`[${requestId}] Processing ${rawAttachments.length} attachment(s)`) + + const attachments = processFilesToUserFiles(rawAttachments, requestId, logger) + + if (attachments.length > 0) { + const totalSize = attachments.reduce((sum, file) => sum + file.size, 0) + const maxSize = 4 * 1024 * 1024 // 4MB + + if (totalSize > maxSize) { + const sizeMB = (totalSize / (1024 * 1024)).toFixed(2) + return NextResponse.json( + { + success: false, + error: `Total attachment size (${sizeMB}MB) exceeds Outlook's limit of 4MB per request`, + }, + { status: 400 } + ) + } + + const attachmentObjects = await Promise.all( + attachments.map(async (file) => { + try { + logger.info( + `[${requestId}] Downloading attachment: ${file.name} (${file.size} bytes)` + ) + + const buffer = await downloadFileFromStorage(file, requestId, logger) + + const base64Content = buffer.toString('base64') + + return { + '@odata.type': '#microsoft.graph.fileAttachment', + name: file.name, + contentType: file.type || 'application/octet-stream', + contentBytes: base64Content, + } + } catch (error) { + logger.error(`[${requestId}] Failed to download attachment ${file.name}:`, error) + throw new Error( + `Failed to download attachment "${file.name}": ${error instanceof Error ? error.message : 'Unknown error'}` + ) + } + }) + ) + + logger.info(`[${requestId}] Converted ${attachmentObjects.length} attachments to base64`) + message.attachments = attachmentObjects + } + } + + const graphEndpoint = 'https://graph.microsoft.com/v1.0/me/messages' + + logger.info(`[${requestId}] Creating draft via Microsoft Graph API`) + + const graphResponse = await fetch(graphEndpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${validatedData.accessToken}`, + }, + body: JSON.stringify(message), + }) + + if (!graphResponse.ok) { + const errorData = await graphResponse.json().catch(() => ({})) + logger.error(`[${requestId}] Microsoft Graph API error:`, errorData) + return NextResponse.json( + { + success: false, + error: errorData.error?.message || 'Failed to create draft', + }, + { status: graphResponse.status } + ) + } + + const responseData = await graphResponse.json() + logger.info(`[${requestId}] Draft created successfully, ID: ${responseData.id}`) + + return NextResponse.json({ + success: true, + output: { + message: 'Draft created successfully', + messageId: responseData.id, + subject: responseData.subject, + attachmentCount: message.attachments?.length || 0, + }, + }) + } catch (error) { + logger.error(`[${requestId}] Error creating Outlook draft:`, error) + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Unknown error occurred', + }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/outlook/send/route.ts b/apps/sim/app/api/tools/outlook/send/route.ts new file mode 100644 index 000000000..7ff3f7bd1 --- /dev/null +++ b/apps/sim/app/api/tools/outlook/send/route.ts @@ -0,0 +1,198 @@ +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkHybridAuth } from '@/lib/auth/hybrid' +import { createLogger } from '@/lib/logs/console/logger' +import { downloadFileFromStorage, processFilesToUserFiles } from '@/lib/uploads/file-processing' +import { generateRequestId } from '@/lib/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('OutlookSendAPI') + +const OutlookSendSchema = z.object({ + accessToken: z.string().min(1, 'Access token is required'), + to: z.string().min(1, 'Recipient email is required'), + subject: z.string().min(1, 'Subject is required'), + body: z.string().min(1, 'Email body is required'), + cc: z.string().optional().nullable(), + bcc: z.string().optional().nullable(), + replyToMessageId: z.string().optional().nullable(), + conversationId: z.string().optional().nullable(), + attachments: z.array(z.any()).optional().nullable(), +}) + +export async function POST(request: NextRequest) { + const requestId = generateRequestId() + + try { + const authResult = await checkHybridAuth(request, { requireWorkflowId: false }) + + if (!authResult.success) { + logger.warn(`[${requestId}] Unauthorized Outlook send attempt: ${authResult.error}`) + return NextResponse.json( + { + success: false, + error: authResult.error || 'Authentication required', + }, + { status: 401 } + ) + } + + logger.info(`[${requestId}] Authenticated Outlook send request via ${authResult.authType}`, { + userId: authResult.userId, + }) + + const body = await request.json() + const validatedData = OutlookSendSchema.parse(body) + + logger.info(`[${requestId}] Sending Outlook email`, { + to: validatedData.to, + subject: validatedData.subject, + hasAttachments: !!(validatedData.attachments && validatedData.attachments.length > 0), + attachmentCount: validatedData.attachments?.length || 0, + }) + + const toRecipients = validatedData.to.split(',').map((email) => ({ + emailAddress: { address: email.trim() }, + })) + + const ccRecipients = validatedData.cc + ? validatedData.cc.split(',').map((email) => ({ + emailAddress: { address: email.trim() }, + })) + : undefined + + const bccRecipients = validatedData.bcc + ? validatedData.bcc.split(',').map((email) => ({ + emailAddress: { address: email.trim() }, + })) + : undefined + + const message: any = { + subject: validatedData.subject, + body: { + contentType: 'Text', + content: validatedData.body, + }, + toRecipients, + } + + if (ccRecipients) { + message.ccRecipients = ccRecipients + } + + if (bccRecipients) { + message.bccRecipients = bccRecipients + } + + if (validatedData.attachments && validatedData.attachments.length > 0) { + const rawAttachments = validatedData.attachments + logger.info(`[${requestId}] Processing ${rawAttachments.length} attachment(s)`) + + const attachments = processFilesToUserFiles(rawAttachments, requestId, logger) + + if (attachments.length > 0) { + const totalSize = attachments.reduce((sum, file) => sum + file.size, 0) + const maxSize = 4 * 1024 * 1024 // 4MB + + if (totalSize > maxSize) { + const sizeMB = (totalSize / (1024 * 1024)).toFixed(2) + return NextResponse.json( + { + success: false, + error: `Total attachment size (${sizeMB}MB) exceeds Outlook's limit of 4MB per request`, + }, + { status: 400 } + ) + } + + const attachmentObjects = await Promise.all( + attachments.map(async (file) => { + try { + logger.info( + `[${requestId}] Downloading attachment: ${file.name} (${file.size} bytes)` + ) + + const buffer = await downloadFileFromStorage(file, requestId, logger) + + const base64Content = buffer.toString('base64') + + return { + '@odata.type': '#microsoft.graph.fileAttachment', + name: file.name, + contentType: file.type || 'application/octet-stream', + contentBytes: base64Content, + } + } catch (error) { + logger.error(`[${requestId}] Failed to download attachment ${file.name}:`, error) + throw new Error( + `Failed to download attachment "${file.name}": ${error instanceof Error ? error.message : 'Unknown error'}` + ) + } + }) + ) + + logger.info(`[${requestId}] Converted ${attachmentObjects.length} attachments to base64`) + message.attachments = attachmentObjects + } + } + + const graphEndpoint = validatedData.replyToMessageId + ? `https://graph.microsoft.com/v1.0/me/messages/${validatedData.replyToMessageId}/reply` + : 'https://graph.microsoft.com/v1.0/me/sendMail' + + logger.info(`[${requestId}] Sending to Microsoft Graph API: ${graphEndpoint}`) + + const graphResponse = await fetch(graphEndpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${validatedData.accessToken}`, + }, + body: JSON.stringify( + validatedData.replyToMessageId + ? { + comment: validatedData.body, + message: message, + } + : { + message: message, + saveToSentItems: true, + } + ), + }) + + if (!graphResponse.ok) { + const errorData = await graphResponse.json().catch(() => ({})) + logger.error(`[${requestId}] Microsoft Graph API error:`, errorData) + return NextResponse.json( + { + success: false, + error: errorData.error?.message || 'Failed to send email', + }, + { status: graphResponse.status } + ) + } + + logger.info(`[${requestId}] Email sent successfully`) + + return NextResponse.json({ + success: true, + output: { + message: 'Email sent successfully', + status: 'sent', + timestamp: new Date().toISOString(), + attachmentCount: message.attachments?.length || 0, + }, + }) + } catch (error) { + logger.error(`[${requestId}] Error sending Outlook email:`, error) + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Unknown error occurred', + }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/s3/copy-object/route.ts b/apps/sim/app/api/tools/s3/copy-object/route.ts new file mode 100644 index 000000000..2c94f6d68 --- /dev/null +++ b/apps/sim/app/api/tools/s3/copy-object/route.ts @@ -0,0 +1,115 @@ +import { CopyObjectCommand, type ObjectCannedACL, S3Client } from '@aws-sdk/client-s3' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkHybridAuth } from '@/lib/auth/hybrid' +import { createLogger } from '@/lib/logs/console/logger' +import { generateRequestId } from '@/lib/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('S3CopyObjectAPI') + +const S3CopyObjectSchema = z.object({ + accessKeyId: z.string().min(1, 'Access Key ID is required'), + secretAccessKey: z.string().min(1, 'Secret Access Key is required'), + region: z.string().min(1, 'Region is required'), + sourceBucket: z.string().min(1, 'Source bucket name is required'), + sourceKey: z.string().min(1, 'Source object key is required'), + destinationBucket: z.string().min(1, 'Destination bucket name is required'), + destinationKey: z.string().min(1, 'Destination object key is required'), + acl: z.string().optional().nullable(), +}) + +export async function POST(request: NextRequest) { + const requestId = generateRequestId() + + try { + const authResult = await checkHybridAuth(request, { requireWorkflowId: false }) + + if (!authResult.success) { + logger.warn(`[${requestId}] Unauthorized S3 copy object attempt: ${authResult.error}`) + return NextResponse.json( + { + success: false, + error: authResult.error || 'Authentication required', + }, + { status: 401 } + ) + } + + logger.info(`[${requestId}] Authenticated S3 copy object request via ${authResult.authType}`, { + userId: authResult.userId, + }) + + const body = await request.json() + const validatedData = S3CopyObjectSchema.parse(body) + + logger.info(`[${requestId}] Copying S3 object`, { + source: `${validatedData.sourceBucket}/${validatedData.sourceKey}`, + destination: `${validatedData.destinationBucket}/${validatedData.destinationKey}`, + }) + + // Initialize S3 client + const s3Client = new S3Client({ + region: validatedData.region, + credentials: { + accessKeyId: validatedData.accessKeyId, + secretAccessKey: validatedData.secretAccessKey, + }, + }) + + // Copy object (properly encode the source key for CopySource parameter) + const encodedSourceKey = validatedData.sourceKey.split('/').map(encodeURIComponent).join('/') + const copySource = `${validatedData.sourceBucket}/${encodedSourceKey}` + const copyCommand = new CopyObjectCommand({ + Bucket: validatedData.destinationBucket, + Key: validatedData.destinationKey, + CopySource: copySource, + ACL: validatedData.acl as ObjectCannedACL | undefined, + }) + + const result = await s3Client.send(copyCommand) + + logger.info(`[${requestId}] Object copied successfully`, { + source: copySource, + destination: `${validatedData.destinationBucket}/${validatedData.destinationKey}`, + etag: result.CopyObjectResult?.ETag, + }) + + // Generate public URL for destination (properly encode the destination key) + const encodedDestKey = validatedData.destinationKey.split('/').map(encodeURIComponent).join('/') + const url = `https://${validatedData.destinationBucket}.s3.${validatedData.region}.amazonaws.com/${encodedDestKey}` + + return NextResponse.json({ + success: true, + output: { + url, + copySourceVersionId: result.CopySourceVersionId, + versionId: result.VersionId, + etag: result.CopyObjectResult?.ETag, + }, + }) + } catch (error) { + if (error instanceof z.ZodError) { + logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors }) + return NextResponse.json( + { + success: false, + error: 'Invalid request data', + details: error.errors, + }, + { status: 400 } + ) + } + + logger.error(`[${requestId}] Error copying S3 object:`, error) + + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Internal server error', + }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/s3/delete-object/route.ts b/apps/sim/app/api/tools/s3/delete-object/route.ts new file mode 100644 index 000000000..8b9879115 --- /dev/null +++ b/apps/sim/app/api/tools/s3/delete-object/route.ts @@ -0,0 +1,106 @@ +import { DeleteObjectCommand, S3Client } from '@aws-sdk/client-s3' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkHybridAuth } from '@/lib/auth/hybrid' +import { createLogger } from '@/lib/logs/console/logger' +import { generateRequestId } from '@/lib/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('S3DeleteObjectAPI') + +const S3DeleteObjectSchema = z.object({ + accessKeyId: z.string().min(1, 'Access Key ID is required'), + secretAccessKey: z.string().min(1, 'Secret Access Key is required'), + region: z.string().min(1, 'Region is required'), + bucketName: z.string().min(1, 'Bucket name is required'), + objectKey: z.string().min(1, 'Object key is required'), +}) + +export async function POST(request: NextRequest) { + const requestId = generateRequestId() + + try { + const authResult = await checkHybridAuth(request, { requireWorkflowId: false }) + + if (!authResult.success) { + logger.warn(`[${requestId}] Unauthorized S3 delete object attempt: ${authResult.error}`) + return NextResponse.json( + { + success: false, + error: authResult.error || 'Authentication required', + }, + { status: 401 } + ) + } + + logger.info( + `[${requestId}] Authenticated S3 delete object request via ${authResult.authType}`, + { + userId: authResult.userId, + } + ) + + const body = await request.json() + const validatedData = S3DeleteObjectSchema.parse(body) + + logger.info(`[${requestId}] Deleting S3 object`, { + bucket: validatedData.bucketName, + key: validatedData.objectKey, + }) + + // Initialize S3 client + const s3Client = new S3Client({ + region: validatedData.region, + credentials: { + accessKeyId: validatedData.accessKeyId, + secretAccessKey: validatedData.secretAccessKey, + }, + }) + + // Delete object + const deleteCommand = new DeleteObjectCommand({ + Bucket: validatedData.bucketName, + Key: validatedData.objectKey, + }) + + const result = await s3Client.send(deleteCommand) + + logger.info(`[${requestId}] Object deleted successfully`, { + bucket: validatedData.bucketName, + key: validatedData.objectKey, + deleteMarker: result.DeleteMarker, + }) + + return NextResponse.json({ + success: true, + output: { + key: validatedData.objectKey, + deleteMarker: result.DeleteMarker, + versionId: result.VersionId, + }, + }) + } catch (error) { + if (error instanceof z.ZodError) { + logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors }) + return NextResponse.json( + { + success: false, + error: 'Invalid request data', + details: error.errors, + }, + { status: 400 } + ) + } + + logger.error(`[${requestId}] Error deleting S3 object:`, error) + + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Internal server error', + }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/s3/list-objects/route.ts b/apps/sim/app/api/tools/s3/list-objects/route.ts new file mode 100644 index 000000000..d4c93185e --- /dev/null +++ b/apps/sim/app/api/tools/s3/list-objects/route.ts @@ -0,0 +1,116 @@ +import { ListObjectsV2Command, S3Client } from '@aws-sdk/client-s3' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkHybridAuth } from '@/lib/auth/hybrid' +import { createLogger } from '@/lib/logs/console/logger' +import { generateRequestId } from '@/lib/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('S3ListObjectsAPI') + +const S3ListObjectsSchema = z.object({ + accessKeyId: z.string().min(1, 'Access Key ID is required'), + secretAccessKey: z.string().min(1, 'Secret Access Key is required'), + region: z.string().min(1, 'Region is required'), + bucketName: z.string().min(1, 'Bucket name is required'), + prefix: z.string().optional().nullable(), + maxKeys: z.number().optional().nullable(), + continuationToken: z.string().optional().nullable(), +}) + +export async function POST(request: NextRequest) { + const requestId = generateRequestId() + + try { + const authResult = await checkHybridAuth(request, { requireWorkflowId: false }) + + if (!authResult.success) { + logger.warn(`[${requestId}] Unauthorized S3 list objects attempt: ${authResult.error}`) + return NextResponse.json( + { + success: false, + error: authResult.error || 'Authentication required', + }, + { status: 401 } + ) + } + + logger.info(`[${requestId}] Authenticated S3 list objects request via ${authResult.authType}`, { + userId: authResult.userId, + }) + + const body = await request.json() + const validatedData = S3ListObjectsSchema.parse(body) + + logger.info(`[${requestId}] Listing S3 objects`, { + bucket: validatedData.bucketName, + prefix: validatedData.prefix || '(none)', + maxKeys: validatedData.maxKeys || 1000, + }) + + // Initialize S3 client + const s3Client = new S3Client({ + region: validatedData.region, + credentials: { + accessKeyId: validatedData.accessKeyId, + secretAccessKey: validatedData.secretAccessKey, + }, + }) + + // List objects + const listCommand = new ListObjectsV2Command({ + Bucket: validatedData.bucketName, + Prefix: validatedData.prefix || undefined, + MaxKeys: validatedData.maxKeys || undefined, + ContinuationToken: validatedData.continuationToken || undefined, + }) + + const result = await s3Client.send(listCommand) + + const objects = (result.Contents || []).map((obj) => ({ + key: obj.Key || '', + size: obj.Size || 0, + lastModified: obj.LastModified?.toISOString() || '', + etag: obj.ETag || '', + })) + + logger.info(`[${requestId}] Listed ${objects.length} objects`, { + bucket: validatedData.bucketName, + isTruncated: result.IsTruncated, + }) + + return NextResponse.json({ + success: true, + output: { + objects, + isTruncated: result.IsTruncated, + nextContinuationToken: result.NextContinuationToken, + keyCount: result.KeyCount, + prefix: validatedData.prefix, + }, + }) + } catch (error) { + if (error instanceof z.ZodError) { + logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors }) + return NextResponse.json( + { + success: false, + error: 'Invalid request data', + details: error.errors, + }, + { status: 400 } + ) + } + + logger.error(`[${requestId}] Error listing S3 objects:`, error) + + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Internal server error', + }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/s3/put-object/route.ts b/apps/sim/app/api/tools/s3/put-object/route.ts new file mode 100644 index 000000000..c08dc0f3a --- /dev/null +++ b/apps/sim/app/api/tools/s3/put-object/route.ts @@ -0,0 +1,153 @@ +import { type ObjectCannedACL, PutObjectCommand, S3Client } from '@aws-sdk/client-s3' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkHybridAuth } from '@/lib/auth/hybrid' +import { createLogger } from '@/lib/logs/console/logger' +import { downloadFileFromStorage, processSingleFileToUserFile } from '@/lib/uploads/file-processing' +import { generateRequestId } from '@/lib/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('S3PutObjectAPI') + +const S3PutObjectSchema = z.object({ + accessKeyId: z.string().min(1, 'Access Key ID is required'), + secretAccessKey: z.string().min(1, 'Secret Access Key is required'), + region: z.string().min(1, 'Region is required'), + bucketName: z.string().min(1, 'Bucket name is required'), + objectKey: z.string().min(1, 'Object key is required'), + file: z.any().optional().nullable(), + content: z.string().optional().nullable(), + contentType: z.string().optional().nullable(), + acl: z.string().optional().nullable(), +}) + +export async function POST(request: NextRequest) { + const requestId = generateRequestId() + + try { + const authResult = await checkHybridAuth(request, { requireWorkflowId: false }) + + if (!authResult.success) { + logger.warn(`[${requestId}] Unauthorized S3 put object attempt: ${authResult.error}`) + return NextResponse.json( + { + success: false, + error: authResult.error || 'Authentication required', + }, + { status: 401 } + ) + } + + logger.info(`[${requestId}] Authenticated S3 put object request via ${authResult.authType}`, { + userId: authResult.userId, + }) + + const body = await request.json() + const validatedData = S3PutObjectSchema.parse(body) + + logger.info(`[${requestId}] Uploading to S3`, { + bucket: validatedData.bucketName, + key: validatedData.objectKey, + hasFile: !!validatedData.file, + hasContent: !!validatedData.content, + }) + + const s3Client = new S3Client({ + region: validatedData.region, + credentials: { + accessKeyId: validatedData.accessKeyId, + secretAccessKey: validatedData.secretAccessKey, + }, + }) + + let uploadBody: Buffer | string + let uploadContentType: string | undefined + + if (validatedData.file) { + const rawFile = validatedData.file + logger.info(`[${requestId}] Processing file upload: ${rawFile.name}`) + + let userFile + try { + userFile = processSingleFileToUserFile(rawFile, requestId, logger) + } catch (error) { + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Failed to process file', + }, + { status: 400 } + ) + } + + const buffer = await downloadFileFromStorage(userFile, requestId, logger) + + uploadBody = buffer + uploadContentType = validatedData.contentType || userFile.type || 'application/octet-stream' + } else if (validatedData.content) { + uploadBody = Buffer.from(validatedData.content, 'utf-8') + uploadContentType = validatedData.contentType || 'text/plain' + } else { + return NextResponse.json( + { + success: false, + error: 'Either file or content must be provided', + }, + { status: 400 } + ) + } + + const putCommand = new PutObjectCommand({ + Bucket: validatedData.bucketName, + Key: validatedData.objectKey, + Body: uploadBody, + ContentType: uploadContentType, + ACL: validatedData.acl as ObjectCannedACL | undefined, + }) + + const result = await s3Client.send(putCommand) + + logger.info(`[${requestId}] File uploaded successfully`, { + etag: result.ETag, + bucket: validatedData.bucketName, + key: validatedData.objectKey, + }) + + const encodedKey = validatedData.objectKey.split('/').map(encodeURIComponent).join('/') + const url = `https://${validatedData.bucketName}.s3.${validatedData.region}.amazonaws.com/${encodedKey}` + + return NextResponse.json({ + success: true, + output: { + url, + etag: result.ETag, + location: url, + key: validatedData.objectKey, + bucket: validatedData.bucketName, + }, + }) + } catch (error) { + if (error instanceof z.ZodError) { + logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors }) + return NextResponse.json( + { + success: false, + error: 'Invalid request data', + details: error.errors, + }, + { status: 400 } + ) + } + + logger.error(`[${requestId}] Error uploading to S3:`, error) + + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Internal server error', + }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/sharepoint/upload/route.ts b/apps/sim/app/api/tools/sharepoint/upload/route.ts new file mode 100644 index 000000000..796877349 --- /dev/null +++ b/apps/sim/app/api/tools/sharepoint/upload/route.ts @@ -0,0 +1,216 @@ +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkHybridAuth } from '@/lib/auth/hybrid' +import { createLogger } from '@/lib/logs/console/logger' +import { downloadFileFromStorage, processFilesToUserFiles } from '@/lib/uploads/file-processing' +import { generateRequestId } from '@/lib/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('SharepointUploadAPI') + +const SharepointUploadSchema = z.object({ + accessToken: z.string().min(1, 'Access token is required'), + siteId: z.string().default('root'), + driveId: z.string().optional().nullable(), + folderPath: z.string().optional().nullable(), + fileName: z.string().optional().nullable(), + files: z.array(z.any()).optional().nullable(), +}) + +export async function POST(request: NextRequest) { + const requestId = generateRequestId() + + try { + const authResult = await checkHybridAuth(request, { requireWorkflowId: false }) + + if (!authResult.success) { + logger.warn(`[${requestId}] Unauthorized SharePoint upload attempt: ${authResult.error}`) + return NextResponse.json( + { + success: false, + error: authResult.error || 'Authentication required', + }, + { status: 401 } + ) + } + + logger.info( + `[${requestId}] Authenticated SharePoint upload request via ${authResult.authType}`, + { + userId: authResult.userId, + } + ) + + const body = await request.json() + const validatedData = SharepointUploadSchema.parse(body) + + logger.info(`[${requestId}] Uploading files to SharePoint`, { + siteId: validatedData.siteId, + driveId: validatedData.driveId, + folderPath: validatedData.folderPath, + hasFiles: !!(validatedData.files && validatedData.files.length > 0), + fileCount: validatedData.files?.length || 0, + }) + + if (!validatedData.files || validatedData.files.length === 0) { + return NextResponse.json( + { + success: false, + error: 'At least one file is required for upload', + }, + { status: 400 } + ) + } + + const userFiles = processFilesToUserFiles(validatedData.files, requestId, logger) + + if (userFiles.length === 0) { + return NextResponse.json( + { + success: false, + error: 'No valid files to upload', + }, + { status: 400 } + ) + } + + let effectiveDriveId = validatedData.driveId + if (!effectiveDriveId) { + logger.info(`[${requestId}] No driveId provided, fetching default drive for site`) + const driveResponse = await fetch( + `https://graph.microsoft.com/v1.0/sites/${validatedData.siteId}/drive`, + { + headers: { + Authorization: `Bearer ${validatedData.accessToken}`, + Accept: 'application/json', + }, + } + ) + + if (!driveResponse.ok) { + const errorData = await driveResponse.json().catch(() => ({})) + logger.error(`[${requestId}] Failed to get default drive:`, errorData) + return NextResponse.json( + { + success: false, + error: errorData.error?.message || 'Failed to get default document library', + }, + { status: driveResponse.status } + ) + } + + const driveData = await driveResponse.json() + effectiveDriveId = driveData.id + logger.info(`[${requestId}] Using default drive: ${effectiveDriveId}`) + } + + const uploadedFiles: any[] = [] + + for (const userFile of userFiles) { + logger.info(`[${requestId}] Uploading file: ${userFile.name}`) + + const buffer = await downloadFileFromStorage(userFile, requestId, logger) + + const fileName = validatedData.fileName || userFile.name + const folderPath = validatedData.folderPath?.trim() || '' + + const fileSizeMB = buffer.length / (1024 * 1024) + + if (fileSizeMB > 250) { + logger.warn( + `[${requestId}] File ${fileName} is ${fileSizeMB.toFixed(2)}MB, exceeds 250MB limit` + ) + continue + } + + let uploadPath = '' + if (folderPath) { + const normalizedPath = folderPath.startsWith('/') ? folderPath : `/${folderPath}` + const cleanPath = normalizedPath.endsWith('/') + ? normalizedPath.slice(0, -1) + : normalizedPath + uploadPath = `${cleanPath}/${fileName}` + } else { + uploadPath = `/${fileName}` + } + + const encodedPath = uploadPath + .split('/') + .map((segment) => (segment ? encodeURIComponent(segment) : '')) + .join('/') + + const uploadUrl = `https://graph.microsoft.com/v1.0/sites/${validatedData.siteId}/drives/${effectiveDriveId}/root:${encodedPath}:/content` + + logger.info(`[${requestId}] Uploading to: ${uploadUrl}`) + + const uploadResponse = await fetch(uploadUrl, { + method: 'PUT', + headers: { + Authorization: `Bearer ${validatedData.accessToken}`, + 'Content-Type': userFile.type || 'application/octet-stream', + }, + body: new Uint8Array(buffer), + }) + + if (!uploadResponse.ok) { + const errorData = await uploadResponse.json().catch(() => ({})) + logger.error(`[${requestId}] Failed to upload file ${fileName}:`, errorData) + + if (uploadResponse.status === 409) { + logger.warn(`[${requestId}] File ${fileName} already exists, attempting to replace`) + continue + } + + return NextResponse.json( + { + success: false, + error: errorData.error?.message || `Failed to upload file: ${fileName}`, + }, + { status: uploadResponse.status } + ) + } + + const uploadData = await uploadResponse.json() + logger.info(`[${requestId}] File uploaded successfully: ${fileName}`) + + uploadedFiles.push({ + id: uploadData.id, + name: uploadData.name, + webUrl: uploadData.webUrl, + size: uploadData.size, + createdDateTime: uploadData.createdDateTime, + lastModifiedDateTime: uploadData.lastModifiedDateTime, + }) + } + + if (uploadedFiles.length === 0) { + return NextResponse.json( + { + success: false, + error: 'No files were uploaded successfully', + }, + { status: 400 } + ) + } + + logger.info(`[${requestId}] Successfully uploaded ${uploadedFiles.length} file(s)`) + + return NextResponse.json({ + success: true, + output: { + uploadedFiles, + fileCount: uploadedFiles.length, + }, + }) + } catch (error) { + logger.error(`[${requestId}] Error uploading files to SharePoint:`, error) + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Unknown error occurred', + }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/slack/send-message/route.ts b/apps/sim/app/api/tools/slack/send-message/route.ts new file mode 100644 index 000000000..c7b213e4f --- /dev/null +++ b/apps/sim/app/api/tools/slack/send-message/route.ts @@ -0,0 +1,227 @@ +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkHybridAuth } from '@/lib/auth/hybrid' +import { createLogger } from '@/lib/logs/console/logger' +import { downloadFileFromStorage, processFilesToUserFiles } from '@/lib/uploads/file-processing' +import { generateRequestId } from '@/lib/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('SlackSendMessageAPI') + +const SlackSendMessageSchema = z.object({ + accessToken: z.string().min(1, 'Access token is required'), + channel: z.string().min(1, 'Channel is required'), + text: z.string().min(1, 'Message text is required'), + files: z.array(z.any()).optional().nullable(), +}) + +export async function POST(request: NextRequest) { + const requestId = generateRequestId() + + try { + const authResult = await checkHybridAuth(request, { requireWorkflowId: false }) + + if (!authResult.success) { + logger.warn(`[${requestId}] Unauthorized Slack send attempt: ${authResult.error}`) + return NextResponse.json( + { + success: false, + error: authResult.error || 'Authentication required', + }, + { status: 401 } + ) + } + + logger.info(`[${requestId}] Authenticated Slack send request via ${authResult.authType}`, { + userId: authResult.userId, + }) + + const body = await request.json() + const validatedData = SlackSendMessageSchema.parse(body) + + logger.info(`[${requestId}] Sending Slack message`, { + channel: validatedData.channel, + hasFiles: !!(validatedData.files && validatedData.files.length > 0), + fileCount: validatedData.files?.length || 0, + }) + + if (!validatedData.files || validatedData.files.length === 0) { + logger.info(`[${requestId}] No files, using chat.postMessage`) + + const response = await fetch('https://slack.com/api/chat.postMessage', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${validatedData.accessToken}`, + }, + body: JSON.stringify({ + channel: validatedData.channel, + text: validatedData.text, + }), + }) + + const data = await response.json() + + if (!data.ok) { + logger.error(`[${requestId}] Slack API error:`, data.error) + return NextResponse.json( + { + success: false, + error: data.error || 'Failed to send message', + }, + { status: 400 } + ) + } + + logger.info(`[${requestId}] Message sent successfully`) + return NextResponse.json({ + success: true, + output: { + ts: data.ts, + channel: data.channel, + }, + }) + } + + logger.info(`[${requestId}] Processing ${validatedData.files.length} file(s)`) + + const userFiles = processFilesToUserFiles(validatedData.files, requestId, logger) + + if (userFiles.length === 0) { + logger.warn(`[${requestId}] No valid files to upload`) + const response = await fetch('https://slack.com/api/chat.postMessage', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${validatedData.accessToken}`, + }, + body: JSON.stringify({ + channel: validatedData.channel, + text: validatedData.text, + }), + }) + + const data = await response.json() + return NextResponse.json({ + success: true, + output: { + ts: data.ts, + channel: data.channel, + }, + }) + } + + const uploadedFileIds: string[] = [] + + for (const userFile of userFiles) { + logger.info(`[${requestId}] Uploading file: ${userFile.name}`) + + const buffer = await downloadFileFromStorage(userFile, requestId, logger) + + const getUrlResponse = await fetch('https://slack.com/api/files.getUploadURLExternal', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: `Bearer ${validatedData.accessToken}`, + }, + body: new URLSearchParams({ + filename: userFile.name, + length: buffer.length.toString(), + }), + }) + + const urlData = await getUrlResponse.json() + + if (!urlData.ok) { + logger.error(`[${requestId}] Failed to get upload URL:`, urlData.error) + continue + } + + logger.info(`[${requestId}] Got upload URL for ${userFile.name}, file_id: ${urlData.file_id}`) + + const uploadResponse = await fetch(urlData.upload_url, { + method: 'POST', + body: new Uint8Array(buffer), + }) + + if (!uploadResponse.ok) { + logger.error(`[${requestId}] Failed to upload file data: ${uploadResponse.status}`) + continue + } + + logger.info(`[${requestId}] File data uploaded successfully`) + uploadedFileIds.push(urlData.file_id) + } + + if (uploadedFileIds.length === 0) { + logger.warn(`[${requestId}] No files uploaded successfully, sending text-only message`) + const response = await fetch('https://slack.com/api/chat.postMessage', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${validatedData.accessToken}`, + }, + body: JSON.stringify({ + channel: validatedData.channel, + text: validatedData.text, + }), + }) + + const data = await response.json() + return NextResponse.json({ + success: true, + output: { + ts: data.ts, + channel: data.channel, + }, + }) + } + + const completeResponse = await fetch('https://slack.com/api/files.completeUploadExternal', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${validatedData.accessToken}`, + }, + body: JSON.stringify({ + files: uploadedFileIds.map((id) => ({ id })), + channel_id: validatedData.channel, + initial_comment: validatedData.text, + }), + }) + + const completeData = await completeResponse.json() + + if (!completeData.ok) { + logger.error(`[${requestId}] Failed to complete upload:`, completeData.error) + return NextResponse.json( + { + success: false, + error: completeData.error || 'Failed to complete file upload', + }, + { status: 400 } + ) + } + + logger.info(`[${requestId}] Files uploaded and shared successfully`) + + return NextResponse.json({ + success: true, + output: { + ts: completeData.files?.[0]?.created || Date.now() / 1000, + channel: validatedData.channel, + fileCount: uploadedFileIds.length, + }, + }) + } catch (error) { + logger.error(`[${requestId}] Error sending Slack message:`, error) + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Unknown error occurred', + }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/telegram/send-document/route.ts b/apps/sim/app/api/tools/telegram/send-document/route.ts new file mode 100644 index 000000000..38a93bca0 --- /dev/null +++ b/apps/sim/app/api/tools/telegram/send-document/route.ts @@ -0,0 +1,149 @@ +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkHybridAuth } from '@/lib/auth/hybrid' +import { createLogger } from '@/lib/logs/console/logger' +import { downloadFileFromStorage, processFilesToUserFiles } from '@/lib/uploads/file-processing' +import { generateRequestId } from '@/lib/utils' +import { convertMarkdownToHTML } from '@/tools/telegram/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('TelegramSendDocumentAPI') + +const TelegramSendDocumentSchema = z.object({ + botToken: z.string().min(1, 'Bot token is required'), + chatId: z.string().min(1, 'Chat ID is required'), + files: z.array(z.any()).optional().nullable(), + caption: z.string().optional().nullable(), +}) + +export async function POST(request: NextRequest) { + const requestId = generateRequestId() + + try { + const authResult = await checkHybridAuth(request, { + requireWorkflowId: false, + }) + + if (!authResult.success) { + logger.warn(`[${requestId}] Unauthorized Telegram send attempt: ${authResult.error}`) + return NextResponse.json( + { + success: false, + error: authResult.error || 'Authentication required', + }, + { status: 401 } + ) + } + + logger.info(`[${requestId}] Authenticated Telegram send request via ${authResult.authType}`, { + userId: authResult.userId, + }) + + const body = await request.json() + const validatedData = TelegramSendDocumentSchema.parse(body) + + logger.info(`[${requestId}] Sending Telegram document`, { + chatId: validatedData.chatId, + hasFiles: !!(validatedData.files && validatedData.files.length > 0), + fileCount: validatedData.files?.length || 0, + }) + + if (!validatedData.files || validatedData.files.length === 0) { + return NextResponse.json( + { + success: false, + error: 'At least one document file is required for sendDocument operation', + }, + { status: 400 } + ) + } + + const userFiles = processFilesToUserFiles(validatedData.files, requestId, logger) + + if (userFiles.length === 0) { + logger.warn(`[${requestId}] No valid files to upload`) + return NextResponse.json( + { + success: false, + error: 'No valid files provided for upload', + }, + { status: 400 } + ) + } + + const maxSize = 50 * 1024 * 1024 // 50MB + const tooLargeFiles = userFiles.filter((file) => file.size > maxSize) + + if (tooLargeFiles.length > 0) { + const filesInfo = tooLargeFiles + .map((f) => `${f.name} (${(f.size / (1024 * 1024)).toFixed(2)}MB)`) + .join(', ') + return NextResponse.json( + { + success: false, + error: `The following files exceed Telegram's 50MB limit: ${filesInfo}`, + }, + { status: 400 } + ) + } + + const userFile = userFiles[0] + logger.info(`[${requestId}] Uploading document: ${userFile.name}`) + + const buffer = await downloadFileFromStorage(userFile, requestId, logger) + + logger.info(`[${requestId}] Downloaded file: ${buffer.length} bytes`) + + const formData = new FormData() + formData.append('chat_id', validatedData.chatId) + + const blob = new Blob([new Uint8Array(buffer)], { type: userFile.type }) + formData.append('document', blob, userFile.name) + + if (validatedData.caption) { + formData.append('caption', convertMarkdownToHTML(validatedData.caption)) + formData.append('parse_mode', 'HTML') + } + + const telegramApiUrl = `https://api.telegram.org/bot${validatedData.botToken}/sendDocument` + logger.info(`[${requestId}] Sending request to Telegram API`) + + const response = await fetch(telegramApiUrl, { + method: 'POST', + body: formData, + }) + + const data = await response.json() + + if (!data.ok) { + logger.error(`[${requestId}] Telegram API error:`, data) + return NextResponse.json( + { + success: false, + error: data.description || 'Failed to send document to Telegram', + }, + { status: response.status } + ) + } + + logger.info(`[${requestId}] Document sent successfully`) + + return NextResponse.json({ + success: true, + output: { + message: 'Document sent successfully', + data: data.result, + }, + }) + } catch (error) { + logger.error(`[${requestId}] Error sending Telegram document:`, error) + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Unknown error occurred', + }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/vision/analyze/route.ts b/apps/sim/app/api/tools/vision/analyze/route.ts new file mode 100644 index 000000000..18a93e9e2 --- /dev/null +++ b/apps/sim/app/api/tools/vision/analyze/route.ts @@ -0,0 +1,231 @@ +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkHybridAuth } from '@/lib/auth/hybrid' +import { createLogger } from '@/lib/logs/console/logger' +import { downloadFileFromStorage, processSingleFileToUserFile } from '@/lib/uploads/file-processing' +import { generateRequestId } from '@/lib/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('VisionAnalyzeAPI') + +const VisionAnalyzeSchema = z.object({ + apiKey: z.string().min(1, 'API key is required'), + imageUrl: z.string().optional().nullable(), + imageFile: z.any().optional().nullable(), + model: z.string().optional().default('gpt-4o'), + prompt: z.string().optional().nullable(), +}) + +export async function POST(request: NextRequest) { + const requestId = generateRequestId() + + try { + const authResult = await checkHybridAuth(request, { requireWorkflowId: false }) + + if (!authResult.success) { + logger.warn(`[${requestId}] Unauthorized Vision analyze attempt: ${authResult.error}`) + return NextResponse.json( + { + success: false, + error: authResult.error || 'Authentication required', + }, + { status: 401 } + ) + } + + logger.info(`[${requestId}] Authenticated Vision analyze request via ${authResult.authType}`, { + userId: authResult.userId, + }) + + const body = await request.json() + const validatedData = VisionAnalyzeSchema.parse(body) + + if (!validatedData.imageUrl && !validatedData.imageFile) { + return NextResponse.json( + { + success: false, + error: 'Either imageUrl or imageFile is required', + }, + { status: 400 } + ) + } + + logger.info(`[${requestId}] Analyzing image`, { + hasFile: !!validatedData.imageFile, + hasUrl: !!validatedData.imageUrl, + model: validatedData.model, + }) + + let imageSource: string = validatedData.imageUrl || '' + + if (validatedData.imageFile) { + const rawFile = validatedData.imageFile + logger.info(`[${requestId}] Processing image file: ${rawFile.name}`) + + let userFile + try { + userFile = processSingleFileToUserFile(rawFile, requestId, logger) + } catch (error) { + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Failed to process image file', + }, + { status: 400 } + ) + } + + const buffer = await downloadFileFromStorage(userFile, requestId, logger) + + const base64 = buffer.toString('base64') + const mimeType = userFile.type || 'image/jpeg' + imageSource = `data:${mimeType};base64,${base64}` + logger.info(`[${requestId}] Converted image to base64 (${buffer.length} bytes)`) + } + + const defaultPrompt = 'Please analyze this image and describe what you see in detail.' + const prompt = validatedData.prompt || defaultPrompt + + const isClaude = validatedData.model.startsWith('claude-3') + const apiUrl = isClaude + ? 'https://api.anthropic.com/v1/messages' + : 'https://api.openai.com/v1/chat/completions' + + const headers: Record = { + 'Content-Type': 'application/json', + } + + if (isClaude) { + headers['x-api-key'] = validatedData.apiKey + headers['anthropic-version'] = '2023-06-01' + } else { + headers.Authorization = `Bearer ${validatedData.apiKey}` + } + + let requestBody: any + + if (isClaude) { + if (imageSource.startsWith('data:')) { + const base64Match = imageSource.match(/^data:([^;]+);base64,(.+)$/) + if (!base64Match) { + return NextResponse.json( + { success: false, error: 'Invalid base64 image format' }, + { status: 400 } + ) + } + const [, mediaType, base64Data] = base64Match + + requestBody = { + model: validatedData.model, + max_tokens: 1024, + messages: [ + { + role: 'user', + content: [ + { type: 'text', text: prompt }, + { + type: 'image', + source: { + type: 'base64', + media_type: mediaType, + data: base64Data, + }, + }, + ], + }, + ], + } + } else { + requestBody = { + model: validatedData.model, + max_tokens: 1024, + messages: [ + { + role: 'user', + content: [ + { type: 'text', text: prompt }, + { + type: 'image', + source: { type: 'url', url: imageSource }, + }, + ], + }, + ], + } + } + } else { + requestBody = { + model: validatedData.model, + messages: [ + { + role: 'user', + content: [ + { type: 'text', text: prompt }, + { + type: 'image_url', + image_url: { + url: imageSource, + }, + }, + ], + }, + ], + max_tokens: 1000, + } + } + + logger.info(`[${requestId}] Sending request to ${isClaude ? 'Anthropic' : 'OpenAI'} API`) + const response = await fetch(apiUrl, { + method: 'POST', + headers, + body: JSON.stringify(requestBody), + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + logger.error(`[${requestId}] Vision API error:`, errorData) + return NextResponse.json( + { + success: false, + error: errorData.error?.message || errorData.message || 'Failed to analyze image', + }, + { status: response.status } + ) + } + + const data = await response.json() + const result = data.content?.[0]?.text || data.choices?.[0]?.message?.content + + logger.info(`[${requestId}] Image analyzed successfully`) + + return NextResponse.json({ + success: true, + output: { + content: result, + model: data.model, + tokens: data.content + ? (data.usage?.input_tokens || 0) + (data.usage?.output_tokens || 0) + : data.usage?.total_tokens, + usage: data.usage + ? { + input_tokens: data.usage.input_tokens, + output_tokens: data.usage.output_tokens, + total_tokens: + data.usage.total_tokens || + (data.usage.input_tokens || 0) + (data.usage.output_tokens || 0), + } + : undefined, + }, + }) + } catch (error) { + logger.error(`[${requestId}] Error analyzing image:`, error) + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Unknown error occurred', + }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/workspaces/[id]/files/[fileId]/download/route.ts b/apps/sim/app/api/workspaces/[id]/files/[fileId]/download/route.ts new file mode 100644 index 000000000..df94528c1 --- /dev/null +++ b/apps/sim/app/api/workspaces/[id]/files/[fileId]/download/route.ts @@ -0,0 +1,91 @@ +import { type NextRequest, NextResponse } from 'next/server' +import { getSession } from '@/lib/auth' +import { createLogger } from '@/lib/logs/console/logger' +import { getPresignedUrlWithConfig, USE_BLOB_STORAGE, USE_S3_STORAGE } from '@/lib/uploads' +import { BLOB_CONFIG, S3_CONFIG } from '@/lib/uploads/setup' +import { getWorkspaceFile } from '@/lib/uploads/workspace-files' +import { generateRequestId } from '@/lib/utils' +import { verifyWorkspaceMembership } from '@/app/api/workflows/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('WorkspaceFileDownloadAPI') + +/** + * POST /api/workspaces/[id]/files/[fileId]/download + * Generate presigned download URL (requires read permission) + * Reuses execution file helper pattern for 5-minute presigned URLs + */ +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ id: string; fileId: string }> } +) { + const requestId = generateRequestId() + const { id: workspaceId, fileId } = await params + + try { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + // Check workspace permissions (requires read) + const userPermission = await verifyWorkspaceMembership(session.user.id, workspaceId) + if (!userPermission) { + logger.warn( + `[${requestId}] User ${session.user.id} lacks permission for workspace ${workspaceId}` + ) + return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }) + } + + const fileRecord = await getWorkspaceFile(workspaceId, fileId) + if (!fileRecord) { + return NextResponse.json({ error: 'File not found' }, { status: 404 }) + } + + // Generate 5-minute presigned URL (same pattern as execution files) + let downloadUrl: string + + if (USE_S3_STORAGE) { + downloadUrl = await getPresignedUrlWithConfig( + fileRecord.key, + { + bucket: S3_CONFIG.bucket, + region: S3_CONFIG.region, + }, + 5 * 60 // 5 minutes + ) + } else if (USE_BLOB_STORAGE) { + downloadUrl = await getPresignedUrlWithConfig( + fileRecord.key, + { + accountName: BLOB_CONFIG.accountName, + accountKey: BLOB_CONFIG.accountKey, + connectionString: BLOB_CONFIG.connectionString, + containerName: BLOB_CONFIG.containerName, + }, + 5 * 60 // 5 minutes + ) + } else { + throw new Error('No cloud storage configured') + } + + logger.info(`[${requestId}] Generated download URL for workspace file: ${fileRecord.name}`) + + return NextResponse.json({ + success: true, + downloadUrl, + fileName: fileRecord.name, + expiresIn: 300, // 5 minutes + }) + } catch (error) { + logger.error(`[${requestId}] Error generating download URL:`, error) + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Failed to generate download URL', + }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/workspaces/[id]/files/[fileId]/route.ts b/apps/sim/app/api/workspaces/[id]/files/[fileId]/route.ts new file mode 100644 index 000000000..be0e3e71e --- /dev/null +++ b/apps/sim/app/api/workspaces/[id]/files/[fileId]/route.ts @@ -0,0 +1,55 @@ +import { type NextRequest, NextResponse } from 'next/server' +import { getSession } from '@/lib/auth' +import { createLogger } from '@/lib/logs/console/logger' +import { getUserEntityPermissions } from '@/lib/permissions/utils' +import { deleteWorkspaceFile } from '@/lib/uploads/workspace-files' +import { generateRequestId } from '@/lib/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('WorkspaceFileAPI') + +/** + * DELETE /api/workspaces/[id]/files/[fileId] + * Delete a workspace file (requires write permission) + */ +export async function DELETE( + request: NextRequest, + { params }: { params: Promise<{ id: string; fileId: string }> } +) { + const requestId = generateRequestId() + const { id: workspaceId, fileId } = await params + + try { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + // Check workspace permissions (requires write) + const userPermission = await getUserEntityPermissions(session.user.id, 'workspace', workspaceId) + if (userPermission !== 'admin' && userPermission !== 'write') { + logger.warn( + `[${requestId}] User ${session.user.id} lacks write permission for workspace ${workspaceId}` + ) + return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }) + } + + await deleteWorkspaceFile(workspaceId, fileId) + + logger.info(`[${requestId}] Deleted workspace file: ${fileId}`) + + return NextResponse.json({ + success: true, + }) + } catch (error) { + logger.error(`[${requestId}] Error deleting workspace file:`, error) + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Failed to delete file', + }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/workspaces/[id]/files/route.ts b/apps/sim/app/api/workspaces/[id]/files/route.ts new file mode 100644 index 000000000..ba8d6448c --- /dev/null +++ b/apps/sim/app/api/workspaces/[id]/files/route.ts @@ -0,0 +1,127 @@ +import { type NextRequest, NextResponse } from 'next/server' +import { getSession } from '@/lib/auth' +import { createLogger } from '@/lib/logs/console/logger' +import { getUserEntityPermissions } from '@/lib/permissions/utils' +import { listWorkspaceFiles, uploadWorkspaceFile } from '@/lib/uploads/workspace-files' +import { generateRequestId } from '@/lib/utils' +import { verifyWorkspaceMembership } from '@/app/api/workflows/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('WorkspaceFilesAPI') + +/** + * GET /api/workspaces/[id]/files + * List all files for a workspace (requires read permission) + */ +export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { + const requestId = generateRequestId() + const { id: workspaceId } = await params + + try { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + // Check workspace permissions (requires read) + const userPermission = await verifyWorkspaceMembership(session.user.id, workspaceId) + if (!userPermission) { + logger.warn( + `[${requestId}] User ${session.user.id} lacks permission for workspace ${workspaceId}` + ) + return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }) + } + + const files = await listWorkspaceFiles(workspaceId) + + logger.info(`[${requestId}] Listed ${files.length} files for workspace ${workspaceId}`) + + return NextResponse.json({ + success: true, + files, + }) + } catch (error) { + logger.error(`[${requestId}] Error listing workspace files:`, error) + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Failed to list files', + }, + { status: 500 } + ) + } +} + +/** + * POST /api/workspaces/[id]/files + * Upload a new file to workspace storage (requires write permission) + */ +export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { + const requestId = generateRequestId() + const { id: workspaceId } = await params + + try { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + // Check workspace permissions (requires write) + const userPermission = await getUserEntityPermissions(session.user.id, 'workspace', workspaceId) + if (userPermission !== 'admin' && userPermission !== 'write') { + logger.warn( + `[${requestId}] User ${session.user.id} lacks write permission for workspace ${workspaceId}` + ) + return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }) + } + + const formData = await request.formData() + const file = formData.get('file') as File + + if (!file) { + return NextResponse.json({ error: 'No file provided' }, { status: 400 }) + } + + // Validate file size (100MB limit) + const maxSize = 100 * 1024 * 1024 + if (file.size > maxSize) { + return NextResponse.json( + { error: `File size exceeds 100MB limit (${(file.size / (1024 * 1024)).toFixed(2)}MB)` }, + { status: 400 } + ) + } + + const buffer = Buffer.from(await file.arrayBuffer()) + + const userFile = await uploadWorkspaceFile( + workspaceId, + session.user.id, + buffer, + file.name, + file.type || 'application/octet-stream' + ) + + logger.info(`[${requestId}] Uploaded workspace file: ${file.name}`) + + return NextResponse.json({ + success: true, + file: userFile, + }) + } catch (error) { + logger.error(`[${requestId}] Error uploading workspace file:`, error) + + // Check if it's a duplicate file error + const errorMessage = error instanceof Error ? error.message : 'Failed to upload file' + const isDuplicate = errorMessage.includes('already exists') + + return NextResponse.json( + { + success: false, + error: errorMessage, + isDuplicate, + }, + { status: isDuplicate ? 409 : 500 } + ) + } +} diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/upload-modal/upload-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/upload-modal/upload-modal.tsx index fb274364f..d1e3455f7 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/upload-modal/upload-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/upload-modal/upload-modal.tsx @@ -1,7 +1,7 @@ 'use client' import { useRef, useState } from 'react' -import { Check, Loader2, X } from 'lucide-react' +import { AlertCircle, Check, Loader2, X } from 'lucide-react' import { Button } from '@/components/ui/button' import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { Label } from '@/components/ui/label' @@ -42,7 +42,7 @@ export function UploadModal({ const [fileError, setFileError] = useState(null) const [isDragging, setIsDragging] = useState(false) - const { isUploading, uploadProgress, uploadFiles } = useKnowledgeUpload({ + const { isUploading, uploadProgress, uploadError, uploadFiles, clearError } = useKnowledgeUpload({ onUploadComplete: () => { logger.info(`Successfully uploaded ${files.length} files`) onUploadComplete?.() @@ -55,6 +55,7 @@ export function UploadModal({ setFiles([]) setFileError(null) + clearError() setIsDragging(false) onOpenChange(false) } @@ -276,7 +277,20 @@ export function UploadModal({ )} - {fileError &&

{fileError}

} + {fileError && ( +
+ {fileError} +
+ )} + + {uploadError && ( +
+
+ +
{uploadError.message}
+
+
+ )} diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/components/create-modal/create-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/components/create-modal/create-modal.tsx index bbe08c4a8..67ccaa0e4 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/components/create-modal/create-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/components/create-modal/create-modal.tsx @@ -79,13 +79,20 @@ export function CreateModal({ open, onOpenChange, onKnowledgeBaseCreated }: Crea const scrollContainerRef = useRef(null) const dropZoneRef = useRef(null) - const { uploadFiles, isUploading, uploadProgress } = useKnowledgeUpload({ + const { uploadFiles, isUploading, uploadProgress, uploadError, clearError } = useKnowledgeUpload({ onUploadComplete: (uploadedFiles) => { logger.info(`Successfully uploaded ${uploadedFiles.length} files`) // Files uploaded and document records created - processing will continue in background }, }) + const handleClose = (open: boolean) => { + if (!open) { + clearError() + } + onOpenChange(open) + } + // Cleanup file preview URLs when component unmounts to prevent memory leaks useEffect(() => { return () => { @@ -319,7 +326,7 @@ export function CreateModal({ open, onOpenChange, onKnowledgeBaseCreated }: Crea files.forEach((file) => URL.revokeObjectURL(file.preview)) setFiles([]) - onOpenChange(false) + handleClose(false) } catch (error) { logger.error('Error creating knowledge base:', error) setSubmitStatus({ @@ -332,7 +339,7 @@ export function CreateModal({ open, onOpenChange, onKnowledgeBaseCreated }: Crea } return ( - + onOpenChange(false)} + onClick={() => handleClose(false)} > Close @@ -368,6 +375,14 @@ export function CreateModal({ open, onOpenChange, onKnowledgeBaseCreated }: Crea )} + {uploadError && ( + + + Upload Error + {uploadError.message} + + )} + {/* Form Fields Section - Fixed at top */}
@@ -621,7 +636,7 @@ export function CreateModal({ open, onOpenChange, onKnowledgeBaseCreated }: Crea {/* Footer */}
- - {multiple && !isUploading && ( - - )} + + + + + + + e.stopPropagation()}> + + { + setAddMoreOpen(false) + handleOpenFileDialog({ + preventDefault: () => {}, + stopPropagation: () => {}, + } as React.MouseEvent) + }} + > + Upload New File + + + + {availableWorkspaceFiles.length === 0 + ? 'No files available.' + : 'No files found.'} + + {availableWorkspaceFiles.length > 0 && ( + + {availableWorkspaceFiles.map((file) => ( + { + handleSelectWorkspaceFile(file.id) + setAddMoreOpen(false) + }} + > + + {truncateMiddle(file.name)} + + + ))} + + )} + + + +
)}
- {/* Show upload button if no files and not uploading */} + {/* Show dropdown selector if no files and not uploading */} {!hasFiles && !isUploading && (
- + + + + + + + e.stopPropagation()}> + + { + setPickerOpen(false) + handleOpenFileDialog({ + preventDefault: () => {}, + stopPropagation: () => {}, + } as React.MouseEvent) + }} + > + Upload New File + + + + {availableWorkspaceFiles.length === 0 + ? 'No files available.' + : 'No files found.'} + + {availableWorkspaceFiles.length > 0 && ( + + {availableWorkspaceFiles.map((file) => ( + { + handleSelectWorkspaceFile(file.id) + setPickerOpen(false) + }} + > + + {truncateMiddle(file.name)} + + + ))} + + )} + + + +
)}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/sub-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/sub-block.tsx index 6f625c31b..f800624d9 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/sub-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/sub-block.tsx @@ -306,6 +306,7 @@ export const SubBlock = memo( isPreview={isPreview} previewValue={previewValue} disabled={isDisabled} + isWide={isWide} /> ) case 'webhook-config': { diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/file-uploads/file-uploads.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/file-uploads/file-uploads.tsx new file mode 100644 index 000000000..49c7fb8dd --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/file-uploads/file-uploads.tsx @@ -0,0 +1,319 @@ +'use client' + +import { useEffect, useMemo, useRef, useState } from 'react' +import { Download, Search, Trash2, Upload } from 'lucide-react' +import { useParams } from 'next/navigation' +import { Input } from '@/components/ui' +import { Button } from '@/components/ui/button' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table' +import { createLogger } from '@/lib/logs/console/logger' +import { getFileExtension } from '@/lib/uploads/file-utils' +import type { WorkspaceFileRecord } from '@/lib/uploads/workspace-files' +import { getDocumentIcon } from '@/app/workspace/[workspaceId]/knowledge/components' +import { useUserPermissions } from '@/hooks/use-user-permissions' +import { useWorkspacePermissions } from '@/hooks/use-workspace-permissions' + +const logger = createLogger('FileUploadsSettings') + +const SUPPORTED_EXTENSIONS = [ + 'pdf', + 'csv', + 'doc', + 'docx', + 'txt', + 'md', + 'xlsx', + 'xls', + 'html', + 'htm', + 'pptx', + 'ppt', +] as const +const ACCEPT_ATTR = '.pdf,.csv,.doc,.docx,.txt,.md,.xlsx,.xls,.html,.htm,.pptx,.ppt' + +export function FileUploads() { + const params = useParams() + const workspaceId = params?.workspaceId as string + const [files, setFiles] = useState([]) + const [loading, setLoading] = useState(true) + const [uploading, setUploading] = useState(false) + const [deletingFileId, setDeletingFileId] = useState(null) + const [uploadError, setUploadError] = useState(null) + const fileInputRef = useRef(null) + + const { permissions: workspacePermissions, loading: permissionsLoading } = + useWorkspacePermissions(workspaceId) + const userPermissions = useUserPermissions(workspacePermissions, permissionsLoading) + + const loadFiles = async () => { + if (!workspaceId) return + + try { + setLoading(true) + const response = await fetch(`/api/workspaces/${workspaceId}/files`) + const data = await response.json() + + if (data.success) { + setFiles(data.files) + } + } catch (error) { + logger.error('Error loading workspace files:', error) + } finally { + setLoading(false) + } + } + + useEffect(() => { + void loadFiles() + }, [workspaceId]) + + const handleUploadClick = () => { + fileInputRef.current?.click() + } + + const handleFileChange = async (e: React.ChangeEvent) => { + const list = e.target.files + if (!list || list.length === 0 || !workspaceId) return + + try { + setUploading(true) + setUploadError(null) + + const filesToUpload = Array.from(list) + const unsupported: string[] = [] + const allowedFiles = filesToUpload.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 + }) + let lastError: string | null = null + + for (const selectedFile of allowedFiles) { + try { + const formData = new FormData() + formData.append('file', selectedFile) + + const response = await fetch(`/api/workspaces/${workspaceId}/files`, { + method: 'POST', + body: formData, + }) + + const data = await response.json() + if (!data.success) { + lastError = data.error || 'Upload failed' + } + } catch (err) { + logger.error('Error uploading file:', err) + lastError = 'Upload failed' + } + } + + await loadFiles() + if (unsupported.length) { + lastError = `Unsupported file type: ${unsupported.join(', ')}` + } + if (lastError) setUploadError(lastError) + } catch (error) { + logger.error('Error uploading file:', error) + setUploadError('Upload failed') + setTimeout(() => setUploadError(null), 5000) + } finally { + setUploading(false) + if (fileInputRef.current) { + fileInputRef.current.value = '' + } + } + } + + const handleDownload = async (file: WorkspaceFileRecord) => { + if (!workspaceId) return + + try { + const response = await fetch(`/api/workspaces/${workspaceId}/files/${file.id}/download`, { + method: 'POST', + }) + const data = await response.json() + + if (data.success && data.downloadUrl) { + window.open(data.downloadUrl, '_blank') + } + } catch (error) { + logger.error('Error downloading file:', error) + } + } + + const handleDelete = async (file: WorkspaceFileRecord) => { + if (!workspaceId) return + + try { + setDeletingFileId(file.id) + + const response = await fetch(`/api/workspaces/${workspaceId}/files/${file.id}`, { + method: 'DELETE', + }) + + const data = await response.json() + + if (data.success) { + await loadFiles() + } + } catch (error) { + logger.error('Error deleting file:', error) + } finally { + setDeletingFileId(null) + } + } + + const formatFileSize = (bytes: number): string => { + if (bytes < 1024) return `${bytes} B` + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB` + return `${(bytes / (1024 * 1024)).toFixed(1)} MB` + } + + const formatDate = (date: Date | string): string => { + const d = new Date(date) + const mm = String(d.getMonth() + 1).padStart(2, '0') + const dd = String(d.getDate()).padStart(2, '0') + const yy = String(d.getFullYear()).slice(2) + return `${mm}/${dd}/${yy}` + } + + const [search, setSearch] = useState('') + const filteredFiles = useMemo(() => { + if (!search) return files + const q = search.toLowerCase() + return files.filter((f) => f.name.toLowerCase().includes(q)) + }, [files, search]) + + const truncateMiddle = (text: string, start = 24, end = 12) => { + if (!text) return '' + if (text.length <= start + end + 3) return text + return `${text.slice(0, start)}...${text.slice(-end)}` + } + + return ( +
+ {/* Header: search left, file count + Upload right */} +
+
+ + setSearch(e.target.value)} + className='flex-1 border-0 bg-transparent px-0 font-[380] font-sans text-base text-foreground leading-none placeholder:text-muted-foreground focus-visible:ring-0 focus-visible:ring-offset-0' + /> +
+
+
+ {files.length} {files.length === 1 ? 'file' : 'files'} +
+ {userPermissions.canEdit && ( +
+ + +
+ )} +
+
+ + {/* Error message */} + {uploadError &&
{uploadError}
} + + {/* Files Table */} +
+ {loading ? ( +
Loading files...
+ ) : files.length === 0 ? ( +
+ No files uploaded yet +
+ ) : ( + + + + Name + Size + Uploaded + Actions + + + + {filteredFiles.map((file) => { + const Icon = getDocumentIcon(file.type || '', file.name) + return ( + + +
+ + + {truncateMiddle(file.name)} + +
+
+ + {formatFileSize(file.size)} + + + {formatDate(file.uploadedAt)} + + +
+ + {userPermissions.canEdit && ( + + )} +
+
+
+ ) + })} +
+
+ )} +
+
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/index.ts index 1bd2b3d26..824493f70 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/index.ts @@ -3,6 +3,7 @@ export { ApiKeys } from './api-keys/api-keys' export { Copilot } from './copilot/copilot' export { Credentials } from './credentials/credentials' export { EnvironmentVariables } from './environment/environment' +export { FileUploads } from './file-uploads/file-uploads' export { General } from './general/general' export { MCP } from './mcp/mcp' export { Privacy } from './privacy/privacy' diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/settings-navigation/settings-navigation.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/settings-navigation/settings-navigation.tsx index 5d40c9699..6fe6fa06d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/settings-navigation/settings-navigation.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/settings-navigation/settings-navigation.tsx @@ -3,6 +3,7 @@ import { Bot, CreditCard, FileCode, + Files, Home, Key, LogIn, @@ -32,6 +33,7 @@ interface SettingsNavigationProps { | 'account' | 'credentials' | 'apikeys' + | 'files' | 'subscription' | 'team' | 'sso' @@ -49,6 +51,7 @@ type NavigationItem = { | 'account' | 'credentials' | 'apikeys' + | 'files' | 'subscription' | 'team' | 'sso' @@ -94,6 +97,11 @@ const allNavigationItems: NavigationItem[] = [ label: 'API Keys', icon: Key, }, + { + id: 'files', + label: 'File Uploads', + icon: Files, + }, { id: 'copilot', label: 'Copilot', diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/plan-configs.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/plan-configs.ts index c4bb43bd2..88b663705 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/plan-configs.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/plan-configs.ts @@ -2,6 +2,7 @@ import { Building2, Clock, Database, + HardDrive, HeadphonesIcon, Infinity as InfinityIcon, MessageSquare, @@ -15,6 +16,7 @@ import type { PlanFeature } from './components/plan-card' export const PRO_PLAN_FEATURES: PlanFeature[] = [ { icon: Zap, text: '25 runs per minute (sync)' }, { icon: Clock, text: '200 runs per minute (async)' }, + { icon: HardDrive, text: '50GB file storage' }, { icon: Building2, text: 'Unlimited workspaces' }, { icon: Workflow, text: 'Unlimited workflows' }, { icon: Users, text: 'Unlimited invites' }, @@ -24,12 +26,14 @@ export const PRO_PLAN_FEATURES: PlanFeature[] = [ export const TEAM_PLAN_FEATURES: PlanFeature[] = [ { icon: Zap, text: '75 runs per minute (sync)' }, { icon: Clock, text: '500 runs per minute (async)' }, + { icon: HardDrive, text: '500GB file storage (pooled)' }, { icon: InfinityIcon, text: 'Everything in Pro' }, { icon: MessageSquare, text: 'Dedicated Slack channel' }, ] export const ENTERPRISE_PLAN_FEATURES: PlanFeature[] = [ { icon: Zap, text: 'Custom rate limits' }, + { icon: HardDrive, text: 'Custom file storage limits' }, { icon: Server, text: 'Enterprise hosting' }, { icon: HeadphonesIcon, text: 'Dedicated support' }, ] diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx index 55a7bfc55..88578e992 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx @@ -10,6 +10,7 @@ import { Copilot, Credentials, EnvironmentVariables, + FileUploads, General, MCP, Privacy, @@ -36,6 +37,7 @@ type SettingsSection = | 'account' | 'credentials' | 'apikeys' + | 'files' | 'subscription' | 'team' | 'sso' @@ -165,6 +167,11 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
)} + {activeSection === 'files' && ( +
+ +
+ )} {isSubscriptionEnabled && activeSection === 'subscription' && (
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/subscription-modal/subscription-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/subscription-modal/subscription-modal.tsx index 40ff7b818..e17b4182f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/subscription-modal/subscription-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/subscription-modal/subscription-modal.tsx @@ -7,6 +7,7 @@ import { Clock, Database, DollarSign, + HardDrive, HeadphonesIcon, Infinity as InfinityIcon, MessageSquare, @@ -82,6 +83,7 @@ export function SubscriptionModal({ open, onOpenChange }: SubscriptionModalProps { text: '$10 free inference credit', included: true, icon: DollarSign }, { text: '10 runs per minute (sync)', included: true, icon: Zap }, { text: '50 runs per minute (async)', included: true, icon: Clock }, + { text: '5GB file storage', included: true, icon: HardDrive }, { text: '7-day log retention', included: true, icon: Database }, ], isActive: subscription.isFree, @@ -94,6 +96,7 @@ export function SubscriptionModal({ open, onOpenChange }: SubscriptionModalProps features: [ { text: '25 runs per minute (sync)', included: true, icon: Zap }, { text: '200 runs per minute (async)', included: true, icon: Clock }, + { text: '50GB file storage', included: true, icon: HardDrive }, { text: 'Unlimited workspaces', included: true, icon: Building2 }, { text: 'Unlimited workflows', included: true, icon: Workflow }, { text: 'Unlimited invites', included: true, icon: Users }, @@ -109,6 +112,7 @@ export function SubscriptionModal({ open, onOpenChange }: SubscriptionModalProps features: [ { text: '75 runs per minute (sync)', included: true, icon: Zap }, { text: '500 runs per minute (async)', included: true, icon: Clock }, + { text: '500GB file storage (pooled)', included: true, icon: HardDrive }, { text: 'Everything in Pro', included: true, icon: InfinityIcon }, { text: 'Dedicated Slack channel', included: true, icon: MessageSquare }, ], @@ -121,6 +125,7 @@ export function SubscriptionModal({ open, onOpenChange }: SubscriptionModalProps description: '', features: [ { text: 'Custom rate limits', included: true, icon: Zap }, + { text: 'Custom file storage', included: true, icon: HardDrive }, { text: 'Enterprise hosting license', included: true, icon: Server }, { text: 'Custom enterprise support', included: true, icon: HeadphonesIcon }, ], diff --git a/apps/sim/blocks/blocks/discord.ts b/apps/sim/blocks/blocks/discord.ts index edde8bf53..f3948da47 100644 --- a/apps/sim/blocks/blocks/discord.ts +++ b/apps/sim/blocks/blocks/discord.ts @@ -86,6 +86,31 @@ export const DiscordBlock: BlockConfig = { placeholder: 'Enter message content...', condition: { field: 'operation', value: 'discord_send_message' }, }, + // File upload (basic mode) + { + id: 'attachmentFiles', + title: 'Attachments', + type: 'file-upload', + layout: 'full', + canonicalParamId: 'files', + placeholder: 'Upload files to attach', + condition: { field: 'operation', value: 'discord_send_message' }, + mode: 'basic', + multiple: true, + required: false, + }, + // Variable reference (advanced mode) + { + id: 'files', + title: 'File Attachments', + type: 'short-input', + layout: 'full', + canonicalParamId: 'files', + placeholder: 'Reference files from previous blocks', + condition: { field: 'operation', value: 'discord_send_message' }, + mode: 'advanced', + required: false, + }, ], tools: { access: [ @@ -120,19 +145,22 @@ export const DiscordBlock: BlockConfig = { const channelId = (params.channelId || '').trim() switch (params.operation) { - case 'discord_send_message': + case 'discord_send_message': { if (!serverId) { throw new Error('Server ID is required.') } if (!channelId) { throw new Error('Channel ID is required.') } + const fileParam = params.attachmentFiles || params.files return { ...commonParams, serverId, channelId, content: params.content, + ...(fileParam && { files: fileParam }), } + } case 'discord_get_messages': if (!serverId) { throw new Error('Server ID is required.') @@ -171,6 +199,8 @@ export const DiscordBlock: BlockConfig = { serverId: { type: 'string', description: 'Discord server identifier' }, channelId: { type: 'string', description: 'Discord channel identifier' }, content: { type: 'string', description: 'Message content' }, + attachmentFiles: { type: 'json', description: 'Files to attach (UI upload)' }, + files: { type: 'json', description: 'Files to attach (UserFile array)' }, limit: { type: 'number', description: 'Message limit' }, userId: { type: 'string', description: 'Discord user identifier' }, }, diff --git a/apps/sim/blocks/blocks/file.ts b/apps/sim/blocks/blocks/file.ts index c7cd9a7a9..9cba3db63 100644 --- a/apps/sim/blocks/blocks/file.ts +++ b/apps/sim/blocks/blocks/file.ts @@ -25,7 +25,7 @@ export const FileBlock: BlockConfig = { layout: 'full' as SubBlockLayout, options: [ { id: 'url', label: 'File URL' }, - { id: 'upload', label: 'Upload Files' }, + { id: 'upload', label: 'Uploaded Files' }, ], }, { @@ -42,7 +42,7 @@ export const FileBlock: BlockConfig = { { id: 'file', - title: 'Upload Files', + title: 'Process Files', type: 'file-upload' as SubBlockType, layout: 'full' as SubBlockLayout, acceptedTypes: '.pdf,.csv,.doc,.docx,.txt,.md,.xlsx,.xls,.html,.htm,.pptx,.ppt', @@ -73,6 +73,7 @@ export const FileBlock: BlockConfig = { return { filePath: fileUrl, fileType: params.fileType || 'auto', + workspaceId: params._context?.workspaceId, } } diff --git a/apps/sim/blocks/blocks/gmail.ts b/apps/sim/blocks/blocks/gmail.ts index adce42375..782c197c0 100644 --- a/apps/sim/blocks/blocks/gmail.ts +++ b/apps/sim/blocks/blocks/gmail.ts @@ -75,6 +75,31 @@ export const GmailBlock: BlockConfig = { condition: { field: 'operation', value: ['send_gmail', 'draft_gmail'] }, required: true, }, + // File upload (basic mode) + { + id: 'attachmentFiles', + title: 'Attachments', + type: 'file-upload', + layout: 'full', + canonicalParamId: 'attachments', + placeholder: 'Upload files to attach', + condition: { field: 'operation', value: ['send_gmail', 'draft_gmail'] }, + mode: 'basic', + multiple: true, + required: false, + }, + // Variable reference (advanced mode) + { + id: 'attachments', + title: 'Attachments', + type: 'short-input', + layout: 'full', + canonicalParamId: 'attachments', + placeholder: 'Reference files from previous blocks', + condition: { field: 'operation', value: ['send_gmail', 'draft_gmail'] }, + mode: 'advanced', + required: false, + }, // Advanced Settings - Additional Recipients { id: 'cc', @@ -225,6 +250,7 @@ export const GmailBlock: BlockConfig = { body: { type: 'string', description: 'Email content' }, cc: { type: 'string', description: 'CC recipients (comma-separated)' }, bcc: { type: 'string', description: 'BCC recipients (comma-separated)' }, + attachments: { type: 'json', description: 'Files to attach (UserFile array)' }, // Read operation inputs folder: { type: 'string', description: 'Gmail folder' }, manualFolder: { type: 'string', description: 'Manual folder name' }, diff --git a/apps/sim/blocks/blocks/google_drive.ts b/apps/sim/blocks/blocks/google_drive.ts index 718a9f664..7348e91a6 100644 --- a/apps/sim/blocks/blocks/google_drive.ts +++ b/apps/sim/blocks/blocks/google_drive.ts @@ -22,6 +22,7 @@ export const GoogleDriveBlock: BlockConfig = { layout: 'full', options: [ { label: 'Create Folder', id: 'create_folder' }, + { label: 'Create File', id: 'create_file' }, { label: 'Upload File', id: 'upload' }, { label: 'List Files', id: 'list' }, ], @@ -39,23 +40,48 @@ export const GoogleDriveBlock: BlockConfig = { requiredScopes: ['https://www.googleapis.com/auth/drive.file'], placeholder: 'Select Google Drive account', }, - // Upload Fields + // Create/Upload File Fields { id: 'fileName', title: 'File Name', type: 'short-input', layout: 'full', - placeholder: 'Name of the file', - condition: { field: 'operation', value: 'upload' }, + placeholder: 'Name of the file (e.g., document.txt)', + condition: { field: 'operation', value: ['create_file', 'upload'] }, required: true, }, + // File upload (basic mode) - binary files + { + id: 'fileUpload', + title: 'Upload File', + type: 'file-upload', + layout: 'full', + canonicalParamId: 'file', + placeholder: 'Upload a file to Google Drive', + condition: { field: 'operation', value: 'upload' }, + mode: 'basic', + multiple: false, + required: false, + }, + // Variable reference (advanced mode) - for referencing files from previous blocks + { + id: 'file', + title: 'File Reference', + type: 'short-input', + layout: 'full', + canonicalParamId: 'file', + placeholder: 'Reference file from previous block (e.g., {{block_name.file}})', + condition: { field: 'operation', value: 'upload' }, + mode: 'advanced', + required: false, + }, { id: 'content', - title: 'Content', + title: 'Text Content', type: 'long-input', layout: 'full', - placeholder: 'Content to upload to the file', - condition: { field: 'operation', value: 'upload' }, + placeholder: 'Text content for the file', + condition: { field: 'operation', value: 'create_file' }, required: true, }, { @@ -64,13 +90,18 @@ export const GoogleDriveBlock: BlockConfig = { type: 'dropdown', layout: 'full', options: [ + { label: 'Auto-detect from file', id: '' }, { label: 'Google Doc', id: 'application/vnd.google-apps.document' }, { label: 'Google Sheet', id: 'application/vnd.google-apps.spreadsheet' }, { label: 'Google Slides', id: 'application/vnd.google-apps.presentation' }, { label: 'PDF (application/pdf)', id: 'application/pdf' }, + { label: 'Plain Text (text/plain)', id: 'text/plain' }, + { label: 'HTML (text/html)', id: 'text/html' }, + { label: 'CSV (text/csv)', id: 'text/csv' }, ], - placeholder: 'Select a file type', - condition: { field: 'operation', value: 'upload' }, + placeholder: 'Select a file type (optional)', + condition: { field: 'operation', value: ['create_file', 'upload'] }, + required: false, }, { id: 'folderSelector', @@ -85,7 +116,7 @@ export const GoogleDriveBlock: BlockConfig = { placeholder: 'Select a parent folder', mode: 'basic', dependsOn: ['credential'], - condition: { field: 'operation', value: 'upload' }, + condition: { field: 'operation', value: ['create_file', 'upload'] }, }, { id: 'manualFolderId', @@ -95,7 +126,7 @@ export const GoogleDriveBlock: BlockConfig = { canonicalParamId: 'folderId', placeholder: 'Enter parent folder ID (leave empty for root folder)', mode: 'advanced', - condition: { field: 'operation', value: 'upload' }, + condition: { field: 'operation', value: ['create_file', 'upload'] }, }, // Get Content Fields // { @@ -223,6 +254,7 @@ export const GoogleDriveBlock: BlockConfig = { config: { tool: (params) => { switch (params.operation) { + case 'create_file': case 'upload': return 'google_drive_upload' case 'create_folder': @@ -254,7 +286,8 @@ export const GoogleDriveBlock: BlockConfig = { credential: { type: 'string', description: 'Google Drive access token' }, // Upload and Create Folder operation inputs fileName: { type: 'string', description: 'File or folder name' }, - content: { type: 'string', description: 'File content' }, + file: { type: 'json', description: 'File to upload (UserFile object)' }, + content: { type: 'string', description: 'Text content to upload' }, mimeType: { type: 'string', description: 'File MIME type' }, // List operation inputs folderSelector: { type: 'string', description: 'Selected folder' }, diff --git a/apps/sim/blocks/blocks/microsoft_teams.ts b/apps/sim/blocks/blocks/microsoft_teams.ts index 62da0e80b..45d005a2d 100644 --- a/apps/sim/blocks/blocks/microsoft_teams.ts +++ b/apps/sim/blocks/blocks/microsoft_teams.ts @@ -138,6 +138,31 @@ export const MicrosoftTeamsBlock: BlockConfig = { condition: { field: 'operation', value: ['write_chat', 'write_channel'] }, required: true, }, + // File upload (basic mode) + { + id: 'attachmentFiles', + title: 'Attachments', + type: 'file-upload', + layout: 'full', + canonicalParamId: 'files', + placeholder: 'Upload files to attach', + condition: { field: 'operation', value: ['write_chat', 'write_channel'] }, + mode: 'basic', + multiple: true, + required: false, + }, + // Variable reference (advanced mode) + { + id: 'files', + title: 'File Attachments', + type: 'short-input', + layout: 'full', + canonicalParamId: 'files', + placeholder: 'Reference files from previous blocks', + condition: { field: 'operation', value: ['write_chat', 'write_channel'] }, + mode: 'advanced', + required: false, + }, { id: 'triggerConfig', title: 'Trigger Configuration', @@ -179,6 +204,8 @@ export const MicrosoftTeamsBlock: BlockConfig = { manualChatId, channelId, manualChannelId, + attachmentFiles, + files, ...rest } = params @@ -186,11 +213,17 @@ export const MicrosoftTeamsBlock: BlockConfig = { const effectiveChatId = (chatId || manualChatId || '').trim() const effectiveChannelId = (channelId || manualChannelId || '').trim() - const baseParams = { + const baseParams: Record = { ...rest, credential, } + // Add files if provided + const fileParam = attachmentFiles || files + if (fileParam && (operation === 'write_chat' || operation === 'write_channel')) { + baseParams.files = fileParam + } + if (operation === 'read_chat' || operation === 'write_chat') { if (!effectiveChatId) { throw new Error('Chat ID is required. Please select a chat or enter a chat ID.') @@ -223,6 +256,8 @@ export const MicrosoftTeamsBlock: BlockConfig = { teamId: { type: 'string', description: 'Team identifier' }, manualTeamId: { type: 'string', description: 'Manual team identifier' }, content: { type: 'string', description: 'Message content' }, + attachmentFiles: { type: 'json', description: 'Files to attach (UI upload)' }, + files: { type: 'json', description: 'Files to attach (UserFile array)' }, }, outputs: { content: { type: 'string', description: 'Formatted message content from chat/channel' }, diff --git a/apps/sim/blocks/blocks/onedrive.ts b/apps/sim/blocks/blocks/onedrive.ts index 512a2a29b..0c6b5600b 100644 --- a/apps/sim/blocks/blocks/onedrive.ts +++ b/apps/sim/blocks/blocks/onedrive.ts @@ -22,6 +22,7 @@ export const OneDriveBlock: BlockConfig = { layout: 'full', options: [ { label: 'Create Folder', id: 'create_folder' }, + { label: 'Create File', id: 'create_file' }, { label: 'Upload File', id: 'upload' }, { label: 'List Files', id: 'list' }, ], @@ -44,22 +45,49 @@ export const OneDriveBlock: BlockConfig = { ], placeholder: 'Select Microsoft account', }, - // Upload Fields + // Create File Fields { id: 'fileName', title: 'File Name', type: 'short-input', layout: 'full', - placeholder: 'Name of the file', + placeholder: 'Name of the file (e.g., document.txt)', + condition: { field: 'operation', value: ['create_file', 'upload'] }, + required: true, + }, + // File upload (basic mode) + { + id: 'file', + title: 'File', + type: 'file-upload', + layout: 'full', + canonicalParamId: 'file', + placeholder: 'Upload a file', condition: { field: 'operation', value: 'upload' }, + mode: 'basic', + multiple: false, + required: false, + }, + // Variable reference (advanced mode) + { + id: 'fileReference', + title: 'File', + type: 'short-input', + layout: 'full', + canonicalParamId: 'file', + placeholder: 'Reference file from previous block (e.g., {{block_1.file}})', + condition: { field: 'operation', value: 'upload' }, + mode: 'advanced', + required: false, }, { id: 'content', - title: 'Content', + title: 'Text Content', type: 'long-input', layout: 'full', - placeholder: 'Content to upload to the file', - condition: { field: 'operation', value: 'upload' }, + placeholder: 'Text content for the file', + condition: { field: 'operation', value: 'create_file' }, + required: true, }, { @@ -82,7 +110,7 @@ export const OneDriveBlock: BlockConfig = { placeholder: 'Select a parent folder', dependsOn: ['credential'], mode: 'basic', - condition: { field: 'operation', value: 'upload' }, + condition: { field: 'operation', value: ['create_file', 'upload'] }, }, { id: 'manualFolderId', @@ -93,7 +121,7 @@ export const OneDriveBlock: BlockConfig = { placeholder: 'Enter parent folder ID (leave empty for root folder)', dependsOn: ['credential'], mode: 'advanced', - condition: { field: 'operation', value: 'upload' }, + condition: { field: 'operation', value: ['create_file', 'upload'] }, }, { id: 'folderName', @@ -194,6 +222,7 @@ export const OneDriveBlock: BlockConfig = { config: { tool: (params) => { switch (params.operation) { + case 'create_file': case 'upload': return 'onedrive_upload' case 'create_folder': @@ -225,7 +254,9 @@ export const OneDriveBlock: BlockConfig = { credential: { type: 'string', description: 'Microsoft account credential' }, // Upload and Create Folder operation inputs fileName: { type: 'string', description: 'File name' }, - content: { type: 'string', description: 'File content' }, + file: { type: 'json', description: 'File to upload (UserFile object)' }, + fileReference: { type: 'json', description: 'File reference from previous block' }, + content: { type: 'string', description: 'Text content to upload' }, // Get Content operation inputs // fileId: { type: 'string', required: false }, // List operation inputs diff --git a/apps/sim/blocks/blocks/outlook.ts b/apps/sim/blocks/blocks/outlook.ts index 31cf6c8ca..3798cfd78 100644 --- a/apps/sim/blocks/blocks/outlook.ts +++ b/apps/sim/blocks/blocks/outlook.ts @@ -97,6 +97,31 @@ export const OutlookBlock: BlockConfig = { condition: { field: 'operation', value: ['send_outlook', 'draft_outlook'] }, required: true, }, + // File upload (basic mode) + { + id: 'attachmentFiles', + title: 'Attachments', + type: 'file-upload', + layout: 'full', + canonicalParamId: 'attachments', + placeholder: 'Upload files to attach', + condition: { field: 'operation', value: ['send_outlook', 'draft_outlook'] }, + mode: 'basic', + multiple: true, + required: false, + }, + // Variable reference (advanced mode) + { + id: 'attachments', + title: 'Attachments', + type: 'short-input', + layout: 'full', + canonicalParamId: 'attachments', + placeholder: 'Reference files from previous blocks', + condition: { field: 'operation', value: ['send_outlook', 'draft_outlook'] }, + mode: 'advanced', + required: false, + }, // Advanced Settings - Threading { id: 'replyToMessageId', @@ -231,6 +256,8 @@ export const OutlookBlock: BlockConfig = { to: { type: 'string', description: 'Recipient email address' }, subject: { type: 'string', description: 'Email subject' }, body: { type: 'string', description: 'Email content' }, + attachmentFiles: { type: 'json', description: 'Files to attach (UI upload)' }, + attachments: { type: 'json', description: 'Files to attach (UserFile array)' }, // Forward operation inputs messageId: { type: 'string', description: 'Message ID to forward' }, comment: { type: 'string', description: 'Optional comment for forwarding' }, diff --git a/apps/sim/blocks/blocks/s3.ts b/apps/sim/blocks/blocks/s3.ts index 577c6cc37..a7ee4a74e 100644 --- a/apps/sim/blocks/blocks/s3.ts +++ b/apps/sim/blocks/blocks/s3.ts @@ -6,15 +6,31 @@ import type { S3Response } from '@/tools/s3/types' export const S3Block: BlockConfig = { type: 's3', name: 'S3', - description: 'View S3 files', + description: 'Upload, download, list, and manage S3 files', authMode: AuthMode.ApiKey, longDescription: - 'Integrate S3 into the workflow. Can get presigned URLs for S3 objects. Requires access key and secret access key.', + 'Integrate S3 into the workflow. Upload files, download objects, list bucket contents, delete objects, and copy objects between buckets. Requires AWS access key and secret access key.', docsLink: 'https://docs.sim.ai/tools/s3', category: 'tools', bgColor: '#E0E0E0', icon: S3Icon, subBlocks: [ + // Operation selector + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Download File', id: 'get_object' }, + { label: 'Upload File', id: 'put_object' }, + { label: 'List Objects', id: 'list_objects' }, + { label: 'Delete Object', id: 'delete_object' }, + { label: 'Copy Object', id: 'copy_object' }, + ], + value: () => 'get_object', + }, + // AWS Credentials { id: 'accessKeyId', title: 'Access Key ID', @@ -33,76 +49,394 @@ export const S3Block: BlockConfig = { password: true, required: true, }, + { + id: 'region', + title: 'AWS Region', + type: 'short-input', + layout: 'full', + placeholder: 'e.g., us-east-1, us-west-2', + condition: { + field: 'operation', + value: ['put_object', 'list_objects', 'delete_object', 'copy_object'], + }, + required: true, + }, + { + id: 'bucketName', + title: 'Bucket Name', + type: 'short-input', + layout: 'full', + placeholder: 'Enter S3 bucket name', + condition: { field: 'operation', value: ['put_object', 'list_objects', 'delete_object'] }, + required: true, + }, + + // ===== UPLOAD (PUT OBJECT) FIELDS ===== + { + id: 'objectKey', + title: 'Object Key/Path', + type: 'short-input', + layout: 'full', + placeholder: 'e.g., myfile.pdf or documents/report.pdf', + condition: { field: 'operation', value: 'put_object' }, + required: true, + }, + { + id: 'uploadFile', + title: 'File to Upload', + type: 'file-upload', + layout: 'full', + canonicalParamId: 'file', + placeholder: 'Upload a file', + condition: { field: 'operation', value: 'put_object' }, + mode: 'basic', + multiple: false, + }, + { + id: 'file', + title: 'File Reference', + type: 'short-input', + layout: 'full', + canonicalParamId: 'file', + placeholder: 'Reference a file from previous blocks', + condition: { field: 'operation', value: 'put_object' }, + mode: 'advanced', + }, + { + id: 'content', + title: 'Text Content', + type: 'long-input', + layout: 'full', + placeholder: 'Or enter text content to upload', + condition: { field: 'operation', value: 'put_object' }, + }, + { + id: 'contentType', + title: 'Content Type', + type: 'short-input', + layout: 'full', + placeholder: 'e.g., text/plain, application/json (auto-detected if not provided)', + condition: { field: 'operation', value: 'put_object' }, + mode: 'advanced', + }, + { + id: 'acl', + title: 'Access Control', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Private', id: 'private' }, + { label: 'Public Read', id: 'public-read' }, + { label: 'Public Read/Write', id: 'public-read-write' }, + { label: 'Authenticated Read', id: 'authenticated-read' }, + ], + placeholder: 'Select ACL (default: private)', + condition: { field: 'operation', value: 'put_object' }, + mode: 'advanced', + }, + + // ===== DOWNLOAD (GET OBJECT) FIELDS ===== { id: 's3Uri', title: 'S3 Object URL', type: 'short-input', layout: 'full', placeholder: 'e.g., https://bucket-name.s3.region.amazonaws.com/path/to/file', + condition: { field: 'operation', value: 'get_object' }, required: true, }, + + // ===== LIST OBJECTS FIELDS ===== + { + id: 'prefix', + title: 'Prefix/Folder', + type: 'short-input', + layout: 'full', + placeholder: 'Filter by prefix (e.g., folder/ or leave empty for all)', + condition: { field: 'operation', value: 'list_objects' }, + }, + { + id: 'maxKeys', + title: 'Max Results', + type: 'short-input', + layout: 'full', + placeholder: 'Maximum number of objects to return (default: 1000)', + condition: { field: 'operation', value: 'list_objects' }, + mode: 'advanced', + }, + { + id: 'continuationToken', + title: 'Continuation Token', + type: 'short-input', + layout: 'full', + placeholder: 'Token for pagination (from previous response)', + condition: { field: 'operation', value: 'list_objects' }, + mode: 'advanced', + }, + + // ===== DELETE OBJECT FIELDS ===== + { + id: 'objectKey', + title: 'Object Key/Path', + type: 'short-input', + layout: 'full', + placeholder: 'e.g., myfile.pdf or documents/report.pdf', + condition: { field: 'operation', value: 'delete_object' }, + required: true, + }, + + // ===== COPY OBJECT FIELDS ===== + { + id: 'sourceBucket', + title: 'Source Bucket', + type: 'short-input', + layout: 'full', + placeholder: 'Source bucket name', + condition: { field: 'operation', value: 'copy_object' }, + required: true, + }, + { + id: 'sourceKey', + title: 'Source Object Key', + type: 'short-input', + layout: 'full', + placeholder: 'e.g., oldfile.pdf or folder/file.pdf', + condition: { field: 'operation', value: 'copy_object' }, + required: true, + }, + { + id: 'destinationBucket', + title: 'Destination Bucket', + type: 'short-input', + layout: 'full', + placeholder: 'Destination bucket name (can be same as source)', + condition: { field: 'operation', value: 'copy_object' }, + required: true, + }, + { + id: 'destinationKey', + title: 'Destination Object Key', + type: 'short-input', + layout: 'full', + placeholder: 'e.g., newfile.pdf or backup/file.pdf', + condition: { field: 'operation', value: 'copy_object' }, + required: true, + }, + { + id: 'copyAcl', + title: 'Access Control', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Private', id: 'private' }, + { label: 'Public Read', id: 'public-read' }, + { label: 'Public Read/Write', id: 'public-read-write' }, + { label: 'Authenticated Read', id: 'authenticated-read' }, + ], + placeholder: 'Select ACL for copied object (default: private)', + condition: { field: 'operation', value: 'copy_object' }, + mode: 'advanced', + canonicalParamId: 'acl', + }, ], tools: { - access: ['s3_get_object'], + access: [ + 's3_put_object', + 's3_get_object', + 's3_list_objects', + 's3_delete_object', + 's3_copy_object', + ], config: { - tool: () => 's3_get_object', + tool: (params) => { + // Default to get_object for backward compatibility with existing workflows + const operation = params.operation || 'get_object' + + switch (operation) { + case 'put_object': + return 's3_put_object' + case 'get_object': + return 's3_get_object' + case 'list_objects': + return 's3_list_objects' + case 'delete_object': + return 's3_delete_object' + case 'copy_object': + return 's3_copy_object' + default: + throw new Error(`Invalid S3 operation: ${operation}`) + } + }, params: (params) => { - // Validate required fields + // Validate required fields (common to all operations) if (!params.accessKeyId) { throw new Error('Access Key ID is required') } if (!params.secretAccessKey) { throw new Error('Secret Access Key is required') } - if (!params.s3Uri) { - throw new Error('S3 Object URL is required') - } - // Parse S3 URI - try { - const url = new URL(params.s3Uri) - const hostname = url.hostname + // Default to get_object for backward compatibility with existing workflows + const operation = params.operation || 'get_object' - // Extract bucket name from hostname - const bucketName = hostname.split('.')[0] + // Operation-specific parameters + switch (operation) { + case 'put_object': { + if (!params.region) { + throw new Error('AWS Region is required') + } + if (!params.bucketName) { + throw new Error('Bucket Name is required') + } + if (!params.objectKey) { + throw new Error('Object Key is required for upload') + } + // Use file from uploadFile if in basic mode, otherwise use file reference + const fileParam = params.uploadFile || params.file - // Extract region from hostname - const regionMatch = hostname.match(/s3[.-]([^.]+)\.amazonaws\.com/) - const region = regionMatch ? regionMatch[1] : 'us-east-1' - - // Extract object key from pathname (remove leading slash) - const objectKey = url.pathname.startsWith('/') ? url.pathname.substring(1) : url.pathname - - if (!bucketName) { - throw new Error('Could not extract bucket name from URL') + return { + accessKeyId: params.accessKeyId, + secretAccessKey: params.secretAccessKey, + region: params.region, + bucketName: params.bucketName, + objectKey: params.objectKey, + file: fileParam, + content: params.content, + contentType: params.contentType, + acl: params.acl, + } } - if (!objectKey) { - throw new Error('No object key found in URL') + case 'get_object': { + if (!params.s3Uri) { + throw new Error('S3 Object URL is required') + } + + // Parse S3 URI for get_object + try { + const url = new URL(params.s3Uri) + const hostname = url.hostname + const bucketName = hostname.split('.')[0] + const regionMatch = hostname.match(/s3[.-]([^.]+)\.amazonaws\.com/) + const region = regionMatch ? regionMatch[1] : params.region + const objectKey = url.pathname.startsWith('/') + ? url.pathname.substring(1) + : url.pathname + + if (!bucketName || !objectKey) { + throw new Error('Could not parse S3 URL') + } + + return { + accessKeyId: params.accessKeyId, + secretAccessKey: params.secretAccessKey, + region, + bucketName, + objectKey, + s3Uri: params.s3Uri, + } + } catch (_error) { + throw new Error( + 'Invalid S3 Object URL format. Expected: https://bucket-name.s3.region.amazonaws.com/path/to/file' + ) + } } - return { - accessKeyId: params.accessKeyId, - secretAccessKey: params.secretAccessKey, - region, - bucketName, - objectKey, + case 'list_objects': + if (!params.region) { + throw new Error('AWS Region is required') + } + if (!params.bucketName) { + throw new Error('Bucket Name is required') + } + return { + accessKeyId: params.accessKeyId, + secretAccessKey: params.secretAccessKey, + region: params.region, + bucketName: params.bucketName, + prefix: params.prefix, + maxKeys: params.maxKeys ? Number.parseInt(params.maxKeys as string, 10) : undefined, + continuationToken: params.continuationToken, + } + + case 'delete_object': + if (!params.region) { + throw new Error('AWS Region is required') + } + if (!params.bucketName) { + throw new Error('Bucket Name is required') + } + if (!params.objectKey) { + throw new Error('Object Key is required for deletion') + } + return { + accessKeyId: params.accessKeyId, + secretAccessKey: params.secretAccessKey, + region: params.region, + bucketName: params.bucketName, + objectKey: params.objectKey, + } + + case 'copy_object': { + if (!params.region) { + throw new Error('AWS Region is required') + } + if (!params.sourceBucket || !params.sourceKey) { + throw new Error('Source bucket and key are required') + } + if (!params.destinationBucket || !params.destinationKey) { + throw new Error('Destination bucket and key are required') + } + // Use copyAcl if provided, map to acl parameter + const acl = params.copyAcl || params.acl + return { + accessKeyId: params.accessKeyId, + secretAccessKey: params.secretAccessKey, + region: params.region, + sourceBucket: params.sourceBucket, + sourceKey: params.sourceKey, + destinationBucket: params.destinationBucket, + destinationKey: params.destinationKey, + acl: acl, + } } - } catch (_error) { - throw new Error( - 'Invalid S3 Object URL format. Expected format: https://bucket-name.s3.region.amazonaws.com/path/to/file' - ) + + default: + throw new Error(`Unknown operation: ${operation}`) } }, }, }, inputs: { + operation: { type: 'string', description: 'Operation to perform' }, accessKeyId: { type: 'string', description: 'AWS access key ID' }, secretAccessKey: { type: 'string', description: 'AWS secret access key' }, + region: { type: 'string', description: 'AWS region' }, + bucketName: { type: 'string', description: 'S3 bucket name' }, + // Upload inputs + objectKey: { type: 'string', description: 'Object key/path in S3' }, + uploadFile: { type: 'json', description: 'File to upload (UI)' }, + file: { type: 'json', description: 'File to upload (reference)' }, + content: { type: 'string', description: 'Text content to upload' }, + contentType: { type: 'string', description: 'Content-Type header' }, + acl: { type: 'string', description: 'Access control list' }, + // Download inputs s3Uri: { type: 'string', description: 'S3 object URL' }, + // List inputs + prefix: { type: 'string', description: 'Prefix filter' }, + maxKeys: { type: 'number', description: 'Maximum results' }, + continuationToken: { type: 'string', description: 'Pagination token' }, + // Copy inputs + sourceBucket: { type: 'string', description: 'Source bucket name' }, + sourceKey: { type: 'string', description: 'Source object key' }, + destinationBucket: { type: 'string', description: 'Destination bucket name' }, + destinationKey: { type: 'string', description: 'Destination object key' }, + copyAcl: { type: 'string', description: 'ACL for copied object' }, }, outputs: { - url: { type: 'string', description: 'Presigned URL' }, - metadata: { type: 'json', description: 'Object metadata' }, + url: { type: 'string', description: 'URL of S3 object' }, + objects: { type: 'json', description: 'List of objects (for list operation)' }, + deleted: { type: 'boolean', description: 'Deletion status' }, + metadata: { type: 'json', description: 'Operation metadata' }, }, } diff --git a/apps/sim/blocks/blocks/sharepoint.ts b/apps/sim/blocks/blocks/sharepoint.ts index 54af92397..cf0dd8f77 100644 --- a/apps/sim/blocks/blocks/sharepoint.ts +++ b/apps/sim/blocks/blocks/sharepoint.ts @@ -31,6 +31,7 @@ export const SharepointBlock: BlockConfig = { { label: 'Read List', id: 'read_list' }, { label: 'Update List', id: 'update_list' }, { label: 'Add List Items', id: 'add_list_items' }, + { label: 'Upload File', id: 'upload_file' }, ], }, { @@ -83,6 +84,7 @@ export const SharepointBlock: BlockConfig = { 'read_list', 'update_list', 'add_list_items', + 'upload_file', ], }, }, @@ -182,6 +184,62 @@ export const SharepointBlock: BlockConfig = { canonicalParamId: 'listItemFields', condition: { field: 'operation', value: ['update_list', 'add_list_items'] }, }, + + // Upload File operation fields + { + id: 'driveId', + title: 'Document Library ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter document library (drive) ID', + canonicalParamId: 'driveId', + condition: { field: 'operation', value: 'upload_file' }, + mode: 'advanced', + }, + { + id: 'folderPath', + title: 'Folder Path', + type: 'short-input', + layout: 'full', + placeholder: 'Optional folder path (e.g., /Documents/Subfolder)', + condition: { field: 'operation', value: 'upload_file' }, + required: false, + }, + { + id: 'fileName', + title: 'File Name', + type: 'short-input', + layout: 'full', + placeholder: 'Optional: override uploaded file name', + condition: { field: 'operation', value: 'upload_file' }, + mode: 'advanced', + required: false, + }, + // File upload (basic mode) + { + id: 'uploadFiles', + title: 'Files', + type: 'file-upload', + layout: 'full', + canonicalParamId: 'files', + placeholder: 'Upload files to SharePoint', + condition: { field: 'operation', value: 'upload_file' }, + mode: 'basic', + multiple: true, + required: false, + }, + // Variable reference (advanced mode) + { + id: 'files', + title: 'Files', + type: 'short-input', + layout: 'full', + canonicalParamId: 'files', + placeholder: 'Reference files from previous blocks', + condition: { field: 'operation', value: 'upload_file' }, + mode: 'advanced', + required: false, + }, ], tools: { access: [ @@ -192,6 +250,7 @@ export const SharepointBlock: BlockConfig = { 'sharepoint_get_list', 'sharepoint_update_list', 'sharepoint_add_list_items', + 'sharepoint_upload_file', ], config: { tool: (params) => { @@ -210,6 +269,8 @@ export const SharepointBlock: BlockConfig = { return 'sharepoint_update_list' case 'add_list_items': return 'sharepoint_add_list_items' + case 'upload_file': + return 'sharepoint_upload_file' default: throw new Error(`Invalid Sharepoint operation: ${params.operation}`) } @@ -225,6 +286,8 @@ export const SharepointBlock: BlockConfig = { listItemFields, includeColumns, includeItems, + uploadFiles, + files, ...others } = rest as any @@ -270,7 +333,9 @@ export const SharepointBlock: BlockConfig = { } catch {} } - return { + // Handle file upload files parameter + const fileParam = uploadFiles || files + const baseParams = { credential, siteId: effectiveSiteId || undefined, pageSize: others.pageSize ? Number.parseInt(others.pageSize as string, 10) : undefined, @@ -281,6 +346,13 @@ export const SharepointBlock: BlockConfig = { includeColumns: coerceBoolean(includeColumns), includeItems: coerceBoolean(includeItems), } + + // Add files if provided + if (fileParam) { + baseParams.files = fileParam + } + + return baseParams }, }, }, @@ -303,6 +375,11 @@ export const SharepointBlock: BlockConfig = { includeItems: { type: 'boolean', description: 'Include items in response' }, listItemId: { type: 'string', description: 'List item ID' }, listItemFields: { type: 'string', description: 'List item fields' }, + driveId: { type: 'string', description: 'Document library (drive) ID' }, + folderPath: { type: 'string', description: 'Folder path for file upload' }, + fileName: { type: 'string', description: 'File name override' }, + uploadFiles: { type: 'json', description: 'Files to upload (UI upload)' }, + files: { type: 'json', description: 'Files to upload (UserFile array)' }, }, outputs: { sites: { @@ -322,6 +399,14 @@ export const SharepointBlock: BlockConfig = { type: 'json', description: 'Array of SharePoint list items with fields', }, + uploadedFiles: { + type: 'json', + description: 'Array of uploaded file objects with id, name, webUrl, size', + }, + fileCount: { + type: 'number', + description: 'Number of files uploaded', + }, success: { type: 'boolean', description: 'Success status', diff --git a/apps/sim/blocks/blocks/slack.ts b/apps/sim/blocks/blocks/slack.ts index 305bb32c2..b75fa2a81 100644 --- a/apps/sim/blocks/blocks/slack.ts +++ b/apps/sim/blocks/blocks/slack.ts @@ -109,6 +109,31 @@ export const SlackBlock: BlockConfig = { }, required: true, }, + // File upload (basic mode) + { + id: 'attachmentFiles', + title: 'Attachments', + type: 'file-upload', + layout: 'full', + canonicalParamId: 'files', + placeholder: 'Upload files to attach', + condition: { field: 'operation', value: 'send' }, + mode: 'basic', + multiple: true, + required: false, + }, + // Variable reference (advanced mode) + { + id: 'files', + title: 'File Attachments', + type: 'short-input', + layout: 'full', + canonicalParamId: 'files', + placeholder: 'Reference files from previous blocks', + condition: { field: 'operation', value: 'send' }, + mode: 'advanced', + required: false, + }, // Canvas specific fields { id: 'title', @@ -194,6 +219,8 @@ export const SlackBlock: BlockConfig = { content, limit, oldest, + attachmentFiles, + files, ...rest } = params @@ -224,12 +251,18 @@ export const SlackBlock: BlockConfig = { // Handle operation-specific params switch (operation) { - case 'send': + case 'send': { if (!rest.text) { throw new Error('Message text is required for send operation') } baseParams.text = rest.text + // Add files if provided + const fileParam = attachmentFiles || files + if (fileParam) { + baseParams.files = fileParam + } break + } case 'canvas': if (!title || !content) { @@ -264,6 +297,8 @@ export const SlackBlock: BlockConfig = { channel: { type: 'string', description: 'Channel identifier' }, manualChannel: { type: 'string', description: 'Manual channel identifier' }, text: { type: 'string', description: 'Message text' }, + attachmentFiles: { type: 'json', description: 'Files to attach (UI upload)' }, + files: { type: 'json', description: 'Files to attach (UserFile array)' }, title: { type: 'string', description: 'Canvas title' }, content: { type: 'string', description: 'Canvas content' }, limit: { type: 'string', description: 'Message limit' }, diff --git a/apps/sim/blocks/blocks/telegram.ts b/apps/sim/blocks/blocks/telegram.ts index 727411bdb..d34b40bd2 100644 --- a/apps/sim/blocks/blocks/telegram.ts +++ b/apps/sim/blocks/blocks/telegram.ts @@ -27,6 +27,7 @@ export const TelegramBlock: BlockConfig = { { label: 'Send Video', id: 'telegram_send_video' }, { label: 'Send Audio', id: 'telegram_send_audio' }, { label: 'Send Animation', id: 'telegram_send_animation' }, + { label: 'Send Document', id: 'telegram_send_document' }, { label: 'Delete Message', id: 'telegram_delete_message' }, ], value: () => 'telegram_message', @@ -107,6 +108,33 @@ export const TelegramBlock: BlockConfig = { required: true, condition: { field: 'operation', value: 'telegram_send_animation' }, }, + // File upload (basic mode) for Send Document + { + id: 'attachmentFiles', + title: 'Document', + type: 'file-upload', + layout: 'full', + canonicalParamId: 'files', + placeholder: 'Upload document file', + condition: { field: 'operation', value: 'telegram_send_document' }, + mode: 'basic', + multiple: false, + required: false, + description: 'Document file to send (PDF, ZIP, DOC, etc.). Max size: 50MB', + }, + // Variable reference (advanced mode) for Send Document + { + id: 'files', + title: 'Document', + type: 'short-input', + layout: 'full', + canonicalParamId: 'files', + placeholder: 'Reference document from previous blocks', + condition: { field: 'operation', value: 'telegram_send_document' }, + mode: 'advanced', + required: false, + description: 'Reference a document file from a previous block', + }, { id: 'caption', title: 'Caption', @@ -121,6 +149,7 @@ export const TelegramBlock: BlockConfig = { 'telegram_send_video', 'telegram_send_audio', 'telegram_send_animation', + 'telegram_send_document', ], }, }, @@ -152,6 +181,7 @@ export const TelegramBlock: BlockConfig = { 'telegram_send_video', 'telegram_send_audio', 'telegram_send_animation', + 'telegram_send_document', ], config: { tool: (params) => { @@ -168,6 +198,8 @@ export const TelegramBlock: BlockConfig = { return 'telegram_send_audio' case 'telegram_send_animation': return 'telegram_send_animation' + case 'telegram_send_document': + return 'telegram_send_document' default: return 'telegram_message' } @@ -238,6 +270,15 @@ export const TelegramBlock: BlockConfig = { animation: params.animation, caption: params.caption, } + case 'telegram_send_document': { + // Handle file upload + const fileParam = params.attachmentFiles || params.files + return { + ...commonParams, + files: fileParam, + caption: params.caption, + } + } default: return { ...commonParams, @@ -256,6 +297,11 @@ export const TelegramBlock: BlockConfig = { video: { type: 'string', description: 'Video URL or file_id' }, audio: { type: 'string', description: 'Audio URL or file_id' }, animation: { type: 'string', description: 'Animation URL or file_id' }, + attachmentFiles: { + type: 'json', + description: 'Files to attach (UI upload)', + }, + files: { type: 'json', description: 'Files to attach (UserFile array)' }, caption: { type: 'string', description: 'Caption for media' }, messageId: { type: 'string', description: 'Message ID to delete' }, }, diff --git a/apps/sim/blocks/blocks/vision.ts b/apps/sim/blocks/blocks/vision.ts index 5d56ad1b1..3f43c4c25 100644 --- a/apps/sim/blocks/blocks/vision.ts +++ b/apps/sim/blocks/blocks/vision.ts @@ -14,13 +14,37 @@ export const VisionBlock: BlockConfig = { bgColor: '#4D5FFF', icon: EyeIcon, subBlocks: [ + // Image file upload (basic mode) { - id: 'imageUrl', - title: 'Image URL', + id: 'imageFile', + title: 'Image File', + type: 'file-upload', + layout: 'full', + canonicalParamId: 'imageFile', + placeholder: 'Upload an image file', + mode: 'basic', + multiple: false, + required: false, + acceptedTypes: '.jpg,.jpeg,.png,.gif,.webp', + }, + // Image file reference (advanced mode) + { + id: 'imageFileReference', + title: 'Image File Reference', type: 'short-input', layout: 'full', - placeholder: 'Enter publicly accessible image URL', - required: true, + canonicalParamId: 'imageFile', + placeholder: 'Reference an image from previous blocks', + mode: 'advanced', + required: false, + }, + { + id: 'imageUrl', + title: 'Image URL (alternative)', + type: 'short-input', + layout: 'full', + placeholder: 'Or enter publicly accessible image URL', + required: false, }, { id: 'model', @@ -58,6 +82,8 @@ export const VisionBlock: BlockConfig = { inputs: { apiKey: { type: 'string', description: 'Provider API key' }, imageUrl: { type: 'string', description: 'Image URL' }, + imageFile: { type: 'json', description: 'Image file (UserFile)' }, + imageFileReference: { type: 'json', description: 'Image file reference' }, model: { type: 'string', description: 'Vision model' }, prompt: { type: 'string', description: 'Analysis prompt' }, }, diff --git a/apps/sim/lib/billing/storage/index.ts b/apps/sim/lib/billing/storage/index.ts new file mode 100644 index 000000000..50e10480a --- /dev/null +++ b/apps/sim/lib/billing/storage/index.ts @@ -0,0 +1,2 @@ +export { checkStorageQuota, getUserStorageLimit, getUserStorageUsage } from './limits' +export { decrementStorageUsage, incrementStorageUsage } from './tracking' diff --git a/apps/sim/lib/billing/storage/limits.ts b/apps/sim/lib/billing/storage/limits.ts new file mode 100644 index 000000000..fd7a23455 --- /dev/null +++ b/apps/sim/lib/billing/storage/limits.ts @@ -0,0 +1,190 @@ +/** + * Storage limit management + * Similar to cost limits but for file storage quotas + */ + +import { db } from '@sim/db' +import { + DEFAULT_ENTERPRISE_STORAGE_LIMIT_GB, + DEFAULT_FREE_STORAGE_LIMIT_GB, + DEFAULT_PRO_STORAGE_LIMIT_GB, + DEFAULT_TEAM_STORAGE_LIMIT_GB, +} from '@sim/db/consts' +import { organization, subscription, userStats } from '@sim/db/schema' +import { eq } from 'drizzle-orm' +import { getEnv } from '@/lib/env' +import { createLogger } from '@/lib/logs/console/logger' + +const logger = createLogger('StorageLimits') + +/** + * Convert GB to bytes + */ +function gbToBytes(gb: number): number { + return gb * 1024 * 1024 * 1024 +} + +/** + * Get storage limits from environment variables with fallback to constants + * Returns limits in bytes + */ +export function getStorageLimits() { + return { + free: gbToBytes( + Number.parseInt(getEnv('FREE_STORAGE_LIMIT_GB') || String(DEFAULT_FREE_STORAGE_LIMIT_GB)) + ), + pro: gbToBytes( + Number.parseInt(getEnv('PRO_STORAGE_LIMIT_GB') || String(DEFAULT_PRO_STORAGE_LIMIT_GB)) + ), + team: gbToBytes( + Number.parseInt(getEnv('TEAM_STORAGE_LIMIT_GB') || String(DEFAULT_TEAM_STORAGE_LIMIT_GB)) + ), + enterpriseDefault: gbToBytes( + Number.parseInt( + getEnv('ENTERPRISE_STORAGE_LIMIT_GB') || String(DEFAULT_ENTERPRISE_STORAGE_LIMIT_GB) + ) + ), + } +} + +/** + * Get storage limit for a specific plan + * Returns limit in bytes + */ +export function getStorageLimitForPlan(plan: string, metadata?: any): number { + const limits = getStorageLimits() + + switch (plan) { + case 'free': + return limits.free + case 'pro': + return limits.pro + case 'team': + return limits.team + case 'enterprise': + // Check for custom limit in metadata (stored in GB) + if (metadata?.storageLimitGB) { + return gbToBytes(Number.parseInt(metadata.storageLimitGB)) + } + return limits.enterpriseDefault + default: + return limits.free + } +} + +/** + * Get storage limit for a user based on their subscription + * Returns limit in bytes + */ +export async function getUserStorageLimit(userId: string): Promise { + try { + // Check if user is in a team/enterprise org + const { getHighestPrioritySubscription } = await import('@/lib/billing/core/subscription') + const sub = await getHighestPrioritySubscription(userId) + + const limits = getStorageLimits() + + if (!sub || sub.plan === 'free') { + return limits.free + } + + if (sub.plan === 'pro') { + return limits.pro + } + + // Team/Enterprise: Use organization limit + if (sub.plan === 'team' || sub.plan === 'enterprise') { + // Get organization storage limit + const orgRecord = await db + .select({ metadata: subscription.metadata }) + .from(subscription) + .where(eq(subscription.id, sub.id)) + .limit(1) + + if (orgRecord.length > 0 && orgRecord[0].metadata) { + const metadata = orgRecord[0].metadata as any + if (metadata.customStorageLimitGB) { + return metadata.customStorageLimitGB * 1024 * 1024 * 1024 + } + } + + // Default for team/enterprise + return sub.plan === 'enterprise' ? limits.enterpriseDefault : limits.team + } + + return limits.free + } catch (error) { + logger.error('Error getting user storage limit:', error) + return getStorageLimits().free + } +} + +/** + * Get current storage usage for a user + * Returns usage in bytes + */ +export async function getUserStorageUsage(userId: string): Promise { + try { + // Check if user is in a team/enterprise org + const { getHighestPrioritySubscription } = await import('@/lib/billing/core/subscription') + const sub = await getHighestPrioritySubscription(userId) + + if (sub && (sub.plan === 'team' || sub.plan === 'enterprise')) { + // Use organization storage + const orgRecord = await db + .select({ storageUsedBytes: organization.storageUsedBytes }) + .from(organization) + .where(eq(organization.id, sub.referenceId)) + .limit(1) + + return orgRecord.length > 0 ? orgRecord[0].storageUsedBytes || 0 : 0 + } + + // Free/Pro: Use user stats + const stats = await db + .select({ storageUsedBytes: userStats.storageUsedBytes }) + .from(userStats) + .where(eq(userStats.userId, userId)) + .limit(1) + + return stats.length > 0 ? stats[0].storageUsedBytes || 0 : 0 + } catch (error) { + logger.error('Error getting user storage usage:', error) + return 0 + } +} + +/** + * Check if user has storage quota available + */ +export async function checkStorageQuota( + userId: string, + additionalBytes: number +): Promise<{ allowed: boolean; currentUsage: number; limit: number; error?: string }> { + try { + const [currentUsage, limit] = await Promise.all([ + getUserStorageUsage(userId), + getUserStorageLimit(userId), + ]) + + const newUsage = currentUsage + additionalBytes + const allowed = newUsage <= limit + + return { + allowed, + currentUsage, + limit, + error: allowed + ? undefined + : `Storage limit exceeded. Used: ${(newUsage / (1024 * 1024 * 1024)).toFixed(2)}GB, Limit: ${(limit / (1024 * 1024 * 1024)).toFixed(0)}GB`, + } + } catch (error) { + logger.error('Error checking storage quota:', error) + return { + allowed: false, + currentUsage: 0, + limit: 0, + error: 'Failed to check storage quota', + } + } +} diff --git a/apps/sim/lib/billing/storage/tracking.ts b/apps/sim/lib/billing/storage/tracking.ts new file mode 100644 index 000000000..906aa7eee --- /dev/null +++ b/apps/sim/lib/billing/storage/tracking.ts @@ -0,0 +1,83 @@ +/** + * Storage usage tracking + * Updates storage_used_bytes for users and organizations + */ + +import { db } from '@sim/db' +import { organization, userStats } from '@sim/db/schema' +import { eq, sql } from 'drizzle-orm' +import { createLogger } from '@/lib/logs/console/logger' + +const logger = createLogger('StorageTracking') + +/** + * Increment storage usage after successful file upload + */ +export async function incrementStorageUsage(userId: string, bytes: number): Promise { + try { + // Check if user is in a team/enterprise org + const { getHighestPrioritySubscription } = await import('@/lib/billing/core/subscription') + const sub = await getHighestPrioritySubscription(userId) + + if (sub && (sub.plan === 'team' || sub.plan === 'enterprise')) { + // Update organization storage + await db + .update(organization) + .set({ + storageUsedBytes: sql`${organization.storageUsedBytes} + ${bytes}`, + }) + .where(eq(organization.id, sub.referenceId)) + + logger.info(`Incremented org storage: ${bytes} bytes for org ${sub.referenceId}`) + } else { + // Update user stats storage + await db + .update(userStats) + .set({ + storageUsedBytes: sql`${userStats.storageUsedBytes} + ${bytes}`, + }) + .where(eq(userStats.userId, userId)) + + logger.info(`Incremented user storage: ${bytes} bytes for user ${userId}`) + } + } catch (error) { + logger.error('Error incrementing storage usage:', error) + throw error + } +} + +/** + * Decrement storage usage after file deletion + */ +export async function decrementStorageUsage(userId: string, bytes: number): Promise { + try { + // Check if user is in a team/enterprise org + const { getHighestPrioritySubscription } = await import('@/lib/billing/core/subscription') + const sub = await getHighestPrioritySubscription(userId) + + if (sub && (sub.plan === 'team' || sub.plan === 'enterprise')) { + // Update organization storage + await db + .update(organization) + .set({ + storageUsedBytes: sql`GREATEST(0, ${organization.storageUsedBytes} - ${bytes})`, + }) + .where(eq(organization.id, sub.referenceId)) + + logger.info(`Decremented org storage: ${bytes} bytes for org ${sub.referenceId}`) + } else { + // Update user stats storage + await db + .update(userStats) + .set({ + storageUsedBytes: sql`GREATEST(0, ${userStats.storageUsedBytes} - ${bytes})`, + }) + .where(eq(userStats.userId, userId)) + + logger.info(`Decremented user storage: ${bytes} bytes for user ${userId}`) + } + } catch (error) { + logger.error('Error decrementing storage usage:', error) + throw error + } +} diff --git a/apps/sim/lib/env.ts b/apps/sim/lib/env.ts index eb3ebc852..713d186d8 100644 --- a/apps/sim/lib/env.ts +++ b/apps/sim/lib/env.ts @@ -42,12 +42,16 @@ export const env = createEnv({ STRIPE_WEBHOOK_SECRET: z.string().min(1).optional(), // General Stripe webhook secret STRIPE_FREE_PRICE_ID: z.string().min(1).optional(), // Stripe price ID for free tier FREE_TIER_COST_LIMIT: z.number().optional(), // Cost limit for free tier users + FREE_STORAGE_LIMIT_GB: z.number().optional(), // Storage limit in GB for free tier users (default: 5GB) STRIPE_PRO_PRICE_ID: z.string().min(1).optional(), // Stripe price ID for pro tier PRO_TIER_COST_LIMIT: z.number().optional(), // Cost limit for pro tier users + PRO_STORAGE_LIMIT_GB: z.number().optional(), // Storage limit in GB for pro tier users (default: 50GB) STRIPE_TEAM_PRICE_ID: z.string().min(1).optional(), // Stripe price ID for team tier TEAM_TIER_COST_LIMIT: z.number().optional(), // Cost limit for team tier users + TEAM_STORAGE_LIMIT_GB: z.number().optional(), // Storage limit in GB for team tier organizations (default: 500GB, pooled) STRIPE_ENTERPRISE_PRICE_ID: z.string().min(1).optional(), // Stripe price ID for enterprise tier ENTERPRISE_TIER_COST_LIMIT: z.number().optional(), // Cost limit for enterprise tier users + ENTERPRISE_STORAGE_LIMIT_GB: z.number().optional(), // Default storage limit in GB for enterprise tier (default: 500GB, can be overridden per org) BILLING_ENABLED: z.boolean().optional(), // Enable billing enforcement and usage tracking OVERAGE_THRESHOLD_DOLLARS: z.number().optional().default(50), // Dollar threshold for incremental overage billing (default: $50) diff --git a/apps/sim/lib/file-parsers/pdf-parser.ts b/apps/sim/lib/file-parsers/pdf-parser.ts index 2f19e302f..ad68f3363 100644 --- a/apps/sim/lib/file-parsers/pdf-parser.ts +++ b/apps/sim/lib/file-parsers/pdf-parser.ts @@ -38,8 +38,11 @@ export class PdfParser implements FileParser { pdfData.text.length ) + // Remove null bytes from content (PostgreSQL JSONB doesn't allow them) + const cleanContent = pdfData.text.replace(/\u0000/g, '') + return { - content: pdfData.text, + content: cleanContent, metadata: { pageCount: pdfData.numpages, info: pdfData.info, diff --git a/apps/sim/lib/knowledge/documents/service.ts b/apps/sim/lib/knowledge/documents/service.ts index 9694337cd..5426564c3 100644 --- a/apps/sim/lib/knowledge/documents/service.ts +++ b/apps/sim/lib/knowledge/documents/service.ts @@ -1,8 +1,13 @@ import crypto, { randomUUID } from 'crypto' import { db } from '@sim/db' -import { document, embedding, knowledgeBaseTagDefinitions } from '@sim/db/schema' +import { document, embedding, knowledgeBase, knowledgeBaseTagDefinitions } from '@sim/db/schema' import { tasks } from '@trigger.dev/sdk' import { and, asc, desc, eq, inArray, isNull, sql } from 'drizzle-orm' +import { + checkStorageQuota, + decrementStorageUsage, + incrementStorageUsage, +} from '@/lib/billing/storage' import { generateEmbeddings } from '@/lib/embeddings/utils' import { env } from '@/lib/env' import { getSlotsForFieldType, type TAG_SLOT_CONFIG } from '@/lib/knowledge/consts' @@ -659,8 +664,32 @@ export async function createDocumentRecords( tag7?: string }>, knowledgeBaseId: string, - requestId: string + requestId: string, + userId?: string ): Promise { + // Check storage limits before creating documents + if (userId) { + const totalSize = documents.reduce((sum, doc) => sum + doc.fileSize, 0) + + // Get knowledge base owner + const kb = await db + .select({ userId: knowledgeBase.userId }) + .from(knowledgeBase) + .where(eq(knowledgeBase.id, knowledgeBaseId)) + .limit(1) + + if (kb.length === 0) { + throw new Error('Knowledge base not found') + } + + // Always meter the knowledge base owner + const quotaCheck = await checkStorageQuota(kb[0].userId, totalSize) + + if (!quotaCheck.allowed) { + throw new Error(quotaCheck.error || 'Storage limit exceeded') + } + } + return await db.transaction(async (tx) => { const now = new Date() const documentRecords = [] @@ -728,6 +757,33 @@ export async function createDocumentRecords( logger.info( `[${requestId}] Bulk created ${documentRecords.length} document records in knowledge base ${knowledgeBaseId}` ) + + // Increment storage usage tracking + if (userId) { + const totalSize = documents.reduce((sum, doc) => sum + doc.fileSize, 0) + + // Get knowledge base owner + const kb = await db + .select({ userId: knowledgeBase.userId }) + .from(knowledgeBase) + .where(eq(knowledgeBase.id, knowledgeBaseId)) + .limit(1) + + if (kb.length > 0) { + // Always meter the knowledge base owner + try { + await incrementStorageUsage(kb[0].userId, totalSize) + logger.info( + `[${requestId}] Updated knowledge base owner storage usage for ${totalSize} bytes` + ) + } catch (error) { + logger.error( + `[${requestId}] Failed to update knowledge base owner storage usage:`, + error + ) + } + } + } } return returnData @@ -928,7 +984,8 @@ export async function createSingleDocument( tag7?: string }, knowledgeBaseId: string, - requestId: string + requestId: string, + userId?: string ): Promise<{ id: string knowledgeBaseId: string @@ -949,6 +1006,27 @@ export async function createSingleDocument( tag6: string | null tag7: string | null }> { + // Check storage limits before creating document + if (userId) { + // Get knowledge base owner + const kb = await db + .select({ userId: knowledgeBase.userId }) + .from(knowledgeBase) + .where(eq(knowledgeBase.id, knowledgeBaseId)) + .limit(1) + + if (kb.length === 0) { + throw new Error('Knowledge base not found') + } + + // Always meter the knowledge base owner + const quotaCheck = await checkStorageQuota(kb[0].userId, documentData.fileSize) + + if (!quotaCheck.allowed) { + throw new Error(quotaCheck.error || 'Storage limit exceeded') + } + } + const documentId = randomUUID() const now = new Date() @@ -994,6 +1072,28 @@ export async function createSingleDocument( logger.info(`[${requestId}] Document created: ${documentId} in knowledge base ${knowledgeBaseId}`) + // Increment storage usage tracking + if (userId) { + // Get knowledge base owner + const kb = await db + .select({ userId: knowledgeBase.userId }) + .from(knowledgeBase) + .where(eq(knowledgeBase.id, knowledgeBaseId)) + .limit(1) + + if (kb.length > 0) { + // Always meter the knowledge base owner + try { + await incrementStorageUsage(kb[0].userId, documentData.fileSize) + logger.info( + `[${requestId}] Updated knowledge base owner storage usage for ${documentData.fileSize} bytes` + ) + } catch (error) { + logger.error(`[${requestId}] Failed to update knowledge base owner storage usage:`, error) + } + } + } + return newDocument as { id: string knowledgeBaseId: string @@ -1023,7 +1123,8 @@ export async function bulkDocumentOperation( knowledgeBaseId: string, operation: 'enable' | 'disable' | 'delete', documentIds: string[], - requestId: string + requestId: string, + userId?: string ): Promise<{ success: boolean successCount: number @@ -1071,6 +1172,23 @@ export async function bulkDocumentOperation( }> if (operation === 'delete') { + // Get file sizes before deletion for storage tracking + let totalSize = 0 + if (userId) { + const documentsToDelete = await db + .select({ fileSize: document.fileSize }) + .from(document) + .where( + and( + eq(document.knowledgeBaseId, knowledgeBaseId), + inArray(document.id, documentIds), + isNull(document.deletedAt) + ) + ) + + totalSize = documentsToDelete.reduce((sum, doc) => sum + doc.fileSize, 0) + } + // Handle bulk soft delete updateResult = await db .update(document) @@ -1085,6 +1203,28 @@ export async function bulkDocumentOperation( ) ) .returning({ id: document.id, deletedAt: document.deletedAt }) + + // Decrement storage usage tracking + if (userId && totalSize > 0) { + // Get knowledge base owner + const kb = await db + .select({ userId: knowledgeBase.userId }) + .from(knowledgeBase) + .where(eq(knowledgeBase.id, knowledgeBaseId)) + .limit(1) + + if (kb.length > 0) { + // Always meter the knowledge base owner + try { + await decrementStorageUsage(kb[0].userId, totalSize) + logger.info( + `[${requestId}] Updated knowledge base owner storage usage for -${totalSize} bytes` + ) + } catch (error) { + logger.error(`[${requestId}] Failed to update knowledge base owner storage usage:`, error) + } + } + } } else { // Handle bulk enable/disable const enabled = operation === 'enable' diff --git a/apps/sim/lib/uploads/file-processing.ts b/apps/sim/lib/uploads/file-processing.ts new file mode 100644 index 000000000..46e680a9f --- /dev/null +++ b/apps/sim/lib/uploads/file-processing.ts @@ -0,0 +1,103 @@ +import type { Logger } from '@/lib/logs/console/logger' +import { extractStorageKey } from '@/lib/uploads/file-utils' +import { downloadFile } from '@/lib/uploads/storage-client' +import { downloadExecutionFile } from '@/lib/workflows/execution-file-storage' +import { isExecutionFile } from '@/lib/workflows/execution-files' +import type { UserFile } from '@/executor/types' + +/** + * Converts a single raw file object to UserFile format + * @param file - Raw file object + * @param requestId - Request ID for logging + * @param logger - Logger instance + * @returns UserFile object + * @throws Error if file has no storage key + */ +export function processSingleFileToUserFile( + file: any, + requestId: string, + logger: Logger +): UserFile { + // Already a UserFile (from variable reference) + if (file.id && file.key && file.uploadedAt) { + return file as UserFile + } + + // Extract storage key from path or key property + const storageKey = file.key || (file.path ? extractStorageKey(file.path) : null) + + if (!storageKey) { + logger.warn(`[${requestId}] File has no storage key: ${file.name || 'unknown'}`) + throw new Error(`File has no storage key: ${file.name || 'unknown'}`) + } + + const userFile: UserFile = { + id: file.id || `file-${Date.now()}`, + name: file.name, + url: file.url || file.path, + size: file.size, + type: file.type || 'application/octet-stream', + key: storageKey, + uploadedAt: file.uploadedAt || new Date().toISOString(), + expiresAt: file.expiresAt || new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), + } + + logger.info(`[${requestId}] Converted file to UserFile: ${userFile.name} (key: ${userFile.key})`) + return userFile +} + +/** + * Converts raw file objects (from file-upload or variable references) to UserFile format + * @param files - Array of raw file objects + * @param requestId - Request ID for logging + * @param logger - Logger instance + * @returns Array of UserFile objects + */ +export function processFilesToUserFiles( + files: any[], + requestId: string, + logger: Logger +): UserFile[] { + const userFiles: UserFile[] = [] + + for (const file of files) { + try { + const userFile = processSingleFileToUserFile(file, requestId, logger) + userFiles.push(userFile) + } catch (error) { + // Log and skip files that can't be processed + logger.warn( + `[${requestId}] Skipping file: ${error instanceof Error ? error.message : 'Unknown error'}` + ) + } + } + + return userFiles +} + +/** + * Downloads a file from storage (execution or regular) + * @param userFile - UserFile object + * @param requestId - Request ID for logging + * @param logger - Logger instance + * @returns Buffer containing file data + */ +export async function downloadFileFromStorage( + userFile: UserFile, + requestId: string, + logger: Logger +): Promise { + let buffer: Buffer + + if (isExecutionFile(userFile)) { + logger.info(`[${requestId}] Downloading from execution storage: ${userFile.key}`) + buffer = await downloadExecutionFile(userFile) + } else if (userFile.key) { + logger.info(`[${requestId}] Downloading from regular storage: ${userFile.key}`) + buffer = await downloadFile(userFile.key) + } else { + throw new Error('File has no key - cannot download') + } + + return buffer +} diff --git a/apps/sim/lib/uploads/file-utils.ts b/apps/sim/lib/uploads/file-utils.ts index a924fbacc..2c02ba134 100644 --- a/apps/sim/lib/uploads/file-utils.ts +++ b/apps/sim/lib/uploads/file-utils.ts @@ -142,3 +142,20 @@ export function getMimeTypeFromExtension(extension: string): string { return extensionMimeMap[extension.toLowerCase()] || 'application/octet-stream' } + +/** + * Extract storage key from a file path + * Handles various path formats: /api/files/serve/xyz, /api/files/serve/s3/xyz, etc. + */ +export function extractStorageKey(filePath: string): string { + if (filePath.includes('/api/files/serve/s3/')) { + return decodeURIComponent(filePath.split('/api/files/serve/s3/')[1]) + } + if (filePath.includes('/api/files/serve/blob/')) { + return decodeURIComponent(filePath.split('/api/files/serve/blob/')[1]) + } + if (filePath.startsWith('/api/files/serve/')) { + return decodeURIComponent(filePath.substring('/api/files/serve/'.length)) + } + return filePath +} diff --git a/apps/sim/lib/uploads/workspace-files.ts b/apps/sim/lib/uploads/workspace-files.ts new file mode 100644 index 000000000..2f3145e60 --- /dev/null +++ b/apps/sim/lib/uploads/workspace-files.ts @@ -0,0 +1,303 @@ +/** + * Workspace file storage system + * Files uploaded at workspace level persist indefinitely and are accessible across all workflows + */ + +import { db } from '@sim/db' +import { workspaceFile } from '@sim/db/schema' +import { and, eq } from 'drizzle-orm' +import { + checkStorageQuota, + decrementStorageUsage, + incrementStorageUsage, +} from '@/lib/billing/storage' +import { createLogger } from '@/lib/logs/console/logger' +import { deleteFile, downloadFile } from '@/lib/uploads/storage-client' +import type { UserFile } from '@/executor/types' + +const logger = createLogger('WorkspaceFileStorage') + +export interface WorkspaceFileRecord { + id: string + workspaceId: string + name: string + key: string + path: string // Full serve path including storage type + url?: string // Presigned URL for external access (optional, regenerated as needed) + size: number + type: string + uploadedBy: string + uploadedAt: Date +} + +/** + * Generate workspace-scoped storage key + * Pattern: {workspaceId}/{timestamp}-{filename} + */ +export function generateWorkspaceFileKey(workspaceId: string, fileName: string): string { + const timestamp = Date.now() + const random = Math.random().toString(36).substring(2, 9) + const safeFileName = fileName.replace(/\s+/g, '-').replace(/[^a-zA-Z0-9.-]/g, '_') + return `${workspaceId}/${timestamp}-${random}-${safeFileName}` +} + +/** + * Upload a file to workspace-scoped storage + */ +export async function uploadWorkspaceFile( + workspaceId: string, + userId: string, + fileBuffer: Buffer, + fileName: string, + contentType: string +): Promise { + logger.info(`Uploading workspace file: ${fileName} for workspace ${workspaceId}`) + + // Check for duplicates + const exists = await fileExistsInWorkspace(workspaceId, fileName) + if (exists) { + throw new Error(`A file named "${fileName}" already exists in this workspace`) + } + + // Check storage quota + const quotaCheck = await checkStorageQuota(userId, fileBuffer.length) + + if (!quotaCheck.allowed) { + throw new Error(quotaCheck.error || 'Storage limit exceeded') + } + + // Generate workspace-scoped storage key + const storageKey = generateWorkspaceFileKey(workspaceId, fileName) + const fileId = `wf_${Date.now()}_${Math.random().toString(36).substring(2, 9)}` + + try { + let uploadResult: any + + logger.info(`Generated storage key: ${storageKey}`) + + // Upload to storage with skipTimestampPrefix to use exact key + const { USE_S3_STORAGE, USE_BLOB_STORAGE, S3_CONFIG, BLOB_CONFIG } = await import( + '@/lib/uploads/setup' + ) + + if (USE_S3_STORAGE) { + const { uploadToS3 } = await import('@/lib/uploads/s3/s3-client') + // Use custom config overload with skipTimestampPrefix + uploadResult = await uploadToS3( + fileBuffer, + storageKey, + contentType, + { + bucket: S3_CONFIG.bucket, + region: S3_CONFIG.region, + }, + fileBuffer.length, + true // skipTimestampPrefix = true + ) + } else if (USE_BLOB_STORAGE) { + const { uploadToBlob } = await import('@/lib/uploads/blob/blob-client') + // Blob doesn't have skipTimestampPrefix, but we pass the full key + uploadResult = await uploadToBlob( + fileBuffer, + storageKey, + contentType, + { + accountName: BLOB_CONFIG.accountName, + accountKey: BLOB_CONFIG.accountKey, + connectionString: BLOB_CONFIG.connectionString, + containerName: BLOB_CONFIG.containerName, + }, + fileBuffer.length + ) + } else { + throw new Error('No storage provider configured') + } + + logger.info(`S3/Blob upload returned key: ${uploadResult.key}`) + logger.info(`Keys match: ${uploadResult.key === storageKey}`) + + // Store metadata in database - use the EXACT key from upload result + await db.insert(workspaceFile).values({ + id: fileId, + workspaceId, + name: fileName, + key: uploadResult.key, // This is what actually got stored in S3 + size: fileBuffer.length, + type: contentType, + uploadedBy: userId, + uploadedAt: new Date(), + }) + + logger.info(`Successfully uploaded workspace file: ${fileName} with key: ${uploadResult.key}`) + + // Increment storage usage tracking + try { + await incrementStorageUsage(userId, fileBuffer.length) + } catch (storageError) { + logger.error(`Failed to update storage tracking:`, storageError) + // Continue - don't fail upload if tracking fails + } + + // Generate presigned URL (valid for 24 hours for initial access) + const { getPresignedUrl } = await import('@/lib/uploads') + let presignedUrl: string | undefined + + try { + presignedUrl = await getPresignedUrl(uploadResult.key, 24 * 60 * 60) // 24 hours + } catch (error) { + logger.warn(`Failed to generate presigned URL for ${fileName}:`, error) + } + + // Return UserFile format (no expiry for workspace files) + return { + id: fileId, + name: fileName, + size: fileBuffer.length, + type: contentType, + url: presignedUrl || uploadResult.path, // Use presigned URL for external access + key: uploadResult.key, + uploadedAt: new Date().toISOString(), + expiresAt: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString(), // 1 year + } + } catch (error) { + logger.error(`Failed to upload workspace file ${fileName}:`, error) + throw new Error( + `Failed to upload file: ${error instanceof Error ? error.message : 'Unknown error'}` + ) + } +} + +/** + * Check if a file with the same name already exists in workspace + */ +export async function fileExistsInWorkspace( + workspaceId: string, + fileName: string +): Promise { + try { + const existing = await db + .select() + .from(workspaceFile) + .where(and(eq(workspaceFile.workspaceId, workspaceId), eq(workspaceFile.name, fileName))) + .limit(1) + + return existing.length > 0 + } catch (error) { + logger.error(`Failed to check file existence for ${fileName}:`, error) + return false + } +} + +/** + * List all files for a workspace + */ +export async function listWorkspaceFiles(workspaceId: string): Promise { + try { + const files = await db + .select() + .from(workspaceFile) + .where(eq(workspaceFile.workspaceId, workspaceId)) + .orderBy(workspaceFile.uploadedAt) + + // Add full serve path for each file (don't generate presigned URLs here) + const { getServePathPrefix } = await import('@/lib/uploads') + const pathPrefix = getServePathPrefix() + + return files.map((file) => ({ + ...file, + path: `${pathPrefix}${encodeURIComponent(file.key)}`, + // url will be generated on-demand during execution for external APIs + })) + } catch (error) { + logger.error(`Failed to list workspace files for ${workspaceId}:`, error) + return [] + } +} + +/** + * Get a specific workspace file + */ +export async function getWorkspaceFile( + workspaceId: string, + fileId: string +): Promise { + try { + const files = await db + .select() + .from(workspaceFile) + .where(and(eq(workspaceFile.id, fileId), eq(workspaceFile.workspaceId, workspaceId))) + .limit(1) + + if (files.length === 0) return null + + // Add full serve path + const { getServePathPrefix } = await import('@/lib/uploads') + const pathPrefix = getServePathPrefix() + + return { + ...files[0], + path: `${pathPrefix}${encodeURIComponent(files[0].key)}`, + } + } catch (error) { + logger.error(`Failed to get workspace file ${fileId}:`, error) + return null + } +} + +/** + * Download workspace file content + */ +export async function downloadWorkspaceFile(fileRecord: WorkspaceFileRecord): Promise { + logger.info(`Downloading workspace file: ${fileRecord.name}`) + + try { + const buffer = await downloadFile(fileRecord.key) + logger.info( + `Successfully downloaded workspace file: ${fileRecord.name} (${buffer.length} bytes)` + ) + return buffer + } catch (error) { + logger.error(`Failed to download workspace file ${fileRecord.name}:`, error) + throw new Error( + `Failed to download file: ${error instanceof Error ? error.message : 'Unknown error'}` + ) + } +} + +/** + * Delete a workspace file (both from storage and database) + */ +export async function deleteWorkspaceFile(workspaceId: string, fileId: string): Promise { + logger.info(`Deleting workspace file: ${fileId}`) + + try { + // Get file record first + const fileRecord = await getWorkspaceFile(workspaceId, fileId) + if (!fileRecord) { + throw new Error('File not found') + } + + // Delete from storage + await deleteFile(fileRecord.key) + + // Delete from database + await db + .delete(workspaceFile) + .where(and(eq(workspaceFile.id, fileId), eq(workspaceFile.workspaceId, workspaceId))) + + // Decrement storage usage tracking + try { + await decrementStorageUsage(fileRecord.uploadedBy, fileRecord.size) + } catch (storageError) { + logger.error(`Failed to update storage tracking:`, storageError) + // Continue - don't fail deletion if tracking fails + } + + logger.info(`Successfully deleted workspace file: ${fileRecord.name}`) + } catch (error) { + logger.error(`Failed to delete workspace file ${fileId}:`, error) + throw new Error( + `Failed to delete file: ${error instanceof Error ? error.message : 'Unknown error'}` + ) + } +} diff --git a/apps/sim/lib/webhooks/gmail-polling-service.ts b/apps/sim/lib/webhooks/gmail-polling-service.ts index c32d3fcc8..e64d32f87 100644 --- a/apps/sim/lib/webhooks/gmail-polling-service.ts +++ b/apps/sim/lib/webhooks/gmail-polling-service.ts @@ -15,6 +15,7 @@ interface GmailWebhookConfig { labelIds: string[] labelFilterBehavior: 'INCLUDE' | 'EXCLUDE' markAsRead: boolean + searchQuery?: string maxEmailsPerPoll?: number lastCheckedTimestamp?: string historyId?: string @@ -308,13 +309,53 @@ async function fetchNewEmails(accessToken: string, config: GmailWebhookConfig, r } } +/** + * Builds a Gmail search query from label and search configuration + */ +function buildGmailSearchQuery(config: { + labelIds?: string[] + labelFilterBehavior?: 'INCLUDE' | 'EXCLUDE' + searchQuery?: string +}): string { + let labelQuery = '' + if (config.labelIds && config.labelIds.length > 0) { + const labelParts = config.labelIds.map((label) => `label:${label}`).join(' OR ') + labelQuery = + config.labelFilterBehavior === 'INCLUDE' + ? config.labelIds.length > 1 + ? `(${labelParts})` + : labelParts + : config.labelIds.length > 1 + ? `-(${labelParts})` + : `-${labelParts}` + } + + let searchQueryPart = '' + if (config.searchQuery?.trim()) { + searchQueryPart = config.searchQuery.trim() + if (searchQueryPart.includes(' OR ') || searchQueryPart.includes(' AND ')) { + searchQueryPart = `(${searchQueryPart})` + } + } + + let baseQuery = '' + if (labelQuery && searchQueryPart) { + baseQuery = `${labelQuery} ${searchQueryPart}` + } else if (searchQueryPart) { + baseQuery = searchQueryPart + } else if (labelQuery) { + baseQuery = labelQuery + } else { + baseQuery = 'in:inbox' + } + + return baseQuery +} + async function searchEmails(accessToken: string, config: GmailWebhookConfig, requestId: string) { try { - // Build query parameters for label filtering - const labelQuery = - config.labelIds && config.labelIds.length > 0 - ? config.labelIds.map((label) => `label:${label}`).join(' ') - : 'in:inbox' + const baseQuery = buildGmailSearchQuery(config) + logger.debug(`[${requestId}] Gmail search query: ${baseQuery}`) // Improved time-based filtering with dynamic buffer let timeConstraint = '' @@ -363,11 +404,8 @@ async function searchEmails(accessToken: string, config: GmailWebhookConfig, req logger.debug(`[${requestId}] No last check time, using default: newer_than:1d`) } - // Combine label and time constraints - const query = - config.labelFilterBehavior === 'INCLUDE' - ? `${labelQuery}${timeConstraint}` - : `-${labelQuery}${timeConstraint}` + // Combine base query and time constraints + const query = `${baseQuery}${timeConstraint}` logger.info(`[${requestId}] Searching for emails with query: ${query}`) diff --git a/apps/sim/lib/workflows/execution-files.ts b/apps/sim/lib/workflows/execution-files.ts index 7f6fdd60f..ecfc97f58 100644 --- a/apps/sim/lib/workflows/execution-files.ts +++ b/apps/sim/lib/workflows/execution-files.ts @@ -58,3 +58,19 @@ export function isFileExpired(userFile: UserFile): boolean { export function getFileExpirationDate(): string { return new Date(Date.now() + 5 * 60 * 1000).toISOString() } + +/** + * Check if a file is from execution storage based on its key pattern + * Execution files have keys in format: workspaceId/workflowId/executionId/filename + * Regular files have keys in format: timestamp-random-filename or just filename + */ +export function isExecutionFile(file: UserFile): boolean { + if (!file.key) { + return false + } + + // Execution files have at least 3 slashes in their key (4 parts) + // e.g., "workspace123/workflow456/execution789/document.pdf" + const parts = file.key.split('/') + return parts.length >= 4 && !file.key.startsWith('/api/') && !file.key.startsWith('http') +} diff --git a/apps/sim/tools/discord/send_message.ts b/apps/sim/tools/discord/send_message.ts index ef6efd827..d6ec496f6 100644 --- a/apps/sim/tools/discord/send_message.ts +++ b/apps/sim/tools/discord/send_message.ts @@ -1,8 +1,4 @@ -import type { - DiscordMessage, - DiscordSendMessageParams, - DiscordSendMessageResponse, -} from '@/tools/discord/types' +import type { DiscordSendMessageParams, DiscordSendMessageResponse } from '@/tools/discord/types' import type { ToolConfig } from '@/tools/types' export const discordSendMessageTool: ToolConfig< @@ -39,46 +35,38 @@ export const discordSendMessageTool: ToolConfig< visibility: 'user-only', description: 'The Discord server ID (guild ID)', }, + files: { + type: 'file[]', + required: false, + visibility: 'user-only', + description: 'Files to attach to the message', + }, }, request: { - url: (params: DiscordSendMessageParams) => - `https://discord.com/api/v10/channels/${params.channelId}/messages`, + url: '/api/tools/discord/send-message', method: 'POST', - headers: (params: DiscordSendMessageParams) => { - const headers: Record = { - 'Content-Type': 'application/json', - } - - if (params.botToken) { - headers.Authorization = `Bot ${params.botToken}` - } - - return headers - }, + headers: () => ({ + 'Content-Type': 'application/json', + }), body: (params: DiscordSendMessageParams) => { - const body: Record = {} - - if (params.content) { - body.content = params.content + return { + botToken: params.botToken, + channelId: params.channelId, + content: params.content || 'Message sent from Sim', + files: params.files || null, } - - if (!body.content) { - body.content = 'Message sent from Sim' - } - - return body }, }, transformResponse: async (response) => { - const data = (await response.json()) as DiscordMessage + const data = await response.json() + if (!data.success) { + throw new Error(data.error || 'Failed to send Discord message') + } return { success: true, - output: { - message: 'Discord message sent successfully', - data, - }, + output: data.output, } }, diff --git a/apps/sim/tools/discord/types.ts b/apps/sim/tools/discord/types.ts index ff94b5099..835fe431f 100644 --- a/apps/sim/tools/discord/types.ts +++ b/apps/sim/tools/discord/types.ts @@ -58,6 +58,7 @@ export interface DiscordSendMessageParams extends DiscordAuthParams { description?: string color?: string | number } + files?: any[] } export interface DiscordGetMessagesParams extends DiscordAuthParams { diff --git a/apps/sim/tools/file/parser.ts b/apps/sim/tools/file/parser.ts index a183c5f84..785fbd63a 100644 --- a/apps/sim/tools/file/parser.ts +++ b/apps/sim/tools/file/parser.ts @@ -85,6 +85,7 @@ export const fileParserTool: ToolConfig = { return { filePath: determinedFilePath, fileType: determinedFileType, + workspaceId: params.workspaceId || params._context?.workspaceId, } }, }, @@ -119,11 +120,6 @@ export const fileParserTool: ToolConfig = { combinedContent, } - // Add named properties for each file for dropdown access - fileResults.forEach((file: FileParseResult, index: number) => { - output[`file${index + 1}`] = file - }) - return { success: true, output, @@ -133,11 +129,10 @@ export const fileParserTool: ToolConfig = { // Handle single file response logger.info('Successfully parsed file:', result.output?.name || 'unknown') - // For a single file, create the output with both array and named property + // For a single file, create the output with just array format const output: FileParserOutputData = { files: [result.output || result], combinedContent: result.output?.content || result.content || '', - file1: result.output || result, } return { diff --git a/apps/sim/tools/gmail/draft.ts b/apps/sim/tools/gmail/draft.ts index a2e7cc93f..2148016c2 100644 --- a/apps/sim/tools/gmail/draft.ts +++ b/apps/sim/tools/gmail/draft.ts @@ -1,8 +1,6 @@ import type { GmailSendParams, GmailToolResponse } from '@/tools/gmail/types' import type { ToolConfig } from '@/tools/types' -const GMAIL_API_BASE = 'https://gmail.googleapis.com/gmail/v1/users/me' - export const gmailDraftTool: ToolConfig = { id: 'gmail_draft', name: 'Gmail Draft', @@ -52,55 +50,50 @@ export const gmailDraftTool: ToolConfig = { visibility: 'user-or-llm', description: 'BCC recipients (comma-separated)', }, + attachments: { + type: 'file[]', + required: false, + visibility: 'user-only', + description: 'Files to attach to the email draft', + }, }, request: { - url: () => `${GMAIL_API_BASE}/drafts`, + url: '/api/tools/gmail/draft', method: 'POST', - headers: (params: GmailSendParams) => ({ - Authorization: `Bearer ${params.accessToken}`, + headers: () => ({ 'Content-Type': 'application/json', }), - body: (params: GmailSendParams): Record => { - const emailHeaders = [ - 'Content-Type: text/plain; charset="UTF-8"', - 'MIME-Version: 1.0', - `To: ${params.to}`, - ] - - if (params.cc) { - emailHeaders.push(`Cc: ${params.cc}`) - } - if (params.bcc) { - emailHeaders.push(`Bcc: ${params.bcc}`) - } - - emailHeaders.push(`Subject: ${params.subject}`, '', params.body) - const email = emailHeaders.join('\n') - - return { - message: { - raw: Buffer.from(email).toString('base64url'), - }, - } - }, + body: (params: GmailSendParams) => ({ + accessToken: params.accessToken, + to: params.to, + subject: params.subject, + body: params.body, + cc: params.cc, + bcc: params.bcc, + attachments: params.attachments, + }), }, transformResponse: async (response) => { const data = await response.json() + if (!data.success) { + return { + success: false, + output: { + content: data.error || 'Failed to create draft', + metadata: {}, + }, + error: data.error, + } + } + return { success: true, output: { - content: 'Email drafted successfully', - metadata: { - id: data.id, - message: { - id: data.message?.id, - threadId: data.message?.threadId, - labelIds: data.message?.labelIds, - }, - }, + content: data.output.content, + metadata: data.output.metadata, }, } }, diff --git a/apps/sim/tools/gmail/send.ts b/apps/sim/tools/gmail/send.ts index 1102d5f87..aceb7944c 100644 --- a/apps/sim/tools/gmail/send.ts +++ b/apps/sim/tools/gmail/send.ts @@ -1,5 +1,4 @@ import type { GmailSendParams, GmailToolResponse } from '@/tools/gmail/types' -import { GMAIL_API_BASE } from '@/tools/gmail/utils' import type { ToolConfig } from '@/tools/types' export const gmailSendTool: ToolConfig = { @@ -51,50 +50,50 @@ export const gmailSendTool: ToolConfig = { visibility: 'user-or-llm', description: 'BCC recipients (comma-separated)', }, + attachments: { + type: 'file[]', + required: false, + visibility: 'user-only', + description: 'Files to attach to the email', + }, }, request: { - url: () => `${GMAIL_API_BASE}/messages/send`, + url: '/api/tools/gmail/send', method: 'POST', - headers: (params: GmailSendParams) => ({ - Authorization: `Bearer ${params.accessToken}`, + headers: () => ({ 'Content-Type': 'application/json', }), - body: (params: GmailSendParams): Record => { - const emailHeaders = [ - 'Content-Type: text/plain; charset="UTF-8"', - 'MIME-Version: 1.0', - `To: ${params.to}`, - ] - - if (params.cc) { - emailHeaders.push(`Cc: ${params.cc}`) - } - if (params.bcc) { - emailHeaders.push(`Bcc: ${params.bcc}`) - } - - emailHeaders.push(`Subject: ${params.subject}`, '', params.body) - const email = emailHeaders.join('\n') - - return { - raw: Buffer.from(email).toString('base64url'), - } - }, + body: (params: GmailSendParams) => ({ + accessToken: params.accessToken, + to: params.to, + subject: params.subject, + body: params.body, + cc: params.cc, + bcc: params.bcc, + attachments: params.attachments, + }), }, transformResponse: async (response) => { const data = await response.json() + if (!data.success) { + return { + success: false, + output: { + content: data.error || 'Failed to send email', + metadata: {}, + }, + error: data.error, + } + } + return { success: true, output: { - content: 'Email sent successfully', - metadata: { - id: data.id, - threadId: data.threadId, - labelIds: data.labelIds, - }, + content: data.output.content, + metadata: data.output.metadata, }, } }, diff --git a/apps/sim/tools/gmail/types.ts b/apps/sim/tools/gmail/types.ts index 4115f7ea2..590cd549c 100644 --- a/apps/sim/tools/gmail/types.ts +++ b/apps/sim/tools/gmail/types.ts @@ -1,3 +1,4 @@ +import type { UserFile } from '@/executor/types' import type { ToolResponse } from '@/tools/types' // Base parameters shared by all operations @@ -12,6 +13,7 @@ export interface GmailSendParams extends BaseGmailParams { bcc?: string subject: string body: string + attachments?: UserFile[] } // Read operation parameters diff --git a/apps/sim/tools/gmail/utils.ts b/apps/sim/tools/gmail/utils.ts index 7ac87bfe6..36a58a9a4 100644 --- a/apps/sim/tools/gmail/utils.ts +++ b/apps/sim/tools/gmail/utils.ts @@ -238,3 +238,91 @@ export function createMessagesSummary(messages: any[]): string { return summary } + +/** + * Generate a unique MIME boundary string + */ +function generateBoundary(): string { + return `----=_Part_${Date.now()}_${Math.random().toString(36).substring(2, 15)}` +} + +/** + * Encode string or buffer to base64url format (URL-safe base64) + * Gmail API requires base64url encoding for the raw message field + */ +export function base64UrlEncode(data: string | Buffer): string { + const base64 = Buffer.from(data).toString('base64') + return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') +} + +/** + * Build a MIME multipart message with optional attachments + * @param params Message parameters including recipients, subject, body, and attachments + * @returns Complete MIME message string ready to be base64url encoded + */ +export interface BuildMimeMessageParams { + to: string + cc?: string + bcc?: string + subject: string + body: string + attachments?: Array<{ + filename: string + mimeType: string + content: Buffer + }> +} + +export function buildMimeMessage(params: BuildMimeMessageParams): string { + const { to, cc, bcc, subject, body, attachments } = params + const boundary = generateBoundary() + const messageParts: string[] = [] + + // Add headers + messageParts.push(`To: ${to}`) + if (cc) { + messageParts.push(`Cc: ${cc}`) + } + if (bcc) { + messageParts.push(`Bcc: ${bcc}`) + } + messageParts.push(`Subject: ${subject}`) + messageParts.push('MIME-Version: 1.0') + + if (attachments && attachments.length > 0) { + // Multipart message with attachments + messageParts.push(`Content-Type: multipart/mixed; boundary="${boundary}"`) + messageParts.push('') + messageParts.push(`--${boundary}`) + messageParts.push('Content-Type: text/plain; charset="UTF-8"') + messageParts.push('Content-Transfer-Encoding: 7bit') + messageParts.push('') + messageParts.push(body) + messageParts.push('') + + // Add each attachment + for (const attachment of attachments) { + messageParts.push(`--${boundary}`) + messageParts.push(`Content-Type: ${attachment.mimeType}`) + messageParts.push(`Content-Disposition: attachment; filename="${attachment.filename}"`) + messageParts.push('Content-Transfer-Encoding: base64') + messageParts.push('') + + // Split base64 content into 76-character lines (MIME standard) + const base64Content = attachment.content.toString('base64') + const lines = base64Content.match(/.{1,76}/g) || [] + messageParts.push(...lines) + messageParts.push('') + } + + messageParts.push(`--${boundary}--`) + } else { + // Simple text message without attachments + messageParts.push('Content-Type: text/plain; charset="UTF-8"') + messageParts.push('MIME-Version: 1.0') + messageParts.push('') + messageParts.push(body) + } + + return messageParts.join('\n') +} diff --git a/apps/sim/tools/google_drive/types.ts b/apps/sim/tools/google_drive/types.ts index e497781a0..c9e7cb1c6 100644 --- a/apps/sim/tools/google_drive/types.ts +++ b/apps/sim/tools/google_drive/types.ts @@ -38,6 +38,7 @@ export interface GoogleDriveToolParams { folderSelector?: string fileId?: string fileName?: string + file?: any // UserFile object content?: string mimeType?: string query?: string diff --git a/apps/sim/tools/google_drive/upload.ts b/apps/sim/tools/google_drive/upload.ts index e2bd487f6..f771735b7 100644 --- a/apps/sim/tools/google_drive/upload.ts +++ b/apps/sim/tools/google_drive/upload.ts @@ -34,17 +34,23 @@ export const uploadTool: ToolConfig { + // Use custom API route if file is provided, otherwise use Google Drive API directly + if (params.file) { + return '/api/tools/google_drive/upload' + } + return 'https://www.googleapis.com/drive/v3/files?supportsAllDrives=true' + }, method: 'POST', - headers: (params) => ({ - Authorization: `Bearer ${params.accessToken}`, - 'Content-Type': 'application/json', - }), + headers: (params) => { + const headers: Record = { + 'Content-Type': 'application/json', + } + // Google Drive API for text-only uploads needs Authorization + if (!params.file) { + headers.Authorization = `Bearer ${params.accessToken}` + } + return headers + }, body: (params) => { + // Custom route handles file uploads + if (params.file) { + return { + accessToken: params.accessToken, + fileName: params.fileName, + file: params.file, + mimeType: params.mimeType, + folderId: params.folderSelector || params.folderId, + } + } + + // Original text-only upload logic const metadata: { name: string | undefined mimeType: string @@ -91,6 +121,23 @@ export const uploadTool: ToolConfig 0) { + return '/api/tools/microsoft_teams/write_channel' + } + const encodedTeamId = encodeURIComponent(teamId) const encodedChannelId = encodeURIComponent(channelId) @@ -87,6 +98,17 @@ export const writeChannelTool: ToolConfig 0) { + return { + accessToken: params.accessToken, + teamId: params.teamId, + channelId: params.channelId, + content: params.content, + files: params.files, + } + } + // Microsoft Teams API expects this specific format for channel messages const requestBody = { body: { @@ -101,7 +123,12 @@ export const writeChannelTool: ToolConfig { const data = await response.json() - // Create document metadata from the response + // Handle custom API route response format + if (data.success !== undefined && data.output) { + return data + } + + // Handle direct Graph API response format const metadata = { messageId: data.id || '', teamId: data.channelIdentity?.teamId || '', diff --git a/apps/sim/tools/microsoft_teams/write_chat.ts b/apps/sim/tools/microsoft_teams/write_chat.ts index 32d85f7b4..203b60453 100644 --- a/apps/sim/tools/microsoft_teams/write_chat.ts +++ b/apps/sim/tools/microsoft_teams/write_chat.ts @@ -32,6 +32,12 @@ export const writeChatTool: ToolConfig 0) { + return '/api/tools/microsoft_teams/write_chat' + } + return `https://graph.microsoft.com/v1.0/chats/${encodeURIComponent(chatId)}/messages` }, method: 'POST', @@ -71,6 +82,16 @@ export const writeChatTool: ToolConfig 0) { + return { + accessToken: params.accessToken, + chatId: params.chatId, + content: params.content, + files: params.files, + } + } + // Microsoft Teams API expects this specific format const requestBody = { body: { @@ -85,7 +106,12 @@ export const writeChatTool: ToolConfig { const data = await response.json() - // Create document metadata from the response + // Handle custom API route response format + if (data.success !== undefined && data.output) { + return data + } + + // Handle direct Graph API response format const metadata = { messageId: data.id || '', chatId: data.chatId || '', diff --git a/apps/sim/tools/mistral/parser.ts b/apps/sim/tools/mistral/parser.ts index 4572b099a..61874df7b 100644 --- a/apps/sim/tools/mistral/parser.ts +++ b/apps/sim/tools/mistral/parser.ts @@ -65,7 +65,7 @@ export const mistralParserTool: ToolConfig { return { @@ -168,11 +168,14 @@ export const mistralParserTool: ToolConfig = { - model: 'mistral-ocr-latest', - document: { - type: 'document_url', - document_url: url.toString(), - }, + apiKey: params.apiKey, + filePath: url.toString(), + } + + // Check if this is an internal workspace file path + if (params.fileUpload?.path?.startsWith('/api/files/serve/')) { + // Update filePath to the internal path for workspace files + requestBody.filePath = params.fileUpload.path } // Add optional parameters with proper validation @@ -181,7 +184,7 @@ export const mistralParserTool: ToolConfig 0) { - requestBody.image_limit = imageLimit + requestBody.imageLimit = imageLimit } else { logger.warn('imageLimit must be a positive integer, ignoring this parameter') } @@ -223,7 +226,7 @@ export const mistralParserTool: ToolConfig 0) { - requestBody.image_min_size = imageMinSize + requestBody.imageMinSize = imageMinSize } else { logger.warn('imageMinSize must be a positive integer, ignoring this parameter') } diff --git a/apps/sim/tools/mistral/types.ts b/apps/sim/tools/mistral/types.ts index b07601e3a..e179de87b 100644 --- a/apps/sim/tools/mistral/types.ts +++ b/apps/sim/tools/mistral/types.ts @@ -10,6 +10,9 @@ export interface MistralParserInput { /** File upload data (from file-upload component) */ fileUpload?: any + /** Internal file path flag (for presigned URL conversion) */ + _internalFilePath?: string + /** Mistral API key for authentication */ apiKey: string diff --git a/apps/sim/tools/onedrive/types.ts b/apps/sim/tools/onedrive/types.ts index 9c1c2654d..e58e18d7e 100644 --- a/apps/sim/tools/onedrive/types.ts +++ b/apps/sim/tools/onedrive/types.ts @@ -53,6 +53,7 @@ export interface OneDriveToolParams { folderName?: string fileId?: string fileName?: string + file?: unknown // UserFile or UserFile array content?: string mimeType?: string query?: string diff --git a/apps/sim/tools/onedrive/upload.ts b/apps/sim/tools/onedrive/upload.ts index ce834dc0d..e0d78591c 100644 --- a/apps/sim/tools/onedrive/upload.ts +++ b/apps/sim/tools/onedrive/upload.ts @@ -36,11 +36,17 @@ export const uploadTool: ToolConfig visibility: 'user-or-llm', description: 'The name of the file to upload', }, + file: { + type: 'file', + required: false, + visibility: 'user-only', + description: 'The file to upload (binary)', + }, content: { type: 'string', - required: true, + required: false, visibility: 'user-or-llm', - description: 'The content of the file to upload', + description: 'The text content to upload (if no file is provided)', }, folderSelector: { type: 'string', @@ -58,6 +64,12 @@ export const uploadTool: ToolConfig request: { url: (params) => { + // If file is provided, use custom API route for binary upload + if (params.file) { + return '/api/tools/onedrive/upload' + } + + // Text-only upload - use direct Microsoft Graph API let fileName = params.fileName || 'untitled' // Always create .txt files for text content @@ -74,17 +86,59 @@ export const uploadTool: ToolConfig // Default to root folder return `https://graph.microsoft.com/v1.0/me/drive/root:/${fileName}:/content` }, - method: 'PUT', - headers: (params) => ({ - Authorization: `Bearer ${params.accessToken}`, - 'Content-Type': 'text/plain', - }), - body: (params) => (params.content || '') as unknown as Record, + method: (params) => { + // Use POST for custom API route, PUT for direct upload + return params.file ? 'POST' : 'PUT' + }, + headers: (params) => { + const headers: Record = {} + // For file uploads via custom API, send JSON + if (params.file) { + headers['Content-Type'] = 'application/json' + } else { + // For text-only uploads, use direct PUT with access token + headers.Authorization = `Bearer ${params.accessToken}` + headers['Content-Type'] = 'text/plain' + } + return headers + }, + body: (params) => { + // For file uploads, send all params as JSON to custom API route + if (params.file) { + return { + accessToken: params.accessToken, + fileName: params.fileName, + file: params.file, + folderId: params.manualFolderId || params.folderSelector, + } + } + // For text-only uploads, send content directly + return (params.content || '') as unknown as Record + }, }, transformResponse: async (response: Response, params?: OneDriveToolParams) => { - // Microsoft Graph API returns the file metadata directly - const fileData = await response.json() + const data = await response.json() + + // Handle response from custom API route (for file uploads) + if (params?.file && data.success !== undefined) { + if (!data.success) { + throw new Error(data.error || 'Failed to upload file') + } + + logger.info('Successfully uploaded file to OneDrive via custom API', { + fileId: data.output?.file?.id, + fileName: data.output?.file?.name, + }) + + return { + success: true, + output: data.output, + } + } + + // Handle response from direct Microsoft Graph API (for text-only uploads) + const fileData = data logger.info('Successfully uploaded file to OneDrive', { fileId: fileData.id, diff --git a/apps/sim/tools/outlook/draft.ts b/apps/sim/tools/outlook/draft.ts index 7ba4e874e..602bd9c87 100644 --- a/apps/sim/tools/outlook/draft.ts +++ b/apps/sim/tools/outlook/draft.ts @@ -49,72 +49,48 @@ export const outlookDraftTool: ToolConfig { - return `https://graph.microsoft.com/v1.0/me/messages` - }, + url: '/api/tools/outlook/draft', method: 'POST', - headers: (params) => { - // Validate access token - if (!params.accessToken) { - throw new Error('Access token is required') - } - + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params: OutlookDraftParams) => { return { - Authorization: `Bearer ${params.accessToken}`, - 'Content-Type': 'application/json', - } - }, - body: (params: OutlookDraftParams): Record => { - // Helper function to parse comma-separated emails - const parseEmails = (emailString?: string) => { - if (!emailString) return [] - return emailString - .split(',') - .map((email) => email.trim()) - .filter((email) => email.length > 0) - .map((email) => ({ emailAddress: { address: email } })) - } - - const message: any = { + accessToken: params.accessToken, + to: params.to, subject: params.subject, - body: { - contentType: 'Text', - content: params.body, - }, - toRecipients: parseEmails(params.to), + body: params.body, + cc: params.cc || null, + bcc: params.bcc || null, + attachments: params.attachments || null, } - - // Add CC if provided - const ccRecipients = parseEmails(params.cc) - if (ccRecipients.length > 0) { - message.ccRecipients = ccRecipients - } - - // Add BCC if provided - const bccRecipients = parseEmails(params.bcc) - if (bccRecipients.length > 0) { - message.bccRecipients = bccRecipients - } - - return message }, }, - transformResponse: async (response) => { - // Outlook draft API returns the created message object - const data = await response.json() + transformResponse: async (response) => { + const data = await response.json() + if (!data.success) { + throw new Error(data.error || 'Failed to create Outlook draft') + } return { success: true, output: { - message: 'Email drafted successfully', + message: data.output.message, results: { - id: data.id, - subject: data.subject, + id: data.output.messageId, + subject: data.output.subject, status: 'drafted', timestamp: new Date().toISOString(), + attachmentCount: data.output.attachmentCount || 0, }, }, } diff --git a/apps/sim/tools/outlook/send.ts b/apps/sim/tools/outlook/send.ts index f7d839c63..ff8916c02 100644 --- a/apps/sim/tools/outlook/send.ts +++ b/apps/sim/tools/outlook/send.ts @@ -61,108 +61,48 @@ export const outlookSendTool: ToolConfig visibility: 'user-or-llm', description: 'BCC recipients (comma-separated)', }, + attachments: { + type: 'file[]', + required: false, + visibility: 'user-only', + description: 'Files to attach to the email', + }, }, request: { - url: (params) => { - // If replying to a specific message, use the reply endpoint - if (params.replyToMessageId) { - return `https://graph.microsoft.com/v1.0/me/messages/${params.replyToMessageId}/reply` - } - // Otherwise use the regular send mail endpoint - return `https://graph.microsoft.com/v1.0/me/sendMail` - }, + url: '/api/tools/outlook/send', method: 'POST', - headers: (params) => { - // Validate access token - if (!params.accessToken) { - throw new Error('Access token is required') - } - + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params: OutlookSendParams) => { return { - Authorization: `Bearer ${params.accessToken}`, - 'Content-Type': 'application/json', - } - }, - body: (params: OutlookSendParams): Record => { - // Helper function to parse comma-separated emails - const parseEmails = (emailString?: string) => { - if (!emailString) return [] - return emailString - .split(',') - .map((email) => email.trim()) - .filter((email) => email.length > 0) - .map((email) => ({ emailAddress: { address: email } })) - } - - // If replying to a message, use the reply format - if (params.replyToMessageId) { - const replyBody: any = { - message: { - body: { - contentType: 'Text', - content: params.body, - }, - }, - } - - // Add CC/BCC if provided - const ccRecipients = parseEmails(params.cc) - const bccRecipients = parseEmails(params.bcc) - - if (ccRecipients.length > 0) { - replyBody.message.ccRecipients = ccRecipients - } - if (bccRecipients.length > 0) { - replyBody.message.bccRecipients = bccRecipients - } - - return replyBody - } - - // Regular send mail format - const toRecipients = parseEmails(params.to) - const ccRecipients = parseEmails(params.cc) - const bccRecipients = parseEmails(params.bcc) - - const message: any = { + accessToken: params.accessToken, + to: params.to, subject: params.subject, - body: { - contentType: 'Text', - content: params.body, - }, - toRecipients, - } - - // Add CC/BCC if provided - if (ccRecipients.length > 0) { - message.ccRecipients = ccRecipients - } - if (bccRecipients.length > 0) { - message.bccRecipients = bccRecipients - } - - // Add conversation ID for threading if provided - if (params.conversationId) { - message.conversationId = params.conversationId - } - - return { - message, - saveToSentItems: true, + body: params.body, + cc: params.cc || null, + bcc: params.bcc || null, + replyToMessageId: params.replyToMessageId || null, + conversationId: params.conversationId || null, + attachments: params.attachments || null, } }, }, transformResponse: async (response) => { - // Outlook sendMail API returns empty body on success + const data = await response.json() + if (!data.success) { + throw new Error(data.error || 'Failed to send Outlook email') + } return { success: true, output: { - message: 'Email sent successfully', + message: data.output.message, results: { - status: 'sent', - timestamp: new Date().toISOString(), + status: data.output.status, + timestamp: data.output.timestamp, + attachmentCount: data.output.attachmentCount || 0, }, }, } diff --git a/apps/sim/tools/outlook/types.ts b/apps/sim/tools/outlook/types.ts index a891a5b7a..7bb2fe4de 100644 --- a/apps/sim/tools/outlook/types.ts +++ b/apps/sim/tools/outlook/types.ts @@ -10,6 +10,7 @@ export interface OutlookSendParams { conversationId?: string cc?: string bcc?: string + attachments?: any[] } export interface OutlookSendResponse extends ToolResponse { @@ -41,6 +42,7 @@ export interface OutlookDraftParams { bcc?: string subject: string body: string + attachments?: any[] } export interface OutlookDraftResponse extends ToolResponse { diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 79de8b169..4ec07f420 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -152,7 +152,13 @@ import { import { qdrantFetchTool, qdrantSearchTool, qdrantUpsertTool } from '@/tools/qdrant' import { redditGetCommentsTool, redditGetPostsTool, redditHotPostsTool } from '@/tools/reddit' import { mailSendTool } from '@/tools/resend' -import { s3GetObjectTool } from '@/tools/s3' +import { + s3CopyObjectTool, + s3DeleteObjectTool, + s3GetObjectTool, + s3ListObjectsTool, + s3PutObjectTool, +} from '@/tools/s3' import { searchTool as serperSearch } from '@/tools/serper' import { sharepointAddListItemTool, @@ -162,6 +168,7 @@ import { sharepointListSitesTool, sharepointReadPageTool, sharepointUpdateListItemTool, + sharepointUploadFileTool, } from '@/tools/sharepoint' import { slackCanvasTool, slackMessageReaderTool, slackMessageTool } from '@/tools/slack' import { smsSendTool } from '@/tools/sms' @@ -180,6 +187,7 @@ import { telegramMessageTool, telegramSendAnimationTool, telegramSendAudioTool, + telegramSendDocumentTool, telegramSendPhotoTool, telegramSendVideoTool, } from '@/tools/telegram' @@ -362,12 +370,17 @@ export const tools: Record = { knowledge_create_document: knowledgeCreateDocumentTool, elevenlabs_tts: elevenLabsTtsTool, s3_get_object: s3GetObjectTool, + s3_put_object: s3PutObjectTool, + s3_list_objects: s3ListObjectsTool, + s3_delete_object: s3DeleteObjectTool, + s3_copy_object: s3CopyObjectTool, telegram_message: telegramMessageTool, telegram_delete_message: telegramDeleteMessageTool, telegram_send_audio: telegramSendAudioTool, telegram_send_animation: telegramSendAnimationTool, telegram_send_photo: telegramSendPhotoTool, telegram_send_video: telegramSendVideoTool, + telegram_send_document: telegramSendDocumentTool, clay_populate: clayPopulateTool, discord_send_message: discordSendMessageTool, discord_get_messages: discordGetMessagesTool, @@ -432,6 +445,5 @@ export const tools: Record = { sharepoint_create_list: sharepointCreateListTool, sharepoint_update_list: sharepointUpdateListItemTool, sharepoint_add_list_items: sharepointAddListItemTool, - // Provider chat tools - // Provider chat tools - handled separately in agent blocks + sharepoint_upload_file: sharepointUploadFileTool, } diff --git a/apps/sim/tools/s3/copy_object.ts b/apps/sim/tools/s3/copy_object.ts new file mode 100644 index 000000000..da583ca30 --- /dev/null +++ b/apps/sim/tools/s3/copy_object.ts @@ -0,0 +1,117 @@ +import type { ToolConfig } from '@/tools/types' + +export const s3CopyObjectTool: ToolConfig = { + id: 's3_copy_object', + name: 'S3 Copy Object', + description: 'Copy an object within or between AWS S3 buckets', + version: '1.0.0', + + params: { + accessKeyId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your AWS Access Key ID', + }, + secretAccessKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your AWS Secret Access Key', + }, + region: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'AWS region (e.g., us-east-1)', + }, + sourceBucket: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Source bucket name', + }, + sourceKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Source object key/path', + }, + destinationBucket: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Destination bucket name', + }, + destinationKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Destination object key/path', + }, + acl: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Access control list for the copied object (e.g., private, public-read)', + }, + }, + + request: { + url: '/api/tools/s3/copy-object', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + accessKeyId: params.accessKeyId, + secretAccessKey: params.secretAccessKey, + region: params.region, + sourceBucket: params.sourceBucket, + sourceKey: params.sourceKey, + destinationBucket: params.destinationBucket, + destinationKey: params.destinationKey, + acl: params.acl, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!data.success) { + return { + success: false, + output: { + url: '', + metadata: { + error: data.error || 'Failed to copy object', + }, + }, + error: data.error, + } + } + + return { + success: true, + output: { + url: data.output.url, + metadata: { + copySourceVersionId: data.output.copySourceVersionId, + versionId: data.output.versionId, + etag: data.output.etag, + }, + }, + } + }, + + outputs: { + url: { + type: 'string', + description: 'URL of the copied S3 object', + }, + metadata: { + type: 'object', + description: 'Copy operation metadata', + }, + }, +} diff --git a/apps/sim/tools/s3/delete_object.ts b/apps/sim/tools/s3/delete_object.ts new file mode 100644 index 000000000..17f94661d --- /dev/null +++ b/apps/sim/tools/s3/delete_object.ts @@ -0,0 +1,96 @@ +import type { ToolConfig } from '@/tools/types' + +export const s3DeleteObjectTool: ToolConfig = { + id: 's3_delete_object', + name: 'S3 Delete Object', + description: 'Delete an object from an AWS S3 bucket', + version: '1.0.0', + + params: { + accessKeyId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your AWS Access Key ID', + }, + secretAccessKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your AWS Secret Access Key', + }, + region: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'AWS region (e.g., us-east-1)', + }, + bucketName: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'S3 bucket name', + }, + objectKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Object key/path to delete', + }, + }, + + request: { + url: '/api/tools/s3/delete-object', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + accessKeyId: params.accessKeyId, + secretAccessKey: params.secretAccessKey, + region: params.region, + bucketName: params.bucketName, + objectKey: params.objectKey, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!data.success) { + return { + success: false, + output: { + deleted: false, + metadata: { + error: data.error || 'Failed to delete object', + }, + }, + error: data.error, + } + } + + return { + success: true, + output: { + deleted: true, + metadata: { + key: data.output.key, + deleteMarker: data.output.deleteMarker, + versionId: data.output.versionId, + }, + }, + } + }, + + outputs: { + deleted: { + type: 'boolean', + description: 'Whether the object was successfully deleted', + }, + metadata: { + type: 'object', + description: 'Deletion metadata', + }, + }, +} diff --git a/apps/sim/tools/s3/index.ts b/apps/sim/tools/s3/index.ts index ad49b7cfd..235c479bf 100644 --- a/apps/sim/tools/s3/index.ts +++ b/apps/sim/tools/s3/index.ts @@ -1,3 +1,7 @@ +import { s3CopyObjectTool } from '@/tools/s3/copy_object' +import { s3DeleteObjectTool } from '@/tools/s3/delete_object' import { s3GetObjectTool } from '@/tools/s3/get_object' +import { s3ListObjectsTool } from '@/tools/s3/list_objects' +import { s3PutObjectTool } from '@/tools/s3/put_object' -export { s3GetObjectTool } +export { s3GetObjectTool, s3PutObjectTool, s3ListObjectsTool, s3DeleteObjectTool, s3CopyObjectTool } diff --git a/apps/sim/tools/s3/list_objects.ts b/apps/sim/tools/s3/list_objects.ts new file mode 100644 index 000000000..7c4d284f4 --- /dev/null +++ b/apps/sim/tools/s3/list_objects.ts @@ -0,0 +1,120 @@ +import type { ToolConfig } from '@/tools/types' + +export const s3ListObjectsTool: ToolConfig = { + id: 's3_list_objects', + name: 'S3 List Objects', + description: 'List objects in an AWS S3 bucket', + version: '1.0.0', + + params: { + accessKeyId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your AWS Access Key ID', + }, + secretAccessKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your AWS Secret Access Key', + }, + region: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'AWS region (e.g., us-east-1)', + }, + bucketName: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'S3 bucket name', + }, + prefix: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Prefix to filter objects (e.g., folder/)', + }, + maxKeys: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Maximum number of objects to return (default: 1000)', + }, + continuationToken: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Token for pagination', + }, + }, + + request: { + url: '/api/tools/s3/list-objects', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + accessKeyId: params.accessKeyId, + secretAccessKey: params.secretAccessKey, + region: params.region, + bucketName: params.bucketName, + prefix: params.prefix, + maxKeys: params.maxKeys, + continuationToken: params.continuationToken, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!data.success) { + return { + success: false, + output: { + objects: [], + metadata: { + error: data.error || 'Failed to list objects', + }, + }, + error: data.error, + } + } + + return { + success: true, + output: { + objects: data.output.objects || [], + metadata: { + isTruncated: data.output.isTruncated, + nextContinuationToken: data.output.nextContinuationToken, + keyCount: data.output.keyCount, + prefix: data.output.prefix, + }, + }, + } + }, + + outputs: { + objects: { + type: 'array', + description: 'List of S3 objects', + items: { + type: 'object', + properties: { + key: { type: 'string', description: 'Object key' }, + size: { type: 'number', description: 'Object size in bytes' }, + lastModified: { type: 'string', description: 'Last modified timestamp' }, + etag: { type: 'string', description: 'Entity tag' }, + }, + }, + }, + metadata: { + type: 'object', + description: 'Listing metadata including pagination info', + }, + }, +} diff --git a/apps/sim/tools/s3/put_object.ts b/apps/sim/tools/s3/put_object.ts new file mode 100644 index 000000000..6a1f596b6 --- /dev/null +++ b/apps/sim/tools/s3/put_object.ts @@ -0,0 +1,125 @@ +import type { ToolConfig } from '@/tools/types' + +export const s3PutObjectTool: ToolConfig = { + id: 's3_put_object', + name: 'S3 Put Object', + description: 'Upload a file to an AWS S3 bucket', + version: '1.0.0', + + params: { + accessKeyId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your AWS Access Key ID', + }, + secretAccessKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your AWS Secret Access Key', + }, + region: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'AWS region (e.g., us-east-1)', + }, + bucketName: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'S3 bucket name', + }, + objectKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Object key/path in S3 (e.g., folder/filename.ext)', + }, + file: { + type: 'file', + required: false, + visibility: 'user-only', + description: 'File to upload', + }, + content: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Text content to upload (alternative to file)', + }, + contentType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Content-Type header (auto-detected from file if not provided)', + }, + acl: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Access control list (e.g., private, public-read)', + }, + }, + + request: { + url: '/api/tools/s3/put-object', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + accessKeyId: params.accessKeyId, + secretAccessKey: params.secretAccessKey, + region: params.region, + bucketName: params.bucketName, + objectKey: params.objectKey, + file: params.file, + content: params.content, + contentType: params.contentType, + acl: params.acl, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!data.success) { + return { + success: false, + output: { + url: '', + metadata: { + error: data.error || 'Failed to upload object', + }, + }, + error: data.error, + } + } + + return { + success: true, + output: { + url: data.output.url, + metadata: { + etag: data.output.etag, + location: data.output.location, + key: data.output.key, + bucket: data.output.bucket, + }, + }, + } + }, + + outputs: { + url: { + type: 'string', + description: 'URL of the uploaded S3 object', + }, + metadata: { + type: 'object', + description: 'Upload metadata including ETag and location', + }, + }, +} diff --git a/apps/sim/tools/s3/types.ts b/apps/sim/tools/s3/types.ts index bdccb226f..67f1fc49d 100644 --- a/apps/sim/tools/s3/types.ts +++ b/apps/sim/tools/s3/types.ts @@ -2,12 +2,30 @@ import type { ToolResponse } from '@/tools/types' export interface S3Response extends ToolResponse { output: { - url: string - metadata: { - fileType: string + url?: string + objects?: Array<{ + key: string size: number - name: string lastModified: string + etag: string + }> + deleted?: boolean + metadata: { + fileType?: string + size?: number + name?: string + lastModified?: string + etag?: string + location?: string + key?: string + bucket?: string + isTruncated?: boolean + nextContinuationToken?: string + keyCount?: number + prefix?: string + deleteMarker?: boolean + versionId?: string + copySourceVersionId?: string error?: string } } diff --git a/apps/sim/tools/sharepoint/index.ts b/apps/sim/tools/sharepoint/index.ts index 1961156f1..3428f6e4f 100644 --- a/apps/sim/tools/sharepoint/index.ts +++ b/apps/sim/tools/sharepoint/index.ts @@ -5,6 +5,7 @@ import { getListTool } from '@/tools/sharepoint/get_list' import { listSitesTool } from '@/tools/sharepoint/list_sites' import { readPageTool } from '@/tools/sharepoint/read_page' import { updateListItemTool } from '@/tools/sharepoint/update_list' +import { uploadFileTool } from '@/tools/sharepoint/upload_file' export const sharepointCreatePageTool = createPageTool export const sharepointCreateListTool = createListTool @@ -13,3 +14,4 @@ export const sharepointListSitesTool = listSitesTool export const sharepointReadPageTool = readPageTool export const sharepointUpdateListItemTool = updateListItemTool export const sharepointAddListItemTool = addListItemTool +export const sharepointUploadFileTool = uploadFileTool diff --git a/apps/sim/tools/sharepoint/types.ts b/apps/sim/tools/sharepoint/types.ts index afa589977..5be8061ce 100644 --- a/apps/sim/tools/sharepoint/types.ts +++ b/apps/sim/tools/sharepoint/types.ts @@ -176,6 +176,11 @@ export interface SharepointToolParams { // Update List Item itemId?: string listItemFields?: Record + // Upload File + driveId?: string + folderPath?: string + fileName?: string + files?: any[] } export interface GraphApiResponse { @@ -260,6 +265,7 @@ export type SharepointResponse = | SharepointCreateListResponse | SharepointUpdateListItemResponse | SharepointAddListItemResponse + | SharepointUploadFileResponse export interface SharepointGetListResponse extends ToolResponse { output: { @@ -292,3 +298,19 @@ export interface SharepointAddListItemResponse extends ToolResponse { } } } + +export interface SharepointUploadedFile { + id: string + name: string + webUrl: string + size: number + createdDateTime?: string + lastModifiedDateTime?: string +} + +export interface SharepointUploadFileResponse extends ToolResponse { + output: { + uploadedFiles: SharepointUploadedFile[] + fileCount: number + } +} diff --git a/apps/sim/tools/sharepoint/upload_file.ts b/apps/sim/tools/sharepoint/upload_file.ts new file mode 100644 index 000000000..d552f4310 --- /dev/null +++ b/apps/sim/tools/sharepoint/upload_file.ts @@ -0,0 +1,115 @@ +import type { SharepointToolParams, SharepointUploadFileResponse } from '@/tools/sharepoint/types' +import type { ToolConfig } from '@/tools/types' + +export const uploadFileTool: ToolConfig = { + id: 'sharepoint_upload_file', + name: 'Upload File to SharePoint', + description: 'Upload files to a SharePoint document library', + version: '1.0', + + oauth: { + required: true, + provider: 'sharepoint', + additionalScopes: [ + 'openid', + 'profile', + 'email', + 'Files.ReadWrite', + 'Sites.ReadWrite.All', + 'offline_access', + ], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the SharePoint API', + }, + siteId: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'The ID of the SharePoint site', + }, + driveId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'The ID of the document library (drive). If not provided, uses default drive.', + }, + folderPath: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Optional folder path within the document library (e.g., /Documents/Subfolder)', + }, + fileName: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Optional: override the uploaded file name', + }, + files: { + type: 'file[]', + required: false, + visibility: 'user-only', + description: 'Files to upload to SharePoint', + }, + }, + + request: { + url: '/api/tools/sharepoint/upload', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params: SharepointToolParams) => { + return { + accessToken: params.accessToken, + siteId: params.siteId || 'root', + driveId: params.driveId || null, + folderPath: params.folderPath || null, + fileName: params.fileName || null, + files: params.files || null, + } + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!data.success) { + throw new Error(data.error || 'Failed to upload files to SharePoint') + } + return { + success: true, + output: { + uploadedFiles: data.output.uploadedFiles, + fileCount: data.output.fileCount, + }, + } + }, + + outputs: { + uploadedFiles: { + type: 'array', + description: 'Array of uploaded file objects', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'The unique ID of the uploaded file' }, + name: { type: 'string', description: 'The name of the uploaded file' }, + webUrl: { type: 'string', description: 'The URL to access the file' }, + size: { type: 'number', description: 'The size of the file in bytes' }, + createdDateTime: { type: 'string', description: 'When the file was created' }, + lastModifiedDateTime: { type: 'string', description: 'When the file was last modified' }, + }, + }, + }, + fileCount: { + type: 'number', + description: 'Number of files uploaded', + }, + }, +} diff --git a/apps/sim/tools/slack/message.ts b/apps/sim/tools/slack/message.ts index 27fa1af15..db52e6338 100644 --- a/apps/sim/tools/slack/message.ts +++ b/apps/sim/tools/slack/message.ts @@ -51,33 +51,38 @@ export const slackMessageTool: ToolConfig ({ + headers: () => ({ 'Content-Type': 'application/json', - Authorization: `Bearer ${params.accessToken || params.botToken}`, }), body: (params: SlackMessageParams) => { - const body: any = { + return { + accessToken: params.accessToken || params.botToken, channel: params.channel, - markdown_text: params.text, + text: params.text, + files: params.files || null, } - - return body }, }, transformResponse: async (response: Response) => { const data = await response.json() + if (!data.success) { + throw new Error(data.error || 'Failed to send Slack message') + } return { success: true, - output: { - ts: data.ts, - channel: data.channel, - }, + output: data.output, } }, diff --git a/apps/sim/tools/slack/types.ts b/apps/sim/tools/slack/types.ts index 85778dbf2..0a924083b 100644 --- a/apps/sim/tools/slack/types.ts +++ b/apps/sim/tools/slack/types.ts @@ -10,6 +10,7 @@ export interface SlackMessageParams extends SlackBaseParams { channel: string text: string thread_ts?: string + files?: any[] } export interface SlackCanvasParams extends SlackBaseParams { diff --git a/apps/sim/tools/telegram/index.ts b/apps/sim/tools/telegram/index.ts index 567a4d016..abb955851 100644 --- a/apps/sim/tools/telegram/index.ts +++ b/apps/sim/tools/telegram/index.ts @@ -2,6 +2,7 @@ import { telegramDeleteMessageTool } from '@/tools/telegram/delete_message' import { telegramMessageTool } from '@/tools/telegram/message' import { telegramSendAnimationTool } from '@/tools/telegram/send_animation' import { telegramSendAudioTool } from '@/tools/telegram/send_audio' +import { telegramSendDocumentTool } from '@/tools/telegram/send_document' import { telegramSendPhotoTool } from '@/tools/telegram/send_photo' import { telegramSendVideoTool } from '@/tools/telegram/send_video' @@ -10,6 +11,7 @@ export { telegramSendAudioTool, telegramDeleteMessageTool, telegramMessageTool, + telegramSendDocumentTool, telegramSendPhotoTool, telegramSendVideoTool, } diff --git a/apps/sim/tools/telegram/send_document.ts b/apps/sim/tools/telegram/send_document.ts new file mode 100644 index 000000000..5efecf4a9 --- /dev/null +++ b/apps/sim/tools/telegram/send_document.ts @@ -0,0 +1,143 @@ +import type { + TelegramSendDocumentParams, + TelegramSendDocumentResponse, +} from '@/tools/telegram/types' +import type { ToolConfig } from '@/tools/types' + +export const telegramSendDocumentTool: ToolConfig< + TelegramSendDocumentParams, + TelegramSendDocumentResponse +> = { + id: 'telegram_send_document', + name: 'Telegram Send Document', + description: + 'Send documents (PDF, ZIP, DOC, etc.) to Telegram channels or users through the Telegram Bot API.', + version: '1.0.0', + + params: { + botToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Telegram Bot API Token', + }, + chatId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Target Telegram chat ID', + }, + files: { + type: 'file[]', + required: false, + visibility: 'user-only', + description: 'Document file to send (PDF, ZIP, DOC, etc.). Max size: 50MB', + }, + caption: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Document caption (optional)', + }, + }, + + request: { + url: '/api/tools/telegram/send-document', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params: TelegramSendDocumentParams) => { + return { + botToken: params.botToken, + chatId: params.chatId, + files: params.files || null, + caption: params.caption, + } + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!data.success) { + throw new Error(data.error || 'Failed to send Telegram document') + } + return { + success: true, + output: data.output, + } + }, + + outputs: { + message: { type: 'string', description: 'Success or error message' }, + data: { + type: 'object', + description: 'Telegram message data including document', + properties: { + message_id: { + type: 'number', + description: 'Unique Telegram message identifier', + }, + from: { + type: 'object', + description: 'Information about the sender', + properties: { + id: { type: 'number', description: 'Sender ID' }, + is_bot: { + type: 'boolean', + description: 'Whether the chat is a bot or not', + }, + first_name: { + type: 'string', + description: "Sender's first name (if available)", + }, + username: { + type: 'string', + description: "Sender's username (if available)", + }, + }, + }, + chat: { + type: 'object', + description: 'Information about the chat where message was sent', + properties: { + id: { type: 'number', description: 'Chat ID' }, + first_name: { + type: 'string', + description: 'Chat first name (if private chat)', + }, + username: { + type: 'string', + description: 'Chat username (for private or channels)', + }, + type: { + type: 'string', + description: 'Type of chat (private, group, supergroup, or channel)', + }, + }, + }, + date: { + type: 'number', + description: 'Unix timestamp when the message was sent', + }, + document: { + type: 'object', + description: 'Document file details', + properties: { + file_name: { type: 'string', description: 'Document file name' }, + mime_type: { type: 'string', description: 'Document MIME type' }, + file_id: { type: 'string', description: 'Document file ID' }, + file_unique_id: { + type: 'string', + description: 'Unique document file identifier', + }, + file_size: { + type: 'number', + description: 'Size of document file in bytes', + }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/telegram/types.ts b/apps/sim/tools/telegram/types.ts index df38b18b2..f1ab4eb9d 100644 --- a/apps/sim/tools/telegram/types.ts +++ b/apps/sim/tools/telegram/types.ts @@ -115,6 +115,11 @@ export interface TelegramSendAnimationParams extends TelegramAuthParams { caption?: string } +export interface TelegramSendDocumentParams extends TelegramAuthParams { + files?: any + caption?: string +} + export interface TelegramDeleteMessageParams extends TelegramAuthParams { messageId: number } @@ -157,11 +162,19 @@ export interface TelegramSendPhotoResponse extends ToolResponse { } } +export interface TelegramSendDocumentResponse extends ToolResponse { + output: { + message: string + data?: TelegramMedia + } +} + export type TelegramResponse = | TelegramSendMessageResponse | TelegramSendPhotoResponse | TelegramSendAudioResponse | TelegramSendMediaResponse + | TelegramSendDocumentResponse | TelegramDeleteMessageResponse // Legacy type for backwards compatibility diff --git a/apps/sim/tools/vision/tool.ts b/apps/sim/tools/vision/tool.ts index 2fac90aa0..2eb186bec 100644 --- a/apps/sim/tools/vision/tool.ts +++ b/apps/sim/tools/vision/tool.ts @@ -17,10 +17,16 @@ export const visionTool: ToolConfig = { }, imageUrl: { type: 'string', - required: true, + required: false, visibility: 'user-only', description: 'Publicly accessible image URL', }, + imageFile: { + type: 'file', + required: false, + visibility: 'user-only', + description: 'Image file to analyze', + }, model: { type: 'string', required: false, @@ -37,93 +43,29 @@ export const visionTool: ToolConfig = { request: { method: 'POST', - url: (params) => { - if (params.model?.startsWith('claude-3')) { - return 'https://api.anthropic.com/v1/messages' - } - return 'https://api.openai.com/v1/chat/completions' - }, - headers: (params) => { - const headers = { - 'Content-Type': 'application/json', - } - - return params.model?.startsWith('claude-3') - ? { - ...headers, - 'x-api-key': params.apiKey, - 'anthropic-version': '2023-06-01', - } - : { - ...headers, - Authorization: `Bearer ${params.apiKey}`, - } - }, + url: '/api/tools/vision/analyze', + headers: () => ({ + 'Content-Type': 'application/json', + }), body: (params) => { - const defaultPrompt = 'Please analyze this image and describe what you see in detail.' - const prompt = params.prompt || defaultPrompt - - if (params.model?.startsWith('claude-3')) { - return { - model: params.model, - messages: [ - { - role: 'user', - content: [ - { type: 'text', text: prompt }, - { - type: 'image', - source: { type: 'url', url: params.imageUrl }, - }, - ], - }, - ], - } - } - return { - model: 'gpt-4o', - messages: [ - { - role: 'user', - content: [ - { type: 'text', text: prompt }, - { - type: 'image_url', - image_url: { - url: params.imageUrl, - }, - }, - ], - }, - ], - max_tokens: 1000, + apiKey: params.apiKey, + imageUrl: params.imageUrl || null, + imageFile: params.imageFile || null, + model: params.model || 'gpt-4o', + prompt: params.prompt || null, } }, }, transformResponse: async (response: Response) => { const data = await response.json() - - const result = data.content?.[0]?.text || data.choices?.[0]?.message?.content - + if (!data.success) { + throw new Error(data.error || 'Failed to analyze image') + } return { success: true, - output: { - content: result, - model: data.model, - tokens: data.content - ? data.usage?.input_tokens + data.usage?.output_tokens - : data.usage?.total_tokens, - usage: data.usage - ? { - input_tokens: data.usage.input_tokens, - output_tokens: data.usage.output_tokens, - total_tokens: - data.usage.total_tokens || data.usage.input_tokens + data.usage.output_tokens, - } - : undefined, - }, + output: data.output, } }, diff --git a/apps/sim/tools/vision/types.ts b/apps/sim/tools/vision/types.ts index d4401e69b..a0c3412b2 100644 --- a/apps/sim/tools/vision/types.ts +++ b/apps/sim/tools/vision/types.ts @@ -2,7 +2,8 @@ import type { ToolResponse } from '@/tools/types' export interface VisionParams { apiKey: string - imageUrl: string + imageUrl?: string + imageFile?: any model?: string prompt?: string } diff --git a/apps/sim/triggers/gmail/poller.ts b/apps/sim/triggers/gmail/poller.ts index b201da919..a168d592d 100644 --- a/apps/sim/triggers/gmail/poller.ts +++ b/apps/sim/triggers/gmail/poller.ts @@ -31,6 +31,14 @@ export const gmailPollingTrigger: TriggerConfig = { 'Include only emails with selected labels, or exclude emails with selected labels', required: true, }, + searchQuery: { + type: 'string', + label: 'Gmail Search Query', + placeholder: 'subject:report OR from:important@example.com', + description: + 'Optional Gmail search query to filter emails. Use the same format as Gmail search box (e.g., "subject:invoice", "from:boss@company.com", "has:attachment"). Leave empty to search all emails.', + required: false, + }, markAsRead: { type: 'boolean', label: 'Mark as Read', diff --git a/packages/db/consts.ts b/packages/db/consts.ts index 81c64a630..39bd76a57 100644 --- a/packages/db/consts.ts +++ b/packages/db/consts.ts @@ -8,6 +8,15 @@ */ export const DEFAULT_FREE_CREDITS = 10 +/** + * Storage limit constants (in GB) + * Can be overridden via environment variables + */ +export const DEFAULT_FREE_STORAGE_LIMIT_GB = 5 +export const DEFAULT_PRO_STORAGE_LIMIT_GB = 50 +export const DEFAULT_TEAM_STORAGE_LIMIT_GB = 500 +export const DEFAULT_ENTERPRISE_STORAGE_LIMIT_GB = 500 + /** * Tag slots available for knowledge base documents and embeddings */ diff --git a/packages/db/migrations/0100_public_black_cat.sql b/packages/db/migrations/0100_public_black_cat.sql new file mode 100644 index 000000000..ca5713d87 --- /dev/null +++ b/packages/db/migrations/0100_public_black_cat.sql @@ -0,0 +1,18 @@ +CREATE TABLE "workspace_file" ( + "id" text PRIMARY KEY NOT NULL, + "workspace_id" text NOT NULL, + "name" text NOT NULL, + "key" text NOT NULL, + "size" integer NOT NULL, + "type" text NOT NULL, + "uploaded_by" text NOT NULL, + "uploaded_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "workspace_file_key_unique" UNIQUE("key") +); +--> statement-breakpoint +ALTER TABLE "organization" ADD COLUMN "storage_used_bytes" bigint DEFAULT 0 NOT NULL;--> statement-breakpoint +ALTER TABLE "user_stats" ADD COLUMN "storage_used_bytes" bigint DEFAULT 0 NOT NULL;--> statement-breakpoint +ALTER TABLE "workspace_file" ADD CONSTRAINT "workspace_file_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "workspace_file" ADD CONSTRAINT "workspace_file_uploaded_by_user_id_fk" FOREIGN KEY ("uploaded_by") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "workspace_file_workspace_id_idx" ON "workspace_file" USING btree ("workspace_id");--> statement-breakpoint +CREATE INDEX "workspace_file_key_idx" ON "workspace_file" USING btree ("key"); \ No newline at end of file diff --git a/packages/db/migrations/meta/0100_snapshot.json b/packages/db/migrations/meta/0100_snapshot.json new file mode 100644 index 000000000..6f513d33d --- /dev/null +++ b/packages/db/migrations/meta/0100_snapshot.json @@ -0,0 +1,7096 @@ +{ + "id": "2252aceb-54e7-498c-8b61-26bc3ae16b42", + "prevId": "0e1860e9-c277-455a-a263-99e630a4b2d3", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "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": {} + } + }, + "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 + }, + "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": {}, + "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.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": "'[]'" + }, + "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, + "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_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": true + }, + "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 + }, + "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_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": {} + } + }, + "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" + } + }, + "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.custom_tools": { + "name": "custom_tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "custom_tools_user_id_user_id_fk": { + "name": "custom_tools_user_id_user_id_fk", + "tableFrom": "custom_tools", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.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 + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": 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 + }, + "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_kb_uploaded_at_idx": { + "name": "doc_kb_uploaded_at_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "uploaded_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_processing_status_idx": { + "name": "doc_processing_status_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "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": {} + } + }, + "foreignKeys": { + "document_knowledge_base_id_knowledge_base_id_fk": { + "name": "document_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.embedding": { + "name": "embedding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_index": { + "name": "chunk_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chunk_hash": { + "name": "chunk_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_length": { + "name": "content_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "start_offset": { + "name": "start_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_offset": { + "name": "end_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "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 + }, + "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_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.idempotency_key": { + "name": "idempotency_key", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "namespace": { + "name": "namespace", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "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_namespace_unique": { + "name": "idempotency_key_namespace_unique", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "namespace", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "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": {} + }, + "idempotency_key_namespace_idx": { + "name": "idempotency_key_namespace_idx", + "columns": [ + { + "expression": "namespace", + "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 + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "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": {} + } + }, + "foreignKeys": { + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base": { + "name": "knowledge_base", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "embedding_dimension": { + "name": "embedding_dimension", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1536 + }, + "chunking_config": { + "name": "chunking_config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"maxSize\": 1024, \"minSize\": 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": {} + } + }, + "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.marketplace": { + "name": "marketplace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_name": { + "name": "author_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "views": { + "name": "views", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "marketplace_workflow_id_workflow_id_fk": { + "name": "marketplace_workflow_id_workflow_id_fk", + "tableFrom": "marketplace", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "marketplace_author_id_user_id_fk": { + "name": "marketplace_author_id_user_id_fk", + "tableFrom": "marketplace", + "tableTo": "user", + "columnsFrom": ["author_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.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 + }, + "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_idx": { + "name": "mcp_servers_workspace_deleted_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "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_idx": { + "name": "member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "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 + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "memory_key_idx": { + "name": "memory_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workflow_idx": { + "name": "memory_workflow_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workflow_key_idx": { + "name": "memory_workflow_key_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_workflow_id_workflow_id_fk": { + "name": "memory_workflow_id_workflow_id_fk", + "tableFrom": "memory", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "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.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.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "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 + }, + "auto_fill_env_vars": { + "name": "auto_fill_env_vars", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "auto_pan": { + "name": "auto_pan", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "console_expanded_by_default": { + "name": "console_expanded_by_default", + "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_floating_controls": { + "name": "show_floating_controls", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_training_controls": { + "name": "show_training_controls", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "copilot_enabled_models": { + "name": "copilot_enabled_models", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "settings_user_id_user_id_fk": { + "name": "settings_user_id_user_id_fk", + "tableFrom": "settings", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_user_id_unique": { + "name": "settings_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.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_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 + }, + "user_id": { + "name": "user_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 + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "views": { + "name": "views", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "stars": { + "name": "stars", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#3972F6'" + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'FileText'" + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "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()" + } + }, + "indexes": { + "templates_workflow_id_idx": { + "name": "templates_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_user_id_idx": { + "name": "templates_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_category_idx": { + "name": "templates_category_idx", + "columns": [ + { + "expression": "category", + "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_category_views_idx": { + "name": "templates_category_views_idx", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_category_stars_idx": { + "name": "templates_category_stars_idx", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stars", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_user_category_idx": { + "name": "templates_user_category_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "category", + "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": "no action", + "onUpdate": "no action" + }, + "templates_user_id_user_id_fk": { + "name": "templates_user_id_user_id_fk", + "tableFrom": "templates", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "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 + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_rate_limits": { + "name": "user_rate_limits", + "schema": "", + "columns": { + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "sync_api_requests": { + "name": "sync_api_requests", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "async_api_requests": { + "name": "async_api_requests", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "api_endpoint_requests": { + "name": "api_endpoint_requests", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "window_start": { + "name": "window_start", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_request_at": { + "name": "last_request_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_rate_limited": { + "name": "is_rate_limited", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "rate_limit_reset_at": { + "name": "rate_limit_reset_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_stats": { + "name": "user_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_manual_executions": { + "name": "total_manual_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_api_calls": { + "name": "total_api_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_webhook_triggers": { + "name": "total_webhook_triggers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_scheduled_executions": { + "name": "total_scheduled_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_chat_executions": { + "name": "total_chat_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_tokens_used": { + "name": "total_tokens_used", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost": { + "name": "total_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_usage_limit": { + "name": "current_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'10'" + }, + "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'" + }, + "total_copilot_cost": { + "name": "total_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "total_copilot_tokens": { + "name": "total_copilot_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_copilot_calls": { + "name": "total_copilot_calls", + "type": "integer", + "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 + } + }, + "indexes": {}, + "foreignKeys": { + "user_stats_user_id_user_id_fk": { + "name": "user_stats_user_id_user_id_fk", + "tableFrom": "user_stats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_stats_user_id_unique": { + "name": "user_stats_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "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 + }, + "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 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "path_idx": { + "name": "path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_workflow_id_workflow_id_fk": { + "name": "webhook_workflow_id_workflow_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_block_id_workflow_blocks_id_fk": { + "name": "webhook_block_id_workflow_blocks_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow_blocks", + "columnsFrom": ["block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#3972F6'" + }, + "last_synced": { + "name": "last_synced", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "is_deployed": { + "name": "is_deployed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deployed_state": { + "name": "deployed_state", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "deployed_at": { + "name": "deployed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "pinned_api_key_id": { + "name": "pinned_api_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "collaborators": { + "name": "collaborators", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "is_published": { + "name": "is_published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "marketplace_data": { + "name": "marketplace_data", + "type": "json", + "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": {} + } + }, + "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" + }, + "workflow_pinned_api_key_id_api_key_id_fk": { + "name": "workflow_pinned_api_key_id_api_key_id_fk", + "tableFrom": "workflow", + "tableTo": "api_key", + "columnsFrom": ["pinned_api_key_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 + }, + "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_workflow_type_idx": { + "name": "workflow_blocks_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_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 + }, + "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_id_idx": { + "name": "workflow_deployment_version_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "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": 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 + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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_execution_id_idx": { + "name": "workflow_execution_logs_execution_id_idx", + "columns": [ + { + "expression": "execution_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_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": {} + } + }, + "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": "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" + } + }, + "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": true + }, + "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": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_folder": { + "name": "workflow_folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'#6B7280'" + }, + "is_expanded": { + "name": "is_expanded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_folder_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": {} + } + }, + "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_log_webhook": { + "name": "workflow_log_webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "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[]" + }, + "active": { + "name": "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": { + "workflow_log_webhook_workflow_id_idx": { + "name": "workflow_log_webhook_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_log_webhook_active_idx": { + "name": "workflow_log_webhook_active_idx", + "columns": [ + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_log_webhook_workflow_id_workflow_id_fk": { + "name": "workflow_log_webhook_workflow_id_workflow_id_fk", + "tableFrom": "workflow_log_webhook", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_log_webhook_delivery": { + "name": "workflow_log_webhook_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": "webhook_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": { + "workflow_log_webhook_delivery_subscription_id_idx": { + "name": "workflow_log_webhook_delivery_subscription_id_idx", + "columns": [ + { + "expression": "subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_log_webhook_delivery_execution_id_idx": { + "name": "workflow_log_webhook_delivery_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_log_webhook_delivery_status_idx": { + "name": "workflow_log_webhook_delivery_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_log_webhook_delivery_next_attempt_idx": { + "name": "workflow_log_webhook_delivery_next_attempt_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_log_webhook_delivery_subscription_id_workflow_log_webhook_id_fk": { + "name": "workflow_log_webhook_delivery_subscription_id_workflow_log_webhook_id_fk", + "tableFrom": "workflow_log_webhook_delivery", + "tableTo": "workflow_log_webhook", + "columnsFrom": ["subscription_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_log_webhook_delivery_workflow_id_workflow_id_fk": { + "name": "workflow_log_webhook_delivery_workflow_id_workflow_id_fk", + "tableFrom": "workflow_log_webhook_delivery", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_schedule": { + "name": "workflow_schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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 + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_schedule_workflow_block_unique": { + "name": "workflow_schedule_workflow_block_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "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_block_id_workflow_blocks_id_fk": { + "name": "workflow_schedule_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_blocks", + "columnsFrom": ["block_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 + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_owner_id_user_id_fk": { + "name": "workspace_owner_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_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 + }, + "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": {} + } + }, + "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_invitation": { + "name": "workspace_invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "workspace_invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'admin'" + }, + "org_invitation_id": { + "name": "org_invitation_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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_invitation_workspace_id_workspace_id_fk": { + "name": "workspace_invitation_workspace_id_workspace_id_fk", + "tableFrom": "workspace_invitation", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_invitation_inviter_id_user_id_fk": { + "name": "workspace_invitation_inviter_id_user_id_fk", + "tableFrom": "workspace_invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_invitation_token_unique": { + "name": "workspace_invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": ["admin", "write", "read"] + }, + "public.webhook_delivery_status": { + "name": "webhook_delivery_status", + "schema": "public", + "values": ["pending", "in_progress", "success", "failed"] + }, + "public.workspace_invitation_status": { + "name": "workspace_invitation_status", + "schema": "public", + "values": ["pending", "accepted", "rejected", "cancelled"] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index 87d9ea223..0b4af6bdc 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -694,6 +694,13 @@ "when": 1760240967304, "tag": "0099_deep_sir_ram", "breakpoints": true + }, + { + "idx": 100, + "version": "7", + "when": 1760661127478, + "tag": "0100_public_black_cat", + "breakpoints": true } ] } diff --git a/packages/db/schema.ts b/packages/db/schema.ts index 8e9f57875..afcf756f8 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -1,5 +1,6 @@ import { type SQL, sql } from 'drizzle-orm' import { + bigint, boolean, check, customType, @@ -572,6 +573,8 @@ export const userStats = pgTable('user_stats', { totalCopilotCost: decimal('total_copilot_cost').notNull().default('0'), totalCopilotTokens: integer('total_copilot_tokens').notNull().default(0), totalCopilotCalls: integer('total_copilot_calls').notNull().default(0), + // Storage tracking (for free/pro users) + storageUsedBytes: bigint('storage_used_bytes', { mode: 'number' }).notNull().default(0), lastActive: timestamp('last_active').notNull().defaultNow(), billingBlocked: boolean('billing_blocked').notNull().default(false), }) @@ -670,6 +673,7 @@ export const organization = pgTable('organization', { logo: text('logo'), metadata: json('metadata'), orgUsageLimit: decimal('org_usage_limit'), + storageUsedBytes: bigint('storage_used_bytes', { mode: 'number' }).notNull().default(0), // Storage tracking for team/enterprise createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), }) @@ -725,6 +729,28 @@ export const workspace = pgTable('workspace', { updatedAt: timestamp('updated_at').notNull().defaultNow(), }) +export const workspaceFile = pgTable( + 'workspace_file', + { + id: text('id').primaryKey(), + workspaceId: text('workspace_id') + .notNull() + .references(() => workspace.id, { onDelete: 'cascade' }), + name: text('name').notNull(), + key: text('key').notNull().unique(), + size: integer('size').notNull(), + type: text('type').notNull(), + uploadedBy: text('uploaded_by') + .notNull() + .references(() => user.id, { onDelete: 'cascade' }), + uploadedAt: timestamp('uploaded_at').notNull().defaultNow(), + }, + (table) => ({ + workspaceIdIdx: index('workspace_file_workspace_id_idx').on(table.workspaceId), + keyIdx: index('workspace_file_key_idx').on(table.key), + }) +) + export const permissionTypeEnum = pgEnum('permission_type', ['admin', 'write', 'read']) export const workspaceInvitationStatusEnum = pgEnum('workspace_invitation_status', [