All Downloads are FREE. Search and download functionalities are using the official Maven repository.

node_modules.graphql.utilities.findBreakingChanges.js.flow Maven / Gradle / Ivy

There is a newer version: 3.3.1
Show newest version
/**
 * Copyright (c) 2016-present, Facebook, Inc.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow strict
 */

import {
  isScalarType,
  isObjectType,
  isInterfaceType,
  isUnionType,
  isEnumType,
  isInputObjectType,
  isNonNullType,
  isListType,
  isNamedType,
} from '../type/definition';

import type {
  GraphQLNamedType,
  GraphQLFieldMap,
  GraphQLType,
  GraphQLArgument,
} from '../type/definition';

import { GraphQLDirective } from '../type/directives';
import { GraphQLSchema } from '../type/schema';
import keyMap from '../jsutils/keyMap';

import type { ObjMap } from '../jsutils/ObjMap';
import type { DirectiveLocationEnum } from '../language/directiveLocation';

export const BreakingChangeType = {
  FIELD_CHANGED_KIND: 'FIELD_CHANGED_KIND',
  FIELD_REMOVED: 'FIELD_REMOVED',
  TYPE_CHANGED_KIND: 'TYPE_CHANGED_KIND',
  TYPE_REMOVED: 'TYPE_REMOVED',
  TYPE_REMOVED_FROM_UNION: 'TYPE_REMOVED_FROM_UNION',
  VALUE_REMOVED_FROM_ENUM: 'VALUE_REMOVED_FROM_ENUM',
  ARG_REMOVED: 'ARG_REMOVED',
  ARG_CHANGED_KIND: 'ARG_CHANGED_KIND',
  NON_NULL_ARG_ADDED: 'NON_NULL_ARG_ADDED',
  NON_NULL_INPUT_FIELD_ADDED: 'NON_NULL_INPUT_FIELD_ADDED',
  INTERFACE_REMOVED_FROM_OBJECT: 'INTERFACE_REMOVED_FROM_OBJECT',
  DIRECTIVE_REMOVED: 'DIRECTIVE_REMOVED',
  DIRECTIVE_ARG_REMOVED: 'DIRECTIVE_ARG_REMOVED',
  DIRECTIVE_LOCATION_REMOVED: 'DIRECTIVE_LOCATION_REMOVED',
  NON_NULL_DIRECTIVE_ARG_ADDED: 'NON_NULL_DIRECTIVE_ARG_ADDED',
};

export const DangerousChangeType = {
  ARG_DEFAULT_VALUE_CHANGE: 'ARG_DEFAULT_VALUE_CHANGE',
  VALUE_ADDED_TO_ENUM: 'VALUE_ADDED_TO_ENUM',
  INTERFACE_ADDED_TO_OBJECT: 'INTERFACE_ADDED_TO_OBJECT',
  TYPE_ADDED_TO_UNION: 'TYPE_ADDED_TO_UNION',
  NULLABLE_INPUT_FIELD_ADDED: 'NULLABLE_INPUT_FIELD_ADDED',
  NULLABLE_ARG_ADDED: 'NULLABLE_ARG_ADDED',
};

export type BreakingChange = {
  type: $Keys,
  description: string,
};

export type DangerousChange = {
  type: $Keys,
  description: string,
};

/**
 * Given two schemas, returns an Array containing descriptions of all the types
 * of breaking changes covered by the other functions down below.
 */
export function findBreakingChanges(
  oldSchema: GraphQLSchema,
  newSchema: GraphQLSchema,
): Array {
  return [
    ...findRemovedTypes(oldSchema, newSchema),
    ...findTypesThatChangedKind(oldSchema, newSchema),
    ...findFieldsThatChangedTypeOnObjectOrInterfaceTypes(oldSchema, newSchema),
    ...findFieldsThatChangedTypeOnInputObjectTypes(oldSchema, newSchema)
      .breakingChanges,
    ...findTypesRemovedFromUnions(oldSchema, newSchema),
    ...findValuesRemovedFromEnums(oldSchema, newSchema),
    ...findArgChanges(oldSchema, newSchema).breakingChanges,
    ...findInterfacesRemovedFromObjectTypes(oldSchema, newSchema),
    ...findRemovedDirectives(oldSchema, newSchema),
    ...findRemovedDirectiveArgs(oldSchema, newSchema),
    ...findAddedNonNullDirectiveArgs(oldSchema, newSchema),
    ...findRemovedDirectiveLocations(oldSchema, newSchema),
  ];
}

