MongoDB query on objects in an array based on condition - mongodb

I have a players inventory that looks like this.
let inventory = [ { name: 'Wood', amount: 6 }, { name: 'Stone', amount: 2 } ]
This is the players resources.
I also have a list of craftable items.
{"name":"CraftingTable","craftingReagents":[{"name":"Stone","amount":"2"}]}
{"name":"CraftingTable2","craftingReagents":[{"name":"Wood","amount":"4"}]}
{"name":"CraftingTable3","craftingReagents":[{"name":"Wood","amount":"5"},{"name":"Stone","amount":"2"}]}
The items schema is as such
let itemSchema = new mongoose.Schema(
{
name:String,
craftingReagents: [
{
name: String,
amount: Number
}
]
}
);
I want a query that will return all craftable objects where the players inventory has sufficient resources to do so.
For example with 6 Wood and 2 Stone the query should return all 3 crafting tables, as the player has sufficient resources to craft all three
This is what I have so far, and I am very lost. Please help!
itemModel.find({
craftingReagents: {
$all: [{
$elemMatch: {
name: {
$in: [
'Wood'
]
},
amount: { $lte: 6 }
}
},
{
$elemMatch: {
name: {
$in: [
'Stone'
]
},
amount: { $lte: 2 }
}
}
]
}
});
Heres the thing. The players inventory can change to an infinite number of different resources. AND the craftable objects can have an infinite number of different requirments. I meerly gave an example of what the inventory, and craftable objects could look like.
How do you list documents that the player has enough resources to craft?

Try this :
itemModel.find({
$or: [{ craftingReagents: { $elemMatch: { name: 'Wood', amount: { $lte: 6 } } } },
{ craftingReagents: { $elemMatch: { name: 'Stone', amount: { $lte: 2 } } } }]
})

Related

How to make and requests in mongodb queries

I've worked on this for about an hour now and I can't figure anything out that works so sorry if this is obvious.
I want my query to only return results where every result matches, but right now it returns a result if at least one match is found.
My document looks like this...
{
country: 'Great Britain',
data: [
{
school: 'King Alberts',
staff: [
{
name: 'John',
age: 37,
position: 'Head Teacher'
},
{
name: 'Grace',
age: 63,
position: 'Retired'
},
{
name: 'Bob',
age: 25,
position: 'Receptionist'
}
]
},
{
school: 'St Johns',
staff: [
{
name: 'Alex',
age: 22,
position: 'Head of Drama'
},
{
name: 'Grace',
age: 51,
position: 'English Teacher'
},
{
name: 'Jack',
age: 33,
position: 'Receptionist'
}
]
},
// ... more schools
]
}
The query I'm currently making looks like...
{ 'data.staff.name': { $in: names } }
and the 'names' array that is being provided looks like ['Bob', 'Grace', 'John', 'Will', 'Tom']. Currently both schools are being returned when I make this query, I think it's because the 'names' array contains 'Grace' which is a name present at both schools and so the document it matching. Does anyone know if there's a query I could make so mongodb only returns the school object if every name in the 'names' array is a member of staff at the school?
You need to use the aggregation pipeline for this, after matching the document we'll just filter out the none matching arrays, like so:
db.collection.aggregate([
{
$match: {
"data.staff.name": {
$in: names
}
}
},
{
$addFields: {
data: {
$filter: {
input: "$data",
cond: {
$eq: [
{
$size: {
"$setIntersection": [
"$$this.staff.name",
names
]
}
},
{
$size: "$$this.staff"
}
]
}
}
}
}
}
])
Mongo Playground

Deleting an item with condition in MongoDB?

I want to remove a product from the Cart by checking its quantity. Its quantity should be decremented by 1 unless it reaches zero, and after that, it should pull out from the product array of the Cart.
here is my Logic : (I want to perform the pull and decrement operation inside the single query. But I m stuck on how to perform these two operations together by a simple condition in MongoDb)
const cart = await Cart.findOneAndUpdate({"products.productId": req.body.productId}, {$inc: {"products.$.quantity": -1}}, {new: true})
await Cart.update({"products.productId": req.body.productId}, {$pull: {quantity: 0}})
here is the model for clarification:
import mongoose from 'mongoose';
const cartSchema = new mongoose.Schema({
userId: {
type: String,
required: true,
},
products: [
{
productId: {
type: String,
},
quantity: {
type: Number,
default: 1
}
}
]
}, {timestamps: true});
const Cart = new mongoose.model('Cart', cartSchema);
export default Cart;
Thanks :)
There is no straight way to do this in single regular update query.
To improve your approach you can try this,
first query to check productId and quantity should greater than 1
const cart = await Cart.updateOne(
{
products: {
$elemMatch: {
productId: req.body.productId,
quantity: { $gt: 1 }
}
}
},
{ $inc: { "products.$.quantity": -1 } }
);
Playground
second query if the first query's result is nModified is 0 then pull the product, by checking condition productId and quantity equal-to 1
if (cart.nModified === 0) {
await Cart.updateOne(
{
products: {
$elemMatch: {
productId: req.body.productId,
quantity: { $eq: 1 }
}
}
},
{ $pull: { products: { productId: req.body.productId } } }
)
}
Playground
If you really want to do using single query you can try update with aggregation pipeline starting from MongoDB 4.2,
$map to iterate loop of products array and check condition, if the productId matches then increment/decrement quantity by $add operator otherwise return current quantity
$filter to iterate loop of above result and check condition if productId and quantity is not zero
await Cart.updateOne(
{ "products.productId": req.body.productId },
[{
$set: {
products: {
$filter: {
input: {
$map: {
input: "$products",
in: {
productId: "$$this.productId",
quantity: {
$cond: [
{ $eq: ["$$this.productId", req.body.productId] },
{ $add: ["$$this.quantity", -1] },
"$$this.quantity"
]
}
}
}
},
cond: {
$and: [
{ $eq: ["$$this.productId", req.body.productId] },
{ $ne: ["$$this.quantity", 0] }
]
}
}
}
}
}
])
Playground

