diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 62382ad..3ae2a1a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,10 +17,10 @@ jobs: toolchain: stable # Try to build all examples - - name: Build simple-crud example - run: cargo build --manifest-path simple-crud/Cargo.toml - - name: Build todo-app example - run: cargo build --manifest-path todo-app/Cargo.toml + - name: Build bakery-backend example + run: cargo build --manifest-path bakery-backend/Cargo.toml + - name: Build rocket-example + run: cargo build --manifest-path rocket-example/Cargo.toml # Try to build mdbooks - name: Install mdbook diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..dc12608 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,2 @@ +[workspace] +members = ["bakery-backend", "rocket-example"] diff --git a/README.md b/README.md index 3f13443..e3d87aa 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ # SeaORM Tutorials -This repository contains step-by-step tutorials on how to use SeaORM to do CRUD operations from simple ones to very complex online applications in Rust Language. +This repository contains step-by-step tutorials on how to use SeaORM to do CRUD operations on databases in the Rust Language. -The tutorial is based on a software system for managing fruits in a fruit market. +The tutorial is based on a software system for managing a simple database to store information of bakeries. -The tutorials are +The tutorials contain the following chapters: -1. [**Simple CRUD operations**](https://www.sea-ql.org/sea-orm-tutorial/ch01-00-simple-crud-getting-started.html) - This tutorials explains how to use SeaORM to do basic tasks like create a table in a database, insert a row into the table, update a column, delete operations and logging the results to the console. The database used is MySQL -2. [**TODO Application**](https://www.sea-ql.org/sea-orm-tutorial/ch02-00-todo-app-getting-started.html) - This tutorial shows how to use SeaORM, SQLite and PostgreSQL to create a realtime sync TODO application where a user can buy fruits from the mango market. +1. [**Bakery Backend**](https://www.sea-ql.org/sea-orm-tutorial/ch01-00-build-backend-getting-started.html) - This chapter covers the basics of using SeaORM to interact with the database (a MySQL database is used for illustration). On top of this backend you can build any interface you need. +2. [**Rocket Integration**](https://www.sea-ql.org/sea-orm-tutorial/ch02-00-integration-with-rocket.html) - This chapter explains how to integrate the SeaORM backend into the Rocket framework to create a web application that provides a web API or even a simple frontend. [![Discord](https://img.shields.io/discord/873880840487206962?label=Discord)](https://discord.com/invite/uCPdDXzbdv) @@ -15,7 +15,7 @@ For additional help on **SeaORM** specific questions, join the support Discord c ## Running the tutorials -To run the tutorial code, switch to the directory of the turorial and run cargo +To run the tutorial code, switch to the directory of the tutorial and run cargo ```sh # Switch to tutorial directory diff --git a/bakery-backend/.gitignore b/bakery-backend/.gitignore new file mode 100644 index 0000000..1de5659 --- /dev/null +++ b/bakery-backend/.gitignore @@ -0,0 +1 @@ +target \ No newline at end of file diff --git a/bakery-backend/Cargo.toml b/bakery-backend/Cargo.toml new file mode 100644 index 0000000..993e89e --- /dev/null +++ b/bakery-backend/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "bakery-backend" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +futures = "0.3.21" +sea-orm = { version = "0.8.0", features = [ "sqlx-mysql", "runtime-async-std-native-tls", "macros", "mock" ] } +sea-orm-migration = "0.8.3" diff --git a/bakery-backend/src/entities/baker.rs b/bakery-backend/src/entities/baker.rs new file mode 100644 index 0000000..a57a551 --- /dev/null +++ b/bakery-backend/src/entities/baker.rs @@ -0,0 +1,33 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] +#[sea_orm(table_name = "baker")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub name: String, + pub contact_details: Option, + pub bakery_id: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::bakery::Entity", + from = "Column::BakeryId", + to = "super::bakery::Column::Id", + on_update = "NoAction", + on_delete = "NoAction" + )] + Bakery, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Bakery.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/bakery-backend/src/entities/bakery.rs b/bakery-backend/src/entities/bakery.rs new file mode 100644 index 0000000..c9ff157 --- /dev/null +++ b/bakery-backend/src/entities/bakery.rs @@ -0,0 +1,26 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] +#[sea_orm(table_name = "bakery")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub name: String, + pub profit_margin: f64, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::baker::Entity")] + Baker, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Baker.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/bakery-backend/src/entities/mod.rs b/bakery-backend/src/entities/mod.rs new file mode 100644 index 0000000..b5f83b2 --- /dev/null +++ b/bakery-backend/src/entities/mod.rs @@ -0,0 +1,6 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0 + +pub mod prelude; + +pub mod baker; +pub mod bakery; diff --git a/bakery-backend/src/entities/prelude.rs b/bakery-backend/src/entities/prelude.rs new file mode 100644 index 0000000..8bc9c6e --- /dev/null +++ b/bakery-backend/src/entities/prelude.rs @@ -0,0 +1,4 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0 + +pub use super::baker::Entity as Baker; +pub use super::bakery::Entity as Bakery; diff --git a/bakery-backend/src/main.rs b/bakery-backend/src/main.rs new file mode 100644 index 0000000..1b0a882 --- /dev/null +++ b/bakery-backend/src/main.rs @@ -0,0 +1,320 @@ +mod entities; +mod migrator; + +use entities::{prelude::*, *}; +use futures::executor::block_on; +use migrator::Migrator; +use sea_orm::*; +use sea_orm_migration::prelude::*; + +const DATABASE_URL: &str = "mysql://root:root@localhost:3306"; + +#[derive(FromQueryResult)] +struct BakerNameResult { + name: String, +} + +async fn run() -> Result<(), DbErr> { + let db = Database::connect(DATABASE_URL).await?; + + let db_name = "bakeries_db"; + let db = &match db.get_database_backend() { + DbBackend::MySql => { + db.execute(Statement::from_string( + db.get_database_backend(), + format!("CREATE DATABASE IF NOT EXISTS `{}`;", db_name), + )) + .await?; + + let url = format!("{}/{}", DATABASE_URL, db_name); + Database::connect(&url).await? + } + DbBackend::Postgres => { + db.execute(Statement::from_string( + db.get_database_backend(), + format!("DROP DATABASE IF EXISTS \"{}\";", db_name), + )) + .await?; + db.execute(Statement::from_string( + db.get_database_backend(), + format!("CREATE DATABASE \"{}\";", db_name), + )) + .await?; + + let url = format!("{}/{}", DATABASE_URL, db_name); + Database::connect(&url).await? + } + DbBackend::Sqlite => db, + }; + let schema_manager = SchemaManager::new(db); // To investigate the schema + + Migrator::refresh(db).await?; + assert!(schema_manager.has_table("bakery").await?); + assert!(schema_manager.has_table("baker").await?); + + // Insert and Update + { + let happy_bakery = bakery::ActiveModel { + name: ActiveValue::Set("Happy Bakery".to_owned()), + profit_margin: ActiveValue::Set(0.0), + ..Default::default() + }; + let res = Bakery::insert(happy_bakery).exec(db).await?; + + let sad_bakery = bakery::ActiveModel { + id: ActiveValue::Set(res.last_insert_id), + name: ActiveValue::Set("Sad Bakery".to_owned()), + profit_margin: ActiveValue::NotSet, + }; + sad_bakery.update(db).await?; + + let john = baker::ActiveModel { + name: ActiveValue::Set("John".to_owned()), + bakery_id: ActiveValue::Set(res.last_insert_id), + ..Default::default() + }; + Baker::insert(john).exec(db).await?; + } + + // Read + { + let bakeries: Vec = Bakery::find().all(db).await?; + assert_eq!(bakeries.len(), 1); + + // Finding by id is built-in + let sad_bakery: Option = Bakery::find_by_id(1).one(db).await?; + assert_eq!(sad_bakery.unwrap().name, "Sad Bakery"); + + // Finding by arbitrary column with `filter()` + let sad_bakery: Option = Bakery::find() + .filter(bakery::Column::Name.eq("Sad Bakery")) + .one(db) + .await?; + assert_eq!(sad_bakery.unwrap().id, 1); + } + + // Delete + { + let john = baker::ActiveModel { + id: ActiveValue::Set(1), // The primary must be set + ..Default::default() + }; + john.delete(db).await?; + + let sad_bakery = bakery::ActiveModel { + id: ActiveValue::Set(1), // The primary must be set + ..Default::default() + }; + sad_bakery.delete(db).await?; + + let bakeries: Vec = Bakery::find().all(db).await?; + assert!(bakeries.is_empty()); + } + + // Relational Select + { + let la_boulangerie = bakery::ActiveModel { + name: ActiveValue::Set("La Boulangerie".to_owned()), + profit_margin: ActiveValue::Set(0.0), + ..Default::default() + }; + let bakery_res = Bakery::insert(la_boulangerie).exec(db).await?; + + for baker_name in ["Jolie", "Charles", "Madeleine", "Frederic"] { + let baker = baker::ActiveModel { + name: ActiveValue::Set(baker_name.to_owned()), + bakery_id: ActiveValue::Set(bakery_res.last_insert_id), + ..Default::default() + }; + Baker::insert(baker).exec(db).await?; + } + + // First find *La Boulangerie* as a Model + let la_boulangerie: bakery::Model = Bakery::find_by_id(bakery_res.last_insert_id) + .one(db) + .await? + .unwrap(); + + let bakers: Vec = la_boulangerie.find_related(Baker).all(db).await?; + let mut baker_names: Vec = bakers.into_iter().map(|b| b.name).collect(); + baker_names.sort_unstable(); + + assert_eq!(baker_names, ["Charles", "Frederic", "Jolie", "Madeleine"]); + } + + // Mock Testing + { + let db = &MockDatabase::new(DatabaseBackend::MySql) + .append_query_results(vec![ + // First query result + vec![bakery::Model { + id: 1, + name: "Happy Bakery".to_owned(), + profit_margin: 0.0, + }], + // Second query result + vec![ + bakery::Model { + id: 1, + name: "Happy Bakery".to_owned(), + profit_margin: 0.0, + }, + bakery::Model { + id: 2, + name: "Sad Bakery".to_owned(), + profit_margin: 100.0, + }, + bakery::Model { + id: 3, + name: "La Boulangerie".to_owned(), + profit_margin: 17.89, + }, + ], + ]) + .append_query_results(vec![ + // Third query result + vec![ + baker::Model { + id: 1, + name: "Jolie".to_owned(), + contact_details: None, + bakery_id: 3, + }, + baker::Model { + id: 2, + name: "Charles".to_owned(), + contact_details: None, + bakery_id: 3, + }, + baker::Model { + id: 3, + name: "Madeleine".to_owned(), + contact_details: None, + bakery_id: 3, + }, + baker::Model { + id: 4, + name: "Frederic".to_owned(), + contact_details: None, + bakery_id: 3, + }, + ], + ]) + .into_connection(); + + let happy_bakery: Option = Bakery::find().one(db).await?; + assert_eq!( + happy_bakery.unwrap(), + bakery::Model { + id: 1, + name: "Happy Bakery".to_owned(), + profit_margin: 0.0, + } + ); + + let all_bakeries: Vec = Bakery::find().all(db).await?; + assert_eq!( + all_bakeries, + vec![ + bakery::Model { + id: 1, + name: "Happy Bakery".to_owned(), + profit_margin: 0.0, + }, + bakery::Model { + id: 2, + name: "Sad Bakery".to_owned(), + profit_margin: 100.0, + }, + bakery::Model { + id: 3, + name: "La Boulangerie".to_owned(), + profit_margin: 17.89, + }, + ] + ); + + let la_boulangerie_bakers: Vec = Baker::find().all(db).await?; + assert_eq!( + la_boulangerie_bakers, + vec![ + baker::Model { + id: 1, + name: "Jolie".to_owned(), + contact_details: None, + bakery_id: 3, + }, + baker::Model { + id: 2, + name: "Charles".to_owned(), + contact_details: None, + bakery_id: 3, + }, + baker::Model { + id: 3, + name: "Madeleine".to_owned(), + contact_details: None, + bakery_id: 3, + }, + baker::Model { + id: 4, + name: "Frederic".to_owned(), + contact_details: None, + bakery_id: 3, + }, + ] + ); + } + + // SeaQuery insert + { + let columns: Vec = ["name", "profit_margin"] + .into_iter() + .map(Alias::new) + .collect(); + + let mut stmt = Query::insert(); + stmt.into_table(bakery::Entity).columns(columns); + + stmt.values_panic(["SQL Bakery".into(), (-100.0).into()]); + + let builder = db.get_database_backend(); + db.execute(builder.build(&stmt)).await?; + } + + // SeaQuery select + { + let column = (baker::Entity, Alias::new("name")); + + let mut stmt = Query::select(); + stmt.column(column.clone()) + .from(baker::Entity) + .join( + JoinType::Join, + bakery::Entity, + Expr::tbl(baker::Entity, Alias::new("bakery_id")) + .equals(bakery::Entity, Alias::new("id")), + ) + .order_by(column, Order::Asc); + + let builder = db.get_database_backend(); + let baker = BakerNameResult::find_by_statement(builder.build(&stmt)) + .all(db) + .await?; + + let baker_names = baker.into_iter().map(|b| b.name).collect::>(); + + assert_eq!( + baker_names, + vec!["Charles", "Frederic", "Jolie", "Madeleine"] + ); + } + + Ok(()) +} + +fn main() { + if let Err(err) = block_on(run()) { + panic!("{}", err); + } +} diff --git a/bakery-backend/src/migrator/m20220602_000001_create_bakery_table.rs b/bakery-backend/src/migrator/m20220602_000001_create_bakery_table.rs new file mode 100644 index 0000000..03ae011 --- /dev/null +++ b/bakery-backend/src/migrator/m20220602_000001_create_bakery_table.rs @@ -0,0 +1,46 @@ +use sea_orm_migration::prelude::*; + +pub struct Migration; + +impl MigrationName for Migration { + fn name(&self) -> &str { + "m_20220602_000001_create_bakery_table" + } +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .if_not_exists() + .table(Bakery::Table) + .col( + ColumnDef::new(Bakery::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(Bakery::Name).string().not_null()) + .col(ColumnDef::new(Bakery::ProfitMargin).double().not_null()) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(Bakery::Table).to_owned()) + .await + } +} + +#[derive(Iden)] +pub enum Bakery { + Table, + Id, + Name, + ProfitMargin, +} diff --git a/bakery-backend/src/migrator/m20220602_000002_create_baker_table.rs b/bakery-backend/src/migrator/m20220602_000002_create_baker_table.rs new file mode 100644 index 0000000..91ee311 --- /dev/null +++ b/bakery-backend/src/migrator/m20220602_000002_create_baker_table.rs @@ -0,0 +1,56 @@ +use sea_orm_migration::prelude::*; + +use super::m20220602_000001_create_bakery_table::Bakery; + +pub struct Migration; + +impl MigrationName for Migration { + fn name(&self) -> &str { + "m_20220602_000002_create_baker_table" + } +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .if_not_exists() + .table(Baker::Table) + .col( + ColumnDef::new(Baker::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(Baker::Name).string().not_null()) + .col(ColumnDef::new(Baker::ContactDetails).json()) + .col(ColumnDef::new(Baker::BakeryId).integer().not_null()) + .foreign_key( + ForeignKey::create() + .name("fk-baker-bakery_id") + .from(Baker::Table, Baker::BakeryId) + .to(Bakery::Table, Bakery::Id), + ) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(Baker::Table).to_owned()) + .await + } +} + +#[derive(Iden)] +pub enum Baker { + Table, + Id, + Name, + ContactDetails, + BakeryId, +} diff --git a/bakery-backend/src/migrator/mod.rs b/bakery-backend/src/migrator/mod.rs new file mode 100644 index 0000000..9cbc98b --- /dev/null +++ b/bakery-backend/src/migrator/mod.rs @@ -0,0 +1,16 @@ +use sea_orm_migration::prelude::*; + +mod m20220602_000001_create_bakery_table; +mod m20220602_000002_create_baker_table; + +pub struct Migrator; + +#[async_trait::async_trait] +impl MigratorTrait for Migrator { + fn migrations() -> Vec> { + vec![ + Box::new(m20220602_000001_create_bakery_table::Migration), + Box::new(m20220602_000002_create_baker_table::Migration), + ] + } +} diff --git a/rocket-example/.gitignore b/rocket-example/.gitignore new file mode 100644 index 0000000..1de5659 --- /dev/null +++ b/rocket-example/.gitignore @@ -0,0 +1 @@ +target \ No newline at end of file diff --git a/rocket-example/Cargo.toml b/rocket-example/Cargo.toml new file mode 100644 index 0000000..f3da933 --- /dev/null +++ b/rocket-example/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "rocket-example" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +rocket = { version = "^0.5.0-rc.2", features = ["json"] } +sea-orm = { version = "0.8.0", features = [ + "sqlx-mysql", + "runtime-async-std-native-tls", + "macros", +] } +sea-orm-migration = "0.8.3" +serde_json = "1.0.81" + +[dependencies.rocket_dyn_templates] +version = "0.1.0-rc.2" +features = ["tera"] diff --git a/rocket-example/Rocket.toml b/rocket-example/Rocket.toml new file mode 100644 index 0000000..bdaeca5 --- /dev/null +++ b/rocket-example/Rocket.toml @@ -0,0 +1,2 @@ +[default] +template_dir = "templates/" diff --git a/rocket-example/src/entities/baker.rs b/rocket-example/src/entities/baker.rs new file mode 100644 index 0000000..a57a551 --- /dev/null +++ b/rocket-example/src/entities/baker.rs @@ -0,0 +1,33 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] +#[sea_orm(table_name = "baker")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub name: String, + pub contact_details: Option, + pub bakery_id: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::bakery::Entity", + from = "Column::BakeryId", + to = "super::bakery::Column::Id", + on_update = "NoAction", + on_delete = "NoAction" + )] + Bakery, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Bakery.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/rocket-example/src/entities/bakery.rs b/rocket-example/src/entities/bakery.rs new file mode 100644 index 0000000..c9ff157 --- /dev/null +++ b/rocket-example/src/entities/bakery.rs @@ -0,0 +1,26 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] +#[sea_orm(table_name = "bakery")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub name: String, + pub profit_margin: f64, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::baker::Entity")] + Baker, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Baker.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/rocket-example/src/entities/mod.rs b/rocket-example/src/entities/mod.rs new file mode 100644 index 0000000..b5f83b2 --- /dev/null +++ b/rocket-example/src/entities/mod.rs @@ -0,0 +1,6 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0 + +pub mod prelude; + +pub mod baker; +pub mod bakery; diff --git a/rocket-example/src/entities/prelude.rs b/rocket-example/src/entities/prelude.rs new file mode 100644 index 0000000..8bc9c6e --- /dev/null +++ b/rocket-example/src/entities/prelude.rs @@ -0,0 +1,4 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0 + +pub use super::baker::Entity as Baker; +pub use super::bakery::Entity as Bakery; diff --git a/rocket-example/src/main.rs b/rocket-example/src/main.rs new file mode 100644 index 0000000..9ab1023 --- /dev/null +++ b/rocket-example/src/main.rs @@ -0,0 +1,151 @@ +mod entities; +mod migrator; +mod setup; + +use entities::{prelude::*, *}; +use migrator::Migrator; +use rocket::{ + fs::{relative, FileServer}, + *, +}; +use rocket_dyn_templates::Template; +use sea_orm::*; +use sea_orm_migration::MigratorTrait; +use serde_json::json; +use setup::set_up_db; + +#[get("/")] +fn index() -> Template { + Template::render("index", json!({})) +} + +#[get("/bakeries")] +async fn bakeries(db: &State) -> Result { + let db = db as &DatabaseConnection; + + let bakeries = Bakery::find() + .all(db) + .await + .map_err(Into::into)? + .into_iter() + .map(|b| json!({ "name": b.name, "id": b.id })) + .collect::>(); + + Ok(Template::render( + "bakeries", + json!({ "bakeries": bakeries, "num_bakeries": bakeries.len() }), + )) +} + +#[get("/bakeries/")] +async fn bakery_by_id(db: &State, id: i32) -> Result { + let db = db as &DatabaseConnection; + + let bakery = Bakery::find_by_id(id).one(db).await.map_err(Into::into)?; + + Ok(if let Some(bakery) = bakery { + Template::render( + "bakery", + json!({ "id": bakery.id, "name": bakery.name, "profit_margin": bakery.profit_margin }), + ) + } else { + return Err(format!("No bakery with id {id} is found.").into()); + }) +} + +#[get("/new")] +fn new() -> Template { + Template::render("new", json!({})) +} + +// Use `GET` to support query parameters to simplify things +#[get("/bakeries?&")] +async fn new_bakery( + db: &State, + name: &str, + profit_margin: Option, +) -> Result { + let db = db as &DatabaseConnection; + + let profit_margin = profit_margin.unwrap_or_default(); + + let new_bakery = bakery::ActiveModel { + name: ActiveValue::Set(name.to_owned()), + profit_margin: ActiveValue::Set(profit_margin), + ..Default::default() + }; + + Bakery::insert(new_bakery) + .exec(db) + .await + .map_err(Into::into)?; + + Ok(Template::render( + "success", + json!({ "name": name, "profit_margin": profit_margin}), + )) +} + +#[post("/reset")] +async fn reset(db: &State) -> Result<(), ErrorResponder> { + Migrator::refresh(db).await.map_err(Into::into)?; + + Ok(()) +} + +#[launch] +async fn rocket() -> _ { + let db = match set_up_db().await { + Ok(db) => db, + Err(err) => panic!("{}", err), + }; + + rocket::build() + .manage(db) + .mount("/", FileServer::from(relative!("/static"))) + .mount( + "/", + routes![index, bakeries, bakery_by_id, new, new_bakery, reset], + ) + .register("/", catchers![not_found]) + .attach(Template::fairing()) +} + +#[catch(404)] +pub fn not_found(req: &Request<'_>) -> Template { + Template::render( + "error/404", + json! ({ + "uri": req.uri() + }), + ) +} + +#[derive(Responder)] +#[response(status = 500, content_type = "json")] +struct ErrorResponder { + message: String, +} + +#[allow(clippy::from_over_into)] +impl Into for DbErr { + fn into(self) -> ErrorResponder { + ErrorResponder { + message: self.to_string(), + } + } +} + +#[allow(clippy::from_over_into)] +impl Into for String { + fn into(self) -> ErrorResponder { + ErrorResponder { message: self } + } +} + +#[allow(clippy::from_over_into)] +impl Into for &str { + fn into(self) -> ErrorResponder { + self.to_owned().into() + } +} diff --git a/rocket-example/src/migrator/m20220602_000001_create_bakery_table.rs b/rocket-example/src/migrator/m20220602_000001_create_bakery_table.rs new file mode 100644 index 0000000..03ae011 --- /dev/null +++ b/rocket-example/src/migrator/m20220602_000001_create_bakery_table.rs @@ -0,0 +1,46 @@ +use sea_orm_migration::prelude::*; + +pub struct Migration; + +impl MigrationName for Migration { + fn name(&self) -> &str { + "m_20220602_000001_create_bakery_table" + } +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .if_not_exists() + .table(Bakery::Table) + .col( + ColumnDef::new(Bakery::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(Bakery::Name).string().not_null()) + .col(ColumnDef::new(Bakery::ProfitMargin).double().not_null()) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(Bakery::Table).to_owned()) + .await + } +} + +#[derive(Iden)] +pub enum Bakery { + Table, + Id, + Name, + ProfitMargin, +} diff --git a/rocket-example/src/migrator/m20220602_000002_create_baker_table.rs b/rocket-example/src/migrator/m20220602_000002_create_baker_table.rs new file mode 100644 index 0000000..91ee311 --- /dev/null +++ b/rocket-example/src/migrator/m20220602_000002_create_baker_table.rs @@ -0,0 +1,56 @@ +use sea_orm_migration::prelude::*; + +use super::m20220602_000001_create_bakery_table::Bakery; + +pub struct Migration; + +impl MigrationName for Migration { + fn name(&self) -> &str { + "m_20220602_000002_create_baker_table" + } +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .if_not_exists() + .table(Baker::Table) + .col( + ColumnDef::new(Baker::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(Baker::Name).string().not_null()) + .col(ColumnDef::new(Baker::ContactDetails).json()) + .col(ColumnDef::new(Baker::BakeryId).integer().not_null()) + .foreign_key( + ForeignKey::create() + .name("fk-baker-bakery_id") + .from(Baker::Table, Baker::BakeryId) + .to(Bakery::Table, Bakery::Id), + ) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(Baker::Table).to_owned()) + .await + } +} + +#[derive(Iden)] +pub enum Baker { + Table, + Id, + Name, + ContactDetails, + BakeryId, +} diff --git a/rocket-example/src/migrator/mod.rs b/rocket-example/src/migrator/mod.rs new file mode 100644 index 0000000..9cbc98b --- /dev/null +++ b/rocket-example/src/migrator/mod.rs @@ -0,0 +1,16 @@ +use sea_orm_migration::prelude::*; + +mod m20220602_000001_create_bakery_table; +mod m20220602_000002_create_baker_table; + +pub struct Migrator; + +#[async_trait::async_trait] +impl MigratorTrait for Migrator { + fn migrations() -> Vec> { + vec![ + Box::new(m20220602_000001_create_bakery_table::Migration), + Box::new(m20220602_000002_create_baker_table::Migration), + ] + } +} diff --git a/rocket-example/src/setup.rs b/rocket-example/src/setup.rs new file mode 100644 index 0000000..f5dfea2 --- /dev/null +++ b/rocket-example/src/setup.rs @@ -0,0 +1,39 @@ +use sea_orm::*; + +const DATABASE_URL: &str = "mysql://root:root@localhost:3306"; + +pub(super) async fn set_up_db() -> Result { + let db = Database::connect(DATABASE_URL).await?; + + let db_name = "bakeries_db"; + let db = match db.get_database_backend() { + DbBackend::MySql => { + db.execute(Statement::from_string( + db.get_database_backend(), + format!("CREATE DATABASE IF NOT EXISTS `{}`;", db_name), + )) + .await?; + + let url = format!("{}/{}", DATABASE_URL, db_name); + Database::connect(&url).await? + } + DbBackend::Postgres => { + db.execute(Statement::from_string( + db.get_database_backend(), + format!("DROP DATABASE IF EXISTS \"{}\";", db_name), + )) + .await?; + db.execute(Statement::from_string( + db.get_database_backend(), + format!("CREATE DATABASE \"{}\";", db_name), + )) + .await?; + + let url = format!("{}/{}", DATABASE_URL, db_name); + Database::connect(&url).await? + } + DbBackend::Sqlite => db, + }; + + Ok(db) +} diff --git a/rocket-example/static/css/normalize.css b/rocket-example/static/css/normalize.css new file mode 100644 index 0000000..458eea1 --- /dev/null +++ b/rocket-example/static/css/normalize.css @@ -0,0 +1,427 @@ +/*! normalize.css v3.0.2 | MIT License | git.io/normalize */ + +/** + * 1. Set default font family to sans-serif. + * 2. Prevent iOS text size adjust after orientation change, without disabling + * user zoom. + */ + +html { + font-family: sans-serif; /* 1 */ + -ms-text-size-adjust: 100%; /* 2 */ + -webkit-text-size-adjust: 100%; /* 2 */ +} + +/** + * Remove default margin. + */ + +body { + margin: 0; +} + +/* HTML5 display definitions + ========================================================================== */ + +/** + * Correct `block` display not defined for any HTML5 element in IE 8/9. + * Correct `block` display not defined for `details` or `summary` in IE 10/11 + * and Firefox. + * Correct `block` display not defined for `main` in IE 11. + */ + +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +main, +menu, +nav, +section, +summary { + display: block; +} + +/** + * 1. Correct `inline-block` display not defined in IE 8/9. + * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. + */ + +audio, +canvas, +progress, +video { + display: inline-block; /* 1 */ + vertical-align: baseline; /* 2 */ +} + +/** + * Prevent modern browsers from displaying `audio` without controls. + * Remove excess height in iOS 5 devices. + */ + +audio:not([controls]) { + display: none; + height: 0; +} + +/** + * Address `[hidden]` styling not present in IE 8/9/10. + * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. + */ + +[hidden], +template { + display: none; +} + +/* Links + ========================================================================== */ + +/** + * Remove the gray background color from active links in IE 10. + */ + +a { + background-color: transparent; +} + +/** + * Improve readability when focused and also mouse hovered in all browsers. + */ + +a:active, +a:hover { + outline: 0; +} + +/* Text-level semantics + ========================================================================== */ + +/** + * Address styling not present in IE 8/9/10/11, Safari, and Chrome. + */ + +abbr[title] { + border-bottom: 1px dotted; +} + +/** + * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. + */ + +b, +strong { + font-weight: bold; +} + +/** + * Address styling not present in Safari and Chrome. + */ + +dfn { + font-style: italic; +} + +/** + * Address variable `h1` font-size and margin within `section` and `article` + * contexts in Firefox 4+, Safari, and Chrome. + */ + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +/** + * Address styling not present in IE 8/9. + */ + +mark { + background: #ff0; + color: #000; +} + +/** + * Address inconsistent and variable font size in all browsers. + */ + +small { + font-size: 80%; +} + +/** + * Prevent `sub` and `sup` affecting `line-height` in all browsers. + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +/* Embedded content + ========================================================================== */ + +/** + * Remove border when inside `a` element in IE 8/9/10. + */ + +img { + border: 0; +} + +/** + * Correct overflow not hidden in IE 9/10/11. + */ + +svg:not(:root) { + overflow: hidden; +} + +/* Grouping content + ========================================================================== */ + +/** + * Address margin not present in IE 8/9 and Safari. + */ + +figure { + margin: 1em 40px; +} + +/** + * Address differences between Firefox and other browsers. + */ + +hr { + -moz-box-sizing: content-box; + box-sizing: content-box; + height: 0; +} + +/** + * Contain overflow in all browsers. + */ + +pre { + overflow: auto; +} + +/** + * Address odd `em`-unit font size rendering in all browsers. + */ + +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; +} + +/* Forms + ========================================================================== */ + +/** + * Known limitation: by default, Chrome and Safari on OS X allow very limited + * styling of `select`, unless a `border` property is set. + */ + +/** + * 1. Correct color not being inherited. + * Known issue: affects color of disabled elements. + * 2. Correct font properties not being inherited. + * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. + */ + +button, +input, +optgroup, +select, +textarea { + color: inherit; /* 1 */ + font: inherit; /* 2 */ + margin: 0; /* 3 */ +} + +/** + * Address `overflow` set to `hidden` in IE 8/9/10/11. + */ + +button { + overflow: visible; +} + +/** + * Address inconsistent `text-transform` inheritance for `button` and `select`. + * All other form control elements do not inherit `text-transform` values. + * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. + * Correct `select` style inheritance in Firefox. + */ + +button, +select { + text-transform: none; +} + +/** + * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` + * and `video` controls. + * 2. Correct inability to style clickable `input` types in iOS. + * 3. Improve usability and consistency of cursor style between image-type + * `input` and others. + */ + +button, +html input[type="button"], /* 1 */ +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; /* 2 */ + cursor: pointer; /* 3 */ +} + +/** + * Re-set default cursor for disabled elements. + */ + +button[disabled], +html input[disabled] { + cursor: default; +} + +/** + * Remove inner padding and border in Firefox 4+. + */ + +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; +} + +/** + * Address Firefox 4+ setting `line-height` on `input` using `!important` in + * the UA stylesheet. + */ + +input { + line-height: normal; +} + +/** + * It's recommended that you don't attempt to style these elements. + * Firefox's implementation doesn't respect box-sizing, padding, or width. + * + * 1. Address box sizing set to `content-box` in IE 8/9/10. + * 2. Remove excess padding in IE 8/9/10. + */ + +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ +} + +/** + * Fix the cursor style for Chrome's increment/decrement buttons. For certain + * `font-size` values of the `input`, it causes the cursor style of the + * decrement button to change from `default` to `text`. + */ + +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +/** + * 1. Address `appearance` set to `searchfield` in Safari and Chrome. + * 2. Address `box-sizing` set to `border-box` in Safari and Chrome + * (include `-moz` to future-proof). + */ + +input[type="search"] { + -webkit-appearance: textfield; /* 1 */ + -moz-box-sizing: content-box; + -webkit-box-sizing: content-box; /* 2 */ + box-sizing: content-box; +} + +/** + * Remove inner padding and search cancel button in Safari and Chrome on OS X. + * Safari (but not Chrome) clips the cancel button when the search input has + * padding (and `textfield` appearance). + */ + +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * Define consistent border, margin, and padding. + */ + +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} + +/** + * 1. Correct `color` not being inherited in IE 8/9/10/11. + * 2. Remove padding so people aren't caught out if they zero out fieldsets. + */ + +legend { + border: 0; /* 1 */ + padding: 0; /* 2 */ +} + +/** + * Remove default vertical scrollbar in IE 8/9/10/11. + */ + +textarea { + overflow: auto; +} + +/** + * Don't inherit the `font-weight` (applied by a rule above). + * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. + */ + +optgroup { + font-weight: bold; +} + +/* Tables + ========================================================================== */ + +/** + * Remove most spacing between table cells. + */ + +table { + border-collapse: collapse; + border-spacing: 0; +} + +td, +th { + padding: 0; +} diff --git a/rocket-example/static/css/skeleton.css b/rocket-example/static/css/skeleton.css new file mode 100644 index 0000000..cdc432a --- /dev/null +++ b/rocket-example/static/css/skeleton.css @@ -0,0 +1,421 @@ +/* +* Skeleton V2.0.4 +* Copyright 2014, Dave Gamache +* www.getskeleton.com +* Free to use under the MIT license. +* https://opensource.org/licenses/mit-license.php +* 12/29/2014 +*/ + + +/* Table of contents +–––––––––––––––––––––––––––––––––––––––––––––––––– +- Grid +- Base Styles +- Typography +- Links +- Buttons +- Forms +- Lists +- Code +- Tables +- Spacing +- Utilities +- Clearing +- Media Queries +*/ + + +/* Grid +–––––––––––––––––––––––––––––––––––––––––––––––––– */ +.container { + position: relative; + width: 100%; + max-width: 960px; + margin: 0 auto; + padding: 0 20px; + box-sizing: border-box; } +.column, +.columns { + width: 100%; + float: left; + box-sizing: border-box; } + +/* For devices larger than 400px */ +@media (min-width: 400px) { + .container { + width: 85%; + padding: 0; } +} + +/* For devices larger than 550px */ +@media (min-width: 550px) { + .container { + width: 80%; } + .column, + .columns { + margin-left: 4%; } + .column:first-child, + .columns:first-child { + margin-left: 0; } + + .one.column, + .one.columns { width: 4.66666666667%; } + .two.columns { width: 13.3333333333%; } + .three.columns { width: 22%; } + .four.columns { width: 30.6666666667%; } + .five.columns { width: 39.3333333333%; } + .six.columns { width: 48%; } + .seven.columns { width: 56.6666666667%; } + .eight.columns { width: 65.3333333333%; } + .nine.columns { width: 74.0%; } + .ten.columns { width: 82.6666666667%; } + .eleven.columns { width: 91.3333333333%; } + .twelve.columns { width: 100%; margin-left: 0; } + + .one-third.column { width: 30.6666666667%; } + .two-thirds.column { width: 65.3333333333%; } + + .one-half.column { width: 48%; } + + /* Offsets */ + .offset-by-one.column, + .offset-by-one.columns { margin-left: 8.66666666667%; } + .offset-by-two.column, + .offset-by-two.columns { margin-left: 17.3333333333%; } + .offset-by-three.column, + .offset-by-three.columns { margin-left: 26%; } + .offset-by-four.column, + .offset-by-four.columns { margin-left: 34.6666666667%; } + .offset-by-five.column, + .offset-by-five.columns { margin-left: 43.3333333333%; } + .offset-by-six.column, + .offset-by-six.columns { margin-left: 52%; } + .offset-by-seven.column, + .offset-by-seven.columns { margin-left: 60.6666666667%; } + .offset-by-eight.column, + .offset-by-eight.columns { margin-left: 69.3333333333%; } + .offset-by-nine.column, + .offset-by-nine.columns { margin-left: 78.0%; } + .offset-by-ten.column, + .offset-by-ten.columns { margin-left: 86.6666666667%; } + .offset-by-eleven.column, + .offset-by-eleven.columns { margin-left: 95.3333333333%; } + + .offset-by-one-third.column, + .offset-by-one-third.columns { margin-left: 34.6666666667%; } + .offset-by-two-thirds.column, + .offset-by-two-thirds.columns { margin-left: 69.3333333333%; } + + .offset-by-one-half.column, + .offset-by-one-half.columns { margin-left: 52%; } + +} + + +/* Base Styles +–––––––––––––––––––––––––––––––––––––––––––––––––– */ +/* NOTE +html is set to 62.5% so that all the REM measurements throughout Skeleton +are based on 10px sizing. So basically 1.5rem = 15px :) */ +html { + font-size: 62.5%; } +body { + font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */ + line-height: 1.6; + font-weight: 400; + font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif; + color: #222; } + + +/* Typography +–––––––––––––––––––––––––––––––––––––––––––––––––– */ +h1, h2, h3, h4, h5, h6 { + margin-top: 0; + margin-bottom: 2rem; + font-weight: 300; } +h1 { font-size: 4.0rem; line-height: 1.2; letter-spacing: -.1rem;} +h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; } +h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; } +h4 { font-size: 2.4rem; line-height: 1.35; letter-spacing: -.08rem; } +h5 { font-size: 1.8rem; line-height: 1.5; letter-spacing: -.05rem; } +h6 { font-size: 1.5rem; line-height: 1.6; letter-spacing: 0; } + +/* Larger than phablet */ +@media (min-width: 550px) { + h1 { font-size: 5.0rem; } + h2 { font-size: 4.2rem; } + h3 { font-size: 3.6rem; } + h4 { font-size: 3.0rem; } + h5 { font-size: 2.4rem; } + h6 { font-size: 1.5rem; } +} + +p { + margin-top: 0; } + + +/* Links +–––––––––––––––––––––––––––––––––––––––––––––––––– */ +a { + color: #1EAEDB; } +a:hover { + color: #0FA0CE; } + + +/* Buttons +–––––––––––––––––––––––––––––––––––––––––––––––––– */ +.button, +button, +input[type="submit"], +input[type="reset"], +input[type="button"] { + display: inline-block; + height: 38px; + padding: 0 30px; + color: #555; + text-align: center; + font-size: 11px; + font-weight: 600; + line-height: 38px; + letter-spacing: .1rem; + text-transform: uppercase; + text-decoration: none; + white-space: nowrap; + background-color: transparent; + border-radius: 4px; + border: 1px solid #bbb; + cursor: pointer; + box-sizing: border-box; } +.button:hover, +button:hover, +input[type="submit"]:hover, +input[type="reset"]:hover, +input[type="button"]:hover, +.button:focus, +button:focus, +input[type="submit"]:focus, +input[type="reset"]:focus, +input[type="button"]:focus { + color: #333; + border-color: #888; + outline: 0; } +.button.button-primary, +button.button-primary, +button.primary, +input[type="submit"].button-primary, +input[type="reset"].button-primary, +input[type="button"].button-primary { + color: #FFF; + background-color: #33C3F0; + border-color: #33C3F0; } +.button.button-primary:hover, +button.button-primary:hover, +button.primary:hover, +input[type="submit"].button-primary:hover, +input[type="reset"].button-primary:hover, +input[type="button"].button-primary:hover, +.button.button-primary:focus, +button.button-primary:focus, +button.primary:focus, +input[type="submit"].button-primary:focus, +input[type="reset"].button-primary:focus, +input[type="button"].button-primary:focus { + color: #FFF; + background-color: #1EAEDB; + border-color: #1EAEDB; } + + +/* Forms +–––––––––––––––––––––––––––––––––––––––––––––––––– */ +input[type="email"], +input[type="number"], +input[type="search"], +input[type="text"], +input[type="tel"], +input[type="url"], +input[type="password"], +textarea, +select { + height: 38px; + padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */ + background-color: #fff; + border: 1px solid #D1D1D1; + border-radius: 4px; + box-shadow: none; + box-sizing: border-box; } +/* Removes awkward default styles on some inputs for iOS */ +input[type="email"], +input[type="number"], +input[type="search"], +input[type="text"], +input[type="tel"], +input[type="url"], +input[type="password"], +textarea { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; } +textarea { + min-height: 65px; + padding-top: 6px; + padding-bottom: 6px; } +input[type="email"]:focus, +input[type="number"]:focus, +input[type="search"]:focus, +input[type="text"]:focus, +input[type="tel"]:focus, +input[type="url"]:focus, +input[type="password"]:focus, +textarea:focus, +select:focus { + border: 1px solid #33C3F0; + outline: 0; } +label, +legend { + display: block; + margin-bottom: .5rem; + font-weight: 600; } +fieldset { + padding: 0; + border-width: 0; } +input[type="checkbox"], +input[type="radio"] { + display: inline; } +label > .label-body { + display: inline-block; + margin-left: .5rem; + font-weight: normal; } + + +/* Lists +–––––––––––––––––––––––––––––––––––––––––––––––––– */ +ul { + list-style: circle inside; } +ol { + list-style: decimal inside; } +ol, ul { + padding-left: 0; + margin-top: 0; } +ul ul, +ul ol, +ol ol, +ol ul { + margin: 1.5rem 0 1.5rem 3rem; + font-size: 90%; } +li { + margin-bottom: 1rem; } + + +/* Code +–––––––––––––––––––––––––––––––––––––––––––––––––– */ +code { + padding: .2rem .5rem; + margin: 0 .2rem; + font-size: 90%; + white-space: nowrap; + background: #F1F1F1; + border: 1px solid #E1E1E1; + border-radius: 4px; } +pre > code { + display: block; + padding: 1rem 1.5rem; + white-space: pre; } + + +/* Tables +–––––––––––––––––––––––––––––––––––––––––––––––––– */ +th, +td { + padding: 12px 15px; + text-align: left; + border-bottom: 1px solid #E1E1E1; } +th:first-child, +td:first-child { + padding-left: 0; } +th:last-child, +td:last-child { + padding-right: 0; } + + +/* Spacing +–––––––––––––––––––––––––––––––––––––––––––––––––– */ +button, +.button { + margin-bottom: 1rem; } +input, +textarea, +select, +fieldset { + margin-bottom: 1.5rem; } +pre, +blockquote, +dl, +figure, +table, +p, +ul, +ol, +form { + margin-bottom: 2.5rem; } + + +/* Utilities +–––––––––––––––––––––––––––––––––––––––––––––––––– */ +.u-full-width { + width: 100%; + box-sizing: border-box; } +.u-max-full-width { + max-width: 100%; + box-sizing: border-box; } +.u-pull-right { + float: right; } +.u-pull-left { + float: left; } + + +/* Misc +–––––––––––––––––––––––––––––––––––––––––––––––––– */ +hr { + margin-top: 3rem; + margin-bottom: 3.5rem; + border-width: 0; + border-top: 1px solid #E1E1E1; } + + +/* Clearing +–––––––––––––––––––––––––––––––––––––––––––––––––– */ + +/* Self Clearing Goodness */ +.container:after, +.row:after, +.u-cf { + content: ""; + display: table; + clear: both; } + + +/* Media Queries +–––––––––––––––––––––––––––––––––––––––––––––––––– */ +/* +Note: The best way to structure the use of media queries is to create the queries +near the relevant code. For example, if you wanted to change the styles for buttons +on small devices, paste the mobile query code up in the buttons section and style it +there. +*/ + + +/* Larger than mobile */ +@media (min-width: 400px) {} + +/* Larger than phablet (also point when grid becomes active) */ +@media (min-width: 550px) {} + +/* Larger than tablet */ +@media (min-width: 750px) {} + +/* Larger than desktop */ +@media (min-width: 1000px) {} + +/* Larger than Desktop HD */ +@media (min-width: 1200px) {} diff --git a/rocket-example/static/css/style.css b/rocket-example/static/css/style.css new file mode 100644 index 0000000..ac2720d --- /dev/null +++ b/rocket-example/static/css/style.css @@ -0,0 +1,73 @@ +.field-error { + border: 1px solid #ff0000 !important; +} + +.field-error-flash { + color: #ff0000; + display: block; + margin: -10px 0 10px 0; +} + +.field-success { + border: 1px solid #5ab953 !important; +} + +.field-success-flash { + color: #5ab953; + display: block; + margin: -10px 0 10px 0; +} + +span.completed { + text-decoration: line-through; +} + +form.inline { + display: inline; +} + +form.link, +button.link { + display: inline; + color: #1eaedb; + border: none; + outline: none; + background: none; + cursor: pointer; + padding: 0; + margin: 0 0 0 0; + height: inherit; + text-decoration: underline; + font-size: inherit; + text-transform: none; + font-weight: normal; + line-height: inherit; + letter-spacing: inherit; +} + +form.link:hover, +button.link:hover { + color: #0fa0ce; +} + +button.small { + height: 20px; + padding: 0 10px; + font-size: 10px; + line-height: 20px; + margin: 0 2.5px; +} + +.post:hover { + background-color: #bce2ee; +} + +.post td { + padding: 5px; + width: 150px; +} + +#delete-button { + color: red; + border-color: red; +} diff --git a/rocket-example/static/images/favicon.png b/rocket-example/static/images/favicon.png new file mode 100644 index 0000000..02b7390 Binary files /dev/null and b/rocket-example/static/images/favicon.png differ diff --git a/rocket-example/templates/bakeries.html.tera b/rocket-example/templates/bakeries.html.tera new file mode 100644 index 0000000..af3929e --- /dev/null +++ b/rocket-example/templates/bakeries.html.tera @@ -0,0 +1,22 @@ +{% extends "base" %} {% block content %} +

