Skip to content

Accessing list of request fields #157

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

Open
dsoprea opened this issue Sep 1, 2016 · 15 comments
Open

Accessing list of request fields #157

dsoprea opened this issue Sep 1, 2016 · 15 comments

Comments

@dsoprea
Copy link

dsoprea commented Sep 1, 2016

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
Copy link

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
Copy link

Look what I found, check this one: #125

@dsoprea
Copy link
Author

dsoprea commented Oct 4, 2016

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
Copy link

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
Copy link

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

#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 #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
Copy link

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

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

@kellyellis
Copy link

kellyellis commented May 20, 2017

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
Copy link

kellyellis commented May 20, 2017

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
Copy link

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

@yookoala
Copy link

yookoala commented Jan 16, 2018

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

@leebenson
Copy link

@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
Copy link

avocade commented Oct 5, 2018

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
Copy link

benmai commented Feb 12, 2019

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
Copy link

ppwfx commented Jun 27, 2019

@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
Copy link
Contributor

atombender commented Feb 25, 2020

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.

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

9 participants