Skip to content

Commit 1441d0e

Browse files
committed
Validate directive arguments inside SDL
1 parent 06b246f commit 1441d0e

File tree

3 files changed

+136
-65
lines changed

3 files changed

+136
-65
lines changed

src/validation/rules/KnownArgumentNames.js

+70-40
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,14 @@
77
* @flow strict
88
*/
99

10-
import type { ValidationContext } from '../ValidationContext';
10+
import type {
11+
ValidationContext,
12+
SDLValidationContext,
13+
} from '../ValidationContext';
1114
import { GraphQLError } from '../../error/GraphQLError';
1215
import type { ASTVisitor } from '../../language/visitor';
16+
import { visitInParallel } from '../../language/visitor';
17+
import invariant from '../../jsutils/invariant';
1318
import suggestionList from '../../jsutils/suggestionList';
1419
import quotedOrList from '../../jsutils/quotedOrList';
1520
import { Kind } from '../../language/kinds';
@@ -48,47 +53,72 @@ export function unknownDirectiveArgMessage(
4853
* that field.
4954
*/
5055
export function KnownArgumentNames(context: ValidationContext): ASTVisitor {
56+
return visitInParallel([
57+
KnownArgumentNamesOnDirectives(context),
58+
{
59+
Directive: () => false,
60+
Argument(node) {
61+
const argName = node.name.value;
62+
const argDef = context.getArgument();
63+
const fieldDef = context.getFieldDef();
64+
const parentType = context.getParentType();
65+
if (!argDef && fieldDef && parentType) {
66+
context.reportError(
67+
new GraphQLError(
68+
unknownArgMessage(
69+
argName,
70+
fieldDef.name,
71+
parentType.name,
72+
suggestionList(argName, fieldDef.args.map(arg => arg.name)),
73+
),
74+
[node],
75+
),
76+
);
77+
}
78+
},
79+
},
80+
]);
81+
}
82+
83+
// @internal
84+
export function KnownArgumentNamesOnDirectives(
85+
context: ValidationContext | SDLValidationContext,
86+
): ASTVisitor {
87+
const directiveArgs = Object.create(null);
88+
const schema = context.getSchema();
89+
90+
if (schema) {
91+
for (const directive of schema.getDirectives()) {
92+
directiveArgs[directive.name] = directive.args.map(arg => arg.name);
93+
}
94+
}
95+
96+
const astDefinitions = context.getDocument().definitions;
97+
for (const def of astDefinitions) {
98+
if (def.kind === Kind.DIRECTIVE_DEFINITION) {
99+
const name = def.name.value;
100+
const args = def.arguments || [];
101+
directiveArgs[name] = args.map(arg => arg.name.value);
102+
}
103+
}
104+
51105
return {
52106
Argument(node, key, parent, path, ancestors) {
53-
const argDef = context.getArgument();
54-
if (!argDef) {
55-
const argumentOf = ancestors[ancestors.length - 1];
56-
if (argumentOf.kind === Kind.FIELD) {
57-
const fieldDef = context.getFieldDef();
58-
const parentType = context.getParentType();
59-
if (fieldDef && parentType) {
60-
context.reportError(
61-
new GraphQLError(
62-
unknownArgMessage(
63-
node.name.value,
64-
fieldDef.name,
65-
parentType.name,
66-
suggestionList(
67-
node.name.value,
68-
fieldDef.args.map(arg => arg.name),
69-
),
70-
),
71-
[node],
72-
),
73-
);
74-
}
75-
} else if (argumentOf.kind === Kind.DIRECTIVE) {
76-
const directive = context.getDirective();
77-
if (directive) {
78-
context.reportError(
79-
new GraphQLError(
80-
unknownDirectiveArgMessage(
81-
node.name.value,
82-
directive.name,
83-
suggestionList(
84-
node.name.value,
85-
directive.args.map(arg => arg.name),
86-
),
87-
),
88-
[node],
89-
),
90-
);
91-
}
107+
const argumentOf = ancestors[ancestors.length - 1];
108+
invariant(!Array.isArray(argumentOf));
109+
110+
if (argumentOf.kind === Kind.DIRECTIVE) {
111+
const argName = node.name.value;
112+
const directiveName = argumentOf.name.value;
113+
const args = directiveArgs[directiveName];
114+
if (args && args.indexOf(argName) === -1) {
115+
const suggestions = suggestionList(argName, args);
116+
context.reportError(
117+
new GraphQLError(
118+
unknownDirectiveArgMessage(argName, directiveName, suggestions),
119+
[node],
120+
),
121+
);
92122
}
93123
}
94124
},

src/validation/rules/ProvidedRequiredArguments.js

+62-25
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@
77
* @flow strict
88
*/
99

10-
import type { ValidationContext } from '../ValidationContext';
10+
import type {
11+
ValidationContext,
12+
SDLValidationContext,
13+
} from '../ValidationContext';
1114
import { GraphQLError } from '../../error/GraphQLError';
15+
import { Kind } from '../../language/kinds';
1216
import inspect from '../../jsutils/inspect';
1317
import keyMap from '../../jsutils/keyMap';
1418
import { isRequiredArgument } from '../../type/definition';
@@ -28,12 +32,13 @@ export function missingFieldArgMessage(
2832
export function missingDirectiveArgMessage(
2933
directiveName: string,
3034
argName: string,
31-
type: string,
35+
type?: ?string,
3236
): string {
33-
return (
34-
`Directive "@${directiveName}" argument "${argName}" of type ` +
35-
`"${type}" is required but not provided.`
36-
);
37+
let msg = `Directive "@${directiveName}" argument "${argName}"`;
38+
if (type) {
39+
msg += ' of type ' + type;
40+
}
41+
return msg + ' is required but not provided.';
3742
}
3843

3944
/**
@@ -46,12 +51,13 @@ export function ProvidedRequiredArguments(
4651
context: ValidationContext,
4752
): ASTVisitor {
4853
return {
54+
...ProvidedRequiredArgumentsOnDirectives(context),
4955
Field: {
5056
// Validate on leave to allow for deeper errors to appear first.
5157
leave(node) {
5258
const fieldDef = context.getFieldDef();
5359
if (!fieldDef) {
54-
return false;
60+
return;
5561
}
5662
const argNodes = node.arguments || [];
5763

@@ -73,30 +79,61 @@ export function ProvidedRequiredArguments(
7379
}
7480
},
7581
},
82+
};
83+
}
7684

85+
// @internal
86+
export function ProvidedRequiredArgumentsOnDirectives(
87+
context: ValidationContext | SDLValidationContext,
88+
): ASTVisitor {
89+
const requiredArgsMap = Object.create(null);
90+
const schema = context.getSchema();
91+
92+
if (schema) {
93+
for (const directive of schema.getDirectives()) {
94+
const requredArgs = directive.args.filter(isRequiredArgument);
95+
requiredArgsMap[directive.name] = requredArgs.map(arg => arg.name);
96+
}
97+
}
98+
99+
const astDefinitions = context.getDocument().definitions;
100+
for (const def of astDefinitions) {
101+
if (def.kind === Kind.DIRECTIVE_DEFINITION) {
102+
const requredArgs = (def.arguments || []).filter(
103+
arg => arg.type.kind === Kind.NON_NULL_TYPE && arg.defaultValue == null,
104+
);
105+
106+
requiredArgsMap[def.name.value] = requredArgs.map(arg => arg.name.value);
107+
}
108+
}
109+
110+
return {
77111
Directive: {
78112
// Validate on leave to allow for deeper errors to appear first.
79113
leave(node) {
80-
const directiveDef = context.getDirective();
81-
if (!directiveDef) {
82-
return false;
83-
}
84-
const argNodes = node.arguments || [];
114+
const directiveName = node.name.value;
115+
const requiredArgs = requiredArgsMap[directiveName];
116+
if (requiredArgs) {
117+
const argNodes = node.arguments || [];
118+
const argNodeMap = keyMap(argNodes, arg => arg.name.value);
119+
for (const argName of requiredArgs) {
120+
if (!argNodeMap[argName]) {
121+
const directiveDef = schema && schema.getDirective(directiveName);
122+
const argDef =
123+
directiveDef &&
124+
directiveDef.args.find(arg => arg.name === argName);
85125

86-
const argNodeMap = keyMap(argNodes, arg => arg.name.value);
87-
for (const argDef of directiveDef.args) {
88-
const argNode = argNodeMap[argDef.name];
89-
if (!argNode && isRequiredArgument(argDef)) {
90-
context.reportError(
91-
new GraphQLError(
92-
missingDirectiveArgMessage(
93-
node.name.value,
94-
argDef.name,
95-
inspect(argDef.type),
126+
context.reportError(
127+
new GraphQLError(
128+
missingDirectiveArgMessage(
129+
directiveName,
130+
argName,
131+
argDef && inspect(argDef.type),
132+
),
133+
[node],
96134
),
97-
[node],
98-
),
99-
);
135+
);
136+
}
100137
}
101138
}
102139
},

src/validation/specifiedRules.js

+4
Original file line numberDiff line numberDiff line change
@@ -123,12 +123,16 @@ export const specifiedRules: $ReadOnlyArray<ValidationRule> = [
123123
];
124124

125125
import { LoneSchemaDefinition } from './rules/LoneSchemaDefinition';
126+
import { KnownArgumentNamesOnDirectives } from './rules/KnownArgumentNames';
127+
import { ProvidedRequiredArgumentsOnDirectives } from './rules/ProvidedRequiredArguments';
126128

127129
// @internal
128130
export const specifiedSDLRules: $ReadOnlyArray<SDLValidationRule> = [
129131
LoneSchemaDefinition,
130132
KnownDirectives,
131133
UniqueDirectivesPerLocation,
134+
KnownArgumentNamesOnDirectives,
132135
UniqueArgumentNames,
133136
UniqueInputFieldNames,
137+
ProvidedRequiredArgumentsOnDirectives,
134138
];

0 commit comments

Comments
 (0)