Skip to content

Commit 3cf66f3

Browse files
robzhuleebyron
authored andcommitted
Update extendSchema to support new Directives (#455)
* Update extendSchema to support new Directives This changes the extendSchema utility to accept and process new Directive definitions. Existing directives cannot be extended or replaced (should throw an error in such cases). See extendSchema-test for examples. * Update extendSchema to support new Directives This changes the extendSchema utility to accept and process new Directive definitions. Existing directives cannot be extended or replaced (should throw an error in such cases). See extendSchema-test for examples. * Fix trailing whitespace lint error * Improve logic in new directive extendSchema Use an Array instead of an object for collecting new directives detected within the schema extension.
1 parent 8df7894 commit 3cf66f3

File tree

2 files changed

+120
-6
lines changed

2 files changed

+120
-6
lines changed

src/utilities/__tests__/extendSchema-test.js

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ import {
2323
GraphQLEnumType,
2424
GraphQLNonNull,
2525
GraphQLList,
26+
GraphQLScalarType,
2627
} from '../../type';
2728

28-
2929
// Test schema.
3030
const SomeInterfaceType = new GraphQLInterfaceType({
3131
name: 'SomeInterface',
@@ -718,6 +718,72 @@ type Subscription {
718718
`);
719719
});
720720

721+
it('may extend directives with new simple directive', () => {
722+
const ast = parse(`
723+
directive @neat on QUERY
724+
`);
725+
726+
const extendedSchema = extendSchema(testSchema, ast);
727+
const newDirective = extendedSchema.getDirective('neat');
728+
expect(newDirective.name).to.equal('neat');
729+
expect(newDirective.locations).to.contain('QUERY');
730+
});
731+
732+
it('may extend directives with new complex directive', () => {
733+
const ast = parse(`
734+
directive @profile(enable: Boolean! tag: String) on QUERY | FIELD
735+
`);
736+
737+
const extendedSchema = extendSchema(testSchema, ast);
738+
const extendedDirective = extendedSchema.getDirective('profile');
739+
expect(extendedDirective.locations).to.contain('QUERY');
740+
expect(extendedDirective.locations).to.contain('FIELD');
741+
742+
const args = extendedDirective.args;
743+
const arg0 = args[0];
744+
const arg1 = args[1];
745+
746+
expect(args.length).to.equal(2);
747+
expect(arg0.name).to.equal('enable');
748+
expect(arg0.type).to.be.instanceof(GraphQLNonNull);
749+
expect(arg0.type.ofType).to.be.instanceof(GraphQLScalarType);
750+
751+
expect(arg1.name).to.equal('tag');
752+
expect(arg1.type).to.be.instanceof(GraphQLScalarType);
753+
});
754+
755+
it('does not allow replacing a default directive', () => {
756+
const ast = parse(`
757+
directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD
758+
`);
759+
760+
expect(() =>
761+
extendSchema(testSchema, ast)
762+
).to.throw(
763+
'Directive "include" already exists in the schema. It cannot be ' +
764+
'redefined.'
765+
);
766+
});
767+
768+
it('does not allow replacing a custom directive', () => {
769+
const ast = parse(`
770+
directive @meow(if: Boolean!) on FIELD | FRAGMENT_SPREAD
771+
`);
772+
773+
const extendedSchema = extendSchema(testSchema, ast);
774+
775+
const replacementAst = parse(`
776+
directive @meow(if: Boolean!) on FIELD | QUERY
777+
`);
778+
779+
expect(() =>
780+
extendSchema(extendedSchema, replacementAst)
781+
).to.throw(
782+
'Directive "meow" already exists in the schema. It cannot be ' +
783+
'redefined.'
784+
);
785+
});
786+
721787
it('does not allow replacing an existing type', () => {
722788
const ast = parse(`
723789
type Bar {

src/utilities/extendSchema.js

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ import {
2828
isOutputType,
2929
} from '../type/definition';
3030

31+
import {
32+
GraphQLDirective,
33+
} from '../type/directives';
34+
3135
import {
3236
__Schema,
3337
__Directive,
@@ -58,6 +62,7 @@ import {
5862
SCALAR_TYPE_DEFINITION,
5963
INPUT_OBJECT_TYPE_DEFINITION,
6064
TYPE_EXTENSION_DEFINITION,
65+
DIRECTIVE_DEFINITION,
6166
} from '../language/kinds';
6267

6368
import type {
@@ -67,6 +72,10 @@ import type {
6772
GraphQLOutputType,
6873
} from '../type/definition';
6974

75+
import type {
76+
DirectiveLocationEnum
77+
} from '../type/directives';
78+
7079
import type {
7180
Document,
7281
InputValueDefinition,
@@ -79,6 +88,7 @@ import type {
7988
ScalarTypeDefinition,
8089
EnumTypeDefinition,
8190
InputObjectTypeDefinition,
91+
DirectiveDefinition,
8292
} from '../language/ast';
8393

8494

@@ -112,6 +122,10 @@ export function extendSchema(
112122
const typeDefinitionMap = {};
113123
const typeExtensionsMap = {};
114124

125+
// New directives and types are separate because a directives and types can
126+
// have the same name. For example, a type named "skip".
127+
const directiveDefinitions : Array<DirectiveDefinition> = [];
128+
115129
for (let i = 0; i < documentAST.definitions.length; i++) {
116130
const def = documentAST.definitions[i];
117131
switch (def.kind) {
@@ -159,13 +173,26 @@ export function extendSchema(
159173
}
160174
typeExtensionsMap[extendedTypeName] = extensions;
161175
break;
176+
case DIRECTIVE_DEFINITION:
177+
const directiveName = def.name.value;
178+
const existingDirective = schema.getDirective(directiveName);
179+
if (existingDirective) {
180+
throw new GraphQLError(
181+
`Directive "${directiveName}" already exists in the schema. It ` +
182+
'cannot be redefined.',
183+
[ def ]
184+
);
185+
}
186+
directiveDefinitions.push(def);
187+
break;
162188
}
163189
}
164190

165-
// If this document contains no new types, then return the same unmodified
166-
// GraphQLSchema instance.
191+
// If this document contains no new types, extensions, or directives then
192+
// return the same unmodified GraphQLSchema instance.
167193
if (Object.keys(typeExtensionsMap).length === 0 &&
168-
Object.keys(typeDefinitionMap).length === 0) {
194+
Object.keys(typeDefinitionMap).length === 0 &&
195+
directiveDefinitions.length === 0) {
169196
return schema;
170197
}
171198

@@ -220,13 +247,22 @@ export function extendSchema(
220247
mutation: mutationType,
221248
subscription: subscriptionType,
222249
types,
223-
// Copy directives.
224-
directives: schema.getDirectives(),
250+
directives: getMergedDirectives(),
225251
});
226252

227253
// Below are functions used for producing this schema that have closed over
228254
// this scope and have access to the schema, cache, and newly defined types.
229255

256+
function getMergedDirectives(): Array<GraphQLDirective> {
257+
const existingDirectives = schema.getDirectives();
258+
invariant(existingDirectives, 'schema must have default directives');
259+
260+
const newDirectives = directiveDefinitions.map(directiveAST =>
261+
getDirective(directiveAST)
262+
);
263+
return existingDirectives.concat(newDirectives);
264+
}
265+
230266
function getTypeFromDef<T: GraphQLNamedType>(typeDef: T): T {
231267
const type = _getNamedType(typeDef.name);
232268
invariant(type, 'Missing type from schema');
@@ -474,6 +510,18 @@ export function extendSchema(
474510
});
475511
}
476512

513+
function getDirective(
514+
directiveAST: DirectiveDefinition
515+
): GraphQLDirective {
516+
return new GraphQLDirective({
517+
name: directiveAST.name.value,
518+
locations: directiveAST.locations.map(
519+
node => ((node.value: any): DirectiveLocationEnum)
520+
),
521+
args: directiveAST.arguments && buildInputValues(directiveAST.arguments),
522+
});
523+
}
524+
477525
function buildImplementedInterfaces(typeAST: ObjectTypeDefinition) {
478526
return typeAST.interfaces &&
479527
typeAST.interfaces.map(getInterfaceTypeFromAST);

0 commit comments

Comments
 (0)