graphql-rust / graphql-client

Typed, correct GraphQL requests and responses in Rust
Apache License 2.0
1.12k stars 152 forks source link

FR: Consider adding support for multi-part form upload spec #441

Open Dig-Doug opened 1 year ago

Dig-Doug commented 1 year ago

This spec proposes an alternate request format to allow uploading files in graphql requests. It's supported by a lot of client/servers, e.g. async-graphql.

Toasterson commented 3 months ago

For everybody coming here, this is doable with the current library and does not need a feature here. The trick is: Graphql uses a multipart map to replace variables with the file after the fact so the request body does not get analyzed during the reading of the request. A simple scalar like this is sufficient to work for async_graphql

#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct Upload(usize);

My full upload code is just a translated curl command to reqwest which ended up looking like this:

let vars = upload_component_file::Variables {
    name,
    version,
    revision,
    gate: gate_id,
    url: None,
    file: Some(Upload(0)),
    kind: kind.into(),
};
let request_body = UploadComponentFile::build_query(vars);
let request_str = serde_json::to_string(&request_body)?;
let client = reqwest::blocking::Client::new();

let file_map = "{ \"0\": [\"variables.file\"] }";

let some_file = reqwest::blocking::multipart::Part::file(file)?;

let form = reqwest::blocking::multipart::Form::new()
    .text("operations", request_str)
    .text("map", file_map)
    .part("0", some_file);

let res = client.post(forge_url).multipart(form).send()?;
res.json()?
simbleau commented 3 months ago

My issue is that I want to be able to derive GraphQLQuery on a example.graphql file like so, with async-graphql:

#[derive(GraphQLQuery)]
#[graphql(
    schema_path = "src/schema.json",
    query_path = "src/assets/upload.graphql",
)]
pub struct Upload;
mutation Upload($file: Upload!) {
    assets {
        upload(file: $file) {
            key
            version
        }
    }
}

How would I be able to derive that? Currently I'm getting a cryptic error:

Does this mean every type in .graphql files must be Serialize?

the trait bound `assets::Upload: ..::_::_serde::Serialize` is not satisfied
the following other types implement trait `...::_::_serde::Serialize`:
  bool
  char
  isize
  i8
  i16
...
Toasterson commented 3 months ago

You run into a name conflict with the type auto-generated and Your struct name. Rename mutation Upload($file: Upload!) { to something like mutation UploadFile($file: Upload!) { and your custom struct to pub struct UploadFile; Then add a separate

#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct Upload(usize);

like I did and it should work if no other types conflict with the name. Also yes all Scalar Types must implement the derives I have, as they do get serialized into the request, they get mapped over by convention.

Use cargo expand plugin or similar helpers to view the generated code and let an IDE check it. It should warn you about the duplication.

simbleau commented 3 months ago

Wow, that worked! Thank you @Toasterson !