findBreakingChanges.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547
  1. 'use strict';
  2. Object.defineProperty(exports, '__esModule', {
  3. value: true,
  4. });
  5. exports.DangerousChangeType = exports.BreakingChangeType = void 0;
  6. exports.findBreakingChanges = findBreakingChanges;
  7. exports.findDangerousChanges = findDangerousChanges;
  8. var _inspect = require('../jsutils/inspect.js');
  9. var _invariant = require('../jsutils/invariant.js');
  10. var _keyMap = require('../jsutils/keyMap.js');
  11. var _printer = require('../language/printer.js');
  12. var _definition = require('../type/definition.js');
  13. var _scalars = require('../type/scalars.js');
  14. var _astFromValue = require('./astFromValue.js');
  15. var _sortValueNode = require('./sortValueNode.js');
  16. var BreakingChangeType;
  17. exports.BreakingChangeType = BreakingChangeType;
  18. (function (BreakingChangeType) {
  19. BreakingChangeType['TYPE_REMOVED'] = 'TYPE_REMOVED';
  20. BreakingChangeType['TYPE_CHANGED_KIND'] = 'TYPE_CHANGED_KIND';
  21. BreakingChangeType['TYPE_REMOVED_FROM_UNION'] = 'TYPE_REMOVED_FROM_UNION';
  22. BreakingChangeType['VALUE_REMOVED_FROM_ENUM'] = 'VALUE_REMOVED_FROM_ENUM';
  23. BreakingChangeType['REQUIRED_INPUT_FIELD_ADDED'] =
  24. 'REQUIRED_INPUT_FIELD_ADDED';
  25. BreakingChangeType['IMPLEMENTED_INTERFACE_REMOVED'] =
  26. 'IMPLEMENTED_INTERFACE_REMOVED';
  27. BreakingChangeType['FIELD_REMOVED'] = 'FIELD_REMOVED';
  28. BreakingChangeType['FIELD_CHANGED_KIND'] = 'FIELD_CHANGED_KIND';
  29. BreakingChangeType['REQUIRED_ARG_ADDED'] = 'REQUIRED_ARG_ADDED';
  30. BreakingChangeType['ARG_REMOVED'] = 'ARG_REMOVED';
  31. BreakingChangeType['ARG_CHANGED_KIND'] = 'ARG_CHANGED_KIND';
  32. BreakingChangeType['DIRECTIVE_REMOVED'] = 'DIRECTIVE_REMOVED';
  33. BreakingChangeType['DIRECTIVE_ARG_REMOVED'] = 'DIRECTIVE_ARG_REMOVED';
  34. BreakingChangeType['REQUIRED_DIRECTIVE_ARG_ADDED'] =
  35. 'REQUIRED_DIRECTIVE_ARG_ADDED';
  36. BreakingChangeType['DIRECTIVE_REPEATABLE_REMOVED'] =
  37. 'DIRECTIVE_REPEATABLE_REMOVED';
  38. BreakingChangeType['DIRECTIVE_LOCATION_REMOVED'] =
  39. 'DIRECTIVE_LOCATION_REMOVED';
  40. })(
  41. BreakingChangeType || (exports.BreakingChangeType = BreakingChangeType = {}),
  42. );
  43. var DangerousChangeType;
  44. exports.DangerousChangeType = DangerousChangeType;
  45. (function (DangerousChangeType) {
  46. DangerousChangeType['VALUE_ADDED_TO_ENUM'] = 'VALUE_ADDED_TO_ENUM';
  47. DangerousChangeType['TYPE_ADDED_TO_UNION'] = 'TYPE_ADDED_TO_UNION';
  48. DangerousChangeType['OPTIONAL_INPUT_FIELD_ADDED'] =
  49. 'OPTIONAL_INPUT_FIELD_ADDED';
  50. DangerousChangeType['OPTIONAL_ARG_ADDED'] = 'OPTIONAL_ARG_ADDED';
  51. DangerousChangeType['IMPLEMENTED_INTERFACE_ADDED'] =
  52. 'IMPLEMENTED_INTERFACE_ADDED';
  53. DangerousChangeType['ARG_DEFAULT_VALUE_CHANGE'] = 'ARG_DEFAULT_VALUE_CHANGE';
  54. })(
  55. DangerousChangeType ||
  56. (exports.DangerousChangeType = DangerousChangeType = {}),
  57. );
  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. 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. 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 ((0, _definition.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: (0, _scalars.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 (
  145. (0, _definition.isEnumType)(oldType) &&
  146. (0, _definition.isEnumType)(newType)
  147. ) {
  148. schemaChanges.push(...findEnumTypeChanges(oldType, newType));
  149. } else if (
  150. (0, _definition.isUnionType)(oldType) &&
  151. (0, _definition.isUnionType)(newType)
  152. ) {
  153. schemaChanges.push(...findUnionTypeChanges(oldType, newType));
  154. } else if (
  155. (0, _definition.isInputObjectType)(oldType) &&
  156. (0, _definition.isInputObjectType)(newType)
  157. ) {
  158. schemaChanges.push(...findInputObjectTypeChanges(oldType, newType));
  159. } else if (
  160. (0, _definition.isObjectType)(oldType) &&
  161. (0, _definition.isObjectType)(newType)
  162. ) {
  163. schemaChanges.push(
  164. ...findFieldChanges(oldType, newType),
  165. ...findImplementedInterfacesChanges(oldType, newType),
  166. );
  167. } else if (
  168. (0, _definition.isInterfaceType)(oldType) &&
  169. (0, _definition.isInterfaceType)(newType)
  170. ) {
  171. schemaChanges.push(
  172. ...findFieldChanges(oldType, newType),
  173. ...findImplementedInterfacesChanges(oldType, newType),
  174. );
  175. } else if (oldType.constructor !== newType.constructor) {
  176. schemaChanges.push({
  177. type: BreakingChangeType.TYPE_CHANGED_KIND,
  178. description:
  179. `${oldType.name} changed from ` +
  180. `${typeKindName(oldType)} to ${typeKindName(newType)}.`,
  181. });
  182. }
  183. }
  184. return schemaChanges;
  185. }
  186. function findInputObjectTypeChanges(oldType, newType) {
  187. const schemaChanges = [];
  188. const fieldsDiff = diff(
  189. Object.values(oldType.getFields()),
  190. Object.values(newType.getFields()),
  191. );
  192. for (const newField of fieldsDiff.added) {
  193. if ((0, _definition.isRequiredInputField)(newField)) {
  194. schemaChanges.push({
  195. type: BreakingChangeType.REQUIRED_INPUT_FIELD_ADDED,
  196. description: `A required field ${newField.name} on input type ${oldType.name} was added.`,
  197. });
  198. } else {
  199. schemaChanges.push({
  200. type: DangerousChangeType.OPTIONAL_INPUT_FIELD_ADDED,
  201. description: `An optional field ${newField.name} on input type ${oldType.name} was added.`,
  202. });
  203. }
  204. }
  205. for (const oldField of fieldsDiff.removed) {
  206. schemaChanges.push({
  207. type: BreakingChangeType.FIELD_REMOVED,
  208. description: `${oldType.name}.${oldField.name} was removed.`,
  209. });
  210. }
  211. for (const [oldField, newField] of fieldsDiff.persisted) {
  212. const isSafe = isChangeSafeForInputObjectFieldOrFieldArg(
  213. oldField.type,
  214. newField.type,
  215. );
  216. if (!isSafe) {
  217. schemaChanges.push({
  218. type: BreakingChangeType.FIELD_CHANGED_KIND,
  219. description:
  220. `${oldType.name}.${oldField.name} changed type from ` +
  221. `${String(oldField.type)} to ${String(newField.type)}.`,
  222. });
  223. }
  224. }
  225. return schemaChanges;
  226. }
  227. function findUnionTypeChanges(oldType, newType) {
  228. const schemaChanges = [];
  229. const possibleTypesDiff = diff(oldType.getTypes(), newType.getTypes());
  230. for (const newPossibleType of possibleTypesDiff.added) {
  231. schemaChanges.push({
  232. type: DangerousChangeType.TYPE_ADDED_TO_UNION,
  233. description: `${newPossibleType.name} was added to union type ${oldType.name}.`,
  234. });
  235. }
  236. for (const oldPossibleType of possibleTypesDiff.removed) {
  237. schemaChanges.push({
  238. type: BreakingChangeType.TYPE_REMOVED_FROM_UNION,
  239. description: `${oldPossibleType.name} was removed from union type ${oldType.name}.`,
  240. });
  241. }
  242. return schemaChanges;
  243. }
  244. function findEnumTypeChanges(oldType, newType) {
  245. const schemaChanges = [];
  246. const valuesDiff = diff(oldType.getValues(), newType.getValues());
  247. for (const newValue of valuesDiff.added) {
  248. schemaChanges.push({
  249. type: DangerousChangeType.VALUE_ADDED_TO_ENUM,
  250. description: `${newValue.name} was added to enum type ${oldType.name}.`,
  251. });
  252. }
  253. for (const oldValue of valuesDiff.removed) {
  254. schemaChanges.push({
  255. type: BreakingChangeType.VALUE_REMOVED_FROM_ENUM,
  256. description: `${oldValue.name} was removed from enum type ${oldType.name}.`,
  257. });
  258. }
  259. return schemaChanges;
  260. }
  261. function findImplementedInterfacesChanges(oldType, newType) {
  262. const schemaChanges = [];
  263. const interfacesDiff = diff(oldType.getInterfaces(), newType.getInterfaces());
  264. for (const newInterface of interfacesDiff.added) {
  265. schemaChanges.push({
  266. type: DangerousChangeType.IMPLEMENTED_INTERFACE_ADDED,
  267. description: `${newInterface.name} added to interfaces implemented by ${oldType.name}.`,
  268. });
  269. }
  270. for (const oldInterface of interfacesDiff.removed) {
  271. schemaChanges.push({
  272. type: BreakingChangeType.IMPLEMENTED_INTERFACE_REMOVED,
  273. description: `${oldType.name} no longer implements interface ${oldInterface.name}.`,
  274. });
  275. }
  276. return schemaChanges;
  277. }
  278. function findFieldChanges(oldType, newType) {
  279. const schemaChanges = [];
  280. const fieldsDiff = diff(
  281. Object.values(oldType.getFields()),
  282. Object.values(newType.getFields()),
  283. );
  284. for (const oldField of fieldsDiff.removed) {
  285. schemaChanges.push({
  286. type: BreakingChangeType.FIELD_REMOVED,
  287. description: `${oldType.name}.${oldField.name} was removed.`,
  288. });
  289. }
  290. for (const [oldField, newField] of fieldsDiff.persisted) {
  291. schemaChanges.push(...findArgChanges(oldType, oldField, newField));
  292. const isSafe = isChangeSafeForObjectOrInterfaceField(
  293. oldField.type,
  294. newField.type,
  295. );
  296. if (!isSafe) {
  297. schemaChanges.push({
  298. type: BreakingChangeType.FIELD_CHANGED_KIND,
  299. description:
  300. `${oldType.name}.${oldField.name} changed type from ` +
  301. `${String(oldField.type)} to ${String(newField.type)}.`,
  302. });
  303. }
  304. }
  305. return schemaChanges;
  306. }
  307. function findArgChanges(oldType, oldField, newField) {
  308. const schemaChanges = [];
  309. const argsDiff = diff(oldField.args, newField.args);
  310. for (const oldArg of argsDiff.removed) {
  311. schemaChanges.push({
  312. type: BreakingChangeType.ARG_REMOVED,
  313. description: `${oldType.name}.${oldField.name} arg ${oldArg.name} was removed.`,
  314. });
  315. }
  316. for (const [oldArg, newArg] of argsDiff.persisted) {
  317. const isSafe = isChangeSafeForInputObjectFieldOrFieldArg(
  318. oldArg.type,
  319. newArg.type,
  320. );
  321. if (!isSafe) {
  322. schemaChanges.push({
  323. type: BreakingChangeType.ARG_CHANGED_KIND,
  324. description:
  325. `${oldType.name}.${oldField.name} arg ${oldArg.name} has changed type from ` +
  326. `${String(oldArg.type)} to ${String(newArg.type)}.`,
  327. });
  328. } else if (oldArg.defaultValue !== undefined) {
  329. if (newArg.defaultValue === undefined) {
  330. schemaChanges.push({
  331. type: DangerousChangeType.ARG_DEFAULT_VALUE_CHANGE,
  332. description: `${oldType.name}.${oldField.name} arg ${oldArg.name} defaultValue was removed.`,
  333. });
  334. } else {
  335. // Since we looking only for client's observable changes we should
  336. // compare default values in the same representation as they are
  337. // represented inside introspection.
  338. const oldValueStr = stringifyValue(oldArg.defaultValue, oldArg.type);
  339. const newValueStr = stringifyValue(newArg.defaultValue, newArg.type);
  340. if (oldValueStr !== newValueStr) {
  341. schemaChanges.push({
  342. type: DangerousChangeType.ARG_DEFAULT_VALUE_CHANGE,
  343. description: `${oldType.name}.${oldField.name} arg ${oldArg.name} has changed defaultValue from ${oldValueStr} to ${newValueStr}.`,
  344. });
  345. }
  346. }
  347. }
  348. }
  349. for (const newArg of argsDiff.added) {
  350. if ((0, _definition.isRequiredArgument)(newArg)) {
  351. schemaChanges.push({
  352. type: BreakingChangeType.REQUIRED_ARG_ADDED,
  353. description: `A required arg ${newArg.name} on ${oldType.name}.${oldField.name} was added.`,
  354. });
  355. } else {
  356. schemaChanges.push({
  357. type: DangerousChangeType.OPTIONAL_ARG_ADDED,
  358. description: `An optional arg ${newArg.name} on ${oldType.name}.${oldField.name} was added.`,
  359. });
  360. }
  361. }
  362. return schemaChanges;
  363. }
  364. function isChangeSafeForObjectOrInterfaceField(oldType, newType) {
  365. if ((0, _definition.isListType)(oldType)) {
  366. return (
  367. // if they're both lists, make sure the underlying types are compatible
  368. ((0, _definition.isListType)(newType) &&
  369. isChangeSafeForObjectOrInterfaceField(
  370. oldType.ofType,
  371. newType.ofType,
  372. )) || // moving from nullable to non-null of the same underlying type is safe
  373. ((0, _definition.isNonNullType)(newType) &&
  374. isChangeSafeForObjectOrInterfaceField(oldType, newType.ofType))
  375. );
  376. }
  377. if ((0, _definition.isNonNullType)(oldType)) {
  378. // if they're both non-null, make sure the underlying types are compatible
  379. return (
  380. (0, _definition.isNonNullType)(newType) &&
  381. isChangeSafeForObjectOrInterfaceField(oldType.ofType, newType.ofType)
  382. );
  383. }
  384. return (
  385. // if they're both named types, see if their names are equivalent
  386. ((0, _definition.isNamedType)(newType) && oldType.name === newType.name) || // moving from nullable to non-null of the same underlying type is safe
  387. ((0, _definition.isNonNullType)(newType) &&
  388. isChangeSafeForObjectOrInterfaceField(oldType, newType.ofType))
  389. );
  390. }
  391. function isChangeSafeForInputObjectFieldOrFieldArg(oldType, newType) {
  392. if ((0, _definition.isListType)(oldType)) {
  393. // if they're both lists, make sure the underlying types are compatible
  394. return (
  395. (0, _definition.isListType)(newType) &&
  396. isChangeSafeForInputObjectFieldOrFieldArg(oldType.ofType, newType.ofType)
  397. );
  398. }
  399. if ((0, _definition.isNonNullType)(oldType)) {
  400. return (
  401. // if they're both non-null, make sure the underlying types are
  402. // compatible
  403. ((0, _definition.isNonNullType)(newType) &&
  404. isChangeSafeForInputObjectFieldOrFieldArg(
  405. oldType.ofType,
  406. newType.ofType,
  407. )) || // moving from non-null to nullable of the same underlying type is safe
  408. (!(0, _definition.isNonNullType)(newType) &&
  409. isChangeSafeForInputObjectFieldOrFieldArg(oldType.ofType, newType))
  410. );
  411. } // if they're both named types, see if their names are equivalent
  412. return (0, _definition.isNamedType)(newType) && oldType.name === newType.name;
  413. }
  414. function typeKindName(type) {
  415. if ((0, _definition.isScalarType)(type)) {
  416. return 'a Scalar type';
  417. }
  418. if ((0, _definition.isObjectType)(type)) {
  419. return 'an Object type';
  420. }
  421. if ((0, _definition.isInterfaceType)(type)) {
  422. return 'an Interface type';
  423. }
  424. if ((0, _definition.isUnionType)(type)) {
  425. return 'a Union type';
  426. }
  427. if ((0, _definition.isEnumType)(type)) {
  428. return 'an Enum type';
  429. }
  430. if ((0, _definition.isInputObjectType)(type)) {
  431. return 'an Input type';
  432. }
  433. /* c8 ignore next 3 */
  434. // Not reachable, all possible types have been considered.
  435. false ||
  436. (0, _invariant.invariant)(
  437. false,
  438. 'Unexpected type: ' + (0, _inspect.inspect)(type),
  439. );
  440. }
  441. function stringifyValue(value, type) {
  442. const ast = (0, _astFromValue.astFromValue)(value, type);
  443. ast != null || (0, _invariant.invariant)(false);
  444. return (0, _printer.print)((0, _sortValueNode.sortValueNode)(ast));
  445. }
  446. function diff(oldArray, newArray) {
  447. const added = [];
  448. const removed = [];
  449. const persisted = [];
  450. const oldMap = (0, _keyMap.keyMap)(oldArray, ({ name }) => name);
  451. const newMap = (0, _keyMap.keyMap)(newArray, ({ name }) => name);
  452. for (const oldItem of oldArray) {
  453. const newItem = newMap[oldItem.name];
  454. if (newItem === undefined) {
  455. removed.push(oldItem);
  456. } else {
  457. persisted.push([oldItem, newItem]);
  458. }
  459. }
  460. for (const newItem of newArray) {
  461. if (oldMap[newItem.name] === undefined) {
  462. added.push(newItem);
  463. }
  464. }
  465. return {
  466. added,
  467. persisted,
  468. removed,
  469. };
  470. }