diff --git a/README.md b/README.md index 15f3aac..6f3f992 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: @@ -275,15 +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 + - `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':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.find({lastName: 'Smith'}, {populate: ['address', 'spouse']})` populates only 'address' and 'spouse' in `Person` object + - `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 f30af1e..762a348 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 {Boolean|Array|Object} fields * @returns {Promise} */ static populate(docs, fields) { @@ -372,7 +373,8 @@ class BaseDocument { _.keys(anInstance._schema).forEach(function(key) { // Only populate specified fields - if (isArray(fields) && fields.indexOf(key) < 0) { + if ((isArray(fields) && fields.indexOf(key) < 0) || + (isObject(fields) && fields.constructor === Object && !fields.hasOwnProperty(key))) { return; } @@ -441,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: false }) + let p = type.find({ '_id': { $in: keyIds } }, { populate: deepPopulate }) .then(function(dereferences) { // Assign each dereferenced object to parent @@ -594,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 0caf53e..82a9737 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,8 @@ class Document extends BaseDocument { } let doc = that._fromData(data); - if (populate === true || (isArray(populate) && populate.length > 0)) { + if (populate === true || (isArray(populate) && populate.length > 0) || + (isObject(populate) && populate.constructor === Object && !_.isEmpty(populate))) { return that.populate(doc, populate); } @@ -354,7 +356,8 @@ class Document extends BaseDocument { let docs = that._fromData(datas); if (options.populate === true || - (isArray(options.populate) && options.populate.length > 0)) { + (isArray(options.populate) && options.populate.length > 0) || + (isObject(options.populate) && options.populate.constructor === Object && !_.isEmpty(options.populate))) { return that.populate(docs, options.populate); } @@ -413,7 +416,7 @@ class Document extends BaseDocument { instancesArray[i]._id = null; } }*/ - + return instances; } @@ -425,7 +428,7 @@ class Document extends BaseDocument { static clearCollection() { return DB().clearCollection(this.collectionName()); } - + } -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 4282d08..e278b21 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) { @@ -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; } @@ -65,6 +74,16 @@ describe('Client', function() { } } + class Breed extends Document { + constructor() { + + super(); + + this.name = String; + + } + } + class Pet extends Document { constructor() { super(); @@ -72,6 +91,7 @@ describe('Client', function() { this.schema({ type: String, name: String, + breed: Breed }); } } @@ -106,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 }); @@ -138,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 }); @@ -170,7 +188,6 @@ describe('Client', function() { it('should populate specified fields', function(done) { let address = Address.create({ street: '123 Fake St.', - city: 'Cityville', zipCode: 12345 }); @@ -198,6 +215,55 @@ describe('Client', function() { 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 + }); + + let germanShepherd = Breed.create({ + name: 'German Shepherd' + }); + + let dog = Pet.create({ + type: 'dog', + name: 'Fido', + breed: germanShepherd + }); + + let user = User.create({ + firstName: 'Billy', + lastName: 'Bob', + pet: dog, + address: address + }); + + Promise.all([city.save(), germanShepherd.save()]).then(function() { + validateId(city); + validateId(germanShepherd); + 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, 'address':['city']}}); + }).then(function(u) { + expect(u.pet).to.be.an.instanceof(Pet); + expect(isNativeId(u.pet.breed)).to.be.true; + expect(u.address).to.be.an.instanceof(Address); + expect(u.address.city).to.be.an.instanceof(City); + }).then(done, done); + }); }); describe('#findOneAndUpdate()', function() { @@ -288,7 +354,7 @@ describe('Client', function() { validateId(SouthPark); validateId(Quahog); done(); - }); + }); }); it('should load multiple objects from the collection', function(done) { @@ -387,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 }); @@ -429,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 }); @@ -471,7 +535,6 @@ describe('Client', function() { it('should populate specified fields', function(done) { let address = Address.create({ street: '123 Fake St.', - city: 'Cityville', zipCode: 12345 }); @@ -509,6 +572,66 @@ describe('Client', function() { 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 + }); + + let germanShepherd = Breed.create({ + name: 'German Shepherd' + }); + + let dog = Pet.create({ + type: 'dog', + name: 'Fido', + breed: germanShepherd + }); + + 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([city.save(), germanShepherd.save()]).then(function() { + validateId(city); + validateId(germanShepherd); + 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, '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(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(users[1].address).to.be.an.instanceof(Address); + expect(isNativeId(users[1].address.city)).to.be.true; + }).then(done, done); + }); }); describe('#count()', function() { @@ -630,4 +753,4 @@ describe('Client', function() { }); -}); \ No newline at end of file +});