diff --git a/apps/sim/app/api/tools/supabase/storage-upload/route.ts b/apps/sim/app/api/tools/supabase/storage-upload/route.ts index 3e2dcd4cdd6..7f02f7791ea 100644 --- a/apps/sim/app/api/tools/supabase/storage-upload/route.ts +++ b/apps/sim/app/api/tools/supabase/storage-upload/route.ts @@ -11,6 +11,7 @@ import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils' import { downloadServableFileFromStorage } from '@/lib/uploads/utils/file-utils.server' import { docNotReadyResponse } from '@/lib/uploads/utils/servable-file-response' import { assertToolFileAccess } from '@/app/api/files/authorization' +import { encodeStoragePath, encodeStorageSegment } from '@/tools/supabase/utils' export const dynamic = 'force-dynamic' @@ -185,7 +186,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => { return NextResponse.json({ success: false, error: projectValidation.error }, { status: 400 }) } - const supabaseUrl = `https://${projectValidation.sanitized}.supabase.co/storage/v1/object/${validatedData.bucket}/${fullPath}` + const encodedBucket = encodeStorageSegment(validatedData.bucket) + const encodedPath = encodeStoragePath(fullPath) + const supabaseUrl = `https://${projectValidation.sanitized}.supabase.co/storage/v1/object/${encodedBucket}/${encodedPath}` const headers: Record = { apikey: validatedData.apiKey, @@ -248,7 +251,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { path: fullPath, }) - const publicUrl = `https://${projectValidation.sanitized}.supabase.co/storage/v1/object/public/${validatedData.bucket}/${fullPath}` + const publicUrl = `https://${projectValidation.sanitized}.supabase.co/storage/v1/object/public/${encodedBucket}/${encodedPath}` return NextResponse.json({ success: true, diff --git a/apps/sim/blocks/blocks/supabase.ts b/apps/sim/blocks/blocks/supabase.ts index 9f0d5eac72e..adadb2b1523 100644 --- a/apps/sim/blocks/blocks/supabase.ts +++ b/apps/sim/blocks/blocks/supabase.ts @@ -45,7 +45,10 @@ export const SupabaseBlock: BlockConfig = { { label: 'Storage: Copy File', id: 'storage_copy' }, { label: 'Storage: Get Public URL', id: 'storage_get_public_url' }, { label: 'Storage: Create Signed URL', id: 'storage_create_signed_url' }, + { label: 'Storage: Create Signed Upload URL', id: 'storage_create_signed_upload_url' }, { label: 'Storage: Create Bucket', id: 'storage_create_bucket' }, + { label: 'Storage: Update Bucket', id: 'storage_update_bucket' }, + { label: 'Storage: Empty Bucket', id: 'storage_empty_bucket' }, { label: 'Storage: List Buckets', id: 'storage_list_buckets' }, { label: 'Storage: Delete Bucket', id: 'storage_delete_bucket' }, ], @@ -686,9 +689,12 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e 'storage_move', 'storage_copy', 'storage_create_bucket', + 'storage_update_bucket', + 'storage_empty_bucket', 'storage_delete_bucket', 'storage_get_public_url', 'storage_create_signed_url', + 'storage_create_signed_upload_url', ], }, required: true, @@ -919,19 +925,53 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e value: () => 'false', condition: { field: 'operation', value: 'storage_create_bucket' }, }, + { + id: 'updateIsPublic', + title: 'Public Bucket', + type: 'dropdown', + options: [ + { label: 'Keep Current', id: '' }, + { label: 'False (Private)', id: 'false' }, + { label: 'True (Public)', id: 'true' }, + ], + value: () => '', + condition: { field: 'operation', value: 'storage_update_bucket' }, + }, { id: 'fileSizeLimit', title: 'File Size Limit (bytes)', type: 'short-input', placeholder: '52428800', - condition: { field: 'operation', value: 'storage_create_bucket' }, + condition: { field: 'operation', value: ['storage_create_bucket', 'storage_update_bucket'] }, + mode: 'advanced', }, { id: 'allowedMimeTypes', title: 'Allowed MIME Types (JSON array)', type: 'code', placeholder: '["image/png", "image/jpeg"]', - condition: { field: 'operation', value: 'storage_create_bucket' }, + condition: { field: 'operation', value: ['storage_create_bucket', 'storage_update_bucket'] }, + mode: 'advanced', + }, + { + id: 'path', + title: 'Destination Path', + type: 'short-input', + placeholder: 'folder/file.jpg', + condition: { field: 'operation', value: 'storage_create_signed_upload_url' }, + required: true, + }, + { + id: 'upsert', + title: 'Allow Overwrite', + type: 'dropdown', + options: [ + { label: 'False', id: 'false' }, + { label: 'True', id: 'true' }, + ], + value: () => 'false', + condition: { field: 'operation', value: 'storage_create_signed_upload_url' }, + mode: 'advanced', }, ], tools: { @@ -955,10 +995,13 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e 'supabase_storage_move', 'supabase_storage_copy', 'supabase_storage_create_bucket', + 'supabase_storage_update_bucket', + 'supabase_storage_empty_bucket', 'supabase_storage_list_buckets', 'supabase_storage_delete_bucket', 'supabase_storage_get_public_url', 'supabase_storage_create_signed_url', + 'supabase_storage_create_signed_upload_url', ], config: { tool: (params) => { @@ -1001,6 +1044,10 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e return 'supabase_storage_copy' case 'storage_create_bucket': return 'supabase_storage_create_bucket' + case 'storage_update_bucket': + return 'supabase_storage_update_bucket' + case 'storage_empty_bucket': + return 'supabase_storage_empty_bucket' case 'storage_list_buckets': return 'supabase_storage_list_buckets' case 'storage_delete_bucket': @@ -1009,6 +1056,8 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e return 'supabase_storage_get_public_url' case 'storage_create_signed_url': return 'supabase_storage_create_signed_url' + case 'storage_create_signed_upload_url': + return 'supabase_storage_create_signed_upload_url' default: throw new Error(`Invalid Supabase operation: ${params.operation}`) } @@ -1028,6 +1077,7 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e functionBody, functionHeaders, method, + updateIsPublic, ...rest } = params @@ -1200,6 +1250,16 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e result.isPublic = parsedIsPublic } + // "Keep Current" (empty string) means the caller didn't choose to + // override visibility — omit `isPublic` entirely so the update + // tool preserves the bucket's existing public/private setting + // instead of defaulting to false. + if (operation === 'storage_update_bucket' && updateIsPublic !== undefined) { + if (updateIsPublic === 'true' || updateIsPublic === 'false') { + result.isPublic = updateIsPublic === 'true' + } + } + if (normalizedFileData !== undefined) { result.fileData = normalizedFileData } @@ -1254,6 +1314,11 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e search: { type: 'string', description: 'Search term for filtering' }, expiresIn: { type: 'number', description: 'Expiration time in seconds for signed URL' }, isPublic: { type: 'boolean', description: 'Whether bucket should be public' }, + updateIsPublic: { + type: 'string', + description: + 'Visibility override for bucket update: "" keeps the current value, "true"/"false" overrides it', + }, fileSizeLimit: { type: 'number', description: 'Maximum file size in bytes' }, allowedMimeTypes: { type: 'array', description: 'Array of allowed MIME types' }, }, @@ -1281,7 +1346,15 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e }, signedUrl: { type: 'string', - description: 'Temporary signed URL for storage file', + description: 'Temporary signed URL for storage file (download or upload)', + }, + token: { + type: 'string', + description: 'Upload token embedded in the signed upload URL', + }, + path: { + type: 'string', + description: 'Destination object path for the signed upload URL', }, tables: { type: 'json', @@ -1300,9 +1373,9 @@ export const SupabaseBlockMeta = { templates: [ { icon: SupabaseIcon, - title: 'Supabase user provisioning', + title: 'Supabase customer record sync', prompt: - 'Build a workflow that listens for Stripe new-customer events, provisions a Supabase user with the correct role and metadata, and emails the welcome login link.', + 'Build a workflow that listens for Stripe new-customer events and upserts a row into a Supabase customers table with the correct plan and metadata, then emails a welcome message.', modules: ['agent', 'workflows'], category: 'operations', tags: ['enterprise', 'automation'], diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index d7e5eda0107..a4a5b3a4abe 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -3698,14 +3698,17 @@ import { supabaseRpcTool, supabaseStorageCopyTool, supabaseStorageCreateBucketTool, + supabaseStorageCreateSignedUploadUrlTool, supabaseStorageCreateSignedUrlTool, supabaseStorageDeleteBucketTool, supabaseStorageDeleteTool, supabaseStorageDownloadTool, + supabaseStorageEmptyBucketTool, supabaseStorageGetPublicUrlTool, supabaseStorageListBucketsTool, supabaseStorageListTool, supabaseStorageMoveTool, + supabaseStorageUpdateBucketTool, supabaseStorageUploadTool, supabaseTextSearchTool, supabaseUpdateTool, @@ -5207,10 +5210,13 @@ export const tools: Record = { supabase_storage_move: supabaseStorageMoveTool, supabase_storage_copy: supabaseStorageCopyTool, supabase_storage_create_bucket: supabaseStorageCreateBucketTool, + supabase_storage_update_bucket: supabaseStorageUpdateBucketTool, + supabase_storage_empty_bucket: supabaseStorageEmptyBucketTool, supabase_storage_list_buckets: supabaseStorageListBucketsTool, supabase_storage_delete_bucket: supabaseStorageDeleteBucketTool, supabase_storage_get_public_url: supabaseStorageGetPublicUrlTool, supabase_storage_create_signed_url: supabaseStorageCreateSignedUrlTool, + supabase_storage_create_signed_upload_url: supabaseStorageCreateSignedUploadUrlTool, tailscale_list_devices: tailscaleListDevicesTool, tailscale_get_device: tailscaleGetDeviceTool, tailscale_delete_device: tailscaleDeleteDeviceTool, diff --git a/apps/sim/tools/supabase/count.ts b/apps/sim/tools/supabase/count.ts index 7e5d2c0f3ad..144b41f00bd 100644 --- a/apps/sim/tools/supabase/count.ts +++ b/apps/sim/tools/supabase/count.ts @@ -7,7 +7,7 @@ export const countTool: ToolConfig = id: 'supabase_count', name: 'Supabase Count', description: 'Count rows in a Supabase table', - version: '1.0', + version: '1.0.0', params: { projectId: { diff --git a/apps/sim/tools/supabase/delete.ts b/apps/sim/tools/supabase/delete.ts index 967229868e1..d7460d6a6bd 100644 --- a/apps/sim/tools/supabase/delete.ts +++ b/apps/sim/tools/supabase/delete.ts @@ -7,7 +7,7 @@ export const deleteTool: ToolConfig 63) { - throw new Error(`Invalid value: ${value}`) - } - return value.replace(/'/g, "''") -} - /** - * SQL query filtered by specific schema - */ -const getSchemaFilteredSQL = (schema: string) => { - const safeSchema = escapeSqlString(schema) - return ` -WITH table_info AS ( - SELECT - t.table_schema, - t.table_name - FROM information_schema.tables t - WHERE t.table_type = 'BASE TABLE' - AND t.table_schema = '${safeSchema}' -), -columns_info AS ( - SELECT - c.table_schema, - c.table_name, - c.column_name, - c.data_type, - c.is_nullable, - c.column_default, - c.ordinal_position - FROM information_schema.columns c - INNER JOIN table_info t ON c.table_schema = t.table_schema AND c.table_name = t.table_name -), -pk_info AS ( - SELECT - tc.table_schema, - tc.table_name, - kcu.column_name - FROM information_schema.table_constraints tc - JOIN information_schema.key_column_usage kcu - ON tc.constraint_name = kcu.constraint_name - AND tc.table_schema = kcu.table_schema - WHERE tc.constraint_type = 'PRIMARY KEY' - AND tc.table_schema = '${safeSchema}' -), -fk_info AS ( - SELECT - tc.table_schema, - tc.table_name, - kcu.column_name, - ccu.table_name AS foreign_table_name, - ccu.column_name AS foreign_column_name - FROM information_schema.table_constraints tc - JOIN information_schema.key_column_usage kcu - ON tc.constraint_name = kcu.constraint_name - AND tc.table_schema = kcu.table_schema - JOIN information_schema.constraint_column_usage ccu - ON ccu.constraint_name = tc.constraint_name - WHERE tc.constraint_type = 'FOREIGN KEY' - AND tc.table_schema = '${safeSchema}' -), -index_info AS ( - SELECT - schemaname AS table_schema, - tablename AS table_name, - indexname AS index_name, - CASE WHEN indexdef LIKE '%UNIQUE%' THEN true ELSE false END AS is_unique, - indexdef - FROM pg_indexes - WHERE schemaname = '${safeSchema}' -) -SELECT json_build_object( - 'tables', ( - SELECT json_agg( - json_build_object( - 'schema', t.table_schema, - 'name', t.table_name, - 'columns', ( - SELECT json_agg( - json_build_object( - 'name', c.column_name, - 'type', c.data_type, - 'nullable', c.is_nullable = 'YES', - 'default', c.column_default, - 'isPrimaryKey', EXISTS ( - SELECT 1 FROM pk_info pk - WHERE pk.table_schema = c.table_schema - AND pk.table_name = c.table_name - AND pk.column_name = c.column_name - ), - 'isForeignKey', EXISTS ( - SELECT 1 FROM fk_info fk - WHERE fk.table_schema = c.table_schema - AND fk.table_name = c.table_name - AND fk.column_name = c.column_name - ), - 'references', ( - SELECT json_build_object('table', fk.foreign_table_name, 'column', fk.foreign_column_name) - FROM fk_info fk - WHERE fk.table_schema = c.table_schema - AND fk.table_name = c.table_name - AND fk.column_name = c.column_name - LIMIT 1 - ) - ) - ORDER BY c.ordinal_position - ) - FROM columns_info c - WHERE c.table_schema = t.table_schema AND c.table_name = t.table_name - ), - 'primaryKey', ( - SELECT COALESCE(json_agg(pk.column_name), '[]'::json) - FROM pk_info pk - WHERE pk.table_schema = t.table_schema AND pk.table_name = t.table_name - ), - 'foreignKeys', ( - SELECT COALESCE(json_agg( - json_build_object( - 'column', fk.column_name, - 'referencesTable', fk.foreign_table_name, - 'referencesColumn', fk.foreign_column_name - ) - ), '[]'::json) - FROM fk_info fk - WHERE fk.table_schema = t.table_schema AND fk.table_name = t.table_name - ), - 'indexes', ( - SELECT COALESCE(json_agg( - json_build_object( - 'name', idx.index_name, - 'unique', idx.is_unique, - 'definition', idx.indexdef - ) - ), '[]'::json) - FROM index_info idx - WHERE idx.table_schema = t.table_schema AND idx.table_name = t.table_name - ) - ) - ) - FROM table_info t - ), - 'schemas', ( - SELECT COALESCE(json_agg(DISTINCT table_schema), '[]'::json) - FROM table_info - ) -) AS result; -` -} - -/** - * Tool for introspecting Supabase database schema - * Uses raw SQL execution via PostgREST to retrieve table structures + * Tool for introspecting Supabase database schema. + * + * PostgREST (which powers `/rest/v1`) has no generic "run arbitrary SQL" + * endpoint, so schema introspection is derived from the project's + * auto-generated OpenAPI spec (`GET /rest/v1/` with an OpenAPI `Accept` + * header) rather than a live `information_schema` query. Primary-key + * detection is a best-effort naming heuristic (`id` column), and + * foreign-key detection only succeeds if the table owner has added a + * matching `references table.column` SQL comment — the OpenAPI spec does + * not expose constraint metadata directly. Index information is not + * available via this API at all. `nullable` is also best-effort: PostgREST + * only lists a column under `required` when it's NOT NULL *and* has no + * default, so a NOT NULL column with a default is reported as nullable. */ export const introspectTool: ToolConfig = { id: 'supabase_introspect', name: 'Supabase Introspect', description: - 'Introspect Supabase database schema to get table structures, columns, and relationships', - version: '1.0', + 'Introspect Supabase database schema from its OpenAPI spec to get table and column structures (best-effort primary/foreign key detection)', + version: '1.0.0', params: { projectId: { @@ -336,127 +53,31 @@ export const introspectTool: ToolConfig { - return `${supabaseBaseUrl(params.projectId)}/rest/v1/rpc/` - }, - method: 'POST', + url: (params) => `${supabaseBaseUrl(params.projectId)}/rest/v1/`, + method: 'GET', headers: (params) => ({ apikey: params.apiKey, Authorization: `Bearer ${params.apiKey}`, - 'Content-Type': 'application/json', + Accept: 'application/openapi+json', + ...(params.schema ? { 'Accept-Profile': params.schema } : {}), }), - body: () => ({}), }, - directExecution: async ( - params: SupabaseIntrospectParams - ): Promise => { - const { apiKey, projectId, schema } = params - - try { - const sqlQuery = schema ? getSchemaFilteredSQL(schema) : INTROSPECTION_SQL - const baseUrl = supabaseBaseUrl(projectId) - - const response = await fetch(`${baseUrl}/rest/v1/rpc/`, { - method: 'POST', - headers: { - apikey: apiKey, - Authorization: `Bearer ${apiKey}`, - 'Content-Type': 'application/json', - Prefer: 'return=representation', - }, - body: JSON.stringify({ - query: sqlQuery, - }), - }) - - if (!response.ok) { - const errorText = await response.text() - logger.warn('Direct RPC call failed, attempting alternative approach', { - status: response.status, - }) - - const pgResponse = await fetch(`${baseUrl}/rest/v1/?select=*`, { - method: 'GET', - headers: { - apikey: apiKey, - Authorization: `Bearer ${apiKey}`, - Accept: 'application/openapi+json', - }, - }) - - if (!pgResponse.ok) { - throw new Error(`Failed to introspect database: ${errorText}`) - } - - const openApiSpec = await pgResponse.json() - const tables = parseOpenApiSpec(openApiSpec, schema) - - return { - success: true, - output: { - message: `Successfully introspected ${tables.length} table(s) from database schema`, - tables, - schemas: [...new Set(tables.map((t) => t.schema))], - }, - } - } - - const data = await response.json() - const result = Array.isArray(data) && data.length > 0 ? data[0].result : data.result || data - - const tables: SupabaseTableSchema[] = (result.tables || []).map((table: any) => ({ - name: table.name, - schema: table.schema, - columns: (table.columns || []).map((col: any) => ({ - name: col.name, - type: col.type, - nullable: col.nullable, - default: col.default, - isPrimaryKey: col.isPrimaryKey, - isForeignKey: col.isForeignKey, - references: col.references, - })), - primaryKey: table.primaryKey || [], - foreignKeys: table.foreignKeys || [], - indexes: (table.indexes || []).map((idx: any) => ({ - name: idx.name, - columns: parseIndexColumns(idx.definition || ''), - unique: idx.unique, - })), - })) - - return { - success: true, - output: { - message: `Successfully introspected ${tables.length} table(s) from database`, - tables, - schemas: result.schemas || [], - }, - } - } catch (error) { - logger.error('Supabase introspection failed', { error }) - const errorMessage = getErrorMessage(error, 'Unknown error occurred') - return { - success: false, - output: { - message: 'Failed to introspect database schema', - tables: [], - schemas: [], - }, - error: errorMessage, - } + transformResponse: async (response: Response, params?: SupabaseIntrospectParams) => { + if (!response.ok) { + const errorText = await response.text() + throw new Error(`Failed to introspect database: ${errorText}`) } - }, - transformResponse: async (response: Response) => { - const data = await response.json() + const openApiSpec = await response.json() + const tables = parseOpenApiSpec(openApiSpec, params?.schema) + return { success: true, output: { - message: 'Schema introspection completed', - tables: data.tables || [], - schemas: data.schemas || [], + message: `Successfully introspected ${tables.length} table(s) from database schema`, + tables, + schemas: [...new Set(tables.map((t) => t.schema))], }, } }, @@ -476,19 +97,13 @@ export const introspectTool: ToolConfig col.trim().replace(/"/g, '')) - } - return [] -} - -/** - * Parse OpenAPI spec to extract table schema information - * This is a fallback method when direct SQL execution is not available + * Parse a PostgREST-generated OpenAPI spec into table schemas. + * + * `isPrimaryKey` is a naming heuristic (`id` column) — PostgREST does not + * expose real constraint metadata in the spec. `isForeignKey`/`references` + * only populate when the table owner has added a `references table.column` + * SQL comment on the column. `indexes` is always empty: index definitions + * are not part of the OpenAPI spec. */ function parseOpenApiSpec(spec: any, filterSchema?: string): SupabaseTableSchema[] { const tables: SupabaseTableSchema[] = [] @@ -511,7 +126,7 @@ function parseOpenApiSpec(spec: any, filterSchema?: string): SupabaseTableSchema for (const [colName, colDef] of Object.entries(properties)) { const col = colDef as any - const isPK = col.description?.includes('primary key') || colName === 'id' + const isPK = colName === 'id' const fkMatch = col.description?.match(/references\s+(\w+)\.(\w+)/) const column: SupabaseColumnSchema = { @@ -539,18 +154,18 @@ function parseOpenApiSpec(spec: any, filterSchema?: string): SupabaseTableSchema columns.push(column) } - const schemaName = filterSchema || 'public' - - if (!filterSchema || schemaName === filterSchema) { - tables.push({ - name: tableName, - schema: schemaName, - columns, - primaryKey, - foreignKeys, - indexes: [], - }) - } + tables.push({ + name: tableName, + // The OpenAPI spec doesn't map tables to schemas, so this can only + // reflect the schema that was actually requested (or "public" when + // introspecting the default schema) — not necessarily the table's + // true schema in a multi-schema database. + schema: filterSchema || 'public', + columns, + primaryKey, + foreignKeys, + indexes: [], + }) } return tables diff --git a/apps/sim/tools/supabase/invoke_function.ts b/apps/sim/tools/supabase/invoke_function.ts index 4b02f65bd85..016a508f741 100644 --- a/apps/sim/tools/supabase/invoke_function.ts +++ b/apps/sim/tools/supabase/invoke_function.ts @@ -34,7 +34,7 @@ export const invokeFunctionTool: ToolConfig< id: 'supabase_invoke_function', name: 'Supabase Invoke Edge Function', description: 'Invoke a Supabase Edge Function over HTTP', - version: '1.0', + version: '1.0.0', params: { projectId: { diff --git a/apps/sim/tools/supabase/query.ts b/apps/sim/tools/supabase/query.ts index 0fcf1b75dda..f80bc866b31 100644 --- a/apps/sim/tools/supabase/query.ts +++ b/apps/sim/tools/supabase/query.ts @@ -7,7 +7,7 @@ export const queryTool: ToolConfig = id: 'supabase_query', name: 'Supabase Query', description: 'Query data from a Supabase table', - version: '1.0', + version: '1.0.0', params: { projectId: { diff --git a/apps/sim/tools/supabase/rpc.ts b/apps/sim/tools/supabase/rpc.ts index c9295c0854a..19c394646a9 100644 --- a/apps/sim/tools/supabase/rpc.ts +++ b/apps/sim/tools/supabase/rpc.ts @@ -7,7 +7,7 @@ export const rpcTool: ToolConfig = { id: 'supabase_rpc', name: 'Supabase RPC', description: 'Call a PostgreSQL function in Supabase', - version: '1.0', + version: '1.0.0', params: { projectId: { diff --git a/apps/sim/tools/supabase/storage_copy.ts b/apps/sim/tools/supabase/storage_copy.ts index 027a03b823f..ac396f0f687 100644 --- a/apps/sim/tools/supabase/storage_copy.ts +++ b/apps/sim/tools/supabase/storage_copy.ts @@ -10,7 +10,7 @@ export const storageCopyTool: ToolConfig = { + id: 'supabase_storage_create_signed_upload_url', + name: 'Supabase Storage Create Signed Upload URL', + description: + 'Create a temporary signed URL a client can use to upload directly to a Supabase storage bucket', + version: '1.0.0', + + params: { + projectId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Supabase project ID (e.g., jdrkgepadsdopsntdlom)', + }, + bucket: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The name of the storage bucket', + }, + path: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The destination path for the uploaded file (e.g., "folder/file.jpg")', + }, + upsert: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'If true, allows overwriting an existing file at this path (default: false)', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Supabase service role secret key', + }, + }, + + request: { + url: (params) => { + const bucket = encodeStorageSegment(params.bucket) + const path = encodeStoragePath(params.path) + return `${supabaseBaseUrl(params.projectId)}/storage/v1/object/upload/sign/${bucket}/${path}` + }, + method: 'POST', + headers: (params) => ({ + apikey: params.apiKey, + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/json', + ...(params.upsert ? { 'x-upsert': 'true' } : {}), + }), + body: () => ({}), + }, + + transformResponse: async ( + response: Response, + params?: SupabaseStorageCreateSignedUploadUrlParams + ) => { + let data + try { + data = await response.json() + } catch (parseError) { + throw new Error( + `Failed to parse Supabase storage create signed upload URL response: ${parseError}` + ) + } + + if (!response.ok) { + throw new Error( + `Failed to create signed upload URL: ${data.message || data.error || response.statusText}` + ) + } + + const relativeUrl = data.url + if (!relativeUrl) { + throw new Error('Supabase did not return a signed upload URL path in its response') + } + if (!params?.projectId) { + throw new Error('projectId is required to construct the signed upload URL') + } + + return { + success: true, + output: { + message: 'Successfully created signed upload URL', + signedUrl: `${supabaseBaseUrl(params.projectId)}/storage/v1${relativeUrl}`, + // The API response has no `path` field — it's the caller-supplied + // destination path, echoed back the same way the official + // storage-js client does. + path: params.path, + token: data.token, + }, + error: undefined, + } + }, + + outputs: { + message: { type: 'string', description: 'Operation status message' }, + signedUrl: { + type: 'string', + description: 'The temporary signed URL a client can PUT the file to', + }, + path: { type: 'string', description: 'The destination object path' }, + token: { type: 'string', description: 'The upload token embedded in the signed URL' }, + }, +} diff --git a/apps/sim/tools/supabase/storage_create_signed_url.ts b/apps/sim/tools/supabase/storage_create_signed_url.ts index 93153af43db..eb10d497667 100644 --- a/apps/sim/tools/supabase/storage_create_signed_url.ts +++ b/apps/sim/tools/supabase/storage_create_signed_url.ts @@ -2,7 +2,7 @@ import type { SupabaseStorageCreateSignedUrlParams, SupabaseStorageCreateSignedUrlResponse, } from '@/tools/supabase/types' -import { supabaseBaseUrl } from '@/tools/supabase/utils' +import { encodeStoragePath, encodeStorageSegment, supabaseBaseUrl } from '@/tools/supabase/utils' import type { ToolConfig } from '@/tools/types' export const storageCreateSignedUrlTool: ToolConfig< @@ -12,7 +12,7 @@ export const storageCreateSignedUrlTool: ToolConfig< id: 'supabase_storage_create_signed_url', name: 'Supabase Storage Create Signed URL', description: 'Create a temporary signed URL for a file in a Supabase storage bucket', - version: '1.0', + version: '1.0.0', params: { projectId: { @@ -55,7 +55,9 @@ export const storageCreateSignedUrlTool: ToolConfig< request: { url: (params) => { - return `${supabaseBaseUrl(params.projectId)}/storage/v1/object/sign/${params.bucket}/${params.path}` + const bucket = encodeStorageSegment(params.bucket) + const path = encodeStoragePath(params.path) + return `${supabaseBaseUrl(params.projectId)}/storage/v1/object/sign/${bucket}/${path}` }, method: 'POST', headers: (params) => ({ @@ -63,17 +65,9 @@ export const storageCreateSignedUrlTool: ToolConfig< Authorization: `Bearer ${params.apiKey}`, 'Content-Type': 'application/json', }), - body: (params) => { - const payload: any = { - expiresIn: Number(params.expiresIn), - } - - if (params.download !== undefined) { - payload.download = params.download - } - - return payload - }, + body: (params) => ({ + expiresIn: Number(params.expiresIn), + }), }, transformResponse: async (response: Response, params?: SupabaseStorageCreateSignedUrlParams) => { @@ -84,11 +78,28 @@ export const storageCreateSignedUrlTool: ToolConfig< throw new Error(`Failed to parse Supabase storage create signed URL response: ${parseError}`) } + if (!response.ok) { + throw new Error( + `Failed to create signed URL: ${data.message || data.error || response.statusText}` + ) + } + const relativePath = data.signedURL || data.signedUrl + if (!relativePath) { + throw new Error('Supabase did not return a signed URL path in its response') + } if (!params?.projectId) { throw new Error('projectId is required to construct the signed URL') } - const fullUrl = `${supabaseBaseUrl(params.projectId)}/storage/v1${relativePath}` + let fullUrl = `${supabaseBaseUrl(params.projectId)}/storage/v1${relativePath}` + + // The Storage API ignores a `download` field in the sign request body — + // forcing download is a client-side query param on the resulting URL. + // An empty value preserves the original filename; a non-empty value + // would override it, so a boolean "true" must never be sent literally. + if (params.download) { + fullUrl += fullUrl.includes('?') ? '&download=' : '?download=' + } return { success: true, diff --git a/apps/sim/tools/supabase/storage_delete.ts b/apps/sim/tools/supabase/storage_delete.ts index e0228bb1810..9cf876a41c9 100644 --- a/apps/sim/tools/supabase/storage_delete.ts +++ b/apps/sim/tools/supabase/storage_delete.ts @@ -3,7 +3,7 @@ import { type SupabaseStorageDeleteParams, type SupabaseStorageDeleteResponse, } from '@/tools/supabase/types' -import { supabaseBaseUrl } from '@/tools/supabase/utils' +import { encodeStorageSegment, supabaseBaseUrl } from '@/tools/supabase/utils' import type { ToolConfig } from '@/tools/types' export const storageDeleteTool: ToolConfig< @@ -13,7 +13,7 @@ export const storageDeleteTool: ToolConfig< id: 'supabase_storage_delete', name: 'Supabase Storage Delete', description: 'Delete files from a Supabase storage bucket', - version: '1.0', + version: '1.0.0', params: { projectId: { @@ -44,7 +44,7 @@ export const storageDeleteTool: ToolConfig< request: { url: (params) => { - return `${supabaseBaseUrl(params.projectId)}/storage/v1/object/${params.bucket}` + return `${supabaseBaseUrl(params.projectId)}/storage/v1/object/${encodeStorageSegment(params.bucket)}` }, method: 'DELETE', headers: (params) => ({ diff --git a/apps/sim/tools/supabase/storage_delete_bucket.ts b/apps/sim/tools/supabase/storage_delete_bucket.ts index 621ba2341ec..25b0dae0829 100644 --- a/apps/sim/tools/supabase/storage_delete_bucket.ts +++ b/apps/sim/tools/supabase/storage_delete_bucket.ts @@ -1,9 +1,9 @@ import { - STORAGE_DELETE_BUCKET_OUTPUT_PROPERTIES, + STORAGE_MESSAGE_OUTPUT_PROPERTIES, type SupabaseStorageDeleteBucketParams, type SupabaseStorageDeleteBucketResponse, } from '@/tools/supabase/types' -import { supabaseBaseUrl } from '@/tools/supabase/utils' +import { encodeStorageSegment, supabaseBaseUrl } from '@/tools/supabase/utils' import type { ToolConfig } from '@/tools/types' export const storageDeleteBucketTool: ToolConfig< @@ -13,7 +13,7 @@ export const storageDeleteBucketTool: ToolConfig< id: 'supabase_storage_delete_bucket', name: 'Supabase Storage Delete Bucket', description: 'Delete a storage bucket in Supabase', - version: '1.0', + version: '1.0.0', params: { projectId: { @@ -38,7 +38,7 @@ export const storageDeleteBucketTool: ToolConfig< request: { url: (params) => { - return `${supabaseBaseUrl(params.projectId)}/storage/v1/bucket/${params.bucket}` + return `${supabaseBaseUrl(params.projectId)}/storage/v1/bucket/${encodeStorageSegment(params.bucket)}` }, method: 'DELETE', headers: (params) => ({ @@ -70,7 +70,7 @@ export const storageDeleteBucketTool: ToolConfig< results: { type: 'object', description: 'Delete operation result', - properties: STORAGE_DELETE_BUCKET_OUTPUT_PROPERTIES, + properties: STORAGE_MESSAGE_OUTPUT_PROPERTIES, }, }, } diff --git a/apps/sim/tools/supabase/storage_download.ts b/apps/sim/tools/supabase/storage_download.ts index d47977f61d8..4079f2e4330 100644 --- a/apps/sim/tools/supabase/storage_download.ts +++ b/apps/sim/tools/supabase/storage_download.ts @@ -4,7 +4,7 @@ import { type SupabaseStorageDownloadParams, type SupabaseStorageDownloadResponse, } from '@/tools/supabase/types' -import { supabaseBaseUrl } from '@/tools/supabase/utils' +import { encodeStoragePath, encodeStorageSegment, supabaseBaseUrl } from '@/tools/supabase/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('SupabaseStorageDownloadTool') @@ -16,7 +16,7 @@ export const storageDownloadTool: ToolConfig< id: 'supabase_storage_download', name: 'Supabase Storage Download', description: 'Download a file from a Supabase storage bucket', - version: '1.0', + version: '1.0.0', params: { projectId: { @@ -53,7 +53,9 @@ export const storageDownloadTool: ToolConfig< request: { url: (params) => { - return `${supabaseBaseUrl(params.projectId)}/storage/v1/object/${params.bucket}/${params.path}` + const bucket = encodeStorageSegment(params.bucket) + const path = encodeStoragePath(params.path) + return `${supabaseBaseUrl(params.projectId)}/storage/v1/object/${bucket}/${path}` }, method: 'GET', headers: (params) => ({ diff --git a/apps/sim/tools/supabase/storage_empty_bucket.ts b/apps/sim/tools/supabase/storage_empty_bucket.ts new file mode 100644 index 00000000000..ba86d845bea --- /dev/null +++ b/apps/sim/tools/supabase/storage_empty_bucket.ts @@ -0,0 +1,86 @@ +import { + STORAGE_MESSAGE_OUTPUT_PROPERTIES, + type SupabaseStorageEmptyBucketParams, + type SupabaseStorageEmptyBucketResponse, +} from '@/tools/supabase/types' +import { encodeStorageSegment, supabaseBaseUrl } from '@/tools/supabase/utils' +import type { ToolConfig } from '@/tools/types' + +export const storageEmptyBucketTool: ToolConfig< + SupabaseStorageEmptyBucketParams, + SupabaseStorageEmptyBucketResponse +> = { + id: 'supabase_storage_empty_bucket', + name: 'Supabase Storage Empty Bucket', + description: + 'Delete all objects inside a Supabase storage bucket without deleting the bucket itself', + version: '1.0.0', + + params: { + projectId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Supabase project ID (e.g., jdrkgepadsdopsntdlom)', + }, + bucket: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The name of the bucket to empty', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Supabase service role secret key', + }, + }, + + request: { + url: (params) => { + const bucket = encodeStorageSegment(params.bucket) + return `${supabaseBaseUrl(params.projectId)}/storage/v1/bucket/${bucket}/empty` + }, + method: 'POST', + headers: (params) => ({ + apikey: params.apiKey, + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/json', + }), + body: () => ({}), + }, + + transformResponse: async (response: Response) => { + let data + try { + data = await response.json() + } catch (parseError) { + throw new Error(`Failed to parse Supabase storage empty bucket response: ${parseError}`) + } + + if (!response.ok) { + throw new Error( + `Failed to empty storage bucket: ${data.message || data.error || response.statusText}` + ) + } + + return { + success: true, + output: { + message: data.message || 'Successfully emptied storage bucket', + results: data, + }, + error: undefined, + } + }, + + outputs: { + message: { type: 'string', description: 'Operation status message' }, + results: { + type: 'object', + description: 'Empty bucket operation result', + properties: STORAGE_MESSAGE_OUTPUT_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/supabase/storage_get_public_url.ts b/apps/sim/tools/supabase/storage_get_public_url.ts index 64bee51541a..b189f9bd543 100644 --- a/apps/sim/tools/supabase/storage_get_public_url.ts +++ b/apps/sim/tools/supabase/storage_get_public_url.ts @@ -2,7 +2,7 @@ import type { SupabaseStorageGetPublicUrlParams, SupabaseStorageGetPublicUrlResponse, } from '@/tools/supabase/types' -import { supabaseBaseUrl } from '@/tools/supabase/utils' +import { encodeStoragePath, encodeStorageSegment, supabaseBaseUrl } from '@/tools/supabase/utils' import type { ToolConfig } from '@/tools/types' export const storageGetPublicUrlTool: ToolConfig< @@ -12,7 +12,7 @@ export const storageGetPublicUrlTool: ToolConfig< id: 'supabase_storage_get_public_url', name: 'Supabase Storage Get Public URL', description: 'Get the public URL for a file in a Supabase storage bucket', - version: '1.0', + version: '1.0.0', params: { projectId: { @@ -48,10 +48,16 @@ export const storageGetPublicUrlTool: ToolConfig< * its response. */ directExecution: async (params: SupabaseStorageGetPublicUrlParams) => { - let publicUrl = `${supabaseBaseUrl(params.projectId)}/storage/v1/object/public/${params.bucket}/${params.path}` + const bucket = encodeStorageSegment(params.bucket) + const path = encodeStoragePath(params.path) + let publicUrl = `${supabaseBaseUrl(params.projectId)}/storage/v1/object/public/${bucket}/${path}` if (params.download) { - publicUrl += '?download=true' + // Supabase's `download` query param is a filename override, not a + // boolean flag — an empty value forces a download while preserving + // the original filename. Sending the literal string "true" would + // instead rename the downloaded file to "true". + publicUrl += '?download=' } return { @@ -65,8 +71,11 @@ export const storageGetPublicUrlTool: ToolConfig< }, request: { - url: (params) => - `${supabaseBaseUrl(params.projectId)}/storage/v1/object/public/${params.bucket}/${params.path}`, + url: (params) => { + const bucket = encodeStorageSegment(params.bucket) + const path = encodeStoragePath(params.path) + return `${supabaseBaseUrl(params.projectId)}/storage/v1/object/public/${bucket}/${path}` + }, method: 'GET', headers: () => ({}), }, diff --git a/apps/sim/tools/supabase/storage_list.ts b/apps/sim/tools/supabase/storage_list.ts index fd13ca475cb..2d20468f3d4 100644 --- a/apps/sim/tools/supabase/storage_list.ts +++ b/apps/sim/tools/supabase/storage_list.ts @@ -3,14 +3,14 @@ import { type SupabaseStorageListParams, type SupabaseStorageListResponse, } from '@/tools/supabase/types' -import { supabaseBaseUrl } from '@/tools/supabase/utils' +import { encodeStorageSegment, supabaseBaseUrl } from '@/tools/supabase/utils' import type { ToolConfig } from '@/tools/types' export const storageListTool: ToolConfig = { id: 'supabase_storage_list', name: 'Supabase Storage List', description: 'List files in a Supabase storage bucket', - version: '1.0', + version: '1.0.0', params: { projectId: { @@ -72,7 +72,7 @@ export const storageListTool: ToolConfig { - return `${supabaseBaseUrl(params.projectId)}/storage/v1/object/list/${params.bucket}` + return `${supabaseBaseUrl(params.projectId)}/storage/v1/object/list/${encodeStorageSegment(params.bucket)}` }, method: 'POST', headers: (params) => ({ diff --git a/apps/sim/tools/supabase/storage_list_buckets.ts b/apps/sim/tools/supabase/storage_list_buckets.ts index a7ede783ca4..e03ddff3016 100644 --- a/apps/sim/tools/supabase/storage_list_buckets.ts +++ b/apps/sim/tools/supabase/storage_list_buckets.ts @@ -13,7 +13,7 @@ export const storageListBucketsTool: ToolConfig< id: 'supabase_storage_list_buckets', name: 'Supabase Storage List Buckets', description: 'List all storage buckets in Supabase', - version: '1.0', + version: '1.0.0', params: { projectId: { diff --git a/apps/sim/tools/supabase/storage_move.ts b/apps/sim/tools/supabase/storage_move.ts index 5f3c2edf30a..23e149e21ee 100644 --- a/apps/sim/tools/supabase/storage_move.ts +++ b/apps/sim/tools/supabase/storage_move.ts @@ -10,7 +10,7 @@ export const storageMoveTool: ToolConfig = { + id: 'supabase_storage_update_bucket', + name: 'Supabase Storage Update Bucket', + description: 'Update the configuration of an existing Supabase storage bucket', + version: '1.0.0', + + params: { + projectId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Supabase project ID (e.g., jdrkgepadsdopsntdlom)', + }, + bucket: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The name of the bucket to update', + }, + isPublic: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: + 'Whether the bucket should be publicly accessible (leave unset to keep the current value)', + }, + fileSizeLimit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum file size in bytes (leave unset to keep the current value)', + }, + allowedMimeTypes: { + type: 'array', + required: false, + visibility: 'user-or-llm', + description: + 'Array of allowed MIME types (e.g., ["image/png", "image/jpeg"]) — leave unset to keep the current value', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Supabase service role secret key', + }, + }, + + /** + * Unreachable: `directExecution` below always handles this tool because + * the update must first read the bucket's current configuration (the + * Storage API's update-bucket endpoint is a full-replace PUT, not a + * partial patch). Declared only to satisfy `ToolConfig`'s required + * `request` field. + */ + request: { + url: (params) => + `${supabaseBaseUrl(params.projectId)}/storage/v1/bucket/${encodeStorageSegment(params.bucket)}`, + method: 'PUT', + headers: (params) => ({ + apikey: params.apiKey, + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/json', + }), + }, + + /** + * The Storage API's update-bucket endpoint is a full-replace PUT + * (`{id, name, public, file_size_limit?, allowed_mime_types?}`), not a + * partial patch. Fetching the bucket's current configuration first lets + * unset params fall back to their existing value instead of silently + * resetting to a default (e.g. flipping a public bucket private just + * because `isPublic` wasn't provided). + */ + directExecution: async ( + params: SupabaseStorageUpdateBucketParams + ): Promise => { + const baseUrl = supabaseBaseUrl(params.projectId) + const bucket = encodeStorageSegment(params.bucket) + const headers = { + apikey: params.apiKey, + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/json', + } + + try { + const currentResponse = await fetch(`${baseUrl}/storage/v1/bucket/${bucket}`, { + method: 'GET', + headers, + }) + + if (!currentResponse.ok) { + const errorText = await currentResponse.text() + throw new Error(`Failed to read current bucket configuration: ${errorText}`) + } + + const current = await currentResponse.json() + + // Block subBlocks for a shared field can forward an empty string + // (e.g. an untouched short-input) rather than omitting the key + // entirely — treat that the same as "not provided" so it falls + // back to the bucket's current value instead of coercing to 0/false. + const hasValue = (value: unknown): boolean => + value !== undefined && value !== null && value !== '' + + const payload: any = { + id: params.bucket, + name: params.bucket, + public: hasValue(params.isPublic) ? params.isPublic : Boolean(current.public), + file_size_limit: hasValue(params.fileSizeLimit) + ? Number(params.fileSizeLimit) + : (current.file_size_limit ?? null), + allowed_mime_types: hasValue(params.allowedMimeTypes) + ? params.allowedMimeTypes + : (current.allowed_mime_types ?? null), + } + + const updateResponse = await fetch(`${baseUrl}/storage/v1/bucket/${bucket}`, { + method: 'PUT', + headers, + body: JSON.stringify(payload), + }) + + if (!updateResponse.ok) { + const errorText = await updateResponse.text() + throw new Error(`Failed to update bucket: ${errorText}`) + } + + const data = await updateResponse.json() + + return { + success: true, + output: { + message: 'Successfully updated storage bucket', + results: data, + }, + error: undefined, + } + } catch (error) { + return { + success: false, + output: { + message: 'Failed to update storage bucket', + results: {}, + }, + error: getErrorMessage(error, 'Unknown error occurred'), + } + } + }, + + outputs: { + message: { type: 'string', description: 'Operation status message' }, + results: { + type: 'object', + description: 'Update operation result', + properties: STORAGE_MESSAGE_OUTPUT_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/supabase/storage_upload.ts b/apps/sim/tools/supabase/storage_upload.ts index 8f99c182797..b4298fd6bc8 100644 --- a/apps/sim/tools/supabase/storage_upload.ts +++ b/apps/sim/tools/supabase/storage_upload.ts @@ -12,7 +12,7 @@ export const storageUploadTool: ToolConfig< id: 'supabase_storage_upload', name: 'Supabase Storage Upload', description: 'Upload a file to a Supabase storage bucket', - version: '1.0', + version: '1.0.0', params: { projectId: { diff --git a/apps/sim/tools/supabase/text_search.ts b/apps/sim/tools/supabase/text_search.ts index 5aaef93bdc1..271518afaba 100644 --- a/apps/sim/tools/supabase/text_search.ts +++ b/apps/sim/tools/supabase/text_search.ts @@ -7,7 +7,7 @@ export const textSearchTool: ToolConfig /** - * Output definition for storage delete bucket response - * Returns a confirmation message + * Output definition for storage bucket operations that only return a + * confirmation message (delete bucket, update bucket, empty bucket) + * @see https://github.com/supabase/storage-js/blob/main/src/packages/StorageBucketApi.ts */ -export const STORAGE_DELETE_BUCKET_OUTPUT_PROPERTIES = { +export const STORAGE_MESSAGE_OUTPUT_PROPERTIES = { message: { type: 'string', description: 'Operation status message' }, } as const satisfies Record @@ -231,13 +232,24 @@ export const INTROSPECT_REFERENCE_OUTPUT_PROPERTIES = { export const INTROSPECT_COLUMN_OUTPUT_PROPERTIES = { name: { type: 'string', description: 'Column name' }, type: { type: 'string', description: 'Column data type' }, - nullable: { type: 'boolean', description: 'Whether the column allows null values' }, + nullable: { + type: 'boolean', + description: + 'Whether the column allows null values — a NOT NULL column that has a default value is misreported as nullable, since the OpenAPI spec this is derived from omits it from the required list in that case', + }, default: { type: 'string', description: 'Default value for the column', optional: true }, - isPrimaryKey: { type: 'boolean', description: 'Whether the column is a primary key' }, - isForeignKey: { type: 'boolean', description: 'Whether the column is a foreign key' }, + isPrimaryKey: { + type: 'boolean', + description: 'Best-effort guess based on the column being named "id" (not authoritative)', + }, + isForeignKey: { + type: 'boolean', + description: + 'True only if the column has a "references table.column" SQL comment; most databases will show false even for real foreign keys', + }, references: { type: 'object', - description: 'Foreign key reference details', + description: 'Foreign key reference details, when detected via SQL comment', optional: true, properties: INTROSPECT_REFERENCE_OUTPUT_PROPERTIES, }, @@ -294,7 +306,8 @@ export const INTROSPECT_TABLE_OUTPUT_PROPERTIES = { }, indexes: { type: 'array', - description: 'Array of index definitions', + description: + 'Always empty — index definitions are not exposed by the OpenAPI spec this tool reads', items: { type: 'object', properties: INTROSPECT_INDEX_OUTPUT_PROPERTIES, @@ -589,6 +602,43 @@ export interface SupabaseStorageCreateSignedUrlResponse extends ToolResponse { error?: string } +export interface SupabaseStorageCreateSignedUploadUrlParams { + apiKey: string + projectId: string + bucket: string + path: string + upsert?: boolean +} + +export interface SupabaseStorageCreateSignedUploadUrlResponse extends ToolResponse { + output: { + message: string + signedUrl: string + path: string + token: string + } + error?: string +} + +export interface SupabaseStorageUpdateBucketParams { + apiKey: string + projectId: string + bucket: string + isPublic?: boolean + fileSizeLimit?: number + allowedMimeTypes?: string[] +} + +export interface SupabaseStorageUpdateBucketResponse extends SupabaseBaseResponse {} + +export interface SupabaseStorageEmptyBucketParams { + apiKey: string + projectId: string + bucket: string +} + +export interface SupabaseStorageEmptyBucketResponse extends SupabaseBaseResponse {} + /** * Parameters for introspecting a Supabase database schema */ diff --git a/apps/sim/tools/supabase/update.ts b/apps/sim/tools/supabase/update.ts index 28c26e53d89..b8e429cefac 100644 --- a/apps/sim/tools/supabase/update.ts +++ b/apps/sim/tools/supabase/update.ts @@ -7,7 +7,7 @@ export const updateTool: ToolConfig encodeURIComponent(segment.trim())) + .join('/') +} diff --git a/apps/sim/tools/supabase/vector_search.ts b/apps/sim/tools/supabase/vector_search.ts index e8c8cc5d166..6ddfbe0fd6a 100644 --- a/apps/sim/tools/supabase/vector_search.ts +++ b/apps/sim/tools/supabase/vector_search.ts @@ -13,7 +13,7 @@ export const vectorSearchTool: ToolConfig< id: 'supabase_vector_search', name: 'Supabase Vector Search', description: 'Perform similarity search using pgvector in a Supabase table', - version: '1.0', + version: '1.0.0', params: { projectId: {