And condition on Join table on the same field multiple times in Sequelize NodeJS? - node.js

I have one table called Users. One user can be present on multiple Email Group.
User Table
id, name
1, Sudhir Roy
2, Rahul Singh
Email Group Table
id, emailType, userID
1, Promotional, 1
2, Advertisement, 2
3, Advertisement, 1
Need to get all users based on Email Type. (Email type is dynamic and send in array form like ["Promotional", "Advertisement"]
How can we find users based on the emailGroup.
["Advertisement"] return [1, 2] from user table
["Advertisement", "Promotional"] return [1] from user table.
I tried to use [Op.and] and [Op.in]. Both are not working.
Here is my sample code.
const user = User.findAll({
where: {
"$emailGroup.emailType$": {
[Op.in]: emailGroup // Array from client
},
},
include: {
model: EmailGroup
}
})
It works well when the email group array is single but not working when we try to find more than one email group.

You can also add where clauses to the include object.
The resulting findAll would then become:
const user = User.findAll({
include: [
{
model: EmailGroup,
where: {
emailType$: {
[Op.in]: emailGroup, // Array from client
},
},
},
],
});
As shown here in the docs: https://sequelize.org/v5/manual/querying.html#relations---associations

Related

How to delete nested entities in TypeORM and Nest.js

I have one-to-many relation:
class User {
#PrimaryGeneratedColumn()
id: number;
#Column()
name: string;
#OneToMany(() => Phone, phone => phone.user, {cascade: true})
phones?: Phone[];
}
Assume current data in database is following: user has two phone numbers:
{
"id": 1,
"name": "Jhon",
"phones": [
{"id": 1, "phone": "123456"},
{"id": 2, "phone": "567809"}
]
}
I build UI (web-interface) and from web-interface I want to have possibility to delete a phone number from user profile. So I do it and following POST request comes from UI: second phone number deleted
{
"id": 1,
"name": "Jhon",
"phones": [
{"id": 1, "phone": "123456"}
]
}
How to delete phone number id=2 from database?
I use Nest CRUD module, as I understand from source code it just merges a new data from request with current data from database, so merging two phone numbers from database with the only phone number from request gives us array of tho phone numbers again and nothing deleted :frowning:
I tried to do it manually and there are a lot of code!
#Override()
async updateOne(
#ParsedRequest() req: CrudRequest,
#ParsedBody() dto: UpdateUserDto,
#Param('id') id: number,
): Promise<Shipment> {
// use standart Nest.js way to save new user data into database
await this.service.updateOne(req, dto);
// load fresh user data from database
const user = await this.service.findOne(id);
const phonesFromUi = dto.phones;
const phonesToBeDeleted = [];
// loop phone numbers from database - detect which to be deleted
user.phones.forEach((phoneFromDb) => {
const hasThisPhoneOnUi = phonesFromUi.find((phoneFromUi) => phoneFromUi.id === phoneFromDb.id);
if (!hasThisPhoneOnUi) {
// looks like this phone number was deleted on ui, so delete it from database too
phonesToBeDeleted.push(phoneFromDb);
}
});
// actually delete phone numbers from database
await this.connection.getRepositoryFor(Phone).remove(phonesToBeDeleted);
// reload fresh user data from database to get fresh list of phone numbers after delition some of them
const user = await this.service.findOne(id);
return user;
}
Is there any way to do it via TypeORM build in functions or methods? How to delete some elements from nested array?
There is no very good support in typeorm for doing a join update, what I advise you to do is receiving the phones parameter and get a select of the phones that are related to the UserId and then delete the ids that are not in the new array:
const phones = await this.phoneRepository.find({userId:1});
const toDeletePhones = phones.filter((element)) => {
return updatePhones.indexOf(element) === -1;
}
this.phoneRepository.remove(toDeletePhones);
then procede to update the user or yo can even do it before this

How to update a key of object with value in json column with knexjs?

I'm trying to update a column in users table the column type is json.
column name is test.
and the column consists of an object default value for example is
{a: "text", b: 0}
how to update let's say the object key b without changing the whole column
the code i'm using is
knexDb('users').where({
email: email
})
.update({
test: { b: 1 }
})
second solution
knexDb('users').where({
email: email
})
.update({
test: knexDb.raw(`jsonb_set(??, '{b}', ?)`, ['test', 1])
})
first solution changes the whole column cell and test will be only { b: 1 }
second solution doesn't work it give an error
function jsonb_set(json, unknown, unknown) does not exist
The expected result
is to manage to update only a certain key value in an object without changing the whole object.
PS
I also want to update an array that consists of objects like the above one for example.
[{a: "text", b: 0}, {c: "another-text", d: 0}]
if i use the code above in kenxjs it'll update the whole array to only {b: 1}
PS after searching a lot found that in order to make it work i need to set column type to jsonb, in order the above jsonb_set() to work
but now i'm facing another issue
how to update multiple keys using jsonb_set
knexDb('users').where({
email: email
})
.update({
test: knexDb.raw(`jsonb_set(??, '{b}', ?)`, ['test', 1]),
test: knexDb.raw(`jsonb_set(??, '{a}', ?)`, ['test', "another-text"]),
})
the first query key b is now not updating, in fact all updates don't work except the last query key a, so can some explain why ?
Your issue is that you're overwriting test. What you're passing into update is a JS object (docs). You cannot have multiple keys with identical values (docs). You'll have to do something like this where you make 1 long string with all your raw SQL as the value to test.
knexDb('users').where({
email: email
})
.update({
test: knexDb.raw(`
jsonb_set(??, '{a}', ?)
jsonb_set(??, '{b}', ?)
`,
['test', "another-text", 'test', 1])
})
Probably a better option exists - one that would be much more readable if you have to do this for several columns is something like what I have included below. In this example, the column containing the jsonb is called json.
const updateUser = async (email, a, b) => {
const user = await knexDb('users')
.where({ email })
.first();
user.json.a = a;
user.json.b = b;
const updatedUser = await knexDb('users')
.where({ email })
.update(user)
.returning('*');
return updatedUser;
}
Update/insert a single field in a JSON column:
knex('table')
.update( {
your_json_col: knex.jsonSet('your_json_col','$.field', 'new value')
})
.where(...)
Update/insert multiple fields
Option 1 (nested)
knex('table')
.update( {
your_json_col: knex.jsonSet(knex.jsonSet('your_json_col','$.field1', 'val1')
'$.field2', 'val2')
})
.where(...)
Option 2 (chained)
knex('table')
.update( {
your_json_col: knex.jsonSet('your_json_col','$.field1', 'val1')
})
.update( {
your_json_col: knex.jsonSet('your_json_col','$.field2', 'val2')
})
.where(...)

Joining same table multiple times with Sequelize

I have the following models:
const User = Sequelize.define('user', {
login: Sequelize.DataTypes.STRING,
password: Sequelize.DataTypes.STRING,
is_manager: Sequelize.DataTypes.BOOLEAN,
notes: Sequelize.DataTypes.STRING
});
const Bike = Sequelize.define('bike', {
model: Sequelize.DataTypes.STRING,
photo: Sequelize.DataTypes.BLOB,
color: Sequelize.DataTypes.STRING,
weight: Sequelize.DataTypes.FLOAT,
location: Sequelize.DataTypes.STRING,
is_available: Sequelize.DataTypes.BOOLEAN
});
const Rate = Sequelize.define('rate', {
rate: Sequelize.DataTypes.INTEGER
});
Rate.belongsTo(User);
User.hasMany(Rate);
Rate.belongsTo(Bike);
Bike.hasMany(Rate);
And I'd like to select bikes with their average rates, plus rates of the current user for each bike:
Bike.findAll({
attributes: {include: [[Sequelize.fn('AVG', Sequelize.col('rates.rate')), 'rate_avg']],
},
include: [{
model: Rate,
attributes: []
}, {
model: Rate,
attributes: ['rate'],
include: [{
model: User,
attributes: [],
where: {
login: req.user.login
}
}]
}],
group: Object.keys(Bike.rawAttributes).map(key => 'bike.' + key) // group by all fields of Bike model
})
It constructs the following query: SELECT [bike].[id], [bike].[model], [bike].[photo], [bike].[color], [bike].[weight], [bike].[location], [bike].[is_available], AVG([rates].[rate]) AS [rate_avg], [rates].[id] AS [rates.id], [rates].[rate] AS [rates.rate] FROM [bikes] AS [bike] LEFT OUTER JOIN [rates] AS [rates] ON [bike].[id] = [rates].[bikeId] LEFT OUTER JOIN ( [rates] AS [rates] INNER JOIN [users] AS [rates->user] ON [rates].[userId] = [rates->user].[id] AND [rates->user].[login] = N'user' ) ON [bike].[id] = [rates].[bikeId] GROUP BY [bike].[id], [bike].[model], [bike].[photo], [bike].[color], [bike].[weight], [bike].[location], [bike].[is_available];
And fails: SequelizeDatabaseError: The correlation name 'rates' is specified multiple times in a FROM clause.
How do I write the query right? I need Sequelize to assign another alias to the rates table used in the 2nd join (and add its columns to the GROUP BY clause, but that's the next step).
You can do multiple inner joins with same table by adding extra same association with that model but with a different alias that is as: 'alias1' , as: 'alias2' ,... - all this existing with the same model + same type of association.
Also posted this solution at github issue: https://github.com/sequelize/sequelize/issues/7754#issuecomment-783404779
E.g. for Chats that have many Receiver
Associations (Duplicating for as many needed)
Chat.hasMany(Receiver, {
// foreignKey: ...
as: 'chatReceiver',
});
Chat.hasMany(Receiver, {
// foreignKey: ...
as: 'chatReceiver2',
});
Now you are left to include associated model multiple times all with different alias so it does not gets overridden.
So you can use them in query as below:
Chat.findAll({
attributes: ["id"],
include: [{
required: true,
model: Receiver,
as: 'chatReceiver', // Alias 1
attributes: [],
where: { userID: 1 }, // condition 1
}, {
required: true,
model: Receiver,
as: 'chatReceiver2', // Alias 2
attributes: [],
where: { userID: 2 }, // condition 2 as needed
}]
});
Solution :
Bike.findAll({
attributes: {include: [[Sequelize.fn('AVG', Sequelize.col('rates.rate')), 'rate_avg']],
},
include: [{
model: Rate,
attributes: []
}, {
model: Rate,
required : false , // 1. just to make sure not making inner join
separate : true , // 2. will run query separately , so your issue will be solved of multiple times
attributes: ['rate'],
include: [{
model: User,
attributes: [],
where: {
login: req.user.login
}
}]
group : [] // 3. <------- This needs to be managed , so please check errors and add fields as per error
}],
group: Object.keys(Bike.rawAttributes).map(key => 'bike.' + key) // group by all fields of Bike model
})
NOTE : READ THE COMMENTS
Sequelize doesn't support including through the same association twice (see here, here, and here). At the model level, you can define 2 different associations between Bike and Rate, but having to change the model, adding new foreign keys etc, is a very hacky solution.
Incidentally, it wouldn't solve your other problem, which is that you're grouping only by Bike but then want to select the user's rate. To fix that, you'd also have to change your grouping to include the user rates. (Note that if a user has more than 1 rate per bike, that might also create some inefficiency, as the rates for the bike are averaged repeatedly for each of the user's rates.)
A proper solution would be using window functions, first averaging the rates per bike and then filtering out all the rates not belonging to the logged in user. Might look something like this:
SELECT *
FROM (
SELECT bike.*,
users.login AS user_login,
AVG (rates.rate) OVER (PARTITION BY bike.id) AS rate_avg
FROM bike
INNER JOIN rates ON rates.bikeId = bike.id
INNER JOIN users ON rates.userId = users.id
)
WHERE user_login = :req_user_login
Unfortunately, as far as I'm aware sequelize doesn't currently support subqueries in the FROM clause and using window functions in this way, so you'd have to fall back to a raw query.

Sequelize join twice on two tables

I have two table: Conversations and ConversationParticipants.
I need to get the list of conversations that both users 1 and 2 participate too.
In MySQL, the query would be this:
SELECT conversation_participants.conversation_id FROM conversation_participants
JOIN conversations t1
ON t1.conversation_id = conversation_participants.conversation_id
AND conversation_participants.user_id = 11
JOIN conversation_participants t2
ON t1.conversation_id = t2.conversation_id
AND t2.user_id = 2
However, in Sequelize I cannot understand how to set models relations so that I can make a single query. I tried this without success (please note that this is almost pseudo code, reported as such for clarity):
var Conversation = sequelize.define('conversations', {
conversationId: Sequelize.INTEGER.UNSIGNED,
});
var ConversationParticipant = sequelize.define('conversation_participants', {
participationId: Sequelize.INTEGER.UNSIGNED,
userId: Sequelize.INTEGER.UNSIGNED,
conversationId : Sequelize.INTEGER.UNSIGNED,
});
Conversation.hasMany(ConversationParticipant, { as : 'Participants'});
and then
ConversationParticipant.findAll({
where : { userId : 1 },
include : [{
model : Conversation,
as : 'conversation',
include : [{
model : ConversationParticipant,
as : 'Participants',
where : { userId : 2 }
}]
}]
I get the following error:
Error: conversation_participants is not associated with conversations!.
Any idea?
One way you could do it is find the conversations that have participants with user 1 or 2, and afterwards filter the conversations that have both of them:
const userIdsFilter = [1,2];
//find Conversations that have user 1 OR user 2 as participants,
Conversation.findAll({
include : [{
model : ConversationParticipant,
as : 'conversation_participants',
where : { userId : { $in: userIdsFilter }}
}]
}).then((conversations) =>
{
//filter the conversations that have the same length participants
//as the userIdsFilter length (that way it excludes the ones
//that have just one of the users as participants)
return conversations.filter((conversation) =>
conversation.conversation_participants.length === userIdsFilter.length);
}).then((conversations) =>
{
//do what you need with conversations..
})
You are missing the belongsTo association in the ConversationParticipant definition. Sequelize model needs explicit association since you are trying to access Conversation via the ConversationParticipant instance even though Conversation is associated ConversationParticipant is not.
Something like this in addition to hasMany association:
ConversationParticipant.belongsTo(Conversation, { as : 'conversation',
foreignKey: 'conversationId', targetKey: 'conversationId'});
And then remove the conversationId field definition from ConversationParticipant model since belongsTo also creates that for you.

Filter elements within object in DynamoDB

I have a DynamoDB table which contains objects looking like the following:
{
'username': ...,
'subscriptions': [
...
],
...
}
And I would like to filter subscriptions for each user based on some criteria, and get back the objects which have subscriptions matching the criteria, but filter those objects so that only the matching subscriptions are present.
If a user has subscribed to 50 things, but only 3 of them match, I would like to get back an object where the `subscriptions' field is only 3 elements long. I also need the other information contained in the object.
To give a more specific example, suppose that I have two elements in my table:
{
'username': 'alice',
'subscriptions': [
1,
2
],
'email': 'alice#a.com'
},
{
'username': 'bob',
'subscriptions': [
2,
3
],
'email': 'bob#a.com'
}
And I would like to filter to get the subscription `1'. I would like to get back
{
'username': 'alice',
'subscriptions': [
1
],
'email': 'alice#a.com'
}
Or perhaps some other structure which contains all the necessary information (and no other information in order to save bandwidth).
I believe I can do this with scan(), but I do not know the specifics.

Resources