Permalink
Browse files

Scopes

  • Loading branch information...
1 parent 14398c2 commit 8e05e59933250d9b9a6f88462e0fbc641f2c5f9b @1602 committed Oct 15, 2011
Showing with 164 additions and 46 deletions.
  1. +6 −6 README.md
  2. +115 −32 lib/abstract-class.js
  3. +7 −3 lib/schema.js
  4. +36 −5 test/common_test.js
View
@@ -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
@@ -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
@@ -62,16 +62,20 @@ 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 {
cb && cb();
}
};
+Schema.prototype.freeze = function freeze() {
+ if (this.adapter.freezeSchema) {
+ this.adapter.freezeSchema();
+ }
+}
+
/**
* Define class
* @param className
View
@@ -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) {

0 comments on commit 8e05e59

Please sign in to comment.