findBreakingChanges.js.flow 18 KB

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