How to convert a list of objects with each object having an array of elements, to an array of objects with the subelement as a property

Viewed 131

Context

  • Node.js project with Mongoose and Pug as the templating engine

Example JSON content (fetched with Mongoose)

[
    {
        "name": "user1",
        "blogs": [
            {
                "title": "blog1",
                "created": "01-01-2020"
            },
            {
                "title": "blog2",
                "created": "01-01-2020"
            }
        ]
    },
    {
        "name": "user2",
        "blogs": [
            {
                "title": "blog3",
                "created": "01-01-2020"
            },
            {
                "title": "blog4",
                "created": "01-01-2020"
            }
        ]
    }
]

Problem

I'm trying to show a list of all blog posts, sorted by the creation date of the post and show the user that created it.

Injecting the above object in Pug and performing a foreach will group all performances by the user and makes it impossible to sort on the creation date of all performances (in thisobject form).

My solution would be the object to the following:

Example JSON, mapping each blog entry to a user

[
    {
        "name": "user1",
        "blog": {
            "title": "blog1",
            "created": "01-01-2020"
        }
    },
    {
        "name": "user1",
        "blog": {
            "title": "blog2",
            "created": "01-01-2020"
        }
    },
    {
        "name": "user2",
        "blog": {
            "title": "blog3",
            "created": "01-01-2020"
        }
    },
    {
        "name": "user2",
        "blog": {
            "title": "blog4",
            "created": "01-01-2020"
        }
    }
]

This object form could make it possible to still display the data of the user with each blog entry and sort on the creation date of the performance.

I'm trying to achieve the latter, but I haven't found a clean way of doing it without foreaching or mapping.

Note

This problem could easily solved by defining a relationship and resolving the keys, but I'm explicitly trying to solve this problem with NoSQL. Consequently, there is no reference of the user in the blog entity.

How would you solve this and what is the characteristic name of this particular operation?

3 Answers

Instead of first fetching the data from the database in the format you don't want and then converting it to a format you want, you could use aggregation to get the data from MongoDB in the desired format:

db.collection.aggregate([
  { $unwind: "$blogs" },
  {
    $project: {
      "_id": 0,
      name: 1,
      blogs: 1,
      date: {
        $dateFromString: {
          dateString: "$blogs.created"
        }
      }
    }
  },
  { $sort: { date: -1 } },
  { $project: { date: 0 } }
])

Explanation of aggregation pipeline stages used in above query:

  1. $unwind - deconstruct blogs array to output a separate document with each object in the blogs array

  2. $project - hide _id field, show name and blogs field and create a new field named date and set its value equal to the field created converted to date. (The value of the created field is converted to a date to sort the documents correctly based on the date instead of the date string.)

  3. $sort - sort the documents based on date field in descending order (-1 for descending, 1 for ascending), i.e. from the most recent date to older date.

  4. $project - hide the date field from the output documents

For details on the above mentioned aggregation pipeline stages, see:

Demo:

This demo shows the above mentioned query in action.

Try with Array.prototype.reduce. Something like this -

const data = [
    {
        "name": "user1",
        "blogs": [
            {
                "title": "blog1",
                "created": "01-01-2020"
            },
            {
                "title": "blog2",
                "created": "01-01-2020"
            }
        ]
    },
    {
        "name": "user2",
        "blogs": [
            {
                "title": "blog3",
                "created": "01-01-2020"
            },
            {
                "title": "blog4",
                "created": "01-01-2020"
            }
        ]
    }
];

const result = data.reduce((acc, curr) => {
    const { name, blogs } = curr;
    const blogArr = blogs.map((blog) => {
        return {
            name,
            blog
        };
    });

    Array.prototype.push.apply(acc, blogArr);
    return acc;
}, []);

console.log(result);

I would use a feature that MongoDB offers named aggregation. What aggregation lets you do is format your data how you want before you fetch it.

Aggregation works in a pipeline format. You give it a list of commands that will be executed in the order you send it in.

For this case the aggregation pipeline would look like:

[{
    $unwind: {
        path: '$blogs'
    }
}, {
    $project: {
        _id: 0,
        name: 1,
        blog: '$blogs'
    }
}, {
    $sort: {
        "blogs.created": -1
    }
}]

Commands used in this pipeline:

$unwind - deconstructs an array of items into multiple documents which all contain the original fields of the original documents except for the unwound field which now have a value of all the deconstructed objects in the array.

$project - it lets you format the data however you want, removing fields, adding fields and renaming fields.

$sort - As the name says, it lets you sort the documents, specifying the fields to sort by and the order (ascending or descending).

For more information about aggregation, visit MongoDB's website:

Related