diff --git a/blocks/blocks/supabase.ts b/blocks/blocks/supabase.ts new file mode 100644 index 000000000..7591dbd99 --- /dev/null +++ b/blocks/blocks/supabase.ts @@ -0,0 +1,159 @@ +import { SupabaseIcon } from '@/components/icons' +import { ToolResponse } from '@/tools/types' +import { BlockConfig } from '../types' + +interface SupabaseResponse extends ToolResponse { + data: any + error: any +} + +export const SupabaseBlock: BlockConfig = { + type: 'supabase', + name: 'Supabase', + description: 'Connect to and interact with Supabase', + longDescription: + 'Integrate with Supabase to manage your database, authentication, storage, and more. Query data, manage users, and interact with Supabase services using OAuth authentication.', + category: 'tools', + bgColor: '#3ECF8E', + icon: SupabaseIcon, + subBlocks: [ + // Operation selector + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Query Data', id: 'query' }, + { label: 'Insert Data', id: 'insert' }, + { label: 'Update Data', id: 'update' }, + ], + }, + // Supabase Credentials + { + id: 'credential', + title: 'Supabase Account', + type: 'oauth-input', + layout: 'full', + provider: 'supabase', + serviceId: 'supabase', + requiredScopes: ['database.read', 'database.write', 'projects.read'], + placeholder: 'Select Supabase account', + }, + // Common Fields + { + id: 'projectId', + title: 'Project ID', + type: 'short-input', + layout: 'full', + placeholder: 'ID of the Supabase project', + }, + { + id: 'table', + title: 'Table', + type: 'short-input', + layout: 'full', + placeholder: 'Name of the table', + }, + // Query-specific Fields + { + id: 'select', + title: 'Select', + type: 'short-input', + layout: 'full', + placeholder: 'Columns to select (e.g., id, name, email)', + condition: { field: 'operation', value: 'query' }, + }, + { + id: 'filter', + title: 'Filter', + type: 'long-input', + layout: 'full', + placeholder: + 'Filter conditions as JSON (e.g., {"column": "name", "operator": "eq", "value": "John"})', + condition: { field: 'operation', value: 'query' }, + }, + // Insert/Update-specific Fields + { + id: 'data', + title: 'Data', + type: 'long-input', + layout: 'full', + placeholder: + 'Data to insert/update as JSON (e.g., {"name": "John", "email": "john@example.com"})', + condition: { field: 'operation', value: 'insert' }, + }, + { + id: 'data', + title: 'Data', + type: 'long-input', + layout: 'full', + placeholder: + 'Data to insert/update as JSON (e.g., {"name": "John", "email": "john@example.com"})', + condition: { field: 'operation', value: 'update' }, + }, + // Update-specific Fields + { + id: 'filter', + title: 'Filter', + type: 'long-input', + layout: 'full', + placeholder: + 'Filter conditions as JSON (e.g., {"column": "id", "operator": "eq", "value": 123})', + condition: { field: 'operation', value: 'update' }, + }, + ], + tools: { + access: ['supabase_query', 'supabase_insert', 'supabase_update'], + config: { + tool: (params) => { + switch (params.operation) { + case 'query': + return 'supabase_query' + case 'insert': + return 'supabase_insert' + case 'update': + return 'supabase_update' + default: + throw new Error(`Invalid Supabase operation: ${params.operation}`) + } + }, + params: (params) => { + const { credential, data, filter, ...rest } = params + + // Parse JSON strings to objects if they exist + const parsedData = data ? JSON.parse(data as string) : undefined + const parsedFilter = filter ? JSON.parse(filter as string) : undefined + + return { + ...rest, + data: parsedData, + filter: parsedFilter, + credential, + } + }, + }, + }, + inputs: { + operation: { type: 'string', required: true }, + credential: { type: 'string', required: true }, + projectId: { type: 'string', required: true }, + table: { type: 'string', required: true }, + // Query operation inputs + select: { type: 'string', required: false }, + filter: { type: 'string', required: false }, + // Insert/Update operation inputs + data: { type: 'string', required: false }, + }, + outputs: { + response: { + type: { + success: 'boolean', + output: 'json', + severity: 'string', + data: 'json', + error: 'json', + }, + }, + }, +} diff --git a/blocks/index.ts b/blocks/index.ts index 83790ec86..d5c6dc506 100644 --- a/blocks/index.ts +++ b/blocks/index.ts @@ -20,6 +20,7 @@ import { SerperBlock } from './blocks/serper' import { GoogleSheetsBlock } from './blocks/sheets' import { SlackBlock } from './blocks/slack' import { StarterBlock } from './blocks/starter' +import { SupabaseBlock } from './blocks/supabase' import { TavilyBlock } from './blocks/tavily' import { TranslateBlock } from './blocks/translate' import { VisionBlock } from './blocks/vision' @@ -48,6 +49,7 @@ export { YouTubeBlock, NotionBlock, GmailBlock, + SupabaseBlock, XBlock, StarterBlock, PineconeBlock, @@ -82,6 +84,7 @@ const blocks: Record = { serper: SerperBlock, slack: SlackBlock, starter: StarterBlock, + supabase: SupabaseBlock, tavily: TavilyBlock, translate: TranslateBlock, vision: VisionBlock, diff --git a/lib/auth.ts b/lib/auth.ts index bb2662b37..84751d42b 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -180,6 +180,19 @@ export const auth = betterAuth({ ], }, + // Supabase provider + { + providerId: 'supabase', + clientId: process.env.SUPABASE_CLIENT_ID as string, + clientSecret: process.env.SUPABASE_CLIENT_SECRET as string, + authorizationUrl: 'https://api.supabase.com/v1/oauth/authorize', + tokenUrl: 'https://api.supabase.com/v1/oauth/token', + userInfoUrl: 'https://api.supabase.com/v1/oauth/userinfo', + scopes: ['database.read', 'database.write', 'projects.read'], + responseType: 'code', + pkce: true, + }, + // Twitter providers // { // providerId: 'twitter-read', diff --git a/lib/oauth.ts b/lib/oauth.ts index 8206a66f4..6c1fe5af7 100644 --- a/lib/oauth.ts +++ b/lib/oauth.ts @@ -7,11 +7,12 @@ import { GoogleDriveIcon, GoogleIcon, GoogleSheetsIcon, - xIcon as TwitterIcon, + SupabaseIcon, + xIcon, } from '@/components/icons' // Define the base OAuth provider type -export type OAuthProvider = 'google' | 'github' | 'twitter' | string +export type OAuthProvider = 'google' | 'github' | 'x' | 'supabase' | string export type OAuthService = | 'google' | 'google-email' @@ -19,7 +20,8 @@ export type OAuthService = | 'google-docs' | 'google-sheets' | 'github' - | 'twitter' + | 'x' + | 'supabase' // Define the interface for OAuth provider configuration export interface OAuthProviderConfig { @@ -117,21 +119,37 @@ export const OAUTH_PROVIDERS: Record = { }, defaultService: 'github', }, - twitter: { - id: 'twitter', - name: 'Twitter', - icon: (props) => TwitterIcon(props), + x: { + id: 'x', + name: 'X', + icon: (props) => xIcon(props), services: { - twitter: { - id: 'twitter', - name: 'Twitter', - description: 'Post tweets and interact with the Twitter API.', - providerId: 'twitter', - icon: (props) => TwitterIcon(props), - scopes: ['tweet.read', 'tweet.write', 'users.read'], + x: { + id: 'x', + name: 'X', + description: 'Post tweets and interact with the X API.', + providerId: 'x', + icon: (props) => xIcon(props), + scopes: [], }, }, - defaultService: 'twitter', + defaultService: 'x', + }, + supabase: { + id: 'supabase', + name: 'Supabase', + icon: (props) => SupabaseIcon(props), + services: { + supabase: { + id: 'supabase', + name: 'Supabase', + description: 'Connect to your Supabase projects and manage data.', + providerId: 'supabase', + icon: (props) => SupabaseIcon(props), + scopes: ['database.read', 'database.write', 'projects.read'], + }, + }, + defaultService: 'supabase', }, } @@ -177,6 +195,8 @@ export function getServiceIdFromScopes(provider: OAuthProvider, scopes: string[] if (scopes.some((scope) => scope.includes('workflow'))) { return 'github-workflow' } + } else if (provider === 'supabase') { + return 'supabase' } return providerConfig.defaultService diff --git a/tools/index.ts b/tools/index.ts index c3ce4f82c..ef70954fd 100644 --- a/tools/index.ts +++ b/tools/index.ts @@ -24,6 +24,7 @@ import { opportunitiesTool as salesforceOpportunities } from './salesforce/oppor import { searchTool as serperSearch } from './serper/search' import { sheetsReadTool, sheetsUpdateTool, sheetsWriteTool } from './sheets' import { slackMessageTool } from './slack/message' +import { supabaseInsertTool, supabaseQueryTool, supabaseUpdateTool } from './supabase' import { tavilyExtractTool, tavilySearchTool } from './tavily' import { ToolConfig, ToolResponse } from './types' import { formatRequestParams, validateToolRequest } from './utils' @@ -48,6 +49,9 @@ export const tools: Record = { serper_search: serperSearch, tavily_search: tavilySearchTool, tavily_extract: tavilyExtractTool, + supabase_query: supabaseQueryTool, + supabase_insert: supabaseInsertTool, + supabase_update: supabaseUpdateTool, youtube_search: youtubeSearchTool, notion_read: notionReadTool, notion_write: notionWriteTool, diff --git a/tools/supabase/index.ts b/tools/supabase/index.ts new file mode 100644 index 000000000..a36d8f5a4 --- /dev/null +++ b/tools/supabase/index.ts @@ -0,0 +1,7 @@ +import { insertTool } from './insert' +import { queryTool } from './query' +import { updateTool } from './update' + +export const supabaseQueryTool = queryTool +export const supabaseInsertTool = insertTool +export const supabaseUpdateTool = updateTool diff --git a/tools/supabase/insert.ts b/tools/supabase/insert.ts new file mode 100644 index 000000000..4acabc4d7 --- /dev/null +++ b/tools/supabase/insert.ts @@ -0,0 +1,86 @@ +import { ToolConfig } from '../types' +import { SupabaseInsertParams, SupabaseInsertResponse } from './types' + +export const insertTool: ToolConfig = { + id: 'supabase_insert', + name: 'Supabase Insert', + description: 'Insert data into a Supabase table', + version: '1.0', + oauth: { + required: true, + provider: 'supabase', + additionalScopes: ['database.write', 'projects.read'], + }, + params: { + credential: { type: 'string', required: true }, + projectId: { type: 'string', required: true }, + table: { type: 'string', required: true }, + data: { type: 'object', required: true }, + }, + request: { + url: (params) => + `https://api.supabase.com/v1/projects/${params.projectId}/tables/${params.table}/insert`, + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.credential}`, + }), + body: (params) => ({ + data: params.data, + }), + }, + directExecution: async (params: SupabaseInsertParams) => { + try { + // This is a mock implementation + console.log( + `Inserting data into Supabase table ${params.table} in project ${params.projectId}` + ) + console.log('Data to insert:', params.data) + + // Mock response + const mockData = [{ ...params.data, id: Math.floor(Math.random() * 1000) }] + + return { + success: true, + output: { + message: `Successfully inserted data into ${params.table}`, + results: mockData, + }, + data: mockData, + error: null, + } + } catch (error) { + console.error('Error inserting into Supabase:', error) + return { + success: false, + output: { + message: `Error inserting into Supabase: ${error instanceof Error ? error.message : String(error)}`, + }, + data: [], + error: error instanceof Error ? error.message : String(error), + } + } + }, + transformResponse: async (response: Response) => { + if (!response.ok) { + const error = await response.json() + throw new Error(error.message || 'Failed to insert data into Supabase') + } + + const data = await response.json() + + return { + success: true, + output: { + message: 'Successfully inserted data into Supabase', + results: data, + }, + severity: 'info', + data: data, + error: null, + } + }, + transformError: (error: any) => { + return error.message || 'An error occurred while inserting data into Supabase' + }, +} diff --git a/tools/supabase/query.ts b/tools/supabase/query.ts new file mode 100644 index 000000000..dd88bf0e6 --- /dev/null +++ b/tools/supabase/query.ts @@ -0,0 +1,88 @@ +import { ToolConfig } from '../types' +import { SupabaseQueryParams, SupabaseQueryResponse } from './types' + +export const queryTool: ToolConfig = { + id: 'supabase_query', + name: 'Supabase Query', + description: 'Query data from a Supabase table', + version: '1.0', + oauth: { + required: true, + provider: 'supabase', + additionalScopes: ['database.read', 'projects.read'], + }, + params: { + credential: { type: 'string', required: true }, + projectId: { type: 'string', required: true }, + table: { type: 'string', required: true }, + select: { type: 'string', required: false }, + filter: { type: 'object', required: false }, + }, + request: { + url: (params) => + `https://api.supabase.com/v1/projects/${params.projectId}/tables/${params.table}/query`, + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.credential}`, + }), + body: (params) => ({ + select: params.select || '*', + filter: params.filter, + }), + }, + directExecution: async (params: SupabaseQueryParams) => { + try { + // This is a mock implementation + console.log(`Querying Supabase table ${params.table} in project ${params.projectId}`) + + // Mock response + const mockData = [ + { id: 1, name: 'Item 1', description: 'Description 1' }, + { id: 2, name: 'Item 2', description: 'Description 2' }, + ] + + return { + success: true, + output: { + message: `Successfully queried data from ${params.table}`, + results: mockData, + }, + data: mockData, + error: null, + } + } catch (error) { + console.error('Error querying Supabase:', error) + return { + success: false, + output: { + message: `Error querying Supabase: ${error instanceof Error ? error.message : String(error)}`, + }, + data: [], + error: error instanceof Error ? error.message : String(error), + } + } + }, + transformResponse: async (response: Response) => { + if (!response.ok) { + const error = await response.json() + throw new Error(error.message || 'Failed to query data from Supabase') + } + + const data = await response.json() + + return { + success: true, + output: { + message: 'Successfully queried data from Supabase', + results: data, + }, + severity: 'info', + data: data, + error: null, + } + }, + transformError: (error: any) => { + return error.message || 'An error occurred while querying Supabase' + }, +} diff --git a/tools/supabase/types.ts b/tools/supabase/types.ts new file mode 100644 index 000000000..c25105f1b --- /dev/null +++ b/tools/supabase/types.ts @@ -0,0 +1,63 @@ +import { ToolResponse } from '../types' + +export interface SupabaseQueryParams { + credential: string + projectId: string + table: string + select?: string + filter?: { + column: string + operator: string + value: any + } +} + +export interface SupabaseInsertParams { + credential: string + projectId: string + table: string + data: Record +} + +export interface SupabaseUpdateParams { + credential: string + projectId: string + table: string + data: Record + filter: { + column: string + operator: string + value: any + } +} + +export interface SupabaseDeleteParams { + credential: string + projectId: string + table: string + filter: { + column: string + operator: string + value: any + } +} + +export interface SupabaseQueryResponse extends ToolResponse { + data: any[] + error: any +} + +export interface SupabaseInsertResponse extends ToolResponse { + data: any[] + error: any +} + +export interface SupabaseUpdateResponse extends ToolResponse { + data: any[] + error: any +} + +export interface SupabaseDeleteResponse extends ToolResponse { + data: any[] + error: any +} diff --git a/tools/supabase/update.ts b/tools/supabase/update.ts new file mode 100644 index 000000000..7f4293ba7 --- /dev/null +++ b/tools/supabase/update.ts @@ -0,0 +1,87 @@ +import { ToolConfig } from '../types' +import { SupabaseUpdateParams, SupabaseUpdateResponse } from './types' + +export const updateTool: ToolConfig = { + id: 'supabase_update', + name: 'Supabase Update', + description: 'Update data in a Supabase table', + version: '1.0', + oauth: { + required: true, + provider: 'supabase', + additionalScopes: ['database.write', 'projects.read'], + }, + params: { + credential: { type: 'string', required: true }, + projectId: { type: 'string', required: true }, + table: { type: 'string', required: true }, + data: { type: 'object', required: true }, + filter: { type: 'object', required: true }, + }, + request: { + url: (params) => + `https://api.supabase.com/v1/projects/${params.projectId}/tables/${params.table}/update`, + method: 'PATCH', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.credential}`, + }), + body: (params) => ({ + data: params.data, + filter: params.filter, + }), + }, + directExecution: async (params: SupabaseUpdateParams) => { + try { + // This is a mock implementation + console.log(`Updating data in Supabase table ${params.table} in project ${params.projectId}`) + console.log('Filter:', params.filter) + console.log('Data to update:', params.data) + + // Mock response + const mockData = [{ ...params.data, id: params.filter.value }] + + return { + success: true, + output: { + message: `Successfully updated data in ${params.table}`, + results: mockData, + }, + data: mockData, + error: null, + } + } catch (error) { + console.error('Error updating Supabase data:', error) + return { + success: false, + output: { + message: `Error updating Supabase data: ${error instanceof Error ? error.message : String(error)}`, + }, + data: [], + error: error instanceof Error ? error.message : String(error), + } + } + }, + transformResponse: async (response: Response) => { + if (!response.ok) { + const error = await response.json() + throw new Error(error.message || 'Failed to update data in Supabase') + } + + const data = await response.json() + + return { + success: true, + output: { + message: 'Successfully updated data in Supabase', + results: data, + }, + severity: 'info', + data: data, + error: null, + } + }, + transformError: (error: any) => { + return error.message || 'An error occurred while updating data in Supabase' + }, +}