Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
153 changes: 153 additions & 0 deletions src/controllers/room.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { messageService } from '../services/message.service.js';
import { roomService } from '../services/room.service.js';
import { Message } from '../data/message.js';
import { userService } from '../services/user.service.js';

const getRoomInfoByRoomId = async (req, res) => {
const { roomId } = req.params;

const id = parseInt(roomId, 10);

if (isNaN(id)) {
return res.status(400).json({ message: 'Invalid roomId' });
}

const room = await roomService.findRoomById(id);

if (!room) {
return res.status(404).json({ message: 'Room not Found' });
}

const messages = await messageService.findRoomsMessages(id);

return res.status(200).json({
room: {
id: room.id,
name: room.name,
},
messages,
Comment on lines +21 to +28

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When you return messages from getRoomInfoByRoomId (and when you start returning messages in joinRoom), make sure each message object contains author, time, and text fields and that time uses a consistent, verifiable format (e.g. ISO string). The task requires that all messages have these fields and a consistent timestamp format. Confirm messageService.findRoomsMessages returns messages in that shape.

});
};

const createRoom = async (req, res) => {
const { name } = req.body;
const user = req.user;

if (!name) {
return res.status(400).json({ message: 'Room name is required' });
}

const newRoom = await roomService.createRoom(name);

if (!newRoom) {
return res.status(400).json({ message: 'Cannot create the room' });
}

await newRoom.addUser(user);
Comment on lines +34 to +46

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createRoom assumes req.user exists and then does newRoom.addUser(user). Ensure req.user is present (return 401 if not authenticated) and confirm whether addUser expects the user instance or an id. If it expects an id, use newRoom.addUser(user.id). Add an explicit check before calling addUser to avoid runtime errors when req.user is absent.


const roomUsers = await newRoom.getUsers();
const roomInfo = {
room: { id: newRoom.id, name: newRoom.name },
members: roomUsers,
};

return res.status(201).json(roomInfo);
};

const deleteRoom = async (req, res) => {
const { roomId } = req.params;
const room = await roomService.findRoomById(roomId);

if (!roomId || !room) {
return res.status(404).json({ message: 'Room not Found' });
}
Comment on lines +57 to +63

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In deleteRoom you call roomService.findRoomById(roomId) without normalizing the id. Also the existence check uses if (!roomId || !room) which doesn't validate numeric IDs. Parse the roomId and validate with Number.isNaN, then check the room. This avoids ambiguity and possible false positives/negatives.


await room.setUsers([]);
await Message.destroy({ where: { roomId: room.id } });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line performing Message.destroy will remove message rows directly from the model. Prefer using a messageService.deleteMessagesByRoom(room.id) if available so message-related logic stays in one place and any hooks/side-effects are handled consistently. If a service helper doesn't exist, add one or document why direct model access is used.

await room.destroy();

res.status(200).json({ message: 'Room deleted successfully' });
};

const renameRoom = async (req, res) => {
const room = req.room;
const { newName } = req.body;

if (!room) {
return res.status(404).json({ message: 'Room not Found' });
}

if (!newName || typeof newName !== 'string' || !newName.trim()) {
return res.status(400).json({ message: 'Invalid room name provided' });
}

room.name = newName.trim();
await room.save();

return res.status(200).json(room);
};

