Skip to content

Commit fdafe32

Browse files
committed
[RFC] Directives in schema language
This adds directives to schema language and to the utilities that use it (schema parser, and buildASTSchema). Directives are one of the few missing pieces from representing a full schema in the schema language. Note: the schema language is still experimental, so there is no corresponding change to the spec yet. DirectiveDefinition : - directive @ Name ArgumentsDefinition? on DirectiveLocations DirectiveLocations : - Name - DirectiveLocations | Name Example: ``` directive @Skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT ```
1 parent bf763b6 commit fdafe32

13 files changed

+170
-19
lines changed

src/language/__tests__/schema-kitchen-sink.graphql

+7
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,10 @@ input InputType {
3636
extend type Foo {
3737
seven(argument: [String]): Type
3838
}
39+
40+
directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
41+
42+
directive @include(if: Boolean!)
43+
on FIELD
44+
| FRAGMENT_SPREAD
45+
| INLINE_FRAGMENT

src/language/__tests__/schema-printer.js

+5
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ describe('Printer', () => {
4949

5050
const printed = print(ast);
5151

52+
/* eslint-disable max-len */
5253
expect(printed).to.equal(
5354
`type Foo implements Bar {
5455
one: Type
@@ -81,6 +82,10 @@ input InputType {
8182
extend type Foo {
8283
seven(argument: [String]): Type
8384
}
85+
86+
directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
87+
88+
directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
8489
`);
8590

8691
});

src/language/ast.js

+10
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export type Node = Name
5757
| EnumValueDefinition
5858
| InputObjectTypeDefinition
5959
| TypeExtensionDefinition
60+
| DirectiveDefinition
6061

6162
// Name
6263

@@ -78,6 +79,7 @@ export type Definition = OperationDefinition
7879
| FragmentDefinition
7980
| TypeDefinition
8081
| TypeExtensionDefinition
82+
| DirectiveDefinition
8183

8284
export type OperationDefinition = {
8385
kind: 'OperationDefinition';
@@ -332,3 +334,11 @@ export type TypeExtensionDefinition = {
332334
loc?: ?Location;
333335
definition: ObjectTypeDefinition;
334336
}
337+
338+
export type DirectiveDefinition = {
339+
kind: 'DirectiveDefinition';
340+
loc?: ?Location;
341+
name: Name;
342+
arguments?: ?Array<InputValueDefinition>;
343+
locations: Array<Name>;
344+
}

src/language/kinds.js

+7
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,11 @@ export const SCALAR_TYPE_DEFINITION = 'ScalarTypeDefinition';
5959
export const ENUM_TYPE_DEFINITION = 'EnumTypeDefinition';
6060
export const ENUM_VALUE_DEFINITION = 'EnumValueDefinition';
6161
export const INPUT_OBJECT_TYPE_DEFINITION = 'InputObjectTypeDefinition';
62+
63+
// Type Extensions
64+
6265
export const TYPE_EXTENSION_DEFINITION = 'TypeExtensionDefinition';
66+
67+
// Directive Definitions
68+
69+
export const DIRECTIVE_DEFINITION = 'DirectiveDefinition';

src/language/parser.js

+38
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ import type {
5151
InputObjectTypeDefinition,
5252

5353
TypeExtensionDefinition,
54+
55+
DirectiveDefinition,
5456
} from './ast';
5557

5658
import {
@@ -94,6 +96,8 @@ import {
9496
INPUT_OBJECT_TYPE_DEFINITION,
9597

9698
TYPE_EXTENSION_DEFINITION,
99+
100+
DIRECTIVE_DEFINITION,
97101
} from './kinds';
98102

99103

@@ -205,6 +209,7 @@ function parseDefinition(parser: Parser): Definition {
205209
case 'enum':
206210
case 'input': return parseTypeDefinition(parser);
207211
case 'extend': return parseTypeExtensionDefinition(parser);
212+
case 'directive': return parseDirectiveDefinition(parser);
208213
}
209214
}
210215

@@ -898,6 +903,39 @@ function parseTypeExtensionDefinition(parser: Parser): TypeExtensionDefinition {
898903
};
899904
}
900905

906+
/**
907+
* DirectiveDefinition :
908+
* - directive @ Name ArgumentsDefinition? on DirectiveLocations
909+
*/
910+
function parseDirectiveDefinition(parser: Parser): DirectiveDefinition {
911+
const start = parser.token.start;
912+
expectKeyword(parser, 'directive');
913+
expect(parser, TokenKind.AT);
914+
const name = parseName(parser);
915+
const args = parseArgumentDefs(parser);
916+
expectKeyword(parser, 'on');
917+
const locations = parseDirectiveLocations(parser);
918+
return {
919+
kind: DIRECTIVE_DEFINITION,
920+
name,
921+
arguments: args,
922+
locations,
923+
loc: loc(parser, start)
924+
};
925+
}
926+
927+
/**
928+
* DirectiveLocations :
929+
* - Name
930+
* - DirectiveLocations | Name
931+
*/
932+
function parseDirectiveLocations(parser: Parser): Array<Name> {
933+
const locations = [];
934+
do {
935+
locations.push(parseName(parser));
936+
} while (skip(parser, TokenKind.PIPE));
937+
return locations;
938+
}
901939

902940
// Core parsing utility functions
903941

src/language/printer.js

+4
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,10 @@ const printDocASTReducer = {
123123
`input ${name} ${block(fields)}`,
124124

125125
TypeExtensionDefinition: ({ definition }) => `extend ${definition}`,
126+
127+
DirectiveDefinition: ({ name, arguments: args, locations }) =>
128+
'directive @' + name + wrap('(', join(args, ', '), ')') +
129+
' on ' + join(locations, ' | '),
126130
};
127131

128132
/**

src/language/visitor.js

+1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export const QueryDocumentKeys = {
4848
EnumValueDefinition: [ 'name' ],
4949
InputObjectTypeDefinition: [ 'name', 'fields' ],
5050
TypeExtensionDefinition: [ 'definition' ],
51+
DirectiveDefinition: [ 'name', 'arguments', 'locations' ],
5152
};
5253

5354
export const BREAK = {};

src/type/directives.js

+41-12
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@
88
* of patent rights can be found in the PATENTS file in the same directory.
99
*/
1010

11-
import { GraphQLNonNull } from './definition';
12-
import type { GraphQLArgument } from './definition';
11+
import { isInputType, GraphQLNonNull } from './definition';
12+
import type {
13+
GraphQLFieldConfigArgumentMap,
14+
GraphQLArgument
15+
} from './definition';
1316
import { GraphQLBoolean } from './scalars';
1417
import invariant from '../jsutils/invariant';
1518
import { assertValidName } from '../utilities/assertValidName';
@@ -47,15 +50,39 @@ export class GraphQLDirective {
4750
this.name = config.name;
4851
this.description = config.description;
4952
this.locations = config.locations;
50-
this.args = config.args || [];
53+
54+
const args = config.args;
55+
if (!args) {
56+
this.args = [];
57+
} else {
58+
invariant(
59+
!Array.isArray(args),
60+
`@${config.name} args must be an object with argument names as keys.`
61+
);
62+
this.args = Object.keys(args).map(argName => {
63+
assertValidName(argName);
64+
const arg = args[argName];
65+
invariant(
66+
isInputType(arg.type),
67+
`@${config.name}(${argName}:) argument type must be ` +
68+
`Input Type but got: ${arg.type}.`
69+
);
70+
return {
71+
name: argName,
72+
description: arg.description === undefined ? null : arg.description,
73+
type: arg.type,
74+
defaultValue: arg.defaultValue === undefined ? null : arg.defaultValue
75+
};
76+
});
77+
}
5178
}
5279
}
5380

