graphql-factory-subscription v1.0.0
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-factoryLibrary object will emit events on data changes. The event name will be the operation name used to set up the subscription. A single
unsubscribefield should be defined on each schema containing subscriptions. A request tounsubscribewill remove the subscription with the operation name if it exists and return an error if it does not.- 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 updatedgraphqlrequest
.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} -graphqlfield resolve infosetupHandler{function} - sets up a new subscription. The handler's first argument is ametadataobject that can be used to store values from setup that are required for theremoveHandler. The handler's second argument is achangecallback that takes an optional customdebounceargument in milliseconds that will overrideoptions.debounce.changeshould be called on each data change.removeHandler{function} - removes the subscription. The handler's first argument is ametadataobject that contains values set in thesetupHandler. The handler's second argument is adonecallback that will send an error response if the first argument is an error.donemust 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} -graphqlfield 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.