Skip to content

Commit 10bb6a9

Browse files
committed
Validate directive arguments inside SDL
1 parent 7cfd686 commit 10bb6a9

File tree

3 files changed

+137
-69
lines changed

3 files changed

+137
-69
lines changed

src/validation/rules/KnownArgumentNames.js

+69-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,71 @@ 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+
export function KnownArgumentNamesOnDirectives(
84+
context: ValidationContext | SDLValidationContext,
85+
): ASTVisitor {
86+
const directiveArgs = Object.create(null);
87+
const schema = context.getSchema();
88+
89+
if (schema) {
90+
for (const directive of schema.getDirectives()) {
91+
directiveArgs[directive.name] = directive.args.map(arg => arg.name);
92+
}
93+
}
94+
95+
const astDefinitions = context.getDocument().definitions;
96+
for (const def of astDefinitions) {
97+
if (def.kind === Kind.DIRECTIVE_DEFINITION) {
98+
const name = def.name.value;
99+
const args = def.arguments || [];
100+
directiveArgs[name] = args.map(arg => arg.name.value);
101+
}
102+
}
103+
51104
return {
52105
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-
}
106+
const argumentOf = ancestors[ancestors.length - 1];
107+
invariant(!Array.isArray(argumentOf));
108+
109+
if (argumentOf.kind === Kind.DIRECTIVE) {
110+
const argName = node.name.value;
111+
const directiveName = argumentOf.name.value;
112+
const args = directiveArgs[directiveName];
113+
if (args && args.indexOf(argName) === -1) {
114+
const suggestions = suggestionList(argName, args);
115+
context.reportError(
116+
new GraphQLError(
117+
unknownDirectiveArgMessage(argName, directiveName, suggestions),
118+
[node],
119+
),
120+
);
92121
}
93122
}
94123
},

src/validation/rules/ProvidedRequiredArguments.js

+64-29
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 { isNonNullType } 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

@@ -77,34 +83,63 @@ export function ProvidedRequiredArguments(
7783
}
7884
},
7985
},
86+
};
87+
}
88+
89+
// @internal
90+
export function ProvidedRequiredArgumentsOnDirectives(
91+
context: ValidationContext | SDLValidationContext,
92+
): ASTVisitor {
93+
const requiredArgsMap = Object.create(null);
94+
const schema = context.getSchema();
8095

96+
if (schema) {
97+
for (const directive of schema.getDirectives()) {
98+
const requredArgs = directive.args.filter(
99+
arg => isNonNullType(arg.type) && arg.defaultValue === undefined,
100+
);
101+
requiredArgsMap[directive.name] = requredArgs.map(arg => arg.name);
102+
}
103+
}
104+
105+
const astDefinitions = context.getDocument().definitions;
106+
for (const def of astDefinitions) {
107+
if (def.kind === Kind.DIRECTIVE_DEFINITION) {
108+
const requredArgs = (def.arguments || []).filter(
109+
arg => arg.type.kind === Kind.NON_NULL_TYPE && arg.defaultValue == null,
110+
);
111+
112+
requiredArgsMap[def.name.value] = requredArgs.map(arg => arg.name.value);
113+
}
114+
}
115+
116+
return {
81117
Directive: {
82118
// Validate on leave to allow for deeper errors to appear first.
83119
leave(node) {
84-
const directiveDef = context.getDirective();
85-
if (!directiveDef) {
86-
return false;
87-
}
88-
const argNodes = node.arguments || [];
120+
const directiveName = node.name.value;
121+
const requiredArgs = requiredArgsMap[directiveName];
122+
if (requiredArgs) {
123+
const argNodes = node.arguments || [];
124+
const argNodeMap = keyMap(argNodes, arg => arg.name.value);
125+
for (const argName of requiredArgs) {
126+
if (!argNodeMap[argName]) {
127+
const directiveDef = schema && schema.getDirective(directiveName);
128+
const argDef =
129+
directiveDef &&
130+
directiveDef.args.find(arg => arg.name === argName);
89131

90-
const argNodeMap = keyMap(argNodes, arg => arg.name.value);
91-
for (const argDef of directiveDef.args) {
92-
const argNode = argNodeMap[argDef.name];
93-
if (
94-
!argNode &&
95-
isNonNullType(argDef.type) &&
96-
argDef.defaultValue === undefined
97-
) {
98-
context.reportError(
99-
new GraphQLError(
100-
missingDirectiveArgMessage(
101-
node.name.value,
102-
argDef.name,
103-
inspect(argDef.type),
132+
context.reportError(
133+
new GraphQLError(
134+
missingDirectiveArgMessage(
135+
directiveName,
136+
argName,
137+
argDef && inspect(argDef.type),
138+
),
139+
[node],
104140
),
105-
[node],
106-
),
107-
);
141+
);
142+
}
108143
}
109144
}
110145
},

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)