Add support for multipart request
This commit is contained in:
parent
bcf1e24268
commit
abfe861749
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "async-graphql"
|
name = "async-graphql"
|
||||||
version = "1.0.0"
|
version = "1.1.0"
|
||||||
authors = ["sunli <scott_s829@163.com>"]
|
authors = ["sunli <scott_s829@163.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
description = "The GraphQL server library implemented by rust"
|
description = "The GraphQL server library implemented by rust"
|
||||||
|
@ -36,8 +36,10 @@ actix-rt = "1.0.0"
|
||||||
slab = "0.4.2"
|
slab = "0.4.2"
|
||||||
tide = "0.6.0"
|
tide = "0.6.0"
|
||||||
mime = "0.3.16"
|
mime = "0.3.16"
|
||||||
|
async-graphql-actix-web = { path = "async-graphql-actix-web", version = "0.1.0" }
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"async-graphql-derive"
|
"async-graphql-derive",
|
||||||
|
"async-graphql-actix-web"
|
||||||
]
|
]
|
|
@ -91,6 +91,8 @@ Open `http://localhost:8000` in browser
|
||||||
- [X] FRAGMENT_SPREAD
|
- [X] FRAGMENT_SPREAD
|
||||||
- [X] INLINE_FRAGMENT
|
- [X] INLINE_FRAGMENT
|
||||||
- [X] Schema
|
- [X] Schema
|
||||||
|
- [X] Multipart Request (https://github.com/jaydenseric/graphql-multipart-request-spec)
|
||||||
|
- [X] Actix-web
|
||||||
- [X] Validation rules
|
- [X] Validation rules
|
||||||
- [X] ArgumentsOfCorrectType
|
- [X] ArgumentsOfCorrectType
|
||||||
- [X] DefaultValuesOfCorrectType
|
- [X] DefaultValuesOfCorrectType
|
||||||
|
|
21
async-graphql-actix-web/Cargo.toml
Normal file
21
async-graphql-actix-web/Cargo.toml
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
[package]
|
||||||
|
name = "async-graphql-actix-web"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["sunli <scott_s829@163.com>"]
|
||||||
|
edition = "2018"
|
||||||
|
description = "The GraphQL server library implemented by rust"
|
||||||
|
publish = true
|
||||||
|
license = "MIT/Apache-2.0"
|
||||||
|
documentation = "https://docs.rs/async-graphql/"
|
||||||
|
homepage = "https://github.com/sunli829/async-graphql"
|
||||||
|
repository = "https://github.com/sunli829/async-graphql"
|
||||||
|
keywords = ["futures", "async", "graphql"]
|
||||||
|
categories = ["network-programming", "asynchronous"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
async-graphql = { path = "..", version = "1.0.0" }
|
||||||
|
actix-web = "2.0.0"
|
||||||
|
actix-multipart = "0.2.0"
|
||||||
|
futures = "0.3.0"
|
||||||
|
serde_json = "1.0.48"
|
||||||
|
mime = "0.3.16"
|
204
async-graphql-actix-web/src/lib.rs
Normal file
204
async-graphql-actix-web/src/lib.rs
Normal file
|
@ -0,0 +1,204 @@
|
||||||
|
use actix_multipart::Multipart;
|
||||||
|
use actix_web::http::{header, HeaderMap};
|
||||||
|
use actix_web::web::Payload;
|
||||||
|
use actix_web::{web, FromRequest, HttpRequest, HttpResponse, Responder};
|
||||||
|
use async_graphql::http::{GQLRequest, GQLResponse};
|
||||||
|
use async_graphql::{GQLObject, Schema};
|
||||||
|
use futures::StreamExt;
|
||||||
|
use mime::Mime;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::future::Future;
|
||||||
|
use std::pin::Pin;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
pub struct HandlerBuilder<Query, Mutation> {
|
||||||
|
schema: Schema<Query, Mutation>,
|
||||||
|
max_file_size: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Query, Mutation> HandlerBuilder<Query, Mutation>
|
||||||
|
where
|
||||||
|
Query: GQLObject + Send + Sync + 'static,
|
||||||
|
Mutation: GQLObject + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
pub fn new(schema: Schema<Query, Mutation>) -> Self {
|
||||||
|
Self {
|
||||||
|
schema,
|
||||||
|
max_file_size: Some(1024 * 1024 * 2),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn max_file_size(self, size: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
max_file_size: Some(size),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build(
|
||||||
|
self,
|
||||||
|
) -> impl Fn(
|
||||||
|
HttpRequest,
|
||||||
|
Payload,
|
||||||
|
) -> Pin<Box<dyn Future<Output = actix_web::Result<HttpResponse>>>>
|
||||||
|
+ 'static
|
||||||
|
+ Clone {
|
||||||
|
let schema = Arc::new(self.schema);
|
||||||
|
let max_file_size = self.max_file_size;
|
||||||
|
|
||||||
|
move |req: HttpRequest, mut payload: Payload| {
|
||||||
|
let schema = schema.clone();
|
||||||
|
Box::pin(async move {
|
||||||
|
if req.method() != "POST" {
|
||||||
|
return Ok(HttpResponse::MethodNotAllowed().finish());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(ct) = get_content_type(req.headers()) {
|
||||||
|
if ct.essence_str() == mime::MULTIPART_FORM_DATA {
|
||||||
|
let mut multipart = Multipart::from_request(&req, &mut payload.0).await?;
|
||||||
|
|
||||||
|
// read operators
|
||||||
|
let mut gql_request = {
|
||||||
|
let data = read_multipart(&mut multipart, "operations").await?;
|
||||||
|
serde_json::from_slice::<GQLRequest>(&data)
|
||||||
|
.map_err(|err| actix_web::error::ErrorBadRequest(err))?
|
||||||
|
};
|
||||||
|
|
||||||
|
// read map
|
||||||
|
let mut map = {
|
||||||
|
let data = read_multipart(&mut multipart, "map").await?;
|
||||||
|
serde_json::from_slice::<HashMap<String, Vec<String>>>(&data)
|
||||||
|
.map_err(|err| actix_web::error::ErrorBadRequest(err))?
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut query = match gql_request.prepare(&schema) {
|
||||||
|
Ok(query) => query,
|
||||||
|
Err(err) => {
|
||||||
|
return Ok(web::Json(GQLResponse(Err(err)))
|
||||||
|
.respond_to(&req)
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if !query.is_upload() {
|
||||||
|
return Err(actix_web::error::ErrorBadRequest(
|
||||||
|
"It's not an upload operation",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// read files
|
||||||
|
while let Some(field) = multipart.next().await {
|
||||||
|
let mut field = field?;
|
||||||
|
if let Some(content_disposition) = field.content_disposition() {
|
||||||
|
if let (Some(name), Some(filename)) = (
|
||||||
|
content_disposition.get_name(),
|
||||||
|
content_disposition.get_filename(),
|
||||||
|
) {
|
||||||
|
if let Some(var_paths) = map.remove(name) {
|
||||||
|
let content_type = field.content_type().to_string();
|
||||||
|
let mut data = Vec::<u8>::new();
|
||||||
|
while let Some(part) = field.next().await {
|
||||||
|
let part = part.map_err(|err| {
|
||||||
|
actix_web::error::ErrorBadRequest(err)
|
||||||
|
})?;
|
||||||
|
data.extend(&part);
|
||||||
|
|
||||||
|
if let Some(max_file_size) = max_file_size {
|
||||||
|
if data.len() > max_file_size {
|
||||||
|
return Err(
|
||||||
|
actix_web::error::ErrorPayloadTooLarge(
|
||||||
|
"payload to large",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for var_path in var_paths {
|
||||||
|
query.set_upload(
|
||||||
|
&var_path,
|
||||||
|
filename,
|
||||||
|
Some(&content_type),
|
||||||
|
data.clone(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(actix_web::error::ErrorBadRequest(
|
||||||
|
"bad request",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(actix_web::error::ErrorBadRequest("bad request"));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(actix_web::error::ErrorBadRequest("bad request"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !map.is_empty() {
|
||||||
|
return Err(actix_web::error::ErrorBadRequest("missing files"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(web::Json(GQLResponse(query.execute().await))
|
||||||
|
.respond_to(&req)
|
||||||
|
.await?)
|
||||||
|
} else if ct.essence_str() == mime::APPLICATION_JSON {
|
||||||
|
let gql_req =
|
||||||
|
web::Json::<GQLRequest>::from_request(&req, &mut payload.0).await?;
|
||||||
|
Ok(web::Json(gql_req.into_inner().execute(&schema).await)
|
||||||
|
.respond_to(&req)
|
||||||
|
.await?)
|
||||||
|
} else {
|
||||||
|
Ok(HttpResponse::UnsupportedMediaType().finish())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(HttpResponse::UnsupportedMediaType().finish())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_content_type(headers: &HeaderMap) -> actix_web::Result<Mime> {
|
||||||
|
if let Some(content_type) = headers.get(&header::CONTENT_TYPE) {
|
||||||
|
if let Ok(content_type) = content_type.to_str() {
|
||||||
|
if let Ok(ct) = content_type.parse::<Mime>() {
|
||||||
|
return Ok(ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(actix_web::error::ErrorUnsupportedMediaType(
|
||||||
|
"unsupported media type",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read_multipart(multipart: &mut Multipart, name: &str) -> actix_web::Result<Vec<u8>> {
|
||||||
|
let data = match multipart.next().await {
|
||||||
|
Some(Ok(mut field)) => {
|
||||||
|
if let Some(content_disposition) = field.content_disposition() {
|
||||||
|
if let Some(current_name) = content_disposition.get_name() {
|
||||||
|
if current_name != name {
|
||||||
|
return Err(actix_web::error::ErrorBadRequest(format!(
|
||||||
|
"expect \"{}\"",
|
||||||
|
name
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut data = Vec::<u8>::new();
|
||||||
|
while let Some(part) = field.next().await {
|
||||||
|
let part = part.map_err(|err| actix_web::error::ErrorBadRequest(err))?;
|
||||||
|
data.extend(&part);
|
||||||
|
}
|
||||||
|
data
|
||||||
|
} else {
|
||||||
|
return Err(actix_web::error::ErrorBadRequest("missing \"operations\""));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(actix_web::error::ErrorBadRequest("bad request"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(Err(err)) => return Err(err.into()),
|
||||||
|
None => return Err(actix_web::error::ErrorBadRequest("bad request")),
|
||||||
|
};
|
||||||
|
Ok(data)
|
||||||
|
}
|
|
@ -1,15 +1,9 @@
|
||||||
mod starwars;
|
mod starwars;
|
||||||
|
|
||||||
use actix_web::{guard, web, App, HttpResponse, HttpServer};
|
use actix_web::{guard, web, App, HttpResponse, HttpServer};
|
||||||
use async_graphql::http::{graphiql_source, playground_source, GQLRequest, GQLResponse};
|
use async_graphql::http::{graphiql_source, playground_source};
|
||||||
use async_graphql::{GQLEmptyMutation, Schema};
|
use async_graphql::{GQLEmptyMutation, Schema};
|
||||||
|
|
||||||
type StarWarsSchema = Schema<starwars::QueryRoot, GQLEmptyMutation>;
|
|
||||||
|
|
||||||
async fn index(s: web::Data<StarWarsSchema>, req: web::Json<GQLRequest>) -> web::Json<GQLResponse> {
|
|
||||||
web::Json(req.into_inner().execute(&s).await)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn gql_playgound() -> HttpResponse {
|
async fn gql_playgound() -> HttpResponse {
|
||||||
HttpResponse::Ok()
|
HttpResponse::Ok()
|
||||||
.content_type("text/html; charset=utf-8")
|
.content_type("text/html; charset=utf-8")
|
||||||
|
@ -25,11 +19,12 @@ async fn gql_graphiql() -> HttpResponse {
|
||||||
#[actix_rt::main]
|
#[actix_rt::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
HttpServer::new(move || {
|
HttpServer::new(move || {
|
||||||
|
let schema =
|
||||||
|
Schema::new(starwars::QueryRoot, GQLEmptyMutation).data(starwars::StarWars::new());
|
||||||
|
let handler = async_graphql_actix_web::HandlerBuilder::new(schema).build();
|
||||||
|
|
||||||
App::new()
|
App::new()
|
||||||
.data(
|
.service(web::resource("/").to(handler))
|
||||||
Schema::new(starwars::QueryRoot, GQLEmptyMutation).data(starwars::StarWars::new()),
|
|
||||||
)
|
|
||||||
.service(web::resource("/").guard(guard::Post()).to(index))
|
|
||||||
.service(web::resource("/").guard(guard::Get()).to(gql_playgound))
|
.service(web::resource("/").guard(guard::Get()).to(gql_playgound))
|
||||||
.service(
|
.service(
|
||||||
web::resource("/graphiql")
|
web::resource("/graphiql")
|
||||||
|
|
46
examples/upload-file.rs
Normal file
46
examples/upload-file.rs
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
use actix_web::{web, App, HttpServer};
|
||||||
|
use async_graphql::{Schema, Upload};
|
||||||
|
|
||||||
|
struct QueryRoot;
|
||||||
|
|
||||||
|
#[async_graphql::Object]
|
||||||
|
impl QueryRoot {}
|
||||||
|
|
||||||
|
struct MutationRoot;
|
||||||
|
|
||||||
|
#[async_graphql::Object]
|
||||||
|
impl MutationRoot {
|
||||||
|
#[field]
|
||||||
|
async fn single_upload(&self, file: Upload) -> bool {
|
||||||
|
println!(
|
||||||
|
"upload: filename={} size={}",
|
||||||
|
file.filename,
|
||||||
|
file.content.len()
|
||||||
|
);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
#[field]
|
||||||
|
async fn multiple_upload(&self, files: Vec<Upload>) -> bool {
|
||||||
|
for upload in files {
|
||||||
|
println!(
|
||||||
|
"upload: filename={} size={}",
|
||||||
|
upload.filename,
|
||||||
|
upload.content.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::main]
|
||||||
|
async fn main() -> std::io::Result<()> {
|
||||||
|
HttpServer::new(move || {
|
||||||
|
let schema = Schema::new(QueryRoot, MutationRoot);
|
||||||
|
let handler = async_graphql_actix_web::HandlerBuilder::new(schema).build();
|
||||||
|
App::new().service(web::resource("/").to(handler))
|
||||||
|
})
|
||||||
|
.bind("127.0.0.1:8000")?
|
||||||
|
.run()
|
||||||
|
.await
|
||||||
|
}
|
|
@ -10,32 +10,104 @@ use std::hash::BuildHasherDefault;
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
|
|
||||||
/// Variables of query
|
/// Variables of query
|
||||||
#[derive(Default)]
|
#[derive(Debug)]
|
||||||
pub struct Variables(BTreeMap<String, Value>);
|
pub struct Variables(Value);
|
||||||
|
|
||||||
|
impl Default for Variables {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(Value::Object(Default::default()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Deref for Variables {
|
impl Deref for Variables {
|
||||||
type Target = BTreeMap<String, Value>;
|
type Target = BTreeMap<String, Value>;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
&self.0
|
if let Value::Object(obj) = &self.0 {
|
||||||
|
obj
|
||||||
|
} else {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DerefMut for Variables {
|
impl DerefMut for Variables {
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
&mut self.0
|
if let Value::Object(obj) = &mut self.0 {
|
||||||
|
obj
|
||||||
|
} else {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Variables {
|
impl Variables {
|
||||||
pub(crate) fn parse_from_json(value: serde_json::Value) -> Result<Self> {
|
pub(crate) fn parse_from_json(value: serde_json::Value) -> Result<Self> {
|
||||||
let gql_value = json_value_to_gql_value(value);
|
let gql_value = json_value_to_gql_value(value);
|
||||||
if let Value::Object(obj) = gql_value {
|
if let Value::Object(_) = gql_value {
|
||||||
Ok(Variables(obj))
|
Ok(Variables(gql_value))
|
||||||
} else {
|
} else {
|
||||||
Ok(Default::default())
|
Ok(Default::default())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_upload(
|
||||||
|
&mut self,
|
||||||
|
var_path: &str,
|
||||||
|
filename: &str,
|
||||||
|
content_type: Option<&str>,
|
||||||
|
content: Vec<u8>,
|
||||||
|
) {
|
||||||
|
let mut it = var_path.split(".").peekable();
|
||||||
|
|
||||||
|
if let Some(first) = it.next() {
|
||||||
|
if first != "variables" {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut current = &mut self.0;
|
||||||
|
while let Some(s) = it.next() {
|
||||||
|
let has_next = it.peek().is_some();
|
||||||
|
|
||||||
|
if let Ok(idx) = s.parse::<i32>() {
|
||||||
|
if let Value::List(ls) = current {
|
||||||
|
if let Some(value) = ls.get_mut(idx as usize) {
|
||||||
|
if !has_next {
|
||||||
|
*value = Value::String(file_string(filename, content_type, &content));
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
current = value;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let Value::Object(obj) = current {
|
||||||
|
if let Some(value) = obj.get_mut(s) {
|
||||||
|
if !has_next {
|
||||||
|
*value = Value::String(file_string(filename, content_type, &content));
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
current = value;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn file_string(filename: &str, content_type: Option<&str>, content: &[u8]) -> String {
|
||||||
|
if let Some(content_type) = content_type {
|
||||||
|
format!("file:{}:{}|", filename, content_type)
|
||||||
|
+ unsafe { std::str::from_utf8_unchecked(content) }
|
||||||
|
} else {
|
||||||
|
format!("file:{}|", filename) + unsafe { std::str::from_utf8_unchecked(content) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn json_value_to_gql_value(value: serde_json::Value) -> Value {
|
fn json_value_to_gql_value(value: serde_json::Value) -> Value {
|
||||||
|
@ -74,11 +146,11 @@ pub type Context<'a> = ContextBase<'a, &'a Field>;
|
||||||
|
|
||||||
pub struct ContextBase<'a, T> {
|
pub struct ContextBase<'a, T> {
|
||||||
pub(crate) item: T,
|
pub(crate) item: T,
|
||||||
pub(crate) variables: Option<&'a Variables>,
|
pub(crate) variables: &'a Variables,
|
||||||
pub(crate) variable_definitions: Option<&'a [VariableDefinition]>,
|
pub(crate) variable_definitions: Option<&'a [VariableDefinition]>,
|
||||||
pub(crate) registry: &'a Registry,
|
pub(crate) registry: &'a Registry,
|
||||||
pub(crate) data: &'a Data,
|
pub(crate) data: &'a Data,
|
||||||
pub(crate) fragments: &'a HashMap<String, &'a FragmentDefinition>,
|
pub(crate) fragments: &'a HashMap<String, FragmentDefinition>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, T> Deref for ContextBase<'a, T> {
|
impl<'a, T> Deref for ContextBase<'a, T> {
|
||||||
|
@ -116,7 +188,7 @@ impl<'a, T> ContextBase<'a, T> {
|
||||||
.variable_definitions
|
.variable_definitions
|
||||||
.and_then(|defs| defs.iter().find(|def| def.name == name));
|
.and_then(|defs| defs.iter().find(|def| def.name == name));
|
||||||
if let Some(def) = def {
|
if let Some(def) = def {
|
||||||
if let Some(var_value) = self.variables.map(|vars| vars.get(&def.name)).flatten() {
|
if let Some(var_value) = self.variables.get(&def.name) {
|
||||||
return Ok(var_value.clone());
|
return Ok(var_value.clone());
|
||||||
} else if let Some(default) = &def.default_value {
|
} else if let Some(default) = &def.default_value {
|
||||||
return Ok(default.clone());
|
return Ok(default.clone());
|
||||||
|
|
|
@ -24,6 +24,9 @@ pub enum QueryError {
|
||||||
#[error("Cannot query field \"{field_name}\" on type \"{object}\".")]
|
#[error("Cannot query field \"{field_name}\" on type \"{object}\".")]
|
||||||
FieldNotFound { field_name: String, object: String },
|
FieldNotFound { field_name: String, object: String },
|
||||||
|
|
||||||
|
#[error("Missing operation")]
|
||||||
|
MissingOperation,
|
||||||
|
|
||||||
#[error("Unknown operation named \"{name}\"")]
|
#[error("Unknown operation named \"{name}\"")]
|
||||||
UnknownOperationNamed { name: String },
|
UnknownOperationNamed { name: String },
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ pub use graphiql_source::graphiql_source;
|
||||||
pub use playground_source::playground_source;
|
pub use playground_source::playground_source;
|
||||||
|
|
||||||
use crate::error::{RuleError, RuleErrors};
|
use crate::error::{RuleError, RuleErrors};
|
||||||
|
use crate::schema::PreparedQuery;
|
||||||
use crate::{GQLObject, PositionError, Result, Schema, Variables};
|
use crate::{GQLObject, PositionError, Result, Schema, Variables};
|
||||||
use graphql_parser::Pos;
|
use graphql_parser::Pos;
|
||||||
use serde::ser::{SerializeMap, SerializeSeq};
|
use serde::ser::{SerializeMap, SerializeSeq};
|
||||||
|
@ -20,32 +21,46 @@ pub struct GQLRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GQLRequest {
|
impl GQLRequest {
|
||||||
pub async fn execute<Query, Mutation>(self, schema: &Schema<Query, Mutation>) -> GQLResponse
|
pub async fn execute<Query, Mutation>(mut self, schema: &Schema<Query, Mutation>) -> GQLResponse
|
||||||
where
|
where
|
||||||
Query: GQLObject + Send + Sync,
|
Query: GQLObject + Send + Sync,
|
||||||
Mutation: GQLObject + Send + Sync,
|
Mutation: GQLObject + Send + Sync,
|
||||||
{
|
{
|
||||||
let vars = match self.variables {
|
match self.prepare(schema) {
|
||||||
|
Ok(query) => GQLResponse(query.execute().await),
|
||||||
|
Err(err) => GQLResponse(Err(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prepare<'a, Query, Mutation>(
|
||||||
|
&'a mut self,
|
||||||
|
schema: &'a Schema<Query, Mutation>,
|
||||||
|
) -> Result<PreparedQuery<'a, Query, Mutation>>
|
||||||
|
where
|
||||||
|
Query: GQLObject + Send + Sync,
|
||||||
|
Mutation: GQLObject + Send + Sync,
|
||||||
|
{
|
||||||
|
let vars = match self.variables.take() {
|
||||||
Some(value) => match Variables::parse_from_json(value) {
|
Some(value) => match Variables::parse_from_json(value) {
|
||||||
Ok(vars) => Some(vars),
|
Ok(vars) => Some(vars),
|
||||||
Err(err) => return GQLResponse(Err(err)),
|
Err(err) => return Err(err),
|
||||||
},
|
},
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
let query = schema.query(&self.query);
|
let query = schema.query(&self.query);
|
||||||
let query = match &vars {
|
let query = match vars {
|
||||||
Some(vars) => query.variables(vars),
|
Some(vars) => query.variables(vars),
|
||||||
None => query,
|
None => query,
|
||||||
};
|
};
|
||||||
let query = match &self.operation_name {
|
let query = match &self.operation_name {
|
||||||
Some(operation_name) => query.operator_name(operation_name),
|
Some(name) => query.operator_name(name),
|
||||||
None => query,
|
None => query,
|
||||||
};
|
};
|
||||||
GQLResponse(query.execute().await)
|
query.prepare()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct GQLResponse(Result<serde_json::Value>);
|
pub struct GQLResponse(pub Result<serde_json::Value>);
|
||||||
|
|
||||||
impl Serialize for GQLResponse {
|
impl Serialize for GQLResponse {
|
||||||
fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
|
fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
|
||||||
|
|
|
@ -80,7 +80,7 @@ pub use error::{ErrorWithPosition, PositionError, QueryError, QueryParseError};
|
||||||
pub use graphql_parser::query::Value;
|
pub use graphql_parser::query::Value;
|
||||||
pub use scalars::ID;
|
pub use scalars::ID;
|
||||||
pub use schema::{QueryBuilder, Schema};
|
pub use schema::{QueryBuilder, Schema};
|
||||||
pub use types::GQLEmptyMutation;
|
pub use types::{GQLEmptyMutation, Upload};
|
||||||
|
|
||||||
pub type Result<T> = anyhow::Result<T>;
|
pub type Result<T> = anyhow::Result<T>;
|
||||||
pub type Error = anyhow::Error;
|
pub type Error = anyhow::Error;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::{model, GQLType, Value};
|
use crate::{model, GQLType, Value};
|
||||||
|
use graphql_parser::query::Type as ParsedType;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
fn parse_non_null(type_name: &str) -> Option<&str> {
|
fn parse_non_null(type_name: &str) -> Option<&str> {
|
||||||
|
@ -228,7 +229,15 @@ impl Registry {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_basic_type(&self, type_name: &str) -> Option<&Type> {
|
pub fn basic_type_by_typename(&self, type_name: &str) -> Option<&Type> {
|
||||||
self.types.get(TypeName::get_basic_typename(type_name))
|
self.types.get(TypeName::get_basic_typename(type_name))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn basic_type_by_parsed_type(&self, query_type: &ParsedType) -> Option<&Type> {
|
||||||
|
match query_type {
|
||||||
|
ParsedType::NonNullType(ty) => self.basic_type_by_parsed_type(ty),
|
||||||
|
ParsedType::ListType(ty) => self.basic_type_by_parsed_type(ty),
|
||||||
|
ParsedType::NamedType(name) => self.types.get(name.as_str()),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
211
src/schema.rs
211
src/schema.rs
|
@ -7,7 +7,9 @@ use crate::{
|
||||||
ContextBase, GQLObject, GQLOutputValue, GQLType, QueryError, QueryParseError, Result, Variables,
|
ContextBase, GQLObject, GQLOutputValue, GQLType, QueryError, QueryParseError, Result, Variables,
|
||||||
};
|
};
|
||||||
use graphql_parser::parse_query;
|
use graphql_parser::parse_query;
|
||||||
use graphql_parser::query::{Definition, OperationDefinition};
|
use graphql_parser::query::{
|
||||||
|
Definition, FragmentDefinition, OperationDefinition, SelectionSet, VariableDefinition,
|
||||||
|
};
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
@ -116,6 +118,11 @@ impl<Query: GQLObject, Mutation: GQLObject> Schema<Query, Mutation> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum Root<'a, Query, Mutation> {
|
||||||
|
Query(&'a QueryRoot<Query>),
|
||||||
|
Mutation(&'a Mutation),
|
||||||
|
}
|
||||||
|
|
||||||
/// Query builder
|
/// Query builder
|
||||||
pub struct QueryBuilder<'a, Query, Mutation> {
|
pub struct QueryBuilder<'a, Query, Mutation> {
|
||||||
query: &'a QueryRoot<Query>,
|
query: &'a QueryRoot<Query>,
|
||||||
|
@ -123,7 +130,7 @@ pub struct QueryBuilder<'a, Query, Mutation> {
|
||||||
registry: &'a Registry,
|
registry: &'a Registry,
|
||||||
query_source: &'a str,
|
query_source: &'a str,
|
||||||
operation_name: Option<&'a str>,
|
operation_name: Option<&'a str>,
|
||||||
variables: Option<&'a Variables>,
|
variables: Option<Variables>,
|
||||||
data: &'a Data,
|
data: &'a Data,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,86 +144,148 @@ impl<'a, Query, Mutation> QueryBuilder<'a, Query, Mutation> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Specify the variables.
|
/// Specify the variables.
|
||||||
pub fn variables(self, vars: &'a Variables) -> Self {
|
pub fn variables(self, vars: Variables) -> Self {
|
||||||
QueryBuilder {
|
QueryBuilder {
|
||||||
variables: Some(vars),
|
variables: Some(vars),
|
||||||
..self
|
..self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Prepare query
|
||||||
|
pub fn prepare(self) -> Result<PreparedQuery<'a, Query, Mutation>> {
|
||||||
|
let document =
|
||||||
|
parse_query(self.query_source).map_err(|err| QueryParseError(err.to_string()))?;
|
||||||
|
|
||||||
|
check_rules(self.registry, &document)?;
|
||||||
|
|
||||||
|
let mut fragments = HashMap::new();
|
||||||
|
let mut selection_set = None;
|
||||||
|
let mut variable_definitions = None;
|
||||||
|
let mut root = None;
|
||||||
|
|
||||||
|
for definition in document.definitions {
|
||||||
|
match definition {
|
||||||
|
Definition::Operation(operation_definition) => match operation_definition {
|
||||||
|
OperationDefinition::SelectionSet(s) => {
|
||||||
|
selection_set = Some(s);
|
||||||
|
root = Some(Root::Query(self.query));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
OperationDefinition::Query(query)
|
||||||
|
if query.name.is_none() || query.name.as_deref() == self.operation_name =>
|
||||||
|
{
|
||||||
|
selection_set = Some(query.selection_set);
|
||||||
|
variable_definitions = Some(query.variable_definitions);
|
||||||
|
root = Some(Root::Query(self.query));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
OperationDefinition::Mutation(mutation)
|
||||||
|
if mutation.name.is_none()
|
||||||
|
|| mutation.name.as_deref() == self.operation_name =>
|
||||||
|
{
|
||||||
|
selection_set = Some(mutation.selection_set);
|
||||||
|
variable_definitions = Some(mutation.variable_definitions);
|
||||||
|
root = Some(Root::Mutation(self.mutation));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
OperationDefinition::Subscription(subscription)
|
||||||
|
if subscription.name.is_none()
|
||||||
|
|| subscription.name.as_deref() == self.operation_name =>
|
||||||
|
{
|
||||||
|
return Err(QueryError::NotSupported.into());
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
Definition::Fragment(fragment) => {
|
||||||
|
fragments.insert(fragment.name.clone(), fragment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(PreparedQuery {
|
||||||
|
registry: self.registry,
|
||||||
|
variables: self.variables.unwrap_or_default(),
|
||||||
|
data: self.data,
|
||||||
|
fragments,
|
||||||
|
selection_set: selection_set.ok_or({
|
||||||
|
if let Some(name) = self.operation_name {
|
||||||
|
QueryError::UnknownOperationNamed {
|
||||||
|
name: name.to_string(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
QueryError::MissingOperation
|
||||||
|
}
|
||||||
|
})?,
|
||||||
|
root: root.unwrap(),
|
||||||
|
variable_definitions,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Execute the query.
|
/// Execute the query.
|
||||||
pub async fn execute(self) -> Result<serde_json::Value>
|
pub async fn execute(self) -> Result<serde_json::Value>
|
||||||
where
|
where
|
||||||
Query: GQLObject + Send + Sync,
|
Query: GQLObject + Send + Sync,
|
||||||
Mutation: GQLObject + Send + Sync,
|
Mutation: GQLObject + Send + Sync,
|
||||||
{
|
{
|
||||||
let document =
|
self.prepare()?.execute().await
|
||||||
parse_query(self.query_source).map_err(|err| QueryParseError(err.to_string()))?;
|
}
|
||||||
let mut fragments = HashMap::new();
|
}
|
||||||
|
|
||||||
check_rules(self.registry, &document)?;
|
pub struct PreparedQuery<'a, Query, Mutation> {
|
||||||
|
root: Root<'a, Query, Mutation>,
|
||||||
for definition in &document.definitions {
|
registry: &'a Registry,
|
||||||
if let Definition::Fragment(fragment) = definition {
|
variables: Variables,
|
||||||
fragments.insert(fragment.name.clone(), fragment);
|
data: &'a Data,
|
||||||
}
|
fragments: HashMap<String, FragmentDefinition>,
|
||||||
}
|
selection_set: SelectionSet,
|
||||||
|
variable_definitions: Option<Vec<VariableDefinition>>,
|
||||||
for definition in &document.definitions {
|
}
|
||||||
match definition {
|
|
||||||
Definition::Operation(OperationDefinition::SelectionSet(selection_set)) => {
|
impl<'a, Query, Mutation> PreparedQuery<'a, Query, Mutation> {
|
||||||
if self.operation_name.is_none() {
|
/// Detects whether any parameter contains the Upload type
|
||||||
let ctx = ContextBase {
|
pub fn is_upload(&self) -> bool {
|
||||||
item: selection_set,
|
if let Some(variable_definitions) = &self.variable_definitions {
|
||||||
variables: self.variables.as_deref(),
|
for d in variable_definitions {
|
||||||
variable_definitions: None,
|
if let Some(ty) = self.registry.basic_type_by_parsed_type(&d.var_type) {
|
||||||
registry: &self.registry,
|
if ty.name() == "Upload" {
|
||||||
data: self.data,
|
return true;
|
||||||
fragments: &fragments,
|
}
|
||||||
};
|
}
|
||||||
return GQLOutputValue::resolve(self.query, &ctx).await;
|
}
|
||||||
}
|
}
|
||||||
}
|
false
|
||||||
Definition::Operation(OperationDefinition::Query(query)) => {
|
}
|
||||||
if self.operation_name.is_none()
|
|
||||||
|| self.operation_name == query.name.as_ref().map(|s| s.as_str())
|
/// Set upload files
|
||||||
{
|
pub fn set_upload(
|
||||||
let ctx = ContextBase {
|
&mut self,
|
||||||
item: &query.selection_set,
|
var_path: &str,
|
||||||
variables: self.variables.as_deref(),
|
filename: &str,
|
||||||
variable_definitions: Some(&query.variable_definitions),
|
content_type: Option<&str>,
|
||||||
registry: self.registry.clone(),
|
content: Vec<u8>,
|
||||||
data: self.data,
|
) {
|
||||||
fragments: &fragments,
|
self.variables
|
||||||
};
|
.set_upload(var_path, filename, content_type, content);
|
||||||
return GQLOutputValue::resolve(self.query, &ctx).await;
|
}
|
||||||
}
|
|
||||||
}
|
/// Execute the query.
|
||||||
Definition::Operation(OperationDefinition::Mutation(mutation)) => {
|
pub async fn execute(self) -> Result<serde_json::Value>
|
||||||
if self.operation_name.is_none()
|
where
|
||||||
|| self.operation_name == mutation.name.as_ref().map(|s| s.as_str())
|
Query: GQLObject + Send + Sync,
|
||||||
{
|
Mutation: GQLObject + Send + Sync,
|
||||||
let ctx = ContextBase {
|
{
|
||||||
item: &mutation.selection_set,
|
let ctx = ContextBase {
|
||||||
variables: self.variables.as_deref(),
|
item: &self.selection_set,
|
||||||
variable_definitions: Some(&mutation.variable_definitions),
|
variables: &self.variables,
|
||||||
registry: self.registry.clone(),
|
variable_definitions: self.variable_definitions.as_deref(),
|
||||||
data: self.data,
|
registry: self.registry.clone(),
|
||||||
fragments: &fragments,
|
data: self.data,
|
||||||
};
|
fragments: &self.fragments,
|
||||||
return GQLOutputValue::resolve(self.mutation, &ctx).await;
|
};
|
||||||
}
|
|
||||||
}
|
match self.root {
|
||||||
_ => {}
|
Root::Query(query) => return GQLOutputValue::resolve(query, &ctx).await,
|
||||||
}
|
Root::Mutation(mutation) => return GQLOutputValue::resolve(mutation, &ctx).await,
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(operation_name) = self.operation_name {
|
|
||||||
anyhow::bail!(QueryError::UnknownOperationNamed {
|
|
||||||
name: operation_name.to_string()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(serde_json::Value::Null)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,9 @@ mod r#enum;
|
||||||
mod list;
|
mod list;
|
||||||
mod optional;
|
mod optional;
|
||||||
mod query_root;
|
mod query_root;
|
||||||
|
mod upload;
|
||||||
|
|
||||||
pub use empty_mutation::GQLEmptyMutation;
|
pub use empty_mutation::GQLEmptyMutation;
|
||||||
pub use query_root::QueryRoot;
|
pub use query_root::QueryRoot;
|
||||||
pub use r#enum::{GQLEnum, GQLEnumItem};
|
pub use r#enum::{GQLEnum, GQLEnumItem};
|
||||||
|
pub use upload::Upload;
|
||||||
|
|
58
src/types/upload.rs
Normal file
58
src/types/upload.rs
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
use crate::{registry, GQLInputValue, GQLType, Value};
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
/// Upload file type
|
||||||
|
///
|
||||||
|
/// Reference: https://github.com/jaydenseric/graphql-multipart-request-spec
|
||||||
|
pub struct Upload {
|
||||||
|
pub filename: String,
|
||||||
|
pub content_type: Option<String>,
|
||||||
|
pub content: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> GQLType for Upload {
|
||||||
|
fn type_name() -> Cow<'static, str> {
|
||||||
|
Cow::Borrowed("Upload")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_type_info(registry: &mut registry::Registry) -> String {
|
||||||
|
registry.create_type::<Self, _>(|_| registry::Type::Scalar {
|
||||||
|
name: Self::type_name().to_string(),
|
||||||
|
description: None,
|
||||||
|
is_valid: |value| match value {
|
||||||
|
Value::String(s) => s.starts_with("file:"),
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> GQLInputValue for Upload {
|
||||||
|
fn parse(value: &Value) -> Option<Self> {
|
||||||
|
if let Value::String(s) = value {
|
||||||
|
if s.starts_with("file:") {
|
||||||
|
let s = &s[5..];
|
||||||
|
if let Some(idx) = s.find("|") {
|
||||||
|
let name_and_type = &s[..idx];
|
||||||
|
let content = &s[idx + 1..];
|
||||||
|
if let Some(type_idx) = name_and_type.find(":") {
|
||||||
|
let name = &name_and_type[..type_idx];
|
||||||
|
let mime_type = &name_and_type[type_idx + 1..];
|
||||||
|
return Some(Self {
|
||||||
|
filename: name.to_string(),
|
||||||
|
content_type: Some(mime_type.to_string()),
|
||||||
|
content: content.as_bytes().to_vec(),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return Some(Self {
|
||||||
|
filename: name_and_type.to_string(),
|
||||||
|
content_type: None,
|
||||||
|
content: content.as_bytes().to_vec(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
|
@ -35,7 +35,8 @@ pub fn check_rules(registry: &Registry, doc: &Document) -> Result<()> {
|
||||||
.with(rules::PossibleFragmentSpreads::default())
|
.with(rules::PossibleFragmentSpreads::default())
|
||||||
.with(rules::ProvidedNonNullArguments)
|
.with(rules::ProvidedNonNullArguments)
|
||||||
.with(rules::KnownDirectives::default())
|
.with(rules::KnownDirectives::default())
|
||||||
.with(rules::OverlappingFieldsCanBeMerged);
|
.with(rules::OverlappingFieldsCanBeMerged)
|
||||||
|
.with(rules::UploadFile);
|
||||||
|
|
||||||
visit(&mut visitor, &mut ctx, doc);
|
visit(&mut visitor, &mut ctx, doc);
|
||||||
if !ctx.errors.is_empty() {
|
if !ctx.errors.is_empty() {
|
||||||
|
|
|
@ -19,6 +19,7 @@ mod unique_argument_names;
|
||||||
mod unique_fragment_names;
|
mod unique_fragment_names;
|
||||||
mod unique_operation_names;
|
mod unique_operation_names;
|
||||||
mod unique_variable_names;
|
mod unique_variable_names;
|
||||||
|
mod upload_file;
|
||||||
mod variables_are_input_types;
|
mod variables_are_input_types;
|
||||||
mod variables_in_allowed_position;
|
mod variables_in_allowed_position;
|
||||||
|
|
||||||
|
@ -43,5 +44,6 @@ pub use unique_argument_names::UniqueArgumentNames;
|
||||||
pub use unique_fragment_names::UniqueFragmentNames;
|
pub use unique_fragment_names::UniqueFragmentNames;
|
||||||
pub use unique_operation_names::UniqueOperationNames;
|
pub use unique_operation_names::UniqueOperationNames;
|
||||||
pub use unique_variable_names::UniqueVariableNames;
|
pub use unique_variable_names::UniqueVariableNames;
|
||||||
|
pub use upload_file::UploadFile;
|
||||||
pub use variables_are_input_types::VariablesAreInputTypes;
|
pub use variables_are_input_types::VariablesAreInputTypes;
|
||||||
pub use variables_in_allowed_position::VariableInAllowedPosition;
|
pub use variables_in_allowed_position::VariableInAllowedPosition;
|
||||||
|
|
|
@ -9,7 +9,7 @@ impl<'a> Visitor<'a> for ScalarLeafs {
|
||||||
fn enter_field(&mut self, ctx: &mut ValidatorContext<'a>, field: &'a Field) {
|
fn enter_field(&mut self, ctx: &mut ValidatorContext<'a>, field: &'a Field) {
|
||||||
if let Some(ty) = ctx.parent_type() {
|
if let Some(ty) = ctx.parent_type() {
|
||||||
if let Some(schema_field) = ty.field_by_name(&field.name) {
|
if let Some(schema_field) = ty.field_by_name(&field.name) {
|
||||||
if let Some(ty) = ctx.registry.get_basic_type(&schema_field.ty) {
|
if let Some(ty) = ctx.registry.basic_type_by_typename(&schema_field.ty) {
|
||||||
if ty.is_leaf() && !field.selection_set.items.is_empty() {
|
if ty.is_leaf() && !field.selection_set.items.is_empty() {
|
||||||
ctx.report_error(vec![field.position], format!(
|
ctx.report_error(vec![field.position], format!(
|
||||||
"Field \"{}\" must not have a selection since type \"{}\" has no subfields",
|
"Field \"{}\" must not have a selection since type \"{}\" has no subfields",
|
||||||
|
|
30
src/validation/rules/upload_file.rs
Normal file
30
src/validation/rules/upload_file.rs
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
use crate::validation::context::ValidatorContext;
|
||||||
|
use crate::validation::visitor::Visitor;
|
||||||
|
use graphql_parser::query::OperationDefinition;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct UploadFile;
|
||||||
|
|
||||||
|
impl<'a> Visitor<'a> for UploadFile {
|
||||||
|
fn enter_operation_definition(
|
||||||
|
&mut self,
|
||||||
|
ctx: &mut ValidatorContext<'a>,
|
||||||
|
operation_definition: &'a OperationDefinition,
|
||||||
|
) {
|
||||||
|
match operation_definition {
|
||||||
|
OperationDefinition::Query(query) => {
|
||||||
|
for var in &query.variable_definitions {
|
||||||
|
if let Some(ty) = ctx.registry.basic_type_by_parsed_type(&var.var_type) {
|
||||||
|
if ty.name() == "Upload" {
|
||||||
|
ctx.report_error(
|
||||||
|
vec![var.position],
|
||||||
|
"The Upload type is only allowed to be defined on a mutation",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,7 +13,7 @@ impl<'a> Visitor<'a> for VariablesAreInputTypes {
|
||||||
) {
|
) {
|
||||||
if let Some(ty) = ctx
|
if let Some(ty) = ctx
|
||||||
.registry
|
.registry
|
||||||
.get_basic_type(&variable_definition.var_type.to_string())
|
.basic_type_by_parsed_type(&variable_definition.var_type)
|
||||||
{
|
{
|
||||||
if !ty.is_input() {
|
if !ty.is_input() {
|
||||||
ctx.report_error(
|
ctx.report_error(
|
||||||
|
|
|
@ -404,7 +404,9 @@ fn visit_selection<'a, V: Visitor<'a>>(
|
||||||
Selection::Field(field) => {
|
Selection::Field(field) => {
|
||||||
if let Some(schema_field) = ctx.current_type().field_by_name(&field.name) {
|
if let Some(schema_field) = ctx.current_type().field_by_name(&field.name) {
|
||||||
ctx.with_type(
|
ctx.with_type(
|
||||||
ctx.registry.get_basic_type(&schema_field.ty).unwrap(),
|
ctx.registry
|
||||||
|
.basic_type_by_typename(&schema_field.ty)
|
||||||
|
.unwrap(),
|
||||||
|ctx| {
|
|ctx| {
|
||||||
visit_field(v, ctx, field);
|
visit_field(v, ctx, field);
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue
Block a user