Mongoose Select Method Not Working As Expected - javascript

Trying to simply include a field that has select set to false within the schema. Overriding it with select method is not working for some reason. Exclusion of other fields work but inclusion doesn't. In the following example I'm trying to include the "active_account" field. What could be causing this behavior?
Schema
const Schema = new mongoose.Schema({
active_account: {
type: Boolean,
default: true,
select: false
}
})
Function
exports.deactivate_user = async ( req , res , next ) => {
const user = await User.findByIdAndUpdate( req.params.id , { active_account: false } ).select( '+active_account' );
res.status( 200 ).json({
status: 'Success',
data: user
});
};

According to docs;
A.findByIdAndUpdate(id, update, options) // returns Query
So,
const user = await User.findByIdAndUpdate(
req.params.id, //id
{ active_account: false }, //update
{ select: 'active_account', new: true } //options
);
set new = true to get the updated version of data.

Related

How to find a document of an ID from an array in JavaScript (Mongoose)

I have a function on my server that is supposed to get a post by its ID. The function works up until the "foundPost" constant, where I can't seem to find one of the documents from the "posts" array. I've tried substituting findOne for find and the ObjectIds work for the const 'post'.
I've double checked that post_id is 62067c1211eea1531d5872f4
Here is the function to find a post:
const postById = async (req, res) => {
const userId = req.params.userId;
const post_id = req.params.post_id;
const posts = await Post.findOne({ user: userId });
console.log(posts); //see this below
const foundPost = await posts.findOne({ "upload": post_id }); //error here
console.log(foundPost);
return res.json({ success: true, Post: foundPost });
};
Here is what 'console.log(posts)' returns:
[
{
upload: new ObjectId("623b681bdf85df9086417723"),
edited: false,
title: 'Test 1',
description: 'testing post 1',
name: 'John ',
sharedPost: 0,
},
{
upload: new ObjectId("62067c1211eea1531d5872f4"),
edited: false,
title: 'Test 2',
description: 'testing post 2',
name: 'John ',
sharedPost: 0,
}
]
I'm hoping that the function will return:
{
success: true,
{
upload: new ObjectId("62067c1211eea1531d5872f4"),
edited: false,
title: 'Test 2',
description: 'testing post 2',
name: 'John ',
sharedPost: 0,
},
}
Can anyone see why the line const foundPost = await posts.findOne({ "upload": post_id }); isn't working?
Thank you for your help.
****** Response to answer ******
Hello, thanks a lot for your answer, unfortunately it's still giving an error. Please see below the model for the code I'm using:
const PostSchema = new Schema({
user: {
type: Schema.Types.ObjectId,
ref: "user",
},
post: [
{
user: {
type: Schema.Types.ObjectId,
ref: "user",
},
upload: {
type: Schema.Types.ObjectId,
ref: "upload",
},
title: {
type: String,
},
description: {
type: String,
},
},
],
date: {
type: Date,
default: Date.now,
},
});
I noticed that you used 'userProfile.posts' which I adapted to 'userProfile.post' to match this schema. I'm also not sure if you wanted to use 'subdoc' or 'subDoc' in line 11 of your code, but I tried both with the same error for each. I determined that the code stuck at the const subDocs = userPosts.filter(filter); line. I've looked into the .filter method you've used and can't find any potential errors. Not sure what the issue is.
Thanks
The issue is that you cannot run another mongo query on objects that were result of a previous query.
//will return a single document if found, or null if not found.
const posts = await Post.findOne({ user: userId });
//this will not work because at this point 'posts' will be either a Document or null value
//So the object will not have the method 'findOne' available.
const foundPost = await posts.findOne({ "upload": post_id });
The solution is to deal correctly with the types of objects you have.
Below is a functional and safe implementation that solves your issue:
const userPosts = await Post.findOne({ user: userId }).exec();
if (!userPosts) {
// document not found with provided userId
return res.json({ success: false });
}
//here we have a Document
//check if document has 'posts' property and is an array
if (userPosts.posts) {
//filter the posts array
const filter = function(subDoc) {
return subdoc.upload === post_id
}
const subDocs = userPosts.filter(filter);
//filter returns an array, so we must check if has itens
//then we grab the first item
if (subDocs.length > 0) {
const foundPost = subDocs[0];
return res.json({ success: true, Post: foundPost });
}
//subDoc not found, return correct response
return res.json({ success: false });
}
If your Post model schema is what I'm supposing to be, this code will work perfectly.
const schema = mongoose.schema({
user: Number,
posts: [{ upload: Number }]
})
In case of error, please add the code of the model schema structure.

can't update boolean mongoose object with a time stamp