const mergeRooms = async (req, res) => {
const currentRoom = req.room;
const user = req.user;
const { targetRoomId } = req.body;

if (!user) {
return res.status(401).json({ error: 'User not authenticated' });
}

if (!currentRoom) {
return res.status(404).json({ error: 'Current room not found' });
}

if (!targetRoomId) {
return res.status(400).json({ error: 'No targetRoomId provided' });
}

const targetRoomIdNum = parseInt(targetRoomId, 10);

if (Number.isNaN(targetRoomIdNum)) {
return res.status(400).json({ error: 'Invalid targetRoomId' });
}

const usersRooms = await roomService.findRoomsByUserId(user.id);
const targetRoom = usersRooms.find((r) => r.id === targetRoomIdNum);

if (!targetRoom) {
return res
.status(403)
.json({ error: 'The user doesn’t have access to the target room' });
}

await userService.mergeUsers(currentRoom.id, targetRoomIdNum);
await messageService.mergeMessages(currentRoom.id, targetRoomIdNum);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential blocking issue in mergeRooms: this handler calls messageService.mergeMessages(currentRoom.id, targetRoomIdNum). Inspect message.service.mergeMessages: it currently calls findRoomsMessages which returns plain DTOs (author/text/time), then attempts to call .save() on those objects. That will throw because DTOs are not Sequelize model instances. Either change mergeMessages to fetch Message model instances (e.g. Message.findAll({ where: { roomId } })) and move them, or implement a bulk Message.update({ roomId: target }, { where: { roomId: source } }). Until the service is fixed, mergeRooms may fail at runtime. See message.service.js for the problematic implementation .


await currentRoom.setUsers([]);
await currentRoom.destroy();
Comment on lines +121 to +126

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mergeRooms calls userService.mergeUsers and messageService.mergeMessages and then immediately clears users and destroys the current room. These multi-step changes should be executed atomically (DB transaction) so failures won't leave the system in an inconsistent state. Move merge logic into a transactional service method or start a transaction here and only destroy the source room after merge success. This addresses requirements 3.3 and 3.4 (preserve history and consistent identity updates).


return res.status(200).json(targetRoom);
};

const joinRoom = async (req, res) => {
const { roomId } = req.params;
const userId = req.user.id;

const room = await roomService.findRoomById(roomId);
Comment on lines +134 to +135

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're passing req.params.roomId directly into findRoomById. Normalize and validate it first (parseInt + Number.isNaN check) to avoid edge cases when the service expects a number. Also validate the result before proceeding. Example: const id = parseInt(roomId, 10); if (Number.isNaN(id)) return 400... then use id in service calls.


if (!room) {
return res.status(404).json({ message: 'Room not found' });
}

await roomService.addUserToRoom(userId, roomId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small positive note: the joinRoom call to roomService.addUserToRoom uses (userId, roomId) which resolves the previously reported argument-order bug. Good work fixing that earlier issue — keep up that attention to service signatures.


Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The router currently mounts the join route without the catchError wrapper, so exceptions thrown by joinRoom won't be forwarded to the central error handler. Ensure this route is wrapped consistently (or handle errors inside this function). See router mounting of other routes for examples (they use catchError) .

return res.status(200).json({ message: 'Joined room successfully' });
Comment on lines +140 to +143

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requirement 1.6 (message history retrieval on join) is not fulfilled here. After adding the user to the room you must return the room's previous messages so the client can display history. Consider calling messageService.findRoomsMessages(...) and include the messages in the JSON response (and/or include members). Example flow:

  1. add user to room
  2. fetch messages = await messageService.findRoomsMessages(parsedRoomId)
  3. return { message: 'Joined room successfully', room: { id, name }, messages }
    This change is required by the task checklist.

};

export const roomController = {
getRoomInfoByRoomId,
createRoom,
deleteRoom,
renameRoom,
mergeRooms,
joinRoom,
};
76 changes: 76 additions & 0 deletions src/controllers/user.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { roomService } from '../services/room.service.js';
import { userService } from '../services/user.service.js';

const createUser = async (req, res) => {
const { name } = req.body;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Input validation is too weak: you read name from the body but do not trim it or check for all-whitespace. A request with { name: ' ' } will pass the if (!name) check and create an invalid user. Trim and validate name (e.g., const name = (req.body.name || '').trim() and then reject empty). This relates to requirement 3.7 (server-side validation of important inputs).


if (!name) {
return res.status(400).json({ message: 'Name is required' });
}

const newUser = await userService.createUser(name);
Comment on lines +5 to +11

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createUser currently trusts req.body.name as-is. Consider trimming and validating the name before creating to avoid names that are only whitespace, and return a clear error if invalid. Also consider handling duplicate-name errors (the DB has a unique constraint on name). This will make behavior consistent with isNotAuth middleware which checks existence by header.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After creating a user you call userService.createUser(name) and return the result, but you do not associate the created user with the current session/connection. Requirement 3.1 requires the server to accept and associate a username sent from the client with that client’s session/connection so messages can be attributed correctly. Depending on your auth approach either set req.user/session here, or return authentication/session details so the client can persist them and send on subsequent requests.


res.status(201).json(newUser);
};

const getUserInfo = async (req, res) => {
const { userId } = req.params;
Comment on lines +16 to +17

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getUserInfo reads userId from req.params but does not validate or normalize it. Consider parsing to an integer and validating the input before passing it to the service (e.g. parseInt and check Number.isInteger). Also consider whether you need to enforce authorization here — the route is protected by isAuth (which sets req.user) but this function allows fetching any user by id without verifying the requester’s rights.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getUserInfo reads userId from params and passes it directly to userService.getUserById. Route params are strings — consider normalizing/validating userId (e.g., const userId = Number(req.params.userId); if (Number.isNaN(userId)) return res.status(400)...). This prevents unexpected behavior in the service and aligns with requirement 3.2 (IDs must be handled consistently).


if (!userId) {
return res.status(400).json({ message: 'Id is required' });
}

const user = await userService.getUserById(userId);

if (!user) {
return res.status(404).json({ message: 'User not found' });
}

const rooms = await roomService.findRoomsByUserId(user.id);
Comment on lines +23 to +29

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You call userService.getUserById(userId) and then fetch rooms via roomService.findRoomsByUserId(user.id). Two issues: 1) If you intend that only the authenticated user can fetch their info, you should compare req.user.id to req.params.userId or otherwise use req.user directly. 2) If you expect room lists or their ordering to be deterministic, ensure the roomService returns ordered results (currently it returns User.Rooms without an explicit order).

