Code Monkey home page Code Monkey logo

async-graphql's Introduction

async-graphql

a high-performance graphql server library that's fully specification compliant

Book中文文档DocsGitHub repositoryCargo package


ci status code coverage Unsafe Rust forbidden Crates.io version docs.rs docs downloads PRs Welcome

This crate uses #![forbid(unsafe_code)] to ensure everything is implemented in 100% safe Rust.

Static schema

use std::error::Error;

use async_graphql::{http::GraphiQLSource, EmptyMutation, EmptySubscription, Object, Schema};
use async_graphql_poem::*;
use poem::{listener::TcpListener, web::Html, *};

struct Query;

#[Object]
impl Query {
    async fn howdy(&self) -> &'static str {
        "partner"
    }
}

#[handler]
async fn graphiql() -> impl IntoResponse {
    Html(GraphiQLSource::build().finish())
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    // create the schema
    let schema = Schema::build(Query, EmptyMutation, EmptySubscription).finish();

    // start the http server
    let app = Route::new().at("/", get(graphiql).post(GraphQL::new(schema)));
    println!("GraphiQL: http://localhost:8000");
    Server::new(TcpListener::bind("0.0.0.0:8000"))
        .run(app)
        .await?;
    Ok(())
}

Dynamic schema

Requires the dynamic-schema feature to be enabled.

use std::error::Error;

use async_graphql::{dynamic::*, http::GraphiQLSource};
use async_graphql_poem::*;
use poem::{listener::TcpListener, web::Html, *};

#[handler]
async fn graphiql() -> impl IntoResponse {
    Html(GraphiQLSource::build().finish())
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    let query = Object::new("Query").field(Field::new(
        "howdy",
        TypeRef::named_nn(TypeRef::STRING),
        |_| FieldFuture::new(async { "partner" }),
    ));

    // create the schema
    let schema = Schema::build(query, None, None).register(query).finish()?;

    // start the http server
    let app = Route::new().at("/", get(graphiql).post(GraphQL::new(schema)));
    println!("GraphiQL: http://localhost:8000");
    Server::new(TcpListener::bind("0.0.0.0:8000"))
        .run(app)
        .await?;
    Ok(())
}

Features

  • Static and dynamic schemas are fully supported
  • Fully supports async/await
  • Type safety
  • Rustfmt friendly (Procedural Macro)
  • Custom scalars
  • Minimal overhead
  • Easy integration (poem, axum, actix-web, tide, warp, rocket ...)
  • Upload files (Multipart request)
  • Subscriptions (WebSocket transport)
  • Custom extensions
  • Error extensions
  • Limit query complexity/depth
  • Batch queries
  • Apollo Persisted Queries
  • Apollo Tracing extension
  • Apollo Federation(v2)

Note: Minimum supported Rust version: 1.75.0 or later

Examples

All examples are in the sub-repository, located in the examples directory.

git submodule update # update the examples repo
cd examples && cargo run --bin [name]

For more information, see the sub-repository README.md.

Integrations

Integrations are what glue async-graphql with your web server, here are provided ones, or you can build your own!

Crate features

This crate offers the following features. Most are not activated by default, except the integrations of GraphiQL (graphiql) and GraphQL Playground (playground):

feature enables
apollo_tracing Enable the Apollo tracing extension.
apollo_persisted_queries Enable the Apollo persisted queries extension.
log Enable the Logger extension.
tracing Enable the Tracing extension.
opentelemetry Enable the OpenTelemetry extension.
unblock Support Asynchronous reader for Upload
bson Integrate with the bson crate.
chrono Integrate with the chrono crate.
chrono-tz Integrate with the chrono-tz crate.
url Integrate with the url crate.
uuid Integrate with the uuid crate.
string_number Enable the StringNumber.
dataloader Support DataLoader.
secrecy Integrate with the secrecy crate.
decimal Integrate with the rust_decimal crate.
bigdecimal Integrate with the bigdecimal crate.
cbor Support for serde_cbor.
smol_str Integrate with the smol_str crate.
hashbrown Integrate with the hashbrown crate.
time Integrate with the time crate.
tokio-sync Integrate with the tokio::sync::RwLock and tokio::sync::Mutex.
fast_chemail Integrate with the fast_chemail crate.
tempfile Save the uploaded content in the temporary file.
dynamic-schema Support dynamic schema
graphiql Enables the GraphiQL IDE integration
playground Enables the GraphQL playground IDE integration

