Sequelize: How to make relationship between 3 tables - node.js

If you have the following entities: Users, Roles, Organizations.
You want to setup the relationships so that each user has an organization-role.
In simple each user can belong to multiple organizations and the user has a specific role in each organization.
How would you model this with Sequelize?
I have tried by creating a junction table called organisation_users and then in that table adding a organisationUsers.belongsTo(role);
From I have read Sequelize doesnt support associations on junction tables and so that solution doesn't work.
Regards,
Emir

Sequelize does supports associations on join table. You should look in the "through" options here >
Example :
const Asso_Organization_User = sequelize.define('Asso_Organization_User', {
id: DataTypes.STRING,
userId: DataTypes.STRING,
organizationId: DataTypes.STRING
});
User.Organizations = User.belongsToMany(Organization, {
through: Asso_Organization_User,
foreignKey: 'userId',
otherKey: 'organizationId',
as: 'organizations'
})
But your case is a bit special, I dont see a way using sequelize to get a user, all of its organizations, and his role for each organization in the same query.
It looks like there is no solution yet, based on this issue: https://github.com/sequelize/sequelize/issues/6671
Maybe you could do it in two queries : get all organizations and all roles, and then merging them with code.
Actually, I see another solution:
const Asso_Organization_User = sequelize.define('Asso_Organization_User', {
id: DataTypes.STRING,
userId: DataTypes.STRING,
organizationId: DataTypes.STRING
});
User.Organizations = User.belongsToMany(Organization, {
through: Asso_Organization_User,
foreignKey: 'userId',
otherKey: 'organizationId',
as: 'organizations'
})
The model would look like the following :
User: id
Role: id, userId, organizationId
Organizations: id
Asso_Organization_User: id, userId, organizationId
Then :
User.Organizations = User.belongsToMany(Organization, {
through: Asso_Organization_User
})
Organization.Roles = Organization.haMany(Role, {
foreignKey: 'organizationId'
})
And then you should be able to query :
User.findAll({
include: [ {
model: Organization,
include: {
model: Role
}
} ]
where: {
'role.userId': Sequelize.col("User.id")
}
});
I'm not totally sure of the exact syntax, but combining Sequelize.col with the hasMany should work. But be careful, if you dont add this where clause, this would return all roles for each organization (for every user having a role in this organization).

