@usebruno/js 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usebruno/js",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "license": "MIT",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -8,12 +8,14 @@
8
8
  "package.json"
9
9
  ],
10
10
  "peerDependencies": {
11
- "@n8n/vm2": "^3.9.23"
11
+ "@usebruno/vm2": "^3.9.13"
12
12
  },
13
13
  "scripts": {
14
- "test": "jest --testPathIgnorePatterns test.js"
14
+ "test": "node --experimental-vm-modules $(npx which jest) --testPathIgnorePatterns test.js",
15
+ "sandbox:bundle-libraries": "node ./src/sandbox/bundle-libraries.js"
15
16
  },
16
17
  "dependencies": {
18
+ "@usebruno/common": "0.3.0",
17
19
  "@usebruno/query": "0.1.0",
18
20
  "ajv": "^8.12.0",
19
21
  "ajv-formats": "^2.1.1",
@@ -23,13 +25,29 @@
23
25
  "chai": "^4.3.7",
24
26
  "chai-string": "^1.5.0",
25
27
  "crypto-js": "^4.1.1",
26
- "handlebars": "^4.7.8",
28
+ "crypto-js-3.1.9-1": "npm:crypto-js@^3.1.9-1",
27
29
  "json-query": "^2.2.2",
28
30
  "lodash": "^4.17.21",
29
31
  "moment": "^2.29.4",
30
32
  "nanoid": "3.3.4",
31
- "node-fetch": "2.*",
33
+ "node-fetch": "^2.7.0",
32
34
  "node-vault": "^0.10.2",
35
+ "path": "^0.12.7",
36
+ "quickjs-emscripten": "^0.29.2",
33
37
  "uuid": "^9.0.0"
38
+ },
39
+ "devDependencies": {
40
+ "@rollup/plugin-commonjs": "^23.0.2",
41
+ "@rollup/plugin-json": "^6.1.0",
42
+ "@rollup/plugin-node-resolve": "^15.0.1",
43
+ "rollup": "3.2.5",
44
+ "rollup-plugin-node-builtins": "^2.1.2",
45
+ "rollup-plugin-node-globals": "^1.4.0",
46
+ "rollup-plugin-polyfill-node": "^0.13.0",
47
+ "rollup-plugin-terser": "^7.0.2",
48
+ "stream": "^0.0.2",
49
+ "terser": "^5.31.1",
50
+ "uglify-js": "^3.18.0",
51
+ "util": "^0.12.5"
34
52
  }
35
53
  }
package/src/bru.js CHANGED
@@ -1,42 +1,47 @@
1
- const Handlebars = require('handlebars');
2
1
  const { cloneDeep } = require('lodash');
3
2
  const { uuid } = require('./utils');
3
+ const { interpolate } = require('@usebruno/common');
4
4
 
5
5
  const variableNameRegex = /^[\w-.]*$/;
6
6
 
