diff --git a/src/utilities/__tests__/buildASTSchema-test.js b/src/utilities/__tests__/buildASTSchema-test.js index 74d06af609..13c4138c82 100644 --- a/src/utilities/__tests__/buildASTSchema-test.js +++ b/src/utilities/__tests__/buildASTSchema-test.js @@ -798,132 +798,3 @@ describe('Schema Builder', () => { buildSchema(sdl, { assumeValidSDL: true }); }); }); - -describe('Failures', () => { - it('Unknown type referenced', () => { - const sdl = ` - schema { - query: Hello - } - - type Hello { - bar: Bar - } - `; - expect(() => buildSchema(sdl)).to.throw( - 'Type "Bar" not found in document.', - ); - }); - - it('Unknown type in interface list', () => { - const sdl = ` - type Query implements Bar { - field: String - } - `; - expect(() => buildSchema(sdl)).to.throw( - 'Type "Bar" not found in document.', - ); - }); - - it('Unknown type in union list', () => { - const sdl = ` - union TestUnion = Bar - type Query { testUnion: TestUnion } - `; - expect(() => buildSchema(sdl)).to.throw( - 'Type "Bar" not found in document.', - ); - }); - - it('Unknown query type', () => { - const sdl = ` - schema { - query: Wat - } - - type Hello { - str: String - } - `; - expect(() => buildSchema(sdl)).to.throw( - 'Specified query type "Wat" not found in document.', - ); - }); - - it('Unknown mutation type', () => { - const sdl = ` - schema { - query: Hello - mutation: Wat - } - - type Hello { - str: String - } - `; - expect(() => buildSchema(sdl)).to.throw( - 'Specified mutation type "Wat" not found in document.', - ); - }); - - it('Unknown subscription type', () => { - const sdl = ` - schema { - query: Hello - mutation: Wat - subscription: Awesome - } - - type Hello { - str: String - } - - type Wat { - str: String - } - `; - expect(() => buildSchema(sdl)).to.throw( - 'Specified subscription type "Awesome" not found in document.', - ); - }); - - it('Does not consider directive names', () => { - const sdl = ` - schema { - query: Foo - } - - directive @Foo on QUERY - `; - expect(() => buildSchema(sdl)).to.throw( - 'Specified query type "Foo" not found in document.', - ); - }); - - it('Does not consider operation names', () => { - const sdl = ` - schema { - query: Foo - } - - query Foo { field } - `; - expect(() => buildSchema(sdl)).to.throw( - 'Specified query type "Foo" not found in document.', - ); - }); - - it('Does not consider fragment names', () => { - const sdl = ` - schema { - query: Foo - } - - fragment Foo on Type { field } - `; - expect(() => buildSchema(sdl)).to.throw( - 'Specified query type "Foo" not found in document.', - ); - }); -}); diff --git a/src/utilities/__tests__/extendSchema-test.js b/src/utilities/__tests__/extendSchema-test.js index 9ca9e9de0b..b3e4f19fa6 100644 --- a/src/utilities/__tests__/extendSchema-test.js +++ b/src/utilities/__tests__/extendSchema-test.js @@ -1107,38 +1107,6 @@ describe('extendSchema', () => { ); }); - it('does not allow referencing an unknown type', () => { - const unknownTypeError = - 'Unknown type: "Quix". Ensure that this type exists either in the ' + - 'original schema, or is added in a type definition.'; - - const typeSDL = ` - extend type Bar { - quix: Quix - } - `; - expect(() => extendTestSchema(typeSDL)).to.throw(unknownTypeError); - - const interfaceSDL = ` - extend interface SomeInterface { - quix: Quix - } - `; - expect(() => extendTestSchema(interfaceSDL)).to.throw(unknownTypeError); - - const unionSDL = ` - extend union SomeUnion = Quix - `; - expect(() => extendTestSchema(unionSDL)).to.throw(unknownTypeError); - - const inputSDL = ` - extend input SomeInput { - quix: Quix - } - `; - expect(() => extendTestSchema(inputSDL)).to.throw(unknownTypeError); - }); - it('does not allow extending an unknown type', () => { [ 'extend scalar UnknownType @foo', diff --git a/src/utilities/buildASTSchema.js b/src/utilities/buildASTSchema.js index f0d66bf304..ad60de576e 100644 --- a/src/utilities/buildASTSchema.js +++ b/src/utilities/buildASTSchema.js @@ -198,14 +198,7 @@ export function buildASTSchema( function getOperationTypes(schema: SchemaDefinitionNode) { const opTypes = {}; for (const operationType of schema.operationTypes) { - const typeName = operationType.type.name.value; - const operation = operationType.operation; - if (!nodeMap[typeName]) { - throw new Error( - `Specified ${operation} type "${typeName}" not found in document.`, - ); - } - opTypes[operation] = operationType.type; + opTypes[operationType.operation] = operationType.type; } return opTypes; } diff --git a/src/utilities/extendSchema.js b/src/utilities/extendSchema.js index f450647f48..f3f6cb3004 100644 --- a/src/utilities/extendSchema.js +++ b/src/utilities/extendSchema.js @@ -177,15 +177,9 @@ export function extendSchema( typeRef => { const typeName = typeRef.name.value; const existingType = schema.getType(typeName); - if (existingType) { - return extendNamedType(existingType); - } - throw new GraphQLError( - `Unknown type: "${typeName}". Ensure that this type exists ` + - 'either in the original schema, or is added in a type definition.', - [typeRef], - ); + invariant(existingType, `Unknown type: "${typeName}".`); + return extendNamedType(existingType); }, ); diff --git a/src/validation/__tests__/KnownTypeNames-test.js b/src/validation/__tests__/KnownTypeNames-test.js index 477b729b80..407ef265b7 100644 --- a/src/validation/__tests__/KnownTypeNames-test.js +++ b/src/validation/__tests__/KnownTypeNames-test.js @@ -8,17 +8,34 @@ */ import { describe, it } from 'mocha'; -import { expectValidationErrors } from './harness'; +import { buildSchema } from '../../utilities'; +import { + expectValidationErrors, + expectValidationErrorsWithSchema, + expectSDLValidationErrors, +} from './harness'; import { KnownTypeNames, unknownTypeMessage } from '../rules/KnownTypeNames'; function expectErrors(queryStr) { return expectValidationErrors(KnownTypeNames, queryStr); } +function expectErrorsWithSchema(schema, queryStr) { + return expectValidationErrorsWithSchema(schema, KnownTypeNames, queryStr); +} + function expectValid(queryStr) { expectErrors(queryStr).to.deep.equal([]); } +function expectSDLErrors(sdlStr, schema) { + return expectSDLValidationErrors(schema, KnownTypeNames, sdlStr); +} + +function expectValidSDL(sdlStr, schema) { + expectSDLErrors(sdlStr, schema).to.deep.equal([]); +} + function unknownType(typeName, suggestedTypes, line, column) { return { message: unknownTypeMessage(typeName, suggestedTypes), @@ -58,23 +75,197 @@ describe('Validate: Known type names', () => { ]); }); - it('ignores type definitions', () => { - expectErrors(` - type NotInTheSchema { - field: FooBar - } - interface FooBar { - field: NotInTheSchema + it('references to standard scalars that are missing in schema', () => { + const schema = buildSchema('type Query { foo: String }'); + const query = ` + query ($id: ID, $float: Float, $int: Int) { + __typename } - union U = A | B - input Blob { - field: UnknownType - } - query Foo($var: NotInTheSchema) { - user(id: $var) { - id + `; + expectErrorsWithSchema(schema, query).to.deep.equal([ + unknownType('ID', [], 2, 19), + unknownType('Float', [], 2, 31), + unknownType('Int', [], 2, 44), + ]); + }); + + describe('within SDL', () => { + it('use standard scalars', () => { + expectValidSDL(` + type Query { + string: String + int: Int + float: Float + boolean: Boolean + id: ID } - } - `).to.deep.equal([unknownType('NotInTheSchema', [], 12, 23)]); + `); + }); + + it('reference types defined inside the same document', () => { + expectValidSDL(` + union SomeUnion = SomeObject | AnotherObject + + type SomeObject implements SomeInterface { + someScalar(arg: SomeInputObject): SomeScalar + } + + type AnotherObject { + foo(arg: SomeInputObject): String + } + + type SomeInterface { + someScalar(arg: SomeInputObject): SomeScalar + } + + input SomeInputObject { + someScalar: SomeScalar + } + + scalar SomeScalar + + type RootQuery { + someInterface: SomeInterface + someUnion: SomeUnion + someScalar: SomeScalar + someObject: SomeObject + } + + schema { + query: RootQuery + } + `); + }); + + it('unknown type references', () => { + expectSDLErrors(` + type A + type B + + type SomeObject implements C { + e(d: D): E + } + + union SomeUnion = F | G + + interface SomeInterface { + i(h: H): I + } + + input SomeInput { + j: J + } + + directive @SomeDirective(k: K) on QUERY + + schema { + query: L + mutation: M + subscription: N + } + `).to.deep.equal([ + unknownType('C', ['A', 'B'], 5, 36), + unknownType('D', ['ID', 'A', 'B'], 6, 16), + unknownType('E', ['A', 'B'], 6, 20), + unknownType('F', ['A', 'B'], 9, 27), + unknownType('G', ['A', 'B'], 9, 31), + unknownType('H', ['A', 'B'], 12, 16), + unknownType('I', ['ID', 'A', 'B'], 12, 20), + unknownType('J', ['A', 'B'], 16, 14), + unknownType('K', ['A', 'B'], 19, 37), + unknownType('L', ['A', 'B'], 22, 18), + unknownType('M', ['A', 'B'], 23, 21), + unknownType('N', ['A', 'B'], 24, 25), + ]); + }); + + it('doesnot consider non-type definitions', () => { + expectSDLErrors(` + query Foo { __typename } + fragment Foo on Query { __typename } + directive @Foo on QUERY + + type Query { + foo: Foo + } + `).to.deep.equal([unknownType('Foo', [], 7, 16)]); + }); + + it('reference standard scalars inside extension document', () => { + const schema = buildSchema('type Foo'); + const sdl = ` + type SomeType { + string: String + int: Int + float: Float + boolean: Boolean + id: ID + } + `; + + expectValidSDL(sdl, schema); + }); + + it('reference types inside extension document', () => { + const schema = buildSchema('type Foo'); + const sdl = ` + type QueryRoot { + foo: Foo + bar: Bar + } + + scalar Bar + + schema { + query: QueryRoot + } + `; + + expectValidSDL(sdl, schema); + }); + + it('unknown type references inside extension document', () => { + const schema = buildSchema('type A'); + const sdl = ` + type B + + type SomeObject implements C { + e(d: D): E + } + + union SomeUnion = F | G + + interface SomeInterface { + i(h: H): I + } + + input SomeInput { + j: J + } + + directive @SomeDirective(k: K) on QUERY + + schema { + query: L + mutation: M + subscription: N + } + `; + + expectSDLErrors(sdl, schema).to.deep.equal([ + unknownType('C', ['A', 'B'], 4, 36), + unknownType('D', ['ID', 'A', 'B'], 5, 16), + unknownType('E', ['A', 'B'], 5, 20), + unknownType('F', ['A', 'B'], 8, 27), + unknownType('G', ['A', 'B'], 8, 31), + unknownType('H', ['A', 'B'], 11, 16), + unknownType('I', ['ID', 'A', 'B'], 11, 20), + unknownType('J', ['A', 'B'], 15, 14), + unknownType('K', ['A', 'B'], 18, 37), + unknownType('L', ['A', 'B'], 21, 18), + unknownType('M', ['A', 'B'], 22, 21), + unknownType('N', ['A', 'B'], 23, 25), + ]); + }); }); }); diff --git a/src/validation/rules/KnownTypeNames.js b/src/validation/rules/KnownTypeNames.js index c65c5bbdbb..7ceef3f559 100644 --- a/src/validation/rules/KnownTypeNames.js +++ b/src/validation/rules/KnownTypeNames.js @@ -7,11 +7,21 @@ * @flow strict */ -import type { ValidationContext } from '../ValidationContext'; +import type { + ValidationContext, + SDLValidationContext, +} from '../ValidationContext'; import { GraphQLError } from '../../error/GraphQLError'; import suggestionList from '../../jsutils/suggestionList'; import quotedOrList from '../../jsutils/quotedOrList'; +import type { ASTNode } from '../../language/ast'; import type { ASTVisitor } from '../../language/visitor'; +import { + isTypeDefinitionNode, + isTypeSystemDefinitionNode, + isTypeSystemExtensionNode, +} from '../../language/predicates'; +import { specifiedScalarTypes } from '../../type/scalars'; export function unknownTypeMessage( typeName: string, @@ -30,30 +40,54 @@ export function unknownTypeMessage( * A GraphQL document is only valid if referenced types (specifically * variable definitions and fragment conditions) are defined by the type schema. */ -export function KnownTypeNames(context: ValidationContext): ASTVisitor { +export function KnownTypeNames( + context: ValidationContext | SDLValidationContext, +): ASTVisitor { + const schema = context.getSchema(); + const existingTypesMap = schema ? schema.getTypeMap() : Object.create(null); + + const definedTypes = Object.create(null); + for (const def of context.getDocument().definitions) { + if (isTypeDefinitionNode(def)) { + definedTypes[def.name.value] = true; + } + } + + const typeNames = Object.keys(existingTypesMap).concat( + Object.keys(definedTypes), + ); + return { - // TODO: when validating IDL, re-enable these. Experimental version does not - // add unreferenced types, resulting in false-positive errors. Squelched - // errors for now. - ObjectTypeDefinition: () => false, - InterfaceTypeDefinition: () => false, - UnionTypeDefinition: () => false, - InputObjectTypeDefinition: () => false, - NamedType(node) { - const schema = context.getSchema(); + NamedType(node, _1, parent, _2, ancestors) { const typeName = node.name.value; - const type = schema.getType(typeName); - if (!type) { + if (!existingTypesMap[typeName] && !definedTypes[typeName]) { + const definitionNode = ancestors[2] || parent; + const isSDL = isSDLNode(definitionNode); + if (isSDL && isSpecifiedScalarName(typeName)) { + return; + } + + const suggestedTypes = suggestionList( + typeName, + isSDL ? specifiedScalarsNames.concat(typeNames) : typeNames, + ); context.reportError( - new GraphQLError( - unknownTypeMessage( - typeName, - suggestionList(typeName, Object.keys(schema.getTypeMap())), - ), - [node], - ), + new GraphQLError(unknownTypeMessage(typeName, suggestedTypes), node), ); } }, }; } + +const specifiedScalarsNames = specifiedScalarTypes.map(type => type.name); +function isSpecifiedScalarName(typeName) { + return specifiedScalarsNames.indexOf(typeName) !== -1; +} + +function isSDLNode(value: ASTNode | $ReadOnlyArray | void): boolean { + return Boolean( + value && + !Array.isArray(value) && + (isTypeSystemDefinitionNode(value) || isTypeSystemExtensionNode(value)), + ); +} diff --git a/src/validation/specifiedRules.js b/src/validation/specifiedRules.js index 63ea576696..2fff3be637 100644 --- a/src/validation/specifiedRules.js +++ b/src/validation/specifiedRules.js @@ -135,6 +135,7 @@ export const specifiedSDLRules: $ReadOnlyArray = [ UniqueOperationTypes, UniqueTypeNames, UniqueDirectiveNames, + KnownTypeNames, KnownDirectives, UniqueDirectivesPerLocation, KnownArgumentNamesOnDirectives,