Skip to main content
Module

x/mongoose/test/types.documentarray.test.js

MongoDB object modeling designed to work in an asynchronous environment.
Go to Latest
File
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766
'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(done) { 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);
done(); });
it('#id', function(done) { let Subdocument = TestDoc();
let sub1 = new Subdocument(); sub1.title = 'Hello again to all my friends'; let id = sub1.id;
let a = new MongooseDocumentArray([sub1]); 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');
// test with custom string _id 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');
// test with custom number _id 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');
// test with object as _id 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); // undefined and null should not match a nonexistent _id assert.strictEqual(null, a.id(undefined)); assert.strictEqual(null, a.id(null));
// test when _id is a populated document 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');
done(); });
describe('inspect', function() { it('works with bad data', function(done) { let threw = false; const a = new MongooseDocumentArray([null]); try { a.inspect(); } catch (err) { threw = true; console.error(err.stack); } assert.ok(!threw); done(); }); });
describe('toObject', function() { it('works with bad data', function(done) { let threw = false; const a = new MongooseDocumentArray([null]); try { a.toObject(); } catch (err) { threw = true; console.error(err.stack); } assert.ok(!threw); done(); }); it('passes options to its documents (gh-1415) (gh-4455)', function(done) { const subSchema = new Schema({ title: { type: String } });
subSchema.set('toObject', { transform: function(doc, ret) { // this should not be called because custom options are // passed during MongooseArray#toObject() calls 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);
done(); }); it('uses the correct transform (gh-1412)', function(done) { 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); done(); }); });
describe('create()', function() { it('works', function(done) { 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); done(); }); });
describe('push()', function() { it('does not re-cast instances of its embedded doc', function(done) { const child = new Schema({ name: String, date: Date }); child.pre('save', function(next) { this.date = new Date(); next(); }); const schema = new Schema({ children: [child] }); const M = db.model('Test', schema); const m = new M(); m.save(function(err) { assert.ifError(err); M.findById(m._id, function(err, doc) { assert.ifError(err); const c = doc.children.create({ name: 'first' }); assert.equal(c.date, undefined); doc.children.push(c); assert.equal(c.date, undefined); doc.save(function(err) { assert.ifError(err); 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);
doc.save(function(err) { assert.ifError(err); M.findById(m._id, function(err, doc) { assert.ifError(err); assert.equal(doc.children.length, 3); doc.children.forEach(function(child) { assert.equal(doc.children[0].id, child.id); }); done(); }); }); }); }); }); });
it('corrects #ownerDocument() and index if value was created with array.create() (gh-1385)', function(done) { 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); done(); });
it('corrects #ownerDocument() if value was created with array.create() and set() (gh-7504)', function(done) { 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']); done(); });
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', function(done) { const Comments = new Schema(); Comments.add({ title: String, comments: [Comments] }); const BlogPost = new Schema({ title: String, comments: [Comments] });
const Post = db.model('BlogPost', BlogPost);
const 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);
p.save(function(err) { assert.ifError(err);
Post.findById(p._id, function(err, p) { assert.ifError(err);
p.comments[0].comments[0].comments[0].comments.push({ title: 'c4' }); p.save(function(err) { assert.ifError(err);
Post.findById(p._id, function(err, p) { assert.ifError(err); assert.equal(p.comments[0].comments[0].comments[0].comments[0].title, 'c4'); done(); }); }); }); }); });
describe('required (gh-6364)', function() { it('on top level', function(done) { 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); done(); });
it('in arr', function(done) { 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']); done(); }); });
describe('invalidate()', function() { it('works', function(done) { const schema = new Schema({ docs: [{ name: 'string' }] }); schema.pre('validate', function(next) { const subdoc = this.docs[this.docs.length - 1]; subdoc.invalidate('name', 'boo boo', '%'); next(); }); 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() { // has no parent array subdoc.invalidate('name', 'junk', 47); }); t.validate(function() { 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, '%'); done(); }); });
it('handles validation failures', function(done) { 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 }] }); m.save(function(err) { assert.equal(err.errors['docs.0.v'].value, 900); done(); }); });
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); });});