UNPKG

mcms-node-eshop

Version:

Eshop module for mcms-node

468 lines (403 loc) 16.7 kB
/* * 1. grab permalink from mongo * 2. perform the query on ES * 3. eager load (or just loop) the results to replace ID's with actual objects * 4. return * */ module.exports = (function(App,Connection,Package,privateMethods){ var async = require('async'); var lo = require('lodash'); var Relationships = Package.modelRelationships; var ProductModel = Connection.models.Product, ExtraFieldsModel = App.Connections[App.Config.database.default].models.ExtraField, CategoryModel = Connection.models.ProductCategory, Options = {}, relatedSkus = [], Filters = {}, returnObj = {}; var eagerLoader = require('mcms-node-eager-loader')(), Loader = new eagerLoader(), ES = App.Connections.elasticSearch, Cache = App.Cache; function find(filters,options,callback){ var asyncArr = []; Options = options, withRelations = [ Relationships.thumb ]; if (typeof filters.permalink != 'undefined' && !lo.isObject(filters.permalink)){ asyncArr.push(getCategoryByPermalink.bind(null,filters.permalink)); } asyncArr.push(function(category,next){ if (arguments.length == 1 && lo.isFunction(category)){//no permalink so everything is changed next = arguments[0]; category = filters || {}; } Loader.set(privateMethods).with(withRelations). exec(getItems.bind(null,category),next); }); async.waterfall(asyncArr,function(err,results){ if (err){ console.log('ERR:',err); return callback(err); } returnObj.items = results; //lets complicate things a bit. If we want the related SKU's, we need to go and execute a brand new async - eager load round if (!Options.with || Options.with.indexOf('relatedSku') == -1){ return callback(null,returnObj); } async.waterfall([ groupSku.bind(null,returnObj.items), fetchSku, mergeResults ],function(errors,final){ if (!returnObj || !returnObj.items){ return callback(null,returnObj); } //so.... now the aggregated array (relatedSkus) is ready. It contains the products in their final form (thumbs and all). //What we need to do now is iterate the ES results and add as an array all of the found items. // The way to do this is to compare the baseSku from the aggregated array to the one in our ES results. //mind fuck intensifies.... returnObj.items.forEach(function(product){ var found = lo.find(final,{baseSku : product.baseSku}); if (found){ product.relatedSkus = {count : found.count,items : found.items}; var me = lo.find(product.relatedSkus.items,{sku : product.sku}); var temp = lo.clone(product.relatedSkus.items); if (me){//remove my self from the counts temp.splice(temp.indexOf(me),1);//we need to remove from a clone so that we will not affect the rest of the results product.relatedSkus.items = temp; product.relatedSkus.count = product.relatedSkus.items.length;//update counts } } }); callback(null,returnObj); returnObj = null; }); }); } function getCategoryByPermalink(permalink,next){ CategoryModel.findOne({permalink : permalink}).exec(function(err,category){ if (!category){ return next('noCategoryFound'); } next(null,category); }); } function getItems(queryFilters,next){ var page = Options.page || 1; var limit = Options.limit || 10; var priceRangeInterval = Options.priceRangeInterval || 40; var priceRangeSteps = Options.priceRangeSteps || 4; var sort = {}, active = [], ranges = [], sortField = (Options.sort) ? Options.sort : 'created_at', activeField = (Options.active) ? Options.active : true; var way = (Options.way) ? Options.way : 'desc'; Filters = (Options.filters) ? Options.filters : {}; sort[sortField] = { order : way }; var simplified = (Options.simplified) ? Options.simplified : false; active.push(activeField); var Query = { bool : { must : { terms : {active : active} }, should : [], minimum_should_match : 9999 } }; for (var i = 0;priceRangeSteps > i;i++){ var tmp = {}, last = 0; if (i == 0){ ranges.push({ to : priceRangeInterval }); continue; } if (i == priceRangeSteps-1){ ranges.push({from : (priceRangeInterval*priceRangeSteps)+1}); continue; } ranges.push({ from : (priceRangeInterval*i), to : (priceRangeInterval*(i+1))-1 }) } var Aggregations = { categories : { "terms": { "field": "categories" } }, ExtraFields : { "nested": { "path": "ExtraFields" }, "aggs": { "fields": { "terms": { "field": "ExtraFields.fieldID", "size" : 0 }, "aggs": { "values": { "terms": { "field": "ExtraFields.value", "size" : 0, "order": { "_count" : "desc" } } } } } } }, priceRange : { "histogram" : { "field" : "eshop.price", "interval" : priceRangeInterval } }, prices : { "stats" : { "field" : "eshop.price" } }, priceRanges : { "range" : { "field" : "eshop.price", "ranges" : ranges } } }; //Filter by category if (queryFilters && typeof queryFilters.id != 'undefined'){ Query.bool.should.push({terms : {categories : [queryFilters.id || queryFilters['_id']]} }); } Query.bool.should.push({"range": {"eshop.quantity": {"gte": 0}}}); Query.bool.should.push({terms : {active : active} }); if (queryFilters.q){ var fuzzyQuery = { "multi_match": { "query": queryFilters.q, "fields": [ "title", "sku" ], "type": "phrase_prefix" } }; Query.bool.should.push(fuzzyQuery); } if (Options.filters.category){ Query.bool.should.push({terms : {categories : [Options.filters.category]} }); } if (Options.filters.ExtraFields && Options.filters.ExtraFields.length > 0){ lo.forEach(Options.filters.ExtraFields,function(field){ var tmp = { nested : { path : "ExtraFields", score_mode : "none", query : { bool : { must : [] } } } }; var val = (!lo.isArray(field.values)) ? [field.values] : field.values; tmp.nested.query.bool.must.push({ terms : {"ExtraFields.value" : val} }); tmp.nested.query.bool.must.push({ match : {"ExtraFields.fieldID": field.fieldID} }); Query.bool.should.push(tmp); }); } if (Options.filters.price){ Query.bool.should.push({"range": {"eshop.price": {"gte": Options.filters.price.from}}}); Query.bool.should.push({"range": {"eshop.price": {"lte": Options.filters.price.to}}}); } var toExecute = { primary : { index : 'products', body: { query : Query, aggregations : Aggregations }, size : limit, from : ((page - 1) * limit), sort : sortField+':'+way }, secondary : { size : limit, from : ((page - 1) * limit) } }; if (Options.debug){ console.log(JSON.stringify(toExecute.primary.body),'\n',JSON.stringify(toExecute.secondary)); } ES.search(toExecute.primary,toExecute.secondary).then(function(results){ var items = (results.hits.count == 0) ? [] : results.hits.hits; returnObj = { category : (queryFilters.id || queryFilters['_id']) ? queryFilters : {}, count : results.hits.total, aggregations : privateMethods.parseEsAggregations(results.aggregations) }; returnObj.aggregations = eagerLoadAggregations(returnObj.aggregations); next(null,parseEsItems(items)); }); } function parseEsItems(items){ var ret = []; lo.forEach(items,function(item){ var tmp = item._source; tmp.id = item._id; tmp._id = item._id; if (tmp.thumb){ tmp.thumb.id = App.Helpers.MongoDB.idToObjId(tmp.thumb.id); } if (tmp.categories){ tmp.categories = App.Helpers.MongoDB.arrayToObjIds(tmp.categories); } if (tmp.mediaFiles.images.length > 0){ for (var i in tmp.mediaFiles.images){ tmp.mediaFiles.images[i].id = App.Helpers.MongoDB.idToObjId(tmp.mediaFiles.images[i].id); } } if (tmp.mediaFiles.documents.length > 0){ for (var i in tmp.mediaFiles.documents){ tmp.mediaFiles.documents[i].id = App.Helpers.MongoDB.idToObjId(tmp.mediaFiles.documents[i].id); } } if (tmp.mediaFiles.videos.length > 0){ for (var i in tmp.mediaFiles.videos){ tmp.mediaFiles.videos[i].id = App.Helpers.MongoDB.idToObjId(tmp.mediaFiles.videos[i].id); } } if (tmp.ExtraFields.length > 0){ for (var i in tmp.ExtraFields){ tmp.ExtraFields[i].fieldID = App.Helpers.MongoDB.idToObjId(tmp.ExtraFields[i].fieldID); } } if (Options.applyDiscounts){ App.Services[Package.packageName].Discount.applyDiscount(tmp);//apply possible discounts } ret.push(tmp); }); return ret; } function eagerLoadAggregations(aggs){ if (!Cache.ExtraFields || !Cache.ProductCategories){ return Package.services.Cache.addModelsToCache(eagerLoadAggregations(aggs));//load everything into cache and retry } if (aggs.ExtraFields){ aggs.ExtraFields = eagerLoadExtraFields(aggs.ExtraFields); } if (aggs.categories){ aggs.categories = eagerLoadCategories(aggs.categories); } return aggs; } function eagerLoadExtraFields(fields){ var processedFields = {}; fields.forEach(function(field){ var found = lo.find(Cache.ExtraFields,{_id : field.id}); if (found){ field = lo.merge(field,found); field.values.forEach(function(val){ var searchFor = lo.find(field.fieldOptions,{varName : val.key.toLowerCase()}); if (searchFor){ val.label = searchFor.title; } }); processedFields[field.varName] = field; } }); return processedFields; } function eagerLoadCategories(categories){ categories.forEach(function(category){ var found = lo.find(Cache.ProductCategories,{_id : category.key}); if (found){ category = lo.merge(category,found); } }); return categories; } function groupSku(products,next){ var skus = []; products.forEach(function(product){ skus.push(new RegExp('^'+product.baseSku,'i')); }); next(null,lo.uniq(skus),products); } /* * We are fetching the aggregated results, and then pass the simplified ones to the the eager loader to get thumbs and stuff */ function fetchSku(skus,products,next){ var EL = new eagerLoader(); EL.set(privateMethods).with(withRelations). exec(executeFetchSku.bind(null,skus),function(err,items){ if (err){ return next(err); } return next(null,items); }); } /* * What this does is goes to mongo, fetches all items aggregated, then simplifies them into a flat array * assigns the aggregated to a global variable and passes the simplified one to the eager loader */ function executeFetchSku(skus,next){ var Query = []; Query.push({'$match': { sku: { $in: skus } } }); Query.push({'$match': {"eshop.quantity": { $gt: 0 }}}); Query.push({'$match': {"active": true}}); Query.push({$project: { _id: 1, title : '$title', sku: 1,baseSku:1, thumb:1,permalink:1 } }); Query.push({'$group': { _id: { base : '$baseSku' }, items : {$push : {sku : '$sku',id : '$_id',permalink : '$permalink',thumb:'$thumb',title : '$title',baseSku:'$baseSku'} },count : {$sum:1} } }); ProductModel.aggregate(Query) .exec(function(err,items){ if (err){ return next(err); } if (items.length == 0){ return next(null,[]); } var ret = []; items.forEach(function(item){ for(var i=0;item.items.length > i;i++){ ret.push(item.items[i]); } relatedSkus.push({baseSku : item._id.base,items : item.items,count : item.count}); }); next(err,ret); }); } /* * The eager loader returns the simplified flat array of products. We now have to merge these with the global aggregated one * and when we are done, we need to merge that, with the original results returned by the ES query. * Some serious mind fuck */ function mergeResults(products,next){ relatedSkus.forEach(function(agg){ agg.items.forEach(function(product){ var found = lo.find(products,{_id : product._id}); if (found){ product = found; } }); }); next(null,relatedSkus); } return find; });