Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Variables dependency incompatible with Apollo Server 2 #12

Open
steverice opened this issue Jun 18, 2018 · 13 comments
Open

Variables dependency incompatible with Apollo Server 2 #12

steverice opened this issue Jun 18, 2018 · 13 comments

Comments

@steverice
Copy link

Currently, the validation rule requires providing the variables object from the request because

This is needed because the variables are not available in the visitor of the graphql-js library.

Unfortunately, version 2 of Apollo Server no longer allows dynamic configuration of all of its server options per-request, but rather only the context. validationRules must be provided when the server is initialized and not within the context of the middleware. Therefore, all that each validation rule has access to is the ValidationContext and there's no built-in way to inject the request variables.

I am reasonably certain that this is an intentional change.

It seems possible to remove graphql-cost-analysis's dependence on the full variables object, given that the ValidationContext provides information about variable usages and also about field arguments.

Is this feasible?

@evans
Copy link

evans commented Jun 20, 2018

An option is to open up the array of functions that create the extensions to receive the same argument as the context creation. Here are the type definitions https://github.com/apollographql/apollo-server/blob/version-2/packages/apollo-server-core/src/types.ts#L56

@pobed2
Copy link

pobed2 commented Jul 26, 2018

@pa-bru Any thoughts on what @evans and @steverice are suggesting?

@smucciArchitech
Copy link

Is there any update with this issue ?

@arianon
Copy link

arianon commented Sep 13, 2018

@smucciArchitech

This is the workaround I'm currently using.

