Search code examples
javascriptpromisebluebirdes6-promise

JS Promise API: Create a loop of promises and merge result into another object


I am trying to understand Promise API.

Test Case: I am using three API's, from jsonplaceholder.

/user/{userId} #Returns a user
/posts?userId=1 #Returs list of posts by user
/comments?postId=1 #Returns list of comments for the post

I need to combine the output of all three API's into a structure like below.

var user = {
    'id' : "1",
    "name" : "Qwerty",
    "posts" : [
        {
            "id" : 1,
            "userid" : 1,
            "message" : "Hello",
            "comments" : [
                {
                    'id' : 1,
                    "postId" :1
                    "userid" : 2,
                    "message" : "Hi!"                   
                },
                {
                    'id' : 2,
                    "postId" :1
                    "userid" : 1,
                    "message" : "Lets meet at 7!"
                }
            ]
        }
    ]
}

The challenge I am facing is in merging comments to each post. Please Help. I mean I am not sure what to do.

Current Implementation.

var xyz = function (userId) {
    return Promise.all(
        [
            usersApi.getUsersByIdPromise(userId),
            postsApi.getPostsForUserPromise(userId)
        ]
    ).spread((user, posts) => {
        user.posts = posts;

        for (let post of user.posts){
            commentsApi.getCommentsForPostPromise(post.id)
                .then(comments => {                   
                    //Merge comments to post
                    //How do i return a merged user object
                });
        }        
    })
}

Solution

  • You were on the right track, see comments:

    var xyz = function (userId) {
        // Start parallel requests for user and posts
        return Promise.all(
            [
                usersApi.getUsersByIdPromise(userId),
                postsApi.getPostsForUserPromise(userId)
            ]
        ).then(([user, posts]) => { // Note the destructuring
            // We have both user and posts, let's add the posts to the user
            user.posts = posts;
    
            // Send parallel queries for all the post comments, by
            // using `map` to get an array of promises for each post's
            // comments
            return Promise.all(user.posts.map(post => 
                commentsApi.getCommentsForPostPromise(post.id)
                    .then(comments => {                   
                        // Have the comments for this post, remember them
                        post.comments = comments;
                    })
                ))
                // When all of those comments requests are done, set the
                // user as the final resolution value in the chain
                .then(_ => user);
        });
    };
    

    Example:

    // Mocks
    var commentId = 0;
    var usersApi = {
        getUsersByIdPromise(userId) {
            return new Promise(resolve => {
                setTimeout(_ => resolve({id: userId, name: "Some User"}), 100);
            });
        }
    };
    var postsApi = {
        getPostsForUserPromise(userId) {
            return new Promise(resolve => {
                setTimeout(_ => resolve([
                    {userId: userId, id: 1, title: "Post 1"},
                    {userId: userId, id: 2, title: "Post 2"}
                ]), 100);
            });
        }
    };
    var commentsApi = {
        getCommentsForPostPromise(postId) {
            return new Promise(resolve => {
                setTimeout(_ => resolve([
                    {postId: postId, id: ++commentId, title: "First comment on post id = " + postId},
                    {postId: postId, id: ++commentId, title: "Second comment on post id = " + postId},
                    {postId: postId, id: ++commentId, title: "Third comment on post id = " + postId}
                ]), 100);
            });
        }
    };
    // Code
    var xyz = function (userId) {
        // Start parallel requests for user and posts
        return Promise.all(
            [
                usersApi.getUsersByIdPromise(userId),
                postsApi.getPostsForUserPromise(userId)
            ]
        ).then(([user, posts]) => { // Note the destructuring
            // We have both user and posts, let's add the posts to the user
            user.posts = posts;
    
            // Send parallel queries for all the post comments, by
            // using `map` to get an array of promises for each post's
            // comments
            return Promise.all(user.posts.map(post => 
                commentsApi.getCommentsForPostPromise(post.id)
                    .then(comments => {                   
                        // Have the comments for this post, remember them
                        post.comments = comments;
                    })
                ))
                // When all of those comments requests are done, set the
                // user as the final resolution value in the chain
                .then(_ => user);
        });
    };
    // Using it
    xyz().then(user => {
        console.log(JSON.stringify(user, null, 2));
    });
    .as-console-wrapper {
      max-height: 100% !important;
    }

    Although actually, we could start requesting the comments for the posts as soon as we have the posts, without waiting for the user until later:

    var xyz = function (userId) {
        return Promise.all(
            [
                usersApi.getUsersByIdPromise(userId),
                postsApi.getPostsForUserPromise(userId).then(posts =>
                    // We have the posts, start parallel requests for their comments
                    Promise.all(posts.map(post => 
                        commentsApi.getCommentsForPostPromise(post.id)
                            .then(comments => {
                                // Have the post's comments, remember them
                                post.comments = comments;
                            })
                    ))
                    // We have all the comments, resolve with posts
                    .then(_ => posts)
                )
            ]
        ).then(([user, posts]) => { // Note the destructuring
            // We have both user and posts (with their filled-in comments)
            // Remember the posts on the user, and return the user as the final
            // resolution value in the chain
            user.posts = posts;
            return user;
        });
    };
    

    Example:

    // Mocks
    var commentId = 0;
    var usersApi = {
        getUsersByIdPromise(userId) {
            return new Promise(resolve => {
                setTimeout(_ => resolve({id: userId, name: "Some User"}), 100);
            });
        }
    };
    var postsApi = {
        getPostsForUserPromise(userId) {
            return new Promise(resolve => {
                setTimeout(_ => resolve([
                    {userId: userId, id: 1, title: "Post 1"},
                    {userId: userId, id: 2, title: "Post 2"}
                ]), 100);
            });
        }
    };
    var commentsApi = {
        getCommentsForPostPromise(postId) {
            return new Promise(resolve => {
                setTimeout(_ => resolve([
                    {postId: postId, id: ++commentId, title: "First comment on post id = " + postId},
                    {postId: postId, id: ++commentId, title: "Second comment on post id = " + postId},
                    {postId: postId, id: ++commentId, title: "Third comment on post id = " + postId}
                ]), 100);
            });
        }
    };
    // Code
    var xyz = function (userId) {
        return Promise.all(
            [
                usersApi.getUsersByIdPromise(userId),
                postsApi.getPostsForUserPromise(userId).then(posts =>
                    // We have the posts, start parallel requests for their comments
                    Promise.all(posts.map(post => 
                        commentsApi.getCommentsForPostPromise(post.id)
                            .then(comments => {
                                // Have the post's comments, remember them
                                post.comments = comments;
                            })
                    ))
                    // We have all the comments, resolve with posts
                    .then(_ => posts)
                )
            ]
        ).then(([user, posts]) => { // Note the destructuring
            // We have both user and posts (with their filled-in comments)
            // Remember the posts on the user, and return the user as the final
            // resolution value in the chain
            user.posts = posts;
            return user;
        });
    };
    // Using it
    xyz().then(user => {
        console.log(JSON.stringify(user, null, 2));
    });
    .as-console-wrapper {
      max-height: 100% !important;
    }