Skip to content

Commit 9bfc094

Browse files
authored
Generate changes for new or deleted schemas; improve root operation type change messages and metadata (#2923)
* Generate changes for new or deleted schemas * Adjust SCHEMA\_\*\_TYPE_CHANGED changes to use null instead of 'unknown' when these types are not defined
1 parent 3f5b414 commit 9bfc094

File tree

12 files changed

+345
-120
lines changed

12 files changed

+345
-120
lines changed

.changeset/sharp-files-sin.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@graphql-inspector/patch': minor
3+
'@graphql-inspector/core': minor
4+
---
5+
6+
Adjust SCHEMA\_\*\_TYPE_CHANGED changes to use null instead of 'unknown' when these types are not
7+
defined and improve the change messages.

.changeset/twelve-ties-pretend.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@graphql-inspector/core': minor
3+
---
4+
5+
diff can be passed null schemas. This lets it output the full list of additions on the new schema.

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

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

64
test('same schema', async () => {
75
const schemaA = buildSchema(/* GraphQL */ `
@@ -69,7 +67,7 @@ test('renamed query', async () => {
6967

7068
expect(changed).toBeDefined();
7169
expect(changed.criticality.level).toEqual(CriticalityLevel.Breaking);
72-
expect(changed.message).toEqual(`Schema query root has changed from 'Query' to 'RootQuery'`);
70+
expect(changed.message).toEqual(`Schema query root type was changed from 'Query' to 'RootQuery'`);
7371
});
7472

7573
test('new field and field changed', async () => {
@@ -767,10 +765,10 @@ test('adding root type should not be breaking', async () => {
767765
"criticality": {
768766
"level": "NON_BREAKING",
769767
},
770-
"message": "Schema subscription root has changed from 'unknown' to 'Subscription'",
768+
"message": "Schema subscription type was set to 'Subscription'.",
771769
"meta": {
772770
"newSubscriptionTypeName": "Subscription",
773-
"oldSubscriptionTypeName": "unknown",
771+
"oldSubscriptionTypeName": null,
774772
},
775773
"type": "SCHEMA_SUBSCRIPTION_TYPE_CHANGED",
776774
},
@@ -803,3 +801,161 @@ test('adding root type should not be breaking', async () => {
803801
]
804802
`);
805803
});
804+
805+
test('null old schema', async () => {
806+
const schemaA = null;
807+
808+
const schemaB = buildSchema(/* GraphQL */ `
809+
type Query {
810+
foo: String
811+
}
812+
813+
type Subscription {
814+
onFoo: String
815+
}
816+
`);
817+
818+
const changes = await diff(schemaA, schemaB);
819+
expect(changes).toMatchInlineSnapshot(`
820+
[
821+
{
822+
"criticality": {
823+
"level": "NON_BREAKING",
824+
},
825+
"message": "Schema query root type was set to 'Query'.",
826+
"meta": {
827+
"newQueryTypeName": "Query",
828+
"oldQueryTypeName": null,
829+
},
830+
"type": "SCHEMA_QUERY_TYPE_CHANGED",
831+
},
832+
{
833+
"criticality": {
834+
"level": "NON_BREAKING",
835+
},
836+
"message": "Schema subscription type was set to 'Subscription'.",
837+
"meta": {
838+
"newSubscriptionTypeName": "Subscription",
839+
"oldSubscriptionTypeName": null,
840+
},
841+
"type": "SCHEMA_SUBSCRIPTION_TYPE_CHANGED",
842+
},
843+
{
844+
"criticality": {
845+
"level": "NON_BREAKING",
846+
},
847+
"message": "Type 'Query' was added",
848+
"meta": {
849+
"addedTypeKind": "ObjectTypeDefinition",
850+
"addedTypeName": "Query",
851+
},
852+
"path": "Query",
853+
"type": "TYPE_ADDED",
854+
},
855+
{
856+
"criticality": {
857+
"level": "NON_BREAKING",
858+
},
859+
"message": "Field 'foo' was added to object type 'Query'",
860+
"meta": {
861+
"addedFieldName": "foo",
862+
"addedFieldReturnType": "String",
863+
"typeName": "Query",
864+
"typeType": "object type",
865+
},
866+
"path": "Query.foo",
867+
"type": "FIELD_ADDED",
868+
},
869+
{
870+
"criticality": {
871+
"level": "NON_BREAKING",
872+
},
873+
"message": "Type 'Subscription' was added",
874+
"meta": {
875+
"addedTypeKind": "ObjectTypeDefinition",
876+
"addedTypeName": "Subscription",
877+
},
878+
"path": "Subscription",
879+
"type": "TYPE_ADDED",
880+
},
881+
{
882+
"criticality": {
883+
"level": "NON_BREAKING",
884+
},
885+
"message": "Field 'onFoo' was added to object type 'Subscription'",
886+
"meta": {
887+
"addedFieldName": "onFoo",
888+
"addedFieldReturnType": "String",
889+
"typeName": "Subscription",
890+
"typeType": "object type",
891+
},
892+
"path": "Subscription.onFoo",
893+
"type": "FIELD_ADDED",
894+
},
895+
]
896+
`);
897+
});
898+
899+
test('null new schema', async () => {
900+
const schemaA = buildSchema(/* GraphQL */ `
901+
type Query {
902+
foo: String
903+
}
904+
905+
type Subscription {
906+
onFoo: String
907+
}
908+
`);
909+
910+
const schemaB = null;
911+
912+
const changes = await diff(schemaA, schemaB);
913+
expect(changes).toMatchInlineSnapshot(`
914+
[
915+
{
916+
"criticality": {
917+
"level": "BREAKING",
918+
},
919+
"message": "Schema query root type 'Query' was removed.",
920+
"meta": {
921+
"newQueryTypeName": null,
922+
"oldQueryTypeName": "Query",
923+
},
924+
"type": "SCHEMA_QUERY_TYPE_CHANGED",
925+
},
926+
{
927+
"criticality": {
928+
"level": "BREAKING",
929+
},
930+
"message": "Schema subscription type 'Subscription' was removed.",
931+
"meta": {
932+
"newSubscriptionTypeName": null,
933+
"oldSubscriptionTypeName": "Subscription",
934+
},
935+
"type": "SCHEMA_SUBSCRIPTION_TYPE_CHANGED",
936+
},
937+
{
938+
"criticality": {
939+
"level": "BREAKING",
940+
},
941+
"message": "Type 'Query' was removed",
942+
"meta": {
943+
"removedTypeName": "Query",
944+
},
945+
"path": "Query",
946+
"type": "TYPE_REMOVED",
947+
},
948+
{
949+
"criticality": {
950+
"level": "BREAKING",
951+
},
952+
"message": "Type 'Subscription' was removed",
953+
"meta": {
954+
"removedTypeName": "Subscription",
955+
},
956+
"path": "Subscription",
957+
"type": "TYPE_REMOVED",
958+
},
959+
]
960+
`);
961+
});

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -564,24 +564,24 @@ export type ObjectTypeInterfaceRemovedChange = {
564564
export type SchemaQueryTypeChangedChange = {
565565
type: typeof ChangeType.SchemaQueryTypeChanged;
566566
meta: {
567-
oldQueryTypeName: string;
568-
newQueryTypeName: string;
567+
oldQueryTypeName: string | null;
568+
newQueryTypeName: string | null;
569569
};
570570
};
571571

572572
export type SchemaMutationTypeChangedChange = {
573573
type: typeof ChangeType.SchemaMutationTypeChanged;
574574
meta: {
575-
oldMutationTypeName: string;
576-
newMutationTypeName: string;
575+
oldMutationTypeName: string | null;
576+
newMutationTypeName: string | null;
577577
};
578578
};
579579

580580
export type SchemaSubscriptionTypeChangedChange = {
581581
type: typeof ChangeType.SchemaSubscriptionTypeChanged;
582582
meta: {
583-
oldSubscriptionTypeName: string;
584-
newSubscriptionTypeName: string;
583+
oldSubscriptionTypeName: string | null;
584+
newSubscriptionTypeName: string | null;
585585
};
586586
};
587587

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ type KindToPayload = {
108108
change: DirectiveUsageEnumValueAddedChange | DirectiveUsageEnumValueRemovedChange;
109109
};
110110
[Kind.SCHEMA_DEFINITION]: {
111-
input: GraphQLSchema;
111+
input: GraphQLSchema | null;
112112
change: DirectiveUsageSchemaAddedChange | DirectiveUsageSchemaRemovedChange;
113113
};
114114
[Kind.SCALAR_TYPE_DEFINITION]: {
@@ -836,9 +836,9 @@ export function directiveUsageAdded<K extends keyof KindToPayload>(
836836
type: ChangeType.DirectiveUsageSchemaAdded,
837837
meta: {
838838
addedDirectiveName: directive.name.value,
839-
schemaTypeName: payload.getQueryType()?.name || '',
839+
schemaTypeName: payload?.getQueryType()?.name || '',
840840
addedToNewType,
841-
directiveRepeatedTimes: directiveRepeatTimes(payload.astNode?.directives ?? [], directive),
841+
directiveRepeatedTimes: directiveRepeatTimes(payload?.astNode?.directives ?? [], directive),
842842
},
843843
});
844844
}
@@ -1016,8 +1016,8 @@ export function directiveUsageRemoved<K extends keyof KindToPayload>(
10161016
type: ChangeType.DirectiveUsageSchemaRemoved,
10171017
meta: {
10181018
removedDirectiveName: directive.name.value,
1019-
schemaTypeName: payload.getQueryType()?.name || '',
1020-
directiveRepeatedTimes: directiveRepeatTimes(payload.astNode?.directives ?? [], directive),
1019+
schemaTypeName: payload?.getQueryType()?.name || '',
1020+
directiveRepeatedTimes: directiveRepeatTimes(payload?.astNode?.directives ?? [], directive),
10211021
},
10221022
});
10231023
}

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

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,21 @@ import {
99
} from './change.js';
1010