It may vary upon your requirements of fetching the data from these 3 tables
Consider this Example :
Tables : login, userProfile, farmer
Requirements : Farmer has one user Profile , User Profile has one Login.
Along with these tables we can register a user as a Farmer.
--------------------------------------------------------------------------
farmer.js
module.exports = (Sequelize, DataTypes) => {
const Farmer = Sequelize.define("farmer", { /*attributes*/});
return Farmer;
};
--------------------------------------------------------------------------
login.js
module.exports = (Sequelize, DataTypes) => {
const Login = Sequelize.define("login", {*attributes*/});
return Login;
};
--------------------------------------------------------------------------
userProfile.js
module.exports = (Sequelize, DataTypes) => {
const UserProfile = Sequelize.define("userProfile", {*attributes*/});
return UserProfile;
};
--------------------------------------------------------------------------
index.js
const dbConfig = require("../config/dbConfig"); // your config file
const { Sequelize, DataTypes } = require("sequelize");
//object initilize. (pass parameter to constructor)
const sequelize = new Sequelize(dbConfig.DB, dbConfig.USER, dbConfig.PASSWORD, {
host: dbConfig.HOST,
dialect: dbConfig.dialect,
operatorsAliases: false, //hide errors
pool: {
max: dbConfig.pool.max,
min: dbConfig.pool.min,
acquire: dbConfig.pool.acquire,
idle: dbConfig.pool.idle,
},
});
sequelize
.authenticate()
.then(() => {
console.log("DB connected!");
})
.catch((err) => {
console.log("Error " + err);
});
const db = {}; // Empty object
db.Sequelize = Sequelize;
db.sequelize = sequelize;
db.login = require("./login.js")(sequelize, DataTypes);
db.userProfile = require("./userProfile.js")(sequelize, DataTypes);
db.farmer = require("./farmer.js")(sequelize, DataTypes);
//relations
db.farmer.userProfile = db.farmer.belongsTo(db.userProfile);
db.userProfile.login = db.userProfile.belongsTo(db.login);
db.sequelize
.sync({ force: false }) //force :true - drop all tables before start
.then(() => {
console.log("yes-sync done!");
});
module.exports = db;
//Declare follwing things in a separate location (may be controllers__.js
-----------INSERT DATA (CREATE)--------------------------
const saved = await Farmer.create(
{
supplierCode: "SUP0001",
userProfile: {
firstName: "ssss",
middleName: "ssss",
lastName: "ssss",
address: "ssss",
login: {
name: "ssss",
email: "ssss",
password: "ssss",
role: "ssss",
lastLogin: null,
avatar: "ssss",
status: "ssss",
},
},
},
{
include: [
{
association: Farmer.userProfile,
include: [Login],
},
],
}
);
Observe the usage of the include option in the Farmer.create call. That is necessary for Sequelize to understand what you are trying to create along with the association.
Note: here, our user model is called farmer, with a lowercase f - This means that the property in the object should also be farmer. If the name given to sequelize.define was Farmer, the key in the object should also be Farmer.
-----------FETCH DATA (SELECT)--------------------------
const users = await Farmer.findAll({
include: [
{
association: Farmer.userProfile,
include: [Login],
},
],
});
Output:
[
{
"id": 1,
"supplierCode": "SUP0001",
"createdAt": "2021-11-17T07:39:13.000Z",
"updatedAt": "2021-11-17T07:39:13.000Z",
"userProfileId": 1,
"userProfile": {
"id": 1,
"firstName": "ssss",
"middleName": "ssss",
"lastName": "ssss",
"address": "ssss",
"createdAt": "2021-11-17T07:39:13.000Z",
"updatedAt": "2021-11-17T07:39:13.000Z",
"loginId": 1,
"login": {
"id": 1,
"name": "ssss",
"email": "ssss",
"password": "ssss",
"role": "ssss",
"lastLogin": null,
"avatar": "ssss",
"status": "ssss",
"createdAt": "2021-11-17T07:39:13.000Z",
"updatedAt": "2021-11-17T07:39:13.000Z"
}
}
}
]
Assume you want to add another user Role/Type (we have Farmer already). then you can make coordinater.js (example user role/type) and defind attributes
in above index.js you can add this relation
//Coordinator relation
db.coordinator.userProfile = db.coordinator.belongsTo(db.userProfile, {
onDelete: "CASCADE",
onUpdate: "CASCADE",
});
Now you can register users with different users Roles/Types :
Coordinator.create.... give correct associations

Related

NodeJS + Sequelize / How to perform triple model query?

I come from this topic:
NodeJS & Sequelize: How can I join 3 models?
I come to you now, because I need someone who can teach me about how to get started on junction models with NodeJS and Sequelize.
What I'm trying to do is, having 2 main models, for example, Employee and Office, a 3rd model called EmployeeOffice enters and connects the 2 main models, including its own field called "employee_chair". When i call a method, for example, Office.findAll, this is what I would get:
{
"id": 1,
"office_name": "Mars"
"office_color": "Red",
"createdAt": "2021-09-30T18:53:38.000Z",
"updatedAt": "2021-09-30T18:53:38.000Z",
"employees": [
{
"id": 1,
"employee_name": "Albert",
"employee_mail": "qalbert443#gmail.com",
"createdAt": "2021-09-30T18:53:45.000Z",
"updatedAt": "2021-09-30T18:53:45.000Z",
"employee_office": {
"createdAt": "2021-09-30T18:53:45.000Z",
"updatedAt": "2021-09-30T18:53:45.000Z"
}
}
]
}
NOW. What I need, is to have the model called instead of the junction table that is automatically created. Because I can join the 3 tables, but the 3rd table has the field "employee_chair", that I mentioned earlier. The desired response would look like this:
{
"id": 1,
"office_name": "Mars"
"office_color": "Red",
"createdAt": "2021-09-30T18:53:38.000Z",
"updatedAt": "2021-09-30T18:53:38.000Z",
"employees": [
{
"id": 1,
"employee_name": "Albert",
"employee_mail": "qalbert443#gmail.com",
"createdAt": "2021-09-30T18:53:45.000Z",
"updatedAt": "2021-09-30T18:53:45.000Z",
"employee_office": {
"employee_chair": 3,
"createdAt": "2021-09-30T18:53:45.000Z",
"updatedAt": "2021-09-30T18:53:45.000Z"
}
}
]
}
How can I do to make (or force) sequelize to make the relations through the model and not through the automatically created table?
Hope you guys can help me, I'm stuck and I don't know where to ask
#cupid22 Here is my index.js, userproject model and the controller function im calling:
index.js:
const dbConfig = require("../config/db.config.js");
const Sequelize = require("sequelize");
const sequelize = new Sequelize(dbConfig.DB, dbConfig.USER, dbConfig.PASSWORD, {
host: dbConfig.HOST,
dialect: dbConfig.dialect,
operatorsAliases: false,
pool: {
max: dbConfig.pool.max,
min: dbConfig.pool.min,
acquire: dbConfig.pool.acquire,
idle: dbConfig.pool.idle
}
});
const db = {};
db.Sequelize = Sequelize;
db.sequelize = sequelize;
db.projects = require("./project.model.js")(sequelize, Sequelize);
db.users = require("./user.model.js")(sequelize, Sequelize);
db.projects.belongsToMany(db.users, {
through: "users_projects",
as: "users",
foreignKey: "user_id",
});
db.users.belongsToMany(db.projects, {
through: "users_projects",
as: "projects",
foreignKey: "project_id",
});
module.exports = db;
Controller function:
// Retrieve all Projects from the database.
exports.findAll = (req, res) => {
const title = req.query.title;
var condition = title ? { title: { [Op.like]: `%${title}%` } } : null;
Project.findAll({
include: [
{
model: User,
as: "users",
}]
})
.then(data => {
res.send(data);
})
.catch(err => {
res.status(500).send({
message:
err.message || "Some error occurred while retrieving projects."
});
});
};
UserProject Model:
module.exports = (sequelize, Sequelize) => {
const UserProject = sequelize.define('user_project', {
user_id: {
type: Sequelize.INTEGER,
references: {
model: 'user',
key: 'id'
}
},
project_id: {
type: Sequelize.INTEGER,
references: {
model: 'project',
key: 'id'
}
},
user_type: {
type: Sequelize.INTEGER,
}
}, {
timestamps: false,
tableName: 'users_projects'
});
db.users.belongsToMany(db.projects, { through: User_Profile });
db.projects.belongsToMany(db.users, { through: User_Profile });
return UserProject;
};

