Skip to content

Commit

Permalink
refactor: rename room to group
Browse files Browse the repository at this point in the history
  • Loading branch information
aseerkt committed Jul 2, 2024
1 parent b60cb42 commit 6f3e877
Show file tree
Hide file tree
Showing 33 changed files with 190 additions and 159 deletions.
40 changes: 25 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,23 @@

### Run final build using docker compose

- Spin up entire stack (redis, mongo, web, server)
- Spin up the entire stack (redis, postgres, web, server)
```bash
docker compose -f docker-compose.prod.yml up --build
pnpm docker:build
```
- Run migrations
```bash
pnpm docker:db:migrate
```
- Seed database
```bash
pnpm docker:db:seed
```
- Go to [http://localhost:3000](http://localhost:3000)

### Development

- Spin up MongoDB and Redis
- Spin up PostgreSQL and Redis
```bash
docker compose up -d
```
Expand All @@ -39,18 +47,24 @@ docker compose up -d
pnpm i
```

- Run development server (web & server)
- Run migrations
```bash
pnpm dev
pnpm --filter server migration:run
```

- Go to [http://localhost:3000](http://localhost:3000)

- (Optional) Seed database
```bash
pnpm --filter server seed
```

- Run development server (web & server)
```bash
pnpm dev
```

- Go to [http://localhost:3000](http://localhost:3000)


### E2E testing

- Run playwright e2e tests
Expand All @@ -68,7 +82,6 @@ pnpm --filter e2e test:codegen

## Features Roadmap


### primary goals

- [x] sign up, login and logout
Expand All @@ -78,28 +91,25 @@ pnpm --filter e2e test:codegen
- [x] realtime messaging
- [x] typing indicators
- [x] socket.io cluster adapter integration
- [x] global error handling
- [x] redis implementation (typing users, online users)
- [x] online status
- [x] member online status
- [x] realtime member list update
- [x] infinite scroll pagination (messages/groups/members)
- [x] infinite scroll cursor pagination (messages/groups/members)
- [x] tanstack react-query integration
- [ ] alert component
- [ ] confirm dialog
- [ ] delete group
- [ ] e2e encryption
- [ ] read receipts
- [ ] extract db operation to dao

### extras

- [x] playwright e2e tests for chat
- [x] switch to postgresql (support transaction)
- [ ] swagger ui
- [ ] switch to postgresql (support transaction)
- [ ] keploy api test generation
- [ ] private groups - invite
- [ ] private groups - invite/add/delete members
- [ ] notifications
- [ ] socket.io redis streams adapter integration

## Authors

Expand Down
38 changes: 26 additions & 12 deletions docker-compose.prod.yml
Original file line number Diff line number Diff line change
@@ -1,37 +1,51 @@
version: '3.8'

services:
mongo:
image: mongo
db:
image: postgres:16-alpine
restart: always
ports:
- 27017:27017
- '5432:5432'
environment:
MONGO_INITDB_ROOT_USERNAME: aseerkt
MONGO_INITDB_ROOT_PASSWORD: secret
POSTGRES_PASSWORD: secret
POSTGRES_DB: mchat
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U postgres']
interval: 30s
timeout: 10s
retries: 5

redis:
image: redis:7.2-alpine
restart: always
ports:
- 6379:6379
- '6379:6379'
healthcheck:
test: ['CMD', 'redis-cli', 'ping']
interval: 30s
timeout: 10s
retries: 5

web:
build:
context: ./web
dockerfile: Dockerfile
ports:
- 3000:3000
- '3000:3000'
environment:
VITE_BACKEND_URL: http://localhost:5000
depends_on:
- server

server:
build:
context: ./server
dockerfile: Dockerfile
container_name: mchat-server
ports:
- 5000:5000
environment:
REDIS_HOST: redis
REDIS_PORT: 6379
MONGO_URI: mongodb://aseerkt:secret@mongo:27017/mchat
- '5000:5000'
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
36 changes: 18 additions & 18 deletions e2e/tests/chat-flow.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,41 @@ import { faker } from '@faker-js/faker'
import { Locator, Page, expect, test } from '@playwright/test'
import { getAuthFromPageContext } from './helpers'

const roomCount = 2
const groupCount = 2
const messageCount = 5

async function createRoom(page: Page) {
const roomName = faker.company.name()
async function createGroup(page: Page) {
const groupName = faker.company.name()
await page.getByRole('button', { name: 'New group' }).click()
await page.getByLabel('Room name').fill(roomName)
await page.getByLabel('Group name').fill(groupName)
await page.getByRole('button', { name: 'Create', exact: true }).click()

const successToast = page.getByText(`Room "${roomName}" created`)
const successToast = page.getByText(`Group "${groupName}" created`)

await expect(successToast).toBeInViewport()
await expect(successToast).not.toBeInViewport()

await page.waitForTimeout(2000)

await expect(page.getByRole('link', { name: roomName })).toBeVisible()
await expect(page.getByRole('link', { name: groupName })).toBeVisible()
await expect(
page.locator('header').filter({ hasText: roomName }),
page.locator('header').filter({ hasText: groupName }),
).toBeVisible()
}

test.describe('chat flow', () => {
test('create room', async ({ page }) => {
test('create group', async ({ page }) => {
await page.goto('http://localhost:3000/chat')

for (let i = 0; i < roomCount; i++) {
await createRoom(page)
for (let i = 0; i < groupCount; i++) {
await createGroup(page)
}
})

test('send message', async ({ page }) => {
await page.goto('http://localhost:3000/chat')

await createRoom(page)
await createGroup(page)

async function sendMessage() {
const message = faker.word.words(3)
Expand All @@ -53,24 +53,24 @@ test.describe('chat flow', () => {
}
})

test('join room', async ({ page }) => {
test('join group', async ({ page }) => {
await page.goto('http://localhost:3000/chat')

await page.getByRole('button', { name: 'Join group' }).click()
const roomLabels = await page.locator('label').all()
const groupLabels = await page.locator('label').all()

async function joinRoom(label: Locator) {
async function joinGroup(label: Locator) {
await label.click()
return label.innerText()
}

const roomNames = await Promise.all(
roomLabels.slice(0, roomCount).map(joinRoom),
const groupNames = await Promise.all(
groupLabels.slice(0, groupCount).map(joinGroup),
)

await page.getByRole('button', { name: 'Join', exact: true }).click()

for (const name of roomNames) {
for (const name of groupNames) {
await expect(page.getByRole('link', { name })).toBeVisible()
}
})
Expand All @@ -80,7 +80,7 @@ test.describe('chat flow', () => {

const auth = await getAuthFromPageContext(page)

await createRoom(page)
await createGroup(page)
// MEMBER LIST
await page.getByRole('button', { name: 'open member drawer' }).click()
// add assertion here
Expand Down
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
"build": "pnpm run -r build",
"lint": "pnpm run -r lint",
"prepare": "husky install",
"commitlint": "commitlint --edit"
"commitlint": "commitlint --edit",
"docker:build": "docker compose -f docker-compose.prod.yml up --build -d",
"docker:db:migrate": "docker exec -it mchat-server pnpm migrate:run",
"docker:db:seed": "docker exec -it mchat-server pnpm seed",
"docker:up": "pnpm docker:build && pnpm docker:db-migrate"
},
"keywords": [
"chat", "realtime", "socket.io", "react", "node.js", "react-query"
Expand Down
1 change: 1 addition & 0 deletions server/.dockerignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
node_modules
.env
eslint.config.mjs
3 changes: 3 additions & 0 deletions server/.env.docker
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
PORT=5000
JWT_SECRET=1009830912idhsjhdjfhiuh29hef98h1f8hf28hf98hef98h2f98fshdfhkjhajh
CORS_ORIGIN=http://localhost:3000
REDIS_HOST=redis
REDIS_PORT=6379
DB_URL="postgresql://postgres:secret@db:5432/mchat"
6 changes: 1 addition & 5 deletions server/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,8 @@ COPY .env.docker .env

COPY . .

RUN pnpm build

RUN rm -rf src

ENV NODE_ENV production

EXPOSE 5000

CMD ["node", "dist/index.js"]
CMD ["pnpm", "start"]
5 changes: 4 additions & 1 deletion server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"description": "mChat server implementation",
"private": true,
"scripts": {
"start": "node dist/index.js",
"start": "tsx src/index.ts",
"dev": "tsx watch src/index.ts",
"build": "tsc",
"lint": "tsc && eslint src/**/*.ts",
Expand Down Expand Up @@ -48,5 +48,8 @@
"tsx": "^4.15.6",
"typescript": "^5.5.3",
"typescript-eslint": "^7.15.0"
},
"_moduleAliases": {
"@": "src"
}
}
2 changes: 1 addition & 1 deletion server/src/middlewares.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const auth: RequestHandler = (req, res, next) => {

const memberRoles: MemberRole[] = ['member', 'admin', 'owner']

export const hasRoomPermission =
export const hasGroupPermission =
(role: MemberRole): RequestHandler =>
async (req, res, next) => {
try {
Expand Down
4 changes: 2 additions & 2 deletions server/src/modules/groups/groups.controller.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { db } from '@/database'
import { withPagination } from '@/database/helpers'
import { notFound } from '@/utils/api'
import { deleteRoomMembersRoles, setMemberRole } from '@/utils/redis'
import { deleteGroupMembersRoles, setMemberRole } from '@/utils/redis'
import { eq, getTableColumns, ne } from 'drizzle-orm'
import { RequestHandler } from 'express'
import { members } from '../members/members.schema'
Expand Down Expand Up @@ -74,7 +74,7 @@ export const deleteGroup: RequestHandler = async (req, res, next) => {
}

// TODO: move these db operations to queue
await deleteRoomMembersRoles(groupId)
await deleteGroupMembersRoles(groupId)
await db.delete(messages).where(eq(messages.groupId, groupId))
await db.delete(members).where(eq(members.groupId, groupId))

Expand Down
10 changes: 5 additions & 5 deletions server/src/modules/groups/groups.routes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { hasRoomPermission } from '@/middlewares'
import { hasGroupPermission } from '@/middlewares'
import { Router } from 'express'
import { getGroupMembers } from '../members/members.controller'
import { createMessage, listMessages } from '../messages/messages.controller'
Expand All @@ -14,10 +14,10 @@ export const router = Router()
router.post('/', createGroup)
router.get('/', listGroups)

router.get('/:groupId', hasRoomPermission('member'), getGroup)
router.delete('/:groupId', hasRoomPermission('owner'), deleteGroup)
router.get('/:groupId', hasGroupPermission('member'), getGroup)
router.delete('/:groupId', hasGroupPermission('owner'), deleteGroup)

router.get('/:groupId/members', hasRoomPermission('member'), getGroupMembers)
router.get('/:groupId/members', hasGroupPermission('member'), getGroupMembers)

router.get('/:groupId/messages', hasRoomPermission('member'), listMessages)
router.get('/:groupId/messages', hasGroupPermission('member'), listMessages)
router.post('/:groupId/messages', createMessage)
2 changes: 1 addition & 1 deletion server/src/socket/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const registerSocketEvents = (io: TypedIOServer) => {
await addOnlineUser(socket.data.user.id)
socket.broadcast.emit('userOnline', socket.data.user.id)

socket.on('joinRoom', groupId => {
socket.on('joinGroup', groupId => {
leaveAllRoom(socket)
socket.join(String(groupId))
})
Expand Down
2 changes: 1 addition & 1 deletion server/src/socket/socket.inteface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export interface ServerToClientEvents {
}

export interface ClientToServerEvents {
joinRoom: (groupId: number) => void
joinGroup: (groupId: number) => void
memberJoin: (
groupIds: number[],
cb: (res: { success: boolean; error?: unknown }) => void,
Expand Down
6 changes: 3 additions & 3 deletions server/src/utils/redis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ getRedisClient()

export const redisKeys = {
ONLINE_USERS: 'online_users',
TYPING_USERS: (groupId: number) => `room:${groupId}:typing_users`,
MEMBER_ROLES: (groupId: number) => `room:${groupId}:member_roles`,
TYPING_USERS: (groupId: number) => `group:${groupId}:typing_users`,
MEMBER_ROLES: (groupId: number) => `group:${groupId}:member_roles`,
}

// MEMBER
Expand All @@ -49,7 +49,7 @@ export const getMemberRole = (groupId: number, userId: number) => {
return redisClient.hget(cacheKey, userId.toString())
}

export const deleteRoomMembersRoles = (groupId: number) => {
export const deleteGroupMembersRoles = (groupId: number) => {
const cacheKey = redisKeys.MEMBER_ROLES(groupId)
return redisClient.hdel(cacheKey)
}
Expand Down
Loading

0 comments on commit 6f3e877

Please sign in to comment.