Merge branch 'master' into 2.0
This commit is contained in:
commit
44de2da21f
|
@ -2,13 +2,11 @@
|
|||
"content": [
|
||||
"lib/datasource.js",
|
||||
"lib/geo.js",
|
||||
"lib/dao.js",
|
||||
"lib/model.js",
|
||||
"lib/model-builder.js",
|
||||
"lib/hooks.js",
|
||||
"lib/include.js",
|
||||
"lib/model-builder.js",
|
||||
"lib/relations.js",
|
||||
"lib/validations.js",
|
||||
"lib/sql.js"
|
||||
"lib/validations.js"
|
||||
],
|
||||
"codeSectionDepth": 4,
|
||||
"assets": {
|
||||
|
|
|
@ -306,9 +306,9 @@ Memory.prototype.all = function all(model, filter, callback) {
|
|||
}
|
||||
|
||||
// limit/skip
|
||||
filter.skip = filter.skip || 0;
|
||||
filter.limit = filter.limit || nodes.length;
|
||||
nodes = nodes.slice(filter.skip, filter.skip + filter.limit);
|
||||
var skip = filter.skip || filter.offset || 0;
|
||||
var limit = filter.limit || nodes.length;
|
||||
nodes = nodes.slice(skip, skip + limit);
|
||||
}
|
||||
|
||||
process.nextTick(function () {
|
||||
|
@ -363,8 +363,40 @@ function applyFilter(filter) {
|
|||
return pass;
|
||||
}
|
||||
|
||||
function toRegExp(pattern) {
|
||||
if (pattern instanceof RegExp) {
|
||||
return pattern;
|
||||
}
|
||||
var regex = '';
|
||||
// Escaping user input to be treated as a literal string within a regular expression
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Writing_a_Regular_Expression_Pattern
|
||||
pattern = pattern.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
|
||||
for (var i = 0, n = pattern.length; i < n; i++) {
|
||||
var char = pattern.charAt(i);
|
||||
if (char === '\\') {
|
||||
i++; // Skip to next char
|
||||
if (i < n) {
|
||||
regex += pattern.charAt(i);
|
||||
}
|
||||
continue;
|
||||
} else if (char === '%') {
|
||||
regex += '.*';
|
||||
} else if (char === '_') {
|
||||
regex += '.';
|
||||
} else if (char === '.') {
|
||||
regex += '\\.';
|
||||
} else if (char === '*') {
|
||||
regex += '\\*';
|
||||
}
|
||||
else {
|
||||
regex += char;
|
||||
}
|
||||
}
|
||||
return regex;
|
||||
}
|
||||
|
||||
function test(example, value) {
|
||||
if (typeof value === 'string' && example && example.constructor.name === 'RegExp') {
|
||||
if (typeof value === 'string' && (example instanceof RegExp)) {
|
||||
return value.match(example);
|
||||
}
|
||||
if (example === undefined || value === undefined) {
|
||||
|
@ -377,24 +409,81 @@ function applyFilter(filter) {
|
|||
}
|
||||
|
||||
if (example.inq) {
|
||||
if (!value) return false;
|
||||
for (var i = 0; i < example.inq.length; i += 1) {
|
||||
if (example.inq[i] == value) return true;
|
||||
// if (!value) return false;
|
||||
for (var i = 0; i < example.inq.length; i++) {
|
||||
if (example.inq[i] == value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isNum(example.gt) && example.gt < value) return true;
|
||||
if (isNum(example.gte) && example.gte <= value) return true;
|
||||
if (isNum(example.lt) && example.lt > value) return true;
|
||||
if (isNum(example.lte) && example.lte >= value) return true;
|
||||
if (example.like || example.nlike) {
|
||||
|
||||
var like = example.like || example.nlike;
|
||||
if (typeof like === 'string') {
|
||||
like = toRegExp(like);
|
||||
}
|
||||
if (example.like) {
|
||||
return !!new RegExp(like).test(value);
|
||||
}
|
||||
|
||||
if (example.nlike) {
|
||||
return !new RegExp(like).test(value);
|
||||
}
|
||||
}
|
||||
|
||||
if (testInEquality(example, value)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// not strict equality
|
||||
return (example !== null ? example.toString() : example) == (value !== null ? value.toString() : value);
|
||||
}
|
||||
|
||||
function isNum(n) {
|
||||
return typeof n === 'number';
|
||||
/**
|
||||
* Compare two values
|
||||
* @param {*} val1 The 1st value
|
||||
* @param {*} val2 The 2nd value
|
||||
* @returns {number} 0: =, positive: >, negative <
|
||||
* @private
|
||||
*/
|
||||
function compare(val1, val2) {
|
||||
if(val1 == null || val2 == null) {
|
||||
// Either val1 or val2 is null or undefined
|
||||
return val1 == val2 ? 0 : NaN;
|
||||
}
|
||||
if (typeof val1 === 'number') {
|
||||
return val1 - val2;
|
||||
}
|
||||
if (typeof val1 === 'string') {
|
||||
return (val1 > val2) ? 1 : ((val1 < val2) ? -1 : (val1 == val2) ? 0 : NaN);
|
||||
}
|
||||
if (typeof val1 === 'boolean') {
|
||||
return val1 - val2;
|
||||
}
|
||||
if (val1 instanceof Date) {
|
||||
var result = val1 - val2;
|
||||
return result;
|
||||
}
|
||||
// Return NaN if we don't know how to compare
|
||||
return (val1 == val2) ? 0 : NaN;
|
||||
}
|
||||
|
||||
function testInEquality(example, val) {
|
||||
if ('gt' in example) {
|
||||
return compare(val, example.gt) > 0;
|
||||
}
|
||||
if ('gte' in example) {
|
||||
return compare(val, example.gte) >= 0;
|
||||
}
|
||||
if ('lt' in example) {
|
||||
return compare(val, example.lt) < 0;
|
||||
}
|
||||
if ('lte' in example) {
|
||||
return compare(val, example.lte) <= 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -434,6 +523,29 @@ Memory.prototype.count = function count(model, callback, where) {
|
|||
});
|
||||
};
|
||||
|
||||
Memory.prototype.update =
|
||||
Memory.prototype.updateAll = function updateAll(model, where, data, cb) {
|
||||
var self = this;
|
||||
var cache = this.cache[model];
|
||||
var filter = null;
|
||||
where = where || {};
|
||||
filter = applyFilter({where: where});
|
||||
|
||||
var ids = Object.keys(cache);
|
||||
async.each(ids, function (id, done) {
|
||||
var inst = self.fromDb(model, cache[id]);
|
||||
if (!filter || filter(inst)) {
|
||||
self.updateAttributes(model, id, data, done);
|
||||
} else {
|
||||
process.nextTick(done);
|
||||
}
|
||||
}, function (err) {
|
||||
if (!err) {
|
||||
self.saveToFile(null, cb);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Memory.prototype.updateAttributes = function updateAttributes(model, id, data, cb) {
|
||||
if (!id) {
|
||||
var err = new Error('You must provide an id when updating attributes!');
|
||||
|
|
228
lib/dao.js
228
lib/dao.js
|
@ -18,6 +18,8 @@ var Memory = require('./connectors/memory').Memory;
|
|||
var utils = require('./utils');
|
||||
var fieldsToArray = utils.fieldsToArray;
|
||||
var removeUndefined = utils.removeUndefined;
|
||||
var util = require('util');
|
||||
var assert = require('assert');
|
||||
|
||||
/**
|
||||
* Base class for all persistent objects.
|
||||
|
@ -79,7 +81,7 @@ DataAccessObject._forDB = function (data) {
|
|||
* });
|
||||
* ```
|
||||
* Note: You must include a callback and use the created model provided in the callback if your code depends on your model being
|
||||
* saved or having an ID.
|
||||
* saved or having an ID.
|
||||
*
|
||||
* @param {Object} data Optional data object
|
||||
* @param {Function} callback Callback function called with these arguments:
|
||||
|
@ -163,7 +165,6 @@ DataAccessObject.create = function (data, callback) {
|
|||
this._adapter().create(modelName, this.constructor._forDB(obj.toObject(true)), function (err, id, rev) {
|
||||
if (id) {
|
||||
obj.__data[_idName] = id;
|
||||
obj.__dataWas[_idName] = id;
|
||||
defineReadonlyProp(obj, _idName, id);
|
||||
}
|
||||
if (rev) {
|
||||
|
@ -194,7 +195,7 @@ function stillConnecting(dataSource, obj, args) {
|
|||
/**
|
||||
* Update or insert a model instance: update exiting record if one is found, such that parameter `data.id` matches `id` of model instance;
|
||||
* otherwise, insert a new record.
|
||||
*
|
||||
*
|
||||
* NOTE: No setters, validations, or hooks are applied when using upsert.
|
||||
* `updateOrCreate` is an alias
|
||||
* @param {Object} data The model instance data
|
||||
|
@ -294,7 +295,7 @@ DataAccessObject.exists = function exists(id, cb) {
|
|||
|
||||
/**
|
||||
* Find model instance by ID.
|
||||
*
|
||||
*
|
||||
* Example:
|
||||
* ```js
|
||||
* User.findById(23, function(err, user) {
|
||||
|
@ -314,8 +315,7 @@ DataAccessObject.findById = function find(id, cb) {
|
|||
if (!getIdValue(this, data)) {
|
||||
setIdValue(this, data, id);
|
||||
}
|
||||
obj = new this();
|
||||
obj._initProperties(data);
|
||||
obj = new this(data, {applySetters: false});
|
||||
}
|
||||
cb(err, obj);
|
||||
}.bind(this));
|
||||
|
@ -350,13 +350,88 @@ var operators = {
|
|||
nlike: 'NOT LIKE'
|
||||
};
|
||||
|
||||
/*
|
||||
* Normalize the filter object and throw errors if invalid values are detected
|
||||
* @param {Object} filter The query filter object
|
||||
* @returns {Object} The normalized filter object
|
||||
* @private
|
||||
*/
|
||||
DataAccessObject._normalize = function (filter) {
|
||||
if (!filter) {
|
||||
return undefined;
|
||||
}
|
||||
var err = null;
|
||||
if ((typeof filter !== 'object') || Array.isArray(filter)) {
|
||||
err = new Error(util.format('The query filter %j is not an object', filter));
|
||||
err.statusCode = 400;
|
||||
throw err;
|
||||
}
|
||||
if (filter.limit || filter.skip || filter.offset) {
|
||||
var limit = Number(filter.limit || 100);
|
||||
var offset = Number(filter.skip || filter.offset || 0);
|
||||
if (isNaN(limit) || limit <= 0 || Math.ceil(limit) !== limit) {
|
||||
err = new Error(util.format('The limit parameter %j is not valid',
|
||||
filter.limit));
|
||||
err.statusCode = 400;
|
||||
throw err;
|
||||
}
|
||||
if (isNaN(offset) || offset < 0 || Math.ceil(offset) !== offset) {
|
||||
err = new Error(util.format('The offset/skip parameter %j is not valid',
|
||||
filter.skip || filter.offset));
|
||||
err.statusCode = 400;
|
||||
throw err;
|
||||
}
|
||||
filter.limit = limit;
|
||||
filter.offset = offset;
|
||||
filter.skip = offset;
|
||||
}
|
||||
|
||||
// normalize fields as array of included property names
|
||||
if (filter.fields) {
|
||||
filter.fields = fieldsToArray(filter.fields,
|
||||
Object.keys(this.definition.properties));
|
||||
}
|
||||
|
||||
filter = removeUndefined(filter);
|
||||
this._coerce(filter.where);
|
||||
return filter;
|
||||
};
|
||||
|
||||
/*
|
||||
* Coerce values based the property types
|
||||
* @param {Object} where The where clause
|
||||
* @returns {Object} The coerced where clause
|
||||
* @private
|
||||
*/
|
||||
DataAccessObject._coerce = function (where) {
|
||||
var self = this;
|
||||
if (!where) {
|
||||
return where;
|
||||
}
|
||||
|
||||
var props = this.getDataSource().getModelDefinition(this.modelName).properties;
|
||||
var err;
|
||||
if (typeof where !== 'object' || Array.isArray(where)) {
|
||||
err = new Error(util.format('The where clause %j is not an object', where));
|
||||
err.statusCode = 400;
|
||||
throw err;
|
||||
}
|
||||
|
||||
var props = self.definition.properties;
|
||||
for (var p in where) {
|
||||
// Handle logical operators
|
||||
if (p === 'and' || p === 'or' || p === 'nor') {
|
||||
var clauses = where[p];
|
||||
if (Array.isArray(clauses)) {
|
||||
for (var i = 0; i < clauses.length; i++) {
|
||||
self._coerce(clauses[i]);
|
||||
}
|
||||
} else {
|
||||
err = new Error(util.format('The %p operator has invalid clauses %j', p, clauses));
|
||||
err.statusCode = 400;
|
||||
throw err;
|
||||
}
|
||||
return where;
|
||||
}
|
||||
var DataType = props[p] && props[p].type;
|
||||
if (!DataType) {
|
||||
continue;
|
||||
|
@ -427,10 +502,14 @@ DataAccessObject._coerce = function (where) {
|
|||
// Coerce the array items
|
||||
if (Array.isArray(val)) {
|
||||
for (var i = 0; i < val.length; i++) {
|
||||
val[i] = DataType(val[i]);
|
||||
if (val[i] !== null && val[i] !== undefined) {
|
||||
val[i] = DataType(val[i]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val = DataType(val);
|
||||
if (val !== null && val !== undefined) {
|
||||
val = DataType(val);
|
||||
}
|
||||
}
|
||||
// Rebuild {property: {operator: value}}
|
||||
if (operator) {
|
||||
|
@ -447,7 +526,7 @@ DataAccessObject._coerce = function (where) {
|
|||
* Find all instances of Model that match the specified query.
|
||||
* Fields used for filter and sort should be declared with `{index: true}` in model definition.
|
||||
* See [Querying models](http://docs.strongloop.com/display/DOC/Querying+models) for more information.
|
||||
*
|
||||
*
|
||||
* For example, find the second page of ten users over age 21 in descending order exluding the password property.
|
||||
*
|
||||
* ```js
|
||||
|
@ -464,7 +543,7 @@ DataAccessObject._coerce = function (where) {
|
|||
* ```
|
||||
*
|
||||
* @options {Object} [query] Optional JSON object that specifies query criteria and parameters.
|
||||
* @property {Object} where Search criteria in JSON format `{ key: val, key2: {gt: 'val2'}}`.
|
||||
* @property {Object} where Search criteria in JSON format `{ key: val, key2: {gt: 'val2'}}`.
|
||||
* Operations:
|
||||
* - gt: >
|
||||
* - gte: >=
|
||||
|
@ -476,7 +555,7 @@ DataAccessObject._coerce = function (where) {
|
|||
* - neq: !=
|
||||
* - like: LIKE
|
||||
* - nlike: NOT LIKE
|
||||
*
|
||||
*
|
||||
* You can also use `and` and `or` operations. See [Querying models](http://docs.strongloop.com/display/DOC/Querying+models) for more information.
|
||||
* @property {String|Object|Array} include Allows you to load relations of several objects and optimize numbers of requests.
|
||||
* Format examples;
|
||||
|
@ -495,7 +574,7 @@ DataAccessObject._coerce = function (where) {
|
|||
* - `['foo', 'bar']` - include the foo and bar properties. Format:
|
||||
* - `{foo: true}` - include only foo
|
||||
* - `{bat: false}` - include all properties, exclude bat
|
||||
*
|
||||
*
|
||||
* @param {Function} callback Required callback function. Call this function with two arguments: `err` (null or Error) and an array of instances.
|
||||
*/
|
||||
|
||||
|
@ -506,24 +585,21 @@ DataAccessObject.find = function find(query, cb) {
|
|||
cb = query;
|
||||
query = null;
|
||||
}
|
||||
var constr = this;
|
||||
var self = this;
|
||||
|
||||
query = query || {};
|
||||
|
||||
if (query.where) {
|
||||
query.where = this._coerce(query.where);
|
||||
try {
|
||||
this._normalize(query);
|
||||
} catch (err) {
|
||||
return process.nextTick(function () {
|
||||
cb && cb(err);
|
||||
});
|
||||
}
|
||||
|
||||
var fields = query.fields;
|
||||
var near = query && geo.nearFilter(query.where);
|
||||
var supportsGeo = !!this.getDataSource().connector.buildNearFilter;
|
||||
|
||||
// normalize fields as array of included property names
|
||||
if (fields) {
|
||||
query.fields = fieldsToArray(fields, Object.keys(this.definition.properties));
|
||||
}
|
||||
|
||||
query = removeUndefined(query);
|
||||
if (near) {
|
||||
if (supportsGeo) {
|
||||
// convert it
|
||||
|
@ -533,15 +609,15 @@ DataAccessObject.find = function find(query, cb) {
|
|||
// using all documents
|
||||
this.getDataSource().connector.all(this.modelName, {}, function (err, data) {
|
||||
var memory = new Memory();
|
||||
var modelName = constr.modelName;
|
||||
var modelName = self.modelName;
|
||||
|
||||
if (err) {
|
||||
cb(err);
|
||||
} else if (Array.isArray(data)) {
|
||||
memory.define({
|
||||
properties: constr.dataSource.definitions[constr.modelName].properties,
|
||||
settings: constr.dataSource.definitions[constr.modelName].settings,
|
||||
model: constr
|
||||
properties: self.dataSource.definitions[self.modelName].properties,
|
||||
settings: self.dataSource.definitions[self.modelName].settings,
|
||||
model: self
|
||||
});
|
||||
|
||||
data.forEach(function (obj) {
|
||||
|
@ -564,9 +640,7 @@ DataAccessObject.find = function find(query, cb) {
|
|||
this.getDataSource().connector.all(this.modelName, query, function (err, data) {
|
||||
if (data && data.forEach) {
|
||||
data.forEach(function (d, i) {
|
||||
var obj = new constr();
|
||||
|
||||
obj._initProperties(d, {fields: query.fields});
|
||||
var obj = new self(d, {fields: query.fields, applySetters: false});
|
||||
|
||||
if (query && query.include) {
|
||||
if (query.collect) {
|
||||
|
@ -643,7 +717,7 @@ DataAccessObject.findOne = function findOne(query, cb) {
|
|||
// removed matching products
|
||||
* });
|
||||
* ````
|
||||
*
|
||||
*
|
||||
* @param {Object} [where] Optional object that defines the criteria. This is a "where" object. Do NOT pass a filter object.
|
||||
* @param {Function} [cb] Callback called with (err)
|
||||
*/
|
||||
|
@ -661,9 +735,15 @@ DataAccessObject.remove = DataAccessObject.deleteAll = DataAccessObject.destroyA
|
|||
if(!err) Model.emit('deletedAll');
|
||||
}.bind(this));
|
||||
} else {
|
||||
// Support an optional where object
|
||||
where = removeUndefined(where);
|
||||
where = this._coerce(where);
|
||||
try {
|
||||
// Support an optional where object
|
||||
where = removeUndefined(where);
|
||||
where = this._coerce(where);
|
||||
} catch (err) {
|
||||
return process.nextTick(function() {
|
||||
cb && cb(err);
|
||||
});
|
||||
}
|
||||
this.getDataSource().connector.destroyAll(this.modelName, where, function (err, data) {
|
||||
cb && cb(err, data);
|
||||
if(!err) Model.emit('deletedAll', where);
|
||||
|
@ -672,7 +752,7 @@ DataAccessObject.remove = DataAccessObject.deleteAll = DataAccessObject.destroyA
|
|||
};
|
||||
|
||||
/**
|
||||
* Delete the record with the specified ID.
|
||||
* Delete the record with the specified ID.
|
||||
* Aliases are `destroyById` and `deleteById`.
|
||||
* @param {*} id The id value
|
||||
* @param {Function} cb Callback called with (err)
|
||||
|
@ -693,7 +773,7 @@ DataAccessObject.removeById = DataAccessObject.deleteById = DataAccessObject.des
|
|||
/**
|
||||
* Return count of matched records. Optional query parameter allows you to count filtered set of model instances.
|
||||
* Example:
|
||||
*
|
||||
*
|
||||
*```js
|
||||
* User.count({approved: true}, function(err, count) {
|
||||
* console.log(count); // 2081
|
||||
|
@ -710,8 +790,14 @@ DataAccessObject.count = function (where, cb) {
|
|||
cb = where;
|
||||
where = null;
|
||||
}
|
||||
where = removeUndefined(where);
|
||||
where = this._coerce(where);
|
||||
try {
|
||||
where = removeUndefined(where);
|
||||
where = this._coerce(where);
|
||||
} catch (err) {
|
||||
return process.nextTick(function () {
|
||||
cb && cb(err);
|
||||
});
|
||||
}
|
||||
this.getDataSource().connector.count(this.modelName, cb, where);
|
||||
};
|
||||
|
||||
|
@ -793,6 +879,58 @@ DataAccessObject.prototype.save = function (options, callback) {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update multiple instances that match the where clause
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
*```js
|
||||
* Employee.update({managerId: 'x001'}, {managerId: 'x002'}, function(err) {
|
||||
* ...
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @param {Object} [where] Search conditions (optional)
|
||||
* @param {Object} data Changes to be made
|
||||
* @param {Function} cb Callback, called with (err, count)
|
||||
*/
|
||||
DataAccessObject.update =
|
||||
DataAccessObject.updateAll = function (where, data, cb) {
|
||||
if (stillConnecting(this.getDataSource(), this, arguments)) return;
|
||||
|
||||
if (arguments.length === 1) {
|
||||
// update(data) is being called
|
||||
data = where;
|
||||
where = null;
|
||||
cb = null;
|
||||
} else if (arguments.length === 2) {
|
||||
if (typeof data === 'function') {
|
||||
// update(data, cb) is being called
|
||||
cb = data;
|
||||
data = where;
|
||||
where = null;
|
||||
} else {
|
||||
// update(where, data) is being called
|
||||
cb = null;
|
||||
}
|
||||
}
|
||||
|
||||
assert(typeof where === 'object', 'The where argument should be an object');
|
||||
assert(typeof data === 'object', 'The data argument should be an object');
|
||||
assert(cb === null || typeof cb === 'function', 'The cb argument should be a function');
|
||||
|
||||
try {
|
||||
where = removeUndefined(where);
|
||||
where = this._coerce(where);
|
||||
} catch (err) {
|
||||
return process.nextTick(function () {
|
||||
cb && cb(err);
|
||||
});
|
||||
}
|
||||
var connector = this.getDataSource().connector;
|
||||
connector.update(this.modelName, where, data, cb);
|
||||
};
|
||||
|
||||
DataAccessObject.prototype.isNewRecord = function () {
|
||||
return !getIdValue(this.constructor, this);
|
||||
};
|
||||
|
@ -891,12 +1029,6 @@ DataAccessObject.prototype.updateAttributes = function updateAttributes(data, cb
|
|||
}
|
||||
|
||||
inst._adapter().updateAttributes(model, getIdValue(inst.constructor, inst), inst.constructor._forDB(typedData), function (err) {
|
||||
if (!err) {
|
||||
// update $was attrs
|
||||
for (var key in data) {
|
||||
inst.__dataWas[key] = inst.__data[key];
|
||||
}
|
||||
}
|
||||
done.call(inst, function () {
|
||||
saveDone.call(inst, function () {
|
||||
if(cb) cb(err, inst);
|
||||
|
@ -914,6 +1046,7 @@ DataAccessObject.prototype.updateAttributes = function updateAttributes(data, cb
|
|||
* Reload object from persistence
|
||||
* Requires `id` member of `object` to be able to call `find`
|
||||
* @param {Function} callback Called with (err, instance) arguments
|
||||
* @private
|
||||
*/
|
||||
DataAccessObject.prototype.reload = function reload(callback) {
|
||||
if (stillConnecting(this.getDataSource(), this, arguments)) {
|
||||
|
@ -924,12 +1057,13 @@ DataAccessObject.prototype.reload = function reload(callback) {
|
|||
};
|
||||
|
||||
|
||||
/*!
|
||||
/*
|
||||
* Define readonly property on object
|
||||
*
|
||||
* @param {Object} obj
|
||||
* @param {String} key
|
||||
* @param {Mixed} value
|
||||
* @private
|
||||
*/
|
||||
function defineReadonlyProp(obj, key, value) {
|
||||
Object.defineProperty(obj, key, {
|
||||
|
@ -955,12 +1089,12 @@ DataAccessObject.scope = function (name, query, targetClass) {
|
|||
defineScope(this, targetClass || this, name, query);
|
||||
};
|
||||
|
||||
/*!
|
||||
/*
|
||||
* Add 'include'
|
||||
*/
|
||||
jutil.mixin(DataAccessObject, Inclusion);
|
||||
|
||||
/*!
|
||||
/*
|
||||
* Add 'relation'
|
||||
*/
|
||||
jutil.mixin(DataAccessObject, Relation);
|
||||
|
|
|
@ -494,7 +494,11 @@ DataSource.prototype.setupDataAccess = function (modelClass, settings) {
|
|||
};
|
||||
|
||||
/**
|
||||
* Define a model class.
|
||||
* Define a model class. Returns newly created model object.
|
||||
* The first (String) argument specifying the model name is required.
|
||||
* You can provide one or two JSON object arguments, to provide configuration options.
|
||||
* See [Model definition reference](http://docs.strongloop.com/display/DOC/Model+definition+reference) for details.
|
||||
*
|
||||
* Simple example:
|
||||
* ```
|
||||
* var User = dataSource.createModel('User', {
|
||||
|
@ -533,9 +537,8 @@ DataSource.prototype.setupDataAccess = function (modelClass, settings) {
|
|||
* ```
|
||||
*
|
||||
* @param {String} className Name of the model to create.
|
||||
* @param {Object} properties Hash of class properties in format `{property: Type, property2: Type2, ...}` or `{property: {type: Type}, property2: {type: Type2}, ...}`
|
||||
* @param {Object} settings Other configuration settings.
|
||||
* @returns newly created class
|
||||
* @param {Object} properties Hash of model properties in format `{property: Type, property2: Type2, ...}` or `{property: {type: Type}, property2: {type: Type2}, ...}`
|
||||
* @options {Object} properties Other configuration options. This corresponds to the options key in the config object.
|
||||
*
|
||||
*/
|
||||
|
||||
|
@ -584,6 +587,7 @@ DataSource.prototype.createModel = DataSource.prototype.define = function define
|
|||
* Mixin DataAccessObject methods.
|
||||
*
|
||||
* @param {Function} ModelCtor The model constructor
|
||||
* @private
|
||||
*/
|
||||
|
||||
DataSource.prototype.mixin = function (ModelCtor) {
|
||||
|
@ -716,12 +720,14 @@ DataSource.prototype.defineProperty = function (model, prop, params) {
|
|||
|
||||
/**
|
||||
* Drop each model table and re-create.
|
||||
* This method applies only to SQL connectors.
|
||||
*
|
||||
* This method applies only to database connectors. For MongoDB, it drops and creates indexes.
|
||||
*
|
||||
* **WARNING**: Calling this function deletes all data! Use `autoupdate()` to preserve data.
|
||||
*
|
||||
* @param {String} model Model to migrate. If not present, apply to all models. Can also be an array of Strings.
|
||||
* @param {Function} cb Callback function. Optional.
|
||||
* @param {Function} [callback] Callback function. Optional.
|
||||
*
|
||||
* WARNING: Calling this function will cause all data to be lost! Use autoupdate if you need to preserve data.
|
||||
|
||||
*/
|
||||
DataSource.prototype.automigrate = function (models, cb) {
|
||||
this.freeze();
|
||||
|
@ -738,7 +744,7 @@ DataSource.prototype.automigrate = function (models, cb) {
|
|||
|
||||
/**
|
||||
* Update existing database tables.
|
||||
* This method make sense only for sql connectors.
|
||||
* This method applies only to database connectors.
|
||||
*
|
||||
* @param {String} model Model to migrate. If not present, apply to all models. Can also be an array of Strings.
|
||||
* @param {Function} [cb] The callback function
|
||||
|
@ -762,14 +768,11 @@ DataSource.prototype.autoupdate = function (models, cb) {
|
|||
*
|
||||
* @param {Object} options The options
|
||||
* @param {Function} Callback function. Optional.
|
||||
* @options {Object} options Discovery options.
|
||||
*
|
||||
* Keys in options object:
|
||||
*
|
||||
* @property all {Boolean} If true, discover all models; if false, discover only models owned by the current user.
|
||||
* @property views {Boolean} If true, nclude views; if false, only tables.
|
||||
* @property limit {Number} Page size
|
||||
* @property offset {Number} Starting index
|
||||
* @options {Object} options Discovery options. See below.
|
||||
* @property {Boolean} all If true, discover all models; if false, discover only models owned by the current user.
|
||||
* @property {Boolean} views If true, nclude views; if false, only tables.
|
||||
* @property {Number} limit Page size
|
||||
* @property {Number} offset Starting index
|
||||
*
|
||||
*/
|
||||
DataSource.prototype.discoverModelDefinitions = function (options, cb) {
|
||||
|
@ -782,8 +785,12 @@ DataSource.prototype.discoverModelDefinitions = function (options, cb) {
|
|||
};
|
||||
|
||||
/**
|
||||
* The synchronous version of discoverModelDefinitions
|
||||
* @param {Object} options The options
|
||||
* The synchronous version of discoverModelDefinitions.
|
||||
* @options {Object} options The options
|
||||
* @property {Boolean} all If true, discover all models; if false, discover only models owned by the current user.
|
||||
* @property {Boolean} views If true, nclude views; if false, only tables.
|
||||
* @property {Number} limit Page size
|
||||
* @property {Number} offset Starting index
|
||||
* @returns {*}
|
||||
*/
|
||||
DataSource.prototype.discoverModelDefinitionsSync = function (options) {
|
||||
|
@ -797,24 +804,22 @@ DataSource.prototype.discoverModelDefinitionsSync = function (options) {
|
|||
/**
|
||||
* Discover properties for a given model.
|
||||
*
|
||||
* property description:
|
||||
* Callback function return value is an object that can have the following properties:
|
||||
*
|
||||
*| Key | Type | Description |
|
||||
*|-----|------|-------------|
|
||||
*|owner | String | Database owner or schema|
|
||||
*|tableName | String | Table/view name|
|
||||
*|columnName | String | Column name|
|
||||
*|dataType | String | Data type|
|
||||
*|dataLength | Number | Data length|
|
||||
*|dataPrecision | Number | Numeric data precision|
|
||||
*|dataScale |Number | Numeric data scale|
|
||||
*|nullable |Boolean | If true, then the data can be null|
|
||||
*
|
||||
* Options:
|
||||
* - owner/schema The database owner/schema
|
||||
*| Key | Type | Description |
|
||||
*|-----|------|-------------|
|
||||
*|owner | String | Database owner or schema|
|
||||
*|tableName | String | Table/view name|
|
||||
*|columnName | String | Column name|
|
||||
*|dataType | String | Data type|
|
||||
*|dataLength | Number | Data length|
|
||||
*|dataPrecision | Number | Numeric data precision|
|
||||
*|dataScale |Number | Numeric data scale|
|
||||
*|nullable |Boolean | If true, then the data can be null|
|
||||
*
|
||||
* @param {String} modelName The table/view name
|
||||
* @param {Object} options The options
|
||||
* @options {Object} options The options
|
||||
* @property {String} owner|schema The database owner or schema
|
||||
* @param {Function} cb Callback function. Optional
|
||||
*
|
||||
*/
|
||||
|
@ -843,22 +848,19 @@ DataSource.prototype.discoverModelPropertiesSync = function (modelName, options)
|
|||
|
||||
/**
|
||||
* Discover primary keys for a given owner/modelName.
|
||||
* Callback function return value is an object that can have the following properties:
|
||||
*
|
||||
* Each primary key column description has the following columns:
|
||||
*
|
||||
* - owner {String} => table schema (may be null)
|
||||
* - tableName {String} => table name
|
||||
* - columnName {String} => column name
|
||||
* - keySeq {Number} => sequence number within primary key( a value of 1 represents the first column of the primary key, a value of 2 would represent the second column within the primary key).
|
||||
* - pkName {String} => primary key name (may be null)
|
||||
*
|
||||
* The owner defaults to current user.
|
||||
*
|
||||
* Options:
|
||||
* - owner/schema The database owner/schema
|
||||
*| Key | Type | Description |
|
||||
*|-----|------|-------------|
|
||||
*| owner |String | Table schema or owner (may be null). Owner defaults to current user.
|
||||
*| tableName |String| Table name
|
||||
*| columnName |String| Column name
|
||||
*| keySeq |Number| Sequence number within primary key (1 indicates the first column in the primary key; 2 indicates the second column in the primary key).
|
||||
*| pkName |String| Primary key name (may be null)
|
||||
*
|
||||
* @param {String} modelName The model name
|
||||
* @param {Object} options The options
|
||||
* @options {Object} options The options
|
||||
* @property {String} owner|schema The database owner or schema
|
||||
* @param {Function} [cb] The callback function
|
||||
*/
|
||||
DataSource.prototype.discoverPrimaryKeys = function (modelName, options, cb) {
|
||||
|
@ -873,7 +875,8 @@ DataSource.prototype.discoverPrimaryKeys = function (modelName, options, cb) {
|
|||
/**
|
||||
* The synchronous version of discoverPrimaryKeys
|
||||
* @param {String} modelName The model name
|
||||
* @param {Object} options The options
|
||||
* @options {Object} options The options
|
||||
* @property {String} owner|schema The database owner orschema
|
||||
* @returns {*}
|
||||
*/
|
||||
DataSource.prototype.discoverPrimaryKeysSync = function (modelName, options) {
|
||||
|
@ -887,24 +890,23 @@ DataSource.prototype.discoverPrimaryKeysSync = function (modelName, options) {
|
|||
/**
|
||||
* Discover foreign keys for a given owner/modelName
|
||||
*
|
||||
* `foreign key description`
|
||||
* Callback function return value is an object that can have the following properties:
|
||||
*
|
||||
* fkOwner String => foreign key table schema (may be null)
|
||||
* fkName String => foreign key name (may be null)
|
||||
* fkTableName String => foreign key table name
|
||||
* fkColumnName String => foreign key column name
|
||||
* keySeq Number => sequence number within a foreign key( a value of 1 represents the first column of the foreign key, a value of 2 would represent the second column within the foreign key).
|
||||
* pkOwner String => primary key table schema being imported (may be null)
|
||||
* pkName String => primary key name (may be null)
|
||||
* pkTableName String => primary key table name being imported
|
||||
* pkColumnName String => primary key column name being imported
|
||||
*
|
||||
* `options`
|
||||
*
|
||||
* owner/schema The database owner/schema
|
||||
*| Key | Type | Description |
|
||||
*|-----|------|-------------|
|
||||
*|fkOwner |String | Foreign key table schema (may be null)
|
||||
*|fkName |String | Foreign key name (may be null)
|
||||
*|fkTableName |String | Foreign key table name
|
||||
*|fkColumnName |String | Foreign key column name
|
||||
*|keySeq |Number | Sequence number within a foreign key( a value of 1 represents the first column of the foreign key, a value of 2 would represent the second column within the foreign key).
|
||||
*|pkOwner |String | Primary key table schema being imported (may be null)
|
||||
*|pkName |String | Primary key name (may be null)
|
||||
*|pkTableName |String | Primary key table name being imported
|
||||
*|pkColumnName |String | Primary key column name being imported
|
||||
*
|
||||
* @param {String} modelName The model name
|
||||
* @param {Object} options The options
|
||||
* @options {Object} options The options
|
||||
* @property {String} owner|schema The database owner or schema
|
||||
* @param {Function} [cb] The callback function
|
||||
*
|
||||
*/
|
||||
|
@ -933,27 +935,26 @@ DataSource.prototype.discoverForeignKeysSync = function (modelName, options) {
|
|||
};
|
||||
|
||||
/**
|
||||
* Retrieves a description of the foreign key columns that reference the given table's primary key columns (the foreign keys exported by a table).
|
||||
* They are ordered by fkTableOwner, fkTableName, and keySeq.
|
||||
* Retrieves a description of the foreign key columns that reference the given table's primary key columns
|
||||
* (the foreign keys exported by a table), ordered by fkTableOwner, fkTableName, and keySeq.
|
||||
*
|
||||
* Callback function return value is an object that can have the following properties:
|
||||
*
|
||||
* `foreign key description`
|
||||
*
|
||||
* fkOwner {String} => foreign key table schema (may be null)
|
||||
* fkName {String} => foreign key name (may be null)
|
||||
* fkTableName {String} => foreign key table name
|
||||
* fkColumnName {String} => foreign key column name
|
||||
* keySeq {Number} => sequence number within a foreign key( a value of 1 represents the first column of the foreign key, a value of 2 would represent the second column within the foreign key).
|
||||
* pkOwner {String} => primary key table schema being imported (may be null)
|
||||
* pkName {String} => primary key name (may be null)
|
||||
* pkTableName {String} => primary key table name being imported
|
||||
* pkColumnName {String} => primary key column name being imported
|
||||
*
|
||||
* `options`
|
||||
*
|
||||
* owner/schema The database owner/schema
|
||||
*| Key | Type | Description |
|
||||
*|-----|------|-------------|
|
||||
*|fkOwner |String | Foreign key table schema (may be null)
|
||||
*|fkName |String | Foreign key name (may be null)
|
||||
*|fkTableName |String | Foreign key table name
|
||||
*|fkColumnName |String | Foreign key column name
|
||||
*|keySeq |Number | Sequence number within a foreign key( a value of 1 represents the first column of the foreign key, a value of 2 would represent the second column within the foreign key).
|
||||
*|pkOwner |String | Primary key table schema being imported (may be null)
|
||||
*|pkName |String | Primary key name (may be null)
|
||||
*|pkTableName |String | Primary key table name being imported
|
||||
*|pkColumnName |String | Primary key column name being imported
|
||||
*
|
||||
* @param {String} modelName The model name
|
||||
* @param {Object} options The options
|
||||
* @options {Object} options The options
|
||||
* @property {String} owner|schema The database owner or schema
|
||||
* @param {Function} [cb] The callback function
|
||||
*/
|
||||
DataSource.prototype.discoverExportedForeignKeys = function (modelName, options, cb) {
|
||||
|
@ -1080,17 +1081,14 @@ DataSource.prototype.discoverSchema = function (modelName, options, cb) {
|
|||
};
|
||||
|
||||
/**
|
||||
* Discover schema from a given modelName/view
|
||||
* Discover schema from a given modelName/view.
|
||||
*
|
||||
* `options`
|
||||
*
|
||||
* {String} owner/schema - The database owner/schema name
|
||||
* {Boolean} relations - If relations (primary key/foreign key) are navigated
|
||||
* {Boolean} all - If all owners are included
|
||||
* {Boolean} views - If views are included
|
||||
*
|
||||
* @param {String} modelName The model name
|
||||
* @param {Object} [options] The options
|
||||
* @param {String} modelName The model name.
|
||||
* @options {Object} [options] Options; see below.
|
||||
* @property {String} owner|schema Database owner or schema name.
|
||||
* @property {Boolean} relations True if relations (primary key/foreign key) are navigated; false otherwise.
|
||||
* @property {Boolean} all True if all owners are included; false otherwise.
|
||||
* @property {Boolean} views True if views are included; false otherwise.
|
||||
* @param {Function} [cb] The callback function
|
||||
*/
|
||||
DataSource.prototype.discoverSchemas = function (modelName, options, cb) {
|
||||
|
@ -1252,15 +1250,12 @@ DataSource.prototype.discoverSchemas = function (modelName, options, cb) {
|
|||
/**
|
||||
* Discover schema from a given table/view synchronously
|
||||
*
|
||||
* `options`
|
||||
*
|
||||
* {String} owner/schema - The database owner/schema name
|
||||
* {Boolean} relations - If relations (primary key/foreign key) are navigated
|
||||
* {Boolean} all - If all owners are included
|
||||
* {Boolean} views - If views are included
|
||||
*
|
||||
* @param {String} modelName The model name
|
||||
* @param {Object} [options] The options
|
||||
* @options {Object} [options] Options; see below.
|
||||
* @property {String} owner|schema Database owner or schema name.
|
||||
* @property {Boolean} relations True if relations (primary key/foreign key) are navigated; false otherwise.
|
||||
* @property {Boolean} all True if all owners are included; false otherwise.
|
||||
* @property {Boolean} views True if views are included; false otherwise.
|
||||
*/
|
||||
DataSource.prototype.discoverSchemasSync = function (modelName, options) {
|
||||
var self = this;
|
||||
|
@ -1392,17 +1387,14 @@ DataSource.prototype.discoverSchemasSync = function (modelName, options) {
|
|||
};
|
||||
|
||||
/**
|
||||
* Discover and build models from the given owner/modelName
|
||||
* Discover and build models from the specified owner/modelName.
|
||||
*
|
||||
* `options`
|
||||
*
|
||||
* {String} owner/schema - The database owner/schema name
|
||||
* {Boolean} relations - If relations (primary key/foreign key) are navigated
|
||||
* {Boolean} all - If all owners are included
|
||||
* {Boolean} views - If views are included
|
||||
*
|
||||
* @param {String} modelName The model name
|
||||
* @param {Object} [options] The options
|
||||
* @param {String} modelName The model name.
|
||||
* @options {Object} [options] Options; see below.
|
||||
* @property {String} owner|schema Database owner or schema name.
|
||||
* @property {Boolean} relations True if relations (primary key/foreign key) are navigated; false otherwise.
|
||||
* @property {Boolean} all True if all owners are included; false otherwise.
|
||||
* @property {Boolean} views True if views are included; false otherwise.
|
||||
* @param {Function} [cb] The callback function
|
||||
*/
|
||||
DataSource.prototype.discoverAndBuildModels = function (modelName, options, cb) {
|
||||
|
@ -1429,15 +1421,15 @@ DataSource.prototype.discoverAndBuildModels = function (modelName, options, cb)
|
|||
};
|
||||
|
||||
/**
|
||||
* Discover and build models from the given owner/modelName synchronously
|
||||
*
|
||||
* `options`
|
||||
*
|
||||
* {String} owner/schema - The database owner/schema name
|
||||
* {Boolean} relations - If relations (primary key/foreign key) are navigated
|
||||
* {Boolean} all - If all owners are included
|
||||
* {Boolean} views - If views are included
|
||||
* Discover and build models from the given owner/modelName synchronously.
|
||||
*
|
||||
* @param {String} modelName The model name.
|
||||
* @options {Object} [options] Options; see below.
|
||||
* @property {String} owner|schema Database owner or schema name.
|
||||
* @property {Boolean} relations True if relations (primary key/foreign key) are navigated; false otherwise.
|
||||
* @property {Boolean} all True if all owners are included; false otherwise.
|
||||
* @property {Boolean} views True if views are included; false otherwise.
|
||||
|
||||
* @param {String} modelName The model name
|
||||
* @param {Object} [options] The options
|
||||
*/
|
||||
|
@ -1456,8 +1448,8 @@ DataSource.prototype.discoverAndBuildModelsSync = function (modelName, options)
|
|||
|
||||
/**
|
||||
* Check whether migrations needed
|
||||
* This method make sense only for sql connectors.
|
||||
* @param {String[]} [models] A model name or an array of model names. If not present, apply to all models
|
||||
* This method applies only to SQL connectors.
|
||||
* @param {String|String[]} [models] A model name or an array of model names. If not present, apply to all models.
|
||||
*/
|
||||
DataSource.prototype.isActual = function (models, cb) {
|
||||
this.freeze();
|
||||
|
@ -1513,8 +1505,8 @@ DataSource.prototype.tableName = function (modelName) {
|
|||
/**
|
||||
* Return column name for specified modelName and propertyName
|
||||
* @param {String} modelName The model name
|
||||
* @param propertyName The property name
|
||||
* @returns {String} columnName
|
||||
* @param {String} propertyName The property name
|
||||
* @returns {String} columnName The column name.
|
||||
*/
|
||||
DataSource.prototype.columnName = function (modelName, propertyName) {
|
||||
return this.getModelDefinition(modelName).columnName(this.connector.name, propertyName);
|
||||
|
@ -1523,7 +1515,7 @@ DataSource.prototype.columnName = function (modelName, propertyName) {
|
|||
/**
|
||||
* Return column metadata for specified modelName and propertyName
|
||||
* @param {String} modelName The model name
|
||||
* @param propertyName The property name
|
||||
* @param {String} propertyName The property name
|
||||
* @returns {Object} column metadata
|
||||
*/
|
||||
DataSource.prototype.columnMetadata = function (modelName, propertyName) {
|
||||
|
@ -1622,7 +1614,7 @@ DataSource.prototype.defineForeignKey = function defineForeignKey(className, key
|
|||
|
||||
/**
|
||||
* Close database connection
|
||||
* @param {Fucntion} cb The callback function. Optional.
|
||||
* @param {Function} [cb] The callback function. Optional.
|
||||
*/
|
||||
DataSource.prototype.disconnect = function disconnect(cb) {
|
||||
var self = this;
|
||||
|
@ -1738,7 +1730,6 @@ DataSource.prototype.enableRemote = function (operation) {
|
|||
* and disabled operations. To list the operations, call `dataSource.operations()`.
|
||||
*
|
||||
*```js
|
||||
*
|
||||
* var oracle = loopback.createDataSource({
|
||||
* connector: require('loopback-connector-oracle'),
|
||||
* host: '...',
|
||||
|
@ -1749,8 +1740,8 @@ DataSource.prototype.enableRemote = function (operation) {
|
|||
* **Notes:**
|
||||
*
|
||||
* - Disabled operations will not be added to attached models.
|
||||
* - Disabling the remoting for a method only affects client access (it will still be available from server models).
|
||||
* - Data sources must enable / disable operations before attaching or creating models.
|
||||
* - Disabling the remoting for a method only affects client access (it will still be available from server models).
|
||||
* - Data sources must enable / disable operations before attaching or creating models.
|
||||
* @param {String} operation The operation name
|
||||
*/
|
||||
|
||||
|
@ -1828,11 +1819,11 @@ DataSource.prototype.isRelational = function () {
|
|||
return this.connector && this.connector.relational;
|
||||
};
|
||||
|
||||
/**
|
||||
/*!
|
||||
* Check if the data source is ready.
|
||||
* Returns a Boolean value.
|
||||
* @param obj {Object} ?
|
||||
* @param args {Object} ?
|
||||
* @param {Object} obj ?
|
||||
* @param {Object} args ?
|
||||
*/
|
||||
DataSource.prototype.ready = function (obj, args) {
|
||||
var self = this;
|
||||
|
|
39
lib/geo.js
39
lib/geo.js
|
@ -106,9 +106,12 @@ exports.GeoPoint = GeoPoint;
|
|||
* });
|
||||
* ```
|
||||
* @class GeoPoint
|
||||
* @param {Object} latlong Object with two Number properties: lat and long.
|
||||
* @prop {Number} lat
|
||||
* @prop {Number} lng
|
||||
* @property {Number} lat The latitude in degrees.
|
||||
* @property {Number} lng The longitude in degrees.
|
||||
*
|
||||
* @options {Object} Options Object with two Number properties: lat and long.
|
||||
* @property {Number} lat The latitude point in degrees. Range: -90 to 90.
|
||||
* @property {Number} lng The longitude point in degrees. Range: -90 to 90.
|
||||
*/
|
||||
|
||||
function GeoPoint(data) {
|
||||
|
@ -138,20 +141,18 @@ function GeoPoint(data) {
|
|||
assert(data.lat <= 90, 'lat must be <= 90');
|
||||
assert(data.lat >= -90, 'lat must be >= -90');
|
||||
|
||||
/**
|
||||
* @property {Number} lat The latitude point in degrees. Range: -90 to 90.
|
||||
*/
|
||||
this.lat = data.lat;
|
||||
|
||||
/**
|
||||
* @property {Number} lng The longitude point in degrees. Range: -90 to 90.
|
||||
*/
|
||||
this.lat = data.lat;
|
||||
this.lng = data.lng;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the spherical distance between two GeoPoints.
|
||||
* Specify units of measurement with the 'type' property in options object. Type can be:
|
||||
*
|
||||
* @param {GeoPoint} pointA Point A
|
||||
* @param {GeoPoint} pointB Point B
|
||||
* @options {Object} options Options object with one key, 'type'. See below.
|
||||
* @property {String} type Unit of measurement, one of:
|
||||
*
|
||||
* - `miles` (default)
|
||||
* - `radians`
|
||||
* - `kilometers`
|
||||
|
@ -159,9 +160,6 @@ function GeoPoint(data) {
|
|||
* - `miles`
|
||||
* - `feet`
|
||||
* - `degrees`
|
||||
* @param {GeoPoint} pointA Point A
|
||||
* @param {GeoPoint} pointB Point B
|
||||
* @param {Object} options Options object; has one key, 'type', to specify the units of measurment (see above). Default is miles.
|
||||
*/
|
||||
|
||||
GeoPoint.distanceBetween = function distanceBetween(a, b, options) {
|
||||
|
@ -190,7 +188,16 @@ GeoPoint.distanceBetween = function distanceBetween(a, b, options) {
|
|||
* GeoPoint.distanceBetween(here, there, {type: 'miles'}) // 438
|
||||
* ```
|
||||
* @param {Object} point GeoPoint object to which to measure distance.
|
||||
* @param {Object} options Use type key to specify units of measurment (default is miles).
|
||||
* @options {Object} options Options object with one key, 'type'. See below.
|
||||
* @property {String} type Unit of measurement, one of:
|
||||
*
|
||||
* - `miles` (default)
|
||||
* - `radians`
|
||||
* - `kilometers`
|
||||
* - `meters`
|
||||
* - `miles`
|
||||
* - `feet`
|
||||
* - `degrees`
|
||||
*/
|
||||
|
||||
GeoPoint.prototype.distanceTo = function (point, options) {
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
/**
|
||||
/*!
|
||||
* Module exports
|
||||
*/
|
||||
module.exports = Hookable;
|
||||
|
||||
/**
|
||||
* Hooks mixins
|
||||
/*
|
||||
* Hooks object.
|
||||
* @class Hookable
|
||||
*/
|
||||
|
||||
function Hookable() {
|
||||
|
|
|
@ -36,7 +36,7 @@ function Inclusion() {
|
|||
*``` Passport.include(passports, {owner: [{posts: 'images'}, 'passports']});
|
||||
*
|
||||
* @param {Array} objects Array of instances
|
||||
* @param {String}, {Object} or {Array} include Which relations to load.
|
||||
* @param {String|Object|Array} include Which relations to load.
|
||||
* @param {Function} cb Callback called when relations are loaded
|
||||
*
|
||||
*/
|
||||
|
|
13
lib/list.js
13
lib/list.js
|
@ -13,7 +13,7 @@ function List(items, itemType, parent) {
|
|||
try {
|
||||
items = JSON.parse(items);
|
||||
} catch (e) {
|
||||
throw new Error('could not create List from JSON string: ', items);
|
||||
throw new Error(util.format('could not create List from JSON string: %j', items));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -22,7 +22,7 @@ function List(items, itemType, parent) {
|
|||
|
||||
items = items || [];
|
||||
if (!Array.isArray(items)) {
|
||||
throw new Error('Items must be an array: ' + items);
|
||||
throw new Error(util.format('Items must be an array: %j', items));
|
||||
}
|
||||
|
||||
if(!itemType) {
|
||||
|
@ -92,12 +92,3 @@ List.prototype.toString = function () {
|
|||
return JSON.stringify(this.toJSON());
|
||||
};
|
||||
|
||||
/*
|
||||
var strArray = new List(['1', 2], String);
|
||||
strArray.push(3);
|
||||
console.log(strArray);
|
||||
console.log(strArray.length);
|
||||
|
||||
console.log(strArray.toJSON());
|
||||
console.log(strArray.toString());
|
||||
*/
|
||||
|
|
|
@ -28,18 +28,14 @@ var slice = Array.prototype.slice;
|
|||
|
||||
/**
|
||||
* ModelBuilder - A builder to define data models.
|
||||
*
|
||||
*
|
||||
* @property {Object} definitions Definitions of the models.
|
||||
* @property {Object} models Model constructors
|
||||
* @class
|
||||
*/
|
||||
function ModelBuilder() {
|
||||
// create blank models pool
|
||||
/**
|
||||
* @property {Object} models Model constructors
|
||||
*/
|
||||
this.models = {};
|
||||
/**
|
||||
* @property {Object} definitions Definitions of the models
|
||||
*/
|
||||
this.definitions = {};
|
||||
}
|
||||
|
||||
|
@ -61,7 +57,7 @@ function isModelClass(cls) {
|
|||
*
|
||||
* @param {String} name The model name
|
||||
* @param {Boolean} forceCreate Whether the create a stub for the given name if a model doesn't exist.
|
||||
* Returns {*} The model class
|
||||
* @returns {*} The model class
|
||||
*/
|
||||
ModelBuilder.prototype.getModel = function (name, forceCreate) {
|
||||
var model = this.models[name];
|
||||
|
@ -268,7 +264,11 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett
|
|||
|
||||
// A function to loop through the properties
|
||||
ModelClass.forEachProperty = function (cb) {
|
||||
Object.keys(ModelClass.definition.properties).forEach(cb);
|
||||
var props = ModelClass.definition.properties;
|
||||
var keys = Object.keys(props);
|
||||
for (var i = 0, n = keys.length; i < n; i++) {
|
||||
cb(keys[i], props[keys[i]]);
|
||||
}
|
||||
};
|
||||
|
||||
// A function to attach the model class to a data source
|
||||
|
@ -276,7 +276,26 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett
|
|||
dataSource.attach(this);
|
||||
};
|
||||
|
||||
// A function to extend the model
|
||||
/** Extend the model with the specified model, properties, and other settings.
|
||||
* For example, to extend an existing model, for example, a built-in model:
|
||||
*
|
||||
* ```js
|
||||
* var Customer = User.extend('customer', {
|
||||
* accountId: String,
|
||||
* vip: Boolean
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* To extend the base model, essentially creating a new model:
|
||||
* ```js
|
||||
* var user = loopback.Model.extend('user', properties, options);
|
||||
* ```
|
||||
*
|
||||
* @param {String} className Name of the new model being defined.
|
||||
* @options {Object} properties Properties to define for the model, added to properties of model being extended.
|
||||
* @options {Object} settings Model settings, such as relations and acls.
|
||||
*
|
||||
*/
|
||||
ModelClass.extend = function (className, subclassProperties, subclassSettings) {
|
||||
var properties = ModelClass.definition.properties;
|
||||
var settings = ModelClass.definition.settings;
|
||||
|
@ -294,27 +313,22 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett
|
|||
}
|
||||
|
||||
// Merging the properties
|
||||
Object.keys(properties).forEach(function (key) {
|
||||
var keys = Object.keys(properties);
|
||||
for (var i = 0, n = keys.length; i < n; i++) {
|
||||
var key = keys[i];
|
||||
|
||||
if (idFound && properties[key].id) {
|
||||
// don't inherit id properties
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
if (subclassProperties[key] === undefined) {
|
||||
subclassProperties[key] = properties[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Merge the settings
|
||||
subclassSettings = mergeSettings(settings, subclassSettings);
|
||||
|
||||
/*
|
||||
Object.keys(settings).forEach(function (key) {
|
||||
if(subclassSettings[key] === undefined) {
|
||||
subclassSettings[key] = settings[key];
|
||||
}
|
||||
});
|
||||
*/
|
||||
|
||||
// Define the subclass
|
||||
var subClass = modelBuilder.define(className, subclassProperties, subclassSettings, ModelClass);
|
||||
|
||||
|
@ -355,20 +369,15 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett
|
|||
var DataType = ModelClass.definition.properties[propertyName].type;
|
||||
if (Array.isArray(DataType) || DataType === Array) {
|
||||
DataType = List;
|
||||
} else if (DataType.name === 'Date') {
|
||||
var OrigDate = Date;
|
||||
DataType = function Date(arg) {
|
||||
return new OrigDate(arg);
|
||||
};
|
||||
} else if (DataType === Date) {
|
||||
DataType = DateType;
|
||||
} else if (typeof DataType === 'string') {
|
||||
DataType = modelBuilder.resolveType(DataType);
|
||||
}
|
||||
if (ModelClass.setter[propertyName]) {
|
||||
ModelClass.setter[propertyName].call(this, value); // Try setter first
|
||||
} else {
|
||||
if (!this.__data) {
|
||||
this.__data = {};
|
||||
}
|
||||
this.__data = this.__data || {};
|
||||
if (value === null || value === undefined) {
|
||||
this.__data[propertyName] = value;
|
||||
} else {
|
||||
|
@ -386,15 +395,6 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett
|
|||
enumerable: true
|
||||
});
|
||||
|
||||
// <propertyName>$was --> __dataWas.<propertyName>
|
||||
Object.defineProperty(ModelClass.prototype, propertyName + '$was', {
|
||||
get: function () {
|
||||
return this.__dataWas && this.__dataWas[propertyName];
|
||||
},
|
||||
configurable: true,
|
||||
enumerable: false
|
||||
});
|
||||
|
||||
// FIXME: [rfeng] Do we need to keep the raw data?
|
||||
// Use $ as the prefix to avoid conflicts with properties such as _id
|
||||
Object.defineProperty(ModelClass.prototype, '$' + propertyName, {
|
||||
|
@ -412,7 +412,13 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett
|
|||
});
|
||||
};
|
||||
|
||||
ModelClass.forEachProperty(ModelClass.registerProperty);
|
||||
var props = ModelClass.definition.properties;
|
||||
var keys = Object.keys(props);
|
||||
var size = keys.length;
|
||||
for (i = 0; i < size; i++) {
|
||||
var propertyName = keys[i];
|
||||
ModelClass.registerProperty(propertyName);
|
||||
}
|
||||
|
||||
ModelClass.emit('defined', ModelClass);
|
||||
|
||||
|
@ -420,6 +426,11 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett
|
|||
|
||||
};
|
||||
|
||||
// DataType for Date
|
||||
function DateType(arg) {
|
||||
return new Date(arg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define single property named `propertyName` on `model`
|
||||
*
|
||||
|
@ -442,17 +453,20 @@ ModelBuilder.prototype.defineValueType = function(type, aliases) {
|
|||
};
|
||||
|
||||
/**
|
||||
* Extend existing model with bunch of properties
|
||||
* Extend existing model with specified properties
|
||||
*
|
||||
* Example:
|
||||
* Instead of doing amending the content model with competition attributes like this:
|
||||
* Instead of extending a model with attributes like this (for example):
|
||||
*
|
||||
* ```js
|
||||
* db.defineProperty('Content', 'competitionType', { type: String });
|
||||
* db.defineProperty('Content', 'expiryDate', { type: Date, index: true });
|
||||
* db.defineProperty('Content', 'isExpired', { type: Boolean, index: true });
|
||||
* db.defineProperty('Content', 'competitionType',
|
||||
* { type: String });
|
||||
* db.defineProperty('Content', 'expiryDate',
|
||||
* { type: Date, index: true });
|
||||
* db.defineProperty('Content', 'isExpired',
|
||||
* { type: Boolean, index: true });
|
||||
*```
|
||||
* The extendModel() method enables you to extend the content model with competition attributes.
|
||||
* This method enables you to extend a model as follows (for example):
|
||||
* ```js
|
||||
* db.extendModel('Content', {
|
||||
* competitionType: String,
|
||||
|
@ -462,14 +476,19 @@ ModelBuilder.prototype.defineValueType = function(type, aliases) {
|
|||
*```
|
||||
*
|
||||
* @param {String} model Name of model
|
||||
* @param {Object} props Hash of properties
|
||||
* @options {Object} properties JSON object specifying properties. Each property is a key whos value is
|
||||
* either the [type](http://docs.strongloop.com/display/DOC/LDL+data+types) or `propertyName: {options}`
|
||||
* where the options are described below.
|
||||
* @property {String} type Datatype of property: Must be an [LDL type](http://docs.strongloop.com/display/DOC/LDL+data+types).
|
||||
* @property {Boolean} index True if the property is an index; false otherwise.
|
||||
*/
|
||||
ModelBuilder.prototype.extendModel = function (model, props) {
|
||||
var t = this;
|
||||
Object.keys(props).forEach(function (propName) {
|
||||
var definition = props[propName];
|
||||
t.defineProperty(model, propName, definition);
|
||||
});
|
||||
var keys = Object.keys(props);
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var definition = props[keys[i]];
|
||||
t.defineProperty(model, keys[i], definition);
|
||||
}
|
||||
};
|
||||
|
||||
ModelBuilder.prototype.copyModel = function copyModel(Master) {
|
||||
|
|
331
lib/model.js
331
lib/model.js
|
@ -8,13 +8,20 @@ module.exports = ModelBaseClass;
|
|||
*/
|
||||
|
||||
var util = require('util');
|
||||
var traverse = require('traverse');
|
||||
var jutil = require('./jutil');
|
||||
var List = require('./list');
|
||||
var Hookable = require('./hooks');
|
||||
var validations = require('./validations.js');
|
||||
|
||||
var BASE_TYPES = ['String', 'Boolean', 'Number', 'Date', 'Text', 'ObjectID'];
|
||||
// Set up an object for quick lookup
|
||||
var BASE_TYPES = {
|
||||
'String': true,
|
||||
'Boolean': true,
|
||||
'Number': true,
|
||||
'Date': true,
|
||||
'Text': true,
|
||||
'ObjectID': true
|
||||
};
|
||||
|
||||
/**
|
||||
* Model class: base class for all persistent objects.
|
||||
|
@ -33,18 +40,6 @@ function ModelBaseClass(data, options) {
|
|||
this._initProperties(data, options);
|
||||
}
|
||||
|
||||
// FIXME: [rfeng] We need to make sure the input data should not be mutated. Disabled cloning for now to get tests passing
|
||||
function clone(data) {
|
||||
/*
|
||||
if(!(data instanceof ModelBaseClass)) {
|
||||
if(data && (Array.isArray(data) || 'object' === typeof data)) {
|
||||
return traverse(data).clone();
|
||||
}
|
||||
}
|
||||
*/
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the model instance with a list of properties
|
||||
* @param {Object} data The data object
|
||||
|
@ -58,10 +53,10 @@ ModelBaseClass.prototype._initProperties = function (data, options) {
|
|||
var ctor = this.constructor;
|
||||
|
||||
if(data instanceof ctor) {
|
||||
// Convert the data to be plain object to avoid polutions
|
||||
// Convert the data to be plain object to avoid pollutions
|
||||
data = data.toObject(false);
|
||||
}
|
||||
var properties = ctor.definition.build();
|
||||
var properties = ctor.definition.properties;
|
||||
data = data || {};
|
||||
|
||||
options = options || {};
|
||||
|
@ -71,133 +66,124 @@ ModelBaseClass.prototype._initProperties = function (data, options) {
|
|||
if(strict === undefined) {
|
||||
strict = ctor.definition.settings.strict;
|
||||
}
|
||||
Object.defineProperty(this, '__cachedRelations', {
|
||||
writable: true,
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
value: {}
|
||||
});
|
||||
|
||||
Object.defineProperty(this, '__data', {
|
||||
writable: true,
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
value: {}
|
||||
});
|
||||
if (ctor.hideInternalProperties) {
|
||||
// Object.defineProperty() is expensive. We only try to make the internal
|
||||
// properties hidden (non-enumerable) if the model class has the
|
||||
// `hideInternalProperties` set to true
|
||||
Object.defineProperties(this, {
|
||||
__cachedRelations: {
|
||||
writable: true,
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
value: {}
|
||||
},
|
||||
|
||||
Object.defineProperty(this, '__dataWas', {
|
||||
writable: true,
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
value: {}
|
||||
});
|
||||
__data: {
|
||||
writable: true,
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
value: {}
|
||||
},
|
||||
|
||||
/**
|
||||
* Instance level data source
|
||||
*/
|
||||
Object.defineProperty(this, '__dataSource', {
|
||||
writable: true,
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
value: options.dataSource
|
||||
});
|
||||
// Instance level data source
|
||||
__dataSource: {
|
||||
writable: true,
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
value: options.dataSource
|
||||
},
|
||||
|
||||
/**
|
||||
* Instance level strict mode
|
||||
*/
|
||||
Object.defineProperty(this, '__strict', {
|
||||
writable: true,
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
value: strict
|
||||
});
|
||||
// Instance level strict mode
|
||||
__strict: {
|
||||
writable: true,
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
value: strict
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.__cachedRelations = {};
|
||||
this.__data = {};
|
||||
this.__dataSource = options.dataSource;
|
||||
this.__strict = strict;
|
||||
}
|
||||
|
||||
if (data.__cachedRelations) {
|
||||
this.__cachedRelations = data.__cachedRelations;
|
||||
}
|
||||
|
||||
for (var i in data) {
|
||||
if (i in properties && typeof data[i] !== 'function') {
|
||||
this.__data[i] = this.__dataWas[i] = clone(data[i]);
|
||||
} else if (i in ctor.relations) {
|
||||
if (ctor.relations[i].type === 'belongsTo' && data[i] !== null && data[i] !== undefined) {
|
||||
var keys = Object.keys(data);
|
||||
var size = keys.length;
|
||||
var p, propVal;
|
||||
for (var k = 0; k < size; k++) {
|
||||
p = keys[k];
|
||||
propVal = data[p];
|
||||
if (typeof propVal === 'function') {
|
||||
continue;
|
||||
}
|
||||
if (properties[p]) {
|
||||
// Managed property
|
||||
if (applySetters) {
|
||||
self[p] = propVal;
|
||||
} else {
|
||||
self.__data[p] = propVal;
|
||||
}
|
||||
} else if (ctor.relations[p]) {
|
||||
// Relation
|
||||
if (ctor.relations[p].type === 'belongsTo' && propVal != null) {
|
||||
// If the related model is populated
|
||||
this.__data[ctor.relations[i].keyFrom] = this.__dataWas[i] = data[i][ctor.relations[i].keyTo];
|
||||
self.__data[ctor.relations[p].keyFrom] = propVal[ctor.relations[p].keyTo];
|
||||
}
|
||||
this.__cachedRelations[i] = data[i];
|
||||
self.__cachedRelations[p] = propVal;
|
||||
} else {
|
||||
// Un-managed property
|
||||
if (strict === false) {
|
||||
this.__data[i] = this.__dataWas[i] = clone(data[i]);
|
||||
self[p] = self.__data[p] = propVal;
|
||||
} else if (strict === 'throw') {
|
||||
throw new Error('Unknown property: ' + i);
|
||||
throw new Error('Unknown property: ' + p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var propertyName;
|
||||
if (applySetters === true) {
|
||||
for (propertyName in data) {
|
||||
if (typeof data[propertyName] !== 'function' && ((propertyName in properties) || (propertyName in ctor.relations))) {
|
||||
self[propertyName] = self.__data[propertyName] || data[propertyName];
|
||||
keys = Object.keys(properties);
|
||||
size = keys.length;
|
||||
|
||||
for (k = 0; k < size; k++) {
|
||||
p = keys[k];
|
||||
propVal = self.__data[p];
|
||||
|
||||
// Set default values
|
||||
if (propVal === undefined) {
|
||||
var def = properties[p]['default'];
|
||||
if (def !== undefined) {
|
||||
if (typeof def === 'function') {
|
||||
self.__data[p] = def();
|
||||
} else {
|
||||
self.__data[p] = def;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set the unknown properties as properties to the object
|
||||
if (strict === false) {
|
||||
for (propertyName in data) {
|
||||
if (typeof data[propertyName] !== 'function' && !(propertyName in properties)) {
|
||||
self[propertyName] = self.__data[propertyName] || data[propertyName];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctor.forEachProperty(function (propertyName) {
|
||||
|
||||
if (undefined === self.__data[propertyName]) {
|
||||
self.__data[propertyName] = self.__dataWas[propertyName] = getDefault(propertyName);
|
||||
} else {
|
||||
self.__dataWas[propertyName] = self.__data[propertyName];
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
ctor.forEachProperty(function (propertyName) {
|
||||
|
||||
var type = properties[propertyName].type;
|
||||
|
||||
if (BASE_TYPES.indexOf(type.name) === -1) {
|
||||
if (typeof self.__data[propertyName] !== 'object' && self.__data[propertyName]) {
|
||||
// Handle complex types (JSON/Object)
|
||||
var type = properties[p].type;
|
||||
if (! BASE_TYPES[type.name]) {
|
||||
if (typeof self.__data[p] !== 'object' && self.__data[p]) {
|
||||
try {
|
||||
self.__data[propertyName] = JSON.parse(self.__data[propertyName] + '');
|
||||
self.__data[p] = JSON.parse(self.__data[p] + '');
|
||||
} catch (e) {
|
||||
self.__data[propertyName] = String(self.__data[propertyName]);
|
||||
self.__data[p] = String(self.__data[p]);
|
||||
}
|
||||
}
|
||||
if (type.name === 'Array' || Array.isArray(type)) {
|
||||
if (!(self.__data[propertyName] instanceof List)
|
||||
&& self.__data[propertyName] !== undefined
|
||||
&& self.__data[propertyName] !== null ) {
|
||||
self.__data[propertyName] = List(self.__data[propertyName], type, self);
|
||||
if (!(self.__data[p] instanceof List)
|
||||
&& self.__data[p] !== undefined
|
||||
&& self.__data[p] !== null ) {
|
||||
self.__data[p] = List(self.__data[p], type, self);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
function getDefault(propertyName) {
|
||||
var def = properties[propertyName]['default'];
|
||||
if (def !== undefined) {
|
||||
if (typeof def === 'function') {
|
||||
return def();
|
||||
} else {
|
||||
return def;
|
||||
}
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
this.trigger('initialize');
|
||||
};
|
||||
|
||||
|
@ -242,7 +228,7 @@ ModelBaseClass.toString = function () {
|
|||
* @param {Boolean} onlySchema Restrict properties to dataSource only. Default is false. If true, the function returns only properties defined in the schema; Otherwise it returns all enumerable properties.
|
||||
*/
|
||||
ModelBaseClass.prototype.toObject = function (onlySchema, removeHidden) {
|
||||
if(onlySchema === undefined) {
|
||||
if (onlySchema === undefined) {
|
||||
onlySchema = true;
|
||||
}
|
||||
var data = {};
|
||||
|
@ -250,47 +236,63 @@ ModelBaseClass.prototype.toObject = function (onlySchema, removeHidden) {
|
|||
var Model = this.constructor;
|
||||
|
||||
// if it is already an Object
|
||||
if(Model === Object) return self;
|
||||
if (Model === Object) {
|
||||
return self;
|
||||
}
|
||||
|
||||
var strict = this.__strict;
|
||||
var schemaLess = (strict === false) || !onlySchema;
|
||||
|
||||
this.constructor.forEachProperty(function (propertyName) {
|
||||
if (removeHidden && Model.isHiddenProperty(propertyName)) {
|
||||
return;
|
||||
}
|
||||
if (typeof self[propertyName] === 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (self[propertyName] instanceof List) {
|
||||
data[propertyName] = self[propertyName].toObject(!schemaLess, removeHidden);
|
||||
} else if (self.__data.hasOwnProperty(propertyName)) {
|
||||
if (self[propertyName] !== undefined && self[propertyName] !== null && self[propertyName].toObject) {
|
||||
data[propertyName] = self[propertyName].toObject(!schemaLess, removeHidden);
|
||||
} else {
|
||||
data[propertyName] = self[propertyName];
|
||||
}
|
||||
} else {
|
||||
data[propertyName] = null;
|
||||
}
|
||||
});
|
||||
var props = Model.definition.properties;
|
||||
var keys = Object.keys(props);
|
||||
var propertyName, val;
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
propertyName = keys[i];
|
||||
val = self[propertyName];
|
||||
|
||||
// Exclude functions
|
||||
if (typeof val === 'function') {
|
||||
continue;
|
||||
}
|
||||
// Exclude hidden properties
|
||||
if (removeHidden && Model.isHiddenProperty(propertyName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (val instanceof List) {
|
||||
data[propertyName] = val.toObject(!schemaLess, removeHidden);
|
||||
} else {
|
||||
if (val !== undefined && val !== null && val.toObject) {
|
||||
data[propertyName] = val.toObject(!schemaLess, removeHidden);
|
||||
} else {
|
||||
data[propertyName] = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var val = null;
|
||||
if (schemaLess) {
|
||||
// Find its own properties which can be set via myModel.myProperty = 'myValue'.
|
||||
// If the property is not declared in the model definition, no setter will be
|
||||
// triggered to add it to __data
|
||||
for (var propertyName in self) {
|
||||
if(removeHidden && Model.isHiddenProperty(propertyName)) {
|
||||
keys = Object.keys(self);
|
||||
var size = keys.length;
|
||||
for (i = 0; i < size; i++) {
|
||||
propertyName = keys[i];
|
||||
if (props[propertyName]) {
|
||||
continue;
|
||||
}
|
||||
if(self.hasOwnProperty(propertyName) && (!data.hasOwnProperty(propertyName))) {
|
||||
val = self[propertyName];
|
||||
if (propertyName.indexOf('__') === 0) {
|
||||
continue;
|
||||
}
|
||||
if (removeHidden && Model.isHiddenProperty(propertyName)) {
|
||||
continue;
|
||||
}
|
||||
val = self[propertyName];
|
||||
if (val !== undefined && data[propertyName] === undefined) {
|
||||
if (typeof val === 'function') {
|
||||
continue;
|
||||
}
|
||||
if (val !== undefined && val !== null && val.toObject) {
|
||||
if (val !== null && val.toObject) {
|
||||
data[propertyName] = val.toObject(!schemaLess, removeHidden);
|
||||
} else {
|
||||
data[propertyName] = val;
|
||||
|
@ -298,15 +300,25 @@ ModelBaseClass.prototype.toObject = function (onlySchema, removeHidden) {
|
|||
}
|
||||
}
|
||||
// Now continue to check __data
|
||||
for (propertyName in self.__data) {
|
||||
if (!data.hasOwnProperty(propertyName)) {
|
||||
if(removeHidden && Model.isHiddenProperty(propertyName)) {
|
||||
keys = Object.keys(self.__data);
|
||||
size = keys.length;
|
||||
for (i = 0; i < size; i++) {
|
||||
propertyName = keys[i];
|
||||
if (propertyName.indexOf('__') === 0) {
|
||||
continue;
|
||||
}
|
||||
if (data[propertyName] === undefined) {
|
||||
if (removeHidden && Model.isHiddenProperty(propertyName)) {
|
||||
continue;
|
||||
}
|
||||
val = self.hasOwnProperty(propertyName) ? self[propertyName] : self.__data[propertyName];
|
||||
var ownVal = self[propertyName];
|
||||
// The ownVal can be a relation function
|
||||
val = (ownVal !== undefined && (typeof ownVal !== 'function'))
|
||||
? ownVal : self.__data[propertyName];
|
||||
if (typeof val === 'function') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (val !== undefined && val !== null && val.toObject) {
|
||||
data[propertyName] = val.toObject(!schemaLess, removeHidden);
|
||||
} else {
|
||||
|
@ -319,12 +331,20 @@ ModelBaseClass.prototype.toObject = function (onlySchema, removeHidden) {
|
|||
return data;
|
||||
};
|
||||
|
||||
ModelBaseClass.isHiddenProperty = function(propertyName) {
|
||||
ModelBaseClass.isHiddenProperty = function (propertyName) {
|
||||
var Model = this;
|
||||
var settings = Model.definition && Model.definition.settings;
|
||||
var hiddenProperties = settings && settings.hidden;
|
||||
if(hiddenProperties) {
|
||||
return ~hiddenProperties.indexOf(propertyName);
|
||||
var hiddenProperties = settings && (settings.hiddenProperties || settings.hidden);
|
||||
if (Array.isArray(hiddenProperties)) {
|
||||
// Cache the hidden properties as an object for quick lookup
|
||||
settings.hiddenProperties = {};
|
||||
for (var i = 0; i < hiddenProperties.length; i++) {
|
||||
settings.hiddenProperties[hiddenProperties[i]] = true;
|
||||
}
|
||||
hiddenProperties = settings.hiddenProperties;
|
||||
}
|
||||
if (hiddenProperties) {
|
||||
return hiddenProperties[propertyName];
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
@ -340,16 +360,6 @@ ModelBaseClass.prototype.fromObject = function (obj) {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks is property changed based on current property and initial value
|
||||
*
|
||||
* @param {String} propertyName Property name
|
||||
* @return Boolean
|
||||
*/
|
||||
ModelBaseClass.prototype.propertyChanged = function propertyChanged(propertyName) {
|
||||
return this.__data[propertyName] !== this.__dataWas[propertyName];
|
||||
};
|
||||
|
||||
/**
|
||||
* Reset dirty attributes.
|
||||
* This method does not perform any database operations; it just resets the object to its
|
||||
|
@ -361,9 +371,6 @@ ModelBaseClass.prototype.reset = function () {
|
|||
if (k !== 'id' && !obj.constructor.dataSource.definitions[obj.constructor.modelName].properties[k]) {
|
||||
delete obj[k];
|
||||
}
|
||||
if (obj.propertyChanged(k)) {
|
||||
obj[k] = obj[k + '$was'];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,838 @@
|
|||
/*!
|
||||
* Dependencies
|
||||
*/
|
||||
var assert = require('assert');
|
||||
var util = require('util');
|
||||
var i8n = require('inflection');
|
||||
var defineScope = require('./scope.js').defineScope;
|
||||
var ModelBaseClass = require('./model.js');
|
||||
|
||||
exports.Relation = Relation;
|
||||
exports.RelationDefinition = RelationDefinition;
|
||||
|
||||
var RelationTypes = {
|
||||
belongsTo: 'belongsTo',
|
||||
hasMany: 'hasMany',
|
||||
hasOne: 'hasOne',
|
||||
hasAndBelongsToMany: 'hasAndBelongsToMany'
|
||||
};
|
||||
|
||||
exports.RelationTypes = RelationTypes;
|
||||
exports.HasMany = HasMany;
|
||||
exports.HasManyThrough = HasManyThrough;
|
||||
exports.HasOne = HasOne;
|
||||
exports.HasAndBelongsToMany = HasAndBelongsToMany;
|
||||
exports.BelongsTo = BelongsTo;
|
||||
|
||||
var RelationClasses = {
|
||||
belongsTo: BelongsTo,
|
||||
hasMany: HasMany,
|
||||
hasManyThrough: HasManyThrough,
|
||||
hasOne: HasOne,
|
||||
hasAndBelongsToMany: HasAndBelongsToMany
|
||||
};
|
||||
|
||||
function normalizeType(type) {
|
||||
if (!type) {
|
||||
return type;
|
||||
}
|
||||
var t1 = type.toLowerCase();
|
||||
for (var t2 in RelationTypes) {
|
||||
if (t2.toLowerCase() === t1) {
|
||||
return t2;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Relation definition class. Use to define relationships between models.
|
||||
* @param {Object} definition
|
||||
* @class RelationDefinition
|
||||
*/
|
||||
function RelationDefinition(definition) {
|
||||
if (!(this instanceof RelationDefinition)) {
|
||||
return new RelationDefinition(definition);
|
||||
}
|
||||
definition = definition || {};
|
||||
this.name = definition.name;
|
||||
assert(this.name, 'Relation name is missing');
|
||||
this.type = normalizeType(definition.type);
|
||||
assert(this.type, 'Invalid relation type: ' + definition.type);
|
||||
this.modelFrom = definition.modelFrom;
|
||||
assert(this.modelFrom);
|
||||
this.keyFrom = definition.keyFrom;
|
||||
this.modelTo = definition.modelTo;
|
||||
assert(this.modelTo);
|
||||
this.keyTo = definition.keyTo;
|
||||
this.modelThrough = definition.modelThrough;
|
||||
this.keyThrough = definition.keyThrough;
|
||||
this.multiple = (this.type !== 'belongsTo' && this.type !== 'hasOne');
|
||||
}
|
||||
|
||||
RelationDefinition.prototype.toJSON = function () {
|
||||
var json = {
|
||||
name: this.name,
|
||||
type: this.type,
|
||||
modelFrom: this.modelFrom.modelName,
|
||||
keyFrom: this.keyFrom,
|
||||
modelTo: this.modelTo.modelName,
|
||||
keyTo: this.keyTo,
|
||||
multiple: this.multiple
|
||||
};
|
||||
if (this.modelThrough) {
|
||||
json.modelThrough = this.modelThrough.modelName;
|
||||
json.keyThrough = this.keyThrough;
|
||||
}
|
||||
return json;
|
||||
};
|
||||
|
||||
/**
|
||||
* A relation attaching to a given model instance
|
||||
* @param {RelationDefinition|Object} definition
|
||||
* @param {Object} modelInstance
|
||||
* @returns {Relation}
|
||||
* @constructor
|
||||
* @class Relation
|
||||
*/
|
||||
function Relation(definition, modelInstance) {
|
||||
if (!(this instanceof Relation)) {
|
||||
return new Relation(definition, modelInstance);
|
||||
}
|
||||
if (!(definition instanceof RelationDefinition)) {
|
||||
definition = new RelationDefinition(definition);
|
||||
}
|
||||
this.definition = definition;
|
||||
this.modelInstance = modelInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* HasMany subclass
|
||||
* @param {RelationDefinition|Object} definition
|
||||
* @param {Object} modelInstance
|
||||
* @returns {HasMany}
|
||||
* @constructor
|
||||
* @class HasMany
|
||||
*/
|
||||
function HasMany(definition, modelInstance) {
|
||||
if (!(this instanceof HasMany)) {
|
||||
return new HasMany(definition, modelInstance);
|
||||
}
|
||||
assert(definition.type === RelationTypes.hasMany);
|
||||
Relation.apply(this, arguments);
|
||||
}
|
||||
|
||||
util.inherits(HasMany, Relation);
|
||||
|
||||
/**
|
||||
* HasManyThrough subclass
|
||||
* @param {RelationDefinition|Object} definition
|
||||
* @param {Object} modelInstance
|
||||
* @returns {HasManyThrough}
|
||||
* @constructor
|
||||
* @class HasManyThrough
|
||||
*/
|
||||
function HasManyThrough(definition, modelInstance) {
|
||||
if (!(this instanceof HasManyThrough)) {
|
||||
return new HasManyThrough(definition, modelInstance);
|
||||
}
|
||||
assert(definition.type === RelationTypes.hasMany);
|
||||
assert(definition.modelThrough);
|
||||
HasMany.apply(this, arguments);
|
||||
}
|
||||
|
||||
util.inherits(HasManyThrough, HasMany);
|
||||
|
||||
/**
|
||||
* BelongsTo subclass
|
||||
* @param {RelationDefinition|Object} definition
|
||||
* @param {Object} modelInstance
|
||||
* @returns {BelongsTo}
|
||||
* @constructor
|
||||
* @class BelongsTo
|
||||
*/
|
||||
function BelongsTo(definition, modelInstance) {
|
||||
if (!(this instanceof BelongsTo)) {
|
||||
return new BelongsTo(definition, modelInstance);
|
||||
}
|
||||
assert(definition.type === RelationTypes.belongsTo);
|
||||
Relation.apply(this, arguments);
|
||||
}
|
||||
|
||||
util.inherits(BelongsTo, Relation);
|
||||
|
||||
/**
|
||||
* HasAndBelongsToMany subclass
|
||||
* @param {RelationDefinition|Object} definition
|
||||
* @param {Object} modelInstance
|
||||
* @returns {HasAndBelongsToMany}
|
||||
* @constructor
|
||||
* @class HasAndBelongsToMany
|
||||
*/
|
||||
function HasAndBelongsToMany(definition, modelInstance) {
|
||||
if (!(this instanceof HasAndBelongsToMany)) {
|
||||
return new HasAndBelongsToMany(definition, modelInstance);
|
||||
}
|
||||
assert(definition.type === RelationTypes.hasAndBelongsToMany);
|
||||
Relation.apply(this, arguments);
|
||||
}
|
||||
|
||||
util.inherits(HasAndBelongsToMany, Relation);
|
||||
|
||||
/**
|
||||
* HasOne subclass
|
||||
* @param {RelationDefinition|Object} definition
|
||||
* @param {Object} modelInstance
|
||||
* @returns {HasOne}
|
||||
* @constructor
|
||||
* @class HasOne
|
||||
*/
|
||||
function HasOne(definition, modelInstance) {
|
||||
if (!(this instanceof HasOne)) {
|
||||
return new HasOne(definition, modelInstance);
|
||||
}
|
||||
assert(definition.type === RelationTypes.hasOne);
|
||||
Relation.apply(this, arguments);
|
||||
}
|
||||
|
||||
util.inherits(HasOne, Relation);
|
||||
|
||||
|
||||
/*!
|
||||
* Find the relation by foreign key
|
||||
* @param {*} foreignKey The foreign key
|
||||
* @returns {Object} The relation object
|
||||
*/
|
||||
function findBelongsTo(modelFrom, modelTo, keyTo) {
|
||||
var relations = modelFrom.relations;
|
||||
var keys = Object.keys(relations);
|
||||
for (var k = 0; k < keys.length; k++) {
|
||||
var rel = relations[keys[k]];
|
||||
if (rel.type === RelationTypes.belongsTo &&
|
||||
rel.modelTo === modelTo &&
|
||||
rel.keyTo === keyTo) {
|
||||
return rel.keyFrom;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/*!
|
||||
* Look up a model by name from the list of given models
|
||||
* @param {Object} models Models keyed by name
|
||||
* @param {String} modelName The model name
|
||||
* @returns {*} The matching model class
|
||||
*/
|
||||
function lookupModel(models, modelName) {
|
||||
if(models[modelName]) {
|
||||
return models[modelName];
|
||||
}
|
||||
var lookupClassName = modelName.toLowerCase();
|
||||
for (var name in models) {
|
||||
if (name.toLowerCase() === lookupClassName) {
|
||||
return models[name];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a "one to many" relationship by specifying the model name
|
||||
*
|
||||
* Examples:
|
||||
* ```
|
||||
* User.hasMany(Post, {as: 'posts', foreignKey: 'authorId'});
|
||||
* ```
|
||||
*
|
||||
* ```
|
||||
* Book.hasMany(Chapter);
|
||||
* ```
|
||||
* Or, equivalently:
|
||||
* ```
|
||||
* Book.hasMany('chapters', {model: Chapter});
|
||||
* ```
|
||||
* @param {Model} modelFrom Source model class
|
||||
* @param {Object|String} modelTo Model object (or String name of model) to which you are creating the relationship.
|
||||
* @options {Object} params Configuration parameters; see below.
|
||||
* @property {String} as Name of the property in the referring model that corresponds to the foreign key field in the related model.
|
||||
* @property {String} foreignKey Property name of foreign key field.
|
||||
* @property {Object} model Model object
|
||||
*/
|
||||
RelationDefinition.hasMany = function hasMany(modelFrom, modelTo, params) {
|
||||
var thisClassName = modelFrom.modelName;
|
||||
params = params || {};
|
||||
if (typeof modelTo === 'string') {
|
||||
params.as = modelTo;
|
||||
if (params.model) {
|
||||
modelTo = params.model;
|
||||
} else {
|
||||
var modelToName = i8n.singularize(modelTo).toLowerCase();
|
||||
modelTo = lookupModel(modelFrom.dataSource.modelBuilder.models, modelToName);
|
||||
}
|
||||
}
|
||||
var relationName = params.as || i8n.camelize(modelTo.pluralModelName, true);
|
||||
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,
|
||||
modelFrom: modelFrom,
|
||||
keyFrom: idName,
|
||||
keyTo: fk,
|
||||
modelTo: modelTo,
|
||||
multiple: true
|
||||
});
|
||||
|
||||
if (params.through) {
|
||||
definition.modelThrough = params.through;
|
||||
var keyThrough = definition.throughKey || i8n.camelize(modelTo.modelName + '_id', true);
|
||||
definition.keyThrough = keyThrough;
|
||||
}
|
||||
|
||||
modelFrom.relations[relationName] = definition;
|
||||
|
||||
if (!params.through) {
|
||||
// obviously, modelTo should have attribute called `fk`
|
||||
modelTo.dataSource.defineForeignKey(modelTo.modelName, fk, this.modelName);
|
||||
}
|
||||
|
||||
var scopeMethods = {
|
||||
findById: scopeMethod(definition, 'findById'),
|
||||
destroy: scopeMethod(definition, 'destroyById')
|
||||
}
|
||||
|
||||
if(definition.modelThrough) {
|
||||
scopeMethods.create = scopeMethod(definition, 'create');
|
||||
scopeMethods.add = scopeMethod(definition, 'add');
|
||||
scopeMethods.remove = scopeMethod(definition, 'remove');
|
||||
}
|
||||
|
||||
// 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];
|
||||
if (params.through) {
|
||||
filter.collect = i8n.camelize(modelTo.modelName, true);
|
||||
filter.include = filter.collect;
|
||||
}
|
||||
return filter;
|
||||
}, scopeMethods);
|
||||
|
||||
};
|
||||
|
||||
function scopeMethod(definition, methodName) {
|
||||
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);
|
||||
};
|
||||
return method;
|
||||
}
|
||||
|
||||
HasMany.prototype.findById = function (id, 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) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
if (!inst) {
|
||||
return cb(new Error('Not found'));
|
||||
}
|
||||
// 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'));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
HasMany.prototype.destroyById = function (id, 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) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
if (!inst) {
|
||||
return cb(new Error('Not found'));
|
||||
}
|
||||
// Check if the foreign key matches the primary key
|
||||
if (inst[fk] && inst[fk].toString() === modelInstance[pk].toString()) {
|
||||
inst.destroy(cb);
|
||||
} else {
|
||||
cb(new Error('Permission denied: foreign key does not match the primary key'));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 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) {
|
||||
var definition = this.definition;
|
||||
var modelTo = definition.modelTo;
|
||||
var modelThrough = definition.modelThrough;
|
||||
|
||||
if (typeof data === 'function' && !done) {
|
||||
done = data;
|
||||
data = {};
|
||||
}
|
||||
|
||||
var modelInstance = this.modelInstance;
|
||||
|
||||
// First create the target model
|
||||
modelTo.create(data, function (err, ac) {
|
||||
if (err) {
|
||||
return done && done(err, ac);
|
||||
}
|
||||
// The primary key for the target model
|
||||
var pk2 = modelTo.dataSource.idName(modelTo.modelName) || 'id';
|
||||
var fk1 = findBelongsTo(modelThrough, definition.modelFrom,
|
||||
definition.keyFrom);
|
||||
var fk2 = findBelongsTo(modelThrough, definition.modelTo, pk2);
|
||||
var d = {};
|
||||
d[fk1] = modelInstance[definition.keyFrom];
|
||||
d[fk2] = ac[pk2];
|
||||
// Then create the through model
|
||||
modelThrough.create(d, function (e) {
|
||||
if (e) {
|
||||
// Undo creation of the target model
|
||||
ac.destroy(function () {
|
||||
done && done(e);
|
||||
});
|
||||
} else {
|
||||
done && done(err, ac);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Add the target model instance to the 'hasMany' relation
|
||||
* @param {Object|ID} acInst The actual instance or id value
|
||||
*/
|
||||
HasManyThrough.prototype.add = function (acInst, done) {
|
||||
var definition = this.definition;
|
||||
var modelThrough = definition.modelThrough;
|
||||
var modelTo = definition.modelTo;
|
||||
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 = modelTo.dataSource.idName(modelTo.modelName) || 'id';
|
||||
|
||||
var fk2 = findBelongsTo(modelThrough, definition.modelTo, pk2);
|
||||
|
||||
query[fk1] = this.modelInstance[pk1];
|
||||
query[fk2] = acInst[pk2] || acInst;
|
||||
|
||||
data[fk1] = this.modelInstance[pk1];
|
||||
data[fk2] = acInst[pk2] || acInst;
|
||||
|
||||
// Create an instance of the through model
|
||||
modelThrough.findOrCreate({where: query}, data, done);
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove the target model instance from the 'hasMany' relation
|
||||
* @param {Object|ID) acInst The actual instance or id value
|
||||
*/
|
||||
HasManyThrough.prototype.remove = function (acInst, done) {
|
||||
var modelThrough = this.definition.modelThrough;
|
||||
var fk2 = this.definition.keyThrough;
|
||||
var pk = this.definition.keyFrom;
|
||||
|
||||
var q = {};
|
||||
q[fk2] = acInst[pk] || acInst;
|
||||
modelThrough.deleteAll(q, done );
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Declare "belongsTo" relation that sets up a one-to-one connection with
|
||||
* another model, such that each instance of the declaring model "belongs to"
|
||||
* one instance of the other model.
|
||||
*
|
||||
* For example, if an application includes users and posts, and each post can
|
||||
* be written by exactly one user. The following code specifies that `Post` has
|
||||
* a reference called `author` to the `User` model via the `userId` property of
|
||||
* `Post` as the foreign key.
|
||||
* ```
|
||||
* Post.belongsTo(User, {as: 'author', foreignKey: 'userId'});
|
||||
* ```
|
||||
*
|
||||
* This optional parameter default value is false, so the related object will
|
||||
* be loaded from cache if available.
|
||||
*
|
||||
* @param {Class|String} modelTo Model object (or String name of model) to
|
||||
* which you are creating the relationship.
|
||||
* @options {Object} params Configuration parameters; see below.
|
||||
* @property {String} as Name of the property in the referring model that
|
||||
* corresponds to the foreign key field in the related model.
|
||||
* @property {String} foreignKey Name of foreign key property.
|
||||
*
|
||||
*/
|
||||
RelationDefinition.belongsTo = function (modelFrom, modelTo, params) {
|
||||
params = params || {};
|
||||
if ('string' === typeof modelTo) {
|
||||
params.as = modelTo;
|
||||
if (params.model) {
|
||||
modelTo = params.model;
|
||||
} else {
|
||||
var modelToName = modelTo.toLowerCase();
|
||||
modelTo = lookupModel(modelFrom.dataSource.modelBuilder.models, modelToName);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
modelFrom: modelFrom,
|
||||
keyFrom: fk,
|
||||
keyTo: idName,
|
||||
modelTo: modelTo
|
||||
});
|
||||
|
||||
modelFrom.dataSource.defineForeignKey(modelFrom.modelName, fk, modelTo.modelName);
|
||||
|
||||
// Define a property for the scope so that we have 'this' for the scoped methods
|
||||
Object.defineProperty(modelFrom.prototype, relationName, {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
get: function() {
|
||||
var relation = new BelongsTo(relationDef, this);
|
||||
var relationMethod = relation.related.bind(relation);
|
||||
relationMethod.create = relation.create.bind(relation);
|
||||
relationMethod.build = relation.build.bind(relation);
|
||||
relationMethod._targetClass = relationDef.modelTo.modelName;
|
||||
return relationMethod;
|
||||
}
|
||||
});
|
||||
|
||||
// Wrap the property into a function for remoting
|
||||
// so that it can be accessed as /api/<model>/<id>/<belongsToRelationName>
|
||||
// For example, /api/orders/1/customer
|
||||
var fn = function() {
|
||||
var f = this[relationName];
|
||||
f.apply(this, arguments);
|
||||
};
|
||||
|
||||
fn.shared = true;
|
||||
fn.http = {verb: 'get', path: '/' + relationName};
|
||||
fn.accepts = {arg: 'refresh', type: 'boolean', http: {source: 'query'}};
|
||||
fn.description = 'Fetches belongsTo relation ' + relationName;
|
||||
fn.returns = {arg: relationName, type: 'object', root: true};
|
||||
|
||||
modelFrom.prototype['__get__' + relationName] = fn;
|
||||
|
||||
};
|
||||
|
||||
BelongsTo.prototype.create = function(targetModelData, cb) {
|
||||
var modelTo = this.definition.modelTo;
|
||||
var fk = this.definition.keyTo;
|
||||
var pk = this.definition.keyFrom;
|
||||
var modelInstance = this.modelInstance;
|
||||
|
||||
modelTo.create(targetModelData, function(err, targetModel) {
|
||||
if(!err) {
|
||||
modelInstance[fk] = targetModel[pk];
|
||||
cb && cb(err, targetModel);
|
||||
} else {
|
||||
cb && cb(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
BelongsTo.prototype.build = function(targetModelData) {
|
||||
var modelTo = this.definition.modelTo;
|
||||
return new modelTo(targetModelData);
|
||||
};
|
||||
|
||||
/**
|
||||
* Define the method for the belongsTo relation itself
|
||||
* It will support one of the following styles:
|
||||
* - order.customer(refresh, callback): Load the target model instance asynchronously
|
||||
* - order.customer(customer): Synchronous setter of the target model instance
|
||||
* - order.customer(): Synchronous getter of the target model instance
|
||||
*
|
||||
* @param refresh
|
||||
* @param params
|
||||
* @returns {*}
|
||||
*/
|
||||
BelongsTo.prototype.related = function (refresh, params) {
|
||||
var modelTo = this.definition.modelTo;
|
||||
var pk = this.definition.keyTo;
|
||||
var fk = this.definition.keyFrom;
|
||||
var modelInstance = this.modelInstance;
|
||||
var relationName = this.definition.name;
|
||||
|
||||
if (arguments.length === 1) {
|
||||
params = refresh;
|
||||
refresh = false;
|
||||
} else if (arguments.length > 2) {
|
||||
throw new Error('Method can\'t be called with more than two arguments');
|
||||
}
|
||||
|
||||
var cachedValue;
|
||||
if (!refresh && modelInstance.__cachedRelations
|
||||
&& (modelInstance.__cachedRelations[relationName] !== undefined)) {
|
||||
cachedValue = modelInstance.__cachedRelations[relationName];
|
||||
}
|
||||
if (params instanceof ModelBaseClass) { // acts as setter
|
||||
modelInstance[fk] = params[pk];
|
||||
modelInstance.__cachedRelations[relationName] = params;
|
||||
} else if (typeof params === 'function') { // acts as async getter
|
||||
var cb = params;
|
||||
if (cachedValue === undefined) {
|
||||
modelTo.findById(modelInstance[fk], function (err, inst) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
if (!inst) {
|
||||
return cb(null, null);
|
||||
}
|
||||
// Check if the foreign key matches the primary key
|
||||
if (inst[pk] === modelInstance[fk]) {
|
||||
cb(null, inst);
|
||||
} else {
|
||||
cb(new Error('Permission denied'));
|
||||
}
|
||||
});
|
||||
return modelInstance[fk];
|
||||
} else {
|
||||
cb(null, cachedValue);
|
||||
return cachedValue;
|
||||
}
|
||||
} else if (params === undefined) { // acts as sync getter
|
||||
return modelInstance[fk];
|
||||
} else { // setter
|
||||
modelInstance[fk] = params;
|
||||
delete modelInstance.__cachedRelations[relationName];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A hasAndBelongsToMany relation creates a direct many-to-many connection with
|
||||
* another model, with no intervening model. For example, if your application
|
||||
* includes users and groups, with each group having many users and each user
|
||||
* appearing in many groups, you could declare the models this way:
|
||||
* ```
|
||||
* User.hasAndBelongsToMany('groups', {model: Group, foreignKey: 'groupId'});
|
||||
* ```
|
||||
*
|
||||
* @param {String|Object} modelTo Model object (or String name of model) to
|
||||
* which you are creating the relationship.
|
||||
* @options {Object} params Configuration parameters; see below.
|
||||
* @property {String} as Name of the property in the referring model that
|
||||
* corresponds to the foreign key field in the related model.
|
||||
* @property {String} foreignKey Property name of foreign key field.
|
||||
* @property {Object} model Model object
|
||||
*/
|
||||
RelationDefinition.hasAndBelongsToMany = function hasAndBelongsToMany(modelFrom, modelTo, params) {
|
||||
params = params || {};
|
||||
var models = modelFrom.dataSource.modelBuilder.models;
|
||||
|
||||
if ('string' === typeof modelTo) {
|
||||
params.as = modelTo;
|
||||
if (params.model) {
|
||||
modelTo = params.model;
|
||||
} else {
|
||||
modelTo = lookupModel(models, i8n.singularize(modelTo)) ||
|
||||
modelTo;
|
||||
}
|
||||
if (typeof modelTo === 'string') {
|
||||
throw new Error('Could not find "' + modelTo + '" relation for ' + modelFrom.modelName);
|
||||
}
|
||||
}
|
||||
|
||||
if (!params.through) {
|
||||
var name1 = modelFrom.modelName + modelTo.modelName;
|
||||
var name2 = modelTo.modelName + modelFrom.modelName;
|
||||
params.through = lookupModel(models, name1) || lookupModel(models, name2) ||
|
||||
modelFrom.dataSource.define(name1);
|
||||
}
|
||||
params.through.belongsTo(modelFrom);
|
||||
params.through.belongsTo(modelTo);
|
||||
|
||||
this.hasMany(modelFrom, modelTo, {as: params.as, through: params.through});
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* A HasOne relation creates a one-to-one connection from modelFrom to modelTo.
|
||||
* This relation indicates that each instance of a model contains or possesses
|
||||
* one instance of another model. For example, each supplier in your application
|
||||
* has only one account.
|
||||
*
|
||||
* @param {Function} modelFrom The declaring model class
|
||||
* @param {String|Function} modelTo Model object (or String name of model) to
|
||||
* which you are creating the relationship.
|
||||
* @options {Object} params Configuration parameters; see below.
|
||||
* @property {String} as Name of the property in the referring model that
|
||||
* corresponds to the foreign key field in the related model.
|
||||
* @property {String} foreignKey Property name of foreign key field.
|
||||
* @property {Object} model Model object
|
||||
*/
|
||||
RelationDefinition.hasOne = function (modelFrom, modelTo, params) {
|
||||
params = params || {};
|
||||
if ('string' === typeof modelTo) {
|
||||
params.as = modelTo;
|
||||
if (params.model) {
|
||||
modelTo = params.model;
|
||||
} else {
|
||||
var modelToName = modelTo.toLowerCase();
|
||||
modelTo = lookupModel(modelFrom.dataSource.modelBuilder.models, modelToName);
|
||||
}
|
||||
}
|
||||
|
||||
var pk = modelFrom.dataSource.idName(modelTo.modelName) || 'id';
|
||||
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
|
||||
});
|
||||
|
||||
modelFrom.dataSource.defineForeignKey(modelTo.modelName, fk, modelFrom.modelName);
|
||||
|
||||
// Define a property for the scope so that we have 'this' for the scoped methods
|
||||
Object.defineProperty(modelFrom.prototype, relationName, {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
get: function() {
|
||||
var relation = new HasOne(relationDef, this);
|
||||
var relationMethod = relation.related.bind(relation)
|
||||
relationMethod.create = relation.create.bind(relation);
|
||||
relationMethod.build = relation.build.bind(relation);
|
||||
return relationMethod;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
HasOne.prototype.create = function(targetModelData, cb) {
|
||||
var modelTo = this.definition.modelTo;
|
||||
var fk = this.definition.keyTo;
|
||||
var pk = this.definition.keyFrom;
|
||||
var modelInstance = this.modelInstance;
|
||||
|
||||
targetModelData = targetModelData || {};
|
||||
targetModelData[fk] = modelInstance[pk];
|
||||
modelTo.create(targetModelData, function(err, targetModel) {
|
||||
if(!err) {
|
||||
cb && cb(err, targetModel);
|
||||
} else {
|
||||
cb && cb(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Build a target model instance
|
||||
* @param {Object} targetModelData The target model data
|
||||
* @returns {Object} The newly built target model instance
|
||||
*/
|
||||
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];
|
||||
return new modelTo(targetModelData);
|
||||
};
|
||||
|
||||
/**
|
||||
* Define the method for the hasOne relation itself
|
||||
* It will support one of the following styles:
|
||||
* - order.customer(refresh, callback): Load the target model instance asynchronously
|
||||
* - order.customer(customer): Synchronous setter of the target model instance
|
||||
* - order.customer(): Synchronous getter of the target model instance
|
||||
*
|
||||
* @param {Boolean} refresh Reload from the data source
|
||||
* @param {Object|Function} params Query parameters
|
||||
* @returns {Object}
|
||||
*/
|
||||
HasOne.prototype.related = function (refresh, params) {
|
||||
var modelTo = this.definition.modelTo;
|
||||
var fk = this.definition.keyTo;
|
||||
var pk = this.definition.keyFrom;
|
||||
var modelInstance = this.modelInstance;
|
||||
var relationName = this.definition.name;
|
||||
|
||||
if (arguments.length === 1) {
|
||||
params = refresh;
|
||||
refresh = false;
|
||||
} else if (arguments.length > 2) {
|
||||
throw new Error('Method can\'t be called with more than two arguments');
|
||||
}
|
||||
|
||||
var cachedValue;
|
||||
if (!refresh && modelInstance.__cachedRelations
|
||||
&& (modelInstance.__cachedRelations[relationName] !== undefined)) {
|
||||
cachedValue = modelInstance.__cachedRelations[relationName];
|
||||
}
|
||||
if (params instanceof ModelBaseClass) { // acts as setter
|
||||
params[fk] = modelInstance[pk];
|
||||
modelInstance.__cachedRelations[relationName] = params;
|
||||
} else if (typeof params === 'function') { // acts as async getter
|
||||
var cb = params;
|
||||
if (cachedValue === undefined) {
|
||||
var query = {where: {}};
|
||||
query.where[fk] = modelInstance[pk];
|
||||
modelTo.findOne(query, function (err, inst) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
if (!inst) {
|
||||
return cb(null, null);
|
||||
}
|
||||
// Check if the foreign key matches the primary key
|
||||
if (inst[fk] === modelInstance[pk]) {
|
||||
cb(null, inst);
|
||||
} else {
|
||||
cb(new Error('Permission denied'));
|
||||
}
|
||||
});
|
||||
return modelInstance[pk];
|
||||
} else {
|
||||
cb(null, cachedValue);
|
||||
return cachedValue;
|
||||
}
|
||||
} else if (params === undefined) { // acts as sync getter
|
||||
return modelInstance[pk];
|
||||
} else { // setter
|
||||
params[fk] = modelInstance[pk];
|
||||
delete modelInstance.__cachedRelations[relationName];
|
||||
}
|
||||
};
|
385
lib/relations.js
385
lib/relations.js
|
@ -1,49 +1,17 @@
|
|||
/*!
|
||||
* Dependencies
|
||||
*/
|
||||
var i8n = require('inflection');
|
||||
var defineScope = require('./scope.js').defineScope;
|
||||
var ModelBaseClass = require('./model.js');
|
||||
var relation = require('./relation-definition');
|
||||
var RelationDefinition = relation.RelationDefinition;
|
||||
|
||||
module.exports = Relation;
|
||||
module.exports = RelationMixin;
|
||||
|
||||
/**
|
||||
* Relations class. Use to define relationships between models.
|
||||
* RelationMixin class. Use to define relationships between models.
|
||||
*
|
||||
* @class Relation
|
||||
* @class RelationMixin
|
||||
*/
|
||||
function Relation() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the relation by foreign key
|
||||
* @param {*} foreignKey The foreign key
|
||||
* @returns {Object} The relation object
|
||||
*/
|
||||
Relation.relationNameFor = function relationNameFor(foreignKey) {
|
||||
for (var rel in this.relations) {
|
||||
if (this.relations[rel].type === 'belongsTo' && this.relations[rel].keyFrom === foreignKey) {
|
||||
return rel;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/*!
|
||||
* Look up a model by name from the list of given models
|
||||
* @param {Object} models Models keyed by name
|
||||
* @param {String} modelName The model name
|
||||
* @returns {*} The matching model class
|
||||
*/
|
||||
function lookupModel(models, modelName) {
|
||||
if(models[modelName]) {
|
||||
return models[modelName];
|
||||
}
|
||||
var lookupClassName = modelName.toLowerCase();
|
||||
for (var name in models) {
|
||||
if (name.toLowerCase() === lookupClassName) {
|
||||
return models[name];
|
||||
}
|
||||
}
|
||||
function RelationMixin() {
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -90,173 +58,14 @@ function lookupModel(models, modelName) {
|
|||
* });
|
||||
* });
|
||||
*```
|
||||
* @param {Object|String} anotherClass Model object (or String name of model) to which you are creating the relationship.
|
||||
* @options {Object} parameters Configuration parameters; see below.
|
||||
* @param {Object|String} modelTo Model object (or String name of model) to which you are creating the relationship.
|
||||
* @options {Object} parameters Configuration parameters; see below.
|
||||
* @property {String} as Name of the property in the referring model that corresponds to the foreign key field in the related model.
|
||||
* @property {String} foreignKey Property name of foreign key field.
|
||||
* @property {Object} model Model object
|
||||
*/
|
||||
Relation.hasMany = function hasMany(anotherClass, params) {
|
||||
var thisClassName = this.modelName;
|
||||
params = params || {};
|
||||
if (typeof anotherClass === 'string') {
|
||||
params.as = anotherClass;
|
||||
if (params.model) {
|
||||
anotherClass = params.model;
|
||||
} else {
|
||||
var anotherClassName = i8n.singularize(anotherClass).toLowerCase();
|
||||
anotherClass = lookupModel(this.dataSource.modelBuilder.models, anotherClassName);
|
||||
}
|
||||
}
|
||||
var methodName = params.as || i8n.camelize(anotherClass.pluralModelName, true);
|
||||
var fk = params.foreignKey || i8n.camelize(thisClassName + '_id', true);
|
||||
|
||||
var idName = this.dataSource.idName(this.modelName) || 'id';
|
||||
|
||||
this.relations[methodName] = {
|
||||
type: 'hasMany',
|
||||
keyFrom: idName,
|
||||
keyTo: fk,
|
||||
modelTo: anotherClass,
|
||||
multiple: true
|
||||
};
|
||||
|
||||
if (params.through) {
|
||||
this.relations[methodName].modelThrough = params.through;
|
||||
}
|
||||
// each instance of this class should have method named
|
||||
// pluralize(anotherClass.modelName)
|
||||
// which is actually just anotherClass.find({where: {thisModelNameId: this[idName]}}, cb);
|
||||
var scopeMethods = {
|
||||
findById: findById,
|
||||
destroy: destroyById
|
||||
};
|
||||
if (params.through) {
|
||||
var fk2 = i8n.camelize(anotherClass.modelName + '_id', true);
|
||||
|
||||
// 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
|
||||
scopeMethods.create = function create(data, done) {
|
||||
if (typeof data !== 'object') {
|
||||
done = data;
|
||||
data = {};
|
||||
}
|
||||
if ('function' !== typeof done) {
|
||||
done = function () {
|
||||
};
|
||||
}
|
||||
var self = this;
|
||||
// First create the target model
|
||||
anotherClass.create(data, function (err, ac) {
|
||||
if (err) return done(err, ac);
|
||||
var d = {};
|
||||
d[params.through.relationNameFor(fk)] = self;
|
||||
d[params.through.relationNameFor(fk2)] = ac;
|
||||
// Then create the through model
|
||||
params.through.create(d, function (e) {
|
||||
if (e) {
|
||||
// Undo creation of the target model
|
||||
ac.destroy(function () {
|
||||
done(e);
|
||||
});
|
||||
} else {
|
||||
done(err, ac);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/*!
|
||||
* Add the target model instance to the 'hasMany' relation
|
||||
* @param {Object|ID} acInst The actual instance or id value
|
||||
*/
|
||||
scopeMethods.add = function (acInst, done) {
|
||||
var data = {};
|
||||
var query = {};
|
||||
query[fk] = this[idName];
|
||||
data[params.through.relationNameFor(fk)] = this;
|
||||
query[fk2] = acInst[idName] || acInst;
|
||||
data[params.through.relationNameFor(fk2)] = acInst;
|
||||
// Create an instance of the through model
|
||||
params.through.findOrCreate({where: query}, data, done);
|
||||
};
|
||||
|
||||
/*!
|
||||
* Remove the target model instance from the 'hasMany' relation
|
||||
* @param {Object|ID) acInst The actual instance or id value
|
||||
*/
|
||||
scopeMethods.remove = function (acInst, done) {
|
||||
var q = {};
|
||||
q[fk2] = acInst[idName] || acInst;
|
||||
params.through.findOne({where: q}, function (err, d) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
if (!d) {
|
||||
return done();
|
||||
}
|
||||
d.destroy(done);
|
||||
});
|
||||
};
|
||||
|
||||
// No destroy method will be injected
|
||||
delete scopeMethods.destroy;
|
||||
}
|
||||
|
||||
// Mix the property and scoped methods into the prototype class
|
||||
defineScope(this.prototype, params.through || anotherClass, methodName, function () {
|
||||
var filter = {};
|
||||
filter.where = {};
|
||||
filter.where[fk] = this[idName];
|
||||
if (params.through) {
|
||||
filter.collect = i8n.camelize(anotherClass.modelName, true);
|
||||
filter.include = filter.collect;
|
||||
}
|
||||
return filter;
|
||||
}, scopeMethods);
|
||||
|
||||
if (!params.through) {
|
||||
// obviously, anotherClass should have attribute called `fk`
|
||||
anotherClass.dataSource.defineForeignKey(anotherClass.modelName, fk, this.modelName);
|
||||
}
|
||||
|
||||
// Find the target model instance by id
|
||||
function findById(id, cb) {
|
||||
anotherClass.findById(id, function (err, inst) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
if (!inst) {
|
||||
return cb(new Error('Not found'));
|
||||
}
|
||||
// Check if the foreign key matches the primary key
|
||||
if (inst[fk] && inst[fk].toString() === this[idName].toString()) {
|
||||
cb(null, inst);
|
||||
} else {
|
||||
cb(new Error('Permission denied'));
|
||||
}
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
// Destroy the target model instance by id
|
||||
function destroyById(id, cb) {
|
||||
var self = this;
|
||||
anotherClass.findById(id, function (err, inst) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
if (!inst) {
|
||||
return cb(new Error('Not found'));
|
||||
}
|
||||
// Check if the foreign key matches the primary key
|
||||
if (inst[fk] && inst[fk].toString() === self[idName].toString()) {
|
||||
inst.destroy(cb);
|
||||
} else {
|
||||
cb(new Error('Permission denied'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
RelationMixin.hasMany = function hasMany(modelTo, params) {
|
||||
RelationDefinition.hasMany(this, modelTo, params);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -303,145 +112,15 @@ Relation.hasMany = function hasMany(anotherClass, params) {
|
|||
* });
|
||||
* ```
|
||||
* This optional parameter default value is false, so the related object will be loaded from cache if available.
|
||||
*
|
||||
* @param {Class|String} anotherClass Model object (or String name of model) to which you are creating the relationship.
|
||||
*
|
||||
* @param {Class|String} modelTo Model object (or String name of model) to which you are creating the relationship.
|
||||
* @options {Object} params Configuration parameters; see below.
|
||||
* @property {String} as Name of the property in the referring model that corresponds to the foreign key field in the related model.
|
||||
* @property {String} foreignKey Name of foreign key property.
|
||||
*
|
||||
*/
|
||||
Relation.belongsTo = function (anotherClass, params) {
|
||||
params = params || {};
|
||||
if ('string' === typeof anotherClass) {
|
||||
params.as = anotherClass;
|
||||
if (params.model) {
|
||||
anotherClass = params.model;
|
||||
} else {
|
||||
var anotherClassName = anotherClass.toLowerCase();
|
||||
anotherClass = lookupModel(this.dataSource.modelBuilder.models, anotherClassName);
|
||||
}
|
||||
}
|
||||
|
||||
var idName = this.dataSource.idName(anotherClass.modelName) || 'id';
|
||||
var methodName = params.as || i8n.camelize(anotherClass.modelName, true);
|
||||
var fk = params.foreignKey || methodName + 'Id';
|
||||
|
||||
this.relations[methodName] = {
|
||||
type: 'belongsTo',
|
||||
keyFrom: fk,
|
||||
keyTo: idName,
|
||||
modelTo: anotherClass,
|
||||
multiple: false
|
||||
};
|
||||
|
||||
this.dataSource.defineForeignKey(this.modelName, fk, anotherClass.modelName);
|
||||
this.prototype.__finders__ = this.prototype.__finders__ || {};
|
||||
|
||||
// Set up a finder to find by id and make sure the foreign key of the declaring
|
||||
// model matches the primary key of the target model
|
||||
this.prototype.__finders__[methodName] = function (id, cb) {
|
||||
if (id === null) {
|
||||
cb(null, null);
|
||||
return;
|
||||
}
|
||||
anotherClass.findById(id, function (err, inst) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
if (!inst) {
|
||||
return cb(null, null);
|
||||
}
|
||||
// Check if the foreign key matches the primary key
|
||||
if (inst[idName] === this[fk]) {
|
||||
cb(null, inst);
|
||||
} else {
|
||||
cb(new Error('Permission denied'));
|
||||
}
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
// Define the method for the belongsTo relation itself
|
||||
// It will support one of the following styles:
|
||||
// - order.customer(refresh, callback): Load the target model instance asynchronously
|
||||
// - order.customer(customer): Synchronous setter of the target model instance
|
||||
// - order.customer(): Synchronous getter of the target model instance
|
||||
var relationMethod = function (refresh, p) {
|
||||
if (arguments.length === 1) {
|
||||
p = refresh;
|
||||
refresh = false;
|
||||
} else if (arguments.length > 2) {
|
||||
throw new Error('Method can\'t be called with more than two arguments');
|
||||
}
|
||||
var self = this;
|
||||
var cachedValue;
|
||||
if (!refresh && this.__cachedRelations && (this.__cachedRelations[methodName] !== undefined)) {
|
||||
cachedValue = this.__cachedRelations[methodName];
|
||||
}
|
||||
if (p instanceof ModelBaseClass) { // acts as setter
|
||||
this[fk] = p[idName];
|
||||
this.__cachedRelations[methodName] = p;
|
||||
} else if (typeof p === 'function') { // acts as async getter
|
||||
if (typeof cachedValue === 'undefined') {
|
||||
this.__finders__[methodName].apply(self, [this[fk], function (err, inst) {
|
||||
if (!err) {
|
||||
self.__cachedRelations[methodName] = inst;
|
||||
}
|
||||
p(err, inst);
|
||||
}]);
|
||||
return this[fk];
|
||||
} else {
|
||||
p(null, cachedValue);
|
||||
return cachedValue;
|
||||
}
|
||||
} else if (typeof p === 'undefined') { // acts as sync getter
|
||||
return this[fk];
|
||||
} else { // setter
|
||||
this[fk] = p;
|
||||
delete this.__cachedRelations[methodName];
|
||||
}
|
||||
};
|
||||
|
||||
// Define a property for the scope so that we have 'this' for the scoped methods
|
||||
Object.defineProperty(this.prototype, methodName, {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
get: function () {
|
||||
var fn = function() {
|
||||
// Call the relation method on the declaring model instance
|
||||
return relationMethod.apply(this, arguments);
|
||||
}
|
||||
// Create an instance of the target model and set the foreign key of the
|
||||
// declaring model instance to the id of the target instance
|
||||
fn.create = function(targetModelData, cb) {
|
||||
var self = this;
|
||||
anotherClass.create(targetModelData, function(err, targetModel) {
|
||||
if(!err) {
|
||||
self[fk] = targetModel[idName];
|
||||
cb && cb(err, targetModel);
|
||||
} else {
|
||||
cb && cb(err);
|
||||
}
|
||||
});
|
||||
}.bind(this);
|
||||
|
||||
// Build an instance of the target model
|
||||
fn.build = function(targetModelData) {
|
||||
return new anotherClass(targetModelData);
|
||||
}.bind(this);
|
||||
|
||||
fn._targetClass = anotherClass.modelName;
|
||||
|
||||
return fn;
|
||||
}});
|
||||
|
||||
// Wrap the property into a function for remoting
|
||||
// so that it can be accessed as /api/<model>/<id>/<belongsToRelationName>
|
||||
// For example, /api/orders/1/customer
|
||||
var fn = function() {
|
||||
var f = this[methodName];
|
||||
f.apply(this, arguments);
|
||||
};
|
||||
this.prototype['__get__' + methodName] = fn;
|
||||
RelationMixin.belongsTo = function (modelTo, params) {
|
||||
RelationDefinition.belongsTo(this, modelTo, params);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -468,39 +147,17 @@ Relation.belongsTo = function (anotherClass, params) {
|
|||
* user.groups.remove(group, callback);
|
||||
* ```
|
||||
*
|
||||
* @param {String|Object} anotherClass Model object (or String name of model) to which you are creating the relationship.
|
||||
* @param {String|Object} modelTo Model object (or String name of model) to which you are creating the relationship.
|
||||
* the relation
|
||||
* @options {Object} params Configuration parameters; see below.
|
||||
* @property {String} as Name of the property in the referring model that corresponds to the foreign key field in the related model.
|
||||
* @property {String} foreignKey Property name of foreign key field.
|
||||
* @property {Object} model Model object
|
||||
*/
|
||||
Relation.hasAndBelongsToMany = function hasAndBelongsToMany(anotherClass, params) {
|
||||
params = params || {};
|
||||
var models = this.dataSource.modelBuilder.models;
|
||||
|
||||
if ('string' === typeof anotherClass) {
|
||||
params.as = anotherClass;
|
||||
if (params.model) {
|
||||
anotherClass = params.model;
|
||||
} else {
|
||||
anotherClass = lookupModel(models, i8n.singularize(anotherClass).toLowerCase()) ||
|
||||
anotherClass;
|
||||
}
|
||||
if (typeof anotherClass === 'string') {
|
||||
throw new Error('Could not find "' + anotherClass + '" relation for ' + this.modelName);
|
||||
}
|
||||
}
|
||||
|
||||
if (!params.through) {
|
||||
var name1 = this.modelName + anotherClass.modelName;
|
||||
var name2 = anotherClass.modelName + this.modelName;
|
||||
params.through = lookupModel(models, name1) || lookupModel(models, name2) ||
|
||||
this.dataSource.define(name1);
|
||||
}
|
||||
params.through.belongsTo(this);
|
||||
params.through.belongsTo(anotherClass);
|
||||
|
||||
this.hasMany(anotherClass, {as: params.as, through: params.through});
|
||||
|
||||
RelationMixin.hasAndBelongsToMany = function hasAndBelongsToMany(modelTo, params) {
|
||||
RelationDefinition.hasAndBelongsToMany(this, modelTo, params);
|
||||
};
|
||||
|
||||
RelationMixin.hasOne = function hasMany(modelTo, params) {
|
||||
RelationDefinition.hasOne(this, modelTo, params);
|
||||
};
|
||||
|
|
249
lib/scope.js
249
lib/scope.js
|
@ -6,6 +6,52 @@ var defineCachedRelations = utils.defineCachedRelations;
|
|||
*/
|
||||
exports.defineScope = defineScope;
|
||||
|
||||
function ScopeDefinition(definition) {
|
||||
this.sourceModel = definition.sourceModel;
|
||||
this.targetModel = definition.targetModel || definition.sourceModel;
|
||||
this.name = definition.name;
|
||||
this.params = definition.params;
|
||||
this.methods = definition.methods;
|
||||
}
|
||||
|
||||
ScopeDefinition.prototype.related = function(receiver, scopeParams, condOrRefresh, cb) {
|
||||
var name = this.name;
|
||||
var self = receiver;
|
||||
|
||||
var actualCond = {};
|
||||
var actualRefresh = false;
|
||||
var saveOnCache = true;
|
||||
if (arguments.length === 3) {
|
||||
cb = condOrRefresh;
|
||||
} else if (arguments.length === 4) {
|
||||
if (typeof condOrRefresh === 'boolean') {
|
||||
actualRefresh = condOrRefresh;
|
||||
} else {
|
||||
actualCond = condOrRefresh;
|
||||
actualRefresh = true;
|
||||
saveOnCache = false;
|
||||
}
|
||||
} else {
|
||||
throw new Error('Method can be only called with one or two arguments');
|
||||
}
|
||||
|
||||
if (!self.__cachedRelations || self.__cachedRelations[name] === undefined
|
||||
|| actualRefresh) {
|
||||
// It either doesn't hit the cache or refresh is required
|
||||
var params = mergeQuery(actualCond, scopeParams);
|
||||
return this.targetModel.find(params, function (err, data) {
|
||||
if (!err && saveOnCache) {
|
||||
defineCachedRelations(self);
|
||||
self.__cachedRelations[name] = data;
|
||||
}
|
||||
cb(err, data);
|
||||
});
|
||||
} else {
|
||||
// Return from cache
|
||||
cb(null, self.__cachedRelations[name]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a scope to the class
|
||||
* @param {Model} cls The class where the scope method is added
|
||||
|
@ -22,7 +68,7 @@ function defineScope(cls, targetClass, name, params, methods) {
|
|||
cls._scopeMeta = {};
|
||||
}
|
||||
|
||||
// only makes sence to add scope in meta if base and target classes
|
||||
// only makes sense to add scope in meta if base and target classes
|
||||
// are same
|
||||
if (cls === targetClass) {
|
||||
cls._scopeMeta[name] = params;
|
||||
|
@ -32,6 +78,14 @@ function defineScope(cls, targetClass, name, params, methods) {
|
|||
}
|
||||
}
|
||||
|
||||
var definition = new ScopeDefinition({
|
||||
sourceModel: cls,
|
||||
targetModel: targetClass,
|
||||
name: name,
|
||||
params: params,
|
||||
methods: methods
|
||||
});
|
||||
|
||||
// Define a property for the scope
|
||||
Object.defineProperty(cls, name, {
|
||||
enumerable: false,
|
||||
|
@ -49,42 +103,18 @@ function defineScope(cls, targetClass, name, params, methods) {
|
|||
*/
|
||||
get: function () {
|
||||
var self = this;
|
||||
var f = function caller(condOrRefresh, cb) {
|
||||
var actualCond = {};
|
||||
var actualRefresh = false;
|
||||
var saveOnCache = true;
|
||||
if (arguments.length === 1) {
|
||||
cb = condOrRefresh;
|
||||
} else if (arguments.length === 2) {
|
||||
if (typeof condOrRefresh === 'boolean') {
|
||||
actualRefresh = condOrRefresh;
|
||||
} else {
|
||||
actualCond = condOrRefresh;
|
||||
actualRefresh = true;
|
||||
saveOnCache = false;
|
||||
}
|
||||
var f = function(condOrRefresh, cb) {
|
||||
if(arguments.length === 1) {
|
||||
definition.related(self, f._scope, condOrRefresh);
|
||||
} else {
|
||||
throw new Error('Method can be only called with one or two arguments');
|
||||
}
|
||||
|
||||
if (!self.__cachedRelations || self.__cachedRelations[name] === undefined
|
||||
|| actualRefresh) {
|
||||
// It either doesn't hit the cache or reresh is required
|
||||
var params = mergeParams(actualCond, caller._scope);
|
||||
return targetClass.find(params, function (err, data) {
|
||||
if (!err && saveOnCache) {
|
||||
defineCachedRelations(self);
|
||||
self.__cachedRelations[name] = data;
|
||||
}
|
||||
cb(err, data);
|
||||
});
|
||||
} else {
|
||||
// Return from cache
|
||||
cb(null, self.__cachedRelations[name]);
|
||||
definition.related(self, f._scope, condOrRefresh, cb);
|
||||
}
|
||||
};
|
||||
f._scope = typeof params === 'function' ? params.call(this) : params;
|
||||
f._targetClass = targetClass.modelName;
|
||||
|
||||
f._scope = typeof definition.params === 'function' ?
|
||||
definition.params.call(self) : definition.params;
|
||||
|
||||
f._targetClass = definition.targetModel.modelName;
|
||||
if (f._scope.collect) {
|
||||
f._targetClass = i8n.capitalize(f._scope.collect);
|
||||
}
|
||||
|
@ -92,20 +122,23 @@ function defineScope(cls, targetClass, name, params, methods) {
|
|||
f.build = build;
|
||||
f.create = create;
|
||||
f.destroyAll = destroyAll;
|
||||
for (var i in methods) {
|
||||
f[i] = methods[i].bind(this);
|
||||
for (var i in definition.methods) {
|
||||
f[i] = definition.methods[i].bind(self);
|
||||
}
|
||||
|
||||
// define sub-scopes
|
||||
// Define scope-chaining, such as
|
||||
// Station.scope('active', {where: {isActive: true}});
|
||||
// Station.scope('subway', {where: {isUndeground: true}});
|
||||
// Station.active.subway(cb);
|
||||
Object.keys(targetClass._scopeMeta).forEach(function (name) {
|
||||
Object.defineProperty(f, name, {
|
||||
enumerable: false,
|
||||
get: function () {
|
||||
mergeParams(f._scope, targetClass._scopeMeta[name]);
|
||||
mergeQuery(f._scope, targetClass._scopeMeta[name]);
|
||||
return f;
|
||||
}
|
||||
});
|
||||
}.bind(this));
|
||||
}.bind(self));
|
||||
return f;
|
||||
}
|
||||
});
|
||||
|
@ -134,9 +167,40 @@ function defineScope(cls, targetClass, name, params, methods) {
|
|||
|
||||
cls['__delete__' + name] = fn_delete;
|
||||
|
||||
/*
|
||||
* Extracting fixed property values for the scope from the where clause into
|
||||
* the data object
|
||||
*
|
||||
* @param {Object} The data object
|
||||
* @param {Object} The where clause
|
||||
*/
|
||||
function setScopeValuesFromWhere(data, where) {
|
||||
for (var i in where) {
|
||||
if (i === 'and') {
|
||||
// Find fixed property values from each subclauses
|
||||
for (var w = 0, n = where[i].length; w < n; w++) {
|
||||
setScopeValuesFromWhere(data, where[i][w]);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
var prop = targetClass.definition.properties[i];
|
||||
if (prop) {
|
||||
var val = where[i];
|
||||
if (typeof val !== 'object' || val instanceof prop.type) {
|
||||
// Only pick the {propertyName: propertyValue}
|
||||
data[i] = where[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// and it should have create/build methods with binded thisModelNameId param
|
||||
function build(data) {
|
||||
return new targetClass(mergeParams(this._scope, {where: data || {}}).where);
|
||||
data = data || {};
|
||||
// Find all fixed property values for the scope
|
||||
var where = (this._scope && this._scope.where) || {};
|
||||
setScopeValuesFromWhere(data, where);
|
||||
return new targetClass(data);
|
||||
}
|
||||
|
||||
function create(data, cb) {
|
||||
|
@ -154,70 +218,57 @@ function defineScope(cls, targetClass, name, params, methods) {
|
|||
- If fetching the Elements on which destroyAll is called results in an error
|
||||
*/
|
||||
function destroyAll(cb) {
|
||||
targetClass.find(this._scope, function (err, data) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
} else {
|
||||
(function loopOfDestruction(data) {
|
||||
if (data.length > 0) {
|
||||
data.shift().destroy(function (err) {
|
||||
if (err && cb) cb(err);
|
||||
loopOfDestruction(data);
|
||||
});
|
||||
} else {
|
||||
if (cb) cb();
|
||||
}
|
||||
}(data));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function mergeParams(base, update) {
|
||||
base = base || {};
|
||||
if (update.where) {
|
||||
base.where = merge(base.where, update.where);
|
||||
}
|
||||
if (update.include) {
|
||||
base.include = update.include;
|
||||
}
|
||||
if (update.collect) {
|
||||
base.collect = update.collect;
|
||||
}
|
||||
|
||||
// overwrite order
|
||||
if (update.order) {
|
||||
base.order = update.order;
|
||||
}
|
||||
|
||||
if(update.limit !== undefined) {
|
||||
base.limit = update.limit;
|
||||
}
|
||||
if(update.skip !== undefined) {
|
||||
base.skip = update.skip;
|
||||
}
|
||||
if(update.offset !== undefined) {
|
||||
base.offset = update.offset;
|
||||
}
|
||||
if(update.fields !== undefined) {
|
||||
base.fields = update.fields;
|
||||
}
|
||||
return base;
|
||||
|
||||
targetClass.destroyAll(this._scope, cb);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge `base` and `update` params
|
||||
* @param {Object} base - base object (updating this object)
|
||||
* @param {Object} update - object with new data to update base
|
||||
* @returns {Object} `base`
|
||||
/*!
|
||||
* Merge query parameters
|
||||
* @param {Object} base The base object to contain the merged results
|
||||
* @param {Object} update The object containing updates to be merged
|
||||
* @returns {*|Object} The base object
|
||||
* @private
|
||||
*/
|
||||
function merge(base, update) {
|
||||
function mergeQuery(base, update) {
|
||||
if (!update) {
|
||||
return;
|
||||
}
|
||||
base = base || {};
|
||||
if (update) {
|
||||
Object.keys(update).forEach(function (key) {
|
||||
base[key] = update[key];
|
||||
});
|
||||
if (update.where && Object.keys(update.where).length > 0) {
|
||||
if (base.where && Object.keys(base.where).length > 0) {
|
||||
base.where = {and: [base.where, update.where]};
|
||||
} else {
|
||||
base.where = update.where;
|
||||
}
|
||||
}
|
||||
|
||||
// Overwrite inclusion
|
||||
if (update.include) {
|
||||
base.include = update.include;
|
||||
}
|
||||
if (update.collect) {
|
||||
base.collect = update.collect;
|
||||
}
|
||||
|
||||
// overwrite order
|
||||
if (update.order) {
|
||||
base.order = update.order;
|
||||
}
|
||||
|
||||
// overwrite pagination
|
||||
if (update.limit !== undefined) {
|
||||
base.limit = update.limit;
|
||||
}
|
||||
if (update.skip !== undefined) {
|
||||
base.skip = update.skip;
|
||||
}
|
||||
if (update.offset !== undefined) {
|
||||
base.offset = update.offset;
|
||||
}
|
||||
|
||||
// Overwrite fields
|
||||
if (update.fields !== undefined) {
|
||||
base.fields = update.fields;
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
"mocha": "~1.18.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"async": "~0.8.0",
|
||||
"async": "~0.9.0",
|
||||
"inflection": "~1.3.5",
|
||||
"loopback-connector": "1.x",
|
||||
"traverse": "~0.6.6",
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// This test written in mocha+should.js
|
||||
var should = require('./init.js');
|
||||
var async = require('async');
|
||||
var db, User;
|
||||
|
||||
describe('basic-querying', function () {
|
||||
|
@ -8,10 +9,13 @@ describe('basic-querying', function () {
|
|||
db = getSchema();
|
||||
|
||||
User = db.define('User', {
|
||||
seq: {type: Number, index: true},
|
||||
name: {type: String, index: true, sort: true},
|
||||
email: {type: String, index: true},
|
||||
birthday: {type: Date, index: true},
|
||||
role: {type: String, index: true},
|
||||
order: {type: Number, index: true, sort: true}
|
||||
order: {type: Number, index: true, sort: true},
|
||||
vip: {type: Boolean}
|
||||
});
|
||||
|
||||
db.automigrate(done);
|
||||
|
@ -69,15 +73,26 @@ describe('basic-querying', function () {
|
|||
});
|
||||
});
|
||||
|
||||
it('should query offset collection with limit', function (done) {
|
||||
User.find({skip: 1, limit: 4}, function (err, users) {
|
||||
it('should query collection with skip & limit', function (done) {
|
||||
User.find({skip: 1, limit: 4, order: 'seq'}, function (err, users) {
|
||||
should.exists(users);
|
||||
should.not.exists(err);
|
||||
users[0].seq.should.be.eql(1);
|
||||
users.should.have.lengthOf(4);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should query collection with offset & limit', function (done) {
|
||||
User.find({offset: 2, limit: 3, order: 'seq'}, function (err, users) {
|
||||
should.exists(users);
|
||||
should.not.exists(err);
|
||||
users[0].seq.should.be.eql(2);
|
||||
users.should.have.lengthOf(3);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should query filtered collection', function (done) {
|
||||
User.find({where: {role: 'lead'}}, function (err, users) {
|
||||
should.exists(users);
|
||||
|
@ -175,6 +190,190 @@ describe('basic-querying', function () {
|
|||
});
|
||||
});
|
||||
|
||||
it('should support date "gte" that is satisfied', function (done) {
|
||||
User.find({order: 'seq', where: { birthday: { "gte": new Date('1980-12-08') }
|
||||
}}, function (err, users) {
|
||||
should.not.exist(err);
|
||||
users.should.have.property('length', 1);
|
||||
users[0].name.should.equal('John Lennon');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should support date "gt" that is not satisfied', function (done) {
|
||||
User.find({order: 'seq', where: { birthday: { "gt": new Date('1980-12-08') }
|
||||
}}, function (err, users) {
|
||||
should.not.exist(err);
|
||||
users.should.have.property('length', 0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should support date "gt" that is satisfied', function (done) {
|
||||
User.find({order: 'seq', where: { birthday: { "gt": new Date('1980-12-07') }
|
||||
}}, function (err, users) {
|
||||
should.not.exist(err);
|
||||
users.should.have.property('length', 1);
|
||||
users[0].name.should.equal('John Lennon');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should support date "lt" that is satisfied', function (done) {
|
||||
User.find({order: 'seq', where: { birthday: { "lt": new Date('1980-12-07') }
|
||||
}}, function (err, users) {
|
||||
should.not.exist(err);
|
||||
users.should.have.property('length', 1);
|
||||
users[0].name.should.equal('Paul McCartney');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should support number "gte" that is satisfied', function (done) {
|
||||
User.find({order: 'seq', where: { order: { "gte": 3}
|
||||
}}, function (err, users) {
|
||||
should.not.exist(err);
|
||||
users.should.have.property('length', 4);
|
||||
users[0].name.should.equal('George Harrison');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should support number "gt" that is not satisfied', function (done) {
|
||||
User.find({order: 'seq', where: { order: { "gt": 6 }
|
||||
}}, function (err, users) {
|
||||
should.not.exist(err);
|
||||
users.should.have.property('length', 0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should support number "gt" that is satisfied', function (done) {
|
||||
User.find({order: 'seq', where: { order: { "gt": 5 }
|
||||
}}, function (err, users) {
|
||||
should.not.exist(err);
|
||||
users.should.have.property('length', 1);
|
||||
users[0].name.should.equal('Ringo Starr');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should support number "lt" that is satisfied', function (done) {
|
||||
User.find({order: 'seq', where: { order: { "lt": 2 }
|
||||
}}, function (err, users) {
|
||||
should.not.exist(err);
|
||||
users.should.have.property('length', 1);
|
||||
users[0].name.should.equal('Paul McCartney');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should support number "gt" that is satisfied by null value', function (done) {
|
||||
User.find({order: 'seq', where: { order: { "gt": null }
|
||||
}}, function (err, users) {
|
||||
should.not.exist(err);
|
||||
users.should.have.property('length', 0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should support number "lt" that is not satisfied by null value', function (done) {
|
||||
User.find({order: 'seq', where: { order: { "lt": null }
|
||||
}}, function (err, users) {
|
||||
should.not.exist(err);
|
||||
users.should.have.property('length', 0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should support string "gte" that is satisfied by null value', function (done) {
|
||||
User.find({order: 'seq', where: { name: { "gte": null}
|
||||
}}, function (err, users) {
|
||||
should.not.exist(err);
|
||||
users.should.have.property('length', 0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should support string "gte" that is satisfied', function (done) {
|
||||
User.find({order: 'seq', where: { name: { "gte": 'Paul McCartney'}
|
||||
}}, function (err, users) {
|
||||
should.not.exist(err);
|
||||
users.should.have.property('length', 4);
|
||||
users[0].name.should.equal('Paul McCartney');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should support string "gt" that is not satisfied', function (done) {
|
||||
User.find({order: 'seq', where: { name: { "gt": 'xyz' }
|
||||
}}, function (err, users) {
|
||||
should.not.exist(err);
|
||||
users.should.have.property('length', 0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should support string "gt" that is satisfied', function (done) {
|
||||
User.find({order: 'seq', where: { name: { "gt": 'Paul McCartney' }
|
||||
}}, function (err, users) {
|
||||
should.not.exist(err);
|
||||
users.should.have.property('length', 3);
|
||||
users[0].name.should.equal('Ringo Starr');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should support string "lt" that is satisfied', function (done) {
|
||||
User.find({order: 'seq', where: { name: { "lt": 'Paul McCartney' }
|
||||
}}, function (err, users) {
|
||||
should.not.exist(err);
|
||||
users.should.have.property('length', 2);
|
||||
users[0].name.should.equal('John Lennon');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should support boolean "gte" that is satisfied', function (done) {
|
||||
User.find({order: 'seq', where: { vip: { "gte": true}
|
||||
}}, function (err, users) {
|
||||
should.not.exist(err);
|
||||
users.should.have.property('length', 3);
|
||||
users[0].name.should.equal('John Lennon');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should support boolean "gt" that is not satisfied', function (done) {
|
||||
User.find({order: 'seq', where: { vip: { "gt": true }
|
||||
}}, function (err, users) {
|
||||
should.not.exist(err);
|
||||
users.should.have.property('length', 0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should support boolean "gt" that is satisfied', function (done) {
|
||||
User.find({order: 'seq', where: { vip: { "gt": false }
|
||||
}}, function (err, users) {
|
||||
should.not.exist(err);
|
||||
users.should.have.property('length', 3);
|
||||
users[0].name.should.equal('John Lennon');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should support boolean "lt" that is satisfied', function (done) {
|
||||
User.find({order: 'seq', where: { vip: { "lt": true }
|
||||
}}, function (err, users) {
|
||||
should.not.exist(err);
|
||||
users.should.have.property('length', 2);
|
||||
users[0].name.should.equal('George Harrison');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should only include fields as specified', function (done) {
|
||||
var remaining = 0;
|
||||
|
||||
|
@ -214,7 +413,7 @@ describe('basic-querying', function () {
|
|||
}
|
||||
|
||||
sample({name: true}).expect(['name']);
|
||||
sample({name: false}).expect(['id', 'email', 'role', 'order']);
|
||||
sample({name: false}).expect(['id', 'seq', 'email', 'role', 'order', 'birthday', 'vip']);
|
||||
sample({name: false, id: true}).expect(['id']);
|
||||
sample({id: true}).expect(['id']);
|
||||
sample('id').expect(['id']);
|
||||
|
@ -354,37 +553,72 @@ describe('basic-querying', function () {
|
|||
|
||||
});
|
||||
|
||||
describe('updateAll ', function () {
|
||||
|
||||
beforeEach(seed);
|
||||
|
||||
it('should only update instances that satisfy the where condition', function (done) {
|
||||
User.update({name: 'John Lennon'}, {name: 'John Smith'}, function () {
|
||||
User.find({where: {name: 'John Lennon'}}, function (err, data) {
|
||||
should.not.exist(err);
|
||||
data.length.should.equal(0);
|
||||
User.find({where: {name: 'John Smith'}}, function (err, data) {
|
||||
should.not.exist(err);
|
||||
data.length.should.equal(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should update all instances without where', function (done) {
|
||||
User.update({name: 'John Smith'}, function () {
|
||||
User.find({where: {name: 'John Lennon'}}, function (err, data) {
|
||||
should.not.exist(err);
|
||||
data.length.should.equal(0);
|
||||
User.find({where: {name: 'John Smith'}}, function (err, data) {
|
||||
should.not.exist(err);
|
||||
data.length.should.equal(6);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
function seed(done) {
|
||||
var count = 0;
|
||||
var beatles = [
|
||||
{
|
||||
seq: 0,
|
||||
name: 'John Lennon',
|
||||
email: 'john@b3atl3s.co.uk',
|
||||
role: 'lead',
|
||||
order: 2
|
||||
birthday: new Date('1980-12-08'),
|
||||
order: 2,
|
||||
vip: true
|
||||
},
|
||||
{
|
||||
seq: 1,
|
||||
name: 'Paul McCartney',
|
||||
email: 'paul@b3atl3s.co.uk',
|
||||
role: 'lead',
|
||||
order: 1
|
||||
birthday: new Date('1942-06-18'),
|
||||
order: 1,
|
||||
vip: true
|
||||
},
|
||||
{name: 'George Harrison', order: 5},
|
||||
{name: 'Ringo Starr', order: 6},
|
||||
{name: 'Pete Best', order: 4},
|
||||
{name: 'Stuart Sutcliffe', order: 3}
|
||||
{seq: 2, name: 'George Harrison', order: 5, vip: false},
|
||||
{seq: 3, name: 'Ringo Starr', order: 6, vip: false},
|
||||
{seq: 4, name: 'Pete Best', order: 4},
|
||||
{seq: 5, name: 'Stuart Sutcliffe', order: 3, vip: true}
|
||||
];
|
||||
User.destroyAll(function () {
|
||||
beatles.forEach(function (beatle) {
|
||||
User.create(beatle, ok);
|
||||
});
|
||||
});
|
||||
|
||||
function ok() {
|
||||
if (++count === beatles.length) {
|
||||
done();
|
||||
async.series([
|
||||
User.destroyAll.bind(User),
|
||||
function(cb) {
|
||||
async.each(beatles, User.create.bind(User), cb);
|
||||
}
|
||||
}
|
||||
], done);
|
||||
}
|
||||
|
|
|
@ -872,27 +872,33 @@ describe('Load models with relations', function () {
|
|||
var Account = ds.define('Account', {userId: Number, type: String}, {relations: {user: {type: 'belongsTo', model: 'User'}}});
|
||||
|
||||
assert(Post.relations['user']);
|
||||
assert.deepEqual(Post.relations['user'], {
|
||||
assert.deepEqual(Post.relations['user'].toJSON(), {
|
||||
name: 'user',
|
||||
type: 'belongsTo',
|
||||
modelFrom: 'Post',
|
||||
keyFrom: 'userId',
|
||||
modelTo: 'User',
|
||||
keyTo: 'id',
|
||||
modelTo: User,
|
||||
multiple: false
|
||||
});
|
||||
assert(User.relations['posts']);
|
||||
assert.deepEqual(User.relations['posts'], {
|
||||
assert.deepEqual(User.relations['posts'].toJSON(), {
|
||||
name: 'posts',
|
||||
type: 'hasMany',
|
||||
modelFrom: 'User',
|
||||
keyFrom: 'id',
|
||||
modelTo: 'Post',
|
||||
keyTo: 'userId',
|
||||
modelTo: Post,
|
||||
multiple: true
|
||||
});
|
||||
assert(User.relations['accounts']);
|
||||
assert.deepEqual(User.relations['accounts'], {
|
||||
assert.deepEqual(User.relations['accounts'].toJSON(), {
|
||||
name: 'accounts',
|
||||
type: 'hasMany',
|
||||
modelFrom: 'User',
|
||||
keyFrom: 'id',
|
||||
modelTo: 'Account',
|
||||
keyTo: 'userId',
|
||||
modelTo: Account,
|
||||
multiple: true
|
||||
});
|
||||
|
||||
|
@ -993,7 +999,7 @@ describe('Model with scopes', function () {
|
|||
});
|
||||
|
||||
describe('DataAccessObject', function () {
|
||||
var ds, model, where;
|
||||
var ds, model, where, error;
|
||||
|
||||
before(function () {
|
||||
ds = new DataSource('memory');
|
||||
|
@ -1007,6 +1013,10 @@ describe('DataAccessObject', function () {
|
|||
});
|
||||
});
|
||||
|
||||
beforeEach(function () {
|
||||
error = null;
|
||||
});
|
||||
|
||||
it('should be able to coerce where clause for string types', function () {
|
||||
where = model._coerce({id: 1});
|
||||
assert.deepEqual(where, {id: '1'});
|
||||
|
@ -1069,6 +1079,139 @@ describe('DataAccessObject', function () {
|
|||
|
||||
});
|
||||
|
||||
it('should be able to coerce where clause with and operators', function () {
|
||||
where = model._coerce({and: [{age: '10'}, {vip: 'true'}]});
|
||||
assert.deepEqual(where, {and: [{age: 10}, {vip: true}]});
|
||||
});
|
||||
|
||||
it('should be able to coerce where clause with or operators', function () {
|
||||
where = model._coerce({or: [{age: '10'}, {vip: 'true'}]});
|
||||
assert.deepEqual(where, {or: [{age: 10}, {vip: true}]});
|
||||
});
|
||||
|
||||
it('should throw if the where property is not an object', function () {
|
||||
try {
|
||||
// The where clause has to be an object
|
||||
model._coerce('abc');
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
assert(error, 'An error should have been thrown');
|
||||
});
|
||||
|
||||
it('should throw if the where property is an array', function () {
|
||||
try {
|
||||
// The where clause cannot be an array
|
||||
model._coerce([
|
||||
{vip: true}
|
||||
]);
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
assert(error, 'An error should have been thrown');
|
||||
});
|
||||
|
||||
it('should throw if the and operator does not take an array', function () {
|
||||
try {
|
||||
// The and operator only takes an array of objects
|
||||
model._coerce({and: {x: 1}});
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
assert(error, 'An error should have been thrown');
|
||||
});
|
||||
|
||||
it('should throw if the or operator does not take an array', function () {
|
||||
try {
|
||||
// The or operator only takes an array of objects
|
||||
model._coerce({or: {x: 1}});
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
assert(error, 'An error should have been thrown');
|
||||
});
|
||||
|
||||
it('should throw if the or operator does not take an array of objects', function () {
|
||||
try {
|
||||
// The or operator only takes an array of objects
|
||||
model._coerce({or: ['x']});
|
||||
} catch(err) {
|
||||
error = err;
|
||||
}
|
||||
assert(error, 'An error should have been thrown');
|
||||
});
|
||||
|
||||
it('should throw if filter property is not an object', function () {
|
||||
var filter = null;
|
||||
try {
|
||||
// The filter clause has to be an object
|
||||
filter = model._normalize('abc');
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
assert(error, 'An error should have been thrown');
|
||||
});
|
||||
|
||||
it('should throw if filter.limit property is not a number', function () {
|
||||
try {
|
||||
// The limit param must be a valid number
|
||||
filter = model._normalize({limit: 'x'});
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
assert(error, 'An error should have been thrown');
|
||||
});
|
||||
|
||||
it('should throw if filter.limit property is nagative', function () {
|
||||
try {
|
||||
// The limit param must be a valid number
|
||||
filter = model._normalize({limit: -1});
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
assert(error, 'An error should have been thrown');
|
||||
});
|
||||
|
||||
it('should throw if filter.limit property is not an integer', function () {
|
||||
try {
|
||||
// The limit param must be a valid number
|
||||
filter = model._normalize({limit: 5.8});
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
assert(error, 'An error should have been thrown');
|
||||
});
|
||||
|
||||
it('should throw if filter.offset property is not a number', function () {
|
||||
try {
|
||||
// The limit param must be a valid number
|
||||
filter = model._normalize({offset: 'x'});
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
assert(error, 'An error should have been thrown');
|
||||
});
|
||||
|
||||
it('should throw if filter.skip property is not a number', function () {
|
||||
try {
|
||||
// The limit param must be a valid number
|
||||
filter = model._normalize({skip: '_'});
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
assert(error, 'An error should have been thrown');
|
||||
});
|
||||
|
||||
it('should normalize limit/offset/skip', function () {
|
||||
filter = model._normalize({limit: '10', skip: 5});
|
||||
assert.deepEqual(filter, {limit: 10, offset: 5, skip: 5});
|
||||
});
|
||||
|
||||
it('should set the default value for limit', function () {
|
||||
filter = model._normalize({skip: 5});
|
||||
assert.deepEqual(filter, {limit: 100, offset: 5, skip: 5});
|
||||
});
|
||||
|
||||
it('should skip GeoPoint', function () {
|
||||
where = model._coerce({location: {near: {lng: 10, lat: 20}, maxDistance: 20}});
|
||||
assert.deepEqual(where, {location: {near: {lng: 10, lat: 20}, maxDistance: 20}});
|
||||
|
@ -1162,15 +1305,24 @@ describe('Load models from json', function () {
|
|||
customer.should.not.have.property('bio');
|
||||
|
||||
// The properties are defined at prototype level
|
||||
assert.equal(Object.keys(customer).length, 0);
|
||||
assert.equal(Object.keys(customer).filter(function (k) {
|
||||
// Remove internal properties
|
||||
return k.indexOf('__') === -1;
|
||||
}).length, 0);
|
||||
var count = 0;
|
||||
for (var p in customer) {
|
||||
if (p.indexOf('__') === 0) {
|
||||
continue;
|
||||
}
|
||||
if (typeof customer[p] !== 'function') {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
assert.equal(count, 7); // Please note there is an injected id from User prototype
|
||||
assert.equal(Object.keys(customer.toObject()).length, 6);
|
||||
assert.equal(Object.keys(customer.toObject()).filter(function (k) {
|
||||
// Remove internal properties
|
||||
return k.indexOf('__') === -1;
|
||||
}).length, 6);
|
||||
|
||||
done(null, customer);
|
||||
});
|
||||
|
|
|
@ -133,14 +133,11 @@ describe('manipulation', function () {
|
|||
Person.findOne(function (err, p) {
|
||||
should.not.exist(err);
|
||||
p.name = 'Hans';
|
||||
p.propertyChanged('name').should.be.true;
|
||||
p.save(function (err) {
|
||||
should.not.exist(err);
|
||||
p.propertyChanged('name').should.be.false;
|
||||
Person.findOne(function (err, p) {
|
||||
should.not.exist(err);
|
||||
p.name.should.equal('Hans');
|
||||
p.propertyChanged('name').should.be.false;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
@ -157,10 +154,8 @@ describe('manipulation', function () {
|
|||
p.name = 'Nana';
|
||||
p.save(function (err) {
|
||||
should.exist(err);
|
||||
p.propertyChanged('name').should.be.true;
|
||||
p.save({validate: false}, function (err) {
|
||||
should.not.exist(err);
|
||||
p.propertyChanged('name').should.be.false;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
@ -244,10 +239,7 @@ describe('manipulation', function () {
|
|||
person = new Person({name: hw});
|
||||
|
||||
person.name.should.equal(hw);
|
||||
person.propertyChanged('name').should.be.false;
|
||||
person.name = 'Goodbye, Lenin';
|
||||
person.name$was.should.equal(hw);
|
||||
person.propertyChanged('name').should.be.true;
|
||||
(person.createdAt >= now).should.be.true;
|
||||
person.isNewRecord().should.be.true;
|
||||
});
|
||||
|
|
|
@ -4,6 +4,7 @@ var path = require('path');
|
|||
var fs = require('fs');
|
||||
var assert = require('assert');
|
||||
var async = require('async');
|
||||
var should = require('./init.js');
|
||||
|
||||
describe('Memory connector', function () {
|
||||
var file = path.join(__dirname, 'memory.json');
|
||||
|
@ -91,5 +92,92 @@ describe('Memory connector', function () {
|
|||
});
|
||||
|
||||
});
|
||||
|
||||
describe('Query for memory connector', function () {
|
||||
var ds = new DataSource({
|
||||
connector: 'memory'
|
||||
});
|
||||
|
||||
var User = ds.define('User', {
|
||||
seq: {type: Number, index: true},
|
||||
name: {type: String, index: true, sort: true},
|
||||
email: {type: String, index: true},
|
||||
birthday: {type: Date, index: true},
|
||||
role: {type: String, index: true},
|
||||
order: {type: Number, index: true, sort: true},
|
||||
vip: {type: Boolean}
|
||||
});
|
||||
|
||||
before(seed);
|
||||
it('should allow to find using like', function (done) {
|
||||
User.find({where: {name: {like: '%St%'}}}, function (err, posts) {
|
||||
should.not.exist(err);
|
||||
posts.should.have.property('length', 2);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should support like for no match', function (done) {
|
||||
User.find({where: {name: {like: 'M%XY'}}}, function (err, posts) {
|
||||
should.not.exist(err);
|
||||
posts.should.have.property('length', 0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow to find using nlike', function (done) {
|
||||
User.find({where: {name: {nlike: '%St%'}}}, function (err, posts) {
|
||||
should.not.exist(err);
|
||||
posts.should.have.property('length', 4);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should support nlike for no match', function (done) {
|
||||
User.find({where: {name: {nlike: 'M%XY'}}}, function (err, posts) {
|
||||
should.not.exist(err);
|
||||
posts.should.have.property('length', 6);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
function seed(done) {
|
||||
var beatles = [
|
||||
{
|
||||
seq: 0,
|
||||
name: 'John Lennon',
|
||||
email: 'john@b3atl3s.co.uk',
|
||||
role: 'lead',
|
||||
birthday: new Date('1980-12-08'),
|
||||
order: 2,
|
||||
vip: true
|
||||
},
|
||||
{
|
||||
seq: 1,
|
||||
name: 'Paul McCartney',
|
||||
email: 'paul@b3atl3s.co.uk',
|
||||
role: 'lead',
|
||||
birthday: new Date('1942-06-18'),
|
||||
order: 1,
|
||||
vip: true
|
||||
},
|
||||
{seq: 2, name: 'George Harrison', order: 5, vip: false},
|
||||
{seq: 3, name: 'Ringo Starr', order: 6, vip: false},
|
||||
{seq: 4, name: 'Pete Best', order: 4},
|
||||
{seq: 5, name: 'Stuart Sutcliffe', order: 3, vip: true}
|
||||
];
|
||||
|
||||
async.series([
|
||||
User.destroyAll.bind(User),
|
||||
function(cb) {
|
||||
async.each(beatles, User.create.bind(User), cb);
|
||||
}
|
||||
], done);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// This test written in mocha+should.js
|
||||
var should = require('./init.js');
|
||||
|
||||
var db, Book, Chapter, Author, Reader;
|
||||
var db, Book, Chapter, Author, Reader, Publisher;
|
||||
|
||||
describe('relations', function () {
|
||||
before(function (done) {
|
||||
|
@ -180,6 +180,42 @@ describe('relations', function () {
|
|||
|
||||
});
|
||||
|
||||
describe('hasOne', function () {
|
||||
var Supplier, Account;
|
||||
|
||||
before(function () {
|
||||
db = getSchema();
|
||||
Supplier = db.define('Supplier', {name: String});
|
||||
Account = db.define('Account', {accountNo: String});
|
||||
});
|
||||
|
||||
it('can be declared using hasOne method', function () {
|
||||
Supplier.hasOne(Account);
|
||||
Object.keys((new Account()).toObject()).should.include('supplierId');
|
||||
(new Supplier()).account.should.be.an.instanceOf(Function);
|
||||
});
|
||||
|
||||
it('can be used to query data', function (done) {
|
||||
// Supplier.hasOne(Account);
|
||||
db.automigrate(function () {
|
||||
Supplier.create({name: 'Supplier 1'}, function (e, supplier) {
|
||||
should.not.exist(e);
|
||||
should.exist(supplier);
|
||||
supplier.account.create({accountNo: 'a01'}, function (err, account) {
|
||||
supplier.account(function (e, act) {
|
||||
should.not.exist(e);
|
||||
should.exist(act);
|
||||
act.should.be.an.instanceOf(Account);
|
||||
supplier.account().should.equal(act.id);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('hasAndBelongsToMany', function () {
|
||||
var Article, Tag, ArticleTag;
|
||||
it('can be declared', function (done) {
|
||||
|
|
|
@ -3,7 +3,7 @@ var should = require('./init.js');
|
|||
|
||||
var db, Railway, Station;
|
||||
|
||||
describe('sc0pe', function () {
|
||||
describe('scope', function () {
|
||||
|
||||
before(function () {
|
||||
db = getSchema();
|
||||
|
|
Loading…
Reference in New Issue