cnak2
cnak2

Reputation: 1841

Conditionally updating items in mongoose query

I have the following code and I'm trying to do two things. First I want to have my query have one condition where it finds the 'originator' value in a doc, but the second par of that is not to update if is also finds 'owner_id' is the same as originator.

The second part of what I'm trying to do is only set/update a field is it is being passed in. Can I use a ternary statement, something like below???

  Contacts.update(
    {
      'originator': profile.owner_id,
      'owner_id': !profile.owner_id
    }, 
    {
      $set: {
        (phoneNumber) ? ('shared.phones.$.phone_number': phoneNumber):null,
        (emailAddress) ? ('shared.emails.$.email_address': emailAddress):null
      }
    }, 
    {
      'multi': true
    },
    function(err) {
      err === null ? console.log('No errors phone updated for contacts.shared') : console.log('Error: ', err);
    }
  ) 

Upvotes: 7

Views: 12240

Answers (2)

Lachezar Raychev
Lachezar Raychev

Reputation: 2113

Here is a simple example with switch condition in some variation like this:

  const transfоrmFunc = function(val) {
    if(val){
      // do whatever you want with the value here
      return val; 
    }
    return null;
  };
  AnyModel.updateMany({ fieldId: { $in: ["MATCH1", "MATCH2"] } }, [
    {
      $set: {
        field2: {
          $switch: {
            branches: [
              {
                case: { $eq: ["$fieldId", "MATCH1"] },
                then: transfоrmFunc("$field3")
              },
              {
                case: { $eq: ["$fieldId", "MATCH2"] },
                then: transfоrmFunc("$field4.subfield")
              }
            ]
          }
        }
      }
    }
  ]);

That way you work with both record data and outside data and update conditionally. You can modify query conditions as pleased. Plus it's really fast.

Upvotes: 1

Neil Lunn
Neil Lunn

Reputation: 151220

You mean something like this:

var updateBlock = {};
if (phoneNumber)
  updateBlock['shared.phones.$.phone_number'] = phoneNumber;
if (emailAddress)
  updateBlock['shared.email.$.email_address'] = emailAddress;

Contacts.updateMany(
  { 
    "originator": profile.owner_id
    "owner_id": { "$ne": profile.owner_id }
  },
  { "$set": updateBlock },
  function(err, numAffected) {
     // work with callback
  }
)

That addresses your two "main" misconceptions here in that the "inequality" in the query condition requires the $ne operator and not the ! JavaScript expression. MongoDB does not use JavaScript expressions here for the query conditions.

The second "main" misconception is the construction of the "update block" with conditional keys. This is by contrast a "JavaScript Object" which you construct separately in order to specify only the keys you wish to effect.

However there is STILL A PROBLEM in that you want to use the positional $ operator. Presuming you actually have "arrays" in the document like this:

{
   "originator": "Bill",
   "owner_id": "Ted",
   "shared": {
     "phones": [ "5555 5555", "4444 4444" ],
     "email": [ "[email protected]", "[email protected]" ]
   }
}

Then your "two-fold" new issue is that:

  1. You must specify a query condition that matches the array element "in the query block" in order to obtain the "matched position" at which to update.

  2. You can only return ONE matched array index via use of the positional $ operator and NOT TWO as would be inherent to updating such a document.

For those reasons ( and others ) it is strongly discouraged to have "multiple arrays" within a single document. The far better approach is to use a "singular" array, and use properties to denote what "type" of entry the list item actually contains:

{
   "originator": "Bill",
   "owner_id": "Ted",
   "shared": [
     { "type": "phone", "value": "5555 5555" },
     { "type": "phone", "value": "4444 4444" },
     { "type": "email", "value": "[email protected]" },
     { "type": "email", "value": "[email protected]" }
   ]
}

In this way you can actually address the "matched" element in which to update:

// phoneNumberMatch = "4444 4444";
// phoneNumber = "7777 7777";
// emailAddress = null;            // don't want this one
// emailAddressMatch = null;       // or this one
// profile = { owner_id: "Bill" };

var query = {
  "originator": profile.owner_id,
  "owner_id": { "$ne": profile.owner_id },
  "shared": {
    "$elemMatch": {
      "type": (phoneNumber) ? "phone" : "email",
      "value": (phoneNumber) ? phoneNumberMatch : emailAddressMatch
    }
  }
};

var updateBlock = {
  "$set": {
    "shared.$.value": (phoneNumber) ? phoneNumber : emailAddress
  }
};

Contacts.updateMany(query, updateBlock, function(err, numAffected) {
  // work with callback
})

In such a case and with a "binary" choice then you "can" use ternary conditions in construction since you are not reliant on "naming keys" within the construction.

If you want "either, or indeed both" supplied values in combination then you need a bit more advanced statement:

// phoneNumberMatch = "5555 5555";
// phoneNumber = "7777 7777";
// emailAddress = "[email protected]";
// emailAddressMatch = "[email protected]";
// profile = { owner_id: "Bill" };

var query = {
  "originator": profile.owner_id,
  "owner_id": { "$ne": profile.owner_id },
  "$or": []
};

var updateBlock = { "$set": {} };
var arrayFilters = [];

if (phoneNumber) {
  // Add $or condition for document match
  query.$or.push(
    {
      "shared.type": "phone",
      "shared.value": phoneNumberMatch
    }
  );

  // Add update statement with named identifier
  updateBlock.$set['shared.$[phone].value'] = phoneNumber;

  // Add filter condition for named identifier
  arrayFilters.push({
    "phone.type": "phone",
    "phone.value": phoneNumberMatch
  })
}

if (emailAddress) {
  // Add $or condition for document match
  query.$or.push(
    {
      "shared.type": "email",
      "shared.value": emailAddressMatch
    }
  );

  // Add update statement with named identifier
  updateBlock.$set['shared.$[email].value'] = emailAddress;

  // Add filter condition for named identifier
  arrayFilters.push({
    "email.type": "email",
    "email.value": emailAddressMatch
  })
}

Contacts.updateMany(query, updateBlock, arrayFilters, function(err, numAffected) {
  // work with callback
})

Noting of course here that the positional filtered $[<identifier>] syntax from MongoDB 3.6 and upwards is required in order to effect multiple array elements within a single update statement.

Much the same applies to the "original" structure I first described using "multiple" arrays in the documents instead of named properties on a "singular" array as the above examples deal with:

var query = {
  "originator": "Bill",
  "owner_id": { "$ne": "Bill" },
  "$or": []
};

var updateBlock = { "$set": {} };
var arrayFilters = [];

if (phoneNumber) {
  query.$or.push({
    "shared.phones": phoneNumberMatch
  });

  updateBlock.$set['shared.phones.$[phone]'] = phoneNumber;

  arrayFilters.push({
    "phone": phoneNumberMatch
  });
}

if (emailAddress) {
  query.$or.push({
    "shared.email": emailAddressMatch
  });

  updateBlock.$set['shared.email.$[email]'] = emailAddress;

  arrayFilters.push({
    "email": emailAddressMatch
  });
}

Contacts.updateMany(query, updateBlock, arrayFilters, function(err, numAffected) {
  // work with callback
})

Of course if you don't even have arrays at all ( the question posted lacks any example document ) then positional matches are not even needed in any form, but you do however still "conditionally" construct JavaScript object "keys" via construction code blocks. You cannot "conditionally" specify a "key" in JSON-like notation.

Upvotes: 18

Related Questions