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 schema
47 .types
48 .iter()
49 .filter_map(|(name, ty)| match ty {
50 apollo_compiler::schema::ExtendedType::Object(node) => {
51 Some((name, &node.fields, false))
52 }
53 apollo_compiler::schema::ExtendedType::Interface(node) => {
54 Some((name, &node.fields, 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 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#[cfg_attr(test, derive(Debug))]
137pub(crate) struct ConnectDirectiveArguments {
138 pub(crate) position: ConnectorPosition,
139
140 pub(crate) source: Option<SourceName>,
144
145 pub(crate) http: Option<ConnectHTTPArguments>,
150
151 pub(crate) selection: JSONSelection,
156
157 pub(crate) connector_id: Option<Name>,
159
160 pub(crate) entity: bool,
166
167 pub(crate) batch: Option<ConnectBatchArguments>,
169
170 pub(crate) errors: Option<ErrorsArguments>,
172
173 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 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#[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 pub(crate) body: Option<JSONSelection>,
307
308 pub(crate) headers: Vec<Header>,
312
313 pub(crate) path: Option<JSONSelection>,
315 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#[derive(Clone, Copy, Debug)]
408pub struct ConnectBatchArguments {
409 pub max_size: Option<usize>,
413}
414
415type 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 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 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 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 let connects =
750 extract_connect_directive_arguments(schema.schema(), &name!(connect)).unwrap();
751 for connect in connects {
752 connect.is_success.unwrap();
754 }
755 }
756}