Adding Attributes on a Join Table in Sequelize - node.js

I'm having trouble setting an attribute on a junction table.
I have a Many-to-Many association defined between two models UserModel and HangModel, through a custom table HangUsers.
const HangModel = rootRequire('/models/Hang');
const UserModel = rootRequire('/models/User');
const HangUsers = database.define('HangUsers', {
id: {
type: Sequelize.INTEGER(10).UNSIGNED,
primaryKey: true,
autoIncrement: true,
},
hangId: {
type: Sequelize.INTEGER(10).UNSIGNED,
references: {
model: HangModel,
key: 'id',
},
},
userId: {
type: Sequelize.INTEGER(10).UNSIGNED,
references: {
model: UserModel,
key: 'id',
},
},
rsvp: {
type: Sequelize.STRING,
allowNull: false,
validate: {
isIn: {
args: [ 'pending', 'joined' ],
msg: 'The rsvp provided is invalid',
},
},
},
});
UserModel.hasMany(HangUsers, { as: 'invitations' });
HangModel.hasMany(HangUsers, { as: 'invites' });
UserModel.belongsToMany(HangModel, { through: HangUsers });
HangModel.belongsToMany(UserModel, { through: HangUsers });
The through table has a column rsvp, that I'm trying to populate when I add users to a hang:
const hang = await HangModel.create();
await hang.addUser(user, { through: { rvsp: 'joined' } });
However, I'm getting an error:
AggregateError
at recursiveBulkCreate (/Users/sgarza62/ditto-app/api/node_modules/sequelize/lib/model.js:2600:17)
at processTicksAndRejections (internal/process/task_queues.js:97:5)
at async Function.bulkCreate (/Users/sgarza62/ditto-app/api/node_modules/sequelize/lib/model.js:2824:12)
at async Promise.all (index 0)
at async BelongsToMany.add (/Users/sgarza62/ditto-app/api/node_modules/sequelize/lib/associations/belongs-to-many.js:740:30)
at async /Users/sgarza62/ditto-app/api/routes/hangs.js:121:3 {
name: 'AggregateError',
errors: [
BulkRecordError [SequelizeBulkRecordError]: notNull Violation: HangUsers.rsvp cannot be null
at /Users/sgarza62/ditto-app/api/node_modules/sequelize/lib/model.js:2594:25
at processTicksAndRejections (internal/process/task_queues.js:97:5) {
name: 'SequelizeBulkRecordError',
errors: [ValidationError],
record: [HangUsers]
}
]
}
When I allow null on the rsvp column, the HangUsers row is created, but the rsvp value is NULL.
It seems the { through: { rsvp: 'joined' } } parameter is being ignored.
I've done this all according to the BelongsToMany docs and the Advanced M:N Associations docs, where it says:
However, defining the model by ourselves has several advantages. We can, for example, define more columns on our through table:
const User_Profile = sequelize.define('User_Profile', {
selfGranted: DataTypes.BOOLEAN
}, { timestamps: false });
User.belongsToMany(Profile, { through: User_Profile });
Profile.belongsToMany(User, { through: User_Profile });
With this, we can now track an extra information at the through table,
namely the selfGranted boolean. For example, when calling the
user.addProfile() we can pass values for the extra columns using the
through option.
Example:
const amidala = await User.create({ username: 'p4dm3', points: 1000 });
const queen = await Profile.create({ name: 'Queen' });
await amidala.addProfile(queen, { through: { selfGranted: false } });
const result = await User.findOne({
where: { username: 'p4dm3' },
include: Profile
});
console.log(result);

Just a typo on the attribute name: 'rvsp' should be 'rsvp'.

Related

Can't update a specific column in Sequelize

