Sequelize nested were when the subject of the include is possibly null - node.js

In my models I have a User which belongsTo({ model: Company, as: 'company' }) (and conversely Company hasMany({ model: User, as: 'members' }))
I have constructed a query as follows
const query = {
where: { onboardingState: 'pending' },
include: [
{
model: Company,
where: { suspendedAt: { [Op.eq]: null } }
}
]
}
const users = await User.findAll(query)
This works fine if the user has an associated company. But there is an edge case (admin users) where the user does not have an associated company, and in that case the findAll query returns []
How do I structure my query to only require company.suspendedAt == null if the company exists, and to ignore the company where clause if the company does not exist?

Related

Model.findAll() only returns one of the same children

I have an application that uses Node, Postgres and Sequelize. Curently I have 3 models in my DB. User, Customer and Ticket.
Ticket has a n:n relation with Customer and User.
When I'm creating a new ticket I'm supposed to pass an array of two users, each with their type in the Ticket-User relation saved to the pivot table.
The problem happens when I try to set both users as the same user, meaning that in that ticket one user played the role of two. So when I try to retrieve this info, Sequelize only returns this user one time with one of the roles played, when in fact I wanted this user to be returned 2 times, each time with it's role.
Here is how I made the query to create a new Ticket (Note that the users field has the user with id 1, playing the OFFICE and INSPECTION role):
Here is how it is created using Sequelize:
const {
interested,
opening_date,
office_status,
inspection_status,
subject,
external_code,
conclusion_method,
obs,
users,
customer_id,
is_owner
} = req.body;
try {
const ticket = await Ticket.create({
interested,
opening_date,
office_status,
inspection_status,
subject,
external_code,
conclusion_method,
obs
});
if (users && users.length > 0) {
users.forEach(({ id, type }) => {
ticket.addUser(id, { through: { type } });
});
}
if (customer_id && is_owner) {
ticket.addCustomer(customer_id, { through: { is_owner } });
}
if (ticket) {
res.status(200).json({ MSG: "SAVE_SUCCESS" });
} else {
throw new Error({ err: "Ticket was not created" });
}
} catch (error) {
if (error.errors && error.errors.length >= 1) {
let errResponse = {};
error.errors.map(err => {
errResponse = { ...errResponse, [err.path]: err.message };
});
res.status(200).json({ err: errResponse });
return;
}
res.status(500).json(error);
}
}
And it works fine, if I manually check the table in the DB it shows the user with ID 1 listed twice, one time with each role.
The problem arises when I try to query it, here is how I am doing it:
const tickets = await Ticket.findAll({
include: [
{
model: User,
attributes: ["id", "fname", "lname", "username", "email"],
through: { model: TicketsUsers, attributes: ["type"] }
},
{
model: Customer,
attributes: ["id", "name", "document"],
through: { model: TicketsCustomer, attributes: ["is_owner"] }
}
]
});
if (tickets) {
res.status(200).json(tickets);
} else {
throw new Error({ err: "EMPTY" });
}
} catch (err) {
res.status(500).json(err);
}
It works when different users are assigned to each role, however when the same user is assigned to both roles, this is what gets returned:
As you can see, the users field only lists user with id 1 one time, using the role OFFICE, when it should have listed user with id 1 two times one with each role it played on that ticket.
Like I said this only happens when I assign the same user for both roles, if different users are used for each role, then the query returns both users in the usersfield as expected, which led me to believe the code is right, I even turned on Sequelize logging and manually executed the query it returned, and sure enough both users where returned on that query. So it is doing something internally that makes a user with same id only appear once.
I have found a similar issue here: https://github.com/sequelize/sequelize/issues/7541
However the problem there was a missing id on one of his tables, which is not my case. All my tables have unique IDs.
So anyone knows why is this happening and how to solve it?
Thank you.

How to add dynamic additional attributes into a junction table with ManyToMany association in Sequelize

I created a many-to-many association by sequelize in my koa app. But I had no idea on how to create additional attributes in the junction table. Thanks.
I referred to the official doc of sequelize but didn't find a solution. In brief:
"an order can have many items"
"an item can exist in many orders"
Then I created OrderItems as junction table.
But I have trouble in inserting value into the junction table
// definitions
const Item = sequelize.define('item', itemSchema);
const Order = sequelize.define('order', orderSchema);
// junction table
const OrderItems = sequelize.define('order_item', {
item_quantity: { type: Sequelize.INTEGER } // number of a certain item in a certain order.
});
// association
Item.belongsToMany(Order, { through: OrderItems, foreignKey: 'item_id' });
Order.belongsToMany(Item, { through: OrderItems, foreignKey: 'order_id' });
// insert value
const itemVals = [{ name: 'foo', price: 6 }, { name: 'bar', price: 7 }];
const orderVals = [
{
date: '2019-01-06',
items: [{ name: 'foo', item_quantity: 12 }]
},
{
date: '2019-01-07',
items: [{ name: 'foo', item_quantity: 14 }]
}
]
items = Item.bulkCreate(itemVals)
orders = Order.bulkCreate(orderVals)
//Questions here: create entries in junction table
for (let order of orders) {
const itemsInOrder = Item.findAll({
where: {
name: {
[Op.in]: order.items.map(item => item.name)
}
}
})
order.addItems(itemsInOrder, {
through: {
item_quantity: 'How to solve here?'
}
})
}
// my current manual solution:
// need to configure column names in junction table manually.
// Just like what we do in native SQL.
const junctionValList =[]
for (let orderVal of orderVals) {
orderVal.id = (await Order.findOne(/* get order id */)).dataValues.id
for (let itemVal of orderVal.items) {
itemVal.id = (await Item.findOne(/* get item id similarly */)).dataValues.id
const entyInJunctionTable = {
item_id: itemVal.id,
order_id: orderVal.id,
item_quantity: itemVal.item_quantity
}
junctionValList.push(entyInJunctionTable)
}
}
OrderItems.bulkCreate(junctionValList).then(/* */).catch(/* */)
In case that this script it's for seeding purpose you can do something like this:
/*
Create an array in which all promises will be stored.
We use it like this because async/await are not allowed inside of 'for', 'map' etc.
*/
const promises = orderVals.map((orderVal) => {
// 1. Create the order
return Order.create({ date: orderVal.date, /* + other properties */ }).then((order) => {
// 2. For each item mentioned in 'orderVal.items'...
return orderVal.items.map((orderedItem) => {
// ...get the DB instance
return Item.findOne({ where: { name: orderedItem.name } }).then((item) => {
// 3. Associate it with current order
return order.addItem(item.id, { through: { item_quantity: orderedItem.item_quantity } });
});
});
});
});
await Promise.all(promises);
But it's not an efficient way to do it in general. First of all, there are a lot of nested functions. But the biggest problem is that you associate items with the orders, based on their name and it's possible that in the future you will have multiple items with the same name.
You should try to use an item id, this way you will be sure about the outcome and also the script it will be much shorter.

