Mongoose: BUG: update with $[] and arrayfilters does not set arrayelements.

Created on 2 Oct 2018  路  5Comments  路  Source: Automattic/mongoose

Mongodb: 4.0.0, Mongoose: 5.3.0.
The $set part never udpates anything in my docs when I use updateMany (or simple update) with arrayFields and $[].
Schema:

export const TableBookingSchema = new Schema({
tables: {
        type: [
            {
                tableID: String,
                numberOfChairs: Number,
                reserved: Boolean,
                location: String,
                inDoors: Boolean,
                reservedDuring: [
                    {
                        reservedFrom: Date,
                        reservedTo: Date,
                        reservedFor: {
                            type: mongoose.Schema.Types.ObjectId,
                            ref: 'Consumer'
                        }
                    }
                ]
            }
        ],
        required: false
    },
});

interface ITableBooking extends mongoose.Document {
tables: any[];
}

export const TableBooking = mongoose.model<ITableBooking>('TableBooking', TableBookingSchema);

update function:
TableBooking.updateMany({}, { '$set': { 'tables.$[].reservedDuring.$[arr].reservedFrom': Date.now() } }, { arrayFilters: [{ 'arr.reservedFrom': { $gte: Date.now() } }], multi: true }, ).then(function (doc)){}

The result is: {"n":2,"nModified":0,"ok":1}. For some reason no matter which array element I want to update (reservedFrom, reservedTo, reservedFor), nothing ever changes, even tough I have 2 docs that meet the conditions in the arrayFilters section.
Sorry in advance if I missed something.

confirmed-bug

Most helpful comment

@vkarpov15 should mongoose cast in arrayFilter? Or should we add a note to the docs about it?

@Jugis mongoose doesn't cast the date in the arrayFilters option for you, and Date.now() returns a number, so the arrayFilter doesn't match anything. You'll need to change your $gte value to a date with new Date() or new Date(Date.now()) and it should work.

Here's a complete repro script that shows it working:

7079.js

#!/usr/bin/env node
'use strict';

const assert = require('assert');
const mongoose = require('mongoose');
const { Schema, connection} = mongoose;
const GH = 'gh7041';
const URI = `mongodb://localhost:27017/${GH}`;
const OPTS = { family: 4, useNewUrlParser: true };

const schema = new Schema({
  tables: {
    type: [{
      reservedDuring: [
        {
          reservedFrom: Date,
          reservedTo: Date
        }
      ]
    }]
  }
});

const Test = mongoose.model('test', schema);

const test = new Test({ 
  tables: [
    {
      reservedDuring: [{
        reservedFrom: Date.now(),
        reservedTo: Date.now() + 3600000
      }]
    },
    {
      reservedDuring: [{
        reservedFrom: Date.now() + 3600000,
        reservedTo: Date.now() + 2 * 3600000 
      }]
    },
    {
      reservedDuring: [{
        reservedFrom: Date.now() + 2 * 3600000,
        reservedTo: Date.now() + 3 * 3600000
      }]
    },
  ]
});

const test1 = test.tables[0].reservedDuring[0].reservedFrom;
const test2 = test.tables[1].reservedDuring[0].reservedFrom;
const test3 = test.tables[2].reservedDuring[0].reservedFrom;

async function run() {
  await mongoose.connect(URI, OPTS);
  await connection.dropDatabase();
  await Test.create(test);
  const cond = {};
  const update = { 
    $set: { 
      'tables.$[].reservedDuring.$[arr].reservedFrom': Date.now() 
    } 
  };
  const opts = { 
    arrayFilters: [{ 'arr.reservedFrom': { $gte: new Date(Date.now()) } }],
  };
  const res = await Test.updateMany(cond, update, opts);
  assert.strictEqual(res.nModified, 1);
  const found = await Test.findOne({}); 
  const found1 = found.tables[0].reservedDuring[0].reservedFrom;
  const found2 = found.tables[1].reservedDuring[0].reservedFrom;
  const found3 = found.tables[2].reservedDuring[0].reservedFrom;

  assert.deepEqual(found1, test1);
  assert.notDeepEqual(found2, test2);
  assert.notDeepEqual(found3, test3);
  console.log('All Assertions Pass.');
  await connection.close();
}

run();

Output:

issues: ./7079.js
All Assertions Pass.
issues:

All 5 comments

@vkarpov15 should mongoose cast in arrayFilter? Or should we add a note to the docs about it?

