How to create a query in sequelize - node.js

I have a Lessons table:
const Lesson = sequelize.define(
'lesson',
{
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
date: { type: DataTypes.DATEONLY },
title: { type: DataTypes.STRING },
status: { type: DataTypes.INTEGER }
},
{ underscored: true, timestamps: false }
);
Which has a belongsToMany relationship to the Teachers table
const Teacher = sequelize.define(
'teacher',
{
id: {
type: DataTypes.INTEGER,
primaryKey: true
},
name: { type: DataTypes.STRING }
},
{ underscored: true, timestamps: false }
);
I am trying to make a request to get a list of lessons:
const data = await Lesson.findAll({
include: [
{ model: Teacher, through: { attributes: [] } }
],
order: [['id', 'DESC']],
limit,
offset
});
How to make a limit on the number of teachers, that is, to accept only those lessons where the number of teachers belonging to this lesson, for example, is equal to 3.

Your query will form something like this
const data = await Lesson.findAll({
include: [
{
model: Teacher,
required: true
attributes: [[Sequelize.literal('(count(teacher.id))')], 'teacherCount']
}
],
order: [['id', 'DESC']],
having: {teacherCount: {[Op.gt]: 3}}
limit: 10,
offset: 0
});
This is still a raw code you might want to do some trial and error on your own, basically you want to do a join and get count of teachers from that join once you have it you just want to filter your query using that virtual column you just created.
P.S: Above code will maybe throw group by error for that you will need to add a group: ['column_name'] so that non-aggregated columns come along in aggreation query
Reference for group by:
https://www.postgresqltutorial.com/postgresql-group-by/
Lastly, I would suggest writing a raw query first and then finding ways to convert that into sequelize that way you know what is the exact issue that you are facing.

Related

Sequelize.js - choosing the right relationship

I'm using NodeJS and sequelize.
I have user table with a column named duel_id, and each user can be assigned to one duel at a time.
Each duel can have multiple users in it.
I have the following User Model:
const User = Model.define(
'User',
{
user_id: {
type: DataType.INTEGER,
primaryKey: true,
},
username: {
type: DataType.STRING(255),
},
character: {
type: DataType.INTEGER,
},
duel_id: {
type: DataType.INTEGER,
},
},
{
indexes: [{ fields: ['user_id', 'username'] }],
tableName: 'users',
timestamps: false,
},
);
User.hasOne(Duel, { as: 'duel', foreignKey: 'id', sourceKey: 'duel_id' });
with the following Duel model:
const Duel = Model.define(
'DuelRoom',
{
id: {
type: DataType.INTEGER,
primaryKey: true,
autoIncrement: true,
},
round_id: {
type: DataType.INTEGER,
},
status: {
type: DataType.STRING,
},
turn_of_user_id: {
type: DataType.INTEGER,
},
winner: {
type: DataType.STRING,
},
},
{
indexes: [{ fields: ['id'] }],
tableName: 'duel_rooms',
timestamps: true,
},
);
The above code works and return the user and the associated duel if he has one.
I want also to return all the users associate to the same duel.
I tried to connect the relationship with hasMany/ belongsTo with no success. The following errors appears:
Error: DuelRoom.hasMany called with something that's not a subclass of Sequelize.Model
I want to be able to query to get the data like this:
user: {
user_id,
username
duel: {
round_number
players: [{user_id, username}]
}
}
Get the current user with the duel info, with all players associated with the same duel_id as an array named players.
Any idea of how I can define such a relation using sequelize to return all users associated to the user duel?
If a User model has dual_id then you should use belongTo from User to DualRoom instead of hasOne:
User.belongsTo(Duel, { as: 'duel', foreignKey: 'duel_id' });
If you wish to have users collection in a Duel model then this will work with the following hasMany:
Duel.hasMany(User, { as: 'users', foreignKey: 'duel_id' });
Take into account that you should register all associations AFTER all model registrations like I advised in this answer
After all this setup you can get what you wish by executing a query like this:
const user = await User.findOne({
where: {
user_id: id
},
include: [{
model: Duel,
as: 'duel',
include: [{
model: User,
separate: true,
as: 'users'
}]
}]
})
As each user can have a duel and one duel can be associated with many users. It is a one-to-many association, so, you should try:
Duel.hasMany(User);
User.belongsTo(Duel);

Sequelize - include all association filtered results

