UNPKG

mongoose-deep-populate

Version:

Mongoose plugin to enable deep population of nested models

624 lines (547 loc) 19 kB
var _ = require('lodash') , expect = require('chai').expect , async = require('async') , mongoose = require('mongoose') , Schema = mongoose.Schema , deepPopulate = require('../index')(mongoose) describe('mongoose-deep-populate', function () { /*==============================================* * Bugs *==============================================*/ describe('Bugs', function () { describe('Bug #23 (Mongoose 4.1 and up only)', function () { var MPromise = mongoose.Promise before(function () { mongoose.Promise = Promise // ES6 Promise }) after(function () { mongoose.Promise = MPromise }) it('fixes bug #23', function (cb) { var dbUrl = process.env.TEST_DB , connection = mongoose.createConnection(dbUrl) var FooSchema = new Schema({ name: String }) var Foo = connection.model('Foo', FooSchema) var BarSchema = new Schema({ foo: { type: Schema.Types.ObjectId, ref: 'Foo' }, }) var Bar = connection.model('Bar', BarSchema) BarSchema.plugin(deepPopulate) var foo = new Foo({ name: 'foo' }) var bar = new Bar({ foo: foo._id }) var promise = foo.save().then(function () { return bar.save() }).then(function () { return Bar.findOne({ _id: bar._id }).deepPopulate('foo') }).then(function (bar) { expect(bar.foo.name).to.equal(foo.name) cb() }, cb) }) }) describe('Bug #12', function () { it('fixes bug #12', function (cb) { var dbUrl = process.env.TEST_DB , connection = mongoose.createConnection(dbUrl) var UserSchema = new Schema({ items: [{ type: Schema.Types.ObjectId, ref: 'Item.bug12' }] }) UserSchema.plugin(deepPopulate) var User = connection.model('User.bug12', UserSchema) var ItemSchema = new Schema({ seller: { type: Schema.Types.ObjectId, ref: 'User.bug12' } }) ItemSchema.plugin(deepPopulate) var Item = connection.model('Item.bug12', ItemSchema) var user = new User() var item = new Item({seller: user}) user.items.addToSet(item) user.save(function (err) { if (err) return cb(err) item.save(function (err) { if (err) return cb(err) user.deepPopulate('items.seller', function (err) { if (err) return cb(err) expect(user.equals(user.items[0].seller)) cb() }) }) }) }) }) }) var UserSchema, User , CommentSchema, Comment , PostSchema, Post , nextModelVersion = 0 /*==============================================* * Specific behaviors of static call pattern *==============================================*/ describe('[static] Specific behaviors', function () { before(setup) it('passes through null or empty document array', function (cb) { async.parallel([ function (cb) { Post.deepPopulate(null, 'comments', function (err, docs) { expect(docs).to.be.null cb() }) }, function (cb) { Post.deepPopulate(undefined, 'comments', function (err, docs) { expect(docs).to.be.undefined cb() }) }, function (cb) { var docs = [] Post.deepPopulate(docs, 'comments', function (err, _docs) { expect(_docs).to.equal(docs) cb() }) } ], cb) }) it('populates the same document array', function (cb) { Post.find({_id: 1}, function (err, docs) { if (err) return cb(err) Post.deepPopulate(docs, 'comments', function (err, _docs) { if (err) return cb(err) expect(_docs).to.equal(docs) cb() }) }) }) }) /*==============================================* * Specific behaviors of instance call pattern *==============================================*/ describe('[instance] Specific behaviors', function () { before(setup) it('populates the same document', function (cb) { Post.findById(1, function (err, doc) { if (err) return cb(err) doc.deepPopulate('comments', function (err, _doc) { if (err) return cb(err) expect(_doc).to.equal(doc) cb() }) }) }) }) /*==============================================* * Specific behaviors of Query call pattern *==============================================*/ describe('[query] Specific behaviors', function () { before(setup) it('passes in undefined if no document is found', function (cb) { async.parallel([ function (cb) { Post.find({_id: 'not exist'}).deepPopulate('comments').exec(function (err, docs) { expect(docs).to.be.undefined cb() }) }, function (cb) { Post.findOne({_id: 'not exist'}).deepPopulate('comments').exec(function (err, doc) { expect(doc).to.be.undefined cb() }) }, function (cb) { Post.findById('not exist').deepPopulate('comments').exec(function (err, doc) { expect(doc).to.be.undefined cb() }) } ], cb) }) it('supports `lean` query option', function (cb) { async.parallel([ function withoutLean(cb) { Post.findOne({}).deepPopulate('user comments').exec(function (err, doc) { checkType(doc, 'model', cb) }) }, function withLean(cb) { Post.findOne({}).deepPopulate('user comments').lean().exec(function (err, doc) { checkType(doc, 'Object', cb) }) }, function withLeanInvokedBeforeDeepPopulate(cb) { Post.findOne({}).lean().deepPopulate('user comments').exec(function (err, doc) { checkType(doc, 'Object', cb) }) }, ], cb) function checkType(doc, expectedType, cb) { expect(doc.constructor.name).to.equal(expectedType) expect(doc.user.constructor.name).to.equal(expectedType) doc.comments.forEach(function (comment) { expect(comment.constructor.name).to.equal(expectedType) }) cb() } }) }) /*==============================================* * Behaviors of all call patterns *==============================================*/ eachPopulationType(function (type, populateFn) { describe(type + ' Using default options', function () { before(setup) it('deeply populates a linked document', function (cb) { populateFn('user.manager', null, function (err, post) { if (err) return cb(err) check(post.user, true) check(post.user.manager, true) cb() }) }) it('deeply populates a linked document and works with an async callback', function (cb) { populateFn('user.manager', null, async function (err, post) { if (err) return cb(err) check(post.user, true) check(post.user.manager, true) cb() }) }) it('deeply populates a document array', function (cb) { populateFn('comments.user.manager', null, function (err, post) { if (err) return cb(err) post.comments.forEach(function (comment) { check(comment, true) check(comment.user, true) check(comment.user.manager, true) }) cb() }) }) it('deeply populates a document array which link back to original model', function (cb) { populateFn('reviewers.mainPage', null, function (err, post) { if (err) return cb(err) post.reviewers.forEach(function (reviewer) { check(reviewer, true) check(reviewer.mainPage, true) }) cb() }) }) it('deeply populates a subdocument', function (cb) { populateFn('approved.user.manager', null, function (err, post) { if (err) return cb(err) check(post.approved.user, true) check(post.approved.user.manager, true) cb() }) }) it('deeply populates a subdocument within a linked document (bug #29)', function (cb) { populateFn('approved.user.manager.mainPage.approved.user', null, function (err, post) { if (err) return cb(err) check(post.approved.user.manager.mainPage.approved.user, true) cb() }) }) it('deeply populates a subdocument array', function (cb) { populateFn('likes.user.manager', null, function (err, post) { if (err) return cb(err) post.likes.forEach(function (like) { check(like.user, true) check(like.user.manager, true) }) cb() }) }) it('supports multiple paths using space-delimited string', function (cb) { populateFn('user.manager comments.user.manager approved.user.manager likes.user.manager', null, function (err, post) { if (err) return cb(err) checkPost(post) cb() }) }) it('supports multiple paths using comma-delimited string', function (cb) { populateFn('user.manager,comments.user.manager,approved.user.manager,likes.user.manager', null, function (err, post) { if (err) return cb(err) checkPost(post) cb() }) }) it('supports multiple paths via array param', function (cb) { populateFn(['user.manager', 'comments.user.manager', 'approved.user.manager', 'likes.user.manager'], null, function (err, post) { if (err) return cb(err) checkPost(post) cb() }) }) it('ignores invalid paths', function (cb) { populateFn('invalid1 invalid2.invalid3 user', null, function (err, post) { if (err) return cb(err) check(post.user, true) cb() }) }) it('ignores null path', function (cb) { populateFn(null, null, cb) }) it('ignores null callback', function (cb) { if (type === 'static') { Post.deepPopulate() cb() } else Post.findOne({}, function (err, post) { if (err) return cb(err) post.deepPopulate() cb() }) }) }) describe(type + ' Using whitelist', function () { before(function (cb) { setup(cb, { whitelist: ['comments'] }) }) it('populates whitelisted paths', function (cb) { populateFn('comments', null, function (err, post) { if (err) return cb(err) post.comments.forEach(function (comment) { check(comment, true) }) cb() }) }) it('ignores nested non-whitelisted subpaths', function (cb) { populateFn('comments.user', null, function (err, post) { if (err) return cb(err) post.comments.forEach(function (comment) { check(comment, true) check(comment.user) }) cb() }) }) it('ignores non-whitelisted paths', function (cb) { populateFn('user', null, function (err, post) { if (err) return cb(err) check(post.user) cb() }) }) }) describe(type + ' Using populate options', function () { before(function (cb) { setup(cb, { populate: { comments : { select : 'user', options: { limit: 1 } }, 'comments.user': { select: 'manager' } } }) }) it('applies populate options for corresponding paths', function (cb) { populateFn('comments.user', null, function (err, post) { if (err) return cb(err) expect(post.comments.length).to.equal(1) post.comments.forEach(function (comment) { check(comment) check(comment.user) }) cb() }) }) }) describe(type + ' Using rewriting', function () { before(function (cb) { setup(cb, { rewrite: { author : 'user', approved: 'approved.user' }, whitelist: [ 'author', 'approved' ], populate: { author: { select: '-manager -_id' } } }) }) it('rewrites paths and whitelist', function (cb) { populateFn('author approved', null, function (err, post) { if (err) return cb(err) check(post.user, true) check(post.approved.user, true) cb() }) }) it('rewrites populate option paths', function (cb) { populateFn('author', null, function (err, post) { if (err) return cb(err) check(post.user, true) expect(post.user.manager && post.user._id).to.be.undefined cb() }) }) }) describe(type + ' Overriding options', function () { before(function (cb) { setup(cb, { whitelist: [], populate: {} }) }) it('use overriding options', function (cb) { var overridingOpts = { whitelist: ['comments.user'], populate: { comments : { select : 'user', options: { limit: 1 } }, 'comments.user': { select: 'manager' } } } populateFn('comments.user', overridingOpts, function (err, post) { if (err) return cb(err) expect(post.comments.length).to.equal(1) post.comments.forEach(function (comment) { check(comment) check(comment.user) }) cb() }) }) }) }) /*==============================================* * Helpers *==============================================*/ function setup(cb, options) { var dbUrl = process.env.TEST_DB , connection = mongoose.createConnection(dbUrl) , modelVersion = ++nextModelVersion UserSchema = new Schema({ _id : Number, manager: {type: Number, ref: 'User' + modelVersion}, mainPage: {type: Number, ref: 'Post' + modelVersion}, loaded : {type: Boolean, default: true} }) User = connection.model('User' + modelVersion, UserSchema) CommentSchema = new Schema({ _id : Number, loaded: {type: Boolean, default: true}, user : {type: Number, ref: 'User' + modelVersion} }) Comment = connection.model('Comment' + modelVersion, CommentSchema) PostSchema = new Schema({ _id : Number, loaded : {type: Boolean, default: true}, user : {type: Number, ref: 'User' + modelVersion}, // linked doc reviewers : [{type: Number, ref: 'User' + modelVersion}], // linked docs comments: [{type: Number, ref: 'Comment' + modelVersion}], // linked docs likes : [{user: {type: Number, ref: 'User' + modelVersion}}], // subdocs approved: {status: Boolean, user: {type: Number, ref: 'User' + modelVersion}} // subdoc }) PostSchema.plugin(deepPopulate, options) Post = connection.model('Post' + modelVersion, PostSchema) async.parallel([ User.create.bind(User, {_id: 1, manager: 2, mainPage: 1}), User.create.bind(User, {_id: 2, mainPage: 2}), Comment.create.bind(Comment, {_id: 1, user: 1}), Comment.create.bind(Comment, {_id: 2, user: 1}), Comment.create.bind(Comment, {_id: 3, user: 1}), Post.create.bind(Post, {_id: 1, user: 1, reviewers: [1, 2], comments: [1, 2], likes: [{user: 1}], approved: {user: 1}}), Post.create.bind(Post, {_id: 2, user: 1, reviewers: [1, 2], comments: [3], likes: [{user: 1}], approved: {user: 1}}) ], cb) } function eachPopulationType(cb) { function doneFn(cb) { return function (err, obj) { if (arguments.length === 2) { if (err) return cb(err) cb(null, _.isArray(obj) ? obj[0] : obj) } else cb(null, _.isArray(err) ? err[0] : err) } } var populationTypes = { '[static]': function (paths, options, cb) { Post.find({}, function (err, posts) { if (err) return cb(err) Post.deepPopulate(posts, paths, options, doneFn(cb)) }) }, '[static-promise]': function (paths, options, cb) { Post.find({}, function (err, posts) { if (err) return cb(err) Post.deepPopulate(posts, paths, options).then(doneFn(cb)) }) }, '[instance]': function (paths, options, cb) { Post.findOne({}, function (err, post) { if (err) return cb(err) post.deepPopulate(paths, options, doneFn(cb)) }) }, '[instance-promise]': function (paths, options, cb) { Post.findOne({}, function (err, post) { if (err) return cb(err) post.deepPopulate(paths, options).then(doneFn(cb)) }) }, '[query-one-callback]': function (paths, options, cb) { Post.findOne({}).deepPopulate(paths, options).exec(doneFn(cb)) }, '[query-one-promise]': function (paths, options, cb) { Post.findOne({}).deepPopulate(paths, options).exec().then(doneFn(cb)) }, '[query-id-callback]': function (paths, options, cb) { Post.findById(1).deepPopulate(paths, options).exec(doneFn(cb)) }, '[query-id-promise]': function (paths, options, cb) { Post.findById(1).deepPopulate(paths, options).exec().then(doneFn(cb)) }, '[query-many-callback]': function (paths, options, cb) { Post.find({_id: 1}).deepPopulate(paths, options).exec(doneFn(cb)) }, '[query-many-promise]': function (paths, options, cb) { Post.find({_id: 1}).deepPopulate(paths, options).exec().then(doneFn(cb)) } } Object.keys(populationTypes).forEach(function (type) { cb(type, populationTypes[type]) }) } function check(obj, loaded) { expect(obj.loaded).to.equal(loaded) } function checkPost(post) { check(post.user, true) check(post.user.manager, true) check(post.approved.user, true) check(post.approved.user.manager, true) post.comments.forEach(function (comment) { check(comment, true) check(comment.user, true) check(comment.user.manager, true) }) post.likes.forEach(function (like) { check(like.user, true) check(like.user.manager, true) }) } })