Skip to content

Commit 306fae9

Browse files
committed
Add GraphQLValidationError as a subclass of GraphQLError
Also deprecated `ValidationContext.reportError` in favor of new `report` function with named argument. Motivation: subclassing allows you to distinguish between errors produced by `graphql-js`. Idea is to subclass all errors into different types e.g. GraphQLValidationError, GraphQLSyntaxError, etc. That way clients can be sure what type of error it is. ```js const { data, errors, extensions } = graphql({ /* ... */ }); const response = { data, errors.map((error) => { if (error instanceof GraphQLValidationError) { // do a special handling of validation errors } else if (error instanceof GraphQLSyntaxError) { // do a special handling of syntax errors } }), extensions, }; ``` Without this change, you can't distinguish one type of error from other types of errors. Subclassing errors for that purpose is a standard approach in JS (e.g. `TypeError` is a subclass of `Error`) and other OOP languages.
1 parent e3aaab0 commit 306fae9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+308
-435
lines changed

src/error/GraphQLError.ts

+18
Original file line numberDiff line numberDiff line change
@@ -237,3 +237,21 @@ export interface GraphQLFormattedError {
237237
*/
238238
readonly extensions?: { [key: string]: unknown };
239239
}
240+
241+
export interface GraphQLValidationErrorOptions {
242+
nodes: ReadonlyArray<ASTNode> | ASTNode;
243+
originalError?: Error | undefined;
244+
extensions?: GraphQLErrorExtensions | undefined;
245+
}
246+
247+
export class GraphQLValidationError extends GraphQLError {
248+
constructor(message: string, options: GraphQLValidationErrorOptions) {
249+
super(message, options);
250+
251+
this.name = 'GraphQLValidationError';
252+
}
253+
254+
get [Symbol.toStringTag](): string {
255+
return 'GraphQLValidationError';
256+
}
257+
}

src/error/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
export { GraphQLError } from './GraphQLError';
1+
export { GraphQLError, GraphQLValidationError } from './GraphQLError';
22
export type {
33
GraphQLErrorOptions,
4+
GraphQLValidationErrorOptions,
45
GraphQLFormattedError,
56
GraphQLErrorExtensions,
67
} from './GraphQLError';

src/index.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -377,10 +377,16 @@ export {
377377
export type { ValidationRule } from './validation/index';
378378

379379
// Create, format, and print GraphQL errors.
380-
export { GraphQLError, syntaxError, locatedError } from './error/index';
380+
export {
381+
GraphQLError,
382+
GraphQLValidationError,
383+
syntaxError,
384+
locatedError,
385+
} from './error/index';
381386

382387
export type {
383388
GraphQLErrorOptions,
389+
GraphQLValidationErrorOptions,
384390
GraphQLFormattedError,
385391
GraphQLErrorExtensions,
386392
} from './error/index';

src/validation/ValidationContext.ts

+25-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import type { Maybe } from '../jsutils/Maybe';
22
import type { ObjMap } from '../jsutils/ObjMap';
33

4-
import type { GraphQLError } from '../error/GraphQLError';
4+
import type {
5+
GraphQLError,
6+
GraphQLErrorExtensions,
7+
} from '../error/GraphQLError';
8+
import { GraphQLValidationError } from '../error/GraphQLError';
59

610
import type {
11+
ASTNode,
712
DocumentNode,
813
FragmentDefinitionNode,
914
FragmentSpreadNode,
@@ -35,6 +40,13 @@ interface VariableUsage {
3540
readonly defaultValue: Maybe<unknown>;
3641
}
3742

43+
interface ValidationReportOptions {
44+
message: string;
45+
nodes: ReadonlyArray<ASTNode> | ASTNode;
46+
originalError?: Error | undefined;
47+
extensions?: GraphQLErrorExtensions | undefined;
48+
}
49+
3850
/**
3951
* An instance of this class is passed as the "this" context to all validators,
4052
* allowing access to commonly useful contextual information from within a
@@ -62,10 +74,22 @@ export class ASTValidationContext {
6274
return 'ASTValidationContext';
6375
}
6476

77+
// TODO: when remove change `onError` to use GraphQLValidationError type instead
78+
/* c8 ignore next 4 */
79+
/** @deprecated Use `report` instead, will be removed in v18 */
6580
reportError(error: GraphQLError): void {
6681
this._onError(error);
6782
}
6883

84+
report(options: ValidationReportOptions): void {
85+
const error = new GraphQLValidationError(options.message, {
86+
nodes: options.nodes,
87+
originalError: options.originalError,
88+
extensions: options.extensions,
89+
});
90+
this._onError(error);
91+
}
92+
6993
getDocument(): DocumentNode {
7094
return this._ast;
7195
}

src/validation/__tests__/validation-test.ts

+4-7
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ import { describe, it } from 'mocha';
33

44
import { expectJSON } from '../../__testUtils__/expectJSON';
55

6-
import { GraphQLError } from '../../error/GraphQLError';
7-
86
import type { DirectiveNode } from '../../language/ast';
97
import { parse } from '../../language/parser';
108

@@ -76,11 +74,10 @@ describe('Validate: Supports full validation', () => {
7674
return {
7775
Directive(node: DirectiveNode) {
7876
const directiveDef = context.getDirective();
79-
const error = new GraphQLError(
80-
'Reporting directive: ' + String(directiveDef),
81-
{ nodes: node },
82-
);
83-
context.reportError(error);
77+
context.report({
78+
message: 'Reporting directive: ' + String(directiveDef),
79+
nodes: node,
80+
});
8481
},
8582
};
8683
}

src/validation/rules/ExecutableDefinitionsRule.ts

+4-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { GraphQLError } from '../../error/GraphQLError';
2-
31
import { Kind } from '../../language/kinds';
42
import { isExecutableDefinitionNode } from '../../language/predicates';
53
import type { ASTVisitor } from '../../language/visitor';
@@ -26,11 +24,10 @@ export function ExecutableDefinitionsRule(
2624
definition.kind === Kind.SCHEMA_EXTENSION
2725
? 'schema'
2826
: '"' + definition.name.value + '"';
29-
context.reportError(
30-
new GraphQLError(`The ${defName} definition is not executable.`, {
31-
nodes: definition,
32-
}),
33-
);
27+
context.report({
28+
message: `The ${defName} definition is not executable.`,
29+
nodes: definition,
30+
});
3431
}
3532
}
3633
return false;

src/validation/rules/FieldsOnCorrectTypeRule.ts

+5-8
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ import { didYouMean } from '../../jsutils/didYouMean';
22
import { naturalCompare } from '../../jsutils/naturalCompare';
33
import { suggestionList } from '../../jsutils/suggestionList';
44

5-
import { GraphQLError } from '../../error/GraphQLError';
6-
75
import type { FieldNode } from '../../language/ast';
86
import type { ASTVisitor } from '../../language/visitor';
97

@@ -54,13 +52,12 @@ export function FieldsOnCorrectTypeRule(
5452
}
5553

5654
// Report an error, including helpful suggestions.
57-
context.reportError(
58-
new GraphQLError(
55+
context.report({
56+
message:
5957
`Cannot query field "${fieldName}" on type "${type.name}".` +
60-
suggestion,
61-
{ nodes: node },
62-
),
63-
);
58+
suggestion,
59+
nodes: node,
60+
});
6461
}
6562
}
6663
},

