From fadb9c19d862e4620b06c2ad92b4b5399dec18d3 Mon Sep 17 00:00:00 2001 From: Denys Semeniuk Date: Fri, 17 Oct 2025 14:48:04 +0300 Subject: [PATCH 1/3] Solution --- src/controllers/message.controller.js | 35 +++++++++++++++ src/controllers/rooms.controller.js | 51 ++++++++++++++++++++++ src/controllers/user.controller.js | 23 ++++++++++ src/db.js | 10 +++++ src/index.js | 63 +++++++++++++++++++++++++++ src/models/message.js | 31 +++++++++++++ src/models/room.js | 23 ++++++++++ src/models/user.js | 16 +++++++ src/routes/message.route.js | 7 +++ src/routes/rooms.route.js | 9 ++++ src/routes/user.route.js | 6 +++ src/services/message.service.js | 14 ++++++ src/services/rooms.service.js | 42 ++++++++++++++++++ src/services/user.service.js | 9 ++++ 14 files changed, 339 insertions(+) create mode 100644 src/controllers/message.controller.js create mode 100644 src/controllers/rooms.controller.js create mode 100644 src/controllers/user.controller.js create mode 100644 src/db.js create mode 100644 src/models/message.js create mode 100644 src/models/room.js create mode 100644 src/models/user.js create mode 100644 src/routes/message.route.js create mode 100644 src/routes/rooms.route.js create mode 100644 src/routes/user.route.js create mode 100644 src/services/message.service.js create mode 100644 src/services/rooms.service.js create mode 100644 src/services/user.service.js diff --git a/src/controllers/message.controller.js b/src/controllers/message.controller.js new file mode 100644 index 000000000..e0ad8a79c --- /dev/null +++ b/src/controllers/message.controller.js @@ -0,0 +1,35 @@ +import { EventEmitter } from 'events'; +import { messageService } from '../services/message.service.js'; + +export const messageEmitter = new EventEmitter(); + +const create = async (req, res) => { + const { roomId } = req.params; + const { text, userId } = req.body; + + if (!roomId || !userId || !text) { + return res.sendStatus(404); + } + + const newMessage = await messageService.createMessageInRoom( + text, + userId, + roomId, + ); + + messageEmitter.emit('message', newMessage); + + res.status(201).json(newMessage); +}; + +export const getMessages = async (req, res) => { + const { roomId } = req.params; + const messages = await messageService.getAllMessagesInRoom(roomId); + + res.status(200).json(messages); +}; + +export const messageController = { + create, + getMessages, +}; diff --git a/src/controllers/rooms.controller.js b/src/controllers/rooms.controller.js new file mode 100644 index 000000000..1fe9657fe --- /dev/null +++ b/src/controllers/rooms.controller.js @@ -0,0 +1,51 @@ +import { roomsService } from '../services/rooms.service.js'; + +const getAllRooms = async (req, res) => { + const rooms = await roomsService.getAllRooms(); + + res.status(200).send(rooms); +}; + +const createRoom = async (req, res) => { + const { title, userId, description } = req.body; + + if (!title || !userId) { + return res.sendStatus(404); + } + + await roomsService.createRoom(title, userId, description); + + res.sendStatus(201); +}; + +const updateRoom = async (req, res) => { + const { roomId } = req.params; + const { title, description } = req.body; + + if ((!title && !description) || !roomId) { + return res.sendStatus(404); + } + + await roomsService.updateRoom(roomId, title, description); + + res.sendStatus(204); +}; + +const deleteRoom = async (req, res) => { + const { roomId } = req.params; + + if (!roomId) { + return res.sendStatus(400); + } + + await roomsService.deleteRoom(roomId); + + res.sendStatus(204); +}; + +export const roomController = { + getAllRooms, + createRoom, + updateRoom, + deleteRoom, +}; diff --git a/src/controllers/user.controller.js b/src/controllers/user.controller.js new file mode 100644 index 000000000..ca64e4420 --- /dev/null +++ b/src/controllers/user.controller.js @@ -0,0 +1,23 @@ +import { User } from '../models/user.js'; +import { userService } from '../services/user.service.js'; + +const create = async (req, res) => { + const { name } = req.body; + const isNameExist = await User.findOne({ where: { name } }); + + if (!name) { + return res.sendStatus(404); + } + + if (isNameExist) { + return res.status(409).json({ message: 'User already exists' }); + } + + await userService.createUser(name); + + return res.status(201).json({ message: 'User was created!' }); +}; + +export const userController = { + create, +}; diff --git a/src/db.js b/src/db.js new file mode 100644 index 000000000..983a53d23 --- /dev/null +++ b/src/db.js @@ -0,0 +1,10 @@ +import 'dotenv/config'; +import { Sequelize } from 'sequelize'; + +export const client = new Sequelize({ + host: process.env.DB_HOST, + username: process.env.DB_USERNAME, + password: process.env.DB_PASS, + database: process.env.DB_DATABASE, + dialect: 'postgres', +}); diff --git a/src/index.js b/src/index.js index ad9a93a7c..31f2d29c4 100644 --- a/src/index.js +++ b/src/index.js @@ -1 +1,64 @@ 'use strict'; +import 'dotenv/config'; +import express from 'express'; +import { WebSocketServer } from 'ws'; +import cors from 'cors'; +import { userRoute } from './routes/user.route.js'; +import { Message } from './models/message.js'; +import { roomsRoute } from './routes/rooms.route.js'; +import { messageRoute } from './routes/message.route.js'; +import { messageEmitter } from './controllers/message.controller.js'; + +const PORT = process.env.PORT || 5000; +const app = express(); + +app.use(cors()); +app.use(express.json()); +app.use('/user', userRoute); +app.use('/rooms', roomsRoute); +app.use(messageRoute); + +const server = app.listen(PORT, () => { + // eslint-disable-next-line no-console + console.log(`Server was started on ${PORT}`); +}); + +const wss = new WebSocketServer({ server }); + +const rooms = {}; + +wss.on('connection', (ws) => { + ws.on('message', async (message) => { + const { roomId } = JSON.parse(message); + + if (!rooms[roomId]) { + rooms[roomId] = []; + } + + rooms[roomId].push(ws); + + const messages = await Message.findAll({ where: { roomId } }); + + messages.forEach((msg) => { + ws.send(JSON.stringify(msg)); + }); + }); + + ws.on('close', () => { + for (const roomId in rooms) { + rooms[roomId] = rooms[roomId].filter((client) => client !== ws); + } + }); +}); + +messageEmitter.on('message', async (message) => { + const { roomId } = message; + + if (rooms[roomId]) { + rooms[roomId].forEach((client) => { + if (client.readyState === 1) { + client.send(JSON.stringify(message)); + } + }); + } +}); diff --git a/src/models/message.js b/src/models/message.js new file mode 100644 index 000000000..5dd7db679 --- /dev/null +++ b/src/models/message.js @@ -0,0 +1,31 @@ +import { DataTypes } from 'sequelize'; +import { client } from '../db.js'; +import { User } from './user.js'; +import { Room } from './room.js'; + +export const Message = client.define( + 'message', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + allowNull: false, + primaryKey: true, + }, + text: { + type: DataTypes.STRING, + allowNull: false, + }, + createdAt: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW, + allowNull: false, + }, + }, + { updatedAt: false }, +); + +Message.belongsTo(User); +User.hasMany(Message); +Message.belongsTo(Room); +Room.hasMany(Message); diff --git a/src/models/room.js b/src/models/room.js new file mode 100644 index 000000000..bdb696d57 --- /dev/null +++ b/src/models/room.js @@ -0,0 +1,23 @@ +import { DataTypes } from 'sequelize'; +import { client } from '../db.js'; +import { User } from './user.js'; + +export const Room = client.define('room', { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + allowNull: false, + primaryKey: true, + }, + title: { + type: DataTypes.STRING, + allowNull: false, + }, + description: { + type: DataTypes.STRING, + allowNull: false, + }, +}); + +Room.belongsTo(User); +User.hasMany(Room); diff --git a/src/models/user.js b/src/models/user.js new file mode 100644 index 000000000..5c6da6b3f --- /dev/null +++ b/src/models/user.js @@ -0,0 +1,16 @@ +import { DataTypes } from 'sequelize'; +import { client } from '../db.js'; + +export const User = client.define('user', { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + allowNull: false, + primaryKey: true, + }, + name: { + type: DataTypes.STRING, + unique: true, + allowNull: false, + }, +}); diff --git a/src/routes/message.route.js b/src/routes/message.route.js new file mode 100644 index 000000000..6b22af319 --- /dev/null +++ b/src/routes/message.route.js @@ -0,0 +1,7 @@ +import express from 'express'; +import { messageController } from '../controllers/message.controller.js'; + +export const messageRoute = new express.Router(); + +messageRoute.get('/rooms/:roomId/messages', messageController.getMessages); +messageRoute.post('/rooms/:roomId/messages', messageController.create); diff --git a/src/routes/rooms.route.js b/src/routes/rooms.route.js new file mode 100644 index 000000000..abe96d678 --- /dev/null +++ b/src/routes/rooms.route.js @@ -0,0 +1,9 @@ +import express from 'express'; +import { roomController } from '../controllers/rooms.controller.js'; + +export const roomsRoute = new express.Router(); + +roomsRoute.get('/', roomController.getAllRooms); +roomsRoute.post('/', roomController.createRoom); +roomsRoute.patch('/:roomId', roomController.updateRoom); +roomsRoute.delete('/:roomId', roomController.deleteRoom); diff --git a/src/routes/user.route.js b/src/routes/user.route.js new file mode 100644 index 000000000..7a9f69903 --- /dev/null +++ b/src/routes/user.route.js @@ -0,0 +1,6 @@ +import express from 'express'; +import { userController } from '../controllers/user.controller.js'; + +export const userRoute = new express.Router(); + +userRoute.post('/', userController.create); diff --git a/src/services/message.service.js b/src/services/message.service.js new file mode 100644 index 000000000..f5881f90b --- /dev/null +++ b/src/services/message.service.js @@ -0,0 +1,14 @@ +import { Message } from '../models/message.js'; + +const getAllMessagesInRoom = (roomId) => { + return Message.findAll({ where: { roomId } }); +}; + +const createMessageInRoom = (text, userId, roomId) => { + return Message.create({ text, userId, roomId }); +}; + +export const messageService = { + getAllMessagesInRoom, + createMessageInRoom, +}; diff --git a/src/services/rooms.service.js b/src/services/rooms.service.js new file mode 100644 index 000000000..36f2a3496 --- /dev/null +++ b/src/services/rooms.service.js @@ -0,0 +1,42 @@ +import { Room } from '../models/room.js'; + +const getAllRooms = () => { + return Room.findAll(); +}; + +const createRoom = (title, userId, description = '') => { + return Room.create({ title, userId, description }); +}; + +const deleteRoom = (id) => { + return Room.destroy({ where: { id } }); +}; + +const updateRoom = (id, title, description) => { + if (!id) { + throw new Error('Room id is required'); + } + + const updatedData = {}; + + if (title) { + updatedData.title = title; + } + + if (description) { + updatedData.description = description; + } + + if (Object.keys(updatedData).length === 0) { + throw new Error('At least one field must be provided'); + } + + return Room.update(updatedData, { where: { id } }); +}; + +export const roomsService = { + getAllRooms, + createRoom, + deleteRoom, + updateRoom, +}; diff --git a/src/services/user.service.js b/src/services/user.service.js new file mode 100644 index 000000000..ac7513b77 --- /dev/null +++ b/src/services/user.service.js @@ -0,0 +1,9 @@ +import { User } from '../models/user.js'; + +const createUser = async (name) => { + return User.create({ name }); +}; + +export const userService = { + createUser, +}; From baee4641c189a9eac56b24c990c312164cd6ec04 Mon Sep 17 00:00:00 2001 From: Denys Semeniuk Date: Fri, 17 Oct 2025 15:07:43 +0300 Subject: [PATCH 2/3] Solution --- src/controllers/message.controller.js | 60 +++++++++++++++++++++------ src/controllers/user.controller.js | 25 ++++++----- src/index.js | 51 ++++++++++++++++------- src/models/message.js | 14 ++++--- src/services/message.service.js | 33 +++++++++++++-- 5 files changed, 136 insertions(+), 47 deletions(-) diff --git a/src/controllers/message.controller.js b/src/controllers/message.controller.js index e0ad8a79c..bccc8323c 100644 --- a/src/controllers/message.controller.js +++ b/src/controllers/message.controller.js @@ -1,5 +1,6 @@ import { EventEmitter } from 'events'; import { messageService } from '../services/message.service.js'; +import { User } from '../models/user.model.js'; export const messageEmitter = new EventEmitter(); @@ -7,26 +8,61 @@ const create = async (req, res) => { const { roomId } = req.params; const { text, userId } = req.body; - if (!roomId || !userId || !text) { - return res.sendStatus(404); + if (!roomId || !userId || !text || typeof text !== 'string') { + return res.status(400).json({ message: 'Missing or invalid parameters' }); } - const newMessage = await messageService.createMessageInRoom( - text, - userId, - roomId, - ); + try { + const newMessage = await messageService.createMessageInRoom( + text, + userId, + roomId, + ); - messageEmitter.emit('message', newMessage); + const user = await User.findByPk(userId); - res.status(201).json(newMessage); + if (!user) { + return res.status(404).json({ message: 'User not found' }); + } + + const formattedMessage = { + author: user.name, + time: newMessage.createdAt, + text: newMessage.text, + }; + + messageEmitter.emit('message', formattedMessage); + + res.status(201).json(formattedMessage); + } catch (err) { + // eslint-disable-next-line no-console + console.error('Error creating message:', err); + res.status(500).json({ message: 'Internal server error' }); + } }; -export const getMessages = async (req, res) => { +const getMessages = async (req, res) => { const { roomId } = req.params; - const messages = await messageService.getAllMessagesInRoom(roomId); - res.status(200).json(messages); + if (!roomId) { + return res.status(400).json({ message: 'Missing roomId' }); + } + + try { + const messages = await messageService.getAllMessagesInRoom(roomId); + + const formattedMessages = messages.map((m) => ({ + author: m.User?.name || 'Unknown', + time: m.createdAt, + text: m.text, + })); + + res.status(200).json(formattedMessages); + } catch (err) { + // eslint-disable-next-line no-console + console.error('Error getting messages:', err); + res.status(500).json({ message: 'Internal server error' }); + } }; export const messageController = { diff --git a/src/controllers/user.controller.js b/src/controllers/user.controller.js index ca64e4420..c32742b4a 100644 --- a/src/controllers/user.controller.js +++ b/src/controllers/user.controller.js @@ -2,20 +2,25 @@ import { User } from '../models/user.js'; import { userService } from '../services/user.service.js'; const create = async (req, res) => { - const { name } = req.body; - const isNameExist = await User.findOne({ where: { name } }); + try { + const { name } = req.body; - if (!name) { - return res.sendStatus(404); - } + if (!name) { + return res.status(400).json({ message: 'Name is required' }); + } - if (isNameExist) { - return res.status(409).json({ message: 'User already exists' }); - } + const isNameExist = await User.findOne({ where: { name } }); - await userService.createUser(name); + if (isNameExist) { + return res.status(409).json({ message: 'User already exists' }); + } - return res.status(201).json({ message: 'User was created!' }); + await userService.createUser(name); + + return res.status(201).json({ message: 'User was created!' }); + } catch { + return res.status(500).json({ message: 'Internal server error' }); + } }; export const userController = { diff --git a/src/index.js b/src/index.js index 31f2d29c4..0eda7b768 100644 --- a/src/index.js +++ b/src/index.js @@ -4,40 +4,51 @@ import express from 'express'; import { WebSocketServer } from 'ws'; import cors from 'cors'; import { userRoute } from './routes/user.route.js'; -import { Message } from './models/message.js'; import { roomsRoute } from './routes/rooms.route.js'; import { messageRoute } from './routes/message.route.js'; import { messageEmitter } from './controllers/message.controller.js'; +import { messageService } from './services/message.service.js'; const PORT = process.env.PORT || 5000; const app = express(); app.use(cors()); app.use(express.json()); + app.use('/user', userRoute); app.use('/rooms', roomsRoute); app.use(messageRoute); -const server = app.listen(PORT, () => { - // eslint-disable-next-line no-console - console.log(`Server was started on ${PORT}`); -}); +const server = app.listen(PORT); const wss = new WebSocketServer({ server }); - const rooms = {}; wss.on('connection', (ws) => { - ws.on('message', async (message) => { - const { roomId } = JSON.parse(message); + ws.on('message', async (rawMessage) => { + let parsed; + + try { + parsed = JSON.parse(rawMessage); + } catch { + return; + } + + const { roomId } = parsed; + + if (!roomId || typeof roomId !== 'number') { + return; + } if (!rooms[roomId]) { rooms[roomId] = []; } - rooms[roomId].push(ws); + if (!rooms[roomId].includes(ws)) { + rooms[roomId].push(ws); + } - const messages = await Message.findAll({ where: { roomId } }); + const messages = await messageService.getAllMessagesInRoom(roomId); messages.forEach((msg) => { ws.send(JSON.stringify(msg)); @@ -54,11 +65,19 @@ wss.on('connection', (ws) => { messageEmitter.on('message', async (message) => { const { roomId } = message; - if (rooms[roomId]) { - rooms[roomId].forEach((client) => { - if (client.readyState === 1) { - client.send(JSON.stringify(message)); - } - }); + if (!roomId || !rooms[roomId]) { + return; } + + const formatted = { + author: message.author || message.User?.name || 'Unknown', + time: message.time || message.createdAt, + text: message.text, + }; + + rooms[roomId].forEach((client) => { + if (client.readyState === 1) { + client.send(JSON.stringify(formatted)); + } + }); }); diff --git a/src/models/message.js b/src/models/message.js index 5dd7db679..a03e5ae2f 100644 --- a/src/models/message.js +++ b/src/models/message.js @@ -22,10 +22,14 @@ export const Message = client.define( allowNull: false, }, }, - { updatedAt: false }, + { + updatedAt: false, + tableName: 'messages', + }, ); -Message.belongsTo(User); -User.hasMany(Message); -Message.belongsTo(Room); -Room.hasMany(Message); +Message.belongsTo(User, { foreignKey: 'userId' }); +User.hasMany(Message, { foreignKey: 'userId' }); + +Message.belongsTo(Room, { foreignKey: 'roomId' }); +Room.hasMany(Message, { foreignKey: 'roomId' }); diff --git a/src/services/message.service.js b/src/services/message.service.js index f5881f90b..94a40bdc3 100644 --- a/src/services/message.service.js +++ b/src/services/message.service.js @@ -1,11 +1,36 @@ import { Message } from '../models/message.js'; +import { User } from '../models/user.model.js'; -const getAllMessagesInRoom = (roomId) => { - return Message.findAll({ where: { roomId } }); +const getAllMessagesInRoom = async (roomId) => { + const messages = await Message.findAll({ + where: { roomId }, + include: [ + { + model: User, + attributes: ['name'], + }, + ], + order: [['createdAt', 'ASC']], + }); + + return messages.map((msg) => ({ + author: msg.User?.name || 'Unknown', + time: msg.createdAt, + text: msg.text, + })); }; -const createMessageInRoom = (text, userId, roomId) => { - return Message.create({ text, userId, roomId }); +const createMessageInRoom = async (text, userId, roomId) => { + const newMessage = await Message.create({ text, userId, roomId }); + + // Підтягуємо користувача, щоб сформувати коректний author + const user = await User.findByPk(userId); + + return { + author: user?.name || 'Unknown', + time: newMessage.createdAt, + text: newMessage.text, + }; }; export const messageService = { From 013757c3d9440f5d84285617cb49b7724fc9d002 Mon Sep 17 00:00:00 2001 From: Denys Semeniuk Date: Fri, 17 Oct 2025 15:39:25 +0300 Subject: [PATCH 3/3] Solution --- src/services/message.service.js | 32 +++++++++++++++++------------ src/services/rooms.service.js | 36 ++++++++++++++++++++++++++------- 2 files changed, 48 insertions(+), 20 deletions(-) diff --git a/src/services/message.service.js b/src/services/message.service.js index 94a40bdc3..db815febc 100644 --- a/src/services/message.service.js +++ b/src/services/message.service.js @@ -1,35 +1,41 @@ import { Message } from '../models/message.js'; -import { User } from '../models/user.model.js'; +import { User } from '../models/user.js'; const getAllMessagesInRoom = async (roomId) => { + if (!roomId) { + throw new Error('roomId is required'); + } + const messages = await Message.findAll({ where: { roomId }, - include: [ - { - model: User, - attributes: ['name'], - }, - ], + include: [{ model: User, attributes: ['name'] }], order: [['createdAt', 'ASC']], }); return messages.map((msg) => ({ - author: msg.User?.name || 'Unknown', + author: msg.user?.name || 'Unknown', time: msg.createdAt, text: msg.text, + roomId: msg.roomId, })); }; const createMessageInRoom = async (text, userId, roomId) => { + if (!text || !userId || !roomId) { + throw new Error('Missing required fields'); + } + const newMessage = await Message.create({ text, userId, roomId }); - // Підтягуємо користувача, щоб сформувати коректний author - const user = await User.findByPk(userId); + const messageWithUser = await Message.findByPk(newMessage.id, { + include: [{ model: User, attributes: ['name'] }], + }); return { - author: user?.name || 'Unknown', - time: newMessage.createdAt, - text: newMessage.text, + author: messageWithUser.user?.name || 'Unknown', + time: messageWithUser.createdAt, + text: messageWithUser.text, + roomId: messageWithUser.roomId, }; }; diff --git a/src/services/rooms.service.js b/src/services/rooms.service.js index 36f2a3496..1ad07b1f3 100644 --- a/src/services/rooms.service.js +++ b/src/services/rooms.service.js @@ -1,18 +1,34 @@ import { Room } from '../models/room.js'; -const getAllRooms = () => { - return Room.findAll(); +const getAllRooms = async () => { + return Room.findAll({ + order: [['createdAt', 'DESC']], + }); }; -const createRoom = (title, userId, description = '') => { +const createRoom = async (title, userId, description = '') => { + if (!title || !userId) { + throw new Error('Title and userId are required'); + } + return Room.create({ title, userId, description }); }; -const deleteRoom = (id) => { - return Room.destroy({ where: { id } }); +const deleteRoom = async (id) => { + if (!id) { + throw new Error('Room id is required'); + } + + const deletedCount = await Room.destroy({ where: { id } }); + + if (deletedCount === 0) { + throw new Error('Room not found'); + } + + return deletedCount; }; -const updateRoom = (id, title, description) => { +const updateRoom = async (id, title, description) => { if (!id) { throw new Error('Room id is required'); } @@ -31,7 +47,13 @@ const updateRoom = (id, title, description) => { throw new Error('At least one field must be provided'); } - return Room.update(updatedData, { where: { id } }); + const [updatedCount] = await Room.update(updatedData, { where: { id } }); + + if (updatedCount === 0) { + throw new Error('Room not found'); + } + + return updatedCount; }; export const roomsService = {