The info
object used in resolvers includes the sub-selection of the query for the current field.
As described here prisma-labs/prisma-binding#118, there are valid use-cases for extracting subfields from this selection to pass that into a binding.
In #118 and also this PR #69, an approach is presented that tries to solve this issue.
In the following I describe how this could be used and discuss what the fundamental limitation of this approach is, that we need to understand to move forward with this topic.
Let's say we have the following data model:
datamodel.graphql
type User {
id: ID! @unique
name: String!
}
type Address {
id: ID! @unique
street: String!
userId: String!
}
In this case, for some reason we couldn't create a direct relation between both types, maybe because they're living in separate sources.
The schema that we actually want to expose is this:
type Query {
me: User
}
type User {
id: ID! @unique
addresses(skip: Int first: Int): [Address!]!
}
type Address {
id: ID! @unique
street: String!
}
Now we want to establish this connection between types using graphql-yoga
& graphql-binding
.
The current approach without the new tooling would look like this:
{
Query: {
me(parent, args, ctx, info) {
const userId = getUserId(ctx)
return ctx.db.query.user({where: {id: userId}}, info)
}
},
User: {
addresses: {
fragment: `fragment UserId on User { id }`,
resolve(parent, args, ctx, info) {
return ctx.binding.query.addresses({where: {userId: parent.id}}, info)
}
}
}
}
As described in #118, it can be more efficient (query batching / saving queries) to combine this into one resolver.
Now makeSubInfo
comes into play.
The API of makeSubInfo
looks like this:
makeSubInfo(info: GraphQLResolveInfo, path: string, fragment?: string): GraphQLResolveInfo
We now can use that util function for our example:
{
Query: {
me(parent, args, ctx, info) {
const userId = getUserId(ctx)
const subInfo= makeSubInfo(info, 'address')
const addresses = ctx.db.query.addresses({where: {userId}}, subInfo)
return {
id: userId,
addresses
}
}
},
}
For queries, that look like this, it's working as expected:
{
me {
id
addresses {
street
}
}
}
Problem
However, the following queries will break this resolver:
1. Not selecting the sub field
Here addresses
are not selected at all, so the delegation to graphql binding will fail.
2. Using aliases in the query
{
me {
id
first5: address(skip: 0, first: 5) {
street
}
next5: address(skip: 5, first: 5) {
street
}
}
}
Here we're using multiple aliases for the same field, but different args, so their respond has to differ.
The current implementation would just return an object of this form:
{
me: {
id: '',
addresses: [],
}
}
To actually provide the correct values for each aliased field, the returned object has to look like this:
{
me: {
id: '',
first5: [],
next5: []
}
}
This means we have to either sacrifice the use of aliases or adjust the API of makeSubInfo
.
Solution
The sacrifice of just not using aliases in queries is trivial, we don't have to further discuss that.
But let's say we still want to support aliases in our queries. This would mean, that we need to keep the output of the delegation to the binding with makeSubInfo
dynamic.
One solution could be, that instead of directly returning the resolved value, we would have to wrap it in an object like this:
const query = `{ me { addresses { street } } }`
const result = binding.query.addresses({}, makeSubInfo(info, 'addresses'))
assert(result).toBe({
addresses: []
})
or in the case of a query containing aliases:
const query = `{ me { addresses1 { street } addresses2 { street } } }`
const result = binding.query.addresses({}, makeSubInfo(info, 'addresses'))
assert(result).toBe({
addresses1: [],
addresses2: [],
})
This, however, would make typings on these bindings useless, as the response could look like anything. This is a trade-off we have to live with in this case.
To keep the nice typings and still have with flexible API, the proposal is to separate these into 2 APIs:
binding.delegateQuery.addresses({}, info)
binding.query.addresses({}, '{ id }')
The .delegateQuery
method would return the type any
as typings are of no value here, the .query
method would return a type that is being generated based on the selection set in the last argument of the method, the fragment.
This is just a rough proposal for the future API, open for discussion.
Anyone interested working on this topic, please respond here or join our public Graphcool slack to join the conversation!