Skip to content

Commit e888d21

Browse files
committed
Support extendSchema()
1 parent 7a93bba commit e888d21

File tree

4 files changed

+234
-21
lines changed

4 files changed

+234
-21
lines changed

src/language/ast.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,7 @@ export type TypeSystemDefinitionNode =
396396
export type SchemaDefinitionNode = {
397397
+kind: 'SchemaDefinition',
398398
+loc?: Location,
399-
+directives: $ReadOnlyArray<DirectiveNode>,
399+
+directives?: $ReadOnlyArray<DirectiveNode>,
400400
+operationTypes: $ReadOnlyArray<OperationTypeDefinitionNode>,
401401
};
402402

@@ -517,8 +517,8 @@ export type TypeSystemExtensionNode = SchemaExtensionNode | TypeExtensionNode;
517517
export type SchemaExtensionNode = {
518518
+kind: 'SchemaExtension',
519519
+loc?: Location,
520-
+directives: $ReadOnlyArray<DirectiveNode>,
521-
+operationTypes: $ReadOnlyArray<OperationTypeDefinitionNode>,
520+
+directives?: $ReadOnlyArray<DirectiveNode>,
521+
+operationTypes?: $ReadOnlyArray<OperationTypeDefinitionNode>,
522522
};
523523

524524
// Type Extensions

