├── .gitignore ├── .rustfmt.toml ├── Cargo.toml ├── README.md ├── actix-web ├── error-extensions │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── starwars │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── subscription │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── token-from-header │ ├── Cargo.toml │ └── src │ │ └── main.rs └── upload │ ├── Cargo.toml │ └── src │ └── main.rs ├── axum ├── starwars │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── subscription │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── token-from-header │ ├── Cargo.toml │ └── src │ │ └── main.rs └── upload │ ├── Cargo.toml │ └── src │ └── main.rs ├── federation ├── dynamic-schema │ ├── README.md │ ├── federation-accounts │ │ ├── Cargo.toml │ │ └── src │ │ │ └── main.rs │ ├── federation-products │ │ ├── Cargo.toml │ │ └── src │ │ │ └── main.rs │ ├── federation-reviews │ │ ├── Cargo.toml │ │ └── src │ │ │ └── main.rs │ ├── query.graphql │ └── start.sh └── static-schema │ ├── README.md │ ├── directives │ ├── Cargo.toml │ └── src │ │ └── lib.rs │ ├── federation-accounts │ ├── Cargo.toml │ └── src │ │ └── main.rs │ ├── federation-products │ ├── Cargo.toml │ └── src │ │ └── main.rs │ ├── federation-reviews │ ├── Cargo.toml │ └── src │ │ └── main.rs │ ├── query.graphql │ └── start.sh ├── loco ├── starwars │ ├── .cargo │ │ └── config.toml │ ├── Cargo.toml │ ├── README.md │ ├── config │ │ └── development.yaml │ └── src │ │ ├── app.rs │ │ ├── bin │ │ └── main.rs │ │ ├── controllers │ │ ├── graphiql.rs │ │ └── mod.rs │ │ └── lib.rs └── subscription │ ├── .cargo │ └── config.toml │ ├── Cargo.toml │ ├── README.md │ ├── config │ └── development.yaml │ └── src │ ├── app.rs │ ├── bin │ └── main.rs │ ├── controllers │ ├── graphiql.rs │ └── mod.rs │ └── lib.rs ├── models ├── books │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ └── simple_broker.rs ├── dynamic-books │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ ├── model.rs │ │ └── simple_broker.rs ├── dynamic-files │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── dynamic-starwars │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ └── model.rs ├── files │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── starwars │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ └── model.rs └── token │ ├── Cargo.toml │ └── src │ └── lib.rs ├── poem ├── dynamic-books │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── dynamic-schema │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── dynamic-starwars │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── dynamic-upload │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── opentelemetry-basic │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── starwars │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── subscription-redis │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── subscription │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── token-from-header │ ├── Cargo.toml │ └── src │ │ └── main.rs └── upload │ ├── Cargo.toml │ └── src │ └── main.rs ├── rocket ├── starwars │ ├── Cargo.toml │ ├── Rocket.toml │ └── src │ │ └── main.rs └── upload │ ├── Cargo.toml │ ├── Rocket.toml │ └── src │ └── main.rs ├── tide ├── dataloader-postgres │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── dataloader │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── starwars │ ├── Cargo.toml │ └── src │ │ └── main.rs └── subscription │ ├── Cargo.toml │ └── src │ └── main.rs └── warp ├── starwars ├── Cargo.toml └── src │ └── main.rs ├── subscription ├── Cargo.toml └── src │ └── main.rs └── token-from-header ├── Cargo.toml └── src └── main.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | node_modules 3 | .idea 4 | .DS_Store 5 | Cargo.lock 6 | memory: 7 | memory:* 8 | 9 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2024" 2 | newline_style = "Unix" 3 | # comments 4 | normalize_comments = true 5 | wrap_comments = true 6 | format_code_in_doc_comments = true 7 | # imports 8 | imports_granularity = "Crate" 9 | group_imports = "StdExternalCrate" 10 | # report 11 | #report_fixme="Unnumbered" 12 | #report_todo="Unnumbered" 13 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "models/starwars", 5 | "models/books", 6 | "models/files", 7 | "models/token", 8 | "models/dynamic-starwars", 9 | "models/dynamic-files", 10 | 11 | "poem/opentelemetry-basic", 12 | "poem/starwars", 13 | "poem/subscription", 14 | "poem/subscription-redis", 15 | "poem/token-from-header", 16 | "poem/upload", 17 | "poem/dynamic-schema", 18 | "poem/dynamic-starwars", 19 | "poem/dynamic-books", 20 | "poem/dynamic-upload", 21 | 22 | "actix-web/token-from-header", 23 | "actix-web/subscription", 24 | "actix-web/upload", 25 | "actix-web/starwars", 26 | "actix-web/error-extensions", 27 | 28 | "warp/starwars", 29 | "warp/subscription", 30 | "warp/token-from-header", 31 | 32 | "rocket/starwars", 33 | "rocket/upload", 34 | 35 | "axum/starwars", 36 | "axum/subscription", 37 | "axum/upload", 38 | "axum/token-from-header", 39 | 40 | "tide/starwars", 41 | "tide/dataloader", 42 | "tide/dataloader-postgres", 43 | "tide/subscription", 44 | 45 | "federation/static-schema/federation-accounts", 46 | "federation/static-schema/federation-products", 47 | "federation/static-schema/federation-reviews", 48 | 49 | "federation/dynamic-schema/federation-accounts", 50 | "federation/dynamic-schema/federation-products", 51 | "federation/dynamic-schema/federation-reviews", 52 | ] 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Examples for async-graphql 2 | 3 | A git submodule that shows example async-graphql projects. 4 | 5 | 6 | ## Directory structure 7 | 8 | - [poem] Examples for `poem` 9 | - [actix-web] Examples for `actix-web` 10 | - [warp] Examples for `warp` 11 | - [tide] Examples for `tide` 12 | - [rocket] Examples for `rocket` 13 | - [axum] Examples for `axum` 14 | - [loco] Examples for `loco` 15 | - [federation] Examples for [Apollo Federation](https://www.apollographql.com/docs/federation/) 16 | 17 | 18 | ## Running Examples 19 | 20 | To run the examples, clone the top-level repo, [async-graphql](https://github.com/async-graphql/async-graphql) and then issue the following commands: 21 | 22 | ```bash 23 | git clone async-graphql/async-graphql 24 | # in async-graphql repo, install needed dependencies 25 | cargo build 26 | 27 | # update this repo as a git submodule 28 | git submodule update 29 | ``` 30 | 31 | To run the example axum-starwars: 32 | ``` 33 | # change into the example folder and run a relevant binary 34 | cargo run --bin axum-starwars 35 | ``` 36 | 37 | To list all available binary targets: 38 | ``` 39 | cargo run --bin 40 | ``` -------------------------------------------------------------------------------- /actix-web/error-extensions/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "actix-web-error-extensions" 3 | version = "0.1.1" 4 | authors = ["sunli "] 5 | edition = "2024" 6 | 7 | [dependencies] 8 | async-graphql = { path = "../../.." } 9 | async-graphql-actix-web = { path = "../../../integrations/actix-web" } 10 | actix-web = { version = "4.5.1", default-features = false, features = [ 11 | "macros", 12 | ] } 13 | thiserror = "1.0" 14 | serde_json = "1.0" 15 | -------------------------------------------------------------------------------- /actix-web/error-extensions/src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate thiserror; 3 | 4 | use actix_web::{App, HttpResponse, HttpServer, guard, web}; 5 | use async_graphql::{ 6 | EmptyMutation, EmptySubscription, ErrorExtensions, FieldError, FieldResult, Object, ResultExt, 7 | Schema, http::GraphiQLSource, 8 | }; 9 | use async_graphql_actix_web::GraphQL; 10 | 11 | #[derive(Debug, Error)] 12 | pub enum MyError { 13 | #[error("Could not find resource")] 14 | NotFound, 15 | 16 | #[error("ServerError")] 17 | ServerError(String), 18 | 19 | #[error("No Extensions")] 20 | ErrorWithoutExtensions, 21 | } 22 | 23 | impl ErrorExtensions for MyError { 24 | // lets define our base extensions 25 | fn extend(&self) -> FieldError { 26 | self.extend_with(|err, e| match err { 27 | MyError::NotFound => e.set("code", "NOT_FOUND"), 28 | MyError::ServerError(reason) => e.set("reason", reason.to_string()), 29 | MyError::ErrorWithoutExtensions => {} 30 | }) 31 | } 32 | } 33 | 34 | struct QueryRoot; 35 | 36 | #[Object] 37 | impl QueryRoot { 38 | // It works on foreign types without extensions as before 39 | async fn parse_without_extensions(&self) -> FieldResult { 40 | Ok("234a".parse()?) 41 | } 42 | 43 | // Foreign types can be extended 44 | async fn parse_with_extensions(&self) -> FieldResult { 45 | "234a" 46 | .parse() 47 | .map_err(|e: std::num::ParseIntError| e.extend_with(|_, e| e.set("code", 404))) 48 | } 49 | 50 | // THIS does unfortunately NOT work because ErrorExtensions is implemented for 51 | // &E and not E. Which is necessary for the overwrite by the user. 52 | 53 | // async fn parse_with_extensions_result(&self) -> FieldResult { 54 | // Ok("234a".parse().extend_err(|_| json!({"code": 404}))?) 55 | // } 56 | 57 | // Using our own types we can implement some base extensions 58 | async fn extend(&self) -> FieldResult { 59 | Err(MyError::NotFound.extend()) 60 | } 61 | 62 | // Or on the result 63 | async fn extend_result(&self) -> FieldResult { 64 | Err(MyError::NotFound).extend() 65 | } 66 | 67 | // Base extensions can be further extended 68 | async fn more_extensions(&self) -> FieldResult { 69 | // resolves to extensions: { "code": "NOT_FOUND", "reason": "my reason" } 70 | Err(MyError::NotFound.extend_with(|_e, e| e.set("reason", "my reason"))) 71 | } 72 | 73 | // works with results as well 74 | async fn more_extensions_on_result(&self) -> FieldResult { 75 | // resolves to extensions: { "code": "NOT_FOUND", "reason": "my reason" } 76 | Err(MyError::NotFound).extend_err(|_e, e| e.set("reason", "my reason")) 77 | } 78 | 79 | // extend_with is chainable 80 | async fn chainable_extensions(&self) -> FieldResult { 81 | let err = MyError::NotFound 82 | .extend_with(|_, e| e.set("ext1", 1)) 83 | .extend_with(|_, e| e.set("ext2", 2)) 84 | .extend_with(|_, e| e.set("ext3", 3)); 85 | Err(err) 86 | } 87 | 88 | // extend_with overwrites keys which are already present 89 | async fn overwrite(&self) -> FieldResult { 90 | Err(MyError::NotFound.extend_with(|_, e| e.set("code", "overwritten"))) 91 | } 92 | } 93 | 94 | async fn gql_playgound() -> HttpResponse { 95 | HttpResponse::Ok() 96 | .content_type("text/html; charset=utf-8") 97 | .body(GraphiQLSource::build().endpoint("/").finish()) 98 | } 99 | 100 | #[actix_web::main] 101 | async fn main() -> std::io::Result<()> { 102 | println!("GraphiQL IDE: http://localhost:8000"); 103 | 104 | HttpServer::new(move || { 105 | let schema = Schema::new(QueryRoot, EmptyMutation, EmptySubscription); 106 | 107 | App::new() 108 | .service( 109 | web::resource("/") 110 | .guard(guard::Post()) 111 | .to(GraphQL::new(schema)), 112 | ) 113 | .service(web::resource("/").guard(guard::Get()).to(gql_playgound)) 114 | }) 115 | .bind("127.0.0.1:8000")? 116 | .run() 117 | .await 118 | } 119 | -------------------------------------------------------------------------------- /actix-web/starwars/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "actix-web-starwars" 3 | version = "0.1.1" 4 | authors = ["sunli "] 5 | edition = "2024" 6 | 7 | [dependencies] 8 | async-graphql = { path = "../../.." } 9 | async-graphql-actix-web = { path = "../../../integrations/actix-web" } 10 | actix-web = { version = "4.5.1", default-features = false, features = [ 11 | "macros", 12 | ] } 13 | starwars = { path = "../../models/starwars" } 14 | -------------------------------------------------------------------------------- /actix-web/starwars/src/main.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{App, HttpResponse, HttpServer, Result, guard, web}; 2 | use async_graphql::{EmptyMutation, EmptySubscription, Schema, http::GraphiQLSource}; 3 | use async_graphql_actix_web::GraphQL; 4 | use starwars::{QueryRoot, StarWars}; 5 | 6 | async fn index_graphiql() -> Result { 7 | Ok(HttpResponse::Ok() 8 | .content_type("text/html; charset=utf-8") 9 | .body(GraphiQLSource::build().endpoint("/").finish())) 10 | } 11 | 12 | #[actix_web::main] 13 | async fn main() -> std::io::Result<()> { 14 | println!("GraphiQL IDE: http://localhost:8000"); 15 | 16 | HttpServer::new(move || { 17 | let schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription) 18 | .data(StarWars::new()) 19 | .finish(); 20 | 21 | App::new() 22 | .service( 23 | web::resource("/") 24 | .guard(guard::Post()) 25 | .to(GraphQL::new(schema)), 26 | ) 27 | .service(web::resource("/").guard(guard::Get()).to(index_graphiql)) 28 | }) 29 | .bind("127.0.0.1:8000")? 30 | .run() 31 | .await 32 | } 33 | -------------------------------------------------------------------------------- /actix-web/subscription/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "actix-web-subscription" 3 | version = "0.1.1" 4 | authors = ["sunli "] 5 | edition = "2024" 6 | 7 | [dependencies] 8 | async-graphql = { path = "../../.." } 9 | async-graphql-actix-web = { path = "../../../integrations/actix-web" } 10 | actix-web = { version = "4.5.1", default-features = false, features = [ 11 | "macros", 12 | ] } 13 | books = { path = "../../models/books" } 14 | -------------------------------------------------------------------------------- /actix-web/subscription/src/main.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{App, HttpRequest, HttpResponse, HttpServer, Result, guard, web}; 2 | use async_graphql::{Schema, http::GraphiQLSource}; 3 | use async_graphql_actix_web::{GraphQL, GraphQLSubscription}; 4 | use books::{BooksSchema, MutationRoot, QueryRoot, Storage, SubscriptionRoot}; 5 | 6 | async fn index_graphiql() -> Result { 7 | Ok(HttpResponse::Ok() 8 | .content_type("text/html; charset=utf-8") 9 | .body( 10 | GraphiQLSource::build() 11 | .endpoint("/") 12 | .subscription_endpoint("/") 13 | .finish(), 14 | )) 15 | } 16 | 17 | async fn index_ws( 18 | schema: web::Data, 19 | req: HttpRequest, 20 | payload: web::Payload, 21 | ) -> Result { 22 | GraphQLSubscription::new(Schema::clone(&*schema)).start(&req, payload) 23 | } 24 | 25 | #[actix_web::main] 26 | async fn main() -> std::io::Result<()> { 27 | println!("GraphiQL IDE: http://localhost:8000"); 28 | 29 | HttpServer::new(move || { 30 | let schema = Schema::build(QueryRoot, MutationRoot, SubscriptionRoot) 31 | .data(Storage::default()) 32 | .finish(); 33 | 34 | App::new() 35 | .service( 36 | web::resource("/") 37 | .guard(guard::Post()) 38 | .to(GraphQL::new(schema.clone())), 39 | ) 40 | .service( 41 | web::resource("/") 42 | .guard(guard::Get()) 43 | .guard(guard::Header("upgrade", "websocket")) 44 | .app_data(web::Data::new(schema)) 45 | .to(index_ws), 46 | ) 47 | .service(web::resource("/").guard(guard::Get()).to(index_graphiql)) 48 | }) 49 | .bind("127.0.0.1:8000")? 50 | .run() 51 | .await 52 | } 53 | -------------------------------------------------------------------------------- /actix-web/token-from-header/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "actix-web-token-from-header" 3 | version = "0.1.1" 4 | authors = ["sunli "] 5 | edition = "2024" 6 | 7 | [dependencies] 8 | async-graphql = { path = "../../.." } 9 | async-graphql-actix-web = { path = "../../../integrations/actix-web" } 10 | actix-web = { version = "4.5.1", default-features = false, features = [ 11 | "macros", 12 | ] } 13 | token = { path = "../../models/token" } 14 | -------------------------------------------------------------------------------- /actix-web/token-from-header/src/main.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{ 2 | App, HttpRequest, HttpResponse, HttpServer, Result, guard, http::header::HeaderMap, web, 3 | }; 4 | use async_graphql::{EmptyMutation, Schema, http::GraphiQLSource}; 5 | use async_graphql_actix_web::{GraphQLRequest, GraphQLResponse, GraphQLSubscription}; 6 | use token::{QueryRoot, SubscriptionRoot, Token, TokenSchema, on_connection_init}; 7 | 8 | async fn graphiql() -> HttpResponse { 9 | HttpResponse::Ok() 10 | .content_type("text/html; charset=utf-8") 11 | .body( 12 | GraphiQLSource::build() 13 | .endpoint("/") 14 | .subscription_endpoint("/ws") 15 | .finish(), 16 | ) 17 | } 18 | 19 | fn get_token_from_headers(headers: &HeaderMap) -> Option { 20 | headers 21 | .get("Token") 22 | .and_then(|value| value.to_str().map(|s| Token(s.to_string())).ok()) 23 | } 24 | 25 | async fn index( 26 | schema: web::Data, 27 | req: HttpRequest, 28 | gql_request: GraphQLRequest, 29 | ) -> GraphQLResponse { 30 | let mut request = gql_request.into_inner(); 31 | if let Some(token) = get_token_from_headers(req.headers()) { 32 | request = request.data(token); 33 | } 34 | schema.execute(request).await.into() 35 | } 36 | 37 | async fn index_ws( 38 | schema: web::Data, 39 | req: HttpRequest, 40 | payload: web::Payload, 41 | ) -> Result { 42 | GraphQLSubscription::new(Schema::clone(&*schema)) 43 | .on_connection_init(on_connection_init) 44 | .start(&req, payload) 45 | } 46 | 47 | #[actix_web::main] 48 | async fn main() -> std::io::Result<()> { 49 | let schema = Schema::new(QueryRoot, EmptyMutation, SubscriptionRoot); 50 | 51 | println!("GraphiQL IDE: http://localhost:8000"); 52 | 53 | HttpServer::new(move || { 54 | App::new() 55 | .app_data(web::Data::new(schema.clone())) 56 | .service(web::resource("/").guard(guard::Get()).to(graphiql)) 57 | .service(web::resource("/").guard(guard::Post()).to(index)) 58 | .service(web::resource("/ws").to(index_ws)) 59 | }) 60 | .bind("127.0.0.1:8000")? 61 | .run() 62 | .await 63 | } 64 | -------------------------------------------------------------------------------- /actix-web/upload/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "actix-web-upload" 3 | version = "0.1.1" 4 | authors = ["sunli "] 5 | edition = "2024" 6 | 7 | [dependencies] 8 | async-graphql = { path = "../../.." } 9 | async-graphql-actix-web = { path = "../../../integrations/actix-web" } 10 | actix-web = { version = "4.5.1", default-features = false, features = [ 11 | "macros", 12 | ] } 13 | files = { path = "../../models/files" } 14 | -------------------------------------------------------------------------------- /actix-web/upload/src/main.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{App, HttpResponse, HttpServer, guard, web, web::Data}; 2 | use async_graphql::{ 3 | EmptySubscription, Schema, 4 | http::{GraphiQLSource, MultipartOptions}, 5 | }; 6 | use async_graphql_actix_web::{GraphQLRequest, GraphQLResponse}; 7 | use files::{FilesSchema, MutationRoot, QueryRoot, Storage}; 8 | 9 | async fn index(schema: web::Data, req: GraphQLRequest) -> GraphQLResponse { 10 | schema.execute(req.into_inner()).await.into() 11 | } 12 | 13 | async fn gql_playgound() -> HttpResponse { 14 | HttpResponse::Ok() 15 | .content_type("text/html; charset=utf-8") 16 | .body(GraphiQLSource::build().endpoint("/").finish()) 17 | } 18 | 19 | #[actix_web::main] 20 | async fn main() -> std::io::Result<()> { 21 | let schema = Schema::build(QueryRoot, MutationRoot, EmptySubscription) 22 | .data(Storage::default()) 23 | .finish(); 24 | 25 | println!("GraphiQL IDE: http://localhost:8000"); 26 | 27 | HttpServer::new(move || { 28 | App::new() 29 | .app_data(Data::new(schema.clone())) 30 | .service( 31 | web::resource("/") 32 | .guard(guard::Post()) 33 | .to(index) 34 | .app_data(MultipartOptions::default().max_num_files(3)), 35 | ) 36 | .service(web::resource("/").guard(guard::Get()).to(gql_playgound)) 37 | }) 38 | .bind("127.0.0.1:8000")? 39 | .run() 40 | .await 41 | } 42 | -------------------------------------------------------------------------------- /axum/starwars/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "axum-starwars" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | async-graphql = { path = "../../.." } 8 | async-graphql-axum = { path = "../../../integrations/axum" } 9 | tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] } 10 | starwars = { path = "../../models/starwars" } 11 | axum = { version = "0.8.1" } 12 | -------------------------------------------------------------------------------- /axum/starwars/src/main.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::{EmptyMutation, EmptySubscription, Schema, http::GraphiQLSource}; 2 | use async_graphql_axum::GraphQL; 3 | use axum::{ 4 | Router, 5 | response::{self, IntoResponse}, 6 | routing::get, 7 | }; 8 | use starwars::{QueryRoot, StarWars}; 9 | use tokio::net::TcpListener; 10 | 11 | async fn graphiql() -> impl IntoResponse { 12 | response::Html(GraphiQLSource::build().endpoint("/").finish()) 13 | } 14 | 15 | #[tokio::main] 16 | async fn main() { 17 | let schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription) 18 | .data(StarWars::new()) 19 | .finish(); 20 | 21 | let app = Router::new().route("/", get(graphiql).post_service(GraphQL::new(schema))); 22 | 23 | println!("GraphiQL IDE: http://localhost:8000"); 24 | 25 | axum::serve(TcpListener::bind("127.0.0.1:8000").await.unwrap(), app) 26 | .await 27 | .unwrap(); 28 | } 29 | -------------------------------------------------------------------------------- /axum/subscription/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "axum-subscription" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | async-graphql = { path = "../../.." } 8 | async-graphql-axum = { path = "../../../integrations/axum" } 9 | tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] } 10 | books = { path = "../../models/books" } 11 | axum = { version = "0.8.1", features = ["ws"] } 12 | -------------------------------------------------------------------------------- /axum/subscription/src/main.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::{Schema, http::GraphiQLSource}; 2 | use async_graphql_axum::{GraphQL, GraphQLSubscription}; 3 | use axum::{ 4 | Router, 5 | response::{self, IntoResponse}, 6 | routing::get, 7 | }; 8 | use books::{MutationRoot, QueryRoot, Storage, SubscriptionRoot}; 9 | use tokio::net::TcpListener; 10 | 11 | async fn graphiql() -> impl IntoResponse { 12 | response::Html( 13 | GraphiQLSource::build() 14 | .endpoint("/") 15 | .subscription_endpoint("/ws") 16 | .finish(), 17 | ) 18 | } 19 | 20 | #[tokio::main] 21 | async fn main() { 22 | let schema = Schema::build(QueryRoot, MutationRoot, SubscriptionRoot) 23 | .data(Storage::default()) 24 | .finish(); 25 | 26 | let app = Router::new() 27 | .route( 28 | "/", 29 | get(graphiql).post_service(GraphQL::new(schema.clone())), 30 | ) 31 | .route_service("/ws", GraphQLSubscription::new(schema)); 32 | 33 | println!("GraphiQL IDE: http://localhost:8000"); 34 | 35 | axum::serve(TcpListener::bind("127.0.0.1:8000").await.unwrap(), app) 36 | .await 37 | .unwrap(); 38 | } 39 | -------------------------------------------------------------------------------- /axum/token-from-header/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "axum-token-from-header" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | async-graphql = { path = "../../.." } 8 | async-graphql-axum = { path = "../../../integrations/axum" } 9 | tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] } 10 | token = { path = "../../models/token" } 11 | axum = { version = "0.8.1", features = ["ws"] } 12 | -------------------------------------------------------------------------------- /axum/token-from-header/src/main.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::{ 2 | EmptyMutation, Schema, 3 | http::{ALL_WEBSOCKET_PROTOCOLS, GraphQLPlaygroundConfig, playground_source}, 4 | }; 5 | use async_graphql_axum::{GraphQLProtocol, GraphQLRequest, GraphQLResponse, GraphQLWebSocket}; 6 | use axum::{ 7 | Router, 8 | extract::{State, ws::WebSocketUpgrade}, 9 | http::header::HeaderMap, 10 | response::{Html, IntoResponse, Response}, 11 | routing::get, 12 | }; 13 | use token::{QueryRoot, SubscriptionRoot, Token, TokenSchema, on_connection_init}; 14 | use tokio::net::TcpListener; 15 | 16 | async fn graphql_playground() -> impl IntoResponse { 17 | Html(playground_source( 18 | GraphQLPlaygroundConfig::new("/").subscription_endpoint("/ws"), 19 | )) 20 | } 21 | 22 | fn get_token_from_headers(headers: &HeaderMap) -> Option { 23 | headers 24 | .get("Token") 25 | .and_then(|value| value.to_str().map(|s| Token(s.to_string())).ok()) 26 | } 27 | 28 | async fn graphql_handler( 29 | State(schema): State, 30 | headers: HeaderMap, 31 | req: GraphQLRequest, 32 | ) -> GraphQLResponse { 33 | let mut req = req.into_inner(); 34 | if let Some(token) = get_token_from_headers(&headers) { 35 | req = req.data(token); 36 | } 37 | schema.execute(req).await.into() 38 | } 39 | 40 | async fn graphql_ws_handler( 41 | State(schema): State, 42 | protocol: GraphQLProtocol, 43 | websocket: WebSocketUpgrade, 44 | ) -> Response { 45 | websocket 46 | .protocols(ALL_WEBSOCKET_PROTOCOLS) 47 | .on_upgrade(move |stream| { 48 | GraphQLWebSocket::new(stream, schema.clone(), protocol) 49 | .on_connection_init(on_connection_init) 50 | .serve() 51 | }) 52 | } 53 | 54 | #[tokio::main] 55 | async fn main() { 56 | let schema = Schema::new(QueryRoot, EmptyMutation, SubscriptionRoot); 57 | 58 | let app = Router::new() 59 | .route("/", get(graphql_playground).post(graphql_handler)) 60 | .route("/ws", get(graphql_ws_handler)) 61 | .with_state(schema); 62 | 63 | println!("Playground: http://localhost:8000"); 64 | 65 | axum::serve(TcpListener::bind("127.0.0.1:8000").await.unwrap(), app) 66 | .await 67 | .unwrap(); 68 | } 69 | -------------------------------------------------------------------------------- /axum/upload/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "axum-upload" 3 | version = "0.1.1" 4 | authors = ["sunli "] 5 | edition = "2024" 6 | 7 | [dependencies] 8 | async-graphql = { path = "../../.." } 9 | async-graphql-axum = { path = "../../../integrations/axum" } 10 | axum = "0.8.1" 11 | files = { path = "../../models/files" } 12 | tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] } 13 | tower-http = { version = "0.5.2", features = ["cors"] } 14 | -------------------------------------------------------------------------------- /axum/upload/src/main.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::{EmptySubscription, Schema, http::GraphiQLSource}; 2 | use async_graphql_axum::GraphQL; 3 | use axum::{ 4 | Router, 5 | http::Method, 6 | response::{Html, IntoResponse}, 7 | routing::get, 8 | }; 9 | use files::{MutationRoot, QueryRoot, Storage}; 10 | use tokio::net::TcpListener; 11 | use tower_http::cors::{AllowOrigin, CorsLayer}; 12 | 13 | async fn graphiql() -> impl IntoResponse { 14 | Html(GraphiQLSource::build().endpoint("/").finish()) 15 | } 16 | 17 | #[tokio::main] 18 | async fn main() { 19 | let schema = Schema::build(QueryRoot, MutationRoot, EmptySubscription) 20 | .data(Storage::default()) 21 | .finish(); 22 | 23 | println!("GraphiQL IDE: http://localhost:8000"); 24 | 25 | let app = Router::new() 26 | .route("/", get(graphiql).post_service(GraphQL::new(schema))) 27 | .layer( 28 | CorsLayer::new() 29 | .allow_origin(AllowOrigin::predicate(|_, _| true)) 30 | .allow_methods([Method::GET, Method::POST]), 31 | ); 32 | 33 | axum::serve(TcpListener::bind("127.0.0.1:8000").await.unwrap(), app) 34 | .await 35 | .unwrap(); 36 | } 37 | -------------------------------------------------------------------------------- /federation/dynamic-schema/README.md: -------------------------------------------------------------------------------- 1 | # Federation Example 2 | 3 | An example of using [Apollo Federation](https://www.apollographql.com/docs/federation/) to compose GraphQL services into a single data graph. 4 | 5 | ## The schema 6 | 7 | You can view the full schema in [Apollo Studio](https://studio.apollographql.com/public/async-graphql-Examples/home?variant=current) without needing to run the example (you will need to run the example in order to query it). 8 | 9 | ## How to run 10 | 11 | 1. Install [Rover](https://www.apollographql.com/docs/rover/) 12 | 2. Run `/start.sh` which will: 13 | 1. Start each subgraph with `cargo run --bin {subgraph_name}` 14 | 2. Add each subgraph to `rover dev` with `rover dev --url http://localhost:{port} --name {subgraph_name}` 15 | 3. Visit `http://localhost:3000` in a browser. 16 | 4. You can now run queries like the one in `query.graphql` against the router. 17 | -------------------------------------------------------------------------------- /federation/dynamic-schema/federation-accounts/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dynamic-federation-accounts" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | async-graphql = { path = "../../../..", features = ["dynamic-schema"] } 8 | async-graphql-poem = { path = "../../../../integrations/poem" } 9 | tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] } 10 | poem = { version = "3.0.0" } 11 | -------------------------------------------------------------------------------- /federation/dynamic-schema/federation-accounts/src/main.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::dynamic::{ 2 | Field, FieldFuture, FieldValue, Object, Schema, SchemaError, TypeRef, 3 | }; 4 | use async_graphql_poem::GraphQL; 5 | use poem::{Route, Server, listener::TcpListener}; 6 | 7 | struct Picture { 8 | url: String, 9 | width: u32, 10 | height: u32, 11 | } 12 | 13 | struct User { 14 | id: String, 15 | username: String, 16 | profile_picture: Option, 17 | review_count: u32, 18 | joined_timestamp: u64, 19 | } 20 | 21 | impl User { 22 | fn me() -> User { 23 | User { 24 | id: "1234".into(), 25 | username: "Me".to_string(), 26 | profile_picture: Some(Picture { 27 | url: "http://localhost:8080/me.jpg".to_string(), 28 | width: 256, 29 | height: 256, 30 | }), 31 | review_count: 0, 32 | joined_timestamp: 1, 33 | } 34 | } 35 | } 36 | 37 | fn schema() -> Result { 38 | let picture = Object::new("Picture") 39 | .field(Field::new( 40 | "url", 41 | TypeRef::named_nn(TypeRef::STRING), 42 | |ctx| { 43 | FieldFuture::new(async move { 44 | let picture = ctx.parent_value.try_downcast_ref::()?; 45 | Ok(Some(FieldValue::value(&picture.url))) 46 | }) 47 | }, 48 | )) 49 | .field(Field::new( 50 | "width", 51 | TypeRef::named_nn(TypeRef::INT), 52 | |ctx| { 53 | FieldFuture::new(async move { 54 | let picture = ctx.parent_value.try_downcast_ref::()?; 55 | Ok(Some(FieldValue::value(picture.width))) 56 | }) 57 | }, 58 | )) 59 | .field(Field::new( 60 | "height", 61 | TypeRef::named_nn(TypeRef::INT), 62 | |ctx| { 63 | FieldFuture::new(async move { 64 | let picture = ctx.parent_value.try_downcast_ref::()?; 65 | Ok(Some(FieldValue::value(picture.height))) 66 | }) 67 | }, 68 | )); 69 | 70 | let user = Object::new("User") 71 | .field(Field::new("id", TypeRef::named_nn(TypeRef::ID), |ctx| { 72 | FieldFuture::new(async move { 73 | let user = ctx.parent_value.try_downcast_ref::()?; 74 | Ok(Some(FieldValue::value(&user.id))) 75 | }) 76 | })) 77 | .field(Field::new( 78 | "username", 79 | TypeRef::named_nn(TypeRef::STRING), 80 | |ctx| { 81 | FieldFuture::new(async move { 82 | let user = ctx.parent_value.try_downcast_ref::()?; 83 | Ok(Some(FieldValue::value(&user.username))) 84 | }) 85 | }, 86 | )) 87 | .field(Field::new( 88 | "profilePicture", 89 | TypeRef::named_nn(TypeRef::STRING), 90 | |ctx| { 91 | FieldFuture::new(async move { 92 | let user = ctx.parent_value.try_downcast_ref::()?; 93 | Ok(user 94 | .profile_picture 95 | .as_ref() 96 | .map(|pic| FieldValue::borrowed_any(pic))) 97 | }) 98 | }, 99 | )) 100 | .field(Field::new( 101 | "reviewCount", 102 | TypeRef::named_nn(TypeRef::INT), 103 | |ctx| { 104 | FieldFuture::new(async move { 105 | let user = ctx.parent_value.try_downcast_ref::()?; 106 | Ok(Some(FieldValue::value(user.review_count))) 107 | }) 108 | }, 109 | )) 110 | .field(Field::new( 111 | "joinedTimestamp", 112 | TypeRef::named_nn(TypeRef::STRING), 113 | |ctx| { 114 | FieldFuture::new(async move { 115 | let user = ctx.parent_value.try_downcast_ref::()?; 116 | Ok(Some(FieldValue::value(user.joined_timestamp.to_string()))) 117 | }) 118 | }, 119 | )) 120 | .key("id"); 121 | 122 | let query = Object::new("Query").field(Field::new( 123 | "me", 124 | TypeRef::named_nn(user.type_name()), 125 | |_| FieldFuture::new(async move { Ok(Some(FieldValue::owned_any(User::me()))) }), 126 | )); 127 | 128 | Schema::build("Query", None, None) 129 | .register(picture) 130 | .register(user) 131 | .register(query) 132 | .entity_resolver(|ctx| { 133 | FieldFuture::new(async move { 134 | let representations = ctx.args.try_get("representations")?.list()?; 135 | let mut values = Vec::new(); 136 | 137 | for item in representations.iter() { 138 | let item = item.object()?; 139 | let typename = item 140 | .try_get("__typename") 141 | .and_then(|value| value.string())?; 142 | 143 | if typename == "User" { 144 | let id = item.try_get("id")?.string()?; 145 | if id == "1234" { 146 | values.push(FieldValue::owned_any(User::me())); 147 | } else { 148 | let username = format!("User {}", id); 149 | let user = User { 150 | id: id.to_string(), 151 | username, 152 | profile_picture: None, 153 | review_count: 0, 154 | joined_timestamp: 1500, 155 | }; 156 | values.push(FieldValue::owned_any(user)); 157 | } 158 | } 159 | } 160 | 161 | Ok(Some(FieldValue::list(values))) 162 | }) 163 | }) 164 | .finish() 165 | } 166 | 167 | #[tokio::main] 168 | async fn main() -> std::io::Result<()> { 169 | Server::new(TcpListener::bind("127.0.0.1:4001")) 170 | .run(Route::new().at("/", GraphQL::new(schema().unwrap()))) 171 | .await 172 | } 173 | -------------------------------------------------------------------------------- /federation/dynamic-schema/federation-products/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dynamic-federation-products" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | async-graphql = { path = "../../../..", features = ["dynamic-schema"] } 8 | async-graphql-poem = { path = "../../../../integrations/poem" } 9 | tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] } 10 | poem = { version = "3.0.0" } 11 | -------------------------------------------------------------------------------- /federation/dynamic-schema/federation-products/src/main.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::dynamic::{ 2 | Field, FieldFuture, FieldValue, Object, Schema, SchemaError, TypeRef, 3 | }; 4 | use async_graphql_poem::GraphQL; 5 | use poem::{Route, Server, listener::TcpListener}; 6 | 7 | struct Product { 8 | upc: String, 9 | name: String, 10 | price: i32, 11 | } 12 | 13 | fn schema() -> Result { 14 | let hats = vec![ 15 | Product { 16 | upc: "top-1".to_string(), 17 | name: "Trilby".to_string(), 18 | price: 11, 19 | }, 20 | Product { 21 | upc: "top-2".to_string(), 22 | name: "Fedora".to_string(), 23 | price: 22, 24 | }, 25 | Product { 26 | upc: "top-3".to_string(), 27 | name: "Boater".to_string(), 28 | price: 33, 29 | }, 30 | ]; 31 | 32 | let product = Object::new("Product") 33 | .field(Field::new( 34 | "upc", 35 | TypeRef::named_nn(TypeRef::STRING), 36 | |ctx| { 37 | FieldFuture::new(async move { 38 | let product = ctx.parent_value.try_downcast_ref::()?; 39 | Ok(Some(FieldValue::value(&product.upc))) 40 | }) 41 | }, 42 | )) 43 | .field(Field::new( 44 | "name", 45 | TypeRef::named_nn(TypeRef::STRING), 46 | |ctx| { 47 | FieldFuture::new(async move { 48 | let product = ctx.parent_value.try_downcast_ref::()?; 49 | Ok(Some(FieldValue::value(&product.name))) 50 | }) 51 | }, 52 | )) 53 | .field( 54 | Field::new("price", TypeRef::named_nn(TypeRef::INT), |ctx| { 55 | FieldFuture::new(async move { 56 | let product = ctx.parent_value.try_downcast_ref::()?; 57 | Ok(Some(FieldValue::value(product.price))) 58 | }) 59 | }) 60 | .shareable(), 61 | ) 62 | .key("upc"); 63 | 64 | let query = Object::new("Query").field(Field::new( 65 | "topProducts", 66 | TypeRef::named_nn_list_nn(product.type_name()), 67 | |ctx| { 68 | FieldFuture::new(async move { 69 | let mut values = Vec::new(); 70 | let products = ctx.data_unchecked::>(); 71 | for product in products { 72 | values.push(FieldValue::borrowed_any(product)); 73 | } 74 | Ok(Some(values)) 75 | }) 76 | }, 77 | )); 78 | 79 | Schema::build("Query", None, None) 80 | .data(hats) 81 | .register(product) 82 | .register(query) 83 | .entity_resolver(|ctx| { 84 | FieldFuture::new(async move { 85 | let products = ctx.data_unchecked::>(); 86 | let representations = ctx.args.try_get("representations")?.list()?; 87 | let mut values = Vec::new(); 88 | 89 | for item in representations.iter() { 90 | let item = item.object()?; 91 | let typename = item 92 | .try_get("__typename") 93 | .and_then(|value| value.string())?; 94 | 95 | if typename == "Product" { 96 | let upc = item.try_get("upc")?.string()?; 97 | if let Some(product) = products.iter().find(|product| product.upc == upc) { 98 | values.push(FieldValue::borrowed_any(product)); 99 | } 100 | } 101 | } 102 | 103 | Ok(Some(FieldValue::list(values))) 104 | }) 105 | }) 106 | .finish() 107 | } 108 | 109 | #[tokio::main] 110 | async fn main() -> std::io::Result<()> { 111 | Server::new(TcpListener::bind("127.0.0.1:4002")) 112 | .run(Route::new().at("/", GraphQL::new(schema().unwrap()))) 113 | .await 114 | } 115 | -------------------------------------------------------------------------------- /federation/dynamic-schema/federation-reviews/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dynamic-federation-reviews" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | async-graphql = { path = "../../../..", features = ["dynamic-schema"] } 8 | async-graphql-poem = { path = "../../../../integrations/poem" } 9 | tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] } 10 | poem = { version = "3.0.0" } 11 | -------------------------------------------------------------------------------- /federation/dynamic-schema/federation-reviews/src/main.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::dynamic::{ 2 | Enum, Field, FieldFuture, FieldValue, Object, Schema, SchemaError, TypeRef, 3 | }; 4 | use async_graphql_poem::GraphQL; 5 | use poem::{Route, Server, listener::TcpListener}; 6 | 7 | struct Picture { 8 | url: String, 9 | width: u32, 10 | height: u32, 11 | alt_text: String, 12 | } 13 | 14 | struct Review { 15 | id: String, 16 | body: String, 17 | pictures: Vec, 18 | } 19 | 20 | struct Product { 21 | upc: String, 22 | price: u32, 23 | } 24 | 25 | impl Review { 26 | fn get_product(&self) -> Product { 27 | match self.id.as_str() { 28 | "review-1" => Product { 29 | upc: "top-1".to_string(), 30 | price: 10, 31 | }, 32 | "review-2" => Product { 33 | upc: "top-2".to_string(), 34 | price: 20, 35 | }, 36 | "review-3" => Product { 37 | upc: "top-3".to_string(), 38 | price: 30, 39 | }, 40 | _ => panic!("Unknown review id"), 41 | } 42 | } 43 | 44 | fn get_author(&self) -> User { 45 | let user_id = match self.id.as_str() { 46 | "review-1" => "1234", 47 | "review-2" => "1234", 48 | "review-3" => "7777", 49 | _ => panic!("Unknown review id"), 50 | } 51 | .to_string(); 52 | user_by_id(user_id, None) 53 | } 54 | } 55 | 56 | struct User { 57 | id: String, 58 | review_count: u32, 59 | joined_timestamp: u64, 60 | } 61 | 62 | fn user_by_id(id: String, joined_timestamp: Option) -> User { 63 | let review_count = match id.as_str() { 64 | "1234" => 2, 65 | "7777" => 1, 66 | _ => 0, 67 | }; 68 | // This will be set if the user requested the fields that require it. 69 | let joined_timestamp = joined_timestamp.unwrap_or(9001); 70 | User { 71 | id, 72 | review_count, 73 | joined_timestamp, 74 | } 75 | } 76 | 77 | fn schema() -> Result { 78 | let picture = Object::new("Picture") 79 | .shareable() 80 | .field(Field::new( 81 | "url", 82 | TypeRef::named_nn(TypeRef::STRING), 83 | |ctx| { 84 | FieldFuture::new(async move { 85 | let picture = ctx.parent_value.try_downcast_ref::()?; 86 | Ok(Some(FieldValue::value(&picture.url))) 87 | }) 88 | }, 89 | )) 90 | .field(Field::new( 91 | "width", 92 | TypeRef::named_nn(TypeRef::INT), 93 | |ctx| { 94 | FieldFuture::new(async move { 95 | let picture = ctx.parent_value.try_downcast_ref::()?; 96 | Ok(Some(FieldValue::value(picture.width))) 97 | }) 98 | }, 99 | )) 100 | .field(Field::new( 101 | "height", 102 | TypeRef::named_nn(TypeRef::INT), 103 | |ctx| { 104 | FieldFuture::new(async move { 105 | let picture = ctx.parent_value.try_downcast_ref::()?; 106 | Ok(Some(FieldValue::value(picture.height))) 107 | }) 108 | }, 109 | )) 110 | .field( 111 | Field::new("altText", TypeRef::named_nn(TypeRef::INT), |ctx| { 112 | FieldFuture::new(async move { 113 | let picture = ctx.parent_value.try_downcast_ref::()?; 114 | Ok(Some(FieldValue::value(&picture.alt_text))) 115 | }) 116 | }) 117 | .inaccessible(), 118 | ); 119 | 120 | let review = Object::new("Review") 121 | .field(Field::new("id", TypeRef::named_nn(TypeRef::ID), |ctx| { 122 | FieldFuture::new(async move { 123 | let review = ctx.parent_value.try_downcast_ref::()?; 124 | Ok(Some(FieldValue::value(&review.id))) 125 | }) 126 | })) 127 | .field(Field::new( 128 | "body", 129 | TypeRef::named_nn(TypeRef::STRING), 130 | |ctx| { 131 | FieldFuture::new(async move { 132 | let review = ctx.parent_value.try_downcast_ref::()?; 133 | Ok(Some(FieldValue::value(&review.body))) 134 | }) 135 | }, 136 | )) 137 | .field(Field::new( 138 | "pictures", 139 | TypeRef::named_nn_list_nn(picture.type_name()), 140 | |ctx| { 141 | FieldFuture::new(async move { 142 | let review = ctx.parent_value.try_downcast_ref::()?; 143 | Ok(Some(FieldValue::list( 144 | review 145 | .pictures 146 | .iter() 147 | .map(|review| FieldValue::borrowed_any(review)), 148 | ))) 149 | }) 150 | }, 151 | )) 152 | .field( 153 | Field::new("product", TypeRef::named_nn(TypeRef::STRING), |ctx| { 154 | FieldFuture::new(async move { 155 | let review = ctx.parent_value.try_downcast_ref::()?; 156 | Ok(Some(FieldValue::owned_any(review.get_product()))) 157 | }) 158 | }) 159 | .provides("price"), 160 | ) 161 | .field(Field::new("author", TypeRef::named_nn("User"), |ctx| { 162 | FieldFuture::new(async move { 163 | let review = ctx.parent_value.try_downcast_ref::()?; 164 | let author = review.get_author(); 165 | Ok(Some(FieldValue::owned_any(author))) 166 | }) 167 | })); 168 | 169 | let trust_worthiness = 170 | Enum::new("Trustworthiness").items(["ReallyTrusted", "KindaTrusted", "NotTrusted"]); 171 | 172 | let user = Object::new("User") 173 | .field(Field::new("id", TypeRef::named_nn(TypeRef::ID), |ctx| { 174 | FieldFuture::new(async move { 175 | let user = ctx.parent_value.try_downcast_ref::()?; 176 | Ok(Some(FieldValue::value(&user.id))) 177 | }) 178 | })) 179 | .field( 180 | Field::new("reviewCount", TypeRef::named_nn(TypeRef::INT), |ctx| { 181 | FieldFuture::new(async move { 182 | let user = ctx.parent_value.try_downcast_ref::()?; 183 | Ok(Some(FieldValue::value(user.review_count))) 184 | }) 185 | }) 186 | .override_from("accounts"), 187 | ) 188 | .field( 189 | Field::new("joinedTimestamp", TypeRef::named_nn(TypeRef::INT), |ctx| { 190 | FieldFuture::new(async move { 191 | let user = ctx.parent_value.try_downcast_ref::()?; 192 | Ok(Some(FieldValue::value(user.joined_timestamp))) 193 | }) 194 | }) 195 | .external(), 196 | ) 197 | .field(Field::new( 198 | "reviews", 199 | TypeRef::named_nn_list_nn(review.type_name()), 200 | |ctx| { 201 | FieldFuture::new(async move { 202 | let reviews = ctx.data::>()?; 203 | Ok(Some(FieldValue::list( 204 | reviews 205 | .iter() 206 | .map(|review| FieldValue::borrowed_any(review)), 207 | ))) 208 | }) 209 | }, 210 | )) 211 | .field( 212 | Field::new( 213 | "trustworthiness", 214 | TypeRef::named_nn_list_nn(review.type_name()), 215 | |ctx| { 216 | FieldFuture::new(async move { 217 | let user = ctx.parent_value.try_downcast_ref::()?; 218 | Ok(Some( 219 | if user.joined_timestamp < 1_000 && user.review_count > 1 { 220 | FieldValue::value("ReallyTrusted") 221 | } else if user.joined_timestamp < 2_000 { 222 | FieldValue::value("KindaTrusted") 223 | } else { 224 | FieldValue::value("NotTrusted") 225 | }, 226 | )) 227 | }) 228 | }, 229 | ) 230 | .requires("joinedTimestamp"), 231 | ) 232 | .key("id"); 233 | 234 | let product = Object::new("Product") 235 | .field(Field::new("upc", TypeRef::named_nn(TypeRef::ID), |ctx| { 236 | FieldFuture::new(async move { 237 | let product = ctx.parent_value.try_downcast_ref::()?; 238 | Ok(Some(FieldValue::value(&product.upc))) 239 | }) 240 | })) 241 | .field( 242 | Field::new("price", TypeRef::named_nn(TypeRef::INT), |ctx| { 243 | FieldFuture::new(async move { 244 | let product = ctx.parent_value.try_downcast_ref::()?; 245 | Ok(Some(FieldValue::value(product.price))) 246 | }) 247 | }) 248 | .external(), 249 | ) 250 | .field(Field::new( 251 | "reviews", 252 | TypeRef::named_nn_list_nn(review.type_name()), 253 | |ctx| { 254 | FieldFuture::new(async move { 255 | let user = ctx.parent_value.try_downcast_ref::()?; 256 | let reviews = ctx.data::>()?; 257 | Ok(Some(FieldValue::list( 258 | reviews 259 | .iter() 260 | .filter(|review| review.get_author().id == user.id) 261 | .map(|review| FieldValue::borrowed_any(review)), 262 | ))) 263 | }) 264 | }, 265 | )) 266 | .key("upc"); 267 | 268 | let reviews = vec![ 269 | Review { 270 | id: "review-1".into(), 271 | body: "A highly effective form of birth control.".into(), 272 | pictures: vec![ 273 | Picture { 274 | url: "http://localhost:8080/ugly_hat.jpg".to_string(), 275 | width: 100, 276 | height: 100, 277 | alt_text: "A Trilby".to_string(), 278 | }, 279 | Picture { 280 | url: "http://localhost:8080/troll_face.jpg".to_string(), 281 | width: 42, 282 | height: 42, 283 | alt_text: "The troll face meme".to_string(), 284 | }, 285 | ], 286 | }, 287 | Review { 288 | id: "review-2".into(), 289 | body: "Fedoras are one of the most fashionable hats around and can look great with a variety of outfits.".into(), 290 | pictures: vec![], 291 | }, 292 | Review { 293 | id: "review-3".into(), 294 | body: "This is the last straw. Hat you will wear. 11/10".into(), 295 | pictures: vec![], 296 | }, 297 | ]; 298 | 299 | let query = Object::new("Query"); 300 | 301 | Schema::build("Query", None, None) 302 | .data(reviews) 303 | .register(picture) 304 | .register(review) 305 | .register(trust_worthiness) 306 | .register(user) 307 | .register(product) 308 | .register(query) 309 | .entity_resolver(|ctx| { 310 | FieldFuture::new(async move { 311 | let representations = ctx.args.try_get("representations")?.list()?; 312 | let mut values = Vec::new(); 313 | 314 | for item in representations.iter() { 315 | let item = item.object()?; 316 | let typename = item 317 | .try_get("__typename") 318 | .and_then(|value| value.string())?; 319 | 320 | if typename == "User" { 321 | let id = item.try_get("id")?.string()?; 322 | let joined_timestamp = item 323 | .get("joinedTimestamp") 324 | .and_then(|value| value.u64().ok()); 325 | values.push(FieldValue::owned_any(user_by_id( 326 | id.to_string(), 327 | joined_timestamp, 328 | ))); 329 | } else if typename == "Product" { 330 | let upc = item.try_get("upc")?.string()?; 331 | values.push(FieldValue::owned_any(Product { 332 | upc: upc.to_string(), 333 | price: 0, 334 | })); 335 | } 336 | } 337 | 338 | Ok(Some(FieldValue::list(values))) 339 | }) 340 | }) 341 | .finish() 342 | } 343 | 344 | #[tokio::main] 345 | async fn main() -> std::io::Result<()> { 346 | Server::new(TcpListener::bind("127.0.0.1:4003")) 347 | .run(Route::new().at("/", GraphQL::new(schema().unwrap()))) 348 | .await 349 | } 350 | -------------------------------------------------------------------------------- /federation/dynamic-schema/query.graphql: -------------------------------------------------------------------------------- 1 | query ExampleQuery { 2 | me { 3 | id 4 | username @lowercase 5 | reviews { 6 | body 7 | ... @defer { 8 | product { 9 | reviews { 10 | author { 11 | username 12 | } 13 | body 14 | } 15 | } 16 | } 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /federation/dynamic-schema/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eumo pipefail 4 | 5 | function cleanup { 6 | for pid in "${PRODUCTS_ROVER_PID:-}" "${REVIEWS_ROVER_PID:-}" "${ACCOUNTS_PID:-}" "${PRODUCTS_PID:-}" "${REVIEWS_PID:-}"; do 7 | # try kill all registered pids 8 | [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null && kill "$pid" || echo "Could not kill $pid" 9 | done 10 | } 11 | trap cleanup EXIT 12 | 13 | cargo build --bin dynamic-federation-accounts 14 | cargo build --bin dynamic-federation-products 15 | cargo build --bin dynamic-federation-reviews 16 | 17 | cargo run --bin dynamic-federation-accounts & 18 | ACCOUNTS_PID=$! 19 | 20 | cargo run --bin dynamic-federation-products & 21 | PRODUCTS_PID=$! 22 | 23 | cargo run --bin dynamic-federation-reviews & 24 | REVIEWS_PID=$! 25 | 26 | sleep 3 27 | 28 | rover dev --url http://localhost:4001 --name accounts & 29 | sleep 1 30 | rover dev --url http://localhost:4002 --name products & 31 | PRODUCTS_ROVER_PID=$! 32 | sleep 1 33 | rover dev --url http://localhost:4003 --name reviews & 34 | REVIEWS_ROVER_PID=$! 35 | fg %4 36 | -------------------------------------------------------------------------------- /federation/static-schema/README.md: -------------------------------------------------------------------------------- 1 | # Federation Example 2 | 3 | An example of using [Apollo Federation](https://www.apollographql.com/docs/federation/) to compose GraphQL services into a single data graph. 4 | 5 | ## The schema 6 | 7 | You can view the full schema in [Apollo Studio](https://studio.apollographql.com/public/async-graphql-Examples/home?variant=current) without needing to run the example (you will need to run the example in order to query it). 8 | 9 | ## How to run 10 | 11 | 1. Install [Rover](https://www.apollographql.com/docs/rover/) 12 | 2. Run `/start.sh` which will: 13 | 1. Start each subgraph with `cargo run --bin {subgraph_name}` 14 | 2. Add each subgraph to `rover dev` with `rover dev --url http://localhost:{port} --name {subgraph_name}` 15 | 3. Visit `http://localhost:3000` in a browser. 16 | 4. You can now run queries like the one in `query.graphql` against the router. 17 | -------------------------------------------------------------------------------- /federation/static-schema/directives/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "directives" 3 | version = "0.1.0" 4 | authors = ["sunli "] 5 | edition = "2024" 6 | 7 | [dependencies] 8 | async-graphql = { path = "../../../.." } 9 | async-trait = "0.1.79" 10 | -------------------------------------------------------------------------------- /federation/static-schema/directives/src/lib.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::{Context, CustomDirective, Directive, ResolveFut, ServerResult, Value}; 2 | 3 | struct LowercaseDirective; 4 | 5 | #[async_trait::async_trait] 6 | impl CustomDirective for LowercaseDirective { 7 | async fn resolve_field( 8 | &self, 9 | _ctx: &Context<'_>, 10 | resolve: ResolveFut<'_>, 11 | ) -> ServerResult> { 12 | resolve.await.map(|value| { 13 | value.map(|value| match value { 14 | Value::String(str) => Value::String(str.to_ascii_lowercase()), 15 | _ => value, 16 | }) 17 | }) 18 | } 19 | } 20 | 21 | #[Directive(location = "Field")] 22 | pub fn lowercase() -> impl CustomDirective { 23 | LowercaseDirective 24 | } 25 | -------------------------------------------------------------------------------- /federation/static-schema/federation-accounts/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "static-federation-accounts" 3 | version = "0.2.0" 4 | authors = ["sunli "] 5 | edition = "2024" 6 | 7 | [dependencies] 8 | async-graphql = { path = "../../../.." } 9 | async-graphql-poem = { path = "../../../../integrations/poem" } 10 | directives = { path = "../directives" } 11 | tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] } 12 | poem = { version = "3.0.0" } 13 | -------------------------------------------------------------------------------- /federation/static-schema/federation-accounts/src/main.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::{EmptyMutation, EmptySubscription, ID, Object, Schema, SimpleObject}; 2 | use async_graphql_poem::GraphQL; 3 | use poem::{Route, Server, listener::TcpListener}; 4 | 5 | #[derive(SimpleObject)] 6 | struct User { 7 | id: ID, 8 | username: String, 9 | profile_picture: Option, 10 | /// This used to be part of this subgraph, but is now being overridden from 11 | /// `reviews` 12 | review_count: u32, 13 | joined_timestamp: u64, 14 | } 15 | 16 | impl User { 17 | fn me() -> User { 18 | User { 19 | id: "1234".into(), 20 | username: "Me".to_string(), 21 | profile_picture: Some(Picture { 22 | url: "http://localhost:8080/me.jpg".to_string(), 23 | width: 256, 24 | height: 256, 25 | }), 26 | review_count: 0, 27 | joined_timestamp: 1, 28 | } 29 | } 30 | } 31 | 32 | #[derive(SimpleObject)] 33 | #[graphql(shareable)] 34 | struct Picture { 35 | url: String, 36 | width: u32, 37 | height: u32, 38 | } 39 | 40 | struct Query; 41 | 42 | #[Object] 43 | impl Query { 44 | async fn me(&self) -> User { 45 | User::me() 46 | } 47 | 48 | #[graphql(entity)] 49 | async fn find_user_by_id(&self, id: ID) -> User { 50 | if id == "1234" { 51 | User::me() 52 | } else { 53 | let username = format!("User {}", id.as_str()); 54 | User { 55 | id, 56 | username, 57 | profile_picture: None, 58 | review_count: 0, 59 | joined_timestamp: 1500, 60 | } 61 | } 62 | } 63 | } 64 | 65 | #[tokio::main] 66 | async fn main() -> std::io::Result<()> { 67 | let schema = Schema::build(Query, EmptyMutation, EmptySubscription) 68 | .directive(directives::lowercase) 69 | .finish(); 70 | Server::new(TcpListener::bind("127.0.0.1:4001")) 71 | .run(Route::new().at("/", GraphQL::new(schema))) 72 | .await 73 | } 74 | -------------------------------------------------------------------------------- /federation/static-schema/federation-products/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "static-federation-products" 3 | version = "0.2.0" 4 | authors = ["sunli "] 5 | edition = "2024" 6 | 7 | [dependencies] 8 | async-graphql = { path = "../../../.." } 9 | async-graphql-poem = { path = "../../../../integrations/poem" } 10 | directives = { path = "../directives" } 11 | tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] } 12 | poem = { version = "3.0.0" } 13 | -------------------------------------------------------------------------------- /federation/static-schema/federation-products/src/main.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::{Context, EmptyMutation, EmptySubscription, Object, Schema, SimpleObject}; 2 | use async_graphql_poem::GraphQL; 3 | use poem::{Route, Server, listener::TcpListener}; 4 | 5 | #[derive(SimpleObject)] 6 | struct Product { 7 | upc: String, 8 | name: String, 9 | #[graphql(shareable)] 10 | price: i32, 11 | } 12 | 13 | struct Query; 14 | 15 | #[Object] 16 | impl Query { 17 | async fn top_products<'a>(&self, ctx: &'a Context<'_>) -> &'a Vec { 18 | ctx.data_unchecked::>() 19 | } 20 | 21 | #[graphql(entity)] 22 | async fn find_product_by_upc<'a>( 23 | &self, 24 | ctx: &'a Context<'_>, 25 | upc: String, 26 | ) -> Option<&'a Product> { 27 | let hats = ctx.data_unchecked::>(); 28 | hats.iter().find(|product| product.upc == upc) 29 | } 30 | } 31 | 32 | #[tokio::main] 33 | async fn main() -> std::io::Result<()> { 34 | let hats = vec![ 35 | Product { 36 | upc: "top-1".to_string(), 37 | name: "Trilby".to_string(), 38 | price: 11, 39 | }, 40 | Product { 41 | upc: "top-2".to_string(), 42 | name: "Fedora".to_string(), 43 | price: 22, 44 | }, 45 | Product { 46 | upc: "top-3".to_string(), 47 | name: "Boater".to_string(), 48 | price: 33, 49 | }, 50 | ]; 51 | 52 | let schema = Schema::build(Query, EmptyMutation, EmptySubscription) 53 | .data(hats) 54 | .directive(directives::lowercase) 55 | .finish(); 56 | 57 | Server::new(TcpListener::bind("127.0.0.1:4002")) 58 | .run(Route::new().at("/", GraphQL::new(schema))) 59 | .await 60 | } 61 | -------------------------------------------------------------------------------- /federation/static-schema/federation-reviews/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "static-federation-reviews" 3 | version = "0.2.0" 4 | authors = ["sunli "] 5 | edition = "2024" 6 | 7 | [dependencies] 8 | async-graphql = { path = "../../../.." } 9 | async-graphql-poem = { path = "../../../../integrations/poem" } 10 | directives = { path = "../directives" } 11 | tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] } 12 | poem = { version = "3.0.0" } 13 | -------------------------------------------------------------------------------- /federation/static-schema/federation-reviews/src/main.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::{ 2 | ComplexObject, Context, EmptyMutation, EmptySubscription, Enum, ID, Object, Schema, 3 | SimpleObject, 4 | }; 5 | use async_graphql_poem::GraphQL; 6 | use poem::{Route, Server, listener::TcpListener}; 7 | 8 | #[derive(SimpleObject)] 9 | #[graphql(complex)] 10 | struct User { 11 | id: ID, 12 | #[graphql(override_from = "accounts")] 13 | review_count: u32, 14 | #[graphql(external)] 15 | joined_timestamp: u64, 16 | } 17 | 18 | #[derive(Enum, Eq, PartialEq, Copy, Clone)] 19 | #[allow(clippy::enum_variant_names)] 20 | enum Trustworthiness { 21 | ReallyTrusted, 22 | KindaTrusted, 23 | NotTrusted, 24 | } 25 | 26 | #[ComplexObject] 27 | impl User { 28 | async fn reviews<'a>(&self, ctx: &'a Context<'_>) -> Vec<&'a Review> { 29 | let reviews = ctx.data_unchecked::>(); 30 | reviews 31 | .iter() 32 | .filter(|review| review.get_author().id == self.id) 33 | .collect() 34 | } 35 | 36 | #[graphql(requires = "joinedTimestamp")] 37 | async fn trustworthiness(&self) -> Trustworthiness { 38 | if self.joined_timestamp < 1_000 && self.review_count > 1 { 39 | Trustworthiness::ReallyTrusted 40 | } else if self.joined_timestamp < 2_000 { 41 | Trustworthiness::KindaTrusted 42 | } else { 43 | Trustworthiness::NotTrusted 44 | } 45 | } 46 | } 47 | 48 | #[derive(SimpleObject)] 49 | #[graphql(complex)] 50 | struct Product { 51 | upc: String, 52 | #[graphql(external)] 53 | price: u32, 54 | } 55 | 56 | #[ComplexObject] 57 | impl Product { 58 | async fn reviews<'a>(&self, ctx: &'a Context<'_>) -> Vec<&'a Review> { 59 | let reviews = ctx.data_unchecked::>(); 60 | reviews 61 | .iter() 62 | .filter(|review| review.get_product().upc == self.upc) 63 | .collect() 64 | } 65 | } 66 | 67 | #[derive(SimpleObject)] 68 | #[graphql(complex)] 69 | struct Review { 70 | id: ID, 71 | body: String, 72 | pictures: Vec, 73 | } 74 | 75 | #[ComplexObject] 76 | impl Review { 77 | #[graphql(provides = "price")] 78 | async fn product<'a>(&self) -> Product { 79 | self.get_product() 80 | } 81 | 82 | async fn author(&self) -> User { 83 | self.get_author() 84 | } 85 | } 86 | 87 | impl Review { 88 | fn get_product(&self) -> Product { 89 | match self.id.as_str() { 90 | "review-1" => Product { 91 | upc: "top-1".to_string(), 92 | price: 10, 93 | }, 94 | "review-2" => Product { 95 | upc: "top-2".to_string(), 96 | price: 20, 97 | }, 98 | "review-3" => Product { 99 | upc: "top-3".to_string(), 100 | price: 30, 101 | }, 102 | _ => panic!("Unknown review id"), 103 | } 104 | } 105 | 106 | fn get_author(&self) -> User { 107 | let user_id: ID = match self.id.as_str() { 108 | "review-1" => "1234", 109 | "review-2" => "1234", 110 | "review-3" => "7777", 111 | _ => panic!("Unknown review id"), 112 | } 113 | .into(); 114 | user_by_id(user_id, None) 115 | } 116 | } 117 | 118 | #[derive(SimpleObject)] 119 | #[graphql(shareable)] 120 | struct Picture { 121 | url: String, 122 | width: u32, 123 | height: u32, 124 | #[graphql(inaccessible)] // Field not added to Accounts yet 125 | alt_text: String, 126 | } 127 | 128 | struct Query; 129 | 130 | #[Object] 131 | impl Query { 132 | #[graphql(entity)] 133 | async fn find_user_by_id(&self, #[graphql(key)] id: ID, joined_timestamp: Option) -> User { 134 | user_by_id(id, joined_timestamp) 135 | } 136 | 137 | #[graphql(entity)] 138 | async fn find_product_by_upc(&self, upc: String) -> Product { 139 | Product { upc, price: 0 } 140 | } 141 | } 142 | 143 | fn user_by_id(id: ID, joined_timestamp: Option) -> User { 144 | let review_count = match id.as_str() { 145 | "1234" => 2, 146 | "7777" => 1, 147 | _ => 0, 148 | }; 149 | // This will be set if the user requested the fields that require it. 150 | let joined_timestamp = joined_timestamp.unwrap_or(9001); 151 | User { 152 | id, 153 | review_count, 154 | joined_timestamp, 155 | } 156 | } 157 | 158 | #[tokio::main] 159 | async fn main() -> std::io::Result<()> { 160 | let reviews = vec![ 161 | Review { 162 | id: "review-1".into(), 163 | body: "A highly effective form of birth control.".into(), 164 | pictures: vec![ 165 | Picture { 166 | url: "http://localhost:8080/ugly_hat.jpg".to_string(), 167 | width: 100, 168 | height: 100, 169 | alt_text: "A Trilby".to_string(), 170 | }, 171 | Picture { 172 | url: "http://localhost:8080/troll_face.jpg".to_string(), 173 | width: 42, 174 | height: 42, 175 | alt_text: "The troll face meme".to_string(), 176 | }, 177 | ], 178 | }, 179 | Review { 180 | id: "review-2".into(), 181 | body: "Fedoras are one of the most fashionable hats around and can look great with a variety of outfits.".into(), 182 | pictures: vec![], 183 | }, 184 | Review { 185 | id: "review-3".into(), 186 | body: "This is the last straw. Hat you will wear. 11/10".into(), 187 | pictures: vec![], 188 | }, 189 | ]; 190 | 191 | let schema = Schema::build(Query, EmptyMutation, EmptySubscription) 192 | .data(reviews) 193 | .directive(directives::lowercase) 194 | .finish(); 195 | 196 | Server::new(TcpListener::bind("127.0.0.1:4003")) 197 | .run(Route::new().at("/", GraphQL::new(schema))) 198 | .await 199 | } 200 | -------------------------------------------------------------------------------- /federation/static-schema/query.graphql: -------------------------------------------------------------------------------- 1 | query ExampleQuery { 2 | me { 3 | id 4 | username @lowercase 5 | reviews { 6 | body 7 | ... @defer { 8 | product { 9 | reviews { 10 | author { 11 | username 12 | } 13 | body 14 | } 15 | } 16 | } 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /federation/static-schema/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eumo pipefail 4 | 5 | function cleanup { 6 | for pid in "${PRODUCTS_ROVER_PID:-}" "${REVIEWS_ROVER_PID:-}" "${ACCOUNTS_PID:-}" "${PRODUCTS_PID:-}" "${REVIEWS_PID:-}"; do 7 | # try kill all registered pids 8 | [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null && kill "$pid" || echo "Could not kill $pid" 9 | done 10 | } 11 | trap cleanup EXIT 12 | 13 | cargo build --bin static-federation-accounts 14 | cargo build --bin static-federation-products 15 | cargo build --bin static-federation-reviews 16 | 17 | cargo run --bin static-federation-accounts & 18 | ACCOUNTS_PID=$! 19 | 20 | cargo run --bin static-federation-products & 21 | PRODUCTS_PID=$! 22 | 23 | cargo run --bin static-federation-reviews & 24 | REVIEWS_PID=$! 25 | 26 | sleep 3 27 | 28 | rover dev --url http://localhost:4001 --name accounts & 29 | sleep 1 30 | rover dev --url http://localhost:4002 --name products & 31 | PRODUCTS_ROVER_PID=$! 32 | sleep 1 33 | rover dev --url http://localhost:4003 --name reviews & 34 | REVIEWS_ROVER_PID=$! 35 | fg %4 -------------------------------------------------------------------------------- /loco/starwars/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | loco = "run --" 3 | -------------------------------------------------------------------------------- /loco/starwars/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | [package] 4 | name = "loco-starwars" 5 | version = "0.1.0" 6 | edition = "2024" 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [dependencies] 11 | loco-rs = { version = "0.6.0", default-features = false, features = ["cli"] } 12 | eyre = "*" 13 | tokio = { version = "1.33.0", default-features = false } 14 | async-trait = "0.1.74" 15 | 16 | axum = "0.7.1" 17 | 18 | # async-graphql dependencies 19 | async-graphql = { path = "../../.." } 20 | async-graphql-axum = { path = "../../../integrations/axum" } 21 | starwars = { path = "../../models/starwars" } 22 | 23 | [[bin]] 24 | name = "starwars-cli" 25 | path = "src/bin/main.rs" 26 | required-features = [] 27 | -------------------------------------------------------------------------------- /loco/starwars/README.md: -------------------------------------------------------------------------------- 1 | # async-graphql with Loco :train: 2 | 3 | Example async-graphql project with [Loco](https://github.com/loco-rs/loco). 4 | 5 | ## Quick Start 6 | 7 | Start your app: 8 | 9 | ``` 10 | $ cargo loco start 11 | Finished dev [unoptimized + debuginfo] target(s) in 21.63s 12 | Running `target/debug/myapp start` 13 | 14 | : 15 | : 16 | : 17 | 18 | controller/app_routes.rs:203: [Middleware] Adding log trace id 19 | 20 | ▄ ▀ 21 | ▀ ▄ 22 | ▄ ▀ ▄ ▄ ▄▀ 23 | ▄ ▀▄▄ 24 | ▄ ▀ ▀ ▀▄▀█▄ 25 | ▀█▄ 26 | ▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▀▀█ 27 | ██████ █████ ███ █████ ███ █████ ███ ▀█ 28 | ██████ █████ ███ █████ ▀▀▀ █████ ███ ▄█▄ 29 | ██████ █████ ███ █████ █████ ███ ████▄ 30 | ██████ █████ ███ █████ ▄▄▄ █████ ███ █████ 31 | ██████ █████ ███ ████ ███ █████ ███ ████▀ 32 | ▀▀▀██▄ ▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀ ██▀ 33 | ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ 34 | 35 | started on port 5150 36 | ``` 37 | -------------------------------------------------------------------------------- /loco/starwars/config/development.yaml: -------------------------------------------------------------------------------- 1 | # Loco configuration file documentation 2 | 3 | # Application logging configuration 4 | logger: 5 | # Enable or disable logging. 6 | enable: true 7 | # Log level, options: trace, debug, info, warn or error. 8 | level: debug 9 | # Define the logging format. options: compact, pretty or json 10 | format: compact 11 | # By default the logger has filtering only logs that came from your code or logs that came from `loco` framework. to see all third party libraries 12 | # Uncomment the line below to override to see all third party libraries you can enable this config and override the logger filters. 13 | # override_filter: trace 14 | 15 | # Web server configuration 16 | server: 17 | # Port on which the server will listen. the server binding is 0.0.0.0:{PORT} 18 | port: 5150 19 | # The UI hostname or IP address that mailers will point to. 20 | host: http://localhost 21 | # Out of the box middleware configuration. to disable middleware you can changed the `enable` field to `false` of comment the middleware block 22 | middlewares: 23 | # Enable Etag cache header middleware 24 | etag: 25 | enable: true 26 | # Allows to limit the payload size request. payload that bigger than this file will blocked the request. 27 | limit_payload: 28 | # Enable/Disable the middleware. 29 | enable: true 30 | # the limit size. can be b,kb,kib,mb,mib,gb,gib 31 | body_limit: 5mb 32 | # Generating a unique request ID and enhancing logging with additional information such as the start and completion of request processing, latency, status code, and other request details. 33 | logger: 34 | # Enable/Disable the middleware. 35 | enable: true 36 | # when your code is panicked, the request still returns 500 status code. 37 | catch_panic: 38 | # Enable/Disable the middleware. 39 | enable: true 40 | # Timeout for incoming requests middleware. requests that take more time from the configuration will cute and 408 status code will returned. 41 | timeout_request: 42 | # Enable/Disable the middleware. 43 | enable: false 44 | # Duration time in milliseconds. 45 | timeout: 5000 46 | -------------------------------------------------------------------------------- /loco/starwars/src/app.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use loco_rs::{ 3 | app::{AppContext, Hooks}, 4 | boot::{create_app, BootResult, StartMode}, 5 | controller::AppRoutes, 6 | environment::Environment, 7 | task::Tasks, 8 | worker::Processor, 9 | Result, 10 | }; 11 | 12 | use crate::controllers; 13 | 14 | pub struct App; 15 | #[async_trait] 16 | impl Hooks for App { 17 | fn app_name() -> &'static str { 18 | env!("CARGO_CRATE_NAME") 19 | } 20 | 21 | fn app_version() -> String { 22 | format!( 23 | "{} ({})", 24 | env!("CARGO_PKG_VERSION"), 25 | option_env!("BUILD_SHA") 26 | .or(option_env!("GITHUB_SHA")) 27 | .unwrap_or("dev") 28 | ) 29 | } 30 | 31 | async fn boot(mode: StartMode, environment: &Environment) -> Result { 32 | create_app::(mode, environment).await 33 | } 34 | 35 | fn routes(_ctx: &AppContext) -> AppRoutes { 36 | AppRoutes::empty().add_route(controllers::graphiql::routes()) 37 | } 38 | 39 | fn connect_workers<'a>(_p: &'a mut Processor, _ctx: &'a AppContext) {} 40 | 41 | fn register_tasks(_tasks: &mut Tasks) {} 42 | } 43 | -------------------------------------------------------------------------------- /loco/starwars/src/bin/main.rs: -------------------------------------------------------------------------------- 1 | use loco_rs::cli; 2 | use loco_starwars::app::App; 3 | 4 | #[tokio::main] 5 | async fn main() -> eyre::Result<()> { 6 | cli::main::().await 7 | } 8 | -------------------------------------------------------------------------------- /loco/starwars/src/controllers/graphiql.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::{http::GraphiQLSource, EmptyMutation, EmptySubscription, Schema}; 2 | use async_graphql_axum::GraphQL; 3 | use axum::debug_handler; 4 | use loco_rs::prelude::*; 5 | use starwars::{QueryRoot, StarWars}; 6 | 7 | #[debug_handler] 8 | async fn graphiql() -> Result { 9 | format::html(&GraphiQLSource::build().endpoint("/").finish()) 10 | } 11 | 12 | pub fn routes() -> Routes { 13 | let schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription) 14 | .data(StarWars::new()) 15 | .finish(); 16 | 17 | Routes::new().add("/", get(graphiql).post_service(GraphQL::new(schema))) 18 | } 19 | -------------------------------------------------------------------------------- /loco/starwars/src/controllers/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod graphiql; 2 | -------------------------------------------------------------------------------- /loco/starwars/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod app; 2 | pub mod controllers; 3 | -------------------------------------------------------------------------------- /loco/subscription/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | loco = "run --" 3 | -------------------------------------------------------------------------------- /loco/subscription/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | [package] 4 | name = "loco-subscription" 5 | version = "0.1.0" 6 | edition = "2024" 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [dependencies] 11 | loco-rs = { version = "0.6.0", default-features = false, features = ["cli"] } 12 | eyre = "*" 13 | tokio = { version = "1.33.0", default-features = false } 14 | async-trait = "0.1.74" 15 | axum = "0.7.1" 16 | 17 | # async-graphql dependencies 18 | async-graphql = { path = "../../.." } 19 | async-graphql-axum = { path = "../../../integrations/axum" } 20 | books = { path = "../../models/books" } 21 | 22 | [[bin]] 23 | name = "starwars-cli" 24 | path = "src/bin/main.rs" 25 | required-features = [] 26 | -------------------------------------------------------------------------------- /loco/subscription/README.md: -------------------------------------------------------------------------------- 1 | # async-graphql with Loco :train: 2 | 3 | Example async-graphql project with [Loco](https://github.com/loco-rs/loco). 4 | 5 | ## Quick Start 6 | 7 | Start your app: 8 | 9 | ``` 10 | $ cargo loco start 11 | Finished dev [unoptimized + debuginfo] target(s) in 21.63s 12 | Running `target/debug/myapp start` 13 | 14 | : 15 | : 16 | : 17 | 18 | controller/app_routes.rs:203: [Middleware] Adding log trace id 19 | 20 | ▄ ▀ 21 | ▀ ▄ 22 | ▄ ▀ ▄ ▄ ▄▀ 23 | ▄ ▀▄▄ 24 | ▄ ▀ ▀ ▀▄▀█▄ 25 | ▀█▄ 26 | ▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▀▀█ 27 | ██████ █████ ███ █████ ███ █████ ███ ▀█ 28 | ██████ █████ ███ █████ ▀▀▀ █████ ███ ▄█▄ 29 | ██████ █████ ███ █████ █████ ███ ████▄ 30 | ██████ █████ ███ █████ ▄▄▄ █████ ███ █████ 31 | ██████ █████ ███ ████ ███ █████ ███ ████▀ 32 | ▀▀▀██▄ ▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀ ██▀ 33 | ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ 34 | 35 | started on port 5150 36 | ``` 37 | -------------------------------------------------------------------------------- /loco/subscription/config/development.yaml: -------------------------------------------------------------------------------- 1 | # Loco configuration file documentation 2 | 3 | # Application logging configuration 4 | logger: 5 | # Enable or disable logging. 6 | enable: true 7 | # Log level, options: trace, debug, info, warn or error. 8 | level: debug 9 | # Define the logging format. options: compact, pretty or json 10 | format: compact 11 | # By default the logger has filtering only logs that came from your code or logs that came from `loco` framework. to see all third party libraries 12 | # Uncomment the line below to override to see all third party libraries you can enable this config and override the logger filters. 13 | # override_filter: trace 14 | 15 | # Web server configuration 16 | server: 17 | # Port on which the server will listen. the server binding is 0.0.0.0:{PORT} 18 | port: 5150 19 | # The UI hostname or IP address that mailers will point to. 20 | host: http://localhost 21 | # Out of the box middleware configuration. to disable middleware you can changed the `enable` field to `false` of comment the middleware block 22 | middlewares: 23 | # Enable Etag cache header middleware 24 | etag: 25 | enable: true 26 | # Allows to limit the payload size request. payload that bigger than this file will blocked the request. 27 | limit_payload: 28 | # Enable/Disable the middleware. 29 | enable: true 30 | # the limit size. can be b,kb,kib,mb,mib,gb,gib 31 | body_limit: 5mb 32 | # Generating a unique request ID and enhancing logging with additional information such as the start and completion of request processing, latency, status code, and other request details. 33 | logger: 34 | # Enable/Disable the middleware. 35 | enable: true 36 | # when your code is panicked, the request still returns 500 status code. 37 | catch_panic: 38 | # Enable/Disable the middleware. 39 | enable: true 40 | # Timeout for incoming requests middleware. requests that take more time from the configuration will cute and 408 status code will returned. 41 | timeout_request: 42 | # Enable/Disable the middleware. 43 | enable: false 44 | # Duration time in milliseconds. 45 | timeout: 5000 46 | -------------------------------------------------------------------------------- /loco/subscription/src/app.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::Schema; 2 | use books::{MutationRoot, QueryRoot, Storage, SubscriptionRoot}; 3 | 4 | use async_graphql_axum::GraphQLSubscription; 5 | use async_trait::async_trait; 6 | use axum::Router as AxumRouter; 7 | use loco_rs::{ 8 | app::{AppContext, Hooks}, 9 | boot::{create_app, BootResult, StartMode}, 10 | controller::AppRoutes, 11 | environment::Environment, 12 | task::Tasks, 13 | worker::Processor, 14 | Result, 15 | }; 16 | 17 | use crate::controllers; 18 | 19 | pub struct App; 20 | #[async_trait] 21 | impl Hooks for App { 22 | fn app_name() -> &'static str { 23 | env!("CARGO_CRATE_NAME") 24 | } 25 | 26 | fn app_version() -> String { 27 | format!( 28 | "{} ({})", 29 | env!("CARGO_PKG_VERSION"), 30 | option_env!("BUILD_SHA") 31 | .or(option_env!("GITHUB_SHA")) 32 | .unwrap_or("dev") 33 | ) 34 | } 35 | 36 | async fn boot(mode: StartMode, environment: &Environment) -> Result { 37 | create_app::(mode, environment).await 38 | } 39 | 40 | fn routes(_ctx: &AppContext) -> AppRoutes { 41 | AppRoutes::empty().add_route(controllers::graphiql::routes()) 42 | } 43 | 44 | async fn after_routes(router: AxumRouter, _ctx: &AppContext) -> Result { 45 | let schema = Schema::build(QueryRoot, MutationRoot, SubscriptionRoot) 46 | .data(Storage::default()) 47 | .finish(); 48 | Ok(router.route_service("/ws", GraphQLSubscription::new(schema))) 49 | } 50 | 51 | fn connect_workers<'a>(_p: &'a mut Processor, _ctx: &'a AppContext) {} 52 | 53 | fn register_tasks(_tasks: &mut Tasks) {} 54 | } 55 | -------------------------------------------------------------------------------- /loco/subscription/src/bin/main.rs: -------------------------------------------------------------------------------- 1 | use loco_rs::cli; 2 | use loco_subscription::app::App; 3 | 4 | #[tokio::main] 5 | async fn main() -> eyre::Result<()> { 6 | cli::main::().await 7 | } 8 | -------------------------------------------------------------------------------- /loco/subscription/src/controllers/graphiql.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::{http::GraphiQLSource, Schema}; 2 | use async_graphql_axum::GraphQL; 3 | use axum::debug_handler; 4 | use books::{MutationRoot, QueryRoot, Storage, SubscriptionRoot}; 5 | use loco_rs::prelude::*; 6 | 7 | #[debug_handler] 8 | async fn graphiql() -> Result { 9 | format::html( 10 | &GraphiQLSource::build() 11 | .endpoint("/") 12 | .subscription_endpoint("/ws") 13 | .finish(), 14 | ) 15 | } 16 | 17 | pub fn routes() -> Routes { 18 | let schema = Schema::build(QueryRoot, MutationRoot, SubscriptionRoot) 19 | .data(Storage::default()) 20 | .finish(); 21 | 22 | Routes::new().add("/", get(graphiql).post_service(GraphQL::new(schema))) 23 | } 24 | -------------------------------------------------------------------------------- /loco/subscription/src/controllers/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod graphiql; 2 | -------------------------------------------------------------------------------- /loco/subscription/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod app; 2 | pub mod controllers; 3 | -------------------------------------------------------------------------------- /models/books/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "books" 3 | version = "0.1.1" 4 | authors = ["sunli "] 5 | edition = "2024" 6 | 7 | [dependencies] 8 | async-graphql = { path = "../../.." } 9 | slab = "0.4.9" 10 | futures-util = "0.3.30" 11 | futures-channel = "0.3.30" 12 | once_cell = "1.19" 13 | futures-timer = "3.0.3" 14 | async-stream = "0.3.5" 15 | -------------------------------------------------------------------------------- /models/books/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod simple_broker; 2 | 3 | use std::{sync::Arc, time::Duration}; 4 | 5 | use async_graphql::{Context, Enum, ID, Object, Result, Schema, Subscription}; 6 | use futures_util::{Stream, StreamExt, lock::Mutex}; 7 | use simple_broker::SimpleBroker; 8 | use slab::Slab; 9 | 10 | pub type BooksSchema = Schema; 11 | 12 | #[derive(Clone)] 13 | pub struct Book { 14 | id: ID, 15 | name: String, 16 | author: String, 17 | } 18 | 19 | #[Object] 20 | impl Book { 21 | async fn id(&self) -> &str { 22 | &self.id 23 | } 24 | 25 | async fn name(&self) -> &str { 26 | &self.name 27 | } 28 | 29 | async fn author(&self) -> &str { 30 | &self.author 31 | } 32 | } 33 | 34 | pub type Storage = Arc>>; 35 | 36 | pub struct QueryRoot; 37 | 38 | #[Object] 39 | impl QueryRoot { 40 | async fn books(&self, ctx: &Context<'_>) -> Vec { 41 | let books = ctx.data_unchecked::().lock().await; 42 | books.iter().map(|(_, book)| book).cloned().collect() 43 | } 44 | } 45 | 46 | pub struct MutationRoot; 47 | 48 | #[Object] 49 | impl MutationRoot { 50 | async fn create_book(&self, ctx: &Context<'_>, name: String, author: String) -> ID { 51 | let mut books = ctx.data_unchecked::().lock().await; 52 | let entry = books.vacant_entry(); 53 | let id: ID = entry.key().into(); 54 | let book = Book { 55 | id: id.clone(), 56 | name, 57 | author, 58 | }; 59 | entry.insert(book); 60 | SimpleBroker::publish(BookChanged { 61 | mutation_type: MutationType::Created, 62 | id: id.clone(), 63 | }); 64 | id 65 | } 66 | 67 | async fn delete_book(&self, ctx: &Context<'_>, id: ID) -> Result { 68 | let mut books = ctx.data_unchecked::().lock().await; 69 | let id = id.parse::()?; 70 | if books.contains(id) { 71 | books.remove(id); 72 | SimpleBroker::publish(BookChanged { 73 | mutation_type: MutationType::Deleted, 74 | id: id.into(), 75 | }); 76 | Ok(true) 77 | } else { 78 | Ok(false) 79 | } 80 | } 81 | } 82 | 83 | #[derive(Enum, Eq, PartialEq, Copy, Clone)] 84 | enum MutationType { 85 | Created, 86 | Deleted, 87 | } 88 | 89 | #[derive(Clone)] 90 | struct BookChanged { 91 | mutation_type: MutationType, 92 | id: ID, 93 | } 94 | 95 | #[Object] 96 | impl BookChanged { 97 | async fn mutation_type(&self) -> MutationType { 98 | self.mutation_type 99 | } 100 | 101 | async fn id(&self) -> &ID { 102 | &self.id 103 | } 104 | 105 | async fn book(&self, ctx: &Context<'_>) -> Result> { 106 | let books = ctx.data_unchecked::().lock().await; 107 | let id = self.id.parse::()?; 108 | Ok(books.get(id).cloned()) 109 | } 110 | } 111 | 112 | pub struct SubscriptionRoot; 113 | 114 | #[Subscription] 115 | impl SubscriptionRoot { 116 | async fn interval(&self, #[graphql(default = 1)] n: i32) -> impl Stream { 117 | let mut value = 0; 118 | async_stream::stream! { 119 | loop { 120 | futures_timer::Delay::new(Duration::from_secs(1)).await; 121 | value += n; 122 | yield value; 123 | } 124 | } 125 | } 126 | 127 | async fn books(&self, mutation_type: Option) -> impl Stream { 128 | SimpleBroker::::subscribe().filter(move |event| { 129 | let res = if let Some(mutation_type) = mutation_type { 130 | event.mutation_type == mutation_type 131 | } else { 132 | true 133 | }; 134 | async move { res } 135 | }) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /models/books/src/simple_broker.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | any::{Any, TypeId}, 3 | collections::HashMap, 4 | marker::PhantomData, 5 | pin::Pin, 6 | sync::Mutex, 7 | task::{Context, Poll}, 8 | }; 9 | 10 | use futures_channel::mpsc::{self, UnboundedReceiver, UnboundedSender}; 11 | use futures_util::{Stream, StreamExt}; 12 | use once_cell::sync::Lazy; 13 | use slab::Slab; 14 | 15 | static SUBSCRIBERS: Lazy>>> = Lazy::new(Default::default); 16 | 17 | struct Senders(Slab>); 18 | 19 | struct BrokerStream(usize, UnboundedReceiver); 20 | 21 | fn with_senders(f: F) -> R 22 | where 23 | T: Sync + Send + Clone + 'static, 24 | F: FnOnce(&mut Senders) -> R, 25 | { 26 | let mut map = SUBSCRIBERS.lock().unwrap(); 27 | let senders = map 28 | .entry(TypeId::of::>()) 29 | .or_insert_with(|| Box::new(Senders::(Default::default()))); 30 | f(senders.downcast_mut::>().unwrap()) 31 | } 32 | 33 | impl Drop for BrokerStream { 34 | fn drop(&mut self) { 35 | with_senders::(|senders| senders.0.remove(self.0)); 36 | } 37 | } 38 | 39 | impl Stream for BrokerStream { 40 | type Item = T; 41 | 42 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 43 | self.1.poll_next_unpin(cx) 44 | } 45 | } 46 | 47 | /// A simple broker based on memory 48 | pub struct SimpleBroker(PhantomData); 49 | 50 | impl SimpleBroker { 51 | /// Publish a message that all subscription streams can receive. 52 | pub fn publish(msg: T) { 53 | with_senders::(|senders| { 54 | for (_, sender) in senders.0.iter_mut() { 55 | sender.start_send(msg.clone()).ok(); 56 | } 57 | }); 58 | } 59 | 60 | /// Subscribe to the message of the specified type and returns a `Stream`. 61 | pub fn subscribe() -> impl Stream { 62 | with_senders::(|senders| { 63 | let (tx, rx) = mpsc::unbounded(); 64 | let id = senders.0.insert(tx); 65 | BrokerStream(id, rx) 66 | }) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /models/dynamic-books/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dynamic-books" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | async-graphql = { path = "../../..", features = ["dynamic-schema"] } 8 | slab = "0.4.9" 9 | futures-util = "0.3.30" 10 | futures-channel = "0.3.30" 11 | once_cell = "1.19" 12 | futures-timer = "3.0.3" 13 | async-stream = "0.3.5" 14 | -------------------------------------------------------------------------------- /models/dynamic-books/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod model; 2 | mod simple_broker; 3 | use std::{str::FromStr, sync::Arc}; 4 | 5 | use async_graphql::ID; 6 | use futures_util::lock::Mutex; 7 | pub use model::schema; 8 | use slab::Slab; 9 | 10 | #[derive(Clone)] 11 | pub struct Book { 12 | id: ID, 13 | name: String, 14 | author: String, 15 | } 16 | 17 | type Storage = Arc>>; 18 | 19 | #[derive(Eq, PartialEq, Copy, Clone, Debug)] 20 | pub enum MutationType { 21 | Created, 22 | Deleted, 23 | } 24 | 25 | impl FromStr for MutationType { 26 | type Err = String; // Error type can be customized based on your needs 27 | 28 | fn from_str(s: &str) -> Result { 29 | match s { 30 | "CREATED" => Ok(MutationType::Created), 31 | "DELETED" => Ok(MutationType::Deleted), 32 | _ => Err(format!("Invalid MutationType: {}", s)), 33 | } 34 | } 35 | } 36 | 37 | #[derive(Clone)] 38 | struct BookChanged { 39 | mutation_type: MutationType, 40 | id: ID, 41 | } 42 | -------------------------------------------------------------------------------- /models/dynamic-books/src/model.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::{ID, Value, dynamic::*}; 2 | use futures_util::StreamExt; 3 | 4 | use crate::{Book, BookChanged, MutationType, Storage, simple_broker::SimpleBroker}; 5 | 6 | impl From for FieldValue<'_> { 7 | fn from(value: MutationType) -> Self { 8 | match value { 9 | MutationType::Created => FieldValue::value("CREATED"), 10 | MutationType::Deleted => FieldValue::value("DELETED"), 11 | } 12 | } 13 | } 14 | 15 | pub fn schema() -> Result { 16 | let mutation_type = Enum::new("MutationType") 17 | .item(EnumItem::new("CREATED").description("New book created.")) 18 | .item(EnumItem::new("DELETED").description("Current book deleted.")); 19 | 20 | let book = Object::new("Book") 21 | .description("A book that will be stored.") 22 | .field(Field::new("id", TypeRef::named_nn(TypeRef::ID), |ctx| { 23 | FieldFuture::new(async move { 24 | let book = ctx.parent_value.try_downcast_ref::()?; 25 | Ok(Some(Value::from(book.id.to_owned()))) 26 | }) 27 | })) 28 | .field(Field::new( 29 | "name", 30 | TypeRef::named_nn(TypeRef::STRING), 31 | |ctx| { 32 | FieldFuture::new(async move { 33 | let book = ctx.parent_value.try_downcast_ref::()?; 34 | Ok(Some(Value::from(book.name.to_owned()))) 35 | }) 36 | }, 37 | )) 38 | .field(Field::new( 39 | "author", 40 | TypeRef::named_nn(TypeRef::STRING), 41 | |ctx| { 42 | FieldFuture::new(async move { 43 | let book = ctx.parent_value.try_downcast_ref::()?; 44 | Ok(Some(Value::from(book.author.to_owned()))) 45 | }) 46 | }, 47 | )); 48 | let book_changed = Object::new("BookChanged") 49 | .field(Field::new( 50 | "mutationType", 51 | TypeRef::named_nn(mutation_type.type_name()), 52 | |ctx| { 53 | FieldFuture::new(async move { 54 | let book_changed = ctx.parent_value.try_downcast_ref::()?; 55 | Ok(Some(FieldValue::from(book_changed.mutation_type))) 56 | }) 57 | }, 58 | )) 59 | .field(Field::new("id", TypeRef::named_nn(TypeRef::ID), |ctx| { 60 | FieldFuture::new(async move { 61 | let book_changed = ctx.parent_value.try_downcast_ref::()?; 62 | Ok(Some(Value::from(book_changed.id.to_owned()))) 63 | }) 64 | })) 65 | .field(Field::new( 66 | "book", 67 | TypeRef::named(book.type_name()), 68 | |ctx| { 69 | FieldFuture::new(async move { 70 | let book_changed = ctx.parent_value.try_downcast_ref::()?; 71 | let id = book_changed.id.to_owned(); 72 | let book_id = id.parse::()?; 73 | let store = ctx.data_unchecked::().lock().await; 74 | let book = store.get(book_id).cloned(); 75 | Ok(book.map(FieldValue::owned_any)) 76 | }) 77 | }, 78 | )); 79 | 80 | let query_root = Object::new("Query") 81 | .field(Field::new( 82 | "getBooks", 83 | TypeRef::named_list(book.type_name()), 84 | |ctx| { 85 | FieldFuture::new(async move { 86 | let store = ctx.data_unchecked::().lock().await; 87 | let books: Vec = store.iter().map(|(_, book)| book.clone()).collect(); 88 | Ok(Some(FieldValue::list( 89 | books.into_iter().map(FieldValue::owned_any), 90 | ))) 91 | }) 92 | }, 93 | )) 94 | .field( 95 | Field::new("getBook", TypeRef::named(book.type_name()), |ctx| { 96 | FieldFuture::new(async move { 97 | let id = ctx.args.try_get("id")?; 98 | let book_id = match id.string() { 99 | Ok(id) => id.to_string(), 100 | Err(_) => id.u64()?.to_string(), 101 | }; 102 | let book_id = book_id.parse::()?; 103 | let store = ctx.data_unchecked::().lock().await; 104 | let book = store.get(book_id).cloned(); 105 | Ok(book.map(FieldValue::owned_any)) 106 | }) 107 | }) 108 | .argument(InputValue::new("id", TypeRef::named_nn(TypeRef::ID))), 109 | ); 110 | 111 | let mutatation_root = Object::new("Mutation") 112 | .field( 113 | Field::new("createBook", TypeRef::named(TypeRef::ID), |ctx| { 114 | FieldFuture::new(async move { 115 | let mut store = ctx.data_unchecked::().lock().await; 116 | let name = ctx.args.try_get("name")?; 117 | let author = ctx.args.try_get("author")?; 118 | let entry = store.vacant_entry(); 119 | let id: ID = entry.key().into(); 120 | let book = Book { 121 | id: id.clone(), 122 | name: name.string()?.to_string(), 123 | author: author.string()?.to_string(), 124 | }; 125 | entry.insert(book); 126 | let book_mutated = BookChanged { 127 | mutation_type: MutationType::Created, 128 | id: id.clone(), 129 | }; 130 | SimpleBroker::publish(book_mutated); 131 | Ok(Some(Value::from(id))) 132 | }) 133 | }) 134 | .argument(InputValue::new("name", TypeRef::named_nn(TypeRef::STRING))) 135 | .argument(InputValue::new( 136 | "author", 137 | TypeRef::named_nn(TypeRef::STRING), 138 | )), 139 | ) 140 | .field( 141 | Field::new("deleteBook", TypeRef::named_nn(TypeRef::BOOLEAN), |ctx| { 142 | FieldFuture::new(async move { 143 | let mut store = ctx.data_unchecked::().lock().await; 144 | let id = ctx.args.try_get("id")?; 145 | let book_id = match id.string() { 146 | Ok(id) => id.to_string(), 147 | Err(_) => id.u64()?.to_string(), 148 | }; 149 | let book_id = book_id.parse::()?; 150 | if store.contains(book_id) { 151 | store.remove(book_id); 152 | let book_mutated = BookChanged { 153 | mutation_type: MutationType::Deleted, 154 | id: book_id.into(), 155 | }; 156 | SimpleBroker::publish(book_mutated); 157 | Ok(Some(Value::from(true))) 158 | } else { 159 | Ok(Some(Value::from(false))) 160 | } 161 | }) 162 | }) 163 | .argument(InputValue::new("id", TypeRef::named_nn(TypeRef::ID))), 164 | ); 165 | let subscription_root = Subscription::new("Subscription").field(SubscriptionField::new( 166 | "bookMutation", 167 | TypeRef::named_nn(book_changed.type_name()), 168 | |_| { 169 | SubscriptionFieldFuture::new(async { 170 | Ok(SimpleBroker::::subscribe() 171 | .map(|book| Ok(FieldValue::owned_any(book)))) 172 | }) 173 | }, 174 | )); 175 | 176 | Schema::build( 177 | query_root.type_name(), 178 | Some(mutatation_root.type_name()), 179 | Some(subscription_root.type_name()), 180 | ) 181 | .register(mutation_type) 182 | .register(book) 183 | .register(book_changed) 184 | .register(query_root) 185 | .register(subscription_root) 186 | .register(mutatation_root) 187 | .data(Storage::default()) 188 | .finish() 189 | } 190 | -------------------------------------------------------------------------------- /models/dynamic-books/src/simple_broker.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | any::{Any, TypeId}, 3 | collections::HashMap, 4 | marker::PhantomData, 5 | pin::Pin, 6 | sync::Mutex, 7 | task::{Context, Poll}, 8 | }; 9 | 10 | use futures_channel::mpsc::{self, UnboundedReceiver, UnboundedSender}; 11 | use futures_util::{Stream, StreamExt}; 12 | use once_cell::sync::Lazy; 13 | use slab::Slab; 14 | 15 | static SUBSCRIBERS: Lazy>>> = Lazy::new(Default::default); 16 | 17 | struct Senders(Slab>); 18 | 19 | struct BrokerStream(usize, UnboundedReceiver); 20 | 21 | fn with_senders(f: F) -> R 22 | where 23 | T: Sync + Send + Clone + 'static, 24 | F: FnOnce(&mut Senders) -> R, 25 | { 26 | let mut map = SUBSCRIBERS.lock().unwrap(); 27 | let senders = map 28 | .entry(TypeId::of::>()) 29 | .or_insert_with(|| Box::new(Senders::(Default::default()))); 30 | f(senders.downcast_mut::>().unwrap()) 31 | } 32 | 33 | impl Drop for BrokerStream { 34 | fn drop(&mut self) { 35 | with_senders::(|senders| senders.0.remove(self.0)); 36 | } 37 | } 38 | 39 | impl Stream for BrokerStream { 40 | type Item = T; 41 | 42 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 43 | self.1.poll_next_unpin(cx) 44 | } 45 | } 46 | 47 | /// A simple broker based on memory 48 | pub struct SimpleBroker(PhantomData); 49 | 50 | impl SimpleBroker { 51 | /// Publish a message that all subscription streams can receive. 52 | pub fn publish(msg: T) { 53 | with_senders::(|senders| { 54 | for (_, sender) in senders.0.iter_mut() { 55 | sender.start_send(msg.clone()).ok(); 56 | } 57 | }); 58 | } 59 | 60 | /// Subscribe to the message of the specified type and returns a `Stream`. 61 | pub fn subscribe() -> impl Stream { 62 | with_senders::(|senders| { 63 | let (tx, rx) = mpsc::unbounded(); 64 | let id = senders.0.insert(tx); 65 | BrokerStream(id, rx) 66 | }) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /models/dynamic-files/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dynamic-files" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | async-graphql = { path = "../../.." } 8 | slab = "0.4.9" 9 | futures = "0.3.30" 10 | -------------------------------------------------------------------------------- /models/dynamic-files/src/lib.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::{ 2 | Value, 3 | dynamic::{Field, FieldFuture, FieldValue, InputValue, Object, Schema, SchemaError, TypeRef}, 4 | }; 5 | use futures::lock::Mutex; 6 | use slab::Slab; 7 | 8 | pub type Storage = Mutex>; 9 | 10 | #[derive(Clone)] 11 | pub struct FileInfo { 12 | pub id: String, 13 | url: String, 14 | } 15 | 16 | pub fn schema() -> Result { 17 | let file_info = Object::new("FileInfo") 18 | .field(Field::new("id", TypeRef::named_nn(TypeRef::ID), |ctx| { 19 | FieldFuture::new(async { 20 | let file_info = ctx.parent_value.try_downcast_ref::()?; 21 | Ok(Some(Value::from(&file_info.id))) 22 | }) 23 | })) 24 | .field(Field::new( 25 | "url", 26 | TypeRef::named_nn(TypeRef::STRING), 27 | |ctx| { 28 | FieldFuture::new(async { 29 | let file_info = ctx.parent_value.try_downcast_ref::()?; 30 | Ok(Some(Value::from(&file_info.url))) 31 | }) 32 | }, 33 | )); 34 | 35 | let query = Object::new("Query").field(Field::new( 36 | "uploads", 37 | TypeRef::named_nn_list_nn(file_info.type_name()), 38 | |ctx| { 39 | FieldFuture::new(async move { 40 | let storage = ctx.data_unchecked::().lock().await; 41 | Ok(Some(FieldValue::list( 42 | storage 43 | .iter() 44 | .map(|(_, file)| FieldValue::owned_any(file.clone())), 45 | ))) 46 | }) 47 | }, 48 | )); 49 | 50 | let mutation = Object::new("Mutation") 51 | .field( 52 | Field::new( 53 | "singleUpload", 54 | TypeRef::named_nn(file_info.type_name()), 55 | |ctx| { 56 | FieldFuture::new(async move { 57 | let mut storage = ctx.data_unchecked::().lock().await; 58 | let file = ctx.args.try_get("file")?.upload()?; 59 | let entry = storage.vacant_entry(); 60 | let upload = file.value(&ctx).unwrap(); 61 | let info = FileInfo { 62 | id: entry.key().to_string(), 63 | url: upload.filename.clone(), 64 | }; 65 | entry.insert(info.clone()); 66 | Ok(Some(FieldValue::owned_any(info))) 67 | }) 68 | }, 69 | ) 70 | .argument(InputValue::new("file", TypeRef::named_nn(TypeRef::UPLOAD))), 71 | ) 72 | .field( 73 | Field::new( 74 | "multipleUpload", 75 | TypeRef::named_nn_list_nn(file_info.type_name()), 76 | |ctx| { 77 | FieldFuture::new(async move { 78 | let mut infos = Vec::new(); 79 | let mut storage = ctx.data_unchecked::().lock().await; 80 | for item in ctx.args.try_get("files")?.list()?.iter() { 81 | let file = item.upload()?; 82 | let entry = storage.vacant_entry(); 83 | let upload = file.value(&ctx).unwrap(); 84 | let info = FileInfo { 85 | id: entry.key().to_string(), 86 | url: upload.filename.clone(), 87 | }; 88 | entry.insert(info.clone()); 89 | infos.push(FieldValue::owned_any(info)) 90 | } 91 | Ok(Some(infos)) 92 | }) 93 | }, 94 | ) 95 | .argument(InputValue::new( 96 | "files", 97 | TypeRef::named_nn_list_nn(TypeRef::UPLOAD), 98 | )), 99 | ); 100 | 101 | Schema::build(query.type_name(), Some(mutation.type_name()), None) 102 | .enable_uploading() 103 | .register(file_info) 104 | .register(query) 105 | .register(mutation) 106 | .data(Storage::default()) 107 | .finish() 108 | } 109 | -------------------------------------------------------------------------------- /models/dynamic-starwars/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dynamic-starwars" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | async-graphql = { path = "../../..", features = ["dynamic-schema"] } 8 | slab = "0.4.9" 9 | -------------------------------------------------------------------------------- /models/dynamic-starwars/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod model; 2 | 3 | use std::collections::HashMap; 4 | 5 | pub use model::schema; 6 | use slab::Slab; 7 | 8 | /// One of the films in the Star Wars Trilogy 9 | #[derive(Copy, Clone, Eq, PartialEq)] 10 | pub enum Episode { 11 | /// Released in 1977. 12 | NewHope, 13 | 14 | /// Released in 1980. 15 | Empire, 16 | 17 | /// Released in 1983. 18 | Jedi, 19 | } 20 | 21 | pub struct StarWarsChar { 22 | id: &'static str, 23 | name: &'static str, 24 | is_human: bool, 25 | friends: Vec, 26 | appears_in: Vec, 27 | home_planet: Option<&'static str>, 28 | primary_function: Option<&'static str>, 29 | } 30 | 31 | pub struct StarWars { 32 | luke: usize, 33 | artoo: usize, 34 | chars: Slab, 35 | chars_by_id: HashMap<&'static str, usize>, 36 | } 37 | 38 | impl StarWars { 39 | #[allow(clippy::new_without_default)] 40 | pub fn new() -> Self { 41 | let mut chars = Slab::new(); 42 | 43 | let luke = chars.insert(StarWarsChar { 44 | id: "1000", 45 | name: "Luke Skywalker", 46 | is_human: true, 47 | friends: vec![], 48 | appears_in: vec![], 49 | home_planet: Some("Tatooine"), 50 | primary_function: None, 51 | }); 52 | 53 | let vader = chars.insert(StarWarsChar { 54 | id: "1001", 55 | name: "Anakin Skywalker", 56 | is_human: true, 57 | friends: vec![], 58 | appears_in: vec![], 59 | home_planet: Some("Tatooine"), 60 | primary_function: None, 61 | }); 62 | 63 | let han = chars.insert(StarWarsChar { 64 | id: "1002", 65 | name: "Han Solo", 66 | is_human: true, 67 | friends: vec![], 68 | appears_in: vec![Episode::Empire, Episode::NewHope, Episode::Jedi], 69 | home_planet: None, 70 | primary_function: None, 71 | }); 72 | 73 | let leia = chars.insert(StarWarsChar { 74 | id: "1003", 75 | name: "Leia Organa", 76 | is_human: true, 77 | friends: vec![], 78 | appears_in: vec![Episode::Empire, Episode::NewHope, Episode::Jedi], 79 | home_planet: Some("Alderaa"), 80 | primary_function: None, 81 | }); 82 | 83 | let tarkin = chars.insert(StarWarsChar { 84 | id: "1004", 85 | name: "Wilhuff Tarkin", 86 | is_human: true, 87 | friends: vec![], 88 | appears_in: vec![Episode::Empire, Episode::NewHope, Episode::Jedi], 89 | home_planet: None, 90 | primary_function: None, 91 | }); 92 | 93 | let threepio = chars.insert(StarWarsChar { 94 | id: "2000", 95 | name: "C-3PO", 96 | is_human: false, 97 | friends: vec![], 98 | appears_in: vec![Episode::Empire, Episode::NewHope, Episode::Jedi], 99 | home_planet: None, 100 | primary_function: Some("Protocol"), 101 | }); 102 | 103 | let artoo = chars.insert(StarWarsChar { 104 | id: "2001", 105 | name: "R2-D2", 106 | is_human: false, 107 | friends: vec![], 108 | appears_in: vec![Episode::Empire, Episode::NewHope, Episode::Jedi], 109 | home_planet: None, 110 | primary_function: Some("Astromech"), 111 | }); 112 | 113 | chars[luke].friends = vec![han, leia, threepio, artoo]; 114 | chars[vader].friends = vec![tarkin]; 115 | chars[han].friends = vec![luke, leia, artoo]; 116 | chars[leia].friends = vec![luke, han, threepio, artoo]; 117 | chars[tarkin].friends = vec![vader]; 118 | chars[threepio].friends = vec![luke, han, leia, artoo]; 119 | chars[artoo].friends = vec![luke, han, leia]; 120 | 121 | let chars_by_id = chars.iter().map(|(idx, ch)| (ch.id, idx)).collect(); 122 | Self { 123 | luke, 124 | artoo, 125 | chars, 126 | chars_by_id, 127 | } 128 | } 129 | 130 | pub fn human(&self, id: &str) -> Option<&StarWarsChar> { 131 | self.chars_by_id 132 | .get(id) 133 | .copied() 134 | .map(|idx| self.chars.get(idx).unwrap()) 135 | .filter(|ch| ch.is_human) 136 | } 137 | 138 | pub fn droid(&self, id: &str) -> Option<&StarWarsChar> { 139 | self.chars_by_id 140 | .get(id) 141 | .copied() 142 | .map(|idx| self.chars.get(idx).unwrap()) 143 | .filter(|ch| !ch.is_human) 144 | } 145 | 146 | pub fn humans(&self) -> Vec<&StarWarsChar> { 147 | self.chars 148 | .iter() 149 | .filter(|(_, ch)| ch.is_human) 150 | .map(|(_, ch)| ch) 151 | .collect() 152 | } 153 | 154 | pub fn droids(&self) -> Vec<&StarWarsChar> { 155 | self.chars 156 | .iter() 157 | .filter(|(_, ch)| !ch.is_human) 158 | .map(|(_, ch)| ch) 159 | .collect() 160 | } 161 | 162 | pub fn friends(&self, ch: &StarWarsChar) -> Vec<&StarWarsChar> { 163 | ch.friends 164 | .iter() 165 | .copied() 166 | .filter_map(|id| self.chars.get(id)) 167 | .collect() 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /models/dynamic-starwars/src/model.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::{Value, dynamic::*}; 2 | 3 | use crate::{Episode, StarWars, StarWarsChar}; 4 | 5 | impl From for FieldValue<'_> { 6 | fn from(value: Episode) -> Self { 7 | match value { 8 | Episode::NewHope => FieldValue::value("NEW_HOPE"), 9 | Episode::Empire => FieldValue::value("EMPIRE"), 10 | Episode::Jedi => FieldValue::value("JEDI"), 11 | } 12 | } 13 | } 14 | 15 | pub fn schema() -> Result { 16 | let episode = Enum::new("Episode") 17 | .item(EnumItem::new("NEW_HOPE").description("Released in 1977.")) 18 | .item(EnumItem::new("EMPIRE").description("Released in 1980.")) 19 | .item(EnumItem::new("JEDI").description("Released in 1983.")); 20 | 21 | let character = Interface::new("Character") 22 | .field(InterfaceField::new( 23 | "id", 24 | TypeRef::named_nn(TypeRef::STRING), 25 | )) 26 | .field(InterfaceField::new( 27 | "name", 28 | TypeRef::named_nn(TypeRef::STRING), 29 | )) 30 | .field(InterfaceField::new( 31 | "friends", 32 | TypeRef::named_nn_list_nn("Character"), 33 | )) 34 | .field(InterfaceField::new( 35 | "appearsIn", 36 | TypeRef::named_nn_list_nn(episode.type_name()), 37 | )); 38 | 39 | let human = Object::new("Human") 40 | .description("A humanoid creature in the Star Wars universe.") 41 | .implement(character.type_name()) 42 | .field( 43 | Field::new("id", TypeRef::named_nn(TypeRef::STRING), |ctx| { 44 | FieldFuture::new(async move { 45 | let char = ctx.parent_value.try_downcast_ref::()?; 46 | Ok(Some(Value::from(char.id))) 47 | }) 48 | }) 49 | .description("The id of the human."), 50 | ) 51 | .field( 52 | Field::new("name", TypeRef::named_nn(TypeRef::STRING), |ctx| { 53 | FieldFuture::new(async move { 54 | let char = ctx.parent_value.try_downcast_ref::()?; 55 | Ok(Some(Value::from(char.name))) 56 | }) 57 | }) 58 | .description("The name of the human."), 59 | ) 60 | .field( 61 | Field::new( 62 | "friends", 63 | TypeRef::named_nn_list_nn(character.type_name()), 64 | |ctx| { 65 | FieldFuture::new(async move { 66 | let char = ctx.parent_value.try_downcast_ref::()?; 67 | let starwars = ctx.data::()?; 68 | let friends = starwars.friends(char); 69 | Ok(Some(FieldValue::list(friends.into_iter().map(|friend| { 70 | FieldValue::borrowed_any(friend).with_type(if friend.is_human { 71 | "Human" 72 | } else { 73 | "Droid" 74 | }) 75 | })))) 76 | }) 77 | }, 78 | ) 79 | .description("The friends of the human, or an empty list if they have none."), 80 | ) 81 | .field( 82 | Field::new( 83 | "appearsIn", 84 | TypeRef::named_nn_list_nn(episode.type_name()), 85 | |ctx| { 86 | FieldFuture::new(async move { 87 | let char = ctx.parent_value.try_downcast_ref::()?; 88 | Ok(Some(FieldValue::list( 89 | char.appears_in.iter().copied().map(FieldValue::from), 90 | ))) 91 | }) 92 | }, 93 | ) 94 | .description("Which movies they appear in."), 95 | ) 96 | .field( 97 | Field::new("homePlanet", TypeRef::named(TypeRef::STRING), |ctx| { 98 | FieldFuture::new(async move { 99 | let char = ctx.parent_value.try_downcast_ref::()?; 100 | Ok(char.home_planet.map(Value::from)) 101 | }) 102 | }) 103 | .description("The home planet of the human, or null if unknown."), 104 | ); 105 | 106 | let droid = Object::new("Droid") 107 | .description("A mechanical creature in the Star Wars universe.") 108 | .implement(character.type_name()) 109 | .field( 110 | Field::new("id", TypeRef::named_nn(TypeRef::STRING), |ctx| { 111 | FieldFuture::new(async move { 112 | let char = ctx.parent_value.try_downcast_ref::()?; 113 | Ok(Some(Value::from(char.id))) 114 | }) 115 | }) 116 | .description("The id of the droid."), 117 | ) 118 | .field( 119 | Field::new("name", TypeRef::named_nn(TypeRef::STRING), |ctx| { 120 | FieldFuture::new(async move { 121 | let char = ctx.parent_value.try_downcast_ref::()?; 122 | Ok(Some(Value::from(char.name))) 123 | }) 124 | }) 125 | .description("The name of the droid."), 126 | ) 127 | .field( 128 | Field::new( 129 | "friends", 130 | TypeRef::named_nn_list_nn(character.type_name()), 131 | |ctx| { 132 | FieldFuture::new(async move { 133 | let char = ctx.parent_value.try_downcast_ref::()?; 134 | let starwars = ctx.data::()?; 135 | let friends = starwars.friends(char); 136 | Ok(Some(FieldValue::list(friends.into_iter().map(|friend| { 137 | FieldValue::borrowed_any(friend).with_type(if friend.is_human { 138 | "Human" 139 | } else { 140 | "Droid" 141 | }) 142 | })))) 143 | }) 144 | }, 145 | ) 146 | .description("The friends of the droid, or an empty list if they have none."), 147 | ) 148 | .field( 149 | Field::new( 150 | "appearsIn", 151 | TypeRef::named_nn_list_nn(episode.type_name()), 152 | |ctx| { 153 | FieldFuture::new(async move { 154 | let char = ctx.parent_value.try_downcast_ref::()?; 155 | Ok(Some(FieldValue::list( 156 | char.appears_in.iter().copied().map(FieldValue::from), 157 | ))) 158 | }) 159 | }, 160 | ) 161 | .description("Which movies they appear in."), 162 | ) 163 | .field( 164 | Field::new("primaryFunction", TypeRef::named(TypeRef::STRING), |ctx| { 165 | FieldFuture::new(async move { 166 | let char = ctx.parent_value.try_downcast_ref::()?; 167 | Ok(char.primary_function.map(Value::from)) 168 | }) 169 | }) 170 | .description("The primary function of the droid."), 171 | ); 172 | 173 | let query = Object::new("Query") 174 | .field( 175 | Field::new("hero", TypeRef::named_nn(character.type_name()), |ctx| { 176 | FieldFuture::new(async move { 177 | let starwars = ctx.data::()?; 178 | let episode = match ctx.args.get("episode") { 179 | Some(episode) => Some(match episode.enum_name()? { 180 | "NEW_HOPE" => Episode::NewHope, 181 | "EMPIRE" => Episode::Empire, 182 | "JEDI" => Episode::Jedi, 183 | _ => unreachable!(), 184 | }), 185 | None => None, 186 | }; 187 | let value = match episode { 188 | Some(episode) => { 189 | if episode == Episode::Empire { 190 | FieldValue::borrowed_any(starwars.chars.get(starwars.luke).unwrap()) 191 | .with_type("Human") 192 | } else { 193 | FieldValue::borrowed_any( 194 | starwars.chars.get(starwars.artoo).unwrap(), 195 | ) 196 | .with_type("Droid") 197 | } 198 | } 199 | None => { 200 | FieldValue::borrowed_any(starwars.chars.get(starwars.luke).unwrap()) 201 | .with_type("Human") 202 | } 203 | }; 204 | Ok(Some(value)) 205 | }) 206 | }) 207 | .argument(InputValue::new( 208 | "episode", 209 | TypeRef::named(episode.type_name()), 210 | )), 211 | ) 212 | .field( 213 | Field::new("human", TypeRef::named(human.type_name()), |ctx| { 214 | FieldFuture::new(async move { 215 | let starwars = ctx.data::()?; 216 | let id = ctx.args.try_get("id")?; 217 | Ok(starwars 218 | .human(id.string()?) 219 | .map(|human| FieldValue::borrowed_any(human))) 220 | }) 221 | }) 222 | .argument(InputValue::new("id", TypeRef::named_nn(TypeRef::STRING))), 223 | ) 224 | .field(Field::new( 225 | "humans", 226 | TypeRef::named_nn_list_nn(human.type_name()), 227 | |ctx| { 228 | FieldFuture::new(async move { 229 | let starwars = ctx.data::()?; 230 | let humans = starwars.humans(); 231 | Ok(Some(FieldValue::list( 232 | humans 233 | .into_iter() 234 | .map(|human| FieldValue::borrowed_any(human)), 235 | ))) 236 | }) 237 | }, 238 | )) 239 | .field( 240 | Field::new("droid", TypeRef::named(human.type_name()), |ctx| { 241 | FieldFuture::new(async move { 242 | let starwars = ctx.data::()?; 243 | let id = ctx.args.try_get("id")?; 244 | Ok(starwars 245 | .droid(id.string()?) 246 | .map(|droid| FieldValue::borrowed_any(droid))) 247 | }) 248 | }) 249 | .argument(InputValue::new("id", TypeRef::named_nn(TypeRef::STRING))), 250 | ) 251 | .field(Field::new( 252 | "droids", 253 | TypeRef::named_nn_list_nn(human.type_name()), 254 | |ctx| { 255 | FieldFuture::new(async move { 256 | let starwars = ctx.data::()?; 257 | let droids = starwars.droids(); 258 | Ok(Some(FieldValue::list( 259 | droids 260 | .into_iter() 261 | .map(|droid| FieldValue::borrowed_any(droid)), 262 | ))) 263 | }) 264 | }, 265 | )); 266 | 267 | Schema::build(query.type_name(), None, None) 268 | .register(episode) 269 | .register(character) 270 | .register(human) 271 | .register(droid) 272 | .register(query) 273 | .data(StarWars::new()) 274 | .finish() 275 | } 276 | -------------------------------------------------------------------------------- /models/files/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "files" 3 | version = "0.1.1" 4 | authors = ["sunli "] 5 | edition = "2024" 6 | 7 | [dependencies] 8 | async-graphql = { path = "../../.." } 9 | slab = "0.4.9" 10 | futures = "0.3.30" 11 | -------------------------------------------------------------------------------- /models/files/src/lib.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::{Context, EmptySubscription, ID, Object, Schema, SimpleObject, Upload}; 2 | use futures::lock::Mutex; 3 | use slab::Slab; 4 | 5 | pub type FilesSchema = Schema; 6 | 7 | #[derive(Clone, SimpleObject)] 8 | pub struct FileInfo { 9 | id: ID, 10 | url: String, 11 | } 12 | 13 | pub type Storage = Mutex>; 14 | 15 | pub struct QueryRoot; 16 | 17 | #[Object] 18 | impl QueryRoot { 19 | async fn uploads(&self, ctx: &Context<'_>) -> Vec { 20 | let storage = ctx.data_unchecked::().lock().await; 21 | storage.iter().map(|(_, file)| file).cloned().collect() 22 | } 23 | } 24 | 25 | pub struct MutationRoot; 26 | 27 | #[Object] 28 | impl MutationRoot { 29 | async fn single_upload(&self, ctx: &Context<'_>, file: Upload) -> FileInfo { 30 | let mut storage = ctx.data_unchecked::().lock().await; 31 | let entry = storage.vacant_entry(); 32 | let upload = file.value(ctx).unwrap(); 33 | let info = FileInfo { 34 | id: entry.key().into(), 35 | url: upload.filename, 36 | }; 37 | entry.insert(info.clone()); 38 | info 39 | } 40 | 41 | async fn multiple_upload(&self, ctx: &Context<'_>, files: Vec) -> Vec { 42 | let mut infos = Vec::new(); 43 | let mut storage = ctx.data_unchecked::().lock().await; 44 | for file in files { 45 | let entry = storage.vacant_entry(); 46 | let upload = file.value(ctx).unwrap(); 47 | let info = FileInfo { 48 | id: entry.key().into(), 49 | url: upload.filename.clone(), 50 | }; 51 | entry.insert(info.clone()); 52 | infos.push(info) 53 | } 54 | infos 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /models/starwars/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "starwars" 3 | version = "0.1.1" 4 | authors = ["sunli "] 5 | edition = "2024" 6 | 7 | [dependencies] 8 | async-graphql = { path = "../../.." } 9 | slab = "0.4.9" 10 | -------------------------------------------------------------------------------- /models/starwars/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod model; 2 | 3 | use std::collections::HashMap; 4 | 5 | use async_graphql::{EmptyMutation, EmptySubscription, Schema}; 6 | use model::Episode; 7 | pub use model::QueryRoot; 8 | use slab::Slab; 9 | pub type StarWarsSchema = Schema; 10 | 11 | pub struct StarWarsChar { 12 | id: &'static str, 13 | name: &'static str, 14 | is_human: bool, 15 | friends: Vec, 16 | appears_in: Vec, 17 | home_planet: Option<&'static str>, 18 | primary_function: Option<&'static str>, 19 | } 20 | 21 | pub struct StarWars { 22 | luke: usize, 23 | artoo: usize, 24 | chars: Slab, 25 | chars_by_id: HashMap<&'static str, usize>, 26 | } 27 | 28 | impl StarWars { 29 | #[allow(clippy::new_without_default)] 30 | pub fn new() -> Self { 31 | let mut chars = Slab::new(); 32 | 33 | let luke = chars.insert(StarWarsChar { 34 | id: "1000", 35 | name: "Luke Skywalker", 36 | is_human: true, 37 | friends: vec![], 38 | appears_in: vec![], 39 | home_planet: Some("Tatooine"), 40 | primary_function: None, 41 | }); 42 | 43 | let vader = chars.insert(StarWarsChar { 44 | id: "1001", 45 | name: "Anakin Skywalker", 46 | is_human: true, 47 | friends: vec![], 48 | appears_in: vec![], 49 | home_planet: Some("Tatooine"), 50 | primary_function: None, 51 | }); 52 | 53 | let han = chars.insert(StarWarsChar { 54 | id: "1002", 55 | name: "Han Solo", 56 | is_human: true, 57 | friends: vec![], 58 | appears_in: vec![Episode::Empire, Episode::NewHope, Episode::Jedi], 59 | home_planet: None, 60 | primary_function: None, 61 | }); 62 | 63 | let leia = chars.insert(StarWarsChar { 64 | id: "1003", 65 | name: "Leia Organa", 66 | is_human: true, 67 | friends: vec![], 68 | appears_in: vec![Episode::Empire, Episode::NewHope, Episode::Jedi], 69 | home_planet: Some("Alderaa"), 70 | primary_function: None, 71 | }); 72 | 73 | let tarkin = chars.insert(StarWarsChar { 74 | id: "1004", 75 | name: "Wilhuff Tarkin", 76 | is_human: true, 77 | friends: vec![], 78 | appears_in: vec![Episode::Empire, Episode::NewHope, Episode::Jedi], 79 | home_planet: None, 80 | primary_function: None, 81 | }); 82 | 83 | let threepio = chars.insert(StarWarsChar { 84 | id: "2000", 85 | name: "C-3PO", 86 | is_human: false, 87 | friends: vec![], 88 | appears_in: vec![Episode::Empire, Episode::NewHope, Episode::Jedi], 89 | home_planet: None, 90 | primary_function: Some("Protocol"), 91 | }); 92 | 93 | let artoo = chars.insert(StarWarsChar { 94 | id: "2001", 95 | name: "R2-D2", 96 | is_human: false, 97 | friends: vec![], 98 | appears_in: vec![Episode::Empire, Episode::NewHope, Episode::Jedi], 99 | home_planet: None, 100 | primary_function: Some("Astromech"), 101 | }); 102 | 103 | chars[luke].friends = vec![han, leia, threepio, artoo]; 104 | chars[vader].friends = vec![tarkin]; 105 | chars[han].friends = vec![luke, leia, artoo]; 106 | chars[leia].friends = vec![luke, han, threepio, artoo]; 107 | chars[tarkin].friends = vec![vader]; 108 | chars[threepio].friends = vec![luke, han, leia, artoo]; 109 | chars[artoo].friends = vec![luke, han, leia]; 110 | 111 | let chars_by_id = chars.iter().map(|(idx, ch)| (ch.id, idx)).collect(); 112 | Self { 113 | luke, 114 | artoo, 115 | chars, 116 | chars_by_id, 117 | } 118 | } 119 | 120 | pub fn human(&self, id: &str) -> Option<&StarWarsChar> { 121 | self.chars_by_id 122 | .get(id) 123 | .copied() 124 | .map(|idx| self.chars.get(idx).unwrap()) 125 | .filter(|ch| ch.is_human) 126 | } 127 | 128 | pub fn droid(&self, id: &str) -> Option<&StarWarsChar> { 129 | self.chars_by_id 130 | .get(id) 131 | .copied() 132 | .map(|idx| self.chars.get(idx).unwrap()) 133 | .filter(|ch| !ch.is_human) 134 | } 135 | 136 | pub fn humans(&self) -> Vec<&StarWarsChar> { 137 | self.chars 138 | .iter() 139 | .filter(|(_, ch)| ch.is_human) 140 | .map(|(_, ch)| ch) 141 | .collect() 142 | } 143 | 144 | pub fn droids(&self) -> Vec<&StarWarsChar> { 145 | self.chars 146 | .iter() 147 | .filter(|(_, ch)| !ch.is_human) 148 | .map(|(_, ch)| ch) 149 | .collect() 150 | } 151 | 152 | pub fn friends(&self, ch: &StarWarsChar) -> Vec<&StarWarsChar> { 153 | ch.friends 154 | .iter() 155 | .copied() 156 | .filter_map(|id| self.chars.get(id)) 157 | .collect() 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /models/starwars/src/model.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::needless_lifetimes)] 2 | 3 | use async_graphql::{ 4 | Context, Enum, Error, Interface, Object, OutputType, Result, 5 | connection::{Connection, Edge, query}, 6 | }; 7 | 8 | use super::StarWars; 9 | use crate::StarWarsChar; 10 | 11 | /// One of the films in the Star Wars Trilogy 12 | #[derive(Enum, Copy, Clone, Eq, PartialEq)] 13 | pub enum Episode { 14 | /// Released in 1977. 15 | NewHope, 16 | 17 | /// Released in 1980. 18 | Empire, 19 | 20 | /// Released in 1983. 21 | Jedi, 22 | } 23 | 24 | pub struct Human<'a>(&'a StarWarsChar); 25 | 26 | /// A humanoid creature in the Star Wars universe. 27 | #[Object] 28 | impl<'a> Human<'a> { 29 | /// The id of the human. 30 | async fn id(&self) -> &str { 31 | self.0.id 32 | } 33 | 34 | /// The name of the human. 35 | async fn name(&self) -> &str { 36 | self.0.name 37 | } 38 | 39 | /// The friends of the human, or an empty list if they have none. 40 | async fn friends<'ctx>(&self, ctx: &Context<'ctx>) -> Vec> { 41 | let star_wars = ctx.data_unchecked::(); 42 | star_wars 43 | .friends(self.0) 44 | .into_iter() 45 | .map(|ch| { 46 | if ch.is_human { 47 | Human(ch).into() 48 | } else { 49 | Droid(ch).into() 50 | } 51 | }) 52 | .collect() 53 | } 54 | 55 | /// Which movies they appear in. 56 | async fn appears_in(&self) -> &[Episode] { 57 | &self.0.appears_in 58 | } 59 | 60 | /// The home planet of the human, or null if unknown. 61 | async fn home_planet(&self) -> &Option<&str> { 62 | &self.0.home_planet 63 | } 64 | } 65 | 66 | pub struct Droid<'a>(&'a StarWarsChar); 67 | 68 | /// A mechanical creature in the Star Wars universe. 69 | #[Object] 70 | impl<'a> Droid<'a> { 71 | /// The id of the droid. 72 | async fn id(&self) -> &str { 73 | self.0.id 74 | } 75 | 76 | /// The name of the droid. 77 | async fn name(&self) -> &str { 78 | self.0.name 79 | } 80 | 81 | /// The friends of the droid, or an empty list if they have none. 82 | async fn friends<'ctx>(&self, ctx: &Context<'ctx>) -> Vec> { 83 | let star_wars = ctx.data_unchecked::(); 84 | star_wars 85 | .friends(self.0) 86 | .into_iter() 87 | .map(|ch| { 88 | if ch.is_human { 89 | Human(ch).into() 90 | } else { 91 | Droid(ch).into() 92 | } 93 | }) 94 | .collect() 95 | } 96 | 97 | /// Which movies they appear in. 98 | async fn appears_in(&self) -> &[Episode] { 99 | &self.0.appears_in 100 | } 101 | 102 | /// The primary function of the droid. 103 | async fn primary_function(&self) -> &Option<&str> { 104 | &self.0.primary_function 105 | } 106 | } 107 | 108 | pub struct QueryRoot; 109 | 110 | #[Object] 111 | impl QueryRoot { 112 | async fn hero<'a>( 113 | &self, 114 | ctx: &Context<'a>, 115 | #[graphql( 116 | desc = "If omitted, returns the hero of the whole saga. If provided, returns the hero of that particular episode." 117 | )] 118 | episode: Option, 119 | ) -> Character<'a> { 120 | let star_wars = ctx.data_unchecked::(); 121 | match episode { 122 | Some(episode) => { 123 | if episode == Episode::Empire { 124 | Human(star_wars.chars.get(star_wars.luke).unwrap()).into() 125 | } else { 126 | Droid(star_wars.chars.get(star_wars.artoo).unwrap()).into() 127 | } 128 | } 129 | None => Human(star_wars.chars.get(star_wars.luke).unwrap()).into(), 130 | } 131 | } 132 | 133 | async fn human<'a>( 134 | &self, 135 | ctx: &Context<'a>, 136 | #[graphql(desc = "id of the human")] id: String, 137 | ) -> Option> { 138 | ctx.data_unchecked::().human(&id).map(Human) 139 | } 140 | 141 | async fn humans<'a>( 142 | &self, 143 | ctx: &Context<'a>, 144 | after: Option, 145 | before: Option, 146 | first: Option, 147 | last: Option, 148 | ) -> Result>> { 149 | let humans = ctx.data_unchecked::().humans().to_vec(); 150 | query_characters(after, before, first, last, &humans, Human).await 151 | } 152 | 153 | async fn droid<'a>( 154 | &self, 155 | ctx: &Context<'a>, 156 | #[graphql(desc = "id of the droid")] id: String, 157 | ) -> Option> { 158 | ctx.data_unchecked::().droid(&id).map(Droid) 159 | } 160 | 161 | async fn droids<'a>( 162 | &self, 163 | ctx: &Context<'a>, 164 | after: Option, 165 | before: Option, 166 | first: Option, 167 | last: Option, 168 | ) -> Result>> { 169 | let droids = ctx.data_unchecked::().droids().to_vec(); 170 | query_characters(after, before, first, last, &droids, Droid).await 171 | } 172 | } 173 | 174 | #[derive(Interface)] 175 | #[allow(clippy::duplicated_attributes)] 176 | #[graphql( 177 | field(name = "id", ty = "&str"), 178 | field(name = "name", ty = "&str"), 179 | field(name = "friends", ty = "Vec>"), 180 | field(name = "appears_in", ty = "&[Episode]") 181 | )] 182 | pub enum Character<'a> { 183 | Human(Human<'a>), 184 | Droid(Droid<'a>), 185 | } 186 | 187 | async fn query_characters<'a, F, T>( 188 | after: Option, 189 | before: Option, 190 | first: Option, 191 | last: Option, 192 | characters: &[&'a StarWarsChar], 193 | map_to: F, 194 | ) -> Result> 195 | where 196 | F: Fn(&'a StarWarsChar) -> T, 197 | T: OutputType, 198 | { 199 | query( 200 | after, 201 | before, 202 | first, 203 | last, 204 | |after, before, first, last| async move { 205 | let mut start = 0usize; 206 | let mut end = characters.len(); 207 | 208 | if let Some(after) = after { 209 | if after >= characters.len() { 210 | return Ok(Connection::new(false, false)); 211 | } 212 | start = after + 1; 213 | } 214 | 215 | if let Some(before) = before { 216 | if before == 0 { 217 | return Ok(Connection::new(false, false)); 218 | } 219 | end = before; 220 | } 221 | 222 | let mut slice = &characters[start..end]; 223 | 224 | if let Some(first) = first { 225 | slice = &slice[..first.min(slice.len())]; 226 | end -= first.min(slice.len()); 227 | } else if let Some(last) = last { 228 | slice = &slice[slice.len() - last.min(slice.len())..]; 229 | start = end - last.min(slice.len()); 230 | } 231 | 232 | let mut connection = Connection::new(start > 0, end < characters.len()); 233 | connection.edges.extend( 234 | slice 235 | .iter() 236 | .enumerate() 237 | .map(|(idx, item)| Edge::new(start + idx, (map_to)(item))), 238 | ); 239 | Ok::<_, Error>(connection) 240 | }, 241 | ) 242 | .await 243 | } 244 | -------------------------------------------------------------------------------- /models/token/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "token" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | async-graphql = { path = "../../.." } 8 | futures-util = "0.3.30" 9 | serde_json = "1.0" 10 | serde = { version = "1.0", features = ["derive"] } 11 | -------------------------------------------------------------------------------- /models/token/src/lib.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::{Context, Data, EmptyMutation, Object, Result, Schema, Subscription}; 2 | use futures_util::Stream; 3 | use serde::Deserialize; 4 | 5 | pub type TokenSchema = Schema; 6 | 7 | pub struct Token(pub String); 8 | 9 | pub struct QueryRoot; 10 | 11 | #[Object] 12 | impl QueryRoot { 13 | async fn current_token<'a>(&self, ctx: &'a Context<'_>) -> Option<&'a str> { 14 | ctx.data_opt::().map(|token| token.0.as_str()) 15 | } 16 | } 17 | 18 | pub struct SubscriptionRoot; 19 | 20 | #[Subscription] 21 | impl SubscriptionRoot { 22 | async fn values(&self, ctx: &Context<'_>) -> Result> { 23 | if ctx.data::()?.0 != "123456" { 24 | return Err("Forbidden".into()); 25 | } 26 | Ok(futures_util::stream::once(async move { 10 })) 27 | } 28 | } 29 | 30 | // For more details see: 31 | // https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md#connectioninit 32 | pub async fn on_connection_init(value: serde_json::Value) -> Result { 33 | #[derive(Deserialize)] 34 | struct Payload { 35 | token: String, 36 | } 37 | 38 | // Coerce the connection params into our `Payload` struct so we can 39 | // validate the token exists in the headers. 40 | if let Ok(payload) = serde_json::from_value::(value) { 41 | let mut data = Data::default(); 42 | data.insert(Token(payload.token)); 43 | Ok(data) 44 | } else { 45 | Err("Token is required".into()) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /poem/dynamic-books/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "poem-dynamic-books" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | async-graphql = { path = "../../..", features = ["dynamic-schema"] } 8 | async-graphql-poem = { path = "../../../integrations/poem" } 9 | tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] } 10 | dynamic-books = { path = "../../models/dynamic-books" } 11 | poem = "3.0.0" 12 | -------------------------------------------------------------------------------- /poem/dynamic-books/src/main.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::http::GraphiQLSource; 2 | use async_graphql_poem::{GraphQL, GraphQLSubscription}; 3 | use poem::{IntoResponse, Route, Server, get, handler, listener::TcpListener, web::Html}; 4 | 5 | #[handler] 6 | async fn graphiql() -> impl IntoResponse { 7 | Html( 8 | GraphiQLSource::build() 9 | .endpoint("/") 10 | .subscription_endpoint("/ws") 11 | .finish(), 12 | ) 13 | } 14 | 15 | #[tokio::main] 16 | async fn main() { 17 | let schema = dynamic_books::schema().unwrap(); 18 | let app = Route::new() 19 | .at("/", get(graphiql).post(GraphQL::new(schema.clone()))) 20 | .at("/ws", get(GraphQLSubscription::new(schema))); 21 | 22 | println!("GraphiQL IDE: http://localhost:8080"); 23 | Server::new(TcpListener::bind("127.0.0.1:8080")) 24 | .run(app) 25 | .await 26 | .unwrap(); 27 | } 28 | -------------------------------------------------------------------------------- /poem/dynamic-schema/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "poem-dynamic-schema" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | async-graphql = { path = "../../..", features = ["dynamic-schema"] } 8 | async-graphql-poem = { path = "../../../integrations/poem" } 9 | tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] } 10 | poem = { version = "3.0.0" } 11 | -------------------------------------------------------------------------------- /poem/dynamic-schema/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | 3 | use async_graphql::{ 4 | Value, 5 | dynamic::*, 6 | http::{GraphQLPlaygroundConfig, playground_source}, 7 | }; 8 | use async_graphql_poem::GraphQL; 9 | use poem::{IntoResponse, Route, Server, get, handler, listener::TcpListener, web::Html}; 10 | 11 | #[handler] 12 | async fn graphql_playground() -> impl IntoResponse { 13 | Html(playground_source(GraphQLPlaygroundConfig::new("/"))) 14 | } 15 | 16 | #[tokio::main] 17 | async fn main() -> Result<(), Box> { 18 | let query = 19 | Object::new("Query").field(Field::new("value", TypeRef::named_nn(TypeRef::INT), |_| { 20 | FieldFuture::new(async { Ok(Some(Value::from(100))) }) 21 | })); 22 | let schema = Schema::build(query.type_name(), None, None) 23 | .register(query) 24 | .finish()?; 25 | 26 | let app = Route::new().at("/", get(graphql_playground).post(GraphQL::new(schema))); 27 | 28 | println!("Playground: http://localhost:8000"); 29 | Server::new(TcpListener::bind("127.0.0.1:8000")) 30 | .run(app) 31 | .await?; 32 | Ok(()) 33 | } 34 | -------------------------------------------------------------------------------- /poem/dynamic-starwars/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "poem-dynamic-starwars" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | async-graphql = { path = "../../..", features = ["dynamic-schema"] } 8 | async-graphql-poem = { path = "../../../integrations/poem" } 9 | tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] } 10 | dynamic-starwars = { path = "../../models/dynamic-starwars" } 11 | poem = "3.0.0" 12 | -------------------------------------------------------------------------------- /poem/dynamic-starwars/src/main.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::http::GraphiQLSource; 2 | use async_graphql_poem::GraphQL; 3 | use poem::{IntoResponse, Route, Server, get, handler, listener::TcpListener, web::Html}; 4 | 5 | #[handler] 6 | async fn graphiql() -> impl IntoResponse { 7 | Html(GraphiQLSource::build().endpoint("/").finish()) 8 | } 9 | 10 | #[tokio::main] 11 | async fn main() { 12 | let app = Route::new().at( 13 | "/", 14 | get(graphiql).post(GraphQL::new(dynamic_starwars::schema().unwrap())), 15 | ); 16 | 17 | println!("GraphiQL IDE: http://localhost:8000"); 18 | Server::new(TcpListener::bind("127.0.0.1:8000")) 19 | .run(app) 20 | .await 21 | .unwrap(); 22 | } 23 | -------------------------------------------------------------------------------- /poem/dynamic-upload/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dynamic-upload" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | async-graphql = { path = "../../..", features = ["dynamic-schema"] } 8 | async-graphql-poem = { path = "../../../integrations/poem" } 9 | tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] } 10 | dynamic-files = { path = "../../models/dynamic-files" } 11 | poem = "3.0.0" 12 | -------------------------------------------------------------------------------- /poem/dynamic-upload/src/main.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::http::GraphiQLSource; 2 | use async_graphql_poem::GraphQL; 3 | use poem::{IntoResponse, Route, Server, get, handler, listener::TcpListener, web::Html}; 4 | 5 | #[handler] 6 | async fn graphiql() -> impl IntoResponse { 7 | Html(GraphiQLSource::build().endpoint("/").finish()) 8 | } 9 | 10 | #[tokio::main] 11 | async fn main() { 12 | let app = Route::new().at( 13 | "/", 14 | get(graphiql).post(GraphQL::new(dynamic_files::schema().unwrap())), 15 | ); 16 | 17 | println!("GraphiQL IDE: http://localhost:8000"); 18 | Server::new(TcpListener::bind("0.0.0.0:8000")) 19 | .run(app) 20 | .await 21 | .unwrap(); 22 | } 23 | -------------------------------------------------------------------------------- /poem/opentelemetry-basic/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "poem-opentelemetry-basic" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | async-graphql = { path = "../../..", features = ["opentelemetry"] } 10 | async-graphql-poem = { path = "../../../integrations/poem" } 11 | tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] } 12 | poem = "3.0.0" 13 | opentelemetry = { version = "0.27.0" } 14 | opentelemetry_sdk = { version = "0.27", features = ["rt-tokio"] } 15 | opentelemetry-stdout = { version = "0.27.0", features = ["trace"] } 16 | -------------------------------------------------------------------------------- /poem/opentelemetry-basic/src/main.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::{ 2 | EmptyMutation, EmptySubscription, Object, Result, Schema, extensions::OpenTelemetry, 3 | }; 4 | use async_graphql_poem::GraphQL; 5 | use opentelemetry::trace::TracerProvider as _; 6 | use opentelemetry_sdk::trace::TracerProvider; 7 | use poem::{EndpointExt, Route, Server, listener::TcpListener, post}; 8 | 9 | struct QueryRoot; 10 | 11 | #[Object] 12 | impl QueryRoot { 13 | async fn hello(&self) -> Result { 14 | Ok("World".to_string()) 15 | } 16 | } 17 | 18 | #[tokio::main] 19 | async fn main() { 20 | let provider = TracerProvider::builder() 21 | .with_simple_exporter(opentelemetry_stdout::SpanExporter::default()) 22 | .build(); 23 | let tracer = provider.tracer("poem-opentelemetry-basic"); 24 | let opentelemetry_extension = OpenTelemetry::new(tracer); 25 | 26 | let schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription) 27 | .extension(opentelemetry_extension) 28 | .finish(); 29 | 30 | let app = Route::new() 31 | .at("/", post(GraphQL::new(schema.clone()))) 32 | .data(schema); 33 | 34 | let example_curl = "\ 35 | curl '127.0.0.1:8000' \ 36 | -X POST \ 37 | -H 'content-type: application/json' \ 38 | --data '{ \"query\": \"{ hello }\" }'"; 39 | 40 | println!( 41 | "Run this curl command from another terminal window to see opentelemetry output in this terminal.\n\n{example_curl}\n\n" 42 | ); 43 | 44 | Server::new(TcpListener::bind("127.0.0.1:8000")) 45 | .run(app) 46 | .await 47 | .unwrap(); 48 | } 49 | -------------------------------------------------------------------------------- /poem/starwars/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "poem-starwars" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | async-graphql = { path = "../../.." } 8 | async-graphql-poem = { path = "../../../integrations/poem" } 9 | tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] } 10 | starwars = { path = "../../models/starwars" } 11 | poem = "3.0.0" 12 | -------------------------------------------------------------------------------- /poem/starwars/src/main.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::{EmptyMutation, EmptySubscription, Schema, http::GraphiQLSource}; 2 | use async_graphql_poem::GraphQL; 3 | use poem::{IntoResponse, Route, Server, get, handler, listener::TcpListener, web::Html}; 4 | use starwars::{QueryRoot, StarWars}; 5 | 6 | #[handler] 7 | async fn graphiql() -> impl IntoResponse { 8 | Html(GraphiQLSource::build().endpoint("/").finish()) 9 | } 10 | 11 | #[tokio::main] 12 | async fn main() { 13 | let schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription) 14 | .data(StarWars::new()) 15 | .finish(); 16 | 17 | let app = Route::new().at("/", get(graphiql).post(GraphQL::new(schema))); 18 | 19 | println!("GraphiQL IDE: http://localhost:8000"); 20 | Server::new(TcpListener::bind("127.0.0.1:8000")) 21 | .run(app) 22 | .await 23 | .unwrap(); 24 | } 25 | -------------------------------------------------------------------------------- /poem/subscription-redis/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "subscription-redis" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | async-graphql = { path = "../../.." } 8 | async-graphql-poem = { path = "../../../integrations/poem" } 9 | tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] } 10 | poem = { version = "3.0.0", features = ["websocket"] } 11 | redis = { version = "0.27.5", features = ["aio", "tokio-comp"] } 12 | futures-util = "0.3.30" 13 | -------------------------------------------------------------------------------- /poem/subscription-redis/src/main.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::{Context, Object, Result, Schema, Subscription, http::GraphiQLSource}; 2 | use async_graphql_poem::{GraphQL, GraphQLSubscription}; 3 | use futures_util::{Stream, StreamExt}; 4 | use poem::{IntoResponse, Route, Server, get, handler, listener::TcpListener, web::Html}; 5 | use redis::{AsyncCommands, Client}; 6 | 7 | struct QueryRoot; 8 | 9 | #[Object] 10 | impl QueryRoot { 11 | async fn version(&self) -> &'static str { 12 | std::env!("CARGO_PKG_VERSION") 13 | } 14 | } 15 | 16 | struct MutationRoot; 17 | 18 | #[Object] 19 | impl MutationRoot { 20 | async fn publish(&self, ctx: &Context<'_>, value: String) -> Result { 21 | let client = ctx.data_unchecked::(); 22 | let mut conn = client.get_multiplexed_async_connection().await?; 23 | conn.publish::<_, _, ()>("values", value).await?; 24 | Ok(true) 25 | } 26 | } 27 | 28 | struct SubscriptionRoot; 29 | 30 | #[Subscription] 31 | impl SubscriptionRoot { 32 | async fn values(&self, ctx: &Context<'_>) -> Result> { 33 | let client = ctx.data_unchecked::(); 34 | let mut conn = client.get_async_pubsub().await?; 35 | conn.subscribe("values").await?; 36 | Ok(conn 37 | .into_on_message() 38 | .filter_map(|msg| async move { msg.get_payload().ok() })) 39 | } 40 | } 41 | 42 | #[handler] 43 | async fn graphiql() -> impl IntoResponse { 44 | Html( 45 | GraphiQLSource::build() 46 | .endpoint("/") 47 | .subscription_endpoint("/ws") 48 | .finish(), 49 | ) 50 | } 51 | 52 | #[tokio::main] 53 | async fn main() { 54 | let client = Client::open("redis://127.0.0.1/").unwrap(); 55 | 56 | let schema = Schema::build(QueryRoot, MutationRoot, SubscriptionRoot) 57 | .data(client) 58 | .finish(); 59 | 60 | let app = Route::new() 61 | .at("/", get(graphiql).post(GraphQL::new(schema.clone()))) 62 | .at("/ws", get(GraphQLSubscription::new(schema))); 63 | 64 | println!("GraphiQL IDE: http://localhost:8000"); 65 | Server::new(TcpListener::bind("127.0.0.1:8000")) 66 | .run(app) 67 | .await 68 | .unwrap(); 69 | } 70 | -------------------------------------------------------------------------------- /poem/subscription/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "poem-subscription" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | async-graphql = { path = "../../.." } 8 | async-graphql-poem = { path = "../../../integrations/poem" } 9 | tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] } 10 | books = { path = "../../models/books" } 11 | poem = { version = "3.0.0", features = ["websocket"] } 12 | -------------------------------------------------------------------------------- /poem/subscription/src/main.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::{Schema, http::GraphiQLSource}; 2 | use async_graphql_poem::{GraphQL, GraphQLSubscription}; 3 | use books::{MutationRoot, QueryRoot, Storage, SubscriptionRoot}; 4 | use poem::{IntoResponse, Route, Server, get, handler, listener::TcpListener, web::Html}; 5 | 6 | #[handler] 7 | async fn graphiql() -> impl IntoResponse { 8 | Html( 9 | GraphiQLSource::build() 10 | .endpoint("/") 11 | .subscription_endpoint("/ws") 12 | .finish(), 13 | ) 14 | } 15 | 16 | #[tokio::main] 17 | async fn main() { 18 | let schema = Schema::build(QueryRoot, MutationRoot, SubscriptionRoot) 19 | .data(Storage::default()) 20 | .finish(); 21 | 22 | let app = Route::new() 23 | .at("/", get(graphiql).post(GraphQL::new(schema.clone()))) 24 | .at("/ws", get(GraphQLSubscription::new(schema))); 25 | 26 | println!("GraphiQL IDE: http://localhost:8000"); 27 | Server::new(TcpListener::bind("127.0.0.1:8000")) 28 | .run(app) 29 | .await 30 | .unwrap(); 31 | } 32 | -------------------------------------------------------------------------------- /poem/token-from-header/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "poem-token-from-header" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | async-graphql = { path = "../../.." } 8 | async-graphql-poem = { path = "../../../integrations/poem" } 9 | token = { path = "../../models/token" } 10 | poem = { version = "3.0.0", features = ["websocket"] } 11 | tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] } 12 | -------------------------------------------------------------------------------- /poem/token-from-header/src/main.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::{ 2 | EmptyMutation, Schema, 3 | http::{ALL_WEBSOCKET_PROTOCOLS, GraphiQLSource}, 4 | }; 5 | use async_graphql_poem::{GraphQLProtocol, GraphQLRequest, GraphQLResponse, GraphQLWebSocket}; 6 | use poem::{ 7 | EndpointExt, IntoResponse, Route, Server, get, handler, 8 | http::HeaderMap, 9 | listener::TcpListener, 10 | web::{Data, Html, websocket::WebSocket}, 11 | }; 12 | use token::{QueryRoot, SubscriptionRoot, Token, TokenSchema, on_connection_init}; 13 | 14 | fn get_token_from_headers(headers: &HeaderMap) -> Option { 15 | headers 16 | .get("Token") 17 | .and_then(|value| value.to_str().map(|s| Token(s.to_string())).ok()) 18 | } 19 | 20 | #[handler] 21 | async fn graphiql() -> impl IntoResponse { 22 | Html( 23 | GraphiQLSource::build() 24 | .endpoint("/") 25 | .subscription_endpoint("/ws") 26 | .finish(), 27 | ) 28 | } 29 | 30 | #[handler] 31 | async fn index( 32 | schema: Data<&TokenSchema>, 33 | headers: &HeaderMap, 34 | req: GraphQLRequest, 35 | ) -> GraphQLResponse { 36 | let mut req = req.0; 37 | if let Some(token) = get_token_from_headers(headers) { 38 | req = req.data(token); 39 | } 40 | schema.execute(req).await.into() 41 | } 42 | 43 | #[handler] 44 | async fn ws( 45 | schema: Data<&TokenSchema>, 46 | protocol: GraphQLProtocol, 47 | websocket: WebSocket, 48 | ) -> impl IntoResponse { 49 | let schema = schema.0.clone(); 50 | websocket 51 | .protocols(ALL_WEBSOCKET_PROTOCOLS) 52 | .on_upgrade(move |stream| { 53 | GraphQLWebSocket::new(stream, schema, protocol) 54 | // connection params are used to extract the token in this fn 55 | .on_connection_init(on_connection_init) 56 | .serve() 57 | }) 58 | } 59 | 60 | #[tokio::main] 61 | async fn main() { 62 | let schema = Schema::new(QueryRoot, EmptyMutation, SubscriptionRoot); 63 | 64 | let app = Route::new() 65 | .at("/", get(graphiql).post(index)) 66 | .at("/ws", get(ws)) 67 | .data(schema); 68 | 69 | println!("GraphiQL IDE: http://localhost:8000"); 70 | Server::new(TcpListener::bind("127.0.0.1:8000")) 71 | .run(app) 72 | .await 73 | .unwrap(); 74 | } 75 | -------------------------------------------------------------------------------- /poem/upload/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "poem-upload" 3 | version = "0.1.1" 4 | authors = ["sunli "] 5 | edition = "2024" 6 | 7 | [dependencies] 8 | async-graphql = { path = "../../.." } 9 | async-graphql-poem = { path = "../../../integrations/poem" } 10 | poem = { version = "3.0.0", features = ["websocket"] } 11 | files = { path = "../../models/files" } 12 | tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] } 13 | -------------------------------------------------------------------------------- /poem/upload/src/main.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::{EmptySubscription, Schema, http::GraphiQLSource}; 2 | use async_graphql_poem::{GraphQLRequest, GraphQLResponse}; 3 | use files::{FilesSchema, MutationRoot, QueryRoot, Storage}; 4 | use poem::{ 5 | EndpointExt, IntoResponse, Route, Server, get, handler, 6 | listener::TcpListener, 7 | middleware::Cors, 8 | web::{Data, Html}, 9 | }; 10 | 11 | #[handler] 12 | async fn index(schema: Data<&FilesSchema>, req: GraphQLRequest) -> GraphQLResponse { 13 | schema.execute(req.0).await.into() 14 | } 15 | 16 | #[handler] 17 | async fn graphiql() -> impl IntoResponse { 18 | Html(GraphiQLSource::build().endpoint("/").finish()) 19 | } 20 | 21 | #[tokio::main] 22 | async fn main() -> std::io::Result<()> { 23 | let schema = Schema::build(QueryRoot, MutationRoot, EmptySubscription) 24 | .data(Storage::default()) 25 | .finish(); 26 | 27 | println!("GraphiQL IDE: http://localhost:8000"); 28 | 29 | let app = Route::new() 30 | .at("/", get(graphiql).post(index)) 31 | .with(Cors::new()) 32 | .data(schema); 33 | Server::new(TcpListener::bind("127.0.0.1:8000")) 34 | .run(app) 35 | .await 36 | } 37 | -------------------------------------------------------------------------------- /rocket/starwars/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rocket-starwars" 3 | version = "0.1.1" 4 | authors = ["Daniel Wiesenberg "] 5 | edition = "2024" 6 | 7 | [dependencies] 8 | async-graphql = { path = "../../.." } 9 | async-graphql-rocket = { path = "../../../integrations/rocket" } 10 | rocket = { version = "0.5.0", default-features = false } 11 | starwars = { path = "../../models/starwars" } 12 | -------------------------------------------------------------------------------- /rocket/starwars/Rocket.toml: -------------------------------------------------------------------------------- 1 | # For more information take a look at https://rocket.rs/guide/configuration/ 2 | [global.limits] 3 | graphql = 131072 4 | 5 | [debug] 6 | address = "127.0.0.1" 7 | port = 8000 8 | keep_alive = 5 9 | log_level = "normal" 10 | 11 | 12 | [staging] 13 | address = "0.0.0.0" 14 | port = 8080 15 | keep_alive = 5 16 | log_level = "normal" 17 | 18 | 19 | [release] 20 | address = "0.0.0.0" 21 | port = 8080 22 | keep_alive = 5 23 | log_level = "critical" 24 | -------------------------------------------------------------------------------- /rocket/starwars/src/main.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::{EmptyMutation, EmptySubscription, Schema, http::GraphiQLSource}; 2 | use async_graphql_rocket::{GraphQLQuery, GraphQLRequest, GraphQLResponse}; 3 | use rocket::{State, response::content, routes}; 4 | use starwars::{QueryRoot, StarWars}; 5 | 6 | pub type StarWarsSchema = Schema; 7 | 8 | #[rocket::get("/")] 9 | fn graphiql() -> content::RawHtml { 10 | content::RawHtml(GraphiQLSource::build().endpoint("/graphql").finish()) 11 | } 12 | 13 | #[rocket::get("/graphql?")] 14 | async fn graphql_query(schema: &State, query: GraphQLQuery) -> GraphQLResponse { 15 | query.execute(schema.inner()).await 16 | } 17 | 18 | #[rocket::post("/graphql", data = "", format = "application/json")] 19 | async fn graphql_request( 20 | schema: &State, 21 | request: GraphQLRequest, 22 | ) -> GraphQLResponse { 23 | request.execute(schema.inner()).await 24 | } 25 | 26 | #[rocket::launch] 27 | fn rocket() -> _ { 28 | let schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription) 29 | .data(StarWars::new()) 30 | .finish(); 31 | 32 | rocket::build() 33 | .manage(schema) 34 | .mount("/", routes![graphql_query, graphql_request, graphiql]) 35 | } 36 | -------------------------------------------------------------------------------- /rocket/upload/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rocket-upload" 3 | version = "0.1.1" 4 | authors = ["Daniel Wiesenberg "] 5 | edition = "2024" 6 | 7 | [dependencies] 8 | async-graphql = { path = "../../.." } 9 | async-graphql-rocket = { path = "../../../integrations/rocket" } 10 | rocket = { version = "0.5.0", default-features = false } 11 | files = { path = "../../models/files" } 12 | -------------------------------------------------------------------------------- /rocket/upload/Rocket.toml: -------------------------------------------------------------------------------- 1 | # For more information take a look at https://rocket.rs/guide/configuration/ 2 | [global.limits] 3 | graphql = 131072 4 | 5 | [debug] 6 | address = "127.0.0.1" 7 | port = 8000 8 | keep_alive = 5 9 | log_level = "normal" 10 | 11 | 12 | [staging] 13 | address = "0.0.0.0" 14 | port = 8080 15 | keep_alive = 5 16 | log_level = "normal" 17 | 18 | 19 | [release] 20 | address = "0.0.0.0" 21 | port = 8080 22 | keep_alive = 5 23 | log_level = "critical" 24 | -------------------------------------------------------------------------------- /rocket/upload/src/main.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::{EmptyMutation, EmptySubscription, Schema, http::GraphiQLSource}; 2 | use async_graphql_rocket::{GraphQLQuery, GraphQLRequest, GraphQLResponse}; 3 | use files::{FilesSchema, MutationRoot, QueryRoot, Storage}; 4 | use rocket::{State, response::content, routes}; 5 | 6 | pub type StarWarsSchema = Schema; 7 | 8 | #[rocket::get("/")] 9 | fn graphiql() -> content::RawHtml { 10 | content::RawHtml(GraphiQLSource::build().endpoint("/graphql").finish()) 11 | } 12 | 13 | #[rocket::get("/graphql?")] 14 | async fn graphql_query(schema: &State, query: GraphQLQuery) -> GraphQLResponse { 15 | query.execute(schema.inner()).await 16 | } 17 | 18 | #[rocket::post("/graphql", data = "", format = "application/json", rank = 1)] 19 | async fn graphql_request(schema: &State, request: GraphQLRequest) -> GraphQLResponse { 20 | request.execute(schema.inner()).await 21 | } 22 | 23 | #[rocket::post( 24 | "/graphql", 25 | data = "", 26 | format = "multipart/form-data", 27 | rank = 2 28 | )] 29 | async fn graphql_request_multipart( 30 | schema: &State, 31 | request: GraphQLRequest, 32 | ) -> GraphQLResponse { 33 | request.execute(schema.inner()).await 34 | } 35 | 36 | #[rocket::launch] 37 | fn rocket() -> _ { 38 | let schema = Schema::build(QueryRoot, MutationRoot, EmptySubscription) 39 | .data(Storage::default()) 40 | .finish(); 41 | 42 | rocket::build().manage(schema).mount( 43 | "/", 44 | routes![ 45 | graphql_query, 46 | graphql_request, 47 | graphql_request_multipart, 48 | graphiql 49 | ], 50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /tide/dataloader-postgres/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dataloader-postgres" 3 | version = "0.1.0" 4 | authors = ["ejez "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | async-graphql = { path = "../../..", features = ["dataloader"] } 9 | async-graphql-tide = { path = "../../../integrations/tide" } 10 | async-std = "1.12.0" 11 | itertools = "0.12.1" 12 | sqlx = { version = "0.7.4", features = [ 13 | "runtime-async-std-rustls", 14 | "postgres", 15 | ] } 16 | tide = "0.16.0" 17 | 18 | [dev-dependencies] 19 | serde_json = "1.0.115" 20 | surf = "2.3.2" 21 | -------------------------------------------------------------------------------- /tide/dataloader-postgres/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, env}; 2 | 3 | use async_graphql::{ 4 | dataloader::{DataLoader, Loader}, 5 | futures_util::TryStreamExt, 6 | http::GraphiQLSource, 7 | Context, EmptyMutation, EmptySubscription, FieldError, Object, Result, Schema, SimpleObject, 8 | }; 9 | use async_std::task; 10 | use sqlx::PgPool; 11 | use tide::{http::mime, Body, Response, StatusCode}; 12 | 13 | #[derive(sqlx::FromRow, Clone, SimpleObject)] 14 | pub struct Book { 15 | id: i32, 16 | name: String, 17 | author: String, 18 | } 19 | 20 | pub struct BookLoader(PgPool); 21 | 22 | impl BookLoader { 23 | fn new(postgres_pool: PgPool) -> Self { 24 | Self(postgres_pool) 25 | } 26 | } 27 | 28 | impl Loader for BookLoader { 29 | type Value = Book; 30 | type Error = FieldError; 31 | 32 | async fn load(&self, keys: &[i32]) -> Result, Self::Error> { 33 | println!("load book by batch {:?}", keys); 34 | 35 | if keys.contains(&9) { 36 | return Err("MOCK DBError".into()); 37 | } 38 | 39 | Ok( 40 | sqlx::query_as("SELECT id, name, author FROM books WHERE id = ANY($1)") 41 | .bind(keys) 42 | .fetch(&self.0) 43 | .map_ok(|book: Book| (book.id, book)) 44 | .try_collect() 45 | .await?, 46 | ) 47 | } 48 | } 49 | 50 | struct QueryRoot; 51 | 52 | #[Object] 53 | impl QueryRoot { 54 | async fn book(&self, ctx: &Context<'_>, id: i32) -> Result> { 55 | println!("pre load book by id {:?}", id); 56 | ctx.data_unchecked::>() 57 | .load_one(id) 58 | .await 59 | } 60 | } 61 | 62 | fn main() -> Result<()> { 63 | task::block_on(run()) 64 | } 65 | 66 | async fn run() -> Result<()> { 67 | let postgres_pool = PgPool::connect(&env::var("DATABASE_URL")?).await?; 68 | 69 | sqlx::query( 70 | r#" 71 | CREATE TABLE IF NOT EXISTS books ( 72 | id INTEGER PRIMARY KEY NOT NULL, 73 | name TEXT NOT NULL, 74 | author TEXT NOT NULL 75 | ); 76 | "#, 77 | ) 78 | .execute(&postgres_pool) 79 | .await?; 80 | 81 | sqlx::query( 82 | r#" 83 | INSERT INTO books (id, name, author) 84 | VALUES (1, 'name1', 'author1'), (2, 'name2', 'author2'), (3, 'name3', 'author3') 85 | ON CONFLICT (id) DO NOTHING 86 | ; 87 | "#, 88 | ) 89 | .execute(&postgres_pool) 90 | .await?; 91 | 92 | let schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription) 93 | .data(DataLoader::new( 94 | BookLoader::new(postgres_pool), 95 | async_std::task::spawn, 96 | )) 97 | .finish(); 98 | 99 | let mut app = tide::new(); 100 | 101 | app.at("/graphql").post(async_graphql_tide::graphql(schema)); 102 | app.at("/").get(|_| async move { 103 | let mut resp = Response::new(StatusCode::Ok); 104 | resp.set_body(Body::from_string( 105 | GraphiQLSource::build().endpoint("/graphql").finish(), 106 | )); 107 | resp.set_content_type(mime::HTML); 108 | Ok(resp) 109 | }); 110 | 111 | println!("GraphiQL IDE: http://127.0.0.1:8000"); 112 | app.listen("127.0.0.1:8000").await?; 113 | 114 | Ok(()) 115 | } 116 | 117 | #[cfg(test)] 118 | mod tests { 119 | use std::time::Duration; 120 | 121 | use async_std::prelude::*; 122 | use serde_json::{json, Value}; 123 | 124 | use super::*; 125 | 126 | #[test] 127 | fn sample() -> Result<()> { 128 | task::block_on(async { 129 | let server: task::JoinHandle> = task::spawn(async move { 130 | run().await?; 131 | Ok(()) 132 | }); 133 | 134 | let client: task::JoinHandle> = task::spawn(async move { 135 | task::sleep(Duration::from_millis(1000)).await; 136 | 137 | // 138 | let string = surf::post("http://127.0.0.1:8000/graphql") 139 | .body( 140 | Body::from(r#"{"query":"{ book1: book(id: 1) {id, name, author} book2: book(id: 2) {id, name, author} book3: book(id: 3) {id, name, author} book4: book(id: 4) {id, name, author} }"}"#), 141 | ) 142 | .header("Content-Type", "application/json") 143 | .recv_string() 144 | .await?; 145 | println!("{}", string); 146 | 147 | let v: Value = serde_json::from_str(&string)?; 148 | assert_eq!( 149 | v["data"]["book1"], 150 | json!({"id": 1, "name": "name1", "author": "author1"}) 151 | ); 152 | assert_eq!( 153 | v["data"]["book2"], 154 | json!({"id": 2, "name": "name2", "author": "author2"}) 155 | ); 156 | assert_eq!( 157 | v["data"]["book3"], 158 | json!({"id": 3, "name": "name3", "author": "author3"}) 159 | ); 160 | assert_eq!(v["data"]["book4"], json!(null)); 161 | 162 | // 163 | let string = surf::post( "http://127.0.0.1:8000/graphql") 164 | .body( 165 | Body::from(r#"{"query":"{ book1: book(id: 1) {id, name, author} book4: book(id: 4) {id, name, author} book9: book(id: 9) {id, name, author} }"}"#), 166 | ) 167 | .header("Content-Type", "application/json") 168 | .recv_string() 169 | .await?; 170 | println!("{}", string); 171 | 172 | let v: Value = serde_json::from_str(&string)?; 173 | let error = v["errors"].as_array().unwrap()[0].clone(); 174 | assert_eq!(error["message"], json!("MOCK DBError")); 175 | 176 | Ok(()) 177 | }); 178 | 179 | server.race(client).await?; 180 | 181 | Ok(()) 182 | }) 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /tide/dataloader/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tide-dataloader" 3 | version = "0.1.1" 4 | authors = ["vkill "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | async-graphql = { path = "../../..", features = ["dataloader"] } 9 | async-graphql-tide = { path = "../../../integrations/tide" } 10 | tide = "0.16" 11 | async-std = "1.12.0" 12 | sqlx = { version = "0.7.4", features = ["sqlite", "runtime-async-std-rustls"] } 13 | itertools = "0.12.1" 14 | 15 | [dev-dependencies] 16 | serde_json = "1.0.115" 17 | surf = "2.3.2" 18 | -------------------------------------------------------------------------------- /tide/dataloader/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use async_graphql::{ 4 | dataloader::{DataLoader, Loader}, 5 | futures_util::TryStreamExt, 6 | http::GraphiQLSource, 7 | Context, EmptyMutation, EmptySubscription, FieldError, Object, Result, Schema, SimpleObject, 8 | }; 9 | use async_std::task; 10 | use itertools::Itertools; 11 | use sqlx::{Pool, Sqlite}; 12 | use tide::{http::mime, Body, Response, StatusCode}; 13 | 14 | #[derive(sqlx::FromRow, Clone, SimpleObject)] 15 | pub struct Book { 16 | id: i32, 17 | name: String, 18 | author: String, 19 | } 20 | 21 | pub struct BookLoader(Pool); 22 | 23 | impl BookLoader { 24 | fn new(sqlite_pool: Pool) -> Self { 25 | Self(sqlite_pool) 26 | } 27 | } 28 | 29 | impl Loader for BookLoader { 30 | type Value = Book; 31 | type Error = FieldError; 32 | 33 | async fn load(&self, keys: &[i32]) -> Result, Self::Error> { 34 | println!("load book by batch {:?}", keys); 35 | 36 | if keys.contains(&9) { 37 | return Err("MOCK DBError".into()); 38 | } 39 | 40 | let query = format!( 41 | "SELECT id, name, author FROM books WHERE id IN ({})", 42 | keys.iter().join(",") 43 | ); 44 | Ok(sqlx::query_as(&query) 45 | .fetch(&self.0) 46 | .map_ok(|book: Book| (book.id, book)) 47 | .try_collect() 48 | .await?) 49 | } 50 | } 51 | 52 | struct QueryRoot; 53 | 54 | #[Object] 55 | impl QueryRoot { 56 | async fn book(&self, ctx: &Context<'_>, id: i32) -> Result> { 57 | println!("pre load book by id {:?}", id); 58 | ctx.data_unchecked::>() 59 | .load_one(id) 60 | .await 61 | } 62 | } 63 | 64 | fn main() -> Result<()> { 65 | task::block_on(run()) 66 | } 67 | 68 | async fn run() -> Result<()> { 69 | let sqlite_pool: Pool = Pool::connect("sqlite::memory:").await?; 70 | 71 | sqlx::query( 72 | r#" 73 | CREATE TABLE IF NOT EXISTS books ( 74 | id INTEGER PRIMARY KEY NOT NULL, 75 | name TEXT NOT NULL, 76 | author TEXT NOT NULL 77 | ); 78 | "#, 79 | ) 80 | .execute(&sqlite_pool) 81 | .await?; 82 | 83 | sqlx::query( 84 | r#" 85 | INSERT OR IGNORE INTO books (id, name, author) 86 | VALUES (1, 'name1', 'author1'), (2, 'name2', 'author2'), (3, 'name3', 'author3') 87 | ; 88 | "#, 89 | ) 90 | .execute(&sqlite_pool) 91 | .await?; 92 | 93 | let schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription) 94 | .data(DataLoader::new( 95 | BookLoader::new(sqlite_pool), 96 | async_std::task::spawn, 97 | )) 98 | .finish(); 99 | 100 | let mut app = tide::new(); 101 | 102 | app.at("/graphql").post(async_graphql_tide::graphql(schema)); 103 | app.at("/").get(|_| async move { 104 | let mut resp = Response::new(StatusCode::Ok); 105 | resp.set_body(Body::from_string( 106 | GraphiQLSource::build().endpoint("/graphql").finish(), 107 | )); 108 | resp.set_content_type(mime::HTML); 109 | Ok(resp) 110 | }); 111 | 112 | println!("GraphiQL IDE: http://127.0.0.1:8000"); 113 | app.listen("127.0.0.1:8000").await?; 114 | 115 | Ok(()) 116 | } 117 | 118 | #[cfg(test)] 119 | mod tests { 120 | use std::time::Duration; 121 | 122 | use async_std::prelude::*; 123 | use serde_json::{json, Value}; 124 | 125 | use super::*; 126 | 127 | #[test] 128 | fn sample() -> Result<()> { 129 | task::block_on(async { 130 | let server: task::JoinHandle> = task::spawn(async move { 131 | run().await?; 132 | Ok(()) 133 | }); 134 | 135 | let client: task::JoinHandle> = task::spawn(async move { 136 | task::sleep(Duration::from_millis(1000)).await; 137 | 138 | // 139 | let string = surf::post("http://127.0.0.1:8000/graphql") 140 | .body( 141 | Body::from(r#"{"query":"{ book1: book(id: 1) {id, name, author} book2: book(id: 2) {id, name, author} book3: book(id: 3) {id, name, author} book4: book(id: 4) {id, name, author} }"}"#), 142 | ) 143 | .header("Content-Type", "application/json") 144 | .recv_string() 145 | .await?; 146 | println!("{}", string); 147 | 148 | let v: Value = serde_json::from_str(&string)?; 149 | assert_eq!( 150 | v["data"]["book1"], 151 | json!({"id": 1, "name": "name1", "author": "author1"}) 152 | ); 153 | assert_eq!( 154 | v["data"]["book2"], 155 | json!({"id": 2, "name": "name2", "author": "author2"}) 156 | ); 157 | assert_eq!( 158 | v["data"]["book3"], 159 | json!({"id": 3, "name": "name3", "author": "author3"}) 160 | ); 161 | assert_eq!(v["data"]["book4"], json!(null)); 162 | 163 | // 164 | let string = surf::post( "http://127.0.0.1:8000/graphql") 165 | .body( 166 | Body::from(r#"{"query":"{ book1: book(id: 1) {id, name, author} book4: book(id: 4) {id, name, author} book9: book(id: 9) {id, name, author} }"}"#), 167 | ) 168 | .header("Content-Type", "application/json") 169 | .recv_string() 170 | .await?; 171 | println!("{}", string); 172 | 173 | let v: Value = serde_json::from_str(&string)?; 174 | let error = v["errors"].as_array().unwrap()[0].clone(); 175 | assert_eq!(error["message"], json!("MOCK DBError")); 176 | 177 | Ok(()) 178 | }); 179 | 180 | server.race(client).await?; 181 | 182 | Ok(()) 183 | }) 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /tide/starwars/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tide-starwars" 3 | version = "0.1.1" 4 | authors = ["vkill "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | async-graphql = { path = "../../.." } 9 | async-graphql-tide = { path = "../../../integrations/tide" } 10 | tide = "0.16" 11 | async-std = "1.12.0" 12 | starwars = { path = "../../models/starwars" } 13 | 14 | [dev-dependencies] 15 | serde_json = "1.0.115" 16 | surf = "2.3.2" 17 | -------------------------------------------------------------------------------- /tide/starwars/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | use async_graphql::{http::GraphiQLSource, EmptyMutation, EmptySubscription, Schema}; 4 | use async_std::task; 5 | use starwars::{QueryRoot, StarWars}; 6 | use tide::{http::mime, Body, Response, StatusCode}; 7 | 8 | type Result = std::result::Result>; 9 | 10 | fn main() -> Result<()> { 11 | task::block_on(run()) 12 | } 13 | 14 | async fn run() -> Result<()> { 15 | let listen_addr = env::var("LISTEN_ADDR").unwrap_or_else(|_| "localhost:8000".to_owned()); 16 | 17 | let schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription) 18 | .data(StarWars::new()) 19 | .finish(); 20 | 21 | println!("GraphiQL IDE: http://{}", listen_addr); 22 | 23 | let mut app = tide::new(); 24 | 25 | app.at("/graphql").post(async_graphql_tide::graphql(schema)); 26 | 27 | app.at("/").get(|_| async move { 28 | let mut resp = Response::new(StatusCode::Ok); 29 | resp.set_body(Body::from_string( 30 | GraphiQLSource::build().endpoint("/graphql").finish(), 31 | )); 32 | resp.set_content_type(mime::HTML); 33 | Ok(resp) 34 | }); 35 | 36 | app.listen(listen_addr).await?; 37 | 38 | Ok(()) 39 | } 40 | 41 | #[cfg(test)] 42 | mod tests { 43 | use std::time::Duration; 44 | 45 | use async_std::prelude::*; 46 | use serde_json::json; 47 | 48 | use super::*; 49 | 50 | #[test] 51 | fn sample() -> Result<()> { 52 | task::block_on(async { 53 | let listen_addr = find_listen_addr().await; 54 | env::set_var("LISTEN_ADDR", format!("{}", listen_addr)); 55 | 56 | let server: task::JoinHandle> = task::spawn(async move { 57 | run().await?; 58 | 59 | Ok(()) 60 | }); 61 | 62 | let client: task::JoinHandle> = task::spawn(async move { 63 | let listen_addr = env::var("LISTEN_ADDR").unwrap(); 64 | 65 | task::sleep(Duration::from_millis(300)).await; 66 | 67 | let string = surf::post(format!("http://{}/graphql", listen_addr)) 68 | .body(Body::from(r#"{"query":"{ human(id:\"1000\") {name} }"}"#)) 69 | .header("Content-Type", "application/json") 70 | .recv_string() 71 | .await?; 72 | 73 | assert_eq!( 74 | string, 75 | json!({"data":{"human":{"name":"Luke Skywalker"}}}).to_string() 76 | ); 77 | 78 | Ok(()) 79 | }); 80 | 81 | server.race(client).await?; 82 | 83 | Ok(()) 84 | }) 85 | } 86 | 87 | async fn find_listen_addr() -> async_std::net::SocketAddr { 88 | async_std::net::TcpListener::bind("localhost:0") 89 | .await 90 | .unwrap() 91 | .local_addr() 92 | .unwrap() 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tide/subscription/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tide-subscription" 3 | version = "0.1.0" 4 | authors = ["Sunli "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | async-graphql = { path = "../../.." } 9 | async-graphql-tide = { path = "../../../integrations/tide" } 10 | books = { path = "../../models/books" } 11 | tide = "0.16" 12 | async-std = "1.12.0" 13 | -------------------------------------------------------------------------------- /tide/subscription/src/main.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::{http::GraphiQLSource, Schema}; 2 | use async_std::task; 3 | use books::{MutationRoot, QueryRoot, Storage, SubscriptionRoot}; 4 | use tide::{http::mime, Body, Response, StatusCode}; 5 | 6 | type Result = std::result::Result>; 7 | 8 | fn main() -> Result<()> { 9 | task::block_on(run()) 10 | } 11 | 12 | async fn run() -> Result<()> { 13 | let schema = Schema::build(QueryRoot, MutationRoot, SubscriptionRoot) 14 | .data(Storage::default()) 15 | .finish(); 16 | 17 | println!("GraphiQL IDE: http://localhost:8000"); 18 | 19 | let mut app = tide::new(); 20 | 21 | app.at("/graphql") 22 | .post(async_graphql_tide::graphql(schema.clone())) 23 | .get(async_graphql_tide::GraphQLSubscription::new(schema).build()); 24 | 25 | app.at("/").get(|_| async move { 26 | let mut resp = Response::new(StatusCode::Ok); 27 | resp.set_body(Body::from_string( 28 | GraphiQLSource::build() 29 | .endpoint("/graphql") 30 | .subscription_endpoint("/graphql") 31 | .finish(), 32 | )); 33 | resp.set_content_type(mime::HTML); 34 | Ok(resp) 35 | }); 36 | 37 | app.listen("0.0.0.0:8000").await?; 38 | 39 | Ok(()) 40 | } 41 | -------------------------------------------------------------------------------- /warp/starwars/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "warp-starwars" 3 | version = "0.1.1" 4 | authors = ["sunli "] 5 | edition = "2024" 6 | 7 | [dependencies] 8 | async-graphql = { path = "../../.." } 9 | async-graphql-warp = { path = "../../../integrations/warp" } 10 | tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] } 11 | warp = "0.3" 12 | starwars = { path = "../../models/starwars" } 13 | http = "0.2" 14 | -------------------------------------------------------------------------------- /warp/starwars/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | 3 | use async_graphql::{EmptyMutation, EmptySubscription, Schema, http::GraphiQLSource}; 4 | use async_graphql_warp::{GraphQLBadRequest, GraphQLResponse}; 5 | use http::StatusCode; 6 | use starwars::{QueryRoot, StarWars}; 7 | use warp::{Filter, Rejection, http::Response as HttpResponse}; 8 | 9 | #[tokio::main] 10 | async fn main() { 11 | let schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription) 12 | .data(StarWars::new()) 13 | .finish(); 14 | 15 | println!("GraphiQL IDE: http://localhost:8000"); 16 | 17 | let graphql_post = async_graphql_warp::graphql(schema).and_then( 18 | |(schema, request): ( 19 | Schema, 20 | async_graphql::Request, 21 | )| async move { 22 | Ok::<_, Infallible>(GraphQLResponse::from(schema.execute(request).await)) 23 | }, 24 | ); 25 | 26 | let graphiql = warp::path::end().and(warp::get()).map(|| { 27 | HttpResponse::builder() 28 | .header("content-type", "text/html") 29 | .body(GraphiQLSource::build().endpoint("/").finish()) 30 | }); 31 | 32 | let routes = graphiql 33 | .or(graphql_post) 34 | .recover(|err: Rejection| async move { 35 | if let Some(GraphQLBadRequest(err)) = err.find() { 36 | return Ok::<_, Infallible>(warp::reply::with_status( 37 | err.to_string(), 38 | StatusCode::BAD_REQUEST, 39 | )); 40 | } 41 | 42 | Ok(warp::reply::with_status( 43 | "INTERNAL_SERVER_ERROR".to_string(), 44 | StatusCode::INTERNAL_SERVER_ERROR, 45 | )) 46 | }); 47 | 48 | warp::serve(routes).run(([127, 0, 0, 1], 8000)).await; 49 | } 50 | -------------------------------------------------------------------------------- /warp/subscription/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "warp-subscription" 3 | version = "0.1.1" 4 | authors = ["sunli "] 5 | edition = "2024" 6 | 7 | [dependencies] 8 | async-graphql = { path = "../../.." } 9 | async-graphql-warp = { path = "../../../integrations/warp" } 10 | tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] } 11 | warp = "0.3" 12 | books = { path = "../../models/books" } 13 | -------------------------------------------------------------------------------- /warp/subscription/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | 3 | use async_graphql::{Schema, http::GraphiQLSource}; 4 | use async_graphql_warp::{GraphQLResponse, graphql_subscription}; 5 | use books::{MutationRoot, QueryRoot, Storage, SubscriptionRoot}; 6 | use warp::{Filter, http::Response as HttpResponse}; 7 | 8 | #[tokio::main] 9 | async fn main() { 10 | let schema = Schema::build(QueryRoot, MutationRoot, SubscriptionRoot) 11 | .data(Storage::default()) 12 | .finish(); 13 | 14 | println!("GraphiQL IDE: http://localhost:8000"); 15 | 16 | let graphql_post = async_graphql_warp::graphql(schema.clone()).and_then( 17 | |(schema, request): ( 18 | Schema, 19 | async_graphql::Request, 20 | )| async move { 21 | Ok::<_, Infallible>(GraphQLResponse::from(schema.execute(request).await)) 22 | }, 23 | ); 24 | 25 | let graphiql = warp::path::end().and(warp::get()).map(|| { 26 | HttpResponse::builder() 27 | .header("content-type", "text/html") 28 | .body( 29 | GraphiQLSource::build() 30 | .endpoint("/") 31 | .subscription_endpoint("/") 32 | .finish(), 33 | ) 34 | }); 35 | 36 | let routes = graphql_subscription(schema).or(graphiql).or(graphql_post); 37 | warp::serve(routes).run(([127, 0, 0, 1], 8000)).await; 38 | } 39 | -------------------------------------------------------------------------------- /warp/token-from-header/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "warp-token-from-header" 3 | version = "0.1.1" 4 | authors = ["sunli "] 5 | edition = "2024" 6 | 7 | [dependencies] 8 | async-graphql = { path = "../../.." } 9 | async-graphql-warp = { path = "../../../integrations/warp" } 10 | tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] } 11 | warp = "0.3" 12 | token = { path = "../../models/token" } 13 | -------------------------------------------------------------------------------- /warp/token-from-header/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | 3 | use async_graphql::{Data, EmptyMutation, Schema, http::GraphiQLSource}; 4 | use async_graphql_warp::{GraphQLResponse, GraphQLWebSocket, graphql_protocol}; 5 | use token::{QueryRoot, SubscriptionRoot, Token, on_connection_init}; 6 | use warp::{Filter, http::Response as HttpResponse, ws::Ws}; 7 | 8 | #[tokio::main] 9 | async fn main() { 10 | let schema = Schema::build(QueryRoot, EmptyMutation, SubscriptionRoot).finish(); 11 | 12 | println!("GraphiQL IDE: http://localhost:8000"); 13 | 14 | let graphiql = warp::path::end().and(warp::get()).map(|| { 15 | HttpResponse::builder() 16 | .header("content-type", "text/html") 17 | .body( 18 | GraphiQLSource::build() 19 | .endpoint("/") 20 | .subscription_endpoint("/ws") 21 | .finish(), 22 | ) 23 | }); 24 | 25 | let graphql_post = warp::header::optional::("token") 26 | .and(async_graphql_warp::graphql(schema.clone())) 27 | .and_then( 28 | |token, 29 | (schema, mut request): ( 30 | Schema, 31 | async_graphql::Request, 32 | )| async move { 33 | if let Some(token) = token { 34 | request = request.data(Token(token)); 35 | } 36 | let resp = schema.execute(request).await; 37 | Ok::<_, Infallible>(GraphQLResponse::from(resp)) 38 | }, 39 | ); 40 | 41 | let subscription = warp::path!("ws") 42 | .and(warp::ws()) 43 | .and(warp::header::optional::("token")) 44 | .and(warp::any().map(move || schema.clone())) 45 | .and(graphql_protocol()) 46 | .map( 47 | move |ws: Ws, 48 | token, 49 | schema: Schema, 50 | protocol| { 51 | let reply = ws.on_upgrade(move |socket| { 52 | let mut data = Data::default(); 53 | if let Some(token) = token { 54 | data.insert(Token(token)); 55 | } 56 | 57 | GraphQLWebSocket::new(socket, schema, protocol) 58 | .with_data(data) 59 | .on_connection_init(on_connection_init) 60 | .serve() 61 | }); 62 | 63 | warp::reply::with_header( 64 | reply, 65 | "Sec-WebSocket-Protocol", 66 | protocol.sec_websocket_protocol(), 67 | ) 68 | }, 69 | ); 70 | 71 | let routes = subscription.or(graphiql).or(graphql_post); 72 | warp::serve(routes).run(([127, 0, 0, 1], 8000)).await; 73 | } 74 | --------------------------------------------------------------------------------