All Bakeries

+ + + +
+ {% if num_bakeries == 0 %} No bakeries {% else %} + + {% endif %} +
+ +{% endblock content %} diff --git a/rocket-example/templates/bakery.html.tera b/rocket-example/templates/bakery.html.tera new file mode 100644 index 0000000..c113ee0 --- /dev/null +++ b/rocket-example/templates/bakery.html.tera @@ -0,0 +1,15 @@ +{% extends "base" %} {% block content %} +

{{ name }}

+ + + +
+

id: {{ id }}

+

profit margin: {{ profit_margin }}

+
+ +{% endblock content %} diff --git a/rocket-example/templates/base.html.tera b/rocket-example/templates/base.html.tera new file mode 100644 index 0000000..4e45823 --- /dev/null +++ b/rocket-example/templates/base.html.tera @@ -0,0 +1,26 @@ + + + + + Rocket SeaORM Example + + + + + + + + + + + +
+

+ {% block content %}{% endblock content %} +
+ + diff --git a/rocket-example/templates/error/404.html.tera b/rocket-example/templates/error/404.html.tera new file mode 100644 index 0000000..afda653 --- /dev/null +++ b/rocket-example/templates/error/404.html.tera @@ -0,0 +1,11 @@ + + + + + 404 - tera + + +

404: Hey! There's nothing here.

+ The page at {{ uri }} does not exist! + + diff --git a/rocket-example/templates/index.html.tera b/rocket-example/templates/index.html.tera new file mode 100644 index 0000000..d3a1d72 --- /dev/null +++ b/rocket-example/templates/index.html.tera @@ -0,0 +1,16 @@ +{% extends "base" %} {% block content %} +

Hello, BakeriesDB!

+ + + + + +{% endblock content %} diff --git a/rocket-example/templates/new.html.tera b/rocket-example/templates/new.html.tera new file mode 100644 index 0000000..a5d1af2 --- /dev/null +++ b/rocket-example/templates/new.html.tera @@ -0,0 +1,38 @@ +{% extends "base" %} {% block content %} +
+

New Bakery

+
+
+ + +
+
+
+ + + +
+
+
+ +
+
+
+
+{% endblock content %} diff --git a/rocket-example/templates/success.html.tera b/rocket-example/templates/success.html.tera new file mode 100644 index 0000000..234a0ca --- /dev/null +++ b/rocket-example/templates/success.html.tera @@ -0,0 +1,20 @@ +{% extends "base" %} {% block content %} +

{{ name }} is successfully registered!

