@itentialopensource/adapter-utils 4.44.9 → 4.45.2
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/CHANGELOG.md +50 -0
- package/lib/connectorRest.js +16 -2
- package/lib/dbUtil.js +71 -8
- package/lib/propertyUtil.js +13 -1
- package/lib/requestHandler.js +2 -2
- package/lib/restHandler.js +33 -3
- package/package.json +4 -4
- package/{actionSchema.json → schemas/actionSchema.json} +0 -0
- package/schemas/globalSchema.json +19 -0
- package/{propertiesSchema.json → schemas/propertiesSchema.json} +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,54 @@
|
|
|
1
1
|
|
|
2
|
+
## 4.45.2 [12-09-2021]
|
|
3
|
+
|
|
4
|
+
* change for sending content-length with empty token body
|
|
5
|
+
|
|
6
|
+
Closes ADAPT-1889
|
|
7
|
+
|
|
8
|
+
See merge request itentialopensource/adapter-utils!228
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 4.45.1 [12-08-2021]
|
|
13
|
+
|
|
14
|
+
* Modified response handling on error to use the translator
|
|
15
|
+
|
|
16
|
+
Closes ADAPT-1876
|
|
17
|
+
|
|
18
|
+
See merge request itentialopensource/adapter-utils!225
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 4.45.0 [12-08-2021]
|
|
23
|
+
|
|
24
|
+
* changes for uriOptions and not encoding query params
|
|
25
|
+
|
|
26
|
+
Closes ADAPT-1866
|
|
27
|
+
|
|
28
|
+
See merge request itentialopensource/adapter-utils!227
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 4.44.11 [10-25-2021]
|
|
33
|
+
|
|
34
|
+
* change to the npm registry url
|
|
35
|
+
|
|
36
|
+
Closes ADAPT-1793
|
|
37
|
+
|
|
38
|
+
See merge request itentialopensource/adapter-utils!224
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## 4.44.10 [10-25-2021]
|
|
43
|
+
|
|
44
|
+
* fixes for dbUtils, proxy, misspelling
|
|
45
|
+
|
|
46
|
+
Closes ADAPT-1019
|
|
47
|
+
|
|
48
|
+
See merge request itentialopensource/adapter-utils!223
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
2
52
|
## 4.44.9 [09-14-2021]
|
|
3
53
|
|
|
4
54
|
* Refactor storage handling
|
package/lib/connectorRest.js
CHANGED
|
@@ -640,7 +640,7 @@ function makeRequest(request, entitySchema, callProperties, startTrip, attempt,
|
|
|
640
640
|
} else if (proxyEnabled) {
|
|
641
641
|
let proxy = `${proxyProtocol}://${proxyHost}:${proxyPort}`;
|
|
642
642
|
if (proxyUser && proxyPassword) {
|
|
643
|
-
proxy = `${
|
|
643
|
+
proxy = `${proxyProtocol}://${proxyUser}:${proxyPassword}@${proxyHost}:${proxyPort}`;
|
|
644
644
|
}
|
|
645
645
|
request.header.agent = new HttpsProxyAgent(proxy);
|
|
646
646
|
|
|
@@ -1760,6 +1760,20 @@ function buildTokenRequest(reqPath, reqBody, callProperties, callback) {
|
|
|
1760
1760
|
}
|
|
1761
1761
|
}
|
|
1762
1762
|
|
|
1763
|
+
// only add global options if there are global options to add
|
|
1764
|
+
if (globalRequest && globalRequest.uriOptions
|
|
1765
|
+
&& Object.keys(globalRequest.uriOptions).length > 0) {
|
|
1766
|
+
const optionString = querystring.stringify(globalRequest.uriOptions);
|
|
1767
|
+
|
|
1768
|
+
// if no query paramters yet - start with ?
|
|
1769
|
+
if (options.path.indexOf('?') < 0) {
|
|
1770
|
+
options.path += `?${optionString}`;
|
|
1771
|
+
} else {
|
|
1772
|
+
// if already have query parameters, add on to end
|
|
1773
|
+
options.path += `&${optionString}`;
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1763
1777
|
// remove the path vars from the reqBody
|
|
1764
1778
|
const actReqBody = Object.assign({}, reqBody);
|
|
1765
1779
|
if (actReqBody && actReqBody.uriPathVars) {
|
|
@@ -1917,7 +1931,7 @@ function buildTokenRequest(reqPath, reqBody, callProperties, callback) {
|
|
|
1917
1931
|
|
|
1918
1932
|
// if there is a body, set the content length of the body and add it to
|
|
1919
1933
|
// the header
|
|
1920
|
-
if (Object.keys(tokenEntity).length > 0) {
|
|
1934
|
+
if (Object.keys(tokenEntity).length > 0 || tokenSchema.sendEmpty) {
|
|
1921
1935
|
options.headers['Content-length'] = Buffer.byteLength(bodyString);
|
|
1922
1936
|
}
|
|
1923
1937
|
|
package/lib/dbUtil.js
CHANGED
|
@@ -14,6 +14,7 @@ const { MongoClient } = require('mongodb');
|
|
|
14
14
|
let adapterDir = '';
|
|
15
15
|
let saveFS = false;
|
|
16
16
|
let storDir = `${adapterDir}/storage`;
|
|
17
|
+
let entityDir = `${adapterDir}/entities`;
|
|
17
18
|
let id = null;
|
|
18
19
|
|
|
19
20
|
const Storage = {
|
|
@@ -60,6 +61,55 @@ function getFromJson(fileName, filter) {
|
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
try {
|
|
64
|
+
if (fileName === 'adapter_configs') {
|
|
65
|
+
const retEntity = filter.entity;
|
|
66
|
+
if (!retEntity || !fs.existsSync(`${entityDir}`) || !fs.existsSync(`${entityDir}/${retEntity}`)) {
|
|
67
|
+
log.warn(`${origin}: Could not find adapter entity directory ${retEntity} - nothing to retrieve`);
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
if (!fs.existsSync(`${entityDir}/${retEntity}/action.json`)) {
|
|
71
|
+
log.warn(`${origin}: Could not find action.json for entity ${retEntity} - nothing to retrieve`);
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// load the mockdatafiles
|
|
76
|
+
let files = fs.readdirSync(`${entityDir}/${retEntity}`);
|
|
77
|
+
const mockdatafiles = {};
|
|
78
|
+
if (files.includes('mockdatafiles') && fs.lstatSync(`${entityDir}/${retEntity}/mockdatafiles`).isDirectory()) {
|
|
79
|
+
fs.readdirSync(`${entityDir}/${retEntity}/mockdatafiles`).forEach((file) => {
|
|
80
|
+
if (file.split('.').pop() === 'json') {
|
|
81
|
+
const mockpath = `${entityDir}/${retEntity}/mockdatafiles/${file}`;
|
|
82
|
+
const data = JSON.parse(fs.readFileSync(mockpath));
|
|
83
|
+
mockdatafiles[mockpath.split('/').pop()] = data;
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// load the action data
|
|
89
|
+
let actions;
|
|
90
|
+
if (files.includes('action.json')) {
|
|
91
|
+
actions = JSON.parse(fs.readFileSync(`${entityDir}/${retEntity}/action.json`));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Load schema.json and other schemas in remaining json files
|
|
95
|
+
files = files.filter((f) => (f !== 'action.json') && f.endsWith('.json'));
|
|
96
|
+
const schema = [];
|
|
97
|
+
files.forEach((file) => {
|
|
98
|
+
const data = JSON.parse(fs.readFileSync(`${entityDir}/${retEntity}/${file}`));
|
|
99
|
+
schema.push({
|
|
100
|
+
name: file,
|
|
101
|
+
schema: data
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// return the data
|
|
106
|
+
return {
|
|
107
|
+
actions: actions.actions,
|
|
108
|
+
schema,
|
|
109
|
+
mockdatafiles
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
63
113
|
// make sure what we need exists and has been provided
|
|
64
114
|
if (!fs.existsSync(`${storDir}`)) {
|
|
65
115
|
log.warn(`${origin}: Could not find adapter storage directory - nothing to retrieve`);
|
|
@@ -80,21 +130,29 @@ function getFromJson(fileName, filter) {
|
|
|
80
130
|
});
|
|
81
131
|
} else {
|
|
82
132
|
// if not metric must match the items in the filter
|
|
83
|
-
|
|
133
|
+
// if no filter, match everything
|
|
134
|
+
let key = [];
|
|
135
|
+
if (useFilter && useFilter.length > 0) {
|
|
136
|
+
key = Object.keys(useFilter);
|
|
137
|
+
}
|
|
84
138
|
content.table.forEach((item) => {
|
|
85
139
|
let push = true;
|
|
140
|
+
// check each key - if it does not match a key do not add to return data
|
|
86
141
|
key.forEach((fil) => {
|
|
87
142
|
if (useFilter[fil] !== item[fil]) push = false;
|
|
88
143
|
});
|
|
89
144
|
if (push) toReturn.push(item);
|
|
90
145
|
});
|
|
91
146
|
}
|
|
147
|
+
// not sure why we do this - a little different than above.
|
|
92
148
|
const filtered = content.table.filter((el) => {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
149
|
+
if (useFilter) {
|
|
150
|
+
Object.keys(useFilter).forEach((obj) => {
|
|
151
|
+
if (el[obj] !== useFilter[obj]) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
98
156
|
return true;
|
|
99
157
|
});
|
|
100
158
|
return toReturn;
|
|
@@ -401,6 +459,7 @@ class DBUtil {
|
|
|
401
459
|
this.baseDir = directory;
|
|
402
460
|
adapterDir = this.baseDir;
|
|
403
461
|
storDir = `${adapterDir}/storage`;
|
|
462
|
+
entityDir = `${adapterDir}/entities`;
|
|
404
463
|
this.props = properties;
|
|
405
464
|
this.adapterMongoClient = null;
|
|
406
465
|
|
|
@@ -1190,7 +1249,11 @@ class DBUtil {
|
|
|
1190
1249
|
// if using file storage
|
|
1191
1250
|
if (storage === Storage.FILESYSTEM) {
|
|
1192
1251
|
// Find it from file in the adapter
|
|
1193
|
-
let toReturn = getFromJson(collectionName,
|
|
1252
|
+
let toReturn = getFromJson(collectionName, options);
|
|
1253
|
+
if (collectionName === 'adapter_configs') {
|
|
1254
|
+
log.debug(`${origin}: Data retrieved from file storage`);
|
|
1255
|
+
return callback(null, [toReturn]);
|
|
1256
|
+
}
|
|
1194
1257
|
if (toReturn && toReturn.length > limit) {
|
|
1195
1258
|
let curEnd = start + limit;
|
|
1196
1259
|
if (curEnd < toReturn.length) {
|
|
@@ -1198,7 +1261,7 @@ class DBUtil {
|
|
|
1198
1261
|
}
|
|
1199
1262
|
toReturn = toReturn.slice(start, curEnd);
|
|
1200
1263
|
}
|
|
1201
|
-
log.
|
|
1264
|
+
log.debug(`${origin}: Data retrieved from file storage`);
|
|
1202
1265
|
return callback(null, toReturn);
|
|
1203
1266
|
}
|
|
1204
1267
|
|
package/lib/propertyUtil.js
CHANGED
|
@@ -187,6 +187,7 @@ class AdapterPropertyUtil {
|
|
|
187
187
|
// get the path for the specific schema file
|
|
188
188
|
const reqSchemaFile = path.join(this.baseDir, `/entities/${entityName}/${reqSchemaName}`);
|
|
189
189
|
const respSchemaFile = path.join(this.baseDir, `/entities/${entityName}/${respSchemaName}`);
|
|
190
|
+
const errorSchemaFile = path.join(this.baseDir, '/entities/.system/errorSchema');
|
|
190
191
|
|
|
191
192
|
// if the file does not exist - error
|
|
192
193
|
if (!fs.existsSync(reqSchemaFile)) {
|
|
@@ -244,6 +245,16 @@ class AdapterPropertyUtil {
|
|
|
244
245
|
throw new Error(JSON.stringify(errorObj));
|
|
245
246
|
}
|
|
246
247
|
|
|
248
|
+
// if the error schema file exist - read it in
|
|
249
|
+
if (fs.existsSync(errorSchemaFile)) {
|
|
250
|
+
entitySchema.errorSchema = JSON.parse(fs.readFileSync(errorSchemaFile, 'utf-8'));
|
|
251
|
+
|
|
252
|
+
// if the error schema file is bad, warn about it but continue and use the global on!
|
|
253
|
+
if (entitySchema.errorSchema && typeof entitySchema.errorSchema !== 'object') {
|
|
254
|
+
log.warn(`${origin}: Invalid error schema, please verify file: ${errorSchemaFile}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
247
258
|
// Merge the information into the entity schema
|
|
248
259
|
entitySchema.protocol = actionInfo.protocol;
|
|
249
260
|
entitySchema.method = actionInfo.method;
|
|
@@ -428,7 +439,8 @@ class AdapterPropertyUtil {
|
|
|
428
439
|
filter: {
|
|
429
440
|
id: this.myid,
|
|
430
441
|
entity: entityName
|
|
431
|
-
}
|
|
442
|
+
},
|
|
443
|
+
entity: entityName
|
|
432
444
|
};
|
|
433
445
|
|
|
434
446
|
// call to get the adapter schema from the database
|
package/lib/requestHandler.js
CHANGED
|
@@ -51,7 +51,7 @@ function validateProperties(properties) {
|
|
|
51
51
|
|
|
52
52
|
try {
|
|
53
53
|
// get the path for the specific action file
|
|
54
|
-
const propertyFile = path.join(__dirname, '/../propertiesSchema.json');
|
|
54
|
+
const propertyFile = path.join(__dirname, '/../schemas/propertiesSchema.json');
|
|
55
55
|
|
|
56
56
|
// Read the action from the file system
|
|
57
57
|
const propertySchema = JSON.parse(fs.readFileSync(propertyFile, 'utf-8'));
|
|
@@ -113,7 +113,7 @@ function walkThroughActionFiles(directory) {
|
|
|
113
113
|
|
|
114
114
|
try {
|
|
115
115
|
// Read the action schema from the file system
|
|
116
|
-
const actionSchemaFile = path.join(__dirname, '/../actionSchema.json');
|
|
116
|
+
const actionSchemaFile = path.join(__dirname, '/../schemas/actionSchema.json');
|
|
117
117
|
const actionSchema = JSON.parse(fs.readFileSync(actionSchemaFile, 'utf-8'));
|
|
118
118
|
const entitydir = `${directory}/entities`;
|
|
119
119
|
|
package/lib/restHandler.js
CHANGED
|
@@ -9,6 +9,8 @@ const jsonQuery = require('json-query');
|
|
|
9
9
|
const jsonxml = require('jsontoxml');
|
|
10
10
|
const xml2js = require('xml2js');
|
|
11
11
|
|
|
12
|
+
const globalSchema = JSON.parse(require('fs').readFileSync(require('path').join(__dirname, '/../schemas/globalSchema.json')));
|
|
13
|
+
|
|
12
14
|
let transUtilInst = null;
|
|
13
15
|
let connectorInst = null;
|
|
14
16
|
|
|
@@ -19,6 +21,7 @@ let basepathGl = null;
|
|
|
19
21
|
let globalRequestGl = null;
|
|
20
22
|
let returnRawGl = false;
|
|
21
23
|
let encodePath = true;
|
|
24
|
+
let encodeUri = true;
|
|
22
25
|
|
|
23
26
|
// INTERNAL FUNCTIONS
|
|
24
27
|
/*
|
|
@@ -178,7 +181,13 @@ function handleRestRequest(request, entityId, entitySchema, callProperties, filt
|
|
|
178
181
|
|
|
179
182
|
// if the return error message was JSON then return the parsed object
|
|
180
183
|
if (retError !== null) {
|
|
181
|
-
|
|
184
|
+
// if there is a local error schema in the entity use that one
|
|
185
|
+
if (Object.hasOwnProperty.call(entitySchema, 'errorSchema')) {
|
|
186
|
+
retErrorObj.response = transUtilInst.mapFromOutboundEntity(retError, entitySchema.errorSchema);
|
|
187
|
+
} else {
|
|
188
|
+
// if there is a no local error schema in the entity use that one
|
|
189
|
+
retErrorObj.response = transUtilInst.mapFromOutboundEntity(retError, globalSchema);
|
|
190
|
+
}
|
|
182
191
|
}
|
|
183
192
|
|
|
184
193
|
// return the error response
|
|
@@ -460,6 +469,9 @@ function handleRestRequest(request, entityId, entitySchema, callProperties, filt
|
|
|
460
469
|
return callback(retObject);
|
|
461
470
|
}
|
|
462
471
|
|
|
472
|
+
// Apply global schema
|
|
473
|
+
// retResponse = transUtilInst.mapFromOutboundEntity(retResponse, globalSchema);
|
|
474
|
+
|
|
463
475
|
// added the translated response to the return Object
|
|
464
476
|
retObject.response = transUtilInst.mapFromOutboundEntity(retResponse, entitySchema.responseSchema);
|
|
465
477
|
|
|
@@ -677,7 +689,23 @@ function buildRequestPath(entity, action, entitySchema, reqPath, uriPathVars, ur
|
|
|
677
689
|
addquery = entitySchema.querykey;
|
|
678
690
|
}
|
|
679
691
|
if (systemQuery !== null) {
|
|
680
|
-
|
|
692
|
+
// if we are encoding - use querystring since it does it all!
|
|
693
|
+
if (encodeUri === true) {
|
|
694
|
+
addquery += querystring.stringify(systemQuery);
|
|
695
|
+
} else {
|
|
696
|
+
// if not encoding we need to build
|
|
697
|
+
const qkeys = Object.keys(systemQuery);
|
|
698
|
+
|
|
699
|
+
// add each query parameter and its value
|
|
700
|
+
for (let k = 0; k < qkeys.length; k += 1) {
|
|
701
|
+
// need to add separator for everything after the first one
|
|
702
|
+
if (k > 0) {
|
|
703
|
+
addquery += '&';
|
|
704
|
+
}
|
|
705
|
+
// adds key=value
|
|
706
|
+
addquery += `${qkeys[k]}=${systemQuery[qkeys[k]]}`;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
681
709
|
}
|
|
682
710
|
}
|
|
683
711
|
|
|
@@ -897,7 +925,7 @@ function buildPayload(entity, action, entitySchema, payload) {
|
|
|
897
925
|
}
|
|
898
926
|
}
|
|
899
927
|
} else {
|
|
900
|
-
log.warn(`${origin}: Payload and
|
|
928
|
+
log.warn(`${origin}: Payload and Global Payload can not be merged!`);
|
|
901
929
|
}
|
|
902
930
|
}
|
|
903
931
|
|
|
@@ -988,6 +1016,8 @@ class RestHandler {
|
|
|
988
1016
|
this.globalRequest = null;
|
|
989
1017
|
this.encode = properties.encode_pathvars;
|
|
990
1018
|
encodePath = this.encode;
|
|
1019
|
+
this.encodeQ = properties.encode_queryvars;
|
|
1020
|
+
encodeUri = this.encodeQ;
|
|
991
1021
|
|
|
992
1022
|
// only need to set returnRaw if the property is true - defaults to false
|
|
993
1023
|
if (properties.request && properties.request.return_raw) {
|
package/package.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@itentialopensource/adapter-utils",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.45.2",
|
|
4
4
|
"description": "Itential Adapter Utility Libraries",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"preinstall": "node utils/setup.js",
|
|
7
7
|
"lint": "eslint . --ext .json --ext .js",
|
|
8
8
|
"lint:errors": "eslint --quiet . --ext .json --ext .js",
|
|
9
|
-
"test:unit": "mocha test/unit/lib/requestHandlerTest.js --LOG=error && mocha test/unit/lib/restHandlerTest.js --LOG=error && mocha test/unit/lib/propertyUtilTest.js --LOG=error && mocha test/unit/lib/translatorUtilTest.js --LOG=error && mocha test/unit/lib/dbUtilTest.js --LOG=
|
|
9
|
+
"test:unit": "mocha test/unit/lib/requestHandlerTest.js --LOG=error && mocha test/unit/lib/restHandlerTest.js --LOG=error && mocha test/unit/lib/propertyUtilTest.js --LOG=error && mocha test/unit/lib/translatorUtilTest.js --LOG=error && mocha test/unit/lib/dbUtilTest.js --LOG=debug",
|
|
10
10
|
"test:integration": "mocha test/integration/lib/requestHandlerTest.js --LOG=error && mocha test/integration/lib/restHandlerTest.js --LOG=error",
|
|
11
11
|
"test:cover": "nyc --reporter html --reporter text mocha --recursive --reporter dot test/*",
|
|
12
12
|
"test": "npm run test:unit && npm run test:integration",
|
|
13
|
-
"deploy": "npm publish --registry=
|
|
13
|
+
"deploy": "npm publish --registry=https://registry.npmjs.org --access=public",
|
|
14
14
|
"build": "npm run deploy"
|
|
15
15
|
},
|
|
16
16
|
"license": "Apache-2.0",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"eslint-plugin-json": "^3.0.0",
|
|
52
52
|
"mocha": "^9.0.1",
|
|
53
53
|
"nyc": "^15.1.0",
|
|
54
|
-
"strip-ansi": "^7.0.
|
|
54
|
+
"strip-ansi": "^7.0.1",
|
|
55
55
|
"strip-ansi-cli": "^3.0.1",
|
|
56
56
|
"testdouble": "^3.16.1",
|
|
57
57
|
"winston": "^3.3.3"
|
|
File without changes
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$id": "schema.json",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"schema": "http://json-schema.org/draft-07/schema#",
|
|
5
|
+
"translate": true,
|
|
6
|
+
"dynamicfields": true,
|
|
7
|
+
"properties": {
|
|
8
|
+
"id": {
|
|
9
|
+
"type": "string",
|
|
10
|
+
"description": "Restricted for: special use within mongo",
|
|
11
|
+
"encrypt": {
|
|
12
|
+
"type": "AES",
|
|
13
|
+
"key": ""
|
|
14
|
+
},
|
|
15
|
+
"external_name": "$id"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"definitions": {}
|
|
19
|
+
}
|
|
@@ -51,6 +51,11 @@
|
|
|
51
51
|
"description": "When true the path variables are encoded in the url",
|
|
52
52
|
"default": true
|
|
53
53
|
},
|
|
54
|
+
"encode_queryvars": {
|
|
55
|
+
"type": "boolean",
|
|
56
|
+
"description": "When true the query variables are encoded in the url",
|
|
57
|
+
"default": true
|
|
58
|
+
},
|
|
54
59
|
"save_metric": {
|
|
55
60
|
"type": [
|
|
56
61
|
"boolean",
|