Mongoose Saving All Updates Apart From One - javascript

I have a Mongo database which uses the following Schema:
const userSchema = {
first_login: {
type: Date,
default: Date.now
},
last_login: {
type: Date,
default: Date.now
},
provider_use_count: {
logins: {
type: Object,
required: true
}
},
total_login_count: {
type: Number,
required: true,
default: 0
},
login_hours: [
{
type: Number,
required: true
}
],
playlists: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'Playlist'
}
],
genres: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'Genre'
}
],
artists: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'Artist'
}
],
auth_providers: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'Provider'
}
],
platform_ids: [
{
type: Object,
required: true,
}
]
}
I am updating it with the following function, which is in a class:
async updateUser(user, playlists, currentProvider) {
return new Promise((resolve, reject) => {
// Add one to the use count for the current provider
user.provider_use_count.logins[currentProvider]++;
user.total_login_count++;
user.last_login = new Date();
user.login_hours.push((new Date()).getHours() + 1);
return user.save((err, doc) => {
if(err){
return reject(err);
} else {
console.log(doc);
return resolve(doc);
}
});
});
}
However, when I use the .save() function it is not storing the updated value for provider_use_count.logins. I have tried everything I can think of: using ++, copying the object and then incrementing it, getting a variable of the current count and then saving it, removing the required field from the Schema, using + 1, using += 1, using an array which houses an object that has a field for count and provider.
I am out of ideas as the save function clearly works -- It saves all other updates. I feel like this could be due to the Schema and it is something which I am either just overlooking or haven't experienced before.
EDIT:
The exact field which is not being saved is:
user.provider_use_count.logins[currentProvider]++;
Also, I am not relying on the console.log(doc) block, I have checked in Atlas and it seems that field just remains at 0 while all others update.
As I have not implemented any other auth providers so the field in question should be identical to total_login_count
An example of the user object which is passed to the function is as follows:
{
total_login_count: 7,
login_hours: [
20, 20, 20, 20,
21, 21, 20, 20
],
playlists: [],
genres: [],
artists: [],
auth_providers: [],
platform_ids: [ { provider: 'spotify', id: 'makingstuffs' } ],
_id: 5dadfe7c9a25cc0c6a4d387c,
first_login: 2019-10-21T18:52:44.814Z,
last_login: 2019-10-21T19:23:30.580Z,
provider_use_count: { logins: { spotify: 0 } },
__v: 7
}

The issue was with the fact that mongoose has issues with saving mixed data types as they are considered schema-less. This means that you have to make a call to person.markModified('name_of_the_update_field') prior to calling the .save() function.
My updateUser() function now looks as follows:
async updateUser(user, playlists, currentProvider) {
return new Promise((resolve, reject) => {
// Add one to the use count for the current provider
user.provider_use_count.logins[currentProvider]++;
user.markModified('provider_use_count');
user.total_login_count++;
user.last_login = new Date();
user.login_hours.push((new Date()).getHours());
return user.save((err, doc) => {
if(err)
return reject(err);
return resolve(doc);
});
});
}

Related

How to find document's array item by its "_id" field in MongoDB (Mongoose)?

