Search code examples
graphqlapolloapollo-server

Apollo conditional data sources & initialization lifecycle


I have a specific use case where a user’s data sources are conditional - e.g based on the data sources saved in the database for every specific user.

This also means every data source has unique credentials for every user, which is fine for RESTDataSource because I can use the willSendRequest to set the Authentication headers before each request.

However, I have custom data sources that have proprietary clients (for example JSForce for Salesforce) - and they have their own fetch mechanism.

As of now - I have a custom transformer directive that fetches the tokens from the database and adds it into the context - however, the directive is ran before the dataSource.initialize() method - so that I can’t use the credentials there because the context still doesn’t have it.

I also don’t want to initialize all data sources for every user even if he doesn’t use said data source in this request - but the dataSources() function doesn’t accept any parameter and is not contextual.

Bottom line is - is it possible to pass data sources conditionally based even on the Express request? When is the right time to pass the tokens and credentials to the dataSource? Maybe add my own custom init function and call it from the directive?


Solution

  • So you have options. Here are 2 choices:

    1. Just add your dataSources

    If you just initialize all dataSources, internally it can check to see if the user has access. You could have a getClient function that resolves on the client or throws an UnauthorizedError, depending.

    2. Don't just add your dataSources

    So if you really don't want to initialize the dataSources at ALL, you can absolutely do this by adding the "dataSources" yourself, just like Apollo does it.

    const server = new ApolloServer({
      // this example uses apollo-server-express
      context: async ({ req, res }) => {
        const accessToken = req.headers?.authorization?.split(' ')[1] || ''
        const user = accessToken && buildUser(accessToken)
    
        const context = { user }
    
        // You can't use the name "dataSources" in your config because ApolloServer will puke, so I called them "services"
        await addServices(context)
        return context
      }
    })
    
    const addServices = async (context) => {
      const { user } = context;
      const services = {
        userAPI: new UserAPI(),
        postAPI: new PostAPI(),
      }
    
      if (user.isAdmin) {
        services.adminAPI = new AdminAPI()
      }
    
      const initializers = [];
      for (const service of Object.values(services)) {
        if (service.initialize) {
          initializers.push(
            service.initialize({
              context,
              cache: null, // or add your own cache
            })
          );
        }
      }
    
      await Promise.all(initializers);
    
      /**
       * this is where you have to deviate from Apollo.
       * You can't use the name "dataSources" in your config because ApolloServer will puke
       * with the error 'Please use the dataSources config option instead of putting dataSources on the context yourself.'
       */
      context.services = services;
    }
    

    Some notes:

    1. You can't call them "dataSources"

    If you return a property called "dataSources" on your context object, Apollo will not like it very much [meaning it throws an Error]. In my example, I used the name "services", but you can do whatever you want... except "dataSources".

    With the above code, in your resolvers, just reference context.services.whatever instead.

    2. This is what Apollo does

    This pattern is copied directly from what Apollo already does for dataSources [source]

    3. I recommend you still treat them as DataSources

    I recommend you stick to the DataSources pattern and that your "services" all extend DataSource. It's going to be easier for everyone involved.

    4. Type safety

    If you're using TypeScript or something, you're going to lose a bit of type safety, since the context.services is either going to be one shape or another. Even if you're not, if you're not careful, you may end up throwing "Cannot read property users of undefined" errors instead of "Unauthorized" errors. You might be better off creating "dummy services" that reflect the same object shape but just throw Unauthorized.