mirror of
https://github.com/vale981/apollo-server
synced 2025-03-06 10:11:40 -05:00
Add additional lifecycle hooks and allow returning didEnd handlers
This commit is contained in:
parent
5b1760d78a
commit
e40f4a2b9b
3 changed files with 132 additions and 20 deletions
|
@ -38,8 +38,9 @@ import {
|
||||||
ValidationRule,
|
ValidationRule,
|
||||||
} from './requestPipelineAPI';
|
} from './requestPipelineAPI';
|
||||||
import {
|
import {
|
||||||
GraphQLRequestListener,
|
|
||||||
ApolloServerPlugin,
|
ApolloServerPlugin,
|
||||||
|
GraphQLRequestListener,
|
||||||
|
DidEndHook,
|
||||||
} from 'apollo-server-plugin-base';
|
} from 'apollo-server-plugin-base';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
@ -98,17 +99,14 @@ export class GraphQLRequestPipeline<TContext> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const dispatcher = new GraphQLRequestListenerDispatcher(requestListeners);
|
||||||
|
|
||||||
const extensionStack = this.initializeExtensionStack();
|
const extensionStack = this.initializeExtensionStack();
|
||||||
(requestContext.context as any)._extensionStack = extensionStack;
|
(requestContext.context as any)._extensionStack = extensionStack;
|
||||||
|
|
||||||
this.initializeDataSources(requestContext);
|
this.initializeDataSources(requestContext);
|
||||||
|
|
||||||
await Promise.all(
|
await dispatcher.prepareRequest(requestContext);
|
||||||
requestListeners.map(
|
|
||||||
listener =>
|
|
||||||
listener.prepareRequest && listener.prepareRequest(requestContext),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const request = requestContext.request;
|
const request = requestContext.request;
|
||||||
|
|
||||||
|
@ -185,11 +183,15 @@ export class GraphQLRequestPipeline<TContext> {
|
||||||
persistedQueryRegister,
|
persistedQueryRegister,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const parsingDidEnd = await dispatcher.parsingDidStart(requestContext);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let document: DocumentNode;
|
let document: DocumentNode;
|
||||||
try {
|
try {
|
||||||
document = parse(query);
|
document = parse(query);
|
||||||
|
parsingDidEnd();
|
||||||
} catch (syntaxError) {
|
} catch (syntaxError) {
|
||||||
|
parsingDidEnd(syntaxError);
|
||||||
return sendResponse({
|
return sendResponse({
|
||||||
errors: [
|
errors: [
|
||||||
fromGraphQLError(syntaxError, {
|
fromGraphQLError(syntaxError, {
|
||||||
|
@ -199,9 +201,14 @@ export class GraphQLRequestPipeline<TContext> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const validationDidEnd = await dispatcher.validationDidStart(
|
||||||
|
requestContext,
|
||||||
|
);
|
||||||
|
|
||||||
const validationErrors = validate(document);
|
const validationErrors = validate(document);
|
||||||
|
|
||||||
if (validationErrors.length > 0) {
|
if (validationErrors.length > 0) {
|
||||||
|
validationDidEnd(validationErrors);
|
||||||
return sendResponse({
|
return sendResponse({
|
||||||
errors: validationErrors.map(validationError =>
|
errors: validationErrors.map(validationError =>
|
||||||
fromGraphQLError(validationError, {
|
fromGraphQLError(validationError, {
|
||||||
|
@ -211,6 +218,8 @@ export class GraphQLRequestPipeline<TContext> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validationDidEnd();
|
||||||
|
|
||||||
const operation = getOperationAST(document, request.operationName);
|
const operation = getOperationAST(document, request.operationName);
|
||||||
|
|
||||||
// If we don't find an operation, we'll leave it to `buildExecutionContext`
|
// 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
|
// 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
|
// `executionDidStart`, we need to throw an error above and not leave this
|
||||||
// to `buildExecutionContext` in `graphql-js`.
|
// to `buildExecutionContext` in `graphql-js`.
|
||||||
requestContext.operation = operation as OperationDefinitionNode;
|
requestContext.operation = operation || undefined;
|
||||||
|
requestContext.operationName =
|
||||||
|
(operation && operation.name && operation.name.value) || '';
|
||||||
|
|
||||||
requestListeners.forEach(
|
const executionDidEnd = await dispatcher.executionDidStart(
|
||||||
listener =>
|
requestContext,
|
||||||
listener.executionDidStart &&
|
|
||||||
listener.executionDidStart(requestContext),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let response: GraphQLResponse;
|
let response: GraphQLResponse;
|
||||||
|
@ -238,7 +247,9 @@ export class GraphQLRequestPipeline<TContext> {
|
||||||
request.operationName,
|
request.operationName,
|
||||||
request.variables,
|
request.variables,
|
||||||
)) as GraphQLResponse;
|
)) as GraphQLResponse;
|
||||||
|
executionDidEnd();
|
||||||
} catch (executionError) {
|
} catch (executionError) {
|
||||||
|
executionDidEnd(executionError);
|
||||||
return sendResponse({
|
return sendResponse({
|
||||||
errors: [fromGraphQLError(executionError)],
|
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,
|
// We override errors, data, and extensions with the passed in response,
|
||||||
// but keep other properties (like http)
|
// but keep other properties (like http)
|
||||||
return (requestContext.response = extensionStack.willSendResponse({
|
requestContext.response = extensionStack.willSendResponse({
|
||||||
graphqlResponse: {
|
graphqlResponse: {
|
||||||
...requestContext.response,
|
...requestContext.response,
|
||||||
errors: response.errors,
|
errors: response.errors,
|
||||||
data: response.data,
|
data: response.data,
|
||||||
extensions: response.extensions,
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {
|
||||||
ASTVisitor,
|
ASTVisitor,
|
||||||
GraphQLError,
|
GraphQLError,
|
||||||
OperationDefinitionNode,
|
OperationDefinitionNode,
|
||||||
|
DocumentNode,
|
||||||
} from 'graphql';
|
} from 'graphql';
|
||||||
import { KeyValueCache } from 'apollo-server-caching';
|
import { KeyValueCache } from 'apollo-server-caching';
|
||||||
|
|
||||||
|
@ -33,7 +34,7 @@ export interface GraphQLRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GraphQLResponse {
|
export interface GraphQLResponse {
|
||||||
data?: object;
|
data?: Record<string, any>;
|
||||||
errors?: GraphQLError[];
|
errors?: GraphQLError[];
|
||||||
extensions?: Record<string, any>;
|
extensions?: Record<string, any>;
|
||||||
http?: {
|
http?: {
|
||||||
|
@ -48,6 +49,10 @@ export interface GraphQLRequestContext<TContext> {
|
||||||
context: TContext;
|
context: TContext;
|
||||||
cache: KeyValueCache;
|
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;
|
operation?: OperationDefinitionNode;
|
||||||
|
|
||||||
debug?: boolean;
|
debug?: boolean;
|
||||||
|
|
|
@ -3,18 +3,38 @@ import {
|
||||||
GraphQLRequestContext,
|
GraphQLRequestContext,
|
||||||
} from 'apollo-server-core/dist/requestPipelineAPI';
|
} from 'apollo-server-core/dist/requestPipelineAPI';
|
||||||
|
|
||||||
|
type ValueOrPromise<T> = T | Promise<T>;
|
||||||
|
|
||||||
export abstract class ApolloServerPlugin {
|
export abstract class ApolloServerPlugin {
|
||||||
async serverWillStart?(service: GraphQLServiceContext): Promise<void>;
|
serverWillStart?(service: GraphQLServiceContext): ValueOrPromise<void>;
|
||||||
requestDidStart?<TContext>(
|
requestDidStart?<TContext>(
|
||||||
requestContext: GraphQLRequestContext<TContext>,
|
requestContext: GraphQLRequestContext<TContext>,
|
||||||
): GraphQLRequestListener<TContext> | void;
|
): 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> {
|
export interface GraphQLRequestListener<TContext> {
|
||||||
prepareRequest?(
|
prepareRequest?(
|
||||||
requestContext: GraphQLRequestContext<TContext>,
|
requestContext: GraphQLRequestContext<TContext>,
|
||||||
): Promise<void>;
|
): ValueOrPromise<void>;
|
||||||
executionDidStart?(requestContext: GraphQLRequestContext<TContext>): 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>;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue