0

I am building a model class for an order data entity, in an API. At the moment this entity has 2 endpoints, one for retrieving summary data /orders/{id}/summary, and another for payment data /orders/{id}/payment. The summary works 100%, and the payment endpoint gets all data, except where there's a secondary DB query.

The code in my order.js model:

'use strict';

/* Include MySQL connection */
var dbConnection = require(appDir + process.env.PATH_LIB + 'database');

/* Include PaymentMethod model */
var PaymentMethod = require('./paymentmethod');


class Order {
  constructor (data, requestTemplate) {
    this.data = data;
    switch (requestTemplate) {
      case 'summary':
        this.data.platform = _getPlatform(data.TakeALot);
        this.data.source = _getSource(data.idKalahariOrder,data.idAffiliate,data.TakeALot);
        this.data.reference = _getExternalReference(data.idKalahariOrder,data.AffiliateReference);
        break;
      case 'payment':
        this.data.health = _getHealth(data.Auth);
        this.data.timeline = _getTimeline(data.Auth);
        break;
    };
  }

  getPayments () {
    var paymentList = [];
    var paymentMethods = [];

    if (this.data.PaymentMethod) {
      paymentList.push(this.data.PaymentMethod);
    };
    if (this.data.PaymentMethods) {
      this.data.PaymentMethods.split(',').forEach(function(e){
        if (paymentList.indexOf(e) == -1) {
          paymentList.push(e);
        }
      })
    };
    for (var i = 0; i < paymentList.length; i++) {
      console.log(paymentList[i]);
      paymentMethods.push(new PaymentMethod(this.data.idOrder,paymentList[i]));
    };
    console.log(paymentMethods);
    this.data.payments = paymentMethods;
  }
}

/* Find a single ORDER SUMMARY data */
Order.findSummaryById = function (id, callback) {
  dbConnection(function(err, conn){
    conn.query('SELECT <<query text removed>> WHERE o.idOrder = ?', [id], function(err, rows) {
      if (err) return callback(err);
      callback(null, new Order(rows[0], 'summary'));
    });
    conn.release();
  })
};

/* Find a single ORDER PAYMENT data */
Order.findPaymentById = function (id, callback) {
  dbConnection(function(err, conn){
    conn.query('SELECT <<query text removed>> WHERE o.idOrder = ?', [id], function(err, rows) {
      if (err) return callback(err);
      let order = new Order(rows[0], 'payment');
      order.getPayments();
      callback(null, order);
    });
    conn.release();
  })
};

/* Build order timeline */
function _getTimeline(status) {
  <<business logic removed>>
  return orderTimeline;
}

/* Determine order health */
 function _getHealth(status) {
  <<business logic removed>>
  return health;
}

/* Determine order source */
 function _getSource(idKalahariOrder, idAffiliate, TakeALot) {
  <<business logic removed>>
  return source;
}

/* Determine client platform */
function _getPlatform(clientId) {
  <<business logic removed>>
  return platform;
}

/* Determine external reference */
function _getExternalReference(idKalahariOrder, AffiliateReference) {
  <<business logic removed>>
  return reference;
}

module.exports = Order;

The code in my paymentmethod.js model:

'use strict';

/* Include MySQL connection */
var dbConnection = require(appDir + process.env.PATH_LIB + 'database');

class PaymentMethod {
  constructor(idOrder, paymentType) {
    var SQL = '';
    switch (paymentType) {
      case 'Credit Card':
        SQL = 'SELECT <<query text removed>> WHERE idOrder = ?';
        break;
      default:
        SQL = 'SELECT <<query text removed>> WHERE idOrder = ?';
    };
    dbConnection(function(err, conn){
      conn.query(SQL, [idOrder], function (err, data) {
        if (err) return callback(err);
        if (data) {
          this.paymentType = paymentType;
          this.paymentAmount = data.paymentAmount;
          this.paymentReference = data.paymentReference;
          this.paymentState = data.paymentState;
        }
      });
      conn.release();
    });
  }
}

module.exports = PaymentMethod;

The issue seems to be in the getPayments method on the Order class. When the push paymentMethods.push(new PaymentMethod(this.data.idOrder,paymentList[i])); is executed, it seems that due to async, the following lines of code are executed before the new class instance of PaymentMethod is pushed into the array. I have even tried to first assign the new class instance to a variable, and then push this, but it has the same affect. Any ideas?

