Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
33245df
Bump word-wrap from 1.2.3 to 1.2.4
dependabot[bot] Jul 20, 2023
13e391f
chore: cleanup
anhtt2211 Aug 3, 2023
f63a7f3
chore: update README
anhtt2211 Aug 23, 2023
3536f7f
chore: update README
anhtt2211 Aug 23, 2023
14d67ee
update README
anhtt2211 Aug 23, 2023
9f2bd99
WIP
anhtt2211 Aug 24, 2023
f31f36e
WIP
anhtt2211 Aug 25, 2023
a29d7b0
chore: update .env
anhtt2211 Aug 30, 2023
ad30267
update: config CORS
anhtt2211 Sep 1, 2023
5b32328
feat: apply CLEAN Architect for user-module
anhtt2211 Oct 13, 2023
892da37
feat: apply CLEAN Architect for article-module
anhtt2211 Oct 13, 2023
54980e9
feat: apply CLEAN Architect for profile-module
anhtt2211 Oct 13, 2023
45ae542
feat: separate db-module
anhtt2211 Oct 13, 2023
a4bd44b
wip
anhtt2211 Oct 13, 2023
efae83a
bug: wrong import file path
anhtt2211 Oct 13, 2023
3cb5e6c
feat: enhance connection variables with .env
anhtt2211 Oct 14, 2023
17bc3be
update: enhance folder structure
anhtt2211 Oct 15, 2023
e0f18cf
bug: wrong import file path
anhtt2211 Oct 13, 2023
d9cfbca
update: enhance redis-config for external dev env
anhtt2211 Oct 16, 2023
a2a141d
update: migrate to hybrid app
anhtt2211 Oct 16, 2023
4f68af5
update: add event listener
anhtt2211 Oct 16, 2023
1334602
feat: apply hybrid app for article-module
anhtt2211 Oct 20, 2023
87aadb9
wip
anhtt2211 Oct 20, 2023
204b813
feat: apply hybrid app for article-module
anhtt2211 Oct 20, 2023
6803431
feat: apply hybrid app for profile-module
anhtt2211 Oct 20, 2023
e17ef18
chore: cleanup rabbitmq-module
anhtt2211 Oct 20, 2023
b7b7aa0
chore: cleanup structure
anhtt2211 Oct 20, 2023
dbb9e97
WIP
anhtt2211 Oct 20, 2023
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
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,18 @@ DATABASE_PASSWORD=postgres
WRITE_DATABASE_NAME=master-db
READ_DATABASE_NAME=slave-db

WRITE_CONNECTION_NAME=
READ_CONNECTION_NAME=

