Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
cf7fcfc
major: diff includes all nested changes when a node is added
jdolle Jul 7, 2025
2adf87b
Fix directive argument changes to match others
jdolle Jul 9, 2025
dfe87bf
Add rule to ignore nested additions
jdolle Jul 9, 2025
c3dcc73
Add a field test
jdolle Jul 9, 2025
6fd13d2
Fix parent path; add more tests
jdolle Jul 9, 2025
0cdcc17
TypeChanged changes
jdolle Jul 9, 2025
985a146
prettier
jdolle Jul 9, 2025
3f781fb
Add more meta to changes
jdolle Jul 17, 2025
441c198
Add directive usage; add tests
jdolle Jul 23, 2025
288ae36
Improve path handling
jdolle Jul 23, 2025
f19e299
WIP: Print test schemas with directives.
jdolle Jul 24, 2025
6cd0ba3
Support all directive usage cases
jdolle Jul 26, 2025
bab8b86
Remove unnecessary import
jdolle Jul 28, 2025
62c16ba
Same
jdolle Jul 28, 2025
e546227
Simplify errors; add readme
jdolle Aug 21, 2025
a430f80
Remove redundant types; patch more descriptions and defaults etc
jdolle Aug 21, 2025
450a572
Prettier
jdolle Aug 21, 2025
d23a3cb
Support more changes
jdolle Aug 22, 2025
7cc882b
fix prettier etc
jdolle Aug 22, 2025
26343fa
Improve error handling and error types
jdolle Aug 26, 2025
39c2616
tweaking errors
jdolle Aug 26, 2025
0cf0b11
More error fixes
jdolle Aug 26, 2025
4d9bd95
Export lower level methods
jdolle Aug 26, 2025
6b83adb
FieldAdded.addedFieldReturnType
jdolle Sep 1, 2025
6f2756b
Fix operation type changes
jdolle Sep 1, 2025
83dfa4e
Consistency; adjust schema operation type logic
jdolle Sep 2, 2025
563b054
Export missing change types
jdolle Sep 3, 2025
f61c716
Change path for deprecated directives; fix duplicating deprecated dir…
jdolle Sep 3, 2025
2a42993
Update seven-jars-yell.md
jdolle Sep 30, 2025
b415723
Repeatable directives are a nightmare
jdolle Oct 30, 2025
a6d80a3
Fix tests
jdolle Oct 30, 2025
cd3db7e
add more complicated tests that are needed for completeness
jdolle Oct 30, 2025
a8fc04d
support repeat directives. Fix argument mutual change
jdolle Oct 30, 2025
e791196
fix import
jdolle Oct 30, 2025
bcd9cef
Detailed changelog
jdolle Oct 31, 2025
82e9bb7
Hide nested changes from CLI by default
jdolle Oct 31, 2025
c8a03d1
Move readme
jdolle Oct 31, 2025
367f544
Add imports on patch on example
jdolle Oct 31, 2025
e51bbd7
Explain requireOldValueMatch
jdolle Oct 31, 2025
4138969
Rename expectPatchToMatch
jdolle Oct 31, 2025
9bffca4
prettier
jdolle Oct 31, 2025
9972ac7
clarify option in types
jdolle Oct 31, 2025
3c870c7
Add meta check to enum value removed
jdolle Oct 31, 2025
a89eec1
inputFieldAddedFromMeta non-breaking cases
jdolle Oct 31, 2025
ee61960
Adjust objectTypeInterfaceAddedFromMeta reason
jdolle Oct 31, 2025
083528d
add path check on union patches
jdolle Oct 31, 2025
15b957a
Fix tests; remove unique patch logic for deprecation
jdolle Oct 31, 2025
9416576
Simplify nested conditions
jdolle Oct 31, 2025
801f0e6
Mention not validating
jdolle Nov 1, 2025
3ce91ba
Simplifying nested conditions
jdolle Nov 1, 2025
aa4ba82
Simplifying nested conditions
jdolle Nov 1, 2025
d1b076a
Simplifying nested conditions
jdolle Nov 1, 2025
87b7f49
Simplifying nested conditions
jdolle Nov 1, 2025
3215991
Make removing deprecation safe
jdolle Nov 4, 2025
e23fe3b
Use version 0.0.0 and add changeset to patch
jdolle Nov 4, 2025
c71a0e8
Adjust error handling
jdolle Nov 5, 2025
5f76581
Fix description removed; move tests outside of src
jdolle Nov 5, 2025
66a21cd
Add failure cases; fix field add error
jdolle Nov 5, 2025
bee17d0
fix removed reference
jdolle Nov 6, 2025
4c697c0
Add edge case tests for directives
jdolle Nov 6, 2025
91e35cd
Improve numerous error messages
jdolle Nov 7, 2025
7450087
More error format improvements
jdolle Nov 7, 2025
af2cfad
Fix changelog
jdolle Nov 7, 2025
408d59a
add fieldremoval to removal list
jdolle Nov 7, 2025
78dc291
consistent missing values
jdolle Nov 7, 2025
d71a60a
Merge branch 'graphql-hive:master' into type-added-meta
jdolle Nov 7, 2025
16a3374
Fix merge conflict
jdolle Nov 7, 2025
e123bc4
prettier
jdolle Nov 7, 2025
0d81d03
Merge branch 'graphql-hive:master' into type-added-meta
jdolle Nov 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/seven-jars-yell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@graphql-inspector/core': major
---

