Mongoose field how to auto increment - node.js

I have a mongoose Schema like below:
var userSchema = new Shema({
userName: {type: String, default: "ABC"},
lastLoginTime: {type: Date, default: Date.now},
loginTimes: {type: Number, default: 1},
......
});
And I want to update lastLoginTime and let loginTimes plus one when user login every time.
It's easy to update lastLoginTime, just give it a new time string.
But how can I make loginTimes plus one every time.

Well you can always use the $inc operator with a form of "update"
Model.update(
{ _id: docId },
{
"$set": { "lastLoginTime": new Date() },
"$inc": { "loginTimes": 1 }
},
function(err,numaffected) {
}
)
That is a general MongoDB preferred way of doing things as there is minimal traffic sent for each actual update.
Also see the .findByIdAndUpdate() method for mongoose since this is likely restricted to one document.
An alternate is to use "pre save" hooks to be very mongoose about it, but it seems sort of cumbersome to me to retrieve and modify a document when you don't really need things like validation for this sort of update.

Related

Mongoose filter nested array in pre find query

I have the following issue. I have some comments that are soft-deletable. So they have a flag is_deleted and it is set to true when a record is deleted.
My comments aren't an independent model, but they are a nested array in another model (simplified model):
let CommentSchema = new Schema({
text: {
type: String,
required: true
},
modified_at: {
type: Date,
default: null
},
created_at: {
type: Date,
default: Date.now
},
is_deleted: {
type: Boolean,
default: false
},
});
let BookSchema = new Schema({
...
comments: [CommentSchema],
...
});
Now when I get all my Books with Books.find({}, (err, books) => {}) I wanted to filter out the deleted comments in:
BookSchema.pre('find', function() {
this.aggregate(
{},
{ $unwind: '$comments'},
{ $match: {'comments.is_deleted': false}})
});
But it does not work. Any idea how to write the query, so that it only return the non-deleted nested comments without creating an independent Comment collection?
EDIT: I didn't mention it's an endpoint where I access only one book object. The resource url is like this: /books/{bookId}/comments. But also nice to have if it would work when getting all book objects.
You can use the positional $ operator and filter directly from find. As greatly explaned by #Blakes Seven Here
BookSchema.find({
'comments.is_deleted': false,
}, {
'comments.$': 1,
});
EDIT:
As you said the $ operator only return one element, here is what the document says :
The positional $ operator limits the contents of an from the
query results to contain only the first element matching the query
document
There is two solution to your problem:
Make an aggregate, which is usually slow to get executed by database
Get the books and filter the comments using loops
BookSchema.find({
'comments.is_deleted': false,
}).map(x => Object.assign(x, {
comments: x.comments.filter(y => !y.is_deleted),
}));
The find get all books that have a non-deleted comment.
The map loop on each book.
We then remove the comments marked as deleted

How to insert data into subdocument using $push in query, instead of retrieving doc and saving it back

Edit: this was actually working
As the Mongoose - Subdocs: "Adding subdocs" documentation says, we can add a subdoc using the push method (i.e. parent.children.push({ name: 'Liesl' });)
But I want to go further, and would like to use the $push operator to insert subdocuments.
I have two Schemas: the ThingSchema:
var ThingSchema = mongoose.Schema({
name: {
type: String,
required: true
},
description: {
type: String
}
});
and the BoxSchema, the main document that has an array of subdocuments (things) of ThingSchema:
var BoxSchema = new mongoose.Schema({
name: {
type: String,
required: true
},
description: {
type: String
},
things: {
type: [ThingSchema]
}
});
var BoxModel = mongoose.model('Box', BoxSchema);
I need every subdocument in things to have unique names - that is, that it would be impossible to insert a new document into this array that has a name value that already exists in the subdocs.
I'm trying to do something like:
var thingObj = ... // the 'thing' object to be inserted
BoxModel.update({
_id: some_box_id, // a valid 'box' ObjectId
"things.name": { "$ne": thingObj.name }
},
{
$push: { things: thingObj}
},
function(err) {
if (err) // handle err
...
});
but not getting any desired results.
What would be the correct way to add a ThingSchema subdocument into BoxSchema's thing array using the $push operator to do so in the query (must not add the subdoc if there's another subdoc named the same), instead of the Mongoose Docs way?
Edit: this is actually the issue
I made a mistake, the code above works as expected but now the problem I have is that when thingObj does not match the ThingSchema, an empty object is inserted into the things array:
// now thingObj is trash
var thingObj = { some: "trash", more: "trash" };
When executing the query given the above trash object, the following empty object is inserted into the subdocs array:
{ _id: ObjectId("an_obj_id") }
What I want this case, when the thingObj doesn't match the ThingSchema, is nothing to be added.
$addToSet adds something unique to the array (as in it checks for duplicates). But it only works for primitives.
What you should do is put things into their own collection and make a unique index on name. Then, make this change
things: {
type: [{type: ObjectId, ref: 'thingscollection'}]
}
this way you can do
BoxModel.update({
_id: some_box_id, // a valid 'box' ObjectId
"things": { "$ne": thingObj._id }
},
{
$addToSet: { things: thingObj._id}
},
function(err) {
if (err) // handle err
...
});
And when you fetch use .populate on things to get the full documents in there.
It's not exactly how you want it, but that's a design that might achieve what you're aiming for.

