diff --git a/README.md b/README.md index 4063b7cc..d946ecf4 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/lib/abstract-class.js b/lib/abstract-class.js index cf0abc08..314c4385 100644 --- a/lib/abstract-class.js +++ b/lib/abstract-class.js @@ -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; } diff --git a/lib/schema.js b/lib/schema.js index f3f5cf59..0f534ed2 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -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 diff --git a/test/common_test.js b/test/common_test.js index 030b650f..6c046843 100644 --- a/test/common_test.js +++ b/test/common_test.js @@ -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) {