/**
 * Given two schemas, returns an Array containing descriptions of all the types
 * of potentially dangerous changes covered by the other functions down below.
 */
export function findDangerousChanges(
  oldSchema: GraphQLSchema,
  newSchema: GraphQLSchema,
): Array {
  return [
    ...findArgChanges(oldSchema, newSchema).dangerousChanges,
    ...findValuesAddedToEnums(oldSchema, newSchema),
    ...findInterfacesAddedToObjectTypes(oldSchema, newSchema),
    ...findTypesAddedToUnions(oldSchema, newSchema),
    ...findFieldsThatChangedTypeOnInputObjectTypes(oldSchema, newSchema)
      .dangerousChanges,
  ];
}

/**
 * Given two schemas, returns an Array containing descriptions of any breaking
 * changes in the newSchema related to removing an entire type.
 */
export function findRemovedTypes(
  oldSchema: GraphQLSchema,
  newSchema: GraphQLSchema,
): Array {
  const oldTypeMap = oldSchema.getTypeMap();
  const newTypeMap = newSchema.getTypeMap();

  const breakingChanges = [];
  Object.keys(oldTypeMap).forEach(typeName => {
    if (!newTypeMap[typeName]) {
      breakingChanges.push({
        type: BreakingChangeType.TYPE_REMOVED,
        description: `${typeName} was removed.`,
      });
    }
  });
  return breakingChanges;
}

/**
 * Given two schemas, returns an Array containing descriptions of any breaking
 * changes in the newSchema related to changing the type of a type.
 */
export function findTypesThatChangedKind(
  oldSchema: GraphQLSchema,
  newSchema: GraphQLSchema,
): Array {
  const oldTypeMap = oldSchema.getTypeMap();
  const newTypeMap = newSchema.getTypeMap();

  const breakingChanges = [];
  Object.keys(oldTypeMap).forEach(typeName => {
    if (!newTypeMap[typeName]) {
      return;
    }
    const oldType = oldTypeMap[typeName];
    const newType = newTypeMap[typeName];
    if (oldType.constructor !== newType.constructor) {
      breakingChanges.push({
        type: BreakingChangeType.TYPE_CHANGED_KIND,
        description:
          `${typeName} changed from ` +
          `${typeKindName(oldType)} to ${typeKindName(newType)}.`,
      });
    }
  });
  return breakingChanges;
}

/**
 * Given two schemas, returns an Array containing descriptions of any
 * breaking or dangerous changes in the newSchema related to arguments
 * (such as removal or change of type of an argument, or a change in an
 * argument's default value).
 */
