Validation error using `create()` in Mongoose transaction - mongodb

I'm testing transactions using mongoose and trying to accomplish a very simple task with the following code:
const session = await mongoose.startSession();
session.startTransaction();
try {
const opts = { session, new: true };
const A = await Author.
create({
firstName: 'Jon',
lastName: 'Snow'}, session);
if(!A) throw new Error('Aborted A by Amit!');
const B = await Author.
findOneAndUpdate({firstName: 'Bill'}, {firstName: 'William'}, opts);
if(!B) throw new Error('Aborted B by Amit!');
await session.commitTransaction();
session.endSession();
} catch (err) {
await session.abortTransaction();
session.endSession();
throw err;
}
All I'm trying to do is first insert (using mongoose create() method) a new document into a collection and then edit (using Mongo findOneAndUpdate() method) another document in the same collection. Failure of either query needs to abort the entire transaction suite.
It's the create() that seems to be giving me problems. The document does get created and inserted, however, it also throws an error:
"Author validation failed: lastName: Path lastName is required.,
firstName: Path firstName is required."
Any idea what this could mean? It seems it's complaining about not being given values for required fields (firstName and lastName) despite me having already given it those.

I have no idea why it would complain about missing values when I've provided them both and they're still getting added to the collection!
This is because Model.create() first parameters accept documents to insert, as an array OR as a spread. For example, these two are equivalent:
// pass a spread of docs
Candy.create({ type: 'jelly bean' }, { type: 'snickers' })
// pass an array of docs
Candy.create([{ type: 'jelly bean' }, { type: 'snickers' }])
The problem with your line, is that it's trying to take the second document {session: session} as another entry for Author, which is missing the required firstName and lastName fields.
Instead you should do:
Author.create([{ firstName: 'Quentin', lastName: 'Tarantino' }],
{ session: session }
);
You may also find Transactions in Mongoose a helpful reference.

Related

TypeError: parent.child.push is not a function - issue pushing my object to database

Problem
I'm trying to send data from my client-side form into my database and am running into the error below:
TypeError: artist.fans.push is not a function
The data is an email address that should be saved into my artist model as a subdoc (in an object).
What I've tried
I'm using the syntax from the mongo docs that says parent.children.push() is the proper way to add subdocs to arrays and parent.children.create() to create new subdocuments. Both yield the same error.
Here's my function:
module.exports.addFanEmail = async (req, res) => {
const fan = req.body;
const artist = await Artist.findById(req.params.id);
artist.fans.create(fan);
await artist.save();
res.redirect(`/artists/${artist._id}`);
}
Right now req.body is only the "fan's" email - here's an example of the object's format: { email: 'tom#test.com' }
DB Model
const artistSchema = new Schema({
image: [ImageSchema],
genre: [ String ],
fans: {
email: String,
subscribed: String,
gender: String,
age: Number
},
});
The object is coming through from the client to the function without any problem, I just can't get it to save to the db?

Circular Reference Issue in Mongoose pre-hook

