Skip to content

Commit ef13125

Browse files
authored
Add patch package to apply diff changes to schemas; improve accuracy of severity levels (#2893)
A major change to the structure on some of the changes, which allows more accurate determination of severity levels, and directive support. Added support for repeatable directives. And introduces a new `@graphql-inspector/patch` package which supports applying (verbose) changes to an existing schema -- effectively rebasing the changes.
1 parent ef2e454 commit ef13125

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+9304
-572
lines changed

.changeset/empty-cougars-grab.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
'@graphql-inspector/patch': patch
3+
---
4+
5+
Initial release. Patch applies a list of changes (output from `@graphql-inspector/core`'s `diff`) to
6+
a GraphQL Schema.
7+
8+
Example usage:
9+
10+
```typescript
11+
import { buildSchema } from "graphql";
12+
import { diff } from "@graphql-inspector/core";
13+
import { patchSchema } from "@graphql-inspector/patch";
14+
15+
const schemaA = buildSchema(before, { assumeValid: true, assumeValidSDL: true });
16+
const schemaB = buildSchema(after, { assumeValid: true, assumeValidSDL: true });
17+
18+
const changes = await diff(schemaA, schemaB);
19+
const patched = patchSchema(schemaA, changes);
20+
```
21+
22+
If working from an AST, you may alternatively use the exported `patch` function. But be careful to make sure directives are included in your AST or those changes will be missed.

.changeset/long-rules-shop.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@graphql-inspector/diff-command': major
3+
---
4+
5+
Added option to include nested changes. Use `--rule verboseChanges`. Enabling this will output nested changes. I.e. if adding a new type, then `verboseChanges` will also include the addition of the fields on that type. By default, these changes are excluded from the output because they don't impact the outcome and create a lot of noise.
6+
7+
Added better directive support.
8+
9+
Adjusted severity level for conditionally safe changes:
10+
- Adding or removing deprecated directive is considered non-breaking
11+
- Adding an interface to a new type is non-breaking
12+
- Adding an argument to a new field is non-breaking
13+
- Adding a directive to a new object (type, interface, etc..) is non-breaking

.changeset/seven-jars-yell.md

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
---
2+
'@graphql-inspector/core': major
3+
---
4+
5+
This is a major change to `@graphql-inspector/core` and introduces a new `@graphql-inspector/patch` package, which applies changes output from `diff` on top of a schema -- essentially rebasing these changes onto any schema.
6+
7+
These changes include:
8+
- Numerous adjustments to Change types to create more accurate severity levels, such as a boolean indicating if the change applies to a new type or an existing type.
9+
- Adjustmented the "path" on several change types in order to consistently map to the exact AST node being changed. For example, `EnumValueDeprecationReasonAdded`'s path previously referenced the enumValue (e.g. `EnumName.value`), not the deprecated directive (e.g. `EnumName.value.@deprecated`).
10+
- Added new attributes in order to provide enough context for a new "@graphql-inspector/patch" function to apply changes accurately.
11+
- Added support for repeatable directives
12+
- Includes all nested changes in `diff` output when a new node is added. This can dramatically increase the number of changes listed which can be noisy, but it makes it possible for "@graphql-inspector/patch" to apply all changes from a schema. This can be optionally filtered using a newly exported `DiffRule.simplifyChanges` rule.
13+
14+
For example, given an existing schema:
15+
16+
```graphql
17+
type User {
18+
id: ID!
19+
name: String!
20+
}
21+
```
22+
23+
And a diff schema:
24+
25+
```graphql
26+
type User {
27+
id: ID!
28+
name: String!
29+
address: Address
30+
}
31+
32+
type Address {
33+
line1: String!
34+
line2: String!
35+
}
36+
```
37+
38+
Then previously the output would be:
39+
40+
```json
41+
[
42+
{
43+
"criticality": {
44+
"level": "NON_BREAKING",
45+
},
46+
"message": "Type 'Address' was added",
47+
"meta": {
48+
"addedTypeKind": "ObjectTypeDefinition",
49+
"addedTypeName": "Address",
50+
},
51+
"path": "Address",
52+
"type": "TYPE_ADDED",
53+
},
54+
{
55+
"criticality": {
56+
"level": "NON_BREAKING",
57+
},
58+
"message": "Field 'address' was added to object type 'User'",
59+
"meta": {
60+
"addedFieldName": "address",
61+
"addedFieldReturnType": "Address",
62+
"typeName": "User",
63+
"typeType": "object type",
64+
},
65+
"path": "User.address",
66+
"type": "FIELD_ADDED",
67+
},
68+
]
69+
```
70+
71+
But now the output also includes the fields inside the new `Address` type:
72+
73+
```json
74+
[
75+
{
76+
"criticality": {
77+
"level": "NON_BREAKING",
78+
},
79+
"message": "Type 'Address' was added",
80+
"meta": {
81+
"addedTypeKind": "ObjectTypeDefinition",
82+
"addedTypeName": "Address",
83+
},
84+
"path": "Address",
85+
"type": "TYPE_ADDED",
86+
},
87+
{
88+
"criticality": {
89+
"level": "NON_BREAKING",
90+
},
91+
"message": "Field 'line1' was added to object type 'Address'",
92+
"meta": {
93+
"addedFieldName": "line1",
94+
"addedFieldReturnType": "String!",
95+
"typeName": "Address",
96+
"typeType": "object type",
97+
},
98+
"path": "Address.line1",
99+
"type": "FIELD_ADDED",
100+
},
101+
{
102+
"criticality": {
103+
"level": "NON_BREAKING",
104+
},
105+
"message": "Field 'line2' was added to object type 'Address'",
106+
"meta": {
107+
"addedFieldName": "line2",
108+
"addedFieldReturnType": "String!",
109+
"typeName": "Address",
110+
"typeType": "object type",
111+
},
112+
"path": "Address.line2",
113+
"type": "FIELD_ADDED",
114+
},
115+
{
116+
"criticality": {
117+
"level": "NON_BREAKING",
118+
},
119+
"message": "Field 'address' was added to object type 'User'",
120+
"meta": {
121+
"addedFieldName": "address",
122+
"addedFieldReturnType": "Address",
123+
"typeName": "User",
124+
"typeType": "object type",
125+
},
126+
"path": "User.address",
127+
"type": "FIELD_ADDED",
128+
},
129+
]
130+
```
131+
132+
These additional changes can be filtered using a new rule:
133+
134+
```js
135+
import { DiffRule, diff } from "@graphql-inspector/core";
136+
const changes = await diff(a, b, [DiffRule.simplifyChanges]);
137+
```

packages/commands/diff/src/index.ts

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,20 +32,27 @@ export async function handler(input: {
3232
? resolveCompletionHandler(input.onComplete)
3333
: failOnBreakingChanges;
3434

35-
const rules = input.rules
36-
? input.rules
37-
.filter(isString)
38-
.map((name): Rule => {
39-
const rule = resolveRule(name);
35+
let verboseChanges = false;
36+
const rules = [...(input.rules ?? [])]
37+
.filter(isString)
38+
.map((name): Rule | undefined => {
39+
if (name === 'verboseChanges') {
40+
verboseChanges = true;
41+
return;
42+
}
4043

41-
if (!rule) {
42-
throw new Error(`Rule '${name}' does not exist!\n`);
43-
}
44+
const rule = resolveRule(name);
4445

45-
return rule;
46-
})
47-
.filter(f => f)
48-
: [];
46+
if (!rule) {
47+
throw new Error(`Rule '${name}' does not exist!\n`);
48+
}
49+
50+
return rule;
51+
})
52+
.filter((f): f is NonNullable<typeof f> => !!f);
53+
if (!verboseChanges) {
54+
rules.push(DiffRule.simplifyChanges);
55+
}
4956

5057
const changes = await diffSchema(input.oldSchema, input.newSchema, rules, {
5158
checkUsage: input.onUsage ? resolveUsageHandler(input.onUsage) : undefined,

0 commit comments

Comments
 (0)