Skip to main content

postrust_graphql/
hasura.rs

1//! The response envelope Hasura clients expect.
2//!
3//! async-graphql answers in the shape the GraphQL specification describes:
4//! `data` is always present, errors carry `locations` and a `path` that is a
5//! list of segments. Hasura answers in a narrower shape of its own, and client
6//! code branches on it:
7//!
8//! ```json
9//! {"errors": [{"message": "...",
10//!              "extensions": {"path": "$.selectionSet.author", "code": "validation-failed"}}]}
11//! ```
12//!
13//! Two differences matter to a client. A failed request has no `data` key at
14//! all, rather than `"data": null` -- code written as `if (body.data)` reads
15//! the two the same way, but code written as `if ('data' in body)` does not.
16//! And `extensions.code` is the machine-readable half: it is what a client
17//! switches on to tell a permission failure from a constraint violation, and
18//! the message text is only for a human.
19//!
20//! The status code is 200 for almost all of this: a GraphQL error is a value
21//! in the response body, not a transport failure. Of the 468 cases in Hasura's
22//! own corpus that this server is measured against, 465 answer 200 --
23//! including every constraint violation, every refusal to authenticate, and
24//! every permission failure at `/v1/graphql`.
25//!
26//! The exception belongs to the *endpoint*, not to the error. All three cases
27//! that answer 400 were sent to `/v1alpha1/graphql`, the address Hasura served
28//! before `/v1`, and all three are a write refused by the `check` of an insert
29//! or update permission. The same refusal at `/v1/graphql` is a 200. Five
30//! cases prove the second half: identical bodies, identical codes, different
31//! status, and the only thing that differs is where they were sent.
32//!
33//! This was read out of the corpus's declared statuses once and read wrongly:
34//! taking it for a rule about the error meant answering 400 at `/v1/graphql`,
35//! which is three cases' worth of wrong. [`status_for`] takes the endpoint.
36
37use async_graphql::{Response, ServerError};
38use serde_json::{json, Map, Value};
39use std::collections::HashMap;
40
41/// Hasura's `extensions.code` for a server error.
42///
43/// The code is inferred from the error's own extensions where the resolver
44/// set one, and otherwise from the message. Guessing from text is not
45/// something to be proud of, but the alternative -- omitting the code -- is
46/// worse: a client that switches on it would fall through to its default
47/// branch for every error this server produces.
48fn code_for(error: &ServerError) -> &'static str {
49    if let Some(extensions) = &error.extensions {
50        if let Some(Value::String(code)) = extensions.get("code").map(value_of) {
51            return match code.as_str() {
52                "validation-failed" => "validation-failed",
53                "bad-request" => "bad-request",
54                // Nothing authenticated the request, or the role it claimed is
55                // not one it may claim. Distinct from `permission-error`,
56                // which is a rule refusing an authenticated caller.
57                "access-denied" => "access-denied",
58                // What Hasura answers when a permission needs a session
59                // variable the caller does not carry: the variable was looked
60                // for and is not there.
61                "not-found" => "not-found",
62                "permission-error" => "permission-error",
63                "constraint-violation" => "constraint-violation",
64                "data-exception" => "data-exception",
65                "not-supported" => "not-supported",
66                "parse-failed" => "parse-failed",
67                _ => "unexpected",
68            };
69        }
70    }
71
72    let message = error.message.to_ascii_lowercase();
73    // Validation is tested first: a rejected enum value says "enumeration type
74    // ... does not contain the value", which mentions no constraint but reads
75    // as one to a later test that only looks for the word.
76    if message.contains("invalid value for argument")
77        || message.contains("does not contain the value")
78        || message.contains("is not defined by operation")
79        // A role granted only reads has no mutation root, so a document naming
80        // a mutation names an operation this schema does not have. That is the
81        // document being wrong about the schema, which is what validation is.
82        // async-graphql's wording, classified here rather than there.
83        || message.contains("not configured for mutations")
84        || message.contains("not configured for subscriptions")
85    {
86        "validation-failed"
87    } else if message.contains("permission") || message.contains("not allowed") {
88        "permission-error"
89    } else if message.contains("violates") || message.contains("constraint") {
90        "constraint-violation"
91    } else if message.contains("unknown argument")
92        || message.contains("cannot query field")
93        || message.contains("expected")
94        || message.contains("unknown field")
95        // Everything the variable rules say. A document that names a variable
96        // it never declared is refused before a resolver runs, so calling it a
97        // database error sends a client looking in the wrong place.
98        || message.starts_with("variable \"")
99    {
100        "validation-failed"
101    } else {
102        "postgres-error"
103    }
104}
105
106fn value_of(value: &async_graphql::Value) -> Value {
107    serde_json::to_value(value).unwrap_or(Value::Null)
108}
109
110/// Render an error's path the way Hasura spells it.
111///
112/// Hasura writes a JSONPath-ish string rooted at the operation --
113/// `$.selectionSet.author.args.where` -- where the specification writes a list
114/// of segments. A list index stays an index; everything else is a field name.
115fn path_for(error: &ServerError) -> String {
116    use async_graphql::PathSegment;
117
118    // Written where the error was raised, when whoever raised it knew where it
119    // was. That is the only way to reach an *argument*: `$.selectionSet.
120    // insert_author.args.objects[0].bio` names a place inside a value the
121    // client sent, and a response path only ever names a field of the answer.
122    if let Some(Value::String(path)) = error
123        .extensions
124        .as_ref()
125        .and_then(|extensions| extensions.get("path"))
126        .map(value_of)
127    {
128        return path;
129    }
130
131    if error.path.is_empty() {
132        return "$".to_string();
133    }
134    // Otherwise the response path, in Hasura's spelling: a field of a field is
135    // written with the selection set that holds it between them, so `author`
136    // then `articles` is `$.selectionSet.author.selectionSet.articles` rather
137    // than `$.selectionSet.author.articles`.
138    //
139    // Which row of a list the error came from is dropped. A response path has
140    // it and Hasura's selection paths never do -- an index appears in one of
141    // its paths only inside an argument, where it names a row the client
142    // *sent*. Naming a field is what a selection path is for.
143    let mut rendered = String::from("$");
144    for segment in &error.path {
145        if let PathSegment::Field(name) = segment {
146            rendered.push_str(".selectionSet.");
147            rendered.push_str(name);
148        }
149    }
150    rendered
151}
152
153/// Convert an async-graphql response into Hasura's envelope.
154pub fn envelope(response: Response) -> Value {
155    if !response.errors.is_empty() {
156        let errors: Vec<Value> = response
157            .errors
158            .iter()
159            .map(|error| {
160                json!({
161                    "message": error.message,
162                    "extensions": {
163                        "path": path_for(error),
164                        "code": code_for(error),
165                    }
166                })
167            })
168            .collect();
169        // No `data` key. Hasura omits it entirely on failure.
170        return json!({ "errors": errors });
171    }
172
173    let mut body = Map::new();
174    body.insert("data".to_string(), value_of(&response.data));
175    Value::Object(body)
176}
177
178/// Which address the request came in on.
179///
180/// The only thing that decides a status here, so it is passed rather than
181/// guessed. `/api/graphql` is this server's own name for the endpoint and
182/// follows `/v1`, which is what a client pointed at it expects.
183#[derive(Debug, Clone, Copy, PartialEq, Eq)]
184pub enum Endpoint {
185    /// `/v1/graphql` and `/api/graphql`: every answer is a 200.
186    Current,
187    /// `/v1alpha1/graphql`: a check refused by a permission is a 400.
188    Legacy,
189}
190
191/// The HTTP status a body is answered with.
192///
193/// 200, unless a write refused by a permission's `check` reached the legacy
194/// endpoint. See the note at the top of this module: the status belongs to the
195/// address, and the same refusal at `/v1/graphql` is a 200.
196pub fn status_for(body: &Value, endpoint: Endpoint) -> u16 {
197    if endpoint == Endpoint::Current {
198        return 200;
199    }
200    let refused = body
201        .get("errors")
202        .and_then(Value::as_array)
203        .is_some_and(|errors| {
204            errors.iter().any(|error| {
205                error.get("message").and_then(Value::as_str) == Some(crate::role::CHECK_FAILED)
206            })
207        });
208    match refused {
209        true => 400,
210        false => 200,
211    }
212}
213
214/// A request refused before it was read.
215///
216/// Nothing authenticated the caller, or the role it named is not one it may
217/// name. The document is never parsed, so there is no path into it to report
218/// and no operation to blame -- which is why Hasura answers `$` here and this
219/// does too. Still a 200: a GraphQL error is a value in the body.
220pub fn denied(message: &str) -> Value {
221    let mut error = ServerError::new(message, None);
222    let mut extensions = async_graphql::ErrorExtensionValues::default();
223    extensions.set("code", "access-denied");
224    error.extensions = Some(extensions);
225
226    let mut response = Response::new(async_graphql::Value::Null);
227    response.errors = vec![error];
228    envelope(response)
229}
230
231/// Whether the request is an Apollo persisted-query handshake.
232///
233/// A client with automatic persisted queries sends the hash alone, and sends
234/// the document only when the server says it has not seen that hash. Neither
235/// server implements the protocol; the difference is that Hasura says so --
236/// `PersistedQueryNotSupported` -- and this used to run whatever document was
237/// beside the hash, which is the answer that looks like the handshake worked.
238/// A client would then send hash-only requests and get nothing back.
239pub fn persisted_query(request: &async_graphql::Request) -> bool {
240    request.extensions.contains_key("persistedQuery")
241}
242
243/// Hasura's refusal for one, which is the whole answer.
244pub fn not_supported(message: &str) -> Value {
245    let mut error = ServerError::new(message, None);
246    let mut extensions = async_graphql::ErrorExtensionValues::default();
247    extensions.set("code", "not-supported");
248    extensions.set("path", "$");
249    error.extensions = Some(extensions);
250
251    let mut response = Response::new(async_graphql::Value::Null);
252    response.errors = vec![error];
253    envelope(response)
254}
255
256/// A refusal about the request itself rather than about what it asked for.
257///
258/// `bad-request` at `$`: nothing in the document is wrong, so there is no part
259/// of it to point at. A header Hasura reads and cannot parse is the case that
260/// needs it.
261pub fn malformed(message: &str) -> Value {
262    let mut error = ServerError::new(message, None);
263    let mut extensions = async_graphql::ErrorExtensionValues::default();
264    extensions.set("code", "bad-request");
265    extensions.set("path", "$");
266    error.extensions = Some(extensions);
267
268    let mut response = Response::new(async_graphql::Value::Null);
269    response.errors = vec![error];
270    envelope(response)
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276    use async_graphql::{PathSegment, ServerError};
277    use pretty_assertions::assert_eq;
278
279    fn error_at(message: &str, path: Vec<PathSegment>) -> ServerError {
280        let mut error = ServerError::new(message, None);
281        error.path = path;
282        error
283    }
284
285    #[test]
286    fn only_a_refused_write_is_answered_with_a_failure_status() {
287        // Hasura's own inconsistency, reproduced because a client may branch
288        // on it: this one error is 400 and every other refusal is 200.
289        let refused = json!({"errors": [{"message": crate::role::CHECK_FAILED,
290                                         "extensions": {"code": "permission-error"}}]});
291        assert_eq!(status_for(&refused, Endpoint::Legacy), 400);
292        // The same refusal at the address a Hasura client actually uses.
293        assert_eq!(status_for(&refused, Endpoint::Current), 200);
294
295        // The same code, a different error: four cases in the corpus.
296        let denied = json!({"errors": [{"message": "not allowed",
297                                        "extensions": {"code": "permission-error"}}]});
298        assert_eq!(status_for(&denied, Endpoint::Legacy), 200);
299        assert_eq!(
300            status_for(&json!({"data": {"article": []}}), Endpoint::Legacy),
301            200
302        );
303    }
304
305    #[test]
306    fn a_role_may_be_refused_the_schema_without_being_refused_its_rows() {
307        let asked = |query: &str| {
308            let doc = async_graphql::parser::parse_query(query).unwrap();
309            introspection_refusal(&doc, "reader").map(|e| e.message)
310        };
311        let refusal = Some("introspection is disabled for role: \"reader\"".to_string());
312        assert_eq!(asked("{ __schema { types { name } } }"), refusal);
313        assert_eq!(asked("{ __type(name: \"article\") { name } }"), refusal);
314        // Not introspection in the sense that matters: every client library
315        // sends it, and Hasura answers it.
316        assert_eq!(asked("{ article { __typename id } }"), None);
317        assert_eq!(asked("{ article { id } }"), None);
318    }
319
320    #[test]
321    fn a_failed_response_has_no_data_key() {
322        let mut response = Response::new(async_graphql::Value::Null);
323        response.errors = vec![error_at("no", vec![])];
324        let body = envelope(response);
325        assert!(body.get("data").is_none());
326        assert!(body.get("errors").is_some());
327    }
328
329    #[test]
330    fn a_successful_response_has_no_errors_key() {
331        let body = envelope(Response::new(async_graphql::Value::Null));
332        assert!(body.get("errors").is_none());
333        assert!(body.get("data").is_some());
334    }
335
336    #[test]
337    fn path_is_rooted_at_the_selection_set() {
338        let error = error_at(
339            "boom",
340            vec![
341                PathSegment::Field("insert_author".to_string()),
342                PathSegment::Index(0),
343                PathSegment::Field("name".to_string()),
344            ],
345        );
346        assert_eq!(
347            path_for(&error),
348            "$.selectionSet.insert_author.selectionSet.name"
349        );
350    }
351
352    /// A path written where the error was raised wins over the response path,
353    /// because it can name a place the response path cannot: inside an
354    /// argument the client sent.
355    /// A hash beside the document is a handshake, and neither server speaks
356    /// it. Saying so is the difference between a client that falls back and
357    /// one that sends hash-only requests into silence.
358    #[test]
359    fn a_persisted_query_is_refused_as_unsupported() {
360        let plain = async_graphql::Request::new("{ author { id } }");
361        assert!(!persisted_query(&plain));
362
363        let mut carried = async_graphql::Request::new("{ author { id } }");
364        carried.extensions.0.insert(
365            "persistedQuery".to_string(),
366            async_graphql::Value::Object(Default::default()),
367        );
368        assert!(persisted_query(&carried));
369
370        assert_eq!(
371            not_supported("PersistedQueryNotSupported"),
372            json!({"errors": [{
373                "message": "PersistedQueryNotSupported",
374                "extensions": {"path": "$", "code": "not-supported"}
375            }]})
376        );
377    }
378
379    #[test]
380    fn a_path_given_with_the_error_is_the_one_answered() {
381        let mut error = error_at(
382            "boom",
383            vec![PathSegment::Field("insert_author".to_string())],
384        );
385        let mut extensions = async_graphql::ErrorExtensionValues::default();
386        extensions.set("code", "permission-error");
387        extensions.set("path", "$.selectionSet.insert_author.args.objects");
388        error.extensions = Some(extensions);
389        assert_eq!(
390            path_for(&error),
391            "$.selectionSet.insert_author.args.objects"
392        );
393    }
394
395    #[test]
396    fn an_error_with_no_path_is_rooted_at_the_document() {
397        assert_eq!(path_for(&error_at("boom", vec![])), "$");
398    }
399
400    #[test]
401    fn a_validation_message_is_coded_as_one() {
402        let error = ServerError::new("Unknown argument \"filter\" on field \"author\"", None);
403        assert_eq!(code_for(&error), "validation-failed");
404    }
405
406    #[test]
407    fn a_constraint_message_is_coded_as_one() {
408        let error = ServerError::new("duplicate key value violates unique constraint", None);
409        assert_eq!(code_for(&error), "constraint-violation");
410    }
411}
412
413/// Drop variable definitions the document never uses.
414///
415/// The specification's "All Variables Used" rule makes
416/// `query ($tags: jsonb) { article(where: {tags: {_contains: "latest"}}) { id } }`
417/// an invalid document, and async-graphql refuses it. Hasura executes it. A
418/// client that has been sending that query for years -- because a filter was
419/// edited and the declaration was left behind -- gets an answer from the server
420/// it is migrating off and an error from this one, which is the kind of
421/// difference a migration discovers in production.
422///
423/// So the document is parsed here, the unused declarations are removed, and
424/// what validation sees has nothing to complain about. Every other rule still
425/// runs: this makes one specific refusal go away, not validation in general.
426/// A document that does not parse is handed on untouched, so the parse error is
427/// reported by the executor with its own position rather than by this.
428pub fn allow_unused_variables(request: async_graphql::Request) -> async_graphql::Request {
429    prepare(None, request, None).unwrap_or_else(|(request, _)| request)
430}
431
432/// Ready a request for execution, and refuse it where async-graphql would not.
433///
434/// Two passes over the same parsed document, because parsing it twice to do
435/// them separately would be the only reason to separate them:
436///
437/// - variable declarations nothing uses are dropped, since Hasura executes
438///   such a document and the specification does not;
439/// - variables used where their declared type does not fit are refused, since
440///   the specification says so and async-graphql does not.
441///
442/// `schema` is what the second pass needs -- the types a variable is being
443/// used against. Without one only the first pass runs.
444///
445/// `Err` carries the request back beside the errors, so the caller can answer
446/// with them rather than executing.
447// The error carries the request back, which is the whole point of it: the
448// caller answers with the errors instead of executing, and needs the request
449// to do anything else with. Boxing it would hide that.
450#[allow(clippy::result_large_err)]
451pub fn prepare(
452    schema: Option<&async_graphql::dynamic::Schema>,
453    request: async_graphql::Request,
454    // The role introspection is withheld from, where it is withheld from this
455    // one. Checked here because this is where the document is already parsed,
456    // and because refusing it is the whole answer: a schema a role may not
457    // read is not one it may read a little of.
458    introspection_disabled_for: Option<&str>,
459) -> Result<async_graphql::Request, (async_graphql::Request, Vec<ServerError>)> {
460    let mut request = rewrite_document(schema, request);
461    // Parsed again rather than threaded through: `set_parsed_query` takes the
462    // document by value and gives nothing back, and re-parsing a document that
463    // has already parsed once is not where the time goes.
464    //
465    // A document that does not parse is answered here rather than left to
466    // async-graphql, which reports it the way a compiler does -- the offending
467    // line, a caret under the column, and what it expected instead. Hasura
468    // says the document is not a query and names the document, and a client
469    // showing that to a user is showing text it already ships. Nothing further
470    // can run either way, so this is the whole of the answer.
471    let doc = match async_graphql::parser::parse_query(&request.query) {
472        Ok(doc) => doc,
473        Err(_) => {
474            return Err((
475                request,
476                vec![coded(
477                    "not a valid graphql query".to_string(),
478                    async_graphql::Pos::default(),
479                    "$.query",
480                )],
481            ))
482        }
483    };
484    if let Some(schema) = schema {
485        if let Some(role) = introspection_disabled_for {
486            if let Some(error) = introspection_refusal(&doc, role) {
487                return Err((request, vec![error]));
488            }
489        }
490        let mut errors = variable_errors(&doc, schema.registry(), &request.variables);
491        // The first refusal is the whole answer. Hasura validates a request
492        // as a parser reads one -- it stops where it cannot go on -- and in
493        // 468 replayed cases it never answered with a second error. A query
494        // naming two fields that do not exist is refused for the first of
495        // them, and so is one whose `on_conflict` carries an unknown key
496        // beside a missing required one.
497        //
498        // Reporting all of them is the more useful answer and the wrong one
499        // here: a client written against Hasura reads `errors[0]`, and the
500        // rest of the list is text it will not show. The walk still runs to
501        // the end, because it is cheap and because stopping it would put the
502        // stopping condition in every arm.
503        errors.truncate(1);
504        if !errors.is_empty() {
505            return Err((request, errors));
506        }
507    }
508    let _ = &mut request;
509    Ok(request)
510}
511
512/// Whether a document asks the schema about itself, and the refusal if so.
513///
514/// `__typename` is not introspection in the sense that matters: it names the
515/// type of a row a client is already reading, every client library sends it,
516/// and Hasura answers it for a role introspection is disabled for. `__schema`
517/// and `__type` are the two that read the schema as data.
518///
519/// The message and the code are Hasura's.
520fn introspection_refusal(
521    doc: &async_graphql::parser::types::ExecutableDocument,
522    role: &str,
523) -> Option<ServerError> {
524    use async_graphql::parser::types::{OperationType, Selection};
525
526    let operations: Vec<_> = match &doc.operations {
527        async_graphql::parser::types::DocumentOperations::Single(one) => vec![one],
528        async_graphql::parser::types::DocumentOperations::Multiple(many) => many.values().collect(),
529    };
530
531    let asks = operations.iter().any(|operation| {
532        operation.node.ty == OperationType::Query
533            && operation
534                .node
535                .selection_set
536                .node
537                .items
538                .iter()
539                .any(|item| match &item.node {
540                    Selection::Field(field) => {
541                        matches!(field.node.name.node.as_str(), "__schema" | "__type")
542                    }
543                    _ => false,
544                })
545    });
546
547    match asks {
548        false => None,
549        true => {
550            let mut error = ServerError::new(
551                format!("introspection is disabled for role: \"{}\"", role),
552                None,
553            );
554            let mut extensions = async_graphql::ErrorExtensionValues::default();
555            extensions.set("code", "not-supported");
556            error.extensions = Some(extensions);
557            Some(error)
558        }
559    }
560}
561
562/// The edits made to a document before it is validated.
563///
564/// A declaration nothing uses is dropped, and a value written as a string
565/// where a number or a boolean is expected becomes one. Both are things
566/// Hasura accepts and the specification does not, and both are edits to the
567/// same parsed document -- which is why they are one pass: `set_parsed_query`
568/// takes the document by value, so a second pass would have to re-parse the
569/// source text and would undo the first.
570fn rewrite_document(
571    schema: Option<&async_graphql::dynamic::Schema>,
572    mut request: async_graphql::Request,
573) -> async_graphql::Request {
574    use async_graphql::parser::types::{
575        DocumentOperations, ExecutableDocument, Selection, SelectionSet,
576    };
577    use async_graphql::Name;
578    // The executable `Value`, which has a `Variable` case; `async_graphql::Value`
579    // is the constant one a variable has already been substituted into.
580    use async_graphql_value::Value;
581
582    let Ok(mut doc): Result<ExecutableDocument, _> =
583        async_graphql::parser::parse_query(&request.query)
584    else {
585        return request;
586    };
587
588    fn from_value(value: &Value, used: &mut std::collections::HashSet<Name>) {
589        match value {
590            Value::Variable(name) => {
591                used.insert(name.clone());
592            }
593            Value::List(items) => items.iter().for_each(|item| from_value(item, used)),
594            Value::Object(fields) => fields.values().for_each(|field| from_value(field, used)),
595            _ => {}
596        }
597    }
598
599    fn from_selection_set(set: &SelectionSet, used: &mut std::collections::HashSet<Name>) {
600        for selection in &set.items {
601            let (directives, arguments, nested) = match &selection.node {
602                Selection::Field(field) => (
603                    &field.node.directives,
604                    Some(&field.node.arguments),
605                    Some(&field.node.selection_set),
606                ),
607                Selection::FragmentSpread(spread) => (&spread.node.directives, None, None),
608                Selection::InlineFragment(fragment) => (
609                    &fragment.node.directives,
610                    None,
611                    Some(&fragment.node.selection_set),
612                ),
613            };
614            for directive in directives {
615                for (_, value) in &directive.node.arguments {
616                    from_value(&value.node, used);
617                }
618            }
619            for (_, value) in arguments.into_iter().flatten() {
620                from_value(&value.node, used);
621            }
622            if let Some(nested) = nested {
623                from_selection_set(&nested.node, used);
624            }
625        }
626    }
627
628    // Every variable named anywhere in the document, rather than per operation.
629    // Over-counting only keeps a declaration that would have been kept before,
630    // and a variable declared by one operation and used by another is exactly
631    // the case this is here to stop refusing.
632    let mut used = std::collections::HashSet::new();
633    for (_, operation) in doc.operations.iter() {
634        for directive in &operation.node.directives {
635            for (_, value) in &directive.node.arguments {
636                from_value(&value.node, &mut used);
637            }
638        }
639        from_selection_set(&operation.node.selection_set.node, &mut used);
640    }
641    for fragment in doc.fragments.values() {
642        from_selection_set(&fragment.node.selection_set.node, &mut used);
643    }
644
645    let mut edited = false;
646    {
647        let mut prune = |operation: &mut async_graphql::Positioned<
648            async_graphql::parser::types::OperationDefinition,
649        >| {
650            let before = operation.node.variable_definitions.len();
651            operation
652                .node
653                .variable_definitions
654                .retain(|definition| used.contains(&definition.node.name.node));
655            edited |= operation.node.variable_definitions.len() != before;
656        };
657        match &mut doc.operations {
658            DocumentOperations::Single(operation) => prune(operation),
659            DocumentOperations::Multiple(operations) => operations.values_mut().for_each(prune),
660        }
661    }
662
663    // A value written as a string where a number or a boolean is expected.
664    // `insert_test_types(objects: [{c1_smallint: "32767", c20_boolean:
665    // "true"}])` is a mutation Hasura performs: a column's value is read the
666    // way PostgreSQL reads a literal, which takes either spelling, while the
667    // schema still introspects as `Int`. So does `article(offset: "1")`.
668    //
669    // `limit` is the exception, and the corpus is explicit about it: `limit:
670    // "3"` is refused in the same breath that `offset: "1"` is answered. It is
671    // the one Int in the schema that is the engine's own rather than a
672    // column's, and it is the only place a string is not a number.
673    if let Some(schema) = schema {
674        let registry = schema.registry();
675        {
676            let mut coerce = |operation: &mut async_graphql::Positioned<
677                async_graphql::parser::types::OperationDefinition,
678            >| {
679                use async_graphql::parser::types::OperationType;
680                let root = match operation.node.ty {
681                    OperationType::Query => Some(registry.query_type.as_str()),
682                    OperationType::Mutation => registry.mutation_type.as_deref(),
683                    OperationType::Subscription => registry.subscription_type.as_deref(),
684                };
685                if let Some(root) = root {
686                    edited |= coerce_selection_set(
687                        registry,
688                        &mut operation.node.selection_set.node,
689                        root,
690                    );
691                }
692            };
693            match &mut doc.operations {
694                DocumentOperations::Single(operation) => coerce(operation),
695                DocumentOperations::Multiple(operations) => {
696                    operations.values_mut().for_each(coerce)
697                }
698            }
699        }
700        // A fragment names the type it is on, so it is walked on its own
701        // rather than from wherever it is spread -- which also means a cyclic
702        // spread cannot walk forever.
703        for fragment in doc.fragments.values_mut() {
704            let on = fragment.node.type_condition.node.on.node.to_string();
705            edited |= coerce_selection_set(registry, &mut fragment.node.selection_set.node, &on);
706        }
707    }
708
709    if edited {
710        request.set_parsed_query(doc);
711    }
712    request
713}
714
715/// Coerce the written values of one selection set, and of everything under it.
716///
717/// Type-directed, the same walk [`Usage`] makes: a field names its arguments,
718/// an argument names an input object, an input object names its fields, and at
719/// the leaves sits the type a written value has to be. Returns whether
720/// anything changed.
721fn coerce_selection_set(
722    registry: &async_graphql::registry::Registry,
723    set: &mut async_graphql::parser::types::SelectionSet,
724    type_name: &str,
725) -> bool {
726    use async_graphql::parser::types::Selection;
727    use async_graphql::registry::MetaTypeName;
728
729    let mut edited = false;
730    for selection in &mut set.items {
731        match &mut selection.node {
732            Selection::Field(field) => {
733                let meta = registry
734                    .types
735                    .get(type_name)
736                    .and_then(|ty| ty.field_by_name(field.node.name.node.as_str()));
737                for (name, value) in &mut field.node.arguments {
738                    // The engine's own Int, which is strict where a column's
739                    // is not.
740                    if name.node.as_str() == "limit" {
741                        continue;
742                    }
743                    let Some(argument) = meta.and_then(|meta| meta.args.get(name.node.as_str()))
744                    else {
745                        continue;
746                    };
747                    let ty = argument.ty.clone();
748                    edited |= coerce_value(registry, &mut value.node, &ty);
749                }
750                if let Some(meta) = meta {
751                    let inner = MetaTypeName::concrete_typename(&meta.ty).to_string();
752                    edited |=
753                        coerce_selection_set(registry, &mut field.node.selection_set.node, &inner);
754                }
755            }
756            Selection::InlineFragment(fragment) => {
757                let inner = fragment
758                    .node
759                    .type_condition
760                    .as_ref()
761                    .map(|condition| condition.node.on.node.to_string())
762                    .unwrap_or_else(|| type_name.to_string());
763                edited |=
764                    coerce_selection_set(registry, &mut fragment.node.selection_set.node, &inner);
765            }
766            Selection::FragmentSpread(_) => {}
767        }
768    }
769    edited
770}
771
772/// One written value, against the type of the place it was written in.
773fn coerce_value(
774    registry: &async_graphql::registry::Registry,
775    value: &mut async_graphql_value::Value,
776    expected: &str,
777) -> bool {
778    use async_graphql::registry::{MetaType, MetaTypeName};
779    use async_graphql_value::Value;
780
781    match value {
782        Value::List(items) => {
783            // A single value may be written where a list is expected, so the
784            // item type stands in for either.
785            let inner = match MetaTypeName::create(expected).unwrap_non_null() {
786                MetaTypeName::List(inner) => inner.to_string(),
787                _ => expected.to_string(),
788            };
789            let mut edited = false;
790            for item in items {
791                edited |= coerce_value(registry, item, &inner);
792            }
793            edited
794        }
795        Value::Object(fields) => {
796            let name = MetaTypeName::concrete_typename(expected);
797            let Some(MetaType::InputObject { input_fields, .. }) = registry.types.get(name) else {
798                return false;
799            };
800            let types: Vec<(async_graphql::Name, String)> = fields
801                .keys()
802                .filter_map(|key| {
803                    input_fields
804                        .get(key.as_str())
805                        .map(|field| (key.clone(), field.ty.clone()))
806                })
807                .collect();
808            let mut edited = false;
809            for (key, ty) in types {
810                if let Some(item) = fields.get_mut(&key) {
811                    edited |= coerce_value(registry, item, &ty);
812                }
813            }
814            edited
815        }
816        Value::String(text) => {
817            let Some(coerced) = as_written(text, MetaTypeName::concrete_typename(expected)) else {
818                return false;
819            };
820            *value = coerced;
821            true
822        }
823        _ => false,
824    }
825}
826
827/// A string read as the type it was written where, if it can be.
828///
829/// Only the three GraphQL scalars that are strict about it: everything else in
830/// this schema is a scalar of its own, which takes a string already.
831fn as_written(text: &str, expected: &str) -> Option<async_graphql_value::Value> {
832    use async_graphql_value::{Number, Value};
833    match expected {
834        "Int" => text.parse::<i64>().ok().map(|n| Value::Number(n.into())),
835        "Float" => text
836            .parse::<f64>()
837            .ok()
838            .and_then(Number::from_f64)
839            .map(Value::Number),
840        "Boolean" => match text {
841            "true" => Some(Value::Boolean(true)),
842            "false" => Some(Value::Boolean(false)),
843            _ => None,
844        },
845        _ => None,
846    }
847}
848
849#[cfg(test)]
850mod unused_variable_tests {
851    use super::*;
852
853    fn declarations(query: &str) -> Vec<String> {
854        let request = allow_unused_variables(async_graphql::Request::new(query));
855        // A request whose declarations were all used is handed on with nothing
856        // parsed, so what the executor sees is the source text.
857        let mut request = request;
858        let doc = request.parsed_query().expect("the query parses");
859        doc.operations
860            .iter()
861            .flat_map(|(_, operation)| operation.node.variable_definitions.iter())
862            .map(|definition| definition.node.name.node.to_string())
863            .collect()
864    }
865
866    #[test]
867    fn a_variable_nothing_names_is_dropped() {
868        assert_eq!(
869            declarations("query ($tags: jsonb) { article(where: {id: {_eq: 1}}) { id } }"),
870            Vec::<String>::new()
871        );
872    }
873
874    #[test]
875    fn a_variable_an_argument_names_is_kept() {
876        assert_eq!(
877            declarations("query ($id: Int) { article(where: {id: {_eq: $id}}) { id } }"),
878            vec!["id".to_string()]
879        );
880    }
881
882    #[test]
883    fn a_variable_named_inside_a_list_is_kept() {
884        assert_eq!(
885            declarations("query ($id: Int) { article(where: {_or: [{id: {_eq: $id}}]}) { id } }"),
886            vec!["id".to_string()]
887        );
888    }
889
890    #[test]
891    fn a_variable_only_a_fragment_names_is_kept() {
892        assert_eq!(
893            declarations(
894                "query ($n: Int) { author { ...rows } } \
895                 fragment rows on author { articles(limit: $n) { id } }"
896            ),
897            vec!["n".to_string()]
898        );
899    }
900
901    #[test]
902    fn a_document_that_does_not_parse_is_left_alone() {
903        let request = allow_unused_variables(async_graphql::Request::new("query ("));
904        assert_eq!(request.query, "query (");
905    }
906}
907
908/// Refuse a variable used where its declared type does not fit.
909///
910/// The specification's "All Variable Usages Are Allowed" rule: `query
911/// ($limit: String) { author(limit: $limit) }` is invalid, because `limit` is
912/// an `Int` and a `String` is not one. async-graphql carries exactly this rule
913/// and it does not fire -- verified against 7.0.17 with a static schema, a
914/// dynamic one, and a built-in directive, none of which report -- so the check
915/// is made here instead. It is the one place where being lax is worse than
916/// being wrong: the client is answered with data when what it wrote cannot
917/// mean what it thinks, and it finds out from the rows.
918///
919/// The messages are Hasura's, since a client that shows them to a developer is
920/// showing text it already ships.
921fn variable_errors(
922    doc: &async_graphql::parser::types::ExecutableDocument,
923    registry: &async_graphql::registry::Registry,
924    variables: &async_graphql::Variables,
925) -> Vec<ServerError> {
926    use async_graphql::parser::types::{DocumentOperations, OperationType};
927
928    let mut errors: Vec<ServerError> = Vec::new();
929
930    let operations: Vec<
931        &async_graphql::Positioned<async_graphql::parser::types::OperationDefinition>,
932    > = match &doc.operations {
933        DocumentOperations::Single(operation) => vec![operation],
934        DocumentOperations::Multiple(operations) => operations.values().collect(),
935    };
936
937    for operation in operations {
938        let root = match operation.node.ty {
939            OperationType::Query => Some(registry.query_type.as_str()),
940            OperationType::Mutation => registry.mutation_type.as_deref(),
941            OperationType::Subscription => registry.subscription_type.as_deref(),
942        };
943        let Some(root) = root else {
944            // A mutation sent to a schema that has no mutation root. Hasura
945            // answers this rather than "the field you named is not there",
946            // because there is no type for it to have not been in.
947            if operation.node.ty == OperationType::Mutation {
948                errors.push(coded("no mutations exist".to_string(), operation.pos, "$"));
949            }
950            continue;
951        };
952
953        // What each variable was declared as, and what a null in it means.
954        let mut declared: HashMap<&str, String> = HashMap::new();
955        for definition in &operation.node.variable_definitions {
956            let name = definition.node.name.node.as_str();
957            let written = definition.node.var_type.node.to_string();
958            // A nullable declaration with a default behaves as a non-null
959            // one: the default stands in wherever the variable is left out.
960            // A default *of* null does not -- `$author: author_insert_input =
961            // null` still cannot be used where a non-null is expected, because
962            // what it stands in with is a null.
963            let defaulted = definition
964                .node
965                .default_value
966                .as_ref()
967                .is_some_and(|value| !matches!(value.node, async_graphql::Value::Null));
968            let effective = match (definition.node.var_type.node.nullable, defaulted) {
969                (true, true) => format!("{}!", written),
970                _ => written.clone(),
971            };
972            declared.insert(name, effective);
973
974            // An explicit null for a non-null variable. The default does not
975            // save it: a default stands in for a variable that was not given,
976            // not for one that was given as null.
977            let given = variables.get(&async_graphql::Name::new(name));
978            if !definition.node.var_type.node.nullable
979                && matches!(given, Some(async_graphql::Value::Null))
980            {
981                errors.push(coded(
982                    format!("null value found for non-nullable type: \"{}\"", written),
983                    definition.pos,
984                    "$",
985                ));
986            }
987        }
988        // Walked even with nothing declared: the second thing this looks for
989        // is a null written straight into a comparison, which needs no
990        // variable to be written.
991        let mut scope = Usage {
992            registry,
993            fragments: &doc.fragments,
994            declared: &declared,
995            variables,
996            errors: &mut errors,
997            spreading: Vec::new(),
998            expansions: 0,
999        };
1000        scope.selection_set(&operation.node.selection_set.node, root, "$");
1001    }
1002
1003    errors
1004}
1005
1006/// One walk of a document, checking every variable against where it is used.
1007struct Usage<'a> {
1008    registry: &'a async_graphql::registry::Registry,
1009    fragments: &'a std::collections::HashMap<
1010        async_graphql::Name,
1011        async_graphql::Positioned<async_graphql::parser::types::FragmentDefinition>,
1012    >,
1013    declared: &'a HashMap<&'a str, String>,
1014    /// What the request gave for each variable, which is the only way to tell
1015    /// a variable standing for a null from one standing for a value.
1016    variables: &'a async_graphql::Variables,
1017    errors: &'a mut Vec<ServerError>,
1018    /// Fragments already walked, so a cycle terminates. async-graphql refuses
1019    /// cyclic fragments too, but this runs first.
1020    /// The fragments currently being spread, outermost first.
1021    ///
1022    /// A stack rather than a set, because what a cycle is named after is the
1023    /// run of fragments from the first occurrence to the repeat -- and because
1024    /// a fragment spread twice in sibling positions is not a cycle and its
1025    /// contents should be checked at both.
1026    spreading: Vec<String>,
1027    /// How many spreads have been expanded. A document whose fragments each
1028    /// spread the next twice doubles the work per level without ever
1029    /// repeating a name, so the stack alone does not bound this.
1030    expansions: usize,
1031}
1032
1033impl Usage<'_> {
1034    /// `path` is where this selection set is, in Hasura's spelling: `$` at the
1035    /// root, and `$.selectionSet.author` for the set inside that field. A
1036    /// fragment carries its spread site's path, since that is where the fields
1037    /// it contributes actually are.
1038    fn selection_set(
1039        &mut self,
1040        set: &async_graphql::parser::types::SelectionSet,
1041        type_name: &str,
1042        path: &str,
1043    ) {
1044        use async_graphql::parser::types::Selection;
1045        use async_graphql::registry::MetaTypeName;
1046
1047        for selection in &set.items {
1048            match &selection.node {
1049                Selection::Field(field) => {
1050                    let named = field.node.name.node.as_str();
1051                    let parent = self.registry.types.get(type_name);
1052                    let meta = parent.and_then(|ty| ty.field_by_name(named));
1053                    let here = format!("{}.selectionSet.{}", path, field.node.name.node);
1054                    // A field the type does not have. async-graphql reports
1055                    // this too, and in its own words -- `Unknown field "x" on
1056                    // type "y". Did you mean ...` -- so saying it here is what
1057                    // makes the answer Hasura's: this walk runs first and a
1058                    // document with an error in it never reaches validation.
1059                    //
1060                    // Only where the parent type is one this registry knows.
1061                    // Not knowing it is not evidence that the field is absent.
1062                    if meta.is_none() && parent.is_some() && !named.starts_with("__") {
1063                        self.errors.push(coded(
1064                            format!("field '{}' not found in type: '{}'", named, type_name),
1065                            field.pos,
1066                            &here,
1067                        ));
1068                        continue;
1069                    }
1070                    for (name, value) in &field.node.arguments {
1071                        let Some(argument) =
1072                            meta.and_then(|meta| meta.args.get(name.node.as_str()))
1073                        else {
1074                            // An argument the field does not take. Reported
1075                            // against the field rather than against the
1076                            // argument -- there is no such argument to point
1077                            // at, which is the complaint.
1078                            if meta.is_some() {
1079                                self.errors.push(coded(
1080                                    format!("'{}' has no argument named '{}'", named, name.node),
1081                                    name.pos,
1082                                    &here,
1083                                ));
1084                            }
1085                            continue;
1086                        };
1087                        // A location with a default of its own accepts a
1088                        // nullable variable where it says non-null, since
1089                        // leaving the variable out is then the same as not
1090                        // writing the argument.
1091                        let expected = relax(&argument.ty, argument.default_value.is_some());
1092                        let at = format!("{}.args.{}", here, name.node);
1093                        self.rows_asked_for(
1094                            name.node.as_str(),
1095                            &value.node,
1096                            &expected,
1097                            value.pos,
1098                            &at,
1099                        );
1100                        self.object_expected(&value.node, &expected, value.pos, &at);
1101                        self.value(&value.node, &expected, value.pos, &at);
1102                    }
1103                    self.one_operator_per_column(&field.node.arguments, &here);
1104                    self.directives(&field.node.directives, &here);
1105                    if let Some(meta) = meta {
1106                        let inner = MetaTypeName::concrete_typename(&meta.ty).to_string();
1107                        self.selection_set(&field.node.selection_set.node, &inner, &here);
1108                    }
1109                }
1110                Selection::InlineFragment(fragment) => {
1111                    let inner = fragment
1112                        .node
1113                        .type_condition
1114                        .as_ref()
1115                        .map(|condition| condition.node.on.node.to_string())
1116                        .unwrap_or_else(|| type_name.to_string());
1117                    self.directives(&fragment.node.directives, path);
1118                    self.selection_set(&fragment.node.selection_set.node, &inner, path);
1119                }
1120                Selection::FragmentSpread(spread) => {
1121                    let name = spread.node.fragment_name.node.to_string();
1122                    self.directives(&spread.node.directives, path);
1123                    // A fragment that spreads its way back to itself. Nothing
1124                    // can be checked inside it -- there is no inside, the
1125                    // document does not describe a finite selection -- so this
1126                    // is reported and the walk stops here. async-graphql finds
1127                    // it too, as a recursion depth, which says that something
1128                    // went too deep rather than what it was.
1129                    if let Some(began) = self.spreading.iter().position(|held| held == &name) {
1130                        let cycle: Vec<&str> =
1131                            self.spreading[began..].iter().map(String::as_str).collect();
1132                        self.errors.push(coded(
1133                            format!("the fragment definition(s) {} form a cycle", listed(&cycle)),
1134                            spread.pos,
1135                            // The selection set the offending spread is in,
1136                            // rather than the spread itself: by then the
1137                            // spread has no selection set of its own to name.
1138                            &format!("{}.selectionSet", path),
1139                        ));
1140                        continue;
1141                    }
1142                    self.expansions += 1;
1143                    if self.expansions > SPREAD_BUDGET {
1144                        continue;
1145                    }
1146                    if let Some(fragment) = self.fragments.get(&async_graphql::Name::new(&name)) {
1147                        let on = fragment.node.type_condition.node.on.node.to_string();
1148                        // A spread is a step in Hasura's path, named after the
1149                        // fragment: `...selectionSet.author.selectionSet.
1150                        // authorFragment.selectionSet.articles`.
1151                        let here = format!("{}.selectionSet.{}", path, name);
1152                        self.spreading.push(name);
1153                        self.selection_set(&fragment.node.selection_set.node, &on, &here);
1154                        self.spreading.pop();
1155                    }
1156                }
1157            }
1158        }
1159    }
1160
1161    fn directives(
1162        &mut self,
1163        directives: &[async_graphql::Positioned<async_graphql::parser::types::Directive>],
1164        path: &str,
1165    ) {
1166        for directive in directives {
1167            let meta = self
1168                .registry
1169                .directives
1170                .get(directive.node.name.node.as_str());
1171            for (name, value) in &directive.node.arguments {
1172                let Some(argument) = meta.and_then(|meta| meta.args.get(name.node.as_str())) else {
1173                    continue;
1174                };
1175                let expected = relax(&argument.ty, argument.default_value.is_some());
1176                let at = format!("{}.args.{}", path, name.node);
1177                self.value(&value.node, &expected, value.pos, &at);
1178            }
1179        }
1180    }
1181
1182    /// Whether a written value is a null, however it was written.
1183    ///
1184    /// A literal one, or a variable the request gave a null for. A variable
1185    /// the request left out is not: an absent variable makes the field itself
1186    /// absent, which is a query with no such comparison rather than one
1187    /// comparing against nothing.
1188    fn stands_for_null(&self, value: &async_graphql_value::Value) -> bool {
1189        use async_graphql_value::Value;
1190        match value {
1191            Value::Null => true,
1192            Value::Variable(name) => matches!(
1193                self.variables.get(&async_graphql::Name::new(name)),
1194                Some(async_graphql::Value::Null)
1195            ),
1196            _ => false,
1197        }
1198    }
1199
1200    /// One written value, against the type of the place it was written in.
1201    fn value(
1202        &mut self,
1203        value: &async_graphql_value::Value,
1204        expected: &str,
1205        pos: async_graphql::Pos,
1206        path: &str,
1207    ) {
1208        use async_graphql::registry::{MetaType, MetaTypeName};
1209        use async_graphql_value::Value;
1210
1211        match value {
1212            Value::Variable(name) => {
1213                let Some(declared) = self.declared.get(name.as_str()) else {
1214                    // Undefined, which async-graphql reports itself.
1215                    return;
1216                };
1217                if !MetaTypeName::create(expected).is_subtype(&MetaTypeName::create(declared)) {
1218                    self.errors.push(coded(
1219                        format!(
1220                            "variable '{}' is declared as '{}', but used where '{}' is expected",
1221                            name, declared, expected
1222                        ),
1223                        pos,
1224                        path,
1225                    ));
1226                    return;
1227                }
1228                // A variable of the right type may still carry a value the
1229                // type does not have: `$color: colors_enum` given
1230                // `"not_a_real_color"` is declared correctly and is still not
1231                // one of the colours.
1232                if let Some(given) = self.variables.get(&async_graphql::Name::new(name)) {
1233                    let given: async_graphql_value::Value = given.clone().into();
1234                    self.enumerated(&given, expected, pos, path, true);
1235                    self.label_path(&given, expected, pos, path);
1236                    self.raster_hex(&given, expected, pos, path);
1237                }
1238            }
1239            Value::Enum(_) | Value::String(_) => {
1240                self.enumerated(value, expected, pos, path, false);
1241                self.label_path(value, expected, pos, path);
1242                self.raster_hex(value, expected, pos, path);
1243            }
1244            Value::Number(_) => self.integral(value, expected, pos, path),
1245            Value::List(items) => {
1246                // A list may be written where one value is expected, in which
1247                // case each item is checked against that same type -- which is
1248                // what list input coercion means.
1249                let inner = match MetaTypeName::create(expected).unwrap_non_null() {
1250                    MetaTypeName::List(inner) => inner.to_string(),
1251                    _ => expected.to_string(),
1252                };
1253                for (index, item) in items.iter().enumerate() {
1254                    self.value(item, &inner, pos, &format!("{}[{}]", path, index));
1255                }
1256            }
1257            Value::Object(fields) => {
1258                let name = MetaTypeName::concrete_typename(expected);
1259                let Some(MetaType::InputObject { input_fields, .. }) =
1260                    self.registry.types.get(name)
1261                else {
1262                    return;
1263                };
1264                // A comparison against null. `where: {id: {_eq: null}}` reads
1265                // as `id = NULL`, which is never true -- so a client that
1266                // wrote it meant something the query cannot mean, and gets
1267                // every row or no rows depending on which. Hasura refuses it,
1268                // and the operand is a nullable `Int` in the schema either
1269                // server publishes, so this is the only place it can be
1270                // refused.
1271                let comparison = name.ends_with("_comparison_exp");
1272                // One object written where a list was expected is that list's
1273                // first item -- input coercion says so, and the path says so
1274                // too: `insert_author(objects: {location: $x})` is refused at
1275                // `args.objects[0].location`, not at `args.objects.location`.
1276                let base = match MetaTypeName::create(expected).unwrap_non_null() {
1277                    MetaTypeName::List(_) => format!("{}[0]", path),
1278                    _ => path.to_string(),
1279                };
1280                for (key, item) in fields {
1281                    let at = format!("{}.{}", base, key.as_str());
1282                    let Some(field) = input_fields.get(key.as_str()) else {
1283                        // A key the input object does not have, in the same
1284                        // words a missing selection field gets: Hasura writes
1285                        // `field 'action' not found in type:
1286                        // 'article_on_conflict'` for both.
1287                        self.errors.push(coded(
1288                            format!("field '{}' not found in type: '{}'", key.as_str(), name),
1289                            pos,
1290                            &at,
1291                        ));
1292                        continue;
1293                    };
1294                    if comparison && self.stands_for_null(item) {
1295                        self.errors.push(coded(
1296                            format!(
1297                                "unexpected null value for type '{}'",
1298                                MetaTypeName::concrete_typename(&field.ty)
1299                            ),
1300                            pos,
1301                            &at,
1302                        ));
1303                        continue;
1304                    }
1305                    let ty = relax(&field.ty, field.default_value.is_some());
1306                    self.value(item, &ty, pos, &at);
1307                }
1308            }
1309            _ => {}
1310        }
1311    }
1312}
1313
1314impl Usage<'_> {
1315    /// `null` written where an object is required.
1316    ///
1317    /// `update_author(where: null)` says nothing about which rows, and `where`
1318    /// is non-null in the schema either server publishes. async-graphql calls
1319    /// it `Invalid value for argument "where", expected type
1320    /// "author_bool_exp"`, which names the argument and not what was wrong
1321    /// with it; Hasura names the type it wanted and the thing it got.
1322    ///
1323    /// Only a *required* input object is refused here. A nullable one written
1324    /// as `null` is the same as leaving it out, which is a request this server
1325    /// has no business turning into an error.
1326    fn object_expected(
1327        &mut self,
1328        value: &async_graphql_value::Value,
1329        expected: &str,
1330        pos: async_graphql::Pos,
1331        path: &str,
1332    ) {
1333        use async_graphql::registry::{MetaType, MetaTypeName};
1334
1335        if !matches!(MetaTypeName::create(expected), MetaTypeName::NonNull(_)) {
1336            return;
1337        }
1338        if !self.stands_for_null(value) {
1339            return;
1340        }
1341        let name = MetaTypeName::concrete_typename(expected);
1342        if !matches!(
1343            self.registry.types.get(name),
1344            Some(MetaType::InputObject { .. })
1345        ) {
1346            return;
1347        }
1348        self.errors.push(coded(
1349            format!("expected an object for type '{}', but found null", name),
1350            pos,
1351            path,
1352        ));
1353    }
1354
1355    /// A column named by two of an update's operators.
1356    ///
1357    /// `_set: {id: 1}` beside `_inc: {id: 2}` asks for one column to be two
1358    /// things in one statement, and the order they would be applied in is not
1359    /// something the request says. PostgreSQL refuses the statement -- a
1360    /// column may appear once in a `SET` list -- and by then the answer names
1361    /// the column alone. Hasura names every column written twice, and reports
1362    /// it against the arguments rather than against any one of them, since no
1363    /// single operator is the wrong one.
1364    ///
1365    /// The operators are Hasura's own seven. A duplicate *within* one of them
1366    /// is not this: an object cannot have a key twice.
1367    fn one_operator_per_column(
1368        &mut self,
1369        arguments: &[(
1370            async_graphql::Positioned<async_graphql::Name>,
1371            async_graphql::Positioned<async_graphql_value::Value>,
1372        )],
1373        path: &str,
1374    ) {
1375        use async_graphql_value::Value;
1376
1377        const OPERATORS: [&str; 7] = [
1378            "_set",
1379            "_inc",
1380            "_append",
1381            "_prepend",
1382            "_delete_key",
1383            "_delete_elem",
1384            "_delete_at_path",
1385        ];
1386
1387        let mut seen: Vec<&str> = Vec::new();
1388        let mut twice: Vec<&str> = Vec::new();
1389        for (name, value) in arguments {
1390            if !OPERATORS.contains(&name.node.as_str()) {
1391                continue;
1392            }
1393            let Value::Object(columns) = &value.node else {
1394                continue;
1395            };
1396            for column in columns.keys() {
1397                let column = column.as_str();
1398                match seen.contains(&column) {
1399                    true if !twice.contains(&column) => twice.push(column),
1400                    true => {}
1401                    false => seen.push(column),
1402                }
1403            }
1404        }
1405        if twice.is_empty() {
1406            return;
1407        }
1408        // Haskell's `show` for a list of strings, which is what the message is
1409        // rendered by: single quotes are what Hasura's own output has.
1410        let listed = twice
1411            .iter()
1412            .map(|column| format!("'{}'", column))
1413            .collect::<Vec<String>>()
1414            .join(", ");
1415        self.errors.push(coded(
1416            format!("Column found in multiple operators: [{}].", listed),
1417            arguments
1418                .first()
1419                .map(|(name, _)| name.pos)
1420                .unwrap_or_default(),
1421            &format!("{}.args", path),
1422        ));
1423    }
1424
1425    /// A number written where an `Int` is expected, against what an `Int` is.
1426    ///
1427    /// GraphQL's `Int` is a signed 32-bit integer, and `2147483648` is not
1428    /// one. PostgreSQL says `integer out of range` when the statement runs;
1429    /// Hasura says so while reading the request, and names the value it could
1430    /// not read. `int8` columns are a `bigint` scalar rather than an `Int`, so
1431    /// nothing that legitimately holds a large number is caught here.
1432    fn integral(
1433        &mut self,
1434        value: &async_graphql_value::Value,
1435        expected: &str,
1436        pos: async_graphql::Pos,
1437        path: &str,
1438    ) {
1439        use async_graphql::registry::MetaTypeName;
1440        use async_graphql_value::Value;
1441
1442        if MetaTypeName::concrete_typename(expected) != "Int" {
1443            return;
1444        }
1445        let Value::Number(number) = value else {
1446            return;
1447        };
1448        if number.as_i64().is_some_and(|n| i32::try_from(n).is_ok()) {
1449            return;
1450        }
1451        let Some(shown) = number.as_f64().map(shown_as_haskell_would) else {
1452            return;
1453        };
1454        self.errors.push(coded_as(
1455            "parse-failed",
1456            format!(
1457                "The value {} lies outside the bounds or is not an integer. \
1458                 Maybe it is a float, or is there integer overflow?",
1459                shown
1460            ),
1461            pos,
1462            path,
1463        ));
1464    }
1465
1466    /// How many rows a query asked for, which `Int` alone does not say.
1467    ///
1468    /// `limit` is typed `Int` in the schema either server publishes and is not
1469    /// any `Int`: a page of -1 rows is not a page, and Hasura refuses it
1470    /// before the query is built rather than letting `LIMIT must not be
1471    /// negative` come back from the database. A string is refused there too,
1472    /// even one that reads as a number.
1473    ///
1474    /// `offset` is *not* the same rule, and that asymmetry is Hasura's rather
1475    /// than an omission here: its own corpus sends `offset: "1"` and expects
1476    /// rows back, and sends `offset: -1` and expects PostgreSQL's own
1477    /// `OFFSET must not be negative`. So this is about the argument named
1478    /// `limit` and nothing else.
1479    fn rows_asked_for(
1480        &mut self,
1481        argument: &str,
1482        value: &async_graphql_value::Value,
1483        expected: &str,
1484        pos: async_graphql::Pos,
1485        path: &str,
1486    ) {
1487        use async_graphql::registry::MetaTypeName;
1488        use async_graphql_value::Value;
1489
1490        if argument != "limit" || MetaTypeName::concrete_typename(expected) != "Int" {
1491            return;
1492        }
1493        // Through a variable, what matters is the value it stands for.
1494        let value = match value {
1495            Value::Variable(name) => {
1496                match self.variables.get(&async_graphql::Name::new(name.as_str())) {
1497                    Some(given) => std::borrow::Cow::Owned(given.clone().into()),
1498                    None => return,
1499                }
1500            }
1501            other => std::borrow::Cow::Borrowed(other),
1502        };
1503
1504        let found = match value.as_ref() {
1505            Value::String(_) => "a string",
1506            Value::Number(number) => match number.as_i64() {
1507                // A number that is not an `Int` at all is left to the rule
1508                // about `Int`: that is the more specific complaint, and both
1509                // rules firing would answer one mistake twice.
1510                None => return,
1511                Some(written) if i32::try_from(written).is_err() => return,
1512                // What is left is a number of rows, and a negative one is not.
1513                Some(written) if written >= 0 => return,
1514                Some(_) => "an integer",
1515            },
1516            _ => return,
1517        };
1518        self.errors.push(coded(
1519            format!(
1520                "expected a non-negative 32-bit integer for type 'Int', but found {}",
1521                found
1522            ),
1523            pos,
1524            path,
1525        ));
1526    }
1527
1528    /// A value written where a `raster` is expected, as hexadecimal.
1529    ///
1530    /// A raster travels as the hex of its well-known binary, and PostGIS
1531    /// answers `rt_raster_from_wkb: wkb size (14) < min size (61)` for
1532    /// something that is not one -- a complaint about the bytes it managed to
1533    /// decode rather than about the text it was given. Hasura reads the text
1534    /// first and says it is not hexadecimal.
1535    ///
1536    /// Only what is certainly not hexadecimal is refused: a character outside
1537    /// `0-9A-Fa-f`, or an odd number of them, since a byte takes two. A
1538    /// well-formed hex string that is too short to be a raster still goes to
1539    /// PostGIS and comes back in PostGIS's words, which is what happens today
1540    /// and is not something the corpus has an opinion about.
1541    fn raster_hex(
1542        &mut self,
1543        value: &async_graphql_value::Value,
1544        expected: &str,
1545        pos: async_graphql::Pos,
1546        path: &str,
1547    ) {
1548        use async_graphql::registry::MetaTypeName;
1549        use async_graphql_value::Value;
1550
1551        if MetaTypeName::concrete_typename(expected) != "raster" {
1552            return;
1553        }
1554        let Value::String(written) = value else {
1555            return;
1556        };
1557        if written.len() % 2 == 0 && written.chars().all(|c| c.is_ascii_hexdigit()) {
1558            return;
1559        }
1560        self.errors.push(coded_as(
1561            "parse-failed",
1562            "invalid hexadecimal representation of raster well known binary format".to_string(),
1563            pos,
1564            path,
1565        ));
1566    }
1567
1568    /// A value written where an `ltree` is expected, as a label path.
1569    ///
1570    /// PostgreSQL refuses `Tree.Collections.` with `ltree syntax error` and
1571    /// nothing else; Hasura reads the path before sending it and says what a
1572    /// path is. Reading it here is also what puts the refusal at
1573    /// `...where.path._ancestor` rather than at the request as a whole.
1574    ///
1575    /// Only an *empty label* is refused, and the narrowness is the point.
1576    /// PostgreSQL's idea of a label character comes from the database's
1577    /// locale -- `a-b`, `a_b`, `1.2` and `Ünï` are all paths on the image the
1578    /// harness runs, and in C locale the last of those is not. Refusing a
1579    /// character this server merely doubts would turn a working query into an
1580    /// error, so what is refused here is the one thing no locale accepts: a
1581    /// label with nothing in it. Everything else is still PostgreSQL's to
1582    /// judge, in PostgreSQL's words.
1583    fn label_path(
1584        &mut self,
1585        value: &async_graphql_value::Value,
1586        expected: &str,
1587        pos: async_graphql::Pos,
1588        path: &str,
1589    ) {
1590        use async_graphql::registry::MetaTypeName;
1591        use async_graphql_value::Value;
1592
1593        if MetaTypeName::concrete_typename(expected) != "ltree" {
1594            return;
1595        }
1596        let Value::String(written) = value else {
1597            return;
1598        };
1599        // The empty string is the empty path, which is a path: "zero or more
1600        // labels" is what the message this raises says, and PostgreSQL agrees.
1601        if written.is_empty() || written.split('.').all(|label| !label.is_empty()) {
1602            return;
1603        }
1604        self.errors.push(coded_as(
1605            "parse-failed",
1606            "Expecting label path: a sequence of zero or more labels separated by \
1607             dots, for example L1.L2.L3"
1608                .to_string(),
1609            pos,
1610            path,
1611        ));
1612    }
1613
1614    /// A value written where an enum is expected, against that enum's members.
1615    ///
1616    /// async-graphql reports this too, in its own words and against the
1617    /// argument rather than against the place inside it. Hasura lists what it
1618    /// would have accepted, which is the half a client can act on.
1619    fn enumerated(
1620        &mut self,
1621        value: &async_graphql_value::Value,
1622        expected: &str,
1623        pos: async_graphql::Pos,
1624        path: &str,
1625        // Whether the value arrived through a variable. JSON has no enum, so a
1626        // variable carries one as a string and the question is which member it
1627        // names. Written into the document, a string is not an enum value at
1628        // all -- `_eq: "red"` is a mistake about the language rather than
1629        // about the colours, and Hasura says which mistake it was.
1630        from_variable: bool,
1631    ) {
1632        use async_graphql::registry::{MetaType, MetaTypeName};
1633        use async_graphql_value::Value;
1634
1635        let name = MetaTypeName::concrete_typename(expected);
1636        let Some(MetaType::Enum { enum_values, .. }) = self.registry.types.get(name) else {
1637            return;
1638        };
1639        let written = match value {
1640            Value::Enum(written) => written.as_str(),
1641            Value::String(written) if from_variable => written.as_str(),
1642            Value::String(_) => {
1643                self.errors.push(coded(
1644                    format!(
1645                        "expected an enum value for type '{}', but found a string",
1646                        name
1647                    ),
1648                    pos,
1649                    path,
1650                ));
1651                return;
1652            }
1653            _ => return,
1654        };
1655        if enum_values.contains_key(written) {
1656            return;
1657        }
1658        // Alphabetical, which is the order Hasura lists them in and not
1659        // necessarily the order they were declared.
1660        let mut allowed: Vec<&str> = enum_values.keys().map(String::as_str).collect();
1661        allowed.sort_unstable();
1662        let listed = allowed
1663            .iter()
1664            .map(|value| format!("'{}'", value))
1665            .collect::<Vec<_>>()
1666            .join(", ");
1667        self.errors.push(coded(
1668            format!(
1669                "expected one of the values [{}] for type '{}', but found '{}'",
1670                listed, name, written
1671            ),
1672            pos,
1673            path,
1674        ));
1675    }
1676}
1677
1678/// How many fragment spreads one document may expand before the walk gives up.
1679///
1680/// Not a limit anyone should reach: it is here because fragments that spread
1681/// other fragments twice each double the work per level without any name
1682/// repeating, so the cycle check alone does not bound the walk. Reaching it
1683/// stops the checking rather than refusing the document -- async-graphql
1684/// validates it afterwards either way.
1685const SPREAD_BUDGET: usize = 1000;
1686
1687/// Names in a sentence, the way Hasura writes them: `a`, `a and b`,
1688/// `a, b and c`.
1689fn listed(names: &[&str]) -> String {
1690    match names {
1691        [] => String::new(),
1692        [one] => one.to_string(),
1693        [rest @ .., last] => format!("{} and {}", rest.join(", "), last),
1694    }
1695}
1696
1697/// A number as Haskell's `show` renders a `Double`, which is how the value in
1698/// Hasura's message is spelled.
1699///
1700/// `2147483648` comes back as `2.147483648e9` rather than as its digits, and
1701/// the message quotes it, so matching the message means matching the
1702/// rendering. Haskell writes a `Double` in fixed notation while its magnitude
1703/// is in `[0.1, 10^7)` and in scientific notation otherwise, always with a
1704/// decimal point in the mantissa. Rust's own two formats are the same two
1705/// spellings and both give the shortest digits that read back exactly, so the
1706/// choice between them is the whole of what this adds.
1707fn shown_as_haskell_would(value: f64) -> String {
1708    if !value.is_finite() {
1709        return value.to_string();
1710    }
1711    let magnitude = value.abs();
1712    let with_a_point = |rendered: String| match rendered.contains('.') {
1713        true => rendered,
1714        false => format!("{}.0", rendered),
1715    };
1716    if magnitude != 0.0 && (magnitude < 0.1 || magnitude >= 1e7) {
1717        let rendered = format!("{:e}", value);
1718        let (mantissa, exponent) = match rendered.split_once('e') {
1719            Some(halves) => halves,
1720            None => return with_a_point(rendered),
1721        };
1722        return format!("{}e{}", with_a_point(mantissa.to_string()), exponent);
1723    }
1724    with_a_point(format!("{}", value))
1725}
1726
1727/// A location type as it accepts a variable, given whether it has a default.
1728///
1729/// A non-null location with a default takes a nullable variable: leaving that
1730/// variable out is the same as not writing the argument, and the default then
1731/// stands in. Without a default it does not.
1732fn relax(ty: &str, has_default: bool) -> String {
1733    match has_default {
1734        true => ty.strip_suffix('!').unwrap_or(ty).to_string(),
1735        false => ty.to_string(),
1736    }
1737}
1738
1739/// A validation failure, coded as one, at the place it was found.
1740///
1741/// The path is carried on the error rather than derived from it: this walk
1742/// happens before execution, so there is no response path to read, and what
1743/// has to be named is a place inside the request -- an argument, a key of an
1744/// input object, an item of a list.
1745fn coded(message: String, pos: async_graphql::Pos, path: &str) -> ServerError {
1746    coded_as("validation-failed", message, pos, path)
1747}
1748
1749/// The same, for a rule whose failure Hasura codes as something else.
1750fn coded_as(
1751    code: &'static str,
1752    message: String,
1753    pos: async_graphql::Pos,
1754    path: &str,
1755) -> ServerError {
1756    let mut error = ServerError::new(message, Some(pos));
1757    let mut extensions = async_graphql::ErrorExtensionValues::default();
1758    extensions.set("code", code);
1759    extensions.set("path", path);
1760    error.extensions = Some(extensions);
1761    error
1762}
1763
1764#[cfg(test)]
1765mod variable_position_tests {
1766    use super::*;
1767    use async_graphql::dynamic::*;
1768
1769    /// A schema with the shapes the rule has to reason about: a scalar
1770    /// argument, a nested input object, a list, a non-null location, and a
1771    /// non-null location that has a default.
1772    fn schema() -> async_graphql::dynamic::Schema {
1773        let filter = InputObject::new("author_bool_exp")
1774            .field(InputValue::new("id", TypeRef::named("Int")))
1775            .field(InputValue::new(
1776                "name",
1777                TypeRef::named("String_comparison_exp"),
1778            ));
1779        let comparison = InputObject::new("String_comparison_exp")
1780            .field(InputValue::new("_eq", TypeRef::named("String")))
1781            .field(InputValue::new("_in", TypeRef::named_nn_list("String")));
1782        // A tree, for the rules that are about a scalar's own shape rather
1783        // than about the type it is written under.
1784        let ltree = InputObject::new("ltree_comparison_exp")
1785            .field(InputValue::new("_ancestor", TypeRef::named("ltree")))
1786            .field(InputValue::new(
1787                "_ancestor_any",
1788                TypeRef::named_list("ltree"),
1789            ));
1790        let kind = Enum::new("kind_enum").item("leaf").item("branch");
1791        let kind_comparison = InputObject::new("kind_enum_comparison_exp")
1792            .field(InputValue::new("_eq", TypeRef::named("kind_enum")));
1793        let raster_comparison = InputObject::new("raster_comparison_exp").field(InputValue::new(
1794            "_st_intersects_rast",
1795            TypeRef::named("raster"),
1796        ));
1797        let tree_filter = InputObject::new("tree_bool_exp")
1798            .field(InputValue::new(
1799                "rast",
1800                TypeRef::named("raster_comparison_exp"),
1801            ))
1802            .field(InputValue::new(
1803                "path",
1804                TypeRef::named("ltree_comparison_exp"),
1805            ))
1806            .field(InputValue::new(
1807                "kind",
1808                TypeRef::named("kind_enum_comparison_exp"),
1809            ));
1810        let insert = InputObject::new("author_insert_input")
1811            .field(InputValue::new("name", TypeRef::named("String")))
1812            .field(InputValue::new("count", TypeRef::named("Int")));
1813        let query = Object::new("query_root")
1814            .field(
1815                Field::new("author", TypeRef::named_nn_list_nn("author"), |_| {
1816                    FieldFuture::new(async move { Ok(Some(async_graphql::Value::List(vec![]))) })
1817                })
1818                .argument(InputValue::new("limit", TypeRef::named("Int")))
1819                .argument(InputValue::new("where", TypeRef::named("author_bool_exp"))),
1820            )
1821            .field(
1822                Field::new("tree", TypeRef::named_nn_list_nn("tree"), |_| {
1823                    FieldFuture::new(async move { Ok(Some(async_graphql::Value::List(vec![]))) })
1824                })
1825                .argument(InputValue::new("where", TypeRef::named("tree_bool_exp"))),
1826            );
1827        let mutation = Object::new("mutation_root")
1828            .field(
1829                Field::new("insert_author_one", TypeRef::named("author"), |_| {
1830                    FieldFuture::new(async move { Ok(None::<async_graphql::Value>) })
1831                })
1832                .argument(InputValue::new(
1833                    "object",
1834                    TypeRef::named_nn("author_insert_input"),
1835                )),
1836            )
1837            .field(
1838                Field::new("insert_author", TypeRef::named("author"), |_| {
1839                    FieldFuture::new(async move { Ok(None::<async_graphql::Value>) })
1840                })
1841                .argument(InputValue::new(
1842                    "objects",
1843                    TypeRef::named_nn_list_nn("author_insert_input"),
1844                ))
1845                .argument(
1846                    InputValue::new("update_columns", TypeRef::named_nn_list_nn("String"))
1847                        .default_value(async_graphql::Value::List(Vec::new())),
1848                ),
1849            )
1850            .field(
1851                Field::new(
1852                    "update_author",
1853                    TypeRef::named("author_mutation_response"),
1854                    |_| FieldFuture::new(async move { Ok(None::<async_graphql::Value>) }),
1855                )
1856                .argument(InputValue::new(
1857                    "where",
1858                    TypeRef::named_nn("author_bool_exp"),
1859                ))
1860                .argument(InputValue::new("_set", TypeRef::named("author_set_input")))
1861                .argument(InputValue::new("_inc", TypeRef::named("author_inc_input"))),
1862            );
1863        let author = Object::new("author").field(Field::new("id", TypeRef::named("Int"), |_| {
1864            FieldFuture::new(async move { Ok(None::<async_graphql::Value>) })
1865        }));
1866        let tree = Object::new("tree")
1867            .field(Field::new("id", TypeRef::named("Int"), |_| {
1868                FieldFuture::new(async move { Ok(None::<async_graphql::Value>) })
1869            }))
1870            .field(Field::new("path", TypeRef::named("ltree"), |_| {
1871                FieldFuture::new(async move { Ok(None::<async_graphql::Value>) })
1872            }));
1873        // What an update writes, and the one required `where` in the fixture.
1874        let set = InputObject::new("author_set_input")
1875            .field(InputValue::new("name", TypeRef::named("String")))
1876            .field(InputValue::new("count", TypeRef::named("Int")));
1877        let inc = InputObject::new("author_inc_input")
1878            .field(InputValue::new("count", TypeRef::named("Int")));
1879        let response = Object::new("author_mutation_response").field(Field::new(
1880            "affected_rows",
1881            TypeRef::named_nn("Int"),
1882            |_| FieldFuture::new(async move { Ok(None::<async_graphql::Value>) }),
1883        ));
1884        Schema::build("query_root", Some("mutation_root"), None)
1885            .register(Scalar::new("ltree"))
1886            .register(Scalar::new("raster"))
1887            .register(raster_comparison)
1888            .register(kind)
1889            .register(kind_comparison)
1890            .register(filter)
1891            .register(comparison)
1892            .register(ltree)
1893            .register(tree_filter)
1894            .register(insert)
1895            .register(set)
1896            .register(inc)
1897            .register(response)
1898            .register(author)
1899            .register(tree)
1900            .register(query)
1901            .register(mutation)
1902            .finish()
1903            .expect("the test schema builds")
1904    }
1905
1906    fn refusals(query: &str, variables: &str) -> Vec<String> {
1907        let request =
1908            async_graphql::Request::new(query).variables(async_graphql::Variables::from_json(
1909                serde_json::from_str(variables).expect("the variables are JSON"),
1910            ));
1911        match prepare(Some(&schema()), request, None) {
1912            Ok(_) => Vec::new(),
1913            Err((_, errors)) => errors.into_iter().map(|e| e.message).collect(),
1914        }
1915    }
1916
1917    /// The same, answering with where each refusal says it happened.
1918    fn refusals_with_paths(query: &str, variables: &str) -> Vec<String> {
1919        let request =
1920            async_graphql::Request::new(query).variables(async_graphql::Variables::from_json(
1921                serde_json::from_str(variables).expect("the variables are JSON"),
1922            ));
1923        match prepare(Some(&schema()), request, None) {
1924            Ok(_) => Vec::new(),
1925            Err((_, errors)) => errors.iter().map(path_for).collect(),
1926        }
1927    }
1928
1929    /// `where: {name: {_eq: null}}` reads as `name = NULL`, which is never
1930    /// true. Hasura refuses it, and so does this.
1931    #[test]
1932    fn a_null_written_into_a_comparison_is_refused() {
1933        assert_eq!(
1934            refusals("{ author(where: {name: {_eq: null}}) { id } }", "{}"),
1935            vec!["unexpected null value for type 'String'"]
1936        );
1937    }
1938
1939    #[test]
1940    fn a_variable_standing_for_a_null_is_refused_there_too() {
1941        assert_eq!(
1942            refusals(
1943                "query ($n: String) { author(where: {name: {_eq: $n}}) { id } }",
1944                r#"{"n": null}"#
1945            ),
1946            vec!["unexpected null value for type 'String'"]
1947        );
1948    }
1949
1950    /// An absent variable makes the comparison itself absent, which is a
1951    /// query with no such filter rather than one filtering on nothing.
1952    /// Where a refusal says it happened, which is a place in the request and
1953    /// not in the answer.
1954    #[test]
1955    fn a_refusal_names_the_argument_it_was_found_in() {
1956        let paths =
1957            |query: &str, variables: &str| -> Vec<String> { refusals_with_paths(query, variables) };
1958        // A key of an input object, under the argument that carried it.
1959        assert_eq!(
1960            paths(
1961                "mutation ($n: Int!) { insert_author_one(object: {name: $n}) { id } }",
1962                r#"{"n": 1}"#
1963            ),
1964            vec!["$.selectionSet.insert_author_one.args.object.name"]
1965        );
1966        // A comparison against a null names the operator it was written in.
1967        assert_eq!(
1968            paths("{ author(where: {name: {_eq: null}}) { id } }", "{}"),
1969            vec!["$.selectionSet.author.args.where.name._eq"]
1970        );
1971        // One object written where a list was expected is that list's first
1972        // item, and the path says so.
1973        assert_eq!(
1974            paths(
1975                "mutation ($n: Int!) { insert_author(objects: {name: $n}) { id } }",
1976                r#"{"n": 1}"#
1977            ),
1978            vec!["$.selectionSet.insert_author.args.objects[0].name"]
1979        );
1980        // And an actual list is indexed by where the value really is.
1981        assert_eq!(
1982            paths(
1983                "mutation ($n: Int!) { insert_author(objects: [{name: \"a\"}, \
1984                 {name: $n}]) { id } }",
1985                r#"{"n": 1}"#
1986            ),
1987            vec!["$.selectionSet.insert_author.args.objects[1].name"]
1988        );
1989    }
1990
1991    /// `where: null` says nothing about which rows, and an update's `where`
1992    /// is required. Hasura names the type it wanted and what it got instead.
1993    #[test]
1994    fn a_null_written_where_an_object_is_required_is_refused_as_one() {
1995        assert_eq!(
1996            refusals(
1997                "mutation { update_author(where: null, _set: {name: \"a\"}) \
1998                 { affected_rows } }",
1999                "{}"
2000            ),
2001            vec!["expected an object for type 'author_bool_exp', but found null"]
2002        );
2003        assert_eq!(
2004            refusals_with_paths(
2005                "mutation { update_author(where: null, _set: {name: \"a\"}) \
2006                 { affected_rows } }",
2007                "{}"
2008            ),
2009            vec!["$.selectionSet.update_author.args.where"]
2010        );
2011        // A *variable* carrying the null is a different complaint, and
2012        // already had Hasura's own words for it: the fault is in the
2013        // variable's declaration, which is where it is reported.
2014        assert_eq!(
2015            refusals(
2016                "mutation ($w: author_bool_exp!) { update_author(where: $w, \
2017                 _set: {name: \"a\"}) { affected_rows } }",
2018                r#"{"w": null}"#
2019            ),
2020            vec!["null value found for non-nullable type: \"author_bool_exp!\""]
2021        );
2022    }
2023
2024    /// A nullable argument written as `null` is the same as leaving it out,
2025    /// which is not something to refuse.
2026    #[test]
2027    fn a_null_written_where_an_object_is_optional_is_left_alone() {
2028        assert_eq!(
2029            refusals("{ author(where: null) { id } }", "{}"),
2030            Vec::<String>::new()
2031        );
2032    }
2033
2034    /// One column named by two of an update's operators asks for it to be two
2035    /// things at once, and the order is not something the request says.
2036    #[test]
2037    fn a_column_written_by_two_operators_is_refused_before_the_statement() {
2038        assert_eq!(
2039            refusals(
2040                "mutation { update_author(_set: {name: \"a\", count: 1}, \
2041                 _inc: {count: 2}, where: {}) { affected_rows } }",
2042                "{}"
2043            ),
2044            vec!["Column found in multiple operators: ['count']."]
2045        );
2046        assert_eq!(
2047            refusals_with_paths(
2048                "mutation { update_author(_set: {count: 1}, _inc: {count: 2}, \
2049                 where: {}) { affected_rows } }",
2050                "{}"
2051            ),
2052            vec!["$.selectionSet.update_author.args"]
2053        );
2054        // Two operators naming different columns is an ordinary update.
2055        assert_eq!(
2056            refusals(
2057                "mutation { update_author(_set: {name: \"a\"}, _inc: {count: 2}, \
2058                 where: {}) { affected_rows } }",
2059                "{}"
2060            ),
2061            Vec::<String>::new()
2062        );
2063    }
2064
2065    /// Two things wrong with one request is one refusal. Hasura stops at the
2066    /// first, and a client written against it reads `errors[0]`.
2067    #[test]
2068    fn a_request_is_refused_once_however_many_faults_it_has() {
2069        assert_eq!(
2070            refusals("{ author { nope } tree { alsoNope } }", "{}"),
2071            vec!["field 'nope' not found in type: 'author'"]
2072        );
2073        // The second fault is a different kind from the first, and is still
2074        // not answered: what is reported is where the walk stopped, not one
2075        // error of each sort.
2076        assert_eq!(
2077            refusals(
2078                "mutation { insert_author(objects: [{nope: 1}],                  unknownArg: 2) { id } }",
2079                "{}"
2080            ),
2081            vec!["field 'nope' not found in type: 'author_insert_input'"]
2082        );
2083    }
2084
2085    /// A raster travels as the hex of its well-known binary. What is
2086    /// certainly not hex is refused here; the rest is still PostGIS's.
2087    #[test]
2088    fn a_raster_that_is_not_hexadecimal_is_refused_as_that() {
2089        let invalid = "invalid hexadecimal representation of raster well known binary format";
2090        assert_eq!(
2091            refusals(
2092                "query ($r: raster) { tree(where: {rast: {_st_intersects_rast: $r}}) { path } }",
2093                r#"{"r": "this is invalid raster value"}"#
2094            ),
2095            vec![invalid]
2096        );
2097        assert_eq!(
2098            refusals_with_paths(
2099                "{ tree(where: {rast: {_st_intersects_rast: \"zz\"}}) { path } }",
2100                "{}"
2101            ),
2102            vec!["$.selectionSet.tree.args.where.rast._st_intersects_rast"]
2103        );
2104        // A byte takes two characters.
2105        assert_eq!(
2106            refusals(
2107                "{ tree(where: {rast: {_st_intersects_rast: \"abc\"}}) { path } }",
2108                "{}"
2109            ),
2110            vec![invalid]
2111        );
2112        // Hex that is too short to be a raster is PostGIS's to refuse, in
2113        // PostGIS's words.
2114        assert!(refusals(
2115            "{ tree(where: {rast: {_st_intersects_rast: \"0100\"}}) { path } }",
2116            "{}"
2117        )
2118        .is_empty());
2119    }
2120
2121    /// A fragment that spreads its way back to itself describes no finite
2122    /// selection. async-graphql notices that something went too deep; Hasura
2123    /// says which fragments went round.
2124    #[test]
2125    fn fragments_that_spread_each_other_are_named_as_a_cycle() {
2126        let cycle = "query { author { ...a } } \
2127                     fragment a on author { id ...b } \
2128                     fragment b on author { id ...a }";
2129        assert_eq!(
2130            refusals(cycle, "{}"),
2131            vec!["the fragment definition(s) a and b form a cycle"]
2132        );
2133        // The selection set the offending spread is in, with each spread a
2134        // step of its own.
2135        assert_eq!(
2136            refusals_with_paths(cycle, "{}"),
2137            vec!["$.selectionSet.author.selectionSet.a.selectionSet.b.selectionSet"]
2138        );
2139        // A fragment that spreads itself is a cycle of one.
2140        assert_eq!(
2141            refusals(
2142                "query { author { ...a } } fragment a on author { id ...a }",
2143                "{}"
2144            ),
2145            vec!["the fragment definition(s) a form a cycle"]
2146        );
2147        // The same fragment twice in sibling positions is not a cycle.
2148        assert!(refusals(
2149            "query { author { ...a } articles: author { ...a } } \
2150             fragment a on author { id }",
2151            "{}"
2152        )
2153        .is_empty());
2154    }
2155
2156    /// GraphQL's `Int` is a signed 32-bit integer, and a number that is not
2157    /// one is refused while the request is read rather than by the database.
2158    #[test]
2159    fn a_number_that_is_not_an_int_is_refused_as_one() {
2160        let bounds = |value: &str| {
2161            format!(
2162                "The value {} lies outside the bounds or is not an integer. \
2163                 Maybe it is a float, or is there integer overflow?",
2164                value
2165            )
2166        };
2167        assert_eq!(
2168            refusals(
2169                "mutation { insert_author_one(object: {count: 2147483648}) { id } }",
2170                "{}"
2171            ),
2172            vec![bounds("2.147483648e9")]
2173        );
2174        assert_eq!(
2175            refusals_with_paths(
2176                "mutation { insert_author(objects: [{count: 2147483648}]) { id } }",
2177                "{}"
2178            ),
2179            vec!["$.selectionSet.insert_author.args.objects[0].count"]
2180        );
2181        // The edges of the type are inside it.
2182        for good in ["2147483647", "-2147483648", "0"] {
2183            assert!(
2184                refusals(
2185                    &format!(
2186                        "mutation {{ insert_author_one(object: {{count: {}}}) {{ id }} }}",
2187                        good
2188                    ),
2189                    "{}"
2190                )
2191                .is_empty(),
2192                "{} was refused",
2193                good
2194            );
2195        }
2196    }
2197
2198    /// Haskell writes a `Double` in fixed notation while it is neither very
2199    /// large nor very small, and Hasura quotes what Haskell wrote.
2200    #[test]
2201    fn a_number_is_shown_the_way_the_message_spells_it() {
2202        assert_eq!(shown_as_haskell_would(2147483648.0), "2.147483648e9");
2203        assert_eq!(shown_as_haskell_would(1.5), "1.5");
2204        assert_eq!(shown_as_haskell_would(1.0), "1.0");
2205        assert_eq!(shown_as_haskell_would(0.0), "0.0");
2206        assert_eq!(shown_as_haskell_would(-2147483649.0), "-2.147483649e9");
2207        assert_eq!(shown_as_haskell_would(1e-3), "1.0e-3");
2208        // The boundaries of the fixed range, which are Haskell's and not
2209        // Rust's.
2210        assert_eq!(shown_as_haskell_would(9999999.0), "9999999.0");
2211        assert_eq!(shown_as_haskell_would(10000000.0), "1.0e7");
2212        assert_eq!(shown_as_haskell_would(0.1), "0.1");
2213    }
2214
2215    /// `limit` is typed `Int` and is not any `Int`, and `offset` is -- which
2216    /// is Hasura's asymmetry, not an omission.
2217    #[test]
2218    fn a_limit_is_a_number_of_rows_and_not_any_integer() {
2219        let refused = |q: &str| refusals(q, "{}");
2220        assert_eq!(
2221            refused("{ author(limit: -1) { id } }"),
2222            vec!["expected a non-negative 32-bit integer for type 'Int', but found an integer"]
2223        );
2224        assert_eq!(
2225            refused("{ author(limit: \"3\") { id } }"),
2226            vec!["expected a non-negative 32-bit integer for type 'Int', but found a string"]
2227        );
2228        assert_eq!(
2229            refusals_with_paths("{ author(limit: -1) { id } }", "{}"),
2230            vec!["$.selectionSet.author.args.limit"]
2231        );
2232        // A variable is the value it stands for.
2233        assert_eq!(
2234            refusals(
2235                "query ($n: Int) { author(limit: $n) { id } }",
2236                r#"{"n": -1}"#
2237            ),
2238            vec!["expected a non-negative 32-bit integer for type 'Int', but found an integer"]
2239        );
2240        // Zero is a number of rows, and so is the largest `Int` there is.
2241        assert!(refused("{ author(limit: 0) { id } }").is_empty());
2242        assert!(refused("{ author(limit: 2147483647) { id } }").is_empty());
2243        assert!(refused("{ author(limit: null) { id } }").is_empty());
2244        // Beyond that it is not a number of rows because it is not an `Int`,
2245        // and it is answered as that rather than as both.
2246        assert_eq!(
2247            refused("{ author(limit: 4294967295) { id } }"),
2248            vec![
2249                "The value 4.294967295e9 lies outside the bounds or is not an \
2250                  integer. Maybe it is a float, or is there integer overflow?"
2251            ]
2252        );
2253    }
2254
2255    /// A quoted enum value is a mistake about the language; an unquoted one
2256    /// that names nothing is a mistake about the members. Hasura says which.
2257    #[test]
2258    fn a_quoted_enum_value_is_not_a_member_that_is_missing() {
2259        assert_eq!(
2260            refusals("{ tree(where: {kind: {_eq: \"leaf\"}}) { path } }", "{}"),
2261            vec!["expected an enum value for type 'kind_enum', but found a string"]
2262        );
2263        assert_eq!(
2264            refusals("{ tree(where: {kind: {_eq: twig}}) { path } }", "{}"),
2265            vec!["expected one of the values ['branch', 'leaf'] for type 'kind_enum', but found 'twig'"]
2266        );
2267        // Through a variable, a string is how JSON carries an enum, so the
2268        // question is which member it names.
2269        assert_eq!(
2270            refusals(
2271                "query ($k: kind_enum) { tree(where: {kind: {_eq: $k}}) { path } }",
2272                r#"{"k": "twig"}"#
2273            ),
2274            vec!["expected one of the values ['branch', 'leaf'] for type 'kind_enum', but found 'twig'"]
2275        );
2276        assert!(refusals(
2277            "query ($k: kind_enum) { tree(where: {kind: {_eq: $k}}) { path } }",
2278            r#"{"k": "leaf"}"#
2279        )
2280        .is_empty());
2281    }
2282
2283    /// A document that does not parse is not a query, and that is all the
2284    /// answer there is: async-graphql reports it the way a compiler does, and
2285    /// Hasura names the document.
2286    #[test]
2287    fn a_document_that_does_not_parse_is_not_a_query() {
2288        assert_eq!(
2289            refusals("{ author(where: {name: {_eq: \"unclosed}}) { id } }", "{}"),
2290            vec!["not a valid graphql query"]
2291        );
2292        assert_eq!(
2293            refusals_with_paths("query { author(", "{}"),
2294            vec!["$.query"]
2295        );
2296        // The rule is only about parsing. A document that parses and is wrong
2297        // about the schema is answered by the rules that know the schema.
2298        assert_eq!(
2299            refusals("{ nothing_here { id } }", "{}"),
2300            vec!["field 'nothing_here' not found in type: 'query_root'"]
2301        );
2302    }
2303
2304    /// An `ltree` with an empty label is refused before it is sent, in the
2305    /// words Hasura uses -- and nothing else is, because what a label may
2306    /// contain is the database's locale to decide.
2307    #[test]
2308    fn a_label_path_with_an_empty_label_is_refused() {
2309        let empty = "Expecting label path: a sequence of zero or more labels \
2310                     separated by dots, for example L1.L2.L3";
2311        assert_eq!(
2312            refusals(
2313                "{ tree(where: {path: {_ancestor: \"a.b.\"}}) { path } }",
2314                "{}"
2315            ),
2316            vec![empty]
2317        );
2318        assert_eq!(
2319            refusals(
2320                "{ tree(where: {path: {_ancestor: \".a\"}}) { path } }",
2321                "{}"
2322            ),
2323            vec![empty]
2324        );
2325        assert_eq!(
2326            refusals(
2327                "{ tree(where: {path: {_ancestor: \"a..b\"}}) { path } }",
2328                "{}"
2329            ),
2330            vec![empty]
2331        );
2332        // The list form says which item, which is what its path is for.
2333        assert_eq!(
2334            refusals_with_paths(
2335                "{ tree(where: {path: {_ancestor_any: [\"a\", \"b.\"]}}) { path } }",
2336                "{}"
2337            ),
2338            vec!["$.selectionSet.tree.args.where.path._ancestor_any[1]"]
2339        );
2340        // Everything PostgreSQL accepts is still sent to it, including the
2341        // characters a stricter reading would have refused.
2342        for good in ["a", "a.b", "", "a-b", "a_b", "1.2", "Ünï", "a@b"] {
2343            assert!(
2344                refusals(
2345                    &format!(
2346                        "{{ tree(where: {{path: {{_ancestor: \"{}\"}}}}) {{ path }} }}",
2347                        good
2348                    ),
2349                    "{}"
2350                )
2351                .is_empty(),
2352                "{} was refused",
2353                good
2354            );
2355        }
2356    }
2357
2358    #[test]
2359    fn a_variable_that_was_not_given_is_not_a_null() {
2360        assert!(refusals(
2361            "query ($n: String) { author(where: {name: {_eq: $n}}) { id } }",
2362            "{}"
2363        )
2364        .is_empty());
2365    }
2366
2367    /// Only comparisons. A column set to null is a column set to null.
2368    #[test]
2369    fn a_null_written_anywhere_else_is_left_alone() {
2370        assert!(refusals(
2371            "mutation { insert_author_one(object: {name: null}) { id } }",
2372            "{}"
2373        )
2374        .is_empty());
2375    }
2376
2377    #[test]
2378    fn a_variable_of_the_wrong_type_is_refused() {
2379        assert_eq!(
2380            refusals("query ($s: String) { author(limit: $s) { id } }", "{}"),
2381            vec!["variable 's' is declared as 'String', but used where 'Int' is expected"]
2382        );
2383    }
2384
2385    #[test]
2386    fn the_check_reaches_inside_an_input_object() {
2387        assert_eq!(
2388            refusals(
2389                "query ($s: String) { author(where: {id: $s}) { id } }",
2390                "{}"
2391            ),
2392            vec!["variable 's' is declared as 'String', but used where 'Int' is expected"]
2393        );
2394    }
2395
2396    #[test]
2397    fn the_check_reaches_inside_a_list() {
2398        assert_eq!(
2399            refusals(
2400                "mutation ($n: Int) { insert_author(objects: [{name: $n}]) { id } }",
2401                "{}"
2402            ),
2403            vec!["variable 'n' is declared as 'Int', but used where 'String' is expected"]
2404        );
2405    }
2406
2407    #[test]
2408    fn a_nullable_variable_cannot_fill_a_non_null_place() {
2409        assert_eq!(
2410            refusals(
2411                "mutation ($a: author_insert_input) { insert_author_one(object: $a) { id } }",
2412                "{}"
2413            ),
2414            vec![
2415                "variable 'a' is declared as 'author_insert_input', but used where \
2416                 'author_insert_input!' is expected"
2417            ]
2418        );
2419    }
2420
2421    #[test]
2422    fn a_default_lets_it() {
2423        // The default stands in wherever the variable is left out, so the
2424        // place can never actually see a null.
2425        assert!(refusals(
2426            "mutation ($a: author_insert_input = {name: \"x\"}) \
2427             { insert_author_one(object: $a) { id } }",
2428            "{}"
2429        )
2430        .is_empty());
2431    }
2432
2433    #[test]
2434    fn a_default_of_null_does_not() {
2435        assert_eq!(
2436            refusals(
2437                "mutation ($a: author_insert_input = null) \
2438                 { insert_author_one(object: $a) { id } }",
2439                "{}"
2440            ),
2441            vec![
2442                "variable 'a' is declared as 'author_insert_input', but used where \
2443                 'author_insert_input!' is expected"
2444            ]
2445        );
2446    }
2447
2448    #[test]
2449    fn a_default_on_the_place_lets_it_too() {
2450        // `update_columns` is non-null with a default: not writing the
2451        // argument is allowed, so a variable that might be absent is too.
2452        assert!(refusals(
2453            "mutation ($c: [String!]) { insert_author(objects: [], update_columns: $c) { id } }",
2454            "{}"
2455        )
2456        .is_empty());
2457    }
2458
2459    #[test]
2460    fn a_non_null_variable_fits_a_nullable_place() {
2461        assert!(refusals(
2462            "query ($n: Int!) { author(limit: $n) { id } }",
2463            "{\"n\": 1}"
2464        )
2465        .is_empty());
2466    }
2467
2468    #[test]
2469    fn a_non_null_variable_given_null_is_refused() {
2470        assert_eq!(
2471            refusals(
2472                "query ($n: Int! = 1) { author(limit: $n) { id } }",
2473                "{\"n\": null}"
2474            ),
2475            vec!["null value found for non-nullable type: \"Int!\""]
2476        );
2477    }
2478
2479    #[test]
2480    fn a_nullable_variable_given_null_is_not() {
2481        assert!(refusals(
2482            "query ($n: Int) { author(limit: $n) { id } }",
2483            "{\"n\": null}"
2484        )
2485        .is_empty());
2486    }
2487
2488    #[test]
2489    fn a_variable_used_through_a_fragment_is_checked() {
2490        assert_eq!(
2491            refusals(
2492                "query ($s: String) { author { ...rows } } \
2493                 fragment rows on author { id }",
2494                "{}"
2495            ),
2496            Vec::<String>::new()
2497        );
2498        assert_eq!(
2499            refusals(
2500                "query ($s: String) { ...roots } \
2501                 fragment roots on query_root { author(limit: $s) { id } }",
2502                "{}"
2503            ),
2504            vec!["variable 's' is declared as 'String', but used where 'Int' is expected"]
2505        );
2506    }
2507}
2508
2509#[cfg(test)]
2510mod coercion_tests {
2511    use super::*;
2512    use async_graphql::dynamic::*;
2513
2514    /// The three places a value is written: a field argument the engine owns,
2515    /// a field argument a column owns, and a column inside an input object.
2516    fn schema() -> async_graphql::dynamic::Schema {
2517        let insert = InputObject::new("test_types_insert_input")
2518            .field(InputValue::new("c1_smallint", TypeRef::named("Int")))
2519            .field(InputValue::new("c6_real", TypeRef::named("Float")))
2520            .field(InputValue::new("c20_boolean", TypeRef::named("Boolean")))
2521            .field(InputValue::new("c13_text", TypeRef::named("String")));
2522        let row = Object::new("test_types").field(Field::new(
2523            "c1_smallint",
2524            TypeRef::named("Int"),
2525            |_| FieldFuture::new(async move { Ok(None::<async_graphql::Value>) }),
2526        ));
2527        let query = Object::new("query_root").field(
2528            Field::new(
2529                "test_types",
2530                TypeRef::named_nn_list_nn("test_types"),
2531                |_| FieldFuture::new(async move { Ok(Some(async_graphql::Value::List(vec![]))) }),
2532            )
2533            .argument(InputValue::new("limit", TypeRef::named("Int")))
2534            .argument(InputValue::new("offset", TypeRef::named("Int"))),
2535        );
2536        let mutation = Object::new("mutation_root").field(
2537            Field::new("insert_test_types", TypeRef::named("test_types"), |_| {
2538                FieldFuture::new(async move { Ok(None::<async_graphql::Value>) })
2539            })
2540            .argument(InputValue::new(
2541                "objects",
2542                TypeRef::named_nn_list_nn("test_types_insert_input"),
2543            )),
2544        );
2545        Schema::build("query_root", Some("mutation_root"), None)
2546            .register(insert)
2547            .register(row)
2548            .register(query)
2549            .register(mutation)
2550            .finish()
2551            .expect("the schema builds")
2552    }
2553
2554    fn rewritten(query: &str) -> String {
2555        let schema = schema();
2556        let request = prepare(Some(&schema), async_graphql::Request::new(query), None)
2557            .unwrap_or_else(|(request, _)| request);
2558        let mut request = request;
2559        format!("{:?}", request.parsed_query().expect("the query parses"))
2560    }
2561
2562    #[test]
2563    fn an_offset_written_as_a_string_becomes_a_number() {
2564        let printed = rewritten("{ test_types(offset: \"1\") { c1_smallint } }");
2565        assert!(printed.contains("Number(1)"), "{}", printed);
2566    }
2567
2568    /// The corpus refuses this one in the same breath as it answers the
2569    /// offset above, so it is left exactly as written.
2570    #[test]
2571    fn a_limit_written_as_a_string_is_left_alone() {
2572        let printed = rewritten("{ test_types(limit: \"3\") { c1_smallint } }");
2573        assert!(printed.contains("String(\"3\")"), "{}", printed);
2574    }
2575
2576    #[test]
2577    fn a_column_written_as_a_string_becomes_what_the_column_is() {
2578        let printed = rewritten(
2579            "mutation { insert_test_types(objects: [{ \
2580             c1_smallint: \"32767\", c6_real: \"0.5\", c20_boolean: \"true\" }]) \
2581             { c1_smallint } }",
2582        );
2583        assert!(printed.contains("Number(32767)"), "{}", printed);
2584        assert!(printed.contains("Number(0.5)"), "{}", printed);
2585        assert!(printed.contains("Boolean(true)"), "{}", printed);
2586    }
2587
2588    /// A text column keeps its digits. This is the whole reason the walk is
2589    /// type-directed rather than a sweep over every string in the document.
2590    #[test]
2591    fn a_string_written_where_a_string_belongs_stays_one() {
2592        let printed = rewritten(
2593            "mutation { insert_test_types(objects: [{ c13_text: \"32767\" }]) { c1_smallint } }",
2594        );
2595        assert!(printed.contains("String(\"32767\")"), "{}", printed);
2596    }
2597}