adding a value to a deeply nested object with arrays in it - javascript

Consider the object:
{
"status": {
"affects": {
"date_of_death": "date_of_death",
"cause_of_death": {
"affects": {
"other_cause_of_death": "other_cause_of_death"
},
"value": "Other"
}
},
"value": "Deceased"
}
}
What I want to do is loop over this and add {hide: true}. But there are some rules:
If the key does not have object as a value, take the value and make it into {hide: true}
If it has affects, like cause of death does, add hide: true after value.
So the resulting object should be:
{
"status": {
"affects": {
"date_of_death": {hide: true},
"cause_of_death": {
"affects": {
"other_cause_of_death": {hide: true}
},
"value": "Other",
"hide": true,
}
},
"value": "Deceased"
}
}
now this might seem easy, until you get something like this:
{
"comorbidities": [
{
"affects": {
"malignancy_type": {
"affects": {
"malignancy_type_other": "malignancy_type_other"
},
"value": "Other"
}
},
"value": "malignancy"
},
{
"affects": {
"heritable_syndrome_type": "heritable_syndrome_type"
},
"value": "heritable syndrome"
}
]
}
The exact same thing should happen here. Accept notice how its an array, This should do it recursively, deep diving into the affects, until no more can be found.
I have gotten this far:
export const createHiddenFields = (dependencies) => {
let hiddenFields = {};
if (dependencies === null) {
return hiddenFields;
}
for (const key in dependencies) {
if (dependencies[key].hasOwnProperty('affects')) {
hiddenFields[key] =
} else {
}
}
return hiddenFields;
}
But I am lost as to how to finish this. I know I want it recursive, but I also want it fast, but still legible.
Any Ideas?

You're probably looking for something like
export function createHiddenFields(dependencies) {
if (Array.isArray(dependencies)) {
return dependencies.map(createHiddenFields);
}
const hiddenFields = {
hide: true,
};
if (typeof dependencies == 'object' && dependencies !== null) {
for (const key in dependencies) {
if (key == 'affects') {
hiddenFields.affects = createHiddenFields(dependencies.affects);
} else {
hiddenFields[key] = dependencies[key];
}
}
}
return hiddenFields;
}

You can do this using recursion and for...in loop and check if the value of the property is object then call the function again or if its not then add the value.
const data = {"comorbidities":[{"affects":{"malignancy_type":{"affects":{"malignancy_type_other":"malignancy_type_other"},"value":"Other"}},"value":"malignancy"},{"affects":{"heritable_syndrome_type":"heritable_syndrome_type"},"value":"heritable syndrome"}]}
function update(obj, val) {
for (let i in obj) {
if (typeof obj[i] == 'object') update(obj[i], val);
else if ('affects' in obj) Object.assign(obj, val);
else obj[i] = val
}
}
update(data, {hide: true});
console.log(data)

I decided to post a late answer anyway, because I see some possible flaws and gotchas in the other answers. Specifically:
for ... in is dangerous because it may loop over any iterable Object.prototype propertyes, which sometimes exist thanks to irresponsible developers. In general it loops over all properties including inherited ones
for ... in is not supposed to be used for looping over an array
It may be unwise to reuse the {hide: true} object. Should you later wish to alter the hide property for some entries in the tree, it would affect all that share the same object reference
So I'd instead write a longer but safer method:
function createHiddenFields(obj) {
// ignore nulls
if(obj == null) {
return;
}
// For an array, just do the update for each object in array
else if(obj instanceof Array) {
for(const subObj of obj) {
createHiddenFields(subObj);
}
}
// For object, do the hidden field thing
else if(typeof obj == "object") {
// Replace all string values with hiddenVal
for(const valueName of Object.getOwnPropertyNames(obj)) {
// Do not override value nodes
if(valueName == "value") {
continue;
}
else {
// Process sub-objects
if(typeof obj[valueName] == "object") {
createHiddenFields(obj[valueName]);
}
// Replace primitive property values
else {
obj[valueName] = { hide: true };
}
}
}
// add hidden field for affects
if(typeof obj.affects == "object") {
obj.hide = true;
}
}
}
demo

Related

I want to use a depth first search recursively to get an object's value using the key which might be nested in that object in javascript

