mirror of
https://github.com/swc-project/swc.git
synced 2024-12-26 07:02:28 +03:00
582 lines
19 KiB
TypeScript
582 lines
19 KiB
TypeScript
// Loaded from https://deno.land/x/graphql_deno@v15.0.0/lib/utilities/extendSchema.js
|
|
|
|
|
|
import objectValues from '../polyfills/objectValues.js';
|
|
import keyMap from '../jsutils/keyMap.js';
|
|
import inspect from '../jsutils/inspect.js';
|
|
import mapValue from '../jsutils/mapValue.js';
|
|
import invariant from '../jsutils/invariant.js';
|
|
import devAssert from '../jsutils/devAssert.js';
|
|
import { Kind } from '../language/kinds.js';
|
|
import { TokenKind } from '../language/tokenKind.js';
|
|
import { dedentBlockStringValue } from '../language/blockString.js';
|
|
import { isTypeDefinitionNode, isTypeExtensionNode } from '../language/predicates.js';
|
|
import { assertValidSDLExtension } from '../validation/validate.js';
|
|
import { getDirectiveValues } from '../execution/values.js';
|
|
import { specifiedScalarTypes, isSpecifiedScalarType } from '../type/scalars.js';
|
|
import { introspectionTypes, isIntrospectionType } from '../type/introspection.js';
|
|
import { GraphQLDirective, GraphQLDeprecatedDirective } from '../type/directives.js';
|
|
import { assertSchema, GraphQLSchema } from '../type/schema.js';
|
|
import { isScalarType, isObjectType, isInterfaceType, isUnionType, isListType, isNonNullType, isEnumType, isInputObjectType, GraphQLList, GraphQLNonNull, GraphQLScalarType, GraphQLObjectType, GraphQLInterfaceType, GraphQLUnionType, GraphQLEnumType, GraphQLInputObjectType } from '../type/definition.js';
|
|
import { valueFromAST } from './valueFromAST.js';
|
|
|
|
/**
|
|
* 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, documentAST, options) {
|
|
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, documentAST, options) {
|
|
// Collect the type definitions and extensions found in the document.
|
|
const typeDefs = [];
|
|
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 = [];
|
|
let schemaDef; // Schema extensions are collected which may add additional operation types.
|
|
|
|
const schemaExtensions = [];
|
|
|
|
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) {
|
|
if (isListType(type)) {
|
|
return new GraphQLList(replaceType(type.ofType));
|
|
} else if (isNonNullType(type)) {
|
|
return new GraphQLNonNull(replaceType(type.ofType));
|
|
}
|
|
|
|
return replaceNamedType(type);
|
|
}
|
|
|
|
function replaceNamedType(type) {
|
|
// 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];
|
|
}
|
|
|
|
function replaceDirective(directive) {
|
|
const config = directive.toConfig();
|
|
return new GraphQLDirective({ ...config,
|
|
args: mapValue(config.args, extendArg)
|
|
});
|
|
}
|
|
|
|
function extendNamedType(type) {
|
|
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);
|
|
}
|
|
|
|
if (isInputObjectType(type)) {
|
|
return extendInputObjectType(type);
|
|
} // Not reachable. All possible types have been considered.
|
|
|
|
|
|
invariant(false, 'Unexpected type: ' + inspect(type));
|
|
}
|
|
|
|
function extendInputObjectType(type) {
|
|
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) {
|
|
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) {
|
|
const config = type.toConfig();
|
|
const extensions = typeExtensionsMap[config.name] ?? [];
|
|
return new GraphQLScalarType({ ...config,
|
|
extensionASTNodes: config.extensionASTNodes.concat(extensions)
|
|
});
|
|
}
|
|
|
|
function extendObjectType(type) {
|
|
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) {
|
|
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) {
|
|
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) {
|
|
return { ...field,
|
|
type: replaceType(field.type),
|
|
args: mapValue(field.args, extendArg)
|
|
};
|
|
}
|
|
|
|
function extendArg(arg) {
|
|
return { ...arg,
|
|
type: replaceType(arg.type)
|
|
};
|
|
}
|
|
|
|
function getOperationTypes(nodes) {
|
|
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;
|
|
}
|
|
|
|
function getNamedType(node) {
|
|
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) {
|
|
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) {
|
|
const locations = node.locations.map(({
|
|
value
|
|
}) => value);
|
|
return new GraphQLDirective({
|
|
name: node.name.value,
|
|
description: getDescription(node, options),
|
|
locations,
|
|
isRepeatable: node.repeatable,
|
|
args: buildArgumentMap(node.arguments),
|
|
astNode: node
|
|
});
|
|
}
|
|
|
|
function buildFieldMap(nodes) {
|
|
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),
|
|
description: getDescription(field, options),
|
|
args: buildArgumentMap(field.arguments),
|
|
deprecationReason: getDeprecationReason(field),
|
|
astNode: field
|
|
};
|
|
}
|
|
}
|
|
|
|
return fieldConfigMap;
|
|
}
|
|
|
|
function buildArgumentMap(args) {
|
|
/* 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 = getWrappedType(arg.type);
|
|
argConfigMap[arg.name.value] = {
|
|
type,
|
|
description: getDescription(arg, options),
|
|
defaultValue: valueFromAST(arg.defaultValue, type),
|
|
astNode: arg
|
|
};
|
|
}
|
|
|
|
return argConfigMap;
|
|
}
|
|
|
|
function buildInputFieldMap(nodes) {
|
|
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 = getWrappedType(field.type);
|
|
inputFieldMap[field.name.value] = {
|
|
type,
|
|
description: getDescription(field, options),
|
|
defaultValue: valueFromAST(field.defaultValue, type),
|
|
astNode: field
|
|
};
|
|
}
|
|
}
|
|
|
|
return inputFieldMap;
|
|
}
|
|
|
|
function buildEnumValueMap(nodes) {
|
|
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) {
|
|
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));
|
|
}
|
|
}
|
|
|
|
return interfaces;
|
|
}
|
|
|
|
function buildUnionTypes(nodes) {
|
|
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));
|
|
}
|
|
}
|
|
|
|
return types;
|
|
}
|
|
|
|
function buildType(astNode) {
|
|
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;
|
|
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;
|
|
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;
|
|
const allNodes = [astNode, ...extensionASTNodes];
|
|
return new GraphQLEnumType({
|
|
name,
|
|
description,
|
|
values: buildEnumValueMap(allNodes),
|
|
astNode,
|
|
extensionASTNodes
|
|
});
|
|
}
|
|
|
|
case Kind.UNION_TYPE_DEFINITION:
|
|
{
|
|
const extensionASTNodes = extensionNodes;
|
|
const allNodes = [astNode, ...extensionASTNodes];
|
|
return new GraphQLUnionType({
|
|
name,
|
|
description,
|
|
types: () => buildUnionTypes(allNodes),
|
|
astNode,
|
|
extensionASTNodes
|
|
});
|
|
}
|
|
|
|
case Kind.SCALAR_TYPE_DEFINITION:
|
|
{
|
|
const extensionASTNodes = extensionNodes;
|
|
return new GraphQLScalarType({
|
|
name,
|
|
description,
|
|
astNode,
|
|
extensionASTNodes
|
|
});
|
|
}
|
|
|
|
case Kind.INPUT_OBJECT_TYPE_DEFINITION:
|
|
{
|
|
const extensionASTNodes = extensionNodes;
|
|
const allNodes = [astNode, ...extensionASTNodes];
|
|
return new GraphQLInputObjectType({
|
|
name,
|
|
description,
|
|
fields: () => buildInputFieldMap(allNodes),
|
|
astNode,
|
|
extensionASTNodes
|
|
});
|
|
}
|
|
} // Not reachable. All possible type definition nodes have been considered.
|
|
|
|
|
|
invariant(false, 'Unexpected type definition node: ' + inspect(astNode));
|
|
}
|
|
}
|
|
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) {
|
|
const deprecated = getDirectiveValues(GraphQLDeprecatedDirective, node);
|
|
return deprecated?.reason;
|
|
}
|
|
/**
|
|
* 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, options) {
|
|
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) {
|
|
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;
|
|
} |