From 4b29c3567827c7a4f4e465077bf688315e6e9949 Mon Sep 17 00:00:00 2001 From: Michael Grenier Date: Sun, 22 Apr 2018 17:01:57 -0400 Subject: [PATCH 01/13] Extend the extend functionality --- src/utilities/__tests__/extendSchema-test.js | 51 +++++++ src/utilities/buildASTSchema.js | 32 +++-- src/utilities/extendSchema.js | 136 ++++++++++++++++++- 3 files changed, 203 insertions(+), 16 deletions(-) diff --git a/src/utilities/__tests__/extendSchema-test.js b/src/utilities/__tests__/extendSchema-test.js index 05740c6a35..31fe3fd5cf 100644 --- a/src/utilities/__tests__/extendSchema-test.js +++ b/src/utilities/__tests__/extendSchema-test.js @@ -842,6 +842,57 @@ describe('extendSchema', () => { ]); }); + it('extend enum', () => { + const extendedSchema = extendTestSchema(` + extend enum SomeEnum { + NEW_ENUM + } + `); + expect(extendedSchema).to.not.equal(testSchema); + expect(printSchema(extendedSchema)).to.contain('NEW_ENUM'); + expect(printSchema(testSchema)).to.not.contain('NEW_ENUM'); + }); + + it('extend input', () => { + const extendedSchema = extendTestSchema(` + extend type Query { + newField(testArg: TestInput): String + } + + input TestInput { + testInputField: String + } + `); + const secondExtensionAST = parse(` + extend input TestInput { + newInputField: String + } + `); + const extendedTwiceSchema = extendSchema( + extendedSchema, + secondExtensionAST, + ); + + expect(extendedSchema).to.not.equal(testSchema); + expect(printSchema(extendedTwiceSchema)).to.contain('newInputField'); + expect(printSchema(extendedSchema)).to.not.contain('newInputField'); + expect(printSchema(testSchema)).to.not.contain('newInputField'); + }); + + it('extend union', () => { + const extendedSchema = extendTestSchema(` + extend union SomeUnion = TestNewType + + type TestNewType { + foo: String + } + `); + + expect(extendedSchema).to.not.equal(testSchema); + expect(printSchema(extendedSchema)).to.contain('Foo | Biz | TestNewType'); + expect(printSchema(testSchema)).to.not.contain('Foo | Biz | TestNewType'); + }); + describe('does not allow extending a non-object type', () => { it('not an object', () => { const ast = parse(` diff --git a/src/utilities/buildASTSchema.js b/src/utilities/buildASTSchema.js index 2ff2f7dd1d..3fa45ee2a5 100644 --- a/src/utilities/buildASTSchema.js +++ b/src/utilities/buildASTSchema.js @@ -41,6 +41,8 @@ import type { import type { DirectiveLocationEnum } from '../language/directiveLocation'; +import type { GraphQLEnumValueConfig } from '../type/definition'; + import { assertNullableType, GraphQLScalarType, @@ -318,6 +320,14 @@ export class ASTDefinitionBuilder { }; } + buildEnumValue(value: EnumValueDefinitionNode): GraphQLEnumValueConfig { + return { + description: getDescription(value, this._options), + deprecationReason: getDeprecationReason(value), + astNode: value, + }; + } + _makeSchemaDef(def: TypeDefinitionNode): GraphQLNamedType { switch (def.kind) { case Kind.OBJECT_TYPE_DEFINITION: @@ -396,21 +406,21 @@ export class ASTDefinitionBuilder { return new GraphQLEnumType({ name: def.name.value, description: getDescription(def, this._options), - values: def.values - ? keyValMap( - def.values, - enumValue => enumValue.name.value, - enumValue => ({ - description: getDescription(enumValue, this._options), - deprecationReason: getDeprecationReason(enumValue), - astNode: enumValue, - }), - ) - : {}, + values: this._makeValueDefMap(def), astNode: def, }); } + _makeValueDefMap(def: EnumTypeDefinitionNode) { + return def.values + ? keyValMap( + def.values, + enumValue => enumValue.name.value, + enumValue => this.buildEnumValue(enumValue), + ) + : {}; + } + _makeUnionDef(def: UnionTypeDefinitionNode) { return new GraphQLUnionType({ name: def.name.value, diff --git a/src/utilities/extendSchema.js b/src/utilities/extendSchema.js index 49fe0fab25..b2268d7a54 100644 --- a/src/utilities/extendSchema.js +++ b/src/utilities/extendSchema.js @@ -17,6 +17,8 @@ import { isIntrospectionType } from '../type/introspection'; import type { GraphQLSchemaValidationOptions } from '../type/schema'; +import type { GraphQLArgumentConfig, GraphQLArgument } from '../type/definition'; + import { isObjectType, isInterfaceType, @@ -28,6 +30,10 @@ import { GraphQLObjectType, GraphQLInterfaceType, GraphQLUnionType, + isEnumType, + GraphQLEnumType, + isInputObjectType, + GraphQLInputObjectType, } from '../type/definition'; import { GraphQLDirective } from '../type/directives'; @@ -127,6 +133,9 @@ export function extendSchema( break; case Kind.OBJECT_TYPE_EXTENSION: case Kind.INTERFACE_TYPE_EXTENSION: + case Kind.ENUM_TYPE_EXTENSION: + case Kind.INPUT_OBJECT_TYPE_EXTENSION: + case Kind.UNION_TYPE_EXTENSION: // Sanity check that this type extension exists within the // schema's existing types. const extendedTypeName = def.name.value; @@ -158,9 +167,6 @@ export function extendSchema( directiveDefinitions.push(def); break; case Kind.SCALAR_TYPE_EXTENSION: - case Kind.UNION_TYPE_EXTENSION: - case Kind.ENUM_TYPE_EXTENSION: - case Kind.INPUT_OBJECT_TYPE_EXTENSION: throw new Error( `The ${def.kind} kind is not yet supported by extendSchema().`, ); @@ -287,10 +293,103 @@ export function extendSchema( if (isUnionType(type)) { return extendUnionType(type); } + if (isEnumType(type)) { + return extendEnumType(type); + } + if (isInputObjectType(type)) { + return extendInputObjectType(type); + } // This type is not yet extendable. return type; } + function extendInputObjectType( + type: GraphQLInputObjectType, + ): GraphQLInputObjectType { + return new GraphQLInputObjectType({ + name: type.name, + description: type.description, + fields: () => extendInputFieldMap(type), + astNode: type.astNode, + }); + } + + function extendInputFieldMap(type: GraphQLInputObjectType) { + const newFieldMap = Object.create(null); + const oldFieldMap = type.getFields(); + Object.keys(oldFieldMap).forEach(fieldName => { + const field = oldFieldMap[fieldName]; + newFieldMap[fieldName] = { + description: field.description, + type: extendFieldType(field.type), + defaultValue: field.defaultValue, + astNode: field.astNode, + }; + }); + + // If there are any extensions to the fields, apply those here. + const extensions = typeExtensionsMap[type.name]; + if (extensions) { + extensions.forEach(extension => { + extension.fields.forEach(field => { + const fieldName = field.name.value; + if (oldFieldMap[fieldName]) { + throw new GraphQLError( + `Field "${type.name}.${fieldName}" already exists in the ` + + 'schema. It cannot also be defined in this type extension.', + [field], + ); + } + newFieldMap[fieldName] = astBuilder.buildField(field); + }); + }); + } + + return newFieldMap; + } + + function extendEnumType(type: GraphQLEnumType): GraphQLEnumType { + return new GraphQLEnumType({ + name: type.name, + values: extendValueMap(type), + astNode: type.astNode, + }); + } + + function extendValueMap(type: GraphQLEnumType) { + const oldValueMap = Object.create(null); + const newValueMap = Object.create(null); + type.getValues().forEach(value => { + newValueMap[value.name] = oldValueMap[value.name] = { + name: value.name, + description: value.description, + value: value.value, + deprecationReason: value.deprecationReason, + astNode: value.astNode, + }; + }); + + // If there are any extensions to the values, apply those here. + const extensions = typeExtensionsMap[type.name]; + if (extensions) { + extensions.forEach(extension => { + extension.values.forEach(value => { + const valueName = value.name.value; + if (oldValueMap[valueName]) { + throw new GraphQLError( + `Enum "${type.name}.${valueName}" already exists in the ` + + 'schema. It cannot also be defined in this enum extension.', + [value], + ); + } + newValueMap[valueName] = astBuilder.buildEnumValue(value); + }); + }); + } + + return newValueMap; + } + function extendObjectType(type: GraphQLObjectType): GraphQLObjectType { const name = type.name; const extensionASTNodes = typeExtensionsMap[name] @@ -309,6 +408,18 @@ export function extendSchema( }); } + function extendArgsMap( + args: GraphQLArgument[], + ): GraphQLArgument[] { + return args.map(arg => ({ + name: arg.name, + type: extendFieldType(arg.type), + defaultValue: arg.defaultValue, + description: arg.description, + astNode: arg.astNode, + })); + } + function extendInterfaceType( type: GraphQLInterfaceType, ): GraphQLInterfaceType { @@ -329,10 +440,25 @@ export function extendSchema( } function extendUnionType(type: GraphQLUnionType): GraphQLUnionType { + const types = type.getTypes().map(getExtendedType); + + // If there are any extensions to the union, apply those here. + const extensions = typeExtensionsMap[type.name]; + if (extensions) { + extensions.forEach(extension => { + extension.types.forEach(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. + types.push((astBuilder.buildType(namedType): any)); + }); + }); + } + return new GraphQLUnionType({ name: type.name, description: type.description, - types: type.getTypes().map(getExtendedType), + types: types, astNode: type.astNode, resolveType: type.resolveType, }); @@ -368,7 +494,7 @@ export function extendSchema( description: field.description, deprecationReason: field.deprecationReason, type: extendFieldType(field.type), - args: keyMap(field.args, arg => arg.name), + args: keyMap(extendArgsMap(field.args), arg => arg.name), astNode: field.astNode, resolve: field.resolve, }; From 38cfdba3d80258de8a230b7c163330f7298cf94b Mon Sep 17 00:00:00 2001 From: Michael Grenier Date: Wed, 25 Apr 2018 20:16:22 -0400 Subject: [PATCH 02/13] Fix flowtype --- src/type/definition.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/type/definition.js b/src/type/definition.js index 2d7025a470..1f68e9a688 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -982,6 +982,7 @@ export class GraphQLUnionType { name: string; description: ?string; astNode: ?UnionTypeDefinitionNode; + extensionASTNodes: ?$ReadOnlyArray; resolveType: ?GraphQLTypeResolver<*, *>; _typeConfig: GraphQLUnionTypeConfig<*, *>; @@ -1071,6 +1072,7 @@ export class GraphQLEnumType /* */ { name: string; description: ?string; astNode: ?EnumTypeDefinitionNode; + extensionASTNodes: ?$ReadOnlyArray; _values: Array */>; _valueLookup: Map; @@ -1217,6 +1219,7 @@ export class GraphQLInputObjectType { name: string; description: ?string; astNode: ?InputObjectTypeDefinitionNode; + extensionASTNodes: ?$ReadOnlyArray; _typeConfig: GraphQLInputObjectTypeConfig; _fields: GraphQLInputFieldMap; From 5e171debb33bad226de86e7ce54b9783885b1c7e Mon Sep 17 00:00:00 2001 From: Michael Grenier Date: Wed, 25 Apr 2018 20:17:22 -0400 Subject: [PATCH 03/13] Extract logic from _makeInputValues to its own function --- src/utilities/buildASTSchema.js | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/utilities/buildASTSchema.js b/src/utilities/buildASTSchema.js index 3fa45ee2a5..85f85ac5cf 100644 --- a/src/utilities/buildASTSchema.js +++ b/src/utilities/buildASTSchema.js @@ -41,7 +41,10 @@ import type { import type { DirectiveLocationEnum } from '../language/directiveLocation'; -import type { GraphQLEnumValueConfig } from '../type/definition'; +import type { + GraphQLEnumValueConfig, + GraphQLInputField, +} from '../type/definition'; import { assertNullableType, @@ -320,6 +323,20 @@ export class ASTDefinitionBuilder { }; } + buildInputField(value: InputValueDefinitionNode): GraphQLInputField { + // 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 { + name: value.name.value, + type, + description: getDescription(value, this._options), + defaultValue: valueFromAST(value.defaultValue, type), + astNode: value, + }; + } + buildEnumValue(value: EnumValueDefinitionNode): GraphQLEnumValueConfig { return { description: getDescription(value, this._options), @@ -378,18 +395,7 @@ export class ASTDefinitionBuilder { return keyValMap( values, value => value.name.value, - value => { - // 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), - defaultValue: valueFromAST(value.defaultValue, type), - astNode: value, - }; - }, + value => this.buildInputField(value), ); } From 50613604e53ff1f5e479c74429036155b0f1b3d3 Mon Sep 17 00:00:00 2001 From: Michael Grenier Date: Wed, 25 Apr 2018 20:18:20 -0400 Subject: [PATCH 04/13] Fix lint and minor refactors --- src/utilities/__tests__/extendSchema-test.js | 13 +++- src/utilities/extendSchema.js | 79 +++++++++++++------- 2 files changed, 65 insertions(+), 27 deletions(-) diff --git a/src/utilities/__tests__/extendSchema-test.js b/src/utilities/__tests__/extendSchema-test.js index 31fe3fd5cf..7a72f1bab3 100644 --- a/src/utilities/__tests__/extendSchema-test.js +++ b/src/utilities/__tests__/extendSchema-test.js @@ -847,10 +847,19 @@ describe('extendSchema', () => { extend enum SomeEnum { NEW_ENUM } + + directive @test(arg: SomeEnum) on FIELD `); expect(extendedSchema).to.not.equal(testSchema); - expect(printSchema(extendedSchema)).to.contain('NEW_ENUM'); - expect(printSchema(testSchema)).to.not.contain('NEW_ENUM'); + + const oldEnum = testSchema.getType('SomeEnum'); + const newEnum = extendedSchema.getType('SomeEnum'); + const testDirective = extendedSchema.getDirective('test'); + + expect(oldEnum._values.length).to.equal(2); + expect(newEnum._values.length).to.equal(3); + expect(newEnum._values[2].name).to.equal('NEW_ENUM'); + expect(newEnum).to.equal(testDirective.args[0].type); }); it('extend input', () => { diff --git a/src/utilities/extendSchema.js b/src/utilities/extendSchema.js index b2268d7a54..6177fa5959 100644 --- a/src/utilities/extendSchema.js +++ b/src/utilities/extendSchema.js @@ -9,6 +9,7 @@ import invariant from '../jsutils/invariant'; import keyMap from '../jsutils/keyMap'; +import keyValMap from '../jsutils/keyValMap'; import objectValues from '../jsutils/objectValues'; import { ASTDefinitionBuilder } from './buildASTSchema'; import { GraphQLError } from '../error/GraphQLError'; @@ -17,7 +18,10 @@ import { isIntrospectionType } from '../type/introspection'; import type { GraphQLSchemaValidationOptions } from '../type/schema'; -import type { GraphQLArgumentConfig, GraphQLArgument } from '../type/definition'; +import type { + GraphQLArgument, + GraphQLFieldConfigArgumentMap, +} from '../type/definition'; import { isObjectType, @@ -25,14 +29,14 @@ import { isUnionType, isListType, isNonNullType, + isEnumType, + isInputObjectType, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLInterfaceType, GraphQLUnionType, - isEnumType, GraphQLEnumType, - isInputObjectType, GraphQLInputObjectType, } from '../type/definition'; @@ -306,11 +310,18 @@ export function extendSchema( function extendInputObjectType( type: GraphQLInputObjectType, ): GraphQLInputObjectType { + const name = type.name; + const extensionASTNodes = typeExtensionsMap[name] + ? type.extensionASTNodes + ? type.extensionASTNodes.concat(typeExtensionsMap[name]) + : typeExtensionsMap[name] + : type.extensionASTNodes; return new GraphQLInputObjectType({ - name: type.name, + name, description: type.description, fields: () => extendInputFieldMap(type), astNode: type.astNode, + extensionASTNodes, }); } @@ -320,6 +331,7 @@ export function extendSchema( Object.keys(oldFieldMap).forEach(fieldName => { const field = oldFieldMap[fieldName]; newFieldMap[fieldName] = { + // name: fieldName, description: field.description, type: extendFieldType(field.type), defaultValue: field.defaultValue, @@ -340,7 +352,7 @@ export function extendSchema( [field], ); } - newFieldMap[fieldName] = astBuilder.buildField(field); + newFieldMap[fieldName] = astBuilder.buildInputField(field); }); }); } @@ -349,21 +361,29 @@ export function extendSchema( } function extendEnumType(type: GraphQLEnumType): GraphQLEnumType { + const name = type.name; + const extensionASTNodes = typeExtensionsMap[name] + ? type.extensionASTNodes + ? type.extensionASTNodes.concat(typeExtensionsMap[name]) + : typeExtensionsMap[name] + : type.extensionASTNodes; return new GraphQLEnumType({ - name: type.name, + name, values: extendValueMap(type), astNode: type.astNode, + extensionASTNodes, }); } function extendValueMap(type: GraphQLEnumType) { - const oldValueMap = Object.create(null); const newValueMap = Object.create(null); - type.getValues().forEach(value => { - newValueMap[value.name] = oldValueMap[value.name] = { + const oldValueMap = keyMap(type.getValues(), value => value.name); + Object.keys(oldValueMap).forEach(valueName => { + const value = oldValueMap[valueName]; + newValueMap[valueName] = { name: value.name, description: value.description, - value: value.value, + value: value.hasOwnProperty('value') ? value.value : valueName, deprecationReason: value.deprecationReason, astNode: value.astNode, }; @@ -408,16 +428,18 @@ export function extendSchema( }); } - function extendArgsMap( - args: GraphQLArgument[], - ): GraphQLArgument[] { - return args.map(arg => ({ - name: arg.name, - type: extendFieldType(arg.type), - defaultValue: arg.defaultValue, - description: arg.description, - astNode: arg.astNode, - })); + function extendArgs(args: GraphQLArgument[]): GraphQLFieldConfigArgumentMap { + return keyValMap( + args, + arg => arg.name, + arg => ({ + name: arg.name, + type: extendFieldType(arg.type), + defaultValue: arg.defaultValue, + description: arg.description, + astNode: arg.astNode, + }), + ); } function extendInterfaceType( @@ -440,7 +462,13 @@ export function extendSchema( } function extendUnionType(type: GraphQLUnionType): GraphQLUnionType { - const types = type.getTypes().map(getExtendedType); + const name = type.name; + const extensionASTNodes = typeExtensionsMap[name] + ? type.extensionASTNodes + ? type.extensionASTNodes.concat(typeExtensionsMap[name]) + : typeExtensionsMap[name] + : type.extensionASTNodes; + const unionTypes = type.getTypes().map(getExtendedType); // If there are any extensions to the union, apply those here. const extensions = typeExtensionsMap[type.name]; @@ -450,17 +478,18 @@ export function extendSchema( // 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. - types.push((astBuilder.buildType(namedType): any)); + unionTypes.push((astBuilder.buildType(namedType): any)); }); }); } return new GraphQLUnionType({ - name: type.name, + name, description: type.description, - types: types, + types: unionTypes, astNode: type.astNode, resolveType: type.resolveType, + extensionASTNodes, }); } @@ -494,7 +523,7 @@ export function extendSchema( description: field.description, deprecationReason: field.deprecationReason, type: extendFieldType(field.type), - args: keyMap(extendArgsMap(field.args), arg => arg.name), + args: extendArgs(field.args), astNode: field.astNode, resolve: field.resolve, }; From db7d4c079f8bafebe4d64a8992b6eb4b2ea1ed02 Mon Sep 17 00:00:00 2001 From: Michael Grenier Date: Thu, 26 Apr 2018 19:37:37 -0400 Subject: [PATCH 05/13] Fix type error due to copy & paste --- src/utilities/extendSchema.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/utilities/extendSchema.js b/src/utilities/extendSchema.js index 6177fa5959..b6c7c2dd7d 100644 --- a/src/utilities/extendSchema.js +++ b/src/utilities/extendSchema.js @@ -331,7 +331,6 @@ export function extendSchema( Object.keys(oldFieldMap).forEach(fieldName => { const field = oldFieldMap[fieldName]; newFieldMap[fieldName] = { - // name: fieldName, description: field.description, type: extendFieldType(field.type), defaultValue: field.defaultValue, @@ -433,7 +432,6 @@ export function extendSchema( args, arg => arg.name, arg => ({ - name: arg.name, type: extendFieldType(arg.type), defaultValue: arg.defaultValue, description: arg.description, From 50ab294771f0a686e4f3484f75f6b5c0ec162b62 Mon Sep 17 00:00:00 2001 From: Michael Grenier Date: Thu, 26 Apr 2018 19:42:02 -0400 Subject: [PATCH 06/13] Fix array syntax --- src/utilities/extendSchema.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/utilities/extendSchema.js b/src/utilities/extendSchema.js index b6c7c2dd7d..b8d2199201 100644 --- a/src/utilities/extendSchema.js +++ b/src/utilities/extendSchema.js @@ -427,7 +427,9 @@ export function extendSchema( }); } - function extendArgs(args: GraphQLArgument[]): GraphQLFieldConfigArgumentMap { + function extendArgs( + args: Array, + ): GraphQLFieldConfigArgumentMap { return keyValMap( args, arg => arg.name, From fed46bf693fed8b3ad5f748e6adcf0a65ffc52e6 Mon Sep 17 00:00:00 2001 From: Michael Grenier Date: Thu, 26 Apr 2018 19:43:11 -0400 Subject: [PATCH 07/13] Fix test more verbose --- src/utilities/__tests__/extendSchema-test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utilities/__tests__/extendSchema-test.js b/src/utilities/__tests__/extendSchema-test.js index 7a72f1bab3..e4adf5f2a1 100644 --- a/src/utilities/__tests__/extendSchema-test.js +++ b/src/utilities/__tests__/extendSchema-test.js @@ -856,10 +856,10 @@ describe('extendSchema', () => { const newEnum = extendedSchema.getType('SomeEnum'); const testDirective = extendedSchema.getDirective('test'); - expect(oldEnum._values.length).to.equal(2); - expect(newEnum._values.length).to.equal(3); + expect(oldEnum._values).to.have.lengthOf(2); + expect(newEnum._values).to.have.lengthOf(3); expect(newEnum._values[2].name).to.equal('NEW_ENUM'); - expect(newEnum).to.equal(testDirective.args[0].type); + expect(testDirective).to.have.nested.property('args[0].type', newEnum); }); it('extend input', () => { From 112e366df954849d2aeecbccbd01ab9a0aec8cc6 Mon Sep 17 00:00:00 2001 From: Michael Grenier Date: Thu, 26 Apr 2018 19:45:52 -0400 Subject: [PATCH 08/13] Removed unnecessary check --- src/utilities/extendSchema.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utilities/extendSchema.js b/src/utilities/extendSchema.js index b8d2199201..ebdb605bb1 100644 --- a/src/utilities/extendSchema.js +++ b/src/utilities/extendSchema.js @@ -382,7 +382,7 @@ export function extendSchema( newValueMap[valueName] = { name: value.name, description: value.description, - value: value.hasOwnProperty('value') ? value.value : valueName, + value: value.value, deprecationReason: value.deprecationReason, astNode: value.astNode, }; From b122ea09ba6734350d4199b0cc9e364c55b800a1 Mon Sep 17 00:00:00 2001 From: Michael Grenier Date: Fri, 27 Apr 2018 20:24:33 -0400 Subject: [PATCH 09/13] Extend directive --- src/utilities/extendSchema.js | 38 +++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/src/utilities/extendSchema.js b/src/utilities/extendSchema.js index ebdb605bb1..9d18c20864 100644 --- a/src/utilities/extendSchema.js +++ b/src/utilities/extendSchema.js @@ -267,7 +267,7 @@ export function extendSchema( // this scope and have access to the schema, cache, and newly defined types. function getMergedDirectives(): Array { - const existingDirectives = schema.getDirectives(); + const existingDirectives = schema.getDirectives().map(extendDirectiveType); invariant(existingDirectives, 'schema must have default directives'); return existingDirectives.concat( @@ -275,6 +275,16 @@ export function extendSchema( ); } + function extendDirectiveType(type: GraphQLDirective): GraphQLDirective { + return new GraphQLDirective({ + name: type.name, + description: type.description, + locations: type.locations, + args: extendArgs(type.args), + astNode: type.astNode, + }); + } + function getExtendedType(type: T): T { if (!extendTypeCache[type.name]) { extendTypeCache[type.name] = extendType(type); @@ -397,7 +407,7 @@ export function extendSchema( if (oldValueMap[valueName]) { throw new GraphQLError( `Enum "${type.name}.${valueName}" already exists in the ` + - 'schema. It cannot also be defined in this enum extension.', + 'schema. It cannot also be defined in this type extension.', [value], ); } @@ -579,5 +589,29 @@ function checkExtensionNode(type, node) { ); } break; + case Kind.ENUM_TYPE_EXTENSION: + if (!isEnumType(type)) { + throw new GraphQLError( + `Cannot extend non-enum type "${type.name}".`, + [node], + ); + } + break; + case Kind.UNION_TYPE_EXTENSION: + if (!isUnionType(type)) { + throw new GraphQLError( + `Cannot extend non-union type "${type.name}".`, + [node], + ); + } + break; + case Kind.INPUT_OBJECT_TYPE_EXTENSION: + if (!isInputObjectType(type)) { + throw new GraphQLError( + `Cannot extend non-input object type "${type.name}".`, + [node], + ); + } + break; } } From 371b09f2f79e06b0b65536df90e808c5016b23ba Mon Sep 17 00:00:00 2001 From: Michael Grenier Date: Fri, 27 Apr 2018 20:25:03 -0400 Subject: [PATCH 10/13] Moved new tests into existing tests --- src/utilities/__tests__/extendSchema-test.js | 350 +++++++++++++++---- 1 file changed, 284 insertions(+), 66 deletions(-) diff --git a/src/utilities/__tests__/extendSchema-test.js b/src/utilities/__tests__/extendSchema-test.js index e4adf5f2a1..448f2c26e6 100644 --- a/src/utilities/__tests__/extendSchema-test.js +++ b/src/utilities/__tests__/extendSchema-test.js @@ -21,6 +21,7 @@ import { GraphQLID, GraphQLString, GraphQLEnumType, + GraphQLInputObjectType, GraphQLNonNull, GraphQLList, isScalarType, @@ -77,6 +78,13 @@ const SomeEnumType = new GraphQLEnumType({ }, }); +const SomeInputType = new GraphQLInputObjectType({ + name: 'SomeInput', + fields: () => ({ + fooArg: { type: GraphQLString }, + }), +}); + const testSchema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'Query', @@ -88,6 +96,10 @@ const testSchema = new GraphQLSchema({ args: { id: { type: GraphQLNonNull(GraphQLID) } }, type: SomeInterfaceType, }, + someInput: { + args: { input: { type: SomeInputType } }, + type: GraphQLString, + }, }), }), types: [FooType, BarType], @@ -200,6 +212,52 @@ describe('extendSchema', () => { `); }); + it('extends enums by adding new values', () => { + const extendedSchema = extendTestSchema(` + extend enum SomeEnum { + NEW_ENUM + } + `); + expect(printTestSchemaChanges(extendedSchema)).to.equal(dedent` + enum SomeEnum { + ONE + TWO + NEW_ENUM + } + `); + }); + + it('extends unions by adding new types', () => { + const extendedSchema = extendTestSchema(` + extend union SomeUnion = TestNewType + + type TestNewType { + foo: String + } + `); + expect(printTestSchemaChanges(extendedSchema)).to.equal(dedent` + union SomeUnion = Foo | Biz | TestNewType + + type TestNewType { + foo: String + } + `); + }); + + it('extends inputs by adding new fields', () => { + const extendedSchema = extendTestSchema(` + extend input SomeInput { + newField: String + } + `); + expect(printTestSchemaChanges(extendedSchema)).to.equal(dedent` + input SomeInput { + fooArg: String + newField: String + } + `); + }); + it('correctly assign AST nodes to new and extended types', () => { const extendedSchema = extendTestSchema(` extend type Query { @@ -314,11 +372,21 @@ describe('extendSchema', () => { extend type Foo { deprecatedField: String @deprecated(reason: "not used anymore") } + + extend enum SomeEnum { + DEPRECATED @deprecated(reason: "do not use") + } `); const deprecatedFieldDef = extendedSchema.getType('Foo').getFields() .deprecatedField; expect(deprecatedFieldDef.isDeprecated).to.equal(true); expect(deprecatedFieldDef.deprecationReason).to.equal('not used anymore'); + + const deprecatedEnumDef = extendedSchema + .getType('SomeEnum') + .getValue('DEPRECATED'); + expect(deprecatedEnumDef.isDeprecated).to.equal(true); + expect(deprecatedEnumDef.deprecationReason).to.equal('do not use'); }); it('extends objects by adding new unused types', () => { @@ -326,12 +394,32 @@ describe('extendSchema', () => { type Unused { someField: String } + + enum UnusedEnum { + SOME + } + + input UnusedInput { + someInput: String + } + + union UnusedUnion = Unused `); expect(extendedSchema).to.not.equal(testSchema); expect(printTestSchemaChanges(extendedSchema)).to.equal(dedent` type Unused { someField: String } + + enum UnusedEnum { + SOME + } + + input UnusedInput { + someInput: String + } + + union UnusedUnion = Unused `); }); @@ -507,6 +595,34 @@ describe('extendSchema', () => { interface NewInterface { buzz: String } + + extend enum SomeEnum { + THREE + } + + extend enum SomeEnum { + FOUR + } + + extend union SomeUnion = Boo + + extend union SomeUnion = Joo + + type Boo { + fieldA: String + } + + type Joo { + fieldB: String + } + + extend input SomeInput { + fieldA: String + } + + extend input SomeInput { + fieldB: String + } `); expect(printTestSchemaChanges(extendedSchema)).to.equal(dedent` type Biz implements NewInterface & SomeInterface { @@ -518,9 +634,32 @@ describe('extendSchema', () => { newFieldB: Float } + type Boo { + fieldA: String + } + + type Joo { + fieldB: String + } + interface NewInterface { buzz: String } + + enum SomeEnum { + ONE + TWO + THREE + FOUR + } + + input SomeInput { + fooArg: String + fieldA: String + fieldB: String + } + + union SomeUnion = Foo | Biz | Boo | Joo `); }); @@ -740,36 +879,102 @@ describe('extendSchema', () => { }); it('does not allow replacing an existing type', () => { - const ast = parse(` + const typeAst = parse(` type Bar { baz: String } `); - expect(() => extendSchema(testSchema, ast)).to.throw( + expect(() => extendSchema(testSchema, typeAst)).to.throw( 'Type "Bar" already exists in the schema. It cannot also be defined ' + 'in this type definition.', ); + + const enumAst = parse(` + enum SomeEnum { + FOO + } + `); + expect(() => extendSchema(testSchema, enumAst)).to.throw( + 'Type "SomeEnum" already exists in the schema. It cannot also be defined ' + + 'in this type definition.', + ); + + const unionAst = parse(` + union SomeUnion = Foo + `); + expect(() => extendSchema(testSchema, unionAst)).to.throw( + 'Type "SomeUnion" already exists in the schema. It cannot also be defined ' + + 'in this type definition.', + ); + + const inputAst = parse(` + input SomeInput { + some: String + } + `); + expect(() => extendSchema(testSchema, inputAst)).to.throw( + 'Type "SomeInput" already exists in the schema. It cannot also be defined ' + + 'in this type definition.', + ); }); it('does not allow replacing an existing field', () => { - const ast = parse(` + const typeAst = parse(` extend type Bar { foo: Foo } `); - expect(() => extendSchema(testSchema, ast)).to.throw( + expect(() => extendSchema(testSchema, typeAst)).to.throw( 'Field "Bar.foo" already exists in the schema. It cannot also be ' + 'defined in this type extension.', ); + + const enumAst = parse(` + extend enum SomeEnum { + ONE + } + `); + expect(() => extendSchema(testSchema, enumAst)).to.throw( + 'Enum "SomeEnum.ONE" already exists in the schema. It cannot also be ' + + 'defined in this type extension.', + ); + + const inputAst = parse(` + extend input SomeInput { + fooArg: String + } + `); + expect(() => extendSchema(testSchema, inputAst)).to.throw( + 'Field "SomeInput.fooArg" already exists in the schema. It cannot also be ' + + 'defined in this type extension.', + ); }); it('does not allow referencing an unknown type', () => { - const ast = parse(` + const typeAst = parse(` extend type Bar { quix: Quix } `); - expect(() => extendSchema(testSchema, ast)).to.throw( + expect(() => extendSchema(testSchema, typeAst)).to.throw( + 'Unknown type: "Quix". Ensure that this type exists either in the ' + + 'original schema, or is added in a type definition.', + ); + + const unionAst = parse(` + extend union SomeUnion = Quix + `); + expect(() => extendSchema(testSchema, unionAst)).to.throw( + 'Unknown type: "Quix". Ensure that this type exists either in the ' + + 'original schema, or is added in a type definition.', + ); + + const inputAst = parse(` + extend input SomeInput { + quix: Quix + } + `); + expect(() => extendSchema(testSchema, inputAst)).to.throw( 'Unknown type: "Quix". Ensure that this type exists either in the ' + 'original schema, or is added in a type definition.', ); @@ -799,6 +1004,44 @@ describe('extendSchema', () => { ); }); + it('does not allow extending an unknown enum type', () => { + const ast = parse(` + extend enum UnknownEnumType { + BAZ + } + `); + expect(() => extendSchema(testSchema, ast)).to.throw( + 'Cannot extend type "UnknownEnumType" because it does not ' + + 'exist in the existing schema.', + ); + }); + + it('does not allow extending an unknown union type', () => { + const ast = parse(` + extend union UnknownUnionType = Baz + + type Baz { + foo: String + } + `); + expect(() => extendSchema(testSchema, ast)).to.throw( + 'Cannot extend type "UnknownUnionType" because it does not ' + + 'exist in the existing schema.', + ); + }); + + it('does not allow extending an unknown input object type', () => { + const ast = parse(` + extend input UnknownInputObjectType { + foo: String + } + `); + expect(() => extendSchema(testSchema, ast)).to.throw( + 'Cannot extend type "UnknownInputObjectType" because it does not ' + + 'exist in the existing schema.', + ); + }); + it('maintains configuration of the original schema object', () => { const testSchemaWithLegacyNames = new GraphQLSchema({ query: new GraphQLObjectType({ @@ -842,66 +1085,6 @@ describe('extendSchema', () => { ]); }); - it('extend enum', () => { - const extendedSchema = extendTestSchema(` - extend enum SomeEnum { - NEW_ENUM - } - - directive @test(arg: SomeEnum) on FIELD - `); - expect(extendedSchema).to.not.equal(testSchema); - - const oldEnum = testSchema.getType('SomeEnum'); - const newEnum = extendedSchema.getType('SomeEnum'); - const testDirective = extendedSchema.getDirective('test'); - - expect(oldEnum._values).to.have.lengthOf(2); - expect(newEnum._values).to.have.lengthOf(3); - expect(newEnum._values[2].name).to.equal('NEW_ENUM'); - expect(testDirective).to.have.nested.property('args[0].type', newEnum); - }); - - it('extend input', () => { - const extendedSchema = extendTestSchema(` - extend type Query { - newField(testArg: TestInput): String - } - - input TestInput { - testInputField: String - } - `); - const secondExtensionAST = parse(` - extend input TestInput { - newInputField: String - } - `); - const extendedTwiceSchema = extendSchema( - extendedSchema, - secondExtensionAST, - ); - - expect(extendedSchema).to.not.equal(testSchema); - expect(printSchema(extendedTwiceSchema)).to.contain('newInputField'); - expect(printSchema(extendedSchema)).to.not.contain('newInputField'); - expect(printSchema(testSchema)).to.not.contain('newInputField'); - }); - - it('extend union', () => { - const extendedSchema = extendTestSchema(` - extend union SomeUnion = TestNewType - - type TestNewType { - foo: String - } - `); - - expect(extendedSchema).to.not.equal(testSchema); - expect(printSchema(extendedSchema)).to.contain('Foo | Biz | TestNewType'); - expect(printSchema(testSchema)).to.not.contain('Foo | Biz | TestNewType'); - }); - describe('does not allow extending a non-object type', () => { it('not an object', () => { const ast = parse(` @@ -935,6 +1118,41 @@ describe('extendSchema', () => { 'Cannot extend non-object type "String".', ); }); + + it('not an enum', () => { + const ast = parse(` + extend enum Foo { + BAZ + } + `); + expect(() => extendSchema(testSchema, ast)).to.throw( + 'Cannot extend non-enum type "Foo".', + ); + }); + + it('not an union', () => { + const ast = parse(` + extend union Foo = Baz + + type Baz { + foo: String + } + `); + expect(() => extendSchema(testSchema, ast)).to.throw( + 'Cannot extend non-union type "Foo".', + ); + }); + + it('not an input object', () => { + const ast = parse(` + extend input Foo { + Baz: String + } + `); + expect(() => extendSchema(testSchema, ast)).to.throw( + 'Cannot extend non-input object type "Foo".', + ); + }); }); describe('can add additional root operation types', () => { From 5b896c6e33fc5e463bf5b971b76cb591864bbb1b Mon Sep 17 00:00:00 2001 From: Michael Grenier Date: Sun, 29 Apr 2018 12:45:46 -0400 Subject: [PATCH 11/13] Fix lint errors --- src/utilities/extendSchema.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/utilities/extendSchema.js b/src/utilities/extendSchema.js index 9d18c20864..d56e12bb27 100644 --- a/src/utilities/extendSchema.js +++ b/src/utilities/extendSchema.js @@ -591,18 +591,16 @@ function checkExtensionNode(type, node) { break; case Kind.ENUM_TYPE_EXTENSION: if (!isEnumType(type)) { - throw new GraphQLError( - `Cannot extend non-enum type "${type.name}".`, - [node], - ); + throw new GraphQLError(`Cannot extend non-enum type "${type.name}".`, [ + node, + ]); } break; case Kind.UNION_TYPE_EXTENSION: if (!isUnionType(type)) { - throw new GraphQLError( - `Cannot extend non-union type "${type.name}".`, - [node], - ); + throw new GraphQLError(`Cannot extend non-union type "${type.name}".`, [ + node, + ]); } break; case Kind.INPUT_OBJECT_TYPE_EXTENSION: From b9bbe8301d2d9ba2cdaca6a7026efccd48167914 Mon Sep 17 00:00:00 2001 From: Michael Grenier Date: Sun, 29 Apr 2018 15:55:32 -0400 Subject: [PATCH 12/13] Added missing description --- src/utilities/extendSchema.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utilities/extendSchema.js b/src/utilities/extendSchema.js index d56e12bb27..c2964090b5 100644 --- a/src/utilities/extendSchema.js +++ b/src/utilities/extendSchema.js @@ -378,6 +378,7 @@ export function extendSchema( : type.extensionASTNodes; return new GraphQLEnumType({ name, + description: type.description, values: extendValueMap(type), astNode: type.astNode, extensionASTNodes, From 717eb0c1b3ef0b1e235767c2c7dc44b389869ff3 Mon Sep 17 00:00:00 2001 From: mmahoney Date: Thu, 7 Jun 2018 16:38:30 -0400 Subject: [PATCH 13/13] fix enum value error test --- src/utilities/__tests__/extendSchema-test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utilities/__tests__/extendSchema-test.js b/src/utilities/__tests__/extendSchema-test.js index b2e98bcc1e..5a439d4a55 100644 --- a/src/utilities/__tests__/extendSchema-test.js +++ b/src/utilities/__tests__/extendSchema-test.js @@ -965,8 +965,8 @@ describe('extendSchema', () => { } `); expect(() => extendSchema(testSchema, enumAst)).to.throw( - 'Enum "SomeEnum.ONE" already exists in the schema. It cannot also be ' + - 'defined in this type extension.', + 'Enum value "SomeEnum.ONE" already exists in the schema. It cannot ' + + 'also be defined in this type extension.', ); const inputAst = parse(`