Unique and Sparse Schema-level Index MongoDB and Mongoose - node.js

I am trying to create an index on two fields of a schema that are to be unique and sparse in MongoDB using Mongoose as follows:
var ArraySchema = new Schema ({
user_id: {type: mongoose.Schema.Types.ObjectId, ref:'User'},
event_id: {type: mongoose.Schema.Types.ObjectId, ref:'Event'}
}, {_id:false});
ListSchema.index({user_id:1, event_id:1}, {sparse:true, unique:true});
Which is then used in an array in the User schema as such:
var User = new Schema({
arrayType1 : {
type: [ArraySchema]
},
arrayType2 : {
type: [ArraySchema]
},
arrayType3 : {
type: [ArraySchema]
}
//More specifications for user schema...
});
However, when trying to save multiple users without the array field, errors are thrown for duplicate fields. The error in Mocha looks similar to this: array.event_id_1 dup key {null, null}. An example of a segment of code that would throw this error is as follows:
var user1, user2;
user1 = new User({
username : 'username1',
password : 'password'
});
user2 = new User({
username : 'username2',
password : 'password'
});
user1.save(function() {
user2.save();
});
Here is my reasoning behind making the the fields of ArraySchema unique and sparse: If the array field is specifed, I do not want the array to contain duplicate objects; however, the array field is not required, so there will be many Users that have null for this field. Obviously I cannot use field-level indices since there are multiple fields that would need an index (arrayType1, arrayType2, arrayType3).