mongoose subdocument sorting

I have an article schema that has a subdocument comments which contains all the comments i got for this particular article.
What i want to do is select an article by id, populate its author field and also the author field in comments. Then sort the comments subdocument by date.
the article schema:
var articleSchema = new Schema({
title: { type: String, default: '', trim: true },
body: { type: String, default: '', trim: true },
author: { type: Schema.ObjectId, ref: 'User' },
comments: [{
body: { type: String, default: '' },
author: { type: Schema.ObjectId, ref: 'User' },
created_at: { type : Date, default : Date.now, get: getCreatedAtDate }
}],
tags: { type: [], get: getTags, set: setTags },
image: {
cdnUri: String,
files: []
},
created_at: { type : Date, default : Date.now, get: getCreatedAtDate }
});
static method on article schema: (i would love to sort the comments here, can i do that?)
load: function (id, cb) {
this.findOne({ _id: id })
.populate('author', 'email profile')
.populate('comments.author')
.exec(cb);
},
I have to sort it elsewhere:
exports.load = function (req, res, next, id) {
var User = require('../models/User');
Article.load(id, function (err, article) {
var sorted = article.toObject({ getters: true });
sorted.comments = _.sortBy(sorted.comments, 'created_at').reverse();
req.article = sorted;
next();
});
};
I call toObject to convert the document to javascript object, i can keep my getters / virtuals, but what about methods??
Anyways, i do the sorting logic on the plain object and done.
I am quite sure there is a lot better way of doing this, please let me know.
I could have written this out as a few things, but on consideration "getting the mongoose objects back" seems to be the main consideration.
So there are various things you "could" do. But since you are "populating references" into an Object and then wanting to alter the order of objects in an array there really is only one way to fix this once and for all.
Fix the data in order as you create it
If you want your "comments" array sorted by the date they are "created_at" this even breaks down into multiple possibilities:
It "should" have been added to in "insertion" order, so the "latest" is last as you note, but you can also "modify" this in recent ( past couple of years now ) versions of MongoDB with $position as a modifier to $push :
Article.update(
{ "_id": articleId },
{
"$push": { "comments": { "$each": [newComment], "$position": 0 } }
},
function(err,result) {
// other work in here
}
);
This "prepends" the array element to the existing array at the "first" (0) index so it is always at the front.
Failing using "positional" updates for logical reasons or just where you "want to be sure", then there has been around for an even "longer" time the $sort modifier to $push :
Article.update(
{ "_id": articleId },
{
"$push": {
"comments": {
"$each": [newComment],
"$sort": { "$created_at": -1 }
}
}
},
function(err,result) {
// other work in here
}
);
And that will "sort" on the property of the array elements documents that contains the specified value on each modification. You can even do:
Article.update(
{ },
{
"$push": {
"comments": {
"$each": [],
"$sort": { "$created_at": -1 }
}
}
},
{ "multi": true },
function(err,result) {
// other work in here
}
);
And that will sort every "comments" array in your entire collection by the specified field in one hit.
Other solutions are possible using either .aggregate() to sort the array and/or "re-casting" to mongoose objects after you have done that operation or after doing your own .sort() on the plain object.
Both of these really involve creating a separate model object and "schema" with the embedded items including the "referenced" information. So you could work upon those lines, but it seems to be unnecessary overhead when you could just sort the data to you "most needed" means in the first place.
The alternate is to make sure that fields like "virtuals" always "serialize" into an object format with .toObject() on call and just live with the fact that all the methods are gone now and work with the properties as presented.
The last is a "sane" approach, but if what you typically use is "created_at" order, then it makes much more sense to "store" your data that way with every operation so when you "retrieve" it, it stays in the order that you are going to use.
You could also use JavaScript's native Array sort method after you've retrieved and populated the results:
// Convert the mongoose doc into a 'vanilla' Array:
const articles = yourArticleDocs.toObject();
articles.comments.sort((a, b) => {
const aDate = new Date(a.updated_at);
const bDate = new Date(b.updated_at);
if (aDate < bDate) return -1;
if (aDate > bDate) return 1;
return 0;
});
As of the current release of MongoDB you must sort the array after database retrieval. But this is easy to do in one line using _.sortBy() from Lodash.
https://lodash.com/docs/4.17.15#sortBy
comments = _.sortBy(sorted.comments, 'created_at').reverse();

