0

I am working on NodeJS with firebase-admin. I've structured my firebase database like this

user node:

user
|
+--- uid (-27JfjDfBetveYQNfStackOverFlow)
      |
      +-- firstname
      +-- lastname
      +-- mainCompanyId: ''
      +-- ....

userCompanies
|
+--- uid (-27JfjDfBetveYQNfStackOverFlow)
      |
      +--- companyId (-KnStackOverFlowF7DezRD0P) : true
      +--- companyId (-MyStackOverFlowF99ezRD0V) : true
      +--- .......... : true

companies
| 
+--- companyId (-KnStackOverFlowF7DezRD0P)
        |
        +-- name
        +-- createdAt
        +-- updatedAt
        +-- createdBy
        +-- ........

The refs are already defined globally on the server:

  • companiesRef = db.ref('companies')
  • usersRef = db.ref('users')
  • userCompaniesRef = db.ref('userCompanies')

What I try to do is to get all companies which are related to the user. To join the data I've created the node userCompanies in the database and saved the companyId as a key. Once I am retrieving the data, I loop through the keys and get the company by their id. In express I've created a route called /api/companies. If I try to return the result back to the client I got an empty array.

This is my route:

app.get('/api/companies/', function (req, res) {

// getting the current session. The variable 'sess' is stored globally.
sess = req.session;

// get the current user from the session
var user = sess.user;

// get the mainCompanyId from the user
var mainCompanyId = user.companyId;

// prepare object to return it back
var myObj = {
    mainCompany: mainCompanyId, // ignore. Just a property to see users first created company
    companies: [], // this is the important property/array where I want to store all the companies
    someOtherProperties: true, // ignore
    ..... : .....
};

userCompaniesRef            // db.ref('userCompanies')
    .child(user.uid)        // -27JfjDfBetveYQNfStackOverFlow
    .once('value', function (userCompaniesSnap) {

        // get all companies which are related to the user
        var userCompaniesGroupList = userCompaniesSnap;

        // check if user has companies
        if (userCompaniesGroupList !== null) {

            // loop through all companies and get the companyId by Key
            userCompaniesGroupList.forEach(userCompaniesGroupListSnap => {

                var companyId = userCompaniesGroupListSnap.key; // -KnStackOverFlowF7DezRD0P

                companiesRef // db.ref('companies')
                    .child(companyId)
                    .once('value', companySnap => {

                        // get the current company
                        var company = companySnap.val();

                        // push it to the prepared object
                        myObj.companies.push(company);

                    }); // companiesRef.once('value)

            }); // userCompaniesGroupList.forEach

        } // if userCompaniesGroupList !== null

    }); // query userCompaniesRef

    res.status(200).send(myObj);
});

But after res.send I get this result:

Empty Array

I don't know what here is the problem. The push is working fine. Once the promise is done, myObj.companies has an empty array. How can a handle this nested queries and return all data in one array ?

---UPDATE---

I've tried with promises. Still the same, I get an empty array back. Here is the code:

// prepare object to return it back
var myObj = {
    mainCompany: mainCompanyId,
    companies: []
};

var getCompanies = function () {
    return new Promise(function (resolve, reject) {

        userCompaniesRef // db.ref('userCompanies')
            .child(user.uid) // -27JfjDfBetveYQNfStackOverFlow
            .once('value', function (userCompaniesSnap) {

                if (userCompaniesSnap.val() !== null)
                    resolve(userCompaniesSnap);

            });
    });
}

var getCompaniesByList = function (companyList) {
    var companyListArray = [];
    return new Promise(function (resolve, reject) {

        // loop through all companies and get the companyId by Key
        companyList.forEach(userCompaniesGroupListSnap => {

            var companyId = userCompaniesGroupListSnap.key; // -KnStackOverFlowF7DezRD0P

            companiesRef // db.ref('companies')
                .child(companyId)
                .once('value', companySnap => {

                    // get the current company
                    var company = companySnap.val();

                    // push it to the prepared object
                    companyListArray.push(company);

                  //  updatedObjectFinal.push(myObj);

                }); // companiesRef.once('value)

        }); // userCompaniesGroupList.forEach
        resolve(companyListArray);
    });
}

