Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Is there a way to auto deside when to call a resolver with a @goField(forceResolver: true) #3446

Open
fearfate opened this issue Dec 21, 2024 · 3 comments

Comments

@fearfate
Copy link

What happened?

If I set a @goField(forceResolver: true) directive, it will be called anyway

What did you expect?

We need a mechanism to automatically determine whether to invoke a resolver, instead of manually parsing fields to determine whether to execute.

like this:

type Product {
        productId: ID!
        
        name: String!

        extraFields: Any
}

type Project implements Node {
	id: ID!
        
        name: String!

	products: [Product!] @goField(forceResolver: true, forceResolverIgnoredKeys: ["productId", "name"])
}

type Query {
        project(id: ID!) Project!
}

the query will not call the products resolver in Project

query  {
        project(id: "xxxxx") {
                 id
                 name
                 products {
                         productId  # or name or both of them
                 }
        }
}

the query will call resolver

query  {
        project(id: "xxxxx") {
                 id
                 name
                 products {
                         productId
                         name
                         extraFields
                 }
        }
}

Minimal graphql.schema and models to reproduce

None

versions

  • go run github.com/99designs/gqlgen version? v0.17.61
  • go version? go version go1.23.4 linux/amd64
@fearfate
Copy link
Author

fearfate commented Dec 23, 2024

It's hard to find the generate code about @goField, could somebody give me a hint, so i can open a PR to impl this

@fearfate
Copy link
Author

I am trying to impl this with Trie Tree rather than using the example in docs, but I found it's impossible to write tests, because I can't create a graphql.OperationContext

package ignore

import (
	"context"
	"fmt"
	"strings"

	"github.com/99designs/gqlgen/graphql"
)

type TrieNode struct {
	children map[string]*TrieNode
}

func NewTrie() *TrieNode {
	return &TrieNode{
		children: make(map[string]*TrieNode),
	}
}

func (t *TrieNode) Insert(path string) {
	parts := strings.Split(path, ".")
	current := t
	for _, part := range parts {
		if _, exists := current.children[part]; !exists {
			current.children[part] = &TrieNode{
				children: make(map[string]*TrieNode),
			}
		}
		current = current.children[part]
	}
}

func (t *TrieNode) IsCommonPath(ctx context.Context) (bool, error) {
	selection := graphql.CollectFieldsCtx(ctx, nil)

	if len(selection) == 0 {
		return true, nil
	}

	// Check if all fields have the same structure
	firstChild := selection[0]
	firstChildTrie := t.children[firstChild.Name]
	if firstChildTrie == nil {
		return false, nil
	}

	// Verify all other fields match the first field's structure
	for _, field := range selection[1:] {
		childTrie := t.children[field.Name]
		if childTrie == nil || !compareTrie(firstChildTrie, childTrie) {
			return false, nil
		}
	}

	// Recursively check child selections
	for _, field := range selection {
		childCtx := graphql.WithFieldContext(ctx, &graphql.FieldContext{
			Field: field,
		})
		common, err := firstChildTrie.IsCommonPath(childCtx)
		if err != nil {
			return false, err
		}
		if !common {
			return false, nil
		}
	}

	return true, nil
}

func compareTrie(a, b *TrieNode) bool {
	if len(a.children) != len(b.children) {
		return false
	}

	for key := range a.children {
		if b.children[key] == nil {
			return false
		}
	}

	return true
}

func Ignore(ctx context.Context, ignore []string) (bool, error) {
	if ctx == nil {
		return false, fmt.Errorf("context cannot be nil")
	}

	// Build trie from selection paths
	trie := NewTrie()
	for _, path := range ignore {
		if path == "" {
			return false, fmt.Errorf("ignore path cannot be empty")
		}
		trie.Insert(path)
	}

	common, err := trie.IsCommonPath(ctx)
	if err != nil {
		return false, fmt.Errorf("error checking common path: %w", err)
	}

	return common, nil
}
package ignore

import (
	"context"
	"testing"

	"github.com/99designs/gqlgen/graphql"
	"github.com/stretchr/testify/assert"
	"github.com/vektah/gqlparser/v2/ast"
)

func TestIgnore(t *testing.T) {
	tests := []struct {
		name     string
		sel      []string
		fields   []graphql.CollectedField
		expected bool
	}{
		{
			name:     "empty selection",
			sel:      []string{"a.b.c"},
			fields:   []graphql.CollectedField{},
			expected: true,
		},
		{
			name: "exact match",
			sel:  []string{"a.b.c"},
			fields: []graphql.CollectedField{
				{
					Field: &ast.Field{
						Name: "a",
						SelectionSet: ast.SelectionSet{
							&ast.Field{
								Name: "b",
								SelectionSet: ast.SelectionSet{
									&ast.Field{Name: "c"},
								},
							},
						},
					},
				},
			},
			expected: true,
		},
		{
			name: "partial match",
			sel:  []string{"a.b.c"},
			fields: []graphql.CollectedField{
				{
					Field: &ast.Field{
						Name: "a",
						SelectionSet: ast.SelectionSet{
							&ast.Field{Name: "b"},
						},
					},
				},
			},
			expected: false,
		},
		{
			name: "multiple paths common structure",
			sel:  []string{"a.b.c", "a.b.d"},
			fields: []graphql.CollectedField{
				{
					Field: &ast.Field{
						Name: "a",
						SelectionSet: ast.SelectionSet{
							&ast.Field{
								Name: "b",
								SelectionSet: ast.SelectionSet{
									&ast.Field{Name: "c"},
									&ast.Field{Name: "d"},
								},
							},
						},
					},
				},
			},
			expected: true,
		},
		{
			name: "different structures",
			sel:  []string{"a.b.c", "a.b.d"},
			fields: []graphql.CollectedField{
				{
					Field: &ast.Field{
						Name: "a",
						SelectionSet: ast.SelectionSet{
							&ast.Field{
								Name: "b",
								SelectionSet: ast.SelectionSet{
									&ast.Field{Name: "c"},
								},
							},
							&ast.Field{Name: "d"},
						},
					},
				},
			},
			expected: false,
		},
		{
			name:     "empty input",
			sel:      []string{},
			fields:   []graphql.CollectedField{},
			expected: true,
		},
		{
			name: "nested paths",
			sel:  []string{"a.b.c.d", "a.b.c.e"},
			fields: []graphql.CollectedField{
				{
					Field: &ast.Field{
						Name: "a",
						SelectionSet: ast.SelectionSet{
							&ast.Field{
								Name: "b",
								SelectionSet: ast.SelectionSet{
									&ast.Field{
										Name: "c",
										SelectionSet: ast.SelectionSet{
											&ast.Field{Name: "d"},
											&ast.Field{Name: "e"},
										},
									},
								},
							},
						},
					},
				},
			},
			expected: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result, err := Ignore(context.Background(), tt.sel)
			assert.NoError(t, err)
			assert.Equal(t, tt.expected, result)
		})
	}
}

@mboudraa
Copy link

if you don't put a @goField(forceResolver: true) annotation on the product keys productId, name but on every other field, those fields will not be resolved individually. It will always assume that when you return the field products in your project query, the fields not annotated with forceResolver have been resolved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants