nicolasdao / schemaglue

Naturally breaks down your monolithic graphql schema into bits and pieces and then glue them back together.
BSD 3-Clause "New" or "Revised" License
116 stars 13 forks source link

Add support for functional dependency injection inside resolvers #12

Closed jeffaburt closed 6 years ago

jeffaburt commented 6 years ago

First off, I want to thank you for an awesome library! I recently started a greenfield project using GraphQL, and this library has been an awesome addition.

To facilitate functional dependency injection, I'm proposing to allow resolvers to be exported as functions with a single context argument. When glueing everything together using glue('src/graphql', options), options.context gets passed into each resolver's context argument. context is best used as a hash of dependencies. This pattern facilitates the unit testing of resolvers by allowing dependencies to be easily mocked.

Basic example passing in raw data:

// app.js
const productMocks = [
  { id: 1, name: 'Product 1', shortDescription: 'Product #1.' }, 
  { id: 2, name: 'Product 2', shortDescription: 'Product #2.' }
]
const { schema, resolver } = glue('src/graphql', { context: { productMocks } })

// src/graphql/product/resolver.js
exports.resolver = context => {
  return {
    Query: {
      products(root, { id }) {
        const { productMocks } = context
        const results = id ? productMocks.filter(p => p.id == id) : productMocks
        return results ||  null
      }
    }
  }
}

Advanced example passing in a curried function that encapsulates the shared database connection. This makes testing resolvers incredibly easy:

// app.js
const sharedDatabaseConnection = ...
const GetProducts = ProductsService.GetProducts({ db: sharedDatabaseConnection }) // curried function so we don't have to pass the shared database connection around
const { schema, resolver } = glue('src/graphql', { context: { GetProducts } })

// src/graphql/product/resolver.js
exports.resolver = context => {
  return {
    Query: {
      products(root, { id }) {
        const { GetProducts } = context
        return GetProducts(id) // resolver has no direct reference to the shared database connection
      }
    }
  }
}

// test/graphql/product/resolver-test.js
it('returns the correct products for a given id', () => {
  const GetProducts = id => ([{ id: id, name: 'Special product', shortDescription: 'My special product mock.' }])
  const { resolver } = glue('src/graphql', { context: { GetProducts } })
  assert.deepEqual(resolver.Query.products(null, { id: 1 }), [{ id: 1, name: 'Special product', shortDescription: 'My special product mock.' }])
})

Thanks!

nicolasdao commented 6 years ago

Hi @jeffaburt ,

You rock! Your suggestion makes total sense. Let me have a quick look into your PR as well as your DI pattern. I usually use the context of the resolver method (products(root, { id }, context)) when I mock. I'm currently on holidays in Thailand and I'll be back on the 7th of August. I'll try to have a look before. Worst case scenario I'll have a look into that when I'm back.

Thanks a million time for your support and for helping us.

Cheers,

Nic

jeffaburt commented 6 years ago

Thanks for the feedback and I hope you're enjoying your vacation! After looking at it again, I don't see a good reason not to just use the context argument in the resolver function, especially since it's already built in. I'll go ahead and close this PR in favor of that. Thanks again for an awesome library!