beforeUpdate doesn't seem to be called - javascript

I have a simple user model as follows :
'use strict';
let hashPassword = (user, options) => {
if (!user.changed('password')) { return; }
return require('bcrypt')
.hash(user.getDataValue('password'), 10)
.then(hash => user.setDataValue('password', hash));
};
module.exports = (sequelize, DataTypes) => {
const User = sequelize.define('User', {
username: {allowNull: false, type: DataTypes.STRING, unique: true},
email: {allowNull: false, type: DataTypes.STRING, unique: true},
password: {allowNull: false, type: DataTypes.STRING, unique: false},
}, {
hooks: {
beforeCreate: hashPassword,
beforeUpdate: hashPassword
}
});
return User;
};
It works very well on user creation, but the beforeUpdate hook doesn't seem to work or be called, and the password is saved in plain text in the database.
Where does it come from and how can it be fixed ?

How are you updating the user? There is a difference between getting an instance of the user and updating it and updating by querying the model. The former is an instance update and the latter a bulk update operation (even if your where filter would return a single item).
This distinction is important because beforeUpdate is an instance hook so it would be triggered on instance updates only. You can either change the way you update the user or implement a beforeBulkUpdate hook as well.

To offer an alternative to Unglückspilz answer. You can also add the option
{ individualHooks: true }
Note: methods like bulkCreate do not emit individual hooks by default - only the bulk hooks. However, if you want individual hooks to be emitted as well, you can pass the { individualHooks: true } option to the query call. However, this can drastically impact performance, depending on the number of records involved (since, among other things, all instances will be loaded into memory).
https://sequelize.org/master/manual/hooks.html#model-hooks

Related

Sequelize BelongsToManyAddAssociationMixin performing update instead of insert in polymorphic table

I'm having a problem within my application that is related to multiple tables/models arranged in a Many:Many relationship but also leverages polymorphic columns and is using the BelongsToManyAddAssociationMixin. I am unable to insert multiple records in a M:N relationship because I'm unable to tell Sequelize to change the unique fields to include roleableId
Essentially I have a User model, a Role model, a relationship table called UserRole and other models which are "roleable" (For the sake of this example, one model is a Facility)
So essentially my UserRole model looks like
export class UserRole extends Model {
public readonly id!: UUID
public RoleId!: UUID
public UserId!: UUID
public roleableType!: string
public roleableId!: string
public readonly User!: User
public readonly Role!: Role
public getUser!: BelongsToGetAssociationMixin<User>
public getRole!: BelongsToGetAssociationMixin<Role>
}
export const initializeUserRole = (sequelize: Sequelize) => {
UserRole.init(
{
RoleId: {
type: DataTypes.UUID,
allowNull: false,
validate: {
isUUID: 4,
},
field: 'role_id',
},
UserId: {
type: DataTypes.UUID,
allowNull: false,
validate: {
isUUID: 4,
},
field: 'user_id',
},
roleableId: {
type: DataTypes.UUID,
field: 'roleable_id',
validate: {
isUUID: 4,
},
},
roleableType: {
type: DataTypes.STRING,
field: 'roleable_type',
},
},
{
timestamps: false,
sequelize,
tableName: TableNames.USER_ROLES,
}
)
}
In my scenario, I would have a role named Sales Rep and it's primary key is a UUID
In theory, I would like to create a new record for the user to have the SalesRep role twice in my UserRole table, one with values for roleableId & roleableType and one where those values are null.
Using mixins, I'm able to add these roles with something like
const randomUser = await userFactory()
const salesRepRole = await roleFactory({ roleName: 'Sales Rep' })
await randomUser.addRole(salesRepRole)
^^^ This works properly
But if I try to do something like:
const randomUser = await userFactory()
const randomFacility = await facilityFactory()
const salesRepRole = await roleFactory({ roleName: 'Sales Rep' })
await randomUser.addRole(salesRepRole)
await randomuser.addRole(salesRepRole, { through: { roleableId: randomFacility.id, roleableType: 'facility' }})
What will happen is that instead of performing a new insert into my UserRole table, instead Sequelize will identify that there is already a record based upon User.id & Role.id and will perform an UPDATE instead, resulting in only one record being returned.
I'm not sure how to indicate to Sequelize that instead of putting a unique constraint on User.id & Role.id, I really need it to be User.id, Role.id & roleableId
My work around right now has been to simply just roll my own add<Model> function which will perform an INSERT properly (adding an index on my DB to check for unique instances of user_id,role_id,roleable that throw an error if this combination already exists) but this feels a bit dirty and I'd rather try to utilize the mixin as much as possible, especially if I also have to add a BelongsToManyAddAssociationsMixin version as well for addRoles
Does anyone have any ideas? I'm happy to elaborate more if that helps.
Thanks!

