@graphql-inspector/core 2.9.0 → 3.1.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/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,4 @@
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';
@@ -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) => {
@@ -75,6 +78,20 @@ function compareLists(oldList, newList, callbacks) {
75
78
  };
76
79
  }
77
80
 
81
+ function isDeprecated(fieldOrEnumValue) {
82
+ var _a, _b;
83
+ if ('isDeprecated' in fieldOrEnumValue) {
84
+ return fieldOrEnumValue['isDeprecated'];
85
+ }
86
+ if (fieldOrEnumValue.deprecationReason != null) {
87
+ return true;
88
+ }
89
+ 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")) {
90
+ return true;
91
+ }
92
+ return false;
93
+ }
94
+
78
95
  function safeChangeForField(oldType, newType) {
79
96
  if (!graphql.isWrappingType(oldType) && !graphql.isWrappingType(newType)) {
80
97
  return oldType.toString() === newType.toString();
@@ -117,7 +134,7 @@ function getTypePrefix(type) {
117
134
  [graphql.Kind.ENUM_TYPE_DEFINITION]: 'enum',
118
135
  [graphql.Kind.INPUT_OBJECT_TYPE_DEFINITION]: 'input',
119
136
  };
120
- return kindsMap[kind];
137
+ return kindsMap[kind.toString()];
121
138
  }
122
139
  function isPrimitive(type) {
123
140
  return (['String', 'Int', 'Float', 'Boolean', 'ID'].indexOf(typeof type === 'string' ? type : type.name) !== -1);
@@ -152,7 +169,7 @@ function findDeprecatedUsages(schema, ast) {
152
169
  },
153
170
  Field(node) {
154
171
  const fieldDef = typeInfo.getFieldDef();
155
- if (fieldDef && fieldDef.isDeprecated) {
172
+ if (fieldDef && isDeprecated(fieldDef)) {
156
173
  const parentType = typeInfo.getParentType();
157
174
  if (parentType) {
158
175
  const reason = fieldDef.deprecationReason;
@@ -162,7 +179,7 @@ function findDeprecatedUsages(schema, ast) {
162
179
  },
163
180
  EnumValue(node) {
164
181
  const enumVal = typeInfo.getEnumValue();
165
- if (enumVal && enumVal.isDeprecated) {
182
+ if (enumVal && isDeprecated(enumVal)) {
166
183
  const type = graphql.getNamedType(typeInfo.getInputType());
167
184
  if (type) {
168
185
  const reason = enumVal.deprecationReason;
@@ -473,7 +490,7 @@ function enumValueRemoved(oldEnum, value) {
473
490
  reason: `Removing an enum value will cause existing queries that use this enum value to error.`,
474
491
  },
475
492
  type: exports.ChangeType.EnumValueRemoved,
476
- message: `Enum value '${value.name}' ${value.isDeprecated ? '(deprecated) ' : ''}was removed from enum '${oldEnum.name}'`,
493
+ message: `Enum value '${value.name}' ${isDeprecated(value) ? '(deprecated) ' : ''}was removed from enum '${oldEnum.name}'`,
477
494
  path: [oldEnum.name, value.name].join('.'),
478
495
  };
479
496
  }
@@ -757,7 +774,7 @@ function fieldRemoved(type, field) {
757
774
  : `Removing a field is a breaking change. It is preferable to deprecate the field before removing it.`,
758
775
  },
759
776
  type: exports.ChangeType.FieldRemoved,
760
- message: `Field '${field.name}' ${field.isDeprecated ? '(deprecated) ' : ''}was removed from ${entity} '${type.name}'`,
777
+ message: `Field '${field.name}' ${isDeprecated(field) ? '(deprecated) ' : ''}was removed from ${entity} '${type.name}'`,
761
778
  path: [type.name, field.name].join('.'),
762
779
  };
763
780
  }
@@ -892,6 +909,73 @@ function fieldArgumentRemoved(type, field, arg) {
892
909
  };
893
910
  }
894
911
 
912
+ function compareTwoStrings(str1, str2) {
913
+ if (!str1.length && !str2.length)
914
+ return 1;
915
+ if (!str1.length || !str2.length)
916
+ return 0;
917
+ if (str1.toUpperCase() === str2.toUpperCase())
918
+ return 1;
919
+ if (str1.length === 1 && str2.length === 1)
920
+ return 0;
921
+ const pairs1 = wordLetterPairs(str1);
922
+ const pairs2 = wordLetterPairs(str2);
923
+ const union = pairs1.length + pairs2.length;
924
+ let intersection = 0;
925
+ pairs1.forEach((pair1) => {
926
+ for (let i = 0, pair2; (pair2 = pairs2[i]); i++) {
927
+ if (pair1 !== pair2)
928
+ continue;
929
+ intersection++;
930
+ pairs2.splice(i, 1);
931
+ break;
932
+ }
933
+ });
934
+ return (intersection * 2) / union;
935
+ }
936
+ function findBestMatch(mainString, targetStrings) {
937
+ if (!areArgsValid(mainString, targetStrings))
938
+ throw new Error('Bad arguments: First argument should be a string, second should be an array of strings');
939
+ const ratings = targetStrings.map((target) => ({
940
+ target,
941
+ rating: compareTwoStrings(mainString, target.value),
942
+ }));
943
+ const bestMatch = Array.from(ratings).sort((a, b) => b.rating - a.rating)[0];
944
+ return { ratings, bestMatch };
945
+ }
946
+ function flattenDeep(arr) {
947
+ return Array.isArray(arr)
948
+ ? arr.reduce((a, b) => a.concat(flattenDeep(b)), [])
949
+ : [arr];
950
+ }
951
+ function areArgsValid(mainString, targetStrings) {
952
+ if (typeof mainString !== 'string')
953
+ return false;
954
+ if (!Array.isArray(targetStrings))
955
+ return false;
956
+ if (!targetStrings.length)
957
+ return false;
958
+ if (targetStrings.find((s) => typeof s.value !== 'string'))
959
+ return false;
960
+ return true;
961
+ }
962
+ function letterPairs(str) {
963
+ const pairs = [];
964
+ for (let i = 0, max = str.length - 1; i < max; i++)
965
+ pairs[i] = str.substring(i, i + 2);
966
+ return pairs;
967
+ }
968
+ function wordLetterPairs(str) {
969
+ const pairs = str.toUpperCase().split(' ').map(letterPairs);
970
+ return flattenDeep(pairs);
971
+ }
972
+ function safeString(obj) {
973
+ if (obj != null && typeof obj.toString === 'function') {
974
+ return `${obj}`;
975
+ }
976
+ return inspect(obj);
977
+ }
978
+
895
979
  function fieldArgumentDescriptionChanged(type, field, oldArg, newArg) {
896
980
  return {
897
981
  criticality: {
@@ -910,8 +994,8 @@ function fieldArgumentDefaultChanged(type, field, oldArg, newArg) {
910
994
  },
911
995
  type: exports.ChangeType.FieldArgumentDefaultChanged,
912
996
  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}'`,
997
+ ? `Default value '${safeString(newArg.defaultValue)}' was added to argument '${newArg.name}' on field '${type.name}.${field.name}'`
998
+ : `Default value for argument '${newArg.name}' on field '${type.name}.${field.name}' changed from '${safeString(oldArg.defaultValue)}' to '${safeString(newArg.defaultValue)}'`,
915
999
  path: [type.name, field.name, oldArg.name].join('.'),
916
1000
  };
917
1001
  }
@@ -966,8 +1050,8 @@ function changesInField(type, oldField, newField, addChange) {
966
1050
  addChange(fieldDescriptionChanged(type, oldField, newField));
967
1051
  }
968
1052
  }
969
- if (isNotEqual(oldField.isDeprecated, newField.isDeprecated)) {
970
- if (newField.isDeprecated) {
1053
+ if (isNotEqual(isDeprecated(oldField), isDeprecated(newField))) {
1054
+ if (isDeprecated(newField)) {
971
1055
  addChange(fieldDeprecationAdded(type, newField));
972
1056
  }
973
1057
  else {
@@ -1108,17 +1192,21 @@ function diffSchema(oldSchema, newSchema) {
1108
1192
  return changes;
1109
1193
  }
1110
1194
  function changesInSchema(oldSchema, newSchema, addChange) {
1195
+ var _a, _b, _c, _d, _e, _f;
1196
+ const defaultNames = {
1197
+ query: 'Query',
1198
+ mutation: 'Mutation',
1199
+ subscription: 'Subscription',
1200
+ };
1111
1201
  const oldRoot = {
1112
- query: (oldSchema.getQueryType() || {}).name,
1113
- mutation: (oldSchema.getMutationType() || {}).name,
1114
- subscription: (oldSchema.getSubscriptionType() || {})
1115
- .name,
1202
+ query: (_a = (oldSchema.getQueryType() || {}).name) !== null && _a !== void 0 ? _a : defaultNames.query,
1203
+ mutation: (_b = (oldSchema.getMutationType() || {}).name) !== null && _b !== void 0 ? _b : defaultNames.mutation,
1204
+ subscription: (_c = (oldSchema.getSubscriptionType() || {}).name) !== null && _c !== void 0 ? _c : defaultNames.subscription,
1116
1205
  };
1117
1206
  const newRoot = {
1118
- query: (newSchema.getQueryType() || {}).name,
1119
- mutation: (newSchema.getMutationType() || {}).name,
1120
- subscription: (newSchema.getSubscriptionType() || {})
1121
- .name,
1207
+ query: (_d = (newSchema.getQueryType() || {}).name) !== null && _d !== void 0 ? _d : defaultNames.query,
1208
+ mutation: (_e = (newSchema.getMutationType() || {}).name) !== null && _e !== void 0 ? _e : defaultNames.mutation,
1209
+ subscription: (_f = (newSchema.getSubscriptionType() || {}).name) !== null && _f !== void 0 ? _f : defaultNames.subscription,
1122
1210
  };
1123
1211
  if (isNotEqual(oldRoot.query, newRoot.query)) {
1124
1212
  addChange(schemaQueryTypeChanged(oldSchema, newSchema));
@@ -1185,7 +1273,7 @@ const suppressRemovalOfDeprecatedField = ({ changes, oldSchema, }) => {
1185
1273
  const type = oldSchema.getType(typeName);
1186
1274
  if (graphql.isObjectType(type) || graphql.isInterfaceType(type)) {
1187
1275
  const field = type.getFields()[fieldName];
1188
- if (field.isDeprecated) {
1276
+ if (isDeprecated(field)) {
1189
1277
  return Object.assign(Object.assign({}, change), { criticality: Object.assign(Object.assign({}, change.criticality), { level: exports.CriticalityLevel.Dangerous }) });
1190
1278
  }
1191
1279
  }
@@ -1197,7 +1285,7 @@ const suppressRemovalOfDeprecatedField = ({ changes, oldSchema, }) => {
1197
1285
  const type = oldSchema.getType(enumName);
1198
1286
  if (graphql.isEnumType(type)) {
1199
1287
  const item = type.getValue(enumItem);
1200
- if (item && item.isDeprecated) {
1288
+ if (item && isDeprecated(item)) {
1201
1289
  return Object.assign(Object.assign({}, change), { criticality: Object.assign(Object.assign({}, change.criticality), { level: exports.CriticalityLevel.Dangerous }) });
1202
1290
  }
1203
1291
  }
@@ -1223,21 +1311,59 @@ const ignoreDescriptionChanges = ({ changes }) => {
1223
1311
  return changes.filter((change) => descriptionChangeTypes.indexOf(change.type) === -1);
1224
1312
  };
1225
1313
 
1314
+ const considerUsage = ({ changes, config, }) => tslib.__awaiter(void 0, void 0, void 0, function* () {
1315
+ if (!config) {
1316
+ throw new Error(`considerUsage rule is missing config`);
1317
+ }
1318
+ const collectedBreakingField = [];
1319
+ changes.forEach((change) => {
1320
+ if (change.criticality.level === exports.CriticalityLevel.Breaking && change.path) {
1321
+ const [typeName, fieldName, argumentName] = parsePath(change.path);
1322
+ collectedBreakingField.push({
1323
+ type: typeName,
1324
+ field: fieldName,
1325
+ argument: argumentName,
1326
+ });
1327
+ }
1328
+ });
1329
+ // True if safe to break, false otherwise
1330
+ const usageList = yield config.checkUsage(collectedBreakingField);
1331
+ // turns an array of booleans into an array of `Type.Field` strings
1332
+ // includes only those that are safe to break the api
1333
+ const suppressedPaths = collectedBreakingField
1334
+ .filter((_, i) => usageList[i] === true)
1335
+ .map(({ type, field, argument }) => [type, field, argument].filter(Boolean).join('.'));
1336
+ return changes.map((change) => {
1337
+ // Turns those "safe to break" changes into "dangerous"
1338
+ if (change.criticality.level === exports.CriticalityLevel.Breaking &&
1339
+ change.path &&
1340
+ suppressedPaths.some((p) => change.path.startsWith(p))) {
1341
+ 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)` });
1342
+ }
1343
+ return change;
1344
+ });
1345
+ });
1346
+
1226
1347
  const rules = /*#__PURE__*/Object.freeze({
1227
1348
  __proto__: null,
1228
1349
  dangerousBreaking: dangerousBreaking,
1229
1350
  suppressRemovalOfDeprecatedField: suppressRemovalOfDeprecatedField,
1230
- ignoreDescriptionChanges: ignoreDescriptionChanges
1351
+ ignoreDescriptionChanges: ignoreDescriptionChanges,
1352
+ considerUsage: considerUsage
1231
1353
  });
1232
1354
 
1233
1355
  const DiffRule = rules;
1234
- function diff(oldSchema, newSchema, rules = []) {
1356
+ function diff(oldSchema, newSchema, rules = [], config) {
1235
1357
  const changes = diffSchema(oldSchema, newSchema);
1236
- return rules.reduce((prev, rule) => rule({
1237
- changes: prev,
1238
- oldSchema,
1239
- newSchema,
1240
- }), changes);
1358
+ return rules.reduce((prev, rule) => tslib.__awaiter(this, void 0, void 0, function* () {
1359
+ const prevChanges = yield prev;
1360
+ return rule({
1361
+ changes: prevChanges,
1362
+ oldSchema,
1363
+ newSchema,
1364
+ config,
1365
+ });
1366
+ }), Promise.resolve(changes));
1241
1367
  }
1242
1368
 
1243
1369
  function readDocument(source) {
@@ -1401,7 +1527,7 @@ function validate(schema, sources, options) {
1401
1527
  .filter((doc) => doc.hasOperations)
1402
1528
  .forEach((doc) => {
1403
1529
  const docWithOperations = {
1404
- kind: 'Document',
1530
+ kind: graphql.Kind.DOCUMENT,
1405
1531
  definitions: doc.operations.map((d) => d.node),
1406
1532
  };
1407
1533
  const extractedFragments = (extractFragments(graphql.print(docWithOperations)) || [])
@@ -1412,7 +1538,7 @@ function validate(schema, sources, options) {
1412
1538
  // remove duplicates
1413
1539
  .filter((def, i, all) => all.findIndex((item) => item.name.value === def.name.value) === i);
1414
1540
  const merged = {
1415
- kind: 'Document',
1541
+ kind: graphql.Kind.DOCUMENT,
1416
1542
  definitions: [...docWithOperations.definitions, ...extractedFragments],
1417
1543
  };
1418
1544
  let transformedSchema = config.apollo
@@ -1475,67 +1601,6 @@ function sumLengths(...arrays) {
1475
1601
  return arrays.reduce((sum, { length }) => sum + length, 0);
1476
1602
  }
1477
1603
 
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
1604
  function similar(schema, typeName, threshold = 0.4) {
1540
1605
  const typeMap = schema.getTypeMap();
1541
1606
  const targets = Object.keys(schema.getTypeMap())
@@ -1680,4 +1745,3 @@ exports.diff = diff;
1680
1745
  exports.getTypePrefix = getTypePrefix;
1681
1746
  exports.similar = similar;
1682
1747
  exports.validate = validate;
1683
- //# sourceMappingURL=index.cjs.js.map