diff --git a/src/utilities/__tests__/findSchemaChanges-test.ts b/src/utilities/__tests__/findSchemaChanges-test.ts index 8c21781554..4aa351707b 100644 --- a/src/utilities/__tests__/findSchemaChanges-test.ts +++ b/src/utilities/__tests__/findSchemaChanges-test.ts @@ -2,7 +2,12 @@ import { expect } from 'chai'; import { describe, it } from 'mocha'; import { buildSchema } from '../buildASTSchema.js'; -import { findSchemaChanges, SafeChangeType } from '../findSchemaChanges.js'; +import { + BreakingChangeType, + DangerousChangeType, + findSchemaChanges, + SafeChangeType, +} from '../findSchemaChanges.js'; describe('findSchemaChanges', () => { it('should detect if a type was added', () => { @@ -171,6 +176,147 @@ describe('findSchemaChanges', () => { ]); }); + it('should detect if a changes argument safely', () => { + const oldSchema = buildSchema(` + directive @Foo(foo: String!) on FIELD_DEFINITION + + type Query { + foo: String + } + `); + + const newSchema = buildSchema(` + directive @Foo(foo: String) on FIELD_DEFINITION + + type Query { + foo: String + } + `); + expect(findSchemaChanges(oldSchema, newSchema)).to.deep.equal([ + { + description: + 'Argument @Foo(foo:) has changed type from String! to String.', + type: SafeChangeType.ARG_CHANGED_KIND_SAFE, + }, + ]); + }); + + it('should detect if a default value is added to an argument', () => { + const oldSchema = buildSchema(` + directive @Foo(foo: String) on FIELD_DEFINITION + + type Query { + foo: String + } + `); + + const newSchema = buildSchema(` + directive @Foo(foo: String = "Foo") on FIELD_DEFINITION + + type Query { + foo: String + } + `); + expect(findSchemaChanges(oldSchema, newSchema)).to.deep.equal([ + { + description: '@Foo(foo:) added a defaultValue "Foo".', + type: SafeChangeType.ARG_DEFAULT_VALUE_ADDED, + }, + ]); + }); + + it('should detect if a default value is removed from an argument', () => { + const newSchema = buildSchema(` + directive @Foo(foo: String) on FIELD_DEFINITION + + type Query { + foo: String + } + `); + + const oldSchema = buildSchema(` + directive @Foo(foo: String = "Foo") on FIELD_DEFINITION + + type Query { + foo: String + } + `); + expect(findSchemaChanges(oldSchema, newSchema)).to.deep.equal([ + { + description: '@Foo(foo:) defaultValue was removed.', + type: DangerousChangeType.ARG_DEFAULT_VALUE_CHANGE, + }, + ]); + }); + + it('should detect if a default value is changed in an argument', () => { + const oldSchema = buildSchema(` + directive @Foo(foo: String = "Bar") on FIELD_DEFINITION + + type Query { + foo: String + } + `); + + const newSchema = buildSchema(` + directive @Foo(foo: String = "Foo") on FIELD_DEFINITION + + type Query { + foo: String + } + `); + expect(findSchemaChanges(oldSchema, newSchema)).to.deep.equal([ + { + description: '@Foo(foo:) has changed defaultValue from "Bar" to "Foo".', + type: DangerousChangeType.ARG_DEFAULT_VALUE_CHANGE, + }, + ]); + }); + + it('should detect if a directive argument does a breaking change', () => { + const oldSchema = buildSchema(` + directive @Foo(foo: String) on FIELD_DEFINITION + + type Query { + foo: String + } + `); + + const newSchema = buildSchema(` + directive @Foo(foo: String!) on FIELD_DEFINITION + + type Query { + foo: String + } + `); + expect(findSchemaChanges(oldSchema, newSchema)).to.deep.equal([ + { + description: + 'Argument @Foo(foo:) has changed type from String to String!.', + type: BreakingChangeType.ARG_CHANGED_KIND, + }, + ]); + }); + + it('should not detect if a directive argument default value does not change', () => { + const oldSchema = buildSchema(` + directive @Foo(foo: String = "FOO") on FIELD_DEFINITION + + type Query { + foo: String + } + `); + + const newSchema = buildSchema(` + directive @Foo(foo: String = "FOO") on FIELD_DEFINITION + + type Query { + foo: String + } + `); + expect(findSchemaChanges(oldSchema, newSchema)).to.deep.equal([]); + }); + it('should detect if a directive changes description', () => { const oldSchema = buildSchema(` directive @Foo on FIELD_DEFINITION diff --git a/src/utilities/findSchemaChanges.ts b/src/utilities/findSchemaChanges.ts index b9c49aa59e..4cffd5aad6 100644 --- a/src/utilities/findSchemaChanges.ts +++ b/src/utilities/findSchemaChanges.ts @@ -196,6 +196,56 @@ function findDirectiveChanges( } for (const [oldArg, newArg] of argsDiff.persisted) { + const isSafe = isChangeSafeForInputObjectFieldOrFieldArg( + oldArg.type, + newArg.type, + ); + + if (!isSafe) { + schemaChanges.push({ + type: BreakingChangeType.ARG_CHANGED_KIND, + description: + `Argument @${oldDirective.name}(${oldArg.name}:) has changed type from ` + + `${String(oldArg.type)} to ${String(newArg.type)}.`, + }); + } else if (oldArg.defaultValue !== undefined) { + if (newArg.defaultValue === undefined) { + schemaChanges.push({ + type: DangerousChangeType.ARG_DEFAULT_VALUE_CHANGE, + description: `@${oldDirective.name}(${oldArg.name}:) defaultValue was removed.`, + }); + } else { + // Since we looking only for client's observable changes we should + // compare default values in the same representation as they are + // represented inside introspection. + const oldValueStr = stringifyValue(oldArg.defaultValue, oldArg.type); + const newValueStr = stringifyValue(newArg.defaultValue, newArg.type); + + if (oldValueStr !== newValueStr) { + schemaChanges.push({ + type: DangerousChangeType.ARG_DEFAULT_VALUE_CHANGE, + description: `@${oldDirective.name}(${oldArg.name}:) has changed defaultValue from ${oldValueStr} to ${newValueStr}.`, + }); + } + } + } else if ( + newArg.defaultValue !== undefined && + oldArg.defaultValue === undefined + ) { + const newValueStr = stringifyValue(newArg.defaultValue, newArg.type); + schemaChanges.push({ + type: SafeChangeType.ARG_DEFAULT_VALUE_ADDED, + description: `@${oldDirective.name}(${oldArg.name}:) added a defaultValue ${newValueStr}.`, + }); + } else if (oldArg.type.toString() !== newArg.type.toString()) { + schemaChanges.push({ + type: SafeChangeType.ARG_CHANGED_KIND_SAFE, + description: + `Argument @${oldDirective.name}(${oldArg.name}:) has changed type from ` + + `${String(oldArg.type)} to ${String(newArg.type)}.`, + }); + } + if (oldArg.description !== newArg.description) { schemaChanges.push({ type: SafeChangeType.DESCRIPTION_CHANGED,