From 29eb3434c764903562e68f135826560649361700 Mon Sep 17 00:00:00 2001 From: Partap Davis Date: Tue, 17 Feb 2015 22:04:31 -0700 Subject: [PATCH] Promisify model relation methods When a callback is omitted from a method on a model relation that supports promises, return that promise. This includes all the standard DAO methods, as well as any user-defined methods that return promises. e.g.: mylist.todos.create({name: 'Item 1'}) // returns Promise This API will use native ES6 promises if available. If not available, or to force the use of another Promise library, you must assign the global.Promise object. e.g.: global.Promise = require('bluebird') Relations affected: - BelongsTo - HasOne - HasMany - HasManyThrough - HasAndBelongsToMany - ReferencesMany - EmbedsOne Exceptions: The EmbedsMany relation has not been promisified, because most of the methods return synchronous values. The base relation getter method [e.g.: mylist.todos()] has not been promisified, due to its default caching behavior. New Methods: - getAsync(condition, cb) A new method "getAsync()" has been added to all relations except EmbedsMany, which always fetches from the datasource rather than from the cache. It takes an optional "where" condition (except for HasOne and BelongsTo) and an optional callback. If the callback is omitted, a Promise is returned. --- lib/relation-definition.js | 118 +++- lib/scope.js | 48 +- test/relations.test.js | 1365 +++++++++++++++++++++++++++++++++++- 3 files changed, 1511 insertions(+), 20 deletions(-) diff --git a/lib/relation-definition.js b/lib/relation-definition.js index aba111cd..c6596d55 100644 --- a/lib/relation-definition.js +++ b/lib/relation-definition.js @@ -758,9 +758,12 @@ HasMany.prototype.findById = function (fkId, cb) { filter.where[idName] = fkId; filter.where[fk] = modelInstance[pk]; + cb = cb || utils.createPromiseCallback(); + if (filter.where[fk] === undefined) { // Foreign key is undefined - return process.nextTick(cb); + process.nextTick(cb); + return cb.promise; } this.definition.applyScope(modelInstance, filter); @@ -784,6 +787,7 @@ HasMany.prototype.findById = function (fkId, cb) { cb(err); } }); + return cb.promise; }; /** @@ -795,6 +799,8 @@ HasMany.prototype.exists = function (fkId, cb) { var fk = this.definition.keyTo; var pk = this.definition.keyFrom; var modelInstance = this.modelInstance; + cb = cb || utils.createPromiseCallback(); + this.findById(fkId, function (err, inst) { if (err) { @@ -810,6 +816,7 @@ HasMany.prototype.exists = function (fkId, cb) { cb(null, false); } }); + return cb.promise; }; /** @@ -818,12 +825,14 @@ HasMany.prototype.exists = function (fkId, cb) { * @param {Function} cb The callback function */ HasMany.prototype.updateById = function (fkId, data, cb) { + cb = cb || utils.createPromiseCallback(); this.findById(fkId, function (err, inst) { if (err) { return cb && cb(err); } inst.updateAttributes(data, cb); }); + return cb.promise; }; /** @@ -832,6 +841,7 @@ HasMany.prototype.updateById = function (fkId, data, cb) { * @param {Function} cb The callback function */ HasMany.prototype.destroyById = function (fkId, cb) { + cb = cb || utils.createPromiseCallback(); var self = this; this.findById(fkId, function(err, inst) { if (err) { @@ -840,6 +850,7 @@ HasMany.prototype.destroyById = function (fkId, cb) { self.removeFromCache(inst[fkId]); inst.destroy(cb); }); + return cb.promise; }; var throughKeys = function(definition) { @@ -875,6 +886,8 @@ HasManyThrough.prototype.findById = function (fkId, cb) { var modelInstance = this.modelInstance; var modelThrough = this.definition.modelThrough; + cb = cb || utils.createPromiseCallback(); + self.exists(fkId, function (err, exists) { if (err || !exists) { if (!err) { @@ -897,6 +910,7 @@ HasManyThrough.prototype.findById = function (fkId, cb) { cb(err, inst); }); }); + return cb.promise; }; /** @@ -911,6 +925,8 @@ HasManyThrough.prototype.destroyById = function (fkId, cb) { var modelInstance = this.modelInstance; var modelThrough = this.definition.modelThrough; + cb = cb || utils.createPromiseCallback(); + self.exists(fkId, function (err, exists) { if (err || !exists) { if (!err) { @@ -928,6 +944,7 @@ HasManyThrough.prototype.destroyById = function (fkId, cb) { modelTo.deleteById(fkId, cb); }); }); + return cb.promise; }; // Create an instance of the target model and connect it to the instance of @@ -942,7 +959,7 @@ HasManyThrough.prototype.create = function create(data, done) { done = data; data = {}; } - done = done || function(){}; + done = done || utils.createPromiseCallback(); var modelInstance = this.modelInstance; @@ -984,6 +1001,7 @@ HasManyThrough.prototype.create = function create(data, done) { else async.map(to, createRelation, done); }); + return done.promise; }; @@ -1005,6 +1023,9 @@ HasManyThrough.prototype.add = function (acInst, data, done) { } var query = {}; + data = data || {}; + done = done || utils.createPromiseCallback(); + // The primary key for the target model var pk2 = definition.modelTo.definition.idName(); @@ -1033,6 +1054,7 @@ HasManyThrough.prototype.add = function (acInst, data, done) { } done(err, ac); }); + return done.promise; }; /** @@ -1060,9 +1082,12 @@ HasManyThrough.prototype.exists = function (acInst, done) { definition.applyScope(this.modelInstance, filter); + done = done || utils.createPromiseCallback(); + modelThrough.count(filter.where, function(err, ac) { done(err, ac > 0); }); + return done.promise; }; /** @@ -1091,12 +1116,15 @@ HasManyThrough.prototype.remove = function (acInst, done) { definition.applyScope(this.modelInstance, filter); + done = done || utils.createPromiseCallback(); + modelThrough.deleteAll(filter.where, function (err) { if (!err) { self.removeFromCache(query[fk2]); } done(err); }); + return done.promise; }; @@ -1186,6 +1214,7 @@ RelationDefinition.belongsTo = function (modelFrom, modelTo, params) { get: function() { var relation = new BelongsTo(definition, this); var relationMethod = relation.related.bind(relation); + relationMethod.getAsync = relation.getAsync.bind(relation); relationMethod.update = relation.update.bind(relation); relationMethod.destroy = relation.destroy.bind(relation); if (!polymorphic) { @@ -1221,6 +1250,7 @@ BelongsTo.prototype.create = function(targetModelData, cb) { cb = targetModelData; targetModelData = {}; } + cb = cb || utils.createPromiseCallback(); this.definition.applyProperties(modelInstance, targetModelData || {}); @@ -1241,6 +1271,7 @@ BelongsTo.prototype.create = function(targetModelData, cb) { cb && cb(err); } }); + return cb.promise; }; BelongsTo.prototype.build = function(targetModelData) { @@ -1250,6 +1281,7 @@ BelongsTo.prototype.build = function(targetModelData) { }; BelongsTo.prototype.update = function (targetModelData, cb) { + cb = cb || utils.createPromiseCallback(); var definition = this.definition; this.fetch(function(err, inst) { if (inst instanceof ModelBaseClass) { @@ -1259,12 +1291,16 @@ BelongsTo.prototype.update = function (targetModelData, cb) { + ' is empty')); } }); + return cb.promise; }; BelongsTo.prototype.destroy = function (cb) { var modelTo = this.definition.modelTo; var modelInstance = this.modelInstance; var fk = this.definition.keyFrom; + + cb = cb || utils.createPromiseCallback(); + this.fetch(function(err, targetModel) { if (targetModel instanceof ModelBaseClass) { modelInstance[fk] = null; @@ -1277,6 +1313,7 @@ BelongsTo.prototype.destroy = function (cb) { + ' is empty')); } }); + return cb.promise; }; /** @@ -1394,6 +1431,21 @@ BelongsTo.prototype.related = function (refresh, params) { } }; + +/** + * Define a Promise-based method for the belongsTo relation itself + * - order.customer.get(cb): Load the target model instance asynchronously + * + * @param {Function} cb Callback of the form function (err, inst) + * @returns {Promise | Undefined} returns promise if callback is omitted + */ +BelongsTo.prototype.getAsync = function (cb) { + cb = cb || utils.createPromiseCallback(); + this.related(true, cb); + return cb.promise; +} + + /** * A hasAndBelongsToMany relation creates a direct many-to-many connection with * another model, with no intervening model. For example, if your application @@ -1504,6 +1556,7 @@ RelationDefinition.hasOne = function (modelFrom, modelTo, params) { get: function() { var relation = new HasOne(definition, this); var relationMethod = relation.related.bind(relation) + relationMethod.getAsync = relation.getAsync.bind(relation); relationMethod.create = relation.create.bind(relation); relationMethod.build = relation.build.bind(relation); relationMethod.update = relation.update.bind(relation); @@ -1559,6 +1612,8 @@ HasOne.prototype.create = function (targetModelData, cb) { targetModelData = {}; } targetModelData = targetModelData || {}; + cb = cb || utils.createPromiseCallback(); + targetModelData[fk] = modelInstance[pk]; var query = {where: {}}; query.where[fk] = targetModelData[fk]; @@ -1579,9 +1634,11 @@ HasOne.prototype.create = function (targetModelData, cb) { + modelTo.modelName)); } }); + return cb.promise; }; HasOne.prototype.update = function(targetModelData, cb) { + cb = cb || utils.createPromiseCallback(); var definition = this.definition; var fk = this.definition.keyTo; this.fetch(function(err, targetModel) { @@ -1593,9 +1650,11 @@ HasOne.prototype.update = function(targetModelData, cb) { + ' is empty')); } }); + return cb.promise; }; HasOne.prototype.destroy = function (cb) { + cb = cb || utils.createPromiseCallback(); this.fetch(function(err, targetModel) { if (targetModel instanceof ModelBaseClass) { targetModel.destroy(cb); @@ -1604,6 +1663,7 @@ HasOne.prototype.destroy = function (cb) { + ' is empty')); } }); + return cb.promise; }; /** @@ -1625,6 +1685,7 @@ HasMany.prototype.create = function (targetModelData, cb) { targetModelData = {}; } targetModelData = targetModelData || {}; + cb = cb || utils.createPromiseCallback(); var fkAndProps = function(item) { item[fk] = modelInstance[pk]; @@ -1650,6 +1711,7 @@ HasMany.prototype.create = function (targetModelData, cb) { cb && cb(err); } }); + return cb.promise; }; /** * Build a target model instance @@ -1741,6 +1803,20 @@ HasOne.prototype.related = function (refresh, params) { } }; +/** + * Define a Promise-based method for the hasOne relation itself + * - order.customer.getAsync(cb): Load the target model instance asynchronously + * + * @param {Function} cb Callback of the form function (err, inst) + * @returns {Promise | Undefined} Returns promise if cb is omitted + */ + +HasOne.prototype.getAsync = function (cb) { + cb = cb || utils.createPromiseCallback(); + this.related(true, cb); + return cb.promise; +}; + RelationDefinition.embedsOne = function (modelFrom, modelTo, params) { params = params || {}; modelTo = lookupModelTo(modelFrom, modelTo, params); @@ -1896,6 +1972,7 @@ EmbedsOne.prototype.create = function (targetModelData, cb) { } targetModelData = targetModelData || {}; + cb = cb || utils.createPromiseCallback(); var inst = this.callScopeMethod('build', targetModelData); @@ -1928,6 +2005,7 @@ EmbedsOne.prototype.create = function (targetModelData, cb) { updateEmbedded(); } } + return cb.promise; }; EmbedsOne.prototype.build = function (targetModelData) { @@ -1970,25 +2048,29 @@ EmbedsOne.prototype.update = function (targetModelData, cb) { var embeddedInstance = modelInstance[propertyName]; if (embeddedInstance instanceof modelTo) { embeddedInstance.setAttributes(data); + cb = cb || utils.createPromiseCallback(); if (typeof cb === 'function') { modelInstance.save(function(err, inst) { cb(err, inst ? inst[propertyName] : embeddedInstance); }); } } else if (!embeddedInstance && cb) { - this.callScopeMethod('create', data, cb); + return this.callScopeMethod('create', data, cb); } else if (!embeddedInstance) { - this.callScopeMethod('build', data); + return this.callScopeMethod('build', data); } + return cb.promise; }; EmbedsOne.prototype.destroy = function (cb) { var modelInstance = this.modelInstance; var propertyName = this.definition.keyFrom; modelInstance.unsetAttribute(propertyName, true); + cb = cb || utils.createPromiseCallback(); modelInstance.save(function (err, result) { cb && cb(err, result); }); + return cb.promise; }; RelationDefinition.embedsMany = function embedsMany(modelFrom, modelTo, params) { @@ -2386,6 +2468,7 @@ EmbedsMany.prototype.create = function (targetModelData, cb) { targetModelData = {}; } targetModelData = targetModelData || {}; + cb = cb || utils.createPromiseCallback(); var embeddedList = this.embeddedList(); @@ -2420,6 +2503,7 @@ EmbedsMany.prototype.create = function (targetModelData, cb) { updateEmbedded(); } } + return cb.promise; }; EmbedsMany.prototype.build = function(targetModelData) { @@ -2478,6 +2562,7 @@ EmbedsMany.prototype.add = function (acInst, data, cb) { cb = data; data = {}; } + cb = cb || utils.createPromiseCallback(); var self = this; var definition = this.definition; @@ -2513,6 +2598,7 @@ EmbedsMany.prototype.add = function (acInst, data, cb) { cb(null, null); } }); + return cb.promise; }; /** @@ -2543,6 +2629,8 @@ EmbedsMany.prototype.remove = function (acInst, cb) { belongsTo.applyScope(modelInstance, filter); + cb = cb || utils.createPromiseCallback(); + modelInstance[definition.name](filter, function(err, items) { if (err) return cb(err); @@ -2554,6 +2642,7 @@ EmbedsMany.prototype.remove = function (acInst, cb) { cb(err); }); }); + return cb.promise; }; RelationDefinition.referencesMany = function referencesMany(modelFrom, modelTo, params) { @@ -2654,6 +2743,7 @@ ReferencesMany.prototype.related = function(receiver, scopeParams, condOrRefresh var actualCond = {}; var actualRefresh = false; + if (arguments.length === 3) { cb = condOrRefresh; } else if (arguments.length === 4) { @@ -2696,6 +2786,8 @@ ReferencesMany.prototype.findById = function (fkId, cb) { this.definition.applyScope(modelInstance, filter); + cb = cb || utils.createPromiseCallback(); + modelTo.findByIds(ids, filter, function (err, instances) { if (err) { return cb(err); @@ -2722,6 +2814,7 @@ ReferencesMany.prototype.findById = function (fkId, cb) { cb(err); } }); + return cb.promise; }; ReferencesMany.prototype.exists = function (fkId, cb) { @@ -2729,7 +2822,10 @@ ReferencesMany.prototype.exists = function (fkId, cb) { var ids = this.modelInstance[fk] || []; var currentIds = ids.map(function(id) { return id.toString(); }); var fkId = (fkId || '').toString(); // mongodb + + cb = cb || utils.createPromiseCallback(); process.nextTick(function() { cb(null, currentIds.indexOf(fkId) > -1) }); + return cb.promise; }; ReferencesMany.prototype.updateById = function (fkId, data, cb) { @@ -2737,27 +2833,33 @@ ReferencesMany.prototype.updateById = function (fkId, data, cb) { cb = data; data = {}; } + cb = cb || utils.createPromiseCallback(); this.findById(fkId, function(err, inst) { if (err) return cb(err); inst.updateAttributes(data, cb); }); + return cb.promise; }; ReferencesMany.prototype.destroyById = function (fkId, cb) { var self = this; + cb = cb || utils.createPromiseCallback(); this.findById(fkId, function(err, inst) { if (err) return cb(err); self.remove(inst, function(err, ids) { inst.destroy(cb); }); }); + return cb.promise; }; ReferencesMany.prototype.at = function (index, cb) { var fk = this.definition.keyFrom; var ids = this.modelInstance[fk] || []; + cb = cb || utils.createPromiseCallback(); this.findById(ids[index], cb); + return cb.promise; }; ReferencesMany.prototype.create = function (targetModelData, cb) { @@ -2774,6 +2876,7 @@ ReferencesMany.prototype.create = function (targetModelData, cb) { targetModelData = {}; } targetModelData = targetModelData || {}; + cb = cb || utils.createPromiseCallback(); var ids = modelInstance[fk] || []; @@ -2799,6 +2902,7 @@ ReferencesMany.prototype.create = function (targetModelData, cb) { cb(err, inst); }); }); + return cb.promise; }; ReferencesMany.prototype.build = function(targetModelData) { @@ -2843,6 +2947,8 @@ ReferencesMany.prototype.add = function (acInst, cb) { }); }; + cb = cb || utils.createPromiseCallback(); + if (acInst instanceof modelTo) { insert(acInst, cb); } else { @@ -2856,6 +2962,7 @@ ReferencesMany.prototype.add = function (acInst, cb) { insert(inst, cb); }); } + return cb.promise; }; /** @@ -2876,6 +2983,8 @@ ReferencesMany.prototype.remove = function (acInst, cb) { var id = (acInst instanceof definition.modelTo) ? acInst[pk] : acInst; id = id.toString(); + cb = cb || utils.createPromiseCallback(); + var index = currentIds.indexOf(id); if (index > -1) { ids.splice(index, 1); @@ -2885,4 +2994,5 @@ ReferencesMany.prototype.remove = function (acInst, cb) { } else { process.nextTick(function() { cb(null, ids); }); } + return cb.promise; }; diff --git a/lib/scope.js b/lib/scope.js index d8666bcd..c2dbd93a 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -39,7 +39,7 @@ ScopeDefinition.prototype.targetModel = function(receiver) { ScopeDefinition.prototype.related = function(receiver, scopeParams, condOrRefresh, cb) { var name = this.name; var self = receiver; - + var actualCond = {}; var actualRefresh = false; var saveOnCache = true; @@ -56,7 +56,7 @@ ScopeDefinition.prototype.related = function(receiver, scopeParams, condOrRefres } 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 @@ -148,7 +148,7 @@ function defineScope(cls, targetClass, name, params, methods, options) { get: function () { var targetModel = definition.targetModel(this); var self = this; - + var f = function(condOrRefresh, cb) { if (arguments.length === 0) { if (typeof f.value === 'function') { @@ -176,16 +176,35 @@ function defineScope(cls, targetClass, name, params, methods, options) { } } }; - + f._receiver = this; f._scope = typeof definition.params === 'function' ? definition.params.call(self) : definition.params; - + f._targetClass = targetModel.modelName; if (f._scope.collect) { f._targetClass = i8n.camelize(f._scope.collect); } - + + f.getAsync = function (cond, cb) { + if (cb === undefined) { + if (cond === undefined) { + // getAsync() + cb = utils.createPromiseCallback(); + cond = true; + } else if (typeof cond !== 'function') { + // getAsync({where:{}}) + cb = utils.createPromiseCallback(); + } else { + // getAsync(function(){}) + cb = cond; + cond = true; + } + } + definition.related(self, f._scope, cond, cb); + return cb.promise; + } + f.build = build; f.create = create; f.updateAll = updateAll; @@ -261,7 +280,7 @@ function defineScope(cls, targetClass, name, params, methods, options) { }; cls['__findOne__' + name] = fn_findOne; - + var fn_count = function (cb) { var f = this[name].count; f.apply(this[name], arguments); @@ -284,9 +303,10 @@ function defineScope(cls, targetClass, name, params, methods, options) { cb = data; data = {}; } - this.build(data).save(cb); + cb = cb || utils.createPromiseCallback(); + return this.build(data).save(cb); } - + /* Callback - The callback will be called after all elements are destroyed @@ -295,10 +315,12 @@ function defineScope(cls, targetClass, name, params, methods, options) { */ function destroyAll(where, cb) { if (typeof where === 'function') cb = where, where = {}; + cb = cb || utils.createPromiseCallback(); + var targetModel = definition.targetModel(this._receiver); var scoped = (this._scope && this._scope.where) || {}; var filter = mergeQuery({ where: scoped }, { where: where || {} }); - targetModel.destroyAll(filter.where, cb); + return targetModel.destroyAll(filter.where, cb); } function updateAll(where, data, cb) { @@ -332,11 +354,13 @@ function defineScope(cls, targetClass, name, params, methods, options) { function count(where, cb) { if (typeof where === 'function') cb = where, where = {}; + cb = cb || utils.createPromiseCallback(); + var targetModel = definition.targetModel(this._receiver); var scoped = (this._scope && this._scope.where) || {}; var filter = mergeQuery({ where: scoped }, { where: where || {} }); - targetModel.count(filter.where, cb); - } + return targetModel.count(filter.where, cb); + } return definition; } diff --git a/test/relations.test.js b/test/relations.test.js index 3dd7acc4..d1038838 100644 --- a/test/relations.test.js +++ b/test/relations.test.js @@ -2,6 +2,7 @@ var should = require('./init.js'); var jdb = require('../'); var DataSource = jdb.DataSource; +var createPromiseCallback = require('../lib/utils.js').createPromiseCallback; var db, tmp, Book, Chapter, Author, Reader; var Category, Job; @@ -90,6 +91,18 @@ describe('relations', function () { }); }); + it('should create record on scope with promises', function (done) { + Book.create() + .then (function (book) { + return book.chapters.create() + .then (function (c) { + should.exist(c); + c.bookId.should.equal(book.id); + done(); + }); + }).catch(done); + }); + it('should create a batch of records on scope', function (done) { var chapters = [ {name: 'a'}, @@ -109,6 +122,25 @@ describe('relations', function () { }); }); + it('should create a batch of records on scope with promises', function (done) { + var chapters = [ + {name: 'a'}, + {name: 'z'}, + {name: 'c'} + ]; + Book.create(function (err, book) { + book.chapters.create(chapters) + .then(function (chs) { + should.exist(chs); + chs.should.have.lengthOf(chapters.length); + chs.forEach(function(c) { + c.bookId.should.equal(book.id); + }); + done(); + }).catch(done); + }); + }); + it('should fetch all scoped instances', function (done) { Book.create(function (err, book) { book.chapters.create({name: 'a'}, function () { @@ -140,6 +172,104 @@ describe('relations', function () { } }); + it('should fetch all scoped instances with promises', function (done) { + Book.create() + .then(function (book) { + return book.chapters.create({name: 'a'}) + .then(function () { + return book.chapters.create({name: 'z'}) + }) + .then(function () { + return book.chapters.create({name: 'c'}) + }) + .then(function () { + return verify(book); + }); + }).catch(done); + + function verify(book) { + return book.chapters.getAsync() + .then(function (ch) { + should.exist(ch); + ch.should.have.lengthOf(3); + + var chapters = book.chapters(); + chapters.should.eql(ch); + + return book.chapters.getAsync({order: 'name DESC'}) + .then(function (c) { + should.exist(c); + + c.shift().name.should.equal('z'); + c.pop().name.should.equal('a'); + done(); + }); + }); + } + }); + + it('should fetch all scoped instances with getAsync with callback and condition', function (done) { + Book.create(function (err, book) { + book.chapters.create({name: 'a'}, function () { + book.chapters.create({name: 'z'}, function () { + book.chapters.create({name: 'c'}, function () { + verify(book); + }); + }); + }); + }); + function verify(book) { + book.chapters(function (err, ch) { + should.not.exist(err); + should.exist(ch); + ch.should.have.lengthOf(3); + + var chapters = book.chapters(); + chapters.should.eql(ch); + + book.chapters.getAsync({order: 'name DESC'}, function (e, c) { + should.not.exist(e); + should.exist(c); + + c.shift().name.should.equal('z'); + c.pop().name.should.equal('a'); + done(); + }); + }); + } + }); + + it('should fetch all scoped instances with getAsync with callback and no condition', function (done) { + Book.create(function (err, book) { + book.chapters.create({name: 'a'}, function () { + book.chapters.create({name: 'z'}, function () { + book.chapters.create({name: 'c'}, function () { + verify(book); + }); + }); + }); + }); + function verify(book) { + book.chapters(function (err, ch) { + should.not.exist(err); + should.exist(ch); + ch.should.have.lengthOf(3); + + var chapters = book.chapters(); + chapters.should.eql(ch); + + book.chapters.getAsync(function (e, c) { + should.not.exist(e); + should.exist(c); + should.exist(c.length); + c.shift().name.should.equal('a'); + c.pop().name.should.equal('c'); + done(); + }); + }); + } + }); + it('should find scoped record', function (done) { var id; Book.create(function (err, book) { @@ -163,6 +293,33 @@ describe('relations', function () { } }); + it('should find scoped record with promises', function (done) { + var id; + Book.create() + .then(function (book) { + return book.chapters.create({name: 'a'}) + .then(function (ch) { + id = ch.id; + return book.chapters.create({name: 'z'}) + }) + .then(function () { + return book.chapters.create({name: 'c'}) + }) + .then(function () { + return verify(book); + }) + }).catch(done); + + function verify(book) { + return book.chapters.findById(id) + .then(function (ch) { + should.exist(ch); + ch.id.should.eql(id); + done(); + }); + } + }); + it('should count scoped records - all and filtered', function (done) { Book.create(function (err, book) { book.chapters.create({name: 'a'}, function (err, ch) { @@ -187,6 +344,34 @@ describe('relations', function () { } }); + it('should count scoped records - all and filtered with promises', function (done) { + Book.create() + .then(function (book) { + book.chapters.create({name: 'a'}) + .then(function () { + return book.chapters.create({name: 'b'}) + }) + .then(function () { + return book.chapters.create({name: 'c'}) + }) + .then(function () { + return verify(book); + }); + }).catch(done); + + function verify(book) { + return book.chapters.count() + .then(function (count) { + count.should.equal(3); + return book.chapters.count({ name: 'b' }) + }) + .then(function (count) { + count.should.equal(1); + done(); + }); + } + }); + it('should set targetClass on scope property', function() { should.equal(Book.prototype.chapters._targetClass, 'Chapter'); }); @@ -213,6 +398,32 @@ describe('relations', function () { } }); + it('should update scoped record with promises', function (done) { + var id; + Book.create() + .then(function (book) { + return book.chapters.create({name: 'a'}) + .then(function (ch) { + id = ch.id; + return book.chapters.updateById(id, {name: 'aa'}) + }) + .then(function(ch) { + return verify(book); + }); + }) + .catch(done); + + function verify(book) { + return book.chapters.findById(id) + .then(function (ch) { + should.exist(ch); + ch.id.should.eql(id); + ch.name.should.equal('aa'); + done(); + }); + } + }); + it('should destroy scoped record', function (done) { var id; Book.create(function (err, book) { @@ -232,6 +443,30 @@ describe('relations', function () { } }); + it('should destroy scoped record with promises', function (done) { + var id; + Book.create() + .then(function (book) { + return book.chapters.create({name: 'a'}) + .then(function (ch) { + id = ch.id; + return book.chapters.destroy(id) + }) + .then(function(ch) { + return verify(book); + }); + }) + .catch(done); + + function verify(book) { + return book.chapters.findById(id) + .catch(function (err) { + should.exist(err); + done(); + }); + } + }); + it('should check existence of a scoped record', function (done) { var id; Book.create(function (err, book) { @@ -254,6 +489,32 @@ describe('relations', function () { } }); + it('should check existence of a scoped record with promises', function (done) { + var id; + Book.create() + .then(function (book) { + return book.chapters.create({name: 'a'}) + .then(function (ch) { + id = ch.id; + return book.chapters.create({name: 'z'}) + }) + .then(function () { + return book.chapters.create({name: 'c'}) + }) + .then(function () { + return verify(book); + }); + }).catch(done); + + function verify(book) { + return book.chapters.exists(id) + .then(function (flag) { + flag.should.be.eql(true); + done(); + }); + } + }); + it('should check ignore related data on creation - array', function (done) { Book.create({ chapters: [] }, function (err, book) { should.not.exist(err); @@ -264,6 +525,16 @@ describe('relations', function () { }); }); + it('should check ignore related data on creation with promises - array', function (done) { + Book.create({ chapters: [] }) + .then(function (book) { + book.chapters.should.be.a.function; + var obj = book.toObject(); + should.not.exist(obj.chapters); + done(); + }).catch(done); + }); + it('should check ignore related data on creation - object', function (done) { Book.create({ chapters: {} }, function (err, book) { should.not.exist(err); @@ -273,6 +544,16 @@ describe('relations', function () { done(); }); }); + + it('should check ignore related data on creation with promises - object', function (done) { + Book.create({ chapters: {} }) + .then(function (book) { + book.chapters.should.be.a.function; + var obj = book.toObject(); + should.not.exist(obj.chapters); + done(); + }).catch(done); + }); }); }); @@ -324,6 +605,21 @@ describe('relations', function () { }); }); + it('should create record on scope with promises', function (done) { + Physician.create() + .then(function (physician) { + return physician.patients.create() + .then(function (patient) { + should.exist(patient); + return Appointment.find({where: {physicianId: physician.id, patientId: patient.id}}) + .then(function (apps) { + apps.should.have.lengthOf(1); + done(); + }); + }); + }).catch(done); + }); + it('should create multiple records on scope', function (done) { var async = require('async'); Physician.create(function (err, physician) { @@ -347,6 +643,29 @@ describe('relations', function () { }); }); + it('should create multiple records on scope with promises', function (done) { + var async = require('async'); + Physician.create() + .then(function (physician) { + return physician.patients.create([{}, {}]) + .then(function (patients) { + should.exist(patients); + patients.should.have.lengthOf(2); + function verifyPatient(patient, next) { + Appointment.find({where: { + physicianId: physician.id, + patientId: patient.id + }}) + .then(function(apps) { + apps.should.have.lengthOf(1); + next(); + }); + } + async.forEach(patients, verifyPatient, done); + }); + }).catch(done); + }); + it('should fetch all scoped instances', function (done) { Physician.create(function (err, physician) { physician.patients.create({name: 'a'}, function () { @@ -371,6 +690,34 @@ describe('relations', function () { } }); + it('should fetch all scoped instances with promises', function (done) { + Physician.create() + .then(function (physician) { + return physician.patients.create({name: 'a'}) + .then(function () { + return physician.patients.create({name: 'z'}) + }) + .then(function () { + return physician.patients.create({name: 'c'}) + }) + .then(function () { + return verify(physician); + }) + }).catch(done); + function verify(physician) { + return physician.patients.getAsync() + .then(function (ch) { + + var patients = physician.patients(); + patients.should.eql(ch); + + should.exist(ch); + ch.should.have.lengthOf(3); + done(); + }); + } + }); + it('should find scoped record', function (done) { var id; Physician.create(function (err, physician) { @@ -394,6 +741,33 @@ describe('relations', function () { } }); + it('should find scoped record with promises', function (done) { + var id; + Physician.create() + .then(function (physician) { + return physician.patients.create({name: 'a'}) + .then(function (ch) { + id = ch.id; + return physician.patients.create({name: 'z'}) + }) + .then(function () { + return physician.patients.create({name: 'c'}) + }) + .then(function () { + return verify(physician); + }); + }).catch(done); + + function verify(physician) { + return physician.patients.findById(id, function (err, ch) { + should.not.exist(err); + should.exist(ch); + ch.id.should.eql(id); + done(); + }); + } + }); + it('should allow to use include syntax on related data', function (done) { Physician.create(function (err, physician) { physician.patients.create({name: 'a'}, function (err, patient) { @@ -421,6 +795,37 @@ describe('relations', function () { } }); + it('should allow to use include syntax on related data with promises', function (done) { + Physician.create() + .then(function (physician) { + return physician.patients.create({name: 'a'}) + .then(function (patient) { + return Address.create({name: 'z'}) + .then(function (address) { + patient.address(address); + return patient.save() + .then(function() { + return verify(physician, address.id); + }); + }); + }); + }).catch(done); + + function verify(physician, addressId) { + return physician.patients.getAsync({include: 'address'}) + .then(function (ch) { + should.exist(ch); + ch.should.have.lengthOf(1); + ch[0].addressId.should.eql(addressId); + var address = ch[0].address(); + should.exist(address); + address.should.be.an.instanceof(Address); + address.name.should.equal('z'); + done(); + }); + } + }); + it('should set targetClass on scope property', function() { should.equal(Physician.prototype.patients._targetClass, 'Patient'); }); @@ -447,6 +852,31 @@ describe('relations', function () { } }); + it('should update scoped record with promises', function (done) { + var id; + Physician.create() + .then(function (physician) { + return physician.patients.create({name: 'a'}) + .then(function (ch) { + id = ch.id; + return physician.patients.updateById(id, {name: 'aa'}) + .then(function(ch) { + return verify(physician); + }); + }); + }).catch(done); + + function verify(physician) { + return physician.patients.findById(id) + .then(function (ch) { + should.exist(ch); + ch.id.should.eql(id); + ch.name.should.equal('aa'); + done(); + }); + } + }); + it('should destroy scoped record', function (done) { var id; Physician.create(function (err, physician) { @@ -466,6 +896,33 @@ describe('relations', function () { } }); + it('should destroy scoped record with promises', function (done) { + var id; + Physician.create() + .then(function (physician) { + return physician.patients.create({name: 'a'}) + .then(function (ch) { + id = ch.id; + return physician.patients.destroy(id) + .then(function(ch) { + return verify(physician); + }); + }); + }).catch(done); + + function verify(physician) { + return physician.patients.findById(id) + .then(function (ch) { + should.not.exist(ch); + done(); + }) + .catch(function (err) { + should.exist(err); + done(); + }); + } + }); + it('should check existence of a scoped record', function (done) { var id; Physician.create(function (err, physician) { @@ -489,6 +946,32 @@ describe('relations', function () { } }); + it('should check existence of a scoped record with promises', function (done) { + var id; + Physician.create() + .then(function (physician) { + return physician.patients.create({name: 'a'}) + .then(function (ch) { + id = ch.id; + return physician.patients.create({name: 'z'}) + }) + .then(function () { + return physician.patients.create({name: 'c'}) + }) + .then(function () { + return verify(physician); + }); + }).catch(done); + + function verify(physician) { + return physician.patients.exists(id) + .then(function (flag) { + flag.should.be.eql(true); + done(); + }); + } + }); + it('should allow to add connection with instance', function (done) { Physician.create({name: 'ph1'}, function (e, physician) { Patient.create({name: 'pa1'}, function (e, patient) { @@ -504,6 +987,23 @@ describe('relations', function () { }); }); + it('should allow to add connection with instance with promises', function (done) { + Physician.create({name: 'ph1'}) + .then(function (physician) { + return Patient.create({name: 'pa1'}) + .then(function (patient) { + return physician.patients.add(patient) + .then(function (app) { + should.exist(app); + app.should.be.an.instanceOf(Appointment); + app.physicianId.should.equal(physician.id); + app.patientId.should.equal(patient.id); + done(); + }); + }); + }).catch(done); + }); + it('should allow to add connection with through data', function (done) { Physician.create({name: 'ph1'}, function (e, physician) { Patient.create({name: 'pa1'}, function (e, patient) { @@ -522,6 +1022,26 @@ describe('relations', function () { }); }); + it('should allow to add connection with through data with promises', function (done) { + Physician.create({name: 'ph1'}) + .then(function (physician) { + return Patient.create({name: 'pa1'}) + .then(function (patient) { + var now = Date.now(); + return physician.patients.add(patient, { date: new Date(now) }) + .then(function (app) { + should.exist(app); + app.should.be.an.instanceOf(Appointment); + app.physicianId.should.equal(physician.id); + app.patientId.should.equal(patient.id); + app.patientId.should.equal(patient.id); + app.date.getTime().should.equal(now); + done(); + }); + }); + }).catch(done); + }); + it('should allow to remove connection with instance', function (done) { var id; Physician.create(function (err, physician) { @@ -542,6 +1062,29 @@ describe('relations', function () { } }); + it('should allow to remove connection with instance with promises', function (done) { + var id; + Physician.create() + .then(function (physician) { + return physician.patients.create({name: 'a'}) + .then(function (patient) { + id = patient.id; + return physician.patients.remove(id) + .then(function (ch) { + return verify(physician); + }); + }); + }).catch(done); + + function verify(physician) { + return physician.patients.exists(id) + .then(function (flag) { + flag.should.be.eql(false); + done(); + }); + } + }); + beforeEach(function (done) { Appointment.destroyAll(function (err) { Physician.destroyAll(function (err) { @@ -733,6 +1276,19 @@ describe('relations', function () { }); }); }); + + it('should create record on scope with promises', function (done) { + Book.create({ type: 'fiction' }) + .then(function (book) { + return book.chapters.create() + .then(function (c) { + should.exist(c); + c.bookId.should.equal(book.id); + c.bookType.should.equal('fiction'); + done(); + }); + }).catch(done); + }); }); describe('hasMany with scope and properties', function () { @@ -771,6 +1327,23 @@ describe('relations', function () { }); }); + it('should create record on scope with promises', function (done) { + Category.create() + .then(function (c) { + return c.jobs.create({ type: 'book' }) + .then(function (p) { + p.categoryId.should.equal(c.id); + p.type.should.equal('book'); + return c.jobs.create({ type: 'widget' }) + .then(function (p) { + p.categoryId.should.equal(c.id); + p.type.should.equal('widget'); + done(); + }); + }); + }).catch(done); + }); + it('should find records on scope', function (done) { Category.findOne(function (err, c) { should.not.exists(err); @@ -782,6 +1355,18 @@ describe('relations', function () { }); }); + it('should find records on scope with promises', function (done) { + Category.findOne() + .then(function (c) { + return c.jobs.getAsync() + }) + .then(function(jobs) { + jobs.should.have.length(2); + done(); + }) + .catch(done); + }); + it('should find record on scope - filtered', function (done) { Category.findOne(function (err, c) { should.not.exists(err); @@ -794,6 +1379,19 @@ describe('relations', function () { }); }); + it('should find record on scope with promises - filtered', function (done) { + Category.findOne() + .then(function (c) { + return c.jobs.getAsync({ where: { type: 'book' } }) + }) + .then(function(jobs) { + jobs.should.have.length(1); + jobs[0].type.should.equal('book'); + done(); + }) + .catch(done); + }); + // So why not just do the above? In LoopBack, the context // that gets passed into a beforeRemote handler contains // a reference to the parent scope/instance: ctx.instance @@ -923,6 +1521,19 @@ describe('relations', function () { }); }); + it('should create polymorphic relation with promises - author', function (done) { + Author.create({name: 'Author 1' }) + .then(function (author) { + return author.avatar.create({ name: 'Avatar' }) + .then(function (p) { + should.exist(p); + p.imageableId.should.equal(author.id); + p.imageableType.should.equal('Author'); + done(); + }); + }).catch(done); + }); + it('should create polymorphic relation - reader', function (done) { Reader.create({name: 'Reader 1' }, function (err, reader) { should.not.exists(err); @@ -966,6 +1577,19 @@ describe('relations', function () { }); }); + it('should find polymorphic relation with promises - reader', function (done) { + Reader.findOne() + .then(function (reader) { + return reader.mugshot.getAsync() + .then(function (p) { + p.name.should.equal('Mugshot'); + p.imageableId.should.eql(reader.id); + p.imageableType.should.equal('Reader'); + done(); + }); + }).catch(done); + }); + it('should find inverse polymorphic relation - author', function (done) { Picture.findOne({ where: { name: 'Avatar' } }, function (err, p) { should.not.exists(err); @@ -1053,6 +1677,19 @@ describe('relations', function () { }); }); + it('should create polymorphic relation with promises - author', function (done) { + Author.create({name: 'Author 1' }) + .then(function (author) { + return author.avatar.create({ name: 'Avatar' }) + .then(function (p) { + should.exist(p); + p.oid.toString().should.equal(author.username.toString()); + p.type.should.equal('Author'); + done(); + }); + }).catch(done); + }); + it('should create polymorphic relation - reader', function (done) { Reader.create({name: 'Reader 1' }, function (err, reader) { should.not.exists(err); @@ -1620,7 +2257,7 @@ describe('relations', function () { Fear.belongsTo('mind', { methods: { check: function() { return true; } } }); - + Object.keys((new Fear).toObject()).should.containEql('mindId'); (new Fear).mind.should.be.an.instanceOf(Function); // (new Fear).mind.build().should.be.an.instanceOf(Mind); @@ -1663,6 +2300,52 @@ describe('relations', function () { }); }); + it('can be used to query data with getAsync with callback', function (done) { + List.hasMany('todos', {model: Item}); + db.automigrate(function () { + List.create({name: 'List 1'}, function (e, list) { + listId = list.id; + should.not.exist(e); + should.exist(list); + list.todos.create({name: 'Item 1'},function (err, todo) { + itemId = todo.id; + todo.list.getAsync(function (e, l) { + should.not.exist(e); + should.exist(l); + l.should.be.an.instanceOf(List); + todo.list().id.should.equal(l.id); + todo.list().name.should.equal('List 1'); + done(); + }); + }); + }); + }); + }); + + it('can be used to query data with promises', function (done) { + List.hasMany('todos', {model: Item}); + db.automigrate(function () { + List.create({name: 'List 1'}) + .then(function (list) { + listId = list.id; + should.exist(list); + return list.todos.create({name: 'Item 1'}) + }) + .then(function (todo) { + itemId = todo.id; + return todo.list.getAsync() + .then(function (l) { + should.exist(l); + l.should.be.an.instanceOf(List); + todo.list().id.should.equal(l.id); + todo.list().name.should.equal('List 1'); + done(); + }); + }) + .catch(done); + }); + }); + it('could accept objects when creating on scope', function (done) { List.create(function (e, list) { should.not.exist(e); @@ -1744,6 +2427,24 @@ describe('relations', function () { }); }); + it('should allow to create belongsTo model in beforeCreate hook with promises', function (done) { + var mind; + Fear.beforeCreate = function (next) { + this.mind.create() + .then(function (m) { + mind = m; + next(); + }).catch(next); + }; + Fear.create() + .then(function (fear) { + should.exists(fear); + fear.mindId.should.be.equal(mind.id); + should.exists(fear.mind()); + done(); + }).catch(done); + }); + }); describe('belongsTo with scope', function () { @@ -1786,6 +2487,36 @@ describe('relations', function () { }); }); + it('should create record on scope with promises', function (done) { + var p = new Passport({ name: 'Passport', notes: 'Some notes...' }); + p.person.create({name: 'Fred', age: 36 }) + .then(function (person) { + p.personId.should.equal(person.id); + person.name.should.equal('Fred'); + person.passportNotes.should.equal('Some notes...'); + return p.save() + }) + .then(function (passport) { + done(); + }) + .catch(done); + }); + + it('should find record on scope with promises', function (done) { + Passport.findOne() + .then(function (p) { + p.personId.should.eql(personCreated.id); + return p.person.getAsync() + }) + .then(function (person) { + person.name.should.equal('Fred'); + person.should.have.property('age', undefined); + person.should.have.property('passportNotes', undefined); + done(); + }) + .catch(done); + }); + }); // Disable the tests until the issue in @@ -1828,6 +2559,16 @@ describe('relations', function () { done(); }); }); + + it('should find record with embedded data with promises', function (done) { + Passport.findOne() + .then(function (p) { + var data = p.toObject(true); + data.person.id.should.equal(p.personId); + data.person.name.should.equal('Fred'); + done(); + }).catch(done); + }); }); describe('hasOne', function () { @@ -1841,7 +2582,7 @@ describe('relations', function () { }); it('can be declared using hasOne method', function () { - Supplier.hasOne(Account, { + Supplier.hasOne(Account, { properties: { name: 'supplierName' }, methods: { check: function() { return true; } } }); @@ -1885,6 +2626,50 @@ describe('relations', function () { }); }); + it('can be used to query data with getAsync with callback', function (done) { + db.automigrate(function () { + Supplier.create({name: 'Supplier 1'}, function (e, supplier) { + supplierId = supplier.id; + should.not.exist(e); + should.exist(supplier); + supplier.account.create({accountNo: 'a01'}, function (err, account) { + supplier.account.getAsync(function (e, act) { + accountId = act.id; + should.not.exist(e); + should.exist(act); + act.should.be.an.instanceOf(Account); + supplier.account().id.should.equal(act.id); + act.supplierName.should.equal(supplier.name); + done(); + }); + }); + }); + }); + }); + + it('can be used to query data with promises', function (done) { + db.automigrate(function () { + Supplier.create({name: 'Supplier 1'}) + .then(function (supplier) { + supplierId = supplier.id; + should.exist(supplier); + return supplier.account.create({accountNo: 'a01'}) + .then(function (account) { + return supplier.account.getAsync() + }) + .then(function (act) { + accountId = act.id; + should.exist(act); + act.should.be.an.instanceOf(Account); + supplier.account().id.should.equal(act.id); + act.supplierName.should.equal(supplier.name); + done(); + }) + }) + .catch(done); + }); + }); + it('should set targetClass on scope property', function() { should.equal(Supplier.prototype.account._targetClass, 'Account'); }); @@ -1901,6 +2686,19 @@ describe('relations', function () { }); }); + it('should update the related item on scope with promises', function(done) { + Supplier.findById(supplierId) + .then(function(supplier) { + should.exist(supplier); + return supplier.account.update({supplierName: 'Supplier B'}) + }) + .then(function(act) { + act.supplierName.should.equal('Supplier B'); + done(); + }) + .catch(done); + }); + it('should ignore the foreign key in the update', function(done) { Supplier.create({name: 'Supplier 2'}, function (e, supplier) { var sid = supplier.id; @@ -1932,6 +2730,20 @@ describe('relations', function () { }); }); + it('should get the related item on scope with promises', function(done) { + Supplier.findById(supplierId) + .then(function(supplier) { + should.exist(supplier); + return supplier.account.getAsync() + }) + .then(function (act) { + should.exist(act); + act.supplierName.should.equal('Supplier A'); + done(); + }) + .catch(done); + }); + it('should destroy the related item on scope', function(done) { Supplier.findById(supplierId, function(e, supplier) { should.not.exist(e); @@ -1943,6 +2755,21 @@ describe('relations', function () { }); }); + it('should destroy the related item on scope with promises', function(done) { + Supplier.findById(supplierId) + .then(function (supplier) { + should.exist(supplier); + return supplier.account.create({accountNo: 'a01'}) + .then(function (account) { + return supplier.account.destroy() + }) + .then(function (err) { + done(); + }); + }) + .catch(done); + }); + it('should get the related item on scope - verify', function(done) { Supplier.findById(supplierId, function(e, supplier) { should.not.exist(e); @@ -1955,6 +2782,19 @@ describe('relations', function () { }); }); + it('should get the related item on scope with promises - verify', function(done) { + Supplier.findById(supplierId) + .then(function(supplier) { + should.exist(supplier); + return supplier.account.getAsync() + }) + .then(function(act) { + should.not.exist(act); + done(); + }) + .catch(done); + }); + it('should have deleted related item', function(done) { Supplier.findById(supplierId, function (e, supplier) { should.not.exist(e); @@ -1983,12 +2823,14 @@ describe('relations', function () { supplierId = supplier.id; should.not.exist(e); should.exist(supplier); - supplier.account.create({accountNo: 'a01'}, function (err, account) { + supplier.account.create({accountNo: 'a01', block: false}, function (err, account) { supplier.account(function (e, act) { accountId = act.id; should.not.exist(e); should.exist(act); act.should.be.an.instanceOf(Account); + should.exist(act.block); + act.block.should.be.false; supplier.account().id.should.equal(act.id); act.supplierName.should.equal(supplier.name); done(); @@ -2009,6 +2851,46 @@ describe('relations', function () { }); }); + it('can be used to query data with promises', function (done) { + db.automigrate(function () { + Supplier.create({name: 'Supplier 1'}) + .then(function (supplier) { + supplierId = supplier.id; + should.exist(supplier); + return supplier.account.create({accountNo: 'a01', block: false}) + .then(function (account) { + return supplier.account.getAsync() + }) + .then(function (act) { + accountId = act.id; + should.exist(act); + act.should.be.an.instanceOf(Account); + should.exist(act.block); + act.block.should.be.false; + supplier.account().id.should.equal(act.id); + act.supplierName.should.equal(supplier.name); + done(); + }); + }) + .catch(done); + }); + }); + + it('should find record that match scope with promises', function (done) { + Account.updateAll({ block: true }) + .then(function () { + return Supplier.findById(supplierId) + }) + .then(function (supplier) { + return supplier.account.getAsync() + }) + .then(function (account) { + should.not.exist(account); + done(); + }) + .catch(done); + }); + }); describe('hasOne with non standard id', function () { @@ -2172,6 +3054,76 @@ describe('relations', function () { }); }); + it('should allow to create instances on scope with promises', function (done) { + db.automigrate(function () { + Article.create() + .then(function (article) { + return article.tagNames.create({name: 'popular'}) + .then(function (t) { + t.should.be.an.instanceOf(TagName); + return ArticleTag.findOne() + .then(function (at) { + should.exist(at); + at.tagNameId.toString().should.equal(t.id.toString()); + at.articleId.toString().should.equal(article.id.toString()); + done(); + }); + }); + }).catch(done); + }); + }); + + it('should allow to fetch scoped instances with promises', function (done) { + Article.findOne() + .then(function (article) { + return article.tagNames.getAsync() + .then(function (tags) { + should.exist(tags); + article.tagNames().should.eql(tags); + done(); + }); + }).catch(done); + }); + + it('should allow to add connection with instance with promises', function (done) { + Article.findOne() + .then(function (article) { + return TagName.create({name: 'awesome'}) + .then(function (tag) { + return article.tagNames.add(tag) + .then(function (at) { + should.exist(at); + at.should.be.an.instanceOf(ArticleTag); + at.tagNameId.should.equal(tag.id); + at.articleId.should.equal(article.id); + done(); + }); + }) + }) + .catch(done); + }); + + it('should allow to remove connection with instance with promises', function (done) { + Article.findOne() + .then(function (article) { + return article.tagNames.getAsync() + .then(function (tags) { + var len = tags.length; + tags.should.not.be.empty; + return article.tagNames.remove(tags[0]) + .then(function () { + return article.tagNames.getAsync() + }) + .then(function (tags) { + tags.should.have.lengthOf(len - 1); + done(); + }); + }); + }) + .catch(done); + }); + + it('should set targetClass on scope property', function() { should.equal(Article.prototype.tagNames._targetClass, 'TagName'); }); @@ -2418,6 +3370,107 @@ describe('relations', function () { }); }); + it('should create an embedded item on scope with promises', function(done) { + Person.create({name: 'Fred'}) + .then(function(p) { + personId = p.id; + p.passportItem.create({name: 'Fredric'}) + .then(function(passport) { + p.passport.toObject().should.eql({name: 'Fredric'}); + p.passport.should.be.an.instanceOf(Passport); + done(); + }); + }).catch(done); + }); + + it('should get an embedded item on scope with promises', function(done) { + Person.findById(personId) + .then(function(p) { + var passport = p.passportItem(); + passport.toObject().should.eql({name: 'Fredric'}); + passport.should.be.an.instanceOf(Passport); + passport.should.equal(p.passport); + passport.should.equal(p.passportItem.value()); + done(); + }).catch(done); + }); + + it('should validate an embedded item on scope with promises - on creation', function(done) { + var p = new Person({name: 'Fred'}); + p.passportItem.create({}) + .then(function(passport) { + should.not.exist(passport); + done(); + }) + .catch(function (err) { + should.exist(err); + err.name.should.equal('ValidationError'); + err.details.messages.name.should.eql(['can\'t be blank']); + done(); + }).catch(done); + }); + + it('should validate an embedded item on scope with promises - on update', function(done) { + Person.findById(personId) + .then(function(p) { + var passport = p.passportItem(); + passport.name = null; + return p.save() + .then(function (p) { + should.not.exist(p); + done(); + }) + .catch(function(err) { + should.exist(err); + err.name.should.equal('ValidationError'); + err.details.messages.passportItem + .should.eql(['is invalid: `name` can\'t be blank']); + done(); + }); + }).catch(done); + }); + + it('should update an embedded item on scope with promises', function(done) { + Person.findById(personId) + .then(function (p) { + return p.passportItem.update({name: 'Jason'}) + .then(function(passport) { + var passport = p.passportItem(); + passport.toObject().should.eql({name: 'Jason'}); + passport.should.be.an.instanceOf(Passport); + passport.should.equal(p.passport); + done(); + }); + }).catch(done); + }); + + it('should get an embedded item on scope with promises - verify', function(done) { + Person.findById(personId) + .then(function(p) { + var passport = p.passportItem(); + passport.toObject().should.eql({name: 'Jason'}); + done(); + }).catch(done); + }); + + it('should destroy an embedded item on scope with promises', function(done) { + Person.findById(personId) + .then(function(p) { + return p.passportItem.destroy() + .then(function() { + should.equal(p.passport, null); + done(); + }); + }).catch(done); + }); + + it('should get an embedded item on scope with promises - verify', function(done) { + Person.findById(personId) + .then(function(p) { + should.equal(p.passport, null); + done(); + }).catch(done); + }); }); describe('embedsOne - persisted model', function () { @@ -2462,6 +3515,18 @@ describe('relations', function () { }); }); + it('should create an embedded item on scope with promises', function(done) { + Person.create({name: 'Barney'}) + .then(function(p) { + return p.passportItem.create({name: 'Barnabus'}) + .then(function(passport) { + p.passport.id.should.eql(3); + p.passport.name.should.equal('Barnabus'); + done(); + }); + }).catch(done); + }); + }); describe('embedsOne - generated id', function () { @@ -2721,7 +3786,7 @@ describe('relations', function () { done(); }); }); - + it('should save an unsaved model', function(done) { var p = new Person({name: 'Fred'}); p.isNewRecord().should.be.true; @@ -3466,12 +4531,14 @@ describe('relations', function () { it('can be declared', function (done) { var reverse = function(cb) { + cb = cb || createPromiseCallback(); var modelInstance = this.modelInstance; var fk = this.definition.keyFrom; var ids = modelInstance[fk] || []; modelInstance.updateAttribute(fk, ids.reverse(), function(err, inst) { cb(err, inst[fk] || []); }); + return cb.promise; }; reverse.shared = true; // remoting @@ -3692,6 +4759,7 @@ describe('relations', function () { Job.exists(job2.id, function(err, exists) { should.not.exist(err); should.exist(exists); + exists.should.be.false; done(); }); }); @@ -3711,8 +4779,274 @@ describe('relations', function () { }); }); + it('should setup test records with promises', function (done) { + db.automigrate(function () { + return Job.create({ name: 'Job 1' }) + .then(function (p) { + job1 = p; + return Job.create({ name: 'Job 3' }) + }) + .then(function (p) { + job3 = p; + done(); + }).catch(done); + }); + }); + + it('should create record on scope with promises', function (done) { + Category.create({ name: 'Category A' }) + .then(function(cat) { + cat.jobIds.should.be.an.array; + cat.jobIds.should.have.length(0); + return cat.jobs.create({ name: 'Job 2' }) + .then(function(p) { + cat.jobIds.should.have.length(1); + cat.jobIds.should.eql([p.id]); + p.name.should.equal('Job 2'); + done(); + }); + }).catch(done); + }); + + + + it('should not allow duplicate record on scope with promises', function (done) { + Category.findOne() + .then(function (cat) { + cat.jobIds = [job2.id, job2.id]; + cat.save() + }) + .then(function (p) { + should.not.exist(p); + done(); + }) + .catch(function (err) { + should.exist(err); + err.name.should.equal('ValidationError'); + err.details.codes.jobs.should.eql(['uniqueness']); + done(); + }); + }); + + it('should find items on scope with promises', function (done) { + Category.findOne() + .then(function (cat) { + cat.jobIds.should.eql([job2.id]); + return cat.jobs.getAsync() + }) + .then(function (jobs) { + var p = jobs[0]; + p.id.should.eql(job2.id); + p.name.should.equal('Job 2'); + done(); + }) + .catch(done); + }); + + it('should find items on scope with promises - findById', function (done) { + Category.findOne() + .then(function (cat) { + cat.jobIds.should.eql([job2.id]); + return cat.jobs.findById(job2.id) + }) + .then(function (p) { + p.should.be.instanceof(Job); + p.id.should.eql(job2.id); + p.name.should.equal('Job 2'); + done(); + }) + .catch(done); + }); + + it('should check if a record exists on scope with promises', function (done) { + Category.findOne() + .then(function (cat) { + return cat.jobs.exists(job2.id) + .then(function (exists) { + should.exist(exists); + done(); + }); + }).catch(done); + }); + + it('should update a record on scope with promises', function (done) { + Category.findOne() + .then(function (cat) { + var attrs = { name: 'Job 2 - edit' }; + return cat.jobs.updateById(job2.id, attrs) + .then(function (p) { + p.name.should.equal(attrs.name); + done(); + }) + }) + .catch(done); + }); + + it('should get a record by index with promises - at', function (done) { + Category.findOne() + .then(function (cat) { + return cat.jobs.at(0) + }) + .then(function (p) { + p.should.be.instanceof(Job); + p.id.should.eql(job2.id); + p.name.should.equal('Job 2 - edit'); + done(); + }) + .catch(done); + }); + + it('should add a record to scope with promises - object', function (done) { + Category.findOne() + .then(function (cat) { + return cat.jobs.add(job1) + .then(function (prod) { + cat.jobIds.should.eql([job2.id, job1.id]); + prod.id.should.eql(job1.id); + prod.should.have.property('name'); + done(); + }); + }) + .catch(done); + }); + + it('should add a record to scope with promises - object', function (done) { + Category.findOne() + .then(function (cat) { + return cat.jobs.add(job3.id) + .then(function (prod) { + var expected = [job2.id, job1.id, job3.id]; + cat.jobIds.should.eql(expected); + prod.id.should.eql(job3.id); + prod.should.have.property('name'); + done(); + }); + }) + .catch(done); + }); + + it('should find items on scope with promises - findById', function (done) { + Category.findOne() + .then(function (cat) { + return cat.jobs.findById(job3.id) + }) + .then(function (p) { + p.id.should.eql(job3.id); + p.name.should.equal('Job 3'); + done(); + }) + .catch(done); + }); + + it('should find items on scope with promises - filter', function (done) { + Category.findOne() + .then(function (cat) { + var filter = { where: { name: 'Job 1' } }; + return cat.jobs.getAsync(filter) + }) + .then(function (jobs) { + jobs.should.have.length(1); + var p = jobs[0]; + p.id.should.eql(job1.id); + p.name.should.equal('Job 1'); + done(); + }) + .catch(done); + }); + + it('should remove items from scope with promises', function (done) { + Category.findOne() + .then(function (cat) { + return cat.jobs.remove(job1.id) + .then(function (ids) { + var expected = [job2.id, job3.id]; + cat.jobIds.should.eql(expected); + ids.should.eql(cat.jobIds); + done(); + }); + }) + .catch(done); + }); + + it('should find items on scope with promises - verify', function (done) { + Category.findOne() + .then(function (cat) { + var expected = [job2.id, job3.id]; + cat.jobIds.should.eql(expected); + return cat.jobs.getAsync() + }) + .then(function (jobs) { + jobs.should.have.length(2); + jobs[0].id.should.eql(job2.id); + jobs[1].id.should.eql(job3.id); + done(); + }) + .catch(done); + }); + + it('should allow custom scope methods with promises - reverse', function(done) { + Category.findOne() + .then(function (cat) { + return cat.jobs.reverse() + .then(function (ids) { + var expected = [job3.id, job2.id]; + ids.should.eql(expected); + cat.jobIds.should.eql(expected); + done(); + }); + }) + .catch(done); + }); + + it('should include related items from scope with promises', function(done) { + Category.find({ include: 'jobs' }) + .then(function (categories) { + categories.should.have.length(1); + var cat = categories[0].toObject(); + cat.name.should.equal('Category A'); + cat.jobs.should.have.length(2); + cat.jobs[0].id.should.eql(job3.id); + cat.jobs[1].id.should.eql(job2.id); + done(); + }).catch(done); + }); + + it('should destroy items from scope with promises - destroyById', function (done) { + Category.findOne() + .then(function (cat) { + return cat.jobs.destroy(job2.id) + .then(function () { + var expected = [job3.id]; + cat.jobIds.should.eql(expected); + return Job.exists(job2.id) + }) + .then(function (exists) { + should.exist(exists); + exists.should.be.false; + done(); + }); + }) + .catch(done); + }); + + it('should find items on scope with promises - verify', function (done) { + Category.findOne() + .then(function (cat) { + var expected = [job3.id]; + cat.jobIds.should.eql(expected); + return cat.jobs.getAsync() + }) + .then(function (jobs) { + jobs.should.have.length(1); + jobs[0].id.should.eql(job3.id); + done(); + }) + .catch(done); + }); + }); + describe('custom relation/scope methods', function () { var categoryId; @@ -3732,6 +5066,7 @@ describe('relations', function () { var relation = Category.hasMany(Job); var summarize = function(cb) { + cb = cb || createPromiseCallback(); var modelInstance = this.modelInstance; this.fetch(function(err, items) { if (err) return cb(err, []); @@ -3742,6 +5077,7 @@ describe('relations', function () { }); cb(null, summary); }); + return cb.promise; }; summarize.shared = true; // remoting @@ -3786,6 +5122,27 @@ describe('relations', function () { }) }); + it('should allow custom scope methods with promises - summarize', function(done) { + var expected = [ + { name: 'Job 1', categoryId: categoryId, categoryName: 'Category A' }, + { name: 'Job 2', categoryId: categoryId, categoryName: 'Category A' } + ]; + + Category.findOne() + .then(function (cat) { + return cat.jobs.summarize() + }) + .then(function (summary) { + var result = summary.map(function(item) { + delete item.id; + return item; + }); + result.should.eql(expected); + done(); + }) + .catch(done); + }); + }); });