graphql-factory-subscription

1.0.0 • Public • Published

graphql-factory-subscription

Subscription Middleware for GraphQL Factory

Overview

graphql-factory-subscription allows you to integrate implement the pub-sub subscription model in your graphql-facory definitions by injecting subscription setup and removal functions into the resolver context.

  • Subscriptions are identified by their operation name. Because of this, operation names should be unique enough to identify the subscription request including variables, root values, and context

  • The graphql-factory Library object will emit events on data changes. The event name will be the operation name used to set up the subscription.

  • A single unsubscribe field should be defined on each schema containing subscriptions. A request to unsubscribe will remove the subscription with the operation name if it exists and return an error if it does not.

  • Example

  • API


The following example will walk through setting up subscriptions on a RethinkDB database

Example

import * as graphql from 'graphql'
import GraphQLFactory from 'graphql-factory'
import GraphQLFactorySubscription from 'graphql-factory-subscription'
import RethinkDBDash from 'rethinkdbdash'

const r = RethinkDBDash({ silent: true })
const factory = GraphQLFactory(graphql)
const subscriptionPlugin = new GraphQLFactorySubscription()

// create a graphql-factory definition
const definition = {
  types: {
    User: {
      fields: {
        id: { type: 'String', primary: true },
        name: { type: 'String' },
        email: { type: 'String' }
      }
    }
  },
  schemas: {
    Users: {
      query: {
        fields: {
          listUsers: {
            type: ['User'],
            args: {
              id: { type: 'String' },
              name: { type: 'String' },
              email: { type: 'String' }
            },
            resolve (source, args) {
              return r.table('user').filter(args).run()
            }
          }
        }
      },
      subscription: {
        fields: {
          subscribeUser: {
            type: ['User'],
            args: {
              id: { type: 'String' },
              name: { type: 'String' },
              email: { type: 'String' }
            },
            resolve (source, args, context, info) {
              const query = r.table('user').filter(args)

              this.subscriptionSetup(
                info,
                function setup (metadata, change) {
                  return query.changes().run().then(cursor => {
                    metadata.cursor = cursor
                    return cursor.each(error => {
                      if (!error) change()
                    })
                  })
                },
                function remove (metadata, done) {
                  try {
                    metadata.cursor.close()
                    return done()
                  } catch (err) {
                    return done(err)
                  }
                }
              )

              return query.run()
            }
          },
          unsubscribe: {
            type: 'Boolean',
            resolve (source, args, context, info) {
              return this.subscriptionRemove(info)
            }
          }
        }
      }
    }
  }
}

Breaking down the subscription resolve you can see that first the query is created

let query = r.table('user').filter(args)

Then a call to this.subscriptionSetup is made passing 3 arguments. The resolve info, a setupHandler function, and a removeHandler function.

this.subscriptionSetup(
  info,
  function setup (metadata, change) {
    return _query.changes().run().then(cursor => {
      metadata.cursor = cursor
      return cursor.each(error => {
        if (!error) change()
      })
    })
  },
  function destroy (metadata, done) {
    try {
      metadata.cursor.close()
      return done()
    } catch (err) {
      return done(err)
    }
  }
)

The setupHandler should contain code to create a new subscription and call the change method on each new data change. For RethinkDB a changefeed is opened. In this example the cursor is stored in the metadata object so that is can be referenced during subscription removal. You should place any data/object required for the removal process in the metadata object during setup.

function setup (metadata, change) {
  return _query.changes().run().then(cursor => {
    metadata.cursor = cursor
    return cursor.each(error => {
      if (!error) change()
    })
  })
}

The removeHandler should remove and clean up the subscription. For RethinkDB the changefeed cursor is closed and the done callback is called with no arguments on success. If and error is passed as the first argument, the error will be sent as a response.

function remove (metadata, done) {
  try {
    metadata.cursor.close()
    return done()
  } catch (err) {
    return done(err)
  }
}

Finally the resolve function should execute the query and return the results. The setup handler is only called once to setup the subscription. Once the subscription is setup the call to subscriptionSetup acts as a noop unless the subscription is removed and then requested again.

return query.run()

Taking a closer look at the unsubscribe resolve you can see that a call to this.subscriptionRemove is made passing the resolve info. Also notice that the method is returned. The return value will be true if the subscription was removed and will throw an error otherwise. Because the subscription removal uses the info to identify the operation name and use that to remove the subscription, only 1 unsubscribe is necessary per GraphQLFactoryLibrary as it can remove any subscription in the libraries SubscriptionManager.

resolve (source, args, context, info) {
  return this.subscriptionRemove(info)
}

Make the library

let lib = factory.make(definition, {
  plugin: [
    subscriptionPlugin
  ]
})

Create a subscription event listener

userSubscription1 will be the subscription id. Data will be a graphql subscription/query response.

lib.on('userSubscription1', data => {
  console.log(data)
})

Perform a subscription request

lib.Users(`subscription userSubscription1 {
  subscribeUser {
    id,
    name,
    email
  }
}`)
.then(result => {
  console.log(result)
})

Make data changes to the user table

Upon making changes to the user table, userSubscription1 events will fire with the updated data.

Unsubscribe

Make a request to unsubscribe with the same operation name userSubscription1 to remove its subscription. Optionally you can also remove the event listener on the library.

lib.Users(`subscription userSubscription1 { unsubscribe } `)

API

SubscriptionPlugin ( [options:Object] )

Creates a new subscription plugin/middleware

  • [options] {object}
    • [debounce=100] - time in ms to wait for a new change before making an updated graphql request

.subscriptionSetup ( info:Info, setupHandler:function, removeHandler:Function )

Function available in the field resolve this context to set up a subscription. Note that this will throw an error if called on a mutation or query field

  • info {object} - graphql field resolve info
  • setupHandler {function} - sets up a new subscription. The handler's first argument is a metadata object that can be used to store values from setup that are required for the removeHandler. The handler's second argument is a change callback that takes an optional custom debounce argument in milliseconds that will override options.debounce. change should be called on each data change.
  • removeHandler {function} - removes the subscription. The handler's first argument is a metadata object that contains values set in the setupHandler. The handler's second argument is a done callback that will send an error response if the first argument is an error. done must always be called.

.subscriptionRemove ( info:Info )

Function available in the field resolve this context to remove a subscription. Returns Boolean or an error response

  • info {object} - graphql field resolve info

.subscriptionInfo ()

Function available in the field resolve this context to return an object containing the current subscriptions and info about them where the key is the subscription name and the value is info about the subscription.

Package Sidebar

Install

npm i graphql-factory-subscription

Weekly Downloads

2

Version

1.0.0

License

MIT

Last publish

Collaborators

  • vbranden