Variables dependency incompatible with Apollo Server 2
steverice opened this issue ยท 13 comments
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?
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
@pa-bru Any thoughts on what @evans and @steverice are suggesting?
Is there any update with this issue ?
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})
]
};
}
}```
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.
@arianon thank you so much, the workaround worked perfectly! ๐ฏ withspectrum/spectrum#3855
@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...
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?
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!
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!) {...."}]
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;
}
}
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;
}
}
@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.