Skip to main content

apollo_federation/connectors/spec/
connect.rs

1use apollo_compiler::Name;
2use apollo_compiler::Node;
3use apollo_compiler::Schema;
4use apollo_compiler::ast::Directive;
5use apollo_compiler::ast::Value;
6use apollo_compiler::name;
7use itertools::Itertools;
8
9use super::errors::ERRORS_ARGUMENT_NAME;
10use super::errors::ErrorsArguments;
11use super::http::HTTP_ARGUMENT_NAME;
12use super::http::PATH_ARGUMENT_NAME;
13use super::http::QUERY_PARAMS_ARGUMENT_NAME;
14use crate::connectors::ConnectSpec;
15use crate::connectors::ConnectorPosition;
16use crate::connectors::ObjectFieldDefinitionPosition;
17use crate::connectors::OriginatingDirective;
18use crate::connectors::SourceName;
19use crate::connectors::id::ObjectTypeDefinitionDirectivePosition;
20use crate::connectors::json_selection::JSONSelection;
21use crate::connectors::models::Header;
22use crate::connectors::spec::connect_spec_from_schema;
23use crate::error::FederationError;
24use crate::schema::position::InterfaceFieldDefinitionPosition;
25use crate::schema::position::ObjectOrInterfaceFieldDefinitionPosition;
26use crate::schema::position::ObjectOrInterfaceFieldDirectivePosition;
27
28pub(crate) const CONNECT_DIRECTIVE_NAME_IN_SPEC: Name = name!("connect");
29pub(crate) const CONNECT_SOURCE_ARGUMENT_NAME: Name = name!("source");
30pub(crate) const CONNECT_SELECTION_ARGUMENT_NAME: Name = name!("selection");
31pub(crate) const CONNECT_ENTITY_ARGUMENT_NAME: Name = name!("entity");
32pub(crate) const CONNECT_ID_ARGUMENT_NAME: Name = name!("id");
33pub(crate) const CONNECT_HTTP_NAME_IN_SPEC: Name = name!("ConnectHTTP");
34pub(crate) const CONNECT_BATCH_NAME_IN_SPEC: Name = name!("ConnectBatch");
35pub(crate) const CONNECT_BODY_ARGUMENT_NAME: Name = name!("body");
36pub(crate) const BATCH_ARGUMENT_NAME: Name = name!("batch");
37pub(crate) const IS_SUCCESS_ARGUMENT_NAME: Name = name!("isSuccess");
38pub(super) const DEFAULT_CONNECT_SPEC: ConnectSpec = ConnectSpec::V0_3;
39
40pub(crate) fn extract_connect_directive_arguments(
41    schema: &Schema,
42    name: &Name,
43) -> Result<Vec<ConnectDirectiveArguments>, FederationError> {
44    // connect on fields
45    schema
46        .types
47        .iter()
48        .filter_map(|(name, ty)| match ty {
49            apollo_compiler::schema::ExtendedType::Object(node) => {
50                Some((name, &node.fields, /* is_interface */ false))
51            }
52            apollo_compiler::schema::ExtendedType::Interface(node) => {
53                Some((name, &node.fields, /* is_interface */ true))
54            }
55            _ => None,
56        })
57        .flat_map(|(type_name, fields, is_interface)| {
58            fields.iter().flat_map(move |(field_name, field_def)| {
59                field_def
60                    .directives
61                    .iter()
62                    .filter(|directive| directive.name == *name)
63                    .enumerate()
64                    .map(move |(i, directive)| {
65                        let field_pos = if is_interface {
66                            ObjectOrInterfaceFieldDefinitionPosition::Interface(
67                                InterfaceFieldDefinitionPosition {
68                                    type_name: type_name.clone(),
69                                    field_name: field_name.clone(),
70                                },
71                            )
72                        } else {
73                            ObjectOrInterfaceFieldDefinitionPosition::Object(
74                                ObjectFieldDefinitionPosition {
75                                    type_name: type_name.clone(),
76                                    field_name: field_name.clone(),
77                                },
78                            )
79                        };
80
81                        let position =
82                            ConnectorPosition::Field(ObjectOrInterfaceFieldDirectivePosition {
83                                field: field_pos,
84                                directive_name: directive.name.clone(),
85                                directive_index: i,
86                            });
87
88                        let connect_spec =
89                            connect_spec_from_schema(schema).unwrap_or(DEFAULT_CONNECT_SPEC);
90
91                        ConnectDirectiveArguments::from_position_and_directive(
92                            position,
93                            directive,
94                            connect_spec,
95                        )
96                    })
97            })
98        })
99        .chain(
100            // connect on types
101            schema
102                .types
103                .iter()
104                .filter_map(|(_, ty)| ty.as_object())
105                .flat_map(|ty| {
106                    ty.directives
107                        .iter()
108                        .filter(|directive| directive.name == *name)
109                        .enumerate()
110                        .map(move |(i, directive)| {
111                            let position =
112                                ConnectorPosition::Type(ObjectTypeDefinitionDirectivePosition {
113                                    type_name: ty.name.clone(),
114                                    directive_name: directive.name.clone(),
115                                    directive_index: i,
116                                });
117
118                            let connect_spec =
119                                connect_spec_from_schema(schema).unwrap_or(DEFAULT_CONNECT_SPEC);
120
121                            ConnectDirectiveArguments::from_position_and_directive(
122                                position,
123                                directive,
124                                connect_spec,
125                            )
126                        })
127                }),
128        )
129        .collect()
130}
131
132/// Arguments to the `@connect` directive
133///
134/// Refer to [ConnectSpecDefinition] for more info.
135#[cfg_attr(test, derive(Debug))]
136pub(crate) struct ConnectDirectiveArguments {
137    pub(crate) position: ConnectorPosition,
138
139    /// The upstream source for shared connector configuration.
140    ///
141    /// Must match the `name` argument of a @source directive in this schema.
142    pub(crate) source: Option<SourceName>,
143
144    /// HTTP options for this connector
145    ///
146    /// Marked as optional in the GraphQL schema to allow for future transports,
147    /// but is currently required.
148    pub(crate) http: Option<ConnectHTTPArguments>,
149
150    /// Fields to extract from the upstream JSON response.
151    ///
152    /// Uses the JSONSelection syntax to define a mapping of connector response to
153    /// GraphQL schema.
154    pub(crate) selection: JSONSelection,
155
156    /// Custom connector ID name
157    pub(crate) connector_id: Option<Name>,
158
159    /// Entity resolver marker
160    ///
161    /// Marks this connector as a canonical resolver for an entity (uniquely
162    /// identified domain model.) If true, the connector must be defined on a field
163    /// of the Query type.
164    pub(crate) entity: bool,
165
166    /// Settings for the connector when it is doing a $batch entity resolver
167    pub(crate) batch: Option<ConnectBatchArguments>,
168
169    /// Configure the error mapping functionality for this connect
170    pub(crate) errors: Option<ErrorsArguments>,
171
172    /// Criteria to use to determine if a request is a success.
173    ///
174    /// Uses the JSONSelection to define a success criteria. This JSON Selection
175    /// _must_ resolve to a boolean value.
176    pub(crate) is_success: Option<JSONSelection>,
177}
178
179impl ConnectDirectiveArguments {
180    fn from_position_and_directive(
181        position: ConnectorPosition,
182        value: &Node<Directive>,
183        connect_spec: ConnectSpec,
184    ) -> Result<Self, FederationError> {
185        let args = &value.arguments;
186        let directive_name = &value.name;
187
188        // We'll have to iterate over the arg list and keep the properties by their name
189        let source = SourceName::from_connect(value);
190        let mut http = None;
191        let mut selection = None;
192        let mut entity = None;
193        let mut connector_id = None;
194        let mut batch = None;
195        let mut errors = None;
196        let mut is_success = None;
197        for arg in args {
198            let arg_name = arg.name.as_str();
199
200            if arg_name == HTTP_ARGUMENT_NAME.as_str() {
201                let http_value = arg.value.as_object().ok_or_else(|| {
202                    FederationError::internal(format!(
203                        "`http` field in `@{directive_name}` directive is not an object"
204                    ))
205                })?;
206
207                http = Some(ConnectHTTPArguments::try_from((
208                    http_value,
209                    directive_name,
210                    connect_spec,
211                ))?);
212            } else if arg_name == BATCH_ARGUMENT_NAME.as_str() {
213                let http_value = arg.value.as_object().ok_or_else(|| {
214                    FederationError::internal(format!(
215                        "`http` field in `@{directive_name}` directive is not an object"
216                    ))
217                })?;
218
219                batch = Some(ConnectBatchArguments::try_from((
220                    http_value,
221                    directive_name,
222                ))?);
223            } else if arg_name == ERRORS_ARGUMENT_NAME.as_str() {
224                let http_value = arg.value.as_object().ok_or_else(|| {
225                    FederationError::internal(format!(
226                        "`errors` field in `@{directive_name}` directive is not an object"
227                    ))
228                })?;
229
230                let errors_value =
231                    ErrorsArguments::try_from((http_value, directive_name, connect_spec))?;
232
233                errors = Some(errors_value);
234            } else if arg_name == CONNECT_SELECTION_ARGUMENT_NAME.as_str() {
235                let selection_value = arg.value.as_str().ok_or_else(|| {
236                    FederationError::internal(format!(
237                        "`selection` field in `@{directive_name}` directive is not a string"
238                    ))
239                })?;
240                selection = Some(
241                    JSONSelection::parse_with_spec(selection_value, connect_spec)
242                        .map_err(|e| FederationError::internal(e.message))?,
243                );
244            } else if arg_name == CONNECT_ID_ARGUMENT_NAME.as_str() {
245                let id = arg.value.as_str().ok_or_else(|| {
246                    FederationError::internal(format!(
247                        "`id` field in `@{directive_name}` directive is not a string"
248                    ))
249                })?;
250
251                connector_id = Some(Name::new(id)?);
252            } else if arg_name == CONNECT_ENTITY_ARGUMENT_NAME.as_str() {
253                let entity_value = arg.value.to_bool().ok_or_else(|| {
254                    FederationError::internal(format!(
255                        "`entity` field in `@{directive_name}` directive is not a boolean"
256                    ))
257                })?;
258
259                entity = Some(entity_value);
260            } else if arg_name == IS_SUCCESS_ARGUMENT_NAME.as_str() {
261                let selection_value = arg.value.as_str().ok_or_else(|| {
262                    FederationError::internal(format!(
263                        "`is_success` field in `@{directive_name}` directive is not a string"
264                    ))
265                })?;
266                is_success = Some(
267                    JSONSelection::parse_with_spec(selection_value, connect_spec)
268                        .map_err(|e| FederationError::internal(e.message))?,
269                );
270            }
271        }
272
273        Ok(Self {
274            position,
275            source,
276            http,
277            connector_id,
278            selection: selection.ok_or_else(|| {
279                FederationError::internal(format!(
280                    "`@{directive_name}` directive is missing a selection"
281                ))
282            })?,
283            entity: entity.unwrap_or_default(),
284            batch,
285            errors,
286            is_success,
287        })
288    }
289}
290
291/// The HTTP arguments needed for a connect request
292#[cfg_attr(test, derive(Debug))]
293pub struct ConnectHTTPArguments {
294    pub(crate) get: Option<String>,
295    pub(crate) post: Option<String>,
296    pub(crate) patch: Option<String>,
297    pub(crate) put: Option<String>,
298    pub(crate) delete: Option<String>,
299
300    /// Request body
301    ///
302    /// Define a request body using JSONSelection. Selections can include values from
303    /// field arguments using `$args.argName` and from fields on the parent type using
304    /// `$this.fieldName`.
305    pub(crate) body: Option<JSONSelection>,
306
307    /// Configuration for headers to attach to the request.
308    ///
309    /// Overrides headers from the associated @source by name.
310    pub(crate) headers: Vec<Header>,
311
312    /// A [`JSONSelection`] that should resolve to an array of strings to append to the path.
313    pub(crate) path: Option<JSONSelection>,
314    /// A [`JSONSelection`] that should resolve to an object to convert to query params.
315    pub(crate) query_params: Option<JSONSelection>,
316}
317
318impl TryFrom<(&ObjectNode, &Name, ConnectSpec)> for ConnectHTTPArguments {
319    type Error = FederationError;
320
321    fn try_from(
322        (values, directive_name, connect_spec): (&ObjectNode, &Name, ConnectSpec),
323    ) -> Result<Self, FederationError> {
324        let mut get = None;
325        let mut post = None;
326        let mut patch = None;
327        let mut put = None;
328        let mut delete = None;
329        let mut body = None;
330        let headers: Vec<Header> =
331            Header::from_http_arg(values, OriginatingDirective::Connect, connect_spec)
332                .into_iter()
333                .try_collect()
334                .map_err(|err| FederationError::internal(err.to_string()))?;
335        let mut path = None;
336        let mut query_params = None;
337        for (name, value) in values {
338            let name = name.as_str();
339
340            if name == CONNECT_BODY_ARGUMENT_NAME.as_str() {
341                let body_value = value.as_str().ok_or_else(|| {
342                    FederationError::internal(format!("`body` field in `@{directive_name}` directive's `http` field is not a string"))
343                })?;
344                body = Some(
345                    JSONSelection::parse_with_spec(body_value, connect_spec)
346                        .map_err(|e| FederationError::internal(e.message))?,
347                );
348            } else if name == "GET" {
349                get = Some(value.as_str().ok_or_else(|| FederationError::internal(format!(
350                    "supplied HTTP template URL in `@{directive_name}` directive's `http` field is not a string"
351                )))?.to_string());
352            } else if name == "POST" {
353                post = Some(value.as_str().ok_or_else(|| FederationError::internal(format!(
354                    "supplied HTTP template URL in `@{directive_name}` directive's `http` field is not a string"
355                )))?.to_string());
356            } else if name == "PATCH" {
357                patch = Some(value.as_str().ok_or_else(|| FederationError::internal(format!(
358                    "supplied HTTP template URL in `@{directive_name}` directive's `http` field is not a string"
359                )))?.to_string());
360            } else if name == "PUT" {
361                put = Some(value.as_str().ok_or_else(|| FederationError::internal(format!(
362                    "supplied HTTP template URL in `@{directive_name}` directive's `http` field is not a string"
363                )))?.to_string());
364            } else if name == "DELETE" {
365                delete = Some(value.as_str().ok_or_else(|| FederationError::internal(format!(
366                    "supplied HTTP template URL in `@{directive_name}` directive's `http` field is not a string"
367                )))?.to_string());
368            } else if name == PATH_ARGUMENT_NAME.as_str() {
369                let value = value.as_str().ok_or_else(|| {
370                    FederationError::internal(format!(
371                        "`{PATH_ARGUMENT_NAME}` field in `@{directive_name}` directive's `http` field is not a string"
372                    ))
373                })?;
374                path = Some(
375                    JSONSelection::parse_with_spec(value, connect_spec)
376                        .map_err(|e| FederationError::internal(e.message))?,
377                );
378            } else if name == QUERY_PARAMS_ARGUMENT_NAME.as_str() {
379                let value = value.as_str().ok_or_else(|| {
380                    FederationError::internal(format!(
381                        "`{QUERY_PARAMS_ARGUMENT_NAME}` field in `@{directive_name}` directive's `http` field is not a string"
382                    ))
383                })?;
384                query_params = Some(
385                    JSONSelection::parse_with_spec(value, connect_spec)
386                        .map_err(|e| FederationError::internal(e.message))?,
387                );
388            }
389        }
390
391        Ok(Self {
392            get,
393            post,
394            patch,
395            put,
396            delete,
397            body,
398            headers,
399            path,
400            query_params,
401        })
402    }
403}
404
405/// Settings for the connector when it is doing a $batch entity resolver
406#[derive(Clone, Copy, Debug)]
407pub struct ConnectBatchArguments {
408    /// Set a maximum number of requests to be batched together.
409    ///
410    /// Over this maximum, will be split into multiple batch requests of `max_size`.
411    pub max_size: Option<usize>,
412}
413
414/// Internal representation of the object type pairs
415type ObjectNode = [(Name, Node<Value>)];
416
417impl TryFrom<(&ObjectNode, &Name)> for ConnectBatchArguments {
418    type Error = FederationError;
419
420    fn try_from((values, directive_name): (&ObjectNode, &Name)) -> Result<Self, FederationError> {
421        let mut max_size = None;
422        for (name, value) in values {
423            let name = name.as_str();
424
425            if name == "maxSize" {
426                let max_size_int = Some(value.to_i32().ok_or_else(|| FederationError::internal(format!(
427                    "supplied 'max_size' field in `@{directive_name}` directive's `batch` field is not a positive integer"
428                )))?);
429                // Convert the int to a usize since it is used for chunking an array later.
430                // Much better to fail here than during the request lifecycle.
431                max_size = max_size_int.map(|i| usize::try_from(i).map_err(|_| FederationError::internal(format!(
432                    "supplied 'max_size' field in `@{directive_name}` directive's `batch` field is not a positive integer"
433                )))).transpose()?;
434            }
435        }
436
437        Ok(Self { max_size })
438    }
439}
440
441#[cfg(test)]
442mod tests {
443    use apollo_compiler::Schema;
444    use apollo_compiler::name;
445
446    use super::*;
447    use crate::ValidFederationSubgraphs;
448    use crate::schema::FederationSchema;
449    use crate::supergraph::extract_subgraphs_from_supergraph;
450
451    static SIMPLE_SUPERGRAPH: &str = include_str!("../tests/schemas/simple.graphql");
452    static IS_SUCCESS_SUPERGRAPH: &str = include_str!("../tests/schemas/is-success.graphql");
453
454    fn get_subgraphs(supergraph_sdl: &str) -> ValidFederationSubgraphs {
455        let schema = Schema::parse(supergraph_sdl, "supergraph.graphql").unwrap();
456        let supergraph_schema = FederationSchema::new(schema).unwrap();
457        extract_subgraphs_from_supergraph(&supergraph_schema, Some(true)).unwrap()
458    }
459
460    #[test]
461    fn test_expected_connect_spec_latest() {
462        // We probably want to update DEFAULT_CONNECT_SPEC when
463        // ConnectSpec::latest() changes, but we don't want it to happen
464        // automatically, so this test failure should serve as a signal to
465        // consider updating.
466        assert_eq!(DEFAULT_CONNECT_SPEC, ConnectSpec::latest());
467    }
468
469    #[test]
470    fn it_parses_at_connect() {
471        let subgraphs = get_subgraphs(SIMPLE_SUPERGRAPH);
472        let subgraph = subgraphs.get("connectors").unwrap();
473        let schema = &subgraph.schema;
474
475        let actual_definition = schema
476            .get_directive_definition(&CONNECT_DIRECTIVE_NAME_IN_SPEC)
477            .unwrap()
478            .get(schema.schema())
479            .unwrap();
480
481        insta::assert_snapshot!(
482            actual_definition.to_string(),
483            @"directive @connect(source: String, id: String, http: connect__ConnectHTTP, batch: connect__ConnectBatch, errors: connect__ConnectorErrors, selection: connect__JSONSelection!, entity: Boolean = false, isSuccess: connect__JSONSelection) repeatable on FIELD_DEFINITION | OBJECT"
484        );
485
486        let fields = schema
487            .referencers()
488            .get_directive(CONNECT_DIRECTIVE_NAME_IN_SPEC.as_str())
489            .object_fields
490            .iter()
491            .map(|f| f.get(schema.schema()).unwrap().to_string())
492            .collect::<Vec<_>>()
493            .join("\n");
494
495        insta::assert_snapshot!(
496            fields,
497            @r###"
498                users: [User] @connect(source: "json", http: {GET: "/users"}, selection: "id name")
499                posts: [Post] @connect(source: "json", http: {GET: "/posts"}, selection: "id title body")
500            "###
501        );
502    }
503
504    #[test]
505    fn it_extracts_at_connect() {
506        let subgraphs = get_subgraphs(SIMPLE_SUPERGRAPH);
507        let subgraph = subgraphs.get("connectors").unwrap();
508        let schema = &subgraph.schema;
509
510        // Extract the connects from the schema definition and map them to their `Connect` equivalent
511        let connects = extract_connect_directive_arguments(schema.schema(), &name!(connect));
512
513        insta::assert_debug_snapshot!(
514            connects.unwrap(),
515            @r#"
516        [
517            ConnectDirectiveArguments {
518                position: Field(
519                    ObjectOrInterfaceFieldDirectivePosition {
520                        field: Object(Query.users),
521                        directive_name: "connect",
522                        directive_index: 0,
523                    },
524                ),
525                source: Some(
526                    "json",
527                ),
528                http: Some(
529                    ConnectHTTPArguments {
530                        get: Some(
531                            "/users",
532                        ),
533                        post: None,
534                        patch: None,
535                        put: None,
536                        delete: None,
537                        body: None,
538                        headers: [],
539                        path: None,
540                        query_params: None,
541                    },
542                ),
543                selection: JSONSelection {
544                    inner: Named(
545                        SubSelection {
546                            selections: [
547                                NamedSelection {
548                                    prefix: None,
549                                    path: WithRange {
550                                        node: Path(
551                                            PathSelection {
552                                                path: WithRange {
553                                                    node: Key(
554                                                        WithRange {
555                                                            node: Field(
556                                                                "id",
557                                                            ),
558                                                            range: Some(
559                                                                0..2,
560                                                            ),
561                                                        },
562                                                        WithRange {
563                                                            node: Empty,
564                                                            range: Some(
565                                                                2..2,
566                                                            ),
567                                                        },
568                                                    ),
569                                                    range: Some(
570                                                        0..2,
571                                                    ),
572                                                },
573                                            },
574                                        ),
575                                        range: Some(
576                                            0..2,
577                                        ),
578                                    },
579                                },
580                                NamedSelection {
581                                    prefix: None,
582                                    path: WithRange {
583                                        node: Path(
584                                            PathSelection {
585                                                path: WithRange {
586                                                    node: Key(
587                                                        WithRange {
588                                                            node: Field(
589                                                                "name",
590                                                            ),
591                                                            range: Some(
592                                                                3..7,
593                                                            ),
594                                                        },
595                                                        WithRange {
596                                                            node: Empty,
597                                                            range: Some(
598                                                                7..7,
599                                                            ),
600                                                        },
601                                                    ),
602                                                    range: Some(
603                                                        3..7,
604                                                    ),
605                                                },
606                                            },
607                                        ),
608                                        range: Some(
609                                            3..7,
610                                        ),
611                                    },
612                                },
613                            ],
614                            range: Some(
615                                0..7,
616                            ),
617                        },
618                    ),
619                    spec: V0_1,
620                },
621                connector_id: None,
622                entity: false,
623                batch: None,
624                errors: None,
625                is_success: None,
626            },
627            ConnectDirectiveArguments {
628                position: Field(
629                    ObjectOrInterfaceFieldDirectivePosition {
630                        field: Object(Query.posts),
631                        directive_name: "connect",
632                        directive_index: 0,
633                    },
634                ),
635                source: Some(
636                    "json",
637                ),
638                http: Some(
639                    ConnectHTTPArguments {
640                        get: Some(
641                            "/posts",
642                        ),
643                        post: None,
644                        patch: None,
645                        put: None,
646                        delete: None,
647                        body: None,
648                        headers: [],
649                        path: None,
650                        query_params: None,
651                    },
652                ),
653                selection: JSONSelection {
654                    inner: Named(
655                        SubSelection {
656                            selections: [
657                                NamedSelection {
658                                    prefix: None,
659                                    path: WithRange {
660                                        node: Path(
661                                            PathSelection {
662                                                path: WithRange {
663                                                    node: Key(
664                                                        WithRange {
665                                                            node: Field(
666                                                                "id",
667                                                            ),
668                                                            range: Some(
669                                                                0..2,
670                                                            ),
671                                                        },
672                                                        WithRange {
673                                                            node: Empty,
674                                                            range: Some(
675                                                                2..2,
676                                                            ),
677                                                        },
678                                                    ),
679                                                    range: Some(
680                                                        0..2,
681                                                    ),
682                                                },
683                                            },
684                                        ),
685                                        range: Some(
686                                            0..2,
687                                        ),
688                                    },
689                                },
690                                NamedSelection {
691                                    prefix: None,
692                                    path: WithRange {
693                                        node: Path(
694                                            PathSelection {
695                                                path: WithRange {
696                                                    node: Key(
697                                                        WithRange {
698                                                            node: Field(
699                                                                "title",
700                                                            ),
701                                                            range: Some(
702                                                                3..8,
703                                                            ),
704                                                        },
705                                                        WithRange {
706                                                            node: Empty,
707                                                            range: Some(
708                                                                8..8,
709                                                            ),
710                                                        },
711                                                    ),
712                                                    range: Some(
713                                                        3..8,
714                                                    ),
715                                                },
716                                            },
717                                        ),
718                                        range: Some(
719                                            3..8,
720                                        ),
721                                    },
722                                },
723                                NamedSelection {
724                                    prefix: None,
725                                    path: WithRange {
726                                        node: Path(
727                                            PathSelection {
728                                                path: WithRange {
729                                                    node: Key(
730                                                        WithRange {
731                                                            node: Field(
732                                                                "body",
733                                                            ),
734                                                            range: Some(
735                                                                9..13,
736                                                            ),
737                                                        },
738                                                        WithRange {
739                                                            node: Empty,
740                                                            range: Some(
741                                                                13..13,
742                                                            ),
743                                                        },
744                                                    ),
745                                                    range: Some(
746                                                        9..13,
747                                                    ),
748                                                },
749                                            },
750                                        ),
751                                        range: Some(
752                                            9..13,
753                                        ),
754                                    },
755                                },
756                            ],
757                            range: Some(
758                                0..13,
759                            ),
760                        },
761                    ),
762                    spec: V0_1,
763                },
764                connector_id: None,
765                entity: false,
766                batch: None,
767                errors: None,
768                is_success: None,
769            },
770        ]
771        "#
772        );
773    }
774
775    #[test]
776    fn it_supports_is_success_in_connect() {
777        let subgraphs = get_subgraphs(IS_SUCCESS_SUPERGRAPH);
778        let subgraph = subgraphs.get("connectors").unwrap();
779        let schema = &subgraph.schema;
780
781        // Extract the connects from the schema definition and map them to their `Connect` equivalent
782        let connects =
783            extract_connect_directive_arguments(schema.schema(), &name!(connect)).unwrap();
784        for connect in connects {
785            // Unwrap and fail if is_success doesn't exist on all as expected.
786            connect.is_success.unwrap();
787        }
788    }
789}