apollo-server/packages/apollo-server-core/src/runQuery.ts
David Glasser 5c65742a39
Factor out runQuery's use of logFunction into an extension (#1128)
This requires a slightly newer graphql-extensions beta which has more arguments
to requestDidStart.

Also make it ok to not pass logFunction to formatApolloErrors, and make sure
custom fieldResolvers continue to work with extensions (by upgrading the
dependency and fixing a logic bug).

The custom fieldResolvers fix means that we now unconditionally put the
extension stack on the GraphQL context, which means that the context can no
longer be (say) a string.  I changed a test that expected string contexts to
work. You couldn't use a string for a context when using extensions before, so
this isn't that big of a change.
2018-06-01 21:16:25 -07:00

284 lines
9.1 KiB
TypeScript

import {
GraphQLSchema,
GraphQLFieldResolver,
ExecutionResult,
DocumentNode,
parse,
validate,
execute,
ExecutionArgs,
getOperationAST,
GraphQLError,
specifiedRules,
ValidationContext,
} from 'graphql';
import {
enableGraphQLExtensions,
GraphQLExtension,
GraphQLExtensionStack,
} from 'graphql-extensions';
import { TracingExtension } from 'apollo-tracing';
import { CacheControlExtension } from 'apollo-cache-control';
import {
fromGraphQLError,
formatApolloErrors,
ValidationError,
SyntaxError,
} from './errors';
import { LogFunction, LogFunctionExtension } from './logging';
export interface GraphQLResponse {
data?: object;
errors?: Array<GraphQLError & object>;
extensions?: object;
}
export interface QueryOptions {
schema: GraphQLSchema;
// Specify exactly one of these. parsedQuery is primarily for use by
// OperationStore.
queryString?: string;
parsedQuery?: DocumentNode;
// If this is specified and the given GraphQL query is not a "query" (eg, it's
// a mutation), throw this error.
nonQueryError?: Error;
rootValue?: any;
context?: any;
variables?: { [key: string]: any };
operationName?: string;
logFunction?: LogFunction;
validationRules?: Array<(context: ValidationContext) => any>;
fieldResolver?: GraphQLFieldResolver<any, any>;
// WARNING: these extra validation rules are only applied to queries
// submitted as string, not those submitted as Document!
formatError?: Function;
formatResponse?: Function;
debug?: boolean;
tracing?: boolean;
// cacheControl?: boolean | CacheControlExtensionOptions;
cacheControl?: boolean | any;
request: Pick<Request, 'url' | 'method' | 'headers'>;
extensions?: Array<() => GraphQLExtension>;
}
function isQueryOperation(query: DocumentNode, operationName: string) {
const operationAST = getOperationAST(query, operationName);
return operationAST.operation === 'query';
}
export function runQuery(options: QueryOptions): Promise<GraphQLResponse> {
// Fiber-aware Promises run their .then callbacks in Fibers.
return Promise.resolve().then(() => doRunQuery(options));
}
function doRunQuery(options: QueryOptions): Promise<GraphQLResponse> {
if (options.queryString && options.parsedQuery) {
throw new Error('Only supply one of queryString and parsedQuery');
}
if (!(options.queryString || options.parsedQuery)) {
throw new Error('Must supply one of queryString and parsedQuery');
}
const debugDefault =
process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test';
const debug = options.debug !== undefined ? options.debug : debugDefault;
const context = options.context || {};
// If custom extension factories were provided, create per-request extension
// objects.
const extensions = options.extensions ? options.extensions.map(f => f()) : [];
// Legacy hard-coded extension factories. The ApolloServer class doesn't use
// this code path, but older APIs did.
if (options.tracing) {
extensions.push(new TracingExtension());
}
if (options.cacheControl === true) {
extensions.push(new CacheControlExtension());
} else if (options.cacheControl) {
extensions.push(new CacheControlExtension(options.cacheControl));
}
if (options.logFunction) {
extensions.push(new LogFunctionExtension(options.logFunction));
}
const 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(options.schema);
}
context._extensionStack = extensionStack;
const requestDidEnd = extensionStack.requestDidStart({
// Since the Request interfacess are not the same between node-fetch and
// typescript's lib dom, we should limit the fields that need to be passed
// into requestDidStart to only the ones we need, currently just the
// headers, method, and url
request: options.request as any,
queryString: options.queryString,
parsedQuery: options.parsedQuery,
operationName: options.operationName,
variables: options.variables,
});
return Promise.resolve()
.then(() => {
// Parse the document.
let documentAST: DocumentNode;
if (options.parsedQuery) {
documentAST = options.parsedQuery;
} else if (!options.queryString) {
throw new Error('Must supply one of queryString and parsedQuery');
} else {
const parsingDidEnd = extensionStack.parsingDidStart({
queryString: options.queryString,
});
let graphqlParseErrors;
try {
documentAST = parse(options.queryString);
} catch (syntaxError) {
graphqlParseErrors = formatApolloErrors(
[
fromGraphQLError(syntaxError, {
errorClass: SyntaxError,
}),
],
{
formatter: options.formatError,
debug,
},
);
} finally {
parsingDidEnd(...(graphqlParseErrors || []));
if (graphqlParseErrors) {
return Promise.resolve({ errors: graphqlParseErrors });
}
}
}
if (
options.nonQueryError &&
!isQueryOperation(documentAST, options.operationName)
) {
// XXX this goes to requestDidEnd, is that correct or should it be
// validation?
throw options.nonQueryError;
}
let rules = specifiedRules;
if (options.validationRules) {
rules = rules.concat(options.validationRules);
}
const validationDidEnd = extensionStack.validationDidStart();
let validationErrors;
try {
validationErrors = validate(options.schema, documentAST, rules);
} catch (validationThrewError) {
// Catch errors thrown by validate, not just those returned by it.
validationErrors = [validationThrewError];
} finally {
try {
if (validationErrors) {
validationErrors = formatApolloErrors(
validationErrors.map(err =>
fromGraphQLError(err, { errorClass: ValidationError }),
),
{
formatter: options.formatError,
logFunction: options.logFunction,
debug,
},
);
}
} finally {
validationDidEnd(...(validationErrors || []));
if (validationErrors && validationErrors.length) {
return Promise.resolve({
errors: validationErrors,
});
}
}
}
const executionArgs: ExecutionArgs = {
schema: options.schema,
document: documentAST,
rootValue: options.rootValue,
contextValue: context,
variableValues: options.variables,
operationName: options.operationName,
fieldResolver: options.fieldResolver,
};
const executionDidEnd = extensionStack.executionDidStart({
executionArgs,
});
return Promise.resolve()
.then(() => execute(executionArgs))
.catch(executionError => {
return {
// These errors will get passed through formatApolloErrors in the
// `then` below.
// TODO accurate code for this error, which describes this error, which
// can occur when:
// * variables incorrectly typed/null when nonnullable
// * unknown operation/operation name invalid
// * operation type is unsupported
// Options: PREPROCESSING_FAILED, GRAPHQL_RUNTIME_CHECK_FAILED
errors: [fromGraphQLError(executionError)],
} as ExecutionResult;
})
.then(result => {
let response: GraphQLResponse = {
data: result.data,
};
if (result.errors) {
response.errors = formatApolloErrors([...result.errors], {
formatter: options.formatError,
logFunction: options.logFunction,
debug,
});
}
executionDidEnd(...result.errors);
const formattedExtensions = extensionStack.format();
if (Object.keys(formattedExtensions).length > 0) {
response.extensions = formattedExtensions;
}
if (options.formatResponse) {
response = options.formatResponse(response, options);
}
return response;
});
})
.catch(err => {
// Handle the case of an internal server failure (or nonQueryError) ---
// we're not returning a GraphQL response so we don't call
// willSendResponse.
requestDidEnd(err);
throw err;
})
.then(graphqlResponse => {
extensionStack.willSendResponse({ graphqlResponse });
requestDidEnd();
return graphqlResponse;
});
}