In my MongoDB/Node backend environment I am using Mongoose pre and post hook middleware to check what's changed on the document, in order to create some system notes as a result.
One problem I'm running into is that when I try and lookup the record for the document in question I get a "Customer.findOne()" is not a function error. This is ONLY a problem when I'm looking up a record from the same collection from which the model just launched this pre and post hook triggers file. In other words, if my "Customer" model kicks off functions in a pre hook function in an external file, then I get an error if I then try and lookup a Customer with a standard findOne():
My customer model looks something like this:
module.exports = mongoose.model(
"Customer",
mongoose
.Schema(
{
__v: {
type: Number,
select: false
},
deleted: {
type: Boolean,
default: false
},
// Other props
searchResults: [
{
matchKey: String,
matchValue: String
}
]
},
{
timestamps: true
}
)
.pre("save", function(next) {
const doc = this;
trigger.preSave(doc);
next();
})
.post("save", function(doc) {
trigger.postSave(doc);
})
.post("update", function(doc) {
trigger.postSave(doc);
})
.post("findOneAndUpdate", function(doc) {
trigger.postSave(doc);
})
);
... the problematic findOne() function in the triggers file being called from the model looks like this:
const Customer = require("../../models/customer");
exports.preSave = async function(doc) {
this.preSaveDoc = await Customer.findOne({
_id: doc._id
}).exec();
};
To clarify, this is NOT a problem if I'm using a findOne() to lookup a record from a different collection in this same triggers file. Then it works fine. See below when finding a Contact -- no problem here:
const Contact = require("../../models/contact");
exports.preSave = async function(doc) {
this.preSaveDoc = await Contact.findOne({
_id: doc._id
}).exec();
};
The workaround I've found is to use Mongo instead of Mongoose, like so:
exports.preSave = async function(doc) {
let MongoClient = await require("../../config/database")();
let db = MongoClient.connection.db;
db.collection("customers")
.findOne({ _id: doc._id })
.then(doc => {
this.preSaveDoc = doc;
});
}
... but I'd prefer to use Mongoose syntax here. How can I use a findOne() in a pre-hook function being called from the same model/collection as the lookup type?
I have ran similar issue few days ago.
Effectively it is a circular dependency problem. When you call .findOne() on your customer model it doesn't exist as it is not exported yet.
You should probably try something like that :
const customerSchema = mongoose.Schema(...);
customerSchema.pre("save", async function(next) {
const customer = await Customer.findOne({
_id: this._id
}).exec();
trigger.setPreSaveDoc(customer);
next();
})
const Customer = mongoose.model("Customer", customerSchema)
module.export Customer;
Here customer will be defined because it is not called (the pre hook) before its creation.
As an easier way (I am not sure about it) but you could try to move the Contact import in your Trigger file under the save function export. That way I think the decencies may works.
Did it helps ?

Correctly inserting and/or updating many datasets to MongoDB (using mongoose)?

So from time to time I get new exports of a cities database of POIs and info about them and I want to have all that data in my MongoDB with a Loopback-API on it. Therefore I reduce the data to my desired structure and try to import it.
For the first time I receive such an export, I can simply insert the data with insertMany().
When I get a new export, it means that it includes updated POIs which I actually want my existing POIs to be replaced with that new data. So I thought I'd use updateMany() but I could'nt figure out how I'd do that in my case.
Here's what I have so far:
const fs = require('fs');
const mongoose = require('mongoose');
const data = JSON.parse(fs.readFileSync('data.json', 'utf8'));
// Connect to database
mongoose.connect('mongodb://localhost/test', {
useMongoClient: true
}, (err) => {
if (err) console.log('Error', err);
});
// Define schema
let poiSchema = new mongoose.Schema({
_id: Number,
name: String,
geo: String,
street: String,
housenumber: String,
phone: String,
website: String,
email: String,
category: String
});
// Set model
let poi = mongoose.model('poi', poiSchema);
// Generate specified data from given export
let reducedData = data['ogr:FeatureCollection']['gml:featureMember'].reduce((endData, iteratedItem) => {
endData = endData.length > 0 ? endData : [];
endData.push({
_id: iteratedItem['service']['fieldX'],
name: iteratedItem['service']['fieldX'],
geo: iteratedItem['service']['fieldX']['fieldY']['fieldZ'],
street: iteratedItem['service']['fieldX'],
housenumber: iteratedItem['service']['fieldX'],
phone: iteratedItem['service']['fieldX'],
website: iteratedItem['service']['fieldX'],
email: iteratedItem['service']['fieldX'],
category: iteratedItem['service']['fieldX']
});
return endData;
}, []);
//
// HERE: ?!?!? Insert/update reduced data in MongoDB collection ?!?!?
//
mongoose.disconnect();
So I just want to update everything that has changed.
Of course if I leave it to insertMany() it fails due to dup key.
For the second time, use mongo's update command with upsert set to true.
db.collection.update(query, update, options)
In the query pass the _id ,in update pass the object and in option set upsert to true. This will update the document if it exists creates a new document if that doesn't exist.

mongoose - how to validate specific fields only?

