From 9465de02ee9d54844e4e88e95de46b049775a7f0 Mon Sep 17 00:00:00 2001 From: Abhishek S Lal Date: Fri, 3 Apr 2026 01:53:11 +0530 Subject: [PATCH] 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> --- .../sandbox/quickjs/shims/bruno-response.js | 31 ++++++- .../quickjs/shims/bruno-response.spec.js | 91 +++++++++++++++++++ 2 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 packages/bruno-js/src/sandbox/quickjs/shims/bruno-response.spec.js diff --git a/packages/bruno-js/src/sandbox/quickjs/shims/bruno-response.js b/packages/bruno-js/src/sandbox/quickjs/shims/bruno-response.js index df0fabe60..68e2b7dc1 100644 --- a/packages/bruno-js/src/sandbox/quickjs/shims/bruno-response.js +++ b/packages/bruno-js/src/sandbox/quickjs/shims/bruno-response.js @@ -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); diff --git a/packages/bruno-js/src/sandbox/quickjs/shims/bruno-response.spec.js b/packages/bruno-js/src/sandbox/quickjs/shims/bruno-response.spec.js new file mode 100644 index 000000000..aa5e7d533 --- /dev/null +++ b/packages/bruno-js/src/sandbox/quickjs/shims/bruno-response.spec.js @@ -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']); + }); +});