1 | import { inspect } from '../jsutils/inspect.mjs';
|
2 | import { invariant } from '../jsutils/invariant.mjs';
|
3 | import { keyMap } from '../jsutils/keyMap.mjs';
|
4 | import { print } from '../language/printer.mjs';
|
5 | import {
|
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';
|
18 | import { isSpecifiedScalarType } from '../type/scalars.mjs';
|
19 | import { astFromValue } from './astFromValue.mjs';
|
20 | import { sortValueNode } from './sortValueNode.mjs';
|
21 | var 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 |
|
47 | export { BreakingChangeType };
|
48 | var 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 |
|
61 | export { DangerousChangeType };
|
62 |
|
63 |
|
64 |
|
65 |
|
66 |
|
67 | export function findBreakingChanges(oldSchema, newSchema) {
|
68 |
|
69 | return findSchemaChanges(oldSchema, newSchema).filter(
|
70 | (change) => change.type in BreakingChangeType,
|
71 | );
|
72 | }
|
73 |
|
74 |
|
75 |
|
76 |
|
77 |
|
78 | export function findDangerousChanges(oldSchema, newSchema) {
|
79 |
|
80 | return findSchemaChanges(oldSchema, newSchema).filter(
|
81 | (change) => change.type in DangerousChangeType,
|
82 | );
|
83 | }
|
84 |
|
85 | function findSchemaChanges(oldSchema, newSchema) {
|
86 | return [
|
87 | ...findTypeChanges(oldSchema, newSchema),
|
88 | ...findDirectiveChanges(oldSchema, newSchema),
|
89 | ];
|
90 | }
|
91 |
|
92 | function 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 |
|
145 | function 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 |
|
191 | function 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 |
|
238 | function 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 |
|
259 | function 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 |
|
280 | function 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 |
|
301 | function 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 |
|
335 | function 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 |
|
367 |
|
368 |
|
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 |
|
399 | function isChangeSafeForObjectOrInterfaceField(oldType, newType) {
|
400 | if (isListType(oldType)) {
|
401 | return (
|
402 |
|
403 | (isListType(newType) &&
|
404 | isChangeSafeForObjectOrInterfaceField(
|
405 | oldType.ofType,
|
406 | newType.ofType,
|
407 | )) ||
|
408 | (isNonNullType(newType) &&
|
409 | isChangeSafeForObjectOrInterfaceField(oldType, newType.ofType))
|
410 | );
|
411 | }
|
412 |
|
413 | if (isNonNullType(oldType)) {
|
414 |
|
415 | return (
|
416 | isNonNullType(newType) &&
|
417 | isChangeSafeForObjectOrInterfaceField(oldType.ofType, newType.ofType)
|
418 | );
|
419 | }
|
420 |
|
421 | return (
|
422 |
|
423 | (isNamedType(newType) && oldType.name === newType.name) ||
|
424 | (isNonNullType(newType) &&
|
425 | isChangeSafeForObjectOrInterfaceField(oldType, newType.ofType))
|
426 | );
|
427 | }
|
428 |
|
429 | function isChangeSafeForInputObjectFieldOrFieldArg(oldType, newType) {
|
430 | if (isListType(oldType)) {
|
431 |
|
432 | return (
|
433 | isListType(newType) &&
|
434 | isChangeSafeForInputObjectFieldOrFieldArg(oldType.ofType, newType.ofType)
|
435 | );
|
436 | }
|
437 |
|
438 | if (isNonNullType(oldType)) {
|
439 | return (
|
440 |
|
441 |
|
442 | (isNonNullType(newType) &&
|
443 | isChangeSafeForInputObjectFieldOrFieldArg(
|
444 | oldType.ofType,
|
445 | newType.ofType,
|
446 | )) ||
|
447 | (!isNonNullType(newType) &&
|
448 | isChangeSafeForInputObjectFieldOrFieldArg(oldType.ofType, newType))
|
449 | );
|
450 | }
|
451 |
|
452 | return isNamedType(newType) && oldType.name === newType.name;
|
453 | }
|
454 |
|
455 | function 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 |
|
480 |
|
481 |
|
482 | false || invariant(false, 'Unexpected type: ' + inspect(type));
|
483 | }
|
484 |
|
485 | function stringifyValue(value, type) {
|
486 | const ast = astFromValue(value, type);
|
487 | ast != null || invariant(false);
|
488 | return print(sortValueNode(ast));
|
489 | }
|
490 |
|
491 | function 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 | }
|