Add didResolveOperation hook to request pipeline API

This commit is contained in:
Martijn Walraven 2018-10-10 15:19:11 +02:00
parent f942af77b2
commit 3fab039c1c
4 changed files with 54 additions and 46 deletions

View file

@ -3,7 +3,6 @@ import {
GraphQLFieldResolver,
specifiedRules,
DocumentNode,
OperationDefinitionNode,
getOperationAST,
ExecutionArgs,
ExecutionResult,
@ -76,15 +75,14 @@ export type DataSources<TContext> = {
[name: string]: DataSource<TContext>;
};
export interface GraphQLRequestPipeline<TContext> {
willExecuteOperation?(operation: OperationDefinitionNode): void;
}
type Mutable<T> = { -readonly [P in keyof T]: T[P] };
export class GraphQLRequestPipeline<TContext> {
plugins: ApolloServerPlugin[];
constructor(private config: GraphQLRequestPipelineConfig<TContext>) {
enableGraphQLExtensions(config.schema);
this.plugins = config.plugins || [];
}
async processRequest(
@ -93,13 +91,11 @@ export class GraphQLRequestPipeline<TContext> {
const config = this.config;
const requestListeners: GraphQLRequestListener<TContext>[] = [];
if (config.plugins) {
for (const plugin of config.plugins) {
if (!plugin.requestDidStart) continue;
const listener = plugin.requestDidStart(requestContext);
if (listener) {
requestListeners.push(listener);
}
for (const plugin of this.plugins) {
if (!plugin.requestDidStart) continue;
const listener = plugin.requestDidStart(requestContext);
if (listener) {
requestListeners.push(listener);
}
}
@ -110,8 +106,6 @@ export class GraphQLRequestPipeline<TContext> {
this.initializeDataSources(requestContext);
await dispatcher.invokeAsync('prepareRequest', requestContext);
const request = requestContext.request;
let { query, extensions } = request;
@ -225,22 +219,25 @@ export class GraphQLRequestPipeline<TContext> {
validationDidEnd();
// FIXME: If we want to guarantee an operation has been set when invoking
// `willExecuteOperation` and executionDidStart`, we need to throw an
// error here and not leave this to `buildExecutionContext` in
// `graphql-js`.
const operation = getOperationAST(document, request.operationName);
// If we don't find an operation, we'll leave it to `buildExecutionContext`
// in `graphql-js` to throw an appropriate error.
if (operation && this.willExecuteOperation) {
this.willExecuteOperation(operation);
}
// FIXME: If we want to guarantee an operation has been set when invoking
// `executionDidStart`, we need to throw an error above and not leave this
// to `buildExecutionContext` in `graphql-js`.
requestContext.operation = operation || undefined;
// We'll set `operationName` to `null` for anonymous operations.
requestContext.operationName =
(operation && operation.name && operation.name.value) || null;
await dispatcher.invokeAsync(
'didResolveOperation',
requestContext as WithRequired<
typeof requestContext,
'document' | 'operation' | 'operationName'
>,
);
const executionDidEnd = await dispatcher.invokeDidStart(
'executionDidStart',
requestContext as WithRequired<

View file

@ -45,7 +45,8 @@ export interface GraphQLRequestContext<TContext = Record<string, any>> {
readonly context: TContext;
readonly cache: KeyValueCache;
document?: DocumentNode;
readonly document?: DocumentNode;
// `operationName` is set based on the operation AST, so it is defined
// even if no `request.operationName` was passed in.
// It will be set to `null` for an anonymous operation.

View file

@ -180,7 +180,6 @@ export async function processHTTPRequest<TContext>(
},
httpRequest: HttpQueryRequest,
): Promise<HttpQueryResponse> {
let isGetRequest: boolean = false;
let requestPayload;
switch (httpRequest.method) {
@ -199,7 +198,6 @@ export async function processHTTPRequest<TContext>(
throw new HttpQueryError(400, 'GET query missing.');
}
isGetRequest = true;
requestPayload = httpRequest.query;
break;
@ -216,6 +214,32 @@ export async function processHTTPRequest<TContext>(
const requestPipeline = new GraphQLRequestPipeline<TContext>(options);
// GET operations should only be queries (not mutations). We want to throw
// a particular HTTP error in that case.
requestPipeline.plugins.push({
requestDidStart() {
return {
didResolveOperation({ request, operation }) {
if (!request.http) return;
if (
request.http.method === 'GET' &&
operation.operation !== 'query'
) {
throw new HttpQueryError(
405,
`GET supports only query operation`,
false,
{
Allow: 'POST',
},
);
}
},
};
},
});
function buildRequestContext(
request: GraphQLRequest,
): GraphQLRequestContext<TContext> {
@ -238,23 +262,6 @@ export async function processHTTPRequest<TContext>(
};
}
// GET operations should only be queries (not mutations). We want to throw
// a particular HTTP error in that case.
if (isGetRequest) {
requestPipeline.willExecuteOperation = operation => {
if (operation.operation !== 'query') {
throw new HttpQueryError(
405,
`GET supports only query operation`,
false,
{
Allow: 'POST',
},
);
}
};
}
const responseInit: ApolloServerHttpResponse = {
headers: {
'Content-Type': 'application/json',

View file

@ -17,15 +17,18 @@ export type WithRequired<T, K extends keyof T> = T & Required<Pick<T, K>>;
export type DidEndHook<TArgs extends any[]> = (...args: TArgs) => void;
export interface GraphQLRequestListener<TContext = Record<string, any>> {
prepareRequest?(
requestContext: GraphQLRequestContext<TContext>,
): ValueOrPromise<void>;
parsingDidStart?(
requestContext: GraphQLRequestContext<TContext>,
): DidEndHook<[Error?]> | void;
validationDidStart?(
requestContext: WithRequired<GraphQLRequestContext<TContext>, 'document'>,
): DidEndHook<[ReadonlyArray<Error>?]> | void;
didResolveOperation?(
requestContext: WithRequired<
GraphQLRequestContext<TContext>,
'document' | 'operationName' | 'operation'
>,
): ValueOrPromise<void>;
executionDidStart?(
requestContext: WithRequired<
GraphQLRequestContext<TContext>,