coerceInputValue.js.flow 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. // @flow strict
  2. import arrayFrom from '../polyfills/arrayFrom';
  3. import objectValues from '../polyfills/objectValues';
  4. import type { Path } from '../jsutils/Path';
  5. import inspect from '../jsutils/inspect';
  6. import invariant from '../jsutils/invariant';
  7. import didYouMean from '../jsutils/didYouMean';
  8. import isObjectLike from '../jsutils/isObjectLike';
  9. import isCollection from '../jsutils/isCollection';
  10. import suggestionList from '../jsutils/suggestionList';
  11. import printPathArray from '../jsutils/printPathArray';
  12. import { addPath, pathToArray } from '../jsutils/Path';
  13. import { GraphQLError } from '../error/GraphQLError';
  14. import type { GraphQLInputType } from '../type/definition';
  15. import {
  16. isLeafType,
  17. isInputObjectType,
  18. isListType,
  19. isNonNullType,
  20. } from '../type/definition';
  21. type OnErrorCB = (
  22. path: $ReadOnlyArray<string | number>,
  23. invalidValue: mixed,
  24. error: GraphQLError,
  25. ) => void;
  26. /**
  27. * Coerces a JavaScript value given a GraphQL Input Type.
  28. */
  29. export function coerceInputValue(
  30. inputValue: mixed,
  31. type: GraphQLInputType,
  32. onError?: OnErrorCB = defaultOnError,
  33. ): mixed {
  34. return coerceInputValueImpl(inputValue, type, onError);
  35. }
  36. function defaultOnError(
  37. path: $ReadOnlyArray<string | number>,
  38. invalidValue: mixed,
  39. error: GraphQLError,
  40. ) {
  41. let errorPrefix = 'Invalid value ' + inspect(invalidValue);
  42. if (path.length > 0) {
  43. errorPrefix += ` at "value${printPathArray(path)}"`;
  44. }
  45. error.message = errorPrefix + ': ' + error.message;
  46. throw error;
  47. }
  48. function coerceInputValueImpl(
  49. inputValue: mixed,
  50. type: GraphQLInputType,
  51. onError: OnErrorCB,
  52. path: Path | void,
  53. ): mixed {
  54. if (isNonNullType(type)) {
  55. if (inputValue != null) {
  56. return coerceInputValueImpl(inputValue, type.ofType, onError, path);
  57. }
  58. onError(
  59. pathToArray(path),
  60. inputValue,
  61. new GraphQLError(
  62. `Expected non-nullable type "${inspect(type)}" not to be null.`,
  63. ),
  64. );
  65. return;
  66. }
  67. if (inputValue == null) {
  68. // Explicitly return the value null.
  69. return null;
  70. }
  71. if (isListType(type)) {
  72. const itemType = type.ofType;
  73. if (isCollection(inputValue)) {
  74. return arrayFrom(inputValue, (itemValue, index) => {
  75. const itemPath = addPath(path, index, undefined);
  76. return coerceInputValueImpl(itemValue, itemType, onError, itemPath);
  77. });
  78. }
  79. // Lists accept a non-list value as a list of one.
  80. return [coerceInputValueImpl(inputValue, itemType, onError, path)];
  81. }
  82. if (isInputObjectType(type)) {
  83. if (!isObjectLike(inputValue)) {
  84. onError(
  85. pathToArray(path),
  86. inputValue,
  87. new GraphQLError(`Expected type "${type.name}" to be an object.`),
  88. );
  89. return;
  90. }
  91. const coercedValue = {};
  92. const fieldDefs = type.getFields();
  93. for (const field of objectValues(fieldDefs)) {
  94. const fieldValue = inputValue[field.name];
  95. if (fieldValue === undefined) {
  96. if (field.defaultValue !== undefined) {
  97. coercedValue[field.name] = field.defaultValue;
  98. } else if (isNonNullType(field.type)) {
  99. const typeStr = inspect(field.type);
  100. onError(
  101. pathToArray(path),
  102. inputValue,
  103. new GraphQLError(
  104. `Field "${field.name}" of required type "${typeStr}" was not provided.`,
  105. ),
  106. );
  107. }
  108. continue;
  109. }
  110. coercedValue[field.name] = coerceInputValueImpl(
  111. fieldValue,
  112. field.type,
  113. onError,
  114. addPath(path, field.name, type.name),
  115. );
  116. }
  117. // Ensure every provided field is defined.
  118. for (const fieldName of Object.keys(inputValue)) {
  119. if (!fieldDefs[fieldName]) {
  120. const suggestions = suggestionList(
  121. fieldName,
  122. Object.keys(type.getFields()),
  123. );
  124. onError(
  125. pathToArray(path),
  126. inputValue,
  127. new GraphQLError(
  128. `Field "${fieldName}" is not defined by type "${type.name}".` +
  129. didYouMean(suggestions),
  130. ),
  131. );
  132. }
  133. }
  134. return coercedValue;
  135. }
  136. // istanbul ignore else (See: 'https://github.com/graphql/graphql-js/issues/2618')
  137. if (isLeafType(type)) {
  138. let parseResult;
  139. // Scalars and Enums determine if a input value is valid via parseValue(),
  140. // which can throw to indicate failure. If it throws, maintain a reference
  141. // to the original error.
  142. try {
  143. parseResult = type.parseValue(inputValue);
  144. } catch (error) {
  145. if (error instanceof GraphQLError) {
  146. onError(pathToArray(path), inputValue, error);
  147. } else {
  148. onError(
  149. pathToArray(path),
  150. inputValue,
  151. new GraphQLError(
  152. `Expected type "${type.name}". ` + error.message,
  153. undefined,
  154. undefined,
  155. undefined,
  156. undefined,
  157. error,
  158. ),
  159. );
  160. }
  161. return;
  162. }
  163. if (parseResult === undefined) {
  164. onError(
  165. pathToArray(path),
  166. inputValue,
  167. new GraphQLError(`Expected type "${type.name}".`),
  168. );
  169. }
  170. return parseResult;
  171. }
  172. // istanbul ignore next (Not reachable. All possible input types have been considered)
  173. invariant(false, 'Unexpected input type: ' + inspect((type: empty)));
  174. }