How to join 3 tables using Sequelize?

I have 3 tables named:
customers
columns are: id, customer_full_name
transaction_details
columns are: id, customer_id, amount, merchant_id
merchants
columns are: id, merchant_full_name
transaction_details table contains two foreign keys of customer_id and merchant_id.
One customer may have multiple transactions. One merchant may have multiple transactions too.
Situation:
Merchant logins to the website to view the transaction details belong to this merchant. What I would like to display is a table with the following columns:
a. Transaction ID
b. Customer Name
c. Transaction Amount
My code as below:
Merchant.findAll({
where: {
id:req.session.userId,
},
include:[{
model:TransactionDetails,
required: false
}]
}).then(resultDetails => {
var results = resultDetails;
});
My code above does not give me the result that I want. How can I fix this ?
What you need is belongsToMany association in case you haven't defined it yet. Here is the example
const Customer = sequelize.define('customer', {
username: Sequelize.STRING,
});
const Merchant = sequelize.define('merchant', {
username: Sequelize.STRING,
});
Customer.belongsToMany(Merchant, { through: 'CustomerMerchant' });
Merchant.belongsToMany(Customer, { through: 'CustomerMerchant' });
sequelize.sync({ force: true })
.then(() => {
Customer.create({
username: 'customer1',
merchants: {
username: 'merchant1'
},
}, { include: [Merchant] }).then((result) => {
Merchant.findAll({
include: [{
model: Customer
}],
}).then((result2) => {
console.log('done', result2);
})
})
});
Now result2 has all the values. Customer data can be accessed at
result2[0].dataValues.customers[0].dataValues. CustomerMerchant data is available at result2[0].dataValues.customers[0].CustomerMerchant

How to query in sequelize by an association without loading the associated object?

I have multiple models which are associated to each other.
e.g:
var User = sequelize.define("user")
var Project = sequelize.define("project")
Project.hasMany(User)
Now I want to query all Projects containing a specific user.
e.g.:
Project.findAll({
include: [
{
model: User,
where: { id }
}
]
})
This works, but loads also the user and attaches it to the project.
How can I tell sequelize, that the user should not be added to the found projects?
I just managed to address the same problem (using Sequelize 4).
You can specify that you don't want any fields of User just using attributes: [], so your code would become:
Project.findAll({
include: [
{
attributes: [],
model: User,
where: { id }
}
]
})
Many-to-many relationships are defined using the belongsToMany() method in sequelize on both sides. For your specific use case you would have to use a through model for the relation and query the through model directly.
var User = sequelize.define("user")
var Project = sequelize.define("project")
var ProjectUser = sequelize.define("projectUser")
Project.belongsToMany(User, {
through: ProjectUser
})
User.belongsToMany(Project, {
through ProjectUser
});
ProjectUser.findAll({
where: {
UserId: 'someId'
},
// We only want the project, not the user.
// You might need to do ProjectUser.belongsTo() for both
// models for this to work.
include: [Project]
})
.then(function(results) {
// Here we are getting an array of ProjectUsers, to return all the projects
// we map it to a new array of only projects.
return results.map(function(userProject) {
return userProject.Project;
});
})

Association sequelize 3 tables, many to many

I have 4 tables
- client
- project
- user
- userProject
One Project belongs to client and it needs to have client foreign key client_id.
UserProject has project_id and user_id foreign keys, belongs to project and user.
One user owns the clients of his projects.
How can I list the clients of one user?
I'm wondering you could use eager loading feature from sequalize:
Client.findAll({
include: [{
model: Project,
include: [{
model: User,
where: {
id: <UserId>
},
required: false
}]
}]
}).then(function(clients) {
/* ... */
});
This creates a many-to-many relation between user and project. Between user and client you therefor have a many-to-many-to-one relation. This is not supported by sequelize. I would have created an instance method on the User model like this:
User = sequelize.define('user', {
// ... definition of user ...
},{
instanceMethods: {
getClients: function() {
return this.getProjects().then(function (projects) {
var id_map = {};
for (var i = 0; i < projects.length; i++) {
id_map[projects[i].clientId] = 1;
}
return Client.findAll({
where: { id: [Object.keys(id_map)] }
});
});
}
}
});
and then call this function when you have a user instance like this, to get the clients:
user.getClients().then(function (clients) {
console.log(clients);
});

Resources