1use 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;
21pub 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#[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
75pub 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
83pub 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 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 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 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 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
312struct 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
408pub 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
442pub 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
489pub 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
522pub 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
534pub 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}