src/type/schema.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ import type {
2121
GraphQLAbstractType,
2222
GraphQLObjectType,
2323
} from './definition';
24-
import type { SchemaDefinitionNode } from '../language/ast';
24+
import type {
25+
SchemaDefinitionNode,
26+
SchemaExtensionNode,
27+
} from '../language/ast';
2528
import {
2629
GraphQLDirective,
2730
isDirective,
@@ -73,6 +76,7 @@ export function isSchema(schema) {
7376
*/
7477
export class GraphQLSchema {
7578
astNode: ?SchemaDefinitionNode;
79+
extensionASTNodes: ?$ReadOnlyArray<SchemaExtensionNode>;
7680
_queryType: ?GraphQLObjectType;
7781
_mutationType: ?GraphQLObjectType;
7882
_subscriptionType: ?GraphQLObjectType;
@@ -120,6 +124,7 @@ export class GraphQLSchema {
120124
// Provide specified directives (e.g. @include and @skip) by default.
121125
this._directives = config.directives || specifiedDirectives;
122126
this.astNode = config.astNode;
127+
this.extensionASTNodes = config.extensionASTNodes;
123128

124129
// Build type map now to detect any errors within this schema.
125130
let initialTypes: Array<?GraphQLNamedType> = [
@@ -255,6 +260,7 @@ export type GraphQLSchemaConfig = {
255260
types?: ?Array<GraphQLNamedType>,
256261
directives?: ?Array<GraphQLDirective>,
257262
astNode?: ?SchemaDefinitionNode,
263+
extensionASTNodes?: ?$ReadOnlyArray<SchemaExtensionNode>,
258264
...GraphQLSchemaValidationOptions,
259265
};
260266

src/utilities/__tests__/extendSchema-test.js

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -876,4 +876,182 @@ describe('extendSchema', () => {
876876
);
877877
});
878878
});
879+
880+
describe('can add additional root operation types', () => {
881+
it('does not automatically include common root type names', () => {
882+
const ast = parse(`
883+
type Mutation {
884+
doSomething: String
885+
}
886+
`);
887+
const schema = extendSchema(testSchema, ast);
888+
expect(schema.getMutationType()).to.equal(null);
889+
});
890+
891+
it('does not allow new schema within an extension', () => {
892+
const ast = parse(`
893+
schema {
894+
mutation: Mutation
895+
}
896+
897+
type Mutation {
898+
doSomething: String
899+
}
900+
`);
901+
expect(() => extendSchema(testSchema, ast)).to.throw(
902+
'Cannot define a new schema within a schema extension.',
903+
);
904+
});
905+
906+
it('adds new root types via schema extension', () => {
907+
const ast = parse(`
908+
extend schema {
909+
mutation: Mutation
910+
}
911+
912+
type Mutation {
913+
doSomething: String
914+
}
915+
`);
916+
const schema = extendSchema(testSchema, ast);
917+
const mutationType = schema.getMutationType();
918+
expect(mutationType && mutationType.name).to.equal('Mutation');
919+
});
920+
921+
it('adds multiple new root types via schema extension', () => {
922+
const ast = parse(`
923+
extend schema {
924+
mutation: Mutation
925+
subscription: Subscription
926+
}
927+
928+
type Mutation {
929+
doSomething: String
930+
}
931+
932+
type Subscription {
933+
hearSomething: String
934+
}
935+
`);
936+
const schema = extendSchema(testSchema, ast);
937+
const mutationType = schema.getMutationType();
938+
const subscriptionType = schema.getSubscriptionType();
939+
expect(mutationType && mutationType.name).to.equal('Mutation');
940+
expect(subscriptionType && subscriptionType.name).to.equal(
941+
'Subscription',
942+
);
943+
});
944+
945+
it('applies multiple schema extensions', () => {
946+
const ast = parse(`
947+
extend schema {
948+
mutation: Mutation
949+
}
950+
951+
extend schema {
952+
subscription: Subscription
953+
}
954+
955+
type Mutation {
956+
doSomething: String
957+
}
958+
959+
type Subscription {
960+
hearSomething: String
961+
}
962+
`);
963+
const schema = extendSchema(testSchema, ast);
964+
const mutationType = schema.getMutationType();
965+
const subscriptionType = schema.getSubscriptionType();
966+
expect(mutationType && mutationType.name).to.equal('Mutation');
967+
expect(subscriptionType && subscriptionType.name).to.equal(
968+
'Subscription',
969+
);
970+
});
971+
972+
it('schema extension AST are available from schema object', () => {
973+
const ast = parse(`
974+
extend schema {
975+
mutation: Mutation
976+
}
977+
978+
extend schema {
979+
subscription: Subscription
980+
}
981+
982+
type Mutation {
983+
doSomething: String
984+
}
985+
986+
type Subscription {
987+
hearSomething: String
988+
}
989+
`);
990+
const schema = extendSchema(testSchema, ast);
991+
expect(schema.extensionASTNodes.map(print).join('\n')).to.equal(dedent`
992+
extend schema {
993+
mutation: Mutation
994+
}
995+
extend schema {
996+
subscription: Subscription
997+
}`);
998+
});
999+
1000+
it('does not allow redefining an existing root type', () => {
1001+
const ast = parse(`
1002+
extend schema {
1003+
query: SomeType
1004+
}
1005+
1006+
type SomeType {
1007+
seeSomething: String
1008+
}
1009+
`);
1010+
expect(() => extendSchema(testSchema, ast)).to.throw(
1011+
'Must provide only one query type in schema.',
1012+
);
1013+
});
1014+
1015+
it('does not allow defining a root operation type twice', () => {
1016+
const ast = parse(`
1017+
extend schema {
1018+
mutation: Mutation
1019+
}
1020+
1021+
extend schema {
1022+
mutation: Mutation
1023+
}
1024+
1025+
type Mutation {
1026+
doSomething: String
1027+
}
1028+
`);
1029+
expect(() => extendSchema(testSchema, ast)).to.throw(
1030+
'Must provide only one mutation type in schema.',
1031+
);
1032+
});
1033+
1034+
it('does not allow defining a root operation type with different types', () => {
1035+
const ast = parse(`
1036+
extend schema {
1037+
mutation: Mutation
1038+
}
1039+
1040+
extend schema {
1041+
mutation: SomethingElse
1042+
}
1043+
1044+
type Mutation {
1045+
doSomething: String
1046+
}
1047+
1048+
type SomethingElse {
1049+
doSomethingElse: String
1050+
}
1051+
`);
1052+
expect(() => extendSchema(testSchema, ast)).to.throw(
1053+
'Must provide only one mutation type in schema.',
1054+
);
1055+
});
1056+
});
8791057
});

src/utilities/extendSchema.js

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,11 @@ import { GraphQLDirective } from '../type/directives';
3535
import { Kind } from '../language/kinds';
3636

