UNPKG

68.9 kBJavaScriptView Raw
1'use strict';
2
3Object.defineProperty(exports, '__esModule', { value: true });
4
5function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
6
7const tslib = require('tslib');
8const graphql = require('graphql');
9const inspect = _interopDefault(require('object-inspect'));
10const dependencyGraph = require('dependency-graph');
11
12function keyMap(list, keyFn) {
13 return list.reduce((map, item) => {
14 map[keyFn(item)] = item;
15 return map;
16 }, Object.create(null));
17}
18function isEqual(a, b) {
19 if (Array.isArray(a) && Array.isArray(b)) {
20 if (a.length !== b.length)
21 return false;
22 for (var index = 0; index < a.length; index++) {
23 if (a[index] !== b[index]) {
24 return false;
25 }
26 }
27 return true;
28 }
29 return a === b || (!a && !b);
30}
31function isNotEqual(a, b) {
32 return !isEqual(a, b);
33}
34function isVoid(a) {
35 return typeof a === 'undefined' || a === null;
36}
37function diffArrays(a, b) {
38 return a.filter((c) => !b.some((d) => d === c));
39}
40function compareLists(oldList, newList, callbacks) {
41 const oldMap = keyMap(oldList, ({ name }) => name);
42 const newMap = keyMap(newList, ({ name }) => name);
43 const added = [];
44 const removed = [];
45 const mutual = [];
46 for (const oldItem of oldList) {
47 const newItem = newMap[oldItem.name];
48 if (newItem === undefined) {
49 removed.push(oldItem);
50 }
51 else {
52 mutual.push({
53 newVersion: newItem,
54 oldVersion: oldItem,
55 });
56 }
57 }
58 for (const newItem of newList) {
59 if (oldMap[newItem.name] === undefined) {
60 added.push(newItem);
61 }
62 }
63 if (callbacks) {
64 if (callbacks.onAdded) {
65 added.forEach(callbacks.onAdded);
66 }
67 if (callbacks.onRemoved) {
68 removed.forEach(callbacks.onRemoved);
69 }
70 if (callbacks.onMutual) {
71 mutual.forEach(callbacks.onMutual);
72 }
73 }
74 return {
75 added,
76 removed,
77 mutual,
78 };
79}
80
81function safeChangeForField(oldType, newType) {
82 if (!graphql.isWrappingType(oldType) && !graphql.isWrappingType(newType)) {
83 return oldType.toString() === newType.toString();
84 }
85 if (graphql.isNonNullType(newType)) {
86 const ofType = graphql.isNonNullType(oldType) ? oldType.ofType : oldType;
87 return safeChangeForField(ofType, newType.ofType);
88 }
89 if (graphql.isListType(oldType)) {
90 return ((graphql.isListType(newType) &&
91 safeChangeForField(oldType.ofType, newType.ofType)) ||
92 (graphql.isNonNullType(newType) && safeChangeForField(oldType, newType.ofType)));
93 }
94 return false;
95}
96function safeChangeForInputValue(oldType, newType) {
97 if (!graphql.isWrappingType(oldType) && !graphql.isWrappingType(newType)) {
98 return oldType.toString() === newType.toString();
99 }
100 if (graphql.isListType(oldType) && graphql.isListType(newType)) {
101 return safeChangeForInputValue(oldType.ofType, newType.ofType);
102 }
103 if (graphql.isNonNullType(oldType)) {
104 const ofType = graphql.isNonNullType(newType) ? newType : newType;
105 return safeChangeForInputValue(oldType.ofType, ofType);
106 }
107 return false;
108}
109function getKind(type) {
110 const node = type.astNode;
111 return (node && node.kind) || '';
112}
113function getTypePrefix(type) {
114 const kind = getKind(type);
115 const kindsMap = {
116 [graphql.Kind.SCALAR_TYPE_DEFINITION]: 'scalar',
117 [graphql.Kind.OBJECT_TYPE_DEFINITION]: 'type',
118 [graphql.Kind.INTERFACE_TYPE_DEFINITION]: 'interface',
119 [graphql.Kind.UNION_TYPE_DEFINITION]: 'union',
120 [graphql.Kind.ENUM_TYPE_DEFINITION]: 'enum',
121 [graphql.Kind.INPUT_OBJECT_TYPE_DEFINITION]: 'input',
122 };
123 return kindsMap[kind];
124}
125function isPrimitive(type) {
126 return (['String', 'Int', 'Float', 'Boolean', 'ID'].indexOf(typeof type === 'string' ? type : type.name) !== -1);
127}
128function isForIntrospection(type) {
129 return ([
130 '__Schema',
131 '__Type',
132 '__TypeKind',
133 '__Field',
134 '__InputValue',
135 '__EnumValue',
136 '__Directive',
137 '__DirectiveLocation',
138 ].indexOf(typeof type === 'string' ? type : type.name) !== -1);
139}
140function findDeprecatedUsages(schema, ast) {
141 const errors = [];
142 const typeInfo = new graphql.TypeInfo(schema);
143 graphql.visit(ast, graphql.visitWithTypeInfo(typeInfo, {
144 Argument(node) {
145 const argument = typeInfo.getArgument();
146 if (argument) {
147 const reason = argument.deprecationReason;
148 if (reason) {
149 const fieldDef = typeInfo.getFieldDef();
150 if (fieldDef) {
151 errors.push(new graphql.GraphQLError(`The argument '${argument === null || argument === void 0 ? void 0 : argument.name}' of '${fieldDef.name}' is deprecated. ${reason}`, [node]));
152 }
153 }
154 }
155 },
156 Field(node) {
157 const fieldDef = typeInfo.getFieldDef();
158 if (fieldDef && fieldDef.isDeprecated) {
159 const parentType = typeInfo.getParentType();
160 if (parentType) {
161 const reason = fieldDef.deprecationReason;
162 errors.push(new graphql.GraphQLError(`The field '${parentType.name}.${fieldDef.name}' is deprecated.${reason ? ' ' + reason : ''}`, [node]));
163 }
164 }
165 },
166 EnumValue(node) {
167 const enumVal = typeInfo.getEnumValue();
168 if (enumVal && enumVal.isDeprecated) {
169 const type = graphql.getNamedType(typeInfo.getInputType());
170 if (type) {
171 const reason = enumVal.deprecationReason;
172 errors.push(new graphql.GraphQLError(`The enum value '${type.name}.${enumVal.name}' is deprecated.${reason ? ' ' + reason : ''}`, [node]));
173 }
174 }
175 },
176 }));
177 return errors;
178}
179function removeFieldIfDirectives(node, directiveNames) {
180 if (node.directives) {
181 if (node.directives.some((d) => directiveNames.indexOf(d.name.value) !== -1)) {
182 return null;
183 }
184 }
185 return node;
186}
187function removeDirectives(node, directiveNames) {
188 if (node.directives) {
189 return Object.assign(Object.assign({}, node), { directives: node.directives.filter((d) => directiveNames.indexOf(d.name.value) === -1) });
190 }
191 return node;
192}
193
194(function (ChangeType) {
195 // Argument
196 ChangeType["FieldArgumentDescriptionChanged"] = "FIELD_ARGUMENT_DESCRIPTION_CHANGED";
197 ChangeType["FieldArgumentDefaultChanged"] = "FIELD_ARGUMENT_DEFAULT_CHANGED";
198 ChangeType["FieldArgumentTypeChanged"] = "FIELD_ARGUMENT_TYPE_CHANGED";
199 // Directive
200 ChangeType["DirectiveRemoved"] = "DIRECTIVE_REMOVED";
201 ChangeType["DirectiveAdded"] = "DIRECTIVE_ADDED";
202 ChangeType["DirectiveDescriptionChanged"] = "DIRECTIVE_DESCRIPTION_CHANGED";
203 ChangeType["DirectiveLocationAdded"] = "DIRECTIVE_LOCATION_ADDED";
204 ChangeType["DirectiveLocationRemoved"] = "DIRECTIVE_LOCATION_REMOVED";
205 ChangeType["DirectiveArgumentAdded"] = "DIRECTIVE_ARGUMENT_ADDED";
206 ChangeType["DirectiveArgumentRemoved"] = "DIRECTIVE_ARGUMENT_REMOVED";
207 ChangeType["DirectiveArgumentDescriptionChanged"] = "DIRECTIVE_ARGUMENT_DESCRIPTION_CHANGED";
208 ChangeType["DirectiveArgumentDefaultValueChanged"] = "DIRECTIVE_ARGUMENT_DEFAULT_VALUE_CHANGED";
209 ChangeType["DirectiveArgumentTypeChanged"] = "DIRECTIVE_ARGUMENT_TYPE_CHANGED";
210 // Enum
211 ChangeType["EnumValueRemoved"] = "ENUM_VALUE_REMOVED";
212 ChangeType["EnumValueAdded"] = "ENUM_VALUE_ADDED";
213 ChangeType["EnumValueDescriptionChanged"] = "ENUM_VALUE_DESCRIPTION_CHANGED";
214 ChangeType["EnumValueDeprecationReasonChanged"] = "ENUM_VALUE_DEPRECATION_REASON_CHANGED";
215 ChangeType["EnumValueDeprecationReasonAdded"] = "ENUM_VALUE_DEPRECATION_REASON_ADDED";
216 ChangeType["EnumValueDeprecationReasonRemoved"] = "ENUM_VALUE_DEPRECATION_REASON_REMOVED";
217 // Field
218 ChangeType["FieldRemoved"] = "FIELD_REMOVED";
219 ChangeType["FieldAdded"] = "FIELD_ADDED";
220 ChangeType["FieldDescriptionChanged"] = "FIELD_DESCRIPTION_CHANGED";
221 ChangeType["FieldDescriptionAdded"] = "FIELD_DESCRIPTION_ADDED";
222 ChangeType["FieldDescriptionRemoved"] = "FIELD_DESCRIPTION_REMOVED";
223 ChangeType["FieldDeprecationAdded"] = "FIELD_DEPRECATION_ADDED";
224 ChangeType["FieldDeprecationRemoved"] = "FIELD_DEPRECATION_REMOVED";
225 ChangeType["FieldDeprecationReasonChanged"] = "FIELD_DEPRECATION_REASON_CHANGED";
226 ChangeType["FieldDeprecationReasonAdded"] = "FIELD_DEPRECATION_REASON_ADDED";
227 ChangeType["FieldDeprecationReasonRemoved"] = "FIELD_DEPRECATION_REASON_REMOVED";
228 ChangeType["FieldTypeChanged"] = "FIELD_TYPE_CHANGED";
229 ChangeType["FieldArgumentAdded"] = "FIELD_ARGUMENT_ADDED";
230 ChangeType["FieldArgumentRemoved"] = "FIELD_ARGUMENT_REMOVED";
231 // Input
232 ChangeType["InputFieldRemoved"] = "INPUT_FIELD_REMOVED";
233 ChangeType["InputFieldAdded"] = "INPUT_FIELD_ADDED";
234 ChangeType["InputFieldDescriptionAdded"] = "INPUT_FIELD_DESCRIPTION_ADDED";
235 ChangeType["InputFieldDescriptionRemoved"] = "INPUT_FIELD_DESCRIPTION_REMOVED";
236 ChangeType["InputFieldDescriptionChanged"] = "INPUT_FIELD_DESCRIPTION_CHANGED";
237 ChangeType["InputFieldDefaultValueChanged"] = "INPUT_FIELD_DEFAULT_VALUE_CHANGED";
238 ChangeType["InputFieldTypeChanged"] = "INPUT_FIELD_TYPE_CHANGED";
239 // Type
240 ChangeType["ObjectTypeInterfaceAdded"] = "OBJECT_TYPE_INTERFACE_ADDED";
241 ChangeType["ObjectTypeInterfaceRemoved"] = "OBJECT_TYPE_INTERFACE_REMOVED";
242 // Schema
243 ChangeType["SchemaQueryTypeChanged"] = "SCHEMA_QUERY_TYPE_CHANGED";
244 ChangeType["SchemaMutationTypeChanged"] = "SCHEMA_MUTATION_TYPE_CHANGED";
245 ChangeType["SchemaSubscriptionTypeChanged"] = "SCHEMA_SUBSCRIPTION_TYPE_CHANGED";
246 // Type
247 ChangeType["TypeRemoved"] = "TYPE_REMOVED";
248 ChangeType["TypeAdded"] = "TYPE_ADDED";
249 ChangeType["TypeKindChanged"] = "TYPE_KIND_CHANGED";
250 ChangeType["TypeDescriptionChanged"] = "TYPE_DESCRIPTION_CHANGED";
251 // TODO
252 ChangeType["TypeDescriptionRemoved"] = "TYPE_DESCRIPTION_REMOVED";
253 // TODO
254 ChangeType["TypeDescriptionAdded"] = "TYPE_DESCRIPTION_ADDED";
255 // Union
256 ChangeType["UnionMemberRemoved"] = "UNION_MEMBER_REMOVED";
257 ChangeType["UnionMemberAdded"] = "UNION_MEMBER_ADDED";
258})(exports.ChangeType || (exports.ChangeType = {}));
259(function (CriticalityLevel) {
260 CriticalityLevel["Breaking"] = "BREAKING";
261 CriticalityLevel["NonBreaking"] = "NON_BREAKING";
262 CriticalityLevel["Dangerous"] = "DANGEROUS";
263})(exports.CriticalityLevel || (exports.CriticalityLevel = {}));
264
265function schemaQueryTypeChanged(oldSchema, newSchema) {
266 const oldName = (oldSchema.getQueryType() || {}).name || 'unknown';
267 const newName = (newSchema.getQueryType() || {}).name || 'unknown';
268 return {
269 criticality: {
270 level: exports.CriticalityLevel.Breaking,
271 },
272 type: exports.ChangeType.SchemaQueryTypeChanged,
273 message: `Schema query root has changed from '${oldName}' to '${newName}'`,
274 };
275}
276function schemaMutationTypeChanged(oldSchema, newSchema) {
277 const oldName = (oldSchema.getMutationType() || {}).name || 'unknown';
278 const newName = (newSchema.getMutationType() || {}).name || 'unknown';
279 return {
280 criticality: {
281 level: exports.CriticalityLevel.Breaking,
282 },
283 type: exports.ChangeType.SchemaMutationTypeChanged,
284 message: `Schema mutation root has changed from '${oldName}' to '${newName}'`,
285 };
286}
287function schemaSubscriptionTypeChanged(oldSchema, newSchema) {
288 const oldName = (oldSchema.getSubscriptionType() || {}).name || 'unknown';
289 const newName = (newSchema.getSubscriptionType() || {}).name || 'unknown';
290 return {
291 criticality: {
292 level: exports.CriticalityLevel.Breaking,
293 },
294 type: exports.ChangeType.SchemaSubscriptionTypeChanged,
295 message: `Schema subscription root has changed from '${oldName}' to '${newName}'`,
296 };
297}
298
299function typeRemoved(type) {
300 return {
301 criticality: {
302 level: exports.CriticalityLevel.Breaking,
303 },
304 type: exports.ChangeType.TypeRemoved,
305 message: `Type '${type.name}' was removed`,
306 path: type.name,
307 };
308}
309function typeAdded(type) {
310 return {
311 criticality: {
312 level: exports.CriticalityLevel.NonBreaking,
313 },
314 type: exports.ChangeType.TypeAdded,
315 message: `Type '${type.name}' was added`,
316 path: type.name,
317 };
318}
319function typeKindChanged(oldType, newType) {
320 return {
321 criticality: {
322 level: exports.CriticalityLevel.Breaking,
323 reason: `Changing the kind of a type is a breaking change because it can cause existing queries to error. For example, turning an object type to a scalar type would break queries that define a selection set for this type.`,
324 },
325 type: exports.ChangeType.TypeKindChanged,
326 message: `'${oldType.name}' kind changed from '${getKind(oldType)}' to '${getKind(newType)}'`,
327 path: oldType.name,
328 };
329}
330function typeDescriptionChanged(oldType, newType) {
331 return {
332 criticality: {
333 level: exports.CriticalityLevel.NonBreaking,
334 },
335 type: exports.ChangeType.TypeDescriptionChanged,
336 message: `Description '${oldType.description}' on type '${oldType.name}' has changed to '${newType.description}'`,
337 path: oldType.name,
338 };
339}
340function typeDescriptionRemoved(type) {
341 return {
342 criticality: {
343 level: exports.CriticalityLevel.NonBreaking,
344 },
345 type: exports.ChangeType.TypeDescriptionRemoved,
346 message: `Description '${type.description}' was removed from object type '${type.name}'`,
347 path: type.name,
348 };
349}
350function typeDescriptionAdded(type) {
351 return {
352 criticality: {
353 level: exports.CriticalityLevel.NonBreaking,
354 },
355 type: exports.ChangeType.TypeDescriptionAdded,
356 message: `Object type '${type.name}' has description '${type.description}'`,
357 path: type.name,
358 };
359}
360
361function directiveRemoved(directive) {
362 return {
363 criticality: {
364 level: exports.CriticalityLevel.Breaking,
365 },
366 type: exports.ChangeType.DirectiveRemoved,
367 message: `Directive '${directive.name}' was removed`,
368 path: `@${directive.name}`,
369 };
370}
371function directiveAdded(directive) {
372 return {
373 criticality: {
374 level: exports.CriticalityLevel.NonBreaking,
375 },
376 type: exports.ChangeType.DirectiveAdded,
377 message: `Directive '${directive.name}' was added`,
378 path: `@${directive.name}`,
379 };
380}
381function directiveDescriptionChanged(oldDirective, newDirective) {
382 return {
383 criticality: {
384 level: exports.CriticalityLevel.NonBreaking,
385 },
386 type: exports.ChangeType.DirectiveDescriptionChanged,
387 message: `Directive '${oldDirective.name}' description changed from '${oldDirective.description}' to '${newDirective.description}'`,
388 path: `@${oldDirective.name}`,
389 };
390}
391function directiveLocationAdded(directive, location) {
392 return {
393 criticality: {
394 level: exports.CriticalityLevel.NonBreaking,
395 },
396 type: exports.ChangeType.DirectiveLocationAdded,
397 message: `Location '${location}' was added to directive '${directive.name}'`,
398 path: `@${directive.name}`,
399 };
400}
401function directiveLocationRemoved(directive, location) {
402 return {
403 criticality: {
404 level: exports.CriticalityLevel.Breaking,
405 },
406 type: exports.ChangeType.DirectiveLocationRemoved,
407 message: `Location '${location}' was removed from directive '${directive.name}'`,
408 path: `@${directive.name}`,
409 };
410}
411function directiveArgumentAdded(directive, arg) {
412 return {
413 criticality: {
414 level: graphql.isNonNullType(arg.type)
415 ? exports.CriticalityLevel.Breaking
416 : exports.CriticalityLevel.NonBreaking,
417 },
418 type: exports.ChangeType.DirectiveArgumentAdded,
419 message: `Argument '${arg.name}' was added to directive '${directive.name}'`,
420 path: `@${directive.name}`,
421 };
422}
423function directiveArgumentRemoved(directive, arg) {
424 return {
425 criticality: {
426 level: exports.CriticalityLevel.Breaking,
427 },
428 type: exports.ChangeType.DirectiveArgumentRemoved,
429 message: `Argument '${arg.name}' was removed from directive '${directive.name}'`,
430 path: `@${directive.name}.${arg.name}`,
431 };
432}
433function directiveArgumentDescriptionChanged(directive, oldArg, newArg) {
434 return {
435 criticality: {
436 level: exports.CriticalityLevel.NonBreaking,
437 },
438 type: exports.ChangeType.DirectiveArgumentDescriptionChanged,
439 message: `Description for argument '${oldArg.name}' on directive '${directive.name}' changed from '${oldArg.description}' to '${newArg.description}'`,
440 path: `@${directive.name}.${oldArg.name}`,
441 };
442}
443function directiveArgumentDefaultValueChanged(directive, oldArg, newArg) {
444 return {
445 criticality: {
446 level: exports.CriticalityLevel.Dangerous,
447 reason: 'Changing the default value for an argument may change the runtime behaviour of a field if it was never provided.',
448 },
449 type: exports.ChangeType.DirectiveArgumentDefaultValueChanged,
450 message: typeof oldArg.defaultValue === 'undefined'
451 ? `Default value '${newArg.defaultValue}' was added to argument '${newArg.name}' on directive '${directive.name}'`
452 : `Default value for argument '${oldArg.name}' on directive '${directive.name}' changed from '${oldArg.defaultValue}' to '${newArg.defaultValue}'`,
453 path: `@${directive.name}.${oldArg.name}`,
454 };
455}
456function directiveArgumentTypeChanged(directive, oldArg, newArg) {
457 return {
458 criticality: safeChangeForInputValue(oldArg.type, newArg.type)
459 ? {
460 level: exports.CriticalityLevel.NonBreaking,
461 reason: 'Changing an input field from non-null to null is considered non-breaking.',
462 }
463 : {
464 level: exports.CriticalityLevel.Breaking,
465 },
466 type: exports.ChangeType.DirectiveArgumentTypeChanged,
467 message: `Type for argument '${oldArg.name}' on directive '${directive.name}' changed from '${oldArg.type}' to '${newArg.type}'`,
468 path: `@${directive.name}.${oldArg.name}`,
469 };
470}
471
472function enumValueRemoved(oldEnum, value) {
473 return {
474 criticality: {
475 level: exports.CriticalityLevel.Breaking,
476 reason: `Removing an enum value will cause existing queries that use this enum value to error.`,
477 },
478 type: exports.ChangeType.EnumValueRemoved,
479 message: `Enum value '${value.name}' ${value.isDeprecated ? '(deprecated) ' : ''}was removed from enum '${oldEnum.name}'`,
480 path: [oldEnum.name, value.name].join('.'),
481 };
482}
483function enumValueAdded(newEnum, value) {
484 return {
485 criticality: {
486 level: exports.CriticalityLevel.Dangerous,
487 reason: `Adding an enum value may break existing clients that were not programming defensively against an added case when querying an enum.`,
488 },
489 type: exports.ChangeType.EnumValueAdded,
490 message: `Enum value '${value.name}' was added to enum '${newEnum.name}'`,
491 path: [newEnum.name, value.name].join('.'),
492 };
493}
494function enumValueDescriptionChanged(newEnum, oldValue, newValue) {
495 return {
496 criticality: {
497 level: exports.CriticalityLevel.NonBreaking,
498 },
499 type: exports.ChangeType.EnumValueDescriptionChanged,
500 message: typeof oldValue.description === 'undefined'
501 ? `Description '${newValue.description}' was added to enum value '${newEnum.name}.${newValue.name}'`
502 : `Description for enum value '${newEnum.name}.${newValue.name}' changed from '${oldValue.description}' to '${newValue.description}'`,
503 path: [newEnum.name, oldValue.name].join('.'),
504 };
505}
506function enumValueDeprecationReasonChanged(newEnum, oldValue, newValue) {
507 return {
508 criticality: {
509 level: exports.CriticalityLevel.NonBreaking,
510 },
511 type: exports.ChangeType.EnumValueDeprecationReasonChanged,
512 message: `Enum value '${newEnum.name}.${newValue.name}' deprecation reason changed from '${oldValue.deprecationReason}' to '${newValue.deprecationReason}'`,
513 path: [newEnum.name, oldValue.name].join('.'),
514 };
515}
516function enumValueDeprecationReasonAdded(newEnum, oldValue, newValue) {
517 return {
518 criticality: {
519 level: exports.CriticalityLevel.NonBreaking,
520 },
521 type: exports.ChangeType.EnumValueDeprecationReasonAdded,
522 message: `Enum value '${newEnum.name}.${newValue.name}' was deprecated with reason '${newValue.deprecationReason}'`,
523 path: [newEnum.name, oldValue.name].join('.'),
524 };
525}
526function enumValueDeprecationReasonRemoved(newEnum, oldValue, newValue) {
527 return {
528 criticality: {
529 level: exports.CriticalityLevel.NonBreaking,
530 },
531 type: exports.ChangeType.EnumValueDeprecationReasonRemoved,
532 message: `Deprecation reason was removed from enum value '${newEnum.name}.${newValue.name}'`,
533 path: [newEnum.name, oldValue.name].join('.'),
534 };
535}
536
537function changesInEnum(oldEnum, newEnum, addChange) {
538 compareLists(oldEnum.getValues(), newEnum.getValues(), {
539 onAdded(value) {
540 addChange(enumValueAdded(newEnum, value));
541 },
542 onRemoved(value) {
543 addChange(enumValueRemoved(oldEnum, value));
544 },
545 onMutual(value) {
546 const oldValue = value.oldVersion;
547 const newValue = value.newVersion;
548 if (isNotEqual(oldValue.description, newValue.description)) {
549 addChange(enumValueDescriptionChanged(newEnum, oldValue, newValue));
550 }
551 if (isNotEqual(oldValue.deprecationReason, newValue.deprecationReason)) {
552 if (isVoid(oldValue.deprecationReason)) {
553 addChange(enumValueDeprecationReasonAdded(newEnum, oldValue, newValue));
554 }
555 else if (isVoid(newValue.deprecationReason)) {
556 addChange(enumValueDeprecationReasonRemoved(newEnum, oldValue, newValue));
557 }
558 else {
559 addChange(enumValueDeprecationReasonChanged(newEnum, oldValue, newValue));
560 }
561 }
562 }
563 });
564}
565
566function unionMemberRemoved(union, type) {
567 return {
568 criticality: {
569 level: exports.CriticalityLevel.Breaking,
570 reason: 'Removing a union member from a union can cause existing queries that use this union member in a fragment spread to error.',
571 },
572 type: exports.ChangeType.UnionMemberRemoved,
573 message: `Member '${type.name}' was removed from Union type '${union.name}'`,
574 path: union.name,
575 };
576}
577function unionMemberAdded(union, type) {
578 return {
579 criticality: {
580 level: exports.CriticalityLevel.Dangerous,
581 reason: 'Adding a possible type to Unions may break existing clients that were not programming defensively against a new possible type.',
582 },
583 type: exports.ChangeType.UnionMemberAdded,
584 message: `Member '${type.name}' was added to Union type '${union.name}'`,
585 path: union.name,
586 };
587}
588
589function changesInUnion(oldUnion, newUnion, addChange) {
590 const oldTypes = oldUnion.getTypes();
591 const newTypes = newUnion.getTypes();
592 compareLists(oldTypes, newTypes, {
593 onAdded(t) {
594 addChange(unionMemberAdded(newUnion, t));
595 },
596 onRemoved(t) {
597 addChange(unionMemberRemoved(oldUnion, t));
598 },
599 });
600}
601
602function inputFieldRemoved(input, field) {
603 return {
604 criticality: {
605 level: exports.CriticalityLevel.Breaking,
606 reason: 'Removing an input field will cause existing queries that use this input field to error.',
607 },
608 type: exports.ChangeType.InputFieldRemoved,
609 message: `Input field '${field.name}' was removed from input object type '${input.name}'`,
610 path: [input.name, field.name].join('.'),
611 };
612}
613function inputFieldAdded(input, field) {
614 return {
615 criticality: graphql.isNonNullType(field.type)
616 ? {
617 level: exports.CriticalityLevel.Breaking,
618 reason: 'Adding a required input field to an existing input object type is a breaking change because it will cause existing uses of this input object type to error.',
619 }
620 : {
621 level: exports.CriticalityLevel.Dangerous,
622 },
623 type: exports.ChangeType.InputFieldAdded,
624 message: `Input field '${field.name}' was added to input object type '${input.name}'`,
625 path: [input.name, field.name].join('.'),
626 };
627}
628function inputFieldDescriptionAdded(type, field) {
629 return {
630 criticality: {
631 level: exports.CriticalityLevel.NonBreaking,
632 },
633 type: exports.ChangeType.InputFieldDescriptionAdded,
634 message: `Input field '${type.name}.${field.name}' has description '${field.description}'`,
635 path: [type.name, field.name].join('.'),
636 };
637}
638function inputFieldDescriptionRemoved(type, field) {
639 return {
640 criticality: {
641 level: exports.CriticalityLevel.NonBreaking,
642 },
643 type: exports.ChangeType.InputFieldDescriptionRemoved,
644 message: `Description was removed from input field '${type.name}.${field.name}'`,
645 path: [type.name, field.name].join('.'),
646 };
647}
648function inputFieldDescriptionChanged(input, oldField, newField) {
649 return {
650 criticality: {
651 level: exports.CriticalityLevel.NonBreaking,
652 },
653 type: exports.ChangeType.InputFieldDescriptionChanged,
654 message: `Input field '${input.name}.${oldField.name}' description changed from '${oldField.description}' to '${newField.description}'`,
655 path: [input.name, oldField.name].join('.'),
656 };
657}
658function inputFieldDefaultValueChanged(input, oldField, newField) {
659 return {
660 criticality: {
661 level: exports.CriticalityLevel.Dangerous,
662 reason: 'Changing the default value for an argument may change the runtime behaviour of a field if it was never provided.',
663 },
664 type: exports.ChangeType.InputFieldDefaultValueChanged,
665 message: `Input field '${input.name}.${oldField.name}' default value changed from '${oldField.defaultValue}' to '${newField.defaultValue}'`,
666 path: [input.name, oldField.name].join('.'),
667 };
668}
669function inputFieldTypeChanged(input, oldField, newField) {
670 return {
671 criticality: safeChangeForInputValue(oldField.type, newField.type)
672 ? {
673 level: exports.CriticalityLevel.NonBreaking,
674 reason: 'Changing an input field from non-null to null is considered non-breaking.',
675 }
676 : {
677 level: exports.CriticalityLevel.Breaking,
678 reason: 'Changing the type of an input field can cause existing queries that use this field to error.',
679 },
680 type: exports.ChangeType.InputFieldTypeChanged,
681 message: `Input field '${input.name}.${oldField.name}' changed type from '${oldField.type.toString()}' to '${newField.type.toString()}'`,
682 path: [input.name, oldField.name].join('.'),
683 };
684}
685
686function changesInInputObject(oldInput, newInput, addChange) {
687 const oldFields = oldInput.getFields();
688 const newFields = newInput.getFields();
689 compareLists(Object.values(oldFields), Object.values(newFields), {
690 onAdded(field) {
691 addChange(inputFieldAdded(newInput, field));
692 },
693 onRemoved(field) {
694 addChange(inputFieldRemoved(oldInput, field));
695 },
696 onMutual(field) {
697 changesInInputField(oldInput, field.oldVersion, field.newVersion, addChange);
698 },
699 });
700}
701function changesInInputField(input, oldField, newField, addChange) {
702 if (isNotEqual(oldField.description, newField.description)) {
703 if (isVoid(oldField.description)) {
704 addChange(inputFieldDescriptionAdded(input, newField));
705 }
706 else if (isVoid(newField.description)) {
707 addChange(inputFieldDescriptionRemoved(input, oldField));
708 }
709 else {
710 addChange(inputFieldDescriptionChanged(input, oldField, newField));
711 }
712 }
713 if (isNotEqual(oldField.defaultValue, newField.defaultValue)) {
714 if (Array.isArray(oldField.defaultValue) &&
715 Array.isArray(newField.defaultValue)) {
716 if (diffArrays(oldField.defaultValue, newField.defaultValue).length > 0) {
717 addChange(inputFieldDefaultValueChanged(input, oldField, newField));
718 }
719 }
720 else if (JSON.stringify(oldField.defaultValue) !==
721 JSON.stringify(newField.defaultValue)) {
722 addChange(inputFieldDefaultValueChanged(input, oldField, newField));
723 }
724 }
725 if (isNotEqual(oldField.type.toString(), newField.type.toString())) {
726 addChange(inputFieldTypeChanged(input, oldField, newField));
727 }
728}
729
730function objectTypeInterfaceAdded(iface, type) {
731 return {
732 criticality: {
733 level: exports.CriticalityLevel.Dangerous,
734 reason: 'Adding an interface to an object type may break existing clients that were not programming defensively against a new possible type.',
735 },
736 type: exports.ChangeType.ObjectTypeInterfaceAdded,
737 message: `'${type.name}' object implements '${iface.name}' interface`,
738 path: type.name,
739 };
740}
741function objectTypeInterfaceRemoved(iface, type) {
742 return {
743 criticality: {
744 level: exports.CriticalityLevel.Breaking,
745 reason: 'Removing an interface from an object type can cause existing queries that use this in a fragment spread to error.',
746 },
747 type: exports.ChangeType.ObjectTypeInterfaceRemoved,
748 message: `'${type.name}' object type no longer implements '${iface.name}' interface`,
749 path: type.name,
750 };
751}
752
753function fieldRemoved(type, field) {
754 const entity = graphql.isInterfaceType(type) ? 'interface' : 'object type';
755 return {
756 criticality: {
757 level: exports.CriticalityLevel.Breaking,
758 reason: field.deprecationReason
759 ? `Removing a deprecated field is a breaking change. Before removing it, you may want to look at the field's usage to see the impact of removing the field.`
760 : `Removing a field is a breaking change. It is preferable to deprecate the field before removing it.`,
761 },
762 type: exports.ChangeType.FieldRemoved,
763 message: `Field '${field.name}' ${field.isDeprecated ? '(deprecated) ' : ''}was removed from ${entity} '${type.name}'`,
764 path: [type.name, field.name].join('.'),
765 };
766}
767function fieldAdded(type, field) {
768 const entity = graphql.isInterfaceType(type) ? 'interface' : 'object type';
769 return {
770 criticality: {
771 level: exports.CriticalityLevel.NonBreaking,
772 },
773 type: exports.ChangeType.FieldAdded,
774 message: `Field '${field.name}' was added to ${entity} '${type.name}'`,
775 path: [type.name, field.name].join('.'),
776 };
777}
778function fieldDescriptionChanged(type, oldField, newField) {
779 return {
780 criticality: {
781 level: exports.CriticalityLevel.NonBreaking,
782 },
783 type: exports.ChangeType.FieldDescriptionChanged,
784 message: `Field '${type.name}.${oldField.name}' description changed from '${oldField.description}' to '${newField.description}'`,
785 path: [type.name, oldField.name].join('.'),
786 };
787}
788function fieldDescriptionAdded(type, field) {
789 return {
790 criticality: {
791 level: exports.CriticalityLevel.NonBreaking,
792 },
793 type: exports.ChangeType.FieldDescriptionAdded,
794 message: `Field '${type.name}.${field.name}' has description '${field.description}'`,
795 path: [type.name, field.name].join('.'),
796 };
797}
798function fieldDescriptionRemoved(type, field) {
799 return {
800 criticality: {
801 level: exports.CriticalityLevel.NonBreaking,
802 },
803 type: exports.ChangeType.FieldDescriptionRemoved,
804 message: `Description was removed from field '${type.name}.${field.name}'`,
805 path: [type.name, field.name].join('.'),
806 };
807}
808function fieldDeprecationAdded(type, field) {
809 return {
810 criticality: {
811 level: exports.CriticalityLevel.NonBreaking,
812 },
813 type: exports.ChangeType.FieldDeprecationAdded,
814 message: `Field '${type.name}.${field.name}' is deprecated`,
815 path: [type.name, field.name].join('.'),
816 };
817}
818function fieldDeprecationRemoved(type, field) {
819 return {
820 criticality: {
821 level: exports.CriticalityLevel.Dangerous,
822 },
823 type: exports.ChangeType.FieldDeprecationRemoved,
824 message: `Field '${type.name}.${field.name}' is no longer deprecated`,
825 path: [type.name, field.name].join('.'),
826 };
827}
828function fieldDeprecationReasonChanged(type, oldField, newField) {
829 return {
830 criticality: {
831 level: exports.CriticalityLevel.NonBreaking,
832 },
833 type: exports.ChangeType.FieldDeprecationReasonChanged,
834 message: `Deprecation reason on field '${type.name}.${newField.name}' has changed from '${oldField.deprecationReason}' to '${newField.deprecationReason}'`,
835 path: [type.name, oldField.name].join('.'),
836 };
837}
838function fieldDeprecationReasonAdded(type, field) {
839 return {
840 criticality: {
841 level: exports.CriticalityLevel.NonBreaking,
842 },
843 type: exports.ChangeType.FieldDeprecationReasonAdded,
844 message: `Field '${type.name}.${field.name}' has deprecation reason '${field.deprecationReason}'`,
845 path: [type.name, field.name].join('.'),
846 };
847}
848function fieldDeprecationReasonRemoved(type, field) {
849 return {
850 criticality: {
851 level: exports.CriticalityLevel.NonBreaking,
852 },
853 type: exports.ChangeType.FieldDeprecationReasonRemoved,
854 message: `Deprecation reason was removed from field '${type.name}.${field.name}'`,
855 path: [type.name, field.name].join('.'),
856 };
857}
858function fieldTypeChanged(type, oldField, newField) {
859 return {
860 criticality: {
861 level: safeChangeForField(oldField.type, newField.type)
862 ? exports.CriticalityLevel.NonBreaking
863 : exports.CriticalityLevel.Breaking,
864 },
865 type: exports.ChangeType.FieldTypeChanged,
866 message: `Field '${type}.${oldField.name}' changed type from '${oldField.type}' to '${newField.type}'`,
867 path: [type.name, oldField.name].join('.'),
868 };
869}
870function fieldArgumentAdded(type, field, arg) {
871 return {
872 criticality: graphql.isNonNullType(arg.type)
873 ? {
874 level: exports.CriticalityLevel.Breaking,
875 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 }
877 : {
878 level: exports.CriticalityLevel.Dangerous,
879 reason: `Adding a new argument to an existing field may involve a change in resolve function logic that potentially may cause some side effects.`,
880 },
881 type: exports.ChangeType.FieldArgumentAdded,
882 message: `Argument '${arg.name}: ${arg.type}' added to field '${type.name}.${field.name}'`,
883 path: [type.name, field.name, arg.name].join('.'),
884 };
885}
886function fieldArgumentRemoved(type, field, arg) {
887 return {
888 criticality: {
889 level: exports.CriticalityLevel.Breaking,
890 reason: `Removing a field argument is a breaking change because it will cause existing queries that use this argument to error.`,
891 },
892 type: exports.ChangeType.FieldArgumentRemoved,
893 message: `Argument '${arg.name}: ${arg.type}' was removed from field '${type.name}.${field.name}'`,
894 path: [type.name, field.name, arg.name].join('.'),
895 };
896}
897
898function compareTwoStrings(str1, str2) {
899 if (!str1.length && !str2.length)
900 return 1;
901 if (!str1.length || !str2.length)
902 return 0;
903 if (str1.toUpperCase() === str2.toUpperCase())
904 return 1;
905 if (str1.length === 1 && str2.length === 1)
906 return 0;
907 const pairs1 = wordLetterPairs(str1);
908 const pairs2 = wordLetterPairs(str2);
909 const union = pairs1.length + pairs2.length;
910 let intersection = 0;
911 pairs1.forEach((pair1) => {
912 for (let i = 0, pair2; (pair2 = pairs2[i]); i++) {
913 if (pair1 !== pair2)
914 continue;
915 intersection++;
916 pairs2.splice(i, 1);
917 break;
918 }
919 });
920 return (intersection * 2) / union;
921}
922function findBestMatch(mainString, targetStrings) {
923 if (!areArgsValid(mainString, targetStrings))
924 throw new Error('Bad arguments: First argument should be a string, second should be an array of strings');
925 const ratings = targetStrings.map((target) => ({
926 target,
927 rating: compareTwoStrings(mainString, target.value),
928 }));
929 const bestMatch = Array.from(ratings).sort((a, b) => b.rating - a.rating)[0];
930 return { ratings, bestMatch };
931}
932function flattenDeep(arr) {
933 return Array.isArray(arr)
934 ? arr.reduce((a, b) => a.concat(flattenDeep(b)), [])
935 : [arr];
936}
937function areArgsValid(mainString, targetStrings) {
938 if (typeof mainString !== 'string')
939 return false;
940 if (!Array.isArray(targetStrings))
941 return false;
942 if (!targetStrings.length)
943 return false;
944 if (targetStrings.find((s) => typeof s.value !== 'string'))
945 return false;
946 return true;
947}
948function letterPairs(str) {
949 const pairs = [];
950 for (let i = 0, max = str.length - 1; i < max; i++)
951 pairs[i] = str.substring(i, i + 2);
952 return pairs;
953}
954function wordLetterPairs(str) {
955 const pairs = str.toUpperCase().split(' ').map(letterPairs);
956 return flattenDeep(pairs);
957}
958function safeString(obj) {
959 if (obj != null && typeof obj.toString === 'function') {
960 return `${obj}`;
961 }
962 return inspect(obj);
963}
964
965function fieldArgumentDescriptionChanged(type, field, oldArg, newArg) {
966 return {
967 criticality: {
968 level: exports.CriticalityLevel.NonBreaking,
969 },
970 type: exports.ChangeType.FieldArgumentDescriptionChanged,
971 message: `Description for argument '${newArg.name}' on field '${type.name}.${field.name}' changed from '${oldArg.description}' to '${newArg.description}'`,
972 path: [type.name, field.name, oldArg.name].join('.'),
973 };
974}
975function fieldArgumentDefaultChanged(type, field, oldArg, newArg) {
976 return {
977 criticality: {
978 level: exports.CriticalityLevel.Dangerous,
979 reason: 'Changing the default value for an argument may change the runtime behaviour of a field if it was never provided.',
980 },
981 type: exports.ChangeType.FieldArgumentDefaultChanged,
982 message: typeof oldArg.defaultValue === 'undefined'
983 ? `Default value '${safeString(newArg.defaultValue)}' was added to argument '${newArg.name}' on field '${type.name}.${field.name}'`
984 : `Default value for argument '${newArg.name}' on field '${type.name}.${field.name}' changed from '${safeString(oldArg.defaultValue)}' to '${safeString(newArg.defaultValue)}'`,
985 path: [type.name, field.name, oldArg.name].join('.'),
986 };
987}
988function fieldArgumentTypeChanged(type, field, oldArg, newArg) {
989 return {
990 criticality: safeChangeForInputValue(oldArg.type, newArg.type)
991 ? {
992 level: exports.CriticalityLevel.NonBreaking,
993 reason: `Changing an input field from non-null to null is considered non-breaking.`,
994 }
995 : {
996 level: exports.CriticalityLevel.Breaking,
997 reason: `Changing the type of a field's argument can cause existing queries that use this argument to error.`,
998 },
999 type: exports.ChangeType.FieldArgumentTypeChanged,
1000 message: `Type for argument '${newArg.name}' on field '${type.name}.${field.name}' changed from '${oldArg.type}' to '${newArg.type}'`,
1001 path: [type.name, field.name, oldArg.name].join('.'),
1002 };
1003}
1004
1005function changesInArgument(type, field, oldArg, newArg, addChange) {
1006 if (isNotEqual(oldArg.description, newArg.description)) {
1007 addChange(fieldArgumentDescriptionChanged(type, field, oldArg, newArg));
1008 }
1009 if (isNotEqual(oldArg.defaultValue, newArg.defaultValue)) {
1010 if (Array.isArray(oldArg.defaultValue) &&
1011 Array.isArray(newArg.defaultValue)) {
1012 const diff = diffArrays(oldArg.defaultValue, newArg.defaultValue);
1013 if (diff.length > 0) {
1014 addChange(fieldArgumentDefaultChanged(type, field, oldArg, newArg));
1015 }
1016 }
1017 else if (JSON.stringify(oldArg.defaultValue) !==
1018 JSON.stringify(newArg.defaultValue)) {
1019 addChange(fieldArgumentDefaultChanged(type, field, oldArg, newArg));
1020 }
1021 }
1022 if (isNotEqual(oldArg.type.toString(), newArg.type.toString())) {
1023 addChange(fieldArgumentTypeChanged(type, field, oldArg, newArg));
1024 }
1025}
1026
1027function changesInField(type, oldField, newField, addChange) {
1028 if (isNotEqual(oldField.description, newField.description)) {
1029 if (isVoid(oldField.description)) {
1030 addChange(fieldDescriptionAdded(type, newField));
1031 }
1032 else if (isVoid(newField.description)) {
1033 addChange(fieldDescriptionRemoved(type, oldField));
1034 }
1035 else {
1036 addChange(fieldDescriptionChanged(type, oldField, newField));
1037 }
1038 }
1039 if (isNotEqual(oldField.isDeprecated, newField.isDeprecated)) {
1040 if (newField.isDeprecated) {
1041 addChange(fieldDeprecationAdded(type, newField));
1042 }
1043 else {
1044 addChange(fieldDeprecationRemoved(type, oldField));
1045 }
1046 }
1047 if (isNotEqual(oldField.deprecationReason, newField.deprecationReason)) {
1048 if (isVoid(oldField.deprecationReason)) {
1049 addChange(fieldDeprecationReasonAdded(type, newField));
1050 }
1051 else if (isVoid(newField.deprecationReason)) {
1052 addChange(fieldDeprecationReasonRemoved(type, oldField));
1053 }
1054 else {
1055 addChange(fieldDeprecationReasonChanged(type, oldField, newField));
1056 }
1057 }
1058 if (isNotEqual(oldField.type.toString(), newField.type.toString())) {
1059 addChange(fieldTypeChanged(type, oldField, newField));
1060 }
1061 compareLists(oldField.args, newField.args, {
1062 onAdded(arg) {
1063 addChange(fieldArgumentAdded(type, newField, arg));
1064 },
1065 onRemoved(arg) {
1066 addChange(fieldArgumentRemoved(type, oldField, arg));
1067 },
1068 onMutual(arg) {
1069 changesInArgument(type, oldField, arg.oldVersion, arg.newVersion, addChange);
1070 }
1071 });
1072}
1073
1074function changesInObject(oldType, newType, addChange) {
1075 const oldInterfaces = oldType.getInterfaces();
1076 const newInterfaces = newType.getInterfaces();
1077 const oldFields = oldType.getFields();
1078 const newFields = newType.getFields();
1079 compareLists(oldInterfaces, newInterfaces, {
1080 onAdded(i) {
1081 addChange(objectTypeInterfaceAdded(i, newType));
1082 },
1083 onRemoved(i) {
1084 addChange(objectTypeInterfaceRemoved(i, oldType));
1085 },
1086 });
1087 compareLists(Object.values(oldFields), Object.values(newFields), {
1088 onAdded(f) {
1089 addChange(fieldAdded(newType, f));
1090 },
1091 onRemoved(f) {
1092 addChange(fieldRemoved(oldType, f));
1093 },
1094 onMutual(f) {
1095 changesInField(oldType, f.oldVersion, f.newVersion, addChange);
1096 },
1097 });
1098}
1099
1100function changesInInterface(oldInterface, newInterface, addChange) {
1101 compareLists(Object.values(oldInterface.getFields()), Object.values(newInterface.getFields()), {
1102 onAdded(field) {
1103 addChange(fieldAdded(newInterface, field));
1104 },
1105 onRemoved(field) {
1106 addChange(fieldRemoved(oldInterface, field));
1107 },
1108 onMutual(field) {
1109 changesInField(oldInterface, field.oldVersion, field.newVersion, addChange);
1110 },
1111 });
1112}
1113
1114function changesInDirective(oldDirective, newDirective, addChange) {
1115 if (isNotEqual(oldDirective.description, newDirective.description)) {
1116 addChange(directiveDescriptionChanged(oldDirective, newDirective));
1117 }
1118 const locations = {
1119 added: diffArrays(newDirective.locations, oldDirective.locations),
1120 removed: diffArrays(oldDirective.locations, newDirective.locations),
1121 };
1122 // locations added
1123 locations.added.forEach((location) => addChange(directiveLocationAdded(newDirective, location)));
1124 // locations removed
1125 locations.removed.forEach((location) => addChange(directiveLocationRemoved(oldDirective, location)));
1126 compareLists(oldDirective.args, newDirective.args, {
1127 onAdded(arg) {
1128 addChange(directiveArgumentAdded(newDirective, arg));
1129 },
1130 onRemoved(arg) {
1131 addChange(directiveArgumentRemoved(oldDirective, arg));
1132 },
1133 onMutual(arg) {
1134 changesInDirectiveArgument(oldDirective, arg.oldVersion, arg.newVersion, addChange);
1135 },
1136 });
1137}
1138function changesInDirectiveArgument(directive, oldArg, newArg, addChange) {
1139 if (isNotEqual(oldArg.description, newArg.description)) {
1140 addChange(directiveArgumentDescriptionChanged(directive, oldArg, newArg));
1141 }
1142 if (isNotEqual(oldArg.defaultValue, newArg.defaultValue)) {
1143 addChange(directiveArgumentDefaultValueChanged(directive, oldArg, newArg));
1144 }
1145 if (isNotEqual(oldArg.type.toString(), newArg.type.toString())) {
1146 addChange(directiveArgumentTypeChanged(directive, oldArg, newArg));
1147 }
1148}
1149
1150function diffSchema(oldSchema, newSchema) {
1151 const changes = [];
1152 function addChange(change) {
1153 changes.push(change);
1154 }
1155 changesInSchema(oldSchema, newSchema, addChange);
1156 compareLists(Object.values(oldSchema.getTypeMap()).filter((t) => !isPrimitive(t)), Object.values(newSchema.getTypeMap()).filter((t) => !isPrimitive(t)), {
1157 onAdded(type) {
1158 addChange(typeAdded(type));
1159 },
1160 onRemoved(type) {
1161 addChange(typeRemoved(type));
1162 },
1163 onMutual(type) {
1164 changesInType(type.oldVersion, type.newVersion, addChange);
1165 },
1166 });
1167 compareLists(oldSchema.getDirectives(), newSchema.getDirectives(), {
1168 onAdded(directive) {
1169 addChange(directiveAdded(directive));
1170 },
1171 onRemoved(directive) {
1172 addChange(directiveRemoved(directive));
1173 },
1174 onMutual(directive) {
1175 changesInDirective(directive.oldVersion, directive.newVersion, addChange);
1176 },
1177 });
1178 return changes;
1179}
1180function changesInSchema(oldSchema, newSchema, addChange) {
1181 var _a, _b, _c, _d, _e, _f;
1182 const defaultNames = {
1183 query: 'Query',
1184 mutation: 'Mutation',
1185 subscription: 'Subscription',
1186 };
1187 const oldRoot = {
1188 query: (_a = (oldSchema.getQueryType() || {}).name) !== null && _a !== void 0 ? _a : defaultNames.query,
1189 mutation: (_b = (oldSchema.getMutationType() || {}).name) !== null && _b !== void 0 ? _b : defaultNames.mutation,
1190 subscription: (_c = (oldSchema.getSubscriptionType() || {}).name) !== null && _c !== void 0 ? _c : defaultNames.subscription,
1191 };
1192 const newRoot = {
1193 query: (_d = (newSchema.getQueryType() || {}).name) !== null && _d !== void 0 ? _d : defaultNames.query,
1194 mutation: (_e = (newSchema.getMutationType() || {}).name) !== null && _e !== void 0 ? _e : defaultNames.mutation,
1195 subscription: (_f = (newSchema.getSubscriptionType() || {}).name) !== null && _f !== void 0 ? _f : defaultNames.subscription,
1196 };
1197 if (isNotEqual(oldRoot.query, newRoot.query)) {
1198 addChange(schemaQueryTypeChanged(oldSchema, newSchema));
1199 }
1200 if (isNotEqual(oldRoot.mutation, newRoot.mutation)) {
1201 addChange(schemaMutationTypeChanged(oldSchema, newSchema));
1202 }
1203 if (isNotEqual(oldRoot.subscription, newRoot.subscription)) {
1204 addChange(schemaSubscriptionTypeChanged(oldSchema, newSchema));
1205 }
1206}
1207function changesInType(oldType, newType, addChange) {
1208 if (graphql.isEnumType(oldType) && graphql.isEnumType(newType)) {
1209 changesInEnum(oldType, newType, addChange);
1210 }
1211 else if (graphql.isUnionType(oldType) && graphql.isUnionType(newType)) {
1212 changesInUnion(oldType, newType, addChange);
1213 }
1214 else if (graphql.isInputObjectType(oldType) && graphql.isInputObjectType(newType)) {
1215 changesInInputObject(oldType, newType, addChange);
1216 }
1217 else if (graphql.isObjectType(oldType) && graphql.isObjectType(newType)) {
1218 changesInObject(oldType, newType, addChange);
1219 }
1220 else if (graphql.isInterfaceType(oldType) && graphql.isInterfaceType(newType)) {
1221 changesInInterface(oldType, newType, addChange);
1222 }
1223 else if (graphql.isScalarType(oldType) && graphql.isScalarType(newType)) ;
1224 else {
1225 addChange(typeKindChanged(oldType, newType));
1226 }
1227 if (isNotEqual(oldType.description, newType.description)) {
1228 if (isVoid(oldType.description)) {
1229 addChange(typeDescriptionAdded(newType));
1230 }
1231 else if (isVoid(newType.description)) {
1232 addChange(typeDescriptionRemoved(oldType));
1233 }
1234 else {
1235 addChange(typeDescriptionChanged(oldType, newType));
1236 }
1237 }
1238}
1239
1240const dangerousBreaking = ({ changes }) => {
1241 return changes.map((change) => {
1242 if (change.criticality.level === exports.CriticalityLevel.Dangerous) {
1243 return Object.assign(Object.assign({}, change), { criticality: Object.assign(Object.assign({}, change.criticality), { level: exports.CriticalityLevel.Breaking }) });
1244 }
1245 return change;
1246 });
1247};
1248
1249function parsePath(path) {
1250 return path.split('.');
1251}
1252
1253const suppressRemovalOfDeprecatedField = ({ changes, oldSchema, }) => {
1254 return changes.map((change) => {
1255 if (change.type === exports.ChangeType.FieldRemoved &&
1256 change.criticality.level === exports.CriticalityLevel.Breaking &&
1257 change.path) {
1258 const [typeName, fieldName] = parsePath(change.path);
1259 const type = oldSchema.getType(typeName);
1260 if (graphql.isObjectType(type) || graphql.isInterfaceType(type)) {
1261 const field = type.getFields()[fieldName];
1262 if (field.isDeprecated) {
1263 return Object.assign(Object.assign({}, change), { criticality: Object.assign(Object.assign({}, change.criticality), { level: exports.CriticalityLevel.Dangerous }) });
1264 }
1265 }
1266 }
1267 if (change.type === exports.ChangeType.EnumValueRemoved &&
1268 change.criticality.level === exports.CriticalityLevel.Breaking &&
1269 change.path) {
1270 const [enumName, enumItem] = parsePath(change.path);
1271 const type = oldSchema.getType(enumName);
1272 if (graphql.isEnumType(type)) {
1273 const item = type.getValue(enumItem);
1274 if (item && item.isDeprecated) {
1275 return Object.assign(Object.assign({}, change), { criticality: Object.assign(Object.assign({}, change.criticality), { level: exports.CriticalityLevel.Dangerous }) });
1276 }
1277 }
1278 }
1279 return change;
1280 });
1281};
1282
1283const descriptionChangeTypes = [
1284 exports.ChangeType.FieldArgumentDescriptionChanged,
1285 exports.ChangeType.DirectiveDescriptionChanged,
1286 exports.ChangeType.DirectiveArgumentDescriptionChanged,
1287 exports.ChangeType.EnumValueDescriptionChanged,
1288 exports.ChangeType.FieldDescriptionChanged,
1289 exports.ChangeType.FieldDescriptionAdded,
1290 exports.ChangeType.FieldDescriptionRemoved,
1291 exports.ChangeType.InputFieldDescriptionAdded,
1292 exports.ChangeType.InputFieldDescriptionRemoved,
1293 exports.ChangeType.InputFieldDescriptionChanged,
1294 exports.ChangeType.TypeDescriptionChanged,
1295];
1296const ignoreDescriptionChanges = ({ changes }) => {
1297 return changes.filter((change) => descriptionChangeTypes.indexOf(change.type) === -1);
1298};
1299
1300const considerUsage = ({ changes, config, }) => tslib.__awaiter(void 0, void 0, void 0, function* () {
1301 if (!config) {
1302 throw new Error(`considerUsage rule is missing config`);
1303 }
1304 const collectedBreakingField = [];
1305 changes.forEach((change) => {
1306 if (change.criticality.level === exports.CriticalityLevel.Breaking && change.path) {
1307 const [typeName, fieldName, argumentName] = parsePath(change.path);
1308 collectedBreakingField.push({
1309 type: typeName,
1310 field: fieldName,
1311 argument: argumentName,
1312 });
1313 }
1314 });
1315 // True if safe to break, false otherwise
1316 const usageList = yield config.checkUsage(collectedBreakingField);
1317 // turns an array of booleans into an array of `Type.Field` strings
1318 // includes only those that are safe to break the api
1319 const suppressedPaths = collectedBreakingField
1320 .filter((_, i) => usageList[i] === true)
1321 .map(({ type, field, argument }) => [type, field, argument].filter(Boolean).join('.'));
1322 return changes.map((change) => {
1323 // Turns those "safe to break" changes into "dangerous"
1324 if (change.criticality.level === exports.CriticalityLevel.Breaking &&
1325 change.path &&
1326 suppressedPaths.some((p) => change.path.startsWith(p))) {
1327 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)` });
1328 }
1329 return change;
1330 });
1331});
1332
1333const rules = /*#__PURE__*/Object.freeze({
1334 __proto__: null,
1335 dangerousBreaking: dangerousBreaking,
1336 suppressRemovalOfDeprecatedField: suppressRemovalOfDeprecatedField,
1337 ignoreDescriptionChanges: ignoreDescriptionChanges,
1338 considerUsage: considerUsage
1339});
1340
1341const DiffRule = rules;
1342function diff(oldSchema, newSchema, rules = [], config) {
1343 const changes = diffSchema(oldSchema, newSchema);
1344 return rules.reduce((prev, rule) => tslib.__awaiter(this, void 0, void 0, function* () {
1345 const prevChanges = yield prev;
1346 return rule({
1347 changes: prevChanges,
1348 oldSchema,
1349 newSchema,
1350 config,
1351 });
1352 }), Promise.resolve(changes));
1353}
1354
1355function readDocument(source) {
1356 const result = {
1357 source,
1358 fragments: [],
1359 operations: [],
1360 hasFragments: false,
1361 hasOperations: false,
1362 };
1363 const documentNode = graphql.parse(source.body);
1364 const filepath = source.name;
1365 const definitions = documentNode.definitions || [];
1366 definitions.forEach((node) => {
1367 if (isOperation(node)) {
1368 result.operations.push({
1369 node,
1370 source: filepath,
1371 });
1372 }
1373 else if (isFragment(node)) {
1374 result.fragments.push({
1375 node,
1376 source: filepath,
1377 });
1378 }
1379 });
1380 result.hasFragments = result.fragments.length > 0;
1381 result.hasOperations = result.operations.length > 0;
1382 return result;
1383}
1384function isOperation(node) {
1385 return node.kind === graphql.Kind.OPERATION_DEFINITION;
1386}
1387function isFragment(node) {
1388 return node.kind === graphql.Kind.FRAGMENT_DEFINITION;
1389}
1390
1391function validateQueryDepth({ source, doc, maxDepth, fragmentGraph, }) {
1392 try {
1393 calculateDepth({
1394 node: doc,
1395 currentDepth: 0,
1396 maxDepth,
1397 getFragment(name) {
1398 return fragmentGraph.getNodeData(name);
1399 },
1400 });
1401 }
1402 catch (errorOrNode) {
1403 if (errorOrNode instanceof Error) {
1404 throw errorOrNode;
1405 }
1406 const node = errorOrNode;
1407 return new graphql.GraphQLError(`Query exceeds maximum depth of ${maxDepth}`, node, source, node.loc && node.loc.start ? [node.loc.start] : undefined);
1408 }
1409}
1410function calculateDepth({ node, currentDepth, maxDepth, getFragment, }) {
1411 if (maxDepth && currentDepth > maxDepth) {
1412 throw node;
1413 }
1414 switch (node.kind) {
1415 case graphql.Kind.FIELD: {
1416 if (node.name.value.startsWith('__') || !node.selectionSet) {
1417 return 0;
1418 }
1419 const maxInnerDepth = calculateDepth({
1420 node: node.selectionSet,
1421 currentDepth: currentDepth + 1,
1422 maxDepth,
1423 getFragment,
1424 });
1425 return 1 + maxInnerDepth;
1426 }
1427 case graphql.Kind.SELECTION_SET: {
1428 return Math.max(...node.selections.map((selection) => {
1429 return calculateDepth({
1430 node: selection,
1431 currentDepth: currentDepth,
1432 maxDepth,
1433 getFragment,
1434 });
1435 }));
1436 }
1437 case graphql.Kind.DOCUMENT: {
1438 return Math.max(...node.definitions.map((def) => {
1439 return calculateDepth({
1440 node: def,
1441 currentDepth: currentDepth,
1442 maxDepth,
1443 getFragment,
1444 });
1445 }));
1446 }
1447 case graphql.Kind.OPERATION_DEFINITION:
1448 case graphql.Kind.INLINE_FRAGMENT:
1449 case graphql.Kind.FRAGMENT_DEFINITION: {
1450 return Math.max(...node.selectionSet.selections.map((selection) => {
1451 return calculateDepth({
1452 node: selection,
1453 currentDepth,
1454 maxDepth,
1455 getFragment,
1456 });
1457 }));
1458 }
1459 case graphql.Kind.FRAGMENT_SPREAD:
1460 return calculateDepth({
1461 node: getFragment(node.name.value),
1462 currentDepth,
1463 maxDepth,
1464 getFragment,
1465 });
1466 default: {
1467 throw new Error(`Couldn't handle ${node.kind}`);
1468 }
1469 }
1470}
1471
1472function transformDocumentWithApollo(doc, { keepClientFields }) {
1473 return graphql.visit(doc, {
1474 Field(node) {
1475 return keepClientFields
1476 ? removeDirectives(node, ['client'])
1477 : removeFieldIfDirectives(node, ['client']);
1478 },
1479 });
1480}
1481function transformSchemaWithApollo(schema) {
1482 return graphql.extendSchema(schema, graphql.parse(/* GraphQL */ `
1483 directive @connection(key: String!, filter: [String]) on FIELD
1484 `));
1485}
1486
1487function validate(schema, sources, options) {
1488 const config = Object.assign({ strictDeprecated: true, strictFragments: true, keepClientFields: false, apollo: false }, options);
1489 const invalidDocuments = [];
1490 // read documents
1491 const documents = sources.map(readDocument);
1492 // keep all named fragments
1493 const fragments = [];
1494 const fragmentNames = [];
1495 const graph = new dependencyGraph.DepGraph({ circular: true });
1496 documents.forEach((doc) => {
1497 doc.fragments.forEach((fragment) => {
1498 fragmentNames.push(fragment.node.name.value);
1499 fragments.push(fragment);
1500 graph.addNode(fragment.node.name.value, fragment.node);
1501 });
1502 });
1503 fragments.forEach((fragment) => {
1504 const depends = extractFragments(graphql.print(fragment.node));
1505 if (depends) {
1506 depends.forEach((name) => {
1507 graph.addDependency(fragment.node.name.value, name);
1508 });
1509 }
1510 });
1511 documents
1512 // since we include fragments, validate only operations
1513 .filter((doc) => doc.hasOperations)
1514 .forEach((doc) => {
1515 const docWithOperations = {
1516 kind: 'Document',
1517 definitions: doc.operations.map((d) => d.node),
1518 };
1519 const extractedFragments = (extractFragments(graphql.print(docWithOperations)) || [])
1520 // resolve all nested fragments
1521 .map((fragmentName) => resolveFragment(graph.getNodeData(fragmentName), graph))
1522 // flatten arrays
1523 .reduce((list, current) => list.concat(current), [])
1524 // remove duplicates
1525 .filter((def, i, all) => all.findIndex((item) => item.name.value === def.name.value) === i);
1526 const merged = {
1527 kind: 'Document',
1528 definitions: [...docWithOperations.definitions, ...extractedFragments],
1529 };
1530 let transformedSchema = config.apollo
1531 ? transformSchemaWithApollo(schema)
1532 : schema;
1533 const transformedDoc = config.apollo
1534 ? transformDocumentWithApollo(merged, {
1535 keepClientFields: config.keepClientFields,
1536 })
1537 : merged;
1538 const errors = graphql.validate(transformedSchema, transformedDoc) || [];
1539 if (config.maxDepth) {
1540 const depthError = validateQueryDepth({
1541 source: doc.source,
1542 doc: transformedDoc,
1543 maxDepth: config.maxDepth,
1544 fragmentGraph: graph,
1545 });
1546 if (depthError) {
1547 errors.push(depthError);
1548 }
1549 }
1550 const deprecated = config.strictDeprecated
1551 ? findDeprecatedUsages(transformedSchema, transformedDoc)
1552 : [];
1553 const duplicatedFragments = config.strictFragments
1554 ? findDuplicatedFragments(fragmentNames)
1555 : [];
1556 if (sumLengths(errors, duplicatedFragments, deprecated) > 0) {
1557 invalidDocuments.push({
1558 source: doc.source,
1559 errors: [...errors, ...duplicatedFragments],
1560 deprecated,
1561 });
1562 }
1563 });
1564 return invalidDocuments;
1565}
1566function findDuplicatedFragments(fragmentNames) {
1567 return fragmentNames
1568 .filter((name, i, all) => all.indexOf(name) !== i)
1569 .map((name) => new graphql.GraphQLError(`Name of '${name}' fragment is not unique`));
1570}
1571//
1572// PostInfo -> AuthorInfo
1573// AuthorInfo -> None
1574//
1575function resolveFragment(fragment, graph) {
1576 return graph
1577 .dependenciesOf(fragment.name.value)
1578 .reduce((list, current) => [
1579 ...list,
1580 ...resolveFragment(graph.getNodeData(current), graph),
1581 ], [fragment]);
1582}
1583function extractFragments(document) {
1584 return (document.match(/[\.]{3}[a-z0-9\_]+\b/gi) || []).map((name) => name.replace('...', ''));
1585}
1586function sumLengths(...arrays) {
1587 return arrays.reduce((sum, { length }) => sum + length, 0);
1588}
1589
1590function similar(schema, typeName, threshold = 0.4) {
1591 const typeMap = schema.getTypeMap();
1592 const targets = Object.keys(schema.getTypeMap())
1593 .filter((name) => !isPrimitive(name) && !isForIntrospection(name))
1594 .map((name) => ({
1595 typeId: name,
1596 value: stripType(typeMap[name]),
1597 }));
1598 const results = {};
1599 if (typeof typeName !== 'undefined' &&
1600 !targets.some((t) => t.typeId === typeName)) {
1601 throw new Error(`Type '${typeName}' doesn't exist`);
1602 }
1603 (typeName ? [{ typeId: typeName, value: '' }] : targets).forEach((source) => {
1604 const sourceType = schema.getType(source.typeId);
1605 const matchWith = targets.filter((target) => schema.getType(target.typeId).astNode.kind ===
1606 sourceType.astNode.kind && target.typeId !== source.typeId);
1607 if (matchWith.length > 0) {
1608 const found = similarTo(sourceType, matchWith, threshold);
1609 if (found) {
1610 results[source.typeId] = found;
1611 }
1612 }
1613 });
1614 return results;
1615}
1616function similarTo(type, targets, threshold) {
1617 const types = targets.filter((target) => target.typeId !== type.name);
1618 const result = findBestMatch(stripType(type), types);
1619 if (result.bestMatch.rating < threshold) {
1620 return;
1621 }
1622 return {
1623 bestMatch: result.bestMatch,
1624 ratings: result.ratings
1625 .filter((r) => r.rating >= threshold && r.target !== result.bestMatch.target)
1626 .sort((a, b) => a.rating - b.rating)
1627 .reverse(),
1628 };
1629}
1630function stripType(type) {
1631 return graphql.printType(type)
1632 .trim()
1633 .replace(/^[a-z]+ [^\{]+\{/g, '')
1634 .replace(/\}$/g, '')
1635 .trim()
1636 .split('\n')
1637 .map((s) => s.trim())
1638 .sort((a, b) => a.localeCompare(b))
1639 .join(' ');
1640}
1641
1642function coverage(schema, sources) {
1643 const coverage = {
1644 sources,
1645 types: {},
1646 };
1647 const typeMap = schema.getTypeMap();
1648 const typeInfo = new graphql.TypeInfo(schema);
1649 const visitor = (source) => ({
1650 Field(node) {
1651 const fieldDef = typeInfo.getFieldDef();
1652 const parent = typeInfo.getParentType();
1653 if (parent &&
1654 parent.name &&
1655 !isForIntrospection(parent.name) &&
1656 fieldDef &&
1657 fieldDef.name &&
1658 fieldDef.name !== '__typename' &&
1659 fieldDef.name !== '__schema') {
1660 const sourceName = source.name;
1661 const typeCoverage = coverage.types[parent.name];
1662 const fieldCoverage = typeCoverage.children[fieldDef.name];
1663 const locations = fieldCoverage.locations[sourceName];
1664 typeCoverage.hits++;
1665 fieldCoverage.hits++;
1666 if (node.loc) {
1667 fieldCoverage.locations[sourceName] = [
1668 node.loc,
1669 ...(locations || []),
1670 ];
1671 }
1672 if (node.arguments) {
1673 for (const argNode of node.arguments) {
1674 const argCoverage = fieldCoverage.children[argNode.name.value];
1675 argCoverage.hits++;
1676 if (argNode.loc) {
1677 argCoverage.locations[sourceName] = [
1678 argNode.loc,
1679 ...(argCoverage.locations[sourceName] || []),
1680 ];
1681 }
1682 }
1683 }
1684 }
1685 },
1686 });
1687 for (const typename in typeMap) {
1688 if (!isForIntrospection(typename) && !isPrimitive(typename)) {
1689 const type = typeMap[typename];
1690 if (graphql.isObjectType(type) || graphql.isInterfaceType(type)) {
1691 const typeCoverage = {
1692 hits: 0,
1693 type,
1694 children: {},
1695 };
1696 const fieldMap = type.getFields();
1697 for (const fieldname in fieldMap) {
1698 const field = fieldMap[fieldname];
1699 typeCoverage.children[field.name] = {
1700 hits: 0,
1701 locations: {},
1702 children: {},
1703 };
1704 for (const arg of field.args) {
1705 typeCoverage.children[field.name].children[arg.name] = {
1706 hits: 0,
1707 locations: {},
1708 };
1709 }
1710 }
1711 coverage.types[type.name] = typeCoverage;
1712 }
1713 }
1714 }
1715 const documents = coverage.sources.map(readDocument);
1716 documents.forEach((doc, i) => {
1717 const source = coverage.sources[i];
1718 doc.operations.forEach((op) => {
1719 graphql.visit(op.node, graphql.visitWithTypeInfo(typeInfo, visitor(source)));
1720 });
1721 doc.fragments.forEach((fr) => {
1722 graphql.visit(fr.node, graphql.visitWithTypeInfo(typeInfo, visitor(source)));
1723 });
1724 });
1725 return coverage;
1726}
1727
1728exports.DiffRule = DiffRule;
1729exports.coverage = coverage;
1730exports.diff = diff;
1731exports.getTypePrefix = getTypePrefix;
1732exports.similar = similar;
1733exports.validate = validate;