How do I map one to many relationships as a batch operation using Mongoose?
I have controllers for building seed data for accounts and users.
This data is sourced from a spreadsheet. I then map ONE account to MANY users with another controller, this mapping is bi-direction, i.e Account has array of userIds and user has single accountId.
The code listed, below all works correctly, but it took me 8 hours to write and was quite painful to debug with all the Async and Callback issues that I had to work with.
I researched into bulk operations, native mongo drive stuff etc... and this was the best I could come up with.
I feel that this code could use faster and/or better techniques but I'm not sure how to improve it.
I have included some screenshots or code for
test data in Excel and JSON
SPEC that fires account, user and relationship generation
relationship builder controller with debug code left in place.
screenshot of the output
I'm hoping for feedback.
Seed Data SPEC
describe('Relationship', function () {
beforeEach(td.accounts.teardown);
beforeEach(td.users.teardown);
beforeEach(td.users.seedData);
beforeEach(td.users.setAuthenticatedTokenAdmin);
it('should create relationships between the user and accounts table', function (done) {
// Build Account Data
request(app).put('/api/seeds/accountCreateSeed').set('Authorization', 'Bearer ' + td.users.authenticatedToken).send({ logDetail: 0, useSampleJson: true })
.end(function (err, res) {
if (err) {
l.logTheUnknown(err);
return done(err);
}
// Build User Data
request(app).put('/api/seeds/userCreateSeed').set('Authorization', 'Bearer ' + td.users.authenticatedToken).send({ logDetail: 0, useSampleJson: true })
.end(function (err, res) {
if (err) {
l.logTheUnknown(err);
return done(err);
}
// Build User /Account Relationship Data
request(app)
.put('/api/seed-relationships/createUserAccountRelationship')
.set('Authorization', 'Bearer ' + td.users.authenticatedToken)
.send({ logDetail: 0, useSampleJson: true })
.end(function (err, res) {
if (err) {
l.logTheUnknown(err);
return done(err);
}
return done(err);
});
});
});
});
});
Seed Data Relationship Controller
// ----------------------------------------------------------------------
// User / Account - Relationships
// ----------------------------------------------------------------------
exports.createUserAccountRelationship = function (req, res) {
var useSampleJson = req.body.useSampleJson ? req.body.useSampleJson : false;
// userAccounts = the input data to be processed
var userAccounts = useSampleJson ? readFile('accountUserRelationship.json') : req.body.data;
var logDetail = req.body.logDetail ? req.body.logDetail : 0;
if (logDetail >= LOG_DETAIL) {
l.kv('useSampleJson', useSampleJson);
l.kv('logDetail', logDetail);
}
if (logDetail >= LOG_LOW) {
l.block('Relationship Data for User/Account');
}
if (logDetail >= LOG_LOW) {
l.line('User / Account Relationships JSON');
if (userAccounts) {
l.kv('Length', userAccounts.length);
l.inspect(userAccounts);
}
}
async.eachSeries(userAccounts,
function (userAccount, callback) {
l.kv('Search Account', userAccount.accountName);
Account.findOne({ name: userAccount.accountName }, function (err, account) {
if (err) {
l.inspect(err);
} else {
if (account) {
findAccountUsers(userAccount, account, callback);
} else {
l.kv('Could not find Account', userAccount.accountName);
callback();
}
}
});
},
function (err) {
if (err) {
l.inspect(err);
} else {
l.block('User Account Relations Processed ');
}
return res.sendStatus(200);
});
};
function findAccountUsers(userAccount, account, callback) {
// userAccount = A single relationship between account and multipleUsers
// account = the DB account to work with
// Find all the users that are associated with this account
l.inspect(userAccount.userEmail);
User.find({ email: { $in: userAccount.userEmail } }, function (err, users) {
l.inspect(_.map(users, function (user) { return user.id + ' - ' + user.email; }));
assignAccountUserRelationship(account, users, callback);
});
}
function assignAccountUserRelationship(account, users, callback) {
// account = the DB account to work with
// users = the DB users to work with
account.userIds = [];
for (var userIndex = 0; userIndex < users.length; userIndex++) {
var user = users[userIndex];
user.accountId = account._id;
account.userIds.push(user._id);
}
account.save(function (err) {
if (err) {
l.inspect(err);
} else {
l.kv('Account Saved', account.name);
}
async.eachSeries(users,
function (user, userCallback) {
user.save(function (err) {
if (err) {
l.inspect(err);
} else {
l.kv('user saved', user.email);
}
userCallback();
});
},
function (err) {
if (err) {
l.inspect(err);
} else {
l.block('User Relation Processed ');
}
callback();
});
});
}
Test Data
Test Data Relationship as JSON
Sample Log Output
Sample Output from Mongo Shell
Related
No matter what I change the user login will keep redirecting to failure instead of success. I don't know if I'm missing something or if I did something wrong. I tried to read the documentation for passport but, I found it pretty confusing. Here is my github link if you need to see the rest of the code. The node files are in app.js and passport-config.js.The sign up part of the website is working. https://github.com/gego144/to-do-list-website/tree/main
const customFields = {
usernameField: 'email',
passwordField: 'password'
}
const verifyCallback = (username, password, done) => {
user_exists = userName_Checker(username), function (err, user) {
if (err) { return done(err); }
if (userName_Checker(username) == false) {
console.log('wrong user');
return done(null, false, { message: 'Incorrect username.' });
}
if (password_finder(username, password)) {
console.log('wrong pass');
return done(null, false, { message: 'Incorrect password.' });
}
console.log('wtf');
return done(null, user);
};
;
}
const strategy = new LocalStrategy(customFields, verifyCallback);
passport.use(strategy);
passport.serializeUser(function(user, done) {
done(null, user);
});
passport.deserializeUser(function(id, done) {
User.findById(id, function(err, user) {
done(err, user);
});
});
// function that checks to see if the users email is in the database
function userName_Checker(email_name){
var sql = "select * from info where email = ?";
var user_email = [[email_name]];
db.query(sql, [user_email],function (err,result){
if (err) throw err;
var not_unique = result.length;
if(not_unique == 0){
return false;
}
else{
return true;
}
}
)}
// function that checks to see if the password in the database matches with the email
function password_finder(email_name, pass){
var sql = "SELECT password FROM info WHERE email = ?";
var user_email = [[email_name]];
db.query(sql, [user_email],function (err,result){
if (err) throw err;
bcrypt.compare(result, pass, function(err, res){
if(err){ throw err};
if(res){
return true;
}
else{
return false;
}
})
}
)}
My post method in my other file.
app.post('/login', passport.authenticate('local', {
successRedirect: '/',
failureRedirect:'/index.html',
failureFlash: true
}))
Edit 1.
I just want to mention that the console.logs you see in verify Callback all don't log anything for some reason too.
The problem might be in the serialization logic.
In passport.serializeUser, you are passing in the whole user object, but when deserializing you are passing the id
Though I am not using SQL, the logic should be similar.
So the code should be something like this:
// Session
// Pass in user id => keep the session data small
passport.serializeUser((id, done) => {
done(null, id);
});
// Deserialize when needed by querying the DB for full user details
passport.deserializeUser(async (id, done) => {
try {
const user = await User_DB.findById(id);
done(null, user);
} catch (err) {
console.error(`Error Deserializing User: ${id}: ${err}`);
}
});
// Export the passport module
module.exports = (passport) => {
passport.use(new LocalStrategy({ usernameField: 'email', }, async (email, password, done) => {
try {
// Lookup the user
const userData = await User_DB.findOne({ email: email, }, {
password: 1, }); // Return the password hash only instead of the whole user object
// If the user does not exist
if (!userData) {
return done(null, false);
}
// Hash the password and compare it to the hash in the database
const passMatch = await bcrypt.compare(password, userData.password);
// If the password hash does not match
if (!passMatch) {
return done(null, false);
}
// Otherwise return the user id
return done(null, userData.id);
} catch (err) {
passLog.error(`Login Error: ${err}`);
}
}));
};
These options for passport seems to malfunction a lot or exhibit weird behaviors, so I suggest you handle the redirection logic like in my controller.
{ successRedirect: '/good',
failureRedirect: '/bad' }
Login controller logic:
(I am omitting the code here for session storage and made some modifications, but this code should work for what you need)
const login = (req, res, next) => {
//Using passport-local
passport.authenticate('local', async (err, user) => {
//If user object does not exist => login failed
if (!user) { return res.redirect('/unauthorized'); }
//If all good, log the dude in
req.logIn(user, (err) => {
if (err) { return res.status(401).json({ msg: 'Login Error', }); }
// Send response to the frontend
return res.redirect('/good');
});
});
})(req, res, next);
};
The actual route:
// Import the controller
const {login} = require('../controllers/auth');
// Use it in the route
router.post('/auth/login', login);
Im using express-validator to check if the req.body entered is valid and to check if there is duplicate email in the MySQL database
Here is my code:
router.post(
"/signup",
[
body("uemail","email is not valid")
.isEmail()
.normalizeEmail()
.custom(async (email, {req} )=>{
const queryString = "SELECT uid FROM EarlyUsers WHERE `uemail` = ?";
return await connection.query(queryString, [email], (err, rows, fields) => {
if (err) {
console.log(err)
}else {
if (rows.length != 0) {
return false
} else {
return true
}
}
});
})
,
body("uname").isLength({ min: 5 })
],
authControllers.signUp
);
I dont know why this custom validator does not work.
I've tried to throw new Error instead of return false, but it just crash the whole thing . I really need help with this
For it to work correctly instead of returning false you reject the Promise.
if (rows.length != 0) {
return Promise.reject("user already exists.");
}
I have achieved this way it might be helpful for others, I'm using sequelize :)
const User = require("../../models/User");
body('email', 'Invalid email').exists().isEmail().trim().escape().custom(userEmail=> {
return new Promise((resolve, reject) => {
User.findOne({ where: { email: userEmail } })
.then(emailExist => {
if(emailExist !== null){
reject(new Error('Email already exists.'))
}else{
resolve(true)
}
})
})
}),
I found this solution to check that the email is not duplicate:
router.post('/register',
body('email').isEmail().normalizeEmail().withMessage('The email format is not correct.').custom((email) => {
const queryString = `SELECT * FROM users WHERE user_email = "${email}"`;
return getFinalEmail(queryString).then(user => {
console.log(user);
if (user) {
return Promise.reject('E-mail already in use');
}
});
}),
// -- other validations
// .....
(req, res) => {
/* your code for this route */
}); // end of router('/register')
function getFinalEmail(param) {
return new Promise(function(resolve, reject) {
getEmailData(param, function(result) {
console.log(result);
resolve(result);
});
});
}
function getEmailData(query, callback) {
database.query(query, function(error, data){
if(data.length > 0) {
return callback(true);
} else {
return callback(false);
}
});
}
In the above code users is the name of my table and user_email is the column that email data of users are stored.
When I have tried to implement Google authentication in my site, using sails JavaScript, and MySQL getting error. I have using passport and passport-Google-auth Strategy. Problem is not getting data to my site from Google
My Express Config(express.js) file is like below,
var passport = require('passport')
, GoogleStrategy = require('passport-google-oauth').OAuth2Strategy;
var verifyHandler = function(token, tokenSecret, profile, done) {
process.nextTick(function() {
console.log(profile)
User.findOne({uid: profile.id}, function(err, user) {
if (user) {
return done(null, user);
} else {
var data = {
provider: profile.provider,
uid: profile.id,
name: profile.displayName
};
if (profile.emails && profile.emails[0] && profile.emails[0].value) {
data.email = profile.emails[0].value;
}
if (profile.name && profile.name.givenName) {
data.firstname = profile.name.givenName;
}
if (profile.name && profile.name.familyName) {
data.lastname = profile.name.familyName;
}
User.create(data, function(err, user) {
return done(err, user);
});
}
});
});
};
passport.serializeUser(function(user, done) {
console.log(user)
done(null, user.uid);
});
passport.deserializeUser(function(uid, done) {
User.findOne({uid: uid}, function(err, user) {
done(err, user);
});
});
module.exports.http = {
customMiddleware: function(app) {
passport.use(new GoogleStrategy({
clientID: 'Client Id here',
clientSecret: 'Secret key here',
callbackURL: 'http://localhost:1337/auth/google/callback'
}, verifyHandler));
app.use(passport.initialize());
app.use(passport.session());
}
};
module.exports.cache = {
// The number of seconds to cache files being served from disk
// (only works in production mode)
maxAge: 31557600000
};
module.exports.userlogin = {
userModel: 'user'
};
And My Auth Controller I have added code like below,
google: function(req, res) {
passport.authenticate('google',{
failureRedirect: '/login', scope: ['profile', 'email']
}, function(err, user) {
req.logIn(user, function(err) {
if (err) {
console.log(err);
res.view('500');
return;
}
res.redirect('/');
return;
});
})(req, res);
},
You didn't post your code, so we can't find the exact problem :/
I usually use this method for google/facebook authentication with sails.js.
I follow at first this documentation to add the authentication buttons in the frontend:
https://developers.google.com/identity/sign-in/web/sign-in
Then I post the token that I got from google/facebook to the backend where I can check if the user is banned or whatever... If everything is correct, I create an account for him in the database, I send him his password to his email and finally authenticate him using sessions
(req.session.userId = createdUser.id)
In the next time the user can log in using his email and password or just using google. And both options lead him to the same account :D
My Sails.js function in the authentication controller:
googleAuth: function(req, res) {
if (_.isUndefined(req.param('googleToken'))) {
return res.json({
success: false,
msg: 'Error! Please post your google token'
});
}
var urlToRq = "https://www.googleapis.com/oauth2/v3/tokeninfo?id_token=" + req.param('googleToken');
// Get information about the google user with the specified access token.
request.get({url: urlToRq}, function(err, response, body) {
if(err) {
return res.json({
success: false,
msg: 'Server Error'
});
}
var receivedData = JSON.parse(body);
var userId = receivedData.sub;
var userEmail = receivedData.email;
var emailVerified = receivedData.email_verified;
var userName = receivedData.name;
var userPicture = receivedData.picture;
if (emailVerified == false) {
return res.json({
success: false,
msg: 'Your email is not verified'
});
}
else {
// AUTHENTICATION VERIFIED, YOU CAN SAVE THE CONNECTED USER IN A SESSION, OR ADD HIM TO THE DATABASE AS A NEW ACCOUNT, OR CHECK IF HE HAS A PREVIOUS ACCOUNT OR WHATEVER YOU WANT...
}
});
},
Of course don't forget to run npm install request --save
If anyone needs the facebookAuth function just tell me :D I will post it for you :)
Im using expressjs and mysql package.
I have a user table, each user could have many skills, projects, certifications stored in different tables. How can I get all of these data into a single user object.
getUserDataById(req.params.id, function (user) {
if (user) {
res.send(user);
}
}
var getUserDataById = function (id, callback) {
connection.query(userSql, id, function (err, rows) {
if (rows[0]) {
user = new User(rows[0]); //parse row data to user obj
// The main problem
//get skills and projects.... and asign to user obj
parseArrayData(skillSql, user.id, Skill, function (skills) {
user.skills = skills;
}
);
parseArrayData(projectSql, user.id, Project, function (project) {
user.projects = projects;
}
);
// is here
callback(user);
}
});
}
var parseArrayData = function (query, id, Obj, callback) {
connection.pool.query(query, id, function (err, rows) {
if (err) {
throw err;
} else {
console.log('rows',rows);
var array = [];
for (var i = 0; i < rows.length; i++) {
array[i] = new Obj(rows[i]); // map obj's attributes with fields
};
callback(rows);
}
});
};
You might have these different tables with names (assume): users, skills, projects, certifications;
function to get result from db for query;
function executeQuery(tablename, userId, callback) {
var sql = 'select * from ' + tablename + ' where id = ' + userId;
connection.query(sql, function (err, rows) {
if (err) { callback(err, null); }
else { callback(null, rows); }
})
}
Function that will fetch all results for user id and return json object;
var getUserDataById = function (userId, callback) {
executeQuery('users', userId, function(users) {
executeQuery('projects', userId, function(projects) {
executeQuery('skills', userId, function(skills) {
executeQuery('certifications', userId, function(certifications) {
var result = { // format you result.
id: userId,
users: users,
projects: projects,
skills: skills,
certifications: certifications
};
callback(result);
});
});
});
});
callback(null);
});
Usage:
getUserDataById(req.params.id, function (user) {
if (user) {
res.send(user);
}
}
first, i connect the db and select DB:
var defaultOptions = {
user: "root",
pwd:'admin',
db:"britcham_dev_local",
server:"local", // Maybe we don't need this variable.
};
var client = new Client();
client.user = defaultOptions.user;
client.password = defaultOptions.pwd;
client.connect(function (error, results) {
//
});
client.query('USE ' + defaultOptions.db, function (error, results) {
//
});
Second, I query with client object:
var self = this;
var this.users;
client.query("SELECT * FROM users", function (error, results, fields) {
if (error) {
//
}
if (results.length > 0) {
self.users = results;
}
});
console.log(this.users);
it's nothing output ??? Why ??
Since node.js is non-blocking and asynchronous, then in this code:
client.query("SELECT * FROM users", function (error, results, fields) {
if (error) {
//
}
if (results.length > 0) {
self.users = results;
}
});
console.log(this.users);
data from DB are not probably loaded yet into users variable when you are trying to log it into console. You can check it out if you do your console.log operation within the query, for example:
client.query("SELECT * FROM users", function (error, results, fields) {
if (error) {
//
}
if (results.length > 0) {
console.log(results);
}
});
To pass the result into a variable when the operation is finished you can wrap your client DB call into a function with callback parameter and set your variable when the callback is invoked, for example:
function query(sql, callback) {
client.query(sql, function (error, results, fields) {
if (error) {
//
}
if (results.length > 0) {
callback(results);
}
});
}
query("SELECT * FROM users", function(results) {
self.users = results;
console.log(self.users);
});
Above code is just a concept.
How is the suggested answer different from this?
var self = this;
var this.users;
client.query("SELECT * FROM users", function (error, results, fields) {
if (error) {
//
}
if (results.length > 0) {
self.users = results;
console.log(this.users);
}
});
I might be wrong this is not different from the suggested answer in that it writes to console no sooner than when we have the data back from the DB.
The suggested answer seems only to add yet another function?