Javascript deep object filter

84 views Asked by At

I am building deepFilterObject.js utility function. Tried many, none of them work. My object before the filter is something like this:

{
  userId: '6501747e258044dcfcf8c765',
  name: 'Morning workout',
  exercises: [
    { exercise: 'Oblique Crunch', sets: [Array] },
    { exercise: 'Chest Fly (Dumbbell)', sets: [Array] },
    { exercise: 'Jump Squat', sets: [Array] }
  ],
  metadata: {
    evaluation: null,
    type: 'realtime',
    copy: false,
    note: 'This is a note. (optional)',
    estimatedDurationMinutes: 60,
    description: 'Some description. (optional)',
    dates: {
      scheduled: 'Tue Aug 08 2023 10:55:56',
      completed: null,
      creation: 'Tue Aug 01 2023 12:51:35'
    },
    access: {
      authorId: 'objectId',
      type: 'public/private (ENUM)',
      comments: [Array],
      copies: 0
    }
  }
}

Here is my function call:

const filteredBody = deepFilterObject(object, 'name', 'exercises', 'metadata.evaluation', 'metadata.access.type');

I expect something like this object as a result:

{
  userId: '6501747e258044dcfcf8c765',
  name: 'Morning workout',
  exercises: [
    { exercise: 'Oblique Crunch', sets: [Array] },
    { exercise: 'Chest Fly (Dumbbell)', sets: [Array] },
    { exercise: 'Jump Squat', sets: [Array] }
  ],
  metadata: {
    evaluation: null,
    access: {
      type: 'public/private (ENUM)',
    }
  }
}

Here is my code for my utility, which I import into my controller as "deepFilterObject":

function createObjectFromNestedArray(inputArray, sourceObject) {
  // Base case: If the input array is empty, return the sourceObject.
  if (inputArray.length === 0) {
    return sourceObject;
  }

  // Get the current key from the inputArray.
  const [currentKey, ...remainingKeys] = inputArray[0];

  // Check if the current key exists in the sourceObject.
  if (sourceObject.hasOwnProperty(currentKey)) {
    // If it exists, recursively call the function with the remaining keys and the existing object.
    sourceObject[currentKey] = createObjectFromNestedArray(remainingKeys, sourceObject[currentKey]);
  } else {
    // If it doesn't exist, create a new object and attach it.
    sourceObject[currentKey] = {};

    // Recursively call the function with the remaining keys and the newly created object.
    sourceObject[currentKey] = createObjectFromNestedArray(remainingKeys, sourceObject[currentKey]);
  }

  // Return the modified sourceObject.
  return sourceObject;
}

function deepFilterObject(obj, ...allowedFields) {
  console.log(obj);
  //   const newObj = {};
  //   Object.keys(obj).forEach(el => {
  //     if (allowedFields.includes(el)) newObj[el] = obj[el];
  //   });

  const deepAllowedFields = allowedFields
    .filter(allowedField => allowedField.split('.').length > 1)
    .map(allowedField => allowedField.split('.'));

  const finalObj = createObjectFromNestedArray(deepAllowedFields, obj);
  //   console.log(finalObj);

  //   return newObj;
}

module.exports = deepFilterObject;

Any help is appreciated! Thanks in advance!

3

There are 3 answers

1
Dimava On BEST ANSWER

The most easy way is to copy each path in order from source to a new object, creating the path in new object if it's missing

function deepFilterObject(obj, ...paths) {
    let result = {};
    for (let s of paths) {
        let from = obj, to = result;
        let path = s.split('.');
        let prop = path.pop();
        for (let p of path) {
            to = (to[p] ??= {});
            from = from[p];
        }
        to[prop] = from[prop];
    }
    return result;
}
0
Konrad On

I would do it like this:

const object = {
  userId: '6501747e258044dcfcf8c765',
  name: 'Morning workout',
  exercises: [
    { exercise: 'Oblique Crunch', sets: [Array] },
    { exercise: 'Chest Fly (Dumbbell)', sets: [Array] },
    { exercise: 'Jump Squat', sets: [Array] }
  ],
  metadata: {
    evaluation: null,
    type: 'realtime',
    copy: false,
    note: 'This is a note. (optional)',
    estimatedDurationMinutes: 60,
    description: 'Some description. (optional)',
    dates: {
      scheduled: 'Tue Aug 08 2023 10:55:56',
      completed: null,
      creation: 'Tue Aug 01 2023 12:51:35'
    },
    access: {
      authorId: 'objectId',
      type: 'public/private (ENUM)',
      comments: [Array],
      copies: 0
    }
  }
}

const filteredBody = deepFilterObject(object, 'name', 'exercises', 'metadata.evaluation', 'metadata.access.type');

document.write(`<pre>${JSON.stringify(filteredBody, null, 4)}</pre>`)

function deepFilterObject(object, ...paths) {
  // parse paths to get the path object
  const pathObject = preparePathObject(paths)
  
  // recursive function
  function filterObject(obj, path) {
    const newObject = {}
    // for each entry in the path object
    for (const [key, subPath] of Object.entries(path)) {
      // get sub object from the original object
      const subObj = obj[key]
      // if the sub object is an object, but not null or array
      if (typeof subObj === 'object' && subObj !== null && !Array.isArray(subObj)) {
        // run recursion
        newObject[key] = filterObject(subObj, subPath)
      } else {
        // else assign the object to the new object
        newObject[key] = subObj
      }
    }
    
    return newObject
  }
  
  return filterObject(object, pathObject)
}

