Skip to content

Commit

Permalink
Merge pull request #16 from dreamit-de/15-aggregateerror
Browse files Browse the repository at this point in the history
15 aggregateerror
  • Loading branch information
sgohlke authored Mar 25, 2022
2 parents 37ece68 + 392e593 commit d1a22ce
Show file tree
Hide file tree
Showing 9 changed files with 157 additions and 33 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,11 @@ By default `execute` from [graphql-js][1] library is called.
the `extensions` field of the response. Given a `Request`, `GraphQLRequestInfo` and `ExecutionResult` it should return
undefined or an ObjMap of key-value-pairs that are added to the`extensions` field. By default `defaultCollectErrorMetrics`
is used and returns undefined.
- **`reassignAggregateError`**: If `true` and the `ExecutionResult` created by the `executeFunction` contains an `AggregateError`
(e.g. an error containing a comma-separated list of errors in the message and an `originalError` containing multiple errors)
this function will reassign the `originalError.errors` to the `ExecutionResult.errors` field. This is helpful if another
application creates `AggregateErrors` while the initiator of the request (e.g. a Frontend app) does not expect or know how
to handle `AggregateErrors`.

### Metrics options
- **`collectErrorMetricsFunction:`**: Given an error name as string, `Error` and request this function can be used
Expand Down
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@dreamit/graphql-server",
"version": "2.1.0",
"version": "2.2.0",
"description": "A GraphQL server written in NodeJS/Typescript.",
"main": "build/src/index.js",
"types": "build/src/index.d.ts",
Expand Down Expand Up @@ -30,17 +30,17 @@
"@types/content-type": "1.1.5",
"@types/express": "4.17.13",
"@types/jest": "27.4.1",
"@types/node": "17.0.21",
"@typescript-eslint/eslint-plugin": "5.12.1",
"@types/node": "17.0.22",
"@typescript-eslint/eslint-plugin": "5.16.0",
"cross-fetch": "3.1.5",
"eslint": "8.10.0",
"eslint": "8.11.0",
"eslint-plugin-deprecation": "1.3.2",
"eslint-plugin-import": "2.25.4",
"eslint-plugin-modules-newline": "0.0.6",
"eslint-plugin-unicorn": "41.0.0",
"eslint-plugin-unicorn": "41.0.1",
"express": "4.17.3",
"jest": "27.5.1",
"jest-html-reporters": "3.0.5",
"jest-html-reporters": "3.0.6",
"ts-jest": "27.1.3",
"typescript": "4.5.5"
},
Expand Down
9 changes: 9 additions & 0 deletions src/error/AggregateError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import {GraphQLError} from 'graphql'

export interface AggregateError extends Error {
errors: GraphQLError[];
}