export function findArgChanges(
  oldSchema: GraphQLSchema,
  newSchema: GraphQLSchema,
): {
  breakingChanges: Array,
  dangerousChanges: Array,
} {
  const oldTypeMap = oldSchema.getTypeMap();
  const newTypeMap = newSchema.getTypeMap();

  const breakingChanges = [];
  const dangerousChanges = [];

  Object.keys(oldTypeMap).forEach(typeName => {
    const oldType = oldTypeMap[typeName];
    const newType = newTypeMap[typeName];
    if (
      !(isObjectType(oldType) || isInterfaceType(oldType)) ||
      !(isObjectType(newType) || isInterfaceType(newType)) ||
      newType.constructor !== oldType.constructor
    ) {
      return;
    }

    const oldTypeFields: GraphQLFieldMap<*, *> = oldType.getFields();
    const newTypeFields: GraphQLFieldMap<*, *> = newType.getFields();

    Object.keys(oldTypeFields).forEach(fieldName => {
      if (!newTypeFields[fieldName]) {
        return;
      }

      oldTypeFields[fieldName].args.forEach(oldArgDef => {
        const newArgs = newTypeFields[fieldName].args;
        const newArgDef = newArgs.find(arg => arg.name === oldArgDef.name);

        // Arg not present
        if (!newArgDef) {
          breakingChanges.push({
            type: BreakingChangeType.ARG_REMOVED,
            description:
              `${oldType.name}.${fieldName} arg ` +
              `${oldArgDef.name} was removed`,
          });
        } else {
          const isSafe = isChangeSafeForInputObjectFieldOrFieldArg(
            oldArgDef.type,
            newArgDef.type,
          );
          if (!isSafe) {
            breakingChanges.push({
              type: BreakingChangeType.ARG_CHANGED_KIND,
              description:
                `${oldType.name}.${fieldName} arg ` +
                `${oldArgDef.name} has changed type from ` +
                `${oldArgDef.type.toString()} to ${newArgDef.type.toString()}`,
            });
          } else if (
            oldArgDef.defaultValue !== undefined &&
            oldArgDef.defaultValue !== newArgDef.defaultValue
          ) {
            dangerousChanges.push({
              type: DangerousChangeType.ARG_DEFAULT_VALUE_CHANGE,
              description:
                `${oldType.name}.${fieldName} arg ` +
                `${oldArgDef.name} has changed defaultValue`,
            });
          }
        }
      });
      // Check if a non-null arg was added to the field
      newTypeFields[fieldName].args.forEach(newArgDef => {
        const oldArgs = oldTypeFields[fieldName].args;
        const oldArgDef = oldArgs.find(arg => arg.name === newArgDef.name);
        if (!oldArgDef) {
          if (isNonNullType(newArgDef.type)) {
            breakingChanges.push({
              type: BreakingChangeType.NON_NULL_ARG_ADDED,
              description:
                `A non-null arg ${newArgDef.name} on ` +
                `${newType.name}.${fieldName} was added`,
            });
          } else {
            dangerousChanges.push({
              type: DangerousChangeType.NULLABLE_ARG_ADDED,
              description:
                `A nullable arg ${newArgDef.name} on ` +
                `${newType.name}.${fieldName} was added`,
            });
          }
        }
      });
    });
  });

  return {
    breakingChanges,
    dangerousChanges,
  };
}

function typeKindName(type: GraphQLNamedType): string {
  if (isScalarType(type)) {
    return 'a Scalar type';
  }
  if (isObjectType(type)) {
    return 'an Object type';
  }
  if (isInterfaceType(type)) {
    return 'an Interface type';
  }
  if (isUnionType(type)) {
    return 'a Union type';
  }
  if (isEnumType(type)) {
    return 'an Enum type';
  }
  if (isInputObjectType(type)) {
    return 'an Input type';
  }
  throw new TypeError('Unknown type ' + type.constructor.name);
}

export function findFieldsThatChangedTypeOnObjectOrInterfaceTypes(
  oldSchema: GraphQLSchema,
  newSchema: GraphQLSchema,
): Array {
  const oldTypeMap = oldSchema.getTypeMap();
  const newTypeMap = newSchema.getTypeMap();

  const breakingChanges = [];
  Object.keys(oldTypeMap).forEach(typeName => {
    const oldType = oldTypeMap[typeName];
    const newType = newTypeMap[typeName];
    if (
      !(isObjectType(oldType) || isInterfaceType(oldType)) ||
      !(isObjectType(newType) || isInterfaceType(newType)) ||
      newType.constructor !== oldType.constructor
    ) {
      return;
    }

    const oldTypeFieldsDef = oldType.getFields();
    const newTypeFieldsDef = newType.getFields();
    Object.keys(oldTypeFieldsDef).forEach(fieldName => {
      // Check if the field is missing on the type in the new schema.
      if (!(fieldName in newTypeFieldsDef)) {
        breakingChanges.push({
          type: BreakingChangeType.FIELD_REMOVED,
          description: `${typeName}.${fieldName} was removed.`,
        });
      } else {
        const oldFieldType = oldTypeFieldsDef[fieldName].type;
        const newFieldType = newTypeFieldsDef[fieldName].type;
        const isSafe = isChangeSafeForObjectOrInterfaceField(
          oldFieldType,
          newFieldType,
        );
        if (!isSafe) {
          const oldFieldTypeString = isNamedType(oldFieldType)
            ? oldFieldType.name
            : oldFieldType.toString();
          const newFieldTypeString = isNamedType(newFieldType)
            ? newFieldType.name
            : newFieldType.toString();
          breakingChanges.push({
            type: BreakingChangeType.FIELD_CHANGED_KIND,
            description:
              `${typeName}.${fieldName} changed type from ` +
              `${oldFieldTypeString} to ${newFieldTypeString}.`,
          });
        }
      }
    });
  });
  return breakingChanges;
}

