Search code examples
javascriptnode.jsexcelbluebirdpg-promise

nodejs - Help "promisifying" a file read with nested promises


So I've recently delved into trying to understand promises and the purpose behind them due to javascripts asynchronous behavior. While I "think" I understand, I still struggle with how to promisify something to return the future value, then execute a new block of code to do something else. Two main node modules I'm using:

  • pg-promise
  • exceljs

What I'd like to do is read a file, then once fully read, iterate of each worksheet executing DB commands. Then once all worksheets are processed, go back and delete the original file I read. Here is the code I have. I have it working to the point everything writes into the database just fine, even when there are multiple worksheets. What I don't have working is setting it up to identify when all the worksheets have been fully processed, then to go remove the file

workbook.csv.readFile(fileName)
            .then(function () {
                // this array I was going to use to somehow populate a true/false array.  
                // Then when done with each sheet, push a true into the array.  
                // When all elements were true could signify all the processing is done... 
                // but have no idea how to utilize this!  
                // So left it in to take up space because wtf...
                var arrWorksheetComplete = [];

                workbook.eachSheet(function (worksheet) {
                    console.log(worksheet.name);
                    db.tx(function (t) {
                        var insertStatements = [];
                        for (var i = 2; i <= worksheet._rows.length; i++) {
                            // here we create a new array from the worksheet, as we need a 0 index based array.
                            // the worksheet values actually begins at element 1.  We will splice to dump the undefined element at index 0.
                            // This will allow the batch promises to work correctly... otherwise everything will be offset by 1
                            var arrValues = Array.from(worksheet.getRow(i).values);
                            arrValues.splice(0, 1);

                            // these queries are upsert.  Inserts will occur first, however if they error on the constraint, an update will occur instead.
                            insertStatements.push(t.one('insert into rq_data' +
                                '(col1, col2, col3) ' +
                                'values($1, $2, $3) ' +
                                'ON CONFLICT ON CONSTRAINT key_constraint DO UPDATE SET ' +
                                '(prodname) = ' +
                                '($3) RETURNING autokey',
                                arrValues));
                        }
                        return t.batch(insertStatements);
                    })
                    .then(function (data) {
                        console.log('Success:', 'Inserted/Updated ' + data.length + ' records');
                    })
                    .catch(function (error) {
                        console.log('ERROR:', error.message || error);
                    });
                });
            });

I would like to be able to say

.then(function(){
    // everything processed!
    removeFile(fileName)
    // this probably also wouldn't work as by now fileName is out of context?
});

But I'm super confused when having a promise inside a promise.. I have the db.tx call which is essentially a promise nested inside the .eachSheet function. Please help a dumb programmer understand! Been beating head against wall for hours on this one. :)


Solution

  • If i understand correctly, you're trying to chain promises.

    I suggest you to read this great article on Promises anti-pattern (see 'The Collection Kerfuffle' section)

    If you need to execute promises in series, this article suggests to use reduce.

    I'll rewrite your snippet to:

    workbook.csv.readFile(fileName).then(function () {
    
      processWorksheets().then(function() {
        // all worksheets processed!
      });
    
    });
    
    function processWorksheets() {
        var worksheets = [];
    
        // first, build an array of worksheet
        workbook.eachSheet(function (worksheet) {
            worksheets.push(worksheet);
        }); 
    
        // then chain promises using Array.reduce
        return worksheets.reduce(function(promise, item) {
            // promise is the the value previously returned in the last invocation of the callback.
            // item is a worksheet
    
            // when the previous promise will be resolved, call saveWorksheet on the next worksheet
            return promise.then(function(result) {
                return saveWorksheet(item, result);
            });        
    
        }, Promise.resolve()); // start chain with a 'fake' promise
    }
    
    // this method returns a promise
    function saveWorksheet(worksheet, result) {
    
        return db.tx(function (t) {
    
          var insertStatements = [];
          for (var i = 2; i <= worksheet._rows.length; i++) {
            // here we create a new array from the worksheet, as we need a 0 index based array.
            // the worksheet values actually begins at element 1.  We will splice to dump the undefined element at index 0.
            // This will allow the batch promises to work correctly... otherwise everything will be offset by 1
            var arrValues = Array.from(worksheet.getRow(i).values);
            arrValues.splice(0, 1);
    
            // these queries are upsert.  Inserts will occur first, however if they error on the constraint, an update will occur instead.
            insertStatements.push(t.one('insert into rq_data' +
                                        '(col1, col2, col3) ' +
                                        'values($1, $2, $3) ' +
                                        'ON CONFLICT ON CONSTRAINT key_constraint DO UPDATE SET ' +
                                        '(prodname) = ' +
                                        '($3) RETURNING autokey',
                                        arrValues));
          }
    
          return t.batch(insertStatements);
        })
        // this two below can be removed...
        .then(function (data) {
           return new Promise((resolve, reject) => { 
              console.log('Success:', 'Inserted/Updated ' + data.length + ' records');
              resolve();
           });
        })
        .catch(function (error) {
            return new Promise((resolve, reject) => {
            console.log('ERROR:', error.message || error);
            reject();
          });
        });
    
    }
    

    Don't forget to include the promise module:

    var Promise = require('promise');
    

    I haven't tested my code, could contains some typo errors.