Mongodb query params ( combine $gt with $in ) - node.js

I'm using api query params library
rooms=2,4,5&rooms>=6
this gives
rooms: { '$gte': 6, '$in': [ 2, 4, 5 ] }
This won't work but if i search with only rooms>=6 it works or with rooms=2,4,5.
How i can combine $gte with $in ?

It sounds like you want to OR if the same key has multiple conditions. (I want all rooms, 2, 4, 5 OR greater than 6.)
To do that in mongo:
$or: [
{rooms: { $gte: 6}},
{rooms: { $in: [2, 4, 5]}}
]

You can also use not in for your query like
db.inventory.find( { rooms: { $nin: [ 1, 6] } } )

So guys i did this with $nin operator.
First i'm using api-query-params library.
let { filter, skip, limit, sort } = aqp(req.query)
It can parse query param --- rooms=1,2,3,4,5
into 'rooms': { '$in': [ 4, 3, 2, 1, 0, 5 ] }
But if we want when user adds 5 to be $gte 5
function addOrOperator(filter, field) {
//if selected value is undefined return
if(!filter[field]) return;
//values to match
let array = [0,1,2,3,4,5];
//if 5 is only selected returned value is 'rooms': 5
if(filter[field] === 5) {
filter[field] = {
$gte: 5
}
}
if(filter[field].$in) {
if(filter[field].$in.includes(5)) {
let difference = array.filter(x => !filter[field].$in.includes(x));
filter[field] = {
$nin: [...difference, null, ""]
}
}
}
return filter;
}
Then we are calling this function inside controller
addOrOperator(filter, 'rooms'); // all filters and key that we want to select

Related

Workaround for projection issue in mongo?

We have project where we used projection like this:
const filter = { id };
const projection = {
providers: { $elemMatch: { providerId: ObjectId(providerId) } },
"providers.payments": 1,
"providers.tax.vatPayer": 1,
"providers.tax.vat": 1
}
return super.findOne(filter, projection);
As you may know, this is illegal after 4.4. Is there any workaround how to filter specific item in array and then specify which fields of this array to project?
What I've tried:
const filter = { id, "providers.providerId": ObjectId(providerId) };
const projection = {
"providers.$.payments": 1,
"providers.$.tax.vatPayer": 1,
"providers.$.tax.vat": 1
}
return super.findOne(filter, projection);
const filter = { id, "providers.providerId": ObjectId(providerId) };
const projection = {
"providers.$": 1,
"providers.payments": 1,
"providers.tax.vatPayer": 1,
"providers.tax.vat": 1
}
return super.findOne(filter, projection);
const filter = { id };
const projection = {
"providers.$.payments": 1,
"providers.$.tax.vatPayer": 1,
"providers.$.tax.vat": 1,
providers: { $elemMatch: { providerId: ObjectId(providerId) } }
}
return super.findOne(filter, projection);
const filter = { id };
const projection = {
providers: {
$elemMatch: { providerId: ObjectId(providerId) },
payments: 1,
"tax.vatPayer": 1,
"tax.vat": 1
}
}
return super.findOne(filter, projection);
Thanks a lot for any suggestions!
$elemMatch in projection
The $elemMatch operator limits the contents of an <array> field from the query results to contain only the first element matching the $elemMatch condition.
return super.findOne(
{ id: id },
{ providers: { $elemMatch: { providerId: ObjectId(providerId) } } }
)
Playground
$ projection
The positional $ operator limits the contents of an <array> to return either:
The first element that matches the query condition on the array.
The first element if no query condition is specified for the array (Starting in MongoDB 4.4).
Both the $ operator and the $elemMatch operator project the first matching element from an array based on a condition.
$elemMatch in match condition and $ projection
return super.findOne(
{ id: id, providers: { $elemMatch: { providerId: ObjectId(providerId) } } },
{ "providers.$": 1 }
)
Playground
dot(.) notation in match condition and $ projection
return super.findOne(
{ id: id, "providers.providerId": ObjectId(providerId) },
{ "providers.$": 1 }
)
Playground
Positional Operator Placement Restriction here:
Starting in MongoDB 4.4, the $ projection operator can only appear at the end of the field path; e.g. "field.$" or "fieldA.fieldB.$".
Positional Operator and $slice Restriction here:
Starting in MongoDB 4.4, find and findAndModify projection cannot include $slice projection expression as part of a $ projection expression.

nodejs+mongodb access node.js object with document's _id field value in query

