Sailsjs/Passport add fields to User - node.js

Sails noob here. I have sailsjs (v0.12.3) running with passport and everything is currently working. The User model is located in /node_modules/sails-auth/api/models/User.js and has username, email, and passports fields. I want to add additional fields (like bday ->string, favorite_car-> string) to the users table, but can't find a good way to do this. Editing the existing User.js file to add the new attributes would probably work, but seems like a bad idea. I've tried creating a User.js file (in myproj/api/models/) but i get an error stating:
Error: on mapping custom foreign keys: role->user. Attribute: roles does not exist on: collection: user
Any insight into the best way to do this would be greatly appreciated.
Thanks
added:
I've tried many different iterations from a empty User.js file (in myproj/api/models/) to the entire code that come with sails-auth (in myproj/node_modules/sails-auth/dist/api/models/User.js). They all produce the same error. Here is code that tried from the node_modules' User.js:
'use strict';
var _ = require('lodash');
var crypto = require('crypto');
/** #module User */
module.exports = {
attributes: {
username: {
type: 'string',
unique: true,
index: true,
notNull: true
},
email: {
type: 'email',
unique: true,
index: true
},
passports: {
collection: 'Passport',
via: 'user'
},
getGravatarUrl: function getGravatarUrl() {
var md5 = crypto.createHash('md5');
md5.update(this.email || '');
return 'https://gravatar.com/avatar/' + md5.digest('hex');
},
toJSON: function toJSON() {
var user = this.toObject();
delete user.password;
user.gravatarUrl = this.getGravatarUrl();
return user;
}
},
beforeCreate: function beforeCreate(user, next) {
if (_.isEmpty(user.username)) {
user.username = user.email;
}
next();
},
/**
* Register a new User with a passport
*/
register: function register(user) {
return new Promise(function (resolve, reject) {
sails.services.passport.protocols.local.createUser(user, function (error, created) {
if (error) return reject(error);
resolve(created);
});
});
}
};

Related

Mongoose.js Unique Validation

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

MongoDB and Nodejs insert ID with auto increment

I am new to NodeJs and MongoDB, i want to insert row with auto increment primary key 'id'. also defined a function called getNextSequence on mongo server.
this is working perfect on Mongodb server
> db.user.insert({
"id" : getNextSequence('user_id'),
"username" : "test",
"email" : "test#test.com",
"password" : "test123"
})
now i want to insert from NodeJs.I have tried this but not working
db.collection('user').insertOne({
id : "getNextSequence('user_id')",
username : query.name,
email: query.email,
password: query.pass
}, function(err, result) {
assert.equal(err, null);
console.log("row insterted ");
callback();
});
Assuming that getNextSequence is a server-script function (i.e. a method you defined and saved via db.system.js.save), it is not callable outside of the server. One way to go is to use eval, which forces the server to evaluate a string as a js code, even though it is not a good practice. Here is an example:
db.eval('getNextSequence(\'user_id\')', function(err, result) {
db.collection('users').insert({
"id" : result,
"username" : "test",
"email" : "test#test.com",
"password" : "test123"
});
});
Another way is to follow the mongo tutorial and to implement the getNextSequence directly in NodeJS. The syntax is pretty much the same:
function getNextSequence(db, name, callback) {
db.collection("counters").findAndModify( { _id: name }, null, { $inc: { seq: 1 } }, function(err, result){
if(err) callback(err, result);
callback(err, result.value.seq);
} );
}
You then use it in your nodeJS code like:
getNextSequence(db, "user_id", function(err, result){
if(!err){
db.collection('users').insert({
"_id": result,
// ...
});
}
});
Note: of course, you need to have set the counters collection as explained in the docs.
You can also use "mongoose-auto-increment".
The code has just 4 lines
var mongoose = require('mongoose');
var autoIncrement = require('mongoose-auto-increment');
autoIncrement.initialize(mongoose.connection);
userSchema.plugin(autoIncrement.plugin, 'user');
example :
npm i mongoose-auto-increment
connections.js :
const mongoose = require('mongoose');
require("dotenv").config;
const uri = process.env.MONGOURL;
mongoose.connect(uri, { useNewUrlParser: true }, (err) => {
if (!err) { console.log('MongoDB Connection Succeeded.') }
else { console.log('Error in DB connection : ' + err) }
});
require('../schema/userSchema');
userSchema.js :
var mongoose = require('mongoose'); // 1. require mongoose
var autoIncrement = require('mongoose-auto-increment'); // 2. require mongoose-auto-increment
var userSchema = new mongoose.Schema({
name: { type: String },
password: { type: String },
email: { type: String, unique: true, required: 'This field is required.' },
});
autoIncrement.initialize(mongoose.connection); // 3. initialize autoIncrement
userSchema.plugin(autoIncrement.plugin, 'user'); // 4. use autoIncrement
mongoose.model('user', userSchema);
To accomplish this, we will create a function that will keep trying to save the document untill it will have been saved with incremented _id
async function retryUntilSave(db, task) {
try {
const index = await db.collection('tasks').find().count() + 1;
const result = await db.collection('tasks').insertOne(Object.assign(task, { _id: index }))
} catch (error) {
if (error.message.includes("_id_ dup key")) {
console.log("ID already exists!")
console.log("Retrying...");
retryUntilSave(db, task)
} else {
console.log(error.message);
}
}
}
We can use task._id: index instead of Object.assign()
finally you can test this by making some concurrent requests
for (let index = 0; index < 20; index++) {
setTimeout(async () => {
await retryUntilSave(db, { title: "Some Task" })
}, 1000);
}
This function will handle easily if two or more tasks submitted at the same time because mogod throws error when we try to insert a document with duplicate _id, then we will retry saving the document again with incremented _id and this process will run until we save the document successfully !
You can also use "mongodb-autoincrement" module of node js. For example:
var autoIncrement = require("mongodb-autoincrement");
exports.yourMethod = function(newData, callback) {
autoIncrement.getNextSequence(db, your-collection-name, function (err, autoIndex) {
newData.id = autoIndex;
//save your code with this autogenerated id
});
}
You can use the below package on a model schema to auto-increment your collection field.
mongoose-auto-increment //you can download it from npm
Here I am not focusing on how to connect MongoDB. I just focus on how you can integrate auto increment in your model/collection/table.
const mongoose = require("mongoose"); //
const autoIncrement = require("mongoose-auto-increment");
const post_schema = new mongoose.Schema({
title: {
type: String,
required: true,
min: 3,
max: 225,
},
slug: {
type: String,
required: true,
},
});
autoIncrement.initialize(mongoose.connection);
post_schema.plugin(autoIncrement.plugin, {
model: "post", // collection or table name in which you want to apply auto increment
field: "_id", // field of model which you want to auto increment
startAt: 1, // start your auto increment value from 1
incrementBy: 1, // incremented by 1
});
module.exports = mongoose.model("post", post_schema);

