Skip to content

Commit 5f56c73

Browse files
committed
Validate directive arguments inside SDL
1 parent 0464006 commit 5f56c73

File tree

4 files changed

+137
-60
lines changed

4 files changed

+137
-60
lines changed

src/validation/rules/KnownArgumentNames.js

+64-35
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,16 @@
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';
1316
import suggestionList from '../../jsutils/suggestionList';
1417
import quotedOrList from '../../jsutils/quotedOrList';
1518
import { Kind } from '../../language/kinds';
19+
import { specifiedDirectives } from '../../type/directives';
1620

1721
export function unknownArgMessage(
1822
argName: string,
@@ -49,48 +53,73 @@ export function unknownDirectiveArgMessage(
4953
*/
5054
export function KnownArgumentNames(context: ValidationContext): ASTVisitor {
5155
return {
52-
Argument(node, key, parent, path, ancestors) {
56+
...KnownArgumentNamesOnDirectives(context),
57+
Argument(node) {
58+
const argName = node.name.value;
5359
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) {
60+
const fieldDef = context.getFieldDef();
61+
const parentType = context.getParentType();
62+
if (!argDef && fieldDef && parentType) {
63+
context.reportError(
64+
new GraphQLError(
65+
unknownArgMessage(
66+
argName,
67+
fieldDef.name,
68+
parentType.name,
69+
suggestionList(argName, fieldDef.args.map(arg => arg.name)),
70+
),
71+
[node],
72+
),
73+
);
74+
}
75+
},
76+
};
77+
}
78+
79+
// @internal
80+
export function KnownArgumentNamesOnDirectives(
81+
context: ValidationContext | SDLValidationContext,
82+
): ASTVisitor {
83+
const directiveArgs = Object.create(null);
84+
85+
const schema = context.getSchema();
86+
const definedDirectives = schema
87+
? schema.getDirectives()
88+
: specifiedDirectives;
89+
for (const directive of definedDirectives) {
90+
directiveArgs[directive.name] = directive.args.map(arg => arg.name);
91+
}
92+
93+
const astDefinitions = context.getDocument().definitions;
94+
for (const def of astDefinitions) {
95+
if (def.kind === Kind.DIRECTIVE_DEFINITION) {
96+
directiveArgs[def.name.value] = def.arguments
97+
? def.arguments.map(arg => arg.name.value)
98+
: [];
99+
}
100+
}
101+
102+
return {
103+
Directive(directiveNode) {
104+
const directiveName = directiveNode.name.value;
105+
const knownArgs = directiveArgs[directiveName];
106+
107+
if (directiveNode.arguments && knownArgs) {
108+
for (const argNode of directiveNode.arguments) {
109+
const argName = argNode.name.value;
110+
if (knownArgs.indexOf(argName) === -1) {
111+
const suggestions = suggestionList(argName, knownArgs);
78112
context.reportError(
79113
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],
114+
unknownDirectiveArgMessage(argName, directiveName, suggestions),
115+
[argNode],
89116
),
90117
);
91118
}
92119
}
93120
}
121+
122+
return false;
94123
},
95124
};
96125
}

src/validation/rules/KnownDirectives.js

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export function KnownDirectives(
3838
context: ValidationContext | SDLValidationContext,
3939
): ASTVisitor {
4040
const locationsMap = Object.create(null);
41+
4142
const schema = context.getSchema();
4243
const definedDirectives = schema
4344
? schema.getDirectives()

src/validation/rules/ProvidedRequiredArguments.js

+68-25
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,17 @@
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';
1519
import type { ASTVisitor } from '../../language/visitor';
20+
import { specifiedDirectives } from '../../type/directives';
1621

1722
export function missingFieldArgMessage(
1823
fieldName: string,
@@ -28,12 +33,13 @@ export function missingFieldArgMessage(
2833
export function missingDirectiveArgMessage(
2934
directiveName: string,
3035
argName: string,
31-
type: string,
36+
type?: ?string,
3237
): string {
33-
return (
34-
`Directive "@${directiveName}" argument "${argName}" of type ` +
35-
`"${type}" is required but not provided.`
36-
);
38+
let msg = `Directive "@${directiveName}" argument "${argName}"`;
39+
if (type) {
40+
msg += ' of type ' + type;
41+
}
42+
return msg + ' is required but not provided.';
3743
}
3844

3945
/**
@@ -46,12 +52,13 @@ export function ProvidedRequiredArguments(
4652
context: ValidationContext,
4753
): ASTVisitor {
4854
return {
55+
...ProvidedRequiredArgumentsOnDirectives(context),
4956
Field: {
5057
// Validate on leave to allow for deeper errors to appear first.
5158
leave(node) {
5259
const fieldDef = context.getFieldDef();
5360
if (!fieldDef) {
54-
return false;
61+
return;
5562
}
5663
const argNodes = node.arguments || [];
5764

@@ -73,33 +80,69 @@ export function ProvidedRequiredArguments(
7380
}
7481
},
7582
},
83+
};
84+
}
7685

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

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),
128+
context.reportError(
129+
new GraphQLError(
130+
missingDirectiveArgMessage(
131+
directiveName,
132+
argName,
133+
argDef && inspect(argDef.type),
134+
),
135+
[node],
96136
),
97-
[node],
98-
),
99-
);
137+
);
138+
}
100139
}
101140
}
102141
},
103142
},
104143
};
105144
}
145+
146+
function isRequiredArgumentNode(arg) {
147+
return arg.type.kind === Kind.NON_NULL_TYPE && arg.defaultValue == null;
148+
}

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)