@blumintinc/flatten-anything
Version:
Flatten objects and replace nested props with 'prop.subprop'. A simple and small integration
181 lines (170 loc) • 5.88 kB
JavaScript
;
/** Returns the object type of the given payload */
function getType(payload) {
return Object.prototype.toString.call(payload).slice(8, -1);
}
/** Returns whether the payload is an array */
function isArray(payload) {
return getType(payload) === 'Array';
}
/**
* Returns whether the payload is a plain JavaScript object (excluding special classes or objects
* with other prototypes)
*/
function isPlainObject(payload) {
if (getType(payload) !== 'Object')
return false;
const prototype = Object.getPrototypeOf(payload);
return !!prototype && prototype.constructor === Object && prototype === Object.prototype;
}
/** Returns whether the payload is a an array with at least 1 item */
function isFullArray(payload) {
return isArray(payload) && payload.length > 0;
}
/**
* Returns whether the payload is a number (but not NaN)
*
* This will return `false` for `NaN`!!
*/
function isNumber(payload) {
return getType(payload) === 'Number' && !isNaN(payload);
}
function pathsAreEqual(path, wildcardPath) {
const wildcardPathPieces = wildcardPath.split('.');
const pathWithWildcards = path
.split('.')
.reduce((carry, piece, index) => {
const add = wildcardPathPieces[index] === '*' ? '*' : piece;
carry.push(add);
return carry;
}, [])
.join('.');
return pathWithWildcards === wildcardPath;
}
function recursiveOmit(obj, omittedKeys, pathUntilNow = '') {
if (!isPlainObject(obj)) {
return obj;
}
return Object.entries(obj).reduce((carry, [key, value]) => {
let path = pathUntilNow;
if (path)
path += '.';
path += key;
if (omittedKeys.some((guardPath) => pathsAreEqual(path, guardPath))) {
return carry;
}
// no further recursion needed
if (!isPlainObject(value)) {
carry[key] = value;
return carry;
}
carry[key] = recursiveOmit(obj[key], omittedKeys, path);
return carry;
}, {});
}
/**
* omit returns a new object without the props you omit
*
* @export
* @template T
* @template K
* @param {T} obj the target object to omit props from
* @param {K[]} keys the prop names you want to omit
* @returns {O.Omit<T, K>} a new object without the omitted props
*/
function omit(obj, keys) {
if (!isFullArray(keys))
return obj;
return recursiveOmit(obj, keys);
}
function retrievePaths(object, path, result, untilDepth) {
if (!isPlainObject(object) ||
!Object.keys(object).length ||
`${object['methodName']}`?.includes('FieldValue')) {
if (!path)
return object;
result[path] = object;
return result;
}
if (isNumber(untilDepth))
untilDepth--;
return Object.keys(object).reduce((carry, key) => {
const pathUntilNow = path ? path + '.' : '';
const newPath = pathUntilNow + key;
// last iteration or not
const extra = untilDepth === -1
? { [newPath]: object[key] }
: retrievePaths(object[key], newPath, result, untilDepth);
return Object.assign(carry, extra);
}, {});
}
/**
* Flattens an object from `{a: {b: {c: 'd'}}}` to `{'a.b.c': 'd'}`
*
* @export
* @param object the object to flatten
* @param [number] how deep you want to flatten. 1 for flattening only the first nested prop, and keeping deeper objects as is.
* @returns the flattened object
*/
function flattenObject(object, untilDepth) {
const result = {};
return retrievePaths(object, null, result, untilDepth);
}
/**
* Flattens an array from `[1, ['a', ['z']], 2]` to `[1, 'a', 'z', 2]`
*
* @export
* @param array the array to flatten
* @returns the flattened array
*/
function flattenArray(array) {
return array.reduce((carry, item) => {
return isArray(item) ? [...carry, ...flattenArray(item)] : [...carry, item];
}, []);
}
/**
* Flattens certain props of an object.
*
* @export
* @param object the object to flatten Eg. `{a: {subA: 1}, b: {subB: 1}}`
* @param [props=[]] the prop names you want to flatten. Eg. `['a']` will return `{'a.subA': 1, b: {subB: 1}}`
* @returns the flattened object
*/
function flattenObjectProps(object, props = []) {
const flatObject = props.reduce((carry, propPath) => {
const firstPropKey = propPath.split('.')[0] ?? '';
const target = { [`${firstPropKey}`]: object[firstPropKey] };
// calculate a certain depth to flatten or `null` to flatten everything
const untilDepth = propPath.split('.').length - 1 || undefined;
const flatPart = flattenObject(target, untilDepth);
const flatPartFiltered = Object.entries(flatPart).reduce((carry, [key, value]) => {
if (!key.startsWith(propPath))
return carry;
carry[key] = value;
return carry;
}, {});
return { ...carry, ...flatPartFiltered };
}, {});
const omittedProps = props;
const objectWithoutFlatProps = omit(object, omittedProps);
return { ...objectWithoutFlatProps, ...flatObject };
}
/**
* Flattens an object or array.
* Object example: `{a: {b: {c: 'd'}}}` to `{'a.b.c': 'd'}`
* Array example: `[1, ['a', ['z']], 2]` to `[1, 'a', 'z', 2]`
*
* @export
* @param objectOrArray the payload to flatten
* @param [number] how deep you want to flatten. (currently only works with objects) 1 for flattening only the first nested prop, and keeping deeper objects as is.
* @returns the flattened result
*/
function flatten(objectOrArray, untilDepth) {
return isArray(objectOrArray)
? flattenArray(objectOrArray)
: flattenObject(objectOrArray, untilDepth);
}
exports.flatten = flatten;
exports.flattenArray = flattenArray;
exports.flattenObject = flattenObject;
exports.flattenObjectProps = flattenObjectProps;