| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245 |
- import {
- ASTKindToNode,
- DocumentNode,
- getNamedType,
- GraphQLInputObjectType,
- GraphQLNamedType,
- GraphQLSchema,
- isInputObjectType,
- isListType,
- isNonNullType,
- OperationDefinitionNode,
- TypeInfo,
- visit,
- Visitor,
- visitWithTypeInfo,
- } from 'graphql';
- export type TypeTree = {
- operation: TypeTreeNode;
- fragments: { [name: string]: TypeTreeNode };
- };
- /**
- * Represents a GraphQLNamedType which pertains to an input variables object or an output.
- * Used when traversing the data object in order to provide the type for the field
- * being visited.
- */
- export type TypeTreeNode = {
- type: GraphQLNamedType | undefined;
- parent: TypeTreeNode | TypeTree;
- isList: boolean;
- fragmentRefs: string[];
- children: { [name: string]: TypeTreeNode };
- };
- /**
- * This class is used to transform the values of input variables or an output object.
- */
- export class GraphqlValueTransformer {
- private outputCache = new WeakMap<DocumentNode, TypeTree>();
- private inputCache = new WeakMap<OperationDefinitionNode, TypeTree>();
- constructor(private schema: GraphQLSchema) {}
- /**
- * Transforms the values in the `data` object into the return value of the `visitorFn`.
- */
- transformValues(
- typeTree: TypeTree,
- data: Record<string, any>,
- visitorFn: (value: any, type: GraphQLNamedType) => any,
- ) {
- this.traverse(data, (key, value, path) => {
- const typeTreeNode = this.getTypeNodeByPath(typeTree, path);
- const type = (typeTreeNode && typeTreeNode.type) as GraphQLNamedType;
- return visitorFn(value, type);
- });
- }
- /**
- * Constructs a tree of TypeTreeNodes for the output of a GraphQL operation.
- */
- getOutputTypeTree(document: DocumentNode): TypeTree {
- const cached = this.outputCache.get(document);
- if (cached) {
- return cached;
- }
- const typeInfo = new TypeInfo(this.schema);
- const typeTree: TypeTree = {
- operation: {} as any,
- fragments: {},
- };
- const rootNode: TypeTreeNode = {
- type: undefined,
- isList: false,
- parent: typeTree,
- fragmentRefs: [],
- children: {},
- };
- typeTree.operation = rootNode;
- let currentNode = rootNode;
- const visitor: Visitor<ASTKindToNode> = {
- enter: node => {
- const type = typeInfo.getType();
- const fieldDef = typeInfo.getFieldDef();
- if (node.kind === 'Field') {
- const newNode: TypeTreeNode = {
- type: (type && getNamedType(type)) || undefined,
- isList: this.isList(type),
- fragmentRefs: [],
- parent: currentNode,
- children: {},
- };
- currentNode.children[node.alias?.value ?? node.name.value] = newNode;
- currentNode = newNode;
- }
- if (node.kind === 'FragmentSpread') {
- currentNode.fragmentRefs.push(node.name.value);
- }
- if (node.kind === 'FragmentDefinition') {
- const rootFragmentNode: TypeTreeNode = {
- type: undefined,
- isList: false,
- fragmentRefs: [],
- parent: typeTree,
- children: {},
- };
- currentNode = rootFragmentNode;
- typeTree.fragments[node.name.value] = rootFragmentNode;
- }
- },
- leave: node => {
- if (node.kind === 'Field') {
- if (!this.isTypeTree(currentNode.parent)) {
- currentNode = currentNode.parent;
- }
- }
- },
- };
- for (const operation of document.definitions) {
- visit(operation, visitWithTypeInfo(typeInfo, visitor));
- }
- this.outputCache.set(document, typeTree);
- return typeTree;
- }
- /**
- * Constructs a tree of TypeTreeNodes for the input variables of a GraphQL operation.
- */
- getInputTypeTree(definition: OperationDefinitionNode): TypeTree {
- const cached = this.inputCache.get(definition);
- if (cached) {
- return cached;
- }
- const typeInfo = new TypeInfo(this.schema);
- const typeTree: TypeTree = {
- operation: {} as any,
- fragments: {},
- };
- const rootNode: TypeTreeNode = {
- type: undefined,
- isList: false,
- parent: typeTree,
- fragmentRefs: [],
- children: {},
- };
- typeTree.operation = rootNode;
- let currentNode = rootNode;
- const visitor: Visitor<ASTKindToNode> = {
- enter: node => {
- if (node.kind === 'Argument') {
- const type = typeInfo.getType();
- const args = typeInfo.getArgument();
- if (args) {
- const inputType = getNamedType(args.type);
- const newNode: TypeTreeNode = {
- type: inputType || undefined,
- isList: this.isList(type),
- parent: currentNode,
- fragmentRefs: [],
- children: {},
- };
- currentNode.children[args.name] = newNode;
- if (isInputObjectType(inputType)) {
- if (isInputObjectType(inputType)) {
- newNode.children = this.getChildrenTreeNodes(inputType, newNode);
- }
- }
- currentNode = newNode;
- }
- }
- },
- leave: node => {
- if (node.kind === 'Argument') {
- if (!this.isTypeTree(currentNode.parent)) {
- currentNode = currentNode.parent;
- }
- }
- },
- };
- visit(definition, visitWithTypeInfo(typeInfo, visitor));
- this.inputCache.set(definition, typeTree);
- return typeTree;
- }
- private getChildrenTreeNodes(
- inputType: GraphQLInputObjectType,
- parent: TypeTreeNode,
- ): { [name: string]: TypeTreeNode } {
- return Object.entries(inputType.getFields()).reduce((result, [key, field]) => {
- const namedType = getNamedType(field.type);
- const child: TypeTreeNode = {
- type: namedType,
- isList: this.isList(field.type),
- parent,
- fragmentRefs: [],
- children: {},
- };
- if (isInputObjectType(namedType)) {
- child.children = this.getChildrenTreeNodes(namedType, child);
- }
- return { ...result, [key]: child };
- }, {} as { [name: string]: TypeTreeNode });
- }
- private isList(t: any): boolean {
- return isListType(t) || (isNonNullType(t) && isListType(t.ofType));
- }
- private getTypeNodeByPath(typeTree: TypeTree, path: Array<string | number>): TypeTreeNode | undefined {
- let targetNode: TypeTreeNode | undefined = typeTree.operation;
- for (const segment of path) {
- if (Number.isNaN(Number.parseInt(segment as string, 10))) {
- if (targetNode) {
- let children: { [name: string]: TypeTreeNode } = targetNode.children;
- if (targetNode.fragmentRefs.length) {
- for (const ref of targetNode.fragmentRefs) {
- children = { ...children, ...typeTree.fragments[ref].children };
- }
- }
- targetNode = children[segment];
- }
- }
- }
- return targetNode;
- }
- private traverse(
- o: { [key: string]: any },
- visitorFn: (key: string, value: any, path: Array<string | number>) => any,
- path: Array<string | number> = [],
- ) {
- for (const key of Object.keys(o)) {
- path.push(key);
- o[key] = visitorFn(key, o[key], path);
- if (o[key] !== null && typeof o[key] === 'object') {
- this.traverse(o[key], visitorFn, path);
- }
- path.pop();
- }
- }
- private isTypeTree(input: TypeTree | TypeTreeNode): input is TypeTree {
- return input.hasOwnProperty('fragments');
- }
- }
|