Observability

One of the tools used to monitor your graphql server in production is Apollo Studio. Apollo Studio is a cloud platform that helps you build, monitor, validate, and secure your organization's data graph. Add the extension crate async_graphql_apollo_studio_extension to make this avaliable.

Who's using async-graphql in production?

Community Showcase

  • rust-actix-graphql-sqlx-postgresql Using GraphQL with Rust and Apollo Federation
  • entity-rs A simplistic framework based on TAO, Facebook's distributed database for Social Graph.
  • vimwiki-server Provides graphql server to inspect and manipulate vimwiki files.
  • Diana Diana is a GraphQL system for Rust that's designed to work as simply as possible out of the box, without sacrificing configuration ability.
  • cindythink
  • sudograph

Blog Posts

References

License

Licensed under either of

async-graphql's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

async-graphql's Issues

Accessing context within subscriptions

Thanks again for the great library and the fast response to my question yesterday! I now have a similar question around subscriptions. How do we pass data into the GraphQL resolver for a subscription? Here is pseudocode for what I would like to do:

pub async fn index_ws(
    schema: web::Data<MySchema>,
    req: HttpRequest,
    payload: web::Payload,
    session: Session,
    db: web::Data<database::Database>
) -> Result<HttpResponse> {
    let user: Option<User> = user_from_session(db.users.clone(), db.sessions.clone(), session).await;
    // something like ".data(...)" to extend context does not seem to exist
    ws::start_with_protocols(WSSubscription::new(&schema).data(user), &["graphql-ws"], &req, payload)
}

And then:

#[async_graphql::Subscription]
impl SubscriptionRoot {
    #[field]
    async fn mydata(&self, ctx: &Context<'_>, mutation_type: Option<MutationType>) -> impl Stream<Item = DataChanged> {
        let current_user = ctx.data::<Option<User>>();
        // logic here, for example use current_user.id to filter a stream ...
    }
}

Is this possible? I reviewed the subscription example and didn't see anything. Looking over the implementation of the library, I also didn't see any way to add additional data to a subscription context, or access the context.

Look into approaching federation.

https://www.apollographql.com/blog/apollo-federation-f260cf525d21 This here is a very nice tool, also to help load-balancing as when the schema is split between services - it's easier to have several different entrypoints with different pools of servers.

Even if one does want to code a monolith - this is easily achievable by having features in the source code and just running the same service with different features enabled.

But using the node library is suboptimal, so I've been thinking this might actually be worth implementing in Rust. It's not really an issue for that repo, but something to chew on. I'll be looking into prototyping this sometime soon.

How to solve the N+1 problem?

Hey there. Nice project! I like it especially because it addresses features like query complexity and uploading files. But what about the N+1 problem? Are there any plans in this direction?

And a second question I want to put here. Is there a way to pass the request object to the async_graphql::Object implementation? I think it would be pretty helpful sometimes (For example to read a cookie header).

Implement conversion from serde_json::Value to Any

I would like to implement impl From serde-json::Value for Any which is currently blocked by graphql-parsers inability to convert from serde_json::Value due to mismatch of underlying types for Number. Once this Issue resolves I will implement the conversion for Any.

Defining fields in traits

I just thought it would be really neat to move some common fields from my concrete impl blocks to a shared trait. I can think of a lot of situations where this might be handy:

  • DRYing code
  • Composing types
  • Ensuring consistency
  • Libraries exposing GQL endpoints for user defined types etc.

