keystoneJS advanced shopping cart

In the previous tutorial we just created an end to end framework. but in real world there are some chalenges to be addressed.

(1) in an order customer may like to include more quantity of same product. 

(2)The price of product may change. Thus the price  when order is made is relevant, and not current price.

(3) discount.

So the Order model having  reference to product model in previous tutorial is not sufficient. The Order should ideally refer to something which not only holds reference to product, but also holds other information like discount and quantity.

mongoose addresses real life complexities

The mongoose documentation for schematype  suggestes that its possible to have nested field using {} or array of fields using [], or mixed (dynamic).  More over it can also embed other schema as subdocument.  

The structure we need now for our use case is this.

    items: [{
            product: { type: Types.Relationship, ref: 'Product', many: false, },
            quantity: { type: Number}
    }]

Does keystoneJS also addresses them?

As keystoneJS used mongoose it must be possible? but when I tried I get this error .

throw new Error('Fields must be specified with a type function');

When asked to keystone developer, He said that

Complex models with nested collections (or, for that matter, using mongoose's Mixed Schema Type) are currently (a) possible, and (b) not supported in the Admin UI yet.

The mongoose schema is available for you to modify to your heart's content, so you could still achieve the schema you want  by  schema.add   e.g Gallery.schema.add({})

Thus  we can not use mongoose syntax in a Keystone JS  function.we  can not use mongoose complex model, or collections in Keystone JS .

However we can add them using the mongoose schema object which is exposed when you're defining the list.

So in our case we can add array of order Items as follow

Order.schema.add({
    items: [{
            productid: Schema.Types.ObjectId,
            quantity: Number,
    }]

 

The Developer also warns that The downside is, obviously, the items field will be completely ignored by the Admin UI, and you won't get those Add, Edit and Trash buttons :)

So lets go with a less risky approach. that is to add a new model called OrderItem.

 

OrderItem.add({
    product: { type: Types.Relationship, ref: 'Product', many: false, index: true , required: true,initial:true},
    quantity: { type: Types.Number, default: 1 },
    price: { type: Types.Number, required: true },
    discount: { type: Types.Number, required: true }
});

 

and then link it inside Order model a s this.

 items: { type: Types.Relationship, ref: 'OrderItem', many: true, index: true , required: true,initial:true},

I have also made UI changes for supporting this new usecase. You can see the youtube video for more detail. basically I have added quantity  when you add a product to cart.

In this tutorial I  have also explained about using async library with great length. that you will need to save all orderItem before you save the order like this.

async.forEach(req.session.cart, function (item, callback){ 
             OrderItem = new OrderItem.model({
                --
            });
                
            OrderItem.save(function(err) {
                  callback();
            });

        }, function() {

               //save the order

      }

 

I have also explained in this tutorial  How can we use deepPopulate module to populate more then 1 level of relationship. so that you can do  mongoose 2 level population in KeystoneJS.

 

For doing this  you first install deep population module .

 

then go to the model which you want to be deeply populated, and add this code before register

var deepPopulate = require('mongoose-deep-populate')(keystone.mongoose);
Order.schema.plugin(deepPopulate);

 

then after you can replace .populate with .deeppopulate any where like this

Order.paginate({page: req.query.page || 1,
        perPage: 2,
        maxPages: 10
    })
    .where('customer', req.user.id)
    //.populate('customer items')
    .deepPopulate('customer items.product')

 

 

aaa