@enkidevs/deepmapper
v2.0.0
Published
Utility for mapping arbitrary structures
Downloads
120
Readme
deepmapper
Map an arbitrary JS structure by altering any portion of it, including itself.
const obj = {
a: [
1,
2,
{
value: 3
},
{
child: [
4,
{
value: 5
}
]
}
],
b: 6,
c: {
value: 7
}
};
const result = deepMapper(obj, x => {
if (Array.isArray(x)) {
return x.slice();
}
if (typeof x === "number") {
return 2 ** x;
}
return { ...x };
});
// gives
/*
const obj = {
a: [
2,
4,
{
value: 8,
},
{
child: [
16,
{
value: 32
}
]
},
],
b: 64,
c: {
value: 128
}
};
*/
The mapping is done as a pre-order traversal. This means that, if you change the parent by removing some of its children, the removed children won't be mapped.
deepMapper({ child: { value: "x" } }, item => {
if (isObject(item)) {
return {
child: "I am changed"
};
}
return item;
});
// gives
/*
{
child: 'I am changed'
}
*/
Commonly, you'd want to map values in an immutable fashion:
deepMapper({
nested: [
1,
'test',
{
bottom: true
}
]
}, item => {
if (Array.isArray(item)) {
return item.slice() // this will do a shallow copy of the nested array but not its values
}
if (item && typeof item === 'object') {
return { ...item }; // this will do a shallow copy of the top object, and the object with the 'bottom' key
}
// primitives in JS are immutable so we're fine here
return item;
})
The above will essentially perform a clone operation on the above object.
Here's how you'd write a deep clone function using deepmapper
and shallow-clone
to handle all possible values:
const deepMapper = require("@enkidevs/deepmapper");
const shallowClone = require("shallow-clone");
const obj = { a: [{ b: "test" }, 1, /abc/g], c: false, d: new Date(2) };
const cloned = deepMapper(obj, shallowClone);
Features
- Can map any primitive value
deepMapper(1, n => n + 1) === 2 // true
deepMapper('a', s => s + 'b') === 'ab' // true
deepMapper(true, b => !b) === false // true
// ...
- Handles circular references
// circular references get properly mapped
const obj = { a: [1, 2, 3], b: { loop: null } };
obj.b.loop = obj.a;
const result = deepMapper(obj, item => {
if (Array.isArray(item)) {
return item.slice();
}
if (item && typeof item === 'object') {
return { ...item };
}
return item + 1;
})
// gives
/*
{
a: [2, 3, 4],
b: {
loop: // points to the mapped result.a, not the original obj.a
},
};
*/
- Doesn't break on repeated references
const ref = { a: [1, 2, 3] };
const arr = [ref, ref, { b: ref }, { c: { value: ref } }];
const result = deepMapper(arr, item => {
if (Array.isArray(item)) {
return item.slice();
}
if (item && typeof item === 'object') {
return { ...item };
}
return item + 1;
});
// gives
/*
{ a: [2, 3, 4] },
{ a: [2, 3, 4] },
{ b: { a: [2, 3, 4] } },
{ c: { value: { a: [2, 3, 4] } } },
*/
Practical examples
- Obfuscate MongoDB object by changing all MongoDB ObjectID
_id
keys toid
:
function cleanMongoId(item) {
if (Array.isArray(item)) {
return item.slice();
}
if (isObject(item)) {
if (ObjectId.isValid(item)) {
return item;
}
// only change the _id property if it's a valid ObjectId
if (ObjectId.isValid(item._id)) {
const { _id, ...cleanItem } = item;
if (_id) {
cleanItem.id = _id;
}
return cleanItem;
}
return { ...item };
}
return item;
}
const id1 = ObjectId();
const id2 = ObjectId();
const doc = [
{ nested1: { _id: id1, whatever: 5 } },
{ nested2: { _id: id2, whatever: 5 } },
];
const result = deepMapper(doc, cleanMongoId);
// gives
/*
[
{ nested1: { id: id1, whatever: 5 } },
{ nested2: { id: id2, whatever: 5 } },
]
*/
- Changing state structures in a Redux reducer:
// ...
const state = {
items: [
{
id: 'todo-1',
data: {
createdAt: Date,
updatedAt: Date,
order: Number,
text: 'yipikaye'
}
},
// ...
]
}
// ... somewhere in a reducer for action { type: 'CHANGE_UPDATED_AT' id: 'todo-1', updatedAt: new Date() }
case [CHANGE_UPDATED_AT]:
return deepMapper(state, chunk => {
if (Array.isArray(chunk)) {
return chunk.slice();
}
if (chunk && typeof chunk === 'object') {
if (chunk.id === action.id) {
return {...chunk, updatedAt: action.updatedAt };
}
return {...chunk };
}
return chunk;
})
License
MIT