Skip to content

Commit 5471c73

Browse files
committed
Merge pull request #153 from facebook/ordering
[RFC] Ordering of queried fields
2 parents 0d325cc + d4b4e67 commit 5471c73

File tree

4 files changed

+140
-22
lines changed

4 files changed

+140
-22
lines changed

spec/Section 2 -- Language.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -779,6 +779,25 @@ curly-braces `{ }`. The values of an object literal may be any input value
779779
literal or variable (ex. `{ name: "Hello world", score: 1.0 }`). We refer to
780780
literal representation of input objects as "object literals."
781781

782+
**Input object fields are unordered**
783+
784+
Input object fields may be provided in any syntactic order and maintain
785+
identical semantic meaning.
786+
787+
These two queries are semantically identical:
788+
789+
```graphql
790+
{
791+
nearestThing(location: { lon: 12.43, lat: -53.211 })
792+
}
793+
```
794+
795+
```graphql
796+
{
797+
nearestThing(location: { lat: -53.211, lon: 12.43 })
798+
}
799+
```
800+
782801
**Semantics**
783802

784803
ObjectValue : { }

spec/Section 3 -- Type System.md

Lines changed: 101 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -234,9 +234,9 @@ While Scalar types describe the leaf values of these hierarchical queries, Objec
234234
describe the intermediate levels.
235235

236236
GraphQL Objects represent a list of named fields, each of which yield a value of
237-
a specific type. Object values are serialized as unordered maps, where the
237+
a specific type. Object values are serialized as ordered maps, where the
238238
queried field names (or aliases) are the keys and the result of evaluating
239-
the field is the value.
239+
the field is the value, ordered by the order in which they appear in the query.
240240

241241
For example, a type `Person` could be described as:
242242

@@ -253,9 +253,9 @@ that will yield an `Int` value, and `picture` a field that will yield a
253253
`Url` value.
254254

255255
A query of an object value must select at least one field. This selection of
256-
fields will yield an unordered map containing exactly the subset of the object
257-
queried. Only fields that are declared on the object type may validly be queried
258-
on that object.
256+
fields will yield an ordered map containing exactly the subset of the object
257+
queried, in the order in which they were queried. Only fields that are declared
258+
on the object type may validly be queried on that object.
259259

260260
For example, selecting all the fields of `Person`:
261261

@@ -281,17 +281,17 @@ While selecting a subset of fields:
281281

282282
```graphql
283283
{
284-
name
285284
age
285+
name
286286
}
287287
```
288288

289289
Must only yield exactly that subset:
290290

291291
```js
292292
{
293-
"name": "Mark Zuckerberg",
294-
"age": 30
293+
"age": 30,
294+
"name": "Mark Zuckerberg"
295295
}
296296
```
297297

