mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-22 21:38:05 -05:00
feat(oauth): added supabase oauth, block, & tools
This commit is contained in:
159
blocks/blocks/supabase.ts
Normal file
159
blocks/blocks/supabase.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
13
lib/auth.ts
13
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',
|
||||
|
||||
50
lib/oauth.ts
50
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<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
|
||||
|
||||
@@ -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
7
tools/supabase/index.ts
Normal 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
86
tools/supabase/insert.ts
Normal 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
88
tools/supabase/query.ts
Normal 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
63
tools/supabase/types.ts
Normal 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
87
tools/supabase/update.ts
Normal 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'
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user