Search through and modify a deeply nested javascript object - javascript

I have an object that can be deeply nested with objects, arrays, arrays of objects, and so on.
Every nested object has a sys property which in turn has an id property.
I have a separate list of id values that correspond to objects that I want to remove from the original object. How can I go about recursively looping through the entire object and modify it to no longer include these?
For example, say I have the following data:
let data = {
sys: {
id: '1'
},
items: [
{
sys: {
id: '2'
},
fields: {
title: 'Item title',
sponsor: {
sys: {
id: '3'
},
fields: {
title: 'Sponsor Title'
}
},
actions: [
{
sys: {
id: '4'
},
fields: {
title: 'Google',
url: 'google.com'
}
},
{
sys: {
id: '5'
},
fields: {
title: 'Yahoo',
url: 'yahoo.com'
}
}
]
}
}
]
}
Then I have an array of id's to remove:
const invalidIds = ['3', '5'];
After I run the function, the resulting object should have the property with sys.id of '3' set to null, and the object with sys.id of '5' should simply be removed from its containing array:
// Desired Output:
{
sys: {
id: '1'
},
items: [
{
sys: {
id: '2'
},
fields: {
title: 'Item title',
sponsor: null,
actions: [
{
sys: {
id: '4'
},
fields: {
title: 'Google',
url: 'google.com'
}
}
]
}
}
]
}
With help from this solution, I'm able to recursively search through the object and its various nested arrays:
const process = (key, value) => {
if (typeof value === 'object' && value.sys && value.sys.id && invalidIds.includes(value.sys.id)) {
console.log('found one', value.sys.id);
}
};
const traverse = (obj, func) => {
for (let key in obj) {
func.apply(this, [key, obj[key]]);
if (obj[key] !== null) {
if (typeof obj[key] === 'object') {
traverse(obj[key], func);
} else if (obj[key].constructor === Array) {
obj[key].map(item => {
if (typeof item === 'object') {
traverse(item, func);
}
});
}
}
}
};
traverse(data, process);
However I can't figure out how to properly modify the array. In addition, I'd prefer to create an entirely new object rather than modify the existing one in order to keep things immutable.

Here are the observations that led to my solution:
To create a new object, you need to use return somewhere in your function.
To remove items from array, you need to filter out valid items first, then recursively call traverse on them.
typeof obj[key] === 'object' will return true even for Array, so
next else if block won't ever be hit.
As for implementation, my first step was to create a helper good function to detect invalid objects.
good = (obj) =>{
try{return !(invalidIds.includes(obj.sys.id));}
catch(err){return true;}
}
Now the main traverse -
traverse = (obj) => {
//I assumed when an object doesn't have 'sys' but have 'id', it must be sys obj.
if (obj==null) return null;
if(obj.constructor === Array) return obj.filter(good).map(traverse);
if(obj.sys==undefined) { //this checks if it's sys object.
if(obj.id!=undefined) return obj;
}
for (let key in obj) {
if (key!=0) {
if (good(obj[key])) {obj[key] = traverse(obj[key]);}
else {obj[key] = null;}
}
}
return obj;
};
In case of Array objects, as per point 2, I filtered out valid objects first, then mapped them to traverse. In case of Objects, = operator was used to catch valid sub-objects, returned by recursive call to traverse, instead of simply modifying them.
Note: I hardly know javascript, but took a go at it anyway because this recursive problem is fairly common. So watch out for JS specific issues. Specifically, as outlined in comment, I'm not content with how I checked for 'sys' objects.

Related

Transform dot notation to a tree data form