getCompanies().then(function (compList) {
    console.log('compList:', compList.val());
    return getCompaniesByList(compList); // compList has the data. 
}).then(function (endResult) {

    console.log('endResult: ', endResult); // endResult is empty again..

    res.status(200).send(endResult);
});

In this case I've tried to get all companies by user id. Then I tried to pass this list to the next promise to get each company by their ID, push the company to an array and return this array back on the end. But is still empty..

UPDATE 3: PROBLEM SOLVED

app.get('/api/companies/', function (req, res) {

// getting the current session
sess = req.session;

// save the user
var user = sess.user;
var userId = user.uid;

var getCompanies = function () {
    return new Promise(function (resolve, reject) {

        userCompaniesRef // db.ref('userCompanies')
            .child(userId) // -27JfjDfBetveYQNfStackOverFlow
            .once('value', function (userCompaniesSnap) {

                // prepare an array
                var companies = [];

                if (userCompaniesSnap.val() !== null) {

                    // loop through the keys and save the ids
                    userCompaniesSnap.forEach(function (companyItem) {
                        // console.log('companyIte,', companyItem.key);
                        companies.push(companyItem.key);
                    });

                    // get the latest item of the array to get the latest key
                    var latestCompanyId = companies[companies.length - 1]

                    // prepare optional object to resolve
                    var finalCompaniesList = {
                        companies: companies,
                        lastCompanyId: latestCompanyId
                    }

                    resolve(finalCompaniesList);

                }

            });
    });
}

var getCompaniesByList = function (companyArray) {

    var companyListArray = [];
    return new Promise(function (resolve, reject) {

        // loop through all companies and get the companyId by Key
        companyArray.companies.forEach(userCompaniesGroupList => {

            var companyId = userCompaniesGroupList; // -KnStackOverFlowF7DezRD0P

            var companyTest = companiesRef // db.ref('companies')
                .child(companyId)
                .once('value', companySnap => {

                    // get the current company
                    var company = companySnap.val();

                    // push it to the prepared object
                    companyListArray.push(company);

                    if (company.id === companyArray.lastCompanyId)
                        resolve(companyListArray); // I am resolving here now the data. 

                }); // companiesRef.once('value)

        }); // userCompaniesGroupList.forEach

    });
}

getCompanies().then(function (compList) {
    return getCompaniesByList(compList);
}).then(function (endResult) {
    res.status(200).send(endResult);
});

});

Big thanks to Frank! I've found a solution for my problem. What I have done now is, in getCompanies I've run a forEach to pre-fill an array with the ids and to get the latest companyId in the array. Once I got the latest id, I've created a custom object and saved the latest id in latestCompanyId and returned the array back. So I know now the latest id and I was able to run the resolve method inside the foreach in the snap promise.

