Trying to add/update a document and increment a count with mongoose - node.js

I am trying to make a pretty simple mongoDB document to track searches by date in a node.js project. Here's what I'm shooting for:
{
"date": 1626930000000,
"searches": [
{
"search": "search 1",
"count": 2
},
{
"search": "search 2",
"count": 5
}
]
}
I want to update/add new searches to each date and increment the count for existing searches. Then on new dates do the same thing. Here is my current attempt using mongoose:
const query = { date: date, searches: { search: search } };
guideSearchesModel.findOneAndUpdate(query, {$addToSet: { searches: search ,$inc: { count: 1 }}}, { upsert: true, new: true }, callback);
But this is what gets added to the DB:
{
"_id": {
"$oid": "60f9eb370f12712140dd29db"
},
"date": 1626930000000,
"searches": {
"_id": {
"$oid": "60f9eb38c4ff591f50137726"
},
"search": "search 1"
}
Its missing the "count", and if I run it again it inserts a new document, doesn't update the one I want to based on the date.
Any ideas would be greatly appreciated.
Thanks!

The query {searches: {search: "search 1"}} is an exact document search, meaning it will match a document that contains a field named searches that contains an object with exactly 1 field named search with the value "search 1". i.e. it will not match those sample documents, and therefore it will always upsert.
To match fields in subdocuments, use dotted notation, like
{"searches.search": "search 1"}
Also note that the query part of that update matches both a date and the search object, which means if a document exists in the collection with the desired date, but doesn't contain the search in its array, a new document with the date will be inserted.
It looks like you want to conditionally update the document by appending the search to the array if it doesn't exist, and incrementing its count if it does. This will require you to use the aggregation pipeline form of the update command, like Update MongoDB field using value of another field.

Related

find a product inside a specific category using mongodb and mongoose

I am new to mongodb. I'm trying to query for a particular product in a specific category (for example if I'm in the books category then I would like to query for a particular book by its name i.e., productName) but I'm unable to do so successfully. Could anybody help me with this. I'm attaching the schema below for reference.
const categorySchema = {
name: String,
description: String,
products: [
{
productName: String,
price: String,
thumbnail: String,
description: String
}
]
};
To search by an object into an array you need to use the dot notation and is very simple.
You need a query similar to this:
db.collection.find({
"_id": 0,
"products.productName": "productName"
})
Note that the find has two conditions.
The first one is to look for in the document you want using its _id (if you want all documents which has 'productName' into the array this condition is not neccesary, but for query a single document it does). This is for query into a specific category document.
The second condition is to get those documents which has the value productName for the key productName into the array products.
Check an example here
Also, if you want to return only the subdocument instead of the entire document, you need this query:
db.collection.find({
"_id": 0
},
{
"products": {
"$elemMatch": {
"productName": "productName"
}
}
})
Example here
Using $elementMatch only the subdocument is returned.
Also, using mongoose the query is the same. Something like this.
var find = await model.find({
"_id": 0,
"products.productName": "productName"
})

mongoose exclude field in array

I have something like:
Schema Subdocument
name: String
data: Mixed
Schema Stuff
documents: [Subdocument]
Now, in my API there are two endpoints, one for the Subdocument and another for Stuff. When I want to get a Subdocument I need to contain the data field, but when I want to get Stuff, I want to show the name of those subdocuments, but I don't want to show the data field because is quite large and it won't be used.
So, to keep things clear, data is not private. It's just that I don't want it to be shown when I get it from Stuff
I tried by doing:
Stuff.findById(id)
.populate("documents")
.populate("-documents.data")
but that doesn't work... I'm getting the Stuffwith the Subdocumentcontaining the name and data. I feel like i'm missing to tell mongoose when I call populate("-documents.data") that documents is an array and I want to exclude the data field for each element in this array.
edit: Sorry the Schema I provided was not for my case. In my case it was not embedded, but a reference, like so:
Schema Subdocument
name: String
data: Mixed
Schema Stuff
documents: [{
type: Schema.Types.ObjectId,
ref: 'Subdocument'
}]
Assuming subDocument is not embedded and using as "ref" as you say populate is working but data part is not included:
Stuff.findById(id).populate( { "path" : "documents", "select" : "-data" })
Your documents have an "embedded" schema, so populate is not used here, it is used only for "referenced" schemas where the other objects are in another collection.
Fortunately with "embedded" there is an easy way using projection:
Stuff.findById(id,{ "documents.name": 1 },function(err,results) {
})
With results like
{ "documents": [{ "name": "this" },{ "name": "that" }] }
Or with .aggregate() and the $map operator:
Stuff.aggregate(
[
{ "$match": { "_id": ObjectID(id) } },
{ "$project": {
"documents": {
"$map": {
"$input": "$documents",
"as": "el",
"in": "$$el.name"
}
}
}}
],function(err,results) {
}
)
That will just tranform into an array of "only" the name "values", which is different to the last form.
{ "documents": ["this", "that"] }
Note, if using .aggregate() you need to properly cast the ObjectId as the autocasting from mongoose schema types does not work in aggregation pipeline stages.

Delete record on timer + one condition

