diff --git a/src/Utils/FindBreakingChanges.php b/src/Utils/FindBreakingChanges.php new file mode 100644 index 0000000..c747a71 --- /dev/null +++ b/src/Utils/FindBreakingChanges.php @@ -0,0 +1,583 @@ +getTypeMap(); + $newTypeMap = $newSchema->getTypeMap(); + + $breakingChanges = []; + foreach ($oldTypeMap as $typeName => $typeDefinition) { + if (!isset($newTypeMap[$typeName])) { + $breakingChanges[] = + ['type' => self::BREAKING_CHANGE_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. + * + * @return array + */ + public static function findTypesThatChangedKind( + Schema $oldSchema, Schema $newSchema + ) + { + $oldTypeMap = $oldSchema->getTypeMap(); + $newTypeMap = $newSchema->getTypeMap(); + + $breakingChanges = []; + foreach ($oldTypeMap as $typeName => $typeDefinition) { + if (!isset($newTypeMap[$typeName])) { + continue; + } + $newTypeDefinition = $newTypeMap[$typeName]; + if (!($typeDefinition instanceof $newTypeDefinition)) { + $oldTypeKindName = self::typeKindName($typeDefinition); + $newTypeKindName = self::typeKindName($newTypeDefinition); + $breakingChanges[] = [ + 'type' => self::BREAKING_CHANGE_TYPE_CHANGED, + 'description' => "${typeName} changed from ${oldTypeKindName} to ${newTypeKindName}." + ]; + } + } + + 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). + * + * @return array + */ + public static function findArgChanges( + Schema $oldSchema, Schema $newSchema + ) + { + $oldTypeMap = $oldSchema->getTypeMap(); + $newTypeMap = $newSchema->getTypeMap(); + + $breakingChanges = []; + $dangerousChanges = []; + foreach ($oldTypeMap as $oldTypeName => $oldTypeDefinition) { + $newTypeDefinition = isset($newTypeMap[$oldTypeName]) ? $newTypeMap[$oldTypeName] : null; + if (!($oldTypeDefinition instanceof ObjectType || $oldTypeDefinition instanceof InterfaceType) || + !($newTypeDefinition instanceof $oldTypeDefinition)) { + continue; + } + + $oldTypeFields = $oldTypeDefinition->getFields(); + $newTypeFields = $newTypeDefinition->getFields(); + + foreach ($oldTypeFields as $fieldName => $fieldDefinition) { + if (!isset($newTypeFields[$fieldName])) { + continue; + } + + foreach ($fieldDefinition->args as $oldArgDef) { + $newArgs = $newTypeFields[$fieldName]->args; + $newArgDef = Utils::find( + $newArgs, function ($arg) use ($oldArgDef) { + return $arg->name === $oldArgDef->name; + } + ); + if (!$newArgDef) { + $argName = $oldArgDef->name; + $breakingChanges[] = [ + 'type' => self::BREAKING_CHANGE_ARG_REMOVED, + 'description' => "${oldTypeName}->${fieldName} arg ${argName} was removed" + ]; + } else { + $isSafe = self::isChangeSafeForInputObjectFieldOrFieldArg($oldArgDef->getType(), $newArgDef->getType()); + $oldArgType = $oldArgDef->getType(); + $oldArgName = $oldArgDef->name; + if (!$isSafe) { + $newArgType = $newArgDef->getType(); + $breakingChanges[] = [ + 'type' => self::BREAKING_CHANGE_ARG_CHANGED, + 'description' => "${oldTypeName}->${fieldName} arg ${oldArgName} has changed type from ${oldArgType} to ${newArgType}." + ]; + } elseif ($oldArgDef->defaultValueExists() && $oldArgDef->defaultValue !== $newArgDef->defaultValue) { + $dangerousChanges[] = [ + 'type' => FindBreakingChanges::DANGEROUS_CHANGE_ARG_DEFAULT_VALUE, + 'description' => "${oldTypeName}->${fieldName} arg ${oldArgName} has changed defaultValue" + ]; + } + } + // Check if a non-null arg was added to the field + foreach ($newTypeFields[$fieldName]->args as $newArgDef) { + $oldArgs = $oldTypeFields[$fieldName]->args; + $oldArgDef = Utils::find( + $oldArgs, function ($arg) use ($newArgDef) { + return $arg->name === $newArgDef->name; + } + ); + + if (!$oldArgDef && $newArgDef->getType() instanceof NonNull) { + $newTypeName = $newTypeDefinition->name; + $newArgName = $newArgDef->name; + $breakingChanges[] = [ + 'type' => self::BREAKING_CHANGE_NON_NULL_ARG_ADDED, + 'description' => "A non-null arg ${newArgName} on ${newTypeName}->${fieldName} was added." + ]; + } + } + } + } + } + + return ['breakingChanges' => $breakingChanges, 'dangerousChanges' => $dangerousChanges]; + } + + /** + * @param Type $type + * @return string + * + * @throws \TypeError + */ + private static function typeKindName(Type $type) + { + if ($type instanceof ScalarType) { + return 'a Scalar type'; + } elseif ($type instanceof ObjectType) { + return 'an Object type'; + } elseif ($type instanceof InterfaceType) { + return 'an Interface type'; + } elseif ($type instanceof UnionType) { + return 'a Union type'; + } elseif ($type instanceof EnumType) { + return 'an Enum type'; + } elseif ($type instanceof InputObjectType) { + return 'an Input type'; + } + + throw new \TypeError('unknown type ' . $type->name); + } + + /** + * Given two schemas, returns an Array containing descriptions of any breaking + * changes in the newSchema related to the fields on a type. This includes if + * a field has been removed from a type, if a field has changed type, or if + * a non-null field is added to an input type. + * + * @return array + */ + public static function findFieldsThatChangedType( + Schema $oldSchema, Schema $newSchema + ) + { + return array_merge( + self::findFieldsThatChangedTypeOnObjectOrInterfaceTypes($oldSchema, $newSchema), + self::findFieldsThatChangedTypeOnInputObjectTypes($oldSchema, $newSchema) + ); + } + + /** + * @param Schema $oldSchema + * @param Schema $newSchema + * + * @return array + */ + private static function findFieldsThatChangedTypeOnObjectOrInterfaceTypes(Schema $oldSchema, Schema $newSchema) + { + $oldTypeMap = $oldSchema->getTypeMap(); + $newTypeMap = $newSchema->getTypeMap(); + + $breakingFieldChanges = []; + foreach ($oldTypeMap as $typeName => $oldType) { + $newType = isset($newTypeMap[$typeName]) ? $newTypeMap[$typeName] : null; + if (!($oldType instanceof ObjectType || $oldType instanceof InterfaceType) || !($newType instanceof $oldType)) { + continue; + } + $oldTypeFieldsDef = $oldType->getFields(); + $newTypeFieldsDef = $newType->getFields(); + foreach ($oldTypeFieldsDef as $fieldName => $fieldDefinition) { + if (!isset($newTypeFieldsDef[$fieldName])) { + $breakingFieldChanges[] = ['type' => self::BREAKING_CHANGE_FIELD_REMOVED, 'description' => "${typeName}->${fieldName} was removed."]; + } else { + $oldFieldType = $oldTypeFieldsDef[$fieldName]->getType(); + $newfieldType = $newTypeFieldsDef[$fieldName]->getType(); + $isSafe = self::isChangeSafeForObjectOrInterfaceField($oldFieldType, $newfieldType); + if (!$isSafe) { + + $oldFieldTypeString = self::isNamedType($oldFieldType) ? $oldFieldType->name : $oldFieldType; + $newFieldTypeString = self::isNamedType($newfieldType) ? $newfieldType->name : $newfieldType; + $breakingFieldChanges[] = ['type' => self::BREAKING_CHANGE_FIELD_CHANGED, 'description' => "${typeName}->${fieldName} changed type from ${oldFieldTypeString} to ${newFieldTypeString}."]; + } + } + } + } + return $breakingFieldChanges; + } + + /** + * @param Schema $oldSchema + * @param Schema $newSchema + * + * @return array + */ + public static function findFieldsThatChangedTypeOnInputObjectTypes( + Schema $oldSchema, Schema $newSchema + ) + { + $oldTypeMap = $oldSchema->getTypeMap(); + $newTypeMap = $newSchema->getTypeMap(); + + $breakingFieldChanges = []; + foreach ($oldTypeMap as $typeName => $oldType) { + $newType = isset($newTypeMap[$typeName]) ? $newTypeMap[$typeName] : null; + if (!($oldType instanceof InputObjectType) || !($newType instanceof InputObjectType)) { + continue; + } + $oldTypeFieldsDef = $oldType->getFields(); + $newTypeFieldsDef = $newType->getFields(); + foreach ($oldTypeFieldsDef as $fieldName => $fieldDefinition) { + if (!isset($newTypeFieldsDef[$fieldName])) { + $breakingFieldChanges[] = ['type' => self::BREAKING_CHANGE_FIELD_REMOVED, 'description' => "${typeName}->${fieldName} was removed."]; + } else { + $oldFieldType = $oldTypeFieldsDef[$fieldName]->getType(); + $newfieldType = $newTypeFieldsDef[$fieldName]->getType(); + $isSafe = self::isChangeSafeForInputObjectFieldOrFieldArg($oldFieldType, $newfieldType); + if (!$isSafe) { + $oldFieldTypeString = self::isNamedType($oldFieldType) ? $oldFieldType->name : $oldFieldType; + $newFieldTypeString = self::isNamedType($newfieldType) ? $newfieldType->name : $newfieldType; + $breakingFieldChanges[] = ['type' => self::BREAKING_CHANGE_FIELD_CHANGED, 'description' => "${typeName}->${fieldName} changed type from ${oldFieldTypeString} to ${newFieldTypeString}."]; + } + } + } + foreach ($newTypeFieldsDef as $fieldName => $fieldDef) { + if (!isset($oldTypeFieldsDef[$fieldName]) && $fieldDef->getType() instanceof NonNull) { + $newTypeName = $newType->name; + $breakingFieldChanges[] = ['type' => self::BREAKING_CHANGE_NON_NULL_INPUT_FIELD_ADDED, 'description' => "A non-null field ${fieldName} on input type ${newTypeName} was added."]; + } + } + } + return $breakingFieldChanges; + + } + + private static function isChangeSafeForObjectOrInterfaceField( + Type $oldType, Type $newType + ) + { + if (self::isNamedType($oldType)) { + // if they're both named types, see if their names are equivalent + return (self::isNamedType($newType) && $oldType->name === $newType->name) + // moving from nullable to non-null of the same underlying type is safe + || ($newType instanceof NonNull + && self::isChangeSafeForObjectOrInterfaceField( + $oldType, $newType->getWrappedType() + )); + } elseif ($oldType instanceof ListOfType) { + // if they're both lists, make sure the underlying types are compatible + return ($newType instanceof ListOfType && + self::isChangeSafeForObjectOrInterfaceField($oldType->getWrappedType(), $newType->getWrappedType())) || + // moving from nullable to non-null of the same underlying type is safe + ($newType instanceof NonNull && + self::isChangeSafeForObjectOrInterfaceField($oldType, $newType->getWrappedType())); + } elseif ($oldType instanceof NonNull) { + // if they're both non-null, make sure the underlying types are compatible + return $newType instanceof NonNull && + self::isChangeSafeForObjectOrInterfaceField($oldType->getWrappedType(), $newType->getWrappedType()); + } + + return false; + } + + /** + * @param Type $oldType + * @param Schema $newSchema + * + * @return bool + */ + private static function isChangeSafeForInputObjectFieldOrFieldArg( + Type $oldType, Type $newType + ) + { + if (self::isNamedType($oldType)) { + return self::isNamedType($newType) && $oldType->name === $newType->name; + } elseif ($oldType instanceof ListOfType) { + return $newType instanceof ListOfType && self::isChangeSafeForInputObjectFieldOrFieldArg($oldType->getWrappedType(), $newType->getWrappedType()); + } elseif ($oldType instanceof NonNull) { + return ( + $newType instanceof NonNull && self::isChangeSafeForInputObjectFieldOrFieldArg($oldType->getWrappedType(), $newType->getWrappedType()) + ) || ( + !($newType instanceof NonNull) && self::isChangeSafeForInputObjectFieldOrFieldArg($oldType->getWrappedType(), $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. + * + * @return array + */ + public static function findTypesRemovedFromUnions( + Schema $oldSchema, Schema $newSchema + ) + { + $oldTypeMap = $oldSchema->getTypeMap(); + $newTypeMap = $newSchema->getTypeMap(); + + $typesRemovedFromUnion = []; + foreach ($oldTypeMap as $typeName => $oldType) { + $newType = isset($newTypeMap[$typeName]) ? $newTypeMap[$typeName] : null; + if (!($oldType instanceof UnionType) || !($newType instanceof UnionType)) { + continue; + } + $typeNamesInNewUnion = []; + foreach ($newType->getTypes() as $type) { + $typeNamesInNewUnion[$type->name] = true; + } + foreach ($oldType->getTypes() as $type) { + if (!isset($typeNamesInNewUnion[$type->name])) { + $missingTypeName = $type->name; + $typesRemovedFromUnion[] = ['type' => self::BREAKING_CHANGE_TYPE_REMOVED_FROM_UNION, 'description' => "${missingTypeName} 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. + * + * @return array + */ + public static function findTypesAddedToUnions( + Schema $oldSchema, Schema $newSchema + ) + { + $oldTypeMap = $oldSchema->getTypeMap(); + $newTypeMap = $newSchema->getTypeMap(); + + $typesAddedToUnion = []; + + foreach ($newTypeMap as $typeName => $newType) { + $oldType = isset($oldTypeMap[$typeName]) ? $oldTypeMap[$typeName] : null; + if (!($oldType instanceof UnionType) || !($newType instanceof UnionType)) { + continue; + } + + $typeNamesInOldUnion = []; + foreach ($oldType->getTypes() as $type) { + $typeNamesInOldUnion[$type->name] = true; + } + foreach ($newType->getTypes() as $type) { + if (!isset($typeNamesInOldUnion[$type->name])) { + $addedTypeName = $type->name; + $typesAddedToUnion[] = ['type' => self::DANGEROUS_CHANGE_TYPE_ADDED_TO_UNION, 'description' => "${addedTypeName} 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. + * + * @return array + */ + public static function findValuesRemovedFromEnums( + Schema $oldSchema, Schema $newSchema + ) + { + $oldTypeMap = $oldSchema->getTypeMap(); + $newTypeMap = $newSchema->getTypeMap(); + + $valuesRemovedFromEnums = []; + + foreach ($oldTypeMap as $typeName => $oldType) { + $newType = isset($newTypeMap[$typeName]) ? $newTypeMap[$typeName] : null; + if (!($oldType instanceof EnumType) || !($newType instanceof EnumType)) { + continue; + } + $valuesInNewEnum = []; + foreach ($newType->getValues() as $value) { + $valuesInNewEnum[$value->name] = true; + } + foreach ($oldType->getValues() as $value) { + if (!isset($valuesInNewEnum[$value->name])) { + $valueName = $value->name; + $valuesRemovedFromEnums[] = ['type' => self::BREAKING_CHANGE_VALUE_REMOVED_FROM_ENUM, 'description' => "${valueName} 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. + * + * @return array + */ + public static function findValuesAddedToEnums( + Schema $oldSchema, Schema $newSchema + ) + { + $oldTypeMap = $oldSchema->getTypeMap(); + $newTypeMap = $newSchema->getTypeMap(); + + $valuesAddedToEnums = []; + foreach ($oldTypeMap as $typeName => $oldType) { + $newType = isset($newTypeMap[$typeName]) ? $newTypeMap[$typeName] : null; + if (!($oldType instanceof EnumType) || !($newType instanceof EnumType)) { + continue; + } + $valuesInOldEnum = []; + foreach ($oldType->getValues() as $value) { + $valuesInOldEnum[$value->name] = true; + } + foreach ($newType->getValues() as $value) { + if (!isset($valuesInOldEnum[$value->name])) { + $valueName = $value->name; + $valuesAddedToEnums[] = ['type' => self::DANGEROUS_CHANGE_VALUE_ADDED_TO_ENUM, 'description' => "${valueName} was added to enum type ${typeName}"]; + } + } + } + + return $valuesAddedToEnums; + } + + /** + * @param Schema $oldSchema + * @param Schema $newSchema + * + * @return array + */ + public static function findInterfacesRemovedFromObjectTypes( + Schema $oldSchema, Schema $newSchema + ) + { + $oldTypeMap = $oldSchema->getTypeMap(); + $newTypeMap = $newSchema->getTypeMap(); + + $breakingChanges = []; + foreach ($oldTypeMap as $typeName => $oldType) { + $newType = isset($newTypeMap[$typeName]) ? $newTypeMap[$typeName] : null; + if (!($oldType instanceof ObjectType) || !($newType instanceof ObjectType)) { + continue; + } + + $oldInterfaces = $oldType->getInterfaces(); + $newInterfaces = $newType->getInterfaces(); + foreach ($oldInterfaces as $oldInterface) { + if (!Utils::find($newInterfaces, function (InterfaceType $interface) use ($oldInterface) { + return $interface->name === $oldInterface->name; + })) { + $oldInterfaceName = $oldInterface->name; + $breakingChanges[] = ['type' => self::BREAKING_CHANGE_INTERFACE_REMOVED_FROM_OBJECT, + 'description' => "${typeName} no longer implements interface ${oldInterfaceName}." + ]; + } + } + } + return $breakingChanges; + } + + /** + * @param Type $type + * + * @return bool + */ + private static function isNamedType(Type $type) + { + return ( + $type instanceof ScalarType || + $type instanceof ObjectType || + $type instanceof InterfaceType || + $type instanceof UnionType || + $type instanceof EnumType || + $type instanceof InputObjectType + ); + } +} \ No newline at end of file diff --git a/tests/Utils/FindBreakingChangesTest.php b/tests/Utils/FindBreakingChangesTest.php new file mode 100644 index 0000000..2fc0f6f --- /dev/null +++ b/tests/Utils/FindBreakingChangesTest.php @@ -0,0 +1,1501 @@ +queryType = new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'field1' => [ + 'type' => Type::string() + ] + ] + ]); + } + + public function testShouldDetectIfTypeWasRemoved() + { + $type1 = new ObjectType([ + 'name' => 'Type1', + 'fields' => [ + 'field1' => ['type' => Type::string()], + ] + ]); + $type2 = new ObjectType([ + 'name' => 'Type2', + 'fields' => [ + 'field1' => ['type' => Type::string()], + ] + ]); + $oldSchema = new Schema([ + 'query' => new ObjectType([ + 'name' => 'root', + 'fields' => [ + 'type1' => $type1, + 'type2' => $type2 + ] + ]) + ]); + $newSchema = new Schema([ + 'query' => new ObjectType([ + 'name' => 'root', + 'fields' => [ + 'type2' => $type2 + ] + ]) + ]); + + $this->assertEquals(['type' => FindBreakingChanges::BREAKING_CHANGE_TYPE_REMOVED, 'description' => 'Type1 was removed.'], + FindBreakingChanges::findRemovedTypes($oldSchema, $newSchema)[0] + ); + + $this->assertEquals([], FindBreakingChanges::findRemovedTypes($oldSchema, $oldSchema)); + } + + public function testShouldDetectTypeChanges() + { + $objectType = new ObjectType([ + 'name' => 'ObjectType', + 'fields' => [ + 'field1' => ['type' => Type::string()], + ] + ]); + + $interfaceType = new InterfaceType([ + 'name' => 'Type1', + 'fields' => [ + 'field1' => ['type' => Type::string()] + ] + ]); + + $unionType = new UnionType([ + 'name' => 'Type1', + 'types' => [new ObjectType(['name' => 'blah'])], + ]); + + $oldSchema = new Schema([ + 'query' => new ObjectType([ + 'name' => 'root', + 'fields' => [ + 'type1' => $interfaceType + ] + ]) + ]); + + $newSchema = new Schema([ + 'query' => new ObjectType([ + 'name' => 'root', + 'fields' => [ + 'type1' => $unionType + ] + ]) + ]); + + $this->assertEquals( + ['type' => FindBreakingChanges::BREAKING_CHANGE_TYPE_CHANGED, 'description' => 'Type1 changed from an Interface type to a Union type.'], + FindBreakingChanges::findTypesThatChangedKind($oldSchema, $newSchema)[0] + ); + } + + public function testShouldDetectFieldChangesAndDeletions() + { + $typeA1 = new ObjectType([ + 'name' => 'TypeA', + 'fields' => [ + 'field1' => ['type' => Type::string()], + ] + ]); + $typeA2 = new ObjectType([ + 'name' => 'TypeA', + 'fields' => [ + 'field1' => ['type' => Type::string()], + ] + ]); + $typeB = new ObjectType([ + 'name' => 'TypeB', + 'fields' => [ + 'field1' => ['type' => Type::string()], + ] + ]); + $oldType1 = new InterfaceType([ + 'name' => 'Type1', + 'fields' => [ + 'field1' => ['type' => $typeA1], + 'field2' => ['type' => Type::string()], + 'field3' => ['type' => Type::string()], + 'field4' => ['type' => $typeA1], + 'field6' => ['type' => Type::string()], + 'field7' => ['type' => Type::listOf(Type::string())], + 'field8' => ['type' => Type::int()], + 'field9' => ['type' => Type::nonNull(Type::int())], + 'field10' => ['type' => Type::nonNull(Type::listOf(Type::int()))], + 'field11' => ['type' => Type::int()], + 'field12' => ['type' => Type::listOf(Type::int())], + 'field13' => ['type' => Type::listOf(Type::nonNull(Type::int()))], + 'field14' => ['type' => Type::listOf(Type::int())], + 'field15' => ['type' => Type::listOf(Type::listOf(Type::int()))], + 'field16' => ['type' => Type::nonNull(Type::int())], + 'field17' => ['type' => Type::listOf(Type::int())], + 'field18' => [ + 'type' => Type::listOf(Type::nonNull( + Type::listOf(Type::nonNull(Type::int())))), + ], + ] + ]); + $newType1 = new InterfaceType([ + 'name' => 'Type1', + 'fields' => [ + 'field1' => ['type' => $typeA2], + 'field3' => ['type' => Type::boolean()], + 'field4' => ['type' => $typeB], + 'field5' => ['type' => Type::string()], + 'field6' => ['type' => Type::listOf(Type::string())], + 'field7' => ['type' => Type::string()], + 'field8' => ['type' => Type::nonNull(Type::int())], + 'field9' => ['type' => Type::int()], + 'field10' => ['type' => Type::listOf(Type::int())], + 'field11' => ['type' => Type::nonNull(Type::listOf(Type::int()))], + 'field12' => ['type' => Type::listOf(Type::nonNull(Type::int()))], + 'field13' => ['type' => Type::listOf(Type::int())], + 'field14' => ['type' => Type::listOf(Type::listOf(Type::int()))], + 'field15' => ['type' => Type::listOf(Type::int())], + 'field16' => ['type' => Type::nonNull(Type::listOf(Type::int()))], + 'field17' => ['type' => Type::nonNull(Type::listOf(Type::int()))], + 'field18' => [ + 'type' => Type::listOf( + Type::listOf(Type::nonNull(Type::int()))), + ], + ] + ]); + + $expectedFieldChanges = [ + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_REMOVED, + 'description' => 'Type1->field2 was removed.', + ], + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED, + 'description' => 'Type1->field3 changed type from String to Boolean.', + ], + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED, + 'description' => 'Type1->field4 changed type from TypeA to TypeB.', + ], + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED, + 'description' => 'Type1->field6 changed type from String to [String].', + ], + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED, + 'description' => 'Type1->field7 changed type from [String] to String.', + ], + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED, + 'description' => 'Type1->field9 changed type from Int! to Int.', + ], + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED, + 'description' => 'Type1->field10 changed type from [Int]! to [Int].', + ], + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED, + 'description' => 'Type1->field11 changed type from Int to [Int]!.', + ], + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED, + 'description' => 'Type1->field13 changed type from [Int!] to [Int].', + ], + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED, + 'description' => 'Type1->field14 changed type from [Int] to [[Int]].', + ], + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED, + 'description' => 'Type1->field15 changed type from [[Int]] to [Int].', + ], + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED, + 'description' => 'Type1->field16 changed type from Int! to [Int]!.', + ], + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED, + 'description' => 'Type1->field18 changed type from [[Int!]!] to [[Int!]].', + ], + ]; + + $oldSchema = new Schema([ + 'query' => new ObjectType([ + 'name' => 'root', + 'fields' => [ + 'Type1' => $oldType1 + ] + ]) + ]); + + $newSchema = new Schema([ + 'query' => new ObjectType([ + 'name' => 'root', + 'fields' => [ + 'Type1' => $newType1 + ] + ]) + ]); + + $this->assertEquals($expectedFieldChanges, FindBreakingChanges::findFieldsThatChangedType($oldSchema, $newSchema)); + } + + + public function testShouldDetectInputFieldChanges() + { + $oldInputType = new InputObjectType([ + 'name' => 'InputType1', + 'fields' => [ + 'field1' => [ + 'type' => Type::string(), + ], + 'field2' => [ + 'type' => Type::boolean(), + ], + 'field3' => [ + 'type' => Type::listOf(Type::string()) + ], + 'field4' => [ + 'type' => Type::nonNull(Type::string()), + ], + 'field5' => [ + 'type' => Type::string(), + ], + 'field6' => [ + 'type' => Type::listOf(Type::int()) + ], + 'field7' => [ + 'type' => Type::nonNull(Type::listOf(Type::int())) + ], + 'field8' => [ + 'type' => Type::int(), + ], + 'field9' => [ + 'type' => Type::listOf(Type::int()) + ], + 'field10' => [ + 'type' => Type::listOf(Type::nonNull(Type::int())) + ], + 'field11' => [ + 'type' => Type::listOf(Type::int()) + ], + 'field12' => [ + 'type' => Type::listOf(Type::listOf(Type::int())) + ], + 'field13' => [ + 'type' => Type::nonNull(Type::int()) + ], + 'field14' => [ + 'type' => Type::listOf(Type::nonNull(Type::listOf(Type::int()))) + ], + 'field15' => [ + 'type' => Type::listOf(Type::nonNull(Type::listOf(Type::int()))) + ] + ] + ]); + + $newInputType = new InputObjectType([ + 'name' => 'InputType1', + 'fields' => [ + 'field1' => [ + 'type' => Type::int(), + ], + 'field3' => [ + 'type' => Type::string() + ], + 'field4' => [ + 'type' => Type::string() + ], + 'field5' => [ + 'type' => Type::nonNull(Type::string()) + ], + 'field6' => [ + 'type' => Type::nonNull(Type::listOf(Type::int())) + ], + 'field7' => [ + 'type' => Type::listOf(Type::int()) + ], + 'field8' => [ + 'type' => Type::nonNull(Type::listOf(Type::int())) + ], + 'field9' => [ + 'type' => Type::listOf(Type::nonNull(Type::int())) + ], + 'field10' => [ + 'type' => Type::listOf(Type::int()) + ], + 'field11' => [ + 'type' => Type::listOf(Type::listOf(Type::int())) + ], + 'field12' => [ + 'type' => Type::listOf(Type::int()) + ], + 'field13' => [ + 'type' => Type::nonNull(Type::listOf(Type::int())) + ], + 'field14' => [ + 'type' => Type::listOf(Type::listOf(Type::int())) + ], + 'field15' => [ + 'type' => Type::listOf(Type::nonNull(Type::listOf(Type::nonNull(Type::int())))) + ] + ] + ]); + + $oldSchema = new Schema([ + 'query' => new ObjectType([ + 'name' => 'root', + 'fields' => [ + 'type1' => $oldInputType + ] + ]) + ]); + + $newSchema = new Schema([ + 'query' => new ObjectType([ + 'name' => 'root', + 'fields' => [ + 'type1' => $newInputType + ] + ]) + ]); + + $expectedFieldChanges = [ + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED, + 'description' => 'InputType1->field1 changed type from String to Int.', + ], + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_REMOVED, + 'description' => 'InputType1->field2 was removed.', + ], + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED, + 'description' => 'InputType1->field3 changed type from [String] to String.', + ], + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED, + 'description' => 'InputType1->field5 changed type from String to String!.', + ], + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED, + 'description' => 'InputType1->field6 changed type from [Int] to [Int]!.', + ], + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED, + 'description' => 'InputType1->field8 changed type from Int to [Int]!.', + ], + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED, + 'description' => 'InputType1->field9 changed type from [Int] to [Int!].', + ], + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED, + 'description' => 'InputType1->field11 changed type from [Int] to [[Int]].', + ], + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED, + 'description' => 'InputType1->field12 changed type from [[Int]] to [Int].', + ], + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED, + 'description' => 'InputType1->field13 changed type from Int! to [Int]!.', + ], + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED, + 'description' => 'InputType1->field15 changed type from [[Int]!] to [[Int!]!].', + ], + ]; + + $this->assertEquals($expectedFieldChanges, FindBreakingChanges::findFieldsThatChangedType($oldSchema, $newSchema)); + } + + public function testDetectsNonNullFieldAddedToInputType() + { + $oldInputType = new InputObjectType([ + 'name' => 'InputType1', + 'fields' => [ + 'field1' => Type::string() + ] + ]); + + $newInputType = new InputObjectType([ + 'name' => 'InputType1', + 'fields' => [ + 'field1' => Type::string(), + 'requiredField' => Type::nonNull(Type::int()), + 'optionalField' => Type::boolean() + ] + ]); + + $oldSchema = new Schema([ + 'query' => new ObjectType([ + 'name' => 'root', + 'fields' => [ + 'type1' => $oldInputType + ] + ]) + ]); + + $newSchema = new Schema([ + 'query' => new ObjectType([ + 'name' => 'root', + 'fields' => [ + 'type1' => $newInputType + ] + ]) + ]); + + $this->assertEquals( + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_NON_NULL_INPUT_FIELD_ADDED, + 'description' => 'A non-null field requiredField on input type InputType1 was added.' + ], + FindBreakingChanges::findFieldsThatChangedType($oldSchema, $newSchema)[0] + ); + } + + public function testDetectsIfTypeWasRemovedFromUnion() + { + $type1 = new ObjectType([ + 'name' => 'Type1', + 'fields' => [ + 'field1' => Type::string() + ] + ]); + + $type1a = new ObjectType([ + 'name' => 'Type1', + 'fields' => [ + 'field1' => Type::string() + ] + ]); + + $type2 = new ObjectType([ + 'name' => 'Type2', + 'fields' => [ + 'field1' => Type::string() + ] + ]); + + $type3 = new ObjectType([ + 'name' => 'Type3', + 'fields' => [ + 'field1' => Type::string() + ] + ]); + + $oldUnionType = new UnionType([ + 'name' => 'UnionType1', + 'types' => [$type1, $type2], + 'resolveType' => function () { + } + ]); + + + $newUnionType = new UnionType([ + 'name' => 'UnionType1', + 'types' => [$type1a, $type3], + 'resolveType' => function () { + } + ]); + + $oldSchema = new Schema([ + 'query' => new ObjectType([ + 'name' => 'root', + 'fields' => [ + 'type1' => $oldUnionType + ] + ]) + ]); + + $newSchema = new Schema([ + 'query' => new ObjectType([ + 'name' => 'root', + 'fields' => [ + 'type1' => $newUnionType + ] + ]) + ]); + + $this->assertEquals( + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_TYPE_REMOVED_FROM_UNION, + 'description' => 'Type2 was removed from union type UnionType1.' + ], + FindBreakingChanges::findTypesRemovedFromUnions($oldSchema, $newSchema)[0] + ); + } + + public function testDetectsValuesRemovedFromEnum() + { + $oldEnumType = new EnumType([ + 'name' => 'EnumType1', + 'values' => [ + 'VALUE0' => 0, + 'VALUE1' => 1, + 'VALUE2' => 2 + ] + ]); + $newEnumType = new EnumType([ + 'name' => 'EnumType1', + 'values' => [ + 'VALUE0' => 0, + 'VALUE2' => 1, + 'VALUE3' => 2 + ] + ]); + + $oldSchema = new Schema([ + 'query' => new ObjectType([ + 'name' => 'root', + 'fields' => [ + 'type1' => $oldEnumType + ] + ]) + ]); + + $newSchema = new Schema([ + 'query' => new ObjectType([ + 'name' => 'root', + 'fields' => [ + 'type1' => $newEnumType + ] + ]) + ]); + + $this->assertEquals( + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_VALUE_REMOVED_FROM_ENUM, + 'description' => 'VALUE1 was removed from enum type EnumType1.' + ], + FindBreakingChanges::findValuesRemovedFromEnums($oldSchema, $newSchema)[0] + ); + } + + public function testDetectsRemovalOfFieldArgument() + { + + $oldType = new ObjectType([ + 'name' => 'Type1', + 'fields' => [ + 'field1' => [ + 'type' => Type::string(), + 'args' => [ + 'name' => Type::string() + ] + ] + ] + ]); + + + $inputType = new InputObjectType([ + 'name' => 'InputType1', + 'fields' => [ + 'field1' => Type::string() + ] + ]); + + $oldInterfaceType = new InterfaceType([ + 'name' => 'Interface1', + 'fields' => [ + 'field1' => [ + 'type' => Type::string(), + 'args' => [ + 'arg1' => Type::boolean(), + 'objectArg' => $inputType + ] + ] + ] + ]); + + $newType = new ObjectType([ + 'name' => 'Type1', + 'fields' => [ + 'field1' => [ + 'type' => Type::string(), + 'args' => [] + ] + ] + ]); + + $newInterfaceType = new InterfaceType([ + 'name' => 'Interface1', + 'fields' => [ + 'field1' => Type::string() + ] + ]); + + $oldSchema = new Schema([ + 'query' => new ObjectType([ + 'name' => 'root', + 'fields' => [ + 'type1' => $oldType, + 'type2' => $oldInterfaceType + ], + 'types' => [$oldType, $oldInterfaceType] + ]) + ]); + + $newSchema = new Schema([ + 'query' => new ObjectType([ + 'name' => 'root', + 'fields' => [ + 'type1' => $newType, + 'type2' => $newInterfaceType + ], + 'types' => [$newType, $newInterfaceType] + ]) + ]); + + $expectedChanges = [ + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_ARG_REMOVED, + 'description' => 'Type1->field1 arg name was removed', + ], + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_ARG_REMOVED, + 'description' => 'Interface1->field1 arg arg1 was removed', + ], + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_ARG_REMOVED, + 'description' => 'Interface1->field1 arg objectArg was removed', + ] + ]; + + $this->assertEquals($expectedChanges, FindBreakingChanges::findArgChanges($oldSchema, $newSchema)['breakingChanges']); + } + + public function testDetectsFieldArgumentTypeChange() + { + + $oldType = new ObjectType([ + 'name' => 'Type1', + 'fields' => [ + 'field1' => [ + 'type' => Type::string(), + 'args' => [ + 'arg1' => Type::string(), + 'arg2' => Type::string(), + 'arg3' => Type::listOf(Type::string()), + 'arg4' => Type::string(), + 'arg5' => Type::nonNull(Type::string()), + 'arg6' => Type::nonNull(Type::string()), + 'arg7' => Type::nonNull(Type::listOf(Type::int())), + 'arg8' => Type::int(), + 'arg9' => Type::listOf(Type::int()), + 'arg10' => Type::listOf(Type::nonNull(Type::int())), + 'arg11' => Type::listOf(Type::int()), + 'arg12' => Type::listOf(Type::listOf(Type::int())), + 'arg13' => Type::nonNull(Type::int()), + 'arg14' => Type::listOf(Type::nonNull(Type::listOf(Type::int()))), + 'arg15' => Type::listOf(Type::nonNull(Type::listOf(Type::int()))) + ] + ] + ] + ]); + + $newType = new ObjectType([ + 'name' => 'Type1', + 'fields' => [ + 'field1' => [ + 'type' => Type::string(), + 'args' => [ + 'arg1' => Type::int(), + 'arg2' => Type::listOf(Type::string()), + 'arg3' => Type::string(), + 'arg4' => Type::nonNull(Type::string()), + 'arg5' => Type::int(), + 'arg6' => Type::nonNull(Type::int()), + 'arg7' => Type::listOf(Type::int()), + 'arg8' => Type::nonNull(Type::listOf(Type::int())), + 'arg9' => Type::listOf(Type::nonNull(Type::int())), + 'arg10' => Type::listOf(Type::int()), + 'arg11' => Type::listOf(Type::listOf(Type::int())), + 'arg12' => Type::listOf(Type::int()), + 'arg13' => Type::nonNull(Type::listOf(Type::int())), + 'arg14' => Type::listOf(Type::listOf(Type::int())), + 'arg15' => Type::listOf(Type::nonNull(Type::listOf(Type::nonNull(Type::int())))) + ] + ] + ] + ]); + + $oldSchema = new Schema([ + 'query' => new ObjectType([ + 'name' => 'root', + 'fields' => [ + 'type1' => $oldType + ] + ]) + ]); + + $newSchema = new Schema([ + 'query' => new ObjectType([ + 'name' => 'root', + 'fields' => [ + 'type1' => $newType + ] + ]) + ]); + + $expectedChanges = [ + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_ARG_CHANGED, + 'description' => 'Type1->field1 arg arg1 has changed type from String to Int.', + ], + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_ARG_CHANGED, + 'description' => 'Type1->field1 arg arg2 has changed type from String to [String].' + ], + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_ARG_CHANGED, + 'description' => 'Type1->field1 arg arg3 has changed type from [String] to String.', + ], + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_ARG_CHANGED, + 'description' => 'Type1->field1 arg arg4 has changed type from String to String!.', + ], + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_ARG_CHANGED, + 'description' => 'Type1->field1 arg arg5 has changed type from String! to Int.', + ], + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_ARG_CHANGED, + 'description' => 'Type1->field1 arg arg6 has changed type from String! to Int!.', + ], + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_ARG_CHANGED, + 'description' => 'Type1->field1 arg arg8 has changed type from Int to [Int]!.', + ], + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_ARG_CHANGED, + 'description' => 'Type1->field1 arg arg9 has changed type from [Int] to [Int!].', + ], + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_ARG_CHANGED, + 'description' => 'Type1->field1 arg arg11 has changed type from [Int] to [[Int]].', + ], + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_ARG_CHANGED, + 'description' => 'Type1->field1 arg arg12 has changed type from [[Int]] to [Int].', + ], + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_ARG_CHANGED, + 'description' => 'Type1->field1 arg arg13 has changed type from Int! to [Int]!.', + ], + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_ARG_CHANGED, + 'description' => 'Type1->field1 arg arg15 has changed type from [[Int]!] to [[Int!]!].', + ], + ]; + + $this->assertEquals($expectedChanges, FindBreakingChanges::findArgChanges($oldSchema, $newSchema)['breakingChanges']); + } + + public function testDetectsAdditionOfFieldArg() + { + $oldType = new ObjectType([ + 'name' => 'Type1', + 'fields' => [ + 'field1' => [ + 'type' => Type::string(), + 'args' => [ + 'arg1' => Type::string() + ]] + ] + ]); + $newType = new ObjectType([ + 'name' => 'Type1', + 'fields' => [ + 'field1' => [ + 'type' => Type::string(), + 'args' => [ + 'arg1' => Type::string(), + 'newRequiredArg' => Type::nonNull(Type::string()), + 'newOptionalArg' => Type::int() + ]] + ] + ]); + $oldSchema = new Schema([ + 'query' => new ObjectType([ + 'name' => 'root', + 'fields' => [ + 'type1' => $oldType, + ] + ]) + ]); + $newSchema = new Schema([ + 'query' => new ObjectType([ + 'name' => 'root', + 'fields' => [ + 'type1' => $newType + ] + ]) + ]); + + $this->assertEquals( + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_NON_NULL_ARG_ADDED, + 'description' => 'A non-null arg newRequiredArg on Type1->field1 was added.' + ], + FindBreakingChanges::findArgChanges($oldSchema, $newSchema)['breakingChanges'][0]); + } + + public function testDoesNotFlagArgsWithSameTypeSignature() + { + $inputType1a = new InputObjectType([ + 'name' => 'InputType1', + 'fields' => [ + 'field1' => Type::string() + ] + ]); + + $inputType1b = new InputObjectType([ + 'name' => 'InputType1', + 'fields' => [ + 'field1' => Type::string() + ] + ]); + + $oldType = new ObjectType([ + 'name' => 'Type1', + 'fields' => [ + 'field1' => [ + 'type' => Type::int(), + 'args' => [ + 'arg1' => Type::nonNull(Type::int()), + 'arg2' => $inputType1a + ] + ] + ] + ]); + + $newType = new ObjectType([ + 'name' => 'Type1', + 'fields' => [ + 'field1' => [ + 'type' => Type::int(), + 'args' => [ + 'arg1' => Type::nonNull(Type::int()), + 'arg2' => $inputType1b + ] + ] + ] + ]); + + $oldSchema = new Schema([ + 'query' => new ObjectType([ + 'name' => 'root', + 'fields' => [ + 'type1' => $oldType, + ] + ]) + ]); + $newSchema = new Schema([ + 'query' => new ObjectType([ + 'name' => 'root', + 'fields' => [ + 'type1' => $newType + ] + ]) + ]); + + $this->assertEquals([], FindBreakingChanges::findArgChanges($oldSchema, $newSchema)['breakingChanges']); + } + + public function testArgsThatMoveAwayFromNonNull() + { + $oldType = new ObjectType([ + 'name' => 'Type1', + 'fields' => [ + 'field1' => [ + 'type' => Type::string(), + 'args' => [ + 'arg1' => Type::nonNull(Type::string()), + ] + ] + ] + ]); + $newType = new ObjectType([ + 'name' => 'Type1', + 'fields' => [ + 'field1' => [ + 'type' => Type::string(), + 'args' => [ + 'arg1' => Type::string() + ] + ] + ] + ]); + + $oldSchema = new Schema([ + 'query' => new ObjectType([ + 'name' => 'root', + 'fields' => [ + 'type1' => $oldType, + ] + ]) + ]); + $newSchema = new Schema([ + 'query' => new ObjectType([ + 'name' => 'root', + 'fields' => [ + 'type1' => $newType + ] + ]) + ]); + + $this->assertEquals([], FindBreakingChanges::findArgChanges($oldSchema, $newSchema)['breakingChanges']); + } + + public function testDetectsRemovalOfInterfaces() + { + $interface1 = new InterfaceType([ + 'name' => 'Interface1', + 'fields' => [ + 'field1' => Type::string() + ], + 'resolveType' => function () { + } + ]); + $oldType = new ObjectType([ + 'name' => 'Type1', + 'interfaces' => [$interface1], + 'fields' => [ + 'field1' => Type::string() + ] + ]); + $newType = new ObjectType([ + 'name' => 'Type1', + 'fields' => [ + 'field1' => Type::string() + ] + ]); + + $oldSchema = new Schema([ + 'query' => new ObjectType([ + 'name' => 'root', + 'fields' => [ + 'type1' => $oldType, + ] + ]) + ]); + $newSchema = new Schema([ + 'query' => new ObjectType([ + 'name' => 'root', + 'fields' => [ + 'type1' => $newType + ] + ]) + ]); + + $this->assertEquals( + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_INTERFACE_REMOVED_FROM_OBJECT, + 'description' => 'Type1 no longer implements interface Interface1.' + ], + FindBreakingChanges::findInterfacesRemovedFromObjectTypes($oldSchema, $newSchema)[0]); + } + + public function testDetectsAllBreakingChanges() + { + $typeThatGetsRemoved = new ObjectType([ + 'name' => 'TypeThatGetsRemoved', + 'fields' => [ + 'field1' => Type::string() + ] + ]); + + $argThatChanges = new ObjectType([ + 'name' => 'ArgThatChanges', + 'fields' => [ + 'field1' => [ + 'type' => Type::string(), + 'args' => [ + 'id' => Type::int() + ] + ] + ] + ]); + + $argChanged = new ObjectType([ + 'name' => 'ArgThatChanges', + 'fields' => [ + 'field1' => [ + 'type' => Type::string(), + 'args' => [ + 'id' => Type::string() + ] + ] + ] + ]); + + $typeThatChangesTypeOld = new ObjectType([ + 'name' => 'TypeThatChangesType', + 'fields' => [ + 'field1' => Type::string() + ] + ]); + + $typeThatChangesTypeNew = new InterfaceType([ + 'name' => 'TypeThatChangesType', + 'fields' => [ + 'field1' => Type::string() + ] + ]); + + $typeThatHasBreakingFieldChangesOld = new InterfaceType([ + 'name' => 'TypeThatHasBreakingFieldChanges', + 'fields' => [ + 'field1' => Type::string(), + 'field2' => Type::string() + ] + ]); + + $typeThatHasBreakingFieldChangesNew = new InterfaceType([ + 'name' => 'TypeThatHasBreakingFieldChanges', + 'fields' => [ + 'field2' => Type::boolean() + ] + ]); + + $typeInUnion1 = new ObjectType([ + 'name' => 'TypeInUnion1', + 'fields' => [ + 'field1' => Type::string() + ] + ]); + + $typeInUnion2 = new ObjectType([ + 'name' => 'TypeInUnion2', + 'fields' => [ + 'field1' => Type::string() + ] + ]); + + $unionTypeThatLosesATypeOld = new UnionType([ + 'name' => 'UnionTypeThatLosesAType', + 'types' => [$typeInUnion1, $typeInUnion2], + 'resolveType' => function () { + } + ]); + + $unionTypeThatLosesATypeNew = new UnionType([ + 'name' => 'UnionTypeThatLosesAType', + 'types' => [$typeInUnion1], + 'resolveType' => function () { + } + ]); + + $enumTypeThatLosesAValueOld = new EnumType([ + 'name' => 'EnumTypeThatLosesAValue', + 'values' => [ + 'VALUE0' => 0, + 'VALUE1' => 1, + 'VALUE2' => 2 + ] + ]); + + $enumTypeThatLosesAValueNew = new EnumType([ + 'name' => 'EnumTypeThatLosesAValue', + 'values' => [ + 'VALUE1' => 1, + 'VALUE2' => 2 + ] + ]); + + $interface1 = new InterfaceType([ + 'name' => 'Interface1', + 'fields' => [ + 'field1' => Type::string() + ], + 'resolveType' => function () { + } + ]); + + $typeThatLosesInterfaceOld = new ObjectType([ + 'name' => 'TypeThatLosesInterface1', + 'interfaces' => [$interface1], + 'fields' => [ + 'field1' => Type::string() + ] + ]); + + $typeThatLosesInterfaceNew = new ObjectType([ + 'name' => 'TypeThatLosesInterface1', + 'fields' => [ + 'field1' => Type::string() + ] + ]); + + $oldSchema = new Schema([ + 'query' => $this->queryType, + 'types' => + [ + 'TypeThatGetsRemoved' => $typeThatGetsRemoved, + 'TypeThatChangesType' => $typeThatChangesTypeOld, + 'TypeThatHasBreakingFieldChanges' => $typeThatHasBreakingFieldChangesOld, + 'UnionTypeThatLosesAType' => $unionTypeThatLosesATypeOld, + 'EnumTypeThatLosesAValue' => $enumTypeThatLosesAValueOld, + 'ArgThatChanges' => $argThatChanges, + 'TypeThatLosesInterface' => $typeThatLosesInterfaceOld + ] + ]); + + $newSchema = new Schema([ + 'query' => $this->queryType, + 'types' => + [ + 'TypeThatChangesType' => $typeThatChangesTypeNew, + 'TypeThatHasBreakingFieldChanges' => $typeThatHasBreakingFieldChangesNew, + 'UnionTypeThatLosesAType' => $unionTypeThatLosesATypeNew, + 'EnumTypeThatLosesAValue' => $enumTypeThatLosesAValueNew, + 'ArgThatChanges' => $argChanged, + 'TypeThatLosesInterface' => $typeThatLosesInterfaceNew, + 'Interface1' => $interface1 + ] + ]); + + $expectedBreakingChanges = [ + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_TYPE_REMOVED, + 'description' => 'TypeThatGetsRemoved was removed.', + ], + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_TYPE_REMOVED, + 'description' => 'TypeInUnion2 was removed.', + ], + /* + // NB the below assertion is included in the graphql-js tests, but it makes no sense. + // Seriously, look for what `int` type was supposed to be removed between the two schemas. There is none. + // I honestly think it's a bug in the js implementation and was put into the test just to make it pass. + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_TYPE_REMOVED, + 'description' => 'Int was removed.' + ],*/ + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_TYPE_CHANGED, + 'description' => 'TypeThatChangesType changed from an Object type to an Interface type.', + ], + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_REMOVED, + 'description' => 'TypeThatHasBreakingFieldChanges->field1 was removed.', + ], + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED, + 'description' => 'TypeThatHasBreakingFieldChanges->field2 changed type from String to Boolean.', + ], + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_TYPE_REMOVED_FROM_UNION, + 'description' => 'TypeInUnion2 was removed from union type UnionTypeThatLosesAType.', + ], + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_VALUE_REMOVED_FROM_ENUM, + 'description' => 'VALUE0 was removed from enum type EnumTypeThatLosesAValue.', + ], + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_ARG_CHANGED, + 'description' => 'ArgThatChanges->field1 arg id has changed type from Int to String.', + ], + [ + 'type' => FindBreakingChanges::BREAKING_CHANGE_INTERFACE_REMOVED_FROM_OBJECT, + 'description' => 'TypeThatLosesInterface1 no longer implements interface Interface1.', + ] + ]; + + $this->assertEquals($expectedBreakingChanges, FindBreakingChanges::findBreakingChanges($oldSchema, $newSchema)); + } + + // findDangerousChanges tests below here + + public function testFindDangerousArgChanges() + { + $oldType = new ObjectType([ + 'name' => 'Type1', + 'fields' => [ + 'field1' => [ + 'type' => Type::string(), + 'args' => [ + 'name' => [ + 'type' => Type::string(), + 'defaultValue' => 'test' + ] + ] + ] + ] + ]); + + $newType = new ObjectType([ + 'name' => 'Type1', + 'fields' => [ + 'field1' => [ + 'type' => Type::string(), + 'args' => [ + 'name' => [ + 'type' => Type::string(), + 'defaultValue' => 'Testertest' + ] + ] + ] + ] + ]); + + $oldSchema = new Schema([ + 'query' => $this->queryType, + 'types' => [ + $oldType + ] + ]); + + $newSchema = new Schema([ + 'query' => $this->queryType, + 'types' => [ + $newType + ] + ]); + + $this->assertEquals( + [ + 'type' => FindBreakingChanges::DANGEROUS_CHANGE_ARG_DEFAULT_VALUE, + 'description' => 'Type1->field1 arg name has changed defaultValue' + ], + FindBreakingChanges::findArgChanges($oldSchema, $newSchema)['dangerousChanges'][0] + ); + } + + public function testDetectsEnumValueAdditions() + { + $oldEnumType = new EnumType([ + 'name' => 'EnumType1', + 'values' => [ + 'VALUE0' => 0, + 'VALUE1' => 1, + ] + ]); + $newEnumType = new EnumType([ + 'name' => 'EnumType1', + 'values' => [ + 'VALUE0' => 0, + 'VALUE1' => 1, + 'VALUE2' => 2 + ] + ]); + + $oldSchema = new Schema([ + 'query' => $this->queryType, + 'types' => [ + $oldEnumType + ] + ]); + + $newSchema = new Schema([ + 'query' => $this->queryType, + 'types' => [ + $newEnumType + ] + ]); + + $this->assertEquals( + [ + 'type' => FindBreakingChanges::DANGEROUS_CHANGE_VALUE_ADDED_TO_ENUM, + 'description' => 'VALUE2 was added to enum type EnumType1' + ], + FindBreakingChanges::findValuesAddedToEnums($oldSchema, $newSchema)[0] + ); + } + + public function testDetectsAdditionsToUnionType() + { + $type1 = new ObjectType([ + 'name' => 'Type1', + 'fields' => [ + 'field1' => Type::string() + ] + ]); + + $type1a = new ObjectType([ + 'name' => 'Type1', + 'fields' => [ + 'field1' => Type::string() + ] + ]); + + $type2 = new ObjectType([ + 'name' => 'Type2', + 'fields' => [ + 'field1' => Type::string() + ] + ]); + + $oldUnionType = new UnionType([ + 'name' => 'UnionType1', + 'types' => [$type1], + 'resolveType' => function () { + } + ]); + + $newUnionType = new UnionType([ + 'name' => 'UnionType1', + 'types' => [$type1a, $type2], + 'resolveType' => function () { + } + ]); + + $oldSchema = new Schema([ + 'query' => $this->queryType, + 'types' => [ + $oldUnionType + ] + ]); + + $newSchema = new Schema([ + 'query' => $this->queryType, + 'types' => [ + $newUnionType + ] + ]); + + $this->assertEquals( + [ + 'type' => FindBreakingChanges::DANGEROUS_CHANGE_TYPE_ADDED_TO_UNION, + 'description' => 'Type2 was added to union type UnionType1' + ], + FindBreakingChanges::findTypesAddedToUnions($oldSchema, $newSchema)[0] + ); + } + + public function testFindsAllDangerousChanges() + { + $enumThatGainsAValueOld = new EnumType([ + 'name' => 'EnumType1', + 'values' => [ + 'VALUE0' => 0, + 'VALUE1' => 1, + ] + ]); + $enumThatGainsAValueNew = new EnumType([ + 'name' => 'EnumType1', + 'values' => [ + 'VALUE0' => 0, + 'VALUE1' => 1, + 'VALUE2' => 2 + ] + ]); + + $oldType = new ObjectType([ + 'name' => 'Type1', + 'fields' => [ + 'field1' => [ + 'type' => Type::string(), + 'args' => [ + 'name' => [ + 'type' => Type::string(), + 'defaultValue' => 'test' + ] + ] + ] + ] + ]); + + $newType = new ObjectType([ + 'name' => 'Type1', + 'fields' => [ + 'field1' => [ + 'type' => Type::string(), + 'args' => [ + 'name' => [ + 'type' => Type::string(), + 'defaultValue' => 'Testertest' + ] + ] + ] + ] + ]); + + $typeInUnion1 = new ObjectType([ + 'name' => 'TypeInUnion1', + 'fields' => [ + 'field1' => Type::string() + ] + ]); + + $typeInUnion2 = new ObjectType([ + 'name' => 'TypeInUnion2', + 'fields' => [ + 'field1' => Type::string() + ] + ]); + + $unionTypeThatGainsATypeOld = new UnionType([ + 'name' => 'UnionType1', + 'types' => [$typeInUnion1], + 'resolveType' => function () { + } + ]); + + $unionTypeThatGainsATypeNew = new UnionType([ + 'name' => 'UnionType1', + 'types' => [$typeInUnion1, $typeInUnion2], + 'resolveType' => function () { + } + ]); + + $oldSchema = new Schema([ + 'query' => $this->queryType, + 'types' => [ + $oldType, + $enumThatGainsAValueOld, + $unionTypeThatGainsATypeOld + ] + ]); + + $newSchema = new Schema([ + 'query' => $this->queryType, + 'types' => [ + $newType, + $enumThatGainsAValueNew, + $unionTypeThatGainsATypeNew + ] + ]); + + $expectedDangerousChanges = [ + [ + 'description' => 'Type1->field1 arg name has changed defaultValue', + 'type' => FindBreakingChanges::DANGEROUS_CHANGE_ARG_DEFAULT_VALUE + ], + [ + 'description' => 'VALUE2 was added to enum type EnumType1', + 'type' => FindBreakingChanges::DANGEROUS_CHANGE_VALUE_ADDED_TO_ENUM + ], + [ + 'type' => FindBreakingChanges::DANGEROUS_CHANGE_TYPE_ADDED_TO_UNION, + 'description' => 'TypeInUnion2 was added to union type UnionType1', + ] + ]; + + $this->assertEquals($expectedDangerousChanges, FindBreakingChanges::findDangerousChanges($oldSchema, $newSchema)); + } +} \ No newline at end of file