Skip to main content

netget/server/graphql/
engine.rs

1//! GraphQL documents, schema, introspection and execution on apollo-compiler. Rust parses and
2//! validates every request against the configured SDL, answers introspection itself and runs
3//! the spec's execution (result coercion, null propagation, error paths) over the data the
4//! handler supplies, so the response always matches the query that was asked.
5use apollo_compiler::{
6    ast,
7    executable::{self, OperationType, Selection},
8    parser::Parser,
9    request::coerce_variable_values,
10    resolvers::{Execution, FieldError, ObjectValue, ResolveInfo, ResolvedValue},
11    response::{ExecutionResponse, GraphQLError, JsonMap, JsonValue},
12    schema::ExtendedType,
13    validation::Valid,
14    ExecutableDocument, Schema,
15};
16use serde_json::{json, Map, Value};
17
18pub const MAX_BODY_BYTES: usize = 1024 * 1024;
19pub const MAX_QUERY_BYTES: usize = 64 * 1024;
20pub const MAX_SCHEMA_BYTES: usize = 256 * 1024;
21/// Parser nesting bound: selection sets, lists and input objects all count toward it.
22pub const RECURSION_LIMIT: usize = 64;
23pub const TOKEN_LIMIT: usize = 15_000;
24const MAX_SHAPE_NODES: usize = 2_000;
25const MAX_ERRORS: usize = 64;
26
27pub fn budget_ok(v: &Value) -> bool {
28    crate::utils::json_budget::within_budget(v, MAX_BODY_BYTES, 50_000, 48)
29}
30
31pub fn load_schema(sdl: &str) -> anyhow::Result<Valid<Schema>> {
32    anyhow::ensure!(
33        !sdl.trim().is_empty() && sdl.len() <= MAX_SCHEMA_BYTES,
34        "schema must be 1 byte..256 KiB of SDL"
35    );
36    let schema = Parser::new()
37        .recursion_limit(RECURSION_LIMIT)
38        .parse_schema(sdl, "schema.graphql")
39        .map_err(|e| anyhow::anyhow!("schema does not parse: {}", first_message(&e.errors)))?;
40    let schema = schema
41        .validate()
42        .map_err(|e| anyhow::anyhow!("schema is invalid: {}", first_message(&e.errors)))?;
43    anyhow::ensure!(
44        schema.schema_definition.query.is_some(),
45        "schema has no Query type"
46    );
47    Ok(schema)
48}
49
50fn first_message(list: &apollo_compiler::validation::DiagnosticList) -> String {
51    list.iter()
52        .next()
53        .map(|d| d.error.to_string())
54        .unwrap_or_else(|| "unknown error".into())
55}
56
57/// A request error: no `data` key, a list of errors with locations where known.
58#[derive(Debug)]
59pub struct RequestErrors(pub Vec<GraphQLError>);
60
61impl RequestErrors {
62    pub fn one(message: impl Into<String>) -> Self {
63        Self(vec![GraphQLError {
64            message: message.into(),
65            locations: vec![],
66            path: vec![],
67            extensions: JsonMap::new(),
68        }])
69    }
70    pub fn body(&self) -> Value {
71        json!({"errors": self.0})
72    }
73}
74
75/// A parsed, validated request ready to execute.
76pub struct Prepared {
77    pub document: Valid<ExecutableDocument>,
78    pub operation_name: Option<String>,
79    pub operation_type: OperationType,
80    pub variables: Valid<JsonMap>,
81}
82
83/// Parse, validate, pick the operation and coerce variables — every request error in the spec's
84/// order (parse → validate → operation → variables).
85pub fn prepare(
86    schema: &Valid<Schema>,
87    query: &str,
88    operation_name: Option<&str>,
89    variables: &Map<String, Value>,
90) -> Result<Prepared, RequestErrors> {
91    if query.len() > MAX_QUERY_BYTES {
92        return Err(RequestErrors::one("query exceeds 64 KiB"));
93    }
94    let diagnostics = |list: &apollo_compiler::validation::DiagnosticList| {
95        RequestErrors(list.iter().take(MAX_ERRORS).map(|d| d.to_json()).collect())
96    };
97    let document = Parser::new()
98        .recursion_limit(RECURSION_LIMIT)
99        .token_limit(TOKEN_LIMIT)
100        .parse_executable(schema, query, "request.graphql")
101        .map_err(|e| diagnostics(&e.errors))?;
102    let document = document
103        .validate(schema)
104        .map_err(|e| diagnostics(&e.errors))?;
105    let operation = document
106        .operations
107        .get(operation_name)
108        .map_err(|_| match operation_name {
109            Some(n) => RequestErrors::one(format!("Unknown operation named \"{n}\".")),
110            None => RequestErrors::one(
111                "Must provide operation name if query contains multiple operations.",
112            ),
113        })?;
114    let raw: JsonMap = serde_json::from_value(Value::Object(variables.clone()))
115        .map_err(|_| RequestErrors::one("variables must be a JSON object"))?;
116    let coerced = coerce_variable_values(schema, operation, &raw)
117        .map_err(|e| RequestErrors(vec![e.to_graphql_error(&document.sources)]))?;
118    let operation_type = operation.operation_type;
119    let operation_name = operation.name.as_ref().map(|n| n.to_string());
120    Ok(Prepared {
121        document,
122        operation_name,
123        operation_type,
124        variables: coerced,
125    })
126}
127
128impl Prepared {
129    pub fn operation(&self) -> &executable::Operation {
130        self.document
131            .operations
132            .get(self.operation_name.as_deref())
133            .expect("operation was selected in prepare")
134    }
135
136    /// Only `__schema`, `__type` and `__typename` at the root: Rust answers it alone.
137    pub fn is_introspection(&self) -> bool {
138        self.operation().is_introspection(&self.document)
139    }
140
141    pub fn variables_json(&self) -> Value {
142        serde_json::to_value(&*self.variables).unwrap_or(json!({}))
143    }
144
145    /// Root fields as `{response_key, field, arguments}` with variables substituted.
146    pub fn root_fields(&self) -> Vec<Value> {
147        let vars = self.variables_json();
148        let mut out = Vec::new();
149        collect_fields(
150            &self.document,
151            &self.operation().selection_set,
152            &mut |f: &executable::Field| {
153                if f.name.starts_with("__") {
154                    return;
155                }
156                let args: Map<String, Value> = f
157                    .arguments
158                    .iter()
159                    .map(|a| (a.name.to_string(), ast_to_json(&a.value, &vars)))
160                    .collect();
161                out.push(json!({"response_key": f.response_key().as_str(), "field": f.name.as_str(), "arguments": args}));
162            },
163        );
164        out
165    }
166
167    /// The data the handler must supply, as a skeleton: response keys mapped to their GraphQL
168    /// type (`"String!"`) for leaves, to nested skeletons for objects, wrapped in `[...]` for lists.
169    pub fn shape(&self, schema: &Valid<Schema>) -> Value {
170        let mut budget = MAX_SHAPE_NODES;
171        shape_of(
172            schema,
173            &self.document,
174            &self.operation().selection_set,
175            &mut budget,
176        )
177    }
178
179    /// Run the operation over `data` (keyed by response key). Field errors land in the
180    /// response with paths; a type mismatch in `data` is a field error, never a crash.
181    pub fn execute(
182        &self,
183        schema: &Valid<Schema>,
184        data: &Value,
185        introspection: bool,
186    ) -> anyhow::Result<ExecutionResponse> {
187        let data: JsonValue = serde_json::from_value(data.clone())?;
188        let empty = JsonMap::new();
189        let root = DataObject {
190            schema,
191            type_name: self.operation().object_type().to_string(),
192            map: data.as_object().unwrap_or(&empty),
193        };
194        Execution::new(schema, &self.document)
195            .operation(self.operation())
196            .coerced_variable_values(&self.variables)
197            .enable_schema_introspection(introspection)
198            .execute_sync(&root)
199            .map_err(|e| anyhow::anyhow!("{}", e.message()))
200    }
201}
202
203fn collect_fields<'a>(
204    document: &'a ExecutableDocument,
205    set: &'a executable::SelectionSet,
206    f: &mut dyn FnMut(&'a executable::Field),
207) {
208    for s in &set.selections {
209        match s {
210            Selection::Field(field) => f(field),
211            Selection::InlineFragment(i) => collect_fields(document, &i.selection_set, f),
212            Selection::FragmentSpread(sp) => {
213                if let Some(frag) = document.fragments.get(&sp.fragment_name) {
214                    collect_fields(document, &frag.selection_set, f)
215                }
216            }
217        }
218    }
219}
220
221fn shape_of(
222    schema: &Valid<Schema>,
223    document: &ExecutableDocument,
224    set: &executable::SelectionSet,
225    budget: &mut usize,
226) -> Value {
227    let mut out = Map::new();
228    if matches!(
229        schema.types.get(&set.ty),
230        Some(ExtendedType::Interface(_) | ExtendedType::Union(_))
231    ) {
232        let mut names: Vec<String> = schema
233            .types
234            .iter()
235            .filter(|(n, t)| t.is_object() && schema.is_subtype(&set.ty, n))
236            .map(|(n, _)| n.to_string())
237            .collect();
238        names.sort();
239        out.insert(
240            "__typename".into(),
241            json!(format!("one of {}", names.join("|"))),
242        );
243    }
244    collect_fields(document, set, &mut |f: &executable::Field| {
245        if *budget == 0 {
246            return;
247        }
248        *budget -= 1;
249        let key = f.response_key().to_string();
250        if f.name == "__typename" {
251            out.entry(key).or_insert(json!("String!"));
252            return;
253        }
254        let value = if f.selection_set.selections.is_empty() {
255            json!(f.definition.ty.to_string())
256        } else {
257            let inner = shape_of(schema, document, &f.selection_set, budget);
258            let mut ty = &f.definition.ty;
259            let mut v = inner;
260            let mut depth = Vec::new();
261            while let Some(item) = list_item(ty) {
262                depth.push(());
263                ty = item;
264            }
265            for _ in depth {
266                v = json!([v]);
267            }
268            v
269        };
270        match (out.get_mut(&key), value) {
271            (Some(Value::Object(existing)), Value::Object(more)) => existing.extend(more),
272            (_, value) => {
273                out.insert(key, value);
274            }
275        }
276    });
277    Value::Object(out)
278}
279
280fn list_item(ty: &ast::Type) -> Option<&ast::Type> {
281    match ty {
282        ast::Type::List(inner) | ast::Type::NonNullList(inner) => Some(inner),
283        _ => None,
284    }
285}
286
287pub fn ast_to_json(v: &ast::Value, vars: &Value) -> Value {
288    match v {
289        ast::Value::Null => Value::Null,
290        ast::Value::Enum(n) => json!(n.as_str()),
291        ast::Value::Variable(n) => vars.get(n.as_str()).cloned().unwrap_or(Value::Null),
292        ast::Value::String(s) => json!(s),
293        ast::Value::Float(f) => f.try_to_f64().map(|x| json!(x)).unwrap_or(Value::Null),
294        ast::Value::Int(i) => i
295            .as_str()
296            .parse::<i64>()
297            .map(|x| json!(x))
298            .unwrap_or(Value::Null),
299        ast::Value::Boolean(b) => json!(b),
300        ast::Value::List(items) => {
301            Value::Array(items.iter().map(|x| ast_to_json(x, vars)).collect())
302        }
303        ast::Value::Object(fields) => Value::Object(
304            fields
305                .iter()
306                .map(|(k, x)| (k.to_string(), ast_to_json(x, vars)))
307                .collect(),
308        ),
309    }
310}
311
312/// An object in the handler's data. Fields are looked up by response key (so aliases with
313/// different arguments get different values), then by field name.
314struct DataObject<'a> {
315    schema: &'a Valid<Schema>,
316    type_name: String,
317    map: &'a JsonMap,
318}
319
320impl ObjectValue for DataObject<'_> {
321    fn type_name(&self) -> &str {
322        &self.type_name
323    }
324
325    fn resolve_field<'b>(
326        &'b self,
327        info: &'b ResolveInfo<'b>,
328    ) -> Result<ResolvedValue<'b>, FieldError> {
329        let field = &info.field_selections()[0];
330        let value = self
331            .map
332            .get(field.response_key().as_str())
333            .or_else(|| self.map.get(field.name.as_str()));
334        match value {
335            None => Ok(ResolvedValue::null()),
336            Some(v) => resolve(self.schema, &info.field_definition().ty, v),
337        }
338    }
339}
340
341fn error(message: impl Into<String>) -> FieldError {
342    FieldError {
343        message: message.into(),
344    }
345}
346
347fn resolve<'a>(
348    schema: &'a Valid<Schema>,
349    ty: &'a ast::Type,
350    v: &'a JsonValue,
351) -> Result<ResolvedValue<'a>, FieldError> {
352    if v.is_null() {
353        return Ok(ResolvedValue::null());
354    }
355    if let Some(item) = list_item(ty) {
356        let items = v
357            .as_array()
358            .ok_or_else(|| error(format!("handler data is not a list for {ty}")))?;
359        return Ok(ResolvedValue::List(Box::new(
360            items.iter().map(move |x| resolve(schema, item, x)),
361        )));
362    }
363    let name = ty.inner_named_type();
364    match schema.types.get(name) {
365        Some(ExtendedType::Scalar(_) | ExtendedType::Enum(_)) => Ok(ResolvedValue::Leaf(v.clone())),
366        Some(ExtendedType::Object(_)) => Ok(ResolvedValue::object(DataObject {
367            schema,
368            type_name: name.to_string(),
369            map: v
370                .as_object()
371                .ok_or_else(|| error(format!("handler data is not an object for {name}")))?,
372        })),
373        Some(ExtendedType::Interface(_) | ExtendedType::Union(_)) => {
374            let map = v
375                .as_object()
376                .ok_or_else(|| error(format!("handler data is not an object for {name}")))?;
377            let candidates: Vec<&str> = schema
378                .types
379                .iter()
380                .filter(|(n, t)| t.is_object() && schema.is_subtype(name, n))
381                .map(|(n, _)| n.as_str())
382                .collect();
383            let concrete = match map.get("__typename").and_then(|t| t.as_str()) {
384                Some(t) if candidates.contains(&t) => t.to_owned(),
385                Some(t) => {
386                    return Err(error(format!(
387                        "__typename {t} is not a possible type of {name}"
388                    )))
389                }
390                None if candidates.len() == 1 => candidates[0].to_owned(),
391                None => {
392                    return Err(error(format!(
393                        "handler data for abstract type {name} needs __typename (one of {})",
394                        candidates.join("|")
395                    )))
396                }
397            };
398            Ok(ResolvedValue::object(DataObject {
399                schema,
400                type_name: concrete,
401                map,
402            }))
403        }
404        _ => Err(error(format!("{name} is not an output type"))),
405    }
406}
407
408/// Handler errors appended to the response: `message` required, `path` optional (strings and
409/// non-negative integers), `extensions` optional.
410pub fn handler_errors(list: Option<&Value>) -> anyhow::Result<Vec<GraphQLError>> {
411    let Some(list) = list.filter(|l| !l.is_null()) else {
412        return Ok(vec![]);
413    };
414    let items = list
415        .as_array()
416        .ok_or_else(|| anyhow::anyhow!("errors must be an array"))?;
417    anyhow::ensure!(items.len() <= MAX_ERRORS, "at most {MAX_ERRORS} errors");
418    items
419        .iter()
420        .map(|e| {
421            let message = e["message"]
422                .as_str()
423                .filter(|m| !m.is_empty() && m.len() <= 4096)
424                .ok_or_else(|| anyhow::anyhow!("each error needs a message of 1..4096 bytes"))?;
425            let mut out = json!({"message": message});
426            if let Some(path) = e.get("path").filter(|p| !p.is_null()) {
427                let ok = path.as_array().is_some_and(|p| {
428                    p.len() <= 64 && p.iter().all(|s| s.is_string() || s.is_u64())
429                });
430                anyhow::ensure!(ok, "error path must be an array of field names and indexes");
431                out["path"] = path.clone();
432            }
433            if let Some(ext) = e.get("extensions").filter(|x| !x.is_null()) {
434                anyhow::ensure!(ext.is_object(), "error extensions must be an object");
435                out["extensions"] = ext.clone();
436            }
437            Ok(serde_json::from_value(out)?)
438        })
439        .collect()
440}
441
442/// Syntax-only parse for the client: operation type and name of the selected operation.
443pub fn parse_operation(
444    query: &str,
445    operation_name: Option<&str>,
446) -> anyhow::Result<(OperationType, Option<String>)> {
447    anyhow::ensure!(
448        !query.trim().is_empty() && query.len() <= MAX_QUERY_BYTES,
449        "query must be 1 byte..64 KiB"
450    );
451    let doc = Parser::new()
452        .recursion_limit(RECURSION_LIMIT)
453        .token_limit(TOKEN_LIMIT)
454        .parse_ast(query, "request.graphql")
455        .map_err(|e| anyhow::anyhow!("query does not parse: {}", first_message(&e.errors)))?;
456    let ops: Vec<&ast::OperationDefinition> = doc
457        .definitions
458        .iter()
459        .filter_map(|d| match d {
460            ast::Definition::OperationDefinition(o) => Some(&**o),
461            _ => None,
462        })
463        .collect();
464    let op = match operation_name {
465        Some(n) => ops
466            .iter()
467            .find(|o| o.name.as_ref().is_some_and(|x| x == n))
468            .ok_or_else(|| anyhow::anyhow!("no operation named {n}"))?,
469        None => {
470            anyhow::ensure!(
471                ops.len() == 1,
472                "name the operation to run (the document has {})",
473                ops.len()
474            );
475            &ops[0]
476        }
477    };
478    Ok((op.operation_type, op.name.as_ref().map(|n| n.to_string())))
479}
480
481pub fn operation_type_name(t: OperationType) -> &'static str {
482    match t {
483        OperationType::Query => "query",
484        OperationType::Mutation => "mutation",
485        OperationType::Subscription => "subscription",
486    }
487}
488
489/// A GraphQL response body as the client sees it: `data` and/or `errors`, each well formed.
490pub fn check_response(body: &Value) -> anyhow::Result<()> {
491    let obj = body
492        .as_object()
493        .ok_or_else(|| anyhow::anyhow!("response is not a JSON object"))?;
494    anyhow::ensure!(
495        obj.contains_key("data") || obj.contains_key("errors"),
496        "response has neither data nor errors"
497    );
498    if let Some(d) = obj.get("data") {
499        anyhow::ensure!(
500            d.is_null() || d.is_object(),
501            "data must be an object or null"
502        );
503    }
504    if let Some(errors) = obj.get("errors") {
505        let list = errors
506            .as_array()
507            .filter(|l| !l.is_empty())
508            .ok_or_else(|| anyhow::anyhow!("errors must be a non-empty array"))?;
509        anyhow::ensure!(
510            list.iter().all(|e| e["message"].is_string()),
511            "every error needs a message"
512        );
513    } else {
514        anyhow::ensure!(!obj["data"].is_null(), "data is null without errors");
515    }
516    if let Some(x) = obj.get("extensions") {
517        anyhow::ensure!(x.is_object(), "extensions must be an object");
518    }
519    Ok(())
520}
521
522/// Introspection the client runs on connect: root types and their fields with argument and
523/// return types, enough for a handler to know what it can ask.
524pub const CLIENT_INTROSPECTION: &str = "query NetGetIntrospection { __schema { queryType { name } mutationType { name } subscriptionType { name } types { kind name fields { name type { ...T } args { name type { ...T } } } } } } fragment T on __Type { kind name ofType { kind name ofType { kind name ofType { kind name } } } }";
525
526fn type_ref(t: &Value) -> String {
527    match t["kind"].as_str() {
528        Some("NON_NULL") => format!("{}!", type_ref(&t["ofType"])),
529        Some("LIST") => format!("[{}]", type_ref(&t["ofType"])),
530        _ => t["name"].as_str().unwrap_or("?").to_owned(),
531    }
532}
533
534/// Summarise an introspection answer as root-field signatures (`book(id: ID!): Book`).
535pub fn root_signatures(data: &Value) -> Value {
536    let schema = &data["__schema"];
537    let types = schema["types"].as_array().cloned().unwrap_or_default();
538    let fields_of = |root: &str| -> Vec<Value> {
539        let Some(name) = schema[root]["name"].as_str() else {
540            return vec![];
541        };
542        types
543            .iter()
544            .find(|t| t["name"] == name)
545            .and_then(|t| t["fields"].as_array())
546            .map(|fs| {
547                fs.iter()
548                    .take(256)
549                    .map(|f| {
550                        let args: Vec<String> = f["args"]
551                            .as_array()
552                            .map(|a| {
553                                a.iter()
554                                    .map(|x| {
555                                        format!(
556                                            "{}: {}",
557                                            x["name"].as_str().unwrap_or("?"),
558                                            type_ref(&x["type"])
559                                        )
560                                    })
561                                    .collect()
562                            })
563                            .unwrap_or_default();
564                        let args = if args.is_empty() {
565                            String::new()
566                        } else {
567                            format!("({})", args.join(", "))
568                        };
569                        json!(format!(
570                            "{}{args}: {}",
571                            f["name"].as_str().unwrap_or("?"),
572                            type_ref(&f["type"])
573                        ))
574                    })
575                    .collect()
576            })
577            .unwrap_or_default()
578    };
579    json!({"query": fields_of("queryType"), "mutation": fields_of("mutationType"), "subscription": fields_of("subscriptionType")})
580}