diff --git a/Cargo.lock b/Cargo.lock index 0892c88..3283d32 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -305,9 +305,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.39" +version = "4.5.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f" +checksum = "50fd97c9dc2399518aa331917ac6f274280ec5eb34e555dd291899745c48ec6f" dependencies = [ "clap_builder", "clap_derive", @@ -315,9 +315,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.39" +version = "4.5.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51" +checksum = "c35b5830294e1fa0462034af85cc95225a4cb07092c088c55bda3147cfcd8f65" dependencies = [ "anstream", "anstyle", @@ -328,9 +328,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.32" +version = "4.5.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" dependencies = [ "heck", "proc-macro2", @@ -865,7 +865,7 @@ dependencies = [ "http-body", "hyper", "pin-project-lite", - "socket2", + "socket2 0.5.8", "tokio", "tower-service", "tracing", @@ -1053,6 +1053,17 @@ dependencies = [ "hashbrown 0.15.2", ] +[[package]] +name = "io-uring" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -1095,9 +1106,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.169" +version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "linux-raw-sys" @@ -2054,6 +2065,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "spin" version = "0.9.8" @@ -2223,20 +2244,22 @@ dependencies = [ [[package]] name = "tokio" -version = "1.45.1" +version = "1.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ "backtrace", "bytes", + "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "slab", + "socket2 0.6.0", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2307,7 +2330,7 @@ dependencies = [ "prost", "rustls-native-certs", "rustls-pemfile", - "socket2", + "socket2 0.5.8", "tokio", "tokio-rustls", "tokio-stream", @@ -2456,7 +2479,7 @@ dependencies = [ [[package]] name = "typedb-driver" version = "0.0.0" -source = "git+https://github.com/typedb/typedb-driver?rev=3b171274a42751376b2a480baa40315ebbf80fce#3b171274a42751376b2a480baa40315ebbf80fce" +source = "git+https://github.com/typedb/typedb-driver?rev=d2ce9ab4f85f8316a9087c130a20c7b1e731e8c4#d2ce9ab4f85f8316a9087c130a20c7b1e731e8c4" dependencies = [ "chrono", "chrono-tz", @@ -2478,7 +2501,7 @@ dependencies = [ [[package]] name = "typedb-protocol" version = "0.0.0" -source = "git+https://github.com/typedb/typedb-protocol?rev=38f66a1cc4db3b7a301676f50800e9530ac5c8a3#38f66a1cc4db3b7a301676f50800e9530ac5c8a3" +source = "git+https://github.com/typedb/typedb-protocol?rev=bac73842bb0f6e4a93ae42d3421f474c75f9be1b#bac73842bb0f6e4a93ae42d3421f474c75f9be1b" dependencies = [ "prost", "tonic", @@ -2603,13 +2626,15 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.16.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" +checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" dependencies = [ "getrandom 0.3.1", + "js-sys", "rand 0.9.0", "serde", + "wasm-bindgen", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 8c5bf21..bf64cf8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ features = {} [dependencies.tokio] features = ["bytes", "default", "fs", "full", "io-std", "io-util", "libc", "macros", "mio", "net", "parking_lot", "process", "rt", "rt-multi-thread", "signal", "signal-hook-registry", "socket2", "sync", "time", "tokio-macros"] - version = "1.45.0" + version = "1.46.0" default-features = false [dependencies.rustyline] @@ -30,7 +30,7 @@ features = {} [dependencies.clap] features = ["color", "default", "derive", "error-context", "help", "std", "suggestions", "usage", "wrap_help"] - version = "4.5.38" + version = "4.5.40" default-features = false [dependencies.glob] @@ -46,8 +46,8 @@ features = {} [dependencies.typedb-driver] features = [] + rev = "d2ce9ab4f85f8316a9087c130a20c7b1e731e8c4" git = "https://github.com/typedb/typedb-driver" - tag = "3.4.0" default-features = false [dependencies.futures] diff --git a/RELEASE_NOTES_LATEST.md b/RELEASE_NOTES_LATEST.md index 7827908..a625a7b 100644 --- a/RELEASE_NOTES_LATEST.md +++ b/RELEASE_NOTES_LATEST.md @@ -1,30 +1,9 @@ ## Distribution -Download from TypeDB Package Repository: https://cloudsmith.io/~typedb/repos/public-release/packages/?q=name:^typedb-console+version:3.4.0 +Download from TypeDB Package Repository: https://cloudsmith.io/~typedb/repos/public-release/packages/?q=name:^typedb-console+version:3.4.4 ## New Features -- **Introduce database export and import** - Add database export and database import operations. - - Database export saves the database information (its schema and data) on the client machine as two files at provided locations: - ``` - # database export - database export my-database export/my-database.typeql export/my-database.typedb - ``` - - Database import uses the exported files to create a new database with equivalent schema and data: - ``` - # database export - database import their-database export/my-database.typeql export/my-database.typedb - ``` - -- **Support relative script and source commands** - - We support using relative paths for the `--script` command (relative to the current directory), as well as relative paths for the REPL `source` command. - - When `source` is invoked _from_ a script, the sourced file is relativised to the script, rather than the current working directory. - ## Bugs Fixed @@ -34,6 +13,19 @@ Download from TypeDB Package Repository: https://cloudsmith.io/~typedb/repos/pub ## Other Improvements -- **Update zlib dependency** - Support build on Apple Clang 17+ by updating dependencies (details: https://github.com/typedb/typedb-dependencies/pull/577). - +- **Create specific exit error codes** + + We generate specific error codes to check when executing commands and scripts programmatically. + We now have the following exit codes: + ``` + Success = 0, + GeneralError = 1, + CommandError = 2, + ConnectionError = 3, + UserInputError = 4, + QueryError = 5, + ``` + + + + diff --git a/VERSION b/VERSION index fbcbf73..5141b61 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.4.0 \ No newline at end of file +3.4.4 \ No newline at end of file diff --git a/dependencies/typedb/repositories.bzl b/dependencies/typedb/repositories.bzl index c98c0a9..bdc69ba 100644 --- a/dependencies/typedb/repositories.bzl +++ b/dependencies/typedb/repositories.bzl @@ -5,17 +5,19 @@ load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository") def typedb_dependencies(): - git_repository( - name = "typedb_dependencies", - remote = "https://github.com/typedb/typedb-dependencies", - commit = "fac1121c903b0c9e5924d391a883e4a0749a82a2", # sync-marker: do not remove this comment, this is used for sync-dependencies by @typedb_dependencies - ) + # TODO: Return ref after merge to master, currently points to 'raft-dependencies-addition' + git_repository( + name = "typedb_dependencies", + remote = "https://github.com/typedb/typedb-dependencies", + commit = "a7f714591ac8da4e8a281ebd1542190346a56da1", # sync-marker: do not remove this comment, this is used for sync-dependencies by @typedb_dependencies + ) def typedb_driver(): + # TODO: Return typedb git_repository( name = "typedb_driver", - remote = "https://github.com/typedb/typedb-driver", - tag = "3.4.0", # sync-marker: do not remove this comment, this is used for sync-dependencies by @typedb_driver + remote = "https://github.com/farost/typedb-driver", + commit = "d2ce9ab4f85f8316a9087c130a20c7b1e731e8c4", # sync-marker: do not remove this comment, this is used for sync-dependencies by @typedb_driver ) def typeql(): diff --git a/src/cli.rs b/src/cli.rs index ecc2769..da4f272 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -6,6 +6,9 @@ use clap::Parser; +pub const ADDRESS_VALUE_NAME: &str = "host:port"; +pub const USERNAME_VALUE_NAME: &str = "username"; + #[derive(Parser, Debug)] #[command(author, about)] pub struct Args { @@ -21,13 +24,33 @@ pub struct Args { #[arg(long, value_name = "path to script file")] pub script: Vec, - /// TypeDB address to connect to. If using TLS encryption, this must start with "https://" - #[arg(long, value_name = "host:port")] - pub address: String, + /// TypeDB address to connect to (host:port). If using TLS encryption, this must start with "https://". + #[arg(long, value_name = ADDRESS_VALUE_NAME, conflicts_with_all = ["addresses", "address_translation"])] + pub address: Option, + + /// A comma-separated list of TypeDB replica addresses of a single cluster to connect to. + #[arg(long, value_name = "host1:port1,host2:port2", conflicts_with_all = ["address", "address_translation"])] + pub addresses: Option, + + /// A comma-separated list of public=private address pairs. Public addresses are the user-facing + /// addresses of the replicas, and private addresses are the addresses used for the server-side + /// connection between replicas. + #[arg(long, value_name = "public=private,...", conflicts_with_all = ["address", "addresses"])] + pub address_translation: Option, + + /// If used in a cluster environment, disables attempts to redirect requests to server replicas, + /// limiting Console to communicate only with the single address specified in the `address` + /// argument. + /// Use for administrative / debug purposes to test a specific replica only: this option will + /// lower the success rate of Console's operations in production. + #[arg(long = "replication-disabled", default_value = "false")] + pub replication_disabled: bool, + + // TODO: Add cluster-related retries/attempts flags from Driver Options? /// Username for authentication - #[arg(long, value_name = "username")] - pub username: String, + #[arg(long, value_name = USERNAME_VALUE_NAME)] + pub username: Option, /// Password for authentication. Will be requested safely by default. #[arg(long, value_name = "password")] @@ -45,8 +68,8 @@ pub struct Args { /// Disable error reporting. Error reporting helps TypeDB improve by reporting /// errors and crashes to the development team. - #[arg(long = "diagnostics-disable", default_value = "false")] - pub diagnostics_disable: bool, + #[arg(long = "diagnostics-disabled", default_value = "false")] + pub diagnostics_disabled: bool, /// Print the Console binary version #[arg(long = "version")] diff --git a/src/main.rs b/src/main.rs index a5ee07b..26bec0c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,15 +17,15 @@ use std::{ rc::Rc, sync::Arc, }; - +use std::collections::HashMap; use clap::Parser; use home::home_dir; use rustyline::error::ReadlineError; use sentry::ClientOptions; -use typedb_driver::{Credentials, DriverOptions, Transaction, TransactionType, TypeDBDriver}; +use typedb_driver::{Addresses, Credentials, DriverOptions, Transaction, TransactionType, TypeDBDriver}; use crate::{ - cli::Args, + cli::{Args, ADDRESS_VALUE_NAME, USERNAME_VALUE_NAME}, completions::{database_name_completer_fn, file_completer}, operations::{ database_create, database_delete, database_export, database_import, database_list, database_schema, @@ -40,6 +40,7 @@ use crate::{ }, runtime::BackgroundRuntime, }; +use crate::operations::{replica_deregister, replica_list, replica_primary, replica_register, server_version}; mod cli; mod completions; @@ -57,6 +58,36 @@ const TRANSACTION_REPL_HISTORY: &'static str = "typedb_console_transaction_repl_ const DIAGNOSTICS_REPORTING_URI: &'static str = "https://7f0ccb67b03abfccbacd7369d1f4ac6b@o4506315929812992.ingest.sentry.io/4506355433537536"; +#[derive(Debug, Copy, Clone)] +enum ExitCode { + Success = 0, + GeneralError = 1, + CommandError = 2, + ConnectionError = 3, + UserInputError = 4, + QueryError = 5, +} + +fn exit_with_error(err: &(dyn std::error::Error + 'static)) -> ! { + use crate::repl::command::ReplError; + if let Some(repl_err) = err.downcast_ref::() { + println_error!("Error: {}", repl_err); + exit(ExitCode::UserInputError as i32); + } else if let Some(io_err) = err.downcast_ref::() { + println_error!("I/O Error: {}", io_err); + exit(ExitCode::GeneralError as i32); + } else if let Some(driver_err) = err.downcast_ref::() { + println_error!("TypeDB Error: {}", driver_err); + exit(ExitCode::QueryError as i32); + } else if let Some(command_error) = err.downcast_ref::() { + println_error!("Command Error: {}", command_error); + exit(ExitCode::CommandError as i32); + } else { + println_error!("Error: {}", err); + exit(ExitCode::GeneralError as i32); + } +} + struct ConsoleContext { invocation_dir: PathBuf, repl_stack: Vec>>, @@ -94,37 +125,51 @@ fn main() { let mut args = Args::parse(); if args.version { println!("{}", VERSION); - exit(0); + exit(ExitCode::Success as i32); } + let address_info = parse_addresses(&args); + if !args.tls_disabled && !address_info.only_https { + println_error!( + "\ + TLS connections can only be enabled when connecting to HTTPS endpoints. \ + For example, using 'https://:port'.\n\ + Please modify the address or disable TLS ('{}'). {}\ + ", + format_argument!("--tls-disabled"), + format_warning!("WARNING: this will send passwords over plaintext!"), + ); + exit(ExitCode::UserInputError as i32); + } + let username = match args.username { + Some(username) => username, + None => { + println_error!( + "username is required for connection authentication ('{}').", + format_argument!("--username <{USERNAME_VALUE_NAME}>") + ); + exit(ExitCode::UserInputError as i32); + } + }; if args.password.is_none() { - args.password = Some(LineReaderHidden::new().readline(&format!("password for '{}': ", args.username))); + args.password = Some(LineReaderHidden::new().readline(&format!("password for '{username}': "))); } - if !args.diagnostics_disable { + if !args.diagnostics_disabled { init_diagnostics() } - if !args.tls_disabled && !args.address.starts_with("https:") { - println!( - "\ - TLS connections can only be enabled when connecting to HTTPS endpoints, for example using 'https://:port'. \ - Please modify the address, or disable TLS (--tls-disabled). WARNING: this will send passwords over plaintext!\ - " - ); - exit(1); - } - let runtime = BackgroundRuntime::new(); let tls_root_ca_path = args.tls_root_ca.as_ref().map(|value| Path::new(value)); + let runtime = BackgroundRuntime::new(); + let driver_options = DriverOptions::new().use_replication(!args.replication_disabled).tls_enabled(!args.tls_disabled).tls_root_ca(tls_root_ca_path).unwrap(); let driver = match runtime.run(TypeDBDriver::new( - args.address, - Credentials::new(&args.username, args.password.as_ref().unwrap()), - DriverOptions::new(!args.tls_disabled, tls_root_ca_path).unwrap(), + address_info.addresses, + Credentials::new(&username, args.password.as_ref().unwrap()), + driver_options, )) { Ok(driver) => Arc::new(driver), Err(err) => { - println!("Failed to create driver connection to server. {}", err); - if !args.tls_disabled { - println!("Verify that the server is also configured with TLS encryption."); - } - exit(1); + let tls_error = + if args.tls_disabled { "" } else { "\nVerify that the server is also configured with TLS encryption." }; + println_error!("Failed to create driver connection to server. {err}{tls_error}"); + exit(ExitCode::ConnectionError as i32); } }; @@ -140,27 +185,34 @@ fn main() { }; if !args.command.is_empty() && !args.script.is_empty() { - println!("Error: Cannot specify both commands and files"); - exit(1); + println_error!("cannot specify both commands and files"); + exit(ExitCode::UserInputError as i32); } else if !args.command.is_empty() { - execute_command_list(&mut context, &args.command); + if let Err(err) = execute_command_list(&mut context, &args.command) { + exit_with_error(&*err); + } } else if !args.script.is_empty() { - execute_scripts(&mut context, &args.script); + if let Err(err) = execute_scripts(&mut context, &args.script) { + exit_with_error(&*err); + } } else { execute_interactive(&mut context); } } -fn execute_scripts(context: &mut ConsoleContext, files: &[String]) { +fn execute_scripts(context: &mut ConsoleContext, files: &[String]) -> Result<(), Box> { for file_path in files { let path = context.convert_path(file_path); if let Ok(file) = File::open(&file_path) { execute_script(context, path, io::BufReader::new(file).lines()) } else { - println!("Error opening file: {}", path.to_string_lossy()); - exit(1); + return Err(Box::new(io::Error::new( + io::ErrorKind::NotFound, + format!("Error opening file: {}", path.to_string_lossy()), + ))); } } + Ok(()) } fn execute_script( @@ -187,13 +239,14 @@ fn execute_script( context.script_dir = None; } -fn execute_command_list(context: &mut ConsoleContext, commands: &[String]) { +fn execute_command_list(context: &mut ConsoleContext, commands: &[String]) -> Result<(), Box> { for command in commands { - if let Err(_) = execute_commands(context, command, true, true) { - println!("### Stopped executing at command: {}", command); - exit(1); + if let Err(err) = execute_commands(context, command, true, true) { + println_error!("### Stopped executing at command: {}", command); + return Err(Box::new(err)); } } + Ok(()) } fn execute_interactive(context: &mut ConsoleContext) { @@ -221,7 +274,7 @@ fn execute_interactive(context: &mut ConsoleContext) { // do nothing } else { // this is unexpected... quit - exit(1) + exit(ExitCode::GeneralError as i32); } } Err(err) => { @@ -236,7 +289,7 @@ fn execute_commands( mut input: &str, coerce_each_command_to_one_line: bool, must_log_command: bool, -) -> Result<(), EmptyError> { +) -> Result<(), CommandError> { let mut multiple_commands = None; while !context.repl_stack.is_empty() && !input.trim().is_empty() { let repl_index = context.repl_stack.len() - 1; @@ -244,8 +297,9 @@ fn execute_commands( input = match current_repl.match_first_command(input, coerce_each_command_to_one_line) { Ok(None) => { - println!("Unrecognised command: {}", input); - return Err(EmptyError {}); + let message = format!("Unrecognised command: {}", input); + println_error!("{}", message); + return Err(CommandError { message }); } Ok(Some((command, arguments, next_command_index))) => { let command_string = &input[0..next_command_index]; @@ -254,19 +308,20 @@ fn execute_commands( } if must_log_command || multiple_commands.is_some_and(|b| b) { - println!("{} {}", "+".repeat(repl_index + 1), command_string.trim()); + println_error!("{} {}", "+".repeat(repl_index + 1), command_string.trim()); } match command.execute(context, arguments) { Ok(_) => &input[next_command_index..], Err(err) => { - println!("Error executing command: '{}'\n{}", command_string.trim(), err); - return Err(EmptyError {}); + let message = format!("Error executing command: '{}'\n{}", command_string.trim(), err); + println_error!("{}", message); + return Err(CommandError { message }); } } } Err(err) => { - println!("{}", err); - return Err(EmptyError {}); + println_error!("{}", err); + return Err(CommandError { message: err.to_string() }); } }; input = input.trim_start(); @@ -275,6 +330,28 @@ fn execute_commands( } fn entry_repl(driver: Arc, runtime: BackgroundRuntime) -> Repl { + let server_commands = Subcommand::new("server") + .add(CommandLeaf::new("version", "Retrieve server version.", server_version)); + + let replica_commands = Subcommand::new("replica") + .add(CommandLeaf::new("list", "List replicas.", replica_list)) + .add(CommandLeaf::new("primary", "Get current primary replica.", replica_primary)) + .add(CommandLeaf::new_with_inputs( + "register", + "Register new replica. Requires a clustering address, not a connection address.", + vec![ + CommandInput::new("replica id", get_word, None, None), + CommandInput::new("clustering address", get_word, None, None), + ], + replica_register, + )) + .add(CommandLeaf::new_with_input( + "deregister", + "Deregister existing replica.", + CommandInput::new("replica id", get_word, None, None), + replica_deregister, + )); + let database_commands = Subcommand::new("database") .add(CommandLeaf::new("list", "List databases on the server.", database_list)) .add(CommandLeaf::new_with_input( @@ -371,8 +448,10 @@ fn entry_repl(driver: Arc, runtime: BackgroundRuntime) -> Repl &'static str { } } +struct AddressInfo { + only_https: bool, + addresses: Addresses, +} + +fn parse_addresses(args: &Args) -> AddressInfo { + if let Some(address) = &args.address { + AddressInfo {only_https: is_https_address(address), addresses: Addresses::try_from_address_str(address).unwrap() } + } else if let Some(addresses) = &args.addresses { + let split = addresses.split(',').map(str::to_string).collect::>(); + let only_https = split.iter().all(|address| is_https_address(address)); + AddressInfo {only_https, addresses: Addresses::try_from_addresses_str(split).unwrap() } + } else if let Some(translation) = &args.address_translation { + let mut map = HashMap::new(); + let mut only_https = true; + for pair in translation.split(',') { + let (public_address, private_address) = pair + .split_once('=') + .unwrap_or_else(|| panic!("Invalid address pair: {pair}. Must be of form public=private")); + only_https = only_https && is_https_address(public_address); + map.insert(public_address.to_string(), private_address.to_string()); + } + println!("Translation map:: {map:?}"); // TODO: Remove + AddressInfo {only_https, addresses: Addresses::try_from_translation_str(map).unwrap() } + } else { + panic!("At least one of --address, --addresses, or --address-translation must be provided."); + } +} + +fn is_https_address(address: &str) -> bool { + address.starts_with("https:") +} + fn init_diagnostics() { let _ = sentry::init(( DIAGNOSTICS_REPORTING_URI, @@ -432,18 +544,20 @@ fn init_diagnostics() { )); } -struct EmptyError {} +struct CommandError { + message: String, +} -impl Debug for EmptyError { +impl Debug for CommandError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { Display::fmt(self, f) } } -impl Display for EmptyError { +impl Display for CommandError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "") + write!(f, "{}", self.message) } } -impl Error for EmptyError {} +impl Error for CommandError {} diff --git a/src/operations.rs b/src/operations.rs index 3fc9710..a57216f 100644 --- a/src/operations.rs +++ b/src/operations.rs @@ -25,6 +25,16 @@ use crate::{ transaction_repl, ConsoleContext, }; +pub(crate) fn server_version(context: &mut ConsoleContext, _input: &[String]) -> CommandResult { + let driver = context.driver.clone(); + let server_version = context + .background_runtime + .run(async move { driver.server_version().await }) + .map_err(|err| Box::new(err) as Box)?; + println!("{}", server_version); + Ok(()) +} + pub(crate) fn database_list(context: &mut ConsoleContext, _input: &[String]) -> CommandResult { let driver = context.driver.clone(); let databases = context @@ -117,7 +127,7 @@ pub(crate) fn user_list(context: &mut ConsoleContext, _input: &[String]) -> Comm println!("No users are present."); } else { for user in users { - println!("{}", user.name); + println!("{}", user.name()); } } Ok(()) @@ -169,13 +179,13 @@ pub(crate) fn user_update_password(context: &mut ConsoleContext, input: &[String }; let current_user = driver .users() - .get_current_user() + .get_current() .await .map_err(|err| Box::new(err) as Box)? .expect("Could not fetch currently logged in user."); user.update_password(new_password).await.map_err(|err| Box::new(err) as Box)?; - Ok(current_user.name == username) + Ok(current_user.name() == username) })?; if updated_current_user { println!("Successfully updated current user's password, exiting console. Please log in with the updated credentials."); @@ -186,6 +196,57 @@ pub(crate) fn user_update_password(context: &mut ConsoleContext, input: &[String Ok(()) } +pub(crate) fn replica_list(context: &mut ConsoleContext, _input: &[String]) -> CommandResult { + let driver = context.driver.clone(); + context.background_runtime.run(async move { + let replicas = driver.replicas().await.map_err(|err| Box::new(err) as Box)?; + if replicas.is_empty() { + println!("No replicas are present."); + } else { + for replica in replicas { + println!("{}", replica.address()); + } + } + Ok(()) + }) +} + +pub(crate) fn replica_primary(context: &mut ConsoleContext, _input: &[String]) -> CommandResult { + let driver = context.driver.clone(); + let primary_replica = driver.primary_replica(); + if let Some(primary_replica) = primary_replica { + println!("{}", primary_replica.address()); + } else { + println!("No primary replica is present."); + } + Ok(()) +} + +pub(crate) fn replica_register(context: &mut ConsoleContext, input: &[String]) -> CommandResult { + let driver = context.driver.clone(); + let replica_id: u64 = input[0].parse().map_err(|_| Box::new(ReplError { message: format!("Replica id '{}' must be a number.", input[0]) }) + as Box)?; + let address = input[1].clone(); + context + .background_runtime + .run(async move { driver.register_replica(replica_id, address).await }) + .map_err(|err| Box::new(err) as Box)?; + println!("Successfully registered replica."); + Ok(()) +} + +pub(crate) fn replica_deregister(context: &mut ConsoleContext, input: &[String]) -> CommandResult { + let driver = context.driver.clone(); + let replica_id: u64 = input[0].parse().map_err(|_| Box::new(ReplError { message: format!("Replica id '{}' must be a number.", input[0]) }) + as Box)?; + context + .background_runtime + .run(async move { driver.deregister_replica(replica_id).await }) + .map_err(|err| Box::new(err) as Box)?; + println!("Successfully deregistered replica."); + Ok(()) +} + pub(crate) fn transaction_read(context: &mut ConsoleContext, input: &[String]) -> CommandResult { let driver = context.driver.clone(); let db_name = &input[0]; diff --git a/src/printer.rs b/src/printer.rs index eddb758..b26bb57 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -4,6 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +use clap::builder::styling::{AnsiColor, Color, Style}; use typedb_driver::{ answer::{ConceptDocument, ConceptRow}, concept::{Concept, Value}, @@ -14,6 +15,54 @@ const TABLE_INDENT: &'static str = " "; const CONTENT_INDENT: &'static str = " "; const TABLE_DASHES: usize = 7; +pub const ERROR_STYLE: Style = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Red))).bold(); +pub const WARNING_STYLE: Style = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Yellow))).bold(); +pub const ARGUMENT_STYLE: Style = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Yellow))); + +#[macro_export] +macro_rules! format_error { + ($($arg:tt)*) => { + $crate::format_colored!($crate::printer::ERROR_STYLE, $($arg)*) + }; +} + +#[macro_export] +macro_rules! format_warning { + ($($arg:tt)*) => { + $crate::format_colored!($crate::printer::WARNING_STYLE, $($arg)*) + }; +} + +#[macro_export] +macro_rules! format_argument { + ($($arg:tt)*) => { + $crate::format_colored!($crate::printer::ARGUMENT_STYLE, $($arg)*) + }; +} + +#[macro_export] +macro_rules! format_colored { + ($style:expr, $($arg:tt)*) => { + format!( + "{}{}{}", + $style.render(), + format!($($arg)*), + $style.render_reset() + ) + }; +} + +#[macro_export] +macro_rules! println_error { + ($($arg:tt)*) => { + eprintln!( + "{} {}", + $crate::format_error!("error:"), + format!($($arg)*) + ); + } +} + fn println(string: &str) { println!("{}", string) } diff --git a/tool/runner/Cargo.toml b/tool/runner/Cargo.toml index a7ab45f..193b708 100644 --- a/tool/runner/Cargo.toml +++ b/tool/runner/Cargo.toml @@ -16,7 +16,7 @@ features = {} [dependencies.clap] features = ["color", "default", "derive", "error-context", "help", "std", "suggestions", "usage", "wrap_help"] - version = "4.5.38" + version = "4.5.40" default-features = false [dependencies.tempdir]