Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
29 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
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.

---

"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
Loading