diff --git a/packages/core/__tests__/diff/directive-usage.test.ts b/packages/core/__tests__/diff/directive-usage.test.ts index 6ce1400400..9bdc005817 100644 --- a/packages/core/__tests__/diff/directive-usage.test.ts +++ b/packages/core/__tests__/diff/directive-usage.test.ts @@ -405,4 +405,51 @@ describe('directive-usage', () => { expect(change.message).toEqual("Directive 'external' was removed from Scalar type 'Foo'"); }); }); + + describe('object-level directives', () => { + test('added directive', async () => { + const a = buildSchema(/* GraphQL */ ` + directive @external on OBJECT + type Foo { + a: String + } + `); + const b = buildSchema(/* GraphQL */ ` + directive @external on OBJECT + type Foo @external { + a: String + } + `); + + const changes = await diff(a, b); + console.log('changes', changes); + const change = findFirstChangeByPath(changes, 'Foo.external'); + + expect(change.criticality.level).toEqual(CriticalityLevel.Dangerous); + expect(change.type).toEqual('DIRECTIVE_USAGE_OBJECT_ADDED'); + expect(change.message).toEqual("Directive 'external' was added to Object type 'Foo'"); + }); + test('removed directive', async () => { + const a = buildSchema(/* GraphQL */ ` + directive @external on OBJECT + type Foo @external { + a: String + } + `); + const b = buildSchema(/* GraphQL */ ` + directive @external on OBJECT + type Foo { + a: String + } + `); + + const changes = await diff(a, b); + console.log('changes', changes); + const change = findFirstChangeByPath(changes, 'Foo.external'); + + expect(change.criticality.level).toEqual(CriticalityLevel.Breaking); + expect(change.type).toEqual('DIRECTIVE_USAGE_OBJECT_REMOVED'); + expect(change.message).toEqual("Directive 'external' was removed from Object type 'Foo'"); + }); + }); }); diff --git a/packages/core/src/diff/changes/change.ts b/packages/core/src/diff/changes/change.ts index 02488f30d2..e8306ab3e7 100644 --- a/packages/core/src/diff/changes/change.ts +++ b/packages/core/src/diff/changes/change.ts @@ -77,6 +77,8 @@ export enum ChangeType { DirectiveUsageFieldRemoved = 'DIRECTIVE_USAGE_FIELD_REMOVED', DirectiveUsageScalarAdded = 'DIRECTIVE_USAGE_SCALAR_ADDED', DirectiveUsageScalarRemoved = 'DIRECTIVE_USAGE_SCALAR_REMOVED', + DirectiveUsageObjectAdded = 'DIRECTIVE_USAGE_OBJECT_ADDED', + DirectiveUsageObjectRemoved = 'DIRECTIVE_USAGE_OBJECT_REMOVED', } export enum CriticalityLevel { @@ -701,6 +703,22 @@ export type DirectiveUsageScalarRemovedChange = { }; }; +export type DirectiveUsageObjectAddedChange = { + type: ChangeType.DirectiveUsageObjectAdded; + meta: { + objectName: string; + addedDirectiveName: string; + }; +}; + +export type DirectiveUsageObjectRemovedChange = { + type: ChangeType.DirectiveUsageObjectRemoved; + meta: { + objectName: string; + removedDirectiveName: string; + }; +}; + type Changes = { [ChangeType.TypeAdded]: TypeAddedChange; [ChangeType.TypeRemoved]: TypeRemovedChange; @@ -788,6 +806,8 @@ type Changes = { [ChangeType.DirectiveUsageFieldRemoved]: DirectiveUsageFieldRemovedChange; [ChangeType.DirectiveUsageScalarAdded]: DirectiveUsageScalarAddedChange; [ChangeType.DirectiveUsageScalarRemoved]: DirectiveUsageScalarRemovedChange; + [ChangeType.DirectiveUsageObjectAdded]: DirectiveUsageObjectAddedChange; + [ChangeType.DirectiveUsageObjectRemoved]: DirectiveUsageObjectRemovedChange; }; export type SerializableChange = Changes[keyof Changes]; diff --git a/packages/core/src/diff/changes/object.ts b/packages/core/src/diff/changes/object.ts index ba32649bdf..8092e04f28 100644 --- a/packages/core/src/diff/changes/object.ts +++ b/packages/core/src/diff/changes/object.ts @@ -1,8 +1,10 @@ -import { GraphQLInterfaceType, GraphQLObjectType } from 'graphql'; +import { ConstDirectiveNode, GraphQLInterfaceType, GraphQLObjectType } from 'graphql'; import { Change, ChangeType, CriticalityLevel, + DirectiveUsageObjectAddedChange, + DirectiveUsageObjectRemovedChange, ObjectTypeInterfaceAddedChange, ObjectTypeInterfaceRemovedChange, } from './change.js'; @@ -68,3 +70,65 @@ export function objectTypeInterfaceRemoved( }, }); } + +function buildObjectDiractiveUsageRemovedMessage(args: DirectiveUsageObjectRemovedChange['meta']) { + return `Directive '${args.removedDirectiveName}' was removed from Object type '${args.objectName}'`; +} + +export function objectDiractiveUsageRemovedFromMeta(args: DirectiveUsageObjectRemovedChange) { + return { + type: ChangeType.DirectiveUsageObjectRemoved, + criticality: { + level: CriticalityLevel.Breaking, + reason: + 'Removing a directive from an object type can cause existing queries that use this directive to error.', + }, + message: buildObjectDiractiveUsageRemovedMessage(args.meta), + meta: args.meta, + path: [args.meta.objectName, args.meta.removedDirectiveName].join('.'), + } as const; +} + +export function objectDiractiveUsageRemoved( + oldObject: GraphQLObjectType, + directive: ConstDirectiveNode, +): Change { + return objectDiractiveUsageRemovedFromMeta({ + type: ChangeType.DirectiveUsageObjectRemoved, + meta: { + objectName: oldObject.name, + removedDirectiveName: directive.name.value, + }, + }); +} + +function buildObjectDiractiveUsageAddedMessage(args: DirectiveUsageObjectAddedChange['meta']) { + return `Directive '${args.addedDirectiveName}' was added to Object type '${args.objectName}'`; +} + +export function objectDiractiveUsageAddedFromMeta(args: DirectiveUsageObjectAddedChange) { + return { + type: ChangeType.DirectiveUsageObjectAdded, + criticality: { + level: CriticalityLevel.Dangerous, + reason: + 'Adding a directive to an object type can cause existing queries that use this directive to error.', + }, + message: buildObjectDiractiveUsageAddedMessage(args.meta), + meta: args.meta, + path: [args.meta.objectName, args.meta.addedDirectiveName].join('.'), + } as const; +} + +export function objectDiractiveUsageAdded( + newObject: GraphQLObjectType, + directive: ConstDirectiveNode, +): Change { + return objectDiractiveUsageAddedFromMeta({ + type: ChangeType.DirectiveUsageObjectAdded, + meta: { + objectName: newObject.name, + addedDirectiveName: directive.name.value, + }, + }); +} diff --git a/packages/core/src/diff/object.ts b/packages/core/src/diff/object.ts index 6cae1214e7..bcf4accd1c 100644 --- a/packages/core/src/diff/object.ts +++ b/packages/core/src/diff/object.ts @@ -1,7 +1,12 @@ import { GraphQLObjectType } from 'graphql'; import { compareLists } from '../utils/compare.js'; import { fieldAdded, fieldRemoved } from './changes/field.js'; -import { objectTypeInterfaceAdded, objectTypeInterfaceRemoved } from './changes/object.js'; +import { + objectDiractiveUsageAdded, + objectDiractiveUsageRemoved, + objectTypeInterfaceAdded, + objectTypeInterfaceRemoved, +} from './changes/object.js'; import { changesInField } from './field.js'; import { AddChange } from './schema.js'; @@ -36,4 +41,13 @@ export function changesInObject( changesInField(oldType, f.oldVersion, f.newVersion, addChange); }, }); + + compareLists(oldType.astNode?.directives || [], newType.astNode?.directives || [], { + onAdded(diractive) { + addChange(objectDiractiveUsageAdded(newType, diractive)); + }, + onRemoved(diractive) { + addChange(objectDiractiveUsageRemoved(newType, diractive)); + }, + }); }