5481
type GraphQLDirectiveConfig = {
5582
name: string;
5683
description?: ?string;
5784
locations: Array<DirectiveLocationEnum>;
58-
args?: ?Array<GraphQLArgument>;
85+
args?: ?GraphQLFieldConfigArgumentMap;
5986
}
6087

6188
/**
@@ -71,11 +98,12 @@ export const GraphQLIncludeDirective = new GraphQLDirective({
7198
DirectiveLocation.FRAGMENT_SPREAD,
7299
DirectiveLocation.INLINE_FRAGMENT,
73100
],
74-
args: [
75-
{ name: 'if',
101+
args: {
102+
if: {
76103
type: new GraphQLNonNull(GraphQLBoolean),
77-
description: 'Included when true.' }
78-
],
104+
description: 'Included when true.'
105+
}
106+
},
79107
});
80108

81109
/**
@@ -91,9 +119,10 @@ export const GraphQLSkipDirective = new GraphQLDirective({
91119
DirectiveLocation.FRAGMENT_SPREAD,
92120
DirectiveLocation.INLINE_FRAGMENT,
93121
],
94-
args: [
95-
{ name: 'if',
122+
args: {
123+
if: {
96124
type: new GraphQLNonNull(GraphQLBoolean),
97-
description: 'Skipped when true.' }
98-
],
125+
description: 'Skipped when true.'
126+
}
127+
},
99128
});

src/utilities/__tests__/buildASTSchema.js

+12
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,18 @@ type HelloScalars {
4141
expect(output).to.equal(body);
4242
});
4343

44+
it('With directives', () => {
45+
const body = `
46+
directive @foo(arg: Int) on FIELD
47+
48+
type Hello {
49+
str: String
50+
}
51+
`;
52+
const output = cycleOutput(body, 'Hello');
53+
expect(output).to.equal(body);
54+
});
55+
4456
it('Type modifiers', () => {
4557
const body = `
4658
type HelloScalars {

src/utilities/__tests__/schemaPrinter.js

+4
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,10 @@ type Root {
508508
const Schema = new GraphQLSchema({ query: Root });
509509
const output = '\n' + printIntrospectionSchema(Schema);
510510
const introspectionSchema = `
511+
directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
512+
513+
directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
514+
511515
type __Directive {
512516
name: String!
513517
description: String

src/utilities/buildASTSchema.js

+20
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
UNION_TYPE_DEFINITION,
2626
SCALAR_TYPE_DEFINITION,
2727
INPUT_OBJECT_TYPE_DEFINITION,
28+
DIRECTIVE_DEFINITION,
2829
} from '../language/kinds';
2930

3031
import type {
@@ -39,6 +40,7 @@ import type {
3940
ScalarTypeDefinition,
4041
EnumTypeDefinition,
4142
InputObjectTypeDefinition,
43+
DirectiveDefinition,
4244
} from '../language/ast';
4345

4446
import {
@@ -58,6 +60,8 @@ import {
5860
GraphQLNonNull,
5961
} from '../type';
6062

63+
import { GraphQLDirective } from '../type/directives';
64+
6165
import type {
6266
GraphQLType,
6367
GraphQLNamedType
@@ -115,6 +119,7 @@ export function buildASTSchema(
115119
}
116120

117121
const typeDefs: Array<TypeDefinition> = [];
122+
const directiveDefs: Array<DirectiveDefinition> = [];
118123
for (let i = 0; i < ast.definitions.length; i++) {
119124
const d = ast.definitions[i];
120125
switch (d.kind) {
@@ -125,6 +130,10 @@ export function buildASTSchema(
125130
case SCALAR_TYPE_DEFINITION:
126131
case INPUT_OBJECT_TYPE_DEFINITION:
127132
typeDefs.push(d);
133+
break;
134+
case DIRECTIVE_DEFINITION:
135+
directiveDefs.push(d);
136+
break;
128137
}
129138
}
130139

@@ -160,13 +169,24 @@ export function buildASTSchema(
160169

161170
typeDefs.forEach(def => typeDefNamed(def.name.value));
162171

172+
const directives = directiveDefs.map(getDirective);
173+
163174
return new GraphQLSchema({
175+
directives,
164176
query: getObjectType(astMap[queryTypeName]),
165177
mutation: mutationTypeName ? getObjectType(astMap[mutationTypeName]) : null,
166178
subscription:
167179
subscriptionTypeName ? getObjectType(astMap[subscriptionTypeName]) : null,
168180
});
169181

182+
function getDirective(directiveAST: DirectiveDefinition): GraphQLDirective {
183+
return new GraphQLDirective({
184+
name: directiveAST.name.value,
185+
locations: directiveAST.locations.map(node => node.value),
186+
args: makeInputValues(directiveAST.arguments),
187+
});
188+
}
189+
170190
function getObjectType(typeAST: TypeDefinition): GraphQLObjectType {
171191
const type = typeDefNamed(typeAST.name.value);
172192
invariant(

src/utilities/buildClientSchema.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,7 @@ export function buildClientSchema(
342342
name: directiveIntrospection.name,
343343
description: directiveIntrospection.description,
344344
locations,
345-
args: directiveIntrospection.args.map(buildInputValue),
345+
args: buildInputValueDefMap(directiveIntrospection.args),
346346
});
347347
}
348348

0 commit comments

Comments
 (0)