Comment on lines +16 to +29

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getUserInfo reads userId from params and directly passes it to userService.getUserById. You should:

  • Validate/coerce userId (e.g., const id = parseInt(userId, 10) and check Number.isNaN).
  • Consider authorization: the route is protected by isAuth (which sets req.user based on header), but this handler allows requesting arbitrary users by id. Decide whether you want to allow that. If not, use req.user or verify req.user.id === id. The current code may leak other users' data if that isn't intended. See the router and isAuth usage for context.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You fetch rooms with roomService.findRoomsByUserId(user.id). Ensure this service call returns only the rooms the user belongs to and handles an empty result gracefully. Also consider handling the case when userService.getUserById throws — either ensure router wraps this handler in catchError or add try/catch here so errors are forwarded to your error middleware.

const userInfo = {
user: { id: user.id, name: user.name, createdAt: user.createdAt },
rooms,
};

res.status(200).json(userInfo);
};

const changeUserName = async (req, res) => {
const user = req.user;
const { newName } = req.body;

if (!user) {
return res.status(404).json({ message: 'User not Found' });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: error message strings are inconsistent in capitalization, e.g. 'User not Found' vs 'User not found'. This is cosmetic but makes client-side handling/error matching harder. Consider standardizing messages/keys returned in error responses.

}
Comment on lines +38 to +44

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changeUserName uses req.user (from isAuth) but the route includes a :userId parameter. This mismatch can be confusing or unsafe: either remove the :userId param from the route if operations should always act on the authenticated user, or fetch the target user by req.params.userId and explicitly verify that the authenticated user is allowed to change that target. Make the expected authorization semantics explicit and return 403 when appropriate.


if (!newName) {
return res.status(400).json({ message: 'No new name provided' });
}

user.name = newName;

await user.save();

res.status(200).json({ id: user.id, userName: user.name });
Comment on lines +46 to +54

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The newName validation exists (good), but after deleting/renaming the controller updates the Sequelize model and calls user.save(). This is fine, but consider returning the canonical fields (id, name, createdAt) instead of a model if you want to keep API responses consistent and avoid exposing internal metadata.

Comment on lines +38 to +54

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changeUserName uses req.user (good for authenticated changes) but the route in user.router includes /:userId (which the handler ignores). Either:

  • Remove :userId from the route so the handler's behavior and the route match, or
  • Validate that req.user.id matches req.params.userId before proceeding.

Also validate newName more strictly: const name = (newName || '').trim(); if (!name) ... so names that are only whitespace are rejected. Finally, the response key userName is inconsistent — consider returning { id, name } to match other endpoints.

Comment on lines +39 to +54

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changeUserName relies on req.user being present; while you check for it, you should document or assert the middleware contract that sets req.user. Also validate/trim newName to avoid setting an all-whitespace name. Finally, the response uses userName as the key — consider returning { id, name } to be consistent with other endpoints/models (consistency helps clients). These changes satisfy requirement 3.10 (username consistency/persistence) and 3.7 (validation).

};