export function findFieldsThatChangedTypeOnInputObjectTypes(
  oldSchema: GraphQLSchema,
  newSchema: GraphQLSchema,
): {
  breakingChanges: Array,
  dangerousChanges: Array,
} {
  const oldTypeMap = oldSchema.getTypeMap();
  const newTypeMap = newSchema.getTypeMap();

  const breakingChanges = [];
  const dangerousChanges = [];
  Object.keys(oldTypeMap).forEach(typeName => {
    const oldType = oldTypeMap[typeName];
    const newType = newTypeMap[typeName];
    if (!isInputObjectType(oldType) || !isInputObjectType(newType)) {
      return;
    }

    const oldTypeFieldsDef = oldType.getFields();
    const newTypeFieldsDef = newType.getFields();
    Object.keys(oldTypeFieldsDef).forEach(fieldName => {
      // Check if the field is missing on the type in the new schema.
      if (!(fieldName in newTypeFieldsDef)) {
        breakingChanges.push({
          type: BreakingChangeType.FIELD_REMOVED,
          description: `${typeName}.${fieldName} was removed.`,
        });
      } else {
        const oldFieldType = oldTypeFieldsDef[fieldName].type;
        const newFieldType = newTypeFieldsDef[fieldName].type;

        const isSafe = isChangeSafeForInputObjectFieldOrFieldArg(
          oldFieldType,
          newFieldType,
        );
        if (!isSafe) {
          const oldFieldTypeString = isNamedType(oldFieldType)
            ? oldFieldType.name
            : oldFieldType.toString();
          const newFieldTypeString = isNamedType(newFieldType)
            ? newFieldType.name
            : newFieldType.toString();
          breakingChanges.push({
            type: BreakingChangeType.FIELD_CHANGED_KIND,
            description:
              `${typeName}.${fieldName} changed type from ` +
              `${oldFieldTypeString} to ${newFieldTypeString}.`,
          });
        }
      }
    });
    // Check if a field was added to the input object type
    Object.keys(newTypeFieldsDef).forEach(fieldName => {
      if (!(fieldName in oldTypeFieldsDef)) {
        if (isNonNullType(newTypeFieldsDef[fieldName].type)) {
          breakingChanges.push({
            type: BreakingChangeType.NON_NULL_INPUT_FIELD_ADDED,
            description:
              `A non-null field ${fieldName} on ` +
              `input type ${newType.name} was added.`,
          });
        } else {
          dangerousChanges.push({
            type: DangerousChangeType.NULLABLE_INPUT_FIELD_ADDED,
            description:
              `A nullable field ${fieldName} on ` +
              `input type ${newType.name} was added.`,
          });
        }
      }
    });
  });
  return {
    breakingChanges,
    dangerousChanges,
  };
}

function isChangeSafeForObjectOrInterfaceField(
  oldType: GraphQLType,
  newType: GraphQLType,
): boolean {
  if (isNamedType(oldType)) {
    return (
      // if they're both named types, see if their names are equivalent
      (isNamedType(newType) && oldType.name === newType.name) ||
      // moving from nullable to non-null of the same underlying type is safe
      (isNonNullType(newType) &&
        isChangeSafeForObjectOrInterfaceField(oldType, newType.ofType))
    );
  } else if (isListType(oldType)) {
    return (
      // if they're both lists, make sure the underlying types are compatible
      (isListType(newType) &&
        isChangeSafeForObjectOrInterfaceField(
          oldType.ofType,
          newType.ofType,
        )) ||
      // moving from nullable to non-null of the same underlying type is safe
      (isNonNullType(newType) &&
        isChangeSafeForObjectOrInterfaceField(oldType, newType.ofType))
    );
  } else if (isNonNullType(oldType)) {
    // if they're both non-null, make sure the underlying types are compatible
    return (
      isNonNullType(newType) &&
      isChangeSafeForObjectOrInterfaceField(oldType.ofType, newType.ofType)
    );
  }
  return false;
}

