Conditional update, depending on field matched - node.js

Say I have a collection of documents, each one managing a discussion between a teacher and a student:
{
_id,
teacherId,
studentId,
teacherLastMessage,
studentLastMessage
}
I will get queries with 3 parameters: an _id, a userId and a message.
I'm looking for a way to update the teacherLastMessage field or studentLastMessage field depending on which one the user is.
At the moment, I have this:
return Promise.all([
// if user is teacher, set teacherLastMessage
db.collection('discussions').findOneAndUpdate({
teacherId: userId,
_id
}, {
$set: {
teacherLastMessage: message
}
}, {
returnOriginal: false
}),
// if user is student, set studentLastMessage
db.collection('discussions').findOneAndUpdate({
studentId: userId,
_id
}, {
$set: {
studentLastMessage: message
}
}, {
returnOriginal: false
})
]).then((results) => {
results = results.filter((result) => result.value);
if (!results.length) {
throw new Error('No matching document');
}
return results[0].value;
});
Is there a way to tell mongo to make a conditional update, based on the field matched? Something like this:
db.collection('discussions').findOneAndUpdate({
$or: [{
teacherId: userId
}, {
studentId: userId
}],
_id
}, {
$set: {
// if field matched was studentId, set studentLastMessage
// if field matched was teacherId, set teacherLastMessage
}
});
Surely it must be possible with mongo 3.2?

What you want would require referencing other fields inside of $set. This is currently impossible. Refer to this ticket as an example.
First of all, your current approach with two update queries looks just fine to me. You can continue using that, just make sure that you have the right indexes in place. Namely, to get the best performance for these updates, you should have two compound indexes:
{ _id: 1, teacherId: 1 }
{ _id: 1, studentId: 1 }.
To look at this from another perspective, you should probably restructure your data. For example:
{
_id: '...',
users: [
{
userId: '...',
userType: 'student',
lastMessage: 'lorem ipsum'
},
{
userId: '...',
userType: 'teacher',
lastMessage: 'dolor sit amet'
}
]
}
This would allow you to perform your update with a single query.

Your data structure is a bit weird, unless you have a specific business case which requires the data the be molded that way i would suggest creating a usertype unless a user can both be a teacher and a student then keep your structure.
The $set{} param can take a object, my suggestion is to do your business logic prior. You should already know prior to your update if the update is going to be for a teacher or student - some sort of variable should be set / authentication level to distinguish teachers from students. Perhaps on a successful login in the callback you could set a cookie/local storage. Regardless - if you have the current type of user, then you could build your object earlier, so make an object literal with the properties you need based on the user type.
So
if(student)
{
var updateObj = { studentLastMsg: msg }
}
else
{
var updateObj = { teacherLastMsg: msg }
}
Then pass in your update for the $set{updateObj} I'll make this a snippet - on mobile

Related

In Mongo db I want to add an entry to a document. If the id already exists it should overwrite it, if not it should add it

I use Mongo DB and node.js. Thanks for any help :).
I want to do the following:
Search for a user in the db using the googleId.
Then if the user does not have a description field yet I want to create it. Within the description field, I want to have several objects that all have the coinId as an index.
If the coinId already exists it should overwrite the content of that particular object with the variables I pass. If it does not already exist it should add the new object to the description field.
The Mongo db document should look like this in the end. Note that 1027 or 123 are the values of the coinIds.
googleId: "PyovWaX8HERRACmeg4IzYCaMK833"
description:
1027:
_id: "ckpi7q8c60002qe9h0e4wgh3r"
coinId: 1027
description: "test1"
date: 2021-06-04T10:56:52.662+00:00
123:
_id: "woienfeiowfnaoewinfefneo"
coinId: 123
description: "test2"
date: 2021-06-04T10:56:52.662+00:00
I already tried the following:
const { result } = await db.collection('users').updateOne(
{ googleId },
{
$set: {
description: { [coinId]: { _id, coinId, description, date } },
},
},
{
returnOriginal: false,
}
);
The problem here is that the entry is always overwritten which I only want to happen if the coinId is the same.
I also tried to do it with $addToSet but then it never overwrites the object if the coinId's match.
How about findAndModify?
findAndModify
In your case, something like this:
db.collection('users').findAndModify({
query: { _id: googleId },
update: {
$setOnInsert: {
description: { [coinId]: { _id, coinId, description, date } },
}
},
new: true, // return new doc if one is upserted
upsert: true // insert the document if it does not exist
})
db.collection.findAndModify({
query: { _id: "some potentially existing id" },
update: {
$setOnInsert: { foo: "bar" }
},
new: true, // return new doc if one is upserted
upsert: true // insert the document if it does not exist
})

