UNPKG

17.6 kBJavaScriptView Raw
1'use strict';
2
3Object.defineProperty(exports, '__esModule', {
4 value: true,
5});
6exports.DangerousChangeType = exports.BreakingChangeType = void 0;
7exports.findBreakingChanges = findBreakingChanges;
8exports.findDangerousChanges = findDangerousChanges;
9
10var _inspect = require('../jsutils/inspect.js');
11
12var _invariant = require('../jsutils/invariant.js');
13
14var _keyMap = require('../jsutils/keyMap.js');
15
16var _printer = require('../language/printer.js');
17
18var _definition = require('../type/definition.js');
19
20var _scalars = require('../type/scalars.js');
21
22var _astFromValue = require('./astFromValue.js');
23
24var _sortValueNode = require('./sortValueNode.js');
25
26var BreakingChangeType;
27exports.BreakingChangeType = BreakingChangeType;
28
29(function (BreakingChangeType) {
30 BreakingChangeType['TYPE_REMOVED'] = 'TYPE_REMOVED';
31 BreakingChangeType['TYPE_CHANGED_KIND'] = 'TYPE_CHANGED_KIND';
32 BreakingChangeType['TYPE_REMOVED_FROM_UNION'] = 'TYPE_REMOVED_FROM_UNION';
33 BreakingChangeType['VALUE_REMOVED_FROM_ENUM'] = 'VALUE_REMOVED_FROM_ENUM';
34 BreakingChangeType['REQUIRED_INPUT_FIELD_ADDED'] =
35 'REQUIRED_INPUT_FIELD_ADDED';
36 BreakingChangeType['IMPLEMENTED_INTERFACE_REMOVED'] =
37 'IMPLEMENTED_INTERFACE_REMOVED';
38 BreakingChangeType['FIELD_REMOVED'] = 'FIELD_REMOVED';
39 BreakingChangeType['FIELD_CHANGED_KIND'] = 'FIELD_CHANGED_KIND';
40 BreakingChangeType['REQUIRED_ARG_ADDED'] = 'REQUIRED_ARG_ADDED';
41 BreakingChangeType['ARG_REMOVED'] = 'ARG_REMOVED';
42 BreakingChangeType['ARG_CHANGED_KIND'] = 'ARG_CHANGED_KIND';
43 BreakingChangeType['DIRECTIVE_REMOVED'] = 'DIRECTIVE_REMOVED';
44 BreakingChangeType['DIRECTIVE_ARG_REMOVED'] = 'DIRECTIVE_ARG_REMOVED';
45 BreakingChangeType['REQUIRED_DIRECTIVE_ARG_ADDED'] =
46 'REQUIRED_DIRECTIVE_ARG_ADDED';
47 BreakingChangeType['DIRECTIVE_REPEATABLE_REMOVED'] =
48 'DIRECTIVE_REPEATABLE_REMOVED';
49 BreakingChangeType['DIRECTIVE_LOCATION_REMOVED'] =
50 'DIRECTIVE_LOCATION_REMOVED';
51})(
52 BreakingChangeType || (exports.BreakingChangeType = BreakingChangeType = {}),
53);
54
55var DangerousChangeType;
56exports.DangerousChangeType = DangerousChangeType;
57
58(function (DangerousChangeType) {
59 DangerousChangeType['VALUE_ADDED_TO_ENUM'] = 'VALUE_ADDED_TO_ENUM';
60 DangerousChangeType['TYPE_ADDED_TO_UNION'] = 'TYPE_ADDED_TO_UNION';
61 DangerousChangeType['OPTIONAL_INPUT_FIELD_ADDED'] =
62 'OPTIONAL_INPUT_FIELD_ADDED';
63 DangerousChangeType['OPTIONAL_ARG_ADDED'] = 'OPTIONAL_ARG_ADDED';
64 DangerousChangeType['IMPLEMENTED_INTERFACE_ADDED'] =
65 'IMPLEMENTED_INTERFACE_ADDED';
66 DangerousChangeType['ARG_DEFAULT_VALUE_CHANGE'] = 'ARG_DEFAULT_VALUE_CHANGE';
67})(
68 DangerousChangeType ||
69 (exports.DangerousChangeType = DangerousChangeType = {}),
70);
71
72/**
73 * Given two schemas, returns an Array containing descriptions of all the types
74 * of breaking changes covered by the other functions down below.
75 */
76function findBreakingChanges(oldSchema, newSchema) {
77 // @ts-expect-error
78 return findSchemaChanges(oldSchema, newSchema).filter(
79 (change) => change.type in BreakingChangeType,
80 );
81}
82/**
83 * Given two schemas, returns an Array containing descriptions of all the types
84 * of potentially dangerous changes covered by the other functions down below.
85 */
86
87function findDangerousChanges(oldSchema, newSchema) {
88 // @ts-expect-error
89 return findSchemaChanges(oldSchema, newSchema).filter(
90 (change) => change.type in DangerousChangeType,
91 );
92}
93
94function findSchemaChanges(oldSchema, newSchema) {
95 return [
96 ...findTypeChanges(oldSchema, newSchema),
97 ...findDirectiveChanges(oldSchema, newSchema),
98 ];
99}
100
101function findDirectiveChanges(oldSchema, newSchema) {
102 const schemaChanges = [];
103 const directivesDiff = diff(
104 oldSchema.getDirectives(),
105 newSchema.getDirectives(),
106 );
107
108 for (const oldDirective of directivesDiff.removed) {
109 schemaChanges.push({
110 type: BreakingChangeType.DIRECTIVE_REMOVED,
111 description: `${oldDirective.name} was removed.`,
112 });
113 }
114
115 for (const [oldDirective, newDirective] of directivesDiff.persisted) {
116 const argsDiff = diff(oldDirective.args, newDirective.args);
117
118 for (const newArg of argsDiff.added) {
119 if ((0, _definition.isRequiredArgument)(newArg)) {
120 schemaChanges.push({
121 type: BreakingChangeType.REQUIRED_DIRECTIVE_ARG_ADDED,
122 description: `A required arg ${newArg.name} on directive ${oldDirective.name} was added.`,
123 });
124 }
125 }
126
127 for (const oldArg of argsDiff.removed) {
128 schemaChanges.push({
129 type: BreakingChangeType.DIRECTIVE_ARG_REMOVED,
130 description: `${oldArg.name} was removed from ${oldDirective.name}.`,
131 });
132 }
133
134 if (oldDirective.isRepeatable && !newDirective.isRepeatable) {
135 schemaChanges.push({
136 type: BreakingChangeType.DIRECTIVE_REPEATABLE_REMOVED,
137 description: `Repeatable flag was removed from ${oldDirective.name}.`,
138 });
139 }
140
141 for (const location of oldDirective.locations) {
142 if (!newDirective.locations.includes(location)) {
143 schemaChanges.push({
144 type: BreakingChangeType.DIRECTIVE_LOCATION_REMOVED,
145 description: `${location} was removed from ${oldDirective.name}.`,
146 });
147 }
148 }
149 }
150
151 return schemaChanges;
152}
153
154function findTypeChanges(oldSchema, newSchema) {
155 const schemaChanges = [];
156 const typesDiff = diff(
157 Object.values(oldSchema.getTypeMap()),
158 Object.values(newSchema.getTypeMap()),
159 );
160
161 for (const oldType of typesDiff.removed) {
162 schemaChanges.push({
163 type: BreakingChangeType.TYPE_REMOVED,
164 description: (0, _scalars.isSpecifiedScalarType)(oldType)
165 ? `Standard scalar ${oldType.name} was removed because it is not referenced anymore.`
166 : `${oldType.name} was removed.`,
167 });
168 }
169
170 for (const [oldType, newType] of typesDiff.persisted) {
171 if (
172 (0, _definition.isEnumType)(oldType) &&
173 (0, _definition.isEnumType)(newType)
174 ) {
175 schemaChanges.push(...findEnumTypeChanges(oldType, newType));
176 } else if (
177 (0, _definition.isUnionType)(oldType) &&
178 (0, _definition.isUnionType)(newType)
179 ) {
180 schemaChanges.push(...findUnionTypeChanges(oldType, newType));
181 } else if (
182 (0, _definition.isInputObjectType)(oldType) &&
183 (0, _definition.isInputObjectType)(newType)
184 ) {
185 schemaChanges.push(...findInputObjectTypeChanges(oldType, newType));
186 } else if (
187 (0, _definition.isObjectType)(oldType) &&
188 (0, _definition.isObjectType)(newType)
189 ) {
190 schemaChanges.push(
191 ...findFieldChanges(oldType, newType),
192 ...findImplementedInterfacesChanges(oldType, newType),
193 );
194 } else if (
195 (0, _definition.isInterfaceType)(oldType) &&
196 (0, _definition.isInterfaceType)(newType)
197 ) {
198 schemaChanges.push(
199 ...findFieldChanges(oldType, newType),
200 ...findImplementedInterfacesChanges(oldType, newType),
201 );
202 } else if (oldType.constructor !== newType.constructor) {
203 schemaChanges.push({
204 type: BreakingChangeType.TYPE_CHANGED_KIND,
205 description:
206 `${oldType.name} changed from ` +
207 `${typeKindName(oldType)} to ${typeKindName(newType)}.`,
208 });
209 }
210 }
211
212 return schemaChanges;
213}
214
215function findInputObjectTypeChanges(oldType, newType) {
216 const schemaChanges = [];
217 const fieldsDiff = diff(
218 Object.values(oldType.getFields()),
219 Object.values(newType.getFields()),
220 );
221
222 for (const newField of fieldsDiff.added) {
223 if ((0, _definition.isRequiredInputField)(newField)) {
224 schemaChanges.push({
225 type: BreakingChangeType.REQUIRED_INPUT_FIELD_ADDED,
226 description: `A required field ${newField.name} on input type ${oldType.name} was added.`,
227 });
228 } else {
229 schemaChanges.push({
230 type: DangerousChangeType.OPTIONAL_INPUT_FIELD_ADDED,
231 description: `An optional field ${newField.name} on input type ${oldType.name} was added.`,
232 });
233 }
234 }
235
236 for (const oldField of fieldsDiff.removed) {
237 schemaChanges.push({
238 type: BreakingChangeType.FIELD_REMOVED,
239 description: `${oldType.name}.${oldField.name} was removed.`,
240 });
241 }
242
243 for (const [oldField, newField] of fieldsDiff.persisted) {
244 const isSafe = isChangeSafeForInputObjectFieldOrFieldArg(
245 oldField.type,
246 newField.type,
247 );
248
249 if (!isSafe) {
250 schemaChanges.push({
251 type: BreakingChangeType.FIELD_CHANGED_KIND,
252 description:
253 `${oldType.name}.${oldField.name} changed type from ` +
254 `${String(oldField.type)} to ${String(newField.type)}.`,
255 });
256 }
257 }
258
259 return schemaChanges;
260}
261
262function findUnionTypeChanges(oldType, newType) {
263 const schemaChanges = [];
264 const possibleTypesDiff = diff(oldType.getTypes(), newType.getTypes());
265
266 for (const newPossibleType of possibleTypesDiff.added) {
267 schemaChanges.push({
268 type: DangerousChangeType.TYPE_ADDED_TO_UNION,
269 description: `${newPossibleType.name} was added to union type ${oldType.name}.`,
270 });
271 }
272
273 for (const oldPossibleType of possibleTypesDiff.removed) {
274 schemaChanges.push({
275 type: BreakingChangeType.TYPE_REMOVED_FROM_UNION,
276 description: `${oldPossibleType.name} was removed from union type ${oldType.name}.`,
277 });
278 }
279
280 return schemaChanges;
281}
282
283function findEnumTypeChanges(oldType, newType) {
284 const schemaChanges = [];
285 const valuesDiff = diff(oldType.getValues(), newType.getValues());
286
287 for (const newValue of valuesDiff.added) {
288 schemaChanges.push({
289 type: DangerousChangeType.VALUE_ADDED_TO_ENUM,
290 description: `${newValue.name} was added to enum type ${oldType.name}.`,
291 });
292 }
293
294 for (const oldValue of valuesDiff.removed) {
295 schemaChanges.push({
296 type: BreakingChangeType.VALUE_REMOVED_FROM_ENUM,
297 description: `${oldValue.name} was removed from enum type ${oldType.name}.`,
298 });
299 }
300
301 return schemaChanges;
302}
303
304function findImplementedInterfacesChanges(oldType, newType) {
305 const schemaChanges = [];
306 const interfacesDiff = diff(oldType.getInterfaces(), newType.getInterfaces());
307
308 for (const newInterface of interfacesDiff.added) {
309 schemaChanges.push({
310 type: DangerousChangeType.IMPLEMENTED_INTERFACE_ADDED,
311 description: `${newInterface.name} added to interfaces implemented by ${oldType.name}.`,
312 });
313 }
314
315 for (const oldInterface of interfacesDiff.removed) {
316 schemaChanges.push({
317 type: BreakingChangeType.IMPLEMENTED_INTERFACE_REMOVED,
318 description: `${oldType.name} no longer implements interface ${oldInterface.name}.`,
319 });
320 }
321
322 return schemaChanges;
323}
324
325function findFieldChanges(oldType, newType) {
326 const schemaChanges = [];
327 const fieldsDiff = diff(
328 Object.values(oldType.getFields()),
329 Object.values(newType.getFields()),
330 );
331
332 for (const oldField of fieldsDiff.removed) {
333 schemaChanges.push({
334 type: BreakingChangeType.FIELD_REMOVED,
335 description: `${oldType.name}.${oldField.name} was removed.`,
336 });
337 }
338
339 for (const [oldField, newField] of fieldsDiff.persisted) {
340 schemaChanges.push(...findArgChanges(oldType, oldField, newField));
341 const isSafe = isChangeSafeForObjectOrInterfaceField(
342 oldField.type,
343 newField.type,
344 );
345
346 if (!isSafe) {
347 schemaChanges.push({
348 type: BreakingChangeType.FIELD_CHANGED_KIND,
349 description:
350 `${oldType.name}.${oldField.name} changed type from ` +
351 `${String(oldField.type)} to ${String(newField.type)}.`,
352 });
353 }
354 }
355
356 return schemaChanges;
357}
358
359function findArgChanges(oldType, oldField, newField) {
360 const schemaChanges = [];
361 const argsDiff = diff(oldField.args, newField.args);
362
363 for (const oldArg of argsDiff.removed) {
364 schemaChanges.push({
365 type: BreakingChangeType.ARG_REMOVED,
366 description: `${oldType.name}.${oldField.name} arg ${oldArg.name} was removed.`,
367 });
368 }
369
370 for (const [oldArg, newArg] of argsDiff.persisted) {
371 const isSafe = isChangeSafeForInputObjectFieldOrFieldArg(
372 oldArg.type,
373 newArg.type,
374 );
375
376 if (!isSafe) {
377 schemaChanges.push({
378 type: BreakingChangeType.ARG_CHANGED_KIND,
379 description:
380 `${oldType.name}.${oldField.name} arg ${oldArg.name} has changed type from ` +
381 `${String(oldArg.type)} to ${String(newArg.type)}.`,
382 });
383 } else if (oldArg.defaultValue !== undefined) {
384 if (newArg.defaultValue === undefined) {
385 schemaChanges.push({
386 type: DangerousChangeType.ARG_DEFAULT_VALUE_CHANGE,
387 description: `${oldType.name}.${oldField.name} arg ${oldArg.name} defaultValue was removed.`,
388 });
389 } else {
390 // Since we looking only for client's observable changes we should
391 // compare default values in the same representation as they are
392 // represented inside introspection.
393 const oldValueStr = stringifyValue(oldArg.defaultValue, oldArg.type);
394 const newValueStr = stringifyValue(newArg.defaultValue, newArg.type);
395
396 if (oldValueStr !== newValueStr) {
397 schemaChanges.push({
398 type: DangerousChangeType.ARG_DEFAULT_VALUE_CHANGE,
399 description: `${oldType.name}.${oldField.name} arg ${oldArg.name} has changed defaultValue from ${oldValueStr} to ${newValueStr}.`,
400 });
401 }
402 }
403 }
404 }
405
406 for (const newArg of argsDiff.added) {
407 if ((0, _definition.isRequiredArgument)(newArg)) {
408 schemaChanges.push({
409 type: BreakingChangeType.REQUIRED_ARG_ADDED,
410 description: `A required arg ${newArg.name} on ${oldType.name}.${oldField.name} was added.`,
411 });
412 } else {
413 schemaChanges.push({
414 type: DangerousChangeType.OPTIONAL_ARG_ADDED,
415 description: `An optional arg ${newArg.name} on ${oldType.name}.${oldField.name} was added.`,
416 });
417 }
418 }
419
420 return schemaChanges;
421}
422
423function isChangeSafeForObjectOrInterfaceField(oldType, newType) {
424 if ((0, _definition.isListType)(oldType)) {
425 return (
426 // if they're both lists, make sure the underlying types are compatible
427 ((0, _definition.isListType)(newType) &&
428 isChangeSafeForObjectOrInterfaceField(
429 oldType.ofType,
430 newType.ofType,
431 )) || // moving from nullable to non-null of the same underlying type is safe
432 ((0, _definition.isNonNullType)(newType) &&
433 isChangeSafeForObjectOrInterfaceField(oldType, newType.ofType))
434 );
435 }
436
437 if ((0, _definition.isNonNullType)(oldType)) {
438 // if they're both non-null, make sure the underlying types are compatible
439 return (
440 (0, _definition.isNonNullType)(newType) &&
441 isChangeSafeForObjectOrInterfaceField(oldType.ofType, newType.ofType)
442 );
443 }
444
445 return (
446 // if they're both named types, see if their names are equivalent
447 ((0, _definition.isNamedType)(newType) && oldType.name === newType.name) || // moving from nullable to non-null of the same underlying type is safe
448 ((0, _definition.isNonNullType)(newType) &&
449 isChangeSafeForObjectOrInterfaceField(oldType, newType.ofType))
450 );
451}
452
453function isChangeSafeForInputObjectFieldOrFieldArg(oldType, newType) {
454 if ((0, _definition.isListType)(oldType)) {
455 // if they're both lists, make sure the underlying types are compatible
456 return (
457 (0, _definition.isListType)(newType) &&
458 isChangeSafeForInputObjectFieldOrFieldArg(oldType.ofType, newType.ofType)
459 );
460 }
461
462 if ((0, _definition.isNonNullType)(oldType)) {
463 return (
464 // if they're both non-null, make sure the underlying types are
465 // compatible
466 ((0, _definition.isNonNullType)(newType) &&
467 isChangeSafeForInputObjectFieldOrFieldArg(
468 oldType.ofType,
469 newType.ofType,
470 )) || // moving from non-null to nullable of the same underlying type is safe
471 (!(0, _definition.isNonNullType)(newType) &&
472 isChangeSafeForInputObjectFieldOrFieldArg(oldType.ofType, newType))
473 );
474 } // if they're both named types, see if their names are equivalent
475
476 return (0, _definition.isNamedType)(newType) && oldType.name === newType.name;
477}
478
479function typeKindName(type) {
480 if ((0, _definition.isScalarType)(type)) {
481 return 'a Scalar type';
482 }
483
484 if ((0, _definition.isObjectType)(type)) {
485 return 'an Object type';
486 }
487
488 if ((0, _definition.isInterfaceType)(type)) {
489 return 'an Interface type';
490 }
491
492 if ((0, _definition.isUnionType)(type)) {
493 return 'a Union type';
494 }
495
496 if ((0, _definition.isEnumType)(type)) {
497 return 'an Enum type';
498 }
499
500 if ((0, _definition.isInputObjectType)(type)) {
501 return 'an Input type';
502 }
503 /* c8 ignore next 3 */
504 // Not reachable, all possible types have been considered.
505
506 false ||
507 (0, _invariant.invariant)(
508 false,
509 'Unexpected type: ' + (0, _inspect.inspect)(type),
510 );
511}
512
513function stringifyValue(value, type) {
514 const ast = (0, _astFromValue.astFromValue)(value, type);
515 ast != null || (0, _invariant.invariant)(false);
516 return (0, _printer.print)((0, _sortValueNode.sortValueNode)(ast));
517}
518
519function diff(oldArray, newArray) {
520 const added = [];
521 const removed = [];
522 const persisted = [];
523 const oldMap = (0, _keyMap.keyMap)(oldArray, ({ name }) => name);
524 const newMap = (0, _keyMap.keyMap)(newArray, ({ name }) => name);
525
526 for (const oldItem of oldArray) {
527 const newItem = newMap[oldItem.name];
528
529 if (newItem === undefined) {
530 removed.push(oldItem);
531 } else {
532 persisted.push([oldItem, newItem]);
533 }
534 }
535
536 for (const newItem of newArray) {
537 if (oldMap[newItem.name] === undefined) {
538 added.push(newItem);
539 }
540 }
541
542 return {
543 added,
544 persisted,
545 removed,
546 };
547}