I have a model. It is for the intermediate(pivot) table.
UserCars.init({
carId: DataTypes.INTEGER,
userId: DataTypes.INTEGER,
title: DataTypes.STRING,
}, {
timestamps: false,
sequelize,
modelName: 'UserCars',
});
and here is my migration for this:
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.createTable('UserCars', {
carId: {
allowNull: false,
type: Sequelize.INTEGER,
references: {
model: 'Cars',
key: 'id'
},
},
userId: {
allowNull: false,
type: Sequelize.INTEGER,
references: {
model: 'Users',
key: 'id'
},
},
title: {
type: Sequelize.STRING
},
}, {
uniqueKeys: {
Items_unique: {
fields: ['carId', 'userId']
}
}
});
},
down: async (queryInterface, Sequelize) => {
await queryInterface.dropTable('UserCars');
}
};
And I'm doing this below to create/update it:
userCar = await UserCars.findOne({
where: {
carId: 10,
userId: req.user.id,
}
});
if(userCar) {
userCar.userId = 20; // <--- This doesn't change
userCar.title = 'some other thing'; // <--- This changes
await userCar.save();
} else {
userCar = await UserCars.create({
userId: 20,
title: 'something'
});
}
The problem is, the title is being updated but the userId is not.
(I believe) this is due to constraints, you cannot use instance method to update FK value.
You need to use M-N association functions, or otherwise you could use raw SQL.
const car = await Car.findByPk(10);
const user = await User.findByPk(newValue);
// This also takes care of deleting the old associations
await car.setUsers(user, {
through: {'title': 'new value'}
});
I hope the upsert function is implemented in the future.
ref: https://github.com/sequelize/sequelize/issues/11836

beforeBulkDestroy not finding model property to change

I am trying to use the beforeBulkDestory Sequelize hook on a user delete that will switch the deleted column boolean to true prior to updating the record to add a timestamp for deleted_at. However, when I console.log the function parameter it provides a list of options and not the model object that I can update for the record of focus. Am I approaching this the wrong way? Is this something that should be set using model instances?
API Call:
import db from '../../../models/index';
const User = db.users;
export default (req, res) => {
const {
query: { id },
} = req
console.log(User)
if (req.method === 'DELETE') {
User.destroy({
where: {
id: id
}
}).then(data => {
res.json({
message: 'Account successfully deleted!'
})
})
} else {
const GET = User.findOne({
where: {
id: id
}
});
GET.then(data => {
res.json(data)
})
}
}
Parameter Values (beforeBulkDestroy, afterBulkDestroy):
beforeBulkDestroy
{
where: { id: '5bff3820-3910-44f0-9ec1-e68263c0f61f' },
hooks: true,
individualHooks: false,
force: false,
cascade: false,
restartIdentity: false,
type: 'BULKDELETE',
model: users
}
afterDestroy
{
where: { id: '5bff3820-3910-44f0-9ec1-e68263c0f61f' },
hooks: true,
individualHooks: true,
force: false,
cascade: false,
restartIdentity: false,
type: 'BULKUPDATE',
model: users
}
Model (users.js):
'use strict';
const Sequelize = require('sequelize');
const { Model } = require('sequelize');
const bcrypt = require("bcrypt");
module.exports = (sequelize, DataTypes) => {
class users extends Model {
/**
* Helper method for defining associations.
* This method is not a part of Sequelize lifecycle.
* The `models/index` file will call this method automatically.
*/
static associate(models) {
// define association here
}
};
users.init({
id: {
type: DataTypes.UUID,
defaultValue: Sequelize.UUIDV4,
primaryKey: true
},
first_name: DataTypes.STRING,
last_name: DataTypes.STRING,
password: {
type: DataTypes.STRING
},
email: DataTypes.STRING,
active: {
type: DataTypes.BOOLEAN,
defaultValue: true
},
deleted: {
type: DataTypes.BOOLEAN,
defaultValue: false
}
}, {
hooks: {
beforeDestroy: (user, options) => {
console.log("beforeDestroy")
console.log(user)
console.log(options)
user.deleted = true
}
},
sequelize,
freezeTableName: true,
modelName: 'users',
omitNull: true,
paranoid: true,
underscored: true,
createdAt: 'created_at',
updatedAt: 'updated_at',
deletedAt: 'deleted_at',
hooks: {
beforeCreate: async function(user){
console.log("beforeCreate")
console.log(user)
const salt = await bcrypt.genSalt(12);
user.password = await bcrypt.hash(user.password, salt);
console.log(user.password)
},
beforeBulkDestroy: async function(user){
console.log("beforeBulkDestroy")
console.log(user)
},
afterBulkDestroy: async function(user){
console.log("afterDestroy")
console.log(user)
}
}
});
users.prototype.validPassword = async function(password) {
console.log("validatePassword")
console.log(password)
return await bcrypt.compare(password, this.password);
}
return users;
};
the before/after bulkDestroy hooks only receive the options, not the instances. One way you could do this is defining a before/after Destroy hook:
hooks: {
beforeDestroy: (user, { transaction }) => {
user.update({ deleted: true }, { transaction });
}
}
and calling User.destroy with the individualHooks option:
User.destroy({ where: { id: id }, individualHooks: true });
Be aware that this will load all selected models into memory.
Docs
Note: In your case, since you're only deleting one record by id, it would be better to just user = User.findByPk(id) then user.destroy(). This would always invoke the hooks and it also makes sure the record you want to delete actually exists.
Note 2: Not sure why you need a deleted column, you could just use deletedAt and coerce it into a boolean (with a virtual field if you want to get fancy).

