@graphql-inspector/core 0.0.0-canary.02a53c5 → 0.0.0-canary.0f79587

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/README.md CHANGED
@@ -36,17 +36,17 @@ import {
36
36
  Change,
37
37
  InvalidDocument,
38
38
  SimilarMap,
39
- SchemaCoverage,
40
- } from '@graphql-inspector/core';
39
+ SchemaCoverage
40
+ } from '@graphql-inspector/core'
41
41
 
42
42
  // diff
43
- const changes: Change[] = diff(schemaA, schemaB);
43
+ const changes: Change[] = diff(schemaA, schemaB)
44
44
  // validate
45
- const invalid: InvalidDocument[] = validate(documentsGlob, schema);
45
+ const invalid: InvalidDocument[] = validate(documentsGlob, schema)
46
46
  // similar
47
- const similar: SimilarMap = similar(schema, typename, threshold);
47
+ const similar: SimilarMap = similar(schema, typename, threshold)
48
48
  // coverage
49
- const schemaCoverage: SchemaCoverage = coverage(schema, documents);
49
+ const schemaCoverage: SchemaCoverage = coverage(schema, documents)
50
50
  // ...
51
51
  ```
52
52
 
package/diff/index.d.ts CHANGED
@@ -5,4 +5,5 @@ import * as rules from './rules';
5
5
  export * from './rules/types';
6
6
  export declare const DiffRule: typeof rules;
7
7
  export * from './onComplete/types';
8
- export declare function diff(oldSchema: GraphQLSchema, newSchema: GraphQLSchema, rules?: Rule[]): Change[];
8
+ export type { UsageHandler } from './rules/consider-usage';
9
+ export declare function diff(oldSchema: GraphQLSchema, newSchema: GraphQLSchema, rules?: Rule[], config?: rules.ConsiderUsageConfig): Promise<Change[]>;
@@ -0,0 +1,2 @@
1
+ import { ConsiderUsageConfig } from './consider-usage';
2
+ export declare type Config = ConsiderUsageConfig;
@@ -0,0 +1,29 @@
1
+ import { Rule } from './types';
2
+ export declare type UsageHandler = (input: Array<{
3
+ type: string;
4
+ field?: string;
5
+ argument?: string;
6
+ }>) => Promise<boolean[]>;
7
+ export interface ConsiderUsageConfig {
8
+ /**
9
+ * Checks if it's safe to introduce a breaking change on a field
10
+ *
11
+ * Because the function is async and resolves to a boolean value
12
+ * you can add pretty much anything here, many different conditions or
13
+ * even any source of data.
14
+ *
15
+ * In the CLI we use a GraphQL endpoint with a query
16
+ * that checks the usage and returns stats like:
17
+ * min/max count and min/max precentage
18
+ * So we know when to allow for a breaking change.
19
+ *
20
+ * Because it returns a boolean,
21
+ * we can't attach any data or even customize a message of an api change.
22
+ * This is the first iteration, we're going to improve it soon.
23
+ *
24
+ * true - NON_BREAKING
25
+ * false - BREAKING
26
+ */
27
+ checkUsage?: UsageHandler;
28
+ }
29
+ export declare const considerUsage: Rule<ConsiderUsageConfig>;
@@ -1,3 +1,5 @@
1
1
  export * from './dangerous-breaking';
2
2
  export * from './suppress-removal-of-deprecated-field';
3
3
  export * from './ignore-description-changes';
4
+ export * from './consider-usage';
5
+ export * from './safe-unreachable';
@@ -0,0 +1,2 @@
1
+ import { Rule } from './types';
2
+ export declare const safeUnreachable: Rule;
@@ -1,7 +1,8 @@
1
1
  import { GraphQLSchema } from 'graphql';
2
2
  import { Change } from '../changes/change';
3
- export declare type Rule = (input: {
3
+ export declare type Rule<TConfig = any> = (input: {
4
4
  changes: Change[];
5
5
  oldSchema: GraphQLSchema;
6
6
  newSchema: GraphQLSchema;
7
- }) => Change[];
7
+ config: TConfig;
8
+ }) => Change[] | Promise<Change[]>;
package/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { diff, DiffRule, Rule, CompletionArgs, CompletionHandler } from './diff';
1
+ export { diff, DiffRule, Rule, CompletionArgs, CompletionHandler, UsageHandler, } from './diff';
2
2
  export { validate, InvalidDocument } from './validate';
3
3
  export { similar, SimilarMap } from './similar';
4
4
  export * from './coverage';
@@ -2,9 +2,12 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
+ function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
6
+
7
+ const tslib = require('tslib');
5
8
  const graphql = require('graphql');
9
+ const inspect = _interopDefault(require('object-inspect'));
6
10
  const dependencyGraph = require('dependency-graph');
7
- require('object-inspect');
8
11
 
9
12
  function keyMap(list, keyFn) {
10
13
  return list.reduce((map, item) => {
@@ -17,7 +20,21 @@ function isEqual(a, b) {
17
20
  if (a.length !== b.length)
18
21
  return false;
19
22
  for (var index = 0; index < a.length; index++) {
20
- if (a[index] !== b[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])) {
21
38
  return false;
22
39
  }
23
40
  }
@@ -32,7 +49,7 @@ function isVoid(a) {
32
49
  return typeof a === 'undefined' || a === null;
33
50
  }
34
51
  function diffArrays(a, b) {
35
- return a.filter((c) => !b.some((d) => d === c));
52
+ return a.filter((c) => !b.some((d) => isEqual(d, c)));
36
53
  }
37
54
  function compareLists(oldList, newList, callbacks) {
38
55
  const oldMap = keyMap(oldList, ({ name }) => name);
@@ -75,6 +92,20 @@ function compareLists(oldList, newList, callbacks) {
75
92
  };
76
93
  }
77
94
 
95
+ function isDeprecated(fieldOrEnumValue) {
96
+ var _a, _b;
97
+ if ('isDeprecated' in fieldOrEnumValue) {
98
+ return fieldOrEnumValue['isDeprecated'];
99
+ }
100
+ if (fieldOrEnumValue.deprecationReason != null) {
101
+ return true;
102
+ }
103
+ if ((_b = (_a = fieldOrEnumValue.astNode) === null || _a === void 0 ? void 0 : _a.directives) === null || _b === void 0 ? void 0 : _b.some((directive) => directive.name.value === 'deprecated')) {
104
+ return true;
105
+ }
106
+ return false;
107
+ }
108
+
78
109
  function safeChangeForField(oldType, newType) {
79
110
  if (!graphql.isWrappingType(oldType) && !graphql.isWrappingType(newType)) {
80
111
  return oldType.toString() === newType.toString();
@@ -98,7 +129,7 @@ function safeChangeForInputValue(oldType, newType) {
98
129
  return safeChangeForInputValue(oldType.ofType, newType.ofType);
99
130
  }
100
131
  if (graphql.isNonNullType(oldType)) {
101
- const ofType = graphql.isNonNullType(newType) ? newType : newType;
132
+ const ofType = graphql.isNonNullType(newType) ? newType.ofType : newType;
102
133
  return safeChangeForInputValue(oldType.ofType, ofType);
103
134
  }
104
135
  return false;
@@ -117,7 +148,7 @@ function getTypePrefix(type) {
117
148
  [graphql.Kind.ENUM_TYPE_DEFINITION]: 'enum',
118
149
  [graphql.Kind.INPUT_OBJECT_TYPE_DEFINITION]: 'input',
119
150
  };
120
- return kindsMap[kind];
151
+ return kindsMap[kind.toString()];
121
152
  }
122
153
  function isPrimitive(type) {
123
154
  return (['String', 'Int', 'Float', 'Boolean', 'ID'].indexOf(typeof type === 'string' ? type : type.name) !== -1);
@@ -152,7 +183,7 @@ function findDeprecatedUsages(schema, ast) {
152
183
  },
153
184
  Field(node) {
154
185
  const fieldDef = typeInfo.getFieldDef();
155
- if (fieldDef && fieldDef.isDeprecated) {
186
+ if (fieldDef && isDeprecated(fieldDef)) {
156
187
  const parentType = typeInfo.getParentType();
157
188
  if (parentType) {
158
189
  const reason = fieldDef.deprecationReason;
@@ -162,7 +193,7 @@ function findDeprecatedUsages(schema, ast) {
162
193
  },
163
194
  EnumValue(node) {
164
195
  const enumVal = typeInfo.getEnumValue();
165
- if (enumVal && enumVal.isDeprecated) {
196
+ if (enumVal && isDeprecated(enumVal)) {
166
197
  const type = graphql.getNamedType(typeInfo.getInputType());
167
198
  if (type) {
168
199
  const reason = enumVal.deprecationReason;
@@ -187,6 +218,75 @@ function removeDirectives(node, directiveNames) {
187
218
  }
188
219
  return node;
189
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
+ }
190
290
 
191
291
  (function (ChangeType) {
192
292
  // Argument
@@ -473,7 +573,7 @@ function enumValueRemoved(oldEnum, value) {
473
573
  reason: `Removing an enum value will cause existing queries that use this enum value to error.`,
474
574
  },
475
575
  type: exports.ChangeType.EnumValueRemoved,
476
- message: `Enum value '${value.name}' ${value.isDeprecated ? '(deprecated) ' : ''}was removed from enum '${oldEnum.name}'`,
576
+ message: `Enum value '${value.name}' ${isDeprecated(value) ? '(deprecated) ' : ''}was removed from enum '${oldEnum.name}'`,
477
577
  path: [oldEnum.name, value.name].join('.'),
478
578
  };
479
579
  }
@@ -556,7 +656,7 @@ function changesInEnum(oldEnum, newEnum, addChange) {
556
656
  addChange(enumValueDeprecationReasonChanged(newEnum, oldValue, newValue));
557
657
  }
558
658
  }
559
- }
659
+ },
560
660
  });
561
661
  }
562
662
 
@@ -596,6 +696,70 @@ function changesInUnion(oldUnion, newUnion, addChange) {
596
696
  });
597
697
  }
598
698
 
699
+ function compareTwoStrings(str1, str2) {
700
+ if (!str1.length && !str2.length)
701
+ return 1;
702
+ if (!str1.length || !str2.length)
703
+ return 0;
704
+ if (str1.toUpperCase() === str2.toUpperCase())
705
+ return 1;
706
+ if (str1.length === 1 && str2.length === 1)
707
+ return 0;
708
+ const pairs1 = wordLetterPairs(str1);
709
+ const pairs2 = wordLetterPairs(str2);
710
+ const union = pairs1.length + pairs2.length;
711
+ let intersection = 0;
712
+ pairs1.forEach((pair1) => {
713
+ for (let i = 0, pair2; (pair2 = pairs2[i]); i++) {
714
+ if (pair1 !== pair2)
715
+ continue;
716
+ intersection++;
717
+ pairs2.splice(i, 1);
718
+ break;
719
+ }
720
+ });
721
+ return (intersection * 2) / union;
722
+ }
723
+ function findBestMatch(mainString, targetStrings) {
724
+ if (!areArgsValid(mainString, targetStrings))
725
+ throw new Error('Bad arguments: First argument should be a string, second should be an array of strings');
726
+ const ratings = targetStrings.map((target) => ({
727
+ target,
728
+ rating: compareTwoStrings(mainString, target.value),
729
+ }));
730
+ const bestMatch = Array.from(ratings).sort((a, b) => b.rating - a.rating)[0];
731
+ return { ratings, bestMatch };
732
+ }
733
+ function flattenDeep(arr) {
734
+ return Array.isArray(arr)
735
+ ? arr.reduce((a, b) => a.concat(flattenDeep(b)), [])
736
+ : [arr];
737
+ }
738
+ function areArgsValid(mainString, targetStrings) {
739
+ if (typeof mainString !== 'string')
740
+ return false;
741
+ if (!Array.isArray(targetStrings))
742
+ return false;
743
+ if (!targetStrings.length)
744
+ return false;
745
+ if (targetStrings.find((s) => typeof s.value !== 'string'))
746
+ return false;
747
+ return true;
748
+ }
749
+ function letterPairs(str) {
750
+ const pairs = [];
751
+ for (let i = 0, max = str.length - 1; i < max; i++)
752
+ pairs[i] = str.substring(i, i + 2);
753
+ return pairs;
754
+ }
755
+ function wordLetterPairs(str) {
756
+ const pairs = str.toUpperCase().split(' ').map(letterPairs);
757
+ return flattenDeep(pairs);
758
+ }
759
+ function safeString(obj) {
760
+ return inspect(obj).replace(/\[Object\: null prototype\] /g, '').replace(/(^')|('$)/g, '');
761
+ }
762
+
599
763
  function inputFieldRemoved(input, field) {
600
764
  return {
601
765
  criticality: {
@@ -603,7 +767,7 @@ function inputFieldRemoved(input, field) {
603
767
  reason: 'Removing an input field will cause existing queries that use this input field to error.',
604
768
  },
605
769
  type: exports.ChangeType.InputFieldRemoved,
606
- message: `Input field '${field.name}' was removed from input object type '${input.name}'`,
770
+ message: `Input field '${field.name}' ${isDeprecated(field) ? '(deprecated) ' : ''}was removed from input object type '${input.name}'`,
607
771
  path: [input.name, field.name].join('.'),
608
772
  };
609
773
  }
@@ -656,10 +820,10 @@ function inputFieldDefaultValueChanged(input, oldField, newField) {
656
820
  return {
657
821
  criticality: {
658
822
  level: exports.CriticalityLevel.Dangerous,
659
- reason: 'Changing the default value for an argument may change the runtime behaviour of a field if it was never provided.',
823
+ reason: 'Changing the default value for an argument may change the runtime behavior of a field if it was never provided.',
660
824
  },
661
825
  type: exports.ChangeType.InputFieldDefaultValueChanged,
662
- message: `Input field '${input.name}.${oldField.name}' default value changed from '${oldField.defaultValue}' to '${newField.defaultValue}'`,
826
+ message: `Input field '${input.name}.${oldField.name}' default value changed from '${safeString(oldField.defaultValue)}' to '${safeString(newField.defaultValue)}'`,
663
827
  path: [input.name, oldField.name].join('.'),
664
828
  };
665
829
  }
@@ -757,7 +921,7 @@ function fieldRemoved(type, field) {
757
921
  : `Removing a field is a breaking change. It is preferable to deprecate the field before removing it.`,
758
922
  },
759
923
  type: exports.ChangeType.FieldRemoved,
760
- message: `Field '${field.name}' ${field.isDeprecated ? '(deprecated) ' : ''}was removed from ${entity} '${type.name}'`,
924
+ message: `Field '${field.name}' ${isDeprecated(field) ? '(deprecated) ' : ''}was removed from ${entity} '${type.name}'`,
761
925
  path: [type.name, field.name].join('.'),
762
926
  };
763
927
  }
@@ -865,8 +1029,10 @@ function fieldTypeChanged(type, oldField, newField) {
865
1029
  };
866
1030
  }
867
1031
  function fieldArgumentAdded(type, field, arg) {
1032
+ const isBreaking = graphql.isNonNullType(arg.type) && typeof arg.defaultValue === 'undefined';
1033
+ const defaultValueMsg = typeof arg.defaultValue !== 'undefined' ? ' (with default value) ' : ' ';
868
1034
  return {
869
- criticality: graphql.isNonNullType(arg.type)
1035
+ criticality: isBreaking
870
1036
  ? {
871
1037
  level: exports.CriticalityLevel.Breaking,
872
1038
  reason: `Adding a required argument to an existing field is a breaking change because it will cause existing uses of this field to error.`,
@@ -876,7 +1042,7 @@ function fieldArgumentAdded(type, field, arg) {
876
1042
  reason: `Adding a new argument to an existing field may involve a change in resolve function logic that potentially may cause some side effects.`,
877
1043
  },
878
1044
  type: exports.ChangeType.FieldArgumentAdded,
879
- message: `Argument '${arg.name}: ${arg.type}' added to field '${type.name}.${field.name}'`,
1045
+ message: `Argument '${arg.name}: ${arg.type}'${defaultValueMsg}added to field '${type.name}.${field.name}'`,
880
1046
  path: [type.name, field.name, arg.name].join('.'),
881
1047
  };
882
1048
  }
@@ -910,8 +1076,8 @@ function fieldArgumentDefaultChanged(type, field, oldArg, newArg) {
910
1076
  },
911
1077
  type: exports.ChangeType.FieldArgumentDefaultChanged,
912
1078
  message: typeof oldArg.defaultValue === 'undefined'
913
- ? `Default value '${newArg.defaultValue}' was added to argument '${newArg.name}' on field '${type.name}.${field.name}'`
914
- : `Default value for argument '${newArg.name}' on field '${type.name}.${field.name}' changed from '${oldArg.defaultValue}' to '${newArg.defaultValue}'`,
1079
+ ? `Default value '${safeString(newArg.defaultValue)}' was added to argument '${newArg.name}' on field '${type.name}.${field.name}'`
1080
+ : `Default value for argument '${newArg.name}' on field '${type.name}.${field.name}' changed from '${safeString(oldArg.defaultValue)}' to '${safeString(newArg.defaultValue)}'`,
915
1081
  path: [type.name, field.name, oldArg.name].join('.'),
916
1082
  };
917
1083
  }
@@ -966,8 +1132,8 @@ function changesInField(type, oldField, newField, addChange) {
966
1132
  addChange(fieldDescriptionChanged(type, oldField, newField));
967
1133
  }
968
1134
  }
969
- if (isNotEqual(oldField.isDeprecated, newField.isDeprecated)) {
970
- if (newField.isDeprecated) {
1135
+ if (isNotEqual(isDeprecated(oldField), isDeprecated(newField))) {
1136
+ if (isDeprecated(newField)) {
971
1137
  addChange(fieldDeprecationAdded(type, newField));
972
1138
  }
973
1139
  else {
@@ -997,7 +1163,7 @@ function changesInField(type, oldField, newField, addChange) {
997
1163
  },
998
1164
  onMutual(arg) {
999
1165
  changesInArgument(type, oldField, arg.oldVersion, arg.newVersion, addChange);
1000
- }
1166
+ },
1001
1167
  });
1002
1168
  }
1003
1169
 
@@ -1108,17 +1274,21 @@ function diffSchema(oldSchema, newSchema) {
1108
1274
  return changes;
1109
1275
  }
1110
1276
  function changesInSchema(oldSchema, newSchema, addChange) {
1277
+ var _a, _b, _c, _d, _e, _f;
1278
+ const defaultNames = {
1279
+ query: 'Query',
1280
+ mutation: 'Mutation',
1281
+ subscription: 'Subscription',
1282
+ };
1111
1283
  const oldRoot = {
1112
- query: (oldSchema.getQueryType() || {}).name,
1113
- mutation: (oldSchema.getMutationType() || {}).name,
1114
- subscription: (oldSchema.getSubscriptionType() || {})
1115
- .name,
1284
+ query: (_a = (oldSchema.getQueryType() || {}).name) !== null && _a !== void 0 ? _a : defaultNames.query,
1285
+ mutation: (_b = (oldSchema.getMutationType() || {}).name) !== null && _b !== void 0 ? _b : defaultNames.mutation,
1286
+ subscription: (_c = (oldSchema.getSubscriptionType() || {}).name) !== null && _c !== void 0 ? _c : defaultNames.subscription,
1116
1287
  };
1117
1288
  const newRoot = {
1118
- query: (newSchema.getQueryType() || {}).name,
1119
- mutation: (newSchema.getMutationType() || {}).name,
1120
- subscription: (newSchema.getSubscriptionType() || {})
1121
- .name,
1289
+ query: (_d = (newSchema.getQueryType() || {}).name) !== null && _d !== void 0 ? _d : defaultNames.query,
1290
+ mutation: (_e = (newSchema.getMutationType() || {}).name) !== null && _e !== void 0 ? _e : defaultNames.mutation,
1291
+ subscription: (_f = (newSchema.getSubscriptionType() || {}).name) !== null && _f !== void 0 ? _f : defaultNames.subscription,
1122
1292
  };
1123
1293
  if (isNotEqual(oldRoot.query, newRoot.query)) {
1124
1294
  addChange(schemaQueryTypeChanged(oldSchema, newSchema));
@@ -1185,7 +1355,7 @@ const suppressRemovalOfDeprecatedField = ({ changes, oldSchema, }) => {
1185
1355
  const type = oldSchema.getType(typeName);
1186
1356
  if (graphql.isObjectType(type) || graphql.isInterfaceType(type)) {
1187
1357
  const field = type.getFields()[fieldName];
1188
- if (field.isDeprecated) {
1358
+ if (isDeprecated(field)) {
1189
1359
  return Object.assign(Object.assign({}, change), { criticality: Object.assign(Object.assign({}, change.criticality), { level: exports.CriticalityLevel.Dangerous }) });
1190
1360
  }
1191
1361
  }
@@ -1197,7 +1367,19 @@ const suppressRemovalOfDeprecatedField = ({ changes, oldSchema, }) => {
1197
1367
  const type = oldSchema.getType(enumName);
1198
1368
  if (graphql.isEnumType(type)) {
1199
1369
  const item = type.getValue(enumItem);
1200
- if (item && item.isDeprecated) {
1370
+ if (item && isDeprecated(item)) {
1371
+ return Object.assign(Object.assign({}, change), { criticality: Object.assign(Object.assign({}, change.criticality), { level: exports.CriticalityLevel.Dangerous }) });
1372
+ }
1373
+ }
1374
+ }
1375
+ if (change.type === exports.ChangeType.InputFieldRemoved &&
1376
+ change.criticality.level === exports.CriticalityLevel.Breaking &&
1377
+ change.path) {
1378
+ const [inputName, inputItem] = parsePath(change.path);
1379
+ const type = oldSchema.getType(inputName);
1380
+ if (graphql.isInputObjectType(type)) {
1381
+ const item = type.getFields()[inputItem];
1382
+ if (item && isDeprecated(item)) {
1201
1383
  return Object.assign(Object.assign({}, change), { criticality: Object.assign(Object.assign({}, change.criticality), { level: exports.CriticalityLevel.Dangerous }) });
1202
1384
  }
1203
1385
  }
@@ -1223,21 +1405,73 @@ const ignoreDescriptionChanges = ({ changes }) => {
1223
1405
  return changes.filter((change) => descriptionChangeTypes.indexOf(change.type) === -1);
1224
1406
  };
1225
1407
 
1408
+ const considerUsage = ({ changes, config, }) => tslib.__awaiter(void 0, void 0, void 0, function* () {
1409
+ if (!config) {
1410
+ throw new Error(`considerUsage rule is missing config`);
1411
+ }
1412
+ const collectedBreakingField = [];
1413
+ changes.forEach((change) => {
1414
+ if (change.criticality.level === exports.CriticalityLevel.Breaking && change.path) {
1415
+ const [typeName, fieldName, argumentName] = parsePath(change.path);
1416
+ collectedBreakingField.push({
1417
+ type: typeName,
1418
+ field: fieldName,
1419
+ argument: argumentName,
1420
+ });
1421
+ }
1422
+ });
1423
+ // True if safe to break, false otherwise
1424
+ const usageList = yield config.checkUsage(collectedBreakingField);
1425
+ // turns an array of booleans into an array of `Type.Field` strings
1426
+ // includes only those that are safe to break the api
1427
+ const suppressedPaths = collectedBreakingField
1428
+ .filter((_, i) => usageList[i] === true)
1429
+ .map(({ type, field, argument }) => [type, field, argument].filter(Boolean).join('.'));
1430
+ return changes.map((change) => {
1431
+ // Turns those "safe to break" changes into "dangerous"
1432
+ if (change.criticality.level === exports.CriticalityLevel.Breaking &&
1433
+ change.path &&
1434
+ suppressedPaths.some((p) => change.path.startsWith(p))) {
1435
+ return Object.assign(Object.assign({}, change), { criticality: Object.assign(Object.assign({}, change.criticality), { level: exports.CriticalityLevel.Dangerous }), message: `${change.message} (non-breaking based on usage)` });
1436
+ }
1437
+ return change;
1438
+ });
1439
+ });
1440
+
1441
+ const safeUnreachable = ({ changes, oldSchema }) => {
1442
+ const reachable = getReachableTypes(oldSchema);
1443
+ return changes.map((change) => {
1444
+ if (change.criticality.level === exports.CriticalityLevel.Breaking && change.path) {
1445
+ const [typeName] = parsePath(change.path);
1446
+ if (!reachable.has(typeName)) {
1447
+ return Object.assign(Object.assign({}, change), { criticality: Object.assign(Object.assign({}, change.criticality), { level: exports.CriticalityLevel.NonBreaking }), message: 'Unreachable from root' });
1448
+ }
1449
+ }
1450
+ return change;
1451
+ });
1452
+ };
1453
+
1226
1454
  const rules = /*#__PURE__*/Object.freeze({
1227
1455
  __proto__: null,
1228
1456
  dangerousBreaking: dangerousBreaking,
1229
1457
  suppressRemovalOfDeprecatedField: suppressRemovalOfDeprecatedField,
1230
- ignoreDescriptionChanges: ignoreDescriptionChanges
1458
+ ignoreDescriptionChanges: ignoreDescriptionChanges,
1459
+ considerUsage: considerUsage,
1460
+ safeUnreachable: safeUnreachable
1231
1461
  });
1232
1462
 
1233
1463
  const DiffRule = rules;
1234
- function diff(oldSchema, newSchema, rules = []) {
1464
+ function diff(oldSchema, newSchema, rules = [], config) {
1235
1465
  const changes = diffSchema(oldSchema, newSchema);
1236
- return rules.reduce((prev, rule) => rule({
1237
- changes: prev,
1238
- oldSchema,
1239
- newSchema,
1240
- }), changes);
1466
+ return rules.reduce((prev, rule) => tslib.__awaiter(this, void 0, void 0, function* () {
1467
+ const prevChanges = yield prev;
1468
+ return rule({
1469
+ changes: prevChanges,
1470
+ oldSchema,
1471
+ newSchema,
1472
+ config,
1473
+ });
1474
+ }), Promise.resolve(changes));
1241
1475
  }
1242
1476
 
1243
1477
  function readDocument(source) {
@@ -1401,7 +1635,7 @@ function validate(schema, sources, options) {
1401
1635
  .filter((doc) => doc.hasOperations)
1402
1636
  .forEach((doc) => {
1403
1637
  const docWithOperations = {
1404
- kind: 'Document',
1638
+ kind: graphql.Kind.DOCUMENT,
1405
1639
  definitions: doc.operations.map((d) => d.node),
1406
1640
  };
1407
1641
  const extractedFragments = (extractFragments(graphql.print(docWithOperations)) || [])
@@ -1412,7 +1646,7 @@ function validate(schema, sources, options) {
1412
1646
  // remove duplicates
1413
1647
  .filter((def, i, all) => all.findIndex((item) => item.name.value === def.name.value) === i);
1414
1648
  const merged = {
1415
- kind: 'Document',
1649
+ kind: graphql.Kind.DOCUMENT,
1416
1650
  definitions: [...docWithOperations.definitions, ...extractedFragments],
1417
1651
  };
1418
1652
  let transformedSchema = config.apollo
@@ -1475,67 +1709,6 @@ function sumLengths(...arrays) {
1475
1709
  return arrays.reduce((sum, { length }) => sum + length, 0);
1476
1710
  }
1477
1711
 
1478
- function compareTwoStrings(str1, str2) {
1479
- if (!str1.length && !str2.length)
1480
- return 1;
1481
- if (!str1.length || !str2.length)
1482
- return 0;
1483
- if (str1.toUpperCase() === str2.toUpperCase())
1484
- return 1;
1485
- if (str1.length === 1 && str2.length === 1)
1486
- return 0;
1487
- const pairs1 = wordLetterPairs(str1);
1488
- const pairs2 = wordLetterPairs(str2);
1489
- const union = pairs1.length + pairs2.length;
1490
- let intersection = 0;
1491
- pairs1.forEach((pair1) => {
1492
- for (let i = 0, pair2; (pair2 = pairs2[i]); i++) {
1493
- if (pair1 !== pair2)
1494
- continue;
1495
- intersection++;
1496
- pairs2.splice(i, 1);
1497
- break;
1498
- }
1499
- });
1500
- return (intersection * 2) / union;
1501
- }
1502
- function findBestMatch(mainString, targetStrings) {
1503
- if (!areArgsValid(mainString, targetStrings))
1504
- throw new Error('Bad arguments: First argument should be a string, second should be an array of strings');
1505
- const ratings = targetStrings.map((target) => ({
1506
- target,
1507
- rating: compareTwoStrings(mainString, target.value),
1508
- }));
1509
- const bestMatch = Array.from(ratings).sort((a, b) => b.rating - a.rating)[0];
1510
- return { ratings, bestMatch };
1511
- }
1512
- function flattenDeep(arr) {
1513
- return Array.isArray(arr)
1514
- ? arr.reduce((a, b) => a.concat(flattenDeep(b)), [])
1515
- : [arr];
1516
- }
1517
- function areArgsValid(mainString, targetStrings) {
1518
- if (typeof mainString !== 'string')
1519
- return false;
1520
- if (!Array.isArray(targetStrings))
1521
- return false;
1522
- if (!targetStrings.length)
1523
- return false;
1524
- if (targetStrings.find((s) => typeof s.value !== 'string'))
1525
- return false;
1526
- return true;
1527
- }
1528
- function letterPairs(str) {
1529
- const pairs = [];
1530
- for (let i = 0, max = str.length - 1; i < max; i++)
1531
- pairs[i] = str.substring(i, i + 2);
1532
- return pairs;
1533
- }
1534
- function wordLetterPairs(str) {
1535
- const pairs = str.toUpperCase().split(' ').map(letterPairs);
1536
- return flattenDeep(pairs);
1537
- }
1538
-
1539
1712
  function similar(schema, typeName, threshold = 0.4) {
1540
1713
  const typeMap = schema.getTypeMap();
1541
1714
  const targets = Object.keys(schema.getTypeMap())
@@ -1680,4 +1853,3 @@ exports.diff = diff;
1680
1853
  exports.getTypePrefix = getTypePrefix;
1681
1854
  exports.similar = similar;
1682
1855
  exports.validate = validate;
1683
- //# sourceMappingURL=index.cjs.js.map