From 80261f10936fad15593880152436d60b62a06a90 Mon Sep 17 00:00:00 2001 From: Sagar Patni Date: Fri, 11 Jul 2025 16:38:03 -0700 Subject: [PATCH 1/5] ResponseFormat to gql directive ResponseFormat is used to map string keys in response json --- package.json | 2 +- src/gql-resolver.ts | 41 +++++++++++++++++++++++++++++++++---- src/index.ts | 2 +- test/index.ts | 50 +++++++++++++++++++++++++++++++++++++++++++++ test/schema.graphql | 23 +++++++++++++++++++++ 5 files changed, 112 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 8cbef05..ab4f497 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@thoughtspot/rise", - "version": "0.7.17", + "version": "0.7.18", "description": "Rise above the REST with GraphQL", "main": "dist/index.js", "scripts": { diff --git a/src/gql-resolver.ts b/src/gql-resolver.ts index 92b4ac4..5d2d531 100644 --- a/src/gql-resolver.ts +++ b/src/gql-resolver.ts @@ -70,13 +70,28 @@ function wrapArgumentsInGql(query = '', info, argwrapper) { return query; } +function mapKeysDeep(obj: any, keyMap: Record = {}): any { + if (Array.isArray(obj)) { + return obj.map((item) => mapKeysDeep(item, keyMap)); + } + if (obj !== null && typeof obj === 'object') { + const mapped: Record = {}; + Object.keys(obj).forEach((key) => { + const newKey = keyMap[key] || key; + mapped[newKey] = mapKeysDeep(obj[key], keyMap); + }); + return mapped; + } + return obj; +} + export function gqlResolver( riseDirective, options: RiseDirectiveOptionsGql, fieldConfig: GraphQLFieldConfig, ) { const url = options.baseURL; - let { argwrapper, gqlVariables } = riseDirective; + let { argwrapper, gqlVariables, responseKeyFormat } = riseDirective; fieldConfig.resolve = (source, args, context, info) => { let urlToFetch = url; @@ -89,7 +104,12 @@ export function gqlResolver( query = wrapArgumentsInGql(query, info, argwrapper); } - const variables = gqlVariables ? generateBodyFromTemplate(gqlVariables, args) : info.variableValues; + const variables = gqlVariables + ? generateBodyFromTemplate(gqlVariables, args) + : info.variableValues; + + console.debug('[Rise] GQL - Variables', variables); + let body = JSON.stringify({ query, variables: wrappingObject ? { [wrappingObject]: variables } : variables, @@ -104,9 +124,22 @@ export function gqlResolver( headers: reqHeaders, body, }) - .then((response) => { + .then(async (response) => { processResHeaders(response, originalContext); - return response.json(); + const data = await response.json(); + if (responseKeyFormat) { + console.debug('[Rise] GQL - Response key format', responseKeyFormat); + let keyMap = {}; + if (typeof responseKeyFormat === 'string') { + try { + keyMap = JSON.parse(responseKeyFormat); + } catch (error) { + console.error('[Rise] GQL - Failed to parse responseKeyFormat', error); + } + } + return mapKeysDeep(data, keyMap); + } + return data; }) .then((response) => { if (response.errors) { diff --git a/src/index.ts b/src/index.ts index ae0a92e..8bebe23 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,7 +21,7 @@ export const getGqlRiseDirectiveTypeDefs = (name: string) => ` name: String! type: String! } - directive @${name}(argwrapper: RiseGQLArgWrapper, gqlVariables: String) on FIELD_DEFINITION + directive @${name}(argwrapper: RiseGQLArgWrapper, gqlVariables: String, responseKeyFormat: String) on FIELD_DEFINITION `; type RiseDirectiveOptions = RiseDirectiveOptionsRest | RiseDirectiveOptionsGql; diff --git a/test/index.ts b/test/index.ts index dcb7656..662d6a4 100644 --- a/test/index.ts +++ b/test/index.ts @@ -664,6 +664,56 @@ describe('Should handle gql type', () => { }); }); + test('when gql query with responseKeyFormat is executed should return data as expected', () => { + nock(GQL_BASE_URL, { + }) + .post('') + .reply(200, (...args) => ({ + data: { + getGQLSessionDetailsWithResponseKeyFormat: { + id: '123', + name: 'John', + email: 'john@doe.com', + extra: 'extra', + }, + }, + })); + + const contextValue = { + req: { + headers: { + Authorization: 'Bearer 123', + cookie: 'a=a', + foo: 'bar', + }, + }, + }; + + return graphql({ + schema, + source: ` + query getSession($sessionId: String, $asd: String) { + getGQLSessionDetailsWithResponseKeyFormat(sessionId: $sessionId, asd: $asd) { + name_test + email + id + } + } + `, + contextValue, + variableValues: { + sessionId: '1234', + asd: 'abc' + } + }).then((response: any) => { + expect(response?.data?.getGQLSessionDetailsWithResponseKeyFormat).toBeDefined(); + expect(response?.data?.getGQLSessionDetailsWithResponseKeyFormat).toMatchObject({ + name_test: 'John', + id: '123', + }); + }); + }); + test('when there is a error in gql query, the error should be responded back', () => { nock(GQL_BASE_URL, { }) diff --git a/test/schema.graphql b/test/schema.graphql index 3a99cae..abd4ec9 100644 --- a/test/schema.graphql +++ b/test/schema.graphql @@ -29,6 +29,24 @@ ) } + type UserResponseKeyFormat { + name_test: String, + email: String! + id: String + account_type: String, + currrent_org: Org, + privileges: [String], + tenant_id: String, + user_groups: [UserGroup], + display_name: String, + key: String + history(from: String): [String] + @service( + path: "/v2/users/$identifier/history?from=$from", + method: "GET" + ) + } + type TokenResponse { token: String!, valid_for_userid: String!, @@ -82,6 +100,11 @@ "asd": "<%= args.asd %>" } """) + getGQLSessionDetailsWithResponseKeyFormat (sessionId: String, asd: String): UserResponseKeyFormat! @gqlrise(responseKeyFormat: """ + { + "name": "name_test" + } + """) } input ParamInput { From 32eb3a5ec980e6947e2c4c333c917b3dc7fdd38c Mon Sep 17 00:00:00 2001 From: Sagar Patni Date: Fri, 11 Jul 2025 18:48:56 -0700 Subject: [PATCH 2/5] more UT's --- test/index.ts | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/test/index.ts b/test/index.ts index 662d6a4..fd581cb 100644 --- a/test/index.ts +++ b/test/index.ts @@ -8,6 +8,24 @@ import fs from 'fs'; import path from 'path'; import { rise } from '../src/index'; +// Import the function we want to test +// Note: Since mapKeysDeep is not exported, we'll need to move it to a separate file or export it +// For now, let's create a local copy for testing +function mapKeysDeep(obj: any, keyMap: Record = {}): any { + if (Array.isArray(obj)) { + return obj.map((item) => mapKeysDeep(item, keyMap)); + } + if (obj !== null && typeof obj === 'object') { + const mapped: Record = {}; + Object.keys(obj).forEach((key) => { + const newKey = keyMap[key] || key; + mapped[newKey] = mapKeysDeep(obj[key], keyMap); + }); + return mapped; + } + return obj; +} + const typeDefs = fs.readFileSync(path.join(__dirname, './schema.graphql'), 'utf8'); class ApolloError extends Error { } @@ -51,6 +69,58 @@ afterAll(() => { const a = function () { return {}; }; + +describe('mapKeysDeep', () => { + test('should map keys according to keyMap', () => { + const input = { firstName: 'John', lastName: 'Doe', age: 30 }; + const keyMap = { firstName: 'first_name', lastName: 'last_name' }; + const expected = { first_name: 'John', last_name: 'Doe', age: 30 }; + + expect(mapKeysDeep(input, keyMap)).toEqual(expected); + }); + + test('should map keys in nested objects and arrays', () => { + const input = { + users: [ + { + firstName: 'John', + contact: { + phoneNumber: '123-456-7890' + } + }, + { + firstName: 'Jane', + contact: { + phoneNumber: '987-654-3210' + } + } + ] + }; + const keyMap = { + firstName: 'first_name', + phoneNumber: 'phone_number' + }; + const expected = { + users: [ + { + first_name: 'John', + contact: { + phone_number: '123-456-7890' + } + }, + { + first_name: 'Jane', + contact: { + phone_number: '987-654-3210' + } + } + ] + }; + + expect(mapKeysDeep(input, keyMap)).toEqual(expected); + }); +}); + describe('Should call the Target', () => { test('with the correct headers', async () => { nock('https://rise.com/callosum/v1', { From ae3a4441d33855eea4a193e46c0714606c934a0c Mon Sep 17 00:00:00 2001 From: Sagar Patni Date: Tue, 15 Jul 2025 14:10:10 -0700 Subject: [PATCH 3/5] review comments --- src/common.ts | 43 ++++++++++++++++++++++++++++------ src/gql-resolver.ts | 56 ++++++++++++++++++--------------------------- test/index.ts | 27 +++++++--------------- 3 files changed, 66 insertions(+), 60 deletions(-) diff --git a/src/common.ts b/src/common.ts index d194ad8..d13592c 100644 --- a/src/common.ts +++ b/src/common.ts @@ -35,19 +35,19 @@ export function getReqHeaders(riseDirective: RiseDirectiveOptions, options, cont headers = {}, contenttype = options.contenttype || 'application/json', forwardheaders = [], - } = riseDirective; + } = riseDirective; - headers = { + headers = { 'Content-Type': contenttype, ...options.headers, ...headers, - }; - forwardheaders.push(...options.forwardheaders); - forwardheaders = forwardheaders.map((h) => h.toLowerCase()); - return { + }; + forwardheaders.push(...options.forwardheaders); + forwardheaders = forwardheaders.map((h) => h.toLowerCase()); + return { ...headers, ..._.pickBy(context.req.headers, (v, h) => forwardheaders.includes(h.toLowerCase())), - }; + }; } export function processResHeaders(response, context) { @@ -71,3 +71,32 @@ export class RestError extends Error { this.errors = errors; } } + +// The function is used to map the keys of the object to the new keys +// By default, it will return the original object if the key is not in the keyMap +// Example: +// const obj = { a: 1, b: 2, c: 3 } +// const keyMap = { a: 'd', b: 'e', f: 'g' } +// const newObj = mapKeysDeep(obj, keyMap) +// newObj will be { d: 1, e: 2, c: 3 } +// The function will not modify the original object +export function mapKeysDeep(obj: any, keyMap: Record = {}): any { + // If keyMap is empty, return original object + if (!keyMap || Object.keys(keyMap).length === 0) { + return obj; + } + // If obj is an array, map each item in the array + if (Array.isArray(obj)) { + return obj.map((item) => mapKeysDeep(item, keyMap)); + } + // If obj is an object, map each key in the object + if (obj !== null && typeof obj === 'object') { + const mapped: Record = {}; + Object.keys(obj).forEach((key) => { + const newKey = keyMap[key] || key; + mapped[newKey] = mapKeysDeep(obj[key], keyMap); + }); + return mapped; + } + return obj; +} diff --git a/src/gql-resolver.ts b/src/gql-resolver.ts index 5d2d531..2a21a76 100644 --- a/src/gql-resolver.ts +++ b/src/gql-resolver.ts @@ -2,7 +2,12 @@ import fetch from 'node-fetch'; import { GraphQLFieldConfig } from 'graphql'; import { print } from 'graphql/language/printer'; import _ from 'lodash'; -import { RiseDirectiveOptions, getReqHeaders, processResHeaders } from './common'; +import { + RiseDirectiveOptions, + getReqHeaders, + mapKeysDeep, + processResHeaders, +} from './common'; import { generateBodyFromTemplate } from './rest-resolver'; export interface RiseDirectiveOptionsGql extends RiseDirectiveOptions { @@ -70,21 +75,6 @@ function wrapArgumentsInGql(query = '', info, argwrapper) { return query; } -function mapKeysDeep(obj: any, keyMap: Record = {}): any { - if (Array.isArray(obj)) { - return obj.map((item) => mapKeysDeep(item, keyMap)); - } - if (obj !== null && typeof obj === 'object') { - const mapped: Record = {}; - Object.keys(obj).forEach((key) => { - const newKey = keyMap[key] || key; - mapped[newKey] = mapKeysDeep(obj[key], keyMap); - }); - return mapped; - } - return obj; -} - export function gqlResolver( riseDirective, options: RiseDirectiveOptionsGql, @@ -93,6 +83,16 @@ export function gqlResolver( const url = options.baseURL; let { argwrapper, gqlVariables, responseKeyFormat } = riseDirective; + console.debug('[Rise] GQL - Response key format', responseKeyFormat); + let keyMap = {}; + if (typeof responseKeyFormat === 'string') { + try { + keyMap = JSON.parse(responseKeyFormat); + } catch (error) { + console.error('[Rise] GQL - Failed to parse responseKeyFormat', error); + } + } + fieldConfig.resolve = (source, args, context, info) => { let urlToFetch = url; let originalContext = context; @@ -124,22 +124,9 @@ export function gqlResolver( headers: reqHeaders, body, }) - .then(async (response) => { + .then((response) => { processResHeaders(response, originalContext); - const data = await response.json(); - if (responseKeyFormat) { - console.debug('[Rise] GQL - Response key format', responseKeyFormat); - let keyMap = {}; - if (typeof responseKeyFormat === 'string') { - try { - keyMap = JSON.parse(responseKeyFormat); - } catch (error) { - console.error('[Rise] GQL - Failed to parse responseKeyFormat', error); - } - } - return mapKeysDeep(data, keyMap); - } - return data; + return response.json(); }) .then((response) => { if (response.errors) { @@ -150,8 +137,9 @@ export function gqlResolver( response.errors, ); } - - return response.data[info.fieldName]; - }); + return response; + }) + .then((data) => mapKeysDeep(data, keyMap)) + .then((response) => response.data[info.fieldName]); }; } diff --git a/test/index.ts b/test/index.ts index fd581cb..4f00cac 100644 --- a/test/index.ts +++ b/test/index.ts @@ -7,24 +7,7 @@ import { makeExecutableSchema } from '@graphql-tools/schema'; import fs from 'fs'; import path from 'path'; import { rise } from '../src/index'; - -// Import the function we want to test -// Note: Since mapKeysDeep is not exported, we'll need to move it to a separate file or export it -// For now, let's create a local copy for testing -function mapKeysDeep(obj: any, keyMap: Record = {}): any { - if (Array.isArray(obj)) { - return obj.map((item) => mapKeysDeep(item, keyMap)); - } - if (obj !== null && typeof obj === 'object') { - const mapped: Record = {}; - Object.keys(obj).forEach((key) => { - const newKey = keyMap[key] || key; - mapped[newKey] = mapKeysDeep(obj[key], keyMap); - }); - return mapped; - } - return obj; -} +import { mapKeysDeep } from '../src/common'; const typeDefs = fs.readFileSync(path.join(__dirname, './schema.graphql'), 'utf8'); @@ -71,11 +54,17 @@ const a = function () { }; describe('mapKeysDeep', () => { + test('should return the original object if keyMap is empty', () => { + const input = { firstName: 'John', lastName: 'Doe', age: 30 }; + const keyMap = {}; + const expected = { firstName: 'John', lastName: 'Doe', age: 30 }; + expect(mapKeysDeep(input, keyMap)).toEqual(expected); + }); + test('should map keys according to keyMap', () => { const input = { firstName: 'John', lastName: 'Doe', age: 30 }; const keyMap = { firstName: 'first_name', lastName: 'last_name' }; const expected = { first_name: 'John', last_name: 'Doe', age: 30 }; - expect(mapKeysDeep(input, keyMap)).toEqual(expected); }); From 43e968a42489f3e24e0cd10262ded1be91a58aa4 Mon Sep 17 00:00:00 2001 From: Sagar Patni Date: Tue, 15 Jul 2025 15:03:32 -0700 Subject: [PATCH 4/5] review comments 2 --- src/common.ts | 30 ++++++++++++++++++++++++++++++ src/gql-resolver.ts | 10 ++-------- test/index.ts | 40 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 71 insertions(+), 9 deletions(-) diff --git a/src/common.ts b/src/common.ts index d13592c..64e11e6 100644 --- a/src/common.ts +++ b/src/common.ts @@ -100,3 +100,33 @@ export function mapKeysDeep(obj: any, keyMap: Record = {}): any } return obj; } + +/** + * Parses responseKeyFormat from string to object format + * @param responseKeyFormat - The response key format, can be string (JSON) or object + * @returns Parsed key mapping object or empty object if parsing fails + */ +export function parseResponseKeyFormat( + responseKeyFormat: string | Record | undefined, +): Record { + if (!responseKeyFormat) { + return {}; + } + // If it's already an object, return it directly + if (typeof responseKeyFormat === 'object') { + return responseKeyFormat; + } + // If it's a string, try to parse it as JSON + if (typeof responseKeyFormat === 'string') { + try { + const parsed = JSON.parse(responseKeyFormat); + console.debug('[Rise] Parsed responseKeyFormat:', parsed); + return parsed; + } catch (error) { + console.error('[Rise] Failed to parse responseKeyFormat as JSON:', error); + console.error('[Rise] Invalid responseKeyFormat:', responseKeyFormat); + return {}; + } + } + return {}; +} diff --git a/src/gql-resolver.ts b/src/gql-resolver.ts index 2a21a76..da2a0bf 100644 --- a/src/gql-resolver.ts +++ b/src/gql-resolver.ts @@ -6,6 +6,7 @@ import { RiseDirectiveOptions, getReqHeaders, mapKeysDeep, + parseResponseKeyFormat, processResHeaders, } from './common'; import { generateBodyFromTemplate } from './rest-resolver'; @@ -84,14 +85,7 @@ export function gqlResolver( let { argwrapper, gqlVariables, responseKeyFormat } = riseDirective; console.debug('[Rise] GQL - Response key format', responseKeyFormat); - let keyMap = {}; - if (typeof responseKeyFormat === 'string') { - try { - keyMap = JSON.parse(responseKeyFormat); - } catch (error) { - console.error('[Rise] GQL - Failed to parse responseKeyFormat', error); - } - } + const keyMap = parseResponseKeyFormat(responseKeyFormat); fieldConfig.resolve = (source, args, context, info) => { let urlToFetch = url; diff --git a/test/index.ts b/test/index.ts index 4f00cac..2b23187 100644 --- a/test/index.ts +++ b/test/index.ts @@ -7,7 +7,7 @@ import { makeExecutableSchema } from '@graphql-tools/schema'; import fs from 'fs'; import path from 'path'; import { rise } from '../src/index'; -import { mapKeysDeep } from '../src/common'; +import { mapKeysDeep, parseResponseKeyFormat } from '../src/common'; const typeDefs = fs.readFileSync(path.join(__dirname, './schema.graphql'), 'utf8'); @@ -110,6 +110,44 @@ describe('mapKeysDeep', () => { }); }); +describe('parseResponseKeyFormat', () => { + test('empty cases', () => { + const result = parseResponseKeyFormat(''); + expect(result).toEqual({}); + + const result2 = parseResponseKeyFormat(null as any); + expect(result2).toEqual({}); + + const result3 = parseResponseKeyFormat(undefined); + expect(result3).toEqual({}); + }); + + test('should parse complex nested JSON string correctly', () => { + const jsonString = '{"user": {"firstName": "first_name"}, "contact": {"phoneNumber": "phone_number"}}'; + const expected = { + user: { firstName: 'first_name' }, + contact: { phoneNumber: 'phone_number' } + }; + const result = parseResponseKeyFormat(jsonString); + expect(result).toEqual(expected); + + const jsonString2 = '{"user-name": "user_name", "email@domain": "email_domain"}'; + const expected2 = { 'user-name': 'user_name', 'email@domain': 'email_domain' }; + const result2 = parseResponseKeyFormat(jsonString2); + expect(result2).toEqual(expected2); + }); + + test('should return empty object for malformed JSON string', () => { + const malformedJson = 'not a json string'; + const result = parseResponseKeyFormat(malformedJson); + expect(result).toEqual({}); + + const syntaxErrorJson = '{"firstName" "first_name"}'; // Missing colon + const result2 = parseResponseKeyFormat(syntaxErrorJson); + expect(result2).toEqual({}); + }); +}); + describe('Should call the Target', () => { test('with the correct headers', async () => { nock('https://rise.com/callosum/v1', { From a8797dbfbca91de32a6eaf7240b4ae9001a23efb Mon Sep 17 00:00:00 2001 From: Sagar Patni Date: Tue, 15 Jul 2025 23:00:36 -0700 Subject: [PATCH 5/5] map query field --- src/common.ts | 35 +++++++++++++++ src/gql-resolver.ts | 8 +++- test/index.ts | 107 +++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 148 insertions(+), 2 deletions(-) diff --git a/src/common.ts b/src/common.ts index 64e11e6..cfb3d3c 100644 --- a/src/common.ts +++ b/src/common.ts @@ -1,3 +1,9 @@ +import { + FieldNode, + parse, + visit, + print, +} from 'graphql'; import _ from 'lodash'; const FORWARD_RESPONSE_HEADERS = [ @@ -130,3 +136,32 @@ export function parseResponseKeyFormat( } return {}; } + +export function reverseKeyValue(obj: Record): Record { + const reversed: Record = {}; + Object.keys(obj).forEach((key) => { + reversed[String(obj[key])] = key; + }); + return reversed; +} + +export function renameFieldsInQuery(query: string, renameMap: { [key: string]: string }): string { + const ast = parse(query); + const updatedAst = visit(ast, { + Field(node: FieldNode): FieldNode | undefined { + const newName = renameMap[node.name.value]; + if (newName) { + return { + ...node, + name: { + ...node.name, + value: newName, + }, + }; + } + return undefined; + }, + }); + console.debug('[Rise] Renamed fields in query:', updatedAst); + return print(updatedAst); +} diff --git a/src/gql-resolver.ts b/src/gql-resolver.ts index da2a0bf..46a8d17 100644 --- a/src/gql-resolver.ts +++ b/src/gql-resolver.ts @@ -8,6 +8,8 @@ import { mapKeysDeep, parseResponseKeyFormat, processResHeaders, + renameFieldsInQuery, + reverseKeyValue, } from './common'; import { generateBodyFromTemplate } from './rest-resolver'; @@ -86,6 +88,7 @@ export function gqlResolver( console.debug('[Rise] GQL - Response key format', responseKeyFormat); const keyMap = parseResponseKeyFormat(responseKeyFormat); + const reverseKeyMap = reverseKeyValue(keyMap); fieldConfig.resolve = (source, args, context, info) => { let urlToFetch = url; @@ -101,8 +104,11 @@ export function gqlResolver( const variables = gqlVariables ? generateBodyFromTemplate(gqlVariables, args) : info.variableValues; - console.debug('[Rise] GQL - Variables', variables); + if (Object.keys(reverseKeyMap).length > 0) { + query = renameFieldsInQuery(query, reverseKeyMap); + } + console.debug('[Rise] GQL - Query', query); let body = JSON.stringify({ query, diff --git a/test/index.ts b/test/index.ts index 2b23187..95694f7 100644 --- a/test/index.ts +++ b/test/index.ts @@ -7,7 +7,7 @@ import { makeExecutableSchema } from '@graphql-tools/schema'; import fs from 'fs'; import path from 'path'; import { rise } from '../src/index'; -import { mapKeysDeep, parseResponseKeyFormat } from '../src/common'; +import { mapKeysDeep, parseResponseKeyFormat, reverseKeyValue, renameFieldsInQuery } from '../src/common'; const typeDefs = fs.readFileSync(path.join(__dirname, './schema.graphql'), 'utf8'); @@ -1026,3 +1026,108 @@ describe('Parse data according to the content type', () => { }); }); }); + +describe('reverseKeyValue', () => { + test('should reverse key-value pairs with string values', () => { + const input = { name: 'John', city: 'NYC', country: 'USA' }; + const expected = { John: 'name', NYC: 'city', USA: 'country' }; + const result = reverseKeyValue(input); + expect(result).toEqual(expected); + }); + + test('should reverse key-value pairs with number values and convert them to strings', () => { + const input = { age: 25, score: 100, year: 2023 }; + const expected = { '25': 'age', '100': 'score', '2023': 'year' }; + const result = reverseKeyValue(input); + expect(result).toEqual(expected); + }); + + test('should handle empty object', () => { + const input = {}; + const expected = {}; + const result = reverseKeyValue(input); + expect(result).toEqual(expected); + }); +}); + +describe('renameFieldsInQuery', () => { + test('should comprehensively rename fields in GraphQL query covering all scenarios', () => { + // Complex query with multiple fields, nested structure, arguments, and aliases + const query = ` + query GetUserData($id: ID!) { + user(id: $id) { + userName + userEmail + profile { + firstName + lastName + address { + street + city + } + } + posts(limit: 10) { + postTitle + postContent + author { + userName + } + } + unchangedField + } + settings { + theme + notifications + } + } + `; + + // Rename map covering various scenarios + const renameMap = { + userName: 'name', // Basic field rename + userEmail: 'email', // Another basic field rename + firstName: 'first_name', // Nested field rename + lastName: 'last_name', // Another nested field rename + postTitle: 'title', // Field in array/list + postContent: 'content', // Another field in array/list + // Note: unchangedField, street, city, theme, notifications are not in map - should remain unchanged + }; + + const result = renameFieldsInQuery(query, renameMap); + + // Verify all specified fields are renamed + expect(result).toContain('name'); // userName -> name + expect(result).toContain('email'); // userEmail -> email + expect(result).toContain('first_name'); // firstName -> first_name + expect(result).toContain('last_name'); // lastName -> last_name + expect(result).toContain('title'); // postTitle -> title + expect(result).toContain('content'); // postContent -> content + + // Verify fields not in rename map remain unchanged + expect(result).toContain('unchangedField'); + expect(result).toContain('street'); + expect(result).toContain('city'); + expect(result).toContain('theme'); + expect(result).toContain('notifications'); + + // Verify old field names are no longer present + expect(result).not.toContain('userName'); + expect(result).not.toContain('userEmail'); + expect(result).not.toContain('firstName'); + expect(result).not.toContain('lastName'); + expect(result).not.toContain('postTitle'); + expect(result).not.toContain('postContent'); + + // Verify the query structure remains valid (contains key GraphQL elements) + expect(result).toContain('query GetUserData'); + expect(result).toContain('$id: ID!'); + expect(result).toContain('user(id: $id)'); + expect(result).toContain('posts(limit: 10)'); + + // Test edge case: empty rename map should return original query + const resultWithEmptyMap = renameFieldsInQuery(query, {}); + expect(resultWithEmptyMap).toContain('userName'); + expect(resultWithEmptyMap).toContain('userEmail'); + expect(resultWithEmptyMap).toContain('firstName'); + }); +});