subgraph_mock/handle/
graphql.rs

1use crate::{
2    handle::ByteResponse,
3    state::{Config, FederatedSchema, State},
4};
5use anyhow::anyhow;
6use apollo_compiler::{
7    ExecutableDocument, Name, Node, Schema,
8    ast::OperationType,
9    executable::{Field, Selection, SelectionSet},
10    request::coerce_variable_values,
11    response::JsonMap,
12    schema::ExtendedType,
13    validation::{Valid, WithErrors},
14};
15use cached::proc_macro::cached;
16use http_body_util::{BodyExt, Empty, Full};
17use hyper::{
18    HeaderMap, Response, StatusCode,
19    body::Bytes,
20    header::{HeaderName, HeaderValue},
21};
22use ordered_float::OrderedFloat;
23use rand::{Rng, rngs::ThreadRng, seq::IteratorRandom};
24use serde::{Deserialize, Deserializer, Serialize};
25use serde_json_bytes::{
26    ByteString, Map, Value, json,
27    serde_json::{self, Number},
28};
29use std::{
30    collections::{BTreeMap, HashMap, HashSet},
31    hash::{DefaultHasher, Hash, Hasher},
32    mem,
33    ops::RangeInclusive,
34    sync::Arc,
35};
36use tracing::{debug, error, trace};
37
38pub async fn handle(
39    body_bytes: Vec<u8>,
40    subgraph_name: Option<&str>,
41    state: Arc<State>,
42) -> anyhow::Result<ByteResponse> {
43    let req: GraphQLRequest = match serde_json::from_slice(&body_bytes) {
44        Ok(req) => req,
45        Err(err) => {
46            error!(%err, "received invalid graphql request");
47            let mut resp = Response::new(
48                Full::new(err.to_string().into_bytes().into())
49                    .map_err(|never| match never {})
50                    .boxed(),
51            );
52            *resp.status_mut() = StatusCode::BAD_REQUEST;
53
54            return Ok(resp);
55        }
56    };
57
58    let config = state.config.read().await;
59    let schema = state.schema.read().await;
60    let rgen_cfg = subgraph_name
61        .and_then(|name| config.subgraph_overrides.response_generation.get(name))
62        .unwrap_or_else(|| &config.response_generation);
63
64    // Since the response gen config and schema can be reloaded, they need to be included in the cache hash
65    // alongside the query itself. This does mean that hot reloads will balloon memory over time since the old
66    // values aren't invalidated. If we find this to actually be a practical problem in test scenarios that
67    // demand a high cardinality of config/schema setups, we can set up more intelligent caching with invalidation.
68    let mut hasher = DefaultHasher::new();
69    req.query.hash(&mut hasher);
70    rgen_cfg.hash(&mut hasher);
71    schema.hash(&mut hasher);
72    let cache_hash = hasher.finish();
73
74    if let Some((numerator, denominator)) = rgen_cfg.http_error_ratio {
75        let mut rng = rand::rng();
76        if rng.random_ratio(numerator, denominator) {
77            return Response::builder()
78                .status(rng.random_range(500..=504))
79                .body(Empty::new().map_err(|never| match never {}).boxed())
80                .map_err(|err| err.into());
81        }
82    }
83
84    let (bytes, status_code) = if subgraph_name
85        .and_then(|name| config.subgraph_overrides.cache_responses.get(name).copied())
86        .unwrap_or_else(|| config.cache_responses)
87    {
88        into_response_bytes_and_status_code(rgen_cfg, req, &schema, cache_hash).await
89    } else {
90        into_response_bytes_and_status_code_no_cache(rgen_cfg, req, &schema, cache_hash).await
91    };
92
93    let mut resp = Response::new(Full::new(bytes).map_err(|never| match never {}).boxed());
94    *resp.status_mut() = status_code;
95
96    let headers = resp.headers_mut();
97    add_headers(&config, rgen_cfg, subgraph_name, headers);
98
99    Ok(resp)
100}
101
102#[derive(Debug, Serialize, Deserialize)]
103#[serde(rename_all = "camelCase")]
104pub struct GraphQLRequest {
105    pub query: String,
106    pub operation_name: Option<String>,
107    #[serde(default)]
108    #[serde(deserialize_with = "null_or_missing_as_default")]
109    pub variables: JsonMap,
110}
111
112/// Allows a field to be either null *or* not present in a request. Some GraphQL implementations
113/// specifically set variables to null rather than omitting them or providing an empty struct.
114fn null_or_missing_as_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
115where
116    D: Deserializer<'de>,
117    T: Default + Deserialize<'de>,
118{
119    Ok(Option::<T>::deserialize(deserializer)?.unwrap_or_default())
120}
121
122fn add_headers(
123    config: &Config,
124    rgen_cfg: &ResponseGenerationConfig,
125    subgraph_name: Option<&str>,
126    headers: &mut HeaderMap,
127) {
128    let mut rng = rand::rng();
129
130    // HeaderMap is a multimap and yields Some(HeaderName) only for the first element of each multimap.
131    // We have to track the last one we saw and treat that as the key for all subsequent None values as such.
132    // Based on that contract, the first iteration will *always* yield a value so we can safely just initialize
133    // this to a dummy value and trust that it will get overwritten instead of using an Option.
134    let mut last_header_name: HeaderName = HeaderName::from_static("unused");
135    let mut last_ratio: Option<Ratio> = None;
136
137    for (header_name, header_value) in subgraph_name
138        .and_then(|name| config.subgraph_overrides.headers.get(name).cloned())
139        .unwrap_or_else(|| config.headers.clone())
140        .into_iter()
141    {
142        if let Some(name) = header_name {
143            last_ratio = rgen_cfg.header_ratio.get(name.as_str()).copied();
144            last_header_name = name;
145        }
146
147        let should_insert = last_ratio
148            .is_none_or(|(numerator, denominator)| rng.random_ratio(numerator, denominator));
149
150        if should_insert {
151            headers.insert(&last_header_name, header_value);
152        }
153    }
154
155    headers.insert("Content-Type", HeaderValue::from_static("application/json"));
156}
157
158#[cached(result = true, key = "u64", convert = "{_cache_hash}")]
159fn parse_and_validate(
160    req: &GraphQLRequest,
161    schema: &Valid<Schema>,
162    _cache_hash: u64,
163) -> Result<Valid<ExecutableDocument>, WithErrors<ExecutableDocument>> {
164    let op_name = req.operation_name.as_deref().unwrap_or("unknown");
165
166    ExecutableDocument::parse_and_validate(schema, &req.query, op_name)
167}
168
169#[tracing::instrument(skip(req))]
170#[cached(key = "u64", convert = "{cache_hash}")]
171async fn into_response_bytes_and_status_code(
172    cfg: &ResponseGenerationConfig,
173    req: GraphQLRequest,
174    schema: &FederatedSchema,
175    cache_hash: u64,
176) -> (Bytes, StatusCode) {
177    debug!(%cache_hash, "handling graphql request");
178    trace!(variables=?req.variables, "request variables");
179
180    let doc = match parse_and_validate(&req, schema, cache_hash) {
181        Ok(doc) => doc,
182        Err(err) => {
183            let errs: Vec<_> = err.errors.iter().map(|d| d.to_json()).collect();
184            error!(?errs, query=%req.query, "invalid graphql query");
185            let bytes = serde_json::to_vec(&json!({ "data": Value::Null, "errors": errs }))
186                .unwrap_or_default();
187            return (bytes.into(), StatusCode::BAD_REQUEST);
188        }
189    };
190
191    let op = doc.operations.iter().next().unwrap();
192    let op_name = op.name.as_ref().map(|name| name.as_str());
193
194    debug!(
195        ?op_name,
196        type=%op.operation_type,
197        n_selections = op.selection_set.selections.len(),
198        "processing operation"
199    );
200
201    let resp = match op.operation_type {
202        OperationType::Query => {
203            match generate_response(cfg, op_name, &doc, schema, &req.variables) {
204                Ok(resp) => resp,
205                Err(err) => {
206                    error!(%err, "unable to generate response");
207                    return (
208                        Bytes::from("unable to generate response"),
209                        StatusCode::INTERNAL_SERVER_ERROR,
210                    );
211                }
212            }
213        }
214
215        // Not currently supporting mutations or subscriptions
216        op_type => {
217            error!("received {op_type} request: not implemented");
218            return (
219                Bytes::from("not implemented"),
220                StatusCode::INTERNAL_SERVER_ERROR,
221            );
222        }
223    };
224
225    match serde_json::to_vec(&resp) {
226        Ok(bytes) => (bytes.into(), StatusCode::OK),
227        Err(err) => {
228            error!(%err, "unable to serialize response");
229            (
230                Bytes::from(err.to_string().into_bytes()),
231                StatusCode::INTERNAL_SERVER_ERROR,
232            )
233        }
234    }
235}
236
237fn generate_response(
238    cfg: &ResponseGenerationConfig,
239    op_name: Option<&str>,
240    doc: &Valid<ExecutableDocument>,
241    schema: &FederatedSchema,
242    variables: &JsonMap,
243) -> anyhow::Result<Value> {
244    let op = match doc.operations.get(op_name) {
245        Ok(op) => op,
246        Err(_) => return Ok(json!({ "data": null })),
247    };
248    let mut rng = rand::rng();
249
250    if let Some((numerator, denominator)) = cfg.graphql_errors.request_error_ratio
251        && rng.random_ratio(numerator, denominator)
252    {
253        return Ok(json!({ "data": null, "errors": [{ "message": "Request error simulated" }]}));
254    }
255
256    // Short-circuit introspection responses if a request is *only* introspection. This does mean that requests
257    // that combine both introspection and non-introspection fields in their query will get random data for
258    // the introspection fields. For our use-cases we only need correct introspection data if that is the only
259    // data being requested, but if we want to make this fully spec-compliant in the future we will need to merge
260    // the result of `partial_execute` with the random data generated on every query (which would be costlier).
261    if op.is_introspection(doc) {
262        return apollo_compiler::introspection::partial_execute(
263            schema,
264            &schema.implementers_map(),
265            doc,
266            op,
267            &coerce_variable_values(schema, op, variables)
268                .map_err(|err| anyhow!("{}", err.message()))?,
269        )
270        .map_err(|err| anyhow!("{}", err.message()))
271        .and_then(|result| serde_json_bytes::to_value(result).map_err(|err| anyhow!("{}", err)));
272    }
273
274    let mut data =
275        ResponseBuilder::new(&mut rng, doc, schema, cfg).selection_set(&op.selection_set)?;
276
277    // Select a random number of top-level fields to "fail" if we are going to have field errors. For the sake of
278    // simplicity and performance, we won't traverse deeper into the response object.
279    if let Some((numerator, denominator)) = cfg.graphql_errors.field_error_ratio
280        && rng.random_ratio(numerator, denominator)
281    {
282        let drop_count = rng.random_range(1..=data.len());
283        let sampled_keys = data.keys().cloned().choose_multiple(&mut rng, drop_count);
284        let to_drop: HashSet<ByteString> = HashSet::from_iter(sampled_keys);
285
286        data.retain(|key, _| !to_drop.contains(key));
287
288        let errors: Vec<Value> = to_drop
289            .into_iter()
290            .map(|key| {
291                json!({
292                    "message": "Field error simulated",
293                    "path": [key]
294                })
295            })
296            .collect();
297
298        Ok(json!({
299            "data": data,
300            "errors": errors,
301        }))
302    } else {
303        Ok(json!({ "data": data }))
304    }
305}
306
307pub type Ratio = (u32, u32);
308
309#[derive(Debug, Default, Clone, Hash, Serialize, Deserialize)]
310pub struct GraphQLErrorConfig {
311    /// The ratio of GraphQL requests that should be responded to with a request error and no data.
312    ///
313    /// Defaults to no requests containing errors.
314    pub request_error_ratio: Option<Ratio>,
315    /// The ratio of GraphQL requests that should include field-level errors and partial data.
316    /// Note that if both this field and the request error ratio are set, this ratio will be applicable
317    /// to the subset of requests that do not have request errors.
318    ///
319    /// For example, if you have a `request_error_ratio` of `[1,3]`, and a `field_error_ratio` of `[1,4]`,
320    /// then only 1 in 6 of your total requests will contain field errors.
321    ///
322    /// Defaults to no requests containing errors.
323    pub field_error_ratio: Option<Ratio>,
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize, Hash)]
327pub struct ResponseGenerationConfig {
328    #[serde(default = "default_scalar_config")]
329    pub scalars: BTreeMap<String, ScalarGenerator>,
330    #[serde(default = "default_array_size")]
331    pub array: ArraySize,
332    #[serde(default = "default_null_ratio")]
333    pub null_ratio: Option<Ratio>,
334    #[serde(default)]
335    pub header_ratio: BTreeMap<String, (u32, u32)>,
336    #[serde(default)]
337    pub http_error_ratio: Option<Ratio>,
338    #[serde(default)]
339    pub graphql_errors: GraphQLErrorConfig,
340}
341
342impl ResponseGenerationConfig {
343    /// Merges the default scalar config with the provided config, allowing users to specify a partial set of scalar
344    /// generators while inheriting the default configuration for those they do not specify.
345    pub(crate) fn merge_default_scalars(&mut self) {
346        let default = default_scalar_config();
347        let provided = mem::replace(&mut self.scalars, default);
348        self.scalars.extend(provided);
349    }
350}
351
352impl Default for ResponseGenerationConfig {
353    fn default() -> Self {
354        Self {
355            scalars: default_scalar_config(),
356            array: default_array_size(),
357            null_ratio: default_null_ratio(),
358            header_ratio: BTreeMap::new(),
359            graphql_errors: GraphQLErrorConfig::default(),
360            http_error_ratio: None,
361        }
362    }
363}
364
365fn default_scalar_config() -> BTreeMap<String, ScalarGenerator> {
366    [
367        ("Boolean".into(), ScalarGenerator::Bool),
368        ("Int".into(), ScalarGenerator::Int { min: 0, max: 100 }),
369        ("ID".into(), ScalarGenerator::Int { min: 0, max: 100 }),
370        (
371            "Float".into(),
372            ScalarGenerator::Float {
373                min: OrderedFloat(-1.0),
374                max: OrderedFloat(1.0),
375            },
376        ),
377        (
378            "String".into(),
379            ScalarGenerator::String {
380                min_len: 1,
381                max_len: 10,
382            },
383        ),
384    ]
385    .into_iter()
386    .collect()
387}
388
389fn default_array_size() -> ArraySize {
390    ArraySize {
391        min_length: 0,
392        max_length: 10,
393    }
394}
395
396fn default_null_ratio() -> Option<Ratio> {
397    Some((1, 2))
398}
399
400#[derive(Debug, Clone, Copy, Serialize, Deserialize, Hash)]
401#[serde(tag = "type", rename_all = "lowercase")]
402pub enum ScalarGenerator {
403    Bool,
404    Float {
405        min: OrderedFloat<f64>,
406        max: OrderedFloat<f64>,
407    },
408    Int {
409        min: i32,
410        max: i32,
411    },
412    String {
413        min_len: usize,
414        max_len: usize,
415    },
416}
417
418impl Default for ScalarGenerator {
419    fn default() -> Self {
420        Self::DEFAULT
421    }
422}
423
424impl ScalarGenerator {
425    const DEFAULT: Self = Self::String {
426        min_len: 1,
427        max_len: 10,
428    };
429
430    fn generate(&self, rng: &mut ThreadRng) -> anyhow::Result<Value> {
431        let val = match *self {
432            Self::Bool => Value::Bool(rng.random_bool(0.5)),
433            Self::Int { min, max } => Value::Number(rng.random_range(min..=max).into()),
434
435            Self::Float { min, max } => Value::Number(
436                Number::from_f64(rng.random_range(*min..=*max)).expect("expected finite float"),
437            ),
438
439            // The default Arbitrary impl for String has a random length so we build based on
440            // characters instead
441            Self::String { min_len, max_len } => {
442                let len = rng.random_range(min_len..=max_len);
443                // Allow for some multibyte chars. May still need to realloc
444                let mut chars = Vec::with_capacity(len * 2);
445                for _ in 0..len {
446                    chars.push(rng.random::<char>());
447                }
448
449                Value::String(ByteString::from(chars.into_iter().collect::<String>()))
450            }
451        };
452
453        Ok(val)
454    }
455}
456
457#[derive(Debug, Clone, Copy, Serialize, Deserialize, Hash)]
458pub struct ArraySize {
459    pub min_length: usize,
460    pub max_length: usize,
461}
462
463impl ArraySize {
464    fn range(&self) -> RangeInclusive<usize> {
465        self.min_length..=self.max_length
466    }
467}
468
469struct ResponseBuilder<'a, 'doc, 'schema> {
470    rng: &'a mut ThreadRng,
471    doc: &'doc Valid<ExecutableDocument>,
472    schema: &'schema FederatedSchema,
473    cfg: &'a ResponseGenerationConfig,
474}
475
476impl<'a, 'doc, 'schema> ResponseBuilder<'a, 'doc, 'schema> {
477    fn new(
478        rng: &'a mut ThreadRng,
479        doc: &'doc Valid<ExecutableDocument>,
480        schema: &'schema FederatedSchema,
481        cfg: &'a ResponseGenerationConfig,
482    ) -> Self {
483        Self {
484            rng,
485            doc,
486            schema,
487            cfg,
488        }
489    }
490
491    fn selection_set(
492        &mut self,
493        selection_set: &SelectionSet,
494    ) -> anyhow::Result<Map<ByteString, Value>> {
495        let grouped_fields = self.collect_fields(selection_set)?;
496        let mut result = Map::new();
497
498        for (key, fields) in grouped_fields {
499            // The first occurrence of a field is representative for metadata that is defined by the schema
500            let meta_field = fields[0];
501
502            let val = if meta_field.name == "__typename" {
503                Value::String(ByteString::from(selection_set.ty.to_string()))
504            } else if meta_field.name == "_service" {
505                let mut service_obj = Map::new();
506                service_obj.insert("sdl".to_string(), Value::String(self.schema.sdl().into()));
507                Value::Object(service_obj)
508            } else if !meta_field.ty().is_non_null() && self.should_be_null() {
509                Value::Null
510            } else {
511                let is_selection_set = !meta_field.selection_set.is_empty();
512                let is_array = meta_field.ty().is_list();
513
514                if is_selection_set {
515                    let mut selections = Vec::new();
516                    for field in fields {
517                        selections.extend_from_slice(&field.selection_set.selections);
518                    }
519                    let full_selection_set = SelectionSet {
520                        ty: meta_field.selection_set.ty.clone(),
521                        selections,
522                    };
523
524                    if is_array {
525                        Value::Array(self.array_selection_set(&full_selection_set)?)
526                    } else {
527                        Value::Object(self.selection_set(&full_selection_set)?)
528                    }
529                } else {
530                    match is_array {
531                        false => self.leaf_field(meta_field.ty().inner_named_type())?,
532                        true => self.array_leaf_field(meta_field.ty().inner_named_type())?,
533                    }
534                }
535            };
536
537            result.insert(key, val);
538        }
539
540        Ok(result)
541    }
542
543    fn collect_fields(
544        &self,
545        selection_set: &'doc SelectionSet,
546    ) -> anyhow::Result<HashMap<String, Vec<&'doc Node<Field>>>> {
547        let mut collected_fields: HashMap<String, Vec<&Node<Field>>> = HashMap::new();
548
549        for selection in &selection_set.selections {
550            match selection {
551                Selection::Field(field) => {
552                    let key = field.alias.as_ref().unwrap_or(&field.name).to_string();
553                    collected_fields.entry(key).or_default().push(field);
554                }
555                Selection::FragmentSpread(fragment) => {
556                    if let Some(fragment_def) = self.doc.fragments.get(&fragment.fragment_name) {
557                        for (key, mut fields) in self.collect_fields(&fragment_def.selection_set)? {
558                            collected_fields.entry(key).or_default().append(&mut fields);
559                        }
560                    }
561                }
562                Selection::InlineFragment(inline_fragment) => {
563                    for (key, mut fields) in self.collect_fields(&inline_fragment.selection_set)? {
564                        collected_fields.entry(key).or_default().append(&mut fields);
565                    }
566                }
567            }
568        }
569
570        Ok(collected_fields)
571    }
572
573    fn leaf_field(&mut self, type_name: &Name) -> anyhow::Result<Value> {
574        match self.schema.types.get(type_name).unwrap() {
575            ExtendedType::Enum(enum_ty) => {
576                let enum_value = enum_ty
577                    .values
578                    .values()
579                    .choose(self.rng)
580                    .ok_or(anyhow!("empty enum: {type_name}"))?;
581
582                Ok(Value::String(ByteString::from(
583                    enum_value.value.to_string(),
584                )))
585            }
586
587            ExtendedType::Scalar(scalar) => self
588                .cfg
589                .scalars
590                .get(scalar.name.as_str())
591                .unwrap_or(&ScalarGenerator::DEFAULT)
592                .generate(self.rng),
593
594            _ => unreachable!("A field with an empty selection set must be a scalar or enum type"),
595        }
596    }
597
598    fn arbitrary_array_len(&mut self) -> anyhow::Result<usize> {
599        Ok(self.rng.random_range(self.cfg.array.range()))
600    }
601
602    fn array_selection_set(&mut self, selection_set: &SelectionSet) -> anyhow::Result<Vec<Value>> {
603        let num_values = self.arbitrary_array_len()?;
604        let mut values = Vec::with_capacity(num_values);
605        for _ in 0..num_values {
606            values.push(Value::Object(self.selection_set(selection_set)?));
607        }
608
609        Ok(values)
610    }
611
612    fn array_leaf_field(&mut self, type_name: &Name) -> anyhow::Result<Value> {
613        let num_values = self.arbitrary_array_len()?;
614        let mut values = Vec::with_capacity(num_values);
615        for _ in 0..num_values {
616            values.push(self.leaf_field(type_name)?);
617        }
618
619        Ok(Value::Array(values))
620    }
621
622    fn should_be_null(&mut self) -> bool {
623        if let Some((numerator, denominator)) = self.cfg.null_ratio {
624            self.rng.random_ratio(numerator, denominator)
625        } else {
626            false
627        }
628    }
629}
630
631#[cfg(test)]
632mod tests {
633    use super::*;
634
635    #[test]
636    fn introspection_short_circuits() -> anyhow::Result<()> {
637        let supergraph = include_str!("../../tests/data/schema.graphql");
638        let schema = FederatedSchema::parse_string(supergraph, "../../tests/data/schema.graphql")?;
639
640        let query = r#"
641            query {
642                __schema {
643                    queryType {
644                        name
645                    }
646                    types {
647                        name
648                        kind
649                    }
650                }
651            }
652        "#;
653
654        let doc = ExecutableDocument::parse_and_validate(&schema, query, "query.graphql").unwrap();
655        let cfg = ResponseGenerationConfig::default();
656        let result = generate_response(&cfg, None, &doc, &schema, &JsonMap::new())?;
657
658        assert!(result.get("data").is_some());
659        let data = result.get("data").unwrap();
660        assert!(data.get("__schema").is_some());
661        // No other random data is included
662        assert!(data.as_object().unwrap().len() == 1);
663
664        let schema_obj = data.get("__schema").unwrap();
665        assert!(schema_obj.get("queryType").is_some());
666
667        let query_type = schema_obj.get("queryType").unwrap();
668        assert_eq!(query_type.get("name").unwrap().as_str().unwrap(), "Query");
669
670        let types = schema_obj.get("types").unwrap().as_array().unwrap();
671        assert!(!types.is_empty());
672
673        let type_names: Vec<&str> = types
674            .iter()
675            .filter_map(|t| t.get("name")?.as_str())
676            .collect();
677        assert!(type_names.contains(&"Query"));
678        assert!(type_names.contains(&"User"));
679        assert!(type_names.contains(&"Post"));
680
681        Ok(())
682    }
683
684    #[test]
685    fn service_introspection_uses_raw_schema() -> anyhow::Result<()> {
686        let supergraph = include_str!("../../tests/data/schema.graphql");
687        let schema = FederatedSchema::parse_string(supergraph, "../../tests/data/schema.graphql")?;
688
689        let query = r#"
690            query {
691                _service {
692                    sdl
693                }
694            }
695        "#;
696
697        let doc = ExecutableDocument::parse_and_validate(&schema, query, "query.graphql").unwrap();
698        let cfg = ResponseGenerationConfig::default();
699        let result = generate_response(&cfg, None, &doc, &schema, &JsonMap::new())?;
700
701        assert!(result.get("data").is_some());
702        let data = result.get("data").unwrap();
703        assert!(data.get("_service").is_some());
704
705        let schema_obj = data.get("_service").unwrap();
706        assert!(schema_obj.get("sdl").is_some());
707
708        let sdl = schema_obj.get("sdl").unwrap().as_str().unwrap();
709        assert_eq!(supergraph, sdl);
710
711        Ok(())
712    }
713}