+ +
+ Profit margin: {{ profit_margin }} +
+ + + + + +{% endblock content %} diff --git a/simple-crud/.env b/simple-crud/.env deleted file mode 100644 index 8666bfa..0000000 --- a/simple-crud/.env +++ /dev/null @@ -1 +0,0 @@ -DATABASE_URL=mysql://webmaster:master_char@localhost/fruit_markets diff --git a/simple-crud/.gitignore b/simple-crud/.gitignore deleted file mode 100644 index 680fe3a..0000000 --- a/simple-crud/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -/target -Cargo.lock -.vscode -postgresql_config.env diff --git a/simple-crud/Cargo.toml b/simple-crud/Cargo.toml deleted file mode 100644 index f5789ff..0000000 --- a/simple-crud/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[workspace] -# Open a separate workspace for each tutorial - -[package] -name = "simple-crud" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -anyhow = "1.0.52" -async-std = { version = "1.10.0", features = ["attributes"] } -chrono = "0.4.19" -sea-orm = { version = "0.5.0", features = [ - "runtime-async-std-rustls", - "sqlx-mysql", - "macros", - "with-chrono", -], default-features = false } diff --git a/simple-crud/src/fruits_table/fruits.rs b/simple-crud/src/fruits_table/fruits.rs deleted file mode 100644 index 412c98a..0000000 --- a/simple-crud/src/fruits_table/fruits.rs +++ /dev/null @@ -1,24 +0,0 @@ -use sea_orm::entity::prelude::*; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] -#[sea_orm(table_name = "fruits")] -pub struct Model { - #[sea_orm(primary_key)] - pub fruit_id: i32, - #[sea_orm(unique)] - pub name: String, - pub datetime_utc: DateTime, - pub unit_price: i32, - pub sku: String, -} - -#[derive(Copy, Clone, Debug, EnumIter)] -pub enum Relation {} - -impl RelationTrait for Relation { - fn def(&self) -> RelationDef { - panic!("No RelationDef") - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/simple-crud/src/fruits_table/mod.rs b/simple-crud/src/fruits_table/mod.rs deleted file mode 100644 index 8fb7ea3..0000000 --- a/simple-crud/src/fruits_table/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod prelude; - -pub mod fruits; diff --git a/simple-crud/src/fruits_table/prelude.rs b/simple-crud/src/fruits_table/prelude.rs deleted file mode 100644 index 869d33d..0000000 --- a/simple-crud/src/fruits_table/prelude.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub use super::fruits::{ - ActiveModel as FruitsActiveModel, Column as FruitsColumn, Entity as Fruits, - Model as FruitsModel, PrimaryKey as FruitsPrimaryKey, Relation as FruitsRelation, -}; diff --git a/simple-crud/src/main.rs b/simple-crud/src/main.rs deleted file mode 100644 index 8191305..0000000 --- a/simple-crud/src/main.rs +++ /dev/null @@ -1,148 +0,0 @@ -mod fruits_table; -use fruits_table::prelude::*; -mod suppliers_table; -use suppliers_table::prelude::*; - -// Import the needed modules for table creation -use sea_orm::{entity::Set, prelude::*, ConnectionTrait, Database, Schema}; -// Handle errors using the `https://crates.io/crates/anyhow` crate -use anyhow::Result; -use chrono::Utc; - -// Convert this main function into async function -#[async_std::main] -async fn main() -> Result<()> { - let env_database_url = include_str!("../.env").trim(); - let split_url: Vec<&str> = env_database_url.split("=").collect(); - let database_url = split_url[1]; - - let db = Database::connect(database_url).await?; - - let builder = db.get_database_backend(); - let schema = Schema::new(builder); - - let create_table_op = db - .execute(builder.build(&schema.create_table_from_entity(Fruits))) - .await; - println!( - "`CREATE TABLE fruits` {:?}", - match create_table_op { - Ok(_) => "Operation Successful".to_owned(), - Err(e) => format!("Unsuccessful - Error {:?}", e), - } - ); - - // Get current system time - let now = chrono::offset::Utc::now(); - - // Convert system time to `NaiveDateTime` since SeaORM `DateTime` expects this; - let naive_system_time = now.naive_utc(); - - let fruit_01 = FruitsActiveModel { - name: Set("Apple".to_owned()), - datetime_utc: Set(naive_system_time), - unit_price: Set(2), - sku: Set("FM2022AKB40".to_owned()), - ..Default::default() - }; - let fruit_insert_operation = Fruits::insert(fruit_01).exec(&db).await; - - println!("INSERTED ONE: {:?}", fruit_insert_operation?); - - let fruit_02 = FruitsActiveModel { - name: Set("Banana".to_owned()), - datetime_utc: Set(Utc::now().naive_utc()), - unit_price: Set(2), - sku: Set("FM2022AKB41".to_owned()), - ..Default::default() - }; - - let fruit_03 = FruitsActiveModel { - name: Set("Pineapple".to_owned()), - datetime_utc: Set(Utc::now().naive_utc()), - unit_price: Set(8), - sku: Set("FM2022AKB42".to_owned()), - ..Default::default() - }; - - let fruit_04 = FruitsActiveModel { - name: Set("Mango".to_owned()), - datetime_utc: Set(Utc::now().naive_utc()), - unit_price: Set(6), - sku: Set("FM2022AKB43".to_owned()), - ..Default::default() - }; - let fruit_insert_operation = Fruits::insert_many(vec![fruit_02, fruit_03, fruit_04]) - .exec(&db) - .await; - - println!("INSERTED ONE: {:?}", fruit_insert_operation?); - - let fruits_table_rows = Fruits::find().all(&db).await; - println!("{:?}", fruits_table_rows?); - - let fruits_by_id = Fruits::find_by_id(2).one(&db).await; - println!("{:?}", fruits_by_id?); - - let find_pineapple = Fruits::find() - .filter(FruitsColumn::Name.contains("pineapple")) - .one(&db) - .await?; - println!("{:?}", find_pineapple.as_ref()); - - // Update the `pineapple` column with a new unit price - if let Some(pineapple_model) = find_pineapple { - let mut pineapple_active_model: FruitsActiveModel = pineapple_model.into(); - pineapple_active_model.unit_price = Set(10); - - let updated_pineapple_model: FruitsModel = pineapple_active_model.update(&db).await?; - - println!("UPDATED PRICE: {:?}", updated_pineapple_model.clone()); - } else { - println!("`Pineapple` column not found"); - } - - // Delete the `mango` column - - let find_mango = Fruits::find() - .filter(FruitsColumn::Name.contains("mango")) - .one(&db) - .await; - - if let Some(mango_model) = find_mango? { - println!("DELETED MANGO: {:?}", mango_model.delete(&db).await?); - } else { - println!("`Mango` column not found"); - } - - let supplier_01 = SuppliersActiveModel { - supplier_name: Set("John Doe".to_owned()), - fruit_id: Set(1_i32), - ..Default::default() - }; - - let supplier_02 = SuppliersActiveModel { - supplier_name: Set("Jane Doe".to_owned()), - fruit_id: Set(2_i32), - ..Default::default() - }; - - let supplier_03 = SuppliersActiveModel { - supplier_name: Set("Junior Doe".to_owned()), - fruit_id: Set(3_i32), - ..Default::default() - }; - - let supplier_insert_operation = - Suppliers::insert_many(vec![supplier_01, supplier_02, supplier_03]) - .exec(&db) - .await; - - println!("INSERTED MANY: {:?}", supplier_insert_operation?); - - let who_supplies = Suppliers::find().find_with_related(Fruits).all(&db).await?; - - dbg!(&who_supplies); - - Ok(()) -} diff --git a/simple-crud/src/suppliers_table/mod.rs b/simple-crud/src/suppliers_table/mod.rs deleted file mode 100644 index 6b27d64..0000000 --- a/simple-crud/src/suppliers_table/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! SeaORM Entity. Generated by sea-orm-codegen 0.5.0 - -pub mod prelude; - -pub mod suppliers; diff --git a/simple-crud/src/suppliers_table/prelude.rs b/simple-crud/src/suppliers_table/prelude.rs deleted file mode 100644 index 393cae7..0000000 --- a/simple-crud/src/suppliers_table/prelude.rs +++ /dev/null @@ -1,6 +0,0 @@ -//! SeaORM Entity. Generated by sea-orm-codegen 0.5.0 - -pub use super::suppliers::{ - ActiveModel as SuppliersActiveModel, Column as SuppliersColumn, Entity as Suppliers, - Model as SuppliersModel, PrimaryKey as SuppliersPrimaryKey, Relation as SuppliersRelation, -}; diff --git a/simple-crud/src/suppliers_table/suppliers.rs b/simple-crud/src/suppliers_table/suppliers.rs deleted file mode 100644 index c1d8873..0000000 --- a/simple-crud/src/suppliers_table/suppliers.rs +++ /dev/null @@ -1,33 +0,0 @@ -//! SeaORM Entity. Generated by sea-orm-codegen 0.5.0 - -use sea_orm::entity::prelude::*; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] -#[sea_orm(table_name = "suppliers")] -pub struct Model { - #[sea_orm(primary_key)] - pub supplier_id: i32, - #[sea_orm(unique)] - pub supplier_name: String, - pub fruit_id: i32, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "crate::Fruits", - from = "Column::FruitId", - to = "crate::FruitsColumn::FruitId", - on_update = "Cascade", - on_delete = "Cascade" - )] - Fruits, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Fruits.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/todo-app/.gitignore b/todo-app/.gitignore deleted file mode 100644 index f5f449c..0000000 --- a/todo-app/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -/target -Cargo.lock -.vscode -.env diff --git a/todo-app/Cargo.toml b/todo-app/Cargo.toml deleted file mode 100644 index 8284b27..0000000 --- a/todo-app/Cargo.toml +++ /dev/null @@ -1,5 +0,0 @@ -[workspace] -members = [ - "server", - "client", -] diff --git a/todo-app/client/.env b/todo-app/client/.env deleted file mode 100644 index e9c6ab8..0000000 --- a/todo-app/client/.env +++ /dev/null @@ -1 +0,0 @@ -DATABASE_URL=sqlite://my_todos.db \ No newline at end of file diff --git a/todo-app/client/.gitignore b/todo-app/client/.gitignore deleted file mode 100644 index 3549fae..0000000 --- a/todo-app/client/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -/target -Cargo.lock -.env \ No newline at end of file diff --git a/todo-app/client/Cargo.toml b/todo-app/client/Cargo.toml deleted file mode 100644 index 1175ff5..0000000 --- a/todo-app/client/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "client" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -anyhow = "1.0.55" -dotenv = "0.15.0" -json = "0.12.4" -minreq = "2.6.0" -sea-orm = { version = "0.6.0", features = ["runtime-tokio-rustls", "sqlx-sqlite", "macros"], default-features = false } -serde = { version = "1.0.136", features = ["derive"] } -serde_json = "1.0.79" -tokio = { version = "1.17.0", features = ["full"] } diff --git a/todo-app/client/src/db_ops.rs b/todo-app/client/src/db_ops.rs deleted file mode 100644 index a9c95c4..0000000 --- a/todo-app/client/src/db_ops.rs +++ /dev/null @@ -1,166 +0,0 @@ -use crate::{synching_to_server, MemDB, MyTodos, MyTodosActiveModel, MyTodosModel}; -use sea_orm::{ - sea_query::{Alias, ColumnDef, Table}, - ActiveModelTrait, ConnectionTrait, DatabaseConnection, EntityTrait, Set, -}; -use serde::{Deserialize, Serialize}; - -// The structure for a TodoList -#[derive(Debug, Serialize, Default, Deserialize)] -pub struct TodoList { - pub queued: Vec, - pub completed: Vec, -} - -pub async fn create_todo_table(db: &DatabaseConnection) -> anyhow::Result<()> { - let database_backend = db.get_database_backend(); - // Create the `todos` table - let todos_table = Table::create() - .table(Alias::new("todo_list")) - .if_not_exists() - .col( - ColumnDef::new(Alias::new("todo_id")) - .integer() - .primary_key() - .not_null() - .auto_increment(), - ) - .col( - ColumnDef::new(Alias::new("todo_name")) - .string() - .unique_key() - .not_null(), - ) - .col(ColumnDef::new(Alias::new("quantity")).string().not_null()) - .col(ColumnDef::new(Alias::new("status")).boolean().not_null()) - .to_owned(); - - // Executing the SQL query to create the `todos` table in SQLite - let create_table_op = db.execute(database_backend.build(&todos_table)).await; - // Print the result in a user friendly way - println!( - "`CREATE TABLE todo_list` {:?}", - match create_table_op { - Ok(_) => "Operation Successful".to_owned(), - Err(e) => format!("Unsuccessful - Error {:?}", e), - } - ); - - Ok(()) -} - -pub async fn get_fruits() -> anyhow::Result> { - let response = minreq::get("http://127.0.0.1:8080/fruits").send()?; - - let fruits_list: Vec = serde_json::from_str(&response.as_str()?)?; - - Ok(fruits_list) -} - -pub async fn store(db: &DatabaseConnection, quantity: &str, todo_name: &str) -> anyhow::Result<()> { - let my_todo = MyTodosActiveModel { - todo_name: Set(todo_name.to_owned()), - quantity: Set(quantity.to_owned()), - status: Set(0), - ..Default::default() - }; - - MyTodos::insert(my_todo).exec(db).await?; - - Ok(()) -} - -pub async fn get(db: &DatabaseConnection) -> Result, sea_orm::DbErr> { - MyTodos::find().all(db).await -} - -pub async fn edit( - db: &DatabaseConnection, - todo_model: &MyTodosModel, - quantity: String, -) -> Result { - let mut todos_active_model: MyTodosActiveModel = todo_model.to_owned().into(); - todos_active_model.quantity = Set(quantity); - - Ok(todos_active_model.update(db).await?) -} - -pub async fn done( - db: &DatabaseConnection, - todo_model: &MyTodosModel, -) -> Result { - let mut todos_active_model: MyTodosActiveModel = todo_model.to_owned().into(); - todos_active_model.status = Set(1); - - Ok(todos_active_model.update(db).await?) -} - -pub async fn undo( - db: &DatabaseConnection, - todo_model: &MyTodosModel, -) -> Result { - let mut todos_active_model: MyTodosActiveModel = todo_model.to_owned().into(); - todos_active_model.status = Set(0); - - Ok(todos_active_model.update(db).await?) -} - -pub(crate) async fn load_sqlite_cache( - db: &DatabaseConnection, - memdb: &mut MemDB, -) -> Result<(), sea_orm::DbErr> { - let sqlite_cache = get(&db).await?; - memdb.lock().await.clear(); - for mytodo_model in sqlite_cache { - memdb - .lock() - .await - .insert(mytodo_model.todo_name.clone(), mytodo_model); - } - - Ok(()) -} - -pub async fn update_remote_storage(memdb: &MemDB, username: &str) -> anyhow::Result<()> { - let mut temp_list = TodoList::default(); - memdb.lock().await.values().for_each(|todo| { - if todo.status == 0 { - temp_list.queued.push(todo.to_owned()); - } else { - temp_list.completed.push(todo.to_owned()); - } - }); - - let todo_list = serde_json::to_string(&temp_list)?; - - synching_to_server(); - - let response = minreq::post("http://127.0.0.1:8080/update_todo") - .with_header("Content-Type", "application/json") - .with_body( - json::object! { - username: username, - todo_list: todo_list.clone(), - } - .dump(), - ) - .send()?; - - if response.status_code == 500 { - let body = serde_json::from_str::>(&response.as_str()?)?; - if body == Some("MODEL_NOT_FOUND".to_owned()) { - minreq::post("http://127.0.0.1:8080/store") - .with_header("Content-Type", "application/json") - .with_body( - json::object! { - username: username, - todo_list: todo_list, - } - .dump(), - ) - .send()?; - } - } - - Ok(()) -} diff --git a/todo-app/client/src/handler.rs b/todo-app/client/src/handler.rs deleted file mode 100644 index d839604..0000000 --- a/todo-app/client/src/handler.rs +++ /dev/null @@ -1,102 +0,0 @@ -use crate::{ - convert_case, done, edit, get_fruits, load_sqlite_cache, loading, read_line, split_words, - store, synching, undo, update_remote_storage, MemDB, -}; -use sea_orm::DatabaseConnection; -use std::collections::HashMap; -use std::io; - -pub async fn input_handler(db: &DatabaseConnection) -> anyhow::Result<()> { - let mut username_buffer = String::default(); - println!("What is Your Username...",); - let stdin = io::stdin(); // We get `Stdin` here. - stdin.read_line(&mut username_buffer)?; - let username = username_buffer.trim().to_string(); - - let fruits_list: Vec = get_fruits().await?; - - let mut buffer = String::new(); - let mut text_buffer: String; - let mut memdb = MemDB::new(HashMap::default()); - loading(); - load_sqlite_cache(db, &mut memdb).await?; - - loop { - read_line(&mut buffer, fruits_list.as_ref(), &memdb).await?; - buffer = buffer.trim().to_owned(); - let words = split_words(buffer.clone()); - let command = words[0].to_lowercase().to_string(); - let mut quantity: &str = ""; - if command.as_str() == "done" || command.as_str() == "undo" { - text_buffer = convert_case(&words[1]); - } else if command.as_str() == "exit" { - update_remote_storage(&memdb, &username).await?; - println!("SYNCED SUCCESSFULLY."); - println!("Bye! :)"); - break; - } else { - quantity = &words[1]; - text_buffer = convert_case(&words[2]); - } - - if !text_buffer.is_empty() { - match fruits_list.iter().find(|&fruit| *fruit == text_buffer) { - None => { - if !text_buffer.is_empty() { - println!("The fruit `{buffer}` is not available.\n",); - } - continue; - } - Some(_) => { - if command.as_str() == "add" { - if memdb.lock().await.contains_key(&text_buffer) { - continue; - //TODO - } else { - synching(); - store(&db, quantity, &text_buffer).await?; - load_sqlite_cache(&db, &mut memdb).await?; - } - } else if command.as_str() == "edit" { - if let Some(mut todo_model) = memdb.lock().await.get_mut(&text_buffer) { - if todo_model.status != 1 { - synching(); - edit(&db, todo_model, quantity.to_owned()).await?; - todo_model.quantity = quantity.to_owned(); - } - } else { - continue; - } - } else if command.as_str() == "done" { - if let Some(todo_model) = memdb.lock().await.get_mut(&text_buffer) { - if todo_model.status == 0 { - synching(); - let updated_model = done(&db, todo_model).await?; - *todo_model = updated_model; - } - continue; - } else { - continue; - } - } else if command.as_str() == "undo" { - if let Some(todo_model) = memdb.lock().await.get_mut(&text_buffer) { - if todo_model.status == 1 { - synching(); - let updated_model = undo(&db, todo_model).await?; - *todo_model = updated_model; - } - continue; - } else { - continue; - } - } else { - dbg!("Unsupported Command"); - break; - } - } - } - } - } - - Ok(()) -} diff --git a/todo-app/client/src/main.rs b/todo-app/client/src/main.rs deleted file mode 100644 index 144e109..0000000 --- a/todo-app/client/src/main.rs +++ /dev/null @@ -1,29 +0,0 @@ -mod db_ops; -mod handler; -mod todo_list_table; -mod user_input; -mod utils; - -pub use db_ops::*; -pub use handler::*; -pub use todo_list_table::prelude::*; -pub use user_input::*; -pub use utils::*; - -use dotenv::dotenv; -use sea_orm::Database; - -#[tokio::main] -async fn main() -> anyhow::Result<()> { - dotenv().ok(); - - // Read the database environment from the `.env` file - let database_url = dotenv::var("DATABASE_URL")?; - let db = Database::connect(database_url).await?; - - create_todo_table(&db).await?; - - input_handler(&db).await?; - - Ok(()) -} diff --git a/todo-app/client/src/todo_list_table/mod.rs b/todo-app/client/src/todo_list_table/mod.rs deleted file mode 100644 index 4f09c4a..0000000 --- a/todo-app/client/src/todo_list_table/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! SeaORM Entity. Generated by sea-orm-codegen 0.6.0 - -pub mod prelude; - -pub mod todo_list; diff --git a/todo-app/client/src/todo_list_table/prelude.rs b/todo-app/client/src/todo_list_table/prelude.rs deleted file mode 100644 index 398fa2f..0000000 --- a/todo-app/client/src/todo_list_table/prelude.rs +++ /dev/null @@ -1,6 +0,0 @@ -//! SeaORM Entity. Generated by sea-orm-codegen 0.6.0 - -pub use super::todo_list::{ - ActiveModel as MyTodosActiveModel, Column as MyTodosColumn, Entity as MyTodos, - Model as MyTodosModel, PrimaryKey as MyTodosPrimaryKey, Relation as MyTodosRelation, -}; diff --git a/todo-app/client/src/todo_list_table/todo_list.rs b/todo-app/client/src/todo_list_table/todo_list.rs deleted file mode 100644 index b39f79e..0000000 --- a/todo-app/client/src/todo_list_table/todo_list.rs +++ /dev/null @@ -1,25 +0,0 @@ -//! SeaORM Entity. Generated by sea-orm-codegen 0.6.0 - -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "todo_list")] -pub struct Model { - #[sea_orm(primary_key)] - pub todo_id: i32, - pub todo_name: String, - pub quantity: String, - pub status: i32, -} - -#[derive(Copy, Clone, Debug, EnumIter)] -pub enum Relation {} - -impl RelationTrait for Relation { - fn def(&self) -> RelationDef { - panic!("No RelationDef") - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/todo-app/client/src/user_input.rs b/todo-app/client/src/user_input.rs deleted file mode 100644 index 1d419ff..0000000 --- a/todo-app/client/src/user_input.rs +++ /dev/null @@ -1,40 +0,0 @@ -use crate::{ - format_todos, MemDB, ADD_COMMAND, DONE_COMMAND, EDIT_COMMAND, EXIT_COMMAND, NUMBER, TITLE, - UNDO_COMMAND, -}; -use std::io; - -pub async fn read_line( - buffer: &mut String, - fruits_list: &Vec, - memdb: &MemDB, - //todo_list: &Vec, -) -> anyhow::Result { - crate::clear_terminal(); - buffer.clear(); - println!("+--------------------------+"); - println!("+ {:^5}{:17}+", "COMMANDS", " "); - println!("+{:26}+", " "); - println!("→ {ADD_COMMAND:5}{:18}+", " "); - println!("→ {DONE_COMMAND:23}+"); - println!("→ {UNDO_COMMAND:23}+"); - println!("→ {EDIT_COMMAND:23}+"); - println!("→ {EXIT_COMMAND:23}+"); - println!("+{:26}+", " "); - println!("+--------------------------+"); - - println!("{NUMBER}| {TITLE:10}"); - println!("----------------"); - for (mut index, item) in fruits_list.iter().enumerate() { - index += 1; - println!("{index:2} | {item:10}"); - } - println!("--------------------------------------------"); - format_todos(&memdb).await; - - println!("Enter a fruit that is available.",); - let stdin = io::stdin(); // We get `Stdin` here. - stdin.read_line(buffer)?; - - Ok(buffer.to_owned()) -} diff --git a/todo-app/client/src/utils.rs b/todo-app/client/src/utils.rs deleted file mode 100644 index 8427e3c..0000000 --- a/todo-app/client/src/utils.rs +++ /dev/null @@ -1,95 +0,0 @@ -use crate::MyTodosModel; -use std::collections::HashMap; -use tokio::sync::Mutex; - -pub(crate) const TITLE: &str = "FRUITS AVAILABLE"; -pub(crate) const NUMBER: &str = "No."; -pub(crate) const ADD_COMMAND: &str = "ADD"; -pub(crate) const DONE_COMMAND: &str = "DONE"; -pub(crate) const UNDO_COMMAND: &str = "UNDO"; -pub(crate) const EDIT_COMMAND: &str = "EDIT"; -pub(crate) const EXIT_COMMAND: &str = "EXIT"; - -const DONE: &str = "DONE TODOS"; -const NOT_DONE: &str = "NOT DONE"; -const QUANTITY: &str = "QUANTITY"; - -pub(crate) type MemDB = Mutex>; - -pub fn clear_terminal() { - print!("\x1B[2J\x1B[1;1H"); -} - -pub fn synching() { - clear_terminal(); - println!("SYNCING TO DATABASE..."); -} -pub fn synching_to_server() { - println!("SYNCING TO SERVER..."); -} - -pub fn loading() { - clear_terminal(); - println!("LOADING FROM DATABASE..."); -} - -pub fn convert_case(word: &str) -> String { - let word = word.to_lowercase(); - let mut chars = word - .chars() - .map(|character| character.to_string()) - .collect::>(); - - chars[0] = chars[0].to_uppercase().to_string(); - - chars.into_iter().collect::() -} - -pub fn split_words(user_input: String) -> Vec { - user_input - .split(" ") - .map(|word| word.to_owned()) - .collect::>() -} - -pub async fn format_todos(todo_models: &MemDB) { - println!("\n\n\n"); - if todo_models.lock().await.is_empty() { - println!("Oh My! There are no TODOs"); - } else { - let mut done = Vec::::default(); - let mut not_done = Vec::::default(); - - todo_models.lock().await.iter().for_each(|todo| { - if todo.1.status == 0 { - not_done.push(todo.1.to_owned()); - } else { - done.push(todo.1.to_owned()); - } - }); - - if not_done.is_empty() { - println!("Wohooo! All TODOs are Completed.") - } else { - println!("{QUANTITY:9}| {NOT_DONE:10}"); - println!("----------------"); - not_done.iter().for_each(|todo| { - println!("{:>8} | {:10}", todo.quantity, todo.todo_name); - }); - println!("----------------\n"); - } - - if done.is_empty() { - println!("----------------"); - println!("Bummer :( You Have Not Completed Any TODOs!"); - println!("----------------\n\n"); - } else { - println!("{QUANTITY:9}| {DONE:10}"); - println!("----------------"); - done.iter().for_each(|todo| { - println!("{:>8} | {:10}", todo.quantity, todo.todo_name); - }); - println!("----------------\n"); - } - } -} diff --git a/todo-app/server/.env b/todo-app/server/.env deleted file mode 100644 index 128bd88..0000000 --- a/todo-app/server/.env +++ /dev/null @@ -1 +0,0 @@ -DATABASE_URL=postgres://webmaster:master_char@localhost/fruits_market \ No newline at end of file diff --git a/todo-app/server/.gitignore b/todo-app/server/.gitignore deleted file mode 100644 index ea8c4bf..0000000 --- a/todo-app/server/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target diff --git a/todo-app/server/Cargo.toml b/todo-app/server/Cargo.toml deleted file mode 100644 index 0902202..0000000 --- a/todo-app/server/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "server" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -anyhow = "1.0.55" -axum = "0.4.8" -dotenv = "0.15.0" -once_cell = "1.10.0" -sea-orm = { version = "0.6.0", features = ["runtime-tokio-rustls", "sqlx-postgres", "macros"], default-features = false } -serde = { version = "1.0.136", features = ["derive"] } -tokio = { version = "1.17.0", features = ["full"] } diff --git a/todo-app/server/src/fruits_list_table/fruits.rs b/todo-app/server/src/fruits_list_table/fruits.rs deleted file mode 100644 index 3776883..0000000 --- a/todo-app/server/src/fruits_list_table/fruits.rs +++ /dev/null @@ -1,23 +0,0 @@ -//! SeaORM Entity. Generated by sea-orm-codegen 0.6.0 - -use sea_orm::entity::prelude::*; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] -#[sea_orm(table_name = "fruits")] -pub struct Model { - #[sea_orm(primary_key)] - pub fruit_id: i32, - #[sea_orm(unique)] - pub fruit_name: String, -} - -#[derive(Copy, Clone, Debug, EnumIter)] -pub enum Relation {} - -impl RelationTrait for Relation { - fn def(&self) -> RelationDef { - panic!("No RelationDef") - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/todo-app/server/src/fruits_list_table/mod.rs b/todo-app/server/src/fruits_list_table/mod.rs deleted file mode 100644 index dc15169..0000000 --- a/todo-app/server/src/fruits_list_table/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! SeaORM Entity. Generated by sea-orm-codegen 0.6.0 - -pub mod prelude; - -pub mod fruits; diff --git a/todo-app/server/src/fruits_list_table/prelude.rs b/todo-app/server/src/fruits_list_table/prelude.rs deleted file mode 100644 index 69e8e7e..0000000 --- a/todo-app/server/src/fruits_list_table/prelude.rs +++ /dev/null @@ -1,6 +0,0 @@ -//! SeaORM Entity. Generated by sea-orm-codegen 0.6.0 - -pub use super::fruits::{ - ActiveModel as FruitsActiveModel, Column as FruitsColumn, Entity as Fruits, - Model as FruitsModel, PrimaryKey as FruitsPrimaryKey, Relation as FruitsRelation, -}; diff --git a/todo-app/server/src/insert_values.rs b/todo-app/server/src/insert_values.rs deleted file mode 100644 index e65f430..0000000 --- a/todo-app/server/src/insert_values.rs +++ /dev/null @@ -1,33 +0,0 @@ -use crate::{Fruits, FruitsActiveModel}; -use sea_orm::{DatabaseConnection, EntityTrait, Set}; - -// Insert suppliers in the `suppliers` table -pub async fn insert_fruits(db: &DatabaseConnection) -> anyhow::Result<()> { - let apple = FruitsActiveModel { - fruit_name: Set("Apple".to_owned()), - ..Default::default() - }; - - let orange = FruitsActiveModel { - fruit_name: Set("Orange".to_owned()), - ..Default::default() - }; - - let mango = FruitsActiveModel { - fruit_name: Set("Mango".to_owned()), - ..Default::default() - }; - - let pineapple = FruitsActiveModel { - fruit_name: Set("Pineapple".to_owned()), - ..Default::default() - }; - - let fruit_insert_operation = Fruits::insert_many(vec![apple, orange, mango, pineapple]) - .exec(db) - .await; - - println!("INSERTED FRUITS: {:?}", fruit_insert_operation?); - - Ok(()) -} diff --git a/todo-app/server/src/main.rs b/todo-app/server/src/main.rs deleted file mode 100644 index 29a0c55..0000000 --- a/todo-app/server/src/main.rs +++ /dev/null @@ -1,115 +0,0 @@ -use axum::{ - routing::{get, post}, - Router, -}; -use dotenv::dotenv; -use once_cell::sync::OnceCell; -use sea_orm::{ - sea_query::{Alias, ColumnDef, Table}, - ConnectionTrait, Database, DatabaseConnection, DbBackend, -}; -use std::net::SocketAddr; - -mod fruits_list_table; -mod insert_values; -mod routing; -mod todo_list_table; - -pub use fruits_list_table::prelude::*; -pub use insert_values::*; -pub use routing::*; -pub use todo_list_table::prelude::*; - -static DATABASE_CONNECTION: OnceCell = OnceCell::new(); - -#[tokio::main] -async fn main() -> anyhow::Result<()> { - //Define the database backend - let db_postgres = DbBackend::Postgres; - - dotenv().ok(); - - // Read the database environment from the `.env` file - let database_url = dotenv::var("DATABASE_URL")?; - let db = Database::connect(database_url).await?; - DATABASE_CONNECTION.set(db).unwrap(); - - // Create the fruits table - let fruits_table = Table::create() - .table(Alias::new("fruits")) - .if_not_exists() - .col( - ColumnDef::new(Alias::new("fruit_id")) - .integer() - .auto_increment() - .primary_key() - .not_null(), - ) - .col( - ColumnDef::new(Alias::new("fruit_name")) - .string() - .unique_key() - .not_null(), - ) - .to_owned(); - - let db = DATABASE_CONNECTION.get().unwrap(); - - // Executing the SQL query to create the `fruits_table` in PostgreSQL - let create_table_op = db.execute(db_postgres.build(&fruits_table)).await; - // Print the result in a user friendly way - println!( - "`CREATE TABLE fruits` {:?}", - match create_table_op { - Ok(_) => "Operation Successful".to_owned(), - Err(e) => format!("Unsuccessful - Error {:?}", e), - } - ); - - // Create the `todos` table - let todos_table = Table::create() - .table(Alias::new("todos")) - .if_not_exists() - .col( - ColumnDef::new(Alias::new("todo_id")) - .integer() - .auto_increment() - .primary_key() - .not_null(), - ) - .col( - ColumnDef::new(Alias::new("username")) - .string() - .unique_key() - .not_null(), - ) - .col(ColumnDef::new(Alias::new("todo_list")).string()) - .to_owned(); - - // Executing the SQL query to create the `todos` table in PostgreSQL - let create_table_op = db.execute(db_postgres.build(&todos_table)).await; - // Print the result in a user friendly way - println!( - "`CREATE TABLE todos` {:?}", - match create_table_op { - Ok(_) => "Operation Successful".to_owned(), - Err(e) => format!("Unsuccessful - Error {:?}", e), - } - ); - - insert_fruits(&db).await?; - - let app = Router::new() - .route("/", get(root)) - .route("/fruits", get(get_fruits)) - .route("/store", post(store_todo)) - .route("/update_todo", post(update_todo)); - - let addr = SocketAddr::from(([127, 0, 0, 1], 8080)); - println!("listening on http://{}", addr); - axum::Server::bind(&addr) - .serve(app.into_make_service()) - .await?; - - Ok(()) -} diff --git a/todo-app/server/src/routing.rs b/todo-app/server/src/routing.rs deleted file mode 100644 index b1e2f57..0000000 --- a/todo-app/server/src/routing.rs +++ /dev/null @@ -1,76 +0,0 @@ -use crate::{Fruits, Todos, TodosActiveModel, TodosColumn, DATABASE_CONNECTION}; -use axum::{http::StatusCode, response::IntoResponse, Json}; -use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set}; -use serde::Deserialize; - -#[derive(Deserialize, Debug)] -pub struct Store { - username: String, - todo_list: String, -} - -pub async fn root() -> &'static str { - "Remote PostgreSQL Server Online!" -} - -pub async fn get_fruits() -> impl IntoResponse { - let db = DATABASE_CONNECTION.get().unwrap(); - - match Fruits::find().all(db).await { - Ok(fruit_models) => { - let fruits = fruit_models - .iter() - .map(|fruit_model| fruit_model.fruit_name.clone()) - .collect::>(); - - (StatusCode::ACCEPTED, Json(fruits)) - } - Err(error) => ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(vec![error.to_string()]), - ), - } -} - -pub async fn store_todo(Json(payload): Json) -> impl IntoResponse { - let db = DATABASE_CONNECTION.get().unwrap(); - - let todo_user = TodosActiveModel { - username: Set(payload.username.to_owned()), - todo_list: Set(Some(payload.todo_list.to_owned())), - ..Default::default() - }; - - match Todos::insert(todo_user).exec(db).await { - Ok(_) => (StatusCode::ACCEPTED, Json("INSERTED".to_owned())), - Err(error) => (StatusCode::INTERNAL_SERVER_ERROR, Json(error.to_string())), - } -} - -pub async fn update_todo(Json(payload): Json) -> impl IntoResponse { - let db = DATABASE_CONNECTION.get().unwrap(); - - match Todos::find() - .filter(TodosColumn::Username.contains(&payload.username)) - .one(db) - .await - { - Ok(found_model) => { - if let Some(model) = found_model { - let mut todo_model: TodosActiveModel = model.into(); - todo_model.todo_list = Set(Some(payload.todo_list.to_owned())); - match todo_model.update(db).await { - Ok(_) => (StatusCode::NO_CONTENT, Json("UPDATED_TODO".to_owned())), - Err(error) => (StatusCode::INTERNAL_SERVER_ERROR, Json(error.to_string())), - } - } else { - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json("MODEL_NOT_FOUND".to_owned()), - ) - } - } - - Err(error) => (StatusCode::INTERNAL_SERVER_ERROR, Json(error.to_string())), - } -} diff --git a/todo-app/server/src/todo_list_table/mod.rs b/todo-app/server/src/todo_list_table/mod.rs deleted file mode 100644 index 77b9eed..0000000 --- a/todo-app/server/src/todo_list_table/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! SeaORM Entity. Generated by sea-orm-codegen 0.6.0 - -pub mod prelude; - -pub mod todos; diff --git a/todo-app/server/src/todo_list_table/prelude.rs b/todo-app/server/src/todo_list_table/prelude.rs deleted file mode 100644 index 5bf2688..0000000 --- a/todo-app/server/src/todo_list_table/prelude.rs +++ /dev/null @@ -1,6 +0,0 @@ -//! SeaORM Entity. Generated by sea-orm-codegen 0.6.0 - -pub use super::todos::{ - ActiveModel as TodosActiveModel, Column as TodosColumn, Entity as Todos, Model as TodosModel, - PrimaryKey as TodosPrimaryKey, Relation as TodosRelation, -}; diff --git a/todo-app/server/src/todo_list_table/todos.rs b/todo-app/server/src/todo_list_table/todos.rs deleted file mode 100644 index 699185d..0000000 --- a/todo-app/server/src/todo_list_table/todos.rs +++ /dev/null @@ -1,24 +0,0 @@ -//! SeaORM Entity. Generated by sea-orm-codegen 0.6.0 - -use sea_orm::entity::prelude::*; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] -#[sea_orm(table_name = "todos")] -pub struct Model { - #[sea_orm(primary_key)] - pub todo_id: i32, - #[sea_orm(unique)] - pub username: String, - pub todo_list: Option, -} - -#[derive(Copy, Clone, Debug, EnumIter)] -pub enum Relation {} - -impl RelationTrait for Relation { - fn def(&self) -> RelationDef { - panic!("No RelationDef") - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/tutorials-book/src/SUMMARY.md b/tutorials-book/src/SUMMARY.md index 24d5076..3c80ca3 100644 --- a/tutorials-book/src/SUMMARY.md +++ b/tutorials-book/src/SUMMARY.md @@ -2,23 +2,27 @@ [Introduction](ch00-00-introduction.md) -## Chapter 1 - Simple CRUD Operations +## Chapter 1 - Building a Backend with SeaORM -- [Chapter 1 - Simple CRUD Operations](ch01-00-simple-crud-getting-started.md) - - [Create Operation](ch01-01-create-operation.md) - - [Insert Operation](ch01-02-insert-operations.md) - - [Read Operation](ch01-03-read-operation.md) - - [Update Operation](ch01-04-update-operation.md) - - [Delete Operation](ch01-05-delete-operation.md) - - [Multi-table relationships](ch01-06-relationships.md) +- [Chapter 1 - Building a Backend with SeaORM](ch01-00-build-backend-getting-started.md) + - [Project Setup](ch01-01-project-setup.md) + - [Migration (CLI)](ch01-02-migration-cli.md) + - [Migration (API)](ch01-03-migration-api.md) + - [Generate Entity from Database](ch01-04-entity-generation.md) + - [Basic CRUD Operations](ch01-05-basic-crud-operations.md) + - [Relational Select](ch01-06-relational-select.md) + - [Testing with Mock Interface](ch01-07-mock-testing.md) + - [Optional: Building SQL Queries with SeaQuery](ch01-08-sql-with-sea-query.md) -## Chapter 2 - A Command-line TODO app +## Chapter 2 - Integration with Rocket -- [Chapter 2 - A Command-line TODO app](ch02-00-todo-app-getting-started.md) - - [Building The Web Server](ch02-01-http-server.md) - - [Creating Tables](ch02-02-tables.md) - - [Building Server Connections and Responses](ch02-03-server.md) - - [Building The TODO HTTP Client](ch02-04-00-client.md) - - [Formatting Utilities](ch02-04-01-utils.md) - - [Remote Database Operations](ch02-04-02-remote-db.md) - - [Reading User Input](ch02-04-03-stdout.md) +- [Chapter 2 - Integration with Rocket](ch02-00-integration-with-rocket.md) + - [Project Setup](ch02-01-project-setup.md) + - [Connect to Database](ch02-02-connect-to-database.md) + - [Error Handling](ch02-03-error-handling.md) + - [Web API Integration](ch02-04-web-api-integration.md) + - [Optional: Simple Frontend Using Templates](ch02-05-simple-frontend-using-templates.md) + +## Chapter 3 - Integration with GraphQL + +- [Chapter 3 - Integration with GraphQL](ch03-00-integration-with-graphql.md) diff --git a/tutorials-book/src/assets/er_diagram.png b/tutorials-book/src/assets/er_diagram.png new file mode 100644 index 0000000..19b658c Binary files /dev/null and b/tutorials-book/src/assets/er_diagram.png differ diff --git a/tutorials-book/src/ch00-00-introduction.md b/tutorials-book/src/ch00-00-introduction.md index 5144019..7c0b6da 100644 --- a/tutorials-book/src/ch00-00-introduction.md +++ b/tutorials-book/src/ch00-00-introduction.md @@ -1,6 +1,6 @@ # Introduction -SeaORM is the most feature rich async ORM for integrating a Rust code base with relational databases aiming to be a write code once and run on any popular Relational Database with current support for MySQL, PostgreSQL, MariaDB and SQLite. The tutorials in this book are a gentle introduction to using the `sea-orm` crate and it's cli tool `sea-orm-cli`. +SeaORM is a feature rich async ORM for integrating a Rust code base with relational databases aiming to be a write code once and run on any popular Relational Database with current support for MySQL, PostgreSQL, MariaDB and SQLite. The tutorials in this book are a gentle introduction to using the `sea-orm` crate and its cli tool `sea-orm-cli`. #### Symbols Used @@ -12,17 +12,14 @@ To show added or removed code from files, we will use comments or `-` to show removed code -`...` is used to show only part of the existing code instead of rewriting already existing code in the examples. +`...` is used to show only part of the existing code instead of rewriting already existing code in the examples -`$ ` shows an operation is done on the console/shell - -`postgres=#` shows a PostgreSQL prompt. +`$` shows an operation is done on the console/shell #### Chapters -Each tutorial is contained in it's own chapter and each chapter has subsections that walk you though the steps of each tutorial. +In the first chapter, we will learn how to build a backend application with SeaORM. It will be compatible with different database implementations. -- Chapter 1 - This chapter illustrates doing `Create`, `Read`, `Update` and `Delete` `(CRUD)`operations using a MySQL database. -- Chapter 2 - This chapter simulates a real world application combining an in-memory cache, a local application SQLite cache and a remote HTTP API with a PostgreSQL backend used for persistence of data. +In the subsequent chapters, we will explore the process of building other applications that integrate with a SeaORM-powered backend. In particular, we will be looking at how to build Rocket and GraphQL applications that interact with the backend we created in the first chapter. Let's get started. diff --git a/tutorials-book/src/ch01-00-build-backend-getting-started.md b/tutorials-book/src/ch01-00-build-backend-getting-started.md new file mode 100644 index 0000000..bb6eaaa --- /dev/null +++ b/tutorials-book/src/ch01-00-build-backend-getting-started.md @@ -0,0 +1,27 @@ +# Chapter 1 - Building a Backend with SeaORM + +In this chapter, we will build a backend application with SeaORM. It will act as a layer of communication with the database. + +The application will simulate the interface of a database of bakeries. For simplicity, there will be only two entities, `Bakery` and `Baker`. We will explore the schema later on. + +## Choosing a Database + +Before we start building the backend, we want to make sure that the database is up and running. Setting up the database is beyond the scope of this tutorial. + +SeaORM itself is agnostic to different database implementations, including MySQL, PostgreSQL, and SQLite (in files or in memory). + +However, depending on the database of your choice, you need to pay attention to the following: + +- The appropriate [DB driver feature](https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime#database_driver) should be enabled. +- A valid database URL should be used: + +| Database | Example Database URL | +| :----------------: | :-----------------------: | +| MySQL | `mysql://root:root@localhost:3306` | +| PostgreSQL | `postgres://root:root@localhost:5432` | +| SQLite (in file) | `sqlite:./sqlite.db?mode=rwc` | +| SQLite (in memory) | `sqlite::memory:` | + +We will showcase exactly how to and where to use them in the next section. + +Once the database is ready, we can proceed to [set up the project](ch01-01-project-setup.md). diff --git a/tutorials-book/src/ch01-00-simple-crud-getting-started.md b/tutorials-book/src/ch01-00-simple-crud-getting-started.md deleted file mode 100644 index 06015b4..0000000 --- a/tutorials-book/src/ch01-00-simple-crud-getting-started.md +++ /dev/null @@ -1,81 +0,0 @@ -# Chapter 1 - Simple CRUD Operations - - In this tutorial, SeaORM is used with `async-std` as the async runtime, `rustls` for database TLS connections and `sqlx-mysql` for the MySQL database backend. - -### Installation of dependencies and tools - -1. Install SeaORM-Cli that will help in reading a database schema and generating the relevant `Entity`, `Model` and `Relation` of every table in our selected database (`schema`). - - ```sh - $ cargo install sea-orm-cli - ``` - -2. Create a new Rust Cargo project - ```sh - $ cargo new SimpleCrud --name simple-crud - ``` - -3. Switch to the new cargo project - - ```sh - $ cd simple-crud - ``` - -4. Add SeaORM as a dependency in `Cargo.toml` file - - If you have `cargo edit` installed, run - - ```sh - $ cargo add sea-orm --no-default-features --features "runtime-async-std-rustls sqlx-mysql macros" - ``` - - or if you don't have `cargo edit` installed, you can install it by running - - ```sh - $ cargo install cargo-edit - ``` - -5. Add the async runtime - - ```sh - $ cargo add anyhow - - $ cargo add async-std --features attributes - ``` - - You can also add them manually in the `Cargo.toml` file - - ```toml - sea-orm = { version = "0.5", features = [ "runtime-async-std-rustls", "sqlx-mysql", "macros" ], default-features = false} - anyhow = "1" - async-std = { version = "1", features = [ "attributes" ] } - ``` - - - -6. Make sure that your database server is running, then login and create a database called `fruit_markets`. - - ```sql - CREATE DATABASE fruit_markets; - ``` - - - -7. Create a new user in the database called `webmaster` and with a password `master_char` - - ```sql - # Step1: Create a new user - CREATE USER 'webmaster'@'localhost' IDENTIFIED BY 'master_char'; - - # Step 2: Allow the user to have Read, Write access to all tables in database `fruit_markets` - GRANT ALL PRIVILEGES ON fruit_markets . * TO 'webmaster'@'localhost'; - - # Step 3: Enable the above settings - FLUSH PRIVILEGES; - - # Step 4: Logout of the database - exit - ``` - -We are all set to perform CRUD operations from the MySQL database side. - diff --git a/tutorials-book/src/ch01-01-create-operation.md b/tutorials-book/src/ch01-01-create-operation.md deleted file mode 100644 index 2286270..0000000 --- a/tutorials-book/src/ch01-01-create-operation.md +++ /dev/null @@ -1,247 +0,0 @@ -# Create Operation - -SeaORM abstracts database opertaions though the `sea_orm::Database::connect()` method which yields a `DatabaseConnection`. We will use this `DatabaseConnection` to execute database operations. Let's create the `fruits` table using the database connection. - -Our goal is to do the SQL `CREATE TABLE` operation: - -```sql -# Create a fruits table -CREATE TABLE fruits( - fruit_id INT NOT NULL AUTO_INCREMENT, - name VARCHAR(255) NOT NULL, - datetime_utc TIMESTAMP NOT NULL, - unit_price INT NOT NULL, - sku VARCHAR(255) NOT NULL, - PRIMARY KEY (fruit_id) -); -``` - -The `fruits` table is a record of all the fruits available, their id (`fruit_id`), name (`name`), the timestamp when the row was entered (`datetime_utc`) in the UTC timezone, the price per kilogram of fruit (`unit_price`) and the stock tracking alphanumeric code (`sku`) commonly known as **Stock Keeping Unit**. - ---- - - - -Add a module `fruits_table` inside the `src` folder and add `mod.rs`, `prelude.rs` and `fruit.rs` files as its children. - -**FILE:** *SimpleCRUD/src/create_fruits_table.rs* - -``` -|-- SimpleCRUD/ - |-- Cargo.toml - |-- Cargo.lock - |-- src/ - |-- main.rs -+ |-- fruits_table/ #Code to create table fruits goes here -+ |-- fruits.rs -+ |-- mod.rs -+ |-- prelude.rs -``` - - - -Then, import this module - -**FILE:** *SimpleCRUD/src/main.rs* - -```rust,no_run -+ mod fruits_table; -+ use fruits_table::prelude::Fruits; - - // Import the needed modules for table creation -+ use sea_orm::{ConnectionTrait, Database, Schema}; -// Handle errors using the `https://crates.io/crates/anyhow` crate -+ use anyhow::Result; - -// Convert this main function into async function -+ #[async_std::main] -+ async fn main() -> Result<()>{ -- fn main { -+ -+ -+ Ok(()) -+ } -``` - -The `#[async_std::main]` attribute is used to convert our main function `fn main() {}` into an async function `async fn main() {}` in order to use `await` inside main using `async-std` as the library. - - - -Inside the `fruit.rs` add: - -**FILE:** *SimpleCRUD/src/fruits_table/fruit.rs* - -```rust,noplayground -use sea_orm::entity::prelude::*; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] -#[sea_orm(table_name = "fruits")] -pub struct Model { - #[sea_orm(primary_key)] - pub fruit_id: i32, - #[sea_orm(unique)] - pub name: String, - pub datetime_utc: DateTime, - pub unit_price: i32, - pub sku: String, -} - -#[derive(Copy, Clone, Debug, EnumIter)] -pub enum Relation {} - -impl RelationTrait for Relation { - fn def(&self) -> RelationDef { - panic!("No RelationDef") - } -} - -impl ActiveModelBehavior for ActiveModel {} - -``` - -The `#[derive(... , DeriveEntityModel)]` proc macro is used to automatically derive the code for `Entity`, `Model` and `ActiveModel` . For this to work, the struct **MUST** be called `Model`. - -The enum `Relation` **MUST** also be created, currently, it has empty fields but if the table had a relationship with another table, this is where it would be illustrated. The `#[derive(... , EnumIter)]` is required on a `Relation` to ensure the type implements and exposes a Rust `Iterator`. - -A `Relation` **MUST** implement the `RelationTrait` trait and the method `def()` of the trait. Currently, there is no relation so the `def()` method returns a `panic!("No RelationDef")` if we try to do operations like joins with other tables. - -Lastly, we implement `ActiveModelBehavior` for the `ActiveModel`. The `ActiveModel` is autogenerated by SeaORM codegen when we derived `#[derive(... , DeriveEntityModel)]` from `Model` struct. - -The `#[sea_orm(primary_key)]` is used to set the primary key and can be called using the `..Default::default()` when instantiating a model. - -`#[sea_orm(unique)]` derive macro is used on the `name` field to ensure that two rows are not entered with the same name. This corresponds to SQL `UNIQUE` constraint. - -To set the field of a `Model` to a default of `NULL` , ensure the field is set to an `Option` , for example, to set `sku` field of `Model` to SQL default of `NULL`: - -```rust,noplayground -#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] -#[sea_orm(table_name = "fruits")] -pub struct Model { - // -- shippet -- -+ pub sku: Option, -- pub sku: String, -} -``` - - - -Inside `prelude.rs` add: - -**FILE:** *SimpleCRUD/src/fruits_table/prelude.rs* - -```rust,noplayground -pub use super::fruits::{ - ActiveModel as FruitsActiveModel, Column as FruitsColumn, Entity as Fruits, - Model as FruitsModel, PrimaryKey as FruitsPrimaryKey, Relation as FruitsRelation, -}; -``` - -This code reads the `Entity` from the generated code and renames it to `Fruits` to avoid name collisions with other existing `Entities`. The same goes for the `Model`, `Relation`, `ActiveModel`, `Column`, etc.. - - - -Inside `mod.rs` , export the modules using: - -**FILE:** *SimpleCRUD/src/fruits_table/mod.rs* - -```rust,noplayground -pub mod prelude; - -pub mod fruits; -``` - - - -Add code to perform execution: - -**FILE:** *SimpleCRUD/src/main.rs* - -```rust,no_run -// Code snippet - -#[async_std::main] -async fn main() -> Result<()>{ - // Read the database environment from the `.env` file -+ let env_database_url = include_str!("../.env").trim(); - // Split the env url -+ let split_url: Vec<&str> = env_database_url.split("=").collect(); - // Get item with the format `database_backend://username:password@localhost/database` -+ let database_url = split_url[1]; -+ -+ let db = Database::connect(database_url).await?; -+ -+ let builder = db.get_database_backend(); -+ let schema = Schema::new(builder); -+ -+ let create_table_op = db.execute(builder.build(&schema.create_table_from_entity(Fruits))).await; -+ println!("`CREATE TABLE fruits` {:?}", -+ match create_table_op { -+ Ok(_) => "Operation Successful".to_owned(), -+ Err(e) => format!("Unsuccessful - Error {:?}", e), -+ } -+ ); - Ok(()) -} -``` - -This operation requires reading the database environment, so we read the `.env` file using `include_str!(".env")` and store that result as the `database_url` variable. - -The `Database::connect(database_url)` creates a `DatabaseConnection` that we will use to connect to the database abd perform operations. Using this connection, the `get_database_backend()` method retrieves the database backend in use and then build a schema using ` Schema::new(builder)` which in turn is used by the database backed value stored in the `builder` variable to build the SQL statement using the `.build()` method. - -Finally, we run the SQL query using the `.execute()` method on `DatabaseConnection` stored as the `db` variable. Running the program using `cargo run` should print: - -```sh -$ `CREATE TABLE fruits` "Operation Successful" -``` - -Running the operation again should print the error: - -```sh -$ `CREATE TABLE fruits` "Unsuccessful - Error Exec(\"error returned from database: 1050 (42S01): Table 'fruits' already exists\")" -``` - - - -### Automatically deriving the code to perform CRUD operations - -If the database we want to use already exists, we can automatically generate an `Entity`, `Model` and `ActiveModel` using `sea-orm-cli` which we installed in the `Installation` part of the `Introduction`. - - `sea-orm-cli` will load the database configuration by reading the `.env` file we created earlier in order to login to the database using the username and password in this file, then it will load the `schema` which is the database we specified, create the `Entities` from all the tables in the selected database and automatically generate the relevant code and of the process is successful, create all the code in the folder we will specify. - -In the current working directory of the project, execute: - -```sh -$ sea-orm-cli generate entity -o src/fruits_table -``` - -The structure of the current working directory after `sea-orm-cli` has done its "magical" code generation: - -```sh -|-- SimpleCRUD/ - |-- Cargo.toml - |-- Cargo.lock - |-- src/ - |-- main.rs -+ |-- fruits_table/ #Model, ActiveModel and Entity code generated by `sea-orm-cli` -+ |-- fruit.rs -+ |-- mod.rs -+ |-- prelude.rs -``` - -Next, import the `fruits_table` module for use with the project - -**File:** *./SimpleCRUD/src/main.rs* - -```rust,no_run -+ mod fruits_table; - -#[async_std::main] -async fn main() -> Result<()>{ - ... - Ok(()) -} -``` - -That's it, we have automatically loaded and created all the tables in our database as `Entities` using `sea-orm-cli`. - -Next, we perform `Insert` operations and print results to the console. diff --git a/tutorials-book/src/ch01-01-project-setup.md b/tutorials-book/src/ch01-01-project-setup.md new file mode 100644 index 0000000..83cde04 --- /dev/null +++ b/tutorials-book/src/ch01-01-project-setup.md @@ -0,0 +1,128 @@ +# Project Setup + +In this section, we will set up our project, including the folder structure and crate dependencies. + +We will be using a **MySQL** database throughout the tutorials, but all functionalities of SeaORM are **agnostic to the database implementation**, as mentioned before. + +## Adding `sea-orm` as a Dependency + +```sh +$ cargo init bakery-backend +``` + +```diff +# Cargo.toml + +... + +[dependencies] ++ sea-orm = { version = "0.8.0", features = [ "sqlx-mysql", "runtime-async-std-native-tls", "macros" ] } + +... + +``` + +The DB driver feature `sqlx-mysql` is used because we are using MySQL. + +The second feature, `runtime-async-std-native-tls` is an async runtime arbitrarily chosen for this project. More information can be found on the [docs](https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime/#async_runtime). + +Finally, the `macros` feature is an optional feature that allows the use of some `Derive` macros. + +## Connecting to the Database server + +We add `futures` as a dependency so that we can make use of asynchronous programming with `async`/`await`. + +```diff +# Cargo.toml + +... + +[dependencies] ++ futures = "0.3.21" +sea-orm = { version = "0.8.0", features = [ "sqlx-mysql", "runtime-async-std-native-tls", "macros" ] } + +... + +``` + +Connect to the database server: + +```rust, no_run +// main.rs + +use futures::executor::block_on; +use sea_orm::{Database, DbErr}; + +// Change this according to your database implementation, +// or supply it as an environment variable. +const DATABASE_URL: &str = "mysql://root:root@localhost:3306"; + +async fn run() -> Result<(), DbErr> { + let db = Database::connect(DATABASE_URL).await?; + + Ok(()) +} + +fn main() { + if let Err(err) = block_on(run()) { + panic!("{}", err); + } +} +``` + +If everything is correctly set up, the program should terminate gracefully. + +If it panicks, it could be that the database URL is wrong. + +If it hangs, it could be that the database is not up and running. + +## Creating a Database + +For MySQL and PostgreSQL, we can create a specific database instance. Let's call it `bakeries_db`. + +```rust, no_run +... +// main.rs + +... + +async fn run() -> Result<(), DbErr> { + let db = Database::connect(DATABASE_URL).await?; + ++ let db_name = "bakeries_db"; ++ let db = &match db.get_database_backend() { ++ DbBackend::MySql => { ++ db.execute(Statement::from_string( ++ db.get_database_backend(), ++ format!("CREATE DATABASE IF NOT EXISTS `{}`;", db_name), ++ )) ++ .await?; ++ ++ let url = format!("{}/{}", DATABASE_URL, db_name); ++ Database::connect(&url).await? ++ } ++ DbBackend::Postgres => { ++ db.execute(Statement::from_string( ++ db.get_database_backend(), ++ format!("DROP DATABASE IF EXISTS \"{}\";", db_name), ++ )) ++ .await?; ++ db.execute(Statement::from_string( ++ db.get_database_backend(), ++ format!("CREATE DATABASE \"{}\";", db_name), ++ )) ++ .await?; ++ ++ let url = format!("{}/{}", DATABASE_URL, db_name); ++ Database::connect(&url).await? ++ } ++ DbBackend::Sqlite => db, ++ }; + + Ok(()) +} + +... +``` + +This snippet shows that SeaORM is database-agnostic. You may only handle the case for your chosen database if you are sure only one type of database will be used. diff --git a/tutorials-book/src/ch01-02-insert-operations.md b/tutorials-book/src/ch01-02-insert-operations.md deleted file mode 100644 index 77c882b..0000000 --- a/tutorials-book/src/ch01-02-insert-operations.md +++ /dev/null @@ -1,140 +0,0 @@ -# Insert Operation - -SeaORM insert and read operations are done using the `Entity` derived from the `Model` struct using the `EntityTrait`. - -Let's insert a fruit `Apple` with a unit price per Kg of $2 and an SKU of `FM2022AKB40`. - -Add `chrono` crate to get the current time from the system time - -```toml -[package] -name = "simple-crud" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -anyhow = "1.0.52" -async-std = { version = "1.10.0", features = ["attributes"] } -sea-orm = { version = "0.5.0", features = [ - "runtime-async-std-rustls", - "sqlx-mysql", - "macros", -], default-features = false } -+ chrono = "0.4.19" # Add chrono here -``` - -Modify the current `sea-orm` features to add the feature `with-chrono`. This activates Date and Time features. - -```TOML -[package] -name = "simple-crud" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -anyhow = "1.0.52" -async-std = { version = "1.10.0", features = ["attributes"] } -chrono = "0.4.19" -sea-orm = { version = "0.5.0", features = [ - "runtime-async-std-rustls", - "sqlx-mysql", - "macros", -+ "with-chrono", # New feature -], default-features = false } -chrono = "0.4.19" # Add chrono here -``` - - - -Next, call `Utc::now()` chrono method to get the system time and then import sea_orm::entity::`Set` to perform convertions of the Rust data types into SQL ready data type `ActiveValue` - -```rust,no_run -// -- code snippet -- -+ use sea_orm::entity::Set; - -#[async_std::main] -async fn main() -> Result<()>{ - ... - - // Get current system time -+ let now = chrono::offset::Utc::now(); - - // Convert system time to `NaiveDateTime` since SeaORM `DateTime` expects this; -+ let naive_system_time = now.naive_utc(); - -+ let fruit_01 = FruitsActiveModel { -+ name: Set("Apple".to_owned()), -+ datetime_utc: Set(naive_system_time), -+ unit_price: Set(2), -+ sku: Set("FM2022AKB40".to_owned()), -+ ..Default::default() -+ }; -+ let fruit_insert_operation = Fruits::insert(fruit_01).exec(&db).await; - -+ println!("INSERTED ONE: {:?}", fruit_insert_operation?); - - Ok(()) -} -``` - -Since an `Entity` implements `EntityTrait`, the insert method is availabe. executing `Fruits::insert(fruit_01)` will perform the operation on the database using `exec(&db).await`. Here, the `insert` operation inserts only one row into the specified database; - -Running the program using `cargo run` should print - -```sh -$ INSERTED ONE: InsertResult { last_insert_id: 1 } -``` - -Let's insert more than one row at a time using the `Fruits::insert_many()` method. - -```rust,no_run -// -- code snippet -- -+ use chrono::offset::Utc; -#[async_std::main] -async fn main() -> Result<()>{ - ... - -+ let fruit_02 = FruitsActiveModel { -+ name: Set("Banana".to_owned()), -+ datetime_utc: Set(Utc::now().naive_utc()), -+ unit_price: Set(2), -+ sku: Set("FM2022AKB41".to_owned()), -+ ..Default::default() -+ }; - -+ let fruit_03 = FruitsActiveModel { -+ name: Set("Pineapple".to_owned()), -+ datetime_utc: Set(Utc::now().naive_utc()), -+ unit_price: Set(8), -+ sku: Set("FM2022AKB42".to_owned()), -+ ..Default::default() -+ }; - -+ let fruit_04 = FruitsActiveModel { -+ name: Set("Mango".to_owned()), -+ datetime_utc: Set(Utc::now().naive_utc()), -+ unit_price: Set(6), -+ sku: Set("FM2022AKB43".to_owned()), -+ ..Default::default() -+ }; -+ let fruit_insert_operation = Fruits::insert_many(vec![fruit_02, fruit_03, fruit_04]).exec(&db).await; - -+ println!("INSERTED MANY: {:?}", fruit_insert_operation?); - - Ok(()) -} -``` - -Running the program with `cargo run` prints - -```sh -$ INSERTED MANY: InsertResult { last_insert_id: 3 } -``` - - - -Next up is reading one value or many values from a table. diff --git a/tutorials-book/src/ch01-02-migration-cli.md b/tutorials-book/src/ch01-02-migration-cli.md new file mode 100644 index 0000000..086778e --- /dev/null +++ b/tutorials-book/src/ch01-02-migration-cli.md @@ -0,0 +1,218 @@ +# Migration (CLI) + +*This and the next sections concern those who are getting a taste of SeaORM by creating a new, toy database schema. If you already have an existing database schema, feel free to skip over to [Section 1.4](ch01-04-entity-generation.md).* + +In this section, we define the following simple schema with migrations. + +![ER diagram of two entities, Bakery and Baker. Baker has a foreign key referencing Bakery.](./assets/er_diagram.png) + +## Initialize using `sea-orm-cli` + +For beginners, it is recommended to use `sea-orm-cli` to define and run the migrations. + +```sh +# Install `sea-orm-cli` +$ cargo install sea-orm-cli + +# List all available migration commands that are supported by `sea-orm-cli` +$ sea-orm-cli migrate -h +``` + +Initialize the `migration` folder: + +```sh +$ sea-orm-cli migrate init + +# The folder structure will be as follows: + +bakery-backend +│ ... +│ +└─── migration +│ │ Cargo.toml +│ │ README.md +│ │ +│ └─── src +│ │ lib.rs +│ │ m20220101_000001_create_table.rs +│ │ main.rs +``` + +## Define the migrations + +Update the migration files to define the `Bakery` and `Baker` tables: + +The filename must follow the format `m_<6-digit-index>_.rs`. + +For more information about defining migrations, read the documentation of [`SchemaManager`](https://docs.rs/sea-orm-migration/0.8.3/sea_orm_migration/manager/struct.SchemaManager.html). + +```diff +- m20220101_000001_create_table.rs ++ m20220101_000001_create_bakery_table.rs ++ m20220101_000002_create_baker_table.rs +``` + +```rust, no_run +// m20220101_000001_create_bakery_table.rs + +use sea_orm_migration::prelude::*; + +pub struct Migration; + +impl MigrationName for Migration { + fn name(&self) -> &str { + "m20220101_000001_create_bakery_table" // Make sure this matches with the file name + } +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + // Define how to apply this migration: Create the Bakery table. + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(Bakery::Table) + .col( + ColumnDef::new(Bakery::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(Bakery::Name).string().not_null()) + .col(ColumnDef::new(Bakery::ProfitMargin).double().not_null()) + .to_owned(), + ) + .await + } + + // Define how to rollback this migration: Drop the Bakery table. + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(Bakery::Table).to_owned()) + .await + } +} + +#[derive(Iden)] +pub enum Bakery { + Table, + Id, + Name, + ProfitMargin, +} +``` + +```rust, no_run +// m20220101_000002_create_baker_table.rs + +use sea_orm_migration::prelude::*; + +use super::m20220101_000001_create_bakery_table::Bakery; + +pub struct Migration; + +impl MigrationName for Migration { + fn name(&self) -> &str { + "m_20220101_000002_create_baker_table" // Make sure this matches with the file name + } +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + // Define how to apply this migration: Create the Baker table. + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(Baker::Table) + .col( + ColumnDef::new(Baker::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(Baker::Name).string().not_null()) + .col(ColumnDef::new(Baker::ContactDetails).json()) + .col(ColumnDef::new(Baker::BakeryId).integer().not_null()) + .foreign_key( + ForeignKey::create() + .name("fk-baker-bakery_id") + .from(Baker::Table, Baker::BakeryId) + .to(Bakery::Table, Bakery::Id), + ) + .to_owned(), + ) + .await + } + + // Define how to rollback this migration: Drop the Baker table. + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(Baker::Table).to_owned()) + .await + } +} + +// For ease of access +#[derive(Iden)] +pub enum Baker { + Table, + Id, + Name, + ContactDetails, + BakeryId, +} +``` + +```rust, no_run +// migration/src/lib.rs + +pub use sea_orm_migration::prelude::*; + +// Add each migration file as a module +mod m20220101_000001_create_bakery_table; +mod m20220101_000002_create_baker_table; + +pub struct Migrator; + +#[async_trait::async_trait] +impl MigratorTrait for Migrator { + fn migrations() -> Vec> { + vec![ + // Define the order of migrations. + Box::new(m20220101_000001_create_bakery_table::Migration), + Box::new(m20220101_000002_create_baker_table::Migration), + ] + } +} +``` + +**Important**: Make sure the following features are enabled in the `migration` crate. The database driver feature must match the database being used. + +```diff +# migration/Cargo.toml + +... + +[dependencies.sea-orm-migration] +version = "^0.8.0" +features = [ ++ "sqlx-mysql", ++ "runtime-async-std-native-tls", +] +``` + +## Perform the migrations + +Perform all the migrations through `sea-orm-cli`: + +*Make sure you are running this command at the project root.* + +```sh +# Change the value of DATABASE_URL according to your database implementation. +# Make sure the database name is also supplied for MySQL or PostgreSQL. +$ DATABASE_URL="mysql://root:root@localhost:3306/bakeries_db" sea-orm-cli migrate refresh +``` diff --git a/tutorials-book/src/ch01-03-migration-api.md b/tutorials-book/src/ch01-03-migration-api.md new file mode 100644 index 0000000..c82f219 --- /dev/null +++ b/tutorials-book/src/ch01-03-migration-api.md @@ -0,0 +1,223 @@ +# Migration (API) + +If you prefer to set up and run the migrations programmatically, we provide the `Migrator` API for that. + +This section covers how to perform migrations without the need to install and use the CLI tool. + +## Preparation + +Add the cargo dependency `sea-orm-migration`: + +```diff +// Cargo.toml + +... + +[dependencies] +futures = "0.3.21" +sea-orm = { version = "0.8.0", features = [ "sqlx-mysql", "runtime-async-std-native-tls", "macros" ] } ++ sea-orm-migration = "0.8.3" + +... + +``` + +Create a module named `migrator`: + +```rust, no_run +// src/main.rs + ++ mod migrator; + +use futures::executor::block_on; +use sea_orm::{ConnectionTrait, Database, DbBackend, DbErr, Statement}; + +... +``` + +```rust, no_run +// src/migrator/mod.rs (create new file) + +use sea_orm_migration::prelude::*; + +pub struct Migrator; + +#[async_trait::async_trait] +impl MigratorTrait for Migrator { + fn migrations() -> Vec> { + vec![] + } +} +``` + +## Define the migrations + +Define a `Migration` in a file and include it in `migrator/mod.rs`: + +The filename must follow the format `m_<6-digit-index>_.rs`. + +For more information about defining migrations, read the documentation of [`SchemaManager`](https://docs.rs/sea-orm-migration/0.8.3/sea_orm_migration/manager/struct.SchemaManager.html). + +```rust, no_run +// src/migrator/m20220602_000001_create_bakery_table.rs (create new file) + +use sea_orm_migration::prelude::*; + +pub struct Migration; + +impl MigrationName for Migration { + fn name(&self) -> &str { + "m_20220602_000001_create_bakery_table" + } +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + // Define how to apply this migration: Create the Bakery table. + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(Bakery::Table) + .col( + ColumnDef::new(Bakery::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(Bakery::Name).string().not_null()) + .col(ColumnDef::new(Bakery::ProfitMargin).double().not_null()) + .to_owned(), + ) + .await + } + + // Define how to rollback this migration: Drop the Bakery table. + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(Bakery::Table).to_owned()) + .await + } +} + +#[derive(Iden)] +pub enum Bakery { + Table, + Id, + Name, + ProfitMargin, +} +``` + +```rust, no_run +// src/migrator/m20220602_000002_create_baker_table.rs (create new file) + +use sea_orm_migration::prelude::*; + +use super::m20220602_000001_create_bakery_table::Bakery; + +pub struct Migration; + +impl MigrationName for Migration { + fn name(&self) -> &str { + "m_20220602_000002_create_baker_table" + } +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + // Define how to apply this migration: Create the Baker table. + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(Baker::Table) + .col( + ColumnDef::new(Baker::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(Baker::Name).string().not_null()) + .col(ColumnDef::new(Baker::ContactDetails).json()) + .col(ColumnDef::new(Baker::BakeryId).integer().not_null()) + .foreign_key( + ForeignKey::create() + .name("fk-baker-bakery_id") + .from(Baker::Table, Baker::BakeryId) + .to(Bakery::Table, Bakery::Id), + ) + .to_owned(), + ) + .await + } + + // Define how to rollback this migration: Drop the Baker table. + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(Baker::Table).to_owned()) + .await + } +} + +#[derive(Iden)] +pub enum Baker { + Table, + Id, + Name, + ContactDetails, + BakeryId, +} +``` + +```rust, no_run +// src/migrator/mod.rs + +use sea_orm_migration::prelude::*; + ++ mod m20220602_000001_create_bakery_table; ++ mod m20220602_000002_create_baker_table; + +pub struct Migrator; + +#[async_trait::async_trait] +impl MigratorTrait for Migrator { + fn migrations() -> Vec> { + vec![ ++ Box::new(m20220602_000001_create_bakery_table::Migration), ++ Box::new(m20220602_000002_create_baker_table::Migration), + ] + } +} +``` + +## Perform the migrations + +Use the [`MigratorTrait API`](https://docs.rs/sea-orm-migration/0.8.3/sea_orm_migration/migrator/trait.MigratorTrait.html) to perform the migrations. Verify the correctness of the database schema with [`SchemaManager`](https://docs.rs/sea-orm-migration/0.8.3/sea_orm_migration/manager/struct.SchemaManager.html). + +```rust, no_run +// src/main.rs + +... + ++ use sea_orm_migration::prelude::*; + +... + +async fn run() -> Result<(), DbErr> { + + ... + ++ let schema_manager = SchemaManager::new(db); // To investigate the schema + ++ Migrator::refresh(db).await?; ++ assert!(schema_manager.has_table("bakery").await?); ++ assert!(schema_manager.has_table("baker").await?); + + Ok(()) +} + +... +``` diff --git a/tutorials-book/src/ch01-03-read-operation.md b/tutorials-book/src/ch01-03-read-operation.md deleted file mode 100644 index ee2e025..0000000 --- a/tutorials-book/src/ch01-03-read-operation.md +++ /dev/null @@ -1,106 +0,0 @@ -# Read Operation - -SeaORM can perform read operations through the `Entity::find()` method. - -#### Find all rows using in a table - -The `.all()` method in `Entity` is used to fetch all rows in a table. - -```rust,no_run -//-- snippet -- - -#[async_std::main] -async fn main() -> Result<()> { - let env_database_url = include_str!("../.env").trim(); - let split_url: Vec<&str> = env_database_url.split("=").collect(); - let database_url = split_url[1]; - - let db = Database::connect(database_url).await?; - - ... - -+ let fruits_table_rows = Fruits::find().all(&db).await; -+ println!("{:?}", fruits_table_rows?); - - Ok(()) -} -``` - -To fetch all the rows inside a table, in this case `Fruits`, call the `.all()` method on `Fruits::find()` - -This should print all the rows in the table `fruits` to the console as an array of `Model`s. - -```sh -$ [Model { fruit_id: 1, name: "Apple", datetime_utc: 2022-01-22T10:36:39, unit_price: 2, sku: "FM2022AKB40" }, Model { fruit_id: 2, name: "Banana", datetime_utc: 2022-01-22T10:36:39, unit_price: 2, sku: "FM2022AKB41" }, Model { fruit_id: 3, name: "Pineapple", datetime_utc: 2022-01-22T10:36:39, unit_price: 8, sku: "FM2022AKB42" }, Model { fruit_id: 4, name: "Mango", datetime_utc: 2022-01-22T10:36:39, unit_price: 6, sku: "FM2022AKB43" }] -``` - - - -#### Find one row by the primary key - -Call the `.find_by_id(primary_key)` on `Fruits` entity (table). - -```rust,no_run -//-- snippet -- - -#[async_std::main] -async fn main() -> Result<()> { - let env_database_url = include_str!("../.env").trim(); - let split_url: Vec<&str> = env_database_url.split("=").collect(); - let database_url = split_url[1]; - - let db = Database::connect(database_url).await?; - - ... - -+ let fruits_by_id = Fruits::find_by_id(2).one(&db).await; -+ println!("{:?}", fruits_by_id?); - - Ok(()) -} -``` - -The `.one()` method is used to retrieve one `Model` that matches the query instead of a `Vec` like the `.all()` method. `.one()` method returns an `Option` where `Some(Model)` is returned if the `Model` exists or a `None` is returned if a `Model` doesn't exist. - -Running the program prints - -```sh -$ Some(Model { fruit_id: 2, name: "Banana", datetime_utc: 2022-01-22T10:36:39, unit_price: 2, sku: "FM2022AKB41" }) -``` - - - -#### Find and Filter a Row by Column Name - -Calling `filter()` method on `Entity::find()` returns a `Model` containing the matching row. - -```rust,no_run -//-- snippet -- - -#[async_std::main] -async fn main() -> Result<()> { - let env_database_url = include_str!("../.env").trim(); - let split_url: Vec<&str> = env_database_url.split("=").collect(); - let database_url = split_url[1]; - - let db = Database::connect(database_url).await?; - - ... - -+ let find_pineapple = Fruits::find() -+ .filter(FruitsColumn::Name.contains("pineapple")) -+ .one(&db) -+ .await; -+ println!("{:?}", find_pineapple?); - - Ok(()) -} -``` -The `FruitsColumn::Name` is a `Column` that was autoderived by SeaORM from the `Model` struct fields, which we imported and renamed using `use super::fruits::Column as FruitsColumn` in the previous section. `.contains()` method on `FruitsColumn` allows filtering of the `Model`with `Pineapple` as it's name. Note that this is case insensitive so even calling `.contains(piNeApPle)` will yield the same results. - -Running the program prints: - -```sh -$ Some(Model { fruit_id: 3, name: "Pineapple", datetime_utc: 2022-01-22T10:36:39, unit_price: 8, sku: "FM2022AKB42" }) -``` - diff --git a/tutorials-book/src/ch01-04-entity-generation.md b/tutorials-book/src/ch01-04-entity-generation.md new file mode 100644 index 0000000..dac5540 --- /dev/null +++ b/tutorials-book/src/ch01-04-entity-generation.md @@ -0,0 +1,33 @@ +# Generate Entity from Database + +Now that we have a database with a defined schema, we can generate the entities with `sea-orm-cli`. + +`sea-orm-cli` is able to discover the schema given the database URL and generated the appropriate entity files. + +```sh +$ cargo install sea-orm-cli +``` + +```sh +# Generate entity files of database `bakeries_db` to `src/entities` +$ sea-orm-cli generate entity \ + -u mysql://root:root@localhost:3306/bakeries_db \ + -o src/entities +``` + +The generated entity files: + +``` +bakery-backend +│ ... +│ +└─── src +│ │ +│ └─── entities +│ │ baker.rs +│ │ bakery.rs +│ │ mod.rs +│ │ prelude.rs +``` + +Put the focus on `baker.rs` and `bakery.rs`, they are the entities representing the tables `Baker` and `Bakery`, respectively. diff --git a/tutorials-book/src/ch01-04-update-operation.md b/tutorials-book/src/ch01-04-update-operation.md deleted file mode 100644 index ad450bb..0000000 --- a/tutorials-book/src/ch01-04-update-operation.md +++ /dev/null @@ -1,51 +0,0 @@ -# Update Operation - - - -To perform an update in SeaORM, first, fetch the row to perform the operation using `Model` convert it into an `ActiveModel` by calling the `into()` methof on the `Model` , perform the operation on the field on the `ActiveModel` and then and then call the `.update()` method on the `ActiveModel`. The executed result returns the `Model` that was updated if successful. - -```rust,no_run -//-- snippet -- - -+ use sea_orm::sea_query::{Expr, Value}; // Types necessary to perform updates and conversions between types - -#[async_std::main] -async fn main() -> Result<()> { - let env_database_url = include_str!("../.env").trim(); - let split_url: Vec<&str> = env_database_url.split("=").collect(); - let database_url = split_url[1]; - - let db = Database::connect(database_url).await?; - - ... - - let find_pineapple = Fruits::find() - .filter(FruitsColumn::Name.contains("pineapple")) - .one(&db) - .await?; -- println!("{:?}", find_pineapple?); -+ println!("{:?}", find_pineapple.as_ref()); // Reference the `Model` instead of owning it - - // Update the `pineapple` column with a new unit price -+ if let Some(pineapple_model) = find_pineapple { -+ let mut pineapple_active_model: FruitsActiveModel = pineapple_model.into(); -+ pineapple_active_model.unit_price = Set(10); - -+ let updated_pineapple_model: FruitsModel = -+ pineapple_active_model.update(&db).await?; - -+ println!("UPDATED PRICE: {:?}", updated_pineapple_model); -+ } else { -+ println!("`Pineapple` column not found"); -+ } - - Ok(()) -} -``` - -Running the program returns - -```sh -$ UPDATED PRICE: Model { fruit_id: 3, name: "Pineapple", datetime_utc: 2022-01-22T13:35:27, unit_price: 10, sku: "FM2022AKB42" } -``` - diff --git a/tutorials-book/src/ch01-05-basic-crud-operations.md b/tutorials-book/src/ch01-05-basic-crud-operations.md new file mode 100644 index 0000000..a355840 --- /dev/null +++ b/tutorials-book/src/ch01-05-basic-crud-operations.md @@ -0,0 +1,119 @@ +# Basic CRUD Operations + +In this section, we showcase how to perform basic operations with the schema we've defined. + +## `use` the Entities + +The entities are the Rust representation of the tables in the database. SeaORM enables us to make use of those entities to perform operations on the database programmatically. + +```rust, no_run +// src/main.rs + ++ mod entities; + +... + ++ use entities::*; + ++ use entities::{prelude::*, *}; + +... +``` + +## Insert and Update + +Insert and update operations can be performed using `ActiveModel` of the entities. + +Let's insert a new bakery called *Happy Bakery* into our `Bakery` table. + +```rust, no_run +// src/main.rs + +... + +use sea_orm::*; + +... + +async fn run() -> Result<(), DbErr> { + + ... + + let happy_bakery = bakery::ActiveModel { + name: ActiveValue::Set("Happy Bakery".to_owned()), + profit_margin: ActiveValue::Set(0.0), + ..Default::default() + }; + let res = Bakery::insert(happy_bakery).exec(db).await?; +} +``` + +Suppose, later on, the owner of *Happy Bakery* adopts a brand new perspective of life, and renames it to *Sad Bakery*. + +We can perform the update as follows: + +```rust, no_run +let sad_bakery = bakery::ActiveModel { + id: ActiveValue::Set(res.last_insert_id), + name: ActiveValue::Set("Sad Bakery".to_owned()), + profit_margin: ActiveValue::NotSet, +}; +sad_bakery.update(db).await?; +``` + +Let's welcome John, the first employee of *Sad Bakery*! + +```rust, no_run +let john = baker::ActiveModel { + name: ActiveValue::Set("John".to_owned()), + bakery_id: ActiveValue::Set(res.last_insert_id), // a foreign key + ..Default::default() +}; +Baker::insert(john).exec(db).await?; +``` + +## Find (single entity) + +We can find all or some of the bakeries in the database as follows: + +```rust, no_run +// Finding all is built-in +let bakeries: Vec = Bakery::find().all(db).await?; +assert_eq!(bakeries.len(), 1); + +// Finding by id is built-in +let sad_bakery: Option = Bakery::find_by_id(1).one(db).await?; +assert_eq!(sad_bakery.unwrap().name, "Sad Bakery"); + +// Finding by arbitrary column with `filter()` +let sad_bakery: Option = Bakery::find() + .filter(bakery::Column::Name.eq("Sad Bakery")) + .one(db) + .await?; +assert_eq!(sad_bakery.unwrap().id, 1); +``` + +For relational select on multiple entities, visit the next [section](ch01-06-relational-select.md). + +## Delete + +Sadly, *Sad Bakery* is unable to survive in the rapidly changing economy; it has been forced to liquidate! + +We have no choice but to remove its entry in our database: + +```rust, no_run +let john = baker::ActiveModel { + id: ActiveValue::Set(1), // The primary key must be set + ..Default::default() +}; +john.delete(db).await?; + +let sad_bakery = bakery::ActiveModel { + id: ActiveValue::Set(1), // The primary key must be set + ..Default::default() +}; +sad_bakery.delete(db).await?; + +let bakeries: Vec = Bakery::find().all(db).await?; +assert!(bakeries.is_empty()); +``` diff --git a/tutorials-book/src/ch01-05-delete-operation.md b/tutorials-book/src/ch01-05-delete-operation.md deleted file mode 100644 index 5d36c36..0000000 --- a/tutorials-book/src/ch01-05-delete-operation.md +++ /dev/null @@ -1,46 +0,0 @@ -# Delete Operation - - -To perform a delete operation in SeaORM, first, fetch the row to perform the operation using `Model` convert it into an `ActiveModel` by calling the `into()` methof on the `Model` , perform the operation on the field on the `ActiveModel` and then and then call the `.delete()` method on the `ActiveModel` or use `Fruit::delete()`. The executed result returns the `Model` that was updated if successful. - -```rust,no_run -//-- snippet -- - -+ use sea_orm::sea_query::{Expr, Value}; // Types necessary to perform updates and conversions between types - -#[async_std::main] -async fn main() -> Result<()> { - let env_database_url = include_str!("../.env").trim(); - let split_url: Vec<&str> = env_database_url.split("=").collect(); - let database_url = split_url[1]; - - let db = Database::connect(database_url).await?; - - ... - - // Delete the `mango` row - -+ let find_mango = Fruits::find() -+ .filter(FruitsColumn::Name.contains("mango")) -+ .one(&db) -+ .await; - -+ if let Some(mango_model) = find_mango? { -+ println!( -+ "DELETED MANGO: {:?}", -+ mango_model.delete(&db).await? -+ ); -+ } else { -+ println!("`Mango` row not found"); -+ } - - Ok(()) -} -``` - -Running the program returns - -```sh -$ DELETED MANGO: DeleteResult { rows_affected: 1 } -``` - diff --git a/tutorials-book/src/ch01-06-relational-select.md b/tutorials-book/src/ch01-06-relational-select.md new file mode 100644 index 0000000..441a9ba --- /dev/null +++ b/tutorials-book/src/ch01-06-relational-select.md @@ -0,0 +1,45 @@ +# Relational Select + +In the previous section, we explored how to [perform select on a single entity](ch01-05-basic-crud-operations.md#find-single-entity). + +However, relational databases are known for connecting entities with relations, such that we can perform queries **across different entities**. + +For example, given a bakery, we can find all the bakers working there. + +Suppose the following code were run before, inserting the bakery and the bakers it employed into the database. + +```rust, no_run +let la_boulangerie = bakery::ActiveModel { + name: ActiveValue::Set("La Boulangerie".to_owned()), + profit_margin: ActiveValue::Set(0.0), + ..Default::default() +}; +let bakery_res = Bakery::insert(la_boulangerie).exec(db).await?; + +for baker_name in ["Jolie", "Charles", "Madeleine", "Frederic"] { + let baker = baker::ActiveModel { + name: ActiveValue::Set(baker_name.to_owned()), + bakery_id: ActiveValue::Set(bakery_res.last_insert_id), + ..Default::default() + }; + Baker::insert(baker).exec(db).await?; +} +``` + +There are 4 bakers working at the bakery *La Boulangerie*, and we can find them later on as follows: + +```rust, no_run +// First find *La Boulangerie* as a Model +let la_boulangerie: bakery::Model = Bakery::find_by_id(bakery_res.last_insert_id) + .one(db) + .await? + .unwrap(); + +let bakers: Vec = la_boulangerie.find_related(Baker).all(db).await?; +let mut baker_names: Vec = bakers.into_iter().map(|b| b.name).collect(); +baker_names.sort_unstable(); + +assert_eq!(baker_names, ["Charles", "Frederic", "Jolie", "Madeleine"]); +``` + +For more advanced usage, visit the [documentation](https://www.sea-ql.org/SeaORM/docs/basic-crud/select/#find-related-models). diff --git a/tutorials-book/src/ch01-06-relationships.md b/tutorials-book/src/ch01-06-relationships.md deleted file mode 100644 index 6a13e8b..0000000 --- a/tutorials-book/src/ch01-06-relationships.md +++ /dev/null @@ -1,259 +0,0 @@ -# Multi-table relationships - -This section shows how to use SeaORM to perform operations between the Fruits table and a new suppliers table. - -Creating relationships between tables can be verbose when doing so using code. Luckily, SeaORM makes this easy. Define a table called `suppliers` in the `fruit_markets` database by taking the following steps: - -1. Login to mysql database using username and password created in the `installation` part of this tutorial in the previous section and switch to fruit_markets database. - - ```sql - # Execute - use fruit_markets; - ``` - -2. Create a table `suppliers` that references the primary key `fruit_id` of table `fruits`. This will show the type of fruit the supplier supplies to the fruit markets. - - ```sql - CREATE TABLE suppliers( - supplier_id INT NOT NULL AUTO_INCREMENT, - supplier_name VARCHAR(255) NOT NULL, - fruit_id INT NOT NULL, - PRIMARY KEY (supplier_id), - CONSTRAINT fk_fruits - FOREIGN KEY (fruit_id) - REFERENCES fruits(fruit_id) - ON UPDATE CASCADE - ON DELETE CASCADE - ) ENGINE=INNODB; - ``` - -3. Use `sea-orm-cli` to generate the `Entity`, `Model`, `Relationship` and `ActiveModel`. - - ```sh - $ sea-orm-cli generate entity -o src/suppliers_table -t suppliers - ``` - - A new directory `suppliers_table` is created in the `src` directory containing serveral files with code generated by `sea-orm-cli`. - -4. Modify the `src/suppliers_table/prelude.rs` file to export memorable names of the `Entity, ActiveModel` etc - - ```rust,noplayground,no_run - - pub use super::suppliers::Entity as Suppliers; - - + pub use super::suppliers::{ - + ActiveModel as SuppliersActiveModel, Column as SuppliersColumn, Entity as Suppliers, - + Model as SuppliersModel, PrimaryKey as SuppliersPrimaryKey, Relation as SuppliersRelation, - + }; - - ``` - -5. The `src/suppliers_table/suppliers.rs` contains errors indicating the `super::fruits` cannot be found in `supper`. This means the module is not exported properly. Fix this by importing the module: - - ```rust,noplayground,no_run - //! SeaORM Entity. Generated by sea-orm-codegen 0.5.0 - - use sea_orm::entity::prelude::*; - - #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] - #[sea_orm(table_name = "suppliers")] - pub struct Model { - #[sea_orm(primary_key)] - pub supplier_id: i32, - pub supplier_name: String, - pub fruit_id: i32, - } - - #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] - pub enum Relation { - #[sea_orm( - - belongs_to = "super::fruits::Entity", - + belongs_to = "crate::Fruits", - from = "Column::FruitId", - - to = "super::fruits::Column::FruitId", - + to = "crate::FruitsColumn::FruitId", - on_update = "Cascade", - on_delete = "Cascade" - )] - Fruits, - } - - - impl Related for Entity { - + impl Related for Entity { - ... - } - - impl ActiveModelBehavior for ActiveModel {} - ``` - - `sea-orm-cli` automatically generates code to bind the `suppliers` table `Model` to the primary key of the `fruits` table using `belongs_to = "crate::Fruits",` `to = "crate::FruitsColumn::FruitId"` and `impl Related for Entity`. This corresponds to the SQL query part - - ```sql - CONSTRAINT fk_fruits - FOREIGN KEY (fruit_id) - REFERENCES fruits(fruit_id) - ON UPDATE CASCADE - ON DELETE CASCADE - ``` - -6. Import the module to the `src/main.rs` file - - ```rust,noplayground,no_run - mod fruits_table; - use fruits_table::prelude::*; - + mod suppliers_table; - + use suppliers_table::prelude::*; - - // -- code snippet -- - - // Convert this main function into async function - #[async_std::main] - async fn main() -> Result<()> { - // -- code snippet -- - } - ``` - - - -### Inserting Values into a Table with a Foreign Key - -Insert many suppliers in the `supplies` table - -```rust,no_run -// -- code snippet -- - -// Convert this main function into async function -#[async_std::main] -async fn main() -> Result<()> { - // -- code snippet -- - -+ let supplier_01 = SuppliersActiveModel { -+ supplier_name: Set("John Doe".to_owned()), -+ fruit_id: Set(1_i32), -+ ..Default::default() -+ }; - -+ let supplier_02 = SuppliersActiveModel { -+ supplier_name: Set("Jane Doe".to_owned()), -+ fruit_id: Set(2_i32), -+ ..Default::default() -+ }; - -+ let supplier_03 = SuppliersActiveModel { -+ supplier_name: Set("Junior Doe".to_owned()), -+ fruit_id: Set(3_i32), -+ ..Default::default() -+ }; - -+ let supplier_insert_operation = -+ Suppliers::insert_many(vec![supplier_01, supplier_02, supplier_03]) -+ .exec(&db) -+ .await; - -+ println!("INSERTED MANY: {:?}", supplier_insert_operation?); - - - Ok(()) -} -``` - - - -Executing the program returns - -```sh -$ INSERTED MANY: InsertResult { last_insert_id: 1 } -``` - -### SELECTing related tables - -SeaORM makes it easy to fetch a table and it's related table referenced by its primary key using the `Entity::find().find_with_related(Other_Entity).all(DatabaseConnection)` chain of methods. - -```rust,no_run -// --- Code Snippet --- -#[async_std::main] -async fn main() -> Result<()> { - let env_database_url = include_str!("../.env").trim(); - let split_url: Vec<&str> = env_database_url.split("=").collect(); - let database_url = split_url[1]; - - let db = Database::connect(database_url).await?; - - // -- Code snippet -- - - - let supplier_insert_operation = - Suppliers::insert_many(vec![supplier_01, supplier_02, supplier_03]) - .exec(&db) - .await; - - println!("INSERTED MANY: {:?}", supplier_insert_operation?); - -+ let who_supplies = Suppliers::find().find_with_related(Fruits).all(&db).await?; -+ dbg!(&who_supplies); - - Ok(()) - -} -``` - -The operation returns a `Vec` which contains a tuple `(Model, Vec) ` which is `Vec<(Model, Vec)>`. This means that the first `Model` , `tuple.0` is the `Model` that has relationships with the other `Model`s in the `tuple.1` index which is `Vec` . - -Running the program, prints: - -```sh -$ -[ - ( - Model { - supplier_id: 1, - supplier_name: "John Doe", - fruit_id: 1, - }, - [ - Model { - fruit_id: 1, - name: "Apple", - datetime_utc: 2022-01-26T09:16:43, - unit_price: 2, - sku: "FM2022AKB40", - }, - ], - ), - ( - Model { - supplier_id: 2, - supplier_name: "Jane Doe", - fruit_id: 2, - }, - [ - Model { - fruit_id: 2, - name: "Banana", - datetime_utc: 2022-01-26T09:16:43, - unit_price: 2, - sku: "FM2022AKB41", - }, - ], - ), - ( - Model { - supplier_id: 3, - supplier_name: "Junior Doe", - fruit_id: 3, - }, - [ - Model { - fruit_id: 3, - name: "Pineapple", - datetime_utc: 2022-01-26T09:16:43, - unit_price: 10, - sku: "FM2022AKB42", - }, - ], - ), -] -``` - ---- - -Thats SeaORM in action. A beginner friendly ORM, one codebase for MySQL, SQLite, MariaDB and PostgreSQL. What else could you ask for :) diff --git a/tutorials-book/src/ch01-07-mock-testing.md b/tutorials-book/src/ch01-07-mock-testing.md new file mode 100644 index 0000000..7531996 --- /dev/null +++ b/tutorials-book/src/ch01-07-mock-testing.md @@ -0,0 +1,163 @@ +# Testing with Mock Interface + +In some cases, we want to verify the application logic without using a real database. As such, SeaORM provides a `MockDatabase` interface to be used in development. + +For example, we don't want to set up and use a real database for unit testing because the database layer should be independent of the application logic layer. Using a mock interface provides stable and correct behavior in the database layer, hence any errors that emerge can only be due to bugs in the application logic layer. + +Also, a real database may not be preferred when we want to maximize the portability of the development environment. Using a mock interface effective takes away the need for setting up and maintaining a real database, therefore application logic developers can do their work virtually anywhere. + +## Add the `mock` Cargo feature + +```diff +// Cargo.toml + +... + +- sea-orm = { version = "0.8.0", features = [ ... ] } ++ sea-orm = { version = "0.8.0", features = [ ... , "mock" ] } + +... +``` + +## Define the expected query results + +First, we define what we want our mock database to return. + +Note that the function `append_query_results()` takes a vector of vectors, where each vector nested inside represent the result of a single query. + +```rust, no_run +let db: &DatabaseConnection = &MockDatabase::new(DatabaseBackend::MySql) + .append_query_results(vec![ + // First query result + vec![bakery::Model { + id: 1, + name: "Happy Bakery".to_owned(), + profit_margin: 0.0, + }], + // Second query result + vec![ + bakery::Model { + id: 1, + name: "Happy Bakery".to_owned(), + profit_margin: 0.0, + }, + bakery::Model { + id: 2, + name: "Sad Bakery".to_owned(), + profit_margin: 100.0, + }, + bakery::Model { + id: 3, + name: "La Boulangerie".to_owned(), + profit_margin: 17.89, + }, + ], + ]) + .append_query_results(vec![ + // Third query result + vec![ + baker::Model { + id: 1, + name: "Jolie".to_owned(), + contact_details: None, + bakery_id: 3, + }, + baker::Model { + id: 2, + name: "Charles".to_owned(), + contact_details: None, + bakery_id: 3, + }, + baker::Model { + id: 3, + name: "Madeleine".to_owned(), + contact_details: None, + bakery_id: 3, + }, + baker::Model { + id: 4, + name: "Frederic".to_owned(), + contact_details: None, + bakery_id: 3, + }, + ] + ]) + .into_connection(); +``` + +*Note: if a query result contains multiple models (like the second and third ones above) and `Entity::find().one(db)` is called, only the first one will be returned. The rest of the models in the query will be discarded.* + +## Use the returned query results + +Then the query results can be mocked and passed to other parts of the application logic. + +```rust, no_run +let happy_bakery: Option = Bakery::find().one(db).await?; +assert_eq!( + happy_bakery.unwrap(), + bakery::Model { + id: 1, + name: "Happy Bakery".to_owned(), + profit_margin: 0.0, + } +); + +let all_bakeries: Vec = Bakery::find().all(db).await?; +assert_eq!( + all_bakeries, + vec![ + bakery::Model { + id: 1, + name: "Happy Bakery".to_owned(), + profit_margin: 0.0, + }, + bakery::Model { + id: 2, + name: "Sad Bakery".to_owned(), + profit_margin: 100.0, + }, + bakery::Model { + id: 3, + name: "La Boulangerie".to_owned(), + profit_margin: 17.89, + }, + ] +); + +let la_boulangerie_bakers: Vec = Baker::find().all(db).await?; +assert_eq!( + la_boulangerie_bakers, + vec![ + baker::Model { + id: 1, + name: "Jolie".to_owned(), + contact_details: None, + bakery_id: 3, + }, + baker::Model { + id: 2, + name: "Charles".to_owned(), + contact_details: None, + bakery_id: 3, + }, + baker::Model { + id: 3, + name: "Madeleine".to_owned(), + contact_details: None, + bakery_id: 3, + }, + baker::Model { + id: 4, + name: "Frederic".to_owned(), + contact_details: None, + bakery_id: 3, + }, + ] +); +``` + +## Mock execution results + +To mock the results of CRUD operations, we can use `append_exec_results()`. + +As it is highly similar to the above, it won't be covered in detail in this tutorial. For more information, refer to the [documentation](https://www.sea-ql.org/SeaORM/docs/write-test/mock/#mocking-execution-result). diff --git a/tutorials-book/src/ch01-08-sql-with-sea-query.md b/tutorials-book/src/ch01-08-sql-with-sea-query.md new file mode 100644 index 0000000..7ddf2cc --- /dev/null +++ b/tutorials-book/src/ch01-08-sql-with-sea-query.md @@ -0,0 +1,96 @@ +# Optional: Building SQL Queries with SeaQuery + +If you prefer the flexibility of SQL, you can use [SeaQuery](https://crates.io/crates/sea-query) to build SQL-like statements for any queries or operations. + +SeaQuery is built-in for SeaORM, so no extra setup is required. + +## Insert statements + +Raw SQL: + +```sql +INSERT INTO `bakery` (`name`, `profit_margin`) VALUES ('SQL Bakery', -100) +``` + +SeaQuery: + +```rust, no_run +use sea_query::{Alias, Query}; + +let columns: Vec = ["name", "profit_margin"] + .into_iter() + .map(Alias::new) + .collect(); + +let mut stmt = Query::insert(); +stmt.into_table(bakery::Entity).columns(columns); + +// Invoke `values_panic()` for each row +stmt.values_panic(["SQL Bakery".into(), (-100.0).into()]); + +let builder = db.get_database_backend(); +db.execute(builder.build(&stmt)).await?; +``` + +## Select statements + +Raw SQL: + +```sql +SELECT `baker`.`name` FROM `baker` JOIN `bakery` ON `baker`.`bakery_id` = `bakery`.`id` ORDER BY `baker`.`name` ASC +``` + +SeaQuery: + +If you are only interested in some of the columns, define a struct to hold the query result. It has to derive from the trait `FromQueryResult`. + +If all columns are of interest, then the generated `Model` structs (e.g. `baker::Model`) can be used. + +The fields of the struct must match the column names of the query result. + +```rust, no_run +use sea_query::{Alias, Expr, JoinType, Order, Query}; + +#[derive(FromQueryResult)] +struct BakerNameResult { + name: String, +} + +... + +let column = (baker::Entity, Alias::new("name")); + +let mut stmt = Query::select(); +stmt.column(column.clone()) // Use `expr_as` instead of `column` if renaming is necessary + .from(baker::Entity) + .join( + JoinType::Join, + bakery::Entity, + Expr::tbl(baker::Entity, Alias::new("bakery_id")) + .equals(bakery::Entity, Alias::new("id")), + ) + .order_by(column, Order::Asc); + +let builder = db.get_database_backend(); +let baker = BakerNameResult::find_by_statement(builder.build(&stmt)) + .all(db) + .await?; + +let baker_names = baker.into_iter().map(|b| b.name).collect::>(); + +assert_eq!( + baker_names, + vec!["Charles", "Frederic", "Jolie", "Madeleine"] +); +``` + +## Testing and Debugging + +It's often useful to check the raw SQL of the SeaQuery-generated statements. + +Use `stmt.to_string(query_builder)` to do that. + +```rust, no_run +// Check the raw SQL of `stmt` in MySQL syntax +println!({}, stmt.to_string(MysqlQueryBuilder)); +``` diff --git a/tutorials-book/src/ch02-00-integration-with-rocket.md b/tutorials-book/src/ch02-00-integration-with-rocket.md new file mode 100644 index 0000000..fd22084 --- /dev/null +++ b/tutorials-book/src/ch02-00-integration-with-rocket.md @@ -0,0 +1,9 @@ +# Chapter 2 - Integration with Rocket + +In Chapter 1, we've explored how to interact with a database in Rust. + +In real applications, however, we'd probably want to expose those operations in a Web API for generic usage. + +To achieve that, we leverage web frameworks like [Rocket](https://rocket.rs/). + +You'll soon discover how simple and painless it is to use SeaORM in a Rocket application! diff --git a/tutorials-book/src/ch02-00-todo-app-getting-started.md b/tutorials-book/src/ch02-00-todo-app-getting-started.md deleted file mode 100644 index 0c1900b..0000000 --- a/tutorials-book/src/ch02-00-todo-app-getting-started.md +++ /dev/null @@ -1,47 +0,0 @@ -# Chapter 2 - A Command-line TODO app - -A simple TODO app that demostrates using SeaORM, SQLite and Postgres to build a simple TODO application. This tutorial will simulate building an app with a local SQLite cache and remote storage of the contents of the cache using a HTTP server with a PostgreSQL backend. - -First, install PostgreSQL and SQLite and ensure PostgreSQL server is running. - -### Initializing the project directory - -A cargo workspace make development easier and share the building environment. The `HTTP` TODO client will be called `client` and the `HTTP server` will be called `server`. - -#### Initialize the `client` and `server` - -Create the workspace directory `todo-app` - -```sh -$ mkdir todo-app -``` - -Then switch to the workspace directory - -```sh -$ cd todo-app -``` - -Create the `client` and `server` projects - -```sh -$ cargo new client -``` - -```sh -$ cargo new server -``` - -Create a `Cargo.toml` in the root of the workspace directory to register the two projects - -`File: todo-app/Cargo.toml` - -```toml -[workspace] -members = [ - "client", - "server", -] -``` - -Next up is building the Web Server. diff --git a/tutorials-book/src/ch02-01-http-server.md b/tutorials-book/src/ch02-01-http-server.md deleted file mode 100644 index fe3e477..0000000 --- a/tutorials-book/src/ch02-01-http-server.md +++ /dev/null @@ -1,174 +0,0 @@ -# Building The Web Server - -### HTTP as the Protocol - -The client and server need a structured way to communicate with each other. HTTP will be the protocol chosen for this tutorial using simple `GET` and `POST`. - -## Install necessary dependencies - - - Switch to the `todo-app/server` directory to build the Web Server - - ```sh - $ cd server - ``` - - - Ensure you have installed Rust programming language [https://www.rust-lang.org/tools/install](https://www.rust-lang.org/tools/install) - - - Ensure you have `sea-orm-cli` installed [https://crates.io/crates/sea-orm-cli](https://crates.io/crates/sea-orm-cli) - - - `tokio` will be used as the async library used as it integrates well with `axum` which is the `HTTP framework` used - - ```sh - $ cargo add tokio --features full - ``` - - This adds `tokio` to `Cargo.toml` file - - - ```toml - [package] - name = "server" - version = "0.1.0" - edition = "2021" - - [dependencies] - + tokio = { version = "1.17.0", features = ["attributes"] } - ``` - - - Add `anyhow` crate for error handling, `axum` crate for HTTP handling, `dotenv` for fetching environment variables and `once_cell` to allow global access to the database connection `sea_orm::DatabaseConnection`. - - ```sh - $ cargo add anyhow axum dotenv once_cell - ``` - - An entry in the `Cargo.toml` file is added - - ```toml - [package] - name = "server" - version = "0.1.0" - edition = "2021" - - [dependencies] - + anyhow = "1.0.53" - + axum = "1.0.0" - + dotenv = "0.15.0" - + once_cell = "1.10.0" - tokio = { version = "1.17.0", features = ["full"] } - ``` - - - Add `serde` with the features to `derive` - - ```sh - $ cargo add serde --features derive - ``` - - This will allow deserialization of `JSON` requests from the client. `serde` is now added to `Cargo.toml` file. - - ```toml - [package] - name = "server" - version = "0.1.0" - edition = "2021" - - [dependencies] - anyhow = "1.0.53" - axum = "1.0.0" - dotenv = "0.15.0" - once_cell = "1.10.0" - + serde = { version = "1.0.136", features = ["derive"] } - tokio = { version = "1.17.0", features = ["full"] } - - ``` - - - - - Add `sea-orm` with the features to enable sql drivers for PostgreSQL backend - - ```sh - $ cargo add sea-orm --no-default-features --features "runtime-tokio-rustls sqlx-postgres macros" - ``` - - This adds sea-orm to `Cargo.toml` - - - ```toml - [package] - name = "server" - version = "0.1.0" - edition = "2021" - - [dependencies] - anyhow = "1.0.53" - axum = "1.0.0" - dotenv = "0.15.0" - once_cell = "1.10.0" - serde = { version = "1.0.136", features = ["derive"] } - tokio = { version = "1.17.0", features = ["full"] } - - + sea-orm = { version = "0.6.0", features = [ - + "runtime-tokio-rustls", - + "sqlx-postgres", - + "macros", - + ], default-features = false } - ``` - Change the main function to async function for integration with `tokio` and using `anyhow` crate to handle and propagate the errors. - - ```rust,no_run,noplayground - - fn main() { - - println!("Hello, world!"); - - } - - + #[tokio::main] - + async fn main() -> anyhow::Result<()> { - + Ok(()) - + } - ``` - -## Creating a new user and database - - - Login to Postgres database and create a new user and database - - ```sh - $ sudo -u postgres psql postgres - ``` - - - Create a new user in the PostgreSQL prompt. - - ```sh - postgres=# CREATE ROLE webmaster LOGIN PASSWORD 'master_char'; - ``` - - - Create the `fruits_market` database and assign it to the `webmaster` user - - ```sh - postgres=# CREATE DATABASE fruits_market WITH OWNER = webmaster; - ``` - -## Configuring the database environment - - - Create a `.env` file in the workspace directory - - The file structure should look - - ```sh - todo-app - |-- Cargo.toml - |-- server - |-- src - |-- Cargo.toml - + |-- .env - |-- client - ``` - - - Configure the database environment by editing the `.env` file - - File: `todo-app/server/.env` - - ```sh - + DATABASE_URL=postgres://webmaster:master_char@localhost/fruits_market - ``` - - - - Next, we will create all the required tables and their relationships diff --git a/tutorials-book/src/ch02-01-project-setup.md b/tutorials-book/src/ch02-01-project-setup.md new file mode 100644 index 0000000..cb9e71a --- /dev/null +++ b/tutorials-book/src/ch02-01-project-setup.md @@ -0,0 +1,55 @@ +# Create a Rocket project + +Create a new binary crate: + +```sh +$ cargo new rocket-example --bin +$ cd rocket-example +``` + +Add [`rocket`](https://crates.io/crates/rocket) as a dependency: + +```diff +# Cargo.toml + +... + +[dependencies] ++ rocket = { version = "^0.5.0-rc.2", features = ["json"] } + +... + +``` + +*Pay attention to the version that you're using. `rocket` and/or its dependencies may not compile on the stable build of the Rust compiler if an early version of `rocket` is used.* + +The following should compile and run: + +```rust, no_run +// src/main.rs + +use rocket::*; + +#[get("/")] +async fn index() -> &'static str { + "Hello, bakeries!" +} + +#[launch] // The "main" function of the program +fn rocket() -> _ { + rocket::build().mount("/", routes![index]) +} + +``` + +To verify it works: + +```sh +$ cargo run +``` + +``` +GET localhost:8000/ + +"Hello, bakeries!" +``` diff --git a/tutorials-book/src/ch02-02-connect-to-database.md b/tutorials-book/src/ch02-02-connect-to-database.md new file mode 100644 index 0000000..f64e8b2 --- /dev/null +++ b/tutorials-book/src/ch02-02-connect-to-database.md @@ -0,0 +1,121 @@ +# Connect to Database + +First, we define a function to help us create a database and/or connect to it. + +It is basically the same as in [Section 1.1](ch01-01-project-setup.html#creating-a-database). + +```rust, no_run +// src/setup.rs + +use sea_orm::*; + +// Replace with your database URL +const DATABASE_URL: &str = "mysql://root:root@localhost:3306"; + +pub(super) async fn set_up_db() -> Result { + let db = Database::connect(DATABASE_URL).await?; + + // Replace with your desired database name + let db_name = "bakeries_db"; + let db = match db.get_database_backend() { + DbBackend::MySql => { + db.execute(Statement::from_string( + db.get_database_backend(), + format!("CREATE DATABASE IF NOT EXISTS `{}`;", db_name), + )) + .await?; + + let url = format!("{}/{}", DATABASE_URL, db_name); + Database::connect(&url).await? + } + DbBackend::Postgres => { + db.execute(Statement::from_string( + db.get_database_backend(), + format!("DROP DATABASE IF EXISTS \"{}\";", db_name), + )) + .await?; + db.execute(Statement::from_string( + db.get_database_backend(), + format!("CREATE DATABASE \"{}\";", db_name), + )) + .await?; + + let url = format!("{}/{}", DATABASE_URL, db_name); + Database::connect(&url).await? + } + DbBackend::Sqlite => db, + }; + + Ok(db) +} +``` + +We instruct Rocket to manage the database connection as a [state](https://rocket.rs/v0.5-rc/guide/state/#state). + +```rust, no_run +// src/main.rs + +#[launch] +fn rocket() -> _ { ++ let db = match set_up_db().await { ++ Ok(db) => db, ++ Err(err) => panic!("{}", err), ++ }; + + rocket::build() ++ .manage(db) + .mount("/", routes![index, bakeries]) +} +``` + +The database connection can then be accessed and used as in [previous sections](ch01-05-basic-crud-operations.md). + +```rust, no_run +// src/main.rs + +use rocket::serde::json::Json; + +... + +#[get("/bakeries")] +async fn bakeries(db: &State) -> Json> { + let db = db as &DatabaseConnection; + + let bakery_names = Bakery::find() + .all(db) + .await + .unwrap() + .into_iter() + .map(|b| b.name) + .collect::>(); + + Json(bakery_names) +} + +... + +#[launch] +fn rocket() -> _ { + rocket::build() + .mount( + "/", + // Don't forget to mount the new endpoint handlers + routes![ + index, ++ bakeries + ] + ) +} +``` + +To verify it works: + +```sh +$ cargo run +``` + +``` +GET localhost:8000/bakeries + +["Bakery Names", "In The", "Database", "If Any"] +``` diff --git a/tutorials-book/src/ch02-02-tables.md b/tutorials-book/src/ch02-02-tables.md deleted file mode 100644 index e6896fd..0000000 --- a/tutorials-book/src/ch02-02-tables.md +++ /dev/null @@ -1,252 +0,0 @@ -# Creating Tables - -First, create a database config for the `sea_orm::DatabaseConnection` to use to connect and authenticate to the PostgreSQL server. - -```rust,no_run,noplayground -+ use once_cell::sync::OnceCell; -+ use sea_orm::{DatabaseConnection, Database}; -+ use dotenv::dotenv; - -+ static DATABASE_CONNECTION: OnceCell = OnceCell::new(); - -#[tokio::main] -async fn main() -> anyhow::Result<()> { - dotenv().ok(); - - // Read the database environment from the `.env` file - let database_url = dotenv::var("DATABASE_URL")?; - let db = Database::connect(database_url).await?; - DATABASE_CONNECTION.set(db).unwrap(); - - Ok(()) -} -``` - -`dotenv::var()` is used to load the configuration `DATABASE_URL` as specified in the `.env` file. This is passed to the `Database::connect()` method in order to create a `sea_orm::DatabaseConnection` which executes queries in the database. The database connection is exported to the global scope using `once_cell` crate as a static global variable - -`static DATABASE_CONNECTION: OnceCell = OnceCell::new();` - -This is later set to the `DatabaseConnection` using ` DATABASE_CONNECTION.set(db).unwrap();`. - -Add the code to create the tables, `todos` and `fruits`. - -**FILE**:***src/main.rs*** - -```rust,no_run,noplayground - use async_std::sync::Arc; -- use sea_orm::{Database, DatabaseConnection}; -+ use sea_orm::{ -+ sea_query::{Alias, ColumnDef, ForeignKey, ForeignKeyAction, Table}, -+ ConnectionTrait, Database, DatabaseConnection, DbBackend, -+ }; - -#[async_std::main] -async fn main() -> anyhow::Result<()> { - -// --- code snippet --- - -//Define the database backend - let db_postgres = DbBackend::Postgres; - - dotenv().ok(); - - // Read the database environment from the `.env` file - let database_url = dotenv::var("DATABASE_URL")?; - let db = Database::connect(database_url).await?; - DATABASE_CONNECTION.set(db).unwrap(); - - // Create the fruits table - let fruits_table = Table::create() - .table(Alias::new("fruits")) - .if_not_exists() - .col( - ColumnDef::new(Alias::new("fruit_id")) - .integer() - .auto_increment() - .primary_key() - .not_null(), - ) - .col( - ColumnDef::new(Alias::new("fruit_name")) - .string() - .unique_key() - .not_null(), - ) - .to_owned(); - - let db = DATABASE_CONNECTION.get().unwrap(); - - // Executing the SQL query to create the `fruits_table` in PostgreSQL - let create_table_op = db.execute(db_postgres.build(&fruits_table)).await; - // Print the result in a user friendly way - println!( - "`CREATE TABLE fruits` {:?}", - match create_table_op { - Ok(_) => "Operation Successful".to_owned(), - Err(e) => format!("Unsuccessful - Error {:?}", e), - } - ); - - // Create the `todos` table - let todos_table = Table::create() - .table(Alias::new("todos")) - .if_not_exists() - .col( - ColumnDef::new(Alias::new("todo_id")) - .integer() - .auto_increment() - .primary_key() - .not_null(), - ) - .col( - ColumnDef::new(Alias::new("username")) - .string() - .unique_key() - .not_null(), - ) - .col(ColumnDef::new(Alias::new("todo_list")).string()) - .to_owned(); - - // Executing the SQL query to create the `todos` table in PostgreSQL - let create_table_op = db.execute(db_postgres.build(&todos_table)).await; - // Print the result in a user friendly way - println!( - "`CREATE TABLE todos` {:?}", - match create_table_op { - Ok(_) => "Operation Successful".to_owned(), - Err(e) => format!("Unsuccessful - Error {:?}", e), - } - ); - - Ok(()) -} -``` - -The previous tutorial gave an introduction on creating tables. `Table::create()` is the method to do this. Then the `db.execute()` method performs the database operation for table creation. - -Next, use `sea-orm-cli` to auto-generate the code for `Entity`, `Model`, `Relation` , etc... - -```sh -$ sea-orm-cli generate entity -o src/todo_list_table -t todos #The todos table - -$ sea-orm-cli generate entity -o src/fruits_list_table -t fruits #The fruits table -``` - -This generates new directories - -```sh -SeaORM-TODO-App - |-- Cargo.toml - |-- .env - |-- src -+ |-- fruits_list_table -+ |-- mod.rs -+ |-- prelude.rs -+ |-- fruits.rs -+ |-- todo_list_table -+ |-- mod.rs -+ |-- prelude.rs -+ |-- todos.rs -``` - -Modify the `src/fruits_list_table/prelude.rs` and import the types using friendly names. - -```rust,no_run,noplayground -- pub use super::fruits::Entity as Fruits; - -+ pub use super::fruits::{ -+ ActiveModel as FruitsActiveModel, Column as FruitsColumn, Entity as Fruits, -+ Model as FruitsModel, PrimaryKey as FruitsPrimaryKey, Relation as FruitsRelation, -+ }; -``` - -Do the same to the `src/todo_list_table/prelude.rs` - -```rust,no_run,noplayground -//! SeaORM Entity. Generated by sea-orm-codegen 0.5.0 -- pub use super::todos::Entity as Todos; - -+ pub use super::todos::{ -+ ActiveModel as TodosActiveModel, Column as TodosColumn, Entity as Todos, Model as TodosModel, -+ PrimaryKey as TodosPrimaryKey, Relation as TodosRelation, -+ }; -``` - -Import these modules into `src/main.rs` - -```rust,no_run,noplayground -// --- code snippet --- -+ mod fruits_list_table; -+ mod todo_list_table; - -+ pub use fruits_list_table::prelude::*; -+ pub use todo_list_table::prelude::*; - -#[async_std::main] -async fn main() -> anyhow::Result<()> { - // --- code snippet --- - - Ok(()) - } -``` - -Next, populate the `fruits` table with a list of fruits. - -Create a new file `src/insert_values.rs` and add the following code: - -```rust,no_run,noplayground -use crate::{Fruits, FruitsActiveModel}; -use sea_orm::{DatabaseConnection, EntityTrait, Set}; - -// Insert suppliers in the `suppliers` table -pub async fn insert_fruits(db: &DatabaseConnection) -> anyhow::Result<()> { - let apple = FruitsActiveModel { - fruit_name: Set("Apple".to_owned()), - ..Default::default() - }; - - let orange = FruitsActiveModel { - fruit_name: Set("Orange".to_owned()), - ..Default::default() - }; - - let mango = FruitsActiveModel { - fruit_name: Set("Mango".to_owned()), - ..Default::default() - }; - - let pineapple = FruitsActiveModel { - fruit_name: Set("Pineapple".to_owned()), - ..Default::default() - }; - - let fruit_insert_operation = Fruits::insert_many(vec![apple, orange, mango, pineapple]) - .exec(db) - .await; - - println!("INSERTED FRUITS: {:?}", fruit_insert_operation?); - - Ok(()) -} - -``` - -Here, `ActiveModel` is used to prepare the data for insertion into the database using `Entity::insert()` . - -Import this module to the `src/main.rs` file and call these functions to perform insert operations - -```rust,no_run,noplayground -// --- code snippet --- -+ mod insert_values; -+ pub use insert_values::*; - -#[async_std::main] -async fn main() -> anyhow::Result<()> { - // --- code snippet --- - -+ insert_fruits(&db).await?; - - Ok(()) -} -``` - diff --git a/tutorials-book/src/ch02-03-error-handling.md b/tutorials-book/src/ch02-03-error-handling.md new file mode 100644 index 0000000..b43a603 --- /dev/null +++ b/tutorials-book/src/ch02-03-error-handling.md @@ -0,0 +1,64 @@ +# Error Handling + +First, define a [custom responder](https://rocket.rs/v0.5-rc/guide/responses/#custom-responders): + +```rust, no_run +// src/main.rs + +#[derive(Responder)] +#[response(status = 500, content_type = "json")] +struct ErrorResponder { + message: String, +} + +// The following impl's are for easy conversion of error types. + +#[allow(clippy::from_over_into)] +impl Into for DbErr { + fn into(self) -> ErrorResponder { + ErrorResponder { + message: self.to_string(), + } + } +} + +#[allow(clippy::from_over_into)] +impl Into for String { + fn into(self) -> ErrorResponder { + ErrorResponder { message: self } + } +} + +#[allow(clippy::from_over_into)] +impl Into for &str { + fn into(self) -> ErrorResponder { + self.to_owned().into() + } +} +``` + +To catch and handle the errors: + +```rust, no_run +// src/main.rs + +#[get("/bakeries")] +async fn bakeries(db: &State) +- -> Json> ++ -> Result>, ErrorResponder> +{ + let db = db as &DatabaseConnection; + + let bakery_names = Bakery::find() + .all(db) + .await +- .unwrap() ++ .map_err(Into::into)? + .into_iter() + .map(|b| b.name) + .collect::>(); + +- Json(bakery_names) ++ Ok(Json(bakery_names)) +} +``` diff --git a/tutorials-book/src/ch02-03-server.md b/tutorials-book/src/ch02-03-server.md deleted file mode 100644 index 412638e..0000000 --- a/tutorials-book/src/ch02-03-server.md +++ /dev/null @@ -1,279 +0,0 @@ -# Building Server Connections and Responses - -Create a new file in the `src` folder called `routing.rs`. - -Then register the module to the `src.main.rs` file - -```rust,no_run,noplayground -// -- code snippet -- - -mod routing; -pub use routing::*; - -#[async_std::main] -async fn main() -> anyhow::Result<()> { - // -- code snippet -- - - Ok(()) -} -``` - - - -#### The HTTP API - -The HTTP API will take the following routes : - -`/` route to indicate that the server is online. - -`/fruits` to fetch a list of all the fruits in the database. - -`/store` to insert a `username` and `todo_list` in the database. - -`/update_todo` to perform an update to the `todo_list`. - -Create these routes in the `connection.rs` file. - -`File: todo-app/src/connection.rs` - -```rust,no_run,noplayground -use crate::{Fruits, Todos, TodosActiveModel, TodosColumn, DATABASE_CONNECTION}; -use axum::{http::StatusCode, response::IntoResponse, Json}; -use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set}; -use serde::Deserialize; - -#[derive(Deserialize, Debug)] -pub struct Store { - username: String, - todo_list: String, -} - -``` - -Here, the `Store` struct is used to handle all incoming JSON data in a `POST` request. It deserializes the `username` and `todo_list` from a JSON string. - -#### Responsders - -The `/` route will be handled by the function `root()` - -```rust,no_run.noplayground -pub async fn root() -> &'static str { - "Remote PostgreSQL Server Online!" -} -``` - -The `/fruits` route will be handled by the function `get_fruits()`. - -```rust,no_run,noplayground -pub async fn get_fruits() -> impl IntoResponse { - let db = DATABASE_CONNECTION.get().unwrap(); - - match Fruits::find().all(db).await { - Ok(fruit_models) => { - let fruits = fruit_models - .iter() - .map(|fruit_model| fruit_model.fruit_name.clone()) - .collect::>(); - - (StatusCode::ACCEPTED, Json(fruits)) - } - Err(error) => ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(vec![error.to_string()]), - ), - } -} -``` - -This function finds all the `fruit model`s in the `fruits` table, iterates over those models if any is found and takes the `fruit_name`s field of the model collecting them into a `Vec` , parses them into a JSON string and returns it as the result together with status code `201` from the `StatusCode::ACCEPTED` enum from the `http` crate re-exported by `axum`. In case of an error from the database, the error is converted into a JSON string using `Json(vec![error.to_string()]),` and returned together with a status code of `StatusCode::INTERNAL_SERVER_ERROR` of the `http` crate. This is a HTTP error `500`. - -Next, is the `insert` and `update` operations handled by the `store_todo()` and `update_todo()` functions respectively. - -```rust,no_run,noplayground - -pub async fn store_todo(Json(payload): Json) -> impl IntoResponse { - let db = DATABASE_CONNECTION.get().unwrap(); - - let todo_user = TodosActiveModel { - username: Set(payload.username.to_owned()), - todo_list: Set(Some(payload.todo_list.to_owned())), - ..Default::default() - }; - - match Todos::insert(todo_user).exec(db).await { - Ok(_) => (StatusCode::ACCEPTED, Json("INSERTED".to_owned())), - Err(error) => (StatusCode::INTERNAL_SERVER_ERROR, Json(error.to_string())), - } -} - -pub async fn update_todo(Json(payload): Json) -> impl IntoResponse { - let db = DATABASE_CONNECTION.get().unwrap(); - - match Todos::find() - .filter(TodosColumn::Username.contains(&payload.username)) - .one(db) - .await - { - Ok(found_model) => { - if let Some(model) = found_model { - let mut todo_model: TodosActiveModel = model.into(); - todo_model.todo_list = Set(Some(payload.todo_list.to_owned())); - match todo_model.update(db).await { - Ok(_) => (StatusCode::NO_CONTENT, Json("UPDATED_TODO".to_owned())), - Err(error) => (StatusCode::INTERNAL_SERVER_ERROR, Json(error.to_string())), - } - } else { - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json("MODEL_NOT_FOUND".to_owned()), - ) - } - } - - Err(error) => (StatusCode::INTERNAL_SERVER_ERROR, Json(error.to_string())), - } -} -``` - -This functions are similar and have the same structure as the `get_fruits()` function in terms of their generated results and the error handling. However, the `store_todo()` function responds with the JSON string `INSERTED` while the `update_todo()` function responds with `UPDATED_TODO` JSON string in case of a successful update or a `MODEL_NOT_FOUND` JSON string in case the `username` that is being updated does not exist. - -Now that we have established how the API will be access and perform database operations, add the code to the source file. - -`File: todo-app/src/routing.rs` - -```rust,no_run,noplayground -use crate::{Fruits, Todos, TodosActiveModel, TodosColumn, DATABASE_CONNECTION}; -use axum::{http::StatusCode, response::IntoResponse, Json}; -use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set}; -use serde::Deserialize; - -#[derive(Deserialize, Debug)] -pub struct Store { - username: String, - todo_list: String, -} - -#[derive(Deserialize, Debug)] -pub struct GetUser { - username: String, -} - -pub async fn root() -> &'static str { - "Remote PostgreSQL Server Online!" -} - -pub async fn get_fruits() -> impl IntoResponse { - let db = DATABASE_CONNECTION.get().unwrap(); - - match Fruits::find().all(db).await { - Ok(fruit_models) => { - let fruits = fruit_models - .iter() - .map(|fruit_model| fruit_model.fruit_name.clone()) - .collect::>(); - - (StatusCode::ACCEPTED, Json(fruits)) - } - Err(error) => ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(vec![error.to_string()]), - ), - } -} - -pub async fn store_todo(Json(payload): Json) -> impl IntoResponse { - let db = DATABASE_CONNECTION.get().unwrap(); - - let todo_user = TodosActiveModel { - username: Set(payload.username.to_owned()), - todo_list: Set(Some(payload.todo_list.to_owned())), - ..Default::default() - }; - - match Todos::insert(todo_user).exec(db).await { - Ok(_) => (StatusCode::ACCEPTED, Json("INSERTED".to_owned())), - Err(error) => (StatusCode::INTERNAL_SERVER_ERROR, Json(error.to_string())), - } -} - -pub async fn update_todo(Json(payload): Json) -> impl IntoResponse { - let db = DATABASE_CONNECTION.get().unwrap(); - - match Todos::find() - .filter(TodosColumn::Username.contains(&payload.username)) - .one(db) - .await - { - Ok(found_model) => { - if let Some(model) = found_model { - let mut todo_model: TodosActiveModel = model.into(); - todo_model.todo_list = Set(Some(payload.todo_list.to_owned())); - match todo_model.update(db).await { - Ok(_) => (StatusCode::NO_CONTENT, Json("UPDATED_TODO".to_owned())), - Err(error) => (StatusCode::INTERNAL_SERVER_ERROR, Json(error.to_string())), - } - } else { - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json("MODEL_NOT_FOUND".to_owned()), - ) - } - } - - Err(error) => (StatusCode::INTERNAL_SERVER_ERROR, Json(error.to_string())), - } -} - -``` - -Lastly, add the routes to the `main.rs` file. - -`File: todo-app/src/main.rs` - -```rust,no_run,noplayground - use dotenv::dotenv; - use once_cell::sync::OnceCell; - use sea_orm::{ - sea_query::{Alias, ColumnDef, Table}, - ConnectionTrait, Database, DatabaseConnection, DbBackend, - }; -+ use std::net::SocketAddr; -+ use axum::{ -+ routing::{get, post}, -+ Router, -+ }; - -#[tokio::main] -async fn main() { -+ let app = Router::new() -+ .route("/", get(root)) -+ .route("/fruits", get(get_fruits)) -+ .route("/get_user", post(get_user)) -+ .route("/store", post(store_todo)) -+ .route("/update_todo", post(update_todo)); - -+ let addr = SocketAddr::from(([127, 0, 0, 1], 8080)); -+ println!("listening on http://{}", addr); -+ axum::Server::bind(&addr) -+ .serve(app.into_make_service()) -+ .await?; - - Ok(()) -} - -``` - - - -Run the program using `cargo run`. It print the following to the terminal - -```sh -$ Running `/media/su43/IGIED-01/Rust-Projects/SeaQL/todo-app/target/debug/server` -`CREATE TABLE fruits` "Operation Successful" -`CREATE TABLE todos` "Operation Successful" -INSERTED FRUITS: InsertResult { last_insert_id: 1 } -``` - -The server is now listening on `127.0.0.1:8080` for incoming `HTTP requests`. - -Next, we build the client. diff --git a/tutorials-book/src/ch02-04-00-client.md b/tutorials-book/src/ch02-04-00-client.md deleted file mode 100644 index be7309f..0000000 --- a/tutorials-book/src/ch02-04-00-client.md +++ /dev/null @@ -1,227 +0,0 @@ -# Building The HTTP Client - -This chapter focuses on creating the HTTP client. Switch to the `client` directory in the workspace. - -#### Configuration - -Add the necessary dependencies to create the client. - -```sh -$ cargo add tokio --features full - -$ cargo add anyhow - -$ cargo add serde --features derive - -$ cargo add serde_json - -$ cargo add minreq - -$ cargo add dotenv - -$ cargo add json -``` - -`serde_json` crate will deserialize the TODO list data structure that contains queued and completed TODOs into a JSON string for remote storage in the PostgreSQL database. The `json` crate will serialize JSON data to be sent over HTTP using the `minreq` crate. - -The todo client will also store local cache, simulating a real world setup especially for a desktop or mobile client. SQLite will be the preferred database for this tutorial due to it's popularity. A command line frontend will be used to keep the tutorial simple and easy to port to other domains like mobile device frameworks, desktop clients. - -Add `sea-orm` crate with the SQLite features enabled for the local persistent cache. The runtime features `runtime-tokio-rustls` are used since the async library for this client is `tokio` crate. - -```sh -$ cargo add sea-orm --features "runtime-tokio-rustls sqlx-sqlite macros" --no-default-features -``` - -Modify the main function in `src/main.rs` to use async-std - -```rust,no_run,noplayground -- fn main() { -- println!("Hello, world!"); -- } - -+ #[tokio::main] -+ async fn main() -> anyhow::Result<()>{ -+ Ok(()) -+ } -``` - -Next, create a `.env` file in the current directory. This will contain the database configuration. - -`File: TODO-Client/.env` - -```sh -DATABASE_URL=sqlite://my_todos.db -``` - -Here, the `sqlite` URL does not take a `username`, `password` and `IP` since SQLite does not have have a server, just the database name `my_todos.db`. - -Create an empty SQLite database using the command: - -```sh -$ sqlite3 my_todos.db "VACUUM;" -``` - -The `"VACUUM;"` part of the command will ensure the created database is not just held in memory but also persisted to the file system even though it is empty. - -#### Local SQLite Database Operations - -Top perform local database operations, create a file `src/db_ops.rs` which will contain functions to perform database operations. - -To serialize and deserialize the SQLite cache for the in-memory database, the struct `TodoList` is used: - -```rust,no_run,noplayground -// The structure for a TodoList -#[derive(Debug, Serialize, Default, Deserialize)] -pub struct TodoList { - pub queued: Vec, - pub completed: Vec, -} -``` - -This data structure holds the completed TODOs in the `completed` field and the incompleted TODOs in the `queued` field. Both of this fields hold a `Vec, - pub completed: Vec, -} - - -pub async fn create_todo_table(db: &DatabaseConnection) -> anyhow::Result<()> { - let database_backend = db.get_database_backend(); - // Create the `todos` table - let todos_table = Table::create() - .table(Alias::new("todo_list")) - .if_not_exists() - .col( - ColumnDef::new(Alias::new("todo_id")) - .integer() - .primary_key() - .not_null() - .auto_increment(), - ) - .col( - ColumnDef::new(Alias::new("todo_name")) - .string() - .unique_key() - .not_null(), - ) - .col(ColumnDef::new(Alias::new("quantity")).string().not_null()) - .col(ColumnDef::new(Alias::new("status")).boolean().not_null()) - .to_owned(); - let create_table_op = db.execute(database_backend.build(&todos_table)).await; - - // Executing the SQL query to create the `todos` table in SQLite - let create_table_op = db.execute(database_backend.build(&todos_table)).await; - // Print the result in a user friendly way - println!( - "`CREATE TABLE todo_list` {:?}", - match create_table_op { - Ok(_) => "Operation Successful".to_owned(), - Err(e) => format!("Unsuccessful - Error {:?}", e), - } - ); - - Ok(()) -} - -``` - -To use the `dotenv` crate to read the `DATABASE_URL` environment variable, add the following code to `src/main.rs`. - -`File: client/src/main.rs` - -```rust,no_run.noplayground -+ use dotenv::dotenv; -+ use sea_orm::Database; - -// -- code snippet -- -#[tokio::main] -async fn main() -> anyhow::Result<()>{ - -+ dotenv().ok(); - - // Read the database environment from the `.env` file -+ let database_url = dotenv::var("DATABASE_URL")?; -+ let db = Database::connect(database_url).await?; - Ok(()) -} -``` - -Then import the `db_ops` module into `src/main.rs` and call both functions. - -```rust,no_run,noplayground -// -- code snippet -- - -+ mod db_ops; -+ pub use db_ops::*; - -#[async_std::main] -async fn main() -> anyhow::Result<()> { - // -- code snippet -- - - // Read the database environment from the `.env` file - let database_url = dotenv::var("DATABASE_URL")?; - let db = Database::connect(database_url).await?; - -+ create_todo_table(&db).await?; - - Ok(()) -} - -``` - -Next is to auto-generate the `Model`, `ActiveModel` , `Entity`, etc... using the `sea-orm-cli` and pass in `--with-serde both` feature flag to auto-generate `serde::Serialize` and `serde::Deserialize` on the Entity. - -```sh -$ sea-orm-cli generate entity -o src/todo_list_table -t todo_list --with-serde both -``` - -This will create a new directory `todo_list_table` in the `src/` directory. - -Open the `src/todo_list_table/prelude.rs` file and import the `Entity`, `Model` and `ActiveModel` using friendly names. - -`File:src/todo_list_table/prelude.rs` - -```rust,no_run,noplayground -//! SeaORM Entity. Generated by sea-orm-codegen 0.6.0 - -- pub use super::todo_list::Entity as TodoList; - -+ pub use super::todo_list::{ -+ ActiveModel as MyTodosActiveModel, Column as MyTodosColumn, Entity as MyTodos, -+ Model as MyTodosModel, PrimaryKey as MyTodosPrimaryKey, Relation as MyTodosRelation, -+ }; - -``` - -Import the modules to the `src/main.rs` file - -```rust,no_run,noplayground - mod db_ops; -+ mod todo_list_table; - - pub use db_ops::*; -+ pub use todo_list_table::prelude::*; - -#[async_std::main] -async fn main() -> anyhow::Result<()> { - let db = database_config().await?; - create_todo_table(&db).await?; - - Ok(()) -} -``` diff --git a/tutorials-book/src/ch02-04-01-utils.md b/tutorials-book/src/ch02-04-01-utils.md deleted file mode 100644 index e47c1eb..0000000 --- a/tutorials-book/src/ch02-04-01-utils.md +++ /dev/null @@ -1,186 +0,0 @@ -# Formatting Utilities - -To create a better user experience, data will be formatted before it is displayed to the command line on `stdout`. - -#### Overview of Code Formatting - -Create a `utils.rs` file which will hold the utilities and add the following code blocks: - -`File: src/utils.rs` - -```rust,no_run,noplayground -use crate::MyTodosModel; -use tokio::sync::Mutex; -use std::collections::HashMap; - -pub(crate) const TITLE: &str = "FRUITS AVAILABLE"; -pub(crate) const NUMBER: &str = "No."; -pub(crate) const ADD_COMMAND: &str = "ADD"; -pub(crate) const DONE_COMMAND: &str = "DONE"; -pub(crate) const UNDO_COMMAND: &str = "UNDO"; -pub(crate) const EDIT_COMMAND: &str = "EDIT"; -pub(crate) const EXIT_COMMAND: &str = "EXIT"; - - -const DONE: &str = "DONE TODOS"; -const NOT_DONE: &str = "NOT DONE"; -const QUANTITY: &str = "QUANTITY"; - -pub(crate) type MemDB = Mutex>; - -pub fn clear_terminal() { - print!("\x1B[2J\x1B[1;1H"); -} - -pub fn synching() { - clear_terminal(); - println!("SYNCING TO DATABASE..."); -} -pub fn synching_to_server() { - println!("SYNCING TO SERVER..."); -} - -pub fn loading() { - clear_terminal(); - println!("LOADING FROM DATABASE..."); -} - -pub fn convert_case(word: &str) -> String { - let word = word.to_lowercase(); - let mut chars = word - .chars() - .map(|character| character.to_string()) - .collect::>(); - - chars[0] = chars[0].to_uppercase().to_string(); - - chars.into_iter().collect::() -} - -pub fn split_words(user_input: String) -> Vec { - user_input - .split(" ") - .map(|word| word.to_owned()) - .collect::>() -} - -``` - -The `TITLE` and `NUMBER` constants are used to format the headings for the `fruits` table which displays the list of fruits on the command-line interface. The constants `DONE`, `NOT_DONE` and `QUANTITY` are used as the headings of the TODO list. - -#### Interaction Commands - -To interact with the client, a user will input a command, similar to pressing a button in a GUI or any other GUI event that performs an operation based on user input. The current list of commands are: - -The `ADD_COMMAND` constant holds the `ADD` command. This command allows a user to `queue` a task in the TODO list. The format is `ADD QUANTITY_IN_KG FRUIT_NAME`. - -The `DONE_COMMAND` constant holds the `DONE` command. This command allows a user to mark a task as `completed` in the TODO list. The format is `DONE FRUIT_NAME`. - -The `UNDO_COMMAND` constant holds the `UNDO` command. This command allows a user to move a completed task back into the `queue` in the TODO list. The format is `UNDO FRUIT_NAME`. - -The `EDIT_COMMAND` constant holds the `EDIT` command. This command allows a user to `modify` a task in the TODO list by changing it's `quantity`. The format is `EDIT QUANTITY_IN_KG FRUIT_NAME`. - -The `EXIT_COMMAND` constant holds the `EXIT` command. This command allows a user to `exit` the client gracefully and sync the local database cache with the remote PostgreSQL server. The format is `EXIT `. - - - -#### Word formating - -A number of functions are presented in the code block above: - -`clear_terminal()` is used to clear the terminal using the command line specific flags `\x1B[2J\x1B[1;1H` - -`synching()` is used to show that the TODO list is being synced to the local SQLite database cache. - -`synching_to_server()` is used to show that the TODO list is being synced to the remote PostgreSQL database using the HTTP API built in the previous chapter. - -`loading()` is used to show that information about the user is being fetched from the remote PostgreSQL database. - -`convert_case()` is used to format the `fruit` name to `Title Case`, for example, a user can enter a fruit named `Apple` as `apple`, `Apple`, `aPPLe`, `ApplE`, etc... This makes the user experience much smoother. - -`split_words()` is used to split the text buffer from the user input into individual parts that correspond with the format specified in the `Commands` like `COMMAND QUANTITY_IN_KG FRUIT_NAME`. - - - -#### In-memory Database - -Instead of doing database I/O by querying SQLite database every time we need to check the existence of data, we will use an in-memory database described by `MemDB` which contains a `Mutex>` scoped to the internals of the crate. This is a `HashMap` indexed using a `String` which is the name of the todo in the `Model` and the value of the indexing key set to the `MyTodosModel`. The HashMap is protected by a `Mutex` for thread-safety. - -#### Formatting the TODO List - -To format the list of TODOs in local cache and display them to the command-line interface, add the following to the - -`File: src/utils.rs` - -```rust,no_run,noplayground -pub async fn format_todos(todo_models: &MemDB) { - println!("\n\n\n"); - if todo_models.lock().await.is_empty() { - println!("Oh My! There are no TODOs"); - } else { - let mut done = Vec::::default(); - let mut not_done = Vec::::default(); - - todo_models.lock().await.iter().for_each(|todo| { - if todo.1.status == 0 { - not_done.push(todo.1.to_owned()); - } else { - done.push(todo.1.to_owned()); - } - }); - - if not_done.is_empty() { - println!("Wohooo! All TODOs are Completed.") - } else { - println!("{QUANTITY:9}| {NOT_DONE:10}"); - println!("----------------"); - not_done.iter().for_each(|todo| { - println!("{:>8} | {:10}", todo.quantity, todo.todo_name); - }); - println!("----------------\n"); - } - - if done.is_empty() { - println!("----------------"); - println!("Bummer :( You Have Not Completed Any TODOs!"); - println!("----------------\n\n"); - } else { - println!("{QUANTITY:9}| {DONE:10}"); - println!("----------------"); - done.iter().for_each(|todo| { - println!("{:>8} | {:10}", todo.quantity, todo.todo_name); - }); - println!("----------------\n"); - } - } -} - -``` - -`format_todos()` functions takes the in-memory database and loops through it, first checking if there are no TODOs and prints `"Oh My! There are no TODOs"` . If TODOs are found, it iterates through them and sorts the `completed` todos into the `done` Vector declared by `let mut done = Vec::::default();` or the `queued` into the `not_done` declared by `let mut not_done = Vec::::default();` There are no completed TODOs but there are queued ones, it prints `"Bummer :( You Have Not Completed Any TODOs!"` and if there are no queued TODOs but completed ones, it prints `"Wohooo! All TODOs are Completed."`. - -The `MyTodosModel` is the `Model` for the `Entity` table `todo_list` in the local SQLite database cache. - -Import the `utils` module in the `src/main.rs` file - -```rust,no_run,noplayground - mod common; - mod db_ops; - mod todo_list_table; -+ mod utils; - - pub use common::*; - pub use db_ops::*; - pub use todo_list_table::prelude::*; -+ pub use utils::*; - -#[async_std::main] -async fn main() -> anyhow::Result<()> { - let db = database_config().await?; - create_todo_table(&db).await?; - - Ok(()) -} - -``` - diff --git a/tutorials-book/src/ch02-04-02-remote-db.md b/tutorials-book/src/ch02-04-02-remote-db.md deleted file mode 100644 index b1207de..0000000 --- a/tutorials-book/src/ch02-04-02-remote-db.md +++ /dev/null @@ -1,178 +0,0 @@ -# Remote Database Operations - -### Data Persistence to the local database - -First, import necessary dependencies - -`File: src/db_ops.rs` - -```rust,no_run,noplayground -+ use serde::{Serialize, Deserialize}; -+ use crate::{synching_to_server, MemDB, MyTodos, MyTodosActiveModel, MyTodosModel}; - -``` - -#### Fetching the fruits - -The `get_fruits()` function fetches the list of fruits from the remote PostgreSQL database using HTTP at the `/fruits` route. The response `response.as_str()?` is serialized using `serde_json` to reate the `fruits` list. - -`File: src/db_ops.rs` - -```rust,no_run,noplayground -pub async fn get_fruits() -> anyhow::Result> { - let response = minreq::get("http://127.0.0.1:8080/fruits").send()?; - - let fruits_list: Vec = serde_json::from_str(&response.as_str()?)?; - - Ok(fruits_list) -} -``` - -#### Storing the fruits in local cache - -The `store()` function takes a `DatabaseConnection` , `quantity` and `todo_name` as arguments and creates an `ActiveModel` as defined by `MyTodosActiveModel` which is then inserted into the SQLite cache using`MyTodos::insert()`. - -`File: src/db_ops.rs` - -```rust,no_run,noplayground -pub async fn store(db: &DatabaseConnection, quantity: &str, todo_name: &str) -> anyhow::Result<()> { - let my_todo = MyTodosActiveModel { - todo_name: Set(todo_name.to_owned()), - quantity: Set(quantity.to_owned()), - status: Set(0), - ..Default::default() - }; - - MyTodos::insert(my_todo).exec(db).await?; - - Ok(()) -} -``` - -#### Fetching the TODO Models from the local SQLite cache - -`get()` function fetches all the `TODO` models using `MyTodos::find().all()` returning all the fetched Models as `Vec` - -`File: src/db_ops.rs` - -```rust,no_run,noplayground -pub async fn get(db: &DatabaseConnection) -> Result, sea_orm::DbErr> { - MyTodos::find().all(db).await -} -``` - -#### Performing modifications on the local SQLite cache - -The `edit()` , `done()` and `undo()` functions perform modifications to the SQLite data. The `edit()` function modifies a TODO in the queue by changing it's `quantity`. The `done()` function moves an incomplete todo from the `queued` field of the `TodoList` struct into the `completed` field of the `TodoList` struct while the `undo()` function does the opposite, moving a TODO from the `completed` field to the `queued` field of the `TodoList` struct. - -`File: src/db_ops.rs` - -```rust,no_run,noplayground -pub async fn edit( - db: &DatabaseConnection, - todo_model: &MyTodosModel, - quantity: String, -) -> Result { - let mut todos_active_model: MyTodosActiveModel = todo_model.to_owned().into(); - todos_active_model.quantity = Set(quantity); - - Ok(todos_active_model.update(db).await?) -} - -pub async fn done( - db: &DatabaseConnection, - todo_model: &MyTodosModel, -) -> Result { - let mut todos_active_model: MyTodosActiveModel = todo_model.to_owned().into(); - todos_active_model.status = Set(1); - - Ok(todos_active_model.update(db).await?) -} - -pub async fn undo( - db: &DatabaseConnection, - todo_model: &MyTodosModel, -) -> Result { - let mut todos_active_model: MyTodosActiveModel = todo_model.to_owned().into(); - todos_active_model.status = Set(0); - - Ok(todos_active_model.update(db).await?) -} -``` - -#### Initializing the In-memory database with the SQLite cache - -Sometimes the `client` might not exit gracefully using the `EXIT` command, this prevents the `client` from syncing the cache with the remote database. The `load_sqlite_cache()` function loads the SQLite cache into in-memory database `MemDB`. It iterates the result of the `get()` function and uses the `todo_name` field of the `MyTodosModel` as the `key` of the `MemDB`. - -```rust,no_run,noplayground -pub(crate) async fn load_sqlite_cache( - db: &DatabaseConnection, - memdb: &mut MemDB, -) -> Result<(), sea_orm::DbErr> { - let sqlite_cache = get(&db).await?; - memdb.lock().await.clear(); - for mytodo_model in sqlite_cache { - memdb - .lock() - .await - .insert(mytodo_model.todo_name.clone(), mytodo_model); - } - - Ok(()) -} -``` - -#### Updating the remote database on graceful exit. - -The `update_remote_storage()` uses the `username` and contents of the `MemDB` to update the remote database over HTTP protocol. The `TodoList` struct is first initialized and the contents of the `MemDB` are sorted into `queued` and `completed` TODOs and then converted into JSON string. The `username` and this JSON string are also converted into JSON using the `json` crate and then sent to the remote server using `minreq` at the `/update_todo` route. If the HTTP response status code is `500` and the matching body data is `ome("MODEL_NOT_FOUND")`, then another request is made to the `/store` route where a new `username` is created and the `todo_list` added under that username. - -```rust,no_run,noplayground -pub async fn update_remote_storage(memdb: &MemDB, username: &str) -> anyhow::Result<()> { - let mut temp_list = TodoList::default(); - memdb.lock().await.values().for_each(|todo| { - if todo.status == 0 { - temp_list.queued.push(todo.to_owned()); - } else { - temp_list.completed.push(todo.to_owned()); - } - }); - - let todo_list = serde_json::to_string(&temp_list)?; - - synching_to_server(); - - let response = minreq::post("http://127.0.0.1:8080/update_todo") - .with_header("Content-Type", "application/json") - .with_body( - json::object! { - username: username, - todo_list: todo_list.clone(), - } - .dump(), - ) - .send()?; - - if response.status_code == 500 { - let body = serde_json::from_str::>(&response.as_str()?)?; - if body == Some("MODEL_NOT_FOUND".to_owned()) { - minreq::post("http://127.0.0.1:8080/store") - .with_header("Content-Type", "application/json") - .with_body( - json::object! { - username: username, - todo_list: todo_list, - } - .dump(), - ) - .send()?; - } - } - - Ok(()) -} - -``` - - - -Up next is reading from the terminal and performing database operations bases on the command. diff --git a/tutorials-book/src/ch02-04-03-stdout.md b/tutorials-book/src/ch02-04-03-stdout.md deleted file mode 100644 index f924acc..0000000 --- a/tutorials-book/src/ch02-04-03-stdout.md +++ /dev/null @@ -1,586 +0,0 @@ -# Reading User Input - -Rust standard library provides an easy way of reading from and writing to the command-line commonly known as `stdout`. First, create a file in the `src` folder called `user_input.rs`. - -`File: src/user_input.rs` - -```rust,no_run,noplayground -use crate::{ - format_todos, MemDB, ADD_COMMAND, DONE_COMMAND, EDIT_COMMAND, EXIT_COMMAND, NUMBER, TITLE, - UNDO_COMMAND, -}; -use std::io; - -pub async fn read_line( - buffer: &mut String, - fruits_list: &Vec, - memdb: &MemDB, - //todo_list: &Vec, -) -> anyhow::Result { - crate::clear_terminal(); - buffer.clear(); - println!("+--------------------------+"); - println!("+ {:^5}{:17}+", "COMMANDS", " "); - println!("+{:26}+", " "); - println!("→ {ADD_COMMAND:5}{:18}+", " "); - println!("→ {DONE_COMMAND:23}+"); - println!("→ {UNDO_COMMAND:23}+"); - println!("→ {EDIT_COMMAND:23}+"); - println!("→ {EXIT_COMMAND:23}+"); - println!("+{:26}+", " "); - println!("+--------------------------+"); - - println!("{NUMBER}| {TITLE:10}"); - println!("----------------"); - for (mut index, item) in fruits_list.iter().enumerate() { - index += 1; - println!("{index:2} | {item:10}"); - } - println!("--------------------------------------------"); - format_todos(&memdb).await; - - println!("Enter a fruit that is available.",); - let stdin = io::stdin(); // We get `Stdin` here. - stdin.read_line(buffer)?; - - Ok(buffer.to_owned()) -} - -``` - -`read_line()` is responsible for reading `stdout` for the user input and returning the user input as a String. It always clears the terminal using `utils::clear_terminal();` before the next input, clears the buffer to prevent stale commands using `buffer.clear()`, lists the list of fruits that the user can add and formats the TODOs printing the sorted TODO list and a set of commands that the user can input to interact with the client. - -#### User Input Handler - -To handle the input create a file in the `src` directory called `handler.rs` - -`File: src/handler.rs` - -```rust,no_run,noplayground -use crate::{ - convert_case, done, edit, get_fruits, load_sqlite_cache, loading, read_line, split_words, - store, synching, undo, update_remote_storage, MemDB, -}; -use std::io; -use sea_orm::DatabaseConnection; -use std::collections::HashMap; - -pub async fn input_handler(db: &DatabaseConnection) -> anyhow::Result<()> { - let mut username_buffer = String::default(); - println!("What is Your Username...",); - let stdin = io::stdin(); // We get `Stdin` here. - stdin.read_line(&mut username_buffer)?; - let username = username_buffer.trim().to_string(); - - let fruits_list: Vec = get_fruits().await?; - - let mut buffer = String::new(); - let mut text_buffer: String; - let mut memdb = MemDB::new(HashMap::default()); - loading(); - load_sqlite_cache(db, &mut memdb).await?; - - loop { - read_line(&mut buffer, fruits_list.as_ref(), &memdb).await?; - buffer = buffer.trim().to_owned(); - let words = split_words(buffer.clone()); - let command = words[0].to_lowercase().to_string(); - let mut quantity: &str = ""; - if command.as_str() == "done" || command.as_str() == "undo" { - text_buffer = convert_case(&words[1]); - } else if command.as_str() == "exit" { - update_remote_storage(&memdb, &username).await?; - println!("SYNCED SUCCESSFULLY."); - println!("Bye! :)"); - break; - } else { - quantity = &words[1]; - text_buffer = convert_case(&words[2]); - } - - if !text_buffer.is_empty() { - match fruits_list.iter().find(|&fruit| *fruit == text_buffer) { - None => { - if !text_buffer.is_empty() { - println!("The fruit `{buffer}` is not available.\n",); - } - continue; - } - Some(_) => { - if command.as_str() == "add" { - if memdb.lock().await.contains_key(&text_buffer) { - continue; - //TODO - } else { - synching(); - store(&db, quantity, &text_buffer).await?; - load_sqlite_cache(&db, &mut memdb).await?; - } - } else if command.as_str() == "edit" { - if let Some(mut todo_model) = memdb.lock().await.get_mut(&text_buffer) { - if todo_model.status != 1 { - synching(); - edit(&db, todo_model, quantity.to_owned()).await?; - todo_model.quantity = quantity.to_owned(); - } - } else { - continue; - } - } else if command.as_str() == "done" { - if let Some(todo_model) = memdb.lock().await.get_mut(&text_buffer) { - if todo_model.status == 0 { - synching(); - let updated_model = done(&db, todo_model).await?; - *todo_model = updated_model; - } - continue; - } else { - continue; - } - } else if command.as_str() == "undo" { - if let Some(todo_model) = memdb.lock().await.get_mut(&text_buffer) { - if todo_model.status == 1 { - synching(); - let updated_model = undo(&db, todo_model).await?; - *todo_model = updated_model; - } - continue; - } else { - continue; - } - } else { - dbg!("Unsupported Command"); - break; - } - } - } - } - } - - Ok(()) -} - -``` - -The code block above is nested and there are comments to help understanding it. Simply, it: - -- reads the `username` -- looks up the `username` from the remote PostgreSQL database -- Loads the local TODO list cache from the local SQLite database -- Stores the loaded local TODO list cache into `MemDB` in-memory database -- reads `stdin` for user input into a `buffer` -- splits the buffer into individual constituents and stores them in an array -- reads the first index of the array to get the command -- performs conditional operations on the command and performs the necessary database operations -- If the command it not available it exits the program -- If the fruit provided is not available, it clears the buffer and reads `stdin` again -- if the command is `EXIT` , it syncs the local SQLite cache with the remote PostgreSQL database and exits. - -Lastly, import the modules into `src/main.rs` - -`File: src/main.rs` - -```rust,no_run,noplayground - mod common; - mod db_ops; -+ mod handler; - mod todo_list_table; -+ mod user_input; - mod utils; - - pub use common::*; - pub use db_ops::*; -+ pub use handler::*; - pub use todo_list_table::prelude::*; -+ pub use user_input::*; - pub use utils::*; - -#[async_std::main] -async fn main() -> anyhow::Result<()> { - let db = database_config().await?; - create_todo_table(&db).await?; - -+ input_handler(&db).await?; - - Ok(()) -} - -``` - -#### Running the Client and Server - -Running both the `todo-server` in the `TODO-Server` directory prints - -```sh -$ ../target/debug/todo-server -`CREATE TABLE fruits` "Operation Successful" -`CREATE TABLE todos` "Operation Successful" -Listening on 127.0.0.1:8080 - -``` - -Running the `todo-client` in the current directory prints. - -```sh -$ Running `target/debug/todo_client` -`CREATE TABLE todo_list` "Operation Successful" -What is Your Username... -``` - -**Enter a username like `user001`** - -This creates a new user in the PostgreSQL database since the user currently does not exist. Querying the PostgreSQL database prints - -```sh -fruits_market=# SELECT * FROM todos; - todo_id | username | todo_list ----------+----------+----------- - 2 | user001 | -(1 row) -``` - -The client then prints a list of fruits, commands and a TODO section: - -```sh -+--------------------------+ -+ COMMANDS + -+ + -→ ADD + -→ DONE + -→ UNDO + -→ EDIT + -→ EXIT + -+ + -+--------------------------+ -No.| FRUITS AVAILABLE ----------------- - 1 | Apple - 2 | Orange - 3 | Mango - 4 | Pineapple --------------------------------------------- - - - - -Oh My! There are no TODOs -Enter a fruit that is available. - -``` - -**Adding a fruit, like `ADD 5kg Apple` prints:** - -```sh -+--------------------------+ -+ COMMANDS + -+ + -→ ADD + -→ DONE + -→ UNDO + -→ EDIT + -→ EXIT + -+ + -+--------------------------+ -No.| FRUITS AVAILABLE ----------------- - 1 | Apple - 2 | Orange - 3 | Mango - 4 | Pineapple --------------------------------------------- - - - - -QUANTITY | NOT DONE ----------------- - 5kg | Apple ----------------- - ----------------- -Bummer :( You Have Not Completed Any TODOs! ----------------- - - -Enter a fruit that is available. - -``` - -A `NOT DONE` table is added and below that the statement `Bummer :( You Have Not Completed Any TODOs!` is printed showing that we have `TODOs` that are not done yet. - -**Add another fruit like `ADD 1kg OraNGe` will print:** - -```sh -+--------------------------+ -+ COMMANDS + -+ + -→ ADD + -→ DONE + -→ UNDO + -→ EDIT + -→ EXIT + -+ + -+--------------------------+ -No.| FRUITS AVAILABLE ----------------- - 1 | Apple - 2 | Orange - 3 | Mango - 4 | Pineapple --------------------------------------------- - - - - -QUANTITY | NOT DONE ----------------- - 5kg | Apple - 1kg | Orange ----------------- - ----------------- -Bummer :( You Have Not Completed Any TODOs! ----------------- - - -Enter a fruit that is available. - -``` - -Here, even though the fruit `Orange` is typed as `OraNGe`, it is still added since we handle this in the code using `convert_case()` function. - -**Now, edit the orange from `1Kg ` to `3kg` with `EDIT 3kg Orange`**. This prints: - -```sh -+--------------------------+ -+ COMMANDS + -+ + -→ ADD + -→ DONE + -→ UNDO + -→ EDIT + -→ EXIT + -+ + -+--------------------------+ -No.| FRUITS AVAILABLE ----------------- - 1 | Apple - 2 | Orange - 3 | Mango - 4 | Pineapple --------------------------------------------- - - - - -QUANTITY | NOT DONE ----------------- - 5kg | Apple - 3kg | Orange ----------------- - ----------------- -Bummer :( You Have Not Completed Any TODOs! ----------------- - - -Enter a fruit that is available. - -``` - -**Next, mark the `Apple` TODO as `done` using `DONE apple`**. This prints: - -```sh -+--------------------------+ -+ COMMANDS + -+ + -→ ADD + -→ DONE + -→ UNDO + -→ EDIT + -→ EXIT + -+ + -+--------------------------+ -No.| FRUITS AVAILABLE ----------------- - 1 | Apple - 2 | Orange - 3 | Mango - 4 | Pineapple --------------------------------------------- - - - - -QUANTITY | NOT DONE ----------------- - 3kg | Orange ----------------- - -QUANTITY | DONE TODOS ----------------- - 5kg | Apple ----------------- - -Enter a fruit that is available. - -``` - -A `DONE TODOS` table is created with the `Apple` as a member. - -**Next, mark the `Apple` as undone with `UNDO Apple`.** This prints: - -```sh -+--------------------------+ -+ COMMANDS + -+ + -→ ADD + -→ DONE + -→ UNDO + -→ EDIT + -→ EXIT + -+ + -+--------------------------+ -No.| FRUITS AVAILABLE ----------------- - 1 | Apple - 2 | Orange - 3 | Mango - 4 | Pineapple --------------------------------------------- - - - - -QUANTITY | NOT DONE ----------------- - 5kg | Apple - 3kg | Orange ----------------- - ----------------- -Bummer :( You Have Not Completed Any TODOs! ----------------- - - -Enter a fruit that is available. - - -``` - -The `Apple` is moved back to the `NOT DONE` table and since there are no DONE TODOs, the `DONE TODO` table is replaced by `Bummer :( You Have Not Completed Any TODOs!` . - -Next, complete all TODOs by marking both the `Orange` and `Apple` as done with: - - 1. `DONE Apple` - 1. `DONE orange` - -This prints: - -```sh -+--------------------------+ -+ COMMANDS + -+ + -→ ADD + -→ DONE + -→ UNDO + -→ EDIT + -→ EXIT + -+ + -+--------------------------+ -No.| FRUITS AVAILABLE ----------------- - 1 | Apple - 2 | Orange - 3 | Mango - 4 | Pineapple --------------------------------------------- - - - - -Wohooo! All TODOs are Completed. -QUANTITY | DONE TODOS ----------------- - 5kg | Apple - 3kg | Orange ----------------- - -Enter a fruit that is available. - - - -``` - -All TODOs are moved to the `DONE TODOS` table and the `NOT DONE` table is replaced by `Wohooo! All TODOs are Completed.` since all `TODOs` are done. This proves that our logic works. - -Lastly, exit the `todo-client` gracefully with the command `EXIT`. This syncs the in-memory database to the remote PostgreSQL server and then exits the program. It prints: - -```sh -SYNCING TO SERVER... -SYNCED SUCCESSFULLY. -Bye! :) -``` - -The state of the SQLite cache is: - -```sql -sqlite> SELECT * FROM todo_list ; -1|Apple|5kg|1 -2|Orange|3kg|1 -sqlite> - -``` - -The state of the PostgreSQL server is: - -```sql -fruits_market=# SELECT * FROM todos; - todo_id | username | todo_list ----------+----------+---------------------------------------------------------------------------------------------------------------------------------------------------------- - 2 | user001 | {"queued":[],"completed":[{"todo_id":2,"todo_name":"Orange","quantity":"3kg","status":1},{"todo_id":1,"todo_name":"Apple","quantity":"5kg","status":1}]} -(1 row) - -``` - -This shows that the TODO list has been successfully synced to remote storage. Running the client again with the same username `user001` should print the `DONE TODOS` from the persisted SQLite cache: - -```sh -+--------------------------+ -+ COMMANDS + -+ + -→ ADD + -→ DONE + -→ UNDO + -→ EDIT + -→ EXIT + -+ + -+--------------------------+ -No.| FRUITS AVAILABLE ----------------- - 1 | Apple - 2 | Orange - 3 | Mango - 4 | Pineapple --------------------------------------------- - - - - -Wohooo! All TODOs are Completed. -QUANTITY | DONE TODOS ----------------- - 5kg | Apple - 3kg | Orange ----------------- - -Enter a fruit that is available. - - -``` - - - -All the source code for the program can be found at [https://github.com/SeaQL/sea-orm-tutorial/tree/master/todo-app](https://github.com/SeaQL/sea-orm-tutorial/tree/master/todo-app). - -That's it for this tutorial. :) diff --git a/tutorials-book/src/ch02-04-web-api-integration.md b/tutorials-book/src/ch02-04-web-api-integration.md new file mode 100644 index 0000000..0567b94 --- /dev/null +++ b/tutorials-book/src/ch02-04-web-api-integration.md @@ -0,0 +1,54 @@ +# Web API Integration + +We can wrap more of the backend's functionalities into our Rocket application. + +Check [Rocket's official documentation](https://rocket.rs/v0.5-rc/guide/) for how to use their interfaces. + +Below are some examples: *(Don't forget to mount all new handlers in `rocket()`!)* + +## Fetch one Bakery by id + +```rust, no_run +#[get("/bakeries/")] +async fn bakery_by_id(db: &State, id: i32) -> Result { + let db = db as &DatabaseConnection; + + let bakery = Bakery::find_by_id(id).one(db).await.map_err(Into::into)?; + + Ok(if let Some(bakery) = bakery { + bakery.name + } else { + return Err(format!("No bakery with id {id} is found.").into()); + }) +} +``` + +## Add a new Bakery + +Query parameters are used for input here for simplicity. Alternatively, use [Body Data](https://rocket.rs/v0.5-rc/guide/requests/#body-data). + +```rust, no_run +use entities::*; + +#[post("/bakeries?&")] +async fn new_bakery( + db: &State, + name: &str, + profit_margin: Option, +) -> Result<(), ErrorResponder> { + let db = db as &DatabaseConnection; + + let new_bakery = bakery::ActiveModel { + name: ActiveValue::Set(name.to_owned()), + profit_margin: ActiveValue::Set(profit_margin.unwrap_or_default()), + ..Default::default() + }; + + Bakery::insert(new_bakery) + .exec(db) + .await + .map_err(Into::into)?; + + Ok(()) +} +``` diff --git a/tutorials-book/src/ch02-05-simple-frontend-using-templates.md b/tutorials-book/src/ch02-05-simple-frontend-using-templates.md new file mode 100644 index 0000000..806e364 --- /dev/null +++ b/tutorials-book/src/ch02-05-simple-frontend-using-templates.md @@ -0,0 +1,7 @@ +# Optional: Simple Frontend Using Templates + +To make your application a little more accessible, Rocket provides the [`Templates`](https://rocket.rs/v0.5-rc/guide/responses/#templates) interface for frontend support. + +Follow their [guide](https://rocket.rs/v0.5-rc/guide/responses/#templates) for a more comprehensive documentation. + +Some examples can be found in the [SeaORM official Rocket example](https://github.com/SeaQL/sea-orm/tree/master/examples/rocket_example) and `rocket-example` in the [repo](https://github.com/SeaQL/sea-orm-tutorial/tree/master/rocket-example) of this tutorial. diff --git a/tutorials-book/src/ch03-00-integration-with-graphql.md b/tutorials-book/src/ch03-00-integration-with-graphql.md new file mode 100644 index 0000000..d3b3e29 --- /dev/null +++ b/tutorials-book/src/ch03-00-integration-with-graphql.md @@ -0,0 +1,3 @@ +# Chapter 3 - Integration with GraphQL + +*Coming Soon!*