| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254 |
- 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
- }
|