Mongoose and Next.js: Unhandled Runtime Error TypeError: Cannot read properties of undefined (reading 'Token')

I basically defined this Model, much like another which doesn't error out; So I am stumped as to why it's not working...
Here is a Minimal, Reproducible Example
Not working:
import mongoose from 'mongoose';
const TokenSchema = new mongoose.Schema({
_userId: { type: mongoose.Schema.Types.ObjectId, required: true, ref: 'User' },
token: { type: String, required: true },
createdAt: { type: Date, required: true, default: Date.now, expires: 43200 }
});
export default mongoose.models.Token || mongoose.model('Token', TokenSchema);
Working:
import mongoose from 'mongoose';
import emailValidator from 'email-validator'
import bcrypt from 'bcrypt'
import crypto from 'crypto'
const SALT_ROUNDS = 12;
const UserSchema = new mongoose.Schema(
{
username: {
type: String,
required: true,
trim: true,
lowercase: true,
index: { unique: true },
validate: {
validator: emailValidator.validate,
message: props => `${props.value} is not a valid email address!`
}
},
password: {
type: String,
required: true,
trim: true,
index: { unique: true },
minlength: 7,
maxlength: 11
},
roles: [{ type: 'String' }],
isVerified: { type: Boolean, default: false },
passwordResetToken: String,
resetPasswordExpires: Date
},
{
timestamps: true
}
);
UserSchema.pre('save', async function preSave(next) {
const user = this;
if (!user.isModified('password')) return next();
try {
const hash = await bcrypt.hash(user.password, SALT_ROUNDS);
user.password = hash;
return next();
} catch (err) {
return next(err);
}
});
UserSchema.methods.generatePasswordReset = function () {
this.resetPasswordToken = crypto
.randomBytes(20)
.toString('hex');
this.resetPasswordExpires = Date.now() + 3600000; // expires in an hour
};
UserSchema.methods.comparePassword = async function comparePassword(candidate) {
return bcrypt.compare(candidate, this.password);
};
export default mongoose.models.User || mongoose.model('User', UserSchema)
Also I'm following this example in the Next.js Examples repo.
Please help! :)
Apparently the TypeError: Cannot read properties of undefined (reading 'Token') is happening because the code is executing on the client side when it is intended to execute on the server side.
What is really happening is that mongoose has not initiated a connection to the database, and so mongoose.models is undefined. But the fix isn't to try to initiate a db connection:
I too was having the a similar issue, when I tried to define too many things in the same file, that is... In my case I'm trying to define Typescript interfaces that can be pulled into client-side code, but the mongoose model definitions end up executing as well...
I read that NextJS does a fair bit of work to split up code that gets sent to client-side and what stays on server-side... The developer should keep this in mind and try to split things that relate to client-side and server-side into different files.
In my case, I placed the Interface definitions and my custom Hooks in a different file from the Mongoose Schema definitions; and then imported only the Interfaces and Hooks when I need those, which made the errors go away.
Trying to keep everything in the same place sounds logical and neat, but doesn't work in this case.
I copied your code and it worked fine (went into the tokens collections versus token like expected possibly) one thing I noticed was the expires field on createdAt - was this a NextJS field? It's not a default field so just curious. Also, can you paste the exact error you are encountering, this will help someone track the issue down.
{
_userId: new ObjectId("5e1a0651741b255ddda996c4"),
token: 'abcd123',
createdAt: 2021-09-24T23:10:24.288Z,
_id: new ObjectId("614e5ae04c741f91ac062530"),
__v: 0
}
Also, consider using the timestamps options property when declaring the model as this will save you the headache of setting createdAt up (and you can also have updatedAt automatically update).
token: { type: String, required: true },
},
{
timestamps: true
}
);
I had the same error. Even though my schema file was correct. the issue was for some reason, I have been importing the model in react component file
import room from "../models/room";

What is the best way to keep track of changes of a document's property in MongoDB?

