diff --git a/src/utilities/__tests__/extendSchema-test.js b/src/utilities/__tests__/extendSchema-test.js index ab188d45a1..a85ad88326 100644 --- a/src/utilities/__tests__/extendSchema-test.js +++ b/src/utilities/__tests__/extendSchema-test.js @@ -1135,7 +1135,7 @@ describe('extendSchema', () => { const schema = extendTestSchema(` type Mutation `); - expect(schema.getMutationType()).to.equal(null); + expect(schema.getMutationType()).to.equal(undefined); }); it('adds schema definition missing in the original schema', () => { diff --git a/src/utilities/buildASTSchema.js b/src/utilities/buildASTSchema.js index 523b6d9096..bee0c2fa13 100644 --- a/src/utilities/buildASTSchema.js +++ b/src/utilities/buildASTSchema.js @@ -6,7 +6,6 @@ import keyMap from '../jsutils/keyMap'; import inspect from '../jsutils/inspect'; import invariant from '../jsutils/invariant'; import devAssert from '../jsutils/devAssert'; -import keyValMap from '../jsutils/keyValMap'; import { type ObjMap } from '../jsutils/ObjMap'; import { Kind } from '../language/kinds'; @@ -17,24 +16,28 @@ import { isTypeDefinitionNode } from '../language/predicates'; import { dedentBlockStringValue } from '../language/blockString'; import { type DirectiveLocationEnum } from '../language/directiveLocation'; import { + type Location, + type StringValueNode, type DocumentNode, - type NameNode, type TypeNode, type NamedTypeNode, type SchemaDefinitionNode, + type SchemaExtensionNode, type TypeDefinitionNode, - type ScalarTypeDefinitionNode, + type InterfaceTypeDefinitionNode, + type InterfaceTypeExtensionNode, type ObjectTypeDefinitionNode, + type ObjectTypeExtensionNode, + type UnionTypeDefinitionNode, + type UnionTypeExtensionNode, type FieldDefinitionNode, + type InputObjectTypeDefinitionNode, + type InputObjectTypeExtensionNode, type InputValueDefinitionNode, - type InterfaceTypeDefinitionNode, - type UnionTypeDefinitionNode, type EnumTypeDefinitionNode, + type EnumTypeExtensionNode, type EnumValueDefinitionNode, - type InputObjectTypeDefinitionNode, type DirectiveDefinitionNode, - type StringValueNode, - type Location, } from '../language/ast'; import { assertValidSDL } from '../validation/validate'; @@ -56,10 +59,10 @@ import { import { type GraphQLType, type GraphQLNamedType, - type GraphQLFieldConfig, - type GraphQLArgumentConfig, - type GraphQLEnumValueConfig, - type GraphQLInputFieldConfig, + type GraphQLFieldConfigMap, + type GraphQLEnumValueConfigMap, + type GraphQLInputFieldConfigMap, + type GraphQLFieldConfigArgumentMap, GraphQLScalarType, GraphQLObjectType, GraphQLInterfaceType, @@ -146,14 +149,16 @@ export function buildASTSchema( return type; }); - const typeMap = keyByNameNode(typeDefs, node => astBuilder.buildType(node)); - + const typeMap = astBuilder.buildTypeMap(typeDefs); const operationTypes = schemaDef - ? getOperationTypes(schemaDef) + ? astBuilder.getOperationTypes([schemaDef]) : { - query: 'Query', - mutation: 'Mutation', - subscription: 'Subscription', + // Note: While this could make early assertions to get the correctly + // typed values below, that would throw immediately while type system + // validation with validateSchema() will produce more actionable results. + query: (typeMap['Query']: any), + mutation: (typeMap['Mutation']: any), + subscription: (typeMap['Subscription']: any), }; const directives = directiveDefs.map(def => astBuilder.buildDirective(def)); @@ -172,30 +177,12 @@ export function buildASTSchema( } return new GraphQLSchema({ - // Note: While this could make early assertions to get the correctly - // typed values below, that would throw immediately while type system - // validation with validateSchema() will produce more actionable results. - query: operationTypes.query ? (typeMap[operationTypes.query]: any) : null, - mutation: operationTypes.mutation - ? (typeMap[operationTypes.mutation]: any) - : null, - subscription: operationTypes.subscription - ? (typeMap[operationTypes.subscription]: any) - : null, - + ...operationTypes, types: objectValues(typeMap), directives, astNode: schemaDef, assumeValid: options && options.assumeValid, }); - - function getOperationTypes(schema: SchemaDefinitionNode) { - const opTypes = {}; - for (const operationType of schema.operationTypes) { - opTypes[operationType.operation] = operationType.type.name.value; - } - return opTypes; - } } type TypeResolver = (typeName: string) => GraphQLNamedType; @@ -214,6 +201,28 @@ export class ASTDefinitionBuilder { this._resolveType = resolveType; } + getOperationTypes( + nodes: $ReadOnlyArray, + ): {| + query: ?GraphQLObjectType, + mutation: ?GraphQLObjectType, + subscription: ?GraphQLObjectType, + |} { + // Note: While this could make early assertions to get the correctly + // typed values below, that would throw immediately while type system + // validation with validateSchema() will produce more actionable results. + const opTypes: any = {}; + for (const node of nodes) { + if (node.operationTypes != null) { + for (const operationType of node.operationTypes) { + const typeName = operationType.type.name.value; + opTypes[operationType.operation] = this._resolveType(typeName); + } + } + } + return opTypes; + } + getNamedType(node: NamedTypeNode): GraphQLNamedType { const name = node.name.value; return stdTypeMap[name] || this._resolveType(name); @@ -239,77 +248,205 @@ export class ASTDefinitionBuilder { description: getDescription(directive, this._options), locations, isRepeatable: directive.repeatable, - args: keyByNameNode(directive.arguments || [], arg => this.buildArg(arg)), + args: this.buildArgumentMap(directive.arguments), astNode: directive, }); } - buildField(field: FieldDefinitionNode): GraphQLFieldConfig { - return { - // Note: While this could make assertions to get the correctly typed - // value, that would throw immediately while type system validation - // with validateSchema() will produce more actionable results. - type: (this.getWrappedType(field.type): any), - description: getDescription(field, this._options), - args: keyByNameNode(field.arguments || [], arg => this.buildArg(arg)), - deprecationReason: getDeprecationReason(field), - astNode: field, - }; + buildFieldMap( + nodes: $ReadOnlyArray< + | InterfaceTypeDefinitionNode + | InterfaceTypeExtensionNode + | ObjectTypeDefinitionNode + | ObjectTypeExtensionNode, + >, + ): GraphQLFieldConfigMap { + const fieldConfigMap = Object.create(null); + for (const node of nodes) { + if (node.fields != null) { + for (const field of node.fields) { + fieldConfigMap[field.name.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. + type: (this.getWrappedType(field.type): any), + description: getDescription(field, this._options), + args: this.buildArgumentMap(field.arguments), + deprecationReason: getDeprecationReason(field), + astNode: field, + }; + } + } + } + return fieldConfigMap; + } + + buildArgumentMap( + args: ?$ReadOnlyArray, + ): GraphQLFieldConfigArgumentMap { + const argConfigMap = Object.create(null); + if (args != null) { + for (const arg of args) { + // 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.getWrappedType(arg.type); + + argConfigMap[arg.name.value] = { + type, + description: getDescription(arg, this._options), + defaultValue: valueFromAST(arg.defaultValue, type), + astNode: arg, + }; + } + } + return argConfigMap; } - buildArg(value: InputValueDefinitionNode): GraphQLArgumentConfig { - // 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.getWrappedType(value.type); - return { - type, - description: getDescription(value, this._options), - defaultValue: valueFromAST(value.defaultValue, type), - astNode: value, - }; + buildInputFieldMap( + nodes: $ReadOnlyArray< + InputObjectTypeDefinitionNode | InputObjectTypeExtensionNode, + >, + ): GraphQLInputFieldConfigMap { + const inputFieldMap = Object.create(null); + for (const node of nodes) { + if (node.fields != null) { + for (const field of node.fields) { + // 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.getWrappedType(field.type); + + inputFieldMap[field.name.value] = { + type, + description: getDescription(field, this._options), + defaultValue: valueFromAST(field.defaultValue, type), + astNode: field, + }; + } + } + } + return inputFieldMap; } - buildInputField(value: InputValueDefinitionNode): GraphQLInputFieldConfig { - // 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.getWrappedType(value.type); - return { - type, - description: getDescription(value, this._options), - defaultValue: valueFromAST(value.defaultValue, type), - astNode: value, - }; + buildEnumValueMap( + nodes: $ReadOnlyArray, + ): GraphQLEnumValueConfigMap { + const enumValueMap = Object.create(null); + for (const node of nodes) { + if (node.values != null) { + for (const value of node.values) { + enumValueMap[value.name.value] = { + description: getDescription(value, this._options), + deprecationReason: getDeprecationReason(value), + astNode: value, + }; + } + } + } + return enumValueMap; } - buildEnumValue(value: EnumValueDefinitionNode): GraphQLEnumValueConfig { - return { - description: getDescription(value, this._options), - deprecationReason: getDeprecationReason(value), - astNode: value, - }; + buildInterfaces( + nodes: $ReadOnlyArray< + | InterfaceTypeDefinitionNode + | InterfaceTypeExtensionNode + | ObjectTypeDefinitionNode + | ObjectTypeExtensionNode, + >, + ): Array { + const interfaces = []; + for (const node of nodes) { + if (node.interfaces != null) { + for (const type of node.interfaces) { + // Note: While this could make assertions to get the correctly typed + // values below, that would throw immediately while type system + // validation with validateSchema() will produce more actionable + // results. + interfaces.push((this.getNamedType(type): any)); + } + } + } + return interfaces; } - buildType(astNode: TypeDefinitionNode): GraphQLNamedType { - const name = astNode.name.value; - if (stdTypeMap[name]) { - return stdTypeMap[name]; + buildUnionTypes( + nodes: $ReadOnlyArray, + ): Array { + const types = []; + for (const node of nodes) { + if (node.types != null) { + for (const type of node.types) { + // Note: While this could make assertions to get the correctly typed + // values below, that would throw immediately while type system + // validation with validateSchema() will produce more actionable + // results. + types.push((this.getNamedType(type): any)); + } + } } + return types; + } + + buildTypeMap( + nodes: $ReadOnlyArray, + ): ObjMap { + const typeMap = Object.create(null); + for (const node of nodes) { + const name = node.name.value; + typeMap[name] = stdTypeMap[name] || this._buildType(node); + } + return typeMap; + } + + _buildType(astNode: TypeDefinitionNode): GraphQLNamedType { + const name = astNode.name.value; + const description = getDescription(astNode, this._options); switch (astNode.kind) { case Kind.OBJECT_TYPE_DEFINITION: - return this._makeTypeDef(astNode); + return new GraphQLObjectType({ + name, + description, + interfaces: () => this.buildInterfaces([astNode]), + fields: () => this.buildFieldMap([astNode]), + astNode, + }); case Kind.INTERFACE_TYPE_DEFINITION: - return this._makeInterfaceDef(astNode); + return new GraphQLInterfaceType({ + name, + description, + interfaces: () => this.buildInterfaces([astNode]), + fields: () => this.buildFieldMap([astNode]), + astNode, + }); case Kind.ENUM_TYPE_DEFINITION: - return this._makeEnumDef(astNode); + return new GraphQLEnumType({ + name, + description, + values: this.buildEnumValueMap([astNode]), + astNode, + }); case Kind.UNION_TYPE_DEFINITION: - return this._makeUnionDef(astNode); + return new GraphQLUnionType({ + name, + description, + types: () => this.buildUnionTypes([astNode]), + astNode, + }); case Kind.SCALAR_TYPE_DEFINITION: - return this._makeScalarDef(astNode); + return new GraphQLScalarType({ + name, + description, + astNode, + }); case Kind.INPUT_OBJECT_TYPE_DEFINITION: - return this._makeInputObjectDef(astNode); + return new GraphQLInputObjectType({ + name, + description, + fields: () => this.buildInputFieldMap([astNode]), + astNode, + }); } // Not reachable. All possible type definition nodes have been considered. @@ -318,116 +455,6 @@ export class ASTDefinitionBuilder { 'Unexpected type definition node: ' + inspect((astNode: empty)), ); } - - _makeTypeDef(astNode: ObjectTypeDefinitionNode) { - const interfaceNodes = astNode.interfaces; - const fieldNodes = astNode.fields; - - // Note: While this could make assertions to get the correctly typed - // values below, that would throw immediately while type system - // validation with validateSchema() will produce more actionable results. - const interfaces = - interfaceNodes && interfaceNodes.length > 0 - ? () => interfaceNodes.map(ref => (this.getNamedType(ref): any)) - : []; - - const fields = - fieldNodes && fieldNodes.length > 0 - ? () => keyByNameNode(fieldNodes, field => this.buildField(field)) - : Object.create(null); - - return new GraphQLObjectType({ - name: astNode.name.value, - description: getDescription(astNode, this._options), - interfaces, - fields, - astNode, - }); - } - - _makeInterfaceDef(astNode: InterfaceTypeDefinitionNode) { - const interfaceNodes = astNode.interfaces; - const fieldNodes = astNode.fields; - - // Note: While this could make assertions to get the correctly typed - // values below, that would throw immediately while type system - // validation with validateSchema() will produce more actionable results. - const interfaces = - interfaceNodes && interfaceNodes.length > 0 - ? () => interfaceNodes.map(ref => (this.getNamedType(ref): any)) - : []; - - const fields = - fieldNodes && fieldNodes.length > 0 - ? () => keyByNameNode(fieldNodes, field => this.buildField(field)) - : Object.create(null); - - return new GraphQLInterfaceType({ - name: astNode.name.value, - description: getDescription(astNode, this._options), - interfaces, - fields, - astNode, - }); - } - - _makeEnumDef(astNode: EnumTypeDefinitionNode) { - const valueNodes = astNode.values || []; - - return new GraphQLEnumType({ - name: astNode.name.value, - description: getDescription(astNode, this._options), - values: keyByNameNode(valueNodes, value => this.buildEnumValue(value)), - astNode, - }); - } - - _makeUnionDef(astNode: UnionTypeDefinitionNode) { - const typeNodes = astNode.types; - - // Note: While this could make assertions to get the correctly typed - // values below, that would throw immediately while type system - // validation with validateSchema() will produce more actionable results. - const types = - typeNodes && typeNodes.length > 0 - ? () => typeNodes.map(ref => (this.getNamedType(ref): any)) - : []; - - return new GraphQLUnionType({ - name: astNode.name.value, - description: getDescription(astNode, this._options), - types, - astNode, - }); - } - - _makeScalarDef(astNode: ScalarTypeDefinitionNode) { - return new GraphQLScalarType({ - name: astNode.name.value, - description: getDescription(astNode, this._options), - astNode, - }); - } - - _makeInputObjectDef(def: InputObjectTypeDefinitionNode) { - const { fields } = def; - - return new GraphQLInputObjectType({ - name: def.name.value, - description: getDescription(def, this._options), - fields: fields - ? () => keyByNameNode(fields, field => this.buildInputField(field)) - : Object.create(null), - astNode: def, - }); - } -} - -function keyByNameNode( - list: $ReadOnlyArray, - valFn: (item: T) => V, -): ObjMap { - return keyValMap(list, ({ name }) => name.value, valFn); } /** diff --git a/src/utilities/extendSchema.js b/src/utilities/extendSchema.js index 0c41cb4764..010df4c3fc 100644 --- a/src/utilities/extendSchema.js +++ b/src/utilities/extendSchema.js @@ -1,13 +1,11 @@ // @flow strict -import flatMap from '../polyfills/flatMap'; import objectValues from '../polyfills/objectValues'; import inspect from '../jsutils/inspect'; import mapValue from '../jsutils/mapValue'; import invariant from '../jsutils/invariant'; import devAssert from '../jsutils/devAssert'; -import keyValMap from '../jsutils/keyValMap'; import { Kind } from '../language/kinds'; import { @@ -150,7 +148,6 @@ export function extendSchema( return schema; } - const schemaConfig = schema.toConfig(); const astBuilder = new ASTDefinitionBuilder(options, typeName => { const type = typeMap[typeName]; if (type === undefined) { @@ -159,46 +156,27 @@ export function extendSchema( return type; }); - const typeMap = keyValMap( - typeDefs, - node => node.name.value, - node => astBuilder.buildType(node), - ); + const typeMap = astBuilder.buildTypeMap(typeDefs); + const schemaConfig = schema.toConfig(); for (const existingType of schemaConfig.types) { typeMap[existingType.name] = extendNamedType(existingType); } - // Get the extended root operation types. const operationTypes = { - query: schemaConfig.query && schemaConfig.query.name, - mutation: schemaConfig.mutation && schemaConfig.mutation.name, - subscription: schemaConfig.subscription && schemaConfig.subscription.name, + // Get the extended root operation types. + query: schemaConfig.query && replaceNamedType(schemaConfig.query), + mutation: schemaConfig.mutation && replaceNamedType(schemaConfig.mutation), + subscription: + schemaConfig.subscription && replaceNamedType(schemaConfig.subscription), + // Then, incorporate schema definition and all schema extensions. + ...astBuilder.getOperationTypes( + concatMaybeArrays(schemaDef && [schemaDef], schemaExts) || [], + ), }; - if (schemaDef) { - for (const { operation, type } of schemaDef.operationTypes) { - operationTypes[operation] = type.name.value; - } - } - - // Then, incorporate schema definition and all schema extensions. - for (const schemaExt of schemaExts) { - if (schemaExt.operationTypes) { - for (const { operation, type } of schemaExt.operationTypes) { - operationTypes[operation] = type.name.value; - } - } - } - // Then produce and return a Schema with these types. return new GraphQLSchema({ - // 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. - query: (getMaybeTypeByName(operationTypes.query): any), - mutation: (getMaybeTypeByName(operationTypes.mutation): any), - subscription: (getMaybeTypeByName(operationTypes.subscription): any), - + ...operationTypes, types: objectValues(typeMap), directives: getMergedDirectives(), astNode: schemaDef || schemaConfig.astNode, @@ -221,17 +199,22 @@ export function extendSchema( } function replaceNamedType(type: T): T { + // 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. return ((typeMap[type.name]: any): T); } - function getMaybeTypeByName(typeName: ?string): ?GraphQLNamedType { - return typeName != null ? typeMap[typeName] : null; - } - function getMergedDirectives(): Array { - const existingDirectives = schema.getDirectives().map(extendDirective); - devAssert(existingDirectives, 'schema must have default directives'); + const existingDirectives = schema.getDirectives().map(directive => { + const config = directive.toConfig(); + return new GraphQLDirective({ + ...config, + args: mapValue(config.args, extendArg), + }); + }); + devAssert(existingDirectives, 'schema must have default directives'); return existingDirectives.concat( directiveDefs.map(node => astBuilder.buildDirective(node)), ); @@ -259,21 +242,11 @@ export function extendSchema( invariant(false, 'Unexpected type: ' + inspect((type: empty))); } - function extendDirective(directive: GraphQLDirective): GraphQLDirective { - const config = directive.toConfig(); - - return new GraphQLDirective({ - ...config, - args: mapValue(config.args, extendArg), - }); - } - function extendInputObjectType( type: GraphQLInputObjectType, ): GraphQLInputObjectType { const config = type.toConfig(); const extensions = typeExtsMap[config.name] || []; - const fieldNodes = flatMap(extensions, node => node.fields || []); return new GraphQLInputObjectType({ ...config, @@ -282,11 +255,7 @@ export function extendSchema( ...field, type: replaceType(field.type), })), - ...keyValMap( - fieldNodes, - field => field.name.value, - field => astBuilder.buildInputField(field), - ), + ...astBuilder.buildInputFieldMap(extensions), }), extensionASTNodes: concatMaybeArrays( config.extensionASTNodes, @@ -298,17 +267,12 @@ export function extendSchema( function extendEnumType(type: GraphQLEnumType): GraphQLEnumType { const config = type.toConfig(); const extensions = typeExtsMap[type.name] || []; - const valueNodes = flatMap(extensions, node => node.values || []); return new GraphQLEnumType({ ...config, values: { ...config.values, - ...keyValMap( - valueNodes, - value => value.name.value, - value => astBuilder.buildEnumValue(value), - ), + ...astBuilder.buildEnumValueMap(extensions), }, extensionASTNodes: concatMaybeArrays( config.extensionASTNodes, @@ -333,25 +297,16 @@ export function extendSchema( function extendObjectType(type: GraphQLObjectType): GraphQLObjectType { const config = type.toConfig(); const extensions = typeExtsMap[config.name] || []; - const interfaceNodes = flatMap(extensions, node => node.interfaces || []); - const fieldNodes = flatMap(extensions, node => node.fields || []); return new GraphQLObjectType({ ...config, interfaces: () => [ ...type.getInterfaces().map(replaceNamedType), - // 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. - ...interfaceNodes.map(node => (astBuilder.getNamedType(node): any)), + ...astBuilder.buildInterfaces(extensions), ], fields: () => ({ ...mapValue(config.fields, extendField), - ...keyValMap( - fieldNodes, - node => node.name.value, - node => astBuilder.buildField(node), - ), + ...astBuilder.buildFieldMap(extensions), }), extensionASTNodes: concatMaybeArrays( config.extensionASTNodes, @@ -365,25 +320,16 @@ export function extendSchema( ): GraphQLInterfaceType { const config = type.toConfig(); const extensions = typeExtsMap[config.name] || []; - const interfaceNodes = flatMap(extensions, node => node.interfaces || []); - const fieldNodes = flatMap(extensions, node => node.fields || []); return new GraphQLInterfaceType({ ...config, interfaces: () => [ ...type.getInterfaces().map(replaceNamedType), - // 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. - ...interfaceNodes.map(node => (astBuilder.getNamedType(node): any)), + ...astBuilder.buildInterfaces(extensions), ], fields: () => ({ ...mapValue(config.fields, extendField), - ...keyValMap( - fieldNodes, - node => node.name.value, - node => astBuilder.buildField(node), - ), + ...astBuilder.buildFieldMap(extensions), }), extensionASTNodes: concatMaybeArrays( config.extensionASTNodes, @@ -395,16 +341,12 @@ export function extendSchema( function extendUnionType(type: GraphQLUnionType): GraphQLUnionType { const config = type.toConfig(); const extensions = typeExtsMap[config.name] || []; - const typeNodes = flatMap(extensions, node => node.types || []); return new GraphQLUnionType({ ...config, types: () => [ ...type.getTypes().map(replaceNamedType), - // 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. - ...typeNodes.map(node => (astBuilder.getNamedType(node): any)), + ...astBuilder.buildUnionTypes(extensions), ], extensionASTNodes: concatMaybeArrays( config.extensionASTNodes,