Skip to content

Commit 150ef55

Browse files
committed
Add connection creation command
1 parent bc1a0fe commit 150ef55

3 files changed

Lines changed: 167 additions & 9 deletions

File tree

src/command.rs

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -329,10 +329,38 @@ pub enum ConnectionsCommands {
329329
format: String,
330330
},
331331

332-
/// Create a new connection
332+
/// Create a new connection, or list/inspect available connection types
333333
Create {
334334
#[command(subcommand)]
335-
command: ConnectionsCreateCommands,
335+
command: Option<ConnectionsCreateCommands>,
336+
337+
/// Workspace ID (defaults to first workspace from login)
338+
#[arg(long)]
339+
workspace_id: Option<String>,
340+
341+
/// Connection name
342+
#[arg(long)]
343+
name: Option<String>,
344+
345+
/// Connection source type (e.g. postgres, mysql, snowflake)
346+
#[arg(long = "type")]
347+
source_type: Option<String>,
348+
349+
/// Connection config as a JSON object
350+
#[arg(long)]
351+
config: Option<String>,
352+
353+
/// Reference to a secret by ID for authentication
354+
#[arg(long, conflicts_with = "secret_name")]
355+
secret_id: Option<String>,
356+
357+
/// Reference to a secret by name for authentication
358+
#[arg(long, conflicts_with = "secret_id")]
359+
secret_name: Option<String>,
360+
361+
/// Output format
362+
#[arg(long, default_value = "table", value_parser = ["table", "json", "yaml"])]
363+
format: String,
336364
},
337365

338366
/// Update a connection in a workspace

src/connections.rs

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,113 @@ struct ListResponse {
158158
connections: Vec<Connection>,
159159
}
160160

161+
pub fn create(
162+
workspace_id: &str,
163+
name: &str,
164+
source_type: &str,
165+
config: &str,
166+
secret_id: Option<&str>,
167+
secret_name: Option<&str>,
168+
format: &str,
169+
) {
170+
let profile_config = match crate::config::load("default") {
171+
Ok(c) => c,
172+
Err(e) => {
173+
eprintln!("{e}");
174+
std::process::exit(1);
175+
}
176+
};
177+
178+
let api_key = match &profile_config.api_key {
179+
Some(key) if key != "PLACEHOLDER" => key.clone(),
180+
_ => {
181+
eprintln!("error: not authenticated. Run 'hotdata auth login' to log in.");
182+
std::process::exit(1);
183+
}
184+
};
185+
186+
let config_value: serde_json::Value = match serde_json::from_str(config) {
187+
Ok(v) => v,
188+
Err(e) => {
189+
eprintln!("error: --config must be a valid JSON object: {e}");
190+
std::process::exit(1);
191+
}
192+
};
193+
194+
let mut body = serde_json::json!({
195+
"name": name,
196+
"source_type": source_type,
197+
"config": config_value,
198+
});
199+
if let Some(id) = secret_id {
200+
body["secret_id"] = serde_json::json!(id);
201+
}
202+
if let Some(sn) = secret_name {
203+
body["secret_name"] = serde_json::json!(sn);
204+
}
205+
206+
let url = format!("{}/connections", profile_config.api_url);
207+
let client = reqwest::blocking::Client::new();
208+
209+
let resp = match client
210+
.post(&url)
211+
.header("Authorization", format!("Bearer {api_key}"))
212+
.header("X-Workspace-Id", workspace_id)
213+
.json(&body)
214+
.send()
215+
{
216+
Ok(r) => r,
217+
Err(e) => {
218+
eprintln!("error connecting to API: {e}");
219+
std::process::exit(1);
220+
}
221+
};
222+
223+
if !resp.status().is_success() {
224+
use crossterm::style::Stylize;
225+
eprintln!("{}", crate::util::api_error(resp.text().unwrap_or_default()).red());
226+
std::process::exit(1);
227+
}
228+
229+
#[derive(Deserialize, Serialize)]
230+
struct CreateResponse {
231+
id: String,
232+
name: String,
233+
source_type: String,
234+
tables_discovered: u64,
235+
discovery_status: String,
236+
discovery_error: Option<String>,
237+
}
238+
239+
let result: CreateResponse = match resp.json() {
240+
Ok(v) => v,
241+
Err(e) => {
242+
eprintln!("error parsing response: {e}");
243+
std::process::exit(1);
244+
}
245+
};
246+
247+
match format {
248+
"json" => println!("{}", serde_json::to_string_pretty(&result).unwrap()),
249+
"yaml" => print!("{}", serde_yaml::to_string(&result).unwrap()),
250+
"table" => {
251+
use crossterm::style::Stylize;
252+
println!("{}", "Connection created".green());
253+
println!("id: {}", result.id);
254+
println!("name: {}", result.name);
255+
println!("source_type: {}", result.source_type);
256+
println!("tables_discovered: {}", result.tables_discovered);
257+
let status_colored = match result.discovery_status.as_str() {
258+
"success" => result.discovery_status.green().to_string(),
259+
"failed" => result.discovery_error.as_deref().unwrap_or("failed").red().to_string(),
260+
_ => result.discovery_status.yellow().to_string(),
261+
};
262+
println!("discovery_status: {status_colored}");
263+
}
264+
_ => unreachable!(),
265+
}
266+
}
267+
161268
pub fn list(workspace_id: &str, format: &str) {
162269
let profile_config = match config::load("default") {
163270
Ok(c) => c,

src/main.rs

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -91,14 +91,37 @@ fn main() {
9191
_ => eprintln!("not yet implemented"),
9292
},
9393
Commands::Connections { command } => match command {
94-
ConnectionsCommands::Create { command } => match command {
95-
ConnectionsCreateCommands::List { name, workspace_id, format } => {
96-
let workspace_id = resolve_workspace(workspace_id);
97-
match name.as_deref() {
98-
Some(name) => connections::types_get(&workspace_id, name, &format),
99-
None => connections::types_list(&workspace_id, &format),
94+
ConnectionsCommands::Create { command, workspace_id, name, source_type, config, secret_id, secret_name, format } => {
95+
match command {
96+
Some(ConnectionsCreateCommands::List { name, workspace_id, format }) => {
97+
let workspace_id = resolve_workspace(workspace_id);
98+
match name.as_deref() {
99+
Some(name) => connections::types_get(&workspace_id, name, &format),
100+
None => connections::types_list(&workspace_id, &format),
101+
}
102+
}
103+
None => {
104+
let missing: Vec<&str> = [
105+
name.is_none().then_some("--name"),
106+
source_type.is_none().then_some("--type"),
107+
config.is_none().then_some("--config"),
108+
].into_iter().flatten().collect();
109+
if !missing.is_empty() {
110+
eprintln!("error: missing required arguments: {}", missing.join(", "));
111+
std::process::exit(1);
112+
}
113+
let workspace_id = resolve_workspace(workspace_id);
114+
connections::create(
115+
&workspace_id,
116+
&name.unwrap(),
117+
&source_type.unwrap(),
118+
&config.unwrap(),
119+
secret_id.as_deref(),
120+
secret_name.as_deref(),
121+
&format,
122+
)
100123
}
101-
},
124+
}
102125
},
103126
ConnectionsCommands::List { workspace_id, format } => {
104127
let workspace_id = resolve_workspace(workspace_id);

0 commit comments

Comments
 (0)