I would like to know how to keep track of the values of a document in MongoDB.
It's a MongoDB Database with a Node and Express backend.
Say I have a document, which is part of the Patients collection.
{
"_id": "4k2lK49938d82kL",
"firstName": "John",
"objective": "Burn fat"
}
Then I edit the "objective" property, so the document results like this:
{
"_id": "4k2lK49938d82kL",
"firstName": "John",
"objective": "Gain muscle"
}
What's the best/most efficient way to keep track of that change? In other words, I would like to know that the "objective" property had the value "Burn fat" in the past, and access it in the future.
Thanks a lot!
Maintaining/tracking history in the same document is not all recommended. As the document size will keep on increasing leading to
probably if there are too many updates, 16mb document size limit
Performance degrades
Instead, you should maintain a separate collection for history. You might have use hibernates' Javers or envers for auditing for your relational databases. if not you can check how they work. A separate table (xyz_AUD) is maintained for each table (xyz). For each row (with primary key abc) in xyz table, there exist multiple rows in xyz_AUD table, where each row is version of that row.
Moreover, Javers also support MongoDB auditing. If you are using java you can directly use it. No need to write your own logic.
Refer - https://nullbeans.com/auditing-using-spring-boot-mongodb-and-javers/
One more thing, Javers Envers Hibernate are java libraries. But I'm sure for other programming languages also, similar libraries will be present.
There is a mongoose plugin as well -
https://www.npmjs.com/package/mongoose-audit (quite oudated 4 years)
https://github.com/nassor/mongoose-history#readme (better)
Maybe you can change the type of "objective" to array and track the changes in it. the last one of the array is the latest value.
Maintain it as a sub-document like below
{
"_id": "4k2lK49938d82kL",
"firstName": "John",
"objective": {
obj1: "Gain muscle",
obj2: "Burn fat"
}
}
You can also maintain it as an array field but remember, mongodb doesn't allow you to maintain uniqueness in an array field and if you plan to index the "objective" field, you'll have to create a multi key index
I think the simplest solution would be to use and update an array:
const patientSchema = new Schema({
firstName: { type: String, required: true },
lastName: { type: String, required: true },
objective: { type: String, required: true }
notes: [{
date: { type: Date, default: Date.now() },
note: { type: String, required: true }
}],
});
Then when you want to update the objective...
const updatePatientObjective = async (req, res) => {
try {
// check if _id and new objective exist in req.body
const { _id, objective, date } = req.body;
if (!_id || !objective) throw "Unable to update patient's objective.";
// make sure provided _id is valid
const existingPatient = await Patient.findOne({ _id });
if (!existingPatient) throw "Unable to locate that patient.";
// pull out objective as previousObjective
const { objective: previousObjective } = existingPatient;
// update patient's objective while pushing
// the previous objective into the notes sub document
await existingPatient.updateOne({
// update current objective
$set { objective },
// push an object with a date and note (previouseObjective)
// into a notes array
$push: {
notes: {
date,
note: previousObjective
},
},
}),
);
// send back response
res
.status(201)
.json({ message: "Successfully updated your objective!" });
} catch (err) {
return res.status(400).json({ err: err.toString() });
}
};
Document will look like:
firstName: "John",
lastName: "Smith",
objective: "Lose body fat.",
notes: [
{
date: 2019-07-19T17:45:43-07:00,
note: "Gain muscle".
},
{
date: 2019-08-09T12:00:38-07:00,
note: "Work on cardio."
}
{
date: 2019-08-29T19:00:38-07:00,
note: "Become a fullstack web developer."
}
...etc
]
Alternatively, if you're worried about document size, then create a separate schema for patient history and reference the user's id (or just store the patient's _id as a string instead of referencing an ObjectId, whichever you prefer):
const patientHistorySchema = new Schema({
_id: { type: Schema.Types.ObjectId, ref: "Patient", required: true },
objective: { type: String, required: true }
});
Then create a new patient history document when the objective is updated...
PatientHistory.create({ _id, objective: previousObjective });
And if you need to access to the patient history documents...
PatientHistory.find({ _id });

Unhandled rejection SequelizeDatabaseError: relation "users" does not exist

I am getting started with Sequelize. I am following the documentation they are providing on their website :http://docs.sequelizejs.com/manual/installation/getting-started.html
const Sequelize = require('sequelize');
const sequelize = new Sequelize('haha', 'postgres', 'postgres', {
host: 'localhost',
dialect: 'postgres',
operatorsAliases: false,
pool: {
max: 5,
min: 0,
acquire: 30000,
idle: 10000
},
// SQLite only
storage: 'path/to/database.sqlite'
});
sequelize
.authenticate()
.then(() => {
console.log('Connection has been established successfully.');
})
.catch(err => {
console.error('Unable to connect to the database:', err);
});
const User = sequelize.define('user', {
firstName: {
type: Sequelize.STRING
},
lastName: {
type: Sequelize.STRING
}
});
// force: true will drop the table if it already exists
User.sync({force: true}).then(() => {
// Table created
return User.create({
firstName: 'John',
lastName: 'Hancock'
});
});
Up until here, everything works perfectly. And the table "user" is correctly built and populated. (Although I do not understand Sequelize appends an "s" automatically to "user", any explanation.)
However when I add the following portion of code:
User.findAll().then(users => {
console.log(users)
})
I get this error :
Unhandled rejection SequelizeDatabaseError: relation "users" does not
exist
So my questions are:
Why does Sequelize add an "s" to user. (I know it makes sense but shouldn't the developer decide that)
What is causing that error? I followed the documentation but it still didn't work?
When you are defining your model you can add configurations, in this case the option that you must add is freezeTableName prevents the names from being plural.
const User = sequelize.define('user', {
firstName: {
type: Sequelize.STRING
},
lastName: {
type: Sequelize.STRING
}
}, {
// disable the modification of table names; By default, sequelize will automatically
// transform all passed model names (first parameter of define) into plural.
// if you don't want that, set the following
freezeTableName: true,
});
There is another interesting way you can avoid this. But you need to really focus on this way of implementation.
const User = sequelize.define("user", {
firstname: {
type: Sequelize.STRING
},
lastname: {
type: Sequelize.STRING
}
});
you intentionally put user here and use users in other places of coding(Assume sequelize will automatically transform all passed model names (first parameter of define) into plural) . This way of coding will simplify your code.
This problem occurs because creating a table is an asynchronous function. The problem is, the findAll() function can get executed while the table has not been created.
to solve this, you can use:
(async ()=>{
await User.sync({force: true});
// Table created
const users=await User.findAll();
console.log(users);
})();
The problem, in my case, was that the table users was not created. You can create the table manually with CREATE TABLE IF NOT EXISTS (SQL) or add the tableName = "users" in the options object:
export const User = db.define('user',
{
id: {
type: DataTypes.UUIDV4,
autoIncrement: true,
primaryKey: true,
},
name: {
type: new DataTypes.STRING(128),
allowNull: false,
},
email: {
type: new DataTypes.STRING(128),
allowNull: true,
},
password: {
type: new DataTypes.STRING(128),
allowNull: true,
},
},
{
freezeTableName: true,
tableName: "users"
}
);
Run that code twice.
Before running the second time, comment out the following code,
// force: true will drop the table if it already exists
User.sync({force: true}).then(() => {
// Table created
return User.create({
firstName: 'John',
lastName: 'Hancock'
});
});
Maybe answer is not entirely connected with you question but I want to describe my experience with this error
Error: relation "users" does not exist.
It appears Sequelize make migrations based on migrations file names and it alphabetical order. My problem was my files naming was not sorted in order to create proper connections.
If you face with this problem make sure yours migration files are fired in proper (in alphabetically) order.
The proper order is to first migrate table without connections (eg. table_A) and then tables with connections to table_A.
As I said this may not be answer for your particular order but I want to share my experiences because I didn't find this information on the internet when I was looking for this error.
Simply append tableName: "Users" to your model configuration.
The easiest way I found to solve, is to explicitly set the tableName on the model. As others have mentioned, sequelize defaults to the plural form of a model as the table name. For instance User, becomes Users.
When you query, sequelize looks after a table with the same name as your model User. By defining the tableName in the model, sequelize should search the correct table. Append tableName: "Users" to your model configuration i.e:
User.init(
{
email: DataTypes.STRING,
password: DataTypes.STRING,
role: DataTypes.INTEGER,
},
{
sequelize,
modelName: 'User',
tableName: 'Users',
}
);
If you want Sequelize to use a singular word ('info') for a model and that same singular word for the table ('info'), you can name the model 'info' and also add tablename: 'info' to the definition of your model.
This is a way to control Sequelize's default behavior of pluralizing model names, or not, on a table-by-table basis.
info.js
module.exports = (sequelize, DataTypes) => {
const info = sequelize.define('info', {
firstname: DataTypes.STRING,
email: DataTypes.STRING,
phone: DataTypes.STRING,
}, {
tableName: 'info'
});
return info;
};

How to process deep update with subdocument in Mongodb/Mongoose?

I would like to use mongoose in this case. Let's say we have a Schema like this:
const userSchema = new Schema({
name: {
first: { type: String, required: true },
last: { type: String, required: true },
},
email: { type: String, required: true, unique: true, lowercase: true },
});
Let's say we already have document with first and last name. I need to update only first name and email with the following args object:
const updateUser = {
name: {
first: Eddy,
},
email: 'eddy#gordo.io'
};
If it will be used with mongoose update methods in most cases it would also change last name to null. That's because JavaScript doesn't support deep object merge and so on. So what's the best way to gain possibility to merge objects properly?
I've found the way to do it below but it doesn't seem to be the best solution:
User.findById(args.id, (error, user) => {
if (error) throw err;
user.name.first = args.name.first ? args.name.first : user.name.first;
user.name.last = args.name.last ? args.name.last : user.name.last;
user.email = args.email ? args.email : user.email;
user.save((err, updatedUser) => {
if (err) throw err;
return updatedUser;
});
});
I and Eslint don't like this with reassigning and unmaintainable code. Have somebody better idea? I'm not sure that I need to use lodash and other libs only for this capability.
Here is what I've done with this issue.
Thanks to Neil Lunn with this idea.
We're need to map all object keys to dot.notation and pass to the $set.
Here is the gist with this function. If we need two level nesting we can add another condition and mapping.
Anyway I hope that Mongoose API will resolve us to make deep merge with nested objects, but for now Neil's solution is the best in my opinion.

Categories