Add support for "flattening" fragment-spreads (#121)
## Summary:
One common use of fragment spreads is as the entirety of a field's
selection, e.g.
```graphql
query MyQuery {
myField {
...MyFragment
}
}
```
In this case, by default, genqlient generates a wrapper type
`MyQueryMyFieldMyType`, which just embeds `MyFragment`. This makes
sense if you later want to add more fields in addition to the fragment
spread. But if you don't -- and you did the fragment because you want
to share types, it's an extra layer of indirection. (Which becomes
especially onerous if `myField` has list type (`[MyType!]`), such that
it's not just an extra attribute-access to get to `MyFragment`.)
The new option `# @genqlient(flatten: true)` simplifies this situation:
if applied to `myField` is skips the wrapper type;
`MyQueryResponse.MyField` will simply have type `MyFragment` (or
`[]MyFragment`, or whatever). This should hopefully make the `typename`
option, which has more limitations, less necessary.
Note that in #30 the initial idea was to support this for fields as
well. This would require significant additional complexity in the
JSON-(un)marshaling code, and has proven less necessary, so I
implemented this option only for fragment-spreads for now. With that
restriction, it was shockingly simple; we have to hook into a bunch of
different places, but they're all quite simple, since the structure of
the Go types still matches the structure in GraphQL.
Issue: https://github.com/Khan/genqlient/issues/30
## Test plan:
make check
Author: benjaminjkraft
Reviewers: csilvers, dnerdy, aberkan, jvoll, mahtabsabet, MiguelCastillo, StevenACoffman
Required Reviewers:
Approved By: csilvers, dnerdy
Checks: ✅ Test (1.17), ✅ Test (1.16), ✅ Test (1.15), ✅ Test (1.14), ✅ Lint, ✅ Test (1.17), ✅ Test (1.16), ✅ Test (1.15), ✅ Test (1.14), ✅ Lint
Pull Request URL: https://github.com/Khan/genqlient/pull/121
This commit is contained in:
@@ -554,6 +554,116 @@ func TestNamedFragments(t *testing.T) {
|
||||
assert.Nil(t, resp.Beings[2])
|
||||
}
|
||||
|
||||
func TestFlatten(t *testing.T) {
|
||||
_ = `# @genqlient
|
||||
# @genqlient(flatten: true)
|
||||
fragment BeingFields on Being {
|
||||
...InnerBeingFields
|
||||
}
|
||||
|
||||
fragment InnerBeingFields on Being {
|
||||
id
|
||||
name
|
||||
... on User {
|
||||
# @genqlient(flatten: true)
|
||||
friends {
|
||||
...FriendsFields
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment FriendsFields on User {
|
||||
id
|
||||
name
|
||||
}
|
||||
|
||||
# @genqlient(flatten: true)
|
||||
fragment FlattenedUserFields on User {
|
||||
...FlattenedLuckyFields
|
||||
}
|
||||
|
||||
# @genqlient(flatten: true)
|
||||
fragment FlattenedLuckyFields on Lucky {
|
||||
...InnerLuckyFields
|
||||
}
|
||||
|
||||
fragment InnerLuckyFields on Lucky {
|
||||
luckyNumber
|
||||
}
|
||||
|
||||
fragment QueryFragment on Query {
|
||||
beings(ids: $ids) {
|
||||
__typename id
|
||||
...FlattenedUserFields
|
||||
... on Animal {
|
||||
# @genqlient(flatten: true)
|
||||
owner {
|
||||
...BeingFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# @genqlient(flatten: true)
|
||||
query queryWithFlatten(
|
||||
$ids: [ID!]!,
|
||||
) {
|
||||
...QueryFragment
|
||||
}`
|
||||
|
||||
ctx := context.Background()
|
||||
server := server.RunServer()
|
||||
defer server.Close()
|
||||
client := newRoundtripClient(t, server.URL)
|
||||
|
||||
resp, err := queryWithFlatten(ctx, client, []string{"1", "3", "12847394823"})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, resp.Beings, 3)
|
||||
|
||||
// We should get the following three beings:
|
||||
// User{Id: 1, Name: "Yours Truly"},
|
||||
// Animal{Id: 3, Name: "Fido"},
|
||||
// null
|
||||
|
||||
// Check fields both via interface and via type-assertion when possible
|
||||
// User has, in total, the fields: __typename id luckyNumber.
|
||||
assert.Equal(t, "User", resp.Beings[0].GetTypename())
|
||||
assert.Equal(t, "1", resp.Beings[0].GetId())
|
||||
// (luckyNumber we need to cast for)
|
||||
|
||||
user, ok := resp.Beings[0].(*QueryFragmentBeingsUser)
|
||||
require.Truef(t, ok, "got %T, not User", resp.Beings[0])
|
||||
assert.Equal(t, "1", user.Id)
|
||||
assert.Equal(t, 17, user.InnerLuckyFieldsUser.LuckyNumber)
|
||||
|
||||
// Animal has, in total, the fields:
|
||||
// __typename
|
||||
// id
|
||||
// owner { id name ... on User { friends { id name } } }
|
||||
assert.Equal(t, "Animal", resp.Beings[1].GetTypename())
|
||||
assert.Equal(t, "3", resp.Beings[1].GetId())
|
||||
// (owner.* we have to cast for)
|
||||
|
||||
animal, ok := resp.Beings[1].(*QueryFragmentBeingsAnimal)
|
||||
require.Truef(t, ok, "got %T, not Animal", resp.Beings[1])
|
||||
assert.Equal(t, "3", animal.Id)
|
||||
// on AnimalFields:
|
||||
assert.Equal(t, "1", animal.Owner.GetId())
|
||||
assert.Equal(t, "Yours Truly", animal.Owner.GetName())
|
||||
// (friends.* we have to cast for, again)
|
||||
|
||||
owner, ok := animal.Owner.(*InnerBeingFieldsUser)
|
||||
require.Truef(t, ok, "got %T, not User", animal.Owner)
|
||||
assert.Equal(t, "1", owner.Id)
|
||||
assert.Equal(t, "Yours Truly", owner.Name)
|
||||
assert.Len(t, owner.Friends, 1)
|
||||
assert.Equal(t, "2", owner.Friends[0].Id)
|
||||
assert.Equal(t, "Raven", owner.Friends[0].Name)
|
||||
|
||||
assert.Nil(t, resp.Beings[2])
|
||||
}
|
||||
|
||||
func TestGeneratedCode(t *testing.T) {
|
||||
// TODO(benkraft): Check that gqlgen is up to date too. In practice that's
|
||||
// less likely to be a problem, since it should only change if you update
|
||||
|
||||
Reference in New Issue
Block a user