function isChangeSafeForInputObjectFieldOrFieldArg(
  oldType: GraphQLType,
  newType: GraphQLType,
): boolean {
  if (isNamedType(oldType)) {
    // if they're both named types, see if their names are equivalent
    return isNamedType(newType) && oldType.name === newType.name;
  } else if (isListType(oldType)) {
    // if they're both lists, make sure the underlying types are compatible
    return (
      isListType(newType) &&
      isChangeSafeForInputObjectFieldOrFieldArg(oldType.ofType, newType.ofType)
    );
  } else if (isNonNullType(oldType)) {
    return (
      // if they're both non-null, make sure the underlying types are
      // compatible
      (isNonNullType(newType) &&
        isChangeSafeForInputObjectFieldOrFieldArg(
          oldType.ofType,
          newType.ofType,
        )) ||
      // moving from non-null to nullable of the same underlying type is safe
      (!isNonNullType(newType) &&
        isChangeSafeForInputObjectFieldOrFieldArg(oldType.ofType, newType))
    );
  }
  return false;
}

/**
 * Given two schemas, returns an Array containing descriptions of any breaking
 * changes in the newSchema related to removing types from a union type.
 */
export function findTypesRemovedFromUnions(
  oldSchema: GraphQLSchema,
  newSchema: GraphQLSchema,
): Array {
  const oldTypeMap = oldSchema.getTypeMap();
  const newTypeMap = newSchema.getTypeMap();

  const typesRemovedFromUnion = [];
  Object.keys(oldTypeMap).forEach(typeName => {
    const oldType = oldTypeMap[typeName];
    const newType = newTypeMap[typeName];
    if (!isUnionType(oldType) || !isUnionType(newType)) {
      return;
    }
    const typeNamesInNewUnion = Object.create(null);
    newType.getTypes().forEach(type => {
      typeNamesInNewUnion[type.name] = true;
    });
    oldType.getTypes().forEach(type => {
      if (!typeNamesInNewUnion[type.name]) {
        typesRemovedFromUnion.push({
          type: BreakingChangeType.TYPE_REMOVED_FROM_UNION,
          description: `${type.name} was removed from union type ${typeName}.`,
        });
      }
    });
  });
  return typesRemovedFromUnion;
}

/**
 * Given two schemas, returns an Array containing descriptions of any dangerous
 * changes in the newSchema related to adding types to a union type.
 */
export function findTypesAddedToUnions(
  oldSchema: GraphQLSchema,
  newSchema: GraphQLSchema,
): Array {
  const oldTypeMap = oldSchema.getTypeMap();
  const newTypeMap = newSchema.getTypeMap();

  const typesAddedToUnion = [];
  Object.keys(newTypeMap).forEach(typeName => {
    const oldType = oldTypeMap[typeName];
    const newType = newTypeMap[typeName];
    if (!isUnionType(oldType) || !isUnionType(newType)) {
      return;
    }
    const typeNamesInOldUnion = Object.create(null);
    oldType.getTypes().forEach(type => {
      typeNamesInOldUnion[type.name] = true;
    });
    newType.getTypes().forEach(type => {
      if (!typeNamesInOldUnion[type.name]) {
        typesAddedToUnion.push({
          type: DangerousChangeType.TYPE_ADDED_TO_UNION,
          description: `${type.name} was added to union type ${typeName}.`,
        });
      }
    });
  });
  return typesAddedToUnion;
}
/**
 * Given two schemas, returns an Array containing descriptions of any breaking
 * changes in the newSchema related to removing values from an enum type.
 */
