/
FieldsOnCorrectTypeRule.js
142 lines (125 loc) · 4.23 KB
/
FieldsOnCorrectTypeRule.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
import { didYouMean } from '../../jsutils/didYouMean';
import { suggestionList } from '../../jsutils/suggestionList';
import { naturalCompare } from '../../jsutils/naturalCompare';
import { GraphQLError } from '../../error/GraphQLError';
import type { FieldNode } from '../../language/ast';
import type { ASTVisitor } from '../../language/visitor';
import type { GraphQLSchema } from '../../type/schema';
import type {
GraphQLOutputType,
GraphQLObjectType,
GraphQLInterfaceType,
} from '../../type/definition';
import {
isObjectType,
isInterfaceType,
isAbstractType,
} from '../../type/definition';
import type { ValidationContext } from '../ValidationContext';
/**
* Fields on correct type
*
* A GraphQL document is only valid if all fields selected are defined by the
* parent type, or are an allowed meta field such as __typename.
*/
export function FieldsOnCorrectTypeRule(
context: ValidationContext,
): ASTVisitor {
return {
Field(node: FieldNode) {
const type = context.getParentType();
if (type) {
const fieldDef = context.getFieldDef();
if (!fieldDef) {
// This field doesn't exist, lets look for suggestions.
const schema = context.getSchema();
const fieldName = node.name.value;
// First determine if there are any suggested types to condition on.
let suggestion = didYouMean(
'to use an inline fragment on',
getSuggestedTypeNames(schema, type, fieldName),
);
// If there are no suggested types, then perhaps this was a typo?
if (suggestion === '') {
suggestion = didYouMean(getSuggestedFieldNames(type, fieldName));
}
// Report an error, including helpful suggestions.
context.reportError(
new GraphQLError(
`Cannot query field "${fieldName}" on type "${type.name}".` +
suggestion,
node,
),
);
}
}
},
};
}
/**
* Go through all of the implementations of type, as well as the interfaces that
* they implement. If any of those types include the provided field, suggest them,
* sorted by how often the type is referenced.
*/
function getSuggestedTypeNames(
schema: GraphQLSchema,
type: GraphQLOutputType,
fieldName: string,
): Array<string> {
if (!isAbstractType(type)) {
// Must be an Object type, which does not have possible fields.
return [];
}
const suggestedTypes: Set<GraphQLObjectType | GraphQLInterfaceType> =
new Set();
const usageCount = Object.create(null);
for (const possibleType of schema.getPossibleTypes(type)) {
if (!possibleType.getFields()[fieldName]) {
continue;
}
// This object type defines this field.
suggestedTypes.add(possibleType);
usageCount[possibleType.name] = 1;
for (const possibleInterface of possibleType.getInterfaces()) {
if (!possibleInterface.getFields()[fieldName]) {
continue;
}
// This interface type defines this field.
suggestedTypes.add(possibleInterface);
usageCount[possibleInterface.name] =
(usageCount[possibleInterface.name] ?? 0) + 1;
}
}
return [...suggestedTypes]
.sort((typeA, typeB) => {
// Suggest both interface and object types based on how common they are.
const usageCountDiff = usageCount[typeB.name] - usageCount[typeA.name];
if (usageCountDiff !== 0) {
return usageCountDiff;
}
// Suggest super types first followed by subtypes
if (isInterfaceType(typeA) && schema.isSubType(typeA, typeB)) {
return -1;
}
if (isInterfaceType(typeB) && schema.isSubType(typeB, typeA)) {
return 1;
}
return naturalCompare(typeA.name, typeB.name);
})
.map((x) => x.name);
}
/**
* For the field name provided, determine if there are any similar field names
* that may be the result of a typo.
*/
function getSuggestedFieldNames(
type: GraphQLOutputType,
fieldName: string,
): Array<string> {
if (isObjectType(type) || isInterfaceType(type)) {
const possibleFieldNames = Object.keys(type.getFields());
return suggestionList(fieldName, possibleFieldNames);
}
// Otherwise, must be a Union type, which does not define fields.
return [];
}