UNPKG

9.04 kBPlain TextView Raw
1import {
2 specifiedRules,
3 NoUnusedFragmentsRule,
4 GraphQLError,
5 FieldNode,
6 ValidationContext,
7 GraphQLSchema,
8 DocumentNode,
9 OperationDefinitionNode,
10 TypeInfo,
11 FragmentDefinitionNode,
12 visit,
13 visitWithTypeInfo,
14 visitInParallel,
15 getLocation,
16 InlineFragmentNode,
17 Kind,
18 isObjectType,
19} from "graphql";
20
21import { TextEdit } from "vscode-languageserver";
22
23import { ToolError, logError } from "./logger";
24import { ValidationRule } from "graphql/validation/ValidationContext";
25import { positionFromSourceLocation } from "../utilities/source";
26import {
27 buildExecutionContext,
28 ExecutionContext,
29} from "graphql/execution/execute";
30import { hasClientDirective, simpleCollectFields } from "../utilities/graphql";
31import { Debug } from "../utilities";
32
33export interface CodeActionInfo {
34 message: string;
35 edits: TextEdit[];
36}
37
38const specifiedRulesToBeRemoved = [NoUnusedFragmentsRule];
39
40export const defaultValidationRules: ValidationRule[] = [
41 NoAnonymousQueries,
42 NoTypenameAlias,
43 NoMissingClientDirectives,
44 ...specifiedRules.filter((rule) => !specifiedRulesToBeRemoved.includes(rule)),
45];
46
47export function getValidationErrors(
48 schema: GraphQLSchema,
49 document: DocumentNode,
50 fragments?: { [fragmentName: string]: FragmentDefinitionNode },
51 rules: ValidationRule[] = defaultValidationRules
52) {
53 const typeInfo = new TypeInfo(schema);
54
55 // The 4th argument to `ValidationContext` is an `onError` callback. This was
56 // introduced by https://github.com/graphql/graphql-js/pull/2074 and first
57 // published in graphql@14.5.0. It is meant to replace the `getErrors` method
58 // which was previously used. Since we support versions of graphql older than
59 // that, it's possible that this callback will not be invoked and we'll need
60 // to resort to using `getErrors`. Therefore, although we'll collect errors
61 // via this callback, if `getErrors` is present on the context we create,
62 // we'll go ahead and use that instead.
63 const errors: GraphQLError[] = [];
64 const onError = (err: GraphQLError) => errors.push(err);
65 const context = new ValidationContext(schema, document, typeInfo, onError);
66
67 if (fragments) {
68 (context as any)._fragments = fragments;
69 }
70
71 const visitors = rules.map((rule) => rule(context));
72 // Visit the whole document with each instance of all provided rules.
73 visit(document, visitWithTypeInfo(typeInfo, visitInParallel(visitors)));
74
75 // @ts-ignore
76 // `getErrors` is gone in `graphql@15`, but we still support older versions.
77 if (typeof context.getErrors === "function") return context.getErrors();
78
79 // If `getErrors` doesn't exist, we must be on a `graphql@15` or higher,
80 // so we'll use the errors we collected via the `onError` callback.
81 return errors;
82}
83
84export function validateQueryDocument(
85 schema: GraphQLSchema,
86 document: DocumentNode
87) {
88 try {
89 const validationErrors = getValidationErrors(schema, document);
90 if (validationErrors && validationErrors.length > 0) {
91 for (const error of validationErrors) {
92 logError(error);
93 }
94 return Debug.error("Validation of GraphQL query document failed");
95 }
96 } catch (e) {
97 console.error(e);
98 throw e;
99 }
100}
101
102export function NoAnonymousQueries(context: ValidationContext) {
103 return {
104 OperationDefinition(node: OperationDefinitionNode) {
105 if (!node.name) {
106 context.reportError(
107 new GraphQLError("Apollo does not support anonymous operations", [
108 node,
109 ])
110 );
111 }
112 return false;
113 },
114 };
115}
116
117export function NoTypenameAlias(context: ValidationContext) {
118 return {
119 Field(node: FieldNode) {
120 const aliasName = node.alias && node.alias.value;
121 if (aliasName == "__typename") {
122 context.reportError(
123 new GraphQLError(
124 "Apollo needs to be able to insert __typename when needed, please do not use it as an alias",
125 [node]
126 )
127 );
128 }
129 },
130 };
131}
132
133function hasClientSchema(schema: GraphQLSchema): boolean {
134 const query = schema.getQueryType();
135 const mutation = schema.getMutationType();
136 const subscription = schema.getSubscriptionType();
137
138 return Boolean(
139 (query && query.clientSchema) ||
140 (mutation && mutation.clientSchema) ||
141 (subscription && subscription.clientSchema)
142 );
143}
144
145export function NoMissingClientDirectives(context: ValidationContext) {
146 const root = context.getDocument();
147 const schema = context.getSchema();
148 // early return if we don't have any client fields on the schema
149 if (!hasClientSchema(schema)) return {};
150
151 // this isn't really execution context, but it does group the fragments and operations
152 // together correctly
153 // XXX we have a simplified version of this in @apollo/gateway that we could probably use
154 // intead of this
155 const executionContext = buildExecutionContext(
156 schema,
157 root,
158 Object.create(null),
159 Object.create(null),
160 undefined,
161 undefined,
162 undefined
163 );
164 function visitor(
165 node: FieldNode | InlineFragmentNode | FragmentDefinitionNode
166 ) {
167 // In cases where we are looking at a FragmentDefinition, there is no parent type
168 // but instead, the FragmentDefinition contains the type that we can read from the
169 // schema
170 const parentType =
171 node.kind === Kind.FRAGMENT_DEFINITION
172 ? schema.getType(node.typeCondition.name.value)
173 : context.getParentType();
174
175 const fieldDef = context.getFieldDef();
176
177 // if we don't have a type to check then we can early return
178 if (!parentType) return;
179
180 // here we collect all of the fields on a type that are marked "local"
181 const clientFields =
182 parentType &&
183 isObjectType(parentType) &&
184 parentType.clientSchema &&
185 parentType.clientSchema.localFields;
186
187 // XXXX in the case of a fragment spread, the directive could be on the fragment definition
188 let clientDirectivePresent = hasClientDirective(node);
189
190 let message = "@client directive is missing on ";
191 let selectsClientFieldSet = false;
192 switch (node.kind) {
193 case Kind.FIELD:
194 // fields are simple because we can just see if the name exists in the local fields
195 // array on the parent type
196 selectsClientFieldSet = Boolean(
197 clientFields && clientFields.includes(fieldDef!.name)
198 );
199 message += `local field "${node.name.value}"`;
200 break;
201 case Kind.INLINE_FRAGMENT:
202 case Kind.FRAGMENT_DEFINITION:
203 // XXX why isn't this type checking below?
204 if (Array.isArray(executionContext)) break;
205
206 const fields = simpleCollectFields(
207 executionContext as ExecutionContext,
208 node.selectionSet,
209 Object.create(null),
210 Object.create(null)
211 );
212
213 // once we have a list of fields on the fragment, we can compare them
214 // to the list of types. The fields within a fragment need to be a
215 // subset of the overall local fields types
216 const fieldNames = Object.entries(fields).map(([name]) => name);
217 selectsClientFieldSet = fieldNames.every(
218 (field) => clientFields && clientFields.includes(field)
219 );
220 message += `fragment ${
221 "name" in node ? `"${node.name.value}" ` : ""
222 }around local fields "${fieldNames.join(",")}"`;
223 break;
224 }
225
226 // if the field's parent is part of the client schema and that type
227 // includes a field with the same name as this node, we can see
228 // if it has an @client directive to resolve locally
229 if (selectsClientFieldSet && !clientDirectivePresent) {
230 let extensions: { [key: string]: any } | null = null;
231 const name = "name" in node && node.name;
232 // TODO support code actions for inline fragments, fragment spreads, and fragment definitions
233 if (name && name.loc) {
234 let { source, end: locToInsertDirective } = name.loc;
235 if (
236 "arguments" in node &&
237 node.arguments &&
238 node.arguments.length !== 0
239 ) {
240 // must insert directive after field arguments
241 const endOfArgs = source.body.indexOf(")", locToInsertDirective);
242 locToInsertDirective = endOfArgs + 1;
243 }
244 const codeAction: CodeActionInfo = {
245 message: `Add @client directive to "${name.value}"`,
246 edits: [
247 TextEdit.insert(
248 positionFromSourceLocation(
249 source,
250 getLocation(source, locToInsertDirective)
251 ),
252 " @client"
253 ),
254 ],
255 };
256 extensions = { codeAction };
257 }
258
259 context.reportError(
260 new GraphQLError(message, [node], null, null, null, null, extensions)
261 );
262 }
263
264 // if we have selected a client field, no need to continue to recurse
265 if (selectsClientFieldSet) {
266 return false;
267 }
268
269 return;
270 }
271 return {
272 InlineFragment: visitor,
273 FragmentDefinition: visitor,
274 Field: visitor,
275 // TODO support directives on FragmentSpread
276 };
277}
278
\No newline at end of file