fix: support response query filtering in safe mode (#7441)

* fix(js): support response query filters in safe mode

* feat: add tests and improve bruno-response shim for query argument handling

- Introduced a new test suite for the bruno-response shim to validate query filtering and function callback behavior.
- Refactored the `res` function to enhance argument handling, allowing for better marshalling of QuickJS function handles and other values.
- Improved error handling within the response processing to ensure robust behavior during query execution.

* fix: correct argument handling in bruno-response shim for function callbacks

- Updated the `toHostQueryArg` function to use `vm.undefined` instead of `vm.global` when calling the provided function argument. This change ensures proper context handling during function execution, improving the reliability of query argument processing.

* chore: clean up .gitignore and enhance error handling in bruno-response tests

- Removed redundant entries from .gitignore to streamline ignored files.
- Improved error handling in the `afterEach` and `afterAll` hooks of the bruno-response tests to ensure proper disposal of resources, preventing potential memory leaks.

---------

Co-authored-by: cryst <230207759+cryst-hq@users.noreply.github.com>
This commit is contained in:
Abhishek S Lal
2026-04-03 01:53:11 +05:30
committed by GitHub
parent 5c1dc1184a
commit 9465de02ee
2 changed files with 120 additions and 2 deletions

View File

@@ -1,8 +1,35 @@
const { marshallToVm } = require('../utils');
// Marshal a QuickJS query argument to a host-compatible value.
// Function handles are wrapped as native callbacks; other values are dumped as-is.
// Safe because @usebruno/query's get() invokes filters synchronously,
// so the borrowed arg handle is still valid.
const toHostQueryArg = (vm, arg) => {
if (vm.typeof(arg) === 'function') {
return (item) => {
const itemHandle = marshallToVm(item, vm);
const result = vm.callFunction(arg, vm.undefined, itemHandle);
itemHandle.dispose();
if (result.error) {
const error = vm.dump(result.error);
result.error.dispose();
throw error;
}
const value = vm.dump(result.value);
result.value.dispose();
return value;
};
}
return vm.dump(arg);
};
const addBrunoResponseShimToContext = (vm, res) => {
let resFn = vm.newFunction('res', function (exprStr) {
return marshallToVm(res(vm.dump(exprStr)), vm);
let resFn = vm.newFunction('res', function (exprStr, ...queryArgs) {
const nativeArgs = queryArgs.map((arg) => toHostQueryArg(vm, arg));
return marshallToVm(res(vm.dump(exprStr), ...nativeArgs), vm);
});
const status = marshallToVm(res?.status, vm);

View File

@@ -0,0 +1,91 @@
const { describe, it, expect, beforeAll, beforeEach, afterEach, afterAll } = require('@jest/globals');
const { newQuickJSWASMModule } = require('quickjs-emscripten');
const addBrunoResponseShimToContext = require('./bruno-response');
describe('bruno response shim', () => {
let vm, module;
beforeAll(async () => {
module = await newQuickJSWASMModule();
});
beforeEach(() => {
vm = module.newContext();
});
afterEach(() => {
if (vm) {
try {
vm.dispose();
} catch (err) {
console.error('Error disposing vm', err);
}
vm = null;
}
});
afterAll(() => {
if (module) {
try {
module.dispose();
} catch (err) {
console.error('Error disposing module', err);
}
module = null;
}
});
it('forwards response query filter callbacks in safe mode', () => {
const resources = [
{ id: 'test', value: 1 },
{ id: 'other', value: 2 }
];
const response = Object.assign(
(expr, filter) => {
expect(expr).toBe('resources[?].id');
return resources.filter(filter).map((item) => item.id);
},
{ status: 200, statusText: 'OK', headers: {}, body: { resources } }
);
addBrunoResponseShimToContext(vm, response);
const result = vm.evalCode(`
res('resources[?].id', r => r.id === 'test')
`);
const valueHandle = vm.unwrapResult(result);
const filtered = vm.dump(valueHandle);
valueHandle.dispose();
expect(filtered).toEqual(['test']);
});
it('still supports object predicates in safe mode', () => {
const resources = [
{ id: 'test', value: 1 },
{ id: 'other', value: 2 }
];
const response = Object.assign(
(expr, predicate) => {
expect(expr).toBe('resources[?].id');
return resources.filter((item) =>
Object.entries(predicate).every(([key, value]) => item[key] === value)
).map((item) => item.id);
},
{ status: 200, statusText: 'OK', headers: {}, body: { resources } }
);
addBrunoResponseShimToContext(vm, response);
const result = vm.evalCode(`
res('resources[?].id', { id: 'other' })
`);
const valueHandle = vm.unwrapResult(result);
const filtered = vm.dump(valueHandle);
valueHandle.dispose();
expect(filtered).toEqual(['other']);
});
});