Skip to main content

apollo_federation/connectors/json_selection/
selection_set.rs

1//! Functions for applying a [`SelectionSet`] to a [`JSONSelection`]. This creates a new
2//! `JSONSelection` mapping to the fields on the selection set, and excluding parts of the
3//! original `JSONSelection` that are not needed by the selection set.
4
5#![cfg_attr(
6    not(test),
7    deny(
8        clippy::exit,
9        clippy::panic,
10        clippy::unwrap_used,
11        clippy::expect_used,
12        clippy::indexing_slicing,
13        clippy::unimplemented,
14        clippy::todo,
15        missing_docs
16    )
17)]
18
19use apollo_compiler::ExecutableDocument;
20use apollo_compiler::Node;
21use apollo_compiler::collections::IndexSet;
22use apollo_compiler::executable::Field;
23use apollo_compiler::executable::FieldSet;
24use apollo_compiler::executable::Selection;
25use apollo_compiler::executable::SelectionSet;
26use multimap::MultiMap;
27
28use super::lit_expr::LitExpr;
29use super::location::Ranged;
30use super::location::WithRange;
31use super::parser::PathList;
32use crate::connectors::JSONSelection;
33use crate::connectors::PathSelection;
34use crate::connectors::SubSelection;
35use crate::connectors::json_selection::Alias;
36use crate::connectors::json_selection::NamedSelection;
37use crate::connectors::json_selection::NamingPrefix;
38use crate::connectors::json_selection::TopLevelSelection;
39
40impl JSONSelection {
41    /// Apply a selection set to create a new [`JSONSelection`]
42    ///
43    /// Operations from the query planner will never contain key fields (because
44    /// it already has them from a previous fetch) but we might need those
45    /// fields for things like sorting entities in a batch. If the optional
46    /// `required_keys` is provided, we'll merge those fields into the selection
47    /// set before applying it to the JSONSelection.
48    pub fn apply_selection_set(
49        &self,
50        abstract_types: &IndexSet<String>,
51        document: &ExecutableDocument,
52        selection_set: &SelectionSet,
53        required_keys: Option<&FieldSet>,
54    ) -> Self {
55        let selection_set = required_keys.map_or_else(
56            || selection_set.clone(),
57            |keys| {
58                keys.selection_set.selections.iter().cloned().fold(
59                    selection_set.clone(),
60                    |mut acc, selection| {
61                        acc.push(selection);
62                        acc
63                    },
64                )
65            },
66        );
67
68        match &self.inner {
69            TopLevelSelection::Named(sub) => Self {
70                inner: TopLevelSelection::Named(sub.apply_selection_set(
71                    abstract_types,
72                    document,
73                    &selection_set,
74                )),
75                spec: self.spec,
76            },
77            TopLevelSelection::Value(lit) => {
78                // For a top-level PathSelection we can still refine its trailing
79                // SubSelection via the GraphQL selection set. For any other
80                // `LitExpr` shape (primitives, arrays, object literals,
81                // `LitPath` chains, operator chains), the expression may have
82                // computed structure that is not safe to prune against a
83                // GraphQL selection set, so we leave those values untouched.
84                let new_lit = match lit.as_ref() {
85                    LitExpr::Path(path) => {
86                        let refined =
87                            path.apply_selection_set(abstract_types, document, &selection_set);
88                        WithRange::new(LitExpr::Path(refined), lit.range())
89                    }
90                    _ => lit.clone(),
91                };
92                Self {
93                    inner: TopLevelSelection::Value(new_lit),
94                    spec: self.spec,
95                }
96            }
97        }
98    }
99}
100
101impl SubSelection {
102    /// Apply a selection set to create a new [`SubSelection`]
103    pub fn apply_selection_set(
104        &self,
105        abstract_types: &IndexSet<String>,
106        document: &ExecutableDocument,
107        selection_set: &SelectionSet,
108    ) -> Self {
109        let mut new_selections = Vec::new();
110        let field_map = map_fields_by_name(document, selection_set);
111
112        // When the operation contains __typename, it might be used to complete
113        // an entity reference (e.g. `__typename id`) for a subsequent fetch.
114        if field_map.contains_key("__typename")
115            // Only inject __typename for non-abstract types because output JSON
116            // for abstract types must provide a concrete __typename, so there's
117            // nothing we can confidently inject here.
118            && !abstract_types.contains(&selection_set.ty.to_string())
119        {
120            // Since `_Entity` is an abstract type (union), we should never see
121            // it here. For reasons I (Lenny) don't understand, persisted
122            // queries may contain `__typename` for `_entities` queries. We
123            // never want to emit `__typename: "_Entity"`, so we'll guard
124            // against that case.
125            debug_assert_ne!(selection_set.ty.to_string(), "_Entity");
126
127            new_selections.push(NamedSelection {
128                prefix: NamingPrefix::Alias(Alias::new("__typename")),
129                // Wrap the literal in `PathList::Expr` so the pretty-printed
130                // output is `__typename: $("User")`. The `$(...)` is harmless
131                // under v0.4 but required for the value to parse back as a
132                // `NamedSelection` under v0.3 and earlier, where bare string
133                // literals are not legal as named-selection values.
134                path: WithRange::new(
135                    LitExpr::Path(PathSelection {
136                        path: WithRange::new(
137                            PathList::Expr(
138                                WithRange::new(LitExpr::String(selection_set.ty.to_string()), None),
139                                WithRange::new(PathList::Empty, None),
140                            ),
141                            None,
142                        ),
143                    }),
144                    None,
145                ),
146            });
147        }
148
149        // Thin helper: apply a GraphQL selection set to a `NamedSelection`'s
150        // value, refining its inner `PathSelection` when present and leaving
151        // non-path `LitExpr` values untouched (a GraphQL selection set
152        // cannot meaningfully prune a literal or computed expression).
153        fn apply_to_named_value(
154            value: &WithRange<LitExpr>,
155            abstract_types: &IndexSet<String>,
156            document: &ExecutableDocument,
157            selection_set: &SelectionSet,
158        ) -> WithRange<LitExpr> {
159            match value.as_ref() {
160                LitExpr::Path(path) => {
161                    let refined = path.apply_selection_set(abstract_types, document, selection_set);
162                    WithRange::new(LitExpr::Path(refined), value.range())
163                }
164                _ => value.clone(),
165            }
166        }
167
168        for selection in &self.selections {
169            if let Some(single_key_for_selection) = selection.get_single_key() {
170                // In the single-Key case, we can filter out any selections
171                // whose single key does not match anything in the field_map.
172                if let Some(fields) = field_map.get_vec(single_key_for_selection.as_str()) {
173                    for field in fields {
174                        let applied_path = apply_to_named_value(
175                            &selection.path,
176                            abstract_types,
177                            document,
178                            &field.selection_set,
179                        );
180
181                        new_selections.push(NamedSelection {
182                            prefix: selection.prefix.clone(),
183                            path: applied_path,
184                        });
185                    }
186                } else {
187                    // If the selection had a single output key and that key
188                    // does not appear in field_map, we can skip the selection.
189                }
190            } else {
191                // If the NamedSelection::Path does not have a single output key
192                // (has no alias and is not a single field selection), then it's
193                // tricky to know if we should prune the selection, so we
194                // conservatively preserve it, using a transformed path.
195                new_selections.push(NamedSelection {
196                    prefix: selection.prefix.clone(),
197                    path: apply_to_named_value(
198                        &selection.path,
199                        abstract_types,
200                        document,
201                        selection_set,
202                    ),
203                });
204            }
205        }
206
207        Self {
208            selections: new_selections,
209            // Keep the old range even though it may be inaccurate after the
210            // removal of selections, since it still indicates where the
211            // original SubSelection came from.
212            range: self.range.clone(),
213        }
214    }
215}
216
217impl PathSelection {
218    /// Apply a selection set to create a new [`PathSelection`]
219    pub fn apply_selection_set(
220        &self,
221        abstract_types: &IndexSet<String>,
222        document: &ExecutableDocument,
223        selection_set: &SelectionSet,
224    ) -> Self {
225        Self {
226            path: WithRange::new(
227                self.path
228                    .apply_selection_set(abstract_types, document, selection_set),
229                self.path.range(),
230            ),
231        }
232    }
233}
234
235impl PathList {
236    pub(crate) fn apply_selection_set(
237        &self,
238        abstract_types: &IndexSet<String>,
239        document: &ExecutableDocument,
240        selection_set: &SelectionSet,
241    ) -> Self {
242        match self {
243            Self::Var(name, path) => Self::Var(
244                name.clone(),
245                WithRange::new(
246                    path.apply_selection_set(abstract_types, document, selection_set),
247                    path.range(),
248                ),
249            ),
250            Self::Key(key, path) => Self::Key(
251                key.clone(),
252                WithRange::new(
253                    path.apply_selection_set(abstract_types, document, selection_set),
254                    path.range(),
255                ),
256            ),
257            Self::Expr(expr, path) => Self::Expr(
258                expr.clone(),
259                WithRange::new(
260                    path.apply_selection_set(abstract_types, document, selection_set),
261                    path.range(),
262                ),
263            ),
264            Self::Method(method_name, args, path) => Self::Method(
265                method_name.clone(),
266                args.clone(),
267                WithRange::new(
268                    path.apply_selection_set(abstract_types, document, selection_set),
269                    path.range(),
270                ),
271            ),
272            Self::Question(tail) => Self::Question(WithRange::new(
273                tail.apply_selection_set(abstract_types, document, selection_set),
274                tail.range(),
275            )),
276            Self::Selection(sub) => {
277                Self::Selection(sub.apply_selection_set(abstract_types, document, selection_set))
278            }
279            Self::Empty => Self::Empty,
280        }
281    }
282}
283
284fn map_fields_by_name<'a>(
285    document: &'a ExecutableDocument,
286    set: &'a SelectionSet,
287) -> MultiMap<String, &'a Node<Field>> {
288    let mut map = MultiMap::new();
289    map_fields_by_name_impl(document, set, &mut map);
290    map
291}
292
293fn map_fields_by_name_impl<'a>(
294    document: &'a ExecutableDocument,
295    set: &'a SelectionSet,
296    map: &mut MultiMap<String, &'a Node<Field>>,
297) {
298    for selection in &set.selections {
299        match selection {
300            Selection::Field(field) => {
301                map.insert(field.name.to_string(), field);
302            }
303            Selection::FragmentSpread(f) => {
304                if let Some(fragment) = f.fragment_def(document) {
305                    map_fields_by_name_impl(document, &fragment.selection_set, map);
306                }
307            }
308            Selection::InlineFragment(fragment) => {
309                map_fields_by_name_impl(document, &fragment.selection_set, map);
310            }
311        }
312    }
313}
314
315#[cfg(test)]
316mod tests {
317    use apollo_compiler::ExecutableDocument;
318    use apollo_compiler::Schema;
319    use apollo_compiler::collections::IndexSet;
320    use apollo_compiler::executable::FieldSet;
321    use apollo_compiler::executable::SelectionSet;
322    use apollo_compiler::name;
323    use apollo_compiler::validation::Valid;
324    use pretty_assertions::assert_eq;
325
326    use crate::assert_snapshot;
327
328    fn selection_set(schema: &Valid<Schema>, s: &str) -> (ExecutableDocument, SelectionSet) {
329        let document = ExecutableDocument::parse_and_validate(schema, s, "./").unwrap();
330        let selection_set = document
331            .operations
332            .anonymous
333            .as_ref()
334            .unwrap()
335            .selection_set
336            .fields()
337            .next()
338            .unwrap()
339            .selection_set
340            .clone();
341        (document.into_inner(), selection_set)
342    }
343
344    #[test]
345    fn test() {
346        let json = super::JSONSelection::parse(
347            r###"
348        $.result {
349          a
350          b: c
351          d: e.f
352          g
353          h: 'i-j'
354          k: { l m: n }
355        }
356        "###,
357        )
358        .unwrap();
359
360        let schema = Schema::parse_and_validate(
361            r###"
362            type Query {
363                t: T
364            }
365
366            type T {
367                a: String
368                b: String
369                d: String
370                g: String
371                h: String
372                k: K
373            }
374
375            type K {
376              l: String
377              m: String
378            }
379            "###,
380            "./",
381        )
382        .unwrap();
383
384        let (document, selection_set) = selection_set(
385            &schema,
386            "{ t { z: a, y: b, x: d, w: h v: k { u: l t: m } } }",
387        );
388
389        let transformed =
390            json.apply_selection_set(&IndexSet::default(), &document, &selection_set, None);
391        assert_eq!(
392            transformed.to_string(),
393            r###"$.result {
394  a
395  b: c
396  d: e.f
397  h: "i-j"
398  k: {
399    l
400    m: n
401  }
402}"###
403        );
404    }
405
406    #[test]
407    fn test_star() {
408        let json_selection = super::JSONSelection::parse(
409            r###"
410        $.result {
411          a
412          b_alias: b
413          c {
414            d
415            e_alias: e
416            h: "h"
417            i: "i"
418            group: {
419              j
420              k
421            }
422          }
423          path_to_f: c.f
424        }
425        "###,
426        )
427        .unwrap();
428
429        let schema = Schema::parse_and_validate(
430            r###"
431            type Query {
432                t: T
433            }
434
435            type T {
436                a: String
437                b_alias: String
438                c: C
439                path_to_f: String
440            }
441
442            type C {
443                d: String
444                e_alias: String
445                h: String
446                i: String
447                group: Group
448            }
449
450            type Group {
451                j: String
452                k: String
453            }
454            "###,
455            "./",
456        )
457        .unwrap();
458
459        let (document, selection_set) = selection_set(
460            &schema,
461            "{ t { a b_alias c { e: e_alias h group { j } } path_to_f } }",
462        );
463
464        let transformed = json_selection.apply_selection_set(
465            &IndexSet::default(),
466            &document,
467            &selection_set,
468            None,
469        );
470        assert_eq!(
471            transformed.to_string(),
472            r###"$.result {
473  a
474  b_alias: b
475  c {
476    e_alias: e
477    h: "h"
478    group: {
479      j
480    }
481  }
482  path_to_f: c.f
483}"###
484        );
485
486        let data = serde_json_bytes::json!({
487            "result": {
488                "a": "a",
489                "b": "b",
490                "c": {
491                  "d": "d",
492                  "e": "e",
493                  "f": "f",
494                  "g": "g",
495                  "h": "h",
496                  "i": "i",
497                  "j": "j",
498                  "k": "k",
499                },
500            }
501        });
502        let result = transformed.apply_to(&data);
503        assert_eq!(
504            result,
505            (
506                Some(serde_json_bytes::json!(
507                {
508                    "a": "a",
509                    "b_alias": "b",
510                    "c": {
511                        "e_alias": "e",
512                        "h": "h",
513                        "group": {
514                          "j": "j"
515                        },
516                    },
517                    "path_to_f": "f",
518                })),
519                vec![]
520            )
521        );
522    }
523
524    #[test]
525    fn test_depth() {
526        let json = super::JSONSelection::parse(
527            r###"
528        $.result {
529          a {
530            b {
531              renamed: c
532            }
533          }
534        }
535        "###,
536        )
537        .unwrap();
538
539        let schema = Schema::parse_and_validate(
540            r###"
541            type Query {
542                t: T
543            }
544
545            type T {
546              a: A
547            }
548
549            type A {
550              b: B
551            }
552
553            type B {
554              renamed: String
555            }
556            "###,
557            "./",
558        )
559        .unwrap();
560
561        let (document, selection_set) = selection_set(&schema, "{ t { a { b { renamed } } } }");
562
563        let transformed =
564            json.apply_selection_set(&IndexSet::default(), &document, &selection_set, None);
565        assert_eq!(
566            transformed.to_string(),
567            r###"$.result {
568  a {
569    b {
570      renamed: c
571    }
572  }
573}"###
574        );
575
576        let data = serde_json_bytes::json!({
577            "result": {
578              "a": {
579                "b": {
580                  "c": "c",
581                }
582              }
583            }
584          }
585        );
586        let result = transformed.apply_to(&data);
587        assert_eq!(
588            result,
589            (
590                Some(serde_json_bytes::json!({"a": { "b": { "renamed": "c" } } } )),
591                vec![]
592            )
593        );
594    }
595
596    #[test]
597    fn test_typename() {
598        let json = super::JSONSelection::parse(
599            r###"
600            $.result {
601              id
602              author: {
603                id: authorId
604              }
605            }
606            "###,
607        )
608        .unwrap();
609
610        let schema = Schema::parse_and_validate(
611            r###"
612        type Query {
613            t: T
614        }
615
616        type T {
617            id: ID
618            author: A
619        }
620
621        type A {
622            id: ID
623        }
624        "###,
625            "./",
626        )
627        .unwrap();
628
629        let (document, selection_set) =
630            selection_set(&schema, "{ t { id __typename author { __typename id } } }");
631
632        let transformed =
633            json.apply_selection_set(&IndexSet::default(), &document, &selection_set, None);
634        assert_eq!(
635            transformed.to_string(),
636            r###"$.result {
637  __typename: $("T")
638  id
639  author: {
640    __typename: $("A")
641    id: authorId
642  }
643}"###
644        );
645    }
646
647    #[test]
648    fn test_fragments() {
649        let json = super::JSONSelection::parse(
650            r###"
651            reviews: result {
652                id
653                product: { upc: product_upc }
654                author: { id: author_id }
655            }
656            "###,
657        )
658        .unwrap();
659
660        let schema = Schema::parse_and_validate(
661            r###"
662        type Query {
663            _entities(representations: [_Any!]!): [_Entity]
664        }
665
666        scalar _Any
667
668        union _Entity = Product
669
670        type Product {
671            upc: String
672            reviews: [Review]
673        }
674
675        type Review {
676            id: ID
677            product: Product
678            author: User
679        }
680
681        type User {
682            id: ID
683        }
684        "###,
685            "./",
686        )
687        .unwrap();
688
689        let (document, selection_set) = selection_set(
690            &schema,
691            "query ($representations: [_Any!]!) {
692                _entities(representations: $representations) {
693                    ..._generated_onProduct1_0
694                }
695            }
696            fragment _generated_onProduct1_0 on Product {
697                reviews {
698                    id
699                    product {
700                        __typename
701                        upc
702                    }
703                    author {
704                        __typename
705                        id
706                    }
707                }
708            }",
709        );
710
711        let transformed =
712            json.apply_selection_set(&IndexSet::default(), &document, &selection_set, None);
713        assert_eq!(
714            transformed.to_string(),
715            r###"reviews: result {
716  id
717  product: {
718    __typename: $("Product")
719    upc: product_upc
720  }
721  author: {
722    __typename: $("User")
723    id: author_id
724  }
725}"###
726        );
727    }
728
729    #[test]
730    fn test_ensuring_key_fields() {
731        let json = super::JSONSelection::parse(
732            r###"
733            id
734            store { id }
735            name
736            price
737            "###,
738        )
739        .unwrap();
740
741        let schema = Schema::parse_and_validate(
742            r###"
743        type Query {
744            product: Product
745        }
746
747        type Product {
748            id: ID!
749            store: Store!
750            name: String!
751            price: String
752        }
753
754        type Store {
755          id: ID!
756        }
757        "###,
758            "./",
759        )
760        .unwrap();
761
762        let (document, selection_set) = selection_set(&schema, "{ product { name price } }");
763
764        let keys =
765            FieldSet::parse_and_validate(&schema, name!(Product), "id store { id }", "").unwrap();
766
767        let transformed =
768            json.apply_selection_set(&IndexSet::default(), &document, &selection_set, Some(&keys));
769        assert_snapshot!(transformed.to_string(), @r"
770        id
771        store {
772          id
773        }
774        name
775        price
776        ");
777    }
778}