Skip to content

Commit 345db59

Browse files
committed
Merge pull request #318 from graphql/directive-lang
[RFC] Directives in schema language
2 parents e89c19d + b633458 commit 345db59

13 files changed

+170
-19
lines changed

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

Lines changed: 7 additions & 0 deletions
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

Lines changed: 5 additions & 0 deletions
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

Lines changed: 10 additions & 0 deletions
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

Lines changed: 7 additions & 0 deletions
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

Lines changed: 38 additions & 0 deletions
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

Lines changed: 4 additions & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 41 additions & 12 deletions
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

Lines changed: 12 additions & 0 deletions
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

Lines changed: 4 additions & 0 deletions
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

Lines changed: 20 additions & 0 deletions
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

Lines changed: 1 addition & 1 deletion
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)