Skip to content

Commit c88f044

Browse files
committed
Add SDL validation rule to validate schema uniques
1 parent 0b15a5d commit c88f044

File tree

6 files changed

+144
-18
lines changed

6 files changed

+144
-18
lines changed

src/utilities/buildASTSchema.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import keyMap from '../jsutils/keyMap';
1111
import keyValMap from '../jsutils/keyValMap';
1212
import type { ObjMap } from '../jsutils/ObjMap';
1313
import { valueFromAST } from './valueFromAST';
14+
import { assertValidSDL } from '../validation/validate';
1415
import blockStringValue from '../language/blockStringValue';
1516
import { TokenKind } from '../language/lexer';
1617
import { parse } from '../language/parser';
@@ -86,6 +87,13 @@ export type BuildSchemaOptions = {
8687
* Default: false
8788
*/
8889
commentDescriptions?: boolean,
90+
91+
/**
92+
* Set to true to assume the SDL is valid.
93+
*
94+
* Default: false
95+
*/
96+
assumeValidSDL?: boolean,
8997
};
9098

9199
/**
@@ -112,6 +120,10 @@ export function buildASTSchema(
112120
throw new Error('Must provide a document ast.');
113121
}
114122

123+
if (!options || !options.assumeValidSDL) {
124+
assertValidSDL(ast);
125+
}
126+
115127
let schemaDef: ?SchemaDefinitionNode;
116128

117129
const typeDefs: Array<TypeDefinitionNode> = [];
@@ -121,9 +133,6 @@ export function buildASTSchema(
121133
const d = ast.definitions[i];
122134
switch (d.kind) {
123135
case Kind.SCHEMA_DEFINITION:
124-
if (schemaDef) {
125-
throw new Error('Must provide only one schema definition.');
126-
}
127136
schemaDef = d;
128137
break;
129138
case Kind.SCALAR_TYPE_DEFINITION:

src/utilities/extendSchema.js

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import keyMap from '../jsutils/keyMap';
1212
import keyValMap from '../jsutils/keyValMap';
1313
import objectValues from '../jsutils/objectValues';
1414
import { ASTDefinitionBuilder } from './buildASTSchema';
15+
import { assertValidSDLExtension } from '../validation/validate';
1516
import { GraphQLError } from '../error/GraphQLError';
1617
import { isSchema, GraphQLSchema } from '../type/schema';
1718
import { isIntrospectionType } from '../type/introspection';
@@ -67,6 +68,13 @@ type Options = {|
6768
* Default: false
6869
*/
6970
commentDescriptions?: boolean,
71+
72+
/**
73+
* Set to true to assume the SDL is valid.
74+
*
75+
* Default: false
76+
*/
77+
assumeValidSDL?: boolean,
7078
|};
7179

7280
/**
@@ -99,6 +107,10 @@ export function extendSchema(
99107
'Must provide valid Document AST',
100108
);
101109

110+
if (!options || !options.assumeValidSDL) {
111+
assertValidSDLExtension(schema, documentAST);
112+
}
113+
102114
// Collect the type definitions and extensions found in the document.
103115
const typeDefinitionMap = Object.create(null);
104116
const typeExtensionsMap = Object.create(null);
@@ -115,18 +127,6 @@ export function extendSchema(
115127
const def = documentAST.definitions[i];
116128
switch (def.kind) {
117129
case Kind.SCHEMA_DEFINITION:
118-
// Sanity check that a schema extension is not overriding the schema
119-
if (
120-
schema.astNode ||
121-
schema.getQueryType() ||
122-
schema.getMutationType() ||
123-
schema.getSubscriptionType()
124-
) {
125-
throw new GraphQLError(
126-
'Cannot define a new schema within a schema extension.',
127-
[def],
128-
);
129-
}
130130
schemaDef = def;
131131
break;
132132
case Kind.SCHEMA_EXTENSION:

src/validation/ValidationContext.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,19 @@ export class BaseValidationContext {
6464
}
6565
}
6666

67+
export class SDLValidationContext extends BaseValidationContext {
68+
_schema: ?GraphQLSchema;
69+
70+
constructor(ast: DocumentNode, schema?: ?GraphQLSchema): void {
71+
super(ast);
72+
this._schema = schema;
73+
}
74+
75+
getSchema(): ?GraphQLSchema {
76+
return this._schema;
77+
}
78+
}
79+
6780
export class ValidationContext extends BaseValidationContext {
6881
_schema: GraphQLSchema;
6982
_typeInfo: TypeInfo;
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* Copyright (c) 2018-present, Facebook, Inc.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict
8+
*/
9+
10+
import type { SDLValidationContext } from '../ValidationContext';
11+
import { GraphQLError } from '../../error';
12+
import type { ASTVisitor } from '../../language/visitor';
13+
14+
export function schemaDefinitionNotAloneMessage(): string {
15+
return 'Must provide only one schema definition.';
16+
}
17+
18+
export function canNotDefineSchemaWithinExtension(): string {
19+
return 'Cannot define a new schema within a schema extension.';
20+
}
21+
22+
/**
23+
* Lone Schema definition
24+
*
25+
* A GraphQL document is only valid if it contains only one schema definition.
26+
*/
27+
export function LoneSchemaDefinition(
28+
context: SDLValidationContext,
29+
): ASTVisitor {
30+
const oldSchema = context.getSchema();
31+
const alreadyDefined =
32+
oldSchema &&
33+
(oldSchema.astNode ||
34+
oldSchema.getQueryType() ||
35+
oldSchema.getMutationType() ||
36+
oldSchema.getSubscriptionType());
37+
38+
const schemaNodes = [];
39+
return {
40+
SchemaDefinition(node) {
41+
if (alreadyDefined) {
42+
context.reportError(
43+
new GraphQLError(canNotDefineSchemaWithinExtension(), [node]),
44+
);
45+
return;
46+
}
47+
schemaNodes.push(node);
48+
},
49+
Document: {
50+
leave() {
51+
if (schemaNodes.length > 1) {
52+
context.reportError(
53+
new GraphQLError(schemaDefinitionNotAloneMessage(), schemaNodes),
54+
);
55+
}
56+
},
57+
},
58+
};
59+
}

