'Flatten a deeply nested data structure of arrays, objects + strings into a list of data items while mapping the former parent-child relationship too

Restructuring array of objects to new array

Problem

There’s an array of objects that contains plain strings and might contain nested arrays as well. We want to create a new Array that will contain a node for each item in the array and separate nodes for each array item connected to its parent. Each parent node should have the following structure:

{
    id: uuidv4(),
    position: { x: 0, y: 0 },
    data: { label: <item data goes here> }
}

Each array node with the following schema above, should also have a connection edge item added to the array with the following properties:

{
    id: ‘e<array item Id>-<parentId>’,
    source: <array item Id>,
    target: <parentId>,
}

Example

We have the following array of objects for example:

[
  {
    "author": "John Doe",
    "age": 26,
    "books": [
      {
        "title": "Book 1"
      },
      {
        "title": "Book 2",
        "chapters": [
          {
            "title": "No Way Home",
            "page": 256
          }
        ]
      }
    ]
  }
]

The expected output is:

[
  {
    "id": "1",
    "data": {
      "label": {
        "author": "John Doe",
        "age": 26,
      }
    }
  },
  {
    "id": "2",
    "data": {
      "label": "books" // key of array
    }
  },
  {
    "id": "3",
    "data": {
      "label": {
        "title": "Book 1"
      }
    }
  },
  {
    "id": "4",
    "data": {
      "label": {
        "title": "Book 2"
      }
    }
  },
  {
    "id": "5",
    "data": {
      "label": "chapters" // key of array
    }
  },
  {
    "id": "6",
    "data": {
      "label": {
        "title": "No Way Home",
        "page": 256
      }
    }
  },
  {
    "id": "e2-1",
    "source": "2",
    "target": "1"
  },
  {
    "id": "e3-2",
    "source": "3",
    "target": "2"
  },
  {
    "id": "e4-2",
    "source": "4",
    "target": "2"
  },
  {
    "id": "e5-4",
    "source": "5",
    "target": "4"
  },
  {
    "id": "e6-5",
    "source": "6",
    "target": "5"
  }
]


Solution 1:[1]

One has to choose a self recursive approach which in a generic way can process both, array-items and object-entries. Also, while the recursive process takes place, one not only has to create and collect the consecutively/serially numbered (the incremented id value) data nodes, but one in addition needs to keep track of every data node's parent reference in order to finally concatenate the list of edge items (as the OP calls it) to the list of data nodes.

function flattenStructureRecursively(source = [], result = [], tracker = {}) {
  let {
    parent = null, edgeItems = [],
    getId = (id => (() => ++id))(0),
  } = tracker;

  const createEdgeItem = (id, pid) => ({
    id: `e${ id }-${ pid }`,
    source: id,
    target: pid,
  });
  const putNodeData = node => {
    result.push(node);

    if (parent !== null) {
      edgeItems.push(createEdgeItem(node.id, parent.id));
    }
    // every data node is a parent entity too.
    parent = node;
  };

  if (Array.isArray(source)) {
    result.push(
      ...source.flatMap(item =>
        flattenStructureRecursively(item, [], {
          getId, parent, edgeItems,
        })
      )
    );
  } else {
    let {
      dataNode,
      childEntries,
    } = Object
      .entries(source)
      .reduce(({ dataNode, childEntries }, [key, value]) => {
        if (value && (Array.isArray(value) || (typeof value === 'object'))) {
          // collect any object's iterable properties.

          childEntries.push([key, value]);
        } else {
          // aggregate any object's non iterable
          // properties at data node level.
          (dataNode ??= {

            id: getId(),
            data: { label: {} }

          }).data.label[key] = value;
        }
        return { dataNode, childEntries };

      }, { dataNode: null, childEntries: [] });

    if (dataNode !== null) {
      putNodeData(dataNode);
    }
    childEntries
      .forEach(([key, value]) => {
        // every object's iterable property is supposed
        // to be created as an own parent entity.

        dataNode = {
          id: getId(),
          data: { label: key },
        };
        putNodeData(dataNode);

        result.push(
          ...flattenStructureRecursively(value, [], {
            getId, parent, edgeItems,
          })
        );
      });
  }
  if (parent === null) {
    // append all additionally collected edge items
    // in the end of all the recursion.

    result.push(...edgeItems);
  }
  return result;
}

console.log(
  flattenStructureRecursively([{
    author: "John Doe",
    pseudonym: "J.D.",
    books: [{
      title: "Book 1",
    }, {
      title: "Book 2",
      chapters: [{
        title: "No Way Home",
        page: 256,
      }],
    }],
    age: 26,
  }])
);
.as-console-wrapper { min-height: 100%!important; top: 0; }

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