Meteor/MongoDB Aggregate Update $set Not Working

Hi,

I’m trying to run an update query on my database. I want to set a field if it does not exist. If it does, I want to leave it as it is.

Running this query in MongoDB works as expected:

db.getCollection('orders').update(
  { _id: "XXX" },
  [{ $set: {
    "acknowledgedAt": { $cond: [ { $not: ["$acknowledgedAt"] }, new Date(), "$acknowledgedAt" ] }
  }}]
)

However, I can’t seem to get this to work in Meteor. I’ve tried various things but get these errors:

Error: After filtering out keys not in the schema, your modifier is now empty

Or

Error: Expected '0' to be a modifier operator like '$set'

Using rawCollection returns an error saying that it only works on server collections (even though I am trying to update from a Method on the server side).

I have tried https://atmospherejs.com/monbro/mongodb-mapreduce-aggregation (which I could not get to work - I am assuming it is because it is old and not maintained). For the same reason, I have not tried https://github.com/meteorhacks/meteor-aggregate either.

This is my current code:

if(Meteor.isServer) {
  console.log('server..')
  OrdersCollection.rawCollection().aggregate([
    { $match: { _id: "XXX" } },
    [{
      $set: {
        hasBeenAcknowledged: true,
        "acknowledgedAt": { $cond: [ { $not: ["$acknowledgedAt"] }, "", "$acknowledgedAt" ] }
      }
    }]
  ])
}

Currently, this is not even updating the ‘hasBeenAcknowledged’ field.

I was using this earlier (which didn’t throw an error, but it did not update the acknowledgedAt field):

OrdersCollection.update(orderId, {
  $set: {
     hasBeenAcknowledged: true,
     hasBeenCompleted: true,
     completedAt: new Date(),
     acknowledgedAt: { $cond: [ { $not: ["$acknowledgedAt"] }, "", "$acknowledgedAt" ] }
  }
});

I’m probably missing something obvious, but I’m not sure what. I’d appreciate any advice.

In your query/update filter (ie the first object where you now have { _id: “XXX” }), add an $exists: false check like this:

db.records.find( { b: { $exists: false } } )

… and then you can keep the set operation nice and clean. Unless there’s something specific regarding aggregation that I am myself missing here.

Hi,

Thanks for you reply.

OrdersCollection.rawCollection().aggregate([
    { _id: "XXX", acknowledgedAt: { $exists: false } },
    {
        $set: {
          hasBeenAcknowledged: true,
          acknowledgedAt: new Date()
        }
    }
])

So my new query should be this?

I have tried to run this and a few variations of it but it didn’t work.

My aim is to always set 4 fields:

$set: {
     hasBeenAcknowledged: true,
     hasBeenCompleted: true,
     completedAt: new Date(),
     acknowledgedAt: // if this exists, leave it, else, set it as a new Date()
  }

The first 3 will always need to be set. I only want to set the ‘acknowledgedAt’ field if it has not already been set. The original question used a “”, but in reality it will need to be ‘new Date()’.

What am I missing?

Using this:

OrdersCollection.update({ _id: orderId, acknowledgedAt: { $exists: false } }, {
    $set: {
        hasBeenAcknowledged: true,
        hasBeenCompleted: true,
        completedAt: new Date(),
        acknowledgedAt: new Date()
    }
});

Only runs the whole query if $exists: false.

Is there a way I can run the whole query every time and only update acknowledgedAt: new Date() if it doesn’t exist?

I could make two separate database calls, I guess. But, I’d like to reduce the number of calls to only 1 if possible.

To always set the other 3 fields and have only the one field be conditional, all in a single call, is indeed trickier. Can’t help you here. Probably time to dig into mongodb docs :slight_smile:

Thank you for the help! I will keep looking.

Running this query directly in Mongo works as expected:

db.getCollection('orders').update(
  { _id: "XXX" },
  [
    { 
        $set: {
            hasBeenAcknowledged: true,
            hasBeenCompleted: true,
            completedAt: new Date(),
            "acknowledgedAt": { $cond: [ { $not: ["$acknowledgedAt"] }, new Date(), "$acknowledgedAt" ] }
        }
    }
  ]
)

It always updates all other fields and only updates acknowledgedAt if it doesn’t exist. Does Meteor work differently?

I get this error:

Error: Expected '0' to be a modifier operator like '$set'

in Meteor when running:

OrdersCollection.update(
        { _id: "XXX" },
        [
          { 
              $set: {
                  hasBeenAcknowledged: true,
                  hasBeenCompleted: true,
                  completedAt: new Date(),
                  "acknowledgedAt": { $cond: [ { $not: ["$acknowledgedAt"] }, new Date(), "$acknowledgedAt" ] }
              }
          }
        ]
      );

I’ve opted to solve this by using 2 database calls for now (so that I can move onto something else):

OrdersCollection.update(orderId, {
   $set: {
      hasBeenAcknowledged: true,
      hasBeenCompleted: true,
      completedAt: new Date()
   }
});
OrdersCollection.update(
    { _id: orderId, acknowledgedAt: { $exists: false } }, 
    { $set: { acknowledgedAt: new Date() } }
);

I wanted to leave some sort of solution here.

I suspect the MongoDB CLI sees the array and either ignores it by resolving [0] only, or loops through it under the covers. The MongoDB API which Meteor uses doesn’t do that, so I suspect removing the array on the modifier should work:

OrdersCollection.rawCollection().update(
        { _id: "XXX" },
        { 
            $set: {
                hasBeenAcknowledged: true,
                hasBeenCompleted: true,
                completedAt: new Date(),
                "acknowledgedAt": { $cond: [ { $not: ["$acknowledgedAt"] }, new Date(), $acknowledgedAt" ] }
            }
        }
    );

Note that I’ve used rawCollection() because as far as I know, $cond is not supported in minimongo.

Strictly speaking, rawCollection().update() returns a Promise, so should be waited on.

3 Likes

Hi @robfallows,

Thank you! I’ve managed to get it working now.

I added the snippet of code in your comment and added await (I was already using async, but did not realise I needed await on this call). That returned this error:

MongoError: The dollar ($) prefixed field '$cond' in 'acknowledgedAt.$cond' is not valid for storage.

I then made $set an array again and it worked!

My final working code is:

await OrdersCollection.rawCollection().update(
    { _id: "XXX" },
    [{ 
        $set: {
            hasBeenAcknowledged: true,
            hasBeenCompleted: true,
            completedAt: new Date(),
            "acknowledgedAt": { $cond: [ { $not: ["$acknowledgedAt"] }, new Date(), "$acknowledgedAt" ] }
        }
    }]
);
2 Likes