findBreakingChanges.mjs 16 KB

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