Skip to content
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

How can I modify the behaviour of a subscription resolver #78

Closed
marktani opened this issue Jan 25, 2018 · 9 comments
Closed

How can I modify the behaviour of a subscription resolver #78

marktani opened this issue Jan 25, 2018 · 9 comments

Comments

@marktani
Copy link
Contributor

marktani commented Jan 25, 2018

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.

@schickling
Copy link
Contributor

There are multiple ways to accomplish this:

  1. By writing a custom AsyncIterator (see here)
  2. By using withFilter (see here)
  3. 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;
      },
    },
  },

@marktani
Copy link
Contributor Author

Thanks so much! 🙏

@tbrannam
Copy link

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?

@tbrannam
Copy link

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()
  }

@danielkcz
Copy link

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)

@schickling
Copy link
Contributor

You're exactly right, @FredyC. This should be a lot easier and will in fact become easier once this issue is tackled: #118

@timsuchanek
Copy link
Contributor

@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?

@timsuchanek timsuchanek reopened this Apr 9, 2018
@danielkcz
Copy link

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 :)

@maticzav maticzav added the FAQ label Nov 19, 2018
@maticzav
Copy link
Collaborator

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. 🙂

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants