From 2ec6a52fed4960818873cb238c5fa828fee513f7 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 12 Aug 2016 21:34:06 +0530 Subject: [PATCH 1/8] feat(lucid): add support for findByOrFail just like findOrFail, findByOrFail can also remove lots of clauses --- src/Lucid/Model/index.js | 19 +++++++++++++++++++ test/unit/lucid.spec.js | 21 +++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/src/Lucid/Model/index.js b/src/Lucid/Model/index.js index 37bae3a6..8de1a46c 100644 --- a/src/Lucid/Model/index.js +++ b/src/Lucid/Model/index.js @@ -554,6 +554,25 @@ class Model { return result } + /** + * find for a row using key/value pairs + * or fail by throwing an exception. + * + * @method findByOrFail + * + * @param {String} key + * @param {Mixed} value + * + * @return {Object} + */ + static * findByOrFail (key, value) { + const result = yield this.findBy(key, value) + if (!result) { + throw new CE.ModelNotFoundException(`Unable to fetch results for ${key} ${value}`) + } + return result + } + /** * returns all records for a given model * diff --git a/test/unit/lucid.spec.js b/test/unit/lucid.spec.js index 5c2b9d60..58a48f37 100644 --- a/test/unit/lucid.spec.js +++ b/test/unit/lucid.spec.js @@ -820,6 +820,27 @@ describe('Lucid', function () { expect(user.username).to.equal('bea') }) + it('should throw exception when unable to find a row using key/value pair', function * () { + class User extends Model { + } + try { + yield User.findByOrFail('username', 'koka') + expect(true).to.equal(false) + } catch (e) { + expect(e.name).to.equal('ModelNotFoundException') + expect(e.message).to.equal('Unable to fetch results for username koka') + } + }) + + it('should be able to find a given record using findByOrFail method', function * () { + class User extends Model { + } + yield User.query().insert({username: 'bea', firstname: 'Bea', lastname: 'Mine'}) + let user = yield User.findByOrFail('username', 'bea') + expect(user instanceof User) + expect(user.username).to.equal('bea') + }) + it('should be able to find a given record using primary key', function * () { class User extends Model { } From fee8e3176c4f887c47b0843ae000479390c0f1b0 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 12 Aug 2016 21:50:19 +0530 Subject: [PATCH 2/8] feat(lucid): add support for fill method fill method can be used to fill bulk values for saving/updating --- src/Lucid/Model/index.js | 12 ++++++++++++ test/unit/lucid.spec.js | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/src/Lucid/Model/index.js b/src/Lucid/Model/index.js index 8de1a46c..7f294939 100644 --- a/src/Lucid/Model/index.js +++ b/src/Lucid/Model/index.js @@ -85,6 +85,18 @@ class Model { } } + /** + * fill bulk values to the model instance + * attributes + * + * @method fill + * + * @param {Object} values + */ + fill (values) { + this.setJSON(values) + } + /** * adds a new hook for a given type for a model. Note * this method has no way of checking duplicate diff --git a/test/unit/lucid.spec.js b/test/unit/lucid.spec.js index 58a48f37..7958edd5 100644 --- a/test/unit/lucid.spec.js +++ b/test/unit/lucid.spec.js @@ -400,6 +400,43 @@ describe('Lucid', function () { expect(affected).to.equal(0) }) + it('should fill json values to the model instance attributes', function * () { + class User extends Model {} + const user = new User() + user.fill({username: 'bana'}) + yield user.save() + expect(user.isNew()).to.equal(false) + expect(user.id).to.exist + }) + + it('should call setters when making use of fill method', function * () { + class User extends Model { + setUsername (value) { + return value.toUpperCase() + } + } + const user = new User() + user.fill({username: 'dukki'}) + yield user.save() + expect(user.isNew()).to.equal(false) + expect(user.username).to.equal('DUKKI') + }) + + it('should update attributes on fill', function * () { + class User extends Model { + setFirstname (value) { + return value.toUpperCase() + } + } + const user = yield User.create({username: 'jgwhite'}) + expect(user.created_at).to.exist + user.fill({firstname: 'foo', lastname: 'bar'}) + yield user.save() + const reFetchUser = yield User.find(user.id) + expect(reFetchUser.firstname).to.equal('FOO') + expect(reFetchUser.lastname).to.equal('bar') + }) + it('should not re-mutate the values on insert', function * () { class User extends Model { setFirstname (value) { From d6b028eae991d4e0e7b8a84aff89cce9fca9224f Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 12 Aug 2016 22:34:16 +0530 Subject: [PATCH 3/8] test(lucid): clean tests to have isolated state each lucid test now has its own test state --- test/unit/lucid.spec.js | 99 ++++++++++++++++++++++++++--------------- 1 file changed, 63 insertions(+), 36 deletions(-) diff --git a/test/unit/lucid.spec.js b/test/unit/lucid.spec.js index 7958edd5..75af1cd1 100644 --- a/test/unit/lucid.spec.js +++ b/test/unit/lucid.spec.js @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -/* global describe, it, after, before, context */ +/* global describe, it, after, before, context, afterEach */ const Model = require('../../src/Lucid/Model') const Database = require('../../src/Database') const chai = require('chai') @@ -24,6 +24,7 @@ const modelFixtures = require('./fixtures/model') const QueryBuilder = require('../../src/Lucid/QueryBuilder') const config = require('./helpers/config') const queryHelpers = require('./helpers/query') +const _ = require('lodash') require('co-mocha') describe('Lucid', function () { @@ -42,6 +43,10 @@ describe('Lucid', function () { Database.close() }) + afterEach(function * () { + yield Database.table('users').truncate() + }) + context('Model', function () { it('should return model boot state as undefined', function () { class User extends Model {} @@ -104,19 +109,6 @@ describe('Lucid', function () { expect(User.globalScope[0]).to.be.a('function') }) - it('should be able to add global scopes to the model instance inside boot method', function () { - class User extends Model { - static boot () { - super.boot() - this.addGlobalScope(function () {}) - } - } - User.bootIfNotBooted() - expect(User.globalScope).to.be.an('array') - expect(User.globalScope.length).to.equal(2) - expect(User.globalScope[0]).to.be.a('function') - }) - it('should make no effect on sibling models when adding global scopes', function () { class User extends Model { static boot () { @@ -163,7 +155,7 @@ describe('Lucid', function () { }) context('Model Instance', function () { - it('should throw an error when trying to initiate a model with an array of values', function () { + it('should throw an error when trying to instantiate a model with an array of values', function () { class User extends Model {} const fn = function () { return new User([{name: 'foo'}, {name: 'bar'}]) @@ -171,7 +163,7 @@ describe('Lucid', function () { expect(fn).to.throw(NE.InvalidArgumentException, /cannot initiate a model with multiple rows./) }) - it('should be able to initiate a model with an object of values', function () { + it('should be able to instantiate a model with an object of values', function () { class User extends Model {} const user = new User({name: 'foo', age: 22}) expect(user.attributes.name).to.equal('foo') @@ -428,7 +420,7 @@ describe('Lucid', function () { return value.toUpperCase() } } - const user = yield User.create({username: 'jgwhite'}) + const user = yield User.create({username: 'dukki'}) expect(user.created_at).to.exist user.fill({firstname: 'foo', lastname: 'bar'}) yield user.save() @@ -459,8 +451,8 @@ describe('Lucid', function () { return `${value}_first` } } - const user = yield User.query().fetch() - const firstUser = user.first() + yield User.create({username: 'dukki'}) + const firstUser = yield User.query().first() const userId = firstUser.id firstUser.firstname = 'new_name' yield firstUser.save() @@ -553,6 +545,9 @@ describe('Lucid', function () { }) .then(function () { expect(user.updated_at).to.be.above(updatedTimestamp) + return User.query().truncate() + }) + .then(function () { done() }) .catch(done) @@ -582,6 +577,7 @@ describe('Lucid', function () { } } User.bootIfNotBooted() + yield User.create({username: 'bana'}) const user = yield User.find(1) expect(user instanceof User).to.equal(true) expect(user.id).to.equal(1) @@ -627,6 +623,7 @@ describe('Lucid', function () { expect(true).to.equal(false) } catch (e) { expect(e.message).to.match(/Restore can only be done when soft deletes are enabled/) + } finally { } }) @@ -634,12 +631,13 @@ describe('Lucid', function () { class User extends Model { } User.bootIfNotBooted() - const user = yield User.find(2) + yield User.create({username: 'foo'}) + const user = yield User.find(1) expect(user instanceof User).to.equal(true) - expect(user.id).to.equal(2) + expect(user.id).to.equal(1) yield user.delete() expect(user.isDeleted()).to.equal(true) - const fetchUser = yield User.query().where('id', 2) + const fetchUser = yield User.query().where('id', 1) expect(user.deleted_at).to.equal(null) expect(fetchUser.length).to.equal(0) }) @@ -654,7 +652,7 @@ describe('Lucid', function () { expect(user.isNew()).to.equal(false) }) - it('should try to find first and create model instance using static findOrCreate method', function * () { + it('should try to find first or create model instance using static findOrCreate method', function * () { class User extends Model { } User.bootIfNotBooted() @@ -738,6 +736,7 @@ describe('Lucid', function () { context('QueryBuilder', function () { it('should return an array of models instance when used fetch method', function * () { class User extends Model {} + yield User.create({username: 'foo'}) const users = yield User.query().fetch() expect(users.first() instanceof User).to.equal(true) }) @@ -761,6 +760,7 @@ describe('Lucid', function () { return super.toJSON() } } + yield User.create({username: 'foo'}) const users = yield User.query().fetch() expect(users.last() instanceof User).to.equal(true) }) @@ -773,6 +773,7 @@ describe('Lucid', function () { return super.toJSON() } } + yield User.createMany([{username: 'virk'}, {username: 'foo'}]) const users = yield User.query().fetch() const jsoned = users.filter(function (user) { return user.username === 'virk' @@ -786,6 +787,7 @@ describe('Lucid', function () { return value.toUpperCase() } } + yield User.create({username: 'foo'}) const users = yield User.query().fetch() const user = users.first() expect(user.username).to.equal(user.attributes.username.toUpperCase()) @@ -795,6 +797,7 @@ describe('Lucid', function () { it('should pick a given number of rows from database using pick method', function * () { class User extends Model { } + yield User.createMany([{username: 'foo'}, {username: 'bar'}, {username: 'baz'}]) let users = yield User.pick(2) users = users.toJSON() expect(users).to.be.an('array') @@ -805,6 +808,7 @@ describe('Lucid', function () { it('should pick a given number of rows from database using query builder pick method', function * () { class User extends Model { } + yield User.createMany([{username: 'foo'}, {username: 'virk'}, {username: 'baz'}]) let users = yield User.query().where('username', 'virk').pick(2) users = users.toJSON() expect(users).to.be.an('array') @@ -814,6 +818,7 @@ describe('Lucid', function () { it('should pick only one row when limit argument is not passed to pick method', function * () { class User extends Model { } + yield User.createMany([{username: 'foo'}, {username: 'virk'}]) let users = yield User.pick() users = users.toJSON() expect(users).to.be.an('array') @@ -823,6 +828,7 @@ describe('Lucid', function () { it('should pick a given number of rows in reverse order from database using pickInverse method', function * () { class User extends Model { } + yield User.createMany([{username: 'foo'}, {username: 'bar'}, {username: 'baz'}]) let users = yield User.pickInverse(2) users = users.toJSON() expect(users).to.be.an('array') @@ -833,6 +839,7 @@ describe('Lucid', function () { it('should pick a given number of rows in reverse order from database using query builder pickInverse method', function * () { class User extends Model { } + yield User.createMany([{username: 'foo'}, {username: 'virk'}]) let users = yield User.query().where('username', 'virk').pickInverse(2) users = users.toJSON() expect(users).to.be.an('array') @@ -842,6 +849,7 @@ describe('Lucid', function () { it('should pick only one row when limit argument is not passed to pickInverse method', function * () { class User extends Model { } + yield User.createMany([{username: 'foo'}, {username: 'virk'}]) let users = yield User.pickInverse() users = users.toJSON() expect(users).to.be.an('array') @@ -899,6 +907,7 @@ describe('Lucid', function () { it('should return all rows inside a database when all method is called', function * () { class User extends Model { } + yield User.createMany([{username: 'foo'}, {username: 'bar'}]) const users = yield User.all() const total = yield User.query().count('* as total') expect(parseInt(total[0].total)).to.equal(users.size()) @@ -907,6 +916,7 @@ describe('Lucid', function () { it('should return an array of ids when using ids method', function * () { class User extends Model { } + yield User.createMany([{username: 'foo'}, {username: 'bar'}]) const userIds = yield User.ids() expect(userIds).to.be.an('array') userIds.forEach(function (id) { @@ -917,17 +927,19 @@ describe('Lucid', function () { it('should be able to fetch ids of the query builder', function * () { class User extends Model { } - const userIds = yield User.query().where('id', '>', 5).ids() + yield User.createMany([{username: 'foo'}, {username: 'bar'}]) + const userIds = yield User.query().where('id', '>', 1).ids() expect(userIds).to.be.an('array') userIds.forEach(function (id) { expect(id).to.be.a('number') - expect(id).to.be.above(5) + expect(id).to.be.above(1) }) }) it('should return a plain object with key/value pairs when using pair method', function * () { class User extends Model { } + yield User.createMany([{username: 'foo'}, {username: 'bar'}]) const usersPair = yield User.pair('id', 'username') const users = yield User.all() let manualPair = users.map(function (user) { @@ -940,12 +952,14 @@ describe('Lucid', function () { it('should be able to use pairs of the query builder chain', function * () { class User extends Model { } + yield User.createMany([{username: 'foo'}, {username: 'bar'}]) const usersPair = yield User.query().pair('id', 'username') const users = yield User.all() let manualPair = users.map(function (user) { return [user.id, user.username] }).fromPairs().value() expect(usersPair).to.be.an('object') + expect(Object.keys(usersPair)).to.have.length.above(1) expect(usersPair).deep.equal(manualPair) }) @@ -957,7 +971,7 @@ describe('Lucid', function () { expect(username).to.equal('unique-user') }) - it('should return null when pluckFirst has nothing found any rows', function * () { + it('should return null when pluckFirst has not found any rows', function * () { class User extends Model { } const username = yield User.query().where('username', 'non-existing-user').pluckFirst('username') @@ -984,7 +998,7 @@ describe('Lucid', function () { } }) - it('should throw ModelNotFoundException when unable to find a record using firstOfFail method', function * () { + it('should throw ModelNotFoundException when unable to find a record using query builder firstOfFail', function * () { class User extends Model { } try { @@ -999,9 +1013,11 @@ describe('Lucid', function () { it('should return model instance using findOrFail method', function * () { class User extends Model { } - const user = yield User.findOrFail(3) + yield User.create({username: 'foo'}) + const user = yield User.findOrFail(1) expect(user instanceof User).to.equal(true) - expect(user.id).to.equal(3) + expect(user.id).to.equal(1) + expect(user.username).to.equal('foo') }) it('should not fetch soft deleted rows when soft deletes are on', function * () { @@ -1072,11 +1088,11 @@ describe('Lucid', function () { } } User.bootIfNotBooted() + const user = yield User.create({username: 'foo'}) + yield user.delete() const trashedUsers = yield User.query().onlyTrashed().fetch() - expect(trashedUsers.size()).to.be.above(0) - trashedUsers.each(function (user) { - expect(user.deleted_at).to.be.a('string') - }) + expect(trashedUsers.size()).to.equal(1) + expect(trashedUsers.first().deleted_at).to.be.a('string') }) it('should call all global scopes when fetch method is called', function * () { @@ -1144,6 +1160,10 @@ describe('Lucid', function () { it('should be able to paginate results using model query builder', function * () { class User extends Model { } + const newUsers = _.range(10).map((i) => { + return {username: `foo_${i}`} + }) + yield User.query().insert(newUsers) const users = yield User.query().paginate(1, 10) const paginatedUsers = users.toJSON() expect(paginatedUsers).to.have.property('total') @@ -1156,6 +1176,10 @@ describe('Lucid', function () { it('should be able to paginate results using model static method', function * () { class User extends Model { } + const newUsers = _.range(10).map((i) => { + return {username: `foo_${i}`} + }) + yield User.query().insert(newUsers) const users = yield User.paginate(1, 10) const paginatedUsers = users.toJSON() expect(paginatedUsers).to.have.property('total') @@ -1433,7 +1457,8 @@ describe('Lucid', function () { hookCalled = true yield next }) - const user = yield User.find(3) + yield User.create({username: 'foo'}) + const user = yield User.findBy('username', 'foo') expect(user instanceof User).to.equal(true) user.firstname = 'new name' yield user.save() @@ -1457,6 +1482,7 @@ describe('Lucid', function () { expect(user.id).to.be.a('number') expect(user.isNew()).to.equal(false) expect(e.message).to.equal('should not have created it') + } finally { } }) @@ -1641,10 +1667,11 @@ describe('Lucid', function () { this.lastname = 'foo' yield next }) - const user = yield User.find(3) + yield User.create({username: 'foo', lastname: 'bar'}) + const user = yield User.findBy('username', 'foo') expect(user instanceof User).to.equal(true) yield user.save() - const reFetchUser = yield User.find(3) + const reFetchUser = yield User.findBy('username', 'foo') expect(reFetchUser.lastname).to.equal('foo') }) }) From 87f16a28c17824c6e6cf94a77f48c741c5fa4ccb Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 12 Aug 2016 22:45:39 +0530 Subject: [PATCH 4/8] feat(lucid): add static truncate static truncate to truncate model database table name --- src/Lucid/Model/index.js | 11 +++++++++++ test/unit/lucid.spec.js | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/Lucid/Model/index.js b/src/Lucid/Model/index.js index 7f294939..094b96c8 100644 --- a/src/Lucid/Model/index.js +++ b/src/Lucid/Model/index.js @@ -622,6 +622,17 @@ class Model { return yield this.query().pair(lhs, rhs) } + /** + * truncate model database table + * + * @method truncate + * + * @return {Boolean} + */ + static * truncate () { + return yield this.query().truncate() + } + /** * shorthand to get access to the with method on * query builder chain. diff --git a/test/unit/lucid.spec.js b/test/unit/lucid.spec.js index 75af1cd1..074cf531 100644 --- a/test/unit/lucid.spec.js +++ b/test/unit/lucid.spec.js @@ -731,6 +731,15 @@ describe('Lucid', function () { expect(e.message).to.match(/cannot save an empty model/i) } }) + + it('should be able to truncate database table using static truncate method', function * () { + class User extends Model {} + User.bootIfNotBooted() + yield User.create({username: 'foo', lastname: 'bar'}) + yield User.truncate() + const users = yield User.query().count('* as total') + expect(users[0].total).to.equal(0) + }) }) context('QueryBuilder', function () { @@ -1659,7 +1668,9 @@ describe('Lucid', function () { User.query().active() expect(new Ref() instanceof User).to.equal(true) }) + }) + context('Regression', function () { it('should consider attributes chaned inside before update as dirty values when updating', function * () { class User extends Model {} User.bootIfNotBooted() From 4d7279417e3e93717540feeaa37cef4127174e40 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 12 Aug 2016 23:35:58 +0530 Subject: [PATCH 5/8] feat(lucid): add fresh method to grab fresh instance fresh method will return the fresh instance by making a query, it is helpful when db has defaults and you want them after writes --- src/Lucid/Model/index.js | 16 ++++++++++++++++ test/unit/fixtures/model.js | 2 +- test/unit/lucid.spec.js | 21 ++++++++++++++++++++- 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/Lucid/Model/index.js b/src/Lucid/Model/index.js index 094b96c8..553a28bd 100644 --- a/src/Lucid/Model/index.js +++ b/src/Lucid/Model/index.js @@ -741,6 +741,22 @@ class Model { return yield this.update() } + /** + * returns a fresh model instance, it is + * useful when database has defaults + * set. + * + * @method fresh + * + * @return {Object} + */ + * fresh () { + if (this.isNew()) { + return this + } + return yield this.constructor.find(this.$primaryKeyValue) + } + /** * initiates and save a model instance with given * values. Create is a short to new Model and diff --git a/test/unit/fixtures/model.js b/test/unit/fixtures/model.js index f318dc1f..029c411e 100644 --- a/test/unit/fixtures/model.js +++ b/test/unit/fixtures/model.js @@ -10,7 +10,7 @@ module.exports = { table.string('username') table.string('firstname') table.string('lastname') - table.string('status') + table.enum('status', ['active', 'suspended']).defaultTo('active') table.timestamps() table.timestamp('deleted_at').nullable() }), diff --git a/test/unit/lucid.spec.js b/test/unit/lucid.spec.js index 074cf531..86495e38 100644 --- a/test/unit/lucid.spec.js +++ b/test/unit/lucid.spec.js @@ -738,7 +738,26 @@ describe('Lucid', function () { yield User.create({username: 'foo', lastname: 'bar'}) yield User.truncate() const users = yield User.query().count('* as total') - expect(users[0].total).to.equal(0) + expect(parseInt(users[0].total)).to.equal(0) + }) + + it('should return a fresh model instance after save', function * () { + class User extends Model {} + User.bootIfNotBooted() + const user = yield User.create({username: 'foo', lastname: 'bar'}) + expect(user.status).to.equal(undefined) + const freshUser = yield user.fresh() + expect(freshUser.status).to.equal('active') + }) + + it('should not hit database when model instance isNew', function * () { + class User extends Model {} + User.bootIfNotBooted() + const user = new User() + user.firstname = 'foo' + expect(user.status).to.equal(undefined) + const freshUser = yield user.fresh() + expect(freshUser.status).to.equal(undefined) }) }) From c74e08141c798f6da6d260defbf165090b6da22a Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Sat, 13 Aug 2016 00:43:30 +0530 Subject: [PATCH 6/8] fix(lucid:relations): keep relation output consistent eager loading relations has inconsistent output, whereas it needs to be consistent Closes #45 --- src/Lucid/Model/Mixins/Serializer.js | 2 +- src/Lucid/Relations/BelongsTo.js | 13 ++ src/Lucid/Relations/EagerLoad.js | 11 +- src/Lucid/Relations/HasOne.js | 13 ++ src/Lucid/Relations/Relation.js | 13 ++ test/unit/fixtures/relations.js | 8 + test/unit/lucid.relations.spec.js | 214 ++++++++++++++++++++++++++- 7 files changed, 268 insertions(+), 6 deletions(-) diff --git a/src/Lucid/Model/Mixins/Serializer.js b/src/Lucid/Model/Mixins/Serializer.js index 6119b8e8..14116a1a 100644 --- a/src/Lucid/Model/Mixins/Serializer.js +++ b/src/Lucid/Model/Mixins/Serializer.js @@ -44,7 +44,7 @@ Serializer.toJSON = function () { */ Serializer.serializeRelations = function () { return _.transform(this.relations, function (result, value, index) { - result[index] = value && value.toJSON() ? value.toJSON() : value + result[index] = _.size(value) && value.toJSON() ? value.toJSON() : value }) } diff --git a/src/Lucid/Relations/BelongsTo.js b/src/Lucid/Relations/BelongsTo.js index e110318b..ce0a33c0 100644 --- a/src/Lucid/Relations/BelongsTo.js +++ b/src/Lucid/Relations/BelongsTo.js @@ -22,6 +22,19 @@ class BelongsTo extends Relation { this.fromKey = foreignKey || this.related.foreignKey } + /** + * empty placeholder to be used when unable to eagerload + * relations. It needs to be an array of many to many + * relationships. + * + * @method eagerLoadFallbackValue + * + * @return {Null} + */ + get eagerLoadFallbackValue () { + return null + } + /** * returns result of this.first * diff --git a/src/Lucid/Relations/EagerLoad.js b/src/Lucid/Relations/EagerLoad.js index d901bd3d..37061d2f 100644 --- a/src/Lucid/Relations/EagerLoad.js +++ b/src/Lucid/Relations/EagerLoad.js @@ -155,9 +155,10 @@ class EagerLoad { * * @private */ - _getJoinKeyValues (mappedKeys, mappedKeysValues, relation, relationInstance, values) { + _getJoinKeyValues (mappedKeys, mappedKeysValues, fallbackValues, relation, relationInstance, values) { if (!mappedKeys[relation]) { mappedKeys[relation] = relationInstance.fromKey + fallbackValues[relation] = relationInstance.eagerLoadFallbackValue if (_.isArray(values)) { mappedKeysValues[relation] = values.map((value) => value[relationInstance.fromKey]) } else { @@ -221,6 +222,7 @@ class EagerLoad { load (result, parent, isSingle) { const self = this const mappedKeys = {} + const fallbackValues = {} const mappedKeysValues = {} /** @@ -230,7 +232,7 @@ class EagerLoad { const response = cf.map(function * (relation) { const relationScope = self.relationsScope[relation] const relationInstance = isSingle ? self._getRelationInstance(relation, parent) : self._getRelationProtoInstance(relation, parent) - const values = self._getJoinKeyValues(mappedKeys, mappedKeysValues, relation, relationInstance, result) + const values = self._getJoinKeyValues(mappedKeys, mappedKeysValues, fallbackValues, relation, relationInstance, result) /** * here we pass nested relations and scopes to the relationInstance @@ -245,7 +247,7 @@ class EagerLoad { return yield relationInstance.eagerLoad(values, relationScope) }, this.withRelations) - return {values: response, keys: mappedKeys} + return {values: response, keys: mappedKeys, fallbackValues} } /** @@ -262,12 +264,13 @@ class EagerLoad { if (_.size(eagerLoadResult)) { const keys = eagerLoadResult.keys const values = eagerLoadResult.values + const fallbackValues = eagerLoadResult.fallbackValues const relationsArray = Object.keys(keys) _.each(values, (value, index) => { const relationKey = relationsArray[index] const fromKey = keys[relationKey] - parent.relations[relationKey] = value[row[fromKey]] || null + parent.relations[relationKey] = value[row[fromKey]] || fallbackValues[relationKey] }) } } diff --git a/src/Lucid/Relations/HasOne.js b/src/Lucid/Relations/HasOne.js index fedee3cf..b3fc3719 100644 --- a/src/Lucid/Relations/HasOne.js +++ b/src/Lucid/Relations/HasOne.js @@ -20,6 +20,19 @@ class HasOne extends Relation { this.toKey = foreignKey || this.parent.constructor.foreignKey } + /** + * empty placeholder to be used when unable to eagerload + * relations. It needs to be an array of many to many + * relationships. + * + * @method eagerLoadFallbackValue + * + * @return {Null} + */ + get eagerLoadFallbackValue () { + return null + } + /** * returns result of this.first * diff --git a/src/Lucid/Relations/Relation.js b/src/Lucid/Relations/Relation.js index c82f68fd..efad5dfa 100644 --- a/src/Lucid/Relations/Relation.js +++ b/src/Lucid/Relations/Relation.js @@ -26,6 +26,19 @@ class Relation { return new Proxy(this, proxyHandler) } + /** + * empty placeholder to be used when unable to eagerload + * relations. It needs to be an array of many to many + * relationships. + * + * @method eagerLoadFallbackValue + * + * @return {Array} + */ + get eagerLoadFallbackValue () { + return [] + } + /** * returns the model from IoC container or returns * the actual binding if model representation is diff --git a/test/unit/fixtures/relations.js b/test/unit/fixtures/relations.js index e91753d2..521e202e 100644 --- a/test/unit/fixtures/relations.js +++ b/test/unit/fixtures/relations.js @@ -88,6 +88,13 @@ module.exports = { table.timestamps() table.timestamp('deleted_at').nullable() }), + knex.schema.createTable('subjects', function (table) { + table.increments() + table.string('title') + table.integer('course_id') + table.timestamps() + table.timestamp('deleted_at').nullable() + }), knex.schema.createTable('course_student', function (table) { table.integer('student_id') table.integer('course_id') @@ -132,6 +139,7 @@ module.exports = { knex.schema.dropTable('comments'), knex.schema.dropTable('courses'), knex.schema.dropTable('students'), + knex.schema.dropTable('subjects'), knex.schema.dropTable('course_student'), knex.schema.dropTable('authors'), knex.schema.dropTable('publications'), diff --git a/test/unit/lucid.relations.spec.js b/test/unit/lucid.relations.spec.js index 57196728..d1bfdc11 100644 --- a/test/unit/lucid.relations.spec.js +++ b/test/unit/lucid.relations.spec.js @@ -293,7 +293,7 @@ describe('Relations', function () { yield relationFixtures.truncate(Database, 'accounts') }) - it('should return null when unable to fetch related results', function * () { + it('should return null when unable to fetch related results via eager loading', function * () { yield relationFixtures.createRecords(Database, 'suppliers', {name: 'redtape'}) class Account extends Model { } @@ -2831,4 +2831,216 @@ describe('Relations', function () { } }) }) + + context('Regression:HasOne', function () { + it('should return null when unable to fetch related results via eager loading', function * () { + yield relationFixtures.createRecords(Database, 'suppliers', {name: 'redtape'}) + class Account extends Model { + } + class Supplier extends Model { + account () { + return this.hasOne(Account) + } + } + const supplier = yield Supplier.query().with('account').first() + expect(supplier.toJSON().account).to.equal(null) + yield relationFixtures.truncate(Database, 'suppliers') + }) + + it('should return null when unable to fetch related results of the model instance', function * () { + yield relationFixtures.createRecords(Database, 'suppliers', {name: 'redtape'}) + class Account extends Model { + } + class Supplier extends Model { + account () { + return this.hasOne(Account) + } + } + const supplier = yield Supplier.find(1) + const account = yield supplier.account().first() + expect(account).to.equal(null) + yield relationFixtures.truncate(Database, 'suppliers') + }) + + it('should return null when unable to fetch related results via lazy eager loading', function * () { + yield relationFixtures.createRecords(Database, 'suppliers', {name: 'redtape'}) + class Account extends Model { + } + class Supplier extends Model { + account () { + return this.hasOne(Account) + } + } + const supplier = yield Supplier.find(1) + yield supplier.related('account').load() + expect(supplier.toJSON().account).to.equal(null) + yield relationFixtures.truncate(Database, 'suppliers') + }) + }) + + context('Regression:BelongsTo', function () { + it('should return null when unable to fetch related results via eager loading', function * () { + yield relationFixtures.createRecords(Database, 'comments', {body: 'Nice article', post_id: 1}) + class Post extends Model { + } + class Comment extends Model { + post () { + return this.belongsTo(Post) + } + } + const comment = yield Comment.query().with('post').first() + expect(comment.toJSON().post).to.equal(null) + yield relationFixtures.truncate(Database, 'comments') + }) + + it('should return null when unable to fetch related results of model instance', function * () { + yield relationFixtures.createRecords(Database, 'comments', {body: 'Nice article', post_id: 1}) + class Post extends Model { + } + class Comment extends Model { + post () { + return this.belongsTo(Post) + } + } + const comment = yield Comment.query().first() + const post = yield comment.post().first() + expect(post).to.equal(null) + yield relationFixtures.truncate(Database, 'comments') + }) + + it('should return null when unable to fetch related results via lazy eager loading', function * () { + yield relationFixtures.createRecords(Database, 'comments', {body: 'Nice article', post_id: 1}) + class Post extends Model { + } + class Comment extends Model { + post () { + return this.belongsTo(Post) + } + } + const comment = yield Comment.query().first() + yield comment.related('post').load() + expect(comment.toJSON().post).to.equal(null) + yield relationFixtures.truncate(Database, 'comments') + }) + }) + + context('Regression:HasMany', function () { + it('should return an empty array when unable to fetch related results via eager loading', function * () { + yield relationFixtures.createRecords(Database, 'posts', {title: 'Adonis 101', body: 'Let\'s learn Adonis'}) + class Comment extends Model { + } + class Post extends Model { + comments () { + return this.hasMany(Comment) + } + } + const post = yield Post.query().with('comments').first() + expect(post.toJSON().comments).deep.equal([]) + yield relationFixtures.truncate(Database, 'posts') + }) + + it('should return an empty array when unable to fetch related results of model instance', function * () { + yield relationFixtures.createRecords(Database, 'posts', {title: 'Adonis 101', body: 'Let\'s learn Adonis'}) + class Comment extends Model { + } + class Post extends Model { + comments () { + return this.hasMany(Comment) + } + } + const post = yield Post.query().first() + const comments = yield post.comments().fetch() + expect(comments.toJSON()).deep.equal([]) + yield relationFixtures.truncate(Database, 'posts') + }) + + it('should return an empty array when unable to fetch related results via lazy eager loading', function * () { + yield relationFixtures.createRecords(Database, 'posts', {title: 'Adonis 101', body: 'Let\'s learn Adonis'}) + class Comment extends Model { + } + class Post extends Model { + comments () { + return this.hasMany(Comment) + } + } + const post = yield Post.query().first() + yield post.related('comments').load() + expect(post.toJSON().comments).deep.equal([]) + yield relationFixtures.truncate(Database, 'posts') + }) + }) + + context('Regression:BelongsToMany', function () { + it('should return an empty array when unable to fetch related results via eager loading', function * () { + yield relationFixtures.createRecords(Database, 'students', {name: 'ricky', id: 29}) + class Course extends Model { + } + class Student extends Model { + courses () { + return this.belongsToMany(Course) + } + } + const student = yield Student.query().with('courses').first() + expect(student.toJSON().courses).deep.equal([]) + yield relationFixtures.truncate(Database, 'students') + }) + + it('should return an empty array when unable to fetch related results of model instance', function * () { + yield relationFixtures.createRecords(Database, 'students', {name: 'ricky', id: 29}) + class Course extends Model { + } + class Student extends Model { + courses () { + return this.belongsToMany(Course) + } + } + const student = yield Student.query().first() + const courses = yield student.courses().fetch() + expect(courses.toJSON()).deep.equal([]) + yield relationFixtures.truncate(Database, 'students') + }) + + it('should return an empty array when unable to fetch related results via lazy eager loading', function * () { + yield relationFixtures.createRecords(Database, 'students', {name: 'ricky', id: 29}) + class Course extends Model { + } + class Student extends Model { + courses () { + return this.belongsToMany(Course) + } + } + const student = yield Student.query().first() + yield student.related('courses').load() + expect(student.toJSON().courses).deep.equal([]) + yield relationFixtures.truncate(Database, 'students') + }) + + it('should return expected output with nested relations', function * () { + const savedStudent = yield relationFixtures.createRecords(Database, 'students', {name: 'ricky', id: 29}) + const savedCourse = yield relationFixtures.createRecords(Database, 'courses', {title: 'geometry'}) + yield relationFixtures.createRecords(Database, 'course_student', {student_id: savedStudent[0], course_id: savedCourse[0]}) + + class Subject extends Model { + } + + class Course extends Model { + subject () { + return this.hasOne(Subject) // situational for testing only + } + } + + class Student extends Model { + courses () { + return this.belongsToMany(Course) + } + } + + const student = yield Student.query().with('courses.subject').first() + expect(student.toJSON().courses).to.be.an('array') + expect(student.toJSON().courses[0].subject).to.equal(null) + yield relationFixtures.truncate(Database, 'students') + yield relationFixtures.truncate(Database, 'courses') + yield relationFixtures.truncate(Database, 'course_student') + }) + }) }) From 59cfa024bc6cd0bb6724ffa73d6dabf8c5c2b279 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Sat, 13 Aug 2016 01:36:07 +0530 Subject: [PATCH 7/8] feat(lucid): add support for transactions now model instance useTransaction method makes it easier to use transactions for database writes Closes #43 --- src/Lucid/Model/Mixins/Persistance.js | 12 +++ src/Lucid/Model/index.js | 22 ++++ src/Lucid/Model/proxyHandler.js | 2 +- test/unit/lucid.spec.js | 148 ++++++++++++++++++++++++++ 4 files changed, 183 insertions(+), 1 deletion(-) diff --git a/src/Lucid/Model/Mixins/Persistance.js b/src/Lucid/Model/Mixins/Persistance.js index 20ec8cd6..9294efaa 100644 --- a/src/Lucid/Model/Mixins/Persistance.js +++ b/src/Lucid/Model/Mixins/Persistance.js @@ -34,6 +34,9 @@ Peristance.insert = function * () { throw new NE.RuntimeException('cannot save an empty model') } const query = this.constructor.query() + if (this.transaction) { + query.transacting(this.transaction) + } const save = yield query.insertAttributes(values).returning(this.constructor.primaryKey) if (save[0]) { this.$primaryKeyValue = save[0] @@ -64,6 +67,9 @@ Peristance.update = function * () { if (!_.size(dirtyValues)) { return 0 } + if (this.transaction) { + query.transacting(this.transaction) + } const affected = yield query.where('id', this.$primaryKeyValue).updateAttributes(dirtyValues) if (affected > 0) { _.merge(this.attributes, dirtyValues) @@ -87,6 +93,9 @@ Peristance.update = function * () { Peristance.delete = function * () { const deleteHandler = function * () { const query = this.constructor.query().where('id', this.$primaryKeyValue) + if (this.transaction) { + query.transacting(this.transaction) + } const values = {} const affected = yield query.deleteAttributes(values) if (affected > 0) { @@ -109,6 +118,9 @@ Peristance.delete = function * () { Peristance.restore = function * () { const restoreHandler = function * () { const query = this.constructor.query().where('id', this.$primaryKeyValue) + if (this.transaction) { + query.transacting(this.transaction) + } const values = {} const affected = yield query.restoreAttributes(values) if (affected > 0) { diff --git a/src/Lucid/Model/index.js b/src/Lucid/Model/index.js index 553a28bd..4408a65a 100644 --- a/src/Lucid/Model/index.js +++ b/src/Lucid/Model/index.js @@ -77,6 +77,7 @@ class Model { instantiate (values) { this.attributes = {} this.original = {} + this.transaction = null // will be added via useTransaction this.relations = {} this.frozen = false this.eagerLoad = new EagerLoad() @@ -757,6 +758,27 @@ class Model { return yield this.constructor.find(this.$primaryKeyValue) } + /** + * uses a transaction for all upcoming + * operations + * + * @method useTransaction + * + * @param {Object} trx + */ + useTransaction (trx) { + this.transaction = trx + } + + /** + * resets transaction of the model instance + * + * @method resetTransaction + */ + resetTransaction () { + this.transaction = null + } + /** * initiates and save a model instance with given * values. Create is a short to new Model and diff --git a/src/Lucid/Model/proxyHandler.js b/src/Lucid/Model/proxyHandler.js index c351148c..6f55cc35 100644 --- a/src/Lucid/Model/proxyHandler.js +++ b/src/Lucid/Model/proxyHandler.js @@ -11,7 +11,7 @@ const proxyHandler = exports = module.exports = {} const NE = require('node-exceptions') -const targetProperties = ['$primaryKeyValue', 'original', 'attributes', 'relations', 'eagerLoad', 'frozen'] +const targetProperties = ['$primaryKeyValue', 'original', 'attributes', 'relations', 'eagerLoad', 'frozen', 'transaction'] /** * proxy handler for getting target properties.Here diff --git a/test/unit/lucid.spec.js b/test/unit/lucid.spec.js index 86495e38..16637539 100644 --- a/test/unit/lucid.spec.js +++ b/test/unit/lucid.spec.js @@ -1705,4 +1705,152 @@ describe('Lucid', function () { expect(reFetchUser.lastname).to.equal('foo') }) }) + + context('Model Transactions', function () { + it('should be able to rollback save operation', function * () { + const trx = yield Database.beginTransaction() + class User extends Model {} + User.bootIfNotBooted() + const user = new User() + user.fill({username: 'foo', firstname: 'Mr.Foo'}) + user.useTransaction(trx) + yield user.save() + trx.rollback() + const getUsers = yield User.all() + expect(getUsers.size()).to.equal(0) + }) + + it('should be able to commit save operation', function * () { + const trx = yield Database.beginTransaction() + class User extends Model {} + User.bootIfNotBooted() + const user = new User() + user.fill({username: 'foo', firstname: 'Mr.Foo'}) + user.useTransaction(trx) + yield user.save() + trx.commit() + const getUsers = yield User.all() + expect(getUsers.size()).to.equal(1) + }) + + it('should be able to rollback update operation', function * () { + class User extends Model {} + User.bootIfNotBooted() + const user = new User() + user.fill({username: 'foo', firstname: 'Mr.Foo'}) + yield user.save() + const getUsers = yield User.all() + expect(getUsers.size()).to.equal(1) + const trx = yield Database.beginTransaction() + user.lastname = 'Baz' + user.useTransaction(trx) + yield user.save() + trx.rollback() + const freshUser = yield user.fresh() + expect(freshUser.lastname).to.equal(null) + }) + + it('should be able to commit update operation', function * () { + class User extends Model {} + User.bootIfNotBooted() + const user = new User() + user.fill({username: 'foo', firstname: 'Mr.Foo'}) + yield user.save() + const getUsers = yield User.all() + expect(getUsers.size()).to.equal(1) + const trx = yield Database.beginTransaction() + user.lastname = 'Baz' + user.useTransaction(trx) + yield user.save() + trx.commit() + const freshUser = yield user.fresh() + expect(freshUser.lastname).to.equal('Baz') + }) + + it('should not make use of same transaction after resetTransaction method', function * () { + const trx = yield Database.beginTransaction() + class User extends Model {} + User.bootIfNotBooted() + const user = new User() + user.fill({username: 'foo', firstname: 'Mr.Foo'}) + user.useTransaction(trx) + yield user.save() + trx.commit() + const getUsers = yield User.all() + expect(getUsers.size()).to.equal(1) + user.lastname = 'Baz' + user.resetTransaction() + yield user.save() + const freshUser = yield user.fresh() + expect(freshUser.lastname).to.equal('Baz') + }) + + it('should be able to rollback delete operation', function * () { + class User extends Model {} + User.bootIfNotBooted() + const user = new User() + user.fill({username: 'foo', firstname: 'Mr.Foo'}) + yield user.save() + const trx = yield Database.beginTransaction() + user.useTransaction(trx) + yield user.delete() + trx.rollback() + const getUsers = yield User.all() + expect(getUsers.size()).to.equal(1) + }) + + it('should be able to commit delete operation', function * () { + class User extends Model {} + User.bootIfNotBooted() + const user = new User() + user.fill({username: 'foo', firstname: 'Mr.Foo'}) + yield user.save() + const trx = yield Database.beginTransaction() + user.useTransaction(trx) + yield user.delete() + trx.commit() + const getUsers = yield User.all() + expect(getUsers.size()).to.equal(0) + }) + + it('should be able to rollback restore operation', function * () { + class User extends Model { + static get deleteTimestamp () { + return 'deleted_at' + } + } + User.bootIfNotBooted() + const user = new User() + user.fill({username: 'foo', firstname: 'Mr.Foo'}) + yield user.save() + yield user.delete() + const freshUser = yield User.query().withTrashed().first() + const trx = yield Database.beginTransaction() + freshUser.useTransaction(trx) + yield freshUser.restore() + trx.rollback() + const getUsers = yield User.all() + expect(getUsers.size()).to.equal(0) + }) + + it('should be able to commit restore operation', function * () { + class User extends Model { + static get deleteTimestamp () { + return 'deleted_at' + } + } + User.bootIfNotBooted() + const user = new User() + user.fill({username: 'foo', firstname: 'Mr.Foo'}) + yield user.save() + yield user.delete() + const freshUser = yield User.query().withTrashed().first() + const trx = yield Database.beginTransaction() + freshUser.useTransaction(trx) + yield freshUser.restore() + trx.commit() + const getUsers = yield User.all() + expect(getUsers.size()).to.equal(1) + }) + }) }) From bc33105c2ea3000b7617a59b093697b7d7944436 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 15 Aug 2016 02:42:53 +0530 Subject: [PATCH 8/8] chore(release): release 3.0.4 --- CHANGELOG.md | 19 +++++++++++++++++++ package.json | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44adb47d..4e13b554 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ + +## [3.0.4](https://github.com/adonisjs/adonis-lucid/compare/v3.0.3...v3.0.4) (2016-08-14) + + +### Bug Fixes + +* **lucid:relations:** keep relation output consistent ([c74e081](https://github.com/adonisjs/adonis-lucid/commit/c74e081)), closes [#45](https://github.com/adonisjs/adonis-lucid/issues/45) + + +### Features + +* **lucid:** add fresh method to grab fresh instance ([4d72794](https://github.com/adonisjs/adonis-lucid/commit/4d72794)) +* **lucid:** add static truncate ([87f16a2](https://github.com/adonisjs/adonis-lucid/commit/87f16a2)) +* **lucid:** add support for fill method ([fee8e31](https://github.com/adonisjs/adonis-lucid/commit/fee8e31)) +* **lucid:** add support for findByOrFail ([2ec6a52](https://github.com/adonisjs/adonis-lucid/commit/2ec6a52)) +* **lucid:** add support for transactions ([59cfa02](https://github.com/adonisjs/adonis-lucid/commit/59cfa02)), closes [#43](https://github.com/adonisjs/adonis-lucid/issues/43) + + + ## [3.0.3](https://github.com/adonisjs/adonis-lucid/compare/v3.0.2...v3.0.3) (2016-08-12) diff --git a/package.json b/package.json index ead762ba..dd457011 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "directories": { "test": "test" }, - "version": "3.0.3", + "version": "3.0.4", "scripts": { "test": "npm run lint && node --harmony_proxies ./node_modules/.bin/istanbul cover _mocha --report lcovonly -- -R spec test/unit test/acceptance && cat ./coverage/lcov.info | coveralls && rm -rf ./coverage", "lint": "standard src/**/*.js src/**/**/*.js src/**/**/**/*.js lib/*.js test/**/*.js providers/*.js",