'Find and clone and add the clone after the found values with javascript in a object of arrays

I am trying to find a match in a object of arrays and clone this, change the ID and insert this after the found match.

Each plan has clusters and each cluster has goals, the idea is that I need to clone a goal and insert this AFTER the cloned goal (it will be loaded below this goal in the UI).

Main structure

{
    "id": 100,
    "title": "Plan ABC",
    "clusters": [
        {
           "id": 1,
           "subject": "Some subject",
           "goals": [
               {
                   "id": 1,
                   "title": "Goal A",
               },
               {
                   "id": 2,
                   "title": "Goal B",
               },
               {
                   "id": 3,
                   "title": "Goal C",
               },
           ], 
        },
        {
            "id": 2,
            "subject": "Some subject",
            "goals": [
                {
                    "id": 4,
                    "title": "Goal D",
                },
                {
                    "id": 5,
                    "title": "Goal E",
                },
                {
                    "id": 6,
                    "title": "Goal F",
                },
            ],
        },
    ]
}

My test code

// this would not work ofcourse!
const newId = 12345;
const matchToId = 2;
plan.clusters?.map(cluster => {
    cluster?.goals?.map((goal, i) => {
        if (goal.id === matchToId) {
            // will copy goal with id 2
            const copyGoal = goal;

            return {...goal, ...copyGoal};
        }
        return {...goal};
  });


  // this will work but it will change the id but not copy and add a the new object
  plan.clusters = clusters.map(cluster => {
        return {
            ...cluster,
            goals: cluster.goals?.filter(goal => {
                if (itemId == goal.id) {
                    const cloned = goal;
                    cloned.id = 12345;
                    return {...goal, cloned};
                }
                return goal;
            }),
        };
    });

What I want

{
    "id": 100,
    "title": "Plan ABC",
    "clusters": [
        {
           "id": 1,
           "subject": "Some subject",
           "goals": [
               {
                   "id": 1,
                   "title": "Goal A",
               },
               {
                   "id": 2,
                   "title": "Goal B",
               },
               // this will be added
               {
                   "id": 12345,
                   "title": "COPIED GOAL",
               },
               // ---
               {
                   "id": 3,
                   "title": "Goal C",
               },
           ], 
        },
        {
            "id": 2,
            "subject": "Some subject",
            "goals": [
                {
                    "id": 4,
                    "title": "Goal D",
                },
                {
                    "id": 5,
                    "title": "Goal E",
                },
                {
                    "id": 6,
                    "title": "Goal F",
                },
            ],
        },
    ]
}


Solution 1:[1]

This may be one possible solution to achieve the desired objective.

Code Snippet

// check if "tgtId" is present in "goals" array (parameter: "arr")
// and if so, insert "newObj" (parameter: "obj") into the array
const copyObjToGoal = (arr, tgtId, obj) => {
  let resObj = {goals: arr};        // the default "result object"
  const tgtIdx = arr.findIndex(     // search for "tgtId"
    ({ id }) => id === tgtId
  );
  if (~tgtIdx) {          // if tgtIdx is not -1 (ie, tgtId was found in arr)
    const newArr = [      // non-shallow-copy of "arr" (to prevent mutation)
      ...arr.map(
        x => ({...x})
      )
    ];                    // mutate the copied "newArr", not the parameter "arr"
    newArr.splice(
      tgtIdx + 1,         // add "after" the "tgtId" position in "goals" array
      0,                  // 0 indicates not to remove any element
      {...obj}            // destructure to shallow-copy the "newObj" object
    );
    resObj = {            // update the resObj by re-assigning
      goals: [...newArr]
    };
  };
  return resObj;          // return the result-object
};

// search and insert new/copied goal (not mutating 'myObj')
const searchAndInsert = (tgtId, newObj, obj) => (
  Object.fromEntries(           // transform below result back into object
    Object.entries(obj)         // iterate key-value pairs of "obj"
    .map(([k, v]) => {          
      if (k !== 'clusters') {   // if key is not "clusters", no change
        return [k, v];
      } else {                  // else (for "clusters"
        return [                // transform the "goals" array for each cluster
          k,                    // where "tgtId" is found
          v.map(                // iterate over array of "clusters"
            ({goals, ...r1}, idx) => {
              return {          // return each "clusters" array object
                ...r1,
                ...copyObjToGoal(goals, tgtId, newObj)
              }
            }
          )
        ];
      }
    })
  )
);

const myObj = {
    "id": 100,
    "title": "Plan ABC",
    "clusters": [
        {
           "id": 1,
           "subject": "Some subject",
           "goals": [
               {
                   "id": 1,
                   "title": "Goal A",
               },
               {
                   "id": 2,
                   "title": "Goal B",
               },
               {
                   "id": 3,
                   "title": "Goal C",
               }
           ]
        },
        {
            "id": 2,
            "subject": "Some subject",
            "goals": [
                {
                    "id": 4,
                    "title": "Goal D",
                },
                {
                    "id": 5,
                    "title": "Goal E",
                },
                {
                    "id": 6,
                    "title": "Goal F",
                }
            ]
        }
    ]
};

const targetGoalId = 2;
const newGoalObj = { id: '12345', title: 'copied goal' };
console.log(
  'Test case 1, from question\n',
  searchAndInsert(targetGoalId, newGoalObj, myObj)
);

console.log(
  'Test case 2, bespoke\n',
  searchAndInsert(6, { id: '12345', title: 'copied again' }, myObj)
);
.as-console-wrapper { max-height: 100% !important; top: 0; }

Explanation

Inline comments in the above snippet describe the significant aspects of the solution.

Notes

This solution employs below JavaScript features:

  1. Array .findIndex()
  2. ... spread
  3. Array .splice()
  4. Array .map()
  5. Object.fromEntries()
  6. Object.entries()
  7. De-structuring

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 jsN00b