sequelize select attributes from the junction table - node.js

I have been searching for a solution for hours now without hope.
I have Many-to-Many relationship with Student and Classes through a Junction Table Enrollment.
Everything is working fine and I am able to insert and retrieve data.
However, I don't want all the fields from the junction table to be returned in the result. Only selected attribute.
First, the below is my code:
Models definition and associations:
const Student = sequelize.define(
"Student",
{ firstName: Sequelize.STRING },
{ timestamps: false }
);
const Class = sequelize.define(
"Class",
{ className: Sequelize.STRING },
{ timestamps: false }
);
const Enrollment = sequelize.define(
"Enrollment",
{
enrollmentType: {
type: DataTypes.STRING,
},
},
{ timestamps: false }
);
Student.belongsToMany(Class, { through: Enrollment });
Class.belongsToMany(Student, { through: Enrollment });
The query:
return Student.findOne({
where: { id: 2 },
include: [
{
model: Class,
attributes: ["className"],
},
],
raw: true,
nest: true,
});
And I got the below result:
{
id: 2,
firstName: 'John',
Classes: {
className: 'Chemistry',
Enrollment: { enrollmentType: 'Normal student', StudentId: 2, ClassId: 1 }
}
}
I am not interested in the repeated StudentId and ClassId returned from the Enrollment table.
Someone suggested that I use through:
return Student.findOne({
where: { id: 2 },
include: [
{
model: Class,
attributes: ["className"],
through: { attributes: ["enrollmentType"] },
},
],
raw: true,
nest: true,
});
But now I am getting the same result, only the order of fields seems to be different
{
id: 2,
firstName: 'John',
Classes: {
className: 'Chemistry',
Enrollment: { ClassId: 1, StudentId: 2, enrollmentType: 'Normal student' }
}
}
How can I only return the enrollmentType without other Enrollment fields?

I am using #Emma response in the comment as the solution to my issue.
This is absolutely correct. I created a simple ExpressJS API which returned the results accurately and fully.
As Emma mentioned, it is a console limitation.
See her full reply in the comments on the question.

Related

Sequelize create instance of object Many-to-many relationship

I'm using Sequelize to create an instance of Many-to-Many relationship, but it not working.
First, I have a model with two class as below:
Room:
Room.init({
id: {
type: DataTypes.UUID,
primaryKey: true,
defaultValue: DataTypes.UUIDV4,
},
name: {
type: DataTypes.STRING,
allowNull: false,
},
}, { sequelize, modelName: "Room" }
);
User:
User.init(
{
id: {
type: DataTypes.UUID,
primaryKey: true,
defaultValue: DataTypes.UUIDV4,
},
name: {
type: DataTypes.STRING,
allowNull: false,
},
},
{ sequelize, modelName: "User"}
);
UserRoom:
UserRoom.init("UserRoom", { sequelize });
Relationship:
User.belongsToMany(Room, { as:'container', through: UserRoom, foreignKey: "userId" });
Room.belongsToMany(User, { as:'joiners', through: UserRoom, foreignKey: "roomId" });
And the UserRoom has another key with Message Model:
UserRoom.belongsTo(Message, { as: "lastReadMessage" });
Ok, now when i create a new Room with code:
const room = await Room.create(
{
name: name,
avatarId: avatarUri,
joiners: [
{
id: '1',
},
{
id: '2',
},
],
},
{
include: [{ association: User, as: 'joiners' }],
}
);
I get an error:
TypeError: Cannot read properties of undefined (reading 'name')
I don't know why error, I try this example but it still wrong.
My question is: How I create object and link to another relationship?
UPDATE
Now, I can run it with below code:
const room = await Room.create({
name: name,
avatarId: avatarUri,
});
await UserRoom.bulkCreate(
[
{
lastReadMessageId: null,
userId: 1,
roomId: room.id,
},
{
lastReadMessageId: null,
userId: 2,
roomId: room.id,
},
]
);
You're trying to create users with id equals to 1 and 2 along with creating a room. If you wish to associate existing users with a new room then you need to call addJoiners of a new room model:
const room = await Room.create(
{
name: name,
avatarId: avatarUri,
);
// pass user instances or ids.
await room.addJoiners([1,2])

Count in Many-to-Many relationship

I want to count the number of likes when I fetch the post details.
User Model
User.init(
{
id: {
allowNull: false,
primaryKey: true,
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
},
userName: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
field: 'user_name',
},
}
);
static associate(models) {
// User has many posts --> One-to-Many Relationship
this.hasMany(models.Post, {
foreignKey: 'userId',
});
// Likes relationship
this.belongsToMany(models.Post, {
through: 'PostLikes',
foreignKey: 'userId',
as: 'likes',
});
}
And here is the Post Model
Post.init(
{
id: {
type: DataTypes.UUID,
primaryKey: true,
allowNull: false,
defaultValue: DataTypes.UUIDV4,
},
body: {
type: DataTypes.STRING,
allowNull: false,
},
}
);
static associate(models) {
// User-Post Relationship
this.belongsTo(models.User, {
foreignKey: 'userId',
});
// Likes Relationship
this.belongsToMany(models.User, {
through: 'PostLikes',
foreignKey: 'postId',
as: 'likes',
});
}
So, it now creates a joined table PostLikes and I am trying query that fetches the post along with the likes and number of likes on that post.
const postsIdCol = '"likes->PostLikes"."postId"';
const usersIdCol = '"likes->PostLikes"."userId"';
const postCol = '"Post"."id"';
const response = await Post.findOne({
where: {
id: postid,
},
// includeIgnoreAttributes : false,
attributes: [
'id', 'body', 'createdAt',
[sequelize.literal(`COUNT(${postsIdCol}) OVER (PARTITION BY ${postCol})`), 'likesCount'],
],
include: [
{
model: Comment, ----> Post is also associated with comments, ignore this
as: 'comments',
attributes: ['id', 'content', 'createdAt', 'updatedAt'],
},
{
model: User,
as: 'likes',
through: {
attributes: [],
},
attributes: ['id', 'userName'],
},
],
});
return response;
The Response I am getting on doing this query is like this :
{
"data": {
"id": "182d6377-5bf6-4b65-9e29-cb79acc85c0a",
"body": "hoping for the best",
"likesCount": "6", -----> this should be 3
"likes": [
{
"id": "1af4b9ea-7c58-486f-a37a-e46461487b06",
"userName": "sdfbsd",
},
{
"id": "484202b0-a6d9-416d-a8e2-6681deffa3d1",
"userName": "ndnadonfsu",
},
{
"id": "b3c70bee-e839-4449-b213-62813af031d1",
"userName": "difniandca",
}
]
}
}
You need another PARTITION BY column from the other association than the one that you want to count.
For example, if you want to count the likes, you need to partition by parent id (Post.id) and other association id (Comment.id).
If you want to count the comments, you need to partition by parent id (Post.id) and other association id ("likes->PostLikes"."UserId").
[Sequelize.literal(`COUNT(${postsIdCol}) OVER (PARTITION BY ${postCol}, "Comments"."id")`), 'likesCount']
Where it says "Comments", you need to add your comment table name.

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: Bind multiple models for join query and create custom columns

