Skip to content

Commit c13c1c7

Browse files
committed
Add kind metadata to TypeAdded change
Capture all changes on type addition
1 parent 8341829 commit c13c1c7

24 files changed

+398
-196
lines changed

.changeset/seven-jars-yell.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@graphql-inspector/core': patch
3+
---
4+
5+
Set additional metadata on TypeAdded changes depending on the type added.

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+
enumIsOld: false,
50+
});
51+
}
52+
});
53+
654
test('value added', async () => {
755
const a = buildSchema(/* GraphQL */ `
856
type Query {

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

Lines changed: 47 additions & 7 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);
7886

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");
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+
}
117+
118+
{
119+
const change = findFirstChangeByPath(changes, 'C.b');
120+
expect(change.criticality.level).toEqual(CriticalityLevel.NonBreaking);
121+
}
82122
});
83123

84124
test('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
});

packages/core/src/diff/argument.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,31 +17,31 @@ import { AddChange } from './schema.js';
1717
export function changesInArgument(
1818
type: GraphQLObjectType | GraphQLInterfaceType,
1919
field: GraphQLField<any, any, any>,
20-
oldArg: GraphQLArgument,
20+
oldArg: GraphQLArgument | null,
2121
newArg: GraphQLArgument,
2222
addChange: AddChange,
2323
) {
24-
if (isNotEqual(oldArg.description, newArg.description)) {
24+
if (isNotEqual(oldArg?.description, newArg.description)) {
2525
addChange(fieldArgumentDescriptionChanged(type, field, oldArg, newArg));
2626
}
2727

28-
if (isNotEqual(oldArg.defaultValue, newArg.defaultValue)) {
29-
if (Array.isArray(oldArg.defaultValue) && Array.isArray(newArg.defaultValue)) {
28+
if (isNotEqual(oldArg?.defaultValue, newArg.defaultValue)) {
29+
if (Array.isArray(oldArg?.defaultValue) && Array.isArray(newArg.defaultValue)) {
3030
const diff = diffArrays(oldArg.defaultValue, newArg.defaultValue);
3131
if (diff.length > 0) {
3232
addChange(fieldArgumentDefaultChanged(type, field, oldArg, newArg));
3333
}
34-
} else if (JSON.stringify(oldArg.defaultValue) !== JSON.stringify(newArg.defaultValue)) {
34+
} else if (JSON.stringify(oldArg?.defaultValue) !== JSON.stringify(newArg.defaultValue)) {
3535
addChange(fieldArgumentDefaultChanged(type, field, oldArg, newArg));
3636
}
3737
}
3838

39-
if (isNotEqual(oldArg.type.toString(), newArg.type.toString())) {
39+
if (isNotEqual(oldArg?.type.toString(), newArg.type.toString())) {
4040
addChange(fieldArgumentTypeChanged(type, field, oldArg, newArg));
4141
}
4242

43-
if (oldArg.astNode?.directives && newArg.astNode?.directives) {
44-
compareLists(oldArg.astNode.directives || [], newArg.astNode.directives || [], {
43+
if (newArg.astNode?.directives) {
44+
compareLists(oldArg?.astNode?.directives || [], newArg.astNode.directives || [], {
4545
onAdded(directive) {
4646
addChange(
4747
directiveUsageAdded(Kind.ARGUMENT, directive, {
@@ -54,7 +54,7 @@ export function changesInArgument(
5454

5555
onRemoved(directive) {
5656
addChange(
57-
directiveUsageRemoved(Kind.ARGUMENT, directive, { argument: oldArg, field, type }),
57+
directiveUsageRemoved(Kind.ARGUMENT, directive, { argument: oldArg!, field, type }),
5858
);
5959
},
6060
});

packages/core/src/diff/changes/argument.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,16 @@ export function fieldArgumentDescriptionChangedFromMeta(
3333
export function fieldArgumentDescriptionChanged(
3434
type: GraphQLObjectType | GraphQLInterfaceType,
3535
field: GraphQLField<any, any, any>,
36-
oldArg: GraphQLArgument,
36+
oldArg: GraphQLArgument | null,
3737
newArg: GraphQLArgument,
3838
): Change<typeof ChangeType.FieldArgumentDescriptionChanged> {
3939
return fieldArgumentDescriptionChangedFromMeta({
4040
type: ChangeType.FieldArgumentDescriptionChanged,
4141
meta: {
4242
typeName: type.name,
4343
fieldName: field.name,
44-
argumentName: oldArg.name,
45-
oldDescription: oldArg.description ?? null,
44+
argumentName: newArg.name,
45+
oldDescription: oldArg?.description ?? null,
4646
newDescription: newArg.description ?? null,
4747
},
4848
});
@@ -75,7 +75,7 @@ export function fieldArgumentDefaultChangedFromMeta(args: FieldArgumentDefaultCh
7575
export function fieldArgumentDefaultChanged(
7676
type: GraphQLObjectType | GraphQLInterfaceType,
7777
field: GraphQLField<any, any, any>,
78-
oldArg: GraphQLArgument,
78+
oldArg: GraphQLArgument | null,
7979
newArg: GraphQLArgument,
8080
): Change<typeof ChangeType.FieldArgumentDefaultChanged> {
8181
const meta: FieldArgumentDefaultChangedChange['meta'] = {
@@ -84,7 +84,7 @@ export function fieldArgumentDefaultChanged(
8484
argumentName: newArg.name,
8585
};
8686

87-
if (oldArg.defaultValue !== undefined) {
87+
if (oldArg?.defaultValue !== undefined) {
8888
meta.oldDefaultValue = safeString(oldArg.defaultValue);
8989
}
9090
if (newArg.defaultValue !== undefined) {
@@ -127,7 +127,7 @@ export function fieldArgumentTypeChangedFromMeta(args: FieldArgumentTypeChangedC
127127
export function fieldArgumentTypeChanged(
128128
type: GraphQLObjectType | GraphQLInterfaceType,
129129
field: GraphQLField<any, any, any>,
130-
oldArg: GraphQLArgument,
130+
oldArg: GraphQLArgument | null,
131131
newArg: GraphQLArgument,
132132
): Change<typeof ChangeType.FieldArgumentTypeChanged> {
133133
return fieldArgumentTypeChangedFromMeta({
@@ -136,9 +136,9 @@ export function fieldArgumentTypeChanged(
136136
typeName: type.name,
137137
fieldName: field.name,
138138
argumentName: newArg.name,
139-
oldArgumentType: oldArg.type.toString(),
139+
oldArgumentType: oldArg?.type.toString() ?? '',
140140
newArgumentType: newArg.type.toString(),
141-
isSafeArgumentTypeChange: safeChangeForInputValue(oldArg.type, newArg.type),
141+
isSafeArgumentTypeChange: !oldArg || safeChangeForInputValue(oldArg.type, newArg.type),
142142
},
143143
});
144144
}

packages/core/src/diff/changes/change.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { Kind } from 'graphql';
2+
13
export enum CriticalityLevel {
24
Breaking = 'BREAKING',
35
NonBreaking = 'NON_BREAKING',
@@ -252,6 +254,7 @@ export type EnumValueAddedChange = {
252254
meta: {
253255
enumName: string;
254256
addedEnumValueName: string;
257+
enumIsOld: boolean;
255258
};
256259
};
257260

@@ -311,6 +314,7 @@ export type FieldAddedChange = {
311314
typeName: string;
312315
addedFieldName: string;
313316
typeType: string;
317+
addedFieldReturnType: string;
314318
};
315319
};
316320

@@ -558,11 +562,26 @@ export type TypeRemovedChange = {
558562
};
559563
};
560564

565+
type TypeAddedMeta<K extends Kind> = {
566+
addedTypeName: string;
567+
addedTypeKind: K;
568+
};
569+
570+
type InputAddedMeta = TypeAddedMeta<Kind.INPUT_OBJECT_TYPE_DEFINITION> & {
571+
addedTypeIsOneOf: boolean;
572+
};
573+
561574
export type TypeAddedChange = {
562575
type: typeof ChangeType.TypeAdded;
563-
meta: {
564-
addedTypeName: string;
565-
};
576+
meta:
577+
| InputAddedMeta
578+
| TypeAddedMeta<
579+
| Kind.ENUM_TYPE_DEFINITION
580+
| Kind.OBJECT_TYPE_DEFINITION
581+
| Kind.INTERFACE_TYPE_DEFINITION
582+
| Kind.UNION_TYPE_DEFINITION
583+
| Kind.SCALAR_TYPE_DEFINITION
584+
>;
566585
};
567586

568587
export type TypeKindChangedChange = {

packages/core/src/diff/changes/directive.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -95,14 +95,14 @@ export function directiveDescriptionChangedFromMeta(args: DirectiveDescriptionCh
9595
}
9696

9797
export function directiveDescriptionChanged(
98-
oldDirective: GraphQLDirective,
98+
oldDirective: GraphQLDirective | null,
9999
newDirective: GraphQLDirective,
100100
): Change<typeof ChangeType.DirectiveDescriptionChanged> {
101101
return directiveDescriptionChangedFromMeta({
102102
type: ChangeType.DirectiveDescriptionChanged,
103103
meta: {
104-
directiveName: oldDirective.name,
105-
oldDirectiveDescription: oldDirective.description ?? null,
104+
directiveName: newDirective.name,
105+
oldDirectiveDescription: oldDirective?.description ?? null,
106106
newDirectiveDescription: newDirective.description ?? null,
107107
},
108108
});
@@ -262,15 +262,15 @@ export function directiveArgumentDescriptionChangedFromMeta(
262262

263263
export function directiveArgumentDescriptionChanged(
264264
directive: GraphQLDirective,
265-
oldArg: GraphQLArgument,
265+
oldArg: GraphQLArgument | null,
266266
newArg: GraphQLArgument,
267267
): Change<typeof ChangeType.DirectiveArgumentDescriptionChanged> {
268268
return directiveArgumentDescriptionChangedFromMeta({
269269
type: ChangeType.DirectiveArgumentDescriptionChanged,
270270
meta: {
271271
directiveName: directive.name,
272-
directiveArgumentName: oldArg.name,
273-
oldDirectiveArgumentDescription: oldArg.description ?? null,
272+
directiveArgumentName: newArg.name,
273+
oldDirectiveArgumentDescription: oldArg?.description ?? null,
274274
newDirectiveArgumentDescription: newArg.description ?? null,
275275
},
276276
});
@@ -304,14 +304,14 @@ export function directiveArgumentDefaultValueChangedFromMeta(
304304

305305
export function directiveArgumentDefaultValueChanged(
306306
directive: GraphQLDirective,
307-
oldArg: GraphQLArgument,
307+
oldArg: GraphQLArgument | null,
308308
newArg: GraphQLArgument,
309309
): Change<typeof ChangeType.DirectiveArgumentDefaultValueChanged> {
310310
const meta: DirectiveArgumentDefaultValueChangedChange['meta'] = {
311311
directiveName: directive.name,
312-
directiveArgumentName: oldArg.name,
312+
directiveArgumentName: newArg.name,
313313
};
314-
if (oldArg.defaultValue !== undefined) {
314+
if (oldArg?.defaultValue !== undefined) {
315315
meta.oldDirectiveArgumentDefaultValue = safeString(oldArg.defaultValue);
316316
}
317317
if (newArg.defaultValue !== undefined) {
@@ -352,17 +352,17 @@ export function directiveArgumentTypeChangedFromMeta(args: DirectiveArgumentType
352352

353353
export function directiveArgumentTypeChanged(
354354
directive: GraphQLDirective,
355-
oldArg: GraphQLArgument,
355+
oldArg: GraphQLArgument | null,
356356
newArg: GraphQLArgument,
357357
): Change<typeof ChangeType.DirectiveArgumentTypeChanged> {
358358
return directiveArgumentTypeChangedFromMeta({
359359
type: ChangeType.DirectiveArgumentTypeChanged,
360360
meta: {
361361
directiveName: directive.name,
362-
directiveArgumentName: oldArg.name,
363-
oldDirectiveArgumentType: oldArg.type.toString(),
362+
directiveArgumentName: newArg.name,
363+
oldDirectiveArgumentType: oldArg?.type.toString() ?? '',
364364
newDirectiveArgumentType: newArg.type.toString(),
365-
isSafeDirectiveArgumentTypeChange: safeChangeForInputValue(oldArg.type, newArg.type),
365+
isSafeDirectiveArgumentTypeChange: safeChangeForInputValue(oldArg?.type ?? null, newArg.type),
366366
},
367367
});
368368
}

0 commit comments

Comments
 (0)