Refactor runQuery into GraphQLRequestProcessor

This commit is contained in:
Martijn Walraven 2018-09-04 20:07:09 +02:00 committed by Jesse Rosenberger
parent 38e7b6a5b6
commit a09d514e59
No known key found for this signature in database
GPG key ID: C0CCCF81AA6C08D8
6 changed files with 314 additions and 60 deletions

View file

@ -215,17 +215,16 @@ export class ApolloServerBase {
// or cacheControl.
this.extensions = [];
const debugDefault =
process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test';
const debug =
requestOptions.debug !== undefined ? requestOptions.debug : debugDefault;
// Error formatting should happen after the engine reporting agent, so that
// engine gets the unmasked errors if necessary
if (this.requestOptions.formatError) {
this.extensions.push(
() =>
new FormatErrorExtension(
this.requestOptions.formatError!,
this.requestOptions.debug,
),
() => new FormatErrorExtension(requestOptions.formatError, debug),
);
}
if (engine || (engine !== false && process.env.ENGINE_API_KEY)) {
this.engineReportingAgent = new EngineReportingAgent(

View file

@ -2,10 +2,10 @@ import { GraphQLExtension, GraphQLResponse } from 'graphql-extensions';
import { formatApolloErrors } from 'apollo-server-errors';
export class FormatErrorExtension extends GraphQLExtension {
private formatError: Function;
private formatError?: Function;
private debug: boolean;
public constructor(formatError: Function, debug: boolean = false) {
public constructor(formatError?: Function, debug: boolean = false) {
super();
this.formatError = formatError;
this.debug = debug;

View file

@ -0,0 +1,250 @@
import { Request } from 'apollo-server-env';
import {
GraphQLSchema,
GraphQLFieldResolver,
ValidationContext,
ASTVisitor,
DocumentNode,
parse,
specifiedRules,
validate,
GraphQLError,
ExecutionArgs,
ExecutionResult,
execute,
getOperationAST,
OperationDefinitionNode,
} from 'graphql';
import {
GraphQLExtension,
GraphQLExtensionStack,
enableGraphQLExtensions,
} from 'graphql-extensions';
import { PersistedQueryOptions } from './';
import {
CacheControlExtension,
CacheControlExtensionOptions,
} from 'apollo-cache-control';
import { TracingExtension } from 'apollo-tracing';
import {
fromGraphQLError,
SyntaxError,
ValidationError,
} from 'apollo-server-errors';
export interface GraphQLRequest {
query?: string;
operationName?: string;
variables?: { [name: string]: any };
extensions?: object;
httpRequest?: Pick<Request, 'url' | 'method' | 'headers'>;
}
export interface GraphQLRequestOptions<TContext = any> {
schema: GraphQLSchema;
rootValue?: any;
context: TContext;
validationRules?: ValidationRule[];
fieldResolver?: GraphQLFieldResolver<any, TContext>;
debug?: boolean;
extensions?: Array<() => GraphQLExtension>;
tracing?: boolean;
persistedQueries?: PersistedQueryOptions;
cacheControl?: CacheControlExtensionOptions;
formatResponse?: Function;
}
export type ValidationRule = (context: ValidationContext) => ASTVisitor;
export class InvalidGraphQLRequestError extends Error {}
export interface GraphQLResponse {
data?: object;
errors?: GraphQLError[];
extensions?: object;
}
export interface GraphQLRequestProcessor {
willExecuteOperation?(operation: OperationDefinitionNode): void;
}
export class GraphQLRequestProcessor {
extensionStack!: GraphQLExtensionStack;
constructor(public options: GraphQLRequestOptions) {
this.initializeExtensions();
}
initializeExtensions() {
// If custom extension factories were provided, create per-request extension
// objects.
const extensions = this.options.extensions
? this.options.extensions.map(f => f())
: [];
// If you're running behind an engineproxy, set these options to turn on
// tracing and cache-control extensions.
if (this.options.tracing) {
extensions.push(new TracingExtension());
}
if (this.options.cacheControl === true) {
extensions.push(new CacheControlExtension());
} else if (this.options.cacheControl) {
extensions.push(new CacheControlExtension(this.options.cacheControl));
}
this.extensionStack = new GraphQLExtensionStack(extensions);
// We unconditionally create an extensionStack, even if there are no
// extensions (so that we don't have to litter the rest of this function with
// `if (extensionStack)`, but we don't instrument the schema unless there
// actually are extensions. We do unconditionally put the stack on the
// context, because if some other call had extensions and the schema is
// already instrumented, that's the only way to get a custom fieldResolver to
// work.
if (extensions.length > 0) {
enableGraphQLExtensions(this.options.schema);
}
this.options.context._extensionStack = this.extensionStack;
}
async processRequest(request: GraphQLRequest): Promise<GraphQLResponse> {
if (!request.query) {
throw new InvalidGraphQLRequestError();
}
const requestDidEnd = this.extensionStack.requestDidStart({
request: request.httpRequest!,
queryString: request.query,
operationName: request.operationName,
variables: request.variables,
persistedQueryHit: false,
persistedQueryRegister: false,
});
try {
let document: DocumentNode;
try {
document = this.parse(request.query);
} catch (syntaxError) {
return this.willSendResponse({
errors: [
fromGraphQLError(syntaxError, {
errorClass: SyntaxError,
}),
],
});
}
const validationErrors = this.validate(document);
if (validationErrors.length > 0) {
return this.willSendResponse({
errors: validationErrors.map(validationError =>
fromGraphQLError(validationError, {
errorClass: ValidationError,
}),
),
});
}
const operation = getOperationAST(document, request.operationName);
// If we don't find an operation, we'll leave it to `buildExecutionContext`
// to throw an appropriate error.
if (operation && this.willExecuteOperation) {
this.willExecuteOperation(operation);
}
let response: GraphQLResponse;
try {
response = (await this.execute(
document,
request.operationName,
request.variables,
)) as GraphQLResponse;
} catch (executionError) {
return this.willSendResponse({
errors: [fromGraphQLError(executionError)],
});
}
const formattedExtensions = this.extensionStack.format();
if (Object.keys(formattedExtensions).length > 0) {
response.extensions = formattedExtensions;
}
if (this.options.formatResponse) {
response = this.options.formatResponse(response);
}
return this.willSendResponse(response);
} finally {
requestDidEnd();
}
}
private willSendResponse(response: GraphQLResponse): GraphQLResponse {
return this.extensionStack.willSendResponse({
graphqlResponse: response,
}).graphqlResponse;
}
parse(query: string): DocumentNode {
const parsingDidEnd = this.extensionStack.parsingDidStart({
queryString: query,
});
try {
return parse(query);
} finally {
parsingDidEnd();
}
}
validate(document: DocumentNode): ReadonlyArray<GraphQLError> {
let rules = specifiedRules;
if (this.options.validationRules) {
rules = rules.concat(this.options.validationRules);
}
const validationDidEnd = this.extensionStack.validationDidStart();
try {
return validate(this.options.schema, document, rules);
} finally {
validationDidEnd();
}
}
async execute(
document: DocumentNode,
operationName: GraphQLRequest['operationName'],
variables: GraphQLRequest['variables'],
): Promise<ExecutionResult> {
const executionArgs: ExecutionArgs = {
schema: this.options.schema,
document,
rootValue: this.options.rootValue,
contextValue: this.options.context,
variableValues: variables,
operationName,
fieldResolver: this.options.fieldResolver,
};
const executionDidEnd = this.extensionStack.executionDidStart({
executionArgs,
});
try {
return execute(executionArgs);
} finally {
executionDidEnd();
}
}
}

View file

@ -3,10 +3,7 @@ const sha256 = require('hash.js/lib/hash/sha/256');
import { CacheControlExtensionOptions } from 'apollo-cache-control';
import { omit } from 'lodash';
import { Request } from 'apollo-server-env';
import { runQuery, QueryOptions } from './runQuery';
import {
default as GraphQLOptions,
resolveGraphqlOptions,
@ -17,6 +14,7 @@ import {
PersistedQueryNotFoundError,
} from 'apollo-server-errors';
import { calculateCacheControlHeaders } from './caching';
import { GraphQLRequestProcessor } from 'apollo-server-core/src/requestProcessing';
export interface HttpQueryRequest {
method: string;
@ -277,23 +275,6 @@ export async function runHttpQuery(
throw new HttpQueryError(400, 'GraphQL queries must be strings.');
}
// GET operations should only be queries (not mutations). We want to throw
// a particular HTTP error in that case, but we don't actually parse the
// query until we're in runQuery, so we declare the error we want to throw
// here and pass it into runQuery.
// TODO this could/should be added as a validation rule rather than an ad hoc error
let nonQueryError;
if (isGetRequest) {
nonQueryError = new HttpQueryError(
405,
`GET supports only query operation`,
false,
{
Allow: 'POST',
},
);
}
const operationName = requestParams.operationName;
let variables = requestParams.variables;
@ -380,33 +361,46 @@ export async function runHttpQuery(
}
}
let params: QueryOptions = {
const requestProcessor = new GraphQLRequestProcessor({
schema: optionsObject.schema,
queryString,
nonQueryError,
variables: variables,
context,
rootValue: optionsObject.rootValue,
operationName: operationName,
context,
validationRules: optionsObject.validationRules,
formatError: optionsObject.formatError,
formatResponse: optionsObject.formatResponse,
fieldResolver: optionsObject.fieldResolver,
debug: optionsObject.debug,
tracing: optionsObject.tracing,
cacheControl: cacheControl
? omit(cacheControl, [
'calculateHttpHeaders',
'stripFormattedExtensions',
])
: false,
request: request.request,
extensions: optionsObject.extensions,
persistedQueryHit,
persistedQueryRegister,
};
return runQuery(params);
extensions: optionsObject.extensions,
tracing: optionsObject.tracing,
cacheControl: cacheControl,
formatResponse: optionsObject.formatResponse,
debug: optionsObject.debug,
});
// GET operations should only be queries (not mutations). We want to throw
// a particular HTTP error in that case.
if (isGetRequest) {
requestProcessor.willExecuteOperation = operation => {
if (operation.operation !== 'query') {
throw new HttpQueryError(
405,
`GET supports only query operation`,
false,
{
Allow: 'POST',
},
);
}
};
}
return requestProcessor.processRequest({
query: queryString,
operationName,
variables,
extensions,
httpRequest: request.request,
});
} catch (e) {
// Populate any HttpQueryError to our handler which should
// convert it to Http Error.

View file

@ -2,7 +2,8 @@
"extends": "../../tsconfig",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist"
"outDir": "./dist",
"noUnusedLocals": false,
},
"include": ["src/**/*"],
"exclude": ["**/__tests__", "**/__mocks__"]

View file

@ -33,7 +33,7 @@ export interface GraphQLResponse {
export class GraphQLExtension<TContext = any> {
public requestDidStart?(o: {
request: Request;
request: Pick<Request, 'url' | 'method' | 'headers'>;
queryString?: string;
parsedQuery?: DocumentNode;
operationName?: string;
@ -71,7 +71,7 @@ export class GraphQLExtensionStack<TContext = any> {
}
public requestDidStart(o: {
request: Request;
request: Pick<Request, 'url' | 'method' | 'headers'>;
queryString?: string;
parsedQuery?: DocumentNode;
operationName?: string;
@ -154,17 +154,27 @@ export class GraphQLExtensionStack<TContext = any> {
const endHandlers: EndHandler[] = [];
this.extensions.forEach(extension => {
// Invoke the start handler, which may return an end handler.
try {
const endHandler = startInvoker(extension);
if (endHandler) {
endHandlers.push(endHandler);
}
} catch (error) {
console.error(error);
}
});
return (...errors: Array<Error>) => {
// We run end handlers in reverse order of start handlers. That way, the
// first handler in the stack "surrounds" the entire event's process
// (helpful for tracing/reporting!)
endHandlers.reverse();
endHandlers.forEach(endHandler => endHandler(...errors));
for (const endHandler of endHandlers) {
try {
endHandler(...errors);
} catch (error) {
console.error(error);
}
}
};
}
}