From 0e5beb5bbe84b07f067c54dc8d030ea14e31f413 Mon Sep 17 00:00:00 2001 From: Dima Snisarenko Date: Tue, 20 Aug 2024 13:17:28 +0200 Subject: [PATCH] feat(FieldArgTypeRewriter): retrieve var name by object path re: #26 --- src/ast.ts | 29 ++++++- src/rewriters/FieldArgTypeRewriter.ts | 19 ++++- test/functional/rewriteFieldArgType.test.ts | 86 +++++++++++++++++++++ 3 files changed, 129 insertions(+), 5 deletions(-) diff --git a/src/ast.ts b/src/ast.ts index 525cde9..4d3a253 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -1,4 +1,10 @@ -import { ASTNode, DocumentNode, FragmentDefinitionNode, VariableDefinitionNode } from 'graphql'; +import { + ASTNode, + DocumentNode, + FragmentDefinitionNode, Kind, + ObjectValueNode, ValueNode, + VariableDefinitionNode +} from 'graphql'; import { pushToArrayAtKey } from './utils'; const ignoreKeys = new Set(['loc']); @@ -294,3 +300,24 @@ export const rewriteResultsAtPath = ( return newResults; }; + +export const getByPath = (node: ObjectValueNode, path: ReadonlyArray): ValueNode | undefined => { + const [firstSegment, ...theRestOfSegments] = path; + + if (!firstSegment) return undefined; + + const theField = node.fields + .find(field => field.name.value === firstSegment); + + if (!theField) return undefined; + + if (theRestOfSegments.length === 0) { + return theField.value; + } + + if (theField.value.kind === Kind.OBJECT) { + return getByPath(theField.value, theRestOfSegments) + } + + return undefined; +} diff --git a/src/rewriters/FieldArgTypeRewriter.ts b/src/rewriters/FieldArgTypeRewriter.ts index dbac77a..4970146 100644 --- a/src/rewriters/FieldArgTypeRewriter.ts +++ b/src/rewriters/FieldArgTypeRewriter.ts @@ -7,15 +7,15 @@ import { parseType, TypeNode, ValueNode, - VariableNode } from 'graphql'; import Maybe from 'graphql/tsutils/Maybe'; -import { NodeAndVarDefs, nodesMatch } from '../ast'; +import { getByPath, NodeAndVarDefs, nodesMatch } from '../ast'; import { identifyFunc } from '../utils'; import Rewriter, { RewriterOpts, Variables } from './Rewriter'; interface FieldArgTypeRewriterOpts extends RewriterOpts { argName: string; + objectPath?: ReadonlyArray; oldType: string; newType: string; coerceVariable?: (variable: any, context: { variables: Variables; args: ArgumentNode[] }) => any; @@ -37,6 +37,7 @@ interface FieldArgTypeRewriterOpts extends RewriterOpts { */ class FieldArgTypeRewriter extends Rewriter { protected argName: string; + protected objectPath: ReadonlyArray; protected oldTypeNode: TypeNode; protected newTypeNode: TypeNode; // Passes context with rest of arguments and variables. @@ -56,6 +57,7 @@ class FieldArgTypeRewriter extends Rewriter { constructor(options: FieldArgTypeRewriterOpts) { super(options); this.argName = options.argName; + this.objectPath = options.objectPath || []; this.oldTypeNode = parseType(options.oldType); this.newTypeNode = parseType(options.newType); this.coerceVariable = options.coerceVariable || identifyFunc; @@ -154,8 +156,17 @@ class FieldArgTypeRewriter extends Rewriter { const matchingArgument = (node.arguments || []).find( arg => arg.name.value === this.argName ) as ArgumentNode; - const variableNode = matchingArgument.value as VariableNode; - return variableNode.kind === Kind.VARIABLE && variableNode.name.value; + + const valueNode = + this.objectPath && matchingArgument.value.kind === Kind.OBJECT + ? getByPath(matchingArgument.value, this.objectPath) + : matchingArgument.value; + + if (!valueNode) { + return false; + } + + return valueNode.kind === Kind.VARIABLE && valueNode.name.value; } } diff --git a/test/functional/rewriteFieldArgType.test.ts b/test/functional/rewriteFieldArgType.test.ts index af47e1c..f65df96 100644 --- a/test/functional/rewriteFieldArgType.test.ts +++ b/test/functional/rewriteFieldArgType.test.ts @@ -275,4 +275,90 @@ describe('Rewrite field arg type', () => { } }); }); + + it('recognizes and considers object path in nested objects', () => { + const handler = new RewriteHandler([ + new FieldArgTypeRewriter({ + fieldName: 'things', + argName: 'input', + objectPath: ['identifier', 'level_2', 'level_3'], + oldType: 'String!', + newType: 'Int!', + }) + ]); + + const query = gqlFmt` + mutation doTheThings($arg1: String!, $arg2: Int!, $arg3: String!) { + things(input: {identifier: {level_2: {level_3: $arg1}}, otherArg: $arg2}) { + cat + dog { + catdog + } + } + otherThing(arg3: $arg3) { + otherThingField + } + } + `; + const expectedRewritenQuery = gqlFmt` + mutation doTheThings($arg1: Int!, $arg2: Int!, $arg3: String!) { + things(input: {identifier: {level_2: {level_3: $arg1}}, otherArg: $arg2}) { + cat + dog { + catdog + } + } + otherThing(arg3: $arg3) { + otherThingField + } + } + `; + expect(handler.rewriteRequest(query)).toEqual({ + query: expectedRewritenQuery, + variables: undefined + }); + }); + + it('recognizes and considers single key path in an object', () => { + const handler = new RewriteHandler([ + new FieldArgTypeRewriter({ + fieldName: 'things', + argName: 'input', + objectPath: ['identifier'], + oldType: 'String!', + newType: 'Int!', + }) + ]); + + const query = gqlFmt` + mutation doTheThings($arg1: String!, $arg2: Int!, $arg3: String!) { + things(input: {identifier: $arg1, otherArg: $arg2}) { + cat + dog { + catdog + } + } + otherThing(arg3: $arg3) { + otherThingField + } + } + `; + const expectedRewritenQuery = gqlFmt` + mutation doTheThings($arg1: Int!, $arg2: Int!, $arg3: String!) { + things(input: {identifier: $arg1, otherArg: $arg2}) { + cat + dog { + catdog + } + } + otherThing(arg3: $arg3) { + otherThingField + } + } + `; + expect(handler.rewriteRequest(query)).toEqual({ + query: expectedRewritenQuery, + variables: undefined + }); + }); });