I have a a model called Post and Country. When I filter by a particular country Id, it returns correctly filtered posts but only returns that specific country in the response and discards all the other countries associated with the Post. How can I include and retain all the countries associated with the post in the response?
Post.js
class Post extends Sequelize.Model {
static init(sequelize, DataTypes) {
return super.init(
{
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true
},
number: {
type: Sequelize.INTEGER,
validate: {
isInt: true
}
},
}
);
static associate(models) {
this.myAssociation = this.belongsToMany(models.Country,
{through: "CountriesImpacted", foreignKey: "id"});
}
}
module.exports = Incident;
Country.js
class Country extends Sequelize.Model {
static init(sequelize, DataTypes) {
return super.init(
{
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
name: {
type: Sequelize.STRING,
primaryKey: true,
unique: true
}
}, {sequelize, modelName: 'country', tableName: 'Countries'}
)
}
}
module.exports = Country;
routes.js
Post.findAndCountAll({
attributes: { exclude: ["createdAt", "updatedAt"] },
order: [Country, "name", "asc"],
include: [{
model: Country,
where: { id: country_ids_from_request }
}],
where: filters,
distinct: true,
offset: offset,
limit: limit
})
example output
{
'0': {
id: 1,
number: 1203021,
countries: [
{
id: 6,
name: 'Australia',
CountriesImpacted: {
id: 1,
countryId: 6
}
},
{
id: 7,
name: 'New Zealand',
CountriesImpacted: {
id: 1,
countryId: 7
}
}
]
}
}
when I filter by country id 6, the repsonse will discard New Zealand...
It requires a sub-query to fetch the eligible Post model. It will look like following
return Post.findAll({
include: [
{
model: Country
}
],
where: {
id : {
[Sequelize.Op.in] : [Sequelize.literal(`(SELECT posts.id FROM posts INNER JOIN countries ON countries.postId = posts.id WHERE ${COUNTRY_ID} IN (countries.id))`)] // Subquery...
}
}
})
Here i assume following
Your Post schema name is posts.
Your Country schema name is countries.
Country has a postsId foreign key REFERENCES TO Post.
If you are not comfortable with having sub-query inside your project while using Sequelize (ORM), then you have to execute two query, first fetching all the eligible Posts then fetch desired for those Posts.

Sequelize : Only returning one of many result for a nested include

