Skip to content

Commit 2fd6287

Browse files
committed
Allow providing directives to GraphQLSchema
This allows GraphQLSchema to represent different sets of directives than those graphql-js is able to use during execution. Critically, this allows GraphQLSchema to be used against servers which support different sets of directives and still use validation utilities.
1 parent 3a6b4d1 commit 2fd6287

File tree

8 files changed

+143
-43
lines changed

8 files changed

+143
-43
lines changed

src/type/__tests__/validation.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,17 @@ describe('Type System: A Schema must have Object root types', () => {
189189
);
190190
});
191191

192+
it('rejects a Schema whose directives are incorrectly typed', () => {
193+
expect(
194+
() => new GraphQLSchema({
195+
query: SomeObjectType,
196+
directives: [ 'somedirective' ]
197+
})
198+
).to.throw(
199+
'Schema directives must be Array<GraphQLDirective> if provided but got: somedirective.'
200+
);
201+
});
202+
192203
});
193204

194205
describe('Type System: A Schema must contain uniquely named types', () => {

src/type/definition.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,7 @@ export type GraphQLFieldConfigArgumentMap = {
488488
export type GraphQLArgumentConfig = {
489489
type: GraphQLInputType;
490490
defaultValue?: any;
491+
description?: ?string;
491492
}
492493

493494
export type GraphQLFieldConfigMap = {

src/type/directives.js

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,20 +28,20 @@ export class GraphQLDirective {
2828
constructor(config: GraphQLDirectiveConfig) {
2929
this.name = config.name;
3030
this.description = config.description;
31-
this.args = config.args;
32-
this.onOperation = config.onOperation;
33-
this.onFragment = config.onFragment;
34-
this.onField = config.onField;
31+
this.args = config.args || [];
32+
this.onOperation = Boolean(config.onOperation);
33+
this.onFragment = Boolean(config.onFragment);
34+
this.onField = Boolean(config.onField);
3535
}
3636
}
3737

3838
type GraphQLDirectiveConfig = {
3939
name: string;
40-
description?: string;
41-
args: Array<GraphQLArgument>;
42-
onOperation: boolean;
43-
onFragment: boolean;
44-
onField: boolean;
40+
description?: ?string;
41+
args?: ?Array<GraphQLArgument>;
42+
onOperation?: ?boolean;
43+
onFragment?: ?boolean;
44+
onField?: ?boolean;
4545
}
4646

4747
/**

src/type/schema.js

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@ import {
1717
GraphQLNonNull
1818
} from './definition';
1919
import type { GraphQLType } from './definition';
20-
import { GraphQLIncludeDirective, GraphQLSkipDirective } from './directives';
21-
import type { GraphQLDirective } from './directives';
20+
import {
21+
GraphQLDirective,
22+
GraphQLIncludeDirective,
23+
GraphQLSkipDirective
24+
} from './directives';
2225
import { __Schema } from './introspection';
2326
import find from '../jsutils/find';
2427
import invariant from '../jsutils/invariant';
@@ -40,30 +43,51 @@ import invariant from '../jsutils/invariant';
4043
*
4144
*/
4245
export class GraphQLSchema {
43-
_schemaConfig: GraphQLSchemaConfig;
44-
_typeMap: TypeMap;
46+
_queryType: GraphQLObjectType;
47+
_mutationType: ?GraphQLObjectType;
48+
_subscriptionType: ?GraphQLObjectType;
4549
_directives: Array<GraphQLDirective>;
50+
_typeMap: TypeMap;
4651

4752
constructor(config: GraphQLSchemaConfig) {
4853
invariant(
4954
typeof config === 'object',
5055
'Must provide configuration object.'
5156
);
57+
5258
invariant(
5359
config.query instanceof GraphQLObjectType,
5460
`Schema query must be Object Type but got: ${config.query}.`
5561
);
62+
this._queryType = config.query;
63+
5664
invariant(
5765
!config.mutation || config.mutation instanceof GraphQLObjectType,
5866
`Schema mutation must be Object Type if provided but ` +
5967
`got: ${config.mutation}.`
6068
);
69+
this._mutationType = config.mutation;
70+
6171
invariant(
6272
!config.subscription || config.subscription instanceof GraphQLObjectType,
6373
`Schema subscription must be Object Type if provided but ` +
6474
`got: ${config.subscription}.`
6575
);
66-
this._schemaConfig = config;
76+
this._subscriptionType = config.subscription;
77+
78+
invariant(
79+
!config.directives ||
80+
Array.isArray(config.directives) && config.directives.every(
81+
directive => directive instanceof GraphQLDirective
82+
),
83+
`Schema directives must be Array<GraphQLDirective> if provided but ` +
84+
`got: ${config.directives}.`
85+
);
86+
// Provide `@include() and `@skip()` directives by default.
87+
this._directives = config.directives || [
88+
GraphQLIncludeDirective,
89+
GraphQLSkipDirective
90+
];
6791

6892
// Build type map now to detect any errors within this schema.
6993
this._typeMap = [
@@ -85,15 +109,15 @@ export class GraphQLSchema {
85109
}
86110

87111
getQueryType(): GraphQLObjectType {
88-
return this._schemaConfig.query;
112+
return this._queryType;
89113
}
90114

91115
getMutationType(): ?GraphQLObjectType {
92-
return this._schemaConfig.mutation;
116+
return this._mutationType;
93117
}
94118

95119
getSubscriptionType(): ?GraphQLObjectType {
96-
return this._schemaConfig.subscription;
120+
return this._subscriptionType;
97121
}
98122

99123
getTypeMap(): TypeMap {
@@ -105,10 +129,7 @@ export class GraphQLSchema {
105129
}
106130

107131
getDirectives(): Array<GraphQLDirective> {
108-
return this._directives || (this._directives = [
109-
GraphQLIncludeDirective,
110-
GraphQLSkipDirective
111-
]);
132+
return this._directives;
112133
}
113134

114135
getDirective(name: string): ?GraphQLDirective {
@@ -122,6 +143,7 @@ type GraphQLSchemaConfig = {
122143
query: GraphQLObjectType;
123144
mutation?: ?GraphQLObjectType;
124145
subscription?: ?GraphQLObjectType;
146+
directives?: ?Array<GraphQLDirective>;
125147
}
126148

127149
function typeMapReducer(map: TypeMap, type: ?GraphQLType): TypeMap {

src/utilities/__tests__/buildClientSchema.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
GraphQLBoolean,
2929
GraphQLID,
3030
} from '../../';
31+
import { GraphQLDirective } from '../../type/directives';
3132

3233

3334
// Test property:
@@ -477,6 +478,31 @@ describe('Type System: build schema from introspection', () => {
477478
await testSchema(schema);
478479
});
479480

481+
it('builds a schema with custom directives', async () => {
482+
483+
var schema = new GraphQLSchema({
484+
query: new GraphQLObjectType({
485+
name: 'Simple',
486+
description: 'This is a simple type',
487+
fields: {
488+
string: {
489+
type: GraphQLString,
490+
description: 'This is a string field'
491+
}
492+
}
493+
}),
494+
directives: [
495+
new GraphQLDirective({
496+
name: 'customDirective',
497+
description: 'This is a custom directive',
498+
onField: true,
499+
})
500+
]
501+
});
502+
503+
await testSchema(schema);
504+
});
505+
480506
it('cannot use client schema for general execution', async () => {
481507
var customScalar = new GraphQLScalarType({
482508
name: 'CustomScalar',

src/utilities/buildClientSchema.js

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ import {
3636
GraphQLID
3737
} from '../type/scalars';
3838

39+
import { GraphQLDirective } from '../type/directives';
40+
3941
import { TypeKind } from '../type/introspection';
4042

4143
import type {
@@ -292,19 +294,35 @@ export function buildClientSchema(
292294
return keyValMap(
293295
inputValueIntrospections,
294296
inputValue => inputValue.name,
295-
inputValue => {
296-
var description = inputValue.description;
297-
var type = getInputType(inputValue.type);
298-
var defaultValue = inputValue.defaultValue ?
299-
valueFromAST(parseValue(inputValue.defaultValue), type) :
300-
null;
301-
return { description, type, defaultValue };
302-
}
297+
buildInputValue
303298
);
304299
}
305300

301+
function buildInputValue(inputValueIntrospection) {
302+
var type = getInputType(inputValueIntrospection.type);
303+
var defaultValue = inputValueIntrospection.defaultValue ?
304+
valueFromAST(parseValue(inputValueIntrospection.defaultValue), type) :
305+
null;
306+
return {
307+
name: inputValueIntrospection.name,
308+
description: inputValueIntrospection.description,
309+
type,
310+
defaultValue,
311+
};
312+
}
313+
314+
function buildDirective(directiveIntrospection) {
315+
return new GraphQLDirective({
316+
name: directiveIntrospection.name,
317+
description: directiveIntrospection.description,
318+
args: directiveIntrospection.args.map(buildInputValue),
319+
onOperation: directiveIntrospection.onOperation,
320+
onFragment: directiveIntrospection.onFragment,
321+
onField: directiveIntrospection.onField,
322+
});
323+
}
324+
306325
// TODO: deprecation
307-
// TODO: directives
308326

309327
// Iterate through all types, getting the type definition for each, ensuring
310328
// that any type not directly referenced by a field will get created.
@@ -313,20 +331,27 @@ export function buildClientSchema(
313331
);
314332

315333
// Get the root Query, Mutation, and Subscription types.
316-
var queryType = getType(schemaIntrospection.queryType);
334+
var queryType = getObjectType(schemaIntrospection.queryType);
335+
317336
var mutationType = schemaIntrospection.mutationType ?
318-
getType(schemaIntrospection.mutationType) :
337+
getObjectType(schemaIntrospection.mutationType) :
319338
null;
339+
320340
var subscriptionType = schemaIntrospection.subscriptionType ?
321-
getType(schemaIntrospection.subscriptionType) :
341+
getObjectType(schemaIntrospection.subscriptionType) :
322342
null;
323343

344+
// Get the directives supported by Introspection, assuming empty-set if
345+
// directives were not queried for.
346+
var directives = schemaIntrospection.directives ?
347+
schemaIntrospection.directives.map(buildDirective) :
348+
[];
349+
324350
// Then produce and return a Schema with these types.
325-
var schema = new GraphQLSchema({
326-
query: (queryType: any),
327-
mutation: (mutationType: any),
328-
subscription: (subscriptionType: any)
351+
return new GraphQLSchema({
352+
query: queryType,
353+
mutation: mutationType,
354+
subscription: subscriptionType,
355+
directives,
329356
});
330-
331-
return schema;
332357
}

src/validation/__tests__/KnownDirectives.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,11 +104,13 @@ describe('Validate: Known directives', () => {
104104
it('with misplaced directives', () => {
105105
expectFailsRule(KnownDirectives, `
106106
query Foo @include(if: true) {
107-
name
108-
...Frag
107+
name @operationOnly
108+
...Frag @operationOnly
109109
}
110110
`, [
111-
misplacedDirective('include', 'operation', 2, 17)
111+
misplacedDirective('include', 'operation', 2, 17),
112+
misplacedDirective('operationOnly', 'field', 3, 14),
113+
misplacedDirective('operationOnly', 'fragment', 4, 17),
112114
]);
113115
});
114116

src/validation/__tests__/harness.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ import {
2626
GraphQLBoolean,
2727
GraphQLID
2828
} from '../../type';
29+
import {
30+
GraphQLDirective,
31+
GraphQLIncludeDirective,
32+
GraphQLSkipDirective,
33+
} from '../../type/directives';
2934

3035

3136
var Being = new GraphQLInterfaceType({
@@ -288,7 +293,15 @@ var QueryRoot = new GraphQLObjectType({
288293
});
289294

290295
var defaultSchema = new GraphQLSchema({
291-
query: QueryRoot
296+
query: QueryRoot,
297+
directives: [
298+
new GraphQLDirective({
299+
name: 'operationOnly',
300+
onOperation: true
301+
}),
302+
GraphQLIncludeDirective,
303+
GraphQLSkipDirective,
304+
]
292305
});
293306

294307
function expectValid(schema, rules, queryString) {

0 commit comments

Comments
 (0)