// XXX: Evil hack to use graphql-cost-analysis
class EnhancedServer extends ApolloServer {
  async createGraphQLServerOptions(
    req: express.Request,
    res: express.Response
  ): Promise<GraphQLOptions> {
    const options = await super.createGraphQLServerOptions(req, res);

    return {
      ...options,
      validationRules: [
        ...options.validationRules,
        costAnalysis({variables: req.body.variables})
      ]
    };
  }
}```

@wKovacs64
Copy link

Another workaround is to provide dummy/mock variable values (as suggested by @zionts in Slack) like so:

const apolloServer = new ApolloServer({
  validationRules: [
    costAnalysis({
      variables: {
        someQueryVarInYourSchema: 42,
        someOtherQueryVarInYourSchema: false,
      },
    }),
  ],
});

It's brittle and doesn't scale well with large schemas, of course, but it works.

@mxstbr
Copy link
Contributor

mxstbr commented Oct 3, 2018

@arianon thank you so much, the workaround worked perfectly! 💯 withspectrum/spectrum#3855

@Kosta-Github
Copy link

@arianon I had to change it to this:

class CostAnalysisApolloServer extends ApolloServer {
  async createGraphQLServerOptions(req, res) {
    const options = await super.createGraphQLServerOptions(req, res);

    options.validationRules = options.validationRules || [];
    options.validationRules.push(graphqlCostAnalysis({
      variables:    req.body.variables,
      maximumCost:  1234,
      defaultCost:  1,
      onComplete:   (costs) => console.log(`costs: ${costs} (max: 1234)`)
    }));

    return options;
  }
}

validationRules is now (apollo-server 2.4.x) an array for me...

@P4sca1
Copy link

P4sca1 commented Apr 30, 2019

How about refactoring the code base to work as an apollo plugin?

new ApolloServer({
  plugins: [
    {
      requestDidStart: () => {
        return {
          // This is called after the validationRules.
          didResolveOperation: requestContext => {
            // requestContext.operations
            // requestContext.request.variables
            // Do cost analysis here.
          },
        }
      },
    },
  ],
})

The only problem I see is how to access the schema. This is currently done via validationContext.getSchema().

@pa-bru Do you have an idea?

@abernix
Copy link

abernix commented Oct 23, 2019

Just passing through here, but thought I'd leave a note!

It's true that validation rules cannot be dynamically changed for each request anymore in Apollo Server 2.x. However, the schema is available to the Apollo Server plugins via the serverWillStart hook (cc @P4sca1), though it's very much worth pointing out that when using Apollo Federation, there is not currently a schemaDidChange hook that will allow it to be updated. There is an open PR (apollographql/apollo-server#2974) to address that, but it will likely not land in this form. If there is anyone specifically interested in Apollo Federation + use of such a hook, there are some other options, but currently they need more explanation than I'll be able to leave here right now. Please upvote https://github.com/apollographql/apollo-server/issues/2972 if that's important to you!

Also, if the intention is to run after validation, I would recommend using the appropriate hook rather than incorrectly leveraging didResolveOperation. While that does run after validation, there is a specific hook that runs after validation. It's certainly possible that it's not clear from the documentation at the moment (sorry!), but such a method can be defined by returning a function from the validationDidStart hook (it's slightly more clear when looking at the plugin type interfaces. Here's a rough example someone might want to explore:

const examplePlugin = () => {
  let schema;
  return {
    serverWillStart({ schema: schemaAtStartup }) {
      // We'll save a reference to the schema.
      schema = schemaAtStartup;
    },
    requestDidStart() {
      return {
        validationDidStart() {
          return function validationDidEnd() {
			console.log("The schema was", schema);
            throw new ForbiddenError("Too complex?");
          }
        },
      }
    },
  }
};

const server = new ApolloServer({
  typeDefs,
  resolvers,
  plugins: [examplePlugin],
});

Hope this helps!

@tot-ra
Copy link

tot-ra commented Jan 7, 2020

How do you handle array of operations? req.body.variables is not intended to work with:

[{"operationName":"getXxx","variables":{"id":14},"query":"query getXxx($id: Int!) {...."}]

@mwinstanley
Copy link

FYI: I added the graphql-cost-analysis to my ApolloServer and found that because the Apollo GraphQL server options contain a reference to the server's validationRules, and not a copy, it is incorrect to push the cost analyzer directly onto the validationRules. Instead, you should make a copy of the rules. Otherwise you'll end up with an increasing number of duplicates in the rules array.

The updated code to add the validation rule looks like this:

class CostAnalysisApolloServer extends ApolloServer {
  async createGraphQLServerOptions(req, res) {
    const options = await super.createGraphQLServerOptions(req, res);

    options.validationRules = options.validationRules ? options.validationRules.slice() : [];
    options.validationRules.push(graphqlCostAnalysis({
      variables:    req.body.variables,
      maximumCost:  1234,
      defaultCost:  1,
      onComplete:   (costs) => console.log(`costs: ${costs} (max: 1234)`)
    }));

    return options;
  }
}

@Xetera
Copy link

Xetera commented Jun 22, 2020

If you're using apollo-server-micro or nextjs, you need to turn off body parsing on your graphql endpoint which breaks the workaround code posted above because you don't have access to req.body.variables, so you have to manually parse the body in this case.

import { json } from "micro";

class CostAnalysisApolloServer extends ApolloServer {
  async createGraphQLServerOptions(req, res) {
    const options = await super.createGraphQLServerOptions(req, res);
    // parse the body
    const { variables } = await json(req)

    options.validationRules = options.validationRules ? options.validationRules.slice() : [];
    options.validationRules.push(graphqlCostAnalysis({
      variables,
      maximumCost:  1234,
      defaultCost:  1,
      onComplete:   (costs) => console.log(`costs: ${costs} (max: 1234)`)
    }));

    return options;
  }
}

@nxAviral
Copy link

nxAviral commented Dec 24, 2021

@P4sca1 that's a great solution however I am struggling to make it work. a little help will be appreciated

const costAnalyzer = request => costAnalysis({
variables: request.variables,
maximumCost: 1000,
defaultCost: 1,
onComplete: cost => console.log('Determined query cost: ', cost),
createError: (maximumCost, cost) => new ApolloError(
Query execution cost limit exceeded. Maximum allowed: ${maximumCost}, received ${cost},
),
})

this.server = new ApolloServer({
...schema,
playground,
introspection,
debug,
formatError,
dataSources: () => ({
servicesexample
}),
context,
plugins: [
{
requestDidStart: () => ({
didResolveOperation({ request }) {
costAnalyzer(request);
},
}),
},
],
});
this isn't working.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests