Create string paths from nested object in Javascript - javascript

I'm trying to turn a nested object that represents my file system into an array of strings that represent the file paths for each folder and file.
Input:
let obj = {
'app': {
'body': {
'abs': {
'muscles.txt': 1
},
'foot.js': 1,
'hand.txt': 1,
'leg.txt': 1
},
'cat.txt': 1,
'dog.js': 1,
'writing': {
'pen.txt': 1,
'phone.txt': 1
}
}
};
Output:
[
'/app',
'/app/body',
'/app/body/abs/',
'/app/body/abs/muscles.txt',
'/app/body/foot.js',
'/app/body/hand.txt',
...
]
What I have so far (it's not working):
function filePaths(obj, oldKey = '', store = []) {
for (let key in obj) {
if (typeof obj[key] === 'object') {
store.push('/' + key);
filePaths(obj[key], key, store);
} else {
store.push('/' + oldKey + '/' + key);
}
}
return store;
}
filePaths(obj);

Here is a working version:
let obj = {
'app': {
'body': {
'abs': {
'muscles.txt': 1
},
'foot.js': 1,
'hand.txt': 1,
'leg.txt': 1
},
'cat.txt': 1,
'dog.js': 1,
'writing': {
'pen.txt': 1,
'phone.txt': 1
}
}
};
function filePaths(obj, prefix = '', store = []) {
for (let key in obj) {
const curPath = `${prefix}/${key}`;
if (typeof obj[key] === 'object') {
store.push(curPath);
filePaths(obj[key], curPath, store);
} else {
store.push(curPath);
}
}
return store;
}
console.log(filePaths(obj));
So I've kept most of your code, but changed the fact that while you kept the "old" key I keep the current path and it serves as a prefix for all the files and as a prefix for all the directories that will get the current key appended.

Here's a recursive solution that leverages the Object.keys method with the spread operator, concat, and map:
let obj = {
'app': {
'body': {
'abs': {
'muscles.txt': 1
},
'foot.js': 1,
'hand.txt': 1,
'leg.txt': 1
},
'cat.txt': 1,
'dog.js': 1,
'writing': {
'pen.txt': 1,
'phone.txt': 1
}
}
};
function filePaths(obj, prefix = "", store = []) {
if (typeof obj !== "object") return [prefix];
return (prefix && [prefix] || []).concat(...Object.keys(obj).map(k => filePaths(obj[k], prefix + "/" + k, store)))
}
console.log(filePaths(obj))

You're on the right track with recursion. For each call, loop over every property in the current object, make a path for the property and add it to the result array. If the property keys to an object, it's a non-terminal node and is called recursively to add paths for its children.
const pathify = (o, res=[], path=[]) => {
for (const dir in o) {
const s = path.join("/");
res.push(`/${s ? `${s}/${dir}` : dir}`);
if (typeof o[dir] === "object") {
pathify(o[dir], res, path.concat(dir));
}
}
return res;
};
const obj = {
'app': {
'body': {
'abs': {
'muscles.txt': 1
},
'foot.js': 1,
'hand.txt': 1,
'leg.txt': 1
},
'cat.txt': 1,
'dog.js': 1,
'writing': {
'pen.txt': 1,
'phone.txt': 1
}
}
};
console.log(pathify(obj));

Related

Javascript: Dot Notation Strings to Nested Object References

We're trying to set nested object values based on dot notation strings.
Example input:
{
"bowtime": [
"30",
" 1",
" 3",
" 20"
],
"bowstate.levi.leviFlo.totalFloQuot": ".95",
"bowstate.crem.cremQuot": "79"
}
Desired output:
{
"bowstate": {
"levi": {
"leviFlo": {
"totalFloQuot": 0.95
}
},
"crem": {
"cremQuot": 79
}
},
"bowtime": [
"30",
" 1",
" 3",
" 20"
],
}
So far the code works fine, but it seems overly complex, and only allows for 4 layers of nesting. How can we simplify this code, and get it working for references with more than 4 layers of nesting:
const dayspace = {};
var keyArr = Object.keys(input);
for (key in keyArr) {
if ( keyArr[key].indexOf('.') > -1 ) {
var setArr = keyArr[key].split('.');
dayspace[setArr[0]] = dayspace[setArr[0]] || {}
for (var s = 0; s < setArr.length; s++) {
if (s == 1) {
if (setArr.length > s + 1) dayspace[setArr[0]][setArr[s]] = {}
else dayspace[setArr[0]][setArr[s]] = req.body[keyArr[key]]
}
if (s == 2) {
if (setArr.length > s + 1) dayspace[setArr[0]][setArr[1]][setArr[s]] = {}
else dayspace[setArr[0]][setArr[1]][setArr[s]] = req.body[keyArr[key]]
}
if (s == 3) {
if (setArr.length > s + 1) dayspace[setArr[0]][setArr[1]][setArr[2]][setArr[s]] = {}
else dayspace[setArr[0]][setArr[1]][setArr[2]][setArr[s]] = req.body[keyArr[key]]
}
if (s == 4) dayspace[setArr[0]][setArr[1]][setArr[2]][setArr[3]][setArr[s]] = req.body[keyArr[key]]
}
}
else {
dayspace[keyArr[key]] = req.body[keyArr[key]]
}
}
I'd split the key by . and use reduce to create all but the last nested value, if needed, and then assign the value to the last object created or found in the reduce callback:
const input = {
"bowtime": [
"30",
" 1",
" 3",
" 20"
],
"bowstate.levi.leviFlo.totalFloQuot": ".95",
"bowstate.crem.cremQuot": "79"
};
const output = Object.entries(input).reduce((outerObj, [key, val]) => {
if (!key.includes('.')) {
outerObj[key] = val;
return outerObj;
}
const keys = key.split('.');
const lastKey = keys.pop();
const lastObj = keys.reduce((a, key) => {
// Create an object at this key if it doesn't exist yet:
if (!a[key]) {
a[key] = {};
}
return a[key];
}, outerObj);
// We now have a reference to the last object created (or the one that already existed
// so, just assign the value:
lastObj[lastKey] = val;
return outerObj;
}, {});
console.log(output);
I have done similar things in my project. I have achieved it with a popular package called Flat. Link: https://github.com/hughsk/flat
var unflatten = require('flat').unflatten
unflatten({
'three.levels.deep': 42,
'three.levels': {
nested: true
}
})
// {
// three: {
// levels: {
// deep: 42,
// nested: true
// }
// }
// }
This package can make your nested structure flat and flatten structure nested as well. There are other useful methods there also. So it will be more flexible.
I think you should use it which will lead to less bugs in your project.
You could use Object.entires to get an array of key-value pairs within your object and then .reduce() your object keys by using .split(".") to get the single object properties into an array which you can then use to build your new object:
const obj = {
"bowtime": [
"30",
" 1",
" 3",
" 20"
],
"bowstate.levi.leviFlo.totalFloQuot": ".95",
"bowstate.crem.cremQuot": "79"
};
const res = Object.entries(obj).reduce((acc, [k, v]) => {
const keys = k.split('.');
let cur = acc;
keys.length > 1 && keys.forEach(ka => {
cur[ka] = cur[ka] || {};
cur = cur[ka];
});
cur[keys.pop()] = v;
return acc;
}, {});
console.log(res);
You could use a shorter approach by using a function for the splitted path to the value and generate new objects for it.
function setValue(object, path, value) {
var last = path.pop();
path.reduce((o, k) => o[k] = o[k] || {}, object)[last] = value;
}
var object = { bowtime: ["30", " 1", " 3", " 20" ], "bowstate.levi.leviFlo.totalFloQuot": ".95", "bowstate.crem.cremQuot": "79" };
Object.entries(object).forEach(([key, value]) => {
if (!key.includes('.')) return;
setValue(object, key.split('.'), value);
delete object[key];
});
console.log(object);
.as-console-wrapper { max-height: 100% !important; top: 0; }

Best way to remove empty key/value pairs from objects recursively

let's say that I have the current data object:
const human = {
name: '',
age: 0,
head: {
rightEye: '',
leftEye: '',
}
limbs: {
arms: '',
legs: '',
}
somethingElse: '',
}
I want to remove every empty key/value pairs. And this part I got it working with the following code:
const removeFalsy = (obj) => {
const newObj = {};
Object.keys(obj).forEach((prop) => {
if (obj[prop]) {
if (typeof obj[prop] === 'object') {
newObj[prop] = removeFalsy(obj[prop]);
} else {
newObj[prop] = obj[prop];
}
}
});
return newObj;
};
The thing is empty objects still stay in the main object. I want to remove every empty object from inside the main object in order to get the following result:
const human = {};
In place of my current results:
const human = {
head: {},
limbs: {},
}
What are my best options?
When recursively calling removeFalsy, only assign to newObj if the result of the recursive call has an object with at least one key:
const removeFalsy = (obj) => {
const newObj = {};
Object.keys(obj).forEach((prop) => {
if (obj[prop]) {
if (typeof obj[prop] === 'object') {
// change below:
const nonFalseyVal = removeFalsy(obj[prop]);
if (Object.keys(nonFalseyVal).length !== 0) {
newObj[prop] = nonFalseyVal;
}
// change above
} else {
newObj[prop] = obj[prop];
}
}
});
return newObj;
};
const human = {
name: '',
age: 0,
head: {
rightEye: '',
leftEye: '',
},
limbs: {
arms: '',
legs: '',
},
somethingElse: '',
}
const nonFalsyHuman = removeFalsy(human);
console.log(nonFalsyHuman);
Note that it might be more appropriate to use .reduce, and you can use Object.entries to get the key and the value immediately:
const removeFalsy = (obj) => {
return Object.entries(obj).reduce((a, [key, val]) => {
if (!val) return a;
if (typeof val === 'object') {
const nonFalseyVal = removeFalsy(val);
if (Object.keys(nonFalseyVal).length !== 0) {
a[key] = nonFalseyVal;
}
} else {
a[key] = obj[key];
}
return a;
}, {});
};
const human = {
name: '',
age: 0,
head: {
rightEye: '',
leftEye: '',
},
limbs: {
arms: '',
legs: '',
},
somethingElse: '',
}
const nonFalsyHuman = removeFalsy(human);
console.log(nonFalsyHuman);
You need to make following changes
Update if (typeof obj[prop] === 'object') { condition where you set value only if the object has some valid keys.
Update if (obj[prop]) { condition to allow other non-falsy values to enter the loop e.g. 0, etc.
const human = {name: '',age: 0,head: {rightEye: '',leftEye: ''},limbs: {arms: '',legs: ''},somethingElse: ''};
const removeFalsy = (obj) => {
const newObj = {};
Object.keys(obj).forEach((prop) => {
if (obj[prop] !== "") {
if (typeof obj[prop] === 'object') {
const temp = removeFalsy(obj[prop]);
if(Object.keys(temp).length) newObj[prop] = temp;
} else {
newObj[prop] = obj[prop];
}
}
});
return newObj;
};
console.log(removeFalsy(human));

JS update nested object key with string of key names concatenating with "."

Lets say that I have this object:
var obj = {
level1 :{
level2: {
level3: {
title: "winner"
}
}
}
}
Now I want to update the title key using the next string (notice, I have a string, not actual variable)
I have:
let myString = "level1.level2.level3.title"; // note - myString value comes from $http method or something
Maybe something like this:
obj[myString] = "super-winner";
Unfortunately the above doesn't work.
In addition - sometimes I need to update an undefined object so I need something to make the object to be defined with a new empty object.
For example, If I have the next object:
var obj = {
level1 : {}
}
}
I still want to modify the obj with the level3.winner as above.
Reminder:
obj[myString] = "super-winner";
How can I do that?
This works
const obj = {
// level1: {
// level2: {
// level3: {
// title: "winner"
// }
// }
// }
}
const myString = "level1.level2.level3.title"; // note - myString value comes from $http method or something
const title = 'super-winner'
myString.split('.')
.reduce(
(acc, curr) => {
if (acc[curr] === undefined && curr !== 'title') {
acc[curr] = {}
}
if (curr === 'title') {
acc[curr] = title
}
return acc[curr]
}, obj
);
console.log(obj) // {"level1":{"level2":{"level3":{"title":"super-winner"}}}}
This is zero-dependency solution, i.e. you don't have to use lodash or something bloating the size of your app.
Used "reduce" to achieve your desired result. Created a function "updateValue" where in you can pass obj - object to modify, str - property path to alter, value - value to be assigned at the property path
var obj1 = {
level1 :{
level2: {
level3: {
title: "winner"
}
}
}
}
var obj2 = { level1: {} }
var obj3 = {
level1 :{
level2: {
level3: {
title: "winner"
}
}
}
}
function updateValue(obj, str, value) {
let props = str.split('.'), arrIndex = -1
props.reduce((o,d,i) => (
arrIndex = d.indexOf('[') > -1 && d[d.indexOf('[') + 1],
arrIndex && (d = d.slice(0, d.indexOf('['))),
i == props.length - 1
? o[d] = value
: (o[d] = o[d] || {}, (arrIndex && (Array.isArray(o[d]) || (o[d] = [o[d]]))), arrIndex && o[d][arrIndex] || o[d])
)
, obj)
}
updateValue(obj1, 'level1.level2.level3.title', 'abcd')
updateValue(obj2, 'level1.level2.level3.title', 'abcd')
updateValue(obj3, 'level1.level2[0].title', 'abcd')
console.log(obj1)
console.log(obj2)
console.log(obj3)
This can be done by hand, indexing into the object structure repeatedly and creating new objects as necessary along the path to the destination key:
const updateField = (o, path, entry) => {
path = path.split(".");
let curr = o;
while (path.length > 1) {
const dir = path.shift();
const parent = curr;
curr = curr[dir];
if (undefined === curr) {
parent[dir] = {};
curr = parent[dir];
}
}
if (path.length === 1) {
curr[path.shift()] = entry;
}
return o;
};
var obj = {
level1 : {
level2: {
level3: {
title: "winner"
}
}
}
};
console.log(JSON.stringify(updateField(obj, "level1.level2.level3.title", "super-winner"), null, 2));
console.log(JSON.stringify(updateField({}, "level1.level2.level3.title", "super-winner"), null, 2));
You can use .set function of lodash https://lodash.com/docs#set
ex: _.set(obj, 'level1.level2.level3.title', 'super-winner');
Or use ES6 syntax function:
var str = 'level1.level2.level3.title';
str.split('.').reduce((p, c, index) => {
if (index === str.split('.').length - 1) {
if (typeof p[c] !== "object") { // string, number, boolean, null, undefined
p[c] = 'super-winner'
}
return p[c];
} else {
if (!p[c] || typeof p[c] !== 'object') {
p[c] = {};
}
return p[c];
}
}, obj)
console.log(obj);

Retrieve Path of Object items

I'm wondering if its possible to retrieve all paths a javascript object contains
Example:
obj = {
prop1 : {
x: 19
y: 43
}
prop2 : {
another: {
here: 1
}
}
prop3: "hello"
}
Where the result would be an array with following elements:
Result: ["prop1.x", "prop1.y", "prop2.another.here", "prop3"]
Is this possible?
Thanks!
function flattenKeys(obj, delimiter) {
delimiter = delimiter || '.';
return recurse(obj, '', []);
function recurse(obj, path, result) {
if (typeof obj === "object") {
Object.keys(obj).forEach(function (key) {
recurse(obj[key], path + delimiter + key, result);
});
} else {
result.push(path.slice(delimiter.length));
}
return result;
}
}
used as
var obj = {
prop1 : {
x: 19,
y: 43
},
prop2 : {
another: {
here: 1
}
},
prop3: "hello"
};
flattenKeys(obj);
// -> ["prop1.x", "prop1.y", "prop2.another.here", "prop3"]
Alternative implementation without string operations:
function flattenKeys(obj, delimiter) {
delimiter = delimiter || '.';
return recurse(obj, [], []);
function recurse(obj, path, result) {
if (typeof obj === "object") {
Object.keys(obj).forEach(function (key) {
path.push(key);
recurse(obj[key], path, result);
path.pop();
});
} else {
result.push(path.join(delimiter));
}
return result;
}
}
Wrote this while Tomalak was putting together here. Recursion's the obvious approach for doing this.
var inputObject = {
prop1: {
x: 19,
y: 43
},
prop2: {
another: {
here: 1
}
},
prop3: "hello"
};
function getProps(obj) {
var props = [];
var findPropsRecursive = function (robj, str) {
robj = robj || {};
var keys = Object.keys(robj);
if (keys.length > 0 && (robj instanceof Object)) {
return keys.map(function (key) {
return findPropsRecursive(robj[key], str + (str ? '.' : '') + key);
});
} else {
props.push(str);
return '';
}
};
findPropsRecursive(obj, '');
return props;
}
console.log(getProps(inputObject));
on jsfiddle: http://jsfiddle.net/jkoudys/w49rcp40/

How to get the path from javascript object from key and value

I have a javascript object width depth.
I need to know the exact path from this key within the object ex: "obj1.obj2.data1"
I already know the key is data1, the value is 123.
My javascript object look like this
{
obj1: {
obj2: {
data1: 213,
data2: "1231",
obj3: {
data: "milf"
}
}
},
obj4: {
description: "toto"
}
}
How could I achieve that ?
here is a jsfiddle : http://jsfiddle.net/3hvav8xf/8/
I am trying to implement getPath.
I think recursive function can help to you (Updated version, to check value)
function path(c, name, v, currentPath, t){
var currentPath = currentPath || "root";
for(var i in c){
if(i == name && c[i] == v){
t = currentPath;
}
else if(typeof c[i] == "object"){
return path(c[i], name, v, currentPath + "." + i);
}
}
return t + "." + name;
};
console.log(path({1: 2, s: 5, 2: {3: {2: {s: 1, p: 2}}}}, "s", 1));
The following finds the path in any level of nested objects. Also with arrays.
It returns all the paths found, which is something you want if you have keys with the same name.
I like this approach because it works with lodash methods get and set out-of-the-box.
function findPathsToKey(options) {
let results = [];
(function findKey({
key,
obj,
pathToKey,
}) {
const oldPath = `${pathToKey ? pathToKey + "." : ""}`;
if (obj.hasOwnProperty(key)) {
results.push(`${oldPath}${key}`);
return;
}
if (obj !== null && typeof obj === "object" && !Array.isArray(obj)) {
for (const k in obj) {
if (obj.hasOwnProperty(k)) {
if (Array.isArray(obj[k])) {
for (let j = 0; j < obj[k].length; j++) {
findKey({
obj: obj[k][j],
key,
pathToKey: `${oldPath}${k}[${j}]`,
});
}
}
if (obj[k] !== null && typeof obj[k] === "object") {
findKey({
obj: obj[k],
key,
pathToKey: `${oldPath}${k}`,
});
}
}
}
}
})(options);
return results;
}
findPathsToKey({ obj: objWithDuplicates, key: "d" })
// ["parentKey.arr[0].c.d", "parentKey.arr[1].c.d", "parentKey.arr[2].c.d"]
Try it here - https://jsfiddle.net/spuhb8v7/1/
If you want the result to be a single key (first encountered), you can change the results to be a string and if defined, then return the function with it.
I ended up with the following function, that works with nested objects/arrays :
function findPath (obj, name, val, currentPath) {
currentPath = currentPath || ''
let matchingPath
if (!obj || typeof obj !== 'object') return
if (obj[name] === val) return `${currentPath}['${name}']`
for (const key of Object.keys(obj)) {
if (key === name && obj[key] === val) {
matchingPath = currentPath
} else {
matchingPath = findPath(obj[key], name, val, `${currentPath}['${key}']`)
}
if (matchingPath) break
}
return matchingPath
}
const treeData = [{
id: 1,
children: [{
id: 2
}]
}, {
id: 3,
children: [{
id: 4,
children: [{
id: 5
}]
}]
}]
console.log(findPath (treeData, 'id', 5))
Here you go!
function getPath(obj, value, path) {
if(typeof obj !== 'object') {
return;
}
for(var key in obj) {
if(obj.hasOwnProperty(key)) {
console.log(key);
var t = path;
var v = obj[key];
if(!path) {
path = key;
}
else {
path = path + '.' + key;
}
if(v === value) {
return path;
}
else if(typeof v !== 'object'){
path = t;
}
var res = getPath(v, value, path);
if(res) {
return res;
}
}
}
}
getPath(yourObject, valueYouWantToFindPath);
Rerutns path if found, else returns undefined.
I have only tested it with objects & comparison is very strict(ie: used ===).
Update:
Updated version that takes key as an argument.
function getPath(obj, key, value, path) {
if(typeof obj !== 'object') {
return;
}
for(var k in obj) {
if(obj.hasOwnProperty(k)) {
console.log(k);
var t = path;
var v = obj[k];
if(!path) {
path = k;
}
else {
path = path + '.' + k;
}
if(v === value) {
if(key === k) {
return path;
}
else {
path = t;
}
}
else if(typeof v !== 'object'){
path = t;
}
var res = getPath(v, key, value, path);
if(res) {
return res;
}
}
}
}
getPath(yourObject, key, valueYouWantToFindPath);
JSON Object can be handled in JavaScript as associative array.
So You can cycle through and store indexes of "parents" in some variables.
Assume the whole object to be stored in variable called obj.
for( var p1 in obj )
{
for( var p2 in obj[ p1 ] )
{
for( var p3 in obj[ p1 ][ p2 ] )
{
// obj[ p1 ][ p2 ][ p3 ] is current node
// so for Your example it is obj.obj1.obj2.data1
}
}
}
Hope answer was helpful.
I would do this job as follows;
Object.prototype.paths = function(root = [], result = {}) {
var ok = Object.keys(this);
return ok.reduce((res,key) => { var path = root.concat(key);
typeof this[key] === "object" &&
this[key] !== null ? this[key].paths(path,res)
: res[this[key]] == 0 || res[this[key]] ? res[this[key]].push(path)
: res[this[key]] = [path];
return res;
},result);
};
var myObj = {
obj1: {
obj2: {
data1: 213,
data2: "1231",
obj3: {
data: "milf"
}
}
},
obj4: {
description: "toto",
cougars: "Jodi",
category: "milf"
}
},
value = "milf",
milfPath = myObj.paths()[value]; // the value can be set dynamically and if exists it's path will be listed.
console.log(milfPath);
A few words of warning: We should be cautious when playing with the Object prototype. Our modification should have the descriptor enumerable = false or it will list in the for in loops and for instance jQuery will not work. (this is how silly jQuery is, since apparently they are not making a hasOwnProperty check in their for in loops) Some good reads are here and here So we have to add this Object method with Object.defineProperty() to make it enumerable = false;. But for the sake of simplicity and to stay in the scope of the question i haven't included that part in the code.
Here is a pretty short, and relatively easy to understand function I wrote for retrieving the JSON Path for every property/field on an Object (no matter how deeply nested, or not).
The getPaths(object) function just takes the Object you'd like the JSON Paths for and returns an array of paths. OR, if you would like the initial object to be denoted with a symbol that is different from the standard JSON Path symbol, $, you can call getPaths(object, path), and each JSON Path will begin with the specified path.
For Example: getPaths({prop: "string"}, 'obj'); would return the following JSON Path: obj.prop, rather than $.prop.
See below for a more detailed, in depth example of what getPaths returns, and how it is used.
object = {
"firstName": "John",
"lastName": "doe",
"age": 26,
"fakeData": true,
"address": {
"streetAddress": "fake street",
"city": "fake city",
"postalCode": "12345"
},
"phoneNumbers": [{
"type": "iPhone",
"number": "0123-4567-8888"
}, {
"type": "home",
"number": "0123-4567-8910"
}]
};
function getPaths(object, path = "$") {
return Object.entries(object).flatMap(function(o, i) {
if (typeof o[1] === "object" && !o[1].length) {
return `${getPaths(o[1], path + '.' + o[0])}`.split(',');
} else if (typeof o[1] === "object" && o[1].length) {
return Object.entries(o[1]).flatMap((no, i) => getPaths(no[1], `${path}.${o[0]}[${i}]`));
} else {
return `${path}.${o[0]}`;
}
});
}
console.log(`%o`, getPaths(object));
I really liked Roland Jegorov's answer, but I had a very complex object that I needed to search through and that answer could not account for it.
If you were in a situation like mine you may want to first make sure you have no circular references (or else you'll run into an infinite search). There are a few ways to do this, but I was having to stringify my object to copy it into other windows, so I ended up using this circular replacer: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cyclic_object_value
(Update here - I made a small change to the getCircularReplacer function from MDN so it no longer leaves out function references since that is what I was looking for!)
(Update 3 - I also wanted to check on methods of any instances of classes, but I was returning just 'function' too early, so I have adjusted it to include instance methods. I think it finally works as I intended!)
const getCircularReplacer = () => {
const seen = new WeakSet();
return (key, value) => {
if (typeof value === "function") {
if (value?.prototype) {
if (seen.has(value.prototype)) {
return;
}
seen.add(value.prototype)
return value.prototype
}
return "function";
}
if (typeof value === "object" && value !== null) {
if (seen.has(value)) {
return;
}
seen.add(value);
}
return value;
};
};
const nonCyclicObject = JSON.parse(JSON.stringify(myComplexObject, getCircularReplacer()));
Then I used this modified version of Roland's answer:
(Update 2: I had to make sure not to return after the key was found as it would always simply return after only calling the function once if the first level of the object had that key)
function findPathsToKey(options) {
let count = 0;
let results = [];
(function findKey({
key,
obj,
pathToKey,
}) {
count += 1;
if (obj === null) return;
const oldPath = `${pathToKey ? pathToKey + "." : ""}`;
if (Object.hasOwnProperty.call(obj, key)) {
results.push(`${oldPath}${key}`);
}
if (typeof obj === "object" && !Array.isArray(obj)) {
for (const k in obj) {
if (Object.hasOwnProperty.call(obj, k)) {
if (Array.isArray(obj[k])) {
for (let j = 0; j < obj[k].length; j++) {
findKey({
obj: obj[k][j],
key,
pathToKey: `${oldPath}${k}[${j}]`,
});
}
}
if (typeof obj[k] === "object") {
findKey({
obj: obj[k],
key,
pathToKey: `${oldPath}${k}`,
});
}
}
}
}
})(options);
return { count, results };
};
The count was just to troubleshoot a little bit and make sure it was actually running through the amount of keys I thought it was. Hope this helps any others looking for a solution!
⚠️ This code doesn't answer the question but does related: transforms nested object to query object with dot.divided.path as keys and non-object values; compatible with URlSearchParams & qs. Maybe will be useful for someone.
const isPlainObject = (v) => {
if (Object.prototype.toString.call(v) !== '[object Object]') return false;
const prototype = Object.getPrototypeOf(v);
return prototype === null || prototype === Object.prototype;
};
const objectToQueryObject = (obj, path) => {
return Object.entries(obj).reduce((acc, [key, value]) => {
const newPath = path ? `${path}.${key}` : key;
if (isPlainObject(value)) {
return {
...acc,
...objectToQueryObject(value, newPath)
};
}
acc[newPath] = value;
return acc;
}, {})
};
const queryObjectRaw = {
value: {
field: {
array: {
'[*]': {
field2: {
eq: 'foo',
ne: 'bar',
}
}
},
someOtherProp: { in: [1, 2, 3],
ne: 'baz',
}
},
someOtherField: {
gt: 123
},
},
otherValue: {
eq: 2
},
};
const result = objectToQueryObject(queryObjectRaw);
console.log('result', result);
const queryString = new URLSearchParams(result).toString();
console.log('queryString', queryString);
If you know only the value and not the key, and want to find all paths with this value use this.
It will find all property with that value, and print the complete path for every founded value.
const createArrayOfKeys = (obj, value) => {
const result = []
function iter(o) {
Object.keys(o).forEach(function(k) {
if (o[k] !== null && typeof o[k] === 'object') {
iter(o[k])
return
}
if (o[k]=== value) {
result.push(k)
return
}
})
}
iter(obj)
return result
}
function findPath (obj, name, val, currentPath) {
currentPath = currentPath || ''
let matchingPath
if (!obj || typeof obj !== 'object') return
if (obj[name] === val) return `${currentPath}/${name}/${val}`
for (const key of Object.keys(obj)) {
if (key === name && obj[key] === val) {
matchingPath = currentPath
} else {
matchingPath = findPath(obj[key], name, val, `${currentPath}/${key}`)
}
if (matchingPath) break
}
return matchingPath
}
const searchMultiplePaths = (obj, value) => {
const keys = createArrayOfKeys(obj, value)
console.log(keys);
keys.forEach(key => {
console.log(findPath(obj, key, value))
})
}
var data = { ffs: false, customer: { customer_id: 1544248, z_cx_id: '123456' }, selected_items: { '3600196': [{ id: 4122652, name: 'Essential Large (up to 8\'x10\')', selected: true }] }, service_partner: { id: 3486, name: 'Some String', street: '1234 King St.', hop: '123456' }, subject: 'Project-2810191 - Orange Juice Stain (Rug)', description: 'Product Type: \n\nIssue: (copy/paste service request details here)\n\nAction Required:', yes: '123456' };
searchMultiplePaths(data, '123456')
I know the post is old but the answers don't really satisfy me.
A simple solution is to add the object path to each object in the structure. Then you can easily read the path when you need it.
let myObject = {
name: 'abc',
arrayWithObject: [
{
name: "def"
},
{
name: "ghi",
obj: {
name: "jkl"
}
}
],
array: [15, 'mno'],
arrayArrayObject: [
[
{
name: '...'
}
]
]
}
function addPath(obj, path = [], objectPathKey = '_path') {
if (Array.isArray(obj)) {
obj.map((item, idx) => addPath(item, [...path, idx]))
} else if (typeof obj === "object") {
obj[objectPathKey] = path;
for (const key in obj) {
obj[key] = addPath(obj[key], [...path, key])
}
}
return obj
}
myObject = addPath(myObject);
let changeMe = _.cloneDeep(myObject.arrayWithObject[0])
changeMe.newProp = "NEW"
changeMe.newNested = {name: "new", deeper: {name: "asdasda"}}
changeMe = addPath(changeMe, changeMe._path)
_.set(myObject, changeMe._path, changeMe);
When your updates are done sanitize your object and remove your _path property.
Advantages of this solution:
You do the work once
you keep your code simple
no need for own property checks
no cognitive overload
I can highly suggest you to use lodash for this problem.
In their documentation this should help you out
// using "_.where" callback shorthand
_.find(characters, { 'age': 1 });
// → { 'name': 'pebbles', 'age': 1, 'blocked': false }

Categories

Resources