Mongoose - query doc if element not in array

I am dealing with an issue while querying a notification schema
receiver: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Profile' }],
readBy: [{
readerId: { type: mongoose.Schema.Types.ObjectId,
ref: 'Profile', default: [] },
readAt: { type: Date }
}]
In order to query latest notifications, this is the query I have written:
GOAL is to check if the "profile.id" DOES NOT exist in the readBy array (which means its unread by that user)
const notifications = await Notification.find({
receiver: profile.id, // this works
readBy: { // but, adding this returns an empty array
$elemMatch: {
readerId: { $ne: profile.id }
}
}
})
Would really appreciate the help, stuck here for days.
I think is easier than we are trying to do.
If you want to know if the element is into the array, then, query looking for it. If the query is empty, it implies that not exists, otherwise yes.
Look this example.
Is a simple query, only check if one document has been received and readed by user profile.id.
db.collection.find([
{
"receiver": profile.id,
"readBy.readerId": profile.id
}
])
Please check if the output is as expected or I misunderstood your petition.
In mongoose you can use this:
var find = await model.find({"receiver":1,"readBy.readerId":1}).countDocuments()
It will return how many documents match the query.
Edit to get documents where readerId is not present in the array:
Here, you only need to add $ne operator
db.collection.find([
{
"receiver": profile.id,
"readBy.readerId": {
"$ne": profile.id
}
}
])
You can check this here

mongoose overwrite field on update

I have a problem trying to update a user with a new profile, mongoose overwrites the previous ones, so it is only allowing them to have 1 profile.
I have in a user document with its associated profiles, buyer or seller with some data of each profile such as id and reputation.
At the beginning the user has no profiles, when I add his buyer or seller profile, there is no drama, but when I try to add the other profile, he writes the one he was on.
At this moment I have this to add the profile (the seller is the same, just change buyer for seller):
const user = await User.findOne({_id: 'userId'});
const updatedUser = await user.updateOne({
profiles: {
buyer: {
_id: buyerId,
reputation: 50
}
}
});
What he does is write to me the one who is already there, and I understand that I am stupidly telling him that by passing that object, but I tried anyway.
I was checking and I saw that they put a $ set, so I tried it like this:
const user = await User.findOne({_id: 'userId'});
const updatedUser = await user.updateOne({
profiles: {
$set: {
buyer: {
_id: buyerId,
reputation: 50
}
}
}
});
that is not supposed to write me over, and only add the field if it does not exist and leave the other there, quiet (it is what I want).
But the only thing I achieved was that I was writing profiles with an empty object, without any profile.
What I am looking for is that the user is left with the profiles placed there, in profiles, like this:
profiles: {
buyer: {
_id: 'fuckinid1',
reputation: 50
},
seller: {
_id: 'fuckinid2',
reputation: 50
},
}
my schema is this:
const schema = new Schema({
_id: { type: String },
name : { type: String },
surname : { type: String },
email : { type: String, lowercase: true },
profiles : {
buyer: {
_id : String,
reputation: Number
},
seller: {
_id : String,
reputation: Number
}
}
});
How can i make this?
You should try like below:
let updateRecord = Test.findOneAndUpdate(
{ _id: test._id },
{ name: 'Test name' },
{ new: true, overwrite: true }
);
As for the more, you should check out the mongoose docs for findOneAndUpdate options. I also hardly recommend you to use findByIdAndUpdate because even if you need to check document on existence, it won't be overwrited without the correct options. And instead, it will create new, if it isn't exist. Also MongoDB by default creates an index for _id field, so your queries will be executed much faster with index, then w/o them.
Also, I noticed that you are using embedded document structure. Well, it's always for you to decide, but I don't sure that you really needed that _id field for sub-documents. Anyway, if you need more info about it, just tag me in the comment below, and I'll try to provide more information about it.

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();

