From 898a033b4096ef389cdca60b79a58e773f0a814b Mon Sep 17 00:00:00 2001 From: James Moriarty Date: Sat, 6 Apr 2019 22:32:45 +1100 Subject: [PATCH] Add support for customValidateFn and rename formatError to customFormatErrorFn * formatError is deprecated. * customFormatErrorFn to replace formatError. --- README.md | 18 ++++--- src/__tests__/http-test.js | 107 +++++++++++++++++++++++++++++++++++-- src/index.js | 52 ++++++++++++++---- 3 files changed, 156 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 0b4fa4d0..71c2b543 100644 --- a/README.md +++ b/README.md @@ -77,10 +77,6 @@ The `graphqlHTTP` function accepts the following options: * **`pretty`**: If `true`, any JSON response will be pretty-printed. - * **`formatError`**: An optional function which will be used to format any - errors produced by fulfilling a GraphQL operation. If no function is - provided, GraphQL's default spec-compliant [`formatError`][] function will be used. - * **`extensions`**: An optional function for adding additional metadata to the GraphQL response as a key-value object. The result will be added to `"extensions"` field in the resulting JSON. This is often a useful place to @@ -91,6 +87,16 @@ The `graphqlHTTP` function accepts the following options: * **`validationRules`**: Optional additional validation rules queries must satisfy in addition to those defined by the GraphQL spec. + * **`customValidateFn`**: An optional function which will be used to validate + instead of default `validate` from `graphql-js`. + + * **`customFormatErrorFn`**: An optional function which will be used to format any + errors produced by fulfilling a GraphQL operation. If no function is + provided, GraphQL's default spec-compliant [`formatError`][] function will be used. + + * **`formatError`**: is deprecated and replaced by `customFormatErrorFn`. It will be + removed in version 1.0.0. + In addition to an object defining each option, options can also be provided as a function (or async function) which returns this options object. This function is provided the arguments `(request, response, graphQLParams)` and is called @@ -288,10 +294,10 @@ graphqlHTTP.getGraphQLParams(request).then(params => { ## Debugging Tips During development, it's useful to get more information from errors, such as -stack traces. Providing a function to `formatError` enables this: +stack traces. Providing a function to `customFormatErrorFn` enables this: ```js -formatError: error => ({ +customFormatErrorFn: error => ({ message: error.message, locations: error.locations, stack: error.stack ? error.stack.split('\n') : [], diff --git a/src/__tests__/http-test.js b/src/__tests__/http-test.js index 33c5dfb0..2aac974d 100644 --- a/src/__tests__/http-test.js +++ b/src/__tests__/http-test.js @@ -28,6 +28,7 @@ import { GraphQLString, GraphQLError, BREAK, + validate, } from 'graphql'; import graphqlHTTP from '../'; @@ -1205,7 +1206,7 @@ describe('test harness', () => { urlString(), graphqlHTTP({ schema: TestSchema, - formatError(error) { + customFormatErrorFn(error) { return { message: 'Custom error format: ' + error.message }; }, }), @@ -1236,7 +1237,7 @@ describe('test harness', () => { urlString(), graphqlHTTP({ schema: TestSchema, - formatError(error) { + customFormatErrorFn(error) { return { message: error.message, locations: error.locations, @@ -1466,7 +1467,7 @@ describe('test harness', () => { }); }); - it('allows for custom error formatting of poorly formed requests', async () => { + it('`formatError` is deprecated', async () => { const app = server(); get( @@ -1480,6 +1481,46 @@ describe('test harness', () => { }), ); + const spy = sinon.spy(console, 'warn'); + + const response = await request(app).get( + urlString({ + variables: 'who:You', + query: 'query helloWho($who: String){ test(who: $who) }', + }), + ); + + expect( + spy.calledWith( + '`formatError` is deprecated and replaced by `customFormatErrorFn`. It will be removed in version 1.0.0.', + ), + ); + expect(response.status).to.equal(400); + expect(JSON.parse(response.text)).to.deep.equal({ + errors: [ + { + message: 'Custom error format: Variables are invalid JSON.', + }, + ], + }); + + spy.restore(); + }); + + it('allows for custom error formatting of poorly formed requests', async () => { + const app = server(); + + get( + app, + urlString(), + graphqlHTTP({ + schema: TestSchema, + customFormatErrorFn(error) { + return { message: 'Custom error format: ' + error.message }; + }, + }), + ); + const response = await request(app).get( urlString({ variables: 'who:You', @@ -1859,6 +1900,64 @@ describe('test harness', () => { }); }); + describe('Custom validate function', () => { + it('returns data', async () => { + const app = server(); + + get( + app, + urlString(), + graphqlHTTP({ + schema: TestSchema, + customValidateFn(schema, documentAST, validationRules) { + return validate(schema, documentAST, validationRules); + }, + }), + ); + + const response = await request(app) + .get(urlString({ query: '{test}', raw: '' })) + .set('Accept', 'text/html'); + + expect(response.status).to.equal(200); + expect(response.text).to.equal('{"data":{"test":"Hello World"}}'); + }); + + it('returns validation errors', async () => { + const app = server(); + + get( + app, + urlString(), + graphqlHTTP({ + schema: TestSchema, + customValidateFn(schema, documentAST, validationRules) { + const errors = validate(schema, documentAST, validationRules); + + const error = new GraphQLError(`custom error ${errors.length}`); + + return [error]; + }, + }), + ); + + const response = await request(app).get( + urlString({ + query: '{thrower}', + }), + ); + + expect(response.status).to.equal(400); + expect(JSON.parse(response.text)).to.deep.equal({ + errors: [ + { + message: 'custom error 0', + }, + ], + }); + }); + }); + describe('Custom validation rules', () => { const AlwaysInvalidRule = function(context) { return { @@ -1946,7 +2045,7 @@ describe('test harness', () => { urlString(), graphqlHTTP({ schema: TestSchema, - formatError: () => null, + customFormatErrorFn: () => null, extensions({ result }) { return { preservedErrors: (result: any).errors }; }, diff --git a/src/index.js b/src/index.js index bac06fbe..71ad83e0 100644 --- a/src/index.js +++ b/src/index.js @@ -72,18 +72,34 @@ export type OptionsData = { */ pretty?: ?boolean, + /** + * An optional array of validation rules that will be applied on the document + * in additional to those defined by the GraphQL spec. + */ + validationRules?: ?Array<(ValidationContext) => ASTVisitor>, + + /** + * An optional function which will be used to validate instead of default `validate` + * from `graphql-js`. + */ + customValidateFn?: ?( + schema: GraphQLSchema, + documentAST: DocumentNode, + rules: $ReadOnlyArray, + ) => $ReadOnlyArray, + /** * An optional function which will be used to format any errors produced by * fulfilling a GraphQL operation. If no function is provided, GraphQL's * default spec-compliant `formatError` function will be used. */ - formatError?: ?(error: GraphQLError) => mixed, + customFormatErrorFn?: ?(error: GraphQLError) => mixed, /** - * An optional array of validation rules that will be applied on the document - * in additional to those defined by the GraphQL spec. + * `formatError` is deprecated and replaced by `customFormatErrorFn`. It will + * be removed in version 1.0.0. */ - validationRules?: ?Array<(ValidationContext) => ASTVisitor>, + formatError?: ?(error: GraphQLError) => mixed, /** * An optional function for adding additional metadata to the GraphQL response @@ -158,7 +174,8 @@ function graphqlHTTP(options: Options): Middleware { let context; let params; let pretty; - let formatErrorFn; + let formatErrorFn = formatError; + let validateFn = validate; let extensionsFn; let showGraphiQL; let query; @@ -201,7 +218,6 @@ function graphqlHTTP(options: Options): Middleware { const rootValue = optionsData.rootValue; const fieldResolver = optionsData.fieldResolver; const graphiql = optionsData.graphiql; - context = optionsData.context || request; let validationRules = specifiedRules; @@ -251,7 +267,12 @@ function graphqlHTTP(options: Options): Middleware { } // Validate AST, reporting any errors. - const validationErrors = validate(schema, documentAST, validationRules); + const validationErrors = validateFn( + schema, + documentAST, + validationRules, + ); + if (validationErrors.length > 0) { // Return 400: Bad Request if any validation errors exist. response.statusCode = 400; @@ -333,9 +354,7 @@ function graphqlHTTP(options: Options): Middleware { } // Format any encountered errors. if (result && result.errors) { - (result: any).errors = result.errors.map( - formatErrorFn || formatError, - ); + (result: any).errors = result.errors.map(formatErrorFn); } // If allowed to show GraphiQL, present it instead of JSON. @@ -380,7 +399,18 @@ function graphqlHTTP(options: Options): Middleware { ); } - formatErrorFn = optionsData.formatError; + if (optionsData.formatError) { + // eslint-disable-next-line no-console + console.warn( + '`formatError` is deprecated and replaced by `customFormatErrorFn`. It will be removed in version 1.0.0.', + ); + } + + validateFn = optionsData.customValidateFn || validateFn; + formatErrorFn = + optionsData.customFormatErrorFn || + optionsData.formatError || + formatErrorFn; extensionsFn = optionsData.extensions; pretty = optionsData.pretty; return optionsData;