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']); + }); +});