My program is an online game. I create a game object which contains players. I want to remove the game record after 3 hours if no other player (than the one who created the game) joined the game.
I can count players, and I know about mongodb TTL but how can I set a TTL that will only trigger is there is no multiple players?
The basics of using a TTL index is that the document will be deleted after the specified seconds has passed from the time recorded in the index field. What may not be clearly apparent is that though the documentation examples use a field called "createdAt", this does not mean this is your only restriction, as the date field can be updated.
So for instance if you create a document and give it an "updatedAt" field, that you populate when the game is created, then provide the index definition:
db.games.createIndex({ "updatedAt": 1 },{ "expireAfterSeconds": 10800 })
Then all you really have to do is "update" that value whenever a new player joins the game, or there is some other action that keeps the game "valid":
db.games.update(
{ "_id": gameId },
{
"$push": { "players": newPlayer },
"$currentDate": { "updatedAt": true }
}
)
Or change score:
db.games.update(
{ "_id": gameId },
{
"$inc": { "score": 20 },
"$currentDate": { "updatedAt": true }
}
)
Also using the $currentDate operator if you choose.
So the "expire date" for a TTL index is not "set", it just works on a process that checks the "current time" and removes any data where the indexed field of a TTL index is older than the current time "minus" the expiry seconds set on the index.
In short, create the index and just keep updating the field to keep the document current. As things change, players join or leave update the field.

How should I find a Mongoose subdocument given the parent document and a value in the subdocument?

I'm just trying to do a basic find similar to .findOne, but on a subdocument array in Mongoose. Should I simply loop through the array looking for the right subdocument?
I have a schema like so:
var RegionSchema = new Schema({
"metadata": {
"regionType": String,
"name": String,
"children": [{
"name": String,
"childType": String
}],
"parent": Schema.ObjectId
},
"data": [DataContainer]
});
I know that I want to find a DataContainer in data with dataYear equal to "2014". I'm new to MongoDB, so I don't know many advanced commands. What would be the most efficient way?
EDIT: dataYear is guaranteed to be unique in that array.
Use the aggregation framework for this. Your aggregation pipeline would consist of a $match operation stage which matches documents in the collection where the data array element object has a field dataYear with value equal to "2014". The next pipeline stage would be the $unwind operation on the data array object and a further $match pipeline to filter the arrays. Final stage would be to $project the deconstructed array. For example, suppose you have a model Region that uses the RegionSchema, your mongoose code would be:
Region.aggregate([
// similar query object you use in findOne() can also be applied to the $match
{
"$match": {
"data.dataYear": "2014",
"metadata.name": "example text"
}
},
{
"$unwind": "$data"
},
{
"$match": {
"data.dataYear": "2014"
}
},
{
"$project": {
"_id": 0,
"dataYear": "$data.dataYear",
"population": "$data.population"
}
}
], function(err, docs){
});
You could add an $elemMatch to your query.
Assuming Region is the model:
Region.findOne(...).select({data: {$elemMatch: {dataYear: 2014}});
This will return the Region document with the data array containing only the matching item.
UPDATE: If you already have a document with an array of subdocuments you can use something like lodash's find method to select the subdocument. NOTE: this will convert the subdocument to a POJO.
_.find(region.data.toObject(), {dataYear: 2014});

Mongoose/Mongodb previous and next in embedded document

I'm learning Mongodb/Mongoose/Express and have come across a fairly complex query (relative to my current level of understanding anyway) that I'm not sure how best to approach. I have a collection - to keep it simple let's call it entities - with an embedded actions array:
name: String
actions: [{
name: String
date: Date
}]
What I'd like to do is to return an array of documents with each containing the most recent action (or most recent to a specified date), and the next action (based on the same date).
Would this be possible with one find() query, or would I need to break this down into multiple queries and merge the results to generate one result array? I'm looking for the most efficient route possible.
Provided that your "actions" are inserted with the "most recent" being the last entry in the list, and usually this will be the case unless you are specifically updating items and changing dates, then all you really want to do is "project" the last item of the array. This is what the $slice projection operation is for:
Model.find({},{ "actions": { "$slice": -1 } },function(err,docs) {
// contains an array with the last item
});
If indeed you are "updating" array items and changing dates, but you want to query for the most recent on a regular basis, then you are probably best off keeping the array ordered. You can do this with a few modifiers such as:
Model.update(
{
"_id": ObjectId("541f7bbb699e6dd5a7caf2d6"),
},
{
"$push": { "actions": { "$each": [], "$sort": { "date": 1 } } }
},
function(err,numAffected) {
}
);
Which is actually more of a trick that you can do with the $sort modifier to simply sort the existing array elements without adding or removing. In versions prior to 2.6 you need the $slice "update" modifier in here as well, but this could be set to a value larger than the expected array elements if you did not actually want to restrict the possible size, but that is probably a good idea.
Unfortunately, if you were "updating" via a $set statement, then you cannot do this "sorting" in a single update statement, as MongoDB will not allow both types of operations on the array at once. But if you can live with that, then this is a way to keep the array ordered so the first query form works.
If it just seems too hard to keep an array ordered by date, then you can in fact retrieve the largest value my means of the .aggregate() method. This allows greater manipulation of the documents than is available to basic queries, at a little more cost:
Model.aggregate([
// Unwind the array to de-normalize as documents
{ "$unwind": "$actions" },
// Sort the contents per document _id and inner date
{ "$sort": { "_id": 1, "actions.date": 1 } },
// Group back with the "last" element only
{ "$group": {
"_id": "$_id",
"name": { "$last": "$name" },
"actions": { "$last": "$actions" }
}}
],
function(err,docs) {
})
And that will "pull apart" the array using the $unwind operator, then process with a next stage to $sort the contents by "date". In the $group pipeline stage the "_id" means to use the original document key to "collect" on, and the $last operator picks the field values from the "last" document ( de-normalized ) on that grouping boundary.
So there are various things that you can do, but of course the best way is to keep your array ordered and use the basic projection operators to simply get the last item in the list.

Resources