@@ -342,6 +342,99 @@ And will yield the subset of each object type queried:
342342
}
343343
```
344344

345+
**Field Ordering**
346+
347+
When querying an Object, the resulting mapping of fields are conceptually
348+
ordered in the same order in which they were encountered during query execution,
349+
excluding fragments for which the type does not apply and fields or
350+
fragments that are skipped via `@skip` or `@include` directives. This ordering
351+
is correctly produced when using the {CollectFields()} algorithm.
352+
353+
Response formats which support ordered maps (such as JSON) must maintain this
354+
ordering. Response formats which do not support ordered maps may disregard
355+
this ordering.
356+
357+
If a fragment is spread before other fields, the fields that fragment specifies
358+
occur in the response before the following fields.
359+
360+
```graphql
361+
{
362+
foo
363+
...Frag
364+
qux
365+
}
366+
367+
fragment Frag on Query {
368+
bar
369+
baz
370+
}
371+
```
372+
373+
Produces the ordered result:
374+
375+
```js
376+
{
377+
"foo": 1,
378+
"bar": 2,
379+
"baz": 3,
380+
"qux": 4
381+
}
382+
```
383+
384+
If a field is queried multiple times in a selection, it is ordered by the first
385+
time it is encountered. However fragments for which the type does not apply does
386+
not affect ordering.
387+
388+
```graphql
389+
{
390+
foo
391+
...Ignored
392+
...Matching
393+
bar
394+
}
395+
396+
fragment Ignored on UnknownType {
397+
qux
398+
baz
399+
}
400+
401+
fragment Matching on Query {
402+
bar
403+
qux
404+
foo
405+
}
406+
```
407+
408+
Produces the ordered result:
409+
410+
```js
411+
{
412+
"foo": 1,
413+
"bar": 2,
414+
"qux": 3
415+
}
416+
```
417+
418+
Also, if directives result in fields being excluded, they are not considered in
419+
the ordering of fields.
420+
421+
```graphql
422+
{
423+
foo @skip(if: true)
424+
bar
425+
foo
426+
}
427+
```
428+
429+
Produces the ordered result:
430+
431+
```js
432+
{
433+
"bar": 1,
434+
"foo": 2
435+
}
436+
```
437+
345438
**Result Coercion**
346439

347440
Determining the result of coercing an object is the heart of the GraphQL

spec/Section 6 -- Execution.md

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -50,20 +50,20 @@ The selection set is converted to a grouped field set by calling
5050

5151
CollectFields(objectType, selectionSet, visitedFragments):
5252

53-
* Initialize {groupedFields} to an empty list of lists.
53+
* Initialize {groupedFields} to an empty ordered list of lists.
5454
* For each {selection} in {selectionSet};
5555
* If {selection} provides the directive `@skip`, let {skipDirective} be that directive.
5656
* If {skipDirective}'s {if} argument is {true}, continue with the
5757
next {selection} in {selectionSet}.
5858
* If {selection} provides the directive `@include`, let {includeDirective} be that directive.
5959
* If {includeDirective}'s {if} argument is {false}, continue with the
6060
next {selection} in {selectionSet}.
61-
* If {selection} is a Field:
61+
* If {selection} is a {Field}:
6262
* Let {responseKey} be the response key of {selection}.
6363
* Let {groupForResponseKey} be the list in {groupedFields} for
6464
{responseKey}; if no such list exists, create it as an empty list.
6565
* Append {selection} to the {groupForResponseKey}.
66-
* If {selection} is a FragmentSpread:
66+
* If {selection} is a {FragmentSpread}:
6767
* Let {fragmentSpreadName} be the name of {selection}.
6868
* If {fragmentSpreadName} is in {visitedFragments}, continue with the
6969
next {selection} in {selectionSet}.
@@ -76,20 +76,20 @@ CollectFields(objectType, selectionSet, visitedFragments):
7676
* If {doesFragmentTypeApply(objectType, fragmentType)} is false, continue
7777
with the next {selection} in {selectionSet}.
7878
* Let {fragmentSelectionSet} be the top-level selection set of {fragment}.
79-
* Let {fragmentGroupedFields} be the result of calling
80-
{CollectFields(objectType, fragmentSelectionSet)}.
81-
* For each {fragmentGroup} in {fragmentGroupedFields}:
79+
* Let {fragmentGroupedFieldSet} be the result of calling
80+
{CollectFields(objectType, fragmentSelectionSet, visitedFragments)}.
81+
* For each {fragmentGroup} in {fragmentGroupedFieldSet}:
8282
* Let {responseKey} be the response key shared by all fields in {fragmentGroup}
8383
* Let {groupForResponseKey} be the list in {groupedFields} for
8484
{responseKey}; if no such list exists, create it as an empty list.
8585
* Append all items in {fragmentGroup} to {groupForResponseKey}.
86-
* If {selection} is an inline fragment:
86+
* If {selection} is an {InlineFragment}:
8787
* Let {fragmentType} be the type condition on {selection}.
8888
* If {fragmentType} is not {null} and {doesFragmentTypeApply(objectType, fragmentType)} is false, continue
8989
with the next {selection} in {selectionSet}.
9090
* Let {fragmentSelectionSet} be the top-level selection set of {selection}.
91-
* Let {fragmentGroupedFields} be the result of calling {CollectFields(objectType, fragmentSelectionSet)}.
92-
* For each {fragmentGroup} in {fragmentGroupedFields}:
91+
* Let {fragmentGroupedFieldSet} be the result of calling {CollectFields(objectType, fragmentSelectionSet, visitedFragments)}.
92+
* For each {fragmentGroup} in {fragmentGroupedFieldSet}:
9393
* Let {responseKey} be the response key shared by all fields in {fragmentGroup}
9494
* Let {groupForResponseKey} be the list in {groupedFields} for
9595
{responseKey}; if no such list exists, create it as an empty list.
@@ -112,8 +112,10 @@ it should be evaluated normally.
112112

113113
## Evaluating a grouped field set
114114

115-
The result of evaluating a grouped field set will be an unordered map. There
116-
will be an entry in this map for every item in the grouped field set.
115+
The result of evaluating a grouped field set will be an ordered map. For each
116+
item in the grouped field set, an entry is added to the resulting ordered map,
117+
where the key is the response key shared by all fields for that entry, and the
118+
value is the result of evaluating those fields.
117119

118120
### Field entries
119121

spec/Section 7 -- Response.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ representations of the following four primitives:
2020
* String
2121
* Null
2222

23+
Serialization formats which only support an ordered map (such as JSON) must
24+
preserve ordering as it is defined by query execution. Serialization formats
25+
which only support an unordered map may omit this ordering information.
26+
2327
A serialization format may support the following primitives, however, strings
2428
may be used as a substitute for those primitives.
2529

@@ -52,13 +56,13 @@ the following JSON concepts:
5256

5357
A response to a GraphQL operation must be a map.
5458

55-
If the operation included execution, the response map must contain an entry
59+
If the operation included execution, the response map must contain a first entry
5660
with key `data`. The value of this entry is described in the "Data" section. If
5761
the operation failed before execution, due to a syntax error, missing
5862
information, or validation error, this entry must not be present.
5963

60-
If the operation encountered any errors, the response map must contain an entry
61-
with key `errors`. The value of this entry is described in the "Errors"
64+
If the operation encountered any errors, the response map must contain a next
65+
entry with key `errors`. The value of this entry is described in the "Errors"
6266
section. If the operation completed without encountering any errors, this entry
6367
must not be present.
6468

0 commit comments

Comments
 (0)