From 6e82f0cf6d4ff0d377c63770e787e90825295aae Mon Sep 17 00:00:00 2001 From: spetrunin Date: Tue, 20 Feb 2024 21:19:49 +0200 Subject: [PATCH 01/15] draft fetch extraction postprocess --- v2/pkg/engine/postprocess/create_fetches.go | 195 +++++++++++ v2/pkg/engine/postprocess/extract_fetches.go | 15 + .../postprocess/extract_fetches_test.go | 305 ++++++++++++++++++ v2/pkg/engine/postprocess/fetch_finder.go | 48 +++ v2/pkg/engine/postprocess/plan_visitor.go | 229 +++++++++++++ v2/pkg/engine/resolve/node_array.go | 4 +- v2/pkg/engine/resolve/response.go | 1 + 7 files changed, 795 insertions(+), 2 deletions(-) create mode 100644 v2/pkg/engine/postprocess/create_fetches.go create mode 100644 v2/pkg/engine/postprocess/extract_fetches.go create mode 100644 v2/pkg/engine/postprocess/extract_fetches_test.go create mode 100644 v2/pkg/engine/postprocess/fetch_finder.go create mode 100644 v2/pkg/engine/postprocess/plan_visitor.go diff --git a/v2/pkg/engine/postprocess/create_fetches.go b/v2/pkg/engine/postprocess/create_fetches.go new file mode 100644 index 000000000..04425bc7f --- /dev/null +++ b/v2/pkg/engine/postprocess/create_fetches.go @@ -0,0 +1,195 @@ +package postprocess + +import ( + "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/plan" + "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/resolve" +) + +type CreateFetchesCopy struct { + *PlanWalker + + data *resolve.Object + + currentObjects []*resolve.Object + currentFields []*resolve.Field + + fieldForPath map[string]*resolve.Field + objectForPath map[string]*resolve.Object + fieldHasFetch map[*resolve.Field]struct{} +} + +func NewFetchesCopier(fieldHasFetch map[*resolve.Field]struct{}) *CreateFetchesCopy { + e := &CreateFetchesCopy{ + PlanWalker: &PlanWalker{}, + objectForPath: make(map[string]*resolve.Object), + fieldForPath: make(map[string]*resolve.Field), + fieldHasFetch: fieldHasFetch, + } + + e.registerObjectVisitor(e) + e.registerArrayVisitor(e) + e.registerFieldVisitor(e) + + return e +} + +func (e *CreateFetchesCopy) Process(pre plan.Plan) plan.Plan { + switch t := pre.(type) { + case *plan.SynchronousResponsePlan: + e.Walk(t.Response.Data, t.Response.Info) + + t.Response.FetchData = e.data + + case *plan.SubscriptionResponsePlan: + e.Walk(t.Response.Response.Data, t.Response.Response.Info) + + t.Response.Response.FetchData = e.data + } + return pre +} + +func (e *CreateFetchesCopy) EnterArray(array *resolve.Array) { + currentField := e.currentFields[len(e.currentFields)-1] + + switch currentField.Value.(type) { + case *resolve.Object: + panic("not implemented") + case *resolve.Array: + // nothing to do + case nil: + cloned := e.CloneArray(array) + currentField.Value = cloned + } + +} + +func (e *CreateFetchesCopy) CloneArray(a *resolve.Array) *resolve.Array { + return &resolve.Array{ + Nullable: a.Nullable, + Path: a.Path, + } +} + +func (e *CreateFetchesCopy) LeaveArray(array *resolve.Array) { + +} + +func (e *CreateFetchesCopy) EnterObject(object *resolve.Object) { + if e.data == nil { + e.data = e.CloneObject(object) + e.currentObjects = append(e.currentObjects, e.data) + object.Fetch = nil + return + } + + currentPath := e.renderPath() + + objectForPath, ok := e.objectForPath[currentPath] + if !ok { + cloned := e.CloneObject(object) + e.objectForPath[currentPath] = cloned + e.currentObjects = append(e.currentObjects, cloned) + e.setFieldObject(cloned) + object.Fetch = nil + return + } + + if object.Fetch != nil { + objectForPath.Fetch = e.appendFetch(objectForPath.Fetch, object.Fetch) + object.Fetch = nil + } + + e.currentObjects = append(e.currentObjects, objectForPath) +} + +func (e *CreateFetchesCopy) setFieldObject(o *resolve.Object) { + currentField := e.currentFields[len(e.currentFields)-1] + + if currentField.Value == nil { + currentField.Value = o + return + } + + switch t := currentField.Value.(type) { + case *resolve.Object: + panic("not implemented") + case *resolve.Array: + t.Item = o + default: + panic("not implemented") + } +} + +func (e *CreateFetchesCopy) appendFetch(existing resolve.Fetch, additional resolve.Fetch) resolve.Fetch { + switch t := existing.(type) { + case *resolve.SingleFetch: + switch at := additional.(type) { + case *resolve.SingleFetch: + return &resolve.MultiFetch{ + Fetches: []*resolve.SingleFetch{t, at}, + } + case *resolve.MultiFetch: + return &resolve.MultiFetch{ + Fetches: append([]*resolve.SingleFetch{t}, at.Fetches...), + } + } + case *resolve.MultiFetch: + switch at := additional.(type) { + case *resolve.SingleFetch: + t.Fetches = append(t.Fetches, at) + case *resolve.MultiFetch: + t.Fetches = append(t.Fetches, at.Fetches...) + } + return t + } + + return existing +} + +func (e *CreateFetchesCopy) CloneObject(o *resolve.Object) *resolve.Object { + return &resolve.Object{ + Nullable: o.Nullable, + Path: o.Path, + Fetch: o.Fetch, + UnescapeResponseJson: o.UnescapeResponseJson, + } +} + +func (e *CreateFetchesCopy) LeaveObject(object *resolve.Object) { + e.currentObjects = e.currentObjects[:len(e.currentObjects)-1] +} + +func (e *CreateFetchesCopy) EnterField(field *resolve.Field) { + if _, ok := e.fieldHasFetch[field]; !ok { + e.SetSkip(true) + return + } + + currentPath := e.renderPath() + + existingField, ok := e.fieldForPath[currentPath] + if !ok { + clonedField := e.CloneField(field) + e.currentFields = append(e.currentFields, clonedField) + e.currentObjects[len(e.currentObjects)-1].Fields = append(e.currentObjects[len(e.currentObjects)-1].Fields, clonedField) + e.fieldForPath[currentPath] = clonedField + return + } + + e.currentFields = append(e.currentFields, existingField) +} + +func (e *CreateFetchesCopy) CloneField(f *resolve.Field) *resolve.Field { + return &resolve.Field{ + Name: f.Name, + } +} + +func (e *CreateFetchesCopy) LeaveField(field *resolve.Field) { + if _, ok := e.fieldHasFetch[field]; !ok { + e.SetSkip(false) + return + } + + e.currentFields = e.currentFields[:len(e.currentFields)-1] +} diff --git a/v2/pkg/engine/postprocess/extract_fetches.go b/v2/pkg/engine/postprocess/extract_fetches.go new file mode 100644 index 000000000..b29efcff6 --- /dev/null +++ b/v2/pkg/engine/postprocess/extract_fetches.go @@ -0,0 +1,15 @@ +package postprocess + +import ( + "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/plan" +) + +type FetchExtractor struct { +} + +func (e *FetchExtractor) Process(pre plan.Plan) plan.Plan { + fieldsWithFetch := NewFetchFinder().Find(pre) + + createFetchesCopy := NewFetchesCopier(fieldsWithFetch) + return createFetchesCopy.Process(pre) +} diff --git a/v2/pkg/engine/postprocess/extract_fetches_test.go b/v2/pkg/engine/postprocess/extract_fetches_test.go new file mode 100644 index 000000000..5dcf7c974 --- /dev/null +++ b/v2/pkg/engine/postprocess/extract_fetches_test.go @@ -0,0 +1,305 @@ +package postprocess + +import ( + "fmt" + "reflect" + "testing" + + "github.com/kylelemons/godebug/pretty" + "github.com/stretchr/testify/assert" + + "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/plan" + "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/resolve" +) + +func TestExtractFetches_Process(t *testing.T) { + type TestCase struct { + name string + pre plan.Plan + expected plan.Plan + } + + cases := []TestCase{ + { + name: "1", + pre: &plan.SynchronousResponsePlan{ + Response: &resolve.GraphQLResponse{ + Data: &resolve.Object{ + Fields: []*resolve.Field{ + { + Name: []byte("field1"), + Value: &resolve.String{ + Path: []string{"field1"}, + }, + }, + }, + Fetch: &resolve.MultiFetch{ + Fetches: []*resolve.SingleFetch{ + {FetchID: 1}, + {FetchID: 2}, + {FetchID: 3}, + }, + }, + }, + }, + }, + expected: &plan.SynchronousResponsePlan{ + Response: &resolve.GraphQLResponse{ + Data: &resolve.Object{ + Fields: []*resolve.Field{ + { + Name: []byte("field1"), + Value: &resolve.String{ + Path: []string{"field1"}, + }, + }, + }, + }, + FetchData: &resolve.Object{ + Fetch: &resolve.MultiFetch{ + Fetches: []*resolve.SingleFetch{ + {FetchID: 1}, + {FetchID: 2}, + {FetchID: 3}, + }, + }, + }, + }, + }, + }, + { + name: "2", + pre: &plan.SynchronousResponsePlan{ + Response: &resolve.GraphQLResponse{ + Data: &resolve.Object{ + Fields: []*resolve.Field{ + { + Name: []byte("obj"), + Value: &resolve.Object{ + Path: []string{"obj"}, + Fields: []*resolve.Field{ + { + Name: []byte("field1"), + Value: &resolve.String{ + Path: []string{"field1"}, + }, + }, + }, + Fetch: &resolve.SingleFetch{FetchID: 2}, + }, + }, + { + Name: []byte("obj"), + Value: &resolve.Object{ + Path: []string{"obj"}, + Fields: []*resolve.Field{ + { + Name: []byte("field1"), + Value: &resolve.String{ + Path: []string{"field1"}, + }, + }, + }, + Fetch: &resolve.SingleFetch{FetchID: 3}, + }, + }, + }, + Fetch: &resolve.SingleFetch{FetchID: 1}, + }, + }, + }, + expected: &plan.SynchronousResponsePlan{ + Response: &resolve.GraphQLResponse{ + Data: &resolve.Object{ + Fields: []*resolve.Field{ + { + Name: []byte("obj"), + Value: &resolve.Object{ + Path: []string{"obj"}, + Fields: []*resolve.Field{ + { + Name: []byte("field1"), + Value: &resolve.String{ + Path: []string{"field1"}, + }, + }, + }, + }, + }, + { + Name: []byte("obj"), + Value: &resolve.Object{ + Path: []string{"obj"}, + Fields: []*resolve.Field{ + { + Name: []byte("field1"), + Value: &resolve.String{ + Path: []string{"field1"}, + }, + }, + }, + }, + }, + }, + }, + FetchData: &resolve.Object{ + Fields: []*resolve.Field{ + { + Name: []byte("obj"), + Value: &resolve.Object{ + Path: []string{"obj"}, + Fetch: &resolve.MultiFetch{ + Fetches: []*resolve.SingleFetch{ + {FetchID: 2}, + {FetchID: 3}, + }, + }, + }, + }, + }, + Fetch: &resolve.SingleFetch{FetchID: 1}, + }, + }, + }, + }, + { + name: "3", + pre: &plan.SynchronousResponsePlan{ + Response: &resolve.GraphQLResponse{ + Data: &resolve.Object{ + Fields: []*resolve.Field{ + { + Name: []byte("objects"), + Value: &resolve.Array{ + Nullable: true, + Path: []string{"objects"}, + Item: &resolve.Object{ + Path: []string{"obj"}, + Fields: []*resolve.Field{ + { + Name: []byte("field1"), + Value: &resolve.String{ + Path: []string{"field1"}, + }, + }, + }, + Fetch: &resolve.SingleFetch{FetchID: 2}, + }, + }, + }, + { + Name: []byte("objects"), + Value: &resolve.Array{ + Nullable: true, + Path: []string{"objects"}, + Item: &resolve.Object{ + Path: []string{"obj"}, + Fields: []*resolve.Field{ + { + Name: []byte("field1"), + Value: &resolve.String{ + Path: []string{"field1"}, + }, + }, + }, + Fetch: &resolve.SingleFetch{FetchID: 3}, + }, + }, + }, + }, + Fetch: &resolve.SingleFetch{FetchID: 1}, + }, + }, + }, + expected: &plan.SynchronousResponsePlan{ + Response: &resolve.GraphQLResponse{ + Data: &resolve.Object{ + Fields: []*resolve.Field{ + { + Name: []byte("objects"), + Value: &resolve.Array{ + Nullable: true, + Path: []string{"objects"}, + Item: &resolve.Object{ + Path: []string{"obj"}, + Fields: []*resolve.Field{ + { + Name: []byte("field1"), + Value: &resolve.String{ + Path: []string{"field1"}, + }, + }, + }, + }, + }, + }, + { + Name: []byte("objects"), + Value: &resolve.Array{ + Nullable: true, + Path: []string{"objects"}, + Item: &resolve.Object{ + Path: []string{"obj"}, + Fields: []*resolve.Field{ + { + Name: []byte("field1"), + Value: &resolve.String{ + Path: []string{"field1"}, + }, + }, + }, + }, + }, + }, + }, + }, + FetchData: &resolve.Object{ + Fields: []*resolve.Field{ + { + Name: []byte("objects"), + Value: &resolve.Array{ + Nullable: true, + Path: []string{"objects"}, + Item: &resolve.Object{ + Path: []string{"obj"}, + Fetch: &resolve.MultiFetch{ + Fetches: []*resolve.SingleFetch{ + {FetchID: 2}, + {FetchID: 3}, + }, + }, + }, + }, + }, + }, + Fetch: &resolve.SingleFetch{FetchID: 1}, + }, + }, + }, + }, + } + + processor := &FetchExtractor{} + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual := processor.Process(c.pre) + + if !assert.Equal(t, c.expected, actual) { + formatterConfig := map[reflect.Type]interface{}{ + reflect.TypeOf([]byte{}): func(b []byte) string { return fmt.Sprintf(`"%s"`, string(b)) }, + } + + prettyCfg := &pretty.Config{ + Diffable: true, + IncludeUnexported: false, + Formatter: formatterConfig, + } + + if diff := prettyCfg.Compare(c.expected, actual); diff != "" { + t.Errorf("Plan does not match(-want +got)\n%s", diff) + } + } + }) + } +} diff --git a/v2/pkg/engine/postprocess/fetch_finder.go b/v2/pkg/engine/postprocess/fetch_finder.go new file mode 100644 index 000000000..ca9fb7617 --- /dev/null +++ b/v2/pkg/engine/postprocess/fetch_finder.go @@ -0,0 +1,48 @@ +package postprocess + +import ( + "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/plan" + "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/resolve" +) + +type FetchFinder struct { + *PlanWalker + + fieldHasFetches map[*resolve.Field]struct{} +} + +func NewFetchFinder() *FetchFinder { + e := &FetchFinder{ + fieldHasFetches: make(map[*resolve.Field]struct{}), + PlanWalker: &PlanWalker{}, + } + + e.registerObjectVisitor(e) + + return e +} + +func (e *FetchFinder) Find(pre plan.Plan) map[*resolve.Field]struct{} { + switch t := pre.(type) { + case *plan.SynchronousResponsePlan: + e.Walk(t.Response.Data, t.Response.Info) + case *plan.SubscriptionResponsePlan: + e.Walk(t.Response.Response.Data, t.Response.Response.Info) + } + return e.fieldHasFetches +} + +func (e *FetchFinder) markCurrentFieldsHasFetch() { + for i := range e.CurrentFields { + e.fieldHasFetches[e.CurrentFields[i]] = struct{}{} + } +} + +func (e *FetchFinder) EnterObject(object *resolve.Object) { + if object.Fetch != nil { + e.markCurrentFieldsHasFetch() + } +} + +func (e *FetchFinder) LeaveObject(object *resolve.Object) { +} diff --git a/v2/pkg/engine/postprocess/plan_visitor.go b/v2/pkg/engine/postprocess/plan_visitor.go new file mode 100644 index 000000000..eeef2ba51 --- /dev/null +++ b/v2/pkg/engine/postprocess/plan_visitor.go @@ -0,0 +1,229 @@ +package postprocess + +import ( + "strings" + + "github.com/wundergraph/graphql-go-tools/v2/pkg/ast" + "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/resolve" +) + +type PlanVisitor interface { + PlanObjectVisitor + PlanArrayVisitor +} + +type PlanObjectVisitor interface { + PlanEnterObjectVisitor + PlanLeaveObjectVisitor +} + +type PlanEnterObjectVisitor interface { + EnterObject(object *resolve.Object) +} + +type PlanLeaveObjectVisitor interface { + LeaveObject(object *resolve.Object) +} + +type PlanArrayVisitor interface { + PlanEnterArrayVisitor + PlanLeaveArrayVisitor +} + +type PlanEnterArrayVisitor interface { + EnterArray(array *resolve.Array) +} + +type PlanLeaveArrayVisitor interface { + LeaveArray(array *resolve.Array) +} + +type PlanFieldVisitor interface { + PlanEnterFieldVisitor + PlanLeaveFieldVisitor +} + +type PlanEnterFieldVisitor interface { + EnterField(field *resolve.Field) +} + +type PlanLeaveFieldVisitor interface { + LeaveField(field *resolve.Field) +} + +type PlanWalker struct { + info *resolve.GraphQLResponseInfo + + CurrentFields []*resolve.Field + CurrentObjects []*resolve.Object + path []string + + objectVisitor PlanObjectVisitor + arrayVisitor PlanArrayVisitor + fieldVisitor PlanFieldVisitor + + skip bool +} + +func (e *PlanWalker) SetSkip(skip bool) { + e.skip = skip +} + +func (e *PlanWalker) registerObjectVisitor(visitor PlanObjectVisitor) { + e.objectVisitor = visitor +} + +func (e *PlanWalker) registerArrayVisitor(visitor PlanArrayVisitor) { + e.arrayVisitor = visitor +} + +func (e *PlanWalker) registerFieldVisitor(visitor PlanFieldVisitor) { + e.fieldVisitor = visitor +} + +func (e *PlanWalker) pushField(field *resolve.Field) { + e.CurrentFields = append(e.CurrentFields, field) +} + +func (e *PlanWalker) popField() { + e.CurrentFields = e.CurrentFields[:len(e.CurrentFields)-1] +} + +func (e *PlanWalker) pushObject(object *resolve.Object) { + e.CurrentObjects = append(e.CurrentObjects, object) +} + +func (e *PlanWalker) popObject() { + e.CurrentObjects = e.CurrentObjects[:len(e.CurrentObjects)-1] +} + +func (e *PlanWalker) pushPath(path []string) { + e.path = append(e.path, path...) +} + +func (e *PlanWalker) popPath(path []string) { + e.path = e.path[:len(e.path)-len(path)] +} + +func (e *PlanWalker) pushArrayPath() { + e.path = append(e.path, "@") +} + +func (e *PlanWalker) popArrayPath() { + e.path = e.path[:len(e.path)-1] +} + +func (e *PlanWalker) renderPath() string { + builder := strings.Builder{} + if e.info != nil { + switch e.info.OperationType { + case ast.OperationTypeQuery: + builder.WriteString("query") + case ast.OperationTypeMutation: + builder.WriteString("mutation") + case ast.OperationTypeSubscription: + builder.WriteString("subscription") + case ast.OperationTypeUnknown: + } + } + if len(e.path) == 0 { + return builder.String() + } + for i := range e.path { + builder.WriteByte('.') + builder.WriteString(e.path[i]) + } + return builder.String() +} + +func (e *PlanWalker) Walk(data *resolve.Object, info *resolve.GraphQLResponseInfo) { + e.info = info + e.walkNode(data) +} + +func (e *PlanWalker) walkNode(node resolve.Node) { + switch n := node.(type) { + case *resolve.Object: + e.walkObject(n) + case *resolve.Array: + e.walkArray(n) + } +} + +func (e *PlanWalker) walkField(field *resolve.Field) { + e.onEnterField(field) + defer e.onLeaveField(field) + + e.pushField(field) + defer e.popField() + + if e.skip { + return + } + + e.walkNode(field.Value) +} + +func (e *PlanWalker) onEnterField(field *resolve.Field) { + if e.fieldVisitor != nil { + e.fieldVisitor.EnterField(field) + } +} + +func (e *PlanWalker) onLeaveField(field *resolve.Field) { + if e.fieldVisitor != nil { + e.fieldVisitor.LeaveField(field) + } +} + +func (e *PlanWalker) walkObject(object *resolve.Object) { + e.objectVisitor.EnterObject(object) + defer e.objectVisitor.LeaveObject(object) + + e.pushObject(object) + defer e.popObject() + + e.pushPath(object.Path) + defer e.popPath(object.Path) + + for i := range object.Fields { + e.walkField(object.Fields[i]) + } +} + +func (e *PlanWalker) onEnterObject(object *resolve.Object) { + if e.objectVisitor != nil { + e.objectVisitor.EnterObject(object) + } +} + +func (e *PlanWalker) onLeaveObject(object *resolve.Object) { + if e.objectVisitor != nil { + e.objectVisitor.LeaveObject(object) + } +} + +func (e *PlanWalker) onEnterArray(array *resolve.Array) { + if e.arrayVisitor != nil { + e.arrayVisitor.EnterArray(array) + } +} + +func (e *PlanWalker) onLeaveArray(array *resolve.Array) { + if e.arrayVisitor != nil { + e.arrayVisitor.LeaveArray(array) + } +} + +func (e *PlanWalker) walkArray(array *resolve.Array) { + e.onEnterArray(array) + defer e.onLeaveArray(array) + + e.pushPath(array.Path) + defer e.popPath(array.Path) + + e.pushArrayPath() + defer e.popArrayPath() + + e.walkNode(array.Item) +} diff --git a/v2/pkg/engine/resolve/node_array.go b/v2/pkg/engine/resolve/node_array.go index 9a9e58dbd..896dc64bf 100644 --- a/v2/pkg/engine/resolve/node_array.go +++ b/v2/pkg/engine/resolve/node_array.go @@ -3,9 +3,9 @@ package resolve type Array struct { Path []string Nullable bool - ResolveAsynchronous bool + ResolveAsynchronous bool // deprecated: remove Item Node - Items []Node + Items []Node // deprecated: remove } func (a *Array) HasChildFetches() bool { diff --git a/v2/pkg/engine/resolve/response.go b/v2/pkg/engine/resolve/response.go index 68cd4e105..3f95951b9 100644 --- a/v2/pkg/engine/resolve/response.go +++ b/v2/pkg/engine/resolve/response.go @@ -24,6 +24,7 @@ type GraphQLSubscriptionTrigger struct { type GraphQLResponse struct { Data *Object + FetchData *Object RenameTypeNames []RenameTypeName Info *GraphQLResponseInfo } From e13b63af3b6a25db4a801f8738769191aefb2d81 Mon Sep 17 00:00:00 2001 From: spetrunin Date: Wed, 22 May 2024 22:53:49 +0300 Subject: [PATCH 02/15] rollback full merge of response nodes --- v2/pkg/engine/plan/planner.go | 6 -- v2/pkg/engine/plan/skip_include_visitor.go | 46 --------- v2/pkg/engine/plan/visitor.go | 110 +++------------------ 3 files changed, 16 insertions(+), 146 deletions(-) delete mode 100644 v2/pkg/engine/plan/skip_include_visitor.go diff --git a/v2/pkg/engine/plan/planner.go b/v2/pkg/engine/plan/planner.go index 4a7c397d9..630880aef 100644 --- a/v2/pkg/engine/plan/planner.go +++ b/v2/pkg/engine/plan/planner.go @@ -114,17 +114,11 @@ func (p *Planner) Plan(operation, definition *ast.Document, operationName string p.debugMessage("Planning visitor:") } - hasConditionalSkipInclude := hasConditionalSkipInclude(operation, definition, report) - if report.HasErrors() { - return nil - } - // configure planning visitor p.planningVisitor.planners = p.configurationVisitor.planners p.planningVisitor.Config = p.config p.planningVisitor.skipFieldsRefs = p.configurationVisitor.skipFieldsRefs - p.planningVisitor.allowFieldMerge = !hasConditionalSkipInclude p.planningWalker.ResetVisitors() p.planningWalker.SetVisitorFilter(p.planningVisitor) diff --git a/v2/pkg/engine/plan/skip_include_visitor.go b/v2/pkg/engine/plan/skip_include_visitor.go deleted file mode 100644 index 6c0b2be8e..000000000 --- a/v2/pkg/engine/plan/skip_include_visitor.go +++ /dev/null @@ -1,46 +0,0 @@ -package plan - -import ( - "github.com/wundergraph/graphql-go-tools/v2/pkg/ast" - "github.com/wundergraph/graphql-go-tools/v2/pkg/astvisitor" - "github.com/wundergraph/graphql-go-tools/v2/pkg/operationreport" -) - -func hasConditionalSkipInclude(operation *ast.Document, definition *ast.Document, report *operationreport.Report) bool { - walker := astvisitor.NewWalker(32) - visitor := &skipIncludeVisitor{ - operation: operation, - definition: definition, - walker: &walker, - } - walker.RegisterEnterInlineFragmentVisitor(visitor) - walker.RegisterEnterFieldVisitor(visitor) - walker.Walk(operation, definition, report) - - return visitor.HasConditionalSkipInclude -} - -type skipIncludeVisitor struct { - operation *ast.Document - definition *ast.Document - walker *astvisitor.Walker - HasConditionalSkipInclude bool -} - -func (v *skipIncludeVisitor) EnterInlineFragment(inlineFragmentRef int) { - v.checkDirectives(v.operation.InlineFragments[inlineFragmentRef].Directives.Refs) -} - -func (v *skipIncludeVisitor) EnterField(fieldRef int) { - v.checkDirectives(v.operation.Fields[fieldRef].Directives.Refs) -} - -func (v *skipIncludeVisitor) checkDirectives(directiveRefs []int) { - _, skip := v.operation.ResolveSkipDirectiveVariable(directiveRefs) - _, include := v.operation.ResolveIncludeDirectiveVariable(directiveRefs) - - if skip || include { - v.HasConditionalSkipInclude = true - v.walker.Stop() - } -} diff --git a/v2/pkg/engine/plan/visitor.go b/v2/pkg/engine/plan/visitor.go index bc4d38520..aa0e4b930 100644 --- a/v2/pkg/engine/plan/visitor.go +++ b/v2/pkg/engine/plan/visitor.go @@ -39,9 +39,6 @@ type Visitor struct { exportedVariables map[string]struct{} skipIncludeOnFragments map[int]skipIncludeInfo disableResolveFieldPositions bool - - fieldByPaths map[string]*resolve.Field - allowFieldMerge bool } func (v *Visitor) debugOnEnterNode(kind ast.NodeKind, ref int) { @@ -272,112 +269,38 @@ func (v *Visitor) EnterField(ref int) { } fieldDefinitionTypeRef := v.Definition.FieldDefinitionType(fieldDefinition) - fullFieldPathWithoutFragments := v.currentFullPath(true) - - // if we already have a field with the same path we merge existing field with the current one - if v.allowFieldMerge && v.handleExistingField(ref, fieldDefinitionTypeRef, fullFieldPathWithoutFragments) { - return - } - skipIncludeInfo := v.resolveSkipIncludeForField(ref) onTypeNames := v.resolveOnTypeNames(ref) + v.currentField = &resolve.Field{ + Name: fieldAliasOrName, + OnTypeNames: onTypeNames, + Position: v.resolveFieldPosition(ref), + SkipDirectiveDefined: skipIncludeInfo.skip, + SkipVariableName: skipIncludeInfo.skipVariableName, + IncludeDirectiveDefined: skipIncludeInfo.include, + IncludeVariableName: skipIncludeInfo.includeVariableName, + Info: v.resolveFieldInfo(ref, fieldDefinitionTypeRef, onTypeNames), + } + if bytes.Equal(fieldName, literal.TYPENAME) { - v.currentField = &resolve.Field{ - Name: fieldAliasOrName, - Value: &resolve.String{ - Nullable: false, - Path: []string{v.Operation.FieldAliasOrNameString(ref)}, - IsTypeName: true, - }, - OnTypeNames: onTypeNames, - Position: v.resolveFieldPosition(ref), - SkipDirectiveDefined: skipIncludeInfo.skip, - SkipVariableName: skipIncludeInfo.skipVariableName, - IncludeDirectiveDefined: skipIncludeInfo.include, - IncludeVariableName: skipIncludeInfo.includeVariableName, - Info: v.resolveFieldInfo(ref, fieldDefinitionTypeRef, onTypeNames), + v.currentField.Value = &resolve.String{ + Nullable: false, + Path: []string{v.Operation.FieldAliasOrNameString(ref)}, + IsTypeName: true, } } else { path := v.resolveFieldPath(ref) - v.currentField = &resolve.Field{ - Name: fieldAliasOrName, - Value: v.resolveFieldValue(ref, fieldDefinitionTypeRef, true, path), - OnTypeNames: onTypeNames, - Position: v.resolveFieldPosition(ref), - SkipDirectiveDefined: skipIncludeInfo.skip, - SkipVariableName: skipIncludeInfo.skipVariableName, - IncludeDirectiveDefined: skipIncludeInfo.include, - IncludeVariableName: skipIncludeInfo.includeVariableName, - Info: v.resolveFieldInfo(ref, fieldDefinitionTypeRef, onTypeNames), - } + v.currentField.Value = v.resolveFieldValue(ref, fieldDefinitionTypeRef, true, path) } // append the field to the current object *v.currentFields[len(v.currentFields)-1].fields = append(*v.currentFields[len(v.currentFields)-1].fields, v.currentField) - // track the added field by its path - v.fieldByPaths[fullFieldPathWithoutFragments] = v.currentField - v.mapFieldConfig(ref) } -func (v *Visitor) handleExistingField(currentFieldRef int, fieldDefinitionTypeRef int, fullFieldPathWithoutFragments string) (exists bool) { - resolveField := v.fieldByPaths[fullFieldPathWithoutFragments] - if resolveField == nil { - return false - } - - // merge on type names - onTypeNames := v.resolveOnTypeNames(currentFieldRef) - hasOnTypeNames := len(resolveField.OnTypeNames) > 0 && len(onTypeNames) > 0 - if hasOnTypeNames { - for _, t := range onTypeNames { - if !slices.ContainsFunc(resolveField.OnTypeNames, func(existingT []byte) bool { - return bytes.Equal(existingT, t) - }) { - resolveField.OnTypeNames = append(resolveField.OnTypeNames, t) - } - } - } else { - // if one of the duplicates request the field unconditionally, we remove the on type names - resolveField.OnTypeNames = nil - } - - // merge field info - maybeAdditionalInfo := v.resolveFieldInfo(currentFieldRef, fieldDefinitionTypeRef, onTypeNames) - if resolveField.Info != nil && maybeAdditionalInfo != nil { - resolveField.Info.Merge(maybeAdditionalInfo) - } - - // set field as current field - v.currentField = resolveField - - var obj *resolve.Object - switch node := v.currentField.Value.(type) { - case *resolve.Array: - obj, _ = node.Item.(*resolve.Object) - case *resolve.Object: - obj = node - } - - if obj != nil { - v.objects = append(v.objects, obj) - v.Walker.DefferOnEnterField(func() { - v.currentFields = append(v.currentFields, objectFields{ - popOnField: currentFieldRef, - fields: &obj.Fields, - }) - }) - } - - // we have to map field config again with a different ref because it is used in input templates rendering - v.mapFieldConfig(currentFieldRef) - - return true -} - func (v *Visitor) mapFieldConfig(ref int) { typeName := v.Walker.EnclosingTypeDefinition.NameString(v.Definition) fieldNameStr := v.Operation.FieldNameString(ref) @@ -884,7 +807,6 @@ func (v *Visitor) EnterDocument(operation, definition *ast.Document) { v.fieldConfigs = map[int]*FieldConfiguration{} v.exportedVariables = map[string]struct{}{} v.skipIncludeOnFragments = map[int]skipIncludeInfo{} - v.fieldByPaths = map[string]*resolve.Field{} } func (v *Visitor) LeaveDocument(_, _ *ast.Document) { From 6d2e85b559a4be11292666f04a50509dc7b6effa Mon Sep 17 00:00:00 2001 From: spetrunin Date: Wed, 22 May 2024 23:03:14 +0300 Subject: [PATCH 03/15] remove unused code --- .../graphql_datasource_test.go | 5 +-- v2/pkg/engine/plan/planner_test.go | 5 +-- v2/pkg/engine/resolve/node_array.go | 25 ++----------- v2/pkg/engine/resolve/node_object.go | 29 --------------- v2/pkg/engine/resolve/resolve_test.go | 33 +++++++---------- v2/pkg/engine/resolve/simple_resolver.go | 37 ++++--------------- v2/pkg/engine/resolve/trace.go | 9 +---- 7 files changed, 28 insertions(+), 115 deletions(-) diff --git a/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_test.go b/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_test.go index f30c587a8..c9e58024f 100644 --- a/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_test.go +++ b/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_test.go @@ -2569,9 +2569,8 @@ func TestGraphQLDataSource(t *testing.T) { { Name: []byte("reviews"), Value: &resolve.Array{ - Path: []string{"reviews"}, - Nullable: true, - ResolveAsynchronous: false, + Path: []string{"reviews"}, + Nullable: true, Item: &resolve.Object{ Nullable: true, Path: nil, diff --git a/v2/pkg/engine/plan/planner_test.go b/v2/pkg/engine/plan/planner_test.go index b9e6ee76e..cd23c115a 100644 --- a/v2/pkg/engine/plan/planner_test.go +++ b/v2/pkg/engine/plan/planner_test.go @@ -92,9 +92,8 @@ func TestPlanner_Plan(t *testing.T) { { Name: []byte("searchResults"), Value: &resolve.Array{ - Path: []string{"searchResults"}, - Nullable: true, - ResolveAsynchronous: false, + Path: []string{"searchResults"}, + Nullable: true, Item: &resolve.Object{ Nullable: true, Fields: []*resolve.Field{ diff --git a/v2/pkg/engine/resolve/node_array.go b/v2/pkg/engine/resolve/node_array.go index 896dc64bf..2d75dd789 100644 --- a/v2/pkg/engine/resolve/node_array.go +++ b/v2/pkg/engine/resolve/node_array.go @@ -1,28 +1,9 @@ package resolve type Array struct { - Path []string - Nullable bool - ResolveAsynchronous bool // deprecated: remove - Item Node - Items []Node // deprecated: remove -} - -func (a *Array) HasChildFetches() bool { - switch t := a.Item.(type) { - case *Object: - if t.Fetch != nil { - return true - } - if t.HasChildFetches() { - return true - } - case *Array: - if t.HasChildFetches() { - return true - } - } - return false + Path []string + Nullable bool + Item Node } func (_ *Array) NodeKind() NodeKind { diff --git a/v2/pkg/engine/resolve/node_object.go b/v2/pkg/engine/resolve/node_object.go index 793abcd06..c341c4a5f 100644 --- a/v2/pkg/engine/resolve/node_object.go +++ b/v2/pkg/engine/resolve/node_object.go @@ -12,35 +12,6 @@ type Object struct { UnescapeResponseJson bool `json:"unescape_response_json,omitempty"` } -func (o *Object) HasChildFetches() bool { - for i := range o.Fields { - switch t := o.Fields[i].Value.(type) { - case *Object: - if t.Fetch != nil { - return true - } - if t.HasChildFetches() { - return true - } - case *Array: - switch at := t.Item.(type) { - case *Object: - if at.Fetch != nil { - return true - } - if at.HasChildFetches() { - return true - } - case *Array: - if at.HasChildFetches() { - return true - } - } - } - } - return false -} - func (_ *Object) NodeKind() NodeKind { return NodeKindObject } diff --git a/v2/pkg/engine/resolve/resolve_test.go b/v2/pkg/engine/resolve/resolve_test.go index ba3ab2ec5..8b13c9d3c 100644 --- a/v2/pkg/engine/resolve/resolve_test.go +++ b/v2/pkg/engine/resolve/resolve_test.go @@ -925,9 +925,8 @@ func TestResolver_ResolveNode(t *testing.T) { { Name: []byte("synchronousFriends"), Value: &Array{ - Path: []string{"friends"}, - ResolveAsynchronous: false, - Nullable: true, + Path: []string{"friends"}, + Nullable: true, Item: &Object{ Fields: []*Field{ { @@ -949,9 +948,8 @@ func TestResolver_ResolveNode(t *testing.T) { { Name: []byte("asynchronousFriends"), Value: &Array{ - Path: []string{"friends"}, - ResolveAsynchronous: true, - Nullable: true, + Path: []string{"friends"}, + Nullable: true, Item: &Object{ Fields: []*Field{ { @@ -981,9 +979,8 @@ func TestResolver_ResolveNode(t *testing.T) { { Name: []byte("strings"), Value: &Array{ - Path: []string{"strings"}, - ResolveAsynchronous: false, - Nullable: true, + Path: []string{"strings"}, + Nullable: true, Item: &String{ Nullable: false, }, @@ -992,9 +989,8 @@ func TestResolver_ResolveNode(t *testing.T) { { Name: []byte("integers"), Value: &Array{ - Path: []string{"integers"}, - ResolveAsynchronous: false, - Nullable: true, + Path: []string{"integers"}, + Nullable: true, Item: &Integer{ Nullable: false, }, @@ -1003,9 +999,8 @@ func TestResolver_ResolveNode(t *testing.T) { { Name: []byte("floats"), Value: &Array{ - Path: []string{"floats"}, - ResolveAsynchronous: false, - Nullable: true, + Path: []string{"floats"}, + Nullable: true, Item: &Float{ Nullable: false, }, @@ -1014,9 +1009,8 @@ func TestResolver_ResolveNode(t *testing.T) { { Name: []byte("booleans"), Value: &Array{ - Path: []string{"booleans"}, - ResolveAsynchronous: false, - Nullable: true, + Path: []string{"booleans"}, + Nullable: true, Item: &Boolean{ Nullable: false, }, @@ -1218,8 +1212,7 @@ func TestResolver_ResolveNode(t *testing.T) { { Name: []byte("pets"), Value: &Array{ - ResolveAsynchronous: true, - Path: []string{"pets"}, + Path: []string{"pets"}, Item: &Object{ Fields: []*Field{ { diff --git a/v2/pkg/engine/resolve/simple_resolver.go b/v2/pkg/engine/resolve/simple_resolver.go index 1371e9125..d5c08b65d 100644 --- a/v2/pkg/engine/resolve/simple_resolver.go +++ b/v2/pkg/engine/resolve/simple_resolver.go @@ -171,39 +171,16 @@ func (r *SimpleResolver) resolveArray(array *Array, data []byte, resolveBuf *fas if hasPreviousItem { arrayBuf.WriteBytes(comma) } - if array.Items != nil { - hasPreviousItem = false - for j := range array.Items { - if hasPreviousItem { - arrayBuf.WriteBytes(comma) - } - err = r.resolveNode(array.Items[j], arrayItems[i], arrayBuf) - if err != nil { - if errors.Is(err, errNonNullableFieldValueIsNull) { - if !array.Nullable { - return err - } - r.resolveNull(resolveBuf) - return nil - } + err = r.resolveNode(array.Item, arrayItems[i], arrayBuf) + if err != nil { + if errors.Is(err, errNonNullableFieldValueIsNull) { + if !array.Nullable { return err } - if !hasPreviousItem { - hasPreviousItem = true - } - } - } else { - 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 + r.resolveNull(resolveBuf) + return nil } + return err } if !hasPreviousItem { hasPreviousItem = true diff --git a/v2/pkg/engine/resolve/trace.go b/v2/pkg/engine/resolve/trace.go index 653d47510..f20e3c42c 100644 --- a/v2/pkg/engine/resolve/trace.go +++ b/v2/pkg/engine/resolve/trace.go @@ -272,14 +272,7 @@ func parseNode(n Node, options *getTraceOptions) *TraceNode { node.Fetch = parseFetch(v.Fetch, options) case *Array: - if v.Item != nil { - node.Items = append(node.Items, parseNode(v.Item, options)) - } else if len(v.Items) > 0 { - for _, item := range v.Items { - node.Items = append(node.Items, parseNode(item, options)) - } - } - + node.Items = append(node.Items, parseNode(v.Item, options)) case *String: node.UnescapeResponseJson = v.UnescapeResponseJson node.IsTypeName = v.IsTypeName From 7949b3f7dbf9c621c55e4317dafb7537d55e9305 Mon Sep 17 00:00:00 2001 From: spetrunin Date: Thu, 23 May 2024 13:50:56 +0300 Subject: [PATCH 04/15] rework postprocessor to use fetch extraction --- .../create_concrete_single_fetch_types.go | 15 ++--- ...create_fetches.go => create_fetch_tree.go} | 62 ++++++++----------- .../postprocess/create_multi_fetch_types.go | 15 ++--- .../create_multi_fetch_types_test.go | 10 +-- v2/pkg/engine/postprocess/extract_fetches.go | 15 ----- v2/pkg/engine/postprocess/fetch_finder.go | 10 +-- v2/pkg/engine/postprocess/plan_visitor.go | 12 ++-- v2/pkg/engine/postprocess/postprocess.go | 31 ++++++++-- ...ct_fetches_test.go => postprocess_test.go} | 10 +-- .../postprocess/resolve_input_templates.go | 17 +++-- .../resolve_input_templates_test.go | 10 +-- v2/pkg/engine/resolve/loader.go | 5 +- v2/pkg/engine/resolve/response.go | 2 +- 13 files changed, 95 insertions(+), 119 deletions(-) rename v2/pkg/engine/postprocess/{create_fetches.go => create_fetch_tree.go} (68%) delete mode 100644 v2/pkg/engine/postprocess/extract_fetches.go rename v2/pkg/engine/postprocess/{extract_fetches_test.go => postprocess_test.go} (97%) diff --git a/v2/pkg/engine/postprocess/create_concrete_single_fetch_types.go b/v2/pkg/engine/postprocess/create_concrete_single_fetch_types.go index 0e68d60ba..4205d11c4 100644 --- a/v2/pkg/engine/postprocess/create_concrete_single_fetch_types.go +++ b/v2/pkg/engine/postprocess/create_concrete_single_fetch_types.go @@ -1,21 +1,18 @@ package postprocess import ( - "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/plan" "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/resolve" ) // CreateConcreteSingleFetchTypes is a postprocessor that transforms fetches into more concrete fetch types type CreateConcreteSingleFetchTypes struct{} -func (d *CreateConcreteSingleFetchTypes) Process(pre plan.Plan) plan.Plan { - switch t := pre.(type) { - case *plan.SynchronousResponsePlan: - d.traverseNode(t.Response.Data) - case *plan.SubscriptionResponsePlan: - d.traverseNode(t.Response.Response.Data) - } - return pre +func (d *CreateConcreteSingleFetchTypes) Process(node resolve.Node) { + d.traverseNode(node) +} + +func (d *CreateConcreteSingleFetchTypes) ProcessSubscription(node resolve.Node, trigger *resolve.GraphQLSubscriptionTrigger) { + d.traverseNode(node) } func (d *CreateConcreteSingleFetchTypes) traverseNode(node resolve.Node) { diff --git a/v2/pkg/engine/postprocess/create_fetches.go b/v2/pkg/engine/postprocess/create_fetch_tree.go similarity index 68% rename from v2/pkg/engine/postprocess/create_fetches.go rename to v2/pkg/engine/postprocess/create_fetch_tree.go index 04425bc7f..86c601402 100644 --- a/v2/pkg/engine/postprocess/create_fetches.go +++ b/v2/pkg/engine/postprocess/create_fetch_tree.go @@ -1,11 +1,10 @@ package postprocess import ( - "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/plan" "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/resolve" ) -type CreateFetchesCopy struct { +type CreateFetchTree struct { *PlanWalker data *resolve.Object @@ -18,8 +17,8 @@ type CreateFetchesCopy struct { fieldHasFetch map[*resolve.Field]struct{} } -func NewFetchesCopier(fieldHasFetch map[*resolve.Field]struct{}) *CreateFetchesCopy { - e := &CreateFetchesCopy{ +func NewFetchTreeCreator(fieldHasFetch map[*resolve.Field]struct{}) *CreateFetchTree { + e := &CreateFetchTree{ PlanWalker: &PlanWalker{}, objectForPath: make(map[string]*resolve.Object), fieldForPath: make(map[string]*resolve.Field), @@ -33,48 +32,38 @@ func NewFetchesCopier(fieldHasFetch map[*resolve.Field]struct{}) *CreateFetchesC return e } -func (e *CreateFetchesCopy) Process(pre plan.Plan) plan.Plan { - switch t := pre.(type) { - case *plan.SynchronousResponsePlan: - e.Walk(t.Response.Data, t.Response.Info) +func (e *CreateFetchTree) ExtractFetchTree(res *resolve.GraphQLResponse) *resolve.Object { + e.Walk(res.Data, res.Info) - t.Response.FetchData = e.data - - case *plan.SubscriptionResponsePlan: - e.Walk(t.Response.Response.Data, t.Response.Response.Info) - - t.Response.Response.FetchData = e.data - } - return pre + return e.data } -func (e *CreateFetchesCopy) EnterArray(array *resolve.Array) { +func (e *CreateFetchTree) EnterArray(array *resolve.Array) { currentField := e.currentFields[len(e.currentFields)-1] switch currentField.Value.(type) { - case *resolve.Object: - panic("not implemented") case *resolve.Array: // nothing to do case nil: cloned := e.CloneArray(array) currentField.Value = cloned + default: + panic("should not happen") } - } -func (e *CreateFetchesCopy) CloneArray(a *resolve.Array) *resolve.Array { +func (e *CreateFetchTree) CloneArray(a *resolve.Array) *resolve.Array { return &resolve.Array{ Nullable: a.Nullable, Path: a.Path, } } -func (e *CreateFetchesCopy) LeaveArray(array *resolve.Array) { +func (e *CreateFetchTree) LeaveArray(_ *resolve.Array) { } -func (e *CreateFetchesCopy) EnterObject(object *resolve.Object) { +func (e *CreateFetchTree) EnterObject(object *resolve.Object) { if e.data == nil { e.data = e.CloneObject(object) e.currentObjects = append(e.currentObjects, e.data) @@ -102,25 +91,22 @@ func (e *CreateFetchesCopy) EnterObject(object *resolve.Object) { e.currentObjects = append(e.currentObjects, objectForPath) } -func (e *CreateFetchesCopy) setFieldObject(o *resolve.Object) { +func (e *CreateFetchTree) setFieldObject(o *resolve.Object) { currentField := e.currentFields[len(e.currentFields)-1] - if currentField.Value == nil { - currentField.Value = o - return - } - switch t := currentField.Value.(type) { + case nil: + currentField.Value = o case *resolve.Object: - panic("not implemented") + // nothing to do case *resolve.Array: t.Item = o default: - panic("not implemented") + panic("should not happen") } } -func (e *CreateFetchesCopy) appendFetch(existing resolve.Fetch, additional resolve.Fetch) resolve.Fetch { +func (e *CreateFetchTree) appendFetch(existing resolve.Fetch, additional resolve.Fetch) resolve.Fetch { switch t := existing.(type) { case *resolve.SingleFetch: switch at := additional.(type) { @@ -141,12 +127,14 @@ func (e *CreateFetchesCopy) appendFetch(existing resolve.Fetch, additional resol t.Fetches = append(t.Fetches, at.Fetches...) } return t + default: + panic("there should be no other fetch types") } return existing } -func (e *CreateFetchesCopy) CloneObject(o *resolve.Object) *resolve.Object { +func (e *CreateFetchTree) CloneObject(o *resolve.Object) *resolve.Object { return &resolve.Object{ Nullable: o.Nullable, Path: o.Path, @@ -155,11 +143,11 @@ func (e *CreateFetchesCopy) CloneObject(o *resolve.Object) *resolve.Object { } } -func (e *CreateFetchesCopy) LeaveObject(object *resolve.Object) { +func (e *CreateFetchTree) LeaveObject(_ *resolve.Object) { e.currentObjects = e.currentObjects[:len(e.currentObjects)-1] } -func (e *CreateFetchesCopy) EnterField(field *resolve.Field) { +func (e *CreateFetchTree) EnterField(field *resolve.Field) { if _, ok := e.fieldHasFetch[field]; !ok { e.SetSkip(true) return @@ -179,13 +167,13 @@ func (e *CreateFetchesCopy) EnterField(field *resolve.Field) { e.currentFields = append(e.currentFields, existingField) } -func (e *CreateFetchesCopy) CloneField(f *resolve.Field) *resolve.Field { +func (e *CreateFetchTree) CloneField(f *resolve.Field) *resolve.Field { return &resolve.Field{ Name: f.Name, } } -func (e *CreateFetchesCopy) LeaveField(field *resolve.Field) { +func (e *CreateFetchTree) LeaveField(field *resolve.Field) { if _, ok := e.fieldHasFetch[field]; !ok { e.SetSkip(false) return diff --git a/v2/pkg/engine/postprocess/create_multi_fetch_types.go b/v2/pkg/engine/postprocess/create_multi_fetch_types.go index ccf1bd037..b2fe3c77e 100644 --- a/v2/pkg/engine/postprocess/create_multi_fetch_types.go +++ b/v2/pkg/engine/postprocess/create_multi_fetch_types.go @@ -3,21 +3,18 @@ package postprocess import ( "slices" - "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/plan" "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/resolve" ) // CreateMultiFetchTypes is a postprocessor that transforms multi fetches into more concrete fetch types type CreateMultiFetchTypes struct{} -func (d *CreateMultiFetchTypes) Process(pre plan.Plan) plan.Plan { - switch t := pre.(type) { - case *plan.SynchronousResponsePlan: - d.traverseNode(t.Response.Data) - case *plan.SubscriptionResponsePlan: - d.traverseNode(t.Response.Response.Data) - } - return pre +func (d *CreateMultiFetchTypes) Process(node resolve.Node) { + d.traverseNode(node) +} + +func (d *CreateMultiFetchTypes) ProcessSubscription(node resolve.Node, trigger *resolve.GraphQLSubscriptionTrigger) { + d.traverseNode(node) } func (d *CreateMultiFetchTypes) traverseNode(node resolve.Node) { diff --git a/v2/pkg/engine/postprocess/create_multi_fetch_types_test.go b/v2/pkg/engine/postprocess/create_multi_fetch_types_test.go index d5d9dc407..7ea7130c0 100644 --- a/v2/pkg/engine/postprocess/create_multi_fetch_types_test.go +++ b/v2/pkg/engine/postprocess/create_multi_fetch_types_test.go @@ -14,8 +14,8 @@ import ( func TestCreateMultiFetchTypes_Process(t *testing.T) { type TestCase struct { name string - pre plan.Plan - expected plan.Plan + pre *plan.SynchronousResponsePlan + expected *plan.SynchronousResponsePlan } cases := []TestCase{ @@ -214,10 +214,10 @@ func TestCreateMultiFetchTypes_Process(t *testing.T) { for _, c := range cases { t.Run(c.name, func(t *testing.T) { - actual := processor.Process(c.pre) + processor.Process(c.pre.Response.Data) - if !assert.Equal(t, c.expected, actual) { - actualBytes, _ := json.MarshalIndent(actual, "", " ") + if !assert.Equal(t, c.expected, c.pre) { + actualBytes, _ := json.MarshalIndent(c.pre, "", " ") expectedBytes, _ := json.MarshalIndent(c.expected, "", " ") if string(expectedBytes) != string(actualBytes) { diff --git a/v2/pkg/engine/postprocess/extract_fetches.go b/v2/pkg/engine/postprocess/extract_fetches.go deleted file mode 100644 index b29efcff6..000000000 --- a/v2/pkg/engine/postprocess/extract_fetches.go +++ /dev/null @@ -1,15 +0,0 @@ -package postprocess - -import ( - "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/plan" -) - -type FetchExtractor struct { -} - -func (e *FetchExtractor) Process(pre plan.Plan) plan.Plan { - fieldsWithFetch := NewFetchFinder().Find(pre) - - createFetchesCopy := NewFetchesCopier(fieldsWithFetch) - return createFetchesCopy.Process(pre) -} diff --git a/v2/pkg/engine/postprocess/fetch_finder.go b/v2/pkg/engine/postprocess/fetch_finder.go index ca9fb7617..eade9f93f 100644 --- a/v2/pkg/engine/postprocess/fetch_finder.go +++ b/v2/pkg/engine/postprocess/fetch_finder.go @@ -1,7 +1,6 @@ package postprocess import ( - "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/plan" "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/resolve" ) @@ -22,13 +21,8 @@ func NewFetchFinder() *FetchFinder { return e } -func (e *FetchFinder) Find(pre plan.Plan) map[*resolve.Field]struct{} { - switch t := pre.(type) { - case *plan.SynchronousResponsePlan: - e.Walk(t.Response.Data, t.Response.Info) - case *plan.SubscriptionResponsePlan: - e.Walk(t.Response.Response.Data, t.Response.Response.Info) - } +func (e *FetchFinder) Find(res *resolve.GraphQLResponse) map[*resolve.Field]struct{} { + e.Walk(res.Data, res.Info) return e.fieldHasFetches } diff --git a/v2/pkg/engine/postprocess/plan_visitor.go b/v2/pkg/engine/postprocess/plan_visitor.go index eeef2ba51..eb381e2ca 100644 --- a/v2/pkg/engine/postprocess/plan_visitor.go +++ b/v2/pkg/engine/postprocess/plan_visitor.go @@ -177,15 +177,15 @@ func (e *PlanWalker) onLeaveField(field *resolve.Field) { } func (e *PlanWalker) walkObject(object *resolve.Object) { + e.pushPath(object.Path) + defer e.popPath(object.Path) + e.objectVisitor.EnterObject(object) defer e.objectVisitor.LeaveObject(object) e.pushObject(object) defer e.popObject() - e.pushPath(object.Path) - defer e.popPath(object.Path) - for i := range object.Fields { e.walkField(object.Fields[i]) } @@ -216,14 +216,14 @@ func (e *PlanWalker) onLeaveArray(array *resolve.Array) { } func (e *PlanWalker) walkArray(array *resolve.Array) { - e.onEnterArray(array) - defer e.onLeaveArray(array) - e.pushPath(array.Path) defer e.popPath(array.Path) e.pushArrayPath() defer e.popArrayPath() + e.onEnterArray(array) + defer e.onLeaveArray(array) + e.walkNode(array.Item) } diff --git a/v2/pkg/engine/postprocess/postprocess.go b/v2/pkg/engine/postprocess/postprocess.go index 62a4a86fc..69133b2a1 100644 --- a/v2/pkg/engine/postprocess/postprocess.go +++ b/v2/pkg/engine/postprocess/postprocess.go @@ -2,10 +2,12 @@ package postprocess import ( "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/plan" + "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/resolve" ) type PostProcessor interface { - Process(pre plan.Plan) plan.Plan + Process(node resolve.Node) + ProcessSubscription(node resolve.Node, trigger *resolve.GraphQLSubscriptionTrigger) } type Processor struct { @@ -22,10 +24,27 @@ func DefaultProcessor() *Processor { } } -func (p *Processor) Process(pre plan.Plan) (post plan.Plan) { - post = pre - for i := range p.postProcessors { - post = p.postProcessors[i].Process(post) +func (p *Processor) Process(pre plan.Plan) plan.Plan { + switch t := pre.(type) { + case *plan.SynchronousResponsePlan: + p.extractFetches(t.Response) + for i := range p.postProcessors { + p.postProcessors[i].Process(t.Response.FetchTree) + } + + case *plan.SubscriptionResponsePlan: + p.extractFetches(t.Response.Response) + for i := range p.postProcessors { + p.postProcessors[i].ProcessSubscription(t.Response.Response.FetchTree, &t.Response.Trigger) + } } - return + + return pre +} + +func (p *Processor) extractFetches(res *resolve.GraphQLResponse) { + fieldsWithFetch := NewFetchFinder().Find(res) + createFetchesCopy := NewFetchTreeCreator(fieldsWithFetch) + + res.FetchTree = createFetchesCopy.ExtractFetchTree(res) } diff --git a/v2/pkg/engine/postprocess/extract_fetches_test.go b/v2/pkg/engine/postprocess/postprocess_test.go similarity index 97% rename from v2/pkg/engine/postprocess/extract_fetches_test.go rename to v2/pkg/engine/postprocess/postprocess_test.go index 5dcf7c974..18ebe9f90 100644 --- a/v2/pkg/engine/postprocess/extract_fetches_test.go +++ b/v2/pkg/engine/postprocess/postprocess_test.go @@ -12,7 +12,7 @@ import ( "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/resolve" ) -func TestExtractFetches_Process(t *testing.T) { +func TestProcess_ExtractFetches(t *testing.T) { type TestCase struct { name string pre plan.Plan @@ -55,7 +55,7 @@ func TestExtractFetches_Process(t *testing.T) { }, }, }, - FetchData: &resolve.Object{ + FetchTree: &resolve.Object{ Fetch: &resolve.MultiFetch{ Fetches: []*resolve.SingleFetch{ {FetchID: 1}, @@ -142,7 +142,7 @@ func TestExtractFetches_Process(t *testing.T) { }, }, }, - FetchData: &resolve.Object{ + FetchTree: &resolve.Object{ Fields: []*resolve.Field{ { Name: []byte("obj"), @@ -253,7 +253,7 @@ func TestExtractFetches_Process(t *testing.T) { }, }, }, - FetchData: &resolve.Object{ + FetchTree: &resolve.Object{ Fields: []*resolve.Field{ { Name: []byte("objects"), @@ -279,7 +279,7 @@ func TestExtractFetches_Process(t *testing.T) { }, } - processor := &FetchExtractor{} + processor := &Processor{} for _, c := range cases { t.Run(c.name, func(t *testing.T) { diff --git a/v2/pkg/engine/postprocess/resolve_input_templates.go b/v2/pkg/engine/postprocess/resolve_input_templates.go index 7160ee955..081037f1a 100644 --- a/v2/pkg/engine/postprocess/resolve_input_templates.go +++ b/v2/pkg/engine/postprocess/resolve_input_templates.go @@ -4,22 +4,19 @@ import ( "strconv" "strings" - "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/plan" "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/resolve" ) // ResolveInputTemplates is a postprocessor that resolves input template type ResolveInputTemplates struct{} -func (d *ResolveInputTemplates) Process(pre plan.Plan) plan.Plan { - switch t := pre.(type) { - case *plan.SynchronousResponsePlan: - d.traverseNode(t.Response.Data) - case *plan.SubscriptionResponsePlan: - d.traverseTrigger(&t.Response.Trigger) - d.traverseNode(t.Response.Response.Data) - } - return pre +func (d *ResolveInputTemplates) Process(node resolve.Node) { + d.traverseNode(node) +} + +func (d *ResolveInputTemplates) ProcessSubscription(node resolve.Node, trigger *resolve.GraphQLSubscriptionTrigger) { + d.traverseTrigger(trigger) + d.traverseNode(node) } func (d *ResolveInputTemplates) traverseNode(node resolve.Node) { diff --git a/v2/pkg/engine/postprocess/resolve_input_templates_test.go b/v2/pkg/engine/postprocess/resolve_input_templates_test.go index 8dc362771..8e1f3bd6b 100644 --- a/v2/pkg/engine/postprocess/resolve_input_templates_test.go +++ b/v2/pkg/engine/postprocess/resolve_input_templates_test.go @@ -14,7 +14,7 @@ import ( func TestDataSourceInput_Process(t *testing.T) { pre := &plan.SynchronousResponsePlan{ Response: &resolve.GraphQLResponse{ - Data: &resolve.Object{ + FetchTree: &resolve.Object{ Fetch: &resolve.SingleFetch{ FetchConfiguration: resolve.FetchConfiguration{ Input: `{"method":"POST","url":"http://localhost:4001/$$0$$","body":{"query":"{me {id username __typename}}"}}`, @@ -159,7 +159,7 @@ func TestDataSourceInput_Process(t *testing.T) { expected := &plan.SynchronousResponsePlan{ Response: &resolve.GraphQLResponse{ - Data: &resolve.Object{ + FetchTree: &resolve.Object{ Fetch: &resolve.SingleFetch{ InputTemplate: resolve.InputTemplate{ Segments: []resolve.TemplateSegment{ @@ -343,10 +343,10 @@ func TestDataSourceInput_Process(t *testing.T) { } processor := &ResolveInputTemplates{} - actual := processor.Process(pre) + processor.Process(pre.Response.Data) - if !assert.Equal(t, expected, actual) { - actualBytes, _ := json.MarshalIndent(actual, "", " ") + if !assert.Equal(t, expected, pre) { + actualBytes, _ := json.MarshalIndent(pre, "", " ") expectedBytes, _ := json.MarshalIndent(expected, "", " ") if string(expectedBytes) != string(actualBytes) { diff --git a/v2/pkg/engine/resolve/loader.go b/v2/pkg/engine/resolve/loader.go index 4d2510093..c51fca1f5 100644 --- a/v2/pkg/engine/resolve/loader.go +++ b/v2/pkg/engine/resolve/loader.go @@ -16,12 +16,11 @@ import ( "github.com/buger/jsonparser" "github.com/pkg/errors" "github.com/tidwall/gjson" - "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/datasource/httpclient" "golang.org/x/sync/errgroup" "github.com/wundergraph/graphql-go-tools/v2/pkg/ast" - "github.com/wundergraph/graphql-go-tools/v2/pkg/astjson" + "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/datasource/httpclient" "github.com/wundergraph/graphql-go-tools/v2/pkg/pool" ) @@ -73,7 +72,7 @@ func (l *Loader) LoadGraphQLResponseData(ctx *Context, response *GraphQLResponse l.errorsRoot = resolvable.errorsRoot l.ctx = ctx l.info = response.Info - return l.walkNode(response.Data, []int{resolvable.dataRoot}) + return l.walkNode(response.FetchTree, []int{resolvable.dataRoot}) } func (l *Loader) walkNode(node Node, items []int) error { diff --git a/v2/pkg/engine/resolve/response.go b/v2/pkg/engine/resolve/response.go index 3f95951b9..d419697f0 100644 --- a/v2/pkg/engine/resolve/response.go +++ b/v2/pkg/engine/resolve/response.go @@ -24,7 +24,7 @@ type GraphQLSubscriptionTrigger struct { type GraphQLResponse struct { Data *Object - FetchData *Object + FetchTree *Object RenameTypeNames []RenameTypeName Info *GraphQLResponseInfo } From 6c72dcf5790c28dc7ace35db3482cc4055d27d3b Mon Sep 17 00:00:00 2001 From: spetrunin Date: Fri, 24 May 2024 18:31:23 +0300 Subject: [PATCH 05/15] use astjson to resolve response --- v2/pkg/astjson/astjson.go | 68 ++++++++ v2/pkg/engine/resolve/loader.go | 9 +- v2/pkg/engine/resolve/resolvable.go | 234 ++++++++++++++++------------ 3 files changed, 210 insertions(+), 101 deletions(-) diff --git a/v2/pkg/astjson/astjson.go b/v2/pkg/astjson/astjson.go index f64a8cc4d..b06b304ec 100644 --- a/v2/pkg/astjson/astjson.go +++ b/v2/pkg/astjson/astjson.go @@ -12,6 +12,8 @@ import ( "github.com/pkg/errors" ) +const InvalidRef = -1 + var ( Pool = &pool{ p: sync.Pool{ @@ -153,6 +155,33 @@ func (j *JSON) SetObjectField(nodeRef, setFieldNodeRef int, key string) bool { return false } +func (j *JSON) SetObjectFieldKeyBytes(nodeRef, setFieldNodeRef int, key []byte) bool { + for i, fieldRef := range j.Nodes[nodeRef].ObjectFields { + if j.objectFieldKeyEqualsBytes(fieldRef, key) { + objectFieldNodeRef := j.Nodes[nodeRef].ObjectFields[i] + j.Nodes[objectFieldNodeRef].ObjectFieldValue = setFieldNodeRef + return true + } + } + j.storage = append(j.storage, key...) + j.Nodes = append(j.Nodes, Node{ + Kind: NodeKindObjectField, + ObjectFieldValue: setFieldNodeRef, + keyStart: len(j.storage) - len(key), + keyEnd: len(j.storage), + }) + objectFieldNodeRef := len(j.Nodes) - 1 + j.Nodes[nodeRef].ObjectFields = append(j.Nodes[nodeRef].ObjectFields, objectFieldNodeRef) + return false +} + +func (j *JSON) AppendArrayValue(arrayNodeRef, newItemRef int) { + if j.Nodes[arrayNodeRef].ArrayValues == nil { + j.Nodes[arrayNodeRef].ArrayValues = j.getIntSlice() + } + j.Nodes[arrayNodeRef].ArrayValues = append(j.Nodes[arrayNodeRef].ArrayValues, newItemRef) +} + func (j *JSON) objectFieldKeyEquals(objectFieldRef int, another string) bool { key := j.ObjectFieldKey(objectFieldRef) if len(key) != len(another) { @@ -315,6 +344,45 @@ func (j *JSON) AppendString(input string) int { }) } +func (j *JSON) NodeIsPrimitive(ref int) bool { + if ref == -1 { + return false + } + switch j.Nodes[ref].Kind { + case NodeKindString, NodeKindNumber, NodeKindBoolean, NodeKindNull: + return true + default: + return false + } +} + +func (j *JSON) ImportPrimitiveNode(from *JSON, ref int) (int, error) { + if !from.NodeIsPrimitive(ref) { + return -1, fmt.Errorf("cannot import non-primitive node kind: %v", from.Nodes[ref].Kind) + } + + fromNode := from.Nodes[ref] + start := len(j.storage) + j.storage = append(j.storage, from.storage[fromNode.valueStart:fromNode.valueEnd]...) + end := len(j.storage) + return j.appendNode(Node{ + Kind: fromNode.Kind, + valueStart: start, + valueEnd: end, + }), nil +} + +func (j *JSON) AppendNull() int { + start := len(j.storage) + j.storage = append(j.storage, null...) + end := len(j.storage) + return j.appendNode(Node{ + Kind: NodeKindNull, + valueStart: start, + valueEnd: end, + }) +} + func (j *JSON) AppendInt(value int) int { start := len(j.storage) j.storage = append(j.storage, strconv.FormatInt(int64(value), 10)...) diff --git a/v2/pkg/engine/resolve/loader.go b/v2/pkg/engine/resolve/loader.go index c51fca1f5..55d2f59f1 100644 --- a/v2/pkg/engine/resolve/loader.go +++ b/v2/pkg/engine/resolve/loader.go @@ -72,7 +72,14 @@ func (l *Loader) LoadGraphQLResponseData(ctx *Context, response *GraphQLResponse l.errorsRoot = resolvable.errorsRoot l.ctx = ctx l.info = response.Info - return l.walkNode(response.FetchTree, []int{resolvable.dataRoot}) + + // fallback to data mostly for tests + fetchTree := response.FetchTree + if response.FetchTree == nil { + fetchTree = response.Data + } + + return l.walkNode(fetchTree, []int{resolvable.dataRoot}) } func (l *Loader) walkNode(node Node, items []int) error { diff --git a/v2/pkg/engine/resolve/resolvable.go b/v2/pkg/engine/resolve/resolvable.go index a7215cfff..238d99f62 100644 --- a/v2/pkg/engine/resolve/resolvable.go +++ b/v2/pkg/engine/resolve/resolvable.go @@ -131,7 +131,7 @@ func (r *Resolvable) Resolve(ctx context.Context, root *Object, out io.Writer) e * For example, if a fetch fails, only propagate that the fetch has failed; do not propagate nested non-null errors. */ - err := r.walkObject(root, r.dataRoot) + _, err := r.walkObject(root, r.dataRoot) if r.authorizationError != nil { return r.authorizationError } @@ -177,11 +177,10 @@ func (r *Resolvable) printData(root *Object) { r.printBytes(literalData) r.printBytes(quote) r.printBytes(colon) - r.printBytes(lBrace) r.print = true - _ = r.walkObject(root, r.dataRoot) + resolvedDataNodeRef, _ := r.walkObject(root, r.dataRoot) + r.printNode(resolvedDataNodeRef) r.print = false - r.printBytes(rBrace) r.wroteData = true } @@ -334,9 +333,9 @@ func (r *Resolvable) popNodePathElement(path []string) { r.depth-- } -func (r *Resolvable) walkNode(node Node, ref int) bool { +func (r *Resolvable) walkNode(node Node, ref int) (nodeRef int, hasError bool) { if r.authorizationError != nil { - return true + return astjson.InvalidRef, true } if r.print { r.ctx.Stats.ResolvedNodes++ @@ -367,18 +366,18 @@ func (r *Resolvable) walkNode(node Node, ref int) bool { case *CustomNode: return r.walkCustom(n, ref) default: - return false + return astjson.InvalidRef, false } } -func (r *Resolvable) walkObject(obj *Object, ref int) bool { +func (r *Resolvable) walkObject(obj *Object, ref int) (nodeRef int, hasError bool) { ref = r.storage.Get(ref, obj.Path) if !r.storage.NodeIsDefined(ref) { if obj.Nullable { return r.walkNull() } r.addNonNullableFieldError(ref, obj.Path) - return r.err() + return astjson.InvalidRef, r.err() } r.pushNodePathElement(obj.Path) isRoot := r.depth < 2 @@ -389,13 +388,16 @@ func (r *Resolvable) walkObject(obj *Object, ref int) bool { } if r.storage.Nodes[ref].Kind != astjson.NodeKindObject { r.addError("Object cannot represent non-object value.", obj.Path) - return r.err() + return astjson.InvalidRef, r.err() } - if r.print && !isRoot { - r.printBytes(lBrace) - r.ctx.Stats.ResolvedObjects++ + + objectNodeRef := astjson.InvalidRef + if r.print { + if !isRoot { + r.ctx.Stats.ResolvedObjects++ + } + objectNodeRef, _ = r.storage.AppendObject(emptyObject) } - addComma := false for i := range obj.Fields { if obj.Fields[i].SkipDirectiveDefined { if r.skipField(obj.Fields[i].SkipVariableName) { @@ -429,34 +431,33 @@ func (r *Resolvable) walkObject(obj *Object, ref int) bool { } else { // if the field value is not nullable and the object is not nullable // we return true to indicate an error - return true + return astjson.InvalidRef, true } continue } } - if r.print { - if addComma { - r.printBytes(comma) - } - r.printBytes(quote) - r.printBytes(obj.Fields[i].Name) - r.printBytes(quote) - r.printBytes(colon) - } - err := r.walkNode(obj.Fields[i].Value, ref) + + fieldNodeRef, err := r.walkNode(obj.Fields[i].Value, ref) if err { if obj.Nullable { + // set ref to null so we have early return on next round of walk r.storage.Nodes[ref].Kind = astjson.NodeKindNull - return false + if r.print { + return r.walkNull() + } + return astjson.InvalidRef, false } - return err + return astjson.InvalidRef, err + } + + if r.print { + fieldTmpObjectRef, _ := r.storage.AppendObject(emptyObject) + r.storage.SetObjectFieldKeyBytes(fieldTmpObjectRef, fieldNodeRef, obj.Fields[i].Name) + objectNodeRef = r.storage.MergeNodes(objectNodeRef, fieldTmpObjectRef) } - addComma = true - } - if r.print && !isRoot { - r.printBytes(rBrace) } - return false + + return objectNodeRef, false } func (r *Resolvable) authorizeField(ref int, field *Field) (skipField bool) { @@ -594,54 +595,58 @@ func (r *Resolvable) excludeField(includeVariableName string) bool { return bytes.Equal(value, literalFalse) } -func (r *Resolvable) walkArray(arr *Array, ref int) bool { +func (r *Resolvable) walkArray(arr *Array, ref int) (nodeRef int, hasError bool) { ref = r.storage.Get(ref, arr.Path) if !r.storage.NodeIsDefined(ref) { if arr.Nullable { return r.walkNull() } r.addNonNullableFieldError(ref, arr.Path) - return r.err() + return astjson.InvalidRef, r.err() } r.pushNodePathElement(arr.Path) defer r.popNodePathElement(arr.Path) if r.storage.Nodes[ref].Kind != astjson.NodeKindArray { r.addError("Array cannot represent non-array value.", arr.Path) - return r.err() + return astjson.InvalidRef, r.err() } + + arrayNodeRef := astjson.InvalidRef if r.print { - r.printBytes(lBrack) + arrayNodeRef, _ = r.storage.AppendArray(emptyArray) } for i, value := range r.storage.Nodes[ref].ArrayValues { - if r.print && i != 0 { - r.printBytes(comma) - } r.pushArrayPathElement(i) - err := r.walkNode(arr.Item, value) + itemNodeRef, err := r.walkNode(arr.Item, value) r.popArrayPathElement() if err { if arr.Nullable { + // set ref to null so we have early return on next round of walk r.storage.Nodes[ref].Kind = astjson.NodeKindNull - return false + if r.print { + return r.walkNull() + } + return astjson.InvalidRef, false } - return err + return astjson.InvalidRef, err + } + + if r.print { + r.storage.AppendArrayValue(arrayNodeRef, itemNodeRef) } } - if r.print { - r.printBytes(rBrack) - } - return false + return arrayNodeRef, false } -func (r *Resolvable) walkNull() bool { +func (r *Resolvable) walkNull() (nodeRef int, hasError bool) { if r.print { - r.printBytes(null) r.ctx.Stats.ResolvedLeafs++ + return r.storage.AppendNull(), false } - return false + return astjson.InvalidRef, false } -func (r *Resolvable) walkString(s *String, ref int) bool { +func (r *Resolvable) walkString(s *String, ref int) (nodeRef int, hasError bool) { if r.print { r.ctx.Stats.ResolvedLeafs++ } @@ -651,45 +656,46 @@ func (r *Resolvable) walkString(s *String, ref int) bool { return r.walkNull() } r.addNonNullableFieldError(ref, s.Path) - return r.err() + return astjson.InvalidRef, r.err() } if r.storage.Nodes[ref].Kind != astjson.NodeKindString { value := string(r.storage.Nodes[ref].ValueBytes(r.storage)) r.addError(fmt.Sprintf("String cannot represent non-string value: \\\"%s\\\"", value), s.Path) - return r.err() + return astjson.InvalidRef, r.err() } if r.print { if s.IsTypeName { value := r.storage.Nodes[ref].ValueBytes(r.storage) for i := range r.renameTypeNames { if bytes.Equal(value, r.renameTypeNames[i].From) { - r.printBytes(quote) - r.printBytes(r.renameTypeNames[i].To) - r.printBytes(quote) - return false + return r.storage.AppendStringBytes(r.renameTypeNames[i].To), false } } - r.printNode(ref) - return false + nodeRef, _ = r.storage.ImportPrimitiveNode(r.storage, ref) + return nodeRef, false } if s.UnescapeResponseJson { value := r.storage.Nodes[ref].ValueBytes(r.storage) value = bytes.ReplaceAll(value, []byte(`\"`), []byte(`"`)) if !gjson.ValidBytes(value) { - r.printBytes(quote) - r.printBytes(value) - r.printBytes(quote) + return r.storage.AppendStringBytes(value), false } else { - r.printBytes(value) + nodeRef, err := r.storage.AppendAnyJSONBytes(value) + if err != nil { + r.addError(err.Error(), s.Path) + return astjson.InvalidRef, r.err() + } + return nodeRef, false } } else { - r.printNode(ref) + nodeRef, _ = r.storage.ImportPrimitiveNode(r.storage, ref) + return nodeRef, false } } - return false + return astjson.InvalidRef, false } -func (r *Resolvable) walkBoolean(b *Boolean, ref int) bool { +func (r *Resolvable) walkBoolean(b *Boolean, ref int) (nodeRef int, hasError bool) { if r.print { r.ctx.Stats.ResolvedLeafs++ } @@ -699,20 +705,21 @@ func (r *Resolvable) walkBoolean(b *Boolean, ref int) bool { return r.walkNull() } r.addNonNullableFieldError(ref, b.Path) - return r.err() + return astjson.InvalidRef, r.err() } if r.storage.Nodes[ref].Kind != astjson.NodeKindBoolean { value := string(r.storage.Nodes[ref].ValueBytes(r.storage)) r.addError(fmt.Sprintf("Bool cannot represent non-boolean value: \\\"%s\\\"", value), b.Path) - return r.err() + return astjson.InvalidRef, r.err() } if r.print { - r.printNode(ref) + nodeRef, _ = r.storage.ImportPrimitiveNode(r.storage, ref) + return nodeRef, false } - return false + return astjson.InvalidRef, false } -func (r *Resolvable) walkInteger(i *Integer, ref int) bool { +func (r *Resolvable) walkInteger(i *Integer, ref int) (nodeRef int, hasError bool) { if r.print { r.ctx.Stats.ResolvedLeafs++ } @@ -722,20 +729,21 @@ func (r *Resolvable) walkInteger(i *Integer, ref int) bool { return r.walkNull() } r.addNonNullableFieldError(ref, i.Path) - return r.err() + return astjson.InvalidRef, r.err() } if r.storage.Nodes[ref].Kind != astjson.NodeKindNumber { value := string(r.storage.Nodes[ref].ValueBytes(r.storage)) r.addError(fmt.Sprintf("Int cannot represent non-integer value: \\\"%s\\\"", value), i.Path) - return r.err() + return astjson.InvalidRef, r.err() } if r.print { - r.printNode(ref) + nodeRef, _ = r.storage.ImportPrimitiveNode(r.storage, ref) + return nodeRef, false } - return false + return astjson.InvalidRef, false } -func (r *Resolvable) walkFloat(f *Float, ref int) bool { +func (r *Resolvable) walkFloat(f *Float, ref int) (nodeRef int, hasError bool) { if r.print { r.ctx.Stats.ResolvedLeafs++ } @@ -745,20 +753,21 @@ func (r *Resolvable) walkFloat(f *Float, ref int) bool { return r.walkNull() } r.addNonNullableFieldError(ref, f.Path) - return r.err() + return astjson.InvalidRef, r.err() } if r.storage.Nodes[ref].Kind != astjson.NodeKindNumber { value := string(r.storage.Nodes[ref].ValueBytes(r.storage)) r.addError(fmt.Sprintf("Float cannot represent non-float value: \\\"%s\\\"", value), f.Path) - return r.err() + return astjson.InvalidRef, r.err() } if r.print { - r.printNode(ref) + nodeRef, _ = r.storage.ImportPrimitiveNode(r.storage, ref) + return nodeRef, false } - return false + return astjson.InvalidRef, false } -func (r *Resolvable) walkBigInt(b *BigInt, ref int) bool { +func (r *Resolvable) walkBigInt(b *BigInt, ref int) (nodeRef int, hasError bool) { if r.print { r.ctx.Stats.ResolvedLeafs++ } @@ -768,15 +777,16 @@ func (r *Resolvable) walkBigInt(b *BigInt, ref int) bool { return r.walkNull() } r.addNonNullableFieldError(ref, b.Path) - return r.err() + return astjson.InvalidRef, r.err() } if r.print { - r.printNode(ref) + nodeRef, _ = r.storage.ImportPrimitiveNode(r.storage, ref) + return nodeRef, false } - return false + return astjson.InvalidRef, false } -func (r *Resolvable) walkScalar(s *Scalar, ref int) bool { +func (r *Resolvable) walkScalar(s *Scalar, ref int) (nodeRef int, hasError bool) { if r.print { r.ctx.Stats.ResolvedLeafs++ } @@ -786,31 +796,50 @@ func (r *Resolvable) walkScalar(s *Scalar, ref int) bool { return r.walkNull() } r.addNonNullableFieldError(ref, s.Path) - return r.err() + return astjson.InvalidRef, r.err() } if r.print { - r.printNode(ref) + if r.storage.NodeIsPrimitive(ref) { + nodeRef, _ = r.storage.ImportPrimitiveNode(r.storage, ref) + return nodeRef, false + } + + buf := pool.BytesBuffer.Get() + defer pool.BytesBuffer.Put(buf) + + err := r.storage.PrintNode(r.storage.Nodes[ref], buf) + if err != nil { + r.printErr = err + return astjson.InvalidRef, r.err() + } + nodeRef, err := r.storage.AppendAnyJSONBytes(buf.Bytes()) + if err != nil { + r.printErr = err + return astjson.InvalidRef, r.err() + } + + return nodeRef, false } - return false + return astjson.InvalidRef, false } -func (r *Resolvable) walkEmptyObject(_ *EmptyObject) bool { +func (r *Resolvable) walkEmptyObject(_ *EmptyObject) (nodeRef int, hasError bool) { if r.print { - r.printBytes(lBrace) - r.printBytes(rBrace) + nodeRef, _ = r.storage.AppendObject(emptyObject) + return nodeRef, false } - return false + return astjson.InvalidRef, false } -func (r *Resolvable) walkEmptyArray(_ *EmptyArray) bool { +func (r *Resolvable) walkEmptyArray(_ *EmptyArray) (nodeRef int, hasError bool) { if r.print { - r.printBytes(lBrack) - r.printBytes(rBrack) + nodeRef, _ = r.storage.AppendArray(emptyArray) + return nodeRef, false } - return false + return astjson.InvalidRef, false } -func (r *Resolvable) walkCustom(c *CustomNode, ref int) bool { +func (r *Resolvable) walkCustom(c *CustomNode, ref int) (nodeRef int, hasError bool) { if r.print { r.ctx.Stats.ResolvedLeafs++ } @@ -820,18 +849,23 @@ func (r *Resolvable) walkCustom(c *CustomNode, ref int) bool { return r.walkNull() } r.addNonNullableFieldError(ref, c.Path) - return r.err() + return astjson.InvalidRef, r.err() } value := r.storage.Nodes[ref].ValueBytes(r.storage) resolved, err := c.Resolve(r.ctx, value) if err != nil { r.addError(err.Error(), c.Path) - return r.err() + return astjson.InvalidRef, r.err() } if r.print { - r.printBytes(resolved) + nodeRef, err = r.storage.AppendAnyJSONBytes(resolved) + if err != nil { + r.addError(err.Error(), c.Path) + return astjson.InvalidRef, r.err() + } + return nodeRef, false } - return false + return astjson.InvalidRef, false } func (r *Resolvable) addNonNullableFieldError(fieldRef int, fieldPath []string) { From 5da9ff3789ccc1cf6cfb1756ff82c3b29e1a4410 Mon Sep 17 00:00:00 2001 From: spetrunin Date: Sat, 25 May 2024 08:23:54 +0300 Subject: [PATCH 06/15] add postprocess constructor make extract fetches configurable --- .../datasourcetesting/datasourcetesting.go | 15 ++++++---- v2/pkg/engine/postprocess/postprocess.go | 29 ++++++++++++++++--- v2/pkg/engine/postprocess/postprocess_test.go | 2 +- .../resolve_input_templates_test.go | 2 +- 4 files changed, 37 insertions(+), 11 deletions(-) diff --git a/v2/pkg/engine/datasourcetesting/datasourcetesting.go b/v2/pkg/engine/datasourcetesting/datasourcetesting.go index 33ccc2444..e2354f887 100644 --- a/v2/pkg/engine/datasourcetesting/datasourcetesting.go +++ b/v2/pkg/engine/datasourcetesting/datasourcetesting.go @@ -24,8 +24,9 @@ import ( ) type testOptions struct { - postProcessors []postprocess.PostProcessor - skipReason string + postProcessors []postprocess.PostProcessor + postProcessExtractFetches bool + skipReason string } func WithPostProcessors(postProcessors ...postprocess.PostProcessor) func(*testOptions) { @@ -34,6 +35,12 @@ func WithPostProcessors(postProcessors ...postprocess.PostProcessor) func(*testO } } +func WithPostProcessExtractFetches() func(*testOptions) { + return func(o *testOptions) { + o.postProcessExtractFetches = true + } +} + func WithSkipReason(reason string) func(*testOptions) { return func(o *testOptions) { o.skipReason = reason @@ -133,9 +140,7 @@ func RunTest(definition, operation, operationName string, expectedPlan plan.Plan } if opts.postProcessors != nil { - for _, pp := range opts.postProcessors { - actualPlan = pp.Process(actualPlan) - } + postprocess.NewProcessor(opts.postProcessors, opts.postProcessExtractFetches).Process(actualPlan) } actualBytes, _ := json.MarshalIndent(actualPlan, "", " ") diff --git a/v2/pkg/engine/postprocess/postprocess.go b/v2/pkg/engine/postprocess/postprocess.go index 69133b2a1..1e7622c8c 100644 --- a/v2/pkg/engine/postprocess/postprocess.go +++ b/v2/pkg/engine/postprocess/postprocess.go @@ -11,16 +11,25 @@ type PostProcessor interface { } type Processor struct { - postProcessors []PostProcessor + postProcessors []PostProcessor + enableExtractFetches bool +} + +func NewProcessor(postProcessors []PostProcessor, extractFetches bool) *Processor { + return &Processor{ + postProcessors: postProcessors, + enableExtractFetches: extractFetches, + } } func DefaultProcessor() *Processor { return &Processor{ - []PostProcessor{ + postProcessors: []PostProcessor{ &ResolveInputTemplates{}, &CreateMultiFetchTypes{}, &CreateConcreteSingleFetchTypes{}, }, + enableExtractFetches: true, } } @@ -29,13 +38,21 @@ func (p *Processor) Process(pre plan.Plan) plan.Plan { case *plan.SynchronousResponsePlan: p.extractFetches(t.Response) for i := range p.postProcessors { - p.postProcessors[i].Process(t.Response.FetchTree) + if p.enableExtractFetches { + p.postProcessors[i].Process(t.Response.FetchTree) + } else { + p.postProcessors[i].Process(t.Response.Data) + } } case *plan.SubscriptionResponsePlan: p.extractFetches(t.Response.Response) for i := range p.postProcessors { - p.postProcessors[i].ProcessSubscription(t.Response.Response.FetchTree, &t.Response.Trigger) + if p.enableExtractFetches { + p.postProcessors[i].ProcessSubscription(t.Response.Response.FetchTree, &t.Response.Trigger) + } else { + p.postProcessors[i].ProcessSubscription(t.Response.Response.Data, &t.Response.Trigger) + } } } @@ -43,6 +60,10 @@ func (p *Processor) Process(pre plan.Plan) plan.Plan { } func (p *Processor) extractFetches(res *resolve.GraphQLResponse) { + if !p.enableExtractFetches { + return + } + fieldsWithFetch := NewFetchFinder().Find(res) createFetchesCopy := NewFetchTreeCreator(fieldsWithFetch) diff --git a/v2/pkg/engine/postprocess/postprocess_test.go b/v2/pkg/engine/postprocess/postprocess_test.go index 18ebe9f90..3c57dfd29 100644 --- a/v2/pkg/engine/postprocess/postprocess_test.go +++ b/v2/pkg/engine/postprocess/postprocess_test.go @@ -279,7 +279,7 @@ func TestProcess_ExtractFetches(t *testing.T) { }, } - processor := &Processor{} + processor := &Processor{enableExtractFetches: true} for _, c := range cases { t.Run(c.name, func(t *testing.T) { diff --git a/v2/pkg/engine/postprocess/resolve_input_templates_test.go b/v2/pkg/engine/postprocess/resolve_input_templates_test.go index 8e1f3bd6b..1bf3f18c6 100644 --- a/v2/pkg/engine/postprocess/resolve_input_templates_test.go +++ b/v2/pkg/engine/postprocess/resolve_input_templates_test.go @@ -343,7 +343,7 @@ func TestDataSourceInput_Process(t *testing.T) { } processor := &ResolveInputTemplates{} - processor.Process(pre.Response.Data) + processor.Process(pre.Response.FetchTree) if !assert.Equal(t, expected, pre) { actualBytes, _ := json.MarshalIndent(pre, "", " ") From 09682b5f982553b5f246c4f8ed41b1e03bd4c48f Mon Sep 17 00:00:00 2001 From: spetrunin Date: Sat, 25 May 2024 11:35:07 +0300 Subject: [PATCH 07/15] fix setting plan visitor current path --- v2/pkg/engine/postprocess/plan_visitor.go | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/v2/pkg/engine/postprocess/plan_visitor.go b/v2/pkg/engine/postprocess/plan_visitor.go index eb381e2ca..bcc2601d8 100644 --- a/v2/pkg/engine/postprocess/plan_visitor.go +++ b/v2/pkg/engine/postprocess/plan_visitor.go @@ -151,6 +151,9 @@ func (e *PlanWalker) walkNode(node resolve.Node) { } func (e *PlanWalker) walkField(field *resolve.Field) { + e.pushPath(field.Value.NodePath()) + defer e.popPath(field.Value.NodePath()) + e.onEnterField(field) defer e.onLeaveField(field) @@ -177,9 +180,6 @@ func (e *PlanWalker) onLeaveField(field *resolve.Field) { } func (e *PlanWalker) walkObject(object *resolve.Object) { - e.pushPath(object.Path) - defer e.popPath(object.Path) - e.objectVisitor.EnterObject(object) defer e.objectVisitor.LeaveObject(object) @@ -216,9 +216,6 @@ func (e *PlanWalker) onLeaveArray(array *resolve.Array) { } func (e *PlanWalker) walkArray(array *resolve.Array) { - e.pushPath(array.Path) - defer e.popPath(array.Path) - e.pushArrayPath() defer e.popArrayPath() From ceb7639fc0958e8b117de71a263b18d060325321 Mon Sep 17 00:00:00 2001 From: spetrunin Date: Sat, 25 May 2024 15:20:47 +0300 Subject: [PATCH 08/15] fix tests - add separate not merged response nodes for different types combination --- ...ource_federation_entity_interfaces_test.go | 634 +++++++++++++++++- .../graphql_datasource_federation_test.go | 25 +- .../graphql_datasource_test.go | 53 +- v2/pkg/engine/plan/planner_test.go | 28 +- v2/pkg/engine/plan/schemausageinfo_test.go | 11 +- 5 files changed, 695 insertions(+), 56 deletions(-) diff --git a/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_entity_interfaces_test.go b/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_entity_interfaces_test.go index 61aacb7e6..a616f4c3b 100644 --- a/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_entity_interfaces_test.go +++ b/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_entity_interfaces_test.go @@ -142,7 +142,7 @@ func TestGraphQLDataSourceFederationEntityInterfaces(t *testing.T) { Value: &resolve.Scalar{ Path: []string{"id"}, }, - OnTypeNames: [][]byte{[]byte("Admin"), []byte("Moderator"), []byte("User")}, + OnTypeNames: [][]byte{[]byte("Admin")}, }, { Name: []byte("locations"), @@ -160,7 +160,57 @@ func TestGraphQLDataSourceFederationEntityInterfaces(t *testing.T) { }, }, }, - OnTypeNames: [][]byte{[]byte("Admin"), []byte("Account"), []byte("Moderator"), []byte("User")}, + OnTypeNames: [][]byte{[]byte("Admin"), []byte("Account")}, + }, + { + Name: []byte("id"), + Value: &resolve.Scalar{ + Path: []string{"id"}, + }, + OnTypeNames: [][]byte{[]byte("Moderator")}, + }, + { + Name: []byte("locations"), + Value: &resolve.Array{ + Path: []string{"locations"}, + Nullable: true, + Item: &resolve.Object{ + Fields: []*resolve.Field{ + { + Name: []byte("country"), + Value: &resolve.String{ + Path: []string{"country"}, + }, + }, + }, + }, + }, + OnTypeNames: [][]byte{[]byte("Moderator"), []byte("Account")}, + }, + { + Name: []byte("id"), + Value: &resolve.Scalar{ + Path: []string{"id"}, + }, + OnTypeNames: [][]byte{[]byte("User")}, + }, + { + Name: []byte("locations"), + Value: &resolve.Array{ + Path: []string{"locations"}, + Nullable: true, + Item: &resolve.Object{ + Fields: []*resolve.Field{ + { + Name: []byte("country"), + Value: &resolve.String{ + Path: []string{"country"}, + }, + }, + }, + }, + }, + OnTypeNames: [][]byte{[]byte("User"), []byte("Account")}, }, }, Fetch: &resolve.SingleFetch{ @@ -280,7 +330,71 @@ func TestGraphQLDataSourceFederationEntityInterfaces(t *testing.T) { Value: &resolve.Scalar{ Path: []string{"id"}, }, - OnTypeNames: [][]byte{[]byte("Admin"), []byte("Moderator"), []byte("User")}, + OnTypeNames: [][]byte{[]byte("Admin")}, + }, + { + Name: []byte("locations"), + Value: &resolve.Array{ + Path: []string{"locations"}, + Nullable: true, + Item: &resolve.Object{ + Fields: []*resolve.Field{ + { + Name: []byte("country"), + Value: &resolve.String{ + Path: []string{"country"}, + }, + }, + }, + }, + }, + OnTypeNames: [][]byte{[]byte("Admin"), []byte("Account")}, + }, + { + Name: []byte("age"), + Value: &resolve.Integer{ + Path: []string{"age"}, + }, + OnTypeNames: [][]byte{[]byte("Admin"), []byte("Account")}, + }, + { + Name: []byte("id"), + Value: &resolve.Scalar{ + Path: []string{"id"}, + }, + OnTypeNames: [][]byte{[]byte("Moderator")}, + }, + { + Name: []byte("locations"), + Value: &resolve.Array{ + Path: []string{"locations"}, + Nullable: true, + Item: &resolve.Object{ + Fields: []*resolve.Field{ + { + Name: []byte("country"), + Value: &resolve.String{ + Path: []string{"country"}, + }, + }, + }, + }, + }, + OnTypeNames: [][]byte{[]byte("Moderator"), []byte("Account")}, + }, + { + Name: []byte("age"), + Value: &resolve.Integer{ + Path: []string{"age"}, + }, + OnTypeNames: [][]byte{[]byte("Moderator"), []byte("Account")}, + }, + { + Name: []byte("id"), + Value: &resolve.Scalar{ + Path: []string{"id"}, + }, + OnTypeNames: [][]byte{[]byte("User")}, }, { Name: []byte("locations"), @@ -298,14 +412,14 @@ func TestGraphQLDataSourceFederationEntityInterfaces(t *testing.T) { }, }, }, - OnTypeNames: [][]byte{[]byte("Admin"), []byte("Account"), []byte("Moderator"), []byte("User")}, + OnTypeNames: [][]byte{[]byte("User"), []byte("Account")}, }, { Name: []byte("age"), Value: &resolve.Integer{ Path: []string{"age"}, }, - OnTypeNames: [][]byte{[]byte("Admin"), []byte("Account"), []byte("Moderator"), []byte("User")}, + OnTypeNames: [][]byte{[]byte("User"), []byte("Account")}, }, }, Fetch: &resolve.ParallelFetch{ @@ -1098,7 +1212,14 @@ func TestGraphQLDataSourceFederationEntityInterfaces(t *testing.T) { Value: &resolve.String{ Path: []string{"title"}, }, - OnTypeNames: [][]byte{[]byte("User"), []byte("Admin")}, + OnTypeNames: [][]byte{[]byte("User")}, + }, + { + Name: []byte("title"), + Value: &resolve.String{ + Path: []string{"title"}, + }, + OnTypeNames: [][]byte{[]byte("Admin")}, }, }, Fetch: &resolve.SerialFetch{ @@ -1243,14 +1364,78 @@ func TestGraphQLDataSourceFederationEntityInterfaces(t *testing.T) { Value: &resolve.Scalar{ Path: []string{"id"}, }, - OnTypeNames: [][]byte{[]byte("Admin"), []byte("Moderator"), []byte("User")}, + OnTypeNames: [][]byte{[]byte("Admin")}, + }, + { + Name: []byte("title"), + Value: &resolve.String{ + Path: []string{"title"}, + }, + OnTypeNames: [][]byte{[]byte("Admin")}, + }, + { + Name: []byte("locations"), + Value: &resolve.Array{ + Path: []string{"locations"}, + Nullable: true, + Item: &resolve.Object{ + Fields: []*resolve.Field{ + { + Name: []byte("country"), + Value: &resolve.String{ + Path: []string{"country"}, + }, + }, + }, + }, + }, + OnTypeNames: [][]byte{[]byte("Admin"), []byte("Account")}, + }, + { + Name: []byte("id"), + Value: &resolve.Scalar{ + Path: []string{"id"}, + }, + OnTypeNames: [][]byte{[]byte("Moderator")}, + }, + { + Name: []byte("title"), + Value: &resolve.String{ + Path: []string{"title"}, + }, + OnTypeNames: [][]byte{[]byte("Moderator")}, + }, + { + Name: []byte("locations"), + Value: &resolve.Array{ + Path: []string{"locations"}, + Nullable: true, + Item: &resolve.Object{ + Fields: []*resolve.Field{ + { + Name: []byte("country"), + Value: &resolve.String{ + Path: []string{"country"}, + }, + }, + }, + }, + }, + OnTypeNames: [][]byte{[]byte("Moderator"), []byte("Account")}, + }, + { + Name: []byte("id"), + Value: &resolve.Scalar{ + Path: []string{"id"}, + }, + OnTypeNames: [][]byte{[]byte("User")}, }, { Name: []byte("title"), Value: &resolve.String{ Path: []string{"title"}, }, - OnTypeNames: [][]byte{[]byte("Admin"), []byte("Moderator"), []byte("User")}, + OnTypeNames: [][]byte{[]byte("User")}, }, { Name: []byte("locations"), @@ -1268,7 +1453,7 @@ func TestGraphQLDataSourceFederationEntityInterfaces(t *testing.T) { }, }, }, - OnTypeNames: [][]byte{[]byte("Admin"), []byte("Account"), []byte("Moderator"), []byte("User")}, + OnTypeNames: [][]byte{[]byte("User"), []byte("Account")}, }, }, Fetch: &resolve.ParallelFetch{ @@ -1430,14 +1615,92 @@ func TestGraphQLDataSourceFederationEntityInterfaces(t *testing.T) { Value: &resolve.Scalar{ Path: []string{"id"}, }, - OnTypeNames: [][]byte{[]byte("Admin"), []byte("Moderator"), []byte("User")}, + OnTypeNames: [][]byte{[]byte("Admin")}, + }, + { + Name: []byte("title"), + Value: &resolve.String{ + Path: []string{"title"}, + }, + OnTypeNames: [][]byte{[]byte("Admin")}, + }, + { + Name: []byte("locations"), + Value: &resolve.Array{ + Path: []string{"locations"}, + Nullable: true, + Item: &resolve.Object{ + Fields: []*resolve.Field{ + { + Name: []byte("country"), + Value: &resolve.String{ + Path: []string{"country"}, + }, + }, + }, + }, + }, + OnTypeNames: [][]byte{[]byte("Admin"), []byte("Account")}, + }, + { + Name: []byte("age"), + Value: &resolve.Integer{ + Path: []string{"age"}, + }, + OnTypeNames: [][]byte{[]byte("Admin"), []byte("Account")}, + }, + { + Name: []byte("id"), + Value: &resolve.Scalar{ + Path: []string{"id"}, + }, + OnTypeNames: [][]byte{[]byte("Moderator")}, + }, + { + Name: []byte("title"), + Value: &resolve.String{ + Path: []string{"title"}, + }, + OnTypeNames: [][]byte{[]byte("Moderator")}, + }, + { + Name: []byte("locations"), + Value: &resolve.Array{ + Path: []string{"locations"}, + Nullable: true, + Item: &resolve.Object{ + Fields: []*resolve.Field{ + { + Name: []byte("country"), + Value: &resolve.String{ + Path: []string{"country"}, + }, + }, + }, + }, + }, + OnTypeNames: [][]byte{[]byte("Moderator"), []byte("Account")}, + }, + { + Name: []byte("age"), + Value: &resolve.Integer{ + Path: []string{"age"}, + }, + OnTypeNames: [][]byte{[]byte("Moderator"), []byte("Account")}, + }, + { + Name: []byte("id"), + Value: &resolve.Scalar{ + Path: []string{"id"}, + }, + OnTypeNames: [][]byte{[]byte("User")}, }, { Name: []byte("title"), Value: &resolve.String{ Path: []string{"title"}, }, - OnTypeNames: [][]byte{[]byte("Admin"), []byte("Moderator"), []byte("User")}, + OnTypeNames: [][]byte{[]byte("User")}, }, { Name: []byte("locations"), @@ -1455,14 +1718,14 @@ func TestGraphQLDataSourceFederationEntityInterfaces(t *testing.T) { }, }, }, - OnTypeNames: [][]byte{[]byte("Admin"), []byte("Account"), []byte("Moderator"), []byte("User")}, + OnTypeNames: [][]byte{[]byte("User"), []byte("Account")}, }, { Name: []byte("age"), Value: &resolve.Integer{ Path: []string{"age"}, }, - OnTypeNames: [][]byte{[]byte("Admin"), []byte("Account"), []byte("Moderator"), []byte("User")}, + OnTypeNames: [][]byte{[]byte("User"), []byte("Account")}, }, }, Fetch: &resolve.ParallelFetch{ @@ -1682,20 +1945,47 @@ func TestGraphQLDataSourceFederationEntityInterfaces(t *testing.T) { Value: &resolve.Scalar{ Path: []string{"id"}, }, - OnTypeNames: [][]byte{[]byte("Admin"), []byte("Account"), []byte("Moderator"), []byte("User")}, + OnTypeNames: [][]byte{[]byte("Admin"), []byte("Account")}, }, { Name: []byte("title"), Value: &resolve.String{ Path: []string{"title"}, }, - OnTypeNames: [][]byte{[]byte("Admin"), []byte("Moderator"), []byte("User")}, + OnTypeNames: [][]byte{[]byte("Admin")}, }, - }, - Fetch: &resolve.SerialFetch{ - Fetches: []resolve.Fetch{ - &resolve.SingleFetch{ - FetchID: 2, + { + Name: []byte("id"), + Value: &resolve.Scalar{ + Path: []string{"id"}, + }, + OnTypeNames: [][]byte{[]byte("Moderator"), []byte("Account")}}, + { + Name: []byte("title"), + Value: &resolve.String{ + Path: []string{"title"}, + }, + OnTypeNames: [][]byte{[]byte("Moderator")}, + }, + { + Name: []byte("id"), + Value: &resolve.Scalar{ + Path: []string{"id"}, + }, + OnTypeNames: [][]byte{[]byte("User"), []byte("Account")}, + }, + { + Name: []byte("title"), + Value: &resolve.String{ + Path: []string{"title"}, + }, + OnTypeNames: [][]byte{[]byte("User")}, + }, + }, + Fetch: &resolve.SerialFetch{ + Fetches: []resolve.Fetch{ + &resolve.SingleFetch{ + FetchID: 2, DependsOnFetchIDs: []int{0}, FetchConfiguration: resolve.FetchConfiguration{ Input: `{"method":"POST","url":"http://localhost:4001/graphql","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations){__typename ... on Admin {__typename} ... on Moderator {title __typename} ... on User {title __typename}}}","variables":{"representations":[$$0$$]}}}`, @@ -1841,14 +2131,58 @@ func TestGraphQLDataSourceFederationEntityInterfaces(t *testing.T) { Value: &resolve.Scalar{ Path: []string{"id"}, }, - OnTypeNames: [][]byte{[]byte("Admin"), []byte("Account"), []byte("Moderator"), []byte("User")}, + OnTypeNames: [][]byte{[]byte("Admin"), []byte("Account")}, + }, + { + Name: []byte("title"), + Value: &resolve.String{ + Path: []string{"title"}, + }, + OnTypeNames: [][]byte{[]byte("Admin")}, + }, + { + Name: []byte("__typename"), + Value: &resolve.String{ + Path: []string{"__typename"}, + IsTypeName: true, + }, + OnTypeNames: [][]byte{[]byte("Admin")}, + }, + { + Name: []byte("id"), + Value: &resolve.Scalar{ + Path: []string{"id"}, + }, + OnTypeNames: [][]byte{[]byte("Moderator"), []byte("Account")}, + }, + { + Name: []byte("title"), + Value: &resolve.String{ + Path: []string{"title"}, + }, + OnTypeNames: [][]byte{[]byte("Moderator")}, + }, + { + Name: []byte("__typename"), + Value: &resolve.String{ + Path: []string{"__typename"}, + IsTypeName: true, + }, + OnTypeNames: [][]byte{[]byte("Moderator")}, + }, + { + Name: []byte("id"), + Value: &resolve.Scalar{ + Path: []string{"id"}, + }, + OnTypeNames: [][]byte{[]byte("User"), []byte("Account")}, }, { Name: []byte("title"), Value: &resolve.String{ Path: []string{"title"}, }, - OnTypeNames: [][]byte{[]byte("Admin"), []byte("Moderator"), []byte("User")}, + OnTypeNames: [][]byte{[]byte("User")}, }, { Name: []byte("__typename"), @@ -1856,7 +2190,7 @@ func TestGraphQLDataSourceFederationEntityInterfaces(t *testing.T) { Path: []string{"__typename"}, IsTypeName: true, }, - OnTypeNames: [][]byte{[]byte("Admin"), []byte("Moderator"), []byte("User")}, + OnTypeNames: [][]byte{[]byte("User")}, }, }, Fetch: &resolve.SerialFetch{ @@ -2008,21 +2342,63 @@ func TestGraphQLDataSourceFederationEntityInterfaces(t *testing.T) { Value: &resolve.Scalar{ Path: []string{"id"}, }, - OnTypeNames: [][]byte{[]byte("Admin"), []byte("Account"), []byte("Moderator"), []byte("User")}, + OnTypeNames: [][]byte{[]byte("Admin"), []byte("Account")}, }, { Name: []byte("title"), Value: &resolve.String{ Path: []string{"title"}, }, - OnTypeNames: [][]byte{[]byte("Admin"), []byte("Moderator"), []byte("User")}, + OnTypeNames: [][]byte{[]byte("Admin")}, }, { Name: []byte("age"), Value: &resolve.Integer{ Path: []string{"age"}, }, - OnTypeNames: [][]byte{[]byte("Admin"), []byte("Account"), []byte("Moderator"), []byte("User")}, + OnTypeNames: [][]byte{[]byte("Admin"), []byte("Account")}, + }, + { + Name: []byte("id"), + Value: &resolve.Scalar{ + Path: []string{"id"}, + }, + OnTypeNames: [][]byte{[]byte("Moderator"), []byte("Account")}, + }, + { + Name: []byte("title"), + Value: &resolve.String{ + Path: []string{"title"}, + }, + OnTypeNames: [][]byte{[]byte("Moderator")}, + }, + { + Name: []byte("age"), + Value: &resolve.Integer{ + Path: []string{"age"}, + }, + OnTypeNames: [][]byte{[]byte("Moderator"), []byte("Account")}, + }, + { + Name: []byte("id"), + Value: &resolve.Scalar{ + Path: []string{"id"}, + }, + OnTypeNames: [][]byte{[]byte("User"), []byte("Account")}, + }, + { + Name: []byte("title"), + Value: &resolve.String{ + Path: []string{"title"}, + }, + OnTypeNames: [][]byte{[]byte("User")}, + }, + { + Name: []byte("age"), + Value: &resolve.Integer{ + Path: []string{"age"}, + }, + OnTypeNames: [][]byte{[]byte("User"), []byte("Account")}, }, }, Fetch: &resolve.SerialFetch{ @@ -2250,7 +2626,71 @@ func TestGraphQLDataSourceFederationEntityInterfaces(t *testing.T) { Value: &resolve.Scalar{ Path: []string{"id"}, }, - OnTypeNames: [][]byte{[]byte("Admin"), []byte("Moderator"), []byte("User")}, + OnTypeNames: [][]byte{[]byte("Admin")}, + }, + { + Name: []byte("locations"), + Value: &resolve.Array{ + Path: []string{"locations"}, + Nullable: true, + Item: &resolve.Object{ + Fields: []*resolve.Field{ + { + Name: []byte("country"), + Value: &resolve.String{ + Path: []string{"country"}, + }, + }, + }, + }, + }, + OnTypeNames: [][]byte{[]byte("Admin"), []byte("Account")}, + }, + { + Name: []byte("age"), + Value: &resolve.Integer{ + Path: []string{"age"}, + }, + OnTypeNames: [][]byte{[]byte("Admin"), []byte("Account")}, + }, + { + Name: []byte("id"), + Value: &resolve.Scalar{ + Path: []string{"id"}, + }, + OnTypeNames: [][]byte{[]byte("Moderator")}, + }, + { + Name: []byte("locations"), + Value: &resolve.Array{ + Path: []string{"locations"}, + Nullable: true, + Item: &resolve.Object{ + Fields: []*resolve.Field{ + { + Name: []byte("country"), + Value: &resolve.String{ + Path: []string{"country"}, + }, + }, + }, + }, + }, + OnTypeNames: [][]byte{[]byte("Moderator"), []byte("Account")}, + }, + { + Name: []byte("age"), + Value: &resolve.Integer{ + Path: []string{"age"}, + }, + OnTypeNames: [][]byte{[]byte("Moderator"), []byte("Account")}, + }, + { + Name: []byte("id"), + Value: &resolve.Scalar{ + Path: []string{"id"}, + }, + OnTypeNames: [][]byte{[]byte("User")}, }, { Name: []byte("locations"), @@ -2268,14 +2708,14 @@ func TestGraphQLDataSourceFederationEntityInterfaces(t *testing.T) { }, }, }, - OnTypeNames: [][]byte{[]byte("Admin"), []byte("Account"), []byte("Moderator"), []byte("User")}, + OnTypeNames: [][]byte{[]byte("User"), []byte("Account")}, }, { Name: []byte("age"), Value: &resolve.Integer{ Path: []string{"age"}, }, - OnTypeNames: [][]byte{[]byte("Admin"), []byte("Account"), []byte("Moderator"), []byte("User")}, + OnTypeNames: [][]byte{[]byte("User"), []byte("Account")}, }, }, Fetch: &resolve.ParallelFetch{ @@ -2468,14 +2908,53 @@ func TestGraphQLDataSourceFederationEntityInterfaces(t *testing.T) { Value: &resolve.Scalar{ Path: []string{"id"}, }, - OnTypeNames: [][]byte{[]byte("Admin"), []byte("Moderator"), []byte("User")}, + OnTypeNames: [][]byte{[]byte("Admin")}, + }, + { + Name: []byte("title"), + Value: &resolve.String{ + Path: []string{"title"}, + }, + OnTypeNames: [][]byte{[]byte("Admin")}, + }, + { + Name: []byte("locations"), + Value: &resolve.Array{ + Path: []string{"locations"}, + Nullable: true, + Item: &resolve.Object{ + Fields: []*resolve.Field{ + { + Name: []byte("country"), + Value: &resolve.String{ + Path: []string{"country"}, + }, + }, + }, + }, + }, + OnTypeNames: [][]byte{[]byte("Admin"), []byte("Account")}, + }, + { + Name: []byte("age"), + Value: &resolve.Integer{ + Path: []string{"age"}, + }, + OnTypeNames: [][]byte{[]byte("Admin"), []byte("Account")}, + }, + { + Name: []byte("id"), + Value: &resolve.Scalar{ + Path: []string{"id"}, + }, + OnTypeNames: [][]byte{[]byte("Moderator")}, }, { Name: []byte("title"), Value: &resolve.String{ Path: []string{"title"}, }, - OnTypeNames: [][]byte{[]byte("Admin"), []byte("Moderator"), []byte("User")}, + OnTypeNames: [][]byte{[]byte("Moderator")}, }, { Name: []byte("locations"), @@ -2493,14 +2972,53 @@ func TestGraphQLDataSourceFederationEntityInterfaces(t *testing.T) { }, }, }, - OnTypeNames: [][]byte{[]byte("Admin"), []byte("Account"), []byte("Moderator"), []byte("User")}, + OnTypeNames: [][]byte{[]byte("Moderator"), []byte("Account")}, }, { Name: []byte("age"), Value: &resolve.Integer{ Path: []string{"age"}, }, - OnTypeNames: [][]byte{[]byte("Admin"), []byte("Account"), []byte("Moderator"), []byte("User")}, + OnTypeNames: [][]byte{[]byte("Moderator"), []byte("Account")}, + }, + { + Name: []byte("id"), + Value: &resolve.Scalar{ + Path: []string{"id"}, + }, + OnTypeNames: [][]byte{[]byte("User")}, + }, + { + Name: []byte("title"), + Value: &resolve.String{ + Path: []string{"title"}, + }, + OnTypeNames: [][]byte{[]byte("User")}, + }, + { + Name: []byte("locations"), + Value: &resolve.Array{ + Path: []string{"locations"}, + Nullable: true, + Item: &resolve.Object{ + Fields: []*resolve.Field{ + { + Name: []byte("country"), + Value: &resolve.String{ + Path: []string{"country"}, + }, + }, + }, + }, + }, + OnTypeNames: [][]byte{[]byte("User"), []byte("Account")}, + }, + { + Name: []byte("age"), + Value: &resolve.Integer{ + Path: []string{"age"}, + }, + OnTypeNames: [][]byte{[]byte("User"), []byte("Account")}, }, }, Fetch: &resolve.ParallelFetch{ @@ -2719,7 +3237,21 @@ func TestGraphQLDataSourceFederationEntityInterfaces(t *testing.T) { Value: &resolve.Integer{ Path: []string{"age"}, }, - OnTypeNames: [][]byte{[]byte("Admin"), []byte("Account"), []byte("Moderator"), []byte("User")}, + OnTypeNames: [][]byte{[]byte("Admin"), []byte("Account")}, + }, + { + Name: []byte("age"), + Value: &resolve.Integer{ + Path: []string{"age"}, + }, + OnTypeNames: [][]byte{[]byte("Moderator"), []byte("Account")}, + }, + { + Name: []byte("age"), + Value: &resolve.Integer{ + Path: []string{"age"}, + }, + OnTypeNames: [][]byte{[]byte("User"), []byte("Account")}, }, }, Fetch: &resolve.SingleFetch{ @@ -2834,7 +3366,7 @@ func TestGraphQLDataSourceFederationEntityInterfaces(t *testing.T) { Value: &resolve.Integer{ Path: []string{"age"}, }, - OnTypeNames: [][]byte{[]byte("Admin"), []byte("Account"), []byte("Moderator"), []byte("User")}, + OnTypeNames: [][]byte{[]byte("Admin"), []byte("Account")}, }, { Name: []byte("__typename"), @@ -2842,7 +3374,37 @@ func TestGraphQLDataSourceFederationEntityInterfaces(t *testing.T) { Path: []string{"__typename"}, IsTypeName: true, }, - OnTypeNames: [][]byte{[]byte("Admin"), []byte("Moderator"), []byte("User")}, + OnTypeNames: [][]byte{[]byte("Admin")}, + }, + { + Name: []byte("age"), + Value: &resolve.Integer{ + Path: []string{"age"}, + }, + OnTypeNames: [][]byte{[]byte("Moderator"), []byte("Account")}, + }, + { + Name: []byte("__typename"), + Value: &resolve.String{ + Path: []string{"__typename"}, + IsTypeName: true, + }, + OnTypeNames: [][]byte{[]byte("Moderator")}, + }, + { + Name: []byte("age"), + Value: &resolve.Integer{ + Path: []string{"age"}, + }, + OnTypeNames: [][]byte{[]byte("User"), []byte("Account")}, + }, + { + Name: []byte("__typename"), + Value: &resolve.String{ + Path: []string{"__typename"}, + IsTypeName: true, + }, + OnTypeNames: [][]byte{[]byte("User")}, }, }, Fetch: &resolve.ParallelFetch{ diff --git a/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_test.go b/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_test.go index c583a82b9..83f00260e 100644 --- a/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_test.go +++ b/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_test.go @@ -4583,7 +4583,14 @@ func TestGraphQLDataSourceFederation(t *testing.T) { Value: &resolve.String{ Path: []string{"title"}, }, - OnTypeNames: [][]byte{[]byte("Admin"), []byte("User")}, + OnTypeNames: [][]byte{[]byte("Admin")}, + }, + { + Name: []byte("title"), + Value: &resolve.String{ + Path: []string{"title"}, + }, + OnTypeNames: [][]byte{[]byte("User")}, }, }, Fetch: &resolve.SingleFetch{ @@ -5078,7 +5085,21 @@ func TestGraphQLDataSourceFederation(t *testing.T) { Value: &resolve.String{ Path: []string{"title"}, }, - OnTypeNames: [][]byte{[]byte("Admin"), []byte("Moderator"), []byte("User")}, + OnTypeNames: [][]byte{[]byte("Admin")}, + }, + { + Name: []byte("title"), + Value: &resolve.String{ + Path: []string{"title"}, + }, + OnTypeNames: [][]byte{[]byte("Moderator")}, + }, + { + Name: []byte("title"), + Value: &resolve.String{ + Path: []string{"title"}, + }, + OnTypeNames: [][]byte{[]byte("User")}, }, }, Fetch: &resolve.ParallelFetch{ diff --git a/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_test.go b/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_test.go index c9e58024f..9878fd76b 100644 --- a/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_test.go +++ b/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_test.go @@ -7152,8 +7152,6 @@ func TestGraphQLDataSource(t *testing.T) { // When user is an entity, the "pets" field can be both declared and resolved only in the pet subgraph // This separation of concerns is recommended: https://www.apollographql.com/docs/federation/v1/#separation-of-concerns t.Run("Federation with interface field query (defined on pet subgraph)", func(t *testing.T) { - t.Skip("problem with merging of resolve.Node") - planConfiguration := plan.Configuration{ DataSources: []plan.DataSource{ mustDataSourceConfiguration( @@ -7399,16 +7397,10 @@ func TestGraphQLDataSource(t *testing.T) { Value: &resolve.Integer{ Path: []string{"age"}, }, - OnTypeNames: [][]byte{[]byte("Cat")}, - }, - { - Name: []byte("hasOwner"), - Value: &resolve.Boolean{ - Path: []string{"hasOwner"}, - }, }, }, }, + OnTypeNames: [][]byte{[]byte("Cat")}, }, { Name: []byte("dogField"), @@ -7424,6 +7416,20 @@ func TestGraphQLDataSource(t *testing.T) { }, OnTypeNames: [][]byte{[]byte("Dog")}, }, + { + Name: []byte("details"), + Value: &resolve.Object{ + Path: []string{"details"}, + Fields: []*resolve.Field{ + { + Name: []byte("hasOwner"), + Value: &resolve.Boolean{ + Path: []string{"hasOwner"}, + }, + }, + }, + }, + }, }, }, }, @@ -7435,8 +7441,7 @@ func TestGraphQLDataSource(t *testing.T) { }, }, }, - planConfiguration, - WithSkipReason("age should have onTypes Cat"))) + planConfiguration)) t.Run("featuring consecutive inline fragments (shared selection in middle)", RunTest( federatedSchemaWithComplexNestedFragments, @@ -7631,7 +7636,7 @@ func TestGraphQLDataSource(t *testing.T) { Data: &resolve.Object{ Fetch: &resolve.SingleFetch{ FetchConfiguration: resolve.FetchConfiguration{ - Input: `{"method":"POST","url":"http://user.service","body":{"query":"{user {username}}"}}`, + Input: `{"method":"POST","url":"http://user.service","body":{"query":"{user {username __typename id}}"}}`, DataSource: &Source{}, PostProcessing: DefaultPostProcessingConfiguration, }, @@ -7672,9 +7677,12 @@ func TestGraphQLDataSource(t *testing.T) { }, ), DataSource: &Source{}, - PostProcessing: EntitiesPostProcessingConfiguration, + PostProcessing: SingleEntityPostProcessingConfiguration, SetTemplateOutputToNullOnVariableNull: true, + RequiresEntityFetch: true, }, + FetchID: 1, + DependsOnFetchIDs: []int{0}, DataSourceIdentifier: []byte("graphql_datasource.Source"), }, Path: []string{"user"}, @@ -7874,7 +7882,7 @@ func TestGraphQLDataSource(t *testing.T) { Value: &resolve.String{ Path: []string{"name"}, }, - OnTypeNames: [][]byte{[]byte("Cat"), []byte("Dog")}, + OnTypeNames: [][]byte{[]byte("Cat")}, }, { Name: []byte("catField"), @@ -7883,6 +7891,13 @@ func TestGraphQLDataSource(t *testing.T) { }, OnTypeNames: [][]byte{[]byte("Cat")}, }, + { + Name: []byte("name"), + Value: &resolve.String{ + Path: []string{"name"}, + }, + OnTypeNames: [][]byte{[]byte("Dog")}, + }, { Name: []byte("dogField"), Value: &resolve.String{ @@ -8144,7 +8159,7 @@ func TestGraphQLDataSource(t *testing.T) { Value: &resolve.String{ Path: []string{"name"}, }, - OnTypeNames: [][]byte{[]byte("Cat"), []byte("Dog")}, + OnTypeNames: [][]byte{[]byte("Cat")}, }, { Name: []byte("catField"), @@ -8153,6 +8168,14 @@ func TestGraphQLDataSource(t *testing.T) { }, OnTypeNames: [][]byte{[]byte("Cat")}, }, + { + Name: []byte("name"), + Value: &resolve.String{ + Path: []string{"name"}, + }, + OnTypeNames: [][]byte{[]byte("Dog")}, + }, + { Name: []byte("dogField"), Value: &resolve.String{ diff --git a/v2/pkg/engine/plan/planner_test.go b/v2/pkg/engine/plan/planner_test.go index cd23c115a..5dbf05c1d 100644 --- a/v2/pkg/engine/plan/planner_test.go +++ b/v2/pkg/engine/plan/planner_test.go @@ -131,7 +131,7 @@ func TestPlanner_Plan(t *testing.T) { DataSources: []DataSource{testDefinitionDSConfiguration}, })) - t.Run("Merging duplicate fields in response", func(t *testing.T) { + t.Run("Merging duplicate fields in response should not happen", func(t *testing.T) { t.Run("Interface response type with type fragments and shared field", test(testDefinition, ` query Hero { hero { @@ -162,6 +162,22 @@ func TestPlanner_Plan(t *testing.T) { Nullable: false, }, }, + { + Name: []byte("name"), + Value: &resolve.String{ + Path: []string{"name"}, + Nullable: false, + }, + OnTypeNames: [][]byte{[]byte("Droid")}, + }, + { + Name: []byte("name"), + Value: &resolve.String{ + Path: []string{"name"}, + Nullable: false, + }, + OnTypeNames: [][]byte{[]byte("Human")}, + }, }, }, }, @@ -207,7 +223,15 @@ func TestPlanner_Plan(t *testing.T) { Path: []string{"name"}, Nullable: false, }, - OnTypeNames: [][]byte{[]byte("Droid"), []byte("Human")}, + OnTypeNames: [][]byte{[]byte("Droid")}, + }, + { + Name: []byte("name"), + Value: &resolve.String{ + Path: []string{"name"}, + Nullable: false, + }, + OnTypeNames: [][]byte{[]byte("Human")}, }, }, }, diff --git a/v2/pkg/engine/plan/schemausageinfo_test.go b/v2/pkg/engine/plan/schemausageinfo_test.go index bc6552eeb..6258449b9 100644 --- a/v2/pkg/engine/plan/schemausageinfo_test.go +++ b/v2/pkg/engine/plan/schemausageinfo_test.go @@ -231,7 +231,7 @@ func TestGetSchemaUsageInfo(t *testing.T) { }, { Path: []string{"searchResults", "name"}, - EnclosingTypeNames: []string{"Human", "Droid"}, + EnclosingTypeNames: []string{"Human"}, FieldName: "name", FieldTypeName: "String", Source: TypeFieldSource{ @@ -247,6 +247,15 @@ func TestGetSchemaUsageInfo(t *testing.T) { IDs: []string{"https://swapi.dev/api"}, }, }, + { + Path: []string{"searchResults", "name"}, + EnclosingTypeNames: []string{"Droid"}, + FieldName: "name", + FieldTypeName: "String", + Source: TypeFieldSource{ + IDs: []string{"https://swapi.dev/api"}, + }, + }, { Path: []string{"searchResults", "length"}, EnclosingTypeNames: []string{"Starship"}, From 5fbf8f2546ffcaed1d13fb23763f731cb9b8733f Mon Sep 17 00:00:00 2001 From: spetrunin Date: Sat, 25 May 2024 15:36:30 +0300 Subject: [PATCH 09/15] ignore nullability on merging fetches --- v2/pkg/engine/postprocess/create_fetch_tree.go | 9 ++++----- v2/pkg/engine/postprocess/postprocess_test.go | 9 +++++++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/v2/pkg/engine/postprocess/create_fetch_tree.go b/v2/pkg/engine/postprocess/create_fetch_tree.go index 86c601402..a1958fee0 100644 --- a/v2/pkg/engine/postprocess/create_fetch_tree.go +++ b/v2/pkg/engine/postprocess/create_fetch_tree.go @@ -54,7 +54,7 @@ func (e *CreateFetchTree) EnterArray(array *resolve.Array) { func (e *CreateFetchTree) CloneArray(a *resolve.Array) *resolve.Array { return &resolve.Array{ - Nullable: a.Nullable, + Nullable: true, // fetches do not care about nullability Path: a.Path, } } @@ -136,10 +136,9 @@ func (e *CreateFetchTree) appendFetch(existing resolve.Fetch, additional resolve func (e *CreateFetchTree) CloneObject(o *resolve.Object) *resolve.Object { return &resolve.Object{ - Nullable: o.Nullable, - Path: o.Path, - Fetch: o.Fetch, - UnescapeResponseJson: o.UnescapeResponseJson, + Nullable: true, // fetches do not care about nullability + Path: o.Path, + Fetch: o.Fetch, } } diff --git a/v2/pkg/engine/postprocess/postprocess_test.go b/v2/pkg/engine/postprocess/postprocess_test.go index 3c57dfd29..1d5eb291a 100644 --- a/v2/pkg/engine/postprocess/postprocess_test.go +++ b/v2/pkg/engine/postprocess/postprocess_test.go @@ -56,6 +56,7 @@ func TestProcess_ExtractFetches(t *testing.T) { }, }, FetchTree: &resolve.Object{ + Nullable: true, Fetch: &resolve.MultiFetch{ Fetches: []*resolve.SingleFetch{ {FetchID: 1}, @@ -143,11 +144,13 @@ func TestProcess_ExtractFetches(t *testing.T) { }, }, FetchTree: &resolve.Object{ + Nullable: true, Fields: []*resolve.Field{ { Name: []byte("obj"), Value: &resolve.Object{ - Path: []string{"obj"}, + Nullable: true, + Path: []string{"obj"}, Fetch: &resolve.MultiFetch{ Fetches: []*resolve.SingleFetch{ {FetchID: 2}, @@ -254,6 +257,7 @@ func TestProcess_ExtractFetches(t *testing.T) { }, }, FetchTree: &resolve.Object{ + Nullable: true, Fields: []*resolve.Field{ { Name: []byte("objects"), @@ -261,7 +265,8 @@ func TestProcess_ExtractFetches(t *testing.T) { Nullable: true, Path: []string{"objects"}, Item: &resolve.Object{ - Path: []string{"obj"}, + Nullable: true, + Path: []string{"obj"}, Fetch: &resolve.MultiFetch{ Fetches: []*resolve.SingleFetch{ {FetchID: 2}, From 0ec419cff0ea92cb9b71ad64ea236491efd0e97e Mon Sep 17 00:00:00 2001 From: spetrunin Date: Sat, 25 May 2024 17:05:38 +0300 Subject: [PATCH 10/15] add fetch deduplication postprocessor --- .../postprocess/deduplicate_multi_fetch.go | 72 +++++++++++ .../deduplicate_multi_fetch_test.go | 119 ++++++++++++++++++ v2/pkg/engine/postprocess/postprocess.go | 3 +- .../postprocess/resolve_input_templates.go | 6 +- v2/pkg/engine/resolve/fetch.go | 52 ++++++++ 5 files changed, 250 insertions(+), 2 deletions(-) create mode 100644 v2/pkg/engine/postprocess/deduplicate_multi_fetch.go create mode 100644 v2/pkg/engine/postprocess/deduplicate_multi_fetch_test.go diff --git a/v2/pkg/engine/postprocess/deduplicate_multi_fetch.go b/v2/pkg/engine/postprocess/deduplicate_multi_fetch.go new file mode 100644 index 000000000..b4f191207 --- /dev/null +++ b/v2/pkg/engine/postprocess/deduplicate_multi_fetch.go @@ -0,0 +1,72 @@ +package postprocess + +import ( + "slices" + + "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/resolve" +) + +// DeduplicateMultiFetch is a postprocessor that transforms multi fetches into more concrete fetch types +type DeduplicateMultiFetch struct{} + +func (d *DeduplicateMultiFetch) Process(node resolve.Node) { + d.traverseNode(node) +} + +func (d *DeduplicateMultiFetch) ProcessSubscription(node resolve.Node, trigger *resolve.GraphQLSubscriptionTrigger) { + d.traverseNode(node) +} + +func (d *DeduplicateMultiFetch) traverseNode(node resolve.Node) { + switch n := node.(type) { + case *resolve.Object: + d.traverseFetch(n.Fetch) + for i := range n.Fields { + d.traverseNode(n.Fields[i].Value) + } + case *resolve.Array: + d.traverseNode(n.Item) + } +} + +func (d *DeduplicateMultiFetch) traverseFetch(fetch resolve.Fetch) { + if fetch == nil { + return + } + switch f := fetch.(type) { + case *resolve.SingleFetch: + return + case *resolve.SerialFetch: + for i := range f.Fetches { + d.traverseFetch(f.Fetches[i]) + } + case *resolve.ParallelFetch: + d.deduplicateParallelFetch(f) + for i := range f.Fetches { + d.traverseFetch(f.Fetches[i]) + } + } +} + +// deduplicateParallelFetch removes duplicated single fetches from a parallel fetch +func (d *DeduplicateMultiFetch) deduplicateParallelFetch(fetch *resolve.ParallelFetch) { + for i := 0; i < len(fetch.Fetches); i++ { + singleFetch, ok := fetch.Fetches[i].(*resolve.SingleFetch) + if !ok { + continue + } + + fetch.Fetches = slices.DeleteFunc(fetch.Fetches, func(other resolve.Fetch) bool { + if other == singleFetch { + return false + } + + otherSingleFetch, ok := other.(*resolve.SingleFetch) + if !ok { + return false + } + + return singleFetch.FetchConfiguration.Equals(&otherSingleFetch.FetchConfiguration) + }) + } +} diff --git a/v2/pkg/engine/postprocess/deduplicate_multi_fetch_test.go b/v2/pkg/engine/postprocess/deduplicate_multi_fetch_test.go new file mode 100644 index 000000000..eed2ac06f --- /dev/null +++ b/v2/pkg/engine/postprocess/deduplicate_multi_fetch_test.go @@ -0,0 +1,119 @@ +package postprocess + +import ( + "encoding/json" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" + + "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/plan" + "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/resolve" +) + +func TestDeduplicateMultiFetch_Process(t *testing.T) { + type TestCase struct { + name string + pre *plan.SynchronousResponsePlan + expected *plan.SynchronousResponsePlan + } + + cases := []TestCase{ + { + name: "parallel fetch without duplicates", + pre: &plan.SynchronousResponsePlan{ + Response: &resolve.GraphQLResponse{ + Data: &resolve.Object{ + Fetch: &resolve.ParallelFetch{ + Fetches: []resolve.Fetch{ + &resolve.SingleFetch{FetchID: 1, FetchConfiguration: resolve.FetchConfiguration{Input: "a"}}, + &resolve.SingleFetch{FetchID: 2, FetchConfiguration: resolve.FetchConfiguration{Input: "b"}}, + &resolve.SingleFetch{FetchID: 3, FetchConfiguration: resolve.FetchConfiguration{Input: "c"}}, + }, + }, + }, + }, + }, + expected: &plan.SynchronousResponsePlan{ + Response: &resolve.GraphQLResponse{ + Data: &resolve.Object{ + Fetch: &resolve.ParallelFetch{ + Fetches: []resolve.Fetch{ + &resolve.SingleFetch{FetchID: 1, FetchConfiguration: resolve.FetchConfiguration{Input: "a"}}, + &resolve.SingleFetch{FetchID: 2, FetchConfiguration: resolve.FetchConfiguration{Input: "b"}}, + &resolve.SingleFetch{FetchID: 3, FetchConfiguration: resolve.FetchConfiguration{Input: "c"}}, + }, + }, + }, + }, + }, + }, + { + name: "multiple parallel fetches with duplicates", + pre: &plan.SynchronousResponsePlan{ + Response: &resolve.GraphQLResponse{ + Data: &resolve.Object{ + Fetch: &resolve.SerialFetch{ + Fetches: []resolve.Fetch{ + &resolve.ParallelFetch{ + Fetches: []resolve.Fetch{ + &resolve.SingleFetch{FetchID: 1, FetchConfiguration: resolve.FetchConfiguration{Input: "a"}}, + &resolve.SingleFetch{FetchID: 2, FetchConfiguration: resolve.FetchConfiguration{Input: "b"}}, + &resolve.SingleFetch{FetchID: 6, FetchConfiguration: resolve.FetchConfiguration{Input: "a"}}, + }, + }, + &resolve.ParallelFetch{ + Fetches: []resolve.Fetch{ + &resolve.SingleFetch{FetchID: 3, FetchConfiguration: resolve.FetchConfiguration{Input: "c"}}, + &resolve.SingleFetch{FetchID: 4, FetchConfiguration: resolve.FetchConfiguration{Input: "c"}}, + }, + }, + &resolve.SingleFetch{FetchID: 5, DependsOnFetchIDs: []int{3}}, + }, + }, + }, + }, + }, + expected: &plan.SynchronousResponsePlan{ + Response: &resolve.GraphQLResponse{ + Data: &resolve.Object{ + Fetch: &resolve.SerialFetch{ + Fetches: []resolve.Fetch{ + &resolve.ParallelFetch{ + Fetches: []resolve.Fetch{ + &resolve.SingleFetch{FetchID: 1, FetchConfiguration: resolve.FetchConfiguration{Input: "a"}}, + &resolve.SingleFetch{FetchID: 2, FetchConfiguration: resolve.FetchConfiguration{Input: "b"}}, + }, + }, + &resolve.ParallelFetch{ + Fetches: []resolve.Fetch{ + &resolve.SingleFetch{FetchID: 3, FetchConfiguration: resolve.FetchConfiguration{Input: "c"}}, + }, + }, + &resolve.SingleFetch{FetchID: 5, DependsOnFetchIDs: []int{3}}, + }, + }, + }, + }, + }, + }, + } + + processor := &DeduplicateMultiFetch{} + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + processor.Process(c.pre.Response.Data) + + if !assert.Equal(t, c.expected, c.pre) { + actualBytes, _ := json.MarshalIndent(c.pre, "", " ") + expectedBytes, _ := json.MarshalIndent(c.expected, "", " ") + + if string(expectedBytes) != string(actualBytes) { + assert.Equal(t, string(expectedBytes), string(actualBytes)) + t.Error(cmp.Diff(string(expectedBytes), string(actualBytes))) + } + } + }) + } +} diff --git a/v2/pkg/engine/postprocess/postprocess.go b/v2/pkg/engine/postprocess/postprocess.go index 1e7622c8c..ebd137501 100644 --- a/v2/pkg/engine/postprocess/postprocess.go +++ b/v2/pkg/engine/postprocess/postprocess.go @@ -25,8 +25,9 @@ func NewProcessor(postProcessors []PostProcessor, extractFetches bool) *Processo func DefaultProcessor() *Processor { return &Processor{ postProcessors: []PostProcessor{ - &ResolveInputTemplates{}, &CreateMultiFetchTypes{}, + &DeduplicateMultiFetch{}, // this processor must be called after CreateMultiFetchTypes, when we remove duplicates we may lack of dependency id, which required to create proper multi fetch types + &ResolveInputTemplates{}, &CreateConcreteSingleFetchTypes{}, }, enableExtractFetches: true, diff --git a/v2/pkg/engine/postprocess/resolve_input_templates.go b/v2/pkg/engine/postprocess/resolve_input_templates.go index 081037f1a..12680b029 100644 --- a/v2/pkg/engine/postprocess/resolve_input_templates.go +++ b/v2/pkg/engine/postprocess/resolve_input_templates.go @@ -38,7 +38,11 @@ func (d *ResolveInputTemplates) traverseFetch(fetch resolve.Fetch) { switch f := fetch.(type) { case *resolve.SingleFetch: d.traverseSingleFetch(f) - case *resolve.MultiFetch: + case *resolve.ParallelFetch: + for i := range f.Fetches { + d.traverseFetch(f.Fetches[i]) + } + case *resolve.SerialFetch: for i := range f.Fetches { d.traverseFetch(f.Fetches[i]) } diff --git a/v2/pkg/engine/resolve/fetch.go b/v2/pkg/engine/resolve/fetch.go index e4db82a7b..5c101120f 100644 --- a/v2/pkg/engine/resolve/fetch.go +++ b/v2/pkg/engine/resolve/fetch.go @@ -2,6 +2,7 @@ package resolve import ( "encoding/json" + "slices" "github.com/wundergraph/graphql-go-tools/v2/pkg/ast" ) @@ -62,6 +63,25 @@ type PostProcessingConfiguration struct { MergePath []string } +// Equals compares two PostProcessingConfiguration objects +func (ppc *PostProcessingConfiguration) Equals(other *PostProcessingConfiguration) bool { + if !slices.Equal(ppc.SelectResponseDataPath, other.SelectResponseDataPath) { + return false + } + + if !slices.Equal(ppc.SelectResponseErrorsPath, other.SelectResponseErrorsPath) { + return false + } + + // Response template is unused in the current codebase - so we can ignore it + + if !slices.Equal(ppc.MergePath, other.MergePath) { + return false + } + + return true +} + func (_ *SingleFetch) FetchKind() FetchKind { return FetchKindSingle } @@ -169,6 +189,38 @@ type FetchConfiguration struct { SetTemplateOutputToNullOnVariableNull bool } +func (fc *FetchConfiguration) Equals(other *FetchConfiguration) bool { + if fc.Input != other.Input { + return false + } + if !slices.EqualFunc(fc.Variables, other.Variables, func(a, b Variable) bool { + return a.Equals(b) + }) { + return false + } + + if fc.DataSource != other.DataSource { + return false + } + if fc.RequiresParallelListItemFetch != other.RequiresParallelListItemFetch { + return false + } + if fc.RequiresEntityFetch != other.RequiresEntityFetch { + return false + } + if fc.RequiresEntityBatchFetch != other.RequiresEntityBatchFetch { + return false + } + if !fc.PostProcessing.Equals(&other.PostProcessing) { + return false + } + if fc.SetTemplateOutputToNullOnVariableNull != other.SetTemplateOutputToNullOnVariableNull { + return false + } + + return true +} + type FetchInfo struct { DataSourceID string RootFields []GraphCoordinate From 3c4e43249057c691e944df2f8dc94d694c5b3822 Mon Sep 17 00:00:00 2001 From: spetrunin Date: Sun, 26 May 2024 08:58:35 +0300 Subject: [PATCH 11/15] add resolve.Node.Equal method fix resolvable variables equal check fix fetch configuration equal check --- v2/pkg/engine/plan/visitor.go | 7 +- v2/pkg/engine/resolve/fetch.go | 5 +- v2/pkg/engine/resolve/node.go | 1 + v2/pkg/engine/resolve/node_array.go | 24 +++++ v2/pkg/engine/resolve/node_custom.go | 21 +++++ v2/pkg/engine/resolve/node_object.go | 52 +++++++++- v2/pkg/engine/resolve/node_scalar.go | 136 +++++++++++++++++++++++++++ v2/pkg/engine/resolve/variables.go | 2 +- 8 files changed, 235 insertions(+), 13 deletions(-) diff --git a/v2/pkg/engine/plan/visitor.go b/v2/pkg/engine/plan/visitor.go index aa0e4b930..c57c7250b 100644 --- a/v2/pkg/engine/plan/visitor.go +++ b/v2/pkg/engine/plan/visitor.go @@ -617,10 +617,9 @@ func (v *Visitor) resolveFieldValue(fieldRef, typeRef int, nullable bool, path [ } case ast.NodeKindObjectTypeDefinition, ast.NodeKindInterfaceTypeDefinition, ast.NodeKindUnionTypeDefinition: object := &resolve.Object{ - Nullable: nullable, - Path: path, - Fields: []*resolve.Field{}, - UnescapeResponseJson: unescapeResponseJson, + Nullable: nullable, + Path: path, + Fields: []*resolve.Field{}, } v.objects = append(v.objects, object) v.Walker.DefferOnEnterField(func() { diff --git a/v2/pkg/engine/resolve/fetch.go b/v2/pkg/engine/resolve/fetch.go index 5c101120f..14211c322 100644 --- a/v2/pkg/engine/resolve/fetch.go +++ b/v2/pkg/engine/resolve/fetch.go @@ -199,9 +199,8 @@ func (fc *FetchConfiguration) Equals(other *FetchConfiguration) bool { return false } - if fc.DataSource != other.DataSource { - return false - } + // Note: we do not compare datasources, as they will always be a different instance + if fc.RequiresParallelListItemFetch != other.RequiresParallelListItemFetch { return false } diff --git a/v2/pkg/engine/resolve/node.go b/v2/pkg/engine/resolve/node.go index cbcec9000..06fe728ec 100644 --- a/v2/pkg/engine/resolve/node.go +++ b/v2/pkg/engine/resolve/node.go @@ -20,6 +20,7 @@ type Node interface { NodeKind() NodeKind NodePath() []string NodeNullable() bool + Equals(Node) bool } type NodeKind int diff --git a/v2/pkg/engine/resolve/node_array.go b/v2/pkg/engine/resolve/node_array.go index 2d75dd789..f23b761fa 100644 --- a/v2/pkg/engine/resolve/node_array.go +++ b/v2/pkg/engine/resolve/node_array.go @@ -1,5 +1,7 @@ package resolve +import "slices" + type Array struct { Path []string Nullable bool @@ -18,6 +20,23 @@ func (a *Array) NodeNullable() bool { return a.Nullable } +func (a *Array) Equals(n Node) bool { + other, ok := n.(*Array) + if !ok { + return false + } + + if a.Nullable != other.Nullable { + return false + } + + if !slices.Equal(a.Path, other.Path) { + return false + } + + return a.Item.Equals(other.Item) +} + type EmptyArray struct{} func (_ *EmptyArray) NodeKind() NodeKind { @@ -31,3 +50,8 @@ func (_ *EmptyArray) NodePath() []string { func (_ *EmptyArray) NodeNullable() bool { return false } + +func (_ *EmptyArray) Equals(n Node) bool { + _, ok := n.(*EmptyArray) + return ok +} diff --git a/v2/pkg/engine/resolve/node_custom.go b/v2/pkg/engine/resolve/node_custom.go index 7591e7cf8..aebebf0fd 100644 --- a/v2/pkg/engine/resolve/node_custom.go +++ b/v2/pkg/engine/resolve/node_custom.go @@ -1,5 +1,9 @@ package resolve +import ( + "slices" +) + type CustomResolve interface { Resolve(ctx *Context, value []byte) ([]byte, error) } @@ -21,3 +25,20 @@ func (c *CustomNode) NodePath() []string { func (c *CustomNode) NodeNullable() bool { return c.Nullable } + +func (c *CustomNode) Equals(n Node) bool { + other, ok := n.(*CustomNode) + if !ok { + return false + } + + if !slices.Equal(c.Path, other.Path) { + return false + } + + if c.CustomResolve != other.CustomResolve { + return false + } + + return true +} diff --git a/v2/pkg/engine/resolve/node_object.go b/v2/pkg/engine/resolve/node_object.go index c341c4a5f..137ce82a1 100644 --- a/v2/pkg/engine/resolve/node_object.go +++ b/v2/pkg/engine/resolve/node_object.go @@ -1,15 +1,15 @@ package resolve import ( + "bytes" "slices" ) type Object struct { - Nullable bool - Path []string - Fields []*Field - Fetch Fetch - UnescapeResponseJson bool `json:"unescape_response_json,omitempty"` + Nullable bool + Path []string + Fields []*Field + Fetch Fetch } func (_ *Object) NodeKind() NodeKind { @@ -24,6 +24,30 @@ func (o *Object) NodeNullable() bool { return o.Nullable } +func (o *Object) Equals(n Node) bool { + other, ok := n.(*Object) + if !ok { + return false + } + if o.Nullable != other.Nullable { + return false + } + + if !slices.Equal(o.Path, other.Path) { + return false + } + + if !slices.EqualFunc(o.Fields, other.Fields, func(a, b *Field) bool { + return a.Equals(b) + }) { + return false + } + + // We ignore fetches in comparison, because we compare shape of the response nodes + + return true +} + type EmptyObject struct{} func (_ *EmptyObject) NodeKind() NodeKind { @@ -38,6 +62,11 @@ func (_ *EmptyObject) NodeNullable() bool { return false } +func (_ *EmptyObject) Equals(n Node) bool { + _, ok := n.(*EmptyObject) + return ok +} + type Field struct { Name []byte Value Node @@ -52,6 +81,19 @@ type Field struct { Info *FieldInfo } +func (f *Field) Equals(n *Field) bool { + // NOTE: a lot of struct fields are not compared here + // because they are not relevant for the value comparison of response nodes + + if !bytes.Equal(f.Name, n.Name) { + return false + } + if !f.Value.Equals(n.Value) { + return false + } + return true +} + type FieldInfo struct { // Name is the name of the field. Name string diff --git a/v2/pkg/engine/resolve/node_scalar.go b/v2/pkg/engine/resolve/node_scalar.go index 9eab2e3df..440089e5d 100644 --- a/v2/pkg/engine/resolve/node_scalar.go +++ b/v2/pkg/engine/resolve/node_scalar.go @@ -1,5 +1,7 @@ package resolve +import "slices" + type Scalar struct { Path []string Nullable bool @@ -18,6 +20,23 @@ func (s *Scalar) NodeNullable() bool { return s.Nullable } +func (s *Scalar) Equals(n Node) bool { + other, ok := n.(*Scalar) + if !ok { + return false + } + + if s.Nullable != other.Nullable { + return false + } + + if !slices.Equal(s.Path, other.Path) { + return false + } + + return true +} + type String struct { Path []string Nullable bool @@ -26,6 +45,31 @@ type String struct { IsTypeName bool `json:"is_type_name,omitempty"` } +func (s *String) Equals(n Node) bool { + other, ok := n.(*String) + if !ok { + return false + } + + if s.Nullable != other.Nullable { + return false + } + + if s.UnescapeResponseJson != other.UnescapeResponseJson { + return false + } + + if s.IsTypeName != other.IsTypeName { + return false + } + + if !slices.Equal(s.Path, other.Path) { + return false + } + + return true +} + func (_ *String) NodeKind() NodeKind { return NodeKindString } @@ -55,6 +99,23 @@ func (s *StaticString) NodeNullable() bool { return false } +func (s *StaticString) Equals(n Node) bool { + other, ok := n.(*StaticString) + if !ok { + return false + } + + if s.Value != other.Value { + return false + } + + if !slices.Equal(s.Path, other.Path) { + return false + } + + return true +} + type Boolean struct { Path []string Nullable bool @@ -73,6 +134,23 @@ func (b *Boolean) NodeNullable() bool { return b.Nullable } +func (b *Boolean) Equals(n Node) bool { + other, ok := n.(*Boolean) + if !ok { + return false + } + + if b.Nullable != other.Nullable { + return false + } + + if !slices.Equal(b.Path, other.Path) { + return false + } + + return true +} + type Float struct { Path []string Nullable bool @@ -91,6 +169,23 @@ func (f *Float) NodeNullable() bool { return f.Nullable } +func (f *Float) Equals(n Node) bool { + other, ok := n.(*Float) + if !ok { + return false + } + + if f.Nullable != other.Nullable { + return false + } + + if !slices.Equal(f.Path, other.Path) { + return false + } + + return true +} + type Integer struct { Path []string Nullable bool @@ -109,6 +204,24 @@ func (i *Integer) NodeNullable() bool { return i.Nullable } +func (i *Integer) Equals(n Node) bool { + other, ok := n.(*Integer) + if !ok { + return false + } + + if i.Nullable != other.Nullable { + return false + } + + if !slices.Equal(i.Path, other.Path) { + return false + } + + return true + +} + type BigInt struct { Path []string Nullable bool @@ -127,6 +240,24 @@ func (b *BigInt) NodeNullable() bool { return b.Nullable } +func (b *BigInt) Equals(n Node) bool { + other, ok := n.(*BigInt) + if !ok { + return false + } + + if b.Nullable != other.Nullable { + return false + } + + if !slices.Equal(b.Path, other.Path) { + return false + } + + return true + +} + type Null struct { } @@ -142,6 +273,11 @@ func (_ *Null) NodeNullable() bool { return true } +func (_ *Null) Equals(n Node) bool { + _, ok := n.(*Null) + return ok +} + // FieldExport takes the value of the field during evaluation (rendering of the field) // and stores it in the variables using the Path as JSON pointer. type FieldExport struct { diff --git a/v2/pkg/engine/resolve/variables.go b/v2/pkg/engine/resolve/variables.go index 3056252ae..407593bfd 100644 --- a/v2/pkg/engine/resolve/variables.go +++ b/v2/pkg/engine/resolve/variables.go @@ -188,5 +188,5 @@ func (h *ResolvableObjectVariable) Equals(another Variable) bool { } anotherVariable := another.(*ResolvableObjectVariable) - return h.Renderer.Node == anotherVariable.Renderer.Node + return h.Renderer.Node.Equals(anotherVariable.Renderer.Node) } From 8a1fe98955e7b1e2d0a85060460aba44af6da8b9 Mon Sep 17 00:00:00 2001 From: spetrunin Date: Mon, 27 May 2024 10:36:04 +0300 Subject: [PATCH 12/15] chore: assert full response in the art test --- .../engine/federation_integration_test.go | 25 +- .../complex_nesting_query_with_art.golden | 722 ++++++++++++++++++ .../federationtesting/gateway/http/http.go | 2 +- 3 files changed, 744 insertions(+), 5 deletions(-) create mode 100644 execution/engine/testdata/complex_nesting_query_with_art.golden diff --git a/execution/engine/federation_integration_test.go b/execution/engine/federation_integration_test.go index 0b1c45298..6c68e5c3b 100644 --- a/execution/engine/federation_integration_test.go +++ b/execution/engine/federation_integration_test.go @@ -9,12 +9,15 @@ import ( "net/http" "net/http/httptest" "path" + "regexp" "strings" "testing" "time" "github.com/jensneuse/abstractlogger" + "github.com/sebdah/goldie/v2" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/wundergraph/graphql-go-tools/execution/federationtesting" "github.com/wundergraph/graphql-go-tools/execution/federationtesting/gateway" @@ -46,7 +49,6 @@ func testQueryPath(name string) string { } func TestFederationIntegrationTestWithArt(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -55,12 +57,27 @@ func TestFederationIntegrationTestWithArt(t *testing.T) { gqlClient := NewGraphqlClient(http.DefaultClient) + normalizeResponse := func(resp string) string { + rex, err := regexp.Compile(`http://127.0.0.1:\d+`) + require.NoError(t, err) + resp = rex.ReplaceAllString(resp, "http://localhost/graphql") + // all nodes have UUIDs, so we need to replace them with a static UUID + rex2, err := regexp.Compile(`"id":"[a-f0-9\-]{36}"`) + require.NoError(t, err) + resp = rex2.ReplaceAllString(resp, `"id":"00000000-0000-0000-0000-000000000000"`) + return resp + } + t.Run("single upstream query operation with ART", func(t *testing.T) { - resp := gqlClient.Query(ctx, setup.GatewayServer.URL, testQueryPath("queries/single_upstream.query"), nil, t) - respString := string(resp) + resp := gqlClient.Query(ctx, setup.GatewayServer.URL, testQueryPath("queries/complex_nesting.graphql"), nil, t) + respString := normalizeResponse(string(resp)) - assert.Contains(t, respString, `{"data":{"me":{"id":"1234","username":"Me"}}`) + assert.Contains(t, respString, `{"data":{"me":{"id":"1234","username":"Me"`) assert.Contains(t, respString, `"extensions":{"trace":{"info":{"trace_start_time"`) + + buf := &bytes.Buffer{} + _ = json.Indent(buf, []byte(respString), "", " ") + goldie.New(t).Assert(t, "complex_nesting_query_with_art", buf.Bytes()) }) } diff --git a/execution/engine/testdata/complex_nesting_query_with_art.golden b/execution/engine/testdata/complex_nesting_query_with_art.golden new file mode 100644 index 000000000..97d69a5dd --- /dev/null +++ b/execution/engine/testdata/complex_nesting_query_with_art.golden @@ -0,0 +1,722 @@ +{ + "data": { + "me": { + "id": "1234", + "username": "Me", + "history": [ + { + "wallet": { + "currency": "USD" + } + }, + { + "location": "Germany", + "product": { + "upc": "top-2", + "name": "Fedora" + } + }, + { + "wallet": { + "currency": "USD" + } + } + ], + "reviews": [ + { + "__typename": "Review", + "attachments": [ + { + "upc": "top-1", + "body": "How do I turn it on?" + } + ] + }, + { + "__typename": "Review", + "attachments": [ + { + "upc": "top-2", + "body": "The best hat I have ever bought in my life." + }, + { + "upc": "top-2", + "size": 13.37 + } + ] + } + ] + } + }, + "extensions": { + "trace": { + "info": { + "trace_start_time": "", + "trace_start_unix": 0, + "parse_stats": { + "duration_nanoseconds": 0, + "duration_pretty": "", + "duration_since_start_nanoseconds": 0, + "duration_since_start_pretty": "" + }, + "normalize_stats": { + "duration_nanoseconds": 0, + "duration_pretty": "", + "duration_since_start_nanoseconds": 0, + "duration_since_start_pretty": "" + }, + "validate_stats": { + "duration_nanoseconds": 0, + "duration_pretty": "", + "duration_since_start_nanoseconds": 0, + "duration_since_start_pretty": "" + }, + "planner_stats": { + "duration_nanoseconds": 5, + "duration_pretty": "5ns", + "duration_since_start_nanoseconds": 20, + "duration_since_start_pretty": "20ns" + } + }, + "fetch": { + "id": "00000000-0000-0000-0000-000000000000", + "type": "single", + "path": "query", + "datasource_load_trace": { + "raw_input_data": {}, + "input": { + "body": { + "query": "{me {id username history {__typename ... on Store {location} ... on Purchase {wallet {currency}} ... on Sale {location product {upc __typename}}} __typename}}" + }, + "header": {}, + "method": "POST", + "url": "http://localhost/graphql" + }, + "output": { + "data": { + "me": { + "id": "1234", + "username": "Me", + "history": [ + { + "__typename": "Purchase", + "wallet": { + "currency": "USD" + } + }, + { + "__typename": "Sale", + "location": "Germany", + "product": { + "upc": "top-2", + "__typename": "Product" + } + }, + { + "__typename": "Purchase", + "wallet": { + "currency": "USD" + } + } + ], + "__typename": "User" + } + }, + "extensions": { + "trace": { + "request": { + "method": "POST", + "url": "http://localhost/graphql", + "headers": { + "Accept": [ + "application/json" + ], + "Accept-Encoding": [ + "gzip", + "deflate" + ], + "Content-Type": [ + "application/json" + ] + } + }, + "response": { + "status_code": 200, + "status": "200 OK", + "headers": { + "Content-Length": [ + "277" + ], + "Content-Type": [ + "application/json" + ] + }, + "body_size": 277 + } + } + } + }, + "duration_since_start_nanoseconds": 1, + "duration_since_start_pretty": "1ns", + "duration_load_nanoseconds": 1, + "duration_load_pretty": "1ns", + "single_flight_used": false, + "single_flight_shared_response": false, + "load_skipped": false, + "load_stats": { + "get_conn": { + "duration_since_start_nanoseconds": 1, + "duration_since_start_pretty": "1ns", + "host_port": "" + }, + "got_conn": { + "duration_since_start_nanoseconds": 1, + "duration_since_start_pretty": "1ns", + "reused": false, + "was_idle": false, + "idle_time_nanoseconds": 0, + "idle_time_pretty": "" + }, + "got_first_response_byte": { + "duration_since_start_nanoseconds": 1, + "duration_since_start_pretty": "1ns" + }, + "dns_start": { + "duration_since_start_nanoseconds": 0, + "duration_since_start_pretty": "", + "host": "" + }, + "dns_done": { + "duration_since_start_nanoseconds": 0, + "duration_since_start_pretty": "" + }, + "connect_start": { + "duration_since_start_nanoseconds": 0, + "duration_since_start_pretty": "", + "network": "", + "addr": "" + }, + "connect_done": { + "duration_since_start_nanoseconds": 0, + "duration_since_start_pretty": "", + "network": "", + "addr": "" + }, + "tls_handshake_start": { + "duration_since_start_nanoseconds": 0, + "duration_since_start_pretty": "" + }, + "tls_handshake_done": { + "duration_since_start_nanoseconds": 0, + "duration_since_start_pretty": "" + }, + "wrote_headers": { + "duration_since_start_nanoseconds": 1, + "duration_since_start_pretty": "1ns" + }, + "wrote_request": { + "duration_since_start_nanoseconds": 1, + "duration_since_start_pretty": "1ns" + } + } + } + }, + "node_type": "object", + "nullable": true, + "fields": [ + { + "name": "me", + "value": { + "fetch": { + "id": "00000000-0000-0000-0000-000000000000", + "type": "entity", + "path": "query.me", + "datasource_load_trace": { + "raw_input_data": { + "id": "1234", + "username": "Me", + "history": [ + { + "__typename": "Purchase", + "wallet": { + "currency": "USD" + } + }, + { + "__typename": "Sale", + "location": "Germany", + "product": { + "upc": "top-2", + "__typename": "Product" + } + }, + { + "__typename": "Purchase", + "wallet": { + "currency": "USD" + } + } + ], + "__typename": "User" + }, + "input": { + "body": { + "query": "query($representations: [_Any!]!){_entities(representations: $representations){__typename ... on User {reviews {__typename attachments {__typename ... on Comment {upc __typename ... on Rating {body}} ... on Question {body} ... on Video {upc size}}}}}}", + "variables": { + "representations": [ + { + "__typename": "User", + "id": "1234" + } + ] + } + }, + "header": {}, + "method": "POST", + "url": "http://localhost/graphql" + }, + "output": { + "data": { + "_entities": [ + { + "__typename": "User", + "reviews": [ + { + "__typename": "Review", + "attachments": [ + { + "__typename": "Question", + "upc": "top-1", + "__typename": "Question", + "body": "How do I turn it on?" + } + ] + }, + { + "__typename": "Review", + "attachments": [ + { + "__typename": "Rating", + "upc": "top-2", + "__typename": "Rating", + "body": "The best hat I have ever bought in my life." + }, + { + "__typename": "Video", + "upc": "top-2", + "size": 13.37 + } + ] + } + ] + } + ] + }, + "extensions": { + "trace": { + "request": { + "method": "POST", + "url": "http://localhost/graphql", + "headers": { + "Accept": [ + "application/json" + ], + "Accept-Encoding": [ + "gzip", + "deflate" + ], + "Content-Type": [ + "application/json" + ] + } + }, + "response": { + "status_code": 200, + "status": "200 OK", + "headers": { + "Content-Length": [ + "395" + ], + "Content-Type": [ + "application/json" + ] + }, + "body_size": 395 + } + } + } + }, + "duration_since_start_nanoseconds": 1, + "duration_since_start_pretty": "1ns", + "duration_load_nanoseconds": 1, + "duration_load_pretty": "1ns", + "single_flight_used": false, + "single_flight_shared_response": false, + "load_skipped": false, + "load_stats": { + "get_conn": { + "duration_since_start_nanoseconds": 1, + "duration_since_start_pretty": "1ns", + "host_port": "" + }, + "got_conn": { + "duration_since_start_nanoseconds": 1, + "duration_since_start_pretty": "1ns", + "reused": false, + "was_idle": false, + "idle_time_nanoseconds": 0, + "idle_time_pretty": "" + }, + "got_first_response_byte": { + "duration_since_start_nanoseconds": 1, + "duration_since_start_pretty": "1ns" + }, + "dns_start": { + "duration_since_start_nanoseconds": 0, + "duration_since_start_pretty": "", + "host": "" + }, + "dns_done": { + "duration_since_start_nanoseconds": 0, + "duration_since_start_pretty": "" + }, + "connect_start": { + "duration_since_start_nanoseconds": 0, + "duration_since_start_pretty": "", + "network": "", + "addr": "" + }, + "connect_done": { + "duration_since_start_nanoseconds": 0, + "duration_since_start_pretty": "", + "network": "", + "addr": "" + }, + "tls_handshake_start": { + "duration_since_start_nanoseconds": 0, + "duration_since_start_pretty": "" + }, + "tls_handshake_done": { + "duration_since_start_nanoseconds": 0, + "duration_since_start_pretty": "" + }, + "wrote_headers": { + "duration_since_start_nanoseconds": 1, + "duration_since_start_pretty": "1ns" + }, + "wrote_request": { + "duration_since_start_nanoseconds": 1, + "duration_since_start_pretty": "1ns" + } + } + } + }, + "node_type": "object", + "path": [ + "me" + ], + "fields": [ + { + "name": "id", + "value": { + "node_type": "scalar", + "path": [ + "id" + ] + } + }, + { + "name": "username", + "value": { + "node_type": "string", + "path": [ + "username" + ] + } + }, + { + "name": "history", + "value": { + "node_type": "array", + "path": [ + "history" + ], + "items": [ + { + "node_type": "object", + "nullable": true, + "fields": [ + { + "name": "location", + "value": { + "node_type": "string", + "path": [ + "location" + ] + } + }, + { + "name": "wallet", + "value": { + "node_type": "object", + "path": [ + "wallet" + ], + "fields": [ + { + "name": "currency", + "value": { + "node_type": "string", + "path": [ + "currency" + ] + } + } + ] + } + }, + { + "name": "product", + "value": { + "fetch": { + "id": "00000000-0000-0000-0000-000000000000", + "type": "batchEntity", + "path": "query.me.history.@.product", + "datasource_load_trace": { + "raw_input_data": { + "upc": "top-2", + "__typename": "Product" + }, + "input": { + "body": { + "query": "query($representations: [_Any!]!){_entities(representations: $representations){__typename ... on Product {name}}}", + "variables": { + "representations": [ + { + "__typename": "Product", + "upc": "top-2" + } + ] + } + }, + "header": {}, + "method": "POST", + "url": "http://localhost/graphql" + }, + "output": { + "data": { + "_entities": [ + { + "__typename": "Product", + "name": "Fedora" + } + ] + }, + "extensions": { + "trace": { + "request": { + "method": "POST", + "url": "http://localhost/graphql", + "headers": { + "Accept": [ + "application/json" + ], + "Accept-Encoding": [ + "gzip", + "deflate" + ], + "Content-Type": [ + "application/json" + ] + } + }, + "response": { + "status_code": 200, + "status": "200 OK", + "headers": { + "Content-Length": [ + "65" + ], + "Content-Type": [ + "application/json" + ] + }, + "body_size": 65 + } + } + } + }, + "duration_since_start_nanoseconds": 1, + "duration_since_start_pretty": "1ns", + "duration_load_nanoseconds": 1, + "duration_load_pretty": "1ns", + "single_flight_used": false, + "single_flight_shared_response": false, + "load_skipped": false, + "load_stats": { + "get_conn": { + "duration_since_start_nanoseconds": 1, + "duration_since_start_pretty": "1ns", + "host_port": "" + }, + "got_conn": { + "duration_since_start_nanoseconds": 1, + "duration_since_start_pretty": "1ns", + "reused": false, + "was_idle": false, + "idle_time_nanoseconds": 0, + "idle_time_pretty": "" + }, + "got_first_response_byte": { + "duration_since_start_nanoseconds": 1, + "duration_since_start_pretty": "1ns" + }, + "dns_start": { + "duration_since_start_nanoseconds": 0, + "duration_since_start_pretty": "", + "host": "" + }, + "dns_done": { + "duration_since_start_nanoseconds": 0, + "duration_since_start_pretty": "" + }, + "connect_start": { + "duration_since_start_nanoseconds": 0, + "duration_since_start_pretty": "", + "network": "", + "addr": "" + }, + "connect_done": { + "duration_since_start_nanoseconds": 0, + "duration_since_start_pretty": "", + "network": "", + "addr": "" + }, + "tls_handshake_start": { + "duration_since_start_nanoseconds": 0, + "duration_since_start_pretty": "" + }, + "tls_handshake_done": { + "duration_since_start_nanoseconds": 0, + "duration_since_start_pretty": "" + }, + "wrote_headers": { + "duration_since_start_nanoseconds": 1, + "duration_since_start_pretty": "1ns" + }, + "wrote_request": { + "duration_since_start_nanoseconds": 1, + "duration_since_start_pretty": "1ns" + } + } + } + }, + "node_type": "object", + "path": [ + "product" + ], + "fields": [ + { + "name": "upc", + "value": { + "node_type": "string", + "path": [ + "upc" + ] + } + }, + { + "name": "name", + "value": { + "node_type": "string", + "path": [ + "name" + ] + } + } + ] + } + } + ] + } + ] + } + }, + { + "name": "reviews", + "value": { + "node_type": "array", + "path": [ + "reviews" + ], + "items": [ + { + "node_type": "object", + "nullable": true, + "fields": [ + { + "name": "__typename", + "value": { + "node_type": "string", + "path": [ + "__typename" + ], + "is_type_name": true + } + }, + { + "name": "attachments", + "value": { + "node_type": "array", + "path": [ + "attachments" + ], + "items": [ + { + "node_type": "object", + "nullable": true, + "fields": [ + { + "name": "upc", + "value": { + "node_type": "string", + "path": [ + "upc" + ] + } + }, + { + "name": "body", + "value": { + "node_type": "string", + "path": [ + "body" + ] + } + }, + { + "name": "size", + "value": { + "node_type": "float", + "path": [ + "size" + ] + } + } + ] + } + ] + } + } + ] + } + ] + } + } + ] + } + } + ] + } + } +} \ No newline at end of file diff --git a/execution/federationtesting/gateway/http/http.go b/execution/federationtesting/gateway/http/http.go index a4ca04ad8..6266f5ffa 100644 --- a/execution/federationtesting/gateway/http/http.go +++ b/execution/federationtesting/gateway/http/http.go @@ -37,7 +37,7 @@ func (g *GraphQLHTTPRequestHandler) handleHTTP(w http.ResponseWriter, r *http.Re ExcludeInput: false, ExcludeOutput: false, ExcludeLoadStats: false, - EnablePredictableDebugTimings: false, + EnablePredictableDebugTimings: true, IncludeTraceOutputInResponseExtensions: true, } From 5449bcb629b01055c872046f7003b844085e6db8 Mon Sep 17 00:00:00 2001 From: spetrunin Date: Mon, 27 May 2024 15:53:17 +0300 Subject: [PATCH 13/15] chore: fix art --- .../engine/federation_integration_test.go | 6 +- ...en => complex_nesting_query_with_art.json} | 140 ------------------ .../federationtesting/gateway/http/http.go | 1 + v2/pkg/engine/resolve/resolvable.go | 18 +-- v2/pkg/engine/resolve/resolvable_test.go | 18 +-- v2/pkg/engine/resolve/resolve.go | 6 +- v2/pkg/engine/resolve/trace.go | 3 +- 7 files changed, 24 insertions(+), 168 deletions(-) rename execution/engine/testdata/{complex_nesting_query_with_art.golden => complex_nesting_query_with_art.json} (81%) diff --git a/execution/engine/federation_integration_test.go b/execution/engine/federation_integration_test.go index 6c68e5c3b..adb285558 100644 --- a/execution/engine/federation_integration_test.go +++ b/execution/engine/federation_integration_test.go @@ -61,10 +61,6 @@ func TestFederationIntegrationTestWithArt(t *testing.T) { rex, err := regexp.Compile(`http://127.0.0.1:\d+`) require.NoError(t, err) resp = rex.ReplaceAllString(resp, "http://localhost/graphql") - // all nodes have UUIDs, so we need to replace them with a static UUID - rex2, err := regexp.Compile(`"id":"[a-f0-9\-]{36}"`) - require.NoError(t, err) - resp = rex2.ReplaceAllString(resp, `"id":"00000000-0000-0000-0000-000000000000"`) return resp } @@ -77,7 +73,7 @@ func TestFederationIntegrationTestWithArt(t *testing.T) { buf := &bytes.Buffer{} _ = json.Indent(buf, []byte(respString), "", " ") - goldie.New(t).Assert(t, "complex_nesting_query_with_art", buf.Bytes()) + goldie.New(t, goldie.WithNameSuffix(".json")).Assert(t, "complex_nesting_query_with_art", buf.Bytes()) }) } diff --git a/execution/engine/testdata/complex_nesting_query_with_art.golden b/execution/engine/testdata/complex_nesting_query_with_art.json similarity index 81% rename from execution/engine/testdata/complex_nesting_query_with_art.golden rename to execution/engine/testdata/complex_nesting_query_with_art.json index 97d69a5dd..8faa4319c 100644 --- a/execution/engine/testdata/complex_nesting_query_with_art.golden +++ b/execution/engine/testdata/complex_nesting_query_with_art.json @@ -222,7 +222,6 @@ } }, "node_type": "object", - "nullable": true, "fields": [ { "name": "me", @@ -416,24 +415,6 @@ "me" ], "fields": [ - { - "name": "id", - "value": { - "node_type": "scalar", - "path": [ - "id" - ] - } - }, - { - "name": "username", - "value": { - "node_type": "string", - "path": [ - "username" - ] - } - }, { "name": "history", "value": { @@ -444,37 +425,7 @@ "items": [ { "node_type": "object", - "nullable": true, "fields": [ - { - "name": "location", - "value": { - "node_type": "string", - "path": [ - "location" - ] - } - }, - { - "name": "wallet", - "value": { - "node_type": "object", - "path": [ - "wallet" - ], - "fields": [ - { - "name": "currency", - "value": { - "node_type": "string", - "path": [ - "currency" - ] - } - } - ] - } - }, { "name": "product", "value": { @@ -614,97 +565,6 @@ "node_type": "object", "path": [ "product" - ], - "fields": [ - { - "name": "upc", - "value": { - "node_type": "string", - "path": [ - "upc" - ] - } - }, - { - "name": "name", - "value": { - "node_type": "string", - "path": [ - "name" - ] - } - } - ] - } - } - ] - } - ] - } - }, - { - "name": "reviews", - "value": { - "node_type": "array", - "path": [ - "reviews" - ], - "items": [ - { - "node_type": "object", - "nullable": true, - "fields": [ - { - "name": "__typename", - "value": { - "node_type": "string", - "path": [ - "__typename" - ], - "is_type_name": true - } - }, - { - "name": "attachments", - "value": { - "node_type": "array", - "path": [ - "attachments" - ], - "items": [ - { - "node_type": "object", - "nullable": true, - "fields": [ - { - "name": "upc", - "value": { - "node_type": "string", - "path": [ - "upc" - ] - } - }, - { - "name": "body", - "value": { - "node_type": "string", - "path": [ - "body" - ] - } - }, - { - "name": "size", - "value": { - "node_type": "float", - "path": [ - "size" - ] - } - } - ] - } ] } } diff --git a/execution/federationtesting/gateway/http/http.go b/execution/federationtesting/gateway/http/http.go index 6266f5ffa..5a255e01c 100644 --- a/execution/federationtesting/gateway/http/http.go +++ b/execution/federationtesting/gateway/http/http.go @@ -38,6 +38,7 @@ func (g *GraphQLHTTPRequestHandler) handleHTTP(w http.ResponseWriter, r *http.Re ExcludeOutput: false, ExcludeLoadStats: false, EnablePredictableDebugTimings: true, + Debug: true, IncludeTraceOutputInResponseExtensions: true, } diff --git a/v2/pkg/engine/resolve/resolvable.go b/v2/pkg/engine/resolve/resolvable.go index 238d99f62..a00ce7a79 100644 --- a/v2/pkg/engine/resolve/resolvable.go +++ b/v2/pkg/engine/resolve/resolvable.go @@ -121,7 +121,7 @@ func (r *Resolvable) InitSubscription(ctx *Context, initialData []byte, postProc return } -func (r *Resolvable) Resolve(ctx context.Context, root *Object, out io.Writer) error { +func (r *Resolvable) Resolve(ctx context.Context, rootData *Object, fetchTree *Object, out io.Writer) error { r.out = out r.print = false r.printErr = nil @@ -131,7 +131,7 @@ func (r *Resolvable) Resolve(ctx context.Context, root *Object, out io.Writer) e * For example, if a fetch fails, only propagate that the fetch has failed; do not propagate nested non-null errors. */ - _, err := r.walkObject(root, r.dataRoot) + _, err := r.walkObject(rootData, r.dataRoot) if r.authorizationError != nil { return r.authorizationError } @@ -147,11 +147,11 @@ func (r *Resolvable) Resolve(ctx context.Context, root *Object, out io.Writer) e r.printBytes(colon) r.printBytes(null) } else { - r.printData(root) + r.printData(rootData) } if r.hasExtensions() { r.printBytes(comma) - r.printErr = r.printExtensions(ctx, root) + r.printErr = r.printExtensions(ctx, fetchTree) } r.printBytes(rBrace) @@ -184,7 +184,7 @@ func (r *Resolvable) printData(root *Object) { r.wroteData = true } -func (r *Resolvable) printExtensions(ctx context.Context, root *Object) error { +func (r *Resolvable) printExtensions(ctx context.Context, fetchTree *Object) error { r.printBytes(quote) r.printBytes(literalExtensions) r.printBytes(quote) @@ -218,7 +218,7 @@ func (r *Resolvable) printExtensions(ctx context.Context, root *Object) error { if writeComma { r.printBytes(comma) } - err := r.printTraceExtension(ctx, root) + err := r.printTraceExtension(ctx, fetchTree) if err != nil { return err } @@ -244,12 +244,12 @@ func (r *Resolvable) printRateLimitingExtension() error { return r.ctx.rateLimiter.RenderResponseExtension(r.ctx, r.out) } -func (r *Resolvable) printTraceExtension(ctx context.Context, root *Object) error { +func (r *Resolvable) printTraceExtension(ctx context.Context, fetchTree *Object) error { var trace *TraceNode if r.ctx.TracingOptions.Debug { - trace = GetTrace(ctx, root, GetTraceDebug()) + trace = GetTrace(ctx, fetchTree, GetTraceDebug()) } else { - trace = GetTrace(ctx, root) + trace = GetTrace(ctx, fetchTree) } traceData, err := json.Marshal(trace) if err != nil { diff --git a/v2/pkg/engine/resolve/resolvable_test.go b/v2/pkg/engine/resolve/resolvable_test.go index 294c3242d..333397055 100644 --- a/v2/pkg/engine/resolve/resolvable_test.go +++ b/v2/pkg/engine/resolve/resolvable_test.go @@ -77,7 +77,7 @@ func TestResolvable_Resolve(t *testing.T) { } out := &bytes.Buffer{} - err = res.Resolve(context.Background(), object, out) + err = res.Resolve(context.Background(), object, nil, out) assert.NoError(t, err) assert.Equal(t, `{"data":{"topProducts":[{"name":"Table","stock":8,"reviews":[{"body":"Love Table!","author":{"name":"user-1"}},{"body":"Prefer other Table.","author":{"name":"user-2"}}]},{"name":"Couch","stock":2,"reviews":[{"body":"Couch Too expensive.","author":{"name":"user-1"}}]},{"name":"Chair","stock":5,"reviews":[{"body":"Chair Could be better.","author":{"name":"user-2"}}]}]}}`, out.String()) } @@ -150,7 +150,7 @@ func TestResolvable_ResolveWithTypeMismatch(t *testing.T) { } out := &bytes.Buffer{} - err = res.Resolve(context.Background(), object, out) + err = res.Resolve(context.Background(), object, nil, out) assert.NoError(t, err) assert.Equal(t, `{"errors":[{"message":"String cannot represent non-string value: \"true\"","path":["topProducts",0,"reviews",0,"author","name"]}],"data":{"topProducts":[{"name":"Table","stock":8,"reviews":[{"body":"Love Table!","author":null},{"body":"Prefer other Table.","author":{"name":"user-2"}}]},{"name":"Couch","stock":2,"reviews":[{"body":"Couch Too expensive.","author":{"name":"user-1"}}]},{"name":"Chair","stock":5,"reviews":[{"body":"Chair Could be better.","author":{"name":"user-2"}}]}]}}`, out.String()) } @@ -223,7 +223,7 @@ func TestResolvable_ResolveWithErrorBubbleUp(t *testing.T) { } out := &bytes.Buffer{} - err = res.Resolve(context.Background(), object, out) + err = res.Resolve(context.Background(), object, nil, out) assert.NoError(t, err) assert.Equal(t, `{"errors":[{"message":"Cannot return null for non-nullable field 'Query.topProducts.reviews.author.name'.","path":["topProducts",0,"reviews",0,"author","name"]}],"data":{"topProducts":[{"name":"Table","stock":8,"reviews":[{"body":"Love Table!","author":null},{"body":"Prefer other Table.","author":{"name":"user-2"}}]},{"name":"Couch","stock":2,"reviews":[{"body":"Couch Too expensive.","author":{"name":"user-1"}}]},{"name":"Chair","stock":5,"reviews":[{"body":"Chair Could be better.","author":{"name":"user-2"}}]}]}}`, out.String()) } @@ -295,7 +295,7 @@ func TestResolvable_ResolveWithErrorBubbleUpUntilData(t *testing.T) { } out := &bytes.Buffer{} - err = res.Resolve(context.Background(), object, out) + err = res.Resolve(context.Background(), object, nil, out) assert.NoError(t, err) assert.Equal(t, `{"errors":[{"message":"Cannot return null for non-nullable field 'Query.topProducts.reviews.author.name'.","path":["topProducts",0,"reviews",1,"author","name"]}],"data":null}`, out.String()) } @@ -373,7 +373,7 @@ func BenchmarkResolvable_Resolve(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { out.Reset() - err = res.Resolve(context.Background(), object, out) + err = res.Resolve(context.Background(), object, nil, out) if err != nil { b.Fatal(err) } @@ -451,7 +451,7 @@ func BenchmarkResolvable_ResolveWithErrorBubbleUp(b *testing.B) { } out := &bytes.Buffer{} - err = res.Resolve(context.Background(), object, out) + err = res.Resolve(context.Background(), object, nil, out) assert.NoError(b, err) expected := []byte(`{"errors":[{"message":"Cannot return null for non-nullable field Query.topProducts.reviews.author.name.","path":["topProducts",0,"reviews",0,"author","name"]}],"data":{"topProducts":[{"name":"Table","stock":8,"reviews":[{"body":"Love Table!","author":null},{"body":"Prefer other Table.","author":{"name":"user-2"}}]},{"name":"Couch","stock":2,"reviews":[{"body":"Couch Too expensive.","author":{"name":"user-1"}}]},{"name":"Chair","stock":5,"reviews":[{"body":"Chair Could be better.","author":{"name":"user-2"}}]}]}}`) b.SetBytes(int64(len(expected))) @@ -459,7 +459,7 @@ func BenchmarkResolvable_ResolveWithErrorBubbleUp(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { out.Reset() - err = res.Resolve(context.Background(), object, out) + err = res.Resolve(context.Background(), object, nil, out) if err != nil { b.Fatal(err) } @@ -488,7 +488,7 @@ func TestResolvable_WithTracingNotStarted(t *testing.T) { }, } out := &bytes.Buffer{} - err = res.Resolve(ctx.ctx, object, out) + err = res.Resolve(ctx.ctx, object, nil, out) assert.NoError(t, err) assert.JSONEq(t, `{ @@ -571,7 +571,7 @@ func TestResolvable_WithTracing(t *testing.T) { SetPlannerStats(ctx.ctx, PhaseStats{}) out := &bytes.Buffer{} - err = res.Resolve(ctx.ctx, object, out) + err = res.Resolve(ctx.ctx, object, nil, out) assert.NoError(t, err) assert.Equal(t, `{"data":{"topProducts":[{"name":"Table","stock":8,"reviews":[{"body":"Love Table!","author":{"name":"user-1"}},{"body":"Prefer other Table.","author":{"name":"user-2"}}]},{"name":"Couch","stock":2,"reviews":[{"body":"Couch Too expensive.","author":{"name":"user-1"}}]},{"name":"Chair","stock":5,"reviews":[{"body":"Chair Could be better.","author":{"name":"user-2"}}]}]},"extensions":{"trace":{"info":{"trace_start_time":"","trace_start_unix":0,"parse_stats":{"duration_nanoseconds":5,"duration_pretty":"5ns","duration_since_start_nanoseconds":5,"duration_since_start_pretty":"5ns"},"normalize_stats":{"duration_nanoseconds":5,"duration_pretty":"5ns","duration_since_start_nanoseconds":10,"duration_since_start_pretty":"10ns"},"validate_stats":{"duration_nanoseconds":5,"duration_pretty":"5ns","duration_since_start_nanoseconds":15,"duration_since_start_pretty":"15ns"},"planner_stats":{"duration_nanoseconds":5,"duration_pretty":"5ns","duration_since_start_nanoseconds":20,"duration_since_start_pretty":"20ns"}},"node_type":"object","nullable":true,"fields":[{"name":"topProducts","value":{"node_type":"array","path":["topProducts"],"items":[{"node_type":"object","nullable":true,"fields":[{"name":"name","value":{"node_type":"string","path":["name"]}},{"name":"stock","value":{"node_type":"integer","path":["stock"]}},{"name":"reviews","value":{"node_type":"array","path":["reviews"],"items":[{"node_type":"object","nullable":true,"fields":[{"name":"body","value":{"node_type":"string","path":["body"]}},{"name":"author","value":{"node_type":"object","path":["author"],"fields":[{"name":"name","value":{"node_type":"string","path":["name"]}}]}}]}]}}]}]}}]}}}`, out.String()) diff --git a/v2/pkg/engine/resolve/resolve.go b/v2/pkg/engine/resolve/resolve.go index 586cb310c..4f7ee8be8 100644 --- a/v2/pkg/engine/resolve/resolve.go +++ b/v2/pkg/engine/resolve/resolve.go @@ -104,7 +104,7 @@ type ResolverOptions struct { // New returns a new Resolver, ctx.Done() is used to cancel all active subscriptions & streams func New(ctx context.Context, options ResolverOptions) *Resolver { - //options.Debug = true + // options.Debug = true resolver := &Resolver{ ctx: ctx, options: options, @@ -191,7 +191,7 @@ func (r *Resolver) ResolveGraphQLResponse(ctx *Context, response *GraphQLRespons return err } - return t.resolvable.Resolve(ctx.ctx, response.Data, writer) + return t.resolvable.Resolve(ctx.ctx, response.Data, response.FetchTree, writer) } type trigger struct { @@ -271,7 +271,7 @@ func (r *Resolver) executeSubscriptionUpdate(ctx *Context, sub *sub, sharedInput } return // subscription was already closed by the client } - if err := t.resolvable.Resolve(ctx.ctx, sub.resolve.Response.Data, sub.writer); err != nil { + if err := t.resolvable.Resolve(ctx.ctx, sub.resolve.Response.Data, sub.resolve.Response.FetchTree, sub.writer); err != nil { buf := pool.BytesBuffer.Get() defer pool.BytesBuffer.Put(buf) r.asyncErrorWriter.WriteError(ctx, err, sub.resolve.Response, sub.writer, buf) diff --git a/v2/pkg/engine/resolve/trace.go b/v2/pkg/engine/resolve/trace.go index f20e3c42c..9d7c3a354 100644 --- a/v2/pkg/engine/resolve/trace.go +++ b/v2/pkg/engine/resolve/trace.go @@ -117,7 +117,6 @@ type TraceNode struct { Info *TraceInfo `json:"info,omitempty"` Fetch *TraceFetch `json:"fetch,omitempty"` NodeType TraceNodeType `json:"node_type,omitempty"` - Nullable bool `json:"nullable,omitempty"` Path []string `json:"path,omitempty"` Fields []*TraceField `json:"fields,omitempty"` Items []*TraceNode `json:"items,omitempty"` @@ -260,12 +259,12 @@ func parseFetch(fetch Fetch, options *getTraceOptions) *TraceFetch { func parseNode(n Node, options *getTraceOptions) *TraceNode { node := &TraceNode{ NodeType: getNodeType(n.NodeKind()), - Nullable: n.NodeKind() == NodeKindNull || n.NodePath() == nil, Path: n.NodePath(), } switch v := n.(type) { case *Object: + node.Fields = make([]*TraceField, 0, len(v.Fields)) for _, field := range v.Fields { node.Fields = append(node.Fields, parseField(field, options)) } From 1b1800fee0163ed9c04c06e0e018f94750ee3f5a Mon Sep 17 00:00:00 2001 From: spetrunin Date: Mon, 27 May 2024 17:22:58 +0300 Subject: [PATCH 14/15] produce more detailed trace for skipped fetches --- v2/pkg/engine/resolve/loader.go | 34 ++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/v2/pkg/engine/resolve/loader.go b/v2/pkg/engine/resolve/loader.go index 55d2f59f1..0bcfa5088 100644 --- a/v2/pkg/engine/resolve/loader.go +++ b/v2/pkg/engine/resolve/loader.go @@ -1027,16 +1027,18 @@ func (l *Loader) loadEntityFetch(ctx context.Context, fetch *EntityFetch, items res.fetchSkipped = true if l.ctx.TracingOptions.Enable { fetch.Trace.LoadSkipped = true + } else { + return nil } - return nil } if bytes.Equal(renderedItem, emptyObject) { // skip fetch if item is empty res.fetchSkipped = true if l.ctx.TracingOptions.Enable { fetch.Trace.LoadSkipped = true + } else { + return nil } - return nil } _, _ = item.WriteTo(preparedInput) err = fetch.Input.Footer.RenderAndCollectUndefinedVariables(l.ctx, nil, preparedInput, &undefinedVariables) @@ -1049,6 +1051,12 @@ func (l *Loader) loadEntityFetch(ctx context.Context, fetch *EntityFetch, items return errors.WithStack(err) } fetchInput := preparedInput.Bytes() + + if l.ctx.TracingOptions.Enable && res.fetchSkipped { + l.setTracingInput(fetchInput, fetch.Trace) + return nil + } + allowed, err := l.validatePreFetch(fetchInput, fetch.Info, res) if err != nil { return err @@ -1156,8 +1164,9 @@ WithNextItem: res.fetchSkipped = true if l.ctx.TracingOptions.Enable { fetch.Trace.LoadSkipped = true + } else { + return nil } - return nil } err = fetch.Input.Footer.RenderAndCollectUndefinedVariables(l.ctx, nil, preparedInput, &undefinedVariables) @@ -1170,6 +1179,12 @@ WithNextItem: return errors.WithStack(err) } fetchInput := preparedInput.Bytes() + + if l.ctx.TracingOptions.Enable && res.fetchSkipped { + l.setTracingInput(fetchInput, fetch.Trace) + return nil + } + allowed, err := l.validatePreFetch(fetchInput, fetch.Info, res) if err != nil { return err @@ -1243,6 +1258,19 @@ func setSingleFlightStats(ctx context.Context, stats *SingleFlightStats) context return context.WithValue(ctx, singleFlightStatsKey{}, stats) } +func (l *Loader) setTracingInput(input []byte, trace *DataSourceLoadTrace) { + trace.Path = l.renderPath() + if !l.ctx.TracingOptions.ExcludeInput { + trace.Input = make([]byte, len(input)) + copy(trace.Input, input) // copy input explicitly, omit __trace__ field + redactedInput, err := redactHeaders(trace.Input) + if err != nil { + return + } + trace.Input = redactedInput + } +} + func (l *Loader) executeSourceLoad(ctx context.Context, source DataSource, input []byte, res *result, trace *DataSourceLoadTrace) { if l.ctx.Extensions != nil { input, res.err = jsonparser.Set(input, l.ctx.Extensions, "body", "extensions") From 61e9dd88c32801f2aec83624c7fc040ca8b20b0f Mon Sep 17 00:00:00 2001 From: spetrunin Date: Mon, 27 May 2024 17:38:27 +0300 Subject: [PATCH 15/15] fix tests --- v2/pkg/engine/resolve/extensions_test.go | 2 +- v2/pkg/engine/resolve/resolvable_test.go | 11 ++++------- v2/pkg/engine/resolve/resolve.go | 8 +++++++- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/v2/pkg/engine/resolve/extensions_test.go b/v2/pkg/engine/resolve/extensions_test.go index a73f4dc79..faeca3e48 100644 --- a/v2/pkg/engine/resolve/extensions_test.go +++ b/v2/pkg/engine/resolve/extensions_test.go @@ -120,7 +120,7 @@ func TestExtensions(t *testing.T) { ctx.ctx = SetTraceStart(ctx.ctx, true) return res, ctx, - `{"errors":[{"message":"Unauthorized request to Subgraph 'users' at Path 'query', Reason: test."},{"message":"Failed to fetch from Subgraph 'reviews' at Path 'query.me'.","extensions":{"errors":[{"message":"could not render fetch input","path":["me"]}]}},{"message":"Failed to fetch from Subgraph 'products' at Path 'query.me.reviews.@.product'.","extensions":{"errors":[{"message":"could not render fetch input","path":["me","reviews","@","product"]}]}}],"data":{"me":null},"extensions":{"authorization":{"missingScopes":[["read:users"]]},"rateLimit":{"Policy":"policy","Allowed":0,"Used":0},"trace":{"info":{"trace_start_time":"","trace_start_unix":0,"parse_stats":{"duration_nanoseconds":0,"duration_pretty":"","duration_since_start_nanoseconds":0,"duration_since_start_pretty":""},"normalize_stats":{"duration_nanoseconds":0,"duration_pretty":"","duration_since_start_nanoseconds":0,"duration_since_start_pretty":""},"validate_stats":{"duration_nanoseconds":0,"duration_pretty":"","duration_since_start_nanoseconds":0,"duration_since_start_pretty":""},"planner_stats":{"duration_nanoseconds":0,"duration_pretty":"","duration_since_start_nanoseconds":0,"duration_since_start_pretty":""}},"fetch":{"id":"00000000-0000-0000-0000-000000000000","type":"single","data_source_id":"users","datasource_load_trace":{"raw_input_data":{},"single_flight_used":false,"single_flight_shared_response":false,"load_skipped":false}},"node_type":"object","nullable":true,"fields":[{"name":"me","value":{"fetch":{"id":"00000000-0000-0000-0000-000000000000","type":"single","data_source_id":"reviews","datasource_load_trace":{"single_flight_used":false,"single_flight_shared_response":false,"load_skipped":false}},"node_type":"object","path":["me"],"fields":[{"name":"id","value":{"node_type":"string","path":["id"]},"data_source_ids":["users"]},{"name":"username","value":{"node_type":"string","path":["username"]},"data_source_ids":["users"]},{"name":"reviews","value":{"node_type":"array","path":["reviews"],"items":[{"node_type":"object","nullable":true,"fields":[{"name":"body","value":{"node_type":"string","path":["body"]},"data_source_ids":["reviews"]},{"name":"product","value":{"fetch":{"id":"00000000-0000-0000-0000-000000000000","type":"single","data_source_id":"products","datasource_load_trace":{"single_flight_used":false,"single_flight_shared_response":false,"load_skipped":false}},"node_type":"object","path":["product"],"fields":[{"name":"upc","value":{"node_type":"string","path":["upc"]},"data_source_ids":["products"]},{"name":"name","value":{"node_type":"string","path":["data","name"]},"data_source_ids":["products"]}]},"data_source_ids":["reviews"]}]}]},"data_source_ids":["reviews"]}]}}]}}}`, + `{"errors":[{"message":"Unauthorized request to Subgraph 'users' at Path 'query', Reason: test."},{"message":"Failed to fetch from Subgraph 'reviews' at Path 'query.me'.","extensions":{"errors":[{"message":"could not render fetch input","path":["me"]}]}},{"message":"Failed to fetch from Subgraph 'products' at Path 'query.me.reviews.@.product'.","extensions":{"errors":[{"message":"could not render fetch input","path":["me","reviews","@","product"]}]}}],"data":{"me":null},"extensions":{"authorization":{"missingScopes":[["read:users"]]},"rateLimit":{"Policy":"policy","Allowed":0,"Used":0},"trace":{"info":{"trace_start_time":"","trace_start_unix":0,"parse_stats":{"duration_nanoseconds":0,"duration_pretty":"","duration_since_start_nanoseconds":0,"duration_since_start_pretty":""},"normalize_stats":{"duration_nanoseconds":0,"duration_pretty":"","duration_since_start_nanoseconds":0,"duration_since_start_pretty":""},"validate_stats":{"duration_nanoseconds":0,"duration_pretty":"","duration_since_start_nanoseconds":0,"duration_since_start_pretty":""},"planner_stats":{"duration_nanoseconds":0,"duration_pretty":"","duration_since_start_nanoseconds":0,"duration_since_start_pretty":""}},"fetch":{"id":"00000000-0000-0000-0000-000000000000","type":"single","data_source_id":"users","datasource_load_trace":{"raw_input_data":{},"single_flight_used":false,"single_flight_shared_response":false,"load_skipped":false}},"node_type":"object","fields":[{"name":"me","value":{"fetch":{"id":"00000000-0000-0000-0000-000000000000","type":"single","data_source_id":"reviews","datasource_load_trace":{"single_flight_used":false,"single_flight_shared_response":false,"load_skipped":false}},"node_type":"object","path":["me"],"fields":[{"name":"id","value":{"node_type":"string","path":["id"]},"data_source_ids":["users"]},{"name":"username","value":{"node_type":"string","path":["username"]},"data_source_ids":["users"]},{"name":"reviews","value":{"node_type":"array","path":["reviews"],"items":[{"node_type":"object","fields":[{"name":"body","value":{"node_type":"string","path":["body"]},"data_source_ids":["reviews"]},{"name":"product","value":{"fetch":{"id":"00000000-0000-0000-0000-000000000000","type":"single","data_source_id":"products","datasource_load_trace":{"single_flight_used":false,"single_flight_shared_response":false,"load_skipped":false}},"node_type":"object","path":["product"],"fields":[{"name":"upc","value":{"node_type":"string","path":["upc"]},"data_source_ids":["products"]},{"name":"name","value":{"node_type":"string","path":["data","name"]},"data_source_ids":["products"]}]},"data_source_ids":["reviews"]}]}]},"data_source_ids":["reviews"]}]}}]}}}`, func(t *testing.T) {} })) } diff --git a/v2/pkg/engine/resolve/resolvable_test.go b/v2/pkg/engine/resolve/resolvable_test.go index 333397055..4551f170d 100644 --- a/v2/pkg/engine/resolve/resolvable_test.go +++ b/v2/pkg/engine/resolve/resolvable_test.go @@ -488,13 +488,10 @@ func TestResolvable_WithTracingNotStarted(t *testing.T) { }, } out := &bytes.Buffer{} - err = res.Resolve(ctx.ctx, object, nil, out) + err = res.Resolve(ctx.ctx, object, object, out) assert.NoError(t, err) - assert.JSONEq(t, `{ - "data": {"hello":"world"}, - "extensions":{"trace":{"node_type":"object","nullable":true,"fields":[{"name":"hello","value":{"node_type":"string","path":["hello"]}}]}} - }`, out.String()) + assert.Equal(t, `{"data":{"hello":"world"},"extensions":{"trace":{"node_type":"object","fields":[{"name":"hello","value":{"node_type":"string","path":["hello"]}}]}}}`, out.String()) } func TestResolvable_WithTracing(t *testing.T) { @@ -571,8 +568,8 @@ func TestResolvable_WithTracing(t *testing.T) { SetPlannerStats(ctx.ctx, PhaseStats{}) out := &bytes.Buffer{} - err = res.Resolve(ctx.ctx, object, nil, out) + err = res.Resolve(ctx.ctx, object, object, out) assert.NoError(t, err) - assert.Equal(t, `{"data":{"topProducts":[{"name":"Table","stock":8,"reviews":[{"body":"Love Table!","author":{"name":"user-1"}},{"body":"Prefer other Table.","author":{"name":"user-2"}}]},{"name":"Couch","stock":2,"reviews":[{"body":"Couch Too expensive.","author":{"name":"user-1"}}]},{"name":"Chair","stock":5,"reviews":[{"body":"Chair Could be better.","author":{"name":"user-2"}}]}]},"extensions":{"trace":{"info":{"trace_start_time":"","trace_start_unix":0,"parse_stats":{"duration_nanoseconds":5,"duration_pretty":"5ns","duration_since_start_nanoseconds":5,"duration_since_start_pretty":"5ns"},"normalize_stats":{"duration_nanoseconds":5,"duration_pretty":"5ns","duration_since_start_nanoseconds":10,"duration_since_start_pretty":"10ns"},"validate_stats":{"duration_nanoseconds":5,"duration_pretty":"5ns","duration_since_start_nanoseconds":15,"duration_since_start_pretty":"15ns"},"planner_stats":{"duration_nanoseconds":5,"duration_pretty":"5ns","duration_since_start_nanoseconds":20,"duration_since_start_pretty":"20ns"}},"node_type":"object","nullable":true,"fields":[{"name":"topProducts","value":{"node_type":"array","path":["topProducts"],"items":[{"node_type":"object","nullable":true,"fields":[{"name":"name","value":{"node_type":"string","path":["name"]}},{"name":"stock","value":{"node_type":"integer","path":["stock"]}},{"name":"reviews","value":{"node_type":"array","path":["reviews"],"items":[{"node_type":"object","nullable":true,"fields":[{"name":"body","value":{"node_type":"string","path":["body"]}},{"name":"author","value":{"node_type":"object","path":["author"],"fields":[{"name":"name","value":{"node_type":"string","path":["name"]}}]}}]}]}}]}]}}]}}}`, out.String()) + assert.Equal(t, `{"data":{"topProducts":[{"name":"Table","stock":8,"reviews":[{"body":"Love Table!","author":{"name":"user-1"}},{"body":"Prefer other Table.","author":{"name":"user-2"}}]},{"name":"Couch","stock":2,"reviews":[{"body":"Couch Too expensive.","author":{"name":"user-1"}}]},{"name":"Chair","stock":5,"reviews":[{"body":"Chair Could be better.","author":{"name":"user-2"}}]}]},"extensions":{"trace":{"info":{"trace_start_time":"","trace_start_unix":0,"parse_stats":{"duration_nanoseconds":5,"duration_pretty":"5ns","duration_since_start_nanoseconds":5,"duration_since_start_pretty":"5ns"},"normalize_stats":{"duration_nanoseconds":5,"duration_pretty":"5ns","duration_since_start_nanoseconds":10,"duration_since_start_pretty":"10ns"},"validate_stats":{"duration_nanoseconds":5,"duration_pretty":"5ns","duration_since_start_nanoseconds":15,"duration_since_start_pretty":"15ns"},"planner_stats":{"duration_nanoseconds":5,"duration_pretty":"5ns","duration_since_start_nanoseconds":20,"duration_since_start_pretty":"20ns"}},"node_type":"object","fields":[{"name":"topProducts","value":{"node_type":"array","path":["topProducts"],"items":[{"node_type":"object","fields":[{"name":"name","value":{"node_type":"string","path":["name"]}},{"name":"stock","value":{"node_type":"integer","path":["stock"]}},{"name":"reviews","value":{"node_type":"array","path":["reviews"],"items":[{"node_type":"object","fields":[{"name":"body","value":{"node_type":"string","path":["body"]}},{"name":"author","value":{"node_type":"object","path":["author"],"fields":[{"name":"name","value":{"node_type":"string","path":["name"]}}]}}]}]}}]}]}}]}}}`, out.String()) } diff --git a/v2/pkg/engine/resolve/resolve.go b/v2/pkg/engine/resolve/resolve.go index 4f7ee8be8..846f87022 100644 --- a/v2/pkg/engine/resolve/resolve.go +++ b/v2/pkg/engine/resolve/resolve.go @@ -191,7 +191,13 @@ func (r *Resolver) ResolveGraphQLResponse(ctx *Context, response *GraphQLRespons return err } - return t.resolvable.Resolve(ctx.ctx, response.Data, response.FetchTree, writer) + fetchTree := response.FetchTree + if fetchTree == nil { + // fallback to the response data + fetchTree = response.Data + } + + return t.resolvable.Resolve(ctx.ctx, response.Data, fetchTree, writer) } type trigger struct {