So Is this possible?

The ability to collect fields from both SimpleObject and Object.

Here's an example: I have an object that has 10 fields, only one of which would need to be defined as a custom Scalar. But in order to expose this object's fields I would have to actually create a getter for each of it's fields. Or otherwise I'd have to use a gql scalar type within my business logic and the database layer, which would also be somewhat painful.

Is it possible to specify a mutable self on a query?

I want to do something like this:

#[Object(desc = "Information about the client's account")]
impl Client {
    #[field(desc = "The client's current balance")]
    async fn balance(&mut self) -> u64 {
        self.client.write().balance()
    }
}

Where the balance function needs to accept self mutably. But I'm getting this error:

   --> src/graphql.rs:151:1
    |
151 | #[Object(desc = "Information about the client's account")]
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    | |
    | `_self` is a `&` reference, so the data it refers to cannot be borrowed as mutable
    | help: consider changing this to be a mutable reference: `&mut graphql::Client`

Is there a way to do this without resorting to interior mutability?

Add "flatten" attribute to field

As discussed in #28, it would be nice to have a flatten attribute on fields. Note: This does not seem to be possible as of now! This issue is just a reminder to implement it when it becomes possible. Here is a very basic example of what we want to accomplish:

#[SimpleObject]
struct Info {
    #[field]
    base_field: String
}


#[SimpleObject]
struct MyInfo {
    #[field(flatten)] // <<<-------------------------
   info: Info,

    #[field]  
    additional_field: String,
}

pub struct QueryRoot;

impl QueryRoot {
    async fn info(&self) -> MyInfo {
        unimplemented!()
    } 
}

Instead of getting this:

{
    info {
        info {
            baseField
        }
        additionalField
    }
}

You would be getting this:

{
    info {
         baseField,
         additionalField
    }

Is it possible to pass session data (or other actix middleware data) into the GraphQL execution context?

First, awesome work on this library! I started using it with actix as Juniper doesn't support subscriptions well and so far everything has worked great. But I've recently run into an issue:

Is it possible to pass actix-web session data into the execution context of a GraphQL resolver? I would like to use this to authenticate certain queries against a session token. Poor pseudocode for what I'm looking to accomplish would be:

impl QueryRoot {
    #[field]
    async fn mydata(&self, ctx: &Context<'_>) -> Option<Vec<MyData>> {
        // the line below is what I would like to accomplish. how to get session data in here?
        let token = ctx.data::<Session>().get("token");
        // then validation is easy. I have the db from Schema::build(..).data(...)
        let db = ctx.data::<Database>();
        match validate(token, db) {
            Some(result) => Some(db.get_all().await),
            None => None
        }
    }
}

In Juniper, it seems like you can extend the Context to do something similar. For example, this issue led to the suggestion: https://github.com/davidpdrsn/graphql-app-example/blob/master/src/graphql.rs#L20

Any ideas how to best accomplish this here? I'd be happy to document it with an example if someone could help me figure it out. Ideally, it would be nice to use actix-session or something similar.

Access to request in schema

How to pass some context to schema with web::Json(req.into_inner().execute(&s).await) ? like a token from request

Use rustdoc comments as graphql desc?

I am looking to replace juniper with this library and I was looking through the docs and it looks like all the examples use proc macro attributes to add a description. Is it currently (or planned) possible to parse rustdoc comments for the graphql description?

Support Upload Stream

The Upload type should provide a stream of bytes instead of a Vec<u8> leaving it to the user to decide what to do with it eg. stream it into a tempfile. I think loading the file contents into memory is a big performance and security issue.

Alternatively let the user define a location for uploads (eg. /tmp) and stream the content into files at this location and pass handles to these files to the user.

The second solution might be easier to integrate into the existing codebase although as a library user I would much prefer the first solution, giving me more control (eg. the opportunity to cancel requests early on before loading the contents / if not enough disk space is available / dynamic filesize checks / streaming to 3rd party APIs etc.)

Interface doesn't work with SimpleObject?

I was trying to implement an Interface for my structs annotated with SimpleObject and it fails with field, not a method. Is it possible to support Interfaces using SimpleObjects?

Here's an example:

#[async_graphql::SimpleObject]
pub struct MyObj {
  #[field]
  pub id: i32,
  #[field]
  pub title: String,
};

#[async_graphql::Interface(
  field(name = "id", type = "i32"),
)]
pub struct Node(MyObj);

