UNPKG

offshore

Version:
660 lines (556 loc) 22.7 kB
/** * Basic Finder Queries */ var usageError = require('../../utils/usageError'); var utils = require('../../utils/helpers'); var normalize = require('../../utils/normalize'); var sorter = require('../../utils/sorter'); var Deferred = require('../deferred'); var Joins = require('./joins'); var Operations = require('./operations'); var Integrator = require('../integrator'); var offshoreCriteria = require('offshore-criteria'); var _ = require('lodash'); var async = require('async'); var hasOwnProperty = utils.object.hasOwnProperty; function groupBy( array , groupBykeys, fct) { var groups = {}; array.forEach(function(item) { var groupValues = []; groupBykeys.forEach(function(key) { groupValues.push(item[key]); }); var group = JSON.stringify(groupValues); groups[group] = groups[group] || []; groups[group].push(item); }); return Object.keys(groups).map(function(group){ return fct(groups[group]); }); } module.exports = { /** * Find a single record that meets criteria * * @param {Object} criteria to search * @param {Function} callback * @return Deferred object if no callback */ findOne: function(criteria, cb, metaContainer) { var self = this; if (typeof criteria === 'function') { cb = criteria; criteria = null; } // If the criteria is an array of objects, wrap it in an "or" if (Array.isArray(criteria) && _.every(criteria, function(crit) {return _.isObject(crit);})) { criteria = {or: criteria}; } // Check if criteria is an integer or string and normalize criteria // to object, using the specified primary key field. criteria = normalize.expandPK(self, criteria); // Normalize criteria criteria = normalize.criteria(criteria); // Return Deferred or pass to adapter if (typeof cb !== 'function') { return new Deferred(this, this.findOne, criteria); } // Transform Search Criteria criteria = self._transformer.serialize(criteria); // If a projection is being used, ensure that the Primary Key is included if(criteria.select) { _.each(this._schema.schema, function(val, key) { if (_.has(val, 'primaryKey') && val.primaryKey) { criteria.select.push(key); } }); criteria.select = _.uniq(criteria.select); } // serialize populated object if (criteria.joins) { criteria.joins.forEach(function(join) { if (join.criteria && join.criteria.where) { var joinCollection = self.offshore.collections[join.child]; join.criteria.where = joinCollection._transformer.serialize(join.criteria.where); } }); } // If there was something defined in the criteria that would return no results, don't even // run the query and just return an empty result set. if (criteria === false || criteria.where === null) { // Build Default Error Message var err = '.findOne() requires a criteria. If you want the first record try .find().limit(1)'; return cb(new Error(err)); } // Build up an operations set var operations = new Operations(self, criteria, 'findOne', metaContainer); // Run the operations operations.run(function(err, values) { if (err) return cb(err); if (!values.cache) return cb(); // If no joins are used grab the only item from the cache and pass to the returnResults // function. if (!criteria.joins) { values = values.cache[self.identity]; return returnResults(values); } // If the values are already combined, return the results if (values.combined) { return returnResults(values.cache[self.identity]); } // Find the primaryKey of the current model so it can be passed down to the integrator. // Use 'id' as a good general default; var primaryKey = 'id'; Object.keys(self._schema.schema).forEach(function(key) { if (self._schema.schema[key].hasOwnProperty('primaryKey') && self._schema.schema[key].primaryKey) { primaryKey = key; } }); // Perform in-memory joins Integrator(values.cache, criteria.joins, primaryKey, function(err, results) { if (err) return cb(err); if (!results) return cb(); // We need to run one last check on the results using the criteria. This allows a self // association where we end up with two records in the cache both having each other as // embedded objects and we only want one result. However we need to filter any join criteria // out of the top level where query so that searchs by primary key still work. var tmpCriteria = _.cloneDeep(criteria.where); if (!tmpCriteria) tmpCriteria = {}; criteria.joins.forEach(function(join) { if (!hasOwnProperty(join, 'parentKey')) return; // Check for `OR` criteria if (hasOwnProperty(tmpCriteria, 'or')) { tmpCriteria.or.forEach(function(search) { if (hasOwnProperty(search, join.parentKey)) { delete search[join.parentKey]; } }); return; } if (hasOwnProperty(tmpCriteria, join.parentKey)) { delete tmpCriteria[join.parentKey]; } }); // Pass results into Offshore-Criteria var _criteria = { where: tmpCriteria }; results = offshoreCriteria('parent', { parent: results }, _criteria).results; results.forEach(function(res) { // Go Ahead and perform any sorts on the associated data criteria.joins.forEach(function(join) { if (!join.criteria) return; var c = normalize.criteria(join.criteria); var alias = join.alias; if (c.average && _.isArray(res[alias])) { if (c.groupBy && _.isArray(c.groupBy)) { res[alias] = groupBy(res[alias], c.groupBy, function(group) { var average = {}; c.groupBy.forEach(function(groupBy) { average[groupBy] = group[0][groupBy]; }); average[c.average[0]] = _.meanBy(group, function(item) { return item[c.average[0]]; }); return average; }); } else { var average = {}; average[c.average[0]] = _.meanBy(res[alias], function(item) { return item[c.average[0]]; }); res[alias] = [average]; } } if (c.min && _.isArray(res[alias])) { if (c.groupBy && _.isArray(c.groupBy)) { res[alias] = groupBy(res[alias], c.groupBy, function(group) { var min = {}; c.groupBy.forEach(function(groupBy) { min[groupBy] = group[0][groupBy]; }); min[c.min[0]] = _.minBy(group, function(item) { return item[c.min[0]]; }); return min; }); } else { var min = {}; min[c.min[0]] = _.minBy(res[alias], function(item) { return item[c.min[0]]; })[c.min[0]]; res[alias] = [min]; } } if (c.max && _.isArray(res[alias])) { if (c.groupBy && _.isArray(c.groupBy)) { res[alias] = groupBy(res[alias], c.groupBy, function(group) { var max = {}; c.groupBy.forEach(function(groupBy) { max[groupBy] = group[0][groupBy]; }); max[c.max[0]] = _.maxBy(group, function(item) { return item[c.max[0]]; }); return max; }); } else { var max = {}; max[c.max[0]] = _.maxBy(res[alias], function(item) { return item[c.max[0]]; })[c.max[0]]; res[alias] = [max]; } } if (c.sum && _.isArray(res[alias])) { if (c.groupBy && _.isArray(c.groupBy)) { res[alias] = groupBy(res[alias], c.groupBy, function(group) { var sum = {}; c.groupBy.forEach(function(groupBy) { sum[groupBy] = group[0][groupBy]; }); sum[c.sum[0]] = _.sumBy(group, function(item) { return item[c.sum[0]]; }); return sum; }); } else { var sum = {}; sum[c.sum[0]] = _.sumBy(res[alias], function(item) { return item[c.sum[0]]; }); res[alias] = [sum]; } } if (!c.sort) return; res[alias] = sorter(res[alias], c.sort); }); }); returnResults(results); }); function returnResults(results) { if (!results) return cb(); // Normalize results to an array if (!Array.isArray(results) && results) results = [results]; // Unserialize each of the results before attempting any join logic on them var unserializedModels = []; results.forEach(function(result) { unserializedModels.push(self._transformer.unserialize(result)); }); var models = []; var joins = criteria.joins ? criteria.joins : []; var data = new Joins(joins, unserializedModels, self.identity, self._schema.schema, self.offshore.collections); // If `data.models` is invalid (not an array) return early to avoid getting into trouble. if (!data || !data.models || !data.models.forEach) { return cb(new Error('Values returned from operations set are not an array...')); } // Create a model for the top level values data.models.forEach(function(model) { models.push(new self._model(model, data.options)._loadQuery(self._query)); }); cb(null, models[0]); } }); }, /** * Find All Records that meet criteria * * @param {Object} search criteria * @param {Object} options * @param {Function} callback * @return Deferred object if no callback */ find: function(criteria, options, cb, metaContainer) { var self = this; var usage = utils.capitalize(this.identity) + '.find([criteria],[options]).exec(callback|switchback)'; if (typeof criteria === 'function') { cb = criteria; criteria = null; if(arguments.length === 1) { options = null; } } // If options is a function, we want to check for any more values before nulling // them out or overriding them. if (typeof options === 'function') { // If cb also exists it means there is a metaContainer value if (cb) { metaContainer = cb; cb = options; options = null; } else { cb = options; options = null; } } // If the criteria is an array of objects, wrap it in an "or" if (Array.isArray(criteria) && _.every(criteria, function(crit) {return _.isObject(crit);})) { criteria = {or: criteria}; } // Check if criteria is an integer or string and normalize criteria // to object, using the specified primary key field. criteria = normalize.expandPK(self, criteria); // Normalize criteria criteria = normalize.criteria(criteria); // Validate Arguments if (typeof criteria === 'function' || typeof options === 'function') { return usageError('Invalid options specified!', usage, cb); } // Return Deferred or pass to adapter if (typeof cb !== 'function') { return new Deferred(this, this.find, criteria, options); } // If there was something defined in the criteria that would return no results, don't even // run the query and just return an empty result set. if (criteria === false) { return cb(null, []); } // Fold in options if (options === Object(options) && criteria === Object(criteria)) { criteria = _.extend({}, criteria, options); } // If a projection is being used, ensure that the Primary Key is included if(criteria.select) { _.each(this._schema.schema, function(val, key) { if (_.has(val, 'primaryKey') && val.primaryKey) { criteria.select.push(key); } }); criteria.select = _.uniq(criteria.select); } // Transform Search Criteria if (!self._transformer) { throw new Error('Offshore can not access transformer-- maybe the context of the method is being overridden?'); } criteria = self._transformer.serialize(criteria); // serialize populated object if (criteria.joins) { criteria.joins.forEach(function(join) { var joinCollection = self.offshore.collections[join.child]; if (join.criteria && join.criteria.where) { join.criteria.where = joinCollection._transformer.serialize(join.criteria.where); } if (join.criteria && join.criteria.sort) { join.criteria.sort = joinCollection._transformer.serialize(join.criteria.sort); } }); } // Build up an operations set var operations = new Operations(self, criteria, 'find', metaContainer); // Run the operations operations.run(function(err, values) { if (err) return cb(err); if (!values.cache) return cb(); // If no joins are used grab current collection's item from the cache and pass to the returnResults // function. if (!criteria.joins) { values = values.cache[self.identity]; return returnResults(values); } // If the values are already combined, return the results if (values.combined) { return returnResults(values.cache[self.identity]); } // Find the primaryKey of the current model so it can be passed down to the integrator. // Use 'id' as a good general default; var primaryKey = 'id'; Object.keys(self._schema.schema).forEach(function(key) { if (self._schema.schema[key].hasOwnProperty('primaryKey') && self._schema.schema[key].primaryKey) { primaryKey = key; } }); // Perform in-memory joins Integrator(values.cache, criteria.joins, primaryKey, function(err, results) { if (err) return cb(err); if (!results) return cb(); // We need to run one last check on the results using the criteria. This allows a self // association where we end up with two records in the cache both having each other as // embedded objects and we only want one result. However we need to filter any join criteria // out of the top level where query so that searchs by primary key still work. var tmpCriteria = _.cloneDeep(criteria.where); if (!tmpCriteria) tmpCriteria = {}; criteria.joins.forEach(function(join) { if (!hasOwnProperty(join, 'parentKey')) return; // Check for `OR` criteria if (hasOwnProperty(tmpCriteria, 'or')) { tmpCriteria.or.forEach(function(search) { if (hasOwnProperty(search, join.parentKey)) { delete search[join.parentKey]; } }); return; } if (hasOwnProperty(tmpCriteria, join.parentKey)) { delete tmpCriteria[join.parentKey]; } }); // Pass results into Offshore-Criteria var _criteria = { where: tmpCriteria }; results = offshoreCriteria('parent', { parent: results }, _criteria).results; // Serialize values coming from an in-memory join before modelizing results.forEach(function(res) { // Go Ahead and perform any sorts on the associated data criteria.joins.forEach(function(join) { if (!join.criteria) return; var c = normalize.criteria(join.criteria); var alias = join.alias; if (c.sort) { res[alias] = sorter(res[alias], c.sort); } if (c.average && _.isArray(res[alias])) { if (c.groupBy && _.isArray(c.groupBy)) { res[alias] = groupBy(res[alias], c.groupBy, function(group) { var average = {}; c.groupBy.forEach(function(groupBy) { average[groupBy] = group[0][groupBy]; }); average[c.average[0]] = _.meanBy(group, function(item) { return item[c.average[0]]; }); return average; }); } else { var average = {}; average[c.average[0]] = _.meanBy(res[alias], function(item) { return item[c.average[0]]; }); res[alias] = [average]; } } if (c.min && _.isArray(res[alias])) { if (c.groupBy && _.isArray(c.groupBy)) { res[alias] = groupBy(res[alias], c.groupBy, function(group) { var min = {}; c.groupBy.forEach(function(groupBy) { min[groupBy] = group[0][groupBy]; }); min[c.min[0]] = _.minBy(group, function(item) { return item[c.min[0]]; }); return min; }); } else { var min = {}; min[c.min[0]] = _.minBy(res[alias], function(item) { return item[c.min[0]]; })[c.min[0]]; res[alias] = [min]; } } if (c.max && _.isArray(res[alias])) { if (c.groupBy && _.isArray(c.groupBy)) { res[alias] = groupBy(res[alias], c.groupBy, function(group) { var max = {}; c.groupBy.forEach(function(groupBy) { max[groupBy] = group[0][groupBy]; }); max[c.max[0]] = _.maxBy(group, function(item) { return item[c.max[0]]; }); return max; }); } else { var max = {}; max[c.max[0]] = _.maxBy(res[alias], function(item) { return item[c.max[0]]; })[c.max[0]]; res[alias] = [max]; } } if (c.sum && _.isArray(res[alias])) { if (c.groupBy && _.isArray(c.groupBy)) { res[alias] = groupBy(res[alias], c.groupBy, function(group) { var sum = {}; c.groupBy.forEach(function(groupBy) { sum[groupBy] = group[0][groupBy]; }); sum[c.sum[0]] = _.sumBy(group, function(item) { return item[c.sum[0]]; }); return sum; }); } else { var sum = {}; sum[c.sum[0]] = _.sumBy(res[alias], function(item) { return item[c.sum[0]]; }); res[alias] = [sum]; } } // If a junction table was used we need to do limit and skip in-memory // This is where it gets nasty, paginated stuff here is a pain and needs work // Hopefully we can get a chance to re-do it in WL2 and not have this. Basically // if you need paginated populates try and have all the tables in the query on the // same connection so it can be done in a nice single query. if (!join.junctionTable) return; if (c.skip) { res[alias].splice(0, c.skip); } if (c.limit) { res[alias] = _.take(res[alias], c.limit); } }); }); returnResults(results); }); function returnResults(results) { if (!results) return cb(null, []); // Normalize results to an array if (!Array.isArray(results) && results) results = [results]; // Unserialize each of the results before attempting any join logic on them var unserializedModels = []; if (results) { results.forEach(function(result) { unserializedModels.push(self._transformer.unserialize(result)); }); } var models = []; var joins = criteria.joins ? criteria.joins : []; var data = new Joins(joins, unserializedModels, self.identity, self._schema.schema, self.offshore.collections); // NOTE: // If a "belongsTo" (i.e. HAS_FK) association is null, should it be transformed into // an empty array here? That is not what is happening currently, and it can cause // unexpected problems when implementing the native join method as an adapter implementor. // ~Mike June 22, 2014 // If `data.models` is invalid (not an array) return early to avoid getting into trouble. if (!data || !data.models || !data.models.forEach) { return cb(new Error('Values returned from operations set are not an array...')); } // Create a model for the top level values data.models.forEach(function(model) { models.push(new self._model(model, data.options)._loadQuery(self._query)); }); cb(null, models); } }); }, where: function() { this.find.apply(this, Array.prototype.slice.call(arguments)); }, select: function() { this.find.apply(this, Array.prototype.slice.call(arguments)); }, /** * findAll * [[ Deprecated! ]] * * @param {Object} criteria * @param {Object} options * @param {Function} cb */ findAll: function(criteria, options, cb) { if (typeof criteria === 'function') { cb = criteria; criteria = null; options = null; } if (typeof options === 'function') { cb = options; options = null; } // Return Deferred or pass to adapter if (typeof cb !== 'function') { return new Deferred(this, this.findAll, criteria); } cb(new Error('In Offshore >= 0.9, findAll() has been deprecated in favor of find().' + '\nPlease visit the migration guide at http://sailsjs.org for help upgrading.')); } };