I am learning Mongodb to use with NodeJS. The below is my code:
let configMap = {
'1': 10,
'2': 12,
'3': 13
}
let myData = await mongo_groups_collection.find({
_id: {
$in: [1,2,3]
},
active: true
}).project({
'_id': 1,
'name': 1,
'members': 1,
'messages': {
$slice: [-(configMap[_id]), (DEFAULT_PAGE_SIZE_FILL * PAGE_SIZE) + (PAGE_SIZE - ((configMap[_id]) % PAGE_SIZE))] //Need to use _id to get configMap value
}
}).toArray();
I am using "configMap" nodeJS variable in Mongo slice function to retrieve certain number of elements from the array. To retrieve I need to lookup using "_id" field value which I am not getting and encounter _id "undefined" error.
That's because (configMap[_id]) gets compiled in node.js code but not on database as a query. So in code when this query gets compiled it's not able to find _id since you've not declared it as the way you did it for configMap variable.
You're expecting _id value to be actual _id from document. So, in case if you use (configMap[$_id]) it doesn't work that way cause you can't club a javascript object with documents field.
You can still do this using aggregate query like below - where we actually add needed v value as a field to document for further usage :
/** Make `configMap` an array of objects */
var configMap = [
{ k: 1, v: 10 },
{ k: 2, v: 12 },
{ k: 3, v: 13 },
];
let myData = await mongo_groups_collection.aggregate([
{
$match: { _id: { $in: [1, 2, 3] }, active: true }
},
/** For each matched doc we'll iterate on input `configMap` array &
* find respective `v` value from matched object where `k == _id` */
{
$addFields: {
valueToBeSliced: {
$let: {
vars: { matchedK: { $arrayElemAt: [ { $filter: { input: configMap, cond: { $eq: ["$$this.k", "$_id"] } } }, 0 ] } },
in: "$$matchedK.v",
}
}
}
},
{
$project: {
messages: { $slice: ["$messages", { $subtract: [0, "$valueToBeSliced"] }, someInputValue ] },
name: 1, members: 1
}
}
]).toArray();
Ref : $let
Note :
In projection you don't need to mention this '_id': 1 as it's included by default.
Since your query is using .find() you might be using $slice-projection-operator which can't be used in aggregation, instead we need to use $slice-aggregation-operator, both does same thing but have syntax variation, you need to refer to docs for that.
Also in someInputValue you need to pass in value of (DEFAULT_PAGE_SIZE_FILL * PAGE_SIZE) + (PAGE_SIZE - ((configMap[_id]) % PAGE_SIZE)) - So try to use aggregation operators $divide to get the value. We're doing { $subtract: [0, "$valueToBeSliced"] } to convert positive valueToBeSliced to negative as we can't just do like -valueToBeSliced which we usually do in javaScript.

Why is addToSet in mongodb not working in an array field?

The documents in the collection have an array field of sub-documents, each with a counter that should be increased up to three, but if the array doesn't have a sub-document with a given key it should create it with the default values.
The documentation for $addToSet says:
Behavior
$addToSet only ensures that there are no duplicate items added to the set and does not affect existing duplicate elements. $addToSet does not guarantee a particular ordering of elements in the modified set.
Missing Field
If you use $addToSet on a field is absent in the document to update, $addToSet creates the array field with the specified value as its element.
The problem is that the array field is not created in the document if it doesn't exist, as stated in the documentation.
This is what I'm currently using to accomplish the operation:
// increase counter in element of array if element exist and counter is less than 3
collection.updateOne({
key_1,
"array.key_2": key_2,
"array.counter": {$lt: 3}
}, {
$inc: {"array.$.counter": 1}
})
.then(res => {
console.log("!!!!1:", res.modifiedCount, res.upsertedId, res.upsertedCount, res.matchedCount);
if (res.matchedCount) return res.matchedCount;
// create element in array with the default values,
// also create the array field if it doesn't exist
collection.updateOne({
key_1
}, {
$addToSet: {
array: {key_2, counter: 1}
}
})
.then(res => {
console.log("!!!!2:", res.modifiedCount, res.upsertedId, res.upsertedCount, res.matchedCount);
return res.matchedCount;
})
.catch(e => console.log(e))
})
.catch(e => console.log(e))
Using upsert in the second query, it creates the array field if it doesn't exist but then when array.counter reaches 3 in subsequent calls to increase its value, the operation creates a new sub-document in the array with the same values for array.key_2 and array.date, effectible duplicating the entry although with array.counter set to 1 instead of 3.
I'm using mongo version 4.2.1
Update:
Below there is a sample document before trying to run the operation on the second subdocument:
{
"key_1": 1,
"array": [
{
"key_2": 1,
"counter" 1
}, {
"key_2": 2,
"counter" 3
}
]
}
This is what I'm getting as a result when using upsert:
{
"key_1": 1,
"array": [
{
"key_2": 1,
"counter" 1
}, {
"key_2": 2,
"counter" 3
}, {
"key_2": 2,
"counter" 1
}
]
}
The operation is duplicating the second subdocument in array, but if upsert is not used then the array field is not created if it's not already in the parent document, which is the oposite of the expected behavior for $addToSet from what it says in the documentation.
Update 2
These are the steps to reproduce the issue:
Run the operation with key_1 set to 1, and upsert disabled. None of the queries modifies the document. The array field is not created.
{
"key_1": 1
}
Enable upsert and run the operation again. The array field is created in the second query:
{
"key_1": 1,
"array": [
{
"key_2": 1,
"counter" 1
}
]
}
Run the operation again twice more. The first query modifies the document twice:
{
"key_1": 1,
"array": [
{
"key_2": 1,
"counter" 3
}
]
}
Run the operation once more. The first query doesn't modifies the document. The second query creates a duplicate:
{
"key_1": 1,
"array": [
{
"key_2": 1,
"counter" 3
}, {
"key_2": 1,
"counter" 1
}
]
}
Please try this :
var key_2Value = 2;
var firstFilterQuery = {
key_1: 1,
array: {
$elemMatch: {
"key_2": key_2Value,
"date": 'someDate',
"conter": { $lte: 3 }
}
}
}
var secondFilterQuery = {
key_1: 1,
"array.key_2": {$ne: key_2Value}
}
var defaultDoc = {key_2 : key_2Value, "date": 'someDefaultDate',counter: 1}
Query :
collection.bulkWrite([
{
updateOne:
{
"filter": firstFilterQuery,
"update": { $inc: { "array.$.conter": 1 } }
}
}, {
updateOne:
{
"filter": secondFilterQuery,
"update": { $push: { array: defaultDoc }
}
}
}
])
With the above query, you can achieve what you wanted in one DB call(at any given case only one 'updateOne' should update the DB), Output should look something like :
{
"acknowledged" : true,
"deletedCount" : 0.0,
"insertedCount" : 0.0,
"matchedCount" : 1.0,
"upsertedCount" : 0.0,
"insertedIds" : {},
"upsertedIds" : {}
}

