water-orm
Version:
A monolith version of Standalone waterline ORM
618 lines (496 loc) • 16.4 kB
JavaScript
/**
* Deferred Object
*
* Used for building up a Query
*/
var util = require('util');
var Promise = require('bluebird');
var _ = require('lodash');
var normalize = require('../utils/normalize');
var utils = require('../utils/helpers');
var acyclicTraversal = require('../utils/acyclicTraversal');
var hasOwnProperty = utils.object.hasOwnProperty;
// Alias "catch" as "fail", for backwards compatibility with projects
// that were created using Q
Promise.prototype.fail = Promise.prototype.catch;
var Deferred = module.exports = function(context, method, criteria, values) {
if (!context) return new Error('Must supply a context to a new Deferred object. Usage: new Deferred(context, method, criteria)');
if (!method) return new Error('Must supply a method to a new Deferred object. Usage: new Deferred(context, method, criteria)');
this._context = context;
this._method = method;
this._criteria = criteria;
this._values = values || null;
this._deferred = null; // deferred object for promises
return this;
};
/**
* Add join clause(s) to the criteria object to populate
* the specified alias all the way down (or at least until a
* circular pattern is detected.)
*
* @param {String} keyName [the initial alias aka named relation]
* @param {Object} criteria [optional]
* @return this
* @chainable
*
* WARNING:
* This method is not finished yet!!
*/
Deferred.prototype.populateDeep = function(keyName, criteria) {
// The identity of the initial model
var identity = this._context.identity;
// The input schema
var schema = this._context.waterline.schema;
// Kick off recursive function to traverse the schema graph.
var plan = acyclicTraversal(schema, identity, keyName);
// TODO: convert populate plan into a join plan
// this._criteria.joins = ....
// TODO: also merge criteria object into query
return this;
};
/**
* Populate all associations of a collection.
*
* @return this
* @chainable
*/
Deferred.prototype.populateAll = function(criteria) {
var self = this;
this._context.associations.forEach(function(association) {
self.populate(association.alias, criteria);
});
return this;
};
/**
* Add a `joins` clause to the criteria object.
*
* Used for populating associations.
*
* @param {String|Array} key, the key to populate or array of string keys
* @return this
* @chainable
*/
Deferred.prototype.populate = function(keyName, criteria) {
var self = this;
var joins = [];
var pk = 'id';
var attr;
var join;
// Adds support for arrays into keyName so that a list of
// populates can be passed
if (_.isArray(keyName)) {
keyName.forEach(function(populate) {
self.populate(populate, criteria);
});
return this;
}
// Normalize sub-criteria
try {
criteria = normalize.criteria(criteria);
////////////////////////////////////////////////////////////////////////
// TODO:
// instead of doing this, target the relevant pieces of code
// with weird expectations and teach them a lesson
// e.g. `lib/waterline/query/finders/operations.js:665:12`
// (delete userCriteria.sort)
//
// Except make sure `where` exists
criteria.where = criteria.where === false ? false : (criteria.where || {});
////////////////////////////////////////////////////////////////////////
} catch (e) {
throw new Error(
'Could not parse sub-criteria passed to ' +
util.format('`.populate("%s")`', keyName) +
'\nSub-criteria:\n' + util.inspect(criteria, false, null) +
'\nDetails:\n' + util.inspect(e, false, null)
);
}
try {
// Set the attr value to the generated schema attribute
attr = this._context.waterline.schema[this._context.identity].attributes[keyName];
// Get the current collection's primary key attribute
Object.keys(this._context._attributes).forEach(function(key) {
if (hasOwnProperty(self._context._attributes[key], 'primaryKey') && self._context._attributes[key].primaryKey) {
pk = self._context._attributes[key].columnName || key;
}
});
if (!attr) {
throw new Error(
'In ' + util.format('`.populate("%s")`', keyName) +
', attempting to populate an attribute that doesn\'t exist'
);
}
// 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.
var parentKey = this._context.waterline.collections[this._context.identity].attributes[keyName];
// Build the initial join object that will link this collection to either another collection
// or to a junction table.
join = {
parent: this._context.identity,
parentKey: attr.columnName || pk,
child: attr.references,
childKey: attr.on,
alias: keyName,
removeParentKey: !!parentKey.model,
model: !!hasOwnProperty(parentKey, 'model'),
collection: !!hasOwnProperty(parentKey, 'collection')
};
// Build select object to use in the integrator
var select = [];
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(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];
var reference = null;
// If linking to a junction table the attributes shouldn't be included in the return value
if (schema.junctionTable) {
join.select = false;
reference = _.find(schema.attributes, function(attribute) {
return attribute.references && attribute.columnName !== attr.on;
});
} else if (schema.throughTable && schema.throughTable[self._context.identity + '.' + keyName]) {
join.select = false;
reference = schema.attributes[schema.throughTable[self._context.identity + '.' + keyName]];
}
joins.push(join);
// If a junction table is used add an additional join to get the data
if (reference && hasOwnProperty(attr, 'on')) {
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: _.uniq(selects),
alias: keyName,
junctionTable: true,
removeParentKey: !!parentKey.model,
model: false,
collection: true
};
joins.push(join);
}
// 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
this._criteria.joins = Array.prototype.concat(this._criteria.joins || [], joins);
return this;
} catch (e) {
throw new Error(
'Encountered unexpected error while building join instructions for ' +
util.format('`.populate("%s")`', keyName) +
'\nDetails:\n' +
util.inspect(e, false, null)
);
}
};
/**
* 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
*
* @param {Object} criteria to append
* @return this
*/
Deferred.prototype.where = function(criteria) {
if (!criteria) return this;
// 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};
}
// Normalize criteria
criteria = normalize.criteria(criteria);
// Wipe out the existing WHERE clause if the specified criteria ends up `false`
// (since neither could match anything)
if (criteria === false) {
this._criteria = false;
}
if (!criteria || !criteria.where) return this;
if (!this._criteria) this._criteria = {};
var where = this._criteria.where || {};
// Merge with existing WHERE clause
Object.keys(criteria.where).forEach(function(key) {
where[key] = criteria.where[key];
});
this._criteria.where = where;
return this;
};
/**
* Add a Limit clause to the criteria object
*
* @param {Integer} number to limit
* @return this
*/
Deferred.prototype.limit = function(limit) {
this._criteria.limit = limit;
return this;
};
/**
* Add a Skip clause to the criteria object
*
* @param {Integer} number to skip
* @return this
*/
Deferred.prototype.skip = function(skip) {
this._criteria.skip = skip;
return this;
};
/**
* Add a Paginate clause to the criteria object
*
* This is syntatical sugar that calls skip and
* limit from a single function.
*
* @param {Object} page and limit
* @return this
*/
Deferred.prototype.paginate = function(options) {
var defaultLimit = 10;
if (options === undefined) options = { page: 0, limit: defaultLimit };
var page = options.page || 0;
var limit = options.limit || defaultLimit;
var skip = 0;
if (page > 0 && limit === 0) skip = page - 1;
if (page > 0 && limit > 0) skip = (page * limit) - limit;
this
.skip(skip)
.limit(limit);
return this;
};
/**
* Add a groupBy clause to the criteria object
*
* @param {Array|Arguments} Keys to group by
* @return this
*/
Deferred.prototype.groupBy = function() {
buildAggregate.call(this, 'groupBy', Array.prototype.slice.call(arguments));
return this;
};
/**
* Add a Sort clause to the criteria object
*
* @param {String|Object} key and order
* @return this
*/
Deferred.prototype.sort = function(criteria) {
if (!criteria) return this;
// Normalize criteria
criteria = normalize.criteria({ sort: criteria });
var sort = this._criteria.sort || {};
Object.keys(criteria.sort).forEach(function(key) {
sort[key] = criteria.sort[key];
});
this._criteria.sort = sort;
return this;
};
/**
* Add a Sum clause to the criteria object
*
* @param {Array|Arguments} Keys to sum over
* @return this
*/
Deferred.prototype.sum = function() {
buildAggregate.call(this, 'sum', Array.prototype.slice.call(arguments));
return this;
};
/**
* Add an Average clause to the criteria object
*
* @param {Array|Arguments} Keys to average over
* @return this
*/
Deferred.prototype.average = function() {
buildAggregate.call(this, 'average', Array.prototype.slice.call(arguments));
return this;
};
/**
* Add a min clause to the criteria object
*
* @param {Array|Arguments} Keys to min over
* @return this
*/
Deferred.prototype.min = function() {
buildAggregate.call(this, 'min', Array.prototype.slice.call(arguments));
return this;
};
/**
* Add a min clause to the criteria object
*
* @param {Array|Arguments} Keys to min over
* @return this
*/
Deferred.prototype.max = function() {
buildAggregate.call(this, 'max', Array.prototype.slice.call(arguments));
return this;
};
/**
* Add values to be used in update or create query
*
* @param {Object, Array} values
* @return this
*/
Deferred.prototype.set = function(values) {
this._values = values;
return this;
};
/**
* Pass metadata down to the adapter that won't be processed or touched by Waterline
*/
Deferred.prototype.meta = function(data) {
this._meta = data;
return this;
};
/**
* Execute a Query using the method passed into the
* constuctor.
*
* @param {Function} callback
* @return callback with parameters (err, results)
*/
Deferred.prototype.exec = function(cb) {
if (_.isUndefined(cb)) {
console.log(
'Error: No callback supplied. Please define a callback function when executing a query. '+
'See http://sailsjs.com/docs/reference/waterline-orm/queries/exec for help.'
);
return;
}
var isValidCb = _.isFunction(cb) || (_.isObject(cb) && !_.isArray(cb));
if (!isValidCb) {
console.log(
'Error: Sorry, `.exec()` doesn\'t know how to handle a callback like that:\n'+
util.inspect(cb, {depth: null})+'\n'+
'Instead, please provide a callback function when executing a query. '+
'See http://sailsjs.com/docs/reference/waterline-orm/queries/exec for help.'
);
return;
}
// Otherwise, the provided callback function is pretty cool, and all is right and well.
// Normalize callback/switchback
cb = normalize.callback(cb);
// Set up arguments + callback
var args = [this._criteria, cb];
if (this._values) args.splice(1, 0, this._values);
// If there is a meta value, throw it on the very end
if(this._meta) {
args.push(this._meta);
}
// Pass control to the adapter with the appropriate arguments.
this._method.apply(this._context, args);
};
/**
* Executes a Query, and returns a promise
*/
Deferred.prototype.toPromise = function() {
if (!this._deferred) {
this._deferred = Promise.promisify(this.exec).bind(this)();
}
return this._deferred;
};
/**
* Executes a Query, and returns a promise that applies cb/ec to the
* result/error.
*/
Deferred.prototype.then = function(cb, ec) {
return this.toPromise().then(cb, ec);
};
/**
* Applies results to function fn.apply, and returns a promise
*/
Deferred.prototype.spread = function(cb) {
return this.toPromise().spread(cb);
};
/**
* returns a promise and gets resolved with error
*/
Deferred.prototype.catch = function(cb) {
return this.toPromise().catch(cb);
};
/**
* Alias "catch" as "fail"
*/
Deferred.prototype.fail = Deferred.prototype.catch;
/**
* Build An Aggregate Criteria Option
*
* @param {String} key
* @api private
*/
function buildAggregate(key, args) {
// If passed in a list, set that as the min criteria
if (args[0] instanceof Array) {
args = args[0];
}
this._criteria[key] = args || {};
}