graphql-value-transformer.ts 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. import {
  2. ASTKindToNode,
  3. DocumentNode,
  4. getNamedType,
  5. GraphQLInputObjectType,
  6. GraphQLNamedType,
  7. GraphQLSchema,
  8. isInputObjectType,
  9. isListType,
  10. isNonNullType,
  11. OperationDefinitionNode,
  12. TypeInfo,
  13. visit,
  14. Visitor,
  15. visitWithTypeInfo,
  16. } from 'graphql';
  17. export type TypeTree = {
  18. operation: TypeTreeNode;
  19. fragments: { [name: string]: TypeTreeNode };
  20. };
  21. /**
  22. * Represents a GraphQLNamedType which pertains to an input variables object or an output.
  23. * Used when traversing the data object in order to provide the type for the field
  24. * being visited.
  25. */
  26. export type TypeTreeNode = {
  27. type: GraphQLNamedType | undefined;
  28. parent: TypeTreeNode | TypeTree;
  29. isList: boolean;
  30. fragmentRefs: string[];
  31. children: { [name: string]: TypeTreeNode };
  32. };
  33. /**
  34. * This class is used to transform the values of input variables or an output object.
  35. */
  36. export class GraphqlValueTransformer {
  37. private outputCache = new WeakMap<DocumentNode, TypeTree>();
  38. private inputCache = new WeakMap<OperationDefinitionNode, TypeTree>();
  39. constructor(private schema: GraphQLSchema) {}
  40. /**
  41. * Transforms the values in the `data` object into the return value of the `visitorFn`.
  42. */
  43. transformValues(
  44. typeTree: TypeTree,
  45. data: Record<string, any>,
  46. visitorFn: (value: any, type: GraphQLNamedType) => any,
  47. ) {
  48. this.traverse(data, (key, value, path) => {
  49. const typeTreeNode = this.getTypeNodeByPath(typeTree, path);
  50. const type = (typeTreeNode && typeTreeNode.type) as GraphQLNamedType;
  51. return visitorFn(value, type);
  52. });
  53. }
  54. /**
  55. * Constructs a tree of TypeTreeNodes for the output of a GraphQL operation.
  56. */
  57. getOutputTypeTree(document: DocumentNode): TypeTree {
  58. const cached = this.outputCache.get(document);
  59. if (cached) {
  60. return cached;
  61. }
  62. const typeInfo = new TypeInfo(this.schema);
  63. const typeTree: TypeTree = {
  64. operation: {} as any,
  65. fragments: {},
  66. };
  67. const rootNode: TypeTreeNode = {
  68. type: undefined,
  69. isList: false,
  70. parent: typeTree,
  71. fragmentRefs: [],
  72. children: {},
  73. };
  74. typeTree.operation = rootNode;
  75. let currentNode = rootNode;
  76. const visitor: Visitor<ASTKindToNode> = {
  77. enter: node => {
  78. const type = typeInfo.getType();
  79. const fieldDef = typeInfo.getFieldDef();
  80. if (node.kind === 'Field') {
  81. const newNode: TypeTreeNode = {
  82. type: (type && getNamedType(type)) || undefined,
  83. isList: this.isList(type),
  84. fragmentRefs: [],
  85. parent: currentNode,
  86. children: {},
  87. };
  88. currentNode.children[node.alias?.value ?? node.name.value] = newNode;
  89. currentNode = newNode;
  90. }
  91. if (node.kind === 'FragmentSpread') {
  92. currentNode.fragmentRefs.push(node.name.value);
  93. }
  94. if (node.kind === 'FragmentDefinition') {
  95. const rootFragmentNode: TypeTreeNode = {
  96. type: undefined,
  97. isList: false,
  98. fragmentRefs: [],
  99. parent: typeTree,
  100. children: {},
  101. };
  102. currentNode = rootFragmentNode;
  103. typeTree.fragments[node.name.value] = rootFragmentNode;
  104. }
  105. },
  106. leave: node => {
  107. if (node.kind === 'Field') {
  108. if (!this.isTypeTree(currentNode.parent)) {
  109. currentNode = currentNode.parent;
  110. }
  111. }
  112. },
  113. };
  114. for (const operation of document.definitions) {
  115. visit(operation, visitWithTypeInfo(typeInfo, visitor));
  116. }
  117. this.outputCache.set(document, typeTree);
  118. return typeTree;
  119. }
  120. /**
  121. * Constructs a tree of TypeTreeNodes for the input variables of a GraphQL operation.
  122. */
  123. getInputTypeTree(definition: OperationDefinitionNode): TypeTree {
  124. const cached = this.inputCache.get(definition);
  125. if (cached) {
  126. return cached;
  127. }
  128. const typeInfo = new TypeInfo(this.schema);
  129. const typeTree: TypeTree = {
  130. operation: {} as any,
  131. fragments: {},
  132. };
  133. const rootNode: TypeTreeNode = {
  134. type: undefined,
  135. isList: false,
  136. parent: typeTree,
  137. fragmentRefs: [],
  138. children: {},
  139. };
  140. typeTree.operation = rootNode;
  141. let currentNode = rootNode;
  142. const visitor: Visitor<ASTKindToNode> = {
  143. enter: node => {
  144. if (node.kind === 'Argument') {
  145. const type = typeInfo.getType();
  146. const args = typeInfo.getArgument();
  147. if (args) {
  148. const inputType = getNamedType(args.type);
  149. const newNode: TypeTreeNode = {
  150. type: inputType || undefined,
  151. isList: this.isList(type),
  152. parent: currentNode,
  153. fragmentRefs: [],
  154. children: {},
  155. };
  156. currentNode.children[args.name] = newNode;
  157. if (isInputObjectType(inputType)) {
  158. if (isInputObjectType(inputType)) {
  159. newNode.children = this.getChildrenTreeNodes(inputType, newNode);
  160. }
  161. }
  162. currentNode = newNode;
  163. }
  164. }
  165. },
  166. leave: node => {
  167. if (node.kind === 'Argument') {
  168. if (!this.isTypeTree(currentNode.parent)) {
  169. currentNode = currentNode.parent;
  170. }
  171. }
  172. },
  173. };
  174. visit(definition, visitWithTypeInfo(typeInfo, visitor));
  175. this.inputCache.set(definition, typeTree);
  176. return typeTree;
  177. }
  178. private getChildrenTreeNodes(
  179. inputType: GraphQLInputObjectType,
  180. parent: TypeTreeNode,
  181. ): { [name: string]: TypeTreeNode } {
  182. return Object.entries(inputType.getFields()).reduce((result, [key, field]) => {
  183. const namedType = getNamedType(field.type);
  184. const child: TypeTreeNode = {
  185. type: namedType,
  186. isList: this.isList(field.type),
  187. parent,
  188. fragmentRefs: [],
  189. children: {},
  190. };
  191. if (isInputObjectType(namedType)) {
  192. child.children = this.getChildrenTreeNodes(namedType, child);
  193. }
  194. return { ...result, [key]: child };
  195. }, {} as { [name: string]: TypeTreeNode });
  196. }
  197. private isList(t: any): boolean {
  198. return isListType(t) || (isNonNullType(t) && isListType(t.ofType));
  199. }
  200. private getTypeNodeByPath(typeTree: TypeTree, path: Array<string | number>): TypeTreeNode | undefined {
  201. let targetNode: TypeTreeNode | undefined = typeTree.operation;
  202. for (const segment of path) {
  203. if (Number.isNaN(Number.parseInt(segment as string, 10))) {
  204. if (targetNode) {
  205. let children: { [name: string]: TypeTreeNode } = targetNode.children;
  206. if (targetNode.fragmentRefs.length) {
  207. for (const ref of targetNode.fragmentRefs) {
  208. children = { ...children, ...typeTree.fragments[ref].children };
  209. }
  210. }
  211. targetNode = children[segment];
  212. }
  213. }
  214. }
  215. return targetNode;
  216. }
  217. private traverse(
  218. o: { [key: string]: any },
  219. visitorFn: (key: string, value: any, path: Array<string | number>) => any,
  220. path: Array<string | number> = [],
  221. ) {
  222. for (const key of Object.keys(o)) {
  223. path.push(key);
  224. o[key] = visitorFn(key, o[key], path);
  225. if (o[key] !== null && typeof o[key] === 'object') {
  226. this.traverse(o[key], visitorFn, path);
  227. }
  228. path.pop();
  229. }
  230. }
  231. private isTypeTree(input: TypeTree | TypeTreeNode): input is TypeTree {
  232. return input.hasOwnProperty('fragments');
  233. }
  234. }