Organize model.js

This commit is contained in:
Anatoliy Chakkaev 2013-04-12 03:23:34 +04:00 committed by Raymond Feng
parent 0974326bdc
commit 136ea91fbf
11 changed files with 548 additions and 512 deletions

View File

@ -14,7 +14,9 @@ jugglingdb-changelog(3) - The History of JugglingDB
**NOTE**: this change could break some code.
* **Datatypes**:
Now object casts type of member on assignment.
Now object casts type of member on assignment. It may cause issues if
mongodb's ObjectID was manually used as type for property. Solution: not use
it as type directly, and specify wrapper instead.
### 0.2.1

View File

@ -2,8 +2,7 @@ var fs = require('fs');
var path = require('path');
exports.Schema = require('./lib/schema').Schema;
exports.AbstractClass = require('./lib/model.js').AbstractClass;
exports.Validatable = require('./lib/validations.js').Validatable;
exports.AbstractClass = require('./lib/model.js');
var baseSQL = './lib/sql';

View File

@ -1,9 +1,16 @@
/**
* Module exports
*/
exports.Hookable = Hookable;
function Hookable() {
// hookable class
};
/**
* Hooks mixins for ./model.js
*/
var Hookable = require('./model.js');
/**
* List of hooks available
*/
Hookable.afterInitialize = null;
Hookable.beforeValidate = null;
Hookable.afterValidate = null;

157
lib/include.js Normal file
View File

@ -0,0 +1,157 @@
/**
* Include mixin for ./model.js
*/
var AbstractClass = require('./model.js');
/**
* Allows you to load relations of several objects and optimize numbers of requests.
*
* @param {Array} objects - array of instances
* @param {String}, {Object} or {Array} include - which relations you want to load.
* @param {Function} cb - Callback called when relations are loaded
*
* Examples:
*
* - User.include(users, 'posts', function() {}); will load all users posts with only one additional request.
* - User.include(users, ['posts'], function() {}); // same
* - User.include(users, ['posts', 'passports'], function() {}); // will load all users posts and passports with two
* additional requests.
* - Passport.include(passports, {owner: 'posts'}, function() {}); // will load all passports owner (users), and all
* posts of each owner loaded
* - Passport.include(passports, {owner: ['posts', 'passports']}); // ...
* - Passport.include(passports, {owner: [{posts: 'images'}, 'passports']}); // ...
*
*/
AbstractClass.include = function (objects, include, cb) {
var self = this;
if (
(include.constructor.name == 'Array' && include.length == 0) ||
(include.constructor.name == 'Object' && Object.keys(include).length == 0)
) {
cb(null, objects);
return;
}
include = processIncludeJoin(include);
var keyVals = {};
var objsByKeys = {};
var nbCallbacks = 0;
for (var i = 0; i < include.length; i++) {
var callback = processIncludeItem(objects, include[i], keyVals, objsByKeys);
if (callback !== null) {
nbCallbacks++;
callback(function() {
nbCallbacks--;
if (nbCallbacks == 0) {
cb(null, objects);
}
});
} else {
cb(null, objects);
}
}
function processIncludeJoin(ij) {
if (typeof ij === 'string') {
ij = [ij];
}
if (ij.constructor.name === 'Object') {
var newIj = [];
for (var key in ij) {
var obj = {};
obj[key] = ij[key];
newIj.push(obj);
}
return newIj;
}
return ij;
}
function processIncludeItem(objs, include, keyVals, objsByKeys) {
var relations = self.relations;
if (include.constructor.name === 'Object') {
var relationName = Object.keys(include)[0];
var subInclude = include[relationName];
} else {
var relationName = include;
var subInclude = [];
}
var relation = relations[relationName];
if (!relation) {
return function() {
cb(new Error('Relation "' + relationName + '" is not defined for ' + self.modelName + ' model'));
}
}
var req = {'where': {}};
if (!keyVals[relation.keyFrom]) {
objsByKeys[relation.keyFrom] = {};
for (var j = 0; j < objs.length; j++) {
if (!objsByKeys[relation.keyFrom][objs[j][relation.keyFrom]]) {
objsByKeys[relation.keyFrom][objs[j][relation.keyFrom]] = [];
}
objsByKeys[relation.keyFrom][objs[j][relation.keyFrom]].push(objs[j]);
}
keyVals[relation.keyFrom] = Object.keys(objsByKeys[relation.keyFrom]);
}
if (keyVals[relation.keyFrom].length > 0) {
// deep clone is necessary since inq seems to change the processed array
var keysToBeProcessed = {};
var inValues = [];
for (var j = 0; j < keyVals[relation.keyFrom].length; j++) {
keysToBeProcessed[keyVals[relation.keyFrom][j]] = true;
if (keyVals[relation.keyFrom][j] !== 'null' && keyVals[relation.keyFrom][j] !== 'undefined') {
inValues.push(keyVals[relation.keyFrom][j]);
}
}
req['where'][relation.keyTo] = {inq: inValues};
req['include'] = subInclude;
return function(cb) {
relation.modelTo.all(req, function(err, objsIncluded) {
for (var i = 0; i < objsIncluded.length; i++) {
delete keysToBeProcessed[objsIncluded[i][relation.keyTo]];
var objectsFrom = objsByKeys[relation.keyFrom][objsIncluded[i][relation.keyTo]];
for (var j = 0; j < objectsFrom.length; j++) {
if (!objectsFrom[j].__cachedRelations) {
objectsFrom[j].__cachedRelations = {};
}
if (relation.multiple) {
if (!objectsFrom[j].__cachedRelations[relationName]) {
objectsFrom[j].__cachedRelations[relationName] = [];
}
objectsFrom[j].__cachedRelations[relationName].push(objsIncluded[i]);
} else {
objectsFrom[j].__cachedRelations[relationName] = objsIncluded[i];
}
}
}
// No relation have been found for these keys
for (var key in keysToBeProcessed) {
var objectsFrom = objsByKeys[relation.keyFrom][key];
for (var j = 0; j < objectsFrom.length; j++) {
if (!objectsFrom[j].__cachedRelations) {
objectsFrom[j].__cachedRelations = {};
}
objectsFrom[j].__cachedRelations[relationName] = relation.multiple ? [] : null;
}
}
cb(err, objsIncluded);
});
};
}
return null;
}
}

