Skip to content

Commit 2c7224c

Browse files
Added partial support for repeatable directives (#1965)
Code is based on #1541 but without introspection changes and without breaking change detection
1 parent 24fa31a commit 2c7224c

15 files changed

+238
-47
lines changed

src/__fixtures__/schema-kitchen-sink.graphql

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,10 @@ directive @include2(if: Boolean!) on
142142
| FRAGMENT_SPREAD
143143
| INLINE_FRAGMENT
144144

145+
directive @myRepeatableDir(name: String!) repeatable on
146+
| OBJECT
147+
| INTERFACE
148+
145149
extend schema @onSchema
146150

147151
extend schema @onSchema {

src/language/__tests__/schema-parser-test.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -815,6 +815,78 @@ input Hello {
815815
);
816816
});
817817

818+
it('Directive definition', () => {
819+
const body = 'directive @foo on OBJECT | INTERFACE';
820+
const doc = parse(body);
821+
822+
expect(toJSONDeep(doc)).to.deep.equal({
823+
kind: 'Document',
824+
definitions: [
825+
{
826+
kind: 'DirectiveDefinition',
827+
description: undefined,
828+
name: {
829+
kind: 'Name',
830+
value: 'foo',
831+
loc: { start: 11, end: 14 },
832+
},
833+
arguments: [],
834+
repeatable: false,
835+
locations: [
836+
{
837+
kind: 'Name',
838+
value: 'OBJECT',
839+
loc: { start: 18, end: 24 },
840+
},
841+
{
842+
kind: 'Name',
843+
value: 'INTERFACE',
844+
loc: { start: 27, end: 36 },
845+
},
846+
],
847+
loc: { start: 0, end: 36 },
848+
},
849+
],
850+
loc: { start: 0, end: 36 },
851+
});
852+
});
853+
854+
it('Repeatable directive definition', () => {
855+
const body = 'directive @foo repeatable on OBJECT | INTERFACE';
856+
const doc = parse(body);
857+
858+
expect(toJSONDeep(doc)).to.deep.equal({
859+
kind: 'Document',
860+
definitions: [
861+
{
862+
kind: 'DirectiveDefinition',
863+
description: undefined,
864+
name: {
865+
kind: 'Name',
866+
value: 'foo',
867+
loc: { start: 11, end: 14 },
868+
},
869+
arguments: [],
870+
repeatable: true,
871+
locations: [
872+
{
873+
kind: 'Name',
874+
value: 'OBJECT',
875+
loc: { start: 29, end: 35 },
876+
},
877+
{
878+
kind: 'Name',
879+
value: 'INTERFACE',
880+
loc: { start: 38, end: 47 },
881+
},
882+
],
883+
loc: { start: 0, end: 47 },
884+
},
885+
],
886+
loc: { start: 0, end: 47 },
887+
});
888+
});
889+
818890
it('Directive with incorrect locations', () => {
819891
expectSyntaxError(
820892
`

src/language/__tests__/schema-printer-test.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,8 @@ describe('Printer: SDL document', () => {
160160
161161
directive @include2(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
162162
163+
directive @myRepeatableDir(name: String!) repeatable on OBJECT | INTERFACE
164+
163165
extend schema @onSchema
164166
165167
extend schema @onSchema {

src/language/ast.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,7 @@ export type DirectiveDefinitionNode = {
508508
+description?: StringValueNode,
509509
+name: NameNode,
510510
+arguments?: $ReadOnlyArray<InputValueDefinitionNode>,
511+
+repeatable: boolean,
511512
+locations: $ReadOnlyArray<NameNode>,
512513
};
513514

src/language/parser.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1358,7 +1358,7 @@ function parseInputObjectTypeExtension(
13581358

13591359
/**
13601360
* DirectiveDefinition :
1361-
* - Description? directive @ Name ArgumentsDefinition? on DirectiveLocations
1361+
* - Description? directive @ Name ArgumentsDefinition? `repeatable`? on DirectiveLocations
13621362
*/
13631363
function parseDirectiveDefinition(lexer: Lexer<*>): DirectiveDefinitionNode {
13641364
const start = lexer.token;
@@ -1367,13 +1367,15 @@ function parseDirectiveDefinition(lexer: Lexer<*>): DirectiveDefinitionNode {
13671367
expectToken(lexer, TokenKind.AT);
13681368
const name = parseName(lexer);
13691369
const args = parseArgumentDefs(lexer);
1370+
const repeatable = expectOptionalKeyword(lexer, 'repeatable');
13701371
expectKeyword(lexer, 'on');
13711372
const locations = parseDirectiveLocations(lexer);
13721373
return {
13731374
kind: Kind.DIRECTIVE_DEFINITION,
13741375
description,
13751376
name,
13761377
arguments: args,
1378+
repeatable,
13771379
locations,
13781380
loc: loc(lexer, start),
13791381
};

src/language/printer.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,12 +184,13 @@ const printDocASTReducer: any = {
184184
),
185185

186186
DirectiveDefinition: addDescription(
187-
({ name, arguments: args, locations }) =>
187+
({ name, arguments: args, repeatable, locations }) =>
188188
'directive @' +
189189
name +
190190
(hasMultilineItems(args)
191191
? wrap('(\n', indent(join(args, '\n')), '\n)')
192192
: wrap('(', join(args, ', '), ')')) +
193+
(repeatable ? ' repeatable' : '') +
193194
' on ' +
194195
join(locations, ' | '),
195196
),

src/type/__tests__/directive-test.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ describe('Type System: Directive', () => {
2222
expect(directive).to.deep.include({
2323
name: 'Foo',
2424
args: [],
25+
isRepeatable: false,
2526
locations: ['QUERY'],
2627
});
2728
});
@@ -54,6 +55,22 @@ describe('Type System: Directive', () => {
5455
astNode: undefined,
5556
},
5657
],
58+
isRepeatable: false,
59+
locations: ['QUERY'],
60+
});
61+
});
62+
63+
it('defines a repeatable directive', () => {
64+
const directive = new GraphQLDirective({
65+
name: 'Foo',
66+
isRepeatable: true,
67+
locations: ['QUERY'],
68+
});
69+
70+
expect(directive).to.deep.include({
71+
name: 'Foo',
72+
args: [],
73+
isRepeatable: true,
5774
locations: ['QUERY'],
5875
});
5976
});

src/type/directives.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,16 @@ export class GraphQLDirective {
5353
name: string;
5454
description: ?string;
5555
locations: Array<DirectiveLocationEnum>;
56+
isRepeatable: boolean;
5657
args: Array<GraphQLArgument>;
5758
astNode: ?DirectiveDefinitionNode;
5859

5960
constructor(config: GraphQLDirectiveConfig): void {
6061
this.name = config.name;
6162
this.description = config.description;
63+
6264
this.locations = config.locations;
65+
this.isRepeatable = config.isRepeatable != null && config.isRepeatable;
6366
this.astNode = config.astNode;
6467
invariant(config.name, 'Directive must be named.');
6568
invariant(
@@ -89,12 +92,14 @@ export class GraphQLDirective {
8992
toConfig(): {|
9093
...GraphQLDirectiveConfig,
9194
args: GraphQLFieldConfigArgumentMap,
95+
isRepeatable: boolean,
9296
|} {
9397
return {
9498
name: this.name,
9599
description: this.description,
96100
locations: this.locations,
97101
args: argsToArgsConfig(this.args),
102+
isRepeatable: this.isRepeatable,
98103
astNode: this.astNode,
99104
};
100105
}
@@ -109,6 +114,7 @@ export type GraphQLDirectiveConfig = {|
109114
description?: ?string,
110115
locations: Array<DirectiveLocationEnum>,
111116
args?: ?GraphQLFieldConfigArgumentMap,
117+
isRepeatable?: ?boolean,
112118
astNode?: ?DirectiveDefinitionNode,
113119
|};
114120

src/utilities/__tests__/buildASTSchema-test.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@ describe('Schema Builder', () => {
121121
it('With directives', () => {
122122
const sdl = dedent`
123123
directive @foo(arg: Int) on FIELD
124+
125+
directive @repeatableFoo(arg: Int) repeatable on FIELD
124126
`;
125127
expect(cycleSDL(sdl)).to.equal(sdl);
126128
});

src/utilities/__tests__/extendSchema-test.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ const FooDirective = new GraphQLDirective({
107107
args: {
108108
input: { type: SomeInputType },
109109
},
110+
isRepeatable: true,
110111
locations: [
111112
DirectiveLocation.SCHEMA,
112113
DirectiveLocation.SCALAR,
@@ -448,7 +449,7 @@ describe('extendSchema', () => {
448449
interfaceField: String
449450
}
450451
451-
directive @test(arg: Int) on FIELD | SCALAR
452+
directive @test(arg: Int) repeatable on FIELD | SCALAR
452453
`);
453454
const extendedTwiceSchema = extendSchema(extendedSchema, ast);
454455

@@ -1091,7 +1092,7 @@ describe('extendSchema', () => {
10911092

10921093
it('may extend directives with new complex directive', () => {
10931094
const extendedSchema = extendTestSchema(`
1094-
directive @profile(enable: Boolean! tag: String) on QUERY | FIELD
1095+
directive @profile(enable: Boolean! tag: String) repeatable on QUERY | FIELD
10951096
`);
10961097

10971098
const extendedDirective = assertDirective(

src/utilities/__tests__/schemaPrinter-test.js

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -461,15 +461,30 @@ describe('Type System Printer', () => {
461461
});
462462

463463
it('Prints custom directives', () => {
464-
const CustomDirective = new GraphQLDirective({
465-
name: 'customDirective',
464+
const SimpleDirective = new GraphQLDirective({
465+
name: 'simpleDirective',
466466
locations: [DirectiveLocation.FIELD],
467467
});
468+
const ComplexDirective = new GraphQLDirective({
469+
name: 'complexDirective',
470+
description: 'Complex Directive',
471+
args: {
472+
stringArg: { type: GraphQLString },
473+
intArg: { type: GraphQLInt, defaultValue: -1 },
474+
},
475+
isRepeatable: true,
476+
locations: [DirectiveLocation.FIELD, DirectiveLocation.QUERY],
477+
});
468478

469-
const Schema = new GraphQLSchema({ directives: [CustomDirective] });
479+
const Schema = new GraphQLSchema({
480+
directives: [SimpleDirective, ComplexDirective],
481+
});
470482
const output = printForTest(Schema);
471483
expect(output).to.equal(dedent`
472-
directive @customDirective on FIELD
484+
directive @simpleDirective on FIELD
485+
486+
"""Complex Directive"""
487+
directive @complexDirective(stringArg: String, intArg: Int = -1) repeatable on FIELD | QUERY
473488
`);
474489
});
475490

src/utilities/buildASTSchema.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ export class ASTDefinitionBuilder {
242242
name: directive.name.value,
243243
description: getDescription(directive, this._options),
244244
locations,
245+
isRepeatable: directive.repeatable,
245246
args: keyByNameNode(directive.arguments || [], arg => this.buildArg(arg)),
246247
astNode: directive,
247248
});

src/utilities/schemaPrinter.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,7 @@ function printDirective(directive, options) {
296296
'directive @' +
297297
directive.name +
298298
printArgs(options, directive.args) +
299+
(directive.isRepeatable ? ' repeatable' : '') +
299300
' on ' +
300301
directive.locations.join(' | ')
301302
);

0 commit comments

Comments
 (0)