Add additional lifecycle hooks and allow returning didEnd handlers

This commit is contained in:
Martijn Walraven 2018-10-09 17:29:21 +02:00
parent 5b1760d78a
commit e40f4a2b9b
3 changed files with 132 additions and 20 deletions

View file

@ -38,8 +38,9 @@ import {
ValidationRule,
} from './requestPipelineAPI';
import {
GraphQLRequestListener,
ApolloServerPlugin,
GraphQLRequestListener,
DidEndHook,
} from 'apollo-server-plugin-base';
export {
@ -98,17 +99,14 @@ export class GraphQLRequestPipeline<TContext> {
}
}
const dispatcher = new GraphQLRequestListenerDispatcher(requestListeners);
const extensionStack = this.initializeExtensionStack();
(requestContext.context as any)._extensionStack = extensionStack;
this.initializeDataSources(requestContext);
await Promise.all(
requestListeners.map(
listener =>
listener.prepareRequest && listener.prepareRequest(requestContext),
),
);
await dispatcher.prepareRequest(requestContext);
const request = requestContext.request;
@ -185,11 +183,15 @@ export class GraphQLRequestPipeline<TContext> {
persistedQueryRegister,
});
const parsingDidEnd = await dispatcher.parsingDidStart(requestContext);
try {
let document: DocumentNode;
try {
document = parse(query);
parsingDidEnd();
} catch (syntaxError) {
parsingDidEnd(syntaxError);
return sendResponse({
errors: [
fromGraphQLError(syntaxError, {
@ -199,9 +201,14 @@ export class GraphQLRequestPipeline<TContext> {
});
}
const validationDidEnd = await dispatcher.validationDidStart(
requestContext,
);
const validationErrors = validate(document);
if (validationErrors.length > 0) {
validationDidEnd(validationErrors);
return sendResponse({
errors: validationErrors.map(validationError =>
fromGraphQLError(validationError, {
@ -211,6 +218,8 @@ export class GraphQLRequestPipeline<TContext> {
});
}
validationDidEnd();
const operation = getOperationAST(document, request.operationName);
// If we don't find an operation, we'll leave it to `buildExecutionContext`
@ -222,12 +231,12 @@ export class GraphQLRequestPipeline<TContext> {
// 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 as OperationDefinitionNode;
requestContext.operation = operation || undefined;
requestContext.operationName =
(operation && operation.name && operation.name.value) || '';
requestListeners.forEach(
listener =>
listener.executionDidStart &&
listener.executionDidStart(requestContext),
const executionDidEnd = await dispatcher.executionDidStart(
requestContext,
);
let response: GraphQLResponse;
@ -238,7 +247,9 @@ export class GraphQLRequestPipeline<TContext> {
request.operationName,
request.variables,
)) as GraphQLResponse;
executionDidEnd();
} catch (executionError) {
executionDidEnd(executionError);
return sendResponse({
errors: [fromGraphQLError(executionError)],
});
@ -316,17 +327,21 @@ export class GraphQLRequestPipeline<TContext> {
}
}
function sendResponse(response: GraphQLResponse): GraphQLResponse {
async function sendResponse(
response: GraphQLResponse,
): Promise<GraphQLResponse> {
// We override errors, data, and extensions with the passed in response,
// but keep other properties (like http)
return (requestContext.response = extensionStack.willSendResponse({
requestContext.response = extensionStack.willSendResponse({
graphqlResponse: {
...requestContext.response,
errors: response.errors,
data: response.data,
extensions: response.extensions,
},
}).graphqlResponse);
}).graphqlResponse;
await dispatcher.willSendResponse(requestContext);
return requestContext.response!;
}
}
@ -379,3 +394,75 @@ export class GraphQLRequestPipeline<TContext> {
}
}
}
type FunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? K : never
}[keyof T];
class Dispatcher<T> {
constructor(protected targets: T[]) {}
protected async invokeAsync(
methodName: FunctionPropertyNames<Required<T>>,
...args: any[]
) {
await Promise.all(
this.targets.map(target => {
const method = target[methodName];
if (method && typeof method === 'function') {
return method(...args);
}
}),
);
}
protected invokeDidStart<TArgs extends any[]>(
methodName: FunctionPropertyNames<Required<T>>,
...args: any[]
): DidEndHook<TArgs> {
const didEndHooks: DidEndHook<TArgs>[] = [];
for (const target of this.targets) {
const method = target[methodName];
if (method && typeof method === 'function') {
const didEndHook = method(...args);
if (didEndHook) {
didEndHooks.push(didEndHook);
}
}
}
return (args: TArgs) => {
didEndHooks.reverse();
for (const didEndHook of didEndHooks) {
didEndHook(args);
}
};
}
}
// FIXME: Properly type the lifecycle hooks in the dispatcher
class GraphQLRequestListenerDispatcher<TContext>
extends Dispatcher<GraphQLRequestListener<TContext>>
implements Required<GraphQLRequestListener<TContext>> {
async prepareRequest(...args: any[]) {
return this.invokeAsync('prepareRequest', ...args);
}
parsingDidStart(...args: any[]): any {
return this.invokeDidStart('parsingDidStart', ...args);
}
validationDidStart(...args: any[]): any {
return this.invokeDidStart('validationDidStart', ...args);
}
executionDidStart(...args: any[]): any {
return this.invokeDidStart('executionDidStart', ...args);
}
async willSendResponse(...args: any[]) {
return this.invokeAsync('willSendResponse', ...args);
}
}

View file

@ -9,6 +9,7 @@ import {
ASTVisitor,
GraphQLError,
OperationDefinitionNode,
DocumentNode,
} from 'graphql';
import { KeyValueCache } from 'apollo-server-caching';
@ -33,7 +34,7 @@ export interface GraphQLRequest {
}
export interface GraphQLResponse {
data?: object;
data?: Record<string, any>;
errors?: GraphQLError[];
extensions?: Record<string, any>;
http?: {
@ -48,6 +49,10 @@ export interface GraphQLRequestContext<TContext> {
context: TContext;
cache: KeyValueCache;
document?: DocumentNode;
// operationName is set based on the selected operation, so it is defined
// even if no request.operationName was passed in.
operationName?: string;
operation?: OperationDefinitionNode;
debug?: boolean;

View file

@ -3,18 +3,38 @@ import {
GraphQLRequestContext,
} from 'apollo-server-core/dist/requestPipelineAPI';
type ValueOrPromise<T> = T | Promise<T>;
export abstract class ApolloServerPlugin {
async serverWillStart?(service: GraphQLServiceContext): Promise<void>;
serverWillStart?(service: GraphQLServiceContext): ValueOrPromise<void>;
requestDidStart?<TContext>(
requestContext: GraphQLRequestContext<TContext>,
): GraphQLRequestListener<TContext> | void;
}
// type WithRequired<T, K extends keyof T> = T & Required<Pick<T, K>>;
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> {
prepareRequest?(
requestContext: GraphQLRequestContext<TContext>,
): Promise<void>;
executionDidStart?(requestContext: GraphQLRequestContext<TContext>): void;
): ValueOrPromise<void>;
parsingDidStart?(
requestContext: GraphQLRequestContext<TContext>,
): DidEndHook<Error> | void;
validationDidStart?(
requestContext: WithRequired<GraphQLRequestContext<TContext>, 'document'>,
): DidEndHook<Error[]> | void;
executionDidStart?(
requestContext: WithRequired<
GraphQLRequestContext<TContext>,
'document' | 'operationName' | 'operation'
>,
): DidEndHook<Error> | void;
willSendResponse?(
requestContext: WithRequired<
GraphQLRequestContext<TContext>,
'document' | 'operationName' | 'operation' | 'response'
>,
): ValueOrPromise<void>;
}