From a62f93605f2c68b1e1c60dfc338c60ee5d0b78b7 Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Tue, 31 May 2022 17:18:58 +0800 Subject: [PATCH 01/47] Revamped outline --- tutorials-book/src/SUMMARY.md | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/tutorials-book/src/SUMMARY.md b/tutorials-book/src/SUMMARY.md index 24d5076..38a75b4 100644 --- a/tutorials-book/src/SUMMARY.md +++ b/tutorials-book/src/SUMMARY.md @@ -2,23 +2,21 @@ [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]() + - [Project Setup]() + - [Migration]() + - [Entity Generation using sea-orm-cli]() + - [Basic CRUD Operations]() + - [Relational Select]() + - [Testing with Mock Interface]() + - [Optional: Building SQL Queries with SeaQuery]() -## 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]() + +## Chapter 3 - Integration with GraphQL + +- [Chapter 3 - Integration with GraphQL]() From b37e29b169e9c8d5ab0be96becf9683159810839 Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Tue, 31 May 2022 17:11:08 +0800 Subject: [PATCH 02/47] Rewrite introduction --- tutorials-book/src/ch00-00-introduction.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tutorials-book/src/ch00-00-introduction.md b/tutorials-book/src/ch00-00-introduction.md index 5144019..f9ff097 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 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 its cli tool `sea-orm-cli`. #### Symbols Used @@ -20,9 +20,8 @@ To show added or removed code from files, we will use comments or #### 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. From 0030125967e6a64a04432a3d2af98d7ba0cbb260 Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Tue, 31 May 2022 18:09:20 +0800 Subject: [PATCH 03/47] Remove old tutorials --- .../ch01-00-simple-crud-getting-started.md | 81 --- .../src/ch01-01-create-operation.md | 247 -------- .../src/ch01-02-insert-operations.md | 140 ----- tutorials-book/src/ch01-03-read-operation.md | 106 ---- .../src/ch01-04-update-operation.md | 51 -- .../src/ch01-05-delete-operation.md | 46 -- tutorials-book/src/ch01-06-relationships.md | 259 -------- .../src/ch02-00-todo-app-getting-started.md | 47 -- tutorials-book/src/ch02-01-http-server.md | 174 ------ tutorials-book/src/ch02-02-tables.md | 252 -------- tutorials-book/src/ch02-03-server.md | 279 --------- tutorials-book/src/ch02-04-00-client.md | 227 ------- tutorials-book/src/ch02-04-01-utils.md | 186 ------ tutorials-book/src/ch02-04-02-remote-db.md | 178 ------ tutorials-book/src/ch02-04-03-stdout.md | 586 ------------------ 15 files changed, 2859 deletions(-) delete mode 100644 tutorials-book/src/ch01-01-create-operation.md delete mode 100644 tutorials-book/src/ch01-02-insert-operations.md delete mode 100644 tutorials-book/src/ch01-03-read-operation.md delete mode 100644 tutorials-book/src/ch01-04-update-operation.md delete mode 100644 tutorials-book/src/ch01-05-delete-operation.md delete mode 100644 tutorials-book/src/ch01-06-relationships.md delete mode 100644 tutorials-book/src/ch02-00-todo-app-getting-started.md delete mode 100644 tutorials-book/src/ch02-01-http-server.md delete mode 100644 tutorials-book/src/ch02-02-tables.md delete mode 100644 tutorials-book/src/ch02-03-server.md delete mode 100644 tutorials-book/src/ch02-04-00-client.md delete mode 100644 tutorials-book/src/ch02-04-01-utils.md delete mode 100644 tutorials-book/src/ch02-04-02-remote-db.md delete mode 100644 tutorials-book/src/ch02-04-03-stdout.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 index 06015b4..e69de29 100644 --- a/tutorials-book/src/ch01-00-simple-crud-getting-started.md +++ b/tutorials-book/src/ch01-00-simple-crud-getting-started.md @@ -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-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-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-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-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-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/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-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-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. :) From d7aed2d9c8c718798ad126b484eca8e3e42f4be4 Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Tue, 31 May 2022 18:10:07 +0800 Subject: [PATCH 04/47] Draft ch01-00 --- tutorials-book/src/SUMMARY.md | 2 +- .../ch01-00-build-backend-getting-started.md | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 tutorials-book/src/ch01-00-build-backend-getting-started.md diff --git a/tutorials-book/src/SUMMARY.md b/tutorials-book/src/SUMMARY.md index 38a75b4..4bfa3ee 100644 --- a/tutorials-book/src/SUMMARY.md +++ b/tutorials-book/src/SUMMARY.md @@ -4,8 +4,8 @@ ## Chapter 1 - Building a Backend with SeaORM -- [Chapter 1 - Building a Backend with SeaORM]() - [Project Setup]() +- [Chapter 1 - Building a Backend with SeaORM](ch01-00-build-backend-getting-started.md) - [Migration]() - [Entity Generation using sea-orm-cli]() - [Basic CRUD Operations]() 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..16bdc42 --- /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 connection string should be used: + +| Database | Example Connection String | +| :----------------: | :-----------------------: | +| MySQL | `mysql://root:root@localhost:3306` | +| PostgreSQL | `postgres://root:root@localhost:5432` | +| SQLite (in files) | `sqlite:./sqlite/` | +| SQLite (in memory) | `sqlite::memory:` | + +We will showcase exactly how to how 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). From 1601c5a267eaddbf03c798558f1cf73a004df57e Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Thu, 2 Jun 2022 12:15:47 +0800 Subject: [PATCH 05/47] Draft ch01-01 --- tutorials-book/src/SUMMARY.md | 2 +- .../ch01-00-build-backend-getting-started.md | 4 +- tutorials-book/src/ch01-01-project-setup.md | 123 ++++++++++++++++++ 3 files changed, 126 insertions(+), 3 deletions(-) create mode 100644 tutorials-book/src/ch01-01-project-setup.md diff --git a/tutorials-book/src/SUMMARY.md b/tutorials-book/src/SUMMARY.md index 4bfa3ee..071edd1 100644 --- a/tutorials-book/src/SUMMARY.md +++ b/tutorials-book/src/SUMMARY.md @@ -4,8 +4,8 @@ ## Chapter 1 - Building a Backend with SeaORM - - [Project Setup]() - [Chapter 1 - Building a Backend with SeaORM](ch01-00-build-backend-getting-started.md) + - [Project Setup](ch01-01-project-setup.md) - [Migration]() - [Entity Generation using sea-orm-cli]() - [Basic CRUD Operations]() diff --git a/tutorials-book/src/ch01-00-build-backend-getting-started.md b/tutorials-book/src/ch01-00-build-backend-getting-started.md index 16bdc42..ec939f8 100644 --- a/tutorials-book/src/ch01-00-build-backend-getting-started.md +++ b/tutorials-book/src/ch01-00-build-backend-getting-started.md @@ -13,9 +13,9 @@ SeaORM itself is agnostic to different database implementations, including MySQL 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 connection string should be used: +- A valid database URL should be used: -| Database | Example Connection String | +| Database | Example Database URL | | :----------------: | :-----------------------: | | MySQL | `mysql://root:root@localhost:3306` | | PostgreSQL | `postgres://root:root@localhost:5432` | 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..d05d87e --- /dev/null +++ b/tutorials-book/src/ch01-01-project-setup.md @@ -0,0 +1,123 @@ +# 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 +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 +... + +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. From 8da194760f5545cdfd7b0b38e62f3876443efd1a Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Thu, 2 Jun 2022 13:08:29 +0800 Subject: [PATCH 06/47] Edit SQLite (in file) example URL --- tutorials-book/src/ch01-00-build-backend-getting-started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tutorials-book/src/ch01-00-build-backend-getting-started.md b/tutorials-book/src/ch01-00-build-backend-getting-started.md index ec939f8..39643c7 100644 --- a/tutorials-book/src/ch01-00-build-backend-getting-started.md +++ b/tutorials-book/src/ch01-00-build-backend-getting-started.md @@ -19,7 +19,7 @@ However, depending on the database of your choice, you need to pay attention to | :----------------: | :-----------------------: | | MySQL | `mysql://root:root@localhost:3306` | | PostgreSQL | `postgres://root:root@localhost:5432` | -| SQLite (in files) | `sqlite:./sqlite/` | +| SQLite (in file) | `sqlite:./sqlite.db?mode=rwc` | | SQLite (in memory) | `sqlite::memory:` | We will showcase exactly how to how and where to use them in the next section. From 8f8089396f8aabd99706afeed2a77a31a13eb2cb Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Tue, 31 May 2022 19:37:55 +0800 Subject: [PATCH 07/47] Add bakery-backend source code --- bakery-backend/.gitignore | 1 + bakery-backend/Cargo.toml | 10 +++++++++ bakery-backend/src/main.rs | 46 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+) create mode 100644 bakery-backend/.gitignore create mode 100644 bakery-backend/Cargo.toml create mode 100644 bakery-backend/src/main.rs 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..08fff2b --- /dev/null +++ b/bakery-backend/Cargo.toml @@ -0,0 +1,10 @@ +[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" ] } diff --git a/bakery-backend/src/main.rs b/bakery-backend/src/main.rs new file mode 100644 index 0000000..10f5537 --- /dev/null +++ b/bakery-backend/src/main.rs @@ -0,0 +1,46 @@ +use futures::executor::block_on; +use sea_orm::{ConnectionTrait, Database, DbBackend, DbErr, Statement}; + +const DATABASE_URL: &str = "mysql://root:root@localhost:3306"; + +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(()) +} + +fn main() { + if let Err(err) = block_on(run()) { + panic!("{}", err); + } +} From f4602848d19296db147382ca75d0fbb9f8c2b09e Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Thu, 2 Jun 2022 12:50:36 +0800 Subject: [PATCH 08/47] Add `sea-orm-migration` dependency --- bakery-backend/Cargo.toml | 1 + bakery-backend/src/main.rs | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/bakery-backend/Cargo.toml b/bakery-backend/Cargo.toml index 08fff2b..1fded76 100644 --- a/bakery-backend/Cargo.toml +++ b/bakery-backend/Cargo.toml @@ -8,3 +8,4 @@ edition = "2021" [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" diff --git a/bakery-backend/src/main.rs b/bakery-backend/src/main.rs index 10f5537..bfbb2f8 100644 --- a/bakery-backend/src/main.rs +++ b/bakery-backend/src/main.rs @@ -1,5 +1,8 @@ +mod migrator; + use futures::executor::block_on; use sea_orm::{ConnectionTrait, Database, DbBackend, DbErr, Statement}; +use sea_orm_migration::prelude::*; const DATABASE_URL: &str = "mysql://root:root@localhost:3306"; @@ -35,6 +38,7 @@ async fn run() -> Result<(), DbErr> { } DbBackend::Sqlite => db, }; + let schema_manager = SchemaManager::new(db); // To investigate the schema Ok(()) } From a1c4b7f8c460d2a44c5ed6a69521b1d939ff01fa Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Thu, 2 Jun 2022 13:18:05 +0800 Subject: [PATCH 09/47] Create migrator module --- bakery-backend/src/migrator/mod.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 bakery-backend/src/migrator/mod.rs diff --git a/bakery-backend/src/migrator/mod.rs b/bakery-backend/src/migrator/mod.rs new file mode 100644 index 0000000..6bc76eb --- /dev/null +++ b/bakery-backend/src/migrator/mod.rs @@ -0,0 +1,11 @@ +use sea_orm_migration::prelude::*; + +pub struct Migrator; + +#[async_trait::async_trait] +impl MigratorTrait for Migrator { + fn migrations() -> Vec> { + vec![ + ] + } +} From 2e9abbcb84dbdc147ef611788a0485714850ab91 Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Thu, 2 Jun 2022 13:26:56 +0800 Subject: [PATCH 10/47] Create migrations --- .../m20220602_000001_create_bakery_table.rs | 46 +++++++++++++++ .../m20220602_000002_create_baker_table.rs | 56 +++++++++++++++++++ bakery-backend/src/migrator/mod.rs | 5 ++ 3 files changed, 107 insertions(+) create mode 100644 bakery-backend/src/migrator/m20220602_000001_create_bakery_table.rs create mode 100644 bakery-backend/src/migrator/m20220602_000002_create_baker_table.rs 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 index 6bc76eb..9cbc98b 100644 --- a/bakery-backend/src/migrator/mod.rs +++ b/bakery-backend/src/migrator/mod.rs @@ -1,11 +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), ] } } From e7bccb5245a1387bdef9bcac9c929de5df2747b6 Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Thu, 2 Jun 2022 15:10:08 +0800 Subject: [PATCH 11/47] Use Migrator to define the database schema --- bakery-backend/src/main.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/bakery-backend/src/main.rs b/bakery-backend/src/main.rs index bfbb2f8..0cd9962 100644 --- a/bakery-backend/src/main.rs +++ b/bakery-backend/src/main.rs @@ -1,6 +1,7 @@ mod migrator; use futures::executor::block_on; +use migrator::Migrator; use sea_orm::{ConnectionTrait, Database, DbBackend, DbErr, Statement}; use sea_orm_migration::prelude::*; @@ -40,6 +41,13 @@ async fn run() -> Result<(), DbErr> { }; let schema_manager = SchemaManager::new(db); // To investigate the schema + Migrator::install(db).await?; + assert!(schema_manager.has_table("seaql_migrations").await?); + + Migrator::refresh(db).await?; + assert!(schema_manager.has_table("bakery").await?); + assert!(schema_manager.has_table("baker").await?); + Ok(()) } From e7b8dcf0107c589964e28ed505c83d02e9a587d3 Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Thu, 2 Jun 2022 18:52:49 +0800 Subject: [PATCH 12/47] Draft ch01-02 --- tutorials-book/src/SUMMARY.md | 5 +- tutorials-book/src/assets/er_diagram.png | Bin 0 -> 15499 bytes tutorials-book/src/ch01-02-migration-cli.md | 202 ++++++++++++++++++++ 3 files changed, 205 insertions(+), 2 deletions(-) create mode 100644 tutorials-book/src/assets/er_diagram.png create mode 100644 tutorials-book/src/ch01-02-migration-cli.md diff --git a/tutorials-book/src/SUMMARY.md b/tutorials-book/src/SUMMARY.md index 071edd1..733bcd0 100644 --- a/tutorials-book/src/SUMMARY.md +++ b/tutorials-book/src/SUMMARY.md @@ -6,8 +6,9 @@ - [Chapter 1 - Building a Backend with SeaORM](ch01-00-build-backend-getting-started.md) - [Project Setup](ch01-01-project-setup.md) - - [Migration]() - - [Entity Generation using sea-orm-cli]() + - [Migration (CLI)](ch01-02-migration-cli.md) + - [Migration (API)](ch01-02-migration-api.md) + - [Optional: Generate Entity from Existing Database]() - [Basic CRUD Operations]() - [Relational Select]() - [Testing with Mock Interface]() diff --git a/tutorials-book/src/assets/er_diagram.png b/tutorials-book/src/assets/er_diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..19b658c31787dd25c1a65f2982e866de1d27fab9 GIT binary patch literal 15499 zcmch;Wmr_tA2*D!G)O8XB`qo4A>EVy(7&`1J`6ozaKOSs>e`Kt56|GfOksg7+F_7+r+9E*^Zvj8#;0Fl_CFc$j3iy2o@mvn_ z-&c`kb5Q>Njg*UcV_r7v5E7EeiK2|uGw7XPnV9v@M=!eDKa1CDd2|u63ZsFfzr@OBP#ZI#hd>1!_ zJoj6E&)A>sRxKQrdA7V>eA5-7q2Les-$4^g)RWzPIk=aIgcsA$X!MfE|2s@EiE(6U{cf(>&vrB{Q{|Aiq-jiA-y9A~+!m;Z zx_)9B^J{=4e2bGLB!Z9n(MCTcmEny@ICHGDrH{jg zYO}f({?~UTOc_~!In8HNP=dexmuv)APIB~2x~Bd6 z@nI22(Rm-gFx=$D)5&AqPzIbn&6mR^<9!kLKorX5fKsHM&G9YbUZOme7rldc7CksG zz>?{U@AUMM4~Q2K~KH-~7an>?M8yAqx5h)N;)t@xzv z+90{_9cwXdu;TK{w@9n5C^FY>O3$9NgVl~&;zoRGEKGkSFoT;{@gL9zvxB6TbD5r3E`)-eYs(gLwxHVc7 z`>fp9XnUfRwQkNevMnqShk{>4P4qy<>-V%xq04HQ>*cSyor9?gixW|6D?z{Se3P4x zZxJ_HvKfQdVM@b&x!z^fvOO46zjXBN*V~&Wx19;qB~|&>o${~7arfEst`5U z%+S@*&+k0@EvH#~67V(@Z_2){nVMqb(PAA=G0!ng`84m7?X6b6sOlr_Pn(YqS1qC+ z&*$Odd%gjuY(KuG7q~Mgtz?b7mXM~%T~uP!q{`vWHlHa@2qrA2wGNU<6v_6-= z$1Ox`dAh0nn)hZE3$lV@941QA(zvZ6DwLhN$knVt8@kl+qcb1az0Wj!uT`j&hK!1p zsxEQOnSODkI9ltI?Q!cf`10qXA7ZD5V(9rF9i}Voj+5B*v`(nh>U&qgXnRpY zGB-u4(sEZWMMS5`~B_5Ji(+{ zSbDkhmSl%z((OhO50oXXEAjiB#)j}?$Vy0z*l*om(eBrFLnryM;-BUEMhca!mf8Z+ zyd$p8-rSsT9M0I6D>k^UAG6#|sb(s7ie7?llXg{OK{y+JElw{H?TyniV+S$%hG>k zp3?NptJ;224GoWqjl{?;zS?10tG}Y(@j`dU=X9P#;nk#9ord_tTQ-w72X7foTzeT7 zM(3CiRjwXix7HzTs?aAOrivD&((l$+Xs>?tS#0i|)Tw)Hx*AN*g1@~Z;1EtXDk`;# z3fb+G5Dqr2g0MYl`IYC4&^+A4@51ieIT%X9gcvngFD}U*rP(UV&l%bk6)_#piJi__XqOqZ(64qXv%G0m0vpT=O>V^WliIX&@^LJ6}Rq?;zZd?MhT+`&wiP>kCE=sD^i$JEm<0lNz8-X~R~@(R?^6C-yMqA+kj03Y{s#d@(!tVw z-1_zZ&U++#1oWBgQ*O^Us1d~rEI7JY_Z|_u@SN^1a-!dT5NG}^LhvZlQe-wd}AMf0fPQBZ+LtHIg{sm$MB?|YYM z5hEn`p8w^eLgkbRerjPih1RP>q8qls;~FuYPBkNoj%6`E=fM<0TToan=Rr zSB+a)xU5-n5<}cebpmTvE)0&=GjkTrykIuNdJVZ~@>o1{FFIhn4oXdZoaX`y4jUHmoNbjA3kzeM)@bF=cjmu~Uv6_5N_Z zzrQfW_>obQ$GEFmL8)GqTy)oaK6|B4T7}$Wp(r{`IO)Y@jY@YVuAINGmJ(g|Y}|8t z&)QKloQ`+7P#HLQAh?fD%I}bhLn+L=?O|C5gg0@ zHCu;mbC}!16%AQY_rHj};JjHBo*>0T)})Xvb20I|uGwp+TX`2s?W@bE`kvl>XW||- z&Xw-N&+@M~TKU$X9_f$QOd7L-^;m0vXstKV>eo!o0wd)2a&*9BNWLBDC^E`jrI!Fj zS6cPo_d4ulvbs3lYV6d`k-XDV25`m7Zxqz)rqPC1Hc}K+uXihOif(Q$oe&V#JKWS+ z$acE2Fh{gg`n+a%9k=FfHK5dI`TU-MZ@{^Kmr@PrOIF;pI&AFvC|&BF0U3 z9_xXjLZMD1sikF>ryrvB&~IUt)0j*dGPtVJXE;hRFSi%BPFHG;4ui@uIjPB%9;TaV~YTRnx7WR9lUH2%^*yUp!aAOF{ z?G7-c80Mjc)V*MP8BN9`m)#Z^?_ZxbM{Hv#i79Q0(e{++lc2@Jkt^8wlI`|XMZS0O zoKVvA7c*(G41k)I?~C%G58lsuQv9??jYh0V1bTtnp9GH+`UIeEmmSOijSjccZA)vO z|HY7G-IX-*0gZnzvZyRPDa+^g^hFYV@@Imlp+y|&ytaA$QwJ}UOLlT@@^2zfJfqf` zLL|rJH0i&~vf)HAl44_UbP*XeFhJ)h_itf|=94CVEoiE%wsjd;$&EA?-7zP)a~|%Z zXG=k8SsywQ$RV&>k1blAsB-?E~|1r z32cI~{gLcH8jgGoIu&3;ZB228hDmieI(KWKq4=Wa3)QcF2+6;bFy=bHIvU&&PsTMJ zXwx`UqGt;_YU^oNnKNLbI+tf7o{97kJrC8P#kusf9XvS!Y-X&@4N|nanpG{Whw;R7 z%Q2F~SnKfaol~Fi=dq9Zm)1MA>LzE&iZ9GH9pTX9;*Myc?XSa)4ocS>r>bV&l>|LD zt7t0D`CK{D^yoJIC#xmfCtX|heFrimIP6fxjEAlpdGmQHZ{WyXFHq4?lW_@bH2XPR zXnk2G0=s`xM$Uc2b3!`B=8ttZ(e>TMwB%bbA4e7?b~zc>>DMvwxfiw-`P^tYDV>=^ zZ(RABEBWn)U*+|AeBS<@(om^u5igoD#4KZet zRM#}VMf-B=mF&N$!eT7xdwrtg^A^>7A0C^PC`=P0z{l7$+MF8kVD}+%^lcIokf4xH#h#$p&s`fYAtw& zWZR(6gTW6=?7PtY`wKkxuha`yr|&u${Q*@iJ{#hgYxe+zm&4sysA zAk*9+PU=y~o(n5g18r3BL(V-mk^1Tw+P9k`P&aZtapqe5CK<3CICX&K`N1hit`S)G zZuz(ZUXS@<1`mE#q`3>ZI;Kt;TtUiy4tf*J)K)aqezdFy`g?{CHxge}VG$SbuZfe) z!=O*J@c=4`R*l{Bb3loSZiw~&d>Jx6x4N8|^a=ip(EK9ic+a=)K=@(~0A;b)wkWf! zXz0ubfG!2*JLIyM-v;{>{UHzTaO4URBA6n51X9W%p={Pkf@C1(Swk$*I3W5STaga{ zPks&iilmrCAEBJLtx9Y&p^O|(za+4*$)dDoWHt|Y+Z|*Er6m03s!hOwF$!El5g>d; z5nyoWh_w-YoDAg3S}ELNMPTs_-A1NoR*8O%QUnQSR@{gGE{S2Ot%Qup_}?Yr(Pk&$ zlD7meW~x7BfJ(;gwmEc5*dYIhAR;s_wH&%nXysCZ&P$T?dKL-jC*=UANQ6Tfq*a8! zJ$V|_251r;Sc)5)54tE=CV%VlpJn^q#0R0{byY^xC*e}2ytuA(S`tBV%SddkPq@B-xZ+gKOc>hTTd4$|L|I8Y$7^QGLDFu<-BW@R!o};x)gu|c=Nm4)mRB@;qKsL8jV7wdufj?Lfp1SWW_IctLokD z09tiD`YvGec^`bvkK>kZLBXApD&)m(Tnc_j*=z7pp+@c#ZYX?7l03@aalWxP5EUEQ zc6uHGdkUTxYfrbR%myxOb57_+pzKE11F6@-R+e1n!PqcY8u{f00jW0^EW$^D7cD)5YO!M9Ea~Gg|-Hd+AvdRJym*x2?S2g{rbtD)|MK-MLhR##_%k z33L_Y4h(gqfDTi50(KjtlXqug!6+ zqwX>FY~EV9S`c-V7(3Z}Q1?kaM=_D{LfRc@4*dX?kv_;$ zU~iroH?A8Ydky%-fF+LQ^Fh#cgaT+*^*vocoyzxYInHZnaMABpEa>qzxA6Q(nUM%A z*lxM~ZKdbFF<%aThX>H8?8tHSigq~amylIGyScQQSKKrN=OaXojUspE-5qySu{jpg zyI;p#R?x+1$q`uV-Ci!<9xI!)c)JIzxbZ|@U!E#2w)%{3hq^YMOcDM}ZGDg>( zuSes-y^3WJ{T&H|Z)E$~8P{up+sl^vDGT5tGT$*HGd!{5IA{wEDHq(o#JEMD{T4wf z9%+{_!#kUgeu#KaBOo*^U-!Oq#Ob(>$vd#ZVv4d)8ow^xovxBUq5?F?rWo49IfEDn zcHZ?jxg1dQ9E}Hflg19}g3AzG{kI!Tx=V6$*%zn#u|xx+xFGo~mOW zj(yaWme{1|q|DFEg;cFjI5lgQdiKXQuyr5AX?n&M6GsKD~er6hy%3N)A`TLg^ zfX+_wn!h?m15SnBu^VV3^xAVkYicr_FJK140ArLrHh>1zXSZTc=y2W5LC+998sKpS zL!hZ6Znig(2|MZmMif3&#cr~Uy~X?Fa2yjr!g6wYU+){B(vgt9hktR~Ul6lw;ZiUw z5ys1({OHf*d$9728?dSIaQH-<2!e4a1C?_f)P{I&t3HSug{aP%pi$^67@=An+Qzzz z9liof{wAnRBLWK?Mvc$7xFU&J9y)tI0ZMlM#vk%`s4jk9Fz}vFd5~5D?L<{zz5obS zmv1K_Ue1oYQx)&=LXZs8foNJ?4M-6#8C0e@5XNIWLVphG0xwwzUf*Yfx-vu0d3Rw} z|0ja$Nl({8C>%9Vkz~=PY$py>R!(B!=glW@!?oO%&Ah&ts31JN%JkQ z>lPw4HqzvfOKye%| zEJkQcS5diiF@o(zS+3paG`%X53sr07xk@h`I#d1E)^b2ZT<`TedlE zll|NX#VABDAAGTVXg=NUsSvaC9`!}k9oEfSFgF{_-KcGsA>Lq8*StqPMYdW1y58qm zc)8&d%qXf&sDxOol@DomDt@T`Pnk3>;_}ArN8?qgrrxM%1ZCg2({)TANKL|+f0PA} zmQJ%ro+k!L!q#5Mt(CUvi&B zbU3*6t7bH9HR4u%#F@RO_!dTFS!7A5vBaWEf_N5vm^b(CG&7{4!Y~5uA5;-xOvJcQ zRYflPUaGb{)o4?5{tlLy(sik6k)XR(Lx5b`@-sWvFXdKqRW<*7Z_g`Md;|Nnv*Kdt zv%oHDzDb0`SD%w9=qYZU1zWMMU9hTK67}tVaoZD5H+~sRjfI6Nql^^$bJ1h=rQMlD zUXUWB)qu2$E5bAb!l`NcpvHETQTiRJ^qJJ}E|}QwDT_dLZDf>1_RxlA(j7If8@L5_ zW`IkW;5%K6E_xguD7$H@Sz$+v!zSrt^}o_hzc0PmI5xxc_cD|5?mxMG%`|$^Ne6TWMh~?7|19XSna(H z8(GE5s)BM$)Hqle7i6}#LwhBV2wbebreOaxBKl`YA9=FuWXZ0g)0p>AllfW#O%>Qk z&<5f0A+LxDF=UY}2qh%L0)kwET_E`wRCnH_G-un5f9}nbjpQZ{Dzu97gnB(5hQ&I3 z^}Rlxdi^pa@>GF7>8cx6W!q+|gkS3euy6=#V&6E1KH_=LOKR*$hJI-pX!WJ! zcrh50Xw2D-YOmxtQ^&*%d4n-7mA-vRI?k0I*@^Nvb=1@Je(!AG_3;~e_a~t%w747h z+$NVq?x+j3^1zUdNJ_J$^>W=6K z*1yqmcW=bhB?2u7t<3lh7hSun&X+byoN~gIKrRc|E-D&fGuPRYKC?nObVp{E|K$v^ zV04ZIGdo7JTrPd_jg*iz1D8L(Ml^#ETbk4t{;lMlPjbun3tv>@lo7rHdh)(pKsK-zbt{-1iLZ}}@ zb-5hjyu?xT?$%Xm2b{nGgCKY%DwwnefJ1^E>d_scOvy{okv%cW2wDF zPlD?+O;fz<`<;AybqUkfViJDF zr4;cPW1=*5%b6E@{TqildZ}^FMeTB7PAqlif)#q1F?Bf z;JpG?)&zDO$+2*1CMJ!f7bwXOa%hGHAXXOl@g`dnqbUToM_)qQajBg|iRTVDC=Kd= zv(PVL&{*Rg_>#s#p-9Se&}u5F4z;CJj!XfQj5>Y~;Vs9}iYNv-iH&JkCNr}!ZyP4Yy2qbRcdoaN#Q%{&PNm-aS zSvO}&L|wQ%2;{fNMKI>%2MCS{M0Mfq77s%h_@OgPd;{T-9qpvIi&&eFI$dGX!PjOT zA-0Wm91=bu_RZdRFH|fp1P)J^INALd5!dDv6kqeZwU@VA zjT*Ushfe7_#Z;KMSp;3X*{6i^W}k$SUIsm2?&zW?EABj=5@#^qbr_sl60$&lS7Dqj z*}6nYfoHjk#5X}nDQIQ$wCm;zMR7qCUvkuQnXVt&(a-70wyUe|S=@`1^3zPg-RAeG zH4nTD-Fdi`?e0#o(|}do(PUvW&{ekjIx&imyvC8jrf^GJk!;6tqBckM%s=1Ay5E`d zZpo$L;4DM8%0`^2pXqUPO5_O^nMfk{#5{FhX$hOHQY!6yvn5sO!1-jk~9Mvkpc* zVHbTh1Q!D$Ax7RGOQRQgf47Kx_jwOK)5)V8`tGq}x0`61#u3z7E2qLJwmE*pulDSu z!Dyu4P0THXuaUc8wqCs@dy}(Mtvw1?rzgi?_i0lfUB}zsNWyby9_UQyF?7SFJvd_a z9sJ1dGlEn(w!~4NJE^nBvL$=)WT* zGS~i&V()=<>r!xc2Fn41|E2wNr8lgq0W&}_81Y`V4Isff5Q3Kphs#M=$}hqqvCH|N zK)&FtQ@!>)X<|?QX^0cA9E46ziw1unUpcm4hFv2oK|546jCIU?EJ}K@weh0=t~=$5 zcCNueBMG#ZC7cP+(2+i7B~j~Ie~4D7f3}uFCROK$!auq}RUXDFdcVX(VrlGj2PtfX zD6n4|69<>gL`#7t4WYOUX}0vv5!FWjXG1wc;Lv8H+EKsL)n=Zs(G`hb(X;HPvM%&M z3wdBx_2`e6#Zzc$XYp_*fte%xvj9Pq9up@0dqCio(~AZPaZ^wJMOO;=0d4=}b35hz zFFIm~pu;JWm8Ack8Dj*`)a+9msQ*ifC?hlsLWTQ(99F=r>EmV)*^%GZ7CS%y_|H3X z4VTm+lZDE}JVBi16_0dcV%)s5`>K*a;Zc6rtt-uRE0qJ@pL~PH?YBtY9ojp7$ck3P z=K_f~|6Kv`NK*?g{4JSN8R3@xITQ#Gp7EMoMH=FR{~Q$zpoO(>^o0M}SP=;7{ZJs{ zJ0eQ*=Llm5U9DK}ANkintzh765jSW$F#bEu0YNL-O_J6A_4Fa+utd{ssqN1B<^I>F zYy9gaX`A+H#yE9;w>KGlG=0UxKl$t@Hkp`y#a4P7%OTU_%OjJn4m}Si+)kH?S1VbtcoC2p0=U0axvndHe+7JRD+NCwZ5QZOS-<~1_i9kFQ0X0aMmG#lEhYf> zQ$A1wqu{U&1<$f?weJ=r4|p;yK+Z{W4YJ^O>jOk%?KKjaxWo%aWQBo*0Ld)ghy~xv zmkq}w$|v$=CSQFK@&ap8H`Qwu@r%uT9DhO|E}*agLa&~ePB1bWoOUlNX*cZX%>VpH zrU*)kcYi>Iuw@T^fEgqoC!53=Di!P0uznwy^`A;iz8Xt#y`X_J)BY+hZ^cU5n)d=1 z=b1C8?x;a^Z}QpaTX1vob)u@H-uCv4F#rlzC%*G)!Uf~wtEUwcK(*vbtiL+!AFq1V zNg&_T%aqLq?qjBFXv%Vo$X*;aN4;e91Ge+yU$Z)Kcnd+`<~}#7%9o2yXn3`O)%-+V z>?BDgQ~3G9>wPg;6tcueK?&chvoEK>Ye}}#Snh5BV!`%_WYf}1LBIglfju_1GhF+R z7zZPW@%x)^Bqlvx8<|X}bFX3%nVwN&ziW5fx%!F>=XT8P$LEW;H$_{BxDNjc(n)(q zfG{vP%O>qVzye0jnTxrBq`#?#77j2t!^!S_;Bb0#?#VP^-{e;tUcWHsj`cZ{NVd+)@Oo zKi*$7Sz=(-ul|VeU6;0C*Ru0Hh@jTBKYJ&oPN_a$y@J@@L}~gQbTyhxH+Y~&hEPEZ z8T&pP{w8XaYLwq*03ez&20O<_AQ$tjNODmq{F%u$7&arobvntDt_yijWA5PHQkELl zw%X`@VmJORep>{(vol^?)OZe(9q#~|dGQL5Qn=uPVwLpz@_1CeYM0uL_4wg&(dzO= z4EsrR3fgJaLm+7kJAJ)oEN}V!{w|G!GH`3*4!9xu}OP0v; zyYWJZGUH2Izz*T)M#z7z6M8nELhqoEXVEbeXLQqGGwGUl#2z7tG1gjwGKXe)ccQxZ zr+z2kQ;hdlu%SDl;G{oQ6Ov8Yh6lyyegFfw_y9G39gy62d-F}$eGS{6iy>63%#uBT zMbq0)mW}(2VXwa29Lk+hbMU)4w-MX89xYTRDP08$;KVvz3i_lckcTcqFYu^DKf1R= zr6|++Ut$pxy@|SlFZq*2VUv0o-&tMm2rbsZHXwbrDbEaZeULN-)I2IKJ0o%Eo7$JN zj9_B7Dq}gg47TBURkE7afS?&)edTNRqeMw={4Pm&yo%puB^4PG_{;s79AOrvz;EIHANj9#P&r;Y0C}1+HEp_KPlQcr z=<@9zvIWLe+|Y>j;p5qC@5QRqb%N>Gy? zA#wiSA=Q*Ci=`f*>ea9nL5S9`W%HS(2i0rQ-8IVE2s)nF%^!OZqsLOmJtK z5Y#FXC}kxf^n>^DW-iKU2cE>3lpzQjJy+pqh4dR2WqQ0%V%B_OIz-Ee$UX(+Ef)q( zTF=$HxB_JfeMM)fu%RI20Q7FmZM{m%@HZfB=Q{AXTVZFSw5D7z z)*GMV2t?!TJeV~Gx-y2>`!H(WGGQ)oz2OQ9jt{QPl#Dk9{5+sgd<<`WE1A(qyzZ?$ zZk6#|3bggsafik zP=I)l=lw?PMDgHV9j?UE3v7t8*b5>^7G}NU<5f)lyUw^c4|{;ZgK{Yt;(A1+!n zBx>u##iNwW(fPeMUwo+>L&at4zfJe1JE*&8l*0D{OO5~{JQlqS(iI;!HsIQ}{{nSoZg(Y%Z59m7FI0KS+ zBAR1|IZav(zo7lf5^PUYz$axqgLNmoPo(xeld_@M6D%^=sF4&ZGj>YTAcY4m1@RAN zODw(??%;FYf1)XP;GJmre)fqO(486)DY)4>r%8t?;eBl4_)`{cu7H5>RHWlOY?(CL zZLDDe&dB@3)``XlL%d5cxBN`9*IbT5%@m0?hn&L|aN(uG4wYksmF~tew+r8FxXfgc zhG6Py`x9)hG&y6gG%oo7f8lu!gfyM&*l<8+3F4^$9Ia;W?pYige6t2@i*eO@L#loE z+~a#)oDi2dM;wdr2mO~YWKPj$r7-Mv2&M(M$i>~4M;zmvATh$IlA1H#W>2hCxox8D zO<<+982qrm+7x-&&n)N_6YMb>k?#;(hHyn2jR!?hGx8L85yWSDJhdO|{+yzz zIlrh>UV3)zaE8yVEs+Qzz$N5utUY$6&iV>NO+ZWbIT^ zJfn-3Y-|!8J?> zr*Jx#?9LjXNevHG_z}l+A6;)eQqoLxIGFSy+u+hS-{rXvVZUI61QQ?VFtg;czub>4 zB-(dPt}BN>swbs%YFrd=#tyrd!XXfqhX&FMs-YSaiqeLd+8?7+-G>MX$HtgUFOMvn z1t?Qw#hc2#AesqCdHB$I6cY;TSnkG}qQR_|pbl0RfBojG z721euc3u8XpLS2yd4}SHfu0zXFp4Cj zaGAvh?qO}Vl=Cg3l);y;^8Lz6Gz_-WHJ*(Ke0ORtEYJ)tdSuRmar15TUM@r`0NPF` zfEof>@PY_%<1n(_Tec+-otL5w&?Nv7fQrwY&-+w~Q)I@|ADzDaMISJSFm$#0nA#j78y6CbGg|25&oFp6O8OvMUi8L%LraxBj+t=;<382n{JI!K6 zyv|RYERXD!1{iZszsk_5jEmy47O!jHs#~u_1fOjv8a({@%cA@rr!!Xtn-NMdtKR%$ zuSqn38!|W&Q7Hc9bolmwK$@$juKzzY7y`H8Esmf|t#h!MqWbN3oFAdDDqM_VShj4Z*EG z3#Cw{I(-}wvY~iCrkJtv-?qf@BY5-oQcL-Nl-KnM=z`kjf&Ra?RMUbkymVv``NO3D z&}|~n1=lCBlZZI>pQ8X5biwu&>omc?(=reon8>?1`Ck`aA~tWN8}!EfcMf=5LvY}X zq1BrIi1hSBXFzqABOg`0jY?Zp01f#gDIg5~`^fvPmt|~5jlA1GWnTOIkEfdLTq7KB zU}$5dqcfE^A1ZClH`VDXl|eE(Q4l z#O@A6lGD}M2!0YUKp>Cw&KQt}1Zw2hwb^(zY&~ z+W?OieASN0lnUzZbcV-_?MxORO6efdYAZHr_$oS0=+y_?$+FA}w1nx}m+2K~?}fHM z<_`cM`hB70wdz}}djryVVwNCe|3bgcvD8O4b8`__U@m7o-{}P%DcH@jFpw!S*$mRC zAAr1UeRFl5_C8EpKYu{LkJpgrd3$gH11LCHGepFHra%BalW8=TE$|L;$axhE^uL%& zonQcwTMe2gD%-MAi2f(+l{y2$xAPFbbLwVG3q7My|fM4las3CTc2>nd8=p^Ue zj((zj+)d7D9?e(Ll?ArX$ddEf(KmVg(&8Mn$@XCcW`7D`R#?f~jUK-q0p2wH>P32X z&SI12{=^_KUdz}XJ^#FxVkGpoPr`Mr$KlmUX)u#k2uRdu{nW0=1rydQ4JoYIS8Yy}}{ok}Nru0Z%f3^i5ONlT1KQ zw1O}&b!#bjtfN4%x6_E+BeKngZL7h<0`of=v&8jrB2b?+OH!19y;_b&fE7f$es9c4 zOiruTL65Ji5^Dq@25=AofU8*&su(S6xWnXoZf3ZiRBV!|qHYW(YQpKl7b`?01v-cI24)%KgD z1!n(B$Rg(lPqcdbA#wzhm)8Nb=uTwKO6sBq!C=74cEwQcq!?WtcEt6w=qxIEJ+uYY zNomHQBP1S_)HXlJ(z!n^ug#AKCm)V?r=Lbg1IymT;XyPxanCg%E-y@Zaq^v!Eigkc zAPZy^r*yhX9YnSWio9gYL4jA$uMZ`3*!Yp@3N}31iNT4z%1?{Y`%T~+(}K)=F;sq6 zF6<=dAhyd2sB5BZ=1K8ut2nd@D(V)rfJuzN+WlXT ze`E6Cmh6FucZITD7Y8kwy(QV04emX$P|2T5JAAMWJmhAV(&&JZozi8il1cej`C?1a zhp~byVUogu8Jb^>TDd9s9k?dGJl>=b2eQ*B(n|`P0-uU~mXXR`!Zb8}I{zlnMSaE* zjS#*M+%-Q?^)J`ubG#9=xih!`z=t14xiu`79TlJc456pU7#FI+`cc)!P-T3}Xi3|0 z(@*IAhPsD{XwhlXzPNcP{G-@m2T^e4fv&hE0P@@GQcb%=rh%jk5Z%|PlXP;J${M+l zew#4b_Jjvz-GnqNq0NC-WhTDovV*iAdNF5ED%4}Qt|k2iR|d8Q0j#~lIx44Qu?Y!-1_piF*nDlfAZ^Xq# zfXv_rKVsKZ)Ud_`z{dGL zfq}wbjDn92Ch`hW8X`gX-$5^Nr?u$sn}FaAeMa*C0Y(0cx~s1v2)%T41^%^W1d*d0 znZx1_{s%|m5zR>u-(mc#(JPQ6O2$v^FSLl}{yD &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 + } +} + +// For ease of access +#[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", +] +``` + +Apply all the migrations through `sea-orm-cli`: + +```sh +# Change the value of DATABASE_URL according to your database implementation, +# or supply it in '.env'. +# Make sure the database name is also supplied. +$ DATABASE_URL="mysql://root:root@localhost:3306/bakeries_db" sea-orm-cli migrate refresh +``` From 6d62e734161168cf66b429de44dff8d6e5802f5a Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Tue, 7 Jun 2022 15:56:45 +0800 Subject: [PATCH 13/47] Draft ch01-03 --- tutorials-book/src/SUMMARY.md | 2 +- tutorials-book/src/ch01-01-project-setup.md | 5 + tutorials-book/src/ch01-02-migration-cli.md | 21 +- tutorials-book/src/ch01-03-migration-api.md | 225 ++++++++++++++++++++ 4 files changed, 246 insertions(+), 7 deletions(-) create mode 100644 tutorials-book/src/ch01-03-migration-api.md diff --git a/tutorials-book/src/SUMMARY.md b/tutorials-book/src/SUMMARY.md index 733bcd0..90e3f8b 100644 --- a/tutorials-book/src/SUMMARY.md +++ b/tutorials-book/src/SUMMARY.md @@ -7,7 +7,7 @@ - [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-02-migration-api.md) + - [Migration (API)](ch01-03-migration-api.md) - [Optional: Generate Entity from Existing Database]() - [Basic CRUD Operations]() - [Relational Select]() diff --git a/tutorials-book/src/ch01-01-project-setup.md b/tutorials-book/src/ch01-01-project-setup.md index d05d87e..83cde04 100644 --- a/tutorials-book/src/ch01-01-project-setup.md +++ b/tutorials-book/src/ch01-01-project-setup.md @@ -48,6 +48,8 @@ sea-orm = { version = "0.8.0", features = [ "sqlx-mysql", "runtime-async-std-nat Connect to the database server: ```rust, no_run +// main.rs + use futures::executor::block_on; use sea_orm::{Database, DbErr}; @@ -79,6 +81,9 @@ If it hangs, it could be that the database is not up and running. 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> { diff --git a/tutorials-book/src/ch01-02-migration-cli.md b/tutorials-book/src/ch01-02-migration-cli.md index d13bdab..6af10c3 100644 --- a/tutorials-book/src/ch01-02-migration-cli.md +++ b/tutorials-book/src/ch01-02-migration-cli.md @@ -4,7 +4,7 @@ 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) -## Using `sea-orm-cli` +## Initialize using `sea-orm-cli` For beginners, it is recommended to use `sea-orm-cli` to define and run the migrations. @@ -33,8 +33,14 @@ bakery-backend │ │ 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 @@ -43,6 +49,7 @@ Update the migration files to define the `Bakery` and `Baker` tables: ```rust, no_run // m20220101_000001_create_bakery_table.rs + use sea_orm_migration::prelude::*; pub struct Migration; @@ -83,7 +90,6 @@ impl MigrationTrait for Migration { } } -// For ease of access #[derive(Iden)] pub enum Bakery { Table, @@ -95,6 +101,7 @@ pub enum Bakery { ```rust, no_run // m20220101_000002_create_baker_table.rs + use sea_orm_migration::prelude::*; use super::m20220101_000001_create_bakery_table::Bakery; @@ -157,6 +164,7 @@ pub enum Baker { ```rust, no_run // migration/src/lib.rs + pub use sea_orm_migration::prelude::*; // Add each migration file as a module @@ -192,11 +200,12 @@ features = [ ] ``` -Apply all the migrations through `sea-orm-cli`: +## Perform the migrations + +Perform all the migrations through `sea-orm-cli`: ```sh -# Change the value of DATABASE_URL according to your database implementation, -# or supply it in '.env'. -# Make sure the database name is also supplied. +# 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..7a0462c --- /dev/null +++ b/tutorials-book/src/ch01-03-migration-api.md @@ -0,0 +1,225 @@ +# 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 + +... + +[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::install(db).await?; ++ assert!(schema_manager.has_table("seaql_migrations").await?); + ++ Migrator::refresh(db).await?; ++ assert!(schema_manager.has_table("bakery").await?); ++ assert!(schema_manager.has_table("baker").await?); + + Ok(()) +} + +... +``` From 15919089fcd501293d79fd5dbd07b90d70a3dfa9 Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Tue, 7 Jun 2022 16:13:26 +0800 Subject: [PATCH 14/47] Draft ch01-04 --- bakery-backend/src/entities/baker.rs | 33 ++++++++++++++++++ bakery-backend/src/entities/bakery.rs | 26 ++++++++++++++ bakery-backend/src/entities/mod.rs | 7 ++++ bakery-backend/src/entities/prelude.rs | 5 +++ .../src/entities/seaql_migrations.rs | 22 ++++++++++++ bakery-backend/src/main.rs | 6 ++-- tutorials-book/src/SUMMARY.md | 4 +-- tutorials-book/src/ch01-02-migration-cli.md | 2 ++ .../src/ch01-04-entity-generation.md | 34 +++++++++++++++++++ 9 files changed, 134 insertions(+), 5 deletions(-) create mode 100644 bakery-backend/src/entities/baker.rs create mode 100644 bakery-backend/src/entities/bakery.rs create mode 100644 bakery-backend/src/entities/mod.rs create mode 100644 bakery-backend/src/entities/prelude.rs create mode 100644 bakery-backend/src/entities/seaql_migrations.rs create mode 100644 tutorials-book/src/ch01-04-entity-generation.md 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..2462768 --- /dev/null +++ b/bakery-backend/src/entities/mod.rs @@ -0,0 +1,7 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0 + +pub mod prelude; + +pub mod baker; +pub mod bakery; +pub mod seaql_migrations; diff --git a/bakery-backend/src/entities/prelude.rs b/bakery-backend/src/entities/prelude.rs new file mode 100644 index 0000000..7fba2d7 --- /dev/null +++ b/bakery-backend/src/entities/prelude.rs @@ -0,0 +1,5 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0 + +pub use super::baker::Entity as Baker; +pub use super::bakery::Entity as Bakery; +pub use super::seaql_migrations::Entity as SeaqlMigrations; diff --git a/bakery-backend/src/entities/seaql_migrations.rs b/bakery-backend/src/entities/seaql_migrations.rs new file mode 100644 index 0000000..c32c701 --- /dev/null +++ b/bakery-backend/src/entities/seaql_migrations.rs @@ -0,0 +1,22 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] +#[sea_orm(table_name = "seaql_migrations")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub version: String, + pub applied_at: i64, +} + +#[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/bakery-backend/src/main.rs b/bakery-backend/src/main.rs index 0cd9962..e51b7c5 100644 --- a/bakery-backend/src/main.rs +++ b/bakery-backend/src/main.rs @@ -1,3 +1,4 @@ +mod entities; mod migrator; use futures::executor::block_on; @@ -5,6 +6,8 @@ use migrator::Migrator; use sea_orm::{ConnectionTrait, Database, DbBackend, DbErr, Statement}; use sea_orm_migration::prelude::*; +use entities::prelude::*; // Bring the entities `Baker` and `Bakery` into scope + const DATABASE_URL: &str = "mysql://root:root@localhost:3306"; async fn run() -> Result<(), DbErr> { @@ -41,9 +44,6 @@ async fn run() -> Result<(), DbErr> { }; let schema_manager = SchemaManager::new(db); // To investigate the schema - Migrator::install(db).await?; - assert!(schema_manager.has_table("seaql_migrations").await?); - Migrator::refresh(db).await?; assert!(schema_manager.has_table("bakery").await?); assert!(schema_manager.has_table("baker").await?); diff --git a/tutorials-book/src/SUMMARY.md b/tutorials-book/src/SUMMARY.md index 90e3f8b..fd53907 100644 --- a/tutorials-book/src/SUMMARY.md +++ b/tutorials-book/src/SUMMARY.md @@ -8,8 +8,8 @@ - [Project Setup](ch01-01-project-setup.md) - [Migration (CLI)](ch01-02-migration-cli.md) - [Migration (API)](ch01-03-migration-api.md) - - [Optional: Generate Entity from Existing Database]() - - [Basic CRUD Operations]() + - [Generate Entity from Database](ch01-04-entity-generation.md) + - [Basic CRUD Operations](ch01-05-basic-crud-operations.md) - [Relational Select]() - [Testing with Mock Interface]() - [Optional: Building SQL Queries with SeaQuery]() diff --git a/tutorials-book/src/ch01-02-migration-cli.md b/tutorials-book/src/ch01-02-migration-cli.md index 6af10c3..01f927c 100644 --- a/tutorials-book/src/ch01-02-migration-cli.md +++ b/tutorials-book/src/ch01-02-migration-cli.md @@ -1,5 +1,7 @@ # 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) 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..c3030c1 --- /dev/null +++ b/tutorials-book/src/ch01-04-entity-generation.md @@ -0,0 +1,34 @@ +# 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 +│ │ seaql_migrations.rs +``` + +Put the focus on `baker.rs` and `bakery.rs`, they are the entities representing the tables `Baker` and `Bakery`, respectively. From 074fc35305ea04dda9e8e13cf8182d2d0e048530 Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Tue, 7 Jun 2022 19:04:24 +0800 Subject: [PATCH 15/47] Insert and update --- bakery-backend/src/main.rs | 29 ++++++- .../src/ch01-05-basic-crud-operations.md | 75 +++++++++++++++++++ 2 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 tutorials-book/src/ch01-05-basic-crud-operations.md diff --git a/bakery-backend/src/main.rs b/bakery-backend/src/main.rs index e51b7c5..1be7224 100644 --- a/bakery-backend/src/main.rs +++ b/bakery-backend/src/main.rs @@ -1,13 +1,12 @@ mod entities; mod migrator; +use entities::{prelude::*, *}; use futures::executor::block_on; use migrator::Migrator; -use sea_orm::{ConnectionTrait, Database, DbBackend, DbErr, Statement}; +use sea_orm::*; use sea_orm_migration::prelude::*; -use entities::prelude::*; // Bring the entities `Baker` and `Bakery` into scope - const DATABASE_URL: &str = "mysql://root:root@localhost:3306"; async fn run() -> Result<(), DbErr> { @@ -48,6 +47,30 @@ async fn run() -> Result<(), DbErr> { 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?; + } + Ok(()) } 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..5c22e5d --- /dev/null +++ b/tutorials-book/src/ch01-05-basic-crud-operations.md @@ -0,0 +1,75 @@ +# 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?; +``` + + From 463d921bee93c2142d292df887ddad9ee382397a Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Thu, 9 Jun 2022 15:26:17 +0800 Subject: [PATCH 16/47] Find (single entity) --- bakery-backend/src/main.rs | 17 +++++++++++++++++ .../src/ch01-05-basic-crud-operations.md | 19 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/bakery-backend/src/main.rs b/bakery-backend/src/main.rs index 1be7224..b48722e 100644 --- a/bakery-backend/src/main.rs +++ b/bakery-backend/src/main.rs @@ -71,6 +71,23 @@ async fn run() -> Result<(), DbErr> { 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); + } + Ok(()) } diff --git a/tutorials-book/src/ch01-05-basic-crud-operations.md b/tutorials-book/src/ch01-05-basic-crud-operations.md index 5c22e5d..6d843c8 100644 --- a/tutorials-book/src/ch01-05-basic-crud-operations.md +++ b/tutorials-book/src/ch01-05-basic-crud-operations.md @@ -72,4 +72,23 @@ let john = baker::ActiveModel { 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); +``` From ec2cc8c44969e87deff23446327e71933fd6b6a1 Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Thu, 9 Jun 2022 15:42:11 +0800 Subject: [PATCH 17/47] Delete --- bakery-backend/src/main.rs | 18 +++++++++++++++ .../src/ch01-05-basic-crud-operations.md | 23 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/bakery-backend/src/main.rs b/bakery-backend/src/main.rs index b48722e..b7ee69f 100644 --- a/bakery-backend/src/main.rs +++ b/bakery-backend/src/main.rs @@ -88,6 +88,24 @@ async fn run() -> Result<(), DbErr> { 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()); + } + Ok(()) } diff --git a/tutorials-book/src/ch01-05-basic-crud-operations.md b/tutorials-book/src/ch01-05-basic-crud-operations.md index 6d843c8..4b02d44 100644 --- a/tutorials-book/src/ch01-05-basic-crud-operations.md +++ b/tutorials-book/src/ch01-05-basic-crud-operations.md @@ -92,3 +92,26 @@ let sad_bakery: Option = Bakery::find() .await?; assert_eq!(sad_bakery.unwrap().id, 1); ``` + +## 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 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()); +``` From dddb2c85d617219ffe93c03bf1ccfae9d7567e05 Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Thu, 9 Jun 2022 16:41:09 +0800 Subject: [PATCH 18/47] Draft ch01-06 --- bakery-backend/src/main.rs | 31 +++++++++++++ tutorials-book/src/SUMMARY.md | 2 +- .../src/ch01-05-basic-crud-operations.md | 2 + .../src/ch01-06-relational-select.md | 45 +++++++++++++++++++ 4 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 tutorials-book/src/ch01-06-relational-select.md diff --git a/bakery-backend/src/main.rs b/bakery-backend/src/main.rs index b7ee69f..bcf69cb 100644 --- a/bakery-backend/src/main.rs +++ b/bakery-backend/src/main.rs @@ -106,6 +106,37 @@ async fn run() -> Result<(), DbErr> { 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"]); + } + Ok(()) } diff --git a/tutorials-book/src/SUMMARY.md b/tutorials-book/src/SUMMARY.md index fd53907..90815e1 100644 --- a/tutorials-book/src/SUMMARY.md +++ b/tutorials-book/src/SUMMARY.md @@ -10,7 +10,7 @@ - [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]() + - [Relational Select](ch01-06-relational-select.md) - [Testing with Mock Interface]() - [Optional: Building SQL Queries with SeaQuery]() diff --git a/tutorials-book/src/ch01-05-basic-crud-operations.md b/tutorials-book/src/ch01-05-basic-crud-operations.md index 4b02d44..76a65ee 100644 --- a/tutorials-book/src/ch01-05-basic-crud-operations.md +++ b/tutorials-book/src/ch01-05-basic-crud-operations.md @@ -93,6 +93,8 @@ let sad_bakery: Option = Bakery::find() 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! 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). From 4aa363d3e704cbdd31418da0ec4c8c313249a125 Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Thu, 9 Jun 2022 18:52:10 +0800 Subject: [PATCH 19/47] Draft ch01-07 --- bakery-backend/Cargo.toml | 2 +- bakery-backend/src/main.rs | 124 ++++++++++++++++ tutorials-book/src/SUMMARY.md | 2 +- tutorials-book/src/ch01-07-mock-testing.md | 163 +++++++++++++++++++++ 4 files changed, 289 insertions(+), 2 deletions(-) create mode 100644 tutorials-book/src/ch01-07-mock-testing.md diff --git a/bakery-backend/Cargo.toml b/bakery-backend/Cargo.toml index 1fded76..993e89e 100644 --- a/bakery-backend/Cargo.toml +++ b/bakery-backend/Cargo.toml @@ -7,5 +7,5 @@ edition = "2021" [dependencies] futures = "0.3.21" -sea-orm = { version = "0.8.0", features = [ "sqlx-mysql", "runtime-async-std-native-tls", "macros" ] } +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/main.rs b/bakery-backend/src/main.rs index bcf69cb..7157745 100644 --- a/bakery-backend/src/main.rs +++ b/bakery-backend/src/main.rs @@ -137,6 +137,130 @@ async fn run() -> Result<(), DbErr> { 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, + }, + ] + ); + } + Ok(()) } diff --git a/tutorials-book/src/SUMMARY.md b/tutorials-book/src/SUMMARY.md index 90815e1..d97425d 100644 --- a/tutorials-book/src/SUMMARY.md +++ b/tutorials-book/src/SUMMARY.md @@ -11,7 +11,7 @@ - [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]() + - [Testing with Mock Interface](ch01-07-mock-testing.md) - [Optional: Building SQL Queries with SeaQuery]() ## Chapter 2 - Integration with Rocket 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). From 206679a65cd4d68a729b8e69ea032fee06ca0b73 Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Thu, 9 Jun 2022 18:54:50 +0800 Subject: [PATCH 20/47] Remove PostgreSQL prompt symbol guide --- tutorials-book/src/ch00-00-introduction.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/tutorials-book/src/ch00-00-introduction.md b/tutorials-book/src/ch00-00-introduction.md index f9ff097..aa88c14 100644 --- a/tutorials-book/src/ch00-00-introduction.md +++ b/tutorials-book/src/ch00-00-introduction.md @@ -16,8 +16,6 @@ To show added or removed code from files, we will use comments or `$ ` shows an operation is done on the console/shell -`postgres=#` shows a PostgreSQL prompt. - #### Chapters In the first chapter, we will learn how to build a backend application with SeaORM. It will be compatible with different database implementations. From 322fc918a1e0a8216ec34b8939c042cba3c63e91 Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Thu, 9 Jun 2022 18:57:29 +0800 Subject: [PATCH 21/47] Add reminder of executing sea-orm-cli at the project root --- tutorials-book/src/ch01-02-migration-cli.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tutorials-book/src/ch01-02-migration-cli.md b/tutorials-book/src/ch01-02-migration-cli.md index 01f927c..1e32c52 100644 --- a/tutorials-book/src/ch01-02-migration-cli.md +++ b/tutorials-book/src/ch01-02-migration-cli.md @@ -206,6 +206,8 @@ features = [ 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. From 16bb659655f32eed0132401d25ffa2b0b92c2747 Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Thu, 9 Jun 2022 19:08:21 +0800 Subject: [PATCH 22/47] Specify file path of Cargo.toml --- tutorials-book/src/ch01-03-migration-api.md | 1 + 1 file changed, 1 insertion(+) diff --git a/tutorials-book/src/ch01-03-migration-api.md b/tutorials-book/src/ch01-03-migration-api.md index 7a0462c..765e1d1 100644 --- a/tutorials-book/src/ch01-03-migration-api.md +++ b/tutorials-book/src/ch01-03-migration-api.md @@ -9,6 +9,7 @@ This section covers how to perform migrations without the need to install and us Add the cargo dependency `sea-orm-migration`: ```diff +// Cargo.toml ... From 00c194afc3a1b58d958cc825fc071ef244aee73c Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Sun, 12 Jun 2022 22:05:57 +0800 Subject: [PATCH 23/47] intro: "the most" -> "a" --- tutorials-book/src/ch00-00-introduction.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tutorials-book/src/ch00-00-introduction.md b/tutorials-book/src/ch00-00-introduction.md index aa88c14..1db5a3e 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 its 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 From c8227cbd1949115d5815d4cba4d57201b5e3ff36 Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Tue, 14 Jun 2022 14:24:52 +0800 Subject: [PATCH 24/47] Remove legacy example source files --- simple-crud/.env | 1 - simple-crud/.gitignore | 4 - simple-crud/Cargo.toml | 20 --- simple-crud/src/fruits_table/fruits.rs | 24 --- simple-crud/src/fruits_table/mod.rs | 3 - simple-crud/src/fruits_table/prelude.rs | 4 - simple-crud/src/main.rs | 148 ---------------- simple-crud/src/suppliers_table/mod.rs | 5 - simple-crud/src/suppliers_table/prelude.rs | 6 - simple-crud/src/suppliers_table/suppliers.rs | 33 ---- todo-app/.gitignore | 4 - todo-app/Cargo.toml | 5 - todo-app/client/.env | 1 - todo-app/client/.gitignore | 3 - todo-app/client/Cargo.toml | 16 -- todo-app/client/src/db_ops.rs | 166 ------------------ todo-app/client/src/handler.rs | 102 ----------- todo-app/client/src/main.rs | 29 --- todo-app/client/src/todo_list_table/mod.rs | 5 - .../client/src/todo_list_table/prelude.rs | 6 - .../client/src/todo_list_table/todo_list.rs | 25 --- todo-app/client/src/user_input.rs | 40 ----- todo-app/client/src/utils.rs | 95 ---------- todo-app/server/.env | 1 - todo-app/server/.gitignore | 1 - todo-app/server/Cargo.toml | 15 -- .../server/src/fruits_list_table/fruits.rs | 23 --- todo-app/server/src/fruits_list_table/mod.rs | 5 - .../server/src/fruits_list_table/prelude.rs | 6 - todo-app/server/src/insert_values.rs | 33 ---- todo-app/server/src/main.rs | 115 ------------ todo-app/server/src/routing.rs | 76 -------- todo-app/server/src/todo_list_table/mod.rs | 5 - .../server/src/todo_list_table/prelude.rs | 6 - todo-app/server/src/todo_list_table/todos.rs | 24 --- 35 files changed, 1055 deletions(-) delete mode 100644 simple-crud/.env delete mode 100644 simple-crud/.gitignore delete mode 100644 simple-crud/Cargo.toml delete mode 100644 simple-crud/src/fruits_table/fruits.rs delete mode 100644 simple-crud/src/fruits_table/mod.rs delete mode 100644 simple-crud/src/fruits_table/prelude.rs delete mode 100644 simple-crud/src/main.rs delete mode 100644 simple-crud/src/suppliers_table/mod.rs delete mode 100644 simple-crud/src/suppliers_table/prelude.rs delete mode 100644 simple-crud/src/suppliers_table/suppliers.rs delete mode 100644 todo-app/.gitignore delete mode 100644 todo-app/Cargo.toml delete mode 100644 todo-app/client/.env delete mode 100644 todo-app/client/.gitignore delete mode 100644 todo-app/client/Cargo.toml delete mode 100644 todo-app/client/src/db_ops.rs delete mode 100644 todo-app/client/src/handler.rs delete mode 100644 todo-app/client/src/main.rs delete mode 100644 todo-app/client/src/todo_list_table/mod.rs delete mode 100644 todo-app/client/src/todo_list_table/prelude.rs delete mode 100644 todo-app/client/src/todo_list_table/todo_list.rs delete mode 100644 todo-app/client/src/user_input.rs delete mode 100644 todo-app/client/src/utils.rs delete mode 100644 todo-app/server/.env delete mode 100644 todo-app/server/.gitignore delete mode 100644 todo-app/server/Cargo.toml delete mode 100644 todo-app/server/src/fruits_list_table/fruits.rs delete mode 100644 todo-app/server/src/fruits_list_table/mod.rs delete mode 100644 todo-app/server/src/fruits_list_table/prelude.rs delete mode 100644 todo-app/server/src/insert_values.rs delete mode 100644 todo-app/server/src/main.rs delete mode 100644 todo-app/server/src/routing.rs delete mode 100644 todo-app/server/src/todo_list_table/mod.rs delete mode 100644 todo-app/server/src/todo_list_table/prelude.rs delete mode 100644 todo-app/server/src/todo_list_table/todos.rs 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 {} From 2617f6e46730dc50691992d8b2b4844dc4d9093c Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Tue, 14 Jun 2022 16:01:51 +0800 Subject: [PATCH 25/47] Draft ch01-08 --- tutorials-book/src/SUMMARY.md | 2 +- .../src/ch01-08-sql-with-sea-query.md | 92 +++++++++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 tutorials-book/src/ch01-08-sql-with-sea-query.md diff --git a/tutorials-book/src/SUMMARY.md b/tutorials-book/src/SUMMARY.md index d97425d..1ec2e94 100644 --- a/tutorials-book/src/SUMMARY.md +++ b/tutorials-book/src/SUMMARY.md @@ -12,7 +12,7 @@ - [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]() + - [Optional: Building SQL Queries with SeaQuery](ch01-08-sql-with-sea-query.md) ## Chapter 2 - Integration with Rocket 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..d4497fb --- /dev/null +++ b/tutorials-book/src/ch01-08-sql-with-sea-query.md @@ -0,0 +1,92 @@ +# 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 +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 +#[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)); +``` From c01ad2e34e9c71a55d8adc98d5514e63faf58bde Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Tue, 14 Jun 2022 16:04:28 +0800 Subject: [PATCH 26/47] Add SeaQuery usage source code --- bakery-backend/src/main.rs | 49 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/bakery-backend/src/main.rs b/bakery-backend/src/main.rs index 7157745..1b0a882 100644 --- a/bakery-backend/src/main.rs +++ b/bakery-backend/src/main.rs @@ -9,6 +9,11 @@ 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?; @@ -261,6 +266,50 @@ async fn run() -> Result<(), DbErr> { ); } + // 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(()) } From 679fc6f1cc18425641406d4b574cbd318a95a236 Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Thu, 16 Jun 2022 17:05:15 +0800 Subject: [PATCH 27/47] Add Rocket integration example source code --- Cargo.toml | 2 + rocket-example/.gitignore | 1 + rocket-example/Cargo.toml | 15 +++ rocket-example/src/entities/baker.rs | 33 ++++++ rocket-example/src/entities/bakery.rs | 26 +++++ rocket-example/src/entities/mod.rs | 7 ++ rocket-example/src/entities/prelude.rs | 5 + .../src/entities/seaql_migrations.rs | 22 ++++ rocket-example/src/main.rs | 104 ++++++++++++++++++ .../m20220602_000001_create_bakery_table.rs | 46 ++++++++ .../m20220602_000002_create_baker_table.rs | 56 ++++++++++ rocket-example/src/migrator/mod.rs | 16 +++ rocket-example/src/setup.rs | 39 +++++++ 13 files changed, 372 insertions(+) create mode 100644 Cargo.toml create mode 100644 rocket-example/.gitignore create mode 100644 rocket-example/Cargo.toml create mode 100644 rocket-example/src/entities/baker.rs create mode 100644 rocket-example/src/entities/bakery.rs create mode 100644 rocket-example/src/entities/mod.rs create mode 100644 rocket-example/src/entities/prelude.rs create mode 100644 rocket-example/src/entities/seaql_migrations.rs create mode 100644 rocket-example/src/main.rs create mode 100644 rocket-example/src/migrator/m20220602_000001_create_bakery_table.rs create mode 100644 rocket-example/src/migrator/m20220602_000002_create_baker_table.rs create mode 100644 rocket-example/src/migrator/mod.rs create mode 100644 rocket-example/src/setup.rs 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/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..1661e70 --- /dev/null +++ b/rocket-example/Cargo.toml @@ -0,0 +1,15 @@ +[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" 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..2462768 --- /dev/null +++ b/rocket-example/src/entities/mod.rs @@ -0,0 +1,7 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0 + +pub mod prelude; + +pub mod baker; +pub mod bakery; +pub mod seaql_migrations; diff --git a/rocket-example/src/entities/prelude.rs b/rocket-example/src/entities/prelude.rs new file mode 100644 index 0000000..7fba2d7 --- /dev/null +++ b/rocket-example/src/entities/prelude.rs @@ -0,0 +1,5 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0 + +pub use super::baker::Entity as Baker; +pub use super::bakery::Entity as Bakery; +pub use super::seaql_migrations::Entity as SeaqlMigrations; diff --git a/rocket-example/src/entities/seaql_migrations.rs b/rocket-example/src/entities/seaql_migrations.rs new file mode 100644 index 0000000..c32c701 --- /dev/null +++ b/rocket-example/src/entities/seaql_migrations.rs @@ -0,0 +1,22 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] +#[sea_orm(table_name = "seaql_migrations")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub version: String, + pub applied_at: i64, +} + +#[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/rocket-example/src/main.rs b/rocket-example/src/main.rs new file mode 100644 index 0000000..a09cf5e --- /dev/null +++ b/rocket-example/src/main.rs @@ -0,0 +1,104 @@ +mod entities; +mod migrator; +mod setup; + +use entities::{prelude::*, *}; +use rocket::{serde::json::Json, *}; +use sea_orm::*; +use setup::set_up_db; + +#[get("/")] +async fn index() -> &'static str { + "Hello, bakeries!" +} + +#[get("/bakeries")] +async fn bakeries(db: &State) -> Result>, ErrorResponder> { + let db = db as &DatabaseConnection; + + let bakery_names = Bakery::find() + .all(db) + .await + .map_err(Into::into)? + .into_iter() + .map(|b| b.name) + .collect::>(); + + Ok(Json(bakery_names)) +} + +#[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()); + }) +} + +#[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(()) +} + +#[launch] +async fn rocket() -> _ { + let db = match set_up_db().await { + Ok(db) => db, + Err(err) => panic!("{}", err), + }; + + rocket::build() + .manage(db) + .mount("/", routes![index, bakeries, bakery_by_id, new_bakery]) +} + +#[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) +} From dc4a156932ae3032b754daae97cbec69f2c5d5e2 Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Thu, 16 Jun 2022 17:05:36 +0800 Subject: [PATCH 28/47] Update SUMMARY --- tutorials-book/src/SUMMARY.md | 6 +++++- tutorials-book/src/ch02-00-integration-with-rocket.md | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 tutorials-book/src/ch02-00-integration-with-rocket.md diff --git a/tutorials-book/src/SUMMARY.md b/tutorials-book/src/SUMMARY.md index 1ec2e94..4f2f951 100644 --- a/tutorials-book/src/SUMMARY.md +++ b/tutorials-book/src/SUMMARY.md @@ -16,7 +16,11 @@ ## Chapter 2 - Integration with Rocket -- [Chapter 2 - Integration with Rocket]() +- [Chapter 2 - Integration with Rocket](ch02-00-integration-with-rocket.md) + - [Project Setup](#) + - [Fetch from Database](#) + - [Error Handling](#) + - [Web API Integration](#) ## Chapter 3 - Integration with GraphQL 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..29e3033 --- /dev/null +++ b/tutorials-book/src/ch02-00-integration-with-rocket.md @@ -0,0 +1,3 @@ +# Chapter 2 - Integration with Rocket + + From 28a805e561bc8d10a4dc757206eae4b0721b5898 Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Thu, 16 Jun 2022 17:07:55 +0800 Subject: [PATCH 29/47] Section rename: "Fetch from Database" -> "Connect to Database" --- tutorials-book/src/SUMMARY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tutorials-book/src/SUMMARY.md b/tutorials-book/src/SUMMARY.md index 4f2f951..2a61bf5 100644 --- a/tutorials-book/src/SUMMARY.md +++ b/tutorials-book/src/SUMMARY.md @@ -18,7 +18,7 @@ - [Chapter 2 - Integration with Rocket](ch02-00-integration-with-rocket.md) - [Project Setup](#) - - [Fetch from Database](#) + - [Connect to Database](#) - [Error Handling](#) - [Web API Integration](#) From ab45421aa99dc85afed66ee5152f19a79d9f1009 Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Thu, 16 Jun 2022 17:23:50 +0800 Subject: [PATCH 30/47] Draft ch02-00 --- tutorials-book/src/ch02-00-integration-with-rocket.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tutorials-book/src/ch02-00-integration-with-rocket.md b/tutorials-book/src/ch02-00-integration-with-rocket.md index 29e3033..fd22084 100644 --- a/tutorials-book/src/ch02-00-integration-with-rocket.md +++ b/tutorials-book/src/ch02-00-integration-with-rocket.md @@ -1,3 +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! From c70b256c46a1360983f6490ff58cbb94e566d7cd Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Thu, 16 Jun 2022 17:25:26 +0800 Subject: [PATCH 31/47] Remove # in SUMMARY.md --- tutorials-book/src/SUMMARY.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tutorials-book/src/SUMMARY.md b/tutorials-book/src/SUMMARY.md index 2a61bf5..aff187c 100644 --- a/tutorials-book/src/SUMMARY.md +++ b/tutorials-book/src/SUMMARY.md @@ -17,10 +17,10 @@ ## Chapter 2 - Integration with Rocket - [Chapter 2 - Integration with Rocket](ch02-00-integration-with-rocket.md) - - [Project Setup](#) - - [Connect to Database](#) - - [Error Handling](#) - - [Web API Integration](#) + - [Project Setup](ch02-01-project-setup.md) + - [Connect to Database]() + - [Error Handling]() + - [Web API Integration]() ## Chapter 3 - Integration with GraphQL From 672529686375c0faa92597c6e2e4c59f92f340f2 Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Thu, 16 Jun 2022 17:44:37 +0800 Subject: [PATCH 32/47] Add `/reset` endpoint in Rocket integration example source code --- rocket-example/src/main.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/rocket-example/src/main.rs b/rocket-example/src/main.rs index a09cf5e..6f54ebb 100644 --- a/rocket-example/src/main.rs +++ b/rocket-example/src/main.rs @@ -3,8 +3,10 @@ mod migrator; mod setup; use entities::{prelude::*, *}; +use migrator::Migrator; use rocket::{serde::json::Json, *}; use sea_orm::*; +use sea_orm_migration::MigratorTrait; use setup::set_up_db; #[get("/")] @@ -62,6 +64,13 @@ async fn new_bakery( Ok(()) } +#[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 { @@ -69,9 +78,10 @@ async fn rocket() -> _ { Err(err) => panic!("{}", err), }; - rocket::build() - .manage(db) - .mount("/", routes![index, bakeries, bakery_by_id, new_bakery]) + rocket::build().manage(db).mount( + "/", + routes![index, bakeries, bakery_by_id, new_bakery, reset], + ) } #[derive(Responder)] From a923a0c183cd503b18c93fa27f11fd31b722f779 Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Thu, 16 Jun 2022 17:59:35 +0800 Subject: [PATCH 33/47] Draft ch02-01 --- tutorials-book/src/ch02-01-project-setup.md | 51 +++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 tutorials-book/src/ch02-01-project-setup.md 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..9c66128 --- /dev/null +++ b/tutorials-book/src/ch02-01-project-setup.md @@ -0,0 +1,51 @@ +# 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, bakeries]) +} + +``` + +To verify it works: + +``` +GET localhost:8000/ + +"Hello, bakeries!" +``` From eefbbcda3e131a94e5846e47bb89b786d1506b17 Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Thu, 23 Jun 2022 14:22:56 +0800 Subject: [PATCH 34/47] Add templates to provide a minimal frontend --- rocket-example/Cargo.toml | 5 + rocket-example/Rocket.toml | 2 + rocket-example/src/main.rs | 71 ++- rocket-example/static/css/normalize.css | 427 +++++++++++++++++++ rocket-example/static/css/skeleton.css | 421 ++++++++++++++++++ rocket-example/static/css/style.css | 73 ++++ rocket-example/static/images/favicon.png | Bin 0 -> 1155 bytes rocket-example/templates/bakeries.html.tera | 22 + rocket-example/templates/bakery.html.tera | 15 + rocket-example/templates/base.html.tera | 26 ++ rocket-example/templates/error/404.html.tera | 11 + rocket-example/templates/index.html.tera | 16 + rocket-example/templates/new.html.tera | 38 ++ rocket-example/templates/success.html.tera | 20 + 14 files changed, 1130 insertions(+), 17 deletions(-) create mode 100644 rocket-example/Rocket.toml create mode 100644 rocket-example/static/css/normalize.css create mode 100644 rocket-example/static/css/skeleton.css create mode 100644 rocket-example/static/css/style.css create mode 100644 rocket-example/static/images/favicon.png create mode 100644 rocket-example/templates/bakeries.html.tera create mode 100644 rocket-example/templates/bakery.html.tera create mode 100644 rocket-example/templates/base.html.tera create mode 100644 rocket-example/templates/error/404.html.tera create mode 100644 rocket-example/templates/index.html.tera create mode 100644 rocket-example/templates/new.html.tera create mode 100644 rocket-example/templates/success.html.tera diff --git a/rocket-example/Cargo.toml b/rocket-example/Cargo.toml index 1661e70..f3da933 100644 --- a/rocket-example/Cargo.toml +++ b/rocket-example/Cargo.toml @@ -13,3 +13,8 @@ sea-orm = { version = "0.8.0", features = [ "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/main.rs b/rocket-example/src/main.rs index 6f54ebb..9ab1023 100644 --- a/rocket-example/src/main.rs +++ b/rocket-example/src/main.rs @@ -4,55 +4,74 @@ mod setup; use entities::{prelude::*, *}; use migrator::Migrator; -use rocket::{serde::json::Json, *}; +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("/")] -async fn index() -> &'static str { - "Hello, bakeries!" +fn index() -> Template { + Template::render("index", json!({})) } #[get("/bakeries")] -async fn bakeries(db: &State) -> Result>, ErrorResponder> { +async fn bakeries(db: &State) -> Result { let db = db as &DatabaseConnection; - let bakery_names = Bakery::find() + let bakeries = Bakery::find() .all(db) .await .map_err(Into::into)? .into_iter() - .map(|b| b.name) - .collect::>(); + .map(|b| json!({ "name": b.name, "id": b.id })) + .collect::>(); - Ok(Json(bakery_names)) + Ok(Template::render( + "bakeries", + json!({ "bakeries": bakeries, "num_bakeries": bakeries.len() }), + )) } #[get("/bakeries/")] -async fn bakery_by_id(db: &State, id: i32) -> Result { +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 + 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()); }) } -#[post("/bakeries/?")] +#[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<(), ErrorResponder> { +) -> 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.unwrap_or_default()), + profit_margin: ActiveValue::Set(profit_margin), ..Default::default() }; @@ -61,7 +80,10 @@ async fn new_bakery( .await .map_err(Into::into)?; - Ok(()) + Ok(Template::render( + "success", + json!({ "name": name, "profit_margin": profit_margin}), + )) } #[post("/reset")] @@ -78,9 +100,24 @@ async fn rocket() -> _ { Err(err) => panic!("{}", err), }; - rocket::build().manage(db).mount( - "/", - routes![index, bakeries, bakery_by_id, new_bakery, reset], + 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() + }), ) } 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 0000000000000000000000000000000000000000..02b7390443ae2877c186898f52da23f062adbdcc GIT binary patch literal 1155 zcmV-}1bq96P)(=Qx>xolP{L(>2x~5PFgqllLG?-XURWh zvn7|E20`C&yWRV!SI1Uhr?1K%v2UB0m}sFhe_PG5av(Pl2<+kH9bD;aRz}QZjK*Wi zzU=Ss53_$_q-W@WteT*|u)Sk6X{KXzp9O21Er;<5zg9bJJVN(-=Zy8l4qegdI#S(evShjLl#Pk=r9jtgPegJHb!|>nu2DZUh(FA*z*X!Ly#;=Z< zcY*h@wRV}ZUw{XTs%x<*&yAr6*O7QQIqlYkGx)Ptvae%$?Bex{(}9JyQ+)L}e`z0B>Y7?qR+gjq`2_(| z*YR1|dc+Lo=!ck$dL#l;$vC2h!$%RW3u(Hst`lh9wh`s^nV%ujFKnUV($0F52BLCWB8{a&>BRO_*@>CN}62 z(UjU)Dh1DMef+rt@phC|?U_NQ(5sA6dU2zmz~B@z{?iJlE7 z#AcOX=vSv4Lj8d7>Y@B!eoVBgSnmWY)={>fquK*F zBSk|8RK;;Ao#{j?9gxf#WYg+-BV)QTNo=ijU>!sLJNa>)EQ2-Nf((u4A6fZ5e+Gh* Ve)_7M0d)WX002ovPDHLkV1i9aF}45z literal 0 HcmV?d00001 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 %} From cad999456f88c15f4691eca51f8ca6aad117cfc5 Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Thu, 23 Jun 2022 14:43:04 +0800 Subject: [PATCH 35/47] Remove non-existent handler in ch02-01 --- tutorials-book/src/ch02-01-project-setup.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tutorials-book/src/ch02-01-project-setup.md b/tutorials-book/src/ch02-01-project-setup.md index 9c66128..6dbc3f4 100644 --- a/tutorials-book/src/ch02-01-project-setup.md +++ b/tutorials-book/src/ch02-01-project-setup.md @@ -37,7 +37,7 @@ async fn index() -> &'static str { #[launch] // The "main" function of the program fn rocket() -> _ { - rocket::build().mount("/", routes![index, bakeries]) + rocket::build().mount("/", routes![index]) } ``` From 17f48da4dc1556c23217cb655e4454fde2da47bb Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Thu, 23 Jun 2022 15:07:28 +0800 Subject: [PATCH 36/47] Draft ch02-02 --- tutorials-book/src/SUMMARY.md | 2 +- tutorials-book/src/ch02-01-project-setup.md | 4 + .../src/ch02-02-connect-to-database.md | 106 ++++++++++++++++++ 3 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 tutorials-book/src/ch02-02-connect-to-database.md diff --git a/tutorials-book/src/SUMMARY.md b/tutorials-book/src/SUMMARY.md index aff187c..9a00a2e 100644 --- a/tutorials-book/src/SUMMARY.md +++ b/tutorials-book/src/SUMMARY.md @@ -18,7 +18,7 @@ - [Chapter 2 - Integration with Rocket](ch02-00-integration-with-rocket.md) - [Project Setup](ch02-01-project-setup.md) - - [Connect to Database]() + - [Connect to Database](ch02-02-connect-to-database.md) - [Error Handling]() - [Web API Integration]() diff --git a/tutorials-book/src/ch02-01-project-setup.md b/tutorials-book/src/ch02-01-project-setup.md index 6dbc3f4..cb9e71a 100644 --- a/tutorials-book/src/ch02-01-project-setup.md +++ b/tutorials-book/src/ch02-01-project-setup.md @@ -44,6 +44,10 @@ fn rocket() -> _ { To verify it works: +```sh +$ cargo run +``` + ``` GET localhost:8000/ 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..7759bbd --- /dev/null +++ b/tutorials-book/src/ch02-02-connect-to-database.md @@ -0,0 +1,106 @@ +# 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) +} +``` + +To verify it works: + +```sh +$ cargo run +``` + +``` +GET localhost:8000/bakeries + +["Bakery Names", "In The", "Database", "If Any"] +``` From f566bb49779e71ce3fad1b08ebfb05d20cd0e079 Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Thu, 23 Jun 2022 15:09:15 +0800 Subject: [PATCH 37/47] Add section for simple frontend using templates in plan --- tutorials-book/src/SUMMARY.md | 1 + 1 file changed, 1 insertion(+) diff --git a/tutorials-book/src/SUMMARY.md b/tutorials-book/src/SUMMARY.md index 9a00a2e..fff6e34 100644 --- a/tutorials-book/src/SUMMARY.md +++ b/tutorials-book/src/SUMMARY.md @@ -21,6 +21,7 @@ - [Connect to Database](ch02-02-connect-to-database.md) - [Error Handling]() - [Web API Integration]() + - [Optional: Simple Frontend Using Templates]() ## Chapter 3 - Integration with GraphQL From 7d5317f4730dba11154d3c72527ca82ad43256d2 Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Thu, 23 Jun 2022 15:23:19 +0800 Subject: [PATCH 38/47] Draft ch02-03 --- tutorials-book/src/SUMMARY.md | 2 +- tutorials-book/src/ch02-03-error-handling.md | 64 ++++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 tutorials-book/src/ch02-03-error-handling.md diff --git a/tutorials-book/src/SUMMARY.md b/tutorials-book/src/SUMMARY.md index fff6e34..1561d42 100644 --- a/tutorials-book/src/SUMMARY.md +++ b/tutorials-book/src/SUMMARY.md @@ -19,7 +19,7 @@ - [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]() + - [Error Handling](ch02-03-error-handling.md) - [Web API Integration]() - [Optional: Simple Frontend Using Templates]() 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)) +} +``` From f42fd8c96d818723871397ad583ffbc0d3e80067 Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Thu, 23 Jun 2022 15:29:37 +0800 Subject: [PATCH 39/47] Add mounting new endpoint handler in ch02-02 --- tutorials-book/src/ch02-02-connect-to-database.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tutorials-book/src/ch02-02-connect-to-database.md b/tutorials-book/src/ch02-02-connect-to-database.md index 7759bbd..f64e8b2 100644 --- a/tutorials-book/src/ch02-02-connect-to-database.md +++ b/tutorials-book/src/ch02-02-connect-to-database.md @@ -91,6 +91,21 @@ async fn bakeries(db: &State) -> Json> { 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: From 3cc3c257ba31500272858499aa6462e05acb1a2d Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Thu, 23 Jun 2022 15:38:49 +0800 Subject: [PATCH 40/47] Draft ch02-04 --- tutorials-book/src/SUMMARY.md | 2 +- .../src/ch02-04-web-api-integration.md | 54 +++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 tutorials-book/src/ch02-04-web-api-integration.md diff --git a/tutorials-book/src/SUMMARY.md b/tutorials-book/src/SUMMARY.md index 1561d42..b18ebb9 100644 --- a/tutorials-book/src/SUMMARY.md +++ b/tutorials-book/src/SUMMARY.md @@ -20,7 +20,7 @@ - [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]() + - [Web API Integration](ch02-04-web-api-integration.md) - [Optional: Simple Frontend Using Templates]() ## Chapter 3 - Integration with GraphQL 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(()) +} +``` From 3bf9e443e024494e89c2489c6222fc01c95aac04 Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Thu, 23 Jun 2022 15:50:07 +0800 Subject: [PATCH 41/47] Draft ch02-05 --- tutorials-book/src/SUMMARY.md | 2 +- .../src/ch02-05-simple-frontend-using-templates.md | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 tutorials-book/src/ch02-05-simple-frontend-using-templates.md diff --git a/tutorials-book/src/SUMMARY.md b/tutorials-book/src/SUMMARY.md index b18ebb9..e186878 100644 --- a/tutorials-book/src/SUMMARY.md +++ b/tutorials-book/src/SUMMARY.md @@ -21,7 +21,7 @@ - [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]() + - [Optional: Simple Frontend Using Templates](ch02-05-simple-frontend-using-templates.md) ## Chapter 3 - Integration with GraphQL 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. From 7735811e3951dca71cc5e665ec8fa6946e54292b Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Mon, 27 Jun 2022 16:54:27 +0800 Subject: [PATCH 42/47] Merge Billy's changes --- bakery-backend/src/entities/mod.rs | 1 - bakery-backend/src/entities/prelude.rs | 1 - .../src/entities/seaql_migrations.rs | 22 ------------------- rocket-example/src/entities/mod.rs | 1 - rocket-example/src/entities/prelude.rs | 1 - .../src/entities/seaql_migrations.rs | 22 ------------------- tutorials-book/src/ch00-00-introduction.md | 4 ++-- .../ch01-00-build-backend-getting-started.md | 2 +- tutorials-book/src/ch01-02-migration-cli.md | 3 +++ tutorials-book/src/ch01-03-migration-api.md | 3 --- .../src/ch01-04-entity-generation.md | 1 - .../src/ch01-05-basic-crud-operations.md | 4 ++-- .../src/ch01-08-sql-with-sea-query.md | 4 ++++ 13 files changed, 12 insertions(+), 57 deletions(-) delete mode 100644 bakery-backend/src/entities/seaql_migrations.rs delete mode 100644 rocket-example/src/entities/seaql_migrations.rs diff --git a/bakery-backend/src/entities/mod.rs b/bakery-backend/src/entities/mod.rs index 2462768..b5f83b2 100644 --- a/bakery-backend/src/entities/mod.rs +++ b/bakery-backend/src/entities/mod.rs @@ -4,4 +4,3 @@ pub mod prelude; pub mod baker; pub mod bakery; -pub mod seaql_migrations; diff --git a/bakery-backend/src/entities/prelude.rs b/bakery-backend/src/entities/prelude.rs index 7fba2d7..8bc9c6e 100644 --- a/bakery-backend/src/entities/prelude.rs +++ b/bakery-backend/src/entities/prelude.rs @@ -2,4 +2,3 @@ pub use super::baker::Entity as Baker; pub use super::bakery::Entity as Bakery; -pub use super::seaql_migrations::Entity as SeaqlMigrations; diff --git a/bakery-backend/src/entities/seaql_migrations.rs b/bakery-backend/src/entities/seaql_migrations.rs deleted file mode 100644 index c32c701..0000000 --- a/bakery-backend/src/entities/seaql_migrations.rs +++ /dev/null @@ -1,22 +0,0 @@ -//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0 - -use sea_orm::entity::prelude::*; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] -#[sea_orm(table_name = "seaql_migrations")] -pub struct Model { - #[sea_orm(primary_key, auto_increment = false)] - pub version: String, - pub applied_at: i64, -} - -#[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/rocket-example/src/entities/mod.rs b/rocket-example/src/entities/mod.rs index 2462768..b5f83b2 100644 --- a/rocket-example/src/entities/mod.rs +++ b/rocket-example/src/entities/mod.rs @@ -4,4 +4,3 @@ pub mod prelude; pub mod baker; pub mod bakery; -pub mod seaql_migrations; diff --git a/rocket-example/src/entities/prelude.rs b/rocket-example/src/entities/prelude.rs index 7fba2d7..8bc9c6e 100644 --- a/rocket-example/src/entities/prelude.rs +++ b/rocket-example/src/entities/prelude.rs @@ -2,4 +2,3 @@ pub use super::baker::Entity as Baker; pub use super::bakery::Entity as Bakery; -pub use super::seaql_migrations::Entity as SeaqlMigrations; diff --git a/rocket-example/src/entities/seaql_migrations.rs b/rocket-example/src/entities/seaql_migrations.rs deleted file mode 100644 index c32c701..0000000 --- a/rocket-example/src/entities/seaql_migrations.rs +++ /dev/null @@ -1,22 +0,0 @@ -//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0 - -use sea_orm::entity::prelude::*; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] -#[sea_orm(table_name = "seaql_migrations")] -pub struct Model { - #[sea_orm(primary_key, auto_increment = false)] - pub version: String, - pub applied_at: i64, -} - -#[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/ch00-00-introduction.md b/tutorials-book/src/ch00-00-introduction.md index 1db5a3e..7c0b6da 100644 --- a/tutorials-book/src/ch00-00-introduction.md +++ b/tutorials-book/src/ch00-00-introduction.md @@ -12,9 +12,9 @@ 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 +`$` shows an operation is done on the console/shell #### Chapters diff --git a/tutorials-book/src/ch01-00-build-backend-getting-started.md b/tutorials-book/src/ch01-00-build-backend-getting-started.md index 39643c7..bb6eaaa 100644 --- a/tutorials-book/src/ch01-00-build-backend-getting-started.md +++ b/tutorials-book/src/ch01-00-build-backend-getting-started.md @@ -22,6 +22,6 @@ However, depending on the database of your choice, you need to pay attention to | SQLite (in file) | `sqlite:./sqlite.db?mode=rwc` | | SQLite (in memory) | `sqlite::memory:` | -We will showcase exactly how to how and where to use them in the next section. +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-02-migration-cli.md b/tutorials-book/src/ch01-02-migration-cli.md index 1e32c52..086778e 100644 --- a/tutorials-book/src/ch01-02-migration-cli.md +++ b/tutorials-book/src/ch01-02-migration-cli.md @@ -11,7 +11,10 @@ In this section, we define the following simple schema with migrations. 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 ``` diff --git a/tutorials-book/src/ch01-03-migration-api.md b/tutorials-book/src/ch01-03-migration-api.md index 765e1d1..c82f219 100644 --- a/tutorials-book/src/ch01-03-migration-api.md +++ b/tutorials-book/src/ch01-03-migration-api.md @@ -212,9 +212,6 @@ async fn run() -> Result<(), DbErr> { + let schema_manager = SchemaManager::new(db); // To investigate the schema -+ Migrator::install(db).await?; -+ assert!(schema_manager.has_table("seaql_migrations").await?); - + Migrator::refresh(db).await?; + assert!(schema_manager.has_table("bakery").await?); + assert!(schema_manager.has_table("baker").await?); diff --git a/tutorials-book/src/ch01-04-entity-generation.md b/tutorials-book/src/ch01-04-entity-generation.md index c3030c1..dac5540 100644 --- a/tutorials-book/src/ch01-04-entity-generation.md +++ b/tutorials-book/src/ch01-04-entity-generation.md @@ -28,7 +28,6 @@ bakery-backend │ │ bakery.rs │ │ mod.rs │ │ prelude.rs -│ │ seaql_migrations.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-05-basic-crud-operations.md b/tutorials-book/src/ch01-05-basic-crud-operations.md index 76a65ee..a355840 100644 --- a/tutorials-book/src/ch01-05-basic-crud-operations.md +++ b/tutorials-book/src/ch01-05-basic-crud-operations.md @@ -103,13 +103,13 @@ 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 must be set + 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 must be set + id: ActiveValue::Set(1), // The primary key must be set ..Default::default() }; sad_bakery.delete(db).await?; diff --git a/tutorials-book/src/ch01-08-sql-with-sea-query.md b/tutorials-book/src/ch01-08-sql-with-sea-query.md index d4497fb..7ddf2cc 100644 --- a/tutorials-book/src/ch01-08-sql-with-sea-query.md +++ b/tutorials-book/src/ch01-08-sql-with-sea-query.md @@ -15,6 +15,8 @@ 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) @@ -47,6 +49,8 @@ If all columns are of interest, then the generated `Model` structs (e.g. `baker: 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, From 1e3a658cb01b318310c1aa816cf4f6b2c2223b56 Mon Sep 17 00:00:00 2001 From: Nicolas Carranza Date: Tue, 7 Jun 2022 02:01:28 -0500 Subject: [PATCH 43/47] Update ch01-00-simple-crud-getting-started.md (#10) Missing features "attributes" for async-std. Thank you! --- .../ch01-00-simple-crud-getting-started.md | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/tutorials-book/src/ch01-00-simple-crud-getting-started.md b/tutorials-book/src/ch01-00-simple-crud-getting-started.md index e69de29..06015b4 100644 --- a/tutorials-book/src/ch01-00-simple-crud-getting-started.md +++ b/tutorials-book/src/ch01-00-simple-crud-getting-started.md @@ -0,0 +1,81 @@ +# 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. + From dce616068bfda0c758cf0457b04221f637a70393 Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Mon, 27 Jun 2022 17:06:48 +0800 Subject: [PATCH 44/47] Remove old tutorial section ch01-00-simple-crud-getting-started.md --- .../ch01-00-simple-crud-getting-started.md | 81 ------------------- 1 file changed, 81 deletions(-) delete mode 100644 tutorials-book/src/ch01-00-simple-crud-getting-started.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. - From 26b5389e952e05c9ad7a355b5ad1a6d645fd6f95 Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Mon, 27 Jun 2022 17:24:33 +0800 Subject: [PATCH 45/47] Update GitHub workflows for building the examples --- .github/workflows/main.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 From e2315ad8e4d7383122d2b87f19f1ba624af5dc00 Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Mon, 27 Jun 2022 17:36:47 +0800 Subject: [PATCH 46/47] Update README.md --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 From 5abd62b734cb9d0ee9f76e51f3babf1108546b13 Mon Sep 17 00:00:00 2001 From: Sanford Pun Date: Mon, 27 Jun 2022 17:38:50 +0800 Subject: [PATCH 47/47] Prepare for early release (postponing the release of chapter 3) --- tutorials-book/src/SUMMARY.md | 2 +- tutorials-book/src/ch03-00-integration-with-graphql.md | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 tutorials-book/src/ch03-00-integration-with-graphql.md diff --git a/tutorials-book/src/SUMMARY.md b/tutorials-book/src/SUMMARY.md index e186878..3c80ca3 100644 --- a/tutorials-book/src/SUMMARY.md +++ b/tutorials-book/src/SUMMARY.md @@ -25,4 +25,4 @@ ## Chapter 3 - Integration with GraphQL -- [Chapter 3 - Integration with GraphQL]() +- [Chapter 3 - Integration with GraphQL](ch03-00-integration-with-graphql.md) 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!*