Unable to remove element from Array in Mongoose - javascript

I am trying to remove an element through its _id from an array model in mongoose. The same technique is working elsewhere in my code, but here it fails to remove the element. After hours of trying to change various parts of it, I am finally posting it here because maybe my lack of sleep is the main reason. Can someone find out what i am doing wrong?
ABC.findOne({
'user': new ObjectId(req.decoded._id),
'activity.ride': new ObjectId(id)
}, {
'activity.$': 1
}, function(err, doc) {
if (doc !== null) {
for (var j = 0; j < doc.activity.length; j++) {
var request = JSON.parse(JSON.stringify(doc.activity[j]));
doc.activity.remove(request._id);
doc.save();
}
}
});
This is the model:
var activityItem = mongoose.Schema({
timestampValue: Number,
xabc: String,
full: Boolean,
comp: Boolean
});
var ABC = mongoose.Schema({
activity: [activityItem],
user: {
type: mongoose.Schema.ObjectId,
ref: 'User'
},
username: String
});

The $pull operator removes from an existing array all instances of a value or values that match a specified condition. And to remove element from array through findOneAndUpdate
ABC.findOneAndUpdate({'user': new ObjectId(req.decoded._id)},
{$pull: {activity: {_id: request._id}}},
{new: true},
function(err, a) {
if (err)
console.log(err);
else
console.log(a);
});
BTW, I did not find ride in the activityItem schema, so I remove the ride from query condition.

Related

Mongoose find by parameters including sub-array