I am having trouble updating a boolean object and i am getting error every time-
this is the object-
const mongoose = require('mongoose');
const PlantProductSchema = new mongoose.Schema({
waterMotor: {
state: {
type: Boolean,
default: false,
time: { type: Date, default: Date.now },
}
},
});
module.exports = mongoose.model('PlantProduct', PlantProductSchema);
this is the update action-
plantProduct.waterMotor.update({state: idResultsObj.motorState });
idResultsObj.motorStat is boolean i chacked
on the other hand when I change I do this -
plantProduct.waterMotor.state = idResultsObj.motorState;
it works but It doesn't give a time stamp.
I appreciate any help I get!!
you are not using the update function correctly. it accepts two arguments, the first is the document to be updated and the second is the action.
you need to pass the id (or any other field like username) of the document that is being updated as the first argument.
plantProduct.waterMotor.update({_id: id}, {state: idResultsObj.motorState });
These are a couple of API which mongoose support for an update operation.
Regarding your code you have used update in the wrong way. update is the property of the Model object that why you are getting undefined. thought said below is the query that might help you.
const mongoose = require('mongoose');
const PlantProductSchema = new mongoose.Schema({
waterMotor: {
state: {
type: Boolean,
default: false,
time: {
type: Date,
default: Date.now
},
}
},
});
const ProductModel = mongoose.model('PlantProduct', PlantProductSchema);
const filterQuery = {}
const updateQuery = {
$set: {
"waterMotor.state": true
}
}
ProductModel.update(filterQuery, updateQuery, function(error, result) {
if (error) {
console.log(error)
} else {
console.log(response)
}
})

Set field in mongoose document to array length

I have a Mongoose document (Mongoose 5.4.13, mongoDB 4.0.12):
var SkillSchema = new mongoose.Schema({
skill: { type: String },
count: { type: Number, default: 0 },
associatedUsers: [{ type : mongoose.Schema.Types.ObjectId, ref: 'User' }]
});
That I update as follows:
var query = { skill: req.body.skill };
var update = { $addToSet: { associatedUsers: req.params.id } };
var options = { upsert: true, new: true, setDefaultsOnInsert: true };
await skillSchema.findOneAndUpdate(query, update, options);
During this update, I would like to also update count to be equal to the length of associatedUsers.
Ideally I want this to happen at the same time as updating the other fields (i.e not in a subsequent update), either via a pre-hook or within findOneAndUpdate.
I've tried using a pre hook after schema definition:
SkillSchema.pre('findOneAndUpdate', async function(){
console.log("counting associated users");
this.count = this.associatedUsers.length;
next();
});
As well as using aggregate in my UPDATE route:
await skillSchema.aggregate([{ $project: { count: { $size: "$associatedUsers" } } } ])
But I can't get either to work.
Does anyone have any suggestions for how I could achieve this?
You could use $set like this in 4.2 which supports aggregation pipeline in update.
The first $set stage calculates a associatedUsers based on the previous and new value. $setUnion to keep the distinct associatedUsers values.
The second $set stage calculates tally based on the associatedUsers calculated in the previous stage.$size to calculate the length of associatedUsers values.
var query = {skill: req.body.skill};
var update = [{ $set: { "associatedUsers":{"$setUnion":[{"$ifNull":["$associatedUsers",[]]}, [req.params.id]] }}}, {$set:{tally:{ $size: "$associatedUsers" }}}];
var options = { upsert: true, new: true, setDefaultsOnInsert: true };
await skillSchema.findOneAndUpdate(query, update, options)
If any argument resolves to a value of null or refers to a field that is missing, $setUnion returns null. So just needed to safeguard our operation with $ifNull
About tally and associatedUsers.length
// define your schema object
var schemaObj = {
skill: { type: String },
associatedUsers: { type: Array }
};
// get the length of users
var lengthOfAsUsers = schemaObj.associatedUsers.length;
// add tally to schema object and set default to the length of users
schemaObj.tally = { type: Number, default: lengthOfAsUsers };
// and pass your schema object to mongoose.Schema
var SkillSchema = new mongoose.Schema(schemaObj);
module.exports = SkillSchema;
EDIT
you can update tally subsequently, but recommended solution would be to use this method
https://mongoosejs.com/docs/populate.html
const id = "nameSomeId";
SkillSchema.find({ _id: id }).then(resp => {
const tallyToUpdate = resp.associatedUsers.length;
SkillSchema.findOneAndUpdate({ _id: id }, { tally: tallyToUpdate }).then(
resp => {
console.log(resp);
}
);
});
The solution I have will only work on mongodb v 4.2 as it has option to use aggregate in the update and will only need one query as:
skillSchemafindOneAndUpdate(
{skill:"art"},
[
{ $set: {
associatedUsers:{
$cond:{
if: {$gte: [{$indexOfArray: ["$associatedUsers", mongoose.Types.ObjectId(req.params.id)]}, 0]},
then: "$associatedUsers",
else: { $cond:{
if: { $isArray: "$associatedUsers" },
then: {$concatArrays:["$associatedUsers",[mongoose.Types.ObjectId(req.params.id)]]},
else: [mongoose.Types.ObjectId(req.params.id)]
}}
}
}}},
{$set:{
associatedUsers:"$associatedUsers",
tally:{$size:"$associatedUsers"},
}}
],
{upsert:true,new:true}
)
ref: https://docs.mongodb.com/manual/reference/method/db.collection.update/#update-with-aggregation-pipeline
The "Group" field does not appear in the schema. On MongoDB Shell, these codes will work.
However, Mongoose will also give an error because the schema is validated.
Is the "Group" field a dynamic field? I think the problem with the schema will be solved.
var mongoose = require("mongoose");
var SkillSchema = new mongoose.Schema({
skill: { type: String },
tally: { type: Number, default: 0 },
associatedUsers: { type: Array },
group: { type: Array }
});

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

