Sequelize how to structure the chat part of the app? - postgresql

I currently have an app where users can log in and make posts. Next to each post, there is button that if pressed, the current user can send a message to the user who created the post.
I have a posts model and a user model. Each user can post as many posts as they want, but each post only belongs to one user.
const User = db.define(
"User",
{
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true,
},
name: {
type: Sequelize.STRING,
},
email: {
type: Sequelize.STRING,
},
password: {
type: Sequelize.STRING,
},
},
);
const Post = db.define(
"Post",
{
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true,
},
title: {
type: Sequelize.STRING,
},
subTitle: {
type: Sequelize.STRING,
},
userId: {
type: Sequelize.INTEGER,
},
},
);
User.hasMany(Post,
{ foreignKey: "userId" }
);
Post.belongsTo(User,
{ foreignKey: "userId" }
);
What i am trying to implement now is the messaging functionality.
Here is my messages Model so far:
const Messages = db.define("Messages", {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER,
},
senderId: {
type: Sequelize.STRING,
},
receiverId: {
type: Sequelize.STRING,
},
message: {
type: Sequelize.STRING,
},
});
User.hasMany(Messages, {
foreignKey: 'senderId'
});
User.hasMany(Messages, {
foreignKey: 'receiverId'
});
Messages.associate = (models) => {
Messages.belongsTo(models.Post, {
foreignKey: "PostId",
});
Messages.belongsTo(models.User, {
foreignKey: "senderId",
});
Messages.belongsTo(models.User, {
foreignKey: "receiverId",
});
};
User.hasMany(Conversations, {
foreignKey: 'conversationId'
});
And here is my conversation model so far:
const Conversations = db.define("Conversations", {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER,
}
});
module.exports = Conversations;
Conversations.associate = (models) => {
Conversations.hasMany(models.Message);
models.Message.belongsTo(Conversations);
};
Also, the message can be sent from 1 user to another not to a group of users.
Are my associations and table structures correct?
Update
User.hasMany(Messages, {
foreignKey: 'senderId'
});
User.hasMany(Messages, {
foreignKey: 'receiverId'
});
Messages.associate = (models) => {
Messages.belongsTo(models.User, {
foreignKey: "senderId",
});
Messages.belongsTo(models.User, {
foreignKey: "receiverId",
});
Messages.belongsTo(models.Conversations,{
foreignKey: "conversationId",
});
};
Conversations.associate = (models) => {
Conversations.hasMany(models.Messages,{
foreignKey: "conversationId",
});
Conversations.belongsTo(models.Post,{
foreignKey: "PostId",
})
};
Post.hasMany(Conversations, { foreignKey: "PostId" });
User.hasMany(Conversations, {
foreignKey: 'user1'
});
User.hasMany(Conversations, {
foreignKey: 'user2'
});
Using this implementation when i try sending a message, the conversationId in the messages table is null.
Here is my post request:
router.post("/",
auth,
async (req, res) => {
const post = await Post.findOne({where:{id:req.body.postId}})
if (!post) return res.status(400).send({ error: "Invalid postId." });
const targetUser = await User.findOne({where:{post.userId}})
if (!targetUser) return res.status(400).send({ error: "Invalid
userId." });
await Conversations.findOne({
where:{
user1:{[Op.or]:[req.user.id,post.userId]},
user2:{[Op.or]:[req.user.id,post.userId]},
PostId:req.body.postId,
}
}).then(conversation=>{
if(conversation){
return conversation
}else{
return Conversations.create({
user1: req.user.id,
user2: post.userId,
PostId:req.body.postId,
})
}
}
)
Messages.create({
senderId: req.user.id,
receiverId: post.userId,
message: req.body.message,
conversationId:conversation.id //MY PROBLEM IS HERE
})
.then(
res.status(201).send({
msg: "upload successful",
}));
const { expoPushToken } = targetUser;
if (Expo.isExpoPushToken(expoPushToken))
await sendPushNotification(expoPushToken, message);
});

All models look good. The issues are with associations.
If you define more then one association between the same two models you should indicate different aliases to distinguish them from each other in queries.
User.hasMany(Messages, {
foreignKey: 'senderId',
as: 'OutgoingMessages'
});
User.hasMany(Messages, {
foreignKey: 'receiverId',
as: 'IncomingMessages'
});
Messages.belongsTo(models.User, {
foreignKey: "senderId",
as: 'Sender'
});
Messages.belongsTo(models.User, {
foreignKey: "receiverId",
as: 'Receiver'
});
Also it's better to define associations in the same manner either directly after model definition or in a static method like associate. The latter approach is preferable because it allows to define each model in its own module without any cross-references using the models parameter in associate method to access other models that should be associated with a given model.
Last note: try to define associations where a model on the left side of an association definition in its own associate method.
It means that
models.Message.belongsTo(Conversations);
should be in Message model associate method:
Message.belongsTo(models.Conversations);
That way you always know where to find all associations that define links from a certain model to other models.
UPDATE
You should store a found or a created conversation to a variable in order to use it while creating a message:
let conversation = await Conversations.findOne({
where:{
user1:{[Op.or]:[req.user.id,post.userId]},
user2:{[Op.or]:[req.user.id,post.userId]},
PostId:req.body.postId,
}
})
if (!conversation){
conversation = await Conversations.create({
user1: req.user.id,
user2: post.userId,
PostId:req.body.postId,
})
}
const newMessage = await Messages.create({
senderId: req.user.id,
receiverId: post.userId,
message: req.body.message,
conversationId:conversation.id
})
res.status(201).send({
msg: "upload successful",
});
Don't try to mix up then/catch and await. If you use await you will already have a result or an exception (which you can handle using try/catch).

Related

Mongoose model unique

I´m rather new to this..
If I dont want the user to be able to add duplicated countries to visitedCountry, shoulden unique true work?
Or are there any easy way to block that in the patch?
const User = mongoose.model('User', {
username: {
type: String,
required: true,
unique: true
},
password: {
type: String,
required: true
},
accessToken: {
type: String,
default: () => crypto.randomBytes(128).toString('hex')
},
visitedCountries:[ {
country: {
type: Object,
ref: "Country",
unique: true
},
comments: String
}]
})
app.patch('/countries', authenticateUser)
app.patch('/countries', async (req, res) => {
const { username, visitedCountry } = req.body
try {
const countryByAlphaCode = await Country.findOne({ alphaCode: visitedCountry }).lean()
const updatedUser = await User.findOneAndUpdate({ username: username, }, {
$push: {
visitedCountries: { country: countryByAlphaCode, comments: "no comments yet"}
},
}, { new: true })
res.json({ success: true, updatedUser })
} catch (error) {
res.status(400).json({ success: false, message: "Invalid request", error })
}
})
The options unique works for all documents. It prevents two (or more) documents from having the same value for your indexed field. It's often used for the email or username.
For your case, I recommend you to perform a check on the user data before you call findOneAndUpdate.

How do I get sequelize to not use timestamp fields when generating SQL for getting n:m associated models?

My models:
'use strict';
module.exports = (sequelize, DataTypes) => {
const Vendor = sequelize.define('Vendor', {
id: { type: DataTypes.INTEGER, allowNull: false, autoIncrement: true, primaryKey: true },
// other fields
}, {});
Vendor.associate = function (models) {
Vendor.belongsToMany(models.Corporate, { through: 'VendorCorporates', foreignKey: 'vendorId' });
};
return Vendor;
};
and
'use strict';
module.exports = (sequelize, DataTypes) => {
const Corporate = sequelize.define('Corporate', {
id: { type: DataTypes.INTEGER, allowNull: false, autoIncrement: true, primaryKey: true },
// other fields
}, {});
Corporate.associate = function (models) {
Corporate.belongsToMany(models.Vendor, { through: 'VendorCorporates', foreignKey: 'corporateId' });
};
return Corporate;
};
and the migration for the association (link) table:
'use strict';
const vendorCorps = 'VendorCorporates', vendorId = 'vendorId', corpId = 'corporateId';
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable(vendorCorps, {
vendorId: {
type: Sequelize.INTEGER,
references: {
model: 'Vendors',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE'
},
corporateId: {
type: Sequelize.INTEGER,
references: {
model: 'Corporates',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE'
},
// notice no timestamps
});
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('VendorCorporates');
}
};
Now when I try to get the Corporates that are associated with a Vendor:
let vendor = await models.Vendor.findByPk(pk);
let corporates = await vendor.getCorporates();
the query fails because sequelize generates SQL with the fields updatedAt and createdAt. How do I tell sequelize to generate SQL without the timestamp fields?
Clarification: I want timestamps in the Corporate & Vendor models, but none in the link/association table, and I want the join query (vendor.getCorporates()) to be generated accordingly.
You can indicate this using the timestamps option. For instance:
const Corporate = sequelize.define('Corporate', {
id: { type: DataTypes.INTEGER, allowNull: false, autoIncrement: true, primaryKey: true },
// other fields
}, {
timestamps: false
});
Finally solved this by writing a model for the n:m association itself:
'use strict';
module.exports = (sequelize, DataTypes) => {
const VendorCorporate = sequelize.define('VendorCorporate', {
vendorId: DataTypes.INTEGER,
corporateId: DataTypes.INTEGER
}, { timestamps: false });
VendorCorporate.associate = function (models) {
// associations can be defined here
};
return VendorCorporate;
};
and changing:
// db/models/vendor.js
Vendor.belongsToMany(models.Corporate, { through: 'VendorCorporates', foreignKey: 'vendorId' });
to:
Vendor.belongsToMany(models.Corporate, { through: models.VendorCorporate, foreignKey: 'vendorId' });
and making a similar change in the Corporate model file.

Sequelize relation "likes" does not exist

I'm not understanding why sequelize is giving me this error.
relation "Likes" does not exist
I referenced a similar question, but it didn't provide me with much of an insight:
Sequelize Error: Relation does not exist
and this too:
Sequelize Migration: relation <table> does not exist
My table names matches the reference model names.
I don't think it has anything to do with the models, but everything to do with the migrations file.
This is what I have
Posts migration
'use strict';
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable('Posts', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
title: {
type: Sequelize.STRING
},
post_content: {
type: Sequelize.STRING
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
},
userId: {
type: Sequelize.INTEGER,
references: {
model: 'Users',
key: 'id'
}
},
likeId: {
type: Sequelize.INTEGER,
onDelete: 'CASCADE',
references: {
model: 'Likes',
key: 'id'
}
},
username: {
type: Sequelize.STRING
},
});
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('Posts');
}
};
Likes migration
'use strict';
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable('Likes', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
like: {
type: Sequelize.BOOLEAN
},
userId: {
type: Sequelize.INTEGER,
references: {
model: 'Users',
key: 'id',
as: 'userId'
}
},
postId: {
type: Sequelize.INTEGER,
references: {
model: 'Posts',
key: 'id',
as: 'postId'
}
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('Likes');
}
};
models/like.js
'use strict';
const Like = (sequelize, DataTypes) => {
const Likes = sequelize.define('Likes', {
like:{
type:DataTypes.BOOLEAN,
allowNull:true
}
}, {});
Likes.associate = function(models) {
Likes.belongsTo(models.User, {
onDelete: "CASCADE",
foreignKey: {
foreignKey: 'userId'
}
})
Likes.belongsTo(models.Post, {
onDelete: 'CASCADE',
foreignKey: 'likeId',
targetKey: 'id',
})
}
return Likes;
};
module.exports = Like;
models/post.js
module.exports = (sequelize, DataTypes) => {
const Post = sequelize.define('Post', {
title: DataTypes.STRING,
post_content: DataTypes.STRING,
username: DataTypes.STRING
}, {});
Post.associate = function(models) {
Post.belongsTo(models.User, { foreignKey: 'userId', targetKey: 'id' });
Post.belongsTo(models.Likes, { foreignKey: 'likeId', targetKey: 'id' });
};
return Post;
};
models/user.js
'use strict';
const User = (sequelize, DataTypes) => {
const myUser = sequelize.define('User', {
username: DataTypes.STRING,
email: DataTypes.STRING,
password: DataTypes.STRING,
resetPasswordToken:DataTypes.STRING,
resetPasswordExpires: DataTypes.DATE
}, {});
myUser.associate = function(models) {
myUser.hasMany(models.Post, { foreignKey: 'userId', as:'users' });
myUser.hasMany(models.Likes, { foreignKey: 'userId', as:'likes' });
};
return myUser;
};
module.exports = User;
Instead of adding likeId in the migration. I needed to add a new migration like so
sequelize migration:generate --name add_likeId_to_posts
so we have now
'use strict';
module.exports = {
up: function (queryInterface, Sequelize) {
return queryInterface.addColumn(
'Posts',
'likeId',
{
type: Sequelize.INTEGER,
allowNull: false,
references: {
model: 'Likes',
key: 'id'
}
}
)
},
down: function (queryInterface, Sequelize) {
return queryInterface.removeColumn(
'Posts',
'likeId'
)
}
};
which gives us
voila!
and likeId is Associated on the table