View File

@ -1,25 +1,23 @@
/**
* Module exports class Model
*/
module.exports = AbstractClass;
/**
* Module dependencies
*/
var util = require('util');
var i8n = require('inflection');
var jutil = require('./jutil.js');
var validations = require('./validations.js');
var Validatable = validations.Validatable;
var ValidationError = validations.ValidationError;
var List = require('./list.js');
var Hookable = require('./hooks.js').Hookable;
var DEFAULT_CACHE_LIMIT = 1000;
require('./hooks.js');
require('./relations.js');
require('./include.js');
var BASE_TYPES = ['String', 'Boolean', 'Number', 'Date', 'Text'];
exports.AbstractClass = AbstractClass;
AbstractClass.__proto__ = Validatable;
AbstractClass.prototype.__proto__ = Validatable.prototype;
jutil.inherits(AbstractClass, Hookable);
/**
* Abstract class - base class for all persist objects
* Model class - base class for all persist objects
* provides **common API** to access any database adapter.
* This class describes only abstract behavior layer, refer to `lib/adapters/*.js`
* to learn more about specific adapter implementations
@ -462,158 +460,6 @@ AbstractClass.count = function (where, cb) {
this.schema.adapter.count(this.modelName, cb, where);
};
/**
* Allows you to load relations of several objects and optimize numbers of requests.
*
* @param {Array} objects - array of instances
* @param {String}, {Object} or {Array} include - which relations you want to load.
* @param {Function} cb - Callback called when relations are loaded
*
* Examples:
*
* - User.include(users, 'posts', function() {}); will load all users posts with only one additional request.
* - User.include(users, ['posts'], function() {}); // same
* - User.include(users, ['posts', 'passports'], function() {}); // will load all users posts and passports with two
* additional requests.
* - Passport.include(passports, {owner: 'posts'}, function() {}); // will load all passports owner (users), and all
* posts of each owner loaded
* - Passport.include(passports, {owner: ['posts', 'passports']}); // ...
* - Passport.include(passports, {owner: [{posts: 'images'}, 'passports']}); // ...
*
*/
AbstractClass.include = function (objects, include, cb) {
var self = this;
if (
(include.constructor.name == 'Array' && include.length == 0) ||
(include.constructor.name == 'Object' && Object.keys(include).length == 0)
) {
cb(null, objects);
return;
}
include = processIncludeJoin(include);
var keyVals = {};
var objsByKeys = {};
var nbCallbacks = 0;
for (var i = 0; i < include.length; i++) {
var callback = processIncludeItem(objects, include[i], keyVals, objsByKeys);
if (callback !== null) {
nbCallbacks++;
callback(function() {
nbCallbacks--;
if (nbCallbacks == 0) {
cb(null, objects);
}
});
} else {
cb(null, objects);
}
}
function processIncludeJoin(ij) {
if (typeof ij === 'string') {
ij = [ij];
}
if (ij.constructor.name === 'Object') {
var newIj = [];
for (var key in ij) {
var obj = {};
obj[key] = ij[key];
newIj.push(obj);
}
return newIj;
}
return ij;
}
function processIncludeItem(objs, include, keyVals, objsByKeys) {
var relations = self.relations;
if (include.constructor.name === 'Object') {
var relationName = Object.keys(include)[0];
var subInclude = include[relationName];
} else {
var relationName = include;
var subInclude = [];
}
var relation = relations[relationName];
if (!relation) {
return function() {
cb(new Error('Relation "' + relationName + '" is not defined for ' + self.modelName + ' model'));
}
}
var req = {'where': {}};
if (!keyVals[relation.keyFrom]) {
objsByKeys[relation.keyFrom] = {};
for (var j = 0; j < objs.length; j++) {
if (!objsByKeys[relation.keyFrom][objs[j][relation.keyFrom]]) {
objsByKeys[relation.keyFrom][objs[j][relation.keyFrom]] = [];
}
objsByKeys[relation.keyFrom][objs[j][relation.keyFrom]].push(objs[j]);
}
keyVals[relation.keyFrom] = Object.keys(objsByKeys[relation.keyFrom]);
}
if (keyVals[relation.keyFrom].length > 0) {
// deep clone is necessary since inq seems to change the processed array
var keysToBeProcessed = {};
var inValues = [];
for (var j = 0; j < keyVals[relation.keyFrom].length; j++) {
keysToBeProcessed[keyVals[relation.keyFrom][j]] = true;
if (keyVals[relation.keyFrom][j] !== 'null' && keyVals[relation.keyFrom][j] !== 'undefined') {
inValues.push(keyVals[relation.keyFrom][j]);
}
}
req['where'][relation.keyTo] = {inq: inValues};
req['include'] = subInclude;
return function(cb) {
relation.modelTo.all(req, function(err, objsIncluded) {
for (var i = 0; i < objsIncluded.length; i++) {
delete keysToBeProcessed[objsIncluded[i][relation.keyTo]];
var objectsFrom = objsByKeys[relation.keyFrom][objsIncluded[i][relation.keyTo]];
for (var j = 0; j < objectsFrom.length; j++) {
if (!objectsFrom[j].__cachedRelations) {
objectsFrom[j].__cachedRelations = {};
}
if (relation.multiple) {
if (!objectsFrom[j].__cachedRelations[relationName]) {
objectsFrom[j].__cachedRelations[relationName] = [];
}
objectsFrom[j].__cachedRelations[relationName].push(objsIncluded[i]);
} else {
objectsFrom[j].__cachedRelations[relationName] = objsIncluded[i];
}
}
}
// No relation have been found for these keys
for (var key in keysToBeProcessed) {
var objectsFrom = objsByKeys[relation.keyFrom][key];
for (var j = 0; j < objectsFrom.length; j++) {
if (!objectsFrom[j].__cachedRelations) {
objectsFrom[j].__cachedRelations = {};
}
objectsFrom[j].__cachedRelations[relationName] = relation.multiple ? [] : null;
}
}
cb(err, objsIncluded);
});
};
}
return null;
}
}
/**
* Return string representation of class
*
@ -893,320 +739,6 @@ AbstractClass.prototype.reset = function () {
});
};
/**
* Declare hasMany relation
*
* @param {Class} anotherClass - class to has many
* @param {Object} params - configuration {as:, foreignKey:}
* @example `User.hasMany(Post, {as: 'posts', foreignKey: 'authorId'});`
*/
AbstractClass.hasMany = function hasMany(anotherClass, params) {
var thisClass = this, 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();
for(var name in this.schema.models) {
if (name.toLowerCase() === anotherClassName) {
anotherClass = this.schema.models[name];
}
}
}
}
var methodName = params.as ||
i8n.camelize(i8n.pluralize(anotherClass.modelName), true);
var fk = params.foreignKey || i8n.camelize(thisClassName + '_id', true);
this.relations[params['as']] = {
type: 'hasMany',
keyFrom: 'id',
keyTo: fk,
modelTo: anotherClass,
multiple: true
};
// each instance of this class should have method named
// pluralize(anotherClass.modelName)
// which is actually just anotherClass.all({where: {thisModelNameId: this.id}}, cb);
defineScope(this.prototype, anotherClass, methodName, function () {
var x = {};
x[fk] = this.id;
return {where: x};
}, {
find: find,
destroy: destroy
});
// obviously, anotherClass should have attribute called `fk`
anotherClass.schema.defineForeignKey(anotherClass.modelName, fk);
function find(id, cb) {
anotherClass.find(id, function (err, inst) {
if (err) return cb(err);
if (!inst) return cb(new Error('Not found'));
if (inst[fk] && inst[fk].toString() == this.id.toString()) {
cb(null, inst);
} else {
cb(new Error('Permission denied'));
}
}.bind(this));
}
function destroy(id, cb) {
this.find(id, function (err, inst) {
if (err) return cb(err);
if (inst) {
inst.destroy(cb);
} else {
cb(new Error('Not found'));
}
});
}
};
/**
* Declare belongsTo relation
*
* @param {Class} anotherClass - class to belong
* @param {Object} params - configuration {as: 'propertyName', foreignKey: 'keyName'}
*
* **Usage examples**
* Suppose model Post have a *belongsTo* relationship with User (the author of the post). You could declare it this way:
* Post.belongsTo(User, {as: 'author', foreignKey: 'userId'});
*
* When a post is loaded, you can load the related author with:
* post.author(function(err, user) {
* // the user variable is your user object
* });
*
* The related object is cached, so if later you try to get again the author, no additional request will be made.
* But there is an optional boolean parameter in first position that set whether or not you want to reload the cache:
* post.author(true, function(err, user) {
* // The user is reloaded, even if it was already cached.
* });
*
* This optional parameter default value is false, so the related object will be loaded from cache if available.
*/
AbstractClass.belongsTo = function (anotherClass, params) {
params = params || {};
if ('string' === typeof anotherClass) {
params.as = anotherClass;
if (params.model) {
anotherClass = params.model;
} else {
var anotherClassName = anotherClass.toLowerCase();
for(var name in this.schema.models) {
if (name.toLowerCase() === anotherClassName) {
anotherClass = this.schema.models[name];
}
}
}
}
var methodName = params.as || i8n.camelize(anotherClass.modelName, true);
var fk = params.foreignKey || methodName + 'Id';
this.relations[params['as']] = {
type: 'belongsTo',
keyFrom: fk,
keyTo: 'id',
modelTo: anotherClass,
multiple: false
};
this.schema.defineForeignKey(this.modelName, fk);
this.prototype['__finders__'] = this.prototype['__finders__'] || {};
this.prototype['__finders__'][methodName] = function (id, cb) {
if (id === null) {
cb(null, null);
return;
}
anotherClass.find(id, function (err,inst) {
if (err) return cb(err);
if (!inst) return cb(null, null);
if (inst.id === this[fk]) {
cb(null, inst);
} else {
cb(new Error('Permission denied'));
}
}.bind(this));
};
this.prototype[methodName] = 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 && (typeof this.__cachedRelations[methodName] !== 'undefined')) {
cachedValue = this.__cachedRelations[methodName];
}
if (p instanceof AbstractClass) { // acts as setter
this[fk] = p.id;
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 scope
* TODO: describe behavior and usage examples
*/
AbstractClass.scope = function (name, params) {
defineScope(this, this, name, params);
};
function defineScope(cls, targetClass, name, params, methods) {
// collect meta info about scope
if (!cls._scopeMeta) {
cls._scopeMeta = {};
}
// only makes sence to add scope in meta if base and target classes
// are same
if (cls === targetClass) {
cls._scopeMeta[name] = params;
} else {
if (!targetClass._scopeMeta) {
targetClass._scopeMeta = {};
}
}
Object.defineProperty(cls, name, {
enumerable: false,
configurable: true,
get: function () {
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;
}
} else {
throw new Error('Method can be only called with one or two arguments');
}
if (!this.__cachedRelations || (typeof this.__cachedRelations[name] == 'undefined') || actualRefresh) {
var self = this;
return targetClass.all(mergeParams(actualCond, caller._scope), function(err, data) {
if (!err && saveOnCache) {
self.__cachedRelations[name] = data;
}
cb(err, data);
});
} else {
cb(null, this.__cachedRelations[name]);
}
};
f._scope = typeof params === 'function' ? params.call(this) : params;
f.build = build;
f.create = create;
f.destroyAll = destroyAll;
for (var i in methods) {
f[i] = methods[i].bind(this);
}
// define sub-scopes
Object.keys(targetClass._scopeMeta).forEach(function (name) {
Object.defineProperty(f, name, {
enumerable: false,
get: function () {
mergeParams(f._scope, targetClass._scopeMeta[name]);
return f;
}
});
}.bind(this));
return f;
}
});
// and it should have create/build methods with binded thisModelNameId param
function build(data) {
return new targetClass(mergeParams(this._scope, {where:data || {}}).where);
}
function create(data, cb) {
if (typeof data === 'function') {
cb = data;
data = {};
}
this.build(data).save(cb);
}
/*
Callback
- The callback will be called after all elements are destroyed
- For every destroy call which results in an error
- If fetching the Elements on which destroyAll is called results in an error
*/
function destroyAll(cb) {
targetClass.all(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) {
if (update.where) {
base.where = merge(base.where, update.where);
}
// overwrite order
if (update.order) {
base.order = update.order;
}
return base;
}
}
AbstractClass.prototype.inspect = function () {
return util.inspect(this.__data, false, 4, true);
};
@ -1222,22 +754,6 @@ function isdef(s) {
return s !== undef;
}
/**
* 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`
*/
function merge(base, update) {
base = base || {};
if (update) {
Object.keys(update).forEach(function (key) {
base[key] = update[key];
});
}
return base;
}
/**
* Define readonly property on object
*
@ -1253,4 +769,3 @@ function defineReadonlyProp(obj, key, value) {
value: value
});
}

191
lib/relations.js Normal file
View File

@ -0,0 +1,191 @@
/**
* Dependencies
*/
var i8n = require('inflection');
var defineScope = require('./scope.js').defineScope;
/**
* Relations mixins for ./model.js
*/
var AbstractClass = require('./model.js');
/**
* Declare hasMany relation
*
* @param {Class} anotherClass - class to has many
* @param {Object} params - configuration {as:, foreignKey:}
* @example `User.hasMany(Post, {as: 'posts', foreignKey: 'authorId'});`
*/
AbstractClass.hasMany = function hasMany(anotherClass, params) {
var thisClass = this, 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();
for(var name in this.schema.models) {
if (name.toLowerCase() === anotherClassName) {
anotherClass = this.schema.models[name];
}
}
}
}
var methodName = params.as ||
i8n.camelize(i8n.pluralize(anotherClass.modelName), true);
var fk = params.foreignKey || i8n.camelize(thisClassName + '_id', true);
this.relations[params['as']] = {
type: 'hasMany',
keyFrom: 'id',
keyTo: fk,
modelTo: anotherClass,
multiple: true
};
// each instance of this class should have method named
// pluralize(anotherClass.modelName)
// which is actually just anotherClass.all({where: {thisModelNameId: this.id}}, cb);
defineScope(this.prototype, anotherClass, methodName, function () {
var x = {};
x[fk] = this.id;
return {where: x};
}, {
find: find,
destroy: destroy
});
// obviously, anotherClass should have attribute called `fk`
anotherClass.schema.defineForeignKey(anotherClass.modelName, fk);
function find(id, cb) {
anotherClass.find(id, function (err, inst) {
if (err) return cb(err);
if (!inst) return cb(new Error('Not found'));
if (inst[fk] && inst[fk].toString() == this.id.toString()) {
cb(null, inst);
} else {
cb(new Error('Permission denied'));
}
}.bind(this));
}
function destroy(id, cb) {
this.find(id, function (err, inst) {
if (err) return cb(err);
if (inst) {
inst.destroy(cb);
} else {
cb(new Error('Not found'));
}
});
}
};
/**
* Declare belongsTo relation
*
* @param {Class} anotherClass - class to belong
* @param {Object} params - configuration {as: 'propertyName', foreignKey: 'keyName'}
*
* **Usage examples**
* Suppose model Post have a *belongsTo* relationship with User (the author of the post). You could declare it this way:
* Post.belongsTo(User, {as: 'author', foreignKey: 'userId'});
*
* When a post is loaded, you can load the related author with:
* post.author(function(err, user) {
* // the user variable is your user object
* });
*
* The related object is cached, so if later you try to get again the author, no additional request will be made.
* But there is an optional boolean parameter in first position that set whether or not you want to reload the cache:
* post.author(true, function(err, user) {
* // The user is reloaded, even if it was already cached.
* });
*
* This optional parameter default value is false, so the related object will be loaded from cache if available.
*/
AbstractClass.belongsTo = function (anotherClass, params) {
params = params || {};
if ('string' === typeof anotherClass) {
params.as = anotherClass;
if (params.model) {
anotherClass = params.model;
} else {
var anotherClassName = anotherClass.toLowerCase();
for(var name in this.schema.models) {
if (name.toLowerCase() === anotherClassName) {
anotherClass = this.schema.models[name];
}
}
}
}
var methodName = params.as || i8n.camelize(anotherClass.modelName, true);
var fk = params.foreignKey || methodName + 'Id';
this.relations[params['as']] = {
type: 'belongsTo',
keyFrom: fk,
keyTo: 'id',
modelTo: anotherClass,
multiple: false
};
this.schema.defineForeignKey(this.modelName, fk);
this.prototype['__finders__'] = this.prototype['__finders__'] || {};
this.prototype['__finders__'][methodName] = function (id, cb) {
if (id === null) {
cb(null, null);
return;
}
anotherClass.find(id, function (err,inst) {
if (err) return cb(err);
if (!inst) return cb(null, null);
if (inst.id === this[fk]) {
cb(null, inst);
} else {
cb(new Error('Permission denied'));
}
}.bind(this));
};
this.prototype[methodName] = 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 && (typeof this.__cachedRelations[methodName] !== 'undefined')) {
cachedValue = this.__cachedRelations[methodName];
}
if (p instanceof AbstractClass) { // acts as setter
this[fk] = p.id;
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];
}
};
};

