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 { 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( value: ConditionalValue, 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> 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(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 => !!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( field.options as ConditionalValue, 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 }