123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407 |
- // @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 {
- isTypeDefinitionNode,
- isTypeExtensionNode,
- } from '../language/predicates';
- import {
- type DocumentNode,
- type DirectiveDefinitionNode,
- type SchemaExtensionNode,
- type SchemaDefinitionNode,
- } from '../language/ast';
- import { assertValidSDLExtension } from '../validation/validate';
- import { GraphQLDirective } from '../type/directives';
- import { isSpecifiedScalarType } from '../type/scalars';
- import { isIntrospectionType } from '../type/introspection';
- import {
- type GraphQLSchemaValidationOptions,
- assertSchema,
- GraphQLSchema,
- } from '../type/schema';
- import {
- type GraphQLNamedType,
- isScalarType,
- isObjectType,
- isInterfaceType,
- isUnionType,
- isListType,
- isNonNullType,
- isEnumType,
- isInputObjectType,
- GraphQLList,
- GraphQLNonNull,
- GraphQLScalarType,
- GraphQLObjectType,
- GraphQLInterfaceType,
- GraphQLUnionType,
- GraphQLEnumType,
- GraphQLInputObjectType,
- } from '../type/definition';
- import { ASTDefinitionBuilder } from './buildASTSchema';
- 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 && documentAST.kind === Kind.DOCUMENT,
- 'Must provide valid Document AST',
- );
- if (!options || !(options.assumeValid || options.assumeValidSDL)) {
- assertValidSDLExtension(documentAST, schema);
- }
- // Collect the type definitions and extensions found in the document.
- const typeDefs = [];
- const typeExtsMap = 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<DirectiveDefinitionNode> = [];
- let schemaDef: ?SchemaDefinitionNode;
- // Schema extensions are collected which may add additional operation types.
- const schemaExts: Array<SchemaExtensionNode> = [];
- for (const def of documentAST.definitions) {
- if (def.kind === Kind.SCHEMA_DEFINITION) {
- schemaDef = def;
- } else if (def.kind === Kind.SCHEMA_EXTENSION) {
- schemaExts.push(def);
- } else if (isTypeDefinitionNode(def)) {
- typeDefs.push(def);
- } else if (isTypeExtensionNode(def)) {
- const extendedTypeName = def.name.value;
- const existingTypeExts = typeExtsMap[extendedTypeName];
- typeExtsMap[extendedTypeName] = existingTypeExts
- ? existingTypeExts.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(typeExtsMap).length === 0 &&
- typeDefs.length === 0 &&
- directiveDefs.length === 0 &&
- schemaExts.length === 0 &&
- !schemaDef
- ) {
- return schema;
- }
- const schemaConfig = schema.toConfig();
- const astBuilder = new ASTDefinitionBuilder(options, typeName => {
- const type = typeMap[typeName];
- if (type === undefined) {
- throw new Error(`Unknown type: "${typeName}".`);
- }
- return type;
- });
- const typeMap = keyValMap(
- typeDefs,
- node => node.name.value,
- node => astBuilder.buildType(node),
- );
- 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,
- };
- 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;
- }
- }
- }
- // Support both original legacy names and extended legacy names.
- const allowedLegacyNames = schemaConfig.allowedLegacyNames.concat(
- (options && options.allowedLegacyNames) || [],
- );
- // 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),
- types: objectValues(typeMap),
- directives: getMergedDirectives(),
- astNode: schemaDef || schemaConfig.astNode,
- extensionASTNodes: schemaConfig.extensionASTNodes.concat(schemaExts),
- allowedLegacyNames,
- });
- // 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) {
- if (isListType(type)) {
- return new GraphQLList(replaceType(type.ofType));
- } else if (isNonNullType(type)) {
- return new GraphQLNonNull(replaceType(type.ofType));
- }
- return replaceNamedType(type);
- }
- function replaceNamedType<T: GraphQLNamedType>(type: T): T {
- return ((typeMap[type.name]: any): T);
- }
- function getMaybeTypeByName(typeName: ?string): ?GraphQLNamedType {
- return typeName ? typeMap[typeName] : null;
- }
- function getMergedDirectives(): Array<GraphQLDirective> {
- const existingDirectives = schema.getDirectives().map(extendDirective);
- devAssert(existingDirectives, 'schema must have default directives');
- return existingDirectives.concat(
- directiveDefs.map(node => astBuilder.buildDirective(node)),
- );
- }
- function extendNamedType(type: GraphQLNamedType): GraphQLNamedType {
- if (isIntrospectionType(type) || isSpecifiedScalarType(type)) {
- // Builtin types are not extended.
- return type;
- } else if (isScalarType(type)) {
- return extendScalarType(type);
- } else if (isObjectType(type)) {
- return extendObjectType(type);
- } else if (isInterfaceType(type)) {
- return extendInterfaceType(type);
- } else if (isUnionType(type)) {
- return extendUnionType(type);
- } else if (isEnumType(type)) {
- return extendEnumType(type);
- } else if (isInputObjectType(type)) {
- return extendInputObjectType(type);
- }
- // Not reachable. All possible types have been considered.
- 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,
- fields: () => ({
- ...mapValue(config.fields, field => ({
- ...field,
- type: replaceType(field.type),
- })),
- ...keyValMap(
- fieldNodes,
- field => field.name.value,
- field => astBuilder.buildInputField(field),
- ),
- }),
- extensionASTNodes: config.extensionASTNodes.concat(extensions),
- });
- }
- 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),
- ),
- },
- extensionASTNodes: config.extensionASTNodes.concat(extensions),
- });
- }
- function extendScalarType(type: GraphQLScalarType): GraphQLScalarType {
- const config = type.toConfig();
- const extensions = typeExtsMap[config.name] || [];
- return new GraphQLScalarType({
- ...config,
- extensionASTNodes: config.extensionASTNodes.concat(extensions),
- });
- }
- 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)),
- ],
- fields: () => ({
- ...mapValue(config.fields, extendField),
- ...keyValMap(
- fieldNodes,
- node => node.name.value,
- node => astBuilder.buildField(node),
- ),
- }),
- extensionASTNodes: config.extensionASTNodes.concat(extensions),
- });
- }
- function extendInterfaceType(
- type: GraphQLInterfaceType,
- ): GraphQLInterfaceType {
- const config = type.toConfig();
- const extensions = typeExtsMap[config.name] || [];
- const fieldNodes = flatMap(extensions, node => node.fields || []);
- return new GraphQLInterfaceType({
- ...config,
- fields: () => ({
- ...mapValue(config.fields, extendField),
- ...keyValMap(
- fieldNodes,
- node => node.name.value,
- node => astBuilder.buildField(node),
- ),
- }),
- extensionASTNodes: config.extensionASTNodes.concat(extensions),
- });
- }
- 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)),
- ],
- extensionASTNodes: config.extensionASTNodes.concat(extensions),
- });
- }
- function extendField(field) {
- return {
- ...field,
- type: replaceType(field.type),
- args: mapValue(field.args, extendArg),
- };
- }
- function extendArg(arg) {
- return {
- ...arg,
- type: replaceType(arg.type),
- };
- }
- }
|