Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JS - property replacement

Dear Stack Overflow Community,
I have a question regarding object property replacement (you can skip to the bottom to read the question) . I'm working on an application, that gets a teacher object from the Back-end. The Back-end is using java hibernate to to a query of the database and serialize the object to be send to the front end (me). I get this teacher object, but there is circular references in the object. Which java handles by adding a reference id in place of the object. So i get this teacher object, and inside the object are these reference id's that i need to replace with the actual object, the problem being that they are nested objects. What i currently do is create a stack, and traverse the objects to find these id's and reference id's. When i find an id i put it on the stack. Not a problem, but when i find a reference id i need to repalce it with the one i found on the stack. Once again this is hard to do because i do these through a recursive function, so i don't have the physical object.

My question is, how can i replace nested object properties

JS BIN

var stack = {};
var add = function(prop, data) {
  if (prop == '@id') {
    stack[data[prop]] = data;
  }
  if (prop == '@ref') {
    // console.log(stack[data[prop]])
    // How do i replace the value with the stack object 
    // test.PROPERTY = stack[data[prop]] is what im looking for
  }
}
var traverse = function(object) {
  for (var property in object) {
    if (object.hasOwnProperty(property)) {
      add(property, object);
      if (object[property] && typeof object[property] === 'object') {
        traverse(object[property]);
      }
    }
  }
}
var test = {
  'teacher': {
    'course': {
      "@id": "1",
      'students': [{
        "@id": "2",
        'name': "John",
        'course': {
          "@ref": "1",
        }
      }, {
        "@id": "2",
        'name': "Next",
        'course': {
          "@ref": "1",
        }
      }]
    }
  }
};
setTimeout(() => {
  console.log(stack);
  // console.log(test); 
}, 500);
traverse(test);
like image 600
AJ_ Avatar asked Nov 22 '17 14:11

AJ_


4 Answers

Consider iterating over the data structure twice:

  1. Recursive traversal for populating an id-to-object map (where the keys are ids and the values are the actual objects). You can initialize the map in the first recursive call, and then pass it to the next recursion levels.

  2. Recursive traversal for replacing the references with the actual objects. Since you now have a map, this replacement can be done in-place.

Code

like image 131
Michael Bar-Sinai Avatar answered Oct 19 '22 17:10

Michael Bar-Sinai


You should first traverse through the object and collect:

  1. object dictionary - map of object id to object reference
  2. list of items, where:
    • first element is object containing property to replace
    • name of property to replace

Having such a list is a key of solution. It allows you both: avoid second recursive traverse of the test object, and storing enough information to replace the whole {"@ref":"..."} object and not only it's contents.

Next, you should iterate through the list and replace all {"@ref":"..."} objects with references to actual objects from dictionary.

var test = {
    'teacher': {
        'course': {
            "@id": "1",
            'students': [{
                "@id": "2",
                'name': "John",
                'course': {
                    "@ref": "1",
                }
            }, {
                "@id": "2",
                'name': "Next",
                'course': {
                    "@ref": "1",
                }
            }]
        }
    }
};

function collectRefs(obj, dic, list) {
    obj.hasOwnProperty('@id') && (dic[obj['@id']] = obj); // populate dictionary 
    Object.keys(obj).forEach(function(key) {
        if(obj[key] && typeof obj[key] === 'object') {
            if(obj[key].hasOwnProperty('@ref')) {
                list.push([obj, key]) // populate list
            } else {
                collectRefs(obj[key], dic, list) // traverse recursively 
            }
        }
    });
    return obj;
}

function recover(obj) {
    var dic = {}, list = [];
    collectRefs(obj, dic, list);
    list.forEach(function(item) {
        item[0][item[1]] = dic[item[0][item[1]]['@ref']]; // make replacements
    });
    return obj;
}

console.log(recover(test).teacher.course.students[0].course['@id']); // 1

At the end all {"@ref": "1"} will be replaced with reference to object {"@id": "1", ...}

P.S.

By the way, there is a better solution to serialize/deserialize JSON with circular references (cycle.js) provided by inventor of JSON format Douglas Crockford. It utilizes JSONPath as value of @ref objects and, therefore does not require to have @id on referred objects. Also, there is no need to have additional data structures (like dictionary and list from example above) to reconstruct original object with circular references. So, if you can change the way your Back-end serializes objects, I recommend you to look into it.

like image 21
Vadim Avatar answered Oct 19 '22 16:10

Vadim


I don't know if it's what you want

var test = {
    'teacher': {
        'course': {
            "@id": "1",
            'students': [{
                "@id": "2",
                'name': "John",
                'course': {
                    "@ref": "1",
                }
            }, {
                "@id": "2",
                'name': "Next",
                'course': {
                    "@ref": "1",
                }
            }]
        }
    }
};

let tmpObj = {};
let noRef = {};

function traverse(obj){
  Object.keys(obj).forEach(function(v){
    if(v !== '@ref' && typeof obj[v] === 'object'){
      var newObj = obj[v];
      if(obj[v]['@id']){
      	if(!tmpObj[v]){
      		tmpObj[v] = {};
      	}
        tmpObj[v][obj[v]['@id']] = obj;
      }
      if(obj[v]['@ref'] ){
        if(tmpObj[v] && tmpObj[v][obj[v]['@ref']]){
          obj[v]['@ref'] = tmpObj[v][obj[v]['@ref']][v];
        }else{
          if(!noRef[v]){
          	noRef[v] = [];
          }
          noRef[v].push(newObj)
        }
      }
      traverse(newObj);
    }
	})
}

traverse(test);
Object.keys(noRef).forEach(function(v){
	v.forEach(function(vv){
		vv['@ref'] = tmpObj[v][vv['@ref']][v];
	})
});

console.log(test);

The object noRef it's in the case the ref object was not yet found, so I put in it the parent object of the current ref, and when traverse has finished I loop on it to put the associatin to not found ref

like image 1
ggirodda Avatar answered Oct 19 '22 16:10

ggirodda


If you directly set the value. in the next iterations, the object will have a circular reference and you end up in an infinite loop. So push the assignment operation into the event queue( a simple way to do it is to call setTimeout passing 0 sec). I hope this will help you.

 var stack = {};
    var add = function(prop, data) {
      if (prop == '@id') {
        stack[data[prop]] = data;
      }
      if (prop == '@ref') {
        // console.log(stack[data[prop]])
        // How do i replace the value with the stack object 
        setTimeout(function(){
          data[prop] = stack
        },0);
      }
    }
    var traverse = function(object) {
      for (var property in object) {
        if (object.hasOwnProperty(property)) {
          add(property, object);
          if (object[property] && typeof object[property] === 'object') {
            traverse(object[property]);
          }
        }
      }
    }
    var test = {
      'teacher': {
        'course': {
          "@id": "1",
          'students': [{
            "@id": "2",
            'name': "John",
            'course': {
              "@ref": "1",
            }
          }, {
            "@id": "2",
            'name': "Next",
            'course': {
              "@ref": "1",
            }
          }]
        }
      }
    };
    setTimeout(() => {
      console.log(stack);
      // console.log(test); 
    }, 500);
    traverse(test);
like image 1
Pranay Kumar Avatar answered Oct 19 '22 18:10

Pranay Kumar