database_replicator/serendb/
picker.rs

1// ABOUTME: Interactive terminal UI for selecting SerenDB projects and databases
2// ABOUTME: Uses dialoguer for consistent UX with existing interactive flows
3
4use crate::serendb::{Branch, ConsoleClient, Project};
5use anyhow::{Context, Result};
6use dialoguer::{theme::ColorfulTheme, Select};
7
8/// Result of the interactive project/database selection
9#[derive(Debug, Clone)]
10pub struct TargetSelection {
11    pub project: Project,
12    pub branch: Branch,
13    pub databases: Vec<String>,
14}
15
16/// Run interactive SerenDB target selection.
17/// Returns the selected project, branch, and database names to mirror the source.
18pub async fn select_target(
19    client: &ConsoleClient,
20    source_databases: &[String],
21) -> Result<TargetSelection> {
22    println!("\n==================================================");
23    println!("SerenDB Target Selection");
24    println!("==================================================\n");
25
26    let projects = client.list_projects().await?;
27
28    if projects.is_empty() {
29        anyhow::bail!(
30            "No SerenDB projects found for this API key.\n\
31             Create a project at: https://console.serendb.com"
32        );
33    }
34
35    let project_labels: Vec<String> = projects
36        .iter()
37        .map(|p| {
38            let short_id: String = p.id.chars().take(8).collect();
39            format!("{} ({})", p.name, short_id)
40        })
41        .collect();
42
43    let project_idx = Select::with_theme(&ColorfulTheme::default())
44        .with_prompt("Select target project")
45        .items(&project_labels)
46        .default(0)
47        .interact()
48        .context("Project selection cancelled")?;
49
50    let project = projects[project_idx].clone();
51    println!("  Selected project: {}\n", project.name);
52
53    let branch = client.get_default_branch(&project.id).await?;
54    println!("  Using branch: {}\n", branch.name);
55
56    let existing = client.list_databases(&project.id, &branch.id).await?;
57    let existing_names: Vec<String> = existing.iter().map(|d| d.name.clone()).collect();
58
59    println!("Source databases to replicate: {:?}", source_databases);
60    println!("Existing target databases: {:?}\n", existing_names);
61
62    let mut target_databases = Vec::new();
63    for source_db in source_databases {
64        if existing_names.contains(source_db) {
65            println!("  \u{2713} {}", source_db);
66        } else {
67            println!("  + {} (will be created)", source_db);
68        }
69        target_databases.push(source_db.clone());
70    }
71
72    println!();
73
74    Ok(TargetSelection {
75        project,
76        branch,
77        databases: target_databases,
78    })
79}
80
81/// Ensure target branch contains all databases required for replication.
82pub async fn create_missing_databases(
83    client: &ConsoleClient,
84    project_id: &str,
85    branch_id: &str,
86    databases: &[String],
87) -> Result<()> {
88    let existing = client.list_databases(project_id, branch_id).await?;
89    let existing_names: Vec<String> = existing.iter().map(|d| d.name.clone()).collect();
90
91    for db_name in databases {
92        if !existing_names.contains(db_name) {
93            println!("  Creating database '{}'...", db_name);
94            client
95                .create_database(project_id, branch_id, db_name)
96                .await?;
97            println!("  \u{2713} Created '{}'", db_name);
98        }
99    }
100
101    Ok(())
102}
103
104#[cfg(test)]
105mod tests {
106    // Interactive picker relies on network + terminal input, so unit tests are not practical here.
107}