Sequelize .save() on instance not working

I have a model defined as below. I then try to run the code below it to update the isTeamLead property of a retrieved instance but I get the error teamMember.save() is not a function.
const TeamMember = sequelize.define('teamMember', {
userId: Sequelize.INTEGER,
teamId: Sequelize.INTEGER,
slotNumber: Sequelize.INTEGER,
isTeamLead: Sequelize.BOOLEAN
});
Promise.all([db.models.TeamMember.findOne({ where: { $and: [{ userId: lead.id }, { teamId: id }] } })]).then((teamMember) => { teamMember.isTeamLead = true; teamMember.save() });
I was able to solve this by making sure I was operating on the instance. Above I was operating on the array not the actual teamMember instance.
Promise.all([db.models.TeamMember.findOne({ where: { $and: [{ userId: lead.id }, { teamId: id }] } })]).then((teamMember) => { teamMember[0].isTeamLead = true; teamMember[0].save() });
If you are using sequelize, you could do something like.
1) first find
2) if exist update
const model = await Model.findById(id);
if(!model) {
return res.status(400).json({ msg: 'Bad Request: Model not found' });
}
const updatedModel = await Model.update({
key: value,
)};

Sequelize one to many association

I am trying to map sequelize one to many association by referring to sequelize documentation but I could not able to find a complete example to get it work.
I have a ClaimType model as follows
const Sequelize = require('sequelize');
module.exports = function (sequelize) {
const ClaimType = sequelize.define('claim_type', {
id: {
type: Sequelize.INTEGER,
autoIncrement: true,
primaryKey: true
},
name: {
type: Sequelize.STRING
}
}, {
timestamps: false,
freezeTableName: true
});
return ClaimType;
};
and MaxClaimAmount
const Sequelize = require('sequelize');
module.exports = function (sequelize) {
const MaxClaimAmount = sequelize.define('max_claim_amount', {
id: {
type: Sequelize.INTEGER,
autoIncrement: true,
primaryKey: true
},
amount: {
type: Sequelize.DECIMAL
},
year: {
type: Sequelize.INTEGER
}
}, {
timestamps: false,
freezeTableName: true
});
return MaxClaimAmount;
};
finally in index.js
const ClaimType = require('./model/claim_type.js')(sequelize);
const MaxClaimAmount = require('./model/max_claim_amount.js')(sequelize);
ClaimType.hasMany(MaxClaimAmount, { as: 'claimAmount' });
sequelize.sync({ force: false }).then(() => {
return sequelize.transaction(function (t) {
return ClaimType.create({
name: 'OPD'
}, { transaction: t }).then(function (claimType) {
// want to know how to associate MaxClaimAmount
});
}).then(function (result) {
sequelize.close();
console.log(result);
}).catch(function (err) {
sequelize.close();
console.log(err);
});
});
ClaimType object is returned from the first part of the transaction and I want to know how to implement the association between ClaimType and MaxClaimAmount?
You make the association in your models.
(Associations: one-to-many in sequelize docs)
const ClaimType = sequelize.define('claim_type', {/* ... */})
const MaxClaimAmount = sequelize.define('max_claim_type', {/* ... */})
ClaimType.hasMany(MaxClaimAmount, {as: 'MaxClaims'})
This will add the attribute claim_typeId or claim_type_id to MaxClaimAmount (you will see the column appear in your table). Instances of ClaimType will get the accessors getMaxClaims and setMaxClaims (so you can set foreign id on table)
The next step would be creating your backend routes and using the accessor instance methods to set a foreign key. The accessor functions are called like so: (instance1).setMaxClaims(instance2)
Instances are returned from queries