TYPEORM_ENTITIES=/**/core/entities/**.entity{.ts,.js}
TYPEORM_MIGRATIONS=/database/migrations/*{.ts,.js}
TYPEORM_MIGRATIONS_DIR=src/database/migrations
TYPEORM_LOGGING=true
TYPEORM_SYNCHRONIZE=false
TYPEORM_MIGRATION_RUN=false

JWT_SECRET_KEY=jwt-secret-key

DROPBOX_TOKEN=
DROPBOX_KEY=
DROPBOX_SECRET=
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@ dist/*

.env
.env.development
.env.docker

app.yaml
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,45 @@
# System Design

## FUNCTION REQUIREMENTS:

- user can login/logout
- user can post the article
- user can upload file
- user can searching
- user can like, comment, follow

## NON FUNCTION REQUIREMENTS:

- high availability, latency < 500ms
- total users: 100M
- daily active user: 1M
- 1 user upload 2 image per day
- image size < 5MB
--> Write 1 * 2 * 5MB * 1M = 10MB = 10TB
- QPS: Write: 1M * 2(image upload) / (24hours * 60 minutes * 60 seconds) == 25 QPS
- QPS: Read: 1M * 50 / (24hours * 60 minutes * 60 seconds) == 580 QPS
==> READ HEAVY
- No lost for image

## API Design

Login: - request: { username, password} - response: { accessToken, refreshToken }
Logout - request: { token } - response: boolean
Post Article - request: { userId, title, description, content, hastag } - response: articleId
Upload File - request: { string base 64 } - response: file_url
Searching - request: { keyword } - response: [ article ]
Like - request: { userId, articleId } - response: boolean
Comment - request: { userId, articleId, content } - response: commentId | Comment
Follow - request: { currentUserId, toUserId } - response: boolean

## DB Schema

![alt text](./assets/db-schema.png)

## Design system

![alt text](./assets/architect.png)

# Getting started

## Installation
Expand Down Expand Up @@ -25,6 +67,7 @@ Run migration
---

## Running on docker

Build images

docker-compose build
Expand Down
Binary file added assets/architect.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/db-schema.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/func-non_func requirements.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
30 changes: 22 additions & 8 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ services:
- rabbitmq
- redis
env_file:
- .env
environment:
- NODE_ENV=staging
- DATABASE_HOST=host.docker.internal
- .env.docker
networks:
- social-network

social-api-2:
build:
context: .
Expand All @@ -31,10 +31,9 @@ services:
- rabbitmq
- redis
env_file:
- .env
environment:
- NODE_ENV=staging
- DATABASE_HOST=host.docker.internal
- .env.docker
networks:
- social-network

db:
container_name: social-postgres
Expand All @@ -46,17 +45,26 @@ services:
- /data/postgres/
ports:
- 5432:5432
networks:
- social-network

rabbitmq:
container_name: social-rabbitmq
image: rabbitmq:3-management
ports:
- "15672:15672"
- "5672:5672"
networks:
- social-network

redis:
container_name: social-redis
image: "redis:alpine"
ports:
- "6379:6379"
networks:
- social-network

nginx:
container_name: social-nginx
build: ./nginx
Expand All @@ -65,3 +73,9 @@ services:
depends_on:
- social-api-1
- social-api-2
networks:
- social-network

networks:
social-network:
driver: bridge
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,13 @@
"@nestjs/config": "^2.2.0",
"@nestjs/core": "^7.0.5",
"@nestjs/cqrs": "^9.0.1",
"@nestjs/microservices": "7.2",
"@nestjs/platform-express": "^7.0.5",
"@nestjs/swagger": "^4.4.0",
"@nestjs/testing": "^7.0.5",
"@nestjs/typeorm": "^7.0.0",
"@nestjs/websockets": "^7.0.5",
"amqp-connection-manager": "^4.1.14",
"amqplib": "^0.10.3",
"argon2": "^0.26.2",
"aws-sdk": "^2.1301.0",
Expand Down
9 changes: 0 additions & 9 deletions src/app.controller.ts

This file was deleted.

44 changes: 4 additions & 40 deletions src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,60 +1,24 @@
import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { TypeOrmModule } from "@nestjs/typeorm";
import * as dotenv from "dotenv";
import { AppController } from "./app.controller";

import { ArticleModule } from "./article/article.module";
import { READ_CONNECTION, WRITE_CONNECTION } from "./config";
import { DatabaseModule } from "./database/database.module";
import { MediaModule } from "./media/media.module";
import { ProfileModule } from "./profile/profile.module";
import { TagModule } from "./tag/tag.module";
import { UserModule } from "./user/user.module";

dotenv.config();

const defaultOptions = {
type: process.env.DATABASE_ENGINE,
host: process.env.DATABASE_HOST,
port: parseInt(process.env.DATABASE_PORT),
username: process.env.DATABASE_USERNAME,
password: process.env.DATABASE_PASSWORD,
entities: [__dirname + process.env.TYPEORM_ENTITIES],
migrations: [__dirname + process.env.TYPEORM_MIGRATIONS],
logging: process.env.TYPEORM_LOGGING === "true",
synchronize: process.env.TYPEORM_SYNCHRONIZE === "true",
migrationsRun: process.env.TYPEORM_MIGRATION_RUN === "true",
migrationsTableName: "migrations",
cli: {
migrationsDir: process.env.TYPEORM_MIGRATIONS_DIR,
},
};

@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
TypeOrmModule.forRootAsync({
name: WRITE_CONNECTION,
useFactory: () => ({
...defaultOptions,
type: "postgres",
database: process.env.WRITE_DATABASE_NAME,
}),
}),
TypeOrmModule.forRootAsync({
name: READ_CONNECTION,
useFactory: () => ({
...defaultOptions,
type: "postgres",
database: process.env.READ_DATABASE_NAME,
}),
}),
DatabaseModule,
ArticleModule,
UserModule,
ProfileModule,
TagModule,
MediaModule,
],
controllers: [AppController],
controllers: [],
providers: [],
})
export class ApplicationModule {}
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { Module } from "@nestjs/common";
import { CqrsModule } from "@nestjs/cqrs";
import { TypeOrmModule } from "@nestjs/typeorm";
import { CommandHandlers } from ".";
import { WRITE_CONNECTION } from "../../config";
import { FollowsEntity } from "../../profile/core/entities/follows.entity";
import { RabbitMqModule } from "../../rabbitmq/rabbitmq.module";
import { UserEntity } from "../../user/core";
import { UserModule } from "../../user/user.module";
import { ArticleEntity, BlockEntity } from "../core";
import { CommentEntity } from "../core/entities/comment.entity";

import { CommandHandlers } from "./index";
import { WRITE_CONNECTION } from "../../../configs";
import { FollowsEntity } from "../../../profile/core/entities/follows.entity";
import { RabbitMqModule } from "../../../rabbitmq/rabbitmq.module";
import { UserEntity } from "../../../user/core";
import { UserModule } from "../../../user/user.module";
import { ArticleEntity, BlockEntity } from "../../core";
import { CommentEntity } from "../../core/entities/comment.entity";
import { ArticleService } from "../services/article.service";

@Module({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { HttpException, HttpStatus } from "@nestjs/common";
import { HttpException, HttpStatus, Inject } from "@nestjs/common";
import { CommandHandler, ICommandHandler } from "@nestjs/cqrs";
import { ClientProxy } from "@nestjs/microservices";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { WRITE_CONNECTION } from "../../../config";
import { PublisherService } from "../../../rabbitmq/publisher.service";
import { ARTICLE_QUEUE } from "../../../rabbitmq/rabbitmq.constants";
import { ArticleEntity } from "../../core/entities/article.entity";
import { MessageType } from "../../core/enums/article.enum";
import { ArticleRO } from "../../core/interfaces/article.interface";

import { ARTICLE_RMQ_CLIENT, WRITE_CONNECTION } from "../../../../configs";
import { ArticleEntity } from "../../../core/entities/article.entity";
import { MessageCmd } from "../../../core/enums/article.enum";
import {
ArticleRO,
IPayloadArticleCreated,
} from "../../../core/interfaces/article.interface";
import { ArticleService } from "../../services/article.service";
import { CreateArticleCommand } from "../impl";

Expand All @@ -18,10 +21,13 @@ export class CreateArticleCommandHandler
constructor(
@InjectRepository(ArticleEntity, WRITE_CONNECTION)
private readonly articleRepository: Repository<ArticleEntity>,
@Inject(ARTICLE_RMQ_CLIENT)
private readonly articleRmqClient: ClientProxy,

private readonly articleService: ArticleService,
private readonly publisher: PublisherService
) {}
private readonly articleService: ArticleService
) {
this.articleRmqClient.connect();
}

async execute({
userId,
Expand All @@ -39,10 +45,10 @@ export class CreateArticleCommandHandler
);

if (article) {
this.publisher.publish(ARTICLE_QUEUE, {
type: MessageType.ARTICLE_CREATED,
payload: { article },
});
this.articleRmqClient.emit<any, IPayloadArticleCreated>(
{ cmd: MessageCmd.ARTICLE_CREATED },
{ article }
);
}

return {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { HttpException, HttpStatus } from "@nestjs/common";
import { HttpException, HttpStatus, Inject } from "@nestjs/common";
import { CommandHandler, ICommandHandler } from "@nestjs/cqrs";
import { ClientProxy } from "@nestjs/microservices";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { WRITE_CONNECTION } from "../../../config";
import { PublisherService } from "../../../rabbitmq/publisher.service";
import { ARTICLE_QUEUE } from "../../../rabbitmq/rabbitmq.constants";
import { UserEntity } from "../../../user/core/entities/user.entity";
import { ArticleEntity } from "../../core/entities/article.entity";
import { CommentEntity } from "../../core/entities/comment.entity";
import { MessageType } from "../../core/enums/article.enum";
import { CommentRO } from "../../core/interfaces/article.interface";

import { ARTICLE_RMQ_CLIENT, WRITE_CONNECTION } from "../../../../configs";
import { UserEntity } from "../../../../user/core/entities";
import { ArticleEntity, CommentEntity } from "../../../core/entities";
import { MessageCmd } from "../../../core/enums";
import { CommentRO, IPayloadCommentCreated } from "../../../core/interfaces";
import { ArticleService } from "../../services/article.service";
import { CreateCommentCommand } from "../impl";

Expand All @@ -24,10 +23,13 @@ export class CreateCommentCommandHandler
private readonly userRepository: Repository<UserEntity>,
@InjectRepository(CommentEntity, WRITE_CONNECTION)
private readonly commentRepository: Repository<CommentEntity>,
@Inject(ARTICLE_RMQ_CLIENT)
private readonly articleRmqClient: ClientProxy,

private readonly articleService: ArticleService,
private readonly publisher: PublisherService
) {}
private readonly articleService: ArticleService
) {
this.articleRmqClient.connect();
}

async execute({
userId,
Expand Down Expand Up @@ -55,10 +57,10 @@ export class CreateCommentCommandHandler
await this.commentRepository.save(comment);

if (comment) {
this.publisher.publish(ARTICLE_QUEUE, {
type: MessageType.COMMENT_CREATED,
payload: { comment },
});
this.articleRmqClient.emit<any, IPayloadCommentCreated>(
{ cmd: MessageCmd.COMMENT_CREATED },
{ comment }
);
}

const commentRO = this.articleService.buildCommentRO(comment);
Expand Down
Loading