1/**
2 * Copyright (c) 2016-present, Facebook, Inc.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 *  strict
8 */
9
10import { isScalarType, isObjectType, isInterfaceType, isUnionType, isEnumType, isInputObjectType, isNonNullType, isListType, isNamedType } from '../type/definition';
11
12import { GraphQLDirective } from '../type/directives';
13import { GraphQLSchema } from '../type/schema';
14import keyMap from '../jsutils/keyMap';
15
16export var BreakingChangeType = {
17  FIELD_CHANGED_KIND: 'FIELD_CHANGED_KIND',
18  FIELD_REMOVED: 'FIELD_REMOVED',
19  TYPE_CHANGED_KIND: 'TYPE_CHANGED_KIND',
20  TYPE_REMOVED: 'TYPE_REMOVED',
21  TYPE_REMOVED_FROM_UNION: 'TYPE_REMOVED_FROM_UNION',
22  VALUE_REMOVED_FROM_ENUM: 'VALUE_REMOVED_FROM_ENUM',
23  ARG_REMOVED: 'ARG_REMOVED',
24  ARG_CHANGED_KIND: 'ARG_CHANGED_KIND',
25  NON_NULL_ARG_ADDED: 'NON_NULL_ARG_ADDED',
26  NON_NULL_INPUT_FIELD_ADDED: 'NON_NULL_INPUT_FIELD_ADDED',
27  INTERFACE_REMOVED_FROM_OBJECT: 'INTERFACE_REMOVED_FROM_OBJECT',
28  DIRECTIVE_REMOVED: 'DIRECTIVE_REMOVED',
29  DIRECTIVE_ARG_REMOVED: 'DIRECTIVE_ARG_REMOVED',
30  DIRECTIVE_LOCATION_REMOVED: 'DIRECTIVE_LOCATION_REMOVED',
31  NON_NULL_DIRECTIVE_ARG_ADDED: 'NON_NULL_DIRECTIVE_ARG_ADDED'
32};
33
34export var DangerousChangeType = {
35  ARG_DEFAULT_VALUE_CHANGE: 'ARG_DEFAULT_VALUE_CHANGE',
36  VALUE_ADDED_TO_ENUM: 'VALUE_ADDED_TO_ENUM',
37  INTERFACE_ADDED_TO_OBJECT: 'INTERFACE_ADDED_TO_OBJECT',
38  TYPE_ADDED_TO_UNION: 'TYPE_ADDED_TO_UNION',
39  NULLABLE_INPUT_FIELD_ADDED: 'NULLABLE_INPUT_FIELD_ADDED',
40  NULLABLE_ARG_ADDED: 'NULLABLE_ARG_ADDED'
41};
42
43/**
44 * Given two schemas, returns an Array containing descriptions of all the types
45 * of breaking changes covered by the other functions down below.
46 */
47export function findBreakingChanges(oldSchema, newSchema) {
48  return [].concat(findRemovedTypes(oldSchema, newSchema), findTypesThatChangedKind(oldSchema, newSchema), findFieldsThatChangedTypeOnObjectOrInterfaceTypes(oldSchema, newSchema), findFieldsThatChangedTypeOnInputObjectTypes(oldSchema, newSchema).breakingChanges, findTypesRemovedFromUnions(oldSchema, newSchema), findValuesRemovedFromEnums(oldSchema, newSchema), findArgChanges(oldSchema, newSchema).breakingChanges, findInterfacesRemovedFromObjectTypes(oldSchema, newSchema), findRemovedDirectives(oldSchema, newSchema), findRemovedDirectiveArgs(oldSchema, newSchema), findAddedNonNullDirectiveArgs(oldSchema, newSchema), findRemovedDirectiveLocations(oldSchema, newSchema));
49}
50
51/**
52 * Given two schemas, returns an Array containing descriptions of all the types
53 * of potentially dangerous changes covered by the other functions down below.
54 */
55export function findDangerousChanges(oldSchema, newSchema) {
56  return [].concat(findArgChanges(oldSchema, newSchema).dangerousChanges, findValuesAddedToEnums(oldSchema, newSchema), findInterfacesAddedToObjectTypes(oldSchema, newSchema), findTypesAddedToUnions(oldSchema, newSchema), findFieldsThatChangedTypeOnInputObjectTypes(oldSchema, newSchema).dangerousChanges);
57}
58
59/**
60 * Given two schemas, returns an Array containing descriptions of any breaking
61 * changes in the newSchema related to removing an entire type.
62 */
63export function findRemovedTypes(oldSchema, newSchema) {
64  var oldTypeMap = oldSchema.getTypeMap();
65  var newTypeMap = newSchema.getTypeMap();
66
67  var breakingChanges = [];
68  Object.keys(oldTypeMap).forEach(function (typeName) {
69    if (!newTypeMap[typeName]) {
70      breakingChanges.push({
71        type: BreakingChangeType.TYPE_REMOVED,
72        description: typeName + ' was removed.'
73      });
74    }
75  });
76  return breakingChanges;
77}
78
79/**
80 * Given two schemas, returns an Array containing descriptions of any breaking
81 * changes in the newSchema related to changing the type of a type.
82 */
83export function findTypesThatChangedKind(oldSchema, newSchema) {
84  var oldTypeMap = oldSchema.getTypeMap();
85  var newTypeMap = newSchema.getTypeMap();
86
87  var breakingChanges = [];
88  Object.keys(oldTypeMap).forEach(function (typeName) {
89    if (!newTypeMap[typeName]) {
90      return;
91    }
92    var oldType = oldTypeMap[typeName];
93    var newType = newTypeMap[typeName];
94    if (oldType.constructor !== newType.constructor) {
95      breakingChanges.push({
96        type: BreakingChangeType.TYPE_CHANGED_KIND,
97        description: typeName + ' changed from ' + (typeKindName(oldType) + ' to ' + typeKindName(newType) + '.')
98      });
99    }
100  });
101  return breakingChanges;
102}
103
104/**
105 * Given two schemas, returns an Array containing descriptions of any
106 * breaking or dangerous changes in the newSchema related to arguments
107 * (such as removal or change of type of an argument, or a change in an
108 * argument's default value).
109 */
110export function findArgChanges(oldSchema, newSchema) {
111  var oldTypeMap = oldSchema.getTypeMap();
112  var newTypeMap = newSchema.getTypeMap();
113
114  var breakingChanges = [];
115  var dangerousChanges = [];
116
117  Object.keys(oldTypeMap).forEach(function (typeName) {
118    var oldType = oldTypeMap[typeName];
119    var newType = newTypeMap[typeName];
120    if (!(isObjectType(oldType) || isInterfaceType(oldType)) || !(isObjectType(newType) || isInterfaceType(newType)) || newType.constructor !== oldType.constructor) {
121      return;
122    }
123
124    var oldTypeFields = oldType.getFields();
125    var newTypeFields = newType.getFields();
126
127    Object.keys(oldTypeFields).forEach(function (fieldName) {
128      if (!newTypeFields[fieldName]) {
129        return;
130      }
131
132      oldTypeFields[fieldName].args.forEach(function (oldArgDef) {
133        var newArgs = newTypeFields[fieldName].args;
134        var newArgDef = newArgs.find(function (arg) {
135          return arg.name === oldArgDef.name;
136        });
137
138        // Arg not present
139        if (!newArgDef) {
140          breakingChanges.push({
141            type: BreakingChangeType.ARG_REMOVED,
142            description: oldType.name + '.' + fieldName + ' arg ' + (oldArgDef.name + ' was removed')
143          });
144        } else {
145          var isSafe = isChangeSafeForInputObjectFieldOrFieldArg(oldArgDef.type, newArgDef.type);
146          if (!isSafe) {
147            breakingChanges.push({
148              type: BreakingChangeType.ARG_CHANGED_KIND,
149              description: oldType.name + '.' + fieldName + ' arg ' + (oldArgDef.name + ' has changed type from ') + (oldArgDef.type.toString() + ' to ' + newArgDef.type.toString())
150            });
151          } else if (oldArgDef.defaultValue !== undefined && oldArgDef.defaultValue !== newArgDef.defaultValue) {
152            dangerousChanges.push({
153              type: DangerousChangeType.ARG_DEFAULT_VALUE_CHANGE,
154              description: oldType.name + '.' + fieldName + ' arg ' + (oldArgDef.name + ' has changed defaultValue')
155            });
156          }
157        }
158      });
159      // Check if a non-null arg was added to the field
160      newTypeFields[fieldName].args.forEach(function (newArgDef) {
161        var oldArgs = oldTypeFields[fieldName].args;
162        var oldArgDef = oldArgs.find(function (arg) {
163          return arg.name === newArgDef.name;
164        });
165        if (!oldArgDef) {
166          if (isNonNullType(newArgDef.type)) {
167            breakingChanges.push({
168              type: BreakingChangeType.NON_NULL_ARG_ADDED,
169              description: 'A non-null arg ' + newArgDef.name + ' on ' + (newType.name + '.' + fieldName + ' was added')
170            });
171          } else {
172            dangerousChanges.push({
173              type: DangerousChangeType.NULLABLE_ARG_ADDED,
174              description: 'A nullable arg ' + newArgDef.name + ' on ' + (newType.name + '.' + fieldName + ' was added')
175            });
176          }
177        }
178      });
179    });
180  });
181
182  return {
183    breakingChanges: breakingChanges,
184    dangerousChanges: dangerousChanges
185  };
186}
187
188function typeKindName(type) {
189  if (isScalarType(type)) {
190    return 'a Scalar type';
191  }
192  if (isObjectType(type)) {
193    return 'an Object type';
194  }
195  if (isInterfaceType(type)) {
196    return 'an Interface type';
197  }
198  if (isUnionType(type)) {
199    return 'a Union type';
200  }
201  if (isEnumType(type)) {
202    return 'an Enum type';
203  }
204  if (isInputObjectType(type)) {
205    return 'an Input type';
206  }
207  throw new TypeError('Unknown type ' + type.constructor.name);
208}
209
210export function findFieldsThatChangedTypeOnObjectOrInterfaceTypes(oldSchema, newSchema) {
211  var oldTypeMap = oldSchema.getTypeMap();
212  var newTypeMap = newSchema.getTypeMap();
213
214  var breakingChanges = [];
215  Object.keys(oldTypeMap).forEach(function (typeName) {
216    var oldType = oldTypeMap[typeName];
217    var newType = newTypeMap[typeName];
218    if (!(isObjectType(oldType) || isInterfaceType(oldType)) || !(isObjectType(newType) || isInterfaceType(newType)) || newType.constructor !== oldType.constructor) {
219      return;
220    }
221
222    var oldTypeFieldsDef = oldType.getFields();
223    var newTypeFieldsDef = newType.getFields();
224    Object.keys(oldTypeFieldsDef).forEach(function (fieldName) {
225      // Check if the field is missing on the type in the new schema.
226      if (!(fieldName in newTypeFieldsDef)) {
227        breakingChanges.push({
228          type: BreakingChangeType.FIELD_REMOVED,
229          description: typeName + '.' + fieldName + ' was removed.'
230        });
231      } else {
232        var oldFieldType = oldTypeFieldsDef[fieldName].type;
233        var newFieldType = newTypeFieldsDef[fieldName].type;
234        var isSafe = isChangeSafeForObjectOrInterfaceField(oldFieldType, newFieldType);
235        if (!isSafe) {
236          var oldFieldTypeString = isNamedType(oldFieldType) ? oldFieldType.name : oldFieldType.toString();
237          var newFieldTypeString = isNamedType(newFieldType) ? newFieldType.name : newFieldType.toString();
238          breakingChanges.push({
239            type: BreakingChangeType.FIELD_CHANGED_KIND,
240            description: typeName + '.' + fieldName + ' changed type from ' + (oldFieldTypeString + ' to ' + newFieldTypeString + '.')
241          });
242        }
243      }
244    });
245  });
246  return breakingChanges;
247}
248
249export function findFieldsThatChangedTypeOnInputObjectTypes(oldSchema, newSchema) {
250  var oldTypeMap = oldSchema.getTypeMap();
251  var newTypeMap = newSchema.getTypeMap();
252
253  var breakingChanges = [];
254  var dangerousChanges = [];
255  Object.keys(oldTypeMap).forEach(function (typeName) {
256    var oldType = oldTypeMap[typeName];
257    var newType = newTypeMap[typeName];
258    if (!isInputObjectType(oldType) || !isInputObjectType(newType)) {
259      return;
260    }
261
262    var oldTypeFieldsDef = oldType.getFields();
263    var newTypeFieldsDef = newType.getFields();
264    Object.keys(oldTypeFieldsDef).forEach(function (fieldName) {
265      // Check if the field is missing on the type in the new schema.
266      if (!(fieldName in newTypeFieldsDef)) {
267        breakingChanges.push({
268          type: BreakingChangeType.FIELD_REMOVED,
269          description: typeName + '.' + fieldName + ' was removed.'
270        });
271      } else {
272        var oldFieldType = oldTypeFieldsDef[fieldName].type;
273        var newFieldType = newTypeFieldsDef[fieldName].type;
274
275        var isSafe = isChangeSafeForInputObjectFieldOrFieldArg(oldFieldType, newFieldType);
276        if (!isSafe) {
277          var oldFieldTypeString = isNamedType(oldFieldType) ? oldFieldType.name : oldFieldType.toString();
278          var newFieldTypeString = isNamedType(newFieldType) ? newFieldType.name : newFieldType.toString();
279          breakingChanges.push({
280            type: BreakingChangeType.FIELD_CHANGED_KIND,
281            description: typeName + '.' + fieldName + ' changed type from ' + (oldFieldTypeString + ' to ' + newFieldTypeString + '.')
282          });
283        }
284      }
285    });
286    // Check if a field was added to the input object type
287    Object.keys(newTypeFieldsDef).forEach(function (fieldName) {
288      if (!(fieldName in oldTypeFieldsDef)) {
289        if (isNonNullType(newTypeFieldsDef[fieldName].type)) {
290          breakingChanges.push({
291            type: BreakingChangeType.NON_NULL_INPUT_FIELD_ADDED,
292            description: 'A non-null field ' + fieldName + ' on ' + ('input type ' + newType.name + ' was added.')
293          });
294        } else {
295          dangerousChanges.push({
296            type: DangerousChangeType.NULLABLE_INPUT_FIELD_ADDED,
297            description: 'A nullable field ' + fieldName + ' on ' + ('input type ' + newType.name + ' was added.')
298          });
299        }
300      }
301    });
302  });
303  return {
304    breakingChanges: breakingChanges,
305    dangerousChanges: dangerousChanges
306  };
307}
308
309function isChangeSafeForObjectOrInterfaceField(oldType, newType) {
310  if (isNamedType(oldType)) {
311    return (
312      // if they're both named types, see if their names are equivalent
313      isNamedType(newType) && oldType.name === newType.name ||
314      // moving from nullable to non-null of the same underlying type is safe
315      isNonNullType(newType) && isChangeSafeForObjectOrInterfaceField(oldType, newType.ofType)
316    );
317  } else if (isListType(oldType)) {
318    return (
319      // if they're both lists, make sure the underlying types are compatible
320      isListType(newType) && isChangeSafeForObjectOrInterfaceField(oldType.ofType, newType.ofType) ||
321      // moving from nullable to non-null of the same underlying type is safe
322      isNonNullType(newType) && isChangeSafeForObjectOrInterfaceField(oldType, newType.ofType)
323    );
324  } else if (isNonNullType(oldType)) {
325    // if they're both non-null, make sure the underlying types are compatible
326    return isNonNullType(newType) && isChangeSafeForObjectOrInterfaceField(oldType.ofType, newType.ofType);
327  }
328  return false;
329}
330
331function isChangeSafeForInputObjectFieldOrFieldArg(oldType, newType) {
332  if (isNamedType(oldType)) {
333    // if they're both named types, see if their names are equivalent
334    return isNamedType(newType) && oldType.name === newType.name;
335  } else if (isListType(oldType)) {
336    // if they're both lists, make sure the underlying types are compatible
337    return isListType(newType) && isChangeSafeForInputObjectFieldOrFieldArg(oldType.ofType, newType.ofType);
338  } else if (isNonNullType(oldType)) {
339    return (
340      // if they're both non-null, make sure the underlying types are
341      // compatible
342      isNonNullType(newType) && isChangeSafeForInputObjectFieldOrFieldArg(oldType.ofType, newType.ofType) ||
343      // moving from non-null to nullable of the same underlying type is safe
344      !isNonNullType(newType) && isChangeSafeForInputObjectFieldOrFieldArg(oldType.ofType, newType)
345    );
346  }
347  return false;
348}
349
350/**
351 * Given two schemas, returns an Array containing descriptions of any breaking
352 * changes in the newSchema related to removing types from a union type.
353 */
354export function findTypesRemovedFromUnions(oldSchema, newSchema) {
355  var oldTypeMap = oldSchema.getTypeMap();
356  var newTypeMap = newSchema.getTypeMap();
357
358  var typesRemovedFromUnion = [];
359  Object.keys(oldTypeMap).forEach(function (typeName) {
360    var oldType = oldTypeMap[typeName];
361    var newType = newTypeMap[typeName];
362    if (!isUnionType(oldType) || !isUnionType(newType)) {
363      return;
364    }
365    var typeNamesInNewUnion = Object.create(null);
366    newType.getTypes().forEach(function (type) {
367      typeNamesInNewUnion[type.name] = true;
368    });
369    oldType.getTypes().forEach(function (type) {
370      if (!typeNamesInNewUnion[type.name]) {
371        typesRemovedFromUnion.push({
372          type: BreakingChangeType.TYPE_REMOVED_FROM_UNION,
373          description: type.name + ' was removed from union type ' + typeName + '.'
374        });
375      }
376    });
377  });
378  return typesRemovedFromUnion;
379}
380
381/**
382 * Given two schemas, returns an Array containing descriptions of any dangerous
383 * changes in the newSchema related to adding types to a union type.
384 */
385export function findTypesAddedToUnions(oldSchema, newSchema) {
386  var oldTypeMap = oldSchema.getTypeMap();
387  var newTypeMap = newSchema.getTypeMap();
388
389  var typesAddedToUnion = [];
390  Object.keys(newTypeMap).forEach(function (typeName) {
391    var oldType = oldTypeMap[typeName];
392    var newType = newTypeMap[typeName];
393    if (!isUnionType(oldType) || !isUnionType(newType)) {
394      return;
395    }
396    var typeNamesInOldUnion = Object.create(null);
397    oldType.getTypes().forEach(function (type) {
398      typeNamesInOldUnion[type.name] = true;
399    });
400    newType.getTypes().forEach(function (type) {
401      if (!typeNamesInOldUnion[type.name]) {
402        typesAddedToUnion.push({
403          type: DangerousChangeType.TYPE_ADDED_TO_UNION,
404          description: type.name + ' was added to union type ' + typeName + '.'
405        });
406      }
407    });
408  });
409  return typesAddedToUnion;
410}
411/**
412 * Given two schemas, returns an Array containing descriptions of any breaking
413 * changes in the newSchema related to removing values from an enum type.
414 */
415export function findValuesRemovedFromEnums(oldSchema, newSchema) {
416  var oldTypeMap = oldSchema.getTypeMap();
417  var newTypeMap = newSchema.getTypeMap();
418
419  var valuesRemovedFromEnums = [];
420  Object.keys(oldTypeMap).forEach(function (typeName) {
421    var oldType = oldTypeMap[typeName];
422    var newType = newTypeMap[typeName];
423    if (!isEnumType(oldType) || !isEnumType(newType)) {
424      return;
425    }
426    var valuesInNewEnum = Object.create(null);
427    newType.getValues().forEach(function (value) {
428      valuesInNewEnum[value.name] = true;
429    });
430    oldType.getValues().forEach(function (value) {
431      if (!valuesInNewEnum[value.name]) {
432        valuesRemovedFromEnums.push({
433          type: BreakingChangeType.VALUE_REMOVED_FROM_ENUM,
434          description: value.name + ' was removed from enum type ' + typeName + '.'
435        });
436      }
437    });
438  });
439  return valuesRemovedFromEnums;
440}
441
442/**
443 * Given two schemas, returns an Array containing descriptions of any dangerous
444 * changes in the newSchema related to adding values to an enum type.
445 */
446export function findValuesAddedToEnums(oldSchema, newSchema) {
447  var oldTypeMap = oldSchema.getTypeMap();
448  var newTypeMap = newSchema.getTypeMap();
449
450  var valuesAddedToEnums = [];
451  Object.keys(oldTypeMap).forEach(function (typeName) {
452    var oldType = oldTypeMap[typeName];
453    var newType = newTypeMap[typeName];
454    if (!isEnumType(oldType) || !isEnumType(newType)) {
455      return;
456    }
457
458    var valuesInOldEnum = Object.create(null);
459    oldType.getValues().forEach(function (value) {
460      valuesInOldEnum[value.name] = true;
461    });
462    newType.getValues().forEach(function (value) {
463      if (!valuesInOldEnum[value.name]) {
464        valuesAddedToEnums.push({
465          type: DangerousChangeType.VALUE_ADDED_TO_ENUM,
466          description: value.name + ' was added to enum type ' + typeName + '.'
467        });
468      }
469    });
470  });
471  return valuesAddedToEnums;
472}
473
474export function findInterfacesRemovedFromObjectTypes(oldSchema, newSchema) {
475  var oldTypeMap = oldSchema.getTypeMap();
476  var newTypeMap = newSchema.getTypeMap();
477  var breakingChanges = [];
478
479  Object.keys(oldTypeMap).forEach(function (typeName) {
480    var oldType = oldTypeMap[typeName];
481    var newType = newTypeMap[typeName];
482    if (!isObjectType(oldType) || !isObjectType(newType)) {
483      return;
484    }
485
486    var oldInterfaces = oldType.getInterfaces();
487    var newInterfaces = newType.getInterfaces();
488    oldInterfaces.forEach(function (oldInterface) {
489      if (!newInterfaces.some(function (int) {
490        return int.name === oldInterface.name;
491      })) {
492        breakingChanges.push({
493          type: BreakingChangeType.INTERFACE_REMOVED_FROM_OBJECT,
494          description: typeName + ' no longer implements interface ' + (oldInterface.name + '.')
495        });
496      }
497    });
498  });
499  return breakingChanges;
500}
501
502export function findInterfacesAddedToObjectTypes(oldSchema, newSchema) {
503  var oldTypeMap = oldSchema.getTypeMap();
504  var newTypeMap = newSchema.getTypeMap();
505  var interfacesAddedToObjectTypes = [];
506
507  Object.keys(newTypeMap).forEach(function (typeName) {
508    var oldType = oldTypeMap[typeName];
509    var newType = newTypeMap[typeName];
510    if (!isObjectType(oldType) || !isObjectType(newType)) {
511      return;
512    }
513
514    var oldInterfaces = oldType.getInterfaces();
515    var newInterfaces = newType.getInterfaces();
516    newInterfaces.forEach(function (newInterface) {
517      if (!oldInterfaces.some(function (int) {
518        return int.name === newInterface.name;
519      })) {
520        interfacesAddedToObjectTypes.push({
521          type: DangerousChangeType.INTERFACE_ADDED_TO_OBJECT,
522          description: newInterface.name + ' added to interfaces implemented ' + ('by ' + typeName + '.')
523        });
524      }
525    });
526  });
527  return interfacesAddedToObjectTypes;
528}
529
530export function findRemovedDirectives(oldSchema, newSchema) {
531  var removedDirectives = [];
532
533  var newSchemaDirectiveMap = getDirectiveMapForSchema(newSchema);
534  oldSchema.getDirectives().forEach(function (directive) {
535    if (!newSchemaDirectiveMap[directive.name]) {
536      removedDirectives.push({
537        type: BreakingChangeType.DIRECTIVE_REMOVED,
538        description: directive.name + ' was removed'
539      });
540    }
541  });
542
543  return removedDirectives;
544}
545
546function findRemovedArgsForDirective(oldDirective, newDirective) {
547  var removedArgs = [];
548  var newArgMap = getArgumentMapForDirective(newDirective);
549
550  oldDirective.args.forEach(function (arg) {
551    if (!newArgMap[arg.name]) {
552      removedArgs.push(arg);
553    }
554  });
555
556  return removedArgs;
557}
558
559export function findRemovedDirectiveArgs(oldSchema, newSchema) {
560  var removedDirectiveArgs = [];
561  var oldSchemaDirectiveMap = getDirectiveMapForSchema(oldSchema);
562
563  newSchema.getDirectives().forEach(function (newDirective) {
564    var oldDirective = oldSchemaDirectiveMap[newDirective.name];
565    if (!oldDirective) {
566      return;
567    }
568
569    findRemovedArgsForDirective(oldDirective, newDirective).forEach(function (arg) {
570      removedDirectiveArgs.push({
571        type: BreakingChangeType.DIRECTIVE_ARG_REMOVED,
572        description: arg.name + ' was removed from ' + newDirective.name
573      });
574    });
575  });
576
577  return removedDirectiveArgs;
578}
579
580function findAddedArgsForDirective(oldDirective, newDirective) {
581  var addedArgs = [];
582  var oldArgMap = getArgumentMapForDirective(oldDirective);
583
584  newDirective.args.forEach(function (arg) {
585    if (!oldArgMap[arg.name]) {
586      addedArgs.push(arg);
587    }
588  });
589
590  return addedArgs;
591}
592
593export function findAddedNonNullDirectiveArgs(oldSchema, newSchema) {
594  var addedNonNullableArgs = [];
595  var oldSchemaDirectiveMap = getDirectiveMapForSchema(oldSchema);
596
597  newSchema.getDirectives().forEach(function (newDirective) {
598    var oldDirective = oldSchemaDirectiveMap[newDirective.name];
599    if (!oldDirective) {
600      return;
601    }
602
603    findAddedArgsForDirective(oldDirective, newDirective).forEach(function (arg) {
604      if (!isNonNullType(arg.type)) {
605        return;
606      }
607
608      addedNonNullableArgs.push({
609        type: BreakingChangeType.NON_NULL_DIRECTIVE_ARG_ADDED,
610        description: 'A non-null arg ' + arg.name + ' on directive ' + (newDirective.name + ' was added')
611      });
612    });
613  });
614
615  return addedNonNullableArgs;
616}
617
618export function findRemovedLocationsForDirective(oldDirective, newDirective) {
619  var removedLocations = [];
620  var newLocationSet = new Set(newDirective.locations);
621
622  oldDirective.locations.forEach(function (oldLocation) {
623    if (!newLocationSet.has(oldLocation)) {
624      removedLocations.push(oldLocation);
625    }
626  });
627
628  return removedLocations;
629}
630
631export function findRemovedDirectiveLocations(oldSchema, newSchema) {
632  var removedLocations = [];
633  var oldSchemaDirectiveMap = getDirectiveMapForSchema(oldSchema);
634
635  newSchema.getDirectives().forEach(function (newDirective) {
636    var oldDirective = oldSchemaDirectiveMap[newDirective.name];
637    if (!oldDirective) {
638      return;
639    }
640
641    findRemovedLocationsForDirective(oldDirective, newDirective).forEach(function (location) {
642      removedLocations.push({
643        type: BreakingChangeType.DIRECTIVE_LOCATION_REMOVED,
644        description: location + ' was removed from ' + newDirective.name
645      });
646    });
647  });
648
649  return removedLocations;
650}
651
652function getDirectiveMapForSchema(schema) {
653  return keyMap(schema.getDirectives(), function (dir) {
654    return dir.name;
655  });
656}
657
658function getArgumentMapForDirective(directive) {
659  return keyMap(directive.args, function (arg) {
660    return arg.name;
661  });
662}