Harry
  • 489
  • 1
  • 5
  • 13
  • A first guess is below. If that does not help you solve the problem, can you reduce the code and text to the [minimum that is needed to reproduce the problem](http://stackoverflow.com/help/mcve)? – Frank van Puffelen Jul 14 '17 at 14:15

1 Answers1

1

As far as I can quickly see you're falling for async programming 101: data is loaded from Firebase asynchronously. When you write your result, the data hasn't loaded yet.

To solve this, move the writing of the response into the callback that fires when all data is available:

userCompaniesRef            // db.ref('userCompanies')
    .child(user.uid)        // -27JfjDfBetveYQNfStackOverFlow
    .once('value', function (userCompaniesSnap) {
        // get all companies which are related to the user
        var userCompaniesGroupList = userCompaniesSnap;

        // check if user has companies
        if (userCompaniesGroupList !== null) {
            // loop through all companies and get the companyId by Key
            userCompaniesGroupList.forEach(userCompaniesGroupListSnap => {

                var companyId = userCompaniesGroupListSnap.key; // -KnStackOverFlowF7DezRD0P

                companiesRef // db.ref('companies')
                    .child(companyId)
                    .once('value', companySnap => {
                        // get the current company
                        var company = companySnap.val();

                        // push it to the prepared object
                        myObj.companies.push(company);

                        // send response to client
                        res.status(200).send(myObj);

                    }); // companiesRef.once('value)
            }); // userCompaniesGroupList.forEach
        } // if userCompaniesGroupList !== null
    }); // query userCompaniesRef
});
Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
  • Thanks Frank! I am verry happy that you've take a look about my question. I've learned a lot based on you answers in other topics about firebase. But the problem is that I cant return all companies. Like in your example above, I get an Error: "Can't set headers after they are sent." because I am in the forEach loop. What I want is to get all companies, push to the myObj.companies and return this object back. I've updated my question, I tried with promises. But I don't even get a result. I get the companies list but the array in myObj.companies is still empty.. Can u give me an example? – Harry Jul 14 '17 at 17:04
  • Ah... that's a different problem: calling `send()` closes the response. You should use a `write()` to write: https://stackoverflow.com/questions/21749590/difference-between-response-send-and-response-write-in-node-js – Frank van Puffelen Jul 14 '17 at 17:21
  • As I understood, I should write/push/save the companies in the foreach directly in the response instead to populate an array and return this back with res.send(myArray) ? – Harry Jul 14 '17 at 17:38
  • Yeah, there are a few ways to do it. Keeping the response open is one. Another one is using `Promise.all()` to first gather all those results and only then send the response. I recommend learning a bit more about Promises in [this documentation](https://firebase.google.com/docs/functions/terminate-functions), [this video](https://www.youtube.com/watch?v=NgZIb6Uwpjc), and [this blog post](https://firebase.googleblog.com/2017/06/keep-your-promises-when-using-cloud.html). – Frank van Puffelen Jul 14 '17 at 17:53
  • Okay, thanks! As you can see in my question I've updated it. Please scroll down until you see the title "UPDATE". Should that not work as well ? I think the problem is because I can't return the forEach as a promise back as you see in the code. getCompanies() works pretty well because I've only one firebase promise. But If I pass then the data to getCompaniesByList(), everything works but I can't resolve the companyListArray. I've returned the resolve in the snap promise but that is not working. – Harry Jul 14 '17 at 17:59
  • The `getCompaniesByList` in your latest sample calls `resolve` before any of its `once()` callbacks have fired. You need `Promise.all()` here **or** the call to resolve must come from within the last `once()` finishes. To debug this and learn more, put some `console.log()` statements in there (or breakpoints). You'll see that you `resolve()` before any `once()` has completed. – Frank van Puffelen Jul 15 '17 at 03:34
  • Big thanks Frank! I spend my time over the weekend to learn more about promises. I've found a way how I can solve my problem. Can you please take a look about my updated question (UPDATE: PROBLEM SOLVED)? Is this a good solution for that? It would be great to know how to handle promises when a forEach loop is running. This was the only tricky thing. Can you maybe make an example to this topic how to handle **multiple promises** inside a forEach loop? That would be awesome! Thanks for help! – Harry Jul 16 '17 at 10:03
  • Pretty much any answer using `Promise.all()` is likely an example of that. See https://stackoverflow.com/questions/42610264/querying-by-multiple-keys-in-firebase/42610381#42610381, https://stackoverflow.com/questions/43484811/firebase-multipath-fetch-data/43485344#43485344, https://stackoverflow.com/questions/44220215/firebase-real-time-database-structure-for-file-upload/44228850#44228850 – Frank van Puffelen Jul 16 '17 at 13:37
  • Thank you! Thats all what I need. Great examples! – Harry Jul 16 '17 at 20:34