Skip to content

Commit e3f53ec

Browse files
committed
Merge pull request #317 from graphql/fine-grain-directives
[RFC] Proposed change to directive location introspection
2 parents 57d71e1 + e89c19d commit e3f53ec

15 files changed

+340
-84
lines changed

src/__tests__/starWarsIntrospectionTests.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ describe('Star Wars Introspection Tests', () => {
7171
},
7272
{
7373
name: '__Directive'
74+
},
75+
{
76+
name: '__DirectiveLocation'
7477
}
7578
]
7679
}

src/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,4 +161,7 @@ export {
161161
isEqualType,
162162
isTypeSubTypeOf,
163163
doTypesOverlap,
164+
165+
// Asserts a string is a valid GraphQL name.
166+
assertValidName,
164167
} from './utilities';

src/type/__tests__/introspection.js

Lines changed: 68 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -640,6 +640,28 @@ describe('Introspection', () => {
640640
isDeprecated: false,
641641
deprecationReason: null
642642
},
643+
{
644+
name: 'locations',
645+
args: [],
646+
type: {
647+
kind: 'NON_NULL',
648+
name: null,
649+
ofType: {
650+
kind: 'LIST',
651+
name: null,
652+
ofType: {
653+
kind: 'NON_NULL',
654+
name: null,
655+
ofType: {
656+
kind: 'ENUM',
657+
name: '__DirectiveLocation'
658+
}
659+
}
660+
}
661+
},
662+
isDeprecated: false,
663+
deprecationReason: null
664+
},
643665
{
644666
name: 'args',
645667
args: [],
@@ -674,8 +696,8 @@ describe('Introspection', () => {
674696
ofType: null,
675697
},
676698
},
677-
isDeprecated: false,
678-
deprecationReason: null
699+
isDeprecated: true,
700+
deprecationReason: 'Use `locations`.'
679701
},
680702
{
681703
name: 'onFragment',
@@ -689,8 +711,8 @@ describe('Introspection', () => {
689711
ofType: null,
690712
},
691713
},
692-
isDeprecated: false,
693-
deprecationReason: null
714+
isDeprecated: true,
715+
deprecationReason: 'Use `locations`.'
694716
},
695717
{
696718
name: 'onField',
@@ -704,19 +726,58 @@ describe('Introspection', () => {
704726
ofType: null,
705727
},
706728
},
707-
isDeprecated: false,
708-
deprecationReason: null
729+
isDeprecated: true,
730+
deprecationReason: 'Use `locations`.'
709731
}
710732
],
711733
inputFields: null,
712734
interfaces: [],
713735
enumValues: null,
714736
possibleTypes: null,
737+
},
738+
{
739+
kind: 'ENUM',
740+
name: '__DirectiveLocation',
741+
fields: null,
742+
inputFields: null,
743+
interfaces: null,
744+
enumValues: [
745+
{
746+
name: 'QUERY',
747+
isDeprecated: false
748+
},
749+
{
750+
name: 'MUTATION',
751+
isDeprecated: false
752+
},
753+
{
754+
name: 'SUBSCRIPTION',
755+
isDeprecated: false
756+
},
757+
{
758+
name: 'FIELD',
759+
isDeprecated: false
760+
},
761+
{
762+
name: 'FRAGMENT_DEFINITION',
763+
isDeprecated: false
764+
},
765+
{
766+
name: 'FRAGMENT_SPREAD',
767+
isDeprecated: false
768+
},
769+
{
770+
name: 'INLINE_FRAGMENT',
771+
isDeprecated: false
772+
},
773+
],
774+
possibleTypes: null,
715775
}
716776
],
717777
directives: [
718778
{
719779
name: 'include',
780+
locations: [ 'FIELD', 'FRAGMENT_SPREAD', 'INLINE_FRAGMENT' ],
720781
args: [
721782
{
722783
defaultValue: null,
@@ -732,12 +793,10 @@ describe('Introspection', () => {
732793
}
733794
}
734795
],
735-
onOperation: false,
736-
onFragment: true,
737-
onField: true
738796
},
739797
{
740798
name: 'skip',
799+
locations: [ 'FIELD', 'FRAGMENT_SPREAD', 'INLINE_FRAGMENT' ],
741800
args: [
742801
{
743802
defaultValue: null,
@@ -753,9 +812,6 @@ describe('Introspection', () => {
753812
}
754813
}
755814
],
756-
onOperation: false,
757-
onFragment: true,
758-
onField: true
759815
}
760816
]
761817
}

src/type/definition.js

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import invariant from '../jsutils/invariant';
1212
import isNullish from '../jsutils/isNullish';
1313
import keyMap from '../jsutils/keyMap';
1414
import { ENUM } from '../language/kinds';
15+
import { assertValidName } from '../utilities/assertValidName';
1516
import type {
1617
OperationDefinition,
1718
Field,
@@ -1061,13 +1062,3 @@ export class GraphQLNonNull<T: GraphQLNullableType> {
10611062
return this.ofType.toString() + '!';
10621063
}
10631064
}
1064-
1065-
const NAME_RX = /^[_a-zA-Z][_a-zA-Z0-9]*$/;
1066-
1067-
// Helper to assert that provided names are valid.
1068-
function assertValidName(name: string): void {
1069-
invariant(
1070-
NAME_RX.test(name),
1071-
`Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but "${name}" does not.`
1072-
);
1073-
}

src/type/directives.js

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,37 +11,51 @@
1111
import { GraphQLNonNull } from './definition';
1212
import type { GraphQLArgument } from './definition';
1313
import { GraphQLBoolean } from './scalars';
14+
import invariant from '../jsutils/invariant';
15+
import { assertValidName } from '../utilities/assertValidName';
1416

1517

18+
export const DirectiveLocation = {
19+
QUERY: 'QUERY',
20+
MUTATION: 'MUTATION',
21+
SUBSCRIPTION: 'SUBSCRIPTION',
22+
FIELD: 'FIELD',
23+
FRAGMENT_DEFINITION: 'FRAGMENT_DEFINITION',
24+
FRAGMENT_SPREAD: 'FRAGMENT_SPREAD',
25+
INLINE_FRAGMENT: 'INLINE_FRAGMENT',
26+
};
27+
28+
export type DirectiveLocationEnum = $Keys<typeof DirectiveLocation>; // eslint-disable-line
29+
1630
/**
1731
* Directives are used by the GraphQL runtime as a way of modifying execution
1832
* behavior. Type system creators will usually not create these directly.
1933
*/
2034
export class GraphQLDirective {
2135
name: string;
2236
description: ?string;
37+
locations: Array<DirectiveLocationEnum>;
2338
args: Array<GraphQLArgument>;
24-
onOperation: boolean;
25-
onFragment: boolean;
26-
onField: boolean;
2739

2840
constructor(config: GraphQLDirectiveConfig) {
41+
invariant(config.name, 'Directive must be named.');
42+
assertValidName(config.name);
43+
invariant(
44+
Array.isArray(config.locations),
45+
'Must provide locations for directive.'
46+
);
2947
this.name = config.name;
3048
this.description = config.description;
49+
this.locations = config.locations;
3150
this.args = config.args || [];
32-
this.onOperation = Boolean(config.onOperation);
33-
this.onFragment = Boolean(config.onFragment);
34-
this.onField = Boolean(config.onField);
3551
}
3652
}
3753

3854
type GraphQLDirectiveConfig = {
3955
name: string;
4056
description?: ?string;
57+
locations: Array<DirectiveLocationEnum>;
4158
args?: ?Array<GraphQLArgument>;
42-
onOperation?: ?boolean;
43-
onFragment?: ?boolean;
44-
onField?: ?boolean;
4559
}
4660

4761
/**
@@ -52,14 +66,16 @@ export const GraphQLIncludeDirective = new GraphQLDirective({
5266
description:
5367
'Directs the executor to include this field or fragment only when ' +
5468
'the `if` argument is true.',
69+
locations: [
70+
DirectiveLocation.FIELD,
71+
DirectiveLocation.FRAGMENT_SPREAD,
72+
DirectiveLocation.INLINE_FRAGMENT,
73+
],
5574
args: [
5675
{ name: 'if',
5776
type: new GraphQLNonNull(GraphQLBoolean),
5877
description: 'Included when true.' }
5978
],
60-
onOperation: false,
61-
onFragment: true,
62-
onField: true
6379
});
6480

6581
/**
@@ -70,12 +86,14 @@ export const GraphQLSkipDirective = new GraphQLDirective({
7086
description:
7187
'Directs the executor to skip this field or fragment when the `if` ' +
7288
'argument is true.',
89+
locations: [
90+
DirectiveLocation.FIELD,
91+
DirectiveLocation.FRAGMENT_SPREAD,
92+
DirectiveLocation.INLINE_FRAGMENT,
93+
],
7394
args: [
7495
{ name: 'if',
7596
type: new GraphQLNonNull(GraphQLBoolean),
7697
description: 'Skipped when true.' }
7798
],
78-
onOperation: false,
79-
onFragment: true,
80-
onField: true
8199
});

src/type/introspection.js

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
GraphQLNonNull,
2323
} from './definition';
2424
import { GraphQLString, GraphQLBoolean } from './scalars';
25+
import { DirectiveLocation } from './directives';
2526
import type { GraphQLFieldDefinition } from './definition';
2627

2728

@@ -78,17 +79,79 @@ const __Directive = new GraphQLObjectType({
7879
fields: () => ({
7980
name: { type: new GraphQLNonNull(GraphQLString) },
8081
description: { type: GraphQLString },
82+
locations: {
83+
type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(
84+
__DirectiveLocation
85+
)))
86+
},
8187
args: {
8288
type:
8389
new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(__InputValue))),
8490
resolve: directive => directive.args || []
8591
},
86-
onOperation: { type: new GraphQLNonNull(GraphQLBoolean) },
87-
onFragment: { type: new GraphQLNonNull(GraphQLBoolean) },
88-
onField: { type: new GraphQLNonNull(GraphQLBoolean) },
92+
// NOTE: the following three fields are deprecated and are no longer part
93+
// of the GraphQL specification.
94+
onOperation: {
95+
deprecationReason: 'Use `locations`.',
96+
type: new GraphQLNonNull(GraphQLBoolean),
97+
resolve: d =>
98+
d.locations.indexOf(DirectiveLocation.QUERY) !== -1 ||
99+
d.locations.indexOf(DirectiveLocation.MUTATION) !== -1 ||
100+
d.locations.indexOf(DirectiveLocation.SUBSCRIPTION) !== -1
101+
},
102+
onFragment: {
103+
deprecationReason: 'Use `locations`.',
104+
type: new GraphQLNonNull(GraphQLBoolean),
105+
resolve: d =>
106+
d.locations.indexOf(DirectiveLocation.FRAGMENT_SPREAD) !== -1 ||
107+
d.locations.indexOf(DirectiveLocation.INLINE_FRAGMENT) !== -1 ||
108+
d.locations.indexOf(DirectiveLocation.FRAGMENT_DEFINITION) !== -1
109+
},
110+
onField: {
111+
deprecationReason: 'Use `locations`.',
112+
type: new GraphQLNonNull(GraphQLBoolean),
113+
resolve: d => d.locations.indexOf(DirectiveLocation.FIELD) !== -1
114+
},
89115
}),
90116
});
91117

118+
const __DirectiveLocation = new GraphQLEnumType({
119+
name: '__DirectiveLocation',
120+
description:
121+
'A Directive can be adjacent to many parts of the GraphQL language, a ' +
122+
'__DirectiveLocation describes one such possible adjacencies.',
123+
values: {
124+
QUERY: {
125+
value: DirectiveLocation.QUERY,
126+
description: 'Location adjacent to a query operation.'
127+
},
128+
MUTATION: {
129+
value: DirectiveLocation.MUTATION,
130+
description: 'Location adjacent to a mutation operation.'
131+
},
132+
SUBSCRIPTION: {
133+
value: DirectiveLocation.SUBSCRIPTION,
134+
description: 'Location adjacent to a subscription operation.'
135+
},
136+
FIELD: {
137+
value: DirectiveLocation.FIELD,
138+
description: 'Location adjacent to a field.'
139+
},
140+
FRAGMENT_DEFINITION: {
141+
value: DirectiveLocation.FRAGMENT_DEFINITION,
142+
description: 'Location adjacent to a fragment definition.'
143+
},
144+
FRAGMENT_SPREAD: {
145+
value: DirectiveLocation.FRAGMENT_SPREAD,
146+
description: 'Location adjacent to a fragment spread.'
147+
},
148+
INLINE_FRAGMENT: {
149+
value: DirectiveLocation.INLINE_FRAGMENT,
150+
description: 'Location adjacent to an inline fragment.'
151+
},
152+
}
153+
});
154+
92155
const __Type = new GraphQLObjectType({
93156
name: '__Type',
94157
description:

0 commit comments

Comments
 (0)