many-to-many relationships in sequelize can't use include? - express

So I have a project in which a User can join a Weekly Ladder so I made a belongsToMany relationship between both fields through a third model called UserLadder where I also added an extra field that keeps track of the user points at this ladder.
Basically now in my express app I need a route to shop users and their points by tournament so I wrote this:
const leaderboard = await UserLadder.findAll({
where: {
weeklyLadderId: req.params.id,
},
limit: req.params.playersNumber,
order: [['userPoints', 'DESC']],
});
res.json(leaderboard);
and it worked in the sense of it did show me everyone in this tournament and ordered them according to their points but problem was that the user showed just as a userId field and I wanted to eager load that using Include but it says that there's no relation between the user model and the userladder model.
the temporary solution I did was:
const ladder = await WeeklyLadder.findByPk(req.params.id);
const users = await ladder.getUsers({
attributes: ['displayName'],
});
res.json(users);
which I got the tournament by Id and then got there users of the tournament but this is a 2 step process and I know there's a way to do it through the third UserLadder model I just cant figure it out
any ideas?
accosiations:
const User = require('./models/User');
const WeeklyLadder = require('./models/WeeklyLadder');
const Game = require('./models/Game');
const UserLadder = require('./models/UserLadder');
User.belongsToMany(WeeklyLadder, {
through: UserLadder,
});
WeeklyLadder.belongsToMany(User, {
through: UserLadder,
});
Game.hasMany(WeeklyLadder);
WeeklyLadder.belongsTo(Game);

You can execute with just one request :
db.User.findOne({
include: [
{
model: db.WeeklyLadder,
as: "WeeklyLadder",
where: {
title: "tournament 1", // where on WeeklyLadder
},
},
],
})
On your models :
User.associate = (models) => {
models.User.belongsToMany(models.WeeklyLadder, {
as: "WeeklyLadder",
through: "UserWeeklyLadder",
foreignKey: "fk_user_id",
otherKey: "fk_weekly_ladder_id",
})
}
...
WeeklyLadder.associate = (models) => {
models.WeeklyLadder.belongsToMany(models.User, {
as: "User",
through: "UserWeeklyLadder",
foreignKey: "fk_weekly_ladder_id",
otherKey: "fk_user_id",
})
}

Related

TypeORM select data from nested relations

