11use crate :: api:: ApiClient ;
22use serde:: { Deserialize , Serialize } ;
33
4+ #[ derive( Deserialize , Serialize ) ]
5+ struct HealthResponse {
6+ #[ allow( dead_code) ]
7+ connection_id : String ,
8+ healthy : bool ,
9+ #[ serde( default , skip_serializing_if = "Option::is_none" ) ]
10+ latency_ms : Option < u64 > ,
11+ #[ serde( default , skip_serializing_if = "Option::is_none" ) ]
12+ error : Option < String > ,
13+ }
14+
15+ /// Result of a best-effort health check. Either the endpoint responded with a
16+ /// parseable body, or it did not — in which case we record why and keep going.
17+ enum HealthStatus {
18+ Available ( HealthResponse ) ,
19+ Unavailable ( String ) ,
20+ }
21+
22+ impl HealthStatus {
23+ fn is_confirmed_unhealthy ( & self ) -> bool {
24+ matches ! ( self , HealthStatus :: Available ( h) if !h. healthy)
25+ }
26+ }
27+
28+ impl Serialize for HealthStatus {
29+ fn serialize < S : serde:: Serializer > ( & self , ser : S ) -> Result < S :: Ok , S :: Error > {
30+ match self {
31+ HealthStatus :: Available ( h) => h. serialize ( ser) ,
32+ HealthStatus :: Unavailable ( _) => ser. serialize_none ( ) ,
33+ }
34+ }
35+ }
36+
37+ fn fetch_health ( api : & ApiClient , connection_id : & str , show_spinner : bool ) -> HealthStatus {
38+ let spinner = show_spinner. then ( || crate :: util:: spinner ( "Checking connection health..." ) ) ;
39+ let ( status, body) = api. get_raw ( & format ! ( "/connections/{connection_id}/health" ) ) ;
40+ if let Some ( s) = spinner { s. finish_and_clear ( ) ; }
41+
42+ if !status. is_success ( ) {
43+ return HealthStatus :: Unavailable ( crate :: util:: api_error ( body) ) ;
44+ }
45+ match serde_json:: from_str :: < HealthResponse > ( & body) {
46+ Ok ( h) => HealthStatus :: Available ( h) ,
47+ Err ( e) => HealthStatus :: Unavailable ( format ! ( "parse error: {e}" ) ) ,
48+ }
49+ }
50+
51+ fn format_health ( health : & HealthStatus ) -> String {
52+ use crossterm:: style:: Stylize ;
53+ match health {
54+ HealthStatus :: Available ( h) if h. healthy => match h. latency_ms {
55+ Some ( ms) => format ! ( "{} {}" , "healthy" . green( ) , format!( "({ms}ms)" ) . dark_grey( ) ) ,
56+ None => "healthy" . green ( ) . to_string ( ) ,
57+ } ,
58+ HealthStatus :: Available ( h) => {
59+ let err = h. error . as_deref ( ) . unwrap_or ( "unknown error" ) ;
60+ format ! ( "{} — {}" , "unhealthy" . red( ) , err)
61+ }
62+ HealthStatus :: Unavailable ( err) => {
63+ format ! ( "{} — {}" , "unavailable" . yellow( ) , err)
64+ }
65+ }
66+ }
67+
468#[ derive( Deserialize , Serialize ) ]
569struct ConnectionType {
670 name : String ,
@@ -88,23 +152,60 @@ struct ListResponse {
88152
89153pub fn get ( workspace_id : & str , connection_id : & str , format : & str ) {
90154 let api = ApiClient :: new ( Some ( workspace_id) ) ;
155+ let is_table = format == "table" ;
156+
157+ let spinner = is_table. then ( || crate :: util:: spinner ( "Fetching connection..." ) ) ;
91158 let detail: ConnectionDetail = api. get ( & format ! ( "/connections/{connection_id}" ) ) ;
159+ if let Some ( s) = spinner { s. finish_and_clear ( ) ; }
160+
161+ let health = fetch_health ( & api, connection_id, is_table) ;
92162
93163 match format {
94- "json" => println ! ( "{}" , serde_json:: to_string_pretty( & detail) . unwrap( ) ) ,
95- "yaml" => print ! ( "{}" , serde_yaml:: to_string( & detail) . unwrap( ) ) ,
164+ "json" => {
165+ let combined = serde_json:: json!( {
166+ "id" : detail. id,
167+ "name" : detail. name,
168+ "source_type" : detail. source_type,
169+ "table_count" : detail. table_count,
170+ "synced_table_count" : detail. synced_table_count,
171+ "health" : & health,
172+ } ) ;
173+ println ! ( "{}" , serde_json:: to_string_pretty( & combined) . unwrap( ) ) ;
174+ }
175+ "yaml" => {
176+ let combined = serde_json:: json!( {
177+ "id" : detail. id,
178+ "name" : detail. name,
179+ "source_type" : detail. source_type,
180+ "table_count" : detail. table_count,
181+ "synced_table_count" : detail. synced_table_count,
182+ "health" : & health,
183+ } ) ;
184+ print ! ( "{}" , serde_yaml:: to_string( & combined) . unwrap( ) ) ;
185+ }
96186 "table" => {
97187 use crossterm:: style:: Stylize ;
98188 let label = |l : & str | format ! ( "{:<16}" , l) . dark_grey ( ) . to_string ( ) ;
99189 println ! ( "{}{}" , label( "id:" ) , detail. id. dark_cyan( ) ) ;
100190 println ! ( "{}{}" , label( "name:" ) , detail. name. white( ) ) ;
101191 println ! ( "{}{}" , label( "source_type:" ) , detail. source_type. green( ) ) ;
102192 println ! ( "{}{}" , label( "tables:" ) , format!( "{} synced / {} total" , detail. synced_table_count. to_string( ) . cyan( ) , detail. table_count. to_string( ) . cyan( ) ) ) ;
193+ println ! ( "{}{}" , label( "health:" ) , format_health( & health) ) ;
103194 }
104195 _ => unreachable ! ( ) ,
105196 }
106197}
107198
199+ #[ derive( Deserialize , Serialize ) ]
200+ struct CreateResponse {
201+ id : String ,
202+ name : String ,
203+ source_type : String ,
204+ tables_discovered : u64 ,
205+ discovery_status : String ,
206+ discovery_error : Option < String > ,
207+ }
208+
108209pub fn create (
109210 workspace_id : & str ,
110211 name : & str ,
@@ -127,22 +228,53 @@ pub fn create(
127228 } ) ;
128229
129230 let api = ApiClient :: new ( Some ( workspace_id) ) ;
231+ let is_table = format == "table" ;
232+
233+ let spinner = is_table. then ( || crate :: util:: spinner ( "Creating connection..." ) ) ;
234+ let ( status, resp_body) = api. post_raw ( "/connections" , & body) ;
235+ if let Some ( s) = & spinner { s. finish_and_clear ( ) ; }
130236
131- #[ derive( Deserialize , Serialize ) ]
132- struct CreateResponse {
133- id : String ,
134- name : String ,
135- source_type : String ,
136- tables_discovered : u64 ,
137- discovery_status : String ,
138- discovery_error : Option < String > ,
237+ if !status. is_success ( ) {
238+ use crossterm:: style:: Stylize ;
239+ eprintln ! ( "{}" , crate :: util:: api_error( resp_body) . red( ) ) ;
240+ std:: process:: exit ( 1 ) ;
139241 }
140242
141- let result: CreateResponse = api. post ( "/connections" , & body) ;
243+ let result: CreateResponse = match serde_json:: from_str ( & resp_body) {
244+ Ok ( v) => v,
245+ Err ( e) => {
246+ eprintln ! ( "error parsing response: {e}" ) ;
247+ std:: process:: exit ( 1 ) ;
248+ }
249+ } ;
250+
251+ let health = fetch_health ( & api, & result. id , is_table) ;
142252
143253 match format {
144- "json" => println ! ( "{}" , serde_json:: to_string_pretty( & result) . unwrap( ) ) ,
145- "yaml" => print ! ( "{}" , serde_yaml:: to_string( & result) . unwrap( ) ) ,
254+ "json" => {
255+ let combined = serde_json:: json!( {
256+ "id" : result. id,
257+ "name" : result. name,
258+ "source_type" : result. source_type,
259+ "tables_discovered" : result. tables_discovered,
260+ "discovery_status" : result. discovery_status,
261+ "discovery_error" : result. discovery_error,
262+ "health" : & health,
263+ } ) ;
264+ println ! ( "{}" , serde_json:: to_string_pretty( & combined) . unwrap( ) ) ;
265+ }
266+ "yaml" => {
267+ let combined = serde_json:: json!( {
268+ "id" : result. id,
269+ "name" : result. name,
270+ "source_type" : result. source_type,
271+ "tables_discovered" : result. tables_discovered,
272+ "discovery_status" : result. discovery_status,
273+ "discovery_error" : result. discovery_error,
274+ "health" : & health,
275+ } ) ;
276+ print ! ( "{}" , serde_yaml:: to_string( & combined) . unwrap( ) ) ;
277+ }
146278 "table" => {
147279 use crossterm:: style:: Stylize ;
148280 println ! ( "{}" , "Connection created" . green( ) ) ;
@@ -156,9 +288,14 @@ pub fn create(
156288 _ => result. discovery_status . yellow ( ) . to_string ( ) ,
157289 } ;
158290 println ! ( "discovery_status: {status_colored}" ) ;
291+ println ! ( "health: {}" , format_health( & health) ) ;
159292 }
160293 _ => unreachable ! ( ) ,
161294 }
295+
296+ if health. is_confirmed_unhealthy ( ) {
297+ std:: process:: exit ( 1 ) ;
298+ }
162299}
163300
164301pub fn list ( workspace_id : & str , format : & str ) {
0 commit comments