Skip to content

Commit 0da6740

Browse files
committed
[validator] Add suggested types to incorrect field message
1 parent 509c85d commit 0da6740

File tree

3 files changed

+98
-24
lines changed

3 files changed

+98
-24
lines changed

src/validation/__tests__/FieldsOnCorrectType.js

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ import {
1515
} from '../rules/FieldsOnCorrectType';
1616

1717

18-
function undefinedField(field, type, line, column) {
18+
function undefinedField(field, type, suggestions, line, column) {
1919
return {
20-
message: undefinedFieldMessage(field, type),
20+
message: undefinedFieldMessage(field, type, suggestions),
2121
locations: [ { line, column } ],
2222
};
2323
}
@@ -84,8 +84,8 @@ describe('Validate: Fields on correct type', () => {
8484
}
8585
}
8686
}`,
87-
[ undefinedField('unknown_pet_field', 'Pet', 3, 9),
88-
undefinedField('unknown_cat_field', 'Cat', 5, 13) ]
87+
[ undefinedField('unknown_pet_field', 'Pet', [], 3, 9),
88+
undefinedField('unknown_cat_field', 'Cat', [], 5, 13) ]
8989
);
9090
});
9191

@@ -94,7 +94,7 @@ describe('Validate: Fields on correct type', () => {
9494
fragment fieldNotDefined on Dog {
9595
meowVolume
9696
}`,
97-
[ undefinedField('meowVolume', 'Dog', 3, 9) ]
97+
[ undefinedField('meowVolume', 'Dog', [], 3, 9) ]
9898
);
9999
});
100100

@@ -105,7 +105,7 @@ describe('Validate: Fields on correct type', () => {
105105
deeper_unknown_field
106106
}
107107
}`,
108-
[ undefinedField('unknown_field', 'Dog', 3, 9) ]
108+
[ undefinedField('unknown_field', 'Dog', [], 3, 9) ]
109109
);
110110
});
111111

@@ -116,7 +116,7 @@ describe('Validate: Fields on correct type', () => {
116116
unknown_field
117117
}
118118
}`,
119-
[ undefinedField('unknown_field', 'Pet', 4, 11) ]
119+
[ undefinedField('unknown_field', 'Pet', [], 4, 11) ]
120120
);
121121
});
122122

@@ -127,7 +127,7 @@ describe('Validate: Fields on correct type', () => {
127127
meowVolume
128128
}
129129
}`,
130-
[ undefinedField('meowVolume', 'Dog', 4, 11) ]
130+
[ undefinedField('meowVolume', 'Dog', [], 4, 11) ]
131131
);
132132
});
133133

@@ -136,7 +136,7 @@ describe('Validate: Fields on correct type', () => {
136136
fragment aliasedFieldTargetNotDefined on Dog {
137137
volume : mooVolume
138138
}`,
139-
[ undefinedField('mooVolume', 'Dog', 3, 9) ]
139+
[ undefinedField('mooVolume', 'Dog', [], 3, 9) ]
140140
);
141141
});
142142

@@ -145,7 +145,7 @@ describe('Validate: Fields on correct type', () => {
145145
fragment aliasedLyingFieldTargetNotDefined on Dog {
146146
barkVolume : kawVolume
147147
}`,
148-
[ undefinedField('kawVolume', 'Dog', 3, 9) ]
148+
[ undefinedField('kawVolume', 'Dog', [], 3, 9) ]
149149
);
150150
});
151151

@@ -154,16 +154,16 @@ describe('Validate: Fields on correct type', () => {
154154
fragment notDefinedOnInterface on Pet {
155155
tailLength
156156
}`,
157-
[ undefinedField('tailLength', 'Pet', 3, 9) ]
157+
[ undefinedField('tailLength', 'Pet', [], 3, 9) ]
158158
);
159159
});
160160