Nested associated data through Sequelize join tables

Using Sequelize, I'm trying to get an output like this:
[{
"Id": 1,
"Name": "Game 1",
"Teams": [{
"Id": 1,
"Name": "Team 1",
"Users": [{
"Id": 1,
"UserName": "User 1"
}]
}]
}, {
"Id": 2,
"Name": "Game 2",
"Teams": [{
"Id": 1,
"Name": "Team 1",
"Users": [{
"Id": 2,
"UserName": "User 2"
}]
}]
}]
Note that Team 1 has 2 different users, but that's only because they're set up that way per game... so a user isn't tied directly to a team, but rather through a team game constraint. Basically, my Game HasMany Teams, and my Game/Team HasMany Users... a many-to-many-to-many relationship. I was trying to follow this thread, but it seems like what they're doing there doesn't actually work, as I tried doing this:
// models/Game.js
module.exports = (sequelize, types) => {
const GameModel = sequelize.define('Game', {
Id: {
type: types.INTEGER,
primaryKey: true,
autoIncrement: true
},
Name: {
type: types.STRING,
allowNull: false
}
});
GameModel.associate = (models) => {
GameModel.belongsToMany(models.Team, {
as: 'Teams',
foreignKey: 'GameId',
through: models.GameTeam
});
};
return GameModel;
};
// models/Team.js
module.exports = (sequelize, types) => {
const TeamModel = sequelize.define('Team', {
Id: {
type: types.INTEGER,
primaryKey: true,
autoIncrement: true
},
Name: {
type: types.STRING,
allowNull: false
}
});
TeamModel.associate = (models) => {
TeamModel.belongsToMany(models.Game, {
as: 'Games',
foreignKey: 'TeamId',
through: models.GameTeam
});
};
return TeamModel;
};
// models/User.js
module.exports = (sequelize, types) => {
const UserModel = sequelize.define('User', {
Id: {
type: types.INTEGER,
primaryKey: true,
autoIncrement: true
},
UserName: {
type: types.STRING,
allowNull: false
}
});
return UserModel;
};
// models/GameTeam.js
module.exports = (sequelize, types) => {
const GameTeamModel = sequelize.define('GameTeam', {
Id: {
type: types.INTEGER,
primaryKey: true,
autoIncrement: true
}
});
GameTeamModel.associate = (models) => {
GameTeamModel.belongsToMany(models.User, {
as: 'Users',
through: 'GameTeamUser'
});
};
return GameTeamModel;
};
The above models create the tables just fine, with what appears to be the appropriate columns. I then do some inserts and try to use a findAll on the Game model like this:
GameModel.findAll({
include: [{
association: GameModel.associations.Teams,
include: [{
association: GameTeamModel.associations.Users,
through: {
attributes: []
}
}],
through: {
attributes: []
}
}]
});
The query starts to go wrong at the 2nd include with the association of the Users. Because I'm trying to nest the users inside of the teams, I figured the join would attempt to use the unique ID on the through table (GameTeams.Id), but instead, the query ends up using this:
LEFT OUTER JOIN `GameTeamUser` AS `Teams->Users->GameTeamUser` ON `Teams`.`Id` = `Teams->Users->GameTeamUser`.`GameTeamId`
I figured the ON would be GameTeams.Id = Teams->Users->GameTeamuser.GameTeamId, but I don't know why it's not, and how to adjust it... I've tried using a custom on in my include (per the docs), but it seems to be ignored completely. Anyone have any advice? Or possibly a better way of structuring this, so it works the way I want it to?
I think you are overcomplicating this thinking you have a many to many to many..and i can see that the fields for your model for GameTeam do not match up with the foreign keys you have declared in your other models...
What do your database tables look like?
Am i correct in saying, that a game has many teams, and a team has many users... however a user can only be on one team at a time, and a team is only in one game at a time? (i am assuming the game/team join and the team/user join are simply temporary records in the join tables disappearing after the game is over etc)