I have object oriented data in the form:
var alist = [
'foo',
'foo.lol1',
'foo.lol2',
'bar.lol1',
'bar.barbar.kk',
...
]
which I would like to transform into a tree structure, to be able to serve them with a tree component (https://github.com/vinz3872/vuejs-tree in particular). The require form is the following:
var ok = [
{
text: "foo",
state: { expanded: false },
nodes: [
{
id: 1,
path: "foo.lol1",
text: "lol1",
checkable: true,
state: { checked: false },
},
{
id: 2,
path: "foo.lol2",
text: "lol2",
checkable: true,
state: { checked: false },
},
]
},
{
text: "bar",
state: { expanded: false },
nodes: [
{
id: 3,
path: "bar.lol1",
text: "lol1",
checkable: true,
state: { checked: false },
},
]
},
{
text: "bar",
state: { expanded: false },
nodes: [
{
id: 3,
path: "bar.lol1",
text: "lol1",
checkable: true,
state: { checked: false },
},
{
text: "barbar",
state: { expanded: false },
nodes: [
{
id: 4,
path: "bar.barbar.kk",
text: "kk",
checkable: true,
state: { checked: false },
},
]
},
]
}
]
I am aware that I should use recursion and I have tried all relevan posts in stackoverflow, i.e. How to build a JSON tree structure using object dot notation.
My main problem is that I have to somehow preserve the information of the full path to the leaves of the tree. As a newbie in js I lost myself in counters and callback for days without any luck.
I would appreciate your help.
Thank you in advance
Basically you could use forEach then split each string into array and then use reduce on that. Then you build nested object where the keys are current paths and also ad to result array.
var alist = [
'foo',
'foo.lol1',
'foo.lol2',
'bar.lol1',
'bar.barbar.kk',
]
const result = []
const levels = {
result
}
let prev = ''
let id = 1
alist.forEach(str => {
str.split('.').reduce((r, text, i, arr) => {
const path = prev += (prev.length ? '.' : '') + text
if (!r[path]) {
r[path] = {result: []}
const obj = {
id: id++,
text,
}
if (i === 0) {
obj.state = {expanded: false}
} else {
obj.state = {checked: false}
obj.checkable = true
obj.path = path
}
obj.nodes = r[path].result
r.result.push(obj)
}
if (i === arr.length - 1) {
prev = ''
}
return r[path]
}, levels)
})
console.log(result)
I found that it was easiest to do this transformation in two steps. The first converts your input into this format:
{
foo: {
lol1: {},
lol2: {}
},
bar: {
barbar: {
kk: {}
},
lol1: {}
},
}
The second uses just this format to create your desired structure. This has two advantages. First, I have tools lying around that make it easy to create this structure from your input. Second, this structure embeds enough information to create your output, with only one branching construct: whether the value at a path is an empty object or has properties. This makes the generation code relatively simple:
const setPath = ([p, ...ps]) => (v) => (o) =>
p == undefined ? v : Object .assign (
Array .isArray (o) || Number .isInteger (p) ? [] : {},
{...o, [p]: setPath (ps) (v) ((o || {}) [p])}
)
const reformat = (o, path = [], nextId = ((id) => () => String (++ id)) (0)) =>
Object .entries (o) .map (([k, v]) => Object .entries (v) .length > 0
? {text: k, state: {exapanded: false}, nodes: reformat (v, [...path, k], nextId)}
: {id: nextId (), path: [...path, k] .join('.'), text: k, checkable: false, state: {checked: false}}
)
const transform = (pathTokens) =>
reformat (pathTokens
.map (s => s .split ('.'))
.reduce ((a, path) => setPath (path) ({}) (a), {})
)
const alist = ['foo', 'foo.lol1', 'foo.lol2', 'bar.lol1', 'bar.barbar.kk']
console .log (transform (alist))
.as-console-wrapper {max-height: 100% !important; top: 0}
We start with setPath, which takes a path, in a format such as ['bar', 'barbar', 'kk'], the value to set at that path, and an object to shallow clone with this new property along that path. Thus setPath (['foo', 'bar', 'baz']) (42) ({foo: {qux: 7}, corge: 6}) yields {foo: {qux: 7, bar: {baz: 42}}, corge: 6}. (There's a little more in this reusable function to also handle array indices instead of string object paths, but we can't reach that from this input format.)
Then we have reformat, which does the format conversion. It simply builds a different input object based upon whether the input value is an empty object.
Finally, transform maps a splitting function over your input array to get the path structure needed for setPath, folds the results into an initially empty object by setting every path value to an empty object, yielding our intermediate format, which we then pas to reformat.
There is one thing I really don't like here, and that is the nextId function, which is a stateful function. We could just have easily used a generator function, but whatever we do here, we're using state to build this output and that bothers me. If someone has a cleaner suggestion for this, I'd love to hear it.

Javascript - Change name of all object keys in nested arrays

This is the array i get:
const packages = [
{
id: '641a1690-6c8b-4ada-ae97-8d82cc4fe7a3',
name: 'com.sample',
children: {
id: 'd7384f60-e4ab-4a86-8e2e-0f66cc32f',
name: 'child.computer.com',
children: { id: 'e4ab-4a86-0f66cc32f560', name: 'child.com' }
}
},
{ id: 'd7384f60-e4ab-4a86-8e2e-0f66cc32f560', name: 'computer.com' },
{ id: 'ca7f972e-64ee-4cb0-80b9-1036fac69d32', name: 'java.util' }
];
So, it is an array of objects, and each object can have children, which again have id, name and possibly children (children is optional), and so on, it can be nested X times.
I want to change key names, id to key, name to title and children will remain children. So, my problem is that i don't know how to change keys inside children, i just change the first level and that is all.. It should be like:
{
key: '641a1690-6c8b-4ada-ae97-8d82cc4fe7a3',
title: 'com.sample',
children: {
key: 'd7384f60-e4ab-4a86-8e2e-0f66cc32f',
title: 'child.computer.com',
children: { key: 'e4ab-4a86-0f66cc32f560', title: 'child.com' }
}
}
You can do this by using Recursion.
Check if the value of the [key-value] pair from the Object#entries() call is an object.
If so, call the transformObj function again recursively for that value. Else return the value as is.
And finally convert the array of [key-value] pairs back to an object by using Object#fromEntries:
const packages = [{ id: '641a1690-6c8b-4ada-ae97-8d82cc4fe7a3', name: 'com.sample', children: { id: 'd7384f60-e4ab-4a86-8e2e-0f66cc32f', name: 'child.computer.com', children: { id: 'e4ab-4a86-0f66cc32f560', name: 'child.com' }}}, { id: 'd7384f60-e4ab-4a86-8e2e-0f66cc32f560', name: 'computer.com' }, { id: 'ca7f972e-64ee-4cb0-80b9-1036fac69d32', name: 'java.util' }];
const replacer = { "id": "key", "name" :"title"};
const transformObj = (obj) => {
if(obj && Object.getPrototypeOf(obj) === Object.prototype){
return Object.fromEntries(
Object.entries(obj)
.map(([k, v]) => [replacer[k] || k, transformObj(v)])
);
}
//Base case, if not an Object literal return value as is
return obj;
}
console.log(packages.map(o => transformObj(o)));
You can try to go through every object inside your array and recursively iterate through its keys. Then you can change the keys you want to change and iterate further through the childrens key.
const packages = [{id: '641a1690-6c8b-4ada-ae97-8d82cc4fe7a3',name:'com.sample',children: {id: 'd7384f60-e4ab-4a86-8e2e-0f66cc32f',name: 'child.computer.com',children: { id: 'e4ab-4a86-0f66cc32f560', name: 'child.com' }}},{ id: 'd7384f60-e4ab-4a86-8e2e-0f66cc32f560', name: 'computer.com' },{ id: 'ca7f972e-64ee-4cb0-80b9-1036fac69d32', name: 'java.util' }];
const renameNestedObjects = (obj) => {
Object.keys(obj).forEach((key, index) => {
if (key == "id") {
obj["key"] = obj["id"];
delete obj["id"];
}
if (key == "name") {
obj["title"] = obj["name"];
delete obj["name"];
}
if (key == "children") {
renameNestedObjects(obj["children"]);
}
});
}
console.log(packages);
packages.forEach(obj => { renameNestedObjects(obj); });
console.log(packages);

Remove object from array inside a recursive function

I have the following model object:
const model = {
_id: '1',
children: [
{
id: '2',
isCriteria: true
},
{
id: '3',
isCriteria: true
}
]
}
PS: The depth of children is unknown, so I have to use a recursive function to navigate through it.
I want to delete specific objects fro children array based on the array of ids.
So for example if make the following call removeCriteria(model, ['2']), the result should be:
const model = {
_id: '1',
children: [
{
id: '2',
isCriteria: true
}
]
}
I implemented this function as follows:
function removeCriteria(node, criteria, parent = []) {
if (node.isCriteria) {
if (criteria.length && !criteria.includes(node.id)) {
parent = parent.filter(criteria => criteria.id !== node.id);
}
console.log(parent) // here the parents object is correct but it doesn't modify the original object
}
if (node.children)
for (const child of node.children) removeCriteria(child, criteria, node.children);
}
Assigning to parent doesn't assign to the object property where the value came from.
You need to filter node.children and assign back to that property.
function removeCriteria(node, criteria) {
if (criteria.length == 0) {
return;
}
if (node.children) {
node.children = node.children.filter(child => !child.isCriteria || criteria.includes(child.id));
node.children.forEach(child => removeCriteria(child, criteria));
}
}
const model = {
_id: '1',
children: [{
id: '2',
isCriteria: true
},
{
id: '3',
isCriteria: true
}
]
}
removeCriteria(model, ['2']);
console.log(model);
The issue is you're reassigning the variable parent, which doesn't accomplish anything since you're not mutating the array to remove objects, and instead you're assigning it to a newly created array. I would suggest introducing a parentObj reference to the object to which parent belongs, so then you can set parentObj.children to parent and actually mutate the original object's array property:
const model = {
_id: '1',
children: [
{
id: '2',
isCriteria: true
},
{
id: '3',
isCriteria: true
}
]
};
function removeCriteria(node, criteria, parent = [], parentObj = {}) {
if (node.isCriteria) {
if (criteria.length && !criteria.includes(node.id)) {
parent = parent.filter(criteria => criteria.id !== node.id);
parentObj.children = parent;
}
console.log('parent', parent) // here the parents object is correct but it doesn't modify the original object
}
if (node.children)
for (const child of node.children) removeCriteria(child, criteria, node.children, node);
}
removeCriteria(model, ['2']);
console.log(model);

How to remove a property from nested javascript objects any level deep?

Let's say I have nested objects, like:
var obj = {
"items":[
{
"name":"Item 1",
"value": "500",
"options": [{...},{...}]
},
{
"name":"Item 2",
"value": "300",
"options": [{...},{...}]
}
],
"name": "Category",
"options": [{...},{...}]
};
I want to remove the options property from any level deep from all the objects. Objects can be nested within objects, and arrays as well.
We're currently using Lodash in the project, but I'm curious about any solutions.
There is no straight forward way to achieve this, however you can use this below function to remove a key from JSON.
function filterObject(obj, key) {
for (var i in obj) {
if (!obj.hasOwnProperty(i)) continue;
if (typeof obj[i] == 'object') {
filterObject(obj[i], key);
} else if (i == key) {
delete obj[key];
}
}
return obj;
}
and use it like
var newObject = filterObject(old_json, "option");
Modifying the above solution, To delete "dataID" which appears multiple times in my JSON . mentioned below code works fine.
var candidate = {
"__dataID__": "Y2FuZGlkYXRlOjkuOTI3NDE5MDExMDU0Mjc2",
"identity": {
"__dataID__": "aWRlbnRpdHk6NjRmcDR2cnhneGE3NGNoZA==",
"name": "Sumanth Suvarnas"
},
};
candidate = removeProp(candidate, "__dataID__")
console.log(JSON.stringify(candidate, undefined, 2));
function removeProp(obj, propToDelete) {
for (var property in obj) {
if (typeof obj[property] == "object") {
delete obj.property
let newJsonData= this.removeProp(obj[property], propToDelete);
obj[property]= newJsonData
} else {
if (property === propToDelete) {
delete obj[property];
}
}
}
return obj
}
A little modification of void's answer that allows for deletion of propertise which are also objects
function filterObject(obj, key) {
for (var i in obj) {
if (!obj.hasOwnProperty(i)) continue;
if (i == key) {
delete obj[key];
} else if (typeof obj[i] == 'object') {
filterObject(obj[i], key);
}
}
return obj;
}
We now use object-scan for data processing tasks like this. It's very powerful once you wrap your head around it. Here is how you'd answer your questions
// const objectScan = require('object-scan');
const prune = (input) => objectScan(['**.options'], {
rtn: 'count',
filterFn: ({ parent, property }) => {
delete parent[property];
}
})(input);
const obj = { items: [{ name: 'Item 1', value: '500', options: [{}, {}] }, { name: 'Item 2', value: '300', options: [{}, {}] }], name: 'Category', options: [{}, {}] };
console.log(prune(obj));
// => 3
console.log(obj);
// => { items: [ { name: 'Item 1', value: '500' }, { name: 'Item 2', value: '300' } ], name: 'Category' }
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/object-scan#13.8.0"></script>
Disclaimer: I'm the author of object-scan
I had similar issues. So, I developed the following library. Please see the source code of the library on GitHub, and you can download it using npm.
You can use the function removePropertiesDeeply together with isInitialized as below to remove all non-initialized properties (such as an empty object, empty array, empty string, or non-finite number).
const {removePropertiesDeeply, isInitialized} = require("#thedolphinos/utility4js");
const object = {
a: null,
b: "",
x: {
a: null,
b: ""
},
y: [],
z: [
null,
"",
{a: null, b: ""},
[[{a: {b: null, c: ""}}]],
"abc"
]
};
removePropertiesDeeply(object, (x) => !isInitialized(x));
console.log(JSON.stringify(object)); // See that the object becomes {"z":["abc"]}.
I had a similar issue and I got it resolved. I hope my solution might be helpful to someone.
I use Es6 ... spread operator to do a shallow copy of an object and made null to property I was not interested.
const newObject = {
...obj.items,
...obj.name,
options: null // option property will be null.
}
function omit(source) {
return isArray(source)
? source.map(omit)
: isObject(source)
? (({ options, ...rst }) => mapValues(rst, omit))(source)
: source;
}
as with lodash, that's an easy thing, also you can specify the key via an param like this
function omit(source, omitKey) {
return isArray(source)
? source.map(partialRight(omit,omitKey)))
: isObject(source)
? (({[omitKey]: _, ...rst }) => mapValues(rst, partialRight(omit,omitKey)))(source)
: source;
}
You can remove properties given a condition using following function:
// Warning: this function mutates original object
const removeProperties = (obj, condition = (key, value) => false) => {
for (var key in obj) {
const value = obj[key]
if (!obj.hasOwnProperty(key)) continue
if (typeof obj[key] === "object") {
removeProperties(obj[key], condition)
} else if (condition(key, value)) {
delete obj[key]
}
}
return obj
}
Examples:
// Remove all properties where key is equal to 'options'
removeProperties(someObject, (key, value) => key === 'options'))
// Remove all properties where key starts with 'ignore_'
removeProperties(someObject, (key, value) => key.startsWith('ignore_'))
// Remove all properties where value is null
removeProperties(someObject, (key, value) => value === null))

