-
Notifications
You must be signed in to change notification settings - Fork 275
add solution #127
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add solution #127
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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, | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When required fields like |
||
| } | ||
|
|
||
| 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); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar to the |
||
| } | ||
|
|
||
| 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, | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 } }); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The logic to check for an existing user is a piece of business logic that should be encapsulated within the |
||
|
|
||
| if (!name) { | ||
| return res.sendStatus(404); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When a required field like |
||
| } | ||
|
|
||
| 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, | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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', | ||
| }); | ||
|
Comment on lines
+4
to
+10
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider adding a |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 } }); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This direct database query using the |
||
|
|
||
| messages.forEach((msg) => { | ||
| ws.send(JSON.stringify(msg)); | ||
| }); | ||
| }); | ||
|
Comment on lines
+31
to
+45
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The logic in the 'message' handler is flawed. The line |
||
|
|
||
| ws.on('close', () => { | ||
| for (const roomId in rooms) { | ||
| rooms[roomId] = rooms[roomId].filter((client) => client !== ws); | ||
| } | ||
| }); | ||
|
Comment on lines
+47
to
+51
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
| }); | ||
|
|
||
| messageEmitter.on('message', async (message) => { | ||
| const { roomId } = message; | ||
|
|
||
| if (rooms[roomId]) { | ||
| rooms[roomId].forEach((client) => { | ||
| if (client.readyState === 1) { | ||
| client.send(JSON.stringify(message)); | ||
| } | ||
| }); | ||
| } | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
|
Comment on lines
+28
to
+31
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The current associations do not specify what should happen to a message if its parent |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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, | ||
|
Comment on lines
+16
to
+18
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
| }, | ||
| }); | ||
|
|
||
| Room.belongsTo(User); | ||
| User.hasMany(Room); | ||
|
Comment on lines
+22
to
+23
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The association between |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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, | ||
|
Comment on lines
+11
to
+14
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
| }, | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
|
Comment on lines
+6
to
+7
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The routes are clear and functional. For improved architectural consistency and to better model the resource hierarchy (messages as a sub-resource of rooms), consider nesting this router within |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
|
Comment on lines
+6
to
+9
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This router is well-defined. To further improve the application's structure, you could centralize all room-related endpoints here. Since messages belong to a room, you could import |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This route is correctly defined for creating a new user. As the application evolves, you could expand this router to include other user management endpoints, such as |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| import { Message } from '../models/message.js'; | ||
|
|
||
| const getAllMessagesInRoom = (roomId) => { | ||
| return Message.findAll({ where: { roomId } }); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To improve efficiency and provide more useful data to the client, consider including the author's information with each message. You can achieve this by using the |
||
| }; | ||
|
|
||
| const createMessageInRoom = (text, userId, roomId) => { | ||
| return Message.create({ text, userId, roomId }); | ||
|
Comment on lines
+7
to
+8
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Before creating a message, it's crucial to validate that the provided |
||
| }; | ||
|
|
||
| export const messageService = { | ||
| getAllMessagesInRoom, | ||
| createMessageInRoom, | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| import { Room } from '../models/room.js'; | ||
|
|
||
| const getAllRooms = () => { | ||
| return Room.findAll(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To provide richer data to the client, consider eager-loading the associated user data. By changing the query to |
||
| }; | ||
|
|
||
| const createRoom = (title, userId, description = '') => { | ||
| return Room.create({ title, userId, description }); | ||
|
Comment on lines
+7
to
+8
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The service currently trusts that the provided |
||
| }; | ||
|
|
||
| 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 } }); | ||
|
Comment on lines
+15
to
+34
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Before attempting to update a room, it is good practice to first verify that a room with the given |
||
| }; | ||
|
|
||
| export const roomsService = { | ||
| getAllRooms, | ||
| createRoom, | ||
| deleteRoom, | ||
| updateRoom, | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import { User } from '../models/user.js'; | ||
|
|
||
| const createUser = async (name) => { | ||
| return User.create({ name }); | ||
| }; | ||
|
Comment on lines
+3
to
+5
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To improve separation of concerns, the logic for checking if a user already exists should be moved from the controller into this function. The |
||
|
|
||
| export const userService = { | ||
| createUser, | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For missing required fields in the request body or parameters, it's more appropriate to return a
400 Bad Requeststatus code. The404 Not Foundstatus should be reserved for when the requested resource (i.e., the URL endpoint itself) cannot be found.