0

I'm trying to create a NodeJS app. Below is code that is supposed to be called when an admin creates a new product. Most of the code works, but I'm having trouble rendering the view only after the rest of the code has executed (the DB functions are asynchronous). I've wrapped much of the code in promises (to make certain blocks execute in the right order) and console logs (to pinpoint problems).

I'd like to point out that the console.dir(rejProducts)just below console.log(111) logs and empty array. Also, adding console.dir(rejProducts) just before the end bracket of the for loop logs an empty array. Thanks! Please let me know if you need more information.

app.post('/products/new', function (req, res, next) {
  // Async function: find all categories
  Category.find(function(err, categories) {
    // Hidden count that tells num products to be created by form
    var numProducts = req.body[`form-item-count`];
    // Array of all rejected products adds
    var rejProducts = [];

    var promiseLoopProducts = new Promise(function(resolve, reject) {
      var promiseProducts = [];
      // Loop through all addded products
      for (let i = 0; i < numProducts; i++) {
        var promiseProductCheck = new Promise(function(resolve, reject) {
          var name = validate.sanitize(req.body[`name_${i}`]);
          var category = validate.sanitize(req.body[`category_${i}`]);
          var price = validate.sanitize(req.body[`price_${i}`].replace(/\$/g, ""));
          var stock = validate.sanitize(req.body[`stock_${i}`]);
          var image = validate.sanitize(req.body[`image_${i}`]);
          var description = validate.sanitize(req.body[`description_${i}`]);

          var rejProduct;
          var rejFields = { 'name': name, 'category': category, 'price': price,
                            'stock': stock, 'image': image,
                            'description': description };
          var rejErrors = {};

          var productData = {
            name: name,
            category: category,
            price: price,
            stock: stock,
            image: image,
            description: description
          };
          var promiseCategoryCheck = new Promise(function(resolve, reject) {
            if (ObjectId.isValid(category)) {
              var promiseCategoryCount = new Promise(function(resolve, reject) {
                Category.count({'_id': category}, function(error, count) {
                  rejErrors['name'] = validate.isEmpty(name);
                  if (count == 0) rejErrors['category'] = true;
                  rejErrors['price'] = !validate.isPrice(price);
                  rejErrors['stock'] = !validate.isInt(stock);

                  if( validate.isEmpty(name) || !validate.isPrice(price) ||
                      count == 0 || !validate.isInt(stock) ) {
                    rejProduct = { 'fields': rejFields, 'errors': rejErrors };
                    rejProducts.push(rejProduct);
                    console.dir(rejProducts);
                    console.log(count);
                    return resolve();
                  }
                  else {
                    Product.create(productData, function (error, product) {
                      if (error) return next(error);
                      console.log(77);
                        console.dir(rejProducts);
                      return resolve();
                    });
                  }
                  if (error) return next(error);
                });
              });
              promiseCategoryCount.then(function() {
                console.dir(rejProducts);
                return resolve();
              });
            } else {
              rejErrors['category'] = true;
              rejProduct = { 'fields': rejFields, 'errors': rejErrors };
              rejProducts.push(rejProduct);
              console.dir(rejProducts);
            }
          });
          promiseCategoryCheck.then(function() {
            console.dir(rejProducts);
            promiseProducts.push(promiseProductCheck);
            console.log(promiseProductCheck);
            console.log(promiseProducts);
            return resolve();
          });
        });
        promiseProductCheck.then(function() {
          console.log(106);
          console.dir(rejProducts);
        });
      }
      Promise.all(promiseProducts).then(function() {
        console.log(111);
        console.dir(rejProducts); // Empty array!
        return resolve();
      });
    });

    promiseLoopProducts.then(function() {
      console.log(118);
      console.dir(rejProducts); // Empty array!
      res.render('products/new', { categories: categories, rejProducts: rejProducts });
    });

  });

});

Edit: I've made some changes to the code. There it seems like util.promisify is not being recognized as a function. I am using Node 9.4.

