Skip to main content

spawn_db/commands/
init.rs

1use crate::commands::{Outcome, TelemetryDescribe, TelemetryInfo};
2use crate::config::ConfigLoaderSaver;
3use crate::engine::{CommandSpec, EngineType, TargetConfig};
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    /// Optional docker-compose generation. If Some(None), uses default name "myproject".
14    /// If Some(Some(name)), uses the provided name.
15    pub docker: Option<Option<String>>,
16}
17
18impl TelemetryDescribe for Init {
19    fn telemetry(&self) -> TelemetryInfo {
20        TelemetryInfo::new("init")
21    }
22}
23
24impl Init {
25    /// Execute the init command. Returns (Outcome, project_id).
26    /// Unlike other commands, this takes an Operator directly since Config doesn't exist yet.
27    pub async fn execute(&self, base_op: &Operator) -> Result<(Outcome, String)> {
28        // Check if spawn.toml already exists
29        if base_op.exists(&self.config_file).await? {
30            return Err(anyhow!(
31                "Config file '{}' already exists. Use a different path or remove the existing file.",
32                &self.config_file
33            ));
34        }
35
36        // Generate a new project_id
37        let project_id = Uuid::new_v4().to_string();
38
39        // Determine database/project name for docker setup
40        let db_name = match &self.docker {
41            Some(Some(name)) => name.clone(),
42            Some(None) => "postgres".to_string(),
43            None => "postgres".to_string(),
44        };
45
46        // Determine container name
47        let container_name = if self.docker.is_some() {
48            format!("{}-db", db_name)
49        } else {
50            "postgres-db".to_string()
51        };
52
53        // Create example target config
54        let mut targets = HashMap::new();
55        targets.insert(
56            "postgres_psql".to_string(),
57            TargetConfig {
58                engine: EngineType::PostgresPSQL,
59                spawn_database: Some(db_name.clone()),
60                spawn_schema: "_spawn".to_string(),
61                environment: "dev".to_string(),
62                command: Some(CommandSpec::Direct {
63                    direct: vec![
64                        "docker".to_string(),
65                        "exec".to_string(),
66                        "-i".to_string(),
67                        container_name.clone(),
68                        "psql".to_string(),
69                        "-U".to_string(),
70                        "postgres".to_string(),
71                        db_name.clone(),
72                    ],
73                }),
74            },
75        );
76
77        // Create default config
78        let config = ConfigLoaderSaver {
79            spawn_folder: "spawn".to_string(),
80            target: Some("postgres_psql".to_string()),
81            environment: None,
82            targets: Some(targets),
83            project_id: Some(project_id.clone()),
84            telemetry: None,
85        };
86
87        // Save the config
88        config
89            .save(&self.config_file, base_op)
90            .await
91            .map_err(|e| e.context("Failed to write config file"))?;
92
93        // Create the spawn folder structure
94        let spawn_folder = &config.spawn_folder;
95        let subfolders = ["migrations", "components", "tests", "pinned"];
96        let mut created_folders = Vec::new();
97
98        for subfolder in &subfolders {
99            let path = format!("{}/{}/", spawn_folder, subfolder);
100            // Create a .gitkeep file to ensure the folder exists
101            base_op
102                .write(&format!("{}.gitkeep", path), "")
103                .await
104                .map_err(|e| {
105                    anyhow::Error::from(e).context(format!("Failed to create {} folder", subfolder))
106                })?;
107            created_folders.push(format!("  {}/{}/", spawn_folder, subfolder));
108        }
109
110        // Generate docker-compose.yaml if requested
111        if self.docker.is_some() {
112            let docker_compose_content = format!(
113                r#"services:
114  postgres:
115    image: postgres:17
116    container_name: {}
117    ports:
118      - "5432:5432"
119    restart: always
120    environment:
121      POSTGRES_USER: postgres
122      POSTGRES_PASSWORD: postgres
123      POSTGRES_DB: {}
124"#,
125                container_name, db_name
126            );
127
128            base_op
129                .write("docker-compose.yaml", docker_compose_content)
130                .await
131                .map_err(|e| {
132                    anyhow::Error::from(e).context("Failed to create docker-compose.yaml")
133                })?;
134
135            println!("Created docker-compose.yaml for database '{}'", db_name);
136            println!("Start the database with: docker compose up -d");
137            println!();
138        }
139
140        // Show telemetry notice
141        crate::show_telemetry_notice();
142
143        println!("Initialized spawn project with project_id: {}", project_id);
144        println!("Created directories:");
145        for folder in &created_folders {
146            println!("{}", folder);
147        }
148        println!(
149            "\nEdit {} to configure your database connection.",
150            &self.config_file
151        );
152
153        Ok((Outcome::Success, project_id))
154    }
155}