Using
await this.budgetRepository.createQueryBuilder("budget")
.leftJoinAndSelect("budget.contact", "contact")
.leftJoinAndSelect("contact.photo", "contactPhoto")
.getMany();
I get a list with objects like this:
Budget {
id: 1,
unnecessary_property1: something,
contact: Contact {
unnecessary_property2: something,
photo: Photo {
unnecessary_property3: something,
url: "url.com"
},
},
}
But I want to select only the necessary properties in the nested objects (relations) and get a list of objects like this:
Budget {
id: 1,
contact: Contact {
photo: Photo {
url: "url.com"
},
},
}
How is that possible with TypeORM?
This is possible but we have to select everything manually with .select()
await this.budgetRepository.createQueryBuilder("budget")
.leftJoinAndSelect("budget.contact", "contact")
.leftJoinAndSelect("contact.photo", "contactPhoto")
.select(['budget.id', 'contactPhoto.url']
.getMany();
If you're using repository pattern that you will be achieve the similar result with:
await this.budgetRepository.find({
relations: ["contact", "contact.photo"]
})
You would have to use the .select() function and pass the given properties you want for each entity.
for your example:
const user = await createQueryBuilder("budget")
.leftJoinAndSelect("budget.contact", "contact")
.leftJoinAndSelect("contact.photo", "contactPhoto")
.select([/* everything from budget */, 'contact.photo.url'....]) // added selection
.getMany();

createQueryBuilder with getOne doesn't return #JoinColumns inside table Typeorm

I have a project written by nestjs and typeorm. In my project I have tables chat and user so I want to get ManyToOne relationships. Here is the chat table
export class Chat extends BaseEntity {
#PrimaryGeneratedColumn()
id: number;
#ManyToOne(() => User, (user) => user.chatsCreater, { nullable: false })
#JoinColumn({ name: 'creatorId' })
creator: User;
#ManyToOne(() => User, (user) => user.chatsCompanion, { nullable: false })
#JoinColumn({ name: 'companionId' })
companion: User;
}
and chunk from user table
...
#OneToMany(() => Chat, (chat) => chat.creator)
chatsCreater: Chat[];
#OneToMany(() => Chat, (chat) => chat.companion)
chatsCompanion: Chat[];
...
When I query data from chat I expect to get the hole table {id, companionId, creatorI} not only value of chat.id. Here is my query
.createQueryBuilder('chat')
.where('chat.creatorId = :creatorId AND chat.companionId = :companionId', { creatorId, companionId })
.getOne()
and the result {id: 1}
So what I want is to get values of companionId and creatorId too when I query from chat.
If I change getOne() to getRaw() I get the desired output. But in case of more complex queries (with multiple joins) it becomes a mess with getRaw so is there a way to get all columns using getOne ?
I was able to do it by using leftJoinAndSelect
.createQueryBuilder('chat')
.leftJoinAndSelect('chat.creator', 'creator')
.leftJoinAndSelect('chat.companion', 'companion')
.where('chat.creatorId = :creatorId AND chat.companionId = :companionId', { creatorId, companionId })
.getOne();
In case if hole table is not needed it's also possible to use leftJoin without select and later add to query addSelect(['creator.id','companion.id'])

Sequelize: Avoid duplicates when create with manyToMany associations

I'm developing a small e-commerce to sell cinema tickets. I'm using Sequelize to design all models and their associations. The issue I'm facing is related with the route defined to create the final order:
Order.create({
userId: req.body.userId, // Number
sessionId: req.body.sessionId, // Number
seats: req.body.seats, // Array of objects
offsiteProducts: req.body.offsiteProducts // Array of objects
},
{
include: [
{
model: Seat,
attributes: ['area', 'number', 'roomId']
},
{
model: OffsiteProduct,
attributes: ['name', 'unitPrice']
}
]
}).then(order => {
res.json(order);
})
The relation between models is as follows:
User.hasMany(Order);
Order.belongsTo(User);
Session.hasMany(Order);
Order.belongsTo(Session);
Seat.belongsToMany(Order, { through: "reserved_seats" });
Order.belongsToMany(Seat, { through: "reserved_seats" });
OffsiteProduct.belongsToMany(Order, { through: ReservedOffsiteProduct });
Order.belongsToMany(OffsiteProduct, { through: ReservedOffsiteProduct });
For the "one-to-many" relationships, passing a foreign key is enough for Sequelize to associate models properly.
But for "many-to-many" associations ("belongsToMany" in Sequelize) it would duplicate the data entered for seats and offsite products and create it both as part of the order and as a new independent seat and offsite product respectively.
How can I avoid this behaviour and include the arrays of seats and offsite products only inside the final order? Thanks.
One way to do this is to execute everything in a transaction. That way, it will be atomic, i.e. "all or nothing".
Sequelize provides some methods for many-to-many associations. In this case, a separate call could be made for each junction table.
With this in mind, the following should accomplish the inserts without any duplicates:
let t = await sequelize.transaction()
try {
let order = await Order.create({
userId: req.body.userId,
sessionId: req.body.sessionId
}, {
transaction: t
})
let seats = await Seat.findAll({
where: {
id: {
[Op.in]: req.body.seatIds
}
}
})
let offsiteProducts = await OffsiteProducts.findAll({
where: {
id: {
[Op.in]: req.body.offsiteProductIds
}
}
})
await order.addSeats(seats, { transaction: t })
await order.addOffsiteProducts(offsiteProducts, { transaction: t })
await t.commit()
} catch (err) {
if (t) {
await t.rollback()
}
}
Alternatively, if the primary keys for the rows in the OffsiteProducts and Seats tables are already known, the above could be shortened to:
let t = await sequelize.transaction()
try {
let order = await Order.create({
userId: req.body.userId,
sessionId: req.body.sessionId
}, {
transaction: t
})
await order.addSeats(req.body.seatIds, { transaction: t })
await order.addOffsiteProducts(req.body.offsiteProductIds, { transaction: t })
await t.commit()
} catch (err) {
if (t) {
await t.rollback()
}
}
There's a bit more explanation for passing arrays of primary keys to the .addSeats and .addOffsiteProducts methods in the docs here.

select next and previous records in Sequelize based on createdAt

What I want: fetch one record from the database based on its ID (UUID) and retrieve next and previous records too
Associations: posts N:M categories, posts 1:N comments, posts N:1 users, posts N:M tags, posts 1:N post_views
Current solution:
const post = await PostModel.findByPk(id, {
include: [
{ model: CategoryModel, attributes: ['title'] },
{ model: TagModel },
{ model: CommentModel, include: { model: UserModel } },
{ model: PostViewModel },
{ model: UserModel, attributes: ['fullname', 'id', 'avatar'] }
]
});
const nextPost = await PostModel.findOne({
where: { createdAt: {
[Op.gt]: post.createdAt
}}
const prevPost = await PostModel.findOne({
where: { createdAt: {
[Op.lt]: post.createdAt
}}
});
but I think this is not efficient and good.

Sequelize query with a where clause on an include of an include

I'm struggling to create a query with sequelize.
Some context
I have the following models:
A Manifestation can have [0..n] Event
An Event belongs to one Manifestation (an Event cannot exist without a Manifestation)
A Place can have [0..n] Event
An Event belongs to one Place (an Event cannot exist without a Place)
A Manifestation can have [1..n] Place
A Place can have [0..n] Manifestation
I model the relations as the following:
Manifestation.hasMany(Event, { onDelete: 'CASCADE', hooks: true })
Event.belongsTo(Manifestation)
Place.hasMany(Event, { onDelete: 'CASCADE', hooks: true })
Event.belongsTo(Place)
Manifestation.belongsToMany(Place, { through: 'manifestation_place' })
Place.belongsToMany(Manifestation, { through: 'manifestation_place' })
For me it seems rather correct, but don't hesitate if you have remarks.
The question
I'm trying to query the Place in order to get all Manifestation and Event happening in a given Place. But for the Event ones, I want to include them within their Manifestation even if the Manifestation doesn't happen in the given Place.
Below is the "JSON" structure I'm trying to achieve:
{
id: 1,
name: "Place Name",
address: "Place address",
latitude: 47.00000,
longitude: -1.540000,
manifestations: [
{
id: 10,
title: "Manifestation one",
placeId: 1,
events: []
},
{
id: 11,
title: "Manifestation two",
placeId: 3,
events: [
id: 5,
title: "3333",
manifestationId: 11,
placeId: 1
]
}
]
}
So I want to include the Manifestation with id: 11, because one of its Event occurs in the given Place (with id: 1)
Update (04/06/20): For now I rely on javascript to get the expected result
I figured out it would be nice if I posted my current solution before asking.
router.get('/test', async (req, res) => {
try {
const placesPromise = place.findAll()
const manifestationsPromise = manifestation.findAll({
include: [
{ model: event },
{
model: place,
attributes: ['id'],
},
],
})
const [places, untransformedManifestations] = await Promise.all([
placesPromise,
manifestationsPromise,
])
const manifestations = untransformedManifestations.map(m => {
const values = m.toJSON()
const places = values.places.map(p => p.id)
return { ...values, places }
})
const result = places
.map(p => {
const values = p.toJSON()
const relatedManifestations = manifestations
.filter(m => {
const eventsPlaceId = m.events.map(e => e.placeId)
return (
m.places.includes(values.id) ||
eventsPlaceId.includes(values.id)
)
})
.map(m => {
const filteredEvents = m.events.filter(
e => e.placeId === values.id
)
return { ...m, events: filteredEvents }
})
return { ...values, manifestations: relatedManifestations }
})
.filter(p => p.manifestations.length)
return res.status(200).json(result)
} catch (err) {
console.log(err)
return res.status(500).send()
}
})
But I'm pretty sure I could do that directly with sequelize. Any ideas or recommendations ?
Thanks
This is not optimum. But you can try it out:
const findPlace = (id) => {
return new Promise(resolve => {
db.Place.findOne({
where: {
id: id
}
}).then(place => {
db.Manefestation.findAll({
include: [{
model: db.Event,
where: {
placeId: id
}
}]
}).then(manifestations => {
const out = Object.assign({}, {
id: place.id,
name: place.name,
address: place.address,
latitude: place.latitude,
longitude: place.longitude,
manifestations: manifestations.reduce((res, manifestation) => {
if (manifestation.placeId === place.id || manifestation.Event.length > 0) {
res.push({
id: manifestation.id,
title: manifestation.id,
placeId: manifestation.placeId,
events: manifestation.Event
})
}
return res;
}, [])
})
})
resolve(out);
})
})
}
From this, you get all manifestations that assigned to place or have any event that assigns. All included events in the manefestations are assigned to the place.
Edit :
You will be able to use the following one too:
const findPlace = (id) => {
return new Promise(resolve => {
db.Place.findOne({
include: [{
model: db.Manefestation,
include: [{
model: db.Event,
where: {
placeId: id
}
}]
}],
where: {
id: id
}
}).then(place => {
db.Manefestation.findAll({
include: [{
model: db.Event,
where: {
placeId: id
}
}],
where: {
placeId: {
$not: id
}
}
}).then(manifestations => {
place.Manefestation = place.Manefestation.concat(manifestations.filter(m=>m.Event.length>0))
resolve(place);// or you can rename, reassign keys here
})
})
})
}
Here I take only direct manifestations in the first query. Then, manifestations that not included and concatenate.
I do not know if you figure it out by now. But the solution is provided below.
Search with Sequelize could get funny :). You have to include inside another include. If the query gets slow use separate:true.
Place.findAll({
include: [
{
model: Manifestation,
attributes: ['id'],
include: [{
model: Event ,
attributes: ['id']
}]
},
],
})
I tried to complete it in a single query but you will still need JavaScript to be able to get the type of output that you want.
(Note: 💡 You need manifestation which is not connected to places but should be included if a event is present of that place. The only SQL way to get that starts by doing a CROSS JOIN between all tables and then filtering out the results which will be a very hefty query)
I came up with this code(tried & executed) which doesn't need you to execute 2 findAll that fetches all data as what you are currently using. Instead it fetched only the data needed for final output in 1 query.
const places = await Place.findAll({
include: [{
model: Manifestation,
// attributes: ['id']
through: {
attributes: [], // this helps not get keys/data of join table
},
}, {
model: Event,
include: [{
model: Manifestation,
// attributes: ['id']
}],
}
],
});
console.log('original output places:', JSON.stringify(places, null, 2));
const result = places.map(p => {
// destructuring to separate out place, manifestation, event object keys
const {
manifestations,
events,
...placeData
} = p.toJSON();
// building modified manifestation with events array
const _manifestations = manifestations.map(m => {
return ({ ...m, events: [] })
});
// going through places->events to push them to respective manifestation events array
// + add manifestation which is not directly associated to place but event is of that manifestation
events.map(e => {
const {
manifestation: e_manifestation, // renaming variable
...eventData
} = e;
const mIndex = _manifestations.findIndex(m1 => m1.id === e.manifestationId)
if (mIndex === -1) { // if manifestation not found add it with the events array
_manifestations.push({ ...e_manifestation, events: [eventData] });
} else { // if found push it into events array
_manifestations[mIndex].events.push(eventData);
}
});
// returning a place object with manifestations array that contains events array
return ({ ...placeData, manifestations: _manifestations });
})
// filter `.filter(p => p.manifestations.length)` as used in your question
console.log('modified places', JSON.stringify(result, null, 2));