Postgres Include throws "name": "SequelizeEagerLoadingError"

I am having trouble figuring out why my include statement throws this error
"name": "SequelizeEagerLoadingError"
My controller function in question looks like this
retrieve (req, res) {
return User
.findOne({
where: {
token_id: req.params.token_id
},
include: [{
model: subscribedcurrency,
as: 'subscribed currency'
}]
})
.then(user => {
if (!user) {
return res.status(404).send({
message: 'User Not Found'
})
}
return res.status(200).send(user)
})
.catch(error => res.status(400).send(error))
},
The model for subscribedcurrency looks like this
module.exports = (sequelize, DataTypes) => {
var SubscribedCurrency = sequelize.define('SubscribedCurrency', {
symbol: {
type: DataTypes.STRING,
allowNull: false
},
name: {
type: DataTypes.STRING,
allowNull: false
},
priceAtSubscription: {
type: DataTypes.STRING,
allowNull: false
}
})
SubscribedCurrency.associate = (models) => {
SubscribedCurrency.hasMany(models.User, {
foreignKey: 'userId',
onDelete: 'CASCADE'
})
}
return SubscribedCurrency
}
I've tried changing how it queries, the primary key of User, just about everything I can think of. The relationship between User and subscribedCurrency is many to many.
without the include statement and the findOne query works perfect fine!
I think all you need is to remove as: 'subscribed currency' , as you haven't define association with alias.
So From this :
include: [{
model: subscribedcurrency,
as: 'subscribed currency'
}]
To this :
include: [{
model: subscribedcurrency,
}]

