Conditionally updating items in mongoose query - node.js

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);
}
)

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": [ "bill#stalyns.org", "bill#example.com" ]
}
}
Then your "two-fold" new issue is that:
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.
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": "bill#stalyns.org" },
{ "type": "email", "value": "bill#example.com" }
]
}
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 = "bill#nomail.com";
// emailAddressMatch = "bill#example.com";
// 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.

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.

Related

Conditionally adding term and multi_match filters to elasticsearch with nodejs client

I am using ElasticSearch 7.9 with a nodejs client. I have the following query :
{
"query": {
"bool": {
"must":[
{ "terms" : { "id" : ["5f0d06fb5112231eb89eb819", "5f0d06fb5112231eb89eb817"] } },
{"query_string": {
"query": "(News) OR (Entertainent)",
"fields": [ "topics", "subTopics", "categories"]
}
},
{
"multi_match": {
"query": "publisher",
"fields": ["text", "name", "title", "subtitle", "description"]
}
}
]
}
}
}
I want to be able to conditionally add the terms filter for id if list of ids coming in to the nodejs/js function is not empty. Similarly for the multi_match query text as well. Add the multi_filter only if the incoming text is not empty
Should all queries be pre-constructed or is it possible to have conditional blocks and add only if the empty text or array of Ids are not empty.
My current method expects both ids and text input to the method to have valid values but these could be empty. Do I separate methods to handle the empty conditions
export const searchResults = async (text, ids) => {
const response = await client.search({
index: "new_index", //customer.id
type: "_doc",
body: {
query: {
bool: {
must: [
{terms: {"id": ids}},
{query_string: {
query: "(News) OR (Entertainent)",
fields: [ "topics", "subTopics", "categories"]
}
},
{
multi_match: {
query: text,
fields: ["text", "name", "title", "subtitle", "description"],
}
}
],
}
}
},
});
return response?.hits?.hits.map(({_source}) => _source) || [];
};
any help is really appreciated.
In a production environment and managing client based applications you should use search-templates, where you can use conditional blocks of queries. Besides, if you would want to change your query you would not neet to redeploy your app, just change it on ES.

Convert query builder conditions to MongoDB operations including nested array of subdocuments