// creates nested path object
function preparePathObject(paths) {
  const obj = {}
  for (const path of paths) {
    const keys = path.split('.')
    let current = obj
    for (const key of keys) {
      if (!(key in current)) {
        current[key] = {}
      }
      current = current[key]
    }
  }
  return obj
}

0
Alexander Nenashev On

Loop the paths and go recursive if the path is multi-level:

function deepFilterObject(obj, ...props){

  const filter = (obj, out, p, idx = 0) => {
      const k = p[idx];
      idx  === p.length - 1 ? out[k] = obj[k] :filter(obj[k], out[k] ??= {}, p, idx + 1);
  };
  
  const out = {};
  for(const p of props){
    p in obj ? out[p] = obj[p] : filter(obj, out, p.split('.'));
  }
  return out;

}

const filteredBody = deepFilterObject(obj, 'name', 'exercises', 'metadata.evaluation', 'metadata.access.type');

$pre.innerText = JSON.stringify(filteredBody, null, 4);
<script>
const obj ={
  userId: '6501747e258044dcfcf8c765',
  name: 'Morning workout',
  exercises: [
    { exercise: 'Oblique Crunch', sets: [Array] },
    { exercise: 'Chest Fly (Dumbbell)', sets: [Array] },
    { exercise: 'Jump Squat', sets: [Array] }
  ],
  metadata: {
    evaluation: null,
    type: 'realtime',
    copy: false,
    note: 'This is a note. (optional)',
    estimatedDurationMinutes: 60,
    description: 'Some description. (optional)',
    dates: {
      scheduled: 'Tue Aug 08 2023 10:55:56',
      completed: null,
      creation: 'Tue Aug 01 2023 12:51:35'
    },
    access: {
      authorId: 'objectId',
      type: 'public/private (ENUM)',
      comments: [Array],
      copies: 0
    }
  }
}
</script>
<pre id="$pre"></pre>

And a benchmark:

Cycles: 1000000 / Chrome/116
--------------------------------------------------------
Alexander;   172/min  1.0x  183  186  181  172  177  187
Dimava       183/min  1.1x  187  187  211  198  199  183
Konrad       534/min  3.1x  556  534  560  577  534  567
--------------------------------------------------------
https://github.com/silentmantra/benchmark

<script benchmark="1000000">

    const obj ={
      userId: '6501747e258044dcfcf8c765',
      name: 'Morning workout',
      exercises: [
        { exercise: 'Oblique Crunch', sets: [Array] },
        { exercise: 'Chest Fly (Dumbbell)', sets: [Array] },
        { exercise: 'Jump Squat', sets: [Array] }
      ],
      metadata: {
        evaluation: null,
        type: 'realtime',
        copy: false,
        note: 'This is a note. (optional)',
        estimatedDurationMinutes: 60,
        description: 'Some description. (optional)',
        dates: {
          scheduled: 'Tue Aug 08 2023 10:55:56',
          completed: null,
          creation: 'Tue Aug 01 2023 12:51:35'
        },
        access: {
          authorId: 'objectId',
          type: 'public/private (ENUM)',
          comments: [Array],
          copies: 0
        }
      }
    }
    
    // @benchmark Konrad
    function deepFilterObject3(object, ...paths) {
  // parse paths to get the path object
  const pathObject = preparePathObject(paths)
  
  // recursive function
  function filterObject(obj, path) {
    const newObject = {}
    // for each entry in the path object
    for (const [key, subPath] of Object.entries(path)) {
      // get sub object from the original object
      const subObj = obj[key]
      // if the sub object is an object, but not null or array
      if (typeof subObj === 'object' && subObj !== null && !Array.isArray(subObj)) {
        // run recursion
        newObject[key] = filterObject(subObj, subPath)
      } else {
        // else assign the object to the new object
        newObject[key] = subObj
      }
    }
    
    return newObject
  }
  
  return filterObject(object, pathObject)
}

// creates nested path object
function preparePathObject(paths) {
  const obj = {}
  for (const path of paths) {
    const keys = path.split('.')
    let current = obj
    for (const key of keys) {
      if (!(key in current)) {
        current[key] = {}
      }
      current = current[key]
    }
  }
  return obj
}
// @run
 deepFilterObject3(obj, 'name', 'exercises', 'metadata.evaluation', 'metadata.access.type');   
    // @benchmark Dimava
    function deepFilterObject2(obj, ...paths) {
    let result = {};
    for (let s of paths) {
        let from = obj, to = result;
        let path = s.split('.');
        let prop = path.pop();
        for (let p of path) {
            to = (to[p] ??= {});
            from = from[p];
        }
        to[prop] = from[prop];
    }
    return result;
    } 
    
    //@run
     deepFilterObject2(obj, 'name', 'exercises', 'metadata.evaluation', 'metadata.access.type');
    
    // @benchmark Alexander;


function deepFilterObject(obj, ...props){

  const filter = (obj, out, p, idx = 0) => {
      const k = p[idx];
      idx  === p.length - 1 ? out[k] = obj[k] :filter(obj[k], out[k] ??= {}, p, idx + 1);
  };
  
  const out = {};
  for(const p of props){
    p in obj ? out[p] = obj[p] : filter(obj, out, p.split('.'));
  }
  return out;

}
    //@run;

    deepFilterObject(obj, 'name', 'exercises', 'metadata.evaluation', 'metadata.access.type');

</script>

<script src="https://cdn.jsdelivr.net/gh/silentmantra/benchmark/loader.js"></script>