graphql-go / graphql

An implementation of GraphQL for Go / Golang
MIT License
9.87k stars 839 forks source link

Accessing list of request fields #157

Open dsoprea opened 8 years ago

dsoprea commented 8 years ago

What's the correct way to determine what data needs to be constructed and returned? Obviously, some fields might have a higher cost to populate than others. This is a resolve function:

    (*fields)["Classifier"] = &graphql.Field{
        Type: classifierType,
        Args: graphql.FieldConfigArgument{
            "uuid": &graphql.ArgumentConfig{
                Type: graphql.ID,
            },
        },
        Resolve: func(p graphql.ResolveParams) (interface{}, error) {
            uuid := p.Args["uuid"].(string)

This is ResolveParams:

type ResolveParams struct {
    // Source is the source value
    Source interface{}

    // Args is a map of arguments for current GraphQL request
    Args map[string]interface{}

    // Info is a collection of information about the current execution state.
    Info ResolveInfo

    // Context argument is a context value that is provided to every resolve function within an execution.
    // It is commonly
    // used to represent an authenticated user, or request-specific caches.
    Context context.Context
}

The only immediately interesting field here is ResolveInfo, but it, too, doesn't seem to provide the requested fields:

type ResolveInfo struct {
    FieldName      string
    FieldASTs      []*ast.Field
    ReturnType     Output
    ParentType     Composite
    Schema         Schema
    Fragments      map[string]ast.Definition
    RootValue      interface{}
    Operation      ast.Definition
    VariableValues map[string]interface{}
}

Thanks.

mortezaalizadeh commented 7 years ago

I have a same problem here, was just looking for a solution and found this Open Issue. Is there any way to find out the list of requested field to optimise the query? Cheers

mortezaalizadeh commented 7 years ago

Look what I found, check this one: https://github.com/graphql-go/graphql/issues/125

dsoprea commented 7 years ago

Thanks for sharing. It looks like that might work.

It frustrates me that this should be such a common requirement and yet there seems to be very few other people interested in sharing a solution for it.

Let me know if it works or doesn't work.

mortezaalizadeh commented 7 years ago

I had to eventually implement it differently, but while I was working on that solution, I managed to get the field names. It really depends on which Resolve function you implement the code in. It was not easy for me to walk down in the tree and find the selection set. Here is the link to the source code that I fixed differently tonight. https://github.com/microbusinesses/AddressService/blob/master/endpoint/endpoints.go

The HTTP query could look like this: http://localhost/Api?query={address(id:"1a76ed6e-4f2b-4241-8514-1fe1cb9e0ed1"){details(keys: ["1" , " 3 4 ", "2", " ", "5"]){value}}}

mortezaalizadeh commented 7 years ago

OK, I finally found the way to do it using the solution provided in:

https://github.com/graphql-go/graphql/issues/125

Note the line in the import is the actual trick. Both golang and graphql-go has thei r implementation of Field struct. if you use what provided in https://github.com/graphql-go/graphql/issues/125, gofmt import the golang implementation by default. You need to make sure you import the right field in the import. I will update my AddressService implementation soon.

import ( ast "github.com/graphql-go/graphql/language/ast" )

func getSelectedFields(selectionPath []string, resolveParams graphql.ResolveParams) []string { fields := resolveParams.Info.FieldASTs for , propName := range selectionPath { found := false for , field := range fields { if field.Name.Value == propName { selections := field.SelectionSet.Selections fields = make([]ast.Field, 0) for , selection := range selections { fields = append(fields, selection.(ast.Field)) } found = true break } } if !found { return []string{} } } var collect []string for , field := range fields { collect = append(collect, field.Name.Value) } return collect }

mortezaalizadeh commented 7 years ago

Job done, look at this to find out how you should implement it

https://github.com/microbusinesses/AddressService/blob/master/endpoint/endpoints.go

kellyellis commented 7 years ago

Just an FYI for anyone reading this, I used the same solution, and it breaks if the client uses fragments:

[interface conversion: ast.Selection is *ast.FragmentSpread, not *ast.Field]

I'll post here when I find a solution to this.

(I'm quite surprised there aren't more users of this library requesting this feature.)

kellyellis commented 7 years ago

Here is my updated recursive solution to account for fragments.

I did modify the original signature and skipped passing in a selectionPath param, because I didn't need it and skipping that loop was cleaner for me...plus then I could just append to the slice directly rather than converting it at the end. If you do need it, it should be fairly straightforward to add back in, I think. The solution here will return the selected fields for the field you are resolving. (The selectionPath param is needed if you would like to find out the selected fields of some nested field.)

I also added errors to handle unexpected scenarios, since this is production code. :)

func getSelectedFields(params graphql.ResolveParams) ([]string, error) {
    fieldASTs := params.Info.FieldASTs
    if len(fieldASTs) == 0 {
        return nil, fmt.Errorf("getSelectedFields: ResolveParams has no fields")
    }
    return selectedFieldsFromSelections(params, fieldASTs[0].SelectionSet.Selections)
}

func selectedFieldsFromSelections(params graphql.ResolveParams, selections []ast.Selection) ([]string, error) {
    var selected []string
    for _, s := range selections {
        switch t := s.(type) {
        case *ast.Field:
            selected = append(selected, s.(*ast.Field).Name.Value)
        case *ast.FragmentSpread:
            n := s.(*ast.FragmentSpread).Name.Value
            frag, ok := params.Info.Fragments[n]
            if !ok {
                return nil, fmt.Errorf("getSelectedFields: no fragment found with name %v", n)
            }
            sel, err := selectedFieldsFromSelections(params, frag.GetSelectionSet().Selections)
            if err != nil {
                return nil, err
            }
            selected = append(selected, sel...)
        default:
            return nil, fmt.Errorf("getSelectedFields: found unexpected selection type %v", t)
        }
    }
    return selected, nil
}

(Edited to correct my original misunderstanding of how selectionPath was used in the first place.)

kellyellis commented 7 years ago

Edit: doesn't work for in-line fragments, sigh. Not fixing it now to do so since my team isn't using them.

yookoala commented 6 years ago

@kellyellis: Seems working fine for fragments in my test.

leebenson commented 6 years ago

@mortezaalizadeh and @kellyellis, thanks so much for your contributions. This is exactly what I'm looking for. Very surprised it's not included as a convenience in the lib, tbh. It's such a common/obvious requirement to know which fields have been requested, so that they can be factored into an SQL builder/query.

I'm guessing the lack of references to this issue/commentary means most devs are just doing a select *, which kinda defeats the point of GraphQL!

avocade commented 5 years ago

Thanks, agree that this should be considered for the library. Major waste to request fields from the DB that we'll later just throw away.

benmai commented 5 years ago

To pile onto this, in addition to selecting unused fields from the DB, there are additional costs when using microservices. I'm running into a case where I can retrieve an entire record from a remote service, but some of the record's fields may cause the request to take additional time (in this case, the fields are encrypted and need to be decrypted). I realize a solution would be to implement a GraphQL endpoint in that microservice and stitch it together, but I'd rather not go through the overhead. It would be great to be able to have access to the fields in order to employ some other method of only requesting data when required.

I'll try out the solutions above and see how that works out for now though!

ppwfx commented 5 years ago

@kellyellis thanks for the function

I modified it to account for nested selects

func getSelectedFields(params graphql.ResolveParams) (map[string]interface{}, error) {
    fieldASTs := params.Info.FieldASTs
    if len(fieldASTs) == 0 {
        return nil, fmt.Errorf("getSelectedFields: ResolveParams has no fields")
    }
    return selectedFieldsFromSelections(params, fieldASTs[0].SelectionSet.Selections)
}

func selectedFieldsFromSelections(params graphql.ResolveParams, selections []ast.Selection) (selected map[string]interface{}, err error) {
    selected = map[string]interface{}{}

    for _, s := range selections {
        switch s := s.(type) {
        case *ast.Field:
            if s.SelectionSet == nil {
                selected[s.Name.Value] = true
            } else {
                selected[s.Name.Value], err = selectedFieldsFromSelections(params, s.SelectionSet.Selections)
                if err != nil {
                    return
                }
            }
        case *ast.FragmentSpread:
            n := s.Name.Value
            frag, ok := params.Info.Fragments[n]
            if !ok {
                err = fmt.Errorf("getSelectedFields: no fragment found with name %v", n)

                return
            }
            selected[s.Name.Value], err = selectedFieldsFromSelections(params, frag.GetSelectionSet().Selections)
            if err != nil {
                return
            }
        default:
            err = fmt.Errorf("getSelectedFields: found unexpected selection type %v", s)

            return
        }
    }

    return
}
atombender commented 4 years ago

It's amazing that this issue is open after four years with no good solution. @chris-ramon?

From what I can tell, graphql does not provide information at runtime about the field selection as correlated to the actual schema.

The AST is useless because the AST only says something about the incoming query, but of course all of those parts map to the underlying schema declared with graphql.Field and so on. You can't use the AST to guide column selection for a database query, for example, because the AST can't tell "native" fields (that are defined in a struct) from "synthetic" (that are not defined there, but as a custom resolver).

For example, let's say you have a basic schema:

type Person struct {
    ID string
    Name string
    EmployerID string
}

type Company struct { ... }

companyType := ...

personType := graphql.NewObject(graphql.ObjectConfig{
    Name:   "Person",
    Fields: graphql.Fields{
        "id": &graphql.Field{Type: graphql.String},
        "name": &graphql.Field{Type: graphql.String},
        "employer": &graphql.Field{
            Type: companyType,
            Resolve: func(p graphql.ResolveParams) (interface{}, error) {
                person := p.Value.(*Person)
                return findCompany(person.EmployerID)
            },
        },
    }
})

graphql.NewSchema(graphql.SchemaConfig{
    Query: graphql.NewObject(graphql.ObjectConfig{
        Name: "RootQuery",
        Args: graphql.FieldConfigArgument{
            "id": &graphql.ArgumentConfig{Type: graphql.String},
        },
        Fields: graphql.Fields{
            "person": &graphql.Field{
                Type: personType,
                Resolve: func(p graphql.ResolveParams) (interface{}, error) {
                    columns := findColumns(p)
                    var person Person
                    // Database pseudo-code:
                    if err := db.Query("select "+columns+" from persons where id = ?", p.Args["id"], &person); err != nil {
                        return nil, err
                    }
                    return &person, nil
                },
            },
        },
    }),
})

Here, person.company is, of course, a synthetic field. It doesn't have a struct field in Person.

If you have a query such as this:

query {
  person(id: "123") {
    name, employer { id }
  }
}

...the challenge here is for the Person resolver to fetch name as a column from the database, so you want to generate something like select name, employer_id from persons. How to know to fetch employer_id is another story, but name should be simple! But it isn't.

We can fiddle with p.Info.FieldASTs to find which fields are requested in the AST, but that gives us just the AST stuff. We don't know how to map those to the schema. Specifically, the field list name, employer refers to one actual struct field, name, but employer is not a database field, so we can't add that to our database column list.

But surely graphql must have this information. In every single resolver, it must know what the requested child fields (as *graphql.Field) are.