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            .unwrap()
491            .object_fields
492            .iter()
493            .map(|f| f.get(schema.schema()).unwrap().to_string())
494            .collect::<Vec<_>>()
495            .join("\n");
496
497        insta::assert_snapshot!(
498            fields,
499            @r###"
500                users: [User] @connect(source: "json", http: {GET: "/users"}, selection: "id name")
501                posts: [Post] @connect(source: "json", http: {GET: "/posts"}, selection: "id title body")
502            "###
503        );
504    }
505
506    #[test]
507    fn it_extracts_at_connect() {
508        let subgraphs = get_subgraphs(SIMPLE_SUPERGRAPH);
509        let subgraph = subgraphs.get("connectors").unwrap();
510        let schema = &subgraph.schema;
511
512        // Extract the connects from the schema definition and map them to their `Connect` equivalent
513        let connects = extract_connect_directive_arguments(schema.schema(), &name!(connect));
514
515        insta::assert_debug_snapshot!(
516            connects.unwrap(),
517            @r###"
518        [
519            ConnectDirectiveArguments {
520                position: Field(
521                    ObjectOrInterfaceFieldDirectivePosition {
522                        field: Object(Query.users),
523                        directive_name: "connect",
524                        directive_index: 0,
525                    },
526                ),
527                source: Some(
528                    "json",
529                ),
530                http: Some(
531                    ConnectHTTPArguments {
532                        get: Some(
533                            "/users",
534                        ),
535                        post: None,
536                        patch: None,
537                        put: None,
538                        delete: None,
539                        body: None,
540                        headers: [],
541                        path: None,
542                        query_params: None,
543                    },
544                ),
545                selection: JSONSelection {
546                    inner: Named(
547                        SubSelection {
548                            selections: [
549                                NamedSelection {
550                                    prefix: None,
551                                    path: 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                                NamedSelection {
576                                    prefix: None,
577                                    path: PathSelection {
578                                        path: WithRange {
579                                            node: Key(
580                                                WithRange {
581                                                    node: Field(
582                                                        "name",
583                                                    ),
584                                                    range: Some(
585                                                        3..7,
586                                                    ),
587                                                },
588                                                WithRange {
589                                                    node: Empty,
590                                                    range: Some(
591                                                        7..7,
592                                                    ),
593                                                },
594                                            ),
595                                            range: Some(
596                                                3..7,
597                                            ),
598                                        },
599                                    },
600                                },
601                            ],
602                            range: Some(
603                                0..7,
604                            ),
605                        },
606                    ),
607                    spec: V0_1,
608                },
609                connector_id: None,
610                entity: false,
611                batch: None,
612                errors: None,
613                is_success: None,
614            },
615            ConnectDirectiveArguments {
616                position: Field(
617                    ObjectOrInterfaceFieldDirectivePosition {
618                        field: Object(Query.posts),
619                        directive_name: "connect",
620                        directive_index: 0,
621                    },
622                ),
623                source: Some(
624                    "json",
625                ),
626                http: Some(
627                    ConnectHTTPArguments {
628                        get: Some(
629                            "/posts",
630                        ),
631                        post: None,
632                        patch: None,
633                        put: None,
634                        delete: None,
635                        body: None,
636                        headers: [],
637                        path: None,
638                        query_params: None,
639                    },
640                ),
641                selection: JSONSelection {
642                    inner: Named(
643                        SubSelection {
644                            selections: [
645                                NamedSelection {
646                                    prefix: None,
647                                    path: PathSelection {
648                                        path: WithRange {
649                                            node: Key(
650                                                WithRange {
651                                                    node: Field(
652                                                        "id",
653                                                    ),
654                                                    range: Some(
655                                                        0..2,
656                                                    ),
657                                                },
658                                                WithRange {
659                                                    node: Empty,
660                                                    range: Some(
661                                                        2..2,
662                                                    ),
663                                                },
664                                            ),
665                                            range: Some(
666                                                0..2,
667                                            ),
668                                        },
669                                    },
670                                },
671                                NamedSelection {
672                                    prefix: None,
673                                    path: PathSelection {
674                                        path: WithRange {
675                                            node: Key(
676                                                WithRange {
677                                                    node: Field(
678                                                        "title",
679                                                    ),
680                                                    range: Some(
681                                                        3..8,
682                                                    ),
683                                                },
684                                                WithRange {
685                                                    node: Empty,
686                                                    range: Some(
687                                                        8..8,
688                                                    ),
689                                                },
690                                            ),
691                                            range: Some(
692                                                3..8,
693                                            ),
694                                        },
695                                    },
696                                },
697                                NamedSelection {
698                                    prefix: None,
699                                    path: PathSelection {
700                                        path: WithRange {
701                                            node: Key(
702                                                WithRange {
703                                                    node: Field(
704                                                        "body",
705                                                    ),
706                                                    range: Some(
707                                                        9..13,
708                                                    ),
709                                                },
710                                                WithRange {
711                                                    node: Empty,
712                                                    range: Some(
713                                                        13..13,
714                                                    ),
715                                                },
716                                            ),
717                                            range: Some(
718                                                9..13,
719                                            ),
720                                        },
721                                    },
722                                },
723                            ],
724                            range: Some(
725                                0..13,
726                            ),
727                        },
728                    ),
729                    spec: V0_1,
730                },
731                connector_id: None,
732                entity: false,
733                batch: None,
734                errors: None,
735                is_success: None,
736            },
737        ]
738        "###
739        );
740    }
741
742    #[test]
743    fn it_supports_is_success_in_connect() {
744        let subgraphs = get_subgraphs(IS_SUCCESS_SUPERGRAPH);
745        let subgraph = subgraphs.get("connectors").unwrap();
746        let schema = &subgraph.schema;
747
748        // Extract the connects from the schema definition and map them to their `Connect` equivalent
749        let connects =
750            extract_connect_directive_arguments(schema.schema(), &name!(connect)).unwrap();
751        for connect in connects {
752            // Unwrap and fail if is_success doesn't exist on all as expected.
753            connect.is_success.unwrap();
754        }
755    }
756}