How to find documents by some conditions for its linked documents

I am a newby in MongoDB and I have a problem when querying a linked documents of the some documents collection.
Here is my database scheme:
var tagScheme = Schema({
name: { type: String, required: true }
});
tagScheme.index({ name: 1 }, { unique: true });
var linkScheme = Schema({
name: { type: String },
tags: [{ type: Schema.Types.ObjectId, ref: 'Tag' }]
});
linkScheme.index({ name: 1 }, { unique: true });
I need to get a count of the appropriate links for the specified tag. I try to execute the following query:
dbschemes.Link.find({ 'tags.name': specifiedTagName }, function (err, links) {
return res.send(500, err);
alert(links.length);
});
This query works not properly: it always returns an empty links list. Could someone exlain me what the problem is?
As JohnnyHK commented, the type of query you want to do is a relational type query and document database such as mongodb simply do not support them. Fix your schema to put that tag data directly in the link schema (nesting or "denormalizing" from a relational standpoint, which is OK in this case), then you can query it:
var LinkSchema = new Schema({
name: String,
tags: [String]
});
With that Schema, your query will work as you expect.
To address the comments below. This is a document database. It's not relational. There are trade-offs. Your data is de-normalized and it gives you some scalability and performance and you trade off flexibility of queries and data consistency to get them. If you wanted to rename a tag, a relatively rare occurrence, you'd have to do a whopping 2 database commands (a $push of the new name then a $pull of the old name) as opposed to relation where a single update command would do it.

How to sort array of embedded documents via Mongoose query?

I'm building a node.js application with Mongoose and have a problem related to sorting embedded documents. Here's the schema I use:
var locationSchema = new Schema({
lat: { type: String, required: true },
lon: { type: String, required: true },
time: { type: Date, required: true },
acc: { type: String }
})
var locationsSchema = new Schema({
userId: { type: ObjectId },
source: { type: ObjectId, required: true },
locations: [ locationSchema ]
});
I'd like to output the locations embedded in the userLocations documented sorted by their time attribute. I currently do the sorting in JavaScript after I retrieved the data from MongoDb like so:
function locationsDescendingTimeOrder(loc1, loc2) {
return loc2.time.getTime() - loc1.time.getTime()
}
LocationsModel.findOne({ userId: theUserId }, function(err, userLocations) {
userLocations.locations.sort(locationsDescendingTimeOrder).forEach(function(location) {
console.log('location: ' + location.time);
}
});
I did read about the sorting API provided by Mongoose but I couldn't figure out if it can be used for sorting arrays of embedded documents and if yes, if it is a sensible approach and how to apply it to this problem. Can anyone help me out here, please?
Thanks in advance and cheers,
Georg
You're doing it the right way, Georg. Your other options are either to sort locations by time upon embedding in the first place, or going the more traditional non-embedded route (or minimally embedded route so that you may be embedding an array of ids or something but you're actually querying the locations separately).
This also can be done using mongoose sort API as well.
LocationsModel.findOne({ userId: theUserId })
// .sort({ "locations.time": "desc" }) // option 1
.sort("-locations.time") // option 2
.exec((err, result) => {
// compute fetched data
})
Sort by field in nested array with Mongoose.js
More methods are mentioned in this answer as well
Sorting Options in mogoose
Mongoose Sort API

Resources