diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3ae2a1a..f986dc0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -21,6 +21,8 @@ jobs: run: cargo build --manifest-path bakery-backend/Cargo.toml - name: Build rocket-example run: cargo build --manifest-path rocket-example/Cargo.toml + - name: Build graphql-example + run: cargo build --manifest-path graphql-example/Cargo.toml # Try to build mdbooks - name: Install mdbook diff --git a/Cargo.toml b/Cargo.toml index dc12608..4f807a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,2 +1,2 @@ [workspace] -members = ["bakery-backend", "rocket-example"] +members = ["bakery-backend", "graphql-example", "rocket-example"] diff --git a/README.md b/README.md index e3d87aa..5116ff8 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ The tutorials contain the following chapters: 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. +3. [**GraphQL Integration**](https://www.sea-ql.org/sea-orm-tutorial/ch03-00-integration-with-graphql.html) - This chapter extends the RESTful API of the web application in Chapter 2 to a GraphQL API with only one endpoint but enhanced flexibility of query. [![Discord](https://img.shields.io/discord/873880840487206962?label=Discord)](https://discord.com/invite/uCPdDXzbdv) diff --git a/graphql-example/.gitignore b/graphql-example/.gitignore new file mode 100644 index 0000000..1de5659 --- /dev/null +++ b/graphql-example/.gitignore @@ -0,0 +1 @@ +target \ No newline at end of file diff --git a/graphql-example/Cargo.toml b/graphql-example/Cargo.toml new file mode 100644 index 0000000..8e27182 --- /dev/null +++ b/graphql-example/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "graphql-example" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +async-graphql = "4.0.4" +async-graphql-rocket = "4.0.4" +rocket = { version = "^0.5.0-rc.2", features = ["json"] } +sea-orm = { version = "0.8.0", features = [ + "sqlx-mysql", + "runtime-async-std-native-tls", + "macros", +] } +sea-orm-migration = "0.8.3" +serde_json = "1.0.81" diff --git a/graphql-example/src/entities/baker.rs b/graphql-example/src/entities/baker.rs new file mode 100644 index 0000000..7371188 --- /dev/null +++ b/graphql-example/src/entities/baker.rs @@ -0,0 +1,35 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0 + +use async_graphql::SimpleObject; +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, SimpleObject)] +#[graphql(complex, name = "Baker")] +#[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/graphql-example/src/entities/bakery.rs b/graphql-example/src/entities/bakery.rs new file mode 100644 index 0000000..4ea7af2 --- /dev/null +++ b/graphql-example/src/entities/bakery.rs @@ -0,0 +1,28 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0 + +use async_graphql::SimpleObject; +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, SimpleObject)] +#[graphql(complex, name = "Bakery")] +#[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/graphql-example/src/entities/mod.rs b/graphql-example/src/entities/mod.rs new file mode 100644 index 0000000..b5f83b2 --- /dev/null +++ b/graphql-example/src/entities/mod.rs @@ -0,0 +1,6 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0 + +pub mod prelude; + +pub mod baker; +pub mod bakery; diff --git a/graphql-example/src/entities/prelude.rs b/graphql-example/src/entities/prelude.rs new file mode 100644 index 0000000..8bc9c6e --- /dev/null +++ b/graphql-example/src/entities/prelude.rs @@ -0,0 +1,4 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0 + +pub use super::baker::Entity as Baker; +pub use super::bakery::Entity as Bakery; diff --git a/graphql-example/src/main.rs b/graphql-example/src/main.rs new file mode 100644 index 0000000..94b8cc3 --- /dev/null +++ b/graphql-example/src/main.rs @@ -0,0 +1,82 @@ +mod entities; +mod migrator; +mod schema; +mod setup; + +use async_graphql::{ + http::{playground_source, GraphQLPlaygroundConfig}, + EmptySubscription, Schema, +}; +use async_graphql_rocket::*; +use rocket::{response::content, *}; +use schema::*; +use sea_orm::DbErr; +use setup::set_up_db; + +type SchemaType = Schema; + +#[get("/")] +fn index() -> String { + "Hello, bakeries!".to_owned() +} + +#[rocket::get("/graphql")] +fn graphql_playground() -> content::RawHtml { + content::RawHtml(playground_source(GraphQLPlaygroundConfig::new("/graphql"))) +} + +#[rocket::post("/graphql", data = "", format = "application/json")] +async fn graphql_request(schema: &State, request: GraphQLRequest) -> GraphQLResponse { + request.execute(schema).await +} + +#[launch] +async fn rocket() -> _ { + let db = match set_up_db().await { + Ok(db) => db, + Err(err) => panic!("{}", err), + }; + + let schema = Schema::build(QueryRoot, MutationRoot, EmptySubscription) + .data(db) + .finish(); + + rocket::build() + .manage(schema) + .mount("/", routes![index, graphql_playground, graphql_request]) + .register("/", catchers![not_found]) +} + +#[catch(404)] +pub fn not_found(req: &Request<'_>) -> String { + format!("{} not found.", req.uri()) +} + +#[derive(Responder)] +#[response(status = 500, content_type = "json")] +struct ErrorResponder { + message: String, +} + +#[allow(clippy::from_over_into)] +impl Into for DbErr { + fn into(self) -> ErrorResponder { + ErrorResponder { + message: self.to_string(), + } + } +} + +#[allow(clippy::from_over_into)] +impl Into for String { + fn into(self) -> ErrorResponder { + ErrorResponder { message: self } + } +} + +#[allow(clippy::from_over_into)] +impl Into for &str { + fn into(self) -> ErrorResponder { + self.to_owned().into() + } +} diff --git a/graphql-example/src/migrator/m20220602_000001_create_bakery_table.rs b/graphql-example/src/migrator/m20220602_000001_create_bakery_table.rs new file mode 100644 index 0000000..03ae011 --- /dev/null +++ b/graphql-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/graphql-example/src/migrator/m20220602_000002_create_baker_table.rs b/graphql-example/src/migrator/m20220602_000002_create_baker_table.rs new file mode 100644 index 0000000..91ee311 --- /dev/null +++ b/graphql-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/graphql-example/src/migrator/mod.rs b/graphql-example/src/migrator/mod.rs new file mode 100644 index 0000000..9cbc98b --- /dev/null +++ b/graphql-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/graphql-example/src/schema.rs b/graphql-example/src/schema.rs new file mode 100644 index 0000000..946da8b --- /dev/null +++ b/graphql-example/src/schema.rs @@ -0,0 +1,98 @@ +use async_graphql::{ComplexObject, Context, Object}; +use sea_orm::*; + +use crate::entities::{prelude::*, *}; + +pub(crate) struct QueryRoot; +pub(crate) struct MutationRoot; + +#[Object] +impl QueryRoot { + async fn hello(&self) -> String { + "Hello GraphQL".to_owned() + } + + async fn bakeries(&self, ctx: &Context<'_>) -> Result, DbErr> { + let db = ctx.data::().unwrap(); + + Bakery::find().all(db).await + } + + async fn bakery(&self, ctx: &Context<'_>, id: i32) -> Result, DbErr> { + let db = ctx.data::().unwrap(); + + Bakery::find_by_id(id).one(db).await + } + + async fn bakers(&self, ctx: &Context<'_>) -> Result, DbErr> { + let db = ctx.data::().unwrap(); + + Baker::find().all(db).await + } + + async fn baker(&self, ctx: &Context<'_>, id: i32) -> Result, DbErr> { + let db = ctx.data::().unwrap(); + + Baker::find_by_id(id).one(db).await + } +} + +#[ComplexObject] +impl bakery::Model { + async fn bakers(&self, ctx: &Context<'_>) -> Result, DbErr> { + let db = ctx.data::().unwrap(); + + self.find_related(Baker).all(db).await + } +} + +#[ComplexObject] +impl baker::Model { + async fn bakery(&self, ctx: &Context<'_>) -> Result { + let db = ctx.data::().unwrap(); + + self.find_related(Bakery).one(db).await.map(|b| b.unwrap()) + } +} + +#[Object] +impl MutationRoot { + async fn add_bakery(&self, ctx: &Context<'_>, name: String) -> Result { + let db = ctx.data::().unwrap(); + + let res = Bakery::insert(bakery::ActiveModel { + name: ActiveValue::Set(name), + profit_margin: ActiveValue::Set(0.0), + ..Default::default() + }) + .exec(db) + .await?; + + Bakery::find_by_id(res.last_insert_id) + .one(db) + .await + .map(|b| b.unwrap()) + } + + async fn add_baker( + &self, + ctx: &Context<'_>, + name: String, + bakery_id: i32, + ) -> Result { + let db = ctx.data::().unwrap(); + + let res = Baker::insert(baker::ActiveModel { + name: ActiveValue::Set(name), + bakery_id: ActiveValue::Set(bakery_id), + ..Default::default() + }) + .exec(db) + .await?; + + Baker::find_by_id(res.last_insert_id) + .one(db) + .await + .map(|b| b.unwrap()) + } +} diff --git a/graphql-example/src/setup.rs b/graphql-example/src/setup.rs new file mode 100644 index 0000000..f5dfea2 --- /dev/null +++ b/graphql-example/src/setup.rs @@ -0,0 +1,39 @@ +use sea_orm::*; + +const DATABASE_URL: &str = "mysql://root:root@localhost:3306"; + +pub(super) async fn set_up_db() -> Result { + let db = Database::connect(DATABASE_URL).await?; + + let db_name = "bakeries_db"; + let db = match db.get_database_backend() { + DbBackend::MySql => { + db.execute(Statement::from_string( + db.get_database_backend(), + format!("CREATE DATABASE IF NOT EXISTS `{}`;", db_name), + )) + .await?; + + let url = format!("{}/{}", DATABASE_URL, db_name); + Database::connect(&url).await? + } + DbBackend::Postgres => { + db.execute(Statement::from_string( + db.get_database_backend(), + format!("DROP DATABASE IF EXISTS \"{}\";", db_name), + )) + .await?; + db.execute(Statement::from_string( + db.get_database_backend(), + format!("CREATE DATABASE \"{}\";", db_name), + )) + .await?; + + let url = format!("{}/{}", DATABASE_URL, db_name); + Database::connect(&url).await? + } + DbBackend::Sqlite => db, + }; + + Ok(db) +} diff --git a/tutorials-book/src/SUMMARY.md b/tutorials-book/src/SUMMARY.md index 3c80ca3..9bf73bd 100644 --- a/tutorials-book/src/SUMMARY.md +++ b/tutorials-book/src/SUMMARY.md @@ -26,3 +26,7 @@ ## Chapter 3 - Integration with GraphQL - [Chapter 3 - Integration with GraphQL](ch03-00-integration-with-graphql.md) + - [Project Setup](ch03-01-project-setup.md) + - [Query](ch03-02-query.md) + - [Mutation](ch03-03-mutation.md) + - [Optional: GraphQL Playground](ch03-04-graphql-playground.md) diff --git a/tutorials-book/src/assets/graphql_playground_autocomplete.png b/tutorials-book/src/assets/graphql_playground_autocomplete.png new file mode 100644 index 0000000..b688d24 Binary files /dev/null and b/tutorials-book/src/assets/graphql_playground_autocomplete.png differ diff --git a/tutorials-book/src/assets/graphql_playground_docs.png b/tutorials-book/src/assets/graphql_playground_docs.png new file mode 100644 index 0000000..5c7ad88 Binary files /dev/null and b/tutorials-book/src/assets/graphql_playground_docs.png differ diff --git a/tutorials-book/src/assets/graphql_playground_hello.png b/tutorials-book/src/assets/graphql_playground_hello.png new file mode 100644 index 0000000..093e416 Binary files /dev/null and b/tutorials-book/src/assets/graphql_playground_hello.png differ diff --git a/tutorials-book/src/assets/graphql_playground_schema.png b/tutorials-book/src/assets/graphql_playground_schema.png new file mode 100644 index 0000000..b4acc5b Binary files /dev/null and b/tutorials-book/src/assets/graphql_playground_schema.png differ 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 bb6eaaa..63010b2 100644 --- a/tutorials-book/src/ch01-00-build-backend-getting-started.md +++ b/tutorials-book/src/ch01-00-build-backend-getting-started.md @@ -1,5 +1,7 @@ # Chapter 1 - Building a Backend with SeaORM +*Full source code available on [GitHub](https://github.com/SeaQL/sea-orm-tutorial/tree/master/bakery-backend).* + 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. diff --git a/tutorials-book/src/ch02-00-integration-with-rocket.md b/tutorials-book/src/ch02-00-integration-with-rocket.md index fd22084..ef3262b 100644 --- a/tutorials-book/src/ch02-00-integration-with-rocket.md +++ b/tutorials-book/src/ch02-00-integration-with-rocket.md @@ -1,5 +1,7 @@ # Chapter 2 - Integration with Rocket +*Full source code available on [GitHub](https://github.com/SeaQL/sea-orm-tutorial/tree/master/rocket-example).* + 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. diff --git a/tutorials-book/src/ch03-00-integration-with-graphql.md b/tutorials-book/src/ch03-00-integration-with-graphql.md index d3b3e29..1430a26 100644 --- a/tutorials-book/src/ch03-00-integration-with-graphql.md +++ b/tutorials-book/src/ch03-00-integration-with-graphql.md @@ -1,3 +1,17 @@ # Chapter 3 - Integration with GraphQL -*Coming Soon!* +*Full source code available on [GitHub](https://github.com/SeaQL/sea-orm-tutorial/tree/master/graphql-example).* + +We've created a web application with Rocket in Chapter 2, but you may notice that the RESTful API of the application lacks flexibility. + +For example, a `GET` request to the endpoint `/bakeries`, if successful, always gives us an array of names of the bakeries in the database. This is a toy implementation to demonstrate how things *could* work, but in reality we also need to provide ways for getting other attributes (e.g. *profit_margin*). + +If we simply return everything in a response every time, the user ends up receiving extra data they didn't need. If we want to keep things small, we'll have to design and model different use cases and create many endpoints to cater for them. + +To combat this, [GraphQL](https://graphql.org/), an alternative solution to RESTful API's, provides the flexibility we *(may)* need. + +With GraphQL, the user describes the desired data in the **request body**. Then the server **prepares exactly that** and sends it back in the response. As a result, only one endpoint is needed and no extra data is transmitted. + +As the experience is greatly enhanced on the client side, the burden of implementing ways to retrieve data flexibly is heavier on the server side. This problem is severe in the world of JavaScript, as quite a lot of boilerplate code is required to implement a GraphQL server there. However, thanks to Rust's powerful type system and macro support, many of GraphQL's features can actually be implemented rather painlessly. + +In this chapter, we'll build a Rocket application with GraphQL support powered by [`async_graphql`](https://crates.io/crates/async-graphql). Of course, `SeaORM` will serve as the bridge between the GraphQL resolvers and the database. diff --git a/tutorials-book/src/ch03-01-project-setup.md b/tutorials-book/src/ch03-01-project-setup.md new file mode 100644 index 0000000..58f1d61 --- /dev/null +++ b/tutorials-book/src/ch03-01-project-setup.md @@ -0,0 +1,142 @@ +# Project Setup + +## Create a Rocket application + +The initial setup of this chapter is vastly similar to that of the previous chapter. + +Refer to [Section 2.1](ch02-01-project-setup.md) and [Section 2.2](ch02-02-connect-to-database.md) to create a Rocket application and configure the database connection. + +## Set up `async_graphql` support + +Add the crates as dependencies: + +```diff +// Cargo.toml + +... + +[dependencies] ++ async-graphql = "4.0.4" ++ async-graphql-rocket = "4.0.4" + +... +``` + +Make sure the entities are generated ([Section 1.4](ch01-04-entity-generation.md)), and extend them to support basic GraphQL queries by attributes: + +```rust, no_run +// src/entities/baker.rs + ++ use async_graphql::SimpleObject; +use sea_orm::entity::prelude::*; + +- #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] ++ #[derive(Clone, Debug, PartialEq, DeriveEntityModel, SimpleObject)] +#[sea_orm(table_name = "baker")] +pub struct Model { + +... +``` + +```rust, no_run +// src/entities/bakery.rs + ++ use async_graphql::SimpleObject; +use sea_orm::entity::prelude::*; + +- #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] ++ #[derive(Clone, Debug, PartialEq, DeriveEntityModel, SimpleObject)] +#[sea_orm(table_name = "bakery")] +pub struct Model { + +... +``` + +Create a struct to serve as the root of queries. The root level query requests will be defined here: + +```rust, no_run +// src/schema.rs + +use async_graphql::Object; + +pub(crate) struct QueryRoot; + +#[Object] +impl QueryRoot { + async fn hello(&self) -> String { + "Hello GraphQL".to_owned() + } +} +``` + +Build the `Schema` and attach it to Rocket as a state, and create an endpoint to serve GraphQL requests: + +```rust, no_run +// src/main.rs + +mod entities; +mod migrator; ++ mod schema; +mod setup; + ++ use async_graphql::{EmptyMutation, EmptySubscription, Schema}; ++ use async_graphql_rocket::*; +use rocket::*; ++ use schema::*; +use sea_orm::*; +use setup::set_up_db; + ++ type SchemaType = Schema; + +... + ++ #[rocket::post("/graphql", data = "", format = "application/json")] ++ async fn graphql_request(schema: &State, request: GraphQLRequest) -> GraphQLResponse { ++ request.execute(schema).await ++ } + +... + +#[launch] +async fn rocket() -> _ { + let db = match set_up_db().await { + Ok(db) => db, + Err(err) => panic!("{}", err), + }; + + // Build the Schema ++ let schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription) ++ .data(db) // Add the database connection to the GraphQL global context ++ .finish(); + + rocket::build() +- .manage(db) // db is now managed by schema ++ .manage(schema) // schema is managed by rocket +- .mount("/", routes![index]) ++ .mount("/", routes![index, graphql_request]) + .register("/", catchers![not_found]) +} +... +``` + +To verify it works: + +```sh +$ cargo run +``` + +For debugging, GraphQL requests can be sent via the [GraphQL Playground](ch03-04-graphql-playground.md). + +``` +GraphQL Request: +{ + hello +} + +Response: +{ + "data": { + "hello": "Hello GraphQL" + } +} +``` diff --git a/tutorials-book/src/ch03-02-query.md b/tutorials-book/src/ch03-02-query.md new file mode 100644 index 0000000..6d65b6f --- /dev/null +++ b/tutorials-book/src/ch03-02-query.md @@ -0,0 +1,149 @@ +# Query with GraphQL + +To support queries, we extend the `QueryRoot` struct: + +## Basic Queries + +```rust, no_run +// src/schema.rs + +- use async_graphql::Object; ++ use async_graphql::{Context, Object}; ++ use sea_orm::*; + ++ use crate::entities::{prelude::*, *}; + +pub(crate) struct QueryRoot; + +#[Object] +impl QueryRoot { + ... + + // For finding all bakeries ++ async fn bakeries(&self, ctx: &Context<'_>) -> Result, DbErr> { ++ let db = ctx.data::().unwrap(); ++ Bakery::find().all(db).await ++ } + + // For finding one bakery by id ++ async fn bakery(&self, ctx: &Context<'_>, id: i32) -> Result, DbErr> { ++ let db = ctx.data::().unwrap(); ++ ++ Bakery::find_by_id(id).one(db).await ++ } +} +``` + +Example queries: + +``` +GraphQL Request: +{ + bakeries { + name + } +} + +Response: +{ + "data": { + "bakeries": [ + { + "name": "ABC Bakery" + }, + { + "name": "La Boulangerie" + }, + { + "name": "Sad Bakery" + } + ] + } +} +``` + +``` +GraphQL Request: +{ + bakery(id: 1) { + name + } +} + +Response: +{ + "data": { + "bakery": { + "name": "ABC Bakery" + } + } +} +``` + +*If `name` is replaced by other fields of `bakery::Model`, the requests will automatically be supported. This is because `bakery::Model` derives from `async_graphql::SimpleObject` in the previous section.* + +## Relational Query + +One of the most appealing features of GraphQL is its convenient support for relational queries. + +Recall that a Bakery may hire many Bakers. We can give `bakery::Model` ComplexObject support to allow for this relational query. + +```rust, no_run +// src/entities/bakery.rs + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, SimpleObject)] ++ #[graphql(complex, name = "Bakery")] +#[sea_orm(table_name = "bakery")] +pub struct Model { + +... +``` + +```rust, no_run +// src/schema.rs + +- use async_graphql::{Context, Object}; ++ use async_graphql::{ComplexObject, Context, Object}; + +... + ++ #[ComplexObject] ++ impl bakery::Model { ++ async fn bakers(&self, ctx: &Context<'_>) -> Result, DbErr> { ++ let db = ctx.data::().unwrap(); ++ ++ self.find_related(Baker).all(db).await ++ } ++ } +``` + +Example query: + +``` +GraphQL Request: +{ + bakery(id: 1) { + name, + bakers { + name + } + } +} + +Response: +{ + "data": { + "bakery": { + "name": "ABC Bakery", + "bakers": [ + { + "name": "Sanford" + }, + { + "name": "Billy" + } + ] + } + } +} +``` diff --git a/tutorials-book/src/ch03-03-mutation.md b/tutorials-book/src/ch03-03-mutation.md new file mode 100644 index 0000000..2f79d98 --- /dev/null +++ b/tutorials-book/src/ch03-03-mutation.md @@ -0,0 +1,162 @@ +# Mutation + +## Preparation + +To support mutations with GraphQL, we need to create a struct to serve as the root, as for queries. + +```rust, no_run +// src/schema.rs + +... + +pub(crate) struct QueryRoot; ++ pub(crate) struct MutationRoot; + +... +``` + +```rust, no_run +// src/main.rs + +... + +- use async_graphql::{EmptyMutation, EmptySubscription, Schema}; ++ use async_graphql::{EmptySubscription, Schema}; + +... + +- type SchemaType = Schema; ++ type SchemaType = Schema; + +... + +#[launch] +async fn rocket() -> _ { + let db = match set_up_db().await { + Ok(db) => db, + Err(err) => panic!("{}", err), + }; + +- let schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription) ++ let schema = Schema::build(QueryRoot, MutationRoot, EmptySubscription) + .data(db) // Add the database connection to the GraphQL global context + .finish(); + +... +``` + +## Define resolvers + +Define the mutation resolvers just like the ones for queries: + +```rust, no_run +// src/schema.rs + +... + +#[Object] +impl MutationRoot { + // For inserting a bakery + async fn add_bakery(&self, ctx: &Context<'_>, name: String) -> Result { + let db = ctx.data::().unwrap(); + + let res = Bakery::insert(bakery::ActiveModel { + name: ActiveValue::Set(name), + profit_margin: ActiveValue::Set(0.0), + ..Default::default() + }) + .exec(db) + .await?; + + Bakery::find_by_id(res.last_insert_id) + .one(db) + .await + .map(|b| b.unwrap()) + } + + // For inserting a baker + async fn add_baker( + &self, + ctx: &Context<'_>, + name: String, + bakery_id: i32, + ) -> Result { + let db = ctx.data::().unwrap(); + + let res = Baker::insert(baker::ActiveModel { + name: ActiveValue::Set(name), + bakery_id: ActiveValue::Set(bakery_id), + ..Default::default() + }) + .exec(db) + .await?; + + Baker::find_by_id(res.last_insert_id) + .one(db) + .await + .map(|b| b.unwrap()) + } +} +``` + +Examples: + +``` +GraphQL Request: +mutation { + addBakery(name: "Excellent Bakery") { + id, + name, + profitMargin + } +} + +Response: +{ + "data": { + "addBakery": { + "id": 4, + "name": "Excellent Bakery", + "profitMargin": 0 + } + } +} +``` + +``` +GraphQL Request: +mutation { + addBaker(name: "Chris", bakeryId: 1) { + id, + name, + bakery { + bakers { + name + } + } + } +} + +Response: +{ + "data": { + "addBaker": { + "id": 3, + "name": "Chris", + "bakery": { + "bakers": [ + { + "name": "Sanford" + }, + { + "name": "Billy" + }, + { + "name": "Chris" + } + ] + } + } + } +} +``` diff --git a/tutorials-book/src/ch03-04-graphql-playground.md b/tutorials-book/src/ch03-04-graphql-playground.md new file mode 100644 index 0000000..6c06109 --- /dev/null +++ b/tutorials-book/src/ch03-04-graphql-playground.md @@ -0,0 +1,66 @@ +# Optional: GraphQL Playground + +## Overview + +When you are developing the GraphQL API, you probably want to send requests to verify the correctness of your implementation. + +A locally hosted GraphQL Playground is perfect for this. + +Simply type your request body on the left and the response data will be shown on the right. + +![GraphQL Playground with the "hello" test request.](assets/graphql_playground_hello.png) + +## Setup + +Add a `GET` handler that returns a `rocket::response::content::RawHTML`, which is the Playground HTML generated by `async-graphql`: + +```rust, no_run +// src/main.rs + +... + +use async_graphql::{EmptySubscription, Schema}; +use async_graphql::{ ++ http::{playground_source, GraphQLPlaygroundConfig}, + EmptySubscription, Schema, +}; +use async_graphql_rocket::*; +- use rocket::*; ++ use rocket::{response::content, *}; + +... + ++ #[rocket::get("/graphql")] ++ fn graphql_playground() -> content::RawHtml { ++ content::RawHtml(playground_source(GraphQLPlaygroundConfig::new("/graphql"))) ++ } + +... + + rocket::build() + .manage(schema) +- .mount("/", routes![index, graphql_request]) ++ .mount("/", routes![index, graphql_playground, graphql_request]) + +... +``` + +## Major Merits + +### Autocomplete requests + +![Requests are autocompleted in GraphQL Playground](assets/graphql_playground_autocomplete.png) + +The Playground discovers all the types and their attributes and autocompletes your requests for you. + +### Preview Documentation + +![Documentation is easily accessible in GraphQL Playground](assets/graphql_playground_docs.png) + +You can open up the DOCS panel on the side to see the comprehensive documentation for your GraphQL API. + +### Export Schema + +![The schema can be viewed and exported in GraphQL Playground](assets/graphql_playground_schema.png) + +On the SCHEMA panel, you can view the [schema](https://graphql.org/learn/schema/) of your GraphQL API, and export it in JSON or SDL format.