Skip to content

Commit fd4da49

Browse files
authored
Merge pull request #13285 from Automattic/vkarpov15/gh-13190
fix(update): handle casting doubly nested arrays with $pullAll
2 parents 9f8fe5d + 110e542 commit fd4da49

File tree

4 files changed

+136
-17
lines changed

4 files changed

+136
-17
lines changed

lib/helpers/discriminator/mergeDiscriminatorSchema.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ module.exports = function mergeDiscriminatorSchema(to, from, path) {
4343
// base schema has a given path as a single nested but discriminator schema
4444
// has the path as a document array, or vice versa (gh-9534)
4545
if ((from[key].$isSingleNested && to[key].$isMongooseDocumentArray) ||
46-
(from[key].$isMongooseDocumentArray && to[key].$isSingleNested)) {
46+
(from[key].$isMongooseDocumentArray && to[key].$isSingleNested) ||
47+
(from[key].$isMongooseDocumentArrayElement && to[key].$isMongooseDocumentArrayElement)) {
4748
continue;
4849
} else if (from[key].instanceOfSchema) {
4950
if (to[key].instanceOfSchema) {

lib/schema/DocumentArrayElement.js

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*!
2+
* Module dependencies.
3+
*/
4+
5+
'use strict';
6+
7+
const MongooseError = require('../error/mongooseError');
8+
const SchemaType = require('../schematype');
9+
const SubdocumentPath = require('./SubdocumentPath');
10+
const getConstructor = require('../helpers/discriminator/getConstructor');
11+
12+
/**
13+
* DocumentArrayElement SchemaType constructor.
14+
*
15+
* @param {String} path
16+
* @param {Object} options
17+
* @inherits SchemaType
18+
* @api public
19+
*/
20+
21+
function DocumentArrayElement(path, options) {
22+
this.$parentSchemaType = options && options.$parentSchemaType;
23+
if (!this.$parentSchemaType) {
24+
throw new MongooseError('Cannot create DocumentArrayElement schematype without a parent');
25+
}
26+
delete options.$parentSchemaType;
27+
28+
SchemaType.call(this, path, options, 'DocumentArrayElement');
29+
30+
this.$isMongooseDocumentArrayElement = true;
31+
}
32+
33+
/**
34+
* This schema type's name, to defend against minifiers that mangle
35+
* function names.
36+
*
37+
* @api public
38+
*/
39+
DocumentArrayElement.schemaName = 'DocumentArrayElement';
40+
41+
DocumentArrayElement.defaultOptions = {};
42+
43+
/*!
44+
* Inherits from SchemaType.
45+
*/
46+
DocumentArrayElement.prototype = Object.create(SchemaType.prototype);
47+
DocumentArrayElement.prototype.constructor = DocumentArrayElement;
48+
49+
/**
50+
* Casts `val` for DocumentArrayElement.
51+
*
52+
* @param {Object} value to cast
53+
* @api private
54+
*/
55+
56+
DocumentArrayElement.prototype.cast = function(...args) {
57+
return this.$parentSchemaType.cast(...args)[0];
58+
};
59+
60+
/**
61+
* Casts contents for queries.
62+
*
63+
* @param {String} $cond
64+
* @param {any} [val]
65+
* @api private
66+
*/
67+
68+
DocumentArrayElement.prototype.doValidate = function(value, fn, scope, options) {
69+
const Constructor = getConstructor(this.caster, value);
70+
71+
if (value && !(value instanceof Constructor)) {
72+
value = new Constructor(value, scope, null, null, options && options.index != null ? options.index : null);
73+
}
74+
75+
return SubdocumentPath.prototype.doValidate.call(this, value, fn, scope, options);
76+
};
77+
78+
/**
79+
* Clone the current SchemaType
80+
*
81+
* @return {DocumentArrayElement} The cloned instance
82+
* @api private
83+
*/
84+
85+
DocumentArrayElement.prototype.clone = function() {
86+
this.options.$parentSchemaType = this.$parentSchemaType;
87+
const ret = SchemaType.prototype.clone.apply(this, arguments);
88+
delete this.options.$parentSchemaType;
89+
90+
ret.caster = this.caster;
91+
ret.schema = this.schema;
92+
93+
return ret;
94+
};
95+
96+
/*!
97+
* Module exports.
98+
*/
99+
100+
module.exports = DocumentArrayElement;

lib/schema/documentarray.js

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66

77
const ArrayType = require('./array');
88
const CastError = require('../error/cast');
9+
const DocumentArrayElement = require('./DocumentArrayElement');
910
const EventEmitter = require('events').EventEmitter;
1011
const SchemaDocumentArrayOptions =
1112
require('../options/SchemaDocumentArrayOptions');
1213
const SchemaType = require('../schematype');
13-
const SubdocumentPath = require('./SubdocumentPath');
1414
const discriminator = require('../helpers/model/discriminator');
1515
const handleIdOption = require('../helpers/schema/handleIdOption');
1616
const handleSpreadDoc = require('../helpers/document/handleSpreadDoc');
@@ -74,25 +74,14 @@ function DocumentArrayPath(key, schema, options, schemaOptions) {
7474
});
7575
}
7676

77-
const parentSchemaType = this;
78-
this.$embeddedSchemaType = new SchemaType(key + '.$', {
77+
const $parentSchemaType = this;
78+
this.$embeddedSchemaType = new DocumentArrayElement(key + '.$', {
7979
required: this &&
8080
this.schemaOptions &&
81-
this.schemaOptions.required || false
81+
this.schemaOptions.required || false,
82+
$parentSchemaType
8283
});
83-
this.$embeddedSchemaType.cast = function(value, doc, init) {
84-
return parentSchemaType.cast(value, doc, init)[0];
85-
};
86-
this.$embeddedSchemaType.doValidate = function(value, fn, scope, options) {
87-
const Constructor = getConstructor(this.caster, value);
88-
89-
if (value && !(value instanceof Constructor)) {
90-
value = new Constructor(value, scope, null, null, options && options.index != null ? options.index : null);
91-
}
9284

93-
return SubdocumentPath.prototype.doValidate.call(this, value, fn, scope, options);
94-
};
95-
this.$embeddedSchemaType.$isMongooseDocumentArrayElement = true;
9685
this.$embeddedSchemaType.caster = this.Constructor;
9786
this.$embeddedSchemaType.schema = this.schema;
9887
}

test/model.update.test.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3346,6 +3346,35 @@ describe('model: updateOne: ', function() {
33463346
assert.equal(fromDb.children[0].name, 'Luke Skywalker');
33473347
});
33483348

3349+
it('works with doubly nested arrays with $pullAll (gh-13190)', async function() {
3350+
const multiArraySchema = new Schema({
3351+
_id: false,
3352+
label: String,
3353+
arr: [Number]
3354+
});
3355+
3356+
const baseTestSchema = new Schema({
3357+
baseLabel: String,
3358+
mArr: [[multiArraySchema]]
3359+
});
3360+
3361+
const Test = db.model('Test', baseTestSchema);
3362+
3363+
const arrB = new Test({
3364+
baseLabel: 'testx',
3365+
mArr: [[{ label: 'testInner', arr: [1, 2, 3, 4] }]]
3366+
});
3367+
await arrB.save();
3368+
const res = await Test.updateOne(
3369+
{ baseLabel: 'testx' },
3370+
{ $pullAll: { 'mArr.0.0.arr': [1, 2] } }
3371+
);
3372+
assert.equal(res.modifiedCount, 1);
3373+
3374+
const { mArr } = await Test.findById(arrB).lean().orFail();
3375+
assert.deepStrictEqual(mArr, [[{ label: 'testInner', arr: [3, 4] }]]);
3376+
});
3377+
33493378
describe('converts dot separated paths to nested structure (gh-10200)', () => {
33503379
it('works with new Model(...)', () => {
33513380
const Payment = getPaymentModel();

0 commit comments

Comments
 (0)