Sequelize Many to Many failing with 'is not associated to' when trying to associate entries?

I am having a problem with my many to many configuration with Sequelize, where it complains that site_article_keyword is not associated to article_keyword. The code below represents a minimal test case to try to understand what I am doing wrong (I hoped to provide something smaller, but this is what I have). I am using bluebird for the Promise API.
const Sequelize = require('sequelize');
var sequelize = new Sequelize(undefined, undefined, undefined, {
dialect: 'sqlite',
storage: './mydatabase',
});
const SiteArticle = sequelize.define('site_article', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true
},
ownerId: {
type: Sequelize.INTEGER,
field: 'owner_id'
},
title: Sequelize.STRING
// other fields omitted
}, {
timestamps: true
});
const ArticleKeyword = sequelize.define('article_keyword', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true
},
name: Sequelize.STRING,
language: Sequelize.STRING
// other fields omitted
}, {
timestamps: true
});
const SiteArticleKeyword = sequelize.define('site_article_keyword', {
siteArticleId: {
type: Sequelize.INTEGER,
field: 'site_article_id',
references: {
model: SiteArticle,
key: 'id'
}
},
articleKeywordId: {
type: Sequelize.INTEGER,
field: 'article_keyword_id',
references: {
model: ArticleKeyword,
key: 'id'
}
}
// other fields omitted
}, {
timestamps: true
});
(ArticleKeyword).belongsToMany(
SiteArticle, { through: SiteArticleKeyword });
(SiteArticle).belongsToMany(
ArticleKeyword, { through: SiteArticleKeyword });
That's the model defined, now for trying to create the source and destination entries, that I then want to associate. The failure happens on the line where I call ArticleKeyword.findAll():
sequelize.sync({}).then(function() {
// populate some data here
let siteArticle;
SiteArticle.create({
ownerId: 1,
title: 'hello world'
}).then(function(entry) {
siteArticle = entry;
console.log('site article: ', JSON.stringify(entry, undefined, 2));
return ArticleKeyword.findOrCreate({
where: {
name: 'keyword1',
language: 'en'
}
});
}).spread(function(entry, success) {
console.log('article keyword: ', JSON.stringify(entry, undefined, 2));
return siteArticle.addArticle_keyword(entry);
}).spread(function(entry, success) {
console.log('site article keyword: ', JSON.stringify(entry, undefined, 2));
const siteArticleId = 1;
const language = 'en';
return ArticleKeyword.findAll({
where: {
language: language,
},
include: [{
model: SiteArticleKeyword,
where: {
siteArticleId: siteArticleId
}
}]
});
}).then(function(articleKeywords) {
if (articleKeywords) {
console.log('entries: ', JSON.stringify(articleKeywords, undefined, 2));
} else {
console.log('entries: ', 'none');
}
}).catch(function(error) {
console.log('ERROR: ', error);
}.bind(this));
}).catch(function(error) {
console.log(error);
});
I am basing my code on the 'Mixin BelongsToMany' example in the Sequelize documentation.
Can anyone suggest what I am doing wrong?
The issue turns out that the reason site_article_keyword is not associated is because it is the association! With that in mind the code becomes:
return ArticleKeyword.findAll({
where: {
language: language,
},
include: [{
model: SiteArticle,
as: 'SiteArticle',
siteArticleId: siteArticleId
}]
});
BTW one minor tweak to my code, is in the inclusion of 'as' to the belongsToMany:
ArticleKeyword.belongsToMany(
SiteArticle,
{ through: SiteArticleKeyword, as: 'SiteArticle' }
);
SiteArticle.belongsToMany(
ArticleKeyword,
{ through: SiteArticleKeyword, as: 'ArticleKeyword' }
);
This allows for addArticleKeyword() instead of addArticle_Keyword().
In through relationships the following should work
ArticleKeyword.findAll({
include: [{
model: SiteArticle,
through: {
attributes: ['createdAt', 'startedAt', 'finishedAt'],
where: {
siteArticleId: siteArticleId
}
}
}]
});

Resources