Merge branch 'release/1.7.0' into production

This commit is contained in:
Raymond Feng 2014-07-15 16:11:33 -07:00
commit e592fb0527
12 changed files with 954 additions and 88 deletions

View File

@ -178,8 +178,8 @@ DataAccessObject.create = function (data, callback) {
});
});
}, obj);
}, obj);
}, obj);
}, obj, callback);
}, obj, callback);
}
// for chaining
@ -330,7 +330,7 @@ setRemoting(DataAccessObject.exists, {
description: 'Check whether a model instance exists in the data source',
accepts: {arg: 'id', type: 'any', description: 'Model id', required: true,
http: {source: 'path'}},
returns: {arg: 'exists', type: 'any'},
returns: {arg: 'exists', type: 'boolean'},
http: {verb: 'get', path: '/:id/exists'}
});
@ -1032,8 +1032,8 @@ DataAccessObject.prototype.save = function (options, callback) {
});
});
});
}, data);
}, data);
}, data, callback);
}, data, callback);
}
};
@ -1136,7 +1136,7 @@ DataAccessObject.prototype.remove =
Model.emit('deleted', id);
});
}.bind(this));
});
}, null, cb);
};
/**
@ -1198,7 +1198,8 @@ DataAccessObject.prototype.updateAttributes = function updateAttributes(data, cb
typedData[key] = inst[key];
}
inst._adapter().updateAttributes(model, getIdValue(inst.constructor, inst), inst.constructor._forDB(typedData), function (err) {
inst._adapter().updateAttributes(model, getIdValue(inst.constructor, inst),
inst.constructor._forDB(typedData), function (err) {
done.call(inst, function () {
saveDone.call(inst, function () {
if(cb) cb(err, inst);
@ -1206,8 +1207,8 @@ DataAccessObject.prototype.updateAttributes = function updateAttributes(data, cb
});
});
});
}, data);
}, data);
}, data, cb);
}, data, cb);
}
}, data);
};

View File

@ -221,10 +221,9 @@ DataSource._resolveConnector = function (name, loader) {
var connector = tryModules(names, loader);
var error = null;
if (!connector) {
error = '\nWARNING: LoopBack connector "' + name
+ '" is not installed at any of the locations ' + names
+ '. To fix, run:\n\n npm install '
+ name + '\n';
error = util.format('\nWARNING: LoopBack connector "%s" is not installed ' +
'as any of the following modules:\n\n %s\n\nTo fix, run:\n\n npm install %s\n',
name, names.join('\n'), names[names.length -1]);
}
return {
connector: connector,

View File

@ -27,7 +27,7 @@ Hookable.beforeDestroy = null;
Hookable.afterDestroy = null;
// TODO: Evaluate https://github.com/bnoguchi/hooks-js/
Hookable.prototype.trigger = function trigger(actionName, work, data) {
Hookable.prototype.trigger = function trigger(actionName, work, data, callback) {
var capitalizedName = capitalize(actionName);
var beforeHook = this.constructor["before" + capitalizedName]
|| this.constructor["pre" + capitalizedName];
@ -42,8 +42,13 @@ Hookable.prototype.trigger = function trigger(actionName, work, data) {
// we only call "before" hook when we have actual action (work) to perform
if (work) {
if (beforeHook) {
// before hook should be called on instance with one param: callback
// before hook should be called on instance with two parameters: next and data
beforeHook.call(inst, function () {
// Check arguments to next(err, result)
if (arguments.length) {
return callback && callback.apply(null, arguments);
}
// No err & result is present, proceed with the real work
// actual action also have one param: callback
work.call(inst, next);
}, data);

View File

@ -37,6 +37,7 @@ function ModelBuilder() {
// create blank models pool
this.models = {};
this.definitions = {};
this.defaultModelBaseClass = DefaultModelBaseClass;
}
// Inherit from EventEmitter
@ -131,7 +132,7 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett
}
// Set up the base model class
var ModelBaseClass = parent || DefaultModelBaseClass;
var ModelBaseClass = parent || this.defaultModelBaseClass;
var baseClass = settings.base || settings['super'];
if (baseClass) {
if (isModelClass(baseClass)) {

View File

@ -5,6 +5,7 @@ var assert = require('assert');
var util = require('util');
var i8n = require('inflection');
var defineScope = require('./scope.js').defineScope;
var mergeQuery = require('./scope.js').mergeQuery;
var ModelBaseClass = require('./model.js');
exports.Relation = Relation;
@ -68,6 +69,8 @@ function RelationDefinition(definition) {
this.modelThrough = definition.modelThrough;
this.keyThrough = definition.keyThrough;
this.multiple = (this.type !== 'belongsTo' && this.type !== 'hasOne');
this.properties = definition.properties || {};
this.scope = definition.scope;
}
RelationDefinition.prototype.toJSON = function () {
@ -87,6 +90,41 @@ RelationDefinition.prototype.toJSON = function () {
return json;
};
/**
* Apply the configured scope to the filter/query object.
* @param {Object} modelInstance
* @param {Object} filter (where, order, limit, fields, ...)
*/
RelationDefinition.prototype.applyScope = function(modelInstance, filter) {
if (typeof this.scope === 'function') {
var scope = this.scope.call(this, modelInstance, filter);
if (typeof scope === 'object') {
mergeQuery(filter, scope);
}
} else if (typeof this.scope === 'object') {
mergeQuery(filter, this.scope);
}
};
/**
* Apply the configured properties to the target object.
* @param {Object} modelInstance
* @param {Object} target
*/
RelationDefinition.prototype.applyProperties = function(modelInstance, target) {
if (typeof this.properties === 'function') {
var data = this.properties.call(this, modelInstance);
for(var k in data) {
target[k] = data[k];
}
} else if (typeof this.properties === 'object') {
for(var k in this.properties) {
var key = this.properties[k];
target[key] = modelInstance[k];
}
}
};
/**
* A relation attaching to a given model instance
* @param {RelationDefinition|Object} definition
@ -315,7 +353,7 @@ RelationDefinition.hasMany = function hasMany(modelFrom, modelTo, params) {
var fk = params.foreignKey || i8n.camelize(thisClassName + '_id', true);
var idName = modelFrom.dataSource.idName(modelFrom.modelName) || 'id';
var definition = new RelationDefinition({
name: relationName,
type: RelationTypes.hasMany,
@ -323,9 +361,11 @@ RelationDefinition.hasMany = function hasMany(modelFrom, modelTo, params) {
keyFrom: idName,
keyTo: fk,
modelTo: modelTo,
multiple: true
multiple: true,
properties: params.properties,
scope: params.scope
});
if (params.through) {
definition.modelThrough = params.through;
var keyThrough = definition.throughKey || i8n.camelize(modelTo.modelName + '_id', true);
@ -341,23 +381,99 @@ RelationDefinition.hasMany = function hasMany(modelFrom, modelTo, params) {
var scopeMethods = {
findById: scopeMethod(definition, 'findById'),
destroy: scopeMethod(definition, 'destroyById')
}
destroy: scopeMethod(definition, 'destroyById'),
updateById: scopeMethod(definition, 'updateById'),
exists: scopeMethod(definition, 'exists')
};
var findByIdFunc = scopeMethods.findById;
findByIdFunc.shared = true;
findByIdFunc.http = {verb: 'get', path: '/' + relationName + '/:fk'};
findByIdFunc.accepts = {arg: 'fk', type: 'any',
description: 'Foreign key for ' + relationName, required: true,
http: {source: 'path'}};
findByIdFunc.description = 'Find a related item by id for ' + relationName;
findByIdFunc.returns = {arg: 'result', type: 'object', root: true};
modelFrom.prototype['__findById__' + relationName] = findByIdFunc;
var destroyByIdFunc = scopeMethods.destroy;
destroyByIdFunc.shared = true;
destroyByIdFunc.http = {verb: 'delete', path: '/' + relationName + '/:fk'};
destroyByIdFunc.accepts = {arg: 'fk', type: 'any',
description: 'Foreign key for ' + relationName, required: true,
http: {source: 'path'}};
destroyByIdFunc.description = 'Delete a related item by id for ' + relationName;
destroyByIdFunc.returns = {};
modelFrom.prototype['__destroyById__' + relationName] = destroyByIdFunc;
var updateByIdFunc = scopeMethods.updateById;
updateByIdFunc.shared = true;
updateByIdFunc.http = {verb: 'put', path: '/' + relationName + '/:fk'};
updateByIdFunc.accepts = {arg: 'fk', type: 'any',
description: 'Foreign key for ' + relationName, required: true,
http: {source: 'path'}};
updateByIdFunc.description = 'Update a related item by id for ' + relationName;
updateByIdFunc.returns = {arg: 'result', type: 'object', root: true};
modelFrom.prototype['__updateById__' + relationName] = updateByIdFunc;
if(definition.modelThrough) {
scopeMethods.create = scopeMethod(definition, 'create');
scopeMethods.add = scopeMethod(definition, 'add');
scopeMethods.remove = scopeMethod(definition, 'remove');
var addFunc = scopeMethods.add;
addFunc.shared = true;
addFunc.http = {verb: 'put', path: '/' + relationName + '/rel/:fk'};
addFunc.accepts = {arg: 'fk', type: 'any',
description: 'Foreign key for ' + relationName, required: true,
http: {source: 'path'}};
addFunc.description = 'Add a related item by id for ' + relationName;
addFunc.returns = {arg: relationName, type: 'object', root: true};
modelFrom.prototype['__link__' + relationName] = addFunc;
var removeFunc = scopeMethods.remove;
removeFunc.shared = true;
removeFunc.http = {verb: 'delete', path: '/' + relationName + '/rel/:fk'};
removeFunc.accepts = {arg: 'fk', type: 'any',
description: 'Foreign key for ' + relationName, required: true,
http: {source: 'path'}};
removeFunc.description = 'Remove the ' + relationName + ' relation to an item by id';
removeFunc.returns = {};
modelFrom.prototype['__unlink__' + relationName] = removeFunc;
// FIXME: [rfeng] How to map a function with callback(err, true|false) to HEAD?
// true --> 200 and false --> 404?
/*
var existsFunc = scopeMethods.exists;
existsFunc.shared = true;
existsFunc.http = {verb: 'head', path: '/' + relationName + '/rel/:fk'};
existsFunc.accepts = {arg: 'fk', type: 'any',
description: 'Foreign key for ' + relationName, required: true,
http: {source: 'path'}};
existsFunc.description = 'Check the existence of ' + relationName + ' relation to an item by id';
existsFunc.returns = {};
modelFrom.prototype['__exists__' + relationName] = existsFunc;
*/
} else {
scopeMethods.create = scopeMethod(definition, 'create');
scopeMethods.build = scopeMethod(definition, 'build');
}
// Mix the property and scoped methods into the prototype class
defineScope(modelFrom.prototype, params.through || modelTo, relationName, function () {
var filter = {};
filter.where = {};
filter.where[fk] = this[idName];
definition.applyScope(this, filter);
if (params.through) {
filter.collect = i8n.camelize(modelTo.modelName, true);
filter.include = filter.collect;
@ -368,61 +484,191 @@ RelationDefinition.hasMany = function hasMany(modelFrom, modelTo, params) {
};
function scopeMethod(definition, methodName) {
var relationClass = RelationClasses[definition.type];
if (definition.type === RelationTypes.hasMany && definition.modelThrough) {
relationClass = RelationClasses.hasManyThrough;
}
var method = function () {
var relationClass = RelationClasses[definition.type];
if (definition.type === RelationTypes.hasMany && definition.modelThrough) {
relationClass = RelationClasses.hasManyThrough;
}
var relation = new relationClass(definition, this);
return relation[methodName].apply(relation, arguments);
};
var relationMethod = relationClass.prototype[methodName];
if (relationMethod.shared) {
method.shared = true;
method.accepts = relationMethod.accepts;
method.returns = relationMethod.returns;
method.http = relationMethod.http;
method.description = relationMethod.description;
}
return method;
}
HasMany.prototype.findById = function (id, cb) {
/**
* Find a related item by foreign key
* @param {*} fkId The foreign key
* @param {Function} cb The callback function
*/
HasMany.prototype.findById = function (fkId, cb) {
var modelTo = this.definition.modelTo;
var fk = this.definition.keyTo;
var pk = this.definition.keyFrom;
var modelInstance = this.modelInstance;
modelTo.findById(id, function (err, inst) {
var idName = this.definition.modelTo.definition.idName();
var filter = {};
filter.where = {};
filter.where[idName] = fkId;
filter.where[fk] = modelInstance[pk];
this.definition.applyScope(modelInstance, filter);
modelTo.findOne(filter, function (err, inst) {
if (err) {
return cb(err);
}
if (!inst) {
return cb(new Error('Not found'));
err = new Error('No instance with id ' + fkId + ' found for ' + modelTo.modelName);
err.statusCode = 404;
return cb(err);
}
// Check if the foreign key matches the primary key
if (inst[fk] && inst[fk].toString() === modelInstance[pk].toString()) {
cb(null, inst);
} else {
cb(new Error('Permission denied: foreign key does not match the primary key'));
err = new Error('Key mismatch: ' + this.definition.modelFrom.modelName + '.' + pk
+ ': ' + modelInstance[pk]
+ ', ' + modelTo.modelName + '.' + fk + ': ' + inst[fk]);
err.statusCode = 400;
cb(err);
}
});
};
HasMany.prototype.destroyById = function (id, cb) {
var self = this;
/**
* Find a related item by foreign key
* @param {*} fkId The foreign key
* @param {Function} cb The callback function
*/
HasMany.prototype.exists = function (fkId, cb) {
var modelTo = this.definition.modelTo;
var fk = this.definition.keyTo;
var pk = this.definition.keyFrom;
var modelInstance = this.modelInstance;
modelTo.findById(id, function (err, inst) {
modelTo.findById(fkId, function (err, inst) {
if (err) {
return cb(err);
}
if (!inst) {
return cb(new Error('Not found'));
return cb(null, false);
}
// Check if the foreign key matches the primary key
if (inst[fk] && inst[fk].toString() === modelInstance[pk].toString()) {
self.removeFromCache(inst[fk]);
inst.destroy(cb);
cb(null, true);
} else {
cb(new Error('Permission denied: foreign key does not match the primary key'));
cb(null, false);
}
});
};
/**
* Update a related item by foreign key
* @param {*} fkId The foreign key
* @param {Function} cb The callback function
*/
HasMany.prototype.updateById = function (fkId, data, cb) {
this.findById(fkId, function (err, inst) {
if (err) {
return cb && cb(err);
}
inst.updateAttributes(data, cb);
});
};
/**
* Delete a related item by foreign key
* @param {*} fkId The foreign key
* @param {Function} cb The callback function
*/
HasMany.prototype.destroyById = function (fkId, cb) {
var self = this;
this.findById(fkId, function(err, inst) {
if (err) {
return cb(err);
}
self.removeFromCache(inst[fkId]);
inst.destroy(cb);
});
};
/**
* Find a related item by foreign key
* @param {*} fkId The foreign key value
* @param {Function} cb The callback function
*/
HasManyThrough.prototype.findById = function (fkId, cb) {
var self = this;
var modelTo = this.definition.modelTo;
var pk = this.definition.keyFrom;
var modelInstance = this.modelInstance;
var modelThrough = this.definition.modelThrough;
self.exists(fkId, function (err, exists) {
if (err || !exists) {
if (!err) {
err = new Error('No relation found in ' + modelThrough.modelName
+ ' for (' + self.definition.modelFrom.modelName + '.' + modelInstance[pk]
+ ',' + modelTo.modelName + '.' + fkId + ')');
err.statusCode = 404;
}
return cb(err);
}
modelTo.findById(fkId, function (err, inst) {
if (err) {
return cb(err);
}
if (!inst) {
err = new Error('No instance with id ' + fkId + ' found for ' + modelTo.modelName);
err.statusCode = 404;
return cb(err);
}
cb(err, inst);
});
});
};
/**
* Delete a related item by foreign key
* @param {*} fkId The foreign key
* @param {Function} cb The callback function
*/
HasManyThrough.prototype.destroyById = function (fkId, cb) {
var self = this;
var modelTo = this.definition.modelTo;
var pk = this.definition.keyFrom;
var modelInstance = this.modelInstance;
var modelThrough = this.definition.modelThrough;
self.exists(fkId, function (err, exists) {
if (err || !exists) {
if (!err) {
err = new Error('No record found in ' + modelThrough.modelName
+ ' for (' + self.definition.modelFrom.modelName + '.' + modelInstance[pk]
+ ' ,' + modelTo.modelName + '.' + fkId + ')');
err.statusCode = 404;
}
return cb(err);
}
self.remove(fkId, function(err) {
if(err) {
return cb(err);
}
modelTo.deleteById(fkId, cb);
});
});
};
// Create an instance of the target model and connect it to the instance of
// the source model by creating an instance of the through model
HasManyThrough.prototype.create = function create(data, done) {
@ -451,6 +697,9 @@ HasManyThrough.prototype.create = function create(data, done) {
var d = {};
d[fk1] = modelInstance[definition.keyFrom];
d[fk2] = to[pk2];
definition.applyProperties(modelInstance, d);
// Then create the through model
modelThrough.create(d, function (e, through) {
if (e) {
@ -486,15 +735,20 @@ HasManyThrough.prototype.add = function (acInst, done) {
var pk2 = definition.modelTo.definition.idName();
var fk2 = findBelongsTo(modelThrough, definition.modelTo, pk2);
query[fk1] = this.modelInstance[pk1];
query[fk2] = acInst[pk2] || acInst;
query[fk2] = (acInst instanceof definition.modelTo) ? acInst[pk2] : acInst;
var filter = { where: query };
definition.applyScope(this.modelInstance, filter);
data[fk1] = this.modelInstance[pk1];
data[fk2] = acInst[pk2] || acInst;
data[fk2] = (acInst instanceof definition.modelTo) ? acInst[pk2] : acInst;
definition.applyProperties(this.modelInstance, data);
// Create an instance of the through model
modelThrough.findOrCreate({where: query}, data, function(err, ac) {
modelThrough.findOrCreate(filter, data, function(err, ac) {
if(!err) {
if (acInst instanceof definition.modelTo) {
self.addToCache(acInst);
@ -504,6 +758,38 @@ HasManyThrough.prototype.add = function (acInst, done) {
});
};
/**
* Check if the target model instance is related to the 'hasMany' relation
* @param {Object|ID} acInst The actual instance or id value
*/
HasManyThrough.prototype.exists = function (acInst, done) {
var definition = this.definition;
var modelThrough = definition.modelThrough;
var pk1 = definition.keyFrom;
var data = {};
var query = {};
var fk1 = findBelongsTo(modelThrough, definition.modelFrom,
definition.keyFrom);
// The primary key for the target model
var pk2 = definition.modelTo.definition.idName();
var fk2 = findBelongsTo(modelThrough, definition.modelTo, pk2);
query[fk1] = this.modelInstance[pk1];
query[fk2] = (acInst instanceof definition.modelTo) ? acInst[pk2] : acInst;
data[fk1] = this.modelInstance[pk1];
data[fk2] = (acInst instanceof definition.modelTo) ? acInst[pk2] : acInst;
modelThrough.count(query, function(err, ac) {
done(err, ac > 0);
});
};
/**
* Remove the target model instance from the 'hasMany' relation
* @param {Object|ID) acInst The actual instance or id value
@ -525,9 +811,13 @@ HasManyThrough.prototype.remove = function (acInst, done) {
var fk2 = findBelongsTo(modelThrough, definition.modelTo, pk2);
query[fk1] = this.modelInstance[pk1];
query[fk2] = acInst[pk2] || acInst;
query[fk2] = (acInst instanceof definition.modelTo) ? acInst[pk2] : acInst;
var filter = { where: query };
definition.applyScope(this.modelInstance, filter);
modelThrough.deleteAll(query, function (err) {
modelThrough.deleteAll(filter.where, function (err) {
if (!err) {
self.removeFromCache(query[fk2]);
}
@ -575,7 +865,7 @@ RelationDefinition.belongsTo = function (modelFrom, modelTo, params) {
var idName = modelFrom.dataSource.idName(modelTo.modelName) || 'id';
var relationName = params.as || i8n.camelize(modelTo.modelName, true);
var fk = params.foreignKey || relationName + 'Id';
var relationDef = modelFrom.relations[relationName] = new RelationDefinition({
name: relationName,
type: RelationTypes.belongsTo,
@ -616,7 +906,6 @@ RelationDefinition.belongsTo = function (modelFrom, modelTo, params) {
fn.returns = {arg: relationName, type: 'object', root: true};
modelFrom.prototype['__get__' + relationName] = fn;
};
BelongsTo.prototype.create = function(targetModelData, cb) {
@ -664,7 +953,7 @@ BelongsTo.prototype.related = function (refresh, params) {
var pk = this.definition.keyTo;
var fk = this.definition.keyFrom;
var modelInstance = this.modelInstance;
if (arguments.length === 1) {
params = refresh;
refresh = false;
@ -694,7 +983,11 @@ BelongsTo.prototype.related = function (refresh, params) {
self.resetCache(inst);
cb(null, inst);
} else {
cb(new Error('Permission denied: foreign key does not match the primary key'));
err = new Error('Key mismatch: ' + self.definition.modelFrom.modelName + '.' + fk
+ ': ' + modelInstance[fk]
+ ', ' + modelTo.modelName + '.' + pk + ': ' + inst[pk]);
err.statusCode = 400;
cb(err);
}
});
return modelInstance[fk];
@ -788,14 +1081,15 @@ RelationDefinition.hasOne = function (modelFrom, modelTo, params) {
var relationName = params.as || i8n.camelize(modelTo.modelName, true);
var fk = params.foreignKey || i8n.camelize(modelFrom.modelName + '_id', true);
var relationDef = modelFrom.relations[relationName] = new RelationDefinition({
name: relationName,
type: RelationTypes.hasOne,
modelFrom: modelFrom,
keyFrom: pk,
keyTo: fk,
modelTo: modelTo
modelTo: modelTo,
properties: params.properties
});
modelFrom.dataSource.defineForeignKey(modelTo.modelName, fk, modelFrom.modelName);
@ -835,7 +1129,11 @@ HasOne.prototype.create = function (targetModelData, cb) {
targetModelData = targetModelData || {};
targetModelData[fk] = modelInstance[pk];
var query = {where: {}};
query.where[fk] = targetModelData[fk]
query.where[fk] = targetModelData[fk];
this.definition.applyScope(modelInstance, query);
this.definition.applyProperties(modelInstance, targetModelData);
modelTo.findOne(query, function(err, result) {
if(err) {
cb(err);
@ -869,13 +1167,16 @@ HasMany.prototype.create = function (targetModelData, cb) {
var fk = this.definition.keyTo;
var pk = this.definition.keyFrom;
var modelInstance = this.modelInstance;
if (typeof targetModelData === 'function' && !cb) {
cb = targetModelData;
targetModelData = {};
}
targetModelData = targetModelData || {};
targetModelData[fk] = modelInstance[pk];
this.definition.applyProperties(modelInstance, targetModelData);
modelTo.create(targetModelData, function(err, targetModel) {
if(!err) {
// Refresh the cache
@ -895,8 +1196,12 @@ HasMany.prototype.build = HasOne.prototype.build = function(targetModelData) {
var modelTo = this.definition.modelTo;
var pk = this.definition.keyFrom;
var fk = this.definition.keyTo;
targetModelData = targetModelData || {};
targetModelData[fk] = this.modelInstance[pk];
this.definition.applyProperties(this.modelInstance, targetModelData);
return new modelTo(targetModelData);
};
@ -916,6 +1221,7 @@ HasOne.prototype.related = function (refresh, params) {
var modelTo = this.definition.modelTo;
var fk = this.definition.keyTo;
var pk = this.definition.keyFrom;
var definition = this.definition;
var modelInstance = this.modelInstance;
if (arguments.length === 1) {
@ -937,6 +1243,7 @@ HasOne.prototype.related = function (refresh, params) {
if (cachedValue === undefined) {
var query = {where: {}};
query.where[fk] = modelInstance[pk];
definition.applyScope(modelInstance, query);
modelTo.findOne(query, function (err, inst) {
if (err) {
return cb(err);
@ -949,7 +1256,11 @@ HasOne.prototype.related = function (refresh, params) {
self.resetCache(inst);
cb(null, inst);
} else {
cb(new Error('Permission denied'));
err = new Error('Key mismatch: ' + self.definition.modelFrom.modelName + '.' + pk
+ ': ' + modelInstance[pk]
+ ', ' + modelTo.modelName + '.' + fk + ': ' + inst[fk]);
err.statusCode = 400;
cb(err);
}
});
return modelInstance[pk];

View File

@ -5,6 +5,7 @@ var defineCachedRelations = utils.defineCachedRelations;
* Module exports
*/
exports.defineScope = defineScope;
exports.mergeQuery = mergeQuery;
function ScopeDefinition(definition) {
this.sourceModel = definition.sourceModel;

View File

@ -46,6 +46,20 @@ function Validatable() {
*/
Validatable.validatesPresenceOf = getConfigurator('presence');
/**
* Validate absence of one or more specified properties.
* A model should not include a property to be considered valid; fails when validated field not blank.
*
* For example, validate absence of reserved
* ```
* Post.validatesAbsenceOf('reserved', { unless: 'special' });
*
* @param {String} propertyName One or more property names.
* @options {Object} errMsg Optional custom error message. Default is "can't be set"
* @property {String} message Error message to use instead of default.
*/
Validatable.validatesAbsenceOf = getConfigurator('absence');
/**
* Validate length. Require a property length to be within a specified range.
* Three kinds of validations: min, max, is.
@ -225,6 +239,15 @@ function validatePresence(attr, conf, err) {
}
}
/*!
* Absence validator
*/
function validateAbsence(attr, conf, err) {
if (!blank(this[attr])) {
err();
}
}
/*!
* Length validator
*/
@ -305,6 +328,9 @@ function validateCustom(attr, conf, err, done) {
* Uniqueness validator
*/
function validateUniqueness(attr, conf, err, done) {
if (blank(this[attr])) {
return process.nextTick(done);
}
var cond = {where: {}};
cond.where[attr] = this[attr];
@ -331,6 +357,7 @@ function validateUniqueness(attr, conf, err, done) {
var validators = {
presence: validatePresence,
absence: validateAbsence,
length: validateLength,
numericality: validateNumericality,
inclusion: validateInclusion,
@ -389,7 +416,7 @@ Validatable.prototype.isValid = function (callback, data) {
validationsDone.call(inst, function () {
callback(valid);
});
});
}, data, callback);
}
return valid;
}
@ -440,7 +467,7 @@ Validatable.prototype.isValid = function (callback, data) {
}
}
}, data);
}, data, callback);
if (async) {
// in case of async validation we should return undefined here,
@ -469,8 +496,11 @@ function validationFailed(inst, v, cb) {
// here we should check skip validation conditions (if, unless)
// that can be specified in conf
if (skipValidation(inst, conf, 'if')) return false;
if (skipValidation(inst, conf, 'unless')) return false;
if (skipValidation(inst, conf, 'if')
|| skipValidation(inst, conf, 'unless')) {
if (cb) cb(true);
return false;
}
var fail = false;
var validator = validators[conf.validation];
@ -478,7 +508,7 @@ function validationFailed(inst, v, cb) {
validatorArguments.push(attr);
validatorArguments.push(conf);
validatorArguments.push(function onerror(kind) {
var message, code = conf.validation;
var message, code = conf.code || conf.validation;
if (conf.message) {
message = conf.message;
}
@ -499,7 +529,7 @@ function validationFailed(inst, v, cb) {
message = 'is invalid';
}
}
inst.errors.add(attr, message, code);
if (kind !== false) inst.errors.add(attr, message, code);
fail = true;
});
if (cb) {
@ -532,6 +562,7 @@ function skipValidation(inst, conf, kind) {
var defaultMessages = {
presence: 'can\'t be blank',
absence: 'can\'t be set',
length: {
min: 'too short',
max: 'too long',

View File

@ -1,6 +1,6 @@
{
"name": "loopback-datasource-juggler",
"version": "1.6.3",
"version": "1.7.0",
"description": "LoopBack DataSoure Juggler",
"keywords": [
"StrongLoop",

View File

@ -81,7 +81,27 @@ describe('hooks', function () {
}
User.afterCreate = function () {
throw new Error('shouldn\'t be called')
throw new Error('shouldn\'t be called');
};
User.create(function (err, user) {
User.dataSource.connector.create = old;
done();
});
});
it('afterCreate should not be triggered on failed beforeCreate', function (done) {
User.beforeCreate = function (next, data) {
// Skip next()
next(new Error('fail in beforeCreate'));
};
var old = User.dataSource.connector.create;
User.dataSource.connector.create = function (modelName, id, cb) {
throw new Error('shouldn\'t be called');
}
User.afterCreate = function () {
throw new Error('shouldn\'t be called');
};
User.create(function (err, user) {
User.dataSource.connector.create = old;
@ -173,6 +193,18 @@ describe('hooks', function () {
});
});
it('beforeSave should be able to skip next', function (done) {
User.create(function (err, user) {
User.beforeSave = function (next, data) {
next(null, 'XYZ');
};
user.save(function(err, result) {
result.should.be.eql('XYZ');
done();
});
});
});
});
describe('update', function () {
@ -221,7 +253,7 @@ describe('hooks', function () {
it('should not trigger after-hook on failed save', function (done) {
User.afterUpdate = function () {
should.fail('afterUpdate shouldn\'t be called')
should.fail('afterUpdate shouldn\'t be called');
};
User.create(function (err, user) {
var save = User.dataSource.connector.save;

View File

@ -1285,6 +1285,39 @@ describe('Load models from json', function () {
}
});
it('should allow customization of default model base class', function () {
var modelBuilder = new ModelBuilder();
var User = modelBuilder.define('User', {
name: String,
bio: ModelBuilder.Text,
approved: Boolean,
joinedAt: Date,
age: Number
});
modelBuilder.defaultModelBaseClass = User;
var Customer = modelBuilder.define('Customer', {customerId: {type: String, id: true}});
assert(Customer.prototype instanceof User);
});
it('should allow model base class', function () {
var modelBuilder = new ModelBuilder();
var User = modelBuilder.define('User', {
name: String,
bio: ModelBuilder.Text,
approved: Boolean,
joinedAt: Date,
age: Number
});
var Customer = modelBuilder.define('Customer',
{customerId: {type: String, id: true}}, {}, User);
assert(Customer.prototype instanceof User);
});
it('should be able to extend models', function (done) {
var modelBuilder = new ModelBuilder();

View File

@ -1,32 +1,31 @@
// This test written in mocha+should.js
var should = require('./init.js');
var db, Book, Chapter, Author, Reader, Publisher;
var db, Book, Chapter, Author, Reader;
var Category, Product;
describe('relations', function () {
before(function (done) {
db = getSchema();
Book = db.define('Book', {name: String});
Chapter = db.define('Chapter', {name: {type: String, index: true}});
Author = db.define('Author', {name: String});
Reader = db.define('Reader', {name: String});
db.automigrate(function () {
Book.destroyAll(function () {
Chapter.destroyAll(function () {
Author.destroyAll(function () {
Reader.destroyAll(done);
describe('hasMany', function () {
before(function (done) {
db = getSchema();
Book = db.define('Book', {name: String, type: String});
Chapter = db.define('Chapter', {name: {type: String, index: true},
bookType: String});
Author = db.define('Author', {name: String});
Reader = db.define('Reader', {name: String});
db.automigrate(function () {
Book.destroyAll(function () {
Chapter.destroyAll(function () {
Author.destroyAll(function () {
Reader.destroyAll(done);
});
});
});
});
});
});
after(function () {
// db.disconnect();
});
describe('hasMany', function () {
it('can be declared in different ways', function (done) {
Book.hasMany(Chapter);
Book.hasMany(Reader, {as: 'users'});
@ -73,12 +72,12 @@ describe('relations', function () {
book.chapters.create({name: 'a'}, function () {
book.chapters.create({name: 'z'}, function () {
book.chapters.create({name: 'c'}, function () {
fetch(book);
verify(book);
});
});
});
});
function fetch(book) {
function verify(book) {
book.chapters(function (err, ch) {
should.not.exist(err);
should.exist(ch);
@ -102,14 +101,170 @@ describe('relations', function () {
id = ch.id;
book.chapters.create({name: 'z'}, function () {
book.chapters.create({name: 'c'}, function () {
fetch(book);
verify(book);
});
});
});
});
function fetch(book) {
function verify(book) {
book.chapters.findById(id, function (err, ch) {
should.not.exist(err);
should.exist(ch);
ch.id.should.eql(id);
done();
});
}
});
it('should set targetClass on scope property', function() {
should.equal(Book.prototype.chapters._targetClass, 'Chapter');
});
it('should update scoped record', function (done) {
var id;
Book.create(function (err, book) {
book.chapters.create({name: 'a'}, function (err, ch) {
id = ch.id;
book.chapters.updateById(id, {name: 'aa'}, function(err, ch) {
verify(book);
});
});
});
function verify(book) {
book.chapters.findById(id, function (err, ch) {
should.not.exist(err);
should.exist(ch);
ch.id.should.eql(id);
ch.name.should.equal('aa');
done();
});
}
});
it('should destroy scoped record', function (done) {
var id;
Book.create(function (err, book) {
book.chapters.create({name: 'a'}, function (err, ch) {
id = ch.id;
book.chapters.destroy(id, function(err, ch) {
verify(book);
});
});
});
function verify(book) {
book.chapters.findById(id, function (err, ch) {
should.exist(err);
done();
});
}
});
it('should check existence of a scoped record', function (done) {
var id;
Book.create(function (err, book) {
book.chapters.create({name: 'a'}, function (err, ch) {
id = ch.id;
book.chapters.create({name: 'z'}, function () {
book.chapters.create({name: 'c'}, function () {
verify(book);
});
});
});
});
function verify(book) {
book.chapters.exists(id, function (err, flag) {
should.not.exist(err);
flag.should.be.eql(true);
done();
});
}
});
});
describe('hasMany through', function () {
var Physician, Patient, Appointment;
before(function (done) {
db = getSchema();
Physician = db.define('Physician', {name: String});
Patient = db.define('Patient', {name: String});
Appointment = db.define('Appointment', {date: {type: Date,
default: function () {
return new Date();
}}});
Physician.hasMany(Patient, {through: Appointment});
Patient.hasMany(Physician, {through: Appointment});
Appointment.belongsTo(Patient);
Appointment.belongsTo(Physician);
db.automigrate(['Physician', 'Patient', 'Appointment'], function (err) {
done(err);
});
});
it('should build record on scope', function (done) {
Physician.create(function (err, physician) {
var patient = physician.patients.build();
patient.physicianId.should.equal(physician.id);
patient.save(done);
});
});
it('should create record on scope', function (done) {
Physician.create(function (err, physician) {
physician.patients.create(function (err, patient) {
should.not.exist(err);
should.exist(patient);
Appointment.find({where: {physicianId: physician.id, patientId: patient.id}},
function(err, apps) {
should.not.exist(err);
apps.should.have.lengthOf(1);
done();
});
});
});
});
it('should fetch all scoped instances', function (done) {
Physician.create(function (err, physician) {
physician.patients.create({name: 'a'}, function () {
physician.patients.create({name: 'z'}, function () {
physician.patients.create({name: 'c'}, function () {
verify(physician);
});
});
});
});
function verify(physician) {
physician.patients(function (err, ch) {
should.not.exist(err);
should.exist(ch);
ch.should.have.lengthOf(3);
done();
});
}
});
it('should find scoped record', function (done) {
var id;
Physician.create(function (err, physician) {
physician.patients.create({name: 'a'}, function (err, ch) {
id = ch.id;
physician.patients.create({name: 'z'}, function () {
physician.patients.create({name: 'c'}, function () {
verify(physician);
});
});
});
});
function verify(physician) {
physician.patients.findById(id, function (err, ch) {
should.not.exist(err);
should.exist(ch);
ch.id.should.equal(id);
@ -119,8 +274,230 @@ describe('relations', function () {
});
it('should set targetClass on scope property', function() {
should.equal(Book.prototype.chapters._targetClass, 'Chapter');
should.equal(Physician.prototype.patients._targetClass, 'Patient');
});
it('should update scoped record', function (done) {
var id;
Physician.create(function (err, physician) {
physician.patients.create({name: 'a'}, function (err, ch) {
id = ch.id;
physician.patients.updateById(id, {name: 'aa'}, function(err, ch) {
verify(physician);
});
});
});
function verify(physician) {
physician.patients.findById(id, function (err, ch) {
should.not.exist(err);
should.exist(ch);
ch.id.should.equal(id);
ch.name.should.equal('aa');
done();
});
}
});
it('should destroy scoped record', function (done) {
var id;
Physician.create(function (err, physician) {
physician.patients.create({name: 'a'}, function (err, ch) {
id = ch.id;
physician.patients.destroy(id, function(err, ch) {
verify(physician);
});
});
});
function verify(physician) {
physician.patients.findById(id, function (err, ch) {
should.exist(err);
done();
});
}
});
it('should check existence of a scoped record', function (done) {
var id;
Physician.create(function (err, physician) {
physician.patients.create({name: 'a'}, function (err, ch) {
id = ch.id;
physician.patients.create({name: 'z'}, function () {
physician.patients.create({name: 'c'}, function () {
verify(physician);
});
});
});
});
function verify(physician) {
physician.patients.exists(id, function (err, flag) {
should.not.exist(err);
flag.should.be.eql(true);
done();
});
}
});
it('should allow to add connection with instance', function (done) {
Physician.create({name: 'ph1'}, function (e, physician) {
Patient.create({name: 'pa1'}, function (e, patient) {
physician.patients.add(patient, function (e, app) {
should.not.exist(e);
should.exist(app);
app.should.be.an.instanceOf(Appointment);
app.physicianId.should.equal(physician.id);
app.patientId.should.equal(patient.id);
done();
});
});
});
});
it('should allow to remove connection with instance', function (done) {
var id;
Physician.create(function (err, physician) {
physician.patients.create({name: 'a'}, function (err, patient) {
id = patient.id;
physician.patients.remove(id, function (err, ch) {
verify(physician);
});
});
});
function verify(physician) {
physician.patients.exists(id, function (err, flag) {
should.not.exist(err);
flag.should.be.eql(false);
done();
});
}
});
beforeEach(function (done) {
Appointment.destroyAll(function (err) {
Physician.destroyAll(function (err) {
Patient.destroyAll(done);
});
});
});
});
describe('hasMany with properties', function () {
it('can be declared with properties', function (done) {
Book.hasMany(Chapter, { properties: { type: 'bookType' } });
db.automigrate(done);
});
it('should create record on scope', function (done) {
Book.create({ type: 'fiction' }, function (err, book) {
book.chapters.create(function (err, c) {
should.not.exist(err);
should.exist(c);
c.bookId.should.equal(book.id);
c.bookType.should.equal('fiction');
done();
});
});
});
});
describe('hasMany with scope', function () {
it('can be declared with properties', function (done) {
db = getSchema();
Category = db.define('Category', {name: String, productType: String});
Product = db.define('Product', {name: String, type: String});
Category.hasMany(Product, {
properties: function(inst) {
if (!inst.productType) return; // skip
return { type: inst.productType };
},
scope: function(inst, filter) {
var m = this.properties(inst); // re-use properties
if (m) return { where: m };
}
});
db.automigrate(done);
});
it('should create record on scope', function (done) {
Category.create(function (err, c) {
c.products.create({ type: 'book' }, function(err, p) {
p.categoryId.should.equal(c.id);
p.type.should.equal('book');
c.products.create({ type: 'widget' }, function(err, p) {
p.categoryId.should.equal(c.id);
p.type.should.equal('widget');
done();
});
});
});
});
it('should find record on scope', function (done) {
Category.findOne(function (err, c) {
c.products(function(err, products) {
products.should.have.length(2);
done();
});
});
});
it('should find record on scope - filtered', function (done) {
Category.findOne(function (err, c) {
c.products({ where: { type: 'book' } }, function(err, products) {
products.should.have.length(1);
products[0].type.should.equal('book');
done();
});
});
});
// So why not just do the above? In LoopBack, the context
// that gets passed into a beforeRemote handler contains
// a reference to the parent scope/instance: ctx.instance
// in order to enforce a (dynamic scope) at runtime
// a temporary property can be set in the beforeRemoting
// handler. Optionally,properties dynamic properties can be declared.
//
// The code below simulates this.
it('should create record on scope - properties', function (done) {
Category.findOne(function (err, c) {
c.productType = 'tool'; // temporary
c.products.create(function(err, p) {
p.categoryId.should.equal(c.id);
p.type.should.equal('tool');
done();
});
});
});
it('should find record on scope - scoped', function (done) {
Category.findOne(function (err, c) {
c.productType = 'book'; // temporary, for scoping
c.products(function(err, products) {
products.should.have.length(1);
products[0].type.should.equal('book');
done();
});
});
});
it('should find record on scope - scoped', function (done) {
Category.findOne(function (err, c) {
c.productType = 'tool'; // temporary, for scoping
c.products(function(err, products) {
products.should.have.length(1);
products[0].type.should.equal('tool');
done();
});
});
});
});
describe('belongsTo', function () {
@ -186,11 +563,11 @@ describe('relations', function () {
before(function () {
db = getSchema();
Supplier = db.define('Supplier', {name: String});
Account = db.define('Account', {accountNo: String});
Account = db.define('Account', {accountNo: String, supplierName: String});
});
it('can be declared using hasOne method', function () {
Supplier.hasOne(Account);
Supplier.hasOne(Account, { properties: { name: 'supplierName' } });
Object.keys((new Account()).toObject()).should.include('supplierId');
(new Supplier()).account.should.be.an.instanceOf(Function);
});
@ -207,6 +584,7 @@ describe('relations', function () {
should.exist(act);
act.should.be.an.instanceOf(Account);
supplier.account().id.should.equal(act.id);
act.supplierName.should.equal(supplier.name);
done();
});
});

View File

@ -159,6 +159,20 @@ describe('validations', function () {
});
});
describe('absence', function () {
it('should validate absence', function () {
User.validatesAbsenceOf('reserved', { if: 'locked' });
var u = new User({reserved: 'foo', locked: true});
u.isValid().should.not.be.true;
u.reserved = null;
u.isValid().should.be.true;
var u = new User({reserved: 'foo', locked: false});
u.isValid().should.be.true;
});
});
describe('uniqueness', function () {
it('should validate uniqueness', function (done) {
@ -227,6 +241,33 @@ describe('validations', function () {
done(err);
});
});
it('should skip blank values', function (done) {
User.validatesUniquenessOf('email');
var u = new User({email: ' '});
Boolean(u.isValid(function (valid) {
valid.should.be.true;
u.save(function () {
var u2 = new User({email: null});
u2.isValid(function (valid) {
valid.should.be.true;
done();
});
});
})).should.be.false;
});
it('should work with if/unless', function (done) {
User.validatesUniquenessOf('email', {
if: function() { return true; },
unless: function() { return false; }
});
var u = new User({email: 'hello'});
Boolean(u.isValid(function (valid) {
valid.should.be.true;
done();
})).should.be.false;
});
});
describe('format', function () {
@ -251,7 +292,40 @@ describe('validations', function () {
});
describe('custom', function () {
it('should validate using custom sync validation');
it('should validate using custom async validation');
it('should validate using custom sync validation', function() {
User.validate('email', function (err) {
if (this.email === 'hello') err();
}, { code: 'invalid-email' });
var u = new User({email: 'hello'});
Boolean(u.isValid()).should.be.false;
u.errors.codes.should.eql({ email: ['invalid-email'] });
});
it('should validate and return detailed error messages', function() {
User.validate('global', function (err) {
if (this.email === 'hello' || this.email === 'hey') {
this.errors.add('email', 'Cannot be `' + this.email + '`', 'invalid-email');
err(false); // false: prevent global error message
}
});
var u = new User({email: 'hello'});
Boolean(u.isValid()).should.be.false;
u.errors.should.eql({ email: ['Cannot be `hello`'] });
u.errors.codes.should.eql({ email: ['invalid-email'] });
});
it('should validate using custom async validation', function(done) {
User.validateAsync('email', function (err, next) {
process.nextTick(next);
}, {
if: function() { return true; },
unless: function() { return false; }
});
var u = new User({email: 'hello'});
Boolean(u.isValid(function (valid) {
valid.should.be.true;
done();
})).should.be.false;
});
});
});