Sequelize: include child if parent attribute is equals to specific value - node.js

I'm using nodejs v8.9.0 + expressjs v4.14.0 + sequelize v3.25.0 to build a web app in which people can ask questions each others. A user can ask a public or a private(for specifics users) question
Here is the user model:
/** USER MODEL **/
module.exports = function(sequelize, DataTypes) {
var User = sequelize.define('users', {
firstName: {
type: DataTypes.STRING(50),
field: 'firstName'
},
lastName: {
type: DataTypes.STRING(200),
field: 'lastName'
},
email: {
type: DataTypes.STRING(200),
field: 'email'
},
},
{
classMethods: {
User.belongsToMany(
models.questions,
{
through: 'QuestionRecipient', foreignKey: 'userId'
}
);
}
})
Here is the question model:
/** QUESTION MODEL **/
module.exports = function(sequelize, DataTypes) {
var Question = sequelize.define('questions', {
content: {
type: DataTypes.TEXT,
field: 'content'
},
target: {
type: DataTypes.INTEGER,
field: 'target',
defaultValue: 1 // 1 = public / 2 = private
}
},
{
classMethods: {
Question.belongsToMany(
models.users,
{
through: 'QuestionRecipient', foreignKey: 'questionId'
}
);
}
})
Now when I'm querying questions, I want to include users through 'QuestionRecipient' only if question.target = 2 which means the question is for specifics users
How to do this query ?
Thanks

Related

Sequelize (PostgresSQL) many to many chat implementation

I've been trying to create a chat app with Node JS and Sequelize. Now i'm stuck at a problem of creating a query to find a conversation that has my id and user's id(the one i'm trying to text). So the thing i'm trying to do is send a post request with and id of a user i'm sending a message to, then i look through my Conversation model and check if that conversation has my id and id of the user i'm texting to.
My models are associated through Many to Many relationship. So the main objective is to find a conversation with only my ID and ID of the user i'm texting to with the same ConversationId.
Here are my models:
User
module.exports = (sequelize, DataTypes) => {
const User = sequelize.define(
"User",
{
name: { type: DataTypes.STRING },
password: { type: DataTypes.STRING, allowNull: false },
username: { type: DataTypes.STRING, allowNull: false },
email: { type: DataTypes.STRING, allowNull: false },
},
{}
);
User.belongsToMany(models.Conversation, {
as: "conversations",
foreignKey: "user_id",
through: models.ConversationUsers,
});
User.hasMany(models.Message, {
as: "messages",
});
};
return User;
};
Conversation
module.exports = (sequelize, DataTypes) => {
const Conversation = sequelize.define(
"Conversation",
{
lastMessage: DataTypes.STRING,
recipients: DataTypes.ARRAY(DataTypes.INTEGER),
},
{
sequelize,
modelName: "Conversation",
}
);
Conversation.associate = (models) => {
Conversation.belongsToMany(models.User, {
as: "participants",
foreignKey: "conversation_id",
through: models.ConversationUsers,
});
Conversation.hasMany(models.Message, {
as: "messages",
});
};
return Conversation;
};
ConversationUsers Many to Many through model
"use strict";
module.exports = (sequelize, DataTypes) => {
const ConversationUsers = sequelize.define(
"ConversationUsers",
{
user_id: DataTypes.INTEGER,
conversation_id: DataTypes.INTEGER,
},
{
sequelize,
modelName: "ConversationUsers",
}
);
return ConversationUsers;
};
Message
module.exports = (sequelize, DataTypes) => {
const Message = sequelize.define(
"Message",
{
conversationId: { type: DataTypes.INTEGER, allowNull: false },
sentTo: DataTypes.INTEGER,
sentFrom: DataTypes.INTEGER,
body: { type: DataTypes.STRING, allowNull: false },
},
{
sequelize,
modelName: "Message",
}
);
Message.associate = (models) => {
Message.belongsTo(models.User, {
as: "messageTo",
foreignKey: "sentTo",
});
Message.belongsTo(models.User, {
as: "messageFrom",
foreignKey: "sentFrom",
});
Message.belongsTo(models.Conversation, {
as: "messages",
});
};
return Message;
};
I think you can remove some pieces from your models and rework it a bit.
Messages don't need a sentTo, they only need a sentFrom. You can use the ConversationUsers table to know who the recipients are. This also gives you the flexibility to have Conversations with more than 2 members, because your current model essentially enforces that a Message can only be to one user.
So let's walk through the models first with changes
module.exports = (sequelize, DataTypes) => {
const User = sequelize.define(
"User",
{
name: { type: DataTypes.STRING },
password: { type: DataTypes.STRING, allowNull: false },
username: { type: DataTypes.STRING, allowNull: false },
email: { type: DataTypes.STRING, allowNull: false },
},
{
// I think moving the associations to other files might make this more clear
}
);
};
return User;
};
module.exports = (sequelize, DataTypes) => {
const Conversation = sequelize.define(
"Conversation",
{
// perhaps something like a subject could go here e.g.
subject: DataTypes.STRING(500),
},
{
sequelize,
modelName: "Conversation",
}
);
Conversation.associate = (models) => {
Conversation.hasMany(models.Message, {
as: "ConversationMessages",
}); // adds ConversationId onto Message, gives us Conversation.getConversationMessages() etc
models.Message.belongsTo(Conversation); // create association both ways for convenience methods to find convo from a message
models.Message.hasOne(Conversation, {
as: 'LastMessage',
constraints: false,
allowNull:true,
defaultValue:null
}); // adds LastMessageId onto Conversation model (you'll have to write code to maintain this value, probably through an afterCreate hook on Message model)
};
return Conversation;
};
module.exports = (sequelize, DataTypes) => {
const Message = sequelize.define(
"Message",
{
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true, // if you want to do the hook thing i talked about to set LastMessageId, you need to put this in
},
body: { type: DataTypes.STRING, allowNull: false },
},
{
sequelize,
modelName: "Message",
}
);
Message.associate = (models) => {
Message.belongsTo(models.User, {as: "sentFromUser"});
};
return Message;
};
// I'm going to rename your many-to-many table "ConversationMembers"
module.exports = (sequelize, DataTypes) => {
const ConversationMembers = sequelize.define(
"ConversationMembers",
{
// again, the associations will build these fields for you
},
{
sequelize,
modelName: "ConversationMembers",
}
);
models.Conversation.belongsToMany(models.User, {
through: "ConversationMember",
as: "Members",
}); // gives us Conversation.getMembers()
models.User.belongsToMany(models.Conversation, {
through: "ConversationMember",
as: "MemberConversations",
}); // gives us User.getMemberConversations()
ConversationMember.belongsTo(models.Message, { as: "LastReadMessage" }); // gives us the potential ability to track the last read message for each convo member as ConversationMember.LastReadMessageId, you'll need to set this value manually on read for each user if you care about having it
models.Conversation.hasMany(ConversationMember);
models.User.hasMany(ConversationMember);
return ConversationMember;
Okay now onto your question, which perhaps becomes simpler at this point. If you already know the ConversationId, all you need to do is check that the person who is sending the message is a member of the conversation. Then if they are, write a row into the Messages table. It doesn't matter who the message is "to"--you're writing to the members of the Conversation, not to any individual.
async function canMessageHelper({conversationId, userId }) {
const convo = await models.Conversation.findOne({
attributes: ["id"], // whatever attrs you need, probably not many if any
where: {
id: conversationId,
},
include: [{
model: models.ConversationMember,
attributes: ["ConversationId"], // whatever you need if anything
where: { // this where is critical, it creates an inner join so convo only returns if myUserId is a member of the Conversation
UserId: userId
}
}]
});
if (!convo) {
return false;
}
return convo;
}
async function sendMessage({conversationId, authorUserId, messageText}) {
const allowMessage = await canMessageHelper({conversationId, userId: authorUserId});
if (!allowMessage) {
return false;
}
await models.Message.create({sentFromUserId: authorUserId, body: messageText});
}
If you want to try this, be sure you remove any tables you've already created with these names from your database before you sync.
I have not provided any code for the hooks I mentioned, but you will have the groundwork to develop those ideas out.

How to fix? Unhandled rejection SequelizeForeignKeyConstraintError: insert or update on table "x" violates foreign key constraint "y_fkey"

This error is showing when I attempt to POST / create a new contact in the database. This is the error Unhandled rejection SequelizeForeignKeyConstraintError: insert or update on table "contacts" violates foreign key constraint "org_name_fkey". Below are the models, does anyone know how to fix this? All of the other fields POST fine, it is only when attempted to add org_name. If I don't include org_name in the POST everything stores in the postgres database.
Contact Model
module.exports = (sequelize, DataTypes) => {
const Contacts = sequelize.define('contact', {
contact_id: {
type: Sequelize.INTEGER,
field: 'contact_id',
primaryKey: 'true'
},
first_name: {
type: Sequelize.STRING,
field: 'first_name'
},
last_name: {
type: Sequelize.STRING,
field: 'last_name'
},
contact_type_id: {
type: Sequelize.STRING,
references: {
model: 'contact_type',
key: 'contact_type_id'
}
},
org_name: {
type: Sequelize.STRING,
references: {
model: 'org',
key: 'org_name'
}
}
},
{
tableName: 'contacts',
}
);
Contacts.associate = (models) => {
Contacts.belongsTo(models.contact_type, {foreignKey: 'contact_type_id'});
Contacts.belongsTo(models.org, {foreignKey: 'org_name'});
};
return Contacts;
};
Contact Type Model
module.exports = (sequelize, DataTypes) => {
const ContactType = sequelize.define('contact_type', {
// attributes
contact_type_id: {
type: Sequelize.STRING,
field: 'contact_type_id'
},
updated_at: {
type: Sequelize.DATE,
field: 'updated_at'
},
created_at: {
type: Sequelize.DATE,
field: 'created_at'
}
},
);
ContactType.associate = (models) => {
ContactType.hasOne(models.contact, {foreignKey: 'contact_id'});
};
return ContactType;
};
Org Model
module.exports = (sequelize, DataTypes) => {
const Org = sequelize.define('org', {
org_name: {
type: Sequelize.STRING,
field: 'org_name',
primaryKey: 'true'
},
org_type_id: {
type: Sequelize.STRING,
field: 'org_type_id'
},
website: {
type: Sequelize.STRING,
field: 'website'
}
},
);
Org.associate = (models) => {
Org.hasMany(models.contact, {foreignKey: 'contact_id'});
};
return Org;
};
What that error tells you is that whatever org_name you are passing into your POST request does not have a corresponding Org record in the database. i.e. You are creating a person who works at ABC Corp, but you haven't added ABC Corp to your Orgs. You would have to fix this by creating the Org first, then the Contact.
You do not give the exact request that causes the error. This would be helpful in tracking down your issue. Also, you should query your database to find the state of the Org table before the POST. Perhaps you forgot to call save on the Org instance.

Problem setting up Sequelize association - query with 'include' is failing

I'm new to Sequelize and trying to test if an n:m association I set up between two models, User and Podcast, is working. When I try to run this query, I get some kind of DB error that isn't specific about what's wrong:
User.findOne({
where: { id: id },
include: [{ model: Podcast }]
});
Does anyone know what I'm messing up? I suspect there's something wrong in how I've set up the association, like I'm referencing the names of tables slightly incorrectly, but the migration to create the association worked.
Here's my User.js model file:
module.exports = (sequelize, DataTypes) => {
const User = sequelize.define('User', {
name: {
type: DataTypes.STRING,
allowNull: false
},
email: {
type: DataTypes.STRING,
allowNull: false,
unique: true
},
photo: {
type: DataTypes.STRING
}
});
User.associate = function(models) {
// associations can be defined here
User.belongsToMany(models.Podcast, {
through: 'user_podcast'
});
};
return User;
};
And here's my Podcast.js file:
'use strict';
module.exports = (sequelize, DataTypes) => {
const Podcast = sequelize.define('Podcast', {
id: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false
},
title: {
type: DataTypes.STRING,
allowNull: false
},
thumbnail: {
type: DataTypes.STRING
},
website: {
type: DataTypes.STRING
}
});
Podcast.associate = function(models) {
// associations can be defined here
Podcast.belongsToMany(models.User, {
through: 'user_podcast'
});
};
return Podcast;
};
And here's the migration I ran to join the two tables:
'use strict';
module.exports = {
up: function(queryInterface, Sequelize) {
return queryInterface.createTable('user_podcast', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
userId: {
type: Sequelize.INTEGER,
references: {
model: 'Users',
key: 'id'
}
},
podcastId: {
type: Sequelize.STRING,
references: {
model: 'Podcasts',
key: 'id'
}
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
},
down: function(queryInterface, Sequelize) {
return queryInterface.dropTable('user_podcast');
}
};
And here's the project on Github for further reference:
https://github.com/olliebeannn/chatterpod
You don't need to create a migration for the M:N table. Now you have something wrong on your user_podcast model. If you are setting a M:N relation between to tables your primary key will be the combination between the foreign key from these two models. If you still want a single id primary key for your table, then you won't use belongsToMany instead use hasMany on user and podcast models pointing to a new model user_podcast.
As far as I see on your first query, it seems that you really need a M:N relation so you can define the model as you do with user and podcast like this:
module.exports = (sequelize, DataTypes) => {
const UserPodcast = sequelize.define('user_podcast', {
userId: {
// field: 'user_id', #Use 'field' attribute is you have to match a different format name on the db
type: DataTypes.INTEGER
},
podcastId: {
// field: 'podcast_id',
type: DataTypes.INTEGER
},
});
UserPodcast.associate = function(models) {
models.User.belongsToMany(models.Podcast, {
as: 'podcasts', //this is very important
through: { model: UserPodcast },
// foreignKey: 'user_id'
});
models.Podcast.belongsToMany(models.User, {
as: 'users',
through: { model: UserPodcast },
// foreignKey: 'podcast_id'
});
};
return UserPodcast;
};
I do prefer to have the belongsToMany associations on the save function where I define the join model, and you have to notice that I used as: attribute on the association. This is very important because this will help sequelize to know which association are you referring on the query.
User.findOne({
where: { id: id },
include: [{
model: Podcast,
as: 'podcasts' //here I use the previous alias
}]
});

Sequelize: include association only IF

I'm using nodejs v8.9.0 + expressjs v4.14.0 + sequelize v3.25.0 to build a web app in which people can publish articles.
I have these models
/** ARTICLE MODEL **/
module.exports = function(sequelize, DataTypes) {
var Article = sequelize.define('articles', {
title: {
type: DataTypes.STRING(255),
field: 'title'
},
content: {
type: DataTypes.TEXT,
field: 'content'
}
anonymous: {
type: DataTypes.BOOLEAN,
field: 'anonymous'
}
},
{
classMethods: {
Article .belongsTo(
models.users,
{
foreignKey: 'userId'
}
);
}
})
And
/** USER MODEL **/
module.exports = function(sequelize, DataTypes) {
var User = sequelize.define('users', {
firstName: {
type: DataTypes.STRING(50),
field: 'firstName'
},
lastName: {
type: DataTypes.STRING(200),
field: 'lastName'
},
email: {
type: DataTypes.STRING(200),
field: 'email'
},
},
{
classMethods: {}
})
Users can publish articles anonymously or not
Now when I'm querying articles list, I want to include User model only if anonymous is false.
How to do that ?
Not so elegant solution, but work:
Article.findAll({
include: [{
model: User,
required: false,
where: sequelize.where(sequelize.literal('anonymous'), false)
}],
})

Sequelize associate not created

A very straightforward question. Am using nodejs 5.6, express 4 and sequelize 4. In the program, I have a user object which belongs to company object.
var User = sequelize.define('user', {
username: {
type: Sequelize.STRING
},
password: {
type: Sequelize.STRING
}
});
const Company= sequelize.define('companny', {
record: {
type: Sequelize.JSON
}
});
Company.associate = function(models) {
console.log(models)
Company.hasMany(models.User, {
foreignKey: {
// name: 'user',
allowNull: true
}
})
}
when running the program, the database is created with user and company table, but there is no foreign key in eight user or company. And the console outputs nothing. Any suggestion?
Finally, the index.js file here answers my question.
I would normally use this method to create my models:
// User model
export default (sequelize, DataTypes) => {
const User = sequelize.define('User', {
email: {
allowNull: false,
type: DataTypes.STRING,
unique: true
},
password: {
allowNull: false,
type: DataTypes.STRING,
}
});
return User;
};
// Company model
export default (sequelize, DataTypes) => {
const Company = sequelize.define('Company', {
record: {
type: DataTypes.JSON
}
});
Company.associate = (models) => {
console.log(models)
Company.hasMany(models.User, {
foreignKey: 'userId'
})
}
return Company;
};

Resources