Support the @defer directive in query

Since the stable version of rust does not support generic specialization, this is a bit of a hassle to implement. I'm doing it in other ways, and I'm not sure if I can finish it.

Add ability to forward field arguments to guard

I'm thinking of something like this:

struct UserGuard {
    id: ID
}

#[async_trait::async_trait]
impl Guard for UserGuard {
    async fn check(&self, ctx: &Context<'_>) -> FieldResult<()> {
        if let Some(current_user) = ctx.data_opt::<User>() {
            if current_user.is_admin() {
                return OK(())
            }

            if current_user.id == self.id {
                return Ok(());
            }
        }

        Err("Forbidden".into())
    }
}

#[async_graphql::Object]
impl QueryRoot {
    #[field(guard(UserGuard(id = "@id")))]
    async fn user(&self, ctx: &Context<'_>, id: ID) -> FieldResult<User> {
        // ...
    }
}

This would allow for more granular ACL implementation at the field guard level.

Field guard

Add the guard attribute in #[field] as a precondition to resolve the field.

#field(guard(RequireAuth(role = ADMIN)))

All fields are exported by default

Export all fields on Object, SimpleObject, InputObject, Subscription by default, and #[field(skip)] is used to disable this feature.

  • Object
  • SimpleObject
  • InputObject
  • Subscription

Accept a SimpleObject as input type.

Currently if I want to define a struct with a bunch of fields as an InputType I would also have to define the impl for it and all the fields there, which makes the amount of boilerplate unbearable.

I'd like to be able to simply define a struct and use it's fields as input fields.

DataSource macro hides errors

While using the #[DataSource] macro, any errors inside the implementation of the trait are marked on the trait itself (and in some weird cases, hoisted to line 1 of main).

I looked into the macro and found it is just adding #[async_trait::async_trait] so I used that directly and the errors are highlighted correctly in my code.

add more test

  • Derive
    • Object
    • Enum
    • InputObject
    • Interface
  • Types
    • Scalar
      • Integer
      • Float
      • String
      • Bool
      • ID
      • DateTime
      • UUID
    • Containers
      • List
      • Non-Null
    • Object
      • Generic Types
      • Lifetime cycle
    • Enum
    • InputObject
      • Field default value
      • Deprecated flag
    • Interface
    • Union
  • Query
    • Fields
    • Arguments
      • Default value
      • Deprecated flag
    • Alias
    • Fragments
    • Inline fragments
    • Operation name
    • Variables
      • Default value
      • Parse value
    • Directives
      • @include
        • FIELD
        • FRAGMENT_SPREAD
        • INLINE_FRAGMENT
      • @Skip
        • FIELD
        • FRAGMENT_SPREAD
        • INLINE_FRAGMENT
    • Schema
  • Validation rules

Upload panics

I get the following error when trying to upload a .png:
panicked at 'byte index 19 is not a char boundary; it is inside '|' (bytes 18..19) of `test.png:image/png|PNG`

And this is my form data:

Content-Disposition: form-data; name="operations"

{"operationName":"upload","variables":{"file":null},"query":"mutation upload($file: Upload!) {\n  upload(file: $file)\n}\n"}
------WebKitFormBoundaryF5RA2VMgDmAIU1OT
Content-Disposition: form-data; name="map"