@Jugis mongoose doesn't cast the date in the arrayFilters option for you, and Date.now() returns a number, so the arrayFilter doesn't match anything. You'll need to change your $gte value to a date with new Date() or new Date(Date.now()) and it should work.

Here's a complete repro script that shows it working:

7079.js

#!/usr/bin/env node
'use strict';

const assert = require('assert');
const mongoose = require('mongoose');
const { Schema, connection} = mongoose;
const GH = 'gh7041';
const URI = `mongodb://localhost:27017/${GH}`;
const OPTS = { family: 4, useNewUrlParser: true };

const schema = new Schema({
  tables: {
    type: [{
      reservedDuring: [
        {
          reservedFrom: Date,
          reservedTo: Date
        }
      ]
    }]
  }
});

const Test = mongoose.model('test', schema);

const test = new Test({ 
  tables: [
    {
      reservedDuring: [{
        reservedFrom: Date.now(),
        reservedTo: Date.now() + 3600000
      }]
    },
    {
      reservedDuring: [{
        reservedFrom: Date.now() + 3600000,
        reservedTo: Date.now() + 2 * 3600000 
      }]
    },
    {
      reservedDuring: [{
        reservedFrom: Date.now() + 2 * 3600000,
        reservedTo: Date.now() + 3 * 3600000
      }]
    },
  ]
});

const test1 = test.tables[0].reservedDuring[0].reservedFrom;
const test2 = test.tables[1].reservedDuring[0].reservedFrom;
const test3 = test.tables[2].reservedDuring[0].reservedFrom;

async function run() {
  await mongoose.connect(URI, OPTS);
  await connection.dropDatabase();
  await Test.create(test);
  const cond = {};
  const update = { 
    $set: { 
      'tables.$[].reservedDuring.$[arr].reservedFrom': Date.now() 
    } 
  };
  const opts = { 
    arrayFilters: [{ 'arr.reservedFrom': { $gte: new Date(Date.now()) } }],
  };
  const res = await Test.updateMany(cond, update, opts);
  assert.strictEqual(res.nModified, 1);
  const found = await Test.findOne({}); 
  const found1 = found.tables[0].reservedDuring[0].reservedFrom;
  const found2 = found.tables[1].reservedDuring[0].reservedFrom;
  const found3 = found.tables[2].reservedDuring[0].reservedFrom;

  assert.deepEqual(found1, test1);
  assert.notDeepEqual(found2, test2);
  assert.notDeepEqual(found3, test3);
  console.log('All Assertions Pass.');
  await connection.close();
}

run();

Output:

issues: ./7079.js
All Assertions Pass.
issues:

Oh wow it works!
I was convinced that my "opts" part was correct: I presumed that in "{"n":2,"nModified":0,"ok":1}" the "n" part indicated how many matches were found for the criteria in "arrayFilters".
Thank you so much, and sorry about the nonsensical question. :)

yeah, the "n" is the number of documents matched by the overall query condition. It's not nonsensical at all, I think it would be neat if mongoose cast that value to a date for us 馃憤

Sorry I missed this notification, it got lost in my inbox. Will investigate.

Confirmed, here's a repro of where array filter casting fails. We need to cast the arrayFilters array.

const assert = require('assert');
const mongoose = require('mongoose');
mongoose.set('debug', true);

const GITHUB_ISSUE = `gh7079`;
const connectionString = `mongodb://localhost:27017/${ GITHUB_ISSUE }`;
const { Schema } = mongoose;

run().then(() => console.log('done')).catch(error => console.error(error.stack));

async function run() {
  await mongoose.connect(connectionString);
  await mongoose.connection.dropDatabase();

  const schema = new Schema({ comments: [{ date: Date }] });
  const Model = mongoose.model('Test', schema);

  await Model.create({
    comments: [
      { date: new Date('2018-06-01') },
      { date: new Date('2017-06-01') },
      { date: new Date('2018-06-02') }
    ]
  });

  await Model.updateMany(
    {},
    { $set: { 'comments.$[x].author': '2018-01-01' } },
    { arrayFilters: [{ 'x.author': { $gte: '2018-01-01' } }] }
  );

  console.log(await Model.findOne());
}
Was this page helpful?
0 / 5 - 0 ratings

Related issues

p3x-robot picture p3x-robot  路  3Comments

weisjohn picture weisjohn  路  3Comments

adamreisnz picture adamreisnz  路  3Comments

CodeurSauvage picture CodeurSauvage  路  3Comments

Igorpollo picture Igorpollo  路  3Comments