1111
function buildSchemaQueryTypeChangedMessage(args: SchemaQueryTypeChangedChange['meta']): string {
12-
return `Schema query root has changed from '${args.oldQueryTypeName}' to '${args.newQueryTypeName}'`;
12+
if (args.oldQueryTypeName === null) {
13+
return `Schema query root type was set to '${args.newQueryTypeName}'.`;
14+
}
15+
if (args.newQueryTypeName === null) {
16+
return `Schema query root type '${args.oldQueryTypeName}' was removed.`;
17+
}
18+
return `Schema query root type was changed from '${args.oldQueryTypeName}' to '${args.newQueryTypeName}'`;
1319
}
1420

1521
export function schemaQueryTypeChangedFromMeta(args: SchemaQueryTypeChangedChange) {
1622
return {
1723
type: ChangeType.SchemaQueryTypeChanged,
1824
criticality: {
1925
level:
20-
args.meta.oldQueryTypeName === 'unknown'
26+
args.meta.oldQueryTypeName === null
2127
? CriticalityLevel.NonBreaking
2228
: CriticalityLevel.Breaking,
2329
},
@@ -27,11 +33,11 @@ export function schemaQueryTypeChangedFromMeta(args: SchemaQueryTypeChangedChang
2733
}
2834

2935
export function schemaQueryTypeChanged(
30-
oldSchema: GraphQLSchema,
31-
newSchema: GraphQLSchema,
36+
oldSchema: GraphQLSchema | null,
37+
newSchema: GraphQLSchema | null,
3238
): Change<typeof ChangeType.SchemaQueryTypeChanged> {
33-
const oldName = (oldSchema.getQueryType() || ({} as any)).name || 'unknown';
34-
const newName = (newSchema.getQueryType() || ({} as any)).name || 'unknown';
39+
const oldName = oldSchema?.getQueryType()?.name || null;
40+
const newName = newSchema?.getQueryType()?.name || null;
3541

3642
return schemaQueryTypeChangedFromMeta({
3743
type: ChangeType.SchemaQueryTypeChanged,
@@ -45,15 +51,21 @@ export function schemaQueryTypeChanged(
4551
function buildSchemaMutationTypeChangedMessage(
4652
args: SchemaMutationTypeChangedChange['meta'],
4753
): string {
48-
return `Schema mutation root has changed from '${args.oldMutationTypeName}' to '${args.newMutationTypeName}'`;
54+
if (args.oldMutationTypeName === null) {
55+
return `Schema mutation type was set to '${args.newMutationTypeName}'.`;
56+
}
57+
if (args.newMutationTypeName === null) {
58+
return `Schema mutation type '${args.oldMutationTypeName}' was removed.`;
59+
}
60+
return `Schema mutation type was changed from '${args.oldMutationTypeName}' to '${args.newMutationTypeName}'`;
4961
}
5062

5163
export function schemaMutationTypeChangedFromMeta(args: SchemaMutationTypeChangedChange) {
5264
return {
5365
type: ChangeType.SchemaMutationTypeChanged,
5466
criticality: {
5567
level:
56-
args.meta.oldMutationTypeName === 'unknown'
68+
args.meta.oldMutationTypeName === null
5769
? CriticalityLevel.NonBreaking
5870
: CriticalityLevel.Breaking,
5971
},
@@ -63,11 +75,11 @@ export function schemaMutationTypeChangedFromMeta(args: SchemaMutationTypeChange
6375
}
6476

6577
export function schemaMutationTypeChanged(
66-
oldSchema: GraphQLSchema,
67-
newSchema: GraphQLSchema,
78+
oldSchema: GraphQLSchema | null,
79+
newSchema: GraphQLSchema | null,
6880
): Change<typeof ChangeType.SchemaMutationTypeChanged> {
69-
const oldName = (oldSchema.getMutationType() || ({} as any)).name || 'unknown';
70-
const newName = (newSchema.getMutationType() || ({} as any)).name || 'unknown';
81+
const oldName = oldSchema?.getMutationType()?.name || null;
82+
const newName = newSchema?.getMutationType()?.name || null;
7183

7284
return schemaMutationTypeChangedFromMeta({
7385
type: ChangeType.SchemaMutationTypeChanged,
@@ -81,15 +93,21 @@ export function schemaMutationTypeChanged(
8193
function buildSchemaSubscriptionTypeChangedMessage(
8294
args: SchemaSubscriptionTypeChangedChange['meta'],
8395
): string {
84-
return `Schema subscription root has changed from '${args.oldSubscriptionTypeName}' to '${args.newSubscriptionTypeName}'`;
96+
if (args.oldSubscriptionTypeName === null) {
97+
return `Schema subscription type was set to '${args.newSubscriptionTypeName}'.`;
98+
}
99+
if (args.newSubscriptionTypeName === null) {
100+
return `Schema subscription type '${args.oldSubscriptionTypeName}' was removed.`;
101+
}
102+
return `Schema subscription type was changed from '${args.oldSubscriptionTypeName}' to '${args.newSubscriptionTypeName}'`;
85103
}
86104

87105
export function schemaSubscriptionTypeChangedFromMeta(args: SchemaSubscriptionTypeChangedChange) {
88106
return {
89107
type: ChangeType.SchemaSubscriptionTypeChanged,
90108
criticality: {
91109
level:
92-
args.meta.oldSubscriptionTypeName === 'unknown'
110+
args.meta.oldSubscriptionTypeName === null
93111
? CriticalityLevel.NonBreaking
94112
: CriticalityLevel.Breaking,
95113
},
@@ -99,11 +117,11 @@ export function schemaSubscriptionTypeChangedFromMeta(args: SchemaSubscriptionTy
99117
}
100118

101119
export function schemaSubscriptionTypeChanged(
102-
oldSchema: GraphQLSchema,
103-
newSchema: GraphQLSchema,
120+
oldSchema: GraphQLSchema | null,
121+
newSchema: GraphQLSchema | null,
104122
): Change<typeof ChangeType.SchemaSubscriptionTypeChanged> {
105-
const oldName = (oldSchema.getSubscriptionType() || ({} as any)).name || 'unknown';
106-
const newName = (newSchema.getSubscriptionType() || ({} as any)).name || 'unknown';
123+
const oldName = oldSchema?.getSubscriptionType()?.name || null;
124+
const newName = newSchema?.getSubscriptionType()?.name || null;
107125

108126
return schemaSubscriptionTypeChangedFromMeta({
109127
type: ChangeType.SchemaSubscriptionTypeChanged,

packages/core/src/diff/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ export * from './onComplete/types.js';
1111
export type { UsageHandler } from './rules/consider-usage.js';
1212

1313
export function diff(
14-
oldSchema: GraphQLSchema,
15-
newSchema: GraphQLSchema,
14+
oldSchema: GraphQLSchema | null,
15+
newSchema: GraphQLSchema | null,
1616
rules: Rule[] = [],
1717
config?: rules.ConsiderUsageConfig,
1818
): Promise<Change[]> {

0 commit comments

Comments
 (0)