export function findValuesRemovedFromEnums(
  oldSchema: GraphQLSchema,
  newSchema: GraphQLSchema,
): Array {
  const oldTypeMap = oldSchema.getTypeMap();
  const newTypeMap = newSchema.getTypeMap();

  const valuesRemovedFromEnums = [];
  Object.keys(oldTypeMap).forEach(typeName => {
    const oldType = oldTypeMap[typeName];
    const newType = newTypeMap[typeName];
    if (!isEnumType(oldType) || !isEnumType(newType)) {
      return;
    }
    const valuesInNewEnum = Object.create(null);
    newType.getValues().forEach(value => {
      valuesInNewEnum[value.name] = true;
    });
    oldType.getValues().forEach(value => {
      if (!valuesInNewEnum[value.name]) {
        valuesRemovedFromEnums.push({
          type: BreakingChangeType.VALUE_REMOVED_FROM_ENUM,
          description: `${value.name} was removed from enum type ${typeName}.`,
        });
      }
    });
  });
  return valuesRemovedFromEnums;
}

/**
 * Given two schemas, returns an Array containing descriptions of any dangerous
 * changes in the newSchema related to adding values to an enum type.
 */
export function findValuesAddedToEnums(
  oldSchema: GraphQLSchema,
  newSchema: GraphQLSchema,
): Array {
  const oldTypeMap = oldSchema.getTypeMap();
  const newTypeMap = newSchema.getTypeMap();

  const valuesAddedToEnums = [];
  Object.keys(oldTypeMap).forEach(typeName => {
    const oldType = oldTypeMap[typeName];
    const newType = newTypeMap[typeName];
    if (!isEnumType(oldType) || !isEnumType(newType)) {
      return;
    }

    const valuesInOldEnum = Object.create(null);
    oldType.getValues().forEach(value => {
      valuesInOldEnum[value.name] = true;
    });
    newType.getValues().forEach(value => {
      if (!valuesInOldEnum[value.name]) {
        valuesAddedToEnums.push({
          type: DangerousChangeType.VALUE_ADDED_TO_ENUM,
          description: `${value.name} was added to enum type ${typeName}.`,
        });
      }
    });
  });
  return valuesAddedToEnums;
}

export function findInterfacesRemovedFromObjectTypes(
  oldSchema: GraphQLSchema,
  newSchema: GraphQLSchema,
): Array {
  const oldTypeMap = oldSchema.getTypeMap();
  const newTypeMap = newSchema.getTypeMap();
  const breakingChanges = [];

  Object.keys(oldTypeMap).forEach(typeName => {
    const oldType = oldTypeMap[typeName];
    const newType = newTypeMap[typeName];
    if (!isObjectType(oldType) || !isObjectType(newType)) {
      return;
    }

    const oldInterfaces = oldType.getInterfaces();
    const newInterfaces = newType.getInterfaces();
    oldInterfaces.forEach(oldInterface => {
      if (!newInterfaces.some(int => int.name === oldInterface.name)) {
        breakingChanges.push({
          type: BreakingChangeType.INTERFACE_REMOVED_FROM_OBJECT,
          description:
            `${typeName} no longer implements interface ` +
            `${oldInterface.name}.`,
        });
      }
    });
  });
  return breakingChanges;
}

export function findInterfacesAddedToObjectTypes(
  oldSchema: GraphQLSchema,
  newSchema: GraphQLSchema,
): Array {
  const oldTypeMap = oldSchema.getTypeMap();
  const newTypeMap = newSchema.getTypeMap();
  const interfacesAddedToObjectTypes = [];

  Object.keys(newTypeMap).forEach(typeName => {
    const oldType = oldTypeMap[typeName];
    const newType = newTypeMap[typeName];
    if (!isObjectType(oldType) || !isObjectType(newType)) {
      return;
    }

    const oldInterfaces = oldType.getInterfaces();
    const newInterfaces = newType.getInterfaces();
    newInterfaces.forEach(newInterface => {
      if (!oldInterfaces.some(int => int.name === newInterface.name)) {
        interfacesAddedToObjectTypes.push({
          type: DangerousChangeType.INTERFACE_ADDED_TO_OBJECT,
          description:
            `${newInterface.name} added to interfaces implemented ` +
            `by ${typeName}.`,
        });
      }
    });
  });
  return interfacesAddedToObjectTypes;
}

export function findRemovedDirectives(
  oldSchema: GraphQLSchema,
  newSchema: GraphQLSchema,
): Array {
  const removedDirectives = [];

  const newSchemaDirectiveMap = getDirectiveMapForSchema(newSchema);
  oldSchema.getDirectives().forEach(directive => {
    if (!newSchemaDirectiveMap[directive.name]) {
      removedDirectives.push({
        type: BreakingChangeType.DIRECTIVE_REMOVED,
        description: `${directive.name} was removed`,
      });
    }
  });

  return removedDirectives;
}