I have 3 tables:
Job post
recruitment phase
Interview slot
Their association is,
jobpost has many recruitment phases and
recruitment phase has many interview slots
I am able to get all the recruitment phases of a job post by including the recruitmentphase association and group clause.
const jobPosts = await JobPost.unscoped().findAll({
where,
include: [
{
model: db.RecruitmentPhase,
include: [{
model: db.InterviewSlot,
},
],
group: ['RecruitmentPhases.id'],
});
But I am only getting one interview slot for the recruitment phase, event though there are many interviewslots for that recruitment phase.
I tried to do group clause inside include.
const jobPosts = await JobPost.unscoped().findAll({
where,
include: [
{
model: db.RecruitmentPhase,
group: ['InterviewSlots.id'],
include: [{
model: db.InterviewSlot,
},
],
group: ['RecruitmentPhases.id'],
});
but it also giving only one interview slot
EDIT
jobpost model :
module.exports = (sequelize, DataTypes) => {
const jobPost = sequelize.define('JobPost', {
id: {
type: DataTypes.BIGINT,
allowNull: true,
autoIncrement: true,
primaryKey: true,
},
jobTitle: {
type: DataTypes.STRING(150),
allowNull: true,
},
}, {
timestamps: true,
defaultScope: {
attributes: { exclude: ['createdAt', 'updatedAt'] },
},
});
jobPost.associate = (models) => {
jobPost.hasMany(models.RecruitmentPhase);
};
return jobPost;
};
Recruitment phase model :
module.exports = (sequelize, DataTypes) => {
const recruitmentPhase = sequelize.define('RecruitmentPhase', {
id: {
type: DataTypes.BIGINT,
allowNull: true,
autoIncrement: true,
primaryKey: true,
},
phaseName: {
type: DataTypes.STRING(200),
allowNull: true,
},
}, {
timestamps: true,
});
recruitmentPhase.associate = (models) => {
recruitmentPhase.belongsTo(models.JobPost);
recruitmentPhase.hasMany(models.InterviewSlot);
};
return recruitmentPhase;
};
Interview slot model :
module.exports = (sequelize, DataTypes) => {
const interviewSlot = sequelize.define('InterviewSlot', {
id: {
type: DataTypes.BIGINT,
allowNull: true,
autoIncrement: true,
primaryKey: true,
},
interviewDate: {
type: DataTypes.DATE,
allowNull: true,
},
});
interviewSlot.associate = (models) => {
interviewSlot.belongsTo(models.RecruitmentPhase);
};
return interviewSlot;
};
Remove group: ['RecruitmentPhases.id'], in order to see the details of InterviewSlots. As is, you're seeing a summary of interview slots...
So, I am not quite sure about, this is the correct answer and is the correct way to do it.
But When I do the grouping for that nested table 'interviewslot' with this '->' , It worked.
group: ['RecruitmentPhases.id', 'RecruitmentPhases->InterviewSlots.id'],
Why do you seem to be fetching JobPost instead of RecruitmentPhase if that is what you are looking to fetch from the database?
From what I understand - you are looking to fetch all the RecruitmentPhases with their job post accompanying the InterviewSlots allocated for each RecruitmentPhase.
Possible Code:
(Updated)
const rec = await db.RecruitmentPhase.findAll({
include:[{model: db.JobPost, where:{ id:job_id }}, {model: db.InterviewSlot}]
});
res.json(rec)
//Expected JSON Data
{
"RecruitmentPhase":[
{
"id":1,
"phaseName":"Phase 1",
"JobPosts": {
"id":1,
"jobTitle":"XYZ"
},
"InterviewSlots":[
{//inteview slot #1 data},
{//interview slot #2 data}
]
},
{
"id":2,
"phaseName":"Phase 2",
"JobPosts": {
"id":1,
"jobTitle":"XYZ"
},
"InterviewSlots":[
{//inteview slot #1 data},
{//interview slot #2 data}
]
}
]
}

Sequelize format of join query response

I have following models defined for Sequelize
const brand = sequelize.define('brands', {
brand_id: {
type: Sequelize.UUID,
primaryKey: true
},
name: Sequelize.STRING
}, {
timestamps: false,
underscored: true
});
const model = sequelize.define('models', {
model_id: {
type: Sequelize.UUID,
primaryKey: true
},
name: Sequelize.STRING
}, {
timestamps: false,
underscored: true
});
model.belongsTo(brand, {foreignKey: 'brand_id'});
I use this code to query the database
model.findOne({
raw: true,
where: {name: 'TIPO'},
include: brand})
.then(car => {
console.log(car);
});
And it returns the rows formatted like this
{ model_id: '7e5a29ba-05b1-45f7-9fee-41f8440fe975',
name: 'TIPO',
brand_id: 'f3e4962c-906f-46c4-b992-7375ab46002a',
'brand.brand_id': 'f3e4962c-906f-46c4-b992-7375ab46002a',
'brand.name': 'FIAT' }
While I would really like for it to look more like this
{ model_id: '7e5a29ba-05b1-45f7-9fee-41f8440fe975',
model_name: 'TIPO',
brand_name: 'FIAT' }
Is there a way to achieve make it work like this?
You need to use the attributes option
model.findOne({
raw: true,
where: {name: 'TIPO'},
include: {
model: model.brand,
attributes: [ 'name' ], // represents brand.name
}
})
.then(car => {
const newCar = car;
// if you want to change/rename the object properties around, do so here via newCar
console.log(newCar);
});
You may want to consider changing "name" in the brand model to bName or brandName to avoid confusion.
You need to add attributes to both model and brand... without these, you will see all fields (e.g. SELECT *). Take a look at the doc

How to add array of ids in a query with many to many relationships Sequelize

I have 2 entities and one for n:m relationship:
const Item = db.define('item', {
id: {
type: Sequelize.BIGINT,
primaryKey: true,
autoIncrement: true,
},
title: Sequelize.STRING,
description: Sequelize.STRING,
price: {
type: Sequelize.FLOAT,
defaultValue: 0.0,
},
});
const Category = db.define('category', {
id: {
type: Sequelize.BIGINT,
primaryKey: true,
autoIncrement: true,
},
title: Sequelize.STRING,
});
const ItemCategory = db.define('item_category', {
id: {
type: Sequelize.BIGINT,
primaryKey: true,
autoIncrement: true,
},
category_id: {
type: Sequelize.BIGINT
},
item_id: {
type: Sequelize.BIGINT
}
});
And relations:
Category.belongsToMany(Item, {
through: {
model: ItemCategory,
unique: false
},
foreignKey: 'category_id',
constraints: false
});
Item.belongsToMany(Category, {
through: {
model: ItemCategory,
unique: false
},
foreignKey: 'item_id',
constraints: false
});
Association is working fine(I guess). But when I try to query Item, the result comes without categories field.
Also I can add include option and it returns category objects:
Item.findAll({ include: [{ model: Category }] })
The QUESTION IS: How to associate ONLY categories_ids when querying Item objects to have something like this in response:
{
id: 1,
categories_ids: [1,3,4],
price: 20
}
You can't actually do that because of how data of nested associations are arranged by default.
Suppose you did
Item.findOne({
where: { item_id: 1},
include: [{
model: ItemCategory,
as: 'categories_ids',
attributes: ['category_id']
}]
})
You will get
{
id: 1,
categories_ids: [
{ category_id: 1 },
{ category_id: 2 },
{...},
{...}
],
price: 20,
}
Of which you can probably re-arrange the information, which involves the process of something like this:
let pojo = JSON.parse(JSON.stringify(data))
pojo.categories_ids.forEach(function(el, index) {
pojo.categories_ids[index] = el.category_id
})
Try something like this
associate your through model directly to item as well so you can include in query
Item.hasMany(ItemCategory, {
foreignKey: 'item_id',
constraints: false
});
Item.findAll({
include: [{
model: ItemCategory,
as: 'categories_ids',
attributes: ['category_id']
}]
});

Resources