Find MongoDB document with the latest date according to different fields

We have data stored in MongoDB by country code. Our document looks like the following,
[
{
title: '1',
US: {
data: { lastReportDate: '2021-09-09' } // will be fetched
},
GB: {
data: { lastReportDate: '2021-09-04' }
}
},
{
title: '2',
US: {
data: { lastReportDate: '2021-09-07' } // will NOT be fetched
}
},
{
title: '3',
US: {
data: null // will NOT be fetched
}
},
{
title: '4',
US: {
data: null
}
GB: {
data: { lastReportDate: '2021-09-08' } // will be fetched
},
NZ: {
data: { lastReportDate: '2021-09-04' }
}
},
{
title: '5',
GB: {
data: null
},
NZ: {
data: { lastReportDate: '2021-09-06' } // will be fetched
}
}
]
I want to fetch the titles which have the latest dates according to the countries.
For EX: in the above DB, we have the latest date for US as '2021-09-09', so I want to fetch all the titles which match this date in lastReportDate. For GB, the latest date is '2021-09-08' and for NZ, its '2021-09-06'.
We have around 180 countries in one document and I want to hit the DB minimum times. So can we build a query that can us latest dates for different countries and then query the Database according to that.
You can try below aggregation:
db.collection.aggregate([
{
$project: {
doc: {
$objectToArray:"$$ROOT"
},
title: "$title"
}
},
{
$unwind: "$doc"
},
{
$match: {
"doc.k": { $nin: [ "_id", "title" ] }
}
},
{
$group: {
_id: "$doc.k",
maxDate: { $max: "$doc.v.data.lastReportDate" },
titles: { $push: { date: "$doc.v.data.lastReportDate", title: "$title" } }
}
},
{
$project: {
_id: 0,
country: "$_id",
maxTitles: { $filter: { input: "$titles", cond: { $eq: [ "$$this.date", "$maxDate" ] } } }
}
}
])
The challenge here is that your countries are represented as keys of your document so you need to start with $obectToArray operator which in conjunction with $unwind will give you a list of countries with corresponding dates and titles.
Once you have them you can use $group to get $max date and then use $filter to get titles related to max date.
Mongo Playground

How to skip a nested level in mongodb query

I have the following document structure
{
name: 'bob',
data: [
{
sport: 'football',
data: [
{
event: 'a',
date: '1628163910'
},
{
event: 'b',
date: '1628146503'
},
// ...
]
}
]
}
If the user doesn't filter the sport, then I want my query to ignore the sport property and search all the sport arrays to filter by the date. Here's what I thought it would be, but this doesn't work?
Oddsmatcher.find(
{ `data.$[].date`: { $lte: DATE_MAX_FILTER } }
)
You cant skip fields, instead you should rethink your data modelling. 2 collections one for sports and one for events will do. You can then find all events irrespective of sports or with sport filter.
UPDATE:
With your updated model, you can query as below
Oddsmatcher.find(
{ `data.data.date`: { $lte: DATE_MAX_FILTER } }
)
As you can't skip a level, construct $or condition programmatically with a clause for each sport. This works if the sports is fixed or you have a master list of sports defined somewhere in system.
{
$or: [{
'data.football.date': {
$lte: DATE_MAX_FILTER
}
}, {
'data.horse_racing.date': {
$lte: DATE_MAX_FILTER
}
}
]
}
Or redesign you model such that sport is a field inside each event.
{
name: 'bob',
data: [{
sport: 'football',
event: 'a',
date: '1628163910'
}, {
sport: 'football',
event: 'b',
date: '1628146503'
},
{
sport: 'horse_racing'
// horse_racing events
}, {
sport: 'tennis'
// tennis events
},
// ...
]
}

How to pull array of documents from array of documents?

I have a document like this
{
users: [
{
name: 'John',
id: 1
},
{
name: 'Mark',
id: 2
},
{
name: 'Mike',
id: 3
},
{
name: 'Anna',
id: 4
}
]
}
and I want to remove users from the array with ids 2 and 4. To do that I execute the following code:
const documents = [
{
id: 2
},
{
id: 4
},
]
Model.updateOne({ document_id: 1 }, { $pull: { users: { $in: documents } } });
But it doesn't remove any user.
Could you say me what I'm doing wrong and how to achieve the needed result?
This works if you can redefine the structure of your documents array:
const documents = [2, 4]
Model.updateOne({ document_id: 1 }, { $pull: { users: { id: { $in: documents } } } })