1

I am having an issue with using async/await using node v8.1. It appears my issue is that I am not returning a promise from my async functions. This is causing the flow of the program to run out of order. I thought by making a function async that the function automatically returns a promise, but that is not the case I am running into.

I expect the program below to output:

Validating xlsx file...

(text from validateParsedXlsx)

Adding users to cognito...

(text from addUsersToCognito)

Adding users to dynamodb...

(text from addUsersToDynamodb)

Instead, I get:

Validating xlsx file...

Adding users to cognito...

Adding users to dynamodb...

(text from validateParsedXlsx) 

(text from addUsersToCognito)  

(text from addUsersToDynamodb)

The issue seems to be pretty obvious, that validateParsedXlsx() addUsersToCognito() and addUsersToDynamodb() are not returning promises. Again, I thought that by using the async keyword, the function automatically took care of this.

Thanks for the help.

Here is my script:

const xlsx = require('xlsx');
const AWS = require('aws-sdk');

AWS.config.update({region: 'us-west-2'});
const documentClient = new AWS.DynamoDB.DocumentClient({convertEmptyValues: true});

async function main(){

    if (!process.argv[2]) {
        console.log('\nAbsolute filepath missing. Pass the absolute filepath in as command line argument.\n')
        process.exit(1);
    }

    const xlsxFilePath = process.argv[2];
    let parsedXlsx = [];
    try {
        parsedXlsx = parseXlsx(xlsxFilePath);
    } catch (error) {
        if(error.code === 'ENOENT') {
            console.log(`\nThe file path: ${process.argv[2]} cannot be resolved\n`)
        } else {
            console.log(error);
        }
    }

    console.log('\n\nValidating xlsx file...\n');
    await validateParsedXlsx(parsedXlsx);
    console.log('\n\nAdding users to cognito...\n');
    await addUsersToCognito(parsedXlsx);
    console.log('\n\nAdding users to dynamodb...\n');
    await addUsersToDynamodb(parsedXlsx);

}

function parseXlsx(filePath) {

    const workbook = xlsx.readFile(filePath);
    const sheetNameList = workbook.SheetNames;

    const parsedXlsxSheets = sheetNameList.map(function (y) {
        const worksheet = workbook.Sheets[y];
        const headers = {};
        const data = [];

        for (z in worksheet) {
            if(z[0] === '!') continue;
            //parse out the column, row, and value
            const col = z.substring(0,1);
            const row = parseInt(z.substring(1));
            const value = worksheet[z].v;

            //store header names
            if(row == 1) {
                headers[col] = value;
                continue;
            }

            if(!data[row]) data[row] = {};
            data[row][headers[col]] = value;
        }
        //drop those first two rows which are empty
        data.shift();
        data.shift();
        return data;
    });

    return parsedXlsxSheets[0]
}

async function validateParsedXlsx(users) {

    let error = false;
    users.forEach(async (user, index) => {
        if (!user.email) {
            console.log(`User at row ${index + 2} doesn't have 'email' entry in xlsx file.`);
            error = true;
        }
        if (!user.displayName) {
            console.log(`User at row ${index + 2} doesn't have 'displayName' entry in xlsx file.`);
            error = true;  
        }
        if (!user.serviceProviderId) {
            console.log(`Userat row ${index + 2} doesn't have 'displayName' entry in xlsx file.`);
            error = true;
        } else {
            const params = {
                TableName: 'service-providers',
                Key: {
                    serviceProviderId: user.serviceProviderId
                }
            }

            const response = await documentClient.get(params).promise();
            if (!response.Item) {
                console.log(`User at row ${index +2} does not have a valid serviceProviderId.`);
                error = true;
            } else {
                console.log(`User ${user.email} is valid, assigned to service provider: ${response.Item.displayName}`);
            }
        }

        if (error) {
            console.log(`Every user in xlsx file must have these attributes, spelled correctly: email, displayName, and serviceProviderId\n\nIn addition, make sure the serviceProviderId is correct by checking the service-providers dynanomdb table.`);
            process.exit(1);
        }
    });

}

async function addUsersToCognito(users) {

    const cognitoIdentityServiceProvider = new AWS.CognitoIdentityServiceProvider();

    const results = await cognitoIdentityServiceProvider.listUserPools({MaxResults: 10}).promise();

    let serviceProviderUserPoolId = '';

    results.UserPools.forEach((userPool) => {
        if(userPool.Name === 'service-provider-users') {
            serviceProviderUserPoolId = userPool.Id;
        }
    });

    users.forEach(async (user) => {

        const params = {
            UserPoolId: serviceProviderUserPoolId,
            Username: user.email,
            DesiredDeliveryMediums: ['EMAIL'],
            TemporaryPassword: 'New_User1',
            UserAttributes: [
                {
                    Name: 'email',
                    Value: user.email
                },
                {
                    Name: 'custom:service_provider_id',
                    Value: user.serviceProviderId
                }
            ]
        }

        try {
            await cognitoIdentityServiceProvider.adminCreateUser(params).promise();
            console.log(`Added user ${user.email} to cognito user pool`);
        } catch (error) {
            if (error.code === 'UsernameExistsException') {
                console.log(`Username: ${user.email} already exists. No action taken.`);
            }
            else {
                console.log(error);
            }
        }
    });

}

async function addUsersToDynamodb(users) {

    users.forEach(async (user) => {
        const params = {
            TableName: 'service-provider-users',
            Item: {
                serviceProviderId: user.serviceProviderId,
                userId: user.email,
                displayName: user.displayName,
                isActive: false,
                role: 'BASIC'
            },
            ConditionExpression: 'attribute_not_exists(userId)'
        }

        try {
            await documentClient.put(params).promise();
            console.log(`Added user ${user.email} to dynamodb user table`);
        } catch (error) {
            if (error.code === 'ConditionalCheckFailedException') {
                console.log(`User ${user.email} already in the dynamodb table service-provider-users`);
            } else {
                console.log(error);
            }
        }
    });

}

main();
Connor
  • 269
  • 1
  • 4
  • 13

1 Answers1

3
  users.forEach(async (user, index) => {

That starts a few promising actions but never awaits them. May do:

  await Promise.all(users.map(async (user, index) => {

... to execute them in parallel or do this:

  await users.reduce((chain, user, index) => async (user, index) => {
   await chain;
   //...
  }, Promise.resolve());

To execute them one after another.


PS: Using process.exit should be the very last option to end your program

Jonas Wilms
  • 132,000
  • 20
  • 149
  • 151
  • To execute in series, `for (const iter or list} { await whatever; }` just looks a bit nicer than using reduce.. :) – Keith Mar 23 '18 at 16:54
  • 1
    @Keith you mean sequentially, right? – Sven Mar 23 '18 at 16:55
  • @Svenskunganka Indeed, I did.. :), just copied pasted in my head what Jonas said.. – Keith Mar 23 '18 at 16:55
  • @Keith Works perfectly, I've never heard of the for...of loop before, thanks for showing! – Connor Mar 23 '18 at 17:07
  • @Connor Oop, just noticed I did a miss-type, `iter or list` instead of `iter of list`.. But glad you saw past my typo.. :) Bit more reference -> https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of – Keith Mar 23 '18 at 17:10
  • Also another side effect, bonus of using `for of` is you can still using `break`, something you don't get with reduce. – Keith Mar 23 '18 at 17:15