Skip to content

Commit c008d0f

Browse files
fix(validate): no reusing root types (#3453)
Co-authored-by: Ivan Goncharov <ivan.goncharov.ua@gmail.com>
1 parent e59ae4c commit c008d0f

File tree

4 files changed

+151
-22
lines changed

4 files changed

+151
-22
lines changed

src/jsutils/didYouMean.ts

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { orList } from './formatList';
2+
13
const MAX_SUGGESTIONS = 5;
24

35
/**
@@ -12,26 +14,21 @@ export function didYouMean(
1214
firstArg: string | ReadonlyArray<string>,
1315
secondArg?: ReadonlyArray<string>,
1416
) {
15-
const [subMessage, suggestionsArg] = secondArg
17+
const [subMessage, suggestions] = secondArg
1618
? [firstArg as string, secondArg]
1719
: [undefined, firstArg as ReadonlyArray<string>];
1820

21+
if (suggestions.length === 0) {
22+
return '';
23+
}
24+
1925
let message = ' Did you mean ';
2026
if (subMessage) {
2127
message += subMessage + ' ';
2228
}
2329

24-
const suggestions = suggestionsArg.map((x) => `"${x}"`);
25-
switch (suggestions.length) {
26-
case 0:
27-
return '';
28-
case 1:
29-
return message + suggestions[0] + '?';
30-
case 2:
31-
return message + suggestions[0] + ' or ' + suggestions[1] + '?';
32-
}
33-
34-
const selected = suggestions.slice(0, MAX_SUGGESTIONS);
35-
const lastItem = selected.pop();
36-
return message + selected.join(', ') + ', or ' + lastItem + '?';
30+
const suggestionList = orList(
31+
suggestions.slice(0, MAX_SUGGESTIONS).map((x) => `"${x}"`),
32+
);
33+
return message + suggestionList + '?';
3734
}

src/jsutils/formatList.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { invariant } from './invariant';
2+
3+
/**
4+
* Given [ A, B, C ] return 'A, B, or C'.
5+
*/
6+
export function orList(items: ReadonlyArray<string>): string {
7+
return formatList('or', items);
8+
}
9+
10+
/**
11+
* Given [ A, B, C ] return 'A, B, and C'.
12+
*/
13+
export function andList(items: ReadonlyArray<string>): string {
14+
return formatList('and', items);
15+
}
16+
17+
function formatList(conjunction: string, items: ReadonlyArray<string>): string {
18+
invariant(items.length !== 0);
19+
20+
switch (items.length) {
21+
case 1:
22+
return items[0];
23+
case 2:
24+
return items[0] + ' ' + conjunction + ' ' + items[1];
25+
}
26+
27+
const allButLast = items.slice(0, -1);
28+
const lastItem = items[items.length - 1];
29+
return allButLast.join(', ') + ', ' + conjunction + ' ' + lastItem;
30+
}

src/type/__tests__/validation-test.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,86 @@ describe('Type System: A Schema must have Object root types', () => {
433433
});
434434
});
435435

436+
describe('Type System: Root types must all be different if provided', () => {
437+
it('accepts a Schema with different root types', () => {
438+
const schema = buildSchema(`
439+
type SomeObject1 {
440+
field: String
441+
}
442+
443+
type SomeObject2 {
444+
field: String
445+
}
446+
447+
type SomeObject3 {
448+
field: String
449+
}
450+
451+
schema {
452+
query: SomeObject1
453+
mutation: SomeObject2
454+
subscription: SomeObject3
455+
}
456+
`);
457+
expectJSON(validateSchema(schema)).toDeepEqual([]);
458+
});
459+
460+
it('rejects a Schema where the same type is used for multiple root types', () => {
461+
const schema = buildSchema(`
462+
type SomeObject {
463+
field: String
464+
}
465+
466+
type UniqueObject {
467+
field: String
468+
}
469+
470+
schema {
471+
query: SomeObject
472+
mutation: UniqueObject
473+
subscription: SomeObject
474+
}
475+
`);
476+
477+
expectJSON(validateSchema(schema)).toDeepEqual([
478+
{
479+
message:
480+
'All root types must be different, "SomeObject" type is used as query and subscription root types.',
481+
locations: [
482+
{ line: 11, column: 16 },
483+
{ line: 13, column: 23 },
484+
],
485+
},
486+
]);
487+
});
488+
489+
it('rejects a Schema where the same type is used for all root types', () => {
490+
const schema = buildSchema(`
491+
type SomeObject {
492+
field: String
493+
}
494+
495+
schema {
496+
query: SomeObject
497+
mutation: SomeObject
498+
subscription: SomeObject
499+
}
500+
`);
501+
502+
expectJSON(validateSchema(schema)).toDeepEqual([
503+
{
504+
message:
505+
'All root types must be different, "SomeObject" type is used as query, mutation, and subscription root types.',
506+
locations: [
507+
{ line: 7, column: 16 },
508+
{ line: 8, column: 19 },
509+
{ line: 9, column: 23 },
510+
],
511+
},
512+
]);
513+
});
514+
});
515+
436516
describe('Type System: Objects must have fields', () => {
437517
it('accepts an Object type with fields object', () => {
438518
const schema = buildSchema(`

src/type/validate.ts

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import { AccumulatorMap } from '../jsutils/AccumulatorMap';
12
import { capitalize } from '../jsutils/capitalize';
3+
import { andList } from '../jsutils/formatList';
24
import { inspect } from '../jsutils/inspect';
35
import type { Maybe } from '../jsutils/Maybe';
46

@@ -118,18 +120,38 @@ function validateRootTypes(context: SchemaValidationContext): void {
118120
context.reportError('Query root type must be provided.', schema.astNode);
119121
}
120122

123+
const rootTypesMap = new AccumulatorMap<
124+
GraphQLObjectType,
125+
OperationTypeNode
126+
>();
121127
for (const operationType of Object.values(OperationTypeNode)) {
122128
const rootType = schema.getRootType(operationType);
123129

124-
if (rootType != null && !isObjectType(rootType)) {
125-
const operationTypeStr = capitalize(operationType);
126-
const rootTypeStr = inspect(rootType);
130+
if (rootType != null) {
131+
if (!isObjectType(rootType)) {
132+
const operationTypeStr = capitalize(operationType);
133+
const rootTypeStr = inspect(rootType);
134+
context.reportError(
135+
operationType === OperationTypeNode.QUERY
136+
? `${operationTypeStr} root type must be Object type, it cannot be ${rootTypeStr}.`
137+
: `${operationTypeStr} root type must be Object type if provided, it cannot be ${rootTypeStr}.`,
138+
getOperationTypeNode(schema, operationType) ??
139+
(rootType as any).astNode,
140+
);
141+
} else {
142+
rootTypesMap.add(rootType, operationType);
143+
}
144+
}
145+
}
146+
147+
for (const [rootType, operationTypes] of rootTypesMap.entries()) {
148+
if (operationTypes.length > 1) {
149+
const operationList = andList(operationTypes);
127150
context.reportError(
128-
operationType === OperationTypeNode.QUERY
129-
? `${operationTypeStr} root type must be Object type, it cannot be ${rootTypeStr}.`
130-
: `${operationTypeStr} root type must be Object type if provided, it cannot be ${rootTypeStr}.`,
131-
getOperationTypeNode(schema, operationType) ??
132-
(rootType as any).astNode,
151+
`All root types must be different, "${rootType.name}" type is used as ${operationList} root types.`,
152+
operationTypes.map((operationType) =>
153+
getOperationTypeNode(schema, operationType),
154+
),
133155
);
134156
}
135157
}

0 commit comments

Comments
 (0)