161-
it('Defined on implmentors but not on interface', () => {
161+
it('Defined on implementors but not on interface', () => {
162162
expectFailsRule(FieldsOnCorrectType, `
163163
fragment definedOnImplementorsButNotInterface on Pet {
164164
nickname
165165
}`,
166-
[ undefinedField('nickname', 'Pet', 3, 9) ]
166+
[ undefinedField('nickname', 'Pet', ['Cat', 'Dog'], 3, 9) ]
167167
);
168168
});
169169

@@ -180,7 +180,7 @@ describe('Validate: Fields on correct type', () => {
180180
fragment directFieldSelectionOnUnion on CatOrDog {
181181
directField
182182
}`,
183-
[ undefinedField('directField', 'CatOrDog', 3, 9) ]
183+
[ undefinedField('directField', 'CatOrDog', [], 3, 9) ]
184184
);
185185
});
186186

@@ -189,7 +189,15 @@ describe('Validate: Fields on correct type', () => {
189189
fragment definedOnImplementorsQueriedOnUnion on CatOrDog {
190190
name
191191
}`,
192-
[ undefinedField('name', 'CatOrDog', 3, 9) ]
192+
[
193+
undefinedField(
194+
'name',
195+
'CatOrDog',
196+
['Being', 'Pet', 'Cat', 'Dog'],
197+
3,
198+
9
199+
)
200+
]
193201
);
194202
});
195203

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: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,70 @@ import type { ValidationContext } from '../index';
1212
import { GraphQLError } from '../../error';
1313
import type { Field } from '../../language/ast';
1414
import type { GraphQLType } from '../../type/definition';
15-
15+
import {
16+
isAbstractType,
17+
GraphQLAbstractType,
18+
GraphQLObjectType,
19+
} from '../../type/definition';
1620

1721
export function undefinedFieldMessage(
18-
fieldName: string,
19-
type: GraphQLType
20-
): string {
21-
return `Cannot query field "${fieldName}" on "${type}".`;
22+
field: string,
23+
type: string,
24+
suggestedTypes: Array<string>): string {
25+
var message = `Cannot query field "${field}" on type "${type}".`;
26+
var MAX_LENGTH = 5;
27+
if (suggestedTypes.length !== 0) {
28+
var 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;
39+
}
40+
41+
/**
42+
* Return implementations of `type` that include `fieldName` as a valid field.
43+
*/
44+
function getImplementationsIncludingField(
45+
type: GraphQLAbstractType,
46+
fieldName: string): Array<string> {
47+
return type.getPossibleTypes()
48+
.filter((t) => t.getFields()[fieldName] !== undefined)
49+
.map((t) => t.name)
50+
.sort();
51+
}
52+
53+
/**
54+
* Go through all of the implementations of type, and find other interaces
55+
* that they implement. If those interfaces include `field` as a valid field,
56+
* return them, sorted by how often the implementations include the other
57+
* interface.
58+
*/
59+
function getSiblingInterfacesIncludingField(
60+
type: GraphQLAbstractType,
61+
fieldName: string): Array<string> {
62+
var implementingObjects = type.getPossibleTypes()
63+
.filter((t) => (t instanceof GraphQLObjectType));
64+
65+
var suggestedInterfaces = implementingObjects.reduce((acc, t) => {
66+
t.getInterfaces().forEach((i) => {
67+
if (i.getFields()[fieldName] === undefined) {
68+
return;
69+
}
70+
if (acc[i.name] === undefined) {
71+
acc[i.name] = 0;
72+
}
73+
acc[i.name] = acc[i.name] + 1;
74+
});
75+
return acc;
76+
}, {});
77+
return Object.keys(suggestedInterfaces)
78+
.sort((a,b) => suggestedInterfaces[a] - suggestedInterfaces[b]);
2279
}
2380

2481
/**
@@ -34,8 +91,17 @@ export function FieldsOnCorrectType(context: ValidationContext): any {
3491
if (type) {
3592
const fieldDef = context.getFieldDef();
3693
if (!fieldDef) {
94+
// This isn't valid. Let's find suggestions, if any.
95+
var suggestedTypes = [];
96+
if (isAbstractType(type)) {
97+
suggestedTypes =
98+
getSiblingInterfacesIncludingField(type, node.name.value);
99+
suggestedTypes = suggestedTypes.concat(
100+
getImplementationsIncludingField(type, node.name.value)
101+
);
102+
}
37103
context.reportError(new GraphQLError(
38-
undefinedFieldMessage(node.name.value, type.name),
104+
undefinedFieldMessage(node.name.value, type.name, suggestedTypes),
39105
[ node ]
40106
));
41107
}

0 commit comments

Comments
 (0)