remove other modes for review

This commit is contained in:
Saxon Fletcher committed 2026-02-02 13:40:29 +10:00
1 parent 993fe45aee
commit bc9afa9d47
17 files changed
+47 -1367

No files matched your search

@@ -23,25 +23,17 @@ export const ConnectSheet = ({ open, onOpenChange, connectTab }: ConnectSheetPro
const {
projectConnectionShowAppFrameworks: showAppFrameworks,
projectConnectionShowMobileFrameworks: showMobileFrameworks,
projectConnectionShowOrms: showOrms,
} = useIsFeatureEnabled([
'project_connection:show_app_frameworks',
'project_connection:show_mobile_frameworks',
'project_connection:show_orms',
])
// Filter available modes based on feature flags
const availableModeIds = useMemo(() => {
const modes: string[] = []
const showFrameworks = showAppFrameworks || showMobileFrameworks
if (showFrameworks) modes.push('framework')
modes.push('direct')
if (showOrms) modes.push('orm')
modes.push('mcp')
return modes
}, [showAppFrameworks, showMobileFrameworks, showOrms])
return showFrameworks ? ['framework'] : []
}, [showAppFrameworks, showMobileFrameworks])
const { state, updateField, setMode, activeFields, resolvedSteps, getFieldOptions, schema } =
useConnectState()
@@ -90,10 +82,7 @@ export const ConnectSheet = ({ open, onOpenChange, connectTab }: ConnectSheetPro
useEffect(() => {
if (!open) return
if (typeof connectTab === 'string') {
if (connectTab === 'mcp') setMode('mcp')
else if (connectTab === 'orms') setMode('orm')
else if (connectTab === 'direct') setMode('direct')
else if (connectTab === 'frameworks' || connectTab === 'mobiles') setMode('framework')
if (connectTab === 'frameworks' || connectTab === 'mobiles') setMode('framework')
}
}, [connectTab, open, setMode])
@@ -123,7 +112,9 @@ export const ConnectSheet = ({ open, onOpenChange, connectTab }: ConnectSheetPro
<div className="flex flex-1 flex-col overflow-y-auto">
{/* Configuration Section */}
<div className="space-y-6 border-b p-8 shrink-0">
<ModeSelector modes={availableModes} selected={state.mode} onChange={setMode} />
{availableModes.length > 1 && (
<ModeSelector modes={availableModes} selected={state.mode} onChange={setMode} />
)}
<div className="border-t pt-8">
<ConnectConfigSection
activeFields={activeFields}
@@ -10,10 +10,7 @@ import type { ConnectState } from './Connect.types'
describe('connect.schema:structure', () => {
test('should have all required modes', () => {
const modeIds = connectSchema.modes.map((m) => m.id)
expect(modeIds).toContain('framework')
expect(modeIds).toContain('direct')
expect(modeIds).toContain('orm')
expect(modeIds).toContain('mcp')
expect(modeIds).toEqual(['framework'])
})
test('each mode should have required properties', () => {
@@ -34,23 +31,19 @@ describe('connect.schema:structure', () => {
expect(frameworkMode?.fields).toContain('frameworkUi')
})
test('direct mode should have correct fields', () => {
test('direct mode should be removed', () => {
const directMode = connectSchema.modes.find((m) => m.id === 'direct')
expect(directMode?.fields).toContain('connectionMethod')
expect(directMode?.fields).toContain('useSharedPooler')
expect(directMode?.fields).toContain('connectionType')
expect(directMode).toBeUndefined()
})
test('orm mode should have correct fields', () => {
test('orm mode should be removed', () => {
const ormMode = connectSchema.modes.find((m) => m.id === 'orm')
expect(ormMode?.fields).toContain('orm')
expect(ormMode).toBeUndefined()
})
test('mcp mode should have correct fields', () => {
test('mcp mode should be removed', () => {
const mcpMode = connectSchema.modes.find((m) => m.id === 'mcp')
expect(mcpMode?.fields).toContain('mcpClient')
expect(mcpMode?.fields).toContain('mcpReadonly')
expect(mcpMode?.fields).toContain('mcpFeatures')
expect(mcpMode).toBeUndefined()
})
test('all mode fields should exist in fields definition', () => {
@@ -89,37 +82,29 @@ describe('connect.schema:fields', () => {
expect(field.dependsOn).toEqual({ framework: ['nextjs', 'react'] })
})
test('connectionMethod field should have radio-list type', () => {
test('connectionMethod field should be removed', () => {
const field = connectSchema.fields.connectionMethod
expect(field.type).toBe('radio-list')
expect(field.options).toEqual({ source: 'connectionMethods' })
expect(field.defaultValue).toBe('direct')
expect(field).toBeUndefined()
})
test('useSharedPooler field should depend on transaction connection method', () => {
test('useSharedPooler field should be removed', () => {
const field = connectSchema.fields.useSharedPooler
expect(field.type).toBe('switch')
expect(field.dependsOn).toEqual({ connectionMethod: ['transaction'] })
expect(field).toBeUndefined()
})
test('orm field should have radio-list type', () => {
test('orm field should be removed', () => {
const field = connectSchema.fields.orm
expect(field.type).toBe('radio-list')
expect(field.options).toEqual({ source: 'orms' })
expect(field.defaultValue).toBe('prisma')
expect(field).toBeUndefined()
})
test('mcpClient field should have select type', () => {
test('mcpClient field should be removed', () => {
const field = connectSchema.fields.mcpClient
expect(field.type).toBe('select')
expect(field.options).toEqual({ source: 'mcpClients' })
expect(field.defaultValue).toBe('cursor')
expect(field).toBeUndefined()
})
test('mcpFeatures field should have multi-select type', () => {
test('mcpFeatures field should be removed', () => {
const field = connectSchema.fields.mcpFeatures
expect(field.type).toBe('multi-select')
expect(field.options).toEqual({ source: 'mcpFeatures' })
expect(field).toBeUndefined()
})
})
@@ -198,101 +183,35 @@ describe('connect.schema:steps resolution', () => {
})
describe('direct mode steps', () => {
test('should resolve connection step for default direct mode', () => {
test('should not resolve steps for direct mode', () => {
const state: ConnectState = { mode: 'direct' }
const steps = resolveSteps(connectSchema, state)
expect(steps.find((s) => s.id === 'connection')).toBeDefined()
})
test('should resolve install and files steps for nodejs connection type', () => {
const state: ConnectState = { mode: 'direct', connectionType: 'nodejs' }
const steps = resolveSteps(connectSchema, state)
expect(steps.find((s) => s.id === 'direct-install')).toBeDefined()
expect(steps.find((s) => s.id === 'direct-files')).toBeDefined()
})
test('should resolve install and files steps for golang connection type', () => {
const state: ConnectState = { mode: 'direct', connectionType: 'golang' }
const steps = resolveSteps(connectSchema, state)
expect(steps.find((s) => s.id === 'direct-install')).toBeDefined()
expect(steps.find((s) => s.id === 'direct-files')).toBeDefined()
})
test('should resolve install and files steps for python connection type', () => {
const state: ConnectState = { mode: 'direct', connectionType: 'python' }
const steps = resolveSteps(connectSchema, state)
expect(steps.find((s) => s.id === 'direct-install')).toBeDefined()
})
test('should resolve install and files steps for dotnet connection type', () => {
const state: ConnectState = { mode: 'direct', connectionType: 'dotnet' }
const steps = resolveSteps(connectSchema, state)
expect(steps.find((s) => s.id === 'direct-install')).toBeDefined()
expect(steps.length).toBe(0)
})
})
describe('orm mode steps', () => {
test('should resolve install and configure steps for prisma', () => {
test('should not resolve steps for orm mode', () => {
const state: ConnectState = { mode: 'orm', orm: 'prisma' }
const steps = resolveSteps(connectSchema, state)
expect(steps.find((s) => s.id === 'install')).toBeDefined()
expect(steps.find((s) => s.id === 'configure')).toBeDefined()
expect(steps.find((s) => s.id === 'install-skills')).toBeDefined()
})
test('should resolve install and configure steps for drizzle', () => {
const state: ConnectState = { mode: 'orm', orm: 'drizzle' }
const steps = resolveSteps(connectSchema, state)
expect(steps.find((s) => s.id === 'install')).toBeDefined()
expect(steps.find((s) => s.id === 'configure')).toBeDefined()
expect(steps.length).toBe(0)
})
})
describe('mcp mode steps', () => {
test('should resolve configure step for cursor client', () => {
test('should not resolve steps for mcp mode', () => {
const state: ConnectState = { mode: 'mcp', mcpClient: 'cursor' }
const steps = resolveSteps(connectSchema, state)
expect(steps.find((s) => s.id === 'configure-mcp')).toBeDefined()
expect(steps.find((s) => s.id === 'install-skills')).toBeDefined()
})
test('should resolve codex-specific steps for codex client', () => {
const state: ConnectState = { mode: 'mcp', mcpClient: 'codex' }
const steps = resolveSteps(connectSchema, state)
expect(steps.find((s) => s.id === 'codex-add-server')).toBeDefined()
expect(steps.find((s) => s.id === 'codex-enable-remote')).toBeDefined()
expect(steps.find((s) => s.id === 'codex-authenticate')).toBeDefined()
expect(steps.find((s) => s.id === 'codex-verify')).toBeDefined()
})
test('should resolve claude-code-specific steps for claude-code client', () => {
const state: ConnectState = { mode: 'mcp', mcpClient: 'claude-code' }
const steps = resolveSteps(connectSchema, state)
expect(steps.find((s) => s.id === 'claude-add-server')).toBeDefined()
expect(steps.find((s) => s.id === 'claude-authenticate')).toBeDefined()
})
test('should resolve default mcp steps for other clients', () => {
const state: ConnectState = { mode: 'mcp', mcpClient: 'unknown-client' }
const steps = resolveSteps(connectSchema, state)
expect(steps.find((s) => s.id === 'configure-mcp')).toBeDefined()
expect(steps.length).toBe(0)
})
})
describe('skills install step', () => {
test('should include skills install step in all modes', () => {
const modes: ConnectState['mode'][] = ['framework', 'direct', 'orm', 'mcp']
const modes: ConnectState['mode'][] = ['framework']
modes.forEach((mode) => {
const state: ConnectState = { mode }
@@ -336,12 +255,11 @@ describe('connect.schema:step content paths', () => {
expect(exploreStep?.content).toBe('steps/shadcn/explore')
})
test('direct connection step should have valid content path', () => {
test('direct connection step should not be resolved', () => {
const state: ConnectState = { mode: 'direct' }
const steps = resolveSteps(connectSchema, state)
const connectionStep = steps.find((s) => s.id === 'connection')
expect(connectionStep?.content).toBe('steps/direct-connection')
expect(steps.length).toBe(0)
})
test('skills install step should have valid content path', () => {
@@ -352,50 +270,31 @@ describe('connect.schema:step content paths', () => {
expect(skillsStep?.content).toBe('steps/skills-install')
})
test('orm configure step should use template content path', () => {
// The ORM configure step uses a template {{orm}} that gets resolved
// by the dynamic import system, not the resolver
test('orm configure step should not be resolved', () => {
const state: ConnectState = { mode: 'orm', orm: 'prisma' }
const steps = resolveSteps(connectSchema, state)
const configureStep = steps.find((s) => s.id === 'configure')
// The content path uses template syntax for the component loader
expect(configureStep?.content).toBe('{{orm}}')
expect(steps.length).toBe(0)
})
test('mcp cursor configure step should have valid content path', () => {
test('mcp cursor configure step should not be resolved', () => {
const state: ConnectState = { mode: 'mcp', mcpClient: 'cursor' }
const steps = resolveSteps(connectSchema, state)
const configureStep = steps.find((s) => s.id === 'configure-mcp')
expect(configureStep?.content).toBe('steps/mcp/cursor')
expect(steps.length).toBe(0)
})
test('codex steps should have valid content paths', () => {
test('codex steps should not be resolved', () => {
const state: ConnectState = { mode: 'mcp', mcpClient: 'codex' }
const steps = resolveSteps(connectSchema, state)
expect(steps.find((s) => s.id === 'codex-add-server')?.content).toBe(
'steps/mcp/codex/add-server'
)
expect(steps.find((s) => s.id === 'codex-enable-remote')?.content).toBe(
'steps/mcp/codex/enable-remote'
)
expect(steps.find((s) => s.id === 'codex-authenticate')?.content).toBe(
'steps/mcp/codex/authenticate'
)
expect(steps.find((s) => s.id === 'codex-verify')?.content).toBe('steps/mcp/codex/verify')
expect(steps.length).toBe(0)
})
test('claude-code steps should have valid content paths', () => {
test('claude-code steps should not be resolved', () => {
const state: ConnectState = { mode: 'mcp', mcpClient: 'claude-code' }
const steps = resolveSteps(connectSchema, state)
expect(steps.find((s) => s.id === 'claude-add-server')?.content).toBe(
'steps/mcp/claude-code/add-server'
)
expect(steps.find((s) => s.id === 'claude-authenticate')?.content).toBe(
'steps/mcp/claude-code/authenticate'
)
expect(steps.length).toBe(0)
})
})
@@ -60,92 +60,6 @@ const frameworkShadcnExploreStep: StepDefinition = {
content: 'steps/shadcn/explore',
}
const directConnectionStep: StepDefinition = {
id: 'connection',
title: 'Connection string',
description: 'Copy the connection details for your database.',
content: 'steps/direct-connection',
}
const directInstallStep: StepDefinition = {
id: 'direct-install',
title: 'Install dependencies',
description: 'Run this command to install the required dependencies.',
content: 'steps/direct-install',
}
const directFilesStep: StepDefinition = {
id: 'direct-files',
title: 'Add files',
description: 'Add the following files to your project.',
content: 'steps/direct-files',
}
const mcpConfigureStep: StepDefinition = {
id: 'configure-mcp',
title: 'Configure MCP',
description: 'Set up your MCP client.',
content: 'steps/mcp/cursor',
}
// Codex-specific MCP steps
const codexAddServerStep: StepDefinition = {
id: 'codex-add-server',
title: 'Add the Supabase MCP server to Codex',
description: 'Run this command to add the server.',
content: 'steps/mcp/codex/add-server',
}
const codexEnableRemoteStep: StepDefinition = {
id: 'codex-enable-remote',
title: 'Enable remote MCP client support',
description: 'Add this to your ~/.codex/config.toml file.',
content: 'steps/mcp/codex/enable-remote',
}
const codexAuthenticateStep: StepDefinition = {
id: 'codex-authenticate',
title: 'Authenticate',
description: 'Run the authentication command.',
content: 'steps/mcp/codex/authenticate',
}
const codexVerifyStep: StepDefinition = {
id: 'codex-verify',
title: 'Verify authentication',
description: 'Run /mcp inside Codex to verify.',
content: 'steps/mcp/codex/verify',
}
const claudeAddServerStep: StepDefinition = {
id: 'claude-add-server',
title: 'Add MCP server',
description: 'Add the MCP server to your project config using the command line.',
content: 'steps/mcp/claude-code/add-server',
}
const claudeAuthenticateStep: StepDefinition = {
id: 'claude-authenticate',
title: 'Authenticate',
description:
'After configuring the MCP server, you need to authenticate. In a regular terminal (not the IDE extension) run:',
content: 'steps/mcp/claude-code/authenticate',
}
const ormInstallStep: StepDefinition = {
id: 'install',
title: 'Install ORM',
description: 'Add the ORM to your project.',
content: 'steps/orm-install',
}
const ormConfigureStep: StepDefinition = {
id: 'configure',
title: 'Configure ORM',
description: 'Set up your ORM configuration.',
content: '{{orm}}',
}
const skillsInstallStep: StepDefinition = {
id: 'install-skills',
title: 'Install Agent Skills (Optional)',
@@ -169,24 +83,6 @@ export const connectSchema: ConnectSchema = {
description: 'Use a client library',
fields: ['framework', 'frameworkVariant', 'library', 'frameworkUi'],
},
{
id: 'direct',
label: 'Direct',
description: 'Connection string',
fields: ['connectionMethod', 'useSharedPooler', 'connectionType'],
},
{
id: 'orm',
label: 'ORM',
description: 'Third-party library',
fields: ['orm'],
},
{
id: 'mcp',
label: 'MCP',
description: 'Connect your agent',
fields: ['mcpClient', 'mcpReadonly', 'mcpFeatures'],
},
],
// -------------------------------------------------------------------------
@@ -224,64 +120,6 @@ export const connectSchema: ConnectSchema = {
defaultValue: false,
dependsOn: { framework: ['nextjs', 'react'] },
},
// Direct connection fields
connectionMethod: {
id: 'connectionMethod',
type: 'radio-list',
label: 'Connection Method',
options: { source: 'connectionMethods' },
defaultValue: 'direct',
},
useSharedPooler: {
id: 'useSharedPooler',
type: 'switch',
label: 'Use IPv4 connection (Shared Pooler)',
description: 'Only recommended when your network does not support IPv6',
defaultValue: false,
dependsOn: { connectionMethod: ['transaction'] },
},
connectionType: {
id: 'connectionType',
type: 'select',
label: 'Type',
options: { source: 'connectionTypes' },
defaultValue: 'uri',
},
// ORM fields
orm: {
id: 'orm',
type: 'radio-list',
label: 'ORM',
options: { source: 'orms' },
defaultValue: 'prisma',
},
// MCP fields
mcpClient: {
id: 'mcpClient',
type: 'select',
label: 'Client',
description: 'Choose the MCP client you are using.',
options: { source: 'mcpClients' },
defaultValue: 'cursor',
},
mcpReadonly: {
id: 'mcpReadonly',
type: 'switch',
label: 'Read-only',
description: 'Only allow read operations on your database',
defaultValue: false,
},
mcpFeatures: {
id: 'mcpFeatures',
type: 'multi-select',
label: 'Feature groups',
description:
'Only enable a subset of features. Helps keep the number of tools within MCP client limits.',
options: { source: 'mcpFeatures' },
},
},
// -------------------------------------------------------------------------
@@ -317,31 +155,6 @@ export const connectSchema: ConnectSchema = {
DEFAULT: [frameworkInstallStep, frameworkConfigureStep, skillsInstallStep],
},
},
direct: {
connectionType: {
nodejs: [directInstallStep, directFilesStep, skillsInstallStep],
golang: [directInstallStep, directFilesStep, skillsInstallStep],
dotnet: [directInstallStep, directFilesStep, skillsInstallStep],
python: [directInstallStep, directFilesStep, skillsInstallStep],
sqlalchemy: [directInstallStep, directFilesStep, skillsInstallStep],
DEFAULT: [directConnectionStep, skillsInstallStep],
},
},
orm: [ormInstallStep, ormConfigureStep, skillsInstallStep],
mcp: {
mcpClient: {
codex: [
codexAddServerStep,
codexEnableRemoteStep,
codexAuthenticateStep,
codexVerifyStep,
skillsInstallStep,
],
'claude-code': [claudeAddServerStep, claudeAuthenticateStep, skillsInstallStep],
DEFAULT: [mcpConfigureStep, skillsInstallStep],
},
},
DEFAULT: [skillsInstallStep],
},
},
}
@@ -1,64 +0,0 @@
import { MultipleCodeBlock } from 'ui-patterns/MultipleCodeBlock'
import type { ContentFileProps } from '@/components/interfaces/ConnectSheet/Connect.types'
const ContentFile = ({ connectionStringPooler }: ContentFileProps) => {
const files = [
{
name: '.env',
language: 'bash',
code:
connectionStringPooler.ipv4SupportedForDedicatedPooler &&
connectionStringPooler.transactionDedicated
? `
DATABASE_URL="${connectionStringPooler.transactionDedicated}"
`
: connectionStringPooler.transactionDedicated &&
!connectionStringPooler.ipv4SupportedForDedicatedPooler
? `
# Use Shared connection pooler (supports both IPv4/IPv6)
DATABASE_URL="${connectionStringPooler.transactionShared}"
# If your network supports IPv6 or you purchased IPv4 addon, use dedicated pooler
# DATABASE_URL="${connectionStringPooler.transactionDedicated}"
`
: `
DATABASE_URL="${connectionStringPooler.transactionShared}"
`,
},
{
name: 'drizzle/schema.tsx',
language: 'tsx',
code: `
import { pgTable, serial, text, varchar } from "drizzle-orm/pg-core";
export const users = pgTable('users', {
id: serial('id').primaryKey(),
fullName: text('full_name'),
phone: varchar('phone', { length: 256 }),
});
`,
},
{
name: 'index.tsx',
language: 'tsx',
code: `
import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
import { users } from './schema'
const connectionString = process.env.DATABASE_URL
// Disable prefetch as it is not supported for "Transaction" pool mode
const client = postgres(connectionString, { prepare: false })
const db = drizzle(client);
const allUsers = await db.select().from(users);
`,
},
]
return <MultipleCodeBlock files={files} />
}
export default ContentFile
@@ -1,62 +0,0 @@
import { IS_PLATFORM } from 'lib/constants'
import { MultipleCodeBlock } from 'ui-patterns/MultipleCodeBlock'
import type { ContentFileProps } from '@/components/interfaces/ConnectSheet/Connect.types'
const ContentFile = ({ connectionStringPooler }: ContentFileProps) => {
const files = [
{
name: '.env.local',
language: 'bash',
code:
connectionStringPooler.ipv4SupportedForDedicatedPooler &&
connectionStringPooler.transactionDedicated
? `
# Connect to Supabase via connection pooling.
DATABASE_URL="${connectionStringPooler.transactionDedicated}?pgbouncer=true"
# Direct connection to the database. Used for migrations.
DIRECT_URL="${connectionStringPooler.sessionDedicated}"
`
: connectionStringPooler.transactionDedicated &&
!connectionStringPooler.ipv4SupportedForDedicatedPooler
? `
# Connect to Supabase via Shared Connection Pooler
DATABASE_URL="${connectionStringPooler.transactionShared}?pgbouncer=true"
# Direct connection to the database through Shared Pooler (supports IPv4/IPv6). Used for migrations.
DIRECT_URL="${connectionStringPooler.sessionShared}"
# If your network supports IPv6 or you purchased IPv4 addon, use dedicated pooler
# DATABASE_URL="${connectionStringPooler.transactionDedicated}?pgbouncer=true"
# DIRECT_URL="${connectionStringPooler.sessionDedicated}"
`
: `
# Connect to Supabase ${IS_PLATFORM ? 'via connection pooling' : ''}
DATABASE_URL="${IS_PLATFORM ? `${connectionStringPooler.transactionShared}?pgbouncer=true` : connectionStringPooler.direct}"
# Direct connection to the database. Used for migrations
DIRECT_URL="${IS_PLATFORM ? connectionStringPooler.sessionShared : connectionStringPooler.direct}"
`,
},
{
name: 'prisma/schema.prisma',
language: 'bash',
code: `
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DIRECT_URL")
}
`,
},
]
return <MultipleCodeBlock files={files} />
}
export default ContentFile
@@ -1,93 +0,0 @@
import { useMemo } from 'react'
import { CodeBlock } from 'ui'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import {
type ConnectionStringMethod,
type DatabaseConnectionType,
} from '../../../Connect.constants'
import type { StepContentProps } from '../../../Connect.types'
import { ConnectionParameters } from '../../../ConnectionParameters'
import {
PASSWORD_PLACEHOLDER,
buildConnectionParameters,
buildSafeConnectionString,
parseConnectionParams,
resolveConnectionString,
} from '../../../ConnectionString.utils'
const buildPsqlCommand = (params: { host: string; port: string; database: string; user: string }) =>
`psql -h ${params.host} -p ${params.port} -d ${params.database} -U ${params.user}`
const buildJdbcString = (params: { host: string; port: string; database: string; user: string }) =>
`jdbc:postgresql://${params.host}:${params.port}/${params.database}?user=${params.user}&password=${PASSWORD_PLACEHOLDER}`
/**
* Step component for direct database connections.
* Uses state to determine which connection string to show.
*/
function DirectConnectionContent({ state, connectionStringPooler }: StepContentProps) {
const connectionType = (state.connectionType as DatabaseConnectionType) ?? 'uri'
const connectionMethod = (state.connectionMethod as ConnectionStringMethod) ?? 'direct'
const useSharedPooler = Boolean(state.useSharedPooler)
// Determine which connection string to use
const resolvedConnectionString = useMemo(
() =>
resolveConnectionString({
connectionMethod,
useSharedPooler,
connectionStringPooler,
}),
[connectionMethod, useSharedPooler, connectionStringPooler]
)
const connectionParams = useMemo(
() => parseConnectionParams(resolvedConnectionString),
[resolvedConnectionString]
)
const safeConnectionString = useMemo(
() => buildSafeConnectionString(resolvedConnectionString, connectionParams),
[resolvedConnectionString, connectionParams]
)
const connectionString = useMemo(() => {
switch (connectionType) {
case 'psql':
return buildPsqlCommand(connectionParams)
case 'jdbc':
return buildJdbcString(connectionParams)
case 'php':
return `DATABASE_URL=${safeConnectionString}`
case 'uri':
default:
return safeConnectionString
}
}, [connectionType, connectionParams, safeConnectionString])
if (!resolvedConnectionString) {
return (
<div className="p-4">
<GenericSkeletonLoader />
</div>
)
}
return (
<div className="flex flex-col gap-2">
<CodeBlock
className="[&_code]:text-foreground"
wrapperClassName="lg:col-span-2"
value={connectionString}
hideLineNumbers
language="bash"
>
{connectionString}
</CodeBlock>
<ConnectionParameters parameters={buildConnectionParameters(connectionParams)} />
</div>
)
}
export default DirectConnectionContent
@@ -1,264 +0,0 @@
import { useEffect, useMemo, useState } from 'react'
import { CodeBlock } from 'ui'
import { MultipleCodeBlock } from 'ui-patterns/MultipleCodeBlock'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import {
type ConnectionStringMethod,
type DatabaseConnectionType,
} from '../../../Connect.constants'
import type { StepContentProps } from '../../../Connect.types'
import { ConnectionParameters } from '../../../ConnectionParameters'
import {
PASSWORD_PLACEHOLDER,
buildConnectionParameters,
buildSafeConnectionString,
parseConnectionParams,
resolveConnectionString,
} from '../../../ConnectionString.utils'
const DOTNET_CONFIG_COMMAND =
'dotnet add package Microsoft.Extensions.Configuration.Json --version YOUR_DOTNET_VERSION'
type DirectFilesConfig = {
files: {
name: string
language?: string
code: string
}[]
connectionStringFile?: string
postCommands?: { label: string; command: string }[]
}
function DirectFilesContent({ state, connectionStringPooler }: StepContentProps) {
const connectionType = (state.connectionType as DatabaseConnectionType) ?? 'uri'
const connectionMethod = (state.connectionMethod as ConnectionStringMethod) ?? 'direct'
const useSharedPooler = Boolean(state.useSharedPooler)
const resolvedConnectionString = useMemo(
() =>
resolveConnectionString({
connectionMethod,
useSharedPooler,
connectionStringPooler,
}),
[connectionMethod, useSharedPooler, connectionStringPooler]
)
const connectionParams = useMemo(
() => parseConnectionParams(resolvedConnectionString),
[resolvedConnectionString]
)
const safeConnectionString = useMemo(
() => buildSafeConnectionString(resolvedConnectionString, connectionParams),
[resolvedConnectionString, connectionParams]
)
const config: DirectFilesConfig | null = useMemo(() => {
const envFile = {
name: '.env',
language: 'bash',
code: `DATABASE_URL=${safeConnectionString}`,
}
switch (connectionType) {
case 'nodejs':
return {
files: [
{
name: 'db.js',
language: 'js',
code: `import postgres from 'postgres'
const connectionString = process.env.DATABASE_URL
const sql = postgres(connectionString)
export default sql`,
},
envFile,
],
connectionStringFile: envFile.name,
}
case 'golang':
return {
files: [
{
name: 'main.go',
language: 'go',
code: `package main
import (
\t"context"
\t"log"
\t"os"
\t"github.com/jackc/pgx/v5"
)
func main() {
\tconn, err := pgx.Connect(context.Background(), os.Getenv("DATABASE_URL"))
\tif err != nil {
\t\tlog.Fatalf("Failed to connect to the database: %v", err)
\t}
\tdefer conn.Close(context.Background())
\t// Example query to test connection
\tvar version string
\tif err := conn.QueryRow(context.Background(), "SELECT version()").Scan(&version); err != nil {
\t\tlog.Fatalf("Query failed: %v", err)
\t}
\tlog.Println("Connected to:", version)
}`,
},
envFile,
],
connectionStringFile: envFile.name,
}
case 'dotnet':
return {
files: [
{
name: 'appsettings.json',
language: 'json',
code: `{
"ConnectionStrings": {
"DefaultConnection": "Host=${connectionParams.host};Database=${connectionParams.database};Username=${connectionParams.user};Password=${PASSWORD_PLACEHOLDER};SSL Mode=Require;Trust Server Certificate=true"
}
}`,
},
],
connectionStringFile: 'appsettings.json',
postCommands: [
{
label: 'Add the configuration package to read the settings.',
command: DOTNET_CONFIG_COMMAND,
},
],
}
case 'python':
return {
files: [
{
name: 'main.py',
language: 'python',
code: `import psycopg2
from dotenv import load_dotenv
import os
# Load environment variables from .env
load_dotenv()
# Fetch variables
DATABASE_URL = os.getenv("DATABASE_URL")
# Connect to the database
connection = psycopg2.connect(DATABASE_URL)`,
},
envFile,
],
connectionStringFile: envFile.name,
}
case 'sqlalchemy':
return {
files: [
{
name: 'main.py',
language: 'python',
code: `from sqlalchemy import create_engine
# from sqlalchemy.pool import NullPool
from dotenv import load_dotenv
import os
# Load environment variables from .env
load_dotenv()
# Fetch variables
USER = os.getenv("user")
PASSWORD = os.getenv("password")
HOST = os.getenv("host")
PORT = os.getenv("port")
DBNAME = os.getenv("dbname")
# Construct the SQLAlchemy connection string
DATABASE_URL = f"postgresql+psycopg2://{USER}:{PASSWORD}@{HOST}:{PORT}/{DBNAME}?sslmode=require"
# Create the SQLAlchemy engine
engine = create_engine(DATABASE_URL)
# If using Transaction Pooler or Session Pooler, we want to ensure we disable SQLAlchemy client side pooling -
# https://docs.sqlalchemy.org/en/20/core/pooling.html#switching-pool-implementations
# engine = create_engine(DATABASE_URL, poolclass=NullPool)
# Test the connection
try:
with engine.connect() as connection:
print("Connection successful!")
except Exception as e:
print(f"Failed to connect: {e}")`,
},
{
name: '.env',
language: 'bash',
code: [
`user=${connectionParams.user}`,
`password=${PASSWORD_PLACEHOLDER}`,
`host=${connectionParams.host}`,
`port=${connectionParams.port}`,
`dbname=${connectionParams.database}`,
].join('\n'),
},
],
connectionStringFile: '.env',
}
default:
return null
}
}, [connectionType, safeConnectionString, connectionParams])
const defaultFile = config?.files[0]?.name ?? ''
const [activeFile, setActiveFile] = useState(defaultFile)
useEffect(() => {
setActiveFile(defaultFile)
}, [connectionType, defaultFile])
if (!resolvedConnectionString) {
return (
<div className="p-4">
<GenericSkeletonLoader />
</div>
)
}
if (!config?.files.length) {
return null
}
return (
<div className="flex flex-col gap-3">
<MultipleCodeBlock files={config.files} value={activeFile} onValueChange={setActiveFile} />
<ConnectionParameters parameters={buildConnectionParameters(connectionParams)} />
{(config.postCommands ?? []).map((command) => (
<div key={command.command} className="flex flex-col gap-2">
<p className="text-sm text-foreground-light">{command.label}</p>
<CodeBlock
className="[&_code]:text-foreground"
wrapperClassName="lg:col-span-2"
value={command.command}
hideLineNumbers
language="bash"
>
{command.command}
</CodeBlock>
</div>
))}
</div>
)
}
export default DirectFilesContent
@@ -1,36 +0,0 @@
import { CodeBlock } from 'ui'
import type { StepContentProps } from '../../../Connect.types'
import examples from '../../../DirectConnectionExamples'
/**
* Step component for direct connection install commands.
*/
function DirectInstallContent({ state }: StepContentProps) {
const connectionType = (state.connectionType as string) ?? 'uri'
const example = examples[connectionType as keyof typeof examples]
const exampleInstallCommands = example?.installCommands ?? []
if (exampleInstallCommands.length === 0) {
return null
}
return (
<div className="flex flex-col gap-2">
{exampleInstallCommands.map((cmd) => (
<CodeBlock
key={`example-install-command-${cmd}`}
className="[&_code]:text-foreground"
wrapperClassName="lg:col-span-2"
value={cmd}
hideLineNumbers
language="bash"
>
{cmd}
</CodeBlock>
))}
</div>
)
}
export default DirectInstallContent
@@ -1,47 +0,0 @@
import { useMemo } from 'react'
import { CodeBlock } from 'ui'
import { IS_PLATFORM } from 'lib/constants'
import { FEATURE_GROUPS_NON_PLATFORM, FEATURE_GROUPS_PLATFORM } from 'ui-patterns/McpUrlBuilder'
import type { StepContentProps } from '../../../../../Connect.types'
function useMcpUrl(
state: StepContentProps['state'],
projectKeys: StepContentProps['projectKeys']
): string {
const readonly = Boolean(state.mcpReadonly)
const selectedFeatures = Array.isArray(state.mcpFeatures) ? state.mcpFeatures : []
const baseUrl = IS_PLATFORM ? 'https://mcp.supabase.com' : projectKeys.apiUrl ?? ''
return useMemo(() => {
const params = new URLSearchParams()
if (readonly) params.set('readonly', 'true')
const supportedFeatures = IS_PLATFORM ? FEATURE_GROUPS_PLATFORM : FEATURE_GROUPS_NON_PLATFORM
const validFeatures = selectedFeatures.filter((f) =>
supportedFeatures.some((group) => group.id === f)
)
if (validFeatures.length > 0) {
params.set('features', validFeatures.join(','))
}
const queryString = params.toString()
return queryString ? `${baseUrl}?${queryString}` : baseUrl
}, [baseUrl, readonly, selectedFeatures])
}
function ClaudeAddServerContent({ state, projectKeys }: StepContentProps) {
const mcpUrl = useMcpUrl(state, projectKeys)
const command = `claude mcp add --scope project --transport http supabase "${mcpUrl}"`
return (
<CodeBlock
className="[&_code]:text-foreground"
value={command}
hideLineNumbers
language="bash"
/>
)
}
export default ClaudeAddServerContent
@@ -1,22 +0,0 @@
import { CodeBlock } from 'ui'
import type { StepContentProps } from '../../../../../Connect.types'
function ClaudeAuthenticateContent(_props: StepContentProps) {
return (
<div className="space-y-2">
<CodeBlock
className="[&_code]:text-foreground"
value="claude /mcp"
hideLineNumbers
language="bash"
/>
<p className="text-sm text-foreground-light">
Select the <code className="text-xs bg-surface-300 px-1 py-0.5 rounded">supabase</code>{' '}
server, then <span className="font-medium">Authenticate</span> to begin the flow.
</p>
</div>
)
}
export default ClaudeAuthenticateContent
@@ -1,47 +0,0 @@
import { useMemo } from 'react'
import { CodeBlock } from 'ui'
import { IS_PLATFORM } from 'lib/constants'
import { FEATURE_GROUPS_NON_PLATFORM, FEATURE_GROUPS_PLATFORM } from 'ui-patterns/McpUrlBuilder'
import type { StepContentProps } from '../../../../../Connect.types'
function useMcpUrl(
state: StepContentProps['state'],
projectKeys: StepContentProps['projectKeys']
): string {
const readonly = Boolean(state.mcpReadonly)
const selectedFeatures = Array.isArray(state.mcpFeatures) ? state.mcpFeatures : []
const baseUrl = IS_PLATFORM ? 'https://mcp.supabase.com' : projectKeys.apiUrl ?? ''
return useMemo(() => {
const params = new URLSearchParams()
if (readonly) params.set('readonly', 'true')
const supportedFeatures = IS_PLATFORM ? FEATURE_GROUPS_PLATFORM : FEATURE_GROUPS_NON_PLATFORM
const validFeatures = selectedFeatures.filter((f) =>
supportedFeatures.some((group) => group.id === f)
)
if (validFeatures.length > 0) {
params.set('features', validFeatures.join(','))
}
const queryString = params.toString()
return queryString ? `${baseUrl}?${queryString}` : baseUrl
}, [baseUrl, readonly, selectedFeatures])
}
function CodexAddServerContent({ state, projectKeys }: StepContentProps) {
const mcpUrl = useMcpUrl(state, projectKeys)
const command = `codex mcp add supabase --url ${mcpUrl}`
return (
<CodeBlock
className="[&_code]:text-foreground"
value={command}
hideLineNumbers
language="bash"
/>
)
}
export default CodexAddServerContent
@@ -1,18 +0,0 @@
import { CodeBlock } from 'ui'
import type { StepContentProps } from '../../../../../Connect.types'
function CodexAuthenticateContent(_props: StepContentProps) {
const command = 'codex mcp login supabase'
return (
<CodeBlock
className="[&_code]:text-foreground"
value={command}
hideLineNumbers
language="bash"
/>
)
}
export default CodexAuthenticateContent
@@ -1,19 +0,0 @@
import { CodeBlock } from 'ui'
import type { StepContentProps } from '../../../../../Connect.types'
function CodexEnableRemoteContent(_props: StepContentProps) {
const configContent = `[mcp]
remote_mcp_client_enabled = true`
return (
<CodeBlock
className="[&_code]:text-foreground"
value={configContent}
hideLineNumbers
language="toml"
/>
)
}
export default CodexEnableRemoteContent
@@ -1,14 +0,0 @@
import type { StepContentProps } from '../../../../../Connect.types'
function CodexVerifyContent(_props: StepContentProps) {
return (
<div className="space-y-2">
<p className="text-sm text-foreground-light">
Run <code className="text-xs bg-surface-300 px-1 py-0.5 rounded">/mcp</code> inside Codex to
verify authentication.
</p>
</div>
)
}
export default CodexVerifyContent
@@ -1,90 +0,0 @@
import { useParams } from 'common'
import { BASE_PATH, IS_PLATFORM } from 'lib/constants'
import { useTrack } from 'lib/telemetry/track'
import { useMemo } from 'react'
import {
createMcpCopyHandler,
FEATURE_GROUPS_NON_PLATFORM,
FEATURE_GROUPS_PLATFORM,
getMcpUrl,
MCP_CLIENTS,
McpConfigurationDisplay,
} from 'ui-patterns/McpUrlBuilder'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
import type { StepContentProps } from '../../../../Connect.types'
function McpCursorContent({ state, projectKeys }: StepContentProps) {
const { ref: projectRef } = useParams()
if (!projectRef) {
return (
<div className="flex flex-col gap-2">
<ShimmeringLoader className="w-3/4" />
<ShimmeringLoader className="w-1/2" />
</div>
)
}
return <McpCursorContentInner projectRef={projectRef} projectKeys={projectKeys} state={state} />
}
function McpCursorContentInner({
projectRef,
projectKeys,
state,
}: {
projectRef: string
projectKeys: StepContentProps['projectKeys']
state: StepContentProps['state']
}) {
const track = useTrack()
const selectedClient = useMemo(() => {
const clientKey = String(state.mcpClient ?? '')
return MCP_CLIENTS.find((c) => c.key === clientKey) ?? MCP_CLIENTS[0]
}, [state.mcpClient])
const readonly = Boolean(state.mcpReadonly)
const selectedFeatures = Array.isArray(state.mcpFeatures) ? state.mcpFeatures : []
const selectedFeaturesSupported = useMemo(() => {
const supportedFeatures = IS_PLATFORM ? FEATURE_GROUPS_PLATFORM : FEATURE_GROUPS_NON_PLATFORM
return selectedFeatures.filter((feature) =>
supportedFeatures.some((group) => group.id === feature)
)
}, [selectedFeatures])
const handleCopy = useMemo(
() =>
createMcpCopyHandler({
selectedClient,
source: 'studio',
onTrack: (event) => track(event.action, event.properties, event.groups),
projectRef,
}),
[selectedClient, track, projectRef]
)
const { clientConfig } = getMcpUrl({
projectRef,
isPlatform: IS_PLATFORM,
apiUrl: projectKeys.apiUrl ?? undefined,
readonly,
features: selectedFeaturesSupported,
selectedClient,
})
return (
<McpConfigurationDisplay
className="space-y-4"
theme="dark"
basePath={BASE_PATH}
selectedClient={selectedClient}
clientConfig={clientConfig}
onCopyCallback={handleCopy}
/>
)
}
export default McpCursorContent
@@ -1,36 +0,0 @@
import { CodeBlock } from 'ui'
import type { StepContentProps } from '../../../Connect.types'
const ORM_INSTALL_COMMANDS: Record<string, string[]> = {
prisma: ['npm install prisma --save-dev', 'npx prisma init'],
drizzle: ['npm install drizzle-orm', 'npm install drizzle-kit --save-dev'],
}
function OrmInstallContent({ state }: StepContentProps) {
const ormKey = String(state.orm ?? '')
const commands = ORM_INSTALL_COMMANDS[ormKey]
if (!commands?.length) {
return null
}
return (
<div className="flex flex-col gap-2">
{commands.map((cmd, index) => (
<CodeBlock
key={index}
className="[&_code]:text-foreground"
wrapperClassName="lg:col-span-2"
value={cmd}
hideLineNumbers
language="bash"
>
{cmd}
</CodeBlock>
))}
</div>
)
}
export default OrmInstallContent
@@ -30,10 +30,11 @@ describe('useConnectState', () => {
test('should accept initial state override', () => {
const { result } = renderHook(() =>
useConnectState({ mode: 'direct', connectionMethod: 'transaction' })
useConnectState({ framework: 'react', frameworkVariant: 'vite' })
)
expect(result.current.state.mode).toBe('direct')
expect(result.current.state.connectionMethod).toBe('transaction')
expect(result.current.state.mode).toBe('framework')
expect(result.current.state.framework).toBe('react')
expect(result.current.state.frameworkVariant).toBe('vite')
})
test('should merge initial state with defaults', () => {
@@ -48,50 +49,7 @@ describe('useConnectState', () => {
// ============================================================================
describe('setMode', () => {
test('should switch to direct mode', () => {
const { result } = renderHook(() => useConnectState())
act(() => {
result.current.setMode('direct')
})
expect(result.current.state.mode).toBe('direct')
})
test('should initialize direct mode defaults when switching', () => {
const { result } = renderHook(() => useConnectState())
act(() => {
result.current.setMode('direct')
})
expect(result.current.state.connectionMethod).toBeDefined()
expect(result.current.state.connectionType).toBeDefined()
})
test('should switch to orm mode and initialize defaults', () => {
const { result } = renderHook(() => useConnectState())
act(() => {
result.current.setMode('orm')
})
expect(result.current.state.mode).toBe('orm')
expect(result.current.state.orm).toBe('prisma')
})
test('should switch to mcp mode and initialize defaults', () => {
const { result } = renderHook(() => useConnectState())
act(() => {
result.current.setMode('mcp')
})
expect(result.current.state.mode).toBe('mcp')
expect(result.current.state.mcpClient).toBeDefined()
})
test('should preserve framework state when switching back to framework mode', () => {
test('should preserve framework state when setting framework mode', () => {
const { result } = renderHook(() => useConnectState())
// Change framework
@@ -99,12 +57,7 @@ describe('useConnectState', () => {
result.current.updateField('framework', 'react')
})
// Switch to direct
act(() => {
result.current.setMode('direct')
})
// Switch back to framework
// Set framework mode again
act(() => {
result.current.setMode('framework')
})
@@ -167,44 +120,6 @@ describe('useConnectState', () => {
expect(result.current.state.library).toBe('supabasejs')
})
test('should update connection method', () => {
const { result } = renderHook(() => useConnectState({ mode: 'direct' }))
act(() => {
result.current.updateField('connectionMethod', 'transaction')
})
expect(result.current.state.connectionMethod).toBe('transaction')
})
test('should clear useSharedPooler when connectionMethod changes to direct', () => {
const { result } = renderHook(() =>
useConnectState({
mode: 'direct',
connectionMethod: 'transaction',
useSharedPooler: true,
})
)
act(() => {
result.current.updateField('connectionMethod', 'direct')
})
// useSharedPooler is cleared because it depends on connectionMethod: ['transaction']
// When the dependency is not satisfied, the field is removed from state
expect(result.current.state.useSharedPooler).toBeUndefined()
})
test('should update MCP client', () => {
const { result } = renderHook(() => useConnectState({ mode: 'mcp' }))
act(() => {
result.current.updateField('mcpClient', 'codex')
})
expect(result.current.state.mcpClient).toBe('codex')
})
test('should update boolean fields', () => {
const { result } = renderHook(() => useConnectState())
@@ -214,16 +129,6 @@ describe('useConnectState', () => {
expect(result.current.state.frameworkUi).toBe(true)
})
test('should update ORM selection', () => {
const { result } = renderHook(() => useConnectState({ mode: 'orm' }))
act(() => {
result.current.updateField('orm', 'drizzle')
})
expect(result.current.state.orm).toBe('drizzle')
})
})
// ============================================================================
@@ -258,47 +163,6 @@ describe('useConnectState', () => {
const fieldIds = result.current.activeFields.map((f) => f.id)
expect(fieldIds).not.toContain('frameworkUi')
})
test('should return direct mode fields', () => {
const { result } = renderHook(() => useConnectState({ mode: 'direct' }))
const fieldIds = result.current.activeFields.map((f) => f.id)
expect(fieldIds).toContain('connectionMethod')
expect(fieldIds).toContain('connectionType')
})
test('should show useSharedPooler only for transaction connection method', () => {
const { result } = renderHook(() =>
useConnectState({ mode: 'direct', connectionMethod: 'transaction' })
)
const fieldIds = result.current.activeFields.map((f) => f.id)
expect(fieldIds).toContain('useSharedPooler')
})
test('should hide useSharedPooler for direct connection method', () => {
const { result } = renderHook(() =>
useConnectState({ mode: 'direct', connectionMethod: 'direct' })
)
const fieldIds = result.current.activeFields.map((f) => f.id)
expect(fieldIds).not.toContain('useSharedPooler')
})
test('should return orm mode fields', () => {
const { result } = renderHook(() => useConnectState({ mode: 'orm' }))
const fieldIds = result.current.activeFields.map((f) => f.id)
expect(fieldIds).toContain('orm')
})
test('should return mcp mode fields', () => {
const { result } = renderHook(() => useConnectState({ mode: 'mcp' }))
const fieldIds = result.current.activeFields.map((f) => f.id)
expect(fieldIds).toContain('mcpClient')
expect(fieldIds).toContain('mcpReadonly')
})
})
// ============================================================================
@@ -319,28 +183,6 @@ describe('useConnectState', () => {
expect(stepIds).toContain('install')
})
test('should resolve different steps for mcp mode', () => {
const { result } = renderHook(() => useConnectState({ mode: 'mcp' }))
const stepIds = result.current.resolvedSteps.map((s) => s.id)
// MCP mode should have configure step
expect(stepIds.some((id) => id.includes('configure') || id.includes('mcp'))).toBe(true)
})
test('should resolve different steps for different mcp clients', () => {
const { result: cursorResult } = renderHook(() =>
useConnectState({ mode: 'mcp', mcpClient: 'cursor' })
)
const { result: codexResult } = renderHook(() =>
useConnectState({ mode: 'mcp', mcpClient: 'codex' })
)
// Codex has more steps than cursor
expect(codexResult.current.resolvedSteps.length).toBeGreaterThanOrEqual(
cursorResult.current.resolvedSteps.length
)
})
test('should include skills install step', () => {
const { result } = renderHook(() => useConnectState())
@@ -348,21 +190,6 @@ describe('useConnectState', () => {
expect(stepIds).toContain('install-skills')
})
test('should resolve steps for direct mode', () => {
const { result } = renderHook(() => useConnectState({ mode: 'direct' }))
expect(result.current.resolvedSteps.length).toBeGreaterThan(0)
})
test('should resolve steps for orm mode', () => {
const { result } = renderHook(() => useConnectState({ mode: 'orm' }))
expect(result.current.resolvedSteps.length).toBeGreaterThan(0)
const stepIds = result.current.resolvedSteps.map((s) => s.id)
expect(stepIds).toContain('install')
expect(stepIds).toContain('configure')
})
test('should resolve shadcn steps when frameworkUi is true', () => {
const { result } = renderHook(() =>
useConnectState({ framework: 'nextjs', frameworkUi: true })
@@ -403,41 +230,6 @@ describe('useConnectState', () => {
expect(options).toEqual([])
})
test('should return connection method options', () => {
const { result } = renderHook(() => useConnectState({ mode: 'direct' }))
const options = result.current.getFieldOptions('connectionMethod')
expect(options.length).toBeGreaterThan(0)
expect(options.some((o) => o.value === 'direct')).toBe(true)
expect(options.some((o) => o.value === 'transaction')).toBe(true)
})
test('should return connection type options', () => {
const { result } = renderHook(() => useConnectState({ mode: 'direct' }))
const options = result.current.getFieldOptions('connectionType')
expect(options.length).toBeGreaterThan(0)
expect(options.some((o) => o.value === 'uri')).toBe(true)
expect(options.some((o) => o.value === 'psql')).toBe(true)
})
test('should return ORM options', () => {
const { result } = renderHook(() => useConnectState({ mode: 'orm' }))
const options = result.current.getFieldOptions('orm')
expect(options.length).toBeGreaterThan(0)
expect(options.some((o) => o.value === 'prisma')).toBe(true)
expect(options.some((o) => o.value === 'drizzle')).toBe(true)
})
test('should return MCP client options', () => {
const { result } = renderHook(() => useConnectState({ mode: 'mcp' }))
const options = result.current.getFieldOptions('mcpClient')
expect(options.length).toBeGreaterThan(0)
expect(options.some((o) => o.value === 'cursor')).toBe(true)
})
test('should return empty array for unknown field', () => {
const { result } = renderHook(() => useConnectState())
@@ -473,10 +265,7 @@ describe('useConnectState', () => {
const { result } = renderHook(() => useConnectState())
const modeIds = result.current.schema.modes.map((m) => m.id)
expect(modeIds).toContain('framework')
expect(modeIds).toContain('direct')
expect(modeIds).toContain('orm')
expect(modeIds).toContain('mcp')
expect(modeIds).toEqual(['framework'])
})
})
})