how to merge response attributes of other table in node js

I have 3 Tables User, Cars and UserCars
User{id, name, phone, email}
Cars{id, name, manufacturer}
UserCars{id, car_id, user_id, role}
User have many cars(through UserCars)
Cars have many users(through UserCars)
I am using express js
router.get('/', async (req, res) => {
try {
let car = await Car.findOne({
where: {
id: req.car_id
}});
let users = await car.getUsers({joinTableAttributes: ['role']})
res.send(users)
} catch (e) {
console.log(e)
res.status(400).send(e)
}
})
and this my response
[
{
"id": 1,
"name": "test",
"email": null,
"phone": null,
"createdAt": "2019-07-09T09:38:11.859Z",
"updatedAt": "2019-07-12T04:34:20.922Z",
"User_car": {
"role": "driver"
}
}
]
but any idea how to include role in the user object, rather then specifying it separately in User_car table,
Is there a way where i can get the below output
[
{
"id": 1,
"name": "test",
"email": null,
"phone": null,
"role": 'driver'
"createdAt": "2019-07-09T09:38:11.859Z",
"updatedAt": "2019-07-12T04:34:20.922Z"
}
]
You can use sequelize.literal to get that field when getting your attributes.
attributtes: [
// define your other fields
[sequelize.literal('`users->User_car`.`role`'), 'role'],
]
Now, I not sure if that is going to work with car.getUsers. I usually do a single query with include and also define the "join" table so I can know how is name it on sequelize. Let me show you an example.
module.exports = (sequelize, DataTypes) => {
const UserCar = sequelize.define('UserCar', {
// id you don't need and id field because this is a N:M relation
role: {
type: DataTypes.STRING
},
carId: {
field: 'car_id',
type: DataTypes.INTEGER
},
userId: {
field: 'user_id',
type: DataTypes.INTEGER
},
}, {
tableName: 'User_car',
underscored: true,
createdAt: 'created_at',
updatedAt: 'updated_at',
});
UserCar.associate = (models) => {
models.user.belongsToMany(models.car, { as: 'cars', through: User_car, foreignKey: 'user_id' });
models.car.belongsToMany(models.user, { as: 'users', through: User_car, foreignKey: 'car_id' });
};
return UserCar;
};
router.get('/', async (req, res) => {
try {
const users = await User.findAll({
include: [{
model: Car,
as: 'cars',
where: { id: req.car_id }
}],
attributtes: [
'id',
'name',
'email',
'phone',
[sequelize.literal('`cars->User_car`.`role`'), 'role'],
]
})
res.send(users)
} catch (e) {
console.log(e)
res.status(400).send(e)
}
});

Node.js sequelize model - can I define order of records on the model?