const deleteUser = async (req, res) => {
const user = req.user;

if (!user) {
return res.status(404).json({ message: 'User not Found' });
}

await user.destroy();

req.user = null;
Comment on lines +57 to +66

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deleteUser similarly ignores req.params.userId and uses req.user. Also, setting req.user = null after await user.destroy() is unnecessary in a stateless HTTP handler — it only mutates the current request object and doesn’t affect client-side state. If you need to revoke authentication, do so via the proper session/logout mechanism.

Comment on lines +64 to +66

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After deleting the user you call await user.destroy() and then req.user = null. Setting req.user = null is not enough to fully dissociate the client: if you use session cookies or tokens you should explicitly invalidate them (destroy session, clear cookie, revoke token). Otherwise the client may remain authenticated. Also consider whether deleting a user must cascade to or orphan other resources; document or implement expected cleanup behavior. This affects requirement 3.10 (clean session handling) and general data integrity.


res.status(200).json({ message: 'User deleted successfully' });
Comment on lines +57 to +68

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deleteUser similarly relies on req.user while the route includes /:userId. Apply the same guidance as for changeUserName (either remove the route param or validate it against req.user.id).

Also: after await user.destroy(), you set req.user = null — this is unnecessary because req is per-request and won't persist; remove it. Consider whether you need to remove the user's associations (messages, room memberships) explicitly or depend on DB cascade rules — currently those behaviors are implicit and may leave orphaned data if cascade is not configured.

};

export const userController = {
createUser,
getUserInfo,
changeUserName,
deleteUser,
};
31 changes: 31 additions & 0 deletions src/createServer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import express from 'express';
import cors from 'cors';

import { userRouter } from './routes/user.router.js';
import { roomRouter } from './routes/room.router.js';

