I need to create an index in MongoDB to store unique slugs.
I use this code to generate the index:
this._db = db;
this._collection = this._db.collection("Topics");
this._collection.ensureIndex( { slug: 1 }, { unique: true });
But when I run my tests it fails on the "beforeEach": (I'm using mongo-clean NPM)
beforeEach(function (done) {
clean(dbURI, function (err, created) {
db = created;
instance = topicManager(db);
done(err);
});
});
Saying:
Uncaught Error: Cannot use a writeConcern without a provided callback
What am I doing wrong? (if I comment the ensureIndex everything works)
As the response implies, you might need to provide a callback function to your call to ensureIndex:
this._db = db;
this._collection = this._db.collection("Topics");
this._collection.ensureIndex( { slug: 1 }, { unique: true }, function(error) {
if (error) {
// oops!
}});
ensueIndex() is now deprecated in v^3.0.0
For those who use ^3.0.0:
Deprecated since version 3.0.0: db.collection.ensureIndex() is now an alias for db.collection.createIndex().
https://docs.mongodb.com/manual/core/index-unique/#index-type-unique
Example:
db.members.createIndex( { "user_id": 1 }, { unique: true } )
Related
NOTE: there's an edit at the bottom of the question:
Can I check the database for uniqueness using either a custom validator or a pre hook in a Mongoose.js model file. I am aware that I can check it in the controller, but I'd rather put it in the model file with the rest of the validators just for consistency.
I am also aware there is an npm package called mongoose-unique-validator that does this but I'm no fan of installing a library to do what should be one to five lines of code tops.
Mongoose also has a "unique" property that will throw an error if the item is not unique. But their documents clearly state this is not a validator. And the error it throws does not get routed the same as the validation errors.
Here is the relevant parts of the model file. This will check the db and if there is no dup then it creates the article but if there is a dup it throws an error but not a validation error which is what I want. If I simply return false if there is a dup it just ignores the validation and creates the duplicate article. This is no doubt related to Promises/Async. Here are the relevant Mongoose docs https://mongoosejs.com/docs/validation.html#async-custom-validators. And they talk about how the unique property is not a validator https://mongoosejs.com/docs/faq.html.
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const articleSchema = new Schema({
title: {
type: String,
required: [true, "Title is required"],
// unique: true,
// isAsync: true,
validate: {
validator: function(value) {
this.constructor.findOne({title: value}, (err, article) => {
if (err || !article) {
return true;
} else {
// return false;
throw new Error('Duplicate');
}
});
},
message: (props) => `Title "${props.value}" is already in use.`
},
},
content: { type: String, required: true }
});
EDIT: I figured this out, but it only works when creating a new article, not on updates. So the question is still open but the focus is on how to get it to work on updates. On update Mongoose does not treat "this" as the document object like it does on create. Instead "this" is the request object, and "this.constructor.findOne()" throws the error "this.constructor.findOne is not a function". Here's the revised validator:
title: {
type: String,
required: [true, "Title is required"],
isAsync: true,
validate: {
validator: async function(value) {
const article = await this.constructor.findOne({title: value});
if (article) {
throw new Error(`${value} is already in use.`);
}
}
}
}
Your validator function will only run the script and it not pass any callback or promise to mongoose, so mongoose assume that the validator return true and continue the process.
According to the document, you should return promise or use callback.
Promise:
validator: function(value) {
var here = this;
return new Promise(function(resolve, reject) {
here.constructor.findOne({title: value}, (err, article) => {
if (err || !article) {
resolve(true);
} else {
resolve(false);
}
});
})
}
Callback: (need to set isAsync: true)
validator: function(value, cb) {
this.constructor.findOne({title: value}, (err, article) => {
if (err || !article) {
cb(true);
} else {
cb(false, "Content is used");
}
});
}
am using sails-sqlserver as my adapter am just trying the create a new row in the database in one of the follwing models.
This is the first model :
// Roles.js
module.exports = {
autoCreatedAt: false,
autoUpdatedAt: false,
attributes: {
name: {
type: 'string'
},
approval_level: {
model: 'approval_levels'
},
specified: {
type: 'boolean'
},
entity_operations: {
collection: 'entity_operations',
via: 'roles',
dominant: true
},
users: {
collection: 'system_users',
via: 'role'
}
},
createRole: function (name, approval_level, cb) {
values = {
name: name,
approval_level: approval_level
};
Roles.create(values).exec(cb);
},
getAll: function (cb) {
Roles.find().exec(cb);
}
};
This is the second model :
// Entity_Operations.js
module.exports = {
autoCreatedAt: false,
autoUpdatedAt: false,
attributes: {
entity_name: {
type: 'string'
},
operation: {
model: 'operations'
},
roles: {
collection: 'roles',
via: 'entity_operations'
}
},
getAll: function (cb) {
Entity_operations.find().exec(cb);
}
};
These two models have a many to many relationship together what am trying to do is just this :
Entity_operations.create({
entity_name: 'example',
operation: 6
}).exec((err, entity: Entity_operations) => {
console.log(entity);
});
then this error comes out without explaining anything that could help me know from where this error is coming from :
/opt/nodejs/back-heaven/dev/node_modules/sails-sqlserver/lib/adapter.js:435
Object.keys(connections[connection].collections[collection].definition).forEach(function(key) {
^
TypeError: Cannot read property 'collections' of undefined
at Object.getPrimaryKey (/opt/nodejs/back-heaven/dev/node_modules/sails-sqlserver/lib/adapter.js:435:42)
at Object.create (/opt/nodejs/back-heaven/dev/node_modules/sails-sqlserver/lib/adapter.js:374:24)
at module.exports.create (/opt/nodejs/back-heaven/dev/node_modules/waterline/lib/waterline/adapter/dql.js:84:13)
at child.createValues (/opt/nodejs/back-heaven/dev/node_modules/waterline/lib/waterline/query/dql/create.js:220:16)
at /opt/nodejs/back-heaven/dev/node_modules/waterline/lib/waterline/query/dql/create.js:74:20
at /opt/nodejs/back-heaven/dev/node_modules/async/lib/async.js:726:13
at /opt/nodejs/back-heaven/dev/node_modules/async/lib/async.js:52:16
at /opt/nodejs/back-heaven/dev/node_modules/async/lib/async.js:269:32
at /opt/nodejs/back-heaven/dev/node_modules/async/lib/async.js:44:16
at /opt/nodejs/back-heaven/dev/node_modules/async/lib/async.js:723:17
at /opt/nodejs/back-heaven/dev/node_modules/async/lib/async.js:167:37
at /opt/nodejs/back-heaven/dev/node_modules/async/lib/async.js:52:16
at /opt/nodejs/back-heaven/dev/node_modules/async/lib/async.js:269:32
at /opt/nodejs/back-heaven/dev/node_modules/async/lib/async.js:44:16
at child.<anonymous> (/opt/nodejs/back-heaven/dev/node_modules/waterline/lib/waterline/utils/schema.js:152:44)
at fn (/opt/nodejs/back-heaven/dev/node_modules/waterline/lib/waterline/utils/callbacksRunner.js:41:10)
at /opt/nodejs/back-heaven/dev/node_modules/async/lib/async.js:181:20
at iterate (/opt/nodejs/back-heaven/dev/node_modules/async/lib/async.js:262:13)
at Object.async.forEachOfSeries.async.eachOfSeries (/opt/nodejs/back-heaven/dev/node_modules/async/lib/async.js:281:9)
at Object.async.forEachSeries.async.eachSeries (/opt/nodejs/back-heaven/dev/node_modules/async/lib/async.js:214:22)
at Object.runner.beforeCreate (/opt/nodejs/back-heaven/dev/node_modules/waterline/lib/waterline/utils/callbacksRunner.js:44:9)
at /opt/nodejs/back-heaven/dev/node_modules/waterline/lib/waterline/query/dql/create.js:180:17
at /opt/nodejs/back-heaven/dev/node_modules/async/lib/async.js:718:13
at iterate (/opt/nodejs/back-heaven/dev/node_modules/async/lib/async.js:262:13)
at /opt/nodejs/back-heaven/dev/node_modules/async/lib/async.js:274:29
at /opt/nodejs/back-heaven/dev/node_modules/async/lib/async.js:44:16
at /opt/nodejs/back-heaven/dev/node_modules/async/lib/async.js:723:17
at /opt/nodejs/back-heaven/dev/node_modules/async/lib/async.js:167:37
when i tried the same code in a controller it succeded am trying it in a seeder of my own implementation this seeder before it starts i make a sails object programmatically and call sails.load
like this :
let myApp = new Sails();
myApp.load({}, (err) => {
if (err) throw err;
// this execute the seeds in the database seeder class
let seeder = require(`../../api/seeders/${scope.args[0]}`);
seeder.handle(() => {
myApp.lower((err) => {
if (err) throw err;
console.log(`Seeder ` + scope.args[0] + ` finished seeding`);
});
});
});
I also tried with sails.lift() and still the same error.
I have Found out the problem i was just making a callback function that should call sails.lower() which unload or close the sails I was putting it in the wrong place where it was called even before the model creation code starts.just forget to call it in the database callback function.
so , for anyone else who is facing this same problem in sails your problem is that sails actually not loaded or maybe when the operation you were working on started sails was working but, after that for some reason sails stopped which made the models do this weird looking problem I hope sails handle such errors in their code to show out a more expressive error messages.
I hope this helps anyone.
I've been learning Node.JS as of recently, and I'm currently using Sequelize.
I have a problem with the update method; it updates just fine, but when I input values that should be incompatible with an attribute's datatype, it still passes it, and updates it in the database.
For exemple: in Postman, when I try to update a record's "completed" attribute with a string, it gets updated even though the datatype was specified as a Boolean, and delivers no error message(request status is 200).
Here is the code:
todo model:
module.exports = function (sequelInst, DataTypes){
return sequelInst.define('todo', {
description: {
type: DataTypes.STRING,
allowNull: false,
validate: {
len: [1, 250]
}
},
completed: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false
}
});
};
server.js:
...
app.put('/todos/:id', function(req,res){
var body =_.pick(req.body, 'description', 'completed');
var attributes = {};
var paramId = parseInt(req.params.id, 10);
if( body.hasOwnProperty('completed')){
attributes.completed = body.completed;
}
if( body.hasOwnProperty('description')) {
attributes.description = body.description;
}
db.todo.findById(paramId)
.then(function(todo){ // First Promise Chain
if(todo){
return todo.update(attributes);
}
else{
res.status(404).send("No todo corresponding to id");
}
}, function () {
res.status(500).send("Server Error");
})
.then(function(todo) { // Second Promise Chain
res.send(todo);
}, function (e){
res.status(400).json(e);
});
});
Instance.update does not validate based on type.
Since you are not getting an error, your probably using SQLite or another storage which does not have strict validation on types at the database level either.
You need to add your own validator. If you do it like this:
completed: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
validate: {
isBoolean: true
}
}
You will get the following error:
Unhandled rejection SequelizeValidationError: Validation error: Validation isBoolean failed
However it looks like this validation is deprecated:
validator *deprecated* you tried to validate a boolean but this library (validator.js) validates strings only. Please update your code as this will be an error soon. node_modules/sequelize/lib/instance-validator.js:276:33
This will work:
var _ = require('lodash');
validate: {
isBoolean: function (val) {
if (!_.isBoolean(val)) {
throw new Error('Not boolean.');
}
}
}
Will give you an error:
Unhandled rejection SequelizeValidationError: Validation error: Not boolean.
I am having issues trying to get the 'runValidators' option to work. My user schema has an email field that has required set to true but each time a new user gets added to the database (using the 'upsert' option) and the email field is empty it does not complain:
var userSchema = new mongoose.Schema({
facebookId: {type: Number, required: true},
activated: {type: Boolean, required: true, default: false},
email: {type: String, required: true}
});
findOneAndUpdate code:
model.user.user.findOneAndUpdate(
{facebookId: request.params.facebookId},
{
$setOnInsert: {
facebookId: request.params.facebookId,
email: request.payload.email,
}
},
{upsert: true,
new: true,
runValidators: true,
setDefaultsOnInsert: true
}, function (err, user) {
if (err) {
console.log(err);
return reply(boom.badRequest(authError));
}
return reply(user);
});
I have no idea what I am doing wrong, I just followed the docs: http://mongoosejs.com/docs/validation.html
In the docs is says the following:
Note that in mongoose 4.x, update validators only run on $set and $unset operations. For instance, the below update will succeed, regardless of the value of number.
I replaced the $setOnInsert with $set but had the same result.
required validators only fail when you try to explicitly $unset the key.
This makes no sense to me but it's what the docs say.
use this plugin:
mongoose-unique-validator
When using methods like findOneAndUpdate you will need to pass this configuration object:
{ runValidators: true, context: 'query' }
ie.
User.findOneAndUpdate(
{ email: 'old-email#example.com' },
{ email: 'new-email#example.com' },
{ runValidators: true, context: 'query' },
function(err) {
// ...
}
In mongoose do same thing in two step.
Find the result using findOne() method.
Add fields and save document using Model.save().
This will update your document.
I fixed the issue by adding a pre hook for findOneAndUpdate():
ExampleSchema.pre('findOneAndUpdate', function (next) {
this.options.runValidators = true
next()
})
Then when I am using findOneAndUpdate the validation is working.
I created a plugin to validate required model properties before doing update operations in mongoose.
Plugin code here
var mongoose = require('mongoose');
var _ = require('lodash');
var s = require('underscore.string');
function validateExtra(schema, options){
schema.methods.validateRequired = function(){
var deferred = Promise.defer();
var self = this;
try {
_.forEach(this.schema.paths, function (val, key) {
if (val.isRequired && _.isUndefined(self[key])) {
throw new Error(s.humanize(key) + ' is not set and is required');
}
});
deferred.resolve();
} catch (err){
deferred.reject(err);
}
return deferred.promise;
}
}
module.exports = validateExtra;
Must be called explicitly as a method from the model, so I recommend chaining it a .then chain prior to the update call.
Plugin in use here
fuelOrderModel(postVars.fuelOrder).validateRequired()
.then(function(){
return fuelOrderModel.findOneAndUpdate({_id: postVars.fuelOrder.fuelOrderId},
postVars.fuelOrder, {runValidators: true, upsert: true,
setDefaultsOnInsert: true, new: true})
.then(function(doc) {
res.json({fuelOrderId: postVars.fuelOrder.fuelOrderId});
});
}, function(err){
global.saveError(err, 'server', req.user);
res.status(500).json(err);
});
If you want to validate with findOneAndUpdate you can not get current document but you can get this keywords's contents and in this keywords's content have "op" property so solution is this :
Note : does not matter if you use context or not. Also, don't forget to send data include both "price" and "priceDiscount" in findOneAndUpdate body.
validate: {
validator: function (value) {
if (this.op === 'findOneAndUpdate') {
console.log(this.getUpdate().$set.priceDiscount);
console.log(this.getUpdate().$set.price);
return (
this.getUpdate().$set.priceDiscount < this.getUpdate().$set.price
);
}
return value < this.price;
},
message: 'Discount price ({VALUE}) should be below regular price',
}
The reason behind this behavior is that mongoose assumes you are just going to update the document, not insert one. The only possibility of having an invalid model with upsert is therefore to perform an $unset. In other words, findOneAndUpdate would be appropriate for a PATCH endpoint.
If you want to validate the model on insert, and be able to perform a update on this endpoint too (it would be a PUT endpoint) you should use replaceOne
I am trying to get by code the next sequence number but it always says "undefined".
I did this in my mongoDB before:
db.PresentationCollection.insert(
{
_id: "editorID",
seq: 0
}
)
my code (name is editorID):
function getNextSequence(name, db) {
var collection = db.get('PresentationCollection');
var ret = collection.findAndModify(
{
query: { _id: name },
update: { $inc: { seq: 1 } },
new: true
}
);
return ret.seq;
}
You're missing the callback. Callback-based asynchronous functions generally do not return anything meaningful. See the documentation for findAndModify in the node binding's readme.
I had the same problem from following this link and it is indeed the callback not being specified and your code not waiting for the returned result - mongo db documents create auto increment
Here is what I did to solve it. Keep in mind I am using Q for promise helping but you could use straight up javascript promises.
function _getNextSequence(name) {
var deferred = Q.defer();
db.counters.findAndModify(
{ _id: name }, //query
[], //sort
{ $inc: { seq: 1 } }, //update
{ new:true }, //options
function(err, doc) { //callback
if (err) deferred.reject(err.name + ': ' + err.message);
if (doc){
deferred.resolve(doc.value.seq);
}
});
return deferred.promise;
}