I am building an application in Angular 8 on the client side and NodeJS 12 with MongoDB 4 / Mongoose 5 on the server side. I have a query generated by the Angular2 query builder module. The Angular query builder object is sent to the server.
I have a server-side controller function that converts the Angular query object to MongoDB operations. This is working perfectly for generating queries for top-level properties such as RecordID and RecordType. This is also working for building nested and/or conditions.
However, I need to also support querying an array of subdocuments (the "Items" array in the example schema).
Schema
Here is the example schema I am trying to query:
{
RecordID: 123,
RecordType: "Item",
Items: [
{
Title: "Example Title 1",
Description: "A description 1"
},
{
Title: "Example 2",
Description: "A description 2"
},
{
Title: "A title 3",
Description: "A description 3"
},
]
}
Working example
Top-level properties only
Here's an example of the query builder output with and/or conditions on top-level properties only:
{ "condition": "or", "rules": [ { "field": "RecordID", "operator": "=", "value": 1 }, { "condition": "and", "rules": [ { "field": "RecordType", "operator": "=", "value": "Item" } ] } ] }
Here's the query builder output after it has been converted to MongoDB operations on top-level properties only:
{ '$expr': { '$or': [ { '$eq': [ '$RecordID', 1 ] }, { '$and': [ { '$eq': [ '$RecordType', 'Item' ] } ] } ] }}
that converts the angular query object to mongodb operators.
Here is the existing query conversion function that
const conditions = { "and": "$and", "or": "$or" };
const operators = { "=": "$eq", "!=": "$ne", "<": "$lt", "<=": "$lte", ">": "$gt", ">=": "$gte" };
const mapRule = rule => ({
[operators[rule.operator]]: [ "$"+rule.field, rule.value ]
});
const mapRuleSet = ruleSet => {
return {
[conditions[ruleSet.condition]]: ruleSet.rules.map(
rule => rule.operator ? mapRule(rule) : mapRuleSet(rule)
)
}
};
let mongoDbQuery = { $expr: mapRuleSet(q) };
console.log(mongoDbQuery);
Issue
The function works for top-level properties only such as RecordID and RecordType, but I need to extend it to support the Items array of subdocuments.
Apparently, to query properties in nested arrays of subdocuments, the $elemMatch operator must be used, based on this related question. However, in my case, the $expr is necessary to build the nested and/or conditions so I can't simply switch to $elemMatch.
QUESTION
How can I extend the query conversion function to also support $elemMatch to query arrays of subdocuments? Is there a way to get the $expr to work?
UI query builder
Here is the UI query builder with the nested "Items" array of subdocuments. In this example, the results should match RecordType equals "Item" AND Items.Title equals "Example Title 1" OR Items.Title contains "Example".
Here is the output generated by the UI query builder. Note: The field and operator property values are configurable.
{"condition":"and","rules":[{"field":"RecordType","operator":"=","value":"Item"},{"condition":"or","rules":[{"field":"Items.Title","operator":"=","value":"Example Title 1"},{"field":"Items.Title","operator":"contains","value":"Example"}]}]}
UPDATE: I may have found a query format that works with the nested and/or conditions with the $elemMatch as well. I had to remove the $expr operator since $elemMatch does not work inside of expressions. I took inspiration from the answer to this similar question.
This is the query that is working. The next step will be for me to figure out how to adjust the query builder conversion function to create the query.
{
"$and": [{
"RecordType": {
"$eq": "Item"
}
},
{
"$or": [{
"RecordID": {
"$eq": 1
}
},
{
"Items": {
"$elemMatch": {
"Title": { "$eq": "Example Title 1" }
}
}
}
]
}
]
}
After more research I have a working solution. Thanks to all of the helpful responders who provided insight.
The function takes a query from the Angular query builder module and converts it to a MongoDB query.
Angular query builder
{
"condition": "and",
"rules": [{
"field": "RecordType",
"operator": "=",
"value": "Item"
}, {
"condition": "or",
"rules": [{
"field": "Items.Title",
"operator": "contains",
"value": "book"
}, {
"field": "Project",
"operator": "in",
"value": ["5d0699380a2958e44503acfb", "5d0699380a2958e44503ad2a", "5d0699380a2958e44503ad18"]
}]
}]
}
MongoDB query result
{
"$and": [{
"RecordType": {
"$eq": "Item"
}
}, {
"$or": [{
"Items.Title": {
"$regex": "book",
"$options": "i"
}
}, {
"Project": {
"$in": ["5d0699380a2958e44503acfb", "5d0699380a2958e44503ad2a", "5d0699380a2958e44503ad18"]
}
}]
}]
}
Code
/**
* Convert a query object generated by UI to MongoDB query
* #param query a query builder object generated by Angular2QueryBuilder module
* #param model the model for the schema to query
* return a MongoDB query
*
*/
apiCtrl.convertQuery = async (query, model) => {
if (!query || !model) {
return {};
}
const conditions = { "and": "$and", "or": "$or" };
const operators = {
"=": "$eq",
"!=": "$ne",
"<": "$lt",
"<=": "$lte",
">": "$gt",
">=": "gte",
"in": "$in",
"not in": "$nin",
"contains": "$regex"
};
// Get Mongoose schema type instance of a field
const getSchemaType = (field) => {
return model.schema.paths[field] ? model.schema.paths[field].instance : false;
}
// Map each rule to a MongoDB query
const mapRule = (rule) => {
let field = rule.field;
let value = rule.value;
if (!value) {
value = null;
}
// Get schema type of current field
const schemaType = getSchemaType(rule.field);
// Check if schema type of current field is ObjectId
if (schemaType === 'ObjectID' && value) {
// Convert string value to MongoDB ObjectId
if (Array.isArray(value)) {
value.map(val => new ObjectId(val));
} else {
value = new ObjectId(value);
}
// Check if schema type of current field is Date
} else if (schemaType === 'Date' && value) {
// Convert string value to ISO date
console.log(value);
value = new Date(value);
}
console.log(schemaType);
console.log(value);
// Set operator
const operator = operators[rule.operator] ? operators[rule.operator] : '$eq';
// Create a MongoDB query
let mongoDBQuery;
// Check if operator is $regex
if (operator === '$regex') {
// Set case insensitive option
mongoDBQuery = {
[field]: {
[operator]: value,
'$options': 'i'
}
};
} else {
mongoDBQuery = { [field]: { [operator]: value } };
}
return mongoDBQuery;
}
const mapRuleSet = (ruleSet) => {
if (ruleSet.rules.length < 1) {
return;
}
// Iterate Rule Set conditions recursively to build database query
return {
[conditions[ruleSet.condition]]: ruleSet.rules.map(
rule => rule.operator ? mapRule(rule) : mapRuleSet(rule)
)
}
};
let mongoDbQuery = mapRuleSet(query);
return mongoDbQuery;
}

