Merge branch 'master' into 2.0
This commit is contained in:
commit
fc710ca55a
|
@ -5,8 +5,8 @@ for interacting with databases, REST APIs, and other data sources. It was
|
||||||
initially forked from [JugglingDB](https://github.com/1602/jugglingdb).
|
initially forked from [JugglingDB](https://github.com/1602/jugglingdb).
|
||||||
|
|
||||||
**For full documentation, see the official StrongLoop documentation**:
|
**For full documentation, see the official StrongLoop documentation**:
|
||||||
* [Data sources and connectors](http://docs.strongloop.com/display/DOC/Data+sources+and+connectors)
|
* [Data sources and connectors](http://docs.strongloop.com/display/LB/Data+sources+and+connectors)
|
||||||
* [Data Source Juggler](http://docs.strongloop.com/display/DOC/Data+Source+Juggler).
|
* [Creating data sources and connected models](http://docs.strongloop.com/display/LB/Creating+data+sources+and+connected+models).
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
|
|
@ -270,6 +270,12 @@ Memory.prototype.all = function all(model, filter, callback) {
|
||||||
}.bind(this));
|
}.bind(this));
|
||||||
|
|
||||||
if (filter) {
|
if (filter) {
|
||||||
|
if (!filter.order) {
|
||||||
|
var idNames = this.idNames(model);
|
||||||
|
if (idNames && idNames.length) {
|
||||||
|
filter.order = idNames;
|
||||||
|
}
|
||||||
|
}
|
||||||
// do we need some sorting?
|
// do we need some sorting?
|
||||||
if (filter.order) {
|
if (filter.order) {
|
||||||
var orders = filter.order;
|
var orders = filter.order;
|
||||||
|
|
139
lib/dao.js
139
lib/dao.js
|
@ -180,8 +180,8 @@ DataAccessObject.create = function (data, callback) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}, obj);
|
}, obj);
|
||||||
}, obj);
|
}, obj, callback);
|
||||||
}, obj);
|
}, obj, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
// for chaining
|
// for chaining
|
||||||
|
@ -201,7 +201,10 @@ function stillConnecting(dataSource, obj, args) {
|
||||||
* @param {Object} data The model instance data
|
* @param {Object} data The model instance data
|
||||||
* @param {Function} callback The callback function (optional).
|
* @param {Function} callback The callback function (optional).
|
||||||
*/
|
*/
|
||||||
DataAccessObject.upsert = DataAccessObject.updateOrCreate = function upsert(data, callback) {
|
// [FIXME] rfeng: This is a hack to set up 'upsert' first so that
|
||||||
|
// 'upsert' will be used as the name for strong-remoting to keep it backward
|
||||||
|
// compatible for angular SDK
|
||||||
|
DataAccessObject.updateOrCreate = DataAccessObject.upsert = function upsert(data, callback) {
|
||||||
if (stillConnecting(this.getDataSource(), this, arguments)) {
|
if (stillConnecting(this.getDataSource(), this, arguments)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -386,6 +389,48 @@ DataAccessObject._normalize = function (filter) {
|
||||||
filter.skip = offset;
|
filter.skip = offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (filter.order) {
|
||||||
|
var order = filter.order;
|
||||||
|
if (!Array.isArray(order)) {
|
||||||
|
order = [order];
|
||||||
|
}
|
||||||
|
var fields = [];
|
||||||
|
for (var i = 0, m = order.length; i < m; i++) {
|
||||||
|
if (typeof order[i] === 'string') {
|
||||||
|
// Normalize 'f1 ASC, f2 DESC, f3' to ['f1 ASC', 'f2 DESC', 'f3']
|
||||||
|
var tokens = order[i].split(/(?:\s*,\s*)+/);
|
||||||
|
for (var t = 0, n = tokens.length; t < n; t++) {
|
||||||
|
var token = tokens[t];
|
||||||
|
if (token.length === 0) {
|
||||||
|
// Skip empty token
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var parts = token.split(/\s+/);
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
var dir = parts[1].toUpperCase();
|
||||||
|
if (dir === 'ASC' || dir === 'DESC') {
|
||||||
|
token = parts[0] + ' ' + dir;
|
||||||
|
} else {
|
||||||
|
err = new Error(util.format('The order %j has invalid direction', token));
|
||||||
|
err.statusCode = 400;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fields.push(token);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = new Error(util.format('The order %j is not valid', order[i]));
|
||||||
|
err.statusCode = 400;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fields.length === 1 && typeof filter.order === 'string') {
|
||||||
|
filter.order = fields[0];
|
||||||
|
} else {
|
||||||
|
filter.order = fields;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// normalize fields as array of included property names
|
// normalize fields as array of included property names
|
||||||
if (filter.fields) {
|
if (filter.fields) {
|
||||||
filter.fields = fieldsToArray(filter.fields,
|
filter.fields = fieldsToArray(filter.fields,
|
||||||
|
@ -397,6 +442,25 @@ DataAccessObject._normalize = function (filter) {
|
||||||
return filter;
|
return filter;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function DateType(arg) {
|
||||||
|
return new Date(arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
function BooleanType(val) {
|
||||||
|
if (val === 'true') {
|
||||||
|
return true;
|
||||||
|
} else if (val === 'false') {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return Boolean(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function NumberType(val) {
|
||||||
|
var num = Number(val);
|
||||||
|
return !isNaN(num) ? num : val;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Coerce values based the property types
|
* Coerce values based the property types
|
||||||
* @param {Object} where The where clause
|
* @param {Object} where The where clause
|
||||||
|
@ -422,11 +486,11 @@ DataAccessObject._coerce = function (where) {
|
||||||
if (p === 'and' || p === 'or' || p === 'nor') {
|
if (p === 'and' || p === 'or' || p === 'nor') {
|
||||||
var clauses = where[p];
|
var clauses = where[p];
|
||||||
if (Array.isArray(clauses)) {
|
if (Array.isArray(clauses)) {
|
||||||
for (var i = 0; i < clauses.length; i++) {
|
for (var k = 0; k < clauses.length; k++) {
|
||||||
self._coerce(clauses[i]);
|
self._coerce(clauses[k]);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
err = new Error(util.format('The %p operator has invalid clauses %j', p, clauses));
|
err = new Error(util.format('The %s operator has invalid clauses %j', p, clauses));
|
||||||
err.statusCode = 400;
|
err.statusCode = 400;
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
@ -440,30 +504,16 @@ DataAccessObject._coerce = function (where) {
|
||||||
DataType = DataType[0];
|
DataType = DataType[0];
|
||||||
}
|
}
|
||||||
if (DataType === Date) {
|
if (DataType === Date) {
|
||||||
var OrigDate = Date;
|
DataType = DateType;
|
||||||
DataType = function Date(arg) {
|
|
||||||
return new OrigDate(arg);
|
|
||||||
};
|
|
||||||
} else if (DataType === Boolean) {
|
} else if (DataType === Boolean) {
|
||||||
DataType = function (val) {
|
DataType = BooleanType;
|
||||||
if (val === 'true') {
|
|
||||||
return true;
|
|
||||||
} else if (val === 'false') {
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
return Boolean(val);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} else if (DataType === Number) {
|
} else if (DataType === Number) {
|
||||||
// This fixes a regression in mongodb connector
|
// This fixes a regression in mongodb connector
|
||||||
// For numbers, only convert it produces a valid number
|
// For numbers, only convert it produces a valid number
|
||||||
// LoopBack by default injects a number id. We should fix it based
|
// LoopBack by default injects a number id. We should fix it based
|
||||||
// on the connector's input, for example, MongoDB should use string
|
// on the connector's input, for example, MongoDB should use string
|
||||||
// while RDBs typically use number
|
// while RDBs typically use number
|
||||||
DataType = function (val) {
|
DataType = NumberType;
|
||||||
var num = Number(val);
|
|
||||||
return !isNaN(num) ? num : val;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!DataType) {
|
if (!DataType) {
|
||||||
|
@ -495,6 +545,31 @@ DataAccessObject._coerce = function (where) {
|
||||||
if (op in val) {
|
if (op in val) {
|
||||||
val = val[op];
|
val = val[op];
|
||||||
operator = op;
|
operator = op;
|
||||||
|
switch(operator) {
|
||||||
|
case 'inq':
|
||||||
|
case 'nin':
|
||||||
|
if (!Array.isArray(val)) {
|
||||||
|
err = new Error(util.format('The %s property has invalid clause %j', p, where[p]));
|
||||||
|
err.statusCode = 400;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'between':
|
||||||
|
if (!Array.isArray(val) || val.length !== 2) {
|
||||||
|
err = new Error(util.format('The %s property has invalid clause %j', p, where[p]));
|
||||||
|
err.statusCode = 400;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'like':
|
||||||
|
case 'nlike':
|
||||||
|
if (!(typeof val === 'string' || val instanceof RegExp)) {
|
||||||
|
err = new Error(util.format('The %s property has invalid clause %j', p, where[p]));
|
||||||
|
err.statusCode = 400;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -758,7 +833,10 @@ DataAccessObject.remove = DataAccessObject.deleteAll = DataAccessObject.destroyA
|
||||||
* @param {Function} cb Callback called with (err)
|
* @param {Function} cb Callback called with (err)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
DataAccessObject.removeById = DataAccessObject.deleteById = DataAccessObject.destroyById = function deleteById(id, cb) {
|
// [FIXME] rfeng: This is a hack to set up 'deleteById' first so that
|
||||||
|
// 'deleteById' will be used as the name for strong-remoting to keep it backward
|
||||||
|
// compatible for angular SDK
|
||||||
|
DataAccessObject.removeById = DataAccessObject.destroyById = DataAccessObject.deleteById = function deleteById(id, cb) {
|
||||||
if (stillConnecting(this.getDataSource(), this, arguments)) return;
|
if (stillConnecting(this.getDataSource(), this, arguments)) return;
|
||||||
var Model = this;
|
var Model = this;
|
||||||
|
|
||||||
|
@ -874,8 +952,8 @@ DataAccessObject.prototype.save = function (options, callback) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}, data);
|
}, data, callback);
|
||||||
}, data);
|
}, data, callback);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -966,7 +1044,7 @@ DataAccessObject.prototype.remove =
|
||||||
Model.emit('deleted', id);
|
Model.emit('deleted', id);
|
||||||
});
|
});
|
||||||
}.bind(this));
|
}.bind(this));
|
||||||
});
|
}, null, cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1028,7 +1106,8 @@ DataAccessObject.prototype.updateAttributes = function updateAttributes(data, cb
|
||||||
typedData[key] = inst[key];
|
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 () {
|
done.call(inst, function () {
|
||||||
saveDone.call(inst, function () {
|
saveDone.call(inst, function () {
|
||||||
if(cb) cb(err, inst);
|
if(cb) cb(err, inst);
|
||||||
|
@ -1036,8 +1115,8 @@ DataAccessObject.prototype.updateAttributes = function updateAttributes(data, cb
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}, data);
|
}, data, cb);
|
||||||
}, data);
|
}, data, cb);
|
||||||
}
|
}
|
||||||
}, data);
|
}, data);
|
||||||
};
|
};
|
||||||
|
|
|
@ -221,10 +221,9 @@ DataSource._resolveConnector = function (name, loader) {
|
||||||
var connector = tryModules(names, loader);
|
var connector = tryModules(names, loader);
|
||||||
var error = null;
|
var error = null;
|
||||||
if (!connector) {
|
if (!connector) {
|
||||||
error = '\nWARNING: LoopBack connector "' + name
|
error = util.format('\nWARNING: LoopBack connector "%s" is not installed ' +
|
||||||
+ '" is not installed at any of the locations ' + names
|
'as any of the following modules:\n\n %s\n\nTo fix, run:\n\n npm install %s\n',
|
||||||
+ '. To fix, run:\n\n npm install '
|
name, names.join('\n'), names[names.length -1]);
|
||||||
+ name + '\n';
|
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
connector: connector,
|
connector: connector,
|
||||||
|
@ -364,7 +363,7 @@ function isModelClass(cls) {
|
||||||
return cls.prototype instanceof ModelBaseClass;
|
return cls.prototype instanceof ModelBaseClass;
|
||||||
}
|
}
|
||||||
|
|
||||||
DataSource.relationTypes = ['belongsTo', 'hasMany', 'hasAndBelongsToMany'];
|
DataSource.relationTypes = ['belongsTo', 'hasMany', 'hasAndBelongsToMany', 'hasOne'];
|
||||||
|
|
||||||
function isModelDataSourceAttached(model) {
|
function isModelDataSourceAttached(model) {
|
||||||
return model && (!model.settings.unresolved) && (model.dataSource instanceof DataSource);
|
return model && (!model.settings.unresolved) && (model.dataSource instanceof DataSource);
|
||||||
|
@ -1579,22 +1578,26 @@ DataSource.prototype.idProperty = function (modelName) {
|
||||||
* @param {String} foreignClassName The foreign model name
|
* @param {String} foreignClassName The foreign model name
|
||||||
*/
|
*/
|
||||||
DataSource.prototype.defineForeignKey = function defineForeignKey(className, key, foreignClassName) {
|
DataSource.prototype.defineForeignKey = function defineForeignKey(className, key, foreignClassName) {
|
||||||
// quit if key already defined
|
var pkType = null;
|
||||||
if (this.getModelDefinition(className).rawProperties[key]) return;
|
|
||||||
|
|
||||||
var defaultType = Number;
|
|
||||||
if (foreignClassName) {
|
|
||||||
var foreignModel = this.getModelDefinition(foreignClassName);
|
var foreignModel = this.getModelDefinition(foreignClassName);
|
||||||
var pkName = foreignModel && foreignModel.idName();
|
var pkName = foreignModel && foreignModel.idName();
|
||||||
if (pkName) {
|
if (pkName) {
|
||||||
defaultType = foreignModel.properties[pkName].type;
|
pkType = foreignModel.properties[pkName].type;
|
||||||
}
|
}
|
||||||
|
var model = this.getModelDefinition(className);
|
||||||
|
if (model.properties[key]) {
|
||||||
|
if (pkType) {
|
||||||
|
// Reset the type of the foreign key
|
||||||
|
model.rawProperties[key].type = model.properties[key].type = pkType;
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.connector.defineForeignKey) {
|
if (this.connector.defineForeignKey) {
|
||||||
var cb = function (err, keyType) {
|
var cb = function (err, keyType) {
|
||||||
if (err) throw err;
|
if (err) throw err;
|
||||||
// Add the foreign key property to the data source _models
|
// Add the foreign key property to the data source _models
|
||||||
this.defineProperty(className, key, {type: keyType || defaultType});
|
this.defineProperty(className, key, {type: keyType || pkType});
|
||||||
}.bind(this);
|
}.bind(this);
|
||||||
switch (this.connector.defineForeignKey.length) {
|
switch (this.connector.defineForeignKey.length) {
|
||||||
case 4:
|
case 4:
|
||||||
|
@ -1607,7 +1610,7 @@ DataSource.prototype.defineForeignKey = function defineForeignKey(className, key
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Add the foreign key property to the data source _models
|
// Add the foreign key property to the data source _models
|
||||||
this.defineProperty(className, key, {type: defaultType});
|
this.defineProperty(className, key, {type: pkType});
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -27,7 +27,7 @@ Hookable.beforeDestroy = null;
|
||||||
Hookable.afterDestroy = null;
|
Hookable.afterDestroy = null;
|
||||||
|
|
||||||
// TODO: Evaluate https://github.com/bnoguchi/hooks-js/
|
// 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 capitalizedName = capitalize(actionName);
|
||||||
var beforeHook = this.constructor["before" + capitalizedName]
|
var beforeHook = this.constructor["before" + capitalizedName]
|
||||||
|| this.constructor["pre" + 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
|
// we only call "before" hook when we have actual action (work) to perform
|
||||||
if (work) {
|
if (work) {
|
||||||
if (beforeHook) {
|
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 () {
|
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
|
// actual action also have one param: callback
|
||||||
work.call(inst, next);
|
work.call(inst, next);
|
||||||
}, data);
|
}, data);
|
||||||
|
|
|
@ -37,6 +37,7 @@ function ModelBuilder() {
|
||||||
// create blank models pool
|
// create blank models pool
|
||||||
this.models = {};
|
this.models = {};
|
||||||
this.definitions = {};
|
this.definitions = {};
|
||||||
|
this.defaultModelBaseClass = DefaultModelBaseClass;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inherit from EventEmitter
|
// Inherit from EventEmitter
|
||||||
|
@ -131,7 +132,7 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up the base model class
|
// Set up the base model class
|
||||||
var ModelBaseClass = parent || DefaultModelBaseClass;
|
var ModelBaseClass = parent || this.defaultModelBaseClass;
|
||||||
var baseClass = settings.base || settings['super'];
|
var baseClass = settings.base || settings['super'];
|
||||||
if (baseClass) {
|
if (baseClass) {
|
||||||
if (isModelClass(baseClass)) {
|
if (isModelClass(baseClass)) {
|
||||||
|
|
|
@ -5,6 +5,7 @@ var assert = require('assert');
|
||||||
var util = require('util');
|
var util = require('util');
|
||||||
var i8n = require('inflection');
|
var i8n = require('inflection');
|
||||||
var defineScope = require('./scope.js').defineScope;
|
var defineScope = require('./scope.js').defineScope;
|
||||||
|
var mergeQuery = require('./scope.js').mergeQuery;
|
||||||
var ModelBaseClass = require('./model.js');
|
var ModelBaseClass = require('./model.js');
|
||||||
|
|
||||||
exports.Relation = Relation;
|
exports.Relation = Relation;
|
||||||
|
@ -68,6 +69,8 @@ function RelationDefinition(definition) {
|
||||||
this.modelThrough = definition.modelThrough;
|
this.modelThrough = definition.modelThrough;
|
||||||
this.keyThrough = definition.keyThrough;
|
this.keyThrough = definition.keyThrough;
|
||||||
this.multiple = (this.type !== 'belongsTo' && this.type !== 'hasOne');
|
this.multiple = (this.type !== 'belongsTo' && this.type !== 'hasOne');
|
||||||
|
this.properties = definition.properties || {};
|
||||||
|
this.scope = definition.scope;
|
||||||
}
|
}
|
||||||
|
|
||||||
RelationDefinition.prototype.toJSON = function () {
|
RelationDefinition.prototype.toJSON = function () {
|
||||||
|
@ -87,6 +90,41 @@ RelationDefinition.prototype.toJSON = function () {
|
||||||
return json;
|
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
|
* A relation attaching to a given model instance
|
||||||
* @param {RelationDefinition|Object} definition
|
* @param {RelationDefinition|Object} definition
|
||||||
|
@ -323,7 +361,9 @@ RelationDefinition.hasMany = function hasMany(modelFrom, modelTo, params) {
|
||||||
keyFrom: idName,
|
keyFrom: idName,
|
||||||
keyTo: fk,
|
keyTo: fk,
|
||||||
modelTo: modelTo,
|
modelTo: modelTo,
|
||||||
multiple: true
|
multiple: true,
|
||||||
|
properties: params.properties,
|
||||||
|
scope: params.scope
|
||||||
});
|
});
|
||||||
|
|
||||||
if (params.through) {
|
if (params.through) {
|
||||||
|
@ -336,18 +376,94 @@ RelationDefinition.hasMany = function hasMany(modelFrom, modelTo, params) {
|
||||||
|
|
||||||
if (!params.through) {
|
if (!params.through) {
|
||||||
// obviously, modelTo should have attribute called `fk`
|
// obviously, modelTo should have attribute called `fk`
|
||||||
modelTo.dataSource.defineForeignKey(modelTo.modelName, fk, this.modelName);
|
modelTo.dataSource.defineForeignKey(modelTo.modelName, fk, modelFrom.modelName);
|
||||||
}
|
}
|
||||||
|
|
||||||
var scopeMethods = {
|
var scopeMethods = {
|
||||||
findById: scopeMethod(definition, 'findById'),
|
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) {
|
if(definition.modelThrough) {
|
||||||
scopeMethods.create = scopeMethod(definition, 'create');
|
scopeMethods.create = scopeMethod(definition, 'create');
|
||||||
scopeMethods.add = scopeMethod(definition, 'add');
|
scopeMethods.add = scopeMethod(definition, 'add');
|
||||||
scopeMethods.remove = scopeMethod(definition, 'remove');
|
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
|
// Mix the property and scoped methods into the prototype class
|
||||||
|
@ -355,6 +471,9 @@ RelationDefinition.hasMany = function hasMany(modelFrom, modelTo, params) {
|
||||||
var filter = {};
|
var filter = {};
|
||||||
filter.where = {};
|
filter.where = {};
|
||||||
filter.where[fk] = this[idName];
|
filter.where[fk] = this[idName];
|
||||||
|
|
||||||
|
definition.applyScope(this, filter);
|
||||||
|
|
||||||
if (params.through) {
|
if (params.through) {
|
||||||
filter.collect = i8n.camelize(modelTo.modelName, true);
|
filter.collect = i8n.camelize(modelTo.modelName, true);
|
||||||
filter.include = filter.collect;
|
filter.include = filter.collect;
|
||||||
|
@ -365,61 +484,191 @@ RelationDefinition.hasMany = function hasMany(modelFrom, modelTo, params) {
|
||||||
};
|
};
|
||||||
|
|
||||||
function scopeMethod(definition, methodName) {
|
function scopeMethod(definition, methodName) {
|
||||||
var method = function () {
|
|
||||||
var relationClass = RelationClasses[definition.type];
|
var relationClass = RelationClasses[definition.type];
|
||||||
if (definition.type === RelationTypes.hasMany && definition.modelThrough) {
|
if (definition.type === RelationTypes.hasMany && definition.modelThrough) {
|
||||||
relationClass = RelationClasses.hasManyThrough;
|
relationClass = RelationClasses.hasManyThrough;
|
||||||
}
|
}
|
||||||
|
var method = function () {
|
||||||
var relation = new relationClass(definition, this);
|
var relation = new relationClass(definition, this);
|
||||||
return relation[methodName].apply(relation, arguments);
|
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;
|
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 modelTo = this.definition.modelTo;
|
||||||
var fk = this.definition.keyTo;
|
var fk = this.definition.keyTo;
|
||||||
var pk = this.definition.keyFrom;
|
var pk = this.definition.keyFrom;
|
||||||
var modelInstance = this.modelInstance;
|
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) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
if (!inst) {
|
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
|
// Check if the foreign key matches the primary key
|
||||||
if (inst[fk] && inst[fk].toString() === modelInstance[pk].toString()) {
|
if (inst[fk] && inst[fk].toString() === modelInstance[pk].toString()) {
|
||||||
cb(null, inst);
|
cb(null, inst);
|
||||||
} else {
|
} 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 modelTo = this.definition.modelTo;
|
||||||
var fk = this.definition.keyTo;
|
var fk = this.definition.keyTo;
|
||||||
var pk = this.definition.keyFrom;
|
var pk = this.definition.keyFrom;
|
||||||
var modelInstance = this.modelInstance;
|
var modelInstance = this.modelInstance;
|
||||||
modelTo.findById(id, function (err, inst) {
|
|
||||||
|
modelTo.findById(fkId, function (err, inst) {
|
||||||
if (err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
if (!inst) {
|
if (!inst) {
|
||||||
return cb(new Error('Not found'));
|
return cb(null, false);
|
||||||
}
|
}
|
||||||
// Check if the foreign key matches the primary key
|
// Check if the foreign key matches the primary key
|
||||||
if (inst[fk] && inst[fk].toString() === modelInstance[pk].toString()) {
|
if (inst[fk] && inst[fk].toString() === modelInstance[pk].toString()) {
|
||||||
self.removeFromCache(inst[fk]);
|
cb(null, true);
|
||||||
inst.destroy(cb);
|
|
||||||
} else {
|
} 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
|
// 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
|
// the source model by creating an instance of the through model
|
||||||
HasManyThrough.prototype.create = function create(data, done) {
|
HasManyThrough.prototype.create = function create(data, done) {
|
||||||
|
@ -448,6 +697,9 @@ HasManyThrough.prototype.create = function create(data, done) {
|
||||||
var d = {};
|
var d = {};
|
||||||
d[fk1] = modelInstance[definition.keyFrom];
|
d[fk1] = modelInstance[definition.keyFrom];
|
||||||
d[fk2] = to[pk2];
|
d[fk2] = to[pk2];
|
||||||
|
|
||||||
|
definition.applyProperties(modelInstance, d);
|
||||||
|
|
||||||
// Then create the through model
|
// Then create the through model
|
||||||
modelThrough.create(d, function (e, through) {
|
modelThrough.create(d, function (e, through) {
|
||||||
if (e) {
|
if (e) {
|
||||||
|
@ -485,13 +737,18 @@ HasManyThrough.prototype.add = function (acInst, done) {
|
||||||
var fk2 = findBelongsTo(modelThrough, definition.modelTo, pk2);
|
var fk2 = findBelongsTo(modelThrough, definition.modelTo, pk2);
|
||||||
|
|
||||||
query[fk1] = this.modelInstance[pk1];
|
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[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
|
// 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(!err) {
|
||||||
if (acInst instanceof definition.modelTo) {
|
if (acInst instanceof definition.modelTo) {
|
||||||
self.addToCache(acInst);
|
self.addToCache(acInst);
|
||||||
|
@ -501,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
|
* Remove the target model instance from the 'hasMany' relation
|
||||||
* @param {Object|ID) acInst The actual instance or id value
|
* @param {Object|ID) acInst The actual instance or id value
|
||||||
|
@ -522,9 +811,13 @@ HasManyThrough.prototype.remove = function (acInst, done) {
|
||||||
var fk2 = findBelongsTo(modelThrough, definition.modelTo, pk2);
|
var fk2 = findBelongsTo(modelThrough, definition.modelTo, pk2);
|
||||||
|
|
||||||
query[fk1] = this.modelInstance[pk1];
|
query[fk1] = this.modelInstance[pk1];
|
||||||
query[fk2] = acInst[pk2] || acInst;
|
query[fk2] = (acInst instanceof definition.modelTo) ? acInst[pk2] : acInst;
|
||||||
|
|
||||||
modelThrough.deleteAll(query, function (err) {
|
var filter = { where: query };
|
||||||
|
|
||||||
|
definition.applyScope(this.modelInstance, filter);
|
||||||
|
|
||||||
|
modelThrough.deleteAll(filter.where, function (err) {
|
||||||
if (!err) {
|
if (!err) {
|
||||||
self.removeFromCache(query[fk2]);
|
self.removeFromCache(query[fk2]);
|
||||||
}
|
}
|
||||||
|
@ -615,6 +908,11 @@ BelongsTo.prototype.create = function(targetModelData, cb) {
|
||||||
var pk = this.definition.keyFrom;
|
var pk = this.definition.keyFrom;
|
||||||
var modelInstance = this.modelInstance;
|
var modelInstance = this.modelInstance;
|
||||||
|
|
||||||
|
if (typeof targetModelData === 'function' && !cb) {
|
||||||
|
cb = targetModelData;
|
||||||
|
targetModelData = {};
|
||||||
|
}
|
||||||
|
|
||||||
modelTo.create(targetModelData, function(err, targetModel) {
|
modelTo.create(targetModelData, function(err, targetModel) {
|
||||||
if(!err) {
|
if(!err) {
|
||||||
modelInstance[fk] = targetModel[pk];
|
modelInstance[fk] = targetModel[pk];
|
||||||
|
@ -678,7 +976,11 @@ BelongsTo.prototype.related = function (refresh, params) {
|
||||||
self.resetCache(inst);
|
self.resetCache(inst);
|
||||||
cb(null, inst);
|
cb(null, inst);
|
||||||
} else {
|
} 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];
|
return modelInstance[fk];
|
||||||
|
@ -687,7 +989,7 @@ BelongsTo.prototype.related = function (refresh, params) {
|
||||||
return cachedValue;
|
return cachedValue;
|
||||||
}
|
}
|
||||||
} else if (params === undefined) { // acts as sync getter
|
} else if (params === undefined) { // acts as sync getter
|
||||||
return modelInstance[fk];
|
return cachedValue;
|
||||||
} else { // setter
|
} else { // setter
|
||||||
modelInstance[fk] = params;
|
modelInstance[fk] = params;
|
||||||
self.resetCache();
|
self.resetCache();
|
||||||
|
@ -779,7 +1081,8 @@ RelationDefinition.hasOne = function (modelFrom, modelTo, params) {
|
||||||
modelFrom: modelFrom,
|
modelFrom: modelFrom,
|
||||||
keyFrom: pk,
|
keyFrom: pk,
|
||||||
keyTo: fk,
|
keyTo: fk,
|
||||||
modelTo: modelTo
|
modelTo: modelTo,
|
||||||
|
properties: params.properties
|
||||||
});
|
});
|
||||||
|
|
||||||
modelFrom.dataSource.defineForeignKey(modelTo.modelName, fk, modelFrom.modelName);
|
modelFrom.dataSource.defineForeignKey(modelTo.modelName, fk, modelFrom.modelName);
|
||||||
|
@ -805,18 +1108,34 @@ RelationDefinition.hasOne = function (modelFrom, modelTo, params) {
|
||||||
* @param {String|Object} err Error string or object
|
* @param {String|Object} err Error string or object
|
||||||
* @param {Object} The newly created target model instance
|
* @param {Object} The newly created target model instance
|
||||||
*/
|
*/
|
||||||
HasOne.prototype.create = function(targetModelData, cb) {
|
HasOne.prototype.create = function (targetModelData, cb) {
|
||||||
var self = this;
|
var self = this;
|
||||||
var modelTo = this.definition.modelTo;
|
var modelTo = this.definition.modelTo;
|
||||||
var fk = this.definition.keyTo;
|
var fk = this.definition.keyTo;
|
||||||
var pk = this.definition.keyFrom;
|
var pk = this.definition.keyFrom;
|
||||||
var modelInstance = this.modelInstance;
|
var modelInstance = this.modelInstance;
|
||||||
var relationName = this.definition.name
|
|
||||||
|
|
||||||
|
if (typeof targetModelData === 'function' && !cb) {
|
||||||
|
cb = targetModelData;
|
||||||
|
targetModelData = {};
|
||||||
|
}
|
||||||
targetModelData = targetModelData || {};
|
targetModelData = targetModelData || {};
|
||||||
targetModelData[fk] = modelInstance[pk];
|
targetModelData[fk] = modelInstance[pk];
|
||||||
modelTo.create(targetModelData, function(err, targetModel) {
|
var query = {where: {}};
|
||||||
if(!err) {
|
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);
|
||||||
|
} else if(result) {
|
||||||
|
cb(new Error('HasOne relation cannot create more than one instance of '
|
||||||
|
+ modelTo.modelName));
|
||||||
|
} else {
|
||||||
|
modelTo.create(targetModelData, function (err, targetModel) {
|
||||||
|
if (!err) {
|
||||||
// Refresh the cache
|
// Refresh the cache
|
||||||
self.resetCache(targetModel);
|
self.resetCache(targetModel);
|
||||||
cb && cb(err, targetModel);
|
cb && cb(err, targetModel);
|
||||||
|
@ -824,19 +1143,58 @@ HasOne.prototype.create = function(targetModelData, cb) {
|
||||||
cb && cb(err);
|
cb && cb(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a target model instance
|
||||||
|
* @param {Object} targetModelData The target model data
|
||||||
|
* @callback {Function} [cb] Callback function
|
||||||
|
* @param {String|Object} err Error string or object
|
||||||
|
* @param {Object} The newly created target model instance
|
||||||
|
*/
|
||||||
|
HasMany.prototype.create = function (targetModelData, cb) {
|
||||||
|
var self = this;
|
||||||
|
var modelTo = this.definition.modelTo;
|
||||||
|
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
|
||||||
|
self.addToCache(targetModel);
|
||||||
|
cb && cb(err, targetModel);
|
||||||
|
} else {
|
||||||
|
cb && cb(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
/**
|
/**
|
||||||
* Build a target model instance
|
* Build a target model instance
|
||||||
* @param {Object} targetModelData The target model data
|
* @param {Object} targetModelData The target model data
|
||||||
* @returns {Object} The newly built target model instance
|
* @returns {Object} The newly built target model instance
|
||||||
*/
|
*/
|
||||||
HasOne.prototype.build = function(targetModelData) {
|
HasMany.prototype.build = HasOne.prototype.build = function(targetModelData) {
|
||||||
var modelTo = this.definition.modelTo;
|
var modelTo = this.definition.modelTo;
|
||||||
var pk = this.definition.keyFrom;
|
var pk = this.definition.keyFrom;
|
||||||
var fk = this.definition.keyTo;
|
var fk = this.definition.keyTo;
|
||||||
|
|
||||||
targetModelData = targetModelData || {};
|
targetModelData = targetModelData || {};
|
||||||
targetModelData[fk] = this.modelInstance[pk];
|
targetModelData[fk] = this.modelInstance[pk];
|
||||||
|
|
||||||
|
this.definition.applyProperties(this.modelInstance, targetModelData);
|
||||||
|
|
||||||
return new modelTo(targetModelData);
|
return new modelTo(targetModelData);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -856,8 +1214,8 @@ HasOne.prototype.related = function (refresh, params) {
|
||||||
var modelTo = this.definition.modelTo;
|
var modelTo = this.definition.modelTo;
|
||||||
var fk = this.definition.keyTo;
|
var fk = this.definition.keyTo;
|
||||||
var pk = this.definition.keyFrom;
|
var pk = this.definition.keyFrom;
|
||||||
|
var definition = this.definition;
|
||||||
var modelInstance = this.modelInstance;
|
var modelInstance = this.modelInstance;
|
||||||
var relationName = this.definition.name;
|
|
||||||
|
|
||||||
if (arguments.length === 1) {
|
if (arguments.length === 1) {
|
||||||
params = refresh;
|
params = refresh;
|
||||||
|
@ -878,6 +1236,7 @@ HasOne.prototype.related = function (refresh, params) {
|
||||||
if (cachedValue === undefined) {
|
if (cachedValue === undefined) {
|
||||||
var query = {where: {}};
|
var query = {where: {}};
|
||||||
query.where[fk] = modelInstance[pk];
|
query.where[fk] = modelInstance[pk];
|
||||||
|
definition.applyScope(modelInstance, query);
|
||||||
modelTo.findOne(query, function (err, inst) {
|
modelTo.findOne(query, function (err, inst) {
|
||||||
if (err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
|
@ -890,7 +1249,11 @@ HasOne.prototype.related = function (refresh, params) {
|
||||||
self.resetCache(inst);
|
self.resetCache(inst);
|
||||||
cb(null, inst);
|
cb(null, inst);
|
||||||
} else {
|
} 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];
|
return modelInstance[pk];
|
||||||
|
@ -899,7 +1262,7 @@ HasOne.prototype.related = function (refresh, params) {
|
||||||
return cachedValue;
|
return cachedValue;
|
||||||
}
|
}
|
||||||
} else if (params === undefined) { // acts as sync getter
|
} else if (params === undefined) { // acts as sync getter
|
||||||
return modelInstance[pk];
|
return cachedValue;
|
||||||
} else { // setter
|
} else { // setter
|
||||||
params[fk] = modelInstance[pk];
|
params[fk] = modelInstance[pk];
|
||||||
self.resetCache();
|
self.resetCache();
|
||||||
|
|
|
@ -5,6 +5,7 @@ var defineCachedRelations = utils.defineCachedRelations;
|
||||||
* Module exports
|
* Module exports
|
||||||
*/
|
*/
|
||||||
exports.defineScope = defineScope;
|
exports.defineScope = defineScope;
|
||||||
|
exports.mergeQuery = mergeQuery;
|
||||||
|
|
||||||
function ScopeDefinition(definition) {
|
function ScopeDefinition(definition) {
|
||||||
this.sourceModel = definition.sourceModel;
|
this.sourceModel = definition.sourceModel;
|
||||||
|
@ -186,7 +187,9 @@ function defineScope(cls, targetClass, name, params, methods) {
|
||||||
var prop = targetClass.definition.properties[i];
|
var prop = targetClass.definition.properties[i];
|
||||||
if (prop) {
|
if (prop) {
|
||||||
var val = where[i];
|
var val = where[i];
|
||||||
if (typeof val !== 'object' || val instanceof prop.type) {
|
if (typeof val !== 'object' || val instanceof prop.type
|
||||||
|
|| prop.type.name === 'ObjectID') // MongoDB key
|
||||||
|
{
|
||||||
// Only pick the {propertyName: propertyValue}
|
// Only pick the {propertyName: propertyValue}
|
||||||
data[i] = where[i];
|
data[i] = where[i];
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,6 +46,20 @@ function Validatable() {
|
||||||
*/
|
*/
|
||||||
Validatable.validatesPresenceOf = getConfigurator('presence');
|
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.
|
* Validate length. Require a property length to be within a specified range.
|
||||||
* Three kinds of validations: min, max, is.
|
* 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
|
* Length validator
|
||||||
*/
|
*/
|
||||||
|
@ -305,6 +328,9 @@ function validateCustom(attr, conf, err, done) {
|
||||||
* Uniqueness validator
|
* Uniqueness validator
|
||||||
*/
|
*/
|
||||||
function validateUniqueness(attr, conf, err, done) {
|
function validateUniqueness(attr, conf, err, done) {
|
||||||
|
if (blank(this[attr])) {
|
||||||
|
return process.nextTick(done);
|
||||||
|
}
|
||||||
var cond = {where: {}};
|
var cond = {where: {}};
|
||||||
cond.where[attr] = this[attr];
|
cond.where[attr] = this[attr];
|
||||||
|
|
||||||
|
@ -331,6 +357,7 @@ function validateUniqueness(attr, conf, err, done) {
|
||||||
|
|
||||||
var validators = {
|
var validators = {
|
||||||
presence: validatePresence,
|
presence: validatePresence,
|
||||||
|
absence: validateAbsence,
|
||||||
length: validateLength,
|
length: validateLength,
|
||||||
numericality: validateNumericality,
|
numericality: validateNumericality,
|
||||||
inclusion: validateInclusion,
|
inclusion: validateInclusion,
|
||||||
|
@ -389,7 +416,7 @@ Validatable.prototype.isValid = function (callback, data) {
|
||||||
validationsDone.call(inst, function () {
|
validationsDone.call(inst, function () {
|
||||||
callback(valid);
|
callback(valid);
|
||||||
});
|
});
|
||||||
});
|
}, data, callback);
|
||||||
}
|
}
|
||||||
return valid;
|
return valid;
|
||||||
}
|
}
|
||||||
|
@ -440,7 +467,7 @@ Validatable.prototype.isValid = function (callback, data) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}, data);
|
}, data, callback);
|
||||||
|
|
||||||
if (async) {
|
if (async) {
|
||||||
// in case of async validation we should return undefined here,
|
// 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)
|
// here we should check skip validation conditions (if, unless)
|
||||||
// that can be specified in conf
|
// that can be specified in conf
|
||||||
if (skipValidation(inst, conf, 'if')) return false;
|
if (skipValidation(inst, conf, 'if')
|
||||||
if (skipValidation(inst, conf, 'unless')) return false;
|
|| skipValidation(inst, conf, 'unless')) {
|
||||||
|
if (cb) cb(true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
var fail = false;
|
var fail = false;
|
||||||
var validator = validators[conf.validation];
|
var validator = validators[conf.validation];
|
||||||
|
@ -478,7 +508,7 @@ function validationFailed(inst, v, cb) {
|
||||||
validatorArguments.push(attr);
|
validatorArguments.push(attr);
|
||||||
validatorArguments.push(conf);
|
validatorArguments.push(conf);
|
||||||
validatorArguments.push(function onerror(kind) {
|
validatorArguments.push(function onerror(kind) {
|
||||||
var message, code = conf.validation;
|
var message, code = conf.code || conf.validation;
|
||||||
if (conf.message) {
|
if (conf.message) {
|
||||||
message = conf.message;
|
message = conf.message;
|
||||||
}
|
}
|
||||||
|
@ -499,7 +529,7 @@ function validationFailed(inst, v, cb) {
|
||||||
message = 'is invalid';
|
message = 'is invalid';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
inst.errors.add(attr, message, code);
|
if (kind !== false) inst.errors.add(attr, message, code);
|
||||||
fail = true;
|
fail = true;
|
||||||
});
|
});
|
||||||
if (cb) {
|
if (cb) {
|
||||||
|
@ -532,6 +562,7 @@ function skipValidation(inst, conf, kind) {
|
||||||
|
|
||||||
var defaultMessages = {
|
var defaultMessages = {
|
||||||
presence: 'can\'t be blank',
|
presence: 'can\'t be blank',
|
||||||
|
absence: 'can\'t be set',
|
||||||
length: {
|
length: {
|
||||||
min: 'too short',
|
min: 'too short',
|
||||||
max: 'too long',
|
max: 'too long',
|
||||||
|
|
|
@ -24,15 +24,14 @@
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"should": "~1.2.2",
|
"should": "~1.2.2",
|
||||||
"mocha": "~1.18.2"
|
"mocha": "~1.20.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"async": "~0.9.0",
|
"async": "~0.9.0",
|
||||||
"inflection": "~1.3.5",
|
|
||||||
"loopback-connector": "1.x",
|
"loopback-connector": "1.x",
|
||||||
"traverse": "~0.6.6",
|
"debug": "~1.0.2",
|
||||||
"qs": "~0.6.6",
|
"qs": "~0.6.6",
|
||||||
"debug": "~0.8.1"
|
"traverse": "~0.6.6"
|
||||||
},
|
},
|
||||||
"license": {
|
"license": {
|
||||||
"name": "Dual MIT/StrongLoop",
|
"name": "Dual MIT/StrongLoop",
|
||||||
|
|
|
@ -81,7 +81,27 @@ describe('hooks', function () {
|
||||||
}
|
}
|
||||||
|
|
||||||
User.afterCreate = 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.create(function (err, user) {
|
||||||
User.dataSource.connector.create = old;
|
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 () {
|
describe('update', function () {
|
||||||
|
@ -221,7 +253,7 @@ describe('hooks', function () {
|
||||||
|
|
||||||
it('should not trigger after-hook on failed save', function (done) {
|
it('should not trigger after-hook on failed save', function (done) {
|
||||||
User.afterUpdate = function () {
|
User.afterUpdate = function () {
|
||||||
should.fail('afterUpdate shouldn\'t be called')
|
should.fail('afterUpdate shouldn\'t be called');
|
||||||
};
|
};
|
||||||
User.create(function (err, user) {
|
User.create(function (err, user) {
|
||||||
var save = User.dataSource.connector.save;
|
var save = User.dataSource.connector.save;
|
||||||
|
|
|
@ -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) {
|
it('should be able to extend models', function (done) {
|
||||||
var modelBuilder = new ModelBuilder();
|
var modelBuilder = new ModelBuilder();
|
||||||
|
|
||||||
|
|
|
@ -141,6 +141,64 @@ describe('Memory connector', function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should throw if the like value is not string or regexp', function (done) {
|
||||||
|
User.find({where: {name: {like: 123}}}, function (err, posts) {
|
||||||
|
should.exist(err);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if the nlike value is not string or regexp', function (done) {
|
||||||
|
User.find({where: {name: {nlike: 123}}}, function (err, posts) {
|
||||||
|
should.exist(err);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if the inq value is not an array', function (done) {
|
||||||
|
User.find({where: {name: {inq: '12'}}}, function (err, posts) {
|
||||||
|
should.exist(err);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if the nin value is not an array', function (done) {
|
||||||
|
User.find({where: {name: {nin: '12'}}}, function (err, posts) {
|
||||||
|
should.exist(err);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if the between value is not an array', function (done) {
|
||||||
|
User.find({where: {name: {between: '12'}}}, function (err, posts) {
|
||||||
|
should.exist(err);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if the between value is not an array of length 2', function (done) {
|
||||||
|
User.find({where: {name: {between: ['12']}}}, function (err, posts) {
|
||||||
|
should.exist(err);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('support order with multiple fields', function (done) {
|
||||||
|
User.find({order: 'vip ASC, seq DESC'}, function (err, posts) {
|
||||||
|
should.not.exist(err);
|
||||||
|
posts[0].seq.should.be.eql(4);
|
||||||
|
posts[1].seq.should.be.eql(3);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if order has wrong direction', function (done) {
|
||||||
|
User.find({order: 'seq ABC'}, function (err, posts) {
|
||||||
|
should.exist(err);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
function seed(done) {
|
function seed(done) {
|
||||||
var beatles = [
|
var beatles = [
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
// This test written in mocha+should.js
|
// This test written in mocha+should.js
|
||||||
var should = require('./init.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 () {
|
describe('relations', function () {
|
||||||
|
|
||||||
|
describe('hasMany', function () {
|
||||||
before(function (done) {
|
before(function (done) {
|
||||||
db = getSchema();
|
db = getSchema();
|
||||||
Book = db.define('Book', {name: String});
|
Book = db.define('Book', {name: String, type: String});
|
||||||
Chapter = db.define('Chapter', {name: {type: String, index: true}});
|
Chapter = db.define('Chapter', {name: {type: String, index: true},
|
||||||
|
bookType: String});
|
||||||
Author = db.define('Author', {name: String});
|
Author = db.define('Author', {name: String});
|
||||||
Reader = db.define('Reader', {name: String});
|
Reader = db.define('Reader', {name: String});
|
||||||
|
|
||||||
|
@ -22,11 +26,6 @@ describe('relations', function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
after(function () {
|
|
||||||
// db.disconnect();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('hasMany', function () {
|
|
||||||
it('can be declared in different ways', function (done) {
|
it('can be declared in different ways', function (done) {
|
||||||
Book.hasMany(Chapter);
|
Book.hasMany(Chapter);
|
||||||
Book.hasMany(Reader, {as: 'users'});
|
Book.hasMany(Reader, {as: 'users'});
|
||||||
|
@ -73,12 +72,12 @@ describe('relations', function () {
|
||||||
book.chapters.create({name: 'a'}, function () {
|
book.chapters.create({name: 'a'}, function () {
|
||||||
book.chapters.create({name: 'z'}, function () {
|
book.chapters.create({name: 'z'}, function () {
|
||||||
book.chapters.create({name: 'c'}, function () {
|
book.chapters.create({name: 'c'}, function () {
|
||||||
fetch(book);
|
verify(book);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
function fetch(book) {
|
function verify(book) {
|
||||||
book.chapters(function (err, ch) {
|
book.chapters(function (err, ch) {
|
||||||
should.not.exist(err);
|
should.not.exist(err);
|
||||||
should.exist(ch);
|
should.exist(ch);
|
||||||
|
@ -102,14 +101,170 @@ describe('relations', function () {
|
||||||
id = ch.id;
|
id = ch.id;
|
||||||
book.chapters.create({name: 'z'}, function () {
|
book.chapters.create({name: 'z'}, function () {
|
||||||
book.chapters.create({name: 'c'}, function () {
|
book.chapters.create({name: 'c'}, function () {
|
||||||
fetch(book);
|
verify(book);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function fetch(book) {
|
function verify(book) {
|
||||||
book.chapters.findById(id, function (err, ch) {
|
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.not.exist(err);
|
||||||
should.exist(ch);
|
should.exist(ch);
|
||||||
ch.id.should.equal(id);
|
ch.id.should.equal(id);
|
||||||
|
@ -119,8 +274,230 @@ describe('relations', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set targetClass on scope property', 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 () {
|
describe('belongsTo', function () {
|
||||||
|
@ -155,7 +532,7 @@ describe('relations', function () {
|
||||||
should.not.exist(e);
|
should.not.exist(e);
|
||||||
should.exist(l);
|
should.exist(l);
|
||||||
l.should.be.an.instanceOf(List);
|
l.should.be.an.instanceOf(List);
|
||||||
todo.list().should.equal(l.id);
|
todo.list().id.should.equal(l.id);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -186,11 +563,11 @@ describe('relations', function () {
|
||||||
before(function () {
|
before(function () {
|
||||||
db = getSchema();
|
db = getSchema();
|
||||||
Supplier = db.define('Supplier', {name: String});
|
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 () {
|
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');
|
Object.keys((new Account()).toObject()).should.include('supplierId');
|
||||||
(new Supplier()).account.should.be.an.instanceOf(Function);
|
(new Supplier()).account.should.be.an.instanceOf(Function);
|
||||||
});
|
});
|
||||||
|
@ -206,7 +583,8 @@ describe('relations', function () {
|
||||||
should.not.exist(e);
|
should.not.exist(e);
|
||||||
should.exist(act);
|
should.exist(act);
|
||||||
act.should.be.an.instanceOf(Account);
|
act.should.be.an.instanceOf(Account);
|
||||||
supplier.account().should.equal(act.id);
|
supplier.account().id.should.equal(act.id);
|
||||||
|
act.supplierName.should.equal(supplier.name);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -160,6 +160,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 () {
|
describe('uniqueness', function () {
|
||||||
it('should validate uniqueness', function (done) {
|
it('should validate uniqueness', function (done) {
|
||||||
User.validatesUniquenessOf('email');
|
User.validatesUniquenessOf('email');
|
||||||
|
@ -227,6 +241,33 @@ describe('validations', function () {
|
||||||
done(err);
|
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 () {
|
describe('format', function () {
|
||||||
|
@ -251,7 +292,40 @@ describe('validations', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('custom', function () {
|
describe('custom', function () {
|
||||||
it('should validate using custom sync validation');
|
it('should validate using custom sync validation', function() {
|
||||||
it('should validate using custom async validation');
|
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;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue