diff --git a/Cargo.lock b/Cargo.lock index 4cc1cda..813a43e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -798,7 +798,7 @@ checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" [[package]] name = "salvo-cli" -version = "0.1.19" +version = "0.1.20" dependencies = [ "ansi_term", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index e3abfe2..7ef08df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "salvo-cli" -version = "0.1.19" +version = "0.1.20" edition = "2021" authors = ["Fankai Liu liufankai137@outlook.com"] keywords = ["salvo", "cli","template"] diff --git a/README.md b/README.md index 0d34a23..8884063 100644 --- a/README.md +++ b/README.md @@ -98,8 +98,11 @@ cargo install --force salvo-cli | ✅ | web site template | | ✅ | Template with SQLx, SeaORM, Diesel, Rbatis (support for SQLite, PostgreSQL, MySQL) | | ✅ | Basic middleware | +| ✅ | Support for MongoDB | | ⏳ | More middleware | -| ⏳ | Support for MongoDB | +| ⏳ | Support for docker | +| ⏳ | More integrations with good crates (validation, embedding, permissions or others?) | +| ⏳ | Split into multiple crates for clearer code organization | ## License diff --git a/locales/app.yml b/locales/app.yml index 415693e..abe087a 100644 --- a/locales/app.yml +++ b/locales/app.yml @@ -347,6 +347,24 @@ db_conn_types_rbatis: th: rbatis:เฟรมเวิร์ก ORM แบบ async ที่คล้ายกับ Mybatis (java) el: rbatis:ένα παρόμοιο Mybatis (java) ασύγχρονο πλαίσιο ORM da: rbatis:Et lignende Mybatis (java) asynkront ORM-rammeverk +db_conn_types_mongodb: + en: The official MongoDB driver for Rust + zh_CN: Rust 的官方 MongoDB 驱动程序 + zh_TW: Rust 的官方 MongoDB 驱動程式 + fr: Le pilote MongoDB officiel pour Rust + ja: Rust の公式 MongoDB ドライバー + es: El controlador MongoDB oficial para Rust + de: Der offizielle MongoDB-Treiber für Rust + ru: Официальный драйвер MongoDB для Rust + it: Il driver MongoDB ufficiale per Rust + pt: O driver oficial do MongoDB para Rust + ko: Rust의 공식 MongoDB 드라이버 + no: Den offisielle MongoDB-driveren for Rust + is: Opinber MongoDB keyrir fyrir Rust + uk: Офіційний драйвер MongoDB для Rust + th: ไดร์เวอร์ MongoDB อย่างเป็นทางการสำหรับ Rust + el: Ο επίσημος οδηγός MongoDB για Rust + da: Den officielle MongoDB-driver for Rust db_conn_types_nothing: en: unnecessary zh_CN: 不需要 diff --git a/locales/code_comment.yml b/locales/code_comment.yml index b5a8956..b1236f9 100644 --- a/locales/code_comment.yml +++ b/locales/code_comment.yml @@ -580,3 +580,21 @@ create_success_sqlx_diesel: th: 🎯 สร้างฐานข้อมูลเริ่มต้นใน `/data/test.db`. \n หลังจากเรียกใช้งาน คุณสามารถเข้าถึง /login ด้วยชื่อผู้ใช้เริ่มต้น:zhangsan และรหัสผ่าน:123 el: 🎯 Δημιουργήθηκε η προεπιλεγμένη βάση δεδομένων στο `/data/test.db`. \n Μετά την εκτέλεσή του, μπορείτε να αποκτήσετε πρόσβαση στο /login με το όνομα χρήστη:zhangsan και τον κωδικό πρόσβασης:123 da: 🎯 Standarddatabasen er oprettet i `/data/test.db`. \n Efter at have kørt det, kan du få adgang til /login med standardbrugernavnet:zhangsan og adgangskoden:123 +mongodb_usage_import_user_data: + en: 🎯 Please first run the program, then import data from data/user.json into the user table + zh_CN: 🎯 请首先运行程序,然后将 data/user.json 中的数据导入到 user 表中 + zh_TW: 🎯 請先運行程序,然後將 data/user.json 中的數據導入到 user 表中 + fr: 🎯 Veuillez d'abord exécuter le programme, puis importer les données de data/user.json dans la table utilisateur + ja: 🎯 プログラムを最初に実行し、その後、data/user.json からデータを user テーブルにインポートしてください + es: 🎯 Por favor, primero ejecute el programa, luego importe los datos de data/user.json a la tabla de usuarios + de: 🎯 Bitte führen Sie zuerst das Programm aus und importieren Sie dann die Daten aus data/user.json in die Benutzertabelle + ru: 🎯 Пожалуйста, сначала запустите программу, затем импортируйте данные из data/user.json в таблицу пользователей + it: 🎯 Si prega di eseguire prima il programma, poi importare i dati da data/user.json nella tabella degli utenti + pt: 🎯 Por favor, primeiro execute o programa, depois importe os dados de data/user.json para a tabela de usuários + ko: 🎯 먼저 프로그램을 실행한 다음, data/user.json의 데이터를 사용자 테이블에 가져와 주세요. + no: 🎯 Vennligst kjør først programmet, deretter importer data fra data/user.json inn i brukertabellen + is: 🎯 Vinsamlegast keyraðu fyrst forritið, síðan flytjaðu gögn úr data/user.json inn í notandatafluna + uk: 🎯 Будь ласка, спочатку запустіть програму, а потім імпортуйте дані з data/user.json до таблиці користувачів + th: 🎯 โปรดเรียกใช้โปรแกรมก่อน, จากนั้นนำเข้าข้อมูลจาก data/user.json ไปยังตารางผู้ใช้ + el: 🎯 Παρακαλώ πρώτα τρέξτε το πρόγραμμα, στη συνέχεια εισάγετε τα δεδομένα από το data/user.json στον πίνακα χρήστη + da: 🎯 Kør venligst først programmet, importér derefter data fra data/user.json ind i brugertabellen diff --git a/src/main.rs b/src/main.rs index 756f66b..2f04bb7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,7 @@ use i18n::set_locale; mod i18n; rust_i18n::i18n!("locales", fallback = "en"); #[derive(Parser, Debug)] -#[clap(version = "0.1.19", author = "Fankai liu ")] +#[clap(version = "0.1.20", author = "Fankai liu ")] struct Opts { #[clap(subcommand)] subcmd: SubCommand, diff --git a/src/template/config/config.hbs b/src/template/config/config.hbs index c0e6625..2e9fadc 100644 --- a/src/template/config/config.hbs +++ b/src/template/config/config.hbs @@ -41,6 +41,9 @@ database_url="mssql://SA:TestPass!123456@localhost:1433/rbatis_example" {{/if}} {{/if}} {{/if}} +{{#if is_mongodb}} +database_url="mongodb://localhost:27017" +{{/if}} [jwt] jwt_secret = "secret" jwt_exp = 6000 diff --git a/src/template/data/users.json b/src/template/data/users.json new file mode 100644 index 0000000..8a2d7dc --- /dev/null +++ b/src/template/data/users.json @@ -0,0 +1,7 @@ +[ + { + "_id": "65507c75ae575ce9f420eb97", + "password": "$argon2id$v=19$m=19456,t=2,p=1$BL/Ly2chRn33DZJePPniYQ$kzJ24lG8j0GR+6Q7PmHUiE7JxlQfomB5svwDwnoL0Ng", + "username": "zhangsan" + } +] \ No newline at end of file diff --git a/src/template/src/app_error.hbs b/src/template/src/app_error.hbs index a6050f7..6d73705 100644 --- a/src/template/src/app_error.hbs +++ b/src/template/src/app_error.hbs @@ -15,6 +15,11 @@ use salvo::{ Depot, Request, Response, Writer, }; {{/if}} +{{#if is_mongodb}} +use mongodb::bson::document::ValueAccessError as MongoBsonAccessError; +use mongodb::bson::oid::Error as MongoBsonOidError; +use mongodb::error::Error as MongoDbError; +{{/if}} use thiserror::Error; #[derive(Error, Debug)] @@ -39,6 +44,14 @@ pub enum AppError { #[error("rbatis::Error:`{0}`")] RbatisErr(#[from] rbatis::Error), {{/if}} + {{#if is_mongodb}} + #[error("mongodb::error::Error:`{0}`")] + MongoDbErr(#[from] MongoDbError), + #[error("mongodb::bson::document::ValueAccessError:`{0}`")] + MongoBsonAccessError(#[from] MongoBsonAccessError), + #[error("mongodb::bson::oid::Error`{0}`")] + MongoBsonOidError(#[from] MongoBsonOidError), + {{/if}} } pub type AppResult = Result; diff --git a/src/template/src/db.hbs b/src/template/src/db.hbs index a0048b0..d127a04 100644 --- a/src/template/src/db.hbs +++ b/src/template/src/db.hbs @@ -72,7 +72,6 @@ pub async fn init_db_conn() { .await; } {{/if}} - {{#if is_sea_orm}} pub static DB: OnceCell = OnceCell::const_new(); @@ -90,7 +89,6 @@ pub async fn init_db_conn() { .await; } {{/if}} - {{#if is_rbatis}} use tokio::sync::OnceCell; use rbatis::rbatis::RBatis; @@ -133,3 +131,29 @@ pub async fn init_db_conn() { .await; } {{/if}} +{{#if is_mongodb}} +use crate::entities::user::User; +use mongodb::{bson::doc, options::IndexOptions, Client, IndexModel}; +use once_cell::sync::OnceCell; +pub const DB_NAME: &str = "myApp"; +pub const COLL_NAME: &str = "users"; +pub static MONGODB_CLIENT: OnceCell = OnceCell::new(); +pub async fn init_db_conn() { + let mongodb_uri = &CFG.database.database_url; + let client = Client::with_uri_str(mongodb_uri) + .await + .expect("failed to connect"); + let options = IndexOptions::builder().unique(true).build(); + let model = IndexModel::builder() + .keys(doc! { "username": 1 }) + .options(options) + .build(); + client + .database(DB_NAME) + .collection::(COLL_NAME) + .create_index(model, None) + .await + .expect("creating an index should succeed"); + MONGODB_CLIENT.get_or_init(|| client); +} +{{/if}} diff --git a/src/template/src/entities/mod.hbs b/src/template/src/entities/mod.hbs index 169dff8..4e48854 100644 --- a/src/template/src/entities/mod.hbs +++ b/src/template/src/entities/mod.hbs @@ -10,4 +10,7 @@ pub mod user; {{/if}} {{#if is_rbatis}} pub mod user; +{{/if}} +{{#if is_mongodb}} +pub mod user; {{/if}} \ No newline at end of file diff --git a/src/template/src/entities/user.hbs b/src/template/src/entities/user.hbs index 3392071..c3a79ba 100644 --- a/src/template/src/entities/user.hbs +++ b/src/template/src/entities/user.hbs @@ -40,4 +40,15 @@ pub struct Users{ } crud!(Users {}); +{{/if}} +{{#if is_mongodb}} +use mongodb::bson::oid::ObjectId; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize)] +pub struct User { + _id: Option, + pub username: String, + pub password: String, +} {{/if}} \ No newline at end of file diff --git a/src/template/src/main_template.hbs b/src/template/src/main_template.hbs index 92c9939..7284b80 100644 --- a/src/template/src/main_template.hbs +++ b/src/template/src/main_template.hbs @@ -4,6 +4,9 @@ use crate::db::init_db_conn; {{#if is_rbatis}} use crate::db::init_db_conn; {{/if}} +{{#if is_mongodb}} +use crate::db::init_db_conn; +{{/if}} use crate::middleware::handle_404::handle_404; use crate::routers::router; use config::{CERT_KEY, CFG}; @@ -27,6 +30,9 @@ mod utils; {{#if is_sea_orm_or_sqlx}} mod entities; {{/if}} +{{#if is_mongodb}} +mod entities; +{{/if}} {{#if is_diesel}} mod models; mod schema; @@ -47,6 +53,9 @@ async fn main() { {{#if is_rbatis}} init_db_conn().await; {{/if}} + {{#if is_mongodb}} + init_db_conn().await; + {{/if}} let (tx, rx) = oneshot::channel(); let router = router(); let service: Service = router.into(); diff --git a/src/template/src/services/user.hbs b/src/template/src/services/user.hbs index b1a729f..e3a5b1e 100644 --- a/src/template/src/services/user.hbs +++ b/src/template/src/services/user.hbs @@ -198,7 +198,7 @@ pub async fn update_user(req: UserUpdateRequest) -> AppResult { } pub async fn delete_user(id: String) -> AppResult<()> { - let db = DB.get().ok_or(anyhow::anyhow!("数据库连接失败"))?; + let db = DB.get().ok_or(anyhow::anyhow!("{{database_connection_failed}}"))?; User::delete_by_id(id).exec(db).await?; Ok(()) } @@ -364,7 +364,7 @@ use crate::{ }; pub async fn add_user(req: UserAddRequest) -> AppResult { - let db = DB.get().ok_or(anyhow::anyhow!("数据库连接失败"))?; + let db = DB.get().ok_or(anyhow::anyhow!("{{database_connection_failed}}"))?; let user = Users { id: Uuid::new_v4().to_string(), username: req.username.clone(), @@ -379,13 +379,13 @@ pub async fn add_user(req: UserAddRequest) -> AppResult { } pub async fn login(req: UserLoginRequest) -> AppResult { - let db = DB.get().ok_or(anyhow::anyhow!("数据库连接失败"))?; + let db = DB.get().ok_or(anyhow::anyhow!("{{database_connection_failed}}"))?; let user = Users::select_by_column(db,"username",&req.username).await?; if user.len()==0 { - return Err(anyhow::anyhow!("用户不存在").into()); + return Err(anyhow::anyhow!("{{user_does_not_exist}}").into()); } if rand_utils::verify_password(req.password, user[0].password.clone()).await.is_err() { - return Err(anyhow::anyhow!("密码不正确").into()); + return Err(anyhow::anyhow!("{{incorrect_password}}").into()); } let (token, exp) = get_token(user[0].username.clone(), user[0].id.clone())?; let res = UserLoginResponse { @@ -398,10 +398,10 @@ pub async fn login(req: UserLoginRequest) -> AppResult { } pub async fn update_user(req: UserUpdateRequest) -> AppResult { - let db = DB.get().ok_or(anyhow::anyhow!("数据库连接失败"))?; + let db = DB.get().ok_or(anyhow::anyhow!("{{database_connection_failed}}"))?; let users = Users::select_by_column(db,"id",&req.id).await?; if users.len()==0 { - return Err(anyhow::anyhow!("用户不存在").into()); + return Err(anyhow::anyhow!("{{user_does_not_exist}}").into()); } let user = Users { id: users[0].clone().id, @@ -416,13 +416,13 @@ pub async fn update_user(req: UserUpdateRequest) -> AppResult { } pub async fn delete_user(req: String) -> AppResult<()> { - let db = DB.get().ok_or(anyhow::anyhow!("数据库连接失败"))?; + let db = DB.get().ok_or(anyhow::anyhow!("{{database_connection_failed}}"))?; Users::delete_by_column(db, "id",&req).await?; Ok(()) } pub async fn users() -> AppResult> { - let db = DB.get().ok_or(anyhow::anyhow!("数据库连接失败"))?; + let db = DB.get().ok_or(anyhow::anyhow!("{{database_connection_failed}}"))?; let users=Users::select_all(db).await?; let res = users @@ -434,4 +434,124 @@ pub async fn users() -> AppResult> { .collect::>(); Ok(res) } -{{/if}} \ No newline at end of file +{{/if}} +{{#if is_mongodb}} +use std::str::FromStr; +use futures_util::StreamExt; +use mongodb::{ + bson::{doc, oid::ObjectId, Document}, + Collection, +}; +use crate::{ + app_error::AppResult, + db::{COLL_NAME, DB_NAME, MONGODB_CLIENT}, + dtos::user::{ + UserAddRequest, UserLoginRequest, UserLoginResponse, UserResponse, UserUpdateRequest, + }, + middleware::jwt::get_token, + utils::rand_utils, +}; + +pub async fn add_user(req: UserAddRequest) -> AppResult { + let db = MONGODB_CLIENT + .get() + .ok_or(anyhow::anyhow!("{{database_connection_failed}}"))?; + let coll_users = db.database(DB_NAME).collection::(COLL_NAME); + + let user = doc! { + "username": req.username, + "password": rand_utils::hash_password(req.password).await? + }; + coll_users.insert_one(user.clone(), None).await?; + let user = coll_users + .find_one(user, None) + .await? + .ok_or(anyhow::anyhow!("{{user_does_not_exist}}"))?; + Ok(UserResponse { + id: user.get_object_id("_id")?.to_string(), + username: user.get_str("username")?.to_string(), + }) +} + +pub async fn login(req: UserLoginRequest) -> AppResult { + let db = MONGODB_CLIENT + .get() + .ok_or(anyhow::anyhow!("{{database_connection_failed}}"))?; + let coll_users: Collection = db.database(DB_NAME).collection(COLL_NAME); + + let user_doc = coll_users + .find_one(doc! { "username": req.username }, None) + .await?; + + let user = match user_doc { + Some(user) => user, + None => return Err(anyhow::anyhow!("{{user_does_not_exist}}").into()), + }; + + if rand_utils::verify_password(req.password, user.get_str("password")?.to_string()) + .await + .is_err() + { + return Err(anyhow::anyhow!("{{incorrect_password}}").into()); + } + let (token, exp) = get_token( + user.get_str("username")?.to_string(), + user.get_object_id("_id")?.to_string(), + )?; + + Ok(UserLoginResponse { + id: user.get_object_id("_id")?.to_string(), + username: user.get_str("username")?.to_owned(), + token, + exp, + }) +} + +pub async fn update_user(req: UserUpdateRequest) -> AppResult { + let db = MONGODB_CLIENT + .get() + .ok_or(anyhow::anyhow!("{{database_connection_failed}}"))?; + let coll_users: Collection = db.database(DB_NAME).collection(COLL_NAME); + let id = ObjectId::from_str(&req.id.clone())?; + let hash_password = rand_utils::hash_password(req.password).await?; + coll_users + .update_one( + doc! { "_id": id }, + doc! { "$set": { "username": req.username.clone(),"password": hash_password } }, + None, + ) + .await?; + Ok(UserResponse { + id: req.id.clone(), + username: req.username, + }) +} + +pub async fn delete_user(req: String) -> AppResult<()> { + let db = MONGODB_CLIENT + .get() + .ok_or(anyhow::anyhow!("{{database_connection_failed}}"))?; + let coll_users: Collection = db.database(DB_NAME).collection(COLL_NAME); + let id = ObjectId::from_str(&req)?; + + coll_users.delete_one(doc! { "_id": id }, None).await?; + Ok(()) +} + +pub async fn users() -> AppResult> { + let db = MONGODB_CLIENT + .get() + .ok_or(anyhow::anyhow!("{{database_connection_failed}}"))?; + let coll_users: Collection = db.database(DB_NAME).collection(COLL_NAME); + + let mut cursor = coll_users.find(None, None).await?; + let mut users = Vec::new(); + while let Some(result) = cursor.next().await { + let document = result?; + let id = document.get_object_id("_id")?.to_string(); + let username = document.get_str("username")?.to_owned(); + users.push(UserResponse { id, username }); + } + Ok(users) +} +{{/if}} diff --git a/src/utils/create_project.rs b/src/utils/create_project.rs index e57155f..743ac3b 100644 --- a/src/utils/create_project.rs +++ b/src/utils/create_project.rs @@ -83,6 +83,10 @@ fn after_print_info(project_name: &String, config: UserSelected) { } _ => {} }, + DbConnectionType::Mongodb => { + success(t!("mongodb_usage_import_user_data").replace(r"\n", "\n")); + success(t!("access_instructions").replace(r"\n", "\n")); + } _ => {} } } @@ -99,6 +103,7 @@ fn write_project_file( let is_sea_orm = user_selected.db_conn_type == DbConnectionType::SeaOrm; let is_diesel = user_selected.db_conn_type == DbConnectionType::Diesel; let is_rbatis = user_selected.db_conn_type == DbConnectionType::Rbatis; + let is_mongodb = user_selected.db_conn_type == DbConnectionType::Mongodb; let is_mysql = user_selected.db_type == DbType::Mysql; let is_postgres = user_selected.db_type == DbType::Postgres; let is_sqlite = user_selected.db_type == DbType::Sqlite; @@ -136,6 +141,7 @@ fn write_project_file( "is_sea_orm_or_sqlx":is_sea_orm_or_sqlx, "is_diesel":is_diesel, "is_rbatis":is_rbatis, + "is_mongodb":is_mongodb, "main_log_message":t!("main_log_message"), "config_error_no_exits":t!("config_error_no_exits"), "config_error_read":t!("config_error_read"), @@ -505,6 +511,31 @@ fn write_project_file( } } } + if is_mongodb { + //src/entities + let entities_path = src_path.join("entities"); + std::fs::create_dir_all(&entities_path)?; + //src/entities/mod.rs + let entities_mod_template = include_str!("../template/src/entities/mod.hbs"); + let entities_mod_rendered = handlebars.render_template(entities_mod_template, &data)?; + let mut entities_mod_file = File::create(entities_path.join("mod.rs"))?; + entities_mod_file.write_all(entities_mod_rendered.as_bytes())?; + + //src/entities/user.rs + let entities_user_template = include_str!("../template/src/entities/user.hbs"); + let entities_user_rendered = + handlebars.render_template(entities_user_template, &data)?; + let mut entities_user_file = File::create(entities_path.join("user.rs"))?; + entities_user_file.write_all(entities_user_rendered.as_bytes())?; + + //data + let data_path = project_path.join("data"); + std::fs::create_dir_all(&data_path)?; + //data/users.json + let users_json_bytes = include_bytes!("../template/data/users.json"); + let mut users_json_file = File::create(data_path.join("users.json"))?; + users_json_file.write_all(users_json_bytes)?; + } } Ok(()) } @@ -696,6 +727,12 @@ fn handle_dependencies( "features": ["debug_mode"] }); } + (DbConnectionType::Mongodb, _) => { + dependencies["mongodb"] = json!({"version":"2.0"}); + dependencies["futures-util"] = json!({ + "version": "0.3", + }); + } _ => {} } //add uuid dependency diff --git a/src/utils/get_selection.rs b/src/utils/get_selection.rs index 8517102..0f75ef2 100644 --- a/src/utils/get_selection.rs +++ b/src/utils/get_selection.rs @@ -37,6 +37,7 @@ pub fn get_user_selected() -> Result> { t!("db_conn_types_sea_orm"), t!("db_conn_types_diesel"), t!("db_conn_types_rbatis"), + t!("db_conn_types_mongodb"), t!("db_conn_types_nothing"), // "custom", ]; @@ -45,16 +46,16 @@ pub fn get_user_selected() -> Result> { .default(0) .items(&db_conn_types[..]) .interact()?; - let db_conn_type = match db_conn_type_selection { 0 => DbConnectionType::Sqlx, 1 => DbConnectionType::SeaOrm, 2 => DbConnectionType::Diesel, 3 => DbConnectionType::Rbatis, - 4 => DbConnectionType::Nothing, + 4 => DbConnectionType::Mongodb, + 5 => DbConnectionType::Nothing, _ => anyhow::bail!("Invalid db connection type selection"), }; - if db_conn_type == DbConnectionType::Nothing { + if db_conn_type == DbConnectionType::Nothing || db_conn_type == DbConnectionType::Mongodb { return Ok(Some(UserSelected { template_type, db_type: DbType::Sqlite, @@ -104,5 +105,6 @@ pub enum DbConnectionType { SeaOrm, Diesel, Rbatis, + Mongodb, Nothing, }