graphql
Version:
A Query Language and Runtime which can target any service.
408 lines (358 loc) • 12.8 kB
Flow
// @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),
};
}
}