View File

@ -1,7 +1,7 @@
/**
* Module dependencies
*/
var AbstractClass = require('./model.js').AbstractClass;
var AbstractClass = require('./model.js');
var List = require('./list.js');
var EventEmitter = require('events').EventEmitter;
var util = require('util');

160
lib/scope.js Normal file
View File

@ -0,0 +1,160 @@
/**
* Module exports
*/
exports.defineScope = defineScope;
/**
* Scope mixin for ./model.js
*/
var Model = require('./model.js');
/**
* Define scope
* TODO: describe behavior and usage examples
*/
Model.scope = function (name, params) {
defineScope(this, this, name, params);
};
function defineScope(cls, targetClass, name, params, methods) {
// collect meta info about scope
if (!cls._scopeMeta) {
cls._scopeMeta = {};
}
// only makes sence to add scope in meta if base and target classes
// are same
if (cls === targetClass) {
cls._scopeMeta[name] = params;
} else {
if (!targetClass._scopeMeta) {
targetClass._scopeMeta = {};
}
}
Object.defineProperty(cls, name, {
enumerable: false,
configurable: true,
get: function () {
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;
}
} else {
throw new Error('Method can be only called with one or two arguments');
}
if (!this.__cachedRelations || (typeof this.__cachedRelations[name] == 'undefined') || actualRefresh) {
var self = this;
return targetClass.all(mergeParams(actualCond, caller._scope), function(err, data) {
if (!err && saveOnCache) {
self.__cachedRelations[name] = data;
}
cb(err, data);
});
} else {
cb(null, this.__cachedRelations[name]);
}
};
f._scope = typeof params === 'function' ? params.call(this) : params;
f.build = build;
f.create = create;
f.destroyAll = destroyAll;
for (var i in methods) {
f[i] = methods[i].bind(this);
}
// define sub-scopes
Object.keys(targetClass._scopeMeta).forEach(function (name) {
Object.defineProperty(f, name, {
enumerable: false,
get: function () {
mergeParams(f._scope, targetClass._scopeMeta[name]);
return f;
}
});
}.bind(this));
return f;
}
});
// and it should have create/build methods with binded thisModelNameId param
function build(data) {
return new targetClass(mergeParams(this._scope, {where:data || {}}).where);
}
function create(data, cb) {
if (typeof data === 'function') {
cb = data;
data = {};
}
this.build(data).save(cb);
}
/*
Callback
- The callback will be called after all elements are destroyed
- For every destroy call which results in an error
- If fetching the Elements on which destroyAll is called results in an error
*/
function destroyAll(cb) {
targetClass.all(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) {
if (update.where) {
base.where = merge(base.where, update.where);
}
// overwrite order
if (update.order) {
base.order = update.order;
}
return base;
}
}
/**
* 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`
*/
function merge(base, update) {
base = base || {};
if (update) {
Object.keys(update).forEach(function (key) {
base[key] = update[key];
});
}
return base;
}

