1+ use std:: io;
2+
13use crate :: common_args;
24use crate :: config:: Config ;
3- use crate :: util:: { add_auth_header_opt, database_identity, get_auth_header} ;
5+ use crate :: util:: { add_auth_header_opt, database_identity, get_auth_header, y_or_n , AuthHeader } ;
46use clap:: { Arg , ArgMatches } ;
7+ use http:: StatusCode ;
8+ use itertools:: Itertools as _;
9+ use reqwest:: Response ;
10+ use spacetimedb_client_api_messages:: http:: { DatabaseDeleteConfirmationResponse , DatabaseTree , DatabaseTreeNode } ;
11+ use spacetimedb_lib:: Hash ;
12+ use tokio:: io:: AsyncWriteExt as _;
513
614pub fn cli ( ) -> clap:: Command {
715 clap:: Command :: new ( "delete" )
@@ -22,11 +30,143 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
2230 let force = args. get_flag ( "force" ) ;
2331
2432 let identity = database_identity ( & config, database, server) . await ?;
25-
26- let builder = reqwest :: Client :: new ( ) . delete ( format ! ( "{}/v1/database/{}" , config . get_host_url ( server ) ? , identity ) ) ;
33+ let host_url = config . get_host_url ( server ) ? ;
34+ let request_path = format ! ( "{host_url }/v1/database/{identity}" ) ;
2735 let auth_header = get_auth_header ( & mut config, false , server, !force) . await ?;
28- let builder = add_auth_header_opt ( builder, & auth_header) ;
29- builder. send ( ) . await ?. error_for_status ( ) ?;
36+ let client = reqwest:: Client :: new ( ) ;
37+
38+ let response = send_request ( & client, & request_path, & auth_header, None ) . await ?;
39+ match response. status ( ) {
40+ StatusCode :: PRECONDITION_REQUIRED => {
41+ let confirm = response. json :: < DatabaseDeleteConfirmationResponse > ( ) . await ?;
42+ println ! ( "WARNING: Deleting the database {identity} will also delete its children!" ) ;
43+ if !force {
44+ print_database_tree_info ( & confirm. database_tree ) . await ?;
45+ }
46+ if y_or_n ( force, "Do you want to proceed deleting above databases?" ) ? {
47+ send_request ( & client, & request_path, & auth_header, Some ( confirm. confirmation_token ) )
48+ . await ?
49+ . error_for_status ( ) ?;
50+ } else {
51+ println ! ( "Aborting" ) ;
52+ }
53+
54+ Ok ( ( ) )
55+ }
56+ StatusCode :: OK => Ok ( ( ) ) ,
57+ _ => response. error_for_status ( ) . map ( drop) . map_err ( Into :: into) ,
58+ }
59+ }
60+
61+ async fn send_request (
62+ client : & reqwest:: Client ,
63+ request_path : & str ,
64+ auth : & AuthHeader ,
65+ confirmation_token : Option < Hash > ,
66+ ) -> Result < Response , reqwest:: Error > {
67+ let mut builder = client. delete ( request_path) ;
68+ builder = add_auth_header_opt ( builder, auth) ;
69+ if let Some ( token) = confirmation_token {
70+ builder = builder. query ( & [ ( "token" , token) ] ) ;
71+ }
72+ builder. send ( ) . await
73+ }
74+
75+ async fn print_database_tree_info ( tree : & DatabaseTree ) -> io:: Result < ( ) > {
76+ tokio:: io:: stdout ( )
77+ . write_all ( as_termtree ( tree) . to_string ( ) . as_bytes ( ) )
78+ . await
79+ }
80+
81+ fn as_termtree ( tree : & DatabaseTree ) -> termtree:: Tree < String > {
82+ let mut stack: Vec < ( & DatabaseTree , bool ) > = vec ! [ ] ;
83+ stack. push ( ( tree, false ) ) ;
84+
85+ let mut built: Vec < termtree:: Tree < String > > = <_ >:: default ( ) ;
86+
87+ while let Some ( ( node, visited) ) = stack. pop ( ) {
88+ if visited {
89+ let mut term_node = termtree:: Tree :: new ( fmt_tree_node ( & node. root ) ) ;
90+ term_node. leaves = built. drain ( built. len ( ) - node. children . len ( ) ..) . collect ( ) ;
91+ term_node. leaves . reverse ( ) ;
92+ built. push ( term_node) ;
93+ } else {
94+ stack. push ( ( node, true ) ) ;
95+ stack. extend ( node. children . iter ( ) . rev ( ) . map ( |child| ( child, false ) ) ) ;
96+ }
97+ }
98+
99+ built
100+ . pop ( )
101+ . expect ( "database tree contains a root and we pushed it last" )
102+ }
103+
104+ fn fmt_tree_node ( node : & DatabaseTreeNode ) -> String {
105+ format ! (
106+ "{}{}" ,
107+ node. database_identity,
108+ if node. database_names. is_empty( ) {
109+ <_>:: default ( )
110+ } else {
111+ format!( ": {}" , node. database_names. iter( ) . join( ", " ) )
112+ }
113+ )
114+ }
115+
116+ #[ cfg( test) ]
117+ mod tests {
118+ use super :: * ;
119+ use spacetimedb_client_api_messages:: http:: { DatabaseTree , DatabaseTreeNode } ;
120+ use spacetimedb_lib:: { sats:: u256, Identity } ;
30121
31- Ok ( ( ) )
122+ #[ test]
123+ fn render_termtree ( ) {
124+ let tree = DatabaseTree {
125+ root : DatabaseTreeNode {
126+ database_identity : Identity :: ONE ,
127+ database_names : [ "parent" . into ( ) ] . into ( ) ,
128+ } ,
129+ children : vec ! [
130+ DatabaseTree {
131+ root: DatabaseTreeNode {
132+ database_identity: Identity :: from_u256( u256:: new( 2 ) ) ,
133+ database_names: [ "child" . into( ) ] . into( ) ,
134+ } ,
135+ children: vec![
136+ DatabaseTree {
137+ root: DatabaseTreeNode {
138+ database_identity: Identity :: from_u256( u256:: new( 3 ) ) ,
139+ database_names: [ "grandchild" . into( ) ] . into( ) ,
140+ } ,
141+ children: vec![ ] ,
142+ } ,
143+ DatabaseTree {
144+ root: DatabaseTreeNode {
145+ database_identity: Identity :: from_u256( u256:: new( 5 ) ) ,
146+ database_names: [ ] . into( ) ,
147+ } ,
148+ children: vec![ ] ,
149+ } ,
150+ ] ,
151+ } ,
152+ DatabaseTree {
153+ root: DatabaseTreeNode {
154+ database_identity: Identity :: from_u256( u256:: new( 4 ) ) ,
155+ database_names: [ "sibling" . into( ) , "bro" . into( ) ] . into( ) ,
156+ } ,
157+ children: vec![ ] ,
158+ } ,
159+ ] ,
160+ } ;
161+ pretty_assertions:: assert_eq!(
162+ "\
163+ 0000000000000000000000000000000000000000000000000000000000000001: parent
164+ ├── 0000000000000000000000000000000000000000000000000000000000000004: bro, sibling
165+ └── 0000000000000000000000000000000000000000000000000000000000000002: child
166+ ├── 0000000000000000000000000000000000000000000000000000000000000005
167+ └── 0000000000000000000000000000000000000000000000000000000000000003: grandchild
168+ " ,
169+ & as_termtree( & tree) . to_string( )
170+ ) ;
171+ }
32172}
0 commit comments