src/validation/rules/FragmentsOnCompositeTypesRule.ts

+8-14
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { GraphQLError } from '../../error/GraphQLError';
2-
31
import { print } from '../../language/printer';
42
import type { ASTVisitor } from '../../language/visitor';
53

@@ -28,25 +26,21 @@ export function FragmentsOnCompositeTypesRule(
2826
const type = typeFromAST(context.getSchema(), typeCondition);
2927
if (type && !isCompositeType(type)) {
3028
const typeStr = print(typeCondition);
31-
context.reportError(
32-
new GraphQLError(
33-
`Fragment cannot condition on non composite type "${typeStr}".`,
34-
{ nodes: typeCondition },
35-
),
36-
);
29+
context.report({
30+
message: `Fragment cannot condition on non composite type "${typeStr}".`,
31+
nodes: typeCondition,
32+
});
3733
}
3834
}
3935
},
4036
FragmentDefinition(node) {
4137
const type = typeFromAST(context.getSchema(), node.typeCondition);
4238
if (type && !isCompositeType(type)) {
4339
const typeStr = print(node.typeCondition);
44-
context.reportError(
45-
new GraphQLError(
46-
`Fragment "${node.name.value}" cannot condition on non composite type "${typeStr}".`,
47-
{ nodes: node.typeCondition },
48-
),
49-
);
40+
context.report({
41+
message: `Fragment "${node.name.value}" cannot condition on non composite type "${typeStr}".`,
42+
nodes: node.typeCondition,
43+
});
5044
}
5145
},
5246
};

src/validation/rules/KnownArgumentNamesRule.ts

+10-14
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import { didYouMean } from '../../jsutils/didYouMean';
22
import { suggestionList } from '../../jsutils/suggestionList';
33

4-
import { GraphQLError } from '../../error/GraphQLError';
5-
64
import { Kind } from '../../language/kinds';
75
import type { ASTVisitor } from '../../language/visitor';
86

