diff --git a/src/utilities/findBreakingChanges.js b/src/utilities/findBreakingChanges.js index cdf294316f..91ac70f05e 100644 --- a/src/utilities/findBreakingChanges.js +++ b/src/utilities/findBreakingChanges.js @@ -34,30 +34,30 @@ import { import { type GraphQLSchema } from '../type/schema'; export const BreakingChangeType = Object.freeze({ - FIELD_CHANGED_KIND: 'FIELD_CHANGED_KIND', - FIELD_REMOVED: 'FIELD_REMOVED', - TYPE_CHANGED_KIND: 'TYPE_CHANGED_KIND', TYPE_REMOVED: 'TYPE_REMOVED', + TYPE_CHANGED_KIND: 'TYPE_CHANGED_KIND', 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', - REQUIRED_ARG_ADDED: 'REQUIRED_ARG_ADDED', REQUIRED_INPUT_FIELD_ADDED: 'REQUIRED_INPUT_FIELD_ADDED', INTERFACE_REMOVED_FROM_OBJECT: 'INTERFACE_REMOVED_FROM_OBJECT', + FIELD_REMOVED: 'FIELD_REMOVED', + FIELD_CHANGED_KIND: 'FIELD_CHANGED_KIND', + REQUIRED_ARG_ADDED: 'REQUIRED_ARG_ADDED', + ARG_REMOVED: 'ARG_REMOVED', + ARG_CHANGED_KIND: 'ARG_CHANGED_KIND', DIRECTIVE_REMOVED: 'DIRECTIVE_REMOVED', DIRECTIVE_ARG_REMOVED: 'DIRECTIVE_ARG_REMOVED', - DIRECTIVE_LOCATION_REMOVED: 'DIRECTIVE_LOCATION_REMOVED', REQUIRED_DIRECTIVE_ARG_ADDED: 'REQUIRED_DIRECTIVE_ARG_ADDED', + DIRECTIVE_LOCATION_REMOVED: 'DIRECTIVE_LOCATION_REMOVED', }); export const DangerousChangeType = Object.freeze({ - 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', OPTIONAL_INPUT_FIELD_ADDED: 'OPTIONAL_INPUT_FIELD_ADDED', OPTIONAL_ARG_ADDED: 'OPTIONAL_ARG_ADDED', + INTERFACE_ADDED_TO_OBJECT: 'INTERFACE_ADDED_TO_OBJECT', + ARG_DEFAULT_VALUE_CHANGE: 'ARG_DEFAULT_VALUE_CHANGE', }); export type BreakingChange = { @@ -108,6 +108,58 @@ function findSchemaChanges( ]; } +function findDirectiveChanges( + oldSchema: GraphQLSchema, + newSchema: GraphQLSchema, +): Array { + const schemaChanges = []; + + const directivesDiff = diff( + oldSchema.getDirectives(), + newSchema.getDirectives(), + ); + + for (const oldDirective of directivesDiff.removed) { + schemaChanges.push({ + type: BreakingChangeType.DIRECTIVE_REMOVED, + description: `${oldDirective.name} was removed.`, + }); + } + + for (const [oldDirective, newDirective] of directivesDiff.persisted) { + const argsDiff = diff(oldDirective.args, newDirective.args); + + for (const newArg of argsDiff.added) { + if (isRequiredArgument(newArg)) { + schemaChanges.push({ + type: BreakingChangeType.REQUIRED_DIRECTIVE_ARG_ADDED, + description: + `A required arg ${newArg.name} on directive ` + + `${oldDirective.name} was added.`, + }); + } + } + + for (const oldArg of argsDiff.removed) { + schemaChanges.push({ + type: BreakingChangeType.DIRECTIVE_ARG_REMOVED, + description: `${oldArg.name} was removed from ${oldDirective.name}.`, + }); + } + + for (const location of oldDirective.locations) { + if (newDirective.locations.indexOf(location) === -1) { + schemaChanges.push({ + type: BreakingChangeType.DIRECTIVE_LOCATION_REMOVED, + description: `${location} was removed from ${oldDirective.name}.`, + }); + } + } + } + + return schemaChanges; +} + function findTypeChanges( oldSchema: GraphQLSchema, newSchema: GraphQLSchema, @@ -150,63 +202,52 @@ function findTypeChanges( return schemaChanges; } -function findArgChanges( - oldType: GraphQLObjectType | GraphQLInterfaceType, - oldField: GraphQLField<*, *>, - newField: GraphQLField<*, *>, +function findInputObjectTypeChanges( + oldType: GraphQLInputObjectType, + newType: GraphQLInputObjectType, ): Array { const schemaChanges = []; - const argsDiff = diff(oldField.args, newField.args); - - for (const oldArg of argsDiff.removed) { - schemaChanges.push({ - type: BreakingChangeType.ARG_REMOVED, - description: `${oldType.name}.${oldField.name} arg ${ - oldArg.name - } was removed.`, - }); - } + const fieldsDiff = diff( + objectValues(oldType.getFields()), + objectValues(newType.getFields()), + ); - for (const [oldArg, newArg] of argsDiff.persisted) { - const isSafe = isChangeSafeForInputObjectFieldOrFieldArg( - oldArg.type, - newArg.type, - ); - if (!isSafe) { + for (const newField of fieldsDiff.added) { + if (isRequiredInputField(newField)) { schemaChanges.push({ - type: BreakingChangeType.ARG_CHANGED_KIND, + type: BreakingChangeType.REQUIRED_INPUT_FIELD_ADDED, description: - `${oldType.name}.${oldField.name} arg ` + - `${oldArg.name} has changed type from ` + - `${String(oldArg.type)} to ${String(newArg.type)}.`, + `A required field ${newField.name} on ` + + `input type ${oldType.name} was added.`, }); - } else if ( - oldArg.defaultValue !== undefined && - oldArg.defaultValue !== newArg.defaultValue - ) { + } else { schemaChanges.push({ - type: DangerousChangeType.ARG_DEFAULT_VALUE_CHANGE, + type: DangerousChangeType.OPTIONAL_INPUT_FIELD_ADDED, description: - `${oldType.name}.${oldField.name} arg ` + - `${oldArg.name} has changed defaultValue.`, + `An optional field ${newField.name} on ` + + `input type ${oldType.name} was added.`, }); } } - for (const newArg of argsDiff.added) { - if (isRequiredArgument(newArg)) { - schemaChanges.push({ - type: BreakingChangeType.REQUIRED_ARG_ADDED, - description: - `A required arg ${newArg.name} on ` + - `${oldType.name}.${oldField.name} was added.`, - }); - } else { + for (const oldField of fieldsDiff.removed) { + schemaChanges.push({ + type: BreakingChangeType.FIELD_REMOVED, + description: `${oldType.name}.${oldField.name} was removed.`, + }); + } + + for (const [oldField, newField] of fieldsDiff.persisted) { + const isSafe = isChangeSafeForInputObjectFieldOrFieldArg( + oldField.type, + newField.type, + ); + if (!isSafe) { schemaChanges.push({ - type: DangerousChangeType.OPTIONAL_ARG_ADDED, + type: BreakingChangeType.FIELD_CHANGED_KIND, description: - `An optional arg ${newArg.name} on ` + - `${oldType.name}.${oldField.name} was added.`, + `${oldType.name}.${oldField.name} changed type from ` + + `${String(oldField.type)} to ${String(newField.type)}.`, }); } } @@ -214,29 +255,86 @@ function findArgChanges( return schemaChanges; } -function typeKindName(type: GraphQLNamedType): string { - if (isScalarType(type)) { - return 'a Scalar type'; +function findUnionTypeChanges( + oldType: GraphQLUnionType, + newType: GraphQLUnionType, +): Array { + const schemaChanges = []; + const possibleTypesDiff = diff(oldType.getTypes(), newType.getTypes()); + + for (const newPossibleType of possibleTypesDiff.added) { + schemaChanges.push({ + type: DangerousChangeType.TYPE_ADDED_TO_UNION, + description: `${newPossibleType.name} was added to union type ${ + oldType.name + }.`, + }); } - if (isObjectType(type)) { - return 'an Object type'; + + for (const oldPossibleType of possibleTypesDiff.removed) { + schemaChanges.push({ + type: BreakingChangeType.TYPE_REMOVED_FROM_UNION, + description: + `${oldPossibleType.name} was removed from ` + + `union type ${oldType.name}.`, + }); } - if (isInterfaceType(type)) { - return 'an Interface type'; + + return schemaChanges; +} + +function findEnumTypeChanges( + oldType: GraphQLEnumType, + newType: GraphQLEnumType, +): Array { + const schemaChanges = []; + const valuesDiff = diff(oldType.getValues(), newType.getValues()); + + for (const newValue of valuesDiff.added) { + schemaChanges.push({ + type: DangerousChangeType.VALUE_ADDED_TO_ENUM, + description: `${newValue.name} was added to enum type ${oldType.name}.`, + }); } - if (isUnionType(type)) { - return 'a Union type'; + + for (const oldValue of valuesDiff.removed) { + schemaChanges.push({ + type: BreakingChangeType.VALUE_REMOVED_FROM_ENUM, + description: `${oldValue.name} was removed from enum type ${ + oldType.name + }.`, + }); } - if (isEnumType(type)) { - return 'an Enum type'; + + return schemaChanges; +} + +function findObjectTypeChanges( + oldType: GraphQLObjectType, + newType: GraphQLObjectType, +): Array { + const schemaChanges = findFieldChanges(oldType, newType); + const interfacesDiff = diff(oldType.getInterfaces(), newType.getInterfaces()); + + for (const newInterface of interfacesDiff.added) { + schemaChanges.push({ + type: DangerousChangeType.INTERFACE_ADDED_TO_OBJECT, + description: + `${newInterface.name} added to interfaces implemented ` + + `by ${oldType.name}.`, + }); } - if (isInputObjectType(type)) { - return 'an Input type'; + + for (const oldInterface of interfacesDiff.removed) { + schemaChanges.push({ + type: BreakingChangeType.INTERFACE_REMOVED_FROM_OBJECT, + description: + `${oldType.name} no longer implements interface ` + + `${oldInterface.name}.`, + }); } - // Not reachable. All possible named types have been considered. - /* istanbul ignore next */ - throw new TypeError(`Unexpected type: ${inspect((type: empty))}.`); + return schemaChanges; } function findFieldChanges( @@ -276,52 +374,63 @@ function findFieldChanges( return schemaChanges; } -function findInputObjectTypeChanges( - oldType: GraphQLInputObjectType, - newType: GraphQLInputObjectType, +function findArgChanges( + oldType: GraphQLObjectType | GraphQLInterfaceType, + oldField: GraphQLField<*, *>, + newField: GraphQLField<*, *>, ): Array { const schemaChanges = []; - const fieldsDiff = diff( - objectValues(oldType.getFields()), - objectValues(newType.getFields()), - ); + const argsDiff = diff(oldField.args, newField.args); - for (const newField of fieldsDiff.added) { - if (isRequiredInputField(newField)) { + for (const oldArg of argsDiff.removed) { + schemaChanges.push({ + type: BreakingChangeType.ARG_REMOVED, + description: `${oldType.name}.${oldField.name} arg ${ + oldArg.name + } was removed.`, + }); + } + + for (const [oldArg, newArg] of argsDiff.persisted) { + const isSafe = isChangeSafeForInputObjectFieldOrFieldArg( + oldArg.type, + newArg.type, + ); + if (!isSafe) { schemaChanges.push({ - type: BreakingChangeType.REQUIRED_INPUT_FIELD_ADDED, + type: BreakingChangeType.ARG_CHANGED_KIND, description: - `A required field ${newField.name} on ` + - `input type ${oldType.name} was added.`, + `${oldType.name}.${oldField.name} arg ` + + `${oldArg.name} has changed type from ` + + `${String(oldArg.type)} to ${String(newArg.type)}.`, }); - } else { + } else if ( + oldArg.defaultValue !== undefined && + oldArg.defaultValue !== newArg.defaultValue + ) { schemaChanges.push({ - type: DangerousChangeType.OPTIONAL_INPUT_FIELD_ADDED, + type: DangerousChangeType.ARG_DEFAULT_VALUE_CHANGE, description: - `An optional field ${newField.name} on ` + - `input type ${oldType.name} was added.`, + `${oldType.name}.${oldField.name} arg ` + + `${oldArg.name} has changed defaultValue.`, }); } } - for (const oldField of fieldsDiff.removed) { - schemaChanges.push({ - type: BreakingChangeType.FIELD_REMOVED, - description: `${oldType.name}.${oldField.name} was removed.`, - }); - } - - for (const [oldField, newField] of fieldsDiff.persisted) { - const isSafe = isChangeSafeForInputObjectFieldOrFieldArg( - oldField.type, - newField.type, - ); - if (!isSafe) { + for (const newArg of argsDiff.added) { + if (isRequiredArgument(newArg)) { schemaChanges.push({ - type: BreakingChangeType.FIELD_CHANGED_KIND, + type: BreakingChangeType.REQUIRED_ARG_ADDED, description: - `${oldType.name}.${oldField.name} changed type from ` + - `${String(oldField.type)} to ${String(newField.type)}.`, + `A required arg ${newArg.name} on ` + + `${oldType.name}.${oldField.name} was added.`, + }); + } else { + schemaChanges.push({ + type: DangerousChangeType.OPTIONAL_ARG_ADDED, + description: + `An optional arg ${newArg.name} on ` + + `${oldType.name}.${oldField.name} was added.`, }); } } @@ -395,138 +504,29 @@ function isChangeSafeForInputObjectFieldOrFieldArg( return isNamedType(newType) && oldType.name === newType.name; } -function findUnionTypeChanges( - oldType: GraphQLUnionType, - newType: GraphQLUnionType, -): Array { - const schemaChanges = []; - const possibleTypesDiff = diff(oldType.getTypes(), newType.getTypes()); - - for (const newPossibleType of possibleTypesDiff.added) { - schemaChanges.push({ - type: DangerousChangeType.TYPE_ADDED_TO_UNION, - description: `${newPossibleType.name} was added to union type ${ - oldType.name - }.`, - }); - } - - for (const oldPossibleType of possibleTypesDiff.removed) { - schemaChanges.push({ - type: BreakingChangeType.TYPE_REMOVED_FROM_UNION, - description: - `${oldPossibleType.name} was removed from ` + - `union type ${oldType.name}.`, - }); - } - - return schemaChanges; -} - -function findEnumTypeChanges( - oldType: GraphQLEnumType, - newType: GraphQLEnumType, -): Array { - const schemaChanges = []; - const valuesDiff = diff(oldType.getValues(), newType.getValues()); - - for (const newValue of valuesDiff.added) { - schemaChanges.push({ - type: DangerousChangeType.VALUE_ADDED_TO_ENUM, - description: `${newValue.name} was added to enum type ${oldType.name}.`, - }); +function typeKindName(type: GraphQLNamedType): string { + if (isScalarType(type)) { + return 'a Scalar type'; } - - for (const oldValue of valuesDiff.removed) { - schemaChanges.push({ - type: BreakingChangeType.VALUE_REMOVED_FROM_ENUM, - description: `${oldValue.name} was removed from enum type ${ - oldType.name - }.`, - }); + if (isObjectType(type)) { + return 'an Object type'; } - - return schemaChanges; -} - -function findObjectTypeChanges( - oldType: GraphQLObjectType, - newType: GraphQLObjectType, -): Array { - const schemaChanges = findFieldChanges(oldType, newType); - const interfacesDiff = diff(oldType.getInterfaces(), newType.getInterfaces()); - - for (const newInterface of interfacesDiff.added) { - schemaChanges.push({ - type: DangerousChangeType.INTERFACE_ADDED_TO_OBJECT, - description: - `${newInterface.name} added to interfaces implemented ` + - `by ${oldType.name}.`, - }); + if (isInterfaceType(type)) { + return 'an Interface type'; } - - for (const oldInterface of interfacesDiff.removed) { - schemaChanges.push({ - type: BreakingChangeType.INTERFACE_REMOVED_FROM_OBJECT, - description: - `${oldType.name} no longer implements interface ` + - `${oldInterface.name}.`, - }); + if (isUnionType(type)) { + return 'a Union type'; } - - return schemaChanges; -} - -function findDirectiveChanges( - oldSchema: GraphQLSchema, - newSchema: GraphQLSchema, -): Array { - const schemaChanges = []; - - const directivesDiff = diff( - oldSchema.getDirectives(), - newSchema.getDirectives(), - ); - - for (const oldDirective of directivesDiff.removed) { - schemaChanges.push({ - type: BreakingChangeType.DIRECTIVE_REMOVED, - description: `${oldDirective.name} was removed.`, - }); + if (isEnumType(type)) { + return 'an Enum type'; } - - for (const [oldDirective, newDirective] of directivesDiff.persisted) { - const argsDiff = diff(oldDirective.args, newDirective.args); - - for (const newArg of argsDiff.added) { - if (isRequiredArgument(newArg)) { - schemaChanges.push({ - type: BreakingChangeType.REQUIRED_DIRECTIVE_ARG_ADDED, - description: - `A required arg ${newArg.name} on directive ` + - `${oldDirective.name} was added.`, - }); - } - } - - for (const oldArg of argsDiff.removed) { - schemaChanges.push({ - type: BreakingChangeType.DIRECTIVE_ARG_REMOVED, - description: `${oldArg.name} was removed from ${oldDirective.name}.`, - }); - } - - for (const location of oldDirective.locations) { - if (newDirective.locations.indexOf(location) === -1) { - schemaChanges.push({ - type: BreakingChangeType.DIRECTIVE_LOCATION_REMOVED, - description: `${location} was removed from ${oldDirective.name}.`, - }); - } - } + if (isInputObjectType(type)) { + return 'an Input type'; } - return schemaChanges; + // Not reachable. All possible named types have been considered. + /* istanbul ignore next */ + throw new TypeError(`Unexpected type: ${inspect((type: empty))}.`); } function diff(