View File

@ -1,8 +1,10 @@
exports.Validatable = Validatable;
/**
* Module exports
*/
exports.ValidationError = ValidationError;
/**
* Validation encapsulated in this abstract class.
* Validation mixins for model.js
*
* Basically validation configurators is just class methods, which adds validations
* configs to AbstractClass._validations. Each of this validations run when
@ -16,9 +18,7 @@ exports.ValidationError = ValidationError;
* In more complicated cases it can be {Hash} of messages (for each case):
* `User.validatesLengthOf('password', { min: 6, max: 20, message: {min: 'too short', max: 'too long'}});`
*/
function Validatable() {
// validatable class
};
var Validatable = require('./model.js');
/**
* Validate presence. This validation fails when validated field is blank.

View File

@ -174,8 +174,7 @@ describe('basic-querying', function() {
});
});
// TODO: it's not basic query, move to advanced querying test
it.skip('should find last record in filtered set', function(done) {
it('should find last record in filtered set', function(done) {
User.findOne({
where: {role: 'lead'},
order: 'order DESC'
@ -188,7 +187,7 @@ describe('basic-querying', function() {
});
});
it.skip('should work even when find by id', function(done) {
it('should work even when find by id', function(done) {
User.findOne(function(e, u) {
User.findOne({where: {id: u.id}}, function(err, user) {
should.not.exist(err);

View File

@ -159,7 +159,13 @@ describe('relations', function() {
});
});
describe('hasAndBelongsToMany', function() {
it('can be declared');
describe.skip('hasAndBelongsToMany', function() {
var Article, Tag;
it('can be declared', function(done) {
Article = db.define('Article', {title: String});
Tag = db.define('Tag', {name: String});
Article.hasAndBelongsToMany('tags');
});
});
});