mappingutils
Version:
Lightweight JSON transformation utility.
358 lines • 14.6 kB
JavaScript
import jp, {} from "jsonpath";
import { isArrPath, isStrPath, parse } from "./lib/validators.js";
import { cartesian } from "./lib/cartesian.js";
/**
* Finds the upper level array type node path
* @example
* findParent(['$', 'items', 0, 'availableCountries', 0, 'country'], 2)
* returns ['$', 'items', 0]
*/
function findParentPath(path, level) {
let occurrence = 0;
for (let i = path.length - 1; i > 0; i--) {
if (Number.isInteger(path[i]))
occurrence++;
if (occurrence === level)
return path.slice(0, i + 1);
}
if (occurrence < level) {
return path.slice(0, -1);
}
return [];
}
/**
* Checks if path contains another node path
* @example
* includesPath(['$', 'items', 0, 'availableCountries', 0, 'country'], ['$', 'items', 0]) returns true
*/
function includesPath(path, otherPath) {
for (let i = 0; i < otherPath.length; i++) {
if (path[i] !== otherPath[i])
return false;
}
return true;
}
function computeFunction(fn, ...args) {
let cleanedArgs = args.map((arg) => {
if (!arg) {
return "";
}
else {
return arg;
}
});
let computed = fn(...cleanedArgs);
if (typeof computed === "function") {
throw new Error("the function cannot return a function type value");
}
if (typeof computed === "undefined") {
return "";
}
return computed;
}
function isCommonProp(nodes) {
if (nodes.length === 1) {
let node = nodes[0];
let lastNumberIndex = node?.path.findLastIndex((n) => Number.isInteger(n));
return !(lastNumberIndex !== node.path.length - 1 &&
node.path.filter((n) => Number.isInteger(n)).length > 1);
}
else {
return false;
}
}
async function resolveNestedPromises(input) {
if (input instanceof Promise) {
let resolved = await input;
return resolveNestedPromises(resolved);
}
if (Array.isArray(input)) {
let resolvedArray = await Promise.all(input.map((item) => resolveNestedPromises(item)));
return resolvedArray;
}
if (input && typeof input === "object") {
let resolvedEntries = await Promise.all(Object.entries(input).map(async ([key, value]) => {
let resolvedValue = await resolveNestedPromises(value);
return [key, resolvedValue];
}));
return Object.fromEntries(resolvedEntries);
}
return input;
}
/**
* Add a `key` property to the object `obj` with the value `value`.
* @param obj - The input object.
* @param key - The path in the resulting object to set the value.
* @param value - The value to set.
* @returns A deep copy of the object with the new property added.
*/
export function addProp(obj, key, value) {
if (key.includes("[]")) {
key = key.replaceAll("[]", "[0]");
}
let nodes = jp.nodes(obj, key);
if (nodes.length === 0) {
let star = key.lastIndexOf("*");
if (star > 0)
key = key.substring(0, star) + 0 + key.substring(star + 1);
}
jp.value(obj, key, value);
return obj;
}
/**
* Merges the values of the `prop` property from each object within the `objArr` array.
* @param objArr - An array of objects.
* @param prop - The path to the array property to merge.
* @returns A deep copy of the first object in `objArr`, with its `prop` array containing the concatenated values from all objects in the `objArr`.
*/
export function mergeObjArr(objArr, prop) {
objArr = structuredClone(objArr);
let firstObj = objArr.shift() ?? {};
if (prop.includes("[") && prop.includes("]")) {
prop = prop.replaceAll("[]", "[*]");
}
let arrToMerge = jp.query(firstObj, prop);
let to = prop;
for (let i = 0; i < objArr.length; i++) {
arrToMerge = arrToMerge.concat(jp.query(objArr[i], to));
}
if (to.slice(-3) === "[*]")
to = to.substring(0, to.length - 3);
return addProp(firstObj ?? {}, to, arrToMerge);
}
/**
* Transforms the `source` object based on the provided `mapping` transformation.
*
* @param source - A source object to transform.
* @param mapping - A mapping object defining the transformation rules. Each mapping object's key-value pair should use JSONPath syntax:
* - The key represents the target field path in the transformed object.
* - The value represents the source field path(s) in the source object.
* - The value can be a literal.
* - If a single source field is required, the value should be a JSONPath string pointing to that field.
* - If a default value is needed, the value can be any valid JSON value.
* - If multiple source fields are required for applying a function, provide an array where:
* - Each element before the last is a JSONPath string pointing to a source field.
* - The last element is a function that takes the resolved source values as arguments and computes the target field value.
*
* @returns An array of transformed objects, with fields derived from applying the `mapping` to the `source` object.
*/
export function mapObj(source, mapping) {
parse(mapping);
let commonProps = {};
let propToObj = new Map();
let propsToMerge = new Set();
let arrNodes = [];
for (let [to, from] of Object.entries(mapping)) {
if (from === undefined)
continue;
if (to.includes("[") && to.includes("]")) {
propsToMerge.add(to);
}
if (!(isStrPath(from) || isArrPath(from))) {
commonProps = addProp(commonProps, to, from);
continue;
}
if (isStrPath(from)) {
let nodes = jp.nodes(source, from);
if (nodes.length === 0)
continue;
if (isCommonProp(nodes)) {
commonProps = addProp(commonProps, to, nodes[0]?.value);
}
else {
for (let node of nodes) {
node.to = to;
}
arrNodes = arrNodes.concat(nodes);
}
}
else if (isArrPath(from) && from.length === 2) {
let arg = from.at(0);
let fn = from.at(1);
let nodes = jp.nodes(source, arg);
if (nodes.length === 0)
continue;
if (isCommonProp(nodes)) {
let value = computeFunction(fn, nodes[0]?.value);
commonProps = addProp(commonProps, to, value);
}
else {
for (let node of nodes) {
node.to = to;
node.value = computeFunction(fn, node.value);
}
arrNodes = arrNodes.concat(nodes);
}
}
else if (isArrPath(from) && from.length > 2) {
let fn = from.at(-1);
let args = from.slice(0, -1);
let argsPaths = [];
let argsValues = [];
for (let arg of args) {
let values = [];
let paths = [];
let nodes = jp.nodes(source, arg);
for (let node of nodes) {
paths.push(node.path);
values.push(node.value);
}
argsPaths.push(paths);
argsValues.push(values);
}
argsValues = argsValues.map((arr) => {
if (arr.length === 0) {
return [undefined];
}
else
return arr;
});
let cartesianValues = cartesian(...argsValues);
let cartesianPaths = cartesian(...argsPaths);
let computedValues = [];
for (let product of cartesianValues) {
let val = computeFunction(fn, ...product);
computedValues.push(val);
}
if (!argsPaths.flat(2).some((el) => Number.isInteger(el))) {
for (let value of computedValues) {
commonProps = addProp(commonProps, to, value);
}
continue;
}
let cnodes = computedValues.map((value, i) => {
return {
value,
path: cartesianPaths[i],
to,
};
});
arrNodes = arrNodes.concat(cnodes);
}
}
if (arrNodes.length > 0) {
arrNodes.sort((a, b) => b.path.length - a.path.length);
for (let node of arrNodes) {
if (node.ignore)
continue;
let key = findParentPath(node.path, 1)?.toString() ?? "";
let obj = propToObj.get(key) ?? {};
obj = addProp(obj, node.to, node.value);
let parentPath = findParentPath(node.path, 2);
if (parentPath) {
let parentNodes = arrNodes.filter((otherNode) => includesPath(otherNode.path, parentPath) &&
otherNode.path.length < node.path.length);
if (parentNodes && parentNodes.length > 0) {
for (let pNode of parentNodes) {
pNode.ignore = true;
obj = addProp(obj, pNode.to, pNode.value);
}
}
}
propToObj.set(key, obj);
}
}
if (Object.keys(commonProps).length > 0) {
if (propToObj.size === 0) {
return [commonProps];
}
for (let obj of propToObj.values()) {
Object.assign(obj, commonProps);
}
}
if (propsToMerge.size > 0) {
let propParentToObjArr = new Map();
for (let to of propsToMerge.values()) {
let nodesMatch = arrNodes.filter((node) => node.to === to);
for (let node of nodesMatch) {
let oldKey = findParentPath(node.path, 1)?.toString() ?? "";
let newKey = findParentPath(node.path, 2)?.toString() ?? "";
let foundNode = propToObj.get(oldKey);
if (foundNode) {
propToObj.delete(oldKey);
let arr = propParentToObjArr.get(newKey) ?? [];
arr.push(foundNode);
propParentToObjArr.set(newKey, [
mergeObjArr(arr, to.substring(0, to.lastIndexOf("[]") + 2)),
]);
}
}
}
return Array.from(new Map([...propParentToObjArr, ...propToObj]).values()).flat();
}
else {
return Array.from(propToObj.values()).flat();
}
}
/**
* Transforms the `source` object based on the provided `mapping` transformation.
*
* @param source - A source object to transform.
* @param mapping - A mapping object defining the transformation rules. Each mapping object's key-value pair should use JSONPath syntax:
* - The key represents the target field path in the transformed object.
* - The value represents the source field path(s) in the source object.
* - The value can be a literal.
* - If a single source field is required, the value should be a JSONPath string pointing to that field.
* - If a default value is needed, the value can be any valid JSON value.
* - If multiple source fields are required for applying a sync or async function, provide an array where:
* - Each element before the last is a JSONPath string pointing to a source field.
* - The last element is a function that takes the resolved source values as arguments and computes the target field value.
*
* @returns An array of transformed objects, with fields derived from applying the `mapping` to the `source` object.
*/
export async function mapObjAsync(source, mapping) {
return resolveNestedPromises(mapObj(source, mapping));
}
/**
* Transforms each object in the `source` array based on the provided `mapping` transformation.
*
* @param source - An array of source objects to transform.
* @param mapping - A mapping object defining the transformation rules. Each mapping object's key-value pair should use JSONPath syntax:
* - The key represents the target field path in the transformed object.
* - The value represents the source field path(s) in the source object.
* - The value can be a literal.
* - If a single source field is required, the value should be a JSONPath string pointing to that field.
* - If a default value is needed, the value can be any valid JSON value.
* - If multiple source fields are required for applying a function, provide an array where:
* - Each element before the last is a JSONPath string pointing to a source field.
* - The last element is a function that takes the resolved source values as arguments and computes the target field value.
*
* @returns An array of transformed objects, with fields derived from applying the `mapping` to each `source` object.
*/
export function mapObjArr(source, mapping) {
let result = [];
for (let obj of source) {
let transformed = mapObj(obj, mapping);
for (let obj of transformed) {
result.push(obj);
}
}
return result;
}
/**
* Transforms each object in the `source` array based on the provided `mapping` transformation.
*
* @param source - An array of source objects to transform.
* @param mapping - A mapping object defining the transformation rules. Each mapping object's key-value pair should use JSONPath syntax:
* - The key represents the target field path in the transformed object.
* - The value represents the source field path(s) in the source object.
* - The value can be a literal.
* - If a single source field is required, the value should be a JSONPath string pointing to that field.
* - If a default value is needed, the value can be any valid JSON value.
* - If multiple source fields are required for applying a sync or async function, provide an array where:
* - Each element before the last is a JSONPath string pointing to a source field.
* - The last element is a function that takes the resolved source values as arguments and computes the target field value.
*
* @returns An array of transformed objects, with fields derived from applying the `mapping` to each `source` object.
*/
export async function mapObjArrAsync(source, mapping) {
let result = [];
for (let obj of source) {
let transformed = await mapObjAsync(obj, mapping);
for (let obj of transformed) {
result.push(obj);
}
}
return result;
}
//# sourceMappingURL=main.js.map