module.exports = function(app){

const util = require('util');
require('util.promisify').shim();
const validate = require('../functions/validate');

const Category = require('../db/categories');
const Product = require('../db/products');

var ObjectId = require('mongoose').Types.ObjectId;
//Category.find as function that returns a promise
const findCategories = util.promisify(Category.find);

const countCategories = (categoryId) => {
  util.promisify(Category.count({'_id': categoryId}));
};

const bodyToProduct = (body, i) => {
  var name = validate.sanitize(body[`name_${i}`]);
  var category = validate.sanitize(body[`category_${i}`]);
  var price = validate.sanitize(body[`price_${i}`].replace(/\$/g, ""));
  var stock = validate.sanitize(body[`stock_${i}`]);
  var image = validate.sanitize(body[`image_${i}`]);
  var description = validate.sanitize(body[`description_${i}`]);
  return {
    name: name,
    category: category,
    price: price,
    stock: stock,
    image: image,
    description: description
  };
};

app.post('/products/new', function (req, res, next) {
  // Async function: find all categories
  return findCategories()
  .then(
    categories=>{
      // Hidden count that tells num products to be created by form
      var numProducts = req.body[`form-item-count`];
      // Array of all rejected products adds
      var rejProducts = [];
      return Promise.all(
        Array.from(new Array(numProducts),(v,i)=>i)
        .map(//map [0...numProducts] to product object
          i=>bodyToProduct(req.body,i)
        )
        .map(
          product => {
            var rejErrors;
            var rejName = validate.isEmpty(name);
            var rejCategory;
            var rejPrice = !validate.isPrice(price);
            var rejStock = !validate.isInt(stock);
            if (ObjectId.isValid(product.category)) {
              return countCategories()
              .then(
                count=> {
                  if (count == 0) rejCategory = true;

                  if(rejName || rejCategory || rejPrice || rejStock ) {
                    rejErrors = {
                      name: rejName,
                      category: rejCategory,
                      price: rejPrice,
                      stock: rejStock
                    }
                    rejProduct = { 'fields': product, 'errors': rejErrors };
                    rejProducts.push(rejProduct);
                    console.dir(rejProducts);
                    console.log(count);
                  } else {
                    Product.create(productData, function (error, product) {
                      if (error) return next(error);
                      console.log(77);
                        console.dir(rejProducts);
                    });
                  }
                }
              ).catch(function() {
                console.log("Count function failed.");
              });
            } else {
              rejCategory = true;
              rejErrors = {
                name: rejName,
                category: rejCategory,
                price: rejPrice,
                stock: rejStock
              }
              rejProduct = { 'fields': product, 'errors': rejErrors };
              rejProducts.push(rejProduct);
              console.dir(rejProducts);
            }
          }
        )
      ).then(function() {
        res.render('products/new', { categories: categories, rejProducts: rejProducts });
      }).catch(function() {
        console.log("Promise all products failed.");
      });
    }
  ).catch(function() {
    console.log("Find categories failed.");
  })
});

}
Wallie
  • 45
  • 9
  • You're pushing promises to the list asynchronously, e.g. your promises are not populated in the list until after the inner code has executed. Hence `Promise.all(promiseProducts)` gets to the list before there actually _is_ anything in the list. – Aleksey Bilogur Feb 20 '18 at 03:46
  • @AlekseyBilogur Hi! Thanks for responding. I think a better question would be: How do I execute `Promise.all(promiseProducts).then(function() {` only after all the for loop code has completed? – Wallie Feb 20 '18 at 04:04

1 Answers1

1

Some tips: if your function is over 100 lines you may be doing to much in the function.

If you have to get data from your request the way you get products of it then write better client side code (products should be an array of product objects that should not need to be sanitized). Validation is needed on the server because you an never trust what the client is sending you. But looking at the sanitize you don't even trust what your client script is sending you.

Try to write small functions that do a small thing and try to use those.

Use .map to map type a to type b (for example req.body to array of products as in the example code).

Use the result of .map as an argument to Promise.all

Use util.promisify to change a callback function into a function that returns a promise, since you are using an old version of node I've added an implementation of promisify:

var promisify = function(fn) {
  return function(){
    var args = [].slice.apply(arguments);
    return new Promise(
      function(resolve,reject){
        fn.apply(
          null,
          args.concat([
            function(){
              var results = [].slice.apply(arguments);
              (results[0])//first argument of callback is error
                ? reject(results[0])//reject with error
                : resolve(results.slice(1,results.length)[0])//resolve with single result
            }
          ])
        )
      }
    );
  }
};