3737
import type { GraphQLType, GraphQLNamedType } from '../type/definition';
38-
import type { DocumentNode, DirectiveDefinitionNode } from '../language/ast';
38+
import type {
39+
DocumentNode,
40+
DirectiveDefinitionNode,
41+
SchemaExtensionNode,
42+
} from '../language/ast';
3943

4044
type Options = {|
4145
...GraphQLSchemaValidationOptions,
@@ -88,9 +92,21 @@ export function extendSchema(
8892
// have the same name. For example, a type named "skip".
8993
const directiveDefinitions: Array<DirectiveDefinitionNode> = [];
9094

95+
// Schema extensions are collected which may add additional operation types.
96+
const schemaExtensions: Array<SchemaExtensionNode> = [];
97+
9198
for (let i = 0; i < documentAST.definitions.length; i++) {
9299
const def = documentAST.definitions[i];
93100
switch (def.kind) {
101+
case Kind.SCHEMA_DEFINITION:
102+
// Sanity check that a schema extension is not defining a new schema
103+
throw new GraphQLError(
104+
'Cannot define a new schema within a schema extension.',
105+
[def],
106+
);
107+
case Kind.SCHEMA_EXTENSION:
108+
schemaExtensions.push(def);
109+
break;
94110
case Kind.OBJECT_TYPE_DEFINITION:
95111
case Kind.INTERFACE_TYPE_DEFINITION:
96112
case Kind.ENUM_TYPE_DEFINITION:
@@ -156,7 +172,8 @@ export function extendSchema(
156172
if (
157173
Object.keys(typeExtensionsMap).length === 0 &&
158174
Object.keys(typeDefinitionMap).length === 0 &&
159-
directiveDefinitions.length === 0
175+
directiveDefinitions.length === 0 &&
176+
schemaExtensions.length === 0
160177
) {
161178
return schema;
162179
}
@@ -181,21 +198,32 @@ export function extendSchema(
181198

182199
const extendTypeCache = Object.create(null);
183200

184-
// Get the root Query, Mutation, and Subscription object types.
201+
// Get the extended root operation types.
185202
const existingQueryType = schema.getQueryType();
186-
const queryType = existingQueryType
187-
? getExtendedType(existingQueryType)
188-
: null;
189-
190203
const existingMutationType = schema.getMutationType();
191-
const mutationType = existingMutationType
192-
? getExtendedType(existingMutationType)
193-
: null;
194-
195204
const existingSubscriptionType = schema.getSubscriptionType();
196-
const subscriptionType = existingSubscriptionType
197-
? getExtendedType(existingSubscriptionType)
198-
: null;
205+
const operationTypes = {
206+
query: existingQueryType ? getExtendedType(existingQueryType) : null,
207+
mutation: existingMutationType
208+
? getExtendedType(existingMutationType)
209+
: null,
210+
subscription: existingSubscriptionType
211+
? getExtendedType(existingSubscriptionType)
212+
: null,
213+
};
214+
215+
// Then, incorporate all schema extensions.
216+
schemaExtensions.forEach(schemaExtension => {
217+
if (schemaExtension.operationTypes) {
218+
schemaExtension.operationTypes.forEach(operationType => {
219+
const operation = operationType.operation;
220+
if (operationTypes[operation]) {
221+
throw new Error(`Must provide only one ${operation} type in schema.`);
222+
}
223+
operationTypes[operation] = astBuilder.buildType(operationType.type);
224+
});
225+
}
226+
});
199227

200228
const types = [
201229
// Iterate through all types, getting the type definition for each, ensuring
@@ -215,12 +243,13 @@ export function extendSchema(
215243

216244
// Then produce and return a Schema with these types.
217245
return new GraphQLSchema({
218-
query: queryType,
219-
mutation: mutationType,
220-
subscription: subscriptionType,
246+
query: operationTypes.query,
247+
mutation: operationTypes.mutation,
248+
subscription: operationTypes.subscription,
221249
types,
222250
directives: getMergedDirectives(),
223251
astNode: schema.astNode,
252+
extensionASTNodes: schemaExtensions,
224253
allowedLegacyNames,
225254
});
226255

0 commit comments

Comments
 (0)