7
7
  class Bru {
8
8
  constructor(
9
9
  envVariables,
10
- collectionVariables,
10
+ runtimeVariables,
11
11
  processEnvVars,
12
12
  collectionPath,
13
13
  historyLogger,
14
- setHtmlVisualizationString,
15
- secretVariables
14
+ setVisualizations,
15
+ secretVariables,
16
+ requestVariables
16
17
  ) {
17
- this.envVariables = envVariables;
18
- this.collectionVariables = collectionVariables;
18
+ this.envVariables = envVariables || {};
19
+ this.runtimeVariables = runtimeVariables || {};
19
20
  this.processEnvVars = cloneDeep(processEnvVars || {});
20
21
  this.secretVariables = cloneDeep(secretVariables || {});
22
+ this.requestVariables = requestVariables || {};
21
23
  this.collectionPath = collectionPath;
22
24
  this.historyLogger = historyLogger;
23
- this.setHtmlVisualizationString = setHtmlVisualizationString;
25
+ this.setVisualizations = setVisualizations;
24
26
  }
25
27
 
26
- _interpolateEnvVar = (str) => {
28
+ _interpolate = (str) => {
27
29
  if (!str || !str.length || typeof str !== 'string') {
28
30
  return str;
29
31
  }
30
32
 
31
- const template = Handlebars.compile(str, { noEscape: true });
32
-
33
- return template({
33
+ const combinedVars = {
34
+ ...this.envVariables,
35
+ ...this.requestVariables,
36
+ ...this.runtimeVariables,
34
37
  process: {
35
38
  env: {
36
39
  ...this.processEnvVars
37
40
  }
38
41
  }
39
- });
42
+ };
43
+
44
+ return interpolate(str, combinedVars);
40
45
  };
41
46
 
42
47
  cwd() {
@@ -51,8 +56,12 @@ class Bru {
51
56
  return this.processEnvVars[key];
52
57
  }
53
58
 
59
+ hasEnvVar(key) {
60
+ return Object.hasOwn(this.envVariables, key);
61
+ }
62
+
54
63
  getEnvVar(key) {
55
- return this._interpolateEnvVar(this.envVariables[key]);
64
+ return this._interpolate(this.envVariables[key]);
56
65
  }
57
66
 
58
67
  setEnvVar(key, value) {
@@ -72,6 +81,10 @@ class Bru {
72
81
  this.envVariables[key] = value;
73
82
  }
74
83
 
84
+ hasVar(key) {
85
+ return Object.hasOwn(this.runtimeVariables, key);
86
+ }
87
+
75
88
  setVar(key, value) {
76
89
  if (!key) {
77
90
  throw new Error('Creating a variable without specifying a name is not allowed.');
@@ -93,7 +106,7 @@ class Bru {
93
106
  });
94
107
  }
95
108
 
96
- this.collectionVariables[key] = value;
109
+ this.runtimeVariables[key] = value;
97
110
  }
98
111
 
99
112
  getVar(key) {
@@ -104,20 +117,51 @@ class Bru {
104
117
  );
105
118
  }
106
119
 
107
- return this.collectionVariables[key];
120
+ return this._interpolate(this.runtimeVariables[key]);
121
+ }
122
+
123
+ deleteVar(key) {
124
+ delete this.runtimeVariables[key];
125
+ }
126
+
127
+ getRequestVar(key) {
128
+ return this._interpolate(this.requestVariables[key]);
108
129
  }
109
130
 
110
131
  setNextRequest(nextRequest) {
111
132
  this.nextRequest = nextRequest;
112
133
  }
113
134
 
114
- visualize(htmlString) {
115
- this.setHtmlVisualizationString(htmlString);
116
- }
117
135
 
118
136
  getSecretVar(key) {
119
137
  return this.secretVariables?.[`$secrets.${key}`];
120
138
  }
139
+
140
+
141
+ visualize(type, data) {
142
+ if (type == 'table') {
143
+ if (data?.provider == 'ag-grid') {
144
+ if (!data?.props?.columnDefinitions) {
145
+ throw new Error(`columns definitions are required`);
146
+ }
147
+ if (!data?.props?.rowData) {
148
+ throw new Error(`row data is required`);
149
+ }
150
+ this.setVisualizations({ uid: uuid(), type, data });
151
+ } else if (data?.provider == 'react-table') {
152
+ this.setVisualizations({ uid: uuid(), type, data });
153
+ }
154
+ } else if (type == 'html') {
155
+ if (!data?.content) {
156
+ throw new Error(`html content is required`);
157
+ }
158
+ this.setVisualizations({ uid: uuid(), type, data });
159
+ }
160
+ }
161
+
162
+ sleep(ms) {
163
+ return new Promise((resolve) => setTimeout(resolve, ms));
164
+ }
121
165
  }
122
166
 
123
167
  module.exports = Bru;
@@ -1,45 +1,17 @@
1
- const Handlebars = require('handlebars');
2
- const { forOwn, cloneDeep } = require('lodash');
1
+ const { interpolate } = require('@usebruno/common');
3
2
 
4
- const interpolateEnvVars = (str, processEnvVars) => {
3
+ const interpolateString = (
4
+ str,
5
+ { envVariables = {}, runtimeVariables = {}, processEnvVars = {}, requestVariables = {} }
6
+ ) => {
5
7
  if (!str || !str.length || typeof str !== 'string') {
6
8
  return str;
7
9
  }
8
10
 
9
- const template = Handlebars.compile(str, { noEscape: true });
10
-
11
- return template({
12
- process: {
13
- env: {
14
- ...processEnvVars
15
- }
16
- }
17
- });
18
- };
19
-
20
- const interpolateString = (str, { envVariables, collectionVariables, processEnvVars }) => {
21
- if (!str || !str.length || typeof str !== 'string') {
22
- return str;
23
- }
24
-
25
- processEnvVars = processEnvVars || {};
26
- collectionVariables = collectionVariables || {};
27
-
28
- // we clone envVariables because we don't want to modify the original object
29
- envVariables = envVariables ? cloneDeep(envVariables) : {};
30
-
31
- // envVariables can inturn have values as {{process.env.VAR_NAME}}
32
- // so we need to interpolate envVariables first with processEnvVars
33
- forOwn(envVariables, (value, key) => {
34
- envVariables[key] = interpolateEnvVars(value, processEnvVars);
35
- });
36
-
37
- const template = Handlebars.compile(str, { noEscape: true });
38
-
39
- // collectionVariables take precedence over envVariables
40
11
  const combinedVars = {
41
12
  ...envVariables,
42
- ...collectionVariables,
13
+ ...requestVariables,
14
+ ...runtimeVariables,
43
15
  process: {
44
16
  env: {
45
17
  ...processEnvVars
@@ -47,7 +19,7 @@ const interpolateString = (str, { envVariables, collectionVariables, processEnvV
47
19
  }
48
20
  };
49
21
 
50
- return template(combinedVars);
22
+ return interpolate(str, combinedVars);
51
23
  };
52
24
 
53
25
  module.exports = {
@@ -5,6 +5,7 @@ const Bru = require('../bru');
5
5
  const BrunoRequest = require('../bruno-request');
6
6
  const { evaluateJsTemplateLiteral, evaluateJsExpression, createResponseParser, uuid } = require('../utils');
7
7
  const { interpolateString } = require('../interpolate-string');
8
+ const { executeQuickJsVm } = require('../sandbox/quickjs');
8
9
 
9
10
  const { expect } = chai;
10
11
  chai.use(require('chai-string'));
@@ -66,6 +67,7 @@ chai.use(function (chai, utils) {
66
67
  * isNumber : is number
67
68
  * isString : is string
68
69
  * isBoolean : is boolean
70
+ * isArray : is array
69
71
  */
70
72
  const parseAssertionOperator = (str = '') => {
71
73
  if (!str || typeof str !== 'string' || !str.length) {
@@ -101,7 +103,8 @@ const parseAssertionOperator = (str = '') => {
101
103
  'isJson',
102
104
  'isNumber',
103
105
  'isString',
104
- 'isBoolean'
106
+ 'isBoolean',
107
+ 'isArray'
105
108
  ];
106
109
 
107
110
  const unaryOperators = [
@@ -114,7 +117,8 @@ const parseAssertionOperator = (str = '') => {
114
117
  'isJson',
115
118
  'isNumber',
116
119
  'isString',
117
- 'isBoolean'
120
+ 'isBoolean',
121
+ 'isArray'
118
122
  ];
119
123
 
120
124
  const [operator, ...rest] = str.trim().split(' ');
@@ -151,19 +155,45 @@ const isUnaryOperator = (operator) => {
151
155
  'isJson',
152
156
  'isNumber',
153
157
  'isString',
154
- 'isBoolean'
158
+ 'isBoolean',
159
+ 'isArray'
155
160
  ];
156
161
 
157
162
  return unaryOperators.includes(operator);
158
163
  };
159
164
 
160
- const evaluateRhsOperand = (rhsOperand, operator, context) => {
165
+ const evaluateJsTemplateLiteralBasedOnRuntime = (literal, context, runtime) => {
166
+ if (runtime === 'quickjs') {
167
+ return executeQuickJsVm({
168
+ script: literal,
169
+ context,
170
+ scriptType: 'template-literal'
171
+ });
172
+ }
173
+
174
+ return evaluateJsTemplateLiteral(literal, context);
175
+ };
176
+
177
+ const evaluateJsExpressionBasedOnRuntime = (expr, context, runtime) => {
178
+ if (runtime === 'quickjs') {
179
+ return executeQuickJsVm({
180
+ script: expr,
181
+ context,
182
+ scriptType: 'expression'
183
+ });
184
+ }
185
+
186
+ return evaluateJsExpression(expr, context);
187
+ };
188
+
189
+ const evaluateRhsOperand = (rhsOperand, operator, context, runtime) => {
161
190
  if (isUnaryOperator(operator)) {
162
191
  return;
163
192
  }
164
193
 
165
194
  const interpolationContext = {
166
- collectionVariables: context.bru.collectionVariables,
195
+ requestVariables: context.bru.requestVariables,
196
+ runtimeVariables: context.bru.runtimeVariables,
167
197
  envVariables: context.bru.envVariables,
168
198
  processEnvVars: context.bru.processEnvVars
169
199
  };
@@ -176,13 +206,17 @@ const evaluateRhsOperand = (rhsOperand, operator, context) => {
176
206
 
177
207
  return rhsOperand
178
208
  .split(',')
179
- .map((v) => evaluateJsTemplateLiteral(interpolateString(v.trim(), interpolationContext), context));
209
+ .map((v) =>
210
+ evaluateJsTemplateLiteralBasedOnRuntime(interpolateString(v.trim(), interpolationContext), context, runtime)
211
+ );
180
212
  }
181
213
 
182
214
  if (operator === 'between') {
183
215
  const [lhs, rhs] = rhsOperand
184
216
  .split(',')
185
- .map((v) => evaluateJsTemplateLiteral(interpolateString(v.trim(), interpolationContext), context));
217
+ .map((v) =>
218
+ evaluateJsTemplateLiteralBasedOnRuntime(interpolateString(v.trim(), interpolationContext), context, runtime)
219
+ );
186
220
  return [lhs, rhs];
187
221
  }
188
222
 
@@ -195,20 +229,16 @@ const evaluateRhsOperand = (rhsOperand, operator, context) => {
195
229
  return interpolateString(rhsOperand, interpolationContext);
196
230
  }
197
231
 
198
- return evaluateJsTemplateLiteral(interpolateString(rhsOperand, interpolationContext), context);
232
+ return evaluateJsTemplateLiteralBasedOnRuntime(interpolateString(rhsOperand, interpolationContext), context, runtime);
199
233
  };
200
234
 
201
235
  class AssertRuntime {
202
- runAssertions(
203
- assertions,
204
- request,
205
- response,
206
- envVariables,
207
- collectionVariables,
208
- collectionPath,
209
- historyLogger,
210
- secretVariables
211
- ) {
236
+ constructor(props) {
237
+ this.runtime = props?.runtime || 'vm2';
238
+ }
239
+
240
+ runAssertions(assertions, request, response, envVariables, runtimeVariables, processEnvVars, historyLogger, secretVariables) {
241
+ const requestVariables = request?.requestVariables || {};
212
242
  const enabledAssertions = _.filter(assertions, (a) => a.enabled);
213
243
  if (!enabledAssertions.length) {
214
244
  return [];
@@ -216,12 +246,13 @@ class AssertRuntime {
216
246
 
217
247
  const bru = new Bru(
218
248
  envVariables,
219
- collectionVariables,
220
- undefined, // processEnvVars,
249
+ runtimeVariables,
250
+ processEnvVars,
221
251
  undefined, // collectionPath,
222
252
  undefined, // historyLogger,
223
- undefined, // setHtmlVisualizationString,
224
- secretVariables
253
+ undefined, // setVisualizations,
254
+ secretVariables,
255
+ requestVariables
225
256
  );
226
257
  const req = new BrunoRequest(request, historyLogger);
227
258
  const res = createResponseParser(response);
@@ -234,7 +265,9 @@ class AssertRuntime {
234
265
 
235
266
  const context = {
236
267
  ...envVariables,
237
- ...collectionVariables,
268
+ ...requestVariables,
269
+ ...runtimeVariables,
270
+ ...processEnvVars,
238
271
  ...bruContext
239
272
  };
240
273
 
@@ -247,8 +280,8 @@ class AssertRuntime {
247
280
  const { operator, value: rhsOperand } = parseAssertionOperator(rhsExpr);
248
281
 
249
282
  try {
250
- const lhs = evaluateJsExpression(lhsExpr, context);
251
- const rhs = evaluateRhsOperand(rhsOperand, operator, context);
283
+ const lhs = evaluateJsExpressionBasedOnRuntime(lhsExpr, context, this.runtime);
284
+ const rhs = evaluateRhsOperand(rhsOperand, operator, context, this.runtime);
252
285
 
253
286
  switch (operator) {
254
287
  case 'eq':
@@ -330,6 +363,9 @@ class AssertRuntime {
330
363
  case 'isBoolean':
331
364
  expect(lhs).to.be.a('boolean');
332
365
  break;
366
+ case 'isArray':
367
+ expect(lhs).to.be.a('array');
368
+ break;
333
369
  default:
334
370
  expect(lhs).to.equal(rhs);
335
371
  break;
@@ -1,4 +1,4 @@
1
- const { NodeVM } = require('vm2');
1
+ const { NodeVM } = require('@usebruno/vm2');
2
2
  const path = require('path');
3
3
  const http = require('http');
4
4
  const https = require('https');
@@ -27,10 +27,13 @@ const axios = require('axios');
27
27
  const fetch = require('node-fetch');
28
28
  const chai = require('chai');
29
29
  const CryptoJS = require('crypto-js');
30
- const NodeSecrets = require('node-vault');
30
+ const NodeVault = require('node-vault');
31
+ const { executeQuickJsVmAsync } = require('../sandbox/quickjs');
31
32
 
32
33
  class ScriptRuntime {
33
- constructor() {}
34
+ constructor(props) {
35
+ this.runtime = props?.runtime || 'vm2';
36
+ }
34
37
 
35
38
  // This approach is getting out of hand
36
39
  // Need to refactor this to use a single arg (object) instead of 7
@@ -38,7 +41,7 @@ class ScriptRuntime {
38
41
  script,
39
42
  request,
40
43
  envVariables,
41
- collectionVariables,
44
+ runtimeVariables,
42
45
  collectionPath,
43
46
  onConsoleLog,
44
47
  processEnvVars,
@@ -46,18 +49,20 @@ class ScriptRuntime {
46
49
  historyLogger,
47
50
  secretVariables
48
51
  ) {
49
- let htmlVisualizationString;
50
- let setHtmlVisualizationString = (htmlString) => {
51
- htmlVisualizationString = htmlString;
52
- };
52
+ let visualizations = [];
53
+ let setVisualizations = (data) => {
54
+ visualizations.push(data);
55
+ }
56
+ const requestVariables = request?.requestVariables || {};
53
57
  const bru = new Bru(
54
58
  envVariables,
55
- collectionVariables,
59
+ runtimeVariables,
56
60
  processEnvVars,
57
61
  collectionPath,
58
62
  historyLogger,
59
- setHtmlVisualizationString,
60
- secretVariables
63
+ setVisualizations,
64
+ secretVariables,
65
+ requestVariables
61
66
  );
62
67
  const req = new BrunoRequest(request, historyLogger);
63
68
  const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false);
@@ -99,6 +104,23 @@ class ScriptRuntime {
99
104
  };
100
105
  }
101
106
 
107
+ if (this.runtime === 'quickjs') {
108
+ await executeQuickJsVmAsync({
109
+ script: script,
110
+ context: context,
111
+ collectionPath
112
+ });
113
+
114
+ return {
115
+ request,
116
+ envVariables: cleanJson(envVariables),
117
+ runtimeVariables: cleanJson(runtimeVariables),
118
+ visualizations,
119
+ nextRequestName: bru.nextRequest
120
+ };
121
+ }
122
+
123
+ // default runtime is vm2
102
124
  const vm = new NodeVM({
103
125
  sandbox: context,
104
126
  require: {
@@ -130,17 +152,18 @@ class ScriptRuntime {
130
152
  'crypto-js': CryptoJS,
131
153
  ...whitelistedModules,
132
154
  fs: allowScriptFilesystemAccess ? fs : undefined,
133
- 'node-vault': NodeSecrets
155
+ 'node-vault': NodeVault
134
156
  }
135
157
  }
136
158
  });
137
159
  const asyncVM = vm.run(`module.exports = async () => { ${script} }`, path.join(collectionPath, 'vm.js'));
138
160
  await asyncVM();
161
+
139
162
  return {
140
163
  request,
141
164
  envVariables: cleanJson(envVariables),
142
- collectionVariables: cleanJson(collectionVariables),
143
- htmlVisualizationString,
165
+ runtimeVariables: cleanJson(runtimeVariables),
166
+ visualizations,
144
167
  nextRequestName: bru.nextRequest
145
168
  };
146
169
  }
@@ -150,7 +173,7 @@ class ScriptRuntime {
150
173
  request,
151
174
  response,
152
175
  envVariables,
153
- collectionVariables,
176
+ runtimeVariables,
154
177
  collectionPath,
155
178
  onConsoleLog,
156
179
  processEnvVars,
@@ -158,18 +181,20 @@ class ScriptRuntime {
158
181
  historyLogger,
159
182
  secretVariables
160
183
  ) {
161
- let htmlVisualizationString;
162
- let setHtmlVisualizationString = (htmlString) => {
163
- htmlVisualizationString = htmlString;
164
- };
184
+ let visualizations = [];
185
+ let setVisualizations = (data) => {
186
+ visualizations.push(data);
187
+ }
188
+ const requestVariables = request?.requestVariables || {};
165
189
  const bru = new Bru(
166
190
  envVariables,
167
- collectionVariables,
191
+ runtimeVariables,
168
192
  processEnvVars,
169
193
  collectionPath,
170
194
  historyLogger,
171
- setHtmlVisualizationString,
172
- secretVariables
195
+ setVisualizations,
196
+ secretVariables,
197
+ requestVariables
173
198
  );
174
199
  const req = new BrunoRequest(request, historyLogger);
175
200
  const res = new BrunoResponse(response);
@@ -203,10 +228,28 @@ class ScriptRuntime {
203
228
  log: customLogger('log'),
204
229
  info: customLogger('info'),
205
230
  warn: customLogger('warn'),
206
- error: customLogger('error')
231
+ error: customLogger('error'),
232
+ debug: customLogger('debug')
233
+ };
234
+ }
235
+
236
+ if (this.runtime === 'quickjs') {
237
+ await executeQuickJsVmAsync({
238
+ script: script,
239
+ context: context,
240
+ collectionPath
241
+ });
242
+
243
+ return {
244
+ response,
245
+ envVariables: cleanJson(envVariables),
246
+ runtimeVariables: cleanJson(runtimeVariables),
247
+ visualizations,
248
+ nextRequestName: bru.nextRequest
207
249
  };
208
250
  }
209
251
 
252
+ // default runtime is vm2
210
253
  const vm = new NodeVM({
211
254
  sandbox: context,
212
255
  require: {
@@ -237,7 +280,7 @@ class ScriptRuntime {
237
280
  'crypto-js': CryptoJS,
238
281
  ...whitelistedModules,
239
282
  fs: allowScriptFilesystemAccess ? fs : undefined,
240
- 'node-vault': NodeSecrets
283
+ 'node-vault': NodeVault
241
284
  }
242
285
  }
243
286
  });
@@ -248,8 +291,8 @@ class ScriptRuntime {
248
291
  return {
249
292
  response,
250
293
  envVariables: cleanJson(envVariables),
251
- collectionVariables: cleanJson(collectionVariables),
252
- htmlVisualizationString,
294
+ runtimeVariables: cleanJson(runtimeVariables),
295
+ visualizations,
253
296
  nextRequestName: bru.nextRequest
254
297
  };
255
298
  }