simplified message model
This commit is contained in:
61
backend/migrations/20241124000007-create-messages.js
Normal file
61
backend/migrations/20241124000007-create-messages.js
Normal file
@@ -0,0 +1,61 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.createTable("Messages", {
|
||||
id: {
|
||||
type: Sequelize.UUID,
|
||||
defaultValue: Sequelize.UUIDV4,
|
||||
primaryKey: true,
|
||||
},
|
||||
senderId: {
|
||||
type: Sequelize.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: "Users",
|
||||
key: "id",
|
||||
},
|
||||
onUpdate: "CASCADE",
|
||||
onDelete: "CASCADE",
|
||||
},
|
||||
receiverId: {
|
||||
type: Sequelize.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: "Users",
|
||||
key: "id",
|
||||
},
|
||||
onUpdate: "CASCADE",
|
||||
onDelete: "CASCADE",
|
||||
},
|
||||
content: {
|
||||
type: Sequelize.TEXT,
|
||||
allowNull: false,
|
||||
},
|
||||
isRead: {
|
||||
type: Sequelize.BOOLEAN,
|
||||
defaultValue: false,
|
||||
},
|
||||
imagePath: {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
createdAt: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
},
|
||||
updatedAt: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
},
|
||||
});
|
||||
|
||||
// Add indexes
|
||||
await queryInterface.addIndex("Messages", ["senderId"]);
|
||||
await queryInterface.addIndex("Messages", ["receiverId"]);
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.dropTable("Messages");
|
||||
},
|
||||
};
|
||||
@@ -23,10 +23,6 @@ const Message = sequelize.define('Message', {
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
subject: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
content: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false
|
||||
@@ -35,14 +31,6 @@ const Message = sequelize.define('Message', {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false
|
||||
},
|
||||
parentMessageId: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: 'Messages',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
imagePath: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
|
||||
@@ -27,11 +27,6 @@ User.hasMany(Message, { as: "sentMessages", foreignKey: "senderId" });
|
||||
User.hasMany(Message, { as: "receivedMessages", foreignKey: "receiverId" });
|
||||
Message.belongsTo(User, { as: "sender", foreignKey: "senderId" });
|
||||
Message.belongsTo(User, { as: "receiver", foreignKey: "receiverId" });
|
||||
Message.hasMany(Message, { as: "replies", foreignKey: "parentMessageId" });
|
||||
Message.belongsTo(Message, {
|
||||
as: "parentMessage",
|
||||
foreignKey: "parentMessageId",
|
||||
});
|
||||
|
||||
// Forum associations
|
||||
User.hasMany(ForumPost, { as: "forumPosts", foreignKey: "authorId" });
|
||||
|
||||
@@ -171,11 +171,11 @@ router.get('/sent', authenticateToken, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Get a single message with replies
|
||||
// Get a single message
|
||||
router.get('/:id', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const message = await Message.findOne({
|
||||
where: {
|
||||
where: {
|
||||
id: req.params.id,
|
||||
[require('sequelize').Op.or]: [
|
||||
{ senderId: req.user.id },
|
||||
@@ -192,15 +192,6 @@ router.get('/:id', authenticateToken, async (req, res) => {
|
||||
model: User,
|
||||
as: 'receiver',
|
||||
attributes: ['id', 'firstName', 'lastName', 'profileImage']
|
||||
},
|
||||
{
|
||||
model: Message,
|
||||
as: 'replies',
|
||||
include: [{
|
||||
model: User,
|
||||
as: 'sender',
|
||||
attributes: ['id', 'firstName', 'lastName', 'profileImage']
|
||||
}]
|
||||
}
|
||||
]
|
||||
});
|
||||
@@ -248,7 +239,7 @@ router.get('/:id', authenticateToken, async (req, res) => {
|
||||
// Send a new message
|
||||
router.post('/', authenticateToken, uploadMessageImage, async (req, res) => {
|
||||
try {
|
||||
const { receiverId, subject, content, parentMessageId } = req.body;
|
||||
const { receiverId, content } = req.body;
|
||||
|
||||
// Check if receiver exists
|
||||
const receiver = await User.findByPk(receiverId);
|
||||
@@ -267,9 +258,7 @@ router.post('/', authenticateToken, uploadMessageImage, async (req, res) => {
|
||||
const message = await Message.create({
|
||||
senderId: req.user.id,
|
||||
receiverId,
|
||||
subject,
|
||||
content,
|
||||
parentMessageId,
|
||||
imagePath
|
||||
});
|
||||
|
||||
@@ -308,8 +297,7 @@ router.post('/', authenticateToken, uploadMessageImage, async (req, res) => {
|
||||
reqLogger.info("Message sent", {
|
||||
senderId: req.user.id,
|
||||
receiverId: receiverId,
|
||||
messageId: message.id,
|
||||
isReply: !!parentMessageId
|
||||
messageId: message.id
|
||||
});
|
||||
|
||||
res.status(201).json(messageWithSender);
|
||||
|
||||
@@ -39,7 +39,6 @@ class MessagingEmailService {
|
||||
* @param {string} sender.firstName - Sender's first name
|
||||
* @param {string} sender.lastName - Sender's last name
|
||||
* @param {Object} message - Message object
|
||||
* @param {string} message.subject - Message subject
|
||||
* @param {string} message.content - Message content
|
||||
* @param {Date} message.createdAt - Message creation timestamp
|
||||
* @returns {Promise<{success: boolean, messageId?: string, error?: string}>}
|
||||
@@ -61,7 +60,6 @@ class MessagingEmailService {
|
||||
const variables = {
|
||||
recipientName: receiver.firstName || "there",
|
||||
senderName: `${sender.firstName} ${sender.lastName}`.trim() || "A user",
|
||||
subject: message.subject,
|
||||
messageContent: message.content,
|
||||
conversationUrl: conversationUrl,
|
||||
timestamp: timestamp,
|
||||
|
||||
@@ -134,13 +134,6 @@
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.message-box .subject {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
margin: 0 0 15px 0;
|
||||
}
|
||||
|
||||
.message-box .content-text {
|
||||
color: #212529;
|
||||
line-height: 1.6;
|
||||
@@ -226,7 +219,6 @@
|
||||
<p>{{senderName}} sent you a message on RentAll.</p>
|
||||
|
||||
<div class="message-box">
|
||||
<div class="subject">Subject: {{subject}}</div>
|
||||
<div class="content-text">{{messageContent}}</div>
|
||||
<div class="timestamp">Sent {{timestamp}}</div>
|
||||
</div>
|
||||
|
||||
@@ -56,7 +56,6 @@ describe('Messages Routes', () => {
|
||||
id: 1,
|
||||
senderId: 2,
|
||||
receiverId: 1,
|
||||
subject: 'Test Message',
|
||||
content: 'Hello there!',
|
||||
isRead: false,
|
||||
createdAt: '2024-01-15T10:00:00.000Z',
|
||||
@@ -71,7 +70,6 @@ describe('Messages Routes', () => {
|
||||
id: 2,
|
||||
senderId: 3,
|
||||
receiverId: 1,
|
||||
subject: 'Another Message',
|
||||
content: 'Hi!',
|
||||
isRead: true,
|
||||
createdAt: '2024-01-14T10:00:00.000Z',
|
||||
@@ -122,7 +120,6 @@ describe('Messages Routes', () => {
|
||||
id: 3,
|
||||
senderId: 1,
|
||||
receiverId: 2,
|
||||
subject: 'My Message',
|
||||
content: 'Hello Jane!',
|
||||
isRead: false,
|
||||
createdAt: '2024-01-15T12:00:00.000Z',
|
||||
@@ -171,7 +168,6 @@ describe('Messages Routes', () => {
|
||||
id: 1,
|
||||
senderId: 2,
|
||||
receiverId: 1,
|
||||
subject: 'Test Message',
|
||||
content: 'Hello there!',
|
||||
isRead: false,
|
||||
createdAt: '2024-01-15T10:00:00.000Z',
|
||||
@@ -187,19 +183,6 @@ describe('Messages Routes', () => {
|
||||
lastName: 'Doe',
|
||||
profileImage: 'john.jpg'
|
||||
},
|
||||
replies: [
|
||||
{
|
||||
id: 4,
|
||||
senderId: 1,
|
||||
content: 'Reply message',
|
||||
sender: {
|
||||
id: 1,
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
profileImage: 'john.jpg'
|
||||
}
|
||||
}
|
||||
],
|
||||
update: jest.fn()
|
||||
};
|
||||
|
||||
@@ -207,7 +190,7 @@ describe('Messages Routes', () => {
|
||||
mockMessageFindOne.mockResolvedValue(mockMessage);
|
||||
});
|
||||
|
||||
it('should get message with replies for receiver', async () => {
|
||||
it('should get message for receiver and mark as read', async () => {
|
||||
mockMessage.update.mockResolvedValue();
|
||||
|
||||
const response = await request(app)
|
||||
@@ -218,7 +201,6 @@ describe('Messages Routes', () => {
|
||||
id: 1,
|
||||
senderId: 2,
|
||||
receiverId: 1,
|
||||
subject: 'Test Message',
|
||||
content: 'Hello there!',
|
||||
isRead: false,
|
||||
createdAt: '2024-01-15T10:00:00.000Z',
|
||||
@@ -233,20 +215,7 @@ describe('Messages Routes', () => {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
profileImage: 'john.jpg'
|
||||
},
|
||||
replies: [
|
||||
{
|
||||
id: 4,
|
||||
senderId: 1,
|
||||
content: 'Reply message',
|
||||
sender: {
|
||||
id: 1,
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
profileImage: 'john.jpg'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
expect(mockMessage.update).toHaveBeenCalledWith({ isRead: true });
|
||||
});
|
||||
@@ -263,7 +232,6 @@ describe('Messages Routes', () => {
|
||||
id: 1,
|
||||
senderId: 1,
|
||||
receiverId: 2,
|
||||
subject: 'Test Message',
|
||||
content: 'Hello there!',
|
||||
isRead: false,
|
||||
createdAt: '2024-01-15T10:00:00.000Z',
|
||||
@@ -278,20 +246,7 @@ describe('Messages Routes', () => {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
profileImage: 'john.jpg'
|
||||
},
|
||||
replies: [
|
||||
{
|
||||
id: 4,
|
||||
senderId: 1,
|
||||
content: 'Reply message',
|
||||
sender: {
|
||||
id: 1,
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
profileImage: 'john.jpg'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
expect(mockMessage.update).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -340,9 +295,7 @@ describe('Messages Routes', () => {
|
||||
id: 5,
|
||||
senderId: 1,
|
||||
receiverId: 2,
|
||||
subject: 'New Message',
|
||||
content: 'Hello Jane!',
|
||||
parentMessageId: null
|
||||
content: 'Hello Jane!'
|
||||
};
|
||||
|
||||
const mockMessageWithSender = {
|
||||
@@ -364,9 +317,7 @@ describe('Messages Routes', () => {
|
||||
it('should create a new message', async () => {
|
||||
const messageData = {
|
||||
receiverId: 2,
|
||||
subject: 'New Message',
|
||||
content: 'Hello Jane!',
|
||||
parentMessageId: null
|
||||
content: 'Hello Jane!'
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
@@ -378,31 +329,8 @@ describe('Messages Routes', () => {
|
||||
expect(mockMessageCreate).toHaveBeenCalledWith({
|
||||
senderId: 1,
|
||||
receiverId: 2,
|
||||
subject: 'New Message',
|
||||
content: 'Hello Jane!',
|
||||
parentMessageId: null
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a reply message with parentMessageId', async () => {
|
||||
const replyData = {
|
||||
receiverId: 2,
|
||||
subject: 'Re: Original Message',
|
||||
content: 'This is a reply',
|
||||
parentMessageId: 1
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/messages')
|
||||
.send(replyData);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(mockMessageCreate).toHaveBeenCalledWith({
|
||||
senderId: 1,
|
||||
receiverId: 2,
|
||||
subject: 'Re: Original Message',
|
||||
content: 'This is a reply',
|
||||
parentMessageId: 1
|
||||
imagePath: null
|
||||
});
|
||||
});
|
||||
|
||||
@@ -413,7 +341,6 @@ describe('Messages Routes', () => {
|
||||
.post('/messages')
|
||||
.send({
|
||||
receiverId: 999,
|
||||
subject: 'Test',
|
||||
content: 'Test message'
|
||||
});
|
||||
|
||||
@@ -426,7 +353,6 @@ describe('Messages Routes', () => {
|
||||
.post('/messages')
|
||||
.send({
|
||||
receiverId: 1, // Same as sender ID
|
||||
subject: 'Self Message',
|
||||
content: 'Hello self!'
|
||||
});
|
||||
|
||||
@@ -441,7 +367,6 @@ describe('Messages Routes', () => {
|
||||
.post('/messages')
|
||||
.send({
|
||||
receiverId: 2,
|
||||
subject: 'Test',
|
||||
content: 'Test message'
|
||||
});
|
||||
|
||||
@@ -596,62 +521,5 @@ describe('Messages Routes', () => {
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle message with no replies', async () => {
|
||||
const messageWithoutReplies = {
|
||||
id: 1,
|
||||
senderId: 2,
|
||||
receiverId: 1,
|
||||
subject: 'Test Message',
|
||||
content: 'Hello there!',
|
||||
isRead: false,
|
||||
replies: [],
|
||||
update: jest.fn()
|
||||
};
|
||||
mockMessageFindOne.mockResolvedValue(messageWithoutReplies);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/messages/1');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.replies).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle missing optional fields in message creation', async () => {
|
||||
const mockReceiver = { id: 2, firstName: 'Jane', lastName: 'Smith' };
|
||||
const mockCreatedMessage = {
|
||||
id: 6,
|
||||
senderId: 1,
|
||||
receiverId: 2,
|
||||
subject: undefined,
|
||||
content: 'Just content',
|
||||
parentMessageId: undefined
|
||||
};
|
||||
const mockMessageWithSender = {
|
||||
...mockCreatedMessage,
|
||||
sender: { id: 1, firstName: 'John', lastName: 'Doe' }
|
||||
};
|
||||
|
||||
mockUserFindByPk.mockResolvedValue(mockReceiver);
|
||||
mockMessageCreate.mockResolvedValue(mockCreatedMessage);
|
||||
mockMessageFindByPk.mockResolvedValue(mockMessageWithSender);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/messages')
|
||||
.send({
|
||||
receiverId: 2,
|
||||
content: 'Just content'
|
||||
// subject and parentMessageId omitted
|
||||
});
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(mockMessageCreate).toHaveBeenCalledWith({
|
||||
senderId: 1,
|
||||
receiverId: 2,
|
||||
subject: undefined,
|
||||
content: 'Just content',
|
||||
parentMessageId: undefined
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user