postman-runtime
Version:
Underlying library of executing Postman Collections
243 lines (202 loc) • 11.5 kB
JavaScript
const _ = require('lodash'),
sdk = require('postman-collection'),
serialisedError = require('serialised-error'),
SYNCABLE_CONTEXT_VARIABLE_SCOPES = ['_variables', 'collectionVariables', 'environment', 'globals'],
EXECUTION_RUN_REQUEST_RESPONSE_EVENT_BASE = 'execution.run_collection_request_response.';
function runNestedRequest ({ executionId, isExecutionSkipped, vaultSecrets, item }) {
// Note: Don't forget to bind this function to the runner to make sure `this` can give you the right options & state
return function (cursor, eventId, requestId, requestToRunId, runRequestOptions = {}, runContext = {}) {
const self = this,
requestResolver = _.get(self, 'options.script.requestResolver'),
runRequestRespEvent = EXECUTION_RUN_REQUEST_RESPONSE_EVENT_BASE + eventId,
maxInvokableNestedRequests = _.get(self, 'options.maxInvokableNestedRequests'),
itemId = item.id,
currentItemState = { isPoppedFromCallStack: false },
popCurrentItemFromCallStack = () => {
if (currentItemState.isPoppedFromCallStack) { return; }
currentItemState.isPoppedFromCallStack = true;
if (!self.state.nestedRequest.callStack.length) { return; }
const { callStack } = self.state.nestedRequest,
itemIndex = callStack.lastIndexOf(itemId);
if (itemIndex !== -1) {
self.state.nestedRequest.callStack.splice(itemIndex, 1);
}
};
function dispatchErrorToListener (err) {
popCurrentItemFromCallStack();
const error = serialisedError(err);
delete error.stack;
return self.host.dispatch(EXECUTION_RUN_REQUEST_RESPONSE_EVENT_BASE + eventId,
requestId, error);
}
// Prepare nested request object for passing down to child request
// Have to keep object reference common so any changes made by nested executions is bubbled back to parent exec
self.state.nestedRequest = _.defaults(self.state.nestedRequest || {}, {
isNestedRequest: true,
rootCursor: cursor,
rootItem: item,
callStack: []
});
self.state.nestedRequest.callStack.push(itemId);
// No more than maxInvokableNestedRequests nested depth
if (self.state.nestedRequest.callStack.length > maxInvokableNestedRequests) {
return dispatchErrorToListener(new Error('Max pm.execution.runRequest depth of ' +
maxInvokableNestedRequests + ' has been reached.'));
}
let rootRequestEventsList = self.state.nestedRequest.rootItem.events?.all?.() || [],
nestedRequestMeta = null,
context = { rootItemId: self.state.nestedRequest.rootItem.id };
for (const event of rootRequestEventsList) {
if (event.script?.requests?.[requestToRunId]) {
nestedRequestMeta = event.script.requests[requestToRunId];
break;
}
}
if (nestedRequestMeta) {
context.nestedRequestMeta = nestedRequestMeta;
}
// Should fetch the request from the consumer of postman-runtime & resolve scripts and variables
requestResolver(requestToRunId, context, function (err, collection) {
if (err) {
return dispatchErrorToListener(err);
}
if (!collection) {
return dispatchErrorToListener(new Error('Expected collection json with request item ' +
'to invoke pm.execution.runRequest'));
}
// Prepare variables that have been set inside the parent's pre-req/post-res script
// so far and pass them to the runner for this request.
// This is important because for nested requests,
// variables set by the parent using pm.<variable-form>.set do not reflect
// in postman-runtime's scope immediately. They are present inside postman-sandbox
// till the parent request's script execution ends.
const globals = { values: runContext.globals || [] },
localVariables = { values: runContext._variables || [] },
environment = { values: runContext.environment || [] },
collectionVariables = { values: runContext.collectionVariables || [] };
const runner = require('.'),
variableOverrides = runRequestOptions.variables ?
Object.entries(runRequestOptions.variables)
.map(function ([key, value]) {
return { key, value };
}) :
[],
runnableRefRequestCollection = new sdk.Collection(collection),
mergedCollectionVariableList = new sdk.VariableList();
// Merge parent collection's variables with this collection's variables
// Why the separate statement? Because we want the referenced request's collection's variables to
// take precedence over the parent collection's variables if there are any conflicts.
mergedCollectionVariableList.populate(collectionVariables.values);
mergedCollectionVariableList.populate(runnableRefRequestCollection.variables.all());
runnableRefRequestCollection.variables = mergedCollectionVariableList;
// Merge local variables from parent requests & scope + nestedRequest.options.variables
localVariables.values = [...localVariables.values, ...variableOverrides];
// Why clone? Each runner execution needs to track and mutate its vault variables separately and propagate
// it back up and further down. We don't want to accidentally reset mutations between executions by sharing
// this scope
let clonedVaultSecrets;
if (vaultSecrets) {
clonedVaultSecrets = new sdk.VariableScope({
values: vaultSecrets.values,
prefix: vaultSecrets.prefix
});
clonedVaultSecrets._ = vaultSecrets._;
}
new runner().run(runnableRefRequestCollection,
{
...self.state,
...self.options,
entrypoint: {
lookupStrategy: 'idOrName',
execute: requestToRunId
},
iterationCount: 1,
globals: globals,
environment: environment,
localVariables: localVariables,
vaultSecrets: clonedVaultSecrets,
stopOnError: true,
host: {
// Reuse current run's sandbox host across nested executions
external: true,
instance: self.host
}
},
function (err, run) {
let exceptionForThisRequest = null,
responseForThisRequest = null,
variableMutationsFromThisExecution = {};
if (err) {
popCurrentItemFromCallStack();
return self.host
.dispatch(EXECUTION_RUN_REQUEST_RESPONSE_EVENT_BASE + eventId,
requestId, err);
}
run.start({
script (_err, _cursor, result) {
// This is to sync changes to pm.variables, pm.environment & pm.globals
// that happened inside the nested request's script
// back to parent request's scripts still currently executing.
// collectionVariables don't need to be synced between parent and nested
// All other global variables defined by syntax like 'a=1'
// are anyway synced as the sandbox's common scope is shared across runs
if (result) {
SYNCABLE_CONTEXT_VARIABLE_SCOPES.forEach(function (type) {
if (!result[type]) {
return;
}
variableMutationsFromThisExecution[type] = [
...(variableMutationsFromThisExecution[type] || []),
result[type].mutations
];
});
if (clonedVaultSecrets && clonedVaultSecrets.mutations) {
const mutations = new sdk.MutationTracker(clonedVaultSecrets.mutations);
mutations.applyOn(vaultSecrets);
}
}
},
request (err, _cursor, ...rest) {
// For nested requests, the request event should bubble up to the parent.
// This event is merely used for notifying the consumer that a nested request was sent
// similar to pm.sendRequest. So the consumer can work this into logs & any other logic.
if (typeof self.triggers.request === 'function' && self.state && self.state.nestedRequest) {
self.triggers.request(err, self.state.nestedRequest.rootCursor, ...rest);
}
},
exception (_, err) {
if (err) {
exceptionForThisRequest = err;
}
},
response (err, _, response) {
if (isExecutionSkipped(executionId)) {
responseForThisRequest = null;
exceptionForThisRequest = null;
return;
}
if (err) {
exceptionForThisRequest = err;
}
if (response) {
responseForThisRequest = response;
}
},
done (err) {
let error = err || exceptionForThisRequest;
if (error) {
error = serialisedError(error);
delete error.stack;
}
popCurrentItemFromCallStack();
return self.host.dispatch(runRequestRespEvent,
requestId, error || null,
responseForThisRequest,
{ variableMutations: variableMutationsFromThisExecution });
}
});
});
});
};
}
module.exports = runNestedRequest;