Skip to content

Commit

Permalink
feat: introduce GraphQLAggregateError
Browse files Browse the repository at this point in the history
  • Loading branch information
yaacovCR committed Jun 22, 2021
1 parent 32fda3b commit 7611c3a
Show file tree
Hide file tree
Showing 8 changed files with 230 additions and 14 deletions.
19 changes: 18 additions & 1 deletion docs/APIReference-Errors.md
Expand Up @@ -3,7 +3,7 @@ title: graphql/error
layout: ../_core/GraphQLJSLayout
category: API Reference
permalink: /graphql-js/error/
sublinks: formatError,GraphQLError,locatedError,syntaxError
sublinks: formatError,GraphQLError,locatedError,syntaxError,GraphQLAggregateError
next: /graphql-js/execution/
---

Expand Down Expand Up @@ -107,3 +107,20 @@ type GraphQLErrorLocation = {
Given a GraphQLError, format it according to the rules described by the
Response Format, Errors section of the GraphQL Specification.
### GraphQLAggregateError
```js
class GraphQLAggregateError extends Error {
constructor(
errors: Array<Error>,
message?: string
)
}
```
A helper class for bundling multiple distinct errors. When a
GraphQLAggregateError is thrown during execution of a GraphQL operation,
a GraphQLError will be produced from each individual errors and will be
reported separately, according to the rules described by the Response
Format, Errors section of the GraphQL Specification.
36 changes: 36 additions & 0 deletions src/error/GraphQLAggregateError.ts
@@ -0,0 +1,36 @@
/**
* A GraphQLAggregateError is a container for multiple errors.
*
* This helper can be used to report multiple distinct errors simultaneously.
* Note that error handlers must be aware aggregated errors may be reported so as to
* properly handle the contained errors.
*
* See also:
* https://tc39.es/ecma262/multipage/fundamental-objects.html#sec-aggregate-error-objects
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AggregateError
* https://github.com/zloirock/core-js/blob/master/packages/core-js/modules/es.aggregate-error.js
* https://github.com/sindresorhus/aggregate-error
*
*/
export class GraphQLAggregateError<T = Error> extends Error {
readonly errors!: ReadonlyArray<T>;

constructor(errors: ReadonlyArray<T>, message?: string) {
super(message);

Object.defineProperties(this, {
name: { value: 'GraphQLAggregateError' },
message: {
value: message,
writable: true,
},
errors: {
value: errors,
},
});
}

get [Symbol.toStringTag](): string {
return 'GraphQLAggregateError';
}
}
25 changes: 25 additions & 0 deletions src/error/__tests__/GraphQLAggregateError-test.ts
@@ -0,0 +1,25 @@
import { expect } from 'chai';
import { describe, it } from 'mocha';

import { GraphQLAggregateError } from '../GraphQLAggregateError';

describe('GraphQLAggregateError', () => {
it('is a class and is a subclass of Error', () => {
const errors = [new Error('Error1'), new Error('Error2')];
expect(new GraphQLAggregateError(errors)).to.be.instanceof(Error);
expect(new GraphQLAggregateError(errors)).to.be.instanceof(
GraphQLAggregateError,
);
});

it('has a name, errors, and a message (if provided)', () => {
const errors = [new Error('Error1'), new Error('Error2')];
const e = new GraphQLAggregateError(errors, 'msg');

expect(e).to.include({
name: 'GraphQLAggregateError',
errors,
message: 'msg',
});
});
});
2 changes: 2 additions & 0 deletions src/error/index.ts
@@ -1,3 +1,5 @@
export { GraphQLAggregateError } from './GraphQLAggregateError';

export { GraphQLError, printError } from './GraphQLError';

export { syntaxError } from './syntaxError';
Expand Down
127 changes: 116 additions & 11 deletions src/execution/__tests__/executor-test.ts
Expand Up @@ -7,6 +7,7 @@ import { invariant } from '../../jsutils/invariant';
import { Kind } from '../../language/kinds';
import { parse } from '../../language/parser';

import { GraphQLAggregateError } from '../../error/GraphQLAggregateError';
import { GraphQLSchema } from '../../type/schema';
import { GraphQLInt, GraphQLBoolean, GraphQLString } from '../../type/scalars';
import {
Expand Down Expand Up @@ -401,17 +402,22 @@ describe('Execute: Handles basic execution tasks', () => {
fields: {
sync: { type: GraphQLString },
syncError: { type: GraphQLString },
syncAggregateError: { type: GraphQLString },
syncRawError: { type: GraphQLString },
syncReturnError: { type: GraphQLString },
syncReturnAggregateError: { type: GraphQLString },
syncReturnErrorList: { type: new GraphQLList(GraphQLString) },
async: { type: GraphQLString },
asyncReject: { type: GraphQLString },
asyncRejectAggregate: { type: GraphQLString },
asyncRejectWithExtensions: { type: GraphQLString },
asyncRawReject: { type: GraphQLString },
asyncEmptyReject: { type: GraphQLString },
asyncError: { type: GraphQLString },
asyncAggregateError: { type: GraphQLString },
asyncRawError: { type: GraphQLString },
asyncReturnError: { type: GraphQLString },
asyncReturnAggregateError: { type: GraphQLString },
asyncReturnErrorWithExtensions: { type: GraphQLString },
},
}),
Expand All @@ -421,16 +427,22 @@ describe('Execute: Handles basic execution tasks', () => {
{
sync
syncError
syncAggregateError
syncRawError
syncReturnError
syncReturnAggregateError
syncReturnErrorList
async
asyncReject
asyncRejectAggregate
asyncRawReject
asyncRawRejectAggregate
asyncEmptyReject
asyncError
asyncAggregateError
asyncRawError
asyncReturnError
asyncReturnAggregateError
asyncReturnErrorWithExtensions
}
`);
Expand All @@ -442,13 +454,25 @@ describe('Execute: Handles basic execution tasks', () => {
syncError() {
throw new Error('Error getting syncError');
},
syncAggregateError() {
throw new GraphQLAggregateError([
new Error('Error1 getting syncAggregateError'),
new Error('Error2 getting syncAggregateError'),
]);
},
syncRawError() {
// eslint-disable-next-line @typescript-eslint/no-throw-literal
throw 'Error getting syncRawError';
},
syncReturnError() {
return new Error('Error getting syncReturnError');
},
syncReturnAggregateError() {
return new GraphQLAggregateError([
new Error('Error1 getting syncReturnAggregateError'),
new Error('Error2 getting syncReturnAggregateError'),
]);
},
syncReturnErrorList() {
return [
'sync0',
Expand All @@ -465,6 +489,16 @@ describe('Execute: Handles basic execution tasks', () => {
reject(new Error('Error getting asyncReject')),
);
},
asyncRejectAggregate() {
return new Promise((_, reject) =>
reject(
new GraphQLAggregateError([
new Error('Error1 getting asyncRejectAggregate'),
new Error('Error2 getting asyncRejectAggregate'),
]),
),
);
},
asyncRawReject() {
// eslint-disable-next-line prefer-promise-reject-errors
return Promise.reject('Error getting asyncRawReject');
Expand All @@ -478,6 +512,14 @@ describe('Execute: Handles basic execution tasks', () => {
throw new Error('Error getting asyncError');
});
},
asyncAggregateError() {
return new Promise(() => {
throw new GraphQLAggregateError([
new Error('Error1 getting asyncAggregateError'),
new Error('Error2 getting asyncAggregateError'),
]);
});
},
asyncRawError() {
return new Promise(() => {
// eslint-disable-next-line @typescript-eslint/no-throw-literal
Expand All @@ -487,6 +529,14 @@ describe('Execute: Handles basic execution tasks', () => {
asyncReturnError() {
return Promise.resolve(new Error('Error getting asyncReturnError'));
},
asyncReturnAggregateError() {
return Promise.resolve(
new GraphQLAggregateError([
new Error('Error1 getting asyncReturnAggregateError'),
new Error('Error2 getting asyncReturnAggregateError'),
]),
);
},
asyncReturnErrorWithExtensions() {
const error = new Error('Error getting asyncReturnErrorWithExtensions');
// @ts-expect-error
Expand All @@ -501,16 +551,21 @@ describe('Execute: Handles basic execution tasks', () => {
data: {
sync: 'sync',
syncError: null,
syncAggregateError: null,
syncRawError: null,
syncReturnError: null,
syncReturnAggregateError: null,
syncReturnErrorList: ['sync0', null, 'sync2', null],
async: 'async',
asyncReject: null,
asyncRejectAggregate: null,
asyncRawReject: null,
asyncEmptyReject: null,
asyncError: null,
asyncAggregateError: null,
asyncRawError: null,
asyncReturnError: null,
asyncReturnAggregateError: null,
asyncReturnErrorWithExtensions: null,
},
errors: [
Expand All @@ -520,58 +575,108 @@ describe('Execute: Handles basic execution tasks', () => {
path: ['syncError'],
},
{
message: 'Unexpected error value: "Error getting syncRawError"',
message: 'Error1 getting syncAggregateError',
locations: [{ line: 5, column: 9 }],
path: ['syncAggregateError'],
},
{
message: 'Error2 getting syncAggregateError',
locations: [{ line: 5, column: 9 }],
path: ['syncAggregateError'],
},
{
message: 'Unexpected error value: "Error getting syncRawError"',
locations: [{ line: 6, column: 9 }],
path: ['syncRawError'],
},
{
message: 'Error getting syncReturnError',
locations: [{ line: 6, column: 9 }],
locations: [{ line: 7, column: 9 }],
path: ['syncReturnError'],
},
{
message: 'Error1 getting syncReturnAggregateError',
locations: [{ line: 8, column: 9 }],
path: ['syncReturnAggregateError'],
},
{
message: 'Error2 getting syncReturnAggregateError',
locations: [{ line: 8, column: 9 }],
path: ['syncReturnAggregateError'],
},
{
message: 'Error getting syncReturnErrorList1',
locations: [{ line: 7, column: 9 }],
locations: [{ line: 9, column: 9 }],
path: ['syncReturnErrorList', 1],
},
{
message: 'Error getting syncReturnErrorList3',
locations: [{ line: 7, column: 9 }],
locations: [{ line: 9, column: 9 }],
path: ['syncReturnErrorList', 3],
},
{
message: 'Error getting asyncReject',
locations: [{ line: 9, column: 9 }],
locations: [{ line: 11, column: 9 }],
path: ['asyncReject'],
},
{
message: 'Error1 getting asyncRejectAggregate',
locations: [{ line: 12, column: 9 }],
path: ['asyncRejectAggregate'],
},
{
message: 'Error2 getting asyncRejectAggregate',
locations: [{ line: 12, column: 9 }],
path: ['asyncRejectAggregate'],
},
{
message: 'Unexpected error value: "Error getting asyncRawReject"',
locations: [{ line: 10, column: 9 }],
locations: [{ line: 13, column: 9 }],
path: ['asyncRawReject'],
},
{
message: 'Unexpected error value: undefined',
locations: [{ line: 11, column: 9 }],
locations: [{ line: 15, column: 9 }],
path: ['asyncEmptyReject'],
},
{
message: 'Error getting asyncError',
locations: [{ line: 12, column: 9 }],
locations: [{ line: 16, column: 9 }],
path: ['asyncError'],
},
{
message: 'Error1 getting asyncAggregateError',
locations: [{ line: 17, column: 9 }],
path: ['asyncAggregateError'],
},
{
message: 'Error2 getting asyncAggregateError',
locations: [{ line: 17, column: 9 }],
path: ['asyncAggregateError'],
},
{
message: 'Unexpected error value: "Error getting asyncRawError"',
locations: [{ line: 13, column: 9 }],
locations: [{ line: 18, column: 9 }],
path: ['asyncRawError'],
},
{
message: 'Error getting asyncReturnError',
locations: [{ line: 14, column: 9 }],
locations: [{ line: 19, column: 9 }],
path: ['asyncReturnError'],
},
{
message: 'Error1 getting asyncReturnAggregateError',
locations: [{ line: 20, column: 9 }],
path: ['asyncReturnAggregateError'],
},
{
message: 'Error2 getting asyncReturnAggregateError',
locations: [{ line: 20, column: 9 }],
path: ['asyncReturnAggregateError'],
},
{
message: 'Error getting asyncReturnErrorWithExtensions',
locations: [{ line: 15, column: 9 }],
locations: [{ line: 21, column: 9 }],
path: ['asyncReturnErrorWithExtensions'],
extensions: { foo: 'bar' },
},
Expand Down

0 comments on commit 7611c3a

Please sign in to comment.