How to delete associated rows with sequelize - node.js

I am trying to delete rows associated with a row in a table, without deleting the main row (thus can't use CASCADE).
This is the raw PostgreSQL query that does what I want to achieve with SQL. Is sequelize able to generate such query:
DELETE FROM session USING user WHERE session.user_id = user.id AND user.username = 'bob'
The model is (not including irrelevant columns):
create table user (
id uuid primary key default uuid_generate_v4(),
username text UNIQUE NOT NULL
);
create table session (
id uuid primary key default uuid_generate_v4(),
user_id uuid NOT NULL references user(id) ON DELETE CASCADE
);
The association is defined in sequelize as:
Session.belongsTo(models.User, {
foreignKey: "user_id"
});
User.hasMany(models.Session, {
foreignKey: "user_id"
});
An alternative version of the query could be:
DELETE FROM session WHERE session.user_id = (SELECT user_id FROM user WHERE username = 'bob');
But I think sequelize doesn't handle subqueries yet?
I tried something along the lines:
return Session.destroy({
include: [
{ model: User, where: { username: 'bob' }}
],
truncate: false
});
However, sequelize complains:
Error: Missing where or truncate attribute in the options parameter of model.destroy.

If anyone gets here, this is how I "delete associated rows with sequelize": little help from the library:
Read the user from db (Users.findOne({ ... }))
call the method setSessions(array) provided by sequelize (the name depends on your model) which returns a promise.
/** #param {string} username */
const clearUserSessions = async (username) {
const userInstance = await Users.findOne({
where: { username },
include: ['sessions']
})
if (!userInstance) {
/* user not found */
return ...
}
await userInstance.setSessions([])
/* removed every session from user */
return ...
};
later:
try {
await clearUserSessions('bob')
} catch(err) {
...
}

return Session.destroy({
where: {
'$users.username$': 'bob'
},
include: [{
model: User,
as: 'users'
}],
});
Hope that helps. Try to reach me with comment.

Related

Sequelize: Hard delete and Insert in a table

I need to delete and insert the same data to the table.
But currently ,after the delete processes it seems that the data are still in the database. I think this was a soft-delete only. I cannot insert the same data to the DB since there are items that are supposed to be unique and it is not deleted from the DB.
After the deleteResults function, I assumed that the items are now deleted to the database, but when the insertQuestions is called, it encounters an error which is some data are the same with the data in the DB.
My model is not set as paranoid so I cannot use the force property.
How to hard-delete in Sequelize?
Here is the code for delete and insert implementation:
const res = await <<Model>>.destroy({
where: {
id: id
},
transaction: transaction
})
// This contains the destroy calls
const deleteResults = await deleteQuestions(questionnaireResult.id, questionnaireResult.qneQuestions, transaction);
//Insert to DB
const insertQuestionsResult = await insertQuestions(questionnaireResult.id, null, qsReponseObj.questions, answerTypes, languages, sortValuesQuestion, transaction);
Note that they are using the same transaction.
You are using transaction to do these two operations but seems did not call commit() function. I did a simple test on my local, before I run my code, data in users table:
After I run the code below, delete the record with id of value 2 and insert a new record with id 2 but new user name:
class Users extends Model {}
Users.init({
name: DataTypes.STRING
}, { sequelize, modelName: 'users' });
(async () => {
const t = await sequelize.transaction();
await Users.destroy({
where: {
id: 2
},transaction: t
});
await Users.create({
id:2,
name:"a new user name"
},{transaction:t})
await t.commit();
})().then(()=>{sequelize.close()})
.catch(error =>{console.log(error)})
Result:

Sequelize insert additional fields into junction table

as described in sequelize documentation here,
Foo.belongsToMany(Bar, { through: Baz })
then there is a method to insert into junction table:
fooInstance.addBars([5,4])
It will insert into Baz junction table two fields: FooId,BarId.
But i need to insert another field value too. sth like this:
fooInstance.addBars([{BarId: 5, otherField:'xxx'}, ...]
How i can achieve that without manual insert?
See Advanced Many-to-Many guide.
const User_Profile = sequelize.define('User_Profile', {
selfGranted: DataTypes.BOOLEAN
}, { timestamps: false });
User.belongsToMany(Profile, { through: User_Profile });
Profile.belongsToMany(User, { through: User_Profile });
With this, we can now track an extra information at the through table, namely the selfGranted boolean. For example, when calling the user.addProfile() we can pass values for the extra columns using the through option.
Example:
const amidala = await User.create({ username: 'p4dm3', points: 1000 });
const queen = await Profile.create({ name: 'Queen' });
await amidala.addProfile(queen, { through: { selfGranted: false } });

Sequelize findAll() with where-parameter returns null while findByPk() returns correct data

I'm setting up resolvers for a GraphQL API right now and running into some problems/questions regarding the findAll() function from sequelize.
I have these 2 resolvers:
User: async (parent, { id }, { models }) => {
return await models.User.findAll({
where: {
ID_User: id
}
});
}
UserPK: async (parent, { id }, { models }) => {
return await models.User.findByPk(id);
}
Models:
type Query {
UserPK(id: ID): User
User(id: ID): User
}
type User {
ID_User: ID,
Username: String,
}
If I now run these queries
{
UserPK(id: 1) {
ID_User
Username
}
User(id: 1) {
ID_User
Username
}
}
Only the UserPK returns (correct) data, the User query returns null for every field which confuses me because the queries sequelize executes are exactly the same.
Executing (default): SELECT `ID_User`, `Username` FROM `User` AS `User` WHERE `User`.`ID_User` = '1';
Executing (default): SELECT `ID_User`, `Username` FROM `User` AS `User` WHERE `User`.`ID_User` = '1';
I'm using apollo server btw if that makes any difference.
The difference between findByPk and findAll is that findByPk returns already a single element whereas findAll returns and array. You don't seem to take that into account. After that the resolvers for User receive an array where they cannot read properties from.
return (await models.User.findAll({
where: {
ID_User: id
}
}))[0];

Sequelize "SELECT * from tablename" query

Why the query below executes SELECT id, email, name FROM users AS users WHERE users.email = 'admin#admin.com'; rather than SELECT * from users WHERE email = admin#admin.com ?
http://docs.sequelizejs.com/en/latest/docs/querying/#where
Documentation states that it'll run a SELECT * query when I do findAll(), but it does something different in my example. What's missing here?
Here's my users model.
var user = sequelize.define('users', {
id : {
type : Sequelize.INTEGER,
primaryKey : true,
autoIncrement : true
},
email : {
type : Sequelize.STRING
},
name : {
type : Sequelize.STRING
}
},
{
tableName: 'users',
freezeTableName: true
});
And this is my query. It seems that it only selects defined columns, but I don't want this.
var email = "admin#admin.com";
user.findAll({where: {email: email}}).then(function (user) {
res.send(user);
}).error(function (err) {
console.log("Error:" + err);
});
This is expected behavior of sequelize selecting only the columns which you have defined. If you want to select all of the columns using sequelize you must define them in your model.

How do I reference an association when creating a row in sequelize without assuming the foreign key column name?

I have the following code:
#!/usr/bin/env node
'use strict';
var Sequelize = require('sequelize');
var sequelize = new Sequelize('sqlite:file.sqlite');
var User = sequelize.define('User', { email: Sequelize.STRING});
var Thing = sequelize.define('Thing', { name: Sequelize.STRING});
Thing.belongsTo(User);
sequelize.sync({force: true}).then(function () {
return User.create({email: 'asdf#example.org'});
}).then(function (user) {
return Thing.create({
name: 'A thing',
User: user
}, {
include: [User]
});
}).then(function (thing) {
return Thing.findOne({where: {id: thing.id}, include: [User]});
}).then(function (thing) {
console.log(JSON.stringify(thing));
});
I get the following output:
ohnobinki#gibby ~/public_html/turbocase1 $ ./sqltest.js
Executing (default): INSERT INTO `Users` (`id`,`email`,`updatedAt`,`createdAt`) VALUES (NULL,'asdf#example.org','2015-12-03 06:11:36.904 +00:00','2015-12-03 06:11:36.904 +00:00');
Executing (default): INSERT INTO `Users` (`id`,`email`,`createdAt`,`updatedAt`) VALUES (1,'asdf#example.org','2015-12-03 06:11:36.904 +00:00','2015-12-03 06:11:37.022 +00:00');
Unhandled rejection SequelizeUniqueConstraintError: Validation error
at Query.formatError (/home/ohnobinki/public_html/turbocase1/node_modules/sequelize/lib/dialects/sqlite/query.js:231:14)
at Statement.<anonymous> (/home/ohnobinki/public_html/turbocase1/node_modules/sequelize/lib/dialects/sqlite/query.js:47:29)
at Statement.replacement (/home/ohnobinki/public_html/turbocase1/node_modules/sqlite3/lib/trace.js:20:31)
It seems that specifying {include: [User]} instructs Sequelize to create a new User instance matching the contents of user. That is not my goal. In fact, I find it hard to believe that such behaviour would ever be useful—I at least have no use for it. I want to be able to have a long-living User record in the database and at arbitrary times create new Things which refer to the User. In my shown example, I wait for the User to be created, but in actual code it would likely have been freshly loaded through User.findOne().
I have seen other questions and answers say that I have to explicitly specify the implicitly-created UserId column in my Thing.create() call. When Sequelize provides an API like Thing.belongsTo(User), I shouldn’t have to be aware of the fact that a Thing.UserId field is created. So what is the clean API-respecting way of creating a new Thing which refers to a particular User without having to guess the name of the UserId field? When I load a Thing and specify {include: [User]}, I access the loaded user through the thing.User property. I don’t think I’m supposed to know about or try to access a thing.UserId field. In my Thing.belongsTo(User) call, I never specify UserId, I just treat that like an implementation detail I shouldn’t care about. How can I continue to avoid caring about that implementation detail when creating a Thing?
The Thing.create() call that works but looks wrong to me:
Thing.create({
name: 'A thing',
UserId: user.id
});
Option 1 - risks DB inconsistency
Sequelize dynamically generates methods for setting associations on instances, e.g. thing.setUser(user);. In your use case:
sequelize.sync({force: true})
.then(function () {
return Promise.all([
User.create({email: 'asdf#example.org'}),
Thing.create({name: 'A thing'})
]);
})
.spread(function(user, thing) {
return thing.setUser(user);
})
.then(function(thing) {
console.log(JSON.stringify(thing));
});
Option 2 - does not work/buggy
It isn't documented, but from a code dive I think the following should work. It doesn't but that seems to be because of a couple of bugs:
// ...
.then(function () {
return models.User.create({email: 'asdf#example.org'});
})
.then(function(user) {
// Fails with SequelizeUniqueConstraintError - the User instance inherits isNewRecord from the Thing instance, but it has already been saved
return models.Thing.create({
name: 'thingthing',
User: user
}, {
include: [{
model: models.User
}],
fields: ['name'] // seems nec to specify all non-included fields because of line 277 in instance.js - another bug?
});
})
Replacing models.User.create with models.User.build doesn't work because the built but not saved instance's primary key is null. Instance#_setInclude ignores the instance if its primary key is null.
Option 3
Wrapping the Thing's create in a transaction prevents an inconsistent state.
sq.sync({ force: true })
.then(models.User.create.bind(models.User, { email: 'asdf#example.org' }))
.then(function(user) {
return sq.transaction(function(tr) {
return models.Thing.create({name: 'A thing'})
.then(function(thing) { return thing.setUser(user); });
});
})
.then(print_result.bind(null, 'Thing with User...'))
.catch(swallow_rejected_promise.bind(null, 'main promise chain'))
.finally(function() {
return sq.close();
});
I have uploaded a script demo'ing option 2 and option 3 here
Tested on sequelize#6.5.1 sqlite3#5.0.2 I can use User.associations.Comments.foreignKey as in:
const Comment = sequelize.define('Comment', {
body: { type: DataTypes.STRING },
});
const User = sequelize.define('User', {
name: { type: DataTypes.STRING },
});
User.hasMany(Comment)
Comment.belongsTo(User)
console.dir(User);
await sequelize.sync({force: true});
const u0 = await User.create({name: 'u0'})
const u1 = await User.create({name: 'u1'})
await Comment.create({body: 'u0c0', [User.associations.Comments.foreignKey]: u0.id});
The association is also returned during creation, so you could also:
const Comments = User.hasMany(Comment)
await Comment.create({body: 'u0c0', [Comments.foreignKey]: u0.id});
and on many-to-many through tables you get foreignKey and otherKey for the second foreign key.
User.associations.Comments.foreignKey contains the foreignKey UserId.
Or analogously with aliases:
User.hasMany(Post, {as: 'authoredPosts', foreignKey: 'authorId'});
Post.belongsTo(User, {as: 'author', foreignKey: 'authorId'});
User.hasMany(Post, {as: 'reviewedPosts', foreignKey: 'reviewerId'});
Post.belongsTo(User, {as: 'reviewer', foreignKey: 'reviewerId'});
await sequelize.sync({force: true});
// Create data.
const users = await User.bulkCreate([
{name: 'user0'},
{name: 'user1'},
])
const posts = await Post.bulkCreate([
{body: 'body00', authorId: users[0].id, reviewerId: users[0].id},
{body: 'body01', [User.associations.authoredPosts.foreignKey]: users[0].id,
[User.associations.reviewedPosts.foreignKey]: users[1].id},
])
But that syntax is so long that I'm tempted to just hardcode the keys everywhere.

Resources