It appears that doing this sort of thing is not supported, at least at this time. The alternative would be to create a compound index on these fields then whenever adding a new element to the field use user.arrayType1.addToSet(). Here is an example of how this would work:
ArraySchema:
var ArraySchema = new Schema ({
user_id: {type: mongoose.Schema.Types.ObjectId, ref:'User'},
event_id: {type: mongoose.Schema.Types.ObjectId, ref:'Event'}
}, {_id:false});
ListSchema.index({user_id:1, event_id:1});
User schema:
var User = new Schema({
arrayType1 : {
type: [ArraySchema]
},
arrayType2 : {
type: [ArraySchema]
},
arrayType3 : {
type: [ArraySchema]
}
//More specifications for user schema...
});
Then I could declare new users as usual (as I did in the question); however, when I want to add a new element to arrayType1, for example, I would use the following line of code to add to new element only if it is not already there:
user.arrayType1.addToSet({user_id : user2._id, event_id : event._id});
user.save(function(err, result) {
//Callback logic.
};
Where user2 and event are defined earlier in the code and saved to the db. Alternatively I could use Mongoose's update function as such:
User.update({_id : user._id}, {
$addToSet : {
arrayType1 : {
user_id : user2._id,
event_id : event._id
}
}
}, function(err) {
//Callback logic.
}
);

Related

Using "deleteMany" post middleware in mongoose

In my Nodejs and Express app, I have a mongoose User schema, a Post schema and a Comment schema as follows:
const UserSchema = new Schema({
username: {
type: String,
required: true,
unique: true
},
password: String,
posts : [
{
type : mongoose.Schema.Types.ObjectId,
ref : 'Post'
}
]
});
const PostSchema = new Schema({
author : {
type : mongoose.Schema.Types.ObjectId,
ref : 'User'
},
createdAt: { type: Date, default: Date.now },
text: String,
comments : [
{
type : mongoose.Schema.Types.ObjectId,
ref : 'Comment'
}
],
});
const CommentSchema = new Schema({
author : {
type : mongoose.Schema.Types.ObjectId,
ref : 'User'
},
createdAt: { type: Date, default: Date.now },
text: String
});
I have coded the general CRUD operations for my User. When deleting my user, I can easily delete all posts associated with that user using deleteMany:
Post.deleteMany ({ _id: {$in : user.posts}});
To delete all the comments for all the deleted posts, I can probably loop through posts and delete all the comments, but I looked at mongoose documentation here and it seems that deleteMany function triggers the deleteMany middleware. So In my Post schema, I went ahead and added the following after defining schema and before exporting the model.
PostSchema.post('deleteMany', async (doc) => {
if (doc) {
await Comment.deleteMany({
_id: {
$in: doc.comments
}
})
}
})
When deleting user, this middleware is triggered, but the comments don't get deleted. I got the value of doc using console.log(doc) and I don't think it includes what I need for what I intend to do. Can someone tell me how to use the deleteMany middleware properly or if this is not the correct path, what is the most efficient way for me to delete all the associated comments when I delete the user and their posts?
deleteMany will not give you access to the affected document because it's a query middleware rather than a document middleware (see https://mongoosejs.com/docs/middleware.html#types-of-middleware). Instead it returns the "Receipt-like" object where it tells it successfully deleted n objects and such.
In order for your hook to work as expected, you'll need to use something other than deleteMany, such as getting all of the documents (or their IDs), and loop through each one, using deleteOne.

Mongoose: schema._getVirtual is not a function

I'm having trouble with population all of a sudden (was working fine before I updated Mongoose package version). Currently using Mongoose 4.7.6.
var userSchema = require('schemas/user'),
User = db.model('User', userSchema); // db is already known
User
.findById(user._id) // user is already known due to auth
.populate('group currentPlayer')
.exec(function (findErr, userPlayer) { ... });
If my Schema for User is necessary, I will post it, but currently haven't due to its length (virtuals, methods, statics).
Error:
/app/node_modules/mongoose/lib/model.js:2986
var virtual = modelForCurrentDoc.schema._getVirtual(options.path);
^
TypeError: modelForCurrentDoc.schema._getVirtual is not a function
at getModelsMapForPopulate (/app/node_modules/mongoose/lib/model.js:2986:49)
at populate (/app/node_modules/mongoose/lib/model.js:2660:15)
at _populate (/app/node_modules/mongoose/lib/model.js:2628:5)
at Function.Model.populate (/app/node_modules/mongoose/lib/model.js:2588:5)
at Immediate.<anonymous> (/app/node_modules/mongoose/lib/query.js:1275:17)
...
User Schema
var
Mongoose = require('mongoose'),
Bcrypt = require('bcrypt'),
ObjectID = Mongoose.Schema.Types.ObjectId,
UserSchema = new Mongoose.Schema({
active : { type: Boolean, default: true },
created : { type: Date, required: true, default: Date.now },
modified : { type: Date, required: true, default: Date.now },
createdBy : { type: ObjectID, ref: 'User' },
modifiedBy : { type: ObjectID, ref: 'User' },
email : { type: String, required: true },
salt : { type: String },
hash : { type: String },
session : String,
group : { type: ObjectID, ref: 'Group', required: true },
currentPlayer : { type: ObjectID, ref: 'Player' },
validated : { type: Boolean, default: false },
ipAddress : String,
lastIp : String,
notes : String
});
var _checkPassword = function (password) { ... };
UserSchema.pre('validate', function (next) {
if (this.password && !_checkPassword(this.password)) {
this.invalidate('password', 'invalid', "Six character minimum, must contain at least one letter and one number or special character.");
}
next();
});
UserSchema.pre('save', function (next) {
this.modified = Date.now();
next();
});
UserSchema.virtual('password')
.get(function () { return this._password; })
.set(function (passwd) {
this.salt = Bcrypt.genSaltSync(10);
this._password = passwd;
this.hash = Bcrypt.hashSync(passwd, this.salt);
});
UserSchema.method('verifyPassword', function (password, done) {
Bcrypt.compare(password, this.hash, done);
});
UserSchema.static('authenticate', function (email, password, done) {
...
});
module.exports = UserSchema;
If anyone comes across this problem, it is probably because you have multiple package.json files with mongoose as a dependency in two of them. Make sure you use one package version of mongoose in your project and register your models there.
I have now filed a bug on GitHub, since reverting to version 4.6.8 allows my application to work again. https://github.com/Automattic/mongoose/issues/4898
After upgrading to Mongoose v4.7, I now receive an error when populating documents.
The chain of events to reproduce this error:
- define a Schema in its own file and use module.exports on the defined Schema object
- require() the Schema file
- use mongoose.model() to build a model from this Schema
- attempt to retrieve a record by using find() and populate()
- TypeError: modelForCurrentDoc.schema._getVirtual is not a function
If I do NOT use an "external" Schema file, and instead define the Schema inline, the problem goes away. However, this is not tenable due to statics, methods and virtuals defined in many Schemas.
Α possible answer can be found here:
The error was raised because I had a field called id that probably was
overriding the internal _id field.
Source : https://stackoverflow.com/a/53569877/5683645

Retrieving ID of subdocument after update in MongoDB

I wrote a service to push a new data to my collection using update statement and I need to retrieve the id of last inserted data. I am using db.collection.update for this, but it just giving a response like this:
{
"result": {
"ok": 1,
"nModified": 1,
"n": 1
}
}
My api for that is:
app.post('/feeds',function(req,res) {
var _id = req.body._id;
var title = req.body.title;
var description = req.body.description;
var latitude = Number(req.body.latitude);
var longitude = Number(req.body.longitude);
db.user.update(
{_id:_id },
{$push : {
feed:{
title: title,
description:description,
latitude:latitude,
longitude:longitude
}
}
},function (err,result) {
if (err) {
res.json({"success": '0', "message": "Error adding data"});
}
else {
res.json({'result':result});
}
});
});
This is my Mongoose schema:
var user = new mongoose.Schema({
username : {type: String},
email : {type: String,index: {unique: true}},
password : {type: String},
feed : [{
title : {type: String},
description : {type: String},
latitude : {type:Number},
longitude : {type:Number},
feedImages : [{
imageUrl: {type: String}
}],
}]
});
I want to add data to feed.
My database structure
I want the id of newly pushed feed.
Embedded way (current schema)
If you want to use one document per feed and pushing sub-documents into (embedded way), you can pre-assign a _id to your newly created feed sub-document:
var feedId = new ObjectId();
db.user.update({
_id: _id
}, {
$push: {
feed: {
_id: feedId,
title: title,
description: description,
latitude: latitude,
longitude: longitude
}
}
}, function(err, result) {
/* ...your code... */
});
Don't forget to add a index for user.feed._id if you want to query for this field (using ensureIndex orcreateIndex depending of your mongodb version).
Using separate collection
As alternative way, you can use a separate feed collection, and use insert statements including a userId foreign key.
You can find more about pro/cons using Multiple Collections vs Embedded Documents here.
You should use db.user.insert() , why do you need the id, when you already have it from the req body?
db.user.insert(objectToInsert, function(err,docsInserted){
console.log(docsInserted);
});
It's a good case for findOneAndUpdate.
Finds a matching document, updates it according to the update arg, passing any options, and returns the found document (if any) to the callback. The query executes immediately if callback is passed.
Available options
new: bool - if true, return the modified document rather than the original. defaults to false (changed in 4.0)

Mongoose: how to structure a schema with a SET of subdocuments (one unique field)?

I'm trying to make the following schema to work:
var FormSchema = new mongoose.Schema({
form_code: { type: String, unique: true },
...
});
var UserSchema = new mongoose.Schema({
...
submissions: [{
form_code: { type: String, unique: true },
last_update: Date,
questions: [{
question_code: String,
answers: [Number]
}]
}],
});
The rationale here is that a user can have many unique forms submitted, but only the last submission of each unique form should be saved. So, ideally, by pushing a submission subdocument when updating a user, the schema would either add the submission object to the set, or update the subdocument containing that form_code.
The following code doesn't work as desired (it pushes the new subdocument even if the form_code is already present):
User.findOneAndUpdate(
{ _id: user.id },
{ $addToSet: { submissions: submission_object } },
function (err, user) {
// will eventually have duplicates of form_code at user.submissions
}
);
The above schema clearly doesn't work, what must be changed to achieve that "upsertToSet"?

Mongoose search from populated document

I have this simplified Mongoose Schema:
UserSchema = new db.Schema({
_id : { type: db.Schema.Types.ObjectId,
name : { type : String },
friends : [{ type: db.Schema.Types.ObjectId, ref: 'User' }]
})
I want to get all friends with name that starts with a certain key string, let's say 'Pe' should return records 'Peter' and 'Petra'.
So I tried populating the friends field first:
user.Model
.findOne({
_id : loggedId,
})
.select('friends')
.populate('friends', 'name')
.exec(function(err, results) {
console.log(results);
});
That will return all the user's friends and their name which is not really what I want.
How do I return only those with a name that starts with certain characters?
TIA.
You can add options to the populate call.
See http://mongoosejs.com/docs/api.html#model_Model.populate

Resources