Sorting by virtual field in mongoDB (mongoose)

Let's say I have some Schema which has a virtual field like this
var schema = new mongoose.Schema(
{
name: { type: String }
},
{
toObject: { virtuals: true },
toJSON: { virtuals: true }
});
schema.virtual("name_length").get(function(){
return this.name.length;
});
In a query is it possible to sort the results by the virtual field? Something like
schema.find().sort("name_length").limit(5).exec(function(docs){ ... });
When I try this, the results are simple not sorted...
You won't be able to sort by a virtual field because they are not stored to the database.
Virtual attributes are attributes that are convenient to have around
but that do not get persisted to mongodb.
http://mongoosejs.com/docs/2.7.x/docs/virtuals.html
Virtuals defined in the Schema are not injected into the generated MongoDB queries. The functions defined are simply run for each document at the appropriate moments, once they have already been retrieved from the database.
In order to reach what you're trying to achieve, you'll also need to define the virtual field within the MongoDB query. For example, in the $project stage of an aggregation.
There are, however, a few things to keep in mind when sorting by virtual fields:
projected documents are only available in memory, so it would come with a huge performance cost if we just add a field and have the entire documents of the search results in memory before sorting
because of the above, indexes will not be used at all when sorting
Here's a general example on how to sort by virtual fields while keeping a relatively good performance:
Imagine you have a collection of teams and each team contains an array of players directly stored into the document. Now, the requirement asks for us to sort those teams by the ranking of the favoredPlayer where the favoredPlayer is basically a virtual property containing the most relevant player of the team under certain criteria (in this example we only want to consider offense and defense players). Also, the aforementioned criteria depend on the users' choices and can, therefore, not be persisted into the document.
To top it off, our "team" document is pretty large, so in order to mitigate the performance hit of sorting in-memory, we project only the fields we need for sorting and then restore the original document after limiting the results.
The query:
[
// find all teams from germany
{ '$match': { country: 'de' } },
// project only the sort-relevant fields
// and add the virtual favoredPlayer field to each team
{ '$project': {
rank: 1,
'favoredPlayer': {
'$arrayElemAt': [
{
// keep only players that match our criteria
$filter: {
input: '$players',
as: 'p',
cond: { $in: ['$$p.position', ['offense', 'defense']] },
},
},
// take first of the filtered players since players are already sorted by relevance in our db
0,
],
},
}},
// sort teams by the ranking of the favoredPlayer
{ '$sort': { 'favoredPlayer.ranking': -1, rank: -1 } },
{ '$limit': 10 },
// $lookup, $unwind, and $replaceRoot are in order to restore the original database document
{ '$lookup': { from: 'teams', localField: '_id', foreignField: '_id', as: 'subdoc' } },
{ '$unwind': { path: '$subdoc' } },
{ '$replaceRoot': { newRoot: '$subdoc' } },
];
For the example you gave above, the code could look something like the following:
var schema = new mongoose.Schema(
{ name: { type: String } },
{
toObject: { virtuals: true },
toJSON: { virtuals: true },
});
schema.virtual('name_length').get(function () {
return this.name.length;
});
const MyModel = mongoose.model('Thing', schema);
MyModel
.aggregate()
.project({
'name_length': {
'$strLenCP': '$name',
},
})
.sort({ 'name_length': -1 })
.exec(function(err, docs) {
console.log(docs);
});

Resources