diff --git a/src/type/__tests__/definition.js b/src/type/__tests__/definition.js index 2a2f57879d..e503f657c3 100644 --- a/src/type/__tests__/definition.js +++ b/src/type/__tests__/definition.js @@ -87,7 +87,7 @@ var ObjectType = new GraphQLObjectType({ }); var InterfaceType = new GraphQLInterfaceType({ name: 'Interface' }); var UnionType = new GraphQLUnionType({ name: 'Union', types: [ ObjectType ] }); -var EnumType = new GraphQLEnumType({ name: 'Enum' }); +var EnumType = new GraphQLEnumType({ name: 'Enum', values: { foo: {} } }); var InputObjectType = new GraphQLInputObjectType({ name: 'InputObject' }); describe('Type System: Example', () => { diff --git a/src/type/__tests__/validation.js b/src/type/__tests__/validation.js index b30fe5b5af..ca9c5c8675 100644 --- a/src/type/__tests__/validation.js +++ b/src/type/__tests__/validation.js @@ -618,6 +618,33 @@ describe('Type System: Input Objects must have fields', () => { }); +describe('Type System: Object types must be assertable', () => { + + it('accepts an Object type with an isTypeOf function', () => { + expect(() => { + schemaWithFieldType(new GraphQLObjectType({ + name: 'AnotherObject', + isTypeOf: () => true, + 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', () => { @@ -669,6 +696,18 @@ describe('Type System: Interface types must be resolvable', () => { }).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.' + ); + }); + it('rejects an Interface type not defining resolveType with implementing type not defining isTypeOf', () => { expect(() => { var InterfaceTypeWithoutResolveType = new GraphQLInterfaceType({ @@ -723,6 +762,18 @@ describe('Type System: Union types must be resolvable', () => { ).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.' + ); + }); + it('rejects a Union type not defining resolveType of Object types not defining isTypeOf', () => { expect(() => schemaWithFieldType(new GraphQLUnionType({ @@ -826,6 +877,97 @@ describe('Type System: Scalar types must be serializable', () => { }); +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.' + ); + }); + + 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.' + ); + }); + + 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 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.' + ); + }); + + it('rejects an Enum type with incorrectly typed value definition', () => { + expect(() => + new GraphQLEnumType({ + name: 'SomeEnum', + values: { + FOO: 10 + } + }) + ).to.throw( + 'SomeEnum.FOO must refer to an object with a "value" key representing ' + + 'an internal value but got: 10.' + ); + }); + +}); + + describe('Type System: Object fields must have output types', () => { function schemaWithObjectFieldOfType(fieldType) { diff --git a/src/type/definition.js b/src/type/definition.js index 4789b90959..7eed7b4dc5 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -9,6 +9,7 @@ */ import invariant from '../jsutils/invariant'; +import isNullish from '../jsutils/isNullish'; import { ENUM } from '../language/kinds'; import type { OperationDefinition, @@ -296,6 +297,12 @@ export class GraphQLObjectType { invariant(config.name, 'Type must be named.'); this.name = config.name; this.description = config.description; + if (config.isTypeOf) { + invariant( + typeof config.isTypeOf === 'function', + `${this} must provide "isTypeOf" as a function.` + ); + } this.isTypeOf = config.isTypeOf; this._typeConfig = config; addImplementationToInterfaces(this); @@ -359,7 +366,7 @@ function defineFieldMap( ): GraphQLFieldDefinitionMap { var fieldMap: any = resolveMaybeThunk(fields); invariant( - typeof fieldMap === 'object' && !Array.isArray(fieldMap), + isPlainObj(fieldMap), `${type} fields must be an object with field names as keys or a ` + `function which returns such an object.` ); @@ -381,7 +388,7 @@ function defineFieldMap( field.args = []; } else { invariant( - typeof field.args === 'object' && !Array.isArray(field.args), + isPlainObj(field.args), `${type}.${fieldName} args must be an object with argument names ` + `as keys.` ); @@ -404,6 +411,10 @@ function defineFieldMap( return fieldMap; } +function isPlainObj(obj) { + return obj && typeof obj === 'object' && !Array.isArray(obj); +} + /** * Update the interfaces to know about this implementation. * This is an rare and unfortunate use of mutation in the type definition @@ -521,6 +532,12 @@ export class GraphQLInterfaceType { invariant(config.name, 'Type must be named.'); this.name = config.name; this.description = config.description; + if (config.resolveType) { + invariant( + typeof config.resolveType === 'function', + `${this} must provide "resolveType" as a function.` + ); + } this.resolveType = config.resolveType; this._typeConfig = config; this._implementations = []; @@ -621,6 +638,12 @@ export class GraphQLUnionType { invariant(config.name, 'Type must be named.'); this.name = config.name; this.description = config.description; + if (config.resolveType) { + invariant( + typeof config.resolveType === 'function', + `${this} must provide "resolveType" as a function.` + ); + } this.resolveType = config.resolveType; invariant( Array.isArray(config.types) && config.types.length > 0, @@ -711,18 +734,19 @@ export class GraphQLEnumType/* */ { description: ?string; _enumConfig: GraphQLEnumTypeConfig/* */; - _values: GraphQLEnumValueDefinitionMap/* */; + _values: Array */>; _valueLookup: Map; - _nameLookup: Map; + _nameLookup: { [valueName: string]: GraphQLEnumValueDefinition }; constructor(config: GraphQLEnumTypeConfig/* */) { this.name = config.name; this.description = config.description; + this._values = defineEnumValues(this, config.values); this._enumConfig = config; } - getValues(): GraphQLEnumValueDefinitionMap/* */ { - return this._values || (this._values = this._defineValueMap()); + getValues(): Array */> { + return this._values; } serialize(value: any/* T */): ?string { @@ -731,7 +755,7 @@ export class GraphQLEnumType/* */ { } parseValue(value: any): ?any/* T */ { - var enumValue = this._getNameLookup().get(value); + var enumValue = this._getNameLookup()[value]; if (enumValue) { return enumValue.value; } @@ -739,31 +763,17 @@ export class GraphQLEnumType/* */ { parseLiteral(valueAST: Value): ?any/* T */ { if (valueAST.kind === ENUM) { - var enumValue = this._getNameLookup().get(valueAST.value); + var enumValue = this._getNameLookup()[valueAST.value]; if (enumValue) { return enumValue.value; } } } - _defineValueMap(): GraphQLEnumValueDefinitionMap/* */ { - var valueMap = (this._enumConfig.values: any); - Object.keys(valueMap).forEach(valueName => { - var value = valueMap[valueName]; - value.name = valueName; - if (!value.hasOwnProperty('value')) { - value.value = valueName; - } - }); - return valueMap; - } - _getValueLookup(): Map { if (!this._valueLookup) { var lookup = new Map(); - var values = this.getValues(); - Object.keys(values).forEach(valueName => { - var value = values[valueName]; + this.getValues().forEach(value => { lookup.set(value.value, value); }); this._valueLookup = lookup; @@ -771,13 +781,11 @@ export class GraphQLEnumType/* */ { return this._valueLookup; } - _getNameLookup(): Map { + _getNameLookup(): { [valueName: string]: GraphQLEnumValueDefinition } { if (!this._nameLookup) { - var lookup = new Map(); - var values = this.getValues(); - Object.keys(values).forEach(valueName => { - var value = values[valueName]; - lookup.set(value.name, value); + var lookup = Object.create(null); + this.getValues().forEach(value => { + lookup[value.name] = value; }); this._nameLookup = lookup; } @@ -789,6 +797,34 @@ export class GraphQLEnumType/* */ { } } +function defineEnumValues( + type: GraphQLEnumType, + valueMap: GraphQLEnumValueConfigMap/* */ +): Array */> { + invariant( + isPlainObj(valueMap), + `${type} values must be an object with value names as keys.` + ); + var valueNames = Object.keys(valueMap); + invariant( + valueNames.length > 0, + `${type} values must be an object with value names as keys.` + ); + return valueNames.map(valueName => { + var value = valueMap[valueName]; + invariant( + isPlainObj(value), + `${type}.${valueName} must refer to an object with a "value" key ` + + `representing an internal value but got: ${value}.` + ); + value.name = valueName; + if (isNullish(value.value)) { + value.value = valueName; + } + return value; + }); +} + export type GraphQLEnumTypeConfig/* */ = { name: string; values: GraphQLEnumValueConfigMap/* */; @@ -805,10 +841,6 @@ export type GraphQLEnumValueConfig/* */ = { description?: ?string; } -export type GraphQLEnumValueDefinitionMap/* */ = { - [valueName: string]: GraphQLEnumValueDefinition/* */; -}; - export type GraphQLEnumValueDefinition/* */ = { name: string; value?: any/* T */; @@ -859,7 +891,7 @@ export class GraphQLInputObjectType { _defineFieldMap(): InputObjectFieldMap { var fieldMap: any = resolveMaybeThunk(this._typeConfig.fields); invariant( - typeof fieldMap === 'object' && !Array.isArray(fieldMap), + isPlainObj(fieldMap), `${this} fields must be an object with field names as keys or a ` + `function which returns such an object.` ); diff --git a/src/type/introspection.js b/src/type/introspection.js index 1f9d93f291..74024faad5 100644 --- a/src/type/introspection.js +++ b/src/type/introspection.js @@ -147,9 +147,7 @@ var __Type = new GraphQLObjectType({ }, resolve(type, { includeDeprecated }) { if (type instanceof GraphQLEnumType) { - var valueMap = type.getValues(); - var values = - Object.keys(valueMap).map(valueName => valueMap[valueName]); + var values = type.getValues(); if (!includeDeprecated) { values = values.filter(value => !value.deprecationReason); } diff --git a/src/utilities/__tests__/buildClientSchema.js b/src/utilities/__tests__/buildClientSchema.js index 8ad8e6572d..5b8baa28d4 100644 --- a/src/utilities/__tests__/buildClientSchema.js +++ b/src/utilities/__tests__/buildClientSchema.js @@ -357,33 +357,23 @@ describe('Type System: build schema from introspection', () => { // Client types do not get server-only values, so `value` mirrors `name`, // rather than using the integers defined in the "server" schema. - expect(clientFoodEnum.getValues()).to.deep.equal({ - DAIRY: { - description: 'Foods that are dairy.', - name: 'DAIRY', - value: 'DAIRY', - }, - FRUITS: { - description: 'Foods that are fruits.', + expect(clientFoodEnum.getValues()).to.deep.equal([ + { description: 'Foods that are vegetables.', + name: 'VEGETABLES', + value: 'VEGETABLES' }, + { description: 'Foods that are fruits.', name: 'FRUITS', - value: 'FRUITS', - }, - MEAT: { - description: 'Foods that are meat.', - name: 'MEAT', - value: 'MEAT', - }, - OILS: { - description: 'Foods that are oils.', + value: 'FRUITS' }, + { description: 'Foods that are oils.', name: 'OILS', - value: 'OILS', - }, - VEGETABLES: { - description: 'Foods that are vegetables.', - name: 'VEGETABLES', - value: 'VEGETABLES', - }, - }); + value: 'OILS' }, + { description: 'Foods that are dairy.', + name: 'DAIRY', + value: 'DAIRY' }, + { description: 'Foods that are meat.', + name: 'MEAT', + value: 'MEAT' }, + ]); }); it('builds a schema with an input object', async () => { diff --git a/src/utilities/schemaPrinter.js b/src/utilities/schemaPrinter.js index 5290bd9b77..3b26c8ac09 100644 --- a/src/utilities/schemaPrinter.js +++ b/src/utilities/schemaPrinter.js @@ -102,8 +102,7 @@ function printUnion(type: GraphQLUnionType): string { } function printEnum(type: GraphQLEnumType): string { - var valueMap = type.getValues(); - var values = Object.keys(valueMap).map(valueName => valueMap[valueName]); + var values = type.getValues(); return `enum ${type.name} {\n` + values.map(v => ' ' + v.name).join('\n') + '\n' + '}';