EDITED:

I have changed the PaymentMethod class to:

class PaymentMethod {
  constructor(idOrder, paymentType) {
    var SQL = '';
    switch (paymentType) {
      case 'Credit Card':
        SQL = 'SELECT <<SQL text>> WHERE idOrder = ?';
        break;
      default:
        SQL = 'SELECT <<SQL text>> WHERE idOrder = ?';
    };
    return new Promise(function (resolve) {
      this.loadData(SQL, idOrder).then(function (data) {
        console.log(data);
        this.paymentType = paymentType;
        this.paymentAmount = data.paymentAmount;
        this.paymentReference = data.paymentReference;
        this.paymentState = data.paymentState;
        resolve (this);
      });
    });
  };

  loadData (SQL, idOrder) {
    console.log('fetching data ...');
    return new Promise (function (resolve) {
      dbConnection(function(err, conn){
        conn.query(SQL, [idOrder], function (err, data) {
          if (err) return callback(err);
          if (data) {
            resolve (data);
          }
        });
        conn.release();
      });
    })
  };
}

And, call it here:

  getPayments () {
    var paymentList = [];
    var paymentMethods = [];

    if (this.data.PaymentMethod) {
      paymentList.push(this.data.PaymentMethod);
    };
    if (this.data.PaymentMethods) {
      this.data.PaymentMethods.split(',').forEach(function(e){
        if (paymentList.indexOf(e) == -1) {
          paymentList.push(e);
        }
      })
    };
    for (var i = 0; i < paymentList.length; i++) {
      console.log(paymentList[i]);
      //var paymentDet = new PaymentMethod(this.data.idOrder,paymentList[i]);
      new PaymentMethod(this.data.idOrder,paymentList[i]).then(function(data) {
        console.log('detail:',data);
        paymentMethods.push(data);
      });
    };
    console.log(paymentMethods);
    this.data.payments = paymentMethods;
  }

But, still no luck ....

FINAL SOLUTION Order class:

class Order {
  constructor (data, requestTemplate) {
    this.data = data;
    switch (requestTemplate) {
      case 'summary':
        this.data.platform = _getPlatform(data.TakeALot);
        this.data.source = _getSource(data.idKalahariOrder,data.idAffiliate,data.TakeALot);
        this.data.reference = _getExternalReference(data.idKalahariOrder,data.AffiliateReference);
        break;
      case 'payment':
        this.data.health = _getHealth(data.Auth);
        this.data.timeline = _getTimeline(data.Auth);
        break;
    };
  }

  getPayments () {
    var self = this;
    return new Promise(function (resolve) {
      var paymentList = [];
      var paymentMethods = [];

      if (self.data.PaymentMethod) {
        paymentList.push(self.data.PaymentMethod);
      };
      if (self.data.PaymentMethods) {
        self.data.PaymentMethods.split(',').forEach(function(e){
          if (paymentList.indexOf(e) == -1) {
            paymentList.push(e);
          }
        })
      };
      for (var i = 0; i < paymentList.length; i++) {
        new PaymentMethod(self.data.idOrder,paymentList[i]).then(function(data) {
          paymentMethods.push(data);
          if (paymentMethods.length == paymentList.length) {
            self.data.payments = paymentMethods;
            resolve(self);
          }
        })
      };
    })
  }
}

PaymentMethod class:

class PaymentMethod {
  constructor(idOrder, paymentType) {
    var SQL = '';
    switch (paymentType) {
      case 'Credit Card':
        SQL = 'SELECT <<query text>> WHERE idOrder = ?';
        break;
      default:
        SQL = 'SELECT <<query text>> WHERE idOrder = ?';
    };

    var self = this;

    return new Promise(function (resolve) {
      self.loadData(SQL, idOrder).then(function (data) {
        self.paymentType = paymentType;
        self.paymentAmount = data.paymentAmount;
        self.paymentReference = data.paymentReference;
        self.paymentState = data.paymentState;
        resolve (self);
      }).catch(function(error) {
        console.log('Error occurred!', error);
      });
    }).catch(function(error) {
      console.log('Error occurred!', error);
    });
  }

