From 6a1f23e1f9c1e6bf4cea837bc9bb6eae0fb5c382 Mon Sep 17 00:00:00 2001 From: Lee Byron Date: Tue, 22 Mar 2016 22:17:43 -0700 Subject: [PATCH] Add GraphQLSchema types field This is a rebased and updated version of @tgriesser's #199. I further extended it to completely remove the side-effectful mutation of Interface types rather than just deferring that mutation to schema creation time. This introduces a *breaking* change to the Type System API. Now, any individual Interface type does not have the required information to answer `getPossibleTypes` or `isPossibleType` without knowing the other types in the Schema. These methods were moved to the Schema API, accepting the abstract type as the first parameter. This also introduces a *breaking* change to the type comparator functions: `isTypeSubTypeOf` and `doTypesOverlap` which now require a Schema as a first argument. --- src/__tests__/starWarsIntrospectionTests.js | 4 +- src/__tests__/starWarsSchema.js | 3 +- src/execution/__tests__/abstract.js | 11 +-- src/execution/__tests__/union-interface.js | 5 +- src/execution/execute.js | 8 +- src/type/__tests__/definition.js | 6 +- src/type/__tests__/validation.js | 17 ++-- src/type/definition.js | 42 +--------- src/type/introspection.js | 18 ++-- src/type/schema.js | 82 +++++++++++++++++-- src/utilities/__tests__/buildClientSchema.js | 9 +- src/utilities/__tests__/extendSchema.js | 63 ++++++++++++-- src/utilities/__tests__/schemaPrinter.js | 10 ++- src/utilities/buildASTSchema.js | 24 +++++- src/utilities/buildClientSchema.js | 22 ++++- src/utilities/extendSchema.js | 39 +++++++-- src/utilities/schemaPrinter.js | 2 +- src/utilities/typeComparators.js | 24 ++++-- .../__tests__/OverlappingFieldsCanBeMerged.js | 5 +- src/validation/__tests__/harness.js | 1 + src/validation/rules/FieldsOnCorrectType.js | 22 +++-- .../rules/PossibleFragmentSpreads.js | 8 +- .../rules/VariablesInAllowedPosition.js | 9 +- 23 files changed, 297 insertions(+), 137 deletions(-) diff --git a/src/__tests__/starWarsIntrospectionTests.js b/src/__tests__/starWarsIntrospectionTests.js index 68f3734b76..6646825e34 100644 --- a/src/__tests__/starWarsIntrospectionTests.js +++ b/src/__tests__/starWarsIntrospectionTests.js @@ -40,10 +40,10 @@ describe('Star Wars Introspection Tests', () => { name: 'Character' }, { - name: 'Human' + name: 'String' }, { - name: 'String' + name: 'Human' }, { name: 'Droid' diff --git a/src/__tests__/starWarsSchema.js b/src/__tests__/starWarsSchema.js index 5a377b76a0..e315986a66 100644 --- a/src/__tests__/starWarsSchema.js +++ b/src/__tests__/starWarsSchema.js @@ -270,5 +270,6 @@ const queryType = new GraphQLObjectType({ * type we defined above) and export it. */ export const StarWarsSchema = new GraphQLSchema({ - query: queryType + query: queryType, + types: [ humanType, droidType ] }); diff --git a/src/execution/__tests__/abstract.js b/src/execution/__tests__/abstract.js index f2a944b900..e839a580f2 100644 --- a/src/execution/__tests__/abstract.js +++ b/src/execution/__tests__/abstract.js @@ -51,9 +51,6 @@ describe('Execute: Handles execution of abstract types', () => { } }); - // Added to interface type when defined - /* eslint-disable no-unused-vars */ - const DogType = new GraphQLObjectType({ name: 'Dog', interfaces: [ PetType ], @@ -74,8 +71,6 @@ describe('Execute: Handles execution of abstract types', () => { } }); - /* eslint-enable no-unused-vars */ - const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'Query', @@ -87,7 +82,8 @@ describe('Execute: Handles execution of abstract types', () => { } } } - }) + }), + types: [ CatType, DogType ] }); const query = `{ @@ -231,7 +227,8 @@ describe('Execute: Handles execution of abstract types', () => { } } } - }) + }), + types: [ CatType, DogType ] }); const query = `{ diff --git a/src/execution/__tests__/union-interface.js b/src/execution/__tests__/union-interface.js index 0d3b205d2b..0156e20c73 100644 --- a/src/execution/__tests__/union-interface.js +++ b/src/execution/__tests__/union-interface.js @@ -97,7 +97,8 @@ const PersonType = new GraphQLObjectType({ }); const schema = new GraphQLSchema({ - query: PersonType + query: PersonType, + types: [ PetType ] }); const garfield = new Cat('Garfield', false); @@ -143,9 +144,9 @@ describe('Execute: Union and intersection types', () => { ], interfaces: null, possibleTypes: [ + { name: 'Person' }, { name: 'Dog' }, { name: 'Cat' }, - { name: 'Person' } ], enumValues: null, inputFields: null diff --git a/src/execution/execute.js b/src/execution/execute.js index eb9419c3e8..3d20d1be03 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -468,7 +468,8 @@ function doesFragmentConditionMatch( return true; } if (isAbstractType(conditionalType)) { - return ((conditionalType: any): GraphQLAbstractType).isPossibleType(type); + const abstractType = ((conditionalType: any): GraphQLAbstractType); + return exeContext.schema.isPossibleType(abstractType, type); } return false; } @@ -806,7 +807,8 @@ function completeAbstractValue( return null; } - if (runtimeType && !returnType.isPossibleType(runtimeType)) { + const schema = exeContext.schema; + if (runtimeType && !schema.isPossibleType(returnType, runtimeType)) { throw new GraphQLError( `Runtime Object type "${runtimeType}" is not a possible type ` + `for "${returnType}".`, @@ -872,7 +874,7 @@ function defaultResolveTypeFn( info: GraphQLResolveInfo, abstractType: GraphQLAbstractType ): ?GraphQLObjectType { - const possibleTypes = abstractType.getPossibleTypes(); + const possibleTypes = info.schema.getPossibleTypes(abstractType); for (let i = 0; i < possibleTypes.length; i++) { const type = possibleTypes[i]; if (typeof type.isTypeOf === 'function' && type.isTypeOf(value, info)) { diff --git a/src/type/__tests__/definition.js b/src/type/__tests__/definition.js index b08af45cf1..4e4ba43a45 100644 --- a/src/type/__tests__/definition.js +++ b/src/type/__tests__/definition.js @@ -227,7 +227,8 @@ describe('Type System: Example', () => { fields: { iface: { type: SomeInterface } } - }) + }), + types: [ SomeSubtype ] }); expect(schema.getTypeMap().SomeSubtype).to.equal(SomeSubtype); @@ -256,7 +257,8 @@ describe('Type System: Example', () => { fields: { iface: { type: SomeInterface } } - }) + }), + types: [ SomeSubtype ] }); expect(schema.getTypeMap().SomeSubtype).to.equal(SomeSubtype); diff --git a/src/type/__tests__/validation.js b/src/type/__tests__/validation.js index 06a476c85e..c73bed3999 100644 --- a/src/type/__tests__/validation.js +++ b/src/type/__tests__/validation.js @@ -108,7 +108,8 @@ function schemaWithFieldType(type) { query: new GraphQLObjectType({ name: 'Query', fields: { f: { type } } - }) + }), + types: [ type ], }); } @@ -261,24 +262,18 @@ describe('Type System: A Schema must contain uniquely named types', () => { fields: { f: { type: GraphQLString } }, }); - /* eslint-disable no-unused-vars */ - - // Automatically included in Interface const FirstBadObject = new GraphQLObjectType({ name: 'BadObject', interfaces: [ AnotherInterface ], fields: { f: { type: GraphQLString } }, }); - // Automatically included in Interface const SecondBadObject = new GraphQLObjectType({ name: 'BadObject', interfaces: [ AnotherInterface ], fields: { f: { type: GraphQLString } }, }); - /* eslint-enable no-unused-vars */ - const QueryType = new GraphQLObjectType({ name: 'Query', fields: { @@ -286,7 +281,10 @@ describe('Type System: A Schema must contain uniquely named types', () => { } }); - return new GraphQLSchema({ query: QueryType }); + return new GraphQLSchema({ + query: QueryType, + types: [ FirstBadObject, SecondBadObject ] + }); }).to.throw( 'Schema must contain unique named types but contains multiple types ' + 'named "BadObject".' @@ -1114,7 +1112,8 @@ describe('Type System: Objects can only implement interfaces', () => { fields: { f: { type: BadObjectType } } - }) + }), + types: [ BadObjectType ] }); } diff --git a/src/type/definition.js b/src/type/definition.js index 9d76874d5b..64dcb952b1 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -10,7 +10,6 @@ import invariant from '../jsutils/invariant'; import isNullish from '../jsutils/isNullish'; -import keyMap from '../jsutils/keyMap'; import { ENUM } from '../language/kinds'; import { assertValidName } from '../utilities/assertValidName'; import type { @@ -321,7 +320,6 @@ export class GraphQLObjectType { } this.isTypeOf = config.isTypeOf; this._typeConfig = config; - addImplementationToInterfaces(this); } getFields(): GraphQLFieldDefinitionMap { @@ -444,18 +442,6 @@ 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 - * implementations, but avoids an expensive "getPossibleTypes" - * implementation for Interface types. - */ -function addImplementationToInterfaces(impl) { - impl.getInterfaces().forEach(type => { - type._implementations.push(impl); - }); -} - export type GraphQLObjectTypeConfig = { name: string; interfaces?: GraphQLInterfacesThunk | Array; @@ -565,8 +551,6 @@ export class GraphQLInterfaceType { _typeConfig: GraphQLInterfaceTypeConfig; _fields: GraphQLFieldDefinitionMap; - _implementations: Array; - _possibleTypes: { [typeName: string]: GraphQLObjectType }; constructor(config: GraphQLInterfaceTypeConfig) { invariant(config.name, 'Type must be named.'); @@ -581,7 +565,6 @@ export class GraphQLInterfaceType { } this.resolveType = config.resolveType; this._typeConfig = config; - this._implementations = []; } getFields(): GraphQLFieldDefinitionMap { @@ -589,17 +572,6 @@ export class GraphQLInterfaceType { (this._fields = defineFieldMap(this, this._typeConfig.fields)); } - getPossibleTypes(): Array { - return this._implementations; - } - - isPossibleType(type: GraphQLObjectType): boolean { - const possibleTypes = this._possibleTypes || (this._possibleTypes = - keyMap(this.getPossibleTypes(), possibleType => possibleType.name) - ); - return Boolean(possibleTypes[type.name]); - } - toString(): string { return this.name; } @@ -686,22 +658,10 @@ export class GraphQLUnionType { this._typeConfig = config; } - getPossibleTypes(): Array { + getTypes(): Array { return this._types; } - isPossibleType(type: GraphQLObjectType): boolean { - let possibleTypeNames = this._possibleTypeNames; - if (!possibleTypeNames) { - this._possibleTypeNames = possibleTypeNames = - this.getPossibleTypes().reduce( - (map, possibleType) => ((map[possibleType.name] = true), map), - {} - ); - } - return possibleTypeNames[type.name] === true; - } - toString(): string { return this.name; } diff --git a/src/type/introspection.js b/src/type/introspection.js index 4d067e8f90..61c7b1cb82 100644 --- a/src/type/introspection.js +++ b/src/type/introspection.js @@ -67,7 +67,7 @@ export const __Schema = new GraphQLObjectType({ }) }); -const __Directive = new GraphQLObjectType({ +export const __Directive = new GraphQLObjectType({ name: '__Directive', description: 'A Directive provides a way to describe alternate runtime execution and ' + @@ -115,7 +115,7 @@ const __Directive = new GraphQLObjectType({ }), }); -const __DirectiveLocation = new GraphQLEnumType({ +export const __DirectiveLocation = new GraphQLEnumType({ name: '__DirectiveLocation', description: 'A Directive can be adjacent to many parts of the GraphQL language, a ' + @@ -152,7 +152,7 @@ const __DirectiveLocation = new GraphQLEnumType({ } }); -const __Type = new GraphQLObjectType({ +export const __Type = new GraphQLObjectType({ name: '__Type', description: 'The fundamental unit of any GraphQL Schema is the type. There are ' + @@ -218,10 +218,10 @@ const __Type = new GraphQLObjectType({ }, possibleTypes: { type: new GraphQLList(new GraphQLNonNull(__Type)), - resolve(type) { + resolve(type, args, { schema }) { if (type instanceof GraphQLInterfaceType || type instanceof GraphQLUnionType) { - return type.getPossibleTypes(); + return schema.getPossibleTypes(type); } } }, @@ -253,7 +253,7 @@ const __Type = new GraphQLObjectType({ }) }); -const __Field = new GraphQLObjectType({ +export const __Field = new GraphQLObjectType({ name: '__Field', description: 'Object and Interface types are described by a list of Fields, each of ' + @@ -277,7 +277,7 @@ const __Field = new GraphQLObjectType({ }) }); -const __InputValue = new GraphQLObjectType({ +export const __InputValue = new GraphQLObjectType({ name: '__InputValue', description: 'Arguments provided to Fields or Directives and the input fields of an ' + @@ -299,7 +299,7 @@ const __InputValue = new GraphQLObjectType({ }) }); -const __EnumValue = new GraphQLObjectType({ +export const __EnumValue = new GraphQLObjectType({ name: '__EnumValue', description: 'One possible value for a given Enum. Enum values are unique values, not ' + @@ -329,7 +329,7 @@ export const TypeKind = { NON_NULL: 'NON_NULL', }; -const __TypeKind = new GraphQLEnumType({ +export const __TypeKind = new GraphQLEnumType({ name: '__TypeKind', description: 'An enum describing what kind of type a given `__Type` is.', values: { diff --git a/src/type/schema.js b/src/type/schema.js index dc6f2f93c2..7075040dfc 100644 --- a/src/type/schema.js +++ b/src/type/schema.js @@ -9,6 +9,7 @@ */ import { + GraphQLAbstractType, GraphQLObjectType, GraphQLInputObjectType, GraphQLInterfaceType, @@ -49,6 +50,9 @@ export class GraphQLSchema { _subscriptionType: ?GraphQLObjectType; _directives: Array; _typeMap: TypeMap; + _implementations: { [interfaceName: string]: Array }; + _possibleTypeMap: + ?{ [abstractName: string]: { [possibleName: string]: boolean } }; constructor(config: GraphQLSchemaConfig) { invariant( @@ -76,6 +80,11 @@ export class GraphQLSchema { ); this._subscriptionType = config.subscription; + invariant( + !config.types || Array.isArray(config.types), + `Schema types must be Array if provided but got: ${config.types}.` + ); + invariant( !config.directives || Array.isArray(config.directives) && config.directives.every( @@ -91,19 +100,45 @@ export class GraphQLSchema { ]; // Build type map now to detect any errors within this schema. - this._typeMap = [ + let initialTypes: Array = [ this.getQueryType(), this.getMutationType(), this.getSubscriptionType(), __Schema - ].reduce(typeMapReducer, {}); + ]; + + const types = config.types; + if (types) { + initialTypes = initialTypes.concat(types); + } - // Enforce correct interface implementations + this._typeMap = initialTypes.reduce( + typeMapReducer, + (Object.create(null): TypeMap) + ); + + // Keep track of all implementations by interface name. + this._implementations = Object.create(null); + Object.keys(this._typeMap).forEach(typeName => { + const type = this._typeMap[typeName]; + if (type instanceof GraphQLObjectType) { + type.getInterfaces().forEach(iface => { + const impls = this._implementations[iface.name]; + if (impls) { + impls.push(type); + } else { + this._implementations[iface.name] = [ type ]; + } + }); + } + }); + + // Enforce correct interface implementations. Object.keys(this._typeMap).forEach(typeName => { const type = this._typeMap[typeName]; if (type instanceof GraphQLObjectType) { type.getInterfaces().forEach( - iface => assertObjectImplementsInterface(type, iface) + iface => assertObjectImplementsInterface(this, type, iface) ); } }); @@ -129,6 +164,36 @@ export class GraphQLSchema { return this.getTypeMap()[name]; } + getPossibleTypes( + abstractType: GraphQLAbstractType + ): Array { + if (abstractType instanceof GraphQLUnionType) { + return abstractType.getTypes(); + } + invariant(abstractType instanceof GraphQLInterfaceType); + return this._implementations[abstractType.name]; + } + + isPossibleType( + abstractType: GraphQLAbstractType, + possibleType: GraphQLObjectType + ): boolean { + let possibleTypeMap = this._possibleTypeMap; + if (!possibleTypeMap) { + this._possibleTypeMap = possibleTypeMap = Object.create(null); + } + + if (!possibleTypeMap[abstractType.name]) { + possibleTypeMap[abstractType.name] = + this.getPossibleTypes(abstractType).reduce( + (map, type) => ((map[type.name] = true), map), + Object.create(null) + ); + } + + return Boolean(possibleTypeMap[abstractType.name][possibleType.name]); + } + getDirectives(): Array { return this._directives; } @@ -144,6 +209,7 @@ type GraphQLSchemaConfig = { query: GraphQLObjectType; mutation?: ?GraphQLObjectType; subscription?: ?GraphQLObjectType; + types?: ?Array; directives?: ?Array; } @@ -166,9 +232,8 @@ function typeMapReducer(map: TypeMap, type: ?GraphQLType): TypeMap { let reducedMap = map; - if (type instanceof GraphQLUnionType || - type instanceof GraphQLInterfaceType) { - reducedMap = type.getPossibleTypes().reduce(typeMapReducer, reducedMap); + if (type instanceof GraphQLUnionType) { + reducedMap = type.getTypes().reduce(typeMapReducer, reducedMap); } if (type instanceof GraphQLObjectType) { @@ -194,6 +259,7 @@ function typeMapReducer(map: TypeMap, type: ?GraphQLType): TypeMap { } function assertObjectImplementsInterface( + schema: GraphQLSchema, object: GraphQLObjectType, iface: GraphQLInterfaceType ): void { @@ -215,7 +281,7 @@ function assertObjectImplementsInterface( // Assert interface field type is satisfied by object field type, by being // a valid subtype. (covariant) invariant( - isTypeSubTypeOf(objectField.type, ifaceField.type), + isTypeSubTypeOf(schema, objectField.type, ifaceField.type), `${iface}.${fieldName} expects type "${ifaceField.type}" but ` + `${object}.${fieldName} provides type "${objectField.type}".` ); diff --git a/src/utilities/__tests__/buildClientSchema.js b/src/utilities/__tests__/buildClientSchema.js index 6e4cb00730..fcfb49e705 100644 --- a/src/utilities/__tests__/buildClientSchema.js +++ b/src/utilities/__tests__/buildClientSchema.js @@ -195,29 +195,28 @@ describe('Type System: build schema from introspection', () => { } }) }); - /* eslint-disable no-new */ - new GraphQLObjectType({ + const dogType = new GraphQLObjectType({ name: 'Dog', interfaces: [ friendlyType ], fields: () => ({ bestFriend: { type: friendlyType } }) }); - new GraphQLObjectType({ + const humanType = new GraphQLObjectType({ name: 'Human', interfaces: [ friendlyType ], fields: () => ({ bestFriend: { type: friendlyType } }) }); - /* eslint-enable no-new */ const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'WithInterface', fields: { friendly: { type: friendlyType } } - }) + }), + types: [ dogType, humanType ] }); await testSchema(schema); diff --git a/src/utilities/__tests__/extendSchema.js b/src/utilities/__tests__/extendSchema.js index 301c8590ae..df0ed398df 100644 --- a/src/utilities/__tests__/extendSchema.js +++ b/src/utilities/__tests__/extendSchema.js @@ -46,7 +46,6 @@ const FooType = new GraphQLObjectType({ }) }); -/* eslint-disable no-unused-vars */ const BarType = new GraphQLObjectType({ name: 'Bar', interfaces: [ SomeInterfaceType ], @@ -56,7 +55,6 @@ const BarType = new GraphQLObjectType({ foo: { type: FooType }, }) }); -/* eslint-enable no-unused-vars */ const BizType = new GraphQLObjectType({ name: 'Biz', @@ -91,7 +89,8 @@ const testSchema = new GraphQLSchema({ type: SomeInterfaceType }, }) - }) + }), + types: [ FooType, BarType ] }); describe('extendSchema', () => { @@ -181,6 +180,58 @@ union SomeUnion = Foo | Biz `); }); + it('extends objects by adding new unused types', () => { + const ast = parse(` + type Unused { + someField: String + } + `); + const originalPrint = printSchema(testSchema); + const extendedSchema = extendSchema(testSchema, ast); + expect(extendedSchema).to.not.equal(testSchema); + expect(printSchema(testSchema)).to.equal(originalPrint); + expect(printSchema(extendedSchema)).to.equal( +`type Bar implements SomeInterface { + name: String + some: SomeInterface + foo: Foo +} + +type Biz { + fizz: String +} + +type Foo implements SomeInterface { + name: String + some: SomeInterface + tree: [Foo]! +} + +type Query { + foo: Foo + someUnion: SomeUnion + someEnum: SomeEnum + someInterface(id: ID!): SomeInterface +} + +enum SomeEnum { + ONE + TWO +} + +interface SomeInterface { + name: String + some: SomeInterface +} + +union SomeUnion = Foo | Biz + +type Unused { + someField: String +} +`); + }); + it('extends objects by adding new fields with arguments', () => { const ast = parse(` extend type Foo { @@ -247,12 +298,6 @@ union SomeUnion = Foo | Biz extend type Foo { newField(arg1: SomeEnum!): SomeEnum } - - input NewInputObj { - field1: Int - field2: [Float] - field3: String! - } `); const originalPrint = printSchema(testSchema); const extendedSchema = extendSchema(testSchema, ast); diff --git a/src/utilities/__tests__/schemaPrinter.js b/src/utilities/__tests__/schemaPrinter.js index de64fbd1ff..8a8295e378 100644 --- a/src/utilities/__tests__/schemaPrinter.js +++ b/src/utilities/__tests__/schemaPrinter.js @@ -282,7 +282,10 @@ type Root { fields: { bar: { type: BarType } }, }); - const Schema = new GraphQLSchema({ query: Root }); + const Schema = new GraphQLSchema({ + query: Root, + types: [ BarType ] + }); const output = printForTest(Schema); expect(output).to.equal(` type Bar implements Foo { @@ -327,7 +330,10 @@ type Root { fields: { bar: { type: BarType } }, }); - const Schema = new GraphQLSchema({ query: Root }); + const Schema = new GraphQLSchema({ + query: Root, + types: [ BarType ] + }); const output = printForTest(Schema); expect(output).to.equal(` interface Baaz { diff --git a/src/utilities/buildASTSchema.js b/src/utilities/buildASTSchema.js index 0d309c4a9a..b1178209d0 100644 --- a/src/utilities/buildASTSchema.js +++ b/src/utilities/buildASTSchema.js @@ -62,6 +62,17 @@ import { import { GraphQLDirective } from '../type/directives'; +import { + __Schema, + __Directive, + __DirectiveLocation, + __Type, + __Field, + __InputValue, + __EnumValue, + __TypeKind, +} from '../type/introspection'; + import type { GraphQLType, GraphQLNamedType @@ -165,18 +176,27 @@ export function buildASTSchema( Float: GraphQLFloat, Boolean: GraphQLBoolean, ID: GraphQLID, + __Schema, + __Directive, + __DirectiveLocation, + __Type, + __Field, + __InputValue, + __EnumValue, + __TypeKind, }; - typeDefs.forEach(def => typeDefNamed(def.name.value)); + const types = typeDefs.map(def => typeDefNamed(def.name.value)); const directives = directiveDefs.map(getDirective); return new GraphQLSchema({ - directives, query: getObjectType(astMap[queryTypeName]), mutation: mutationTypeName ? getObjectType(astMap[mutationTypeName]) : null, subscription: subscriptionTypeName ? getObjectType(astMap[subscriptionTypeName]) : null, + types, + directives, }); function getDirective(directiveAST: DirectiveDefinition): GraphQLDirective { diff --git a/src/utilities/buildClientSchema.js b/src/utilities/buildClientSchema.js index d67f1ae003..1161f1fea5 100644 --- a/src/utilities/buildClientSchema.js +++ b/src/utilities/buildClientSchema.js @@ -28,6 +28,17 @@ import { GraphQLNonNull, } from '../type/definition'; +import { + __Schema, + __Directive, + __DirectiveLocation, + __Type, + __Field, + __InputValue, + __EnumValue, + __TypeKind, +} from '../type/introspection'; + import { GraphQLInt, GraphQLFloat, @@ -93,6 +104,14 @@ export function buildClientSchema( Float: GraphQLFloat, Boolean: GraphQLBoolean, ID: GraphQLID, + __Schema, + __Directive, + __DirectiveLocation, + __Type, + __Field, + __InputValue, + __EnumValue, + __TypeKind, }; // Given a type reference in introspection, return the GraphQLType instance. @@ -342,7 +361,7 @@ export function buildClientSchema( // Iterate through all types, getting the type definition for each, ensuring // that any type not directly referenced by a field will get created. - schemaIntrospection.types.forEach( + const types = schemaIntrospection.types.map( typeIntrospection => getNamedType(typeIntrospection.name) ); @@ -368,6 +387,7 @@ export function buildClientSchema( query: queryType, mutation: mutationType, subscription: subscriptionType, + types, directives, }); } diff --git a/src/utilities/extendSchema.js b/src/utilities/extendSchema.js index b22dc4cdee..2a7f57a75d 100644 --- a/src/utilities/extendSchema.js +++ b/src/utilities/extendSchema.js @@ -26,6 +26,17 @@ import { GraphQLInputObjectType, } from '../type/definition'; +import { + __Schema, + __Directive, + __DirectiveLocation, + __Type, + __Field, + __InputValue, + __EnumValue, + __TypeKind, +} from '../type/introspection'; + import { GraphQLString, GraphQLInt, @@ -155,14 +166,23 @@ export function extendSchema( } // A cache to use to store the actual GraphQLType definition objects by name. - // Initialize to the GraphQL built in scalars. All functions below are inline - // so that this type def cache is within the scope of the closure. + // Initialize to the GraphQL built in scalars and introspection types. All + // functions below are inline so that this type def cache is within the scope + // of the closure. const typeDefCache = { String: GraphQLString, Int: GraphQLInt, Float: GraphQLFloat, Boolean: GraphQLBoolean, ID: GraphQLID, + __Schema, + __Directive, + __DirectiveLocation, + __Type, + __Field, + __InputValue, + __EnumValue, + __TypeKind, }; // Get the root Query, Mutation, and Subscription types. @@ -180,20 +200,21 @@ export function extendSchema( // Iterate through all types, getting the type definition for each, ensuring // that any type not directly referenced by a field will get created. - Object.keys(schema.getTypeMap()).forEach( - typeName => getTypeFromDef(schema.getType(typeName)) + const types = Object.keys(schema.getTypeMap()).map(typeName => + getTypeFromDef(schema.getType(typeName)) ); - // Do the same with new types. - Object.keys(typeDefinitionMap).forEach( - typeName => getTypeFromAST(typeDefinitionMap[typeName]) - ); + // Do the same with new types, appending to the list of defined types. + Object.keys(typeDefinitionMap).forEach(typeName => { + types.push(getTypeFromAST(typeDefinitionMap[typeName])); + }); // Then produce and return a Schema with these types. return new GraphQLSchema({ query: queryType, mutation: mutationType, subscription: subscriptionType, + types, // Copy directives. directives: schema.getDirectives(), }); @@ -281,7 +302,7 @@ export function extendSchema( return new GraphQLUnionType({ name: type.name, description: type.description, - types: type.getPossibleTypes().map(getTypeFromDef), + types: type.getTypes().map(getTypeFromDef), resolveType: cannotExecuteClientSchema, }); } diff --git a/src/utilities/schemaPrinter.js b/src/utilities/schemaPrinter.js index 3c36eddcf8..45d4afe6bb 100644 --- a/src/utilities/schemaPrinter.js +++ b/src/utilities/schemaPrinter.js @@ -107,7 +107,7 @@ function printInterface(type: GraphQLInterfaceType): string { } function printUnion(type: GraphQLUnionType): string { - return `union ${type.name} = ${type.getPossibleTypes().join(' | ')}`; + return `union ${type.name} = ${type.getTypes().join(' | ')}`; } function printEnum(type: GraphQLEnumType): string { diff --git a/src/utilities/typeComparators.js b/src/utilities/typeComparators.js index 3ad7178f87..bfd563ab57 100644 --- a/src/utilities/typeComparators.js +++ b/src/utilities/typeComparators.js @@ -21,6 +21,9 @@ import type { GraphQLCompositeType, GraphQLAbstractType } from '../type/definition'; +import type { + GraphQLSchema +} from '../type/schema'; /** @@ -51,6 +54,7 @@ export function isEqualType(typeA: GraphQLType, typeB: GraphQLType): boolean { * equal or a subset of the second super type (covariant). */ export function isTypeSubTypeOf( + schema: GraphQLSchema, maybeSubType: GraphQLType, superType: GraphQLType ): boolean { @@ -62,18 +66,18 @@ export function isTypeSubTypeOf( // If superType is non-null, maybeSubType must also be nullable. if (superType instanceof GraphQLNonNull) { if (maybeSubType instanceof GraphQLNonNull) { - return isTypeSubTypeOf(maybeSubType.ofType, superType.ofType); + return isTypeSubTypeOf(schema, maybeSubType.ofType, superType.ofType); } return false; } else if (maybeSubType instanceof GraphQLNonNull) { // If superType is nullable, maybeSubType may be non-null. - return isTypeSubTypeOf(maybeSubType.ofType, superType); + return isTypeSubTypeOf(schema, maybeSubType.ofType, superType); } // If superType type is a list, maybeSubType type must also be a list. if (superType instanceof GraphQLList) { if (maybeSubType instanceof GraphQLList) { - return isTypeSubTypeOf(maybeSubType.ofType, superType.ofType); + return isTypeSubTypeOf(schema, maybeSubType.ofType, superType.ofType); } return false; } else if (maybeSubType instanceof GraphQLList) { @@ -85,7 +89,10 @@ export function isTypeSubTypeOf( // possible object type. if (isAbstractType(superType) && maybeSubType instanceof GraphQLObjectType && - ((superType: any): GraphQLAbstractType).isPossibleType(maybeSubType)) { + schema.isPossibleType( + ((superType: any): GraphQLAbstractType), + maybeSubType + )) { return true; } @@ -103,6 +110,7 @@ export function isTypeSubTypeOf( * This function is commutative. */ export function doTypesOverlap( + schema: GraphQLSchema, typeA: GraphQLCompositeType, typeB: GraphQLCompositeType ): boolean { @@ -120,16 +128,18 @@ export function doTypesOverlap( _typeB instanceof GraphQLUnionType) { // If both types are abstract, then determine if there is any intersection // between possible concrete types of each. - return typeA.getPossibleTypes().some(type => _typeB.isPossibleType(type)); + return schema.getPossibleTypes(typeA).some( + type => schema.isPossibleType(_typeB, type) + ); } // Determine if the latter type is a possible concrete type of the former. - return typeA.isPossibleType(_typeB); + return schema.isPossibleType(typeA, _typeB); } if (_typeB instanceof GraphQLInterfaceType || _typeB instanceof GraphQLUnionType) { // Determine if the former type is a possible concrete type of the latter. - return _typeB.isPossibleType(typeA); + return schema.isPossibleType(_typeB, typeA); } // Otherwise the types do not overlap. diff --git a/src/validation/__tests__/OverlappingFieldsCanBeMerged.js b/src/validation/__tests__/OverlappingFieldsCanBeMerged.js index 6c4fc66894..a704f1956b 100644 --- a/src/validation/__tests__/OverlappingFieldsCanBeMerged.js +++ b/src/validation/__tests__/OverlappingFieldsCanBeMerged.js @@ -373,7 +373,6 @@ describe('Validate: Overlapping fields can be merged', () => { } }); - /* eslint-disable no-unused-vars */ const StringBox = new GraphQLObjectType({ name: 'StringBox', interfaces: [ SomeBox ], @@ -425,7 +424,6 @@ describe('Validate: Overlapping fields can be merged', () => { unrelatedField: { type: GraphQLString }, } }); - /* eslint-enable no-unused-vars */ const Connection = new GraphQLObjectType({ name: 'Connection', @@ -456,7 +454,8 @@ describe('Validate: Overlapping fields can be merged', () => { someBox: { type: SomeBox }, connection: { type: Connection } }) - }) + }), + types: [ IntBox, NonNullStringBox1Impl, NonNullStringBox2Impl ] }); it('conflicting return types which potentially overlap', () => { diff --git a/src/validation/__tests__/harness.js b/src/validation/__tests__/harness.js index a4d2812591..b9491e9b70 100644 --- a/src/validation/__tests__/harness.js +++ b/src/validation/__tests__/harness.js @@ -304,6 +304,7 @@ const QueryRoot = new GraphQLObjectType({ export const testSchema = new GraphQLSchema({ query: QueryRoot, + types: [ Cat, Dog, Human, Alien ], directives: [ new GraphQLDirective({ name: 'operationOnly', diff --git a/src/validation/rules/FieldsOnCorrectType.js b/src/validation/rules/FieldsOnCorrectType.js index 2fe05d4225..f2962afa68 100644 --- a/src/validation/rules/FieldsOnCorrectType.js +++ b/src/validation/rules/FieldsOnCorrectType.js @@ -11,8 +11,9 @@ import type { ValidationContext } from '../index'; import { GraphQLError } from '../../error'; import type { Field } from '../../language/ast'; +import type { GraphQLSchema } from '../../type/schema'; import type { GraphQLAbstractType } from '../../type/definition'; -import { isAbstractType, GraphQLObjectType } from '../../type/definition'; +import { isAbstractType } from '../../type/definition'; export function undefinedFieldMessage( @@ -52,10 +53,14 @@ export function FieldsOnCorrectType(context: ValidationContext): any { // This isn't valid. Let's find suggestions, if any. let suggestedTypes = []; if (isAbstractType(type)) { - suggestedTypes = - getSiblingInterfacesIncludingField(type, node.name.value); + const schema = context.getSchema(); + suggestedTypes = getSiblingInterfacesIncludingField( + schema, + type, + node.name.value + ); suggestedTypes = suggestedTypes.concat( - getImplementationsIncludingField(type, node.name.value) + getImplementationsIncludingField(schema, type, node.name.value) ); } context.reportError(new GraphQLError( @@ -72,10 +77,11 @@ export function FieldsOnCorrectType(context: ValidationContext): any { * Return implementations of `type` that include `fieldName` as a valid field. */ function getImplementationsIncludingField( + schema: GraphQLSchema, type: GraphQLAbstractType, fieldName: string ): Array { - return type.getPossibleTypes() + return schema.getPossibleTypes(type) .filter(t => t.getFields()[fieldName] !== undefined) .map(t => t.name) .sort(); @@ -88,13 +94,11 @@ function getImplementationsIncludingField( * interface. */ function getSiblingInterfacesIncludingField( + schema: GraphQLSchema, type: GraphQLAbstractType, fieldName: string ): Array { - const implementingObjects = type.getPossibleTypes() - .filter(t => t instanceof GraphQLObjectType); - - const suggestedInterfaces = implementingObjects.reduce((acc, t) => { + const suggestedInterfaces = schema.getPossibleTypes(type).reduce((acc, t) => { t.getInterfaces().forEach(i => { if (i.getFields()[fieldName] === undefined) { return; diff --git a/src/validation/rules/PossibleFragmentSpreads.js b/src/validation/rules/PossibleFragmentSpreads.js index aa8263c813..a02bdc3f1a 100644 --- a/src/validation/rules/PossibleFragmentSpreads.js +++ b/src/validation/rules/PossibleFragmentSpreads.js @@ -44,7 +44,9 @@ export function PossibleFragmentSpreads(context: ValidationContext): any { InlineFragment(node) { const fragType = context.getType(); const parentType = context.getParentType(); - if (fragType && parentType && !doTypesOverlap(fragType, parentType)) { + if (fragType && + parentType && + !doTypesOverlap(context.getSchema(), fragType, parentType)) { context.reportError(new GraphQLError( typeIncompatibleAnonSpreadMessage(parentType, fragType), [ node ] @@ -55,7 +57,9 @@ export function PossibleFragmentSpreads(context: ValidationContext): any { const fragName = node.name.value; const fragType = getFragmentType(context, fragName); const parentType = context.getParentType(); - if (fragType && parentType && !doTypesOverlap(fragType, parentType)) { + if (fragType && + parentType && + !doTypesOverlap(context.getSchema(), fragType, parentType)) { context.reportError(new GraphQLError( typeIncompatibleSpreadMessage(fragName, parentType, fragType), [ node ] diff --git a/src/validation/rules/VariablesInAllowedPosition.js b/src/validation/rules/VariablesInAllowedPosition.js index eac6e9ff78..930e7077e3 100644 --- a/src/validation/rules/VariablesInAllowedPosition.js +++ b/src/validation/rules/VariablesInAllowedPosition.js @@ -48,9 +48,12 @@ export function VariablesInAllowedPosition(context: ValidationContext): any { // the variable type is non-null when the expected type is nullable. // If both are list types, the variable item type can be more strict // than the expected item type (contravariant). - const varType = typeFromAST(context.getSchema(), varDef.type); - if (varType && - !isTypeSubTypeOf(effectiveType(varType, varDef), type)) { + const schema = context.getSchema(); + const varType = typeFromAST(schema, varDef.type); + if ( + varType && + !isTypeSubTypeOf(schema, effectiveType(varType, varDef), type) + ) { context.reportError(new GraphQLError( badVarPosMessage(varName, varType, type), [ varDef, node ]