'use strict';
* Module dependencies.
*/
const start = require('./common');
const DocumentArray = require('../lib/types/documentArray');
const ArraySubdocument = require('../lib/types/arraySubdocument');
const assert = require('assert');
const idGetter = require('../lib/helpers/schema/idGetter');
const setValue = require('../lib/utils').setValue;
const mongoose = require('./common').mongoose;
const Schema = mongoose.Schema;
const MongooseDocumentArray = mongoose.Types.DocumentArray;
* Setup.
*/
function TestDoc(schema) {
const Subdocument = function() {
ArraySubdocument.call(this, {}, new DocumentArray());
};
* Inherits from ArraySubdocument.
*/
Object.setPrototypeOf(Subdocument.prototype, ArraySubdocument.prototype);
* Set schema.
*/
const SubSchema = new Schema({
title: { type: String }
});
Subdocument.prototype.$__setSchema(idGetter(schema || SubSchema));
return Subdocument;
}
* Test.
*/
describe('types.documentarray', function() {
let db;
before(function() {
db = start();
});
after(async function() {
await db.close();
});
beforeEach(() => db.deleteModel(/.*/));
afterEach(() => require('./util').clearTestData(db));
afterEach(() => require('./util').stopRemainingOps(db));
it('behaves and quacks like an array', function() {
const a = new MongooseDocumentArray();
assert.ok(a instanceof Array);
assert.ok(a.isMongooseArray);
assert.ok(a.isMongooseDocumentArray);
assert.ok(Array.isArray(a));
assert.deepEqual(a.$atomics().constructor, Object);
});
it('#id', function() {
let Subdocument = TestDoc();
let sub1 = new Subdocument();
sub1.title = 'Hello again to all my friends';
let id = sub1.id;
let a = new MongooseDocumentArray([sub1], 'test', null);
assert.equal(a.id(id).title, 'Hello again to all my friends');
assert.equal(a.id(sub1._id).title, 'Hello again to all my friends');
let Custom = new Schema({
title: { type: String },
_id: { type: String, required: true }
});
Subdocument = TestDoc(Custom);
let sub2 = new Subdocument();
sub2.title = 'together we can play some rock-n-roll';
sub2._id = 'a25';
const id2 = sub2.id;
a = new MongooseDocumentArray([sub2]);
assert.equal(a.id(id2).title, 'together we can play some rock-n-roll');
assert.equal(a.id(sub2._id).title, 'together we can play some rock-n-roll');
const CustNumber = new Schema({
title: { type: String },
_id: { type: Number, required: true }
});
Subdocument = TestDoc(CustNumber);
const sub3 = new Subdocument();
sub3.title = 'rock-n-roll';
sub3._id = 1995;
const id3 = sub3.id;
a = new MongooseDocumentArray([sub3]);
assert.equal(a.id(id3).title, 'rock-n-roll');
assert.equal(a.id(sub3._id).title, 'rock-n-roll');
Custom = new Schema({
title: { type: String },
_id: { one: { type: String }, two: { type: String } }
});
Subdocument = TestDoc(Custom);
sub1 = new Subdocument();
sub1._id = { one: 'rolling', two: 'rock' };
sub1.title = 'to be a rock and not to roll';
sub2 = new Subdocument();
sub2._id = { one: 'rock', two: 'roll' };
sub2.title = 'rock-n-roll';
a = new MongooseDocumentArray([sub1, sub2]);
assert.notEqual(a.id({ one: 'rolling', two: 'rock' }).title, 'rock-n-roll');
assert.equal(a.id({ one: 'rock', two: 'roll' }).title, 'rock-n-roll');
let sub4 = new Subdocument();
sub4.title = 'rock-n-roll';
a = new MongooseDocumentArray([sub4]);
let threw = false;
try {
a.id('i better not throw');
} catch (err) {
threw = err;
}
assert.equal(threw, false);
sub4 = new Subdocument();
sub4.title = 'rock-n-roll';
a = new MongooseDocumentArray([sub4]);
threw = false;
try {
a.id('i better not throw');
} catch (err) {
threw = err;
}
assert.equal(threw, false);
assert.strictEqual(null, a.id(undefined));
assert.strictEqual(null, a.id(null));
Custom = new Schema({
title: { type: String }
});
const Custom1 = new Schema({}, { id: false });
Subdocument = TestDoc(Custom);
const Subdocument1 = TestDoc(Custom1);
const sub = new Subdocument1();
sub1 = new Subdocument1();
sub.title = 'Hello again to all my friends';
id = sub1._id.toString();
setValue('_id', sub1, sub);
a = new MongooseDocumentArray([sub]);
assert.equal(a.id(id).title, 'Hello again to all my friends');
});
it('#id with custom schematype (gh-15725)', function() {
const schema = new Schema({ _id: Number, name: String });
const Subdocument = TestDoc(schema);
const sub1 = new Subdocument();
sub1._id = 42;
sub1.title = 'Hello again to all my friends';
const parentDoc = { $__: true, $__schema: new Schema({ subdocs: [schema] }) };
const a = new MongooseDocumentArray([sub1], 'subdocs', parentDoc);
assert.equal(a.id('42').title, 'Hello again to all my friends');
assert.equal(a.id(sub1.id).title, 'Hello again to all my friends');
assert.ok(!a.id('43'));
assert.ok(!a.id('not a number'));
});
describe('inspect', function() {
it('works with bad data', function() {
let threw = false;
const a = new MongooseDocumentArray([null]);
try {
a.inspect();
} catch (err) {
threw = true;
console.error(err.stack);
}
assert.ok(!threw);
});
});
describe('toObject', function() {
it('works with bad data', function() {
let threw = false;
const a = new MongooseDocumentArray([null]);
try {
a.toObject();
} catch (err) {
threw = true;
console.error(err.stack);
}
assert.ok(!threw);
});
it('passes options to its documents (gh-1415) (gh-4455)', function() {
const subSchema = new Schema({
title: { type: String }
});
subSchema.set('toObject', {
transform: function(doc, ret) {
ret.changed = 123;
return ret;
}
});
const db = mongoose.createConnection();
const M = db.model('Test', { docs: [subSchema] });
const m = new M();
m.docs.push({ docs: [{ title: 'hello' }] });
const delta = m.$__delta()[1];
assert.equal(delta.$push.docs.$each[0].changed, undefined);
});
it('uses the correct transform (gh-1412)', function() {
const SecondSchema = new Schema({});
SecondSchema.set('toObject', {
transform: function second(doc, ret) {
ret.secondToObject = true;
return ret;
}
});
const FirstSchema = new Schema({
second: [SecondSchema]
});
FirstSchema.set('toObject', {
transform: function first(doc, ret) {
ret.firstToObject = true;
return ret;
}
});
const First = db.model('Test', FirstSchema);
const Second = db.model('Test1', SecondSchema);
const first = new First({});
first.second.push(new Second());
first.second.push(new Second());
const obj = first.toObject();
assert.ok(obj.firstToObject);
assert.ok(obj.second[0].secondToObject);
assert.ok(obj.second[1].secondToObject);
assert.ok(!obj.second[0].firstToObject);
assert.ok(!obj.second[1].firstToObject);
});
});
describe('create()', function() {
it('works', function() {
const a = new MongooseDocumentArray([]);
assert.equal(typeof a.create, 'function');
const schema = new Schema({ docs: [new Schema({ name: 'string' })] });
mongoose.deleteModel(/Test/);
const T = mongoose.model('Test', schema);
const t = new T;
assert.equal(typeof t.docs.create, 'function');
const subdoc = t.docs.create({ name: 100 });
assert.ok(subdoc._id);
assert.equal(subdoc.name, '100');
assert.ok(subdoc instanceof ArraySubdocument);
});
});
describe('push()', function() {
it('does not re-cast instances of its embedded doc', async function() {
const child = new Schema({ name: String, date: Date });
child.pre('save', function() {
this.date = new Date();
});
const schema = new Schema({ children: [child] });
const M = db.model('Test', schema);
const m = new M();
await m.save();
let doc = await M.findById(m._id);
const c = doc.children.create({ name: 'first' });
assert.equal(c.date, undefined);
doc.children.push(c);
assert.equal(c.date, undefined);
await doc.save();
assert.ok(doc.children[doc.children.length - 1].date);
assert.equal(c.date, doc.children[doc.children.length - 1].date);
doc.children.push(c);
doc.children.push(c);
await doc.save();
doc = await M.findById(m._id);
assert.equal(doc.children.length, 3);
doc.children.forEach(function(child) {
assert.equal(doc.children[0].id, child.id);
});
});
it('corrects #ownerDocument() and index if value was created with array.create() (gh-1385)', function() {
const mg = new mongoose.Mongoose();
const M = mg.model('Test', { docs: [{ name: String }] });
const m = new M();
const doc = m.docs.create({ name: 'test 1385' });
assert.equal(String(doc.ownerDocument()._id), String(m._id));
m.docs.push(doc);
assert.equal(doc.ownerDocument()._id, String(m._id));
assert.strictEqual(doc.__index, 0);
});
it('corrects #ownerDocument() if value was created with array.create() and set() (gh-7504)', function() {
const M = db.model('Test', {
docs: [{ name: { type: String, validate: () => false } }]
});
const m = new M({});
const doc = m.docs.create({ name: 'test' });
m.set('docs', [doc]);
assert.equal(doc.ownerDocument()._id.toString(), String(m._id));
assert.strictEqual(doc.__index, 0);
assert.ok(m.validateSync().errors['docs.0.name']);
});
it('reports validation errors with correct index path (gh-7724)', function() {
const parentSchema = new Schema({
name: String,
children: [{
name: { type: String, required: true },
gender: { type: String, required: true }
}]
});
mongoose.deleteModel(/Test/);
const Parent = mongoose.model('Test', parentSchema);
const p = new Parent({
name: 'Eddard Stark',
children: [{ name: 'Arya Stark', gender: 'F' }]
});
p.children.push({ name: 'Sansa Stark' });
p.children.push({ gender: 'M' });
p.children.push({ name: 'Bran Stark', gender: 'M' });
p.children.push({ name: 'Jon Snow' });
const error = p.validateSync();
assert.ok(error);
assert.ok(error.errors);
assert.deepStrictEqual(
Object.keys(error.errors), ['children.1.gender', 'children.2.name', 'children.4.gender']);
});
});
it('#push should work on ArraySubdocument more than 2 levels deep', async function() {
const Comments = new Schema();
Comments.add({
title: String,
comments: [Comments]
});
const BlogPost = new Schema({
title: String,
comments: [Comments]
});
const Post = db.model('BlogPost', BlogPost);
let p = new Post({ title: 'comment nesting' });
const c1 = p.comments.create({ title: 'c1' });
const c2 = c1.comments.create({ title: 'c2' });
const c3 = c2.comments.create({ title: 'c3' });
p.comments.push(c1);
c1.comments.push(c2);
c2.comments.push(c3);
await p.save();
p = await Post.findById(p._id);
p.comments[0].comments[0].comments[0].comments.push({ title: 'c4' });
await p.save();
p = await Post.findById(p._id);
assert.equal(p.comments[0].comments[0].comments[0].comments[0].title, 'c4');
});
describe('required (gh-6364)', function() {
it('on top level', function() {
const calls = [];
const schema = new Schema({
docs: {
type: [{ name: 'string' }],
required: function() {
calls.push(this);
return true;
}
}
});
mongoose.deleteModel(/Test/);
const T = mongoose.model('Test', schema);
const t = new T({});
t.docs.push({ name: 'test1' });
t.docs.push({ name: 'test2' });
t.validateSync();
assert.equal(calls.length, 1);
});
it('in arr', function() {
const calls = [];
const schema = new Schema({
docs: [{
type: new Schema({ name: 'string' }),
required: function() {
calls.push(this);
return true;
}
}]
});
mongoose.deleteModel(/Test/);
const T = mongoose.model('Test', schema);
const t = new T({});
t.docs.push(null);
t.docs.push({ name: 'test2' });
const err = t.validateSync();
assert.equal(calls.length, 2);
assert.ok(err);
assert.ok(err.errors['docs.0']);
});
});
describe('invalidate()', function() {
it('works', async function() {
const schema = new Schema({ docs: [{ name: 'string' }] });
schema.pre('validate', function() {
const subdoc = this.docs[this.docs.length - 1];
subdoc.invalidate('name', 'boo boo', '%');
});
mongoose.deleteModel(/Test/);
const T = mongoose.model('Test', schema);
const t = new T();
t.docs.push({ name: 100 });
const subdoc = t.docs.create({ name: 'yep' });
assert.throws(function() {
subdoc.invalidate('name', 'junk', 47);
});
await t.validate().catch(() => {});
const e = t.errors['docs.0.name'];
assert.ok(e);
assert.equal(e.path, 'docs.0.name');
assert.equal(e.kind, 'user defined');
assert.equal(e.message, 'boo boo');
assert.equal(e.value, '%');
});
it('handles validation failures', async function() {
const nested = new Schema({ v: { type: Number, max: 30 } });
const schema = new Schema({
docs: [nested]
});
const M = db.model('Test', schema);
const m = new M({ docs: [{ v: 900 }] });
const err = await m.save().then(() => null, err => err);
assert.equal(err.errors['docs.0.v'].value, 900);
});
it('clears listeners on cast error (gh-6723)', function() {
const nested = new Schema({ v: Number });
const schema = new Schema({
docs: [nested]
});
const M = db.model('Test', schema);
const m = new M({});
m.docs = [50];
m.docs = [];
m.docs.push({ v: 50 });
return m.save();
});
it('slice() copies parent and path (gh-8317)', function() {
const nested = new Schema({ v: Number });
const schema = new Schema({
docs: [nested]
});
const M = db.model('Test', schema);
const doc = M.hydrate({ docs: [{ v: 1 }, { v: 2 }] });
let arr = doc.docs;
arr = arr.slice();
arr.splice(0, 1);
assert.equal(arr.length, 1);
assert.equal(doc.docs.length, 2);
});
it('map() works (gh-8317)', function() {
const personSchema = new Schema({ friends: [{ name: { type: String } }] });
mongoose.deleteModel(/Test/);
const Person = mongoose.model('Test', personSchema);
const person = new Person({ friends: [{ name: 'Hafez' }] });
const friendsNames = person.friends.map(friend => friend.name);
friendsNames.push('Sam');
assert.equal(friendsNames.length, 2);
assert.equal(friendsNames[1], 'Sam');
});
it('slice() after map() works (gh-8399)', function() {
const MyModel = db.model('Test', Schema({
myArray: [{ name: String }]
}));
const doc = new MyModel({
myArray: [{ name: 'a' }, { name: 'b' }]
});
let myArray = doc.myArray;
myArray = myArray.map(val => ({ name: `${val.name} mapped` }));
myArray.splice(1, 1, { name: 'c' });
myArray.splice(2, 0, { name: 'd' });
assert.deepEqual(myArray.map(v => v.name), [
'a mapped',
'c',
'd'
]);
});
it('unshift() after map() works (gh-9012)', function() {
const MyModel = db.model('Test', Schema({
myArray: [{ name: String }]
}));
const doc = new MyModel({
myArray: [{ name: 'b' }, { name: 'c' }]
});
let myArray = doc.myArray;
myArray = myArray.map(val => ({ name: `${val.name} mapped` }));
myArray.unshift({ name: 'a inserted' });
assert.deepEqual(myArray.map(v => v.name), [
'a inserted',
'b mapped',
'c mapped'
]);
});
});
it('cleans modified subpaths on splice() (gh-7249)', async function() {
const childSchema = mongoose.Schema({
name: { type: String, required: true }
}, { _id: false });
const parentSchema = new mongoose.Schema({
children: [childSchema]
});
const Parent = db.model('Test', parentSchema);
let parent = await Parent.create({
children: [{ name: '1' }, { name: '2' }]
});
parent = await Parent.findOne();
parent.children[1].name = '3';
parent.children.splice(0, 1);
await parent.save();
parent = await Parent.findOne();
assert.deepEqual(parent.toObject().children, [{ name: '3' }]);
});
it('modifies ownerDocument() on set (gh-8479)', function() {
const nestedArraySchema = Schema({
name: String,
subDocArray: [{ name: String }]
});
const Model = db.model('Test', nestedArraySchema);
const doc1 = new Model({
name: 'doc1',
subDocArray: [{
name: 'subDoc'
}]
});
const doc2 = new Model({
name: 'doc2',
subDocArray: [{
name: 'subDoc'
}]
});
doc1.subDocArray = doc2.subDocArray;
assert.equal(doc2.subDocArray[0].ownerDocument().name, 'doc2');
assert.equal(doc1.subDocArray[0].ownerDocument().name, 'doc1');
});
it('modifying subdoc path after `slice()` (gh-8356)', function() {
mongoose.deleteModel(/Test/);
const nestedArraySchema = Schema({
name: String,
subDocArray: [{ name: String }]
});
const Model = db.model('Test', nestedArraySchema);
const doc = new Model().init({
name: 'test',
subDocArray: [{ name: 'foo' }, { name: 'bar' }]
});
doc.subDocArray.slice(1, 2)[0].name = 'baz';
assert.ok(doc.isModified('subDocArray.1.name'));
});
it('supports setting to newly constructed array with no path or parent (gh-8108)', function() {
const nestedArraySchema = Schema({
name: String,
subDocArray: [{ _id: false, name: String }]
});
const Model = db.model('Test', nestedArraySchema);
const doc = new Model({ name: 'doc1' });
doc.subDocArray = new DocumentArray([]);
doc.subDocArray.push({ name: 'foo' });
return doc.save().
then(() => Model.findById(doc)).
then(doc => assert.deepEqual(doc.toObject().subDocArray, [{ name: 'foo' }]));
});
it('keeps atomics after setting (gh-10272)', function() {
const MyChildSchema = new Schema({
name: String
});
const MyModelSchema = new Schema({
children: [MyChildSchema]
});
const Test = db.model('Test', MyModelSchema);
const doc = new Test();
doc.init({
children: [{ name: 'John' }, { name: 'Jane' }]
});
doc.children = [{ name: 'John' }];
doc.children = doc.children.concat([]);
doc.children.push({ name: 'Mary' });
assert.ok(doc.children.$atomics().$set);
assert.deepEqual(doc.children.$atomics().$set.map(v => v.name), ['John', 'Mary']);
});
it('handles `DocumentArray#create()` with populated paths (gh-10749)', async function() {
const schema = new Schema({ subdoc: { ref: 'Test2', type: 'ObjectId' }, arr: [{ num: Number }] });
const Test = mongoose.model('Test', schema);
const Test2 = mongoose.model('Test2', Schema({ name: String }));
const doc2 = new Test2({ name: 'test' });
const doc = new Test({ subdoc: doc2, arr: [] });
const subdoc = doc.arr.create({ num: '42' });
await subdoc.validate();
assert.strictEqual(subdoc.num, 42);
doc.arr.push(subdoc);
await doc.validate();
});
it('applies _id default (gh-12264)', function() {
mongoose.deleteModel(/Test/);
const nestedArraySchema = Schema({
subDocArray: [{ name: String }]
});
const Model = db.model('Test', nestedArraySchema);
const doc = new Model().init({
subDocArray: [{ name: 'foo' }]
});
assert.ok(doc.subDocArray[0]._id instanceof mongoose.Types.ObjectId);
});
it('gets correct path when underneath map (gh-12997)', function() {
const mapArraySchema = Schema({
myMap: {
type: Map,
of: [Schema({ name: String })]
}
});
const Model = db.model('Test', mapArraySchema);
const doc = new Model({
myMap: {
foo: [{ name: 'bar' }]
}
});
assert.equal(doc.myMap.get('foo').$path(), 'myMap.foo');
});
it('bubbles up validation errors from doubly nested doc arrays (gh-14101)', async function() {
const optionsSchema = new mongoose.Schema({
val: {
type: Number,
required: true
}
});
const testSchema = new mongoose.Schema({
name: String,
options: {
type: [[optionsSchema]],
required: true
}
});
const Test = db.model('Test', testSchema);
await assert.rejects(
Test.create({ name: 'test', options: [[{ val: null }]] }),
/options.0.0.val: Path `val` is required./
);
});
it('stores all schematype options in the embedded schematype', function() {
const schema = new mongoose.Schema({
docArr: [{
type: new mongoose.Schema({ name: String }),
someCustomOption: 'test 42'
}]
});
assert.strictEqual(schema.path('docArr').getEmbeddedSchemaType().options.someCustomOption, 'test 42');
});
it('updates __index when reassigning array with reordered subdocs (gh-15973)', async function() {
const childSchema = new mongoose.Schema({
id: Number,
v: Number
});
const parentSchema = new mongoose.Schema({
_id: String,
array: {
type: [childSchema],
_id: false
}
});
const Test = db.model('Test', parentSchema);
await Test.deleteOne({ _id: 'test' });
await Test.create({
_id: 'test',
array: [
{ id: 1, v: 0 },
{ id: 2, v: 0 }
]
});
const doc = await Test.findById('test').orFail();
doc.array = [doc.array[1], doc.array[0]];
await doc.save();
doc.array[0].v = 999;
assert.ok(doc.modifiedPaths().includes('array.0.v'));
assert.ok(!doc.modifiedPaths().includes('array.1.v'));
await doc.save();
const fetched = await Test.findById('test').orFail().lean();
assert.strictEqual(fetched.array[0].v, 999);
assert.strictEqual(fetched.array[1].v, 0);
});
describe('document array indexes after removal', function() {
it('reindexes subdocs after pull() so subsequent nested changes save correctly', async function() {
const { User, user } = await createTestContext();
user.addresses.pull(user.addresses[0]._id);
await user.save();
assert.strictEqual(user.isModified(), false, 'sanity: saved doc starts clean');
user.addresses[0].city = 'New York';
assert.strictEqual(
user.addresses[0].$__fullPath('city'),
'addresses.0.city',
'first remaining subdoc should use its current array index'
);
assert.deepStrictEqual(user.modifiedPaths(), ['addresses', 'addresses.0', 'addresses.0.city']);
assert.deepStrictEqual(user.$getChanges(), { $set: { 'addresses.0.city': 'New York' } });
await user.save();
const fetched = await User.findById(user._id).orFail().lean();
assert.strictEqual(fetched.addresses[0].city, 'New York');
assert.strictEqual(fetched.addresses[1].city, 'Denver');
});
it('reindexes subdocs after splice() so subsequent nested changes save correctly', async function() {
const { User, user } = await createTestContext();
user.addresses.splice(0, 1);
await user.save();
assert.strictEqual(user.isModified(), false, 'sanity: saved doc starts clean');
user.addresses[0].city = 'New York';
assert.strictEqual(
user.addresses[0].$__fullPath('city'),
'addresses.0.city',
'first remaining subdoc should use its current array index'
);
assert.deepStrictEqual(user.modifiedPaths(), ['addresses', 'addresses.0', 'addresses.0.city']);
assert.deepStrictEqual(user.$getChanges(), { $set: { 'addresses.0.city': 'New York' } });
await user.save();
const fetched = await User.findById(user._id).orFail().lean();
assert.strictEqual(fetched.addresses[0].city, 'New York');
assert.strictEqual(fetched.addresses[1].city, 'Denver');
});
it('reindexes subdocs after shift() so subsequent nested changes save correctly', async function() {
const { User, user } = await createTestContext();
user.addresses.shift();
await user.save();
assert.strictEqual(user.isModified(), false, 'sanity: saved doc starts clean');
user.addresses[0].city = 'New York';
assert.strictEqual(
user.addresses[0].$__fullPath('city'),
'addresses.0.city',
'first remaining subdoc should use its current array index'
);
assert.deepStrictEqual(user.modifiedPaths(), ['addresses', 'addresses.0', 'addresses.0.city']);
assert.deepStrictEqual(user.$getChanges(), { $set: { 'addresses.0.city': 'New York' } });
await user.save();
const fetched = await User.findById(user._id).orFail().lean();
assert.strictEqual(fetched.addresses[0].city, 'New York');
assert.strictEqual(fetched.addresses[1].city, 'Denver');
});
it('reindexes subdocs after $shift() so subsequent nested changes save correctly', async function() {
const { User, user } = await createTestContext();
user.addresses.$shift();
await user.save();
assert.strictEqual(user.isModified(), false, 'sanity: saved doc starts clean');
user.addresses[0].city = 'New York';
assert.strictEqual(
user.addresses[0].$__fullPath('city'),
'addresses.0.city',
'first remaining subdoc should use its current array index'
);
assert.deepStrictEqual(user.modifiedPaths(), ['addresses', 'addresses.0', 'addresses.0.city']);
assert.deepStrictEqual(user.$getChanges(), { $set: { 'addresses.0.city': 'New York' } });
await user.save();
const fetched = await User.findById(user._id).orFail().lean();
assert.strictEqual(fetched.addresses[0].city, 'New York');
assert.strictEqual(fetched.addresses[1].city, 'Denver');
});
async function createTestContext() {
const addressSchema = new mongoose.Schema({
street: String,
city: String
});
const userSchema = new mongoose.Schema({
name: String,
addresses: [addressSchema]
});
const User = db.model('UserDocumentArrayIndex', userSchema);
const user = new User({
name: 'John',
addresses: [
{ street: '1 Main', city: 'Boston' },
{ street: '2 Main', city: 'Chicago' },
{ street: '3 Main', city: 'Denver' }
]
});
await user.save();
return { User, user };
}
});
describe('document array indexes after reordering', function() {
it('reindexes subdocs after unshift() so subsequent nested changes save correctly', async function() {
const { User, user } = await createTestContext();
user.addresses.unshift({ street: '0 Main', city: 'Amsterdam' });
await user.save();
assert.strictEqual(user.isModified(), false, 'sanity: saved doc starts clean');
user.addresses[1].city = 'New York';
assert.strictEqual(
user.addresses[1].$__fullPath('city'),
'addresses.1.city',
'shifted subdoc should use its current array index'
);
assert.deepStrictEqual(user.modifiedPaths(), ['addresses', 'addresses.1', 'addresses.1.city']);
assert.deepStrictEqual(user.$getChanges(), { $set: { 'addresses.1.city': 'New York' } });
await user.save();
const fetched = await User.findById(user._id).orFail().lean();
assert.deepStrictEqual(
fetched.addresses.map(address => address.city),
['Amsterdam', 'New York', 'Chicago', 'Denver']
);
});
it('reindexes subdocs after positioned push() so subsequent nested changes save correctly', async function() {
const { User, user } = await createTestContext();
user.addresses.push({
$each: [
{ street: '0 Main', city: 'Amsterdam' },
{ street: '0 Main', city: 'Rotterdam' }
],
$position: 0
});
await user.save();
assert.strictEqual(user.isModified(), false, 'sanity: saved doc starts clean');
user.addresses[1].city = 'New York';
assert.strictEqual(
user.addresses[0].$__fullPath('city'),
'addresses.0.city',
'first positioned pushed subdoc should use its current array index'
);
assert.strictEqual(
user.addresses[1].$__fullPath('city'),
'addresses.1.city',
'second positioned pushed subdoc should use its current array index'
);
assert.deepStrictEqual(user.modifiedPaths(), ['addresses', 'addresses.1', 'addresses.1.city']);
assert.deepStrictEqual(user.$getChanges(), { $set: { 'addresses.1.city': 'New York' } });
await user.save();
const fetched = await User.findById(user._id).orFail().lean();
assert.deepStrictEqual(
fetched.addresses.map(address => address.city),
['Amsterdam', 'New York', 'Boston', 'Chicago', 'Denver']
);
});
it('does not restamp existing subdocs after append-only push()', async function() {
const { user } = await createTestContext();
const originalSetIndex = user.addresses[0].$setIndex;
let setIndexCalls = 0;
user.addresses[0].$setIndex = function(index) {
++setIndexCalls;
return originalSetIndex.call(this, index);
};
user.addresses.push({ street: '4 Main', city: 'Austin' });
assert.strictEqual(setIndexCalls, 0);
assert.strictEqual(
user.addresses[3].$__fullPath('city'),
'addresses.3.city',
'appended subdoc should use its appended array index'
);
assert.deepStrictEqual(user.$getChanges(), {
$push: {
addresses: {
$each: [{ street: '4 Main', city: 'Austin', _id: user.addresses[3]._id }]
}
},
$inc: { __v: 1 }
});
});
it('reindexes subdocs after sort() so subsequent nested changes save correctly', async function() {
const { User, user } = await createTestContext();
user.addresses.sort((a, b) => b.city.localeCompare(a.city));
await user.save();
assert.strictEqual(user.isModified(), false, 'sanity: saved doc starts clean');
user.addresses[0].city = 'New York';
assert.strictEqual(
user.addresses[0].$__fullPath('city'),
'addresses.0.city',
'sorted subdoc should use its current array index'
);
assert.deepStrictEqual(user.modifiedPaths(), ['addresses', 'addresses.0', 'addresses.0.city']);
assert.deepStrictEqual(user.$getChanges(), { $set: { 'addresses.0.city': 'New York' } });
await user.save();
const fetched = await User.findById(user._id).orFail().lean();
assert.deepStrictEqual(
fetched.addresses.map(address => address.city),
['New York', 'Chicago', 'Boston']
);
});
it('keeps subdoc indexes correct after reverse() so subsequent nested changes save correctly', async function() {
const { User, user } = await createTestContext();
user.addresses.reverse();
await user.save();
assert.strictEqual(user.isModified(), false, 'sanity: saved doc starts clean');
user.addresses[0].city = 'New York';
assert.strictEqual(
user.addresses[0].$__fullPath('city'),
'addresses.0.city',
'reversed subdoc should use its current array index'
);
assert.deepStrictEqual(user.modifiedPaths(), ['addresses', 'addresses.0', 'addresses.0.city']);
assert.deepStrictEqual(user.$getChanges(), { $set: { 'addresses.0.city': 'New York' } });
await user.save();
const fetched = await User.findById(user._id).orFail().lean();
assert.deepStrictEqual(
fetched.addresses.map(address => address.city),
['New York', 'Chicago', 'Boston']
);
});
it('registers full array atomics after reverse() follows append-only push()', async function() {
const { user } = await createTestContext();
user.addresses.push({ street: '4 Main', city: 'Austin' });
assert.deepStrictEqual(Object.keys(user.addresses.$atomics()), ['$push'], 'sanity: append-only push starts as $push');
const ret = user.addresses.reverse();
assert.strictEqual(ret, user.addresses);
assert.deepStrictEqual(Object.keys(user.addresses.$atomics()), ['$set']);
assert.deepStrictEqual(
user.addresses.$atomics().$set.map(address => address.city),
['Austin', 'Denver', 'Chicago', 'Boston']
);
assert.deepStrictEqual(user.$getChanges(), {
$set: {
addresses: [
{ street: '4 Main', city: 'Austin', _id: user.addresses[0]._id },
{ street: '3 Main', city: 'Denver', _id: user.addresses[1]._id },
{ street: '2 Main', city: 'Chicago', _id: user.addresses[2]._id },
{ street: '1 Main', city: 'Boston', _id: user.addresses[3]._id }
]
},
$inc: { __v: 1 }
});
});
async function createTestContext() {
const addressSchema = new mongoose.Schema({
street: String,
city: String
});
const userSchema = new mongoose.Schema({
name: String,
addresses: [addressSchema]
});
const User = db.model('UserDocumentArrayReorderIndex', userSchema);
const user = new User({
name: 'John',
addresses: [
{ street: '1 Main', city: 'Boston' },
{ street: '2 Main', city: 'Chicago' },
{ street: '3 Main', city: 'Denver' }
]
});
await user.save();
return { User, user };
}
});
describe('document array populated paths', function() {
it('updates top-level populated() after sort() reorders a document array', async function() {
const { Trip, locations, trip, getPopulatedLocationIds } = await createTestContext();
const fromDb = await Trip.findById(trip._id).orFail().populate('stops.location');
assert.deepStrictEqual(
getPopulatedLocationIds(fromDb),
locations.map(location => location._id.toString())
);
fromDb.stops.sort((left, right) => left.sequence - right.sequence);
assert.deepStrictEqual(
getPopulatedLocationIds(fromDb),
[
locations[1]._id.toString(),
locations[0]._id.toString(),
locations[2]._id.toString()
]
);
});
it('updates top-level populated() after reverse() reorders a document array', async function() {
const { Trip, locations, trip, getPopulatedLocationIds } = await createTestContext();
const fromDb = await Trip.findById(trip._id).orFail().populate('stops.location');
assert.deepStrictEqual(
getPopulatedLocationIds(fromDb),
locations.map(location => location._id.toString())
);
fromDb.stops.reverse();
assert.deepStrictEqual(
getPopulatedLocationIds(fromDb),
[
locations[2]._id.toString(),
locations[1]._id.toString(),
locations[0]._id.toString()
]
);
});
it('updates top-level populated() after pop() removes the last subdoc', async function() {
const { Trip, locations, trip, getPopulatedLocationIds } = await createTestContext();
const fromDb = await Trip.findById(trip._id).orFail().populate('stops.location');
assert.deepStrictEqual(
getPopulatedLocationIds(fromDb),
locations.map(location => location._id.toString())
);
fromDb.stops.pop();
assert.deepStrictEqual(
getPopulatedLocationIds(fromDb),
[locations[0]._id.toString(), locations[1]._id.toString()]
);
});
it('updates top-level populated() after $pop() removes the last subdoc', async function() {
const { Trip, locations, trip, getPopulatedLocationIds } = await createTestContext();
const fromDb = await Trip.findById(trip._id).orFail().populate('stops.location');
assert.deepStrictEqual(
getPopulatedLocationIds(fromDb),
locations.map(location => location._id.toString())
);
fromDb.stops.$pop();
assert.deepStrictEqual(
getPopulatedLocationIds(fromDb),
[locations[0]._id.toString(), locations[1]._id.toString()]
);
});
async function createTestContext() {
const locationSchema = new Schema({ name: String });
const stopSchema = new Schema({
location: { type: Schema.Types.ObjectId, ref: 'Location' },
sequence: Number
});
const tripSchema = new Schema({
name: String,
stops: [stopSchema]
});
const Location = db.model('Location', locationSchema);
const Trip = db.model('Trip', tripSchema);
const locations = await Location.create([
{ name: 'Boston' },
{ name: 'Chicago' },
{ name: 'Denver' }
]);
const trip = await Trip.create({
name: 'Route planning',
stops: [
{ location: locations[0]._id, sequence: 2 },
{ location: locations[1]._id, sequence: 1 },
{ location: locations[2]._id, sequence: 3 }
]
});
return { Trip, locations, trip, getPopulatedLocationIds };
}
function getPopulatedLocationIds(trip) {
return trip.populated('stops.location').map(id => id.toString());
}
});
it('clones subdoc when same subdoc reference appears multiple times in array (gh-15973)', async function() {
const childSchema = new mongoose.Schema({
id: Number,
v: Number
});
const parentSchema = new mongoose.Schema({
_id: String,
array: {
type: [childSchema],
_id: false
}
});
const Test = db.model('Test', parentSchema);
await Test.deleteOne({ _id: 'test' });
await Test.create({
_id: 'test',
array: [{ id: 1, v: 0 }]
});
const doc = await Test.findById('test').orFail();
const subdoc = doc.array[0];
doc.array = [subdoc, subdoc];
assert.notStrictEqual(doc.array[0], doc.array[1]);
assert.strictEqual(doc.array[0].__index, 0);
assert.strictEqual(doc.array[1].__index, 1);
await doc.save();
doc.array[1].v = 999;
assert.strictEqual(doc.array[0].v, 0);
assert.strictEqual(doc.array[1].v, 999);
assert.ok(doc.modifiedPaths().includes('array.1.v'));
assert.ok(!doc.modifiedPaths().includes('array.0.v'));
await doc.save();
const fetched = await Test.findById('test').orFail().lean();
assert.strictEqual(fetched.array[0].v, 0);
assert.strictEqual(fetched.array[1].v, 999);
});
describe('document array indexes after removal', function() {
it('reindexes subdocs after pull() so subsequent nested changes save correctly', async function() {
const { User, user } = await createTestContext();
user.addresses.pull(user.addresses[0]._id);
await user.save();
assert.strictEqual(user.isModified(), false, 'sanity: saved doc starts clean');
user.addresses[0].city = 'New York';
assert.strictEqual(
user.addresses[0].$__fullPath('city'),
'addresses.0.city',
'first remaining subdoc should use its current array index'
);
assert.deepStrictEqual(user.modifiedPaths(), ['addresses', 'addresses.0', 'addresses.0.city']);
assert.deepStrictEqual(user.getChanges(), { $set: { 'addresses.0.city': 'New York' } });
await user.save();
const fetched = await User.findById(user._id).orFail().lean();
assert.strictEqual(fetched.addresses[0].city, 'New York');
assert.strictEqual(fetched.addresses[1].city, 'Denver');
});
it('reindexes subdocs after splice() so subsequent nested changes save correctly', async function() {
const { User, user } = await createTestContext();
user.addresses.splice(0, 1);
await user.save();
assert.strictEqual(user.isModified(), false, 'sanity: saved doc starts clean');
user.addresses[0].city = 'New York';
assert.strictEqual(
user.addresses[0].$__fullPath('city'),
'addresses.0.city',
'first remaining subdoc should use its current array index'
);
assert.deepStrictEqual(user.modifiedPaths(), ['addresses', 'addresses.0', 'addresses.0.city']);
assert.deepStrictEqual(user.getChanges(), { $set: { 'addresses.0.city': 'New York' } });
await user.save();
const fetched = await User.findById(user._id).orFail().lean();
assert.strictEqual(fetched.addresses[0].city, 'New York');
assert.strictEqual(fetched.addresses[1].city, 'Denver');
});
it('reindexes subdocs after shift() so subsequent nested changes save correctly', async function() {
const { User, user } = await createTestContext();
user.addresses.shift();
await user.save();
assert.strictEqual(user.isModified(), false, 'sanity: saved doc starts clean');
user.addresses[0].city = 'New York';
assert.strictEqual(
user.addresses[0].$__fullPath('city'),
'addresses.0.city',
'first remaining subdoc should use its current array index'
);
assert.deepStrictEqual(user.modifiedPaths(), ['addresses', 'addresses.0', 'addresses.0.city']);
assert.deepStrictEqual(user.getChanges(), { $set: { 'addresses.0.city': 'New York' } });
await user.save();
const fetched = await User.findById(user._id).orFail().lean();
assert.strictEqual(fetched.addresses[0].city, 'New York');
assert.strictEqual(fetched.addresses[1].city, 'Denver');
});
it('reindexes subdocs after $shift() so subsequent nested changes save correctly', async function() {
const { User, user } = await createTestContext();
user.addresses.$shift();
await user.save();
assert.strictEqual(user.isModified(), false, 'sanity: saved doc starts clean');
user.addresses[0].city = 'New York';
assert.strictEqual(
user.addresses[0].$__fullPath('city'),
'addresses.0.city',
'first remaining subdoc should use its current array index'
);
assert.deepStrictEqual(user.modifiedPaths(), ['addresses', 'addresses.0', 'addresses.0.city']);
assert.deepStrictEqual(user.getChanges(), { $set: { 'addresses.0.city': 'New York' } });
await user.save();
const fetched = await User.findById(user._id).orFail().lean();
assert.strictEqual(fetched.addresses[0].city, 'New York');
assert.strictEqual(fetched.addresses[1].city, 'Denver');
});
async function createTestContext() {
const addressSchema = new mongoose.Schema({
street: String,
city: String
});
const userSchema = new mongoose.Schema({
name: String,
addresses: [addressSchema]
});
const User = db.model('UserDocumentArrayIndex', userSchema);
const user = new User({
name: 'John',
addresses: [
{ street: '1 Main', city: 'Boston' },
{ street: '2 Main', city: 'Chicago' },
{ street: '3 Main', city: 'Denver' }
]
});
await user.save();
return { User, user };
}
});
describe('document array indexes after reordering', function() {
it('reindexes subdocs after unshift() so subsequent nested changes save correctly', async function() {
const { User, user } = await createTestContext();
user.addresses.unshift({ street: '0 Main', city: 'Amsterdam' });
await user.save();
assert.strictEqual(user.isModified(), false, 'sanity: saved doc starts clean');
user.addresses[1].city = 'New York';
assert.strictEqual(
user.addresses[1].$__fullPath('city'),
'addresses.1.city',
'shifted subdoc should use its current array index'
);
assert.deepStrictEqual(user.modifiedPaths(), ['addresses', 'addresses.1', 'addresses.1.city']);
assert.deepStrictEqual(user.getChanges(), { $set: { 'addresses.1.city': 'New York' } });
await user.save();
const fetched = await User.findById(user._id).orFail().lean();
assert.deepStrictEqual(
fetched.addresses.map(address => address.city),
['Amsterdam', 'New York', 'Chicago', 'Denver']
);
});
it('reindexes subdocs after positioned push() so subsequent nested changes save correctly', async function() {
const { User, user } = await createTestContext();
user.addresses.push({
$each: [
{ street: '0 Main', city: 'Amsterdam' },
{ street: '0 Main', city: 'Rotterdam' }
],
$position: 0
});
await user.save();
assert.strictEqual(user.isModified(), false, 'sanity: saved doc starts clean');
user.addresses[1].city = 'New York';
assert.strictEqual(
user.addresses[0].$__fullPath('city'),
'addresses.0.city',
'first positioned pushed subdoc should use its current array index'
);
assert.strictEqual(
user.addresses[1].$__fullPath('city'),
'addresses.1.city',
'second positioned pushed subdoc should use its current array index'
);
assert.deepStrictEqual(user.modifiedPaths(), ['addresses', 'addresses.1', 'addresses.1.city']);
assert.deepStrictEqual(user.getChanges(), { $set: { 'addresses.1.city': 'New York' } });
await user.save();
const fetched = await User.findById(user._id).orFail().lean();
assert.deepStrictEqual(
fetched.addresses.map(address => address.city),
['Amsterdam', 'New York', 'Boston', 'Chicago', 'Denver']
);
});
it('does not restamp existing subdocs after append-only push()', async function() {
const { user } = await createTestContext();
const originalSetIndex = user.addresses[0].$setIndex;
let setIndexCalls = 0;
user.addresses[0].$setIndex = function(index) {
++setIndexCalls;
return originalSetIndex.call(this, index);
};
user.addresses.push({ street: '4 Main', city: 'Austin' });
assert.strictEqual(setIndexCalls, 0);
assert.strictEqual(
user.addresses[3].$__fullPath('city'),
'addresses.3.city',
'appended subdoc should use its appended array index'
);
assert.deepStrictEqual(user.getChanges(), {
$push: {
addresses: {
$each: [{ street: '4 Main', city: 'Austin', _id: user.addresses[3]._id }]
}
},
$inc: { __v: 1 }
});
});
it('reindexes subdocs after sort() so subsequent nested changes save correctly', async function() {
const { User, user } = await createTestContext();
user.addresses.sort((a, b) => b.city.localeCompare(a.city));
await user.save();
assert.strictEqual(user.isModified(), false, 'sanity: saved doc starts clean');
user.addresses[0].city = 'New York';
assert.strictEqual(
user.addresses[0].$__fullPath('city'),
'addresses.0.city',
'sorted subdoc should use its current array index'
);
assert.deepStrictEqual(user.modifiedPaths(), ['addresses', 'addresses.0', 'addresses.0.city']);
assert.deepStrictEqual(user.getChanges(), { $set: { 'addresses.0.city': 'New York' } });
await user.save();
const fetched = await User.findById(user._id).orFail().lean();
assert.deepStrictEqual(
fetched.addresses.map(address => address.city),
['New York', 'Chicago', 'Boston']
);
});
it('keeps subdoc indexes correct after reverse() so subsequent nested changes save correctly', async function() {
const { User, user } = await createTestContext();
user.addresses.reverse();
await user.save();
assert.strictEqual(user.isModified(), false, 'sanity: saved doc starts clean');
user.addresses[0].city = 'New York';
assert.strictEqual(
user.addresses[0].$__fullPath('city'),
'addresses.0.city',
'reversed subdoc should use its current array index'
);
assert.deepStrictEqual(user.modifiedPaths(), ['addresses', 'addresses.0', 'addresses.0.city']);
assert.deepStrictEqual(user.getChanges(), { $set: { 'addresses.0.city': 'New York' } });
await user.save();
const fetched = await User.findById(user._id).orFail().lean();
assert.deepStrictEqual(
fetched.addresses.map(address => address.city),
['New York', 'Chicago', 'Boston']
);
});
it('registers full array atomics after reverse() follows append-only push()', async function() {
const { user } = await createTestContext();
user.addresses.push({ street: '4 Main', city: 'Austin' });
assert.deepStrictEqual(Object.keys(user.addresses.$atomics()), ['$push'], 'sanity: append-only push starts as $push');
const ret = user.addresses.reverse();
assert.strictEqual(ret, user.addresses);
assert.deepStrictEqual(Object.keys(user.addresses.$atomics()), ['$set']);
assert.deepStrictEqual(
user.addresses.$atomics().$set.map(address => address.city),
['Austin', 'Denver', 'Chicago', 'Boston']
);
assert.deepStrictEqual(user.getChanges(), {
$set: {
addresses: [
{ street: '4 Main', city: 'Austin', _id: user.addresses[0]._id },
{ street: '3 Main', city: 'Denver', _id: user.addresses[1]._id },
{ street: '2 Main', city: 'Chicago', _id: user.addresses[2]._id },
{ street: '1 Main', city: 'Boston', _id: user.addresses[3]._id }
]
},
$inc: { __v: 1 }
});
});
async function createTestContext() {
const addressSchema = new mongoose.Schema({
street: String,
city: String
});
const userSchema = new mongoose.Schema({
name: String,
addresses: [addressSchema]
});
const User = db.model('UserDocumentArrayReorderIndex', userSchema);
const user = new User({
name: 'John',
addresses: [
{ street: '1 Main', city: 'Boston' },
{ street: '2 Main', city: 'Chicago' },
{ street: '3 Main', city: 'Denver' }
]
});
await user.save();
return { User, user };
}
});
describe('document array populated paths', function() {
it('updates top-level populated() after sort() reorders a document array', async function() {
const { Trip, locations, trip, getPopulatedLocationIds } = await createTestContext();
const fromDb = await Trip.findById(trip._id).orFail().populate('stops.location');
assert.deepStrictEqual(
getPopulatedLocationIds(fromDb),
locations.map(location => location._id.toString())
);
fromDb.stops.sort((left, right) => left.sequence - right.sequence);
assert.deepStrictEqual(
getPopulatedLocationIds(fromDb),
[
locations[1]._id.toString(),
locations[0]._id.toString(),
locations[2]._id.toString()
]
);
});
it('updates top-level populated() after reverse() reorders a document array', async function() {
const { Trip, locations, trip, getPopulatedLocationIds } = await createTestContext();
const fromDb = await Trip.findById(trip._id).orFail().populate('stops.location');
assert.deepStrictEqual(
getPopulatedLocationIds(fromDb),
locations.map(location => location._id.toString())
);
fromDb.stops.reverse();
assert.deepStrictEqual(
getPopulatedLocationIds(fromDb),
[
locations[2]._id.toString(),
locations[1]._id.toString(),
locations[0]._id.toString()
]
);
});
it('updates top-level populated() after pop() removes the last subdoc', async function() {
const { Trip, locations, trip, getPopulatedLocationIds } = await createTestContext();
const fromDb = await Trip.findById(trip._id).orFail().populate('stops.location');
assert.deepStrictEqual(
getPopulatedLocationIds(fromDb),
locations.map(location => location._id.toString())
);
fromDb.stops.pop();
assert.deepStrictEqual(
getPopulatedLocationIds(fromDb),
[locations[0]._id.toString(), locations[1]._id.toString()]
);
});
it('updates top-level populated() after $pop() removes the last subdoc', async function() {
const { Trip, locations, trip, getPopulatedLocationIds } = await createTestContext();
const fromDb = await Trip.findById(trip._id).orFail().populate('stops.location');
assert.deepStrictEqual(
getPopulatedLocationIds(fromDb),
locations.map(location => location._id.toString())
);
fromDb.stops.$pop();
assert.deepStrictEqual(
getPopulatedLocationIds(fromDb),
[locations[0]._id.toString(), locations[1]._id.toString()]
);
});
async function createTestContext() {
const locationSchema = new Schema({ name: String });
const stopSchema = new Schema({
location: { type: Schema.Types.ObjectId, ref: 'Location' },
sequence: Number
});
const tripSchema = new Schema({
name: String,
stops: [stopSchema]
});
const Location = db.model('Location', locationSchema);
const Trip = db.model('Trip', tripSchema);
const locations = await Location.create([
{ name: 'Boston' },
{ name: 'Chicago' },
{ name: 'Denver' }
]);
const trip = await Trip.create({
name: 'Route planning',
stops: [
{ location: locations[0]._id, sequence: 2 },
{ location: locations[1]._id, sequence: 1 },
{ location: locations[2]._id, sequence: 3 }
]
});
return { Trip, locations, trip, getPopulatedLocationIds };
}
function getPopulatedLocationIds(trip) {
return trip.populated('stops.location').map(id => id.toString());
}
});
});