2

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.

dougmacklin
  • 2,560
  • 10
  • 42
  • 69

1 Answers1

1

Here are the observations that led to my solution:

  1. To create a new object, you need to use return somewhere in your function.
  2. To remove items from array, you need to filter out valid items first, then recursively call traverse on them.
  3. 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.

Shihab Shahriar Khan
  • 4,930
  • 1
  • 18
  • 26
  • thank you! I got it working after removing the `if (obj.sys==undefined)` clause, as that was resulting in a number of nested objects returning before being fully traversed. I also added in an `if (typeof obj === 'object')` before looping through the keys. – dougmacklin Nov 02 '17 at 10:21
  • your `typeof` makes sense. But your sys object is a special case, how did you handle it then? – Shihab Shahriar Khan Nov 02 '17 at 12:10
  • Your `good()` function check's to see if an object contains a `sys.id` matching one in the `invalidIds` list. If we start at the root of the object and traverse our way down, I don't think there is a way to ever get to a point where you're in the `sys` property that hasn't already been caught. doing `if (obj.sys==undefined) if (obj.id!=undefined)` prevents it from traversing through nodes that have any `sys.id`, even if they have children that we need to remove. right? – dougmacklin Nov 02 '17 at 14:30
  • "`if ` prevents it from traversing through nodes that have any `sys.id`, even if they have children that we need to remove" - no they won't be removed, only nodes that doesn't have `sys` but have `id`. So yes..if any normal node have `id`, but no `sys`, then it won't be traversed. But that goes against 2nd line of your question. – Shihab Shahriar Khan Nov 02 '17 at 14:54
  • ah I apologize for the confusion, I should have stated it better. basically every object has a sys property which contains an id property. no item will have an id property not contained within a parent sys property. I should have made that more clear – dougmacklin Nov 02 '17 at 16:25