Home > Mobile >  Query a DynamoDB table while passing a parameter nested within a forEach() method of an array
Query a DynamoDB table while passing a parameter nested within a forEach() method of an array

Time:12-09

I'm scanning all items from a DynamoDB table - within a Lambda function - with DocumentClient. I'm then looping through each item and extracting the payload that I need. I'll use that item from the payload as a parameter with ExpressionAttributeValues in a new query. Everything works dandy independently. The issue is with the use of the asynchronous function queryItems when nested within an array forEach() method. I getting a parsing error with the function queryItems. I can query the table when I call the function outside of the loop but how else am I going to query each item independently? I'm not sure how to handle this.

'use strict';

const aws = require('aws-sdk');
const docClient = new aws.DynamoDB.DocumentClient();

var paramsAll = {
  TableName: 'MyTable',
  Select: "ALL_ATTRIBUTES"
};

exports.handler = async (event, context) => {
  try {
    let arr = [];
    let sequence = '';
    //scan all items in table
    docClient.scan(paramsAll, function(err, data) {
      if (err) {
        //handle error
      }
      else {
        //Loop through each item in the table:
        let items = (data.Items);
        items.forEach(function(Item) {
          let p = (Item.payload);
          //Extract sequence from the payload
          sequence = (p.seq);
          arr.push(sequence);
          //perform other function with this array (not listed for brevity)
        });
        //Here is where I'm having the issue:
        arr.forEach(function(Item) {
          //Pass these items as a paramater within queryItems function but getting Parsing Error: unexpected token queryItems
          const results = await queryItems(Item);
          //do something with the results...
        })
      }
    });
  }
  catch (err) {
    return { error: err };
  }
};

async function queryItems(p) {
  try {
    var params = {
      TableName: 'MyTable',
      KeyConditionExpression: '#seq = :value',
      ExpressionAttributeValues: { ':value': p },
      ExpressionAttributeNames: { '#seq': 'seq' }
    };
    const data = await docClient.query(params).promise();
    return data;
  }
  catch (err) {
    return err;
  }
}

CodePudding user response:

I've definitely run into a similar issue. What I believe is happening is just a Javascript syntax issue, where awaiting queryItems inside the synchronous function provided to forEach will produce an error. (Although, when running the code, I do get the specific error "SyntaxError: await is only valid in async functions and the top level bodies of modules", so there might be something else going on.)

I see nothing wrong with the DynamoDB queries, but hoangdv's suggestions are spot on. Specifically, I'd also suggest using the promise style for scan, and while a for...loop will definitely work, using Promise.all and map will be a lot quicker to complete all the queries. Here's how I'd modify the code:

'use strict';

const aws = require('aws-sdk');
const docClient = new aws.DynamoDB.DocumentClient();

// avoid var unless you specifically require it's hoisting behavior.
const paramsAll = {
  TableName: 'MyTable',
  Select: "ALL_ATTRIBUTES" // most likely not needed, I'd review this section of the docs: https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Scan.html#DDB-Scan-request-Select
};

exports.handler = async (event, context) => {
  try {
    // unless you need to assign a new array to this variable, it is better practice to use const instead.
    const arr = [];
    // let sequence = ''; // see line 24 for why I commented this out.

    // scan all items in table.
    // Destructure Items out of the response.
    // You may also need to continue scanning with the LastEvaluatedKey depending on the size of your table, and/or your use case.
    // You'd continue scanning in a while loop, for example.
    const { Items, LastEvaluatedKey } = await docClient.scan(paramsAll).promise();

    // push the sequence to the arr. 
    // There is most likely a reason you omitted for brevity to have sequence defined above,
    // but since this example doesn't need it above, I've omitted it entirely
    Items.forEach(Item => {
      const p = Item.payload;
      arr.push(p.seq);
    });

    // use a for loop or map here instead. forEach will return undefined, which cannot be await'ed.
    // instead, map will return a new array of Promises (since the callback is async).
    // Then, you can use Promise.all to await until each Promise in the array is resolved.
    // Keep in mind, depending on how many items you are iterating through, you may run into DynamoDB's ThrottlingException.
    //    You would have to batch the queries (in other words, split the arr into pieces, and iterate over each piece), which would have to be done before using map. Then, sleep for a few milliseconds before starting on the next piece.
    //    I doubt the queries will be quick enough to cause this when using a for loop, though.
    await Promise.all(arr.map(async Item => {
      const results = await queryItems(Item);
      // do something with the results...
    }));
  }
  catch (err) {
    // Again, not sure what the use case is, but just FYI this is not a valid return value if this lambda function is intended for use with using API Gateway.
    // See here :) https://docs.aws.amazon.com/lambda/latest/dg/services-apigateway.html#apigateway-types-transforms
    return { error: err };
  }
};

// Presumably, MyTable has a partitionKey of seq, otherwise this KeyConditionExpression is invalid.
async function queryItems(p) {
  try {
    var params = {
      TableName: 'MyTable',
      KeyConditionExpression: '#seq = :value',
      ExpressionAttributeValues: { ':value': p },
      ExpressionAttributeNames: { '#seq': 'seq' }
    };
    const data = await docClient.query(params).promise();
    return data;
  }
  catch (err) {
    return err;
  }
}

CodePudding user response:

Your issue is how you await on the for loop, its best to use Promise.all() with a map to await inside of a loop:

    await Promise.all(arr.map(async Item => {
      const results = await queryItems(Item);
      // do something with the results...
    }));

However, I cannot seem to understand your logic really well.

  1. You Scan a table called MyTable, but you do not paginate, meaning you are only getting up to 1MB worth of data.
  2. With the results, you strip out the seq value and then once again read every item from MyTable this time using a Query and seq as the key?
  • Related