export function createServer() {
const server = express();

server.use(express.json());

server.use(
cors({
origin: process.env.CLIENT_ORIGIN || 'http://localhost:3000',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're setting CORS origin from process.env.CLIENT_ORIGIN || 'http://localhost:3000' which is fine for local development. Just a reminder: when credentials: true is set, the origin must not be '*' — confirm that CLIENT_ORIGIN is provided in production and matches allowed client origin(s).

credentials: true,
}),
Comment on lines +12 to +16

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CORS alignment suggestion: this file sets CORS origin with process.env.CLIENT_ORIGIN || 'http://localhost:3000', while Socket.IO in src/index.js uses { origin: '*', credentials: true }. Consider centralizing or aligning CORS configuration so browser socket + http requests have consistent allowed origins (or document why they differ). See Socket.IO setup in src/index.js.

);
Comment on lines +12 to +17

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're enabling credentials in CORS — when you set credentials: true the origin must be an explicit origin (not ''). The code already reads CLIENT_ORIGIN from env, which is good; just ensure that variable is set in production. Also note: src/index.js configures Socket.IO with origin: '' and credentials: true which is invalid for browser credentialed requests — align the Socket.IO CORS to use the same explicit origin. See index.js where Socket.IO is configured.


server.get('/', (req, res) => {
res.status(200).json({ message: 'Server is running' });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good: the root health route now uses res.status(200).json({ message: 'Server is running' }), which avoids the double-response bug noted earlier. This fixes the previous issue where res.sendStatus(200).json(...) would throw. Keep this as-is.

});

server.use('/user', userRouter);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: it may be helpful to document (or add a comment) that authentication is header-based in this app — the isAuth middleware expects the username in x-username. This will help future maintainers/front-end devs understand how to send the username. See isAuth for details.

server.use('/room', roomRouter);
Comment on lines +23 to +24

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mount the user and room routers here which is correct. I verified that userRouter and roomRouter exist and expose the expected routes (user routes in src/routes/user.router.js and room routes including join/create/delete/rename in src/routes/room.router.js). Good.


server.use((req, res, next) => {
res.status(404).json({ message: 'Route not found' });
Comment on lines +26 to +27

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The app registers a 404 fallback but does not register a centralized error-handling middleware (i.e. app.use((err, req, res, next) => { ... })). Many controllers/middlewares in this project use next(error) (see catchError) — without a JSON error handler you will get Express's default HTML error responses. Consider adding a JSON error handler after the routers, for example:

server.use((err, req, res, next) => {
  // eslint-disable-next-line no-console
  console.error(err);
  res.status(err.status || 500).json({ message: err.message || 'Internal Server Error' });
});

This will ensure consistent JSON errors for clients and tests.

});
Comment on lines +26 to +28

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Improve error handling: there is a 404 handler, but no centralized error-handling middleware to catch thrown errors and return a proper 500 response. Add an Express error handler after the 404 handler, for example:

server.use((err, req, res, next) => { console.error(err); res.status(500).json({ message: 'Internal Server Error' }); });

Placing this after the 404 ensures unexpected exceptions are returned consistently to clients.

Comment on lines +26 to +28

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no global error-handling middleware in this file. The project uses catchError to forward async errors with next(error) (see utils/catchError). Without an Express error handler the client won't get consistent JSON errors. Add an error handler after the routes/404, for example:

server.use((err, req, res, next) => {
  // log
  console.error(err);
  res.status(err.status || 500).json({ error: err.message || 'Internal Server Error' });
});

Place it after the 404 handler so all forwarded errors are caught.


return server;
}
12 changes: 12 additions & 0 deletions src/data/associations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { User } from './user.js';
import { Message } from './message.js';
import { Room } from './room.js';

User.hasMany(Message, { foreignKey: 'userId' });
Message.belongsTo(User, { foreignKey: 'userId' });
Comment on lines +5 to +6

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The User <-> Message association is correct. Consider adding referential action options like onDelete: 'CASCADE' on the Message.belongsTo(User) or User.hasMany(Message) relationship if you want messages removed automatically when a user is deleted. This helps maintain DB integrity without manual deletes elsewhere.


Room.hasMany(Message, { foreignKey: 'roomId' });
Message.belongsTo(Room, { foreignKey: 'roomId' });
Comment on lines +8 to +9

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Room <-> Message association is correct. As with users, you may want to add onDelete: 'CASCADE' on Message.belongsTo(Room, { foreignKey: 'roomId', onDelete: 'CASCADE' }) so messages are cleaned up automatically when a room is destroyed. Right now the controller calls Message.destroy(...) explicitly when deleting a room, which is acceptable but redundant with a cascade rule.

Comment on lines +5 to +9

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The associations here correctly express the relationships needed by the app (User ⇄ Message, Room ⇄ Message, User ⇄ Room). Good — these are necessary for features like per-room message history and membership checks.

Comment on lines +8 to +9

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding DB-level cascade for Room -> Message so messages are removed when a room is deleted. That makes deletion semantics more robust and directly supports the requirement that deleting a room removes its message history. Example suggestion: Room.hasMany(Message, { foreignKey: 'roomId', onDelete: 'CASCADE' }) and Message.belongsTo(Room, { foreignKey: 'roomId', onDelete: 'CASCADE' }). Note: the controller currently destroys messages explicitly, but the cascade is a good defense-in-depth. See room controller for current delete behavior.


