From 63c7307e434480688fd9adc6f719159fe1d3ec51 Mon Sep 17 00:00:00 2001 From: EETagent Date: Sun, 23 Oct 2022 18:43:12 +0200 Subject: [PATCH] chore: project setup --- .gitignore | 14 ++++++ Cargo.toml | 12 +++++ Rocket.toml | 5 +++ api/Cargo.toml | 25 +++++++++++ api/src/lib.rs | 100 ++++++++++++++++++++++++++++++++++++++++++ api/src/pool.rs | 41 +++++++++++++++++ core/Cargo.toml | 25 +++++++++++ core/src/lib.rs | 7 +++ core/src/mutation.rs | 53 ++++++++++++++++++++++ core/src/query.rs | 26 +++++++++++ core/tests/mock.rs | 79 +++++++++++++++++++++++++++++++++ core/tests/prepare.rs | 50 +++++++++++++++++++++ entity/Cargo.toml | 17 +++++++ entity/src/lib.rs | 4 ++ entity/src/post.rs | 18 ++++++++ migration/Cargo.toml | 17 +++++++ migration/README.md | 37 ++++++++++++++++ migration/src/lib.rs | 10 +++++ migration/src/main.rs | 17 +++++++ src/main.rs | 3 ++ 20 files changed, 560 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 Rocket.toml create mode 100644 api/Cargo.toml create mode 100644 api/src/lib.rs create mode 100644 api/src/pool.rs create mode 100644 core/Cargo.toml create mode 100644 core/src/lib.rs create mode 100644 core/src/mutation.rs create mode 100644 core/src/query.rs create mode 100644 core/tests/mock.rs create mode 100644 core/tests/prepare.rs create mode 100644 entity/Cargo.toml create mode 100644 entity/src/lib.rs create mode 100644 entity/src/post.rs create mode 100644 migration/Cargo.toml create mode 100644 migration/README.md create mode 100644 migration/src/lib.rs create mode 100644 migration/src/main.rs create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ada8be9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..0da4e67 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "sea-orm-rocket-example" +version = "0.1.0" +authors = ["Vojtěch Jungmann", "Sebastian Pravda"] +edition = "2021" +publish = false + +[workspace] +members = [".", "api", "core", "entity", "migration"] + +[dependencies] +portfolio-api = { path = "api" } diff --git a/Rocket.toml b/Rocket.toml new file mode 100644 index 0000000..a0d4956 --- /dev/null +++ b/Rocket.toml @@ -0,0 +1,5 @@ +#[default] +#template_dir = "api/templates/" + +[default.databases.sea_orm] +#url = "postgres://root:root@localhost/rocket_example" diff --git a/api/Cargo.toml b/api/Cargo.toml new file mode 100644 index 0000000..9b40183 --- /dev/null +++ b/api/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "portfolio-api" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +async-stream = { version = "^0.3" } +async-trait = { version = "0.1" } +futures = { version = "^0.3" } +futures-util = { version = "^0.3" } +rocket = { version = "0.5.0-rc.2", features = [ + "json", +] } + +serde_json = { version = "^1" } + +portfolio-entity = { path = "../entity" } +portfolio-migration = { path = "../migration" } +portfolio-core = { path = "../core" } + +tokio = "1.21.2" + +[dependencies.sea-orm-rocket] +version = "0.5.1" diff --git a/api/src/lib.rs b/api/src/lib.rs new file mode 100644 index 0000000..f595676 --- /dev/null +++ b/api/src/lib.rs @@ -0,0 +1,100 @@ +#[macro_use] +extern crate rocket; + +use rocket::fairing::{self, AdHoc}; +use rocket::form::{ Form}; +use rocket::fs::{relative, FileServer}; +use rocket::response::{Flash, Redirect}; +use rocket::{Build, Rocket}; +use portfolio_core::{Mutation, Query}; + +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; + + +#[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.") +} + +#[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(()) +} + +async fn run_migrations(rocket: Rocket) -> fairing::Result { + let conn = &Db::fetch(&rocket).unwrap().conn; + let _ = migration::Migrator::up(conn, None).await; + Ok(rocket) +} + +#[tokio::main] +async fn start() -> Result<(), rocket::Error> { + rocket::build() + .attach(Db::init()) + .attach(AdHoc::try_on_ignite("Migrations", run_migrations)) + //.mount("/", FileServer::from(relative!("/static"))) + .mount("/", routes![create, delete, destroy, update]) + .register("/", catchers![]) + .launch() + .await + .map(|_| ()) +} + +pub fn main() { + let result = start(); + + println!("Rocket: deorbit."); + + if let Some(err) = result.err() { + println!("Error: {}", err); + } +} diff --git a/api/src/pool.rs b/api/src/pool.rs new file mode 100644 index 0000000..69287ae --- /dev/null +++ b/api/src/pool.rs @@ -0,0 +1,41 @@ +use portfolio_core::sea_orm; + +use async_trait::async_trait; +use sea_orm::ConnectOptions; +use sea_orm_rocket::{rocket::figment::Figment, Config, Database}; +use std::time::Duration; + +#[derive(Database, Debug)] +#[database("sea_orm")] +pub struct Db(SeaOrmPool); + +#[derive(Debug, Clone)] +pub struct SeaOrmPool { + pub conn: sea_orm::DatabaseConnection, +} + +#[async_trait] +impl sea_orm_rocket::Pool for SeaOrmPool { + type Error = sea_orm::DbErr; + + type Connection = sea_orm::DatabaseConnection; + + async fn init(figment: &Figment) -> Result { + let config = figment.extract::().unwrap(); + let mut options: ConnectOptions = config.url.into(); + options + .max_connections(config.max_connections as u32) + .min_connections(config.min_connections.unwrap_or_default()) + .connect_timeout(Duration::from_secs(config.connect_timeout)); + if let Some(idle_timeout) = config.idle_timeout { + options.idle_timeout(Duration::from_secs(idle_timeout)); + } + let conn = sea_orm::Database::connect(options).await?; + + Ok(SeaOrmPool { conn }) + } + + fn borrow(&self) -> &Self::Connection { + &self.conn + } +} diff --git a/core/Cargo.toml b/core/Cargo.toml new file mode 100644 index 0000000..0fcb32c --- /dev/null +++ b/core/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "portfolio-core" +version = "0.1.0" +edition = "2021" + +[dependencies] +portfolio-entity = { path = "../entity" } + +[dependencies.sea-orm] +version = "^0.10.0" +features = [ + "runtime-tokio-native-tls", + "sqlx-postgres", + "sqlx-sqlite", +] + +[dev-dependencies] +tokio = "1.21.2" + +[features] +mock = ["sea-orm/mock"] + +[[test]] +name = "mock" +required-features = ["mock"] diff --git a/core/src/lib.rs b/core/src/lib.rs new file mode 100644 index 0000000..4a80f23 --- /dev/null +++ b/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/core/src/mutation.rs b/core/src/mutation.rs new file mode 100644 index 0000000..dd6891d --- /dev/null +++ b/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 = 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, + 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 = Post::find_by_id(id) + .one(db) + .await? + .ok_or(DbErr::Custom("Cannot find post.".to_owned())) + .map(Into::into)?; + + post.delete(db).await + } + + pub async fn delete_all_posts(db: &DbConn) -> Result { + Post::delete_many().exec(db).await + } +} diff --git a/core/src/query.rs b/core/src/query.rs new file mode 100644 index 0000000..e8d2668 --- /dev/null +++ b/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/core/tests/mock.rs b/core/tests/mock.rs new file mode 100644 index 0000000..84b187e --- /dev/null +++ b/core/tests/mock.rs @@ -0,0 +1,79 @@ +mod prepare; + +use entity::post; +use prepare::prepare_mock_db; +use rocket_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/core/tests/prepare.rs b/core/tests/prepare.rs new file mode 100644 index 0000000..4518049 --- /dev/null +++ b/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/entity/Cargo.toml b/entity/Cargo.toml new file mode 100644 index 0000000..b45247b --- /dev/null +++ b/entity/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "portfolio-entity" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +name = "entity" +path = "src/lib.rs" + +[dependencies] +rocket = { version = "0.5.0-rc.2", features = [ + "json", +] } + +[dependencies.sea-orm] +version = "^0.10.0" diff --git a/entity/src/lib.rs b/entity/src/lib.rs new file mode 100644 index 0000000..06480a1 --- /dev/null +++ b/entity/src/lib.rs @@ -0,0 +1,4 @@ +#[macro_use] +extern crate rocket; + +pub mod post; diff --git a/entity/src/post.rs b/entity/src/post.rs new file mode 100644 index 0000000..ecf3f98 --- /dev/null +++ b/entity/src/post.rs @@ -0,0 +1,18 @@ +use rocket::serde::{Deserialize, Serialize}; +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Deserialize, Serialize, FromForm)] +#[serde(crate = "rocket::serde")] +#[sea_orm(table_name = "posts")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub title: String, + #[sea_orm(column_type = "Text")] + pub text: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/migration/Cargo.toml b/migration/Cargo.toml new file mode 100644 index 0000000..fdd77e4 --- /dev/null +++ b/migration/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "portfolio-migration" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +name = "migration" +path = "src/lib.rs" + +[dependencies] +rocket = { version = "0.5.0-rc.2" } +async-std = { version = "^1", features = ["attributes", "tokio1"] } + +[dependencies.sea-orm-migration] +version = "^0.10.0" +features = [ "runtime-tokio-native-tls", "sqlx-postgres", "sqlx-sqlite"] diff --git a/migration/README.md b/migration/README.md new file mode 100644 index 0000000..963caae --- /dev/null +++ b/migration/README.md @@ -0,0 +1,37 @@ +# Running Migrator CLI + +- Apply all pending migrations + ```sh + cargo run + ``` + ```sh + cargo run -- up + ``` +- Apply first 10 pending migrations + ```sh + cargo run -- up -n 10 + ``` +- Rollback last applied migrations + ```sh + cargo run -- down + ``` +- Rollback last 10 applied migrations + ```sh + cargo run -- down -n 10 + ``` +- Drop all tables from the database, then reapply all migrations + ```sh + cargo run -- fresh + ``` +- Rollback all applied migrations, then reapply all migrations + ```sh + cargo run -- refresh + ``` +- Rollback all applied migrations + ```sh + cargo run -- reset + ``` +- Check the status of all migrations + ```sh + cargo run -- status + ``` diff --git a/migration/src/lib.rs b/migration/src/lib.rs new file mode 100644 index 0000000..838e47a --- /dev/null +++ b/migration/src/lib.rs @@ -0,0 +1,10 @@ +pub use sea_orm_migration::prelude::*; + +pub struct Migrator; + +#[async_trait::async_trait] +impl MigratorTrait for Migrator { + fn migrations() -> Vec> { + vec![] + } +} diff --git a/migration/src/main.rs b/migration/src/main.rs new file mode 100644 index 0000000..4626e82 --- /dev/null +++ b/migration/src/main.rs @@ -0,0 +1,17 @@ +use sea_orm_migration::prelude::*; + +#[async_std::main] +async fn main() { + // Setting `DATABASE_URL` environment variable + let key = "DATABASE_URL"; + if std::env::var(key).is_err() { + // Getting the database URL from Rocket.toml if it's not set + let figment = rocket::Config::figment(); + let database_url: String = figment + .extract_inner("databases.sea_orm.url") + .expect("Cannot find Database URL in Rocket.toml"); + std::env::set_var(key, database_url); + } + + cli::run_cli(migration::Migrator).await; +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..946b45d --- /dev/null +++ b/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + portfolio_api::main(); +}