From f8af015e3f1d8717f0e93a3a6ea02088e9003bf3 Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Tue, 19 Jul 2022 17:50:12 +0800 Subject: [PATCH 01/31] Move core implementations to a standalone crate --- examples/rocket_example/Cargo.toml | 13 +--- examples/rocket_example/core/Cargo.toml | 19 ++++++ examples/rocket_example/core/src/lib.rs | 7 +++ examples/rocket_example/core/src/mutation.rs | 53 +++++++++++++++++ examples/rocket_example/core/src/query.rs | 26 ++++++++ examples/rocket_example/src/main.rs | 62 ++++++-------------- examples/rocket_example/src/pool.rs | 2 + 7 files changed, 127 insertions(+), 55 deletions(-) create mode 100644 examples/rocket_example/core/Cargo.toml create mode 100644 examples/rocket_example/core/src/lib.rs create mode 100644 examples/rocket_example/core/src/mutation.rs create mode 100644 examples/rocket_example/core/src/query.rs diff --git a/examples/rocket_example/Cargo.toml b/examples/rocket_example/Cargo.toml index 49cc2856b..417f3490d 100644 --- a/examples/rocket_example/Cargo.toml +++ b/examples/rocket_example/Cargo.toml @@ -6,11 +6,12 @@ edition = "2021" publish = false [workspace] -members = [".", "entity", "migration"] +members = [".", "core", "entity", "migration"] [dependencies] async-stream = { version = "^0.3" } async-trait = { version = "0.1" } +rocket-example-core = { path = "core" } futures = { version = "^0.3" } futures-util = { version = "^0.3" } rocket = { version = "0.5.0-rc.1", features = [ @@ -26,13 +27,3 @@ migration = { path = "migration" } [dependencies.sea-orm-rocket] path = "../../sea-orm-rocket/lib" # remove this line in your own project and use the git line # git = "https://github.com/SeaQL/sea-orm" - -[dependencies.sea-orm] -path = "../../" # remove this line in your own project -version = "^0.9.0" # sea-orm version -features = [ - "runtime-tokio-native-tls", - "sqlx-postgres", - # "sqlx-mysql", - # "sqlx-sqlite", -] diff --git a/examples/rocket_example/core/Cargo.toml b/examples/rocket_example/core/Cargo.toml new file mode 100644 index 000000000..a180ef549 --- /dev/null +++ b/examples/rocket_example/core/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "rocket-example-core" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +entity = { path = "../entity" } + +[dependencies.sea-orm] +path = "../../../" # remove this line in your own project +version = "^0.9.0" # sea-orm version +features = [ + "runtime-tokio-native-tls", + "sqlx-postgres", + # "sqlx-mysql", + # "sqlx-sqlite", +] diff --git a/examples/rocket_example/core/src/lib.rs b/examples/rocket_example/core/src/lib.rs new file mode 100644 index 000000000..4a80f2391 --- /dev/null +++ b/examples/rocket_example/core/src/lib.rs @@ -0,0 +1,7 @@ +mod mutation; +mod query; + +pub use mutation::*; +pub use query::*; + +pub use sea_orm; diff --git a/examples/rocket_example/core/src/mutation.rs b/examples/rocket_example/core/src/mutation.rs new file mode 100644 index 000000000..7f0150a63 --- /dev/null +++ b/examples/rocket_example/core/src/mutation.rs @@ -0,0 +1,53 @@ +use ::entity::{post, post::Entity as Post}; +use sea_orm::*; + +pub struct Mutation; + +impl Mutation { + pub async fn create_post( + db: &DbConn, + form_data: post::Model, + ) -> Result { + post::ActiveModel { + title: Set(form_data.title.to_owned()), + text: Set(form_data.text.to_owned()), + ..Default::default() + } + .save(db) + .await + } + + pub async fn update_post_by_id( + db: &DbConn, + id: i32, + form_data: post::Model, + ) -> Result { + let post: post::ActiveModel = if let Ok(Some(post)) = Post::find_by_id(id).one(db).await { + post.into() + } else { + return Err(DbErr::Custom("Cannot find post.".to_owned())); + }; + + post::ActiveModel { + id: post.id, + title: Set(form_data.title.to_owned()), + text: Set(form_data.text.to_owned()), + } + .update(db) + .await + } + + pub async fn delete_post(db: &DbConn, id: i32) -> Result { + let post: post::ActiveModel = if let Ok(Some(post)) = Post::find_by_id(id).one(db).await { + post.into() + } else { + return Err(DbErr::Custom("Cannot find post.".to_owned())); + }; + + post.delete(db).await + } + + pub async fn delete_all_posts(db: &DbConn) -> Result { + Post::delete_many().exec(db).await + } +} diff --git a/examples/rocket_example/core/src/query.rs b/examples/rocket_example/core/src/query.rs new file mode 100644 index 000000000..f08e80c77 --- /dev/null +++ b/examples/rocket_example/core/src/query.rs @@ -0,0 +1,26 @@ +use ::entity::{post, post::Entity as Post}; +use sea_orm::*; + +pub struct Query; + +impl Query { + pub async fn find_post_by_id(db: &DbConn, id: i32) -> Result, DbErr> { + Post::find_by_id(id).one(db).await + } + + /// If ok, returns (post models, num pages). + pub async fn find_posts_in_page( + db: &DbConn, + page: usize, + posts_per_page: usize, + ) -> Result<(Vec, usize), DbErr> { + // Setup paginator + let paginator = Post::find() + .order_by_asc(post::Column::Id) + .paginate(db, posts_per_page); + let num_pages = paginator.num_pages().await?; + + // Fetch paginated posts + paginator.fetch_page(page - 1).await.map(|p| (p, num_pages)) + } +} diff --git a/examples/rocket_example/src/main.rs b/examples/rocket_example/src/main.rs index fed44f96a..d5fc27c82 100644 --- a/examples/rocket_example/src/main.rs +++ b/examples/rocket_example/src/main.rs @@ -8,10 +8,10 @@ use rocket::request::FlashMessage; use rocket::response::{Flash, Redirect}; use rocket::{Build, Request, Rocket}; use rocket_dyn_templates::Template; +use rocket_example_core::{Mutation, Query}; use serde_json::json; use migration::MigratorTrait; -use sea_orm::{entity::*, query::*}; use sea_orm_rocket::{Connection, Database}; mod pool; @@ -33,14 +33,9 @@ async fn create(conn: Connection<'_, Db>, post_form: Form) -> Flash let form = post_form.into_inner(); - post::ActiveModel { - title: Set(form.title.to_owned()), - text: Set(form.text.to_owned()), - ..Default::default() - } - .save(db) - .await - .expect("could not insert post"); + Mutation::create_post(db, form) + .await + .expect("could not insert post"); Flash::success(Redirect::to("/"), "Post successfully added.") } @@ -53,26 +48,11 @@ async fn update( ) -> Flash { let db = conn.into_inner(); - let post: post::ActiveModel = Post::find_by_id(id).one(db).await.unwrap().unwrap().into(); - let form = post_form.into_inner(); - db.transaction::<_, (), sea_orm::DbErr>(|txn| { - Box::pin(async move { - post::ActiveModel { - id: post.id, - title: Set(form.title.to_owned()), - text: Set(form.text.to_owned()), - } - .save(txn) - .await - .expect("could not edit post"); - - Ok(()) - }) - }) - .await - .unwrap(); + Mutation::update_post_by_id(db, id, form) + .await + .expect("could not update post"); Flash::success(Redirect::to("/"), "Post successfully edited.") } @@ -93,17 +73,9 @@ async fn list( panic!("Page number cannot be zero"); } - // Setup paginator - let paginator = Post::find() - .order_by_asc(post::Column::Id) - .paginate(db, posts_per_page); - let num_pages = paginator.num_pages().await.ok().unwrap(); - - // Fetch paginated posts - let posts = paginator - .fetch_page(page - 1) + let (posts, num_pages) = Query::find_posts_in_page(db, page, posts_per_page) .await - .expect("could not retrieve posts"); + .expect("Cannot find posts in page"); Template::render( "index", @@ -121,8 +93,7 @@ async fn list( async fn edit(conn: Connection<'_, Db>, id: i32) -> Template { let db = conn.into_inner(); - let post: Option = Post::find_by_id(id) - .one(db) + let post: Option = Query::find_post_by_id(db, id) .await .expect("could not find post"); @@ -138,18 +109,21 @@ async fn edit(conn: Connection<'_, Db>, id: i32) -> Template { async fn delete(conn: Connection<'_, Db>, id: i32) -> Flash { let db = conn.into_inner(); - let post: post::ActiveModel = Post::find_by_id(id).one(db).await.unwrap().unwrap().into(); - - post.delete(db).await.unwrap(); + Mutation::delete_post(db, id) + .await + .expect("could not delete post"); Flash::success(Redirect::to("/"), "Post successfully deleted.") } #[delete("/")] -async fn destroy(conn: Connection<'_, Db>) -> Result<(), rocket::response::Debug> { +async fn destroy(conn: Connection<'_, Db>) -> Result<(), rocket::response::Debug> { let db = conn.into_inner(); - Post::delete_many().exec(db).await.unwrap(); + Mutation::delete_all_posts(db) + .await + .map_err(|e| e.to_string())?; + Ok(()) } diff --git a/examples/rocket_example/src/pool.rs b/examples/rocket_example/src/pool.rs index fe5f8c6eb..b1c056779 100644 --- a/examples/rocket_example/src/pool.rs +++ b/examples/rocket_example/src/pool.rs @@ -1,3 +1,5 @@ +use rocket_example_core::sea_orm; + use async_trait::async_trait; use sea_orm::ConnectOptions; use sea_orm_rocket::{rocket::figment::Figment, Config, Database}; From 784e376c7c001ea9e6e318c66a3193b6c1fdb2be Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Tue, 19 Jul 2022 18:34:00 +0800 Subject: [PATCH 02/31] Set up integration test skeleton in `core` --- examples/rocket_example/core/Cargo.toml | 4 ++++ examples/rocket_example/core/tests/mock.rs | 4 ++++ 2 files changed, 8 insertions(+) create mode 100644 examples/rocket_example/core/tests/mock.rs diff --git a/examples/rocket_example/core/Cargo.toml b/examples/rocket_example/core/Cargo.toml index a180ef549..bc2565782 100644 --- a/examples/rocket_example/core/Cargo.toml +++ b/examples/rocket_example/core/Cargo.toml @@ -17,3 +17,7 @@ features = [ # "sqlx-mysql", # "sqlx-sqlite", ] + +[[test]] +name = "mock" +required-features = ["sea-orm/mock"] diff --git a/examples/rocket_example/core/tests/mock.rs b/examples/rocket_example/core/tests/mock.rs new file mode 100644 index 000000000..c945152d3 --- /dev/null +++ b/examples/rocket_example/core/tests/mock.rs @@ -0,0 +1,4 @@ +#[test] +fn test_add() { + assert_eq!(3 + 2 + 1, 5); +} From 83e662dd5ab1595932af4c99681afcdaba34348d Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Tue, 19 Jul 2022 19:18:45 +0800 Subject: [PATCH 03/31] Demonstrate mock testing with query --- examples/rocket_example/core/Cargo.toml | 3 +++ examples/rocket_example/core/tests/mock.rs | 23 ++++++++++++++++--- examples/rocket_example/core/tests/prepare.rs | 21 +++++++++++++++++ 3 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 examples/rocket_example/core/tests/prepare.rs diff --git a/examples/rocket_example/core/Cargo.toml b/examples/rocket_example/core/Cargo.toml index bc2565782..79226dc8d 100644 --- a/examples/rocket_example/core/Cargo.toml +++ b/examples/rocket_example/core/Cargo.toml @@ -18,6 +18,9 @@ features = [ # "sqlx-sqlite", ] +[dev-dependencies] +tokio = "1.20.0" + [[test]] name = "mock" required-features = ["sea-orm/mock"] diff --git a/examples/rocket_example/core/tests/mock.rs b/examples/rocket_example/core/tests/mock.rs index c945152d3..2331d68a8 100644 --- a/examples/rocket_example/core/tests/mock.rs +++ b/examples/rocket_example/core/tests/mock.rs @@ -1,4 +1,21 @@ -#[test] -fn test_add() { - assert_eq!(3 + 2 + 1, 5); +mod prepare; + +use prepare::prepare_mock_db; +use rocket_example_core::Query; + +#[tokio::test] +async fn main() { + let db = &prepare_mock_db(); + + { + let post = Query::find_post_by_id(db, 1).await.unwrap().unwrap(); + + assert_eq!(post.id, 1); + } + + { + let post = Query::find_post_by_id(db, 5).await.unwrap().unwrap(); + + assert_eq!(post.id, 5); + } } diff --git a/examples/rocket_example/core/tests/prepare.rs b/examples/rocket_example/core/tests/prepare.rs new file mode 100644 index 000000000..772e25cb8 --- /dev/null +++ b/examples/rocket_example/core/tests/prepare.rs @@ -0,0 +1,21 @@ +use ::entity::post; +use sea_orm::*; + +pub fn prepare_mock_db() -> DatabaseConnection { + MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results(vec![ + // First query result + vec![post::Model { + id: 1, + title: "Title A".to_owned(), + text: "Text A".to_owned(), + }], + // Second query result + vec![post::Model { + id: 5, + title: "Title C".to_owned(), + text: "Text C".to_owned(), + }], + ]) + .into_connection() +} From 8946d79318ea0b5469f34a369281ec0ec64f97df Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Thu, 21 Jul 2022 15:13:52 +0800 Subject: [PATCH 04/31] Move Rocket api code to a standalone crate --- examples/rocket_example/Cargo.toml | 21 +-- examples/rocket_example/README.md | 4 +- examples/rocket_example/api/Cargo.toml | 26 +++ examples/rocket_example/{ => api}/Rocket.toml | 0 examples/rocket_example/api/src/lib.rs | 168 ++++++++++++++++++ examples/rocket_example/{ => api}/src/pool.rs | 0 .../{ => api}/static/css/normalize.css | 0 .../{ => api}/static/css/skeleton.css | 0 .../{ => api}/static/css/style.css | 0 .../{ => api}/static/images/favicon.png | Bin .../{ => api}/templates/base.html.tera | 0 .../{ => api}/templates/edit.html.tera | 0 .../{ => api}/templates/error/404.html.tera | 0 .../{ => api}/templates/index.html.tera | 0 .../{ => api}/templates/new.html.tera | 0 examples/rocket_example/src/main.rs | 159 +---------------- 16 files changed, 200 insertions(+), 178 deletions(-) create mode 100644 examples/rocket_example/api/Cargo.toml rename examples/rocket_example/{ => api}/Rocket.toml (100%) create mode 100644 examples/rocket_example/api/src/lib.rs rename examples/rocket_example/{ => api}/src/pool.rs (100%) rename examples/rocket_example/{ => api}/static/css/normalize.css (100%) rename examples/rocket_example/{ => api}/static/css/skeleton.css (100%) rename examples/rocket_example/{ => api}/static/css/style.css (100%) rename examples/rocket_example/{ => api}/static/images/favicon.png (100%) rename examples/rocket_example/{ => api}/templates/base.html.tera (100%) rename examples/rocket_example/{ => api}/templates/edit.html.tera (100%) rename examples/rocket_example/{ => api}/templates/error/404.html.tera (100%) rename examples/rocket_example/{ => api}/templates/index.html.tera (100%) rename examples/rocket_example/{ => api}/templates/new.html.tera (100%) diff --git a/examples/rocket_example/Cargo.toml b/examples/rocket_example/Cargo.toml index 417f3490d..992ad0360 100644 --- a/examples/rocket_example/Cargo.toml +++ b/examples/rocket_example/Cargo.toml @@ -6,24 +6,7 @@ edition = "2021" publish = false [workspace] -members = [".", "core", "entity", "migration"] +members = [".", "api", "core", "entity", "migration"] [dependencies] -async-stream = { version = "^0.3" } -async-trait = { version = "0.1" } -rocket-example-core = { path = "core" } -futures = { version = "^0.3" } -futures-util = { version = "^0.3" } -rocket = { version = "0.5.0-rc.1", features = [ - "json", -] } -rocket_dyn_templates = { version = "0.1.0-rc.1", features = [ - "tera", -] } -serde_json = { version = "^1" } -entity = { path = "entity" } -migration = { path = "migration" } - -[dependencies.sea-orm-rocket] -path = "../../sea-orm-rocket/lib" # remove this line in your own project and use the git line -# git = "https://github.com/SeaQL/sea-orm" +rocket-example-api = { path = "api" } diff --git a/examples/rocket_example/README.md b/examples/rocket_example/README.md index a1e3af0fa..e89b29c82 100644 --- a/examples/rocket_example/README.md +++ b/examples/rocket_example/README.md @@ -2,9 +2,9 @@ # Rocket with SeaORM example app -1. Modify the `url` var in `Rocket.toml` to point to your chosen database +1. Modify the `url` var in `api/Rocket.toml` to point to your chosen database -1. Turn on the appropriate database feature for your chosen db in `Cargo.toml` (the `"sqlx-postgres",` line) +1. Turn on the appropriate database feature for your chosen db in `core/Cargo.toml` (the `"sqlx-postgres",` line) 1. Execute `cargo run` to start the server diff --git a/examples/rocket_example/api/Cargo.toml b/examples/rocket_example/api/Cargo.toml new file mode 100644 index 000000000..e0d718d8e --- /dev/null +++ b/examples/rocket_example/api/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "rocket-example-api" +version = "0.1.0" +authors = ["Sam Samai "] +edition = "2021" +publish = false + +[dependencies] +async-stream = { version = "^0.3" } +async-trait = { version = "0.1" } +rocket-example-core = { path = "../core" } +futures = { version = "^0.3" } +futures-util = { version = "^0.3" } +rocket = { version = "0.5.0-rc.1", features = [ + "json", +] } +rocket_dyn_templates = { version = "0.1.0-rc.1", features = [ + "tera", +] } +serde_json = { version = "^1" } +entity = { path = "../entity" } +migration = { path = "../migration" } + +[dependencies.sea-orm-rocket] +path = "../../../sea-orm-rocket/lib" # remove this line in your own project and use the git line +# git = "https://github.com/SeaQL/sea-orm" diff --git a/examples/rocket_example/Rocket.toml b/examples/rocket_example/api/Rocket.toml similarity index 100% rename from examples/rocket_example/Rocket.toml rename to examples/rocket_example/api/Rocket.toml diff --git a/examples/rocket_example/api/src/lib.rs b/examples/rocket_example/api/src/lib.rs new file mode 100644 index 000000000..14280c11e --- /dev/null +++ b/examples/rocket_example/api/src/lib.rs @@ -0,0 +1,168 @@ +#[macro_use] +extern crate rocket; + +use futures::executor::block_on; +use rocket::fairing::{self, AdHoc}; +use rocket::form::{Context, Form}; +use rocket::fs::{relative, FileServer}; +use rocket::request::FlashMessage; +use rocket::response::{Flash, Redirect}; +use rocket::{Build, Request, Rocket}; +use rocket_dyn_templates::Template; +use rocket_example_core::{Mutation, Query}; +use serde_json::json; + +use migration::MigratorTrait; +use sea_orm_rocket::{Connection, Database}; + +mod pool; +use pool::Db; + +pub use entity::post; +pub use entity::post::Entity as Post; + +const DEFAULT_POSTS_PER_PAGE: usize = 5; + +#[get("/new")] +async fn new() -> Template { + Template::render("new", &Context::default()) +} + +#[post("/", data = "")] +async fn create(conn: Connection<'_, Db>, post_form: Form) -> Flash { + let db = conn.into_inner(); + + let form = post_form.into_inner(); + + Mutation::create_post(db, form) + .await + .expect("could not insert post"); + + Flash::success(Redirect::to("/"), "Post successfully added.") +} + +#[post("/", data = "")] +async fn update( + conn: Connection<'_, Db>, + id: i32, + post_form: Form, +) -> Flash { + let db = conn.into_inner(); + + let form = post_form.into_inner(); + + Mutation::update_post_by_id(db, id, form) + .await + .expect("could not update post"); + + Flash::success(Redirect::to("/"), "Post successfully edited.") +} + +#[get("/?&")] +async fn list( + conn: Connection<'_, Db>, + page: Option, + posts_per_page: Option, + flash: Option>, +) -> Template { + let db = conn.into_inner(); + + // Set page number and items per page + let page = page.unwrap_or(1); + let posts_per_page = posts_per_page.unwrap_or(DEFAULT_POSTS_PER_PAGE); + if page == 0 { + panic!("Page number cannot be zero"); + } + + let (posts, num_pages) = Query::find_posts_in_page(db, page, posts_per_page) + .await + .expect("Cannot find posts in page"); + + Template::render( + "index", + json! ({ + "page": page, + "posts_per_page": posts_per_page, + "num_pages": num_pages, + "posts": posts, + "flash": flash.map(FlashMessage::into_inner), + }), + ) +} + +#[get("/")] +async fn edit(conn: Connection<'_, Db>, id: i32) -> Template { + let db = conn.into_inner(); + + let post: Option = Query::find_post_by_id(db, id) + .await + .expect("could not find post"); + + Template::render( + "edit", + json! ({ + "post": post, + }), + ) +} + +#[delete("/")] +async fn delete(conn: Connection<'_, Db>, id: i32) -> Flash { + let db = conn.into_inner(); + + Mutation::delete_post(db, id) + .await + .expect("could not delete post"); + + Flash::success(Redirect::to("/"), "Post successfully deleted.") +} + +#[delete("/")] +async fn destroy(conn: Connection<'_, Db>) -> Result<(), rocket::response::Debug> { + let db = conn.into_inner(); + + Mutation::delete_all_posts(db) + .await + .map_err(|e| e.to_string())?; + + Ok(()) +} + +#[catch(404)] +pub fn not_found(req: &Request<'_>) -> Template { + Template::render( + "error/404", + json! ({ + "uri": req.uri() + }), + ) +} + +async fn run_migrations(rocket: Rocket) -> fairing::Result { + let conn = &Db::fetch(&rocket).unwrap().conn; + let _ = migration::Migrator::up(conn, None).await; + Ok(rocket) +} + +fn rocket() -> Rocket { + rocket::build() + .attach(Db::init()) + .attach(AdHoc::try_on_ignite("Migrations", run_migrations)) + .mount("/", FileServer::from(relative!("/static"))) + .mount( + "/", + routes![new, create, delete, destroy, list, edit, update], + ) + .register("/", catchers![not_found]) + .attach(Template::fairing()) +} + +pub fn main() { + let result = block_on(rocket().launch()); + + println!("Rocket: deorbit."); + + if let Some(err) = result.err() { + println!("Error: {}", err); + } +} diff --git a/examples/rocket_example/src/pool.rs b/examples/rocket_example/api/src/pool.rs similarity index 100% rename from examples/rocket_example/src/pool.rs rename to examples/rocket_example/api/src/pool.rs diff --git a/examples/rocket_example/static/css/normalize.css b/examples/rocket_example/api/static/css/normalize.css similarity index 100% rename from examples/rocket_example/static/css/normalize.css rename to examples/rocket_example/api/static/css/normalize.css diff --git a/examples/rocket_example/static/css/skeleton.css b/examples/rocket_example/api/static/css/skeleton.css similarity index 100% rename from examples/rocket_example/static/css/skeleton.css rename to examples/rocket_example/api/static/css/skeleton.css diff --git a/examples/rocket_example/static/css/style.css b/examples/rocket_example/api/static/css/style.css similarity index 100% rename from examples/rocket_example/static/css/style.css rename to examples/rocket_example/api/static/css/style.css diff --git a/examples/rocket_example/static/images/favicon.png b/examples/rocket_example/api/static/images/favicon.png similarity index 100% rename from examples/rocket_example/static/images/favicon.png rename to examples/rocket_example/api/static/images/favicon.png diff --git a/examples/rocket_example/templates/base.html.tera b/examples/rocket_example/api/templates/base.html.tera similarity index 100% rename from examples/rocket_example/templates/base.html.tera rename to examples/rocket_example/api/templates/base.html.tera diff --git a/examples/rocket_example/templates/edit.html.tera b/examples/rocket_example/api/templates/edit.html.tera similarity index 100% rename from examples/rocket_example/templates/edit.html.tera rename to examples/rocket_example/api/templates/edit.html.tera diff --git a/examples/rocket_example/templates/error/404.html.tera b/examples/rocket_example/api/templates/error/404.html.tera similarity index 100% rename from examples/rocket_example/templates/error/404.html.tera rename to examples/rocket_example/api/templates/error/404.html.tera diff --git a/examples/rocket_example/templates/index.html.tera b/examples/rocket_example/api/templates/index.html.tera similarity index 100% rename from examples/rocket_example/templates/index.html.tera rename to examples/rocket_example/api/templates/index.html.tera diff --git a/examples/rocket_example/templates/new.html.tera b/examples/rocket_example/api/templates/new.html.tera similarity index 100% rename from examples/rocket_example/templates/new.html.tera rename to examples/rocket_example/api/templates/new.html.tera diff --git a/examples/rocket_example/src/main.rs b/examples/rocket_example/src/main.rs index d5fc27c82..182a6875b 100644 --- a/examples/rocket_example/src/main.rs +++ b/examples/rocket_example/src/main.rs @@ -1,158 +1,3 @@ -#[macro_use] -extern crate rocket; - -use rocket::fairing::{self, AdHoc}; -use rocket::form::{Context, Form}; -use rocket::fs::{relative, FileServer}; -use rocket::request::FlashMessage; -use rocket::response::{Flash, Redirect}; -use rocket::{Build, Request, Rocket}; -use rocket_dyn_templates::Template; -use rocket_example_core::{Mutation, Query}; -use serde_json::json; - -use migration::MigratorTrait; -use sea_orm_rocket::{Connection, Database}; - -mod pool; -use pool::Db; - -pub use entity::post; -pub use entity::post::Entity as Post; - -const DEFAULT_POSTS_PER_PAGE: usize = 5; - -#[get("/new")] -async fn new() -> Template { - Template::render("new", &Context::default()) -} - -#[post("/", data = "")] -async fn create(conn: Connection<'_, Db>, post_form: Form) -> Flash { - let db = conn.into_inner(); - - let form = post_form.into_inner(); - - Mutation::create_post(db, form) - .await - .expect("could not insert post"); - - Flash::success(Redirect::to("/"), "Post successfully added.") -} - -#[post("/", data = "")] -async fn update( - conn: Connection<'_, Db>, - id: i32, - post_form: Form, -) -> Flash { - let db = conn.into_inner(); - - let form = post_form.into_inner(); - - Mutation::update_post_by_id(db, id, form) - .await - .expect("could not update post"); - - Flash::success(Redirect::to("/"), "Post successfully edited.") -} - -#[get("/?&")] -async fn list( - conn: Connection<'_, Db>, - page: Option, - posts_per_page: Option, - flash: Option>, -) -> Template { - let db = conn.into_inner(); - - // Set page number and items per page - let page = page.unwrap_or(1); - let posts_per_page = posts_per_page.unwrap_or(DEFAULT_POSTS_PER_PAGE); - if page == 0 { - panic!("Page number cannot be zero"); - } - - let (posts, num_pages) = Query::find_posts_in_page(db, page, posts_per_page) - .await - .expect("Cannot find posts in page"); - - Template::render( - "index", - json! ({ - "page": page, - "posts_per_page": posts_per_page, - "num_pages": num_pages, - "posts": posts, - "flash": flash.map(FlashMessage::into_inner), - }), - ) -} - -#[get("/")] -async fn edit(conn: Connection<'_, Db>, id: i32) -> Template { - let db = conn.into_inner(); - - let post: Option = Query::find_post_by_id(db, id) - .await - .expect("could not find post"); - - Template::render( - "edit", - json! ({ - "post": post, - }), - ) -} - -#[delete("/")] -async fn delete(conn: Connection<'_, Db>, id: i32) -> Flash { - let db = conn.into_inner(); - - Mutation::delete_post(db, id) - .await - .expect("could not delete post"); - - Flash::success(Redirect::to("/"), "Post successfully deleted.") -} - -#[delete("/")] -async fn destroy(conn: Connection<'_, Db>) -> Result<(), rocket::response::Debug> { - let db = conn.into_inner(); - - Mutation::delete_all_posts(db) - .await - .map_err(|e| e.to_string())?; - - Ok(()) -} - -#[catch(404)] -pub fn not_found(req: &Request<'_>) -> Template { - Template::render( - "error/404", - json! ({ - "uri": req.uri() - }), - ) -} - -async fn run_migrations(rocket: Rocket) -> fairing::Result { - let conn = &Db::fetch(&rocket).unwrap().conn; - let _ = migration::Migrator::up(conn, None).await; - Ok(rocket) -} - -#[launch] -fn rocket() -> _ { - rocket::build() - .attach(Db::init()) - .attach(AdHoc::try_on_ignite("Migrations", run_migrations)) - .mount("/", FileServer::from(relative!("/static"))) - .mount( - "/", - routes![new, create, delete, destroy, list, edit, update], - ) - .register("/", catchers![not_found]) - .attach(Template::fairing()) +fn main() { + rocket_example_api::main(); } From a77ce867022792623adaf3720b1a9e18e168aae6 Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Thu, 21 Jul 2022 15:56:06 +0800 Subject: [PATCH 05/31] Add mock execution --- examples/rocket_example/core/tests/mock.rs | 60 ++++++++++++++++++- examples/rocket_example/core/tests/prepare.rs | 32 +++++++++- 2 files changed, 89 insertions(+), 3 deletions(-) diff --git a/examples/rocket_example/core/tests/mock.rs b/examples/rocket_example/core/tests/mock.rs index 2331d68a8..84b187e5c 100644 --- a/examples/rocket_example/core/tests/mock.rs +++ b/examples/rocket_example/core/tests/mock.rs @@ -1,7 +1,8 @@ mod prepare; +use entity::post; use prepare::prepare_mock_db; -use rocket_example_core::Query; +use rocket_example_core::{Mutation, Query}; #[tokio::test] async fn main() { @@ -18,4 +19,61 @@ async fn main() { assert_eq!(post.id, 5); } + + { + let post = Mutation::create_post( + db, + post::Model { + id: 0, + title: "Title D".to_owned(), + text: "Text D".to_owned(), + }, + ) + .await + .unwrap(); + + assert_eq!( + post, + post::ActiveModel { + id: sea_orm::ActiveValue::Unchanged(6), + title: sea_orm::ActiveValue::Unchanged("Title D".to_owned()), + text: sea_orm::ActiveValue::Unchanged("Text D".to_owned()) + } + ); + } + + { + let post = Mutation::update_post_by_id( + db, + 1, + post::Model { + id: 1, + title: "New Title A".to_owned(), + text: "New Text A".to_owned(), + }, + ) + .await + .unwrap(); + + assert_eq!( + post, + post::Model { + id: 1, + title: "New Title A".to_owned(), + text: "New Text A".to_owned(), + } + ); + } + + { + let result = Mutation::delete_post(db, 5).await.unwrap(); + + assert_eq!(result.rows_affected, 1); + } + + { + let result = Mutation::delete_all_posts(db).await.unwrap(); + + assert_eq!(result.rows_affected, 5); + } } diff --git a/examples/rocket_example/core/tests/prepare.rs b/examples/rocket_example/core/tests/prepare.rs index 772e25cb8..1afb9af92 100644 --- a/examples/rocket_example/core/tests/prepare.rs +++ b/examples/rocket_example/core/tests/prepare.rs @@ -4,18 +4,46 @@ use sea_orm::*; pub fn prepare_mock_db() -> DatabaseConnection { MockDatabase::new(DatabaseBackend::Postgres) .append_query_results(vec![ - // First query result vec![post::Model { id: 1, title: "Title A".to_owned(), text: "Text A".to_owned(), }], - // Second query result vec![post::Model { id: 5, title: "Title C".to_owned(), text: "Text C".to_owned(), }], + vec![post::Model { + id: 6, + title: "Title D".to_owned(), + text: "Text D".to_owned(), + }], + vec![post::Model { + id: 1, + title: "Title A".to_owned(), + text: "Text A".to_owned(), + }], + vec![post::Model { + id: 1, + title: "New Title A".to_owned(), + text: "New Text A".to_owned(), + }], + vec![post::Model { + id: 5, + title: "Title C".to_owned(), + text: "Text C".to_owned(), + }], + ]) + .append_exec_results(vec![ + MockExecResult { + last_insert_id: 6, + rows_affected: 1, + }, + MockExecResult { + last_insert_id: 6, + rows_affected: 5, + }, ]) .into_connection() } From 58bb1f817347e83b48559102f26fc0c624e45457 Mon Sep 17 00:00:00 2001 From: Amit Goyani <63532626+itsAmitGoyani@users.noreply.github.com> Date: Tue, 19 Jul 2022 18:38:25 +0530 Subject: [PATCH 06/31] Add MyDataMyConsent in COMMUNITY.md (#889) * Add MyDataMyConsent in COMMUNITY.md * Update MyDataMyConsent description in COMMUNITY.md --- COMMUNITY.md | 1 + 1 file changed, 1 insertion(+) diff --git a/COMMUNITY.md b/COMMUNITY.md index b2ca420f1..058ff6292 100644 --- a/COMMUNITY.md +++ b/COMMUNITY.md @@ -6,6 +6,7 @@ If you have built an app using SeaORM and want to showcase it, feel free to open ### Startups +- [My Data My Consent](https://mydatamyconsent.com/) | Online data sharing for people and businesses simplified - [Caido](https://caido.io/) | A lightweight web security auditing toolkit - [Svix](https://www.svix.com/) ([repository](https://github.com/svix/svix-webhooks)) | The enterprise ready webhooks service - [Sensei](https://l2.technology/sensei) ([repository](https://github.com/L2-Technology/sensei)) | A Bitcoin lightning node implementation From c2ee8f6e444293c0170d7506f77fb206fa9057a4 Mon Sep 17 00:00:00 2001 From: Billy Chan Date: Tue, 19 Jul 2022 21:19:23 +0800 Subject: [PATCH 07/31] Update COMMUNITY.md Chronological order --- COMMUNITY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/COMMUNITY.md b/COMMUNITY.md index 058ff6292..408280a2a 100644 --- a/COMMUNITY.md +++ b/COMMUNITY.md @@ -6,11 +6,11 @@ If you have built an app using SeaORM and want to showcase it, feel free to open ### Startups -- [My Data My Consent](https://mydatamyconsent.com/) | Online data sharing for people and businesses simplified - [Caido](https://caido.io/) | A lightweight web security auditing toolkit - [Svix](https://www.svix.com/) ([repository](https://github.com/svix/svix-webhooks)) | The enterprise ready webhooks service - [Sensei](https://l2.technology/sensei) ([repository](https://github.com/L2-Technology/sensei)) | A Bitcoin lightning node implementation - [Spyglass](https://docs.spyglass.fyi/) ([repository](https://github.com/a5huynh/spyglass)) | 🔭 A personal search engine that indexes what you want w/ a simple set of rules. +- [My Data My Consent](https://mydatamyconsent.com/) | Online data sharing for people and businesses simplified ### Frameworks From d4819893901051530f9770750a22f1f4c01cb031 Mon Sep 17 00:00:00 2001 From: Billy Chan Date: Wed, 20 Jul 2022 16:42:59 +0800 Subject: [PATCH 08/31] [cli] bump sea-schema to 0.9.3 (SeaQL/sea-orm#876) --- sea-orm-cli/Cargo.toml | 2 +- sea-orm-migration/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sea-orm-cli/Cargo.toml b/sea-orm-cli/Cargo.toml index 0f2658f8a..c0b9bc9fa 100644 --- a/sea-orm-cli/Cargo.toml +++ b/sea-orm-cli/Cargo.toml @@ -33,7 +33,7 @@ clap = { version = "^3.2", features = ["env", "derive"] } dotenv = { version = "^0.15" } async-std = { version = "^1.9", features = [ "attributes", "tokio1" ] } sea-orm-codegen = { version = "^0.9.0", path = "../sea-orm-codegen", optional = true } -sea-schema = { version = "^0.9.2" } +sea-schema = { version = "^0.9.3" } sqlx = { version = "^0.6", default-features = false, features = [ "mysql", "postgres" ], optional = true } tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing = { version = "0.1" } diff --git a/sea-orm-migration/Cargo.toml b/sea-orm-migration/Cargo.toml index fdf1b9283..6488193cb 100644 --- a/sea-orm-migration/Cargo.toml +++ b/sea-orm-migration/Cargo.toml @@ -23,7 +23,7 @@ clap = { version = "^3.2", features = ["env", "derive"] } dotenv = { version = "^0.15" } sea-orm = { version = "^0.9.0", path = "../", default-features = false, features = ["macros"] } sea-orm-cli = { version = "^0.9.0", path = "../sea-orm-cli", default-features = false } -sea-schema = { version = "^0.9.2" } +sea-schema = { version = "^0.9.3" } tracing = { version = "0.1", features = ["log"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] } From 5a1e1e1b7f78c97b0a5fa8687b5b44eae7e3e920 Mon Sep 17 00:00:00 2001 From: Billy Chan Date: Wed, 20 Jul 2022 16:46:44 +0800 Subject: [PATCH 09/31] Update CHNAGELOG PR links --- CHANGELOG.md | 54 ++++++++++++++++++++++++++-------------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d961a8658..ae71f0ca5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### House keeping -* Remove unnecessary `async_trait` (#737) +* Remove unnecessary `async_trait` https://github.com/SeaQL/sea-orm/pull/737 ## 0.9.1 - Pending @@ -19,47 +19,47 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### New Features -* Cursor pagination (#822) -* Custom join on conditions (#793) -* `DeriveMigrationName` and `sea_orm_migration::util::get_file_stem` (#736) -* `FromJsonQueryResult` for deserializing `Json` from query result (#794) +* Cursor pagination https://github.com/SeaQL/sea-orm/pull/822 +* Custom join on conditions https://github.com/SeaQL/sea-orm/pull/793 +* `DeriveMigrationName` and `sea_orm_migration::util::get_file_stem` https://github.com/SeaQL/sea-orm/pull/736 +* `FromJsonQueryResult` for deserializing `Json` from query result https://github.com/SeaQL/sea-orm/pull/794 ### Enhancements -* Added `sqlx_logging_level` to `ConnectOptions` (#800) -* Added `num_items_and_pages` to `Paginator` (#768) -* Added `TryFromU64` for `time` (#849) -* Added `Insert::on_conflict` (#791) -* Added `QuerySelect::join_as` and `QuerySelect::join_as_rev` (#852) -* Include column name in `TryGetError::Null` (#853) -* [sea-orm-cli] Improve logging (#735) -* [sea-orm-cli] Generate enum with numeric like variants (#588) -* [sea-orm-cli] Allow old pending migration to be applied (#755) -* [sea-orm-cli] Skip generating entity for ignored tables (#837) -* [sea-orm-cli] Generate code for `time` crate (#724) -* [sea-orm-cli] Add various blob column types (#850) -* [sea-orm-cli] Generate entity files with Postgres's schema name (#422) +* Added `sqlx_logging_level` to `ConnectOptions` https://github.com/SeaQL/sea-orm/pull/800 +* Added `num_items_and_pages` to `Paginator` https://github.com/SeaQL/sea-orm/pull/768 +* Added `TryFromU64` for `time` https://github.com/SeaQL/sea-orm/pull/849 +* Added `Insert::on_conflict` https://github.com/SeaQL/sea-orm/pull/791 +* Added `QuerySelect::join_as` and `QuerySelect::join_as_rev` https://github.com/SeaQL/sea-orm/pull/852 +* Include column name in `TryGetError::Null` https://github.com/SeaQL/sea-orm/pull/853 +* [sea-orm-cli] Improve logging https://github.com/SeaQL/sea-orm/pull/735 +* [sea-orm-cli] Generate enum with numeric like variants https://github.com/SeaQL/sea-orm/pull/588 +* [sea-orm-cli] Allow old pending migration to be applied https://github.com/SeaQL/sea-orm/pull/755 +* [sea-orm-cli] Skip generating entity for ignored tables https://github.com/SeaQL/sea-orm/pull/837 +* [sea-orm-cli] Generate code for `time` crate https://github.com/SeaQL/sea-orm/pull/724 +* [sea-orm-cli] Add various blob column types https://github.com/SeaQL/sea-orm/pull/850 +* [sea-orm-cli] Generate entity files with Postgres's schema name https://github.com/SeaQL/sea-orm/pull/422 ### Upgrades -* Upgrade `clap` to 3.2 (#706) -* Upgrade `time` to 0.3 (#834) -* Upgrade `sqlx` to 0.6 (#834) -* Upgrade `uuid` to 1.0 (#834) -* Upgrade `sea-query` to 0.26 (#834) -* Upgrade `sea-schema` to 0.9 (#834) +* Upgrade `clap` to 3.2 https://github.com/SeaQL/sea-orm/pull/706 +* Upgrade `time` to 0.3 https://github.com/SeaQL/sea-orm/pull/834 +* Upgrade `sqlx` to 0.6 https://github.com/SeaQL/sea-orm/pull/834 +* Upgrade `uuid` to 1.0 https://github.com/SeaQL/sea-orm/pull/834 +* Upgrade `sea-query` to 0.26 https://github.com/SeaQL/sea-orm/pull/834 +* Upgrade `sea-schema` to 0.9 https://github.com/SeaQL/sea-orm/pull/834 ### House keeping -* Refactor stream metrics (#778) +* Refactor stream metrics https://github.com/SeaQL/sea-orm/pull/778 ### Bug Fixes -* [sea-orm-cli] skip checking connection string for credentials (#851) +* [sea-orm-cli] skip checking connection string for credentials https://github.com/SeaQL/sea-orm/pull/851 ### Breaking changes -* `SelectTwoMany::one()` has been dropped (#813), you can get `(Entity, Vec)` by first querying a single model from Entity, then use [`ModelTrait::find_related`] on the model. +* `SelectTwoMany::one()` has been dropped https://github.com/SeaQL/sea-orm/pull/813, you can get `(Entity, Vec)` by first querying a single model from Entity, then use [`ModelTrait::find_related`] on the model. ## sea-orm-migration 0.8.3 From 5c461626ab57ab0ac7e5c749b125ac0bc62025b4 Mon Sep 17 00:00:00 2001 From: Billy Chan Date: Wed, 20 Jul 2022 16:51:13 +0800 Subject: [PATCH 10/31] 0.9.1 CHANGELOG --- CHANGELOG.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae71f0ca5..17dd60884 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## 0.9.1 - Pending -* [sea-orm-cli] Codegen support for `VarBinary` column type +### Enhancements + +* [sea-orm-cli] Codegen support for `VarBinary` column type https://github.com/SeaQL/sea-orm/pull/746 +* [sea-orm-cli] Generate entity for SYSTEM VERSIONED tables on MariaDB https://github.com/SeaQL/sea-orm/pull/876 + +### Bug Fixes + +* The `on_conflict` field in `RelationDef` and `RelationBuilder` should be marked as `Send` and `Sync` https://github.com/SeaQL/sea-orm/pull/898 ## 0.9.0 - 2022-07-17 From 689af5f25df21a3f5cce9cdd7d5a55835428d9b1 Mon Sep 17 00:00:00 2001 From: Billy Chan Date: Thu, 21 Jul 2022 13:46:12 +0800 Subject: [PATCH 11/31] Auto discover and run all issues & examples CI (#903) * Auto discover and run all [issues] CI * Auto discover and run all examples CI * Fixup * Testing * Test [issues] --- .github/workflows/rust.yml | 85 +++++++++++++++++++++++--------------- 1 file changed, 52 insertions(+), 33 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 66197633d..e6e126090 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -64,7 +64,7 @@ jobs: init: name: Init - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest outputs: run-sqlite: ${{ contains(steps.git-log.outputs.message, '[sqlite]') }} run-mysql: ${{ contains(steps.git-log.outputs.message, '[mysql]') }} @@ -89,7 +89,7 @@ jobs: clippy: name: Clippy - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -144,8 +144,9 @@ jobs: needs.init.outputs.run-partial == 'false' || (needs.init.outputs.run-partial == 'true' && needs.init.outputs.run-sqlite == 'true') }} - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest strategy: + fail-fast: false matrix: runtime: [async-std] tls: [native-tls, rustls] @@ -183,8 +184,9 @@ jobs: needs.init.outputs.run-partial == 'false' || (needs.init.outputs.run-partial == 'true' && needs.init.outputs.run-mysql == 'true') }} - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest strategy: + fail-fast: false matrix: runtime: [actix] tls: [native-tls, rustls] @@ -222,8 +224,9 @@ jobs: needs.init.outputs.run-partial == 'false' || (needs.init.outputs.run-partial == 'true' && needs.init.outputs.run-postgres == 'true') }} - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest strategy: + fail-fast: false matrix: runtime: [tokio] tls: [native-tls, rustls] @@ -255,7 +258,7 @@ jobs: test: name: Unit Test - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -287,10 +290,7 @@ jobs: name: CLI needs: init if: ${{ (needs.init.outputs.run-partial == 'true' && needs.init.outputs.run-cli == 'true') }} - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest] + runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -307,14 +307,26 @@ jobs: --path sea-orm-cli --debug + examples-matrix: + name: Examples Matrix + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - id: set-matrix + run: echo "::set-output name=path_matrix::$(find examples -type f -name 'Cargo.toml' -printf '%P\0' | jq -Rc '[ split("\u0000") | .[] | "examples/\(.)" ]')" + outputs: + path_matrix: ${{ steps.set-matrix.outputs.path_matrix }} + examples: name: Examples - runs-on: ${{ matrix.os }} + runs-on: ubuntu-latest + needs: examples-matrix strategy: fail-fast: false + max-parallel: 12 matrix: - os: [ubuntu-latest] - path: [basic, actix_example, actix3_example, axum_example, graphql_example, rocket_example, poem_example, jsonrpsee_example, tonic_example] + path: ${{ fromJson(needs.examples-matrix.outputs.path_matrix) }} steps: - uses: actions/checkout@v2 @@ -328,31 +340,38 @@ jobs: with: command: build args: > - --manifest-path examples/${{ matrix.path }}/Cargo.toml - - - name: Check existence of migration directory - id: migration_dir_exists - uses: andstor/file-existence-action@v1 - with: - files: examples/${{ matrix.path }}/migration/Cargo.toml + --manifest-path ${{ matrix.path }} - uses: actions-rs/cargo@v1 - if: steps.migration_dir_exists.outputs.files_exists == 'true' with: - command: build + command: test args: > - --manifest-path examples/${{ matrix.path }}/migration/Cargo.toml + --manifest-path ${{ matrix.path }} + + issues-matrix: + name: Issues Matrix + needs: init + if: ${{ (needs.init.outputs.run-partial == 'true' && needs.init.outputs.run-issues == 'true') }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - id: set-matrix + run: echo "::set-output name=path_matrix::$(find issues -type f -name 'Cargo.toml' -printf '%P\0' | jq -Rc '[ split("\u0000") | .[] | "issues/\(.)" ]')" + outputs: + path_matrix: ${{ steps.set-matrix.outputs.path_matrix }} issues: name: Issues - needs: init + needs: + - init + - issues-matrix if: ${{ (needs.init.outputs.run-partial == 'true' && needs.init.outputs.run-issues == 'true') }} - runs-on: ${{ matrix.os }} + runs-on: ubuntu-latest strategy: fail-fast: false matrix: - os: [ubuntu-latest] - path: [86, 249, 262, 319, 324, 352, 356, 471, 630, 693] + path: ${{ fromJson(needs.issues-matrix.outputs.path_matrix) }} steps: - uses: actions/checkout@v2 @@ -366,13 +385,13 @@ jobs: with: command: build args: > - --manifest-path issues/${{ matrix.path }}/Cargo.toml + --manifest-path ${{ matrix.path }} - uses: actions-rs/cargo@v1 with: command: test args: > - --manifest-path issues/${{ matrix.path }}/Cargo.toml + --manifest-path ${{ matrix.path }} sqlite: name: SQLite @@ -384,7 +403,7 @@ jobs: needs.init.outputs.run-partial == 'false' || (needs.init.outputs.run-partial == 'true' && needs.init.outputs.run-sqlite == 'true') }} - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest env: DATABASE_URL: "sqlite::memory:" strategy: @@ -435,7 +454,7 @@ jobs: needs.init.outputs.run-partial == 'false' || (needs.init.outputs.run-partial == 'true' && needs.init.outputs.run-mysql == 'true') }} - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest env: DATABASE_URL: "mysql://root:@localhost" strategy: @@ -504,7 +523,7 @@ jobs: needs.init.outputs.run-partial == 'false' || (needs.init.outputs.run-partial == 'true' && needs.init.outputs.run-mysql == 'true') }} - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest env: DATABASE_URL: "mysql://root:@localhost" strategy: @@ -565,7 +584,7 @@ jobs: needs.init.outputs.run-partial == 'false' || (needs.init.outputs.run-partial == 'true' && needs.init.outputs.run-postgres == 'true') }} - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest env: DATABASE_URL: "postgres://root:root@localhost" strategy: From e6e8236e057c613aed398db83530edd8a53d5dc8 Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Thu, 21 Jul 2022 17:58:05 +0800 Subject: [PATCH 12/31] Compile prepare_mock_db() conditionally based on "mock" feature --- examples/rocket_example/core/Cargo.toml | 5 ++++- examples/rocket_example/core/tests/prepare.rs | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/rocket_example/core/Cargo.toml b/examples/rocket_example/core/Cargo.toml index 79226dc8d..7190d11d4 100644 --- a/examples/rocket_example/core/Cargo.toml +++ b/examples/rocket_example/core/Cargo.toml @@ -21,6 +21,9 @@ features = [ [dev-dependencies] tokio = "1.20.0" +[features] +mock = ["sea-orm/mock"] + [[test]] name = "mock" -required-features = ["sea-orm/mock"] +required-features = ["mock"] diff --git a/examples/rocket_example/core/tests/prepare.rs b/examples/rocket_example/core/tests/prepare.rs index 1afb9af92..451804937 100644 --- a/examples/rocket_example/core/tests/prepare.rs +++ b/examples/rocket_example/core/tests/prepare.rs @@ -1,6 +1,7 @@ use ::entity::post; use sea_orm::*; +#[cfg(feature = "mock")] pub fn prepare_mock_db() -> DatabaseConnection { MockDatabase::new(DatabaseBackend::Postgres) .append_query_results(vec![ From cfb83be8a9fceeef80b756cd3b3a422f177e77af Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Thu, 21 Jul 2022 18:17:27 +0800 Subject: [PATCH 13/31] Update workflow job to run mock test if `core` folder exists --- .github/workflows/rust.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index e6e126090..c2dd70fe5 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -348,6 +348,20 @@ jobs: args: > --manifest-path ${{ matrix.path }} + - name: Check existence of core directory + id: core_dir_exists + uses: andstor/file-existence-action@v1 + with: + files: examples/${{ matrix.path }}/core/Cargo.toml + + - uses: actions-rs/cargo@v1 + if: steps.core_dir_exists.outputs.files_exists == 'true' + with: + command: test + args: > + --manifest-path examples/${{ matrix.path }}/core/Cargo.toml + --features mock + issues-matrix: name: Issues Matrix needs: init From ef1132cbe163a301dcc4f6508c93b96123872032 Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Wed, 7 Sep 2022 22:40:40 +0100 Subject: [PATCH 14/31] Update Actix3 example --- examples/actix3_example/Cargo.toml | 27 +-- examples/actix3_example/api/Cargo.toml | 22 ++ examples/actix3_example/api/src/lib.rs | 219 +++++++++++++++++ .../{ => api}/static/css/normalize.css | 0 .../{ => api}/static/css/skeleton.css | 0 .../{ => api}/static/css/style.css | 0 .../{ => api}/static/images/favicon.png | Bin .../{ => api}/templates/edit.html.tera | 0 .../{ => api}/templates/error/404.html.tera | 0 .../{ => api}/templates/index.html.tera | 0 .../{ => api}/templates/layout.html.tera | 0 .../{ => api}/templates/new.html.tera | 0 examples/actix3_example/core/Cargo.toml | 30 +++ examples/actix3_example/core/src/lib.rs | 7 + examples/actix3_example/core/src/mutation.rs | 53 ++++ examples/actix3_example/core/src/query.rs | 26 ++ examples/actix3_example/core/tests/mock.rs | 79 ++++++ examples/actix3_example/core/tests/prepare.rs | 50 ++++ examples/actix3_example/src/main.rs | 228 +----------------- 19 files changed, 490 insertions(+), 251 deletions(-) create mode 100644 examples/actix3_example/api/Cargo.toml create mode 100644 examples/actix3_example/api/src/lib.rs rename examples/actix3_example/{ => api}/static/css/normalize.css (100%) rename examples/actix3_example/{ => api}/static/css/skeleton.css (100%) rename examples/actix3_example/{ => api}/static/css/style.css (100%) rename examples/actix3_example/{ => api}/static/images/favicon.png (100%) rename examples/actix3_example/{ => api}/templates/edit.html.tera (100%) rename examples/actix3_example/{ => api}/templates/error/404.html.tera (100%) rename examples/actix3_example/{ => api}/templates/index.html.tera (100%) rename examples/actix3_example/{ => api}/templates/layout.html.tera (100%) rename examples/actix3_example/{ => api}/templates/new.html.tera (100%) create mode 100644 examples/actix3_example/core/Cargo.toml create mode 100644 examples/actix3_example/core/src/lib.rs create mode 100644 examples/actix3_example/core/src/mutation.rs create mode 100644 examples/actix3_example/core/src/query.rs create mode 100644 examples/actix3_example/core/tests/mock.rs create mode 100644 examples/actix3_example/core/tests/prepare.rs diff --git a/examples/actix3_example/Cargo.toml b/examples/actix3_example/Cargo.toml index 8d1f8d6f6..a449de4ca 100644 --- a/examples/actix3_example/Cargo.toml +++ b/examples/actix3_example/Cargo.toml @@ -6,30 +6,7 @@ edition = "2021" publish = false [workspace] -members = [".", "entity", "migration"] +members = [".", "api", "core", "entity", "migration"] [dependencies] -actix-http = "2" -actix-web = "3" -actix-flash = "0.2" -actix-files = "0.5" -futures = { version = "^0.3" } -futures-util = { version = "^0.3" } -tera = "1.8.0" -dotenv = "0.15" -listenfd = "0.3.3" -serde = "1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } -entity = { path = "entity" } -migration = { path = "migration" } - -[dependencies.sea-orm] -path = "../../" # remove this line in your own project -version = "^0.9.0" # sea-orm version -features = [ - "debug-print", - "runtime-async-std-native-tls", - "sqlx-mysql", - # "sqlx-postgres", - # "sqlx-sqlite", -] +actix3-example-api = { path = "api" } diff --git a/examples/actix3_example/api/Cargo.toml b/examples/actix3_example/api/Cargo.toml new file mode 100644 index 000000000..5f38712e0 --- /dev/null +++ b/examples/actix3_example/api/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "actix3-example-api" +version = "0.1.0" +authors = ["Sam Samai "] +edition = "2021" +publish = false + +[dependencies] +actix3-example-core = { path = "../core" } +actix-http = "2" +actix-web = "3" +actix-flash = "0.2" +actix-files = "0.5" +futures = { version = "^0.3" } +futures-util = { version = "^0.3" } +tera = "1.8.0" +dotenv = "0.15" +listenfd = "0.3.3" +serde = "1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +entity = { path = "../entity" } +migration = { path = "../migration" } diff --git a/examples/actix3_example/api/src/lib.rs b/examples/actix3_example/api/src/lib.rs new file mode 100644 index 000000000..d5160f876 --- /dev/null +++ b/examples/actix3_example/api/src/lib.rs @@ -0,0 +1,219 @@ +use actix3_example_core::{ + sea_orm::{Database, DatabaseConnection}, + Mutation, Query, +}; +use actix_files as fs; +use actix_web::{ + error, get, middleware, post, web, App, Error, HttpRequest, HttpResponse, HttpServer, Result, +}; + +use entity::post; +use listenfd::ListenFd; +use migration::{Migrator, MigratorTrait}; +use serde::{Deserialize, Serialize}; +use std::env; +use tera::Tera; + +const DEFAULT_POSTS_PER_PAGE: usize = 5; + +#[derive(Debug, Clone)] +struct AppState { + templates: tera::Tera, + conn: DatabaseConnection, +} +#[derive(Debug, Deserialize)] +pub struct Params { + page: Option, + posts_per_page: Option, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +struct FlashData { + kind: String, + message: String, +} + +#[get("/")] +async fn list( + req: HttpRequest, + data: web::Data, + opt_flash: Option>, +) -> Result { + let template = &data.templates; + let conn = &data.conn; + + // get params + let params = web::Query::::from_query(req.query_string()).unwrap(); + + let page = params.page.unwrap_or(1); + let posts_per_page = params.posts_per_page.unwrap_or(DEFAULT_POSTS_PER_PAGE); + + let (posts, num_pages) = Query::find_posts_in_page(conn, page, posts_per_page) + .await + .expect("Cannot find posts in page"); + + let mut ctx = tera::Context::new(); + ctx.insert("posts", &posts); + ctx.insert("page", &page); + ctx.insert("posts_per_page", &posts_per_page); + ctx.insert("num_pages", &num_pages); + + if let Some(flash) = opt_flash { + let flash_inner = flash.into_inner(); + ctx.insert("flash", &flash_inner); + } + + let body = template + .render("index.html.tera", &ctx) + .map_err(|_| error::ErrorInternalServerError("Template error"))?; + Ok(HttpResponse::Ok().content_type("text/html").body(body)) +} + +#[get("/new")] +async fn new(data: web::Data) -> Result { + let template = &data.templates; + let ctx = tera::Context::new(); + let body = template + .render("new.html.tera", &ctx) + .map_err(|_| error::ErrorInternalServerError("Template error"))?; + Ok(HttpResponse::Ok().content_type("text/html").body(body)) +} + +#[post("/")] +async fn create( + data: web::Data, + post_form: web::Form, +) -> actix_flash::Response { + let conn = &data.conn; + + let form = post_form.into_inner(); + + Mutation::create_post(conn, form) + .await + .expect("could not insert post"); + + let flash = FlashData { + kind: "success".to_owned(), + message: "Post successfully added.".to_owned(), + }; + + actix_flash::Response::with_redirect(flash, "/") +} + +#[get("/{id}")] +async fn edit(data: web::Data, id: web::Path) -> Result { + let conn = &data.conn; + let template = &data.templates; + let id = id.into_inner(); + + let post: post::Model = Query::find_post_by_id(conn, id) + .await + .expect("could not find post") + .unwrap_or_else(|| panic!("could not find post with id {}", id)); + + let mut ctx = tera::Context::new(); + ctx.insert("post", &post); + + let body = template + .render("edit.html.tera", &ctx) + .map_err(|_| error::ErrorInternalServerError("Template error"))?; + Ok(HttpResponse::Ok().content_type("text/html").body(body)) +} + +#[post("/{id}")] +async fn update( + data: web::Data, + id: web::Path, + post_form: web::Form, +) -> actix_flash::Response { + let conn = &data.conn; + let form = post_form.into_inner(); + let id = id.into_inner(); + + Mutation::update_post_by_id(conn, id, form) + .await + .expect("could not edit post"); + + let flash = FlashData { + kind: "success".to_owned(), + message: "Post successfully updated.".to_owned(), + }; + + actix_flash::Response::with_redirect(flash, "/") +} + +#[post("/delete/{id}")] +async fn delete( + data: web::Data, + id: web::Path, +) -> actix_flash::Response { + let conn = &data.conn; + let id = id.into_inner(); + + Mutation::delete_post(conn, id) + .await + .expect("could not delete post"); + + let flash = FlashData { + kind: "success".to_owned(), + message: "Post successfully deleted.".to_owned(), + }; + + actix_flash::Response::with_redirect(flash, "/") +} + +#[actix_web::main] +async fn start() -> std::io::Result<()> { + std::env::set_var("RUST_LOG", "debug"); + tracing_subscriber::fmt::init(); + + // get env vars + dotenv::dotenv().ok(); + let db_url = env::var("DATABASE_URL").expect("DATABASE_URL is not set in .env file"); + let host = env::var("HOST").expect("HOST is not set in .env file"); + let port = env::var("PORT").expect("PORT is not set in .env file"); + let server_url = format!("{}:{}", host, port); + + // create post table if not exists + let conn = Database::connect(&db_url).await.unwrap(); + Migrator::up(&conn, None).await.unwrap(); + let templates = Tera::new(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*")).unwrap(); + let state = AppState { templates, conn }; + + let mut listenfd = ListenFd::from_env(); + let mut server = HttpServer::new(move || { + App::new() + .data(state.clone()) + .wrap(middleware::Logger::default()) // enable logger + .wrap(actix_flash::Flash::default()) + .configure(init) + .service(fs::Files::new("/static", "./api/static").show_files_listing()) + }); + + server = match listenfd.take_tcp_listener(0)? { + Some(listener) => server.listen(listener)?, + None => server.bind(&server_url)?, + }; + + println!("Starting server at {}", server_url); + server.run().await?; + + Ok(()) +} + +fn init(cfg: &mut web::ServiceConfig) { + cfg.service(list); + cfg.service(new); + cfg.service(create); + cfg.service(edit); + cfg.service(update); + cfg.service(delete); +} + +pub fn main() { + let result = start(); + + if let Some(err) = result.err() { + println!("Error: {}", err) + } +} diff --git a/examples/actix3_example/static/css/normalize.css b/examples/actix3_example/api/static/css/normalize.css similarity index 100% rename from examples/actix3_example/static/css/normalize.css rename to examples/actix3_example/api/static/css/normalize.css diff --git a/examples/actix3_example/static/css/skeleton.css b/examples/actix3_example/api/static/css/skeleton.css similarity index 100% rename from examples/actix3_example/static/css/skeleton.css rename to examples/actix3_example/api/static/css/skeleton.css diff --git a/examples/actix3_example/static/css/style.css b/examples/actix3_example/api/static/css/style.css similarity index 100% rename from examples/actix3_example/static/css/style.css rename to examples/actix3_example/api/static/css/style.css diff --git a/examples/actix3_example/static/images/favicon.png b/examples/actix3_example/api/static/images/favicon.png similarity index 100% rename from examples/actix3_example/static/images/favicon.png rename to examples/actix3_example/api/static/images/favicon.png diff --git a/examples/actix3_example/templates/edit.html.tera b/examples/actix3_example/api/templates/edit.html.tera similarity index 100% rename from examples/actix3_example/templates/edit.html.tera rename to examples/actix3_example/api/templates/edit.html.tera diff --git a/examples/actix3_example/templates/error/404.html.tera b/examples/actix3_example/api/templates/error/404.html.tera similarity index 100% rename from examples/actix3_example/templates/error/404.html.tera rename to examples/actix3_example/api/templates/error/404.html.tera diff --git a/examples/actix3_example/templates/index.html.tera b/examples/actix3_example/api/templates/index.html.tera similarity index 100% rename from examples/actix3_example/templates/index.html.tera rename to examples/actix3_example/api/templates/index.html.tera diff --git a/examples/actix3_example/templates/layout.html.tera b/examples/actix3_example/api/templates/layout.html.tera similarity index 100% rename from examples/actix3_example/templates/layout.html.tera rename to examples/actix3_example/api/templates/layout.html.tera diff --git a/examples/actix3_example/templates/new.html.tera b/examples/actix3_example/api/templates/new.html.tera similarity index 100% rename from examples/actix3_example/templates/new.html.tera rename to examples/actix3_example/api/templates/new.html.tera diff --git a/examples/actix3_example/core/Cargo.toml b/examples/actix3_example/core/Cargo.toml new file mode 100644 index 000000000..2c4c3f39b --- /dev/null +++ b/examples/actix3_example/core/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "actix3-example-core" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +entity = { path = "../entity" } + +[dependencies.sea-orm] +path = "../../../" # remove this line in your own project +version = "^0.9.0" # sea-orm version +features = [ + "debug-print", + "runtime-async-std-native-tls", + "sqlx-mysql", + # "sqlx-postgres", + # "sqlx-sqlite", +] + +[dev-dependencies] +tokio = { version = "1.20.0", features = ["macros", "rt"] } + +[features] +mock = ["sea-orm/mock"] + +[[test]] +name = "mock" +required-features = ["mock"] diff --git a/examples/actix3_example/core/src/lib.rs b/examples/actix3_example/core/src/lib.rs new file mode 100644 index 000000000..4a80f2391 --- /dev/null +++ b/examples/actix3_example/core/src/lib.rs @@ -0,0 +1,7 @@ +mod mutation; +mod query; + +pub use mutation::*; +pub use query::*; + +pub use sea_orm; diff --git a/examples/actix3_example/core/src/mutation.rs b/examples/actix3_example/core/src/mutation.rs new file mode 100644 index 000000000..7f0150a63 --- /dev/null +++ b/examples/actix3_example/core/src/mutation.rs @@ -0,0 +1,53 @@ +use ::entity::{post, post::Entity as Post}; +use sea_orm::*; + +pub struct Mutation; + +impl Mutation { + pub async fn create_post( + db: &DbConn, + form_data: post::Model, + ) -> Result { + post::ActiveModel { + title: Set(form_data.title.to_owned()), + text: Set(form_data.text.to_owned()), + ..Default::default() + } + .save(db) + .await + } + + pub async fn update_post_by_id( + db: &DbConn, + id: i32, + form_data: post::Model, + ) -> Result { + let post: post::ActiveModel = if let Ok(Some(post)) = Post::find_by_id(id).one(db).await { + post.into() + } else { + return Err(DbErr::Custom("Cannot find post.".to_owned())); + }; + + post::ActiveModel { + id: post.id, + title: Set(form_data.title.to_owned()), + text: Set(form_data.text.to_owned()), + } + .update(db) + .await + } + + pub async fn delete_post(db: &DbConn, id: i32) -> Result { + let post: post::ActiveModel = if let Ok(Some(post)) = Post::find_by_id(id).one(db).await { + post.into() + } else { + return Err(DbErr::Custom("Cannot find post.".to_owned())); + }; + + post.delete(db).await + } + + pub async fn delete_all_posts(db: &DbConn) -> Result { + Post::delete_many().exec(db).await + } +} diff --git a/examples/actix3_example/core/src/query.rs b/examples/actix3_example/core/src/query.rs new file mode 100644 index 000000000..f08e80c77 --- /dev/null +++ b/examples/actix3_example/core/src/query.rs @@ -0,0 +1,26 @@ +use ::entity::{post, post::Entity as Post}; +use sea_orm::*; + +pub struct Query; + +impl Query { + pub async fn find_post_by_id(db: &DbConn, id: i32) -> Result, DbErr> { + Post::find_by_id(id).one(db).await + } + + /// If ok, returns (post models, num pages). + pub async fn find_posts_in_page( + db: &DbConn, + page: usize, + posts_per_page: usize, + ) -> Result<(Vec, usize), DbErr> { + // Setup paginator + let paginator = Post::find() + .order_by_asc(post::Column::Id) + .paginate(db, posts_per_page); + let num_pages = paginator.num_pages().await?; + + // Fetch paginated posts + paginator.fetch_page(page - 1).await.map(|p| (p, num_pages)) + } +} diff --git a/examples/actix3_example/core/tests/mock.rs b/examples/actix3_example/core/tests/mock.rs new file mode 100644 index 000000000..190cb2906 --- /dev/null +++ b/examples/actix3_example/core/tests/mock.rs @@ -0,0 +1,79 @@ +mod prepare; + +use actix3_example_core::{Mutation, Query}; +use entity::post; +use prepare::prepare_mock_db; + +#[tokio::test] +async fn main() { + let db = &prepare_mock_db(); + + { + let post = Query::find_post_by_id(db, 1).await.unwrap().unwrap(); + + assert_eq!(post.id, 1); + } + + { + let post = Query::find_post_by_id(db, 5).await.unwrap().unwrap(); + + assert_eq!(post.id, 5); + } + + { + let post = Mutation::create_post( + db, + post::Model { + id: 0, + title: "Title D".to_owned(), + text: "Text D".to_owned(), + }, + ) + .await + .unwrap(); + + assert_eq!( + post, + post::ActiveModel { + id: sea_orm::ActiveValue::Unchanged(6), + title: sea_orm::ActiveValue::Unchanged("Title D".to_owned()), + text: sea_orm::ActiveValue::Unchanged("Text D".to_owned()) + } + ); + } + + { + let post = Mutation::update_post_by_id( + db, + 1, + post::Model { + id: 1, + title: "New Title A".to_owned(), + text: "New Text A".to_owned(), + }, + ) + .await + .unwrap(); + + assert_eq!( + post, + post::Model { + id: 1, + title: "New Title A".to_owned(), + text: "New Text A".to_owned(), + } + ); + } + + { + let result = Mutation::delete_post(db, 5).await.unwrap(); + + assert_eq!(result.rows_affected, 1); + } + + { + let result = Mutation::delete_all_posts(db).await.unwrap(); + + assert_eq!(result.rows_affected, 5); + } +} diff --git a/examples/actix3_example/core/tests/prepare.rs b/examples/actix3_example/core/tests/prepare.rs new file mode 100644 index 000000000..451804937 --- /dev/null +++ b/examples/actix3_example/core/tests/prepare.rs @@ -0,0 +1,50 @@ +use ::entity::post; +use sea_orm::*; + +#[cfg(feature = "mock")] +pub fn prepare_mock_db() -> DatabaseConnection { + MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results(vec![ + vec![post::Model { + id: 1, + title: "Title A".to_owned(), + text: "Text A".to_owned(), + }], + vec![post::Model { + id: 5, + title: "Title C".to_owned(), + text: "Text C".to_owned(), + }], + vec![post::Model { + id: 6, + title: "Title D".to_owned(), + text: "Text D".to_owned(), + }], + vec![post::Model { + id: 1, + title: "Title A".to_owned(), + text: "Text A".to_owned(), + }], + vec![post::Model { + id: 1, + title: "New Title A".to_owned(), + text: "New Text A".to_owned(), + }], + vec![post::Model { + id: 5, + title: "Title C".to_owned(), + text: "Text C".to_owned(), + }], + ]) + .append_exec_results(vec![ + MockExecResult { + last_insert_id: 6, + rows_affected: 1, + }, + MockExecResult { + last_insert_id: 6, + rows_affected: 5, + }, + ]) + .into_connection() +} diff --git a/examples/actix3_example/src/main.rs b/examples/actix3_example/src/main.rs index 1a6a70223..5a0e63b82 100644 --- a/examples/actix3_example/src/main.rs +++ b/examples/actix3_example/src/main.rs @@ -1,227 +1,3 @@ -use actix_files as fs; -use actix_web::{ - error, get, middleware, post, web, App, Error, HttpRequest, HttpResponse, HttpServer, Result, -}; - -use entity::post; -use entity::post::Entity as Post; -use listenfd::ListenFd; -use migration::{Migrator, MigratorTrait}; -use sea_orm::DatabaseConnection; -use sea_orm::{entity::*, query::*}; -use serde::{Deserialize, Serialize}; -use std::env; -use tera::Tera; - -const DEFAULT_POSTS_PER_PAGE: usize = 5; - -#[derive(Debug, Clone)] -struct AppState { - templates: tera::Tera, - conn: DatabaseConnection, -} -#[derive(Debug, Deserialize)] -pub struct Params { - page: Option, - posts_per_page: Option, -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -struct FlashData { - kind: String, - message: String, -} - -#[get("/")] -async fn list( - req: HttpRequest, - data: web::Data, - opt_flash: Option>, -) -> Result { - let template = &data.templates; - let conn = &data.conn; - - // get params - let params = web::Query::::from_query(req.query_string()).unwrap(); - - let page = params.page.unwrap_or(1); - let posts_per_page = params.posts_per_page.unwrap_or(DEFAULT_POSTS_PER_PAGE); - let paginator = Post::find() - .order_by_asc(post::Column::Id) - .paginate(conn, posts_per_page); - let num_pages = paginator.num_pages().await.ok().unwrap(); - - let posts = paginator - .fetch_page(page - 1) - .await - .expect("could not retrieve posts"); - let mut ctx = tera::Context::new(); - ctx.insert("posts", &posts); - ctx.insert("page", &page); - ctx.insert("posts_per_page", &posts_per_page); - ctx.insert("num_pages", &num_pages); - - if let Some(flash) = opt_flash { - let flash_inner = flash.into_inner(); - ctx.insert("flash", &flash_inner); - } - - let body = template - .render("index.html.tera", &ctx) - .map_err(|_| error::ErrorInternalServerError("Template error"))?; - Ok(HttpResponse::Ok().content_type("text/html").body(body)) -} - -#[get("/new")] -async fn new(data: web::Data) -> Result { - let template = &data.templates; - let ctx = tera::Context::new(); - let body = template - .render("new.html.tera", &ctx) - .map_err(|_| error::ErrorInternalServerError("Template error"))?; - Ok(HttpResponse::Ok().content_type("text/html").body(body)) -} - -#[post("/")] -async fn create( - data: web::Data, - post_form: web::Form, -) -> actix_flash::Response { - let conn = &data.conn; - - let form = post_form.into_inner(); - - post::ActiveModel { - title: Set(form.title.to_owned()), - text: Set(form.text.to_owned()), - ..Default::default() - } - .save(conn) - .await - .expect("could not insert post"); - - let flash = FlashData { - kind: "success".to_owned(), - message: "Post successfully added.".to_owned(), - }; - - actix_flash::Response::with_redirect(flash, "/") -} - -#[get("/{id}")] -async fn edit(data: web::Data, id: web::Path) -> Result { - let conn = &data.conn; - let template = &data.templates; - - let post: post::Model = Post::find_by_id(id.into_inner()) - .one(conn) - .await - .expect("could not find post") - .unwrap(); - - let mut ctx = tera::Context::new(); - ctx.insert("post", &post); - - let body = template - .render("edit.html.tera", &ctx) - .map_err(|_| error::ErrorInternalServerError("Template error"))?; - Ok(HttpResponse::Ok().content_type("text/html").body(body)) -} - -#[post("/{id}")] -async fn update( - data: web::Data, - id: web::Path, - post_form: web::Form, -) -> actix_flash::Response { - let conn = &data.conn; - let form = post_form.into_inner(); - - post::ActiveModel { - id: Set(id.into_inner()), - title: Set(form.title.to_owned()), - text: Set(form.text.to_owned()), - } - .save(conn) - .await - .expect("could not edit post"); - - let flash = FlashData { - kind: "success".to_owned(), - message: "Post successfully updated.".to_owned(), - }; - - actix_flash::Response::with_redirect(flash, "/") -} - -#[post("/delete/{id}")] -async fn delete( - data: web::Data, - id: web::Path, -) -> actix_flash::Response { - let conn = &data.conn; - - let post: post::ActiveModel = Post::find_by_id(id.into_inner()) - .one(conn) - .await - .unwrap() - .unwrap() - .into(); - - post.delete(conn).await.unwrap(); - - let flash = FlashData { - kind: "success".to_owned(), - message: "Post successfully deleted.".to_owned(), - }; - - actix_flash::Response::with_redirect(flash, "/") -} - -#[actix_web::main] -async fn main() -> std::io::Result<()> { - std::env::set_var("RUST_LOG", "debug"); - tracing_subscriber::fmt::init(); - - // get env vars - dotenv::dotenv().ok(); - let db_url = env::var("DATABASE_URL").expect("DATABASE_URL is not set in .env file"); - let host = env::var("HOST").expect("HOST is not set in .env file"); - let port = env::var("PORT").expect("PORT is not set in .env file"); - let server_url = format!("{}:{}", host, port); - - // create post table if not exists - let conn = sea_orm::Database::connect(&db_url).await.unwrap(); - Migrator::up(&conn, None).await.unwrap(); - let templates = Tera::new(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*")).unwrap(); - let state = AppState { templates, conn }; - - let mut listenfd = ListenFd::from_env(); - let mut server = HttpServer::new(move || { - App::new() - .data(state.clone()) - .wrap(middleware::Logger::default()) // enable logger - .wrap(actix_flash::Flash::default()) - .configure(init) - .service(fs::Files::new("/static", "./static").show_files_listing()) - }); - - server = match listenfd.take_tcp_listener(0)? { - Some(listener) => server.listen(listener)?, - None => server.bind(&server_url)?, - }; - - println!("Starting server at {}", server_url); - server.run().await?; - - Ok(()) -} - -pub fn init(cfg: &mut web::ServiceConfig) { - cfg.service(list); - cfg.service(new); - cfg.service(create); - cfg.service(edit); - cfg.service(update); - cfg.service(delete); +fn main() { + actix3_example_api::main(); } From 7fcc8faa6e6752fa38fadfb43d29f040dd70de2d Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Wed, 7 Sep 2022 22:57:07 +0100 Subject: [PATCH 15/31] Fix merge conflict human error --- examples/actix3_example/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/actix3_example/Cargo.toml b/examples/actix3_example/Cargo.toml index 8cf97ed25..de7a7335a 100644 --- a/examples/actix3_example/Cargo.toml +++ b/examples/actix3_example/Cargo.toml @@ -10,7 +10,6 @@ members = [".", "api", "core", "entity", "migration"] [dependencies] actix3-example-api = { path = "api" } -======= actix-http = "2" actix-web = "3" actix-flash = "0.2" From 0fea81eb5e54e73766f74d0ceff69bbe2fdb221c Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Wed, 7 Sep 2022 23:09:56 +0100 Subject: [PATCH 16/31] Update usize used in paginate to u64 (PR#789) --- examples/actix3_example/Cargo.toml | 24 ----------------------- examples/actix3_example/api/src/lib.rs | 6 +++--- examples/actix3_example/core/Cargo.toml | 2 +- examples/actix3_example/core/src/query.rs | 6 +++--- 4 files changed, 7 insertions(+), 31 deletions(-) diff --git a/examples/actix3_example/Cargo.toml b/examples/actix3_example/Cargo.toml index de7a7335a..a449de4ca 100644 --- a/examples/actix3_example/Cargo.toml +++ b/examples/actix3_example/Cargo.toml @@ -10,27 +10,3 @@ members = [".", "api", "core", "entity", "migration"] [dependencies] actix3-example-api = { path = "api" } -actix-http = "2" -actix-web = "3" -actix-flash = "0.2" -actix-files = "0.5" -futures = { version = "^0.3" } -futures-util = { version = "^0.3" } -tera = "1.8.0" -dotenv = "0.15" -listenfd = "0.3.3" -serde = "1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } -entity = { path = "entity" } -migration = { path = "migration" } - -[dependencies.sea-orm] -path = "../../" # remove this line in your own project -version = "^0.10.0" # sea-orm version -features = [ - "debug-print", - "runtime-async-std-native-tls", - "sqlx-mysql", - # "sqlx-postgres", - # "sqlx-sqlite", -] diff --git a/examples/actix3_example/api/src/lib.rs b/examples/actix3_example/api/src/lib.rs index d5160f876..275e4a7cc 100644 --- a/examples/actix3_example/api/src/lib.rs +++ b/examples/actix3_example/api/src/lib.rs @@ -14,7 +14,7 @@ use serde::{Deserialize, Serialize}; use std::env; use tera::Tera; -const DEFAULT_POSTS_PER_PAGE: usize = 5; +const DEFAULT_POSTS_PER_PAGE: u64 = 5; #[derive(Debug, Clone)] struct AppState { @@ -23,8 +23,8 @@ struct AppState { } #[derive(Debug, Deserialize)] pub struct Params { - page: Option, - posts_per_page: Option, + page: Option, + posts_per_page: Option, } #[derive(Deserialize, Serialize, Debug, Clone)] diff --git a/examples/actix3_example/core/Cargo.toml b/examples/actix3_example/core/Cargo.toml index 2c4c3f39b..c0548ff49 100644 --- a/examples/actix3_example/core/Cargo.toml +++ b/examples/actix3_example/core/Cargo.toml @@ -10,7 +10,7 @@ entity = { path = "../entity" } [dependencies.sea-orm] path = "../../../" # remove this line in your own project -version = "^0.9.0" # sea-orm version +version = "^0.10.0" # sea-orm version features = [ "debug-print", "runtime-async-std-native-tls", diff --git a/examples/actix3_example/core/src/query.rs b/examples/actix3_example/core/src/query.rs index f08e80c77..e8d2668f5 100644 --- a/examples/actix3_example/core/src/query.rs +++ b/examples/actix3_example/core/src/query.rs @@ -11,9 +11,9 @@ impl Query { /// If ok, returns (post models, num pages). pub async fn find_posts_in_page( db: &DbConn, - page: usize, - posts_per_page: usize, - ) -> Result<(Vec, usize), DbErr> { + page: u64, + posts_per_page: u64, + ) -> Result<(Vec, u64), DbErr> { // Setup paginator let paginator = Post::find() .order_by_asc(post::Column::Id) From df4dee34495a8a0963855003cefae688b4446f8b Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Wed, 7 Sep 2022 23:13:59 +0100 Subject: [PATCH 17/31] Update sea-orm version in the Rocket example to 0.10.0 --- examples/rocket_example/api/src/lib.rs | 6 +++--- examples/rocket_example/core/Cargo.toml | 10 +++++----- examples/rocket_example/core/src/query.rs | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/examples/rocket_example/api/src/lib.rs b/examples/rocket_example/api/src/lib.rs index 14280c11e..f283b1015 100644 --- a/examples/rocket_example/api/src/lib.rs +++ b/examples/rocket_example/api/src/lib.rs @@ -21,7 +21,7 @@ use pool::Db; pub use entity::post; pub use entity::post::Entity as Post; -const DEFAULT_POSTS_PER_PAGE: usize = 5; +const DEFAULT_POSTS_PER_PAGE: u64 = 5; #[get("/new")] async fn new() -> Template { @@ -61,8 +61,8 @@ async fn update( #[get("/?&")] async fn list( conn: Connection<'_, Db>, - page: Option, - posts_per_page: Option, + page: Option, + posts_per_page: Option, flash: Option>, ) -> Template { let db = conn.into_inner(); diff --git a/examples/rocket_example/core/Cargo.toml b/examples/rocket_example/core/Cargo.toml index 7190d11d4..a57a55608 100644 --- a/examples/rocket_example/core/Cargo.toml +++ b/examples/rocket_example/core/Cargo.toml @@ -10,12 +10,12 @@ entity = { path = "../entity" } [dependencies.sea-orm] path = "../../../" # remove this line in your own project -version = "^0.9.0" # sea-orm version +version = "^0.10.0" # sea-orm version features = [ - "runtime-tokio-native-tls", - "sqlx-postgres", - # "sqlx-mysql", - # "sqlx-sqlite", + "runtime-tokio-native-tls", + "sqlx-postgres", + # "sqlx-mysql", + # "sqlx-sqlite", ] [dev-dependencies] diff --git a/examples/rocket_example/core/src/query.rs b/examples/rocket_example/core/src/query.rs index f08e80c77..e8d2668f5 100644 --- a/examples/rocket_example/core/src/query.rs +++ b/examples/rocket_example/core/src/query.rs @@ -11,9 +11,9 @@ impl Query { /// If ok, returns (post models, num pages). pub async fn find_posts_in_page( db: &DbConn, - page: usize, - posts_per_page: usize, - ) -> Result<(Vec, usize), DbErr> { + page: u64, + posts_per_page: u64, + ) -> Result<(Vec, u64), DbErr> { // Setup paginator let paginator = Post::find() .order_by_asc(post::Column::Id) From 4e56b4c54569b2b5ee073f59f5cbead1bf7c5267 Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Thu, 8 Sep 2022 00:24:59 +0100 Subject: [PATCH 18/31] Fix GitHub workflow to run mock test for core crates --- .github/workflows/rust.yml | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 96e166198..2c0aa406e 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -405,18 +405,13 @@ jobs: args: > --manifest-path ${{ matrix.path }} - - name: Check existence of core directory - id: core_dir_exists - uses: andstor/file-existence-action@v1 - with: - files: examples/${{ matrix.path }}/core/Cargo.toml - - - uses: actions-rs/cargo@v1 - if: steps.core_dir_exists.outputs.files_exists == 'true' + - name: Run mock test if it is core crate + uses: actions-rs/cargo@v1 + if: ${{ contains(matrix.path, 'core') }} with: command: test args: > - --manifest-path examples/${{ matrix.path }}/core/Cargo.toml + --manifest-path ${{ matrix.path }} --features mock - name: check rustfmt From 1a09e0078b479b57c5de544494f8dc81b3ec8b0a Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Thu, 8 Sep 2022 21:51:49 +0100 Subject: [PATCH 19/31] Increase the robustness of core crate check by verifying that the `core` folder is a crate --- .github/workflows/rust.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 2c0aa406e..037d21def 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -407,7 +407,7 @@ jobs: - name: Run mock test if it is core crate uses: actions-rs/cargo@v1 - if: ${{ contains(matrix.path, 'core') }} + if: ${{ contains(matrix.path, 'core/Cargo.toml') }} with: command: test args: > From e06e85c72cb59c3b37ad410819a887ee3df1f903 Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Thu, 8 Sep 2022 22:20:19 +0100 Subject: [PATCH 20/31] Update Actix(4) example --- examples/actix_example/Cargo.toml | 27 +-- examples/actix_example/api/Cargo.toml | 21 ++ examples/actix_example/api/src/lib.rs | 215 +++++++++++++++++ .../{ => api}/static/css/normalize.css | 0 .../{ => api}/static/css/skeleton.css | 0 .../{ => api}/static/css/style.css | 0 .../{ => api}/static/images/favicon.png | Bin .../{ => api}/templates/edit.html.tera | 0 .../{ => api}/templates/error/404.html.tera | 0 .../{ => api}/templates/index.html.tera | 0 .../{ => api}/templates/layout.html.tera | 0 .../{ => api}/templates/new.html.tera | 0 examples/actix_example/core/Cargo.toml | 30 +++ examples/actix_example/core/src/lib.rs | 7 + examples/actix_example/core/src/mutation.rs | 53 +++++ examples/actix_example/core/src/query.rs | 26 ++ examples/actix_example/core/tests/mock.rs | 79 ++++++ examples/actix_example/core/tests/prepare.rs | 50 ++++ examples/actix_example/src/main.rs | 224 +----------------- 19 files changed, 485 insertions(+), 247 deletions(-) create mode 100644 examples/actix_example/api/Cargo.toml create mode 100644 examples/actix_example/api/src/lib.rs rename examples/actix_example/{ => api}/static/css/normalize.css (100%) rename examples/actix_example/{ => api}/static/css/skeleton.css (100%) rename examples/actix_example/{ => api}/static/css/style.css (100%) rename examples/actix_example/{ => api}/static/images/favicon.png (100%) rename examples/actix_example/{ => api}/templates/edit.html.tera (100%) rename examples/actix_example/{ => api}/templates/error/404.html.tera (100%) rename examples/actix_example/{ => api}/templates/index.html.tera (100%) rename examples/actix_example/{ => api}/templates/layout.html.tera (100%) rename examples/actix_example/{ => api}/templates/new.html.tera (100%) create mode 100644 examples/actix_example/core/Cargo.toml create mode 100644 examples/actix_example/core/src/lib.rs create mode 100644 examples/actix_example/core/src/mutation.rs create mode 100644 examples/actix_example/core/src/query.rs create mode 100644 examples/actix_example/core/tests/mock.rs create mode 100644 examples/actix_example/core/tests/prepare.rs diff --git a/examples/actix_example/Cargo.toml b/examples/actix_example/Cargo.toml index 8be0a188a..7dc877c2c 100644 --- a/examples/actix_example/Cargo.toml +++ b/examples/actix_example/Cargo.toml @@ -6,30 +6,7 @@ edition = "2021" publish = false [workspace] -members = [".", "entity", "migration"] +members = [".", "api", "core", "entity", "migration"] [dependencies] -actix-files = "0.6" -actix-http = "3" -actix-rt = "2.7" -actix-service = "2" -actix-web = "4" - -tera = "1.15.0" -dotenv = "0.15" -listenfd = "0.5" -serde = "1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } -entity = { path = "entity" } -migration = { path = "migration" } - -[dependencies.sea-orm] -path = "../../" # remove this line in your own project -version = "^0.10.0" # sea-orm version -features = [ - "debug-print", - "runtime-actix-native-tls", - "sqlx-mysql", - # "sqlx-postgres", - # "sqlx-sqlite", -] +actix-example-api = { path = "api" } diff --git a/examples/actix_example/api/Cargo.toml b/examples/actix_example/api/Cargo.toml new file mode 100644 index 000000000..078e5561a --- /dev/null +++ b/examples/actix_example/api/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "actix-example-api" +version = "0.1.0" +authors = ["Sam Samai "] +edition = "2021" +publish = false + +[dependencies] +actix-example-core = { path = "../core" } +actix-files = "0.6" +actix-http = "3" +actix-rt = "2.7" +actix-service = "2" +actix-web = "4" +tera = "1.15.0" +dotenv = "0.15" +listenfd = "0.5" +serde = "1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +entity = { path = "../entity" } +migration = { path = "../migration" } diff --git a/examples/actix_example/api/src/lib.rs b/examples/actix_example/api/src/lib.rs new file mode 100644 index 000000000..f95200e84 --- /dev/null +++ b/examples/actix_example/api/src/lib.rs @@ -0,0 +1,215 @@ +use actix_example_core::{ + sea_orm::{Database, DatabaseConnection}, + Mutation, Query, +}; +use actix_files::Files as Fs; +use actix_web::{ + error, get, middleware, post, web, App, Error, HttpRequest, HttpResponse, HttpServer, Result, +}; + +use entity::post; +use listenfd::ListenFd; +use migration::{Migrator, MigratorTrait}; +use serde::{Deserialize, Serialize}; +use std::env; +use tera::Tera; + +const DEFAULT_POSTS_PER_PAGE: u64 = 5; + +#[derive(Debug, Clone)] +struct AppState { + templates: tera::Tera, + conn: DatabaseConnection, +} + +#[derive(Debug, Deserialize)] +pub struct Params { + page: Option, + posts_per_page: Option, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +struct FlashData { + kind: String, + message: String, +} + +#[get("/")] +async fn list(req: HttpRequest, data: web::Data) -> Result { + let template = &data.templates; + let conn = &data.conn; + + // get params + let params = web::Query::::from_query(req.query_string()).unwrap(); + + let page = params.page.unwrap_or(1); + let posts_per_page = params.posts_per_page.unwrap_or(DEFAULT_POSTS_PER_PAGE); + + let (posts, num_pages) = Query::find_posts_in_page(conn, page, posts_per_page) + .await + .expect("Cannot find posts in page"); + + let mut ctx = tera::Context::new(); + ctx.insert("posts", &posts); + ctx.insert("page", &page); + ctx.insert("posts_per_page", &posts_per_page); + ctx.insert("num_pages", &num_pages); + + let body = template + .render("index.html.tera", &ctx) + .map_err(|_| error::ErrorInternalServerError("Template error"))?; + Ok(HttpResponse::Ok().content_type("text/html").body(body)) +} + +#[get("/new")] +async fn new(data: web::Data) -> Result { + let template = &data.templates; + let ctx = tera::Context::new(); + let body = template + .render("new.html.tera", &ctx) + .map_err(|_| error::ErrorInternalServerError("Template error"))?; + Ok(HttpResponse::Ok().content_type("text/html").body(body)) +} + +#[post("/")] +async fn create( + data: web::Data, + post_form: web::Form, +) -> Result { + let conn = &data.conn; + + let form = post_form.into_inner(); + + Mutation::create_post(conn, form) + .await + .expect("could not insert post"); + + Ok(HttpResponse::Found() + .append_header(("location", "/")) + .finish()) +} + +#[get("/{id}")] +async fn edit(data: web::Data, id: web::Path) -> Result { + let conn = &data.conn; + let template = &data.templates; + let id = id.into_inner(); + + let post: post::Model = Query::find_post_by_id(conn, id) + .await + .expect("could not find post") + .unwrap_or_else(|| panic!("could not find post with id {}", id)); + + let mut ctx = tera::Context::new(); + ctx.insert("post", &post); + + let body = template + .render("edit.html.tera", &ctx) + .map_err(|_| error::ErrorInternalServerError("Template error"))?; + Ok(HttpResponse::Ok().content_type("text/html").body(body)) +} + +#[post("/{id}")] +async fn update( + data: web::Data, + id: web::Path, + post_form: web::Form, +) -> Result { + let conn = &data.conn; + let form = post_form.into_inner(); + let id = id.into_inner(); + + Mutation::update_post_by_id(conn, id, form) + .await + .expect("could not edit post"); + + Ok(HttpResponse::Found() + .append_header(("location", "/")) + .finish()) +} + +#[post("/delete/{id}")] +async fn delete(data: web::Data, id: web::Path) -> Result { + let conn = &data.conn; + let id = id.into_inner(); + + Mutation::delete_post(conn, id) + .await + .expect("could not delete post"); + + Ok(HttpResponse::Found() + .append_header(("location", "/")) + .finish()) +} + +async fn not_found(data: web::Data, request: HttpRequest) -> Result { + let mut ctx = tera::Context::new(); + ctx.insert("uri", request.uri().path()); + + let template = &data.templates; + let body = template + .render("error/404.html.tera", &ctx) + .map_err(|_| error::ErrorInternalServerError("Template error"))?; + + Ok(HttpResponse::Ok().content_type("text/html").body(body)) +} + +#[actix_web::main] +async fn start() -> std::io::Result<()> { + std::env::set_var("RUST_LOG", "debug"); + tracing_subscriber::fmt::init(); + + // get env vars + dotenv::dotenv().ok(); + let db_url = env::var("DATABASE_URL").expect("DATABASE_URL is not set in .env file"); + let host = env::var("HOST").expect("HOST is not set in .env file"); + let port = env::var("PORT").expect("PORT is not set in .env file"); + let server_url = format!("{}:{}", host, port); + + // establish connection to database and apply migrations + // -> create post table if not exists + let conn = Database::connect(&db_url).await.unwrap(); + Migrator::up(&conn, None).await.unwrap(); + + // load tera templates and build app state + let templates = Tera::new(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*")).unwrap(); + let state = AppState { templates, conn }; + + // create server and try to serve over socket if possible + let mut listenfd = ListenFd::from_env(); + let mut server = HttpServer::new(move || { + App::new() + .service(Fs::new("/static", "./api/static")) + .app_data(web::Data::new(state.clone())) + .wrap(middleware::Logger::default()) // enable logger + .default_service(web::route().to(not_found)) + .configure(init) + }); + + server = match listenfd.take_tcp_listener(0)? { + Some(listener) => server.listen(listener)?, + None => server.bind(&server_url)?, + }; + + println!("Starting server at {}", server_url); + server.run().await?; + + Ok(()) +} + +fn init(cfg: &mut web::ServiceConfig) { + cfg.service(list); + cfg.service(new); + cfg.service(create); + cfg.service(edit); + cfg.service(update); + cfg.service(delete); +} + +pub fn main() { + let result = start(); + + if let Some(err) = result.err() { + println!("Error: {}", err); + } +} diff --git a/examples/actix_example/static/css/normalize.css b/examples/actix_example/api/static/css/normalize.css similarity index 100% rename from examples/actix_example/static/css/normalize.css rename to examples/actix_example/api/static/css/normalize.css diff --git a/examples/actix_example/static/css/skeleton.css b/examples/actix_example/api/static/css/skeleton.css similarity index 100% rename from examples/actix_example/static/css/skeleton.css rename to examples/actix_example/api/static/css/skeleton.css diff --git a/examples/actix_example/static/css/style.css b/examples/actix_example/api/static/css/style.css similarity index 100% rename from examples/actix_example/static/css/style.css rename to examples/actix_example/api/static/css/style.css diff --git a/examples/actix_example/static/images/favicon.png b/examples/actix_example/api/static/images/favicon.png similarity index 100% rename from examples/actix_example/static/images/favicon.png rename to examples/actix_example/api/static/images/favicon.png diff --git a/examples/actix_example/templates/edit.html.tera b/examples/actix_example/api/templates/edit.html.tera similarity index 100% rename from examples/actix_example/templates/edit.html.tera rename to examples/actix_example/api/templates/edit.html.tera diff --git a/examples/actix_example/templates/error/404.html.tera b/examples/actix_example/api/templates/error/404.html.tera similarity index 100% rename from examples/actix_example/templates/error/404.html.tera rename to examples/actix_example/api/templates/error/404.html.tera diff --git a/examples/actix_example/templates/index.html.tera b/examples/actix_example/api/templates/index.html.tera similarity index 100% rename from examples/actix_example/templates/index.html.tera rename to examples/actix_example/api/templates/index.html.tera diff --git a/examples/actix_example/templates/layout.html.tera b/examples/actix_example/api/templates/layout.html.tera similarity index 100% rename from examples/actix_example/templates/layout.html.tera rename to examples/actix_example/api/templates/layout.html.tera diff --git a/examples/actix_example/templates/new.html.tera b/examples/actix_example/api/templates/new.html.tera similarity index 100% rename from examples/actix_example/templates/new.html.tera rename to examples/actix_example/api/templates/new.html.tera diff --git a/examples/actix_example/core/Cargo.toml b/examples/actix_example/core/Cargo.toml new file mode 100644 index 000000000..2044b6f22 --- /dev/null +++ b/examples/actix_example/core/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "actix-example-core" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +entity = { path = "../entity" } + +[dependencies.sea-orm] +path = "../../../" # remove this line in your own project +version = "^0.10.0" # sea-orm version +features = [ + "debug-print", + "runtime-async-std-native-tls", + "sqlx-mysql", + # "sqlx-postgres", + # "sqlx-sqlite", +] + +[dev-dependencies] +tokio = { version = "1.20.0", features = ["macros", "rt"] } + +[features] +mock = ["sea-orm/mock"] + +[[test]] +name = "mock" +required-features = ["mock"] diff --git a/examples/actix_example/core/src/lib.rs b/examples/actix_example/core/src/lib.rs new file mode 100644 index 000000000..4a80f2391 --- /dev/null +++ b/examples/actix_example/core/src/lib.rs @@ -0,0 +1,7 @@ +mod mutation; +mod query; + +pub use mutation::*; +pub use query::*; + +pub use sea_orm; diff --git a/examples/actix_example/core/src/mutation.rs b/examples/actix_example/core/src/mutation.rs new file mode 100644 index 000000000..7f0150a63 --- /dev/null +++ b/examples/actix_example/core/src/mutation.rs @@ -0,0 +1,53 @@ +use ::entity::{post, post::Entity as Post}; +use sea_orm::*; + +pub struct Mutation; + +impl Mutation { + pub async fn create_post( + db: &DbConn, + form_data: post::Model, + ) -> Result { + post::ActiveModel { + title: Set(form_data.title.to_owned()), + text: Set(form_data.text.to_owned()), + ..Default::default() + } + .save(db) + .await + } + + pub async fn update_post_by_id( + db: &DbConn, + id: i32, + form_data: post::Model, + ) -> Result { + let post: post::ActiveModel = if let Ok(Some(post)) = Post::find_by_id(id).one(db).await { + post.into() + } else { + return Err(DbErr::Custom("Cannot find post.".to_owned())); + }; + + post::ActiveModel { + id: post.id, + title: Set(form_data.title.to_owned()), + text: Set(form_data.text.to_owned()), + } + .update(db) + .await + } + + pub async fn delete_post(db: &DbConn, id: i32) -> Result { + let post: post::ActiveModel = if let Ok(Some(post)) = Post::find_by_id(id).one(db).await { + post.into() + } else { + return Err(DbErr::Custom("Cannot find post.".to_owned())); + }; + + post.delete(db).await + } + + pub async fn delete_all_posts(db: &DbConn) -> Result { + Post::delete_many().exec(db).await + } +} diff --git a/examples/actix_example/core/src/query.rs b/examples/actix_example/core/src/query.rs new file mode 100644 index 000000000..e8d2668f5 --- /dev/null +++ b/examples/actix_example/core/src/query.rs @@ -0,0 +1,26 @@ +use ::entity::{post, post::Entity as Post}; +use sea_orm::*; + +pub struct Query; + +impl Query { + pub async fn find_post_by_id(db: &DbConn, id: i32) -> Result, DbErr> { + Post::find_by_id(id).one(db).await + } + + /// If ok, returns (post models, num pages). + pub async fn find_posts_in_page( + db: &DbConn, + page: u64, + posts_per_page: u64, + ) -> Result<(Vec, u64), DbErr> { + // Setup paginator + let paginator = Post::find() + .order_by_asc(post::Column::Id) + .paginate(db, posts_per_page); + let num_pages = paginator.num_pages().await?; + + // Fetch paginated posts + paginator.fetch_page(page - 1).await.map(|p| (p, num_pages)) + } +} diff --git a/examples/actix_example/core/tests/mock.rs b/examples/actix_example/core/tests/mock.rs new file mode 100644 index 000000000..76531b671 --- /dev/null +++ b/examples/actix_example/core/tests/mock.rs @@ -0,0 +1,79 @@ +mod prepare; + +use actix_example_core::{Mutation, Query}; +use entity::post; +use prepare::prepare_mock_db; + +#[tokio::test] +async fn main() { + let db = &prepare_mock_db(); + + { + let post = Query::find_post_by_id(db, 1).await.unwrap().unwrap(); + + assert_eq!(post.id, 1); + } + + { + let post = Query::find_post_by_id(db, 5).await.unwrap().unwrap(); + + assert_eq!(post.id, 5); + } + + { + let post = Mutation::create_post( + db, + post::Model { + id: 0, + title: "Title D".to_owned(), + text: "Text D".to_owned(), + }, + ) + .await + .unwrap(); + + assert_eq!( + post, + post::ActiveModel { + id: sea_orm::ActiveValue::Unchanged(6), + title: sea_orm::ActiveValue::Unchanged("Title D".to_owned()), + text: sea_orm::ActiveValue::Unchanged("Text D".to_owned()) + } + ); + } + + { + let post = Mutation::update_post_by_id( + db, + 1, + post::Model { + id: 1, + title: "New Title A".to_owned(), + text: "New Text A".to_owned(), + }, + ) + .await + .unwrap(); + + assert_eq!( + post, + post::Model { + id: 1, + title: "New Title A".to_owned(), + text: "New Text A".to_owned(), + } + ); + } + + { + let result = Mutation::delete_post(db, 5).await.unwrap(); + + assert_eq!(result.rows_affected, 1); + } + + { + let result = Mutation::delete_all_posts(db).await.unwrap(); + + assert_eq!(result.rows_affected, 5); + } +} diff --git a/examples/actix_example/core/tests/prepare.rs b/examples/actix_example/core/tests/prepare.rs new file mode 100644 index 000000000..451804937 --- /dev/null +++ b/examples/actix_example/core/tests/prepare.rs @@ -0,0 +1,50 @@ +use ::entity::post; +use sea_orm::*; + +#[cfg(feature = "mock")] +pub fn prepare_mock_db() -> DatabaseConnection { + MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results(vec![ + vec![post::Model { + id: 1, + title: "Title A".to_owned(), + text: "Text A".to_owned(), + }], + vec![post::Model { + id: 5, + title: "Title C".to_owned(), + text: "Text C".to_owned(), + }], + vec![post::Model { + id: 6, + title: "Title D".to_owned(), + text: "Text D".to_owned(), + }], + vec![post::Model { + id: 1, + title: "Title A".to_owned(), + text: "Text A".to_owned(), + }], + vec![post::Model { + id: 1, + title: "New Title A".to_owned(), + text: "New Text A".to_owned(), + }], + vec![post::Model { + id: 5, + title: "Title C".to_owned(), + text: "Text C".to_owned(), + }], + ]) + .append_exec_results(vec![ + MockExecResult { + last_insert_id: 6, + rows_affected: 1, + }, + MockExecResult { + last_insert_id: 6, + rows_affected: 5, + }, + ]) + .into_connection() +} diff --git a/examples/actix_example/src/main.rs b/examples/actix_example/src/main.rs index fa55c507a..a9cdecdcf 100644 --- a/examples/actix_example/src/main.rs +++ b/examples/actix_example/src/main.rs @@ -1,223 +1,3 @@ -use actix_files::Files as Fs; -use actix_web::{ - error, get, middleware, post, web, App, Error, HttpRequest, HttpResponse, HttpServer, Result, -}; - -use entity::post; -use entity::post::Entity as Post; -use listenfd::ListenFd; -use migration::{Migrator, MigratorTrait}; -use sea_orm::DatabaseConnection; -use sea_orm::{entity::*, query::*}; -use serde::{Deserialize, Serialize}; -use std::env; -use tera::Tera; - -const DEFAULT_POSTS_PER_PAGE: u64 = 5; - -#[derive(Debug, Clone)] -struct AppState { - templates: tera::Tera, - conn: DatabaseConnection, -} - -#[derive(Debug, Deserialize)] -pub struct Params { - page: Option, - posts_per_page: Option, -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -struct FlashData { - kind: String, - message: String, -} - -#[get("/")] -async fn list(req: HttpRequest, data: web::Data) -> Result { - let template = &data.templates; - let conn = &data.conn; - - // get params - let params = web::Query::::from_query(req.query_string()).unwrap(); - - let page = params.page.unwrap_or(1); - let posts_per_page = params.posts_per_page.unwrap_or(DEFAULT_POSTS_PER_PAGE); - let paginator = Post::find() - .order_by_asc(post::Column::Id) - .paginate(conn, posts_per_page); - let num_pages = paginator.num_pages().await.ok().unwrap(); - - let posts = paginator - .fetch_page(page - 1) - .await - .expect("could not retrieve posts"); - let mut ctx = tera::Context::new(); - ctx.insert("posts", &posts); - ctx.insert("page", &page); - ctx.insert("posts_per_page", &posts_per_page); - ctx.insert("num_pages", &num_pages); - - let body = template - .render("index.html.tera", &ctx) - .map_err(|_| error::ErrorInternalServerError("Template error"))?; - Ok(HttpResponse::Ok().content_type("text/html").body(body)) -} - -#[get("/new")] -async fn new(data: web::Data) -> Result { - let template = &data.templates; - let ctx = tera::Context::new(); - let body = template - .render("new.html.tera", &ctx) - .map_err(|_| error::ErrorInternalServerError("Template error"))?; - Ok(HttpResponse::Ok().content_type("text/html").body(body)) -} - -#[post("/")] -async fn create( - data: web::Data, - post_form: web::Form, -) -> Result { - let conn = &data.conn; - - let form = post_form.into_inner(); - - post::ActiveModel { - title: Set(form.title.to_owned()), - text: Set(form.text.to_owned()), - ..Default::default() - } - .save(conn) - .await - .expect("could not insert post"); - - Ok(HttpResponse::Found() - .append_header(("location", "/")) - .finish()) -} - -#[get("/{id}")] -async fn edit(data: web::Data, id: web::Path) -> Result { - let conn = &data.conn; - let template = &data.templates; - - let post: post::Model = Post::find_by_id(id.into_inner()) - .one(conn) - .await - .expect("could not find post") - .unwrap(); - - let mut ctx = tera::Context::new(); - ctx.insert("post", &post); - - let body = template - .render("edit.html.tera", &ctx) - .map_err(|_| error::ErrorInternalServerError("Template error"))?; - Ok(HttpResponse::Ok().content_type("text/html").body(body)) -} - -#[post("/{id}")] -async fn update( - data: web::Data, - id: web::Path, - post_form: web::Form, -) -> Result { - let conn = &data.conn; - let form = post_form.into_inner(); - - post::ActiveModel { - id: Set(id.into_inner()), - title: Set(form.title.to_owned()), - text: Set(form.text.to_owned()), - } - .save(conn) - .await - .expect("could not edit post"); - - Ok(HttpResponse::Found() - .append_header(("location", "/")) - .finish()) -} - -#[post("/delete/{id}")] -async fn delete(data: web::Data, id: web::Path) -> Result { - let conn = &data.conn; - - let post: post::ActiveModel = Post::find_by_id(id.into_inner()) - .one(conn) - .await - .unwrap() - .unwrap() - .into(); - - post.delete(conn).await.unwrap(); - - Ok(HttpResponse::Found() - .append_header(("location", "/")) - .finish()) -} - -async fn not_found(data: web::Data, request: HttpRequest) -> Result { - let mut ctx = tera::Context::new(); - ctx.insert("uri", request.uri().path()); - - let template = &data.templates; - let body = template - .render("error/404.html.tera", &ctx) - .map_err(|_| error::ErrorInternalServerError("Template error"))?; - - Ok(HttpResponse::Ok().content_type("text/html").body(body)) -} - -#[actix_web::main] -async fn main() -> std::io::Result<()> { - std::env::set_var("RUST_LOG", "debug"); - tracing_subscriber::fmt::init(); - - // get env vars - dotenv::dotenv().ok(); - let db_url = env::var("DATABASE_URL").expect("DATABASE_URL is not set in .env file"); - let host = env::var("HOST").expect("HOST is not set in .env file"); - let port = env::var("PORT").expect("PORT is not set in .env file"); - let server_url = format!("{}:{}", host, port); - - // establish connection to database and apply migrations - // -> create post table if not exists - let conn = sea_orm::Database::connect(&db_url).await.unwrap(); - Migrator::up(&conn, None).await.unwrap(); - - // load tera templates and build app state - let templates = Tera::new(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*")).unwrap(); - let state = AppState { templates, conn }; - - // create server and try to serve over socket if possible - let mut listenfd = ListenFd::from_env(); - let mut server = HttpServer::new(move || { - App::new() - .service(Fs::new("/static", "./static")) - .app_data(web::Data::new(state.clone())) - .wrap(middleware::Logger::default()) // enable logger - .default_service(web::route().to(not_found)) - .configure(init) - }); - - server = match listenfd.take_tcp_listener(0)? { - Some(listener) => server.listen(listener)?, - None => server.bind(&server_url)?, - }; - - println!("Starting server at {}", server_url); - server.run().await?; - - Ok(()) -} - -pub fn init(cfg: &mut web::ServiceConfig) { - cfg.service(list); - cfg.service(new); - cfg.service(create); - cfg.service(edit); - cfg.service(update); - cfg.service(delete); +fn main() { + actix_example_api::main(); } From ffd34dca50a0415303a6db5aa95ac88ab9ce9557 Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Thu, 8 Sep 2022 22:47:39 +0100 Subject: [PATCH 21/31] Update Axum example --- examples/axum_example/Cargo.toml | 28 +-- examples/axum_example/api/Cargo.toml | 22 ++ examples/axum_example/{ => api}/src/flash.rs | 0 examples/axum_example/api/src/lib.rs | 210 +++++++++++++++++ .../{ => api}/static/css/normalize.css | 0 .../{ => api}/static/css/skeleton.css | 0 .../{ => api}/static/css/style.css | 0 .../{ => api}/static/images/favicon.png | Bin .../{ => api}/templates/edit.html.tera | 0 .../{ => api}/templates/error/404.html.tera | 0 .../{ => api}/templates/index.html.tera | 0 .../{ => api}/templates/layout.html.tera | 0 .../{ => api}/templates/new.html.tera | 0 examples/axum_example/core/Cargo.toml | 30 +++ examples/axum_example/core/src/lib.rs | 7 + examples/axum_example/core/src/mutation.rs | 53 +++++ examples/axum_example/core/src/query.rs | 26 +++ examples/axum_example/core/tests/mock.rs | 79 +++++++ examples/axum_example/core/tests/prepare.rs | 50 ++++ examples/axum_example/src/main.rs | 221 +----------------- 20 files changed, 481 insertions(+), 245 deletions(-) create mode 100644 examples/axum_example/api/Cargo.toml rename examples/axum_example/{ => api}/src/flash.rs (100%) create mode 100644 examples/axum_example/api/src/lib.rs rename examples/axum_example/{ => api}/static/css/normalize.css (100%) rename examples/axum_example/{ => api}/static/css/skeleton.css (100%) rename examples/axum_example/{ => api}/static/css/style.css (100%) rename examples/axum_example/{ => api}/static/images/favicon.png (100%) rename examples/axum_example/{ => api}/templates/edit.html.tera (100%) rename examples/axum_example/{ => api}/templates/error/404.html.tera (100%) rename examples/axum_example/{ => api}/templates/index.html.tera (100%) rename examples/axum_example/{ => api}/templates/layout.html.tera (100%) rename examples/axum_example/{ => api}/templates/new.html.tera (100%) create mode 100644 examples/axum_example/core/Cargo.toml create mode 100644 examples/axum_example/core/src/lib.rs create mode 100644 examples/axum_example/core/src/mutation.rs create mode 100644 examples/axum_example/core/src/query.rs create mode 100644 examples/axum_example/core/tests/mock.rs create mode 100644 examples/axum_example/core/tests/prepare.rs diff --git a/examples/axum_example/Cargo.toml b/examples/axum_example/Cargo.toml index 43478ad0f..0387b657b 100644 --- a/examples/axum_example/Cargo.toml +++ b/examples/axum_example/Cargo.toml @@ -5,32 +5,8 @@ authors = ["Yoshiera Huang "] edition = "2021" publish = false -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [workspace] -members = [".", "entity", "migration"] +members = [".", "api", "core", "entity", "migration"] [dependencies] -tokio = { version = "1.18.1", features = ["full"] } -axum = "0.5.4" -tower = "0.4.12" -tower-http = { version = "0.3.3", features = ["fs"] } -tower-cookies = "0.6.0" -anyhow = "1.0.57" -dotenv = "0.15.0" -serde = "1.0.137" -serde_json = "1.0.81" -tera = "1.15.0" -tracing-subscriber = { version = "0.3.11", features = ["env-filter"] } -entity = { path = "entity" } -migration = { path = "migration" } - -[dependencies.sea-orm] -path = "../../" # remove this line in your own project -version = "^0.10.0" # sea-orm version -features = [ - "debug-print", - "runtime-tokio-native-tls", - "sqlx-postgres", - # "sqlx-mysql", - # "sqlx-sqlite", -] +axum-example-api = { path = "api" } diff --git a/examples/axum_example/api/Cargo.toml b/examples/axum_example/api/Cargo.toml new file mode 100644 index 000000000..dc1f5ce49 --- /dev/null +++ b/examples/axum_example/api/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "axum-example-api" +version = "0.1.0" +authors = ["Yoshiera Huang "] +edition = "2021" +publish = false + +[dependencies] +axum-example-core = { path = "../core" } +tokio = { version = "1.18.1", features = ["full"] } +axum = "0.5.4" +tower = "0.4.12" +tower-http = { version = "0.3.3", features = ["fs"] } +tower-cookies = "0.6.0" +anyhow = "1.0.57" +dotenv = "0.15.0" +serde = "1.0.137" +serde_json = "1.0.81" +tera = "1.15.0" +tracing-subscriber = { version = "0.3.11", features = ["env-filter"] } +entity = { path = "../entity" } +migration = { path = "../migration" } diff --git a/examples/axum_example/src/flash.rs b/examples/axum_example/api/src/flash.rs similarity index 100% rename from examples/axum_example/src/flash.rs rename to examples/axum_example/api/src/flash.rs diff --git a/examples/axum_example/api/src/lib.rs b/examples/axum_example/api/src/lib.rs new file mode 100644 index 000000000..ead63e7b1 --- /dev/null +++ b/examples/axum_example/api/src/lib.rs @@ -0,0 +1,210 @@ +mod flash; + +use axum::{ + extract::{Extension, Form, Path, Query}, + http::StatusCode, + response::Html, + routing::{get, get_service, post}, + Router, Server, +}; +use axum_example_core::{ + sea_orm::{Database, DatabaseConnection}, + Mutation as MutationCore, Query as QueryCore, +}; +use entity::post; +use flash::{get_flash_cookie, post_response, PostResponse}; +use migration::{Migrator, MigratorTrait}; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; +use std::{env, net::SocketAddr}; +use tera::Tera; +use tower::ServiceBuilder; +use tower_cookies::{CookieManagerLayer, Cookies}; +use tower_http::services::ServeDir; + +#[tokio::main] +async fn start() -> anyhow::Result<()> { + env::set_var("RUST_LOG", "debug"); + tracing_subscriber::fmt::init(); + + dotenv::dotenv().ok(); + let db_url = env::var("DATABASE_URL").expect("DATABASE_URL is not set in .env file"); + let host = env::var("HOST").expect("HOST is not set in .env file"); + let port = env::var("PORT").expect("PORT is not set in .env file"); + let server_url = format!("{}:{}", host, port); + + let conn = Database::connect(db_url) + .await + .expect("Database connection failed"); + Migrator::up(&conn, None).await.unwrap(); + let templates = Tera::new(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*")) + .expect("Tera initialization failed"); + // let state = AppState { templates, conn }; + + let app = Router::new() + .route("/", get(list_posts).post(create_post)) + .route("/:id", get(edit_post).post(update_post)) + .route("/new", get(new_post)) + .route("/delete/:id", post(delete_post)) + .nest( + "/static", + get_service(ServeDir::new(concat!( + env!("CARGO_MANIFEST_DIR"), + "/static" + ))) + .handle_error(|error: std::io::Error| async move { + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Unhandled internal error: {}", error), + ) + }), + ) + .layer( + ServiceBuilder::new() + .layer(CookieManagerLayer::new()) + .layer(Extension(conn)) + .layer(Extension(templates)), + ); + + let addr = SocketAddr::from_str(&server_url).unwrap(); + Server::bind(&addr).serve(app.into_make_service()).await?; + + Ok(()) +} + +#[derive(Deserialize)] +struct Params { + page: Option, + posts_per_page: Option, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +struct FlashData { + kind: String, + message: String, +} + +async fn list_posts( + Extension(ref templates): Extension, + Extension(ref conn): Extension, + Query(params): Query, + cookies: Cookies, +) -> Result, (StatusCode, &'static str)> { + let page = params.page.unwrap_or(1); + let posts_per_page = params.posts_per_page.unwrap_or(5); + + let (posts, num_pages) = QueryCore::find_posts_in_page(conn, page, posts_per_page) + .await + .expect("Cannot find posts in page"); + + let mut ctx = tera::Context::new(); + ctx.insert("posts", &posts); + ctx.insert("page", &page); + ctx.insert("posts_per_page", &posts_per_page); + ctx.insert("num_pages", &num_pages); + + if let Some(value) = get_flash_cookie::(&cookies) { + ctx.insert("flash", &value); + } + + let body = templates + .render("index.html.tera", &ctx) + .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Template error"))?; + + Ok(Html(body)) +} + +async fn new_post( + Extension(ref templates): Extension, +) -> Result, (StatusCode, &'static str)> { + let ctx = tera::Context::new(); + let body = templates + .render("new.html.tera", &ctx) + .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Template error"))?; + + Ok(Html(body)) +} + +async fn create_post( + Extension(ref conn): Extension, + form: Form, + mut cookies: Cookies, +) -> Result { + let form = form.0; + + MutationCore::create_post(conn, form) + .await + .expect("could not insert post"); + + let data = FlashData { + kind: "success".to_owned(), + message: "Post succcessfully added".to_owned(), + }; + + Ok(post_response(&mut cookies, data)) +} + +async fn edit_post( + Extension(ref templates): Extension, + Extension(ref conn): Extension, + Path(id): Path, +) -> Result, (StatusCode, &'static str)> { + let post: post::Model = QueryCore::find_post_by_id(conn, id) + .await + .expect("could not find post") + .unwrap_or_else(|| panic!("could not find post with id {}", id)); + + let mut ctx = tera::Context::new(); + ctx.insert("post", &post); + + let body = templates + .render("edit.html.tera", &ctx) + .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Template error"))?; + + Ok(Html(body)) +} + +async fn update_post( + Extension(ref conn): Extension, + Path(id): Path, + form: Form, + mut cookies: Cookies, +) -> Result { + let form = form.0; + + MutationCore::update_post_by_id(conn, id, form) + .await + .expect("could not edit post"); + + let data = FlashData { + kind: "success".to_owned(), + message: "Post succcessfully updated".to_owned(), + }; + + Ok(post_response(&mut cookies, data)) +} + +async fn delete_post( + Extension(ref conn): Extension, + Path(id): Path, + mut cookies: Cookies, +) -> Result { + MutationCore::delete_post(conn, id) + .await + .expect("could not delete post"); + + let data = FlashData { + kind: "success".to_owned(), + message: "Post succcessfully deleted".to_owned(), + }; + + Ok(post_response(&mut cookies, data)) +} + +pub fn main() { + let result = start(); + + if let Some(err) = result.err() { + println!("Error: {}", err); + } +} diff --git a/examples/axum_example/static/css/normalize.css b/examples/axum_example/api/static/css/normalize.css similarity index 100% rename from examples/axum_example/static/css/normalize.css rename to examples/axum_example/api/static/css/normalize.css diff --git a/examples/axum_example/static/css/skeleton.css b/examples/axum_example/api/static/css/skeleton.css similarity index 100% rename from examples/axum_example/static/css/skeleton.css rename to examples/axum_example/api/static/css/skeleton.css diff --git a/examples/axum_example/static/css/style.css b/examples/axum_example/api/static/css/style.css similarity index 100% rename from examples/axum_example/static/css/style.css rename to examples/axum_example/api/static/css/style.css diff --git a/examples/axum_example/static/images/favicon.png b/examples/axum_example/api/static/images/favicon.png similarity index 100% rename from examples/axum_example/static/images/favicon.png rename to examples/axum_example/api/static/images/favicon.png diff --git a/examples/axum_example/templates/edit.html.tera b/examples/axum_example/api/templates/edit.html.tera similarity index 100% rename from examples/axum_example/templates/edit.html.tera rename to examples/axum_example/api/templates/edit.html.tera diff --git a/examples/axum_example/templates/error/404.html.tera b/examples/axum_example/api/templates/error/404.html.tera similarity index 100% rename from examples/axum_example/templates/error/404.html.tera rename to examples/axum_example/api/templates/error/404.html.tera diff --git a/examples/axum_example/templates/index.html.tera b/examples/axum_example/api/templates/index.html.tera similarity index 100% rename from examples/axum_example/templates/index.html.tera rename to examples/axum_example/api/templates/index.html.tera diff --git a/examples/axum_example/templates/layout.html.tera b/examples/axum_example/api/templates/layout.html.tera similarity index 100% rename from examples/axum_example/templates/layout.html.tera rename to examples/axum_example/api/templates/layout.html.tera diff --git a/examples/axum_example/templates/new.html.tera b/examples/axum_example/api/templates/new.html.tera similarity index 100% rename from examples/axum_example/templates/new.html.tera rename to examples/axum_example/api/templates/new.html.tera diff --git a/examples/axum_example/core/Cargo.toml b/examples/axum_example/core/Cargo.toml new file mode 100644 index 000000000..2ba788749 --- /dev/null +++ b/examples/axum_example/core/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "axum-example-core" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +entity = { path = "../entity" } + +[dependencies.sea-orm] +path = "../../../" # remove this line in your own project +version = "^0.10.0" # sea-orm version +features = [ + "debug-print", + "runtime-async-std-native-tls", + "sqlx-postgres", + # "sqlx-mysql", + # "sqlx-sqlite", +] + +[dev-dependencies] +tokio = { version = "1.20.0", features = ["macros", "rt"] } + +[features] +mock = ["sea-orm/mock"] + +[[test]] +name = "mock" +required-features = ["mock"] diff --git a/examples/axum_example/core/src/lib.rs b/examples/axum_example/core/src/lib.rs new file mode 100644 index 000000000..4a80f2391 --- /dev/null +++ b/examples/axum_example/core/src/lib.rs @@ -0,0 +1,7 @@ +mod mutation; +mod query; + +pub use mutation::*; +pub use query::*; + +pub use sea_orm; diff --git a/examples/axum_example/core/src/mutation.rs b/examples/axum_example/core/src/mutation.rs new file mode 100644 index 000000000..7f0150a63 --- /dev/null +++ b/examples/axum_example/core/src/mutation.rs @@ -0,0 +1,53 @@ +use ::entity::{post, post::Entity as Post}; +use sea_orm::*; + +pub struct Mutation; + +impl Mutation { + pub async fn create_post( + db: &DbConn, + form_data: post::Model, + ) -> Result { + post::ActiveModel { + title: Set(form_data.title.to_owned()), + text: Set(form_data.text.to_owned()), + ..Default::default() + } + .save(db) + .await + } + + pub async fn update_post_by_id( + db: &DbConn, + id: i32, + form_data: post::Model, + ) -> Result { + let post: post::ActiveModel = if let Ok(Some(post)) = Post::find_by_id(id).one(db).await { + post.into() + } else { + return Err(DbErr::Custom("Cannot find post.".to_owned())); + }; + + post::ActiveModel { + id: post.id, + title: Set(form_data.title.to_owned()), + text: Set(form_data.text.to_owned()), + } + .update(db) + .await + } + + pub async fn delete_post(db: &DbConn, id: i32) -> Result { + let post: post::ActiveModel = if let Ok(Some(post)) = Post::find_by_id(id).one(db).await { + post.into() + } else { + return Err(DbErr::Custom("Cannot find post.".to_owned())); + }; + + post.delete(db).await + } + + pub async fn delete_all_posts(db: &DbConn) -> Result { + Post::delete_many().exec(db).await + } +} diff --git a/examples/axum_example/core/src/query.rs b/examples/axum_example/core/src/query.rs new file mode 100644 index 000000000..e8d2668f5 --- /dev/null +++ b/examples/axum_example/core/src/query.rs @@ -0,0 +1,26 @@ +use ::entity::{post, post::Entity as Post}; +use sea_orm::*; + +pub struct Query; + +impl Query { + pub async fn find_post_by_id(db: &DbConn, id: i32) -> Result, DbErr> { + Post::find_by_id(id).one(db).await + } + + /// If ok, returns (post models, num pages). + pub async fn find_posts_in_page( + db: &DbConn, + page: u64, + posts_per_page: u64, + ) -> Result<(Vec, u64), DbErr> { + // Setup paginator + let paginator = Post::find() + .order_by_asc(post::Column::Id) + .paginate(db, posts_per_page); + let num_pages = paginator.num_pages().await?; + + // Fetch paginated posts + paginator.fetch_page(page - 1).await.map(|p| (p, num_pages)) + } +} diff --git a/examples/axum_example/core/tests/mock.rs b/examples/axum_example/core/tests/mock.rs new file mode 100644 index 000000000..832105302 --- /dev/null +++ b/examples/axum_example/core/tests/mock.rs @@ -0,0 +1,79 @@ +mod prepare; + +use axum_example_core::{Mutation, Query}; +use entity::post; +use prepare::prepare_mock_db; + +#[tokio::test] +async fn main() { + let db = &prepare_mock_db(); + + { + let post = Query::find_post_by_id(db, 1).await.unwrap().unwrap(); + + assert_eq!(post.id, 1); + } + + { + let post = Query::find_post_by_id(db, 5).await.unwrap().unwrap(); + + assert_eq!(post.id, 5); + } + + { + let post = Mutation::create_post( + db, + post::Model { + id: 0, + title: "Title D".to_owned(), + text: "Text D".to_owned(), + }, + ) + .await + .unwrap(); + + assert_eq!( + post, + post::ActiveModel { + id: sea_orm::ActiveValue::Unchanged(6), + title: sea_orm::ActiveValue::Unchanged("Title D".to_owned()), + text: sea_orm::ActiveValue::Unchanged("Text D".to_owned()) + } + ); + } + + { + let post = Mutation::update_post_by_id( + db, + 1, + post::Model { + id: 1, + title: "New Title A".to_owned(), + text: "New Text A".to_owned(), + }, + ) + .await + .unwrap(); + + assert_eq!( + post, + post::Model { + id: 1, + title: "New Title A".to_owned(), + text: "New Text A".to_owned(), + } + ); + } + + { + let result = Mutation::delete_post(db, 5).await.unwrap(); + + assert_eq!(result.rows_affected, 1); + } + + { + let result = Mutation::delete_all_posts(db).await.unwrap(); + + assert_eq!(result.rows_affected, 5); + } +} diff --git a/examples/axum_example/core/tests/prepare.rs b/examples/axum_example/core/tests/prepare.rs new file mode 100644 index 000000000..451804937 --- /dev/null +++ b/examples/axum_example/core/tests/prepare.rs @@ -0,0 +1,50 @@ +use ::entity::post; +use sea_orm::*; + +#[cfg(feature = "mock")] +pub fn prepare_mock_db() -> DatabaseConnection { + MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results(vec![ + vec![post::Model { + id: 1, + title: "Title A".to_owned(), + text: "Text A".to_owned(), + }], + vec![post::Model { + id: 5, + title: "Title C".to_owned(), + text: "Text C".to_owned(), + }], + vec![post::Model { + id: 6, + title: "Title D".to_owned(), + text: "Text D".to_owned(), + }], + vec![post::Model { + id: 1, + title: "Title A".to_owned(), + text: "Text A".to_owned(), + }], + vec![post::Model { + id: 1, + title: "New Title A".to_owned(), + text: "New Text A".to_owned(), + }], + vec![post::Model { + id: 5, + title: "Title C".to_owned(), + text: "Text C".to_owned(), + }], + ]) + .append_exec_results(vec![ + MockExecResult { + last_insert_id: 6, + rows_affected: 1, + }, + MockExecResult { + last_insert_id: 6, + rows_affected: 5, + }, + ]) + .into_connection() +} diff --git a/examples/axum_example/src/main.rs b/examples/axum_example/src/main.rs index 3669faca2..e0b58d2ec 100644 --- a/examples/axum_example/src/main.rs +++ b/examples/axum_example/src/main.rs @@ -1,220 +1,3 @@ -mod flash; - -use axum::{ - extract::{Extension, Form, Path, Query}, - http::StatusCode, - response::Html, - routing::{get, get_service, post}, - Router, Server, -}; -use entity::post; -use flash::{get_flash_cookie, post_response, PostResponse}; -use migration::{Migrator, MigratorTrait}; -use post::Entity as Post; -use sea_orm::{prelude::*, Database, QueryOrder, Set}; -use serde::{Deserialize, Serialize}; -use std::str::FromStr; -use std::{env, net::SocketAddr}; -use tera::Tera; -use tower::ServiceBuilder; -use tower_cookies::{CookieManagerLayer, Cookies}; -use tower_http::services::ServeDir; - -#[tokio::main] -async fn main() -> anyhow::Result<()> { - env::set_var("RUST_LOG", "debug"); - tracing_subscriber::fmt::init(); - - dotenv::dotenv().ok(); - let db_url = env::var("DATABASE_URL").expect("DATABASE_URL is not set in .env file"); - let host = env::var("HOST").expect("HOST is not set in .env file"); - let port = env::var("PORT").expect("PORT is not set in .env file"); - let server_url = format!("{}:{}", host, port); - - let conn = Database::connect(db_url) - .await - .expect("Database connection failed"); - Migrator::up(&conn, None).await.unwrap(); - let templates = Tera::new(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*")) - .expect("Tera initialization failed"); - // let state = AppState { templates, conn }; - - let app = Router::new() - .route("/", get(list_posts).post(create_post)) - .route("/:id", get(edit_post).post(update_post)) - .route("/new", get(new_post)) - .route("/delete/:id", post(delete_post)) - .nest( - "/static", - get_service(ServeDir::new(concat!( - env!("CARGO_MANIFEST_DIR"), - "/static" - ))) - .handle_error(|error: std::io::Error| async move { - ( - StatusCode::INTERNAL_SERVER_ERROR, - format!("Unhandled internal error: {}", error), - ) - }), - ) - .layer( - ServiceBuilder::new() - .layer(CookieManagerLayer::new()) - .layer(Extension(conn)) - .layer(Extension(templates)), - ); - - let addr = SocketAddr::from_str(&server_url).unwrap(); - Server::bind(&addr).serve(app.into_make_service()).await?; - - Ok(()) -} - -#[derive(Deserialize)] -struct Params { - page: Option, - posts_per_page: Option, -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -struct FlashData { - kind: String, - message: String, -} - -async fn list_posts( - Extension(ref templates): Extension, - Extension(ref conn): Extension, - Query(params): Query, - cookies: Cookies, -) -> Result, (StatusCode, &'static str)> { - let page = params.page.unwrap_or(1); - let posts_per_page = params.posts_per_page.unwrap_or(5); - let paginator = Post::find() - .order_by_asc(post::Column::Id) - .paginate(conn, posts_per_page); - let num_pages = paginator.num_pages().await.ok().unwrap(); - let posts = paginator - .fetch_page(page - 1) - .await - .expect("could not retrieve posts"); - - let mut ctx = tera::Context::new(); - ctx.insert("posts", &posts); - ctx.insert("page", &page); - ctx.insert("posts_per_page", &posts_per_page); - ctx.insert("num_pages", &num_pages); - - if let Some(value) = get_flash_cookie::(&cookies) { - ctx.insert("flash", &value); - } - - let body = templates - .render("index.html.tera", &ctx) - .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Template error"))?; - - Ok(Html(body)) -} - -async fn new_post( - Extension(ref templates): Extension, -) -> Result, (StatusCode, &'static str)> { - let ctx = tera::Context::new(); - let body = templates - .render("new.html.tera", &ctx) - .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Template error"))?; - - Ok(Html(body)) -} - -async fn create_post( - Extension(ref conn): Extension, - form: Form, - mut cookies: Cookies, -) -> Result { - let model = form.0; - - post::ActiveModel { - title: Set(model.title.to_owned()), - text: Set(model.text.to_owned()), - ..Default::default() - } - .save(conn) - .await - .expect("could not insert post"); - - let data = FlashData { - kind: "success".to_owned(), - message: "Post succcessfully added".to_owned(), - }; - - Ok(post_response(&mut cookies, data)) -} - -async fn edit_post( - Extension(ref templates): Extension, - Extension(ref conn): Extension, - Path(id): Path, -) -> Result, (StatusCode, &'static str)> { - let post: post::Model = Post::find_by_id(id) - .one(conn) - .await - .expect("could not find post") - .unwrap(); - - let mut ctx = tera::Context::new(); - ctx.insert("post", &post); - - let body = templates - .render("edit.html.tera", &ctx) - .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Template error"))?; - - Ok(Html(body)) -} - -async fn update_post( - Extension(ref conn): Extension, - Path(id): Path, - form: Form, - mut cookies: Cookies, -) -> Result { - let model = form.0; - - post::ActiveModel { - id: Set(id), - title: Set(model.title.to_owned()), - text: Set(model.text.to_owned()), - } - .save(conn) - .await - .expect("could not edit post"); - - let data = FlashData { - kind: "success".to_owned(), - message: "Post succcessfully updated".to_owned(), - }; - - Ok(post_response(&mut cookies, data)) -} - -async fn delete_post( - Extension(ref conn): Extension, - Path(id): Path, - mut cookies: Cookies, -) -> Result { - let post: post::ActiveModel = Post::find_by_id(id) - .one(conn) - .await - .unwrap() - .unwrap() - .into(); - - post.delete(conn).await.unwrap(); - - let data = FlashData { - kind: "success".to_owned(), - message: "Post succcessfully deleted".to_owned(), - }; - - Ok(post_response(&mut cookies, data)) +fn main() { + axum_example_api::main(); } From d211f0bbebf739cc148d7bb99dbfff1581559c27 Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Thu, 8 Sep 2022 23:49:42 +0100 Subject: [PATCH 22/31] Update GraphQL example --- examples/graphql_example/Cargo.toml | 19 +---- examples/graphql_example/api/Cargo.toml | 15 ++++ examples/graphql_example/api/src/db.rs | 21 +++++ .../{ => api}/src/graphql/mod.rs | 0 .../{ => api}/src/graphql/mutation/mod.rs | 0 .../{ => api}/src/graphql/mutation/note.rs | 28 ++++--- .../{ => api}/src/graphql/query/mod.rs | 0 .../{ => api}/src/graphql/query/note.rs | 10 +-- .../{ => api}/src/graphql/schema.rs | 0 examples/graphql_example/api/src/lib.rs | 49 ++++++++++++ examples/graphql_example/core/Cargo.toml | 30 +++++++ examples/graphql_example/core/src/lib.rs | 7 ++ examples/graphql_example/core/src/mutation.rs | 54 +++++++++++++ examples/graphql_example/core/src/query.rs | 30 +++++++ examples/graphql_example/core/tests/mock.rs | 79 +++++++++++++++++++ .../graphql_example/core/tests/prepare.rs | 50 ++++++++++++ examples/graphql_example/src/db.rs | 19 ----- examples/graphql_example/src/main.rs | 50 +----------- 18 files changed, 361 insertions(+), 100 deletions(-) create mode 100644 examples/graphql_example/api/Cargo.toml create mode 100644 examples/graphql_example/api/src/db.rs rename examples/graphql_example/{ => api}/src/graphql/mod.rs (100%) rename examples/graphql_example/{ => api}/src/graphql/mutation/mod.rs (100%) rename examples/graphql_example/{ => api}/src/graphql/mutation/note.rs (68%) rename examples/graphql_example/{ => api}/src/graphql/query/mod.rs (100%) rename examples/graphql_example/{ => api}/src/graphql/query/note.rs (75%) rename examples/graphql_example/{ => api}/src/graphql/schema.rs (100%) create mode 100644 examples/graphql_example/api/src/lib.rs create mode 100644 examples/graphql_example/core/Cargo.toml create mode 100644 examples/graphql_example/core/src/lib.rs create mode 100644 examples/graphql_example/core/src/mutation.rs create mode 100644 examples/graphql_example/core/src/query.rs create mode 100644 examples/graphql_example/core/tests/mock.rs create mode 100644 examples/graphql_example/core/tests/prepare.rs delete mode 100644 examples/graphql_example/src/db.rs diff --git a/examples/graphql_example/Cargo.toml b/examples/graphql_example/Cargo.toml index 107de341c..c3e342dc3 100644 --- a/examples/graphql_example/Cargo.toml +++ b/examples/graphql_example/Cargo.toml @@ -7,22 +7,7 @@ publish = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [workspace] -members = [".", "entity", "migration"] +members = [".", "api", "core", "entity", "migration"] [dependencies] -tokio = { version = "1.0", features = ["full"] } -axum = "^0.5.1" -dotenv = "0.15.0" -async-graphql-axum = "^4.0.6" -entity = { path = "entity" } -migration = { path = "migration" } - -[dependencies.sea-orm] -path = "../../" # remove this line in your own project -version = "^0.10.0" # sea-orm version -features = [ - "runtime-tokio-native-tls", - # "sqlx-postgres", - # "sqlx-mysql", - "sqlx-sqlite" -] +graphql-example-api = { path = "api" } diff --git a/examples/graphql_example/api/Cargo.toml b/examples/graphql_example/api/Cargo.toml new file mode 100644 index 000000000..e4526410b --- /dev/null +++ b/examples/graphql_example/api/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "graphql-example-api" +authors = ["Aaron Leopold "] +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +graphql-example-core = { path = "../core" } +tokio = { version = "1.0", features = ["full"] } +axum = "^0.5.1" +dotenv = "0.15.0" +async-graphql-axum = "^4.0.6" +entity = { path = "../entity" } +migration = { path = "../migration" } diff --git a/examples/graphql_example/api/src/db.rs b/examples/graphql_example/api/src/db.rs new file mode 100644 index 000000000..a1b79cd41 --- /dev/null +++ b/examples/graphql_example/api/src/db.rs @@ -0,0 +1,21 @@ +use graphql_example_core::sea_orm::DatabaseConnection; + +pub struct Database { + pub connection: DatabaseConnection, +} + +impl Database { + pub async fn new() -> Self { + let connection = graphql_example_core::sea_orm::Database::connect( + std::env::var("DATABASE_URL").unwrap(), + ) + .await + .expect("Could not connect to database"); + + Database { connection } + } + + pub fn get_connection(&self) -> &DatabaseConnection { + &self.connection + } +} diff --git a/examples/graphql_example/src/graphql/mod.rs b/examples/graphql_example/api/src/graphql/mod.rs similarity index 100% rename from examples/graphql_example/src/graphql/mod.rs rename to examples/graphql_example/api/src/graphql/mod.rs diff --git a/examples/graphql_example/src/graphql/mutation/mod.rs b/examples/graphql_example/api/src/graphql/mutation/mod.rs similarity index 100% rename from examples/graphql_example/src/graphql/mutation/mod.rs rename to examples/graphql_example/api/src/graphql/mutation/mod.rs diff --git a/examples/graphql_example/src/graphql/mutation/note.rs b/examples/graphql_example/api/src/graphql/mutation/note.rs similarity index 68% rename from examples/graphql_example/src/graphql/mutation/note.rs rename to examples/graphql_example/api/src/graphql/mutation/note.rs index 600b462b8..a3ce9f48f 100644 --- a/examples/graphql_example/src/graphql/mutation/note.rs +++ b/examples/graphql_example/api/src/graphql/mutation/note.rs @@ -1,7 +1,7 @@ use async_graphql::{Context, Object, Result}; use entity::async_graphql::{self, InputObject, SimpleObject}; use entity::note; -use sea_orm::{ActiveModelTrait, Set}; +use graphql_example_core::Mutation; use crate::db::Database; @@ -14,6 +14,16 @@ pub struct CreateNoteInput { pub text: String, } +impl CreateNoteInput { + fn into_model_with_arbitrary_id(self) -> note::Model { + note::Model { + id: 0, + title: self.title, + text: self.text, + } + } +} + #[derive(SimpleObject)] pub struct DeleteResult { pub success: bool, @@ -31,22 +41,18 @@ impl NoteMutation { input: CreateNoteInput, ) -> Result { let db = ctx.data::().unwrap(); + let conn = db.get_connection(); - let note = note::ActiveModel { - title: Set(input.title), - text: Set(input.text), - ..Default::default() - }; - - Ok(note.insert(db.get_connection()).await?) + Ok(Mutation::create_note(conn, input.into_model_with_arbitrary_id()).await?) } pub async fn delete_note(&self, ctx: &Context<'_>, id: i32) -> Result { let db = ctx.data::().unwrap(); + let conn = db.get_connection(); - let res = note::Entity::delete_by_id(id) - .exec(db.get_connection()) - .await?; + let res = Mutation::delete_note(conn, id) + .await + .expect("Cannot delete note"); if res.rows_affected <= 1 { Ok(DeleteResult { diff --git a/examples/graphql_example/src/graphql/query/mod.rs b/examples/graphql_example/api/src/graphql/query/mod.rs similarity index 100% rename from examples/graphql_example/src/graphql/query/mod.rs rename to examples/graphql_example/api/src/graphql/query/mod.rs diff --git a/examples/graphql_example/src/graphql/query/note.rs b/examples/graphql_example/api/src/graphql/query/note.rs similarity index 75% rename from examples/graphql_example/src/graphql/query/note.rs rename to examples/graphql_example/api/src/graphql/query/note.rs index 696d47206..1ac2549d9 100644 --- a/examples/graphql_example/src/graphql/query/note.rs +++ b/examples/graphql_example/api/src/graphql/query/note.rs @@ -1,6 +1,6 @@ use async_graphql::{Context, Object, Result}; use entity::{async_graphql, note}; -use sea_orm::EntityTrait; +use graphql_example_core::Query; use crate::db::Database; @@ -11,18 +11,18 @@ pub struct NoteQuery; impl NoteQuery { async fn get_notes(&self, ctx: &Context<'_>) -> Result> { let db = ctx.data::().unwrap(); + let conn = db.get_connection(); - Ok(note::Entity::find() - .all(db.get_connection()) + Ok(Query::get_all_notes(conn) .await .map_err(|e| e.to_string())?) } async fn get_note_by_id(&self, ctx: &Context<'_>, id: i32) -> Result> { let db = ctx.data::().unwrap(); + let conn = db.get_connection(); - Ok(note::Entity::find_by_id(id) - .one(db.get_connection()) + Ok(Query::find_note_by_id(conn, id) .await .map_err(|e| e.to_string())?) } diff --git a/examples/graphql_example/src/graphql/schema.rs b/examples/graphql_example/api/src/graphql/schema.rs similarity index 100% rename from examples/graphql_example/src/graphql/schema.rs rename to examples/graphql_example/api/src/graphql/schema.rs diff --git a/examples/graphql_example/api/src/lib.rs b/examples/graphql_example/api/src/lib.rs new file mode 100644 index 000000000..42e63e1cf --- /dev/null +++ b/examples/graphql_example/api/src/lib.rs @@ -0,0 +1,49 @@ +mod db; +mod graphql; + +use entity::async_graphql; + +use async_graphql::http::{playground_source, GraphQLPlaygroundConfig}; +use async_graphql_axum::{GraphQLRequest, GraphQLResponse}; +use axum::{ + extract::Extension, + response::{Html, IntoResponse}, + routing::get, + Router, +}; +use graphql::schema::{build_schema, AppSchema}; + +#[cfg(debug_assertions)] +use dotenv::dotenv; + +async fn graphql_handler(schema: Extension, req: GraphQLRequest) -> GraphQLResponse { + schema.execute(req.into_inner()).await.into() +} + +async fn graphql_playground() -> impl IntoResponse { + Html(playground_source(GraphQLPlaygroundConfig::new( + "/api/graphql", + ))) +} + +#[tokio::main] +pub async fn main() { + #[cfg(debug_assertions)] + dotenv().ok(); + + let schema = build_schema().await; + + let app = Router::new() + .route( + "/api/graphql", + get(graphql_playground).post(graphql_handler), + ) + .layer(Extension(schema)); + + println!("Playground: http://localhost:3000/api/graphql"); + + axum::Server::bind(&"0.0.0.0:3000".parse().unwrap()) + .serve(app.into_make_service()) + .await + .unwrap(); +} diff --git a/examples/graphql_example/core/Cargo.toml b/examples/graphql_example/core/Cargo.toml new file mode 100644 index 000000000..739d4b38f --- /dev/null +++ b/examples/graphql_example/core/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "graphql-example-core" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +entity = { path = "../entity" } + +[dependencies.sea-orm] +path = "../../../" # remove this line in your own project +version = "^0.10.0" # sea-orm version +features = [ + "debug-print", + "runtime-async-std-native-tls", + # "sqlx-postgres", + # "sqlx-mysql", + "sqlx-sqlite", +] + +[dev-dependencies] +tokio = { version = "1.20.0", features = ["macros", "rt"] } + +[features] +mock = ["sea-orm/mock"] + +[[test]] +name = "mock" +required-features = ["mock"] diff --git a/examples/graphql_example/core/src/lib.rs b/examples/graphql_example/core/src/lib.rs new file mode 100644 index 000000000..4a80f2391 --- /dev/null +++ b/examples/graphql_example/core/src/lib.rs @@ -0,0 +1,7 @@ +mod mutation; +mod query; + +pub use mutation::*; +pub use query::*; + +pub use sea_orm; diff --git a/examples/graphql_example/core/src/mutation.rs b/examples/graphql_example/core/src/mutation.rs new file mode 100644 index 000000000..6eac6814a --- /dev/null +++ b/examples/graphql_example/core/src/mutation.rs @@ -0,0 +1,54 @@ +use ::entity::{note, note::Entity as Note}; +use sea_orm::*; + +pub struct Mutation; + +impl Mutation { + pub async fn create_note(db: &DbConn, form_data: note::Model) -> Result { + let active_model = note::ActiveModel { + title: Set(form_data.title.to_owned()), + text: Set(form_data.text.to_owned()), + ..Default::default() + }; + let res = Note::insert(active_model).exec(db).await?; + + Ok(note::Model { + id: res.last_insert_id, + ..form_data + }) + } + + pub async fn update_note_by_id( + db: &DbConn, + id: i32, + form_data: note::Model, + ) -> Result { + let note: note::ActiveModel = if let Ok(Some(note)) = Note::find_by_id(id).one(db).await { + note.into() + } else { + return Err(DbErr::Custom("Cannot find note.".to_owned())); + }; + + note::ActiveModel { + id: note.id, + title: Set(form_data.title.to_owned()), + text: Set(form_data.text.to_owned()), + } + .update(db) + .await + } + + pub async fn delete_note(db: &DbConn, id: i32) -> Result { + let note: note::ActiveModel = if let Ok(Some(note)) = Note::find_by_id(id).one(db).await { + note.into() + } else { + return Err(DbErr::Custom("Cannot find note.".to_owned())); + }; + + note.delete(db).await + } + + pub async fn delete_all_notes(db: &DbConn) -> Result { + Note::delete_many().exec(db).await + } +} diff --git a/examples/graphql_example/core/src/query.rs b/examples/graphql_example/core/src/query.rs new file mode 100644 index 000000000..7b6dc1b7d --- /dev/null +++ b/examples/graphql_example/core/src/query.rs @@ -0,0 +1,30 @@ +use ::entity::{note, note::Entity as Note}; +use sea_orm::*; + +pub struct Query; + +impl Query { + pub async fn find_note_by_id(db: &DbConn, id: i32) -> Result, DbErr> { + Note::find_by_id(id).one(db).await + } + + pub async fn get_all_notes(db: &DbConn) -> Result, DbErr> { + Note::find().all(db).await + } + + /// If ok, returns (note models, num pages). + pub async fn find_notes_in_page( + db: &DbConn, + page: u64, + notes_per_page: u64, + ) -> Result<(Vec, u64), DbErr> { + // Setup paginator + let paginator = Note::find() + .order_by_asc(note::Column::Id) + .paginate(db, notes_per_page); + let num_pages = paginator.num_pages().await?; + + // Fetch paginated notes + paginator.fetch_page(page - 1).await.map(|p| (p, num_pages)) + } +} diff --git a/examples/graphql_example/core/tests/mock.rs b/examples/graphql_example/core/tests/mock.rs new file mode 100644 index 000000000..16a56189f --- /dev/null +++ b/examples/graphql_example/core/tests/mock.rs @@ -0,0 +1,79 @@ +mod prepare; + +use entity::note; +use graphql_example_core::{Mutation, Query}; +use prepare::prepare_mock_db; + +#[tokio::test] +async fn main() { + let db = &prepare_mock_db(); + + { + let note = Query::find_note_by_id(db, 1).await.unwrap().unwrap(); + + assert_eq!(note.id, 1); + } + + { + let note = Query::find_note_by_id(db, 5).await.unwrap().unwrap(); + + assert_eq!(note.id, 5); + } + + { + let note = Mutation::create_note( + db, + note::Model { + id: 0, + title: "Title D".to_owned(), + text: "Text D".to_owned(), + }, + ) + .await + .unwrap(); + + assert_eq!( + note, + note::Model { + id: 6, + title: "Title D".to_owned(), + text: "Text D".to_owned(), + } + ); + } + + { + let note = Mutation::update_note_by_id( + db, + 1, + note::Model { + id: 1, + title: "New Title A".to_owned(), + text: "New Text A".to_owned(), + }, + ) + .await + .unwrap(); + + assert_eq!( + note, + note::Model { + id: 1, + title: "New Title A".to_owned(), + text: "New Text A".to_owned(), + } + ); + } + + { + let result = Mutation::delete_note(db, 5).await.unwrap(); + + assert_eq!(result.rows_affected, 1); + } + + { + let result = Mutation::delete_all_notes(db).await.unwrap(); + + assert_eq!(result.rows_affected, 5); + } +} diff --git a/examples/graphql_example/core/tests/prepare.rs b/examples/graphql_example/core/tests/prepare.rs new file mode 100644 index 000000000..fd55936d6 --- /dev/null +++ b/examples/graphql_example/core/tests/prepare.rs @@ -0,0 +1,50 @@ +use ::entity::note; +use sea_orm::*; + +#[cfg(feature = "mock")] +pub fn prepare_mock_db() -> DatabaseConnection { + MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results(vec![ + vec![note::Model { + id: 1, + title: "Title A".to_owned(), + text: "Text A".to_owned(), + }], + vec![note::Model { + id: 5, + title: "Title C".to_owned(), + text: "Text C".to_owned(), + }], + vec![note::Model { + id: 6, + title: "Title D".to_owned(), + text: "Text D".to_owned(), + }], + vec![note::Model { + id: 1, + title: "Title A".to_owned(), + text: "Text A".to_owned(), + }], + vec![note::Model { + id: 1, + title: "New Title A".to_owned(), + text: "New Text A".to_owned(), + }], + vec![note::Model { + id: 5, + title: "Title C".to_owned(), + text: "Text C".to_owned(), + }], + ]) + .append_exec_results(vec![ + MockExecResult { + last_insert_id: 6, + rows_affected: 1, + }, + MockExecResult { + last_insert_id: 6, + rows_affected: 5, + }, + ]) + .into_connection() +} diff --git a/examples/graphql_example/src/db.rs b/examples/graphql_example/src/db.rs deleted file mode 100644 index 3cc41e27b..000000000 --- a/examples/graphql_example/src/db.rs +++ /dev/null @@ -1,19 +0,0 @@ -use sea_orm::DatabaseConnection; - -pub struct Database { - pub connection: DatabaseConnection, -} - -impl Database { - pub async fn new() -> Self { - let connection = sea_orm::Database::connect(std::env::var("DATABASE_URL").unwrap()) - .await - .expect("Could not connect to database"); - - Database { connection } - } - - pub fn get_connection(&self) -> &DatabaseConnection { - &self.connection - } -} diff --git a/examples/graphql_example/src/main.rs b/examples/graphql_example/src/main.rs index 665f79f23..308b656a4 100644 --- a/examples/graphql_example/src/main.rs +++ b/examples/graphql_example/src/main.rs @@ -1,49 +1,3 @@ -mod db; -mod graphql; - -use entity::async_graphql; - -use async_graphql::http::{playground_source, GraphQLPlaygroundConfig}; -use async_graphql_axum::{GraphQLRequest, GraphQLResponse}; -use axum::{ - extract::Extension, - response::{Html, IntoResponse}, - routing::get, - Router, -}; -use graphql::schema::{build_schema, AppSchema}; - -#[cfg(debug_assertions)] -use dotenv::dotenv; - -async fn graphql_handler(schema: Extension, req: GraphQLRequest) -> GraphQLResponse { - schema.execute(req.into_inner()).await.into() -} - -async fn graphql_playground() -> impl IntoResponse { - Html(playground_source(GraphQLPlaygroundConfig::new( - "/api/graphql", - ))) -} - -#[tokio::main] -async fn main() { - #[cfg(debug_assertions)] - dotenv().ok(); - - let schema = build_schema().await; - - let app = Router::new() - .route( - "/api/graphql", - get(graphql_playground).post(graphql_handler), - ) - .layer(Extension(schema)); - - println!("Playground: http://localhost:3000/api/graphql"); - - axum::Server::bind(&"0.0.0.0:3000".parse().unwrap()) - .serve(app.into_make_service()) - .await - .unwrap(); +fn main() { + graphql_example_api::main(); } From 4e179dbdf6baf82d2c98b285ff347187f1a37bec Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Fri, 9 Sep 2022 23:38:53 +0100 Subject: [PATCH 23/31] Update Jsonrpsee example --- examples/jsonrpsee_example/Cargo.toml | 25 +-- examples/jsonrpsee_example/api/Cargo.toml | 19 +++ examples/jsonrpsee_example/api/src/lib.rs | 143 +++++++++++++++++ examples/jsonrpsee_example/core/Cargo.toml | 30 ++++ examples/jsonrpsee_example/core/src/lib.rs | 7 + .../jsonrpsee_example/core/src/mutation.rs | 53 +++++++ examples/jsonrpsee_example/core/src/query.rs | 26 +++ examples/jsonrpsee_example/core/tests/mock.rs | 79 ++++++++++ .../jsonrpsee_example/core/tests/prepare.rs | 50 ++++++ examples/jsonrpsee_example/src/main.rs | 149 +----------------- 10 files changed, 411 insertions(+), 170 deletions(-) create mode 100644 examples/jsonrpsee_example/api/Cargo.toml create mode 100644 examples/jsonrpsee_example/api/src/lib.rs create mode 100644 examples/jsonrpsee_example/core/Cargo.toml create mode 100644 examples/jsonrpsee_example/core/src/lib.rs create mode 100644 examples/jsonrpsee_example/core/src/mutation.rs create mode 100644 examples/jsonrpsee_example/core/src/query.rs create mode 100644 examples/jsonrpsee_example/core/tests/mock.rs create mode 100644 examples/jsonrpsee_example/core/tests/prepare.rs diff --git a/examples/jsonrpsee_example/Cargo.toml b/examples/jsonrpsee_example/Cargo.toml index d507a5c6b..7365aa551 100644 --- a/examples/jsonrpsee_example/Cargo.toml +++ b/examples/jsonrpsee_example/Cargo.toml @@ -6,28 +6,7 @@ publish = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [workspace] -members = [".", "entity", "migration"] +members = [".", "api", "core", "entity", "migration"] [dependencies] -jsonrpsee = { version = "^0.8.0", features = ["full"] } -jsonrpsee-core = "0.9.0" -tokio = { version = "1.8.0", features = ["full"] } -serde = { version = "1", features = ["derive"] } -dotenv = "0.15" -entity = { path = "entity" } -migration = { path = "migration" } -anyhow = "1.0.52" -async-trait = "0.1.52" -log = { version = "0.4", features = ["std"] } -simplelog = "*" - -[dependencies.sea-orm] -path = "../../" # remove this line in your own project -version = "^0.10.0" # sea-orm version -features = [ - "debug-print", - "runtime-tokio-native-tls", - "sqlx-sqlite", - # "sqlx-postgres", - # "sqlx-mysql", -] +jsonrpsee-example-api = { path = "api" } diff --git a/examples/jsonrpsee_example/api/Cargo.toml b/examples/jsonrpsee_example/api/Cargo.toml new file mode 100644 index 000000000..51c959e6a --- /dev/null +++ b/examples/jsonrpsee_example/api/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "jsonrpsee-example-api" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +jsonrpsee-example-core = { path = "../core" } +jsonrpsee = { version = "^0.8.0", features = ["full"] } +jsonrpsee-core = "0.9.0" +tokio = { version = "1.8.0", features = ["full"] } +serde = { version = "1", features = ["derive"] } +dotenv = "0.15" +entity = { path = "../entity" } +migration = { path = "../migration" } +anyhow = "1.0.52" +async-trait = "0.1.52" +log = { version = "0.4", features = ["std"] } +simplelog = "*" diff --git a/examples/jsonrpsee_example/api/src/lib.rs b/examples/jsonrpsee_example/api/src/lib.rs new file mode 100644 index 000000000..2e5f2c723 --- /dev/null +++ b/examples/jsonrpsee_example/api/src/lib.rs @@ -0,0 +1,143 @@ +use std::env; + +use anyhow::anyhow; +use entity::post; +use jsonrpsee::core::{async_trait, RpcResult}; +use jsonrpsee::http_server::HttpServerBuilder; +use jsonrpsee::proc_macros::rpc; +use jsonrpsee::types::error::CallError; +use jsonrpsee_example_core::sea_orm::{Database, DatabaseConnection}; +use jsonrpsee_example_core::{Mutation, Query}; +use log::info; +use migration::{Migrator, MigratorTrait}; +use simplelog::*; +use std::fmt::Display; +use std::net::SocketAddr; +use tokio::signal::ctrl_c; +use tokio::signal::unix::{signal, SignalKind}; + +const DEFAULT_POSTS_PER_PAGE: u64 = 5; + +#[rpc(server, client)] +trait PostRpc { + #[method(name = "Post.List")] + async fn list( + &self, + page: Option, + posts_per_page: Option, + ) -> RpcResult>; + + #[method(name = "Post.Insert")] + async fn insert(&self, p: post::Model) -> RpcResult; + + #[method(name = "Post.Update")] + async fn update(&self, p: post::Model) -> RpcResult; + + #[method(name = "Post.Delete")] + async fn delete(&self, id: i32) -> RpcResult; +} + +struct PpcImpl { + conn: DatabaseConnection, +} + +#[async_trait] +impl PostRpcServer for PpcImpl { + async fn list( + &self, + page: Option, + posts_per_page: Option, + ) -> RpcResult> { + let page = page.unwrap_or(1); + let posts_per_page = posts_per_page.unwrap_or(DEFAULT_POSTS_PER_PAGE); + + Query::find_posts_in_page(&self.conn, page, posts_per_page) + .await + .map(|(p, _)| p) + .internal_call_error() + } + + async fn insert(&self, p: post::Model) -> RpcResult { + let new_post = Mutation::create_post(&self.conn, p) + .await + .expect("could not insert post"); + + Ok(new_post.id.unwrap()) + } + + async fn update(&self, p: post::Model) -> RpcResult { + Mutation::update_post_by_id(&self.conn, p.id, p) + .await + .map(|_| true) + .internal_call_error() + } + async fn delete(&self, id: i32) -> RpcResult { + Mutation::delete_post(&self.conn, id) + .await + .map(|res| res.rows_affected == 1) + .internal_call_error() + } +} + +trait IntoJsonRpcResult { + fn internal_call_error(self) -> RpcResult; +} + +impl IntoJsonRpcResult for Result +where + E: Display, +{ + fn internal_call_error(self) -> RpcResult { + self.map_err(|e| jsonrpsee::core::Error::Call(CallError::Failed(anyhow!("{}", e)))) + } +} + +#[tokio::main] +async fn start() -> std::io::Result<()> { + let _ = TermLogger::init( + LevelFilter::Trace, + Config::default(), + TerminalMode::Mixed, + ColorChoice::Auto, + ); + + // get env vars + dotenv::dotenv().ok(); + let db_url = env::var("DATABASE_URL").expect("DATABASE_URL is not set in .env file"); + let host = env::var("HOST").expect("HOST is not set in .env file"); + let port = env::var("PORT").expect("PORT is not set in .env file"); + let server_url = format!("{}:{}", host, port); + + // create post table if not exists + let conn = Database::connect(&db_url).await.unwrap(); + Migrator::up(&conn, None).await.unwrap(); + + let server = HttpServerBuilder::default() + .build(server_url.parse::().unwrap()) + .unwrap(); + + let rpc_impl = PpcImpl { conn }; + let server_addr = server.local_addr().unwrap(); + let handle = server.start(rpc_impl.into_rpc()).unwrap(); + + info!("starting listening {}", server_addr); + let mut sig_int = signal(SignalKind::interrupt()).unwrap(); + let mut sig_term = signal(SignalKind::terminate()).unwrap(); + + tokio::select! { + _ = sig_int.recv() => info!("receive SIGINT"), + _ = sig_term.recv() => info!("receive SIGTERM"), + _ = ctrl_c() => info!("receive Ctrl C"), + } + handle.stop().unwrap(); + info!("Shutdown program"); + Ok(()) +} + +pub fn main() { + let result = start(); + + if let Some(err) = result.err() { + println!("Error: {}", err); + } +} diff --git a/examples/jsonrpsee_example/core/Cargo.toml b/examples/jsonrpsee_example/core/Cargo.toml new file mode 100644 index 000000000..31ebbfcca --- /dev/null +++ b/examples/jsonrpsee_example/core/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "jsonrpsee-example-core" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +entity = { path = "../entity" } + +[dependencies.sea-orm] +path = "../../../" # remove this line in your own project +version = "^0.10.0" # sea-orm version +features = [ + "debug-print", + "runtime-tokio-native-tls", + "sqlx-sqlite", + # "sqlx-postgres", + # "sqlx-mysql", +] + +[dev-dependencies] +tokio = { version = "1.20.0", features = ["macros", "rt"] } + +[features] +mock = ["sea-orm/mock"] + +[[test]] +name = "mock" +required-features = ["mock"] diff --git a/examples/jsonrpsee_example/core/src/lib.rs b/examples/jsonrpsee_example/core/src/lib.rs new file mode 100644 index 000000000..4a80f2391 --- /dev/null +++ b/examples/jsonrpsee_example/core/src/lib.rs @@ -0,0 +1,7 @@ +mod mutation; +mod query; + +pub use mutation::*; +pub use query::*; + +pub use sea_orm; diff --git a/examples/jsonrpsee_example/core/src/mutation.rs b/examples/jsonrpsee_example/core/src/mutation.rs new file mode 100644 index 000000000..7f0150a63 --- /dev/null +++ b/examples/jsonrpsee_example/core/src/mutation.rs @@ -0,0 +1,53 @@ +use ::entity::{post, post::Entity as Post}; +use sea_orm::*; + +pub struct Mutation; + +impl Mutation { + pub async fn create_post( + db: &DbConn, + form_data: post::Model, + ) -> Result { + post::ActiveModel { + title: Set(form_data.title.to_owned()), + text: Set(form_data.text.to_owned()), + ..Default::default() + } + .save(db) + .await + } + + pub async fn update_post_by_id( + db: &DbConn, + id: i32, + form_data: post::Model, + ) -> Result { + let post: post::ActiveModel = if let Ok(Some(post)) = Post::find_by_id(id).one(db).await { + post.into() + } else { + return Err(DbErr::Custom("Cannot find post.".to_owned())); + }; + + post::ActiveModel { + id: post.id, + title: Set(form_data.title.to_owned()), + text: Set(form_data.text.to_owned()), + } + .update(db) + .await + } + + pub async fn delete_post(db: &DbConn, id: i32) -> Result { + let post: post::ActiveModel = if let Ok(Some(post)) = Post::find_by_id(id).one(db).await { + post.into() + } else { + return Err(DbErr::Custom("Cannot find post.".to_owned())); + }; + + post.delete(db).await + } + + pub async fn delete_all_posts(db: &DbConn) -> Result { + Post::delete_many().exec(db).await + } +} diff --git a/examples/jsonrpsee_example/core/src/query.rs b/examples/jsonrpsee_example/core/src/query.rs new file mode 100644 index 000000000..e8d2668f5 --- /dev/null +++ b/examples/jsonrpsee_example/core/src/query.rs @@ -0,0 +1,26 @@ +use ::entity::{post, post::Entity as Post}; +use sea_orm::*; + +pub struct Query; + +impl Query { + pub async fn find_post_by_id(db: &DbConn, id: i32) -> Result, DbErr> { + Post::find_by_id(id).one(db).await + } + + /// If ok, returns (post models, num pages). + pub async fn find_posts_in_page( + db: &DbConn, + page: u64, + posts_per_page: u64, + ) -> Result<(Vec, u64), DbErr> { + // Setup paginator + let paginator = Post::find() + .order_by_asc(post::Column::Id) + .paginate(db, posts_per_page); + let num_pages = paginator.num_pages().await?; + + // Fetch paginated posts + paginator.fetch_page(page - 1).await.map(|p| (p, num_pages)) + } +} diff --git a/examples/jsonrpsee_example/core/tests/mock.rs b/examples/jsonrpsee_example/core/tests/mock.rs new file mode 100644 index 000000000..068f31b6c --- /dev/null +++ b/examples/jsonrpsee_example/core/tests/mock.rs @@ -0,0 +1,79 @@ +mod prepare; + +use entity::post; +use jsonrpsee_example_core::{Mutation, Query}; +use prepare::prepare_mock_db; + +#[tokio::test] +async fn main() { + let db = &prepare_mock_db(); + + { + let post = Query::find_post_by_id(db, 1).await.unwrap().unwrap(); + + assert_eq!(post.id, 1); + } + + { + let post = Query::find_post_by_id(db, 5).await.unwrap().unwrap(); + + assert_eq!(post.id, 5); + } + + { + let post = Mutation::create_post( + db, + post::Model { + id: 0, + title: "Title D".to_owned(), + text: "Text D".to_owned(), + }, + ) + .await + .unwrap(); + + assert_eq!( + post, + post::ActiveModel { + id: sea_orm::ActiveValue::Unchanged(6), + title: sea_orm::ActiveValue::Unchanged("Title D".to_owned()), + text: sea_orm::ActiveValue::Unchanged("Text D".to_owned()) + } + ); + } + + { + let post = Mutation::update_post_by_id( + db, + 1, + post::Model { + id: 1, + title: "New Title A".to_owned(), + text: "New Text A".to_owned(), + }, + ) + .await + .unwrap(); + + assert_eq!( + post, + post::Model { + id: 1, + title: "New Title A".to_owned(), + text: "New Text A".to_owned(), + } + ); + } + + { + let result = Mutation::delete_post(db, 5).await.unwrap(); + + assert_eq!(result.rows_affected, 1); + } + + { + let result = Mutation::delete_all_posts(db).await.unwrap(); + + assert_eq!(result.rows_affected, 5); + } +} diff --git a/examples/jsonrpsee_example/core/tests/prepare.rs b/examples/jsonrpsee_example/core/tests/prepare.rs new file mode 100644 index 000000000..451804937 --- /dev/null +++ b/examples/jsonrpsee_example/core/tests/prepare.rs @@ -0,0 +1,50 @@ +use ::entity::post; +use sea_orm::*; + +#[cfg(feature = "mock")] +pub fn prepare_mock_db() -> DatabaseConnection { + MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results(vec![ + vec![post::Model { + id: 1, + title: "Title A".to_owned(), + text: "Text A".to_owned(), + }], + vec![post::Model { + id: 5, + title: "Title C".to_owned(), + text: "Text C".to_owned(), + }], + vec![post::Model { + id: 6, + title: "Title D".to_owned(), + text: "Text D".to_owned(), + }], + vec![post::Model { + id: 1, + title: "Title A".to_owned(), + text: "Text A".to_owned(), + }], + vec![post::Model { + id: 1, + title: "New Title A".to_owned(), + text: "New Text A".to_owned(), + }], + vec![post::Model { + id: 5, + title: "Title C".to_owned(), + text: "Text C".to_owned(), + }], + ]) + .append_exec_results(vec![ + MockExecResult { + last_insert_id: 6, + rows_affected: 1, + }, + MockExecResult { + last_insert_id: 6, + rows_affected: 5, + }, + ]) + .into_connection() +} diff --git a/examples/jsonrpsee_example/src/main.rs b/examples/jsonrpsee_example/src/main.rs index e1d26f328..2625a68d0 100644 --- a/examples/jsonrpsee_example/src/main.rs +++ b/examples/jsonrpsee_example/src/main.rs @@ -1,148 +1,3 @@ -use std::env; - -use anyhow::anyhow; -use entity::post; -use jsonrpsee::core::{async_trait, RpcResult}; -use jsonrpsee::http_server::HttpServerBuilder; -use jsonrpsee::proc_macros::rpc; -use jsonrpsee::types::error::CallError; -use log::info; -use migration::{Migrator, MigratorTrait}; -use sea_orm::NotSet; -use sea_orm::{entity::*, query::*, DatabaseConnection}; -use simplelog::*; -use std::fmt::Display; -use std::net::SocketAddr; -use tokio::signal::ctrl_c; -use tokio::signal::unix::{signal, SignalKind}; - -const DEFAULT_POSTS_PER_PAGE: u64 = 5; - -#[rpc(server, client)] -pub trait PostRpc { - #[method(name = "Post.List")] - async fn list( - &self, - page: Option, - posts_per_page: Option, - ) -> RpcResult>; - - #[method(name = "Post.Insert")] - async fn insert(&self, p: post::Model) -> RpcResult; - - #[method(name = "Post.Update")] - async fn update(&self, p: post::Model) -> RpcResult; - - #[method(name = "Post.Delete")] - async fn delete(&self, id: i32) -> RpcResult; -} - -pub struct PpcImpl { - conn: DatabaseConnection, -} - -#[async_trait] -impl PostRpcServer for PpcImpl { - async fn list( - &self, - page: Option, - posts_per_page: Option, - ) -> RpcResult> { - let page = page.unwrap_or(1); - let posts_per_page = posts_per_page.unwrap_or(DEFAULT_POSTS_PER_PAGE); - let paginator = post::Entity::find() - .order_by_asc(post::Column::Id) - .paginate(&self.conn, posts_per_page); - paginator.fetch_page(page - 1).await.internal_call_error() - } - - async fn insert(&self, p: post::Model) -> RpcResult { - let active_post = post::ActiveModel { - id: NotSet, - title: Set(p.title), - text: Set(p.text), - }; - let new_post = active_post.insert(&self.conn).await.internal_call_error()?; - Ok(new_post.id) - } - - async fn update(&self, p: post::Model) -> RpcResult { - let update_post = post::ActiveModel { - id: Set(p.id), - title: Set(p.title), - text: Set(p.text), - }; - update_post - .update(&self.conn) - .await - .map(|_| true) - .internal_call_error() - } - async fn delete(&self, id: i32) -> RpcResult { - let post = post::Entity::find_by_id(id) - .one(&self.conn) - .await - .internal_call_error()?; - - post.unwrap() - .delete(&self.conn) - .await - .map(|res| res.rows_affected == 1) - .internal_call_error() - } -} - -pub trait IntoJsonRpcResult { - fn internal_call_error(self) -> RpcResult; -} - -impl IntoJsonRpcResult for Result -where - E: Display, -{ - fn internal_call_error(self) -> RpcResult { - self.map_err(|e| jsonrpsee::core::Error::Call(CallError::Failed(anyhow!("{}", e)))) - } -} - -#[tokio::main] -async fn main() -> std::io::Result<()> { - let _ = TermLogger::init( - LevelFilter::Trace, - Config::default(), - TerminalMode::Mixed, - ColorChoice::Auto, - ); - - // get env vars - dotenv::dotenv().ok(); - let db_url = env::var("DATABASE_URL").expect("DATABASE_URL is not set in .env file"); - let host = env::var("HOST").expect("HOST is not set in .env file"); - let port = env::var("PORT").expect("PORT is not set in .env file"); - let server_url = format!("{}:{}", host, port); - - // create post table if not exists - let conn = sea_orm::Database::connect(&db_url).await.unwrap(); - Migrator::up(&conn, None).await.unwrap(); - - let server = HttpServerBuilder::default() - .build(server_url.parse::().unwrap()) - .unwrap(); - - let rpc_impl = PpcImpl { conn }; - let server_addr = server.local_addr().unwrap(); - let handle = server.start(rpc_impl.into_rpc()).unwrap(); - - info!("starting listening {}", server_addr); - let mut sig_int = signal(SignalKind::interrupt()).unwrap(); - let mut sig_term = signal(SignalKind::terminate()).unwrap(); - - tokio::select! { - _ = sig_int.recv() => info!("receive SIGINT"), - _ = sig_term.recv() => info!("receive SIGTERM"), - _ = ctrl_c() => info!("receive Ctrl C"), - } - handle.stop().unwrap(); - info!("Shutdown program"); - Ok(()) +fn main() { + jsonrpsee_example_api::main(); } From 3f9b04d28f7b00ed2fae5211003e373b77e08cd7 Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Sat, 10 Sep 2022 00:04:07 +0100 Subject: [PATCH 24/31] Update Poem example --- examples/poem_example/Cargo.toml | 22 +-- examples/poem_example/api/Cargo.toml | 15 ++ examples/poem_example/api/src/lib.rs | 163 ++++++++++++++++++ .../{ => api}/static/css/normalize.css | 0 .../{ => api}/static/css/skeleton.css | 0 .../{ => api}/static/css/style.css | 0 .../{ => api}/static/images/favicon.png | Bin .../{ => api}/templates/edit.html.tera | 0 .../{ => api}/templates/error/404.html.tera | 0 .../{ => api}/templates/index.html.tera | 0 .../{ => api}/templates/layout.html.tera | 0 .../{ => api}/templates/new.html.tera | 0 examples/poem_example/core/Cargo.toml | 30 ++++ examples/poem_example/core/src/lib.rs | 7 + examples/poem_example/core/src/mutation.rs | 53 ++++++ examples/poem_example/core/src/query.rs | 26 +++ examples/poem_example/core/tests/mock.rs | 79 +++++++++ examples/poem_example/core/tests/prepare.rs | 50 ++++++ examples/poem_example/src/main.rs | 163 +----------------- 19 files changed, 427 insertions(+), 181 deletions(-) create mode 100644 examples/poem_example/api/Cargo.toml create mode 100644 examples/poem_example/api/src/lib.rs rename examples/poem_example/{ => api}/static/css/normalize.css (100%) rename examples/poem_example/{ => api}/static/css/skeleton.css (100%) rename examples/poem_example/{ => api}/static/css/style.css (100%) rename examples/poem_example/{ => api}/static/images/favicon.png (100%) rename examples/poem_example/{ => api}/templates/edit.html.tera (100%) rename examples/poem_example/{ => api}/templates/error/404.html.tera (100%) rename examples/poem_example/{ => api}/templates/index.html.tera (100%) rename examples/poem_example/{ => api}/templates/layout.html.tera (100%) rename examples/poem_example/{ => api}/templates/new.html.tera (100%) create mode 100644 examples/poem_example/core/Cargo.toml create mode 100644 examples/poem_example/core/src/lib.rs create mode 100644 examples/poem_example/core/src/mutation.rs create mode 100644 examples/poem_example/core/src/query.rs create mode 100644 examples/poem_example/core/tests/mock.rs create mode 100644 examples/poem_example/core/tests/prepare.rs diff --git a/examples/poem_example/Cargo.toml b/examples/poem_example/Cargo.toml index 0139a5dae..9024d729c 100644 --- a/examples/poem_example/Cargo.toml +++ b/examples/poem_example/Cargo.toml @@ -5,25 +5,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [workspace] -members = [".", "entity", "migration"] +members = [".", "api", "core", "entity", "migration"] [dependencies] -tokio = { version = "1.15.0", features = ["macros", "rt-multi-thread"] } -poem = { version = "1.2.33", features = ["static-files"] } -tracing-subscriber = { version = "0.3", features = ["env-filter"] } -serde = { version = "1", features = ["derive"] } -tera = "1.8.0" -dotenv = "0.15" -entity = { path = "entity" } -migration = { path = "migration" } - -[dependencies.sea-orm] -path = "../../" # remove this line in your own project -version = "^0.10.0" # sea-orm version -features = [ - "debug-print", - "runtime-tokio-native-tls", - "sqlx-sqlite", - # "sqlx-postgres", - # "sqlx-mysql", -] +poem-example-api = { path = "api" } diff --git a/examples/poem_example/api/Cargo.toml b/examples/poem_example/api/Cargo.toml new file mode 100644 index 000000000..379bc1815 --- /dev/null +++ b/examples/poem_example/api/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "poem-example-api" +version = "0.1.0" +edition = "2021" + +[dependencies] +poem-example-core = { path = "../core" } +tokio = { version = "1.15.0", features = ["macros", "rt-multi-thread"] } +poem = { version = "1.2.33", features = ["static-files"] } +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +serde = { version = "1", features = ["derive"] } +tera = "1.8.0" +dotenv = "0.15" +entity = { path = "../entity" } +migration = { path = "../migration" } diff --git a/examples/poem_example/api/src/lib.rs b/examples/poem_example/api/src/lib.rs new file mode 100644 index 000000000..3dc8fb037 --- /dev/null +++ b/examples/poem_example/api/src/lib.rs @@ -0,0 +1,163 @@ +use std::env; + +use entity::post; +use migration::{Migrator, MigratorTrait}; +use poem::endpoint::StaticFilesEndpoint; +use poem::error::InternalServerError; +use poem::http::StatusCode; +use poem::listener::TcpListener; +use poem::web::{Data, Form, Html, Path, Query}; +use poem::{get, handler, post, EndpointExt, Error, IntoResponse, Result, Route, Server}; +use poem_example_core::{ + sea_orm::{Database, DatabaseConnection}, + Mutation as MutationCore, Query as QueryCore, +}; +use serde::Deserialize; +use tera::Tera; + +const DEFAULT_POSTS_PER_PAGE: u64 = 5; + +#[derive(Debug, Clone)] +struct AppState { + templates: tera::Tera, + conn: DatabaseConnection, +} + +#[derive(Deserialize)] +struct Params { + page: Option, + posts_per_page: Option, +} + +#[handler] +async fn create(state: Data<&AppState>, form: Form) -> Result { + let form = form.0; + let conn = &state.conn; + + MutationCore::create_post(conn, form) + .await + .map_err(InternalServerError)?; + + Ok(StatusCode::FOUND.with_header("location", "/")) +} + +#[handler] +async fn list(state: Data<&AppState>, Query(params): Query) -> Result { + let conn = &state.conn; + let page = params.page.unwrap_or(1); + let posts_per_page = params.posts_per_page.unwrap_or(DEFAULT_POSTS_PER_PAGE); + + let (posts, num_pages) = QueryCore::find_posts_in_page(conn, page, posts_per_page) + .await + .map_err(InternalServerError)?; + + let mut ctx = tera::Context::new(); + ctx.insert("posts", &posts); + ctx.insert("page", &page); + ctx.insert("posts_per_page", &posts_per_page); + ctx.insert("num_pages", &num_pages); + + let body = state + .templates + .render("index.html.tera", &ctx) + .map_err(InternalServerError)?; + Ok(Html(body)) +} + +#[handler] +async fn new(state: Data<&AppState>) -> Result { + let ctx = tera::Context::new(); + let body = state + .templates + .render("new.html.tera", &ctx) + .map_err(InternalServerError)?; + Ok(Html(body)) +} + +#[handler] +async fn edit(state: Data<&AppState>, Path(id): Path) -> Result { + let conn = &state.conn; + + let post: post::Model = QueryCore::find_post_by_id(conn, id) + .await + .map_err(InternalServerError)? + .ok_or_else(|| Error::from_status(StatusCode::NOT_FOUND))?; + + let mut ctx = tera::Context::new(); + ctx.insert("post", &post); + + let body = state + .templates + .render("edit.html.tera", &ctx) + .map_err(InternalServerError)?; + Ok(Html(body)) +} + +#[handler] +async fn update( + state: Data<&AppState>, + Path(id): Path, + form: Form, +) -> Result { + let conn = &state.conn; + let form = form.0; + + MutationCore::update_post_by_id(conn, id, form) + .await + .map_err(InternalServerError)?; + + Ok(StatusCode::FOUND.with_header("location", "/")) +} + +#[handler] +async fn delete(state: Data<&AppState>, Path(id): Path) -> Result { + let conn = &state.conn; + + MutationCore::delete_post(conn, id) + .await + .map_err(InternalServerError)?; + + Ok(StatusCode::FOUND.with_header("location", "/")) +} + +#[tokio::main] +async fn start() -> std::io::Result<()> { + std::env::set_var("RUST_LOG", "debug"); + tracing_subscriber::fmt::init(); + + // get env vars + dotenv::dotenv().ok(); + let db_url = env::var("DATABASE_URL").expect("DATABASE_URL is not set in .env file"); + let host = env::var("HOST").expect("HOST is not set in .env file"); + let port = env::var("PORT").expect("PORT is not set in .env file"); + let server_url = format!("{}:{}", host, port); + + // create post table if not exists + let conn = Database::connect(&db_url).await.unwrap(); + Migrator::up(&conn, None).await.unwrap(); + let templates = Tera::new(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*")).unwrap(); + let state = AppState { templates, conn }; + + println!("Starting server at {}", server_url); + + let app = Route::new() + .at("/", post(create).get(list)) + .at("/new", new) + .at("/:id", get(edit).post(update)) + .at("/delete/:id", post(delete)) + .nest( + "/static", + StaticFilesEndpoint::new(concat!(env!("CARGO_MANIFEST_DIR"), "/static")), + ) + .data(state); + let server = Server::new(TcpListener::bind(format!("{}:{}", host, port))); + server.run(app).await +} + +pub fn main() { + let result = start(); + + if let Some(err) = result.err() { + println!("Error: {}", err); + } +} diff --git a/examples/poem_example/static/css/normalize.css b/examples/poem_example/api/static/css/normalize.css similarity index 100% rename from examples/poem_example/static/css/normalize.css rename to examples/poem_example/api/static/css/normalize.css diff --git a/examples/poem_example/static/css/skeleton.css b/examples/poem_example/api/static/css/skeleton.css similarity index 100% rename from examples/poem_example/static/css/skeleton.css rename to examples/poem_example/api/static/css/skeleton.css diff --git a/examples/poem_example/static/css/style.css b/examples/poem_example/api/static/css/style.css similarity index 100% rename from examples/poem_example/static/css/style.css rename to examples/poem_example/api/static/css/style.css diff --git a/examples/poem_example/static/images/favicon.png b/examples/poem_example/api/static/images/favicon.png similarity index 100% rename from examples/poem_example/static/images/favicon.png rename to examples/poem_example/api/static/images/favicon.png diff --git a/examples/poem_example/templates/edit.html.tera b/examples/poem_example/api/templates/edit.html.tera similarity index 100% rename from examples/poem_example/templates/edit.html.tera rename to examples/poem_example/api/templates/edit.html.tera diff --git a/examples/poem_example/templates/error/404.html.tera b/examples/poem_example/api/templates/error/404.html.tera similarity index 100% rename from examples/poem_example/templates/error/404.html.tera rename to examples/poem_example/api/templates/error/404.html.tera diff --git a/examples/poem_example/templates/index.html.tera b/examples/poem_example/api/templates/index.html.tera similarity index 100% rename from examples/poem_example/templates/index.html.tera rename to examples/poem_example/api/templates/index.html.tera diff --git a/examples/poem_example/templates/layout.html.tera b/examples/poem_example/api/templates/layout.html.tera similarity index 100% rename from examples/poem_example/templates/layout.html.tera rename to examples/poem_example/api/templates/layout.html.tera diff --git a/examples/poem_example/templates/new.html.tera b/examples/poem_example/api/templates/new.html.tera similarity index 100% rename from examples/poem_example/templates/new.html.tera rename to examples/poem_example/api/templates/new.html.tera diff --git a/examples/poem_example/core/Cargo.toml b/examples/poem_example/core/Cargo.toml new file mode 100644 index 000000000..e7b64f355 --- /dev/null +++ b/examples/poem_example/core/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "poem-example-core" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +entity = { path = "../entity" } + +[dependencies.sea-orm] +path = "../../../" # remove this line in your own project +version = "^0.10.0" # sea-orm version +features = [ + "debug-print", + "runtime-async-std-native-tls", + # "sqlx-mysql", + # "sqlx-postgres", + "sqlx-sqlite", +] + +[dev-dependencies] +tokio = { version = "1.20.0", features = ["macros", "rt"] } + +[features] +mock = ["sea-orm/mock"] + +[[test]] +name = "mock" +required-features = ["mock"] diff --git a/examples/poem_example/core/src/lib.rs b/examples/poem_example/core/src/lib.rs new file mode 100644 index 000000000..4a80f2391 --- /dev/null +++ b/examples/poem_example/core/src/lib.rs @@ -0,0 +1,7 @@ +mod mutation; +mod query; + +pub use mutation::*; +pub use query::*; + +pub use sea_orm; diff --git a/examples/poem_example/core/src/mutation.rs b/examples/poem_example/core/src/mutation.rs new file mode 100644 index 000000000..7f0150a63 --- /dev/null +++ b/examples/poem_example/core/src/mutation.rs @@ -0,0 +1,53 @@ +use ::entity::{post, post::Entity as Post}; +use sea_orm::*; + +pub struct Mutation; + +impl Mutation { + pub async fn create_post( + db: &DbConn, + form_data: post::Model, + ) -> Result { + post::ActiveModel { + title: Set(form_data.title.to_owned()), + text: Set(form_data.text.to_owned()), + ..Default::default() + } + .save(db) + .await + } + + pub async fn update_post_by_id( + db: &DbConn, + id: i32, + form_data: post::Model, + ) -> Result { + let post: post::ActiveModel = if let Ok(Some(post)) = Post::find_by_id(id).one(db).await { + post.into() + } else { + return Err(DbErr::Custom("Cannot find post.".to_owned())); + }; + + post::ActiveModel { + id: post.id, + title: Set(form_data.title.to_owned()), + text: Set(form_data.text.to_owned()), + } + .update(db) + .await + } + + pub async fn delete_post(db: &DbConn, id: i32) -> Result { + let post: post::ActiveModel = if let Ok(Some(post)) = Post::find_by_id(id).one(db).await { + post.into() + } else { + return Err(DbErr::Custom("Cannot find post.".to_owned())); + }; + + post.delete(db).await + } + + pub async fn delete_all_posts(db: &DbConn) -> Result { + Post::delete_many().exec(db).await + } +} diff --git a/examples/poem_example/core/src/query.rs b/examples/poem_example/core/src/query.rs new file mode 100644 index 000000000..e8d2668f5 --- /dev/null +++ b/examples/poem_example/core/src/query.rs @@ -0,0 +1,26 @@ +use ::entity::{post, post::Entity as Post}; +use sea_orm::*; + +pub struct Query; + +impl Query { + pub async fn find_post_by_id(db: &DbConn, id: i32) -> Result, DbErr> { + Post::find_by_id(id).one(db).await + } + + /// If ok, returns (post models, num pages). + pub async fn find_posts_in_page( + db: &DbConn, + page: u64, + posts_per_page: u64, + ) -> Result<(Vec, u64), DbErr> { + // Setup paginator + let paginator = Post::find() + .order_by_asc(post::Column::Id) + .paginate(db, posts_per_page); + let num_pages = paginator.num_pages().await?; + + // Fetch paginated posts + paginator.fetch_page(page - 1).await.map(|p| (p, num_pages)) + } +} diff --git a/examples/poem_example/core/tests/mock.rs b/examples/poem_example/core/tests/mock.rs new file mode 100644 index 000000000..eb2d64180 --- /dev/null +++ b/examples/poem_example/core/tests/mock.rs @@ -0,0 +1,79 @@ +mod prepare; + +use poem_example_core::{Mutation, Query}; +use entity::post; +use prepare::prepare_mock_db; + +#[tokio::test] +async fn main() { + let db = &prepare_mock_db(); + + { + let post = Query::find_post_by_id(db, 1).await.unwrap().unwrap(); + + assert_eq!(post.id, 1); + } + + { + let post = Query::find_post_by_id(db, 5).await.unwrap().unwrap(); + + assert_eq!(post.id, 5); + } + + { + let post = Mutation::create_post( + db, + post::Model { + id: 0, + title: "Title D".to_owned(), + text: "Text D".to_owned(), + }, + ) + .await + .unwrap(); + + assert_eq!( + post, + post::ActiveModel { + id: sea_orm::ActiveValue::Unchanged(6), + title: sea_orm::ActiveValue::Unchanged("Title D".to_owned()), + text: sea_orm::ActiveValue::Unchanged("Text D".to_owned()) + } + ); + } + + { + let post = Mutation::update_post_by_id( + db, + 1, + post::Model { + id: 1, + title: "New Title A".to_owned(), + text: "New Text A".to_owned(), + }, + ) + .await + .unwrap(); + + assert_eq!( + post, + post::Model { + id: 1, + title: "New Title A".to_owned(), + text: "New Text A".to_owned(), + } + ); + } + + { + let result = Mutation::delete_post(db, 5).await.unwrap(); + + assert_eq!(result.rows_affected, 1); + } + + { + let result = Mutation::delete_all_posts(db).await.unwrap(); + + assert_eq!(result.rows_affected, 5); + } +} diff --git a/examples/poem_example/core/tests/prepare.rs b/examples/poem_example/core/tests/prepare.rs new file mode 100644 index 000000000..451804937 --- /dev/null +++ b/examples/poem_example/core/tests/prepare.rs @@ -0,0 +1,50 @@ +use ::entity::post; +use sea_orm::*; + +#[cfg(feature = "mock")] +pub fn prepare_mock_db() -> DatabaseConnection { + MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results(vec![ + vec![post::Model { + id: 1, + title: "Title A".to_owned(), + text: "Text A".to_owned(), + }], + vec![post::Model { + id: 5, + title: "Title C".to_owned(), + text: "Text C".to_owned(), + }], + vec![post::Model { + id: 6, + title: "Title D".to_owned(), + text: "Text D".to_owned(), + }], + vec![post::Model { + id: 1, + title: "Title A".to_owned(), + text: "Text A".to_owned(), + }], + vec![post::Model { + id: 1, + title: "New Title A".to_owned(), + text: "New Text A".to_owned(), + }], + vec![post::Model { + id: 5, + title: "Title C".to_owned(), + text: "Text C".to_owned(), + }], + ]) + .append_exec_results(vec![ + MockExecResult { + last_insert_id: 6, + rows_affected: 1, + }, + MockExecResult { + last_insert_id: 6, + rows_affected: 5, + }, + ]) + .into_connection() +} diff --git a/examples/poem_example/src/main.rs b/examples/poem_example/src/main.rs index 14c7c3f4f..e3c79da1b 100644 --- a/examples/poem_example/src/main.rs +++ b/examples/poem_example/src/main.rs @@ -1,162 +1,3 @@ -use std::env; - -use entity::post; -use migration::{Migrator, MigratorTrait}; -use poem::endpoint::StaticFilesEndpoint; -use poem::error::{BadRequest, InternalServerError}; -use poem::http::StatusCode; -use poem::listener::TcpListener; -use poem::web::{Data, Form, Html, Path, Query}; -use poem::{get, handler, post, EndpointExt, Error, IntoResponse, Result, Route, Server}; -use sea_orm::{entity::*, query::*, DatabaseConnection}; -use serde::Deserialize; -use tera::Tera; - -const DEFAULT_POSTS_PER_PAGE: u64 = 5; - -#[derive(Debug, Clone)] -struct AppState { - templates: tera::Tera, - conn: DatabaseConnection, -} - -#[derive(Deserialize)] -struct Params { - page: Option, - posts_per_page: Option, -} - -#[handler] -async fn create(state: Data<&AppState>, form: Form) -> Result { - post::ActiveModel { - title: Set(form.title.to_owned()), - text: Set(form.text.to_owned()), - ..Default::default() - } - .save(&state.conn) - .await - .map_err(InternalServerError)?; - - Ok(StatusCode::FOUND.with_header("location", "/")) -} - -#[handler] -async fn list(state: Data<&AppState>, Query(params): Query) -> Result { - let page = params.page.unwrap_or(1); - let posts_per_page = params.posts_per_page.unwrap_or(DEFAULT_POSTS_PER_PAGE); - let paginator = post::Entity::find() - .order_by_asc(post::Column::Id) - .paginate(&state.conn, posts_per_page); - let num_pages = paginator.num_pages().await.map_err(BadRequest)?; - let posts = paginator - .fetch_page(page - 1) - .await - .map_err(InternalServerError)?; - - let mut ctx = tera::Context::new(); - ctx.insert("posts", &posts); - ctx.insert("page", &page); - ctx.insert("posts_per_page", &posts_per_page); - ctx.insert("num_pages", &num_pages); - - let body = state - .templates - .render("index.html.tera", &ctx) - .map_err(InternalServerError)?; - Ok(Html(body)) -} - -#[handler] -async fn new(state: Data<&AppState>) -> Result { - let ctx = tera::Context::new(); - let body = state - .templates - .render("new.html.tera", &ctx) - .map_err(InternalServerError)?; - Ok(Html(body)) -} - -#[handler] -async fn edit(state: Data<&AppState>, Path(id): Path) -> Result { - let post: post::Model = post::Entity::find_by_id(id) - .one(&state.conn) - .await - .map_err(InternalServerError)? - .ok_or_else(|| Error::from_status(StatusCode::NOT_FOUND))?; - - let mut ctx = tera::Context::new(); - ctx.insert("post", &post); - - let body = state - .templates - .render("edit.html.tera", &ctx) - .map_err(InternalServerError)?; - Ok(Html(body)) -} - -#[handler] -async fn update( - state: Data<&AppState>, - Path(id): Path, - form: Form, -) -> Result { - post::ActiveModel { - id: Set(id), - title: Set(form.title.to_owned()), - text: Set(form.text.to_owned()), - } - .save(&state.conn) - .await - .map_err(InternalServerError)?; - - Ok(StatusCode::FOUND.with_header("location", "/")) -} - -#[handler] -async fn delete(state: Data<&AppState>, Path(id): Path) -> Result { - let post: post::ActiveModel = post::Entity::find_by_id(id) - .one(&state.conn) - .await - .map_err(InternalServerError)? - .ok_or_else(|| Error::from_status(StatusCode::NOT_FOUND))? - .into(); - post.delete(&state.conn) - .await - .map_err(InternalServerError)?; - - Ok(StatusCode::FOUND.with_header("location", "/")) -} - -#[tokio::main] -async fn main() -> std::io::Result<()> { - std::env::set_var("RUST_LOG", "debug"); - tracing_subscriber::fmt::init(); - - // get env vars - dotenv::dotenv().ok(); - let db_url = env::var("DATABASE_URL").expect("DATABASE_URL is not set in .env file"); - let host = env::var("HOST").expect("HOST is not set in .env file"); - let port = env::var("PORT").expect("PORT is not set in .env file"); - let server_url = format!("{}:{}", host, port); - - // create post table if not exists - let conn = sea_orm::Database::connect(&db_url).await.unwrap(); - Migrator::up(&conn, None).await.unwrap(); - let templates = Tera::new(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*")).unwrap(); - let state = AppState { templates, conn }; - - println!("Starting server at {}", server_url); - - let app = Route::new() - .at("/", post(create).get(list)) - .at("/new", new) - .at("/:id", get(edit).post(update)) - .at("/delete/:id", post(delete)) - .nest( - "/static", - StaticFilesEndpoint::new(concat!(env!("CARGO_MANIFEST_DIR"), "/static")), - ) - .data(state); - let server = Server::new(TcpListener::bind(format!("{}:{}", host, port))); - server.run(app).await +fn main() { + poem_example_api::main(); } From c96b69e0d6b07237f323512a77693a478db446b1 Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Sat, 10 Sep 2022 00:54:31 +0100 Subject: [PATCH 25/31] Update Tonic example --- examples/tonic_example/Cargo.toml | 32 +--- examples/tonic_example/api/Cargo.toml | 20 +++ examples/tonic_example/{ => api}/build.rs | 0 .../tonic_example/{ => api}/proto/post.proto | 0 examples/tonic_example/api/src/lib.rs | 143 ++++++++++++++++++ examples/tonic_example/core/Cargo.toml | 30 ++++ examples/tonic_example/core/src/lib.rs | 7 + examples/tonic_example/core/src/mutation.rs | 53 +++++++ examples/tonic_example/core/src/query.rs | 26 ++++ examples/tonic_example/core/tests/mock.rs | 79 ++++++++++ examples/tonic_example/core/tests/prepare.rs | 50 ++++++ examples/tonic_example/src/client.rs | 2 +- examples/tonic_example/src/lib.rs | 3 - examples/tonic_example/src/server.rs | 131 +--------------- 14 files changed, 417 insertions(+), 159 deletions(-) create mode 100644 examples/tonic_example/api/Cargo.toml rename examples/tonic_example/{ => api}/build.rs (100%) rename examples/tonic_example/{ => api}/proto/post.proto (100%) create mode 100644 examples/tonic_example/api/src/lib.rs create mode 100644 examples/tonic_example/core/Cargo.toml create mode 100644 examples/tonic_example/core/src/lib.rs create mode 100644 examples/tonic_example/core/src/mutation.rs create mode 100644 examples/tonic_example/core/src/query.rs create mode 100644 examples/tonic_example/core/tests/mock.rs create mode 100644 examples/tonic_example/core/tests/prepare.rs delete mode 100644 examples/tonic_example/src/lib.rs diff --git a/examples/tonic_example/Cargo.toml b/examples/tonic_example/Cargo.toml index 4bb3346fe..d3d147048 100644 --- a/examples/tonic_example/Cargo.toml +++ b/examples/tonic_example/Cargo.toml @@ -7,37 +7,17 @@ publish = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [workspace] -members = [".", "entity", "migration"] +members = [".", "api", "core", "entity", "migration"] [dependencies] +tonic-example-api = { path = "api" } tonic = "0.7" tokio = { version = "1.17", features = ["macros", "rt-multi-thread", "full"] } -entity = { path = "entity" } -migration = { path = "migration" } -prost = "0.10.0" -serde = "1.0" - -[dependencies.sea-orm] -path = "../../" # remove this line in your own project -version = "^0.10.0" # sea-orm version -features = [ - "debug-print", - "runtime-tokio-rustls", - # "sqlx-mysql", - "sqlx-postgres", - # "sqlx-sqlite", -] - -[lib] -path = "./src/lib.rs" [[bin]] -name="server" -path="./src/server.rs" +name = "server" +path = "./src/server.rs" [[bin]] -name="client" -path="./src/client.rs" - -[build-dependencies] -tonic-build = "0.7" +name = "client" +path = "./src/client.rs" diff --git a/examples/tonic_example/api/Cargo.toml b/examples/tonic_example/api/Cargo.toml new file mode 100644 index 000000000..6253c5311 --- /dev/null +++ b/examples/tonic_example/api/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "tonic-example-api" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +tonic-example-core = { path = "../core" } +tonic = "0.7" +tokio = { version = "1.17", features = ["macros", "rt-multi-thread", "full"] } +entity = { path = "../entity" } +migration = { path = "../migration" } +prost = "0.10.0" +serde = "1.0" + +[lib] +path = "./src/lib.rs" + +[build-dependencies] +tonic-build = "0.7" diff --git a/examples/tonic_example/build.rs b/examples/tonic_example/api/build.rs similarity index 100% rename from examples/tonic_example/build.rs rename to examples/tonic_example/api/build.rs diff --git a/examples/tonic_example/proto/post.proto b/examples/tonic_example/api/proto/post.proto similarity index 100% rename from examples/tonic_example/proto/post.proto rename to examples/tonic_example/api/proto/post.proto diff --git a/examples/tonic_example/api/src/lib.rs b/examples/tonic_example/api/src/lib.rs new file mode 100644 index 000000000..9b3e2582d --- /dev/null +++ b/examples/tonic_example/api/src/lib.rs @@ -0,0 +1,143 @@ +use tonic::transport::Server; +use tonic::{Request, Response, Status}; + +use entity::post; +use migration::{Migrator, MigratorTrait}; +use tonic_example_core::{ + sea_orm::{Database, DatabaseConnection}, + Mutation, Query, +}; + +use std::env; + +pub mod post_mod { + tonic::include_proto!("post"); +} + +use post_mod::{ + blogpost_server::{Blogpost, BlogpostServer}, + Post, PostId, PostList, PostPerPage, ProcessStatus, +}; + +impl Post { + fn into_model(self) -> post::Model { + post::Model { + id: self.id, + title: self.title, + text: self.content, + } + } +} + +#[derive(Default)] +pub struct MyServer { + connection: DatabaseConnection, +} + +#[tonic::async_trait] +impl Blogpost for MyServer { + async fn get_posts(&self, request: Request) -> Result, Status> { + let conn = &self.connection; + let posts_per_page = request.into_inner().per_page; + + let mut response = PostList { post: Vec::new() }; + + let (posts, _) = Query::find_posts_in_page(conn, 1, posts_per_page) + .await + .expect("Cannot find posts in page"); + + for post in posts { + response.post.push(Post { + id: post.id, + title: post.title, + content: post.text, + }); + } + + Ok(Response::new(response)) + } + + async fn add_post(&self, request: Request) -> Result, Status> { + let conn = &self.connection; + + let input = request.into_inner().into_model(); + + let inserted = Mutation::create_post(conn, input) + .await + .expect("could not insert post"); + + let response = PostId { + id: inserted.id.unwrap(), + }; + + Ok(Response::new(response)) + } + + async fn update_post(&self, request: Request) -> Result, Status> { + let conn = &self.connection; + let input = request.into_inner().into_model(); + + match Mutation::update_post_by_id(conn, input.id, input).await { + Ok(_) => Ok(Response::new(ProcessStatus { success: true })), + Err(_) => Ok(Response::new(ProcessStatus { success: false })), + } + } + + async fn delete_post( + &self, + request: Request, + ) -> Result, Status> { + let conn = &self.connection; + let id = request.into_inner().id; + + match Mutation::delete_post(conn, id).await { + Ok(_) => Ok(Response::new(ProcessStatus { success: true })), + Err(_) => Ok(Response::new(ProcessStatus { success: false })), + } + } + + async fn get_post_by_id(&self, request: Request) -> Result, Status> { + let conn = &self.connection; + let id = request.into_inner().id; + + if let Some(post) = Query::find_post_by_id(conn, id).await.ok().flatten() { + Ok(Response::new(Post { + id, + title: post.title, + content: post.text, + })) + } else { + Err(Status::new( + tonic::Code::Aborted, + "Could not find post with id ".to_owned() + &id.to_string(), + )) + } + } +} + +#[tokio::main] +async fn start() -> Result<(), Box> { + let addr = "0.0.0.0:50051".parse()?; + + let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); + + // establish database connection + let connection = Database::connect(&database_url).await?; + Migrator::up(&connection, None).await?; + + let hello_server = MyServer { connection }; + Server::builder() + .add_service(BlogpostServer::new(hello_server)) + .serve(addr) + .await?; + + Ok(()) +} + +pub fn main() { + let result = start(); + + if let Some(err) = result.err() { + println!("Error: {}", err); + } +} diff --git a/examples/tonic_example/core/Cargo.toml b/examples/tonic_example/core/Cargo.toml new file mode 100644 index 000000000..dd9dd4e0e --- /dev/null +++ b/examples/tonic_example/core/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "tonic-example-core" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +entity = { path = "../entity" } + +[dependencies.sea-orm] +path = "../../../" # remove this line in your own project +version = "^0.10.0" # sea-orm version +features = [ + "debug-print", + "runtime-tokio-rustls", + # "sqlx-mysql", + "sqlx-postgres", + # "sqlx-sqlite", +] + +[dev-dependencies] +tokio = { version = "1.20.0", features = ["macros", "rt"] } + +[features] +mock = ["sea-orm/mock"] + +[[test]] +name = "mock" +required-features = ["mock"] diff --git a/examples/tonic_example/core/src/lib.rs b/examples/tonic_example/core/src/lib.rs new file mode 100644 index 000000000..4a80f2391 --- /dev/null +++ b/examples/tonic_example/core/src/lib.rs @@ -0,0 +1,7 @@ +mod mutation; +mod query; + +pub use mutation::*; +pub use query::*; + +pub use sea_orm; diff --git a/examples/tonic_example/core/src/mutation.rs b/examples/tonic_example/core/src/mutation.rs new file mode 100644 index 000000000..7f0150a63 --- /dev/null +++ b/examples/tonic_example/core/src/mutation.rs @@ -0,0 +1,53 @@ +use ::entity::{post, post::Entity as Post}; +use sea_orm::*; + +pub struct Mutation; + +impl Mutation { + pub async fn create_post( + db: &DbConn, + form_data: post::Model, + ) -> Result { + post::ActiveModel { + title: Set(form_data.title.to_owned()), + text: Set(form_data.text.to_owned()), + ..Default::default() + } + .save(db) + .await + } + + pub async fn update_post_by_id( + db: &DbConn, + id: i32, + form_data: post::Model, + ) -> Result { + let post: post::ActiveModel = if let Ok(Some(post)) = Post::find_by_id(id).one(db).await { + post.into() + } else { + return Err(DbErr::Custom("Cannot find post.".to_owned())); + }; + + post::ActiveModel { + id: post.id, + title: Set(form_data.title.to_owned()), + text: Set(form_data.text.to_owned()), + } + .update(db) + .await + } + + pub async fn delete_post(db: &DbConn, id: i32) -> Result { + let post: post::ActiveModel = if let Ok(Some(post)) = Post::find_by_id(id).one(db).await { + post.into() + } else { + return Err(DbErr::Custom("Cannot find post.".to_owned())); + }; + + post.delete(db).await + } + + pub async fn delete_all_posts(db: &DbConn) -> Result { + Post::delete_many().exec(db).await + } +} diff --git a/examples/tonic_example/core/src/query.rs b/examples/tonic_example/core/src/query.rs new file mode 100644 index 000000000..e8d2668f5 --- /dev/null +++ b/examples/tonic_example/core/src/query.rs @@ -0,0 +1,26 @@ +use ::entity::{post, post::Entity as Post}; +use sea_orm::*; + +pub struct Query; + +impl Query { + pub async fn find_post_by_id(db: &DbConn, id: i32) -> Result, DbErr> { + Post::find_by_id(id).one(db).await + } + + /// If ok, returns (post models, num pages). + pub async fn find_posts_in_page( + db: &DbConn, + page: u64, + posts_per_page: u64, + ) -> Result<(Vec, u64), DbErr> { + // Setup paginator + let paginator = Post::find() + .order_by_asc(post::Column::Id) + .paginate(db, posts_per_page); + let num_pages = paginator.num_pages().await?; + + // Fetch paginated posts + paginator.fetch_page(page - 1).await.map(|p| (p, num_pages)) + } +} diff --git a/examples/tonic_example/core/tests/mock.rs b/examples/tonic_example/core/tests/mock.rs new file mode 100644 index 000000000..d54989ea4 --- /dev/null +++ b/examples/tonic_example/core/tests/mock.rs @@ -0,0 +1,79 @@ +mod prepare; + +use tonic_example_core::{Mutation, Query}; +use entity::post; +use prepare::prepare_mock_db; + +#[tokio::test] +async fn main() { + let db = &prepare_mock_db(); + + { + let post = Query::find_post_by_id(db, 1).await.unwrap().unwrap(); + + assert_eq!(post.id, 1); + } + + { + let post = Query::find_post_by_id(db, 5).await.unwrap().unwrap(); + + assert_eq!(post.id, 5); + } + + { + let post = Mutation::create_post( + db, + post::Model { + id: 0, + title: "Title D".to_owned(), + text: "Text D".to_owned(), + }, + ) + .await + .unwrap(); + + assert_eq!( + post, + post::ActiveModel { + id: sea_orm::ActiveValue::Unchanged(6), + title: sea_orm::ActiveValue::Unchanged("Title D".to_owned()), + text: sea_orm::ActiveValue::Unchanged("Text D".to_owned()) + } + ); + } + + { + let post = Mutation::update_post_by_id( + db, + 1, + post::Model { + id: 1, + title: "New Title A".to_owned(), + text: "New Text A".to_owned(), + }, + ) + .await + .unwrap(); + + assert_eq!( + post, + post::Model { + id: 1, + title: "New Title A".to_owned(), + text: "New Text A".to_owned(), + } + ); + } + + { + let result = Mutation::delete_post(db, 5).await.unwrap(); + + assert_eq!(result.rows_affected, 1); + } + + { + let result = Mutation::delete_all_posts(db).await.unwrap(); + + assert_eq!(result.rows_affected, 5); + } +} diff --git a/examples/tonic_example/core/tests/prepare.rs b/examples/tonic_example/core/tests/prepare.rs new file mode 100644 index 000000000..451804937 --- /dev/null +++ b/examples/tonic_example/core/tests/prepare.rs @@ -0,0 +1,50 @@ +use ::entity::post; +use sea_orm::*; + +#[cfg(feature = "mock")] +pub fn prepare_mock_db() -> DatabaseConnection { + MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results(vec![ + vec![post::Model { + id: 1, + title: "Title A".to_owned(), + text: "Text A".to_owned(), + }], + vec![post::Model { + id: 5, + title: "Title C".to_owned(), + text: "Text C".to_owned(), + }], + vec![post::Model { + id: 6, + title: "Title D".to_owned(), + text: "Text D".to_owned(), + }], + vec![post::Model { + id: 1, + title: "Title A".to_owned(), + text: "Text A".to_owned(), + }], + vec![post::Model { + id: 1, + title: "New Title A".to_owned(), + text: "New Text A".to_owned(), + }], + vec![post::Model { + id: 5, + title: "Title C".to_owned(), + text: "Text C".to_owned(), + }], + ]) + .append_exec_results(vec![ + MockExecResult { + last_insert_id: 6, + rows_affected: 1, + }, + MockExecResult { + last_insert_id: 6, + rows_affected: 5, + }, + ]) + .into_connection() +} diff --git a/examples/tonic_example/src/client.rs b/examples/tonic_example/src/client.rs index 4cd46cb41..f63ad46a6 100644 --- a/examples/tonic_example/src/client.rs +++ b/examples/tonic_example/src/client.rs @@ -1,7 +1,7 @@ use tonic::transport::Endpoint; use tonic::Request; -use sea_orm_tonic_example::post::{blogpost_client::BlogpostClient, PostPerPage}; +use tonic_example_api::post_mod::{blogpost_client::BlogpostClient, PostPerPage}; #[tokio::main] async fn main() -> Result<(), Box> { diff --git a/examples/tonic_example/src/lib.rs b/examples/tonic_example/src/lib.rs deleted file mode 100644 index cd2028969..000000000 --- a/examples/tonic_example/src/lib.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod post { - tonic::include_proto!("post"); -} diff --git a/examples/tonic_example/src/server.rs b/examples/tonic_example/src/server.rs index 0857ac7b9..a9838cdc2 100644 --- a/examples/tonic_example/src/server.rs +++ b/examples/tonic_example/src/server.rs @@ -1,130 +1,3 @@ -use tonic::transport::Server; -use tonic::{Request, Response, Status}; - -use sea_orm_tonic_example::post::{ - blogpost_server::{Blogpost, BlogpostServer}, - Post, PostId, PostList, PostPerPage, ProcessStatus, -}; - -use entity::post::{self, Entity as PostEntity}; -use migration::{Migrator, MigratorTrait}; -use sea_orm::{self, entity::*, query::*, DatabaseConnection}; - -use std::env; - -#[derive(Default)] -pub struct MyServer { - connection: DatabaseConnection, -} - -#[tonic::async_trait] -impl Blogpost for MyServer { - async fn get_posts(&self, request: Request) -> Result, Status> { - let mut response = PostList { post: Vec::new() }; - - let posts = PostEntity::find() - .order_by_asc(post::Column::Id) - .limit(request.into_inner().per_page) - .all(&self.connection) - .await - .unwrap(); - - for post in posts { - response.post.push(Post { - id: post.id, - title: post.title, - content: post.text, - }); - } - - Ok(Response::new(response)) - } - - async fn add_post(&self, request: Request) -> Result, Status> { - let input = request.into_inner(); - let insert_details = post::ActiveModel { - title: Set(input.title.clone()), - text: Set(input.content.clone()), - ..Default::default() - }; - - let response = PostId { - id: insert_details.insert(&self.connection).await.unwrap().id, - }; - - Ok(Response::new(response)) - } - - async fn update_post(&self, request: Request) -> Result, Status> { - let input = request.into_inner(); - let mut update_post: post::ActiveModel = PostEntity::find_by_id(input.id) - .one(&self.connection) - .await - .unwrap() - .unwrap() - .into(); - - update_post.title = Set(input.title.clone()); - update_post.text = Set(input.content.clone()); - - let update = update_post.update(&self.connection).await; - - match update { - Ok(_) => Ok(Response::new(ProcessStatus { success: true })), - Err(_) => Ok(Response::new(ProcessStatus { success: false })), - } - } - - async fn delete_post( - &self, - request: Request, - ) -> Result, Status> { - let delete_post: post::ActiveModel = PostEntity::find_by_id(request.into_inner().id) - .one(&self.connection) - .await - .unwrap() - .unwrap() - .into(); - - let status = delete_post.delete(&self.connection).await; - - match status { - Ok(_) => Ok(Response::new(ProcessStatus { success: true })), - Err(_) => Ok(Response::new(ProcessStatus { success: false })), - } - } - - async fn get_post_by_id(&self, request: Request) -> Result, Status> { - let post = PostEntity::find_by_id(request.into_inner().id) - .one(&self.connection) - .await - .unwrap() - .unwrap(); - - let response = Post { - id: post.id, - title: post.title, - content: post.text, - }; - Ok(Response::new(response)) - } -} - -#[tokio::main] -async fn main() -> Result<(), Box> { - let addr = "0.0.0.0:50051".parse()?; - - let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); - - // establish database connection - let connection = sea_orm::Database::connect(&database_url).await?; - Migrator::up(&connection, None).await?; - - let hello_server = MyServer { connection }; - Server::builder() - .add_service(BlogpostServer::new(hello_server)) - .serve(addr) - .await?; - - Ok(()) +fn main() { + tonic_example_api::main(); } From 3a5867022a85def4d9306452302c390824222787 Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Sat, 10 Sep 2022 00:56:38 +0100 Subject: [PATCH 26/31] Cargo fmt --- examples/poem_example/core/tests/mock.rs | 2 +- examples/tonic_example/core/tests/mock.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/poem_example/core/tests/mock.rs b/examples/poem_example/core/tests/mock.rs index eb2d64180..e4b3ef4f5 100644 --- a/examples/poem_example/core/tests/mock.rs +++ b/examples/poem_example/core/tests/mock.rs @@ -1,7 +1,7 @@ mod prepare; -use poem_example_core::{Mutation, Query}; use entity::post; +use poem_example_core::{Mutation, Query}; use prepare::prepare_mock_db; #[tokio::test] diff --git a/examples/tonic_example/core/tests/mock.rs b/examples/tonic_example/core/tests/mock.rs index d54989ea4..522d3e452 100644 --- a/examples/tonic_example/core/tests/mock.rs +++ b/examples/tonic_example/core/tests/mock.rs @@ -1,8 +1,8 @@ mod prepare; -use tonic_example_core::{Mutation, Query}; use entity::post; use prepare::prepare_mock_db; +use tonic_example_core::{Mutation, Query}; #[tokio::test] async fn main() { From e66f40b8da703393235c8813bf2a0158c65d57db Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Sat, 10 Sep 2022 01:17:19 +0100 Subject: [PATCH 27/31] Update Salvo example --- examples/salvo_example/Cargo.toml | 22 +- examples/salvo_example/api/Cargo.toml | 15 ++ examples/salvo_example/api/src/lib.rs | 182 +++++++++++++++++ .../{ => api}/static/css/normalize.css | 0 .../{ => api}/static/css/skeleton.css | 0 .../{ => api}/static/css/style.css | 0 .../{ => api}/static/images/favicon.png | Bin .../{ => api}/templates/edit.html.tera | 0 .../{ => api}/templates/error/404.html.tera | 0 .../{ => api}/templates/index.html.tera | 0 .../{ => api}/templates/layout.html.tera | 0 .../{ => api}/templates/new.html.tera | 0 examples/salvo_example/core/Cargo.toml | 30 +++ examples/salvo_example/core/src/lib.rs | 7 + examples/salvo_example/core/src/mutation.rs | 53 +++++ examples/salvo_example/core/src/query.rs | 26 +++ examples/salvo_example/core/tests/mock.rs | 79 +++++++ examples/salvo_example/core/tests/prepare.rs | 50 +++++ examples/salvo_example/src/main.rs | 192 +----------------- 19 files changed, 446 insertions(+), 210 deletions(-) create mode 100644 examples/salvo_example/api/Cargo.toml create mode 100644 examples/salvo_example/api/src/lib.rs rename examples/salvo_example/{ => api}/static/css/normalize.css (100%) rename examples/salvo_example/{ => api}/static/css/skeleton.css (100%) rename examples/salvo_example/{ => api}/static/css/style.css (100%) rename examples/salvo_example/{ => api}/static/images/favicon.png (100%) rename examples/salvo_example/{ => api}/templates/edit.html.tera (100%) rename examples/salvo_example/{ => api}/templates/error/404.html.tera (100%) rename examples/salvo_example/{ => api}/templates/index.html.tera (100%) rename examples/salvo_example/{ => api}/templates/layout.html.tera (100%) rename examples/salvo_example/{ => api}/templates/new.html.tera (100%) create mode 100644 examples/salvo_example/core/Cargo.toml create mode 100644 examples/salvo_example/core/src/lib.rs create mode 100644 examples/salvo_example/core/src/mutation.rs create mode 100644 examples/salvo_example/core/src/query.rs create mode 100644 examples/salvo_example/core/tests/mock.rs create mode 100644 examples/salvo_example/core/tests/prepare.rs diff --git a/examples/salvo_example/Cargo.toml b/examples/salvo_example/Cargo.toml index a1028a771..aef022844 100644 --- a/examples/salvo_example/Cargo.toml +++ b/examples/salvo_example/Cargo.toml @@ -5,25 +5,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [workspace] -members = [".", "entity", "migration"] +members = [".", "api", "core", "entity", "migration"] [dependencies] -tokio = { version = "1.15.0", features = ["macros", "rt-multi-thread"] } -salvo = { version = "0.27", features = ["affix", "serve-static"] } -tracing-subscriber = { version = "0.3", features = ["env-filter"] } -serde = { version = "1", features = ["derive"] } -tera = "1.8.0" -dotenv = "0.15" -entity = { path = "entity" } -migration = { path = "migration" } - -[dependencies.sea-orm] -path = "../../" # remove this line in your own project -version = "^0.10.0" # sea-orm version -features = [ - "debug-print", - "runtime-tokio-native-tls", - "sqlx-sqlite", - # "sqlx-postgres", - # "sqlx-mysql", -] +salvo-example-api = { path = "api" } diff --git a/examples/salvo_example/api/Cargo.toml b/examples/salvo_example/api/Cargo.toml new file mode 100644 index 000000000..cc26a7746 --- /dev/null +++ b/examples/salvo_example/api/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "salvo-example-api" +version = "0.1.0" +edition = "2021" + +[dependencies] +salvo-example-core = { path = "../core" } +tokio = { version = "1.15.0", features = ["macros", "rt-multi-thread"] } +salvo = { version = "0.27", features = ["affix", "serve-static"] } +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +serde = { version = "1", features = ["derive"] } +tera = "1.8.0" +dotenv = "0.15" +entity = { path = "../entity" } +migration = { path = "../migration" } diff --git a/examples/salvo_example/api/src/lib.rs b/examples/salvo_example/api/src/lib.rs new file mode 100644 index 000000000..225080552 --- /dev/null +++ b/examples/salvo_example/api/src/lib.rs @@ -0,0 +1,182 @@ +use std::env; + +use entity::post; +use migration::{Migrator, MigratorTrait}; +use salvo::extra::affix; +use salvo::extra::serve_static::DirHandler; +use salvo::prelude::*; +use salvo::writer::Text; +use salvo_example_core::{ + sea_orm::{Database, DatabaseConnection}, + Mutation, Query, +}; +use tera::Tera; + +const DEFAULT_POSTS_PER_PAGE: u64 = 5; +type Result = std::result::Result; + +#[derive(Debug, Clone)] +struct AppState { + templates: tera::Tera, + conn: DatabaseConnection, +} + +#[handler] +async fn create(req: &mut Request, depot: &mut Depot, res: &mut Response) -> Result<()> { + let state = depot + .obtain::() + .ok_or_else(StatusError::internal_server_error)?; + let conn = &state.conn; + + let form = req + .extract_form::() + .await + .map_err(|_| StatusError::bad_request())?; + + Mutation::create_post(conn, form) + .await + .map_err(|_| StatusError::internal_server_error())?; + + res.redirect_found("/"); + Ok(()) +} + +#[handler] +async fn list(req: &mut Request, depot: &mut Depot) -> Result> { + let state = depot + .obtain::() + .ok_or_else(StatusError::internal_server_error)?; + let conn = &state.conn; + + let page = req.query("page").unwrap_or(1); + let posts_per_page = req + .query("posts_per_page") + .unwrap_or(DEFAULT_POSTS_PER_PAGE); + + let (posts, num_pages) = Query::find_posts_in_page(conn, page, posts_per_page) + .await + .map_err(|_| StatusError::internal_server_error())?; + + let mut ctx = tera::Context::new(); + ctx.insert("posts", &posts); + ctx.insert("page", &page); + ctx.insert("posts_per_page", &posts_per_page); + ctx.insert("num_pages", &num_pages); + + let body = state + .templates + .render("index.html.tera", &ctx) + .map_err(|_| StatusError::internal_server_error())?; + Ok(Text::Html(body)) +} + +#[handler] +async fn new(depot: &mut Depot) -> Result> { + let state = depot + .obtain::() + .ok_or_else(StatusError::internal_server_error)?; + let ctx = tera::Context::new(); + let body = state + .templates + .render("new.html.tera", &ctx) + .map_err(|_| StatusError::internal_server_error())?; + Ok(Text::Html(body)) +} + +#[handler] +async fn edit(req: &mut Request, depot: &mut Depot) -> Result> { + let state = depot + .obtain::() + .ok_or_else(StatusError::internal_server_error)?; + let conn = &state.conn; + let id = req.param::("id").unwrap_or_default(); + + let post: post::Model = Query::find_post_by_id(conn, id) + .await + .map_err(|_| StatusError::internal_server_error())? + .ok_or_else(StatusError::not_found)?; + + let mut ctx = tera::Context::new(); + ctx.insert("post", &post); + + let body = state + .templates + .render("edit.html.tera", &ctx) + .map_err(|_| StatusError::internal_server_error())?; + Ok(Text::Html(body)) +} + +#[handler] +async fn update(req: &mut Request, depot: &mut Depot, res: &mut Response) -> Result<()> { + let state = depot + .obtain::() + .ok_or_else(StatusError::internal_server_error)?; + let conn = &state.conn; + let id = req.param::("id").unwrap_or_default(); + let form = req + .extract_form::() + .await + .map_err(|_| StatusError::bad_request())?; + + Mutation::update_post_by_id(conn, id, form) + .await + .map_err(|_| StatusError::internal_server_error())?; + + res.redirect_found("/"); + Ok(()) +} + +#[handler] +async fn delete(req: &mut Request, depot: &mut Depot, res: &mut Response) -> Result<()> { + let state = depot + .obtain::() + .ok_or_else(StatusError::internal_server_error)?; + let conn = &state.conn; + let id = req.param::("id").unwrap_or_default(); + + Mutation::delete_post(conn, id) + .await + .map_err(|_| StatusError::internal_server_error())?; + + res.redirect_found("/"); + Ok(()) +} + +#[tokio::main] +pub async fn main() { + std::env::set_var("RUST_LOG", "debug"); + tracing_subscriber::fmt::init(); + + // get env vars + dotenv::dotenv().ok(); + let db_url = env::var("DATABASE_URL").expect("DATABASE_URL is not set in .env file"); + let host = env::var("HOST").expect("HOST is not set in .env file"); + let port = env::var("PORT").expect("PORT is not set in .env file"); + let server_url = format!("{}:{}", host, port); + + // create post table if not exists + let conn = Database::connect(&db_url).await.unwrap(); + Migrator::up(&conn, None).await.unwrap(); + let templates = Tera::new(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*")).unwrap(); + let state = AppState { templates, conn }; + + println!("Starting server at {}", server_url); + + let router = Router::new() + .hoop(affix::inject(state)) + .post(create) + .get(list) + .push(Router::with_path("new").get(new)) + .push(Router::with_path("").get(edit).post(update)) + .push(Router::with_path("delete/").post(delete)) + .push( + Router::with_path("static/<**>").get(DirHandler::new(concat!( + env!("CARGO_MANIFEST_DIR"), + "/static" + ))), + ); + + Server::new(TcpListener::bind(&format!("{}:{}", host, port))) + .serve(router) + .await; +} diff --git a/examples/salvo_example/static/css/normalize.css b/examples/salvo_example/api/static/css/normalize.css similarity index 100% rename from examples/salvo_example/static/css/normalize.css rename to examples/salvo_example/api/static/css/normalize.css diff --git a/examples/salvo_example/static/css/skeleton.css b/examples/salvo_example/api/static/css/skeleton.css similarity index 100% rename from examples/salvo_example/static/css/skeleton.css rename to examples/salvo_example/api/static/css/skeleton.css diff --git a/examples/salvo_example/static/css/style.css b/examples/salvo_example/api/static/css/style.css similarity index 100% rename from examples/salvo_example/static/css/style.css rename to examples/salvo_example/api/static/css/style.css diff --git a/examples/salvo_example/static/images/favicon.png b/examples/salvo_example/api/static/images/favicon.png similarity index 100% rename from examples/salvo_example/static/images/favicon.png rename to examples/salvo_example/api/static/images/favicon.png diff --git a/examples/salvo_example/templates/edit.html.tera b/examples/salvo_example/api/templates/edit.html.tera similarity index 100% rename from examples/salvo_example/templates/edit.html.tera rename to examples/salvo_example/api/templates/edit.html.tera diff --git a/examples/salvo_example/templates/error/404.html.tera b/examples/salvo_example/api/templates/error/404.html.tera similarity index 100% rename from examples/salvo_example/templates/error/404.html.tera rename to examples/salvo_example/api/templates/error/404.html.tera diff --git a/examples/salvo_example/templates/index.html.tera b/examples/salvo_example/api/templates/index.html.tera similarity index 100% rename from examples/salvo_example/templates/index.html.tera rename to examples/salvo_example/api/templates/index.html.tera diff --git a/examples/salvo_example/templates/layout.html.tera b/examples/salvo_example/api/templates/layout.html.tera similarity index 100% rename from examples/salvo_example/templates/layout.html.tera rename to examples/salvo_example/api/templates/layout.html.tera diff --git a/examples/salvo_example/templates/new.html.tera b/examples/salvo_example/api/templates/new.html.tera similarity index 100% rename from examples/salvo_example/templates/new.html.tera rename to examples/salvo_example/api/templates/new.html.tera diff --git a/examples/salvo_example/core/Cargo.toml b/examples/salvo_example/core/Cargo.toml new file mode 100644 index 000000000..58280ed28 --- /dev/null +++ b/examples/salvo_example/core/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "salvo-example-core" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +entity = { path = "../entity" } + +[dependencies.sea-orm] +path = "../../../" # remove this line in your own project +version = "^0.10.0" # sea-orm version +features = [ + "debug-print", + "runtime-tokio-native-tls", + # "sqlx-mysql", + # "sqlx-postgres", + "sqlx-sqlite", +] + +[dev-dependencies] +tokio = { version = "1.20.0", features = ["macros", "rt"] } + +[features] +mock = ["sea-orm/mock"] + +[[test]] +name = "mock" +required-features = ["mock"] diff --git a/examples/salvo_example/core/src/lib.rs b/examples/salvo_example/core/src/lib.rs new file mode 100644 index 000000000..4a80f2391 --- /dev/null +++ b/examples/salvo_example/core/src/lib.rs @@ -0,0 +1,7 @@ +mod mutation; +mod query; + +pub use mutation::*; +pub use query::*; + +pub use sea_orm; diff --git a/examples/salvo_example/core/src/mutation.rs b/examples/salvo_example/core/src/mutation.rs new file mode 100644 index 000000000..7f0150a63 --- /dev/null +++ b/examples/salvo_example/core/src/mutation.rs @@ -0,0 +1,53 @@ +use ::entity::{post, post::Entity as Post}; +use sea_orm::*; + +pub struct Mutation; + +impl Mutation { + pub async fn create_post( + db: &DbConn, + form_data: post::Model, + ) -> Result { + post::ActiveModel { + title: Set(form_data.title.to_owned()), + text: Set(form_data.text.to_owned()), + ..Default::default() + } + .save(db) + .await + } + + pub async fn update_post_by_id( + db: &DbConn, + id: i32, + form_data: post::Model, + ) -> Result { + let post: post::ActiveModel = if let Ok(Some(post)) = Post::find_by_id(id).one(db).await { + post.into() + } else { + return Err(DbErr::Custom("Cannot find post.".to_owned())); + }; + + post::ActiveModel { + id: post.id, + title: Set(form_data.title.to_owned()), + text: Set(form_data.text.to_owned()), + } + .update(db) + .await + } + + pub async fn delete_post(db: &DbConn, id: i32) -> Result { + let post: post::ActiveModel = if let Ok(Some(post)) = Post::find_by_id(id).one(db).await { + post.into() + } else { + return Err(DbErr::Custom("Cannot find post.".to_owned())); + }; + + post.delete(db).await + } + + pub async fn delete_all_posts(db: &DbConn) -> Result { + Post::delete_many().exec(db).await + } +} diff --git a/examples/salvo_example/core/src/query.rs b/examples/salvo_example/core/src/query.rs new file mode 100644 index 000000000..e8d2668f5 --- /dev/null +++ b/examples/salvo_example/core/src/query.rs @@ -0,0 +1,26 @@ +use ::entity::{post, post::Entity as Post}; +use sea_orm::*; + +pub struct Query; + +impl Query { + pub async fn find_post_by_id(db: &DbConn, id: i32) -> Result, DbErr> { + Post::find_by_id(id).one(db).await + } + + /// If ok, returns (post models, num pages). + pub async fn find_posts_in_page( + db: &DbConn, + page: u64, + posts_per_page: u64, + ) -> Result<(Vec, u64), DbErr> { + // Setup paginator + let paginator = Post::find() + .order_by_asc(post::Column::Id) + .paginate(db, posts_per_page); + let num_pages = paginator.num_pages().await?; + + // Fetch paginated posts + paginator.fetch_page(page - 1).await.map(|p| (p, num_pages)) + } +} diff --git a/examples/salvo_example/core/tests/mock.rs b/examples/salvo_example/core/tests/mock.rs new file mode 100644 index 000000000..261652bfe --- /dev/null +++ b/examples/salvo_example/core/tests/mock.rs @@ -0,0 +1,79 @@ +mod prepare; + +use entity::post; +use prepare::prepare_mock_db; +use salvo_example_core::{Mutation, Query}; + +#[tokio::test] +async fn main() { + let db = &prepare_mock_db(); + + { + let post = Query::find_post_by_id(db, 1).await.unwrap().unwrap(); + + assert_eq!(post.id, 1); + } + + { + let post = Query::find_post_by_id(db, 5).await.unwrap().unwrap(); + + assert_eq!(post.id, 5); + } + + { + let post = Mutation::create_post( + db, + post::Model { + id: 0, + title: "Title D".to_owned(), + text: "Text D".to_owned(), + }, + ) + .await + .unwrap(); + + assert_eq!( + post, + post::ActiveModel { + id: sea_orm::ActiveValue::Unchanged(6), + title: sea_orm::ActiveValue::Unchanged("Title D".to_owned()), + text: sea_orm::ActiveValue::Unchanged("Text D".to_owned()) + } + ); + } + + { + let post = Mutation::update_post_by_id( + db, + 1, + post::Model { + id: 1, + title: "New Title A".to_owned(), + text: "New Text A".to_owned(), + }, + ) + .await + .unwrap(); + + assert_eq!( + post, + post::Model { + id: 1, + title: "New Title A".to_owned(), + text: "New Text A".to_owned(), + } + ); + } + + { + let result = Mutation::delete_post(db, 5).await.unwrap(); + + assert_eq!(result.rows_affected, 1); + } + + { + let result = Mutation::delete_all_posts(db).await.unwrap(); + + assert_eq!(result.rows_affected, 5); + } +} diff --git a/examples/salvo_example/core/tests/prepare.rs b/examples/salvo_example/core/tests/prepare.rs new file mode 100644 index 000000000..451804937 --- /dev/null +++ b/examples/salvo_example/core/tests/prepare.rs @@ -0,0 +1,50 @@ +use ::entity::post; +use sea_orm::*; + +#[cfg(feature = "mock")] +pub fn prepare_mock_db() -> DatabaseConnection { + MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results(vec![ + vec![post::Model { + id: 1, + title: "Title A".to_owned(), + text: "Text A".to_owned(), + }], + vec![post::Model { + id: 5, + title: "Title C".to_owned(), + text: "Text C".to_owned(), + }], + vec![post::Model { + id: 6, + title: "Title D".to_owned(), + text: "Text D".to_owned(), + }], + vec![post::Model { + id: 1, + title: "Title A".to_owned(), + text: "Text A".to_owned(), + }], + vec![post::Model { + id: 1, + title: "New Title A".to_owned(), + text: "New Text A".to_owned(), + }], + vec![post::Model { + id: 5, + title: "Title C".to_owned(), + text: "Text C".to_owned(), + }], + ]) + .append_exec_results(vec![ + MockExecResult { + last_insert_id: 6, + rows_affected: 1, + }, + MockExecResult { + last_insert_id: 6, + rows_affected: 5, + }, + ]) + .into_connection() +} diff --git a/examples/salvo_example/src/main.rs b/examples/salvo_example/src/main.rs index ba38042b7..687ea53e4 100644 --- a/examples/salvo_example/src/main.rs +++ b/examples/salvo_example/src/main.rs @@ -1,191 +1,3 @@ -use std::env; - -use entity::post; -use migration::{Migrator, MigratorTrait}; -use salvo::extra::affix; -use salvo::extra::serve_static::DirHandler; -use salvo::prelude::*; -use salvo::writer::Text; -use sea_orm::{entity::*, query::*, DatabaseConnection}; -use tera::Tera; - -const DEFAULT_POSTS_PER_PAGE: u64 = 5; -type Result = std::result::Result; - -#[derive(Debug, Clone)] -struct AppState { - templates: tera::Tera, - conn: DatabaseConnection, -} - -#[handler] -async fn create(req: &mut Request, depot: &mut Depot, res: &mut Response) -> Result<()> { - let state = depot - .obtain::() - .ok_or_else(StatusError::internal_server_error)?; - let form = req - .extract_form::() - .await - .map_err(|_| StatusError::bad_request())?; - post::ActiveModel { - title: Set(form.title.to_owned()), - text: Set(form.text.to_owned()), - ..Default::default() - } - .save(&state.conn) - .await - .map_err(|_| StatusError::internal_server_error())?; - - res.redirect_found("/"); - Ok(()) -} - -#[handler] -async fn list(req: &mut Request, depot: &mut Depot) -> Result> { - let state = depot - .obtain::() - .ok_or_else(StatusError::internal_server_error)?; - let page = req.query("page").unwrap_or(1); - let posts_per_page = req - .query("posts_per_page") - .unwrap_or(DEFAULT_POSTS_PER_PAGE); - let paginator = post::Entity::find() - .order_by_asc(post::Column::Id) - .paginate(&state.conn, posts_per_page); - let num_pages = paginator - .num_pages() - .await - .map_err(|_| StatusError::bad_request())?; - let posts = paginator - .fetch_page(page - 1) - .await - .map_err(|_| StatusError::internal_server_error())?; - - let mut ctx = tera::Context::new(); - ctx.insert("posts", &posts); - ctx.insert("page", &page); - ctx.insert("posts_per_page", &posts_per_page); - ctx.insert("num_pages", &num_pages); - - let body = state - .templates - .render("index.html.tera", &ctx) - .map_err(|_| StatusError::internal_server_error())?; - Ok(Text::Html(body)) -} - -#[handler] -async fn new(depot: &mut Depot) -> Result> { - let state = depot - .obtain::() - .ok_or_else(StatusError::internal_server_error)?; - let ctx = tera::Context::new(); - let body = state - .templates - .render("new.html.tera", &ctx) - .map_err(|_| StatusError::internal_server_error())?; - Ok(Text::Html(body)) -} - -#[handler] -async fn edit(req: &mut Request, depot: &mut Depot) -> Result> { - let state = depot - .obtain::() - .ok_or_else(StatusError::internal_server_error)?; - let id = req.param::("id").unwrap_or_default(); - let post: post::Model = post::Entity::find_by_id(id) - .one(&state.conn) - .await - .map_err(|_| StatusError::internal_server_error())? - .ok_or_else(StatusError::not_found)?; - - let mut ctx = tera::Context::new(); - ctx.insert("post", &post); - - let body = state - .templates - .render("edit.html.tera", &ctx) - .map_err(|_| StatusError::internal_server_error())?; - Ok(Text::Html(body)) -} - -#[handler] -async fn update(req: &mut Request, depot: &mut Depot, res: &mut Response) -> Result<()> { - let state = depot - .obtain::() - .ok_or_else(StatusError::internal_server_error)?; - let id = req.param::("id").unwrap_or_default(); - let form = req - .extract_form::() - .await - .map_err(|_| StatusError::bad_request())?; - post::ActiveModel { - id: Set(id), - title: Set(form.title.to_owned()), - text: Set(form.text.to_owned()), - } - .save(&state.conn) - .await - .map_err(|_| StatusError::internal_server_error())?; - res.redirect_found("/"); - Ok(()) -} - -#[handler] -async fn delete(req: &mut Request, depot: &mut Depot, res: &mut Response) -> Result<()> { - let state = depot - .obtain::() - .ok_or_else(StatusError::internal_server_error)?; - let id = req.param::("id").unwrap_or_default(); - let post: post::ActiveModel = post::Entity::find_by_id(id) - .one(&state.conn) - .await - .map_err(|_| StatusError::internal_server_error())? - .ok_or_else(StatusError::not_found)? - .into(); - post.delete(&state.conn) - .await - .map_err(|_| StatusError::internal_server_error())?; - - res.redirect_found("/"); - Ok(()) -} - -#[tokio::main] -async fn main() { - std::env::set_var("RUST_LOG", "debug"); - tracing_subscriber::fmt::init(); - - // get env vars - dotenv::dotenv().ok(); - let db_url = env::var("DATABASE_URL").expect("DATABASE_URL is not set in .env file"); - let host = env::var("HOST").expect("HOST is not set in .env file"); - let port = env::var("PORT").expect("PORT is not set in .env file"); - let server_url = format!("{}:{}", host, port); - - // create post table if not exists - let conn = sea_orm::Database::connect(&db_url).await.unwrap(); - Migrator::up(&conn, None).await.unwrap(); - let templates = Tera::new(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*")).unwrap(); - let state = AppState { templates, conn }; - - println!("Starting server at {}", server_url); - - let router = Router::new() - .hoop(affix::inject(state)) - .post(create) - .get(list) - .push(Router::with_path("new").get(new)) - .push(Router::with_path("").get(edit).post(update)) - .push(Router::with_path("delete/").post(delete)) - .push( - Router::with_path("static/<**>").get(DirHandler::new(concat!( - env!("CARGO_MANIFEST_DIR"), - "/static" - ))), - ); - - Server::new(TcpListener::bind(&format!("{}:{}", host, port))) - .serve(router) - .await; +fn main() { + salvo_example_api::main(); } From 3c79b015bad54ab9bd88ae3f8d887825f5348779 Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Sat, 10 Sep 2022 01:21:05 +0100 Subject: [PATCH 28/31] Update path of core/Cargo.toml in README.md --- examples/actix3_example/README.md | 2 +- examples/actix_example/README.md | 2 +- examples/axum_example/README.md | 2 +- examples/graphql_example/README.md | 2 +- examples/jsonrpsee_example/README.md | 10 +++++----- examples/poem_example/README.md | 2 +- examples/salvo_example/README.md | 2 +- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/examples/actix3_example/README.md b/examples/actix3_example/README.md index 2cd8c3584..e7f39ad2b 100644 --- a/examples/actix3_example/README.md +++ b/examples/actix3_example/README.md @@ -6,7 +6,7 @@ 1. Modify the `DATABASE_URL` var in `.env` to point to your chosen database -1. Turn on the appropriate database feature for your chosen db in `Cargo.toml` (the `"sqlx-mysql",` line) +1. Turn on the appropriate database feature for your chosen db in `core/Cargo.toml` (the `"sqlx-mysql",` line) 1. Execute `cargo run` to start the server diff --git a/examples/actix_example/README.md b/examples/actix_example/README.md index a2acf4140..08acbe43a 100644 --- a/examples/actix_example/README.md +++ b/examples/actix_example/README.md @@ -4,7 +4,7 @@ 1. Modify the `DATABASE_URL` var in `.env` to point to your chosen database -1. Turn on the appropriate database feature for your chosen db in `Cargo.toml` (the `"sqlx-mysql",` line) +1. Turn on the appropriate database feature for your chosen db in `core/Cargo.toml` (the `"sqlx-mysql",` line) 1. Execute `cargo run` to start the server diff --git a/examples/axum_example/README.md b/examples/axum_example/README.md index a3c624225..38d114459 100644 --- a/examples/axum_example/README.md +++ b/examples/axum_example/README.md @@ -4,7 +4,7 @@ 1. Modify the `DATABASE_URL` var in `.env` to point to your chosen database -1. Turn on the appropriate database feature for your chosen db in `Cargo.toml` (the `"sqlx-postgres",` line) +1. Turn on the appropriate database feature for your chosen db in `core/Cargo.toml` (the `"sqlx-postgres",` line) 1. Execute `cargo run` to start the server diff --git a/examples/graphql_example/README.md b/examples/graphql_example/README.md index 88501958d..55bd445e7 100644 --- a/examples/graphql_example/README.md +++ b/examples/graphql_example/README.md @@ -6,7 +6,7 @@ 1. Modify the `DATABASE_URL` var in `.env` to point to your chosen database -1. Turn on the appropriate database feature for your chosen db in `Cargo.toml` (the `"sqlx-sqlite",` line) +1. Turn on the appropriate database feature for your chosen db in `core/Cargo.toml` (the `"sqlx-sqlite",` line) 1. Execute `cargo run` to start the server diff --git a/examples/jsonrpsee_example/README.md b/examples/jsonrpsee_example/README.md index 804893319..ce08f3c1e 100644 --- a/examples/jsonrpsee_example/README.md +++ b/examples/jsonrpsee_example/README.md @@ -2,11 +2,11 @@ 1. Modify the `DATABASE_URL` var in `.env` to point to your chosen database -1. Turn on the appropriate database feature for your chosen db in `Cargo.toml` (the `"sqlx-sqlite",` line) +1. Turn on the appropriate database feature for your chosen db in `core/Cargo.toml` (the `"sqlx-sqlite",` line) 1. Execute `cargo run` to start the server -2. Send jsonrpc request to server +1. Send jsonrpc request to server ```shell #insert @@ -20,7 +20,7 @@ curl --location --request POST 'http://127.0.0.1:8000' \ } ], "id": 2}' -#list +#list curl --location --request POST 'http://127.0.0.1:8000' \ --header 'Content-Type: application/json' \ --data-raw '{ @@ -33,7 +33,7 @@ curl --location --request POST 'http://127.0.0.1:8000' \ "id": 2 }' -#delete +#delete curl --location --request POST 'http://127.0.0.1:8000' \ --header 'Content-Type: application/json' \ --data-raw '{ @@ -61,4 +61,4 @@ curl --location --request POST 'http://127.0.0.1:8000' \ "id": 2 }' -``` \ No newline at end of file +``` diff --git a/examples/poem_example/README.md b/examples/poem_example/README.md index bd4a45398..f8852710e 100644 --- a/examples/poem_example/README.md +++ b/examples/poem_example/README.md @@ -4,7 +4,7 @@ 1. Modify the `DATABASE_URL` var in `.env` to point to your chosen database -1. Turn on the appropriate database feature for your chosen db in `Cargo.toml` (the `"sqlx-sqlite",` line) +1. Turn on the appropriate database feature for your chosen db in `core/Cargo.toml` (the `"sqlx-sqlite",` line) 1. Execute `cargo run` to start the server diff --git a/examples/salvo_example/README.md b/examples/salvo_example/README.md index bd4a45398..f8852710e 100644 --- a/examples/salvo_example/README.md +++ b/examples/salvo_example/README.md @@ -4,7 +4,7 @@ 1. Modify the `DATABASE_URL` var in `.env` to point to your chosen database -1. Turn on the appropriate database feature for your chosen db in `Cargo.toml` (the `"sqlx-sqlite",` line) +1. Turn on the appropriate database feature for your chosen db in `core/Cargo.toml` (the `"sqlx-sqlite",` line) 1. Execute `cargo run` to start the server From 2fbb964e39a0deec1b6591d0b78532acdd57ff28 Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Sat, 10 Sep 2022 01:24:47 +0100 Subject: [PATCH 29/31] Add mock test instruction in README.md --- examples/actix3_example/README.md | 7 +++++++ examples/actix_example/README.md | 7 +++++++ examples/axum_example/README.md | 7 +++++++ examples/graphql_example/README.md | 7 +++++++ examples/jsonrpsee_example/README.md | 7 +++++++ examples/poem_example/README.md | 7 +++++++ examples/rocket_example/README.md | 7 +++++++ examples/salvo_example/README.md | 7 +++++++ examples/tonic_example/README.md | 11 ++++++++++- 9 files changed, 66 insertions(+), 1 deletion(-) diff --git a/examples/actix3_example/README.md b/examples/actix3_example/README.md index e7f39ad2b..472638261 100644 --- a/examples/actix3_example/README.md +++ b/examples/actix3_example/README.md @@ -18,3 +18,10 @@ Run server with auto-reloading: cargo install systemfd systemfd --no-pid -s http::8000 -- cargo watch -x run ``` + +Run mock test on the core logic crate: + +```bash +cd core +cargo test --features mock +``` diff --git a/examples/actix_example/README.md b/examples/actix_example/README.md index 08acbe43a..d844b96a2 100644 --- a/examples/actix_example/README.md +++ b/examples/actix_example/README.md @@ -16,3 +16,10 @@ Run server with auto-reloading: cargo install systemfd cargo-watch systemfd --no-pid -s http::8000 -- cargo watch -x run ``` + +Run mock test on the core logic crate: + +```bash +cd core +cargo test --features mock +``` diff --git a/examples/axum_example/README.md b/examples/axum_example/README.md index 38d114459..fe120e715 100644 --- a/examples/axum_example/README.md +++ b/examples/axum_example/README.md @@ -9,3 +9,10 @@ 1. Execute `cargo run` to start the server 1. Visit [localhost:8000](http://localhost:8000) in browser + +Run mock test on the core logic crate: + +```bash +cd core +cargo test --features mock +``` diff --git a/examples/graphql_example/README.md b/examples/graphql_example/README.md index 55bd445e7..351c7df55 100644 --- a/examples/graphql_example/README.md +++ b/examples/graphql_example/README.md @@ -11,3 +11,10 @@ 1. Execute `cargo run` to start the server 1. Visit [localhost:3000/api/graphql](http://localhost:3000/api/graphql) in browser + +Run mock test on the core logic crate: + +```bash +cd core +cargo test --features mock +``` diff --git a/examples/jsonrpsee_example/README.md b/examples/jsonrpsee_example/README.md index ce08f3c1e..4d45e5b64 100644 --- a/examples/jsonrpsee_example/README.md +++ b/examples/jsonrpsee_example/README.md @@ -62,3 +62,10 @@ curl --location --request POST 'http://127.0.0.1:8000' \ }' ``` + +Run mock test on the core logic crate: + +```bash +cd core +cargo test --features mock +``` diff --git a/examples/poem_example/README.md b/examples/poem_example/README.md index f8852710e..d0aa6973b 100644 --- a/examples/poem_example/README.md +++ b/examples/poem_example/README.md @@ -9,3 +9,10 @@ 1. Execute `cargo run` to start the server 1. Visit [localhost:8000](http://localhost:8000) in browser after seeing the `server started` line + +Run mock test on the core logic crate: + +```bash +cd core +cargo test --features mock +``` diff --git a/examples/rocket_example/README.md b/examples/rocket_example/README.md index e89b29c82..20845db6a 100644 --- a/examples/rocket_example/README.md +++ b/examples/rocket_example/README.md @@ -9,3 +9,10 @@ 1. Execute `cargo run` to start the server 1. Visit [localhost:8000](http://localhost:8000) in browser after seeing the `🚀 Rocket has launched from http://localhost:8000` line + +Run mock test on the core logic crate: + +```bash +cd core +cargo test --features mock +``` diff --git a/examples/salvo_example/README.md b/examples/salvo_example/README.md index f8852710e..d0aa6973b 100644 --- a/examples/salvo_example/README.md +++ b/examples/salvo_example/README.md @@ -9,3 +9,10 @@ 1. Execute `cargo run` to start the server 1. Visit [localhost:8000](http://localhost:8000) in browser after seeing the `server started` line + +Run mock test on the core logic crate: + +```bash +cd core +cargo test --features mock +``` diff --git a/examples/tonic_example/README.md b/examples/tonic_example/README.md index 22ef798f1..7c11feace 100644 --- a/examples/tonic_example/README.md +++ b/examples/tonic_example/README.md @@ -3,11 +3,20 @@ Simple implementation of gRPC using SeaORM. run server using + ```bash cargo run --bin server ``` run client using + ```bash cargo run --bin client -``` \ No newline at end of file +``` + +Run mock test on the core logic crate: + +```bash +cd core +cargo test --features mock +``` From ac78c716f5a7dac0c5ecbf0092007a22bc09569b Mon Sep 17 00:00:00 2001 From: Billy Chan Date: Wed, 21 Sep 2022 16:48:40 +0800 Subject: [PATCH 30/31] Refactoring --- examples/actix3_example/core/src/mutation.rs | 20 +++++++++---------- examples/actix_example/core/src/mutation.rs | 20 +++++++++---------- examples/axum_example/core/src/mutation.rs | 20 +++++++++---------- examples/graphql_example/core/src/mutation.rs | 20 +++++++++---------- examples/jsonrpsee_example/api/src/lib.rs | 2 +- .../jsonrpsee_example/core/src/mutation.rs | 20 +++++++++---------- examples/poem_example/core/src/mutation.rs | 20 +++++++++---------- examples/rocket_example/core/src/mutation.rs | 20 +++++++++---------- examples/salvo_example/core/src/mutation.rs | 20 +++++++++---------- examples/tonic_example/core/src/mutation.rs | 20 +++++++++---------- 10 files changed, 91 insertions(+), 91 deletions(-) diff --git a/examples/actix3_example/core/src/mutation.rs b/examples/actix3_example/core/src/mutation.rs index 7f0150a63..dd6891d4a 100644 --- a/examples/actix3_example/core/src/mutation.rs +++ b/examples/actix3_example/core/src/mutation.rs @@ -22,11 +22,11 @@ impl Mutation { id: i32, form_data: post::Model, ) -> Result { - let post: post::ActiveModel = if let Ok(Some(post)) = Post::find_by_id(id).one(db).await { - post.into() - } else { - return Err(DbErr::Custom("Cannot find post.".to_owned())); - }; + let post: post::ActiveModel = Post::find_by_id(id) + .one(db) + .await? + .ok_or(DbErr::Custom("Cannot find post.".to_owned())) + .map(Into::into)?; post::ActiveModel { id: post.id, @@ -38,11 +38,11 @@ impl Mutation { } pub async fn delete_post(db: &DbConn, id: i32) -> Result { - let post: post::ActiveModel = if let Ok(Some(post)) = Post::find_by_id(id).one(db).await { - post.into() - } else { - return Err(DbErr::Custom("Cannot find post.".to_owned())); - }; + let post: post::ActiveModel = Post::find_by_id(id) + .one(db) + .await? + .ok_or(DbErr::Custom("Cannot find post.".to_owned())) + .map(Into::into)?; post.delete(db).await } diff --git a/examples/actix_example/core/src/mutation.rs b/examples/actix_example/core/src/mutation.rs index 7f0150a63..dd6891d4a 100644 --- a/examples/actix_example/core/src/mutation.rs +++ b/examples/actix_example/core/src/mutation.rs @@ -22,11 +22,11 @@ impl Mutation { id: i32, form_data: post::Model, ) -> Result { - let post: post::ActiveModel = if let Ok(Some(post)) = Post::find_by_id(id).one(db).await { - post.into() - } else { - return Err(DbErr::Custom("Cannot find post.".to_owned())); - }; + let post: post::ActiveModel = Post::find_by_id(id) + .one(db) + .await? + .ok_or(DbErr::Custom("Cannot find post.".to_owned())) + .map(Into::into)?; post::ActiveModel { id: post.id, @@ -38,11 +38,11 @@ impl Mutation { } pub async fn delete_post(db: &DbConn, id: i32) -> Result { - let post: post::ActiveModel = if let Ok(Some(post)) = Post::find_by_id(id).one(db).await { - post.into() - } else { - return Err(DbErr::Custom("Cannot find post.".to_owned())); - }; + let post: post::ActiveModel = Post::find_by_id(id) + .one(db) + .await? + .ok_or(DbErr::Custom("Cannot find post.".to_owned())) + .map(Into::into)?; post.delete(db).await } diff --git a/examples/axum_example/core/src/mutation.rs b/examples/axum_example/core/src/mutation.rs index 7f0150a63..dd6891d4a 100644 --- a/examples/axum_example/core/src/mutation.rs +++ b/examples/axum_example/core/src/mutation.rs @@ -22,11 +22,11 @@ impl Mutation { id: i32, form_data: post::Model, ) -> Result { - let post: post::ActiveModel = if let Ok(Some(post)) = Post::find_by_id(id).one(db).await { - post.into() - } else { - return Err(DbErr::Custom("Cannot find post.".to_owned())); - }; + let post: post::ActiveModel = Post::find_by_id(id) + .one(db) + .await? + .ok_or(DbErr::Custom("Cannot find post.".to_owned())) + .map(Into::into)?; post::ActiveModel { id: post.id, @@ -38,11 +38,11 @@ impl Mutation { } pub async fn delete_post(db: &DbConn, id: i32) -> Result { - let post: post::ActiveModel = if let Ok(Some(post)) = Post::find_by_id(id).one(db).await { - post.into() - } else { - return Err(DbErr::Custom("Cannot find post.".to_owned())); - }; + let post: post::ActiveModel = Post::find_by_id(id) + .one(db) + .await? + .ok_or(DbErr::Custom("Cannot find post.".to_owned())) + .map(Into::into)?; post.delete(db).await } diff --git a/examples/graphql_example/core/src/mutation.rs b/examples/graphql_example/core/src/mutation.rs index 6eac6814a..1f2447fba 100644 --- a/examples/graphql_example/core/src/mutation.rs +++ b/examples/graphql_example/core/src/mutation.rs @@ -23,11 +23,11 @@ impl Mutation { id: i32, form_data: note::Model, ) -> Result { - let note: note::ActiveModel = if let Ok(Some(note)) = Note::find_by_id(id).one(db).await { - note.into() - } else { - return Err(DbErr::Custom("Cannot find note.".to_owned())); - }; + let note: note::ActiveModel = Note::find_by_id(id) + .one(db) + .await? + .ok_or(DbErr::Custom("Cannot find note.".to_owned())) + .map(Into::into)?; note::ActiveModel { id: note.id, @@ -39,11 +39,11 @@ impl Mutation { } pub async fn delete_note(db: &DbConn, id: i32) -> Result { - let note: note::ActiveModel = if let Ok(Some(note)) = Note::find_by_id(id).one(db).await { - note.into() - } else { - return Err(DbErr::Custom("Cannot find note.".to_owned())); - }; + let note: note::ActiveModel = Note::find_by_id(id) + .one(db) + .await? + .ok_or(DbErr::Custom("Cannot find note.".to_owned())) + .map(Into::into)?; note.delete(db).await } diff --git a/examples/jsonrpsee_example/api/src/lib.rs b/examples/jsonrpsee_example/api/src/lib.rs index 2e5f2c723..f98cc1832 100644 --- a/examples/jsonrpsee_example/api/src/lib.rs +++ b/examples/jsonrpsee_example/api/src/lib.rs @@ -60,7 +60,7 @@ impl PostRpcServer for PpcImpl { async fn insert(&self, p: post::Model) -> RpcResult { let new_post = Mutation::create_post(&self.conn, p) .await - .expect("could not insert post"); + .internal_call_error()?; Ok(new_post.id.unwrap()) } diff --git a/examples/jsonrpsee_example/core/src/mutation.rs b/examples/jsonrpsee_example/core/src/mutation.rs index 7f0150a63..dd6891d4a 100644 --- a/examples/jsonrpsee_example/core/src/mutation.rs +++ b/examples/jsonrpsee_example/core/src/mutation.rs @@ -22,11 +22,11 @@ impl Mutation { id: i32, form_data: post::Model, ) -> Result { - let post: post::ActiveModel = if let Ok(Some(post)) = Post::find_by_id(id).one(db).await { - post.into() - } else { - return Err(DbErr::Custom("Cannot find post.".to_owned())); - }; + let post: post::ActiveModel = Post::find_by_id(id) + .one(db) + .await? + .ok_or(DbErr::Custom("Cannot find post.".to_owned())) + .map(Into::into)?; post::ActiveModel { id: post.id, @@ -38,11 +38,11 @@ impl Mutation { } pub async fn delete_post(db: &DbConn, id: i32) -> Result { - let post: post::ActiveModel = if let Ok(Some(post)) = Post::find_by_id(id).one(db).await { - post.into() - } else { - return Err(DbErr::Custom("Cannot find post.".to_owned())); - }; + let post: post::ActiveModel = Post::find_by_id(id) + .one(db) + .await? + .ok_or(DbErr::Custom("Cannot find post.".to_owned())) + .map(Into::into)?; post.delete(db).await } diff --git a/examples/poem_example/core/src/mutation.rs b/examples/poem_example/core/src/mutation.rs index 7f0150a63..dd6891d4a 100644 --- a/examples/poem_example/core/src/mutation.rs +++ b/examples/poem_example/core/src/mutation.rs @@ -22,11 +22,11 @@ impl Mutation { id: i32, form_data: post::Model, ) -> Result { - let post: post::ActiveModel = if let Ok(Some(post)) = Post::find_by_id(id).one(db).await { - post.into() - } else { - return Err(DbErr::Custom("Cannot find post.".to_owned())); - }; + let post: post::ActiveModel = Post::find_by_id(id) + .one(db) + .await? + .ok_or(DbErr::Custom("Cannot find post.".to_owned())) + .map(Into::into)?; post::ActiveModel { id: post.id, @@ -38,11 +38,11 @@ impl Mutation { } pub async fn delete_post(db: &DbConn, id: i32) -> Result { - let post: post::ActiveModel = if let Ok(Some(post)) = Post::find_by_id(id).one(db).await { - post.into() - } else { - return Err(DbErr::Custom("Cannot find post.".to_owned())); - }; + let post: post::ActiveModel = Post::find_by_id(id) + .one(db) + .await? + .ok_or(DbErr::Custom("Cannot find post.".to_owned())) + .map(Into::into)?; post.delete(db).await } diff --git a/examples/rocket_example/core/src/mutation.rs b/examples/rocket_example/core/src/mutation.rs index 7f0150a63..dd6891d4a 100644 --- a/examples/rocket_example/core/src/mutation.rs +++ b/examples/rocket_example/core/src/mutation.rs @@ -22,11 +22,11 @@ impl Mutation { id: i32, form_data: post::Model, ) -> Result { - let post: post::ActiveModel = if let Ok(Some(post)) = Post::find_by_id(id).one(db).await { - post.into() - } else { - return Err(DbErr::Custom("Cannot find post.".to_owned())); - }; + let post: post::ActiveModel = Post::find_by_id(id) + .one(db) + .await? + .ok_or(DbErr::Custom("Cannot find post.".to_owned())) + .map(Into::into)?; post::ActiveModel { id: post.id, @@ -38,11 +38,11 @@ impl Mutation { } pub async fn delete_post(db: &DbConn, id: i32) -> Result { - let post: post::ActiveModel = if let Ok(Some(post)) = Post::find_by_id(id).one(db).await { - post.into() - } else { - return Err(DbErr::Custom("Cannot find post.".to_owned())); - }; + let post: post::ActiveModel = Post::find_by_id(id) + .one(db) + .await? + .ok_or(DbErr::Custom("Cannot find post.".to_owned())) + .map(Into::into)?; post.delete(db).await } diff --git a/examples/salvo_example/core/src/mutation.rs b/examples/salvo_example/core/src/mutation.rs index 7f0150a63..dd6891d4a 100644 --- a/examples/salvo_example/core/src/mutation.rs +++ b/examples/salvo_example/core/src/mutation.rs @@ -22,11 +22,11 @@ impl Mutation { id: i32, form_data: post::Model, ) -> Result { - let post: post::ActiveModel = if let Ok(Some(post)) = Post::find_by_id(id).one(db).await { - post.into() - } else { - return Err(DbErr::Custom("Cannot find post.".to_owned())); - }; + let post: post::ActiveModel = Post::find_by_id(id) + .one(db) + .await? + .ok_or(DbErr::Custom("Cannot find post.".to_owned())) + .map(Into::into)?; post::ActiveModel { id: post.id, @@ -38,11 +38,11 @@ impl Mutation { } pub async fn delete_post(db: &DbConn, id: i32) -> Result { - let post: post::ActiveModel = if let Ok(Some(post)) = Post::find_by_id(id).one(db).await { - post.into() - } else { - return Err(DbErr::Custom("Cannot find post.".to_owned())); - }; + let post: post::ActiveModel = Post::find_by_id(id) + .one(db) + .await? + .ok_or(DbErr::Custom("Cannot find post.".to_owned())) + .map(Into::into)?; post.delete(db).await } diff --git a/examples/tonic_example/core/src/mutation.rs b/examples/tonic_example/core/src/mutation.rs index 7f0150a63..dd6891d4a 100644 --- a/examples/tonic_example/core/src/mutation.rs +++ b/examples/tonic_example/core/src/mutation.rs @@ -22,11 +22,11 @@ impl Mutation { id: i32, form_data: post::Model, ) -> Result { - let post: post::ActiveModel = if let Ok(Some(post)) = Post::find_by_id(id).one(db).await { - post.into() - } else { - return Err(DbErr::Custom("Cannot find post.".to_owned())); - }; + let post: post::ActiveModel = Post::find_by_id(id) + .one(db) + .await? + .ok_or(DbErr::Custom("Cannot find post.".to_owned())) + .map(Into::into)?; post::ActiveModel { id: post.id, @@ -38,11 +38,11 @@ impl Mutation { } pub async fn delete_post(db: &DbConn, id: i32) -> Result { - let post: post::ActiveModel = if let Ok(Some(post)) = Post::find_by_id(id).one(db).await { - post.into() - } else { - return Err(DbErr::Custom("Cannot find post.".to_owned())); - }; + let post: post::ActiveModel = Post::find_by_id(id) + .one(db) + .await? + .ok_or(DbErr::Custom("Cannot find post.".to_owned())) + .map(Into::into)?; post.delete(db).await } From 32169c0c8bc3253070815acfb7b6fe8da810da78 Mon Sep 17 00:00:00 2001 From: Billy Chan Date: Wed, 21 Sep 2022 17:43:15 +0800 Subject: [PATCH 31/31] Fix Rocket examples --- examples/rocket_example/{api => }/Rocket.toml | 2 +- examples/rocket_example/api/Cargo.toml | 1 + examples/rocket_example/api/src/lib.rs | 9 ++++++--- 3 files changed, 8 insertions(+), 4 deletions(-) rename examples/rocket_example/{api => }/Rocket.toml (91%) diff --git a/examples/rocket_example/api/Rocket.toml b/examples/rocket_example/Rocket.toml similarity index 91% rename from examples/rocket_example/api/Rocket.toml rename to examples/rocket_example/Rocket.toml index fc294bd2f..41e0183c9 100644 --- a/examples/rocket_example/api/Rocket.toml +++ b/examples/rocket_example/Rocket.toml @@ -1,5 +1,5 @@ [default] -template_dir = "templates/" +template_dir = "api/templates/" [default.databases.sea_orm] # Mysql diff --git a/examples/rocket_example/api/Cargo.toml b/examples/rocket_example/api/Cargo.toml index e0d718d8e..1a88308fc 100644 --- a/examples/rocket_example/api/Cargo.toml +++ b/examples/rocket_example/api/Cargo.toml @@ -20,6 +20,7 @@ rocket_dyn_templates = { version = "0.1.0-rc.1", features = [ serde_json = { version = "^1" } entity = { path = "../entity" } migration = { path = "../migration" } +tokio = "1.20.0" [dependencies.sea-orm-rocket] path = "../../../sea-orm-rocket/lib" # remove this line in your own project and use the git line diff --git a/examples/rocket_example/api/src/lib.rs b/examples/rocket_example/api/src/lib.rs index f283b1015..51918c7a3 100644 --- a/examples/rocket_example/api/src/lib.rs +++ b/examples/rocket_example/api/src/lib.rs @@ -1,7 +1,6 @@ #[macro_use] extern crate rocket; -use futures::executor::block_on; use rocket::fairing::{self, AdHoc}; use rocket::form::{Context, Form}; use rocket::fs::{relative, FileServer}; @@ -144,7 +143,8 @@ async fn run_migrations(rocket: Rocket) -> fairing::Result { Ok(rocket) } -fn rocket() -> Rocket { +#[tokio::main] +async fn start() -> Result<(), rocket::Error> { rocket::build() .attach(Db::init()) .attach(AdHoc::try_on_ignite("Migrations", run_migrations)) @@ -155,10 +155,13 @@ fn rocket() -> Rocket { ) .register("/", catchers![not_found]) .attach(Template::fairing()) + .launch() + .await + .map(|_| ()) } pub fn main() { - let result = block_on(rocket().launch()); + let result = start(); println!("Rocket: deorbit.");