I have the following schema (NestJS + Mongoose):
#Schema({ timestamps: true })
export class Listing {
#Prop({
type: [{
bidderId: { type: Types.ObjectId, required: true, select: false, ref: User.name, index: true },
amount: { type: Number, required: true },
date: { type: Date, required: true }
}],
select: false
})
bids!: Bid[]
}
so basically every listing document has an array of bids.
now I notice that automatically mongoDB (or mongoose) creates _id field for every bid item I put into the bids array.
My question is, If I have a bid's _id, how can I query it's item from the listing's bids array? something like:
// Adding a new bid to the listing, and retrieving the updated listing
const listingWithAddedBid = await this.listingModel.findByIdAndUpdate(listingId, {
$push: {
bids: {
$each: [{
amount: bidInfo.amount,
bidderId: new Types.ObjectId(user.id),
date: new Date()
}],
$sort: { amount: -1 }
}
}
}, { new: true })
// Getting the new bid's _id from the array (it will be at index 0 because we sort buy amount)
const newBidId = listingWithAddedBid.bids[0]._id
// here how can I query the entire bid item from the array with 'newBidId'? this won't work
this.listingModel.findById(newBidId) // returns null
https://docs.mongodb.com/manual/tutorial/query-array-of-documents/
https://docs.mongodb.com/manual/indexes/#default-_id-index
this.listingModel.findOne({ "bids._id": newBidId, {}, {}, (error, doc) =>
{
if (error) {
....
}
if (doc) {
....
} else {
...//'Not Found';
}
});

Mongoose How to get a nested object within a document via findOne

I need to get a nested object within a certain document (searched by user ID) that also has an object inside of it (there's no guarantee that this object will be the same object).
I have the User model to be:
const mongoose = require('mongoose');
const { bool } = require('#hapi/joi');
const monitoringSchema = new mongoose.Schema({
type: Object,
default: {}
})
const hubSchema = new mongoose.Schema({
hubID: {
type: String,
default: ""
},
isSetup: {
type: Boolean,
default: false
},
monitoring: {
type: monitoringSchema
}
}, {strict:false})
const finalUserSchema = new mongoose.Schema({
username: {
type: String,
required: true,
max: 255
},
email: {
type: String,
required: true,
max: 255,
},
password: {
type: String,
required: true,
min: 10,
max: 1024,
},
date: {
type: Date,
default: Date.now
},
isVerified: {
type: Boolean,
default: false
},
hub: {
type: hubSchema
}
}, {strict:false});
module.exports = mongoose.model('User', finalUserSchema);
OR It has the layout:
_id: "id"
isVerified: true
username: "nathan"
email: "email#email.com"
hub:
hubID: "id"
monitoring: // WHOLE OBJECT I NEED TO RETREIVE
exampleObject:
exampleValue: exampleKey
I have an array of user IDs I need to update and I tried the query:
for(i in usersToUpdate){
User.findOne({_id: usersToUpdate[i], "hub.monitoring": {}}, {}, callbackResponse);
function callbackResponse(err, data){
if(err) return console.log(err)
console.log(data)
}
}
But it returns null as the data so obviously the query is wrong. I know the error is:
{_id: usersToUpdate[i], "hub.monitoring": {}}
more specifically:
"hub.monitoring": {}
I'm using {} to reference an object within monitoring, what's the correct reference to reference an unknown object and get it's values back, like a wildcard? I've tried:
{_id: usersToUpdate[i], "hub.monitoring": Object}
and it still doesn't work. I've seen this answer, however they reference a value that they already know, like a name?
To retrieve only the monitoring object, aggregation pipeline can be used.
Using $match to filter and $project to output/ supress fields.
User.aggregate([
{
$match: {
_id: mongoose.Types.ObjectId(usersToUpdate[i]),
},
},
{
$project: {
monitoring: "$hub.monitoring",
_id: 0,
},
},
]).exec(callbackResponse);
Playground example
You can try using the 2 object form of findOne where the first object is the query and the second object is the projection of what you want to return.
User.findOne({_id: usersToUpdate[i]}, {"hub.monitoring": {$exists: true}}, callbackResponse);
function callbackResponse(err, data){
if(err) return console.log(err)
console.log(data)
}
This way, the object will be returned if the monitoring object exist.

Dynamically push, pull, and set on mongoose schema update

I am trying to setup my patch api so that I can create a dynamic query to push, pull, and set data in my mongoose schema. I have plenty of values that I would change using set, but I also have an array of objects which would require me to call push when I need to insert and pull when I need to remove an item. I'm trying to find the best way to combine this into a dynamic structure.
Schema:
const StepSchema = new Schema({
position: {
type: Number,
required: true
},
name: {
type: String,
required: true
},
due_date: {
type: Date
},
status: [{
label: {
type: String,
enum: ['Inactive', 'In Progress', 'Flagged', 'Complete'],
default: 'Inactive'
},
user: {
type: Schema.Types.ObjectId,
ref: 'users',
},
date: {
type: Date
}
}],
comments: [{
user: {
type: Schema.Types.ObjectId,
ref: 'users',
required: true
},
body: {
type: String,
required: true
},
date: {
type: Date,
required: true
},
}],
});
Api:
router.patch('/',
async (req, res) => {
let setQuery = req.body;
let pushQuery = {};
let pullQuery = {};
//remove id from set query
delete setQuery.id;
//if there is a comment
if(req.body.comment){
pushQuery.comments = req.body.comment
}
//if I need to remove a comment
if(req.body.remove_comment){
pullQuery.comments = {_id: req.body.remove_comment.id}
}
//Push new status into array
if(req.body.status) {
pushQuery.status = {
label: req.body.status,
user: req.user._id,
date: new Date()
};
delete setQuery.status;
}
//update step
await Step.findByIdAndUpdate(req.body.id, {$set: setQuery, $push: pushQuery, $pull: pushQuery})
.then(step => {
if(!step){
errors.noflow = "There was a problem updating the step";
return res.status(400).json(errors);
}
res.json(step)
})
.catch(err => {
console.log(err);
res.status(404).json(err);
});
});
I've been getting the following error when trying to push a new status into my document:
operationTime: Timestamp { bsontype: 'Timestamp', low: 1, high_:
1560978288 }, ok: 0, errmsg: 'Updating the path \'status\' would
create a conflict at \'status\'', code: 40, codeName:
'ConflictingUpdateOperators', '$clusterTime': { clusterTime:
Timestamp { bsontype: 'Timestamp', low: 1, high_: 1560978288 },
signature: { hash: [Object], keyId: [Object] } },
Oh, you're doing that $set and $push on a status. Your pushQuery is trying to have status be an array on the document, and your setQuery wants to set it to whatever it was on the actual body (I'm guessing the same object.
A quickfix would be to remove it from the set object:
delete setQuery.status
A reasonable and stable way to do this would be to actually only take the things from req.body which you really want for each of the stages. Example:
const { position, name, dueDate, status, comment, remove_comment } = req.body;
const setQuery = { position, name, dueDate };
const pushQuery = { status, comments: comment };
// ...
That way your queries are not conflicting in any way.

Pushing a JSON in a sub-array of a Mongo document with Mongoose for Node.js

I have a NodeJS application where I use the mongoose library to communicate with my mongo database.
The application is about a game, where multiple rounds are played. And after each round, the results of the round are submitted! I want the values (a json) to be push to players.rounds. I have an _id and a players.id to determine where to push.
This is what I thought would be the right way (and I'm still a newbie in mongoose). It prints me no error, but the db document is not affected. Still zero items in players.rounds.
This is what I thought would be the right way (and I'm still a newbie in mongoose).
My mongoose schema:
const gameSchema = new mongoose.Schema(
{
categories: [
{ type: String }
],
countdown: Number,
players: [{
_id: false,
id: String,
rounds: [
{ type: Map, of: String }
],
score: { type: Number, default: 0 },
ready: { type: Boolean, default: false }
}]
}
);
The place where I'm executing:
Game.findOneAndUpdate(
{ _id: gameId, 'players.id': client.id },
{ $push: { 'players.$.rounds': values } }, function(err) {
if (err) {
console.log('ERROR when submitting round');
console.log(err);
}
});
It prints me no error, but the db document is not affected. Still zero items in players.rounds.
you need to change your schema Object. we need to specify {strict: false} for changing the inserted documents in mongoose.
const gameSchema = new mongoose.Schema(
{
categories: [
{ type: String }
],
countdown: Number,
players: [{
_id: false,
id: String,
rounds: [
{ type: Map, of: String }
],
score: { type: Number, default: 0 },
ready: { type: Boolean, default: false }
}]
}, {strict:false} );

Mongoose update conditions matching wrong object

Model Schema
const PollSchema = new Schema({
title: { type: String, trim: true, required: true },
choices: [
{
title: { type: String, trim: true, required: true },
votes: { type: Number, default: 0, min: 0 },
}
],
date: { type: Date, default: Date.now },
url: { type: String, required: true, unique: true },
})
Update call
async vote (pollId, choiceId, unvoteId = '') {
try {
await pollModel.update(
{ '_id': pollId, 'choices._id': choiceId },
{ $inc: { 'choices.$.votes': 1 } },
)
if (unvoteId) {
await pollModel.update(
{
"$and": [
{ '_id': pollId },
{ 'choices._id': unvoteId },
{ 'choices.votes': { $gt: 0 } }
],
},
{ $inc: { 'choices.$.votes': -1 } },
)
}
return await pollModel.findById(pollId)
} catch (e) {
throw new ApiError(
500, 'Error: Poll:Vote', e
)
}
}
I have been trying a plethora of combinations trying to get this to work. Voting +1 works as intended, but when trying to -1, the query conditions are not properly matched. I have tried $and, $elemMatch, plain object with the 3 conditions (according to the docs this is sufficient and implicitly means and too.
Whenever I send through an unvoteId, no matter which _id I choose in the array, it will always match the FIRST element that has $gt 0 votes. It is as if choices._id is completely ignored, and as soon as it meets a choice that has > 0 votes, it returns that for the $ positional param.
Is this intended? I assumed $and would only match if all 3 conditions were satisfied.
What I am trying to do is update the votes atomically using $inc, while also ensuring that when someone votes, they cannot bring the value below 0. As the mongoose validators do not get run during updates, I am trying to validate this via the query itself.
Try something like this.
pollModel.update({
_id: pollId,
choices: {
$elemMatch: {
_id: unvoteId,
votes: {
$gt: 0
}
}
}
}, {
$inc: {
"choices.$.votes": -1
}
})

Categories