skip to Main Content

I’ve a customer document with attributes array. I want to insert new attributes and update already existing ones.

Already existing "attributes" within the document:

[{
      key: "email",
      value: "[email protected]"
    }, {
      key: "name",
      value: "Somebody"
}]

I want to** add or update** within "attributes"

    [{
      key: "email",
      value: "[email protected]"
    }, {
      key: "mobile",
      value: "1234567890"
    }]

So, the result should be:

[{
key: "email",
value: "[email protected]"
},{
key: "name",
value: "Somebody"
},{
key: "mobile",
value: "1234567890"
}]

I achieved the result using below written code. But I want to use only MongoDB to get the same result.

const { attributes, uid } = req.body;
const customer: any = await getCustomer({ uid });
const oldAttributes = customer?.attributes.filter((attr: any) => !attributes.find((att: any) => att.key === attr.key));
const updatedAttributes = [...oldAttributes, ...attributes];
const response = await updateCustomer({ uid }, { $set: { attributes: updatedAttributes } } );

2

Answers


  1. You can set upsert: true in options when you call updateCustomer as follows:

    const options = { upsert: true };
    
    const response = await updateCustomer({ uid }, { $set: { attributes: updatedAttributes } }, options );
    

    You can refer to more at: https://www.mongodb.com/docs/drivers/node/current/fundamentals/crud/write-operations/upsert/

    Login or Signup to reply.
  2. Since using upsert would overwrite the old attributes with the new attributes, rather than updating just the attributes array, this would need a full pipeline, ending with a merge back into the collection.

    Assuming a document like this:

    [
      {
        _id: 12345,
        attributes: [
          {
            key: "email",
            value: "[email protected]"
          },
          {
            key: "name",
            value: "Somebody"
          }
        ]
      }
    ]
    

    Steps at a high-level:

    1. Convert the array of {key: "<key_name>", value: "<value>"} to an object having {<key_name>: <value>, ...}
      • This assumes each key is unique with the array of attributes.
      • Otherwise, the last occurrence overwrites the first.
    2. Merge it with the values provided by the user: so existing values get updated, new value get added, fields not provided remain.
    3. Convert it back to an array of {key: "<key_name>", value: "<value>"}

    User input would be required for the first $match step for the document _id and then for the $mergeObjects step where you add user provided values.

    Notes:

    • It’s possible to use a $let or nested "$set": { "attributes": ... } but it feels cleaner and clearer as separate pipeline steps.
    • If you know the attributes will always be only name, email, mobile, then you can change the object to array step with an explicit set/project with [{"key": "name", "value": "$attribute.name}, {"key": "email", "value": "$attribute.value"}, ...] in the objecToArray step not require the re-mapping of labels after that.
    • For the final merge, I have put whenNotMatched: "fail" since you probably can’t use this for records which don’t already exist.
    • Added an alternate playground using db.collection.update() instead of aggregate. So the final $merge is not required. But you will need to add {upsert: false, multi: false} for the 3rd param; which finally becomes the same length/complexity as the original anyway.
    db.collection.aggregate([
      {
        "$match": {
          // whichever doc you're updating
          "_id": 12345
        }
      },
      {
        // unwrap the key value pairs to actual "key: value"
        "$set": {
          "attributes": {
            "$arrayToObject": {
              "$map": {
                "input": "$attributes",
                "as": "i",
                "in": {
                  k: "$$i.key",
                  v: "$$i.value"
                }
              }
            }
          }
        }
      },
      {
        // merge it with new/updated data
        "$set": {
          "attributes": {
            "$mergeObjects": [
              "$attributes",
              {
                // this is where you add new field and set existing ones
                "email": "[email protected]",
                "mobile": "1234567890"
              }
            ]
          }
        }
      },
      {
        // convert it back to an arry of [{"k": <k1>, v: <v1>}, {"k": k2, "v": <v2>}]
        "$set": {
          "attributes": {
            "$objectToArray": "$attributes"
          }
        }
      },
      {
        // convert "k" --> "key" and "v" --> "value"
        "$set": {
          "attributes": {
            "$map": {
              "input": "$attributes",
              "as": "e",
              "in": {
                "key": "$$e.k",
                "value": "$$e.v"
              }
            }
          }
        }
      },
      {
        // write to the collection
        $merge: {
          into: "collection",
          on: "_id",
          whenMatched: "replace",
          whenNotMatched: "fail"
        }
      }
    ])
    

    Result:

    [
      {
        "_id": 12345,
        "attributes": [
          {
            "key": "email",
            "value": "[email protected]"
          },
          {
            "key": "name",
            "value": "Somebody"
          },
          {
            "key": "mobile",
            "value": "1234567890"
          }
        ]
      }
    ]
    

    Mongo Playground

    Alternate, with collection.update().

    PS. I think $objectToArray really needs some options for customising the label for the key and value.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search