@usebruno/js 0.51.0 → 0.52.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.51.0",
3
+ "version": "0.52.0",
4
4
  "license": "MIT",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -14,12 +14,12 @@
14
14
  "prepack": "npm run test"
15
15
  },
16
16
  "dependencies": {
17
- "@usebruno/common": "0.25.0",
17
+ "@usebruno/common": "0.26.0",
18
18
  "@usebruno/query": "0.2.2",
19
- "ajv": "^8.12.0",
19
+ "ajv": "^8.18.0",
20
20
  "ajv-formats": "^2.1.1",
21
21
  "atob": "^2.1.2",
22
- "axios": "1.16.0",
22
+ "axios": "1.18.0",
23
23
  "btoa": "^1.2.1",
24
24
  "chai": "^4.3.7",
25
25
  "chai-string": "^1.5.0",
@@ -30,7 +30,7 @@
30
30
  "jsonwebtoken": "^9.0.3",
31
31
  "lodash": "4.18.1",
32
32
  "moment": "^2.29.4",
33
- "nanoid": "3.3.8",
33
+ "nanoid": "3.3.18",
34
34
  "node-fetch": "^2.7.0",
35
35
  "path": "^0.12.7",
36
36
  "quickjs-emscripten": "^0.32.0",
@@ -38,7 +38,7 @@
38
38
  "uuid": "^10.0.0",
39
39
  "xml-formatter": "^3.5.0",
40
40
  "xml2js": "^0.6.2",
41
- "yaml": "^2.3.4"
41
+ "yaml": "2.3.4"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@rollup/plugin-commonjs": "^23.0.2",
package/src/bru.js CHANGED
@@ -38,6 +38,15 @@ const SAFE_HBS_OPTIONS = new Set([
38
38
  ]);
39
39
  const variableNameRegex = /^[\w-.]*$/;
40
40
 
41
+ const assertValidVariableName = (key) => {
42
+ if (variableNameRegex.test(key) === false) {
43
+ throw new Error(
44
+ `Variable name: "${key}" contains invalid characters!`
45
+ + ' Names must only contain alpha-numeric characters, "-", "_", "."'
46
+ );
47
+ }
48
+ };
49
+
41
50
  class Bru {
42
51
  /**
43
52
  * @param {object} options - Single options object (destructured)
@@ -248,11 +257,7 @@ class Bru {
248
257
  throw new Error('Creating a env variable without specifying a name is not allowed.');
249
258
  }
250
259
 
251
- if (variableNameRegex.test(key) === false) {
252
- throw new Error(
253
- `Variable name: "${key}" contains invalid characters! Names must only contain alpha-numeric characters, "-", "_", "."`
254
- );
255
- }
260
+ assertValidVariableName(key);
256
261
 
257
262
  // Deep-equal compare so object/array writes that mutate in place
258
263
  // (e.g. `const c = bru.getEnvVar('cfg'); c.port = 4000; bru.setEnvVar('cfg', c);`)
@@ -302,6 +307,8 @@ class Bru {
302
307
  throw new Error('Creating a env variable without specifying a name is not allowed.');
303
308
  }
304
309
 
310
+ assertValidVariableName(key);
311
+
305
312
  if (!Object.hasOwn(this.globalEnvironmentVariables, key) || !isEqual(this.globalEnvironmentVariables[key], value)) {
306
313
  this.globalEnvironmentVariables[key] = value;
307
314
  this._globalEnvDirty = true;
@@ -359,12 +366,7 @@ class Bru {
359
366
  throw new Error('Creating a variable without specifying a name is not allowed.');
360
367
  }
361
368
 
362
- if (variableNameRegex.test(key) === false) {
363
- throw new Error(
364
- `Variable name: "${key}" contains invalid characters!`
365
- + ' Names must only contain alpha-numeric characters, "-", "_", "."'
366
- );
367
- }
369
+ assertValidVariableName(key);
368
370
 
369
371
  if (!Object.hasOwn(this.runtimeVariables, key) || !isEqual(this.runtimeVariables[key], value)) {
370
372
  this.runtimeVariables[key] = value;
@@ -373,12 +375,7 @@ class Bru {
373
375
  }
374
376
 
375
377
  getVar(key) {
376
- if (variableNameRegex.test(key) === false) {
377
- throw new Error(
378
- `Variable name: "${key}" contains invalid characters!`
379
- + ' Names must only contain alpha-numeric characters, "-", "_", "."'
380
- );
381
- }
378
+ assertValidVariableName(key);
382
379
 
383
380
  return this.interpolate(this.runtimeVariables[key]);
384
381
  }
@@ -412,12 +409,7 @@ class Bru {
412
409
  throw new Error('Creating a variable without specifying a name is not allowed.');
413
410
  }
414
411
 
415
- if (variableNameRegex.test(key) === false) {
416
- throw new Error(
417
- `Variable name: "${key}" contains invalid characters!`
418
- + ' Names must only contain alpha-numeric characters, "-", "_", "."'
419
- );
420
- }
412
+ assertValidVariableName(key);
421
413
 
422
414
  if (!Object.hasOwn(this.collectionVariables, key) || !isEqual(this.collectionVariables[key], value)) {
423
415
  this.collectionVariables[key] = value;
@@ -139,6 +139,10 @@ class BrunoRequest {
139
139
  return this.req.headers;
140
140
  }
141
141
 
142
+ /**
143
+ * Replaces the whole header set, dropping headers set at collection/folder level.
144
+ * TODO: make this upsert instead, since setHeaders is the bulk form of setHeader.
145
+ */
142
146
  setHeaders(headers) {
143
147
  this.req.headers = headers;
144
148
  }
@@ -0,0 +1,52 @@
1
+ const GrpcMetadataList = require('./grpc-metadata-list');
2
+ const GrpcMessageList = require('./grpc-message-list');
3
+ const GrpcMessage = require('./grpc-message');
4
+
5
+ /**
6
+ * Reached from a hook as `bru.grpc.request`.
7
+ *
8
+ * scalar values are un-interpolated placeholders and become stale values after interpolation
9
+ * similar to the http workflow
10
+ *
11
+ * Keep quickjs shim up to date on any updates to this class
12
+ */
13
+ class BrunoGrpcRequest {
14
+ #request;
15
+
16
+ /**
17
+ * @param {object} request - The prepared gRPC request
18
+ * @param {object} [options]
19
+ * @param {boolean} [options.metadataWritable=false] - When true, `metadata` accepts writes
20
+ * @param {object[]} [options.sentMessages=[]] - What the call sent, as `{ data, timestamp }`.
21
+ * `messages` always answers with these, never with the messages authored in the UI — the two
22
+ * differ whenever the user streams a subset of the authored messages, or none. It is therefore
23
+ * empty in `beforeCallStart`, where the call has yet to send anything.
24
+ * @param {object} [options.message] - The single message about to be sent, as `{ data, timestamp }`.
25
+ * Supplied only by `beforeMessageSend`; in the call hooks the `message` property is absent from
26
+ * the model entirely, so `'message' in bru.grpc.request` is `false` there.
27
+ */
28
+ constructor(request, { metadataWritable = false, sentMessages = [], message } = {}) {
29
+ this.#request = request;
30
+ this.url = request.url;
31
+ this.method = request.method;
32
+ this.methodType = request.methodType;
33
+ this.authMode = request.authMode || 'none';
34
+ this.protoPath = request.protoPath;
35
+ this.name = request.name;
36
+ this.metadata = new GrpcMetadataList(() => this.#metadataEntries(), { writable: metadataWritable });
37
+ // The list clones what it is given, so a hook editing a message cannot reach what the call sent.
38
+ this.messages = new GrpcMessageList(sentMessages);
39
+ if (message) {
40
+ this.message = new GrpcMessage(message);
41
+ }
42
+ }
43
+
44
+ // Provides reference for in-memory edits on setters
45
+ #metadataEntries() {
46
+ this.#request.headers ??= {};
47
+
48
+ return this.#request.headers;
49
+ }
50
+ }
51
+
52
+ module.exports = BrunoGrpcRequest;
@@ -0,0 +1,43 @@
1
+ const { toMetadataObject } = require('./grpc-metadata');
2
+ const GrpcMetadataList = require('./grpc-metadata-list');
3
+ const GrpcMessageList = require('./grpc-message-list');
4
+ const GrpcMessage = require('./grpc-message');
5
+
6
+ /**
7
+ * Reached from hooks as `bru.grpc.response`.
8
+ *
9
+ * `messages`, `metadata` and `trailers` are the same list types `bru.grpc.request` uses, always
10
+ * read-only here.
11
+ *
12
+ * Scalar values have interpolated values unlike the request counterpart.
13
+ * Keep quickjs shim up to date on any updates to this class
14
+ */
15
+ class BrunoGrpcResponse {
16
+ #response;
17
+
18
+ /**
19
+ * @param {object} response - The call so far; complete in `afterCallEnd`, partial in `afterMessageReceive`
20
+ * @param {object} [options]
21
+ * @param {object} [options.message] - The single message just received, as `{ data, timestamp }`.
22
+ * Supplied only by `afterMessageReceive`
23
+ */
24
+ constructor(response, { message } = {}) {
25
+ this.#response = response;
26
+ this.statusCode = response.statusCode;
27
+ this.statusText = response.statusText;
28
+ this.messages = new GrpcMessageList(response.messages);
29
+ this.metadata = new GrpcMetadataList(() => toMetadataObject(this.#response.metadata), { writable: false });
30
+ this.trailers = new GrpcMetadataList(() => toMetadataObject(this.#response.trailers), { writable: false });
31
+ this.duration = response.duration;
32
+
33
+ // Assigned conditionally, as on the request, so `afterCallEnd` has no such property at all.
34
+ if (message) {
35
+ this.message = new GrpcMessage(message);
36
+ }
37
+
38
+ // Deliberately a plain object, where HTTP's `BrunoResponse` returns a callable so `res('user.id')`
39
+ // queries the body. gRPC body (messages) will vary based on method type, so skipping here.
40
+ }
41
+ }
42
+
43
+ module.exports = BrunoGrpcResponse;
@@ -0,0 +1,74 @@
1
+ const { cloneDeep } = require('lodash');
2
+
3
+ /**
4
+ * GrpcMessageList — the `bru.grpc.request.messages` and `bru.grpc.response.messages` API in
5
+ * hooks, and the only way a hook reads gRPC messages. Read-only: both lists report what the call
6
+ * sent and received, so the class exposes no way to change either.
7
+ * Keep quickjs shim up to date on any updates to this class
8
+ */
9
+ class GrpcMessageList {
10
+ #messages;
11
+
12
+ /**
13
+ * @param {object[]} [messages] - The backing messages, deep-cloned once here so a hook editing
14
+ * a message cannot reach what the call actually sent or received. A list is built fresh for each
15
+ * hook run, so this snapshot is never stale.
16
+ */
17
+ constructor(messages) {
18
+ this.#messages = cloneDeep(messages) ?? [];
19
+ }
20
+
21
+ /** @returns {Array} — a copy, so the snapshot cannot be edited through it */
22
+ all() {
23
+ return [...this.#messages];
24
+ }
25
+
26
+ /**
27
+ * The message at `index`. Unary and server-streaming calls have exactly one, hence the default.
28
+ * @param {number} [index=0]
29
+ * @returns {*}
30
+ */
31
+ get(index = 0) {
32
+ return this.all()[index];
33
+ }
34
+
35
+ /** @returns {number} */
36
+ count() {
37
+ return this.all().length;
38
+ }
39
+
40
+ /** @param {Function} fn @param {*} [context] @returns {*} */
41
+ find(fn, context) {
42
+ return this.all().find(context !== undefined ? fn.bind(context) : fn);
43
+ }
44
+
45
+ /** @param {Function} fn @param {*} [context] @returns {Array} */
46
+ filter(fn, context) {
47
+ return this.all().filter(context !== undefined ? fn.bind(context) : fn);
48
+ }
49
+
50
+ /** @param {Function} fn @param {*} [context] @returns {Array} */
51
+ map(fn, context) {
52
+ return this.all().map(context !== undefined ? fn.bind(context) : fn);
53
+ }
54
+
55
+ /** @param {Function} fn @param {*} [context] */
56
+ each(fn, context) {
57
+ this.all().forEach(context !== undefined ? fn.bind(context) : fn);
58
+ }
59
+
60
+ /** @param {Function} fn @param {*} [accumulator] @param {*} [context] @returns {*} */
61
+ reduce(fn, ...args) {
62
+ const bound = args.length > 1 ? fn.bind(args[1]) : fn;
63
+ const messages = this.all();
64
+
65
+ return args.length ? messages.reduce(bound, args[0]) : messages.reduce(bound);
66
+ }
67
+
68
+ /** @returns {Array} — same as `all()`, so `JSON.stringify` yields the messages */
69
+ toJSON() {
70
+ return this.all();
71
+ }
72
+ }
73
+
74
+ module.exports = GrpcMessageList;
@@ -0,0 +1,20 @@
1
+ const { cloneDeep } = require('lodash');
2
+
3
+ /**
4
+ * `bru.grpc.request.message` / `bru.grpc.response.message` in the message hooks — the single message
5
+ * being sent or received, as opposed to the `GrpcMessageList` of all of them.
6
+ */
7
+ class GrpcMessage {
8
+ /**
9
+ * @param {object} [message]
10
+ * @param {*} [message.data] - The parsed message payload
11
+ * @param {number} [message.timestamp] - Epoch ms
12
+ */
13
+ constructor({ data, timestamp } = {}) {
14
+ // Cloned for immutability during script execution
15
+ this.data = cloneDeep(data);
16
+ this.timestamp = timestamp;
17
+ }
18
+ }
19
+
20
+ module.exports = GrpcMessage;
@@ -0,0 +1,214 @@
1
+ const ReadOnlyPropertyList = require('../readonly-property-list');
2
+ const { setMetadataKey } = require('./grpc-metadata');
3
+
4
+ // Extends ReadOnlyPropertyList in dynamic mode: entries are read through the accessor (live headers) on every access.
5
+ // Keep quickjs shim up to date on any updates to this class
6
+ class GrpcMetadataList extends ReadOnlyPropertyList {
7
+ #readMetadata;
8
+ #writable;
9
+
10
+ /**
11
+ * @param {Function} readMetadata - Returns the `{ key: value }` map backing the list. A writable
12
+ * list needs it to return the same live object every call, since that object is what writes edit.
13
+ * @param {object} [options]
14
+ * @param {boolean} [options.writable=false] - When false, write methods throw
15
+ */
16
+ constructor(readMetadata, { writable = false } = {}) {
17
+ super({
18
+ keyProperty: 'key',
19
+ valueProperty: 'value',
20
+ dataSource: () => Object.entries(readMetadata()).map(([key, value]) => ({ key, value }))
21
+ });
22
+ this.#readMetadata = readMetadata;
23
+ this.#writable = writable;
24
+ }
25
+
26
+ // Positional access is deliberately removed from ReadOnlyPropertyList.
27
+ idx = undefined;
28
+
29
+ #assertWritable(method) {
30
+ if (!this.#writable) {
31
+ throw new Error(
32
+ `metadata.${method}() is not available once the call has been sent — change metadata in the beforeCallStart hook`
33
+ );
34
+ }
35
+ }
36
+
37
+ #findKey(name) {
38
+ if (typeof name !== 'string') {
39
+ return undefined;
40
+ }
41
+
42
+ const target = name.toLowerCase();
43
+
44
+ return Object.keys(this.#readMetadata()).find((key) => key.toLowerCase() === target);
45
+ }
46
+
47
+ /**
48
+ * Get the value of an entry by key.
49
+ * @param {string} key
50
+ * @returns {*}
51
+ */
52
+ get(key) {
53
+ const match = this.#findKey(key);
54
+
55
+ return match === undefined ? undefined : this.#readMetadata()[match];
56
+ }
57
+
58
+ /**
59
+ * Get the full entry by key.
60
+ * @param {string} key
61
+ * @returns {object|undefined}
62
+ */
63
+ one(key) {
64
+ const match = this.#findKey(key);
65
+
66
+ return match === undefined ? undefined : { key: match, value: this.#readMetadata()[match] };
67
+ }
68
+
69
+ /**
70
+ * Check whether an entry exists, optionally matching its value too.
71
+ * @param {string} key
72
+ * @param {*} [value]
73
+ * @returns {boolean}
74
+ */
75
+ has(key, value) {
76
+ const match = this.#findKey(key);
77
+
78
+ if (match === undefined) {
79
+ return false;
80
+ }
81
+
82
+ return value === undefined || this.#readMetadata()[match] === value;
83
+ }
84
+
85
+ /**
86
+ * Get the index of an entry, by key string or by `{ key, value }`.
87
+ * @param {string|object} item
88
+ * @returns {number} -1 if not found
89
+ */
90
+ indexOf(item) {
91
+ const match = this.#findKey(typeof item === 'string' ? item : item?.key);
92
+
93
+ if (match === undefined) {
94
+ return -1;
95
+ }
96
+
97
+ const entries = this.all();
98
+
99
+ if (typeof item === 'string') {
100
+ return entries.findIndex((entry) => entry.key === match);
101
+ }
102
+
103
+ return entries.findIndex((entry) => entry.key === match && entry.value === item.value);
104
+ }
105
+
106
+ // ── Iteration overrides (optional context binding) ────────────────────
107
+
108
+ /** @param {Function} fn @param {*} [context] */
109
+ each(fn, context) {
110
+ super.each(context !== undefined ? fn.bind(context) : fn);
111
+ }
112
+
113
+ /** @param {Function} fn @param {*} [context] @returns {object|undefined} */
114
+ find(fn, context) {
115
+ return super.find(context !== undefined ? fn.bind(context) : fn);
116
+ }
117
+
118
+ /** @param {Function} fn @param {*} [context] @returns {Array} */
119
+ filter(fn, context) {
120
+ return super.filter(context !== undefined ? fn.bind(context) : fn);
121
+ }
122
+
123
+ /** @param {Function} fn @param {*} [context] @returns {Array} */
124
+ map(fn, context) {
125
+ return super.map(context !== undefined ? fn.bind(context) : fn);
126
+ }
127
+
128
+ /** @param {Function} fn @param {*} [accumulator] @param {*} [context] @returns {*} */
129
+ reduce(fn, ...args) {
130
+ const bound = args.length > 1 ? fn.bind(args[1]) : fn;
131
+
132
+ return args.length ? super.reduce(bound, args[0]) : super.reduce(bound);
133
+ }
134
+
135
+ // ── Transform override ────────────────────────────────────────────────
136
+
137
+ /** `key: value` per line — how metadata travels as HTTP/2 headers. */
138
+ toString() {
139
+ return this.all()
140
+ .map((entry) => `${entry.key}: ${entry.value}`)
141
+ .join('\n');
142
+ }
143
+
144
+ // ── Write methods (edit the backing map) ──────────────────────────────
145
+
146
+ /**
147
+ * Insert a key, or update it in place when it already exists.
148
+ *
149
+ * @param {string} key
150
+ * @param {*} value
151
+ */
152
+ upsert(key, value) {
153
+ this.#assertWritable('upsert');
154
+
155
+ if (typeof key !== 'string' || !key.length) {
156
+ return;
157
+ }
158
+
159
+ const metadata = this.#readMetadata();
160
+ const existing = this.#findKey(key);
161
+
162
+ // A server reads `X-Token` and `x-token` as one key, so a re-cased upsert replaces instead of
163
+ // leaving two entries the transport would send as duplicates.
164
+ if (existing !== undefined && existing !== key) {
165
+ delete metadata[existing];
166
+ }
167
+
168
+ setMetadataKey(metadata, key, value);
169
+ }
170
+
171
+ /**
172
+ * Upsert an entry from the `{ key, value }` shape `all()` returns, so an entry read from one list
173
+ * can be handed straight to another.
174
+ *
175
+ * @param {object} item
176
+ */
177
+ add(item) {
178
+ this.#assertWritable('add');
179
+
180
+ if (!item || typeof item !== 'object') {
181
+ return;
182
+ }
183
+
184
+ this.upsert(item.key, item.value);
185
+ }
186
+
187
+ /**
188
+ * Remove the entry with the given key.
189
+ * @param {string} key
190
+ */
191
+ remove(key) {
192
+ this.#assertWritable('remove');
193
+
194
+ const existing = this.#findKey(key);
195
+
196
+ if (existing !== undefined) {
197
+ delete this.#readMetadata()[existing];
198
+ }
199
+ }
200
+
201
+ /** Remove every entry. */
202
+ clear() {
203
+ this.#assertWritable('clear');
204
+
205
+ const metadata = this.#readMetadata();
206
+
207
+ // Emptied in place rather than reassigned
208
+ for (const key of Object.keys(metadata)) {
209
+ delete metadata[key];
210
+ }
211
+ }
212
+ }
213
+
214
+ module.exports = GrpcMetadataList;
@@ -0,0 +1,21 @@
1
+ // assigning for a reserved key like '__proto__' works with defineProperty by not polluting the object prototype.
2
+ const setMetadataKey = (target, key, value) => {
3
+ Object.defineProperty(target, key, { value, writable: true, enumerable: true, configurable: true });
4
+ };
5
+
6
+ /**
7
+ * Normalizes the `[{ name, value }]` display shape the response pane renders into a plain object.
8
+ * Multi-valued keys are already joined into one comma-separated string upstream.
9
+ */
10
+ const toMetadataObject = (entries) => {
11
+ if (!Array.isArray(entries)) {
12
+ return {};
13
+ }
14
+
15
+ return entries.reduce((acc, { name, value }) => {
16
+ setMetadataKey(acc, name, value);
17
+ return acc;
18
+ }, {});
19
+ };
20
+
21
+ module.exports = { toMetadataObject, setMetadataKey };