Skip to content

Commit 4c6e0a9

Browse files
IvanGoncharovleebyron
authored andcommitted
Fix extend schema from #1314 (#1323)
* Simplify + fix Flow types * Fix 'extensionASTNodes' for schema extended twice * Replace 'getAll*Nodes' with generic 'getAllNodes' function * Correctly report error for invalid root type in extended schema
1 parent 4ac4bf4 commit 4c6e0a9

File tree

4 files changed

+110
-52
lines changed

4 files changed

+110
-52
lines changed

src/type/__tests__/validation-test.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,59 @@ describe('Type System: A Schema must have Object root types', () => {
323323
]);
324324
});
325325

326+
it('rejects a schema extended with invalid root types', () => {
327+
let schema = buildSchema(`
328+
input SomeInputObject {
329+
test: String
330+
}
331+
`);
332+
333+
schema = extendSchema(
334+
schema,
335+
parse(`
336+
extend schema {
337+
query: SomeInputObject
338+
}
339+
`),
340+
);
341+
342+
schema = extendSchema(
343+
schema,
344+
parse(`
345+
extend schema {
346+
mutation: SomeInputObject
347+
}
348+
`),
349+
);
350+
351+
schema = extendSchema(
352+
schema,
353+
parse(`
354+
extend schema {
355+
subscription: SomeInputObject
356+
}
357+
`),
358+
);
359+
360+
expect(validateSchema(schema)).to.deep.equal([
361+
{
362+
message:
363+
'Query root type must be Object type, it cannot be SomeInputObject.',
364+
locations: [{ line: 3, column: 18 }],
365+
},
366+
{
367+
message:
368+
'Mutation root type must be Object type if provided, it cannot be SomeInputObject.',
369+
locations: [{ line: 3, column: 21 }],
370+
},
371+
{
372+
message:
373+
'Subscription root type must be Object type if provided, it cannot be SomeInputObject.',
374+
locations: [{ line: 3, column: 25 }],
375+
},
376+
]);
377+
});
378+
326379
it('rejects a Schema whose directives are incorrectly typed', () => {
327380
const schema = new GraphQLSchema({
328381
query: SomeObjectType,

src/type/validate.js

Lines changed: 24 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,6 @@ import objectValues from '../jsutils/objectValues';
3636
import { GraphQLError } from '../error/GraphQLError';
3737
import type {
3838
ASTNode,
39-
ObjectTypeDefinitionNode,
40-
ObjectTypeExtensionNode,
41-
InterfaceTypeDefinitionNode,
42-
InterfaceTypeExtensionNode,
4339
FieldDefinitionNode,
4440
EnumValueDefinitionNode,
4541
InputValueDefinitionNode,
@@ -156,13 +152,17 @@ function getOperationTypeNode(
156152
type: GraphQLObjectType,
157153
operation: string,
158154
): ?ASTNode {
159-
const astNode = schema.astNode;
160-
const operationTypeNode =
161-
astNode &&
162-
astNode.operationTypes.find(
163-
operationType => operationType.operation === operation,
164-
);
165-
return operationTypeNode ? operationTypeNode.type : type && type.astNode;
155+
for (const node of getAllNodes(schema)) {
156+
if (node.operationTypes) {
157+
for (const operationType of node.operationTypes) {
158+
if (operationType.operation === operation) {
159+
return operationType.type;
160+
}
161+
}
162+
}
163+
}
164+
165+
return type.astNode;
166166
}
167167

168168
function validateDirectives(context: SchemaValidationContext): void {
@@ -283,7 +283,7 @@ function validateFields(
283283
if (fields.length === 0) {
284284
context.reportError(
285285
`Type ${type.name} must define one or more fields.`,
286-
getAllObjectOrInterfaceNodes(type),
286+
getAllNodes(type),
287287
);
288288
}
289289

@@ -578,29 +578,16 @@ function validateInputFields(
578578
});
579579
}
580580

581-
function getAllObjectNodes(
582-
type: GraphQLObjectType,
583-
): $ReadOnlyArray<ObjectTypeDefinitionNode | ObjectTypeExtensionNode> {
584-
return type.astNode
585-
? type.extensionASTNodes
586-
? [type.astNode].concat(type.extensionASTNodes)
587-
: [type.astNode]
588-
: type.extensionASTNodes || [];
589-
}
590-
591-
function getAllObjectOrInterfaceNodes(
592-
type: GraphQLObjectType | GraphQLInterfaceType,
593-
): $ReadOnlyArray<
594-
| ObjectTypeDefinitionNode
595-
| ObjectTypeExtensionNode
596-
| InterfaceTypeDefinitionNode
597-
| InterfaceTypeExtensionNode,
598-
> {
599-
return type.astNode
600-
? type.extensionASTNodes
601-
? [type.astNode].concat(type.extensionASTNodes)
602-
: [type.astNode]
603-
: type.extensionASTNodes || [];
581+
function getAllNodes<T: ASTNode, K: ASTNode>(object: {
582+
+astNode: ?T,
583+
+extensionASTNodes?: ?$ReadOnlyArray<K>,
584+
}): $ReadOnlyArray<T | K> {
585+
const { astNode, extensionASTNodes } = object;
586+
return astNode
587+
? extensionASTNodes
588+
? [astNode].concat(extensionASTNodes)
589+
: [astNode]
590+
: extensionASTNodes || [];
604591
}
605592

606593
function getImplementsInterfaceNode(
@@ -615,7 +602,7 @@ function getAllImplementsInterfaceNodes(
615602
iface: GraphQLInterfaceType,
616603
): $ReadOnlyArray<NamedTypeNode> {
617604
const implementsNodes = [];
618-
const astNodes = getAllObjectNodes(type);
605+
const astNodes = getAllNodes(type);
619606
for (let i = 0; i < astNodes.length; i++) {
620607
const astNode = astNodes[i];
621608
if (astNode && astNode.interfaces) {
@@ -641,7 +628,7 @@ function getAllFieldNodes(
641628
fieldName: string,
642629
): $ReadOnlyArray<FieldDefinitionNode> {
643630
const fieldNodes = [];
644-
const astNodes = getAllObjectOrInterfaceNodes(type);
631+
const astNodes = getAllNodes(type);
645632
for (let i = 0; i < astNodes.length; i++) {
646633
const astNode = astNodes[i];
647634
if (astNode && astNode.fields) {

src/utilities/__tests__/extendSchema-test.js

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -987,14 +987,25 @@ describe('extendSchema', () => {
987987
hearSomething: String
988988
}
989989
`);
990-
const schema = extendSchema(testSchema, ast);
991-
expect(schema.extensionASTNodes.map(print).join('\n')).to.equal(dedent`
990+
let schema = extendSchema(testSchema, ast);
991+
992+
const secondAST = parse(`
993+
extend schema @foo
994+
995+
directive @foo on SCHEMA
996+
`);
997+
schema = extendSchema(schema, secondAST);
998+
999+
const nodes = schema.extensionASTNodes;
1000+
expect(nodes.map(n => print(n) + '\n').join('')).to.equal(dedent`
9921001
extend schema {
9931002
mutation: Mutation
9941003
}
9951004
extend schema {
9961005
subscription: Subscription
997-
}`);
1006+
}
1007+
extend schema @foo
1008+
`);
9981009
});
9991010

10001011
it('does not allow redefining an existing root type', () => {

src/utilities/extendSchema.js

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -199,17 +199,10 @@ export function extendSchema(
199199
const extendTypeCache = Object.create(null);
200200

201201
// Get the extended root operation types.
202-
const existingQueryType = schema.getQueryType();
203-
const existingMutationType = schema.getMutationType();
204-
const existingSubscriptionType = schema.getSubscriptionType();
205202
const operationTypes = {
206-
query: existingQueryType ? getExtendedType(existingQueryType) : null,
207-
mutation: existingMutationType
208-
? getExtendedType(existingMutationType)
209-
: null,
210-
subscription: existingSubscriptionType
211-
? getExtendedType(existingSubscriptionType)
212-
: null,
203+
query: getExtendedMaybeType(schema.getQueryType()),
204+
mutation: getExtendedMaybeType(schema.getMutationType()),
205+
subscription: getExtendedMaybeType(schema.getSubscriptionType()),
213206
};
214207

215208
// Then, incorporate all schema extensions.
@@ -220,11 +213,21 @@ export function extendSchema(
220213
if (operationTypes[operation]) {
221214
throw new Error(`Must provide only one ${operation} type in schema.`);
222215
}
223-
operationTypes[operation] = astBuilder.buildType(operationType.type);
216+
const typeRef = operationType.type;
217+
// Note: While this could make early assertions to get the correctly
218+
// typed values, that would throw immediately while type system
219+
// validation with validateSchema() will produce more actionable results.
220+
operationTypes[operation] = (astBuilder.buildType(typeRef): any);
224221
});
225222
}
226223
});
227224

225+
const schemaExtensionASTNodes = schemaExtensions
226+
? schema.extensionASTNodes
227+
? schema.extensionASTNodes.concat(schemaExtensions)
228+
: schemaExtensions
229+
: schema.extensionASTNodes;
230+
228231
const types = [
229232
// Iterate through all types, getting the type definition for each, ensuring
230233
// that any type not directly referenced by a field will get created.
@@ -249,7 +252,7 @@ export function extendSchema(
249252
types,
250253
directives: getMergedDirectives(),
251254
astNode: schema.astNode,
252-
extensionASTNodes: schemaExtensions,
255+
extensionASTNodes: schemaExtensionASTNodes,
253256
allowedLegacyNames,
254257
});
255258

@@ -265,6 +268,10 @@ export function extendSchema(
265268
);
266269
}
267270

271+
function getExtendedMaybeType<T: GraphQLNamedType>(type: ?T): ?T {
272+
return type ? getExtendedType(type) : null;
273+
}
274+
268275
function getExtendedType<T: GraphQLNamedType>(type: T): T {
269276
if (!extendTypeCache[type.name]) {
270277
extendTypeCache[type.name] = extendType(type);

0 commit comments

Comments
 (0)