|
| 1 | ++++ |
| 2 | +title = "GraphQL Deep Dive - Part 1: merged fields" |
| 3 | +author = "Andreas Marek" |
| 4 | +tags = [] |
| 5 | +categories = [] |
| 6 | +date = 2019-01-22T00:00:00+10:00 |
| 7 | ++++ |
| 8 | + |
| 9 | +# GraphQL Deep Dive series |
| 10 | + |
| 11 | +Welcome to the new series "GraphQL deep dive" where we will explore advanced or unknown GraphQL topics. The plan is to discuss things mostly in a language and implementation neutral way, even if it is hosted on graphql-java.com. |
| 12 | + |
| 13 | +# Merged Fields |
| 14 | + |
| 15 | +First thing we are looking at is "merged fields". |
| 16 | + |
| 17 | +GraphQL allows for a field to be declared multiple times in a query as long as it can be merged. |
| 18 | + |
| 19 | +Valid GraphQL queries are: |
| 20 | + |
| 21 | +{{< highlight Scala "linenos=table" >}} |
| 22 | +{ |
| 23 | + foo |
| 24 | + foo |
| 25 | +} |
| 26 | +{{< / highlight >}} |
| 27 | + |
| 28 | +<p/> |
| 29 | + |
| 30 | +{{< highlight Scala "linenos=table" >}} |
| 31 | +{ |
| 32 | + foo(id: "123") |
| 33 | + foo(id: "123") |
| 34 | + foo(id: "123") |
| 35 | +} |
| 36 | +{{< / highlight >}} |
| 37 | +<p/> |
| 38 | + |
| 39 | +{{< highlight Scala "linenos=table" >}} |
| 40 | +{ |
| 41 | + foo(id: "123") { |
| 42 | + id |
| 43 | + } |
| 44 | + foo(id: "123") { |
| 45 | + name |
| 46 | + } |
| 47 | + foo(id: "123") { |
| 48 | + id |
| 49 | + name |
| 50 | + } |
| 51 | +} |
| 52 | +{{< / highlight >}} |
| 53 | + |
| 54 | +Each of these queries will result in a result with just one "foo" key, not two or three. |
| 55 | + |
| 56 | +Invalid Queries are: |
| 57 | + |
| 58 | +{{< highlight Scala "linenos=table" >}} |
| 59 | +{ |
| 60 | + foo |
| 61 | + foo(id: "123") |
| 62 | +} |
| 63 | +{{< / highlight >}} |
| 64 | +<p/> |
| 65 | +{{< highlight Scala "linenos=table" >}} |
| 66 | +{ |
| 67 | + foo(id: "123") |
| 68 | + foo(id: "456", id2: "123") |
| 69 | +} |
| 70 | +{{< / highlight >}} |
| 71 | +<p/> |
| 72 | +{{< highlight Scala "linenos=table" >}} |
| 73 | +{ |
| 74 | + foo(id: "123") |
| 75 | + foo: foo2 |
| 76 | +} |
| 77 | +{{< / highlight >}} |
| 78 | + |
| 79 | +The reason why they are not valid, is because the fields are different: in the first two examples the arguments differ and the third query actually has two different fields under the same key. |
| 80 | + |
| 81 | +# Motivation |
| 82 | + |
| 83 | +The examples so far don't seem really useful, but it all makes sense when you add fragments: |
| 84 | + |
| 85 | +{{< highlight Scala "linenos=table" >}} |
| 86 | +{ |
| 87 | + ...myFragment1 |
| 88 | + ...myFragment2 |
| 89 | +} |
| 90 | + |
| 91 | +fragment myFragment1 on Query { |
| 92 | + foo(id: "123") { |
| 93 | + name |
| 94 | + } |
| 95 | +} |
| 96 | +fragment myFragment2 on Query { |
| 97 | + foo(id: "123") { |
| 98 | + url |
| 99 | + } |
| 100 | +} |
| 101 | + |
| 102 | +{{< / highlight >}} |
| 103 | + |
| 104 | +<p/> |
| 105 | +Fragments are designed to be written by different parties (for example different components in a UI) which should not know anything about each other. Requiring that every field can only be declared once would make this objective unfeasible. |
| 106 | + |
| 107 | +But by allowing fields merging, as long as the fields are the same, allows fragments to be authored in an independent way from each other. |
| 108 | + |
| 109 | + |
| 110 | +# Rules when fields can be merged |
| 111 | + |
| 112 | +The specific details when fields can be merged are written down in [Field Selection Merging](https://facebook.github.io/graphql/draft/#sec-Field-Selection-Merging) in the spec. |
| 113 | + |
| 114 | +The rules are what you would expect in general and they basically say that fields must be the same. The following examples are taken from the spec and they are all valid: |
| 115 | + |
| 116 | +{{< highlight Scala "linenos=table" >}} |
| 117 | +fragment mergeIdenticalFields on Dog { |
| 118 | + name |
| 119 | + name |
| 120 | +} |
| 121 | +fragment mergeIdenticalAliasesAndFields on Dog { |
| 122 | + otherName: name |
| 123 | + otherName: name |
| 124 | +} |
| 125 | +fragment mergeIdenticalFieldsWithIdenticalArgs on Dog { |
| 126 | + doesKnowCommand(dogCommand: SIT) |
| 127 | + doesKnowCommand(dogCommand: SIT) |
| 128 | +} |
| 129 | +fragment mergeIdenticalFieldsWithIdenticalValues on Dog { |
| 130 | + doesKnowCommand(dogCommand: $dogCommand) |
| 131 | + doesKnowCommand(dogCommand: $dogCommand) |
| 132 | +} |
| 133 | +{{< / highlight >}} |
| 134 | + |
| 135 | +The most complex case happens when you have fields in fragments on different types: |
| 136 | + |
| 137 | +{{< highlight Scala "linenos=table" >}} |
| 138 | +fragment safeDifferingFields on Pet { |
| 139 | + ... on Dog { |
| 140 | + volume: barkVolume |
| 141 | + } |
| 142 | + ... on Cat { |
| 143 | + volume: meowVolume |
| 144 | + } |
| 145 | +} |
| 146 | +{{< / highlight >}} |
| 147 | + |
| 148 | +This is normally invalid because `volume` is an alias for two different fields `barkVolume` and `meowVolume` but because only one of the some are actually resolved and they both return a value of the same type (we assume here that `barkVolume` and `meowVolume` are both of the same type) it is valid. |
| 149 | + |
| 150 | +{{< highlight Scala "linenos=table" >}} |
| 151 | +fragment safeDifferingArgs on Pet { |
| 152 | + ... on Dog { |
| 153 | + doesKnowCommand(dogCommand: SIT) |
| 154 | + } |
| 155 | + ... on Cat { |
| 156 | + doesKnowCommand(catCommand: JUMP) |
| 157 | + } |
| 158 | +} |
| 159 | +{{< / highlight >}} |
| 160 | + |
| 161 | +This is again a valid case because even if the first `doesKnowCommand` has a different argument than the second `doesKnowCommand` only one of them is actually resolved. |
| 162 | + |
| 163 | +In the next example `someValue` has different types (we assume that `nickname` is a `String` and `meowVolume` is a `Int`) and therefore the query is not valid: |
| 164 | + |
| 165 | +{{< highlight Scala "linenos=table" >}} |
| 166 | +fragment conflictingDifferingResponses on Pet { |
| 167 | + ... on Dog { |
| 168 | + someValue: nickname |
| 169 | + } |
| 170 | + ... on Cat { |
| 171 | + someValue: meowVolume |
| 172 | + } |
| 173 | +} |
| 174 | +{{< / highlight >}} |
| 175 | + |
| 176 | +# Sub selections and directives |
| 177 | + |
| 178 | +One thing to keep in my mind is that the sub selections of fields are merged together. For example here `foo` is resolved once and than `id` and `name` is resolved. |
| 179 | + |
| 180 | +{{< highlight Scala "linenos=table" >}} |
| 181 | +{ |
| 182 | + foo(id: "123") { |
| 183 | + id |
| 184 | + } |
| 185 | + foo(id: "123") { |
| 186 | + name |
| 187 | + } |
| 188 | +} |
| 189 | +{{< / highlight >}} |
| 190 | + |
| 191 | +This query is the same as: |
| 192 | + |
| 193 | +{{< highlight Scala "linenos=table" >}} |
| 194 | +{ |
| 195 | + foo(id: "123") { |
| 196 | + id |
| 197 | + name |
| 198 | + } |
| 199 | +} |
| 200 | +{{< / highlight >}} |
| 201 | + |
| 202 | +The second thing to keep in mind is that different directives can be on each field: |
| 203 | + |
| 204 | +{{< highlight Scala "linenos=table" >}} |
| 205 | +{ |
| 206 | + foo(id: "123") @myDirective { |
| 207 | + id |
| 208 | + } |
| 209 | + foo(id: "123") @myOtherDirective { |
| 210 | + name |
| 211 | + } |
| 212 | +} |
| 213 | +{{< / highlight >}} |
| 214 | + |
| 215 | +So if you want to know all directives for the current field you are resolving you actually need to look at all of the merged fields from the query. |
| 216 | + |
| 217 | +# Merged fields in graphql-js and GraphQl Java |
| 218 | + |
| 219 | +In graphql-js merged fields are relevant when you implement a resolver and you need access to the specific ast field of the query. The `info` objects has a property `fieldNodes` which gives you access to all ast fields which are merged together. |
| 220 | + |
| 221 | +In GraphQL Java depending on the version you are running you have `List<Field> getFields()` in the `DataFetcherEnvironment` or for GraphQL Java newer than `12.0` you have also `MergedField getMergedField()` which is the recommend way to access all merged fields. |
| 222 | + |
0 commit comments