How can I modify the behaviour of a subscription resolver
marktani opened this issue · comments
Consider this subscription resolver:
Subscription: {
publications: {
subscribe: async (parent, args, ctx, info) => {
return ctx.db.subscription.post({ }, info)
},
},
},
As it is written, it will send events for any mutation to the Post
model. However, I might want to cover different scenarios:
- block deleted events
- rename the title of the post in the subscription event being sent out
How can I handle these and other scenarios? Basically I want to intercept the subscription event with any custom logic.
There are multiple ways to accomplish this:
- By writing a custom
AsyncIterator
(see here) - By using
withFilter
(see here) - Implement the
resolve
method (see here):
Subscription: {
publications: {
subscribe: async (parent, args, ctx, info) => {
return ctx.db.subscription.post({ }, info)
},
resolve: (payload, args, context, info) => {
// Manipulate and return the new value
return payload;
},
},
},
Thanks so much! 🙏
I'm confused how withFilter
would function in conjunction of return ctx.db.subscription.post({ }, info)
- most of the example use cases of withFilter
are tied to returning an AsyncIterator
- is that the responsibility of ctx.db.subscription
somehow?
withFilter
seems to need some handholding to work as I would expect. withFilter
requires a resolved AsyncIterator
, but ctx.db.subscription.link
returns a Promise - so you have to await
. Finally I was surprised that I have to invoke the function filteredSubscription
on the return - it feels like this is different than the usual examples that leverage PubSub - where the function itself is returned.
Can somebody help explain why this works, and if the syntax is expected?
subscribe: async (parent, args, ctx, info) => {
const subscription = await ctx.db.subscription.link(
{ },
info,
)
filteredSubscription = withFilter( () => subscription , (payload, variables) => {
return true
})
return filteredSubscription()
}
I have another question related to this regarding the info
. Consider the following where I want to return an actual type instead of that whole subscription payload as a client doesn't need to know other details about a subscription (eg. kind of mutation, changed fields...).
type Subscription {
onFightStart: Fight!
}
# and query might look like...
subscription {
onFightStart {
id
attackerId
}
}
On the resolver side, it means that info
argument is structured based on my input query and cannot be passed to ctx.db.subscription.fight({}, info)
call directly. I need to somehow transform it so only relevant fields are being queried. I did found this article about demystifying the info
, but it still looks like a lot of legwork and feels like shoving my hands to intestines of something not particularly appealing.
const onFightStart = {
subscribe(_, args, ctx, info) {
const fightIterator = await ctx.db.subscription.fight({}, /* how to build correct info here? */)
const nextFight = async () => {
const { value, done } = await fightIterator.next()
// value is returned in { mutation, node } structure here, not what client wants
const newValue = transformValueByInfo(value, info) // <-- not sure how do this
return { value: newValue, done }
}
return { /* AsyncIterator juggling */ }
}
}
I am aware that if I don't pass any info
argument to prisma binding function, it will create one to contain every scalar field, but that doesn't work with subscriptions as an only scalar field is a mutation
. I had also figured out it's possible to pass a query string to build that info object, but that's hardly flexible. Ideally I want to grab fields that client is asking for.
const selectionSet = `{
mutation
node {
id
attacker {
id
}
}
}`
ctx.db.subscription.fight({}, selectionSet)
@FredyC if I understood you correctly, you have a subscription to the underlying service like this:
type Subscription {
fight: SubscriptionPayload
}
type SubscriptionPayload {
...
node: Fight
}
and want to expose this:
type Subscription {
fight: Fight
}
This means, that the info
object of the resolver for the fight
subscription will contain a query like this:
subscription {
fight {
id
}
}
but the underlying service needs to receive this:
subscription {
fight {
node {
id
}
}
}
In other words, the received query needs to be "embeded" into another query / we need to wrap it.
An API for this could look like this:
const onFightStart = {
subscribe(_, args, ctx, info) {
const fightIterator = await ctx.db.subscription.fight({}, wrapInfo(info, 'node'))
},
resolve(fight) {
return fight.node
}
}
@FredyC does that make sense to you?
You are correct on input information. However, I want to watch for changes in different fields than those requested by the client. I did not realize that before mainly because subscriptions are a bit confusing about this. Data you request will be watched for changes and that's not always what you want.
const onFightStart = {
subscribe(_, args, ctx, info) {
const fightIterator = await ctx.db.subscription.fight({}, watchFragment) // <-- just a string to specify what to watch
const nextFight = async () => {
const { value, done } = await fightIterator.next()
const fight = await ctx.db.query.fight({ where: { id: value.node.fightId } }, info) // <-- use info from client
return { onFightStart: fight }
}
return { /* AsyncIterator juggling */ }
}
}
I don't have any big issue with this right now, it's just kinda unintuitive and I had to spent quite some time to figuring that out and digging in the source code. I just hope that your breaking change in dotansimha/graphql-binding#80 won't prevent me doing this :)
Hey 👋,
I believe the initial question has been well resolved throughout the conversation. In need for gaining a better overview of the issues facing the current version, I'll close it.
Feel free to reopen the issue if you believe we should further discuss its context. 🙂