diff --git a/packages/apollo-server-core/package.json b/packages/apollo-server-core/package.json index cb86408e466..4a2fc4f7c8c 100644 --- a/packages/apollo-server-core/package.json +++ b/packages/apollo-server-core/package.json @@ -47,8 +47,8 @@ }, "dependencies": { "apollo-cache-control": "^0.1.1", - "apollo-tracing": "^0.1.0", - "graphql-extensions": "^0.0.x", + "apollo-tracing": "^0.2.0-beta.0", + "graphql-extensions": "0.1.0-beta.7", "graphql-subscriptions": "^0.5.8", "graphql-tools": "^3.0.2", "subscriptions-transport-ws": "^0.9.9", diff --git a/packages/apollo-server-core/src/ApolloServer.ts b/packages/apollo-server-core/src/ApolloServer.ts index 310e36159b3..289da52a748 100644 --- a/packages/apollo-server-core/src/ApolloServer.ts +++ b/packages/apollo-server-core/src/ApolloServer.ts @@ -15,6 +15,7 @@ import { ValidationContext, FieldDefinitionNode, } from 'graphql'; +import { GraphQLExtension } from 'graphql-extensions'; import { ApolloEngine } from 'apollo-engine'; import { diff --git a/packages/apollo-server-core/src/graphqlOptions.ts b/packages/apollo-server-core/src/graphqlOptions.ts index 23561a68cc7..72098e2b891 100644 --- a/packages/apollo-server-core/src/graphqlOptions.ts +++ b/packages/apollo-server-core/src/graphqlOptions.ts @@ -19,7 +19,7 @@ import { GraphQLExtension } from 'graphql-extensions'; * - (optional) formatResponse: a function applied to each graphQL execution result * - (optional) fieldResolver: a custom default field resolver * - (optional) debug: a boolean that will print additional debug logging if execution errors occur - * - (optional) extensions: an array of GraphQLExtension + * - (optional) extensions: an array of GraphQLExtensions * */ export interface GraphQLServerOptions< @@ -40,7 +40,7 @@ export interface GraphQLServerOptions< tracing?: boolean; // cacheControl?: boolean | CacheControlExtensionOptions; cacheControl?: boolean | any; - extensions?: Array; + extensions?: Array; } export default GraphQLServerOptions; diff --git a/packages/apollo-server-core/src/runQuery.test.ts b/packages/apollo-server-core/src/runQuery.test.ts index b60e5192bbb..9c544418f88 100644 --- a/packages/apollo-server-core/src/runQuery.test.ts +++ b/packages/apollo-server-core/src/runQuery.test.ts @@ -375,9 +375,9 @@ describe('runQuery', () => { } it('creates the extension stack', async () => { - const query = `{ testString }`; + const queryString = `{ testString }`; const expected = { testString: 'it works' }; - const extensions = [CustomExtension]; + const extensions = [new CustomExtension()]; return runQuery({ schema: new GraphQLSchema({ query: new GraphQLObjectType({ @@ -397,16 +397,22 @@ describe('runQuery', () => { }, }), }), - query, + queryString, extensions, + request: new MockReq(), }); }); it('runs format response from extensions', async () => { - const query = `{ testString }`; + const queryString = `{ testString }`; const expected = { testString: 'it works' }; - const extensions = [CustomExtension]; - return runQuery({ schema, query: query, extensions }).then(res => { + const extensions = [new CustomExtension()]; + return runQuery({ + schema, + queryString, + extensions, + request: new MockReq(), + }).then(res => { return expect(res.extensions).to.deep.equal({ customExtension: { foo: 'bar' }, }); diff --git a/packages/apollo-server-core/src/runQuery.ts b/packages/apollo-server-core/src/runQuery.ts index c2a383d7a15..2d205165880 100644 --- a/packages/apollo-server-core/src/runQuery.ts +++ b/packages/apollo-server-core/src/runQuery.ts @@ -7,6 +7,7 @@ import { print, validate, execute, + ExecutionArgs, getOperationAST, GraphQLError, specifiedRules, @@ -17,6 +18,7 @@ import { enableGraphQLExtensions, GraphQLExtension, GraphQLExtensionStack, + EndHandler, } from 'graphql-extensions'; import { TracingExtension } from 'apollo-tracing'; import { CacheControlExtension } from 'apollo-cache-control'; @@ -29,6 +31,7 @@ import { } from './errors'; import { LogStep, LogAction, LogMessage, LogFunction } from './logging'; +import { GraphQLRequest } from 'apollo-fetch'; export interface GraphQLResponse { data?: object; @@ -64,7 +67,7 @@ export interface QueryOptions { // cacheControl?: boolean | CacheControlExtensionOptions; cacheControl?: boolean | any; request: Request; - extensions?: Array; + extensions?: Array; } function isQueryOperation(query: DocumentNode, operationName: string) { @@ -78,8 +81,6 @@ export function runQuery(options: QueryOptions): Promise { } function doRunQuery(options: QueryOptions): Promise { - let documentAST: DocumentNode; - if (options.queryString && options.parsedQuery) { throw new Error('Only supply one of queryString and parsedQuery'); } @@ -99,166 +100,207 @@ function doRunQuery(options: QueryOptions): Promise { logFunction({ action: LogAction.request, step: LogStep.start }); const context = options.context || {}; - let extensions = options.extensions !== undefined ? options.extensions : []; + let extensions = + options.extensions !== undefined ? [...options.extensions] : []; if (options.tracing) { - extensions.push(TracingExtension); + extensions.push(new TracingExtension()); } if (options.cacheControl === true) { - extensions.push(CacheControlExtension); + extensions.push(new CacheControlExtension()); } else if (options.cacheControl) { extensions.push(new CacheControlExtension(options.cacheControl)); } - const extensionStack = - extensions.length > 0 && new GraphQLExtensionStack(extensions); + const extensionStack = new GraphQLExtensionStack(extensions); - if (extensionStack) { + // We unconditionally create an extensionStack (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. + if (extensions.length > 0) { context._extensionStack = extensionStack; enableGraphQLExtensions(options.schema); - - extensionStack.requestDidStart(); } - const loggedQuery = options.queryString || print(options.parsedQuery); - logFunction({ - action: LogAction.request, - step: LogStep.status, - key: 'query', - data: loggedQuery, - }); - logFunction({ - action: LogAction.request, - step: LogStep.status, - key: 'variables', - data: options.variables, + const requestDidEnd = extensionStack.requestDidStart({ + request: options.request, }); - logFunction({ - action: LogAction.request, - step: LogStep.status, - key: 'operationName', - data: options.operationName, - }); - - // Parse and validate the query, unless it is already an AST (eg, if using - // OperationStore with formatParams). - if (options.queryString) { - try { - logFunction({ action: LogAction.parse, step: LogStep.start }); - documentAST = parse(options.queryString); - logFunction({ action: LogAction.parse, step: LogStep.end }); - } catch (syntaxError) { - logFunction({ action: LogAction.parse, step: LogStep.end }); - return Promise.resolve({ - errors: formatApolloErrors( - [ - fromGraphQLError(syntaxError, { - errorClass: SyntaxError, - }), - ], - { - formatter: options.formatError, - debug, - }, - ), + return Promise.resolve() + .then(() => { + const loggedQuery = options.queryString || print(options.parsedQuery); + logFunction({ + action: LogAction.request, + step: LogStep.status, + key: 'query', + data: loggedQuery, + }); + logFunction({ + action: LogAction.request, + step: LogStep.status, + key: 'variables', + data: options.variables, + }); + logFunction({ + action: LogAction.request, + step: LogStep.status, + key: 'operationName', + data: options.operationName, }); - } - } else { - documentAST = options.parsedQuery; - } - - if ( - options.nonQueryError && - !isQueryOperation(documentAST, options.operationName) - ) { - throw options.nonQueryError; - } - let rules = specifiedRules; - if (options.validationRules) { - rules = rules.concat(options.validationRules); - } - logFunction({ action: LogAction.validation, step: LogStep.start }); - const validationErrors = validate(options.schema, documentAST, rules); - logFunction({ action: LogAction.validation, step: LogStep.end }); + // 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 { + logFunction({ action: LogAction.parse, step: LogStep.start }); + 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 || [])); + logFunction({ action: LogAction.parse, step: LogStep.end }); + if (graphqlParseErrors) { + return Promise.resolve({ errors: graphqlParseErrors }); + } + } + } - if (validationErrors.length) { - return Promise.resolve({ - errors: formatApolloErrors( - validationErrors.map(err => - fromGraphQLError(err, { errorClass: ValidationError }), - ), - { - formatter: options.formatError, - logFunction, - debug, - }, - ), - }); - } + if ( + options.nonQueryError && + !isQueryOperation(documentAST, options.operationName) + ) { + // XXX this goes to requestDidEnd, is that correct or should it be + // validation? + throw options.nonQueryError; + } - if (extensionStack) { - extensionStack.executionDidStart(); - } + let rules = specifiedRules; + if (options.validationRules) { + rules = rules.concat(options.validationRules); + } + logFunction({ action: LogAction.validation, step: LogStep.start }); + 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, + debug, + }, + ); + } + } finally { + validationDidEnd(...(validationErrors || [])); + logFunction({ action: LogAction.validation, step: LogStep.end }); - try { - logFunction({ action: LogAction.execute, step: LogStep.start }); - return Promise.resolve( - execute( - options.schema, - documentAST, - options.rootValue, - context, - options.variables, - options.operationName, - options.fieldResolver, - ), - ).then(result => { - logFunction({ action: LogAction.execute, step: LogStep.end }); + if (validationErrors && validationErrors.length) { + return Promise.resolve({ + errors: validationErrors, + }); + } + } + } - let response: GraphQLResponse = { - data: result.data, + const executionArgs: ExecutionArgs = { + schema: options.schema, + document: documentAST, + rootValue: options.rootValue, + contextValue: context, + variableValues: options.variables, + operationName: options.operationName, + fieldResolver: options.fieldResolver, }; + logFunction({ action: LogAction.execute, step: LogStep.start }); + 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 - if (result.errors) { - response.errors = formatApolloErrors([...result.errors], { - formatter: options.formatError, - logFunction, - debug, - }); - } + errors: [fromGraphQLError(executionError)], + } as ExecutionResult; + }) + .then(result => { + let response: GraphQLResponse = { + data: result.data, + }; - if (extensionStack) { - extensionStack.executionDidEnd(); - extensionStack.requestDidEnd(); - response.extensions = extensionStack.format(); - } + if (result.errors) { + response.errors = formatApolloErrors([...result.errors], { + formatter: options.formatError, + logFunction, + debug, + }); + } - if (options.formatResponse) { - response = options.formatResponse(response, options); - } + executionDidEnd(...result.errors); + logFunction({ action: LogAction.execute, step: LogStep.end }); + + 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); + logFunction({ action: LogAction.request, step: LogStep.end }); + throw err; + }) + .then(graphqlResponse => { + extensionStack.willSendResponse({ graphqlResponse }); + requestDidEnd(); logFunction({ action: LogAction.request, step: LogStep.end, key: 'response', - data: response, + data: graphqlResponse, }); - - return response; + return graphqlResponse; }); - } catch (executionError) { - logFunction({ action: LogAction.execute, step: LogStep.end }); - logFunction({ action: LogAction.request, step: LogStep.end }); - return Promise.resolve({ - //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: formatApolloErrors([fromGraphQLError(executionError)], { - formatter: options.formatError, - debug, - }), - }); - } }