Mongoose TypeError: User is not a constructor

I'm trying to add a subdocument to a parent schema with Mongoose and MongoDB however I'm being thrown the following error:
TypeError: User is not a constructor
This is based off Mongoose's documentation on subdocuments and I think everything is the same. How can I debug this further?
Router
// Add a destination to the DB
router.post('/add', function(req, res, next) {
let airport = req.body.destination
let month = req.body.month
let id = (req.user.id)
User.findById(id , function (err, User) {
if (err) return handleError(err)
function addToCart (airport, month, id) {
var user = new User ({
destinations: [(
airport = '',
month = ''
)]
})
dog.destinations[0].airport = airport
dog.destinations[0].month = month
dog.save(callback)
res.status(200).send('added')
}
addToCart()
})
console.log(airport)
})
Schema
var destinationSchema = new Schema({
airport: String,
month: String
})
// Define the scheme
var User = new Schema ({
firstName: {
type: String,
index: true
},
lastName: {
type: String,
index: true
},
email: {
type: String,
index: true
},
homeAirport: {
type: String,
index: true
},
destinations: [destinationSchema]
})
User.plugin(passportLocalMongoose)
module.exports = mongoose.model('User', User)
JavaScript is case sensitive about the variable names. You have User model and the User result with the same name.
Your code will work with the following change :
User.findById(id , function (err, user) {
/* ^ use small `u` */
if (err) return handleError(err)
/* rest of your code */
Also keep in mind that further in your code you are declaring another variable named user. You will need to change that to something different.

Building model "user" to "user_flag" to "user"

I have two models:
User
'use strict';
var Mystical = require('../mystical'),
Blog = require('./blog'),
Flag = require('./user/flag');
module.exports = Mystical.db.Model.extend({
tableName: 'user',
defaults: {
isAdmin: 0,
isConfirmed: 0
},
blogs: function blogs() {
return this.hasMany(Blog, 'userId');
},
flags: function flags() {
return this.hasMany(Flag, 'userId');
}
});
...and Flag:
'use strict';
var Mystical = require('../../mystical'),
User = require('../../model/user');
module.exports = Mystical.db.Model.extend({
tableName: 'user_flag',
user: function user() {
return this.belongsTo(User, 'id');
}
});
On a page I try to get a flag:
(new Flag({ content: req.params.identifier, flag: 'register_complete' })).fetch({ withRelated: ['user'] }).then(onData).otherwise(function(error) {
console.log(error);
});
But every time I call this function the otherwise callback gets triggered:
TypeError: object is not a function
at exports.Relation.RelationBase.extend.relatedInstance (/my/secret/dir/node_modules/bookshelf/dialects/sql/relation.js:217:29)
at exports.Relation.RelationBase.extend.init (/my/secret/dir/node_modules/bookshelf/dialects/sql/relation.js:43:39)
at exports.Model.ModelBase.extend.belongsTo (/my/secret/dir/node_modules/bookshelf/dialects/sql/model.js:37:76)
at user (/my/secret/dir/core/model/user/flag.js:10:21)
at EagerBase.fetch (/my/secret/dir/node_modules/bookshelf/dialects/base/eager.js:56:40)
at /my/secret/dir/node_modules/bookshelf/dialects/sql/model.js:232:60
at NearFulfilledProxy.when (/my/secret/dir/node_modules/bookshelf/node_modules/when/when.js:465:43)
at Object._message (/my/secret/dir/node_modules/bookshelf/node_modules/when/when.js:389:25)
at deliver (/my/secret/dir/node_modules/bookshelf/node_modules/when/when.js:299:7)
at /my/secret/dir/node_modules/bookshelf/node_modules/when/when.js:296:63
For now I have tried to generate my association with hasMany, hasOne, belongsTo as well as belongsToMany. But nothing works...
Anyone an idea?
I have a feeling this might be a circular reference problem:
Try replacing:
return this.hasMany(Flag, 'userId');
with
return this.hasMany(require('./user/flag'), 'userId');
and
return this.belongsTo(User, 'id');
with
return this.belongsTo(require('../../model/user'), 'id');
and see if you still have the same issue.

Setting a virtual field in a Model based on an async query from another model

I want to have a user setting (in a user model) that is derived from the sum of values in another model.
What I have tried to do is create a virtual value using a query like this:
var schemaOptions = {
toObject: {
virtuals: true
}
,toJSON: {
virtuals: true
}
};
/**
* User Schema
*/
var UserSchema = new Schema({
firstname: String,
lastname: String,
email: String,
username: String,
provider: String,
phonenumber: Number,
country: String,
emailverificationcode: {type:String, default:'verifyme'},
phoneverificationcode: {type:Number, default:4321 },
emailverified: {type:Boolean, default:false},
phoneverified: {type:Boolean,default:false},
}, schemaOptions)
UserSchema
.virtual('credits')
.get(function(){
//Load Credits model
var Credit = mongoose.model('Credit');
Credit.aggregate([
{ $group: {
_id: '5274d0e5a84be03f42000002',
currentCredits: { $sum: '$amount'}
}}
], function (err, results) {
if (err) {
return 'N/A'
} else {
return results[0].currentCredits.toString();
//return '40';
}
}
);
})
Now, this gets the value but it fails to work correctly (I cannot retrieve the virtual 'value' credits). I think this is because of the async nature of the call.
Can someone suggest the correct way to achieve this?
Once again many thanks for any input you can provide.
Edit:
So I am trying to follow the suggested way but no luck so far. I cannot get my 'getCredits' method to call.
Here is what I have so far:
UserSchema.method.getCredits = function(cb) {
//Load Credits model
var Credit = mongoose.model('Credit');
Credit.aggregate([
{ $group: {
_id: '5274d0e5a84be03f42000002',
currentCredits: { $sum: '$amount'}
}}
], function (err, results) {
cb(results);
}
);
};
var User = mongoose.model('User');
User.findOne({ _id : req.user._id })
.exec(function (err, tempuser) {
tempuser.getCredits(function(result){
});
})
Any ideas? Thanks again
There are a few issues with your implementation:
UserSchema.method.getCredits
^^^^^^ should be 'methods'
Also, you have to make sure that you add methods (and virtuals/statics) to your schema before you create the model, otherwise they won't be attached to the model.
So this isn't going to work:
var MySchema = new mongoose.Schema(...);
var MyModel = mongoose.model('MyModel', MySchema);
MySchema.methods.myMethod = ... // too late, model already exists
Instead, use this:
var MySchema = new mongoose.Schema(...);
MySchema.methods.myMethod = ...
var MyModel = mongoose.model('MyModel', MySchema);
I would also advise you to always check/propagate errors.

Resources