From e8a1cebefa2f3281fe4d4f3928bfdb8237a6512f Mon Sep 17 00:00:00 2001 From: Odonno Date: Mon, 10 Apr 2023 18:51:49 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20list=20all=20migrations=20applied?= =?UTF-8?q?=20to=20the=20database?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 55 ++++++++++++++++ Cargo.toml | 3 +- src/apply.rs | 10 +-- src/cli.rs | 40 +++++++++++- src/list.rs | 106 ++++++++++++++++++++++++++++++ src/main.rs | 10 +++ src/models.rs | 7 ++ tests/cli/list.rs | 160 ++++++++++++++++++++++++++++++++++++++++++++++ tests/cli/main.rs | 1 + 9 files changed, 380 insertions(+), 12 deletions(-) create mode 100644 src/list.rs create mode 100644 src/models.rs create mode 100644 tests/cli/list.rs diff --git a/Cargo.lock b/Cargo.lock index 1248267..71a8289 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -331,6 +331,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "chrono-human-duration" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19283df3a144dfdeee6568e42ad7f0939d3c261bcdfe954b1a1098f6f7c1b908" +dependencies = [ + "chrono", +] + [[package]] name = "cipher" version = "0.4.4" @@ -378,6 +387,29 @@ dependencies = [ "os_str_bytes", ] +[[package]] +name = "cli-table" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adfbb116d9e2c4be7011360d0c0bee565712c11e969c9609b25b619366dc379d" +dependencies = [ + "cli-table-derive", + "csv", + "termcolor", + "unicode-width", +] + +[[package]] +name = "cli-table-derive" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af3bfb9da627b0a6c467624fb7963921433774ed435493b5c08a3053e829ad4" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "codespan-reporting" version = "0.11.1" @@ -437,6 +469,27 @@ dependencies = [ "typenum", ] +[[package]] +name = "csv" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b015497079b9a9d69c02ad25de6c0a6edef051ea6360a327d0bd05802ef64ad" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90" +dependencies = [ + "memchr", +] + [[package]] name = "cxx" version = "1.0.92" @@ -2024,7 +2077,9 @@ version = "0.9.0" dependencies = [ "assert_cmd", "chrono", + "chrono-human-duration", "clap", + "cli-table", "diffy", "dir-diff", "fs_extra", diff --git a/Cargo.toml b/Cargo.toml index 07c0035..2066fd6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,9 @@ edition = "2021" [dependencies] chrono = "0.4.24" +chrono-human-duration = "0.1.1" clap = { version = "4.1.8", features = ["derive"] } +cli-table = "0.4.7" diffy = "0.3.0" fs_extra = "1.3.0" regex = "1.7.1" @@ -29,4 +31,3 @@ surrealdb = "1.0.0-beta.9" assert_cmd = "2.0.10" dir-diff = "0.3.2" serial_test = "2.0.0" - diff --git a/src/apply.rs b/src/apply.rs index 6d13152..fada91f 100644 --- a/src/apply.rs +++ b/src/apply.rs @@ -1,15 +1,8 @@ use fs_extra::dir::{DirEntryAttr, DirEntryValue}; -use serde::{Deserialize, Serialize}; use std::{collections::HashSet, path::Path, process}; use surrealdb::{engine::remote::ws::Ws, opt::auth::Root, Surreal}; -use crate::{config, definitions}; - -#[derive(Serialize, Deserialize, Debug)] -struct ScriptMigration { - script_name: String, - executed_at: String, -} +use crate::{config, definitions, models::ScriptMigration}; fn within_transaction(inner_query: String) -> String { format!( @@ -71,7 +64,6 @@ pub async fn main( } let mut migrations_applied: Vec = response.unwrap(); - migrations_applied.sort_by_key(|m| m.executed_at.clone()); let mut config = HashSet::new(); diff --git a/src/cli.rs b/src/cli.rs index 355329f..77da003 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -38,17 +38,53 @@ pub enum Action { /// This parameter allows you to skip ulterior migrations. #[clap(long)] up: Option, + /// Url of the surrealdb instance. + /// Default value is `localhost:8000`. #[clap(long)] url: Option, + /// Namespace to use inside the surrealdb instance. + /// Default value is `test`. #[clap(long)] ns: Option, + /// Name of the database to use inside the surrealdb instance. + /// Default value is `test`. #[clap(long)] db: Option, + /// Username used to authenticate to the surrealdb instance. + /// Default value is `root`. #[clap(short, long)] username: Option, + /// Password used to authenticate to the surrealdb instance. + /// Default value is `root`. #[clap(short, long)] password: Option, }, + /// List all migrations applied to the database + #[clap(aliases = vec!["ls"])] + List { + /// Url of the surrealdb instance. + /// Default value is `localhost:8000`. + #[clap(long)] + url: Option, + /// Namespace to use inside the surrealdb instance. + /// Default value is `test`. + #[clap(long)] + ns: Option, + /// Name of the database to use inside the surrealdb instance. + /// Default value is `test`. + #[clap(long)] + db: Option, + /// Username used to authenticate to the surrealdb instance. + /// Default value is `root`. + #[clap(short, long)] + username: Option, + /// Password used to authenticate to the surrealdb instance. + /// Default value is `root`. + #[clap(short, long)] + password: Option, + #[clap(long)] + no_color: bool, + }, } #[derive(Subcommand, Debug)] @@ -58,7 +94,7 @@ pub enum CreateAction { Schema { /// Name of the schema to generate name: String, - /// A list of fields to define on the table + /// A list of fields to define on the table, using "," as a delimiter #[clap(short, long, value_delimiter = ',')] fields: Option>, #[clap(long)] @@ -69,7 +105,7 @@ pub enum CreateAction { Event { /// Name of the event to generate name: String, - /// A list of fields to define on the table + /// A list of fields to define on the table, using "," as a delimiter #[clap(short, long, value_delimiter = ',')] fields: Option>, #[clap(long)] diff --git a/src/list.rs b/src/list.rs new file mode 100644 index 0000000..9860c67 --- /dev/null +++ b/src/list.rs @@ -0,0 +1,106 @@ +use chrono::{DateTime, Utc}; +use chrono_human_duration::ChronoHumanDuration; +use cli_table::{format::Border, Cell, ColorChoice, Style, Table}; +use std::process; +use surrealdb::{engine::remote::ws::Ws, opt::auth::Root, Surreal}; + +use crate::{config, models::ScriptMigration}; + +pub async fn main( + url: Option, + ns: Option, + db: Option, + username: Option, + password: Option, + no_color: bool, +) { + let db_config = config::retrieve_db_config(); + + let url = url.or(db_config.url).unwrap_or("localhost:8000".to_owned()); + + let connection = Surreal::new::(url.to_owned()).await; + + if let Err(error) = connection { + eprintln!("{}", error); + process::exit(1); + } + + let client = connection.unwrap(); + + let username = username.or(db_config.username).unwrap_or("root".to_owned()); + let password = password.or(db_config.password).unwrap_or("root".to_owned()); + + client + .signin(Root { + username: &username, + password: &password, + }) + .await + .unwrap(); + + let ns = ns.or(db_config.ns).unwrap_or("test".to_owned()); + let db = db.or(db_config.db).unwrap_or("test".to_owned()); + + client + .use_ns(ns.to_owned()) + .use_db(db.to_owned()) + .await + .unwrap(); + + let response = client.select("script_migration").await; + + if let Err(error) = response { + eprintln!("{}", error); + process::exit(1); + } + + let mut migrations_applied: Vec = response.unwrap(); + migrations_applied.sort_by_key(|m| m.executed_at.clone()); + + if migrations_applied.is_empty() { + println!("No migrations applied yet!"); + } else { + let now = Utc::now(); + + let rows = migrations_applied + .iter() + .map(|m| { + let display_name = m + .script_name + .split("_") + .skip(2) + .map(|s| s.to_string()) + .collect::>() + .join("_"); + + let executed_at = DateTime::parse_from_rfc3339(&m.executed_at).unwrap(); + let since = now.signed_duration_since(executed_at); + let since = since.format_human().to_string(); + + let file_name = m.script_name.clone() + ".surql"; + + vec![display_name.cell(), since.cell(), file_name.cell()] + }) + .collect::>(); + + let color_choice = if no_color { + ColorChoice::Never + } else { + ColorChoice::Auto + }; + + let table = rows + .table() + .title(vec![ + "Name".cell().bold(true), + "Executed at".cell().bold(true), + "File name".cell().bold(true), + ]) + .color_choice(color_choice) + .border(Border::builder().build()); + + let table_display = table.display().unwrap(); + + println!("{}", table_display); + } +} diff --git a/src/main.rs b/src/main.rs index ab5f80a..dc73091 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,8 @@ mod cli; mod config; mod create; mod definitions; +mod list; +mod models; mod scaffold; #[tokio::main] @@ -46,5 +48,13 @@ async fn main() { username, password, } => apply::main(up, url, ns, db, username, password).await, + Action::List { + url, + ns, + db, + username, + password, + no_color, + } => list::main(url, ns, db, username, password, no_color).await, }; } diff --git a/src/models.rs b/src/models.rs new file mode 100644 index 0000000..0d6e98e --- /dev/null +++ b/src/models.rs @@ -0,0 +1,7 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug)] +pub struct ScriptMigration { + pub script_name: String, + pub executed_at: String, +} diff --git a/tests/cli/list.rs b/tests/cli/list.rs new file mode 100644 index 0000000..2230e9e --- /dev/null +++ b/tests/cli/list.rs @@ -0,0 +1,160 @@ +use assert_cmd::Command; +use chrono::Local; +use serial_test::serial; + +use crate::helpers; + +#[test] +#[serial] +fn list_empty_migrations() { + helpers::clear_files_dir(); + + let mut child_process = std::process::Command::new("surreal") + .arg("start") + .arg("--user") + .arg("root") + .arg("--pass") + .arg("root") + .arg("memory") + .spawn() + .unwrap(); + + { + let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap(); + + cmd.arg("scaffold").arg("empty"); + + let result = cmd.assert().try_success(); + + match result { + Ok(_) => {} + Err(error) => { + child_process.kill().unwrap(); + panic!("{}", error); + } + } + } + + { + let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap(); + + cmd.arg("apply"); + + let result = cmd.assert().try_success(); + + match result { + Ok(_) => {} + Err(error) => { + child_process.kill().unwrap(); + panic!("{}", error); + } + } + } + + { + let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap(); + + cmd.arg("list"); + + let result = cmd + .assert() + .try_success() + .and_then(|assert| assert.try_stdout("No migrations applied yet!\n")); + + match result { + Ok(_) => {} + Err(error) => { + child_process.kill().unwrap(); + panic!("{}", error); + } + } + } + + child_process.kill().unwrap(); +} + +#[test] +#[serial] +fn list_blog_migrations() { + helpers::clear_files_dir(); + + let mut child_process = std::process::Command::new("surreal") + .arg("start") + .arg("--user") + .arg("root") + .arg("--pass") + .arg("root") + .arg("memory") + .spawn() + .unwrap(); + + let now = Local::now(); + + { + let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap(); + + cmd.arg("scaffold").arg("blog"); + + let result = cmd.assert().try_success(); + + match result { + Ok(_) => {} + Err(error) => { + child_process.kill().unwrap(); + panic!("{}", error); + } + } + } + + { + let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap(); + + cmd.arg("apply"); + + let result = cmd.assert().try_success(); + + match result { + Ok(_) => {} + Err(error) => { + child_process.kill().unwrap(); + panic!("{}", error); + } + } + } + + { + let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap(); + + cmd.arg("list").arg("--no-color"); + + let date_prefix = now.format("%Y%m%d_%H%M").to_string(); + + let expected = format!( + " Name | Executed at | File name +--------------+-------------+------------------------------------ + AddAdminUser | just now | {0}01_AddAdminUser.surql +--------------+-------------+------------------------------------ + AddPost | just now | {0}02_AddPost.surql +--------------+-------------+------------------------------------ + CommentPost | just now | {0}03_CommentPost.surql +\n", + date_prefix + ); + println!("{}", expected); + + let result = cmd + .assert() + .try_success() + .and_then(|assert| assert.try_stdout(expected)); + + match result { + Ok(_) => {} + Err(error) => { + child_process.kill().unwrap(); + panic!("{}", error); + } + } + } + + child_process.kill().unwrap(); +} diff --git a/tests/cli/main.rs b/tests/cli/main.rs index 8327ae3..c0d2e95 100644 --- a/tests/cli/main.rs +++ b/tests/cli/main.rs @@ -1,4 +1,5 @@ mod apply; mod create; mod helpers; +mod list; mod scaffold;