Skip to content

Commit cf7fcfc

Browse files
committed
major: diff includes all nested changes when a node is added
1 parent 8341829 commit cf7fcfc

29 files changed

+687
-265
lines changed

.changeset/seven-jars-yell.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@graphql-inspector/core': major
3+
---
4+
5+
"diff" includes all nested changes when a node is added. Some change types have had additional meta fields added.
6+
On deprecation add with a reason, a separate "fieldDeprecationReasonAdded" change is no longer included.

packages/core/__tests__/diff/directive-usage.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,29 @@ describe('directive-usage', () => {
2828
expect(change.message).toEqual("Directive 'external' was added to field 'Query.a'");
2929
});
3030

31+
test('added directive on added field', async () => {
32+
const a = buildSchema(/* GraphQL */ `
33+
type Query {
34+
_: String
35+
}
36+
`);
37+
const b = buildSchema(/* GraphQL */ `
38+
directive @external on FIELD_DEFINITION
39+
40+
type Query {
41+
_: String
42+
a: String @external
43+
}
44+
`);
45+
46+
const changes = await diff(a, b);
47+
const change = findFirstChangeByPath(changes, 'Query.a.external');
48+
49+
expect(change.criticality.level).toEqual(CriticalityLevel.NonBreaking);
50+
expect(change.type).toEqual('DIRECTIVE_USAGE_FIELD_DEFINITION_ADDED');
51+
expect(change.message).toEqual("Directive 'external' was added to field 'Query.a'");
52+
});
53+
3154
test('removed directive', async () => {
3255
const a = buildSchema(/* GraphQL */ `
3356
directive @external on FIELD_DEFINITION

packages/core/__tests__/diff/enum.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,54 @@ import { CriticalityLevel, diff, DiffRule } from '../../src/index.js';
33
import { findFirstChangeByPath } from '../../utils/testing.js';
44

55
describe('enum', () => {
6+
test('added', async () => {
7+
const a = buildSchema(/* GraphQL */ `
8+
type Query {
9+
fieldA: String
10+
}
11+
`);
12+
13+
const b = buildSchema(/* GraphQL */ `
14+
type Query {
15+
fieldA: String
16+
}
17+
18+
enum enumA {
19+
"""
20+
A is the first letter in the alphabet
21+
"""
22+
A
23+
B
24+
}
25+
`);
26+
27+
const changes = await diff(a, b);
28+
expect(changes.length).toEqual(4);
29+
30+
{
31+
const change = findFirstChangeByPath(changes, 'enumA');
32+
expect(change.meta).toMatchObject({
33+
addedTypeKind: 'EnumTypeDefinition',
34+
addedTypeName: 'enumA',
35+
});
36+
expect(change.criticality.level).toEqual(CriticalityLevel.NonBreaking);
37+
expect(change.criticality.reason).not.toBeDefined();
38+
expect(change.message).toEqual(`Type 'enumA' was added`);
39+
}
40+
41+
{
42+
const change = findFirstChangeByPath(changes, 'enumA.A');
43+
expect(change.criticality.level).toEqual(CriticalityLevel.NonBreaking);
44+
expect(change.criticality.reason).not.toBeDefined();
45+
expect(change.message).toEqual(`Enum value 'A' was added to enum 'enumA'`);
46+
expect(change.meta).toMatchObject({
47+
addedEnumValueName: 'A',
48+
enumName: 'enumA',
49+
addedToNewType: true,
50+
});
51+
}
52+
});
53+
654
test('value added', async () => {
755
const a = buildSchema(/* GraphQL */ `
856
type Query {

packages/core/__tests__/diff/input.test.ts

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { buildSchema } from 'graphql';
22
import { CriticalityLevel, diff, DiffRule } from '../../src/index.js';
3-
import { findFirstChangeByPath } from '../../utils/testing.js';
3+
import { findChangesByPath, findFirstChangeByPath } from '../../utils/testing.js';
44

55
describe('input', () => {
66
describe('fields', () => {
@@ -38,6 +38,61 @@ describe('input', () => {
3838
"Input field 'd' of type 'String' was added to input object type 'Foo'",
3939
);
4040
});
41+
42+
test('added with a default value', async () => {
43+
const a = buildSchema(/* GraphQL */ `
44+
input Foo {
45+
a: String!
46+
}
47+
`);
48+
const b = buildSchema(/* GraphQL */ `
49+
input Foo {
50+
a: String!
51+
b: String! = "B"
52+
}
53+
`);
54+
55+
const change = findFirstChangeByPath(await diff(a, b), 'Foo.b');
56+
expect(change.criticality.level).toEqual(CriticalityLevel.Dangerous);
57+
expect(change.type).toEqual('INPUT_FIELD_ADDED');
58+
expect(change.meta).toMatchObject({
59+
addedFieldDefault: '"B"',
60+
addedInputFieldName: 'b',
61+
addedInputFieldType: 'String!',
62+
addedToNewType: false,
63+
inputName: 'Foo',
64+
isAddedInputFieldTypeNullable: false,
65+
});
66+
expect(change.message).toEqual(
67+
`Input field 'b' of type 'String!' with default value '"B"' was added to input object type 'Foo'`,
68+
);
69+
});
70+
71+
test('added to an added input', async () => {
72+
const a = buildSchema(/* GraphQL */ `
73+
type Query {
74+
_: String
75+
}
76+
`);
77+
const b = buildSchema(/* GraphQL */ `
78+
type Query {
79+
_: String
80+
}
81+
82+
input Foo {
83+
a: String!
84+
}
85+
`);
86+
87+
const change = findFirstChangeByPath(await diff(a, b), 'Foo.a');
88+
89+
expect(change.type).toEqual('INPUT_FIELD_ADDED');
90+
expect(change.criticality.level).toEqual(CriticalityLevel.NonBreaking);
91+
expect(change.message).toEqual(
92+
"Input field 'a' of type 'String!' was added to input object type 'Foo'",
93+
);
94+
});
95+
4196
test('removed', async () => {
4297
const a = buildSchema(/* GraphQL */ `
4398
input Foo {

packages/core/__tests__/diff/interface.test.ts

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -169,24 +169,24 @@ describe('interface', () => {
169169
const changes = await diff(a, b);
170170
const change = {
171171
a: findFirstChangeByPath(changes, 'Foo.a'),
172-
b: findChangesByPath(changes, 'Foo.b')[1],
173-
c: findChangesByPath(changes, 'Foo.c')[1],
172+
b: findFirstChangeByPath(changes, 'Foo.b'),
173+
c: findFirstChangeByPath(changes, 'Foo.c'),
174174
};
175175

176176
// Changed
177-
expect(change.a.criticality.level).toEqual(CriticalityLevel.NonBreaking);
178177
expect(change.a.type).toEqual('FIELD_DEPRECATION_REASON_CHANGED');
178+
expect(change.a.criticality.level).toEqual(CriticalityLevel.NonBreaking);
179179
expect(change.a.message).toEqual(
180180
"Deprecation reason on field 'Foo.a' has changed from 'OLD' to 'NEW'",
181181
);
182182
// Removed
183-
expect(change.b.criticality.level).toEqual(CriticalityLevel.NonBreaking);
184-
expect(change.b.type).toEqual('FIELD_DEPRECATION_REASON_REMOVED');
185-
expect(change.b.message).toEqual("Deprecation reason was removed from field 'Foo.b'");
183+
expect(change.b.type).toEqual('FIELD_DEPRECATION_REMOVED');
184+
expect(change.b.criticality.level).toEqual(CriticalityLevel.Dangerous);
185+
expect(change.b.message).toEqual("Field 'Foo.b' is no longer deprecated");
186186
// Added
187+
expect(change.c.type).toEqual('FIELD_DEPRECATION_ADDED');
187188
expect(change.c.criticality.level).toEqual(CriticalityLevel.NonBreaking);
188-
expect(change.c.type).toEqual('FIELD_DEPRECATION_REASON_ADDED');
189-
expect(change.c.message).toEqual("Field 'Foo.c' has deprecation reason 'CCC'");
189+
expect(change.c.message).toEqual("Field 'Foo.c' is deprecated");
190190
});
191191

192192
test('deprecation added / removed', async () => {
@@ -219,4 +219,32 @@ describe('interface', () => {
219219
expect(change.b.message).toEqual("Field 'Foo.b' is deprecated");
220220
});
221221
});
222+
223+
test('deprecation added w/reason', async () => {
224+
const a = buildSchema(/* GraphQL */ `
225+
interface Foo {
226+
a: String!
227+
}
228+
`);
229+
const b = buildSchema(/* GraphQL */ `
230+
interface Foo {
231+
a: String! @deprecated(reason: "A is the first letter.")
232+
}
233+
`);
234+
235+
const changes = await diff(a, b);
236+
237+
expect(findChangesByPath(changes, 'Foo.a')).toHaveLength(1);
238+
const change = findFirstChangeByPath(changes, 'Foo.a');
239+
240+
// added
241+
expect(change.criticality.level).toEqual(CriticalityLevel.NonBreaking);
242+
expect(change.type).toEqual('FIELD_DEPRECATION_ADDED');
243+
expect(change.message).toEqual("Field 'Foo.a' is deprecated");
244+
expect(change.meta).toMatchObject({
245+
deprecationReason: 'A is the first letter.',
246+
fieldName: 'a',
247+
typeName: 'Foo',
248+
});
249+
});
222250
});

packages/core/__tests__/diff/object.test.ts

Lines changed: 55 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,18 @@ describe('object', () => {
3131
}
3232
`);
3333

34-
const change = findFirstChangeByPath(await diff(a, b), 'B');
35-
const mutation = findFirstChangeByPath(await diff(a, b), 'Mutation');
34+
const changes = await diff(a, b);
35+
expect(changes).toHaveLength(4);
36+
37+
const change = findFirstChangeByPath(changes, 'B');
38+
const mutation = findFirstChangeByPath(changes, 'Mutation');
3639

3740
expect(change.criticality.level).toEqual(CriticalityLevel.NonBreaking);
3841
expect(mutation.criticality.level).toEqual(CriticalityLevel.NonBreaking);
42+
expect(change.meta).toMatchObject({
43+
addedTypeKind: 'ObjectTypeDefinition',
44+
addedTypeName: 'B',
45+
});
3946
});
4047

4148
describe('interfaces', () => {
@@ -63,7 +70,8 @@ describe('object', () => {
6370
b: String!
6471
}
6572
66-
interface C {
73+
interface C implements B {
74+
b: String!
6775
c: String!
6876
}
6977
@@ -74,11 +82,43 @@ describe('object', () => {
7482
}
7583
`);
7684

77-
const change = findFirstChangeByPath(await diff(a, b), 'Foo');
85+
const changes = await diff(a, b);
86+
87+
{
88+
const change = findFirstChangeByPath(changes, 'Foo');
89+
expect(change.criticality.level).toEqual(CriticalityLevel.Dangerous);
90+
expect(change.type).toEqual('OBJECT_TYPE_INTERFACE_ADDED');
91+
expect(change.message).toEqual("'Foo' object implements 'C' interface");
92+
expect(change.meta).toMatchObject({
93+
addedInterfaceName: 'C',
94+
objectTypeName: 'Foo',
95+
});
96+
}
97+
98+
const cChanges = findChangesByPath(changes, 'C');
99+
expect(cChanges).toHaveLength(2);
100+
{
101+
const change = cChanges[0];
102+
expect(change.type).toEqual('TYPE_ADDED');
103+
expect(change.meta).toMatchObject({
104+
addedTypeKind: 'InterfaceTypeDefinition',
105+
addedTypeName: 'C',
106+
});
107+
}
108+
109+
{
110+
const change = cChanges[1];
111+
expect(change.type).toEqual('OBJECT_TYPE_INTERFACE_ADDED');
112+
expect(change.meta).toMatchObject({
113+
addedInterfaceName: 'B',
114+
objectTypeName: 'C',
115+
});
116+
}
78117

79-
expect(change.criticality.level).toEqual(CriticalityLevel.Dangerous);
80-
expect(change.type).toEqual('OBJECT_TYPE_INTERFACE_ADDED');
81-
expect(change.message).toEqual("'Foo' object implements 'C' interface");
118+
{
119+
const change = findFirstChangeByPath(changes, 'C.b');
120+
expect(change.criticality.level).toEqual(CriticalityLevel.NonBreaking);
121+
}
82122
});
83123

84124
test('removed', async () => {
@@ -290,24 +330,24 @@ describe('object', () => {
290330
const changes = await diff(a, b);
291331
const change = {
292332
a: findFirstChangeByPath(changes, 'Foo.a'),
293-
b: findChangesByPath(changes, 'Foo.b')[1],
294-
c: findChangesByPath(changes, 'Foo.c')[1],
333+
b: findFirstChangeByPath(changes, 'Foo.b'),
334+
c: findFirstChangeByPath(changes, 'Foo.c'),
295335
};
296336

297337
// Changed
298-
expect(change.a.criticality.level).toEqual(CriticalityLevel.NonBreaking);
299338
expect(change.a.type).toEqual('FIELD_DEPRECATION_REASON_CHANGED');
339+
expect(change.a.criticality.level).toEqual(CriticalityLevel.NonBreaking);
300340
expect(change.a.message).toEqual(
301341
"Deprecation reason on field 'Foo.a' has changed from 'OLD' to 'NEW'",
302342
);
303343
// Removed
304-
expect(change.b.criticality.level).toEqual(CriticalityLevel.NonBreaking);
305-
expect(change.b.type).toEqual('FIELD_DEPRECATION_REASON_REMOVED');
306-
expect(change.b.message).toEqual("Deprecation reason was removed from field 'Foo.b'");
344+
expect(change.b.type).toEqual('FIELD_DEPRECATION_REMOVED');
345+
expect(change.b.criticality.level).toEqual(CriticalityLevel.Dangerous);
346+
expect(change.b.message).toEqual("Field 'Foo.b' is no longer deprecated");
307347
// Added
348+
expect(change.c.type).toEqual('FIELD_DEPRECATION_ADDED');
308349
expect(change.c.criticality.level).toEqual(CriticalityLevel.NonBreaking);
309-
expect(change.c.type).toEqual('FIELD_DEPRECATION_REASON_ADDED');
310-
expect(change.c.message).toEqual("Field 'Foo.c' has deprecation reason 'CCC'");
350+
expect(change.c.message).toEqual("Field 'Foo.c' is deprecated");
311351
});
312352

313353
test('deprecation added / removed', async () => {

packages/core/__tests__/diff/schema.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { buildClientSchema, buildSchema, introspectionFromSchema } from 'graphql';
22
import { Change, CriticalityLevel, diff } from '../../src/index.js';
33
import { findBestMatch } from '../../src/utils/string.js';
4+
import { findChangesByPath, findFirstChangeByPath } from '../../utils/testing.js';
45

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

822823
const changes = await diff(schemaA, schemaB);
823-
const subscription = changes[0];
824+
expect(changes).toHaveLength(2);
824825

825-
expect(changes).toHaveLength(1);
826+
const subscription = findFirstChangeByPath(changes, 'Subscription');
826827
expect(subscription).toBeDefined();
827828
expect(subscription!.criticality.level).toEqual(CriticalityLevel.NonBreaking);
828829
});

0 commit comments

Comments
 (0)