Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
108 changes: 101 additions & 7 deletions src/common.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
import {
FieldNode,
parse,
visit,
print,
} from 'graphql';
import _ from 'lodash';

const FORWARD_RESPONSE_HEADERS = [
Expand Down Expand Up @@ -35,19 +41,19 @@
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) {
Expand All @@ -71,3 +77,91 @@
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<string, string> = {}): 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<string, any> = {};
Object.keys(obj).forEach((key) => {
const newKey = keyMap[key] || key;
mapped[newKey] = mapKeysDeep(obj[key], keyMap);
});
return mapped;
}
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<string, string> | undefined,
): Record<string, string> {
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);

Check warning on line 129 in src/common.ts

View workflow job for this annotation

GitHub Actions / build (16.x)

Unexpected console statement
return parsed;
} catch (error) {
console.error('[Rise] Failed to parse responseKeyFormat as JSON:', error);

Check warning on line 132 in src/common.ts

View workflow job for this annotation

GitHub Actions / build (16.x)

Unexpected console statement
console.error('[Rise] Invalid responseKeyFormat:', responseKeyFormat);

Check warning on line 133 in src/common.ts

View workflow job for this annotation

GitHub Actions / build (16.x)

Unexpected console statement
return {};
}
}
return {};
}

export function reverseKeyValue(obj: Record<string, string | number>): Record<string, string> {
const reversed: Record<string, string> = {};
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);

Check warning on line 165 in src/common.ts

View workflow job for this annotation

GitHub Actions / build (16.x)

Unexpected console statement
return print(updatedAst);
}
33 changes: 27 additions & 6 deletions src/gql-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@
import { GraphQLFieldConfig } from 'graphql';
import { print } from 'graphql/language/printer';
import _ from 'lodash';
import { RiseDirectiveOptions, getReqHeaders, processResHeaders } from './common';
import {
RiseDirectiveOptions,
getReqHeaders,
mapKeysDeep,
parseResponseKeyFormat,
processResHeaders,
renameFieldsInQuery,
reverseKeyValue,
} from './common';
import { generateBodyFromTemplate } from './rest-resolver';

export interface RiseDirectiveOptionsGql extends RiseDirectiveOptions {
Expand Down Expand Up @@ -76,9 +84,13 @@
fieldConfig: GraphQLFieldConfig<any, any, any>,
) {
const url = options.baseURL;
let { argwrapper, gqlVariables } = riseDirective;
let { argwrapper, gqlVariables, responseKeyFormat } = riseDirective;

console.debug('[Rise] GQL - Response key format', responseKeyFormat);

Check warning on line 89 in src/gql-resolver.ts

View workflow job for this annotation

GitHub Actions / build (16.x)

Unexpected console statement
const keyMap = parseResponseKeyFormat(responseKeyFormat);
const reverseKeyMap = reverseKeyValue(keyMap);

fieldConfig.resolve = (source, args, context, info) => {

Check warning on line 93 in src/gql-resolver.ts

View workflow job for this annotation

GitHub Actions / build (16.x)

Assignment to property of function parameter 'fieldConfig'
let urlToFetch = url;
let originalContext = context;
let query = print(info.operation);
Expand All @@ -89,16 +101,24 @@
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);

Check warning on line 107 in src/gql-resolver.ts

View workflow job for this annotation

GitHub Actions / build (16.x)

Unexpected console statement
if (Object.keys(reverseKeyMap).length > 0) {
query = renameFieldsInQuery(query, reverseKeyMap);
}
console.debug('[Rise] GQL - Query', query);

Check warning on line 111 in src/gql-resolver.ts

View workflow job for this annotation

GitHub Actions / build (16.x)

Unexpected console statement

let body = JSON.stringify({
query,
variables: wrappingObject ? { [wrappingObject]: variables } : variables,
});
console.debug('[Rise] GQL request body:', body);

Check warning on line 117 in src/gql-resolver.ts

View workflow job for this annotation

GitHub Actions / build (16.x)

Unexpected console statement

const reqHeaders = getReqHeaders(riseDirective, options, originalContext);

console.debug('[Rise] GQL - Downstream URL and operation', urlToFetch, info.fieldName);

Check warning on line 121 in src/gql-resolver.ts

View workflow job for this annotation

GitHub Actions / build (16.x)

Unexpected console statement
return fetch(urlToFetch, {
method: 'POST',
headers: reqHeaders,
Expand All @@ -117,8 +137,9 @@
response.errors,
);
}

return response.data[info.fieldName];
});
return response;
})
.then((data) => mapKeysDeep(data, keyMap))
.then((response) => response.data[info.fieldName]);
};
}
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading