Files
Joshen Lim bf54231c82 Reinstate source param in new connect sheet to select read replicas (#44018)
## Context

Adds a "source" param in the new Connect sheet to allow selecting read
replicas - this was present in the old Connect dialog

<img width="962" height="713" alt="image"
src="https://github.com/user-attachments/assets/3c805e41-e8df-431b-83d0-e5f227d68475"
/>

Selecting sources will update the connect steps at the bottom -
connection string, host etc
2026-03-23 13:22:34 +08:00

255 lines
7.5 KiB
TypeScript

import type {
ConditionalValue,
ConnectSchema,
ConnectState,
FieldOption,
ResolvedField,
ResolvedStep,
StepDefinition,
StepFieldValueMap,
StepTree,
} from './Connect.types'
/**
* The order in which state keys are checked during conditional value resolution.
* Used for ConditionalValue (value-keyed) resolution, not for step trees.
*/
const STATE_KEY_ORDER = [
'mode',
'framework',
'frameworkVariant',
'library',
'frameworkUi',
'orm',
'connectionMethod',
'connectionType',
'mcpClient',
] as const
/**
* Check if a value is a conditional object (has nested state keys or DEFAULT)
*/
function isConditionalObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
/**
* Resolves a conditional value based on current state.
* Walks the tree using stateKeys in order, falling back to DEFAULT at each level.
*
* Example: Given state { mode: 'mcp', mcpClient: 'codex' }
* and stateKeys ['mode', 'framework', ..., 'mcpClient']
*
* 1. Look up 'mcp' (state.mode value) in tree -> found, continue
* 2. At mcp subtree { codex: [...], DEFAULT: [...] }, skip irrelevant keys
* until we find a key whose state value matches an entry in the object
* 3. Look up 'codex' (state.mcpClient value) in that subtree -> found, return value
* 4. If no state key matches, try DEFAULT at that level
*/
export function resolveConditional<T>(
value: ConditionalValue<T>,
state: ConnectState,
stateKeys: readonly string[] = STATE_KEY_ORDER
): T | undefined {
// Base case: we've reached a leaf value (string, array, null, boolean, etc.)
if (!isConditionalObject(value)) {
return value as T
}
const conditionalObj = value as Record<string, ConditionalValue<T>>
const objectKeys = Object.keys(conditionalObj).filter((k) => k !== 'DEFAULT')
// Try each state key in order to find one that matches an entry in the object
for (let i = 0; i < stateKeys.length; i++) {
const currentKey = stateKeys[i]
const stateValue = String(state[currentKey] ?? '')
// If this state value matches a key in the conditional object, use it
if (stateValue && objectKeys.includes(stateValue)) {
const nextValue = conditionalObj[stateValue]
// Continue resolving with remaining keys (after this one)
return resolveConditional(nextValue, state, stateKeys.slice(i + 1))
}
}
// No state key matched - use DEFAULT if available
if (conditionalObj.DEFAULT !== undefined) {
return resolveConditional(conditionalObj.DEFAULT, state, stateKeys)
}
return undefined
}
/**
* Resolves the steps array based on current state.
* Returns only steps that have non-null content.
*/
export function resolveSteps(schema: ConnectSchema, state: ConnectState): ResolvedStep[] {
const steps = resolveStepTree(schema.steps, state)
if (steps.length === 0) return []
return steps
.map((step) => {
const content = resolveConditional<string | null>(step.content, state)
return {
id: step.id,
title: step.title,
description: step.description,
content: content ?? '',
}
})
.filter((step) => step.content !== '' && step.content !== null)
}
/**
* Resolves a step tree by evaluating field-specific branches in insertion order.
* Each matching branch appends its steps to the final list.
*/
function resolveStepTree(tree: StepTree, state: ConnectState): StepDefinition[] {
if (Array.isArray(tree)) return tree
if (!isConditionalObject(tree)) return []
const resolved: StepDefinition[] = []
for (const [fieldId, valueMap] of Object.entries(tree)) {
if (fieldId === 'DEFAULT') continue
if (!isConditionalObject(valueMap)) continue
const branch = resolveStepBranch(valueMap as StepFieldValueMap, state[fieldId])
if (!branch) continue
resolved.push(...resolveStepTree(branch, state))
}
return resolved
}
function resolveStepBranch(
valueMap: StepFieldValueMap,
stateValue: ConnectState[keyof ConnectState] | undefined
): StepTree | undefined {
const key = String(stateValue ?? '')
if (key && Object.prototype.hasOwnProperty.call(valueMap, key)) {
return valueMap[key]
}
if (valueMap.DEFAULT !== undefined) {
return valueMap.DEFAULT
}
return undefined
}
/**
* Gets the active fields for the current mode, filtering by dependsOn conditions.
*/
export function getActiveFields(schema: ConnectSchema, state: ConnectState): ResolvedField[] {
const currentMode = schema.modes.find((m) => m.id === state.mode)
if (!currentMode) return []
return currentMode.fields
.map((fieldId) => schema.fields[fieldId])
.filter((field): field is NonNullable<typeof field> => !!field)
.filter((field) => {
// Check dependsOn conditions
if (!field.dependsOn) return true
return Object.entries(field.dependsOn).every(([key, values]) => {
const stateValue = String(state[key] ?? '')
return values.includes(stateValue)
})
})
.map((field) => ({
...field,
resolvedOptions: resolveFieldOptions(field, state),
}))
}
/**
* Resolves field options based on current state.
*/
function resolveFieldOptions(field: { options?: unknown }, state: ConnectState): FieldOption[] {
if (!field.options) return []
// Static options array
if (Array.isArray(field.options)) {
return field.options
}
// Reference to data source (handled elsewhere)
if (
typeof field.options === 'object' &&
'source' in field.options &&
typeof field.options.source === 'string'
) {
// This will be resolved by the component using getFieldOptionsFromSource
return []
}
// Conditional options
const resolved = resolveConditional<FieldOption[]>(
field.options as ConditionalValue<FieldOption[]>,
state
)
return resolved ?? []
}
/**
* Gets default state for the schema, using first mode and default field values.
*/
export function getDefaultState({ schema }: { schema: ConnectSchema }): ConnectState {
const defaultMode = schema.modes[0]?.id ?? 'direct'
const state: ConnectState = { mode: defaultMode }
// Set default values for all fields
Object.values(schema.fields).forEach((field) => {
if (field.defaultValue !== undefined) {
state[field.id] = field.defaultValue
}
})
return state
}
/**
* Resets dependent fields when a parent field changes.
* For example, changing framework should reset frameworkVariant.
*/
export function resetDependentFields(
state: ConnectState,
changedFieldId: string,
schema: ConnectSchema
): ConnectState {
const newState = { ...state }
// Find fields that depend on the changed field
Object.values(schema.fields).forEach((field) => {
if (field.dependsOn && changedFieldId in field.dependsOn) {
// Only reset if dependency conditions are no longer satisfied
const dependencySatisfied = Object.entries(field.dependsOn).every(([key, values]) => {
const stateValue = String(newState[key] ?? '')
return values.includes(stateValue)
})
if (!dependencySatisfied) {
delete newState[field.id]
}
}
})
// Special case: changing mode resets all mode-specific fields
if (changedFieldId === 'mode') {
const previousMode = schema.modes.find((m) => m.id !== state.mode)
const currentMode = schema.modes.find((m) => m.id === state.mode)
// Reset fields from previous mode that aren't in current mode
previousMode?.fields.forEach((fieldId) => {
if (!currentMode?.fields.includes(fieldId)) {
delete newState[fieldId]
}
})
}
return newState
}