diff --git a/go.work b/go.work index 7aabbb775..48af77fa1 100644 --- a/go.work +++ b/go.work @@ -22,4 +22,6 @@ use ( replace github.com/99designs/gqlgen => github.com/99designs/gqlgen v0.17.22 -replace github.com/tidwall/sjson => github.com/tidwall/sjson v1.0.4 \ No newline at end of file +replace github.com/tidwall/sjson => github.com/tidwall/sjson v1.0.4 + +//replace github.com/wundergraph/astjson => ../wundergraph-projects/astjson \ No newline at end of file diff --git a/v2/go.mod b/v2/go.mod index 18692d555..43487484d 100644 --- a/v2/go.mod +++ b/v2/go.mod @@ -26,7 +26,7 @@ require ( github.com/tidwall/gjson v1.17.0 github.com/tidwall/sjson v1.2.5 github.com/vektah/gqlparser/v2 v2.5.14 - github.com/wundergraph/astjson v0.0.0-20241105103047-3b2e8a2b2779 + github.com/wundergraph/astjson v0.0.0-20241108124845-44485579ffa5 go.uber.org/atomic v1.11.0 go.uber.org/goleak v1.3.0 go.uber.org/zap v1.26.0 diff --git a/v2/go.sum b/v2/go.sum index eb3a2a64d..377dbfc2e 100644 --- a/v2/go.sum +++ b/v2/go.sum @@ -106,6 +106,8 @@ github.com/vektah/gqlparser/v2 v2.5.14 h1:dzLq75BJe03jjQm6n56PdH1oweB8ana42wj7E4 github.com/vektah/gqlparser/v2 v2.5.14/go.mod h1:WQQjFc+I1YIzoPvZBhUQX7waZgg3pMLi0r8KymvAE2w= github.com/wundergraph/astjson v0.0.0-20241105103047-3b2e8a2b2779 h1:c9pa8s5eOFEOBH9Vs+VsP4EcsdcHzAaDKooHBdUmmK0= github.com/wundergraph/astjson v0.0.0-20241105103047-3b2e8a2b2779/go.mod h1:eOTL6acwctsN4F3b7YE+eE2t8zcJ/doLm9sZzsxxxrE= +github.com/wundergraph/astjson v0.0.0-20241108124845-44485579ffa5 h1:rc+IQxG3rrAXEjBywirkzhKkyCKvXLGQXABVD8GiUtU= +github.com/wundergraph/astjson v0.0.0-20241108124845-44485579ffa5/go.mod h1:eOTL6acwctsN4F3b7YE+eE2t8zcJ/doLm9sZzsxxxrE= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= diff --git a/v2/pkg/engine/datasource/httpclient/nethttpclient.go b/v2/pkg/engine/datasource/httpclient/nethttpclient.go index 08a722d31..3d7adb6a6 100644 --- a/v2/pkg/engine/datasource/httpclient/nethttpclient.go +++ b/v2/pkg/engine/datasource/httpclient/nethttpclient.go @@ -15,7 +15,6 @@ import ( "os" "slices" "strings" - "sync" "time" "github.com/buger/jsonparser" @@ -116,23 +115,6 @@ func respBodyReader(res *http.Response) (io.Reader, error) { } } -var ( - requestBufferPool = &sync.Pool{ - New: func() any { - return &bytes.Buffer{} - }, - } -) - -func getBuffer() *bytes.Buffer { - return requestBufferPool.Get().(*bytes.Buffer) -} - -func releaseBuffer(buf *bytes.Buffer) { - buf.Reset() - requestBufferPool.Put(buf) -} - type bodyHashContextKey struct{} func BodyHashFromContext(ctx context.Context) (uint64, bool) { @@ -217,14 +199,16 @@ func makeHTTPRequest(client *http.Client, ctx context.Context, url, method, head } if !enableTrace { + if response.ContentLength > 0 { + out.Grow(int(response.ContentLength)) + } else { + out.Grow(1024 * 4) + } _, err = out.ReadFrom(respReader) return } - buf := getBuffer() - defer releaseBuffer(buf) - - _, err = buf.ReadFrom(respReader) + data, err := io.ReadAll(respReader) if err != nil { return err } @@ -238,14 +222,14 @@ func makeHTTPRequest(client *http.Client, ctx context.Context, url, method, head StatusCode: response.StatusCode, Status: response.Status, Headers: redactHeaders(response.Header), - BodySize: buf.Len(), + BodySize: len(data), }, } trace, err := json.Marshal(responseTrace) if err != nil { return err } - responseWithTraceExtension, err := jsonparser.Set(buf.Bytes(), trace, "extensions", "trace") + responseWithTraceExtension, err := jsonparser.Set(data, trace, "extensions", "trace") if err != nil { return err } diff --git a/v2/pkg/engine/resolve/inputtemplate_test.go b/v2/pkg/engine/resolve/inputtemplate_test.go index 98512fb1c..b78e28ae5 100644 --- a/v2/pkg/engine/resolve/inputtemplate_test.go +++ b/v2/pkg/engine/resolve/inputtemplate_test.go @@ -372,18 +372,66 @@ func TestInputTemplate_Render(t *testing.T) { }) t.Run("GraphQLVariableResolveRenderer", func(t *testing.T) { - t.Run("nested objects", func(t *testing.T) { + t.Run("optional fields", func(t *testing.T) { template := InputTemplate{ Segments: []TemplateSegment{ { - SegmentType: StaticSegmentType, - Data: []byte(`{"key":`), + SegmentType: VariableSegmentType, + VariableKind: ResolvableObjectVariableKind, + Renderer: NewGraphQLVariableResolveRenderer(&Object{ + Nullable: false, + Fields: []*Field{ + { + Name: []byte("name"), + Value: &String{ + Path: []string{"name"}, + Nullable: true, + }, + }, + }, + }), }, + }, + } + + data := astjson.MustParseBytes([]byte(`{"name":"foo"}`)) + ctx := &Context{ + ctx: context.Background(), + } + buf := &bytes.Buffer{} + + err := template.Render(ctx, data, buf) + assert.NoError(t, err) + out := buf.String() + assert.Equal(t, `{"name":"foo"}`, out) + + data = astjson.MustParseBytes([]byte(`{}`)) + buf.Reset() + err = template.Render(ctx, data, buf) + assert.NoError(t, err) + out = buf.String() + assert.Equal(t, `{"name":null}`, out) + + data = astjson.MustParseBytes([]byte(`{"name":null}`)) + buf.Reset() + err = template.Render(ctx, data, buf) + assert.NoError(t, err) + out = buf.String() + assert.Equal(t, `{"name":null}`, out) + + data = astjson.MustParseBytes([]byte(`{"name":123}`)) + buf.Reset() + err = template.Render(ctx, data, buf) + assert.Error(t, err) + }) + t.Run("nested objects", func(t *testing.T) { + template := InputTemplate{ + Segments: []TemplateSegment{ { SegmentType: VariableSegmentType, VariableKind: ResolvableObjectVariableKind, Renderer: NewGraphQLVariableResolveRenderer(&Object{ - Nullable: false, + Nullable: true, Fields: []*Field{ { Name: []byte("address"), @@ -422,21 +470,49 @@ func TestInputTemplate_Render(t *testing.T) { }, }), }, - { - SegmentType: StaticSegmentType, - Data: []byte(`}`), - }, }, } ctx := &Context{ ctx: context.Background(), Variables: astjson.MustParseBytes([]byte(`{}`)), } - buf := &bytes.Buffer{} - err := template.Render(ctx, astjson.MustParseBytes([]byte(`{"name":"home","address":{"zip":"00000","items":[{"name":"home","active":true}]}}`)), buf) - assert.NoError(t, err) - out := buf.String() - assert.Equal(t, `{"key":{"address":{"zip":"00000","items":[{"active":true}]}}}`, out) + + cases := []struct { + name string + input string + expected string + expectErr bool + }{ + { + name: "data is present", + input: `{"name":"home","address":{"zip":"00000","items":[{"name":"home","active":true}]}}`, + expected: `{"address":{"zip":"00000","items":[{"active":true}]}}`, + }, + { + name: "data is missing", + input: `{"name":"home"}`, + expectErr: true, + }, + { + name: "partial data", + input: `{"name":"home","address":{}}`, + expectErr: true, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + buf := &bytes.Buffer{} + err := template.Render(ctx, astjson.MustParseBytes([]byte(c.input)), buf) + if c.expectErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + out := buf.String() + assert.Equal(t, c.expected, out) + }) + } }) }) diff --git a/v2/pkg/engine/resolve/loader.go b/v2/pkg/engine/resolve/loader.go index 482408537..18788ee02 100644 --- a/v2/pkg/engine/resolve/loader.go +++ b/v2/pkg/engine/resolve/loader.go @@ -448,7 +448,7 @@ func (l *Loader) mergeResult(fetchItem *FetchItem, res *result, items []*astjson if res.out.Len() == 0 { return l.renderErrorsFailedToFetch(fetchItem, res, emptyGraphQLResponse) } - value, err := l.resolvable.parseJSONBytes(res.out.Bytes()) + value, err := astjson.ParseBytesWithoutCache(res.out.Bytes()) if err != nil { return l.renderErrorsFailedToFetch(fetchItem, res, invalidGraphQLResponse) } @@ -647,7 +647,7 @@ func (l *Loader) mergeErrors(res *result, fetchItem *FetchItem, value *astjson.V } // Wrap mode (default) - errorObject, err := l.resolvable.parseJSONString(l.renderSubgraphBaseError(res.ds, fetchItem.ResponsePath, failedToFetchNoReason)) + errorObject, err := astjson.ParseWithoutCache(l.renderSubgraphBaseError(res.ds, fetchItem.ResponsePath, failedToFetchNoReason)) if err != nil { return err } @@ -826,7 +826,7 @@ func (l *Loader) optionallyRewriteErrorPaths(fetchItem *FetchItem, values []*ast newPath = append(newPath, unsafebytes.BytesToString(pathItems[j].GetStringBytes())) } newPathJSON, _ := json.Marshal(newPath) - pathBytes, err := l.resolvable.parseJSONBytes(newPathJSON) + pathBytes, err := astjson.ParseBytesWithoutCache(newPathJSON) if err != nil { continue } @@ -852,13 +852,13 @@ func (l *Loader) setSubgraphStatusCode(values []*astjson.Value, statusCode int) if extensions.Type() != astjson.TypeObject { continue } - v, err := l.resolvable.parseJSONString(strconv.Itoa(statusCode)) + v, err := astjson.ParseWithoutCache(strconv.Itoa(statusCode)) if err != nil { continue } extensions.Set("statusCode", v) } else { - v, err := l.resolvable.parseJSONString(`{"statusCode":` + strconv.Itoa(statusCode) + `}`) + v, err := astjson.ParseWithoutCache(`{"statusCode":` + strconv.Itoa(statusCode) + `}`) if err != nil { continue } @@ -883,7 +883,7 @@ func (l *Loader) renderAtPathErrorPart(path string) string { func (l *Loader) renderErrorsFailedToFetch(fetchItem *FetchItem, res *result, reason string) error { l.ctx.appendSubgraphError(goerrors.Join(res.err, NewSubgraphError(res.ds, fetchItem.ResponsePath, reason, res.statusCode))) - errorObject, err := l.resolvable.parseJSONString(l.renderSubgraphBaseError(res.ds, fetchItem.ResponsePath, reason)) + errorObject, err := astjson.ParseWithoutCache(l.renderSubgraphBaseError(res.ds, fetchItem.ResponsePath, reason)) if err != nil { return err } @@ -914,13 +914,13 @@ func (l *Loader) renderAuthorizationRejectedErrors(fetchItem *FetchItem, res *re if res.ds.Name == "" { for _, reason := range res.authorizationRejectedReasons { if reason == "" { - errorObject, err := l.resolvable.parseJSONString(fmt.Sprintf(`{"message":"Unauthorized Subgraph request%s."}`, pathPart)) + errorObject, err := astjson.ParseWithoutCache(fmt.Sprintf(`{"message":"Unauthorized Subgraph request%s."}`, pathPart)) if err != nil { continue } astjson.AppendToArray(l.resolvable.errors, errorObject) } else { - errorObject, err := l.resolvable.parseJSONString(fmt.Sprintf(`{"message":"Unauthorized Subgraph request%s, Reason: %s."}`, pathPart, reason)) + errorObject, err := astjson.ParseWithoutCache(fmt.Sprintf(`{"message":"Unauthorized Subgraph request%s, Reason: %s."}`, pathPart, reason)) if err != nil { continue } @@ -930,13 +930,13 @@ func (l *Loader) renderAuthorizationRejectedErrors(fetchItem *FetchItem, res *re } else { for _, reason := range res.authorizationRejectedReasons { if reason == "" { - errorObject, err := l.resolvable.parseJSONString(fmt.Sprintf(`{"message":"Unauthorized request to Subgraph '%s'%s."}`, res.ds.Name, pathPart)) + errorObject, err := astjson.ParseWithoutCache(fmt.Sprintf(`{"message":"Unauthorized request to Subgraph '%s'%s."}`, res.ds.Name, pathPart)) if err != nil { continue } astjson.AppendToArray(l.resolvable.errors, errorObject) } else { - errorObject, err := l.resolvable.parseJSONString(fmt.Sprintf(`{"message":"Unauthorized request to Subgraph '%s'%s, Reason: %s."}`, res.ds.Name, pathPart, reason)) + errorObject, err := astjson.ParseWithoutCache(fmt.Sprintf(`{"message":"Unauthorized request to Subgraph '%s'%s, Reason: %s."}`, res.ds.Name, pathPart, reason)) if err != nil { continue } @@ -952,13 +952,13 @@ func (l *Loader) renderRateLimitRejectedErrors(fetchItem *FetchItem, res *result pathPart := l.renderAtPathErrorPart(fetchItem.ResponsePath) if res.ds.Name == "" { if res.rateLimitRejectedReason == "" { - errorObject, err := l.resolvable.parseJSONString(fmt.Sprintf(`{"message":"Rate limit exceeded for Subgraph request%s."}`, pathPart)) + errorObject, err := astjson.ParseWithoutCache(fmt.Sprintf(`{"message":"Rate limit exceeded for Subgraph request%s."}`, pathPart)) if err != nil { return err } astjson.AppendToArray(l.resolvable.errors, errorObject) } else { - errorObject, err := l.resolvable.parseJSONString(fmt.Sprintf(`{"message":"Rate limit exceeded for Subgraph request%s, Reason: %s."}`, pathPart, res.rateLimitRejectedReason)) + errorObject, err := astjson.ParseWithoutCache(fmt.Sprintf(`{"message":"Rate limit exceeded for Subgraph request%s, Reason: %s."}`, pathPart, res.rateLimitRejectedReason)) if err != nil { return err } @@ -966,13 +966,13 @@ func (l *Loader) renderRateLimitRejectedErrors(fetchItem *FetchItem, res *result } } else { if res.rateLimitRejectedReason == "" { - errorObject, err := l.resolvable.parseJSONString(fmt.Sprintf(`{"message":"Rate limit exceeded for Subgraph '%s'%s."}`, res.ds.Name, pathPart)) + errorObject, err := astjson.ParseWithoutCache(fmt.Sprintf(`{"message":"Rate limit exceeded for Subgraph '%s'%s."}`, res.ds.Name, pathPart)) if err != nil { return err } astjson.AppendToArray(l.resolvable.errors, errorObject) } else { - errorObject, err := l.resolvable.parseJSONString(fmt.Sprintf(`{"message":"Rate limit exceeded for Subgraph '%s'%s, Reason: %s."}`, res.ds.Name, pathPart, res.rateLimitRejectedReason)) + errorObject, err := astjson.ParseWithoutCache(fmt.Sprintf(`{"message":"Rate limit exceeded for Subgraph '%s'%s, Reason: %s."}`, res.ds.Name, pathPart, res.rateLimitRejectedReason)) if err != nil { return err } @@ -1570,7 +1570,7 @@ func (l *Loader) compactJSON(data []byte) ([]byte, error) { return nil, err } out := dst.Bytes() - v, err := astjson.ParseBytes(out) + v, err := astjson.ParseBytesWithoutCache(out) if err != nil { return nil, err } diff --git a/v2/pkg/engine/resolve/loader_test.go b/v2/pkg/engine/resolve/loader_test.go index b5c767152..27f6346d0 100644 --- a/v2/pkg/engine/resolve/loader_test.go +++ b/v2/pkg/engine/resolve/loader_test.go @@ -853,7 +853,7 @@ func BenchmarkLoader_LoadGraphQLResponseData(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { loader.Free() - resolvable.Reset(0) + resolvable.Reset() err := resolvable.Init(ctx, nil, ast.OperationTypeQuery) if err != nil { b.Fatal(err) diff --git a/v2/pkg/engine/resolve/resolvable.go b/v2/pkg/engine/resolve/resolvable.go index c87bfd521..a3ead5fda 100644 --- a/v2/pkg/engine/resolve/resolvable.go +++ b/v2/pkg/engine/resolve/resolvable.go @@ -77,27 +77,7 @@ func NewResolvable(options ResolvableOptions) *Resolvable { } } -var ( - parsers = &astjson.ParserPool{} -) - -func (r *Resolvable) parseJSONBytes(data []byte) (*astjson.Value, error) { - parser := parsers.Get() - r.parsers = append(r.parsers, parser) - return parser.ParseBytes(data) -} - -func (r *Resolvable) parseJSONString(data string) (*astjson.Value, error) { - parser := parsers.Get() - r.parsers = append(r.parsers, parser) - return parser.Parse(data) -} - -func (r *Resolvable) Reset(maxRecyclableParserSize int) { - for i := range r.parsers { - parsers.PutIfSizeLessThan(r.parsers[i], maxRecyclableParserSize) - r.parsers[i] = nil - } +func (r *Resolvable) Reset() { r.parsers = r.parsers[:0] r.typeNames = r.typeNames[:0] r.enclosingTypeNames = r.enclosingTypeNames[:0] @@ -132,7 +112,7 @@ func (r *Resolvable) Init(ctx *Context, initialData []byte, operationType ast.Op r.data = r.astjsonArena.NewObject() r.errors = r.astjsonArena.NewArray() if initialData != nil { - initialValue, err := r.parseJSONBytes(initialData) + initialValue, err := astjson.ParseBytesWithoutCache(initialData) if err != nil { return err } @@ -146,7 +126,7 @@ func (r *Resolvable) InitSubscription(ctx *Context, initialData []byte, postProc r.operationType = ast.OperationTypeSubscription r.renameTypeNames = ctx.RenameTypeNames if initialData != nil { - initialValue, err := r.parseJSONBytes(initialData) + initialValue, err := astjson.ParseBytesWithoutCache(initialData) if err != nil { return err } diff --git a/v2/pkg/engine/resolve/resolvable_test.go b/v2/pkg/engine/resolve/resolvable_test.go index 4e91a2f56..31f0fa6a9 100644 --- a/v2/pkg/engine/resolve/resolvable_test.go +++ b/v2/pkg/engine/resolve/resolvable_test.go @@ -946,7 +946,7 @@ func TestResolvable_ValueCompletion(t *testing.T) { assert.NoError(t, err) assert.Equal(t, `{"data":{"object":null},"extensions":{"valueCompletion":[{"message":"Invalid __typename found for object at field Query.object.","path":["object"],"extensions":{"code":"INVALID_GRAPHQL"}}]}}`, out.String()) - res.Reset(1024) + res.Reset() err = res.Init(ctx, []byte(`{"object":{"hello":"world","__typename":"Hello"}}`), ast.OperationTypeQuery) assert.NoError(t, err) out.Reset() @@ -954,7 +954,7 @@ func TestResolvable_ValueCompletion(t *testing.T) { assert.NoError(t, err) assert.Equal(t, `{"data":{"object":{"hello":"world"}}}`, out.String()) - res.Reset(1024) + res.Reset() err = res.Init(ctx, []byte(`{"object":{"hello":"world","__typename":"NotEvenATinyBitHello"}}`), ast.OperationTypeQuery) assert.NoError(t, err) out.Reset() diff --git a/v2/pkg/engine/resolve/resolve_federation_test.go b/v2/pkg/engine/resolve/resolve_federation_test.go index 9b8a27e9c..fc282c951 100644 --- a/v2/pkg/engine/resolve/resolve_federation_test.go +++ b/v2/pkg/engine/resolve/resolve_federation_test.go @@ -371,6 +371,1543 @@ func TestResolveGraphQLResponse_Federation(t *testing.T) { })) }) + t.Run("federation input render", func(t *testing.T) { + t.Run("batch entity fetch", func(t *testing.T) { + t.Run("batching on union", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { + + userService := NewMockDataSource(ctrl) + userService.EXPECT(). + Load(gomock.Any(), gomock.Any(), gomock.AssignableToTypeOf(&bytes.Buffer{})). + DoAndReturn(func(ctx context.Context, input []byte, w io.Writer) (err error) { + actual := string(input) + expected := `{"method":"POST","url":"http://localhost:4001","body":{"query":"{ user { name infoOrAddress { ... on Info {id __typename} ... on Address {id __typename}}}}"}}` + assert.Equal(t, expected, actual) + pair := NewBufPair() + pair.Data.WriteString(`{"user":{"name":"Bill","infoOrAddress":[{"id":11,"__typename":"Info"},{"id": 55,"__typename":"Address"}]}}`) + return writeGraphqlResponse(pair, w, false) + }) + + infoService := NewMockDataSource(ctrl) + infoService.EXPECT(). + Load(gomock.Any(), gomock.Any(), gomock.AssignableToTypeOf(&bytes.Buffer{})). + DoAndReturn(func(ctx context.Context, input []byte, w io.Writer) (err error) { + actual := string(input) + expected := `{"method":"POST","url":"http://localhost:4002","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations){query($representations: [_Any!]!){_entities(representations: $representations) { ... on Info { age } ... on Address { line1 }}}}}","variables":{"representations":[{"id":11,"__typename":"Info"},{"id":55,"__typename":"Address"}]}}}` + assert.Equal(t, expected, actual) + pair := NewBufPair() + pair.Data.WriteString(`{"_entities":[{"age":21,"__typename":"Info"},{"line1":"Munich","__typename":"Address"}]}`) + return writeGraphqlResponse(pair, w, false) + }) + + return &GraphQLResponse{ + Fetches: Sequence( + SingleWithPath(&SingleFetch{ + InputTemplate: InputTemplate{ + Segments: []TemplateSegment{ + { + Data: []byte(`{"method":"POST","url":"http://localhost:4001","body":{"query":"{ user { name infoOrAddress { ... on Info {id __typename} ... on Address {id __typename}}}}"}}`), + SegmentType: StaticSegmentType, + }, + }, + }, + FetchConfiguration: FetchConfiguration{ + DataSource: userService, + PostProcessing: PostProcessingConfiguration{ + SelectResponseDataPath: []string{"data"}, + }, + }, + }, "query"), + SingleWithPath(&BatchEntityFetch{ + Input: BatchInput{ + Header: InputTemplate{ + Segments: []TemplateSegment{ + { + Data: []byte(`{"method":"POST","url":"http://localhost:4002","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations){query($representations: [_Any!]!){_entities(representations: $representations) { ... on Info { age } ... on Address { line1 }}}}}","variables":{"representations":[`), + SegmentType: StaticSegmentType, + }, + }, + }, + Items: []InputTemplate{ + { + Segments: []TemplateSegment{ + { + SegmentType: VariableSegmentType, + VariableKind: ResolvableObjectVariableKind, + Renderer: NewGraphQLVariableResolveRenderer(&Object{ + Fields: []*Field{ + { + Name: []byte("id"), + Value: &Integer{ + Path: []string{"id"}, + }, + OnTypeNames: [][]byte{[]byte("Info"), []byte("Address")}, + }, + { + Name: []byte("__typename"), + Value: &String{ + Path: []string{"__typename"}, + }, + OnTypeNames: [][]byte{[]byte("Info"), []byte("Address")}, + }, + }, + }), + }, + }, + }, + }, + Separator: InputTemplate{ + Segments: []TemplateSegment{ + { + Data: []byte(`,`), + SegmentType: StaticSegmentType, + }, + }, + }, + Footer: InputTemplate{ + Segments: []TemplateSegment{ + { + Data: []byte(`]}}}`), + SegmentType: StaticSegmentType, + }, + }, + }, + SkipNullItems: true, + SkipEmptyObjectItems: true, + SkipErrItems: true, + }, + DataSource: infoService, + PostProcessing: PostProcessingConfiguration{ + SelectResponseDataPath: []string{"data", "_entities"}, + }, + }, "user.infoOrAddress", ObjectPath("user"), ArrayPath("infoOrAddress")), + ), + Data: &Object{ + Fields: []*Field{ + { + Name: []byte("user"), + Value: &Object{ + Path: []string{"user"}, + Fields: []*Field{ + { + Name: []byte("name"), + Value: &String{ + Path: []string{"name"}, + }, + }, + { + Name: []byte("infoOrAddress"), + Value: &Array{ + Path: []string{"infoOrAddress"}, + Item: &Object{ + Fields: []*Field{ + { + Name: []byte("age"), + Value: &Integer{ + Path: []string{"age"}, + }, + OnTypeNames: [][]byte{[]byte("Info")}, + }, + { + Name: []byte("line1"), + Value: &String{ + Path: []string{"line1"}, + }, + OnTypeNames: [][]byte{[]byte("Address")}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, Context{ctx: context.Background(), Variables: nil}, `{"data":{"user":{"name":"Bill","infoOrAddress":[{"age":21},{"line1":"Munich"}]}}}` + })) + + t.Run("batching on union - all not matching items", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { + + userService := NewMockDataSource(ctrl) + userService.EXPECT(). + Load(gomock.Any(), gomock.Any(), gomock.AssignableToTypeOf(&bytes.Buffer{})). + DoAndReturn(func(ctx context.Context, input []byte, w io.Writer) (err error) { + actual := string(input) + expected := `{"method":"POST","url":"http://localhost:4001","body":{"query":"{ user { name infoOrAddress { ... on Info {id __typename} ... on Address {id __typename}}}}"}}` + assert.Equal(t, expected, actual) + pair := NewBufPair() + pair.Data.WriteString(`{"user":{"name":"Bill","infoOrAddress":[{"id":11,"__typename":"Whatever"},{"id": 55,"__typename":"Whatever"}]}}`) + return writeGraphqlResponse(pair, w, false) + }) + + infoService := NewMockDataSource(ctrl) + infoService.EXPECT(). + Load(gomock.Any(), gomock.Any(), gomock.AssignableToTypeOf(&bytes.Buffer{})). + Times(0) + + return &GraphQLResponse{ + Fetches: Sequence( + SingleWithPath(&SingleFetch{ + InputTemplate: InputTemplate{ + Segments: []TemplateSegment{ + { + Data: []byte(`{"method":"POST","url":"http://localhost:4001","body":{"query":"{ user { name infoOrAddress { ... on Info {id __typename} ... on Address {id __typename}}}}"}}`), + SegmentType: StaticSegmentType, + }, + }, + }, + FetchConfiguration: FetchConfiguration{ + DataSource: userService, + PostProcessing: PostProcessingConfiguration{ + SelectResponseDataPath: []string{"data"}, + }, + }, + }, "query"), + SingleWithPath(&BatchEntityFetch{ + Input: BatchInput{ + Header: InputTemplate{ + Segments: []TemplateSegment{ + { + Data: []byte(`{"method":"POST","url":"http://localhost:4002","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations){query($representations: [_Any!]!){_entities(representations: $representations) { ... on Info { age } ... on Address { line1 }}}}}","variables":{"representations":[`), + SegmentType: StaticSegmentType, + }, + }, + }, + Items: []InputTemplate{ + { + Segments: []TemplateSegment{ + { + SegmentType: VariableSegmentType, + VariableKind: ResolvableObjectVariableKind, + Renderer: NewGraphQLVariableResolveRenderer(&Object{ + Fields: []*Field{ + { + Name: []byte("id"), + Value: &Integer{ + Path: []string{"id"}, + }, + OnTypeNames: [][]byte{[]byte("Info"), []byte("Address")}, + }, + { + Name: []byte("__typename"), + Value: &String{ + Path: []string{"__typename"}, + }, + OnTypeNames: [][]byte{[]byte("Info"), []byte("Address")}, + }, + }, + }), + }, + }, + }, + }, + Separator: InputTemplate{ + Segments: []TemplateSegment{ + { + Data: []byte(`,`), + SegmentType: StaticSegmentType, + }, + }, + }, + Footer: InputTemplate{ + Segments: []TemplateSegment{ + { + Data: []byte(`]}}}`), + SegmentType: StaticSegmentType, + }, + }, + }, + SkipNullItems: true, + SkipEmptyObjectItems: true, + SkipErrItems: true, + }, + DataSource: infoService, + PostProcessing: PostProcessingConfiguration{ + SelectResponseDataPath: []string{"data", "_entities"}, + }, + }, "user.infoOrAddress", ObjectPath("user"), ArrayPath("infoOrAddress")), + ), + Data: &Object{ + Fields: []*Field{ + { + Name: []byte("user"), + Value: &Object{ + Path: []string{"user"}, + Fields: []*Field{ + { + Name: []byte("name"), + Value: &String{ + Path: []string{"name"}, + }, + }, + { + Name: []byte("infoOrAddress"), + Value: &Array{ + Path: []string{"infoOrAddress"}, + Item: &Object{ + Fields: []*Field{ + { + Name: []byte("age"), + Value: &Integer{ + Path: []string{"age"}, + }, + OnTypeNames: [][]byte{[]byte("Info")}, + }, + { + Name: []byte("line1"), + Value: &String{ + Path: []string{"line1"}, + }, + OnTypeNames: [][]byte{[]byte("Address")}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, Context{ctx: context.Background(), Variables: nil}, `{"data":{"user":{"name":"Bill","infoOrAddress":[{},{}]}}}` + })) + + t.Run("batching on a field", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { + userService := NewMockDataSource(ctrl) + userService.EXPECT(). + Load(gomock.Any(), gomock.Any(), gomock.AssignableToTypeOf(&bytes.Buffer{})). + DoAndReturn(func(ctx context.Context, input []byte, w io.Writer) (err error) { + actual := string(input) + expected := `{"method":"POST","url":"http://localhost:4001","body":{"query":"{ users { name info {id __typename}}}}"}}` + assert.Equal(t, expected, actual) + pair := NewBufPair() + pair.Data.WriteString(`{"users":[{"name":"Bill","info":{"id":11,"__typename":"Info"}},{"name":"John","info":{"id":12,"__typename":"Info"}},{"name":"Jane","info":{"id":13,"__typename":"Info"}}]}`) + return writeGraphqlResponse(pair, w, false) + }) + + infoService := NewMockDataSource(ctrl) + infoService.EXPECT(). + Load(gomock.Any(), gomock.Any(), gomock.AssignableToTypeOf(&bytes.Buffer{})). + DoAndReturn(func(ctx context.Context, input []byte, w io.Writer) (err error) { + actual := string(input) + expected := `{"method":"POST","url":"http://localhost:4002","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations) { ... on Info { age }}}}}","variables":{"representations":[{"id":11,"__typename":"Info"},{"id":12,"__typename":"Info"},{"id":13,"__typename":"Info"}]}}}` + assert.Equal(t, expected, actual) + pair := NewBufPair() + pair.Data.WriteString(`{"_entities":[{"age":21,"__typename":"Info"},{"age":22,"__typename":"Info"},{"age":23,"__typename":"Info"}]}`) + return writeGraphqlResponse(pair, w, false) + }) + + return &GraphQLResponse{ + Fetches: Sequence( + Single(&SingleFetch{ + InputTemplate: InputTemplate{ + Segments: []TemplateSegment{ + { + Data: []byte(`{"method":"POST","url":"http://localhost:4001","body":{"query":"{ users { name info {id __typename}}}}"}}`), + SegmentType: StaticSegmentType, + }, + }, + }, + FetchConfiguration: FetchConfiguration{ + DataSource: userService, + PostProcessing: PostProcessingConfiguration{ + SelectResponseDataPath: []string{"data"}, + }, + }, + }), + SingleWithPath(&BatchEntityFetch{ + Input: BatchInput{ + Header: InputTemplate{ + Segments: []TemplateSegment{ + { + Data: []byte(`{"method":"POST","url":"http://localhost:4002","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations) { ... on Info { age }}}}}","variables":{"representations":[`), + SegmentType: StaticSegmentType, + }, + }, + }, + Items: []InputTemplate{ + { + Segments: []TemplateSegment{ + { + SegmentType: VariableSegmentType, + VariableKind: ResolvableObjectVariableKind, + Renderer: NewGraphQLVariableResolveRenderer(&Object{ + Fields: []*Field{ + { + Name: []byte("id"), + Value: &Integer{ + Path: []string{"id"}, + }, + OnTypeNames: [][]byte{[]byte("Info")}, + }, + { + Name: []byte("__typename"), + Value: &String{ + Path: []string{"__typename"}, + }, + OnTypeNames: [][]byte{[]byte("Info")}, + }, + }, + }), + }, + }, + }, + }, + Separator: InputTemplate{ + Segments: []TemplateSegment{ + { + Data: []byte(`,`), + SegmentType: StaticSegmentType, + }, + }, + }, + Footer: InputTemplate{ + Segments: []TemplateSegment{ + { + Data: []byte(`]}}}`), + SegmentType: StaticSegmentType, + }, + }, + }, + SkipNullItems: true, + SkipEmptyObjectItems: true, + SkipErrItems: true, + }, + DataSource: infoService, + PostProcessing: PostProcessingConfiguration{ + SelectResponseDataPath: []string{"data", "_entities"}, + }, + }, "users.info", ArrayPath("users"), ObjectPath("info")), + ), + Data: &Object{ + Fields: []*Field{ + { + Name: []byte("users"), + Value: &Array{ + Path: []string{"users"}, + Item: &Object{ + Fields: []*Field{ + { + Name: []byte("name"), + Value: &String{ + Path: []string{"name"}, + }, + }, + { + Name: []byte("info"), + Value: &Object{ + Path: []string{"info"}, + Fields: []*Field{ + { + Name: []byte("age"), + Value: &Integer{ + Path: []string{"age"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, Context{ctx: context.Background(), Variables: nil}, `{"data":{"users":[{"name":"Bill","info":{"age":21}},{"name":"John","info":{"age":22}},{"name":"Jane","info":{"age":23}}]}}` + })) + + t.Run("batching with duplicates", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { + userService := NewMockDataSource(ctrl) + userService.EXPECT(). + Load(gomock.Any(), gomock.Any(), gomock.AssignableToTypeOf(&bytes.Buffer{})). + DoAndReturn(func(ctx context.Context, input []byte, w io.Writer) (err error) { + actual := string(input) + expected := `{"method":"POST","url":"http://localhost:4001","body":{"query":"{ users { name info {id __typename}}}}"}}` + assert.Equal(t, expected, actual) + pair := NewBufPair() + pair.Data.WriteString(`{"users":[{"name":"Bill","info":{"id":11,"__typename":"Info"}},{"name":"John","info":{"id":11,"__typename":"Info"}},{"name":"Jane","info":{"id":11,"__typename":"Info"}}]}`) + return writeGraphqlResponse(pair, w, false) + }) + + infoService := NewMockDataSource(ctrl) + infoService.EXPECT(). + Load(gomock.Any(), gomock.Any(), gomock.AssignableToTypeOf(&bytes.Buffer{})). + DoAndReturn(func(ctx context.Context, input []byte, w io.Writer) (err error) { + actual := string(input) + expected := `{"method":"POST","url":"http://localhost:4002","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations) { ... on Info { age }}}}}","variables":{"representations":[{"id":11,"__typename":"Info"}]}}}` + assert.Equal(t, expected, actual) + pair := NewBufPair() + pair.Data.WriteString(`{"_entities":[{"age":77,"__typename":"Info"}]}`) + return writeGraphqlResponse(pair, w, false) + }) + + return &GraphQLResponse{ + Fetches: Sequence( + Single(&SingleFetch{ + InputTemplate: InputTemplate{ + Segments: []TemplateSegment{ + { + Data: []byte(`{"method":"POST","url":"http://localhost:4001","body":{"query":"{ users { name info {id __typename}}}}"}}`), + SegmentType: StaticSegmentType, + }, + }, + }, + FetchConfiguration: FetchConfiguration{ + DataSource: userService, + PostProcessing: PostProcessingConfiguration{ + SelectResponseDataPath: []string{"data"}, + }, + }, + }), + SingleWithPath(&BatchEntityFetch{ + Input: BatchInput{ + Header: InputTemplate{ + Segments: []TemplateSegment{ + { + Data: []byte(`{"method":"POST","url":"http://localhost:4002","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations) { ... on Info { age }}}}}","variables":{"representations":[`), + SegmentType: StaticSegmentType, + }, + }, + }, + Items: []InputTemplate{ + { + Segments: []TemplateSegment{ + { + SegmentType: VariableSegmentType, + VariableKind: ResolvableObjectVariableKind, + Renderer: NewGraphQLVariableResolveRenderer(&Object{ + Fields: []*Field{ + { + Name: []byte("id"), + Value: &Integer{ + Path: []string{"id"}, + }, + OnTypeNames: [][]byte{[]byte("Info")}, + }, + { + Name: []byte("__typename"), + Value: &String{ + Path: []string{"__typename"}, + }, + OnTypeNames: [][]byte{[]byte("Info")}, + }, + }, + }), + }, + }, + }, + }, + Separator: InputTemplate{ + Segments: []TemplateSegment{ + { + Data: []byte(`,`), + SegmentType: StaticSegmentType, + }, + }, + }, + Footer: InputTemplate{ + Segments: []TemplateSegment{ + { + Data: []byte(`]}}}`), + SegmentType: StaticSegmentType, + }, + }, + }, + }, + DataSource: infoService, + PostProcessing: PostProcessingConfiguration{ + SelectResponseDataPath: []string{"data", "_entities"}, + }, + }, "users.info", ArrayPath("users"), ObjectPath("info")), + ), + Data: &Object{ + Fields: []*Field{ + { + Name: []byte("users"), + Value: &Array{ + Path: []string{"users"}, + Item: &Object{ + Fields: []*Field{ + { + Name: []byte("name"), + Value: &String{ + Path: []string{"name"}, + }, + }, + { + Name: []byte("info"), + Value: &Object{ + Path: []string{"info"}, + Fields: []*Field{ + { + Name: []byte("age"), + Value: &Integer{ + Path: []string{"age"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, Context{ctx: context.Background(), Variables: nil}, `{"data":{"users":[{"name":"Bill","info":{"age":77}},{"name":"John","info":{"age":77}},{"name":"Jane","info":{"age":77}}]}}` + })) + + t.Run("batching with null entry", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { + userService := NewMockDataSource(ctrl) + userService.EXPECT(). + Load(gomock.Any(), gomock.Any(), gomock.AssignableToTypeOf(&bytes.Buffer{})). + DoAndReturn(func(ctx context.Context, input []byte, w io.Writer) (err error) { + actual := string(input) + expected := `{"method":"POST","url":"http://localhost:4001","body":{"query":"{ users { name info {id __typename}}}}"}}` + assert.Equal(t, expected, actual) + pair := NewBufPair() + pair.Data.WriteString(`{"users":[{"name":"Bill","info":{"id":11,"__typename":"Info"}},{"name":"John","info":null},{"name":"Jane","info":{"id":13,"__typename":"Info"}}]}`) + return writeGraphqlResponse(pair, w, false) + }) + + infoService := NewMockDataSource(ctrl) + infoService.EXPECT(). + Load(gomock.Any(), gomock.Any(), gomock.AssignableToTypeOf(&bytes.Buffer{})). + DoAndReturn(func(ctx context.Context, input []byte, w io.Writer) (err error) { + actual := string(input) + expected := `{"method":"POST","url":"http://localhost:4002","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations) { ... on Info { age }}}}}","variables":{"representations":[{"id":11,"__typename":"Info"},{"id":13,"__typename":"Info"}]}}}` + assert.Equal(t, expected, actual) + pair := NewBufPair() + pair.Data.WriteString(`{"_entities":[{"age":21,"__typename":"Info"},{"age":23,"__typename":"Info"}]}`) + return writeGraphqlResponse(pair, w, false) + }) + + return &GraphQLResponse{ + Fetches: Sequence( + Single(&SingleFetch{ + InputTemplate: InputTemplate{ + Segments: []TemplateSegment{ + { + Data: []byte(`{"method":"POST","url":"http://localhost:4001","body":{"query":"{ users { name info {id __typename}}}}"}}`), + SegmentType: StaticSegmentType, + }, + }, + }, + FetchConfiguration: FetchConfiguration{ + DataSource: userService, + PostProcessing: PostProcessingConfiguration{ + SelectResponseDataPath: []string{"data"}, + }, + }, + }), + SingleWithPath(&BatchEntityFetch{ + Input: BatchInput{ + Header: InputTemplate{ + Segments: []TemplateSegment{ + { + Data: []byte(`{"method":"POST","url":"http://localhost:4002","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations) { ... on Info { age }}}}}","variables":{"representations":[`), + SegmentType: StaticSegmentType, + }, + }, + }, + Items: []InputTemplate{ + { + Segments: []TemplateSegment{ + { + SegmentType: VariableSegmentType, + VariableKind: ResolvableObjectVariableKind, + Renderer: NewGraphQLVariableResolveRenderer(&Object{ + Fields: []*Field{ + { + Name: []byte("id"), + Value: &Integer{ + Path: []string{"id"}, + }, + OnTypeNames: [][]byte{[]byte("Info")}, + }, + { + Name: []byte("__typename"), + Value: &String{ + Path: []string{"__typename"}, + }, + OnTypeNames: [][]byte{[]byte("Info")}, + }, + }, + }), + }, + }, + }, + }, + Separator: InputTemplate{ + Segments: []TemplateSegment{ + { + Data: []byte(`,`), + SegmentType: StaticSegmentType, + }, + }, + }, + Footer: InputTemplate{ + Segments: []TemplateSegment{ + { + Data: []byte(`]}}}`), + SegmentType: StaticSegmentType, + }, + }, + }, + SkipNullItems: true, + SkipEmptyObjectItems: true, + SkipErrItems: true, + }, + DataSource: infoService, + PostProcessing: PostProcessingConfiguration{ + SelectResponseDataPath: []string{"data", "_entities"}, + }, + }, "users.info", ArrayPath("users"), ObjectPath("info")), + ), + Data: &Object{ + Fields: []*Field{ + { + Name: []byte("users"), + Value: &Array{ + Path: []string{"users"}, + Item: &Object{ + Fields: []*Field{ + { + Name: []byte("name"), + Value: &String{ + Path: []string{"name"}, + }, + }, + { + Name: []byte("info"), + Value: &Object{ + Nullable: true, + Path: []string{"info"}, + Fields: []*Field{ + { + Name: []byte("age"), + Value: &Integer{ + Path: []string{"age"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, Context{ctx: context.Background(), Variables: nil}, `{"data":{"users":[{"name":"Bill","info":{"age":21}},{"name":"John","info":null},{"name":"Jane","info":{"age":23}}]}}` + })) + + t.Run("batching with all null entries", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { + userService := NewMockDataSource(ctrl) + userService.EXPECT(). + Load(gomock.Any(), gomock.Any(), gomock.AssignableToTypeOf(&bytes.Buffer{})). + DoAndReturn(func(ctx context.Context, input []byte, w io.Writer) (err error) { + actual := string(input) + expected := `{"method":"POST","url":"http://localhost:4001","body":{"query":"{ users { name info {id __typename}}}}"}}` + assert.Equal(t, expected, actual) + pair := NewBufPair() + pair.Data.WriteString(`{"users":[{"name":"Bill","info":null},{"name":"John","info":null},{"name":"Jane","info":null}]}`) + return writeGraphqlResponse(pair, w, false) + }) + + infoService := NewMockDataSource(ctrl) + infoService.EXPECT(). + Load(gomock.Any(), gomock.Any(), gomock.AssignableToTypeOf(&bytes.Buffer{})). + Times(0) + + return &GraphQLResponse{ + Fetches: Sequence( + Single(&SingleFetch{ + InputTemplate: InputTemplate{ + Segments: []TemplateSegment{ + { + Data: []byte(`{"method":"POST","url":"http://localhost:4001","body":{"query":"{ users { name info {id __typename}}}}"}}`), + SegmentType: StaticSegmentType, + }, + }, + }, + FetchConfiguration: FetchConfiguration{ + DataSource: userService, + PostProcessing: PostProcessingConfiguration{ + SelectResponseDataPath: []string{"data"}, + }, + }, + }), + SingleWithPath(&BatchEntityFetch{ + Input: BatchInput{ + Header: InputTemplate{ + Segments: []TemplateSegment{ + { + Data: []byte(`{"method":"POST","url":"http://localhost:4002","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations) { ... on Info { age }}}}}","variables":{"representations":[`), + SegmentType: StaticSegmentType, + }, + }, + }, + Items: []InputTemplate{ + { + Segments: []TemplateSegment{ + { + SegmentType: VariableSegmentType, + VariableKind: ResolvableObjectVariableKind, + Renderer: NewGraphQLVariableResolveRenderer(&Object{ + Fields: []*Field{ + { + Name: []byte("id"), + Value: &Integer{ + Path: []string{"id"}, + }, + OnTypeNames: [][]byte{[]byte("Info")}, + }, + { + Name: []byte("__typename"), + Value: &String{ + Path: []string{"__typename"}, + }, + OnTypeNames: [][]byte{[]byte("Info")}, + }, + }, + }), + }, + }, + }, + }, + Separator: InputTemplate{ + Segments: []TemplateSegment{ + { + Data: []byte(`,`), + SegmentType: StaticSegmentType, + }, + }, + }, + Footer: InputTemplate{ + Segments: []TemplateSegment{ + { + Data: []byte(`]}}}`), + SegmentType: StaticSegmentType, + }, + }, + }, + SkipNullItems: true, + SkipEmptyObjectItems: true, + SkipErrItems: true, + }, + DataSource: infoService, + PostProcessing: PostProcessingConfiguration{ + SelectResponseDataPath: []string{"data", "_entities"}, + }, + }, "users.info", ArrayPath("users"), ObjectPath("info")), + ), + Data: &Object{ + Fields: []*Field{ + { + Name: []byte("users"), + Value: &Array{ + Path: []string{"users"}, + Item: &Object{ + Fields: []*Field{ + { + Name: []byte("name"), + Value: &String{ + Path: []string{"name"}, + }, + }, + { + Name: []byte("info"), + Value: &Object{ + Nullable: true, + Path: []string{"info"}, + Fields: []*Field{ + { + Name: []byte("age"), + Value: &Integer{ + Path: []string{"age"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, Context{ctx: context.Background(), Variables: nil}, `{"data":{"users":[{"name":"Bill","info":null},{"name":"John","info":null},{"name":"Jane","info":null}]}}` + })) + + t.Run("batching with render error", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { + userService := NewMockDataSource(ctrl) + userService.EXPECT(). + Load(gomock.Any(), gomock.Any(), gomock.AssignableToTypeOf(&bytes.Buffer{})). + DoAndReturn(func(ctx context.Context, input []byte, w io.Writer) (err error) { + actual := string(input) + expected := `{"method":"POST","url":"http://localhost:4001","body":{"query":"{ users { name info {id __typename}}}}"}}` + assert.Equal(t, expected, actual) + pair := NewBufPair() + // render error - first item id is boolean + pair.Data.WriteString(`{"users":[{"name":"Bill","info":{"id":true,"__typename":"Info"}},{"name":"John","info":{"id":12,"__typename":"Info"}},{"name":"Jane","info":{"id":13,"__typename":"Info"}}]}`) + return writeGraphqlResponse(pair, w, false) + }) + + infoService := NewMockDataSource(ctrl) + infoService.EXPECT(). + Load(gomock.Any(), gomock.Any(), gomock.AssignableToTypeOf(&bytes.Buffer{})). + DoAndReturn(func(ctx context.Context, input []byte, w io.Writer) (err error) { + actual := string(input) + expected := `{"method":"POST","url":"http://localhost:4002","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations) { ... on Info { age }}}}}","variables":{"representations":[{"id":12,"__typename":"Info"},{"id":13,"__typename":"Info"}]}}}` + assert.Equal(t, expected, actual) + pair := NewBufPair() + pair.Data.WriteString(`{"_entities":[{"age":21,"__typename":"Info"},{"age":22,"__typename":"Info"}]}`) + return writeGraphqlResponse(pair, w, false) + }) + + return &GraphQLResponse{ + Fetches: Sequence( + Single(&SingleFetch{ + InputTemplate: InputTemplate{ + Segments: []TemplateSegment{ + { + Data: []byte(`{"method":"POST","url":"http://localhost:4001","body":{"query":"{ users { name info {id __typename}}}}"}}`), + SegmentType: StaticSegmentType, + }, + }, + }, + FetchConfiguration: FetchConfiguration{ + DataSource: userService, + PostProcessing: PostProcessingConfiguration{ + SelectResponseDataPath: []string{"data"}, + }, + }, + }), + SingleWithPath(&BatchEntityFetch{ + Input: BatchInput{ + Header: InputTemplate{ + Segments: []TemplateSegment{ + { + Data: []byte(`{"method":"POST","url":"http://localhost:4002","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations) { ... on Info { age }}}}}","variables":{"representations":[`), + SegmentType: StaticSegmentType, + }, + }, + }, + Items: []InputTemplate{ + { + Segments: []TemplateSegment{ + { + SegmentType: VariableSegmentType, + VariableKind: ResolvableObjectVariableKind, + Renderer: NewGraphQLVariableResolveRenderer(&Object{ + Fields: []*Field{ + { + Name: []byte("id"), + Value: &Integer{ + Path: []string{"id"}, + }, + OnTypeNames: [][]byte{[]byte("Info")}, + }, + { + Name: []byte("__typename"), + Value: &String{ + Path: []string{"__typename"}, + }, + OnTypeNames: [][]byte{[]byte("Info")}, + }, + }, + }), + }, + }, + }, + }, + Separator: InputTemplate{ + Segments: []TemplateSegment{ + { + Data: []byte(`,`), + SegmentType: StaticSegmentType, + }, + }, + }, + Footer: InputTemplate{ + Segments: []TemplateSegment{ + { + Data: []byte(`]}}}`), + SegmentType: StaticSegmentType, + }, + }, + }, + SkipNullItems: true, + SkipEmptyObjectItems: true, + SkipErrItems: true, + }, + DataSource: infoService, + PostProcessing: PostProcessingConfiguration{ + SelectResponseDataPath: []string{"data", "_entities"}, + }, + }, "users.info", ArrayPath("users"), ObjectPath("info")), + ), + Data: &Object{ + Fields: []*Field{ + { + Name: []byte("users"), + Value: &Array{ + Path: []string{"users"}, + Item: &Object{ + Fields: []*Field{ + { + Name: []byte("name"), + Value: &String{ + Path: []string{"name"}, + }, + }, + { + Name: []byte("info"), + Value: &Object{ + Path: []string{"info"}, + Fields: []*Field{ + { + Name: []byte("age"), + Value: &Integer{ + Path: []string{"age"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, Context{ctx: context.Background(), Variables: nil}, `{"errors":[{"message":"Cannot return null for non-nullable field 'Query.users.info.age'.","path":["users",0,"info","age"]}],"data":null}` + })) + }) + + t.Run("single entity fetch", func(t *testing.T) { + t.Run("all data", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { + userService := NewMockDataSource(ctrl) + userService.EXPECT(). + Load(gomock.Any(), gomock.Any(), gomock.AssignableToTypeOf(&bytes.Buffer{})). + DoAndReturn(func(ctx context.Context, input []byte, w io.Writer) (err error) { + actual := string(input) + expected := `{"method":"POST","url":"http://localhost:4001","body":{"query":"{ user { name info {id __typename}}}}"}}` + assert.Equal(t, expected, actual) + pair := NewBufPair() + pair.Data.WriteString(`{"user":{"name":"Bill","info":{"id":11,"__typename":"Info"}}}`) + return writeGraphqlResponse(pair, w, false) + }) + + infoService := NewMockDataSource(ctrl) + infoService.EXPECT(). + Load(gomock.Any(), gomock.Any(), gomock.AssignableToTypeOf(&bytes.Buffer{})). + DoAndReturn(func(ctx context.Context, input []byte, w io.Writer) (err error) { + actual := string(input) + expected := `{"method":"POST","url":"http://localhost:4002","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations) { ... on Info { age }}}}}","variables":{"representations":[{"id":11,"__typename":"Info"}]}}}` + assert.Equal(t, expected, actual) + pair := NewBufPair() + pair.Data.WriteString(`{"_entities":[{"age":21,"__typename":"Info"}]}`) + return writeGraphqlResponse(pair, w, false) + }) + + return &GraphQLResponse{ + Fetches: Sequence( + Single(&SingleFetch{ + InputTemplate: InputTemplate{ + Segments: []TemplateSegment{ + { + Data: []byte(`{"method":"POST","url":"http://localhost:4001","body":{"query":"{ user { name info {id __typename}}}}"}}`), + SegmentType: StaticSegmentType, + }, + }, + }, + FetchConfiguration: FetchConfiguration{ + DataSource: userService, + PostProcessing: PostProcessingConfiguration{ + SelectResponseDataPath: []string{"data"}, + }, + }, + }), + SingleWithPath(&EntityFetch{ + FetchDependencies: FetchDependencies{ + FetchID: 1, + DependsOnFetchIDs: []int{0}, + }, + Input: EntityInput{ + Header: InputTemplate{ + Segments: []TemplateSegment{ + { + Data: []byte(`{"method":"POST","url":"http://localhost:4002","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations) { ... on Info { age }}}}}","variables":{"representations":[`), + SegmentType: StaticSegmentType, + }, + }, + }, + Item: InputTemplate{ + Segments: []TemplateSegment{ + { + SegmentType: VariableSegmentType, + VariableKind: ResolvableObjectVariableKind, + Renderer: NewGraphQLVariableResolveRenderer(&Object{ + Fields: []*Field{ + { + Name: []byte("id"), + Value: &Integer{ + Path: []string{"id"}, + }, + OnTypeNames: [][]byte{[]byte("Info")}, + }, + { + Name: []byte("__typename"), + Value: &String{ + Path: []string{"__typename"}, + }, + OnTypeNames: [][]byte{[]byte("Info")}, + }, + }, + }), + }, + }, + }, + Footer: InputTemplate{ + Segments: []TemplateSegment{ + { + Data: []byte(`]}}}`), + SegmentType: StaticSegmentType, + }, + }, + }, + SkipErrItem: true, + }, + DataSource: infoService, + PostProcessing: PostProcessingConfiguration{ + SelectResponseDataPath: []string{"data", "_entities", "0"}, + }, + }, "user.info", ObjectPath("user"), ObjectPath("info")), + ), + Data: &Object{ + Fields: []*Field{ + { + Name: []byte("user"), + Value: &Object{ + Path: []string{"user"}, + Fields: []*Field{ + { + Name: []byte("name"), + Value: &String{ + Path: []string{"name"}, + }, + }, + { + Name: []byte("info"), + Value: &Object{ + Path: []string{"info"}, + Fields: []*Field{ + { + Name: []byte("age"), + Value: &Integer{ + Path: []string{"age"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, Context{ctx: context.Background(), Variables: nil}, `{"data":{"user":{"name":"Bill","info":{"age":21}}}}` + })) + + t.Run("null info data", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { + userService := NewMockDataSource(ctrl) + userService.EXPECT(). + Load(gomock.Any(), gomock.Any(), gomock.AssignableToTypeOf(&bytes.Buffer{})). + DoAndReturn(func(ctx context.Context, input []byte, w io.Writer) (err error) { + actual := string(input) + expected := `{"method":"POST","url":"http://localhost:4001","body":{"query":"{ user { name info {id __typename}}}}"}}` + assert.Equal(t, expected, actual) + pair := NewBufPair() + pair.Data.WriteString(`{"user":{"name":"Bill","info":null}}`) + return writeGraphqlResponse(pair, w, false) + }) + + infoService := NewMockDataSource(ctrl) + infoService.EXPECT(). + Load(gomock.Any(), gomock.Any(), gomock.AssignableToTypeOf(&bytes.Buffer{})). + Times(0) + + return &GraphQLResponse{ + Fetches: Sequence( + Single(&SingleFetch{ + InputTemplate: InputTemplate{ + Segments: []TemplateSegment{ + { + Data: []byte(`{"method":"POST","url":"http://localhost:4001","body":{"query":"{ user { name info {id __typename}}}}"}}`), + SegmentType: StaticSegmentType, + }, + }, + }, + FetchConfiguration: FetchConfiguration{ + DataSource: userService, + PostProcessing: PostProcessingConfiguration{ + SelectResponseDataPath: []string{"data"}, + }, + }, + }), + SingleWithPath(&EntityFetch{ + FetchDependencies: FetchDependencies{ + FetchID: 1, + DependsOnFetchIDs: []int{0}, + }, + Input: EntityInput{ + Header: InputTemplate{ + Segments: []TemplateSegment{ + { + Data: []byte(`{"method":"POST","url":"http://localhost:4002","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations) { ... on Info { age }}}}}","variables":{"representations":[`), + SegmentType: StaticSegmentType, + }, + }, + }, + Item: InputTemplate{ + Segments: []TemplateSegment{ + { + SegmentType: VariableSegmentType, + VariableKind: ResolvableObjectVariableKind, + Renderer: NewGraphQLVariableResolveRenderer(&Object{ + Fields: []*Field{ + { + Name: []byte("id"), + Value: &Integer{ + Path: []string{"id"}, + }, + OnTypeNames: [][]byte{[]byte("Info")}, + }, + { + Name: []byte("__typename"), + Value: &String{ + Path: []string{"__typename"}, + }, + OnTypeNames: [][]byte{[]byte("Info")}, + }, + }, + }), + }, + }, + }, + Footer: InputTemplate{ + Segments: []TemplateSegment{ + { + Data: []byte(`]}}}`), + SegmentType: StaticSegmentType, + }, + }, + }, + SkipErrItem: true, + }, + DataSource: infoService, + PostProcessing: PostProcessingConfiguration{ + SelectResponseDataPath: []string{"data", "_entities", "0"}, + }, + }, "user.info", ObjectPath("user"), ObjectPath("info")), + ), + Data: &Object{ + Fields: []*Field{ + { + Name: []byte("user"), + Value: &Object{ + Path: []string{"user"}, + Fields: []*Field{ + { + Name: []byte("name"), + Value: &String{ + Path: []string{"name"}, + }, + }, + { + Name: []byte("info"), + Value: &Object{ + Nullable: true, + Path: []string{"info"}, + Fields: []*Field{ + { + Name: []byte("age"), + Value: &Integer{ + Path: []string{"age"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, Context{ctx: context.Background(), Variables: nil}, `{"data":{"user":{"name":"Bill","info":null}}}` + })) + + t.Run("wrong type data", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { + userService := NewMockDataSource(ctrl) + userService.EXPECT(). + Load(gomock.Any(), gomock.Any(), gomock.AssignableToTypeOf(&bytes.Buffer{})). + DoAndReturn(func(ctx context.Context, input []byte, w io.Writer) (err error) { + actual := string(input) + expected := `{"method":"POST","url":"http://localhost:4001","body":{"query":"{ user { name info {id __typename}}}}"}}` + assert.Equal(t, expected, actual) + pair := NewBufPair() + pair.Data.WriteString(`{"user":{"name":"Bill","info":{"id":false,"__typename":"Info"}}}`) + return writeGraphqlResponse(pair, w, false) + }) + + infoService := NewMockDataSource(ctrl) + infoService.EXPECT(). + Load(gomock.Any(), gomock.Any(), gomock.AssignableToTypeOf(&bytes.Buffer{})). + Times(0) + + return &GraphQLResponse{ + Fetches: Sequence( + Single(&SingleFetch{ + InputTemplate: InputTemplate{ + Segments: []TemplateSegment{ + { + Data: []byte(`{"method":"POST","url":"http://localhost:4001","body":{"query":"{ user { name info {id __typename}}}}"}}`), + SegmentType: StaticSegmentType, + }, + }, + }, + FetchConfiguration: FetchConfiguration{ + DataSource: userService, + PostProcessing: PostProcessingConfiguration{ + SelectResponseDataPath: []string{"data"}, + }, + }, + }), + SingleWithPath(&EntityFetch{ + FetchDependencies: FetchDependencies{ + FetchID: 1, + DependsOnFetchIDs: []int{0}, + }, + Input: EntityInput{ + Header: InputTemplate{ + Segments: []TemplateSegment{ + { + Data: []byte(`{"method":"POST","url":"http://localhost:4002","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations) { ... on Info { age }}}}}","variables":{"representations":[`), + SegmentType: StaticSegmentType, + }, + }, + }, + Item: InputTemplate{ + Segments: []TemplateSegment{ + { + SegmentType: VariableSegmentType, + VariableKind: ResolvableObjectVariableKind, + Renderer: NewGraphQLVariableResolveRenderer(&Object{ + Fields: []*Field{ + { + Name: []byte("id"), + Value: &Integer{ + Path: []string{"id"}, + }, + OnTypeNames: [][]byte{[]byte("Info")}, + }, + { + Name: []byte("__typename"), + Value: &String{ + Path: []string{"__typename"}, + }, + OnTypeNames: [][]byte{[]byte("Info")}, + }, + }, + }), + }, + }, + }, + Footer: InputTemplate{ + Segments: []TemplateSegment{ + { + Data: []byte(`]}}}`), + SegmentType: StaticSegmentType, + }, + }, + }, + SkipErrItem: true, + }, + DataSource: infoService, + PostProcessing: PostProcessingConfiguration{ + SelectResponseDataPath: []string{"data", "_entities", "0"}, + }, + }, "user.info", ObjectPath("user"), ObjectPath("info")), + ), + Data: &Object{ + Fields: []*Field{ + { + Name: []byte("user"), + Value: &Object{ + Path: []string{"user"}, + Fields: []*Field{ + { + Name: []byte("name"), + Value: &String{ + Path: []string{"name"}, + }, + }, + { + Name: []byte("info"), + Value: &Object{ + Nullable: true, + Path: []string{"info"}, + Fields: []*Field{ + { + Name: []byte("age"), + Value: &Integer{ + Path: []string{"age"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, Context{ctx: context.Background(), Variables: nil}, `{"errors":[{"message":"Cannot return null for non-nullable field 'Query.user.info.age'.","path":["user","info","age"]}],"data":{"user":{"name":"Bill","info":null}}}` + })) + + t.Run("not matching type data", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { + userService := NewMockDataSource(ctrl) + userService.EXPECT(). + Load(gomock.Any(), gomock.Any(), gomock.AssignableToTypeOf(&bytes.Buffer{})). + DoAndReturn(func(ctx context.Context, input []byte, w io.Writer) (err error) { + actual := string(input) + expected := `{"method":"POST","url":"http://localhost:4001","body":{"query":"{ user { name info {id __typename}}}}"}}` + assert.Equal(t, expected, actual) + pair := NewBufPair() + pair.Data.WriteString(`{"user":{"name":"Bill","info":{"id":1,"__typename":"Whatever"}}}`) + return writeGraphqlResponse(pair, w, false) + }) + + infoService := NewMockDataSource(ctrl) + infoService.EXPECT(). + Load(gomock.Any(), gomock.Any(), gomock.AssignableToTypeOf(&bytes.Buffer{})). + Times(0) + + return &GraphQLResponse{ + Fetches: Sequence( + Single(&SingleFetch{ + InputTemplate: InputTemplate{ + Segments: []TemplateSegment{ + { + Data: []byte(`{"method":"POST","url":"http://localhost:4001","body":{"query":"{ user { name info {id __typename}}}}"}}`), + SegmentType: StaticSegmentType, + }, + }, + }, + FetchConfiguration: FetchConfiguration{ + DataSource: userService, + PostProcessing: PostProcessingConfiguration{ + SelectResponseDataPath: []string{"data"}, + }, + }, + }), + SingleWithPath(&EntityFetch{ + FetchDependencies: FetchDependencies{ + FetchID: 1, + DependsOnFetchIDs: []int{0}, + }, + Input: EntityInput{ + Header: InputTemplate{ + Segments: []TemplateSegment{ + { + Data: []byte(`{"method":"POST","url":"http://localhost:4002","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations) { ... on Info { age }}}}}","variables":{"representations":[`), + SegmentType: StaticSegmentType, + }, + }, + }, + Item: InputTemplate{ + Segments: []TemplateSegment{ + { + SegmentType: VariableSegmentType, + VariableKind: ResolvableObjectVariableKind, + Renderer: NewGraphQLVariableResolveRenderer(&Object{ + Fields: []*Field{ + { + Name: []byte("id"), + Value: &Integer{ + Path: []string{"id"}, + }, + OnTypeNames: [][]byte{[]byte("Info")}, + }, + { + Name: []byte("__typename"), + Value: &String{ + Path: []string{"__typename"}, + }, + OnTypeNames: [][]byte{[]byte("Info")}, + }, + }, + }), + }, + }, + }, + Footer: InputTemplate{ + Segments: []TemplateSegment{ + { + Data: []byte(`]}}}`), + SegmentType: StaticSegmentType, + }, + }, + }, + SkipErrItem: true, + }, + DataSource: infoService, + PostProcessing: PostProcessingConfiguration{ + SelectResponseDataPath: []string{"data", "_entities", "0"}, + }, + }, "user.info", ObjectPath("user"), ObjectPath("info")), + ), + Data: &Object{ + Fields: []*Field{ + { + Name: []byte("user"), + Value: &Object{ + Path: []string{"user"}, + Fields: []*Field{ + { + Name: []byte("name"), + Value: &String{ + Path: []string{"name"}, + }, + }, + { + Name: []byte("info"), + Value: &Object{ + Nullable: true, + Path: []string{"info"}, + Fields: []*Field{ + { + Name: []byte("age"), + Value: &Integer{ + Path: []string{"age"}, + }, + OnTypeNames: [][]byte{[]byte("Info")}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, Context{ctx: context.Background(), Variables: nil}, `{"data":{"user":{"name":"Bill","info":{}}}}` + })) + }) + }) + t.Run("serial fetch", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) { user := mockedDS(t, ctrl, diff --git a/v2/pkg/engine/resolve/simple_resolver.go b/v2/pkg/engine/resolve/simple_resolver.go deleted file mode 100644 index d5c08b65d..000000000 --- a/v2/pkg/engine/resolve/simple_resolver.go +++ /dev/null @@ -1,307 +0,0 @@ -package resolve - -import ( - "bytes" - "errors" - "fmt" - - "github.com/buger/jsonparser" - - "github.com/wundergraph/graphql-go-tools/v2/pkg/fastbuffer" - "github.com/wundergraph/graphql-go-tools/v2/pkg/pool" -) - -type SimpleResolver struct { -} - -func NewSimpleResolver() *SimpleResolver { - return &SimpleResolver{} -} - -func (r *SimpleResolver) resolveNode(node Node, data []byte, buf *fastbuffer.FastBuffer) (err error) { - switch n := node.(type) { - case *Object: - return r.resolveObject(n, data, buf) - case *Array: - return r.resolveArray(n, data, buf) - case *Null: - r.resolveNull(buf) - return - case *String: - return r.resolveString(n, data, buf) - case *StaticString: - return r.resolveStaticString(n, data, buf) - case *Boolean: - return r.resolveBoolean(n, data, buf) - case *Integer: - return r.resolveInteger(n, data, buf) - case *Float: - return r.resolveFloat(n, data, buf) - case *Scalar: - return r.resolveScalar(n, data, buf) - case *EmptyObject: - r.resolveEmptyObject(buf) - return - case *EmptyArray: - r.resolveEmptyArray(buf) - return - default: - return - } -} - -func (r *SimpleResolver) resolveObject(object *Object, data []byte, resolveBuf *fastbuffer.FastBuffer) (err error) { - if len(object.Path) != 0 { - data, _, _, _ = jsonparser.Get(data, object.Path...) - - if len(data) == 0 || bytes.Equal(data, null) { - if object.Nullable { - // write empty object to resolve buffer - r.resolveNull(resolveBuf) - return - } - - return errNonNullableFieldValueIsNull - } - } - - objectBuf := pool.FastBuffer.Get() - defer pool.FastBuffer.Put(objectBuf) - - typeNameSkip := false - first := true - for i := range object.Fields { - if object.Fields[i].OnTypeNames != nil { - typeName, _, _, _ := jsonparser.Get(data, "__typename") - hasMatch := false - for _, onTypeName := range object.Fields[i].OnTypeNames { - if bytes.Equal(typeName, onTypeName) { - hasMatch = true - break - } - } - if !hasMatch { - typeNameSkip = true - continue - } - } - - fieldData := data - if first { - objectBuf.WriteBytes(lBrace) - first = false - } else { - objectBuf.WriteBytes(comma) - } - objectBuf.WriteBytes(quote) - objectBuf.WriteBytes(object.Fields[i].Name) - objectBuf.WriteBytes(quote) - objectBuf.WriteBytes(colon) - err = r.resolveNode(object.Fields[i].Value, fieldData, objectBuf) - if err != nil { - if errors.Is(err, errNonNullableFieldValueIsNull) { - objectBuf.Reset() - - if object.Nullable { - // write empty object to resolve buffer - r.resolveNull(resolveBuf) - return nil - } - } - return err - } - } - - if first { - if typeNameSkip { - r.resolveEmptyObject(resolveBuf) - return - } - if !object.Nullable { - return errNonNullableFieldValueIsNull - } - // write empty object to resolve buffer - r.resolveNull(resolveBuf) - return - } - objectBuf.WriteBytes(rBrace) - - // write full object to resolve buffer - resolveBuf.WriteBytes(objectBuf.Bytes()) - - return -} - -func (r *SimpleResolver) resolveArray(array *Array, data []byte, resolveBuf *fastbuffer.FastBuffer) (err error) { - if len(array.Path) != 0 { - data, _, _, _ = jsonparser.Get(data, array.Path...) - } - - if bytes.Equal(data, emptyArray) { - r.resolveEmptyArray(resolveBuf) - return - } - - var arrayItems [][]byte - - _, _ = jsonparser.ArrayEach(data, func(value []byte, dataType jsonparser.ValueType, offset int, err error) { - if err == nil && dataType == jsonparser.String { - value = data[offset-2 : offset+len(value)] // add quotes to string values - } - - arrayItems = append(arrayItems, value) - }) - - if len(arrayItems) == 0 { - if !array.Nullable { - r.resolveEmptyArray(resolveBuf) - return errNonNullableFieldValueIsNull - } - r.resolveNull(resolveBuf) - return nil - } - - arrayBuf := pool.FastBuffer.Get() - defer pool.FastBuffer.Put(arrayBuf) - - hasPreviousItem := false - - arrayBuf.WriteBytes(lBrack) - for i := range arrayItems { - if hasPreviousItem { - arrayBuf.WriteBytes(comma) - } - err = r.resolveNode(array.Item, arrayItems[i], arrayBuf) - if err != nil { - if errors.Is(err, errNonNullableFieldValueIsNull) { - if !array.Nullable { - return err - } - r.resolveNull(resolveBuf) - return nil - } - return err - } - if !hasPreviousItem { - hasPreviousItem = true - } - } - arrayBuf.WriteBytes(rBrack) - - resolveBuf.WriteBytes(arrayBuf.Bytes()) - return nil -} - -func (r *SimpleResolver) resolveNull(b *fastbuffer.FastBuffer) { - b.WriteBytes(null) -} - -func (r *SimpleResolver) resolveInteger(integer *Integer, data []byte, integerBuf *fastbuffer.FastBuffer) error { - value, dataType, _, err := jsonparser.Get(data, integer.Path...) - if err != nil || dataType != jsonparser.Number { - if !integer.Nullable { - return errNonNullableFieldValueIsNull - } - r.resolveNull(integerBuf) - return nil - } - integerBuf.WriteBytes(value) - return nil -} - -func (r *SimpleResolver) resolveFloat(floatValue *Float, data []byte, floatBuf *fastbuffer.FastBuffer) error { - value, dataType, _, err := jsonparser.Get(data, floatValue.Path...) - if err != nil || dataType != jsonparser.Number { - if !floatValue.Nullable { - return errNonNullableFieldValueIsNull - } - r.resolveNull(floatBuf) - return nil - } - floatBuf.WriteBytes(value) - return nil -} - -func (r *SimpleResolver) resolveBoolean(boolean *Boolean, data []byte, booleanBuf *fastbuffer.FastBuffer) error { - value, valueType, _, err := jsonparser.Get(data, boolean.Path...) - if err != nil || valueType != jsonparser.Boolean { - if !boolean.Nullable { - return errNonNullableFieldValueIsNull - } - r.resolveNull(booleanBuf) - return nil - } - booleanBuf.WriteBytes(value) - return nil -} - -func (r *SimpleResolver) resolveString(str *String, data []byte, stringBuf *fastbuffer.FastBuffer) error { - var ( - value []byte - valueType jsonparser.ValueType - err error - ) - - value, valueType, _, err = jsonparser.Get(data, str.Path...) - if err != nil || valueType != jsonparser.String { - if value != nil && valueType != jsonparser.Null { - return fmt.Errorf("invalid value type '%s' for path %s, expecting string, got: %v", valueType, str.Path, string(value)) - } - if !str.Nullable { - return errNonNullableFieldValueIsNull - } - r.resolveNull(stringBuf) - return nil - } - - if value == nil { - if !str.Nullable { - return errNonNullableFieldValueIsNull - } - r.resolveNull(stringBuf) - return nil - } - - // value = r.renameTypeName(str, value) - - stringBuf.WriteBytes(quote) - stringBuf.WriteBytes(value) - stringBuf.WriteBytes(quote) - return nil -} - -func (r *SimpleResolver) resolveStaticString(str *StaticString, data []byte, stringBuf *fastbuffer.FastBuffer) error { - stringBuf.WriteBytes(quote) - stringBuf.WriteBytes([]byte(str.Value)) - stringBuf.WriteBytes(quote) - return nil -} - -func (r *SimpleResolver) resolveEmptyArray(b *fastbuffer.FastBuffer) { - b.WriteBytes(lBrack) - b.WriteBytes(rBrack) -} - -func (r *SimpleResolver) resolveEmptyObject(b *fastbuffer.FastBuffer) { - b.WriteBytes(lBrace) - b.WriteBytes(rBrace) -} - -func (r *SimpleResolver) resolveScalar(scalarValue *Scalar, data []byte, scalarBuf *fastbuffer.FastBuffer) error { - value, valueType, _, err := jsonparser.Get(data, scalarValue.Path...) - switch { - case err != nil, valueType == jsonparser.Null: - if !scalarValue.Nullable { - return errNonNullableFieldValueIsNull - } - r.resolveNull(scalarBuf) - return nil - case valueType == jsonparser.String: - scalarBuf.WriteBytes(quote) - scalarBuf.WriteBytes(value) - scalarBuf.WriteBytes(quote) - default: - scalarBuf.WriteBytes(value) - } - return nil -} diff --git a/v2/pkg/engine/resolve/variables_renderer.go b/v2/pkg/engine/resolve/variables_renderer.go index 599acc72e..bbc68af09 100644 --- a/v2/pkg/engine/resolve/variables_renderer.go +++ b/v2/pkg/engine/resolve/variables_renderer.go @@ -327,20 +327,14 @@ func (c *CSVVariableRenderer) RenderVariable(_ context.Context, data *astjson.Va } type GraphQLVariableResolveRenderer struct { - Kind string - Node Node - isArray bool - isObject bool - isNullable bool + Kind string + Node Node } func NewGraphQLVariableResolveRenderer(node Node) *GraphQLVariableResolveRenderer { return &GraphQLVariableResolveRenderer{ - Kind: VariableRendererKindGraphqlResolve, - Node: node, - isArray: node.NodeKind() == NodeKindArray, - isObject: node.NodeKind() == NodeKindObject, - isNullable: node.NodeNullable(), + Kind: VariableRendererKindGraphqlResolve, + Node: node, } } @@ -361,35 +355,15 @@ func (g *GraphQLVariableResolveRenderer) getResolvable() *Resolvable { } func (g *GraphQLVariableResolveRenderer) putResolvable(r *Resolvable) { - r.Reset(256) + r.Reset() _graphQLVariableResolveRendererPool.Put(r) } func (g *GraphQLVariableResolveRenderer) RenderVariable(ctx context.Context, data *astjson.Value, out io.Writer) error { - r := g.getResolvable() defer g.putResolvable(r) - if g.isObject { - _, _ = out.Write(literal.LBRACE) - } - - err := r.ResolveNode(g.Node, data, out) - if err != nil { - if g.isNullable { - if g.isObject { - _, _ = out.Write(literal.RBRACE) - return nil - } - _, _ = out.Write(literal.NULL) - return nil - } - return err - } - - if g.isObject { - _, _ = out.Write(literal.RBRACE) - } - - return nil + // make depth 1 to not render as the root object fields - we need braces + r.depth = 1 + return r.ResolveNode(g.Node, data, out) }