{"1":["variables.file"]}
------WebKitFormBoundaryF5RA2VMgDmAIU1OT
Content-Disposition: form-data; name="1"; filename="test.png"
Content-Type: image/png


------WebKitFormBoundaryF5RA2VMgDmAIU1OT--

Provide example with global stream for subscription

Now that subscription handlers return a stream. What is the most idiomatic way for subscribers sharing a stream as opposed to each subscriber starting a new stream? Also is there an idiomatic way of implementing something like the publish method to emit to subscribers from a mutation handler directly?
I can think of two solutions:

  1. Use lazy_static or
  2. Store a channel on the SubscriptionRoot

Is there a better solution I am not aware of? I find it quite hard to migrate my code to 1.6.7 in a clean fashion.
If you could point me in the right direction I could extend the examples

Support Error Extensions

Hey,
first of all, I really enjoy using your library so far. I think it is already very comprehensive. One thing I am missing though is returning extensions for errors as described here in the spec. Something like:

  "errors": [
    {
      "message": "Name for character with ID 1002 could not be fetched.",
      "locations": [ { "line": 6, "column": 7 } ],
      "path": [ "hero", "heroFriends", 1, "name" ],
      "extensions": {
        "code": "CAN_NOT_FETCH_BY_ID",
        "timestamp": "Fri Feb 9 14:33:09 UTC 2018"
      }
    }
  ]
}

In terms of api, I think it would be nice to have a trait with a single method with_extensions which you could implement on your errors. Something like:

// somewhere in async-graphql
pub trait ErrorExtensions {
    fn with_extensions(&self) -> Option<serde_json::Value> {
        None
    }
}

impl<T: Into<Error>> ErrorExtensions for T {}
// somewhere in users code...
impl ErrorExtensions for MyError {
  fn with_extensions(&self) -> Option<serde_json::Value> {

    let extensions = serde_json::json!({
       "code": self.code.to_string(),
       "timestamp": get_current_timestamp()
    });

    Some(extensions)
  }
}

I am happy to contribute, so if you want any help just let me know.

Interfaces don't support FieldResult type

It seems that Interfaces don't support the FieldResult type. This might be a scoping issue in the macro expansion?

Here is an example:

use async_graphql::FieldResult;

struct MyObj;

#[async_graphql::Object]
impl MyObj {
    #[field]
    async fn value_a(&self) -> FieldResult<i32> {
        Ok(1)
    }
}

#[async_graphql::Interface(field(name = "value_a", type = "FieldResult<i32>"))]
struct InterfaceA(MyObj);

Serde alias gets ignored on Enums

I ran into the following bug:

use serde::Deserialize;
  
#[async_graphql::Enum]
#[derive(Deserialize)]
enum Test {
  #[serde(alias = "Other")]
    Real,
  }
  
#[derive(Deserialize)]
struct TestStruct {
  value: Test,
}
  
