@graphql-tools/utils
Version:
Common package containing utils and types for GraphQL tools
178 lines (177 loc) • 6.92 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.healSchema = healSchema;
exports.healTypes = healTypes;
const graphql_1 = require("graphql");
// Update any references to named schema types that disagree with the named
// types found in schema.getTypeMap().
//
// healSchema and its callers (visitSchema/visitSchemaDirectives) all modify the schema in place.
// Therefore, private variables (such as the stored implementation map and the proper root types)
// are not updated.
//
// If this causes issues, the schema could be more aggressively healed as follows:
//
// healSchema(schema);
// const config = schema.toConfig()
// const healedSchema = new GraphQLSchema({
// ...config,
// query: schema.getType('<desired new root query type name>'),
// mutation: schema.getType('<desired new root mutation type name>'),
// subscription: schema.getType('<desired new root subscription type name>'),
// });
//
// One can then also -- if necessary -- assign the correct private variables to the initial schema
// as follows:
// Object.assign(schema, healedSchema);
//
// These steps are not taken automatically to preserve backwards compatibility with graphql-tools v4.
// See https://github.com/ardatan/graphql-tools/issues/1462
//
// They were briefly taken in v5, but can now be phased out as they were only required when other
// areas of the codebase were using healSchema and visitSchema more extensively.
//
function healSchema(schema) {
healTypes(schema.getTypeMap(), schema.getDirectives());
return schema;
}
function healTypes(originalTypeMap, directives) {
const actualNamedTypeMap = Object.create(null);
// If any of the .name properties of the GraphQLNamedType objects in
// schema.getTypeMap() have changed, the keys of the type map need to
// be updated accordingly.
for (const typeName in originalTypeMap) {
const namedType = originalTypeMap[typeName];
if (namedType == null || typeName.startsWith('__')) {
continue;
}
const actualName = namedType.name;
if (actualName.startsWith('__')) {
continue;
}
if (actualNamedTypeMap[actualName] != null) {
console.warn(`Duplicate schema type name ${actualName} found; keeping the existing one found in the schema`);
continue;
}
actualNamedTypeMap[actualName] = namedType;
// Note: we are deliberately leaving namedType in the schema by its
// original name (which might be different from actualName), so that
// references by that name can be healed.
}
// Now add back every named type by its actual name.
for (const typeName in actualNamedTypeMap) {
const namedType = actualNamedTypeMap[typeName];
originalTypeMap[typeName] = namedType;
}
// Directive declaration argument types can refer to named types.
for (const decl of directives) {
decl.args = decl.args.filter(arg => {
arg.type = healType(arg.type);
return arg.type !== null;
});
}
for (const typeName in originalTypeMap) {
const namedType = originalTypeMap[typeName];
// Heal all named types, except for dangling references, kept only to redirect.
if (!typeName.startsWith('__') && typeName in actualNamedTypeMap) {
if (namedType != null) {
healNamedType(namedType);
}
}
}
for (const typeName in originalTypeMap) {
if (!typeName.startsWith('__') && !(typeName in actualNamedTypeMap)) {
delete originalTypeMap[typeName];
}
}
function healNamedType(type) {
if ((0, graphql_1.isObjectType)(type)) {
healFields(type);
healInterfaces(type);
return;
}
else if ((0, graphql_1.isInterfaceType)(type)) {
healFields(type);
if ('getInterfaces' in type) {
healInterfaces(type);
}
return;
}
else if ((0, graphql_1.isUnionType)(type)) {
healUnderlyingTypes(type);
return;
}
else if ((0, graphql_1.isInputObjectType)(type)) {
healInputFields(type);
return;
}
else if ((0, graphql_1.isLeafType)(type)) {
return;
}
throw new Error(`Unexpected schema type: ${type}`);
}
function healFields(type) {
const fieldMap = type.getFields();
for (const [key, field] of Object.entries(fieldMap)) {
field.args
.map(arg => {
arg.type = healType(arg.type);
return arg.type === null ? null : arg;
})
.filter(Boolean);
field.type = healType(field.type);
if (field.type === null) {
delete fieldMap[key];
}
}
}
function healInterfaces(type) {
if ('getInterfaces' in type) {
const interfaces = type.getInterfaces();
interfaces.push(...interfaces
.splice(0)
.map(iface => healType(iface))
.filter(Boolean));
}
}
function healInputFields(type) {
const fieldMap = type.getFields();
for (const [key, field] of Object.entries(fieldMap)) {
field.type = healType(field.type);
if (field.type === null) {
delete fieldMap[key];
}
}
}
function healUnderlyingTypes(type) {
const types = type.getTypes();
types.push(...types
.splice(0)
.map(t => healType(t))
.filter(Boolean));
}
function healType(type) {
// Unwrap the two known wrapper types
if ((0, graphql_1.isListType)(type)) {
const healedType = healType(type.ofType);
return healedType != null ? new graphql_1.GraphQLList(healedType) : null;
}
else if ((0, graphql_1.isNonNullType)(type)) {
const healedType = healType(type.ofType);
return healedType != null ? new graphql_1.GraphQLNonNull(healedType) : null;
}
else if ((0, graphql_1.isNamedType)(type)) {
// If a type annotation on a field or an argument or a union member is
// any `GraphQLNamedType` with a `name`, then it must end up identical
// to `schema.getType(name)`, since `schema.getTypeMap()` is the source
// of truth for all named schema types.
// Note that new types can still be simply added by adding a field, as
// the official type will be undefined, not null.
const officialType = originalTypeMap[type.name];
if (officialType && type !== officialType) {
return officialType;
}
}
return type;
}
}
;