  loadData (SQL, idOrder) {
    return new Promise (function (resolve) {
      dbConnection(function(err, conn){
        conn.query(SQL, [idOrder], function (err, data) {
          if (err) return callback(err);
          if (data) {
            resolve (data[0]);
          }
        });
        conn.release();
      });
    }).catch(function(error) {
      console.log('Error occurred!', error);
    });
  };
}

Code that instantiates a new Order instance:

Order.findPaymentById = function (id, callback) {
  dbConnection(function(err, conn){
    conn.query('SELECT <<query text>> WHERE o.idOrder = ?', [id], function(err, rows) {
      if (err) return callback(err);
      let order = new Order(rows[0], 'payment');
      order.getPayments().then(function(){
        callback(null, order);
      });
    });
    conn.release();
  })
};
go4cas
  • 1,171
  • 5
  • 14
  • 27
  • Just some extra info: The models are called from the controller, in the form: exports.getOrderPaymentById = function(request, reply) { Order.findPaymentById(request.params.id, function (err, order) { reply(order); }) }; The DB connector is using the node-mysql package. – go4cas Dec 26 '15 at 11:46
  • Since it's async, you'll have to wait for it to finish before continuing execution. Have a look at using Promises and [this similar question](http://stackoverflow.com/questions/28432401/replacing-callbacks-with-promises-in-node-js). – Daniel B Dec 26 '15 at 12:00
  • @DanielB, I had look at native Promises, but I couldn't manage to wrap a class instantiate within a Promise. – go4cas Dec 26 '15 at 12:04

1 Answers1

0

In order to print the results and return the right data, I would advise returning a promise in the getPayments function - that way the callee function will have access to all the payment methods inside a .then closure.

For example:

Callee function:

getPayments().then(function(paymethods) { 
  //use paymethods array here for something
  console.log(paymethods);
});

getPayments function:

function getPayments () {
  var paymentList = [];
  var paymentMethods = [];

  if (this.data.PaymentMethod) {
    paymentList.push(this.data.PaymentMethod);
  };
  if (this.data.PaymentMethods) {
    this.data.PaymentMethods.split(',').forEach(function(e){
      if (paymentList.indexOf(e) == -1) {
        paymentList.push(e);
       }
    })
  };
  return new Promise(function(resolve) {
    var last = false;
    for (var i = 0; i < paymentList.length; i++) {
      if (i === paymentList.length-1) { last=true }
      console.log(paymentList[i]);
      new PaymentMethod(this.data.idOrder,paymentList[i]).then( function(data) {
        console.log('detail:',data);
        paymentMethods.push(data);
        if (last) {
          resolve(paymentMethods);
        }
      });
    };
  });
}
jsdeveloper
  • 3,945
  • 1
  • 15
  • 14
  • Thanks, @jsdeveloper. I've been trying to implement the ES6 example in my PaymentMethod class, but I'm obviously missing something basic here. The example seems to create 2 Promises. So, I'm not sure how to implement this for my use case. – go4cas Dec 26 '15 at 14:11
  • The new code will work much better, but you wont be able to copy or print the paymentMethods array at the end of the loop since the .then call is still asyc and it wont be populated yet. Why dont you make the assignment inside the .then function? – jsdeveloper Dec 26 '15 at 18:43
  • are you referring to the then() function of the loadData, or the one in the order model which creates the new instance of PaymentMethod? – go4cas Dec 27 '15 at 10:31
  • Assigning the array should work after the loop since it will be passed by reference (and not cloned), its just that the array wont be populated until some time in the future. If you need to do something at the very end, you need to calculate the last loop iteration and pass that value into the .then handler in the getpayments function. – jsdeveloper Dec 27 '15 at 12:14
  • thanks for your help so far. But, this is bednign my brain at the moment. I'm not sure how I can move the paymentMethods.push(res); out of the for loop, as the res value will change with every loop iteration. Sorry, if my questions are completely stupid ... – go4cas Dec 27 '15 at 12:21
  • hmm - your final solution code appears to resolve the promise after the first paymentmethod is processed. I.e. this won't work for multiple payment methods - which is why you need to track the last payment method and resolve the promise only when that returns... – jsdeveloper Dec 27 '15 at 14:06