From ddb5074da2249ef9af1eb2c22e709efcdc619069 Mon Sep 17 00:00:00 2001 From: nnance Date: Sat, 10 Sep 2016 14:34:27 -0500 Subject: [PATCH 1/4] add debug option to runQuery --- src/core/runQuery.test.ts | 77 +++++++++++++++++++++++++++++++++------ src/core/runQuery.ts | 9 +++++ 2 files changed, 74 insertions(+), 12 deletions(-) diff --git a/src/core/runQuery.test.ts b/src/core/runQuery.test.ts index cd7c95af8c3..4411bb630f5 100644 --- a/src/core/runQuery.test.ts +++ b/src/core/runQuery.test.ts @@ -58,6 +58,12 @@ const QueryType = new GraphQLObjectType({ return 'it ' + (Promise).await('works'); }, }, + testError: { + type: GraphQLString, + resolve() { + throw new Error('Secret error message'); + }, + }, }, }); @@ -84,18 +90,65 @@ describe('runQuery', () => { }); }); - it('returns a syntax error if the query string contains one', () => { - const query = `query { test`; - const expected = /Syntax Error GraphQL/; - return runQuery({ - schema: Schema, - query: query, - variables: { base: 1 }, - }).then((res) => { - expect(res.data).to.be.undefined; - expect(res.errors.length).to.equal(1); - return expect(res.errors[0].message).to.match(expected); - }); + it('returns a syntax error if the query string contains one', () => { + const query = `query { test `; + const expected = /Syntax Error GraphQL/; + return runQuery({ + schema: Schema, + query: query, + variables: { base: 1 }, + }).then((res) => { + expect(res.data).to.be.undefined; + expect(res.errors.length).to.equal(1); + return expect(res.errors[0].message).to.match(expected); + }); + }); + + it('sends stack trace to error if in an error occurs and debug mode not set', () => { + const query = `query { testError }`; + const expected = /at resolveOrError/; + const stackTrace = []; + const origError = console.error; + console.error = (...args) => stackTrace.push(args); + return runQuery({ + schema: Schema, + query: query, + }).then((res) => { + console.error = origError; + return expect(stackTrace[0][0]).to.match(expected); + }); + }); + + it('sends stack trace to error if in an error occurs and debug mode is set', () => { + const query = `query { testError }`; + const expected = /at resolveOrError/; + const stackTrace = []; + const origError = console.error; + console.error = (...args) => stackTrace.push(args); + return runQuery({ + schema: Schema, + query: query, + debug: true, + }).then((res) => { + console.error = origError; + return expect(stackTrace[0][0]).to.match(expected); + }); + }); + + it('does not send stack trace if in an error occurs and not in debug mode', () => { + const query = `query { testError }`; + const expected = []; + const stackTrace = []; + const origError = console.error; + console.error = (...args) => stackTrace.push(args); + return runQuery({ + schema: Schema, + query: query, + debug: false, + }).then((res) => { + console.error = origError; + return expect(stackTrace).to.deep.equals(expected); + }); }); it('returns a validation error if the query string does not pass validation', () => { diff --git a/src/core/runQuery.ts b/src/core/runQuery.ts index 63e3ba26aeb..8faf7e163a0 100644 --- a/src/core/runQuery.ts +++ b/src/core/runQuery.ts @@ -30,6 +30,7 @@ export interface QueryOptions { formatError?: Function; formatResponse?: Function; + debug?: boolean; } const resolvedPromise = Promise.resolve(); @@ -43,6 +44,7 @@ function doRunQuery(options: QueryOptions): Promise { let documentAST: Document; const logFunction = options.logFunction || function(){ return null; }; + const debug = typeof options.debug !== 'undefined' ? options.debug : process.env.NODE_ENV !== 'production'; logFunction('request.start'); @@ -54,6 +56,10 @@ function doRunQuery(options: QueryOptions): Promise { return errors.map(options.formatError || formatError as any) as Array; } + function printStackTrace(error: Error) { + console.error(error.stack); + } + logFunction('request.query', typeof options.query === 'string' ? options.query : print(options.query)); logFunction('request.variables', options.variables); logFunction('request.operationName', options.operationName); @@ -103,6 +109,9 @@ function doRunQuery(options: QueryOptions): Promise { }; if (gqlResponse.errors) { response['errors'] = format(gqlResponse.errors); + if (debug) { + gqlResponse.errors.map(printStackTrace); + } } if (options.formatResponse) { response = options.formatResponse(response, options); From f080d4a41b985ee50e054cc90383912848107fbc Mon Sep 17 00:00:00 2001 From: nnance Date: Sat, 10 Sep 2016 18:17:02 -0600 Subject: [PATCH 2/4] add test for integrations --- package.json | 1 + src/core/runQuery.test.ts | 38 ++++++----------------- src/core/runQuery.ts | 3 +- src/integrations/apolloOptions.ts | 2 ++ src/integrations/expressApollo.ts | 1 + src/integrations/hapiApollo.ts | 1 + src/integrations/integrations.test.ts | 44 +++++++++++++++++++++++++-- src/integrations/koaApollo.ts | 1 + typings.json | 3 +- 9 files changed, 60 insertions(+), 34 deletions(-) diff --git a/package.json b/package.json index 674c6a269d2..ef01fbe8d8d 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "mocha": "^3.0.0", "multer": "^1.1.0", "remap-istanbul": "^0.6.4", + "sinon": "^1.17.5", "supertest": "^2.0.0", "supertest-as-promised": "^4.0.0", "tslint": "^3.13.0", diff --git a/src/core/runQuery.test.ts b/src/core/runQuery.test.ts index 4411bb630f5..e3e2a3a2e86 100644 --- a/src/core/runQuery.test.ts +++ b/src/core/runQuery.test.ts @@ -1,6 +1,5 @@ -import { - expect, -} from 'chai'; +import { expect } from 'chai'; +import { stub } from 'sinon'; import { GraphQLSchema, @@ -104,50 +103,31 @@ describe('runQuery', () => { }); }); - it('sends stack trace to error if in an error occurs and debug mode not set', () => { - const query = `query { testError }`; - const expected = /at resolveOrError/; - const stackTrace = []; - const origError = console.error; - console.error = (...args) => stackTrace.push(args); - return runQuery({ - schema: Schema, - query: query, - }).then((res) => { - console.error = origError; - return expect(stackTrace[0][0]).to.match(expected); - }); - }); - it('sends stack trace to error if in an error occurs and debug mode is set', () => { const query = `query { testError }`; const expected = /at resolveOrError/; - const stackTrace = []; - const origError = console.error; - console.error = (...args) => stackTrace.push(args); + const logStub = stub(console, 'error'); return runQuery({ schema: Schema, query: query, debug: true, }).then((res) => { - console.error = origError; - return expect(stackTrace[0][0]).to.match(expected); + logStub.restore(); + expect(logStub.callCount).to.equal(1); + return expect(logStub.getCall(0).args[0]).to.match(expected); }); }); it('does not send stack trace if in an error occurs and not in debug mode', () => { const query = `query { testError }`; - const expected = []; - const stackTrace = []; - const origError = console.error; - console.error = (...args) => stackTrace.push(args); + const logStub = stub(console, 'error'); return runQuery({ schema: Schema, query: query, debug: false, }).then((res) => { - console.error = origError; - return expect(stackTrace).to.deep.equals(expected); + logStub.restore(); + return expect(logStub.callCount).to.equal(0); }); }); diff --git a/src/core/runQuery.ts b/src/core/runQuery.ts index 8faf7e163a0..ad9aaaafd27 100644 --- a/src/core/runQuery.ts +++ b/src/core/runQuery.ts @@ -44,7 +44,8 @@ function doRunQuery(options: QueryOptions): Promise { let documentAST: Document; const logFunction = options.logFunction || function(){ return null; }; - const debug = typeof options.debug !== 'undefined' ? options.debug : process.env.NODE_ENV !== 'production'; + const debugDefault = process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test'; + const debug = typeof options.debug !== 'undefined' ? options.debug : debugDefault; logFunction('request.start'); diff --git a/src/integrations/apolloOptions.ts b/src/integrations/apolloOptions.ts index e79bc485fa5..942795469e1 100644 --- a/src/integrations/apolloOptions.ts +++ b/src/integrations/apolloOptions.ts @@ -11,6 +11,7 @@ import * as graphql from 'graphql'; * - (optional) formatParams: a function applied to the parameters of every invocation of runQuery * - (optional) validationRules: extra validation rules applied to requests * - (optional) formatResponse: a function applied to each graphQL execution result + * - (optional) debug: a boolean that will print additional debug logging if execution errors occur * */ interface ApolloOptions { @@ -22,6 +23,7 @@ interface ApolloOptions { formatParams?: Function; validationRules?: Array; formatResponse?: Function; + debug?: boolean; } export default ApolloOptions; diff --git a/src/integrations/expressApollo.ts b/src/integrations/expressApollo.ts index 1cef167ce26..3bbb5a04317 100644 --- a/src/integrations/expressApollo.ts +++ b/src/integrations/expressApollo.ts @@ -98,6 +98,7 @@ export function apolloExpress(options: ApolloOptions | ExpressApolloOptionsFunct validationRules: optionsObject.validationRules, formatError: formatErrorFn, formatResponse: optionsObject.formatResponse, + debug: optionsObject.debug, }; if (optionsObject.formatParams) { diff --git a/src/integrations/hapiApollo.ts b/src/integrations/hapiApollo.ts index 9e661dc530e..f822151ff28 100644 --- a/src/integrations/hapiApollo.ts +++ b/src/integrations/hapiApollo.ts @@ -141,6 +141,7 @@ async function processQuery(graphqlParams, optionsObject: ApolloOptions, reply) validationRules: optionsObject.validationRules, formatError: formatErrorFn, formatResponse: optionsObject.formatResponse, + debug: optionsObject.debug, }; if (optionsObject.formatParams) { diff --git a/src/integrations/integrations.test.ts b/src/integrations/integrations.test.ts index 33a47b85cc9..863a5627d4f 100644 --- a/src/integrations/integrations.test.ts +++ b/src/integrations/integrations.test.ts @@ -1,6 +1,5 @@ -import { - expect, -} from 'chai'; +import { expect } from 'chai'; +import { stub } from 'sinon'; import { GraphQLSchema, @@ -425,6 +424,45 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { }); }); + it('sends stack trace to error if debug mode is set', () => { + const expected = /at resolveOrError/; + const stackTrace = []; + const origError = console.error; + console.error = (...args) => stackTrace.push(args); + app = createApp({apolloOptions: { + schema: Schema, + debug: true, + }}); + const req = request(app) + .post('/graphql') + .send({ + query: 'query test{ testError }', + }); + return req.then((res) => { + console.error = origError; + return expect(stackTrace[0][0]).to.match(expected); + }); + }); + + it('sends stack trace to error log if debug mode is set', () => { + const logStub = stub(console, 'error'); + const expected = /at resolveOrError/; + app = createApp({apolloOptions: { + schema: Schema, + debug: true, + }}); + const req = request(app) + .post('/graphql') + .send({ + query: 'query test{ testError }', + }); + return req.then((res) => { + logStub.restore(); + expect(logStub.callCount).to.equal(1); + return expect(logStub.getCall(0).args[0]).to.match(expected); + }); + }); + it('applies additional validationRules', () => { const expected = 'AlwaysInvalidRule was really invalid!'; const AlwaysInvalidRule = function (context) { diff --git a/src/integrations/koaApollo.ts b/src/integrations/koaApollo.ts index d2825413868..386e83c5200 100644 --- a/src/integrations/koaApollo.ts +++ b/src/integrations/koaApollo.ts @@ -75,6 +75,7 @@ export function apolloKoa(options: ApolloOptions | KoaApolloOptionsFunction): Ko validationRules: optionsObject.validationRules, formatError: formatErrorFn, formatResponse: optionsObject.formatResponse, + debug: optionsObject.debug, }; if (optionsObject.formatParams) { diff --git a/typings.json b/typings.json index 780cd73f541..6e486b1fb3a 100644 --- a/typings.json +++ b/typings.json @@ -2,7 +2,8 @@ "dependencies": { "chai": "registry:npm/chai#3.5.0+20160723033700", "graphql": "github:nitintutlani/typed-graphql#ffe7e46e2249cc8f3824a5d15a44938f4354afe9", - "http-errors": "registry:npm/http-errors#1.4.0+20160723033700" + "http-errors": "registry:npm/http-errors#1.4.0+20160723033700", + "sinon": "registry:npm/sinon#1.16.0+20160723033700" }, "globalDependencies": { "body-parser": "registry:dt/body-parser#0.0.0+20160619023215", From 6d37480e271f96d7a0cc302f17c0399d94d2647c Mon Sep 17 00:00:00 2001 From: nnance Date: Sun, 11 Sep 2016 11:45:12 -0700 Subject: [PATCH 3/4] update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f002de77e4..0dc47c3557b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ [PR #131](https://github.com/apollostack/apollo-server/pull/131) * Improve logging function. Issue #79. ([@nnance](https://github.com/nnance)) in [PR #136](https://github.com/apollostack/apollo-server/pull/136) +* Output stack trace for errors in debug mode. Issue #111. ([@nnance](https://github.com/nnance)) in +[PR #137](https://github.com/apollostack/apollo-server/pull/137) ### v0.2.6 * Expose the OperationStore as part of the public API. ([@nnance](https://github.com/nnance)) From 24c24eb68de08353ada51d527b8bc57b888d8bf3 Mon Sep 17 00:00:00 2001 From: nnance Date: Mon, 12 Sep 2016 05:46:12 -0700 Subject: [PATCH 4/4] fix lint error --- src/core/runQuery.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/runQuery.ts b/src/core/runQuery.ts index b2fb96da70a..ef5314d24c9 100644 --- a/src/core/runQuery.ts +++ b/src/core/runQuery.ts @@ -76,7 +76,7 @@ function doRunQuery(options: QueryOptions): Promise { return errors.map(options.formatError || formatError as any) as Array; } - function printStackTrace(error: Error) { + function printStackTrace(error: Error) { console.error(error.stack); }