UNPKG

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