fn main() {
  serde_json::from_str::<TestStruct>(r#"{ "value" : "Other" }"#).unwrap();
}

thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Error("unknown variant `Other`, expected `Real`", line: 1, column: 19)', src/libcore/result.rs:1188:5

Without the async_graphql::Enum annotation everything is fine.

Provide prelude

I think it is time to export the most important types and traits in a prelude to increase usability.

Dynamic schema

Async-graphql lacks an important feature that most graphql implementations do not. Support both static and dynamic schema, and only with dynamic schema can write something like hasaru.

I have some preliminary ideas about the implementation of this feature. Do you think this feature is important?

@nicolaiunrein @phated

What happened to `async_graphql_actix_web::HandlerBuilder`?

I was about to upgrade from 1.7.8 to 1.9.3 when I realised that quite a lot has changed. So I looked at the examples and codebase but I could not find any info on how I would transition the below code to 1.9.3. Where do I specify max_file_size and temp_dir?

    HttpServer::new(move || {
        let schema = Schema::build(QueryRoot, MutationRoot, SubscriptionRoot)
            .extension(|| ApolloTracing::default())
            .finish();

        let handler = async_graphql_actix_web::HandlerBuilder::new(schema)
            .enable_ui("http://localhost:3001", Some("ws://localhost:3001"))
            .max_file_size(100_000_000_000) //100 GB
            .enable_subscription()
            .build();

        App::new()
            .wrap(Logger::default())
            .wrap(Cors::new().finish())
            .service(web::resource("/").to(handler))
    })
    .bind(format!("0.0.0.0:{}", 3001))
    .unwrap()
    .run()

Examples for inputValueType.

I need to build a query endpoint that would support something like this:

pub struct Criteria {
    pub value: HashMap<String, Vec<Criterion>>,
}
impl QueryRoot {
async fn records(&self, criteria: Criteria, start: i64, skip: i64, order: SortCriteria) 
}

I'm trying to figure out a way to do this without manually implementing InputValueType. I'm sure it's possible, but I couldn't find a relevant example.

Fragments on subscribtions nevers push data to client

Hello,
When using fragments in subscriptions, I never receive any notifications in the websocket

This works:

subscription s {
  order(id: "5eabbe77103ca80276efdf70") {
    id
    createdAt
  }
}

This doesn't:

subscription s {
  order(id: "5eabbe77103ca80276efdf70") {
    id
    ...orderFrag
  }
}

fragment orderFrag on Order {
  id
  createdAt
}

tested with both playground and an apollo client

How to access a future through parking_lot guard

I have a repro here:

https://github.com/dusty-phillips/async-graphql/blob/parking_lot_repro/examples/parking_lot.rs

In short, I'm using the parking_lot crate to get an RwLock. I've put the lock inside an Arc, and tried to access it from both a query and a mutation. However, I'm getting the following compile errors:

master* $ cargo run --example parking_lot
   Compiling async-graphql v0.10.8 (/home/dusty/beanstalk/async-graphql)
error: future cannot be sent between threads safely
  --> examples/parking_lot.rs:26:1
   |
26 | #[Object]
   | ^^^^^^^^^ future returned by `__resolve_field` is not `Send`
   |
   = help: within `impl std::future::Future`, the trait `std::marker::Send` is not implemented for `*mut ()`
note: future is not `Send` as this value is used across an await
  --> examples/parking_lot.rs:30:9
   |
30 |         self.thing.read().get_the_value().await
   |         -----------------^^^^^^^^^^^^^^^^^^^^^^
   |         |
   |         await occurs here, with `self.thing.read()` maybe used later
   |         has type `lock_api::rwlock::RwLockReadGuard<'_, parking_lot::raw_rwlock::RawRwLock, Thing>`
31 |     }
   |     - `self.thing.read()` is later dropped here
   = note: required for the cast to the object type `dyn std::future::Future<Output = std::result::Result<serde_json::value::Value, anyhow::Error>> + std::marker::Send`

error: future cannot be sent between threads safely
  --> examples/parking_lot.rs:38:1
   |
38 | #[Object]
   | ^^^^^^^^^ future returned by `__resolve_field` is not `Send`
   |
   = help: within `impl std::future::Future`, the trait `std::marker::Send` is not implemented for `*mut ()`
note: future is not `Send` as this value is used across an await
  --> examples/parking_lot.rs:42:9
   |
42 |         self.thing.write().set_the_value(thing_value).await;
   |         ------------------^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^- `self.thing.write()` is later dropped here
   |         |
   |         await occurs here, with `self.thing.write()` maybe used later
   |         has type `lock_api::rwlock::RwLockWriteGuard<'_, parking_lot::raw_rwlock::RawRwLock, Thing>`
   = note: required for the cast to the object type `dyn std::future::Future<Output = std::result::Result<serde_json::value::Value, anyhow::Error>> + std::marker::Send`

error: aborting due to 2 previous errors

error: could not compile `async-graphql`.

