Add JSONpath option to AggregateValues service
Currently the AggregateValues enhancement service only supports the own "navigation" options. It could also be extended to use JSONpath like some of the other services (e.g. ListManipulator) to get the values to aggregate.
This would however complicate the caching that is currently implemented.
The relevant code from the ListManipulator for JSONpath:
/**
* Gets all the values to be used for processing using one or several JSONpath queries.
* It is important to consider what exactly the JSONpath query is selecting (no matter if _query or _value), since it will put it exactly like that into the result with one exception: if a "source_value" selects an array then the arrays values are added to the result (to provide a simple shortcut).
* Example: If the data looks like:
* {
* "a": {
* "b": [1, 2, 3]
* }
* }
* Then the following results will be achieved:
* * source_query = "$.a.b" -> [[1, 2, 3]] // note the array in the array
* * source_query = "$.a.b.*" -> [1, 2, 3] // the .* at the end selects all the values of "b" instead
* * source_value = "$.a.b" -> [1, 2, 3] // shortcut for _value which places the values of "b" in the result
* @param {Object} instruction an object containing the details of where to select the values from.
* @param {string|string[]} instruction.source_query one or several JSONpath queries. Each query can result in no, one or several values.
* @param {string|string[]} instruction.source_value one or several JSONpath value queries. Each value query can result in no or one value. If multiple values would have been possible only the first one is returned.
* @param {Object} obj the object from where to
* @returns an array containing the values that have been selected. It can be empty, contain values, objects or even other arrays, depending on what exactly the source_query / source_value has selected.
*/
#getValuesToUse = function(instruction, obj) {
let result = [];
// Put all the results from the queries into the result
if (typeof(instruction.source_query) === 'string') {
result.push(...jp.query(obj, instruction.source_query));
} else if (Array.isArray(instruction.source_query)) {
for (const query of instruction.source_query) {
result.push(...jp.query(obj, query));
}
}
// Put all the results from the value into the result
let tempvalue;
if (typeof(instruction.source_value) === 'string') {
tempvalue = jp.value(obj, instruction.source_value);
if (Array.isArray(tempvalue)) {
result.push(...tempvalue);
} else {
result.push(tempvalue);
}
} else if (Array.isArray(instruction.source_value)) {
for (const query of instruction.source_value) {
tempvalue = jp.value(obj, query);
if (Array.isArray(tempvalue)) {
result.push(...tempvalue);
} else {
result.push(tempvalue);
}
}
}
return result;
};
And also the code to set values using JSONpath (also from ListManipulator)
#setValueInObject = function(instruction, obj, value) {
let pushToTarget = false; // (typeof instruction['target_push'] === 'boolean') && instruction['target_push'];
if (instruction.target_query) {
// Note: this only works if the paths for target_query already exist. It will not create new paths. Would have to be improved to be properly usable though, e.g. mixing it with a target_value (target_query selects all the object that the target_value should then be applied to).
let queryPaths = jp.paths(obj, instruction.target_query);
for (const path of queryPaths) {
this.#setTargetValue(obj, path, value, pushToTarget);
}
} else if (instruction.target_value) {
this.#setTargetValue(obj, instruction.target_value, value, pushToTarget);
}
};
/**
* Sets a value in an object based on a provided JSONpath.
* @param {Object} obj the object where the value should be set.
* @param {string|string[]} path the path in the object where the value should be set.
* @param {*} value the value to set at the end of the path.
* @param {boolean} pushToTarget if true then it will either push the value into the array at the end of the path, or set the value at the end of the path to be an array containing the value.
*/
#setTargetValue = function(obj, path, value, pushToTarget) {
if (! this.disableAdditionalLogging) {
console.log(`Setting value of type ${typeof value} at path ${path}. Pushing into array is ${pushToTarget}.`);
}
if (pushToTarget) {
// Try to push it into an existing array and should that fail set the value to be an array instead.
try {
jp.value(obj, path).push(value);
} catch (err) {
jp.value(obj, path, [value]);
}
} else {
jp.value(obj, path, value);
}
};