I am newish to node, and can't figure out how to only return 1 record of a hasMany relationship, based on an attribute.
There's a user, level, and levels_user table.
On user, I want to include 1 levels_user where level_id is highest.
Can I put a condition on this model file, something like:
order_by: ['level_id', 'DESC']
LevelsUsers Model
'use strict';
module.exports = (sequelize, DataTypes) => {
let LevelsUsers = sequelize.define('LevelsUsers', {
user_id: DataTypes.INTEGER,
level_id: DataTypes.INTEGER,
created_at: DataTypes.DATE,
updated_at: DataTypes.DATE,
},{
timestamps: false,
freezeTableName: true,
schema: "public",
tableName: "levels_users"
});
return LevelsUsers;
};
Users model association:
Users.hasMany(models.LevelsUsers, {
as: 'levels_users',
targetKey: 'id',
foreignKey: 'user_id',
});
This is my call:
users.getProfileByUserId = (req, res) => {
models.Users.findOne({
where: {id: req.params.userid},
include: [
{
model: models.LevelsUsers,
as: 'levels_users',
limit: 1,
}
]
order: []
}).then(user ....
I tried adding:
`order: [
[ models.LevelsUsers , 'level_id', 'DESC']
]`
Did not work and I think it's evaluated after limit: 1 anyways.
Can I put order_by on the model, to return highest to lowest by level_id each time? If not, what's a better way to accomplish returning only the highest levels_users record where level_id is highest?
The hasMany property is more suited when you want to include all the levels in the user object. In your case, I would advise to just pull the user without it's levels, and then do a second request to pull the higher level linked to that user by querying directly the LevelsUsers model :
models.LevelsUsers.findAll({
where: {
user_id: user.id,
},
order: ['level_id', 'DESC'],
limit: 1,
});
Well i reading over the documentation of sequelize for working with ordering and limit inside the includes tag.
Update query
users.getProfileByUserId = (req, res) => {
models.Users.findOne({
where: {id: req.params.userid},
include: [
{
model: models.LevelsUsers,
as: 'levels_users',
order: [
[ { model: models.LevelsUsers }, 'level_id', 'DESC']
],
limit: 1,
}
]
}).then(user ....
For references go over the following links - https://github.com/sequelize/sequelize/issues/4553#issuecomment-341261957

How to insert initial data using sequelize migrations/seeds?

I'm trying to create my initial migration to populate the test database but I can't get it working. This is what I have in my migration:
'use strict';
module.exports = {
up: function (queryInterface, Sequelize) {
return [
queryInterface.bulkInsert('Users', [
{ username: "user1" },
{ username: "user2" }
])];
},
down: function (queryInterface, Sequelize) {
return queryInterface.dropTable('Users');
}
};
And I get this error:
== 20151024144833-create-conjugation: migrating =======
{ [SequelizeUniqueConstraintError: Validation error]
name: 'SequelizeUniqueConstraintError',
message: 'Validation error',
errors: [],
fields: [] }
There must be an easier way to do this. I've checked other SO questions, but the syntax has changed in the current version of sequelize.
UPDATE
Ok, I realized my mistake: I was assuming that sequelize would take care of the timestamps. This fixes the problem:
up: function (queryInterface, Sequelize) {
console.log(User);
return [
queryInterface.bulkInsert('Users', [
{ username: "user1", createdAt: Date.now(), updatedAt: Date.now() },
{ username: "user2", createdAt: Date.now(), updatedAt: Date.now() }
])
];
}
But I'm still wondering if this is the right way to seed my database. Is there a way to do it using User.create({})?
new Date()
also required for mysql, i.e.
return queryInterface.bulkInsert('users', [
{
"Forename":"A",
"Surname": "User",
"UserType":"1",
"Email":"auser#gmail.com",
"Password":"password",
"LastLogin":0,
"Tokens": JSON.stringify({"tokens":[]}),
"CreatedBy": 999,
"CreatedAt": new Date(),
"UpdatedAt": new Date()
}]);
You can use next:
const City = sequelize.define('city', {
name: { type: Sequelize.STRING },
order_: { type: Sequelize.INTEGER }
});
City.sync().then(() => {
City.create({
name: 'Neuquen',
order_: 0
});
City.create({
name: 'General Roca',
order_: 1
});
});
Or read about "migrations" at http://docs.sequelizejs.com/en/latest/docs/migrations/
An alternative could be use : sequelize fixtures , you could init your tables with default data declared as a json file or other format.
For a quick and easy way (without seeds or migrations) on sequelize v6:
I modified my sequelize.sync() call:
import { Sequelize } from 'sequelize';
// This will create an in-memory sqlite db
const sequelize = new Sequelize('sqlite::memory:', {
logging: sequelizeLogger
});
await sequelize
.sync({ force: true })
.then(() => {
// seed db
Users.create({ username: 'user1' })
});

Resources