How to exclude one particular field from a collection in Mongoose?

I have a NodeJS application with Mongoose ODM(Mongoose 3.3.1). I want to retrieve all fields except 1 from my collection.For Example: I have a collection Product Which have 6 fields,I want to select all except a field "Image" . I used "exclude" method, but got error..
This was my code.
var Query = models.Product.find();
Query.exclude('title Image');
if (req.params.id) {
Query.where('_id', req.params.id);
}
Query.exec(function (err, product) {
if (!err) {
return res.send({ 'statusCode': 200, 'statusText': 'OK', 'data': product });
} else {
return res.send(500);
}
});
But this returns error
Express
500 TypeError: Object #<Query> has no method 'exclude'.........
Also I tried, var Query = models.Product.find().exclude('title','Image'); and var Query = models.Product.find({}).exclude('title','Image'); But getting the same error. How to exclude one/(two) particular fields from a collection in Mongoose.
Use query.select for field selection in the current (3.x) Mongoose builds.
Prefix a field name you want to exclude with a -; so in your case:
Query.select('-Image');
Quick aside: in JavaScript, variables starting with a capital letter should be reserved for constructor functions. So consider renaming Query as query in your code.
I don't know where you read about that .exclude function, because I can't find it in any documentation.
But you can exclude fields by using the second parameter of the find method.
Here is an example from the official documentation:
db.inventory.find( { type: 'food' }, { type:0 } )
This operation returns all documents where the value of the type field is food, but does not include the type field in the output.
Model.findOne({ _id: Your Id}, { password: 0, name: 0 }, function(err, user){
// put your code
});
this code worked in my project. Thanks!! have a nice day.
You could do this
const products = await Product.find().select(['-image'])
I am use this with async await
async (req, res) => {
try {
await User.findById(req.user,'name email',(err, user) => {
if(err || !user){
return res.status(404)
} else {
return res.status(200).json({
user,
});
}
});
} catch (error) {
console.log(error);
}
In the updated version of Mongoose you can use it in this way as below to get selected fields.
user.findById({_id: req.body.id}, 'username phno address').then(response => {
res.status(200).json({
result: true,
details: response
});
}).catch(err => {
res.status(500).json({ result: false });
});
I'm working on a feature. I store a userId array name "collectedUser" than who is collected the project. And I just want to return a field "isCollected" instead of "collectedUsers". So select is not what I want. But I got this solution.
This is after I get projects from database, I add "isCollected".
for (const item of projects) {
item.set("isCollected", item.collectedUsers.includes(userId), {
strict: false,
})
}
And this is in Decorator #Schema
#Schema({
timestamps: true,
toObject: {
virtuals: true,
versionKey: false,
transform: (doc, ret, options): Partial<Project> => {
return {
...ret,
projectManagers: undefined,
projectMembers: undefined,
collectedUsers: undefined
}
}
}
})
Finally in my controller
projects = projects.map(i => i.toObject())
It's a strange tricks that set undefined, but it really work.
Btw I'm using nestjs.
You can do it like this
const products = await Product.find().select({
"image": 0
});
For anyone looking for a way to always omit a field - more like a global option rather than doing so in the query e.g. a password field, using a getter that returns undefined also works
{
password: {
type: String,
required: true,
get: () => undefined,
},
}
NB: Getters must be enabled with option { toObject: { getters:true } }
you can exclude the field from the schema definition
by adding the attribute
excludedField : {
...
select: false,
...
}
whenever you want to add it to your result,
add this to your find()
find().select('+excludedFiled')

Categories

Resources