UNPKG

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