export function isAggregateError(object: object): object is AggregateError {
return 'errors' in object
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* It uses the standard graphql library to receive GraphQL
* requests and send back appropriate responses.
*/
export * from './error/AggregateError'
export * from './error/ErrorNameConstants'
export * from './error/GraphQLErrorWithStatusCode'

Expand Down
48 changes: 35 additions & 13 deletions src/server/GraphQLServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,11 @@ import {
MISSING_QUERY_PARAMETER_ERROR,
SCHEMA_VALIDATION_ERROR,
SYNTAX_ERROR,
VALIDATION_ERROR,
VALIDATION_ERROR,
GraphQLServerRequest,
GraphQLServerResponse ,
RequestInformationExtractor
GraphQLServerResponse,
RequestInformationExtractor,
isAggregateError
} from '..'


Expand Down Expand Up @@ -109,6 +110,12 @@ export class GraphQLServer {
* introspection request by using recommendations to explore the schema.
*/
private removeValidationRecommendations?: boolean

/**
* Reassign AggregateError containing more than one error back to the original
* errors field of the ExecutionResult.
*/
private reassignAggregateError?: boolean
private validateSchemaFunction:
(schema: GraphQLSchema,
documentAST: DocumentNode,
Expand All @@ -117,11 +124,11 @@ export class GraphQLServer {
typeInfo?: TypeInfo,) => ReadonlyArray<GraphQLError> = validate
private rootValue?: unknown
private contextFunction:
(request: GraphQLServerRequest,
(request: GraphQLServerRequest,
response: GraphQLServerResponse) => unknown = this.defaultContextFunction
private fieldResolver?: Maybe<GraphQLFieldResolver<unknown, unknown>>
private typeResolver?: Maybe<GraphQLTypeResolver<unknown, unknown>>
private executeFunction: (arguments_: ExecutionArgs)
private executeFunction: (arguments_: ExecutionArgs)
=> PromiseOrValue<ExecutionResult> = execute
private extensionFunction: (request: GraphQLServerRequest,
requestInformation: GraphQLRequestInfo,
Expand Down Expand Up @@ -153,6 +160,10 @@ export class GraphQLServer {
options.removeValidationRecommendations === undefined
? true
: options.removeValidationRecommendations
this.reassignAggregateError =
options.reassignAggregateError === undefined
? false
: options.reassignAggregateError
this.validateSchemaFunction = options.validateFunction || validate
this.rootValue = options.rootValue
this.contextFunction = options.contextFunction || this.defaultContextFunction
Expand Down Expand Up @@ -225,7 +236,7 @@ export class GraphQLServer {
return this.metricsClient.getMetrics()
}

async handleRequest(request: GraphQLServerRequest,
async handleRequest(request: GraphQLServerRequest,
response: GraphQLServerResponse): Promise<void> {
// Increase request throughput
this.metricsClient.increaseRequestThroughput(request)
Expand Down Expand Up @@ -380,6 +391,17 @@ export class GraphQLServer {
// Collect error metrics for execution result
if (executionResult.errors && executionResult.errors.length > 0) {
for (const error of executionResult.errors) {
if (this.reassignAggregateError
&& error.originalError
&& isAggregateError(error.originalError)) {

this.logDebugIfEnabled('Error is AggregateError and ' +
'reassignAggregateError feature is enabled. AggregateError ' +
'will be reassigned to original errors field.',
request)
executionResult.errors = error.originalError.errors
}

this.logger.error('While processing the request ' +
'the following error occurred: ',
error,
Expand Down Expand Up @@ -435,7 +457,7 @@ export class GraphQLServer {
}

/** Sends a fitting response if the schema used by the GraphQL server is invalid */
sendInvalidSchemaResponse(request: GraphQLServerRequest,
sendInvalidSchemaResponse(request: GraphQLServerRequest,
response: GraphQLServerResponse): void {
return this.sendResponse(response,
{errors: [invalidSchemaError]},
Expand All @@ -454,8 +476,8 @@ export class GraphQLServer {
}

/** Sends a fitting response if a syntax error occurred during document parsing */
sendSyntaxErrorResponse(request: GraphQLServerRequest,
response: GraphQLServerResponse,
sendSyntaxErrorResponse(request: GraphQLServerRequest,
response: GraphQLServerResponse,
syntaxError: GraphQLError): void {
return this.sendResponse(response,
{errors: [syntaxError]},
Expand Down Expand Up @@ -544,7 +566,7 @@ export class GraphQLServer {
* @param {GraphQLServerRequest} request - The initial request
* @param {GraphQLServerResponse} response - The response to send back
*/
defaultContextFunction(request: GraphQLServerRequest,
defaultContextFunction(request: GraphQLServerRequest,
response: GraphQLServerResponse): unknown {
this.logDebugIfEnabled(
`Calling defaultContextFunction with request ${request} and response ${response}`,
Expand Down Expand Up @@ -580,8 +602,8 @@ export class GraphQLServer {
* @param {GraphQLError} error - An optional GraphQL error
* @param {GraphQLServerRequest} request - The initial request
*/
defaultCollectErrorMetrics(errorName: string,
error?: unknown,
defaultCollectErrorMetrics(errorName: string,
error?: unknown,
request?: GraphQLServerRequest): void {
this.logDebugIfEnabled(
`Calling defaultCollectErrorMetrics with request ${request}`+
Expand All @@ -596,7 +618,7 @@ export class GraphQLServer {
* @param {unknown} error - An error
* @param {GraphQLServerRequest} request - The initial request
*/
increaseFetchOrGraphQLErrorMetric(error: unknown,
increaseFetchOrGraphQLErrorMetric(error: unknown,
request: GraphQLServerRequest): void {
this.logDebugIfEnabled(
`Calling increaseFetchOrGraphQLErrorMetric with request ${request}`+
Expand Down
7 changes: 4 additions & 3 deletions src/server/GraphQLServerOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import {
RequestInformationExtractor,
GraphQLRequestInfo,
MetricsClient,
GraphQLServerRequest,
GraphQLServerResponse
GraphQLServerRequest,
GraphQLServerResponse
} from '..'
import {
DocumentNode,
Expand Down Expand Up @@ -46,14 +46,15 @@ export interface GraphQLServerOptions {
readonly validationTypeInfo?: TypeInfo
readonly validationOptions?: { maxErrors?: number }
readonly removeValidationRecommendations?: boolean
readonly reassignAggregateError?: boolean
readonly validateFunction?: (schema: GraphQLSchema,
documentAST: DocumentNode,
rules?: ReadonlyArray<ValidationRule>,
options?: { maxErrors?: number },
typeInfo?: TypeInfo,
) => ReadonlyArray<GraphQLError>
readonly rootValue?: unknown | undefined
readonly contextFunction?: (request: GraphQLServerRequest,
readonly contextFunction?: (request: GraphQLServerRequest,
response: GraphQLServerResponse) => unknown
readonly fieldResolver?: Maybe<GraphQLFieldResolver<unknown, unknown>>
readonly typeResolver?: Maybe<GraphQLTypeResolver<unknown, unknown>>
Expand Down
20 changes: 19 additions & 1 deletion tests/ExampleSchemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import {
GraphQLError,
GraphQLSchema
} from 'graphql'
import {GraphQLRequestInfo} from '../src'
import {
AggregateError,
GraphQLRequestInfo
} from '../src'

// Contains example schemas and data that can be used across tests

Expand Down Expand Up @@ -100,3 +103,18 @@ export const userSchemaResolvers= {
}
}

export const multipleErrorResponse = {
errors: [new GraphQLError('The first error!, The second error!',
{
originalError:
{
name: 'AggregateError',
message:'The first error!, The second error!',
errors: [
new GraphQLError('The first error!', {}),
new GraphQLError('The second error!', {})
]
} as AggregateError
})]
}

39 changes: 29 additions & 10 deletions tests/GraphQLServer.integration.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
/* eslint-disable @typescript-eslint/naming-convention */
import express, {Express} from 'express'
import {Server} from 'node:http'
import {GraphQLServer} from '../src/'
import {
GraphQLServer
} from '../src/'
import fetch from 'cross-fetch'
import {
usersRequest,
Expand All @@ -16,7 +18,8 @@ import {
userQuery,
userVariables,
introspectionQuery,
usersQueryWithUnknownField
usersQueryWithUnknownField,
multipleErrorResponse
} from './ExampleSchemas'
import {
GraphQLError,
Expand Down Expand Up @@ -112,7 +115,7 @@ test('Should get unfiltered error response if a' +
rootValue: userSchemaResolvers,
logger: LOGGER,
debug: true,
removeValidationRecommendations: false
removeValidationRecommendations: false
})
const response = await fetchResponse('{"query":"query users{ users { userIdABC userName } }"}')
const responseObject = await response.json()
Expand Down Expand Up @@ -179,7 +182,7 @@ test('Should get error response when GraphQL context error' +
rootValue: userSchemaResolvers,
logger: LOGGER,
debug: true,
executeFunction: () => {throw new GraphQLError('A GraphQL context error occurred!', {})}
executeFunction: () => {throw new GraphQLError('A GraphQL context error occurred!', {})}
})
const response = await fetchResponse(`{"query":"${usersQuery}"}`)
const responseObject = await response.json()
Expand Down Expand Up @@ -270,7 +273,7 @@ test('Should get error response if invalid schema is used', async() => {
rootValue: userSchemaResolvers,
logger: LOGGER,
debug: true,
schemaValidationFunction: () => [new GraphQLError('Schema is not valid!', {})]
schemaValidationFunction: () => [new GraphQLError('Schema is not valid!', {})]
})
const response = await fetchResponse('doesnotmatter')
const responseObject = await response.json()
Expand All @@ -297,7 +300,7 @@ test('Should get extensions in GraphQL response if extension function is defined
logger: LOGGER,
debug: true,
removeValidationRecommendations: true,
extensionFunction: () => extensionTestData
extensionFunction: () => extensionTestData
})
const response = await fetchResponse(`{"query":"${usersQuery}"}`)
const responseObject = await response.json()
Expand All @@ -321,7 +324,7 @@ test('Should get error response if introspection is requested ' +
logger: LOGGER,
debug: true,
removeValidationRecommendations: true,
customValidationRules: [NoSchemaIntrospectionCustomRule]
customValidationRules: [NoSchemaIntrospectionCustomRule]
})
const response = await fetchResponse(`{"query":"${introspectionQuery}"}`)
const responseObject = await response.json()
Expand All @@ -340,7 +343,7 @@ test('Should get error response if query with unknown field is executed ' +
logger: LOGGER,
debug: true,
removeValidationRecommendations: true,
customValidationRules: [NoSchemaIntrospectionCustomRule]
customValidationRules: [NoSchemaIntrospectionCustomRule]
})
const response = await fetchResponse(`{"query":"${usersQueryWithUnknownField}"}`)
const responseObject = await response.json()
Expand All @@ -356,7 +359,7 @@ test('Should get error response if query with unknown field is executed ' +
logger: LOGGER,
debug: true,
removeValidationRecommendations: true,
customValidationRules: []
customValidationRules: []
})
const response = await fetchResponse(`{"query":"${usersQueryWithUnknownField}"}`)
const responseObject = await response.json()
Expand All @@ -373,14 +376,30 @@ test('Should get data response if query with unknown field is executed ' +
debug: true,
removeValidationRecommendations: true,
defaultValidationRules: [],
customValidationRules: []
customValidationRules: []
})
const response = await fetchResponse(`{"query":"${usersQueryWithUnknownField}"}`)
const responseObject = await response.json()
expect(responseObject.data.users).toStrictEqual([userOne, userTwo])
customGraphQLServer.setOptions(INITIAL_GRAPHQL_SERVER_OPTIONS)
})

test('Should not reassign AggregateError to original errors field' +
' when reassignAggregateError is disabled', async() => {
customGraphQLServer.setOptions({
schema: userSchema,
rootValue: userSchemaResolvers,
logger: LOGGER,
debug: true,
reassignAggregateError: false,
executeFunction: () => (multipleErrorResponse)
})
const response = await fetchResponse(`{"query":"${returnErrorQuery}"}`)
const responseObject = await response.json()
expect(responseObject.errors[0].message).toBe('The first error!, The second error!')
customGraphQLServer.setOptions(INITIAL_GRAPHQL_SERVER_OPTIONS)
})

function setupGraphQLServer(): Express {
const graphQLServerExpress = express()
customGraphQLServer = new GraphQLServer(INITIAL_GRAPHQL_SERVER_OPTIONS)
Expand Down
Loading

0 comments on commit d1a22ce

Please sign in to comment.