Skip to main content

spawn_db/commands/
init.rs

1use crate::commands::{Outcome, TelemetryDescribe, TelemetryInfo};
2use crate::config::ConfigLoaderSaver;
3use crate::engine::{CommandSpec, DatabaseConfig, EngineType};
4use anyhow::{anyhow, Result};
5use opendal::Operator;
6use std::collections::HashMap;
7use uuid::Uuid;
8
9/// Init command - special case that doesn't implement Command trait
10/// because it doesn't require a loaded Config (it creates the config).
11pub struct Init {
12    pub config_file: String,
13}
14
15impl TelemetryDescribe for Init {
16    fn telemetry(&self) -> TelemetryInfo {
17        TelemetryInfo::new("init")
18    }
19}
20
21impl Init {
22    /// Execute the init command. Returns (Outcome, project_id).
23    /// Unlike other commands, this takes an Operator directly since Config doesn't exist yet.
24    pub async fn execute(&self, base_op: &Operator) -> Result<(Outcome, String)> {
25        // Check if spawn.toml already exists
26        if base_op.exists(&self.config_file).await? {
27            return Err(anyhow!(
28                "Config file '{}' already exists. Use a different path or remove the existing file.",
29                &self.config_file
30            ));
31        }
32
33        // Generate a new project_id
34        let project_id = Uuid::new_v4().to_string();
35
36        // Create example database config
37        let mut databases = HashMap::new();
38        databases.insert(
39            "postgres_psql".to_string(),
40            DatabaseConfig {
41                engine: EngineType::PostgresPSQL,
42                spawn_database: "spawn".to_string(),
43                spawn_schema: "_spawn".to_string(),
44                environment: "dev".to_string(),
45                command: Some(CommandSpec::Direct {
46                    direct: vec![
47                        "docker".to_string(),
48                        "exec".to_string(),
49                        "-i".to_string(),
50                        "spawn".to_string(),
51                        "psql".to_string(),
52                        "-U".to_string(),
53                        "spawn".to_string(),
54                        "spawn".to_string(),
55                    ],
56                }),
57            },
58        );
59
60        // Create default config
61        let config = ConfigLoaderSaver {
62            spawn_folder: "spawn".to_string(),
63            database: Some("postgres_psql".to_string()),
64            environment: None,
65            databases: Some(databases),
66            project_id: Some(project_id.clone()),
67            telemetry: None,
68        };
69
70        // Save the config
71        config
72            .save(&self.config_file, base_op)
73            .await
74            .map_err(|e| e.context("Failed to write config file"))?;
75
76        // Create the spawn folder structure
77        let spawn_folder = &config.spawn_folder;
78        let subfolders = ["migrations", "components", "tests", "pinned"];
79        let mut created_folders = Vec::new();
80
81        for subfolder in &subfolders {
82            let path = format!("{}/{}/", spawn_folder, subfolder);
83            // Create a .gitkeep file to ensure the folder exists
84            base_op
85                .write(&format!("{}.gitkeep", path), "")
86                .await
87                .map_err(|e| {
88                    anyhow::Error::from(e).context(format!("Failed to create {} folder", subfolder))
89                })?;
90            created_folders.push(format!("  {}/{}/", spawn_folder, subfolder));
91        }
92
93        // Show telemetry notice
94        crate::show_telemetry_notice();
95
96        println!("Initialized spawn project with project_id: {}", project_id);
97        println!("Created directories:");
98        for folder in &created_folders {
99            println!("{}", folder);
100        }
101        println!(
102            "\nEdit {} to configure your database connection.",
103            &self.config_file
104        );
105
106        Ok((Outcome::Success, project_id))
107    }
108}