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