// @flow strict import objectValues from '../polyfills/objectValues'; import keyMap from '../jsutils/keyMap'; import inspect from '../jsutils/inspect'; import mapValue from '../jsutils/mapValue'; import invariant from '../jsutils/invariant'; import devAssert from '../jsutils/devAssert'; import type { DirectiveLocationEnum } from '../language/directiveLocation'; import type { Location, DocumentNode, StringValueNode, TypeNode, NamedTypeNode, SchemaDefinitionNode, SchemaExtensionNode, TypeDefinitionNode, InterfaceTypeDefinitionNode, InterfaceTypeExtensionNode, ObjectTypeDefinitionNode, ObjectTypeExtensionNode, UnionTypeDefinitionNode, UnionTypeExtensionNode, FieldDefinitionNode, InputObjectTypeDefinitionNode, InputObjectTypeExtensionNode, InputValueDefinitionNode, EnumTypeDefinitionNode, EnumTypeExtensionNode, EnumValueDefinitionNode, DirectiveDefinitionNode, ScalarTypeDefinitionNode, ScalarTypeExtensionNode, } from '../language/ast'; import { Kind } from '../language/kinds'; import { TokenKind } from '../language/tokenKind'; import { dedentBlockStringValue } from '../language/blockString'; import { isTypeDefinitionNode, isTypeExtensionNode, } from '../language/predicates'; import { assertValidSDLExtension } from '../validation/validate'; import { getDirectiveValues } from '../execution/values'; import type { GraphQLSchemaValidationOptions, GraphQLSchemaNormalizedConfig, } from '../type/schema'; import type { GraphQLType, GraphQLNamedType, GraphQLFieldConfig, GraphQLFieldConfigMap, GraphQLArgumentConfig, GraphQLFieldConfigArgumentMap, GraphQLEnumValueConfigMap, GraphQLInputFieldConfigMap, } from '../type/definition'; import { assertSchema, GraphQLSchema } from '../type/schema'; import { specifiedScalarTypes, isSpecifiedScalarType } from '../type/scalars'; import { introspectionTypes, isIntrospectionType } from '../type/introspection'; import { GraphQLDirective, GraphQLDeprecatedDirective, GraphQLSpecifiedByDirective, } from '../type/directives'; import { isScalarType, isObjectType, isInterfaceType, isUnionType, isListType, isNonNullType, isEnumType, isInputObjectType, GraphQLList, GraphQLNonNull, GraphQLScalarType, GraphQLObjectType, GraphQLInterfaceType, GraphQLUnionType, GraphQLEnumType, GraphQLInputObjectType, } from '../type/definition'; import { valueFromAST } from './valueFromAST'; type Options = {| ...GraphQLSchemaValidationOptions, /** * Descriptions are defined as preceding string literals, however an older * experimental version of the SDL supported preceding comments as * descriptions. Set to true to enable this deprecated behavior. * This option is provided to ease adoption and will be removed in v16. * * Default: false */ commentDescriptions?: boolean, /** * Set to true to assume the SDL is valid. * * Default: false */ assumeValidSDL?: boolean, |}; /** * Produces a new schema given an existing schema and a document which may * contain GraphQL type extensions and definitions. The original schema will * remain unaltered. * * Because a schema represents a graph of references, a schema cannot be * extended without effectively making an entire copy. We do not know until it's * too late if subgraphs remain unchanged. * * This algorithm copies the provided schema, applying extensions while * producing the copy. The original schema remains unaltered. * * Accepts options as a third argument: * * - commentDescriptions: * Provide true to use preceding comments as the description. * */ export function extendSchema( schema: GraphQLSchema, documentAST: DocumentNode, options?: Options, ): GraphQLSchema { assertSchema(schema); devAssert( documentAST != null && documentAST.kind === Kind.DOCUMENT, 'Must provide valid Document AST.', ); if (options?.assumeValid !== true && options?.assumeValidSDL !== true) { assertValidSDLExtension(documentAST, schema); } const schemaConfig = schema.toConfig(); const extendedConfig = extendSchemaImpl(schemaConfig, documentAST, options); return schemaConfig === extendedConfig ? schema : new GraphQLSchema(extendedConfig); } /** * @internal */ export function extendSchemaImpl( schemaConfig: GraphQLSchemaNormalizedConfig, documentAST: DocumentNode, options?: Options, ): GraphQLSchemaNormalizedConfig { // Collect the type definitions and extensions found in the document. const typeDefs: Array = []; const typeExtensionsMap = Object.create(null); // New directives and types are separate because a directives and types can // have the same name. For example, a type named "skip". const directiveDefs: Array = []; let schemaDef: ?SchemaDefinitionNode; // Schema extensions are collected which may add additional operation types. const schemaExtensions: 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); } } // If this document contains no new types, extensions, or directives then // return the same unmodified GraphQLSchema instance. if ( Object.keys(typeExtensionsMap).length === 0 && typeDefs.length === 0 && directiveDefs.length === 0 && schemaExtensions.length === 0 && schemaDef == null ) { return schemaConfig; } const typeMap = Object.create(null); for (const existingType of schemaConfig.types) { typeMap[existingType.name] = extendNamedType(existingType); } for (const typeNode of typeDefs) { const name = typeNode.name.value; typeMap[name] = stdTypeMap[name] ?? buildType(typeNode); } const operationTypes = { // 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. ...(schemaDef && getOperationTypes([schemaDef])), ...getOperationTypes(schemaExtensions), }; // Then produce and return a Schema config with these types. return { description: schemaDef?.description?.value, ...operationTypes, types: objectValues(typeMap), directives: [ ...schemaConfig.directives.map(replaceDirective), ...directiveDefs.map(buildDirective), ], extensions: undefined, astNode: schemaDef ?? schemaConfig.astNode, extensionASTNodes: schemaConfig.extensionASTNodes.concat(schemaExtensions), assumeValid: options?.assumeValid ?? false, }; // Below are functions used for producing this schema that have closed over // this scope and have access to the schema, cache, and newly defined types. function replaceType(type: T): T { if (isListType(type)) { // $FlowFixMe[incompatible-return] return new GraphQLList(replaceType(type.ofType)); } if (isNonNullType(type)) { // $FlowFixMe[incompatible-return] return new GraphQLNonNull(replaceType(type.ofType)); } return replaceNamedType(type); } 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 replaceDirective(directive: GraphQLDirective): GraphQLDirective { const config = directive.toConfig(); return new GraphQLDirective({ ...config, args: mapValue(config.args, extendArg), }); } function extendNamedType(type: GraphQLNamedType): GraphQLNamedType { if (isIntrospectionType(type) || isSpecifiedScalarType(type)) { // Builtin types are not extended. return type; } if (isScalarType(type)) { return extendScalarType(type); } if (isObjectType(type)) { return extendObjectType(type); } if (isInterfaceType(type)) { return extendInterfaceType(type); } if (isUnionType(type)) { return extendUnionType(type); } if (isEnumType(type)) { return extendEnumType(type); } // istanbul ignore else (See: 'https://github.com/graphql/graphql-js/issues/2618') if (isInputObjectType(type)) { return extendInputObjectType(type); } // istanbul ignore next (Not reachable. All possible types have been considered) invariant(false, 'Unexpected type: ' + inspect((type: empty))); } function extendInputObjectType( type: GraphQLInputObjectType, ): GraphQLInputObjectType { const config = type.toConfig(); const extensions = typeExtensionsMap[config.name] ?? []; return new GraphQLInputObjectType({ ...config, fields: () => ({ ...mapValue(config.fields, (field) => ({ ...field, type: replaceType(field.type), })), ...buildInputFieldMap(extensions), }), extensionASTNodes: config.extensionASTNodes.concat(extensions), }); } function extendEnumType(type: GraphQLEnumType): GraphQLEnumType { const config = type.toConfig(); const extensions = typeExtensionsMap[type.name] ?? []; return new GraphQLEnumType({ ...config, values: { ...config.values, ...buildEnumValueMap(extensions), }, extensionASTNodes: config.extensionASTNodes.concat(extensions), }); } function extendScalarType(type: GraphQLScalarType): GraphQLScalarType { const config = type.toConfig(); const extensions = typeExtensionsMap[config.name] ?? []; let specifiedByUrl = config.specifiedByUrl; for (const extensionNode of extensions) { specifiedByUrl = getSpecifiedByUrl(extensionNode) ?? specifiedByUrl; } return new GraphQLScalarType({ ...config, specifiedByUrl, extensionASTNodes: config.extensionASTNodes.concat(extensions), }); } function extendObjectType(type: GraphQLObjectType): GraphQLObjectType { const config = type.toConfig(); const extensions = typeExtensionsMap[config.name] ?? []; return new GraphQLObjectType({ ...config, interfaces: () => [ ...type.getInterfaces().map(replaceNamedType), ...buildInterfaces(extensions), ], fields: () => ({ ...mapValue(config.fields, extendField), ...buildFieldMap(extensions), }), extensionASTNodes: config.extensionASTNodes.concat(extensions), }); } function extendInterfaceType( type: GraphQLInterfaceType, ): GraphQLInterfaceType { const config = type.toConfig(); const extensions = typeExtensionsMap[config.name] ?? []; return new GraphQLInterfaceType({ ...config, interfaces: () => [ ...type.getInterfaces().map(replaceNamedType), ...buildInterfaces(extensions), ], fields: () => ({ ...mapValue(config.fields, extendField), ...buildFieldMap(extensions), }), extensionASTNodes: config.extensionASTNodes.concat(extensions), }); } function extendUnionType(type: GraphQLUnionType): GraphQLUnionType { const config = type.toConfig(); const extensions = typeExtensionsMap[config.name] ?? []; return new GraphQLUnionType({ ...config, types: () => [ ...type.getTypes().map(replaceNamedType), ...buildUnionTypes(extensions), ], extensionASTNodes: config.extensionASTNodes.concat(extensions), }); } function extendField( field: GraphQLFieldConfig, ): GraphQLFieldConfig { return { ...field, type: replaceType(field.type), // $FlowFixMe[incompatible-call] args: mapValue(field.args, extendArg), }; } function extendArg(arg: GraphQLArgumentConfig) { return { ...arg, type: replaceType(arg.type), }; } function getOperationTypes( nodes: $ReadOnlyArray, ): {| query: ?GraphQLObjectType, mutation: ?GraphQLObjectType, subscription: ?GraphQLObjectType, |} { const opTypes = {}; for (const node of nodes) { // istanbul ignore next (See: 'https://github.com/graphql/graphql-js/issues/2203') const operationTypesNodes = node.operationTypes ?? []; for (const operationType of operationTypesNodes) { opTypes[operationType.operation] = getNamedType(operationType.type); } } // 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. return (opTypes: any); } function getNamedType(node: NamedTypeNode): GraphQLNamedType { const name = node.name.value; const type = stdTypeMap[name] ?? typeMap[name]; if (type === undefined) { throw new Error(`Unknown type: "${name}".`); } return type; } function getWrappedType(node: TypeNode): GraphQLType { if (node.kind === Kind.LIST_TYPE) { return new GraphQLList(getWrappedType(node.type)); } if (node.kind === Kind.NON_NULL_TYPE) { return new GraphQLNonNull(getWrappedType(node.type)); } return getNamedType(node); } function buildDirective(node: DirectiveDefinitionNode): GraphQLDirective { const locations = node.locations.map( ({ value }) => ((value: any): DirectiveLocationEnum), ); return new GraphQLDirective({ name: node.name.value, description: getDescription(node, options), locations, isRepeatable: node.repeatable, args: buildArgumentMap(node.arguments), astNode: node, }); } function buildFieldMap( nodes: $ReadOnlyArray< | InterfaceTypeDefinitionNode | InterfaceTypeExtensionNode | ObjectTypeDefinitionNode | ObjectTypeExtensionNode, >, ): GraphQLFieldConfigMap { const fieldConfigMap = Object.create(null); for (const node of nodes) { // istanbul ignore next (See: 'https://github.com/graphql/graphql-js/issues/2203') const nodeFields = node.fields ?? []; for (const field of nodeFields) { 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: (getWrappedType(field.type): any), description: getDescription(field, options), args: buildArgumentMap(field.arguments), deprecationReason: getDeprecationReason(field), astNode: field, }; } } return fieldConfigMap; } function buildArgumentMap( args: ?$ReadOnlyArray, ): GraphQLFieldConfigArgumentMap { // istanbul ignore next (See: 'https://github.com/graphql/graphql-js/issues/2203') const argsNodes = args ?? []; const argConfigMap = Object.create(null); for (const arg of argsNodes) { // 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 = getWrappedType(arg.type); argConfigMap[arg.name.value] = { type, description: getDescription(arg, options), defaultValue: valueFromAST(arg.defaultValue, type), deprecationReason: getDeprecationReason(arg), astNode: arg, }; } return argConfigMap; } function buildInputFieldMap( nodes: $ReadOnlyArray< InputObjectTypeDefinitionNode | InputObjectTypeExtensionNode, >, ): GraphQLInputFieldConfigMap { const inputFieldMap = Object.create(null); for (const node of nodes) { // istanbul ignore next (See: 'https://github.com/graphql/graphql-js/issues/2203') const fieldsNodes = node.fields ?? []; for (const field of fieldsNodes) { // 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 = getWrappedType(field.type); inputFieldMap[field.name.value] = { type, description: getDescription(field, options), defaultValue: valueFromAST(field.defaultValue, type), deprecationReason: getDeprecationReason(field), astNode: field, }; } } return inputFieldMap; } function buildEnumValueMap( nodes: $ReadOnlyArray, ): GraphQLEnumValueConfigMap { const enumValueMap = Object.create(null); for (const node of nodes) { // istanbul ignore next (See: 'https://github.com/graphql/graphql-js/issues/2203') const valuesNodes = node.values ?? []; for (const value of valuesNodes) { enumValueMap[value.name.value] = { description: getDescription(value, options), deprecationReason: getDeprecationReason(value), astNode: value, }; } } return enumValueMap; } function buildInterfaces( nodes: $ReadOnlyArray< | InterfaceTypeDefinitionNode | InterfaceTypeExtensionNode | ObjectTypeDefinitionNode | ObjectTypeExtensionNode, >, ): Array { const interfaces = []; for (const node of nodes) { // istanbul ignore next (See: 'https://github.com/graphql/graphql-js/issues/2203') const interfacesNodes = node.interfaces ?? []; for (const type of interfacesNodes) { // 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((getNamedType(type): any)); } } return interfaces; } function buildUnionTypes( nodes: $ReadOnlyArray, ): Array { const types = []; for (const node of nodes) { // istanbul ignore next (See: 'https://github.com/graphql/graphql-js/issues/2203') const typeNodes = node.types ?? []; for (const type of typeNodes) { // 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((getNamedType(type): any)); } } return types; } function buildType(astNode: TypeDefinitionNode): GraphQLNamedType { const name = astNode.name.value; const description = getDescription(astNode, options); const extensionNodes = typeExtensionsMap[name] ?? []; switch (astNode.kind) { case Kind.OBJECT_TYPE_DEFINITION: { const extensionASTNodes = (extensionNodes: any); const allNodes = [astNode, ...extensionASTNodes]; return new GraphQLObjectType({ name, description, interfaces: () => buildInterfaces(allNodes), fields: () => buildFieldMap(allNodes), astNode, extensionASTNodes, }); } case Kind.INTERFACE_TYPE_DEFINITION: { const extensionASTNodes = (extensionNodes: any); const allNodes = [astNode, ...extensionASTNodes]; return new GraphQLInterfaceType({ name, description, interfaces: () => buildInterfaces(allNodes), fields: () => buildFieldMap(allNodes), astNode, extensionASTNodes, }); } case Kind.ENUM_TYPE_DEFINITION: { const extensionASTNodes = (extensionNodes: any); const allNodes = [astNode, ...extensionASTNodes]; return new GraphQLEnumType({ name, description, values: buildEnumValueMap(allNodes), astNode, extensionASTNodes, }); } case Kind.UNION_TYPE_DEFINITION: { const extensionASTNodes = (extensionNodes: any); const allNodes = [astNode, ...extensionASTNodes]; return new GraphQLUnionType({ name, description, types: () => buildUnionTypes(allNodes), astNode, extensionASTNodes, }); } case Kind.SCALAR_TYPE_DEFINITION: { const extensionASTNodes = (extensionNodes: any); return new GraphQLScalarType({ name, description, specifiedByUrl: getSpecifiedByUrl(astNode), astNode, extensionASTNodes, }); } case Kind.INPUT_OBJECT_TYPE_DEFINITION: { const extensionASTNodes = (extensionNodes: any); const allNodes = [astNode, ...extensionASTNodes]; return new GraphQLInputObjectType({ name, description, fields: () => buildInputFieldMap(allNodes), astNode, extensionASTNodes, }); } } // istanbul ignore next (Not reachable. All possible type definition nodes have been considered) invariant( false, 'Unexpected type definition node: ' + inspect((astNode: empty)), ); } } const stdTypeMap = keyMap( specifiedScalarTypes.concat(introspectionTypes), (type) => type.name, ); /** * Given a field or enum value node, returns the string value for the * deprecation reason. */ function getDeprecationReason( node: | EnumValueDefinitionNode | FieldDefinitionNode | InputValueDefinitionNode, ): ?string { const deprecated = getDirectiveValues(GraphQLDeprecatedDirective, node); return (deprecated?.reason: any); } /** * Given a scalar node, returns the string value for the specifiedByUrl. */ function getSpecifiedByUrl( node: ScalarTypeDefinitionNode | ScalarTypeExtensionNode, ): ?string { const specifiedBy = getDirectiveValues(GraphQLSpecifiedByDirective, node); return (specifiedBy?.url: any); } /** * Given an ast node, returns its string description. * @deprecated: provided to ease adoption and will be removed in v16. * * Accepts options as a second argument: * * - commentDescriptions: * Provide true to use preceding comments as the description. * */ export function getDescription( node: { +description?: StringValueNode, +loc?: Location, ... }, options: ?{ commentDescriptions?: boolean, ... }, ): void | string { if (node.description) { return node.description.value; } if (options?.commentDescriptions === true) { const rawValue = getLeadingCommentBlock(node); if (rawValue !== undefined) { return dedentBlockStringValue('\n' + rawValue); } } } function getLeadingCommentBlock(node): void | string { const loc = node.loc; if (!loc) { return; } const comments = []; let token = loc.startToken.prev; while ( token != null && token.kind === TokenKind.COMMENT && token.next && token.prev && token.line + 1 === token.next.line && token.line !== token.prev.line ) { const value = String(token.value); comments.push(value); token = token.prev; } return comments.length > 0 ? comments.reverse().join('\n') : undefined; }