module.exports = function(app){
  const validate = require('../functions/validate');
  const Category = require('../db/categories');
  const Product = require('../db/products');
  var ObjectId = require('mongoose').Types.ObjectId;
  //Category.find as function that returns a promise
  const findCategories = promisify(Category.find.bind(Category));
  const countCategories = (categoryId) => {
    promisify(Category.count.bind(Category))({'_id': categoryId});
  };
  const createProduct = promisify(Product.create.bind(Product));
  const REJECTED = {};
  const bodyToProduct = (body, i) => {
    var name = validate.sanitize(body[`name_${i}`]);
    var category = validate.sanitize(body[`category_${i}`]);
    var price = validate.sanitize(body[`price_${i}`].replace(/\$/g, ""));
    var stock = validate.sanitize(body[`stock_${i}`]);
    var image = validate.sanitize(body[`image_${i}`]);
    var description = validate.sanitize(body[`description_${i}`]);
    return {
      name: name,
      category: category,
      price: price,
      stock: stock,
      image: image,
      description: description
    };
  };

  const setReject = product => {
    var rejErrors;
    var rejName = validate.isEmpty(product.name);
    var rejCategory;
    var rejPrice = !validate.isPrice(product.price);
    var rejStock = !validate.isInt(product.stock);
    const countPromise = (ObjectId.isValid(product.category))
      ? countCategories()
      : Promise.resolve(0);
    return countPromise
    .then(
      count => {
        if (count == 0) rejCategory = true;

        if (rejName || rejCategory || rejPrice || rejStock) {
          rejErrors = {
            type:REJECTED,
            name: rejName,
            category: rejCategory,
            price: rejPrice,
            stock: rejStock
          }
          return rejErrors;
        }
        return false;
      }
    );
  };

  const productWithReject = product =>
    Promise.all([
      product,
      setReject(product)
    ]);

  const saveProductIfNoRejected = ([product,reject]) =>
    (reject===false)
      ? Product.create(product)
        .catch(
          err=>({
            type:REJECTED,
            error:err
          })
        )
      : reject;

  app.post('/products/new', function (req, res, next) {
    // Async function: find all categories
    return findCategories()
    .then(
      categories => {
        // Hidden count that tells num products to be created by form
        var numProducts = req.body[`form-item-count`];
        // Array of all rejected products adds
        var rejProducts = [];
        return Promise.all(
          Array.from(new Array(numProducts), (v, i) => i)
            .map(//map [0...numProducts] to product object
              i => bodyToProduct(req.body, i)
            )
            .map(
              product=>
                productWithReject(product)
              .then(saveProductIfNoRejected)
            )
        ).then(
          results =>
            res.render(
              'products/new',
              { 
                categories: categories,
                rejProducts: results.filter(x=>(x&&x.type)===REJECTED)
              }
            )
        ).catch(
          error=>
            console.log("Promise all products failed.",error)
        );
      }
    ).catch(
      error=>
        console.log("Find categories failed.",error)
    )
  });
}
HMR
  • 37,593
  • 24
  • 91
  • 160
  • Hello! Thank you very much for responding! I think this has pretty much fixed my problem, except I'm getting a `UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 2): TypeError: Cannot read property 'Query' of undefined` when I run the code. – Wallie Feb 20 '18 at 20:44
  • @Wallie Can you please update your question with the current code you are using? I may not have time right now but will have a look later. – HMR Feb 20 '18 at 21:41
  • Just updated. The new code is the one at the bottom. – Wallie Feb 21 '18 at 01:11
  • Hmm... I'm still getting the same error - might not be a problem with the actual code. Did I require all necessary modules? – Wallie Feb 22 '18 at 03:11
  • @Wallie `Cannot read property 'Query' of undefined` There is no `Query` in the code provided. – HMR Feb 22 '18 at 03:21
  • Ah, sorry - I was referring to the `util.promisify is not recognized as a function` error. – Wallie Feb 22 '18 at 05:19
  • @Wallie, you are using an older version of node I think. There are some solutions [here](https://stackoverflow.com/questions/46476741/nodejs-util-promisify-is-not-a-function) – HMR Feb 22 '18 at 05:41
  • @Wallie updated the answer with own implementation of promisify. – HMR Feb 22 '18 at 06:16