graphql-factory-subscription
v1.0.0
Published
Subscription Middleware for Graphql Factory
Downloads
3
Readme
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 tounsubscribe
will remove the subscription with the operation name if it exists and return an error if it does not.
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 updatedgraphql
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 infosetupHandler
{function
} - sets up a new subscription. The handler's first argument is ametadata
object that can be used to store values from setup that are required for theremoveHandler
. The handler's second argument is achange
callback that takes an optional customdebounce
argument in milliseconds that will overrideoptions.debounce
.change
should be called on each data change.removeHandler
{function
} - removes the subscription. The handler's first argument is ametadata
object that contains values set in thesetupHandler
. The handler's second argument is adone
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.