diff --git a/src/utilities/__tests__/buildASTSchema-test.js b/src/utilities/__tests__/buildASTSchema-test.js index cc4f051d26..f2f685f51f 100644 --- a/src/utilities/__tests__/buildASTSchema-test.js +++ b/src/utilities/__tests__/buildASTSchema-test.js @@ -6,6 +6,7 @@ import { describe, it } from 'mocha'; import dedent from '../../jsutils/dedent'; import invariant from '../../jsutils/invariant'; +import { Kind } from '../../language/kinds'; import { parse } from '../../language/parser'; import { print } from '../../language/printer'; @@ -34,7 +35,7 @@ import { import { graphqlSync } from '../../graphql'; -import { printSchema } from '../schemaPrinter'; +import { printType, printSchema } from '../schemaPrinter'; import { buildASTSchema, buildSchema } from '../buildASTSchema'; /** @@ -54,6 +55,14 @@ function printASTNode(obj) { return print(obj.astNode); } +function printAllASTNodes(obj) { + invariant(obj.astNode != null && obj.extensionASTNodes != null); + return print({ + kind: Kind.DOCUMENT, + definitions: [obj.astNode, ...obj.extensionASTNodes], + }); +} + describe('Schema Builder', () => { it('can use built schema for limited execution', () => { const schema = buildASTSchema( @@ -754,6 +763,168 @@ describe('Schema Builder', () => { }); }); + it('Correctly extend scalar type', () => { + const scalarSDL = dedent` + scalar SomeScalar + + extend scalar SomeScalar @foo + + extend scalar SomeScalar @bar + `; + const schema = buildSchema(` + ${scalarSDL} + directive @foo on SCALAR + directive @bar on SCALAR + `); + + const someScalar = assertScalarType(schema.getType('SomeScalar')); + expect(printType(someScalar) + '\n').to.equal(dedent` + scalar SomeScalar + `); + + expect(printAllASTNodes(someScalar)).to.equal(scalarSDL); + }); + + it('Correctly extend object type', () => { + const objectSDL = dedent` + type SomeObject implements Foo { + first: String + } + + extend type SomeObject implements Bar { + second: Int + } + + extend type SomeObject implements Baz { + third: Float + } + `; + const schema = buildSchema(` + ${objectSDL} + interface Foo + interface Bar + interface Baz + `); + + const someObject = assertObjectType(schema.getType('SomeObject')); + expect(printType(someObject) + '\n').to.equal(dedent` + type SomeObject implements Foo & Bar & Baz { + first: String + second: Int + third: Float + } + `); + + expect(printAllASTNodes(someObject)).to.equal(objectSDL); + }); + + it('Correctly extend interface type', () => { + const interfaceSDL = dedent` + interface SomeInterface { + first: String + } + + extend interface SomeInterface { + second: Int + } + + extend interface SomeInterface { + third: Float + } + `; + const schema = buildSchema(interfaceSDL); + + const someInterface = assertInterfaceType(schema.getType('SomeInterface')); + expect(printType(someInterface) + '\n').to.equal(dedent` + interface SomeInterface { + first: String + second: Int + third: Float + } + `); + + expect(printAllASTNodes(someInterface)).to.equal(interfaceSDL); + }); + + it('Correctly extend union type', () => { + const unionSDL = dedent` + union SomeUnion = FirstType + + extend union SomeUnion = SecondType + + extend union SomeUnion = ThirdType + `; + const schema = buildSchema(` + ${unionSDL} + type FirstType + type SecondType + type ThirdType + `); + + const someUnion = assertUnionType(schema.getType('SomeUnion')); + expect(printType(someUnion) + '\n').to.equal(dedent` + union SomeUnion = FirstType | SecondType | ThirdType + `); + + expect(printAllASTNodes(someUnion)).to.equal(unionSDL); + }); + + it('Correctly extend enum type', () => { + const enumSDL = dedent` + enum SomeEnum { + FIRST + } + + extend enum SomeEnum { + SECOND + } + + extend enum SomeEnum { + THIRD + } + `; + const schema = buildSchema(enumSDL); + + const someEnum = assertEnumType(schema.getType('SomeEnum')); + expect(printType(someEnum) + '\n').to.equal(dedent` + enum SomeEnum { + FIRST + SECOND + THIRD + } + `); + + expect(printAllASTNodes(someEnum)).to.equal(enumSDL); + }); + + it('Correctly extend input object type', () => { + const inputSDL = dedent` + input SomeInput { + first: String + } + + extend input SomeInput { + second: Int + } + + extend input SomeInput { + third: Float + } + `; + const schema = buildSchema(inputSDL); + + const someInput = assertInputObjectType(schema.getType('SomeInput')); + expect(printType(someInput) + '\n').to.equal(dedent` + input SomeInput { + first: String + second: Int + third: Float + } + `); + + expect(printAllASTNodes(someInput)).to.equal(inputSDL); + }); + it('Correctly assign AST nodes', () => { const sdl = dedent` schema { diff --git a/src/utilities/__tests__/extendSchema-test.js b/src/utilities/__tests__/extendSchema-test.js index 482a0dd805..7ca413a6a7 100644 --- a/src/utilities/__tests__/extendSchema-test.js +++ b/src/utilities/__tests__/extendSchema-test.js @@ -31,6 +31,7 @@ import { assertScalarType, } from '../../type/definition'; +import { concatAST } from '../concatAST'; import { printSchema } from '../schemaPrinter'; import { extendSchema } from '../extendSchema'; import { buildSchema } from '../buildASTSchema'; @@ -419,6 +420,14 @@ describe('extendSchema', () => { secondExtensionAST, ); + const extendedInOneGoSchema = extendSchema( + schema, + concatAST([firstExtensionAST, secondExtensionAST]), + ); + expect(printSchema(extendedInOneGoSchema)).to.equal( + printSchema(extendedTwiceSchema), + ); + const query = assertObjectType(extendedTwiceSchema.getType('Query')); const someEnum = assertEnumType(extendedTwiceSchema.getType('SomeEnum')); const someUnion = assertUnionType(extendedTwiceSchema.getType('SomeUnion')); diff --git a/src/utilities/buildASTSchema.js b/src/utilities/buildASTSchema.js index 337efd125d..0545b61ed8 100644 --- a/src/utilities/buildASTSchema.js +++ b/src/utilities/buildASTSchema.js @@ -6,20 +6,24 @@ import keyMap from '../jsutils/keyMap'; import inspect from '../jsutils/inspect'; import invariant from '../jsutils/invariant'; import devAssert from '../jsutils/devAssert'; -import { type ObjMap } from '../jsutils/ObjMap'; +import { type ObjMap, type ReadOnlyObjMap } from '../jsutils/ObjMap'; import { Kind } from '../language/kinds'; import { type Source } from '../language/source'; import { TokenKind } from '../language/tokenKind'; import { type ParseOptions, parse } from '../language/parser'; -import { isTypeDefinitionNode } from '../language/predicates'; import { dedentBlockStringValue } from '../language/blockString'; import { type DirectiveLocationEnum } from '../language/directiveLocation'; +import { + isTypeDefinitionNode, + isTypeExtensionNode, +} from '../language/predicates'; import { type Location, type StringValueNode, type DocumentNode, type TypeNode, + type TypeExtensionNode, type NamedTypeNode, type SchemaDefinitionNode, type SchemaExtensionNode, @@ -125,15 +129,28 @@ export function buildASTSchema( assertValidSDL(documentAST); } + // Collect the definitions and extensions found in the document. let schemaDef: ?SchemaDefinitionNode; + const schemaExtensions: Array = []; const typeDefs: Array = []; + const typeExtensionsMap: ObjMap> = Object.create( + null, + ); const directiveDefs: Array = []; for (const def of documentAST.definitions) { if (def.kind === Kind.SCHEMA_DEFINITION) { schemaDef = def; + } else if (def.kind === Kind.SCHEMA_EXTENSION) { + schemaExtensions.push(def); } else if (isTypeDefinitionNode(def)) { typeDefs.push(def); + } else if (isTypeExtensionNode(def)) { + const extendedTypeName = def.name.value; + const existingTypeExtensions = typeExtensionsMap[extendedTypeName]; + typeExtensionsMap[extendedTypeName] = existingTypeExtensions + ? existingTypeExtensions.concat([def]) + : [def]; } else if (def.kind === Kind.DIRECTIVE_DEFINITION) { directiveDefs.push(def); } @@ -147,9 +164,9 @@ export function buildASTSchema( return type; }); - const typeMap = astBuilder.buildTypeMap(typeDefs); + const typeMap = astBuilder.buildTypeMap(typeDefs, typeExtensionsMap); const operationTypes = schemaDef - ? astBuilder.getOperationTypes([schemaDef]) + ? astBuilder.getOperationTypes([schemaDef, ...schemaExtensions]) : { // Note: While this could make early assertions to get the correctly // typed values below, that would throw immediately while type system @@ -179,6 +196,7 @@ export function buildASTSchema( types: objectValues(typeMap), directives, astNode: schemaDef, + extensionASTNodes: schemaExtensions, assumeValid: options && options.assumeValid, }); } @@ -392,63 +410,97 @@ export class ASTDefinitionBuilder { buildTypeMap( nodes: $ReadOnlyArray, + extensionMap: ReadOnlyObjMap<$ReadOnlyArray>, ): ObjMap { const typeMap = Object.create(null); for (const node of nodes) { const name = node.name.value; - typeMap[name] = stdTypeMap[name] || this._buildType(node); + typeMap[name] = + stdTypeMap[name] || this._buildType(node, extensionMap[name] || []); } return typeMap; } - _buildType(astNode: TypeDefinitionNode): GraphQLNamedType { + _buildType( + astNode: TypeDefinitionNode, + extensionNodes: $ReadOnlyArray, + ): GraphQLNamedType { const name = astNode.name.value; const description = getDescription(astNode, this._options); switch (astNode.kind) { - case Kind.OBJECT_TYPE_DEFINITION: + case Kind.OBJECT_TYPE_DEFINITION: { + const extensionASTNodes = (extensionNodes: any); + const allNodes = [astNode, ...extensionASTNodes]; + return new GraphQLObjectType({ name, description, - interfaces: () => this.buildInterfaces([astNode]), - fields: () => this.buildFieldMap([astNode]), + interfaces: () => this.buildInterfaces(allNodes), + fields: () => this.buildFieldMap(allNodes), astNode, + extensionASTNodes, }); - case Kind.INTERFACE_TYPE_DEFINITION: + } + case Kind.INTERFACE_TYPE_DEFINITION: { + const extensionASTNodes = (extensionNodes: any); + const allNodes = [astNode, ...extensionASTNodes]; + return new GraphQLInterfaceType({ name, description, - interfaces: () => this.buildInterfaces([astNode]), - fields: () => this.buildFieldMap([astNode]), + interfaces: () => this.buildInterfaces(allNodes), + fields: () => this.buildFieldMap(allNodes), astNode, + extensionASTNodes, }); - case Kind.ENUM_TYPE_DEFINITION: + } + case Kind.ENUM_TYPE_DEFINITION: { + const extensionASTNodes = (extensionNodes: any); + const allNodes = [astNode, ...extensionASTNodes]; + return new GraphQLEnumType({ name, description, - values: this.buildEnumValueMap([astNode]), + values: this.buildEnumValueMap(allNodes), astNode, + extensionASTNodes, }); - case Kind.UNION_TYPE_DEFINITION: + } + case Kind.UNION_TYPE_DEFINITION: { + const extensionASTNodes = (extensionNodes: any); + const allNodes = [astNode, ...extensionASTNodes]; + return new GraphQLUnionType({ name, description, - types: () => this.buildUnionTypes([astNode]), + types: () => this.buildUnionTypes(allNodes), astNode, + extensionASTNodes, }); - case Kind.SCALAR_TYPE_DEFINITION: + } + case Kind.SCALAR_TYPE_DEFINITION: { + const extensionASTNodes = (extensionNodes: any); + return new GraphQLScalarType({ name, description, astNode, + extensionASTNodes, }); - case Kind.INPUT_OBJECT_TYPE_DEFINITION: + } + case Kind.INPUT_OBJECT_TYPE_DEFINITION: { + const extensionASTNodes = (extensionNodes: any); + const allNodes = [astNode, ...extensionASTNodes]; + return new GraphQLInputObjectType({ name, description, - fields: () => this.buildInputFieldMap([astNode]), + fields: () => this.buildInputFieldMap(allNodes), astNode, + extensionASTNodes, }); + } } // Not reachable. All possible type definition nodes have been considered. diff --git a/src/utilities/extendSchema.js b/src/utilities/extendSchema.js index 48a0815db7..c0155be275 100644 --- a/src/utilities/extendSchema.js +++ b/src/utilities/extendSchema.js @@ -156,7 +156,7 @@ export function extendSchema( return type; }); - const typeMap = astBuilder.buildTypeMap(typeDefs); + const typeMap = astBuilder.buildTypeMap(typeDefs, typeExtensionsMap); const schemaConfig = schema.toConfig(); for (const existingType of schemaConfig.types) { typeMap[existingType.name] = extendNamedType(existingType);