"diff" includes all nested changes when a node is added. Some change types have had additional meta fields added.
On deprecation add with a reason, a separate "fieldDeprecationReasonAdded" change is no longer included.
23 changes: 23 additions & 0 deletions packages/core/__tests__/diff/directive-usage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,29 @@ describe('directive-usage', () => {
expect(change.message).toEqual("Directive 'external' was added to field 'Query.a'");
});

test('added directive on added field', async () => {
const a = buildSchema(/* GraphQL */ `
type Query {
_: String
}
`);
const b = buildSchema(/* GraphQL */ `
directive @external on FIELD_DEFINITION

type Query {
_: String
a: String @external
}
`);

const changes = await diff(a, b);
const change = findFirstChangeByPath(changes, 'Query.a.external');

expect(change.criticality.level).toEqual(CriticalityLevel.NonBreaking);
expect(change.type).toEqual('DIRECTIVE_USAGE_FIELD_DEFINITION_ADDED');
expect(change.message).toEqual("Directive 'external' was added to field 'Query.a'");
});

test('removed directive', async () => {
const a = buildSchema(/* GraphQL */ `
directive @external on FIELD_DEFINITION
Expand Down
48 changes: 48 additions & 0 deletions packages/core/__tests__/diff/enum.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,54 @@ import { CriticalityLevel, diff, DiffRule } from '../../src/index.js';
import { findFirstChangeByPath } from '../../utils/testing.js';

describe('enum', () => {
test('added', async () => {
const a = buildSchema(/* GraphQL */ `
type Query {
fieldA: String
}
`);

const b = buildSchema(/* GraphQL */ `
type Query {
fieldA: String
}

enum enumA {
"""
A is the first letter in the alphabet
"""
A
B
}
`);

const changes = await diff(a, b);
expect(changes.length).toEqual(4);

{
const change = findFirstChangeByPath(changes, 'enumA');
expect(change.meta).toMatchObject({
addedTypeKind: 'EnumTypeDefinition',
addedTypeName: 'enumA',
});
expect(change.criticality.level).toEqual(CriticalityLevel.NonBreaking);
expect(change.criticality.reason).not.toBeDefined();
expect(change.message).toEqual(`Type 'enumA' was added`);
}

{
const change = findFirstChangeByPath(changes, 'enumA.A');
expect(change.criticality.level).toEqual(CriticalityLevel.NonBreaking);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test shows that enum additions also contain all nested changes within that enum, and that those changes are flagged as non-breaking.

expect(change.criticality.reason).not.toBeDefined();
expect(change.message).toEqual(`Enum value 'A' was added to enum 'enumA'`);
expect(change.meta).toMatchObject({
addedEnumValueName: 'A',
enumName: 'enumA',
addedToNewType: true,
});
}
});

test('value added', async () => {
const a = buildSchema(/* GraphQL */ `
type Query {
Expand Down
57 changes: 56 additions & 1 deletion packages/core/__tests__/diff/input.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { buildSchema } from 'graphql';
import { CriticalityLevel, diff, DiffRule } from '../../src/index.js';
import { findFirstChangeByPath } from '../../utils/testing.js';
import { findChangesByPath, findFirstChangeByPath } from '../../utils/testing.js';

describe('input', () => {
describe('fields', () => {
Expand Down Expand Up @@ -38,6 +38,61 @@ describe('input', () => {
"Input field 'd' of type 'String' was added to input object type 'Foo'",
);
});

test('added with a default value', async () => {
const a = buildSchema(/* GraphQL */ `
input Foo {
a: String!
}
`);
const b = buildSchema(/* GraphQL */ `
input Foo {
a: String!
b: String! = "B"
}
`);

const change = findFirstChangeByPath(await diff(a, b), 'Foo.b');
expect(change.criticality.level).toEqual(CriticalityLevel.Dangerous);
expect(change.type).toEqual('INPUT_FIELD_ADDED');
expect(change.meta).toMatchObject({
addedFieldDefault: '"B"',
addedInputFieldName: 'b',
addedInputFieldType: 'String!',
addedToNewType: false,
inputName: 'Foo',
isAddedInputFieldTypeNullable: false,
});
expect(change.message).toEqual(
`Input field 'b' of type 'String!' with default value '"B"' was added to input object type 'Foo'`,
);
});

test('added to an added input', async () => {
const a = buildSchema(/* GraphQL */ `
type Query {
_: String
}
`);
const b = buildSchema(/* GraphQL */ `
type Query {
_: String
}

input Foo {
a: String!
}
`);

const change = findFirstChangeByPath(await diff(a, b), 'Foo.a');

expect(change.type).toEqual('INPUT_FIELD_ADDED');
expect(change.criticality.level).toEqual(CriticalityLevel.NonBreaking);
expect(change.message).toEqual(
"Input field 'a' of type 'String!' was added to input object type 'Foo'",
);
});

test('removed', async () => {
const a = buildSchema(/* GraphQL */ `
input Foo {
Expand Down
44 changes: 36 additions & 8 deletions packages/core/__tests__/diff/interface.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,24 +169,24 @@ describe('interface', () => {
const changes = await diff(a, b);
const change = {
a: findFirstChangeByPath(changes, 'Foo.a'),
b: findChangesByPath(changes, 'Foo.b')[1],
c: findChangesByPath(changes, 'Foo.c')[1],
b: findFirstChangeByPath(changes, 'Foo.b'),
c: findFirstChangeByPath(changes, 'Foo.c'),
};

// Changed
expect(change.a.criticality.level).toEqual(CriticalityLevel.NonBreaking);
expect(change.a.type).toEqual('FIELD_DEPRECATION_REASON_CHANGED');
expect(change.a.criticality.level).toEqual(CriticalityLevel.NonBreaking);
expect(change.a.message).toEqual(
"Deprecation reason on field 'Foo.a' has changed from 'OLD' to 'NEW'",
);
// Removed
expect(change.b.criticality.level).toEqual(CriticalityLevel.NonBreaking);
expect(change.b.type).toEqual('FIELD_DEPRECATION_REASON_REMOVED');
expect(change.b.message).toEqual("Deprecation reason was removed from field 'Foo.b'");
expect(change.b.type).toEqual('FIELD_DEPRECATION_REMOVED');
expect(change.b.criticality.level).toEqual(CriticalityLevel.Dangerous);
expect(change.b.message).toEqual("Field 'Foo.b' is no longer deprecated");
// Added
expect(change.c.type).toEqual('FIELD_DEPRECATION_ADDED');
expect(change.c.criticality.level).toEqual(CriticalityLevel.NonBreaking);
expect(change.c.type).toEqual('FIELD_DEPRECATION_REASON_ADDED');
expect(change.c.message).toEqual("Field 'Foo.c' has deprecation reason 'CCC'");
expect(change.c.message).toEqual("Field 'Foo.c' is deprecated");
});

test('deprecation added / removed', async () => {
Expand Down Expand Up @@ -219,4 +219,32 @@ describe('interface', () => {
expect(change.b.message).toEqual("Field 'Foo.b' is deprecated");
});
});

test('deprecation added w/reason', async () => {
const a = buildSchema(/* GraphQL */ `
interface Foo {
a: String!
}
`);
const b = buildSchema(/* GraphQL */ `
interface Foo {
a: String! @deprecated(reason: "A is the first letter.")
}
`);

const changes = await diff(a, b);

expect(findChangesByPath(changes, 'Foo.a')).toHaveLength(1);
const change = findFirstChangeByPath(changes, 'Foo.a');

// added
expect(change.criticality.level).toEqual(CriticalityLevel.NonBreaking);
expect(change.type).toEqual('FIELD_DEPRECATION_ADDED');
expect(change.message).toEqual("Field 'Foo.a' is deprecated");
expect(change.meta).toMatchObject({
deprecationReason: 'A is the first letter.',
fieldName: 'a',
typeName: 'Foo',
});
});
});
70 changes: 55 additions & 15 deletions packages/core/__tests__/diff/object.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,18 @@ describe('object', () => {
}
`);

const change = findFirstChangeByPath(await diff(a, b), 'B');
const mutation = findFirstChangeByPath(await diff(a, b), 'Mutation');
const changes = await diff(a, b);
expect(changes).toHaveLength(4);

const change = findFirstChangeByPath(changes, 'B');
const mutation = findFirstChangeByPath(changes, 'Mutation');

expect(change.criticality.level).toEqual(CriticalityLevel.NonBreaking);
expect(mutation.criticality.level).toEqual(CriticalityLevel.NonBreaking);
expect(change.meta).toMatchObject({
addedTypeKind: 'ObjectTypeDefinition',
addedTypeName: 'B',
});
});

describe('interfaces', () => {
Expand Down Expand Up @@ -63,7 +70,8 @@ describe('object', () => {
b: String!
}

interface C {
interface C implements B {
b: String!
c: String!
}

Expand All @@ -74,11 +82,43 @@ describe('object', () => {
}
`);

const change = findFirstChangeByPath(await diff(a, b), 'Foo');
const changes = await diff(a, b);

{
const change = findFirstChangeByPath(changes, 'Foo');
expect(change.criticality.level).toEqual(CriticalityLevel.Dangerous);
expect(change.type).toEqual('OBJECT_TYPE_INTERFACE_ADDED');
expect(change.message).toEqual("'Foo' object implements 'C' interface");
expect(change.meta).toMatchObject({
addedInterfaceName: 'C',
objectTypeName: 'Foo',
});
}

const cChanges = findChangesByPath(changes, 'C');
expect(cChanges).toHaveLength(2);
{
const change = cChanges[0];
expect(change.type).toEqual('TYPE_ADDED');
expect(change.meta).toMatchObject({
addedTypeKind: 'InterfaceTypeDefinition',
addedTypeName: 'C',
});
}

{
const change = cChanges[1];
expect(change.type).toEqual('OBJECT_TYPE_INTERFACE_ADDED');
expect(change.meta).toMatchObject({
addedInterfaceName: 'B',
objectTypeName: 'C',
});
}

expect(change.criticality.level).toEqual(CriticalityLevel.Dangerous);
expect(change.type).toEqual('OBJECT_TYPE_INTERFACE_ADDED');
expect(change.message).toEqual("'Foo' object implements 'C' interface");
{
const change = findFirstChangeByPath(changes, 'C.b');
expect(change.criticality.level).toEqual(CriticalityLevel.NonBreaking);
}
});

test('removed', async () => {
Expand Down Expand Up @@ -290,24 +330,24 @@ describe('object', () => {
const changes = await diff(a, b);
const change = {
a: findFirstChangeByPath(changes, 'Foo.a'),
b: findChangesByPath(changes, 'Foo.b')[1],
c: findChangesByPath(changes, 'Foo.c')[1],
b: findFirstChangeByPath(changes, 'Foo.b'),
c: findFirstChangeByPath(changes, 'Foo.c'),
};

// Changed
expect(change.a.criticality.level).toEqual(CriticalityLevel.NonBreaking);
expect(change.a.type).toEqual('FIELD_DEPRECATION_REASON_CHANGED');
expect(change.a.criticality.level).toEqual(CriticalityLevel.NonBreaking);
expect(change.a.message).toEqual(
"Deprecation reason on field 'Foo.a' has changed from 'OLD' to 'NEW'",
);
// Removed
expect(change.b.criticality.level).toEqual(CriticalityLevel.NonBreaking);
expect(change.b.type).toEqual('FIELD_DEPRECATION_REASON_REMOVED');
expect(change.b.message).toEqual("Deprecation reason was removed from field 'Foo.b'");
expect(change.b.type).toEqual('FIELD_DEPRECATION_REMOVED');
expect(change.b.criticality.level).toEqual(CriticalityLevel.Dangerous);
expect(change.b.message).toEqual("Field 'Foo.b' is no longer deprecated");
// Added
expect(change.c.type).toEqual('FIELD_DEPRECATION_ADDED');
expect(change.c.criticality.level).toEqual(CriticalityLevel.NonBreaking);
expect(change.c.type).toEqual('FIELD_DEPRECATION_REASON_ADDED');
expect(change.c.message).toEqual("Field 'Foo.c' has deprecation reason 'CCC'");
expect(change.c.message).toEqual("Field 'Foo.c' is deprecated");
});

test('deprecation added / removed', async () => {
Expand Down
5 changes: 3 additions & 2 deletions packages/core/__tests__/diff/schema.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { buildClientSchema, buildSchema, introspectionFromSchema } from 'graphql';
import { Change, CriticalityLevel, diff } from '../../src/index.js';
import { findBestMatch } from '../../src/utils/string.js';
import { findChangesByPath, findFirstChangeByPath } from '../../utils/testing.js';

test('same schema', async () => {
const schemaA = buildSchema(/* GraphQL */ `
Expand Down Expand Up @@ -820,9 +821,9 @@ test('adding root type should not be breaking', async () => {
`);

const changes = await diff(schemaA, schemaB);
const subscription = changes[0];
expect(changes).toHaveLength(2);

expect(changes).toHaveLength(1);
const subscription = findFirstChangeByPath(changes, 'Subscription');
expect(subscription).toBeDefined();
expect(subscription!.criticality.level).toEqual(CriticalityLevel.NonBreaking);
});
Loading