diff --git a/.rustfmt.toml b/.rustfmt.toml index 066a39d..364029d 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -1,4 +1,4 @@ -edition = "2021" +edition = "2024" newline_style = "Unix" # comments normalize_comments = true diff --git a/Cargo.toml b/Cargo.toml index 70ba245..0c12c45 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,12 @@ [workspace] +resolver = "2" members = [ "models/starwars", "models/books", "models/files", "models/token", + "models/dynamic-starwars", + "models/dynamic-files", "poem/opentelemetry-basic", "poem/starwars", @@ -12,6 +15,9 @@ members = [ "poem/token-from-header", "poem/upload", "poem/dynamic-schema", + "poem/dynamic-starwars", + "poem/dynamic-books", + "poem/dynamic-upload", "actix-web/token-from-header", "actix-web/subscription", @@ -23,10 +29,6 @@ members = [ "warp/subscription", "warp/token-from-header", - "federation/federation-accounts", - "federation/federation-products", - "federation/federation-reviews", - "rocket/starwars", "rocket/upload", @@ -39,4 +41,12 @@ members = [ "tide/dataloader", "tide/dataloader-postgres", "tide/subscription", + + "federation/static-schema/federation-accounts", + "federation/static-schema/federation-products", + "federation/static-schema/federation-reviews", + + "federation/dynamic-schema/federation-accounts", + "federation/dynamic-schema/federation-products", + "federation/dynamic-schema/federation-reviews", ] diff --git a/README.md b/README.md index 21aa3f8..796a79b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # Examples for async-graphql +A git submodule that shows example async-graphql projects. + + ## Directory structure - [poem] Examples for `poem` @@ -8,4 +11,30 @@ - [tide] Examples for `tide` - [rocket] Examples for `rocket` - [axum] Examples for `axum` +- [loco] Examples for `loco` - [federation] Examples for [Apollo Federation](https://www.apollographql.com/docs/federation/) + + +## Running Examples + +To run the examples, clone the top-level repo, [async-graphql](https://github.com/async-graphql/async-graphql) and then issue the following commands: + +```bash +git clone async-graphql/async-graphql +# in async-graphql repo, install needed dependencies +cargo build + +# update this repo as a git submodule +git submodule update +``` + +To run the example axum-starwars: +``` +# change into the example folder and run a relevant binary +cargo run --bin axum-starwars +``` + +To list all available binary targets: +``` +cargo run --bin +``` \ No newline at end of file diff --git a/actix-web/error-extensions/Cargo.toml b/actix-web/error-extensions/Cargo.toml index a8281e4..1a5d347 100644 --- a/actix-web/error-extensions/Cargo.toml +++ b/actix-web/error-extensions/Cargo.toml @@ -2,11 +2,13 @@ name = "actix-web-error-extensions" version = "0.1.1" authors = ["sunli "] -edition = "2021" +edition = "2024" [dependencies] async-graphql = { path = "../../.." } async-graphql-actix-web = { path = "../../../integrations/actix-web" } -actix-web = { version = "4.0.1", default-features = false, features = ["macros"] } +actix-web = { version = "4.5.1", default-features = false, features = [ + "macros", +] } thiserror = "1.0" serde_json = "1.0" diff --git a/actix-web/error-extensions/src/main.rs b/actix-web/error-extensions/src/main.rs index 703041c..ff2f8fa 100644 --- a/actix-web/error-extensions/src/main.rs +++ b/actix-web/error-extensions/src/main.rs @@ -1,12 +1,12 @@ #[macro_use] extern crate thiserror; -use actix_web::{guard, web, web::Data, App, HttpResponse, HttpServer}; +use actix_web::{App, HttpResponse, HttpServer, guard, web}; use async_graphql::{ - http::GraphiQLSource, EmptyMutation, EmptySubscription, ErrorExtensions, FieldError, - FieldResult, Object, ResultExt, Schema, + EmptyMutation, EmptySubscription, ErrorExtensions, FieldError, FieldResult, Object, ResultExt, + Schema, http::GraphiQLSource, }; -use async_graphql_actix_web::{GraphQLRequest, GraphQLResponse}; +use async_graphql_actix_web::GraphQL; #[derive(Debug, Error)] pub enum MyError { @@ -91,21 +91,10 @@ impl QueryRoot { } } -async fn index( - schema: web::Data>, - req: GraphQLRequest, -) -> GraphQLResponse { - schema.execute(req.into_inner()).await.into() -} - async fn gql_playgound() -> HttpResponse { HttpResponse::Ok() .content_type("text/html; charset=utf-8") - .body( - GraphiQLSource::build() - .endpoint("http://localhost:8000") - .finish(), - ) + .body(GraphiQLSource::build().endpoint("/").finish()) } #[actix_web::main] @@ -113,13 +102,14 @@ async fn main() -> std::io::Result<()> { println!("GraphiQL IDE: http://localhost:8000"); HttpServer::new(move || { + let schema = Schema::new(QueryRoot, EmptyMutation, EmptySubscription); + App::new() - .app_data(Data::new(Schema::new( - QueryRoot, - EmptyMutation, - EmptySubscription, - ))) - .service(web::resource("/").guard(guard::Post()).to(index)) + .service( + web::resource("/") + .guard(guard::Post()) + .to(GraphQL::new(schema)), + ) .service(web::resource("/").guard(guard::Get()).to(gql_playgound)) }) .bind("127.0.0.1:8000")? diff --git a/actix-web/starwars/Cargo.toml b/actix-web/starwars/Cargo.toml index f02f3cf..379228f 100644 --- a/actix-web/starwars/Cargo.toml +++ b/actix-web/starwars/Cargo.toml @@ -2,10 +2,12 @@ name = "actix-web-starwars" version = "0.1.1" authors = ["sunli "] -edition = "2021" +edition = "2024" [dependencies] async-graphql = { path = "../../.." } async-graphql-actix-web = { path = "../../../integrations/actix-web" } -actix-web = { version = "4.0.1", default-features = false, features = ["macros"] } +actix-web = { version = "4.5.1", default-features = false, features = [ + "macros", +] } starwars = { path = "../../models/starwars" } diff --git a/actix-web/starwars/src/main.rs b/actix-web/starwars/src/main.rs index a9d7e79..d85ede7 100644 --- a/actix-web/starwars/src/main.rs +++ b/actix-web/starwars/src/main.rs @@ -1,34 +1,29 @@ -use actix_web::{guard, web, web::Data, App, HttpResponse, HttpServer, Result}; -use async_graphql::{http::GraphiQLSource, EmptyMutation, EmptySubscription, Schema}; -use async_graphql_actix_web::{GraphQLRequest, GraphQLResponse}; -use starwars::{QueryRoot, StarWars, StarWarsSchema}; - -async fn index(schema: web::Data, req: GraphQLRequest) -> GraphQLResponse { - schema.execute(req.into_inner()).await.into() -} +use actix_web::{App, HttpResponse, HttpServer, Result, guard, web}; +use async_graphql::{EmptyMutation, EmptySubscription, Schema, http::GraphiQLSource}; +use async_graphql_actix_web::GraphQL; +use starwars::{QueryRoot, StarWars}; async fn index_graphiql() -> Result { Ok(HttpResponse::Ok() .content_type("text/html; charset=utf-8") - .body( - GraphiQLSource::build() - .endpoint("http://localhost:8000") - .finish(), - )) + .body(GraphiQLSource::build().endpoint("/").finish())) } #[actix_web::main] async fn main() -> std::io::Result<()> { - let schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription) - .data(StarWars::new()) - .finish(); - println!("GraphiQL IDE: http://localhost:8000"); HttpServer::new(move || { + let schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription) + .data(StarWars::new()) + .finish(); + App::new() - .app_data(Data::new(schema.clone())) - .service(web::resource("/").guard(guard::Post()).to(index)) + .service( + web::resource("/") + .guard(guard::Post()) + .to(GraphQL::new(schema)), + ) .service(web::resource("/").guard(guard::Get()).to(index_graphiql)) }) .bind("127.0.0.1:8000")? diff --git a/actix-web/subscription/Cargo.toml b/actix-web/subscription/Cargo.toml index ef6b143..caf53d4 100644 --- a/actix-web/subscription/Cargo.toml +++ b/actix-web/subscription/Cargo.toml @@ -2,10 +2,12 @@ name = "actix-web-subscription" version = "0.1.1" authors = ["sunli "] -edition = "2021" +edition = "2024" [dependencies] async-graphql = { path = "../../.." } async-graphql-actix-web = { path = "../../../integrations/actix-web" } -actix-web = { version = "4.0.1", default-features = false, features = ["macros"] } +actix-web = { version = "4.5.1", default-features = false, features = [ + "macros", +] } books = { path = "../../models/books" } diff --git a/actix-web/subscription/src/main.rs b/actix-web/subscription/src/main.rs index e561b57..8831b0c 100644 --- a/actix-web/subscription/src/main.rs +++ b/actix-web/subscription/src/main.rs @@ -1,19 +1,15 @@ -use actix_web::{guard, web, web::Data, App, HttpRequest, HttpResponse, HttpServer, Result}; -use async_graphql::{http::GraphiQLSource, Schema}; -use async_graphql_actix_web::{GraphQLRequest, GraphQLResponse, GraphQLSubscription}; +use actix_web::{App, HttpRequest, HttpResponse, HttpServer, Result, guard, web}; +use async_graphql::{Schema, http::GraphiQLSource}; +use async_graphql_actix_web::{GraphQL, GraphQLSubscription}; use books::{BooksSchema, MutationRoot, QueryRoot, Storage, SubscriptionRoot}; -async fn index(schema: web::Data, req: GraphQLRequest) -> GraphQLResponse { - schema.execute(req.into_inner()).await.into() -} - async fn index_graphiql() -> Result { Ok(HttpResponse::Ok() .content_type("text/html; charset=utf-8") .body( GraphiQLSource::build() - .endpoint("http://localhost:8000") - .subscription_endpoint("ws://localhost:8000") + .endpoint("/") + .subscription_endpoint("/") .finish(), )) } @@ -28,20 +24,24 @@ async fn index_ws( #[actix_web::main] async fn main() -> std::io::Result<()> { - let schema = Schema::build(QueryRoot, MutationRoot, SubscriptionRoot) - .data(Storage::default()) - .finish(); - println!("GraphiQL IDE: http://localhost:8000"); HttpServer::new(move || { + let schema = Schema::build(QueryRoot, MutationRoot, SubscriptionRoot) + .data(Storage::default()) + .finish(); + App::new() - .app_data(Data::new(schema.clone())) - .service(web::resource("/").guard(guard::Post()).to(index)) + .service( + web::resource("/") + .guard(guard::Post()) + .to(GraphQL::new(schema.clone())), + ) .service( web::resource("/") .guard(guard::Get()) .guard(guard::Header("upgrade", "websocket")) + .app_data(web::Data::new(schema)) .to(index_ws), ) .service(web::resource("/").guard(guard::Get()).to(index_graphiql)) diff --git a/actix-web/token-from-header/Cargo.toml b/actix-web/token-from-header/Cargo.toml index e27dd08..2db48e8 100644 --- a/actix-web/token-from-header/Cargo.toml +++ b/actix-web/token-from-header/Cargo.toml @@ -2,10 +2,12 @@ name = "actix-web-token-from-header" version = "0.1.1" authors = ["sunli "] -edition = "2021" +edition = "2024" [dependencies] async-graphql = { path = "../../.." } async-graphql-actix-web = { path = "../../../integrations/actix-web" } -actix-web = { version = "4.0.1", default-features = false, features = ["macros"] } +actix-web = { version = "4.5.1", default-features = false, features = [ + "macros", +] } token = { path = "../../models/token" } diff --git a/actix-web/token-from-header/src/main.rs b/actix-web/token-from-header/src/main.rs index 8bcb639..700e265 100644 --- a/actix-web/token-from-header/src/main.rs +++ b/actix-web/token-from-header/src/main.rs @@ -1,17 +1,17 @@ use actix_web::{ - guard, http::header::HeaderMap, web, App, HttpRequest, HttpResponse, HttpServer, Result, + App, HttpRequest, HttpResponse, HttpServer, Result, guard, http::header::HeaderMap, web, }; -use async_graphql::{http::GraphiQLSource, EmptyMutation, Schema}; +use async_graphql::{EmptyMutation, Schema, http::GraphiQLSource}; use async_graphql_actix_web::{GraphQLRequest, GraphQLResponse, GraphQLSubscription}; -use token::{on_connection_init, QueryRoot, SubscriptionRoot, Token, TokenSchema}; +use token::{QueryRoot, SubscriptionRoot, Token, TokenSchema, on_connection_init}; async fn graphiql() -> HttpResponse { HttpResponse::Ok() .content_type("text/html; charset=utf-8") .body( GraphiQLSource::build() - .endpoint("http://localhost:8000") - .subscription_endpoint("ws://localhost:8000/ws") + .endpoint("/") + .subscription_endpoint("/ws") .finish(), ) } diff --git a/actix-web/upload/Cargo.toml b/actix-web/upload/Cargo.toml index 8974300..d0a17ae 100644 --- a/actix-web/upload/Cargo.toml +++ b/actix-web/upload/Cargo.toml @@ -2,10 +2,12 @@ name = "actix-web-upload" version = "0.1.1" authors = ["sunli "] -edition = "2021" +edition = "2024" [dependencies] async-graphql = { path = "../../.." } async-graphql-actix-web = { path = "../../../integrations/actix-web" } -actix-web = { version = "4.0.1", default-features = false, features = ["macros"] } +actix-web = { version = "4.5.1", default-features = false, features = [ + "macros", +] } files = { path = "../../models/files" } diff --git a/actix-web/upload/src/main.rs b/actix-web/upload/src/main.rs index 50d9b47..f9993ac 100644 --- a/actix-web/upload/src/main.rs +++ b/actix-web/upload/src/main.rs @@ -1,7 +1,7 @@ -use actix_web::{guard, web, web::Data, App, HttpResponse, HttpServer}; +use actix_web::{App, HttpResponse, HttpServer, guard, web, web::Data}; use async_graphql::{ - http::{GraphiQLSource, MultipartOptions}, EmptySubscription, Schema, + http::{GraphiQLSource, MultipartOptions}, }; use async_graphql_actix_web::{GraphQLRequest, GraphQLResponse}; use files::{FilesSchema, MutationRoot, QueryRoot, Storage}; @@ -13,11 +13,7 @@ async fn index(schema: web::Data, req: GraphQLRequest) -> GraphQLRe async fn gql_playgound() -> HttpResponse { HttpResponse::Ok() .content_type("text/html; charset=utf-8") - .body( - GraphiQLSource::build() - .endpoint("http://localhost:8000") - .finish(), - ) + .body(GraphiQLSource::build().endpoint("/").finish()) } #[actix_web::main] diff --git a/axum/starwars/Cargo.toml b/axum/starwars/Cargo.toml index 123936f..1221e1b 100644 --- a/axum/starwars/Cargo.toml +++ b/axum/starwars/Cargo.toml @@ -1,12 +1,11 @@ [package] name = "axum-starwars" version = "0.1.0" -edition = "2021" +edition = "2024" [dependencies] async-graphql = { path = "../../.." } async-graphql-axum = { path = "../../../integrations/axum" } -tokio = { version = "1.8", features = ["macros", "rt-multi-thread"] } +tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] } starwars = { path = "../../models/starwars" } -hyper = "0.14" -axum = { version = "0.5.1", features = ["headers"] } +axum = { version = "0.8.1" } diff --git a/axum/starwars/src/main.rs b/axum/starwars/src/main.rs index 8ee1607..cbb1720 100644 --- a/axum/starwars/src/main.rs +++ b/axum/starwars/src/main.rs @@ -1,26 +1,15 @@ -use async_graphql::{http::GraphiQLSource, EmptyMutation, EmptySubscription, Schema}; -use async_graphql_axum::{GraphQLRequest, GraphQLResponse}; +use async_graphql::{EmptyMutation, EmptySubscription, Schema, http::GraphiQLSource}; +use async_graphql_axum::GraphQL; use axum::{ - extract::Extension, + Router, response::{self, IntoResponse}, routing::get, - Router, Server, }; -use starwars::{QueryRoot, StarWars, StarWarsSchema}; - -async fn graphql_handler( - schema: Extension, - req: GraphQLRequest, -) -> GraphQLResponse { - schema.execute(req.into_inner()).await.into() -} +use starwars::{QueryRoot, StarWars}; +use tokio::net::TcpListener; async fn graphiql() -> impl IntoResponse { - response::Html( - GraphiQLSource::build() - .endpoint("http://localhost:8000") - .finish(), - ) + response::Html(GraphiQLSource::build().endpoint("/").finish()) } #[tokio::main] @@ -29,14 +18,11 @@ async fn main() { .data(StarWars::new()) .finish(); - let app = Router::new() - .route("/", get(graphiql).post(graphql_handler)) - .layer(Extension(schema)); + let app = Router::new().route("/", get(graphiql).post_service(GraphQL::new(schema))); println!("GraphiQL IDE: http://localhost:8000"); - Server::bind(&"127.0.0.1:8000".parse().unwrap()) - .serve(app.into_make_service()) + axum::serve(TcpListener::bind("127.0.0.1:8000").await.unwrap(), app) .await .unwrap(); } diff --git a/axum/subscription/Cargo.toml b/axum/subscription/Cargo.toml index f2dc3dc..4d5eb88 100644 --- a/axum/subscription/Cargo.toml +++ b/axum/subscription/Cargo.toml @@ -1,12 +1,11 @@ [package] -name = "subscription" +name = "axum-subscription" version = "0.1.0" -edition = "2021" +edition = "2024" [dependencies] async-graphql = { path = "../../.." } async-graphql-axum = { path = "../../../integrations/axum" } -tokio = { version = "1.8", features = ["macros", "rt-multi-thread"] } +tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] } books = { path = "../../models/books" } -hyper = "0.14" -axum = { version = "0.5.1", features = ["ws", "headers"] } +axum = { version = "0.8.1", features = ["ws"] } diff --git a/axum/subscription/src/main.rs b/axum/subscription/src/main.rs index 011ae47..8675c97 100644 --- a/axum/subscription/src/main.rs +++ b/axum/subscription/src/main.rs @@ -1,22 +1,18 @@ -use async_graphql::{http::GraphiQLSource, Schema}; -use async_graphql_axum::{GraphQLRequest, GraphQLResponse, GraphQLSubscription}; +use async_graphql::{Schema, http::GraphiQLSource}; +use async_graphql_axum::{GraphQL, GraphQLSubscription}; use axum::{ - extract::Extension, + Router, response::{self, IntoResponse}, routing::get, - Router, Server, }; -use books::{BooksSchema, MutationRoot, QueryRoot, Storage, SubscriptionRoot}; - -async fn graphql_handler(schema: Extension, req: GraphQLRequest) -> GraphQLResponse { - schema.execute(req.into_inner()).await.into() -} +use books::{MutationRoot, QueryRoot, Storage, SubscriptionRoot}; +use tokio::net::TcpListener; async fn graphiql() -> impl IntoResponse { response::Html( GraphiQLSource::build() - .endpoint("http://localhost:8000") - .subscription_endpoint("ws://localhost:8000/ws") + .endpoint("/") + .subscription_endpoint("/ws") .finish(), ) } @@ -28,14 +24,15 @@ async fn main() { .finish(); let app = Router::new() - .route("/", get(graphiql).post(graphql_handler)) - .route("/ws", GraphQLSubscription::new(schema.clone())) - .layer(Extension(schema)); + .route( + "/", + get(graphiql).post_service(GraphQL::new(schema.clone())), + ) + .route_service("/ws", GraphQLSubscription::new(schema)); println!("GraphiQL IDE: http://localhost:8000"); - Server::bind(&"127.0.0.1:8000".parse().unwrap()) - .serve(app.into_make_service()) + axum::serve(TcpListener::bind("127.0.0.1:8000").await.unwrap(), app) .await .unwrap(); } diff --git a/axum/token-from-header/Cargo.toml b/axum/token-from-header/Cargo.toml index 5f490a7..25fc994 100644 --- a/axum/token-from-header/Cargo.toml +++ b/axum/token-from-header/Cargo.toml @@ -1,12 +1,11 @@ [package] name = "axum-token-from-header" version = "0.1.0" -edition = "2021" +edition = "2024" [dependencies] async-graphql = { path = "../../.." } async-graphql-axum = { path = "../../../integrations/axum" } -tokio = { version = "1.8", features = ["macros", "rt-multi-thread"] } +tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] } token = { path = "../../models/token" } -hyper = "0.14" -axum = { version = "0.5.1", features = ["ws", "headers"] } +axum = { version = "0.8.1", features = ["ws"] } diff --git a/axum/token-from-header/src/main.rs b/axum/token-from-header/src/main.rs index eb63eb9..65d938c 100644 --- a/axum/token-from-header/src/main.rs +++ b/axum/token-from-header/src/main.rs @@ -1,16 +1,17 @@ use async_graphql::{ - http::{playground_source, GraphQLPlaygroundConfig, ALL_WEBSOCKET_PROTOCOLS}, EmptyMutation, Schema, + http::{ALL_WEBSOCKET_PROTOCOLS, GraphQLPlaygroundConfig, playground_source}, }; use async_graphql_axum::{GraphQLProtocol, GraphQLRequest, GraphQLResponse, GraphQLWebSocket}; use axum::{ - extract::{ws::WebSocketUpgrade, Extension}, + Router, + extract::{State, ws::WebSocketUpgrade}, http::header::HeaderMap, response::{Html, IntoResponse, Response}, routing::get, - Router, Server, }; -use token::{on_connection_init, QueryRoot, SubscriptionRoot, Token, TokenSchema}; +use token::{QueryRoot, SubscriptionRoot, Token, TokenSchema, on_connection_init}; +use tokio::net::TcpListener; async fn graphql_playground() -> impl IntoResponse { Html(playground_source( @@ -25,9 +26,9 @@ fn get_token_from_headers(headers: &HeaderMap) -> Option { } async fn graphql_handler( - req: GraphQLRequest, - Extension(schema): Extension, + State(schema): State, headers: HeaderMap, + req: GraphQLRequest, ) -> GraphQLResponse { let mut req = req.into_inner(); if let Some(token) = get_token_from_headers(&headers) { @@ -37,7 +38,7 @@ async fn graphql_handler( } async fn graphql_ws_handler( - Extension(schema): Extension, + State(schema): State, protocol: GraphQLProtocol, websocket: WebSocketUpgrade, ) -> Response { @@ -57,12 +58,11 @@ async fn main() { let app = Router::new() .route("/", get(graphql_playground).post(graphql_handler)) .route("/ws", get(graphql_ws_handler)) - .layer(Extension(schema)); + .with_state(schema); println!("Playground: http://localhost:8000"); - Server::bind(&"127.0.0.1:8000".parse().unwrap()) - .serve(app.into_make_service()) + axum::serve(TcpListener::bind("127.0.0.1:8000").await.unwrap(), app) .await .unwrap(); } diff --git a/axum/upload/Cargo.toml b/axum/upload/Cargo.toml index 3f821e3..dcdf687 100644 --- a/axum/upload/Cargo.toml +++ b/axum/upload/Cargo.toml @@ -2,13 +2,12 @@ name = "axum-upload" version = "0.1.1" authors = ["sunli "] -edition = "2021" +edition = "2024" [dependencies] async-graphql = { path = "../../.." } async-graphql-axum = { path = "../../../integrations/axum" } -axum = "0.5.1" +axum = "0.8.1" files = { path = "../../models/files" } -tokio = { version = "1.8", features = ["macros", "rt-multi-thread"] } -hyper = "0.14" -tower-http = { version = "0.2.1", features = ["cors"] } +tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] } +tower-http = { version = "0.5.2", features = ["cors"] } diff --git a/axum/upload/src/main.rs b/axum/upload/src/main.rs index f758a1c..5b3de6e 100644 --- a/axum/upload/src/main.rs +++ b/axum/upload/src/main.rs @@ -1,25 +1,17 @@ -use async_graphql::{http::GraphiQLSource, EmptySubscription, Schema}; -use async_graphql_axum::{GraphQLRequest, GraphQLResponse}; +use async_graphql::{EmptySubscription, Schema, http::GraphiQLSource}; +use async_graphql_axum::GraphQL; use axum::{ - extract::Extension, + Router, + http::Method, response::{Html, IntoResponse}, routing::get, - Router, }; -use files::{FilesSchema, MutationRoot, QueryRoot, Storage}; -use hyper::{Method, Server}; -use tower_http::cors::{CorsLayer, Origin}; - -async fn graphql_handler(schema: Extension, req: GraphQLRequest) -> GraphQLResponse { - schema.execute(req.0).await.into() -} +use files::{MutationRoot, QueryRoot, Storage}; +use tokio::net::TcpListener; +use tower_http::cors::{AllowOrigin, CorsLayer}; async fn graphiql() -> impl IntoResponse { - Html( - GraphiQLSource::build() - .endpoint("http://localhost:8000") - .finish(), - ) + Html(GraphiQLSource::build().endpoint("/").finish()) } #[tokio::main] @@ -31,16 +23,14 @@ async fn main() { println!("GraphiQL IDE: http://localhost:8000"); let app = Router::new() - .route("/", get(graphiql).post(graphql_handler)) - .layer(Extension(schema)) + .route("/", get(graphiql).post_service(GraphQL::new(schema))) .layer( CorsLayer::new() - .allow_origin(Origin::predicate(|_, _| true)) - .allow_methods(vec![Method::GET, Method::POST]), + .allow_origin(AllowOrigin::predicate(|_, _| true)) + .allow_methods([Method::GET, Method::POST]), ); - Server::bind(&"127.0.0.1:8000".parse().unwrap()) - .serve(app.into_make_service()) + axum::serve(TcpListener::bind("127.0.0.1:8000").await.unwrap(), app) .await .unwrap(); } diff --git a/federation/README.md b/federation/dynamic-schema/README.md similarity index 90% rename from federation/README.md rename to federation/dynamic-schema/README.md index 332fdd6..ceb95a4 100644 --- a/federation/README.md +++ b/federation/dynamic-schema/README.md @@ -13,3 +13,4 @@ You can view the full schema in [Apollo Studio](https://studio.apollographql.com 1. Start each subgraph with `cargo run --bin {subgraph_name}` 2. Add each subgraph to `rover dev` with `rover dev --url http://localhost:{port} --name {subgraph_name}` 3. Visit `http://localhost:3000` in a browser. +4. You can now run queries like the one in `query.graphql` against the router. diff --git a/federation/dynamic-schema/federation-accounts/Cargo.toml b/federation/dynamic-schema/federation-accounts/Cargo.toml new file mode 100644 index 0000000..fa0ffec --- /dev/null +++ b/federation/dynamic-schema/federation-accounts/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "dynamic-federation-accounts" +version = "0.1.0" +edition = "2024" + +[dependencies] +async-graphql = { path = "../../../..", features = ["dynamic-schema"] } +async-graphql-poem = { path = "../../../../integrations/poem" } +tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] } +poem = { version = "3.0.0" } diff --git a/federation/dynamic-schema/federation-accounts/src/main.rs b/federation/dynamic-schema/federation-accounts/src/main.rs new file mode 100644 index 0000000..cb81bc7 --- /dev/null +++ b/federation/dynamic-schema/federation-accounts/src/main.rs @@ -0,0 +1,172 @@ +use async_graphql::dynamic::{ + Field, FieldFuture, FieldValue, Object, Schema, SchemaError, TypeRef, +}; +use async_graphql_poem::GraphQL; +use poem::{Route, Server, listener::TcpListener}; + +struct Picture { + url: String, + width: u32, + height: u32, +} + +struct User { + id: String, + username: String, + profile_picture: Option, + review_count: u32, + joined_timestamp: u64, +} + +impl User { + fn me() -> User { + User { + id: "1234".into(), + username: "Me".to_string(), + profile_picture: Some(Picture { + url: "http://localhost:8080/me.jpg".to_string(), + width: 256, + height: 256, + }), + review_count: 0, + joined_timestamp: 1, + } + } +} + +fn schema() -> Result { + let picture = Object::new("Picture") + .field(Field::new( + "url", + TypeRef::named_nn(TypeRef::STRING), + |ctx| { + FieldFuture::new(async move { + let picture = ctx.parent_value.try_downcast_ref::()?; + Ok(Some(FieldValue::value(&picture.url))) + }) + }, + )) + .field(Field::new( + "width", + TypeRef::named_nn(TypeRef::INT), + |ctx| { + FieldFuture::new(async move { + let picture = ctx.parent_value.try_downcast_ref::()?; + Ok(Some(FieldValue::value(picture.width))) + }) + }, + )) + .field(Field::new( + "height", + TypeRef::named_nn(TypeRef::INT), + |ctx| { + FieldFuture::new(async move { + let picture = ctx.parent_value.try_downcast_ref::()?; + Ok(Some(FieldValue::value(picture.height))) + }) + }, + )); + + let user = Object::new("User") + .field(Field::new("id", TypeRef::named_nn(TypeRef::ID), |ctx| { + FieldFuture::new(async move { + let user = ctx.parent_value.try_downcast_ref::()?; + Ok(Some(FieldValue::value(&user.id))) + }) + })) + .field(Field::new( + "username", + TypeRef::named_nn(TypeRef::STRING), + |ctx| { + FieldFuture::new(async move { + let user = ctx.parent_value.try_downcast_ref::()?; + Ok(Some(FieldValue::value(&user.username))) + }) + }, + )) + .field(Field::new( + "profilePicture", + TypeRef::named_nn(TypeRef::STRING), + |ctx| { + FieldFuture::new(async move { + let user = ctx.parent_value.try_downcast_ref::()?; + Ok(user + .profile_picture + .as_ref() + .map(|pic| FieldValue::borrowed_any(pic))) + }) + }, + )) + .field(Field::new( + "reviewCount", + TypeRef::named_nn(TypeRef::INT), + |ctx| { + FieldFuture::new(async move { + let user = ctx.parent_value.try_downcast_ref::()?; + Ok(Some(FieldValue::value(user.review_count))) + }) + }, + )) + .field(Field::new( + "joinedTimestamp", + TypeRef::named_nn(TypeRef::STRING), + |ctx| { + FieldFuture::new(async move { + let user = ctx.parent_value.try_downcast_ref::()?; + Ok(Some(FieldValue::value(user.joined_timestamp.to_string()))) + }) + }, + )) + .key("id"); + + let query = Object::new("Query").field(Field::new( + "me", + TypeRef::named_nn(user.type_name()), + |_| FieldFuture::new(async move { Ok(Some(FieldValue::owned_any(User::me()))) }), + )); + + Schema::build("Query", None, None) + .register(picture) + .register(user) + .register(query) + .entity_resolver(|ctx| { + FieldFuture::new(async move { + let representations = ctx.args.try_get("representations")?.list()?; + let mut values = Vec::new(); + + for item in representations.iter() { + let item = item.object()?; + let typename = item + .try_get("__typename") + .and_then(|value| value.string())?; + + if typename == "User" { + let id = item.try_get("id")?.string()?; + if id == "1234" { + values.push(FieldValue::owned_any(User::me())); + } else { + let username = format!("User {}", id); + let user = User { + id: id.to_string(), + username, + profile_picture: None, + review_count: 0, + joined_timestamp: 1500, + }; + values.push(FieldValue::owned_any(user)); + } + } + } + + Ok(Some(FieldValue::list(values))) + }) + }) + .finish() +} + +#[tokio::main] +async fn main() -> std::io::Result<()> { + Server::new(TcpListener::bind("127.0.0.1:4001")) + .run(Route::new().at("/", GraphQL::new(schema().unwrap()))) + .await +} diff --git a/federation/dynamic-schema/federation-products/Cargo.toml b/federation/dynamic-schema/federation-products/Cargo.toml new file mode 100644 index 0000000..535a220 --- /dev/null +++ b/federation/dynamic-schema/federation-products/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "dynamic-federation-products" +version = "0.1.0" +edition = "2024" + +[dependencies] +async-graphql = { path = "../../../..", features = ["dynamic-schema"] } +async-graphql-poem = { path = "../../../../integrations/poem" } +tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] } +poem = { version = "3.0.0" } diff --git a/federation/dynamic-schema/federation-products/src/main.rs b/federation/dynamic-schema/federation-products/src/main.rs new file mode 100644 index 0000000..f3d5cf1 --- /dev/null +++ b/federation/dynamic-schema/federation-products/src/main.rs @@ -0,0 +1,114 @@ +use async_graphql::dynamic::{ + Field, FieldFuture, FieldValue, Object, Schema, SchemaError, TypeRef, +}; +use async_graphql_poem::GraphQL; +use poem::{Route, Server, listener::TcpListener}; + +struct Product { + upc: String, + name: String, + price: i32, +} + +fn schema() -> Result { + let hats = vec![ + Product { + upc: "top-1".to_string(), + name: "Trilby".to_string(), + price: 11, + }, + Product { + upc: "top-2".to_string(), + name: "Fedora".to_string(), + price: 22, + }, + Product { + upc: "top-3".to_string(), + name: "Boater".to_string(), + price: 33, + }, + ]; + + let product = Object::new("Product") + .field(Field::new( + "upc", + TypeRef::named_nn(TypeRef::STRING), + |ctx| { + FieldFuture::new(async move { + let product = ctx.parent_value.try_downcast_ref::()?; + Ok(Some(FieldValue::value(&product.upc))) + }) + }, + )) + .field(Field::new( + "name", + TypeRef::named_nn(TypeRef::STRING), + |ctx| { + FieldFuture::new(async move { + let product = ctx.parent_value.try_downcast_ref::()?; + Ok(Some(FieldValue::value(&product.name))) + }) + }, + )) + .field( + Field::new("price", TypeRef::named_nn(TypeRef::INT), |ctx| { + FieldFuture::new(async move { + let product = ctx.parent_value.try_downcast_ref::()?; + Ok(Some(FieldValue::value(product.price))) + }) + }) + .shareable(), + ) + .key("upc"); + + let query = Object::new("Query").field(Field::new( + "topProducts", + TypeRef::named_nn_list_nn(product.type_name()), + |ctx| { + FieldFuture::new(async move { + let mut values = Vec::new(); + let products = ctx.data_unchecked::>(); + for product in products { + values.push(FieldValue::borrowed_any(product)); + } + Ok(Some(values)) + }) + }, + )); + + Schema::build("Query", None, None) + .data(hats) + .register(product) + .register(query) + .entity_resolver(|ctx| { + FieldFuture::new(async move { + let products = ctx.data_unchecked::>(); + let representations = ctx.args.try_get("representations")?.list()?; + let mut values = Vec::new(); + + for item in representations.iter() { + let item = item.object()?; + let typename = item + .try_get("__typename") + .and_then(|value| value.string())?; + + if typename == "Product" { + let upc = item.try_get("upc")?.string()?; + if let Some(product) = products.iter().find(|product| product.upc == upc) { + values.push(FieldValue::borrowed_any(product)); + } + } + } + + Ok(Some(FieldValue::list(values))) + }) + }) + .finish() +} + +#[tokio::main] +async fn main() -> std::io::Result<()> { + Server::new(TcpListener::bind("127.0.0.1:4002")) + .run(Route::new().at("/", GraphQL::new(schema().unwrap()))) + .await +} diff --git a/federation/dynamic-schema/federation-reviews/Cargo.toml b/federation/dynamic-schema/federation-reviews/Cargo.toml new file mode 100644 index 0000000..d4a7028 --- /dev/null +++ b/federation/dynamic-schema/federation-reviews/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "dynamic-federation-reviews" +version = "0.1.0" +edition = "2024" + +[dependencies] +async-graphql = { path = "../../../..", features = ["dynamic-schema"] } +async-graphql-poem = { path = "../../../../integrations/poem" } +tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] } +poem = { version = "3.0.0" } diff --git a/federation/dynamic-schema/federation-reviews/src/main.rs b/federation/dynamic-schema/federation-reviews/src/main.rs new file mode 100644 index 0000000..a5b441e --- /dev/null +++ b/federation/dynamic-schema/federation-reviews/src/main.rs @@ -0,0 +1,349 @@ +use async_graphql::dynamic::{ + Enum, Field, FieldFuture, FieldValue, Object, Schema, SchemaError, TypeRef, +}; +use async_graphql_poem::GraphQL; +use poem::{Route, Server, listener::TcpListener}; + +struct Picture { + url: String, + width: u32, + height: u32, + alt_text: String, +} + +struct Review { + id: String, + body: String, + pictures: Vec, +} + +struct Product { + upc: String, + price: u32, +} + +impl Review { + fn get_product(&self) -> Product { + match self.id.as_str() { + "review-1" => Product { + upc: "top-1".to_string(), + price: 10, + }, + "review-2" => Product { + upc: "top-2".to_string(), + price: 20, + }, + "review-3" => Product { + upc: "top-3".to_string(), + price: 30, + }, + _ => panic!("Unknown review id"), + } + } + + fn get_author(&self) -> User { + let user_id = match self.id.as_str() { + "review-1" => "1234", + "review-2" => "1234", + "review-3" => "7777", + _ => panic!("Unknown review id"), + } + .to_string(); + user_by_id(user_id, None) + } +} + +struct User { + id: String, + review_count: u32, + joined_timestamp: u64, +} + +fn user_by_id(id: String, joined_timestamp: Option) -> User { + let review_count = match id.as_str() { + "1234" => 2, + "7777" => 1, + _ => 0, + }; + // This will be set if the user requested the fields that require it. + let joined_timestamp = joined_timestamp.unwrap_or(9001); + User { + id, + review_count, + joined_timestamp, + } +} + +fn schema() -> Result { + let picture = Object::new("Picture") + .shareable() + .field(Field::new( + "url", + TypeRef::named_nn(TypeRef::STRING), + |ctx| { + FieldFuture::new(async move { + let picture = ctx.parent_value.try_downcast_ref::()?; + Ok(Some(FieldValue::value(&picture.url))) + }) + }, + )) + .field(Field::new( + "width", + TypeRef::named_nn(TypeRef::INT), + |ctx| { + FieldFuture::new(async move { + let picture = ctx.parent_value.try_downcast_ref::()?; + Ok(Some(FieldValue::value(picture.width))) + }) + }, + )) + .field(Field::new( + "height", + TypeRef::named_nn(TypeRef::INT), + |ctx| { + FieldFuture::new(async move { + let picture = ctx.parent_value.try_downcast_ref::()?; + Ok(Some(FieldValue::value(picture.height))) + }) + }, + )) + .field( + Field::new("altText", TypeRef::named_nn(TypeRef::INT), |ctx| { + FieldFuture::new(async move { + let picture = ctx.parent_value.try_downcast_ref::()?; + Ok(Some(FieldValue::value(&picture.alt_text))) + }) + }) + .inaccessible(), + ); + + let review = Object::new("Review") + .field(Field::new("id", TypeRef::named_nn(TypeRef::ID), |ctx| { + FieldFuture::new(async move { + let review = ctx.parent_value.try_downcast_ref::()?; + Ok(Some(FieldValue::value(&review.id))) + }) + })) + .field(Field::new( + "body", + TypeRef::named_nn(TypeRef::STRING), + |ctx| { + FieldFuture::new(async move { + let review = ctx.parent_value.try_downcast_ref::()?; + Ok(Some(FieldValue::value(&review.body))) + }) + }, + )) + .field(Field::new( + "pictures", + TypeRef::named_nn_list_nn(picture.type_name()), + |ctx| { + FieldFuture::new(async move { + let review = ctx.parent_value.try_downcast_ref::()?; + Ok(Some(FieldValue::list( + review + .pictures + .iter() + .map(|review| FieldValue::borrowed_any(review)), + ))) + }) + }, + )) + .field( + Field::new("product", TypeRef::named_nn(TypeRef::STRING), |ctx| { + FieldFuture::new(async move { + let review = ctx.parent_value.try_downcast_ref::()?; + Ok(Some(FieldValue::owned_any(review.get_product()))) + }) + }) + .provides("price"), + ) + .field(Field::new("author", TypeRef::named_nn("User"), |ctx| { + FieldFuture::new(async move { + let review = ctx.parent_value.try_downcast_ref::()?; + let author = review.get_author(); + Ok(Some(FieldValue::owned_any(author))) + }) + })); + + let trust_worthiness = + Enum::new("Trustworthiness").items(["ReallyTrusted", "KindaTrusted", "NotTrusted"]); + + let user = Object::new("User") + .field(Field::new("id", TypeRef::named_nn(TypeRef::ID), |ctx| { + FieldFuture::new(async move { + let user = ctx.parent_value.try_downcast_ref::()?; + Ok(Some(FieldValue::value(&user.id))) + }) + })) + .field( + Field::new("reviewCount", TypeRef::named_nn(TypeRef::INT), |ctx| { + FieldFuture::new(async move { + let user = ctx.parent_value.try_downcast_ref::()?; + Ok(Some(FieldValue::value(user.review_count))) + }) + }) + .override_from("accounts"), + ) + .field( + Field::new("joinedTimestamp", TypeRef::named_nn(TypeRef::INT), |ctx| { + FieldFuture::new(async move { + let user = ctx.parent_value.try_downcast_ref::()?; + Ok(Some(FieldValue::value(user.joined_timestamp))) + }) + }) + .external(), + ) + .field(Field::new( + "reviews", + TypeRef::named_nn_list_nn(review.type_name()), + |ctx| { + FieldFuture::new(async move { + let reviews = ctx.data::>()?; + Ok(Some(FieldValue::list( + reviews + .iter() + .map(|review| FieldValue::borrowed_any(review)), + ))) + }) + }, + )) + .field( + Field::new( + "trustworthiness", + TypeRef::named_nn_list_nn(review.type_name()), + |ctx| { + FieldFuture::new(async move { + let user = ctx.parent_value.try_downcast_ref::()?; + Ok(Some( + if user.joined_timestamp < 1_000 && user.review_count > 1 { + FieldValue::value("ReallyTrusted") + } else if user.joined_timestamp < 2_000 { + FieldValue::value("KindaTrusted") + } else { + FieldValue::value("NotTrusted") + }, + )) + }) + }, + ) + .requires("joinedTimestamp"), + ) + .key("id"); + + let product = Object::new("Product") + .field(Field::new("upc", TypeRef::named_nn(TypeRef::ID), |ctx| { + FieldFuture::new(async move { + let product = ctx.parent_value.try_downcast_ref::()?; + Ok(Some(FieldValue::value(&product.upc))) + }) + })) + .field( + Field::new("price", TypeRef::named_nn(TypeRef::INT), |ctx| { + FieldFuture::new(async move { + let product = ctx.parent_value.try_downcast_ref::()?; + Ok(Some(FieldValue::value(product.price))) + }) + }) + .external(), + ) + .field(Field::new( + "reviews", + TypeRef::named_nn_list_nn(review.type_name()), + |ctx| { + FieldFuture::new(async move { + let user = ctx.parent_value.try_downcast_ref::()?; + let reviews = ctx.data::>()?; + Ok(Some(FieldValue::list( + reviews + .iter() + .filter(|review| review.get_author().id == user.id) + .map(|review| FieldValue::borrowed_any(review)), + ))) + }) + }, + )) + .key("upc"); + + let reviews = vec![ + Review { + id: "review-1".into(), + body: "A highly effective form of birth control.".into(), + pictures: vec![ + Picture { + url: "http://localhost:8080/ugly_hat.jpg".to_string(), + width: 100, + height: 100, + alt_text: "A Trilby".to_string(), + }, + Picture { + url: "http://localhost:8080/troll_face.jpg".to_string(), + width: 42, + height: 42, + alt_text: "The troll face meme".to_string(), + }, + ], + }, + Review { + id: "review-2".into(), + body: "Fedoras are one of the most fashionable hats around and can look great with a variety of outfits.".into(), + pictures: vec![], + }, + Review { + id: "review-3".into(), + body: "This is the last straw. Hat you will wear. 11/10".into(), + pictures: vec![], + }, + ]; + + let query = Object::new("Query"); + + Schema::build("Query", None, None) + .data(reviews) + .register(picture) + .register(review) + .register(trust_worthiness) + .register(user) + .register(product) + .register(query) + .entity_resolver(|ctx| { + FieldFuture::new(async move { + let representations = ctx.args.try_get("representations")?.list()?; + let mut values = Vec::new(); + + for item in representations.iter() { + let item = item.object()?; + let typename = item + .try_get("__typename") + .and_then(|value| value.string())?; + + if typename == "User" { + let id = item.try_get("id")?.string()?; + let joined_timestamp = item + .get("joinedTimestamp") + .and_then(|value| value.u64().ok()); + values.push(FieldValue::owned_any(user_by_id( + id.to_string(), + joined_timestamp, + ))); + } else if typename == "Product" { + let upc = item.try_get("upc")?.string()?; + values.push(FieldValue::owned_any(Product { + upc: upc.to_string(), + price: 0, + })); + } + } + + Ok(Some(FieldValue::list(values))) + }) + }) + .finish() +} + +#[tokio::main] +async fn main() -> std::io::Result<()> { + Server::new(TcpListener::bind("127.0.0.1:4003")) + .run(Route::new().at("/", GraphQL::new(schema().unwrap()))) + .await +} diff --git a/federation/dynamic-schema/query.graphql b/federation/dynamic-schema/query.graphql new file mode 100644 index 0000000..35c656a --- /dev/null +++ b/federation/dynamic-schema/query.graphql @@ -0,0 +1,19 @@ +query ExampleQuery { + me { + id + username @lowercase + reviews { + body + ... @defer { + product { + reviews { + author { + username + } + body + } + } + } + } + } +} \ No newline at end of file diff --git a/federation/dynamic-schema/start.sh b/federation/dynamic-schema/start.sh new file mode 100755 index 0000000..6e6b8a1 --- /dev/null +++ b/federation/dynamic-schema/start.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +set -eumo pipefail + +function cleanup { + for pid in "${PRODUCTS_ROVER_PID:-}" "${REVIEWS_ROVER_PID:-}" "${ACCOUNTS_PID:-}" "${PRODUCTS_PID:-}" "${REVIEWS_PID:-}"; do + # try kill all registered pids + [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null && kill "$pid" || echo "Could not kill $pid" + done +} +trap cleanup EXIT + +cargo build --bin dynamic-federation-accounts +cargo build --bin dynamic-federation-products +cargo build --bin dynamic-federation-reviews + +cargo run --bin dynamic-federation-accounts & +ACCOUNTS_PID=$! + +cargo run --bin dynamic-federation-products & +PRODUCTS_PID=$! + +cargo run --bin dynamic-federation-reviews & +REVIEWS_PID=$! + +sleep 3 + +rover dev --url http://localhost:4001 --name accounts & +sleep 1 +rover dev --url http://localhost:4002 --name products & +PRODUCTS_ROVER_PID=$! +sleep 1 +rover dev --url http://localhost:4003 --name reviews & +REVIEWS_ROVER_PID=$! +fg %4 diff --git a/federation/federation-accounts/Cargo.toml b/federation/federation-accounts/Cargo.toml deleted file mode 100644 index 64f09ee..0000000 --- a/federation/federation-accounts/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "federation-accounts" -version = "0.2.0" -authors = ["sunli "] -edition = "2021" - -[dependencies] -async-graphql = { path = "../../.." } -async-graphql-poem = { path = "../../../integrations/poem" } -tokio = { version = "1.0.2", features = ["macros", "rt-multi-thread"] } -poem = { version = "1.3.43" } diff --git a/federation/federation-products/Cargo.toml b/federation/federation-products/Cargo.toml deleted file mode 100644 index 279c2c2..0000000 --- a/federation/federation-products/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "federation-products" -version = "0.2.0" -authors = ["sunli "] -edition = "2021" - -[dependencies] -async-graphql = { path = "../../.." } -async-graphql-poem = { path = "../../../integrations/poem" } -tokio = { version = "1.0.2", features = ["macros", "rt-multi-thread"] } -poem = { version = "1.3.43" } diff --git a/federation/federation-reviews/Cargo.toml b/federation/federation-reviews/Cargo.toml deleted file mode 100644 index 6bfe00e..0000000 --- a/federation/federation-reviews/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "federation-reviews" -version = "0.2.0" -authors = ["sunli "] -edition = "2021" - -[dependencies] -async-graphql = { path = "../../.." } -async-graphql-poem = { path = "../../../integrations/poem" } -tokio = { version = "1.0.2", features = ["macros", "rt-multi-thread"] } -poem = { version = "1.3.43" } diff --git a/federation/start.sh b/federation/start.sh deleted file mode 100755 index 35c3a04..0000000 --- a/federation/start.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/bash - -set -eumo pipefail - -function cleanup { - kill "$PRODUCTS_ROVER_PID" - kill "$REVIEWS_ROVER_PID" - kill "$ACCOUNTS_PID" - kill "$PRODUCTS_PID" - kill "$REVIEWS_PID" -} -trap cleanup EXIT - -cargo build --bin federation-accounts -cargo build --bin federation-products -cargo build --bin federation-reviews - -cargo run --bin federation-accounts & -ACCOUNTS_PID=$! - -cargo run --bin federation-products & -PRODUCTS_PID=$! - -cargo run --bin federation-reviews & -REVIEWS_PID=$! - -sleep 3 - -rover dev --url http://localhost:4001 --name accounts & -sleep 1 -rover dev --url http://localhost:4002 --name products & -PRODUCTS_ROVER_PID=$! -sleep 1 -rover dev --url http://localhost:4003 --name reviews & -REVIEWS_ROVER_PID=$! -fg %4 \ No newline at end of file diff --git a/federation/static-schema/README.md b/federation/static-schema/README.md new file mode 100644 index 0000000..ceb95a4 --- /dev/null +++ b/federation/static-schema/README.md @@ -0,0 +1,16 @@ +# Federation Example + +An example of using [Apollo Federation](https://www.apollographql.com/docs/federation/) to compose GraphQL services into a single data graph. + +## The schema + +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). + +## How to run + +1. Install [Rover](https://www.apollographql.com/docs/rover/) +2. Run `/start.sh` which will: + 1. Start each subgraph with `cargo run --bin {subgraph_name}` + 2. Add each subgraph to `rover dev` with `rover dev --url http://localhost:{port} --name {subgraph_name}` +3. Visit `http://localhost:3000` in a browser. +4. You can now run queries like the one in `query.graphql` against the router. diff --git a/federation/static-schema/directives/Cargo.toml b/federation/static-schema/directives/Cargo.toml new file mode 100644 index 0000000..cfd89ce --- /dev/null +++ b/federation/static-schema/directives/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "directives" +version = "0.1.0" +authors = ["sunli "] +edition = "2024" + +[dependencies] +async-graphql = { path = "../../../.." } +async-trait = "0.1.79" diff --git a/federation/static-schema/directives/src/lib.rs b/federation/static-schema/directives/src/lib.rs new file mode 100644 index 0000000..773c262 --- /dev/null +++ b/federation/static-schema/directives/src/lib.rs @@ -0,0 +1,24 @@ +use async_graphql::{Context, CustomDirective, Directive, ResolveFut, ServerResult, Value}; + +struct LowercaseDirective; + +#[async_trait::async_trait] +impl CustomDirective for LowercaseDirective { + async fn resolve_field( + &self, + _ctx: &Context<'_>, + resolve: ResolveFut<'_>, + ) -> ServerResult> { + resolve.await.map(|value| { + value.map(|value| match value { + Value::String(str) => Value::String(str.to_ascii_lowercase()), + _ => value, + }) + }) + } +} + +#[Directive(location = "Field")] +pub fn lowercase() -> impl CustomDirective { + LowercaseDirective +} diff --git a/federation/static-schema/federation-accounts/Cargo.toml b/federation/static-schema/federation-accounts/Cargo.toml new file mode 100644 index 0000000..64288ab --- /dev/null +++ b/federation/static-schema/federation-accounts/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "static-federation-accounts" +version = "0.2.0" +authors = ["sunli "] +edition = "2024" + +[dependencies] +async-graphql = { path = "../../../.." } +async-graphql-poem = { path = "../../../../integrations/poem" } +directives = { path = "../directives" } +tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] } +poem = { version = "3.0.0" } diff --git a/federation/federation-accounts/src/main.rs b/federation/static-schema/federation-accounts/src/main.rs similarity index 84% rename from federation/federation-accounts/src/main.rs rename to federation/static-schema/federation-accounts/src/main.rs index c1eb085..2def417 100644 --- a/federation/federation-accounts/src/main.rs +++ b/federation/static-schema/federation-accounts/src/main.rs @@ -1,6 +1,6 @@ -use async_graphql::{EmptyMutation, EmptySubscription, Object, Schema, SimpleObject, ID}; +use async_graphql::{EmptyMutation, EmptySubscription, ID, Object, Schema, SimpleObject}; use async_graphql_poem::GraphQL; -use poem::{listener::TcpListener, Route, Server}; +use poem::{Route, Server, listener::TcpListener}; #[derive(SimpleObject)] struct User { @@ -64,7 +64,9 @@ impl Query { #[tokio::main] async fn main() -> std::io::Result<()> { - let schema = Schema::new(Query, EmptyMutation, EmptySubscription); + let schema = Schema::build(Query, EmptyMutation, EmptySubscription) + .directive(directives::lowercase) + .finish(); Server::new(TcpListener::bind("127.0.0.1:4001")) .run(Route::new().at("/", GraphQL::new(schema))) .await diff --git a/federation/static-schema/federation-products/Cargo.toml b/federation/static-schema/federation-products/Cargo.toml new file mode 100644 index 0000000..c754ce9 --- /dev/null +++ b/federation/static-schema/federation-products/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "static-federation-products" +version = "0.2.0" +authors = ["sunli "] +edition = "2024" + +[dependencies] +async-graphql = { path = "../../../.." } +async-graphql-poem = { path = "../../../../integrations/poem" } +directives = { path = "../directives" } +tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] } +poem = { version = "3.0.0" } diff --git a/federation/federation-products/src/main.rs b/federation/static-schema/federation-products/src/main.rs similarity index 93% rename from federation/federation-products/src/main.rs rename to federation/static-schema/federation-products/src/main.rs index cf4cd36..8d192de 100644 --- a/federation/federation-products/src/main.rs +++ b/federation/static-schema/federation-products/src/main.rs @@ -1,6 +1,6 @@ use async_graphql::{Context, EmptyMutation, EmptySubscription, Object, Schema, SimpleObject}; use async_graphql_poem::GraphQL; -use poem::{listener::TcpListener, Route, Server}; +use poem::{Route, Server, listener::TcpListener}; #[derive(SimpleObject)] struct Product { @@ -51,6 +51,7 @@ async fn main() -> std::io::Result<()> { let schema = Schema::build(Query, EmptyMutation, EmptySubscription) .data(hats) + .directive(directives::lowercase) .finish(); Server::new(TcpListener::bind("127.0.0.1:4002")) diff --git a/federation/static-schema/federation-reviews/Cargo.toml b/federation/static-schema/federation-reviews/Cargo.toml new file mode 100644 index 0000000..5a86fa8 --- /dev/null +++ b/federation/static-schema/federation-reviews/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "static-federation-reviews" +version = "0.2.0" +authors = ["sunli "] +edition = "2024" + +[dependencies] +async-graphql = { path = "../../../.." } +async-graphql-poem = { path = "../../../../integrations/poem" } +directives = { path = "../directives" } +tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] } +poem = { version = "3.0.0" } diff --git a/federation/federation-reviews/src/main.rs b/federation/static-schema/federation-reviews/src/main.rs similarity index 96% rename from federation/federation-reviews/src/main.rs rename to federation/static-schema/federation-reviews/src/main.rs index 8f874a4..4f566c3 100644 --- a/federation/federation-reviews/src/main.rs +++ b/federation/static-schema/federation-reviews/src/main.rs @@ -1,9 +1,9 @@ use async_graphql::{ - ComplexObject, Context, EmptyMutation, EmptySubscription, Enum, Object, Schema, SimpleObject, - ID, + ComplexObject, Context, EmptyMutation, EmptySubscription, Enum, ID, Object, Schema, + SimpleObject, }; use async_graphql_poem::GraphQL; -use poem::{listener::TcpListener, Route, Server}; +use poem::{Route, Server, listener::TcpListener}; #[derive(SimpleObject)] #[graphql(complex)] @@ -16,6 +16,7 @@ struct User { } #[derive(Enum, Eq, PartialEq, Copy, Clone)] +#[allow(clippy::enum_variant_names)] enum Trustworthiness { ReallyTrusted, KindaTrusted, @@ -189,6 +190,7 @@ async fn main() -> std::io::Result<()> { let schema = Schema::build(Query, EmptyMutation, EmptySubscription) .data(reviews) + .directive(directives::lowercase) .finish(); Server::new(TcpListener::bind("127.0.0.1:4003")) diff --git a/federation/static-schema/query.graphql b/federation/static-schema/query.graphql new file mode 100644 index 0000000..35c656a --- /dev/null +++ b/federation/static-schema/query.graphql @@ -0,0 +1,19 @@ +query ExampleQuery { + me { + id + username @lowercase + reviews { + body + ... @defer { + product { + reviews { + author { + username + } + body + } + } + } + } + } +} \ No newline at end of file diff --git a/federation/static-schema/start.sh b/federation/static-schema/start.sh new file mode 100755 index 0000000..d174905 --- /dev/null +++ b/federation/static-schema/start.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +set -eumo pipefail + +function cleanup { + for pid in "${PRODUCTS_ROVER_PID:-}" "${REVIEWS_ROVER_PID:-}" "${ACCOUNTS_PID:-}" "${PRODUCTS_PID:-}" "${REVIEWS_PID:-}"; do + # try kill all registered pids + [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null && kill "$pid" || echo "Could not kill $pid" + done +} +trap cleanup EXIT + +cargo build --bin static-federation-accounts +cargo build --bin static-federation-products +cargo build --bin static-federation-reviews + +cargo run --bin static-federation-accounts & +ACCOUNTS_PID=$! + +cargo run --bin static-federation-products & +PRODUCTS_PID=$! + +cargo run --bin static-federation-reviews & +REVIEWS_PID=$! + +sleep 3 + +rover dev --url http://localhost:4001 --name accounts & +sleep 1 +rover dev --url http://localhost:4002 --name products & +PRODUCTS_ROVER_PID=$! +sleep 1 +rover dev --url http://localhost:4003 --name reviews & +REVIEWS_ROVER_PID=$! +fg %4 \ No newline at end of file diff --git a/loco/starwars/.cargo/config.toml b/loco/starwars/.cargo/config.toml new file mode 100644 index 0000000..5ebf033 --- /dev/null +++ b/loco/starwars/.cargo/config.toml @@ -0,0 +1,2 @@ +[alias] +loco = "run --" diff --git a/loco/starwars/Cargo.toml b/loco/starwars/Cargo.toml new file mode 100644 index 0000000..37ec452 --- /dev/null +++ b/loco/starwars/Cargo.toml @@ -0,0 +1,26 @@ +[workspace] + +[package] +name = "loco-starwars" +version = "0.1.0" +edition = "2024" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +loco-rs = { version = "0.6.0", default-features = false, features = ["cli"] } +eyre = "*" +tokio = { version = "1.33.0", default-features = false } +async-trait = "0.1.74" + +axum = "0.7.1" + +# async-graphql dependencies +async-graphql = { path = "../../.." } +async-graphql-axum = { path = "../../../integrations/axum" } +starwars = { path = "../../models/starwars" } + +[[bin]] +name = "starwars-cli" +path = "src/bin/main.rs" +required-features = [] diff --git a/loco/starwars/README.md b/loco/starwars/README.md new file mode 100644 index 0000000..55aba3c --- /dev/null +++ b/loco/starwars/README.md @@ -0,0 +1,36 @@ +# async-graphql with Loco :train: + +Example async-graphql project with [Loco](https://github.com/loco-rs/loco). + +## Quick Start + +Start your app: + +``` +$ cargo loco start +Finished dev [unoptimized + debuginfo] target(s) in 21.63s + Running `target/debug/myapp start` + + : + : + : + +controller/app_routes.rs:203: [Middleware] Adding log trace id + + ▄ ▀ + ▀ ▄ + ▄ ▀ ▄ ▄ ▄▀ + ▄ ▀▄▄ + ▄ ▀ ▀ ▀▄▀█▄ + ▀█▄ +▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▀▀█ + ██████ █████ ███ █████ ███ █████ ███ ▀█ + ██████ █████ ███ █████ ▀▀▀ █████ ███ ▄█▄ + ██████ █████ ███ █████ █████ ███ ████▄ + ██████ █████ ███ █████ ▄▄▄ █████ ███ █████ + ██████ █████ ███ ████ ███ █████ ███ ████▀ + ▀▀▀██▄ ▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀ ██▀ + ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ + +started on port 5150 +``` diff --git a/loco/starwars/config/development.yaml b/loco/starwars/config/development.yaml new file mode 100644 index 0000000..fa21014 --- /dev/null +++ b/loco/starwars/config/development.yaml @@ -0,0 +1,45 @@ +# Loco configuration file documentation + +# Application logging configuration +logger: + # Enable or disable logging. + enable: true + # Log level, options: trace, debug, info, warn or error. + level: debug + # Define the logging format. options: compact, pretty or json + format: compact + # 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 + # Uncomment the line below to override to see all third party libraries you can enable this config and override the logger filters. + # override_filter: trace + +# Web server configuration +server: + # Port on which the server will listen. the server binding is 0.0.0.0:{PORT} + port: 5150 + # The UI hostname or IP address that mailers will point to. + host: http://localhost + # Out of the box middleware configuration. to disable middleware you can changed the `enable` field to `false` of comment the middleware block + middlewares: + # Enable Etag cache header middleware + etag: + enable: true + # Allows to limit the payload size request. payload that bigger than this file will blocked the request. + limit_payload: + # Enable/Disable the middleware. + enable: true + # the limit size. can be b,kb,kib,mb,mib,gb,gib + body_limit: 5mb + # 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. + logger: + # Enable/Disable the middleware. + enable: true + # when your code is panicked, the request still returns 500 status code. + catch_panic: + # Enable/Disable the middleware. + enable: true + # Timeout for incoming requests middleware. requests that take more time from the configuration will cute and 408 status code will returned. + timeout_request: + # Enable/Disable the middleware. + enable: false + # Duration time in milliseconds. + timeout: 5000 diff --git a/loco/starwars/src/app.rs b/loco/starwars/src/app.rs new file mode 100644 index 0000000..6f0d33e --- /dev/null +++ b/loco/starwars/src/app.rs @@ -0,0 +1,42 @@ +use async_trait::async_trait; +use loco_rs::{ + app::{AppContext, Hooks}, + boot::{create_app, BootResult, StartMode}, + controller::AppRoutes, + environment::Environment, + task::Tasks, + worker::Processor, + Result, +}; + +use crate::controllers; + +pub struct App; +#[async_trait] +impl Hooks for App { + fn app_name() -> &'static str { + env!("CARGO_CRATE_NAME") + } + + fn app_version() -> String { + format!( + "{} ({})", + env!("CARGO_PKG_VERSION"), + option_env!("BUILD_SHA") + .or(option_env!("GITHUB_SHA")) + .unwrap_or("dev") + ) + } + + async fn boot(mode: StartMode, environment: &Environment) -> Result { + create_app::(mode, environment).await + } + + fn routes(_ctx: &AppContext) -> AppRoutes { + AppRoutes::empty().add_route(controllers::graphiql::routes()) + } + + fn connect_workers<'a>(_p: &'a mut Processor, _ctx: &'a AppContext) {} + + fn register_tasks(_tasks: &mut Tasks) {} +} diff --git a/loco/starwars/src/bin/main.rs b/loco/starwars/src/bin/main.rs new file mode 100644 index 0000000..ffe90e7 --- /dev/null +++ b/loco/starwars/src/bin/main.rs @@ -0,0 +1,7 @@ +use loco_rs::cli; +use loco_starwars::app::App; + +#[tokio::main] +async fn main() -> eyre::Result<()> { + cli::main::().await +} diff --git a/loco/starwars/src/controllers/graphiql.rs b/loco/starwars/src/controllers/graphiql.rs new file mode 100644 index 0000000..6263797 --- /dev/null +++ b/loco/starwars/src/controllers/graphiql.rs @@ -0,0 +1,18 @@ +use async_graphql::{http::GraphiQLSource, EmptyMutation, EmptySubscription, Schema}; +use async_graphql_axum::GraphQL; +use axum::debug_handler; +use loco_rs::prelude::*; +use starwars::{QueryRoot, StarWars}; + +#[debug_handler] +async fn graphiql() -> Result { + format::html(&GraphiQLSource::build().endpoint("/").finish()) +} + +pub fn routes() -> Routes { + let schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription) + .data(StarWars::new()) + .finish(); + + Routes::new().add("/", get(graphiql).post_service(GraphQL::new(schema))) +} diff --git a/loco/starwars/src/controllers/mod.rs b/loco/starwars/src/controllers/mod.rs new file mode 100644 index 0000000..982badf --- /dev/null +++ b/loco/starwars/src/controllers/mod.rs @@ -0,0 +1 @@ +pub mod graphiql; diff --git a/loco/starwars/src/lib.rs b/loco/starwars/src/lib.rs new file mode 100644 index 0000000..cee1ed4 --- /dev/null +++ b/loco/starwars/src/lib.rs @@ -0,0 +1,2 @@ +pub mod app; +pub mod controllers; diff --git a/loco/subscription/.cargo/config.toml b/loco/subscription/.cargo/config.toml new file mode 100644 index 0000000..5ebf033 --- /dev/null +++ b/loco/subscription/.cargo/config.toml @@ -0,0 +1,2 @@ +[alias] +loco = "run --" diff --git a/loco/subscription/Cargo.toml b/loco/subscription/Cargo.toml new file mode 100644 index 0000000..ea2f83c --- /dev/null +++ b/loco/subscription/Cargo.toml @@ -0,0 +1,25 @@ +[workspace] + +[package] +name = "loco-subscription" +version = "0.1.0" +edition = "2024" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +loco-rs = { version = "0.6.0", default-features = false, features = ["cli"] } +eyre = "*" +tokio = { version = "1.33.0", default-features = false } +async-trait = "0.1.74" +axum = "0.7.1" + +# async-graphql dependencies +async-graphql = { path = "../../.." } +async-graphql-axum = { path = "../../../integrations/axum" } +books = { path = "../../models/books" } + +[[bin]] +name = "starwars-cli" +path = "src/bin/main.rs" +required-features = [] diff --git a/loco/subscription/README.md b/loco/subscription/README.md new file mode 100644 index 0000000..55aba3c --- /dev/null +++ b/loco/subscription/README.md @@ -0,0 +1,36 @@ +# async-graphql with Loco :train: + +Example async-graphql project with [Loco](https://github.com/loco-rs/loco). + +## Quick Start + +Start your app: + +``` +$ cargo loco start +Finished dev [unoptimized + debuginfo] target(s) in 21.63s + Running `target/debug/myapp start` + + : + : + : + +controller/app_routes.rs:203: [Middleware] Adding log trace id + + ▄ ▀ + ▀ ▄ + ▄ ▀ ▄ ▄ ▄▀ + ▄ ▀▄▄ + ▄ ▀ ▀ ▀▄▀█▄ + ▀█▄ +▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▀▀█ + ██████ █████ ███ █████ ███ █████ ███ ▀█ + ██████ █████ ███ █████ ▀▀▀ █████ ███ ▄█▄ + ██████ █████ ███ █████ █████ ███ ████▄ + ██████ █████ ███ █████ ▄▄▄ █████ ███ █████ + ██████ █████ ███ ████ ███ █████ ███ ████▀ + ▀▀▀██▄ ▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀ ██▀ + ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ + +started on port 5150 +``` diff --git a/loco/subscription/config/development.yaml b/loco/subscription/config/development.yaml new file mode 100644 index 0000000..fa21014 --- /dev/null +++ b/loco/subscription/config/development.yaml @@ -0,0 +1,45 @@ +# Loco configuration file documentation + +# Application logging configuration +logger: + # Enable or disable logging. + enable: true + # Log level, options: trace, debug, info, warn or error. + level: debug + # Define the logging format. options: compact, pretty or json + format: compact + # 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 + # Uncomment the line below to override to see all third party libraries you can enable this config and override the logger filters. + # override_filter: trace + +# Web server configuration +server: + # Port on which the server will listen. the server binding is 0.0.0.0:{PORT} + port: 5150 + # The UI hostname or IP address that mailers will point to. + host: http://localhost + # Out of the box middleware configuration. to disable middleware you can changed the `enable` field to `false` of comment the middleware block + middlewares: + # Enable Etag cache header middleware + etag: + enable: true + # Allows to limit the payload size request. payload that bigger than this file will blocked the request. + limit_payload: + # Enable/Disable the middleware. + enable: true + # the limit size. can be b,kb,kib,mb,mib,gb,gib + body_limit: 5mb + # 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. + logger: + # Enable/Disable the middleware. + enable: true + # when your code is panicked, the request still returns 500 status code. + catch_panic: + # Enable/Disable the middleware. + enable: true + # Timeout for incoming requests middleware. requests that take more time from the configuration will cute and 408 status code will returned. + timeout_request: + # Enable/Disable the middleware. + enable: false + # Duration time in milliseconds. + timeout: 5000 diff --git a/loco/subscription/src/app.rs b/loco/subscription/src/app.rs new file mode 100644 index 0000000..3cd8413 --- /dev/null +++ b/loco/subscription/src/app.rs @@ -0,0 +1,54 @@ +use async_graphql::Schema; +use books::{MutationRoot, QueryRoot, Storage, SubscriptionRoot}; + +use async_graphql_axum::GraphQLSubscription; +use async_trait::async_trait; +use axum::Router as AxumRouter; +use loco_rs::{ + app::{AppContext, Hooks}, + boot::{create_app, BootResult, StartMode}, + controller::AppRoutes, + environment::Environment, + task::Tasks, + worker::Processor, + Result, +}; + +use crate::controllers; + +pub struct App; +#[async_trait] +impl Hooks for App { + fn app_name() -> &'static str { + env!("CARGO_CRATE_NAME") + } + + fn app_version() -> String { + format!( + "{} ({})", + env!("CARGO_PKG_VERSION"), + option_env!("BUILD_SHA") + .or(option_env!("GITHUB_SHA")) + .unwrap_or("dev") + ) + } + + async fn boot(mode: StartMode, environment: &Environment) -> Result { + create_app::(mode, environment).await + } + + fn routes(_ctx: &AppContext) -> AppRoutes { + AppRoutes::empty().add_route(controllers::graphiql::routes()) + } + + async fn after_routes(router: AxumRouter, _ctx: &AppContext) -> Result { + let schema = Schema::build(QueryRoot, MutationRoot, SubscriptionRoot) + .data(Storage::default()) + .finish(); + Ok(router.route_service("/ws", GraphQLSubscription::new(schema))) + } + + fn connect_workers<'a>(_p: &'a mut Processor, _ctx: &'a AppContext) {} + + fn register_tasks(_tasks: &mut Tasks) {} +} diff --git a/loco/subscription/src/bin/main.rs b/loco/subscription/src/bin/main.rs new file mode 100644 index 0000000..90f3acc --- /dev/null +++ b/loco/subscription/src/bin/main.rs @@ -0,0 +1,7 @@ +use loco_rs::cli; +use loco_subscription::app::App; + +#[tokio::main] +async fn main() -> eyre::Result<()> { + cli::main::().await +} diff --git a/loco/subscription/src/controllers/graphiql.rs b/loco/subscription/src/controllers/graphiql.rs new file mode 100644 index 0000000..e39a1d4 --- /dev/null +++ b/loco/subscription/src/controllers/graphiql.rs @@ -0,0 +1,23 @@ +use async_graphql::{http::GraphiQLSource, Schema}; +use async_graphql_axum::GraphQL; +use axum::debug_handler; +use books::{MutationRoot, QueryRoot, Storage, SubscriptionRoot}; +use loco_rs::prelude::*; + +#[debug_handler] +async fn graphiql() -> Result { + format::html( + &GraphiQLSource::build() + .endpoint("/") + .subscription_endpoint("/ws") + .finish(), + ) +} + +pub fn routes() -> Routes { + let schema = Schema::build(QueryRoot, MutationRoot, SubscriptionRoot) + .data(Storage::default()) + .finish(); + + Routes::new().add("/", get(graphiql).post_service(GraphQL::new(schema))) +} diff --git a/loco/subscription/src/controllers/mod.rs b/loco/subscription/src/controllers/mod.rs new file mode 100644 index 0000000..982badf --- /dev/null +++ b/loco/subscription/src/controllers/mod.rs @@ -0,0 +1 @@ +pub mod graphiql; diff --git a/loco/subscription/src/lib.rs b/loco/subscription/src/lib.rs new file mode 100644 index 0000000..cee1ed4 --- /dev/null +++ b/loco/subscription/src/lib.rs @@ -0,0 +1,2 @@ +pub mod app; +pub mod controllers; diff --git a/models/books/Cargo.toml b/models/books/Cargo.toml index aef63bc..e5bc6a7 100644 --- a/models/books/Cargo.toml +++ b/models/books/Cargo.toml @@ -2,13 +2,13 @@ name = "books" version = "0.1.1" authors = ["sunli "] -edition = "2021" +edition = "2024" [dependencies] async-graphql = { path = "../../.." } -slab = "0.4.2" -futures-util = "0.3.0" -futures-channel = "0.3.0" -once_cell = "1.0" -futures-timer = "3.0.2" -async-stream = "0.3.0" +slab = "0.4.9" +futures-util = "0.3.30" +futures-channel = "0.3.30" +once_cell = "1.19" +futures-timer = "3.0.3" +async-stream = "0.3.5" diff --git a/models/books/src/lib.rs b/models/books/src/lib.rs index c364597..31ae117 100644 --- a/models/books/src/lib.rs +++ b/models/books/src/lib.rs @@ -2,8 +2,8 @@ mod simple_broker; use std::{sync::Arc, time::Duration}; -use async_graphql::{Context, Enum, Object, Result, Schema, Subscription, ID}; -use futures_util::{lock::Mutex, Stream, StreamExt}; +use async_graphql::{Context, Enum, ID, Object, Result, Schema, Subscription}; +use futures_util::{Stream, StreamExt, lock::Mutex}; use simple_broker::SimpleBroker; use slab::Slab; diff --git a/models/dynamic-books/Cargo.toml b/models/dynamic-books/Cargo.toml new file mode 100644 index 0000000..e8296e5 --- /dev/null +++ b/models/dynamic-books/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "dynamic-books" +version = "0.1.0" +edition = "2024" + +[dependencies] +async-graphql = { path = "../../..", features = ["dynamic-schema"] } +slab = "0.4.9" +futures-util = "0.3.30" +futures-channel = "0.3.30" +once_cell = "1.19" +futures-timer = "3.0.3" +async-stream = "0.3.5" diff --git a/models/dynamic-books/src/lib.rs b/models/dynamic-books/src/lib.rs new file mode 100644 index 0000000..e9a2438 --- /dev/null +++ b/models/dynamic-books/src/lib.rs @@ -0,0 +1,41 @@ +mod model; +mod simple_broker; +use std::{str::FromStr, sync::Arc}; + +use async_graphql::ID; +use futures_util::lock::Mutex; +pub use model::schema; +use slab::Slab; + +#[derive(Clone)] +pub struct Book { + id: ID, + name: String, + author: String, +} + +type Storage = Arc>>; + +#[derive(Eq, PartialEq, Copy, Clone, Debug)] +pub enum MutationType { + Created, + Deleted, +} + +impl FromStr for MutationType { + type Err = String; // Error type can be customized based on your needs + + fn from_str(s: &str) -> Result { + match s { + "CREATED" => Ok(MutationType::Created), + "DELETED" => Ok(MutationType::Deleted), + _ => Err(format!("Invalid MutationType: {}", s)), + } + } +} + +#[derive(Clone)] +struct BookChanged { + mutation_type: MutationType, + id: ID, +} diff --git a/models/dynamic-books/src/model.rs b/models/dynamic-books/src/model.rs new file mode 100644 index 0000000..4952984 --- /dev/null +++ b/models/dynamic-books/src/model.rs @@ -0,0 +1,189 @@ +use async_graphql::{ID, Value, dynamic::*}; +use futures_util::StreamExt; + +use crate::{Book, BookChanged, MutationType, Storage, simple_broker::SimpleBroker}; + +impl From for FieldValue<'_> { + fn from(value: MutationType) -> Self { + match value { + MutationType::Created => FieldValue::value("CREATED"), + MutationType::Deleted => FieldValue::value("DELETED"), + } + } +} + +pub fn schema() -> Result { + let mutation_type = Enum::new("MutationType") + .item(EnumItem::new("CREATED").description("New book created.")) + .item(EnumItem::new("DELETED").description("Current book deleted.")); + + let book = Object::new("Book") + .description("A book that will be stored.") + .field(Field::new("id", TypeRef::named_nn(TypeRef::ID), |ctx| { + FieldFuture::new(async move { + let book = ctx.parent_value.try_downcast_ref::()?; + Ok(Some(Value::from(book.id.to_owned()))) + }) + })) + .field(Field::new( + "name", + TypeRef::named_nn(TypeRef::STRING), + |ctx| { + FieldFuture::new(async move { + let book = ctx.parent_value.try_downcast_ref::()?; + Ok(Some(Value::from(book.name.to_owned()))) + }) + }, + )) + .field(Field::new( + "author", + TypeRef::named_nn(TypeRef::STRING), + |ctx| { + FieldFuture::new(async move { + let book = ctx.parent_value.try_downcast_ref::()?; + Ok(Some(Value::from(book.author.to_owned()))) + }) + }, + )); + let book_changed = Object::new("BookChanged") + .field(Field::new( + "mutationType", + TypeRef::named_nn(mutation_type.type_name()), + |ctx| { + FieldFuture::new(async move { + let book_changed = ctx.parent_value.try_downcast_ref::()?; + Ok(Some(FieldValue::from(book_changed.mutation_type))) + }) + }, + )) + .field(Field::new("id", TypeRef::named_nn(TypeRef::ID), |ctx| { + FieldFuture::new(async move { + let book_changed = ctx.parent_value.try_downcast_ref::()?; + Ok(Some(Value::from(book_changed.id.to_owned()))) + }) + })) + .field(Field::new( + "book", + TypeRef::named(book.type_name()), + |ctx| { + FieldFuture::new(async move { + let book_changed = ctx.parent_value.try_downcast_ref::()?; + let id = book_changed.id.to_owned(); + let book_id = id.parse::()?; + let store = ctx.data_unchecked::().lock().await; + let book = store.get(book_id).cloned(); + Ok(book.map(FieldValue::owned_any)) + }) + }, + )); + + let query_root = Object::new("Query") + .field(Field::new( + "getBooks", + TypeRef::named_list(book.type_name()), + |ctx| { + FieldFuture::new(async move { + let store = ctx.data_unchecked::().lock().await; + let books: Vec = store.iter().map(|(_, book)| book.clone()).collect(); + Ok(Some(FieldValue::list( + books.into_iter().map(FieldValue::owned_any), + ))) + }) + }, + )) + .field( + Field::new("getBook", TypeRef::named(book.type_name()), |ctx| { + FieldFuture::new(async move { + let id = ctx.args.try_get("id")?; + let book_id = match id.string() { + Ok(id) => id.to_string(), + Err(_) => id.u64()?.to_string(), + }; + let book_id = book_id.parse::()?; + let store = ctx.data_unchecked::().lock().await; + let book = store.get(book_id).cloned(); + Ok(book.map(FieldValue::owned_any)) + }) + }) + .argument(InputValue::new("id", TypeRef::named_nn(TypeRef::ID))), + ); + + let mutatation_root = Object::new("Mutation") + .field( + Field::new("createBook", TypeRef::named(TypeRef::ID), |ctx| { + FieldFuture::new(async move { + let mut store = ctx.data_unchecked::().lock().await; + let name = ctx.args.try_get("name")?; + let author = ctx.args.try_get("author")?; + let entry = store.vacant_entry(); + let id: ID = entry.key().into(); + let book = Book { + id: id.clone(), + name: name.string()?.to_string(), + author: author.string()?.to_string(), + }; + entry.insert(book); + let book_mutated = BookChanged { + mutation_type: MutationType::Created, + id: id.clone(), + }; + SimpleBroker::publish(book_mutated); + Ok(Some(Value::from(id))) + }) + }) + .argument(InputValue::new("name", TypeRef::named_nn(TypeRef::STRING))) + .argument(InputValue::new( + "author", + TypeRef::named_nn(TypeRef::STRING), + )), + ) + .field( + Field::new("deleteBook", TypeRef::named_nn(TypeRef::BOOLEAN), |ctx| { + FieldFuture::new(async move { + let mut store = ctx.data_unchecked::().lock().await; + let id = ctx.args.try_get("id")?; + let book_id = match id.string() { + Ok(id) => id.to_string(), + Err(_) => id.u64()?.to_string(), + }; + let book_id = book_id.parse::()?; + if store.contains(book_id) { + store.remove(book_id); + let book_mutated = BookChanged { + mutation_type: MutationType::Deleted, + id: book_id.into(), + }; + SimpleBroker::publish(book_mutated); + Ok(Some(Value::from(true))) + } else { + Ok(Some(Value::from(false))) + } + }) + }) + .argument(InputValue::new("id", TypeRef::named_nn(TypeRef::ID))), + ); + let subscription_root = Subscription::new("Subscription").field(SubscriptionField::new( + "bookMutation", + TypeRef::named_nn(book_changed.type_name()), + |_| { + SubscriptionFieldFuture::new(async { + Ok(SimpleBroker::::subscribe() + .map(|book| Ok(FieldValue::owned_any(book)))) + }) + }, + )); + + Schema::build( + query_root.type_name(), + Some(mutatation_root.type_name()), + Some(subscription_root.type_name()), + ) + .register(mutation_type) + .register(book) + .register(book_changed) + .register(query_root) + .register(subscription_root) + .register(mutatation_root) + .data(Storage::default()) + .finish() +} diff --git a/models/dynamic-books/src/simple_broker.rs b/models/dynamic-books/src/simple_broker.rs new file mode 100644 index 0000000..6b23e70 --- /dev/null +++ b/models/dynamic-books/src/simple_broker.rs @@ -0,0 +1,68 @@ +use std::{ + any::{Any, TypeId}, + collections::HashMap, + marker::PhantomData, + pin::Pin, + sync::Mutex, + task::{Context, Poll}, +}; + +use futures_channel::mpsc::{self, UnboundedReceiver, UnboundedSender}; +use futures_util::{Stream, StreamExt}; +use once_cell::sync::Lazy; +use slab::Slab; + +static SUBSCRIBERS: Lazy>>> = Lazy::new(Default::default); + +struct Senders(Slab>); + +struct BrokerStream(usize, UnboundedReceiver); + +fn with_senders(f: F) -> R +where + T: Sync + Send + Clone + 'static, + F: FnOnce(&mut Senders) -> R, +{ + let mut map = SUBSCRIBERS.lock().unwrap(); + let senders = map + .entry(TypeId::of::>()) + .or_insert_with(|| Box::new(Senders::(Default::default()))); + f(senders.downcast_mut::>().unwrap()) +} + +impl Drop for BrokerStream { + fn drop(&mut self) { + with_senders::(|senders| senders.0.remove(self.0)); + } +} + +impl Stream for BrokerStream { + type Item = T; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.1.poll_next_unpin(cx) + } +} + +/// A simple broker based on memory +pub struct SimpleBroker(PhantomData); + +impl SimpleBroker { + /// Publish a message that all subscription streams can receive. + pub fn publish(msg: T) { + with_senders::(|senders| { + for (_, sender) in senders.0.iter_mut() { + sender.start_send(msg.clone()).ok(); + } + }); + } + + /// Subscribe to the message of the specified type and returns a `Stream`. + pub fn subscribe() -> impl Stream { + with_senders::(|senders| { + let (tx, rx) = mpsc::unbounded(); + let id = senders.0.insert(tx); + BrokerStream(id, rx) + }) + } +} diff --git a/models/dynamic-files/Cargo.toml b/models/dynamic-files/Cargo.toml new file mode 100644 index 0000000..208e619 --- /dev/null +++ b/models/dynamic-files/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "dynamic-files" +version = "0.1.0" +edition = "2024" + +[dependencies] +async-graphql = { path = "../../.." } +slab = "0.4.9" +futures = "0.3.30" diff --git a/models/dynamic-files/src/lib.rs b/models/dynamic-files/src/lib.rs new file mode 100644 index 0000000..6f1bd0d --- /dev/null +++ b/models/dynamic-files/src/lib.rs @@ -0,0 +1,108 @@ +use async_graphql::{ + Value, + dynamic::{Field, FieldFuture, FieldValue, InputValue, Object, Schema, SchemaError, TypeRef}, +}; +use futures::lock::Mutex; +use slab::Slab; + +pub type Storage = Mutex>; + +#[derive(Clone)] +pub struct FileInfo { + pub id: String, + url: String, +} + +pub fn schema() -> Result { + let file_info = Object::new("FileInfo") + .field(Field::new("id", TypeRef::named_nn(TypeRef::ID), |ctx| { + FieldFuture::new(async { + let file_info = ctx.parent_value.try_downcast_ref::()?; + Ok(Some(Value::from(&file_info.id))) + }) + })) + .field(Field::new( + "url", + TypeRef::named_nn(TypeRef::STRING), + |ctx| { + FieldFuture::new(async { + let file_info = ctx.parent_value.try_downcast_ref::()?; + Ok(Some(Value::from(&file_info.url))) + }) + }, + )); + + let query = Object::new("Query").field(Field::new( + "uploads", + TypeRef::named_nn_list_nn(file_info.type_name()), + |ctx| { + FieldFuture::new(async move { + let storage = ctx.data_unchecked::().lock().await; + Ok(Some(FieldValue::list( + storage + .iter() + .map(|(_, file)| FieldValue::owned_any(file.clone())), + ))) + }) + }, + )); + + let mutation = Object::new("Mutation") + .field( + Field::new( + "singleUpload", + TypeRef::named_nn(file_info.type_name()), + |ctx| { + FieldFuture::new(async move { + let mut storage = ctx.data_unchecked::().lock().await; + let file = ctx.args.try_get("file")?.upload()?; + let entry = storage.vacant_entry(); + let upload = file.value(&ctx).unwrap(); + let info = FileInfo { + id: entry.key().to_string(), + url: upload.filename.clone(), + }; + entry.insert(info.clone()); + Ok(Some(FieldValue::owned_any(info))) + }) + }, + ) + .argument(InputValue::new("file", TypeRef::named_nn(TypeRef::UPLOAD))), + ) + .field( + Field::new( + "multipleUpload", + TypeRef::named_nn_list_nn(file_info.type_name()), + |ctx| { + FieldFuture::new(async move { + let mut infos = Vec::new(); + let mut storage = ctx.data_unchecked::().lock().await; + for item in ctx.args.try_get("files")?.list()?.iter() { + let file = item.upload()?; + let entry = storage.vacant_entry(); + let upload = file.value(&ctx).unwrap(); + let info = FileInfo { + id: entry.key().to_string(), + url: upload.filename.clone(), + }; + entry.insert(info.clone()); + infos.push(FieldValue::owned_any(info)) + } + Ok(Some(infos)) + }) + }, + ) + .argument(InputValue::new( + "files", + TypeRef::named_nn_list_nn(TypeRef::UPLOAD), + )), + ); + + Schema::build(query.type_name(), Some(mutation.type_name()), None) + .enable_uploading() + .register(file_info) + .register(query) + .register(mutation) + .data(Storage::default()) + .finish() +} diff --git a/models/dynamic-starwars/Cargo.toml b/models/dynamic-starwars/Cargo.toml new file mode 100644 index 0000000..933e3d7 --- /dev/null +++ b/models/dynamic-starwars/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "dynamic-starwars" +version = "0.1.0" +edition = "2024" + +[dependencies] +async-graphql = { path = "../../..", features = ["dynamic-schema"] } +slab = "0.4.9" diff --git a/models/dynamic-starwars/src/lib.rs b/models/dynamic-starwars/src/lib.rs new file mode 100644 index 0000000..43c0fc3 --- /dev/null +++ b/models/dynamic-starwars/src/lib.rs @@ -0,0 +1,169 @@ +mod model; + +use std::collections::HashMap; + +pub use model::schema; +use slab::Slab; + +/// One of the films in the Star Wars Trilogy +#[derive(Copy, Clone, Eq, PartialEq)] +pub enum Episode { + /// Released in 1977. + NewHope, + + /// Released in 1980. + Empire, + + /// Released in 1983. + Jedi, +} + +pub struct StarWarsChar { + id: &'static str, + name: &'static str, + is_human: bool, + friends: Vec, + appears_in: Vec, + home_planet: Option<&'static str>, + primary_function: Option<&'static str>, +} + +pub struct StarWars { + luke: usize, + artoo: usize, + chars: Slab, + chars_by_id: HashMap<&'static str, usize>, +} + +impl StarWars { + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + let mut chars = Slab::new(); + + let luke = chars.insert(StarWarsChar { + id: "1000", + name: "Luke Skywalker", + is_human: true, + friends: vec![], + appears_in: vec![], + home_planet: Some("Tatooine"), + primary_function: None, + }); + + let vader = chars.insert(StarWarsChar { + id: "1001", + name: "Anakin Skywalker", + is_human: true, + friends: vec![], + appears_in: vec![], + home_planet: Some("Tatooine"), + primary_function: None, + }); + + let han = chars.insert(StarWarsChar { + id: "1002", + name: "Han Solo", + is_human: true, + friends: vec![], + appears_in: vec![Episode::Empire, Episode::NewHope, Episode::Jedi], + home_planet: None, + primary_function: None, + }); + + let leia = chars.insert(StarWarsChar { + id: "1003", + name: "Leia Organa", + is_human: true, + friends: vec![], + appears_in: vec![Episode::Empire, Episode::NewHope, Episode::Jedi], + home_planet: Some("Alderaa"), + primary_function: None, + }); + + let tarkin = chars.insert(StarWarsChar { + id: "1004", + name: "Wilhuff Tarkin", + is_human: true, + friends: vec![], + appears_in: vec![Episode::Empire, Episode::NewHope, Episode::Jedi], + home_planet: None, + primary_function: None, + }); + + let threepio = chars.insert(StarWarsChar { + id: "2000", + name: "C-3PO", + is_human: false, + friends: vec![], + appears_in: vec![Episode::Empire, Episode::NewHope, Episode::Jedi], + home_planet: None, + primary_function: Some("Protocol"), + }); + + let artoo = chars.insert(StarWarsChar { + id: "2001", + name: "R2-D2", + is_human: false, + friends: vec![], + appears_in: vec![Episode::Empire, Episode::NewHope, Episode::Jedi], + home_planet: None, + primary_function: Some("Astromech"), + }); + + chars[luke].friends = vec![han, leia, threepio, artoo]; + chars[vader].friends = vec![tarkin]; + chars[han].friends = vec![luke, leia, artoo]; + chars[leia].friends = vec![luke, han, threepio, artoo]; + chars[tarkin].friends = vec![vader]; + chars[threepio].friends = vec![luke, han, leia, artoo]; + chars[artoo].friends = vec![luke, han, leia]; + + let chars_by_id = chars.iter().map(|(idx, ch)| (ch.id, idx)).collect(); + Self { + luke, + artoo, + chars, + chars_by_id, + } + } + + pub fn human(&self, id: &str) -> Option<&StarWarsChar> { + self.chars_by_id + .get(id) + .copied() + .map(|idx| self.chars.get(idx).unwrap()) + .filter(|ch| ch.is_human) + } + + pub fn droid(&self, id: &str) -> Option<&StarWarsChar> { + self.chars_by_id + .get(id) + .copied() + .map(|idx| self.chars.get(idx).unwrap()) + .filter(|ch| !ch.is_human) + } + + pub fn humans(&self) -> Vec<&StarWarsChar> { + self.chars + .iter() + .filter(|(_, ch)| ch.is_human) + .map(|(_, ch)| ch) + .collect() + } + + pub fn droids(&self) -> Vec<&StarWarsChar> { + self.chars + .iter() + .filter(|(_, ch)| !ch.is_human) + .map(|(_, ch)| ch) + .collect() + } + + pub fn friends(&self, ch: &StarWarsChar) -> Vec<&StarWarsChar> { + ch.friends + .iter() + .copied() + .filter_map(|id| self.chars.get(id)) + .collect() + } +} diff --git a/models/dynamic-starwars/src/model.rs b/models/dynamic-starwars/src/model.rs new file mode 100644 index 0000000..b69706b --- /dev/null +++ b/models/dynamic-starwars/src/model.rs @@ -0,0 +1,275 @@ +use async_graphql::{Value, dynamic::*}; + +use crate::{Episode, StarWars, StarWarsChar}; + +impl From for FieldValue<'_> { + fn from(value: Episode) -> Self { + match value { + Episode::NewHope => FieldValue::value("NEW_HOPE"), + Episode::Empire => FieldValue::value("EMPIRE"), + Episode::Jedi => FieldValue::value("JEDI"), + } + } +} + +pub fn schema() -> Result { + let episode = Enum::new("Episode") + .item(EnumItem::new("NEW_HOPE").description("Released in 1977.")) + .item(EnumItem::new("EMPIRE").description("Released in 1980.")) + .item(EnumItem::new("JEDI").description("Released in 1983.")); + + let character = Interface::new("Character") + .field(InterfaceField::new( + "id", + TypeRef::named_nn(TypeRef::STRING), + )) + .field(InterfaceField::new( + "name", + TypeRef::named_nn(TypeRef::STRING), + )) + .field(InterfaceField::new( + "friends", + TypeRef::named_nn_list_nn("Character"), + )) + .field(InterfaceField::new( + "appearsIn", + TypeRef::named_nn_list_nn(episode.type_name()), + )); + + let human = Object::new("Human") + .description("A humanoid creature in the Star Wars universe.") + .implement(character.type_name()) + .field( + Field::new("id", TypeRef::named_nn(TypeRef::STRING), |ctx| { + FieldFuture::new(async move { + let char = ctx.parent_value.try_downcast_ref::()?; + Ok(Some(Value::from(char.id))) + }) + }) + .description("The id of the human."), + ) + .field( + Field::new("name", TypeRef::named_nn(TypeRef::STRING), |ctx| { + FieldFuture::new(async move { + let char = ctx.parent_value.try_downcast_ref::()?; + Ok(Some(Value::from(char.name))) + }) + }) + .description("The name of the human."), + ) + .field( + Field::new( + "friends", + TypeRef::named_nn_list_nn(character.type_name()), + |ctx| { + FieldFuture::new(async move { + let char = ctx.parent_value.try_downcast_ref::()?; + let starwars = ctx.data::()?; + let friends = starwars.friends(char); + Ok(Some(FieldValue::list(friends.into_iter().map(|friend| { + FieldValue::borrowed_any(friend).with_type(if friend.is_human { + "Human" + } else { + "Droid" + }) + })))) + }) + }, + ) + .description("The friends of the human, or an empty list if they have none."), + ) + .field( + Field::new( + "appearsIn", + TypeRef::named_nn_list_nn(episode.type_name()), + |ctx| { + FieldFuture::new(async move { + let char = ctx.parent_value.try_downcast_ref::()?; + Ok(Some(FieldValue::list( + char.appears_in.iter().copied().map(FieldValue::from), + ))) + }) + }, + ) + .description("Which movies they appear in."), + ) + .field( + Field::new("homePlanet", TypeRef::named(TypeRef::STRING), |ctx| { + FieldFuture::new(async move { + let char = ctx.parent_value.try_downcast_ref::()?; + Ok(char.home_planet.map(Value::from)) + }) + }) + .description("The home planet of the human, or null if unknown."), + ); + + let droid = Object::new("Droid") + .description("A mechanical creature in the Star Wars universe.") + .implement(character.type_name()) + .field( + Field::new("id", TypeRef::named_nn(TypeRef::STRING), |ctx| { + FieldFuture::new(async move { + let char = ctx.parent_value.try_downcast_ref::()?; + Ok(Some(Value::from(char.id))) + }) + }) + .description("The id of the droid."), + ) + .field( + Field::new("name", TypeRef::named_nn(TypeRef::STRING), |ctx| { + FieldFuture::new(async move { + let char = ctx.parent_value.try_downcast_ref::()?; + Ok(Some(Value::from(char.name))) + }) + }) + .description("The name of the droid."), + ) + .field( + Field::new( + "friends", + TypeRef::named_nn_list_nn(character.type_name()), + |ctx| { + FieldFuture::new(async move { + let char = ctx.parent_value.try_downcast_ref::()?; + let starwars = ctx.data::()?; + let friends = starwars.friends(char); + Ok(Some(FieldValue::list(friends.into_iter().map(|friend| { + FieldValue::borrowed_any(friend).with_type(if friend.is_human { + "Human" + } else { + "Droid" + }) + })))) + }) + }, + ) + .description("The friends of the droid, or an empty list if they have none."), + ) + .field( + Field::new( + "appearsIn", + TypeRef::named_nn_list_nn(episode.type_name()), + |ctx| { + FieldFuture::new(async move { + let char = ctx.parent_value.try_downcast_ref::()?; + Ok(Some(FieldValue::list( + char.appears_in.iter().copied().map(FieldValue::from), + ))) + }) + }, + ) + .description("Which movies they appear in."), + ) + .field( + Field::new("primaryFunction", TypeRef::named(TypeRef::STRING), |ctx| { + FieldFuture::new(async move { + let char = ctx.parent_value.try_downcast_ref::()?; + Ok(char.primary_function.map(Value::from)) + }) + }) + .description("The primary function of the droid."), + ); + + let query = Object::new("Query") + .field( + Field::new("hero", TypeRef::named_nn(character.type_name()), |ctx| { + FieldFuture::new(async move { + let starwars = ctx.data::()?; + let episode = match ctx.args.get("episode") { + Some(episode) => Some(match episode.enum_name()? { + "NEW_HOPE" => Episode::NewHope, + "EMPIRE" => Episode::Empire, + "JEDI" => Episode::Jedi, + _ => unreachable!(), + }), + None => None, + }; + let value = match episode { + Some(episode) => { + if episode == Episode::Empire { + FieldValue::borrowed_any(starwars.chars.get(starwars.luke).unwrap()) + .with_type("Human") + } else { + FieldValue::borrowed_any( + starwars.chars.get(starwars.artoo).unwrap(), + ) + .with_type("Droid") + } + } + None => { + FieldValue::borrowed_any(starwars.chars.get(starwars.luke).unwrap()) + .with_type("Human") + } + }; + Ok(Some(value)) + }) + }) + .argument(InputValue::new( + "episode", + TypeRef::named(episode.type_name()), + )), + ) + .field( + Field::new("human", TypeRef::named(human.type_name()), |ctx| { + FieldFuture::new(async move { + let starwars = ctx.data::()?; + let id = ctx.args.try_get("id")?; + Ok(starwars + .human(id.string()?) + .map(|human| FieldValue::borrowed_any(human))) + }) + }) + .argument(InputValue::new("id", TypeRef::named_nn(TypeRef::STRING))), + ) + .field(Field::new( + "humans", + TypeRef::named_nn_list_nn(human.type_name()), + |ctx| { + FieldFuture::new(async move { + let starwars = ctx.data::()?; + let humans = starwars.humans(); + Ok(Some(FieldValue::list( + humans + .into_iter() + .map(|human| FieldValue::borrowed_any(human)), + ))) + }) + }, + )) + .field( + Field::new("droid", TypeRef::named(human.type_name()), |ctx| { + FieldFuture::new(async move { + let starwars = ctx.data::()?; + let id = ctx.args.try_get("id")?; + Ok(starwars + .droid(id.string()?) + .map(|droid| FieldValue::borrowed_any(droid))) + }) + }) + .argument(InputValue::new("id", TypeRef::named_nn(TypeRef::STRING))), + ) + .field(Field::new( + "droids", + TypeRef::named_nn_list_nn(human.type_name()), + |ctx| { + FieldFuture::new(async move { + let starwars = ctx.data::()?; + let droids = starwars.droids(); + Ok(Some(FieldValue::list( + droids + .into_iter() + .map(|droid| FieldValue::borrowed_any(droid)), + ))) + }) + }, + )); + + Schema::build(query.type_name(), None, None) + .register(episode) + .register(character) + .register(human) + .register(droid) + .register(query) + .data(StarWars::new()) + .finish() +} diff --git a/models/files/Cargo.toml b/models/files/Cargo.toml index a08696d..bc3e979 100644 --- a/models/files/Cargo.toml +++ b/models/files/Cargo.toml @@ -2,9 +2,9 @@ name = "files" version = "0.1.1" authors = ["sunli "] -edition = "2021" +edition = "2024" [dependencies] async-graphql = { path = "../../.." } -slab = "0.4.2" -futures = "0.3.0" +slab = "0.4.9" +futures = "0.3.30" diff --git a/models/files/src/lib.rs b/models/files/src/lib.rs index da273ef..5f19a21 100644 --- a/models/files/src/lib.rs +++ b/models/files/src/lib.rs @@ -1,4 +1,4 @@ -use async_graphql::{Context, EmptySubscription, Object, Schema, SimpleObject, Upload, ID}; +use async_graphql::{Context, EmptySubscription, ID, Object, Schema, SimpleObject, Upload}; use futures::lock::Mutex; use slab::Slab; @@ -28,7 +28,6 @@ pub struct MutationRoot; impl MutationRoot { async fn single_upload(&self, ctx: &Context<'_>, file: Upload) -> FileInfo { let mut storage = ctx.data_unchecked::().lock().await; - println!("files count: {}", storage.len()); let entry = storage.vacant_entry(); let upload = file.value(ctx).unwrap(); let info = FileInfo { diff --git a/models/starwars/Cargo.toml b/models/starwars/Cargo.toml index ebf5ba6..72fa92e 100644 --- a/models/starwars/Cargo.toml +++ b/models/starwars/Cargo.toml @@ -2,8 +2,8 @@ name = "starwars" version = "0.1.1" authors = ["sunli "] -edition = "2021" +edition = "2024" [dependencies] async-graphql = { path = "../../.." } -slab = "0.4.2" +slab = "0.4.9" diff --git a/models/starwars/src/model.rs b/models/starwars/src/model.rs index 160bd72..2f569c9 100644 --- a/models/starwars/src/model.rs +++ b/models/starwars/src/model.rs @@ -1,8 +1,8 @@ #![allow(clippy::needless_lifetimes)] use async_graphql::{ - connection::{query, Connection, Edge}, Context, Enum, Error, Interface, Object, OutputType, Result, + connection::{Connection, Edge, query}, }; use super::StarWars; @@ -119,8 +119,8 @@ impl QueryRoot { ) -> Character<'a> { let star_wars = ctx.data_unchecked::(); match episode { - Some(episode_name) => { - if episode_name == Episode::Empire { + Some(episode) => { + if episode == Episode::Empire { Human(star_wars.chars.get(star_wars.luke).unwrap()).into() } else { Droid(star_wars.chars.get(star_wars.artoo).unwrap()).into() @@ -172,11 +172,12 @@ impl QueryRoot { } #[derive(Interface)] +#[allow(clippy::duplicated_attributes)] #[graphql( - field(name = "id", type = "&str"), - field(name = "name", type = "&str"), - field(name = "friends", type = "Vec>"), - field(name = "appears_in", type = "&[Episode]") + field(name = "id", ty = "&str"), + field(name = "name", ty = "&str"), + field(name = "friends", ty = "Vec>"), + field(name = "appears_in", ty = "&[Episode]") )] pub enum Character<'a> { Human(Human<'a>), @@ -233,7 +234,7 @@ where slice .iter() .enumerate() - .map(|(idx, item)| Edge::new(start + idx, (map_to)(*item))), + .map(|(idx, item)| Edge::new(start + idx, (map_to)(item))), ); Ok::<_, Error>(connection) }, diff --git a/models/token/Cargo.toml b/models/token/Cargo.toml index 1050312..cde96e9 100644 --- a/models/token/Cargo.toml +++ b/models/token/Cargo.toml @@ -1,10 +1,10 @@ [package] name = "token" version = "0.1.0" -edition = "2021" +edition = "2024" [dependencies] async-graphql = { path = "../../.." } -futures-util = "0.3.0" +futures-util = "0.3.30" serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } diff --git a/poem/dynamic-books/Cargo.toml b/poem/dynamic-books/Cargo.toml new file mode 100644 index 0000000..0acac2c --- /dev/null +++ b/poem/dynamic-books/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "poem-dynamic-books" +version = "0.1.0" +edition = "2024" + +[dependencies] +async-graphql = { path = "../../..", features = ["dynamic-schema"] } +async-graphql-poem = { path = "../../../integrations/poem" } +tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] } +dynamic-books = { path = "../../models/dynamic-books" } +poem = "3.0.0" diff --git a/poem/dynamic-books/src/main.rs b/poem/dynamic-books/src/main.rs new file mode 100644 index 0000000..c16c538 --- /dev/null +++ b/poem/dynamic-books/src/main.rs @@ -0,0 +1,27 @@ +use async_graphql::http::GraphiQLSource; +use async_graphql_poem::{GraphQL, GraphQLSubscription}; +use poem::{IntoResponse, Route, Server, get, handler, listener::TcpListener, web::Html}; + +#[handler] +async fn graphiql() -> impl IntoResponse { + Html( + GraphiQLSource::build() + .endpoint("/") + .subscription_endpoint("/ws") + .finish(), + ) +} + +#[tokio::main] +async fn main() { + let schema = dynamic_books::schema().unwrap(); + let app = Route::new() + .at("/", get(graphiql).post(GraphQL::new(schema.clone()))) + .at("/ws", get(GraphQLSubscription::new(schema))); + + println!("GraphiQL IDE: http://localhost:8080"); + Server::new(TcpListener::bind("127.0.0.1:8080")) + .run(app) + .await + .unwrap(); +} diff --git a/poem/dynamic-schema/Cargo.toml b/poem/dynamic-schema/Cargo.toml index 4060a9e..84fdb4c 100644 --- a/poem/dynamic-schema/Cargo.toml +++ b/poem/dynamic-schema/Cargo.toml @@ -1,10 +1,10 @@ [package] name = "poem-dynamic-schema" version = "0.1.0" -edition = "2021" +edition = "2024" [dependencies] async-graphql = { path = "../../..", features = ["dynamic-schema"] } async-graphql-poem = { path = "../../../integrations/poem" } -tokio = { version = "1.8", features = ["macros", "rt-multi-thread"] } -poem = "1.3.48" +tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] } +poem = { version = "3.0.0" } diff --git a/poem/dynamic-schema/src/main.rs b/poem/dynamic-schema/src/main.rs index 22d983c..8403a98 100644 --- a/poem/dynamic-schema/src/main.rs +++ b/poem/dynamic-schema/src/main.rs @@ -1,12 +1,12 @@ use std::error::Error; use async_graphql::{ - dynamic::*, - http::{playground_source, GraphQLPlaygroundConfig}, Value, + dynamic::*, + http::{GraphQLPlaygroundConfig, playground_source}, }; use async_graphql_poem::GraphQL; -use poem::{get, handler, listener::TcpListener, web::Html, IntoResponse, Route, Server}; +use poem::{IntoResponse, Route, Server, get, handler, listener::TcpListener, web::Html}; #[handler] async fn graphql_playground() -> impl IntoResponse { @@ -15,9 +15,10 @@ async fn graphql_playground() -> impl IntoResponse { #[tokio::main] async fn main() -> Result<(), Box> { - let query = Object::new("Query").field(Field::new("value", TypeRef::INT, |_| { - FieldFuture::new(async { Ok(Some(Value::from(100))) }) - })); + let query = + Object::new("Query").field(Field::new("value", TypeRef::named_nn(TypeRef::INT), |_| { + FieldFuture::new(async { Ok(Some(Value::from(100))) }) + })); let schema = Schema::build(query.type_name(), None, None) .register(query) .finish()?; diff --git a/poem/dynamic-starwars/Cargo.toml b/poem/dynamic-starwars/Cargo.toml new file mode 100644 index 0000000..6ccc630 --- /dev/null +++ b/poem/dynamic-starwars/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "poem-dynamic-starwars" +version = "0.1.0" +edition = "2024" + +[dependencies] +async-graphql = { path = "../../..", features = ["dynamic-schema"] } +async-graphql-poem = { path = "../../../integrations/poem" } +tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] } +dynamic-starwars = { path = "../../models/dynamic-starwars" } +poem = "3.0.0" diff --git a/poem/dynamic-starwars/src/main.rs b/poem/dynamic-starwars/src/main.rs new file mode 100644 index 0000000..11b2063 --- /dev/null +++ b/poem/dynamic-starwars/src/main.rs @@ -0,0 +1,22 @@ +use async_graphql::http::GraphiQLSource; +use async_graphql_poem::GraphQL; +use poem::{IntoResponse, Route, Server, get, handler, listener::TcpListener, web::Html}; + +#[handler] +async fn graphiql() -> impl IntoResponse { + Html(GraphiQLSource::build().endpoint("/").finish()) +} + +#[tokio::main] +async fn main() { + let app = Route::new().at( + "/", + get(graphiql).post(GraphQL::new(dynamic_starwars::schema().unwrap())), + ); + + println!("GraphiQL IDE: http://localhost:8000"); + Server::new(TcpListener::bind("127.0.0.1:8000")) + .run(app) + .await + .unwrap(); +} diff --git a/poem/dynamic-upload/Cargo.toml b/poem/dynamic-upload/Cargo.toml new file mode 100644 index 0000000..4211e9d --- /dev/null +++ b/poem/dynamic-upload/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "dynamic-upload" +version = "0.1.0" +edition = "2024" + +[dependencies] +async-graphql = { path = "../../..", features = ["dynamic-schema"] } +async-graphql-poem = { path = "../../../integrations/poem" } +tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] } +dynamic-files = { path = "../../models/dynamic-files" } +poem = "3.0.0" diff --git a/poem/dynamic-upload/src/main.rs b/poem/dynamic-upload/src/main.rs new file mode 100644 index 0000000..81a583c --- /dev/null +++ b/poem/dynamic-upload/src/main.rs @@ -0,0 +1,22 @@ +use async_graphql::http::GraphiQLSource; +use async_graphql_poem::GraphQL; +use poem::{IntoResponse, Route, Server, get, handler, listener::TcpListener, web::Html}; + +#[handler] +async fn graphiql() -> impl IntoResponse { + Html(GraphiQLSource::build().endpoint("/").finish()) +} + +#[tokio::main] +async fn main() { + let app = Route::new().at( + "/", + get(graphiql).post(GraphQL::new(dynamic_files::schema().unwrap())), + ); + + println!("GraphiQL IDE: http://localhost:8000"); + Server::new(TcpListener::bind("0.0.0.0:8000")) + .run(app) + .await + .unwrap(); +} diff --git a/poem/opentelemetry-basic/Cargo.toml b/poem/opentelemetry-basic/Cargo.toml index b7ae51e..f2d3882 100644 --- a/poem/opentelemetry-basic/Cargo.toml +++ b/poem/opentelemetry-basic/Cargo.toml @@ -1,13 +1,15 @@ [package] name = "poem-opentelemetry-basic" version = "0.1.0" -edition = "2021" +edition = "2024" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] async-graphql = { path = "../../..", features = ["opentelemetry"] } async-graphql-poem = { path = "../../../integrations/poem" } -tokio = { version = "1.8", features = ["macros", "rt-multi-thread"] } -poem = "1.3.42" -opentelemetry = { version = "0.18.0", features = ["rt-tokio"] } +tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] } +poem = "3.0.0" +opentelemetry = { version = "0.27.0" } +opentelemetry_sdk = { version = "0.27", features = ["rt-tokio"] } +opentelemetry-stdout = { version = "0.27.0", features = ["trace"] } diff --git a/poem/opentelemetry-basic/src/main.rs b/poem/opentelemetry-basic/src/main.rs index 6647856..e689ca8 100644 --- a/poem/opentelemetry-basic/src/main.rs +++ b/poem/opentelemetry-basic/src/main.rs @@ -1,9 +1,10 @@ use async_graphql::{ - extensions::OpenTelemetry, EmptyMutation, EmptySubscription, Object, Result, Schema, + EmptyMutation, EmptySubscription, Object, Result, Schema, extensions::OpenTelemetry, }; use async_graphql_poem::GraphQL; -use opentelemetry::sdk::export::trace::stdout; -use poem::{listener::TcpListener, post, EndpointExt, Route, Server}; +use opentelemetry::trace::TracerProvider as _; +use opentelemetry_sdk::trace::TracerProvider; +use poem::{EndpointExt, Route, Server, listener::TcpListener, post}; struct QueryRoot; @@ -16,7 +17,10 @@ impl QueryRoot { #[tokio::main] async fn main() { - let tracer = stdout::new_pipeline().install_simple(); + let provider = TracerProvider::builder() + .with_simple_exporter(opentelemetry_stdout::SpanExporter::default()) + .build(); + let tracer = provider.tracer("poem-opentelemetry-basic"); let opentelemetry_extension = OpenTelemetry::new(tracer); let schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription) @@ -33,7 +37,9 @@ async fn main() { -H 'content-type: application/json' \ --data '{ \"query\": \"{ hello }\" }'"; - println!("Run this curl command from another terminal window to see opentelemetry output in this terminal.\n\n{example_curl}\n\n"); + println!( + "Run this curl command from another terminal window to see opentelemetry output in this terminal.\n\n{example_curl}\n\n" + ); Server::new(TcpListener::bind("127.0.0.1:8000")) .run(app) diff --git a/poem/starwars/Cargo.toml b/poem/starwars/Cargo.toml index 06567d1..d521325 100644 --- a/poem/starwars/Cargo.toml +++ b/poem/starwars/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "poem-starwars" version = "0.1.0" -edition = "2021" +edition = "2024" [dependencies] async-graphql = { path = "../../.." } async-graphql-poem = { path = "../../../integrations/poem" } -tokio = { version = "1.8", features = ["macros", "rt-multi-thread"] } +tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] } starwars = { path = "../../models/starwars" } -poem = "1.3.48" +poem = "3.0.0" diff --git a/poem/starwars/src/main.rs b/poem/starwars/src/main.rs index 3519cdb..45f6aad 100644 --- a/poem/starwars/src/main.rs +++ b/poem/starwars/src/main.rs @@ -1,15 +1,11 @@ -use async_graphql::{http::GraphiQLSource, EmptyMutation, EmptySubscription, Schema}; +use async_graphql::{EmptyMutation, EmptySubscription, Schema, http::GraphiQLSource}; use async_graphql_poem::GraphQL; -use poem::{get, handler, listener::TcpListener, web::Html, IntoResponse, Route, Server}; +use poem::{IntoResponse, Route, Server, get, handler, listener::TcpListener, web::Html}; use starwars::{QueryRoot, StarWars}; #[handler] async fn graphiql() -> impl IntoResponse { - Html( - GraphiQLSource::build() - .endpoint("http://localhost:8000") - .finish(), - ) + Html(GraphiQLSource::build().endpoint("/").finish()) } #[tokio::main] diff --git a/poem/subscription-redis/Cargo.toml b/poem/subscription-redis/Cargo.toml index ee2913a..827cbc6 100644 --- a/poem/subscription-redis/Cargo.toml +++ b/poem/subscription-redis/Cargo.toml @@ -1,12 +1,12 @@ [package] name = "subscription-redis" version = "0.1.0" -edition = "2021" +edition = "2024" [dependencies] async-graphql = { path = "../../.." } async-graphql-poem = { path = "../../../integrations/poem" } -tokio = { version = "1.8", features = ["macros", "rt-multi-thread"] } -poem = { version = "1.3.48", features = ["websocket"] } -redis = { version = "0.21.4", features = ["aio", "tokio-comp"] } -futures-util = "0.3.19" +tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] } +poem = { version = "3.0.0", features = ["websocket"] } +redis = { version = "0.27.5", features = ["aio", "tokio-comp"] } +futures-util = "0.3.30" diff --git a/poem/subscription-redis/src/main.rs b/poem/subscription-redis/src/main.rs index a66fade..3a4c335 100644 --- a/poem/subscription-redis/src/main.rs +++ b/poem/subscription-redis/src/main.rs @@ -1,7 +1,7 @@ -use async_graphql::{http::GraphiQLSource, Context, Object, Result, Schema, Subscription}; +use async_graphql::{Context, Object, Result, Schema, Subscription, http::GraphiQLSource}; use async_graphql_poem::{GraphQL, GraphQLSubscription}; use futures_util::{Stream, StreamExt}; -use poem::{get, handler, listener::TcpListener, web::Html, IntoResponse, Route, Server}; +use poem::{IntoResponse, Route, Server, get, handler, listener::TcpListener, web::Html}; use redis::{AsyncCommands, Client}; struct QueryRoot; @@ -19,8 +19,8 @@ struct MutationRoot; impl MutationRoot { async fn publish(&self, ctx: &Context<'_>, value: String) -> Result { let client = ctx.data_unchecked::(); - let mut conn = client.get_async_connection().await?; - conn.publish("values", value).await?; + let mut conn = client.get_multiplexed_async_connection().await?; + conn.publish::<_, _, ()>("values", value).await?; Ok(true) } } @@ -31,7 +31,7 @@ struct SubscriptionRoot; impl SubscriptionRoot { async fn values(&self, ctx: &Context<'_>) -> Result> { let client = ctx.data_unchecked::(); - let mut conn = client.get_async_connection().await?.into_pubsub(); + let mut conn = client.get_async_pubsub().await?; conn.subscribe("values").await?; Ok(conn .into_on_message() @@ -43,8 +43,8 @@ impl SubscriptionRoot { async fn graphiql() -> impl IntoResponse { Html( GraphiQLSource::build() - .endpoint("http://localhost:8000") - .subscription_endpoint("ws://localhost:8000/ws") + .endpoint("/") + .subscription_endpoint("/ws") .finish(), ) } diff --git a/poem/subscription/Cargo.toml b/poem/subscription/Cargo.toml index b80dda2..f024aba 100644 --- a/poem/subscription/Cargo.toml +++ b/poem/subscription/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "poem-subscription" version = "0.1.0" -edition = "2021" +edition = "2024" [dependencies] async-graphql = { path = "../../.." } async-graphql-poem = { path = "../../../integrations/poem" } -tokio = { version = "1.8", features = ["macros", "rt-multi-thread"] } +tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] } books = { path = "../../models/books" } -poem = { version = "1.3.48", features = ["websocket"] } +poem = { version = "3.0.0", features = ["websocket"] } diff --git a/poem/subscription/src/main.rs b/poem/subscription/src/main.rs index 05e00ed..23ff2aa 100644 --- a/poem/subscription/src/main.rs +++ b/poem/subscription/src/main.rs @@ -1,14 +1,14 @@ -use async_graphql::{http::GraphiQLSource, Schema}; +use async_graphql::{Schema, http::GraphiQLSource}; use async_graphql_poem::{GraphQL, GraphQLSubscription}; use books::{MutationRoot, QueryRoot, Storage, SubscriptionRoot}; -use poem::{get, handler, listener::TcpListener, web::Html, IntoResponse, Route, Server}; +use poem::{IntoResponse, Route, Server, get, handler, listener::TcpListener, web::Html}; #[handler] async fn graphiql() -> impl IntoResponse { Html( GraphiQLSource::build() - .endpoint("http://localhost:8000") - .subscription_endpoint("ws://localhost:8000/ws") + .endpoint("/") + .subscription_endpoint("/ws") .finish(), ) } diff --git a/poem/token-from-header/Cargo.toml b/poem/token-from-header/Cargo.toml index bb9a52a..acc10b3 100644 --- a/poem/token-from-header/Cargo.toml +++ b/poem/token-from-header/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "poem-token-from-header" version = "0.1.0" -edition = "2021" +edition = "2024" [dependencies] async-graphql = { path = "../../.." } async-graphql-poem = { path = "../../../integrations/poem" } token = { path = "../../models/token" } -poem = { version = "1.3.48", features = ["websocket"] } -tokio = { version = "1.8", features = ["macros", "rt-multi-thread"] } +poem = { version = "3.0.0", features = ["websocket"] } +tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] } diff --git a/poem/token-from-header/src/main.rs b/poem/token-from-header/src/main.rs index bb9d9ae..7290b85 100644 --- a/poem/token-from-header/src/main.rs +++ b/poem/token-from-header/src/main.rs @@ -1,16 +1,15 @@ use async_graphql::{ - http::{GraphiQLSource, ALL_WEBSOCKET_PROTOCOLS}, EmptyMutation, Schema, + http::{ALL_WEBSOCKET_PROTOCOLS, GraphiQLSource}, }; use async_graphql_poem::{GraphQLProtocol, GraphQLRequest, GraphQLResponse, GraphQLWebSocket}; use poem::{ - get, handler, + EndpointExt, IntoResponse, Route, Server, get, handler, http::HeaderMap, listener::TcpListener, - web::{websocket::WebSocket, Data, Html}, - EndpointExt, IntoResponse, Route, Server, + web::{Data, Html, websocket::WebSocket}, }; -use token::{on_connection_init, QueryRoot, SubscriptionRoot, Token, TokenSchema}; +use token::{QueryRoot, SubscriptionRoot, Token, TokenSchema, on_connection_init}; fn get_token_from_headers(headers: &HeaderMap) -> Option { headers @@ -22,8 +21,8 @@ fn get_token_from_headers(headers: &HeaderMap) -> Option { async fn graphiql() -> impl IntoResponse { Html( GraphiQLSource::build() - .endpoint("http://localhost:8000") - .subscription_endpoint("ws://localhost:8000/ws") + .endpoint("/") + .subscription_endpoint("/ws") .finish(), ) } diff --git a/poem/upload/Cargo.toml b/poem/upload/Cargo.toml index c52f2c3..6498e04 100644 --- a/poem/upload/Cargo.toml +++ b/poem/upload/Cargo.toml @@ -2,11 +2,11 @@ name = "poem-upload" version = "0.1.1" authors = ["sunli "] -edition = "2021" +edition = "2024" [dependencies] async-graphql = { path = "../../.." } async-graphql-poem = { path = "../../../integrations/poem" } -poem = { version = "1.3.48", features = ["websocket"] } +poem = { version = "3.0.0", features = ["websocket"] } files = { path = "../../models/files" } -tokio = { version = "1.8", features = ["macros", "rt-multi-thread"] } +tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] } diff --git a/poem/upload/src/main.rs b/poem/upload/src/main.rs index 35c3c70..807b082 100644 --- a/poem/upload/src/main.rs +++ b/poem/upload/src/main.rs @@ -1,12 +1,11 @@ -use async_graphql::{http::GraphiQLSource, EmptySubscription, Schema}; +use async_graphql::{EmptySubscription, Schema, http::GraphiQLSource}; use async_graphql_poem::{GraphQLRequest, GraphQLResponse}; use files::{FilesSchema, MutationRoot, QueryRoot, Storage}; use poem::{ - get, handler, + EndpointExt, IntoResponse, Route, Server, get, handler, listener::TcpListener, middleware::Cors, web::{Data, Html}, - EndpointExt, IntoResponse, Route, Server, }; #[handler] @@ -16,11 +15,7 @@ async fn index(schema: Data<&FilesSchema>, req: GraphQLRequest) -> GraphQLRespon #[handler] async fn graphiql() -> impl IntoResponse { - Html( - GraphiQLSource::build() - .endpoint("http://localhost:8000") - .finish(), - ) + Html(GraphiQLSource::build().endpoint("/").finish()) } #[tokio::main] diff --git a/rocket/starwars/Cargo.toml b/rocket/starwars/Cargo.toml index a8234f6..1ef2b8c 100644 --- a/rocket/starwars/Cargo.toml +++ b/rocket/starwars/Cargo.toml @@ -2,10 +2,10 @@ name = "rocket-starwars" version = "0.1.1" authors = ["Daniel Wiesenberg "] -edition = "2021" +edition = "2024" [dependencies] async-graphql = { path = "../../.." } async-graphql-rocket = { path = "../../../integrations/rocket" } -rocket = { version = "0.5.0-rc.2", default-features = false } +rocket = { version = "0.5.0", default-features = false } starwars = { path = "../../models/starwars" } diff --git a/rocket/starwars/src/main.rs b/rocket/starwars/src/main.rs index 07fed6b..4949619 100644 --- a/rocket/starwars/src/main.rs +++ b/rocket/starwars/src/main.rs @@ -1,6 +1,6 @@ -use async_graphql::{http::GraphiQLSource, EmptyMutation, EmptySubscription, Schema}; +use async_graphql::{EmptyMutation, EmptySubscription, Schema, http::GraphiQLSource}; use async_graphql_rocket::{GraphQLQuery, GraphQLRequest, GraphQLResponse}; -use rocket::{response::content, routes, State}; +use rocket::{State, response::content, routes}; use starwars::{QueryRoot, StarWars}; pub type StarWarsSchema = Schema; diff --git a/rocket/upload/Cargo.toml b/rocket/upload/Cargo.toml index a5eca42..b39af4c 100644 --- a/rocket/upload/Cargo.toml +++ b/rocket/upload/Cargo.toml @@ -2,10 +2,10 @@ name = "rocket-upload" version = "0.1.1" authors = ["Daniel Wiesenberg "] -edition = "2021" +edition = "2024" [dependencies] async-graphql = { path = "../../.." } async-graphql-rocket = { path = "../../../integrations/rocket" } -rocket = { version = "0.5.0-rc.2", default-features = false } +rocket = { version = "0.5.0", default-features = false } files = { path = "../../models/files" } diff --git a/rocket/upload/src/main.rs b/rocket/upload/src/main.rs index 501cca5..029959b 100644 --- a/rocket/upload/src/main.rs +++ b/rocket/upload/src/main.rs @@ -1,7 +1,7 @@ -use async_graphql::{http::GraphiQLSource, EmptyMutation, EmptySubscription, Schema}; +use async_graphql::{EmptyMutation, EmptySubscription, Schema, http::GraphiQLSource}; use async_graphql_rocket::{GraphQLQuery, GraphQLRequest, GraphQLResponse}; use files::{FilesSchema, MutationRoot, QueryRoot, Storage}; -use rocket::{response::content, routes, State}; +use rocket::{State, response::content, routes}; pub type StarWarsSchema = Schema; diff --git a/tide/dataloader-postgres/Cargo.toml b/tide/dataloader-postgres/Cargo.toml index e4881ee..8d60f87 100644 --- a/tide/dataloader-postgres/Cargo.toml +++ b/tide/dataloader-postgres/Cargo.toml @@ -7,12 +7,14 @@ edition = "2018" [dependencies] async-graphql = { path = "../../..", features = ["dataloader"] } async-graphql-tide = { path = "../../../integrations/tide" } -async-std = "1.9.0" -async-trait = "0.1.42" -itertools = "0.10.0" -sqlx = { version = "0.5.5", features = ["runtime-async-std-rustls", "postgres"] } +async-std = "1.12.0" +itertools = "0.12.1" +sqlx = { version = "0.7.4", features = [ + "runtime-async-std-rustls", + "postgres", +] } tide = "0.16.0" [dev-dependencies] -serde_json = "1.0.61" -surf = "2.1.0" +serde_json = "1.0.115" +surf = "2.3.2" diff --git a/tide/dataloader-postgres/src/main.rs b/tide/dataloader-postgres/src/main.rs index c3f9f15..26d22a6 100644 --- a/tide/dataloader-postgres/src/main.rs +++ b/tide/dataloader-postgres/src/main.rs @@ -7,9 +7,7 @@ use async_graphql::{ Context, EmptyMutation, EmptySubscription, FieldError, Object, Result, Schema, SimpleObject, }; use async_std::task; -use async_trait::async_trait; -use itertools::Itertools; -use sqlx::{Pool, Postgres}; +use sqlx::PgPool; use tide::{http::mime, Body, Response, StatusCode}; #[derive(sqlx::FromRow, Clone, SimpleObject)] @@ -19,15 +17,14 @@ pub struct Book { author: String, } -pub struct BookLoader(Pool); +pub struct BookLoader(PgPool); impl BookLoader { - fn new(postgres_pool: Pool) -> Self { + fn new(postgres_pool: PgPool) -> Self { Self(postgres_pool) } } -#[async_trait] impl Loader for BookLoader { type Value = Book; type Error = FieldError; @@ -39,15 +36,14 @@ impl Loader for BookLoader { return Err("MOCK DBError".into()); } - let query = format!( - "SELECT id, name, author FROM books WHERE id IN ({})", - keys.iter().join(",") - ); - Ok(sqlx::query_as(&query) - .fetch(&self.0) - .map_ok(|book: Book| (book.id, book)) - .try_collect() - .await?) + Ok( + sqlx::query_as("SELECT id, name, author FROM books WHERE id = ANY($1)") + .bind(keys) + .fetch(&self.0) + .map_ok(|book: Book| (book.id, book)) + .try_collect() + .await?, + ) } } @@ -68,7 +64,7 @@ fn main() -> Result<()> { } async fn run() -> Result<()> { - let postgres_pool: Pool = Pool::connect(&env::var("DATABASE_URL")?).await?; + let postgres_pool = PgPool::connect(&env::var("DATABASE_URL")?).await?; sqlx::query( r#" @@ -106,9 +102,7 @@ async fn run() -> Result<()> { app.at("/").get(|_| async move { let mut resp = Response::new(StatusCode::Ok); resp.set_body(Body::from_string( - GraphiQLSource::build() - .endpoint("http://localhost:8000/graphql") - .finish(), + GraphiQLSource::build().endpoint("/graphql").finish(), )); resp.set_content_type(mime::HTML); Ok(resp) diff --git a/tide/dataloader/Cargo.toml b/tide/dataloader/Cargo.toml index ec7da1d..bbd1808 100644 --- a/tide/dataloader/Cargo.toml +++ b/tide/dataloader/Cargo.toml @@ -8,11 +8,10 @@ edition = "2018" async-graphql = { path = "../../..", features = ["dataloader"] } async-graphql-tide = { path = "../../../integrations/tide" } tide = "0.16" -async-std = "1.9.0" -sqlx = { version = "0.5.5", features = ["sqlite", "runtime-async-std-rustls"] } -async-trait = "0.1.30" -itertools = "0.9.0" +async-std = "1.12.0" +sqlx = { version = "0.7.4", features = ["sqlite", "runtime-async-std-rustls"] } +itertools = "0.12.1" [dev-dependencies] -serde_json = "1.0.51" -surf = "2.0.0-alpha.1" +serde_json = "1.0.115" +surf = "2.3.2" diff --git a/tide/dataloader/src/main.rs b/tide/dataloader/src/main.rs index 7c0c862..a30f083 100644 --- a/tide/dataloader/src/main.rs +++ b/tide/dataloader/src/main.rs @@ -7,7 +7,6 @@ use async_graphql::{ Context, EmptyMutation, EmptySubscription, FieldError, Object, Result, Schema, SimpleObject, }; use async_std::task; -use async_trait::async_trait; use itertools::Itertools; use sqlx::{Pool, Sqlite}; use tide::{http::mime, Body, Response, StatusCode}; @@ -27,7 +26,6 @@ impl BookLoader { } } -#[async_trait] impl Loader for BookLoader { type Value = Book; type Error = FieldError; @@ -105,9 +103,7 @@ async fn run() -> Result<()> { app.at("/").get(|_| async move { let mut resp = Response::new(StatusCode::Ok); resp.set_body(Body::from_string( - GraphiQLSource::build() - .endpoint("http://localhost:8000/graphql") - .finish(), + GraphiQLSource::build().endpoint("/graphql").finish(), )); resp.set_content_type(mime::HTML); Ok(resp) diff --git a/tide/starwars/Cargo.toml b/tide/starwars/Cargo.toml index 3dd2845..c619a27 100644 --- a/tide/starwars/Cargo.toml +++ b/tide/starwars/Cargo.toml @@ -8,9 +8,9 @@ edition = "2018" async-graphql = { path = "../../.." } async-graphql-tide = { path = "../../../integrations/tide" } tide = "0.16" -async-std = "1.9.0" +async-std = "1.12.0" starwars = { path = "../../models/starwars" } [dev-dependencies] -serde_json = "1.0.51" -surf = "2.0.0-alpha.5" +serde_json = "1.0.115" +surf = "2.3.2" diff --git a/tide/subscription/Cargo.toml b/tide/subscription/Cargo.toml index 3f05f52..50b0d66 100644 --- a/tide/subscription/Cargo.toml +++ b/tide/subscription/Cargo.toml @@ -9,4 +9,4 @@ async-graphql = { path = "../../.." } async-graphql-tide = { path = "../../../integrations/tide" } books = { path = "../../models/books" } tide = "0.16" -async-std = "1.9.0" +async-std = "1.12.0" diff --git a/tide/subscription/src/main.rs b/tide/subscription/src/main.rs index 84a8410..a95559f 100644 --- a/tide/subscription/src/main.rs +++ b/tide/subscription/src/main.rs @@ -26,15 +26,15 @@ async fn run() -> Result<()> { let mut resp = Response::new(StatusCode::Ok); resp.set_body(Body::from_string( GraphiQLSource::build() - .endpoint("http://localhost:8000/graphql") - .subscription_endpoint("ws://localhost:8000/graphql") + .endpoint("/graphql") + .subscription_endpoint("/graphql") .finish(), )); resp.set_content_type(mime::HTML); Ok(resp) }); - app.listen("127.0.0.1:8000").await?; + app.listen("0.0.0.0:8000").await?; Ok(()) } diff --git a/warp/starwars/Cargo.toml b/warp/starwars/Cargo.toml index b53aaa8..10c3ead 100644 --- a/warp/starwars/Cargo.toml +++ b/warp/starwars/Cargo.toml @@ -2,12 +2,12 @@ name = "warp-starwars" version = "0.1.1" authors = ["sunli "] -edition = "2021" +edition = "2024" [dependencies] async-graphql = { path = "../../.." } async-graphql-warp = { path = "../../../integrations/warp" } -tokio = { version = "1.0.2", features = ["macros", "rt-multi-thread"] } +tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] } warp = "0.3" starwars = { path = "../../models/starwars" } http = "0.2" diff --git a/warp/starwars/src/main.rs b/warp/starwars/src/main.rs index b8ff5b2..3b5023e 100644 --- a/warp/starwars/src/main.rs +++ b/warp/starwars/src/main.rs @@ -1,10 +1,10 @@ use std::convert::Infallible; -use async_graphql::{http::GraphiQLSource, EmptyMutation, EmptySubscription, Schema}; +use async_graphql::{EmptyMutation, EmptySubscription, Schema, http::GraphiQLSource}; use async_graphql_warp::{GraphQLBadRequest, GraphQLResponse}; use http::StatusCode; use starwars::{QueryRoot, StarWars}; -use warp::{http::Response as HttpResponse, Filter, Rejection}; +use warp::{Filter, Rejection, http::Response as HttpResponse}; #[tokio::main] async fn main() { @@ -26,11 +26,7 @@ async fn main() { let graphiql = warp::path::end().and(warp::get()).map(|| { HttpResponse::builder() .header("content-type", "text/html") - .body( - GraphiQLSource::build() - .endpoint("http://localhost:8000") - .finish(), - ) + .body(GraphiQLSource::build().endpoint("/").finish()) }); let routes = graphiql diff --git a/warp/subscription/Cargo.toml b/warp/subscription/Cargo.toml index 00a3e37..44f2e6a 100644 --- a/warp/subscription/Cargo.toml +++ b/warp/subscription/Cargo.toml @@ -2,11 +2,11 @@ name = "warp-subscription" version = "0.1.1" authors = ["sunli "] -edition = "2021" +edition = "2024" [dependencies] async-graphql = { path = "../../.." } async-graphql-warp = { path = "../../../integrations/warp" } -tokio = { version = "1.0.2", features = ["macros", "rt-multi-thread"] } +tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] } warp = "0.3" books = { path = "../../models/books" } diff --git a/warp/subscription/src/main.rs b/warp/subscription/src/main.rs index 54b6d79..9c9c563 100644 --- a/warp/subscription/src/main.rs +++ b/warp/subscription/src/main.rs @@ -1,9 +1,9 @@ use std::convert::Infallible; -use async_graphql::{http::GraphiQLSource, Schema}; -use async_graphql_warp::{graphql_subscription, GraphQLResponse}; +use async_graphql::{Schema, http::GraphiQLSource}; +use async_graphql_warp::{GraphQLResponse, graphql_subscription}; use books::{MutationRoot, QueryRoot, Storage, SubscriptionRoot}; -use warp::{http::Response as HttpResponse, Filter}; +use warp::{Filter, http::Response as HttpResponse}; #[tokio::main] async fn main() { @@ -27,8 +27,8 @@ async fn main() { .header("content-type", "text/html") .body( GraphiQLSource::build() - .endpoint("http://localhost:8000") - .subscription_endpoint("ws://localhost:8000") + .endpoint("/") + .subscription_endpoint("/") .finish(), ) }); diff --git a/warp/token-from-header/Cargo.toml b/warp/token-from-header/Cargo.toml index 78e2bed..bd09f1f 100644 --- a/warp/token-from-header/Cargo.toml +++ b/warp/token-from-header/Cargo.toml @@ -2,11 +2,11 @@ name = "warp-token-from-header" version = "0.1.1" authors = ["sunli "] -edition = "2021" +edition = "2024" [dependencies] async-graphql = { path = "../../.." } async-graphql-warp = { path = "../../../integrations/warp" } -tokio = { version = "1.0.2", features = ["macros", "rt-multi-thread"] } +tokio = { version = "1.37", features = ["macros", "rt-multi-thread"] } warp = "0.3" token = { path = "../../models/token" } diff --git a/warp/token-from-header/src/main.rs b/warp/token-from-header/src/main.rs index 2daf706..dbfd422 100644 --- a/warp/token-from-header/src/main.rs +++ b/warp/token-from-header/src/main.rs @@ -1,9 +1,9 @@ use std::convert::Infallible; -use async_graphql::{http::GraphiQLSource, Data, EmptyMutation, Schema}; -use async_graphql_warp::{graphql_protocol, GraphQLResponse, GraphQLWebSocket}; -use token::{on_connection_init, QueryRoot, SubscriptionRoot, Token}; -use warp::{http::Response as HttpResponse, ws::Ws, Filter}; +use async_graphql::{Data, EmptyMutation, Schema, http::GraphiQLSource}; +use async_graphql_warp::{GraphQLResponse, GraphQLWebSocket, graphql_protocol}; +use token::{QueryRoot, SubscriptionRoot, Token, on_connection_init}; +use warp::{Filter, http::Response as HttpResponse, ws::Ws}; #[tokio::main] async fn main() { @@ -16,8 +16,8 @@ async fn main() { .header("content-type", "text/html") .body( GraphiQLSource::build() - .endpoint("http://localhost:8000") - .subscription_endpoint("ws://localhost:8000/ws") + .endpoint("/") + .subscription_endpoint("/ws") .finish(), ) });