diff --git a/src/type/__tests__/definition-test.js b/src/type/__tests__/definition-test.js index da8aec0841..a7c70cd6ac 100644 --- a/src/type/__tests__/definition-test.js +++ b/src/type/__tests__/definition-test.js @@ -102,6 +102,16 @@ const ScalarType = new GraphQLScalarType({ parseLiteral() {}, }); +function schemaWithFieldType(type) { + return new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { field: { type } }, + }), + types: [type], + }); +} + describe('Type System: Example', () => { it('defines a query only schema', () => { const BlogSchema = new GraphQLSchema({ @@ -270,7 +280,7 @@ describe('Type System: Example', () => { expect(schema.getTypeMap().NestedInputObject).to.equal(NestedInputObject); }); - it("includes interfaces' subtypes in the type map", () => { + it('includes interface possible types in the type map', () => { const SomeInterface = new GraphQLInterfaceType({ name: 'SomeInterface', fields: { @@ -374,26 +384,13 @@ describe('Type System: Example', () => { }); }); - it('prohibits putting non-Object types in unions', () => { - const badUnionTypes = [ - GraphQLInt, - GraphQLNonNull(GraphQLInt), - GraphQLList(GraphQLInt), - InterfaceType, - UnionType, - EnumType, - InputObjectType, - ]; - badUnionTypes.forEach(x => { - expect(() => - new GraphQLUnionType({ name: 'BadUnion', types: [x] }).getTypes(), - ).to.throw( - `BadUnion may only contain Object types, it cannot contain: ${x}.`, - ); - }); + it('prohibits nesting NonNull inside NonNull', () => { + expect(() => GraphQLNonNull(GraphQLNonNull(GraphQLInt))).to.throw( + 'Expected Int! to be a GraphQL nullable type.', + ); }); - it("allows a thunk for Union's types", () => { + it('allows a thunk for Union member types', () => { const union = new GraphQLUnionType({ name: 'ThunkUnion', types: () => [ObjectType], @@ -470,6 +467,638 @@ describe('Type System: Example', () => { }); }); +describe('Field config must be object', () => { + it('accepts an Object type with a field function', () => { + const objType = new GraphQLObjectType({ + name: 'SomeObject', + fields() { + return { + f: { type: GraphQLString }, + }; + }, + }); + expect(objType.getFields().f.type).to.equal(GraphQLString); + }); + + it('rejects an Object type field with undefined config', () => { + const objType = new GraphQLObjectType({ + name: 'SomeObject', + fields: { + f: undefined, + }, + }); + expect(() => objType.getFields()).to.throw( + 'SomeObject.f field config must be an object', + ); + }); + + it('rejects an Object type with incorrectly typed fields', () => { + const objType = new GraphQLObjectType({ + name: 'SomeObject', + fields: [{ field: GraphQLString }], + }); + expect(() => objType.getFields()).to.throw( + 'SomeObject fields must be an object with field names as keys or a ' + + 'function which returns such an object.', + ); + }); + + it('rejects an Object type with a field function that returns incorrect type', () => { + const objType = new GraphQLObjectType({ + name: 'SomeObject', + fields() { + return [{ field: GraphQLString }]; + }, + }); + expect(() => objType.getFields()).to.throw( + 'SomeObject fields must be an object with field names as keys or a ' + + 'function which returns such an object.', + ); + }); +}); + +describe('Field arg config must be object', () => { + it('accepts an Object type with field args', () => { + const objType = new GraphQLObjectType({ + name: 'SomeObject', + fields: { + goodField: { + type: GraphQLString, + args: { + goodArg: { type: GraphQLString }, + }, + }, + }, + }); + expect(() => objType.getFields()).not.to.throw(); + }); + + it('rejects an Object type with incorrectly typed field args', () => { + const objType = new GraphQLObjectType({ + name: 'SomeObject', + fields: { + badField: { + type: GraphQLString, + args: [{ badArg: GraphQLString }], + }, + }, + }); + expect(() => objType.getFields()).to.throw( + 'SomeObject.badField args must be an object with argument names as keys.', + ); + }); + + it('does not allow isDeprecated without deprecationReason on field', () => { + expect(() => { + const OldObject = new GraphQLObjectType({ + name: 'OldObject', + fields: { + field: { + type: GraphQLString, + isDeprecated: true, + }, + }, + }); + + return schemaWithFieldType(OldObject); + }).to.throw( + 'OldObject.field should provide "deprecationReason" instead ' + + 'of "isDeprecated".', + ); + }); +}); + +describe('Object interfaces must be array', () => { + it('accepts an Object type with array interfaces', () => { + const objType = new GraphQLObjectType({ + name: 'SomeObject', + interfaces: [InterfaceType], + fields: { f: { type: GraphQLString } }, + }); + expect(objType.getInterfaces()[0]).to.equal(InterfaceType); + }); + + it('accepts an Object type with interfaces as a function returning an array', () => { + const objType = new GraphQLObjectType({ + name: 'SomeObject', + interfaces: () => [InterfaceType], + fields: { f: { type: GraphQLString } }, + }); + expect(objType.getInterfaces()[0]).to.equal(InterfaceType); + }); + + it('rejects an Object type with incorrectly typed interfaces', () => { + const objType = new GraphQLObjectType({ + name: 'SomeObject', + interfaces: {}, + fields: { f: { type: GraphQLString } }, + }); + expect(() => objType.getInterfaces()).to.throw( + 'SomeObject interfaces must be an Array or a function which returns an Array.', + ); + }); + + it('rejects an Object type with interfaces as a function returning an incorrect type', () => { + const objType = new GraphQLObjectType({ + name: 'SomeObject', + interfaces() { + return {}; + }, + fields: { f: { type: GraphQLString } }, + }); + expect(() => objType.getInterfaces()).to.throw( + 'SomeObject interfaces must be an Array or a function which returns an Array.', + ); + }); +}); + +describe('Type System: Object fields must have valid resolve values', () => { + function schemaWithObjectWithFieldResolver(resolveValue) { + const BadResolverType = new GraphQLObjectType({ + name: 'BadResolver', + fields: { + badField: { + type: GraphQLString, + resolve: resolveValue, + }, + }, + }); + + return new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + f: { type: BadResolverType }, + }, + }), + }); + } + + it('accepts a lambda as an Object field resolver', () => { + expect(() => schemaWithObjectWithFieldResolver(() => ({}))).not.to.throw(); + }); + + it('rejects an empty Object field resolver', () => { + expect(() => schemaWithObjectWithFieldResolver({})).to.throw( + 'BadResolver.badField field resolver must be a function if provided, ' + + 'but got: [object Object].', + ); + }); + + it('rejects a constant scalar value resolver', () => { + expect(() => schemaWithObjectWithFieldResolver(0)).to.throw( + 'BadResolver.badField field resolver must be a function if provided, ' + + 'but got: 0.', + ); + }); +}); + +describe('Type System: Interface types must be resolvable', () => { + it('accepts an Interface type defining resolveType', () => { + expect(() => { + const AnotherInterfaceType = new GraphQLInterfaceType({ + name: 'AnotherInterface', + fields: { f: { type: GraphQLString } }, + }); + + schemaWithFieldType( + new GraphQLObjectType({ + name: 'SomeObject', + interfaces: [AnotherInterfaceType], + fields: { f: { type: GraphQLString } }, + }), + ); + }).not.to.throw(); + }); + + it('accepts an Interface with implementing type defining isTypeOf', () => { + expect(() => { + const InterfaceTypeWithoutResolveType = new GraphQLInterfaceType({ + name: 'InterfaceTypeWithoutResolveType', + fields: { f: { type: GraphQLString } }, + }); + + schemaWithFieldType( + new GraphQLObjectType({ + name: 'SomeObject', + interfaces: [InterfaceTypeWithoutResolveType], + fields: { f: { type: GraphQLString } }, + }), + ); + }).not.to.throw(); + }); + + it('accepts an Interface type defining resolveType with implementing type defining isTypeOf', () => { + expect(() => { + const AnotherInterfaceType = new GraphQLInterfaceType({ + name: 'AnotherInterface', + fields: { f: { type: GraphQLString } }, + }); + + schemaWithFieldType( + new GraphQLObjectType({ + name: 'SomeObject', + interfaces: [AnotherInterfaceType], + fields: { f: { type: GraphQLString } }, + }), + ); + }).not.to.throw(); + }); + + it('rejects an Interface type with an incorrect type for resolveType', () => { + expect( + () => + new GraphQLInterfaceType({ + name: 'AnotherInterface', + resolveType: {}, + fields: { f: { type: GraphQLString } }, + }), + ).to.throw('AnotherInterface must provide "resolveType" as a function.'); + }); +}); + +describe('Type System: Union types must be resolvable', () => { + const ObjectWithIsTypeOf = new GraphQLObjectType({ + name: 'ObjectWithIsTypeOf', + fields: { f: { type: GraphQLString } }, + }); + + it('accepts a Union type defining resolveType', () => { + expect(() => + schemaWithFieldType( + new GraphQLUnionType({ + name: 'SomeUnion', + types: [ObjectType], + }), + ), + ).not.to.throw(); + }); + + it('accepts a Union of Object types defining isTypeOf', () => { + expect(() => + schemaWithFieldType( + new GraphQLUnionType({ + name: 'SomeUnion', + types: [ObjectWithIsTypeOf], + }), + ), + ).not.to.throw(); + }); + + it('accepts a Union type defining resolveType of Object types defining isTypeOf', () => { + expect(() => + schemaWithFieldType( + new GraphQLUnionType({ + name: 'SomeUnion', + types: [ObjectWithIsTypeOf], + }), + ), + ).not.to.throw(); + }); + + it('rejects an Interface type with an incorrect type for resolveType', () => { + expect(() => + schemaWithFieldType( + new GraphQLUnionType({ + name: 'SomeUnion', + resolveType: {}, + types: [ObjectWithIsTypeOf], + }), + ), + ).to.throw('SomeUnion must provide "resolveType" as a function.'); + }); +}); + +describe('Type System: Scalar types must be serializable', () => { + it('accepts a Scalar type defining serialize', () => { + expect(() => + schemaWithFieldType( + new GraphQLScalarType({ + name: 'SomeScalar', + serialize: () => null, + }), + ), + ).not.to.throw(); + }); + + it('rejects a Scalar type not defining serialize', () => { + expect(() => + schemaWithFieldType( + new GraphQLScalarType({ + name: 'SomeScalar', + }), + ), + ).to.throw( + 'SomeScalar must provide "serialize" function. If this custom Scalar ' + + 'is also used as an input type, ensure "parseValue" and "parseLiteral" ' + + 'functions are also provided.', + ); + }); + + it('rejects a Scalar type defining serialize with an incorrect type', () => { + expect(() => + schemaWithFieldType( + new GraphQLScalarType({ + name: 'SomeScalar', + serialize: {}, + }), + ), + ).to.throw( + 'SomeScalar must provide "serialize" function. If this custom Scalar ' + + 'is also used as an input type, ensure "parseValue" and "parseLiteral" ' + + 'functions are also provided.', + ); + }); + + it('accepts a Scalar type defining parseValue and parseLiteral', () => { + expect(() => + schemaWithFieldType( + new GraphQLScalarType({ + name: 'SomeScalar', + serialize: () => null, + parseValue: () => null, + parseLiteral: () => null, + }), + ), + ).not.to.throw(); + }); + + it('rejects a Scalar type defining parseValue but not parseLiteral', () => { + expect(() => + schemaWithFieldType( + new GraphQLScalarType({ + name: 'SomeScalar', + serialize: () => null, + parseValue: () => null, + }), + ), + ).to.throw( + 'SomeScalar must provide both "parseValue" and "parseLiteral" functions.', + ); + }); + + it('rejects a Scalar type defining parseLiteral but not parseValue', () => { + expect(() => + schemaWithFieldType( + new GraphQLScalarType({ + name: 'SomeScalar', + serialize: () => null, + parseLiteral: () => null, + }), + ), + ).to.throw( + 'SomeScalar must provide both "parseValue" and "parseLiteral" functions.', + ); + }); + + it('rejects a Scalar type defining parseValue and parseLiteral with an incorrect type', () => { + expect(() => + schemaWithFieldType( + new GraphQLScalarType({ + name: 'SomeScalar', + serialize: () => null, + parseValue: {}, + parseLiteral: {}, + }), + ), + ).to.throw( + 'SomeScalar must provide both "parseValue" and "parseLiteral" functions.', + ); + }); +}); + +describe('Type System: Object types must be assertable', () => { + it('accepts an Object type with an isTypeOf function', () => { + expect(() => { + schemaWithFieldType( + new GraphQLObjectType({ + name: 'AnotherObject', + fields: { f: { type: GraphQLString } }, + }), + ); + }).not.to.throw(); + }); + + it('rejects an Object type with an incorrect type for isTypeOf', () => { + expect(() => { + schemaWithFieldType( + new GraphQLObjectType({ + name: 'AnotherObject', + isTypeOf: {}, + fields: { f: { type: GraphQLString } }, + }), + ); + }).to.throw('AnotherObject must provide "isTypeOf" as a function.'); + }); +}); + +describe('Type System: Union types must be array', () => { + it('accepts a Union type with array types', () => { + expect(() => + schemaWithFieldType( + new GraphQLUnionType({ + name: 'SomeUnion', + types: [ObjectType], + }), + ), + ).not.to.throw(); + }); + + it('accepts a Union type with function returning an array of types', () => { + expect(() => + schemaWithFieldType( + new GraphQLUnionType({ + name: 'SomeUnion', + types: () => [ObjectType], + }), + ), + ).not.to.throw(); + }); + + it('rejects a Union type without types', () => { + expect(() => + schemaWithFieldType( + new GraphQLUnionType({ + name: 'SomeUnion', + }), + ), + ).not.to.throw(); + }); + + it('rejects a Union type with incorrectly typed types', () => { + expect(() => + schemaWithFieldType( + new GraphQLUnionType({ + name: 'SomeUnion', + types: { + ObjectType, + }, + }), + ), + ).to.throw( + 'Must provide Array of types or a function which returns such an array ' + + 'for Union SomeUnion.', + ); + }); +}); + +describe('Type System: Input Objects must have fields', () => { + it('accepts an Input Object type with fields', () => { + const inputObjType = new GraphQLInputObjectType({ + name: 'SomeInputObject', + fields: { + f: { type: GraphQLString }, + }, + }); + expect(inputObjType.getFields().f.type).to.equal(GraphQLString); + }); + + it('accepts an Input Object type with a field function', () => { + const inputObjType = new GraphQLInputObjectType({ + name: 'SomeInputObject', + fields() { + return { + f: { type: GraphQLString }, + }; + }, + }); + expect(inputObjType.getFields().f.type).to.equal(GraphQLString); + }); + + it('rejects an Input Object type with incorrect fields', () => { + const inputObjType = new GraphQLInputObjectType({ + name: 'SomeInputObject', + fields: [], + }); + expect(() => inputObjType.getFields()).to.throw( + 'SomeInputObject fields must be an object with field names as keys or a ' + + 'function which returns such an object.', + ); + }); + + it('rejects an Input Object type with fields function that returns incorrect type', () => { + const inputObjType = new GraphQLInputObjectType({ + name: 'SomeInputObject', + fields() { + return []; + }, + }); + expect(() => inputObjType.getFields()).to.throw( + 'SomeInputObject fields must be an object with field names as keys or a ' + + 'function which returns such an object.', + ); + }); +}); + +describe('Type System: Input Object fields must not have resolvers', () => { + it('rejects an Input Object type with resolvers', () => { + const inputObjType = new GraphQLInputObjectType({ + name: 'SomeInputObject', + fields: { + f: { + type: GraphQLString, + resolve: () => { + return 0; + }, + }, + }, + }); + expect(() => inputObjType.getFields()).to.throw( + 'SomeInputObject.f field type has a resolve property, ' + + 'but Input Types cannot define resolvers.', + ); + }); + + it('rejects an Input Object type with resolver constant', () => { + const inputObjType = new GraphQLInputObjectType({ + name: 'SomeInputObject', + fields: { + f: { + type: GraphQLString, + resolve: {}, + }, + }, + }); + expect(() => inputObjType.getFields()).to.throw( + 'SomeInputObject.f field type has a resolve property, ' + + 'but Input Types cannot define resolvers.', + ); + }); +}); + +describe('Type System: Enum types must be well defined', () => { + it('accepts a well defined Enum type with empty value definition', () => { + const enumType = new GraphQLEnumType({ + name: 'SomeEnum', + values: { + FOO: {}, + BAR: {}, + }, + }); + expect(enumType.getValue('FOO').value).to.equal('FOO'); + expect(enumType.getValue('BAR').value).to.equal('BAR'); + }); + + it('accepts a well defined Enum type with internal value definition', () => { + const enumType = new GraphQLEnumType({ + name: 'SomeEnum', + values: { + FOO: { value: 10 }, + BAR: { value: 20 }, + }, + }); + expect(enumType.getValue('FOO').value).to.equal(10); + expect(enumType.getValue('BAR').value).to.equal(20); + }); + + it('rejects an Enum type with incorrectly typed values', () => { + const enumType = new GraphQLEnumType({ + name: 'SomeEnum', + values: [{ FOO: 10 }], + }); + expect(() => enumType.getValue()).to.throw( + 'SomeEnum values must be an object with value names as keys.', + ); + }); + + it('rejects an Enum type with missing value definition', () => { + const enumType = new GraphQLEnumType({ + name: 'SomeEnum', + values: { FOO: null }, + }); + expect(() => enumType.getValues()).to.throw( + 'SomeEnum.FOO must refer to an object with a "value" key representing ' + + 'an internal value but got: null.', + ); + }); + + it('rejects an Enum type with incorrectly typed value definition', () => { + const enumType = new GraphQLEnumType({ + name: 'SomeEnum', + values: { FOO: 10 }, + }); + expect(() => enumType.getValues()).to.throw( + 'SomeEnum.FOO must refer to an object with a "value" key representing ' + + 'an internal value but got: 10.', + ); + }); + + it('does not allow isDeprecated without deprecationReason on enum', () => { + const enumType = new GraphQLEnumType({ + name: 'SomeEnum', + values: { + FOO: { + isDeprecated: true, + }, + }, + }); + expect(() => enumType.getValues()).to.throw( + 'SomeEnum.FOO should provide "deprecationReason" instead ' + + 'of "isDeprecated".', + ); + }); +}); + describe('Type System: List must accept only types', () => { const types = [ GraphQLString, @@ -535,3 +1164,90 @@ describe('Type System: NonNull must only accept non-nullable types', () => { }); }); }); + +describe('Type System: A Schema must contain uniquely named types', () => { + it('rejects a Schema which redefines a built-in type', () => { + expect(() => { + const FakeString = new GraphQLScalarType({ + name: 'String', + serialize: () => null, + }); + + const QueryType = new GraphQLObjectType({ + name: 'Query', + fields: { + normal: { type: GraphQLString }, + fake: { type: FakeString }, + }, + }); + + return new GraphQLSchema({ query: QueryType }); + }).to.throw( + 'Schema must contain unique named types but contains multiple types ' + + 'named "String".', + ); + }); + + it('rejects a Schema which defines an object type twice', () => { + expect(() => { + const A = new GraphQLObjectType({ + name: 'SameName', + fields: { f: { type: GraphQLString } }, + }); + + const B = new GraphQLObjectType({ + name: 'SameName', + fields: { f: { type: GraphQLString } }, + }); + + const QueryType = new GraphQLObjectType({ + name: 'Query', + fields: { + a: { type: A }, + b: { type: B }, + }, + }); + + return new GraphQLSchema({ query: QueryType }); + }).to.throw( + 'Schema must contain unique named types but contains multiple types ' + + 'named "SameName".', + ); + }); + + it('rejects a Schema which have same named objects implementing an interface', () => { + expect(() => { + const AnotherInterface = new GraphQLInterfaceType({ + name: 'AnotherInterface', + fields: { f: { type: GraphQLString } }, + }); + + const FirstBadObject = new GraphQLObjectType({ + name: 'BadObject', + interfaces: [AnotherInterface], + fields: { f: { type: GraphQLString } }, + }); + + const SecondBadObject = new GraphQLObjectType({ + name: 'BadObject', + interfaces: [AnotherInterface], + fields: { f: { type: GraphQLString } }, + }); + + const QueryType = new GraphQLObjectType({ + name: 'Query', + fields: { + iface: { type: AnotherInterface }, + }, + }); + + return new GraphQLSchema({ + query: QueryType, + types: [FirstBadObject, SecondBadObject], + }); + }).to.throw( + 'Schema must contain unique named types but contains multiple types ' + + 'named "BadObject".', + ); + }); +}); diff --git a/src/type/__tests__/validation-test.js b/src/type/__tests__/validation-test.js index b6b0a3449b..45f46ce467 100644 --- a/src/type/__tests__/validation-test.js +++ b/src/type/__tests__/validation-test.js @@ -22,8 +22,10 @@ import { GraphQLNonNull, GraphQLString, } from '../../'; +import { parse } from '../../language/parser'; import { validateSchema } from '../validate'; import { buildSchema } from '../../utilities/buildASTSchema'; +import { extendSchema } from '../../utilities/extendSchema'; const SomeScalarType = new GraphQLScalarType({ name: 'SomeScalar', @@ -37,11 +39,6 @@ const SomeObjectType = new GraphQLObjectType({ fields: { f: { type: GraphQLString } }, }); -const ObjectWithIsTypeOf = new GraphQLObjectType({ - name: 'ObjectWithIsTypeOf', - fields: { f: { type: GraphQLString } }, -}); - const SomeUnionType = new GraphQLUnionType({ name: 'SomeUnion', types: [SomeObjectType], @@ -82,7 +79,7 @@ const outputTypes = withModifiers([ SomeInterfaceType, ]); -const notOutputTypes = withModifiers([SomeInputObjectType]).concat(String); +const notOutputTypes = withModifiers([SomeInputObjectType]).concat(Number); const inputTypes = withModifiers([ GraphQLString, @@ -95,7 +92,7 @@ const notInputTypes = withModifiers([ SomeObjectType, SomeUnionType, SomeInterfaceType, -]).concat(String); +]).concat(Number); function schemaWithFieldType(type) { return new GraphQLSchema({ @@ -224,7 +221,7 @@ describe('Type System: A Schema must have Object root types', () => { `); expect(validateSchema(schema)).to.containSubset([ { - message: 'Query root type must be Object type but got: Query.', + message: 'Query root type must be Object type, it cannot be Query.', locations: [{ line: 2, column: 7 }], }, ]); @@ -241,7 +238,7 @@ describe('Type System: A Schema must have Object root types', () => { expect(validateSchema(schemaWithDef)).to.containSubset([ { message: - 'Query root type must be Object type but got: SomeInputObject.', + 'Query root type must be Object type, it cannot be SomeInputObject.', locations: [{ line: 3, column: 16 }], }, ]); @@ -260,7 +257,7 @@ describe('Type System: A Schema must have Object root types', () => { expect(validateSchema(schema)).to.containSubset([ { message: - 'Mutation root type must be Object type if provided but got: Mutation.', + 'Mutation root type must be Object type if provided, it cannot be Mutation.', locations: [{ line: 6, column: 7 }], }, ]); @@ -282,7 +279,7 @@ describe('Type System: A Schema must have Object root types', () => { expect(validateSchema(schemaWithDef)).to.containSubset([ { message: - 'Mutation root type must be Object type if provided but got: SomeInputObject.', + 'Mutation root type must be Object type if provided, it cannot be SomeInputObject.', locations: [{ line: 4, column: 19 }], }, ]); @@ -301,7 +298,7 @@ describe('Type System: A Schema must have Object root types', () => { expect(validateSchema(schema)).to.containSubset([ { message: - 'Subscription root type must be Object type if provided but got: Subscription.', + 'Subscription root type must be Object type if provided, it cannot be Subscription.', locations: [{ line: 6, column: 7 }], }, ]); @@ -323,7 +320,7 @@ describe('Type System: A Schema must have Object root types', () => { expect(validateSchema(schemaWithDef)).to.containSubset([ { message: - 'Subscription root type must be Object type if provided but got: SomeInputObject.', + 'Subscription root type must be Object type if provided, it cannot be SomeInputObject.', locations: [{ line: 4, column: 23 }], }, ]); @@ -342,1047 +339,386 @@ describe('Type System: A Schema must have Object root types', () => { }); }); -describe('Type System: A Schema must contain uniquely named types', () => { - it('rejects a Schema which redefines a built-in type', () => { - expect(() => { - const FakeString = new GraphQLScalarType({ - name: 'String', - serialize: () => null, - }); - - const QueryType = new GraphQLObjectType({ - name: 'Query', - fields: { - normal: { type: GraphQLString }, - fake: { type: FakeString }, - }, - }); - - return new GraphQLSchema({ query: QueryType }); - }).to.throw( - 'Schema must contain unique named types but contains multiple types ' + - 'named "String".', - ); - }); - - it('rejects a Schema which defines an object type twice', () => { - expect(() => { - const A = new GraphQLObjectType({ - name: 'SameName', - fields: { f: { type: GraphQLString } }, - }); - - const B = new GraphQLObjectType({ - name: 'SameName', - fields: { f: { type: GraphQLString } }, - }); - - const QueryType = new GraphQLObjectType({ - name: 'Query', - fields: { - a: { type: A }, - b: { type: B }, - }, - }); - - return new GraphQLSchema({ query: QueryType }); - }).to.throw( - 'Schema must contain unique named types but contains multiple types ' + - 'named "SameName".', - ); - }); - - it('rejects a Schema which have same named objects implementing an interface', () => { - expect(() => { - const AnotherInterface = new GraphQLInterfaceType({ - name: 'AnotherInterface', - fields: { f: { type: GraphQLString } }, - }); - - const FirstBadObject = new GraphQLObjectType({ - name: 'BadObject', - interfaces: [AnotherInterface], - fields: { f: { type: GraphQLString } }, - }); - - const SecondBadObject = new GraphQLObjectType({ - name: 'BadObject', - interfaces: [AnotherInterface], - fields: { f: { type: GraphQLString } }, - }); - - const QueryType = new GraphQLObjectType({ - name: 'Query', - fields: { - iface: { type: AnotherInterface }, - }, - }); - - return new GraphQLSchema({ - query: QueryType, - types: [FirstBadObject, SecondBadObject], - }); - }).to.throw( - 'Schema must contain unique named types but contains multiple types ' + - 'named "BadObject".', - ); - }); -}); - describe('Type System: Objects must have fields', () => { it('accepts an Object type with fields object', () => { - expect(() => - schemaWithFieldType( - new GraphQLObjectType({ - name: 'SomeObject', - fields: { - f: { type: GraphQLString }, - }, - }), - ), - ).not.to.throw(); - }); + const schema = buildSchema(` + type Query { + field: SomeObject + } - it('accepts an Object type with a field function', () => { - expect(() => - schemaWithFieldType( - new GraphQLObjectType({ - name: 'SomeObject', - fields() { - return { - f: { type: GraphQLString }, - }; - }, - }), - ), - ).not.to.throw(); + type SomeObject { + field: String + } + `); + expect(validateSchema(schema)).to.deep.equal([]); }); it('rejects an Object type with missing fields', () => { - expect(() => - schemaWithFieldType( - new GraphQLObjectType({ - name: 'SomeObject', - }), - ), - ).to.throw( - 'SomeObject fields must be an object with field names as keys or a ' + - 'function which returns such an object.', - ); - }); - - it('rejects an Object type field with undefined config', () => { - expect(() => - schemaWithFieldType( - new GraphQLObjectType({ - name: 'SomeObject', - fields: { - f: undefined, - }, - }), - ), - ).to.throw('SomeObject.f field config must be an object'); - }); - - it('rejects an Object type with incorrectly named fields', () => { - expect(() => - schemaWithFieldType( - new GraphQLObjectType({ - name: 'SomeObject', - fields: { 'bad-name-with-dashes': { type: GraphQLString } }, - }), - ), - ).to.throw( - 'Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but "bad-name-with-dashes" does not.', - ); - }); - - it('warns about an Object type with reserved named fields', () => { - /* eslint-disable no-console */ - const realConsoleWarn = console.warn; - const calls = []; - console.warn = function() { - calls.push(Array.prototype.slice.call(arguments)); - }; - try { - schemaWithFieldType( - new GraphQLObjectType({ - name: 'SomeObject', - fields: { __notPartOfIntrospection: { type: GraphQLString } }, - }), - ); - - expect(calls[0][0]).contains( - 'Name "__notPartOfIntrospection" must not begin with "__", which is reserved by GraphQL introspection.', - ); - } finally { - console.warn = realConsoleWarn; - } - /* eslint-enable no-console */ - }); + const schema = buildSchema(` + type Query { + test: IncompleteObject + } - it('rejects an Object type with incorrectly typed fields', () => { - expect(() => - schemaWithFieldType( - new GraphQLObjectType({ - name: 'SomeObject', - fields: [{ field: GraphQLString }], - }), - ), - ).to.throw( - 'SomeObject fields must be an object with field names as keys or a ' + - 'function which returns such an object.', - ); - }); + type IncompleteObject + `); + expect(validateSchema(schema)).to.containSubset([ + { + message: 'Type IncompleteObject must define one or more fields.', + locations: [{ line: 6, column: 7 }], + }, + ]); - it('rejects an Object type with empty fields', () => { - expect(() => - schemaWithFieldType( - new GraphQLObjectType({ - name: 'SomeObject', - fields: {}, - }), - ), - ).to.throw( - 'SomeObject fields must be an object with field names as keys or a ' + - 'function which returns such an object.', + const manualSchema = schemaWithFieldType( + new GraphQLObjectType({ + name: 'IncompleteObject', + fields: {}, + }), ); - }); + expect(validateSchema(manualSchema)).to.containSubset([ + { + message: 'Type IncompleteObject must define one or more fields.', + }, + ]); - it('rejects an Object type with a field function that returns nothing', () => { - expect(() => - schemaWithFieldType( - new GraphQLObjectType({ - name: 'SomeObject', - fields() {}, - }), - ), - ).to.throw( - 'SomeObject fields must be an object with field names as keys or a ' + - 'function which returns such an object.', + const manualSchema2 = schemaWithFieldType( + new GraphQLObjectType({ + name: 'IncompleteObject', + fields() { + return {}; + }, + }), ); + expect(validateSchema(manualSchema2)).to.containSubset([ + { + message: 'Type IncompleteObject must define one or more fields.', + }, + ]); }); - it('rejects an Object type with a field function that returns empty', () => { - expect(() => - schemaWithFieldType( - new GraphQLObjectType({ - name: 'SomeObject', - fields() { - return {}; - }, - }), - ), - ).to.throw( - 'SomeObject fields must be an object with field names as keys or a ' + - 'function which returns such an object.', + it('rejects an Object type with incorrectly named fields', () => { + const schema = schemaWithFieldType( + new GraphQLObjectType({ + name: 'SomeObject', + fields: { 'bad-name-with-dashes': { type: GraphQLString } }, + }), ); + expect(validateSchema(schema)).to.containSubset([ + { + message: + 'Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but ' + + '"bad-name-with-dashes" does not.', + }, + ]); }); }); describe('Type System: Fields args must be properly named', () => { it('accepts field args with valid names', () => { - expect(() => - schemaWithFieldType( - new GraphQLObjectType({ - name: 'SomeObject', - fields: { - goodField: { - type: GraphQLString, - args: { - goodArg: { type: GraphQLString }, - }, - }, - }, - }), - ), - ).not.to.throw(); - }); - - it('rejects field arg with invalid names', () => { - expect(() => { - const QueryType = new GraphQLObjectType({ + const schema = schemaWithFieldType( + new GraphQLObjectType({ name: 'SomeObject', fields: { - badField: { + goodField: { type: GraphQLString, args: { - 'bad-name-with-dashes': { type: GraphQLString }, + goodArg: { type: GraphQLString }, }, }, }, - }); - return new GraphQLSchema({ query: QueryType }); - }).to.throw( - 'Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but "bad-name-with-dashes" does not.', + }), ); - }); -}); - -describe('Type System: Fields args must be objects', () => { - it('accepts an Object type with field args', () => { - expect(() => - schemaWithFieldType( - new GraphQLObjectType({ - name: 'SomeObject', - fields: { - goodField: { - type: GraphQLString, - args: { - goodArg: { type: GraphQLString }, - }, - }, - }, - }), - ), - ).not.to.throw(); + expect(validateSchema(schema)).to.deep.equal([]); }); - it('rejects an Object type with incorrectly typed field args', () => { - expect(() => - schemaWithFieldType( - new GraphQLObjectType({ - name: 'SomeObject', - fields: { - badField: { - type: GraphQLString, - args: [{ badArg: GraphQLString }], - }, + it('rejects field arg with invalid names', () => { + const QueryType = new GraphQLObjectType({ + name: 'SomeObject', + fields: { + badField: { + type: GraphQLString, + args: { + 'bad-name-with-dashes': { type: GraphQLString }, }, - }), - ), - ).to.throw( - 'SomeObject.badField args must be an object with argument names as keys.', - ); + }, + }, + }); + const schema = new GraphQLSchema({ query: QueryType }); + expect(validateSchema(schema)).to.containSubset([ + { + message: + 'Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but "bad-name-with-dashes" does not.', + }, + ]); }); }); -describe('Type System: Object interfaces must be array', () => { - it('accepts an Object type with array interfaces', () => { - expect(() => { - const AnotherInterfaceType = new GraphQLInterfaceType({ - name: 'AnotherInterface', - fields: { f: { type: GraphQLString } }, - }); - - schemaWithFieldType( - new GraphQLObjectType({ - name: 'SomeObject', - interfaces: [AnotherInterfaceType], - fields: { f: { type: GraphQLString } }, - }), - ); - }).not.to.throw(); - }); - - it('accepts an Object type with interfaces as a function returning an array', () => { - expect(() => { - const AnotherInterfaceType = new GraphQLInterfaceType({ - name: 'AnotherInterface', - fields: { f: { type: GraphQLString } }, - }); - - schemaWithFieldType( - new GraphQLObjectType({ - name: 'SomeObject', - interfaces: () => [AnotherInterfaceType], - fields: { f: { type: GraphQLString } }, - }), - ); - }).not.to.throw(); - }); - - it('rejects an Object type with incorrectly typed interfaces', () => { - expect(() => - schemaWithFieldType( - new GraphQLObjectType({ - name: 'SomeObject', - interfaces: {}, - fields: { f: { type: GraphQLString } }, - }), - ), - ).to.throw( - 'SomeObject interfaces must be an Array or a function which returns an Array.', - ); - }); - - it('rejects an Object type with interfaces as a function returning an incorrect type', () => { - expect(() => - schemaWithFieldType( - new GraphQLObjectType({ - name: 'SomeObject', - interfaces() { - return {}; - }, - fields: { f: { type: GraphQLString } }, - }), - ), - ).to.throw( - 'SomeObject interfaces must be an Array or a function which returns an Array.', - ); - }); -}); +describe('Type System: Union types must be valid', () => { + it('accepts a Union type with member types', () => { + const schema = buildSchema(` + type Query { + test: GoodUnion + } -describe('Type System: Union types must be array', () => { - it('accepts a Union type with array types', () => { - expect(() => - schemaWithFieldType( - new GraphQLUnionType({ - name: 'SomeUnion', - types: [SomeObjectType], - }), - ), - ).not.to.throw(); - }); + type TypeA { + field: String + } - it('accepts a Union type with function returning an array of types', () => { - expect(() => - schemaWithFieldType( - new GraphQLUnionType({ - name: 'SomeUnion', - types: () => [SomeObjectType], - }), - ), - ).not.to.throw(); - }); + type TypeB { + field: String + } - it('rejects a Union type without types', () => { - expect(() => - schemaWithFieldType( - new GraphQLUnionType({ - name: 'SomeUnion', - }), - ), - ).to.throw( - 'Must provide Array of types or a function which returns such an array ' + - 'for Union SomeUnion.', - ); + union GoodUnion = + | TypeA + | TypeB + `); + expect(validateSchema(schema)).to.deep.equal([]); }); it('rejects a Union type with empty types', () => { - expect(() => - schemaWithFieldType( - new GraphQLUnionType({ - name: 'SomeUnion', - types: [], - }), - ), - ).to.throw( - 'Must provide Array of types or a function which returns such an array ' + - 'for Union SomeUnion.', - ); - }); + const schema = buildSchema(` + type Query { + test: BadUnion + } - it('rejects a Union type with incorrectly typed types', () => { - expect(() => - schemaWithFieldType( - new GraphQLUnionType({ - name: 'SomeUnion', - types: { - SomeObjectType, - }, - }), - ), - ).to.throw( - 'Must provide Array of types or a function which returns such an array ' + - 'for Union SomeUnion.', - ); + union BadUnion + `); + expect(validateSchema(schema)).to.containSubset([ + { + message: 'Union type BadUnion must define one or more member types.', + locations: [{ line: 6, column: 7 }], + }, + ]); }); it('rejects a Union type with duplicated member type', () => { - expect(() => - schemaWithFieldType( - new GraphQLUnionType({ - name: 'SomeUnion', - types: [SomeObjectType, SomeObjectType], - }), - ), - ).to.throw('SomeUnion can include SomeObject type only once.'); - }); -}); - -describe('Type System: Input Objects must have fields', () => { - function schemaWithInputObject(inputObjectType) { - return new GraphQLSchema({ - query: new GraphQLObjectType({ - name: 'Query', - fields: { - f: { - type: GraphQLString, - args: { - badArg: { type: inputObjectType }, - }, - }, - }, - }), - }); - } + const schema = buildSchema(` + type Query { + test: BadUnion + } - it('accepts an Input Object type with fields', () => { - expect(() => - schemaWithInputObject( - new GraphQLInputObjectType({ - name: 'SomeInputObject', - fields: { - f: { type: GraphQLString }, - }, - }), - ), - ).not.to.throw(); - }); + type TypeA { + field: String + } - it('accepts an Input Object type with a field function', () => { - expect(() => - schemaWithInputObject( - new GraphQLInputObjectType({ - name: 'SomeInputObject', - fields() { - return { - f: { type: GraphQLString }, - }; - }, - }), - ), - ).not.to.throw(); - }); + type TypeB { + field: String + } - it('rejects an Input Object type with missing fields', () => { - expect(() => - schemaWithInputObject( - new GraphQLInputObjectType({ - name: 'SomeInputObject', - }), - ), - ).to.throw( - 'SomeInputObject fields must be an object with field names as keys or a ' + - 'function which returns such an object.', - ); + union BadUnion = + | TypeA + | TypeB + | TypeA + `); + expect(validateSchema(schema)).to.containSubset([ + { + message: 'Union type BadUnion can only include type TypeA once.', + locations: [{ line: 15, column: 11 }, { line: 17, column: 11 }], + }, + ]); }); - it('rejects an Input Object type with incorrectly typed fields', () => { - expect(() => - schemaWithInputObject( - new GraphQLInputObjectType({ - name: 'SomeInputObject', - fields: [{ field: GraphQLString }], - }), - ), - ).to.throw( - 'SomeInputObject fields must be an object with field names as keys or a ' + - 'function which returns such an object.', - ); - }); + it('rejects a Union type with non-Object members types', () => { + const schema = buildSchema(` + type Query { + test: BadUnion + } - it('rejects an Input Object type with empty fields', () => { - expect(() => - schemaWithInputObject( - new GraphQLInputObjectType({ - name: 'SomeInputObject', - fields: {}, - }), - ), - ).to.throw( - 'SomeInputObject fields must be an object with field names as keys or a ' + - 'function which returns such an object.', - ); - }); + type TypeA { + field: String + } - it('rejects an Input Object type with a field function that returns nothing', () => { - expect(() => - schemaWithInputObject( - new GraphQLInputObjectType({ - name: 'SomeInputObject', - fields() {}, - }), - ), - ).to.throw( - 'SomeInputObject fields must be an object with field names as keys or a ' + - 'function which returns such an object.', - ); - }); + type TypeB { + field: String + } - it('rejects an Input Object type with a field function that returns empty', () => { - expect(() => - schemaWithInputObject( - new GraphQLInputObjectType({ - name: 'SomeInputObject', - fields() { - return {}; - }, - }), - ), - ).to.throw( - 'SomeInputObject fields must be an object with field names as keys or a ' + - 'function which returns such an object.', - ); - }); -}); + union BadUnion = + | TypeA + | String + | TypeB + `); + expect(validateSchema(schema)).to.containSubset([ + { + message: + 'Union type BadUnion can only include Object types, ' + + 'it cannot include String.', + locations: [{ line: 16, column: 11 }], + }, + ]); -describe('Type System: Input Object fields must not have resolvers', () => { - function schemaWithInputObject(inputObjectType) { - return new GraphQLSchema({ - query: new GraphQLObjectType({ - name: 'Query', - fields: { - f: { - type: GraphQLString, - args: { - input: { type: inputObjectType }, - }, - }, + const badUnionMemberTypes = [ + GraphQLString, + new GraphQLNonNull(SomeObjectType), + new GraphQLList(SomeObjectType), + SomeInterfaceType, + SomeUnionType, + SomeEnumType, + SomeInputObjectType, + ]; + badUnionMemberTypes.forEach(memberType => { + const badSchema = schemaWithFieldType( + new GraphQLUnionType({ name: 'BadUnion', types: [memberType] }), + ); + expect(validateSchema(badSchema)).to.containSubset([ + { + message: + 'Union type BadUnion can only include Object types, ' + + `it cannot include ${memberType}.`, }, - }), + ]); }); - } - - it('accepts an Input Object type with no resolver', () => { - expect(() => - schemaWithInputObject( - new GraphQLInputObjectType({ - name: 'SomeInputObject', - fields: { - f: { - type: GraphQLString, - }, - }, - }), - ), - ).not.to.throw(); - }); - - it('accepts an Input Object type with null resolver', () => { - expect(() => - schemaWithInputObject( - new GraphQLInputObjectType({ - name: 'SomeInputObject', - fields: { - f: { - type: GraphQLString, - resolve: null, - }, - }, - }), - ), - ).not.to.throw(); - }); - - it('accepts an Input Object type with undefined resolver', () => { - expect(() => - schemaWithInputObject( - new GraphQLInputObjectType({ - name: 'SomeInputObject', - fields: { - f: { - type: GraphQLString, - resolve: undefined, - }, - }, - }), - ), - ).not.to.throw(); - }); - - it('rejects an Input Object type with resolver function', () => { - expect(() => - schemaWithInputObject( - new GraphQLInputObjectType({ - name: 'SomeInputObject', - fields: { - f: { - type: GraphQLString, - resolve: () => { - return 0; - }, - }, - }, - }), - ), - ).to.throw( - 'SomeInputObject.f field type has a resolve property,' + - ' but Input Types cannot define resolvers.', - ); - }); - - it('rejects an Input Object type with resolver constant', () => { - expect(() => - schemaWithInputObject( - new GraphQLInputObjectType({ - name: 'SomeInputObject', - fields: { - f: { - type: GraphQLString, - resolve: {}, - }, - }, - }), - ), - ).to.throw( - 'SomeInputObject.f field type has a resolve property,' + - ' but Input Types cannot define resolvers.', - ); }); }); -describe('Type System: Object types must be assertable', () => { - it('accepts an Object type with an isTypeOf function', () => { - expect(() => { - schemaWithFieldType( - new GraphQLObjectType({ - name: 'AnotherObject', - fields: { f: { type: GraphQLString } }, - }), - ); - }).not.to.throw(); - }); - - it('rejects an Object type with an incorrect type for isTypeOf', () => { - expect(() => { - schemaWithFieldType( - new GraphQLObjectType({ - name: 'AnotherObject', - isTypeOf: {}, - fields: { f: { type: GraphQLString } }, - }), - ); - }).to.throw('AnotherObject must provide "isTypeOf" as a function.'); - }); -}); - -describe('Type System: Interface types must be resolvable', () => { - it('accepts an Interface type defining resolveType', () => { - expect(() => { - const AnotherInterfaceType = new GraphQLInterfaceType({ - name: 'AnotherInterface', - fields: { f: { type: GraphQLString } }, - }); - - schemaWithFieldType( - new GraphQLObjectType({ - name: 'SomeObject', - interfaces: [AnotherInterfaceType], - fields: { f: { type: GraphQLString } }, - }), - ); - }).not.to.throw(); - }); - - it('accepts an Interface with implementing type defining isTypeOf', () => { - expect(() => { - const InterfaceTypeWithoutResolveType = new GraphQLInterfaceType({ - name: 'InterfaceTypeWithoutResolveType', - fields: { f: { type: GraphQLString } }, - }); - - schemaWithFieldType( - new GraphQLObjectType({ - name: 'SomeObject', - interfaces: [InterfaceTypeWithoutResolveType], - fields: { f: { type: GraphQLString } }, - }), - ); - }).not.to.throw(); - }); - - it('accepts an Interface type defining resolveType with implementing type defining isTypeOf', () => { - expect(() => { - const AnotherInterfaceType = new GraphQLInterfaceType({ - name: 'AnotherInterface', - fields: { f: { type: GraphQLString } }, - }); - - schemaWithFieldType( - new GraphQLObjectType({ - name: 'SomeObject', - interfaces: [AnotherInterfaceType], - fields: { f: { type: GraphQLString } }, - }), - ); - }).not.to.throw(); - }); - - it('rejects an Interface type with an incorrect type for resolveType', () => { - expect( - () => - new GraphQLInterfaceType({ - name: 'AnotherInterface', - resolveType: {}, - fields: { f: { type: GraphQLString } }, - }), - ).to.throw('AnotherInterface must provide "resolveType" as a function.'); - }); -}); - -describe('Type System: Union types must be resolvable', () => { - it('accepts a Union type defining resolveType', () => { - expect(() => - schemaWithFieldType( - new GraphQLUnionType({ - name: 'SomeUnion', - types: [SomeObjectType], - }), - ), - ).not.to.throw(); - }); - - it('accepts a Union of Object types defining isTypeOf', () => { - expect(() => - schemaWithFieldType( - new GraphQLUnionType({ - name: 'SomeUnion', - types: [ObjectWithIsTypeOf], - }), - ), - ).not.to.throw(); - }); - - it('accepts a Union type defining resolveType of Object types defining isTypeOf', () => { - expect(() => - schemaWithFieldType( - new GraphQLUnionType({ - name: 'SomeUnion', - types: [ObjectWithIsTypeOf], - }), - ), - ).not.to.throw(); - }); - - it('rejects an Interface type with an incorrect type for resolveType', () => { - expect(() => - schemaWithFieldType( - new GraphQLUnionType({ - name: 'SomeUnion', - resolveType: {}, - types: [ObjectWithIsTypeOf], - }), - ), - ).to.throw('SomeUnion must provide "resolveType" as a function.'); - }); -}); +describe('Type System: Input Objects must have fields', () => { + it('accepts an Input Object type with fields', () => { + const schema = buildSchema(` + type Query { + field(arg: SomeInputObject): String + } -describe('Type System: Scalar types must be serializable', () => { - it('accepts a Scalar type defining serialize', () => { - expect(() => - schemaWithFieldType( - new GraphQLScalarType({ - name: 'SomeScalar', - serialize: () => null, - }), - ), - ).not.to.throw(); + input SomeInputObject { + field: String + } + `); + expect(validateSchema(schema)).to.deep.equal([]); }); - it('rejects a Scalar type not defining serialize', () => { - expect(() => - schemaWithFieldType( - new GraphQLScalarType({ - name: 'SomeScalar', - }), - ), - ).to.throw( - 'SomeScalar must provide "serialize" function. If this custom Scalar ' + - 'is also used as an input type, ensure "parseValue" and "parseLiteral" ' + - 'functions are also provided.', - ); - }); + it('rejects an Input Object type with missing fields', () => { + const schema = buildSchema(` + type Query { + field(arg: SomeInputObject): String + } - it('rejects a Scalar type defining serialize with an incorrect type', () => { - expect(() => - schemaWithFieldType( - new GraphQLScalarType({ - name: 'SomeScalar', - serialize: {}, - }), - ), - ).to.throw( - 'SomeScalar must provide "serialize" function. If this custom Scalar ' + - 'is also used as an input type, ensure "parseValue" and "parseLiteral" ' + - 'functions are also provided.', - ); + input SomeInputObject + `); + expect(validateSchema(schema)).to.containSubset([ + { + message: + 'Input Object type SomeInputObject must define one or more fields.', + locations: [{ line: 6, column: 7 }], + }, + ]); }); - it('accepts a Scalar type defining parseValue and parseLiteral', () => { - expect(() => - schemaWithFieldType( - new GraphQLScalarType({ - name: 'SomeScalar', - serialize: () => null, - parseValue: () => null, - parseLiteral: () => null, - }), - ), - ).not.to.throw(); - }); + it('rejects an Input Object type with incorrectly typed fields', () => { + const schema = buildSchema(` + type Query { + field(arg: SomeInputObject): String + } - it('rejects a Scalar type defining parseValue but not parseLiteral', () => { - expect(() => - schemaWithFieldType( - new GraphQLScalarType({ - name: 'SomeScalar', - serialize: () => null, - parseValue: () => null, - }), - ), - ).to.throw( - 'SomeScalar must provide both "parseValue" and "parseLiteral" functions.', - ); - }); + type SomeObject { + field: String + } - it('rejects a Scalar type defining parseLiteral but not parseValue', () => { - expect(() => - schemaWithFieldType( - new GraphQLScalarType({ - name: 'SomeScalar', - serialize: () => null, - parseLiteral: () => null, - }), - ), - ).to.throw( - 'SomeScalar must provide both "parseValue" and "parseLiteral" functions.', - ); - }); + union SomeUnion = SomeObject - it('rejects a Scalar type defining parseValue and parseLiteral with an incorrect type', () => { - expect(() => - schemaWithFieldType( - new GraphQLScalarType({ - name: 'SomeScalar', - serialize: () => null, - parseValue: {}, - parseLiteral: {}, - }), - ), - ).to.throw( - 'SomeScalar must provide both "parseValue" and "parseLiteral" functions.', - ); + input SomeInputObject { + badObject: SomeObject + badUnion: SomeUnion + goodInputObject: SomeInputObject + } + `); + expect(validateSchema(schema)).to.containSubset([ + { + message: + 'The type of SomeInputObject.badObject must be Input Type but got: SomeObject.', + locations: [{ line: 13, column: 20 }], + }, + { + message: + 'The type of SomeInputObject.badUnion must be Input Type but got: SomeUnion.', + locations: [{ line: 14, column: 19 }], + }, + ]); }); }); describe('Type System: Enum types must be well defined', () => { - it('accepts a well defined Enum type with empty value definition', () => { - expect( - () => - new GraphQLEnumType({ - name: 'SomeEnum', - values: { - FOO: {}, - BAR: {}, - }, - }), - ).not.to.throw(); - }); - - it('accepts a well defined Enum type with internal value definition', () => { - expect( - () => - new GraphQLEnumType({ - name: 'SomeEnum', - values: { - FOO: { value: 10 }, - BAR: { value: 20 }, - }, - }), - ).not.to.throw(); - }); - it('rejects an Enum type without values', () => { - expect( - () => - new GraphQLEnumType({ - name: 'SomeEnum', - }), - ).to.throw('SomeEnum values must be an object with value names as keys.'); - }); + const schema = buildSchema(` + type Query { + field: SomeEnum + } - it('rejects an Enum type with empty values', () => { - expect( - () => - new GraphQLEnumType({ - name: 'SomeEnum', - values: {}, - }), - ).to.throw('SomeEnum values must be an object with value names as keys.'); + enum SomeEnum + `); + expect(validateSchema(schema)).to.containSubset([ + { + message: 'Enum type SomeEnum must define one or more values.', + locations: [{ line: 6, column: 7 }], + }, + ]); }); - it('rejects an Enum type with incorrectly typed values', () => { - expect( - () => - new GraphQLEnumType({ - name: 'SomeEnum', - values: [{ FOO: 10 }], - }), - ).to.throw('SomeEnum values must be an object with value names as keys.'); - }); + it('rejects an Enum type with duplicate values', () => { + const schema = buildSchema(` + type Query { + field: SomeEnum + } - it('rejects an Enum type with missing value definition', () => { - expect( - () => - new GraphQLEnumType({ - name: 'SomeEnum', - values: { - FOO: null, - }, - }), - ).to.throw( - 'SomeEnum.FOO must refer to an object with a "value" key representing ' + - 'an internal value but got: null.', - ); + enum SomeEnum { + SOME_VALUE + SOME_VALUE + } + `); + expect(validateSchema(schema)).to.containSubset([ + { + message: 'Enum type SomeEnum can include value SOME_VALUE only once.', + locations: [{ line: 7, column: 9 }, { line: 8, column: 9 }], + }, + ]); }); - it('rejects an Enum type with incorrectly typed value definition', () => { - expect( - () => + it('rejects an Enum type with incorrectly named values', () => { + function schemaWithEnum(name) { + return schemaWithFieldType( new GraphQLEnumType({ name: 'SomeEnum', values: { - FOO: 10, + [name]: {}, }, }), - ).to.throw( - 'SomeEnum.FOO must refer to an object with a "value" key representing ' + - 'an internal value but got: 10.', - ); - }); - - it('rejects an Enum type with incorrectly named values', () => { - function enumValue(name) { - return new GraphQLEnumType({ - name: 'SomeEnum', - values: { - [name]: {}, - }, - }); + ); } - expect(() => enumValue('#value')).to.throw( - 'Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but "#value" does not.', - ); + const schema1 = schemaWithEnum('#value'); + expect(validateSchema(schema1)).to.containSubset([ + { + message: + 'Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but "#value" does not.', + }, + ]); - expect(() => enumValue('true')).to.throw( - 'Name "true" can not be used as an Enum value.', - ); + const schema2 = schemaWithEnum('1value'); + expect(validateSchema(schema2)).to.containSubset([ + { + message: + 'Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but "1value" does not.', + }, + ]); - expect(() => enumValue('false')).to.throw( - 'Name "false" can not be used as an Enum value.', - ); + const schema3 = schemaWithEnum('KEBAB-CASE'); + expect(validateSchema(schema3)).to.containSubset([ + { + message: + 'Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but "KEBAB-CASE" does not.', + }, + ]); - expect(() => enumValue('null')).to.throw( - 'Name "null" can not be used as an Enum value.', - ); - }); + const schema4 = schemaWithEnum('true'); + expect(validateSchema(schema4)).to.containSubset([ + { message: 'Enum type SomeEnum cannot include value: true.' }, + ]); - it('does not allow isDeprecated without deprecationReason on enum', () => { - expect( - () => - new GraphQLEnumType({ - name: 'SomeEnum', - values: { - value: { isDeprecated: true }, - }, - }), - ).to.throw( - 'SomeEnum.value should provide "deprecationReason" instead ' + - 'of "isDeprecated".', - ); + const schema5 = schemaWithEnum('false'); + expect(validateSchema(schema5)).to.containSubset([ + { message: 'Enum type SomeEnum cannot include value: false.' }, + ]); + + const schema6 = schemaWithEnum('null'); + expect(validateSchema(schema6)).to.containSubset([ + { message: 'Enum type SomeEnum cannot include value: null.' }, + ]); }); }); @@ -1407,63 +743,51 @@ describe('Type System: Object fields must have output types', () => { outputTypes.forEach(type => { it(`accepts an output type as an Object field type: ${type}`, () => { - expect(() => schemaWithObjectFieldOfType(type)).not.to.throw(); + const schema = schemaWithObjectFieldOfType(type); + expect(validateSchema(schema)).to.deep.equal([]); }); }); it('rejects an empty Object field type', () => { - expect(() => schemaWithObjectFieldOfType(undefined)).to.throw( - 'BadObject.badField field type must be Output Type but got: undefined.', - ); + const schema = schemaWithObjectFieldOfType(undefined); + expect(validateSchema(schema)).to.containSubset([ + { + message: + 'The type of BadObject.badField must be Output Type but got: undefined.', + }, + ]); }); notOutputTypes.forEach(type => { it(`rejects a non-output type as an Object field type: ${type}`, () => { - expect(() => schemaWithObjectFieldOfType(type)).to.throw( - `BadObject.badField field type must be Output Type but got: ${type}.`, - ); - }); - }); -}); - -describe('Type System: Object fields must have valid resolve values', () => { - function schemaWithObjectWithFieldResolver(resolveValue) { - const BadResolverType = new GraphQLObjectType({ - name: 'BadResolver', - fields: { - badField: { - type: GraphQLString, - resolve: resolveValue, - }, - }, - }); - - return new GraphQLSchema({ - query: new GraphQLObjectType({ - name: 'Query', - fields: { - f: { type: BadResolverType }, + const schema = schemaWithObjectFieldOfType(type); + expect(validateSchema(schema)).to.containSubset([ + { + message: `The type of BadObject.badField must be Output Type but got: ${ + type + }.`, }, - }), + ]); }); - } - - it('accepts a lambda as an Object field resolver', () => { - expect(() => schemaWithObjectWithFieldResolver(() => ({}))).not.to.throw(); }); - it('rejects an empty Object field resolver', () => { - expect(() => schemaWithObjectWithFieldResolver({})).to.throw( - 'BadResolver.badField field resolver must be a function if provided, ' + - 'but got: [object Object].', - ); - }); + it('rejects with relevant locations for a non-output type as an Object field type', () => { + const schema = buildSchema(` + type Query { + field: [SomeInputObject] + } - it('rejects a constant scalar value resolver', () => { - expect(() => schemaWithObjectWithFieldResolver(0)).to.throw( - 'BadResolver.badField field resolver must be a function if provided, ' + - 'but got: 0.', - ); + input SomeInputObject { + field: String + } + `); + expect(validateSchema(schema)).to.containSubset([ + { + message: + 'The type of Query.field must be Output Type but got: [SomeInputObject].', + locations: [{ line: 3, column: 16 }], + }, + ]); }); }); @@ -1485,7 +809,7 @@ describe('Type System: Objects can only implement unique interfaces', () => { expect(validateSchema(schema)).to.containSubset([ { message: - 'BadObject must only implement Interface types, it cannot implement SomeInputObject.', + 'Type BadObject must only implement Interface types, it cannot implement SomeInputObject.', locations: [{ line: 10, column: 33 }], }, ]); @@ -1507,49 +831,36 @@ describe('Type System: Objects can only implement unique interfaces', () => { `); expect(validateSchema(schema)).to.containSubset([ { - message: - 'AnotherObject must declare it implements AnotherInterface only once.', + message: 'Type AnotherObject can only implement AnotherInterface once.', locations: [{ line: 10, column: 37 }, { line: 10, column: 55 }], }, ]); }); -}); - -describe('Type System: Unions must represent Object types', () => { - function schemaWithUnionOfType(type) { - const BadUnionType = new GraphQLUnionType({ - name: 'BadUnion', - types: [type], - }); - return new GraphQLSchema({ - query: new GraphQLObjectType({ - name: 'Query', - fields: { - f: { type: BadUnionType }, - }, - }), - }); - } + it('rejects an Object implementing the same interface twice due to extension', () => { + const schema = buildSchema(` + type Query { + test: AnotherObject + } - it('accepts a Union of an Object Type', () => { - expect(() => schemaWithUnionOfType(SomeObjectType)).not.to.throw(); - }); + interface AnotherInterface { + field: String + } - const notObjectTypes = withModifiers([ - SomeScalarType, - SomeEnumType, - SomeInterfaceType, - SomeUnionType, - SomeInputObjectType, - ]); - - notObjectTypes.forEach(type => { - it(`rejects a Union of a non-Object type: ${type}`, () => { - expect(() => schemaWithUnionOfType(type)).to.throw( - `BadUnion may only contain Object types, it cannot contain: ${type}.`, - ); - }); + type AnotherObject implements AnotherInterface { + field: String + } + `); + const extendedSchema = extendSchema( + schema, + parse('extend type AnotherObject implements AnotherInterface'), + ); + expect(validateSchema(extendedSchema)).to.containSubset([ + { + message: 'Type AnotherObject can only implement AnotherInterface once.', + locations: [{ line: 10, column: 37 }, { line: 1, column: 38 }], + }, + ]); }); }); @@ -1574,25 +885,56 @@ describe('Type System: Interface fields must have output types', () => { outputTypes.forEach(type => { it(`accepts an output type as an Interface field type: ${type}`, () => { - expect(() => schemaWithInterfaceFieldOfType(type)).not.to.throw(); + const schema = schemaWithInterfaceFieldOfType(type); + expect(validateSchema(schema)).to.deep.equal([]); }); }); it('rejects an empty Interface field type', () => { - expect(() => schemaWithInterfaceFieldOfType(undefined)).to.throw( - 'BadInterface.badField field type must be Output Type but got: undefined.', - ); + const schema = schemaWithInterfaceFieldOfType(undefined); + expect(validateSchema(schema)).to.containSubset([ + { + message: + 'The type of BadInterface.badField must be Output Type but got: undefined.', + }, + ]); }); notOutputTypes.forEach(type => { it(`rejects a non-output type as an Interface field type: ${type}`, () => { - expect(() => schemaWithInterfaceFieldOfType(type)).to.throw( - `BadInterface.badField field type must be Output Type but got: ${ - type - }.`, - ); + const schema = schemaWithInterfaceFieldOfType(type); + expect(validateSchema(schema)).to.containSubset([ + { + message: `The type of BadInterface.badField must be Output Type but got: ${ + type + }.`, + }, + ]); }); }); + + it('rejects a non-output type as an Interface field type with locations', () => { + const schema = buildSchema(` + type Query { + test: SomeInterface + } + + interface SomeInterface { + field: SomeInputObject + } + + input SomeInputObject { + foo: String + } + `); + expect(validateSchema(schema)).to.containSubset([ + { + message: + 'The type of SomeInterface.field must be Output Type but got: SomeInputObject.', + locations: [{ line: 7, column: 16 }], + }, + ]); + }); }); describe('Type System: Field arguments must have input types', () => { @@ -1621,25 +963,52 @@ describe('Type System: Field arguments must have input types', () => { inputTypes.forEach(type => { it(`accepts an input type as a field arg type: ${type}`, () => { - expect(() => schemaWithArgOfType(type)).not.to.throw(); + const schema = schemaWithArgOfType(type); + expect(validateSchema(schema)).to.deep.equal([]); }); }); it('rejects an empty field arg type', () => { - expect(() => schemaWithArgOfType(undefined)).to.throw( - 'BadObject.badField(badArg:) argument type must be Input Type but got: undefined.', - ); + const schema = schemaWithArgOfType(undefined); + expect(validateSchema(schema)).to.containSubset([ + { + message: + 'The type of BadObject.badField(badArg:) must be Input Type but got: undefined.', + }, + ]); }); notInputTypes.forEach(type => { it(`rejects a non-input type as a field arg type: ${type}`, () => { - expect(() => schemaWithArgOfType(type)).to.throw( - `BadObject.badField(badArg:) argument type must be Input Type but got: ${ - type - }.`, - ); + const schema = schemaWithArgOfType(type); + expect(validateSchema(schema)).to.containSubset([ + { + message: `The type of BadObject.badField(badArg:) must be Input Type but got: ${ + type + }.`, + }, + ]); }); }); + + it('rejects a non-input type as a field arg with locations', () => { + const schema = buildSchema(` + type Query { + test(arg: SomeObject): String + } + + type SomeObject { + foo: String + } + `); + expect(validateSchema(schema)).to.containSubset([ + { + message: + 'The type of Query.test(arg:) must be Input Type but got: SomeObject.', + locations: [{ line: 3, column: 19 }], + }, + ]); + }); }); describe('Type System: Input Object fields must have input types', () => { @@ -1668,25 +1037,56 @@ describe('Type System: Input Object fields must have input types', () => { inputTypes.forEach(type => { it(`accepts an input type as an input field type: ${type}`, () => { - expect(() => schemaWithInputFieldOfType(type)).not.to.throw(); + const schema = schemaWithInputFieldOfType(type); + expect(validateSchema(schema)).to.deep.equal([]); }); }); it('rejects an empty input field type', () => { - expect(() => schemaWithInputFieldOfType(undefined)).to.throw( - 'BadInputObject.badField field type must be Input Type but got: undefined.', - ); + const schema = schemaWithInputFieldOfType(undefined); + expect(validateSchema(schema)).to.containSubset([ + { + message: + 'The type of BadInputObject.badField must be Input Type but got: undefined.', + }, + ]); }); notInputTypes.forEach(type => { it(`rejects a non-input type as an input field type: ${type}`, () => { - expect(() => schemaWithInputFieldOfType(type)).to.throw( - `BadInputObject.badField field type must be Input Type but got: ${ - type - }.`, - ); + const schema = schemaWithInputFieldOfType(type); + expect(validateSchema(schema)).to.containSubset([ + { + message: `The type of BadInputObject.badField must be Input Type but got: ${ + type + }.`, + }, + ]); }); }); + + it('rejects a non-input type as an input object field with locations', () => { + const schema = buildSchema(` + type Query { + test(arg: SomeInputObject): String + } + + input SomeInputObject { + foo: SomeObject + } + + type SomeObject { + bar: String + } + `); + expect(validateSchema(schema)).to.containSubset([ + { + message: + 'The type of SomeInputObject.foo must be Input Type but got: SomeObject.', + locations: [{ line: 7, column: 14 }], + }, + ]); + }); }); describe('Objects must adhere to Interface they implement', () => { @@ -1759,8 +1159,8 @@ describe('Objects must adhere to Interface they implement', () => { expect(validateSchema(schema)).to.containSubset([ { message: - '"AnotherInterface" expects field "field" but ' + - '"AnotherObject" does not provide it.', + 'Interface field AnotherInterface.field expected but ' + + 'AnotherObject does not provide it.', locations: [{ line: 7, column: 9 }, { line: 10, column: 7 }], }, ]); @@ -1783,8 +1183,8 @@ describe('Objects must adhere to Interface they implement', () => { expect(validateSchema(schema)).to.containSubset([ { message: - 'AnotherInterface.field expects type "String" but ' + - 'AnotherObject.field is type "Int".', + 'Interface field AnotherInterface.field expects type String but ' + + 'AnotherObject.field is type Int.', locations: [{ line: 7, column: 31 }, { line: 11, column: 31 }], }, ]); @@ -1810,8 +1210,8 @@ describe('Objects must adhere to Interface they implement', () => { expect(validateSchema(schema)).to.containSubset([ { message: - 'AnotherInterface.field expects type "A" but ' + - 'AnotherObject.field is type "B".', + 'Interface field AnotherInterface.field expects type A but ' + + 'AnotherObject.field is type B.', locations: [{ line: 10, column: 16 }, { line: 14, column: 16 }], }, ]); @@ -1874,8 +1274,8 @@ describe('Objects must adhere to Interface they implement', () => { expect(validateSchema(schema)).to.containSubset([ { message: - 'AnotherInterface.field expects argument "input" but ' + - 'AnotherObject.field does not provide it.', + 'Interface field argument AnotherInterface.field(input:) expected ' + + 'but AnotherObject.field does not provide it.', locations: [{ line: 7, column: 15 }, { line: 11, column: 9 }], }, ]); @@ -1898,8 +1298,8 @@ describe('Objects must adhere to Interface they implement', () => { expect(validateSchema(schema)).to.containSubset([ { message: - 'AnotherInterface.field(input:) expects type "String" but ' + - 'AnotherObject.field(input:) is type "Int".', + 'Interface field argument AnotherInterface.field(input:) expects ' + + 'type String but AnotherObject.field(input:) is type Int.', locations: [{ line: 7, column: 22 }, { line: 11, column: 22 }], }, ]); @@ -1922,14 +1322,14 @@ describe('Objects must adhere to Interface they implement', () => { expect(validateSchema(schema)).to.containSubset([ { message: - 'AnotherInterface.field expects type "String" but ' + - 'AnotherObject.field is type "Int".', + 'Interface field AnotherInterface.field expects type String but ' + + 'AnotherObject.field is type Int.', locations: [{ line: 7, column: 31 }, { line: 11, column: 28 }], }, { message: - 'AnotherInterface.field(input:) expects type "String" but ' + - 'AnotherObject.field(input:) is type "Int".', + 'Interface field argument AnotherInterface.field(input:) expects ' + + 'type String but AnotherObject.field(input:) is type Int.', locations: [{ line: 7, column: 22 }, { line: 11, column: 22 }], }, ]); @@ -1952,9 +1352,9 @@ describe('Objects must adhere to Interface they implement', () => { expect(validateSchema(schema)).to.containSubset([ { message: - 'AnotherObject.field(anotherInput:) is of required type ' + - '"String!" but is not also provided by the interface ' + - 'AnotherInterface.field.', + 'Object field argument AnotherObject.field(anotherInput:) is of ' + + 'required type String! but is not also provided by the Interface ' + + 'field AnotherInterface.field.', locations: [{ line: 11, column: 44 }, { line: 7, column: 9 }], }, ]); @@ -1994,8 +1394,8 @@ describe('Objects must adhere to Interface they implement', () => { expect(validateSchema(schema)).to.containSubset([ { message: - 'AnotherInterface.field expects type "[String]" but ' + - 'AnotherObject.field is type "String".', + 'Interface field AnotherInterface.field expects type [String] ' + + 'but AnotherObject.field is type String.', locations: [{ line: 7, column: 16 }, { line: 11, column: 16 }], }, ]); @@ -2018,8 +1418,8 @@ describe('Objects must adhere to Interface they implement', () => { expect(validateSchema(schema)).to.containSubset([ { message: - 'AnotherInterface.field expects type "String" but ' + - 'AnotherObject.field is type "[String]".', + 'Interface field AnotherInterface.field expects type String but ' + + 'AnotherObject.field is type [String].', locations: [{ line: 7, column: 16 }, { line: 11, column: 16 }], }, ]); @@ -2059,29 +1459,10 @@ describe('Objects must adhere to Interface they implement', () => { expect(validateSchema(schema)).to.containSubset([ { message: - 'AnotherInterface.field expects type "String!" but ' + - 'AnotherObject.field is type "String".', + 'Interface field AnotherInterface.field expects type String! ' + + 'but AnotherObject.field is type String.', locations: [{ line: 7, column: 16 }, { line: 11, column: 16 }], }, ]); }); - - it('does not allow isDeprecated without deprecationReason on field', () => { - expect(() => { - const OldObject = new GraphQLObjectType({ - name: 'OldObject', - fields: { - field: { - type: GraphQLString, - isDeprecated: true, - }, - }, - }); - - return schemaWithFieldType(OldObject); - }).to.throw( - 'OldObject.field should provide "deprecationReason" instead ' + - 'of "isDeprecated".', - ); - }); }); diff --git a/src/type/definition.js b/src/type/definition.js index 3107904ba6..5fa22e6cd7 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -12,7 +12,6 @@ import invariant from '../jsutils/invariant'; import isInvalid from '../jsutils/isInvalid'; import type { ObjMap } from '../jsutils/ObjMap'; import * as Kind from '../language/kinds'; -import { assertValidName } from '../utilities/assertValidName'; import { valueFromASTUntyped } from '../utilities/valueFromASTUntyped'; import type { ScalarTypeDefinitionNode, @@ -454,10 +453,11 @@ export class GraphQLScalarType { _scalarConfig: GraphQLScalarTypeConfig<*, *>; constructor(config: GraphQLScalarTypeConfig<*, *>): void { - assertValidName(config.name); this.name = config.name; this.description = config.description; this.astNode = config.astNode; + this._scalarConfig = config; + invariant(typeof config.name === 'string', 'Must provide name.'); invariant( typeof config.serialize === 'function', `${this.name} must provide "serialize" function. If this custom Scalar ` + @@ -472,7 +472,6 @@ export class GraphQLScalarType { 'functions.', ); } - this._scalarConfig = config; } // Serializes an internal value to include in a response. @@ -563,7 +562,7 @@ export class GraphQLObjectType { name: string; description: ?string; astNode: ?ObjectTypeDefinitionNode; - extensionASTNodes: ?Array; + extensionASTNodes: ?$ReadOnlyArray; isTypeOf: ?GraphQLIsTypeOfFn<*, *>; _typeConfig: GraphQLObjectTypeConfig<*, *>; @@ -571,19 +570,19 @@ export class GraphQLObjectType { _interfaces: Array; constructor(config: GraphQLObjectTypeConfig<*, *>): void { - assertValidName(config.name, config.isIntrospection); this.name = config.name; this.description = config.description; this.astNode = config.astNode; this.extensionASTNodes = config.extensionASTNodes; + this.isTypeOf = config.isTypeOf; + this._typeConfig = config; + invariant(typeof config.name === 'string', 'Must provide name.'); if (config.isTypeOf) { invariant( typeof config.isTypeOf === 'function', `${this.name} must provide "isTypeOf" as a function.`, ); } - this.isTypeOf = config.isTypeOf; - this._typeConfig = config; } getFields(): GraphQLFieldMap<*, *> { @@ -616,10 +615,7 @@ function defineInterfaces( type: GraphQLObjectType, interfacesThunk: Thunk>, ): Array { - const interfaces = resolveThunk(interfacesThunk); - if (!interfaces) { - return []; - } + const interfaces = resolveThunk(interfacesThunk) || []; invariant( Array.isArray(interfaces), `${type.name} interfaces must be an Array or a function which returns ` + @@ -632,23 +628,15 @@ function defineFieldMap( type: GraphQLNamedType, fieldsThunk: Thunk>, ): GraphQLFieldMap { - const fieldMap = resolveThunk(fieldsThunk); + const fieldMap = resolveThunk(fieldsThunk) || {}; invariant( isPlainObj(fieldMap), `${type.name} fields must be an object with field names as keys or a ` + 'function which returns such an object.', ); - const fieldNames = Object.keys(fieldMap); - invariant( - fieldNames.length > 0, - `${type.name} fields must be an object with field names as keys or a ` + - 'function which returns such an object.', - ); - const resultFieldMap = Object.create(null); - fieldNames.forEach(fieldName => { - assertValidName(fieldName); + Object.keys(fieldMap).forEach(fieldName => { const fieldConfig = fieldMap[fieldName]; invariant( isPlainObj(fieldConfig), @@ -664,11 +652,6 @@ function defineFieldMap( isDeprecated: Boolean(fieldConfig.deprecationReason), name: fieldName, }; - invariant( - isOutputType(field.type), - `${type.name}.${fieldName} field type must be Output Type but ` + - `got: ${String(field.type)}.`, - ); invariant( isValidResolver(field.resolve), `${type.name}.${fieldName} field resolver must be a function if ` + @@ -684,13 +667,7 @@ function defineFieldMap( 'names as keys.', ); field.args = Object.keys(argsConfig).map(argName => { - assertValidName(argName); const arg = argsConfig[argName]; - invariant( - isInputType(arg.type), - `${type.name}.${fieldName}(${argName}:) argument type must be ` + - `Input Type but got: ${String(arg.type)}.`, - ); return { name: argName, description: arg.description === undefined ? null : arg.description, @@ -720,9 +697,8 @@ export type GraphQLObjectTypeConfig = { fields: Thunk>, isTypeOf?: ?GraphQLIsTypeOfFn, description?: ?string, - isIntrospection?: boolean, astNode?: ?ObjectTypeDefinitionNode, - extensionASTNodes?: ?Array, + extensionASTNodes?: ?$ReadOnlyArray, }; export type GraphQLTypeResolver = ( @@ -831,26 +807,26 @@ export class GraphQLInterfaceType { name: string; description: ?string; astNode: ?InterfaceTypeDefinitionNode; - extensionASTNodes: ?Array; + extensionASTNodes: ?$ReadOnlyArray; resolveType: ?GraphQLTypeResolver<*, *>; _typeConfig: GraphQLInterfaceTypeConfig<*, *>; _fields: GraphQLFieldMap<*, *>; constructor(config: GraphQLInterfaceTypeConfig<*, *>): void { - assertValidName(config.name); this.name = config.name; this.description = config.description; this.astNode = config.astNode; this.extensionASTNodes = config.extensionASTNodes; + this.resolveType = config.resolveType; + this._typeConfig = config; + invariant(typeof config.name === 'string', 'Must provide name.'); if (config.resolveType) { invariant( typeof config.resolveType === 'function', `${this.name} must provide "resolveType" as a function.`, ); } - this.resolveType = config.resolveType; - this._typeConfig = config; } getFields(): GraphQLFieldMap<*, *> { @@ -883,7 +859,7 @@ export type GraphQLInterfaceTypeConfig = { resolveType?: ?GraphQLTypeResolver, description?: ?string, astNode?: ?InterfaceTypeDefinitionNode, - extensionASTNodes?: ?Array, + extensionASTNodes?: ?$ReadOnlyArray, }; /** @@ -919,18 +895,18 @@ export class GraphQLUnionType { _types: Array; constructor(config: GraphQLUnionTypeConfig<*, *>): void { - assertValidName(config.name); this.name = config.name; this.description = config.description; this.astNode = config.astNode; + this.resolveType = config.resolveType; + this._typeConfig = config; + invariant(typeof config.name === 'string', 'Must provide name.'); if (config.resolveType) { invariant( typeof config.resolveType === 'function', `${this.name} must provide "resolveType" as a function.`, ); } - this.resolveType = config.resolveType; - this._typeConfig = config; } getTypes(): Array { @@ -955,27 +931,12 @@ function defineTypes( unionType: GraphQLUnionType, typesThunk: Thunk>, ): Array { - const types = resolveThunk(typesThunk); - + const types = resolveThunk(typesThunk) || []; invariant( - Array.isArray(types) && types.length > 0, + Array.isArray(types), 'Must provide Array of types or a function which returns ' + `such an array for Union ${unionType.name}.`, ); - const includedTypeNames = Object.create(null); - types.forEach(objType => { - invariant( - isObjectType(objType), - `${unionType.name} may only contain Object types, it cannot contain: ` + - `${String(objType)}.`, - ); - invariant( - !includedTypeNames[objType.name], - `${unionType.name} can include ${objType.name} type only once.`, - ); - includedTypeNames[objType.name] = true; - }); - return types; } @@ -1025,15 +986,17 @@ export class GraphQLEnumType /* */ { constructor(config: GraphQLEnumTypeConfig /* */): void { this.name = config.name; - assertValidName(config.name, config.isIntrospection); this.description = config.description; this.astNode = config.astNode; - this._values = defineEnumValues(this, config.values); this._enumConfig = config; + invariant(typeof config.name === 'string', 'Must provide name.'); } getValues(): Array */> { - return this._values; + return ( + this._values || + (this._values = defineEnumValues(this, this._enumConfig.values)) + ); } getValue(name: string): ?GraphQLEnumValue { @@ -1108,18 +1071,7 @@ function defineEnumValues( isPlainObj(valueMap), `${type.name} values must be an object with value names as keys.`, ); - const valueNames = Object.keys(valueMap); - invariant( - valueNames.length > 0, - `${type.name} values must be an object with value names as keys.`, - ); - return valueNames.map(valueName => { - assertValidName(valueName); - invariant( - ['true', 'false', 'null'].indexOf(valueName) === -1, - `Name "${valueName}" can not be used as an Enum value.`, - ); - + return Object.keys(valueMap).map(valueName => { const value = valueMap[valueName]; invariant( isPlainObj(value), @@ -1147,7 +1099,6 @@ export type GraphQLEnumTypeConfig /* */ = { values: GraphQLEnumValueConfigMap /* */, description?: ?string, astNode?: ?EnumTypeDefinitionNode, - isIntrospection?: boolean, }; export type GraphQLEnumValueConfigMap /* */ = ObjMap< @@ -1199,11 +1150,11 @@ export class GraphQLInputObjectType { _fields: GraphQLInputFieldMap; constructor(config: GraphQLInputObjectTypeConfig): void { - assertValidName(config.name); this.name = config.name; this.description = config.description; this.astNode = config.astNode; this._typeConfig = config; + invariant(typeof config.name === 'string', 'Must provide name.'); } getFields(): GraphQLInputFieldMap { @@ -1211,32 +1162,20 @@ export class GraphQLInputObjectType { } _defineFieldMap(): GraphQLInputFieldMap { - const fieldMap: any = resolveThunk(this._typeConfig.fields); + const fieldMap: any = resolveThunk(this._typeConfig.fields) || {}; invariant( isPlainObj(fieldMap), `${this.name} fields must be an object with field names as keys or a ` + 'function which returns such an object.', ); - const fieldNames = Object.keys(fieldMap); - invariant( - fieldNames.length > 0, - `${this.name} fields must be an object with field names as keys or a ` + - 'function which returns such an object.', - ); const resultFieldMap = Object.create(null); - fieldNames.forEach(fieldName => { - assertValidName(fieldName); + Object.keys(fieldMap).forEach(fieldName => { const field = { ...fieldMap[fieldName], name: fieldName, }; invariant( - isInputType(field.type), - `${this.name}.${fieldName} field type must be Input Type but ` + - `got: ${String(field.type)}.`, - ); - invariant( - field.resolve == null, + !field.hasOwnProperty('resolve'), `${this.name}.${fieldName} field type has a resolve property, but ` + 'Input Types cannot define resolvers.', ); diff --git a/src/type/directives.js b/src/type/directives.js index 885087808d..d87efe495b 100644 --- a/src/type/directives.js +++ b/src/type/directives.js @@ -7,17 +7,14 @@ * @flow */ -import { isInputType } from './definition'; -import { GraphQLNonNull } from './wrappers'; - import type { GraphQLFieldConfigArgumentMap, GraphQLArgument, } from './definition'; +import { GraphQLNonNull } from './wrappers'; import { GraphQLString, GraphQLBoolean } from './scalars'; import instanceOf from '../jsutils/instanceOf'; import invariant from '../jsutils/invariant'; -import { assertValidName } from '../utilities/assertValidName'; import type { DirectiveDefinitionNode } from '../language/ast'; import { DirectiveLocation, @@ -47,16 +44,15 @@ export class GraphQLDirective { astNode: ?DirectiveDefinitionNode; constructor(config: GraphQLDirectiveConfig): void { + this.name = config.name; + this.description = config.description; + this.locations = config.locations; + this.astNode = config.astNode; invariant(config.name, 'Directive must be named.'); - assertValidName(config.name); invariant( Array.isArray(config.locations), 'Must provide locations for directive.', ); - this.name = config.name; - this.description = config.description; - this.locations = config.locations; - this.astNode = config.astNode; const args = config.args; if (!args) { @@ -67,13 +63,7 @@ export class GraphQLDirective { `@${config.name} args must be an object with argument names as keys.`, ); this.args = Object.keys(args).map(argName => { - assertValidName(argName); const arg = args[argName]; - invariant( - isInputType(arg.type), - `@${config.name}(${argName}:) argument type must be ` + - `Input Type but got: ${String(arg.type)}.`, - ); return { name: argName, description: arg.description === undefined ? null : arg.description, diff --git a/src/type/validate.js b/src/type/validate.js index ef50d785e3..bf6ec8315a 100644 --- a/src/type/validate.js +++ b/src/type/validate.js @@ -8,26 +8,45 @@ */ import { - isType, isObjectType, isInterfaceType, + isUnionType, + isEnumType, + isInputObjectType, isNonNullType, + isNamedType, + isInputType, + isOutputType, +} from './definition'; +import type { + GraphQLObjectType, + GraphQLInterfaceType, + GraphQLUnionType, + GraphQLEnumType, + GraphQLInputObjectType, } from './definition'; -import type { GraphQLInterfaceType, GraphQLObjectType } from './definition'; import { isDirective } from './directives'; +import type { GraphQLDirective } from './directives'; +import { isIntrospectionType } from './introspection'; import { isSchema } from './schema'; import type { GraphQLSchema } from './schema'; import find from '../jsutils/find'; import invariant from '../jsutils/invariant'; -import { isEqualType, isTypeSubTypeOf } from '../utilities/typeComparators'; +import { GraphQLError } from '../error/GraphQLError'; import type { ASTNode, + ObjectTypeDefinitionNode, + ObjectTypeExtensionNode, + InterfaceTypeDefinitionNode, + InterfaceTypeExtensionNode, FieldDefinitionNode, + EnumValueDefinitionNode, InputValueDefinitionNode, NamedTypeNode, TypeNode, } from '../language/ast'; -import { GraphQLError } from '../error/GraphQLError'; +import { isValidNameError } from '../utilities/assertValidName'; +import { isEqualType, isTypeSubTypeOf } from '../utilities/typeComparators'; /** * Implements the "Type Validation" sub-sections of the specification's @@ -51,10 +70,10 @@ export function validateSchema( } // Validate the schema, producing a list of errors. - const context = new SchemaValidationContext(); - validateRootTypes(context, schema); - validateDirectives(context, schema); - validateTypes(context, schema); + const context = new SchemaValidationContext(schema); + validateRootTypes(context); + validateDirectives(context); + validateTypes(context); // Persist the results of validation before returning to ensure validation // does not run multiple times for this schema. @@ -75,10 +94,12 @@ export function assertValidSchema(schema: GraphQLSchema): void { } class SchemaValidationContext { - _errors: Array; + +_errors: Array; + +schema: GraphQLSchema; - constructor() { + constructor(schema) { this._errors = []; + this.schema = schema; } reportError( @@ -86,7 +107,11 @@ class SchemaValidationContext { nodes?: $ReadOnlyArray | ?ASTNode, ): void { const _nodes = (Array.isArray(nodes) ? nodes : [nodes]).filter(Boolean); - this._errors.push(new GraphQLError(message, _nodes)); + this.addError(new GraphQLError(message, _nodes)); + } + + addError(error: GraphQLError): void { + this._errors.push(error); } getErrors(): $ReadOnlyArray { @@ -94,13 +119,14 @@ class SchemaValidationContext { } } -function validateRootTypes(context, schema) { +function validateRootTypes(context) { + const schema = context.schema; const queryType = schema.getQueryType(); if (!queryType) { context.reportError(`Query root type must be provided.`, schema.astNode); } else if (!isObjectType(queryType)) { context.reportError( - `Query root type must be Object type but got: ${String(queryType)}.`, + `Query root type must be Object type, it cannot be ${String(queryType)}.`, getOperationTypeNode(schema, queryType, 'query'), ); } @@ -108,7 +134,7 @@ function validateRootTypes(context, schema) { const mutationType = schema.getMutationType(); if (mutationType && !isObjectType(mutationType)) { context.reportError( - 'Mutation root type must be Object type if provided but got: ' + + 'Mutation root type must be Object type if provided, it cannot be ' + `${String(mutationType)}.`, getOperationTypeNode(schema, mutationType, 'mutation'), ); @@ -117,7 +143,7 @@ function validateRootTypes(context, schema) { const subscriptionType = schema.getSubscriptionType(); if (subscriptionType && !isObjectType(subscriptionType)) { context.reportError( - 'Subscription root type must be Object type if provided but got: ' + + 'Subscription root type must be Object type if provided, it cannot be ' + `${String(subscriptionType)}.`, getOperationTypeNode(schema, subscriptionType, 'subscription'), ); @@ -138,65 +164,200 @@ function getOperationTypeNode( return operationTypeNode ? operationTypeNode.type : type && type.astNode; } -function validateDirectives( - context: SchemaValidationContext, - schema: GraphQLSchema, -): void { - const directives = schema.getDirectives(); +function validateDirectives(context: SchemaValidationContext): void { + const directives = context.schema.getDirectives(); directives.forEach(directive => { + // Ensure all directives are in fact GraphQL directives. if (!isDirective(directive)) { context.reportError( `Expected directive but got: ${String(directive)}.`, directive && directive.astNode, ); + return; } + + // Ensure they are named correctly. + validateName(context, directive); + + // TODO: Ensure proper locations. + + // Ensure the arguments are valid. + const argNames = Object.create(null); + directive.args.forEach(arg => { + const argName = arg.name; + + // Ensure they are named correctly. + validateName(context, arg); + + // Ensure they are unique per directive. + if (argNames[argName]) { + context.reportError( + `Argument @${directive.name}(${argName}:) can only be defined once.`, + getAllDirectiveArgNodes(directive, argName), + ); + return; // continue loop + } + argNames[argName] = true; + + // Ensure the type is an input type. + if (!isInputType(arg.type)) { + context.reportError( + `The type of @${directive.name}(${argName}:) must be Input Type ` + + `but got: ${String(arg.type)}.`, + getDirectiveArgTypeNode(directive, argName), + ); + } + }); }); } -function validateTypes( +function validateName( context: SchemaValidationContext, - schema: GraphQLSchema, + node: { +name: string, +astNode: ?ASTNode }, ): void { - const typeMap = schema.getTypeMap(); + // Ensure names are valid, however introspection types opt out. + const error = isValidNameError(node.name, node.astNode || undefined); + if (error && !isIntrospectionType((node: any))) { + context.addError(error); + } +} + +function validateTypes(context: SchemaValidationContext): void { + const typeMap = context.schema.getTypeMap(); Object.keys(typeMap).forEach(typeName => { const type = typeMap[typeName]; // Ensure all provided types are in fact GraphQL type. - if (!isType(type)) { + if (!isNamedType(type)) { context.reportError( - `Expected GraphQL type but got: ${String(type)}.`, + `Expected GraphQL named type but got: ${String(type)}.`, type && type.astNode, ); + return; } - // Ensure objects implement the interfaces they claim to. + // Ensure they are named correctly. + validateName(context, type); + if (isObjectType(type)) { - const implementedTypeNames = Object.create(null); - - type.getInterfaces().forEach(iface => { - if (implementedTypeNames[iface.name]) { - context.reportError( - `${type.name} must declare it implements ${iface.name} only once.`, - getAllImplementsInterfaceNode(type, iface), - ); - } - implementedTypeNames[iface.name] = true; - validateObjectImplementsInterface(context, schema, type, iface); - }); + // Ensure fields are valid + validateFields(context, type); + + // Ensure objects implement the interfaces they claim to. + validateObjectInterfaces(context, type); + } else if (isInterfaceType(type)) { + // Ensure fields are valid. + validateFields(context, type); + } else if (isUnionType(type)) { + // Ensure Unions include valid member types. + validateUnionMembers(context, type); + } else if (isEnumType(type)) { + // Ensure Enums have valid values. + validateEnumValues(context, type); + } else if (isInputObjectType(type)) { + // Ensure Input Object fields are valid. + validateInputFields(context, type); + } + }); +} + +function validateFields( + context: SchemaValidationContext, + type: GraphQLObjectType | GraphQLInterfaceType, +): void { + const fieldMap = type.getFields(); + const fieldNames = Object.keys(fieldMap); + + // Objects and Interfaces both must define one or more fields. + if (fieldNames.length === 0) { + context.reportError( + `Type ${type.name} must define one or more fields.`, + getAllObjectOrInterfaceNodes(type), + ); + } + + fieldNames.forEach(fieldName => { + const field = fieldMap[fieldName]; + + // Ensure they are named correctly. + validateName(context, field); + + // Ensure they were defined at most once. + const fieldNodes = getAllFieldNodes(type, fieldName); + if (fieldNodes.length > 1) { + context.reportError( + `Field ${type.name}.${fieldName} can only be defined once.`, + fieldNodes, + ); + return; // continue loop } + + // Ensure the type is an output type + if (!isOutputType(field.type)) { + context.reportError( + `The type of ${type.name}.${fieldName} must be Output Type ` + + `but got: ${String(field.type)}.`, + getFieldTypeNode(type, fieldName), + ); + } + + // Ensure the arguments are valid + const argNames = Object.create(null); + field.args.forEach(arg => { + const argName = arg.name; + + // Ensure they are named correctly. + validateName(context, arg); + + // Ensure they are unique per field. + if (argNames[argName]) { + context.reportError( + `Field argument ${type.name}.${fieldName}(${argName}:) can only ` + + 'be defined once.', + getAllFieldArgNodes(type, fieldName, argName), + ); + } + argNames[argName] = true; + + // Ensure the type is an input type + if (!isInputType(arg.type)) { + context.reportError( + `The type of ${type.name}.${fieldName}(${argName}:) must be Input ` + + `Type but got: ${String(arg.type)}.`, + getFieldArgTypeNode(type, fieldName, argName), + ); + } + }); + }); +} + +function validateObjectInterfaces( + context: SchemaValidationContext, + object: GraphQLObjectType, +): void { + const implementedTypeNames = Object.create(null); + object.getInterfaces().forEach(iface => { + if (implementedTypeNames[iface.name]) { + context.reportError( + `Type ${object.name} can only implement ${iface.name} once.`, + getAllImplementsInterfaceNodes(object, iface), + ); + return; // continue loop + } + implementedTypeNames[iface.name] = true; + validateObjectImplementsInterface(context, object, iface); }); } function validateObjectImplementsInterface( context: SchemaValidationContext, - schema: GraphQLSchema, object: GraphQLObjectType, iface: GraphQLInterfaceType, ): void { if (!isInterfaceType(iface)) { context.reportError( - `${String(object)} must only implement Interface types, it cannot ` + - `implement ${String(iface)}.`, + `Type ${String(object)} must only implement Interface types, ` + + `it cannot implement ${String(iface)}.`, getImplementsInterfaceNode(object, iface), ); return; @@ -213,8 +374,8 @@ function validateObjectImplementsInterface( // Assert interface field exists on object. if (!objectField) { context.reportError( - `"${iface.name}" expects field "${fieldName}" but "${object.name}" ` + - 'does not provide it.', + `Interface field ${iface.name}.${fieldName} expected but ` + + `${object.name} does not provide it.`, [getFieldNode(iface, fieldName), object.astNode], ); // Continue loop over fields. @@ -223,11 +384,11 @@ function validateObjectImplementsInterface( // Assert interface field type is satisfied by object field type, by being // a valid subtype. (covariant) - if (!isTypeSubTypeOf(schema, objectField.type, ifaceField.type)) { + if (!isTypeSubTypeOf(context.schema, objectField.type, ifaceField.type)) { context.reportError( - `${iface.name}.${fieldName} expects type ` + - `"${String(ifaceField.type)}" but ${object.name}.${fieldName} is ` + - `type "${String(objectField.type)}".`, + `Interface field ${iface.name}.${fieldName} expects type ` + + `${String(ifaceField.type)} but ${object.name}.${fieldName} ` + + `is type ${String(objectField.type)}.`, [ getFieldTypeNode(iface, fieldName), getFieldTypeNode(object, fieldName), @@ -243,8 +404,8 @@ function validateObjectImplementsInterface( // Assert interface field arg exists on object field. if (!objectArg) { context.reportError( - `${iface.name}.${fieldName} expects argument "${argName}" but ` + - `${object.name}.${fieldName} does not provide it.`, + `Interface field argument ${iface.name}.${fieldName}(${argName}:) ` + + `expected but ${object.name}.${fieldName} does not provide it.`, [ getFieldArgNode(iface, fieldName, argName), getFieldNode(object, fieldName), @@ -259,10 +420,10 @@ function validateObjectImplementsInterface( // TODO: change to contravariant? if (!isEqualType(ifaceArg.type, objectArg.type)) { context.reportError( - `${iface.name}.${fieldName}(${argName}:) expects type ` + - `"${String(ifaceArg.type)}" but ` + + `Interface field argument ${iface.name}.${fieldName}(${argName}:) ` + + `expects type ${String(ifaceArg.type)} but ` + `${object.name}.${fieldName}(${argName}:) is type ` + - `"${String(objectArg.type)}".`, + `${String(objectArg.type)}.`, [ getFieldArgTypeNode(iface, fieldName, argName), getFieldArgTypeNode(object, fieldName, argName), @@ -279,9 +440,9 @@ function validateObjectImplementsInterface( const ifaceArg = find(ifaceField.args, arg => arg.name === argName); if (!ifaceArg && isNonNullType(objectArg.type)) { context.reportError( - `${object.name}.${fieldName}(${argName}:) is of required type ` + - `"${String(objectArg.type)}" but is not also provided by the ` + - `interface ${iface.name}.${fieldName}.`, + `Object field argument ${object.name}.${fieldName}(${argName}:) ` + + `is of required type ${String(objectArg.type)} but is not also ` + + `provided by the Interface field ${iface.name}.${fieldName}.`, [ getFieldArgTypeNode(object, fieldName, argName), getFieldNode(iface, fieldName), @@ -292,19 +453,148 @@ function validateObjectImplementsInterface( }); } +function validateUnionMembers( + context: SchemaValidationContext, + union: GraphQLUnionType, +): void { + const memberTypes = union.getTypes(); + + if (memberTypes.length === 0) { + context.reportError( + `Union type ${union.name} must define one or more member types.`, + union.astNode, + ); + } + + const includedTypeNames = Object.create(null); + memberTypes.forEach(memberType => { + if (includedTypeNames[memberType.name]) { + context.reportError( + `Union type ${union.name} can only include type ` + + `${memberType.name} once.`, + getUnionMemberTypeNodes(union, memberType.name), + ); + return; // continue loop + } + includedTypeNames[memberType.name] = true; + if (!isObjectType(memberType)) { + context.reportError( + `Union type ${union.name} can only include Object types, ` + + `it cannot include ${String(memberType)}.`, + getUnionMemberTypeNodes(union, String(memberType)), + ); + } + }); +} + +function validateEnumValues( + context: SchemaValidationContext, + enumType: GraphQLEnumType, +): void { + const enumValues = enumType.getValues(); + + if (enumValues.length === 0) { + context.reportError( + `Enum type ${enumType.name} must define one or more values.`, + enumType.astNode, + ); + } + + enumValues.forEach(enumValue => { + const valueName = enumValue.name; + + // Ensure no duplicates. + const allNodes = getEnumValueNodes(enumType, valueName); + if (allNodes && allNodes.length > 1) { + context.reportError( + `Enum type ${enumType.name} can include value ${valueName} only once.`, + allNodes, + ); + } + + // Ensure valid name. + validateName(context, enumValue); + if (valueName === 'true' || valueName === 'false' || valueName === 'null') { + context.reportError( + `Enum type ${enumType.name} cannot include value: ${valueName}.`, + enumValue.astNode, + ); + } + }); +} + +function validateInputFields( + context: SchemaValidationContext, + inputObj: GraphQLInputObjectType, +): void { + const fieldMap = inputObj.getFields(); + const fieldNames = Object.keys(fieldMap); + + if (fieldNames.length === 0) { + context.reportError( + `Input Object type ${inputObj.name} must define one or more fields.`, + inputObj.astNode, + ); + } + + // Ensure the arguments are valid + fieldNames.forEach(fieldName => { + const field = fieldMap[fieldName]; + + // Ensure they are named correctly. + validateName(context, field); + + // TODO: Ensure they are unique per field. + + // Ensure the type is an input type + if (!isInputType(field.type)) { + context.reportError( + `The type of ${inputObj.name}.${fieldName} must be Input Type ` + + `but got: ${String(field.type)}.`, + field.astNode && field.astNode.type, + ); + } + }); +} + +function getAllObjectNodes( + type: GraphQLObjectType, +): $ReadOnlyArray { + return type.astNode + ? type.extensionASTNodes + ? [type.astNode].concat(type.extensionASTNodes) + : [type.astNode] + : type.extensionASTNodes || []; +} + +function getAllObjectOrInterfaceNodes( + type: GraphQLObjectType | GraphQLInterfaceType, +): $ReadOnlyArray< + | ObjectTypeDefinitionNode + | ObjectTypeExtensionNode + | InterfaceTypeDefinitionNode + | InterfaceTypeExtensionNode, +> { + return type.astNode + ? type.extensionASTNodes + ? [type.astNode].concat(type.extensionASTNodes) + : [type.astNode] + : type.extensionASTNodes || []; +} + function getImplementsInterfaceNode( type: GraphQLObjectType, iface: GraphQLInterfaceType, ): ?NamedTypeNode { - return getAllImplementsInterfaceNode(type, iface)[0]; + return getAllImplementsInterfaceNodes(type, iface)[0]; } -function getAllImplementsInterfaceNode( +function getAllImplementsInterfaceNodes( type: GraphQLObjectType, iface: GraphQLInterfaceType, -): Array { +): $ReadOnlyArray { const implementsNodes = []; - const astNodes = [type.astNode].concat(type.extensionASTNodes || []); + const astNodes = getAllObjectNodes(type); for (let i = 0; i < astNodes.length; i++) { const astNode = astNodes[i]; if (astNode && astNode.interfaces) { @@ -322,17 +612,26 @@ function getFieldNode( type: GraphQLObjectType | GraphQLInterfaceType, fieldName: string, ): ?FieldDefinitionNode { - const astNodes = [type.astNode].concat(type.extensionASTNodes || []); + return getAllFieldNodes(type, fieldName)[0]; +} + +function getAllFieldNodes( + type: GraphQLObjectType | GraphQLInterfaceType, + fieldName: string, +): $ReadOnlyArray { + const fieldNodes = []; + const astNodes = getAllObjectOrInterfaceNodes(type); for (let i = 0; i < astNodes.length; i++) { const astNode = astNodes[i]; - const fieldNode = - astNode && - astNode.fields && - astNode.fields.find(node => node.name.value === fieldName); - if (fieldNode) { - return fieldNode; + if (astNode && astNode.fields) { + astNode.fields.forEach(node => { + if (node.name.value === fieldName) { + fieldNodes.push(node); + } + }); } } + return fieldNodes; } function getFieldTypeNode( @@ -348,12 +647,24 @@ function getFieldArgNode( fieldName: string, argName: string, ): ?InputValueDefinitionNode { + return getAllFieldArgNodes(type, fieldName, argName)[0]; +} + +function getAllFieldArgNodes( + type: GraphQLObjectType | GraphQLInterfaceType, + fieldName: string, + argName: string, +): $ReadOnlyArray { + const argNodes = []; const fieldNode = getFieldNode(type, fieldName); - return ( - fieldNode && - fieldNode.arguments && - fieldNode.arguments.find(node => node.name.value === argName) - ); + if (fieldNode && fieldNode.arguments) { + fieldNode.arguments.forEach(node => { + if (node.name.value === argName) { + argNodes.push(node); + } + }); + } + return argNodes; } function getFieldArgTypeNode( @@ -364,3 +675,49 @@ function getFieldArgTypeNode( const fieldArgNode = getFieldArgNode(type, fieldName, argName); return fieldArgNode && fieldArgNode.type; } + +function getAllDirectiveArgNodes( + directive: GraphQLDirective, + argName: string, +): $ReadOnlyArray { + const argNodes = []; + const directiveNode = directive.astNode; + if (directiveNode && directiveNode.arguments) { + directiveNode.arguments.forEach(node => { + if (node.name.value === argName) { + argNodes.push(node); + } + }); + } + return argNodes; +} + +function getDirectiveArgTypeNode( + directive: GraphQLDirective, + argName: string, +): ?TypeNode { + const argNode = getAllDirectiveArgNodes(directive, argName)[0]; + return argNode && argNode.type; +} + +function getUnionMemberTypeNodes( + union: GraphQLUnionType, + typeName: string, +): ?$ReadOnlyArray { + return ( + union.astNode && + union.astNode.types && + union.astNode.types.filter(type => type.name.value === typeName) + ); +} + +function getEnumValueNodes( + enumType: GraphQLEnumType, + valueName: string, +): ?$ReadOnlyArray { + return ( + enumType.astNode && + enumType.astNode.values && + enumType.astNode.values.filter(value => value.name.value === valueName) + ); +} diff --git a/src/utilities/__tests__/assertValidName-test.js b/src/utilities/__tests__/assertValidName-test.js index 4ed63d71e5..99fef0f82b 100644 --- a/src/utilities/__tests__/assertValidName-test.js +++ b/src/utilities/__tests__/assertValidName-test.js @@ -5,176 +5,22 @@ * LICENSE file in the root directory of this source tree. */ -import { afterEach, beforeEach, describe, it } from 'mocha'; -import chai, { expect } from 'chai'; -import { formatWarning } from '../assertValidName'; -import dedent from '../../jsutils/dedent'; - -/* eslint-disable no-console */ - -/** - * Convenience method for creating an Error object with a defined stack. - */ -function createErrorObject(message, stack) { - const error = new Error(message); - error.stack = stack; - return error; -} +import { describe, it } from 'mocha'; +import { expect } from 'chai'; +import { assertValidName } from '../assertValidName'; describe('assertValidName()', () => { - let assertValidName; - let noNameWarning; - let warn; - - beforeEach(() => { - noNameWarning = process.env.GRAPHQL_NO_NAME_WARNING; - delete process.env.GRAPHQL_NO_NAME_WARNING; - warn = console.warn; - console.warn = chai.spy(); - - // Make sure module-internal state is reset for each test. - delete require.cache[require.resolve('../assertValidName')]; - assertValidName = require('../assertValidName').assertValidName; - }); - - afterEach(() => { - console.warn = warn; - if (noNameWarning === undefined) { - delete process.env.GRAPHQL_NO_NAME_WARNING; - } else { - process.env.GRAPHQL_NO_NAME_WARNING = noNameWarning; - } - }); - - it('warns against use of leading double underscores', () => { - assertValidName('__bad'); - /* eslint-disable no-unused-expressions */ - expect(console.warn).to.have.been.called.once; - /* eslint-enable no-unused-expressions */ - expect(console.warn.__spy.calls[0][0]).to.match(/must not begin with/); - }); - - it('warns exactly once even in the presence of multiple violations', () => { - assertValidName('__bad'); - assertValidName('__alsoBad'); - /* eslint-disable no-unused-expressions */ - expect(console.warn).to.have.been.called.once; - /* eslint-enable no-unused-expressions */ + it('throws for use of leading double underscores', () => { + expect(() => assertValidName('__bad')).to.throw( + '"__bad" must not begin with "__", which is reserved by GraphQL introspection.', + ); }); it('throws for non-strings', () => { - expect(() => assertValidName({})).to.throw(/Must be named/); + expect(() => assertValidName({})).to.throw(/Expected string/); }); it('throws for names with invalid characters', () => { expect(() => assertValidName('>--()-->')).to.throw(/Names must match/); }); - - it('does not warn during introspection', () => { - assertValidName('__bad', true); - expect(console.warn).not.to.have.been.called(); - }); - - it('does not warn when GRAPHQL_NO_NAME_WARNING is in effect', () => { - process.env.GRAPHQL_NO_NAME_WARNING = '1'; - assertValidName('__bad', true); - expect(console.warn).not.to.have.been.called(); - }); -}); - -describe('formatWarning()', () => { - it('formats given a Chrome-style stack property', () => { - const chromeStack = dedent` - Error: foo - at z (:1:21) - at y (:1:15) - at x (:1:15) - at :1:6`; - const error = createErrorObject('foo', chromeStack); - expect(formatWarning(error)).to.equal( - dedent` - foo - at z (:1:21) - at y (:1:15) - at x (:1:15) - at :1:6`, - ); - }); - - it('formats given a Node-style stack property', () => { - const nodeStack = dedent` - Error: foo - at z (repl:1:29) - at y (repl:1:23) - at x (repl:1:23) - at repl:1:6 - at ContextifyScript.Script.runInThisContext (vm.js:23:33) - at REPLServer.defaultEval (repl.js:340:29) - at bound (domain.js:280:14) - at REPLServer.runBound [as eval] (domain.js:293:12) - at REPLServer.onLine (repl.js:537:10) - at emitOne (events.js:101:20)`; - const error = createErrorObject('foo', nodeStack); - expect(formatWarning(error)).to.equal( - dedent` - foo - at z (repl:1:29) - at y (repl:1:23) - at x (repl:1:23) - at repl:1:6 - at ContextifyScript.Script.runInThisContext (vm.js:23:33) - at REPLServer.defaultEval (repl.js:340:29) - at bound (domain.js:280:14) - at REPLServer.runBound [as eval] (domain.js:293:12) - at REPLServer.onLine (repl.js:537:10) - at emitOne (events.js:101:20)`, - ); - }); - - it('formats given a Firefox-style stack property', () => { - const firefoxStack = dedent` - z@debugger eval code:1:20 - y@debugger eval code:1:14 - x@debugger eval code:1:14 - @debugger eval code:1:5`; - const error = createErrorObject('foo', firefoxStack); - expect(formatWarning(error)).to.equal( - dedent` - foo - z@debugger eval code:1:20 - y@debugger eval code:1:14 - x@debugger eval code:1:14 - @debugger eval code:1:5`, - ); - }); - - it('formats given a Safari-style stack property', () => { - const safariStack = dedent` - z - y - x - global code - evaluateWithScopeExtension@[native code] - _evaluateOn - _evaluateAndWrap - evaluate`; - const error = createErrorObject('foo', safariStack); - expect(formatWarning(error)).to.equal( - dedent` - foo - z - y - x - global code - evaluateWithScopeExtension@[native code] - _evaluateOn - _evaluateAndWrap - evaluate`, - ); - }); - - it('formats in the absence of a stack property', () => { - const error = createErrorObject('foo'); - expect(formatWarning(error)).to.equal('foo'); - }); }); diff --git a/src/utilities/__tests__/extendSchema-test.js b/src/utilities/__tests__/extendSchema-test.js index aecee24072..bf97c8735c 100644 --- a/src/utilities/__tests__/extendSchema-test.js +++ b/src/utilities/__tests__/extendSchema-test.js @@ -970,18 +970,6 @@ describe('extendSchema', () => { ); }); - it('does not allow implementing an existing interface', () => { - const ast = parse(` - extend type Foo implements SomeInterface { - otherField: String - } - `); - expect(() => extendSchema(testSchema, ast)).to.throw( - 'Type "Foo" already implements "SomeInterface". It cannot also be ' + - 'implemented in this type extension.', - ); - }); - it('does not allow referencing an unknown type', () => { const ast = parse(` extend type Bar { diff --git a/src/utilities/assertValidName.js b/src/utilities/assertValidName.js index 5077dc5059..07082b4699 100644 --- a/src/utilities/assertValidName.js +++ b/src/utilities/assertValidName.js @@ -7,65 +7,42 @@ * @flow */ -const NAME_RX = /^[_a-zA-Z][_a-zA-Z0-9]*$/; -const ERROR_PREFIX_RX = /^Error: /; - -// Silences warnings if an environment flag is enabled -const noNameWarning = Boolean( - typeof process !== 'undefined' && - process && - process.env && - process.env.GRAPHQL_NO_NAME_WARNING, -); +import { GraphQLError } from '../error/GraphQLError'; +import type { ASTNode } from '../language/ast'; +import invariant from '../jsutils/invariant'; -// Ensures console warnings are only issued once. -let hasWarnedAboutDunder = false; +const NAME_RX = /^[_a-zA-Z][_a-zA-Z0-9]*$/; /** * Upholds the spec rules about naming. */ -export function assertValidName(name: string, isIntrospection?: boolean): void { - if (!name || typeof name !== 'string') { - throw new Error(`Must be named. Unexpected name: ${name}.`); - } - if ( - !isIntrospection && - !hasWarnedAboutDunder && - !noNameWarning && - name.slice(0, 2) === '__' - ) { - hasWarnedAboutDunder = true; - /* eslint-disable no-console */ - if (console && console.warn) { - const error = new Error( - `Name "${name}" must not begin with "__", which is reserved by ` + - 'GraphQL introspection. In a future release of graphql this will ' + - 'become a hard error.', - ); - console.warn(formatWarning(error)); - } - /* eslint-enable no-console */ - } - if (!NAME_RX.test(name)) { - throw new Error( - `Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but "${name}" does not.`, - ); +export function assertValidName(name: string): string { + const error = isValidNameError(name); + if (error) { + throw error; } + return name; } /** - * Returns a human-readable warning based an the supplied Error object, - * including stack trace information if available. + * Returns an Error if a name is invalid. */ -export function formatWarning(error: Error): string { - let formatted = ''; - const errorString = String(error).replace(ERROR_PREFIX_RX, ''); - const stack = error.stack; - if (stack) { - formatted = stack.replace(ERROR_PREFIX_RX, ''); +export function isValidNameError( + name: string, + node?: ASTNode | void, +): GraphQLError | void { + invariant(typeof name === 'string', 'Expected string'); + if (name.length > 1 && name[0] === '_' && name[1] === '_') { + return new GraphQLError( + `Name "${name}" must not begin with "__", which is reserved by ` + + 'GraphQL introspection.', + node, + ); } - if (formatted.indexOf(errorString) === -1) { - formatted = errorString + '\n' + formatted; + if (!NAME_RX.test(name)) { + return new GraphQLError( + `Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but "${name}" does not.`, + node, + ); } - return formatted.trim(); } diff --git a/src/utilities/buildASTSchema.js b/src/utilities/buildASTSchema.js index 7058df8c4c..9000c7173b 100644 --- a/src/utilities/buildASTSchema.js +++ b/src/utilities/buildASTSchema.js @@ -40,17 +40,13 @@ import type { import type { DirectiveLocationEnum } from '../language/directiveLocation'; import { + assertNullableType, GraphQLScalarType, GraphQLObjectType, GraphQLInterfaceType, GraphQLUnionType, GraphQLEnumType, GraphQLInputObjectType, - assertObjectType, - assertInterfaceType, - assertInputType, - assertOutputType, - assertNullableType, } from '../type/definition'; import { GraphQLList, GraphQLNonNull } from '../type/wrappers'; @@ -71,8 +67,6 @@ import { GraphQLSchema } from '../type/schema'; import type { GraphQLType, GraphQLNamedType, - GraphQLInputType, - GraphQLOutputType, GraphQLFieldConfig, } from '../type/definition'; @@ -299,24 +293,6 @@ export class ASTDefinitionBuilder { return this._buildType(ref.name.value, ref); } - _buildInputType(typeNode: TypeNode): GraphQLInputType { - return assertInputType(this._buildWrappedType(typeNode)); - } - - _buildOutputType(typeNode: TypeNode): GraphQLOutputType { - return assertOutputType(this._buildWrappedType(typeNode)); - } - - buildObjectType(ref: string | NamedTypeNode): GraphQLObjectType { - const type = this.buildType(ref); - return assertObjectType(type); - } - - buildInterfaceType(ref: string | NamedTypeNode): GraphQLInterfaceType { - const type = this.buildType(ref); - return assertInterfaceType(type); - } - _buildWrappedType(typeNode: TypeNode): GraphQLType { const typeDef = this.buildType(getNamedTypeNode(typeNode)); return buildWrappedType(typeDef, typeNode); @@ -338,7 +314,10 @@ export class ASTDefinitionBuilder { buildField(field: FieldDefinitionNode): GraphQLFieldConfig<*, *> { return { - type: this._buildOutputType(field.type), + // Note: While this could make assertions to get the correctly typed + // value, that would throw immediately while type system validation + // with validateSchema() will produce more actionable results. + type: (this._buildWrappedType(field.type): any), description: getDescription(field, this._options), args: field.arguments && this._makeInputValues(field.arguments), deprecationReason: getDeprecationReason(field), @@ -403,7 +382,10 @@ export class ASTDefinitionBuilder { values, value => value.name.value, value => { - const type = this._buildInputType(value.type); + // Note: While this could make assertions to get the correctly typed + // value, that would throw immediately while type system validation + // with validateSchema() will produce more actionable results. + const type: any = this._buildWrappedType(value.type); return { type, description: getDescription(value, this._options), @@ -446,7 +428,10 @@ export class ASTDefinitionBuilder { return new GraphQLUnionType({ name: def.name.value, description: getDescription(def, this._options), - types: def.types ? def.types.map(t => this.buildObjectType(t)) : [], + // Note: While this could make assertions to get the correctly typed + // values below, that would throw immediately while type system + // validation with validateSchema() will produce more actionable results. + types: def.types ? def.types.map(t => (this.buildType(t): any)) : [], astNode: def, }); } diff --git a/src/utilities/extendSchema.js b/src/utilities/extendSchema.js index 36e7956458..673999001f 100644 --- a/src/utilities/extendSchema.js +++ b/src/utilities/extendSchema.js @@ -192,19 +192,22 @@ export function extendSchema( ); // Get the root Query, Mutation, and Subscription object types. + // Note: While this could make early assertions to get the correctly + // typed values below, that would throw immediately while type system + // validation with validateSchema() will produce more actionable results. const existingQueryType = schema.getQueryType(); const queryType = existingQueryType - ? definitionBuilder.buildObjectType(existingQueryType.name) + ? (definitionBuilder.buildType(existingQueryType.name): any) : null; const existingMutationType = schema.getMutationType(); const mutationType = existingMutationType - ? definitionBuilder.buildObjectType(existingMutationType.name) + ? (definitionBuilder.buildType(existingMutationType.name): any) : null; const existingSubscriptionType = schema.getSubscriptionType(); const subscriptionType = existingSubscriptionType - ? definitionBuilder.buildObjectType(existingSubscriptionType.name) + ? (definitionBuilder.buildType(existingSubscriptionType.name): any) : null; // Iterate through all types, getting the type definition for each, ensuring @@ -312,15 +315,10 @@ export function extendSchema( if (extensions) { extensions.forEach(extension => { extension.interfaces.forEach(namedType => { - const interfaceName = namedType.name.value; - if (interfaces.some(def => def.name === interfaceName)) { - throw new GraphQLError( - `Type "${type.name}" already implements "${interfaceName}". ` + - 'It cannot also be implemented in this type extension.', - [namedType], - ); - } - interfaces.push(definitionBuilder.buildInterfaceType(namedType)); + // Note: While this could make early assertions to get the correctly + // typed values, that would throw immediately while type system + // validation with validateSchema() will produce more actionable results. + interfaces.push((definitionBuilder.buildType(namedType): any)); }); }); }