Skip to content

Commit ce1d49d

Browse files
committed
Add SDL validation rule to validate schema uniques
1 parent d40e418 commit ce1d49d

File tree

6 files changed

+155
-22
lines changed

6 files changed

+155
-22
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(documentAST, schema);
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: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import type {
1919
FragmentSpreadNode,
2020
FragmentDefinitionNode,
2121
} from '../language/ast';
22+
import type { ASTVisitor } from '../language/visitor';
2223
import type { GraphQLSchema } from '../type/schema';
2324
import type {
2425
GraphQLInputType,
@@ -64,6 +65,21 @@ export class ASTValidationContext {
6465
}
6566
}
6667

68+
export class SDLValidationContext extends ASTValidationContext {
69+
_schema: ?GraphQLSchema;
70+
71+
constructor(ast: DocumentNode, schema?: ?GraphQLSchema): void {
72+
super(ast);
73+
this._schema = schema;
74+
}
75+
76+
getSchema(): ?GraphQLSchema {
77+
return this._schema;
78+
}
79+
}
80+
81+
export type SDLValidationRule = SDLValidationContext => ASTVisitor;
82+
6783
export class ValidationContext extends ASTValidationContext {
6884
_schema: GraphQLSchema;
6985
_typeInfo: TypeInfo;
@@ -234,3 +250,5 @@ export class ValidationContext extends ASTValidationContext {
234250
return this._typeInfo.getArgument();
235251
}
236252
}
253+
254+
export type ValidationRule = ValidationContext => ASTVisitor;
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: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@
77
* @flow strict
88
*/
99

10-
import type { ASTVisitor } from '../language/visitor';
11-
import type { ValidationContext } from './ValidationContext';
10+
import type { ValidationRule, SDLValidationRule } from './ValidationContext';
1211

1312
// Spec Section: "Executable Definitions"
1413
import { ExecutableDefinitions } from './rules/ExecutableDefinitions';
@@ -94,7 +93,7 @@ import { UniqueInputFieldNames } from './rules/UniqueInputFieldNames';
9493
* The order of the rules in this list has been adjusted to lead to the
9594
* most clear output when encountering multiple validation errors.
9695
*/
97-
export const specifiedRules: Array<(ValidationContext) => ASTVisitor> = [
96+
export const specifiedRules: $ReadOnlyArray<ValidationRule> = [
9897
ExecutableDefinitions,
9998
UniqueOperationNames,
10099
LoneAnonymousOperation,
@@ -122,3 +121,10 @@ export const specifiedRules: Array<(ValidationContext) => ASTVisitor> = [
122121
OverlappingFieldsCanBeMerged,
123122
UniqueInputFieldNames,
124123
];
124+
125+
import { LoneSchemaDefinition } from './rules/LoneSchemaDefinition';
126+
127+
// @internal
128+
export const specifiedSDLRules: $ReadOnlyArray<SDLValidationRule> = [
129+
LoneSchemaDefinition,
130+
];

src/validation/validate.js

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@
99

1010
import invariant from '../jsutils/invariant';
1111
import type { GraphQLError } from '../error';
12-
import type { ASTVisitor } from '../language/visitor';
1312
import { visit, visitInParallel, visitWithTypeInfo } from '../language/visitor';
1413
import type { DocumentNode } from '../language/ast';
1514
import type { GraphQLSchema } from '../type/schema';
1615
import { assertValidSchema } from '../type/validate';
1716
import { TypeInfo } from '../utilities/TypeInfo';
18-
import { specifiedRules } from './specifiedRules';
19-
import { ValidationContext } from './ValidationContext';
17+
import { specifiedRules, specifiedSDLRules } from './specifiedRules';
18+
import type { SDLValidationRule, ValidationRule } from './ValidationContext';
19+
import { SDLValidationContext, ValidationContext } from './ValidationContext';
2020

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

0 commit comments

Comments
 (0)