0

I was having some problem with multi-level of promises. What I tried to do is first get list of receipt items under certain category, then for each receipt item, I get its detail & receipt ID, after I get the receipt ID, I search for the account ID. Then, I get the account details based on account ID. Here is my code:

var query =  // get receipt items under certain category
    var outerPromise = query.once('value').then(data => {
        var promises = [];
        var datasetarr = [];
        
        data.forEach(snapshot => {
            var itemData = // get receipt item unique push ID
            
            var query = // get details of receipt items 
            var promise = query.once('value').then(data => { 
                var itemDetail = // get receipt item detail
                
                if(type == subtype){    
                    var receiptID = itemDetail.receiptID;

                    var query = // query receipts table by receiptID
                    return query.once('value').then(data => {   
                        data.forEach(snapshot => {
                            snapshot.forEach(childSnapshot => {
                                if(childSnapshot.key == receiptID){
                                    var accountKey = // get accountID
                                    
                                    var query = // query accounts table
                                    return query.once('value').then(data => {
                                        var accountDetail = data.val();
                                        var age = accountDetail.age;
                                        var gender = accountDetail.gender;
                                        console.log(age + ' ' + gender);
                                        datasetarr.push({age: age, gender: gender});
                                    });
                                }
                            });
                        });
                    }); 
                }
            });
            promises.push(promise);
        }); 

        return Promise.all(promises).then(()=> datasetarr);
});

I managed to print out the result from the console.log above. However, when I tried to print out here which is when the promise is done:

outerPromise.then((arr) => {
        console.log('promise done');
        for(var i = 0; i < arr.length; i++){
            console.log(arr[i].age + ' ' + arr[i].gender);
        }
    }); 

I get nothing here. The console now is showing 'promise done' first before any other results I printed out above.

How can I do this correctly?

mkrieger1
  • 19,194
  • 5
  • 54
  • 65
