Skip to content

Commit

Permalink
Extract check for unique type names into separate rule
Browse files Browse the repository at this point in the history
  • Loading branch information
IvanGoncharov committed Dec 22, 2018
1 parent 51eda7b commit 257797a
Show file tree
Hide file tree
Showing 7 changed files with 215 additions and 77 deletions.
19 changes: 0 additions & 19 deletions src/utilities/__tests__/buildASTSchema-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -988,23 +988,4 @@ describe('Failures', () => {
'Specified query type "Foo" not found in document.',
);
});

it('Forbids duplicate type definitions', () => {
const sdl = `
schema {
query: Repeated
}
type Repeated {
id: Int
}
type Repeated {
id: String
}
`;
expect(() => buildSchema(sdl)).to.throw(
'Type "Repeated" was defined more than once.',
);
});
});
46 changes: 0 additions & 46 deletions src/utilities/__tests__/extendSchema-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1109,52 +1109,6 @@ describe('extendSchema', () => {
);
});

it('does not allow replacing an existing type', () => {
const existingTypeError = type =>
`Type "${type}" already exists in the schema.` +
' It cannot also be defined in this type definition.';

const typeSDL = `
type Bar
`;
expect(() => extendTestSchema(typeSDL)).to.throw(existingTypeError('Bar'));

const scalarSDL = `
scalar SomeScalar
`;
expect(() => extendTestSchema(scalarSDL)).to.throw(
existingTypeError('SomeScalar'),
);

const interfaceSDL = `
interface SomeInterface
`;
expect(() => extendTestSchema(interfaceSDL)).to.throw(
existingTypeError('SomeInterface'),
);

const enumSDL = `
enum SomeEnum
`;
expect(() => extendTestSchema(enumSDL)).to.throw(
existingTypeError('SomeEnum'),
);

const unionSDL = `
union SomeUnion
`;
expect(() => extendTestSchema(unionSDL)).to.throw(
existingTypeError('SomeUnion'),
);

const inputSDL = `
input SomeInput
`;
expect(() => extendTestSchema(inputSDL)).to.throw(
existingTypeError('SomeInput'),
);
});

