Apollo Server: How to access 'context' outside of resolvers in Dataloader from REST API Datasource

4.4k views Asked by At

hopefully someone can help me with this little problem, I just cannot figure it out right now.

Problem Statement:

I want to access 'context' for the sake of authentication in my DataLoader. This DataLoaderis defined in a seperate path /loaders. In my resolvers.js file I can access my context nicely with dataSources.userAPI.getAllUsers(). But how to access it anywhere else in my serverside application, like f.e. in my /loaders folder? I just cant get it how to get access to my context object to then pass the token to the DataLoader to then load the data from my API and then pass this data to my resolvers.js file. Every help is highly appreciated, I don't know how to solve this simple thing .. Thanks!

Here comes the code:

index.js

const express = require('express');
const connectDB = require('./config/db');
const path = require('path');
var app = express();
const cors = require('cors')
const axios = require('axios')

// apollo graphql
const { ApolloServer } = require('apollo-server-express');
const DataLoader = require('dataloader')
const { userDataLoader } = require('./loaders/index')

// Connect Database
connectDB();

// gql import
const typeDefs = require('./schema');
const resolvers = require('./resolvers')

// apis
const UserAPI = require('./datasources/user')


// datasources
const dataSources = () => ({
    userAPI: new UserAPI(),
});

// context
const context = ({ req, res }) => ({

    token: req.headers.authorization || null,
    loaders: {
        userLoader: userDataLoader,
    },
    res
})


// init server
const server = new ApolloServer({
    typeDefs,
    resolvers,
    dataSources,
    context
});

// middleware
app.use(express.json());


// cors
var corsOptions = {
    credentials: true
}
app.use(cors(corsOptions))


// serve middleware
server.applyMiddleware({
    app
});


// run server
app.listen({ port: 4000 }, () =>
    console.log(`Server ready at http://localhost:4000${server.graphqlPath}`)
);

module.exports = {
    dataSources,
    context,
    typeDefs,
    resolvers,
   loaders,
    ApolloServer,
    UserAPI,
    server,
};

loaders/index.js

   const userDataLoader = require('./user')

module.exports = {
    userDataLoader
}

loaders/user.js

const UserAPI = require('../datasources/users')
// init loader
const userDataLoader = new DataLoader(keys => batchUser(keys))

// batch
const batchUsers = async (keys) => {

   // this part is not working!
   // How to access the UserAPI methods in my DataLoader?
   // Or lets say: How to access context from here,
   // so I can add auth for the server I am requesting data from?

    const userAPI = new UserAPI()
    const users = userAPI.getAllUsers()
        .then(res => {
            return res.data
        })


    return keys.map(userId => users.find(user=> user._id === userId))
}

module.exports = userDataLoader

resolvers.js

// here is just my api call to get the data from my
// dataloader with userLoader.load() and this works perfectly
// if I just make API calls with axios in my loaders/user
// here just a little snippet from the resolver file

....
users: async (parent, args, { loaders }) => {
            const { userLoader } = loaders
            if (!parent.users) {
                return null;
            }
            return await userLoader.load(parent.user)
        },
....

datasources/user.js

const { RESTDataSource } = require('apollo-datasource-rest');

class UserAPI extends RESTDataSource {
    constructor() {
        super()
        this.baseURL = 'http://mybaseurl.com/api'
    }


    willSendRequest(request) {
        request.headers.set('Authorization',
            this.context.token
        );
    }

    async getUserById(id) {
        return this.get(`/users/${id}`)
    }

    async getAllUsers() {
        const data = await this.get('/users');
        return data;
    }
}

module.exports = UserAPI;
1

There are 1 answers

1
Herku On BEST ANSWER

I recommend to create a function that creates data loaders and provides the needed state inside of a closure and not use data sources at all:

module.exports.createDataloaders = function createDataLoaders(options) {
  const batchUsers = ids => {
    const users = await fetch('/users/', { headers: { Authorization: options.auth } });
    // ...
  }

  return {
    userLoader: new Dataloader(batchUsers);
  };
}

// now in index.js
// context
const context = ({ req, res }) => ({
    token: req.headers.authorization || null,
    loaders: createDataloaders({ auth: req.headers.authorization || null }),
    res
})


// init server
const server = new ApolloServer({
    typeDefs,
    resolvers,
    context
});

Or consider flipping your layers around:

const { RESTDataSource } = require('apollo-datasource-rest');

class UserAPI extends RESTDataSource {
    constructor() {
        super()
        this.baseURL = 'http://mybaseurl.com/api'
        this.dataloader = new Dataloader(ids => {
          // use this.get here
        });
    }


    willSendRequest(request) {
        request.headers.set('Authorization',
            this.context.token
        );
    }

    async getUserById(id) {
        return this.dataloader.load(id);
    }

    async getAllUsers() {
        const data = await this.get('/users');
        return data;
    }
}

module.exports = UserAPI;

But Datasources are not designed to be used with Dataloader as explained here in the Apollo Docs itself. So if you want to keep the sources maybe get rid of the loaders alltogether.

Basically what they are saying is that GraphQL APIs that wrap rest APIs benefit more from (global) request caching than from pre request data loaders. Caching across requests can lead to authorization issues though, so be careful.