feat(oauth): added supabase oauth, block, & tools

This commit is contained in:
Waleed Latif
2025-03-09 14:59:21 -07:00
parent a6802e36ce
commit 52350a856c
10 changed files with 545 additions and 15 deletions

159
blocks/blocks/supabase.ts Normal file
View File

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

View File

@@ -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<string, BlockConfig> = {
serper: SerperBlock,
slack: SlackBlock,
starter: StarterBlock,
supabase: SupabaseBlock,
tavily: TavilyBlock,
translate: TranslateBlock,
vision: VisionBlock,

View File

@@ -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',

View File

@@ -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<string, OAuthProviderConfig> = {
},
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

View File

@@ -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<string, ToolConfig> = {
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,

7
tools/supabase/index.ts Normal file
View File

@@ -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

86
tools/supabase/insert.ts Normal file
View File

@@ -0,0 +1,86 @@
import { ToolConfig } from '../types'
import { SupabaseInsertParams, SupabaseInsertResponse } from './types'
export const insertTool: ToolConfig<SupabaseInsertParams, SupabaseInsertResponse> = {
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'
},
}

88
tools/supabase/query.ts Normal file
View File

@@ -0,0 +1,88 @@
import { ToolConfig } from '../types'
import { SupabaseQueryParams, SupabaseQueryResponse } from './types'
export const queryTool: ToolConfig<SupabaseQueryParams, SupabaseQueryResponse> = {
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'
},
}

63
tools/supabase/types.ts Normal file
View File

@@ -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<string, any>
}
export interface SupabaseUpdateParams {
credential: string
projectId: string
table: string
data: Record<string, any>
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
}

87
tools/supabase/update.ts Normal file
View File

@@ -0,0 +1,87 @@
import { ToolConfig } from '../types'
import { SupabaseUpdateParams, SupabaseUpdateResponse } from './types'
export const updateTool: ToolConfig<SupabaseUpdateParams, SupabaseUpdateResponse> = {
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'
},
}