Find when the keys are unkown in mongodb

How can I getthe data that has email as abc#gmail.com in mongoDB?I don't know the Key Name and I want to iterate through all the data.
I have data like this:
{
"_id":"5c0a1589a5a41b2ae707317b",
"test1":{
"email":"abc#gmail.com",
"phoneNo":"123456897",
"endpointId":"test1"
}
}
{
"_id":"5c0a1989a5a41b2ae807317b",
"test2":{
"email":"abc#gmail.com",
"phoneNo":"123456897",
"endpointId":"test2"
}
}
{
"_id":"5c0a1989a5a41b2ae807317b",
"test2":{
"email":"pqr#gmail.com",
"phoneNo":"123456897",
"endpointId":"test3"
}
}
But the object key is not known at the time of searching. I want to iterate through all the data and get matched data that has specific email.
If I know the key name like test1,test2 etc then I can use find({test1:{...}}) but Here I don't know the key value.
So, how can I do that?
You can use below aggregation using $objectToArray in mongodb 3.4 and above
db.collection.aggregate([
{ "$addFields": {
"field": { "$objectToArray": "$$ROOT" }
}},
{ "$match": { "field.v.email": "abc#gmail.com" }},
{ "$project": { "field": 0 }}
])
I am assuming you get the objects in array type.
I made a method named findObject. This method will take the object array and the desired email.
Finally, return the first object, that matched with the email.
const data = [{
"_id":"5c0a1589a5a41b2ae707317b",
"test1":{
"email": "abc#gmail.com",
"phoneNo": "123456897",
"endpointId":"test1"
}
},
{
"_id":"5c0a1989a5a41b2ae807317b",
"test2":{
"email": "abc#gmail.com",
"phoneNo": "123456897",
"endpointId":"test2"
}
},
{
"_id":"5c0a1989a5a41b2ae807317b",
"test2":{
"email": "pqr#gmail.com",
"phoneNo": "123456897",
"endpointId": "test3"
}
}];
const findObject = (data, email) => {
for (let index=0; index<data.length; index++) {
const currentData = data[index];
for (let property in currentData) {
if (property != '_id' && currentData[property].email == email) {
return currentData;
}
}
}
return null;
}
let desiredObject;
const desiredEmail = 'abc#gmail.com';
desiredObject = findObject(data, desiredEmail);
console.log(desiredObject);
And the output will be
{ _id: '5c0a1589a5a41b2ae707317b',
test1:
{ email: 'abc#gmail.com',
phoneNo: '123456897',
endpointId: 'test1' } }
I think you can't do query on totally unknown field! if you could change your schema see here for more info, also you could write script to migrate to a new DB with new schema:
// new doc instance
{
"_id":"5c0a1589a5a41b2ae707317b",
"obj": {
"name": "test1"
"email":"abc#gmail.com"
"phoneNo":"123456897",
"endpointId":"test1"
}
},
{
"_id":"5c0a1989a5a41b2ae807317b",
"obj": {
"name": "test2"
"email":"abc#gmail.com"
"phoneNo":"123456897",
"endpointId":"test2"
}
},
{
"_id":"5c0a1989a5a41b2ae807317b",
"obj": {
"name": "test3"
"email":"pqr#gmail.com"
"phoneNo":"123456897",
"endpointId":"test3"
}
}
otherwise, check this may works correctly. if all of them is not effective so make a query to get all of your data as an Array and use filter method on it:
Model.find({}, (err, docs) => {
const result = docs.filter((doc) => {
for (key in doc) {
if (doc[key].email === 'abc#gmail.com')
return doc;
}
});
console.log(result);
});

