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 = { 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 = { 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' }, }, }