To learn more, run the command again with --verbose.

I've tried moving the guard out, but it doesn't seem to work. Intuitively, I can imagine it might not be possible to hold a lock across an await, but there is no compile error when I try to access it in the main() function (line 72) I don't get a compile error there. So the issue seems to be something in the Object macro, but I don't know how to debug. Any suggestions?

Undocumented feature: all fields expands to FieldResult

Hello,
I'm not sure if this is realted to the recent changes on FieldResult but I've just noticed that we can use ? in field resolvers even if they don't return a FieldResult:

#[async_graphql::Object]
impl QueryRoot {
    #[field]
    async fn user(&self, ctx: &Context<'_>, id: ID) -> models::User {
        let id = ObjectId::with_string(&id.to_string())?;
        ctx.data::<Loaders>().user.load(id).await?
    }
}

It feel a bit odd, but is also very convenient and makes error management easier. The documentation doesn't seems to mention this at the moment, is this a feature we can rely on or is it subject to change in the future ?

Cheers

Multiple interfaces?

First, I wanted to say thank you for this amazing project! 🙇

I'm currently implementing a Relay client against my async-graphql server and they require everything to implement the Node { id } interface for every object.

If I implement Node for every interface, I'm not sure how to implement another interface on the same object. Have you considered this for the #[async_graphql::Interface] macro?

Remove type safety from Interface

The type safety of the interface causes some functionality to be unavailable. I'll remove the compile-time type safety of the interface and check it when I call Schema::new. It doesn't have any performance loss, but there is no guarantee that a compile-time type safety, I think it is worth to do so.

