From f8cd88f84f477819d0b4b975ddff22db5bc8c33f Mon Sep 17 00:00:00 2001 From: Diego Nogueira Teixeira Date: Thu, 13 Apr 2017 16:17:11 -0300 Subject: [PATCH 1/3] Changed populate option strategy to accept deep populate --- lib/base-document.js | 17 ++++++++------- lib/document.js | 14 ++++++------- test/client.test.js | 50 +++++++++++++++++++++++++++++++++++--------- 3 files changed, 56 insertions(+), 25 deletions(-) diff --git a/lib/base-document.js b/lib/base-document.js index f30af1e..4cac818 100644 --- a/lib/base-document.js +++ b/lib/base-document.js @@ -8,6 +8,7 @@ const isValidType = require('./validate').isValidType; const isEmptyValue = require('./validate').isEmptyValue; const isInChoices = require('./validate').isInChoices; const isArray = require('./validate').isArray; +const isObject = require('./validate').isObject; const isDocument = require('./validate').isDocument; const isEmbeddedDocument = require('./validate').isEmbeddedDocument; const isString = require('./validate').isString; @@ -24,7 +25,7 @@ const normalizeType = function(property) { } else if (isSupportedType(property)) { typeDeclaration.type = property; } else { - throw new Error('Unsupported type or bad variable. ' + + throw new Error('Unsupported type or bad variable. ' + 'Remember, non-persisted objects must start with an underscore (_). Got:', property); } @@ -239,10 +240,10 @@ class BaseDocument { _.keys(that._schema).forEach(function(key) { let value = that[key]; - + if (that._schema[key].type === Date && isDate(value)) { that[key] = new Date(value); - } else if (value !== null && value !== undefined && + } else if (value !== null && value !== undefined && value.documentClass && value.documentClass() === 'embedded') { // TODO: This should probably be in Document, not BaseDocument value.canonicalize(); @@ -281,7 +282,7 @@ class BaseDocument { return instance; } - // TODO: Should probably move some of this to + // TODO: Should probably move some of this to // Embedded and Document classes since Base shouldn't // need to know about child classes static _fromData(datas) { @@ -311,7 +312,7 @@ class BaseDocument { if (type.documentClass && type.documentClass() === 'embedded') { // Initialize EmbeddedDocument instance[key] = type._fromData(value); - } else if (isArray(type) && type.length > 0 && + } else if (isArray(type) && type.length > 0 && type[0].documentClass && type[0].documentClass() === 'embedded') { // Initialize array of EmbeddedDocuments instance[key] = []; @@ -346,7 +347,7 @@ class BaseDocument { * * TODO : EMBEDDED * @param {Array|Document} docs - * @param {Array} fields + * @param {Object} fields * @returns {Promise} */ static populate(docs, fields) { @@ -372,7 +373,7 @@ class BaseDocument { _.keys(anInstance._schema).forEach(function(key) { // Only populate specified fields - if (isArray(fields) && fields.indexOf(key) < 0) { + if (isObject(fields) && !fields.hasOwnProperty(key)) { return; } @@ -442,7 +443,7 @@ class BaseDocument { } // Bulk load dereferences - let p = type.find({ '_id': { $in: keyIds } }, { populate: false }) + let p = type.find({ '_id': { $in: keyIds } }, { populate: fields[key] }) .then(function(dereferences) { // Assign each dereferenced object to parent diff --git a/lib/document.js b/lib/document.js index 0caf53e..0dad4be 100644 --- a/lib/document.js +++ b/lib/document.js @@ -6,6 +6,7 @@ const DB = require('./clients').getClient; const BaseDocument = require('./base-document'); const isSupportedType = require('./validate').isSupportedType; const isArray = require('./validate').isArray; +const isObject = require('./validate').isObject; const isReferenceable = require('./validate').isReferenceable; const isEmbeddedDocument = require('./validate').isEmbeddedDocument; const isString = require('./validate').isString; @@ -156,7 +157,7 @@ class Document extends BaseDocument { */ delete() { const that = this; - + let preDeletePromises = that._getHookPromises('preDelete'); return Promise.all(preDeletePromises).then(function() { @@ -191,7 +192,7 @@ class Document extends BaseDocument { if (query === undefined || query === null) { query = {}; } - + return DB().deleteMany(this.collectionName(), query); } @@ -226,7 +227,7 @@ class Document extends BaseDocument { } let doc = that._fromData(data); - if (populate === true || (isArray(populate) && populate.length > 0)) { + if (populate === true || (isObject(populate) && !_.isEmpty(populate))) { return that.populate(doc, populate); } @@ -353,8 +354,7 @@ class Document extends BaseDocument { .then(function(datas) { let docs = that._fromData(datas); - if (options.populate === true || - (isArray(options.populate) && options.populate.length > 0)) { + if (options.populate === true || (isObject(options.populate) && !_.isEmpty(options.populate))) { return that.populate(docs, options.populate); } @@ -413,7 +413,7 @@ class Document extends BaseDocument { instancesArray[i]._id = null; } }*/ - + return instances; } @@ -425,7 +425,7 @@ class Document extends BaseDocument { static clearCollection() { return DB().clearCollection(this.collectionName()); } - + } module.exports = Document; \ No newline at end of file diff --git a/test/client.test.js b/test/client.test.js index 4282d08..1d8a267 100644 --- a/test/client.test.js +++ b/test/client.test.js @@ -37,7 +37,7 @@ describe('Client', function() { after(function(done) { database.dropDatabase().then(function() {}).then(done, done); - }); + }); describe('#save()', function() { it('should persist the object and its members to the database', function(done) { @@ -65,6 +65,16 @@ describe('Client', function() { } } + class Breed extends Document { + constructor() { + + super(); + + this.name = String; + + } + } + class Pet extends Document { constructor() { super(); @@ -72,6 +82,7 @@ describe('Client', function() { this.schema({ type: String, name: String, + breed: Breed }); } } @@ -174,9 +185,14 @@ describe('Client', function() { zipCode: 12345 }); + let germanShepherd = Breed.create({ + name: 'German Shepherd' + }); + let dog = Pet.create({ type: 'dog', name: 'Fido', + breed: germanShepherd }); let user = User.create({ @@ -186,15 +202,19 @@ describe('Client', function() { address: address }); - Promise.all([address.save(), dog.save()]).then(function() { + Promise.all([address.save(), germanShepherd.save()]).then(function() { validateId(address); - validateId(dog); - return user.save(); + validateId(germanShepherd); + return dog.save().then(function() { + validateId(dog); + return user.save(); + }) }).then(function() { validateId(user); - return User.findOne({_id: user._id}, {populate: ['pet']}); + return User.findOne({_id: user._id}, {populate: {'pet': false}}); }).then(function(u) { expect(u.pet).to.be.an.instanceof(Pet); + expect(isNativeId(u.pet.breed)).to.be.true; expect(isNativeId(u.address)).to.be.true; }).then(done, done); }); @@ -288,7 +308,7 @@ describe('Client', function() { validateId(SouthPark); validateId(Quahog); done(); - }); + }); }); it('should load multiple objects from the collection', function(done) { @@ -475,9 +495,14 @@ describe('Client', function() { zipCode: 12345 }); + let germanShepherd = Breed.create({ + name: 'German Shepherd' + }); + let dog = Pet.create({ type: 'dog', name: 'Fido', + breed: germanShepherd }); let user1 = User.create({ @@ -494,18 +519,23 @@ describe('Client', function() { address: address }); - Promise.all([address.save(), dog.save()]).then(function() { + Promise.all([address.save(), germanShepherd.save()]).then(function() { validateId(address); - validateId(dog); - return Promise.all([user1.save(), user2.save()]); + validateId(germanShepherd); + return dog.save().then(function() { + validateId(dog); + return Promise.all([user1.save(), user2.save()]); + }) }).then(function() { validateId(user1); validateId(user2); - return User.find({}, {populate: ['pet']}); + return User.find({}, {populate: {'pet': true}}); }).then(function(users) { expect(users[0].pet).to.be.an.instanceof(Pet); + expect(users[0].pet.breed).to.be.an.instanceof(Breed); expect(isNativeId(users[0].address)).to.be.true; expect(users[1].pet).to.be.an.instanceof(Pet); + expect(users[1].pet.breed).to.be.an.instanceof(Breed); expect(isNativeId(users[1].address)).to.be.true; }).then(done, done); }); From f3a07ac0c58ced78627983ab1a40213df672045b Mon Sep 17 00:00:00 2001 From: Diego Nogueira Teixeira Date: Thu, 13 Apr 2017 16:45:10 -0300 Subject: [PATCH 2/3] Updated changelog, docs a package for new version --- CHANGELOG.md | 6 +++++- README.md | 10 +++++++--- package.json | 2 +- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04e7649..9c855be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.13.0 (2017-04-13) +Features: + - Added support for deep populate option ([#13](https://github.com/scottwrobinson/camo/issues/13)) + ## 0.12.3 (2016-11-04) Bugfixes: - Merged PR that allows changes to persist in `postValidate` and `preSave` hooks ([#85](https://github.com/scottwrobinson/camo/pull/85)). Fixes [#43](https://github.com/scottwrobinson/camo/pull/43). @@ -39,7 +43,7 @@ Bugfixes: - Improved some validation tests - Fixed min validation - Fixed validation for array of embedded documents - - Moved `collectionName` method to `BaseDocument` so `EmbeddedDocument` can use it ([#26](https://github.com/scottwrobinson/camo/issues/26)) + - Moved `collectionName` method to `BaseDocument` so `EmbeddedDocument` can use it ([#26](https://github.com/scottwrobinson/camo/issues/26)) - Deprecated `id` alias from document object ([#20](https://github.com/scottwrobinson/camo/issues/20)) - Fixed serialization test for MongoDB IDs diff --git a/README.md b/README.md index 15f3aac..bb78e8c 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ For a short tutorial on using Camo, check out [this](http://stackabuse.com/getti ### Connect to the Database Before using any document methods, you must first connect to your underlying database. All supported databases have their own unique URI string used for connecting. The URI string usually describes the network location or file location of the database. However, some databases support more than just network or file locations. NeDB, for example, supports storing data in-memory, which can be specified to Camo via `nedb://memory`. See below for details: -- MongoDB: +- MongoDB: - Format: mongodb://[username:password@]host[:port][/db-name] - Example: `var uri = 'mongodb://scott:abc123@localhost:27017/animals';` - NeDB: @@ -277,13 +277,17 @@ Dog.findOne({ name: 'Lassie' }).then(function(l) { - `populate`: Boolean value to load all or no references. Pass an array of field names to only populate the specified references - `Person.findOne({name: 'Billy'}, {populate: true})` populates all references in `Person` object - - `Person.findOne({name: 'Billy'}, {populate: ['address', 'spouse']})` populates only 'address' and 'spouse' in `Person` object + - `Person.findOne({name: 'Billy'}, {populate: {'address':false, 'spouse':false}})` populates only 'address' and 'spouse' in `Person` object, but not its references. + - `Person.findOne({name: 'Billy'}, {populate: {'address':true, 'spouse':true}})` populates only 'address' and 'spouse' in `Person` object, and all its references. + - `Person.findOne({name: 'Billy'}, {populate: {'address': {'city':false}}})` populates only 'address' in `Person` object, and 'city' in `Address` object, but none of the `City` references. `.find()` currently accepts the following options: - `populate`: Boolean value to load all or no references. Pass an array of field names to only populate the specified references - `Person.find({lastName: 'Smith'}, {populate: true})` populates all references in `Person` object - - `Person.find({lastName: 'Smith'}, {populate: ['address', 'spouse']})` populates only 'address' and 'spouse' in `Person` object + - `Person.findOne({name: 'Billy'}, {populate: {'address':false, 'spouse':false}})` populates only 'address' and 'spouse' in `Person` object, but not its references. + - `Person.findOne({name: 'Billy'}, {populate: {'address':true, 'spouse':true}})` populates only 'address' and 'spouse' in `Person` object, and all its references. + - `Person.findOne({name: 'Billy'}, {populate: {'address': {'city':false}}})` populates only 'address' in `Person` object, and 'city' in `Address` object, but none of the `City` references. - `sort`: Sort the documents by the given field(s) - `Person.find({}, {sort: '-age'})` sorts by age in descending order - `Person.find({}, {sort: ['age', 'name']})` sorts by ascending age and then name, alphabetically diff --git a/package.json b/package.json index c480b77..5b825f6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "camo", - "version": "0.12.3", + "version": "0.13.0", "description": "A class-based ES6 ODM for Mongo-like databases.", "author": { "name": "Scott Robinson", From 094927251586e48614ccc0b3f60df677444ff788 Mon Sep 17 00:00:00 2001 From: Diego Nogueira Teixeira Date: Thu, 13 Apr 2017 18:15:08 -0300 Subject: [PATCH 3/3] Improved to accept both array and object as populate option --- README.md | 16 ++++-- lib/base-document.js | 13 +++-- lib/document.js | 9 ++- test/client.test.js | 131 ++++++++++++++++++++++++++++++++++++------- 4 files changed, 137 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index bb78e8c..6f3f992 100644 --- a/README.md +++ b/README.md @@ -275,19 +275,23 @@ Dog.findOne({ name: 'Lassie' }).then(function(l) { `.findOne()` currently accepts the following option: -- `populate`: Boolean value to load all or no references. Pass an array of field names to only populate the specified references +- `populate`: Boolean value to load all or no references. Pass an array of field names to only populate the specified references or an Object to deeply populate specified references - `Person.findOne({name: 'Billy'}, {populate: true})` populates all references in `Person` object + - `Person.findOne({name: 'Billy'}, {populate: ['address', 'spouse']})` populates only 'address' and 'spouse' in `Person` object, but not its references. - `Person.findOne({name: 'Billy'}, {populate: {'address':false, 'spouse':false}})` populates only 'address' and 'spouse' in `Person` object, but not its references. - `Person.findOne({name: 'Billy'}, {populate: {'address':true, 'spouse':true}})` populates only 'address' and 'spouse' in `Person` object, and all its references. - - `Person.findOne({name: 'Billy'}, {populate: {'address': {'city':false}}})` populates only 'address' in `Person` object, and 'city' in `Address` object, but none of the `City` references. + - `Person.findOne({name: 'Billy'}, {populate: {'address': {'city':true}}})` populates only 'address' in `Person` object, and 'city' in `Address` object, and all of the `City` references. + - `Person.findOne({name: 'Billy'}, {populate: {'address': ['city']}})` populates only 'address' in `Person` object, and 'city' in `Address` object, but none of the `City` references. `.find()` currently accepts the following options: -- `populate`: Boolean value to load all or no references. Pass an array of field names to only populate the specified references +- `populate`: Boolean value to load all or no references. Pass an array of field names to only populate the specified references or an Object to deeply populate specified references - `Person.find({lastName: 'Smith'}, {populate: true})` populates all references in `Person` object - - `Person.findOne({name: 'Billy'}, {populate: {'address':false, 'spouse':false}})` populates only 'address' and 'spouse' in `Person` object, but not its references. - - `Person.findOne({name: 'Billy'}, {populate: {'address':true, 'spouse':true}})` populates only 'address' and 'spouse' in `Person` object, and all its references. - - `Person.findOne({name: 'Billy'}, {populate: {'address': {'city':false}}})` populates only 'address' in `Person` object, and 'city' in `Address` object, but none of the `City` references. + - `Person.findOne({lastName: 'Smith'}, {populate: ['address', 'spouse']})` populates only 'address' and 'spouse' in `Person` object, but not its references. + - `Person.findOne({lastName: 'Smith'}, {populate: {'address':false, 'spouse':false}})` populates only 'address' and 'spouse' in `Person` object, but not its references. + - `Person.findOne({lastName: 'Smith'}, {populate: {'address':true, 'spouse':true}})` populates only 'address' and 'spouse' in `Person` object, and all its references. + - `Person.findOne({lastName: 'Smith'}, {populate: {'address': {'city':true}}})` populates only 'address' in `Person` object, and 'city' in `Address` object, and all of the `City` references. + - `Person.findOne({lastName: 'Smith'}, {populate: {'address': ['city']}})` populates only 'address' in `Person` object, and 'city' in `Address` object, but none of the `City` references. - `sort`: Sort the documents by the given field(s) - `Person.find({}, {sort: '-age'})` sorts by age in descending order - `Person.find({}, {sort: ['age', 'name']})` sorts by ascending age and then name, alphabetically diff --git a/lib/base-document.js b/lib/base-document.js index 4cac818..762a348 100644 --- a/lib/base-document.js +++ b/lib/base-document.js @@ -347,7 +347,7 @@ class BaseDocument { * * TODO : EMBEDDED * @param {Array|Document} docs - * @param {Object} fields + * @param {Boolean|Array|Object} fields * @returns {Promise} */ static populate(docs, fields) { @@ -373,7 +373,8 @@ class BaseDocument { _.keys(anInstance._schema).forEach(function(key) { // Only populate specified fields - if (isObject(fields) && !fields.hasOwnProperty(key)) { + if ((isArray(fields) && fields.indexOf(key) < 0) || + (isObject(fields) && fields.constructor === Object && !fields.hasOwnProperty(key))) { return; } @@ -442,8 +443,12 @@ class BaseDocument { type = anInstance._schema[key].type; } + let deepPopulate = false; + if(isObject(fields) && fields.constructor === Object) + deepPopulate = fields[key]; + // Bulk load dereferences - let p = type.find({ '_id': { $in: keyIds } }, { populate: fields[key] }) + let p = type.find({ '_id': { $in: keyIds } }, { populate: deepPopulate }) .then(function(dereferences) { // Assign each dereferenced object to parent @@ -595,4 +600,4 @@ class BaseDocument { } } -module.exports = BaseDocument; \ No newline at end of file +module.exports = BaseDocument; diff --git a/lib/document.js b/lib/document.js index 0dad4be..82a9737 100644 --- a/lib/document.js +++ b/lib/document.js @@ -227,7 +227,8 @@ class Document extends BaseDocument { } let doc = that._fromData(data); - if (populate === true || (isObject(populate) && !_.isEmpty(populate))) { + if (populate === true || (isArray(populate) && populate.length > 0) || + (isObject(populate) && populate.constructor === Object && !_.isEmpty(populate))) { return that.populate(doc, populate); } @@ -354,7 +355,9 @@ class Document extends BaseDocument { .then(function(datas) { let docs = that._fromData(datas); - if (options.populate === true || (isObject(options.populate) && !_.isEmpty(options.populate))) { + if (options.populate === true || + (isArray(options.populate) && options.populate.length > 0) || + (isObject(options.populate) && options.populate.constructor === Object && !_.isEmpty(options.populate))) { return that.populate(docs, options.populate); } @@ -428,4 +431,4 @@ class Document extends BaseDocument { } -module.exports = Document; \ No newline at end of file +module.exports = Document; diff --git a/test/client.test.js b/test/client.test.js index 1d8a267..e278b21 100644 --- a/test/client.test.js +++ b/test/client.test.js @@ -51,12 +51,21 @@ describe('Client', function() { }); }); + class City extends Document { + constructor() { + super(); + + this.name = String; + this.county = String; + } + } + class Address extends Document { constructor() { super(); this.street = String; - this.city = String; + this.city = City; this.zipCode = Number; } @@ -117,7 +126,6 @@ describe('Client', function() { it('should populate all fields', function(done) { let address = Address.create({ street: '123 Fake St.', - city: 'Cityville', zipCode: 12345 }); @@ -149,7 +157,6 @@ describe('Client', function() { it('should not populate any fields', function(done) { let address = Address.create({ street: '123 Fake St.', - city: 'Cityville', zipCode: 12345 }); @@ -181,7 +188,44 @@ describe('Client', function() { it('should populate specified fields', function(done) { let address = Address.create({ street: '123 Fake St.', - city: 'Cityville', + zipCode: 12345 + }); + + let dog = Pet.create({ + type: 'dog', + name: 'Fido', + }); + + let user = User.create({ + firstName: 'Billy', + lastName: 'Bob', + pet: dog, + address: address + }); + + Promise.all([address.save(), dog.save()]).then(function() { + validateId(address); + validateId(dog); + return user.save(); + }).then(function() { + validateId(user); + return User.findOne({_id: user._id}, {populate: ['pet']}); + }).then(function(u) { + expect(u.pet).to.be.an.instanceof(Pet); + expect(isNativeId(u.address)).to.be.true; + }).then(done, done); + }); + + it('should deeply populate specified fields', function(done) { + + let city = City.create({ + name: 'Cityville', + county: 'County' + }); + + let address = Address.create({ + street: '123 Fake St.', + city: city, zipCode: 12345 }); @@ -202,20 +246,22 @@ describe('Client', function() { address: address }); - Promise.all([address.save(), germanShepherd.save()]).then(function() { - validateId(address); + Promise.all([city.save(), germanShepherd.save()]).then(function() { + validateId(city); validateId(germanShepherd); - return dog.save().then(function() { + return Promise.all([address.save(), dog.save()]).then(function() { + validateId(address); validateId(dog); return user.save(); }) }).then(function() { validateId(user); - return User.findOne({_id: user._id}, {populate: {'pet': false}}); + return User.findOne({_id: user._id}, {populate: {'pet': false, 'address':['city']}}); }).then(function(u) { expect(u.pet).to.be.an.instanceof(Pet); expect(isNativeId(u.pet.breed)).to.be.true; - expect(isNativeId(u.address)).to.be.true; + expect(u.address).to.be.an.instanceof(Address); + expect(u.address.city).to.be.an.instanceof(City); }).then(done, done); }); }); @@ -407,7 +453,6 @@ describe('Client', function() { it('should populate all fields', function(done) { let address = Address.create({ street: '123 Fake St.', - city: 'Cityville', zipCode: 12345 }); @@ -449,7 +494,6 @@ describe('Client', function() { it('should not populate any fields', function(done) { let address = Address.create({ street: '123 Fake St.', - city: 'Cityville', zipCode: 12345 }); @@ -491,7 +535,53 @@ describe('Client', function() { it('should populate specified fields', function(done) { let address = Address.create({ street: '123 Fake St.', - city: 'Cityville', + zipCode: 12345 + }); + + let dog = Pet.create({ + type: 'dog', + name: 'Fido', + }); + + let user1 = User.create({ + firstName: 'Billy', + lastName: 'Bob', + pet: dog, + address: address + }); + + let user2 = User.create({ + firstName: 'Sally', + lastName: 'Bob', + pet: dog, + address: address + }); + + Promise.all([address.save(), dog.save()]).then(function() { + validateId(address); + validateId(dog); + return Promise.all([user1.save(), user2.save()]); + }).then(function() { + validateId(user1); + validateId(user2); + return User.find({}, {populate: ['pet']}); + }).then(function(users) { + expect(users[0].pet).to.be.an.instanceof(Pet); + expect(isNativeId(users[0].address)).to.be.true; + expect(users[1].pet).to.be.an.instanceof(Pet); + expect(isNativeId(users[1].address)).to.be.true; + }).then(done, done); + }); + + it('should deeply populate specified fields', function(done) { + let city = City.create({ + name: 'Cityville', + county: 'County' + }); + + let address = Address.create({ + street: '123 Fake St.', + city: city, zipCode: 12345 }); @@ -519,24 +609,27 @@ describe('Client', function() { address: address }); - Promise.all([address.save(), germanShepherd.save()]).then(function() { - validateId(address); + Promise.all([city.save(), germanShepherd.save()]).then(function() { + validateId(city); validateId(germanShepherd); - return dog.save().then(function() { + return Promise.all([address.save(), dog.save()]).then(function() { + validateId(address); validateId(dog); return Promise.all([user1.save(), user2.save()]); }) }).then(function() { validateId(user1); validateId(user2); - return User.find({}, {populate: {'pet': true}}); + return User.find({}, {populate: {'pet': true, 'address': false}}); }).then(function(users) { expect(users[0].pet).to.be.an.instanceof(Pet); expect(users[0].pet.breed).to.be.an.instanceof(Breed); - expect(isNativeId(users[0].address)).to.be.true; + expect(users[0].address).to.be.an.instanceof(Address); + expect(isNativeId(users[0].address.city)).to.be.true; expect(users[1].pet).to.be.an.instanceof(Pet); expect(users[1].pet.breed).to.be.an.instanceof(Breed); - expect(isNativeId(users[1].address)).to.be.true; + expect(users[1].address).to.be.an.instanceof(Address); + expect(isNativeId(users[1].address.city)).to.be.true; }).then(done, done); }); }); @@ -660,4 +753,4 @@ describe('Client', function() { }); -}); \ No newline at end of file +});