@@ -35,13 +33,12 @@ export function KnownArgumentNamesRule(context: ValidationContext): ASTVisitor {
3533
const argName = argNode.name.value;
3634
const knownArgsNames = fieldDef.args.map((arg) => arg.name);
3735
const suggestions = suggestionList(argName, knownArgsNames);
38-
context.reportError(
39-
new GraphQLError(
36+
context.report({
37+
message:
4038
`Unknown argument "${argName}" on field "${parentType.name}.${fieldDef.name}".` +
41-
didYouMean(suggestions),
42-
{ nodes: argNode },
43-
),
44-
);
39+
didYouMean(suggestions),
40+
nodes: argNode,
41+
});
4542
}
4643
},
4744
};
@@ -84,13 +81,12 @@ export function KnownArgumentNamesOnDirectivesRule(
8481
const argName = argNode.name.value;
8582
if (!knownArgs.includes(argName)) {
8683
const suggestions = suggestionList(argName, knownArgs);
87-
context.reportError(
88-
new GraphQLError(
84+
context.report({
85+
message:
8986
`Unknown argument "${argName}" on directive "@${directiveName}".` +
90-
didYouMean(suggestions),
91-
{ nodes: argNode },
92-
),
93-
);
87+
didYouMean(suggestions),
88+
nodes: argNode,
89+
});
9490
}
9591
}
9692
}

src/validation/rules/KnownDirectivesRule.ts

+8-11
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import { inspect } from '../../jsutils/inspect';
22
import { invariant } from '../../jsutils/invariant';
33

4-
import { GraphQLError } from '../../error/GraphQLError';
5-
64
import type { ASTNode } from '../../language/ast';
75
import { OperationTypeNode } from '../../language/ast';
86
import { DirectiveLocation } from '../../language/directiveLocation';
@@ -50,20 +48,19 @@ export function KnownDirectivesRule(
5048
const locations = locationsMap[name];
5149

5250
if (!locations) {
53-
context.reportError(
54-
new GraphQLError(`Unknown directive "@${name}".`, { nodes: node }),
55-
);
51+
context.report({
52+
message: `Unknown directive "@${name}".`,
53+
nodes: node,
54+
});
5655
return;
5756
}
5857

5958
const candidateLocation = getDirectiveLocationForASTPath(ancestors);
6059
if (candidateLocation && !locations.includes(candidateLocation)) {
61-
context.reportError(
62-
new GraphQLError(
63-
`Directive "@${name}" may not be used on ${candidateLocation}.`,
64-
{ nodes: node },
65-
),
66-
);
60+
context.report({
61+
message: `Directive "@${name}" may not be used on ${candidateLocation}.`,
62+
nodes: node,
63+
});
6764
}
6865
},
6966
};

src/validation/rules/KnownFragmentNamesRule.ts

+4-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { GraphQLError } from '../../error/GraphQLError';
2-
31
import type { ASTVisitor } from '../../language/visitor';
42

53
import type { ValidationContext } from '../ValidationContext';
@@ -18,11 +16,10 @@ export function KnownFragmentNamesRule(context: ValidationContext): ASTVisitor {
1816
const fragmentName = node.name.value;
1917
const fragment = context.getFragment(fragmentName);
2018
if (!fragment) {
21-
context.reportError(
22-
new GraphQLError(`Unknown fragment "${fragmentName}".`, {
23-
nodes: node.name,
24-
}),
25-
);
19+
context.report({
20+
message: `Unknown fragment "${fragmentName}".`,
21+
nodes: node.name,
22+
});
2623
}
2724
},
2825
};

src/validation/rules/KnownTypeNamesRule.ts

+4-8
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import { didYouMean } from '../../jsutils/didYouMean';
22
import { suggestionList } from '../../jsutils/suggestionList';
33

4-
import { GraphQLError } from '../../error/GraphQLError';
5-
64
import type { ASTNode } from '../../language/ast';
75
import {
86
isTypeDefinitionNode,
@@ -59,12 +57,10 @@ export function KnownTypeNamesRule(
5957
typeName,
6058
isSDL ? standardTypeNames.concat(typeNames) : typeNames,
6159
);
62-
context.reportError(
63-
new GraphQLError(
64-
`Unknown type "${typeName}".` + didYouMean(suggestedTypes),
65-
{ nodes: node },
66-
),
67-
);
60+
context.report({
61+
message: `Unknown type "${typeName}".` + didYouMean(suggestedTypes),
62+
nodes: node,
63+
});
6864
}
6965
},
7066
};

src/validation/rules/LoneAnonymousOperationRule.ts

+4-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { GraphQLError } from '../../error/GraphQLError';
2-
31
import { Kind } from '../../language/kinds';
42
import type { ASTVisitor } from '../../language/visitor';
53

@@ -25,12 +23,11 @@ export function LoneAnonymousOperationRule(
2523
},
2624
OperationDefinition(node) {
2725
if (!node.name && operationCount > 1) {
28-
context.reportError(
29-
new GraphQLError(
26+
context.report({
27+
message:
3028
'This anonymous operation must be the only defined operation.',
31-
{ nodes: node },
32-
),
33-
);
29+
nodes: node,
30+
});
3431
}
3532
},
3633
};

0 commit comments

Comments
 (0)