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, +};