chore: import upstream snapshot with attribution
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
+110
View File
@@ -0,0 +1,110 @@
import { validateDatabaseIdentifier } from '@/lib/core/security/input-validation'
import type { SupabaseCountParams, SupabaseCountResponse } from '@/tools/supabase/types'
import { supabaseBaseUrl } from '@/tools/supabase/utils'
import type { ToolConfig } from '@/tools/types'
export const countTool: ToolConfig<SupabaseCountParams, SupabaseCountResponse> = {
id: 'supabase_count',
name: 'Supabase Count',
description: 'Count rows in a Supabase table',
version: '1.0.0',
params: {
projectId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Supabase project ID (e.g., jdrkgepadsdopsntdlom)',
},
table: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The name of the Supabase table to count rows from',
},
schema: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Database schema to count from (default: public). Use this to access tables in other schemas.',
},
filter: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'PostgREST filter (e.g., "status=eq.active")',
},
countType: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Count type: exact, planned, or estimated (default: exact)',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Supabase service role secret key',
},
},
request: {
url: (params) => {
const tableValidation = validateDatabaseIdentifier(params.table, 'table')
if (!tableValidation.isValid) throw new Error(tableValidation.error)
let url = `${supabaseBaseUrl(params.projectId)}/rest/v1/${encodeURIComponent(params.table)}?select=*`
if (params.filter?.trim()) {
url += `&${params.filter.trim()}`
}
return url
},
method: 'HEAD',
headers: (params) => {
const countType = params.countType || 'exact'
const headers: Record<string, string> = {
apikey: params.apiKey,
Authorization: `Bearer ${params.apiKey}`,
Prefer: `count=${countType}`,
}
if (params.schema) {
headers['Accept-Profile'] = params.schema
}
return headers
},
},
transformResponse: async (response: Response) => {
// Extract count from Content-Range header
const contentRange = response.headers.get('content-range')
if (!contentRange) {
throw new Error('No content-range header found in response')
}
// Parse the content-range header (format: "0-9/100" or "*/100")
const countMatch = contentRange.match(/\/(\d+)$/)
if (!countMatch) {
throw new Error(`Invalid content-range header format: ${contentRange}`)
}
const count = Number.parseInt(countMatch[1], 10)
return {
success: true,
output: {
message: `Successfully counted ${count} row${count === 1 ? '' : 's'}`,
count: count,
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
count: { type: 'number', description: 'Number of rows matching the filter' },
},
}
+118
View File
@@ -0,0 +1,118 @@
import { validateDatabaseIdentifier } from '@/lib/core/security/input-validation'
import type { SupabaseDeleteParams, SupabaseDeleteResponse } from '@/tools/supabase/types'
import { supabaseBaseUrl } from '@/tools/supabase/utils'
import type { ToolConfig } from '@/tools/types'
export const deleteTool: ToolConfig<SupabaseDeleteParams, SupabaseDeleteResponse> = {
id: 'supabase_delete',
name: 'Supabase Delete Row',
description: 'Delete rows from a Supabase table based on filter criteria',
version: '1.0.0',
params: {
projectId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Supabase project ID (e.g., jdrkgepadsdopsntdlom)',
},
table: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The name of the Supabase table to delete from',
},
schema: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Database schema to delete from (default: public). Use this to access tables in other schemas.',
},
filter: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'PostgREST filter to identify rows to delete (e.g., "id=eq.123")',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Supabase service role secret key',
},
},
request: {
url: (params) => {
const tableValidation = validateDatabaseIdentifier(params.table, 'table')
if (!tableValidation.isValid) throw new Error(tableValidation.error)
let url = `${supabaseBaseUrl(params.projectId)}/rest/v1/${encodeURIComponent(params.table)}?select=*`
if (params.filter?.trim()) {
url += `&${params.filter.trim()}`
} else {
throw new Error(
'Filter is required for delete operations to prevent accidental deletion of all rows'
)
}
return url
},
method: 'DELETE',
headers: (params) => {
const headers: Record<string, string> = {
apikey: params.apiKey,
Authorization: `Bearer ${params.apiKey}`,
Prefer: 'return=representation',
}
if (params.schema) {
headers['Content-Profile'] = params.schema
}
return headers
},
},
transformResponse: async (response: Response) => {
const text = await response.text()
let data
if (text?.trim()) {
try {
data = JSON.parse(text)
} catch (parseError) {
throw new Error(`Failed to parse Supabase response: ${parseError}`)
}
} else {
data = []
}
const deletedCount = Array.isArray(data) ? data.length : 0
if (deletedCount === 0) {
return {
success: true,
output: {
message: 'No rows were deleted (no matching records found)',
results: data,
},
error: undefined,
}
}
return {
success: true,
output: {
message: `Successfully deleted ${deletedCount} row${deletedCount === 1 ? '' : 's'}`,
results: data,
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
results: { type: 'array', description: 'Array of deleted records' },
},
}
+109
View File
@@ -0,0 +1,109 @@
import { validateDatabaseIdentifier } from '@/lib/core/security/input-validation'
import type { SupabaseGetRowParams, SupabaseGetRowResponse } from '@/tools/supabase/types'
import { supabaseBaseUrl } from '@/tools/supabase/utils'
import type { ToolConfig } from '@/tools/types'
export const getRowTool: ToolConfig<SupabaseGetRowParams, SupabaseGetRowResponse> = {
id: 'supabase_get_row',
name: 'Supabase Get Row',
description: 'Get a single row from a Supabase table based on filter criteria',
version: '1.0.0',
params: {
projectId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Supabase project ID (e.g., jdrkgepadsdopsntdlom)',
},
table: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The name of the Supabase table to query',
},
schema: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Database schema to query from (default: public). Use this to access tables in other schemas.',
},
select: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Columns to return (comma-separated). Defaults to * (all columns)',
},
filter: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'PostgREST filter to find the specific row (e.g., "id=eq.123")',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Supabase service role secret key',
},
},
request: {
url: (params) => {
const tableValidation = validateDatabaseIdentifier(params.table, 'table')
if (!tableValidation.isValid) throw new Error(tableValidation.error)
const selectColumns = params.select?.trim() || '*'
let url = `${supabaseBaseUrl(params.projectId)}/rest/v1/${encodeURIComponent(params.table)}?select=${encodeURIComponent(selectColumns)}`
if (params.filter?.trim()) {
url += `&${params.filter.trim()}`
}
url += `&limit=1`
return url
},
method: 'GET',
headers: (params) => {
const headers: Record<string, string> = {
apikey: params.apiKey,
Authorization: `Bearer ${params.apiKey}`,
}
if (params.schema) {
headers['Accept-Profile'] = params.schema
}
return headers
},
},
transformResponse: async (response: Response) => {
let data
try {
data = await response.json()
} catch (parseError) {
throw new Error(`Failed to parse Supabase response: ${parseError}`)
}
const rowFound = data.length > 0
const results = rowFound ? [data[0]] : []
return {
success: true,
output: {
message: rowFound ? 'Successfully found 1 row' : 'No row found matching the criteria',
results: results,
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
results: {
type: 'array',
description: 'Array containing the row data if found, empty array if not found',
},
},
}
+53
View File
@@ -0,0 +1,53 @@
import { countTool } from '@/tools/supabase/count'
import { deleteTool } from '@/tools/supabase/delete'
import { getRowTool } from '@/tools/supabase/get_row'
import { insertTool } from '@/tools/supabase/insert'
import { introspectTool } from '@/tools/supabase/introspect'
import { invokeFunctionTool } from '@/tools/supabase/invoke_function'
import { queryTool } from '@/tools/supabase/query'
import { rpcTool } from '@/tools/supabase/rpc'
import { storageCopyTool } from '@/tools/supabase/storage_copy'
import { storageCreateBucketTool } from '@/tools/supabase/storage_create_bucket'
import { storageCreateSignedUploadUrlTool } from '@/tools/supabase/storage_create_signed_upload_url'
import { storageCreateSignedUrlTool } from '@/tools/supabase/storage_create_signed_url'
import { storageDeleteTool } from '@/tools/supabase/storage_delete'
import { storageDeleteBucketTool } from '@/tools/supabase/storage_delete_bucket'
import { storageDownloadTool } from '@/tools/supabase/storage_download'
import { storageEmptyBucketTool } from '@/tools/supabase/storage_empty_bucket'
import { storageGetPublicUrlTool } from '@/tools/supabase/storage_get_public_url'
import { storageListTool } from '@/tools/supabase/storage_list'
import { storageListBucketsTool } from '@/tools/supabase/storage_list_buckets'
import { storageMoveTool } from '@/tools/supabase/storage_move'
import { storageUpdateBucketTool } from '@/tools/supabase/storage_update_bucket'
import { storageUploadTool } from '@/tools/supabase/storage_upload'
import { textSearchTool } from '@/tools/supabase/text_search'
import { updateTool } from '@/tools/supabase/update'
import { upsertTool } from '@/tools/supabase/upsert'
import { vectorSearchTool } from '@/tools/supabase/vector_search'
export const supabaseQueryTool = queryTool
export const supabaseInsertTool = insertTool
export const supabaseGetRowTool = getRowTool
export const supabaseUpdateTool = updateTool
export const supabaseDeleteTool = deleteTool
export const supabaseUpsertTool = upsertTool
export const supabaseVectorSearchTool = vectorSearchTool
export const supabaseRpcTool = rpcTool
export const supabaseIntrospectTool = introspectTool
export const supabaseInvokeFunctionTool = invokeFunctionTool
export const supabaseTextSearchTool = textSearchTool
export const supabaseCountTool = countTool
export const supabaseStorageUploadTool = storageUploadTool
export const supabaseStorageDownloadTool = storageDownloadTool
export const supabaseStorageListTool = storageListTool
export const supabaseStorageDeleteTool = storageDeleteTool
export const supabaseStorageMoveTool = storageMoveTool
export const supabaseStorageCopyTool = storageCopyTool
export const supabaseStorageCreateBucketTool = storageCreateBucketTool
export const supabaseStorageListBucketsTool = storageListBucketsTool
export const supabaseStorageDeleteBucketTool = storageDeleteBucketTool
export const supabaseStorageGetPublicUrlTool = storageGetPublicUrlTool
export const supabaseStorageCreateSignedUrlTool = storageCreateSignedUrlTool
export const supabaseStorageCreateSignedUploadUrlTool = storageCreateSignedUploadUrlTool
export const supabaseStorageUpdateBucketTool = storageUpdateBucketTool
export const supabaseStorageEmptyBucketTool = storageEmptyBucketTool
+126
View File
@@ -0,0 +1,126 @@
import { validateDatabaseIdentifier } from '@/lib/core/security/input-validation'
import type { SupabaseInsertParams, SupabaseInsertResponse } from '@/tools/supabase/types'
import { supabaseBaseUrl } from '@/tools/supabase/utils'
import type { ToolConfig } from '@/tools/types'
export const insertTool: ToolConfig<SupabaseInsertParams, SupabaseInsertResponse> = {
id: 'supabase_insert',
name: 'Supabase Insert',
description: 'Insert data into a Supabase table',
version: '1.0.0',
params: {
projectId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Supabase project ID (e.g., jdrkgepadsdopsntdlom)',
},
table: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The name of the Supabase table to insert data into',
},
schema: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Database schema to insert into (default: public). Use this to access tables in other schemas.',
},
data: {
type: 'array',
required: true,
visibility: 'user-or-llm',
description: 'The data to insert (array of objects or a single object)',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Supabase service role secret key',
},
},
request: {
url: (params) => {
const tableValidation = validateDatabaseIdentifier(params.table, 'table')
if (!tableValidation.isValid) throw new Error(tableValidation.error)
return `${supabaseBaseUrl(params.projectId)}/rest/v1/${encodeURIComponent(params.table)}?select=*`
},
method: 'POST',
headers: (params) => {
const headers: Record<string, string> = {
apikey: params.apiKey,
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
Prefer: 'return=representation',
}
if (params.schema) {
headers['Content-Profile'] = params.schema
}
return headers
},
body: (params) => {
// Prepare the data - if it's an object but not an array, wrap it in an array
const dataToSend =
typeof params.data === 'object' && !Array.isArray(params.data) ? [params.data] : params.data
return dataToSend
},
},
transformResponse: async (response: Response) => {
const text = await response.text()
if (!text || text.trim() === '') {
return {
success: true,
output: {
message: 'Successfully inserted data into Supabase (no data returned)',
results: [],
},
error: undefined,
}
}
let data
try {
data = JSON.parse(text)
} catch (parseError) {
throw new Error(`Failed to parse Supabase response: ${parseError}`)
}
// Check if results array is empty and provide better feedback
const resultsArray = Array.isArray(data) ? data : [data]
const isEmpty = resultsArray.length === 0 || (resultsArray.length === 1 && !resultsArray[0])
if (isEmpty) {
return {
success: false,
output: {
message: 'No data was inserted into Supabase',
results: data,
},
error:
'No data was inserted into Supabase. This usually indicates invalid data format or schema mismatch. Please check that your JSON is valid and matches your table schema.',
}
}
const insertedCount = resultsArray.length
return {
success: true,
output: {
message: `Successfully inserted ${insertedCount} row${insertedCount === 1 ? '' : 's'} into Supabase`,
results: data,
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
results: { type: 'array', description: 'Array of inserted records' },
},
}
+172
View File
@@ -0,0 +1,172 @@
import {
INTROSPECT_TABLE_OUTPUT_PROPERTIES,
type SupabaseColumnSchema,
type SupabaseIntrospectParams,
type SupabaseIntrospectResponse,
type SupabaseTableSchema,
} from '@/tools/supabase/types'
import { supabaseBaseUrl } from '@/tools/supabase/utils'
import type { ToolConfig } from '@/tools/types'
/**
* 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<SupabaseIntrospectParams, SupabaseIntrospectResponse> = {
id: 'supabase_introspect',
name: 'Supabase Introspect',
description:
'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: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Supabase project ID (e.g., jdrkgepadsdopsntdlom)',
},
schema: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Database schema to introspect (defaults to all user schemas, commonly "public")',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Supabase service role secret key',
},
},
request: {
url: (params) => `${supabaseBaseUrl(params.projectId)}/rest/v1/`,
method: 'GET',
headers: (params) => ({
apikey: params.apiKey,
Authorization: `Bearer ${params.apiKey}`,
Accept: 'application/openapi+json',
...(params.schema ? { 'Accept-Profile': params.schema } : {}),
}),
},
transformResponse: async (response: Response, params?: SupabaseIntrospectParams) => {
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Failed to introspect database: ${errorText}`)
}
const openApiSpec = await response.json()
const tables = parseOpenApiSpec(openApiSpec, params?.schema)
return {
success: true,
output: {
message: `Successfully introspected ${tables.length} table(s) from database schema`,
tables,
schemas: [...new Set(tables.map((t) => t.schema))],
},
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
tables: {
type: 'array',
description: 'Array of table schemas with columns, keys, and indexes',
items: {
type: 'object',
properties: INTROSPECT_TABLE_OUTPUT_PROPERTIES,
},
},
schemas: { type: 'array', description: 'List of schemas found in the database' },
},
}
/**
* 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[] = []
const definitions = spec.definitions || spec.components?.schemas || {}
for (const [tableName, tableDef] of Object.entries(definitions)) {
if (tableName.startsWith('_') || tableName === 'Error') continue
const definition = tableDef as any
const properties = definition.properties || {}
const required = definition.required || []
const columns: SupabaseColumnSchema[] = []
const primaryKey: string[] = []
const foreignKeys: Array<{
column: string
referencesTable: string
referencesColumn: string
}> = []
for (const [colName, colDef] of Object.entries(properties)) {
const col = colDef as any
const isPK = colName === 'id'
const fkMatch = col.description?.match(/references\s+(\w+)\.(\w+)/)
const column: SupabaseColumnSchema = {
name: colName,
type: col.format || col.type || 'unknown',
nullable: !required.includes(colName),
default: col.default || null,
isPrimaryKey: isPK,
isForeignKey: !!fkMatch,
}
if (fkMatch) {
column.references = { table: fkMatch[1], column: fkMatch[2] }
foreignKeys.push({
column: colName,
referencesTable: fkMatch[1],
referencesColumn: fkMatch[2],
})
}
if (isPK) {
primaryKey.push(colName)
}
columns.push(column)
}
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
}
+137
View File
@@ -0,0 +1,137 @@
import type {
SupabaseInvokeFunctionParams,
SupabaseInvokeFunctionResponse,
} from '@/tools/supabase/types'
import { supabaseBaseUrl } from '@/tools/supabase/utils'
import type { HttpMethod, ToolConfig } from '@/tools/types'
const ALLOWED_METHODS = new Set<HttpMethod>(['GET', 'POST', 'PUT', 'PATCH', 'DELETE'])
function resolveMethod(method?: string): HttpMethod {
const normalized = (method || 'POST').toUpperCase() as HttpMethod
return ALLOWED_METHODS.has(normalized) ? normalized : 'POST'
}
/**
* Edge Function names are URL path segments and may contain letters, digits,
* underscores, and hyphens (e.g. `hello-world`). Reject anything else to
* prevent path traversal / injection.
*/
function validateFunctionName(name: string): string {
const trimmed = name?.trim()
if (!trimmed || !/^[A-Za-z0-9_-]+$/.test(trimmed)) {
throw new Error(
'Invalid function name: must contain only letters, digits, underscores, and hyphens'
)
}
return trimmed
}
export const invokeFunctionTool: ToolConfig<
SupabaseInvokeFunctionParams,
SupabaseInvokeFunctionResponse
> = {
id: 'supabase_invoke_function',
name: 'Supabase Invoke Edge Function',
description: 'Invoke a Supabase Edge Function over HTTP',
version: '1.0.0',
params: {
projectId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Supabase project ID (e.g., jdrkgepadsdopsntdlom)',
},
functionName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The name of the Edge Function to invoke (e.g., "hello-world")',
},
method: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'HTTP method to use: GET, POST, PUT, PATCH, or DELETE (default: POST)',
},
body: {
type: 'json',
required: false,
visibility: 'user-or-llm',
description: 'Request payload to send to the function as a JSON object (ignored for GET)',
},
headers: {
type: 'json',
required: false,
visibility: 'user-or-llm',
description: 'Additional request headers as a JSON object of header name to value',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Supabase service role secret key',
},
},
request: {
url: (params) => {
const functionName = validateFunctionName(params.functionName)
return `${supabaseBaseUrl(params.projectId)}/functions/v1/${functionName}`
},
method: (params) => resolveMethod(params.method),
headers: (params) => {
const headers: Record<string, string> = {
apikey: params.apiKey,
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}
if (params.headers && typeof params.headers === 'object' && !Array.isArray(params.headers)) {
for (const [key, value] of Object.entries(params.headers)) {
headers[key] = String(value)
}
}
return headers
},
body: (params) => {
if (resolveMethod(params.method) === 'GET') {
return undefined
}
return params.body ?? {}
},
},
/**
* Only the success path is handled here — the tool executor throws on
* non-OK responses before `transformResponse` runs, surfacing the Edge
* Function's error body via the shared error extractor.
*/
transformResponse: async (response: Response) => {
const contentType = response.headers.get('content-type') || ''
let results: unknown
if (contentType.includes('application/json')) {
try {
results = await response.json()
} catch (parseError) {
throw new Error(`Failed to parse Supabase Edge Function response: ${parseError}`)
}
} else {
results = await response.text()
}
return {
success: true,
output: {
message: 'Successfully invoked Edge Function',
results,
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
results: { type: 'json', description: 'Response body returned by the Edge Function' },
},
}
+162
View File
@@ -0,0 +1,162 @@
import { validateDatabaseIdentifier } from '@/lib/core/security/input-validation'
import type { SupabaseQueryParams, SupabaseQueryResponse } from '@/tools/supabase/types'
import { supabaseBaseUrl } from '@/tools/supabase/utils'
import type { ToolConfig } from '@/tools/types'
export const queryTool: ToolConfig<SupabaseQueryParams, SupabaseQueryResponse> = {
id: 'supabase_query',
name: 'Supabase Query',
description: 'Query data from a Supabase table',
version: '1.0.0',
params: {
projectId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Supabase project ID (e.g., jdrkgepadsdopsntdlom)',
},
table: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The name of the Supabase table to query',
},
schema: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Database schema to query from (default: public). Use this to access tables in other schemas.',
},
select: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Columns to return (comma-separated). Defaults to * (all columns)',
},
filter: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'PostgREST filter (e.g., "id=eq.123")',
},
orderBy: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Column to order by (add DESC for descending)',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of rows to return',
},
offset: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of rows to skip (for pagination)',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Supabase service role secret key',
},
},
request: {
url: (params) => {
const tableValidation = validateDatabaseIdentifier(params.table, 'table')
if (!tableValidation.isValid) throw new Error(tableValidation.error)
const selectColumns = params.select?.trim() || '*'
let url = `${supabaseBaseUrl(params.projectId)}/rest/v1/${encodeURIComponent(params.table)}?select=${encodeURIComponent(selectColumns)}`
// Add filters if provided - using PostgREST syntax
if (params.filter?.trim()) {
url += `&${params.filter.trim()}`
}
// Add order by if provided
if (params.orderBy) {
let orderParam = params.orderBy.trim()
// Check if DESC is specified (case-insensitive)
if (/\s+DESC$/i.test(orderParam)) {
orderParam = `${orderParam.replace(/\s+DESC$/i, '').trim()}.desc`
}
// Check if ASC is specified (case-insensitive)
else if (/\s+ASC$/i.test(orderParam)) {
orderParam = `${orderParam.replace(/\s+ASC$/i, '').trim()}.asc`
}
// Default to ascending if no direction specified
else {
orderParam = `${orderParam}.asc`
}
url += `&order=${orderParam}`
}
// Add limit if provided
if (params.limit !== undefined && params.limit !== null) {
url += `&limit=${Number(params.limit)}`
}
// Add offset if provided
if (params.offset !== undefined && params.offset !== null) {
url += `&offset=${Number(params.offset)}`
}
return url
},
method: 'GET',
headers: (params) => {
const headers: Record<string, string> = {
apikey: params.apiKey,
Authorization: `Bearer ${params.apiKey}`,
}
if (params.schema) {
headers['Accept-Profile'] = params.schema
}
return headers
},
},
transformResponse: async (response: Response) => {
let data
try {
data = await response.json()
} catch (parseError) {
throw new Error(`Failed to parse Supabase response: ${parseError}`)
}
const rowCount = Array.isArray(data) ? data.length : 0
if (rowCount === 0) {
return {
success: true,
output: {
message: 'No rows found matching the query criteria',
results: data,
},
error: undefined,
}
}
return {
success: true,
output: {
message: `Successfully queried ${rowCount} row${rowCount === 1 ? '' : 's'} from Supabase`,
results: data,
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
results: { type: 'array', description: 'Array of records returned from the query' },
},
}
+78
View File
@@ -0,0 +1,78 @@
import { validateDatabaseIdentifier } from '@/lib/core/security/input-validation'
import type { SupabaseRpcParams, SupabaseRpcResponse } from '@/tools/supabase/types'
import { supabaseBaseUrl } from '@/tools/supabase/utils'
import type { ToolConfig } from '@/tools/types'
export const rpcTool: ToolConfig<SupabaseRpcParams, SupabaseRpcResponse> = {
id: 'supabase_rpc',
name: 'Supabase RPC',
description: 'Call a PostgreSQL function in Supabase',
version: '1.0.0',
params: {
projectId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Supabase project ID (e.g., jdrkgepadsdopsntdlom)',
},
functionName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The name of the PostgreSQL function to call',
},
params: {
type: 'object',
required: false,
visibility: 'user-or-llm',
description: 'Parameters to pass to the function as a JSON object',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Supabase service role secret key',
},
},
request: {
url: (params) => {
const fnValidation = validateDatabaseIdentifier(params.functionName, 'functionName')
if (!fnValidation.isValid) throw new Error(fnValidation.error)
return `${supabaseBaseUrl(params.projectId)}/rest/v1/rpc/${encodeURIComponent(params.functionName)}`
},
method: 'POST',
headers: (params) => ({
apikey: params.apiKey,
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
body: (params) => {
return params.params || {}
},
},
transformResponse: async (response: Response) => {
let data
try {
data = await response.json()
} catch (parseError) {
throw new Error(`Failed to parse Supabase RPC response: ${parseError}`)
}
return {
success: true,
output: {
message: 'Successfully executed PostgreSQL function',
results: data,
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
results: { type: 'json', description: 'Result returned from the function' },
},
}
+93
View File
@@ -0,0 +1,93 @@
import {
STORAGE_COPY_OUTPUT_PROPERTIES,
type SupabaseStorageCopyParams,
type SupabaseStorageCopyResponse,
} from '@/tools/supabase/types'
import { supabaseBaseUrl } from '@/tools/supabase/utils'
import type { ToolConfig } from '@/tools/types'
export const storageCopyTool: ToolConfig<SupabaseStorageCopyParams, SupabaseStorageCopyResponse> = {
id: 'supabase_storage_copy',
name: 'Supabase Storage Copy',
description: 'Copy a file within 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',
},
fromPath: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The path of the source file (e.g., "folder/source.jpg")',
},
toPath: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The path for the copied file (e.g., "folder/copy.jpg")',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Supabase service role secret key',
},
},
request: {
url: (params) => {
return `${supabaseBaseUrl(params.projectId)}/storage/v1/object/copy`
},
method: 'POST',
headers: (params) => ({
apikey: params.apiKey,
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
body: (params) => {
return {
bucketId: params.bucket,
sourceKey: params.fromPath,
destinationKey: params.toPath,
}
},
},
transformResponse: async (response: Response) => {
let data
try {
data = await response.json()
} catch (parseError) {
throw new Error(`Failed to parse Supabase storage copy response: ${parseError}`)
}
return {
success: true,
output: {
message: 'Successfully copied file in storage',
results: data,
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
results: {
type: 'object',
description: 'Copy operation result with the destination object key',
properties: STORAGE_COPY_OUTPUT_PROPERTIES,
},
},
}
@@ -0,0 +1,112 @@
import {
STORAGE_CREATE_BUCKET_OUTPUT_PROPERTIES,
type SupabaseStorageCreateBucketParams,
type SupabaseStorageCreateBucketResponse,
} from '@/tools/supabase/types'
import { supabaseBaseUrl } from '@/tools/supabase/utils'
import type { ToolConfig } from '@/tools/types'
export const storageCreateBucketTool: ToolConfig<
SupabaseStorageCreateBucketParams,
SupabaseStorageCreateBucketResponse
> = {
id: 'supabase_storage_create_bucket',
name: 'Supabase Storage Create Bucket',
description: 'Create a new storage bucket in Supabase',
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 create',
},
isPublic: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether the bucket should be publicly accessible (default: false)',
},
fileSizeLimit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum file size in bytes (optional)',
},
allowedMimeTypes: {
type: 'array',
required: false,
visibility: 'user-or-llm',
description: 'Array of allowed MIME types (e.g., ["image/png", "image/jpeg"])',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Supabase service role secret key',
},
},
request: {
url: (params) => {
return `${supabaseBaseUrl(params.projectId)}/storage/v1/bucket`
},
method: 'POST',
headers: (params) => ({
apikey: params.apiKey,
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const payload: any = {
id: params.bucket,
name: params.bucket,
public: params.isPublic || false,
}
if (params.fileSizeLimit) {
payload.file_size_limit = Number(params.fileSizeLimit)
}
if (params.allowedMimeTypes && params.allowedMimeTypes.length > 0) {
payload.allowed_mime_types = params.allowedMimeTypes
}
return payload
},
},
transformResponse: async (response: Response) => {
let data
try {
data = await response.json()
} catch (parseError) {
throw new Error(`Failed to parse Supabase storage create bucket response: ${parseError}`)
}
return {
success: true,
output: {
message: 'Successfully created storage bucket',
results: data,
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
results: {
type: 'object',
description: 'Created bucket result (name)',
properties: STORAGE_CREATE_BUCKET_OUTPUT_PROPERTIES,
},
},
}
@@ -0,0 +1,118 @@
import type {
SupabaseStorageCreateSignedUploadUrlParams,
SupabaseStorageCreateSignedUploadUrlResponse,
} from '@/tools/supabase/types'
import { encodeStoragePath, encodeStorageSegment, supabaseBaseUrl } from '@/tools/supabase/utils'
import type { ToolConfig } from '@/tools/types'
export const storageCreateSignedUploadUrlTool: ToolConfig<
SupabaseStorageCreateSignedUploadUrlParams,
SupabaseStorageCreateSignedUploadUrlResponse
> = {
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' },
},
}
@@ -0,0 +1,121 @@
import type {
SupabaseStorageCreateSignedUrlParams,
SupabaseStorageCreateSignedUrlResponse,
} from '@/tools/supabase/types'
import { encodeStoragePath, encodeStorageSegment, supabaseBaseUrl } from '@/tools/supabase/utils'
import type { ToolConfig } from '@/tools/types'
export const storageCreateSignedUrlTool: ToolConfig<
SupabaseStorageCreateSignedUrlParams,
SupabaseStorageCreateSignedUrlResponse
> = {
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.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 path to the file (e.g., "folder/file.jpg")',
},
expiresIn: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description: 'Number of seconds until the URL expires (e.g., 3600 for 1 hour)',
},
download: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'If true, forces download instead of inline display (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/sign/${bucket}/${path}`
},
method: 'POST',
headers: (params) => ({
apikey: params.apiKey,
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
body: (params) => ({
expiresIn: Number(params.expiresIn),
}),
},
transformResponse: async (response: Response, params?: SupabaseStorageCreateSignedUrlParams) => {
let data
try {
data = await response.json()
} catch (parseError) {
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')
}
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,
output: {
message: 'Successfully created signed URL',
signedUrl: fullUrl,
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
signedUrl: {
type: 'string',
description: 'The temporary signed URL to access the file',
},
},
}
+91
View File
@@ -0,0 +1,91 @@
import {
STORAGE_DELETED_FILE_OUTPUT_PROPERTIES,
type SupabaseStorageDeleteParams,
type SupabaseStorageDeleteResponse,
} from '@/tools/supabase/types'
import { encodeStorageSegment, supabaseBaseUrl } from '@/tools/supabase/utils'
import type { ToolConfig } from '@/tools/types'
export const storageDeleteTool: ToolConfig<
SupabaseStorageDeleteParams,
SupabaseStorageDeleteResponse
> = {
id: 'supabase_storage_delete',
name: 'Supabase Storage Delete',
description: 'Delete files from 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',
},
paths: {
type: 'array',
required: true,
visibility: 'user-or-llm',
description: 'Array of file paths to delete (e.g., ["folder/file1.jpg", "folder/file2.jpg"])',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Supabase service role secret key',
},
},
request: {
url: (params) => {
return `${supabaseBaseUrl(params.projectId)}/storage/v1/object/${encodeStorageSegment(params.bucket)}`
},
method: 'DELETE',
headers: (params) => ({
apikey: params.apiKey,
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
body: (params) => {
return {
prefixes: params.paths,
}
},
},
transformResponse: async (response: Response) => {
let data
try {
data = await response.json()
} catch (parseError) {
throw new Error(`Failed to parse Supabase storage delete response: ${parseError}`)
}
return {
success: true,
output: {
message: 'Successfully deleted files from storage',
results: data,
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
results: {
type: 'array',
description: 'Array of deleted file objects',
items: {
type: 'object',
properties: STORAGE_DELETED_FILE_OUTPUT_PROPERTIES,
},
},
},
}
@@ -0,0 +1,76 @@
import {
STORAGE_MESSAGE_OUTPUT_PROPERTIES,
type SupabaseStorageDeleteBucketParams,
type SupabaseStorageDeleteBucketResponse,
} from '@/tools/supabase/types'
import { encodeStorageSegment, supabaseBaseUrl } from '@/tools/supabase/utils'
import type { ToolConfig } from '@/tools/types'
export const storageDeleteBucketTool: ToolConfig<
SupabaseStorageDeleteBucketParams,
SupabaseStorageDeleteBucketResponse
> = {
id: 'supabase_storage_delete_bucket',
name: 'Supabase Storage Delete Bucket',
description: 'Delete a storage bucket in Supabase',
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 delete',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Supabase service role secret key',
},
},
request: {
url: (params) => {
return `${supabaseBaseUrl(params.projectId)}/storage/v1/bucket/${encodeStorageSegment(params.bucket)}`
},
method: 'DELETE',
headers: (params) => ({
apikey: params.apiKey,
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response: Response) => {
let data
try {
data = await response.json()
} catch (parseError) {
throw new Error(`Failed to parse Supabase storage delete bucket response: ${parseError}`)
}
return {
success: true,
output: {
message: 'Successfully deleted storage bucket',
results: data,
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
results: {
type: 'object',
description: 'Delete operation result',
properties: STORAGE_MESSAGE_OUTPUT_PROPERTIES,
},
},
}
+129
View File
@@ -0,0 +1,129 @@
import { createLogger } from '@sim/logger'
import {
STORAGE_DOWNLOAD_OUTPUT_PROPERTIES,
type SupabaseStorageDownloadParams,
type SupabaseStorageDownloadResponse,
} from '@/tools/supabase/types'
import { encodeStoragePath, encodeStorageSegment, supabaseBaseUrl } from '@/tools/supabase/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('SupabaseStorageDownloadTool')
export const storageDownloadTool: ToolConfig<
SupabaseStorageDownloadParams,
SupabaseStorageDownloadResponse
> = {
id: 'supabase_storage_download',
name: 'Supabase Storage Download',
description: 'Download a file from 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 path to the file to download (e.g., "folder/file.jpg")',
},
fileName: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Optional filename override',
},
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/${bucket}/${path}`
},
method: 'GET',
headers: (params) => ({
apikey: params.apiKey,
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response: Response, params?: SupabaseStorageDownloadParams) => {
try {
if (!response.ok) {
logger.error('Failed to download file from Supabase storage', {
status: response.status,
statusText: response.statusText,
})
throw new Error(`Failed to download file: ${response.statusText}`)
}
const contentType = response.headers.get('content-type') || 'application/octet-stream'
const pathParts = params?.path?.split('/') || []
const defaultFileName = pathParts[pathParts.length - 1] || 'download'
const resolvedName = params?.fileName || defaultFileName
logger.info('Downloading file from Supabase storage', {
bucket: params?.bucket,
path: params?.path,
fileName: resolvedName,
contentType,
})
const arrayBuffer = await response.arrayBuffer()
const fileBuffer = Buffer.from(arrayBuffer)
logger.info('File downloaded successfully from Supabase storage', {
name: resolvedName,
size: fileBuffer.length,
contentType,
})
const base64Data = fileBuffer.toString('base64')
return {
success: true,
output: {
file: {
name: resolvedName,
mimeType: contentType,
data: base64Data,
size: fileBuffer.length,
},
},
error: undefined,
}
} catch (error: any) {
logger.error('Error downloading file from Supabase storage', {
error: error.message,
stack: error.stack,
})
throw error
}
},
outputs: {
file: {
type: 'file',
description: 'Downloaded file stored in execution files',
properties: STORAGE_DOWNLOAD_OUTPUT_PROPERTIES,
},
},
}
@@ -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,
},
},
}
@@ -0,0 +1,90 @@
import type {
SupabaseStorageGetPublicUrlParams,
SupabaseStorageGetPublicUrlResponse,
} from '@/tools/supabase/types'
import { encodeStoragePath, encodeStorageSegment, supabaseBaseUrl } from '@/tools/supabase/utils'
import type { ToolConfig } from '@/tools/types'
export const storageGetPublicUrlTool: ToolConfig<
SupabaseStorageGetPublicUrlParams,
SupabaseStorageGetPublicUrlResponse
> = {
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.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 path to the file (e.g., "folder/file.jpg")',
},
download: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'If true, forces download instead of inline display (default: false)',
},
},
/**
* Public URLs are deterministic and built entirely from the project ID,
* bucket, and path — no network request is required. `directExecution`
* short-circuits the HTTP request so we never hit the API just to discard
* its response.
*/
directExecution: async (params: SupabaseStorageGetPublicUrlParams) => {
const bucket = encodeStorageSegment(params.bucket)
const path = encodeStoragePath(params.path)
let publicUrl = `${supabaseBaseUrl(params.projectId)}/storage/v1/object/public/${bucket}/${path}`
if (params.download) {
// 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 {
success: true,
output: {
message: 'Successfully generated public URL',
publicUrl,
},
error: undefined,
}
},
request: {
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: () => ({}),
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
publicUrl: {
type: 'string',
description: 'The public URL to access the file',
},
},
}
+136
View File
@@ -0,0 +1,136 @@
import {
STORAGE_FILE_OUTPUT_PROPERTIES,
type SupabaseStorageListParams,
type SupabaseStorageListResponse,
} from '@/tools/supabase/types'
import { encodeStorageSegment, supabaseBaseUrl } from '@/tools/supabase/utils'
import type { ToolConfig } from '@/tools/types'
export const storageListTool: ToolConfig<SupabaseStorageListParams, SupabaseStorageListResponse> = {
id: 'supabase_storage_list',
name: 'Supabase Storage List',
description: 'List files in 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: false,
visibility: 'user-or-llm',
description: 'The folder path to list files from (default: root)',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of files to return (default: 100)',
},
offset: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of files to skip (for pagination)',
},
sortBy: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Column to sort by: name, created_at, updated_at, last_accessed_at (default: name)',
},
sortOrder: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Sort order: asc or desc (default: asc)',
},
search: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Search term to filter files by name',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Supabase service role secret key',
},
},
request: {
url: (params) => {
return `${supabaseBaseUrl(params.projectId)}/storage/v1/object/list/${encodeStorageSegment(params.bucket)}`
},
method: 'POST',
headers: (params) => ({
apikey: params.apiKey,
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const payload: any = {
prefix: params.path || '',
limit: params.limit ? Number(params.limit) : 100,
offset: params.offset ? Number(params.offset) : 0,
}
if (params.sortBy) {
payload.sortBy = {
column: params.sortBy,
order: params.sortOrder || 'asc',
}
}
if (params.search) {
payload.search = params.search
}
return payload
},
},
transformResponse: async (response: Response) => {
let data
try {
data = await response.json()
} catch (parseError) {
throw new Error(`Failed to parse Supabase storage list response: ${parseError}`)
}
const fileCount = Array.isArray(data) ? data.length : 0
return {
success: true,
output: {
message: `Successfully listed ${fileCount} file${fileCount === 1 ? '' : 's'}`,
results: data,
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
results: {
type: 'array',
description: 'Array of file objects with metadata',
items: {
type: 'object',
properties: STORAGE_FILE_OUTPUT_PROPERTIES,
},
},
},
}
@@ -0,0 +1,75 @@
import {
STORAGE_BUCKET_OUTPUT_PROPERTIES,
type SupabaseStorageListBucketsParams,
type SupabaseStorageListBucketsResponse,
} from '@/tools/supabase/types'
import { supabaseBaseUrl } from '@/tools/supabase/utils'
import type { ToolConfig } from '@/tools/types'
export const storageListBucketsTool: ToolConfig<
SupabaseStorageListBucketsParams,
SupabaseStorageListBucketsResponse
> = {
id: 'supabase_storage_list_buckets',
name: 'Supabase Storage List Buckets',
description: 'List all storage buckets in Supabase',
version: '1.0.0',
params: {
projectId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Supabase project ID (e.g., jdrkgepadsdopsntdlom)',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Supabase service role secret key',
},
},
request: {
url: (params) => {
return `${supabaseBaseUrl(params.projectId)}/storage/v1/bucket`
},
method: 'GET',
headers: (params) => ({
apikey: params.apiKey,
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response: Response) => {
let data
try {
data = await response.json()
} catch (parseError) {
throw new Error(`Failed to parse Supabase storage list buckets response: ${parseError}`)
}
const bucketCount = Array.isArray(data) ? data.length : 0
return {
success: true,
output: {
message: `Successfully listed ${bucketCount} bucket${bucketCount === 1 ? '' : 's'}`,
results: data,
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
results: {
type: 'array',
description: 'Array of bucket objects',
items: {
type: 'object',
properties: STORAGE_BUCKET_OUTPUT_PROPERTIES,
},
},
},
}
+93
View File
@@ -0,0 +1,93 @@
import {
STORAGE_MOVE_OUTPUT_PROPERTIES,
type SupabaseStorageMoveParams,
type SupabaseStorageMoveResponse,
} from '@/tools/supabase/types'
import { supabaseBaseUrl } from '@/tools/supabase/utils'
import type { ToolConfig } from '@/tools/types'
export const storageMoveTool: ToolConfig<SupabaseStorageMoveParams, SupabaseStorageMoveResponse> = {
id: 'supabase_storage_move',
name: 'Supabase Storage Move',
description: 'Move a file within 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',
},
fromPath: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The current path of the file (e.g., "folder/old.jpg")',
},
toPath: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The new path for the file (e.g., "newfolder/new.jpg")',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Supabase service role secret key',
},
},
request: {
url: (params) => {
return `${supabaseBaseUrl(params.projectId)}/storage/v1/object/move`
},
method: 'POST',
headers: (params) => ({
apikey: params.apiKey,
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
body: (params) => {
return {
bucketId: params.bucket,
sourceKey: params.fromPath,
destinationKey: params.toPath,
}
},
},
transformResponse: async (response: Response) => {
let data
try {
data = await response.json()
} catch (parseError) {
throw new Error(`Failed to parse Supabase storage move response: ${parseError}`)
}
return {
success: true,
output: {
message: 'Successfully moved file in storage',
results: data,
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
results: {
type: 'object',
description: 'Move operation result',
properties: STORAGE_MOVE_OUTPUT_PROPERTIES,
},
},
}
@@ -0,0 +1,170 @@
import { getErrorMessage } from '@sim/utils/errors'
import {
STORAGE_MESSAGE_OUTPUT_PROPERTIES,
type SupabaseStorageUpdateBucketParams,
type SupabaseStorageUpdateBucketResponse,
} from '@/tools/supabase/types'
import { encodeStorageSegment, supabaseBaseUrl } from '@/tools/supabase/utils'
import type { ToolConfig } from '@/tools/types'
export const storageUpdateBucketTool: ToolConfig<
SupabaseStorageUpdateBucketParams,
SupabaseStorageUpdateBucketResponse
> = {
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<SupabaseStorageUpdateBucketResponse> => {
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,
},
},
}
+103
View File
@@ -0,0 +1,103 @@
import {
STORAGE_UPLOAD_OUTPUT_PROPERTIES,
type SupabaseStorageUploadParams,
type SupabaseStorageUploadResponse,
} from '@/tools/supabase/types'
import type { ToolConfig } from '@/tools/types'
export const storageUploadTool: ToolConfig<
SupabaseStorageUploadParams,
SupabaseStorageUploadResponse
> = {
id: 'supabase_storage_upload',
name: 'Supabase Storage Upload',
description: 'Upload a file 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',
},
fileName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The name of the file (e.g., "document.pdf", "image.jpg")',
},
path: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Optional folder path (e.g., "folder/subfolder/")',
},
fileData: {
type: 'json',
required: true,
visibility: 'user-or-llm',
description:
'File to upload - UserFile object (basic mode) or string content (advanced mode: base64 or plain text). Supports data URLs.',
},
contentType: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'MIME type of the file (e.g., "image/jpeg", "text/plain")',
},
cacheControl: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Cache-Control header value in seconds for the stored object (e.g., "3600"; default: "3600")',
},
upsert: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'If true, overwrites existing file (default: false)',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Supabase service role secret key',
},
},
request: {
url: '/api/tools/supabase/storage-upload',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
projectId: params.projectId,
apiKey: params.apiKey,
bucket: params.bucket,
fileName: params.fileName,
path: params.path,
fileData: params.fileData,
contentType: params.contentType,
cacheControl: params.cacheControl,
upsert: params.upsert,
}),
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
results: {
type: 'object',
description: 'Upload result including file path, bucket, and public URL',
properties: STORAGE_UPLOAD_OUTPUT_PROPERTIES,
},
},
}
+178
View File
@@ -0,0 +1,178 @@
import { validateDatabaseIdentifier } from '@/lib/core/security/input-validation'
import type { SupabaseTextSearchParams, SupabaseTextSearchResponse } from '@/tools/supabase/types'
import { supabaseBaseUrl } from '@/tools/supabase/utils'
import type { ToolConfig } from '@/tools/types'
export const textSearchTool: ToolConfig<SupabaseTextSearchParams, SupabaseTextSearchResponse> = {
id: 'supabase_text_search',
name: 'Supabase Text Search',
description: 'Perform full-text search on a Supabase table',
version: '1.0.0',
params: {
projectId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Supabase project ID (e.g., jdrkgepadsdopsntdlom)',
},
table: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The name of the Supabase table to search',
},
schema: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Database schema to search in (default: public). Use this to access tables in other schemas.',
},
column: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The column to search in',
},
query: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The search query',
},
searchType: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Search type: plain, phrase, or websearch (default: websearch)',
},
language: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Language for text search configuration (default: english)',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of rows to return',
},
offset: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of rows to skip (for pagination)',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Supabase service role secret key',
},
},
request: {
url: (params) => {
const tableValidation = validateDatabaseIdentifier(params.table, 'table')
if (!tableValidation.isValid) throw new Error(tableValidation.error)
const columnValidation = validateDatabaseIdentifier(params.column, 'column')
if (!columnValidation.isValid) throw new Error(columnValidation.error)
const searchType = params.searchType || 'websearch'
const language = params.language || 'english'
const languageValidation = validateDatabaseIdentifier(language, 'language')
if (!languageValidation.isValid) throw new Error(languageValidation.error)
let url = `${supabaseBaseUrl(params.projectId)}/rest/v1/${encodeURIComponent(params.table)}?select=*`
// Map search types to PostgREST operators
// plfts = plainto_tsquery (natural language), phfts = phraseto_tsquery, wfts = websearch_to_tsquery
const operatorMap: Record<string, string> = {
plain: 'plfts',
phrase: 'phfts',
websearch: 'wfts',
}
const operator = operatorMap[searchType] || 'wfts'
// Add text search filter using PostgREST syntax
url += `&${params.column}=${operator}(${language}).${encodeURIComponent(params.query)}`
// Add limit if provided
if (params.limit !== undefined && params.limit !== null) {
url += `&limit=${Number(params.limit)}`
}
// Add offset if provided
if (params.offset !== undefined && params.offset !== null) {
url += `&offset=${Number(params.offset)}`
}
return url
},
method: 'GET',
headers: (params) => {
const headers: Record<string, string> = {
apikey: params.apiKey,
Authorization: `Bearer ${params.apiKey}`,
}
if (params.schema) {
headers['Accept-Profile'] = params.schema
}
return headers
},
},
transformResponse: async (response: Response) => {
let data
try {
data = await response.json()
} catch (parseError) {
throw new Error(`Failed to parse Supabase text search response: ${parseError}`)
}
if (!response.ok && data?.message) {
const errorMessage = data.message
if (errorMessage.includes('to_tsvector') && errorMessage.includes('does not exist')) {
throw new Error(
'Full-text search can only be performed on text columns. The selected column appears to be a non-text type (e.g., integer, boolean). Please select a text/varchar column or use a different operation.'
)
}
if (errorMessage.includes('column') && errorMessage.includes('does not exist')) {
throw new Error(`The specified column does not exist in the table. Error: ${errorMessage}`)
}
throw new Error(errorMessage)
}
const rowCount = Array.isArray(data) ? data.length : 0
if (rowCount === 0) {
return {
success: true,
output: {
message: 'No results found matching the search query',
results: data,
},
error: undefined,
}
}
return {
success: true,
output: {
message: `Successfully found ${rowCount} result${rowCount === 1 ? '' : 's'}`,
results: data,
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
results: { type: 'array', description: 'Array of records matching the search query' },
},
}
+686
View File
@@ -0,0 +1,686 @@
import type { UserFile } from '@/executor/types'
import type { OutputProperty, ToolResponse } from '@/tools/types'
/**
* Shared output property definitions for Supabase API responses.
* Based on official Supabase REST API and Storage API documentation.
* @see https://supabase.com/docs/reference/javascript/introduction
* @see https://supabase.com/docs/guides/storage
*/
/**
* Output definition for storage file metadata object
* Returned by storage list operations
* @see https://github.com/supabase/storage-js/blob/main/src/lib/types.ts
*/
export const STORAGE_FILE_METADATA_OUTPUT_PROPERTIES = {
size: { type: 'number', description: 'File size in bytes', optional: true },
mimetype: { type: 'string', description: 'MIME type of the file', optional: true },
cacheControl: { type: 'string', description: 'Cache control header value', optional: true },
lastModified: { type: 'string', description: 'Last modified timestamp', optional: true },
eTag: { type: 'string', description: 'Entity tag for caching', optional: true },
} as const satisfies Record<string, OutputProperty>
/**
* Output definition for storage file objects returned by list operations
* @see https://github.com/supabase/storage-js/blob/main/src/lib/types.ts
*/
export const STORAGE_FILE_OUTPUT_PROPERTIES = {
id: { type: 'string', description: 'Unique file identifier' },
name: { type: 'string', description: 'File name' },
bucket_id: { type: 'string', description: 'Bucket identifier the file belongs to' },
owner: { type: 'string', description: 'Owner identifier', optional: true },
created_at: { type: 'string', description: 'File creation timestamp' },
updated_at: { type: 'string', description: 'Last update timestamp' },
last_accessed_at: { type: 'string', description: 'Last access timestamp' },
metadata: {
type: 'object',
description: 'File metadata including size and MIME type',
properties: STORAGE_FILE_METADATA_OUTPUT_PROPERTIES,
optional: true,
},
} as const satisfies Record<string, OutputProperty>
/**
* Complete storage file object output definition
*/
export const STORAGE_FILE_OUTPUT: OutputProperty = {
type: 'object',
description: 'Storage file object with metadata',
properties: STORAGE_FILE_OUTPUT_PROPERTIES,
}
/**
* Output definition for storage bucket objects
* @see https://github.com/supabase/storage-js/blob/main/src/lib/types.ts
*/
export const STORAGE_BUCKET_OUTPUT_PROPERTIES = {
id: { type: 'string', description: 'Unique bucket identifier' },
name: { type: 'string', description: 'Bucket name' },
owner: { type: 'string', description: 'Owner identifier', optional: true },
public: { type: 'boolean', description: 'Whether the bucket is publicly accessible' },
created_at: { type: 'string', description: 'Bucket creation timestamp' },
updated_at: { type: 'string', description: 'Last update timestamp' },
file_size_limit: {
type: 'number',
description: 'Maximum file size allowed in bytes',
optional: true,
},
allowed_mime_types: {
type: 'array',
description: 'List of allowed MIME types for uploads',
items: { type: 'string', description: 'MIME type' },
optional: true,
},
} as const satisfies Record<string, OutputProperty>
/**
* Complete storage bucket object output definition
*/
export const STORAGE_BUCKET_OUTPUT: OutputProperty = {
type: 'object',
description: 'Storage bucket object with configuration',
properties: STORAGE_BUCKET_OUTPUT_PROPERTIES,
}
/**
* Output definition for storage upload response.
* The Supabase Storage REST API returns `{ Id, Key }` (Key required); Sim's
* upload route augments this with `path`, `bucket`, and `publicUrl`.
* @see https://supabase.com/docs/reference/javascript/storage-from-upload
*/
export const STORAGE_UPLOAD_OUTPUT_PROPERTIES = {
Id: { type: 'string', description: 'Unique identifier for the uploaded file', optional: true },
Key: { type: 'string', description: 'Full object key including bucket name' },
path: { type: 'string', description: 'Path to the uploaded file within the bucket' },
bucket: { type: 'string', description: 'Name of the bucket the file was uploaded to' },
publicUrl: { type: 'string', description: 'Public URL for the uploaded file' },
} as const satisfies Record<string, OutputProperty>
/**
* Output definition for storage move response.
* The move endpoint returns `{ message, Id, Key }`.
* @see https://supabase.com/docs/reference/javascript/storage-from-move
*/
export const STORAGE_MOVE_OUTPUT_PROPERTIES = {
message: { type: 'string', description: 'Operation status message' },
Id: { type: 'string', description: 'Identifier of the destination object', optional: true },
Key: { type: 'string', description: 'Full object key of the destination', optional: true },
} as const satisfies Record<string, OutputProperty>
/**
* Output definition for storage copy response.
* The copy endpoint returns `{ Id, Key }` (Key required; Id deprecated).
* @see https://github.com/supabase/storage/blob/master/src/http/routes/object/copyObject.ts
*/
export const STORAGE_COPY_OUTPUT_PROPERTIES = {
Key: { type: 'string', description: 'Full object key of the copied file' },
Id: { type: 'string', description: 'Identifier of the copied object', optional: true },
} as const satisfies Record<string, OutputProperty>
/**
* 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_MESSAGE_OUTPUT_PROPERTIES = {
message: { type: 'string', description: 'Operation status message' },
} as const satisfies Record<string, OutputProperty>
/**
* Output definition for storage create bucket response
* @see https://supabase.com/docs/reference/javascript/storage-createbucket
*/
export const STORAGE_CREATE_BUCKET_OUTPUT_PROPERTIES = {
name: { type: 'string', description: 'Created bucket name' },
} as const satisfies Record<string, OutputProperty>
/**
* Output definition for storage delete files response
* Returns array of deleted file objects
*/
export const STORAGE_DELETED_FILE_OUTPUT_PROPERTIES = {
name: { type: 'string', description: 'Name of the deleted file' },
bucket_id: { type: 'string', description: 'Bucket identifier', optional: true },
owner: { type: 'string', description: 'Owner identifier', optional: true },
id: { type: 'string', description: 'Unique file identifier', optional: true },
updated_at: { type: 'string', description: 'Last update timestamp', optional: true },
created_at: { type: 'string', description: 'File creation timestamp', optional: true },
last_accessed_at: { type: 'string', description: 'Last access timestamp', optional: true },
} as const satisfies Record<string, OutputProperty>
/**
* Output definition for download file response
*/
export const STORAGE_DOWNLOAD_OUTPUT_PROPERTIES = {
name: { type: 'string', description: 'File name' },
mimeType: { type: 'string', description: 'MIME type of the file' },
data: { type: 'string', description: 'Base64 encoded file content' },
size: { type: 'number', description: 'File size in bytes' },
} as const satisfies Record<string, OutputProperty>
/**
* Complete download file object output definition
*/
export const STORAGE_DOWNLOAD_FILE_OUTPUT: OutputProperty = {
type: 'object',
description: 'Downloaded file with content and metadata',
properties: STORAGE_DOWNLOAD_OUTPUT_PROPERTIES,
}
/**
* Output definition for public URL response
*/
export const STORAGE_PUBLIC_URL_OUTPUT_PROPERTIES = {
publicUrl: { type: 'string', description: 'The public URL to access the file' },
} as const satisfies Record<string, OutputProperty>
/**
* Output definition for signed URL response
*/
export const STORAGE_SIGNED_URL_OUTPUT_PROPERTIES = {
signedUrl: { type: 'string', description: 'The temporary signed URL to access the file' },
} as const satisfies Record<string, OutputProperty>
/**
* Storage files array output definition for list operations
*/
export const STORAGE_FILES_OUTPUT: OutputProperty = {
type: 'array',
description: 'Array of file objects with metadata',
items: {
type: 'object',
properties: STORAGE_FILE_OUTPUT_PROPERTIES,
},
}
/**
* Storage buckets array output definition for list buckets operations
*/
export const STORAGE_BUCKETS_OUTPUT: OutputProperty = {
type: 'array',
description: 'Array of bucket objects',
items: {
type: 'object',
properties: STORAGE_BUCKET_OUTPUT_PROPERTIES,
},
}
/**
* Storage deleted files array output definition
*/
export const STORAGE_DELETED_FILES_OUTPUT: OutputProperty = {
type: 'array',
description: 'Array of deleted file objects',
items: {
type: 'object',
properties: STORAGE_DELETED_FILE_OUTPUT_PROPERTIES,
},
}
/**
* Output definition for foreign key reference in column schema
*/
export const INTROSPECT_REFERENCE_OUTPUT_PROPERTIES = {
table: { type: 'string', description: 'Referenced table name' },
column: { type: 'string', description: 'Referenced column name' },
} as const satisfies Record<string, OutputProperty>
/**
* Output definition for introspect column schema
*/
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 — 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: '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, when detected via SQL comment',
optional: true,
properties: INTROSPECT_REFERENCE_OUTPUT_PROPERTIES,
},
} as const satisfies Record<string, OutputProperty>
/**
* Output definition for introspect foreign key
*/
export const INTROSPECT_FOREIGN_KEY_OUTPUT_PROPERTIES = {
column: { type: 'string', description: 'Local column name' },
referencesTable: { type: 'string', description: 'Referenced table name' },
referencesColumn: { type: 'string', description: 'Referenced column name' },
} as const satisfies Record<string, OutputProperty>
/**
* Output definition for introspect index
*/
export const INTROSPECT_INDEX_OUTPUT_PROPERTIES = {
name: { type: 'string', description: 'Index name' },
columns: {
type: 'array',
description: 'Columns included in the index',
items: { type: 'string', description: 'Column name' },
},
unique: { type: 'boolean', description: 'Whether the index enforces uniqueness' },
} as const satisfies Record<string, OutputProperty>
/**
* Output definition for introspect table schema
*/
export const INTROSPECT_TABLE_OUTPUT_PROPERTIES = {
name: { type: 'string', description: 'Table name' },
schema: { type: 'string', description: 'Database schema name' },
columns: {
type: 'array',
description: 'Array of column definitions',
items: {
type: 'object',
properties: INTROSPECT_COLUMN_OUTPUT_PROPERTIES,
},
},
primaryKey: {
type: 'array',
description: 'Array of primary key column names',
items: { type: 'string', description: 'Column name' },
},
foreignKeys: {
type: 'array',
description: 'Array of foreign key relationships',
items: {
type: 'object',
properties: INTROSPECT_FOREIGN_KEY_OUTPUT_PROPERTIES,
},
},
indexes: {
type: 'array',
description:
'Always empty — index definitions are not exposed by the OpenAPI spec this tool reads',
items: {
type: 'object',
properties: INTROSPECT_INDEX_OUTPUT_PROPERTIES,
},
},
} as const satisfies Record<string, OutputProperty>
/**
* Introspect tables array output definition
*/
export const INTROSPECT_TABLES_OUTPUT: OutputProperty = {
type: 'array',
description: 'Array of table schemas with columns, keys, and indexes',
items: {
type: 'object',
properties: INTROSPECT_TABLE_OUTPUT_PROPERTIES,
},
}
export interface SupabaseQueryParams {
apiKey: string
projectId: string
table: string
schema?: string
select?: string
filter?: string
orderBy?: string
limit?: number
offset?: number
}
export interface SupabaseInsertParams {
apiKey: string
projectId: string
table: string
schema?: string
data: any
}
export interface SupabaseGetRowParams {
apiKey: string
projectId: string
table: string
schema?: string
select?: string
filter: string
}
export interface SupabaseUpdateParams {
apiKey: string
projectId: string
table: string
schema?: string
filter: string
data: any
}
export interface SupabaseDeleteParams {
apiKey: string
projectId: string
table: string
schema?: string
filter: string
}
export interface SupabaseUpsertParams {
apiKey: string
projectId: string
table: string
schema?: string
data: any
onConflict?: string
}
export interface SupabaseVectorSearchParams {
apiKey: string
projectId: string
functionName: string
queryEmbedding: number[]
matchThreshold?: number
matchCount?: number
}
interface SupabaseBaseResponse extends ToolResponse {
output: {
message: string
results: any
}
error?: string
}
export interface SupabaseQueryResponse extends SupabaseBaseResponse {}
export interface SupabaseInsertResponse extends SupabaseBaseResponse {}
export interface SupabaseGetRowResponse extends SupabaseBaseResponse {}
export interface SupabaseUpdateResponse extends SupabaseBaseResponse {}
export interface SupabaseDeleteResponse extends SupabaseBaseResponse {}
export interface SupabaseUpsertResponse extends SupabaseBaseResponse {}
export interface SupabaseVectorSearchResponse extends SupabaseBaseResponse {}
export interface SupabaseResponse extends SupabaseBaseResponse {}
export interface SupabaseRpcParams {
apiKey: string
projectId: string
functionName: string
params?: any
}
export interface SupabaseRpcResponse extends SupabaseBaseResponse {}
export interface SupabaseTextSearchParams {
apiKey: string
projectId: string
table: string
schema?: string
column: string
query: string
searchType?: string
language?: string
limit?: number
offset?: number
}
export interface SupabaseTextSearchResponse extends SupabaseBaseResponse {}
export interface SupabaseCountParams {
apiKey: string
projectId: string
table: string
schema?: string
filter?: string
countType?: string
}
export interface SupabaseCountResponse extends ToolResponse {
output: {
message: string
count: number
}
error?: string
}
export interface SupabaseInvokeFunctionParams {
apiKey: string
projectId: string
functionName: string
method?: string
body?: any
headers?: Record<string, string>
}
export interface SupabaseInvokeFunctionResponse extends SupabaseBaseResponse {}
export interface SupabaseStorageUploadParams {
apiKey: string
projectId: string
bucket: string
fileName: string
path?: string
fileData: UserFile | string
contentType?: string
cacheControl?: string
upsert?: boolean
}
export interface SupabaseStorageUploadResponse extends SupabaseBaseResponse {}
export interface SupabaseStorageDownloadParams {
apiKey: string
projectId: string
bucket: string
path: string
fileName?: string
}
export interface SupabaseStorageDownloadResponse extends ToolResponse {
output: {
file: {
name: string
mimeType: string
data: string | Buffer
size: number
}
}
error?: string
}
export interface SupabaseStorageListParams {
apiKey: string
projectId: string
bucket: string
path?: string
limit?: number
offset?: number
sortBy?: string
sortOrder?: string
search?: string
}
export interface SupabaseStorageListResponse extends SupabaseBaseResponse {}
export interface SupabaseStorageDeleteParams {
apiKey: string
projectId: string
bucket: string
paths: string[]
}
export interface SupabaseStorageDeleteResponse extends SupabaseBaseResponse {}
export interface SupabaseStorageMoveParams {
apiKey: string
projectId: string
bucket: string
fromPath: string
toPath: string
}
export interface SupabaseStorageMoveResponse extends SupabaseBaseResponse {}
export interface SupabaseStorageCopyParams {
apiKey: string
projectId: string
bucket: string
fromPath: string
toPath: string
}
export interface SupabaseStorageCopyResponse extends SupabaseBaseResponse {}
export interface SupabaseStorageCreateBucketParams {
apiKey: string
projectId: string
bucket: string
isPublic?: boolean
fileSizeLimit?: number
allowedMimeTypes?: string[]
}
export interface SupabaseStorageCreateBucketResponse extends SupabaseBaseResponse {}
export interface SupabaseStorageListBucketsParams {
apiKey: string
projectId: string
}
export interface SupabaseStorageListBucketsResponse extends SupabaseBaseResponse {}
export interface SupabaseStorageDeleteBucketParams {
apiKey: string
projectId: string
bucket: string
}
export interface SupabaseStorageDeleteBucketResponse extends SupabaseBaseResponse {}
export interface SupabaseStorageGetPublicUrlParams {
projectId: string
bucket: string
path: string
download?: boolean
}
export interface SupabaseStorageGetPublicUrlResponse extends ToolResponse {
output: {
message: string
publicUrl: string
}
error?: string
}
export interface SupabaseStorageCreateSignedUrlParams {
apiKey: string
projectId: string
bucket: string
path: string
expiresIn: number
download?: boolean
}
export interface SupabaseStorageCreateSignedUrlResponse extends ToolResponse {
output: {
message: string
signedUrl: string
}
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
*/
export interface SupabaseIntrospectParams {
apiKey: string
projectId: string
schema?: string
}
/**
* Column information for a database table
*/
export interface SupabaseColumnSchema {
name: string
type: string
nullable: boolean
default: string | null
isPrimaryKey: boolean
isForeignKey: boolean
references?: { table: string; column: string }
}
/**
* Table schema information including columns, keys, and indexes
*/
export interface SupabaseTableSchema {
name: string
schema: string
columns: SupabaseColumnSchema[]
primaryKey: string[]
foreignKeys: Array<{ column: string; referencesTable: string; referencesColumn: string }>
indexes: Array<{ name: string; columns: string[]; unique: boolean }>
}
/**
* Response from the introspect operation
*/
export interface SupabaseIntrospectResponse extends ToolResponse {
output: {
message: string
tables: SupabaseTableSchema[]
schemas: string[]
}
error?: string
}
+126
View File
@@ -0,0 +1,126 @@
import { validateDatabaseIdentifier } from '@/lib/core/security/input-validation'
import type { SupabaseUpdateParams, SupabaseUpdateResponse } from '@/tools/supabase/types'
import { supabaseBaseUrl } from '@/tools/supabase/utils'
import type { ToolConfig } from '@/tools/types'
export const updateTool: ToolConfig<SupabaseUpdateParams, SupabaseUpdateResponse> = {
id: 'supabase_update',
name: 'Supabase Update Row',
description: 'Update rows in a Supabase table based on filter criteria',
version: '1.0.0',
params: {
projectId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Supabase project ID (e.g., jdrkgepadsdopsntdlom)',
},
table: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The name of the Supabase table to update',
},
schema: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Database schema to update in (default: public). Use this to access tables in other schemas.',
},
filter: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'PostgREST filter to identify rows to update (e.g., "id=eq.123")',
},
data: {
type: 'object',
required: true,
visibility: 'user-or-llm',
description: 'Data to update in the matching rows',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Supabase service role secret key',
},
},
request: {
url: (params) => {
const tableValidation = validateDatabaseIdentifier(params.table, 'table')
if (!tableValidation.isValid) throw new Error(tableValidation.error)
let url = `${supabaseBaseUrl(params.projectId)}/rest/v1/${encodeURIComponent(params.table)}?select=*`
if (params.filter?.trim()) {
url += `&${params.filter.trim()}`
} else {
throw new Error(
'Filter is required for update operations to prevent accidental update of all rows'
)
}
return url
},
method: 'PATCH',
headers: (params) => {
const headers: Record<string, string> = {
apikey: params.apiKey,
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
Prefer: 'return=representation',
}
if (params.schema) {
headers['Content-Profile'] = params.schema
}
return headers
},
body: (params) => params.data,
},
transformResponse: async (response: Response) => {
const text = await response.text()
let data
if (text?.trim()) {
try {
data = JSON.parse(text)
} catch (parseError) {
throw new Error(`Failed to parse Supabase response: ${parseError}`)
}
} else {
data = []
}
const updatedCount = Array.isArray(data) ? data.length : 0
if (updatedCount === 0) {
return {
success: true,
output: {
message: 'No rows were updated (no matching records found)',
results: data,
},
error: undefined,
}
}
return {
success: true,
output: {
message: `Successfully updated ${updatedCount} row${updatedCount === 1 ? '' : 's'}`,
results: data,
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
results: { type: 'array', description: 'Array of updated records' },
},
}
+137
View File
@@ -0,0 +1,137 @@
import { validateDatabaseIdentifier } from '@/lib/core/security/input-validation'
import type { SupabaseUpsertParams, SupabaseUpsertResponse } from '@/tools/supabase/types'
import { supabaseBaseUrl } from '@/tools/supabase/utils'
import type { ToolConfig } from '@/tools/types'
export const upsertTool: ToolConfig<SupabaseUpsertParams, SupabaseUpsertResponse> = {
id: 'supabase_upsert',
name: 'Supabase Upsert',
description: 'Insert or update data in a Supabase table (upsert operation)',
version: '1.0.0',
params: {
projectId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Supabase project ID (e.g., jdrkgepadsdopsntdlom)',
},
table: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The name of the Supabase table to upsert data into',
},
schema: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Database schema to upsert into (default: public). Use this to access tables in other schemas.',
},
data: {
type: 'array',
required: true,
visibility: 'user-or-llm',
description: 'The data to upsert (insert or update) - array of objects or a single object',
},
onConflict: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Comma-separated column(s) with a unique or primary key constraint to resolve conflicts on (e.g., "email"). Defaults to the primary key.',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Supabase service role secret key',
},
},
request: {
url: (params) => {
const tableValidation = validateDatabaseIdentifier(params.table, 'table')
if (!tableValidation.isValid) throw new Error(tableValidation.error)
let url = `${supabaseBaseUrl(params.projectId)}/rest/v1/${encodeURIComponent(params.table)}?select=*`
if (params.onConflict?.trim()) {
url += `&on_conflict=${encodeURIComponent(params.onConflict.trim())}`
}
return url
},
method: 'POST',
headers: (params) => {
const headers: Record<string, string> = {
apikey: params.apiKey,
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
Prefer: 'return=representation,resolution=merge-duplicates',
}
if (params.schema) {
headers['Content-Profile'] = params.schema
}
return headers
},
body: (params) => {
// Prepare the data - if it's an object but not an array, wrap it in an array
const dataToSend =
typeof params.data === 'object' && !Array.isArray(params.data) ? [params.data] : params.data
return dataToSend
},
},
transformResponse: async (response: Response) => {
const text = await response.text()
if (!text || text.trim() === '') {
return {
success: true,
output: {
message: 'Successfully upserted data into Supabase (no data returned)',
results: [],
},
error: undefined,
}
}
let data
try {
data = JSON.parse(text)
} catch (parseError) {
throw new Error(`Failed to parse Supabase response: ${parseError}`)
}
// Check if results array is empty and provide better feedback
const resultsArray = Array.isArray(data) ? data : [data]
const isEmpty = resultsArray.length === 0 || (resultsArray.length === 1 && !resultsArray[0])
if (isEmpty) {
return {
success: false,
output: {
message: 'No data was upserted into Supabase',
results: data,
},
error:
'No data was upserted into Supabase. This usually indicates invalid data format or schema mismatch. Please check that your JSON is valid and matches your table schema.',
}
}
const upsertedCount = resultsArray.length
return {
success: true,
output: {
message: `Successfully upserted ${upsertedCount} row${upsertedCount === 1 ? '' : 's'} into Supabase`,
results: data,
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
results: { type: 'array', description: 'Array of upserted records' },
},
}
+40
View File
@@ -0,0 +1,40 @@
/**
* @vitest-environment node
*/
import { envFlagsMock } from '@sim/testing'
import { describe, expect, it, vi } from 'vitest'
vi.mock('@/lib/core/config/env-flags', () => envFlagsMock)
import { supabaseBaseUrl } from '@/tools/supabase/utils'
describe('supabaseBaseUrl', () => {
it.concurrent('should return the correct URL for a valid project ID', () => {
const url = supabaseBaseUrl('jdrkgepadsdopsntdlom')
expect(url).toBe('https://jdrkgepadsdopsntdlom.supabase.co')
})
it.concurrent('should throw on fragment injection attempt', () => {
expect(() => supabaseBaseUrl('evil#attacker.com')).toThrow()
})
it.concurrent('should throw on empty string', () => {
expect(() => supabaseBaseUrl('')).toThrow()
})
it.concurrent('should throw on path traversal', () => {
expect(() => supabaseBaseUrl('evil/../../etc')).toThrow()
})
it.concurrent('should throw on authority injection', () => {
expect(() => supabaseBaseUrl('evil@attacker.com')).toThrow()
})
it.concurrent('should throw on uppercase letters', () => {
expect(() => supabaseBaseUrl('ABCDEFGHIJKLMNOPQRST')).toThrow()
})
it.concurrent('should throw on too-short IDs', () => {
expect(() => supabaseBaseUrl('abc')).toThrow()
})
})
+35
View File
@@ -0,0 +1,35 @@
import { validateSupabaseProjectId } from '@/lib/core/security/input-validation'
/**
* Returns the validated Supabase REST API base URL for a given project ID.
* Throws if the project ID contains characters that could alter the URL
* (e.g. `#`, `/`, `@`), preventing SSRF via fragment injection.
*/
export function supabaseBaseUrl(projectId: string): string {
const result = validateSupabaseProjectId(projectId)
if (!result.isValid) {
throw new Error(result.error)
}
return `https://${result.sanitized}.supabase.co`
}
/**
* URL-encodes a single storage path segment (bucket name), trimming
* copy-paste whitespace first so the value is safe to interpolate into a URL.
*/
export function encodeStorageSegment(segment: string): string {
return encodeURIComponent(segment.trim())
}
/**
* URL-encodes a storage object path for use inside a URL, preserving `/`
* as a path separator while encoding each segment (and trimming
* copy-paste whitespace) so spaces, `#`, `?`, and other reserved
* characters in file names don't corrupt the request.
*/
export function encodeStoragePath(path: string): string {
return path
.split('/')
.map((segment) => encodeURIComponent(segment.trim()))
.join('/')
}
+128
View File
@@ -0,0 +1,128 @@
import { validateDatabaseIdentifier } from '@/lib/core/security/input-validation'
import type {
SupabaseVectorSearchParams,
SupabaseVectorSearchResponse,
} from '@/tools/supabase/types'
import { supabaseBaseUrl } from '@/tools/supabase/utils'
import type { ToolConfig } from '@/tools/types'
export const vectorSearchTool: ToolConfig<
SupabaseVectorSearchParams,
SupabaseVectorSearchResponse
> = {
id: 'supabase_vector_search',
name: 'Supabase Vector Search',
description: 'Perform similarity search using pgvector in a Supabase table',
version: '1.0.0',
params: {
projectId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Supabase project ID (e.g., jdrkgepadsdopsntdlom)',
},
functionName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The name of the PostgreSQL function that performs vector search (e.g., match_documents)',
},
queryEmbedding: {
type: 'array',
required: true,
visibility: 'user-or-llm',
description: 'The query vector/embedding to search for similar items',
},
matchThreshold: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Minimum similarity threshold (0-1), typically 0.7-0.9',
},
matchCount: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of results to return (default: 10)',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Supabase service role secret key',
},
},
request: {
url: (params) => {
const fnValidation = validateDatabaseIdentifier(params.functionName, 'functionName')
if (!fnValidation.isValid) throw new Error(fnValidation.error)
return `${supabaseBaseUrl(params.projectId)}/rest/v1/rpc/${encodeURIComponent(params.functionName)}`
},
method: 'POST',
headers: (params) => ({
apikey: params.apiKey,
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
body: (params) => {
// Build the RPC call parameters
const rpcParams: Record<string, any> = {
query_embedding: params.queryEmbedding,
}
// Add optional parameters if provided
if (params.matchThreshold !== undefined) {
rpcParams.match_threshold = Number(params.matchThreshold)
}
if (params.matchCount !== undefined) {
rpcParams.match_count = Number(params.matchCount)
}
return rpcParams
},
},
transformResponse: async (response: Response) => {
let data
try {
data = await response.json()
} catch (parseError) {
throw new Error(`Failed to parse Supabase vector search response: ${parseError}`)
}
const resultCount = Array.isArray(data) ? data.length : 0
if (resultCount === 0) {
return {
success: true,
output: {
message: 'No similar vectors found matching the search criteria',
results: data,
},
error: undefined,
}
}
return {
success: true,
output: {
message: `Successfully found ${resultCount} similar vector${resultCount === 1 ? '' : 's'}`,
results: data,
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
results: {
type: 'array',
description:
'Array of records with similarity scores from the vector search. Each record includes a similarity field (0-1) indicating how similar it is to the query vector.',
},
},
}