Skip to content

Commit 7011034

Browse files
committed
Merge pull request #282 from graphql/errormessage
[validator] Add suggested types to incorrect field message
2 parents f320fd0 + 7861b22 commit 7011034

File tree

4 files changed

+137
-24
lines changed

4 files changed

+137
-24
lines changed

src/validation/__tests__/FieldsOnCorrectType.js

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* of patent rights can be found in the PATENTS file in the same directory.
88
*/
99

10+
import { expect } from 'chai';
1011
import { describe, it } from 'mocha';
1112
import { expectPassesRule, expectFailsRule } from './harness';
1213
import {
@@ -15,9 +16,9 @@ import {
1516
} from '../rules/FieldsOnCorrectType';
1617

1718

18-
function undefinedField(field, type, line, column) {
19+
function undefinedField(field, type, suggestions, line, column) {
1920
return {
20-
message: undefinedFieldMessage(field, type),
21+
message: undefinedFieldMessage(field, type, suggestions),
2122
locations: [ { line, column } ],
2223
};
2324
}
@@ -84,8 +85,8 @@ describe('Validate: Fields on correct type', () => {
8485
}
8586
}
8687
}`,
87-
[ undefinedField('unknown_pet_field', 'Pet', 3, 9),
88-
undefinedField('unknown_cat_field', 'Cat', 5, 13) ]
88+
[ undefinedField('unknown_pet_field', 'Pet', [], 3, 9),
89+
undefinedField('unknown_cat_field', 'Cat', [], 5, 13) ]
8990
);
9091
});
9192

@@ -94,7 +95,7 @@ describe('Validate: Fields on correct type', () => {
9495
fragment fieldNotDefined on Dog {
9596
meowVolume
9697
}`,
97-
[ undefinedField('meowVolume', 'Dog', 3, 9) ]
98+
[ undefinedField('meowVolume', 'Dog', [], 3, 9) ]
9899
);
99100
});
100101

@@ -105,7 +106,7 @@ describe('Validate: Fields on correct type', () => {
105106
deeper_unknown_field
106107
}
107108
}`,
108-
[ undefinedField('unknown_field', 'Dog', 3, 9) ]
109+
[ undefinedField('unknown_field', 'Dog', [], 3, 9) ]
109110
);
110111
});
111112

@@ -116,7 +117,7 @@ describe('Validate: Fields on correct type', () => {
116117
unknown_field
117118
}
118119
}`,
119-
[ undefinedField('unknown_field', 'Pet', 4, 11) ]
120+
[ undefinedField('unknown_field', 'Pet', [], 4, 11) ]
120121
);
121122
});
122123

@@ -127,7 +128,7 @@ describe('Validate: Fields on correct type', () => {
127128
meowVolume
128129
}
129130
}`,
130-
[ undefinedField('meowVolume', 'Dog', 4, 11) ]
131+
[ undefinedField('meowVolume', 'Dog', [], 4, 11) ]
131132
);
132133
});
133134

@@ -136,7 +137,7 @@ describe('Validate: Fields on correct type', () => {
136137
fragment aliasedFieldTargetNotDefined on Dog {
137138
volume : mooVolume
138139
}`,
139-
[ undefinedField('mooVolume', 'Dog', 3, 9) ]
140+
[ undefinedField('mooVolume', 'Dog', [], 3, 9) ]
140141
);
141142
});
142143

@@ -145,7 +146,7 @@ describe('Validate: Fields on correct type', () => {
145146
fragment aliasedLyingFieldTargetNotDefined on Dog {
146147
barkVolume : kawVolume
147148
}`,
148-
[ undefinedField('kawVolume', 'Dog', 3, 9) ]
149+
[ undefinedField('kawVolume', 'Dog', [], 3, 9) ]
149150
);
150151
});
151152

@@ -154,16 +155,16 @@ describe('Validate: Fields on correct type', () => {
154155
fragment notDefinedOnInterface on Pet {
155156
tailLength
156157
}`,
157-
[ undefinedField('tailLength', 'Pet', 3, 9) ]
158+
[ undefinedField('tailLength', 'Pet', [], 3, 9) ]
158159
);
159160
});
160161