User.belongsToMany(Room, { through: 'user_room', foreignKey: 'userId' });
Room.belongsToMany(User, { through: 'user_room', foreignKey: 'roomId' });
Comment on lines +11 to +12

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The many-to-many User <-> Room mapping via through: 'user_room' is fine for the current requirements. If later you need to store metadata on the membership (like joinedAt or role), define a through model instead of a string and/or add otherKey to make keys explicit.

Comment on lines +11 to +12

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For clarity, you may want to make the join-table keys explicit by adding otherKey on belongsToMany calls. For example: User.belongsToMany(Room, { through: 'user_room', foreignKey: 'userId', otherKey: 'roomId' }). This isn’t required but improves readability and prevents ambiguity when maintaining the model relationships.

29 changes: 29 additions & 0 deletions src/data/message.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { DataTypes } from 'sequelize';
import { client } from '../db/db.js';

export const Message = client.define(
'Message',
{
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
roomId: {
type: DataTypes.INTEGER,
allowNull: false,
Comment on lines +12 to +14

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The model correctly requires roomId, userId, and text. Consider adding explicit references if you want schema-level clarity, but it is not required because associations.js defines Message.belongsTo(Room) and Message.belongsTo(User). If you keep associations, foreign keys will be created at sync time.

},
Comment on lines +12 to +15

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The roomId field exists and is allowNull: false, which satisfies the requirement to associate messages with rooms. If you want DB-level foreign-key constraints, consider adding a references property pointing to the Room model and onDelete behavior. Associations are set up separately in associations.js, so this is optional depending on whether you want DB-enforced FKs.

userId: {
type: DataTypes.INTEGER,
allowNull: false,
Comment on lines +16 to +18

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

userId is present which is correct — this is how a message links to its author. However, to meet the requirement that messages delivered to clients include an author name (not just userId) you must query messages with the User association and map the result to include the author (user.name). See associations in src/data/associations.js and the message service where messages are fetched (message.service.findRoomsMessages currently doesn't include the User) .

},
Comment on lines +16 to +19

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

userId is defined and allowNull: false, which provides the link to the message author. Again, associations in associations.js resolve the relation at the ORM level; if you want enforced DB constraints add references and onDelete here.

text: {
type: DataTypes.STRING,
allowNull: false,
Comment on lines +20 to +22

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

text is non-nullable which is correct. Consider whether you need a length limit or validations (optional). The current design stores the message body and will satisfy the requirement that messages have text. If you add validations, keep them in the service/controller or as model validators depending on desired behavior.

Comment on lines +20 to +22

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Message text is defined as DataTypes.STRING. That often maps to a limited length (e.g., 255). If the chat should support longer messages, consider switching to DataTypes.TEXT to avoid truncation.

},
Comment on lines +20 to +23

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

text is defined as DataTypes.STRING. If you expect long messages, prefer DataTypes.TEXT to avoid length limits and potential truncation by the DB dialect.

},
{
timestamps: true,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Timestamps are enabled (createdAt will be recorded). Ensure when you return message history to clients you order by createdAt (ASC) so new users receive messages in chronological order. This is enforced at query time in the message service (e.g. Message.findAll({ where: { roomId }, include: User, order: [['createdAt','ASC']] })). The model supports this via timestamps: true, but the service must use it when retrieving messages .

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You enabled timestamps: true, which provides createdAt to be used as the time field required by the task. Good — the controller/service layers can read createdAt and expose it as time in message DTOs.

tableName: 'messages',
Comment on lines +25 to +27

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Timestamps are enabled which provides createdAt (used as message time). To guarantee a consistent time format for clients, convert createdAt to a stable representation (e.g., createdAt.toISOString() or UNIX epoch) in the service/controller before returning messages. The message service currently returns createdAt directly — consider formatting there.

},
);
22 changes: 22 additions & 0 deletions src/data/room.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { DataTypes } from 'sequelize';
import { client } from '../db/db.js';

export const Room = client.define(
'Room',
{
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
name: {
type: DataTypes.STRING,
allowNull: false,
unique: true,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have unique: true for the name field which is correct, but be aware renaming to an existing name will raise a DB error. Make sure your rename controller handles unique-constraint errors and returns a clear HTTP error (e.g., 400 or 409) to the client.

},
Comment on lines +12 to +16

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider enforcing non-empty room names at the model level to avoid whitespace-only or empty names making it into the DB. For example add validate: { notEmpty: true } or a setter that trims the name before saving. This complements the existing controller checks and improves data integrity.

},
{
timestamps: true,
tableName: 'rooms',
},
);
22 changes: 22 additions & 0 deletions src/data/user.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { DataTypes } from 'sequelize';
import { client } from '../db/db.js';

export const User = client.define(
'User',
{
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
name: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
Comment on lines +12 to +15

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because name is unique, attempts to create a duplicate username will trigger a DB constraint error. Make sure user creation code (userService/userController) catches that and returns a clear 4xx response rather than letting an uncaught exception bubble up. The createUser controller uses userService.createUser — handle uniqueness there if not already handled .

Comment on lines +12 to +15

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name field is allowNull: false and unique: true, which is appropriate. Consider adding trimming/validation to avoid entries like " alice " vs "alice" and to enforce reasonable length limits. You can handle trimming either in the controller/service or via a model hook or a validate rule (e.g., validate: { notEmpty: true, len: [1, 50] }). This helps avoid duplicate-creation or unexpected auth failures when x-username is sent from the client.

},
Comment on lines +12 to +16

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Model correctly defines name as non-nullable and unique. Consider adding validate: { notEmpty: true } to prevent empty-string usernames at the DB level and provide clearer validation errors.

Comment on lines +12 to +16

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a notEmpty validator on the name field to prevent empty string usernames being persisted. Request-level checks are useful but adding ORM-level validation is safer (e.g., validate: { notEmpty: true }). Also consider a set or a trim in the service when creating users to normalize input.

Comment on lines +12 to +16

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You currently use DataTypes.STRING which is fine for usernames. If you want to limit username length explicitly, add a validate: { len: [1, 50] } to avoid extremely long names. Alternatively, if you expect very long names, consider DataTypes.TEXT. Decide based on requirements.

},
{
timestamps: true,
Comment on lines +18 to +19

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

timestamps: true is present which provides createdAt — good: this supports recording when users were created (useful for user info endpoints). Ensure controllers surface the relevant createdAt if needed by clients.

tableName: 'users',
Comment on lines +18 to +20

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

timestamps: true provides createdAt which the app uses for user info (good). This satisfies the requirement to include user creation time for display in user info endpoints.

},
);
17 changes: 17 additions & 0 deletions src/db/db.init.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { client } from './db.js';
import '../data/associations.js';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Importing associations before syncing is correct — it ensures model relationships are registered before client.sync runs. Keep this import so associations are applied prior to schema sync (associations are defined in src/data/associations.js) .

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Importing associations here is correct because associations must be registered before calling client.sync(). Consider adding a short inline comment that this import is intentional (it sets up model relationships via side effects).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Importing associations before calling sync is correct and necessary so Sequelize knows about relationships when syncing. Good placement. See associations file where relationships are defined.


