-
Notifications
You must be signed in to change notification settings - Fork 1.5k
/
Copy pathoutputnode_graphql.go
1285 lines (1183 loc) · 50.7 KB
/
outputnode_graphql.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
* Copyright 2020 Dgraph Labs, Inc. and Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package query
import (
"bytes"
"context"
"encoding/json"
"fmt"
"reflect"
"strconv"
"sync"
gqlSchema "github.com/dgraph-io/dgraph/graphql/schema"
"github.com/dgraph-io/dgraph/x"
)
// graphQLEncodingCtx is used to encode JSON for GraphQl queries.
type graphQLEncodingCtx struct {
// buf is the buffer which stores the encoded GraphQL response.
buf *bytes.Buffer
// gqlErrs stores GraphQL errors, if any. Even if there are GraphQL errors,
// the buffer will always have a valid JSON response.
gqlErrs x.GqlErrorList
// dgraphTypeAttrId stores the id for attr `dgraph.type`.
// dgraph.type predicate is requested by GraphQL layer for abstract types. We are caching
// the id for this attr here so that instead of doing this:
// enc.attrForID(enc.getAttr(child)) == "dgraph.type"
// we would now be able to do just this:
// enc.getAttr(child) == dgraphTypeAttrId
// Meaning, instead of looking up an int in a map and then comparing strings,
// we can just compare ints directly.
dgraphTypeAttrId uint16
// errChan is used to process the errors resulting from custom field resolution.
// It simply appends all those errors to gqlErrs.
errChan chan x.GqlErrorList
// customFieldResultChan is used to process the fastJson tree updates resulting from custom
// field resolution
customFieldResultChan chan customFieldResult
}
// customFieldResult represents the fastJson tree updates for custom fields.
type customFieldResult struct {
// parents are all the parents which have the same resolved value for the custom childField
parents []fastJsonNode
// childField is the custom field which has been resolved
childField gqlSchema.Field
// childVal is the result of resolving the custom childField from remote HTTP endpoint.
// A child node is attached to all the parents with this value.
childVal []byte
}
// cantCoerceScalar tells whether a scalar value can be coerced to its corresponding GraphQL scalar.
func cantCoerceScalar(val []byte, field gqlSchema.Field) bool {
switch field.Type().Name() {
case "Int":
// Although GraphQL layer would have input coercion for Int,
// we still need to do this as there can be cases like schema migration when Int64 was
// changed to Int, or if someone was using DQL mutations but GraphQL queries. The GraphQL
// layer must always honor the spec.
// valToBytes() uses []byte(strconv.FormatInt(num, 10)) to convert int values to byte slice.
// so, we should do the reverse, parse the string back to int and check that it fits in the
// range of int32.
// TODO: think if we can do this check in a better way without parsing the bytes.
if _, err := strconv.ParseInt(string(val), 10, 32); err != nil {
return true
}
case "String", "ID", "Boolean", "Int64", "Float", "DateTime":
// do nothing, as for these types the GraphQL schema is same as the dgraph schema.
// Hence, the value coming in from fastJson node should already be in the correct form.
// So, no need to coerce it.
default:
enumValues := field.EnumValues()
// At this point we should only get fields which are of ENUM type, so we can return
// an error if we don't get any enum values.
if len(enumValues) == 0 {
return true
}
// Lets check that the enum value is valid.
if !x.HasString(enumValues, toString(val)) {
return true
}
}
return false
}
// toString converts the json encoded string value val to a go string.
// It should be used only in scenarios where the underlying string is simple, i.e., it doesn't
// contain any escape sequence or any other string magic. Otherwise, better to use json.Unmarshal().
func toString(val []byte) string {
strVal := string(val)
strVal = strVal[1 : len(strVal)-1] // remove `"` from beginning and end
return strVal
}
// encode creates a JSON encoded GraphQL response.
func (gqlCtx *graphQLEncodingCtx) encode(enc *encoder, fj fastJsonNode, fjIsRoot bool,
childSelectionSet []gqlSchema.Field, parentField gqlSchema.Field,
parentPath []interface{}) bool {
child := enc.children(fj)
// This is a scalar value for DQL.
if child == nil {
val, err := enc.getScalarVal(fj)
if err != nil {
gqlCtx.gqlErrs = append(gqlCtx.gqlErrs, parentField.GqlErrorf(parentPath, err.Error()))
// return false so that the caller can appropriately handle null writing.
return false
}
if val == nil {
// val being nil here can only be the case for a top-level query and not for a nested
// field. val being nil indicates that the top-level query has no value to resolve
// to, and we need to write null/[]/raise an error depending on the return type of the
// query. Now, for queries which return a list (whether nullable or not), [] would
// anyways be written by the parent encode() call. If we return false from here,
// then too the parent encode() call will write [], but then we won't be able to
// distinguish between whether the first item of the list was null or the whole query
// had no results.
// So, for lists lets return true.
// We will return false for single valued cases so that the caller can correctly write
// null or raise an error.
// Note that we don't need to add any errors to the gqlErrs here.
return parentField.Type().ListType() != nil
}
// here we have a valid value, lets write it to buffer appropriately.
if parentField.Type().IsGeo() {
var geoVal map[string]interface{}
x.Check(json.Unmarshal(val, &geoVal)) // this unmarshal can't error
if err := completeGeoObject(parentPath, parentField, geoVal, gqlCtx.buf); err != nil {
gqlCtx.gqlErrs = append(gqlCtx.gqlErrs, err)
return false
}
} else {
// we got a GraphQL scalar
// check coercion rules to see if it matches the GraphQL spec requirements.
if cantCoerceScalar(val, parentField) {
gqlCtx.gqlErrs = append(gqlCtx.gqlErrs, parentField.GqlErrorf(parentPath,
"Error coercing value '%s' for field '%s' to type %s.",
string(val), parentField.Name(), parentField.Type().Name()))
// if it can't be coerced, return false so that the caller can appropriately
// handle null writing
return false
}
x.Check2(gqlCtx.buf.Write(val))
}
// we have successfully written the value, lets return true to indicate that this
// call to encode() was successful.
return true
}
// if we are here, ensure that GraphQL was expecting an object, otherwise return error.
if len(childSelectionSet) == 0 {
gqlCtx.gqlErrs = append(gqlCtx.gqlErrs, parentField.GqlErrorf(parentPath,
gqlSchema.ErrExpectedScalar))
// return false so that the caller can appropriately handle null writing.
return false
}
// If the parent field had any immediate @custom(http: {...}) children, then we need to
// find the custom fastJson nodes which should be used for encoding those custom fields.
// The custom fastJson nodes will always be at the start of the list.
var customNodes map[uint16]fastJsonNode
if enc.getCustom(child) {
// allocate memory for the map only when there are custom nodes
customNodes = make(map[uint16]fastJsonNode)
for ; child != nil && enc.getCustom(child); child = child.next {
customNodes[enc.getAttr(child)] = child
}
}
// if GraphQL layer requested dgraph.type predicate, then it would always be the first child in
// the response as it is always written first in DQL query. So, if we get data for dgraph.type
// predicate then just save it in dgraphTypes slice, no need to write it to JSON yet.
child, dgraphTypes := gqlCtx.extractDgraphTypes(enc, child)
// This is an internal node. Write the opening { for the JSON object
x.Check2(gqlCtx.buf.WriteRune('{'))
cnt := 0 // used to figure out how many times continuously we have seen the current attr
i := 0 // used to iterate over childSelectionSet
keyEndPos := 0 // used to store the length of output buffer at which a JSON key ends to
// correctly write value as null, if need be.
nullWritten := false // indicates whether null has been written as value for the current
// selection or not. Used to figure out whether to write the closing ] for JSON arrays.
seenField := make(map[string]bool) // seenField map keeps track of fields which have been seen
// as part of interface to avoid double entry in the resulting response
var curSelection gqlSchema.Field // used to store the current selection in the childSelectionSet
var curSelectionIsDgList bool // indicates whether the curSelection is list stored in Dgraph
var cur, next fastJsonNode // used to iterate over data in fastJson nodes
// We need to keep iterating only if:
// 1. There is data to be processed for the current level. AND,
// 2. There are unprocessed fields in the childSelectionSet
// These are the respective examples to consider for each case:
// 1. Sometimes GraphQL layer requests `dgraph.uid: uid` in the rewritten DQL query as the last
// field at every level. This is not part of GraphQL selection set at any level,
// but just exists as the last field in the DQL query resulting as the last fastJsonNode
// child, and hence we need to ignore it so as not to put it in the user facing JSON.
// This is case 1 where we have data to be processed but no child left in the selection set
// to use it. The condition `i < len(childSelectionSet)` comes handy in this case.
// 2. It may happen that a field requested in a GraphQL query, may not have any data for some
// nodes in the result. If such a field is the last field in the selection set or there is a
// set of such fields which are at the end of selection set, then we need to write null
// values for them with appropriate errors. This is case 2 where there are unprocessed fields
// but no data for them. This is handled after this for loop.
for child != nil && i < len(childSelectionSet) {
cnt++
nullWritten = false // reset it at every iteration
curSelection = childSelectionSet[i]
cur = child
next = cur.next
// Step-1: Skip the field OR Write JSON key and opening [ for JSON arrays
if cnt == 1 {
// we need to check if the field should be skipped only when it is encountered for
// the first time
if curSelection.SkipField(dgraphTypes, seenField) {
cnt = 0 // Reset the count,
// indicating that we need to write the JSON key in next iteration.
i++
// if this is the last field and shouldn't be included,
// then need to remove comma from the buffer if one was present.
if i == len(childSelectionSet) {
checkAndStripComma(gqlCtx.buf)
}
// also if there was any data for this field, need to skip that. There may not be
// data in case this field was added from a fragment on another type.
attrId := enc.idForAttr(curSelection.DgraphAlias())
if enc.getAttr(cur) == attrId {
for next != nil && enc.getAttr(next) == attrId {
next = next.next
}
child = next
}
continue
}
// Write JSON key and opening [ for JSON arrays
curSelection.CompleteAlias(gqlCtx.buf)
keyEndPos = gqlCtx.buf.Len()
curSelectionIsDgList = (curSelection.Type().ListType() != nil) && !curSelection.
IsCustomHTTP()
if curSelectionIsDgList {
x.Check2(gqlCtx.buf.WriteRune('['))
}
}
// Step-2: Write JSON value
if curSelection.Name() == gqlSchema.Typename {
// If the current selection is __typename then we find out the typename using the
// dgraphTypes slice saved earlier.
x.Check2(gqlCtx.buf.Write(getTypename(curSelection, dgraphTypes)))
// We don't need to iterate to next fastJson node in this case,
// as the current node will have data for the next field in the selection set.
} else if curSelection.IsCustomHTTP() {
// if the current field had @custom(http: {...}), then need to write it using
// the customNodes mapping stored earlier.
if !gqlCtx.writeCustomField(enc, curSelection, customNodes, parentPath) {
// if custom field wasn't written successfully, need to write null
if nullWritten = writeGraphQLNull(curSelection, gqlCtx.buf,
keyEndPos); !nullWritten {
gqlCtx.gqlErrs = append(gqlCtx.gqlErrs, curSelection.GqlErrorf(append(
parentPath, curSelection.ResponseName()), gqlSchema.ErrExpectedNonNull,
curSelection.Name(), curSelection.Type()))
return false
}
}
// We don't need to iterate to next fastJson node in this case,
// as the current node will have data for the next field in the selection set.
} else if curSelection.DgraphAlias() != enc.attrForID(enc.getAttr(cur)) {
// if the current fastJson node doesn't hold data for the current GraphQL selection,
// then there can be two cases:
// 1. The current fastJson node holds data for a next selection and there was no data
// present for the current GraphQL selection, so need to write null for the current
// GraphQL selection with appropriate errors.
// 2. The current fastJson node holds data for count(pred), the current GraphQL
// selection is an aggregate field at child level and there was no data present for
// it. So, need to write null for the children of current GraphQL selection but also
// need to skip all the count(pred) fastJson nodes which were requested from within
// the current GraphQL selection.
// 3. The current fastJson node holds data which wasn't requested by any GraphQL
// selection, but instead by a DQL selection added by GraphQL layer; and the data
// for current selection may be present in an upcoming fastJson node.
// Point to note is that this case doesn't happen as the GraphQL layer adds such
// DQL selections only at the beginning (dgraph.type) or end (dgraph.uid: uid) of a
// DQL selection set, but not in middle. The beginning case we have already handled,
// and the end case would either be ignored by this for loop or handled as case 1.
// So, we don't have a need to handle case 3, and need to always write null with
// appropriate errors.
// TODO: once @custom(dql: "") is fixed, check if case 3 can happen for it with the new
// way of rewriting.
if !fjIsRoot && curSelection.IsAggregateField() {
// handles null writing for case 2
child = gqlCtx.completeAggregateChildren(enc, cur, curSelection,
append(parentPath, curSelection.ResponseName()), true)
} else {
// handles null writing for case 1
if nullWritten = writeGraphQLNull(curSelection, gqlCtx.buf,
keyEndPos); !nullWritten {
gqlCtx.gqlErrs = append(gqlCtx.gqlErrs, curSelection.GqlErrorf(append(
parentPath, curSelection.ResponseName()), gqlSchema.ErrExpectedNonNull,
curSelection.Name(), curSelection.Type()))
return false
}
// we don't need to iterate to next fastJson node here.
}
} else {
// This is the case where the current fastJson node holds data for the current
// GraphQL selection. There are following possible sub-cases:
// 1. current GraphQL selection == list type
// current fastJson node == list type
// => Both GraphQL and DQL schema are in list form, recursively encode it.
// 2. current GraphQL selection == list type
// current fastJson node != list type
// => There is a mismatch between the GraphQL and DQL schema. Raise a field error.
// 3. current GraphQL selection != list type
// current fastJson node == list type
// => There is a mismatch between the GraphQL and DQL schema. Raise a field error.
// 4. current GraphQL selection != list type
// current fastJson node != list type
// => Both GraphQL and DQL schema are in non-list form, recursively encode it.
// Apart from these there is a special case of aggregate queries/fields where:
// current GraphQL selection != list type
// current fastJson node == list type
// => This is not a mismatch between the GraphQL and DQL schema and should be
// handled appropriately.
if curSelectionIsDgList && enc.getList(cur) {
// handles case 1
itemPos := gqlCtx.buf.Len()
// List items which are scalars will never have null as a value returned
// from Dgraph, but there can be coercion errors due to which their encoding
// may return false and we will need to write null as a value for them.
// Similarly, List items which are objects will also not have null as a
// value returned from Dgraph, but there can be a nested non-nullable field
// which may trigger the object to turn out to be null.
if !gqlCtx.encode(enc, cur, false, curSelection.SelectionSet(), curSelection,
append(parentPath, curSelection.ResponseName(), cnt-1)) {
// Unlike the choice in curSelection.NullValue(), where we turn missing
// list fields into [], the spec explicitly calls out:
// "If a List type wraps a Non-Null type, and one of the
// elements of that list resolves to null, then the entire list
// must resolve to null."
//
// The list gets reduced to null, but an error recording that must
// already be in errs. See
// https://graphql.github.io/graphql-spec/June2018/#sec-Errors-and-Non-Nullability
// "If the field returns null because of an error which has already
// been added to the "errors" list in the response, the "errors"
// list must not be further affected."
// The behavior is also in the examples in here:
// https://graphql.github.io/graphql-spec/June2018/#sec-Errors
typ := curSelection.Type()
if typ.ListType().Nullable() {
gqlCtx.buf.Truncate(itemPos)
x.Check2(gqlCtx.buf.Write(gqlSchema.JsonNull))
} else if typ.Nullable() {
gqlCtx.buf.Truncate(keyEndPos)
x.Check2(gqlCtx.buf.Write(gqlSchema.JsonNull))
// set nullWritten to true so we don't write closing ] for this list
nullWritten = true
// skip all data for the current list selection
attrId := enc.idForAttr(curSelection.DgraphAlias())
for next != nil && enc.getAttr(next) == attrId {
cur = next
next = next.next
}
// just set the child to point to the data for last item in the list and not
// the data for next field in the selection set as child would anyways be
// moved forward later.
child = cur
} else {
// this is the case of [T!]!, where we can't write null either for a
// list item or the list itself. So, mark the encoding as failed,
// and let the parent handle null writing.
return false
}
}
// we need to iterate to the next fastJson node because we have used the data from
// the current fastJson node.
child = child.next
} else if !curSelectionIsDgList && (!enc.getList(cur) || (fjIsRoot && (next == nil || enc.
getAttr(cur) != enc.getAttr(next)) && !curSelection.Type().IsAggregateResult())) {
// handles case 4
// Root fastJson node's children contain the results for top level GraphQL queries.
// They are marked as list during fastJson node pre-processing even though they
// may not be list. So, we also need to consider such nodes if they actually have
// only one value and the current selection is not an aggregate field.
if !gqlCtx.encode(enc, cur, false, curSelection.SelectionSet(), curSelection,
append(parentPath, curSelection.ResponseName())) {
if nullWritten = writeGraphQLNull(curSelection, gqlCtx.buf,
keyEndPos); !nullWritten {
return false
}
}
// we need to iterate to the next fastJson node because we have used the data from
// the current fastJson node.
child = child.next
} else if !curSelectionIsDgList && enc.getList(cur) && curSelection.Type().
IsAggregateResult() {
// handles special case of aggregate fields
if fjIsRoot {
// this is the case of aggregate query at root
next = gqlCtx.completeRootAggregateQuery(enc, cur, curSelection,
append(parentPath, curSelection.ResponseName()))
} else {
// this case is of deep aggregate fields
next = gqlCtx.completeAggregateChildren(enc, cur, curSelection,
append(parentPath, curSelection.ResponseName()), false)
}
child = next
} else if !curSelectionIsDgList {
// handles case 3
gqlCtx.gqlErrs = append(gqlCtx.gqlErrs, curSelection.GqlErrorf(append(parentPath,
curSelection.ResponseName()), gqlSchema.ErrExpectedSingleItem))
if nullWritten = writeGraphQLNull(curSelection, gqlCtx.buf,
keyEndPos); !nullWritten {
return false
}
// need to skip all data points for the current selection, as they are of no use.
attrId := enc.idForAttr(curSelection.DgraphAlias())
for next != nil && enc.getAttr(next) == attrId {
next = next.next
}
child = next
} else {
// handles case 2
gqlCtx.gqlErrs = append(gqlCtx.gqlErrs, curSelection.GqlErrorf(append(parentPath,
curSelection.ResponseName()), gqlSchema.ErrExpectedList))
if nullWritten = writeGraphQLNull(curSelection, gqlCtx.buf,
keyEndPos); !nullWritten {
return false
}
// need to skip the only data point for the current selection, as it is of no use.
child = child.next
}
}
// Step-3: Update counters and Write closing ] for JSON arrays
if !curSelectionIsDgList || next == nil || enc.getAttr(cur) != enc.getAttr(next) {
if curSelectionIsDgList && !nullWritten {
x.Check2(gqlCtx.buf.WriteRune(']'))
}
cnt = 0 // Reset the count,
// indicating that we need to write the JSON key in next iteration.
i++ // all the results for curSelection have been picked up,
// so iterate to the next field in the child selection set.
}
// Step-4: Print comma except for the last field.
if i < len(childSelectionSet) {
x.Check2(gqlCtx.buf.WriteRune(','))
}
}
// We have iterated over all the useful data from Dgraph, and corresponding GraphQL fields.
// But, the GraphQL query may still have some fields which haven't been iterated upon.
// If there are un-iterated custom fields, then need to encode them using the data obtained
// from fastJson nodes stored in customNodes.
// For rest of the fields, we need to encode them as null valued fields.
for i < len(childSelectionSet) {
curSelection = childSelectionSet[i]
if curSelection.SkipField(dgraphTypes, seenField) {
i++
// if this is the last field and shouldn't be included,
// then need to remove comma from the buffer if one was present.
if i == len(childSelectionSet) {
checkAndStripComma(gqlCtx.buf)
}
continue
}
// Step-1: Write JSON key
curSelection.CompleteAlias(gqlCtx.buf)
// Step-2: Write JSON value
if curSelection.Name() == gqlSchema.Typename {
x.Check2(gqlCtx.buf.Write(getTypename(curSelection, dgraphTypes)))
} else if curSelection.IsCustomHTTP() && gqlCtx.writeCustomField(enc, curSelection,
customNodes, parentPath) {
// do nothing, value for field has already been written.
// If the value weren't written, the next else would write null.
} else {
if !writeGraphQLNull(curSelection, gqlCtx.buf, gqlCtx.buf.Len()) {
gqlCtx.gqlErrs = append(gqlCtx.gqlErrs, curSelection.GqlErrorf(append(parentPath,
curSelection.ResponseName()), gqlSchema.ErrExpectedNonNull, curSelection.Name(),
curSelection.Type()))
return false
}
}
i++ // iterate to next field
// Step-3: Print comma except for the last field.
if i < len(childSelectionSet) {
x.Check2(gqlCtx.buf.WriteRune(','))
}
}
// write the closing } for the JSON object
x.Check2(gqlCtx.buf.WriteRune('}'))
// encoding has successfully finished for this call to encode().
// Lets return true to indicate that.
return true
}
// writeCustomField is used to write the value when the currentSelection is a custom field.
// If the current field had @custom(http: {...}), then we need to find the fastJson node which
// stores data for this field from the customNodes mapping, and use that to write the value
// for this field.
func (gqlCtx *graphQLEncodingCtx) writeCustomField(enc *encoder, curSelection gqlSchema.Field,
customNodes map[uint16]fastJsonNode, parentPath []interface{}) bool {
if cNode := customNodes[enc.idForAttr(curSelection.DgraphAlias())]; cNode != nil {
// if we found the custom fastJson node, then directly write the value stored
// in that, as it would have been already completed.
val, err := enc.getScalarVal(cNode)
if err == nil {
x.Check2(gqlCtx.buf.Write(val))
// return true to indicate that the field was written successfully
return true
}
// if there was an error getting the value, append the error
gqlCtx.gqlErrs = append(gqlCtx.gqlErrs, curSelection.GqlErrorf(append(parentPath,
curSelection.ResponseName()), err.Error()))
}
// if no custom fastJson node was found or there was error getting the value, return false
return false
}
// resolveCustomFields resolves fields with custom directive. Here is the rough algorithm that it
// follows.
// queryUser {
// name @custom
// age
// school {
// name
// children
// class @custom {
// name
// numChildren
// }
// }
// cars @custom {
// name
// }
// }
// For fields with @custom directive
// 1. There would be one query sent to the remote endpoint.
// 2. In the above example, to fetch class all the school ids would be aggregated across different
// users deduplicated and then one query sent. The results would then be filled back appropriately.
//
// For fields without custom directive we recursively call resolveCustomFields and let it do the
// work.
func (gqlCtx *graphQLEncodingCtx) resolveCustomFields(ctx context.Context, enc *encoder,
parentNodeHeads []fastJsonNode, childFields []gqlSchema.Field) {
wg := &sync.WaitGroup{}
for _, childField := range childFields {
if childField.Skip() || !childField.Include() {
continue
}
if childField.IsCustomHTTP() {
wg.Add(1)
go gqlCtx.resolveCustomField(ctx, enc, parentNodeHeads, childField, wg)
} else if childField.HasCustomHTTPChild() {
wg.Add(1)
go gqlCtx.resolveNestedFields(ctx, enc, parentNodeHeads, childField, wg)
}
}
// wait for all the goroutines to finish
wg.Wait()
}
// extractDgraphTypes extracts the all values for dgraph.type predicate from the given child
// fastJson node. It returns the next fastJson node which doesn't store value for dgraph.type
// predicate along with the extracted values for dgraph.type.
func (gqlCtx *graphQLEncodingCtx) extractDgraphTypes(enc *encoder,
child fastJsonNode) (fastJsonNode, []string) {
var dgraphTypes []string
for ; child != nil && enc.getAttr(child) == gqlCtx.dgraphTypeAttrId; child = child.next {
if val, err := enc.getScalarVal(child); err == nil {
// val is a quoted string like: "Human"
dgraphTypes = append(dgraphTypes, toString(val))
}
}
return child, dgraphTypes
}
// extractRequiredFieldsData is used to extract the data of fields which are required to resolve
// a custom field from a given parentNode.
// It returns a map containing the extracted data along with the dgraph.type values for parentNode.
// The keys in the returned map correspond to the name of a required field.
// Values in the map correspond to the extracted data for a required field.
func (gqlCtx *graphQLEncodingCtx) extractRequiredFieldsData(enc *encoder, parentNode fastJsonNode,
rfDefs map[string]gqlSchema.FieldDefinition) (map[string]interface{}, []string) {
child := enc.children(parentNode)
// first, just skip all the custom nodes
for ; child != nil && enc.getCustom(child); child = child.next {
// do nothing
}
// then, extract data for dgraph.type
child, dgraphTypes := gqlCtx.extractDgraphTypes(enc, child)
// now, iterate over rest of the children of the parentNode and find out the data for
// requiredFields. We can stop iterating as soon as we have the data for all the requiredFields.
rfData := make(map[string]interface{})
for fj := child; fj != nil && len(rfData) < len(rfDefs); fj = fj.next {
// check if this node has the data for a requiredField. If yes, we need to
// extract that in the rfData map to be used later in substitution.
if rfDef := rfDefs[enc.attrForID(enc.getAttr(fj))]; rfDef != nil {
// if the requiredField is of list type, then need to extract all the data for the list.
// using enc.getList() instead of `rfDef.Type().ListType() != nil` as for custom fields
// both have the same behaviour and enc.getList() is fast.
if enc.getList(fj) {
var vals []interface{}
for ; fj.next != nil && enc.getAttr(fj.next) == enc.getAttr(fj); fj = fj.next {
if val, err := enc.getScalarVal(fj); err == nil {
vals = append(vals, json.RawMessage(val))
}
}
// append the last list value
if val, err := enc.getScalarVal(fj); err == nil {
vals = append(vals, json.RawMessage(val))
}
rfData[rfDef.Name()] = vals
} else {
// this requiredField is of non-list type, need to extract the only
// data point for this.
if val, err := enc.getScalarVal(fj); err == nil {
rfData[rfDef.Name()] = json.RawMessage(val)
}
}
}
}
return rfData, dgraphTypes
}
// resolveCustomField resolves the @custom childField by making an external HTTP request and then
// updates the fastJson tree with results of that HTTP request.
// It accepts the following arguments:
// - ctx: context to use for finding auth info and propagating that to lambda server
// - enc: the encoder which stores all the data
// - parentNodeHeads: a list of head pointers to the parent nodes of childField
// - childField: the @custom field which needs to be resolved
// - wg: a wait group to signal the calling goroutine when the execution of this goroutine is
// finished
// TODO:
// - benchmark concurrency for the worker goroutines: channels vs mutexes?
// https://medium.com/@_orcaman/when-too-much-concurrency-slows-you-down-golang-9c144ca305a
// - worry about path in errors and how to deal with them, specially during completion step
func (gqlCtx *graphQLEncodingCtx) resolveCustomField(ctx context.Context, enc *encoder,
parentNodeHeads []fastJsonNode, childField gqlSchema.Field, wg *sync.WaitGroup) {
defer wg.Done() // signal when this goroutine finishes execution
fconf, err := childField.CustomHTTPConfig()
if err != nil {
gqlCtx.errChan <- x.GqlErrorList{childField.GqlErrorf(nil, err.Error())}
return
}
// for resolving a custom field, we need to carry out following steps:
// 1: Find the requiredFields data for uniqueParents from all the parentNodes
// 2. Construct correct URL and body using that data
// 3. Make the request to external HTTP endpoint using the URL and body
// 4. Decode the HTTP response
// 5. Run GraphQL completion on the decoded HTTP response
// 6. Create fastJson nodes which contain the completion result for this custom field for
// all the duplicate parents and
// 7. Update the fastJson tree with those fastJson nodes
var parentNodeHeadAttr uint16
if len(parentNodeHeads) > 0 {
parentNodeHeadAttr = enc.getAttr(parentNodeHeads[0])
}
isGraphqlReq := fconf.RemoteGqlQueryName != ""
requiredFields := childField.CustomRequiredFields()
// we need to find the ID or @id field from requiredFields as we want to make HTTP requests
// only for unique parent nodes. That means, we send/receive less data over the network,
// and thus minimize the network latency as much as possible.
idFieldName := ""
idFieldValue := ""
for _, fieldDef := range requiredFields {
if fieldDef.IsID() || fieldDef.HasIDDirective() {
idFieldName = fieldDef.Name()
break
}
}
if idFieldName == "" {
// This should not happen as we only allow custom fields which either use ID field or a
// field with @id directive.
gqlCtx.errChan <- x.GqlErrorList{childField.GqlErrorf(nil,
"unable to find a required field with type ID! or @id directive for @custom field %s.",
childField.Name())}
return
}
// we don't know the number of unique parents in advance,
// so can't allocate this list with a pre-defined size
var uniqueParents []interface{}
// uniqueParentIdxToIdFieldVal stores the idFieldValue for each unique rfData
var uniqueParentIdxToIdFieldVal []string
// parentNodes is a map from idFieldValue to all the parentNodes for that idFieldValue.
parentNodes := make(map[string][]fastJsonNode)
// Step-1: Find the requiredFields data for uniqueParents from all the parentNodes
for _, parentNodeHead := range parentNodeHeads {
// iterate over all the siblings of this parentNodeHead which have the same attr as this
for parentNode := parentNodeHead; parentNode != nil && enc.getAttr(
parentNode) == parentNodeHeadAttr; parentNode = parentNode.next {
// find the data for requiredFields from parentNode
rfData, dgraphTypes := gqlCtx.extractRequiredFieldsData(enc, parentNode, requiredFields)
// check if this childField needs to be included for this parent node
if !childField.IncludeAbstractField(dgraphTypes) {
continue
}
if val, _ := rfData[idFieldName].(json.RawMessage); val != nil {
idFieldValue = string(val)
} else {
// this case can't happen as ID or @id fields are not list values
continue
}
// add rfData to uniqueParents only if we haven't encountered any parentNode before
// with this idFieldValue
if len(parentNodes[idFieldValue]) == 0 {
uniqueParents = append(uniqueParents, rfData)
uniqueParentIdxToIdFieldVal = append(uniqueParentIdxToIdFieldVal, idFieldValue)
}
// always add the parent node to the slice for this idFieldValue, so that we can
// build the response for all the duplicate parents
parentNodes[idFieldValue] = append(parentNodes[idFieldValue], parentNode)
}
}
switch fconf.Mode {
case gqlSchema.SINGLE:
// In SINGLE mode, we can consider steps 2-5 as a single isolated unit of computation,
// which can be executed in parallel for each uniqueParent.
// Step 6-7 can be executed in parallel to Step 2-5 in a separate goroutine to minimize
// contention.
// used to wait on goroutines started for each uniqueParent
uniqueParentWg := &sync.WaitGroup{}
// iterate over all the uniqueParents to make HTTP requests
for i := range uniqueParents {
uniqueParentWg.Add(1)
go func(idx int) {
defer uniqueParentWg.Done() // signal when this goroutine finishes execution
// Step-2: Construct correct URL and body using the data of requiredFields
url := fconf.URL
var body interface{}
if isGraphqlReq {
// If it is a remote GraphQL request, then URL can't have variables.
// So, we only need to construct the body.
body = map[string]interface{}{
"query": fconf.RemoteGqlQuery,
"variables": uniqueParents[idx],
}
} else {
// for REST requests, we need to correctly construct both URL & body
url, err = gqlSchema.SubstituteVarsInURL(url,
uniqueParents[idx].(map[string]interface{}))
if err != nil {
gqlCtx.errChan <- x.GqlErrorList{childField.GqlErrorf(nil,
"Evaluation of custom field failed while substituting variables "+
"into URL for remote endpoint with an error: %s for field: %s "+
"within type: %s.", err, childField.Name(),
childField.GetObjectName())}
return
}
body = gqlSchema.SubstituteVarsInBody(fconf.Template,
uniqueParents[idx].(map[string]interface{}))
}
// Step-3 & 4: Make the request to external HTTP endpoint using the URL and
// body. Then, Decode the HTTP response.
response, errs, hardErrs := fconf.MakeAndDecodeHTTPRequest(nil, url, body,
childField)
if hardErrs != nil {
gqlCtx.errChan <- hardErrs
return
}
// Step-5. Run GraphQL completion on the decoded HTTP response
b, gqlErrs := gqlSchema.CompleteValue(nil, childField, response)
errs = append(errs, gqlErrs...)
// finally, send the fastJson tree update over the channel
if b != nil {
gqlCtx.customFieldResultChan <- customFieldResult{
parents: parentNodes[uniqueParentIdxToIdFieldVal[idx]],
childField: childField,
childVal: b,
}
}
// if we are here, it means the fastJson tree update was successfully sent.
// i.e., this custom childField was successfully resolved for given parentNode.
// now, send all the collected errors together
gqlCtx.errChan <- errs
}(i)
}
uniqueParentWg.Wait()
case gqlSchema.BATCH:
// In BATCH mode, we can break the above steps into following isolated units of computation:
// a. Step 2-4
// b. Step 5
// c. Step 6-7
// i.e., step-a has to be executed only once irrespective of the number of parentNodes.
// Then, step-b can be executed in parallel for each parentNode.
// step-c can run in parallel to step-b in a separate goroutine to minimize contention.
// Step-2: Construct correct body for the batch request
var body interface{}
if isGraphqlReq {
body = map[string]interface{}{
"query": fconf.RemoteGqlQuery,
"variables": map[string]interface{}{fconf.GraphqlBatchModeArgument: uniqueParents},
}
} else {
for i := range uniqueParents {
uniqueParents[i] = gqlSchema.SubstituteVarsInBody(fconf.Template,
uniqueParents[i].(map[string]interface{}))
}
if childField.HasLambdaDirective() {
body = gqlSchema.GetBodyForLambda(ctx, childField, uniqueParents, nil)
} else {
body = uniqueParents
}
}
// Step-3 & 4: Make the request to external HTTP endpoint using the URL and
// body. Then, Decode the HTTP response.
response, errs, hardErrs := fconf.MakeAndDecodeHTTPRequest(nil, fconf.URL, body, childField)
if hardErrs != nil {
gqlCtx.errChan <- hardErrs
return
}
batchedResult, ok := response.([]interface{})
if !ok {
gqlCtx.errChan <- append(errs, childField.GqlErrorf(nil,
"Evaluation of custom field failed because expected result of "+
"external BATCH request to be of list type, got: %v for field: %s within type: %s.",
reflect.TypeOf(response).Name(), childField.Name(), childField.GetObjectName()))
return
}
if len(batchedResult) != len(uniqueParents) {
gqlCtx.errChan <- append(errs, childField.GqlErrorf(nil,
"Evaluation of custom field failed because expected result of "+
"external request to be of size %v, got: %v for field: %s within type: %s.",
len(uniqueParents), len(batchedResult), childField.Name(), childField.GetObjectName()))
return
}
batchedErrs := make([]x.GqlErrorList, len(batchedResult))
batchedResultWg := &sync.WaitGroup{}
for i := range batchedResult {
batchedResultWg.Add(1)
go func(idx int) {
defer batchedResultWg.Done() // signal when this goroutine finishes execution
// Step-5. Run GraphQL completion on the decoded HTTP response
b, gqlErrs := gqlSchema.CompleteValue(nil, childField, batchedResult[idx])
// finally, send the fastJson tree update over the channel
if b != nil {
gqlCtx.customFieldResultChan <- customFieldResult{
parents: parentNodes[uniqueParentIdxToIdFieldVal[idx]],
childField: childField,
childVal: b,
}
}
// set the errors obtained from completion
batchedErrs[idx] = gqlErrs
}(i)
}
batchedResultWg.Wait()
// we are doing this just to send all the related errors together, otherwise if we directly
// send it over the error channel, they may get spread here and there in errors.
for _, batchedErr := range batchedErrs {
if batchedErr != nil {
errs = append(errs, batchedErr...)
}
}
// now, send all the collected errors together
gqlCtx.errChan <- errs
}
}
// resolveNestedFields resolves fields which themselves don't have the @custom directive but their
// children might.
//
// queryUser {
// id
// classes {
// name @custom...
// }
// }
// In the example above, resolveNestedFields would be called on classes field and parentNodeHeads
// would be the list of head pointers for all the user fastJson nodes.
func (gqlCtx *graphQLEncodingCtx) resolveNestedFields(ctx context.Context, enc *encoder,
parentNodeHeads []fastJsonNode, childField gqlSchema.Field, wg *sync.WaitGroup) {
defer wg.Done() // signal when this goroutine finishes execution
var childNodeHeads []fastJsonNode
var parentNodeHeadAttr uint16
if len(parentNodeHeads) > 0 {
parentNodeHeadAttr = enc.getAttr(parentNodeHeads[0])
}
childFieldAttr := enc.idForAttr(childField.DgraphAlias())
// iterate over all the parentNodeHeads and build the list of childNodeHeads for this childField
for _, parentNodeHead := range parentNodeHeads {
// iterate over all the siblings of this parentNodeHead which have the same attr as this
for parentNode := parentNodeHead; parentNode != nil && enc.getAttr(
parentNode) == parentNodeHeadAttr; parentNode = parentNode.next {
// find the first child node which has data for childField
fj := enc.children(parentNode)
for ; fj != nil && enc.getAttr(fj) != childFieldAttr; fj = fj.next {
// do nothing, just keep skipping unnecessary data
}
if fj != nil {
// we found the first node that has data for childField,
// add that node to the list of childNodeHeads
childNodeHeads = append(childNodeHeads, fj)
}
}
}
// if we found some data for the child field, then only we need to
// resolve the custom fields in the selection set of childField
if len(childNodeHeads) > 0 {
gqlCtx.resolveCustomFields(ctx, enc, childNodeHeads, childField.SelectionSet())
}
}
// checkAndStripComma checks whether there is a comma at the end of the given buffer. If yes,
// it removes that comma from the buffer.
func checkAndStripComma(buf *bytes.Buffer) {
b := buf.Bytes()
if len(b) > 0 && b[len(b)-1] == ',' {
buf.Truncate(buf.Len() - 1)
}
}
// getTypename returns the JSON bytes for the __typename field, given the dgraph.type values
// extracted from dgraph response.
func getTypename(f gqlSchema.Field, dgraphTypes []string) []byte {
return []byte(`"` + f.TypeName(dgraphTypes) + `"`)
}
// writeGraphQLNull writes null value for the given field to the buffer.
// If the field is non-nullable, it returns false, otherwise it returns true.
func writeGraphQLNull(f gqlSchema.Field, buf *bytes.Buffer, keyEndPos int) bool {
if b := f.NullValue(); b != nil {
buf.Truncate(keyEndPos) // truncate to make sure we write null correctly
x.Check2(buf.Write(b))
return true
}
return false
}
// completeRootAggregateQuery builds GraphQL JSON for aggregate queries at root.
// Root aggregate queries return a single object of type `TypeAggregateResult` which contains the
// aggregate properties. But, in the Dgraph results those properties are returned as a list of
// objects, each object having only one property. So we need to handle encoding root aggregate
// queries accordingly.
// Dgraph result:
// {
// "aggregateCountry": [
// {
// "CountryAggregateResult.count": 3
// }, {
// "CountryAggregateResult.nameMin": "US1"
// }, {
// "CountryAggregateResult.nameMax": "US2"
// }
// ]