findBreakingChanges.js.flow 17 KB

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