I have following mongoose model and routing file.
user.js
var mongoose = require('mongoose'),
Schema = mongoose.Schema,
ObjectId = Schema.ObjectId,
var userSchema = new Schema({
nick_name: {
type: String,
unique: true
},
email: {
type: String,
unique: true
},
first_name: String,
last_name: String,
birth_date: {
type: Date
},
password: {
type: String,
select: true
},
user_type: {
type: Number,
},
is_active: {
type: Number,
default: -1
}
}, { collection: 'user' });
/*
*Validations
*/
userSchema.path('nick_name').required(true, 'nick name is required!');
userSchema.path('email').required(true, 'email is required!');
userSchema.path('password').required(true, 'password is required!');
userSchema.path('user_type').required(true, 'user type is required!');
userSchema.path('is_active').required(true, 'is active is required!');
userSchema.path('is_close').required(true, 'is close is required!');
userSchema.path('first_name').required(true, 'first name is required!');
userSchema.path('last_name').required(true, 'last name is required!');
userSchema.path('birth_date').required(true, 'birth date is required!');
var User = module.exports = mongoose.model("User", userSchema);
router.js
var express = require('express');
var router = express.Router();
var User = require('../models/user');
router
.route('/api/user/register')
.post(
function(req, res, next) {
var user_ = new User(req.body);
/*
*here all validations are required
*/
user_.validate(function(err) {
if (err) {
res.json({ "status": 0, "error": err });
} else {
user_.save(function(err) {
if (err) {
res.json({ "status": 0, "error": { "other": "Oops! something went wrong, please try again later." } });
} else {
res.json({ error: 1, message: 'User registered' });
}
});
}
}
});
}
});
In above routing file I can validate all fields by using validate() method but, I have need validation as following conditions
->When user register, following fields are required
nick_name
email
password
user_type
is_active
->When user edit his profile (after register), all fields are required.
Can anybody help me to solve this issue ?
I just found myself in this situation, want to update a comment model and want a specific field validation for field 'content'.
Im thinking about a hack, pull off that full comment document from the database, then create a new schema object with the same properties from the comment document that i just pulled off from the database and validate this document model copy as if i were to create a new document, but i wont, i wont use the save() method. If there is an error with the 'content' field, which is the only one i care, i would know after validation, if there is no errors then i forget about that new object schema copy that i created by pulling off the comment document from the database, ill forget about it since i already know my 'content' field is valid since no errors where shown, so ill proceed with my flow.
Perhaps instead of pulling off that document from the database i can just create a new object with some fake but valid fields... Then pass the real value i want to test which in my case is 'content', i wouldnt fake that value since i already have it.
NOTE: my comment model has property 'createdAt' so i would replace that for the current date, cause i could have errors at validation saying new comment must be from current date and no from past dates, but since i wont be saving that new date to the database i can add the current date, recall that i will forget about that new object, i wont save it to the database, all i care is the 'content' field validation and see if there is any errors.

Mongoose: How can I access a select:false property in a schema method?

Quick code:
var userSchema = new mongoose.Schema({
username: String,
password: {type: String, select: false}
});
userSchema.methods.checkPassword = function(password, done) {
console.log(password); // Password to check
console.log(this.password); // stored password
...
};
I don't want the password to be accessible by default, but I need a method to check against a user inputted password before authenticating the user. I know I can do a query to the DB to include these values, but I'm a bit lost on how I could access the hidden property on the schema method itself. this in the method itself is just the returned query, so it seems like it is inaccessible? Should I be doing the checkPassword() function elsewhere?
You can use select to select password in query. This is an example query.
User.findOne().select('password').exec(callback);
And this must be what you want to check password.
userSchema.methods.checkPassword = function(password, done) {
User.findOne({username: this.username}).select('password').exec(function (err, user) {
if (user.password == password)
return true;
else
return false;
});
}
I hope this might help you.
You can explicitly allow the password field (with {select:"false"}) to be returned in your find call with "+" operator before field e.g.:
User.findOne({}).select("+password") // "+" = allow select hidden field
A right way is writing the fields on method findOne. You can ask the fields that you want to return. In your case, it should be:
await User.findOne({ username: this.username }, 'password').exec();
Documentation:
mongoose.findOne
Above answers only show selection for a single property.
For multiple properties, syntax is this one:
await this.userModel
.findOne({ email }, { status: 1, firstName: 1, religion: 1 })
.exec();
This will return:
{
_id: new ObjectId("62de5a5158b809468b812345"),
status: 'Active',
firstName: 'John',
religion: 'Christian Orthodox'
}