161-
it('Defined on implmentors but not on interface', () => {
162+
it('Defined on implementors but not on interface', () => {
162163
expectFailsRule(FieldsOnCorrectType, `
163164
fragment definedOnImplementorsButNotInterface on Pet {
164165
nickname
165166
}`,
166-
[ undefinedField('nickname', 'Pet', 3, 9) ]
167+
[ undefinedField('nickname', 'Pet', [ 'Cat', 'Dog' ], 3, 9) ]
167168
);
168169
});
169170

@@ -180,7 +181,7 @@ describe('Validate: Fields on correct type', () => {
180181
fragment directFieldSelectionOnUnion on CatOrDog {
181182
directField
182183
}`,
183-
[ undefinedField('directField', 'CatOrDog', 3, 9) ]
184+
[ undefinedField('directField', 'CatOrDog', [], 3, 9) ]
184185
);
185186
});
186187

@@ -189,7 +190,15 @@ describe('Validate: Fields on correct type', () => {
189190
fragment definedOnImplementorsQueriedOnUnion on CatOrDog {
190191
name
191192
}`,
192-
[ undefinedField('name', 'CatOrDog', 3, 9) ]
193+
[
194+
undefinedField(
195+
'name',
196+
'CatOrDog',
197+
[ 'Being', 'Pet', 'Canine', 'Cat', 'Dog' ],
198+
3,
199+
9
200+
)
201+
]
193202
);
194203
});
195204

@@ -206,4 +215,29 @@ describe('Validate: Fields on correct type', () => {
206215
`);
207216
});
208217

218+
describe('Fields on correct type error message', () => {
219+
it('Works with no suggestions', () => {
220+
expect(
221+
undefinedFieldMessage('T', 'f', [])
222+
).to.equal('Cannot query field "T" on type "f".');
223+
});
224+
225+
it('Works with no small numbers of suggestions', () => {
226+
expect(
227+
undefinedFieldMessage('T', 'f', [ 'A', 'B' ])
228+
).to.equal('Cannot query field "T" on type "f". ' +
229+
'However, this field exists on "A", "B". ' +
230+
'Perhaps you meant to use an inline fragment?');
231+
});
232+
233+
it('Works with lots of suggestions', () => {
234+
expect(
235+
undefinedFieldMessage('T', 'f', [ 'A', 'B', 'C', 'D', 'E', 'F' ])
236+
).to.equal('Cannot query field "T" on type "f". ' +
237+
'However, this field exists on "A", "B", "C", "D", "E", ' +
238+
'and 1 other types. ' +
239+
'Perhaps you meant to use an inline fragment?');
240+
});
241+
});
209242
});
243+

src/validation/__tests__/harness.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,16 @@ const Pet = new GraphQLInterfaceType({
5353
}),
5454
});
5555

56+
const Canine = new GraphQLInterfaceType({
57+
name: 'Canine',
58+
fields: () => ({
59+
name: {
60+
type: GraphQLString,
61+
args: { surname: { type: GraphQLBoolean } },
62+
}
63+
}),
64+
});
65+
5666
const DogCommand = new GraphQLEnumType({
5767
name: 'DogCommand',
5868
values: {
@@ -93,7 +103,7 @@ const Dog = new GraphQLObjectType({
93103
args: { x: { type: GraphQLInt }, y: { type: GraphQLInt } },
94104
},
95105
}),
96-
interfaces: [ Being, Pet ],
106+
interfaces: [ Being, Pet, Canine ],
97107
});
98108

99109
const Cat = new GraphQLObjectType({

src/validation/__tests__/validation.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,9 @@ describe('Validate: Supports full validation', () => {
6565
);
6666

6767
expect(errors).to.deep.equal([
68-
{ message: 'Cannot query field "catOrDog" on "QueryRoot".' },
69-
{ message: 'Cannot query field "furColor" on "Cat".' },
70-
{ message: 'Cannot query field "isHousetrained" on "Dog".' }
68+
{ message: 'Cannot query field "catOrDog" on type "QueryRoot".' },
69+
{ message: 'Cannot query field "furColor" on type "Cat".' },
70+
{ message: 'Cannot query field "isHousetrained" on type "Dog".' }
7171
]);
7272
});
7373

src/validation/rules/FieldsOnCorrectType.js

Lines changed: 74 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,31 @@
1111
import type { ValidationContext } from '../index';
1212
import { GraphQLError } from '../../error';
1313
import type { Field } from '../../language/ast';
14-
import type { GraphQLType } from '../../type/definition';
15-
14+
import {
15+
isAbstractType,
16+
GraphQLAbstractType,
17+
GraphQLObjectType,
18+
} from '../../type/definition';
1619

1720
export function undefinedFieldMessage(
1821
fieldName: string,
19-
type: GraphQLType
22+
type: string,
23+
suggestedTypes: Array<string>
2024
): string {
21-
return `Cannot query field "${fieldName}" on "${type}".`;
25+
let message = `Cannot query field "${fieldName}" on type "${type}".`;
26+
const MAX_LENGTH = 5;
27+
if (suggestedTypes.length !== 0) {
28+
let suggestions = suggestedTypes
29+
.slice(0, MAX_LENGTH)
30+
.map(t => `"${t}"`)
31+
.join(', ');
32+
if (suggestedTypes.length > MAX_LENGTH) {
33+
suggestions += `, and ${suggestedTypes.length - MAX_LENGTH} other types`;
34+
}
35+
message += ` However, this field exists on ${suggestions}.`;
36+
message += ` Perhaps you meant to use an inline fragment?`;
37+
}
38+
return message;
2239
}
2340

2441
/**
@@ -34,12 +51,64 @@ export function FieldsOnCorrectType(context: ValidationContext): any {
3451
if (type) {
3552
const fieldDef = context.getFieldDef();
3653
if (!fieldDef) {
54+
// This isn't valid. Let's find suggestions, if any.
55+
let suggestedTypes = [];
56+
if (isAbstractType(type)) {
57+
suggestedTypes =
58+
getSiblingInterfacesIncludingField(type, node.name.value);
59+
suggestedTypes = suggestedTypes.concat(
60+
getImplementationsIncludingField(type, node.name.value)
61+
);
62+
}
3763
context.reportError(new GraphQLError(
38-
undefinedFieldMessage(node.name.value, type.name),
64+
undefinedFieldMessage(node.name.value, type.name, suggestedTypes),
3965
[ node ]
4066
));
4167
}
4268
}
4369
}
4470
};
4571
}
72+
73+
/**
74+
* Return implementations of `type` that include `fieldName` as a valid field.
75+
*/
76+
function getImplementationsIncludingField(
77+
type: GraphQLAbstractType,
78+
fieldName: string
79+
): Array<string> {
80+
return type.getPossibleTypes()
81+
.filter(t => t.getFields()[fieldName] !== undefined)
82+
.map(t => t.name)
83+
.sort();
84+
}
85+
86+
/**
87+
* Go through all of the implementations of type, and find other interaces
88+
* that they implement. If those interfaces include `field` as a valid field,
89+
* return them, sorted by how often the implementations include the other
90+
* interface.
91+
*/
92+
function getSiblingInterfacesIncludingField(
93+
type: GraphQLAbstractType,
94+
fieldName: string
95+
): Array<string> {
96+
const implementingObjects = type.getPossibleTypes()
97+
.filter(t => t instanceof GraphQLObjectType);
98+
99+
const suggestedInterfaces = implementingObjects.reduce((acc, t) => {
100+
t.getInterfaces().forEach(i => {
101+
if (i.getFields()[fieldName] === undefined) {
102+
return;
103+
}
104+
if (acc[i.name] === undefined) {
105+
acc[i.name] = 0;
106+
}
107+
acc[i.name] += 1;
108+
});
109+
return acc;
110+
}, {});
111+
return Object.keys(suggestedInterfaces)
112+
.sort((a,b) => suggestedInterfaces[b] - suggestedInterfaces[a]);
113+
}
114+

0 commit comments

Comments
 (0)