function findRemovedArgsForDirective(
  oldDirective: GraphQLDirective,
  newDirective: GraphQLDirective,
): Array {
  const removedArgs = [];
  const newArgMap = getArgumentMapForDirective(newDirective);

  oldDirective.args.forEach(arg => {
    if (!newArgMap[arg.name]) {
      removedArgs.push(arg);
    }
  });

  return removedArgs;
}

export function findRemovedDirectiveArgs(
  oldSchema: GraphQLSchema,
  newSchema: GraphQLSchema,
): Array {
  const removedDirectiveArgs = [];
  const oldSchemaDirectiveMap = getDirectiveMapForSchema(oldSchema);

  newSchema.getDirectives().forEach(newDirective => {
    const oldDirective = oldSchemaDirectiveMap[newDirective.name];
    if (!oldDirective) {
      return;
    }

    findRemovedArgsForDirective(oldDirective, newDirective).forEach(arg => {
      removedDirectiveArgs.push({
        type: BreakingChangeType.DIRECTIVE_ARG_REMOVED,
        description: `${arg.name} was removed from ${newDirective.name}`,
      });
    });
  });

  return removedDirectiveArgs;
}

function findAddedArgsForDirective(
  oldDirective: GraphQLDirective,
  newDirective: GraphQLDirective,
): Array {
  const addedArgs = [];
  const oldArgMap = getArgumentMapForDirective(oldDirective);

  newDirective.args.forEach(arg => {
    if (!oldArgMap[arg.name]) {
      addedArgs.push(arg);
    }
  });

  return addedArgs;
}

export function findAddedNonNullDirectiveArgs(
  oldSchema: GraphQLSchema,
  newSchema: GraphQLSchema,
): Array {
  const addedNonNullableArgs = [];
  const oldSchemaDirectiveMap = getDirectiveMapForSchema(oldSchema);

  newSchema.getDirectives().forEach(newDirective => {
    const oldDirective = oldSchemaDirectiveMap[newDirective.name];
    if (!oldDirective) {
      return;
    }

    findAddedArgsForDirective(oldDirective, newDirective).forEach(arg => {
      if (!isNonNullType(arg.type)) {
        return;
      }

      addedNonNullableArgs.push({
        type: BreakingChangeType.NON_NULL_DIRECTIVE_ARG_ADDED,
        description:
          `A non-null arg ${arg.name} on directive ` +
          `${newDirective.name} was added`,
      });
    });
  });

  return addedNonNullableArgs;
}

export function findRemovedLocationsForDirective(
  oldDirective: GraphQLDirective,
  newDirective: GraphQLDirective,
): Array {
  const removedLocations = [];
  const newLocationSet = new Set(newDirective.locations);

  oldDirective.locations.forEach(oldLocation => {
    if (!newLocationSet.has(oldLocation)) {
      removedLocations.push(oldLocation);
    }
  });

  return removedLocations;
}

export function findRemovedDirectiveLocations(
  oldSchema: GraphQLSchema,
  newSchema: GraphQLSchema,
): Array {
  const removedLocations = [];
  const oldSchemaDirectiveMap = getDirectiveMapForSchema(oldSchema);

  newSchema.getDirectives().forEach(newDirective => {
    const oldDirective = oldSchemaDirectiveMap[newDirective.name];
    if (!oldDirective) {
      return;
    }

    findRemovedLocationsForDirective(oldDirective, newDirective).forEach(
      location => {
        removedLocations.push({
          type: BreakingChangeType.DIRECTIVE_LOCATION_REMOVED,
          description: `${location} was removed from ${newDirective.name}`,
        });
      },
    );
  });

  return removedLocations;
}

function getDirectiveMapForSchema(
  schema: GraphQLSchema,
): ObjMap {
  return keyMap(schema.getDirectives(), dir => dir.name);
}

function getArgumentMapForDirective(
  directive: GraphQLDirective,
): ObjMap {
  return keyMap(directive.args, arg => arg.name);
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy