Skip to content

Commit

Permalink
Merge pull request #551 from 99designs/improved-collect-fields-api
Browse files Browse the repository at this point in the history
Improved Collect Fields API and Documentation
  • Loading branch information
Mathew Byrne committed Feb 18, 2019
2 parents 6439f19 + e61b3e0 commit 327a1a3
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 2 deletions.
57 changes: 57 additions & 0 deletions docs/content/reference/field-collection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
title: 'Determining which fields were requested by a query'
description: How to determine which fields a query requested in a resolver.
linkTitle: Field Collection
menu: { main: { parent: 'reference' } }
---

Often it is useful to know which fields were queried for in a resolver. Having this information can allow a resolver to only fetch the set of fields required from a data source, rather than over-fetching everything and allowing gqlgen to do the rest.

This process is known as [Field Collection](https://facebook.github.io/graphql/draft/#sec-Field-Collection) — gqlgen automatically does this in order to know which fields should be a part of the response payload. The set of collected fields does however depend on the type being resolved. Queries can contain fragments, and resolvers can return interfaces and unions, therefore the set of collected fields cannot be fully determined until the type of the resolved object is known.

Within a resolver, there are several API methods available to query the selected fields.

## CollectAllFields

`CollectAllFields` is the simplest way to get the set of queried fields. It will return a slice of strings of the field names from the query. This will be a unique set of fields, and will return all fragment fields, ignoring fragment Type Conditions.

Given the following example query:

```graphql
query {
foo {
fieldA
... on Bar {
fieldB
}
... on Baz {
fieldC
}
}
}
```

Calling `CollectAllFields` from a resolver will yield a string slice containing `fieldA`, `fieldB`, and `fieldC`.

## CollectFieldsCtx

`CollectFieldsCtx` is useful in cases where more information on matches is required, or the set of collected fields should match fragment type conditions for a resolved type. `CollectFieldsCtx` takes a `satisfies` parameter, which should be a slice of strings of types that the resolved type will satisfy.

For example, given the following schema:

```graphql
interface Shape {
area: Float
}
type Circle implements Shape {
radius: Float
area: Float
}
union Shapes = Circle
```

The type `Circle` would satisfy `Circle`, `Shape`, and `Shapes` — these values should be passed to `CollectFieldsCtx` to get the set of collected fields for a resolved `Circle` object.

> Note
>
> `CollectFieldsCtx` is just a convenience wrapper around `CollectFields` that calls the later with the selection set automatically passed through from the resolver context.
18 changes: 18 additions & 0 deletions graphql/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,24 @@ func CollectFieldsCtx(ctx context.Context, satisfies []string) []CollectedField
return CollectFields(ctx, resctx.Field.Selections, satisfies)
}

// CollectAllFields returns a slice of all GraphQL field names that were selected for the current resolver context.
// The slice will contain the unique set of all field names requested regardless of fragment type conditions.
func CollectAllFields(ctx context.Context) []string {
resctx := GetResolverContext(ctx)
collected := CollectFields(ctx, resctx.Field.Selections, nil)
uniq := make([]string, 0, len(collected))
Next:
for _, f := range collected {
for _, name := range uniq {
if name == f.Name {
continue Next
}
}
uniq = append(uniq, f.Name)
}
return uniq
}

// Errorf sends an error string to the client, passing it through the formatter.
func (c *RequestContext) Errorf(ctx context.Context, format string, args ...interface{}) {
c.errorsMu.Lock()
Expand Down
69 changes: 69 additions & 0 deletions graphql/context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,72 @@ func TestGetResolverContext(t *testing.T) {
rc := &ResolverContext{}
require.Equal(t, rc, GetResolverContext(WithResolverContext(context.Background(), rc)))
}

func testContext(sel ast.SelectionSet) context.Context {

ctx := context.Background()

rqCtx := &RequestContext{}
ctx = WithRequestContext(ctx, rqCtx)

root := &ResolverContext{
Field: CollectedField{
Selections: sel,
},
}
ctx = WithResolverContext(ctx, root)

return ctx
}

func TestCollectAllFields(t *testing.T) {
t.Run("collect fields", func(t *testing.T) {
ctx := testContext(ast.SelectionSet{
&ast.Field{
Name: "field",
},
})
s := CollectAllFields(ctx)
require.Equal(t, []string{"field"}, s)
})

t.Run("unique field names", func(t *testing.T) {
ctx := testContext(ast.SelectionSet{
&ast.Field{
Name: "field",
},
&ast.Field{
Name: "field",
Alias: "field alias",
},
})
s := CollectAllFields(ctx)
require.Equal(t, []string{"field"}, s)
})

t.Run("collect fragments", func(t *testing.T) {
ctx := testContext(ast.SelectionSet{
&ast.Field{
Name: "fieldA",
},
&ast.InlineFragment{
TypeCondition: "ExampleTypeA",
SelectionSet: ast.SelectionSet{
&ast.Field{
Name: "fieldA",
},
},
},
&ast.InlineFragment{
TypeCondition: "ExampleTypeB",
SelectionSet: ast.SelectionSet{
&ast.Field{
Name: "fieldB",
},
},
},
})
s := CollectAllFields(ctx)
require.Equal(t, []string{"fieldA", "fieldB"}, s)
})
}
7 changes: 5 additions & 2 deletions graphql/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ type ExecutableSchema interface {
Subscription(ctx context.Context, op *ast.OperationDefinition) func() *Response
}

// CollectFields returns the set of fields from an ast.SelectionSet where all collected fields satisfy at least one of the GraphQL types
// passed through satisfies. Providing an empty or nil slice for satisfies will return collect all fields regardless of fragment
// type conditions.
func CollectFields(ctx context.Context, selSet ast.SelectionSet, satisfies []string) []CollectedField {
return collectFields(GetRequestContext(ctx), selSet, satisfies, map[string]bool{})
}
Expand All @@ -38,7 +41,7 @@ func collectFields(reqCtx *RequestContext, selSet ast.SelectionSet, satisfies []
if !shouldIncludeNode(sel.Directives, reqCtx.Variables) {
continue
}
if !instanceOf(sel.TypeCondition, satisfies) {
if len(satisfies) > 0 && !instanceOf(sel.TypeCondition, satisfies) {
continue
}
for _, childField := range collectFields(reqCtx, sel.SelectionSet, satisfies, visited) {
Expand All @@ -62,7 +65,7 @@ func collectFields(reqCtx *RequestContext, selSet ast.SelectionSet, satisfies []
panic(fmt.Errorf("missing fragment %s", fragmentName))
}

if !instanceOf(fragment.TypeCondition, satisfies) {
if len(satisfies) > 0 && !instanceOf(fragment.TypeCondition, satisfies) {
continue
}

Expand Down

0 comments on commit 327a1a3

Please sign in to comment.