export const dbInit = async () => {
await client.authenticate();
Comment on lines +4 to +5

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a local try/catch inside dbInit to produce DB-specific diagnostics (for example to log more context about DB authentication failures) even though index.js wraps dbInit in a top-level try/catch. This can make startup errors easier to debug.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

await client.authenticate() is fine and ensures DB connectivity before syncing. Note: index.js wraps dbInit in a try/catch so startup errors will be handled at a higher level — you may optionally add local try/catch for more specific DB logs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling client.authenticate() is appropriate. If you want more granular startup errors, you can add a try/catch here, but index.js already wraps dbInit() in try/catch so this is acceptable.

// eslint-disable-next-line no-console
console.log('✅ Database connected');

await client.sync({ alter: true });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

client.sync({ alter: true }) will alter tables to match models. This is convenient in development but can be dangerous in production (may change schemas unexpectedly). Prefer explicit migrations or document that this mode is only for development.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

await client.sync({ alter: true }) will attempt to alter the DB schema to match models. For development this is convenient, but in production it's safer to use migrations because alter can cause unintended schema changes. Consider adding a comment or switching to a migration workflow for production deployments.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sync({ alter: true }) is convenient for development but can be unsafe in production because it can silently alter schema. Consider using Sequelize migrations for production environments or gate this behavior behind an environment flag (e.g., run alter only for NODE_ENV !== 'production').

// eslint-disable-next-line no-console
console.log('✅ Tables synced');

const tables = await client.getQueryInterface().showAllTables();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You log tables after showAllTables(), which is useful during development. For better testability consider returning tables (or a result boolean) from dbInit so tests can assert initialization succeeded without relying on console output.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

showAllTables and printing the result is useful for debugging but may be noisy or leak schema info in non-dev runs. Consider gating this log with a debug flag or removing it for production.


// eslint-disable-next-line no-console
console.log(tables);
Comment on lines +13 to +16

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logging tables is useful for development but consider removing or restricting this in production (or guard it with an environment check) to reduce noisy logs and avoid exposing DB structure in logs.

};
10 changes: 10 additions & 0 deletions src/db/db.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Sequelize } from 'sequelize';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Importing Sequelize here is correct and aligns with the rest of the project where client is imported by models and DB init.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import is correct and Sequelize is available. This client is used by db.init and models to connect and sync the DB. Good.


