Merge pull request #4661 from devendra-bruno/fix/gql-introspection-variable-interpolation

Added combined Vars for prepareGqlIntrospectionRequest for all interp…
This commit is contained in:
lohit
2025-06-05 18:05:45 +05:30
committed by GitHub
5 changed files with 526 additions and 90 deletions

View File

@@ -7,8 +7,10 @@ import Dropdown from '../../Dropdown';
const GraphQLSchemaActions = ({ item, collection, onSchemaLoad, toggleDocs }) => {
const url = item.draft ? get(item, 'draft.request.url', '') : get(item, 'request.url', '');
const pathname = item.draft ? get(item, 'draft.pathname', '') : get(item, 'pathname', '');
const uid = item.draft ? get(item, 'draft.uid', '') : get(item, 'uid', '');
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
const request = item.draft ? item.draft.request : item.request;
const request = item.draft ? { ...item.draft.request, pathname, uid } : { ...item.request, pathname, uid };
let {
schema,

View File

@@ -9,7 +9,7 @@ const contentDispositionParser = require('content-disposition');
const mime = require('mime-types');
const FormData = require('form-data');
const { ipcMain } = require('electron');
const { each, get, extend, cloneDeep } = require('lodash');
const { each, get, extend, cloneDeep, merge } = require('lodash');
const { NtlmClient } = require('axios-ntlm');
const { VarsRuntime, AssertRuntime, ScriptRuntime, TestRuntime } = require('@usebruno/js');
const { interpolateString } = require('./interpolate-string');
@@ -24,7 +24,7 @@ const { uuid, safeStringifyJSON, safeParseJSON, parseDataFromResponse, parseData
const { chooseFileToSave, writeBinaryFile, writeFile } = require('../../utils/filesystem');
const { addCookieToJar, getDomainsWithCookies, getCookieStringForUrl } = require('../../utils/cookies');
const { createFormData } = require('../../utils/form-data');
const { findItemInCollectionByPathname, sortFolder, getAllRequestsInFolderRecursively, getEnvVars } = require('../../utils/collection');
const { findItemInCollectionByPathname, sortFolder, getAllRequestsInFolderRecursively, getEnvVars, getTreePathFromCollectionToItem, mergeVars } = require('../../utils/collection');
const { getOAuth2TokenUsingAuthorizationCode, getOAuth2TokenUsingClientCredentials, getOAuth2TokenUsingPasswordCredentials } = require('../../utils/oauth2');
const { preferencesUtil } = require('../../store/preferences');
const { getProcessEnvVars } = require('../../store/process-env');
@@ -317,6 +317,77 @@ const configureRequest = async (
return axiosInstance;
};
const fetchGqlSchemaHandler = async (event, endpoint, environment, _request, collection) => {
try {
const requestTreePath = getTreePathFromCollectionToItem(collection, _request);
// Create a clone of the request to avoid mutating the original
const resolvedRequest = cloneDeep(_request);
// mergeVars modifies the request in place, but we'll assign it to ensure consistency
mergeVars(collection, resolvedRequest, requestTreePath);
const envVars = getEnvVars(environment);
const globalEnvironmentVars = collection.globalEnvironmentVariables;
const folderVars = resolvedRequest.folderVariables;
const requestVariables = resolvedRequest.requestVariables;
const collectionVariables = resolvedRequest.collectionVariables;
const runtimeVars = collection.runtimeVariables;
// Precedence: runtimeVars > requestVariables > folderVars > envVars > collectionVariables > globalEnvironmentVars
const resolvedVars = merge(
{},
globalEnvironmentVars,
collectionVariables,
envVars,
folderVars,
requestVariables,
runtimeVars
);
const collectionRoot = get(collection, 'root', {});
const request = prepareGqlIntrospectionRequest(endpoint, resolvedVars, _request, collectionRoot);
request.timeout = preferencesUtil.getRequestTimeout();
if (!preferencesUtil.shouldVerifyTls()) {
request.httpsAgent = new https.Agent({
rejectUnauthorized: false
});
}
const collectionPath = collection.pathname;
const processEnvVars = getProcessEnvVars(collection.uid);
const axiosInstance = await configureRequest(
collection.uid,
request,
envVars,
collection.runtimeVariables,
processEnvVars,
collectionPath
);
const response = await axiosInstance(request);
return {
status: response.status,
statusText: response.statusText,
headers: response.headers,
data: response.data
};
} catch (error) {
if (error.response) {
return {
status: error.response.status,
statusText: error.response.statusText,
headers: error.response.headers,
data: error.response.data
};
}
return Promise.reject(error);
}
};
const registerNetworkIpc = (mainWindow) => {
const onConsoleLog = (type, args) => {
console[type](...args);
@@ -804,84 +875,8 @@ const registerNetworkIpc = (mainWindow) => {
});
});
ipcMain.handle('fetch-gql-schema', async (event, endpoint, environment, _request, collection) => {
try {
const envVars = getEnvVars(environment);
const collectionRoot = get(collection, 'root', {});
const request = prepareGqlIntrospectionRequest(endpoint, envVars, _request, collectionRoot);
request.timeout = preferencesUtil.getRequestTimeout();
if (!preferencesUtil.shouldVerifyTls()) {
request.httpsAgent = new https.Agent({
rejectUnauthorized: false
});
}
const requestUid = uuid();
const collectionPath = collection.pathname;
const collectionUid = collection.uid;
const runtimeVariables = collection.runtimeVariables;
const processEnvVars = getProcessEnvVars(collectionUid);
const brunoConfig = getBrunoConfig(collection.uid);
const scriptingConfig = get(brunoConfig, 'scripts', {});
scriptingConfig.runtime = getJsSandboxRuntime(collection);
await runPreRequest(
request,
requestUid,
envVars,
collectionPath,
collection,
collectionUid,
runtimeVariables,
processEnvVars,
scriptingConfig
);
interpolateVars(request, envVars, collection.runtimeVariables, processEnvVars);
const axiosInstance = await configureRequest(
collection.uid,
request,
envVars,
collection.runtimeVariables,
processEnvVars,
collectionPath
);
const response = await axiosInstance(request);
await runPostResponse(
request,
response,
requestUid,
envVars,
collectionPath,
collection,
collectionUid,
runtimeVariables,
processEnvVars,
scriptingConfig
);
return {
status: response.status,
statusText: response.statusText,
headers: response.headers,
data: response.data
};
} catch (error) {
if (error.response) {
return {
status: error.response.status,
statusText: error.response.statusText,
headers: error.response.headers,
data: error.response.data
};
}
return Promise.reject(error);
}
});
// handler for fetch-gql-schema
ipcMain.handle('fetch-gql-schema', fetchGqlSchemaHandler)
ipcMain.handle(
'renderer:run-collection-folder',
@@ -1342,3 +1337,4 @@ const registerNetworkIpc = (mainWindow) => {
module.exports = registerNetworkIpc;
module.exports.configureRequest = configureRequest;
module.exports.getCertsAndProxyConfig = getCertsAndProxyConfig;
module.exports.fetchGqlSchemaHandler = fetchGqlSchemaHandler;

View File

@@ -3,9 +3,9 @@ const { interpolate } = require('@usebruno/common');
const { getIntrospectionQuery } = require('graphql');
const { setAuthHeaders } = require('./prepare-request');
const prepareGqlIntrospectionRequest = (endpoint, envVars, request, collectionRoot) => {
const prepareGqlIntrospectionRequest = (endpoint, resolvedVars, request, collectionRoot) => {
if (endpoint && endpoint.length) {
endpoint = interpolate(endpoint, envVars);
endpoint = interpolate(endpoint, resolvedVars);
}
const queryParams = {
@@ -16,7 +16,7 @@ const prepareGqlIntrospectionRequest = (endpoint, envVars, request, collectionRo
method: 'POST',
url: endpoint,
headers: {
...mapHeaders(request.headers, get(collectionRoot, 'request.headers', [])),
...mapHeaders(request.headers, get(collectionRoot, 'request.headers', []), resolvedVars),
Accept: 'application/json',
'Content-Type': 'application/json'
},
@@ -26,19 +26,20 @@ const prepareGqlIntrospectionRequest = (endpoint, envVars, request, collectionRo
return setAuthHeaders(axiosRequest, request, collectionRoot);
};
const mapHeaders = (requestHeaders, collectionHeaders) => {
const mapHeaders = (requestHeaders, collectionHeaders, resolvedVars) => {
const headers = {};
each(requestHeaders, (h) => {
// Add collection headers first
each(collectionHeaders, (h) => {
if (h.enabled) {
headers[h.name] = h.value;
headers[h.name] = interpolate(h.value, resolvedVars);
}
});
// collection headers
each(collectionHeaders, (h) => {
// Then add request headers, which will overwrite if names overlap
each(requestHeaders, (h) => {
if (h.enabled) {
headers[h.name] = h.value;
headers[h.name] = interpolate(h.value, resolvedVars);
}
});

View File

@@ -0,0 +1,371 @@
const prepareGqlIntrospectionRequest = require('../../src/ipc/network/prepare-gql-introspection-request');
const { fetchGqlSchemaHandler } = require('../../src/ipc/network');
// Mock only the prepare-gql-introspection-request to avoid network calls
jest.mock('../../src/ipc/network/prepare-gql-introspection-request', () => {
return jest.fn().mockImplementation((endpoint, vars, request, root) => {
return {
url: endpoint,
method: 'POST',
headers: request?.headers || {},
data: {
query: '{ __schema { types { name } } }'
}
};
});
});
describe('fetchGqlSchemaHandler - variable precedence', () => {
beforeEach(() => {
jest.clearAllMocks();
});
afterEach(() => {
jest.restoreAllMocks();
});
it('should override global environment variables with environment variables', async () => {
const endpoint = 'https://example.com/';
const environment = {
variables: [
{ name: 'SHARED_VAR', value: 'env-value', enabled: true }
]
};
const request = {
uid: 'test-request',
vars: {
req: [] // No request variables
}
};
const collection = {
uid: 'test-collection',
pathname: '/test',
runtimeVariables: {},
globalEnvironmentVariables: {
SHARED_VAR: 'global-value'
},
items: [
{
uid: 'test-request',
request: {
vars: {
req: [] // No request variables
}
}
}
],
root: {
request: {
headers: [],
vars: {
req: [] // No collection variables
}
}
}
};
await fetchGqlSchemaHandler(null, endpoint, environment, request, collection);
expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith(
endpoint,
expect.objectContaining({
SHARED_VAR: 'env-value'
}),
request,
collection.root
);
});
it('should override environment variables with folder-level variables', async () => {
const endpoint = 'https://example.com/';
const environment = {
variables: [
{ name: 'SHARED_VAR', value: 'env-value', enabled: true }
]
};
const request = {
uid: 'test-request',
vars: {
req: [] // No request variables
}
};
const collection = {
uid: 'test-collection',
pathname: '/test',
runtimeVariables: {},
globalEnvironmentVariables: {},
items: [
{
uid: 'test-folder',
type: 'folder',
root: {
request: {
vars: {
req: [
{ name: 'SHARED_VAR', value: 'folder-value', enabled: true }
]
}
}
},
items: [
{
uid: 'test-request',
request: {
vars: {
req: [] // No request variables
}
}
}
]
}
],
root: {
request: {
headers: [],
vars: {
req: [] // No collection variables
}
}
}
};
await fetchGqlSchemaHandler(null, endpoint, environment, request, collection);
expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith(
endpoint,
expect.objectContaining({
SHARED_VAR: 'folder-value'
}),
request,
collection.root
);
});
it('should override folder-level variables with request variables', async () => {
const endpoint = 'https://example.com/';
const environment = {
variables: []
};
const request = {
uid: 'test-request',
vars: {
req: [
{ name: 'SHARED_VAR', value: 'request-value', enabled: true }
]
}
};
const collection = {
uid: 'test-collection',
pathname: '/test',
runtimeVariables: {},
globalEnvironmentVariables: {},
items: [
{
uid: 'test-folder',
type: 'folder',
root: {
request: {
vars: {
req: [
{ name: 'SHARED_VAR', value: 'folder-value', enabled: true }
]
}
}
},
items: [
{
uid: 'test-request',
request: {
vars: {
req: [
{ name: 'SHARED_VAR', value: 'request-value', enabled: true }
]
}
}
}
]
}
],
root: {
request: {
headers: [],
vars: {
req: [] // No collection variables
}
}
}
};
await fetchGqlSchemaHandler(null, endpoint, environment, request, collection);
expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith(
endpoint,
expect.objectContaining({
SHARED_VAR: 'request-value'
}),
request,
collection.root
);
});
it('should override global environment variables with collection variables', async () => {
const endpoint = 'https://example.com/';
const environment = {
variables: []
};
const request = {
uid: 'test-request',
vars: {
req: [] // No request variables
}
};
const collection = {
uid: 'test-collection',
pathname: '/test',
runtimeVariables: {},
globalEnvironmentVariables: {
SHARED_VAR: 'global-value'
},
items: [
{
uid: 'test-request',
request: {
vars: {
req: [] // No request variables
}
}
}
],
root: {
request: {
headers: [],
vars: {
req: [
{ name: 'SHARED_VAR', value: 'collection-value', enabled: true }
]
}
}
}
};
await fetchGqlSchemaHandler(null, endpoint, environment, request, collection);
expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith(
endpoint,
expect.objectContaining({
SHARED_VAR: 'collection-value'
}),
request,
collection.root
);
});
it('should override collection variables with environment variables', async () => {
const endpoint = 'https://example.com/';
const environment = {
variables: [
{ name: 'SHARED_VAR', value: 'env-value', enabled: true }
]
};
const request = {
uid: 'test-request',
vars: {
req: [] // No request variables
}
};
const collection = {
uid: 'test-collection',
pathname: '/test',
runtimeVariables: {},
globalEnvironmentVariables: {},
items: [
{
uid: 'test-request',
request: {
vars: {
req: [] // No request variables
}
}
}
],
root: {
request: {
headers: [],
vars: {
req: [
{ name: 'SHARED_VAR', value: 'collection-value', enabled: true }
]
}
}
}
};
await fetchGqlSchemaHandler(null, endpoint, environment, request, collection);
expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith(
endpoint,
expect.objectContaining({
SHARED_VAR: 'env-value'
}),
request,
collection.root
);
});
it('should override request variables with runtime variables', async () => {
const endpoint = 'https://example.com/';
const environment = {
variables: []
};
const request = {
uid: 'test-request',
vars: {
req: [
{ name: 'SHARED_VAR', value: 'request-value', enabled: true }
]
}
};
const collection = {
uid: 'test-collection',
pathname: '/test',
runtimeVariables: {
SHARED_VAR: 'runtime-value'
},
items: [
{
uid: 'test-request',
request: {
vars: {
req: [
{ name: 'SHARED_VAR', value: 'request-value', enabled: true }
]
}
}
}
],
root: {
request: {
headers: [],
vars: {
req: [] // No collection variables
}
}
}
};
await fetchGqlSchemaHandler(null, endpoint, environment, request, collection);
expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith(
endpoint,
expect.objectContaining({
SHARED_VAR: 'runtime-value'
}),
request,
collection.root
);
})
});

View File

@@ -0,0 +1,66 @@
const prepareGqlIntrospectionRequest = require('../../src/ipc/network/prepare-gql-introspection-request');
describe('prepareGqlIntrospectionRequest', () => {
const createBasicSetup = () => ({
endpoint: 'https://example.com/',
request: {
headers: []
},
collectionRoot: {
request: {
headers: []
}
}
});
it('should handle environment variables in headers', () => {
const setup = createBasicSetup();
setup.request.headers = [
{ name: 'Authorization', value: 'Bearer {{AUTH_TOKEN}}', enabled: true }
];
const vars = {
AUTH_TOKEN: 'token-value'
};
const result = prepareGqlIntrospectionRequest(setup.endpoint, vars, setup.request, setup.collectionRoot);
expect(result.headers['Authorization']).toBe('Bearer token-value');
expect(result.method).toBe('POST');
expect(result.url).toBe(setup.endpoint);
});
it('should override collection headers with request headers', () => {
const setup = createBasicSetup();
setup.collectionRoot.request.headers = [
{ name: 'X-Header', value: 'collection-value', enabled: true }
];
setup.request.headers = [
{ name: 'X-Header', value: 'request-value', enabled: true }
];
const result = prepareGqlIntrospectionRequest(setup.endpoint, {}, setup.request, setup.collectionRoot);
expect(result.headers['X-Header']).toBe('request-value');
});
it('should handle enabled and disabled headers', () => {
const setup = createBasicSetup();
setup.request.headers = [
{ name: 'X-Enabled', value: 'enabled', enabled: true },
{ name: 'X-Disabled', value: 'disabled', enabled: false }
];
const result = prepareGqlIntrospectionRequest(setup.endpoint, {}, setup.request, setup.collectionRoot);
expect(result.headers['X-Enabled']).toBe('enabled');
expect(result.headers['X-Disabled']).toBeUndefined();
});
it('should always include required GraphQL headers', () => {
const setup = createBasicSetup();
const result = prepareGqlIntrospectionRequest(setup.endpoint, {}, setup.request, setup.collectionRoot);
expect(result.headers['Accept']).toBe('application/json');
expect(result.headers['Content-Type']).toBe('application/json');
});
});