Apollo-server: Dynamic resolvers based on express request

Created on 11 Apr 2019  路  4Comments  路  Source: apollographql/apollo-server

In version 1 of apollo-server-express, the way to pass the schema and resolver set was to return the information in the graphqlExpress util, like:

graphqlExpress((req?: Request) => {
    // Create schema based on request
    ...

    return {
        context,
        formatError: (error: GraphQLError) => ...,
        schema,
    }
})

With this setup, it used to be easy to create dynamic resolver sets, based on the context of the express request.

In version 2 of apollo-server-express, schema's are provided when creating a new ApolloServer, before the server is actually running. Because of this change, we loose the ability to have this dynamic resolver setup.

How to accomplish this in version 2 (apart from shifting the logic to the resolvers itself) or why was support dropped for this?

Most helpful comment

To get the v1 functionality, you need to create a custom class YourServer extends ApolloServer and override one method applyMiddleware. You can also choose to modify the root context if needed.

You are basically skipping the server's this.schema that is saved on startup, like v1.

You have to do similar for subscriptions, since those requests take a different path.

Note the graphqlExpress import at top.

import { renderPlaygroundPage } from '@apollographql/graphql-playground-html'
import { ApolloServer, makeExecutableSchema } from 'apollo-server-express'
import { graphqlExpress } from 'apollo-server-express/dist/expressApollo'


class DynamicServer extends ApolloServer {
  applyMiddleware({ app, path }) {
    app.use(path, async (req, res, next) => {
      if (ENABLE_PLAYGROUND) {
        // copy playground code from apollo applyMiddleware source
      }

      const customSchema = getYourCustomOrCachedSchemaBasedOnRequest()

     // this.context is the current server / request context, you can keep or override below
      const contextObj = { ...this.context, request: req } // ...other context etc

      const gqlOptions = {
        ...this,
        graphqlPath: path,
        schema: await makeExecutableSchema(customSchema),
        context: contextObj,
      }
      return graphqlExpress(super.createGraphQLServerOptions.bind(gqlOptions))(
        req,
        res,
        next
      )
    })
  }

All 4 comments

To get the v1 functionality, you need to create a custom class YourServer extends ApolloServer and override one method applyMiddleware. You can also choose to modify the root context if needed.

You are basically skipping the server's this.schema that is saved on startup, like v1.

You have to do similar for subscriptions, since those requests take a different path.

Note the graphqlExpress import at top.

import { renderPlaygroundPage } from '@apollographql/graphql-playground-html'
import { ApolloServer, makeExecutableSchema } from 'apollo-server-express'
import { graphqlExpress } from 'apollo-server-express/dist/expressApollo'


class DynamicServer extends ApolloServer {
  applyMiddleware({ app, path }) {
    app.use(path, async (req, res, next) => {
      if (ENABLE_PLAYGROUND) {
        // copy playground code from apollo applyMiddleware source
      }

      const customSchema = getYourCustomOrCachedSchemaBasedOnRequest()

     // this.context is the current server / request context, you can keep or override below
      const contextObj = { ...this.context, request: req } // ...other context etc

      const gqlOptions = {
        ...this,
        graphqlPath: path,
        schema: await makeExecutableSchema(customSchema),
        context: contextObj,
      }
      return graphqlExpress(super.createGraphQLServerOptions.bind(gqlOptions))(
        req,
        res,
        next
      )
    })
  }

I'm going to close this in lieu of the above suggestion. It's also worth noting that, using the Express integration (i.e. apollo-server-express), it's now possible to construct multiple Apollo Server instances and use the getMiddleware method on each to get the appropriate middleware and bind it depending on route specific context (or other properties).

@abernix Based on your last statement, are you saying that JIT schema generation and resolver mapping is supported by apollo-server-express out of the box?

Hello everybody.
I am currently trying to implement a dynamic schema, based on the token in the request.

I tried to follow the described solution above, but I realised that the getMiddleware function is applying several middlewares and I didn't want to just stupidly copy and paste that code.
So I now tried to just override the the one middleware that actually inits the GraphqlExpress Server.

It runs now, but the newly generated schema isn't picked up, but instead the default schema that I pass in when I init the server is picked up:
new DynamicServer({schema: defaultSchema})

So it seems like my newly generated schema can't override the initial one (which I have to pass in, otherwise it throws an error)

Does maybe someone know what I am missing here?

class DynamicServer extends ApolloServer {
  applyMiddleware({ app, path, ...rest }) {
    const router = this.getMiddleware({ path, rest });

    // delete middleware that initializes the default apollo server
    router.stack.pop();

    // init dynamic schema server
    router.use(path, async (req, res, next) => {
      if (this.playgroundOptions && req.method === "GET") {
        // here I copy and pasted the code from the ApolloServer source code
      }
      const token = req.headers.authorization;
     let userId = null;
      try {
        userId = await verifyToken(token);
      } catch (error) {
        throw new AuthenticationError("Authentication Token required.");
      }


      /* Not necessary, but removing to ensure schema built on the request */
      const { schema, ...serverObj } = this;

      const contextObj = {
        ...serverObj.context,
        request: req,
        me: userId,
      };
      const newSchema = await generateSchema(userId);

      const graphqlOptions = {
        ...serverObj,
        graphqlPath: path,
        schema: newSchema,
        context: contextObj
      };

      return graphqlExpress(
        super.createGraphQLServerOptions.bind(graphqlOptions)
      )(req, res, next);
    });

    // Finally applying all middleware
    app.use(router);
  }
}

const dynamicAdminServer = new DynamicServer({
  /* adding a default graphql schema initially */
  typeDefs: `type Query { _: Boolean }`
});
dynamicAdminServer.applyMiddleware({ app, path: "/test" });

Was this page helpful?
0 / 5 - 0 ratings