src/validation/specifiedRules.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@
88
*/
99

1010
import type { ASTVisitor } from '../language/visitor';
11-
import type { ValidationContext } from './ValidationContext';
11+
import type {
12+
ValidationContext,
13+
SDLValidationContext,
14+
} from './ValidationContext';
1215

1316
// Spec Section: "Executable Definitions"
1417
import { ExecutableDefinitions } from './rules/ExecutableDefinitions';
@@ -122,3 +125,9 @@ export const specifiedRules: Array<(ValidationContext) => ASTVisitor> = [
122125
OverlappingFieldsCanBeMerged,
123126
UniqueInputFieldNames,
124127
];
128+
129+
import { LoneSchemaDefinition } from './rules/LoneSchemaDefinition';
130+
131+
export const specifiedSDLRules: Array<(SDLValidationContext) => ASTVisitor> = [
132+
LoneSchemaDefinition,
133+
];

src/validation/validate.js

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ import type { DocumentNode } from '../language/ast';
1515
import type { GraphQLSchema } from '../type/schema';
1616
import { assertValidSchema } from '../type/validate';
1717
import { TypeInfo } from '../utilities/TypeInfo';
18-
import { specifiedRules } from './specifiedRules';
19-
import { ValidationContext } from './ValidationContext';
18+
import { specifiedRules, specifiedSDLRules } from './specifiedRules';
19+
import { SDLValidationContext, ValidationContext } from './ValidationContext';
2020

2121
/**
2222
* Implements the "Validation" section of the spec.
@@ -52,3 +52,39 @@ export function validate(
5252
visit(documentAST, visitWithTypeInfo(typeInfo, visitor));
5353
return context.getErrors();
5454
}
55+
56+
export function validateSDL(
57+
documentAST: DocumentNode,
58+
schemaToExtend?: ?GraphQLSchema,
59+
rules?: $ReadOnlyArray<any> = specifiedSDLRules,
60+
): $ReadOnlyArray<GraphQLError> {
61+
const context = new SDLValidationContext(documentAST, schemaToExtend);
62+
const visitors = rules.map(rule => rule(context));
63+
visit(documentAST, visitInParallel(visitors));
64+
return context.getErrors();
65+
}
66+
67+
/**
68+
* Utility function which asserts a SDL document is valid by throwing an error
69+
* if it is invalid.
70+
*/
71+
export function assertValidSDL(documentAST: DocumentNode): void {
72+
const errors = validateSDL(documentAST);
73+
if (errors.length !== 0) {
74+
throw new Error(errors.map(error => error.message).join('\n\n'));
75+
}
76+
}
77+
78+
/**
79+
* Utility function which asserts a SDL document is valid by throwing an error
80+
* if it is invalid.
81+
*/
82+
export function assertValidSDLExtension(
83+
schema: GraphQLSchema,
84+
documentAST: DocumentNode,
85+
): void {
86+
const errors = validateSDL(documentAST, schema);
87+
if (errors.length !== 0) {
88+
throw new Error(errors.map(error => error.message).join('\n\n'));
89+
}
90+
}

0 commit comments

Comments
 (0)