I'd like to apply a join and groupBy in Sequelize v5 that will fetch the records from five models tables and return the records as below format.
{
"data": [
{
"products": {
"id": "01",
"name": "lorium",
"description": "ipsum",
"product_images": [
{
"url": "", // From images tbl
"image_type": "Front" // From imge_types tbl
},
{
"url": "",
"image_type": "Back"
}
]
},
"vendor": {
"first_name": "john",
"last_name": "doe"
}
}
]
}
I have created separate all five models and assign association to them.
Product Model::
const Product = SQLize.define('product', {
id: { type: DataTypes.INTEGER.UNSIGNED, autoIncrement: true, primaryKey: true, }
product_title: { type: new DataTypes.STRING(255) },
vendor_id: { type: DataTypes.INTEGER.UNSIGNED }
});
Product.hasMany(ProductImages, {foreignKey: 'product_id', targetKey: 'id', as :'product_img_refs'})
export { Product };
Vendor Model::
const Vendor = SQLize.define('vendor', {
id: { type: DataTypes.INTEGER.UNSIGNED, autoIncrement: true, primaryKey: true, },
first_name: { type: DataTypes.STRING(100) },
last_name: { type: DataTypes.STRING(100) }
});
Product.belongsTo(Vendor, {foreignKey: 'id'})
Vendor.hasOne(Product, {foreignKey: 'id'})
export { Vendor }
Product Images Model::
const ProductImages = SQLize.define('product_images', {
id: { type: DataTypes.INTEGER.UNSIGNED, autoIncrement: true, primaryKey: true, },
product_id: { type: DataTypes.INTEGER },
product_image_id: { type: DataTypes.INTEGER }
img_type_id: { type: DataTypes.INTEGER }
});
export {ProductImages}
Images Model::
const ImagesModel = SQLize.define('images', {
id: { type: DataTypes.INTEGER.UNSIGNED, autoIncrement: true, primaryKey: true, },
img_url: { type: DataTypes.STRING }
});
export { ImagesModel }
Image Types Model::
const ImageTypes = SQLize.define('image_types', {
id: { type: DataTypes.INTEGER.UNSIGNED, autoIncrement: true, primaryKey: true, },
image_type: { type: DataTypes.STRING }
});
export { ImageTypes }
Below is the repository file on which i have performed the SQLize operation: Updated::
public async getProductData() {
var prodData = Product.findAll({
include: [
{ model: Vendor, as: 'vendor' },
{ model: ProductImages, as: 'product_img_refs' }
]
});
return prodData;
}
I am not getting the correct way to bind the all models that will return me a result as described in the above json format.
To get the nested output as shown in the question, you would need to create associations between the following:
ProductImages and ImagesModel
ProductImages and ImageTypes
Once done, you can nest the models in the findAll options as shown below:
// Create associations (depends on your data, may be different)
ProductImages.hasOne(ImagesModel);
ProductImages.hasOne(ImageTypes);
// Return product data with nested models
let prodData = Product.findAll({
include: [
{ model: Vendor, as: 'vendor' },
{
model: ProductImages, as: 'product_img_refs',
include: [
{ model: ImagesModel }, // Join ImagesModel onto ProductImages (add aliases as needed)
{ model: ImageTypes } // Join ImageTypes onto ProductImages (add aliases as needed)
]
}
]
});
I found the issue. you were trying to include ProductImages model into Vendor. As per your association, ProductImages associate with Product not with Vendor.
So please try this
let prodData = Product.findAll({
include: [
{ model: Vendor, as: 'vendor' },
{ model: ProductImages }
]
});

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.

Resources