Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
53 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
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
Copy link
Collaborator Author

@jdolle jdolle Jul 9, 2025

Choose a reason for hiding this comment

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

I could be convinced that this is a minor patch because the changes to the output doesn't change the existing format/definitions -- only their content such as the paths.
However, the content changes are significant, which is why I thought we should be safe and declare a major change.

Copy link
Collaborator

Choose a reason for hiding this comment

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

For the output changes, we need to see how we can introduce this backwards-compatible into Hive, Consoewhile still supporting the old "changes". Have you thought about this already, and might it become an issue?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I general I think this changeset could be much more detailed on changes, like what new types are there, how does it affect other types. But ofc this can be delayed until everything else is "complete".

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

we need to see how we can introduce this backwards-compatible into Hive Console

Agreed. My strategy is to make the new fields optional in Hive Console. If I'm careful about it, this should let us safely migrate without breaking any existing change logic.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I've added a much more detailed changelog message but didn't go to the length of listing every single field being changed.
If there's a case for listing every single field being changed, then I can do that. Or maybe it would be better suited for a migration guide?

Copy link
Collaborator

Choose a reason for hiding this comment

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

We should also add instructions on how to achieve the previous output, without showing nested change output? Maybe also adjust the docs accordingly? Also people using the CLI, should be able to only output non-nested changes 🤔

---

