How to update a collection item without removing non existing values send on the query?

Given this simple example, an item saved as:

{
  "test": {
    "a1": "a1"
  }
}

When i call collection.updateOne with $set and the following object:
await collection.updateOne(doc, {$set: {"test": {"a2": "a2"}}}, {upsert: true});
it replaces the saved test object to:

{
  "test": {
    "a2": "a2"
  }
}

The a1 key will no longer exist as it also does not exist in the object sent.

I’m trying to update saved items with the values passed in the object **without ** removing non-existing values, I mean:
{"test": {"a2": "a2"}} would make the item be updated to:

{
  "test": {
    "a1": "a1",
    "a2": "a2"
  }
}

I have written a function to recursively parse objects/arrays and build a query for this use case.

In my mind it looks like a mess and I’m not sure if it’s correct:

// The function attempts to parse arrays and objects recursively and recreate the query
function buildUpdateQuery(object, prefix = '') 
{
    let updateQuery = {};

    for (let [key, value] of Object.entries(object))
    {
        let newPrefix = prefix ? `${ prefix }.${ key }` : key;

        if (Array.isArray(value)) 
        {
            for (let i = 0; i < value.length; i++) 
            {
                let arrayPrefix = `${newPrefix}.${i}`;
                if (typeof value[i] === 'object' && value[i] !== null)
                    updateQuery = {...updateQuery, ...buildUpdateQuery(value[i], arrayPrefix)};
                else
                    updateQuery[arrayPrefix] = value[i];
            }
        }
        else if (typeof value === 'object' && value !== null)
            updateQuery = {...updateQuery, ...buildUpdateQuery(value, newPrefix)};
        else
            updateQuery[newPrefix] = value;
    }

    return updateQuery;
}

export const set = async (req, res /*key, object*/) =>   // Using mongodb and express
{
    try
    {
        const [collection, key] = req.body.key.split("|");
        if (collection === undefined || key === undefined)
            return res ? res.send('fail') : 'fail'

        const doc = buildUpdateQuery(req.body.object);
        let r = await db.collection(collection).updateOne({ [key]: { $exists: true } }, { $set: doc }, { upsert: true });

        r = r.modifiedCount ? 'ok' : 'fail';
        return res ? res.send(r) : r
    }
    catch (error)
    {
        console.log(`\n[set]\n${ JSON.stringify(req.body, null, 2) }\n\n`, error);
        return res ? res.send('fail') : 'fail'
    }
}

Calling the set function with:

{"key": "vars|test", "object": {"test": {"a2": "a2"}}}

at const doc = buildUpdateQuery(req.body.object);

doc is {test.a2: 'a2'} and it indeed updates

{"test": {"a1": "a1"}} to {"test": {"a1": "a1"},{"a2":"a2"}}

I’m afraid it could mess up a collection at some time, maybe parsing something wrong.

I’m aware that i could first get the existing object with:

const document = await db.collection(collection).findOne({ [key]: { $exists: true } });

and then modify it with the new values, but that’s not what I’m looking for.

Is there any built-in method in MongoDB for such a thing?

If not, is the buildUpdateQuery correct or there’s any other better approach for this use case?

Hi @michel_guedes, welcome to the community forum!

You need to specify the mutation part of your update using dot notation. It would look like this:

await collection.updateOne(doc, {$set: {"test.a2":  "a2"}}, {upsert: true})
2 Likes

I’m already doing this in the buildUpdateQuery function
let newPrefix = prefix ? ${ prefix }.${ key } : key;

Im asking if Mongo theres any builtin to do this automatic instead of relying on building a function to iterate the object appending the .
Im afraid my function could parse something wrong and mess-up a collection item.

There is no tool for doing it automatically. I believe it wouldn’t be practicle due to the database’s dynamic schema characteristic, there is no “wrong” schema from the database’s perspective.

Try to use the dot notation directly, even if the document doesn’t exist, I think it would work.

1 Like