QWERTY
  • 2,303
  • 9
  • 44
  • 85
  • 1
    the inner `data.forEach` returns `undefined` - so the nested promises inside that are not waited for (and that's just the most obvious problem) – Jaromanda X Aug 06 '17 at 01:12
  • 1
    to be honest, this sort of code would probably be better split into multiple functions - would be easier if `snapshot` and the `data` result of `query.once` had map methods (I know they do not, so don't even try) – Jaromanda X Aug 06 '17 at 01:16
  • If the `if` condition is not met no value is `return`ed from `.then()` see [Why is value undefined at .then() chained to Promise?](https://stackoverflow.com/q/44439596/) – guest271314 Aug 06 '17 at 01:23
  • try `[...snapshot].map` – Jaromanda X Aug 06 '17 at 02:39
  • ...snapshot won't work because snapshot isn't iterable (I've researched some of firebase in the meantime) - check if [this code](https://pastebin.com/DkFUxCR5) actually works (I don't do firebase, so I can't test) – Jaromanda X Aug 06 '17 at 04:27
  • no, the snippet replaces everything in your first code snippet – Jaromanda X Aug 06 '17 at 04:48
  • I changed the return statement to return({age: age, gender: gender}); and when I tried to loop thru the array in .then(), it tells me cannot read property age of undefined. But the console log inside the nested promise is actually printing out the data – QWERTY Aug 06 '17 at 04:54
  • in `outerPromise.then((arr) => {` do a `console.log(JSON.stringify(arr))` - it may be that there's an extra level of array I didn't account for – Jaromanda X Aug 06 '17 at 04:59
  • no, wait, I stuffed something up :p – Jaromanda X Aug 06 '17 at 04:59
  • try this one - https://pastebin.com/h43EPuJf ... FYI: I keep using `return {age, gender};` because that's (valid in later javascript) shorthand for `return {age:age, gender:gender};` – Jaromanda X Aug 06 '17 at 05:04
  • clearly I'm not filtering properly - https://pastebin.com/qwgBZKTq - just line 34 changed from `})));` to `}).filter(result => !!result)));` – Jaromanda X Aug 06 '17 at 05:12
  • @JaromandaX Hmm the result is still the same. I checked multiple times to follow your code entirely, but I am still getting those nulls. :( – QWERTY Aug 06 '17 at 05:19
  • yeah, I see why ... https://pastebin.com/11aRG50W – Jaromanda X Aug 06 '17 at 05:27
  • @JaromandaX Cool that works! Thanks sooooooo much! But do you mind to post it as answer and explain a little bit as I get confused on all the .map .filter stuffs. Also, after I stringify the array as suggested by you right, I tried to access its attribute by 'arr[i].age', it returns me undefined. In this case, how can I actually access them because I intended to perform some sorting based on the attribute then store them into different array in the later part! – QWERTY Aug 06 '17 at 05:34
  • Yeah, I'll post the answer - will explain it a bit later, prior engagement – Jaromanda X Aug 06 '17 at 05:35

1 Answers1

3

I will provide a more detailed explanation in a couple of hours, I have a prior engagement which means I can't provide details now

First step to a "easy" solution is to make a function to make an array out of a firebase snapshot, so we can use map/concat/filter etc

const snapshotToArray = snapshot => {
    const ret = [];
    snapshot.forEach(childSnapshot => {
        ret.push(childSnapshot);
    });
    return ret;
};

Now, the code can be written as follows

// get list of receipt items under category
var query // = // get receipt items under certain category
var outerPromise = query.once('value').then(data => {
    return Promise.all(snapshotToArray(data).map(snapshot => {
        var itemData // = // get receipt item unique push ID
        var query // = // get details of receipt items 
        return query.once('value').then(data => { 
            var itemDetail // = // get receipt item detail
            if(type == subtype){    
                var receiptID = itemDetail.receiptID;
                var query //= // query receipts table by receiptID
                return query.once('value').then(data => {   
                    return Promise.all([].concat(...snapshotToArray(data).map(snapshot => {
                        return snapshotToArray(snapshot).map(childSnapshot => {
                            if(childSnapshot.key == receiptID){
                                var accountKey //= // get accountID
                                var query //= // query accounts table
                                return query.once('value').then(data => {
                                    var accountDetail = data.val();
                                    var age = accountDetail.age;
                                    var gender = accountDetail.gender;
                                    console.log(age + ' ' + gender);
                                    return({age, gender});
                                });
                            }
                        }).filter(result => !!result);
                    }).filter(result => !!result)));
                }); 
            }
        });
    })).then([].concat(...results => results.filter(result => !!result))); 
});

To explain questions in the comments

[].concat used to add the content of multiple arrays to a new array, i.e

[].concat([1,2,3],[4,5,6]) => [1,2,3,4,5,6]

...snapshotToArray(data).map(etc

... is the spread operator, used as an argument to a function, it takes the iterable and "spreads" it to multiple arguments

console.log(...[1,2,3]) == console.log(1,2,3)

In this case snapshotToArray(data).map returns an array of arrays, to give a console log example

console.log(...[[1,2],[3,4]]) == console.log([1,2], [3,4])

adding the concat

[].concat(...[[1,2],[3,4]]) == [].concat([1,2],[3,4]) == [1,2,3,4]

so it flattens a two level array to a single level, i.e.

console.log(...[[1,2],[3,4]]) == console.log(1,2,3,4)

So in summary, what that code fragment does is flatten a two level array

filter(result => !!result)

simply "filters" out any array elements that are "falsey". As you have this condition

if(childSnapshot.key == receiptID){

if that is false, the result will be undefined for that map - all other results will be an array, and even empty arrays are truthy - that's why the filtering is done so often! There's probably a better way to do all that, but unless you're dealing with literally millions of items, there's no real issue with filtering empty results like this

End result is a flat array with only the Promises returned from the code within

Jaromanda X
  • 53,868
  • 5
  • 73
  • 87
  • Hey may I know what does '[].concat(...snapshotToArray(data)' and 'filter(result => !!result)' does? – QWERTY Aug 08 '17 at 14:32
  • Most of the code is variations on those concepts too :p – Jaromanda X Aug 09 '17 at 00:58
  • I added the edited portion, and the error message that I have encountered is cannot read property 'age' of undefined. Do you have any ideas how to fix it? – QWERTY Aug 10 '17 at 05:06
  • which line of code is trying to access property age, if it's `var age = accountDetail.age;` then the issue is with the data not being what you think it is, though I don't know why your if statement would cause that - and why did [guest176969](https://stackoverflow.com/users/7691120/guest176969) just ask about your code? – Jaromanda X Aug 10 '17 at 05:11
  • The part where I trying to access them in the .then(). My current data set got 3 records, two for this year and one for last year. If I removed the IF statement, it simply retrieve all and works fine. However, after I added the IF statement, when I tried to access the array returned after resolved, it just tell me the error message above. I guess it is something to do with the mappings. The error message is not pointing to where I fetch the data, but rather, the part where I tried to access in the .then() – QWERTY Aug 10 '17 at 05:22
  • looks like the filtering of "undefined" isn't working all of a sudden, not sure why that would be - interestingly the question asked by guest176969 showed this if statement in the context of your original code - maybe you've misplaced a `}` or something - can you put your current code in a jsfiddle or pastebin or somewhere – Jaromanda X Aug 10 '17 at 05:25
  • if you just console.log the `JSON.stringify(data)` like before, do you see a bunch of nulls again? – Jaromanda X Aug 10 '17 at 05:32
  • 1
    ahhh, empty arrays - didn't think of that - seems like another level of flattening needs to be done too - which would remove those empty arrays – Jaromanda X Aug 10 '17 at 05:35
  • 1
    try changing `})).then(results => results.filter(result => !!result));` to `})).then(results => [].concat(...results.filter(result => !!result)));` - note, now the result is a single level array, whereas it was arrays (of one or no elements) within an array - I've amended to answer accordinlgy – Jaromanda X Aug 10 '17 at 05:39