"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.
75 changes: 49 additions & 26 deletions packages/core/__tests__/diff/directive-usage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,36 @@ describe('directive-usage', () => {
`);

const changes = await diff(a, b);
const change = findFirstChangeByPath(changes, 'Query.a.external');
const change = findFirstChangeByPath(changes, 'Query.a.@external');
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Adding an indicator for directives is necessary to distinguish them from arguments. This makes the paths more meaningful and useful as lookups.

Copy link
Collaborator

@n1ru4l n1ru4l Oct 31, 2025

Choose a reason for hiding this comment

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

We would need to for sure check how this is backwards compatible with what we have in hive console db and have some translation layer if needed.


expect(change.criticality.level).toEqual(CriticalityLevel.Dangerous);
expect(change.type).toEqual('DIRECTIVE_USAGE_FIELD_DEFINITION_ADDED');
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 All @@ -44,7 +67,7 @@ describe('directive-usage', () => {
}
`);

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

expect(change.criticality.level).toEqual(CriticalityLevel.Dangerous);
expect(change.type).toEqual('DIRECTIVE_USAGE_FIELD_DEFINITION_REMOVED');
Expand All @@ -68,7 +91,7 @@ describe('directive-usage', () => {
`);

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

expect(change.criticality.level).toEqual(CriticalityLevel.Breaking);
expect(change.type).toEqual('DIRECTIVE_USAGE_FIELD_DEFINITION_ADDED');
Expand All @@ -91,7 +114,7 @@ describe('directive-usage', () => {
}
`);

const change = findFirstChangeByPath(await diff(a, b), 'Query.a.oneOf');
const change = findFirstChangeByPath(await diff(a, b), 'Query.a.@oneOf');

expect(change.criticality.level).toEqual(CriticalityLevel.NonBreaking);
expect(change.type).toEqual('DIRECTIVE_USAGE_FIELD_DEFINITION_REMOVED');
Expand Down Expand Up @@ -128,7 +151,7 @@ describe('directive-usage', () => {
union Foo @external = A | B
`);

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

expect(change.criticality.level).toEqual(CriticalityLevel.Dangerous);
expect(change.type).toEqual('DIRECTIVE_USAGE_UNION_MEMBER_ADDED');
Expand Down Expand Up @@ -164,7 +187,7 @@ describe('directive-usage', () => {
`);

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

expect(change.criticality.level).toEqual(CriticalityLevel.Dangerous);
expect(change.type).toEqual('DIRECTIVE_USAGE_UNION_MEMBER_REMOVED');
Expand Down Expand Up @@ -199,7 +222,7 @@ describe('directive-usage', () => {
union Foo @oneOf = A | B
`);

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

expect(change.criticality.level).toEqual(CriticalityLevel.Breaking);
expect(change.type).toEqual('DIRECTIVE_USAGE_UNION_MEMBER_ADDED');
Expand Down Expand Up @@ -235,7 +258,7 @@ describe('directive-usage', () => {
`);

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

expect(change.criticality.level).toEqual(CriticalityLevel.NonBreaking);
expect(change.type).toEqual('DIRECTIVE_USAGE_UNION_MEMBER_REMOVED');
Expand Down Expand Up @@ -270,7 +293,7 @@ describe('directive-usage', () => {
`);

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

expect(change.criticality.level).toEqual(CriticalityLevel.Dangerous);
expect(change.criticality.reason).toBeDefined();
Expand Down Expand Up @@ -302,7 +325,7 @@ describe('directive-usage', () => {
}
`);

const change = findFirstChangeByPath(await diff(a, b), 'enumA.external');
const change = findFirstChangeByPath(await diff(a, b), 'enumA.@external');

expect(change.criticality.level).toEqual(CriticalityLevel.Dangerous);
expect(change.type).toEqual('DIRECTIVE_USAGE_ENUM_REMOVED');
Expand Down Expand Up @@ -338,7 +361,7 @@ describe('directive-usage', () => {
`);

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

expect(changes.length).toEqual(1);
expect(change.criticality.level).toEqual(CriticalityLevel.Dangerous);
Expand Down Expand Up @@ -373,7 +396,7 @@ describe('directive-usage', () => {
`);

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

expect(changes.length).toEqual(1);
expect(change.criticality.level).toEqual(CriticalityLevel.Dangerous);
Expand All @@ -400,7 +423,7 @@ describe('directive-usage', () => {
`);

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

expect(changes.length).toEqual(1);
expect(change.criticality.level).toEqual(CriticalityLevel.Dangerous);
Expand All @@ -424,7 +447,7 @@ describe('directive-usage', () => {
`);

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

expect(changes.length).toEqual(1);
expect(change.criticality.level).toEqual(CriticalityLevel.Dangerous);
Expand All @@ -451,7 +474,7 @@ describe('directive-usage', () => {
`);

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

expect(changes.length).toEqual(1);
expect(change.criticality.level).toEqual(CriticalityLevel.Dangerous);
Expand All @@ -477,7 +500,7 @@ describe('directive-usage', () => {
`);

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

expect(changes.length).toEqual(1);
expect(change.criticality.level).toEqual(CriticalityLevel.Dangerous);
Expand All @@ -500,7 +523,7 @@ describe('directive-usage', () => {
`);

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

expect(changes.length).toEqual(1);
expect(change.criticality.level).toEqual(CriticalityLevel.Dangerous);
Expand All @@ -518,7 +541,7 @@ describe('directive-usage', () => {
`);

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

expect(changes.length).toEqual(1);
expect(change.criticality.level).toEqual(CriticalityLevel.Dangerous);
Expand All @@ -543,7 +566,7 @@ describe('directive-usage', () => {
`);

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

expect(change.criticality.level).toEqual(CriticalityLevel.Dangerous);
expect(change.type).toEqual('DIRECTIVE_USAGE_OBJECT_ADDED');
Expand All @@ -564,7 +587,7 @@ describe('directive-usage', () => {
`);

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

expect(change.criticality.level).toEqual(CriticalityLevel.Dangerous);
expect(change.type).toEqual('DIRECTIVE_USAGE_OBJECT_REMOVED');
Expand All @@ -588,7 +611,7 @@ describe('directive-usage', () => {
`);

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

expect(change.criticality.level).toEqual(CriticalityLevel.Dangerous);
expect(change.type).toEqual('DIRECTIVE_USAGE_INTERFACE_ADDED');
Expand All @@ -610,7 +633,7 @@ describe('directive-usage', () => {
`);

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

expect(change.criticality.level).toEqual(CriticalityLevel.Dangerous);
expect(change.type).toEqual('DIRECTIVE_USAGE_INTERFACE_REMOVED');
Expand All @@ -634,7 +657,7 @@ describe('directive-usage', () => {
`);

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

expect(change.criticality.level).toEqual(CriticalityLevel.Dangerous);
expect(change.type).toEqual('DIRECTIVE_USAGE_ARGUMENT_DEFINITION_ADDED');
Expand All @@ -658,7 +681,7 @@ describe('directive-usage', () => {
`);

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

expect(change.criticality.level).toEqual(CriticalityLevel.Dangerous);
expect(change.type).toEqual('DIRECTIVE_USAGE_ARGUMENT_DEFINITION_REMOVED');
Expand Down Expand Up @@ -690,7 +713,7 @@ describe('directive-usage', () => {
`);

const changes = await diff(a, b);
const change = findFirstChangeByPath(changes, 'Foo.external');
const change = findFirstChangeByPath(changes, '.@external');
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 indicates the directive is applied to the schema object. This . is necessary to distinguish directive usages from directive definitions at the root level.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Same as #2893 (comment)


expect(change.criticality.level).toEqual(CriticalityLevel.Dangerous);
expect(change.type).toEqual('DIRECTIVE_USAGE_SCHEMA_ADDED');
Expand All @@ -717,7 +740,7 @@ describe('directive-usage', () => {
`);

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

expect(change.criticality.level).toEqual(CriticalityLevel.Dangerous);
expect(change.type).toEqual('DIRECTIVE_USAGE_SCHEMA_REMOVED');
Expand Down
12 changes: 6 additions & 6 deletions packages/core/__tests__/diff/directive.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,13 +173,13 @@ describe('directive', () => {
};

// Nullable
expect(change.a.criticality.level).toEqual(CriticalityLevel.NonBreaking);
expect(change.a.type).toEqual('DIRECTIVE_ARGUMENT_ADDED');
expect(change.a.message).toEqual(`Argument 'name' was added to directive 'a'`);
expect(change.a?.type).toEqual('DIRECTIVE_ARGUMENT_ADDED');
expect(change.a?.criticality.level).toEqual(CriticalityLevel.NonBreaking);
expect(change.a?.message).toEqual(`Argument 'name' was added to directive 'a'`);
// Non-nullable
expect(change.b.criticality.level).toEqual(CriticalityLevel.Breaking);
expect(change.b.type).toEqual('DIRECTIVE_ARGUMENT_ADDED');
expect(change.b.message).toEqual(`Argument 'name' was added to directive 'b'`);
expect(change.b?.type).toEqual('DIRECTIVE_ARGUMENT_ADDED');
expect(change.b?.criticality.level).toEqual(CriticalityLevel.Breaking);
expect(change.b?.message).toEqual(`Argument 'name' was added to directive 'b'`);
});

test('removed', async () => {
Expand Down
79 changes: 71 additions & 8 deletions packages/core/__tests__/diff/enum.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,56 @@
import { buildSchema } from 'graphql';
import { CriticalityLevel, diff, DiffRule } from '../../src/index.js';
import { findFirstChangeByPath } from '../../utils/testing.js';
import { ChangeType, CriticalityLevel, diff, DiffRule } from '../../src/index.js';
import { findChangesByPath, 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 Expand Up @@ -130,7 +178,7 @@ describe('enum', () => {
`);

const changes = await diff(a, b);
const change = findFirstChangeByPath(changes, 'enumA.A');
const change = findFirstChangeByPath(changes, 'enumA.A.@deprecated');

expect(changes.length).toEqual(1);
expect(change.criticality.level).toEqual(CriticalityLevel.NonBreaking);
Expand Down Expand Up @@ -163,11 +211,26 @@ describe('enum', () => {
`);

const changes = await diff(a, b);
const change = findFirstChangeByPath(changes, 'enumA.A');

expect(changes.length).toEqual(2);
expect(change.criticality.level).toEqual(CriticalityLevel.NonBreaking);
expect(change.message).toEqual(`Enum value 'enumA.A' was deprecated with reason 'New Reason'`);
expect(changes).toHaveLength(3);
const directiveChanges = findChangesByPath(changes, 'enumA.A.@deprecated');
expect(directiveChanges).toHaveLength(2);

for (const change of directiveChanges) {
expect(change.criticality.level).toEqual(CriticalityLevel.NonBreaking);
if (change.type === ChangeType.EnumValueDeprecationReasonAdded) {
expect(change.message).toEqual(
`Enum value 'enumA.A' was deprecated with reason 'New Reason'`,
);
} else if (change.type === ChangeType.DirectiveUsageEnumValueAdded) {
expect(change.message).toEqual(`Directive 'deprecated' was added to enum value 'enumA.A'`);
}
}

{
const change = findFirstChangeByPath(changes, '[email protected]');
expect(change.type).toEqual(ChangeType.DirectiveUsageArgumentAdded);
expect(change.message).toEqual(`Argument 'reason' was added to '@deprecated'`);
}
});

test('deprecation reason removed', async () => {
Expand Down
Loading
Loading