diff --git a/lib/waterline/adapter/sync/strategies/alter.js b/lib/waterline/adapter/sync/strategies/alter.js index cdfcd3a74..3ecca77f9 100644 --- a/lib/waterline/adapter/sync/strategies/alter.js +++ b/lib/waterline/adapter/sync/strategies/alter.js @@ -40,7 +40,7 @@ module.exports = function(cb) { var collectionName = _.find(self.query.waterline.schema, {tableName: self.collection}).identity; // Create a mapping of column names -> attribute names - var columnNamesMap = _.reduce(self.query.waterline.schema[collectionName].attributes, function(memo, val, key) { + var columnNamesMap = _.reduce(self.query.waterline.schema[collectionName].definition, function(memo, val, key) { // If the attribute has a custom column name, use it as the key for the mapping if (val.columnName) { memo[val.columnName] = key; diff --git a/lib/waterline/query/deferred.js b/lib/waterline/query/deferred.js index 677e68f43..a0547f902 100644 --- a/lib/waterline/query/deferred.js +++ b/lib/waterline/query/deferred.js @@ -105,7 +105,7 @@ Deferred.prototype.populate = function(keyName, criteria) { }); return this; } - + // Normalize sub-criteria try { criteria = normalize.criteria(criteria); @@ -149,32 +149,6 @@ Deferred.prototype.populate = function(keyName, criteria) { ); } - ////////////////////////////////////////////////////////////////////// - // (there has been significant progress made towards both of these /// - // goals-- contact @mikermcneil if you want to help) ///////////////// - ////////////////////////////////////////////////////////////////////// - // TODO: - // Create synonym for `.populate()` syntax using criteria object - // syntax. i.e. instead of using `joins` key in criteria object - // at the app level. - ////////////////////////////////////////////////////////////////////// - // TODO: - // Support Mongoose-style `foo.bar.baz` syntax for nested `populate`s. - // (or something comparable.) - // One solution would be: - // .populate({ - // friends: { - // where: { name: 'mike' }, - // populate: { - // dentist: { - // where: { name: 'rob' } - // } - // } - // } - // }, optionalCriteria ) - //////////////////////////////////////////////////////////////////// - - // Grab the key being populated to check if it is a has many to belongs to // If it's a belongs_to the adapter needs to know that it should replace the foreign key // with the associated value. @@ -187,7 +161,6 @@ Deferred.prototype.populate = function(keyName, criteria) { parentKey: attr.columnName || pk, child: attr.references, childKey: attr.on, - select: Object.keys(this._context.waterline.schema[attr.references].attributes), alias: keyName, removeParentKey: !!parentKey.model, model: !!hasOwnProperty(parentKey, 'model'), @@ -196,16 +169,42 @@ Deferred.prototype.populate = function(keyName, criteria) { // Build select object to use in the integrator var select = []; - Object.keys(this._context.waterline.schema[attr.references].attributes).forEach(function(key) { - var obj = self._context.waterline.schema[attr.references].attributes[key]; - if (!hasOwnProperty(obj, 'columnName')) { + var customSelect = criteria.select && _.isArray(criteria.select); + _.each(this._context.waterline.schema[attr.references].attributes, function(val, key) { + // Ignore virtual attributes + if(_.has(val, 'collection')) { + return; + } + + // Check if the user has defined a custom select and if so normalize it + if(customSelect && !_.includes(criteria.select, key)) { + return; + } + + if (!_.has(val, 'columnName')) { select.push(key); return; } - select.push(obj.columnName); + select.push(val.columnName); + }); + + // Ensure the PK and FK on the child are always selected - otherwise things + // like the integrator won't work correctly + var childPk; + _.each(this._context.waterline.schema[attr.references].attributes, function(val, key) { + if(_.has(val, 'primaryKey') && val.primaryKey) { + childPk = val.columnName || key; + } }); + select.push(childPk); + + // Add the foreign key for collections + if(join.collection) { + select.push(attr.on); + } + join.select = select; var schema = this._context.waterline.schema[attr.references]; @@ -226,19 +225,42 @@ Deferred.prototype.populate = function(keyName, criteria) { // If a junction table is used add an additional join to get the data if (reference && hasOwnProperty(attr, 'on')) { - // Build out the second join object that will link a junction table with the - // values being populated - var selects = _.map(_.keys(this._context.waterline.schema[reference.references].attributes), function(attr) { - var expandedAttr = self._context.waterline.schema[reference.references].attributes[attr]; - return expandedAttr.columnName || attr; + var selects = []; + _.each(this._context.waterline.schema[reference.references].attributes, function(val, key) { + // Ignore virtual attributes + if(_.has(val, 'collection')) { + return; + } + + // Check if the user has defined a custom select and if so normalize it + if(customSelect && !_.includes(criteria.select, key)) { + return; + } + + if (!_.has(val, 'columnName')) { + selects.push(key); + return; + } + + selects.push(val.columnName); + }); + + // Ensure the PK and FK are always selected - otherwise things like the + // integrator won't work correctly + _.each(this._context.waterline.schema[reference.references].attributes, function(val, key) { + if(_.has(val, 'primaryKey') && val.primaryKey) { + childPk = val.columnName || key; + } }); + selects.push(childPk); + join = { parent: attr.references, parentKey: reference.columnName, child: reference.references, childKey: reference.on, - select: selects, + select: _.uniq(selects), alias: keyName, junctionTable: true, removeParentKey: !!parentKey.model, @@ -252,8 +274,10 @@ Deferred.prototype.populate = function(keyName, criteria) { // Append the criteria to the correct join if available if (criteria && joins.length > 1) { joins[1].criteria = criteria; + joins[1].criteria.select = join.select; } else if (criteria) { joins[0].criteria = criteria; + joins[0].criteria.select = join.select; } // Set the criteria joins @@ -270,6 +294,25 @@ Deferred.prototype.populate = function(keyName, criteria) { } }; +/** + * Add projections to the parent + * + * @param {Array} attributes to select + * @return this + */ + +Deferred.prototype.select = function(attributes) { + if(!_.isArray(attributes)) { + attributes = [attributes]; + } + + var select = this._criteria.select || []; + select = select.concat(attributes); + this._criteria.select = _.uniq(select); + + return this; +}; + /** * Add a Where clause to the criteria object * @@ -468,9 +511,8 @@ Deferred.prototype.set = function(values) { */ Deferred.prototype.exec = function(cb) { - if (!cb) { - console.log(new Error('Error: No Callback supplied, you must define a callback.').message); + console.log('Error: No Callback supplied, you must define a callback.'); return; } diff --git a/lib/waterline/query/finders/basic.js b/lib/waterline/query/finders/basic.js index e688693a1..9a4424e93 100644 --- a/lib/waterline/query/finders/basic.js +++ b/lib/waterline/query/finders/basic.js @@ -53,6 +53,17 @@ module.exports = { // 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) { @@ -240,6 +251,17 @@ module.exports = { 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('Waterline can not access transformer-- maybe the context of the method is being overridden?'); @@ -320,7 +342,6 @@ module.exports = { results = waterlineCriteria('parent', { parent: results }, _criteria).results; // Serialize values coming from an in-memory join before modelizing - var _results = []; results.forEach(function(res) { // Go Ahead and perform any sorts on the associated data diff --git a/lib/waterline/query/finders/operations.js b/lib/waterline/query/finders/operations.js index 57af14085..081b67fac 100644 --- a/lib/waterline/query/finders/operations.js +++ b/lib/waterline/query/finders/operations.js @@ -119,8 +119,6 @@ Operations.prototype._seedCache = function _seedCache() { */ Operations.prototype._buildOperations = function _buildOperations() { - - var self = this; var operations = []; // Check if joins were used, if not only a single operation is needed on a single connection @@ -216,7 +214,7 @@ Operations.prototype._stageOperations = function _stageOperations(connections) { // Look into the previous operations and see if this is a child of any of them var child = false; localOpts.forEach(function(localOpt) { - if (localOpt.join.child != join.parent) return; + if (localOpt.join.child !== join.parent) return; localOpt.child = operation; child = true; }); @@ -281,7 +279,6 @@ Operations.prototype._createParentOperation = function _createParentOperation(co // Remove the joins from the criteria object, this will be an in-memory join var tmpCriteria = _.cloneDeep(this.criteria); delete tmpCriteria.joins; - connectionName = this.context.adapterDictionary[this.parent]; // If findOne was used, use the same connection `find` is on. @@ -505,13 +502,11 @@ Operations.prototype._buildChildOpts = function _buildChildOpts(parentResults, c obj[pk] = _.clone(userCriteria.where); userCriteria.where = obj; } - - userCriteria = userCriteria.where; } } - criteria = _.merge(userCriteria, criteria); + criteria = _.merge(userCriteria, { where: criteria }); } // Normalize criteria @@ -670,16 +665,17 @@ Operations.prototype._runChildOperations = function _runChildOperations(intermed if (hasOwnProperty(userCriteria, 'where')) { if (userCriteria.where === undefined) { delete userCriteria.where; - } else { - userCriteria = userCriteria.where; } } delete userCriteria.sort; - criteria = _.extend(criteria, userCriteria); + delete userCriteria.skip; + delete userCriteria.limit; + + criteria = _.merge({}, userCriteria, { where: criteria }); } - criteria = normalize.criteria({ where: criteria }); + criteria = normalize.criteria(criteria); // Empty the cache for the join table so we can only add values used var cacheCopy = _.cloneDeep(self.cache[opt.join.parent]); diff --git a/lib/waterline/utils/normalize.js b/lib/waterline/utils/normalize.js index 9192ea128..bb71f2b4a 100644 --- a/lib/waterline/utils/normalize.js +++ b/lib/waterline/utils/normalize.js @@ -5,7 +5,7 @@ var switchback = require('switchback'); var errorify = require('../error'); var WLUsageError = require('../error/WLUsageError'); -var normalize = module.exports = { +module.exports = { // Expand Primary Key criteria into objects expandPK: function(context, options) { @@ -178,8 +178,22 @@ var normalize = module.exports = { delete criteria.where.max; } - if (hop(criteria, 'where') && criteria.where !== null && hop(criteria.where, 'select')) { - criteria.select = _.clone(criteria.where.select); + if (hop(criteria, 'where') && criteria.where !== null && hop(criteria.where, 'select') || hop(criteria, 'select')) { + + if(criteria.where.select) { + criteria.select = _.clone(criteria.where.select); + } + + // If the select contains a '*' then remove the whole projection, a '*' + // will always return all records. + if(!_.isArray(criteria.select)) { + criteria.select = [criteria.select]; + } + + if(_.includes(criteria.select, '*')) { + delete criteria.select; + } + delete criteria.where.select; } diff --git a/test/unit/adapter/strategy.alter.schema.js b/test/unit/adapter/strategy.alter.schema.js index 1628ee32f..c32ebd964 100644 --- a/test/unit/adapter/strategy.alter.schema.js +++ b/test/unit/adapter/strategy.alter.schema.js @@ -47,7 +47,7 @@ describe('Alter Mode Recovery with an enforced schema', function () { results = _.find(persistentData, options.where); } // Psuedo support for select (needed to act like a real adapter) - if(options.select) { + if(options.select && _.isArray(options.select) && options.select.length) { // Force ID in query options.select.push('id'); @@ -60,7 +60,8 @@ describe('Alter Mode Recovery with an enforced schema', function () { cb(null, results); }, create: function (connectionName, collectionName, data, cb, connection) { - persistentData.push(data); + var schemaData = _.pick(data, ['id', 'name', 'age']); + persistentData.push(schemaData); cb(null, data); }, drop: function (connectionName, collectionName, relations, cb, connection) { @@ -125,4 +126,3 @@ describe('Alter Mode Recovery with an enforced schema', function () { }); }); -