Multiple chained promises is sailsjs

This is my first attempt at attempting to chain multiple finds together. The debug running shows that all the code executes correctly but there is a delay in receiving the users array back and therefore unable to present the data back.
The concept is a user may belong to multiple organizations, and there may be more than one user (other than the current user) that may belong to organizations. The function is trying to receive all users for all the organizations the current user belongs to.
getUserOrganizationsUsers: function (userId) {
var users = [];
sails.log.info('Getting the current users organizations [' + userId + ']');
return UserOrganization.find({ user_id: userId, deleted: null })
.populate('organization_id', { deleted: null })
.populate('user_id', { deleted: null })
.then(function (userorganization) {
return userorganization;
})
.then(function (userorgs) {
/* From all the organizations I want to get all the users from those organizations */
_.forEach(userorgs, function (userorg) {
UserOrganization.find({ organization_id: userorg.organization_id.id })
.populate('organization_id', { deleted: null })
.populate('user_id', { deleted: null })
.then(function (otherusrs) {
_.forEach(otherusrs, function (otherusr) {
sails.log.info('other userss each loop ');
var users = _.find(otherusrs, {id: otherusr.organization_id.id});
users.push(users);
})
})
});
return Q.when(employees);
})
},
Organization.js
module.exports = {
attributes: {
companyName: {
type: 'string',
required: true
},
Address: {
type: 'string'
},
ABN: {
type: 'string'
},
City: {
type: 'string'
},
contactNumber: {
type: 'string'
},
country: {
type: 'string'
},
icon: {
type: 'string'
},
users:
{ collection: 'userorganization',
via : 'user_id'
},
deleted: {
type: 'date',
defaultsTo: null
},
toJSON: function () {
var obj = this.toObject();
obj = _.pick(obj, Organization.publicFields);
return obj;
}
},
editableFields: [
'companyName',
'users'
// 'industries'
],
publicFields: [
'id',
'companyName',
'users'
],
};
UserOrganization.js
module.exports = {
attributes: {
organization_id: {
model : 'organization',
required: true
},
user_id: {
model: 'user',
required: true
},
organizationRole: {
type: 'string',
required: true
},
deleted: {
type: 'date',
defaultsTo: null
},
toJSON: function () {
var obj = this.toObject();
obj = _.pick(obj, UserOrganization.publicFields);
return obj;
}
},
editableFields: [
'organization_id',
'user_id',
'organizationRole',
],
publicFields: [
'id',
'organization_id',
'user_id',
'organizationRole'
],
};
and the user.js
var bcrypt = require('bcrypt-nodejs');
module.exports = {
attributes: {
email: {
type: 'email',
required: true,
unique: true
},
password: {
type: 'string',
required: true
},
firstName: {
type: 'string'
},
lastName: {
type: 'string'
},
verified: {
type: 'boolean',
defaultsTo: false
},
organizations:
{ collection: 'userorganization',
via : 'user_id'
}, deleted: {
type: 'date',
defaultsTo: null
},
fullName: function () {
return this.firstName + ' ' + this.lastName;
},
toJSON: function () {
var obj = this.toObject();
obj = _.pick(obj, User.publicFields);
return obj;
}
},
// TODO: Add initialFields
editableFields: [
'password',
'email',
'firstName',
'lastName',
'organizations'],
publicFields: [
'id',
'email',
'verified',
'firstName',
'lastName',
'fullName',
'organizations'
],
comparePassword: function (password, user, cb) {
bcrypt.compare(password, user.password, function (err, match) {
if(err) return cb(err);
cb(null, match);
})
},
beforeCreate: function (user, cb) {
bcrypt.genSalt(10, function (err, salt) {
bcrypt.hash(user.password, salt, function () {}, function (err, hash) {
if (err) {
sails.log.error(err);
return cb(err);
}
user.password = hash;
cb(null, user);
});
});
}
};
Okay, I think I understand what you're doing. It would be a lot simpler to have the User belong to an organization directly.
Anyways, if I understood your model structure correctly, something like this should work:
getUserOrganizationsUsers: function (userId) {
UserOrganization.find({ user_id: userId, deleted: null })
.then(function (userOrgs) {
// return array of organization IDs
return _.map(userOrgs, function(org){
return org.id;
});
})
.then(function (userOrgs) {
Organization.find(userOrgs)
.populate('users') // users is a collection of UserOrganization
.exec(function(err, orgs){ // lookup organizations
if(err) //handle error
else {
return _.flatten( // return basic array for next promise handler
_.map(orgs, function(org){ // for each organization
return _.map(org.users, function(user){ // return an array of user_ids
return user.user_id;
})
})
)
}
})
})
.then(function(allUserOrgs){
UserOrganization.find(allUserOrgs)
.populate('user_id')
.exec(function(err, userOrgsList){
return _.map(userOrgsList, function(user){
return user.user_id;
})
})
})
.then(function(users){
// users should be an array of all the users form allt he organizations that the current users belongs to
})
},