Skip to content

Commit

Permalink
Refactor runQuery into GraphQLRequestProcessor
Browse files Browse the repository at this point in the history
  • Loading branch information
martijnwalraven committed Sep 4, 2018
1 parent b80a8f0 commit de34c7b
Show file tree
Hide file tree
Showing 6 changed files with 313 additions and 59 deletions.
17 changes: 8 additions & 9 deletions packages/apollo-server-core/src/ApolloServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,17 +214,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,
),
);
}
this.extensions.push(
() => new FormatErrorExtension(requestOptions.formatError, debug),
);

if (engine || (engine !== false && process.env.ENGINE_API_KEY)) {
this.engineReportingAgent = new EngineReportingAgent(
Expand Down
4 changes: 2 additions & 2 deletions packages/apollo-server-core/src/formatters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
250 changes: 250 additions & 0 deletions packages/apollo-server-core/src/requestProcessing.ts
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
Loading

0 comments on commit de34c7b

Please sign in to comment.