This commit is contained in:
Anatoliy Chakkaev 2011-10-15 19:57:35 +04:00
parent 14398c24d7
commit 8e05e59933
4 changed files with 164 additions and 46 deletions

View File

@ -34,8 +34,8 @@ var User = schema.define('User', {
User.hasMany(Post, {as: 'posts', foreignKey: 'userId'});
// creates instance methods:
// user.posts(conds)
// user.buildPost(data) // like new Post({userId: user.id});
// user.createPost(data) // build and save
// user.posts.build(data) // like new Post({userId: user.id});
// user.posts.create(data) // build and save
Post.belongsTo(User, {as: 'author', foreignKey: 'userId'});
// creates instance methods:
@ -48,7 +48,7 @@ s.automigrate(); // required only for mysql NOTE: it will drop User and Post tab
// work with models:
var user = new User;
user.save(function (err) {
var post = user.buildPost({title: 'Hello world'});
var post = user.posts.build({title: 'Hello world'});
post.save(console.log);
});
@ -65,9 +65,9 @@ Post.all({userId: user.id});
// the same as prev
user.posts(cb)
// same as new Post({userId: user.id});
user.buildPost
user.posts.build
// save as Post.create({userId: user.id}, cb);
user.createPost(cb)
user.posts.create(cb)
// find instance by id
User.find(1, cb)
// count instances
@ -139,8 +139,8 @@ If you have found a bug please write unit test, and make sure all other tests st
### Common:
+ transparent interface to APIs
+ validations
+ -before and -after hooks on save, update, destroy
+ scopes
+ default values
+ more relationships stuff
+ docs

View File

@ -309,45 +309,46 @@ AbstractClass.hasMany = function (anotherClass, params) {
// each instance of this class should have method named
// pluralize(anotherClass.modelName)
// which is actually just anotherClass.all({thisModelNameId: this.id}, cb);
this.prototype[methodName] = function (cond, cb) {
var actualCond;
if (arguments.length === 1) {
actualCond = {};
cb = cond;
} else if (arguments.length === 2) {
actualCond = cond;
} else {
throw new Error(anotherClass.modelName + ' only can be called with one or two arguments');
}
actualCond[fk] = this.id;
return anotherClass.all(actualCond, cb);
};
defineScope(this.prototype, anotherClass, methodName, function () {
var x = {};
x[fk] = this.id;
return x;
}, {
find: find,
destroy: destroy
});
// obviously, anotherClass should have attribute called `fk`
anotherClass.schema.defineForeignKey(anotherClass.modelName, fk);
// and it should have create/build methods with binded thisModelNameId param
this.prototype['build' + anotherClass.modelName] = function (data) {
data = data || {};
data[fk] = this.id; // trick! this.fk defined at runtime (when got it)
// but we haven't instance here to schedule this action
return new anotherClass(data);
};
function find(id, cb) {
anotherClass.find(id, function (err, inst) {
if (err) return cb(err);
if (inst[fk] === this.id) {
cb(null, inst);
} else {
cb(new Error('Permission denied'));
}
}.bind(this));
}
this.prototype['create' + anotherClass.modelName] = function (data, cb) {
if (typeof data === 'function') {
cb = data;
data = {};
}
this['build' + anotherClass.modelName](data).save(cb);
};
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'));
}
});
}
};
AbstractClass.belongsTo = function (anotherClass, params) {
var methodName = params.as;
var fk = params.foreignKey;
// anotherClass.schema.defineForeignKey(anotherClass.modelName, fk);
this.schema.defineForeignKey(anotherClass.modelName, fk);
this.prototype[methodName] = function (p, cb) {
if (p instanceof AbstractClass) { // acts as setter
this[fk] = p.id;
@ -360,9 +361,88 @@ AbstractClass.belongsTo = function (anotherClass, params) {
} else if (!p) { // acts as sync getter
return this.cachedRelations[methodName] || this[fk];
}
}
};
};
AbstractClass.scope = function (name, params) {
defineScope(this, this, name, params);
};
function defineScope(class, targetClass, name, params, methods) {
// collect meta info about scope
if (!class._scopeMeta) {
class._scopeMeta = {};
}
// anly make sence to add scope in meta if base and target classes
// are same
if (class === targetClass) {
class._scopeMeta[name] = params;
} else {
if (!targetClass._scopeMeta) {
targetClass._scopeMeta = {};
}
}
Object.defineProperty(class, name, {
enumerable: false,
configurable: true,
get: function () {
var f = function caller(cond, cb) {
var actualCond;
if (arguments.length === 1) {
actualCond = {};
cb = cond;
} else if (arguments.length === 2) {
actualCond = cond;
} else {
throw new Error('Method only can be called with one or two arguments');
}
return targetClass.all(merge(actualCond, caller._scope), cb);
};
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;
}
// define sub-scopes
Object.keys(targetClass._scopeMeta).forEach(function (name) {
Object.defineProperty(f, name, {
enumerable: false,
get: function () {
merge(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) {
data = data || {};
return new targetClass(merge(this._scope, data));
}
function create(data, cb) {
if (typeof data === 'function') {
cb = data;
data = {};
}
this.build(data).save(cb);
}
function destroyAll(id, cb) {
// implement me
}
}
// helper methods
//
function isdef(s) {
@ -371,9 +451,12 @@ function isdef(s) {
}
function merge(base, update) {
Object.keys(update).forEach(function (key) {
base[key] = update[key];
});
base = base || {};
if (update) {
Object.keys(update).forEach(function (key) {
base[key] = update[key];
});
}
return base;
}

View File

@ -62,9 +62,7 @@ function Text() {
Schema.Text = Text;
Schema.prototype.automigrate = function (cb) {
if (this.adapter.freezeSchema) {
this.adapter.freezeSchema();
}
this.freeze();
if (this.adapter.automigrate) {
this.adapter.automigrate(cb);
} else {
@ -72,6 +70,12 @@ Schema.prototype.automigrate = function (cb) {
}
};
Schema.prototype.freeze = function freeze() {
if (this.adapter.freezeSchema) {
this.adapter.freezeSchema();
}
}
/**
* Define class
* @param className

View File

@ -50,8 +50,9 @@ function testOrm(schema) {
User.hasMany(Post, {as: 'posts', foreignKey: 'userId'});
// creates instance methods:
// user.posts(conds)
// user.buildPost(data) // like new Post({userId: user.id});
// user.createPost(data) // build and save
// user.posts.build(data) // like new Post({userId: user.id});
// user.posts.create(data) // build and save
// user.posts.find
Post.belongsTo(User, {as: 'author', foreignKey: 'userId'});
// creates instance methods:
@ -280,9 +281,9 @@ function testOrm(schema) {
User.create(function (err, u) {
if (err) return console.log(err);
test.ok(u.posts, 'Method defined: posts');
test.ok(u.buildPost, 'Method defined: buildPost');
test.ok(u.createPost, 'Method defined: createPost');
u.createPost(function (err, post) {
test.ok(u.posts.build, 'Method defined: posts.build');
test.ok(u.posts.create, 'Method defined: posts.create');
u.posts.create(function (err, post) {
if (err) return console.log(err);
test.ok(post.author(), u.id);
u.posts(function (err, posts) {
@ -293,6 +294,36 @@ function testOrm(schema) {
});
});
it('should support scopes', function (test) {
var wait = 2;
test.ok(Post.scope, 'Scope supported');
Post.scope('published', {published: true});
test.ok(typeof Post.published === 'function');
test.ok(Post.published._scope.published = true);
var post = Post.published.build();
test.ok(post.published, 'Can build');
test.ok(post.isNewRecord());
Post.published.create(function (err, psto) {
if (err) return console.log(err);
test.ok(psto.published);
test.ok(!psto.isNewRecord());
done();
});
User.create(function (err, u) {
if (err) return console.log(err);
test.ok(typeof u.posts.published == 'function');
test.ok(u.posts.published._scope.published);
test.equal(u.posts.published._scope.userId, u.id);
done();
});
function done() {
if (--wait === 0) test.done();
};
});
it('should destroy all records', function (test) {
Post.destroyAll(function (err) {
Post.all(function (err, posts) {