Node.js, mongodb and filtered queries

I'm using the native API of mongodb and I'm trying to query the data on my collection.
This is my filter object:
{
email: 'admin#email.it',
login: { '$exists': true }
}
and this is one document that it should find:
{
"_id": "5829cd89a48a7813f0cc7429",
"timestamp": "2016-11-14T14:43:18.705Z",
"login": {
"clientIPaddr": "::1",
"clientProxy": "none"
},
"userData": {
"sessdata": {
"sessionID": "CRTZaqpaUs-ep0J6rvYMBlQTdDakGwle",
"email": "admin#email.it",
"token": "3PlfQBVBoftlIpl-FizeCW5TbYMgcYTl4ZPTkHMVyxqv-TldWb_6U3eusJ27gtI64v7EqjT-KPlUUwkJK7hPnQ"
}
}
}
But the query doesn't return anything! Why?
It doesn't return anything because the email field is in an embedded document within the userData field, hence it tries to look for an email field at a higher level within the document that does not exist.
To make this work, you need to modify the filter or create a new query object which includes the embedded field, albeit the key will be in dot notation field i.e. the query should resemble
{
"userData.sessdata.email": "admin#email.it",
"login": { "$exists": true }
}
You can use the bracket notation to create the required field. For example:
var filter = {
email: 'admin#email.it',
login: { '$exists': true }
},
query = {};
Object.keys(filter).forEach(function(key){
if (key === "email") {
query["userData.sessdata."+key] = filter[key];
} else {
query[key] = filter[key];
}
});
console.log(JSON.stringify(query, null, 4));
Output
{
"userData.sessdata.email": "admin#email.it",
"login": {
"$exists": true
}
}
You can then use the query object in your find() query
collection.find(query).toArray(function(err, docs) {
// access the docs array here
})

$addToSet Based on Object key exists

