Skip to content

Commit e359733

Browse files
authored
Scope-sharing capabilities for multi-protocol collections (#1101)
* WIP: feat: Scope sharing capabilities for multi-protocol collections * Handle for response type based response class getter * Add sanitization for execution.event for backwards compatibility * Pass forward responseType for nested executions * Support init options with createContext Accepts template value and boolean for disableLegacyAPIs * Update received template object + Its handling * fix: Incorrect target for initializing execution * Add support for templates as map * fix: getProtocolMetadata not utilized * fix: Incorrect target property for execution class * Have disableLegacyAPIs as a picked param * Update tests * Make the logic to get protocol name from target more future-safe * Add tests for multiple templates and chai plugin * fix: templateName to be passed by consumer for any custom handling * Add both `template` and `templates` option for init * Add changelog * Add templateName as a parameter to sandbox.execute args * Streamline sandbox scope sharing capabilities * test: Error dispatched as expected for execution without a templateName param * Update tests + execute api * Refactor execute block
1 parent 2cadd57 commit e359733

File tree

8 files changed

+397
-65
lines changed

8 files changed

+397
-65
lines changed

CHANGELOG.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
unreleased:
2+
new features:
3+
- GH-1101 Add support for multiple item-type templates and explicitly passed
4+
chai plugin in one sandbox instance
5+
16
6.3.0:
27
date: 2025-12-01
38
new features:

lib/index.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
const _ = require('lodash'),
22
bootcode = require('./bootcode'),
33
PostmanSandbox = require('./postman-sandbox'),
4-
PostmanSandboxFleet = require('./postman-sandbox-fleet');
4+
PostmanSandboxFleet = require('./postman-sandbox-fleet'),
5+
6+
INIT_OPTIONS_AVAILABLE = ['disableLegacyAPIs', 'disabledAPIs', 'template', 'templates', 'chaiPlugin'];
57

68
module.exports = {
79
/**
@@ -16,14 +18,16 @@ module.exports = {
1618
options = {};
1719
}
1820

19-
options = _.clone(options);
21+
const initOptions = _.pick(options, INIT_OPTIONS_AVAILABLE),
22+
connectOptions = _.omit(options, INIT_OPTIONS_AVAILABLE);
23+
2024
bootcode((err, code) => {
2125
if (err) { return callback(err); }
2226
if (!code) { return callback(new Error('sandbox: bootcode missing!')); }
2327

24-
options.bootCode = code; // assign the code in options
28+
connectOptions.bootCode = code; // assign the code in connect options
2529

26-
new PostmanSandbox().initialize({}, options, callback);
30+
new PostmanSandbox().initialize(initOptions, connectOptions, callback);
2731
});
2832
},
2933

lib/postman-sandbox.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ class PostmanSandbox extends UniversalVM {
7171
* @param {Number} options.timeout -
7272
* @param {Object} options.cursor -
7373
* @param {Object} options.context -
74+
* @param {String} options.templateName -
7475
* @param {Boolean} options.serializeLogs -
7576
* @param {Function} callback -
7677
*/
@@ -98,6 +99,7 @@ class PostmanSandbox extends UniversalVM {
9899
cursor = _.clone(_.get(options, 'cursor', {})), // clone the cursor as it travels through IPC for mutation
99100
resolvedPackages = _.get(options, 'resolvedPackages'),
100101
debugMode = _.has(options, 'debug') ? options.debug : this.debug,
102+
templateName = _.get(options, 'templateName'),
101103

102104
// send the code to the sandbox to be intercepted and executed
103105
dispatchExecute = () => {
@@ -107,7 +109,8 @@ class PostmanSandbox extends UniversalVM {
107109
timeout: executionTimeout,
108110
resolvedPackages: resolvedPackages,
109111
legacy: _.get(options, 'legacy'),
110-
disabledAPIs: options.disabledAPIs
112+
disabledAPIs: options.disabledAPIs,
113+
templateName: templateName
111114
});
112115
};
113116

lib/sandbox/execute.js

Lines changed: 76 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,30 @@ const _ = require('lodash'),
3030

3131
executeContext = require('./execute-context');
3232

33+
function execTemplate (template) {
34+
return new Promise((resolve, reject) => {
35+
const _module = { exports: {} },
36+
scope = Scope.create({
37+
eval: true,
38+
ignore: ['require'],
39+
block: ['bridge']
40+
});
41+
42+
scope.import({
43+
Buffer: require('buffer').Buffer,
44+
module: _module
45+
});
46+
47+
scope.exec(template, (err) => {
48+
if (err) {
49+
return reject(err);
50+
}
51+
52+
return resolve(_module?.exports || {});
53+
});
54+
});
55+
}
56+
3357
module.exports = function (bridge, glob) {
3458
// @note we use a common scope for all executions. this causes issues when scripts are run inside the sandbox
3559
// in parallel, but we still use this way for the legacy "persistent" behaviour needed in environment
@@ -42,53 +66,70 @@ module.exports = function (bridge, glob) {
4266
// For caching required information provided during
4367
// initialization which will be used during execution
4468
let initializationOptions = {},
45-
initializeExecution;
69+
templatesRegistry = {},
70+
getProtocolMetadata = (protocolName) => {
71+
return templatesRegistry[protocolName] || templatesRegistry._default || null;
72+
};
4673

4774
/**
4875
* @param {Object} options
49-
* @param {String} [options.template]
76+
* @param {String} [options.template] - Template string - To be deprecated, use `templates` instead
77+
* @param {Record<String, String>} [options.templates] - Map of template names to template strings
78+
* @param {String} [options.chaiPlugin]
5079
* @param {Boolean} [options.disableLegacyAPIs]
5180
* @param {Array.<String>} [options.disabledAPIs]
5281
*/
53-
bridge.once('initialize', ({ template, ...initOptions }) => {
82+
bridge.once('initialize', async (initOptions) => {
5483
initializationOptions = initOptions || {};
5584

56-
// If no custom template is provided, go ahead with the default steps
57-
if (!template) {
85+
const { template, templates, chaiPlugin } = initializationOptions,
86+
availableTemplates = templates || (template ? { _default: template } : {}),
87+
templateNames = Object.keys(availableTemplates),
88+
promises = templateNames.map((name) => {
89+
return execTemplate(availableTemplates[name]);
90+
});
91+
92+
// If no custom template or chai plugin is provided, go ahead with the default steps
93+
if (!templates && !template && !chaiPlugin) {
5894
chai.use(require('chai-postman')(sdk, _, Ajv));
5995

6096
return bridge.dispatch('initialize');
6197
}
6298

63-
const _module = { exports: {} },
64-
scope = Scope.create({
65-
eval: true,
66-
ignore: ['require'],
67-
block: ['bridge']
68-
});
99+
// Templates can pass a chai plugin as a string or a function
100+
// or a chaiPlugin arg is supported that works with all templates and receives the registry as an argument
101+
promises.push(chaiPlugin ? execTemplate(chaiPlugin) : Promise.resolve(null));
69102

70-
scope.import({
71-
Buffer: require('buffer').Buffer,
72-
module: _module
73-
});
103+
try {
104+
const results = await Promise.all(promises),
105+
templateChaiPlugins = [],
106+
chaiPluginResult = results.pop();
74107

75-
scope.exec(template, (err) => {
76-
if (err) {
77-
return bridge.dispatch('initialize', err);
78-
}
108+
for (let i = 0; i < results.length; i++) {
109+
const result = results[i];
79110

80-
const { chaiPlugin, initializeExecution: setupExecution } = (_module && _module.exports) || {};
111+
templatesRegistry[templateNames[i]] = result;
81112

82-
if (_.isFunction(chaiPlugin)) {
83-
chai.use(chaiPlugin);
113+
if (_.isFunction(result.chaiPlugin)) {
114+
templateChaiPlugins.push(result.chaiPlugin);
115+
}
84116
}
85117

86-
if (_.isFunction(setupExecution)) {
87-
initializeExecution = setupExecution;
118+
if (_.isFunction(chaiPluginResult)) {
119+
chai.use(chaiPluginResult(templatesRegistry));
120+
}
121+
else if (templateChaiPlugins.length === 1) {
122+
chai.use(templateChaiPlugins[0]);
123+
}
124+
else if (templateChaiPlugins.length > 1) {
125+
throw new Error('sandbox: multiple chai plugins are not supported');
88126
}
89127

90128
bridge.dispatch('initialize');
91-
});
129+
}
130+
catch (error) {
131+
return bridge.dispatch('initialize', error);
132+
}
92133
});
93134

94135
/**
@@ -108,6 +149,10 @@ module.exports = function (bridge, glob) {
108149
return bridge.dispatch('error', new Error('sandbox: execution identifier parameter(s) missing'));
109150
}
110151

152+
if (initializationOptions.templates && !(options.templateName && _.isString(options.templateName))) {
153+
return bridge.dispatch('error', new Error('sandbox: template name parameter is missing from options'));
154+
}
155+
111156
!options && (options = {});
112157
!context && (context = {});
113158
event = (new PostmanEvent(event));
@@ -137,8 +182,10 @@ module.exports = function (bridge, glob) {
137182
return isNonLegacySandbox(code) ? `${getNonLegacyCodeMarker()}${asyncCode}` : asyncCode;
138183
})(event.script?.toSource()),
139184

185+
protocolMetadata = getProtocolMetadata(options.templateName),
186+
140187
// create the execution object
141-
execution = new Execution(id, event, context, { ...options, initializeExecution }),
188+
execution = new Execution(id, event, context, { ...options, protocolMetadata }),
142189

143190
disabledAPIs = [
144191
...(initializationOptions.disabledAPIs || []),
@@ -242,7 +289,7 @@ module.exports = function (bridge, glob) {
242289
bridge.on(runRequestResponseEventName, function (id, err, response, results) {
243290
// Apply variable mutations from the scripts executed via pm.execution.runRequest
244291
// to the current execution scope
245-
const { variableMutations = {}, skipResponseCasting = false } = results || {};
292+
const { variableMutations = {}, responseType } = results || {};
246293

247294
CONTEXT_VARIABLE_SCOPES.forEach(function (variableType) {
248295
let mutableVariableScope = execution[variableType],
@@ -264,7 +311,7 @@ module.exports = function (bridge, glob) {
264311
});
265312
});
266313

267-
timers.clearEvent(id, err, response, { skipResponseCasting });
314+
timers.clearEvent(id, err, response, { responseType });
268315
});
269316

270317
// handle cookies event from outside sandbox
@@ -331,7 +378,7 @@ module.exports = function (bridge, glob) {
331378
context);
332379
},
333380
createPostmanRequire(options.resolvedPackages, scope),
334-
{ disabledAPIs })
381+
{ disabledAPIs, getProtocolMetadata })
335382
),
336383
dispatchAssertions,
337384
{ disableLegacyAPIs: initializationOptions.disableLegacyAPIs });

lib/sandbox/execution.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ class Execution {
2929
this.target = event.listen || PROPERTY.SCRIPT;
3030
this.legacy = options.legacy || {};
3131
this.cursor = _.isObject(options.cursor) ? options.cursor : {};
32-
3332
this.data = _.get(context, PROPERTY.DATA, {});
3433
this.cookies = new sdk.CookieList(null, context.cookies);
3534

@@ -42,14 +41,17 @@ class Execution {
4241
this[variableScope].enableTracking(trackingOptions);
4342
});
4443

45-
if (options.initializeExecution) {
46-
const { request, response, message } = options.initializeExecution(this.target, context) || {};
44+
if (options.protocolMetadata && options.protocolMetadata.initializeExecution) {
45+
const { request, response, message } = options.protocolMetadata
46+
.initializeExecution(this.target, context) || {};
4747

4848
this.request = request;
4949
this.response = response;
5050
this.message = message;
5151
}
5252
else {
53+
// If the associated item type does not have an initializer, then fallback to the defaults (HTTP-SDK).
54+
5355
if (TARGETS_WITH_REQUEST[this.target] || _.has(context, PROPERTY.REQUEST)) {
5456
/**
5557
* @note:

lib/sandbox/pmapi.js

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,7 @@ function Postman (execution,
421421
}
422422

423423
onRunRequest(requestId, options || {},
424-
function (err, resp, context = { skipResponseCasting: false }) {
424+
function (err, resp, context) {
425425
if (err) {
426426
return reject(err);
427427
}
@@ -433,15 +433,15 @@ function Postman (execution,
433433
return resolve(null);
434434
}
435435

436-
// Temp
437-
// TODO: Make this work via custom response classes based on the response types
438-
// passed from consumers.
439-
if (!context || !context.skipResponseCasting) {
440-
resolve(PostmanResponse.isResponse(resp) ? resp : (new PostmanResponse(resp)));
441-
}
442-
else {
443-
resolve(resp);
436+
const ResponseClass = context && context.responseType ?
437+
options.getProtocolMetadata(context.responseType)?.Response :
438+
PostmanResponse;
439+
440+
if (!ResponseClass) {
441+
return resolve(resp);
444442
}
443+
444+
resolve(ResponseClass.isResponse(resp) ? resp : (new ResponseClass(resp)));
445445
});
446446
});
447447
}

test/unit/sandbox-libraries/pm.test.js

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1479,29 +1479,52 @@ describe('sandbox library - pm api', function () {
14791479
});
14801480

14811481
it('should handle response types for multi-protocol:others', function (done) {
1482-
const executionId = '8',
1483-
sampleRequestToRunId = '5d559eb8-cd89-43a3-b93c-1e398d79c670';
1482+
const executionId = '1',
1483+
individualTemplate = `
1484+
class Response {
1485+
constructor(response) {
1486+
this.statusCode = response.statusCode;
1487+
this.responseTime = response.responseTime;
1488+
this.isCustomGRPCResponseClass = true;
1489+
}
1490+
}
14841491
1485-
context.on('execution.run_collection_request.' + executionId,
1486-
function (cursor, id, reqId) {
1487-
context.dispatch(`execution.run_collection_request_response.${id}`, reqId, null, {
1488-
statusCode: 0, responseTime: 100
1489-
}, { skipResponseCasting: true });
1490-
});
1492+
module.exports = { Response };
1493+
`;
1494+
1495+
Sandbox.createContext({
1496+
templates: { grpc: individualTemplate }
1497+
}, (errorInitializingSandbox, sandboxContext) => {
1498+
if (errorInitializingSandbox) { return done(errorInitializingSandbox); }
1499+
1500+
sandboxContext.on('execution.run_collection_request.' + executionId,
1501+
function (_cursor, id, reqId) {
1502+
sandboxContext.dispatch(`execution.run_collection_request_response.${id}`,
1503+
reqId,
1504+
null,
1505+
{ statusCode: 0, responseTime: 100 },
1506+
{ responseType: 'grpc' });
1507+
});
14911508

1492-
let consoleMessage = '';
1509+
let consoleMessage = '';
14931510

1494-
context.on('console', (_cursor, _level, message) => {
1495-
consoleMessage = message;
1496-
});
1511+
sandboxContext.on('console', (_cursor, _level, message) => {
1512+
consoleMessage = message;
1513+
expect(consoleMessage).to.eql({
1514+
statusCode: 0,
1515+
responseTime: 100,
1516+
isCustomGRPCResponseClass: true
1517+
});
1518+
});
14971519

1498-
context.execute(`
1499-
const grpcRequestResponse = await pm.execution.runRequest('${sampleRequestToRunId}');
1500-
console.log(grpcRequestResponse);
1501-
`, { id: executionId }, function () {
1502-
// No processing for non-http protocol responses, logged as is
1503-
expect(consoleMessage).to.eql({ statusCode: 0, responseTime: 100 });
1504-
done();
1520+
sandboxContext.execute(`
1521+
const grpcRequestResponse = await pm.execution.runRequest('sample-request-id');
1522+
console.log(grpcRequestResponse);`,
1523+
{ id: executionId, templateName: 'grpc' },
1524+
function (err) {
1525+
done(err);
1526+
sandboxContext.dispose();
1527+
});
15051528
});
15061529
});
15071530
});

0 commit comments

Comments
 (0)