Mongoose return document only if all sub-list elements match query

I have simple structure like this:
{
_id: "4f23f23f432f43" //random _id
list: [
{
price: 8
},
{
price: 13
},
{
price: 17
},
]
}
Above example is simple schema, which in base is list of few objects.
My problem is that I cannot get this to work:
dbQuery.menu = {
$elemMatch : {
price: {
$gte: request.query.minPrice
}
}
}
I need to find only these documents which all array elements matches this query. Meaning, return object only when it has list which contains only values greater (or equal) than specified query value.
Now it returns all object which at least one list element is higher than specified value, which is wrong.
UPDATE:
If you want to add a maxPrice field to the query condition, it must be a an $or-combined statement, not $and. Because n >= minPrice && n <= maxPrice should equal to !(n < minPrice || n > maxPrice).
So the query now should look like:
YourModel.find({
'list': {
$not: {
$elemMatch: {
'price': {
$or: [
{ $lt: request.query.minPrice },
{ $gt: request.query.maxPrice }
]
}
}
}
}
});
=====
(This is old answer)
Try this:
YourModel.find({
'list': {
$not: {
$elemMatch: {
'price': {
$lt: request.query.minPrice
}
}
}
}
});

Handling concurrent modification w/ mongoose

I have a couple places in my application that uses mongoose where I need to handle concurrent modification.
I know there is a version '__v' in all my documents. Most everything I look at points back to this blog:
http://aaronheckmann.tumblr.com/post/48943525537/mongoose-v3-part-1-versioning
Which outlines using the __v field to protect against concurrent array modification within the document. ie:
posts.update({ _id: postId, __v: versionNumber }
, { $set: { 'comments.3.body': updatedText }})
Do mongoose use the __v field automatically in any way to validate documents when they are saved? Or do you always have to add it to your query statement?
After reviewing the Mongoose source codes, the __v is really only used for arrays, the VERSION_WHERE is set when updating the array path.
if ('$push' == op || '$pushAll' == op || '$addToSet' == op) {
self.$__.version = VERSION_INC;
}
// now handling $set, $unset
else if (/\.\d+\.|\.\d+$/.test(data.path)) {
// subpath of array
self.$__.version = VERSION_WHERE;
}
And per this answer,
var t = Test();
t.name = 'hi'
t.arr = [1, 2, 3, 4, 5, 6];
t.save(function (err, result) {
console.log(result);
// use Mongoose pull method on the array
t.arr.pull(3);
t.save(function(err2, result2) {
console.log(result2)
});
});
Results:
{ __v: 0,
name: 'hi',
_id: 53f59d2a6522edb12114b98c,
arr: [ 1, 2, 3, 4, 5, 6 ] }
{ __v: 1,
name: 'hi',
_id: 53f59d2a6522edb12114b98c,
arr: [ 1, 2, 4, 5, 6 ] }

Resources