I have array 'pets': [{'fido': ['abc']} that is a embeded document. When I add a pet to the array, how can I check to see if that pet already exists? For instance, if I added fido again... how can I check if only fido exists and not add it? I was hoping I could use $addToSet but I only want to check part of the set(the pets name).
User.prototype.updatePetArray = function(userId, petName) {
userId = { _id: ObjectId(userId) };
return this.collection.findOneAndUpdate(userId,
{ $addToSet: { pets: { [petName]: [] } } },
{ returnOriginal: false,
maxTimeMS: QUERY_TIME });
Result of adding fido twice:
{u'lastErrorObject': {u'updatedExisting': True, u'n': 1}, u'ok': 1, u'value': {u'username': u'bob123', u'_id': u'56d5fc8381c9c28b3056f794', u'location': u'AT', u'pets': [{u'fido': []}]}}
{u'lastErrorObject': {u'updatedExisting': True, u'n': 1}, u'ok': 1, u'value': {u'username': u'bob123', u'_id': u'56d5fc8381c9c28b3056f794', u'location': u'AT', u'pets': [{u'fido': [u'abc']}, {u'fido': []}]}}
If there is always going to be "variable" content within each member of the "pets" array ( i.e petName as the key ) then $addToSet is not for you. At least not not at the array level where you are looking to apply it.
Instead you basically need an $exists test on the "key" of the document being contained in the array, then either $addToSet to the "contained" array of that matched key with the positional $ operator, or where the "key" was not matched then $push directly to the "pets" array, with the new inner content directly as the sole array member.
So if you can live with not returning the modified document, then "Bulk" operations are for you. In modern drivers with bulkWrite():
User.prototype.updatePetArray = function(userId, petName, content) {
var filter1 = { "_id": ObjectId(userId) },
filter2 = { "_id": ObjectId(userId) },
update1 = { "$addToSet": {} },
update2 = { "$push": { "pets": {} } };
filter1["pets." + petName] = { "$exists": true };
filter2["pets." + petName] = { "$exists": false };
var setter1 = {};
setter1["pets.$." + petName] = content;
update1["$addToSet"] = setter1;
var setter2 = {};
setter2[petName] = [content];
update2["$push"]["pets"] = setter2;
// Return the promise that yields the BulkWriteResult of both calls
return this.collection.bulkWrite([
{ "updateOne": {
"filter": filter1,
"update": update1
}},
{ "updateOne": {
"filter": filter2,
"update": update2
}}
]);
};
If you must return the modified document, then you are going to need to resolve each call and return the one that actually matched something:
User.prototype.updatePetArray = function(userId, petName, content) {
var filter1 = { "_id": ObjectId(userId) },
filter2 = { "_id": ObjectId(userId) },
update1 = { "$addToSet": {} },
update2 = { "$push": { "pets": {} } };
filter1["pets." + petName] = { "$exists": true };
filter2["pets." + petName] = { "$exists": false };
var setter1 = {};
setter1["pets.$." + petName] = content;
update1["$addToSet"] = setter1;
var setter2 = {};
setter2[petName] = [content];
update2["$push"]["pets"] = setter2;
// Return the promise that returns the result that matched and modified
return new Promise(function(resolve,reject) {
var operations = [
this.collection.findOneAndUpdate(filter1,update1,{ "returnOriginal": false}),
this.collection.findOneAndUpdate(filter2,update2,{ "returnOriginal": false})
];
// Promise.all runs both, and discard the null document
Promise.all(operations).then(function(result) {
resolve(result.filter(function(el) { return el.value != null } )[0].value);
},reject);
});
};
In either case this requires "two" update attempts where only "one" will actually succeed and modify the document, since only one of the $exists tests is going to be true.
So as an example of that first case, the "query" and "update" are resolving after interpolation as:
{
"_id": ObjectId("56d7b759e955e2812c6c8c1b"),
"pets.fido": { "$exists": true }
},
{ "$addToSet": { "pets.$.fido": "ccc" } }
And the second update as:
{
"_id": ObjectId("56d7b759e955e2812c6c8c1b"),
"pets.fido": { "$exists": false }
},
{ "$push": { "pets": { "fido": ["ccc"] } } }
Given varibles of:
userId = "56d7b759e955e2812c6c8c1b",
petName = "fido",
content = "ccc";
Personally I would not be naming keys like this, but rather change the structure to:
{
"_id": ObjectId("56d7b759e955e2812c6c8c1b"),
"pets": [{ "name": "fido", "data": ["abc"] }]
}
That makes the update statements easier, and without the need for variable interpolation into the key names. For example:
{
"_id": ObjectId(userId),
"pets.name": petName
},
{ "$addToSet": { "pets.$.data": content } }
and:
{
"_id": ObjectId(userId),
"pets.name": { "$ne": petName }
},
{ "$push": { "pets": { "name": petName, "data": [content] } } }
Which feels a whole lot cleaner and can actually use an "index" for matching, which of course $exists simply cannot.
There is of course more overhead if using .findOneAndUpdate(), since this is afterall "two" actual calls to the server for which you need to await a response as opposed to the Bulk method which is just "one".
But if you need the returned document ( option is the default in the driver anyway ) then either do that or similarly await the Promise resolve from the .bulkWrite() and then fetch the document via .findOne() after completion. Albeit that doing it via .findOne() after the modification would not truly be "atomic" and could possibly return the document "after" another similar modification was made, and not only in the state of that particular change.
N.B Also assuming that apart from the keys of the subdocuments in "pets" as a "set" that your other intention for the array contained was adding to that "set" as well via the additional content supplied to the function. If you just wanted to overwrite a value, then just apply $set instead of $addToSet and similarly wrap as an array.
But it sounds reasonable that the former was what you were asking.
BTW. Please clean up by horrible setup code in this example for the query and update objects in your actual code :)
As a self contained listing to demonstrate:
var async = require('async'),
mongodb = require('mongodb'),
MongoClient = mongodb.MongoClient;
MongoClient.connect('mongodb://localhost/test',function(err,db) {
var coll = db.collection('pettest');
var petName = "fido",
content = "bbb";
var filter1 = { "_id": 1 },
filter2 = { "_id": 1 },
update1 = { "$addToSet": {} },
update2 = { "$push": { "pets": {} } };
filter1["pets." + petName] = { "$exists": true };
filter2["pets." + petName] = { "$exists": false };
var setter1 = {};
setter1["pets.$." + petName] = content;
update1["$addToSet"] = setter1;
var setter2 = {};
setter2[petName] = [content];
update2["$push"]["pets"] = setter2;
console.log(JSON.stringify(update1,undefined,2));
console.log(JSON.stringify(update2,undefined,2));
function CleanInsert(callback) {
async.series(
[
// Clean data
function(callback) {
coll.deleteMany({},callback);
},
// Insert sample
function(callback) {
coll.insert({ "_id": 1, "pets": [{ "fido": ["abc"] }] },callback);
}
],
callback
);
}
async.series(
[
CleanInsert,
// Modify Bulk
function(callback) {
coll.bulkWrite([
{ "updateOne": {
"filter": filter1,
"update": update1
}},
{ "updateOne": {
"filter": filter2,
"update": update2
}}
]).then(function(res) {
console.log(JSON.stringify(res,undefined,2));
coll.findOne({ "_id": 1 }).then(function(res) {
console.log(JSON.stringify(res,undefined,2));
callback();
});
},callback);
},
CleanInsert,
// Modify Promise all
function(callback) {
var operations = [
coll.findOneAndUpdate(filter1,update1,{ "returnOriginal": false }),
coll.findOneAndUpdate(filter2,update2,{ "returnOriginal": false })
];
Promise.all(operations).then(function(res) {
//console.log(JSON.stringify(res,undefined,2));
console.log(
JSON.stringify(
res.filter(function(el) { return el.value != null })[0].value
)
);
callback();
},callback);
}
],
function(err) {
if (err) throw err;
db.close();
}
);
});
And the output:
{
"$addToSet": {
"pets.$.fido": "bbb"
}
}
{
"$push": {
"pets": {
"fido": [
"bbb"
]
}
}
}
{
"ok": 1,
"writeErrors": [],
"writeConcernErrors": [],
"insertedIds": [],
"nInserted": 0,
"nUpserted": 0,
"nMatched": 1,
"nModified": 1,
"nRemoved": 0,
"upserted": []
}
{
"_id": 1,
"pets": [
{
"fido": [
"abc",
"bbb"
]
}
]
}
{"_id":1,"pets":[{"fido":["abc","bbb"]}]}
Feel free to change to different values to see how different "sets" are applied.
Please try this one with string template, here is one example running under mongo shell
> var name = 'fido';
> var t = `pets.${name}`; \\ string temple, could parse name variable
> db.pets.find()
{ "_id" : ObjectId("56d7b5019ed174b9eae2b9c5"), "pets" : [ { "fido" : [ "abc" ]} ] }
With the following update command, it will not update it if the same pet name exists.
> db.pets.update({[t]: {$exists: false}}, {$addToSet: {pets: {[name]: []}}})
WriteResult({ "nMatched" : 0, "nUpserted" : 0, "nModified" : 0 })
If the pets document is
> db.pets.find()
{ "_id" : ObjectId("56d7b7149ed174b9eae2b9c6"), "pets" : [ { "fi" : [ "abc" ] } ] }
After update with
> db.pets.update({[t]: {$exists: false}}, {$addToSet: {pets: {[name]: []}}})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
The result shows add the pet name if it does Not exist.
> db.pets.find()
{ "_id" : ObjectId("56d7b7149ed174b9eae2b9c6"), "pets" : [ { "fi" : [ "abc" ] }, { "fido" : [ ] } ] }

Resources