I have this object, and I want to get the 'user' key's value, I usually can use a for loop to get the value, what if that key I am searching for is nested in another object? How can I write a recursive JS function that automatically gets the key/value I am looking for?
"http://converge.amwell.com/claims": {
"ehr": "DEMO-CVSHI-EPIC-1",
"connection": "con_9ydmEmrkyPulwlwU",
"role": "PATIENT",
"tenant": "CVSHI",
"user": "b3e9c94d-18e4-4743-9f4e-fe7e251e7b17",
"patient": "b3e9c94d-18e4-4743-9f4e-fe7e251e7b17",
"id_type": "INTERNAL_FHIR"
},
"iss": "https://amwellstage.amwell-dev.auth0.com/",
"sub": "oauth2|CVSHI-PATIENT|sick-cost#16xkn1tf.mailosaur.net",
"aud": [
"https://amwellstage.amwell-dev.auth0.com/api/v2/",
"https://amwellstage.amwell-dev.auth0.com/userinfo"
],
"iat": 1665480351,
"exp": 1665481251,
"azp": "cKftX4PfLpO0gFULuQiyDiX57BtTbpwg",
"scope": "openid profile email offline_access",
"org_id": "org_HMAePTi76uUGOfdG"
}```
```for (const [key, value] of Object.entries(obj)) {
// if (key === 'user') {
// user = value
// }
// if (value instanceof Object) {
// for (const [subKey, subValue] of Object.entries(value)) {
// if (subKey === 'user') {
// user = subValue
// }
// }
// }
// }```
You can use this:
function getPropFromObj(obj, prop) {
let valueToFindByKey;
if (!Array.isArray(obj) && obj !== null && typeof obj === "object") {
if (obj.hasOwnProperty(prop)) {
valueToFindByKey = obj[prop];
console.log(valueToFindByKey);
} else {
let i;
for (i = 0; i < Object.keys(obj).length; i++) {
getPropFromObj(obj[Object.keys(obj)[i]], prop);
}
}
}
return null;
}
getPropFromObj(obj, "user");
The above getPropFromObj will do a deep search on the object and pull the value defined by the given key.
Hope it helps.

Collect all paths in JSON object

I'm dealing with the following JavaScript object:
{
"gender": "man",
"jobinfo": {
"type": "teacher"
},
"children": [
{
"name": "Daniel",
"age": 12,
"pets": [
{
"type": "cat",
"name": "Willy",
"age": 2
},
{
"type": "dog",
"name": "Jimmie",
"age": 5
}
]
}
]
}
I want to print out each of the paths (keys and array indices) within the object, including the parents (i.e. children should be printed as should everything in it).
gender
jobinfo,
jobinfo.type,
children,
children.0.name,
children.0.age,
children.0.pets,
children.0.pets.0.type,
children.0.pets.0.name,
children.0.pets.0.age,
children.0.pets.1.type,
children.0.pets.1.name,
children.0.pets.1.age
I tried this code with modifications but it didnt work for me:
function getPath(object) {
for (key in object) {
if (Array.isArray(object[key]) === true) {
console.log(key)
getPath(object[key])
} else if (typeof object[key] === 'object') {
console.log(key)
getPath(object[key])
} else {
console.log(key)
}
}
}
It's printing all keys in the JSON, but I'm struggling with joining the paths, especially in nested elements.
This works:
const data = {"gender":"man","jobinfo":{"type":"teacher"},"children":[{"name":"Daniel","age":12,"pets":[{"type":"cat","name":"Willy","age":2},{"type":"dog","name":"Jimmie","age":5}]}]};
const getPath = (currPath, item) => {
console.log(currPath);
if (Array.isArray(item)) {
item.forEach((el, idx) => getPath(`${currPath}.${idx}`, el));
} else if (typeof item == "object") {
Object.entries(item).forEach(([key, value]) => {
getPath(`${currPath}.${key}`, value);
});
}
};
Object.entries(data).forEach(([key, value]) => {
getPath(key, value);
});
Basically I just loop through each of the entries in the initial object, using the key as the path at that stage and checking if the value is an object or array. I always print the path within the function (to provide the outer layers you want) and then I recurse over the inner layers, adding to the path as needed.
In this version array keys that are consisted of numbers like 'children.0' and so on handled and this gives the result exactly what you wanted:
const json = {"gender":"man","jobinfo":{"type":"teacher"},"children":[{"name":"Daniel","age":12,"pets":[{"type":"cat","name":"Willy","age":2},{"type":"dog","name":"Jimmie","age":5}]}]};
function getPath(object, previousPath) {
for (key in object) {
let currentPath = previousPath ? `${previousPath}.${key}` : key
if (Array.isArray(object[key])) {
console.log(currentPath)
getPath(object[key], currentPath)
} else if (typeof object[key] === 'object') {
if (!Array.isArray(object)) { // skipping logging array keys like children.0
console.log(currentPath)
}
getPath(object[key], currentPath)
} else {
console.log(currentPath)
}
}
}
getPath(json)

Returning value corresponding to a searched key in a deeply nested Javascript object

I have an object like below in Javascript,
const obj = {
"group1": {
"sub_group1_1": {
"inner_sub_group1_1_1": {
"name": "abc"
},
"inner_sub_group1_1_2": {
"name": "def"
}
},
"sub_group1_2": {
"inner_sub_group1_2_1": {
"name": "ghi"
},
"inner_sub_group1_2_2": {
"name": "jkl"
}
}
},
"group2": {
"sub_group2_1": {
"inner_sub_group2_1_1": {
"name": "mno"
},
"inner_sub_group2_1_2": {
"name": "pqr"
}
},
"sub_group2_2": {
"inner_sub_group2_2_1": {
"name": "stu"
},
"inner_sub_group2_2_2": {
"name": "wxy"
}
}
}
}
I want to write a function which could search for a key in the above object and return me the corresponding value of that key, for e.g.,
Input
filterObject(obj, 'inner_sub_group2_2_2')
Output
{
"name": "wxy
}
Input
filterObject(obj, 'inner_sub_group1_1_1')
Output
{
"name": "abc
}
Approach
I have written a recursive function for the same, but it doesn't seem to work for the second scenario,
const filterObject = (obj, searchedKey) => {
let result = {};
for (const key in obj) {
const currentObj = obj[key];
if (key === searchedKey) {
result = currentObj;
break;
} else if (typeof currentObj === 'object') {
result = filterObject(currentObj, searchedKey);
}
}
return result;
};
Any sort of help would be highly appreciated. Thanks!
You need to be able to check whether the recursive call found something, which you aren't doing - right now, you're just reassigning result, and then ignoring it and going on to the next iteration of the loop.
An approach that would work for this particular situation would be to check to see if the resulting object has any keys.
const obj={group1:{sub_group1_1:{inner_sub_group1_1_1:{name:"abc"},inner_sub_group1_1_2:{name:"def"}},sub_group1_2:{inner_sub_group1_2_1:{name:"ghi"},inner_sub_group1_2_2:{name:"jkl"}}},group2:{sub_group2_1:{inner_sub_group2_1_1:{name:"mno"},inner_sub_group2_1_2:{name:"pqr"}},sub_group2_2:{inner_sub_group2_2_1:{name:"stu"},inner_sub_group2_2_2:{name:"wxy"}}}};
const filterObject = (obj, searchedKey) => {
if (searchedKey in obj) {
return obj[searchedKey];
}
for (const key in obj) {
const currentObj = obj[key];
if (typeof currentObj === 'object') {
const result = filterObject(currentObj, searchedKey);
if (Object.keys(result).length) return result;
}
}
return {};
};
console.log(filterObject(obj, 'inner_sub_group2_2_2'));
console.log(filterObject(obj, 'inner_sub_group1_1_1'));
But that won't work if the value found isn't an object, or if the found object is empty. (A different object reference unconnected with the original structure would be returned.)
For the more general situation, the recursive call should return two things: whether the nested value was found or not, and the found value (if any). One approach to this is to return an object if something was found, and nothing otherwise.
const obj={group1:{sub_group1_1:{inner_sub_group1_1_1:{name:"abc"},inner_sub_group1_1_2:{name:"def"}},sub_group1_2:{inner_sub_group1_2_1:{name:"ghi"},inner_sub_group1_2_2:{name:"jkl"}}},group2:{sub_group2_1:{inner_sub_group2_1_1:{name:"mno"},inner_sub_group2_1_2:{name:"pqr"}},sub_group2_2:{inner_sub_group2_2_1:{name:"stu"},inner_sub_group2_2_2:{name:"wxy"}}}};
const filterObject = (obj, searchedKey) => {
if (searchedKey in obj) {
return { result: obj[searchedKey] };
}
for (const key in obj) {
const currentObj = obj[key];
if (typeof currentObj === 'object') {
const result = filterObject(currentObj, searchedKey);
if (result) return result;
}
}
};
console.log(filterObject(obj, 'inner_sub_group2_2_2').result);
console.log(filterObject(obj, 'inner_sub_group1_1_1').result);
The issue in your code is that if the recursive call is successful, the loop still continues, with another recursive call being made that may not be successful, and so the good result is lost.
You need an if after that recursive call that detects success and breaks out if it is one.
Now, to make the distinction between failure and success you'll make things much easier if you return null when there is no success.
Two lines need to be adjusted:
const filterObject = (obj, searchedKey) => {
let result = null; // To indicate nothing found yet
for (const key in obj) {
const currentObj = obj[key];
if (key === searchedKey) {
result = currentObj;
break;
} else if (typeof currentObj === 'object') {
result = filterObject(currentObj, searchedKey);
if (result) break; // <--- break out when success!
}
}
return result;
};
A little improvement might be needed on your is-object test, because typeof null === 'object' is a true expression.
You can use this instead:
if (Object(currentObj) === currentObj)
Here is a variation of #trincot's and #certainperformance's answers. Nothing fundamentally new, just a slightly different style:
const obj={group1:{sub_group1_1:{inner_sub_group1_1_1:{name:"abc"},inner_sub_group1_1_2:{name:"def"}},sub_group1_2:{inner_sub_group1_2_1:{name:"ghi"},inner_sub_group1_2_2:{name:"jkl",spoiler:null}}},group2:{sub_group2_1:{inner_sub_group2_1_1:{name:"mno"},inner_sub_group2_1_2:{name:"pqr",xtr:false}},sub_group2_2:{inner_sub_group2_2_1:{name:"stu"},inner_sub_group2_2_2:{name:"wxy"}}}};
function deepGet(obj,k,r){ // call with two arguments only
if(obj&&typeof obj=="object") {
if(k in obj) return obj[k];
for(l in obj) if((r=deepGet(obj[l],k))!=null) return r;
}
return null;
}
console.log(['inner_sub_group2_2_2','inner_sub_group1_1_1','xtr'].map(k=>deepGet(obj,k)));

Comparing 2 nested data-structures,target+source,what are appropriate merge-strategies for missing target values compared to their source counterpart?

What is a better way of doing this. I'am assigning either of two property values (from two different objects), depending on their existence, to a third data-structure.
In case the args object's value is nullish a non nullish value gets accessed from the default object and assigned to the final structure.
return {
first: {
visible: args.first?.visible ?? defaulttest.first?.visible,
emoji: args.first?.emoji ?? defaulttest.first?.emoji,
style: args.first?.style ?? defaulttest.first?.style,
},
back: {
visible: args.back?.visible ?? defaulttest.back?.visible,
emoji: args.back?.emoji ?? defaulttest.back?.emoji,
style: args.back?.style ?? defaulttest.back?.style,
},
page: {
visible: args.page?.visible ?? defaulttest.page?.visible,
emoji: args.page?.emoji ?? defaulttest.page?.emoji,
style: args.page?.style ?? defaulttest.page?.style,
},
forward: {
visible: args.forward?.visible ?? defaulttest.forward?.visible,
emoji: args.forward?.emoji ?? defaulttest.forward?.emoji,
style: args.forward?.style ?? defaulttest.forward?.style,
},
last: {
visible: args.last?.visible ?? defaulttest.last?.visible,
emoji: args.last?.emoji ?? defaulttest.last?.emoji,
style: args.last?.style ?? defaulttest.last?.style,
},
Mdelete: {
visible: args.Mdelete?.visible ?? defaulttest.Mdelete?.visible,
emoji: args.Mdelete?.emoji ?? defaulttest.Mdelete?.emoji,
style: args.Mdelete?.style ?? defaulttest.Mdelete?.style,
},
removeBtn: {
visible: args.removeBtn?.visible ?? defaulttest.removeBtn?.visible,
emoji: args.removeBtn?.emoji ?? defaulttest.removeBtn?.emoji,
style: args.removeBtn?.style ?? defaulttest.removeBtn?.style,
},
};
From my above comments ...
1/2 ... The OP actually is not really comparing. For a certain set of properties the OP looks up each property at a target object, and only in case it features a nullish value there will be an assignment from a source object's counterpart to the missing property. Thus an approach I would choose was ...
2/2 ... implementing a generic function which merges two objects in a way that a source property can only be written/assigned in case the target structure does not already provide a non nullish value. This function then has to be invoked twice once for args and defaulttest and a second time for the to be returned entirely empty data structure and args.
The above statement was a bit ambitious for there are at least 2 strategies of how one could achieve such kind of mergers.
Thus the below provided example code implements two approaches
one, called refit, which follows a pushing/patching agenda due to forcing the assignement of every non nullish property in a source-object to its non nullish counterpart of a target-object.
a 2nd one, called revive, which resembles a pulling approach for it just reassigns the nullish target-object properties with their non nullish source-object counterparts.
The difference in the results they produce for one and the same preset is going to be demonstrated herby ...
// "refit" ... a pushing/patching approach.
// - force the assignement of every non nullish property in source
// to its non nullish counterpart in target ... hence a *refit*.
function refitNullishValuesRecursively(target, source) {
if (
// are both values array-types?
Array.isArray(source) &&
Array.isArray(target)
) {
source
// for patching always iterate the source items ...
.forEach((sourceItem, idx) => {
// ... and look whether a target counterpart exists.
if (target[idx] == null) {
// either assign an existing structured clone ...
if (sourceItem != null) {
target[idx] = cloneDataStructure(sourceItem);
}
} else {
// ... or proceed recursively.
refitNullishValuesRecursively(target[idx], sourceItem);
}
});
} else if (
// are both values object-types?
source && target &&
'object' === typeof source &&
'object' === typeof target
) {
Object
// for patching ...
.entries(source)
// ... always iterate the source entries (key value pairs) ...
.forEach(([key, sourceValue], idx) => {
// ... and look whether a target counterpart exists.
if (target[key] == null) {
// either assign an existing structured clone ...
if (sourceValue != null) {
target[key] = cloneDataStructure(sourceValue);
}
} else {
// ... or proceed recursively.
refitNullishValuesRecursively(target[key], sourceValue);
}
});
}
return target;
}
// "revive" ... a pulling approach.
// - just reassign the nullish target properties with their
// non nullish source counterparts ... hence a *revive*.
function reviveNullishValuesRecursively(target, source) {
if (
// are both values array-types?
Array.isArray(target) &&
Array.isArray(source)
) {
target
// for fixing always iterate the target items.
.forEach((targetItem, idx) => {
if (targetItem == null) {
// either assign an existing structured clone ...
target[idx] = cloneDataStructure(source[idx]) ?? targetItem;
} else {
// ... or proceed recursively.
reviveNullishValuesRecursively(targetItem, source[idx]);
}
});
} else if (
// are both values object-types?
target && source &&
'object' === typeof target &&
'object' === typeof source
) {
Object
// for fixing ...
.entries(target)
// ... always iterate the target entries (key value pairs).
.forEach(([key, targetValue], idx) => {
if (targetValue == null) {
// either assign an existing structured clone ...
target[key] = cloneDataStructure(source[key]) ?? targetValue;
} else {
// ... or proceed recursively.
reviveNullishValuesRecursively(targetValue, source[key]);
}
});
}
return target;
}
const cloneDataStructure =
('function' === typeof structuredClone)
&& structuredClone
|| (value => JSON.parse(JSON.stringify(value)));
const targetBlueprint = {
x: { xFoo: 'foo', xBar: 'bar', xBaz: { xBiz: null } },
y: { yFoo: 'foo', yBar: null },
};
const patch = {
x: { xFoo: null, xBar: null, xBaz: { xBiz: 'biz' } },
y: { yFoo: null, yBar: 'bar', yBaz: { yBiz: 'biz' } },
};
let target = cloneDataStructure(targetBlueprint);
console.log('"refit" ... a pushing/patching approach.');
console.log('before refit ...', { target, patch });
refitNullishValuesRecursively(target, patch);
console.log('after refit ...', { target, patch });
target = cloneDataStructure(targetBlueprint);
console.log('"revive" ... a pulling approach.');
console.log('before revive ...', { target, patch });
reviveNullishValuesRecursively(target, patch);
console.log('after revive ...', { target, patch });
.as-console-wrapper { min-height: 100%!important; top: 0; }
As for the OP's example which targets the creation of kind of a config-object, one could fully patch/refit a clone of the temporary or current args-config, whereas within the last step one has control over the config-object's final structure by providing the most basic empty config-base which just gets revived by the before created full patch/refit-config.
function refitNullishValuesRecursively(target, source) {
if (Array.isArray(source) && Array.isArray(target)) {
source
.forEach((sourceItem, idx) => {
if (target[idx] == null) {
if (sourceItem != null) {
target[idx] = cloneDataStructure(sourceItem);
}
} else {
refitNullishValuesRecursively(target[idx], sourceItem);
}
});
} else if (
source && target &&
'object' === typeof source &&
'object' === typeof target
) {
Object
.entries(source)
.forEach(([key, sourceValue], idx) => {
if (target[key] == null) {
if (sourceValue != null) {
target[key] = cloneDataStructure(sourceValue);
}
} else {
refitNullishValuesRecursively(target[key], sourceValue);
}
});
}
return target;
}
function reviveNullishValuesRecursively(target, source) {
if (Array.isArray(target) && Array.isArray(source)) {
target
.forEach((targetItem, idx) => {
if (targetItem == null) {
target[idx] = cloneDataStructure(source[idx]) ?? targetItem;
} else {
reviveNullishValuesRecursively(targetItem, source[idx]);
}
});
} else if (
target && source &&
'object' === typeof target &&
'object' === typeof source
) {
Object
.entries(target)
.forEach(([key, targetValue], idx) => {
if (targetValue == null) {
target[key] = cloneDataStructure(source[key]) ?? targetValue;
} else {
reviveNullishValuesRecursively(targetValue, source[key]);
}
});
}
return target;
}
const cloneDataStructure =
('function' === typeof structuredClone)
&& structuredClone
|| (value => JSON.parse(JSON.stringify(value)));
const defaultConfig = {
first: {
visible: 'default.first.visible',
emoji: 'default.first.emoji',
style: 'default.first.style',
},
forward: {
visible: 'default.forward.visible',
emoji: 'default.forward.emoji',
style: 'default.forward.style',
},
removeBtn: {
visible: 'default.removeBtn.visible',
emoji: 'default.removeBtn.emoji',
style: 'default.removeBtn.style',
},
};
const currentConfig = {
first: {
visible: 'current.first.visible',
emoji: 'current.first.emoji',
style: 'current.first.style',
},
forward: {
visible: 'current.forward.visible',
emoji: null,
},
FOO: {
visible: 'current.FOO.visible',
emoji: 'current.FOO.emoji',
style: 'current.FOO.style',
}
};
function getConfiguration(baseConfig) {
return reviveNullishValuesRecursively(
cloneDataStructure(baseConfig),
refitNullishValuesRecursively(
cloneDataStructure(currentConfig),
defaultConfig,
),
);
}
console.log(
getConfiguration({
first: null,
forward: null,
removeBtn: null,
})
);
.as-console-wrapper { min-height: 100%!important; top: 0; }
If the structure of your object is the one you presented you can do:
function normalize(input, defaultValue) {
// Loop on the outer keys
Object.keys(input).forEach(mainKey => {
// Loop on the inner keys
Object.keys(input[mainKey]).forEach(key => {
// set the value of the key as itself or default if null
input[mainKey][key] = input[mainKey]?.[key] ?? defaultValue[mainKey]?.[key]
})
})
return input;
}
Calling normalize(args, defaulttest) you will loop on each inner key, check if it exist and if it does not exist you substitute it with the default in the same path.
Example:
const x = {
a: {a1: '1', a2: '2'},
b: {b1: '1', b2: null}
}
const y = {b: {b2: '5'}}
console.log(normalize(x,y))
Output:
{
"a": {
"a1": "1",
"a2": "2"
},
"b": {
"b1": "1",
"b2": "5"
}
}
With this approach you must have the key in the args input. If the key is missing, it will not be substituted with the default. To make it work even with not-present keys you need to use a third structure with all the possible path for example.

Update existing javascript object with new data recursively. Arrays elements should be merged by id

I've created an update object API that receives new update data of an existing document.
Let's say, I have two objects oldData and newData
oldData = {
me:{
name:{
short:'will'
long:'william'
}
},
friends:[
{
id: 1,
name:{
short:'mike'
long:'michael'
},games:[]
},
{
id: 2,
name:{
short:'fred'
long:'freddy'
}
},
],
favoriteGames:[
'gta',
'animal crossing',
'mortal kombat'
],
favoriteFood:['bacon'],
}
newData = {
me:{
name:{
long:'willy'
longer:'william'
}
},
friends:[
{
id:3,
name:{
short:'max',
long:'maxwell'
}
},
{
id:1,
name:{
short:'mic',
}
},
],
favoriteGames:[
'tekken'
]
}
calling applyUpdate(oldData, newData)should return
result = {
me:{
name:{
short:'will',
long:'willy',
longer:'william'
}
},
friends:[
{
id:3,
name:{
short:'max',
long:'maxwell'
}
},
{
id: 1,
name:{
short:'mic'
long:'michael'
},games:[]
}
],
favoriteGames:[
'tekken'
],
favoriteFood:['bacon'],
}
Basically, the rules for merging are:
If a key in an object is specified with new data, it overrides the
value of the same key in old data.
If a key is not specified, the
value of the same key in old data is kept.
If the value of a key in new data is an array of objects:
Each object must be merged BY id with elements in the array of the same key in old data.
Elements not included in the arrays of newData are removed from the result.
The order of elements in the arrays of newData should be preserved.
Merging must be done deeply, since nested arrays and objects of unspecified depth should be possible.
I've actually successfully implemented this with a horrendously long and ugly recursive function. But am worried about performance and readability issues. I am open to suggestions using lodash or underscore.
Thanks!
Try this. It's hard to write this in a readable way.
function customizer(oldProp, newProp) {
if (Array.isArray(newProp)) {
// check if `newProp` is an array of objects which has property `id`
if (typeof newProp[0] === 'object' && newProp[0].hasOwnProperty('id')) {
if (!Array.isArray(oldProp)) {
return newProp;
}
// merge objects of 2 arrays in `oldProp` and `newProp`
const mergedArr = [];
for (const objNewArr of newProp) {
const objOldArr = oldProp.find(o => o.id === objNewArr.id);
if (objOldArr) {
mergedArr.push(_.merge(objOldArr, objNewArr));
} else {
mergedArr.push(objNewArr);
}
}
return mergedArr;
}
return newProp;
}
if (typeof newProp === 'object') {
return _.merge(oldProp, newProp);
}
return newProp;
}
_.mergeWith(oldData, newData, customizer); // returns the merged object
Here's what worked. Thanks Duc.
function customizer(oldProp, newProp) {
if (Array.isArray(newProp)) {
if (typeof newProp[0] === 'object') {
const mergedArr = [];
for (const objNewArr of newProp) {
const objOldArr = oldProp.find(o => o._id === objNewArr._id);
if (objOldArr) {
mergedArr.push(_.mergeWith(_.cloneDeep(objOldArr), _.cloneDeep(objNewArr), customizer));
} else {
mergedArr.push(objNewArr);
}
}
return mergedArr;
}else{
return newProp;
}
}else if (typeof newProp === 'object') {
return _.merge(oldProp, newProp);
}else{
return undefined;
}
}
var result = _.mergeWith(_.cloneDeep(oldData), _.cloneDeep(newData), customizer); // returns the merged object

Categories