I am creating an app where a job is created and that job's id is added to another collection (client) so the job can be referenced from the client itself. I have been able to add the job's id to the client's collection so far, but I am having trouble figuring out how to remove the job's id from the client's collection if the job is deleted. This is because the id is stored as a sub-collection within the client. The code I am trying to get to work is below:
// delete
app.delete("/jobs/:id", function(req, res){
Client.find({jobs._id: req.params.id}, function (err, foundClient){ //This part doesn't work
if (err) {
console.log(err);
} else {
// Add id identifier to Client
foundClient.jobs.pull(req.params.id);
foundClient.save();
}
});
// Delete Job
Job.findByIdAndRemove(req.params.id, function(err, deletedJob){
if (err){
console.log(err)
} else {
// Redirect
res.redirect("/jobs");
}
});
});
I am trying to get the logic of this part to work:
Client.find({jobs._id: req.params.id},
Here is the Client Schema
// =======================Client Schema
var clientSchema = new mongoose.Schema({
organization_name: String,
first_name: String,
middle_name: String,
last_name: String,
email_address: String,
phone_number: String,
street: String,
city: String,
state: String,
zip: String,
description: String,
active: {type: Boolean, deafult: true},
date_added: {type: Date, default: Date.now},
transactions: [{type: mongoose.Schema.Types.ObjectID, ref: "Transaction"}],
jobs: [{type: mongoose.Schema.Types.ObjectID, ref: "Job"}]
});
module.exports = mongoose.model("Client", clientSchema);
Basically, what I am trying to tell it to do is find the Client where the client's jobs array contains an id equal to the id of the job being deleted. Of course, this syntax is incorrect, so it does not work. I have not been able to find documentation that explains how I would be able to do this. Is there a more straightforward way of doing this, the way I am writing it out here? I know that I can query the db this way if the job itself was not an array and only contained one singular variable. Is there a way to do this or do I need to write a completely separate looping function to get this to work? Thank you.
jobs is an array of ids, so to find some documents in Client collection that have req.params.id in the jobs array, the query should be something like this
Client.find({jobs: req.params.id})
this will return an array of documents, each document has an array of jobs Ids
If you are sure that the req.params.id exists only in one document, you can use findOne instead of find, and this will return only one document with an array of jobs Ids
this is regarding the find part
regarding the remove job Id from jobs array, we can use one of the following methods
1- as you suggested, we can find the clients documents that have this job Id first, then remove this id from all the jobs arrays in all matching documents
like this
Client.find({ jobs: req.params.id }, async function (err, foundClients) {
if (err) {
console.log(err);
} else {
// loop over the foundClients array then update the jobs array
for (let i = 0; i < foundClients.length; i++) {
// filter the jobs array in each client
foundClients[i].jobs = foundClients[i].jobs || []; // double check the jobs array
foundClients[i].jobs = foundClients[i].jobs.filter(jobId => jobId.toString() !== req.params.id.toString());
// return all the jobs Ids that not equal to req.params.id
// convert both jobId and req.params.id to string for full matching (type and value)
await foundClients[i].save(); // save the document
}
}
});
2- we can use $pull array update operator to update the jobs array directly
something like this
Client.updateMany(
{ jobs: req.params.id }, // filter part
{
$pull: { jobs: { $in: [req.params.id] } } // update part
},
function (err) {
if (err) {
console.log(err);
} else {
console.log('job id removed from client documents successfully');
}
}
);
hope it helps

Updating a model in anguler (mongoose/mongodb) specifically an array of objects that is an attribute of the model

I have the following schema:
const AuthorSchema = new mongoose.Schema({
fullName: { type: String, required: [true, "Authors must have a full name"], minlength: [6, "Author's full name must have at least 6 characters"] },
quotes: [{content: {type: String, minlength: [6, "Quotes should be six characters"]}, vote: {type: Number, default: 0}}],
}, { timestamps: true });
const Author = mongoose.model('Author', AuthorSchema);
module.exports = Author
And I am trying to update the "vote" in a particular "quote" using this in my controller:
upvote: function(req, res) {
console.log("UPVOTE_CONTROLLER", req.body);
var author = Author.findOne({_id: req.body.authid})
.then(author =>{ (console.log(author))
let quotes = author.quotes;
for (var key in quotes){
if (key._id == req.body.quoteid){
key.vote = key.vote + 1;
console.log("**UPDATED_QUOTE_BEFORE_SAVE", key)
return key.save();
}
}
})
.then(saveresult => res.json(saveresult))
.catch(err => {
console.log("****ERRROR HERE****");
console.log(err);
for (var key in err.errors) {
req.flash('registration', err.errors[key].message);
}
res.json({errors: err.errors});
});
},
I get into all the way into the controller method, no errors are thrown, but the for loop never seems to run,
I have tried many different ways of changing the vote ex. key.vote += 1; key.vote +1; etc I have tried saving the entire author not just the specific quote that is represented by key.
The "challenge" on this project is to do this using only a single schema, so I cannot use the normal Quote.findOne({_id: request.body.id}) as there is no Quote schema.
any help here would be appreciated.
I figured it out:
In the for loop it should have been for ( let key OF quotes){ instead of IN.
Other changes after the for loop ran:
key.note += save;
author.save(); instead of key.save();

Mongoose: how do I update/save a document?

I need to save a document to a mongo collection.
I want to save the 'insertedAt' and 'updatedAt' Date fields, so I suppose I can't do it in one step...
This is my last try:
my topic = new Topic(); // Topic is the model
topic.id = '123'; // my univocal id, !== _id
topic.author = 'Marco';
...
Topic.findOne({ id: topic.id }, function(err, doc) {
if (err) {
console.error('topic', topic.id, 'could not be searched:', err);
return false;
}
var now = new Date();
if (doc) { // old document
topic.updatedAt = now;
} else { // new document
topic.insertedAt = now;
}
topic.save(function(err) {
if (err) {
console.error('topic', topic.id, 'could not be saved:', err);
return false;
}
console.log('topic', topic.id, 'saved successfully');
return true;
});
});
But this way I end up duplicating records... :-(
Any suggestion?
Rather than doing whatever you are doing I prefer a very easy way to updating document with upsert. For this you need to keep in mind don't use the model to create an instance to insert. You need to create an object manually.
//don't put `updatedAt` field in this document.
var dataToSave = {
createdAt: new Date(),
id: 1,
author: "noor"
.......
}
Topic.update({ id: 123 }, { $set:{ updatedAt: new Date() }, $setOnInsert: dataToSave}, { upsert: true }, function(err, res){
//do your stuff here
})
This query will first check wether any document is there is the collection if yes then it will only update udpatedAt if not then it will insert the whole new document in the collection. Hope this answers your query.
set timestamps to false in schema definition and then add the fields on creation as you would like.
See sample schema definition below:
var mongoose = require('mongoose')
, Schema = mongoose.Schema;
var Topic = new Schema({
id:{
type:String,
required: true
},
author:{
type:String,
required: true
}
},{
timestamps: false
});

Mongoose auto increment

According to this mongodb article it is possible to auto increment a field and I would like the use the counters collection way.
The problem with that example is that I don't have thousands of people typing the data in the database using the mongo console. Instead I am trying to use mongoose.
So my schema looks something like this:
var entitySchema = mongoose.Schema({
testvalue:{type:String,default:function getNextSequence() {
console.log('what is this:',mongoose);//this is mongoose
var ret = db.counters.findAndModify({
query: { _id:'entityId' },
update: { $inc: { seq: 1 } },
new: true
}
);
return ret.seq;
}
}
});
I have created the counters collection in the same database and added a page with the _id of 'entityId'. From here I am not sure how to use mongoose to update that page and get the incrementing number.
There is no schema for counters and I would like it to stay that way because this is not really an entity used by the application. It should only be used in the schema(s) to auto increment fields.
Here is an example how you can implement auto-increment field in Mongoose:
var CounterSchema = Schema({
_id: {type: String, required: true},
seq: { type: Number, default: 0 }
});
var counter = mongoose.model('counter', CounterSchema);
var entitySchema = mongoose.Schema({
testvalue: {type: String}
});
entitySchema.pre('save', function(next) {
var doc = this;
counter.findByIdAndUpdate({_id: 'entityId'}, {$inc: { seq: 1} }, function(error, counter) {
if(error)
return next(error);
doc.testvalue = counter.seq;
next();
});
});
You can use mongoose-auto-increment package as follows:
var mongoose = require('mongoose');
var autoIncrement = require('mongoose-auto-increment');
/* connect to your database here */
/* define your CounterSchema here */
autoIncrement.initialize(mongoose.connection);
CounterSchema.plugin(autoIncrement.plugin, 'Counter');
var Counter = mongoose.model('Counter', CounterSchema);
You only need to initialize the autoIncrement once.
The most voted answer doesn't work. This is the fix:
var CounterSchema = new mongoose.Schema({
_id: {type: String, required: true},
seq: { type: Number, default: 0 }
});
var counter = mongoose.model('counter', CounterSchema);
var entitySchema = mongoose.Schema({
sort: {type: String}
});
entitySchema.pre('save', function(next) {
var doc = this;
counter.findByIdAndUpdateAsync({_id: 'entityId'}, {$inc: { seq: 1} }, {new: true, upsert: true}).then(function(count) {
console.log("...count: "+JSON.stringify(count));
doc.sort = count.seq;
next();
})
.catch(function(error) {
console.error("counter error-> : "+error);
throw error;
});
});
The options parameters gives you the result of the update and it creates a new document if it doesn't exist.
You can check here the official doc.
And if you need a sorted index check this doc
So combining multiple answers, this is what I ended up using:
counterModel.js
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
const counterSchema = new Schema(
{
_id: {type: String, required: true},
seq: { type: Number, default: 0 }
}
);
counterSchema.index({ _id: 1, seq: 1 }, { unique: true })
const counterModel = mongoose.model('counter', counterSchema);
const autoIncrementModelID = function (modelName, doc, next) {
counterModel.findByIdAndUpdate( // ** Method call begins **
modelName, // The ID to find for in counters model
{ $inc: { seq: 1 } }, // The update
{ new: true, upsert: true }, // The options
function(error, counter) { // The callback
if(error) return next(error);
doc.id = counter.seq;
next();
}
); // ** Method call ends **
}
module.exports = autoIncrementModelID;
myModel.js
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
const autoIncrementModelID = require('./counterModel');
const myModel = new Schema({
id: { type: Number, unique: true, min: 1 },
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date },
someOtherField: { type: String }
});
myModel.pre('save', function (next) {
if (!this.isNew) {
next();
return;
}
autoIncrementModelID('activities', this, next);
});
module.exports = mongoose.model('myModel', myModel);
Attention!
As hammerbot and dan-dascalescu pointed out this does not work if you remove documents.
If you insert 3 documents with id 1, 2 and 3 - you remove 2 and insert another a new one it'll get 3 as id which is already used!
In case you don't ever remove documents, here you go:
I know this has already a lot of answers, but I would share my solution which is IMO short and easy understandable:
// Use pre middleware
entitySchema.pre('save', function (next) {
// Only increment when the document is new
if (this.isNew) {
entityModel.count().then(res => {
this._id = res; // Increment count
next();
});
} else {
next();
}
});
Make sure that entitySchema._id has type:Number.
Mongoose version: 5.0.1.
This problem is sufficiently complicated and there are enough pitfalls that it's best to rely on a tested mongoose plugin.
Out of the plethora of "autoincrement" plugins at http://plugins.mongoosejs.io/, the best maintained and documented (and not a fork) is mongoose sequence.
I've combined all the (subjectively and objectively) good parts of the answers, and came up with this code:
const counterSchema = new mongoose.Schema({
_id: {
type: String,
required: true,
},
seq: {
type: Number,
default: 0,
},
});
// Add a static "increment" method to the Model
// It will recieve the collection name for which to increment and return the counter value
counterSchema.static('increment', async function(counterName) {
const count = await this.findByIdAndUpdate(
counterName,
{$inc: {seq: 1}},
// new: return the new value
// upsert: create document if it doesn't exist
{new: true, upsert: true}
);
return count.seq;
});
const CounterModel = mongoose.model('Counter', counterSchema);
entitySchema.pre('save', async function() {
// Don't increment if this is NOT a newly created document
if(!this.isNew) return;
const testvalue = await CounterModel.increment('entity');
this.testvalue = testvalue;
});
One of the benefits of this approach is that all the counter related logic is separate. You can store it in a separate file and use it for multiple models importing the CounterModel.
If you are going to increment the _id field, you should add its definition in your schema:
const entitySchema = new mongoose.Schema({
_id: {
type: Number,
alias: 'id',
required: true,
},
<...>
});
test.pre("save",function(next){
if(this.isNew){
this.constructor.find({}).then((result) => {
console.log(result)
this.id = result.length + 1;
next();
});
}
})
I didn't wan to use any plugin (an extra dependencie, initializing the mongodb connection apart from the one I use in the server.js, etc...) so I did an extra module, I can use it at any schema and even, I'm considering when you remove a document from the DB.
module.exports = async function(model, data, next) {
// Only applies to new documents, so updating with model.save() method won't update id
// We search for the biggest id into the documents (will search in the model, not whole db
// We limit the search to one result, in descendant order.
if(data.isNew) {
let total = await model.find().sort({id: -1}).limit(1);
data.id = total.length === 0 ? 1 : Number(total[0].id) + 1;
next();
};
};
And how to use it:
const autoincremental = require('../modules/auto-incremental');
Work.pre('save', function(next) {
autoincremental(model, this, next);
// Arguments:
// model: The model const here below
// this: The schema, the body of the document you wan to save
// next: next fn to continue
});
const model = mongoose.model('Work', Work);
module.exports = model;
Hope it helps you.
(If this Is wrong, please, tell me. I've been having no issues with this, but, not an expert)
Here is a proposal.
Create a separate collection to holds the max value for a model collection
const autoIncrementSchema = new Schema({
name: String,
seq: { type: Number, default: 0 }
});
const AutoIncrement = mongoose.model('AutoIncrement', autoIncrementSchema);
Now for each needed schema, add a pre-save hook.
For example, let the collection name is Test
schema.pre('save', function preSave(next) {
const doc = this;
if (doc.isNew) {
const nextSeq = AutoIncrement.findOneAndUpdate(
{ name: 'Test' },
{ $inc: { seq: 1 } },
{ new: true, upsert: true }
);
nextSeq
.then(nextValue => doc[autoIncrementableField] = nextValue)
.then(next);
}
else next();
}
As findOneAndUpdate is an atomic operation, no two updates will return same seq value. Thus each of your insertion will get an incremental seq regardless of number of concurrent insertions. Also this can be extended to more complex auto incremental logic and the auto increment sequence is not limited to Number type
This is not a tested code. Test before you use until I make a plugin for mongoose.
Update I found that this plugin implemented related approach.
The answers seem to increment the sequence even if the document already has an _id field (sort, whatever). This would be the case if you 'save' to update an existing document. No?
If I'm right, you'd want to call next() if this._id !== 0
The mongoose docs aren't super clear about this. If it is doing an update type query internally, then pre('save' may not be called.
CLARIFICATION
It appears the 'save' pre method is indeed called on updates.
I don't think you want to increment your sequence needlessly. It costs you a query and wastes the sequence number.
I had an issue using Mongoose Document when assigning value to Schema's field through put(). The count returns an Object itself and I have to access it's property.
I played at #Tigran's answer and here's my output:
// My goal is to auto increment the internalId field
export interface EntityDocument extends mongoose.Document {
internalId: number
}
entitySchema.pre<EntityDocument>('save', async function() {
if(!this.isNew) return;
const count = await counter.findByIdAndUpdate(
{_id: 'entityId'},
{$inc: {seq: 1}},
{new: true, upsert: true}
);
// Since count is returning an array
// I used get() to access its child
this.internalId = Number(count.get('seq'))
});
Version: mongoose#5.11.10
None of above answer works when you have unique fields in your schema
because unique check at db level and increment happen before db level validation, so you may skip lots of numbers in auto increments like above solutions
only in post save can find if data already saved on db or return error
schmea.post('save', function(error, doc, next) {
if (error.name === 'MongoError' && error.code === 11000) {
next(new Error('email must be unique'));
} else {
next(error);
}
});
https://stackoverflow.com/a/41479297/10038067
that is why none of above answers are not like atomic operations auto increment in sql like dbs
I use together #cluny85 and #edtech.
But I don't complete finish this issues.
counterModel.findByIdAndUpdate({_id: 'aid'}, {$inc: { seq: 1} }, function(error,counter){
But in function "pre('save...) then response of update counter finish after save document.
So I don't update counter to document.
Please check again all answer.Thank you.
Sorry. I can't add comment. Because I am newbie.
var CounterSchema = Schema({
_id: { type: String, required: true },
seq: { type: Number, default: 0 }
});
var counter = mongoose.model('counter', CounterSchema);
var entitySchema = mongoose.Schema({
testvalue: { type: String }
});
entitySchema.pre('save', function(next) {
if (this.isNew) {
var doc = this;
counter.findByIdAndUpdate({ _id: 'entityId' }, { $inc: { seq: 1 } }, { new: true, upsert: true })
.then(function(count) {
doc.testvalue = count.seq;
next();
})
.catch(function(error) {
throw error;
});
} else {
next();
}
});

Mongoose populate documents

I got 3 database models in mongoose that looks like this:
//profile.js
var ProfileSchema = new Schema({
username: { type: String, required: true },
password: { type: String, required: true },
matches: [{ type: Schema.Types.ObjectId, ref: 'Match' }]
});
//match.js
var MatchSchema = new Schema({
scores: [{ type: Schema.Types.ObjectId, ref: 'Score', required: true }],
});
//score.js
var ScoreSchema = new Schema({
score: {type: Number, required: true},
achivement: [{type: String, required: true}],
});
And I try to populate a profile with
Profile.findOne({ _id: mongoose.Types.ObjectId(profile_id) })
.populate('matches')
.populate('matches.scores')
.exec(function(err, profile) {
if (err) {...}
if (profile) {
console.log(profile);
}
});
The matches get populated but I dont get the scores in matches to populate. Is this not supported in mongoose or do I do something wrong? Populate gives me this:
{
user_token: "539b07397c045fc00efc8b84"
username: "username002"
sex: 0
country: "SE"
friends: []
-matches: [
-{
__v: 1
_id: "539eddf9eac17bb8185b950c"
-scores: [
"539ee1c876f274701e17c068"
"539ee1c876f274701e17c069"
"539ee1c876f274701e17c06a"
]
}
]
}
But I want to populate the score array in the match array. Can I do this?
Yes, you are right. I tried using Chaining of populate I got same output.
For your query please use async.js and then populate by the method mentioned below.
For more details please have a look at this code snippet. It is a working, tested, sample code according to your query. Please go through the commented code for better understanding in the code below and the link of the snippet provided.
//Find the profile and populate all matches related to that profile
Profile.findOne({
_id: mongoose.Types.ObjectId(profile_id)
})
.populate('matches')
.exec(function(err, profile) {
if (err) throw err;
//We got the profile and populated matches in Array Form
if (profile) {
// Now there are multiple Matches
// We want to fetch score of each Match
// for that particular profile
// For each match related to that profile
async.forEach(profile.matches, function(match) {
console.log(match, 'match')
// For each match related to that profile
// Populate score achieved by that person
Match.find({
_id:match.id
})
.populate('scores', 'score')
.exec(function (err, score) {
if (err) throw err;
// here is score of all the matches
// played by the person whose profile id
// is passed
console.log(score);
})
})
}
});
Profile.findOne({ _id: mongoose.Types.ObjectId(profile_id) })
.populate('matches.scores')
.exec(function(err, profile) {
if (err) {...}
if (profile) {
console.log(profile);
}
});

Categories

Resources