useConnectState.ts355 lines · main
1import { useParams } from 'common'
2import { useCallback, useMemo, useState } from 'react'
3import { FEATURE_GROUPS_PLATFORM, MCP_CLIENTS } from 'ui-patterns/McpUrlBuilder'
4
5import {
6 connectionStringMethodOptions,
7 DATABASE_CONNECTION_TYPES,
8 FRAMEWORKS,
9 MOBILES,
10 ORMS,
11} from './Connect.constants'
12import {
13 getActiveFields,
14 getDefaultState,
15 resetDependentFields,
16 resolveSteps,
17} from './connect.resolver'
18import { connectSchema } from './connect.schema'
19import type {
20 ConnectMode,
21 ConnectSchema,
22 ConnectState,
23 FieldOption,
24 ResolvedField,
25 ResolvedStep,
26} from './Connect.types'
27import { resolveFrameworkLibraryKey } from './Connect.utils'
28import { Database, useReadReplicasQuery } from '@/data/read-replicas/replicas-query'
29import { formatDatabaseID, formatDatabaseRegion } from '@/data/read-replicas/replicas.utils'
30import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
31import { useIsHighAvailability } from '@/hooks/misc/useSelectedProject'
32
33// ============================================================================
34// Data Source Helpers
35// ============================================================================
36
37/**
38 * Get field options from a data source reference.
39 * This maps source names to actual data.
40 */
41function getFieldOptionsFromSource({
42 source,
43 state,
44 databases,
45}: {
46 source: string
47 state: ConnectState
48 databases: Database[]
49}): FieldOption[] {
50 switch (source) {
51 case 'frameworks':
52 return [...FRAMEWORKS, ...MOBILES].map((f) => ({
53 value: f.key,
54 label: f.label,
55 icon: f.icon,
56 }))
57
58 case 'frameworkVariants': {
59 // Get variants for the selected framework
60 const allFrameworks = [...FRAMEWORKS, ...MOBILES]
61 const selected = allFrameworks.find((f) => f.key === state.framework)
62 if (!selected?.children?.length) return []
63 // Only return if there are multiple children (variants)
64 if (selected.children.length <= 1) return []
65 return selected.children.map((c) => ({
66 value: c.key,
67 label: c.label,
68 icon: c.icon,
69 }))
70 }
71
72 case 'libraries': {
73 // Get libraries for the selected framework and variant
74 const allFrameworks = [...FRAMEWORKS, ...MOBILES]
75 const selectedFramework = allFrameworks.find((f) => f.key === state.framework)
76 if (!selectedFramework) return []
77
78 // If framework has variants, look in the variant
79 if (selectedFramework.children?.length > 1 && state.frameworkVariant) {
80 const variant = selectedFramework.children.find((c) => c.key === state.frameworkVariant)
81 if (variant?.children?.length) {
82 return variant.children.map((c) => ({
83 value: c.key,
84 label: c.label,
85 icon: c.icon,
86 }))
87 }
88 }
89
90 // Otherwise look directly in framework children
91 if (selectedFramework.children?.length === 1) {
92 const child = selectedFramework.children[0]
93 if (child.children?.length) {
94 return child.children.map((c) => ({
95 value: c.key,
96 label: c.label,
97 icon: c.icon,
98 }))
99 }
100 // The child itself is the library
101 return [{ value: child.key, label: child.label, icon: child.icon }]
102 }
103
104 return []
105 }
106
107 case 'connectionMethods':
108 return Object.values(connectionStringMethodOptions).map((m) => ({
109 value: m.value,
110 label: m.label,
111 description: m.description,
112 }))
113
114 case 'connectionSources':
115 return databases.map((db) => {
116 const region = formatDatabaseRegion(db?.region ?? '')
117 const id = formatDatabaseID(db.identifier ?? '')
118 const label = db.identifier.includes('-rr-')
119 ? `Read Replica (${region} - ${id}}`
120 : 'Primary Database'
121 return { value: db.identifier, label }
122 })
123
124 case 'connectionTypes':
125 return DATABASE_CONNECTION_TYPES.map((t) => ({
126 value: t.id,
127 label: t.label,
128 }))
129
130 case 'orms':
131 return ORMS.map((o) => ({
132 value: o.key,
133 label: o.label,
134 icon: o.icon,
135 }))
136
137 case 'mcpClients':
138 return MCP_CLIENTS.map((c) => ({
139 value: c.key,
140 label: c.label,
141 icon: c.icon,
142 }))
143
144 case 'mcpFeatures':
145 return FEATURE_GROUPS_PLATFORM.map((f) => ({
146 value: f.id,
147 label: f.name,
148 description: f.description,
149 }))
150
151 default:
152 return []
153 }
154}
155
156/**
157 * Resolve field options, handling both static options and data source references.
158 */
159function resolveFieldOptionsWithSource({
160 field,
161 state,
162 databases,
163}: {
164 field: ResolvedField
165 state: ConnectState
166 databases: Database[]
167}): FieldOption[] {
168 // If already resolved (from conditional resolution)
169 if (field.resolvedOptions.length > 0) {
170 return field.resolvedOptions
171 }
172
173 // Check if it's a source reference
174 const options = connectSchema.fields[field.id]?.options
175 if (options && typeof options === 'object' && 'source' in options) {
176 return getFieldOptionsFromSource({ source: options.source as string, state, databases })
177 }
178
179 return []
180}
181
182// ============================================================================
183// Hook
184// ============================================================================
185
186export interface UseConnectStateReturn {
187 state: ConnectState
188 updateField: (fieldId: string, value: string | boolean | string[]) => void
189 setMode: (mode: ConnectMode) => void
190 activeFields: ResolvedField[]
191 resolvedSteps: ResolvedStep[]
192 getFieldOptions: (fieldId: string) => FieldOption[]
193 schema: ConnectSchema
194}
195
196export function useConnectState(initialState?: Partial<ConnectState>): UseConnectStateReturn {
197 const { ref: projectRef } = useParams()
198 const { data: databases = [] } = useReadReplicasQuery({ projectRef })
199 const { hasAccess: hasDedicatedPooler } = useCheckEntitlements('dedicated_pooler')
200 const isHighAvailability = useIsHighAvailability()
201
202 const [state, setState] = useState<ConnectState>(() => {
203 const defaults = getDefaultState({ schema: connectSchema })
204
205 // Set initial framework if mode is framework
206 if (defaults.mode === 'framework' && !defaults.framework) {
207 const firstFramework = FRAMEWORKS[0]
208 defaults.framework = firstFramework?.key ?? ''
209
210 // Set initial variant if framework has variants
211 if (firstFramework?.children?.length > 1) {
212 defaults.frameworkVariant = firstFramework.children[0]?.key ?? ''
213 }
214
215 // Set initial library
216 const libraryKey = resolveFrameworkLibraryKey({
217 framework: defaults.framework,
218 frameworkVariant: defaults.frameworkVariant,
219 library: defaults.library,
220 })
221 if (libraryKey) defaults.library = libraryKey
222 }
223
224 // Set initial ORM if mode is orm
225 if (defaults.mode === 'orm' && !defaults.orm) {
226 defaults.orm = ORMS[0]?.key ?? ''
227 }
228
229 // Set initial MCP client if mode is mcp
230 if (defaults.mode === 'mcp' && !defaults.mcpClient) {
231 defaults.mcpClient = MCP_CLIENTS[0]?.key ?? ''
232 }
233
234 return { ...defaults, ...initialState } as ConnectState
235 })
236
237 const updateField = useCallback((fieldId: string, value: string | boolean | string[]) => {
238 setState((prev) => {
239 const next = { ...prev, [fieldId]: value }
240
241 // Handle cascading updates for framework selection
242 if (fieldId === 'framework') {
243 const allFrameworks = [...FRAMEWORKS, ...MOBILES]
244 const selected = allFrameworks.find((f) => f.key === value)
245
246 // Reset variant if framework changed
247 if (selected?.children && selected.children.length > 1) {
248 next.frameworkVariant = selected.children[0]?.key ?? ''
249 } else {
250 delete next.frameworkVariant
251 }
252
253 // Reset library
254 const libraryKey = resolveFrameworkLibraryKey({
255 framework: next.framework,
256 frameworkVariant: next.frameworkVariant,
257 })
258 if (libraryKey) {
259 next.library = libraryKey
260 } else {
261 delete next.library
262 }
263 }
264
265 // Handle cascading updates for variant selection
266 if (fieldId === 'frameworkVariant') {
267 const libraryKey = resolveFrameworkLibraryKey({
268 framework: prev.framework,
269 frameworkVariant: String(value),
270 })
271 if (libraryKey) next.library = libraryKey
272 }
273
274 // Reset useSharedPooler when connectionMethod changes to 'direct'
275 if (fieldId === 'connectionMethod' && value === 'direct') {
276 next.useSharedPooler = false
277 }
278
279 return resetDependentFields(next, fieldId, connectSchema)
280 })
281 }, [])
282
283 const setMode = useCallback(
284 (mode: ConnectMode) => {
285 setState((prev) => {
286 const next: ConnectState = { ...prev, mode }
287
288 // Initialize mode-specific defaults
289 if (mode === 'framework' && !next.framework) {
290 const firstFramework = FRAMEWORKS[0]
291 next.framework = firstFramework?.key ?? ''
292 if (firstFramework?.children?.length > 1) {
293 next.frameworkVariant = firstFramework.children[0]?.key ?? ''
294 }
295 const libraryKey = resolveFrameworkLibraryKey({
296 framework: next.framework,
297 frameworkVariant: next.frameworkVariant,
298 })
299 if (libraryKey) next.library = libraryKey
300 }
301
302 if (mode === 'direct') {
303 next.connectionMethod = next.connectionMethod ?? 'direct'
304 next.connectionType = next.connectionType ?? 'uri'
305 next.connectionSource = projectRef ?? '_'
306 }
307
308 if (mode === 'orm' && !next.orm) {
309 next.orm = ORMS[0]?.key ?? ''
310 }
311
312 if (mode === 'mcp' && !next.mcpClient) {
313 next.mcpClient = MCP_CLIENTS[0]?.key ?? ''
314 }
315
316 return next
317 })
318 },
319 [projectRef]
320 )
321
322 const activeFields = useMemo(() => {
323 let fields = getActiveFields(connectSchema, state)
324 if (!hasDedicatedPooler) {
325 fields = fields.filter((f) => f.id !== 'useSharedPooler')
326 }
327 if (isHighAvailability) {
328 fields = fields
329 .filter((f) => f.id !== 'connectionMethod' && f.id !== 'useSharedPooler')
330 .map((f) => (f.id === 'connectionType' ? { ...f, label: 'Connection Type' } : f))
331 }
332 return fields
333 }, [state, hasDedicatedPooler, isHighAvailability])
334
335 const resolvedSteps = useMemo(() => resolveSteps(connectSchema, state), [state])
336
337 const getFieldOptions = useCallback(
338 (fieldId: string): FieldOption[] => {
339 const field = activeFields.find((f) => f.id === fieldId)
340 if (!field) return []
341 return resolveFieldOptionsWithSource({ field, state, databases })
342 },
343 [activeFields, state, databases]
344 )
345
346 return {
347 state,
348 updateField,
349 setMode,
350 activeFields,
351 resolvedSteps,
352 getFieldOptions,
353 schema: connectSchema,
354 }
355}