export const client = new Sequelize({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You create and export the Sequelize client here. For better portability and to avoid hard-coding credentials in source, consider using environment variables for database, username, password, host and port (e.g. process.env.DB_NAME, process.env.DB_USER, etc.). This will make local, CI and production setups easier to manage.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Sequelize client is created correctly and exported as client. This matches other files that import { client }. No syntax issues found.

database: 'postgres',
username: 'postgres',
host: 'localhost',
dialect: 'postgres',
port: 5432,
password: 'Rerlol100',
Comment on lines +4 to +9

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recommendation: avoid hard-coding DB credentials in source. Use environment variables (e.g., process.env.DB_NAME, process.env.DB_USER, process.env.DB_PASS, process.env.DB_HOST, process.env.DB_PORT) to make the configuration flexible and avoid committing secrets.

});
Comment on lines +3 to +10

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The connection options are minimal and valid, but you may want to add optional configuration such as logging: false (for CI/tests), a pool configuration, or dialectOptions for SSL in remote DBs. These improve robustness across environments.

Comment on lines +3 to +10

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure the database server and credentials you provide here actually exist in the environment where the app runs. The DB initialization (dbInit) calls client.authenticate() and client.sync(), so a misconfigured DB will cause startup to fail — verify CI/test environments provide the DB or use env-based configuration for tests.

Comment on lines +3 to +10

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional improvement: consider providing pool and logging options to the Sequelize constructor (for example, pool: { max: 5, min: 0, idle: 10000 }) to control connection behavior in higher-load or production scenarios.

Comment on lines +3 to +10

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Sequelize client is created with hardcoded connection parameters (database, username, host, port, password). It’s better to read these from environment variables (e.g. process.env.DB_NAME, process.env.DB_USER, process.env.DB_HOST, process.env.DB_PORT, process.env.DB_PASS) so credentials and environment-specific values aren’t embedded in source. Consider validating required env vars in startup and providing sensible local defaults only for development.

Comment on lines +3 to +10

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: consider adding logging: false in non-development environments (or driven by an env var) and/or a pool configuration to tune connection pooling for production workloads. These are not required but help robustness.

Loading