Filter through an object

I am trying to write a filter function which takes in an object as a parameter and the query string as its second parameter. The function should return the list of all the values from the object that match the query string.
For example
var data = [{
label: 'Cars',
children: [{
label: 'Volkswagan',
children: [{
label: 'Passat'
}]
}, {
label: 'Toyota'
}]
}, {
label: 'Fruits',
children: [{
label: 'Grapes'
}, {
label: 'Oranges'
}]
}];
function filter(data, query){}
filter(data,'ra'); //['Grapes', 'Oranges']
My question is how to tackle the nested 'children' property for each indexed object?
You want to use recursion for this.
function filter(data, query){
var ret = [];
data.forEach(function(e){
// See if this element matches
if(e.label.indexOf(query) > -1){
ret.push(e.label);
}
// If there are children, then call filter() again
// to see if any children match
if(e.children){
ret = ret.concat(filter(e.children, query));
}
});
return ret;
}
Try using recursive calls based on the data type of each property. For example, in the case of a nested property that's an array, you will want to call filter on each element of that array. Similar logic in the case that the nested element is an object, you want to look at each property and call filter. I wrote this off the cuff, so I haven't tested all of the corner cases, but it works for your test example:
var results = [];
filter(data,'ra'); //['Grapes', 'Oranges']
console.log(results);
function filter(data,query){
for(var prop in data){
//array
if(Array.isArray(data[prop])){
for(var i = 0; i < data[prop].length; i++){
filter(data[prop][i],query);
}
} else if (typeof data[prop] === "object"){
filter(data[prop],query);
} else if(typeof data[prop] === "string"){
if(data[prop].indexOf(query) > -1){
results.push(data[prop]);
}
}
}
}

Categories

Resources