@usebruno/js 0.50.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 +6 -6
- package/src/bru.js +29 -28
- package/src/bruno-request.js +4 -0
- package/src/bruno-response.js +4 -1
- package/src/grpc/bruno-grpc-request.js +52 -0
- package/src/grpc/bruno-grpc-response.js +43 -0
- package/src/grpc/grpc-message-list.js +74 -0
- package/src/grpc/grpc-message.js +20 -0
- package/src/grpc/grpc-metadata-list.js +214 -0
- package/src/grpc/grpc-metadata.js +21 -0
- package/src/grpc/grpc-script-runtime.js +302 -0
- package/src/index.js +2 -0
- package/src/interpolate-string.js +2 -1
- package/src/runtime/assert-runtime.js +1 -0
- package/src/runtime/script-runtime.js +20 -4
- package/src/runtime/test-runtime.js +15 -8
- package/src/sandbox/bundle-browser-rollup.js +32 -32
- package/src/sandbox/node-vm/cjs-loader.js +416 -51
- package/src/sandbox/node-vm/index.js +96 -65
- package/src/sandbox/node-vm/index.spec.js +896 -0
- package/src/sandbox/quickjs/index.js +101 -17
- package/src/sandbox/quickjs/shims/bru.js +54 -54
- package/src/sandbox/quickjs/shims/bruno-grpc.js +29 -0
- package/src/sandbox/quickjs/shims/grpc/bruno-grpc-request.js +47 -0
- package/src/sandbox/quickjs/shims/grpc/bruno-grpc-response.js +40 -0
- package/src/sandbox/quickjs/shims/grpc/grpc-message-list.js +29 -0
- package/src/sandbox/quickjs/shims/grpc/grpc-metadata-list.js +31 -0
- package/src/sandbox/quickjs/utils/index.js +42 -23
- package/src/utils/error-formatter.js +13 -9
- package/src/utils/error-formatter.spec.js +97 -2
- package/src/utils/results.js +30 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usebruno/js",
|
|
3
|
-
"version": "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.
|
|
17
|
+
"@usebruno/common": "0.26.0",
|
|
18
18
|
"@usebruno/query": "0.2.2",
|
|
19
|
-
"ajv": "^8.
|
|
19
|
+
"ajv": "^8.18.0",
|
|
20
20
|
"ajv-formats": "^2.1.1",
|
|
21
21
|
"atob": "^2.1.2",
|
|
22
|
-
"axios": "1.
|
|
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.
|
|
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": "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
@@ -500,13 +492,22 @@ class Bru {
|
|
|
500
492
|
}
|
|
501
493
|
if (type === 'table') {
|
|
502
494
|
if (data?.provider === 'ag-grid') {
|
|
503
|
-
|
|
504
|
-
|
|
495
|
+
const { columnDefinitions, rowData } = data?.props || {};
|
|
496
|
+
let error;
|
|
497
|
+
if (rowData == null) {
|
|
498
|
+
error = 'Row data is required. Please provide an array of objects for table rendering.';
|
|
499
|
+
} else if (!Array.isArray(rowData)) {
|
|
500
|
+
error = `Invalid row data: expected an array of objects, received ${typeof rowData}.`;
|
|
501
|
+
} else if (columnDefinitions == null) {
|
|
502
|
+
error = 'Column definitions are required.';
|
|
503
|
+
} else if (!Array.isArray(columnDefinitions)) {
|
|
504
|
+
error = `Invalid column definitions: expected an array, received ${typeof columnDefinitions}.`;
|
|
505
505
|
}
|
|
506
|
-
if (
|
|
507
|
-
|
|
506
|
+
if (error) {
|
|
507
|
+
this.setVisualizations({ uid: uuid(), type, data: { ...data, error } });
|
|
508
|
+
} else {
|
|
509
|
+
this.setVisualizations({ uid: uuid(), type, data });
|
|
508
510
|
}
|
|
509
|
-
this.setVisualizations({ uid: uuid(), type, data });
|
|
510
511
|
} else if (data?.provider === 'react-table') {
|
|
511
512
|
this.setVisualizations({ uid: uuid(), type, data });
|
|
512
513
|
}
|
package/src/bruno-request.js
CHANGED
|
@@ -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
|
}
|
package/src/bruno-response.js
CHANGED
|
@@ -32,7 +32,10 @@ class BrunoResponse {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
getHeader(name) {
|
|
35
|
-
|
|
35
|
+
if (typeof name !== 'string' || !this.res?.headers) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return this.res.headers[name.toLowerCase()];
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
getHeaders() {
|
|
@@ -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 };
|