it('does not allow replacing an existing field', () => {
const existingFieldError = (type, field) =>
`Field "${type}.${field}" already exists in the schema.` +
Expand Down
3 changes: 0 additions & 3 deletions src/utilities/buildASTSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,6 @@ export function buildASTSchema(
schemaDef = def;
} else if (isTypeDefinitionNode(def)) {
const typeName = def.name.value;
if (nodeMap[typeName]) {
throw new Error(`Type "${typeName}" was defined more than once.`);
}
typeDefs.push(def);
nodeMap[typeName] = def;
} else if (def.kind === Kind.DIRECTIVE_DEFINITION) {
Expand Down
9 changes: 0 additions & 9 deletions src/utilities/extendSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,16 +135,7 @@ export function extendSchema(
} else if (def.kind === Kind.SCHEMA_EXTENSION) {
schemaExtensions.push(def);
} else if (isTypeDefinitionNode(def)) {
// Sanity check that none of the defined types conflict with the
// schema's existing types.
const typeName = def.name.value;
if (schema.getType(typeName)) {
throw new GraphQLError(
`Type "${typeName}" already exists in the schema. It cannot also ` +
'be defined in this type definition.',
[def],
);
}
typeDefinitionMap[typeName] = def;
} else if (isTypeExtensionNode(def)) {
// Sanity check that this type extension exists within the
Expand Down
146 changes: 146 additions & 0 deletions src/validation/__tests__/UniqueTypeNames-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/**
* Copyright (c) 2018-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
*/

import { describe, it } from 'mocha';
import { buildSchema } from '../../utilities';
import { expectSDLValidationErrors } from './harness';
import {
UniqueTypeNames,
existedTypeNameMessage,
duplicateTypeNameMessage,
} from '../rules/UniqueTypeNames';

function expectSDLErrors(sdlStr, schema) {
return expectSDLValidationErrors(schema, UniqueTypeNames, sdlStr);
}

function expectValidSDL(sdlStr, schema) {
expectSDLErrors(sdlStr, schema).to.deep.equal([]);
}

describe('Validate: Unique type names', () => {
it('no types', () => {
expectValidSDL(`
directive @test on SCHEMA
`);
});

it('one type', () => {
expectValidSDL(`
type Foo
`);
});

it('many types', () => {
expectValidSDL(`
type Foo
type Bar
type Baz
`);
});

it('type and non-type definitions named the same', () => {
expectValidSDL(`
query Foo { __typename }
fragment Foo on Query { __typename }
directive @Foo on SCHEMA
type Foo
`);
});

it('types named the same', () => {
expectSDLErrors(`
type Foo
scalar Foo
type Foo
interface Foo
union Foo
enum Foo
input Foo
`).to.deep.equal([
{
message: duplicateTypeNameMessage('Foo'),
locations: [{ line: 2, column: 12 }, { line: 4, column: 14 }],
},
{
message: duplicateTypeNameMessage('Foo'),
locations: [{ line: 2, column: 12 }, { line: 5, column: 12 }],
},
{
message: duplicateTypeNameMessage('Foo'),
locations: [{ line: 2, column: 12 }, { line: 6, column: 17 }],
},
{
message: duplicateTypeNameMessage('Foo'),
locations: [{ line: 2, column: 12 }, { line: 7, column: 13 }],
},
{
message: duplicateTypeNameMessage('Foo'),
locations: [{ line: 2, column: 12 }, { line: 8, column: 12 }],
},
{
message: duplicateTypeNameMessage('Foo'),
locations: [{ line: 2, column: 12 }, { line: 9, column: 13 }],
},
]);
});

it('adding new type to existing schema', () => {
const schema = buildSchema('type Foo');

expectValidSDL('type Bar', schema);
});

it('adding new type to existing schema with same-named directive', () => {
const schema = buildSchema('directive @Foo on SCHEMA');

expectValidSDL('type Foo', schema);
});

it('adding conflicting types to existing schema', () => {
const schema = buildSchema('type Foo');
const sdl = `
scalar Foo
type Foo
interface Foo
union Foo
enum Foo
input Foo
`;

expectSDLErrors(sdl, schema).to.deep.equal([
{
message: existedTypeNameMessage('Foo'),
locations: [{ line: 2, column: 14 }],
},
{
message: existedTypeNameMessage('Foo'),
locations: [{ line: 3, column: 12 }],
},
{
message: existedTypeNameMessage('Foo'),
locations: [{ line: 4, column: 17 }],
},
{
message: existedTypeNameMessage('Foo'),
locations: [{ line: 5, column: 13 }],
},
{
message: existedTypeNameMessage('Foo'),
locations: [{ line: 6, column: 12 }],
},
{
message: existedTypeNameMessage('Foo'),
locations: [{ line: 7, column: 13 }],
},
]);
});
});
67 changes: 67 additions & 0 deletions src/validation/rules/UniqueTypeNames.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* Copyright (c) 2018-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
*/

import type { SDLValidationContext } from '../ValidationContext';
import { GraphQLError } from '../../error/GraphQLError';
import type { ASTVisitor } from '../../language/visitor';
import type { TypeDefinitionNode } from '../../language/ast';

export function duplicateTypeNameMessage(typeName: string): string {
return `There can be only one type named "${typeName}".`;
}

export function existedTypeNameMessage(typeName: string): string {
return (
`Type "${typeName}" already exists in the schema. ` +
'It cannot also be defined in this type definition.'
);
}

/**
* Unique type names
*
* A GraphQL document is only valid if all defined types have unique names.
*/
export function UniqueTypeNames(context: SDLValidationContext): ASTVisitor {
const knownTypeNames = Object.create(null);
const schema = context.getSchema();

return {
ScalarTypeDefinition: checkTypeName,
ObjectTypeDefinition: checkTypeName,
InterfaceTypeDefinition: checkTypeName,
UnionTypeDefinition: checkTypeName,
EnumTypeDefinition: checkTypeName,
InputObjectTypeDefinition: checkTypeName,
};

function checkTypeName(node: TypeDefinitionNode) {
const typeName = node.name.value;

if (schema && schema.getType(typeName)) {
context.reportError(
new GraphQLError(existedTypeNameMessage(typeName), node.name),
);
return;
}

if (knownTypeNames[typeName]) {
context.reportError(
new GraphQLError(duplicateTypeNameMessage(typeName), [
knownTypeNames[typeName],
node.name,
]),
);
} else {
knownTypeNames[typeName] = node.name;
}

return false;
}
}
2 changes: 2 additions & 0 deletions src/validation/specifiedRules.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,14 @@ export const specifiedRules: $ReadOnlyArray<ValidationRule> = [
];

import { LoneSchemaDefinition } from './rules/LoneSchemaDefinition';
import { UniqueTypeNames } from './rules/UniqueTypeNames';
import { KnownArgumentNamesOnDirectives } from './rules/KnownArgumentNames';
import { ProvidedRequiredArgumentsOnDirectives } from './rules/ProvidedRequiredArguments';

// @internal
export const specifiedSDLRules: $ReadOnlyArray<SDLValidationRule> = [
LoneSchemaDefinition,
UniqueTypeNames,
KnownDirectives,
UniqueDirectivesPerLocation,
KnownArgumentNamesOnDirectives,
Expand Down

0 comments on commit 257797a

Please sign in to comment.