InputObject not validating or i`m missing something

Using the actix subscriptions example, adding validations to InputObject fails to validate

use actix_web::{web, App, HttpServer};
use async_graphql::validators::StringMinLength;
use async_graphql::{publish, Context, InputObject, Result, Schema, ID};
use futures::lock::Mutex;
use slab::Slab;
use std::sync::Arc;

#[derive(Clone)]
struct Book {
    id: ID,
    name: String,
    author: String,
}

#[InputObject]
struct CreateBookInput {
    #[field(
        desc = "name of the book",
        validator(StringMinLength(length = 5))
    )]
    name: String,
    #[field(
        desc = "name of the author",
        validator(StringMinLength(length = 5))
    )]
    author: String,
}

#[async_graphql::Object]
impl Book {
    #[field]
    async fn id(&self) -> &str {
        &self.id
    }

    #[field]
    async fn name(&self) -> &str {
        &self.name
    }

    #[field]
    async fn author(&self) -> &str {
        &self.author
    }
}

type Storage = Arc<Mutex<Slab<Book>>>;

struct QueryRoot;

#[async_graphql::Object(cache_control(max_age = 5))]
impl QueryRoot {
    #[field]
    async fn books(&self, ctx: &Context<'_>) -> Vec<Book> {
        let books = ctx.data::<Storage>().lock().await;
        books.iter().map(|(_, book)| book).cloned().collect()
    }
}

struct MutationRoot;

#[async_graphql::Object]
impl MutationRoot {
    #[field]
    async fn create_book_input(
        &self,
        ctx: &Context<'_>,
        input: CreateBookInput,
    ) -> ID {
        let mut books = ctx.data::<Storage>().lock().await;
        let entry = books.vacant_entry();
        let id: ID = entry.key().into();
        let book = Book {
            id: id.clone(),
            name: input.name,
            author: input.author,
        };
        entry.insert(book);
        publish(BookChanged {
            mutation_type: MutationType::Created,
            id: id.clone(),
        })
        .await;
        id
    }

    #[field]
    async fn create_book_args(
        &self,
        ctx: &Context<'_>,
        #[arg(validator(StringMinLength(length = 5)))] name: String,
        #[arg(validator(StringMinLength(length = 5)))] author: String,
    ) -> ID {
        let mut books = ctx.data::<Storage>().lock().await;
        let entry = books.vacant_entry();
        let id: ID = entry.key().into();
        let book = Book {
            id: id.clone(),
            name: name,
            author: author,
        };
        entry.insert(book);
        publish(BookChanged {
            mutation_type: MutationType::Created,
            id: id.clone(),
        })
        .await;
        id
    }
}

#[async_graphql::Enum]
enum MutationType {
    Created,
    Deleted,
}

struct BookChanged {
    mutation_type: MutationType,
    id: ID,
}

#[async_graphql::Object]
impl BookChanged {
    #[field]
    async fn mutation_type(&self) -> &MutationType {
        &self.mutation_type
    }

    #[field]
    async fn id(&self) -> &ID {
        &self.id
    }
}

struct SubscriptionRoot;

#[async_graphql::Subscription]
impl SubscriptionRoot {
    #[field]
    fn books(
        &self,
        changed: &BookChanged,
        mutation_type: Option<MutationType>,
    ) -> bool {
        if let Some(mutation_type) = mutation_type {
            return changed.mutation_type == mutation_type;
        }
        true
    }
}

#[actix_rt::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(move || {
        let schema = Schema::build(QueryRoot, MutationRoot, SubscriptionRoot)
            .data(Storage::default())
            .finish();
        let handler = async_graphql_actix_web::HandlerBuilder::new(schema)
            .enable_ui("http://localhost:8000", Some("ws://localhost:8000"))
            .enable_subscription()
            .build();
        App::new().service(web::resource("/").to(handler))
    })
    .bind("127.0.0.1:8000")?
    .run()
    .await
}

#[cfg(test)]
mod tests {
    use super::*;
    use actix_web::dev::Service;
    use actix_web::{
        http::{header, StatusCode},
        test, web, App,
    };

    #[actix_rt::test]
    async fn test_validates_input_with_errors_fails() {
        let schema = Schema::build(QueryRoot, MutationRoot, SubscriptionRoot)
            .data(Storage::default())
            .finish();
        let handler = async_graphql_actix_web::HandlerBuilder::new(schema)
            .enable_subscription()
            .build();
        let mut app = test::init_service(
            App::new().service(web::resource("/").to(handler)),
        )
        .await;
        let req = test::TestRequest::post()
            .uri("/")
            .header(header::CONTENT_TYPE, "application/json")
            .set_payload(r#"{"operationName":null,"variables":{},"query":"mutation { createBookInput(input: {name: \"foo\", author: \"bar\"})}"}"#)
            .to_request();
        let res = app.call(req).await.unwrap();
        assert_eq!(res.status(), StatusCode::BAD_REQUEST);
    }

    #[actix_rt::test]
    async fn test_validates_arguments_with_errors() {
        let schema = Schema::build(QueryRoot, MutationRoot, SubscriptionRoot)
            .data(Storage::default())
            .finish();
        let handler = async_graphql_actix_web::HandlerBuilder::new(schema)
            .enable_subscription()
            .build();
        let mut app = test::init_service(
            App::new().service(web::resource("/").to(handler)),
        )
        .await;
        let req = test::TestRequest::post()
            .uri("/")
            .header(header::CONTENT_TYPE, "application/json")
            .set_payload(r#"{"operationName":null,"variables":{},"query":"mutation { createBookArgs(author: \"foo\", name: \"bar\")}"}"#)
            .to_request();
        let res = app.call(req).await.unwrap();
        assert_eq!(res.status(), StatusCode::BAD_REQUEST);
        let response_body = match res.response().body().as_ref() {
            Some(actix_web::body::Body::Bytes(bytes)) => bytes,
            _ => panic!("Response error"),
        };
        assert_eq!(
            response_body,
            r#"Invalid value for argument "author", The value length is 3, but the length must be greater than or equal to 5
Invalid value for argument "name", The value length is 3, but the length must be greater than or equal to 5
"#
        );
    }
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.