@graphql-inspector/core 3.1.2 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/diff/rules/index.d.ts +1 -0
- package/index.js +114 -5
- package/index.mjs +115 -6
- package/package.json +1 -1
- package/utils/compare.d.ts +0 -1
- package/utils/isDeprecated.d.ts +2 -2
package/diff/rules/index.d.ts
CHANGED
package/index.js
CHANGED
|
@@ -20,7 +20,21 @@ function isEqual(a, b) {
|
|
|
20
20
|
if (a.length !== b.length)
|
|
21
21
|
return false;
|
|
22
22
|
for (var index = 0; index < a.length; index++) {
|
|
23
|
-
if (a[index]
|
|
23
|
+
if (!isEqual(a[index], b[index])) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
if (a && b && typeof a === 'object' && typeof b === 'object') {
|
|
30
|
+
const aRecord = a;
|
|
31
|
+
const bRecord = b;
|
|
32
|
+
const aKeys = Object.keys(aRecord);
|
|
33
|
+
const bKeys = Object.keys(bRecord);
|
|
34
|
+
if (aKeys.length !== bKeys.length)
|
|
35
|
+
return false;
|
|
36
|
+
for (const key of aKeys) {
|
|
37
|
+
if (!isEqual(aRecord[key], bRecord[key])) {
|
|
24
38
|
return false;
|
|
25
39
|
}
|
|
26
40
|
}
|
|
@@ -35,7 +49,7 @@ function isVoid(a) {
|
|
|
35
49
|
return typeof a === 'undefined' || a === null;
|
|
36
50
|
}
|
|
37
51
|
function diffArrays(a, b) {
|
|
38
|
-
return a.filter((c) => !b.some((d) => d
|
|
52
|
+
return a.filter((c) => !b.some((d) => isEqual(d, c)));
|
|
39
53
|
}
|
|
40
54
|
function compareLists(oldList, newList, callbacks) {
|
|
41
55
|
const oldMap = keyMap(oldList, ({ name }) => name);
|
|
@@ -115,7 +129,7 @@ function safeChangeForInputValue(oldType, newType) {
|
|
|
115
129
|
return safeChangeForInputValue(oldType.ofType, newType.ofType);
|
|
116
130
|
}
|
|
117
131
|
if (graphql.isNonNullType(oldType)) {
|
|
118
|
-
const ofType = graphql.isNonNullType(newType) ? newType : newType;
|
|
132
|
+
const ofType = graphql.isNonNullType(newType) ? newType.ofType : newType;
|
|
119
133
|
return safeChangeForInputValue(oldType.ofType, ofType);
|
|
120
134
|
}
|
|
121
135
|
return false;
|
|
@@ -204,6 +218,75 @@ function removeDirectives(node, directiveNames) {
|
|
|
204
218
|
}
|
|
205
219
|
return node;
|
|
206
220
|
}
|
|
221
|
+
function getReachableTypes(schema) {
|
|
222
|
+
const reachableTypes = new Set();
|
|
223
|
+
const collect = (type) => {
|
|
224
|
+
const typeName = type.name;
|
|
225
|
+
if (reachableTypes.has(typeName)) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
reachableTypes.add(typeName);
|
|
229
|
+
if (graphql.isScalarType(type)) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
else if (graphql.isInterfaceType(type) || graphql.isObjectType(type)) {
|
|
233
|
+
if (graphql.isInterfaceType(type)) {
|
|
234
|
+
const { objects, interfaces } = schema.getImplementations(type);
|
|
235
|
+
for (const child of objects) {
|
|
236
|
+
collect(child);
|
|
237
|
+
}
|
|
238
|
+
for (const child of interfaces) {
|
|
239
|
+
collect(child);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const fields = type.getFields();
|
|
243
|
+
for (const fieldName in fields) {
|
|
244
|
+
const field = fields[fieldName];
|
|
245
|
+
collect(resolveOutputType(field.type));
|
|
246
|
+
const args = field.args;
|
|
247
|
+
for (const argName in args) {
|
|
248
|
+
const arg = args[argName];
|
|
249
|
+
collect(resolveInputType(arg.type));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
else if (graphql.isUnionType(type)) {
|
|
254
|
+
const types = type.getTypes();
|
|
255
|
+
for (const child of types) {
|
|
256
|
+
collect(child);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
else if (graphql.isInputObjectType(type)) {
|
|
260
|
+
const fields = type.getFields();
|
|
261
|
+
for (const fieldName in fields) {
|
|
262
|
+
const field = fields[fieldName];
|
|
263
|
+
collect(resolveInputType(field.type));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
for (const type of [
|
|
268
|
+
schema.getQueryType(),
|
|
269
|
+
schema.getMutationType(),
|
|
270
|
+
schema.getSubscriptionType(),
|
|
271
|
+
]) {
|
|
272
|
+
if (type) {
|
|
273
|
+
collect(type);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return reachableTypes;
|
|
277
|
+
}
|
|
278
|
+
function resolveOutputType(output) {
|
|
279
|
+
if (graphql.isListType(output) || graphql.isNonNullType(output)) {
|
|
280
|
+
return resolveOutputType(output.ofType);
|
|
281
|
+
}
|
|
282
|
+
return output;
|
|
283
|
+
}
|
|
284
|
+
function resolveInputType(input) {
|
|
285
|
+
if (graphql.isListType(input) || graphql.isNonNullType(input)) {
|
|
286
|
+
return resolveInputType(input.ofType);
|
|
287
|
+
}
|
|
288
|
+
return input;
|
|
289
|
+
}
|
|
207
290
|
|
|
208
291
|
(function (ChangeType) {
|
|
209
292
|
// Argument
|
|
@@ -620,7 +703,7 @@ function inputFieldRemoved(input, field) {
|
|
|
620
703
|
reason: 'Removing an input field will cause existing queries that use this input field to error.',
|
|
621
704
|
},
|
|
622
705
|
type: exports.ChangeType.InputFieldRemoved,
|
|
623
|
-
message: `Input field '${field.name}' was removed from input object type '${input.name}'`,
|
|
706
|
+
message: `Input field '${field.name}' ${isDeprecated(field) ? '(deprecated) ' : ''}was removed from input object type '${input.name}'`,
|
|
624
707
|
path: [input.name, field.name].join('.'),
|
|
625
708
|
};
|
|
626
709
|
}
|
|
@@ -1287,6 +1370,18 @@ const suppressRemovalOfDeprecatedField = ({ changes, oldSchema, }) => {
|
|
|
1287
1370
|
}
|
|
1288
1371
|
}
|
|
1289
1372
|
}
|
|
1373
|
+
if (change.type === exports.ChangeType.InputFieldRemoved &&
|
|
1374
|
+
change.criticality.level === exports.CriticalityLevel.Breaking &&
|
|
1375
|
+
change.path) {
|
|
1376
|
+
const [inputName, inputItem] = parsePath(change.path);
|
|
1377
|
+
const type = oldSchema.getType(inputName);
|
|
1378
|
+
if (graphql.isInputObjectType(type)) {
|
|
1379
|
+
const item = type.getFields()[inputItem];
|
|
1380
|
+
if (item && isDeprecated(item)) {
|
|
1381
|
+
return Object.assign(Object.assign({}, change), { criticality: Object.assign(Object.assign({}, change.criticality), { level: exports.CriticalityLevel.Dangerous }) });
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1290
1385
|
return change;
|
|
1291
1386
|
});
|
|
1292
1387
|
};
|
|
@@ -1341,12 +1436,26 @@ const considerUsage = ({ changes, config, }) => tslib.__awaiter(void 0, void 0,
|
|
|
1341
1436
|
});
|
|
1342
1437
|
});
|
|
1343
1438
|
|
|
1439
|
+
const safeUnreachable = ({ changes, oldSchema }) => {
|
|
1440
|
+
const reachable = getReachableTypes(oldSchema);
|
|
1441
|
+
return changes.map((change) => {
|
|
1442
|
+
if (change.criticality.level === exports.CriticalityLevel.Breaking && change.path) {
|
|
1443
|
+
const [typeName] = parsePath(change.path);
|
|
1444
|
+
if (!reachable.has(typeName)) {
|
|
1445
|
+
return Object.assign(Object.assign({}, change), { criticality: Object.assign(Object.assign({}, change.criticality), { level: exports.CriticalityLevel.NonBreaking }), message: 'Unreachable from root' });
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
return change;
|
|
1449
|
+
});
|
|
1450
|
+
};
|
|
1451
|
+
|
|
1344
1452
|
const rules = /*#__PURE__*/Object.freeze({
|
|
1345
1453
|
__proto__: null,
|
|
1346
1454
|
dangerousBreaking: dangerousBreaking,
|
|
1347
1455
|
suppressRemovalOfDeprecatedField: suppressRemovalOfDeprecatedField,
|
|
1348
1456
|
ignoreDescriptionChanges: ignoreDescriptionChanges,
|
|
1349
|
-
considerUsage: considerUsage
|
|
1457
|
+
considerUsage: considerUsage,
|
|
1458
|
+
safeUnreachable: safeUnreachable
|
|
1350
1459
|
});
|
|
1351
1460
|
|
|
1352
1461
|
const DiffRule = rules;
|
package/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { __awaiter } from 'tslib';
|
|
2
|
-
import { Kind, TypeInfo, visit, visitWithTypeInfo, GraphQLError, getNamedType,
|
|
2
|
+
import { Kind, TypeInfo, visit, visitWithTypeInfo, GraphQLError, getNamedType, isScalarType, isInterfaceType, isObjectType, isUnionType, isInputObjectType, isListType, isNonNullType, isWrappingType, isEnumType, parse, extendSchema, print, validate as validate$1, printType } from 'graphql';
|
|
3
3
|
import inspect from 'object-inspect';
|
|
4
4
|
import { DepGraph } from 'dependency-graph';
|
|
5
5
|
|
|
@@ -14,7 +14,21 @@ function isEqual(a, b) {
|
|
|
14
14
|
if (a.length !== b.length)
|
|
15
15
|
return false;
|
|
16
16
|
for (var index = 0; index < a.length; index++) {
|
|
17
|
-
if (a[index]
|
|
17
|
+
if (!isEqual(a[index], b[index])) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
if (a && b && typeof a === 'object' && typeof b === 'object') {
|
|
24
|
+
const aRecord = a;
|
|
25
|
+
const bRecord = b;
|
|
26
|
+
const aKeys = Object.keys(aRecord);
|
|
27
|
+
const bKeys = Object.keys(bRecord);
|
|
28
|
+
if (aKeys.length !== bKeys.length)
|
|
29
|
+
return false;
|
|
30
|
+
for (const key of aKeys) {
|
|
31
|
+
if (!isEqual(aRecord[key], bRecord[key])) {
|
|
18
32
|
return false;
|
|
19
33
|
}
|
|
20
34
|
}
|
|
@@ -29,7 +43,7 @@ function isVoid(a) {
|
|
|
29
43
|
return typeof a === 'undefined' || a === null;
|
|
30
44
|
}
|
|
31
45
|
function diffArrays(a, b) {
|
|
32
|
-
return a.filter((c) => !b.some((d) => d
|
|
46
|
+
return a.filter((c) => !b.some((d) => isEqual(d, c)));
|
|
33
47
|
}
|
|
34
48
|
function compareLists(oldList, newList, callbacks) {
|
|
35
49
|
const oldMap = keyMap(oldList, ({ name }) => name);
|
|
@@ -109,7 +123,7 @@ function safeChangeForInputValue(oldType, newType) {
|
|
|
109
123
|
return safeChangeForInputValue(oldType.ofType, newType.ofType);
|
|
110
124
|
}
|
|
111
125
|
if (isNonNullType(oldType)) {
|
|
112
|
-
const ofType = isNonNullType(newType) ? newType : newType;
|
|
126
|
+
const ofType = isNonNullType(newType) ? newType.ofType : newType;
|
|
113
127
|
return safeChangeForInputValue(oldType.ofType, ofType);
|
|
114
128
|
}
|
|
115
129
|
return false;
|
|
@@ -198,6 +212,75 @@ function removeDirectives(node, directiveNames) {
|
|
|
198
212
|
}
|
|
199
213
|
return node;
|
|
200
214
|
}
|
|
215
|
+
function getReachableTypes(schema) {
|
|
216
|
+
const reachableTypes = new Set();
|
|
217
|
+
const collect = (type) => {
|
|
218
|
+
const typeName = type.name;
|
|
219
|
+
if (reachableTypes.has(typeName)) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
reachableTypes.add(typeName);
|
|
223
|
+
if (isScalarType(type)) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
else if (isInterfaceType(type) || isObjectType(type)) {
|
|
227
|
+
if (isInterfaceType(type)) {
|
|
228
|
+
const { objects, interfaces } = schema.getImplementations(type);
|
|
229
|
+
for (const child of objects) {
|
|
230
|
+
collect(child);
|
|
231
|
+
}
|
|
232
|
+
for (const child of interfaces) {
|
|
233
|
+
collect(child);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const fields = type.getFields();
|
|
237
|
+
for (const fieldName in fields) {
|
|
238
|
+
const field = fields[fieldName];
|
|
239
|
+
collect(resolveOutputType(field.type));
|
|
240
|
+
const args = field.args;
|
|
241
|
+
for (const argName in args) {
|
|
242
|
+
const arg = args[argName];
|
|
243
|
+
collect(resolveInputType(arg.type));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
else if (isUnionType(type)) {
|
|
248
|
+
const types = type.getTypes();
|
|
249
|
+
for (const child of types) {
|
|
250
|
+
collect(child);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
else if (isInputObjectType(type)) {
|
|
254
|
+
const fields = type.getFields();
|
|
255
|
+
for (const fieldName in fields) {
|
|
256
|
+
const field = fields[fieldName];
|
|
257
|
+
collect(resolveInputType(field.type));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
for (const type of [
|
|
262
|
+
schema.getQueryType(),
|
|
263
|
+
schema.getMutationType(),
|
|
264
|
+
schema.getSubscriptionType(),
|
|
265
|
+
]) {
|
|
266
|
+
if (type) {
|
|
267
|
+
collect(type);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return reachableTypes;
|
|
271
|
+
}
|
|
272
|
+
function resolveOutputType(output) {
|
|
273
|
+
if (isListType(output) || isNonNullType(output)) {
|
|
274
|
+
return resolveOutputType(output.ofType);
|
|
275
|
+
}
|
|
276
|
+
return output;
|
|
277
|
+
}
|
|
278
|
+
function resolveInputType(input) {
|
|
279
|
+
if (isListType(input) || isNonNullType(input)) {
|
|
280
|
+
return resolveInputType(input.ofType);
|
|
281
|
+
}
|
|
282
|
+
return input;
|
|
283
|
+
}
|
|
201
284
|
|
|
202
285
|
var ChangeType;
|
|
203
286
|
(function (ChangeType) {
|
|
@@ -616,7 +699,7 @@ function inputFieldRemoved(input, field) {
|
|
|
616
699
|
reason: 'Removing an input field will cause existing queries that use this input field to error.',
|
|
617
700
|
},
|
|
618
701
|
type: ChangeType.InputFieldRemoved,
|
|
619
|
-
message: `Input field '${field.name}' was removed from input object type '${input.name}'`,
|
|
702
|
+
message: `Input field '${field.name}' ${isDeprecated(field) ? '(deprecated) ' : ''}was removed from input object type '${input.name}'`,
|
|
620
703
|
path: [input.name, field.name].join('.'),
|
|
621
704
|
};
|
|
622
705
|
}
|
|
@@ -1283,6 +1366,18 @@ const suppressRemovalOfDeprecatedField = ({ changes, oldSchema, }) => {
|
|
|
1283
1366
|
}
|
|
1284
1367
|
}
|
|
1285
1368
|
}
|
|
1369
|
+
if (change.type === ChangeType.InputFieldRemoved &&
|
|
1370
|
+
change.criticality.level === CriticalityLevel.Breaking &&
|
|
1371
|
+
change.path) {
|
|
1372
|
+
const [inputName, inputItem] = parsePath(change.path);
|
|
1373
|
+
const type = oldSchema.getType(inputName);
|
|
1374
|
+
if (isInputObjectType(type)) {
|
|
1375
|
+
const item = type.getFields()[inputItem];
|
|
1376
|
+
if (item && isDeprecated(item)) {
|
|
1377
|
+
return Object.assign(Object.assign({}, change), { criticality: Object.assign(Object.assign({}, change.criticality), { level: CriticalityLevel.Dangerous }) });
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1286
1381
|
return change;
|
|
1287
1382
|
});
|
|
1288
1383
|
};
|
|
@@ -1337,12 +1432,26 @@ const considerUsage = ({ changes, config, }) => __awaiter(void 0, void 0, void 0
|
|
|
1337
1432
|
});
|
|
1338
1433
|
});
|
|
1339
1434
|
|
|
1435
|
+
const safeUnreachable = ({ changes, oldSchema }) => {
|
|
1436
|
+
const reachable = getReachableTypes(oldSchema);
|
|
1437
|
+
return changes.map((change) => {
|
|
1438
|
+
if (change.criticality.level === CriticalityLevel.Breaking && change.path) {
|
|
1439
|
+
const [typeName] = parsePath(change.path);
|
|
1440
|
+
if (!reachable.has(typeName)) {
|
|
1441
|
+
return Object.assign(Object.assign({}, change), { criticality: Object.assign(Object.assign({}, change.criticality), { level: CriticalityLevel.NonBreaking }), message: 'Unreachable from root' });
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
return change;
|
|
1445
|
+
});
|
|
1446
|
+
};
|
|
1447
|
+
|
|
1340
1448
|
const rules = /*#__PURE__*/Object.freeze({
|
|
1341
1449
|
__proto__: null,
|
|
1342
1450
|
dangerousBreaking: dangerousBreaking,
|
|
1343
1451
|
suppressRemovalOfDeprecatedField: suppressRemovalOfDeprecatedField,
|
|
1344
1452
|
ignoreDescriptionChanges: ignoreDescriptionChanges,
|
|
1345
|
-
considerUsage: considerUsage
|
|
1453
|
+
considerUsage: considerUsage,
|
|
1454
|
+
safeUnreachable: safeUnreachable
|
|
1346
1455
|
});
|
|
1347
1456
|
|
|
1348
1457
|
const DiffRule = rules;
|
package/package.json
CHANGED
package/utils/compare.d.ts
CHANGED
|
@@ -3,7 +3,6 @@ export declare function isEqual<T>(a: T, b: T): boolean;
|
|
|
3
3
|
export declare function isNotEqual<T>(a: T, b: T): boolean;
|
|
4
4
|
export declare function isVoid<T>(a: T): boolean;
|
|
5
5
|
export declare function diffArrays<T>(a: T[] | readonly T[], b: T[] | readonly T[]): T[];
|
|
6
|
-
export declare function unionArrays<T>(a: T[] | readonly T[], b: T[] | readonly T[]): T[];
|
|
7
6
|
export declare function compareLists<T extends {
|
|
8
7
|
name: string;
|
|
9
8
|
}>(oldList: readonly T[], newList: readonly T[], callbacks?: {
|
package/utils/isDeprecated.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { GraphQLEnumValue, GraphQLField } from 'graphql';
|
|
2
|
-
export declare function isDeprecated(fieldOrEnumValue: GraphQLField<any, any> | GraphQLEnumValue): boolean;
|
|
1
|
+
import { GraphQLInputField, GraphQLEnumValue, GraphQLField } from 'graphql';
|
|
2
|
+
export declare function isDeprecated(fieldOrEnumValue: GraphQLField<any, any> | GraphQLEnumValue | GraphQLInputField): boolean;
|