Given stringX = "term|quote|text". I want to do a query on mongodb of the following documents:
[{
id:"1",
description:"term"
},
{
id:"2",
description:"term2"
},
{
id:"3",
description:"term3"
}]
How do I find the document with id:1? The pseudo query is the like of:
"find document, which description is included (to be found) in stringX".
Create a query expression with the $in operator from the string. Your ultimate query object should resemble:
var query = {
"description": { "$in": ["term", "quote", "text"] }
}
Or if you prefer using $or:
var query = {
"$or": [
{ "description": "term" },
{ "description": "quote" },
{ "description": "text" }
]
}
which you can then use in your query asin
db.collection.find(query)
To get the query in the first form, split() the input string with the pipe as the delimiter and use the resulting array as the $in operator's value:
var query = {
"description": { "$in": stringX.split("|") }
};
db.collection.find(query)
For the second form, consider using the Array.map() method on the split string, something like the following
var orOperator = stringX.split("|").map(function (str){ return { "description": str } }),
query = { "$or": orOperator };
db.collection.find(query)
Related
I have this document structure in the collection:
{"_id":"890138075223711744",
"guildID":"854557773990854707",
"name":"test-lab",
"game": {
"usedWords":["akşam","elma","akım"]
}
}
What is the most efficient way to get its fields except the array (it can be really large), and at the same time, see if an item exists in the array ?
I tried this:
let query = {_id: channelID}
const options = { sort: { name: 1 }, projection: { name: 1, "game.usedWords": { $elemMatch: { word}}}}
mongoClient.db(db).collection("channels").findOne(query, options);
but I got the error: "$elemMatch can not be used on nested fields"
If I've understood correctly you can use this query:
Using positional operator $ you can return only the matched word.
db.collection.find({
"game.usedWords": "akşam"
},
{
"name": 1,
"game.usedWords.$": 1
})
Example here
The output is only name and the matched word (also _id which is returned by default)
[
{
"_id": "890138075223711744",
"game": {
"usedWords": [
"akşam"
]
},
"name": "test-lab"
}
]
Document in mongodb collection 'users' is
{
"$oid": "5e612272bcb362513824ff9b",
"name": "abcd",
"email": "test#test.com",
"cart": {
"items": [{
"productId": {
"$oid": "5e614367cae25319c4388288"
},
"quantity": {
"$numberInt": "1"
}
}]
}
}
For a particular users._id and a productId in cart.items, I need to increase the quantity by 1
My nodejs code is
IncrCartItem(prodId){
const db=getDb()
return db
.collection('users').
updateOne({_id: new mongodb.ObjectId(this._id),'this.cart.items.productId': new mongodb.ObjectId(prodId)},{$inc : {'this.cart.items.quantity':1}})
}
Is the query right for checking multiple conditions in updateOne()??
You're kinda there, to "and" them all together you just keep appending them, however the field is wrong and you need to use the positional operator ($) - https://docs.mongodb.com/manual/reference/operator/update/positional/
const filter = {
_id: new mongodb.ObjectId(this._id),
'cart.items.productId': new mongodb.ObjectId(prodId)
};
const update = {
$inc : { 'this.cart.items.$.quantity': 1 }
};
IncrCartItem(prodId){
const db=getDb()
return db
.collection('users').
updateOne(filter,update)
}
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;
}
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.
how can i create a query in before hook that will match nested object and return desired result ? I have objects like following in rethinkdb and I want to use something like hook.params.query = {paymentAccounting : {Contact : {Name : 'XYZ'}}} in the before find hook . It seems working for a straight forward object but what about nested object?How to match paymentAccounting >Contact > Name ?
{
"id": "5f45451a-653a-4dc7-b135-25dec3aa25b5",
"paymentAccounting": {
"Account": {
"value": "4"
},
"Amount": 50,
"Contact": {
"ContactID": "68",
"Name": "XYZ"
},
"Invoice": {
"Date": "2018-01-05",
"InvoiceID": "230"
},
"PaymentID": "237"
}
}
feathers-rethinkdb does not support querying nested fields at the moment but it is possible to customize the query and add your custom query parameters in a hook:
app.service('mesages').hooks({
before: {
find(context) {
const query = this.createQuery(context.params.query);
const searchString = "my search string";
hook.params.rethinkdb = query.filter(doc => {
return doc('paymentAccounting')('Contact')('name').eq('XYZ')
});
}
}
});