-1

I'm using the below code as a lambda function executed on an API call.

The issue I'm having is when the response and callback are in their current position the array scans returns [] even though a scan object is populated and pushed to the array. However when response and callback are placed in ***PLACEHOLDER*** section, the array returns populated with the scan object.

I understand this has something to do with the asynchronous nature of the code as I have looked at many similar questions on stack but alongside the AWS-SDK code I can't figure out what to do to correct it.

const AWS = require('aws-sdk');
const ddb = new AWS.DynamoDB.DocumentClient();
const iot = new AWS.Iot;

exports.handler = (event, context, callback) => {   
    iot.listThings(null, function(err, data) {    
        var scans = [];    
        if (err) {
            callback(err, null);
        }
        else {   
            for (var i = 0; i < data.things.length; i++) {
                var device = data.things[i].attributes;
                const params = {
                  // redacted 
                };    
                ddb.query(params, function(err, data) {
                    if (err) {
                        callback(err, null);
                    }
                    else {
                        var scan = {
                            "area": device.area,
                            "count": data["Count"]
                        };
                        scans.push(scan);
                        // ***PLACEHOLDER***
                    }
                });
            }
        }
        var response = {
            "statusCode": 200,
            "headers": {},
            "body": JSON.stringify(scans),
            "isBase64Encoded": false
        };
        callback(null, response);
    });
};
  • 2
    Possible duplicate of [How do I return the response from an asynchronous call?](https://stackoverflow.com/questions/14220321/how-do-i-return-the-response-from-an-asynchronous-call) (Retracted my close vote because I think this will help but I'm not sure it fully covers the question at-hand) – Tyler Roper Sep 24 '19 at 18:34
  • Basically, scans is defined inside `iot.listThings` and isn't accessible outside of `iot.listThings`. Move `var scans = []` above `exports.handler` or pass it in via the context parameter and that should resolve the issue. – Adam H Sep 24 '19 at 18:41

1 Answers1

1

this is an async issue, not Lambda. Your code should be like the below:

const AWS = require('aws-sdk');
const ddb = new AWS.DynamoDB.DocumentClient();
const iot = new AWS.Iot;

exports.handler = (event, context, callback) => {   
    iot.listThings(null, function(err, data) {    
        var scans = [];    
        if (err) {
            callback(err, null);
        }
        else {   
            populateScans(data).then(res => {

                callback(null,{
                    "statusCode": 200,
                    "headers": {},
                    "body": JSON.stringify(res),
                    "isBase64Encoded": false
                })
            }).catch(callback)
        }
    });
};

function populateScans(data) {
    return Promise.all(data.things.map(thing => {
        let device = thing.attributes
        const params = {}
        return ddb.query(params).promise().then(res => {
            return {
                area: device.area,
                count: res["Count"]
            }

        })
    }))
}
LostJon
  • 2,287
  • 11
  • 20
  • Nope, his issue is that scans is defined inside his loop and isn't accessible outside of `iot.listThings`. Move `var scans = []` above `exports.handler` or pass it in via the context parameter and that should resolve his issue. – Adam H Sep 24 '19 at 18:40
  • @AdamH disagree...scans is populated in a subsequent callback, and hence lambda callback needs to be called within such. – LostJon Sep 24 '19 at 18:41
  • @LostJon cannot place the callback & response there as it's inside a for loop. Only want one response from the Lambda function containing the full scans array after each for loop iteration. –  Sep 24 '19 at 18:42
  • `scans` is populated in the `ddb.query` callback which is hoisted to `var scans` defined right after `iot.listThings` which isn't accessible outside the scope of the code displayed. It's a scope issue 100% – Adam H Sep 24 '19 at 18:42
  • 2
    (facepalm) I see the building of response now - I am a fool. Leaving my old comments so you don't look like a crazy person fighting with nobody – Adam H Sep 24 '19 at 18:44
  • 1
    callback hell is hurting my brain. in Promise land or async/await, id have this for you by now – LostJon Sep 24 '19 at 18:48
  • changed edit to promisify...code is untested, beware! – LostJon Sep 24 '19 at 19:01
  • @LostJon thanks for your help. One bolt on question, if it's simple.. Your answer returns an array, for example: `[{\"area\":\"area1\",\"count\":1},{\"area\":\"area1\",\"count\":2}]` is there an easy way to make this return `[{\"area\":\"area1\",\"count\":3}]`? –  Sep 25 '19 at 11:27
  • 1
    @Giles, not sure where count = 3 came from. I see 2 object in the array – LostJon Sep 25 '19 at 11:50
  • @LostJon When the area name already exists in the array add to the count. In the example I give above area1 is already placed in the array from the first iteration and has a count of 1, when the next object is pushed into the array for example`{\"area\":\"area1\",\"count\":2}` and since the area names match the existing area1 count should increase by 2. –  Sep 25 '19 at 12:04
  • @Giles still not 100% on that, count is populated by a response object from a DDB query. to do a population of data outside of that seems like a path to a bad place. you can use something like this to reduce it, though: `let reduced = scans.reduce((acc,scan) => { return { ...acc, ...scan } })` – LostJon Sep 25 '19 at 12:26