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